diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 309ab0a..c6d7249 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -261,11 +261,12 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p automation, including the OpenGL command-plan support counts for that upload stream, and is covered by `pano_cli_simulate_document_render_smoke`. -- `pano_cli plan-canvas-document-snapshot` exposes the metadata-only bridge from - live canvas state toward `pp_document::CanvasDocument`, including dimensions, - active layer/frame, layer visibility/opacity/alpha/blend metadata, frame - durations, and pending renderer payload-readback counts, and is covered by - `pano_cli_plan_canvas_document_snapshot_smoke`. +- `pano_cli plan-canvas-document-snapshot` exposes the bridge from live canvas + state toward `pp_document::CanvasDocument`, including dimensions, active + layer/frame, layer visibility/opacity/alpha/blend metadata, frame durations, + captured RGBA8 face payloads, and remaining renderer payload-readback counts, + and is covered by `pano_cli_plan_canvas_document_snapshot_smoke` plus the + payload-bearing snapshot smoke. - `pano_cli save-document-project` writes that pure document export to a PPI file and is covered by `pano_cli_save_document_project_roundtrip_smoke`, which inspects and loads the generated file. diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index cb0ecb1..50f27b7 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -11,7 +11,7 @@ and validation command. | Capability | Current Area | Target Owner | Required Tests | | --- | --- | --- | --- | -| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture, metadata-only live-canvas-to-`pp_document` snapshot projection with pending renderer payload-readback counts | +| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture, live-canvas-to-`pp_document` snapshot projection with captured RGBA8 payloads and pending renderer-readback counts | | Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke | | Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b4d9188..48ef861 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -465,6 +465,14 @@ agent or engineer to remove them without reconstructing context from chat. snapshot from retained `Canvas` dimensions, active layer/frame, layer visibility/opacity/alpha/blend metadata, and frame durations. Renderer-owned cube-face pixel payload readback remains open under DEBT-0010/DEBT-0036. +- 2026-06-05: DEBT-0010 was narrowed again. The canvas snapshot planner now + accepts captured RGBA8 face payloads and attaches them to + `pp_document::CanvasDocument`; `pano_cli plan-canvas-document-snapshot + --captured-face-payloads-per-layer` covers payload-bearing automation, and + `src/legacy_document_canvas_services.*` exposes an opt-in dirty-face payload + snapshot path backed by retained `Layer::snapshot()` readback. Live save, + export, and action-command adoption of that payload-bearing snapshot remains + open under DEBT-0010/DEBT-0013/DEBT-0036. ## Open Debt @@ -478,7 +486,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, and root CMake now exposes a focused `panopainter_platform_build_vcpkg_ui_core` target for the vcpkg-backed `pp_ui_core`/tinyxml2 boundary, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | | DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | -| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, OpenGL command-planner validation through CLI render automation, and metadata-only live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, but legacy save/action commands and renderer-owned cube-face pixel payload readback are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | +| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, and captured RGBA8 payload attachment to `pp_document`, but legacy save/action commands, full live payload-snapshot adoption, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, and reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, and retained Linux/WebGL CMake baseline metadata are reachable from package validation and root CMake package-readiness targets, but Windows AppX/APK/Linux/Apple/WebGL package outputs are still `blocked` because root CMake package targets do not exist yet | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | | DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, and decoded pixel attachment to `pp_document`, but full legacy PPI round-trip parity is not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index afd7cb1..c1cf107 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -665,10 +665,11 @@ bridge before the legacy `Canvas::clear` adapter continues. `pano_cli plan-canvas-document-snapshot` exposes the app-core projection from live canvas metadata into a pure `pp_document::CanvasDocument`, including dimensions, active layer/frame, layer visibility/opacity/alpha/blend metadata, -frame durations, and pending renderer payload-readback counts; the retained -`legacy_document_canvas_services` bridge now builds the same metadata-only -snapshot from live `Canvas` state while cube-face pixel payload extraction -remains under `DEBT-0010`/`DEBT-0036`. +frame durations, captured RGBA8 face payloads, and remaining renderer +payload-readback counts; the retained `legacy_document_canvas_services` bridge +now builds the same metadata snapshot from live `Canvas` state and has an +opt-in dirty-face payload snapshot path backed by retained `Layer::snapshot()` +readback while live save/export adoption remains under `DEBT-0010`/`DEBT-0036`. `pano_cli plan-image-import` exposes app-core planning for File > Import image route decisions, including wide equirectangular images, legacy vertical cube strips, regular transform-placement images, and invalid image dimensions; live @@ -2189,18 +2190,19 @@ Results: - `pp_app_core_document_canvas_tests` passed, covering clear-current-layer undo/dirty intent, no-canvas no-op behavior, and invalid clear color rejection, service dispatch color forwarding, no-op execution preservation, - invalid execution color rejection, and metadata-only canvas-to-`pp_document` + invalid execution color rejection, and canvas-to-`pp_document` snapshot projection with layer visibility, opacity, alpha-lock, blend mode, - frame duration, active layer/frame, default-name, no-canvas, bad blend, and - bad duration coverage. + frame duration, active layer/frame, captured RGBA8 face payload attachment, + default-name, no-canvas, bad blend, bad payload, and bad duration coverage. - `pano_cli_plan_canvas_clear_smoke`, `pano_cli_plan_canvas_clear_no_canvas_smoke`, and `pano_cli_plan_canvas_clear_rejects_bad_color` passed and expose toolbar canvas clear planning as JSON automation. - `pano_cli_plan_canvas_document_snapshot_smoke` and - `pano_cli_plan_canvas_document_snapshot_no_canvas` passed and expose the - metadata-only live-canvas-to-`pp_document` projection, including pending - renderer payload-readback counts, as JSON automation. + `pano_cli_plan_canvas_document_snapshot_payload_smoke` plus the no-canvas + rejection smoke passed and expose live-canvas-to-`pp_document` projection, + including captured-versus-pending renderer payload-readback counts, as JSON + automation. - `pp_app_core_document_import_tests` passed, covering wide equirectangular, legacy vertical cube strip, regular transform-placement, and invalid-dimension import route decisions, equirectangular service dispatch, transform import diff --git a/src/app_core/document_canvas.h b/src/app_core/document_canvas.h index 3af3332..2eb9b05 100644 --- a/src/app_core/document_canvas.h +++ b/src/app_core/document_canvas.h @@ -26,6 +26,16 @@ struct DocumentCanvasClearPlan { bool no_op = true; }; +struct DocumentCanvasFacePayloadInput { + std::uint32_t frame_index = 0; + std::uint32_t face_index = 0; + std::uint32_t x = 0; + std::uint32_t y = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::span rgba8; +}; + struct DocumentCanvasLayerSnapshotInput { std::string_view name; bool visible = true; @@ -34,6 +44,7 @@ struct DocumentCanvasLayerSnapshotInput { int blend_mode = 0; std::span frame_durations_ms; std::size_t pending_face_payloads = 0; + std::span captured_face_payloads; }; struct DocumentCanvasSnapshotInput { @@ -50,7 +61,8 @@ struct DocumentCanvasSnapshotResult { std::size_t layer_count = 0; std::size_t frame_count = 0; std::size_t pending_face_payloads = 0; - bool metadata_only = true; + std::size_t captured_face_payloads = 0; + bool metadata_only = false; bool requires_renderer_payload_readback = false; }; @@ -87,9 +99,11 @@ public: std::size_t frame_count = 1U; std::size_t pending_face_payloads = 0U; + std::size_t captured_face_payloads = 0U; for (const auto& layer : input.layers) { frame_count = std::max(frame_count, layer.frame_durations_ms.size()); pending_face_payloads += layer.pending_face_payloads; + captured_face_payloads += layer.captured_face_payloads.size(); } if (input.active_layer_index >= input.layers.size()) { @@ -161,6 +175,26 @@ public: return pp::foundation::Result::failure(document.status()); } + for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) { + for (const auto& payload : input.layers[layer_index].captured_face_payloads) { + pp::document::LayerFacePixels pixels { + .face_index = payload.face_index, + .x = payload.x, + .y = payload.y, + .width = payload.width, + .height = payload.height, + .rgba8 = std::vector(payload.rgba8.begin(), payload.rgba8.end()), + }; + const auto payload_status = document.value().set_layer_frame_face_pixels( + layer_index, + payload.frame_index, + std::move(pixels)); + if (!payload_status.ok()) { + return pp::foundation::Result::failure(payload_status); + } + } + } + auto active_status = document.value().set_active_layer(input.active_layer_index); if (!active_status.ok()) { return pp::foundation::Result::failure(active_status); @@ -176,8 +210,9 @@ public: .layer_count = input.layers.size(), .frame_count = frame_count, .pending_face_payloads = pending_face_payloads, - .metadata_only = true, - .requires_renderer_payload_readback = pending_face_payloads > 0U, + .captured_face_payloads = captured_face_payloads, + .metadata_only = captured_face_payloads == 0U, + .requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads, }); } diff --git a/src/legacy_document_canvas_services.cpp b/src/legacy_document_canvas_services.cpp index 8bfb9a2..dd3adbd 100644 --- a/src/legacy_document_canvas_services.cpp +++ b/src/legacy_document_canvas_services.cpp @@ -7,6 +7,7 @@ #include "legacy_history_services.h" #include +#include #include #include #include @@ -79,6 +80,55 @@ private: return static_cast(frame_count) * pp::document::cube_face_count; } +[[nodiscard]] std::uint32_t legacy_nonnegative_u32(float value) noexcept +{ + if (value <= 0.0F) { + return 0U; + } + return static_cast(value); +} + +struct LegacyLayerPayloadStorage { + std::vector> bytes; + std::vector payloads; +}; + +void append_legacy_layer_payloads( + Layer& layer, + int frame_index, + LegacyLayerPayloadStorage& storage) +{ + auto snapshot = layer.snapshot(frame_index); + for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) { + if (!snapshot.m_dirty_face[face_index] || snapshot.image[face_index] == nullptr) { + continue; + } + + const auto& box = snapshot.m_dirty_box[face_index]; + const auto x = legacy_nonnegative_u32(box.x); + const auto y = legacy_nonnegative_u32(box.y); + const auto width = legacy_nonnegative_u32(box.z - box.x); + const auto height = legacy_nonnegative_u32(box.w - box.y); + if (width == 0U || height == 0U) { + continue; + } + + const auto byte_count = static_cast(width) * static_cast(height) + * pp::document::rgba8_components; + storage.bytes.push_back(std::vector(byte_count)); + std::memcpy(storage.bytes.back().data(), snapshot.image[face_index].get(), byte_count); + storage.payloads.push_back(pp::app::DocumentCanvasFacePayloadInput { + .frame_index = static_cast(std::max(frame_index, 0)), + .face_index = face_index, + .x = x, + .y = y, + .width = width, + .height = height, + .rgba8 = std::span(storage.bytes.back().data(), storage.bytes.back().size()), + }); + } +} + } // namespace bool legacy_document_canvas_available(const App& app) noexcept @@ -141,6 +191,67 @@ pp::foundation::Result capture_legacy_can return capture_legacy_canvas_document_snapshot(*app.canvas->m_canvas); } +pp::foundation::Result capture_legacy_canvas_document_payload_snapshot( + Canvas& canvas) +{ + std::vector layer_names; + std::vector> layer_frame_durations; + std::vector layer_payload_storage; + std::vector layers; + layer_names.reserve(canvas.m_layers.size()); + layer_frame_durations.reserve(canvas.m_layers.size()); + layer_payload_storage.reserve(canvas.m_layers.size()); + layers.reserve(canvas.m_layers.size()); + + for (const auto& legacy_layer : canvas.m_layers) { + if (!legacy_layer) { + continue; + } + + layer_names.push_back(legacy_layer->m_name); + auto& durations = layer_frame_durations.emplace_back(); + auto& payload_storage = layer_payload_storage.emplace_back(); + const auto frame_count = legacy_layer->frames_count(); + durations.reserve(static_cast(std::max(frame_count, 0))); + for (int frame_index = 0; frame_index < frame_count; ++frame_index) { + durations.push_back(legacy_u32_or_zero(legacy_layer->frame_duration(frame_index))); + append_legacy_layer_payloads(*legacy_layer, frame_index, payload_storage); + } + + layers.push_back(pp::app::DocumentCanvasLayerSnapshotInput { + .name = layer_names.back(), + .visible = legacy_layer->m_visible, + .alpha_locked = legacy_layer->m_alpha_locked, + .opacity = legacy_layer->m_opacity, + .blend_mode = legacy_layer->m_blend_mode, + .frame_durations_ms = std::span(durations), + .pending_face_payloads = payload_storage.payloads.size(), + .captured_face_payloads = std::span( + payload_storage.payloads), + }); + } + + return pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { + .has_canvas = true, + .width = legacy_u32_or_zero(canvas.m_width), + .height = legacy_u32_or_zero(canvas.m_height), + .active_layer_index = static_cast(std::max(canvas.m_current_layer_idx, 0)), + .active_frame_index = static_cast(std::max(canvas.m_anim_frame, 0)), + .layers = std::span(layers), + }); +} + +pp::foundation::Result capture_legacy_canvas_document_payload_snapshot( + App& app) +{ + if (!legacy_document_canvas_available(app)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("legacy document canvas payload snapshot requires a canvas")); + } + + return capture_legacy_canvas_document_payload_snapshot(*app.canvas->m_canvas); +} + pp::foundation::Status execute_legacy_document_canvas_clear_plan( App& app, const pp::app::DocumentCanvasClearPlan& plan) diff --git a/src/legacy_document_canvas_services.h b/src/legacy_document_canvas_services.h index a09d58a..f3eb276 100644 --- a/src/legacy_document_canvas_services.h +++ b/src/legacy_document_canvas_services.h @@ -14,6 +14,10 @@ namespace pp::panopainter { capture_legacy_canvas_document_snapshot(const Canvas& canvas); [[nodiscard]] pp::foundation::Result capture_legacy_canvas_document_snapshot(const App& app); +[[nodiscard]] pp::foundation::Result +capture_legacy_canvas_document_payload_snapshot(Canvas& canvas); +[[nodiscard]] pp::foundation::Result +capture_legacy_canvas_document_payload_snapshot(App& app); [[nodiscard]] pp::foundation::Status execute_legacy_document_canvas_clear_plan( App& app, const pp::app::DocumentCanvasClearPlan& plan); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23f4ad0..49a4bf6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1476,6 +1476,12 @@ if(TARGET pano_cli) LABELS "app;document;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"width\":128.*\"height\":64.*\"layers\":3.*\"frames\":2.*\"activeLayer\":2.*\"activeFrame\":1.*\"activeLayerName\":\"Layer 3\".*\"activeLayerOpacity\":0.5.*\"activeLayerBlend\":\"overlay\".*\"activeLayerAlphaLocked\":true.*\"pendingFacePayloads\":18.*\"metadataOnly\":true.*\"requiresRendererPayloadReadback\":true.*\"documentFacePayloads\":0") + add_test(NAME pano_cli_plan_canvas_document_snapshot_payload_smoke + COMMAND pano_cli plan-canvas-document-snapshot --width 128 --height 64 --layers 2 --frames 2 --current-layer 1 --current-frame 1 --pending-face-payloads-per-layer 2 --captured-face-payloads-per-layer 2) + set_tests_properties(pano_cli_plan_canvas_document_snapshot_payload_smoke PROPERTIES + LABELS "app;document;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"layers\":2.*\"frames\":2.*\"activeLayer\":1.*\"activeFrame\":1.*\"pendingFacePayloads\":4.*\"capturedFacePayloads\":4.*\"metadataOnly\":false.*\"requiresRendererPayloadReadback\":false.*\"documentFacePayloads\":4") + add_test(NAME pano_cli_plan_canvas_document_snapshot_no_canvas COMMAND pano_cli plan-canvas-document-snapshot --no-canvas) set_tests_properties(pano_cli_plan_canvas_document_snapshot_no_canvas PROPERTIES diff --git a/tests/app_core/document_canvas_tests.cpp b/tests/app_core/document_canvas_tests.cpp index 6765a0f..aff4583 100644 --- a/tests/app_core/document_canvas_tests.cpp +++ b/tests/app_core/document_canvas_tests.cpp @@ -119,6 +119,56 @@ void snapshot_plan_defaults_empty_names_and_frames(pp::tests::Harness& harness) PP_EXPECT(harness, result.value().document.frames()[0].duration_ms == 100U); } +void snapshot_plan_attaches_captured_face_payloads(pp::tests::Harness& harness) +{ + const std::uint32_t frames[] { 100U }; + const std::uint8_t rgba[] { + 255U, 0U, 0U, 255U, + 0U, 255U, 0U, 255U, + }; + const pp::app::DocumentCanvasFacePayloadInput payloads[] { + { + .frame_index = 0U, + .face_index = 2U, + .x = 3U, + .y = 4U, + .width = 2U, + .height = 1U, + .rgba8 = std::span(rgba), + }, + }; + const pp::app::DocumentCanvasLayerSnapshotInput layers[] { + { + .name = "Paint", + .frame_durations_ms = std::span(frames), + .pending_face_payloads = 1U, + .captured_face_payloads = std::span(payloads), + }, + }; + + const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { + .has_canvas = true, + .width = 16U, + .height = 8U, + .layers = std::span(layers), + }); + + PP_EXPECT(harness, result); + if (!result) { + return; + } + + PP_EXPECT(harness, result.value().pending_face_payloads == 1U); + PP_EXPECT(harness, result.value().captured_face_payloads == 1U); + PP_EXPECT(harness, !result.value().metadata_only); + PP_EXPECT(harness, !result.value().requires_renderer_payload_readback); + PP_EXPECT(harness, result.value().document.face_pixel_payload_count() == 1U); + PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].face_index == 2U); + PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].x == 3U); + PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[4] == 0U); + PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[5] == 255U); +} + void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) { const std::uint32_t frames[] { 100U }; @@ -168,11 +218,38 @@ void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) .height = 8U, .layers = std::span(bad_duration_layers), }); + const std::uint8_t bad_rgba[] { 1U, 2U, 3U }; + const pp::app::DocumentCanvasFacePayloadInput bad_payloads[] { + { + .frame_index = 0U, + .face_index = 0U, + .x = 0U, + .y = 0U, + .width = 1U, + .height = 1U, + .rgba8 = std::span(bad_rgba), + }, + }; + const pp::app::DocumentCanvasLayerSnapshotInput bad_payload_layers[] { + { + .name = "Layer", + .frame_durations_ms = std::span(frames), + .pending_face_payloads = 1U, + .captured_face_payloads = std::span(bad_payloads), + }, + }; + const auto bad_payload = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { + .has_canvas = true, + .width = 16U, + .height = 8U, + .layers = std::span(bad_payload_layers), + }); PP_EXPECT(harness, !no_canvas); PP_EXPECT(harness, !bad_layer); PP_EXPECT(harness, !bad_blend); PP_EXPECT(harness, !bad_duration); + PP_EXPECT(harness, !bad_payload); } void clear_plan_records_legacy_canvas_effects(pp::tests::Harness& harness) @@ -270,6 +347,7 @@ int main() pp::tests::Harness harness; harness.run("snapshot plan projects canvas metadata", snapshot_plan_projects_canvas_metadata); harness.run("snapshot plan defaults empty names and frames", snapshot_plan_defaults_empty_names_and_frames); + harness.run("snapshot plan attaches captured face payloads", snapshot_plan_attaches_captured_face_payloads); harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state); harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects); harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 2e99e39..30d2d94 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -415,6 +415,7 @@ struct PlanCanvasDocumentSnapshotArgs { float opacity = 0.75F; int blend_mode = 4; std::uint32_t pending_face_payloads_per_layer = pp::document::cube_face_count; + std::uint32_t captured_face_payloads_per_layer = 0; }; struct PlanImageImportArgs { @@ -2407,7 +2408,7 @@ void print_help() << " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n" << " plan-about-menu --command help|about|news|crash|performance [--version-major N] [--version-minor N] [--version-fix N] [--no-diagnostics] [--no-canvas]\n" << " plan-canvas-clear [--no-canvas] [--r N] [--g N] [--b N] [--a N]\n" - << " plan-canvas-document-snapshot [--no-canvas] [--width N] [--height N] [--layers N] [--frames N] [--frame-duration-ms N] [--current-layer N] [--current-frame N] [--hidden-layer N] [--alpha-locked-layer N] [--opacity N] [--blend-mode N] [--pending-face-payloads-per-layer N]\n" + << " plan-canvas-document-snapshot [--no-canvas] [--width N] [--height N] [--layers N] [--frames N] [--frame-duration-ms N] [--current-layer N] [--current-frame N] [--hidden-layer N] [--alpha-locked-layer N] [--opacity N] [--blend-mode N] [--pending-face-payloads-per-layer N] [--captured-face-payloads-per-layer N]\n" << " plan-image-import --width N --height N\n" << " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n" << " plan-layer-rename --old-name NAME --new-name NAME\n" @@ -5867,7 +5868,7 @@ pp::foundation::Status parse_plan_canvas_document_snapshot_args( } else if (key == "--width" || key == "--height" || key == "--layers" || key == "--frames" || key == "--frame-duration-ms" || key == "--current-layer" || key == "--current-frame" || key == "--hidden-layer" || key == "--alpha-locked-layer" - || key == "--pending-face-payloads-per-layer") { + || key == "--pending-face-payloads-per-layer" || key == "--captured-face-payloads-per-layer") { if (i + 1 >= argc) { return pp::foundation::Status::invalid_argument("missing value for option"); } @@ -5893,8 +5894,10 @@ pp::foundation::Status parse_plan_canvas_document_snapshot_args( args.hidden_layer = value.value(); } else if (key == "--alpha-locked-layer") { args.alpha_locked_layer = value.value(); - } else { + } else if (key == "--pending-face-payloads-per-layer") { args.pending_face_payloads_per_layer = value.value(); + } else { + args.captured_face_payloads_per_layer = value.value(); } } else if (key == "--opacity") { if (i + 1 >= argc) { @@ -5933,11 +5936,36 @@ int plan_canvas_document_snapshot(int argc, char** argv) std::vector frame_durations(args.frames, args.frame_duration_ms); std::vector layer_names; + std::vector> payload_bytes; + std::vector> captured_payloads; std::vector layers; layer_names.reserve(args.layers); + payload_bytes.reserve(static_cast(args.layers) * args.captured_face_payloads_per_layer); + captured_payloads.reserve(args.layers); layers.reserve(args.layers); for (std::uint32_t layer_index = 0; layer_index < args.layers; ++layer_index) { layer_names.push_back("Layer " + std::to_string(layer_index + 1U)); + captured_payloads.push_back({}); + auto& layer_payloads = captured_payloads.back(); + layer_payloads.reserve(args.captured_face_payloads_per_layer); + for (std::uint32_t payload_index = 0; payload_index < args.captured_face_payloads_per_layer; ++payload_index) { + const auto face_index = payload_index % pp::document::cube_face_count; + payload_bytes.push_back(std::vector { + static_cast((layer_index + 1U) & 0xFFU), + static_cast(face_index & 0xFFU), + static_cast((payload_index + 1U) & 0xFFU), + 255U, + }); + layer_payloads.push_back(pp::app::DocumentCanvasFacePayloadInput { + .frame_index = args.frames == 0U ? 0U : args.current_frame % args.frames, + .face_index = face_index, + .x = args.width == 0U ? 0U : payload_index % args.width, + .y = args.height == 0U ? 0U : payload_index % args.height, + .width = 1U, + .height = 1U, + .rgba8 = std::span(payload_bytes.back().data(), payload_bytes.back().size()), + }); + } layers.push_back(pp::app::DocumentCanvasLayerSnapshotInput { .name = layer_names.back(), .visible = layer_index != args.hidden_layer, @@ -5946,6 +5974,7 @@ int plan_canvas_document_snapshot(int argc, char** argv) .blend_mode = layer_index == args.current_layer ? args.blend_mode : 0, .frame_durations_ms = std::span(frame_durations), .pending_face_payloads = args.pending_face_payloads_per_layer, + .captured_face_payloads = std::span(layer_payloads), }); } @@ -5984,6 +6013,7 @@ int plan_canvas_document_snapshot(int argc, char** argv) << ",\"activeLayerBlend\":\"" << pp::paint::blend_mode_name(active_layer.blend_mode) << "\",\"activeLayerAlphaLocked\":" << json_bool(active_layer.alpha_locked) << ",\"pendingFacePayloads\":" << value.pending_face_payloads + << ",\"capturedFacePayloads\":" << value.captured_face_payloads << ",\"metadataOnly\":" << json_bool(value.metadata_only) << ",\"requiresRendererPayloadReadback\":" << json_bool(value.requires_renderer_payload_readback)