diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 1123948..a82d8b1 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -128,7 +128,8 @@ Known local toolchain state: - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and is covered by `pano_cli_create_animation_document_smoke`. - `pano_cli simulate-document-edits` exercises pure document layer/frame edit - operations and is covered by `pano_cli_simulate_document_edits_smoke`. + operations, renderer-free face payloads, and renderer-free selection masks, + and is covered by `pano_cli_simulate_document_edits_smoke`. - `pano_cli simulate-document-history` exercises pure document history apply/undo/redo behavior and is covered by `pano_cli_simulate_document_history_smoke`. @@ -272,8 +273,9 @@ Known local toolchain state: apply/undo/redo state through JSON automation and is covered by `pano_cli_simulate_document_history_smoke`. - `pano_cli simulate-document-edits` exposes `pp_document` layer metadata, - frame order, active-index, and tiny face-payload state through JSON - automation and is covered by `pano_cli_simulate_document_edits_smoke`. + frame order, active-index, tiny face-payload state, and selection-mask state + through JSON automation and is covered by + `pano_cli_simulate_document_edits_smoke`. - `pano_cli simulate-image-import` exposes embedded PNG decode and document face-payload attachment through JSON automation and is covered by `pano_cli_simulate_image_import_smoke`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 0173f4c..b765ba7 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -28,7 +28,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, 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 | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | 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` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | 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` | 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, and renderer-free RGBA8 face payload storage, but it is not yet wired to legacy `Canvas`, selection masks, 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`; `pp_document_ppi_import_tests` | Legacy document behavior is represented by `pp_document` 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, and renderer-free alpha8 selection-mask storage, but it is not yet wired to legacy `Canvas`, 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`; `pp_document_ppi_import_tests`; `pano_cli_simulate_document_edits_smoke` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | 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` | Package-smoke covers Windows AppX, Android APK variants, 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`, `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 layer opacity/blend/alpha-lock/visibility metadata, metadata-only and dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, 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`; `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` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c7da7f8..c5e74a3 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -323,8 +323,9 @@ stroke spacing/interpolation plus a pure text stroke-script parser. `pp_document` has started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot construction, per-layer frame metadata, layer metadata operations, frame -move/duration queries, renderer-free RGBA8 cube-face payload storage, PPI image -import, and layer/frame/undo-redo history invariant tests. +move/duration queries, renderer-free RGBA8 cube-face payload storage, +renderer-free alpha8 selection-mask storage, PPI image import, and +layer/frame/undo-redo history invariant tests. `pp_renderer_api` has started with renderer-neutral texture/readback descriptors and validation tests. `pp_paint_renderer` has started with deterministic CPU layer compositing over renderer extents using @@ -349,8 +350,8 @@ extracted `pp_assets` writer and round-trips metadata-only and test dirty-face-payload variants through `load-project`. `pano_cli create-document` can create simple animation documents with explicit frame count/duration. `pano_cli simulate-document-edits` exercises pure -layer metadata, frame reordering, active-index preservation, and tiny -face-payload attachment. `pano_cli simulate-document-history` exercises the +layer metadata, frame reordering, active-index preservation, tiny face-payload +attachment, and selection-mask attachment. `pano_cli simulate-document-history` exercises the pure `pp_document::DocumentHistory` apply/undo/redo path and emits JSON state summaries. `pano_cli simulate-image-import` decodes an embedded tiny PNG through `pp_assets` and attaches the resulting RGBA8 payload to `pp_document`. @@ -781,8 +782,8 @@ Results: apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation. - `pano_cli simulate-document-edits` exercises pure document layer/frame edit - operations and emits JSON metadata, frame order, and face-payload state for - agent automation. + operations and emits JSON metadata, frame order, face-payload state, and + selection-mask state for agent automation. - `pano_cli simulate-image-import` exercises embedded PNG decode through `pp_assets` and `pp_document` face-payload attachment through JSON automation. diff --git a/src/document/document.cpp b/src/document/document.cpp index ff70823..f24989c 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace pp::document { @@ -107,37 +108,70 @@ namespace { return pp::foundation::Status::success(); } -[[nodiscard]] pp::foundation::Result rgba8_byte_size( +[[nodiscard]] pp::foundation::Result byte_size( std::uint32_t width, - std::uint32_t height) noexcept + std::uint32_t height, + std::uint32_t components, + const char* dimensions_overflow_message, + const char* byte_size_overflow_message, + const char* payload_limit_message, + const char* addressable_memory_message) noexcept { const auto width64 = static_cast(width); const auto height64 = static_cast(height); if (width64 > std::numeric_limits::max() / height64) { return pp::foundation::Result::failure( - pp::foundation::Status::out_of_range("face pixel dimensions overflow")); + pp::foundation::Status::out_of_range(dimensions_overflow_message)); } const auto pixels = width64 * height64; - if (pixels > std::numeric_limits::max() / rgba8_components) { + if (pixels > std::numeric_limits::max() / components) { return pp::foundation::Result::failure( - pp::foundation::Status::out_of_range("face pixel byte size overflows")); + pp::foundation::Status::out_of_range(byte_size_overflow_message)); } - const auto bytes = pixels * rgba8_components; + const auto bytes = pixels * components; if (bytes > max_face_pixel_payload_bytes) { return pp::foundation::Result::failure( - pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit")); + pp::foundation::Status::out_of_range(payload_limit_message)); } if (bytes > static_cast(std::numeric_limits::max())) { return pp::foundation::Result::failure( - pp::foundation::Status::out_of_range("face pixel payload exceeds addressable memory")); + pp::foundation::Status::out_of_range(addressable_memory_message)); } return pp::foundation::Result::success(static_cast(bytes)); } +[[nodiscard]] pp::foundation::Result rgba8_byte_size( + std::uint32_t width, + std::uint32_t height) noexcept +{ + return byte_size( + width, + height, + rgba8_components, + "face pixel dimensions overflow", + "face pixel byte size overflows", + "face pixel payload exceeds the configured limit", + "face pixel payload exceeds addressable memory"); +} + +[[nodiscard]] pp::foundation::Result alpha8_byte_size( + std::uint32_t width, + std::uint32_t height) noexcept +{ + return byte_size( + width, + height, + alpha8_components, + "selection mask dimensions overflow", + "selection mask byte size overflows", + "selection mask payload exceeds the configured limit", + "selection mask payload exceeds addressable memory"); +} + [[nodiscard]] pp::foundation::Status validate_face_pixels( LayerFacePixels pixels, std::uint32_t document_width, @@ -168,6 +202,36 @@ namespace { return pp::foundation::Status::success(); } +[[nodiscard]] pp::foundation::Status validate_selection_mask( + SelectionMask mask, + std::uint32_t document_width, + std::uint32_t document_height) noexcept +{ + if (mask.face_index >= cube_face_count) { + return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document"); + } + + if (mask.width == 0 || mask.height == 0) { + return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero"); + } + + if (mask.x > document_width || mask.width > document_width - mask.x + || mask.y > document_height || mask.height > document_height - mask.y) { + return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document"); + } + + const auto expected_bytes = alpha8_byte_size(mask.width, mask.height); + if (!expected_bytes) { + return expected_bytes.status(); + } + + if (mask.alpha8.size() != expected_bytes.value()) { + return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions"); + } + + return pp::foundation::Status::success(); +} + } pp::foundation::Result CanvasDocument::create(DocumentConfig config) @@ -272,6 +336,14 @@ pp::foundation::Result CanvasDocument::create_from_snapshot(Docu document.frames_.push_back(frame_config); } + for (const auto& mask : config.selection_masks) { + const auto mask_status = validate_selection_mask(mask, document.width_, document.height_); + if (!mask_status.ok()) { + return pp::foundation::Result::failure(mask_status); + } + document.selection_masks_.push_back(mask); + } + return pp::foundation::Result::success(document); } @@ -325,6 +397,11 @@ std::size_t CanvasDocument::face_pixel_payload_count() const noexcept return count; } +std::size_t CanvasDocument::selection_mask_payload_count() const noexcept +{ + return selection_masks_.size(); +} + std::span CanvasDocument::layers() const noexcept { return layers_; @@ -335,6 +412,11 @@ std::span CanvasDocument::frames() const noexcept return frames_; } +std::span CanvasDocument::selection_masks() const noexcept +{ + return selection_masks_; +} + pp::foundation::Result CanvasDocument::add_layer(std::string_view name) { if (layers_.size() >= max_layer_count) { @@ -656,6 +738,47 @@ pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels( return pp::foundation::Status::success(); } +pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask) +{ + const auto mask_status = validate_selection_mask(mask, width_, height_); + if (!mask_status.ok()) { + return mask_status; + } + + const auto existing = std::find_if( + selection_masks_.begin(), + selection_masks_.end(), + [face_index = mask.face_index](const SelectionMask& candidate) { + return candidate.face_index == face_index; + }); + if (existing == selection_masks_.end()) { + selection_masks_.push_back(std::move(mask)); + } else { + *existing = std::move(mask); + } + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept +{ + if (face_index >= cube_face_count) { + return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document"); + } + + const auto existing = std::find_if( + selection_masks_.begin(), + selection_masks_.end(), + [face_index](const SelectionMask& candidate) { + return candidate.face_index == face_index; + }); + if (existing == selection_masks_.end()) { + return pp::foundation::Status::out_of_range("selection mask face is not present"); + } + + selection_masks_.erase(existing); + return pp::foundation::Status::success(); +} + pp::foundation::Result DocumentHistory::create( CanvasDocument initial_document, std::size_t max_entries) diff --git a/src/document/document.h b/src/document/document.h index 6d65844..e486f5f 100644 --- a/src/document/document.h +++ b/src/document/document.h @@ -20,6 +20,7 @@ constexpr std::size_t max_document_history_entries = 10000; constexpr std::size_t max_layer_name_length = 128; constexpr std::uint32_t cube_face_count = 6; constexpr std::uint32_t rgba8_components = 4; +constexpr std::uint32_t alpha8_components = 1; constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL; struct DocumentConfig { @@ -37,6 +38,15 @@ struct LayerFacePixels { std::vector rgba8; }; +struct SelectionMask { + 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::vector alpha8; +}; + struct AnimationFrame { std::uint32_t duration_ms = 100; std::vector face_pixels; @@ -65,6 +75,7 @@ struct DocumentSnapshotConfig { std::uint32_t height = 0; std::span layers; std::span frames; + std::span selection_masks; }; class CanvasDocument { @@ -79,8 +90,10 @@ public: [[nodiscard]] std::uint64_t animation_duration_ms() const noexcept; [[nodiscard]] pp::foundation::Result layer_animation_duration_ms(std::size_t index) const noexcept; [[nodiscard]] std::size_t face_pixel_payload_count() const noexcept; + [[nodiscard]] std::size_t selection_mask_payload_count() const noexcept; [[nodiscard]] std::span layers() const noexcept; [[nodiscard]] std::span frames() const noexcept; + [[nodiscard]] std::span selection_masks() const noexcept; [[nodiscard]] pp::foundation::Result add_layer(std::string_view name); [[nodiscard]] pp::foundation::Status remove_layer(std::size_t index); @@ -102,6 +115,8 @@ public: std::size_t layer_index, std::size_t frame_index, LayerFacePixels pixels); + [[nodiscard]] pp::foundation::Status set_selection_mask(SelectionMask mask); + [[nodiscard]] pp::foundation::Status clear_selection_mask(std::uint32_t face_index) noexcept; private: std::uint32_t width_ = 0; @@ -110,6 +125,7 @@ private: std::size_t active_frame_index_ = 0; std::vector layers_; std::vector frames_; + std::vector selection_masks_; }; class DocumentHistory { diff --git a/src/document/ppi_import.cpp b/src/document/ppi_import.cpp index 175d896..594595a 100644 --- a/src/document/ppi_import.cpp +++ b/src/document/ppi_import.cpp @@ -79,6 +79,7 @@ namespace { .height = project.body.summary.height, .layers = layers, .frames = frames, + .selection_masks = {}, }); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9dca2c2..9004609 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -334,7 +334,7 @@ if(TARGET pano_cli) COMMAND pano_cli simulate-document-edits --width 128 --height 64) set_tests_properties(pano_cli_simulate_document_edits_smoke PROPERTIES LABELS "document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"simulate-document-edits\".*\"document\":\\{\"width\":128,\"height\":64,\"layers\":2,\"frames\":3,\"activeLayer\":0,\"activeFrame\":0,\"animationDurationMs\":683,\"facePayloads\":1\\}.*\"activeLayer\":\\{\"name\":\"Ink\",\"visible\":false,\"alphaLocked\":true,\"opacity\":0.625,\"blendMode\":\"overlay\",\"frames\":3\\}.*\"frames\":\\[250,100,333\\].*\"activeLayerFrameDurations\":\\[250,100,333\\]") + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-document-edits\".*\"document\":\\{\"width\":128,\"height\":64,\"layers\":2,\"frames\":3,\"activeLayer\":0,\"activeFrame\":0,\"animationDurationMs\":683,\"facePayloads\":1,\"selectionMasks\":1\\}.*\"activeLayer\":\\{\"name\":\"Ink\",\"visible\":false,\"alphaLocked\":true,\"opacity\":0.625,\"blendMode\":\"overlay\",\"frames\":3\\}.*\"frames\":\\[250,100,333\\].*\"activeLayerFrameDurations\":\\[250,100,333\\].*\"selectionMask\":\\{\"face\":4,\"x\":3,\"y\":5,\"width\":2,\"height\":2,\"bytes\":4,\"maxAlpha\":255\\}") add_test(NAME pano_cli_simulate_document_history_smoke COMMAND pano_cli simulate-document-history --width 64 --height 32 --history 4) diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp index da04ddc..a596213 100644 --- a/tests/document/document_tests.cpp +++ b/tests/document/document_tests.cpp @@ -12,6 +12,7 @@ using pp::document::DocumentLayerConfig; using pp::document::DocumentSnapshotConfig; using pp::document::AnimationFrame; using pp::document::LayerFacePixels; +using pp::document::SelectionMask; using pp::document::max_document_history_entries; using pp::document::max_canvas_dimension; using pp::document::max_frame_count; @@ -196,6 +197,7 @@ void creates_document_from_snapshot_metadata(pp::tests::Harness& h) .height = 64, .layers = layers, .frames = frames, + .selection_masks = {}, }); PP_EXPECT(h, document_result.ok()); @@ -238,6 +240,7 @@ void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h) .height = 64, .layers = layers, .frames = project_frames, + .selection_masks = {}, }); PP_EXPECT(h, document_result.ok()); @@ -265,30 +268,35 @@ void rejects_invalid_snapshot_metadata(pp::tests::Harness& h) .height = 64, .layers = {}, .frames = frames, + .selection_masks = {}, }); const auto no_frames = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 64, .height = 64, .layers = layers, .frames = {}, + .selection_masks = {}, }); const auto bad_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 64, .height = 64, .layers = layers, .frames = bad_frames, + .selection_masks = {}, }); const auto bad_layer = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 64, .height = 64, .layers = bad_layers, .frames = frames, + .selection_masks = {}, }); const auto bad_layer_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 64, .height = 64, .layers = bad_layer_frames, .frames = frames, + .selection_masks = {}, }); PP_EXPECT(h, !no_layers.ok()); @@ -503,6 +511,114 @@ void rejects_invalid_face_pixel_payloads(pp::tests::Harness& h) PP_EXPECT(h, document.face_pixel_payload_count() == 0U); } +void manages_selection_masks(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 32, .layer_count = 1 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + PP_EXPECT(h, document.set_selection_mask(SelectionMask { + .face_index = 4, + .x = 3, + .y = 5, + .width = 2, + .height = 2, + .alpha8 = { 0, 64, 128, 255 }, + }).ok()); + + PP_EXPECT(h, document.selection_mask_payload_count() == 1U); + PP_EXPECT(h, document.selection_masks().size() == 1U); + PP_EXPECT(h, document.selection_masks()[0].face_index == 4U); + PP_EXPECT(h, document.selection_masks()[0].x == 3U); + PP_EXPECT(h, document.selection_masks()[0].alpha8[3] == 255U); + + PP_EXPECT(h, document.set_selection_mask(SelectionMask { + .face_index = 4, + .x = 7, + .y = 9, + .width = 1, + .height = 1, + .alpha8 = { 200 }, + }).ok()); + + PP_EXPECT(h, document.selection_mask_payload_count() == 1U); + PP_EXPECT(h, document.selection_masks()[0].x == 7U); + PP_EXPECT(h, document.selection_masks()[0].alpha8[0] == 200U); + + PP_EXPECT(h, document.clear_selection_mask(4).ok()); + PP_EXPECT(h, document.selection_mask_payload_count() == 0U); +} + +void rejects_invalid_selection_masks(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 32, .layer_count = 1 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + const auto bad_face = document.set_selection_mask( + SelectionMask { .face_index = 6, .x = 0, .y = 0, .width = 1, .height = 1, .alpha8 = { 1 } }); + const auto zero_width = document.set_selection_mask( + SelectionMask { .face_index = 0, .x = 0, .y = 0, .width = 0, .height = 1, .alpha8 = {} }); + const auto outside_bounds = document.set_selection_mask( + SelectionMask { .face_index = 0, .x = 63, .y = 0, .width = 2, .height = 1, .alpha8 = { 1, 2 } }); + const auto bad_byte_count = document.set_selection_mask( + SelectionMask { .face_index = 0, .x = 0, .y = 0, .width = 2, .height = 1, .alpha8 = { 1 } }); + const auto missing_clear = document.clear_selection_mask(2); + const auto bad_clear_face = document.clear_selection_mask(6); + + PP_EXPECT(h, !bad_face.ok()); + PP_EXPECT(h, bad_face.code == StatusCode::out_of_range); + PP_EXPECT(h, !zero_width.ok()); + PP_EXPECT(h, zero_width.code == StatusCode::invalid_argument); + PP_EXPECT(h, !outside_bounds.ok()); + PP_EXPECT(h, outside_bounds.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_byte_count.ok()); + PP_EXPECT(h, bad_byte_count.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_clear.ok()); + PP_EXPECT(h, missing_clear.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_clear_face.ok()); + PP_EXPECT(h, bad_clear_face.code == StatusCode::out_of_range); + PP_EXPECT(h, document.selection_mask_payload_count() == 0U); +} + +void preserves_selection_masks_in_snapshots_and_history(pp::tests::Harness& h) +{ + const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } }; + const AnimationFrame frames[] { { .duration_ms = 100, .face_pixels = {} } }; + const SelectionMask masks[] { + SelectionMask { + .face_index = 3, + .x = 1, + .y = 2, + .width = 1, + .height = 2, + .alpha8 = { 12, 240 }, + }, + }; + + const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { + .width = 64, + .height = 32, + .layers = layers, + .frames = frames, + .selection_masks = masks, + }); + PP_EXPECT(h, document_result.ok()); + PP_EXPECT(h, document_result.value().selection_mask_payload_count() == 1U); + PP_EXPECT(h, document_result.value().selection_masks()[0].alpha8[1] == 240U); + + auto history_result = DocumentHistory::create(document_result.value(), 3); + PP_EXPECT(h, history_result.ok()); + auto history = history_result.value(); + auto next = history.current(); + PP_EXPECT(h, next.clear_selection_mask(3).ok()); + PP_EXPECT(h, history.apply(next).ok()); + PP_EXPECT(h, history.undo().ok()); + PP_EXPECT(h, history.current().selection_mask_payload_count() == 1U); +} + void records_document_history_and_restores_snapshots(pp::tests::Harness& h) { auto document_result = CanvasDocument::create( @@ -634,6 +750,9 @@ int main() harness.run("attaches_layer_frame_face_pixels", attaches_layer_frame_face_pixels); harness.run("replaces_existing_face_pixel_payload", replaces_existing_face_pixel_payload); harness.run("rejects_invalid_face_pixel_payloads", rejects_invalid_face_pixel_payloads); + harness.run("manages_selection_masks", manages_selection_masks); + harness.run("rejects_invalid_selection_masks", rejects_invalid_selection_masks); + harness.run("preserves_selection_masks_in_snapshots_and_history", preserves_selection_masks_in_snapshots_and_history); harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots); harness.run("applying_after_undo_discards_redo_branch", applying_after_undo_discards_redo_branch); harness.run("bounds_document_history_capacity", bounds_document_history_capacity); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index c7d2836..1aa236d 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1285,7 +1285,21 @@ int simulate_document_edits(int argc, char** argv) return 2; } + edit_status = document.set_selection_mask(pp::document::SelectionMask { + .face_index = 4, + .x = 3, + .y = 5, + .width = 2, + .height = 2, + .alpha8 = { 0, 64, 128, 255 }, + }); + if (!edit_status.ok()) { + print_error("simulate-document-edits", edit_status.message); + return 2; + } + const auto& active_layer = document.layers()[document.active_layer_index()]; + const auto& selection_mask = document.selection_masks()[0]; std::cout << "{\"ok\":true,\"command\":\"simulate-document-edits\"" << ",\"document\":{\"width\":" << document.width() << ",\"height\":" << document.height() @@ -1295,6 +1309,7 @@ int simulate_document_edits(int argc, char** argv) << ",\"activeFrame\":" << document.active_frame_index() << ",\"animationDurationMs\":" << document.animation_duration_ms() << ",\"facePayloads\":" << document.face_pixel_payload_count() + << ",\"selectionMasks\":" << document.selection_mask_payload_count() << "},\"activeLayer\":{\"name\":\"" << json_escape(active_layer.name) << "\",\"visible\":" << (active_layer.visible ? "true" : "false") << ",\"alphaLocked\":" << (active_layer.alpha_locked ? "true" : "false") @@ -1315,7 +1330,14 @@ int simulate_document_edits(int argc, char** argv) } std::cout << active_layer.frames[frame_index].duration_ms; } - std::cout << "]}\n"; + std::cout << "],\"selectionMask\":{\"face\":" << selection_mask.face_index + << ",\"x\":" << selection_mask.x + << ",\"y\":" << selection_mask.y + << ",\"width\":" << selection_mask.width + << ",\"height\":" << selection_mask.height + << ",\"bytes\":" << selection_mask.alpha8.size() + << ",\"maxAlpha\":" << static_cast(selection_mask.alpha8.back()) + << "}}\n"; return 0; }