Add document animation frame tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,6 +48,7 @@ pp::foundation::Result<CanvasDocument> 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<CanvasDocument>::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<const Layer> CanvasDocument::layers() const noexcept
|
||||
{
|
||||
return layers_;
|
||||
}
|
||||
|
||||
std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
|
||||
{
|
||||
return frames_;
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> 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<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
|
||||
{
|
||||
if (frames_.size() >= max_frame_count) {
|
||||
return pp::foundation::Result<std::size_t>::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<std::size_t>::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<std::size_t>::success(active_frame_index_);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
|
||||
{
|
||||
if (index >= frames_.size()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("frame index is outside the document"));
|
||||
}
|
||||
|
||||
if (frames_.size() >= max_frame_count) {
|
||||
return pp::foundation::Result<std::size_t>::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<std::ptrdiff_t>(insert_at), frames_[index]);
|
||||
active_frame_index_ = insert_at;
|
||||
return pp::foundation::Result<std::size_t>::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<std::ptrdiff_t>(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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<CanvasDocument> 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<const Layer> layers() const noexcept;
|
||||
[[nodiscard]] std::span<const AnimationFrame> frames() 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);
|
||||
[[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<std::size_t> add_frame(std::uint32_t duration_ms);
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> 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<Layer> layers_;
|
||||
std::vector<AnimationFrame> frames_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user