Project legacy canvas metadata into documents
This commit is contained in:
@@ -291,6 +291,7 @@ target_include_directories(pp_app_core
|
|||||||
target_link_libraries(pp_app_core
|
target_link_libraries(pp_app_core
|
||||||
PUBLIC
|
PUBLIC
|
||||||
pp_foundation
|
pp_foundation
|
||||||
|
pp_document
|
||||||
pp_project_options
|
pp_project_options
|
||||||
PRIVATE
|
PRIVATE
|
||||||
pp_project_warnings)
|
pp_project_warnings)
|
||||||
|
|||||||
@@ -261,6 +261,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
|
|||||||
automation, including the OpenGL command-plan support counts for that upload
|
automation, including the OpenGL command-plan support counts for that upload
|
||||||
stream, and is covered by
|
stream, and is covered by
|
||||||
`pano_cli_simulate_document_render_smoke`.
|
`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 save-document-project` writes that pure document export to a PPI
|
- `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`,
|
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
|
||||||
which inspects and loads the generated file.
|
which inspects and loads the generated file.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ and validation command.
|
|||||||
|
|
||||||
| Capability | Current Area | Target Owner | Required Tests |
|
| 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 |
|
| 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 |
|
||||||
| 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 |
|
| 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 |
|
| 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 |
|
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
|
||||||
@@ -46,7 +46,7 @@ and validation command.
|
|||||||
| Capability | Current Area | Target Owner | Required Tests |
|
| Capability | Current Area | Target Owner | Required Tests |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Layer rename/add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document`, `pp_app_core` | Rename and operation planning, service-dispatch, no-op preservation, undo/redo invariant tests |
|
| Layer rename/add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document`, `pp_app_core` | Rename and operation planning, service-dispatch, no-op preservation, undo/redo invariant tests |
|
||||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_app_core`, `pp_paint_renderer` | Metadata planning, service-dispatch, CPU model and render golden |
|
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_app_core`, `pp_paint_renderer` | Metadata planning, service-dispatch, live-canvas-to-`pp_document` snapshot projection, CPU model and render golden |
|
||||||
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
||||||
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
||||||
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
|
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
|
||||||
|
|||||||
@@ -458,6 +458,13 @@ agent or engineer to remove them without reconstructing context from chat.
|
|||||||
commands through `pp_renderer_gl::plan_recorded_render_commands`, proving
|
commands through `pp_renderer_gl::plan_recorded_render_commands`, proving
|
||||||
the six texture uploads and transitions are accepted by the current OpenGL
|
the six texture uploads and transitions are accepted by the current OpenGL
|
||||||
command planner while live legacy GL execution remains retained.
|
command planner while live legacy GL execution remains retained.
|
||||||
|
- 2026-06-05: DEBT-0010 was narrowed again. `pp_app_core` now owns a tested
|
||||||
|
metadata-only live-canvas-to-`pp_document::CanvasDocument` snapshot planner,
|
||||||
|
`pano_cli plan-canvas-document-snapshot` exposes the projection as JSON
|
||||||
|
automation, and `src/legacy_document_canvas_services.*` can build the same
|
||||||
|
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.
|
||||||
|
|
||||||
## Open Debt
|
## Open Debt
|
||||||
|
|
||||||
@@ -471,7 +478,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-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-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-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, and OpenGL command-planner validation through CLI render automation, but it is not yet wired to legacy `Canvas`, legacy save, or legacy action commands | 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`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_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, 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-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-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-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 |
|
| 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 |
|
||||||
|
|||||||
@@ -662,6 +662,13 @@ clear-current-layer command, including clear color validation, no-canvas
|
|||||||
handling, undo recording intent, and dirty-state intent; live toolbar execution
|
handling, undo recording intent, and dirty-state intent; live toolbar execution
|
||||||
and Layer menu clear now dispatch through the shared app-shell document-canvas
|
and Layer menu clear now dispatch through the shared app-shell document-canvas
|
||||||
bridge before the legacy `Canvas::clear` adapter continues.
|
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`.
|
||||||
`pano_cli plan-image-import` exposes app-core planning for File > Import image
|
`pano_cli plan-image-import` exposes app-core planning for File > Import image
|
||||||
route decisions, including wide equirectangular images, legacy vertical cube
|
route decisions, including wide equirectangular images, legacy vertical cube
|
||||||
strips, regular transform-placement images, and invalid image dimensions; live
|
strips, regular transform-placement images, and invalid image dimensions; live
|
||||||
@@ -2182,11 +2189,18 @@ Results:
|
|||||||
- `pp_app_core_document_canvas_tests` passed, covering clear-current-layer
|
- `pp_app_core_document_canvas_tests` passed, covering clear-current-layer
|
||||||
undo/dirty intent, no-canvas no-op behavior, and invalid clear color
|
undo/dirty intent, no-canvas no-op behavior, and invalid clear color
|
||||||
rejection, service dispatch color forwarding, no-op execution preservation,
|
rejection, service dispatch color forwarding, no-op execution preservation,
|
||||||
and invalid execution color rejection.
|
invalid execution color rejection, and metadata-only 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.
|
||||||
- `pano_cli_plan_canvas_clear_smoke`,
|
- `pano_cli_plan_canvas_clear_smoke`,
|
||||||
`pano_cli_plan_canvas_clear_no_canvas_smoke`, and
|
`pano_cli_plan_canvas_clear_no_canvas_smoke`, and
|
||||||
`pano_cli_plan_canvas_clear_rejects_bad_color` passed and expose toolbar
|
`pano_cli_plan_canvas_clear_rejects_bad_color` passed and expose toolbar
|
||||||
canvas clear planning as JSON automation.
|
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.
|
||||||
- `pp_app_core_document_import_tests` passed, covering wide equirectangular,
|
- `pp_app_core_document_import_tests` passed, covering wide equirectangular,
|
||||||
legacy vertical cube strip, regular transform-placement, and invalid-dimension
|
legacy vertical cube strip, regular transform-placement, and invalid-dimension
|
||||||
import route decisions, equirectangular service dispatch, transform import
|
import route decisions, equirectangular service dispatch, transform import
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "document/document.h"
|
||||||
#include "foundation/result.h"
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace pp::app {
|
namespace pp::app {
|
||||||
|
|
||||||
@@ -17,6 +26,34 @@ struct DocumentCanvasClearPlan {
|
|||||||
bool no_op = true;
|
bool no_op = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DocumentCanvasLayerSnapshotInput {
|
||||||
|
std::string_view name;
|
||||||
|
bool visible = true;
|
||||||
|
bool alpha_locked = false;
|
||||||
|
float opacity = 1.0F;
|
||||||
|
int blend_mode = 0;
|
||||||
|
std::span<const std::uint32_t> frame_durations_ms;
|
||||||
|
std::size_t pending_face_payloads = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentCanvasSnapshotInput {
|
||||||
|
bool has_canvas = true;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::size_t active_layer_index = 0;
|
||||||
|
std::size_t active_frame_index = 0;
|
||||||
|
std::span<const DocumentCanvasLayerSnapshotInput> layers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentCanvasSnapshotResult {
|
||||||
|
pp::document::CanvasDocument document;
|
||||||
|
std::size_t layer_count = 0;
|
||||||
|
std::size_t frame_count = 0;
|
||||||
|
std::size_t pending_face_payloads = 0;
|
||||||
|
bool metadata_only = true;
|
||||||
|
bool requires_renderer_payload_readback = false;
|
||||||
|
};
|
||||||
|
|
||||||
class DocumentCanvasClearServices {
|
class DocumentCanvasClearServices {
|
||||||
public:
|
public:
|
||||||
virtual ~DocumentCanvasClearServices() = default;
|
virtual ~DocumentCanvasClearServices() = default;
|
||||||
@@ -35,6 +72,115 @@ public:
|
|||||||
return pp::foundation::Status::success();
|
return pp::foundation::Status::success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
|
||||||
|
DocumentCanvasSnapshotInput input)
|
||||||
|
{
|
||||||
|
if (!input.has_canvas) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.layers.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t frame_count = 1U;
|
||||||
|
std::size_t pending_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.active_layer_index >= input.layers.size()) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.active_frame_index >= frame_count) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pp::document::AnimationFrame> root_frames;
|
||||||
|
root_frames.reserve(frame_count);
|
||||||
|
for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
||||||
|
std::uint32_t duration_ms = 100U;
|
||||||
|
for (const auto& layer : input.layers) {
|
||||||
|
if (frame_index < layer.frame_durations_ms.size()) {
|
||||||
|
duration_ms = layer.frame_durations_ms[frame_index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> layer_names;
|
||||||
|
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
|
||||||
|
std::vector<pp::document::DocumentLayerConfig> layer_configs;
|
||||||
|
layer_names.reserve(input.layers.size());
|
||||||
|
layer_frames.reserve(input.layers.size());
|
||||||
|
layer_configs.reserve(input.layers.size());
|
||||||
|
|
||||||
|
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
||||||
|
const auto& layer = input.layers[layer_index];
|
||||||
|
if (layer.name.empty()) {
|
||||||
|
layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
|
||||||
|
} else {
|
||||||
|
layer_names.push_back(std::string(layer.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_frames.push_back({});
|
||||||
|
auto& frames = layer_frames.back();
|
||||||
|
frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
|
||||||
|
if (layer.frame_durations_ms.empty()) {
|
||||||
|
frames = root_frames;
|
||||||
|
} else {
|
||||||
|
for (const auto duration_ms : layer.frame_durations_ms) {
|
||||||
|
frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_configs.push_back(pp::document::DocumentLayerConfig {
|
||||||
|
.name = layer_names.back(),
|
||||||
|
.visible = layer.visible,
|
||||||
|
.alpha_locked = layer.alpha_locked,
|
||||||
|
.opacity = layer.opacity,
|
||||||
|
.blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
|
||||||
|
.frames = std::span<const pp::document::AnimationFrame>(frames),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
|
||||||
|
.width = input.width,
|
||||||
|
.height = input.height,
|
||||||
|
.layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
|
||||||
|
.frames = std::span<const pp::document::AnimationFrame>(root_frames),
|
||||||
|
});
|
||||||
|
if (!document) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto active_status = document.value().set_active_layer(input.active_layer_index);
|
||||||
|
if (!active_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
active_status = document.value().set_active_frame(input.active_frame_index);
|
||||||
|
if (!active_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
|
||||||
|
.document = std::move(document.value()),
|
||||||
|
.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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
||||||
bool has_canvas,
|
bool has_canvas,
|
||||||
float r = 0.0F,
|
float r = 0.0F,
|
||||||
|
|||||||
@@ -3,8 +3,15 @@
|
|||||||
#include "legacy_document_canvas_services.h"
|
#include "legacy_document_canvas_services.h"
|
||||||
|
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "canvas.h"
|
||||||
#include "legacy_history_services.h"
|
#include "legacy_history_services.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace pp::panopainter {
|
namespace pp::panopainter {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -58,6 +65,20 @@ private:
|
|||||||
App& app_;
|
App& app_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t legacy_u32_or_zero(int value) noexcept
|
||||||
|
{
|
||||||
|
return value <= 0 ? 0U : static_cast<std::uint32_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t pending_face_payload_count(const Layer& layer) noexcept
|
||||||
|
{
|
||||||
|
const auto frame_count = layer.frames_count();
|
||||||
|
if (frame_count <= 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return static_cast<std::size_t>(frame_count) * pp::document::cube_face_count;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool legacy_document_canvas_available(const App& app) noexcept
|
bool legacy_document_canvas_available(const App& app) noexcept
|
||||||
@@ -65,6 +86,61 @@ bool legacy_document_canvas_available(const App& app) noexcept
|
|||||||
return app.canvas != nullptr && app.canvas->m_canvas != nullptr;
|
return app.canvas != nullptr && app.canvas->m_canvas != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_snapshot(
|
||||||
|
const Canvas& canvas)
|
||||||
|
{
|
||||||
|
std::vector<std::string> layer_names;
|
||||||
|
std::vector<std::vector<std::uint32_t>> layer_frame_durations;
|
||||||
|
std::vector<pp::app::DocumentCanvasLayerSnapshotInput> layers;
|
||||||
|
layer_names.reserve(canvas.m_layers.size());
|
||||||
|
layer_frame_durations.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();
|
||||||
|
const auto frame_count = legacy_layer->frames_count();
|
||||||
|
durations.reserve(static_cast<std::size_t>(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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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<const std::uint32_t>(durations),
|
||||||
|
.pending_face_payloads = pending_face_payload_count(*legacy_layer),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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::size_t>(std::max(canvas.m_current_layer_idx, 0)),
|
||||||
|
.active_frame_index = static_cast<std::size_t>(std::max(canvas.m_anim_frame, 0)),
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_snapshot(
|
||||||
|
const App& app)
|
||||||
|
{
|
||||||
|
if (!legacy_document_canvas_available(app)) {
|
||||||
|
return pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("legacy document canvas snapshot requires a canvas"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return capture_legacy_canvas_document_snapshot(*app.canvas->m_canvas);
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Status execute_legacy_document_canvas_clear_plan(
|
pp::foundation::Status execute_legacy_document_canvas_clear_plan(
|
||||||
App& app,
|
App& app,
|
||||||
const pp::app::DocumentCanvasClearPlan& plan)
|
const pp::app::DocumentCanvasClearPlan& plan)
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
#include "foundation/result.h"
|
#include "foundation/result.h"
|
||||||
|
|
||||||
class App;
|
class App;
|
||||||
|
class Canvas;
|
||||||
|
|
||||||
namespace pp::panopainter {
|
namespace pp::panopainter {
|
||||||
|
|
||||||
[[nodiscard]] bool legacy_document_canvas_available(const App& app) noexcept;
|
[[nodiscard]] bool legacy_document_canvas_available(const App& app) noexcept;
|
||||||
|
[[nodiscard]] pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult>
|
||||||
|
capture_legacy_canvas_document_snapshot(const Canvas& canvas);
|
||||||
|
[[nodiscard]] pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult>
|
||||||
|
capture_legacy_canvas_document_snapshot(const App& app);
|
||||||
[[nodiscard]] pp::foundation::Status execute_legacy_document_canvas_clear_plan(
|
[[nodiscard]] pp::foundation::Status execute_legacy_document_canvas_clear_plan(
|
||||||
App& app,
|
App& app,
|
||||||
const pp::app::DocumentCanvasClearPlan& plan);
|
const pp::app::DocumentCanvasClearPlan& plan);
|
||||||
|
|||||||
@@ -1470,6 +1470,18 @@ if(TARGET pano_cli)
|
|||||||
LABELS "app;document;integration;desktop-fast;fuzz"
|
LABELS "app;document;integration;desktop-fast;fuzz"
|
||||||
WILL_FAIL TRUE)
|
WILL_FAIL TRUE)
|
||||||
|
|
||||||
|
add_test(NAME pano_cli_plan_canvas_document_snapshot_smoke
|
||||||
|
COMMAND pano_cli plan-canvas-document-snapshot --width 128 --height 64 --layers 3 --frames 2 --current-layer 2 --current-frame 1 --hidden-layer 0 --alpha-locked-layer 2 --opacity 0.5 --blend-mode 4 --pending-face-payloads-per-layer 6)
|
||||||
|
set_tests_properties(pano_cli_plan_canvas_document_snapshot_smoke PROPERTIES
|
||||||
|
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_no_canvas
|
||||||
|
COMMAND pano_cli plan-canvas-document-snapshot --no-canvas)
|
||||||
|
set_tests_properties(pano_cli_plan_canvas_document_snapshot_no_canvas PROPERTIES
|
||||||
|
LABELS "app;document;integration;desktop-fast;fuzz"
|
||||||
|
WILL_FAIL TRUE)
|
||||||
|
|
||||||
add_test(NAME pano_cli_plan_image_import_wide_equirect_smoke
|
add_test(NAME pano_cli_plan_image_import_wide_equirect_smoke
|
||||||
COMMAND pano_cli plan-image-import --width 4096 --height 2048)
|
COMMAND pano_cli plan-image-import --width 4096 --height 2048)
|
||||||
set_tests_properties(pano_cli_plan_image_import_wide_equirect_smoke PROPERTIES
|
set_tests_properties(pano_cli_plan_image_import_wide_equirect_smoke PROPERTIES
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "app_core/document_canvas.h"
|
#include "app_core/document_canvas.h"
|
||||||
#include "test_harness.h"
|
#include "test_harness.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -26,6 +28,153 @@ public:
|
|||||||
std::string call_order;
|
std::string call_order;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void snapshot_plan_projects_canvas_metadata(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const std::uint32_t base_frames[] { 120U, 240U };
|
||||||
|
const std::uint32_t paint_frames[] { 180U };
|
||||||
|
const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
|
||||||
|
{
|
||||||
|
.name = "Base",
|
||||||
|
.visible = false,
|
||||||
|
.alpha_locked = true,
|
||||||
|
.opacity = 0.5F,
|
||||||
|
.blend_mode = 2,
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(base_frames),
|
||||||
|
.pending_face_payloads = 6U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Paint",
|
||||||
|
.visible = true,
|
||||||
|
.alpha_locked = false,
|
||||||
|
.opacity = 0.75F,
|
||||||
|
.blend_mode = 4,
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(paint_frames),
|
||||||
|
.pending_face_payloads = 3U,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = true,
|
||||||
|
.width = 64U,
|
||||||
|
.height = 32U,
|
||||||
|
.active_layer_index = 1U,
|
||||||
|
.active_frame_index = 1U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(harness, result);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& value = result.value();
|
||||||
|
PP_EXPECT(harness, value.layer_count == 2U);
|
||||||
|
PP_EXPECT(harness, value.frame_count == 2U);
|
||||||
|
PP_EXPECT(harness, value.pending_face_payloads == 9U);
|
||||||
|
PP_EXPECT(harness, value.metadata_only);
|
||||||
|
PP_EXPECT(harness, value.requires_renderer_payload_readback);
|
||||||
|
PP_EXPECT(harness, value.document.width() == 64U);
|
||||||
|
PP_EXPECT(harness, value.document.height() == 32U);
|
||||||
|
PP_EXPECT(harness, value.document.active_layer_index() == 1U);
|
||||||
|
PP_EXPECT(harness, value.document.active_frame_index() == 1U);
|
||||||
|
PP_EXPECT(harness, value.document.layers().size() == 2U);
|
||||||
|
PP_EXPECT(harness, value.document.frames().size() == 2U);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[0].name == "Base");
|
||||||
|
PP_EXPECT(harness, !value.document.layers()[0].visible);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[0].alpha_locked);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[0].opacity == 0.5F);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[0].blend_mode == pp::paint::BlendMode::screen);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[0].frames[1].duration_ms == 240U);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[1].name == "Paint");
|
||||||
|
PP_EXPECT(harness, value.document.layers()[1].blend_mode == pp::paint::BlendMode::overlay);
|
||||||
|
PP_EXPECT(harness, value.document.layers()[1].frames.size() == 1U);
|
||||||
|
PP_EXPECT(harness, value.document.face_pixel_payload_count() == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void snapshot_plan_defaults_empty_names_and_frames(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
|
||||||
|
{
|
||||||
|
.name = "",
|
||||||
|
.frame_durations_ms = {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = true,
|
||||||
|
.width = 16U,
|
||||||
|
.height = 8U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(harness, result);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PP_EXPECT(harness, result.value().frame_count == 1U);
|
||||||
|
PP_EXPECT(harness, result.value().pending_face_payloads == 0U);
|
||||||
|
PP_EXPECT(harness, !result.value().requires_renderer_payload_readback);
|
||||||
|
PP_EXPECT(harness, result.value().document.layers()[0].name == "Layer 1");
|
||||||
|
PP_EXPECT(harness, result.value().document.frames()[0].duration_ms == 100U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const std::uint32_t frames[] { 100U };
|
||||||
|
const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
|
||||||
|
{
|
||||||
|
.name = "Layer",
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(frames),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto no_canvas = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = false,
|
||||||
|
.width = 16U,
|
||||||
|
.height = 8U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
const auto bad_layer = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = true,
|
||||||
|
.width = 16U,
|
||||||
|
.height = 8U,
|
||||||
|
.active_layer_index = 1U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
const pp::app::DocumentCanvasLayerSnapshotInput bad_blend_layers[] {
|
||||||
|
{
|
||||||
|
.name = "Layer",
|
||||||
|
.blend_mode = 64,
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(frames),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const auto bad_blend = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = true,
|
||||||
|
.width = 16U,
|
||||||
|
.height = 8U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(bad_blend_layers),
|
||||||
|
});
|
||||||
|
const std::uint32_t bad_frames[] { 0U };
|
||||||
|
const pp::app::DocumentCanvasLayerSnapshotInput bad_duration_layers[] {
|
||||||
|
{
|
||||||
|
.name = "Layer",
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(bad_frames),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const auto bad_duration = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = true,
|
||||||
|
.width = 16U,
|
||||||
|
.height = 8U,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(bad_duration_layers),
|
||||||
|
});
|
||||||
|
|
||||||
|
PP_EXPECT(harness, !no_canvas);
|
||||||
|
PP_EXPECT(harness, !bad_layer);
|
||||||
|
PP_EXPECT(harness, !bad_blend);
|
||||||
|
PP_EXPECT(harness, !bad_duration);
|
||||||
|
}
|
||||||
|
|
||||||
void clear_plan_records_legacy_canvas_effects(pp::tests::Harness& harness)
|
void clear_plan_records_legacy_canvas_effects(pp::tests::Harness& harness)
|
||||||
{
|
{
|
||||||
const auto plan = pp::app::plan_document_canvas_clear(true, 0.0F, 0.1F, 0.2F, 0.3F);
|
const auto plan = pp::app::plan_document_canvas_clear(true, 0.0F, 0.1F, 0.2F, 0.3F);
|
||||||
@@ -119,6 +268,9 @@ void clear_executor_rejects_invalid_color(pp::tests::Harness& harness)
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
pp::tests::Harness harness;
|
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 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 records legacy canvas effects", clear_plan_records_legacy_canvas_effects);
|
||||||
harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas);
|
harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas);
|
||||||
harness.run("clear plan rejects bad color channels", clear_plan_rejects_bad_color_channels);
|
harness.run("clear plan rejects bad color channels", clear_plan_rejects_bad_color_channels);
|
||||||
|
|||||||
@@ -401,6 +401,22 @@ struct PlanCanvasClearArgs {
|
|||||||
float a = 0.0F;
|
float a = 0.0F;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PlanCanvasDocumentSnapshotArgs {
|
||||||
|
bool has_canvas = true;
|
||||||
|
std::uint32_t width = 64;
|
||||||
|
std::uint32_t height = 32;
|
||||||
|
std::uint32_t layers = 2;
|
||||||
|
std::uint32_t frames = 2;
|
||||||
|
std::uint32_t frame_duration_ms = 100;
|
||||||
|
std::uint32_t current_layer = 1;
|
||||||
|
std::uint32_t current_frame = 0;
|
||||||
|
std::uint32_t hidden_layer = 0;
|
||||||
|
std::uint32_t alpha_locked_layer = 1;
|
||||||
|
float opacity = 0.75F;
|
||||||
|
int blend_mode = 4;
|
||||||
|
std::uint32_t pending_face_payloads_per_layer = pp::document::cube_face_count;
|
||||||
|
};
|
||||||
|
|
||||||
struct PlanImageImportArgs {
|
struct PlanImageImportArgs {
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
@@ -2391,6 +2407,7 @@ void print_help()
|
|||||||
<< " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n"
|
<< " 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-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-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-image-import --width N --height N\n"
|
<< " plan-image-import --width N --height N\n"
|
||||||
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n"
|
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n"
|
||||||
<< " plan-layer-rename --old-name NAME --new-name NAME\n"
|
<< " plan-layer-rename --old-name NAME --new-name NAME\n"
|
||||||
@@ -5838,6 +5855,143 @@ int plan_canvas_clear(int argc, char** argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status parse_plan_canvas_document_snapshot_args(
|
||||||
|
int argc,
|
||||||
|
char** argv,
|
||||||
|
PlanCanvasDocumentSnapshotArgs& args)
|
||||||
|
{
|
||||||
|
for (int i = 2; i < argc; ++i) {
|
||||||
|
const std::string_view key(argv[i]);
|
||||||
|
if (key == "--no-canvas") {
|
||||||
|
args.has_canvas = false;
|
||||||
|
} 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") {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||||
|
}
|
||||||
|
const auto value = pp::foundation::parse_u32(argv[++i]);
|
||||||
|
if (!value) {
|
||||||
|
return value.status();
|
||||||
|
}
|
||||||
|
if (key == "--width") {
|
||||||
|
args.width = value.value();
|
||||||
|
} else if (key == "--height") {
|
||||||
|
args.height = value.value();
|
||||||
|
} else if (key == "--layers") {
|
||||||
|
args.layers = value.value();
|
||||||
|
} else if (key == "--frames") {
|
||||||
|
args.frames = value.value();
|
||||||
|
} else if (key == "--frame-duration-ms") {
|
||||||
|
args.frame_duration_ms = value.value();
|
||||||
|
} else if (key == "--current-layer") {
|
||||||
|
args.current_layer = value.value();
|
||||||
|
} else if (key == "--current-frame") {
|
||||||
|
args.current_frame = value.value();
|
||||||
|
} else if (key == "--hidden-layer") {
|
||||||
|
args.hidden_layer = value.value();
|
||||||
|
} else if (key == "--alpha-locked-layer") {
|
||||||
|
args.alpha_locked_layer = value.value();
|
||||||
|
} else {
|
||||||
|
args.pending_face_payloads_per_layer = value.value();
|
||||||
|
}
|
||||||
|
} else if (key == "--opacity") {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||||
|
}
|
||||||
|
const auto value = parse_float_arg(argv[++i]);
|
||||||
|
if (!value) {
|
||||||
|
return value.status();
|
||||||
|
}
|
||||||
|
args.opacity = value.value();
|
||||||
|
} else if (key == "--blend-mode") {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||||
|
}
|
||||||
|
const auto value = parse_i32_arg(argv[++i]);
|
||||||
|
if (!value) {
|
||||||
|
return value.status();
|
||||||
|
}
|
||||||
|
args.blend_mode = value.value();
|
||||||
|
} else {
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown option");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
int plan_canvas_document_snapshot(int argc, char** argv)
|
||||||
|
{
|
||||||
|
PlanCanvasDocumentSnapshotArgs args;
|
||||||
|
const auto status = parse_plan_canvas_document_snapshot_args(argc, argv, args);
|
||||||
|
if (!status.ok()) {
|
||||||
|
print_error("plan-canvas-document-snapshot", status.message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint32_t> frame_durations(args.frames, args.frame_duration_ms);
|
||||||
|
std::vector<std::string> layer_names;
|
||||||
|
std::vector<pp::app::DocumentCanvasLayerSnapshotInput> layers;
|
||||||
|
layer_names.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));
|
||||||
|
layers.push_back(pp::app::DocumentCanvasLayerSnapshotInput {
|
||||||
|
.name = layer_names.back(),
|
||||||
|
.visible = layer_index != args.hidden_layer,
|
||||||
|
.alpha_locked = layer_index == args.alpha_locked_layer,
|
||||||
|
.opacity = layer_index == args.current_layer ? args.opacity : 1.0F,
|
||||||
|
.blend_mode = layer_index == args.current_layer ? args.blend_mode : 0,
|
||||||
|
.frame_durations_ms = std::span<const std::uint32_t>(frame_durations),
|
||||||
|
.pending_face_payloads = args.pending_face_payloads_per_layer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
|
||||||
|
.has_canvas = args.has_canvas,
|
||||||
|
.width = args.width,
|
||||||
|
.height = args.height,
|
||||||
|
.active_layer_index = args.current_layer,
|
||||||
|
.active_frame_index = args.current_frame,
|
||||||
|
.layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
print_error("plan-canvas-document-snapshot", result.status().message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& value = result.value();
|
||||||
|
const auto& document = value.document;
|
||||||
|
const auto& active_layer = document.layers()[document.active_layer_index()];
|
||||||
|
std::cout << "{\"ok\":true,\"command\":\"plan-canvas-document-snapshot\""
|
||||||
|
<< ",\"state\":{\"hasCanvas\":" << json_bool(args.has_canvas)
|
||||||
|
<< ",\"width\":" << args.width
|
||||||
|
<< ",\"height\":" << args.height
|
||||||
|
<< ",\"layers\":" << args.layers
|
||||||
|
<< ",\"frames\":" << args.frames
|
||||||
|
<< ",\"currentLayer\":" << args.current_layer
|
||||||
|
<< ",\"currentFrame\":" << args.current_frame
|
||||||
|
<< "},\"snapshot\":{\"width\":" << document.width()
|
||||||
|
<< ",\"height\":" << document.height()
|
||||||
|
<< ",\"layers\":" << value.layer_count
|
||||||
|
<< ",\"frames\":" << value.frame_count
|
||||||
|
<< ",\"activeLayer\":" << document.active_layer_index()
|
||||||
|
<< ",\"activeFrame\":" << document.active_frame_index()
|
||||||
|
<< ",\"activeLayerName\":\"" << json_escape(active_layer.name)
|
||||||
|
<< "\",\"activeLayerOpacity\":" << active_layer.opacity
|
||||||
|
<< ",\"activeLayerBlend\":\"" << pp::paint::blend_mode_name(active_layer.blend_mode)
|
||||||
|
<< "\",\"activeLayerAlphaLocked\":" << json_bool(active_layer.alpha_locked)
|
||||||
|
<< ",\"pendingFacePayloads\":" << value.pending_face_payloads
|
||||||
|
<< ",\"metadataOnly\":" << json_bool(value.metadata_only)
|
||||||
|
<< ",\"requiresRendererPayloadReadback\":"
|
||||||
|
<< json_bool(value.requires_renderer_payload_readback)
|
||||||
|
<< ",\"documentFacePayloads\":" << document.face_pixel_payload_count()
|
||||||
|
<< "}}\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Status parse_plan_image_import_args(
|
pp::foundation::Status parse_plan_image_import_args(
|
||||||
int argc,
|
int argc,
|
||||||
char** argv,
|
char** argv,
|
||||||
@@ -11727,6 +11881,10 @@ int main(int argc, char** argv)
|
|||||||
return plan_canvas_clear(argc, argv);
|
return plan_canvas_clear(argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == "plan-canvas-document-snapshot") {
|
||||||
|
return plan_canvas_document_snapshot(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
if (command == "plan-image-import") {
|
if (command == "plan-image-import") {
|
||||||
return plan_image_import(argc, argv);
|
return plan_image_import(argc, argv);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user