Project legacy canvas metadata into documents

This commit is contained in:
2026-06-05 17:54:45 +02:00
parent a9ef2c598c
commit d0412e3bf9
11 changed files with 580 additions and 4 deletions

View File

@@ -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)

View File

@@ -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.

View 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 |

View File

@@ -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 |

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
} }