From dfdb7a4468785383ceebd5e449a57733a18e2b4f Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 00:16:34 +0200 Subject: [PATCH] Add document animation frame tests --- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 4 +- src/document/document.cpp | 90 +++++++++++++++++++++++++++++++ src/document/document.h | 16 ++++++ tests/document/document_tests.cpp | 60 +++++++++++++++++++++ tools/pano_cli/main.cpp | 2 + 6 files changed, 171 insertions(+), 3 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 05b3328..7ddd354 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` exists but CMake is not yet using a validated vcpkg toolchain on this machine | `vcpkg` is not available on PATH and Visual Studio reports manifest mode is disabled | `cmake --preset windows-msvc-default` currently configures with vendored dependencies | Add validated vcpkg toolchain/preset integration for desktop, Android, and Apple triplets | | 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/document model but is not yet wired to legacy `Canvas`, PPI load/save, animation frames, or undo/redo | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | 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 model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, or undo/redo | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | 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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index f0a1aa2..89506f6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -306,8 +306,8 @@ input. A deterministic `TraceRecorder` now records component/name/thread/frame and stroke timing spans with invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection and corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader -blend modes. `pp_document` has started with a pure canvas/layer model and -layer invariant tests. `pp_renderer_api` has started with renderer-neutral +blend modes. `pp_document` has started with a pure canvas/layer/frame model +and layer/frame 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 the paint blend reference. `pp_ui_core` has started with XML-layout-facing diff --git a/src/document/document.cpp b/src/document/document.cpp index a31dc32..32c7b07 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -48,6 +48,7 @@ pp::foundation::Result CanvasDocument::create(DocumentConfig con for (std::uint32_t i = 0; i < config.layer_count; ++i) { document.layers_.push_back(Layer { .name = default_layer_name(i) }); } + document.frames_.push_back(AnimationFrame {}); return pp::foundation::Result::success(document); } @@ -67,11 +68,21 @@ std::size_t CanvasDocument::active_layer_index() const noexcept return active_layer_index_; } +std::size_t CanvasDocument::active_frame_index() const noexcept +{ + return active_frame_index_; +} + std::span CanvasDocument::layers() const noexcept { return layers_; } +std::span CanvasDocument::frames() const noexcept +{ + return frames_; +} + pp::foundation::Result CanvasDocument::add_layer(std::string_view name) { if (layers_.size() >= max_layer_count) { @@ -141,4 +152,83 @@ pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexc return pp::foundation::Status::success(); } +pp::foundation::Result CanvasDocument::add_frame(std::uint32_t duration_ms) +{ + if (frames_.size() >= max_frame_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document frame count exceeds the configured limit")); + } + + if (duration_ms < min_frame_duration_ms) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("frame duration must be greater than zero")); + } + + frames_.push_back(AnimationFrame { .duration_ms = duration_ms }); + active_frame_index_ = frames_.size() - 1U; + return pp::foundation::Result::success(active_frame_index_); +} + +pp::foundation::Result CanvasDocument::duplicate_frame(std::size_t index) +{ + if (index >= frames_.size()) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("frame index is outside the document")); + } + + if (frames_.size() >= max_frame_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document frame count exceeds the configured limit")); + } + + const auto insert_at = index + 1U; + frames_.insert(frames_.begin() + static_cast(insert_at), frames_[index]); + active_frame_index_ = insert_at; + return pp::foundation::Result::success(active_frame_index_); +} + +pp::foundation::Status CanvasDocument::remove_frame(std::size_t index) +{ + if (index >= frames_.size()) { + return pp::foundation::Status::out_of_range("frame index is outside the document"); + } + + if (frames_.size() == 1U) { + return pp::foundation::Status::invalid_argument("document must keep at least one frame"); + } + + frames_.erase(frames_.begin() + static_cast(index)); + if (active_frame_index_ >= frames_.size()) { + active_frame_index_ = frames_.size() - 1U; + } else if (active_frame_index_ > index) { + --active_frame_index_; + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept +{ + if (index >= frames_.size()) { + return pp::foundation::Status::out_of_range("frame index is outside the document"); + } + + if (duration_ms < min_frame_duration_ms) { + return pp::foundation::Status::invalid_argument("frame duration must be greater than zero"); + } + + frames_[index].duration_ms = duration_ms; + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept +{ + if (index >= frames_.size()) { + return pp::foundation::Status::out_of_range("frame index is outside the document"); + } + + active_frame_index_ = index; + return pp::foundation::Status::success(); +} + } diff --git a/src/document/document.h b/src/document/document.h index 64af7f6..510d537 100644 --- a/src/document/document.h +++ b/src/document/document.h @@ -13,6 +13,8 @@ namespace pp::document { constexpr std::uint32_t max_canvas_dimension = 131072; constexpr std::uint32_t max_layer_count = 1024; +constexpr std::uint32_t max_frame_count = 100000; +constexpr std::uint32_t min_frame_duration_ms = 1; struct DocumentConfig { std::uint32_t width = 0; @@ -27,6 +29,10 @@ struct Layer { pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal; }; +struct AnimationFrame { + std::uint32_t duration_ms = 100; +}; + class CanvasDocument { public: [[nodiscard]] static pp::foundation::Result create(DocumentConfig config); @@ -34,18 +40,28 @@ public: [[nodiscard]] std::uint32_t width() const noexcept; [[nodiscard]] std::uint32_t height() const noexcept; [[nodiscard]] std::size_t active_layer_index() const noexcept; + [[nodiscard]] std::size_t active_frame_index() const noexcept; [[nodiscard]] std::span layers() const noexcept; + [[nodiscard]] std::span frames() const noexcept; [[nodiscard]] pp::foundation::Result add_layer(std::string_view name); [[nodiscard]] pp::foundation::Status remove_layer(std::size_t index); [[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to); [[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept; + [[nodiscard]] pp::foundation::Result add_frame(std::uint32_t duration_ms); + [[nodiscard]] pp::foundation::Result duplicate_frame(std::size_t index); + [[nodiscard]] pp::foundation::Status remove_frame(std::size_t index); + [[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept; + [[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept; + private: std::uint32_t width_ = 0; std::uint32_t height_ = 0; std::size_t active_layer_index_ = 0; + std::size_t active_frame_index_ = 0; std::vector layers_; + std::vector frames_; }; } diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp index 91f7389..634f4c4 100644 --- a/tests/document/document_tests.cpp +++ b/tests/document/document_tests.cpp @@ -6,6 +6,7 @@ using pp::document::CanvasDocument; using pp::document::DocumentConfig; using pp::document::max_canvas_dimension; +using pp::document::max_frame_count; using pp::document::max_layer_count; using pp::foundation::StatusCode; @@ -23,6 +24,9 @@ void creates_document_with_default_layers(pp::tests::Harness& h) PP_EXPECT(h, document.value().layers()[0].name == std::string_view("Layer 1")); PP_EXPECT(h, document.value().layers()[1].name == std::string_view("Layer 2")); PP_EXPECT(h, document.value().active_layer_index() == 0U); + PP_EXPECT(h, document.value().frames().size() == 1U); + PP_EXPECT(h, document.value().frames()[0].duration_ms == 100U); + PP_EXPECT(h, document.value().active_frame_index() == 0U); } void rejects_invalid_document_configs(pp::tests::Harness& h) @@ -87,6 +91,60 @@ void moves_layers_and_preserves_active_layer_identity(pp::tests::Harness& h) PP_EXPECT(h, bad_move.code == StatusCode::out_of_range); } +void manages_animation_frames_and_duration(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 1 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + const auto added = document.add_frame(250); + PP_EXPECT(h, added.ok()); + PP_EXPECT(h, added.value() == 1U); + PP_EXPECT(h, document.active_frame_index() == 1U); + PP_EXPECT(h, document.frames()[1].duration_ms == 250U); + + const auto duplicated = document.duplicate_frame(1); + PP_EXPECT(h, duplicated.ok()); + PP_EXPECT(h, duplicated.value() == 2U); + PP_EXPECT(h, document.frames()[2].duration_ms == 250U); + PP_EXPECT(h, document.set_frame_duration(2, 333).ok()); + PP_EXPECT(h, document.frames()[2].duration_ms == 333U); + + PP_EXPECT(h, document.remove_frame(1).ok()); + PP_EXPECT(h, document.frames().size() == 2U); + PP_EXPECT(h, document.active_frame_index() == 1U); +} + +void rejects_invalid_animation_frame_operations(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 1 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + const auto zero_duration = document.add_frame(0); + const auto duplicate_missing = document.duplicate_frame(9); + const auto remove_missing = document.remove_frame(9); + const auto remove_only = document.remove_frame(0); + const auto set_bad_duration = document.set_frame_duration(0, 0); + const auto set_missing_active = document.set_active_frame(2); + + PP_EXPECT(h, !zero_duration.ok()); + PP_EXPECT(h, zero_duration.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !duplicate_missing.ok()); + PP_EXPECT(h, duplicate_missing.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !remove_missing.ok()); + PP_EXPECT(h, remove_missing.code == StatusCode::out_of_range); + PP_EXPECT(h, !remove_only.ok()); + PP_EXPECT(h, remove_only.code == StatusCode::invalid_argument); + PP_EXPECT(h, !set_bad_duration.ok()); + PP_EXPECT(h, set_bad_duration.code == StatusCode::invalid_argument); + PP_EXPECT(h, !set_missing_active.ok()); + PP_EXPECT(h, set_missing_active.code == StatusCode::out_of_range); + PP_EXPECT(h, max_frame_count > document.frames().size()); +} + } int main() @@ -96,5 +154,7 @@ int main() harness.run("rejects_invalid_document_configs", rejects_invalid_document_configs); harness.run("manages_layer_add_remove_and_active_index", manages_layer_add_remove_and_active_index); harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity); + harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration); + harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index d63d874..4c2381c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -100,6 +100,8 @@ int create_document(int argc, char** argv) << ",\"height\":" << document.value().height() << ",\"layers\":" << document.value().layers().size() << ",\"activeLayer\":" << document.value().active_layer_index() + << ",\"frames\":" << document.value().frames().size() + << ",\"activeFrame\":" << document.value().active_frame_index() << "}}\n"; return 0; }