From 551013c771359419c8b0e630e84611563d9ba698 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 08:34:26 +0200 Subject: [PATCH] Add document layer metadata tests --- docs/modernization/roadmap.md | 4 +- src/document/document.cpp | 100 +++++++++++++++++++++++++++++- src/document/document.h | 5 ++ tests/document/document_tests.cpp | 63 +++++++++++++++++++ 4 files changed, 167 insertions(+), 5 deletions(-) diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 866fd4f..2073166 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -312,8 +312,8 @@ PPI header recognition, and a pure typed settings document model, with corrupt/truncated/unsupported and key/value limit tests. `pp_paint` has started with CPU reference math for the five current shader blend modes plus deterministic stroke spacing/interpolation. `pp_document` has -started with a pure canvas/layer/frame model and layer/frame/undo-redo history -invariant tests. `pp_renderer_api` has started with renderer-neutral +started with a pure canvas/layer/frame model, layer metadata operations, 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 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 4af2364..87ff5fa 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -1,6 +1,7 @@ #include "document/document.h" #include +#include namespace pp::document { @@ -32,6 +33,28 @@ namespace { return "Layer " + std::to_string(index + 1U); } +[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept +{ + if (name.empty()) { + return pp::foundation::Status::invalid_argument("layer name must not be empty"); + } + + if (name.size() > max_layer_name_length) { + return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept +{ + if (index >= layer_count) { + return pp::foundation::Status::out_of_range("layer index is outside the document"); + } + + return pp::foundation::Status::success(); +} + } pp::foundation::Result CanvasDocument::create(DocumentConfig config) @@ -91,7 +114,15 @@ pp::foundation::Result CanvasDocument::add_layer(std::string_view n } Layer layer; - layer.name = name.empty() ? default_layer_name(layers_.size()) : std::string(name); + if (name.empty()) { + layer.name = default_layer_name(layers_.size()); + } else { + const auto name_status = validate_layer_name(name); + if (!name_status.ok()) { + return pp::foundation::Result::failure(name_status); + } + layer.name = std::string(name); + } layers_.push_back(layer); active_layer_index_ = layers_.size() - 1U; return pp::foundation::Result::success(active_layer_index_); @@ -144,14 +175,77 @@ pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept { - if (index >= layers_.size()) { - return pp::foundation::Status::out_of_range("layer index is outside the document"); + const auto index_status = validate_layer_index(index, layers_.size()); + if (!index_status.ok()) { + return index_status; } active_layer_index_ = index; return pp::foundation::Status::success(); } +pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name) +{ + const auto index_status = validate_layer_index(index, layers_.size()); + if (!index_status.ok()) { + return index_status; + } + + const auto name_status = validate_layer_name(name); + if (!name_status.ok()) { + return name_status; + } + + layers_[index].name = std::string(name); + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept +{ + const auto index_status = validate_layer_index(index, layers_.size()); + if (!index_status.ok()) { + return index_status; + } + + layers_[index].visible = visible; + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept +{ + const auto index_status = validate_layer_index(index, layers_.size()); + if (!index_status.ok()) { + return index_status; + } + + if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) { + return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"); + } + + layers_[index].opacity = opacity; + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept +{ + const auto index_status = validate_layer_index(index, layers_.size()); + if (!index_status.ok()) { + return index_status; + } + + switch (blend_mode) { + case pp::paint::BlendMode::normal: + case pp::paint::BlendMode::multiply: + case pp::paint::BlendMode::screen: + case pp::paint::BlendMode::color_dodge: + case pp::paint::BlendMode::overlay: + layers_[index].blend_mode = blend_mode; + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("layer blend mode is not supported"); +} + pp::foundation::Result CanvasDocument::add_frame(std::uint32_t duration_ms) { if (frames_.size() >= max_frame_count) { diff --git a/src/document/document.h b/src/document/document.h index f0d2364..e9207b5 100644 --- a/src/document/document.h +++ b/src/document/document.h @@ -17,6 +17,7 @@ constexpr std::uint32_t max_frame_count = 100000; constexpr std::uint32_t min_frame_duration_ms = 1; constexpr std::size_t min_document_history_entries = 2; constexpr std::size_t max_document_history_entries = 10000; +constexpr std::size_t max_layer_name_length = 128; struct DocumentConfig { std::uint32_t width = 0; @@ -50,6 +51,10 @@ public: [[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::Status rename_layer(std::size_t index, std::string_view name); + [[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept; + [[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept; + [[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept; [[nodiscard]] pp::foundation::Result add_frame(std::uint32_t duration_ms); [[nodiscard]] pp::foundation::Result duplicate_frame(std::size_t index); diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp index a8ead15..bbde62f 100644 --- a/tests/document/document_tests.cpp +++ b/tests/document/document_tests.cpp @@ -1,8 +1,10 @@ #include "document/document.h" #include "test_harness.h" +#include #include +using pp::paint::BlendMode; using pp::document::CanvasDocument; using pp::document::DocumentHistory; using pp::document::DocumentConfig; @@ -10,6 +12,7 @@ using pp::document::max_document_history_entries; using pp::document::max_canvas_dimension; using pp::document::max_frame_count; using pp::document::max_layer_count; +using pp::document::max_layer_name_length; using pp::foundation::StatusCode; namespace { @@ -93,6 +96,64 @@ void moves_layers_and_preserves_active_layer_identity(pp::tests::Harness& h) PP_EXPECT(h, bad_move.code == StatusCode::out_of_range); } +void updates_layer_metadata(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 2 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + PP_EXPECT(h, document.rename_layer(1, "Ink").ok()); + PP_EXPECT(h, document.set_layer_visible(1, false).ok()); + PP_EXPECT(h, document.set_layer_opacity(1, 0.25F).ok()); + PP_EXPECT(h, document.set_layer_blend_mode(1, BlendMode::multiply).ok()); + + PP_EXPECT(h, document.layers()[1].name == std::string_view("Ink")); + PP_EXPECT(h, !document.layers()[1].visible); + PP_EXPECT(h, std::fabs(document.layers()[1].opacity - 0.25F) < 0.0001F); + PP_EXPECT(h, document.layers()[1].blend_mode == BlendMode::multiply); +} + +void rejects_invalid_layer_metadata(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 empty_name = document.rename_layer(0, ""); + const auto long_name = document.rename_layer(0, std::string(max_layer_name_length + 1U, 'x')); + const auto missing_name = document.rename_layer(4, "Missing"); + const auto bad_opacity_low = document.set_layer_opacity(0, -0.1F); + const auto bad_opacity_high = document.set_layer_opacity(0, 1.1F); + const auto bad_opacity_nan = document.set_layer_opacity(0, std::nanf("")); + const auto missing_visible = document.set_layer_visible(2, true); + const auto missing_blend = document.set_layer_blend_mode(2, BlendMode::normal); + const auto bad_blend = document.set_layer_blend_mode(0, static_cast(255)); + const auto bad_add_layer = document.add_layer(std::string(max_layer_name_length + 1U, 'x')); + + PP_EXPECT(h, !empty_name.ok()); + PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); + PP_EXPECT(h, !long_name.ok()); + PP_EXPECT(h, long_name.code == StatusCode::out_of_range); + PP_EXPECT(h, !missing_name.ok()); + PP_EXPECT(h, missing_name.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_opacity_low.ok()); + PP_EXPECT(h, bad_opacity_low.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_opacity_high.ok()); + PP_EXPECT(h, bad_opacity_high.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_opacity_nan.ok()); + PP_EXPECT(h, bad_opacity_nan.code == StatusCode::out_of_range); + PP_EXPECT(h, !missing_visible.ok()); + PP_EXPECT(h, missing_visible.code == StatusCode::out_of_range); + PP_EXPECT(h, !missing_blend.ok()); + PP_EXPECT(h, missing_blend.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_blend.ok()); + PP_EXPECT(h, bad_blend.code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_add_layer.ok()); + PP_EXPECT(h, bad_add_layer.status().code == StatusCode::out_of_range); +} + void manages_animation_frames_and_duration(pp::tests::Harness& h) { auto document_result = CanvasDocument::create( @@ -267,6 +328,8 @@ 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("updates_layer_metadata", updates_layer_metadata); + harness.run("rejects_invalid_layer_metadata", rejects_invalid_layer_metadata); harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration); harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations); harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);