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