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-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-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-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 |
|
| 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
|
## 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
|
and stroke timing spans with invalid-end tests. `pp_assets` has started with
|
||||||
PNG/JPEG signature detection and corrupt/truncated/unsupported tests.
|
PNG/JPEG signature detection and corrupt/truncated/unsupported tests.
|
||||||
`pp_paint` has started with CPU reference math for the five current shader
|
`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
|
blend modes. `pp_document` has started with a pure canvas/layer/frame model
|
||||||
layer invariant tests. `pp_renderer_api` has started with renderer-neutral
|
and layer/frame invariant tests. `pp_renderer_api` has started with renderer-neutral
|
||||||
texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
||||||
started with deterministic CPU layer compositing over renderer extents using
|
started with deterministic CPU layer compositing over renderer extents using
|
||||||
the paint blend reference. `pp_ui_core` has started with XML-layout-facing
|
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) {
|
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
|
||||||
document.layers_.push_back(Layer { .name = default_layer_name(i) });
|
document.layers_.push_back(Layer { .name = default_layer_name(i) });
|
||||||
}
|
}
|
||||||
|
document.frames_.push_back(AnimationFrame {});
|
||||||
|
|
||||||
return pp::foundation::Result<CanvasDocument>::success(document);
|
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||||
}
|
}
|
||||||
@@ -67,11 +68,21 @@ std::size_t CanvasDocument::active_layer_index() const noexcept
|
|||||||
return active_layer_index_;
|
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
|
std::span<const Layer> CanvasDocument::layers() const noexcept
|
||||||
{
|
{
|
||||||
return layers_;
|
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)
|
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
|
||||||
{
|
{
|
||||||
if (layers_.size() >= max_layer_count) {
|
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();
|
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_canvas_dimension = 131072;
|
||||||
constexpr std::uint32_t max_layer_count = 1024;
|
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 {
|
struct DocumentConfig {
|
||||||
std::uint32_t width = 0;
|
std::uint32_t width = 0;
|
||||||
@@ -27,6 +29,10 @@ struct Layer {
|
|||||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AnimationFrame {
|
||||||
|
std::uint32_t duration_ms = 100;
|
||||||
|
};
|
||||||
|
|
||||||
class CanvasDocument {
|
class CanvasDocument {
|
||||||
public:
|
public:
|
||||||
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig config);
|
[[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 width() const noexcept;
|
||||||
[[nodiscard]] std::uint32_t height() const noexcept;
|
[[nodiscard]] std::uint32_t height() const noexcept;
|
||||||
[[nodiscard]] std::size_t active_layer_index() 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 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::Result<std::size_t> add_layer(std::string_view name);
|
||||||
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
|
[[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 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::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:
|
private:
|
||||||
std::uint32_t width_ = 0;
|
std::uint32_t width_ = 0;
|
||||||
std::uint32_t height_ = 0;
|
std::uint32_t height_ = 0;
|
||||||
std::size_t active_layer_index_ = 0;
|
std::size_t active_layer_index_ = 0;
|
||||||
|
std::size_t active_frame_index_ = 0;
|
||||||
std::vector<Layer> layers_;
|
std::vector<Layer> layers_;
|
||||||
|
std::vector<AnimationFrame> frames_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
using pp::document::CanvasDocument;
|
using pp::document::CanvasDocument;
|
||||||
using pp::document::DocumentConfig;
|
using pp::document::DocumentConfig;
|
||||||
using pp::document::max_canvas_dimension;
|
using pp::document::max_canvas_dimension;
|
||||||
|
using pp::document::max_frame_count;
|
||||||
using pp::document::max_layer_count;
|
using pp::document::max_layer_count;
|
||||||
using pp::foundation::StatusCode;
|
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()[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().layers()[1].name == std::string_view("Layer 2"));
|
||||||
PP_EXPECT(h, document.value().active_layer_index() == 0U);
|
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)
|
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);
|
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()
|
int main()
|
||||||
@@ -96,5 +154,7 @@ int main()
|
|||||||
harness.run("rejects_invalid_document_configs", rejects_invalid_document_configs);
|
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("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("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();
|
return harness.finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ int create_document(int argc, char** argv)
|
|||||||
<< ",\"height\":" << document.value().height()
|
<< ",\"height\":" << document.value().height()
|
||||||
<< ",\"layers\":" << document.value().layers().size()
|
<< ",\"layers\":" << document.value().layers().size()
|
||||||
<< ",\"activeLayer\":" << document.value().active_layer_index()
|
<< ",\"activeLayer\":" << document.value().active_layer_index()
|
||||||
|
<< ",\"frames\":" << document.value().frames().size()
|
||||||
|
<< ",\"activeFrame\":" << document.value().active_frame_index()
|
||||||
<< "}}\n";
|
<< "}}\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user