Add document selection mask automation
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::document {
|
||||
@@ -107,37 +108,70 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> 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<std::uint64_t>(width);
|
||||
const auto height64 = static_cast<std::uint64_t>(height);
|
||||
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
|
||||
return pp::foundation::Result<std::size_t>::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<std::uint64_t>::max() / rgba8_components) {
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||
return pp::foundation::Result<std::size_t>::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<std::size_t>::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::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||
return pp::foundation::Result<std::size_t>::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<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> 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<std::size_t> 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> CanvasDocument::create(DocumentConfig config)
|
||||
@@ -272,6 +336,14 @@ pp::foundation::Result<CanvasDocument> 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<CanvasDocument>::failure(mask_status);
|
||||
}
|
||||
document.selection_masks_.push_back(mask);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::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<const Layer> CanvasDocument::layers() const noexcept
|
||||
{
|
||||
return layers_;
|
||||
@@ -335,6 +412,11 @@ std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
|
||||
return frames_;
|
||||
}
|
||||
|
||||
std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
|
||||
{
|
||||
return selection_masks_;
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> 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> DocumentHistory::create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries)
|
||||
|
||||
@@ -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<std::uint8_t> 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<std::uint8_t> alpha8;
|
||||
};
|
||||
|
||||
struct AnimationFrame {
|
||||
std::uint32_t duration_ms = 100;
|
||||
std::vector<LayerFacePixels> face_pixels;
|
||||
@@ -65,6 +75,7 @@ struct DocumentSnapshotConfig {
|
||||
std::uint32_t height = 0;
|
||||
std::span<const DocumentLayerConfig> layers;
|
||||
std::span<const AnimationFrame> frames;
|
||||
std::span<const SelectionMask> selection_masks;
|
||||
};
|
||||
|
||||
class CanvasDocument {
|
||||
@@ -79,8 +90,10 @@ public:
|
||||
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> 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<const Layer> layers() const noexcept;
|
||||
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
|
||||
[[nodiscard]] std::span<const SelectionMask> selection_masks() const noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> 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<Layer> layers_;
|
||||
std::vector<AnimationFrame> frames_;
|
||||
std::vector<SelectionMask> selection_masks_;
|
||||
};
|
||||
|
||||
class DocumentHistory {
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace {
|
||||
.height = project.body.summary.height,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
.selection_masks = {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<int>(selection_mask.alpha8.back())
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user