From 5752bc6ae97a3de073cc24f5fe52f14456f34148 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 16:39:14 +0200 Subject: [PATCH] Extend animation panel frame dispatch --- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 15 ++-- src/app_core/document_animation.h | 83 ++++++++++++++++++ src/node_panel_animation.cpp | 45 +++++++--- tests/CMakeLists.txt | 18 ++++ tests/app_core/document_animation_tests.cpp | 94 ++++++++++++++++++++- tools/pano_cli/main.cpp | 34 +++++++- 7 files changed, 269 insertions(+), 22 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index fba7cbe..b5cdd24 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -39,7 +39,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings | | DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing | | DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | -| DEBT-0022 | Open | Modernization | Animation panel frame command planning and panel-control/timeline execution dispatch now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but selected-frame click handling, playback/play-mode toggles, and the live adapter still mutate or read legacy `Canvas`/`Layer` frame state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer` adapter and UI nodes acting only as adapters or removed entirely | +| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel-control/timeline execution dispatch, selected-frame click dispatch, and playback tick stepping now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but play-mode toggles and the live adapter still mutate or read legacy `Canvas`/`Layer` frame state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer` adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, and the `BrushUiServices` boundary, but the live adapter still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings execution is owned by injected brush/app/asset/UI services with no legacy brush adapter | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c216097..d776aad 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -499,8 +499,9 @@ adapter continues execution. `pano_cli plan-animation-operation` exposes app-core planning for animation frame add, duplicate, remove, duration adjustment, timeline moves, timeline goto/next/previous, and onion-size updates used by the live animation panel -Panel-control and timeline execution now dispatch through -`DocumentAnimationServices` before the legacy `Canvas`/`Layer` adapter +plus frame selection and no-reload playback stepping. Panel-control, +timeline, selected-frame click, and playback tick execution now dispatch +through `DocumentAnimationServices` before the legacy `Canvas`/`Layer` adapter continues. `pano_cli plan-brush-operation` exposes app-core planning for brush color changes, tip/pattern/dual texture changes, preset brush replacement, and stroke @@ -1203,12 +1204,16 @@ Results: add/duplicate/remove planning, selected-frame rejection, last-frame remove rejection, duration floor/overflow handling, timeline move edge behavior, goto/next/previous wrapping, onion-size rejection, service dispatch ordering, + frame-click selection planning, no-reload playback step planning, non-mutating duration no-ops, and malformed execution payload rejection. - `pano_cli_plan_animation_operation_add_smoke`, `pano_cli_plan_animation_operation_duration_floor_smoke`, - `pano_cli_plan_animation_operation_next_wrap_smoke`, and - `pano_cli_plan_animation_operation_rejects_remove_last_frame` passed and - expose live animation-panel planning as JSON automation. + `pano_cli_plan_animation_operation_next_wrap_smoke`, + `pano_cli_plan_animation_operation_select_smoke`, + `pano_cli_plan_animation_operation_playback_smoke`, + `pano_cli_plan_animation_operation_rejects_remove_last_frame`, and + `pano_cli_plan_animation_operation_rejects_bad_selection` passed and expose + live animation-panel planning as JSON automation. - `pp_app_core_brush_ui_tests` passed, covering brush color channel validation, invalid color rejection, texture-path validation, preset-brush availability, preserve-current-color intent, stroke-settings refresh intent, service diff --git a/src/app_core/document_animation.h b/src/app_core/document_animation.h index d58531f..599f33d 100644 --- a/src/app_core/document_animation.h +++ b/src/app_core/document_animation.h @@ -16,9 +16,11 @@ enum class DocumentAnimationOperation { remove_frame, adjust_duration, move_frame, + select_frame, goto_frame, goto_next, goto_previous, + playback_step, set_onion_size, }; @@ -32,6 +34,8 @@ struct DocumentAnimationOperationPlan { int duration_delta = 0; int move_offset = 0; int onion_size = 1; + int layer_index = 0; + std::uint32_t layer_id = 0; bool requires_selected_frame = false; bool mutates_document = false; bool reloads_animation_layers = false; @@ -48,9 +52,13 @@ public: virtual void remove_frame(int selected_frame, int target_frame) = 0; virtual void set_frame_duration(int selected_frame, int duration) = 0; virtual int move_frame(int selected_frame, int move_offset) = 0; + virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0; + virtual void select_layer(int layer_index) = 0; virtual void goto_frame(int target_frame) = 0; + virtual void set_timeline_frame(int target_frame) = 0; virtual void set_onion_size(int onion_size) = 0; virtual void update_canvas_animation() = 0; + virtual void update_frame_status() = 0; virtual void reload_animation_layers() = 0; virtual void mark_unsaved() = 0; }; @@ -236,6 +244,34 @@ public: return pp::foundation::Result::success(plan); } +[[nodiscard]] inline pp::foundation::Result plan_animation_select_frame( + int frame_count, + int layer_index, + std::uint32_t layer_id, + int selected_frame) +{ + const auto index_status = validate_animation_frame_index(frame_count, selected_frame); + if (!index_status.ok()) { + return pp::foundation::Result::failure(index_status); + } + + if (layer_index < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("animation layer index must not be negative")); + } + + DocumentAnimationOperationPlan plan; + plan.operation = DocumentAnimationOperation::select_frame; + plan.frame_count = frame_count; + plan.selected_frame = selected_frame; + plan.target_frame = selected_frame; + plan.layer_index = layer_index; + plan.layer_id = layer_id; + plan.requires_selected_frame = true; + plan.updates_canvas_animation = true; + return pp::foundation::Result::success(plan); +} + [[nodiscard]] inline pp::foundation::Result plan_animation_goto_frame( int total_duration, int frame) @@ -290,6 +326,23 @@ public: return pp::foundation::Result::success(plan); } +[[nodiscard]] inline pp::foundation::Result plan_animation_playback_step( + int total_duration, + int current_frame, + int offset) +{ + const auto step = plan_animation_step_frame(total_duration, current_frame, offset); + if (!step) { + return pp::foundation::Result::failure(step.status()); + } + + auto plan = step.value(); + plan.operation = DocumentAnimationOperation::playback_step; + plan.move_offset = offset; + plan.reloads_animation_layers = false; + return pp::foundation::Result::success(plan); +} + [[nodiscard]] inline pp::foundation::Result plan_animation_onion_size(int onion_size) { if (onion_size < 0) { @@ -342,12 +395,28 @@ public: } return validate_animation_frame_index(plan.frame_count, plan.selected_frame); + case DocumentAnimationOperation::select_frame: + if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) { + return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state"); + } + { + const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame); + if (!index_status.ok()) { + return index_status; + } + } + return validate_animation_frame_index(plan.frame_count, plan.target_frame); + case DocumentAnimationOperation::goto_frame: case DocumentAnimationOperation::goto_next: case DocumentAnimationOperation::goto_previous: + case DocumentAnimationOperation::playback_step: if (!plan.updates_canvas_animation) { return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation"); } + if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) { + return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero"); + } return validate_animation_frame_index(plan.frame_count, plan.target_frame); case DocumentAnimationOperation::set_onion_size: @@ -434,6 +503,20 @@ public: } return pp::foundation::Status::success(); + case DocumentAnimationOperation::select_frame: + services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame); + if (plan.updates_canvas_animation) { + services.goto_frame(plan.target_frame); + } + services.select_layer(plan.layer_index); + return pp::foundation::Status::success(); + + case DocumentAnimationOperation::playback_step: + services.goto_frame(plan.target_frame); + services.set_timeline_frame(plan.target_frame); + services.update_frame_status(); + return pp::foundation::Status::success(); + case DocumentAnimationOperation::set_onion_size: services.set_onion_size(plan.onion_size); if (plan.updates_canvas_animation) { diff --git a/src/node_panel_animation.cpp b/src/node_panel_animation.cpp index 182973f..22d080e 100644 --- a/src/node_panel_animation.cpp +++ b/src/node_panel_animation.cpp @@ -69,11 +69,28 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation return panel_.m_selected_frame_index; } + void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) override + { + panel_.m_selected_frame_layer_id = layer_id; + panel_.m_selected_frame_index = selected_frame; + panel_.m_timeline->m_frame = selected_frame; + } + + void select_layer(int layer_index) override + { + App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(layer_index)); + } + void goto_frame(int target_frame) override { Canvas::I->anim_goto_frame(target_frame); } + void set_timeline_frame(int target_frame) override + { + panel_.m_timeline->m_frame = target_frame; + } + void set_onion_size(int onion_size) override { panel_.m_timeline->m_onion_size = onion_size; @@ -84,6 +101,11 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation Canvas::I->anim_update(); } + void update_frame_status() override + { + panel_.update_frames(); + } + void reload_animation_layers() override { panel_.load_layers(); @@ -288,11 +310,13 @@ void NodePanelAnimation::load_layers() m_selected_frame->set_active(false); frame->set_active(true); m_selected_frame = frame; - m_selected_frame_layer_id = lid; - m_selected_frame_index = fi; - m_timeline->m_frame = fi; - Canvas::I->anim_goto_frame(fi); - App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(i)); + const auto plan = pp::app::plan_animation_select_frame( + Canvas::I->m_layers[i]->frames_count(), + i, + lid, + fi); + if (plan) + execute_animation_plan(plan.value(), Canvas::I->m_layers[i].get()); }; } } @@ -317,13 +341,12 @@ void NodePanelAnimation::on_tick(float dt) if (m_playback_timer > (1.f / m_fps->get_float())) { m_playback_timer = 0; - const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, 1); + const auto plan = pp::app::plan_animation_playback_step( + Canvas::I->anim_duration(), + Canvas::I->m_anim_frame, + 1); if (plan) - { - Canvas::I->anim_goto_frame(plan.value().target_frame); - m_timeline->m_frame = Canvas::I->m_anim_frame; - update_frames(); - } + execute_animation_plan(plan.value()); } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 698e181..a6dfd65 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1048,12 +1048,30 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"goto-next\".*\"currentFrame\":4.*\"targetFrame\":0.*\"updatesCanvasAnimation\":true") + add_test(NAME pano_cli_plan_animation_operation_select_smoke + COMMAND pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42) + set_tests_properties(pano_cli_plan_animation_operation_select_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"select-frame\".*\"selectedFrame\":1.*\"targetFrame\":1.*\"layerIndex\":2.*\"layerId\":42.*\"reloadsAnimationLayers\":false.*\"updatesCanvasAnimation\":true") + + add_test(NAME pano_cli_plan_animation_operation_playback_smoke + COMMAND pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1) + set_tests_properties(pano_cli_plan_animation_operation_playback_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"playback-step\".*\"currentFrame\":4.*\"targetFrame\":0.*\"reloadsAnimationLayers\":false.*\"updatesCanvasAnimation\":true") + add_test(NAME pano_cli_plan_animation_operation_rejects_remove_last_frame COMMAND pano_cli plan-animation-operation --kind remove --frame-count 1 --selected-frame 0) set_tests_properties(pano_cli_plan_animation_operation_rejects_remove_last_frame PROPERTIES LABELS "app;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_animation_operation_rejects_bad_selection + COMMAND pano_cli plan-animation-operation --kind select --frame-count 2 --selected-frame 2 --layer-index 0 --layer-id 42) + set_tests_properties(pano_cli_plan_animation_operation_rejects_bad_selection PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_brush_operation_color_smoke COMMAND pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1) set_tests_properties(pano_cli_plan_brush_operation_color_smoke PROPERTIES diff --git a/tests/app_core/document_animation_tests.cpp b/tests/app_core/document_animation_tests.cpp index 7d72a49..05c550b 100644 --- a/tests/app_core/document_animation_tests.cpp +++ b/tests/app_core/document_animation_tests.cpp @@ -55,6 +55,22 @@ public: return move_result; } + void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) override + { + frame_selects += 1; + last_layer_id = layer_id; + last_layer_index = layer_index; + last_selected_frame = selected_frame; + call_order += "select-frame;"; + } + + void select_layer(int layer_index) override + { + layer_selects += 1; + last_layer_index = layer_index; + call_order += "select-layer;"; + } + void goto_frame(int target_frame) override { gotos += 1; @@ -62,6 +78,13 @@ public: call_order += "goto;"; } + void set_timeline_frame(int target_frame) override + { + timeline_sets += 1; + last_timeline_frame = target_frame; + call_order += "timeline;"; + } + void set_onion_size(int onion_size) override { onion_sets += 1; @@ -75,6 +98,12 @@ public: call_order += "update;"; } + void update_frame_status() override + { + frame_status_updates += 1; + call_order += "frame-status;"; + } + void reload_animation_layers() override { reloads += 1; @@ -92,16 +121,23 @@ public: int removes = 0; int duration_sets = 0; int moves = 0; + int frame_selects = 0; + int layer_selects = 0; int gotos = 0; + int timeline_sets = 0; int onion_sets = 0; int canvas_updates = 0; + int frame_status_updates = 0; int reloads = 0; int unsaved_marks = 0; int last_selected_frame = -1; int last_target_frame = -1; + int last_timeline_frame = -1; int last_duration = -1; int last_move_offset = 0; int last_onion_size = -1; + int last_layer_index = -1; + std::uint32_t last_layer_id = 0; int move_result = 0; std::string call_order; }; @@ -199,6 +235,34 @@ void move_and_timeline_plans_handle_edges(pp::tests::Harness& harness) PP_EXPECT(harness, !pp::app::plan_animation_step_frame(0, 0, 1)); } +void frame_selection_and_playback_plans_keep_ui_refresh_scoped(pp::tests::Harness& harness) +{ + const auto select = pp::app::plan_animation_select_frame(3, 2, 42, 1); + PP_REQUIRE(harness, select); + PP_EXPECT(harness, select.value().operation == pp::app::DocumentAnimationOperation::select_frame); + PP_EXPECT(harness, select.value().layer_index == 2); + PP_EXPECT(harness, select.value().layer_id == 42); + PP_EXPECT(harness, select.value().selected_frame == 1); + PP_EXPECT(harness, select.value().target_frame == 1); + PP_EXPECT(harness, select.value().updates_canvas_animation); + PP_EXPECT(harness, !select.value().reloads_animation_layers); + PP_EXPECT(harness, !select.value().mutates_document); + + const auto playback = pp::app::plan_animation_playback_step(4, 3, 1); + PP_REQUIRE(harness, playback); + PP_EXPECT(harness, playback.value().operation == pp::app::DocumentAnimationOperation::playback_step); + PP_EXPECT(harness, playback.value().target_frame == 0); + PP_EXPECT(harness, playback.value().move_offset == 1); + PP_EXPECT(harness, playback.value().updates_canvas_animation); + PP_EXPECT(harness, !playback.value().reloads_animation_layers); + PP_EXPECT(harness, !playback.value().mutates_document); + + PP_EXPECT(harness, !pp::app::plan_animation_select_frame(2, -1, 42, 0)); + PP_EXPECT(harness, !pp::app::plan_animation_select_frame(2, 0, 42, 2)); + PP_EXPECT(harness, !pp::app::plan_animation_playback_step(0, 0, 1)); + PP_EXPECT(harness, !pp::app::plan_animation_playback_step(3, 0, 0)); +} + void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness) { const auto plan = pp::app::plan_animation_onion_size(2); @@ -267,20 +331,37 @@ void executor_dispatches_timeline_and_parameter_operations(pp::tests::Harness& h PP_REQUIRE(harness, onion); PP_EXPECT(harness, pp::app::execute_animation_operation_plan(onion.value(), services).ok()); + const auto select = pp::app::plan_animation_select_frame(3, 2, 42, 1); + PP_REQUIRE(harness, select); + PP_EXPECT(harness, pp::app::execute_animation_operation_plan(select.value(), services).ok()); + + const auto playback = pp::app::plan_animation_playback_step(5, 4, 1); + PP_REQUIRE(harness, playback); + PP_EXPECT(harness, pp::app::execute_animation_operation_plan(playback.value(), services).ok()); + PP_EXPECT(harness, services.duration_sets == 1); PP_EXPECT(harness, services.last_duration == 5); PP_EXPECT(harness, services.moves == 1); PP_EXPECT(harness, services.last_move_offset == 1); - PP_EXPECT(harness, services.gotos == 2); + PP_EXPECT(harness, services.frame_selects == 1); + PP_EXPECT(harness, services.layer_selects == 1); + PP_EXPECT(harness, services.gotos == 4); + PP_EXPECT(harness, services.timeline_sets == 1); PP_EXPECT(harness, services.last_target_frame == 0); + PP_EXPECT(harness, services.last_timeline_frame == 0); + PP_EXPECT(harness, services.last_layer_index == 2); + PP_EXPECT(harness, services.last_layer_id == 42); PP_EXPECT(harness, services.onion_sets == 1); PP_EXPECT(harness, services.last_onion_size == 3); PP_EXPECT(harness, services.unsaved_marks == 2); PP_EXPECT(harness, services.canvas_updates == 2); + PP_EXPECT(harness, services.frame_status_updates == 1); PP_EXPECT(harness, services.reloads == 3); PP_EXPECT( harness, - services.call_order == "duration;unsaved;update;reload;move;unsaved;goto;reload;goto;reload;onion;update;"); + services.call_order + == "duration;unsaved;update;reload;move;unsaved;goto;reload;goto;reload;onion;update;" + "select-frame;goto;select-layer;goto;timeline;frame-status;"); } void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness) @@ -307,6 +388,14 @@ void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness) go.target_frame = 3; PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(go, services).ok()); + auto select = pp::app::plan_animation_select_frame(2, 0, 42, 1).value(); + select.layer_index = -1; + PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(select, services).ok()); + + auto playback = pp::app::plan_animation_playback_step(3, 1, 1).value(); + playback.move_offset = 0; + PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(playback, services).ok()); + PP_EXPECT(harness, services.adds == 0); PP_EXPECT(harness, services.duration_sets == 0); PP_EXPECT(harness, services.gotos == 0); @@ -321,6 +410,7 @@ int main() harness.run("add duplicate and remove validate frame bounds", add_duplicate_and_remove_validate_frame_bounds); harness.run("duration plans clamp floor and reject overflow", duration_plans_clamp_floor_and_reject_overflow); harness.run("move and timeline plans handle edges", move_and_timeline_plans_handle_edges); + harness.run("frame selection and playback plans keep ui refresh scoped", frame_selection_and_playback_plans_keep_ui_refresh_scoped); harness.run("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation); harness.run("executor dispatches mutating frame operations", executor_dispatches_mutating_frame_operations); harness.run("executor dispatches timeline and parameter operations", executor_dispatches_timeline_and_parameter_operations); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 7efa038..bfd5c9c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -288,6 +288,8 @@ struct PlanAnimationOperationArgs { int total_duration = 1; int current_frame = 0; int selected_frame = 0; + int layer_index = 0; + std::uint32_t layer_id = 0; int current_duration = 1; int delta = 1; int offset = 1; @@ -981,12 +983,16 @@ const char* document_animation_operation_name(pp::app::DocumentAnimationOperatio return "adjust-duration"; case pp::app::DocumentAnimationOperation::move_frame: return "move-frame"; + case pp::app::DocumentAnimationOperation::select_frame: + return "select-frame"; case pp::app::DocumentAnimationOperation::goto_frame: return "goto-frame"; case pp::app::DocumentAnimationOperation::goto_next: return "goto-next"; case pp::app::DocumentAnimationOperation::goto_previous: return "goto-previous"; + case pp::app::DocumentAnimationOperation::playback_step: + return "playback-step"; case pp::app::DocumentAnimationOperation::set_onion_size: return "set-onion-size"; } @@ -1547,7 +1553,7 @@ void print_help() << " plan-layer-rename --old-name NAME --new-name NAME\n" << " plan-layer-menu --command clear|rename|merge [--no-current-layer] [--current-index N] [--animation-duration N] [--current-name NAME] [--lower-name NAME]\n" << " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n" - << " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n" + << " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n" << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" @@ -3853,7 +3859,8 @@ pp::foundation::Status parse_plan_animation_operation_args( args.kind = argv[++i]; } else if (key == "--frame-count" || key == "--total-duration" || key == "--current-frame" || key == "--selected-frame" || key == "--current-duration" || key == "--delta" - || key == "--offset" || key == "--onion-size") { + || key == "--offset" || key == "--onion-size" || key == "--layer-index" + || key == "--layer-id") { if (i + 1 >= argc) { return pp::foundation::Status::invalid_argument("missing value for option"); } @@ -3875,8 +3882,15 @@ pp::foundation::Status parse_plan_animation_operation_args( args.delta = value.value(); } else if (key == "--offset") { args.offset = value.value(); - } else { + } else if (key == "--onion-size") { args.onion_size = value.value(); + } else if (key == "--layer-index") { + args.layer_index = value.value(); + } else { + if (value.value() < 0) { + return pp::foundation::Status::out_of_range("animation layer id must not be negative"); + } + args.layer_id = static_cast(value.value()); } } else { return pp::foundation::Status::invalid_argument("unknown option"); @@ -3908,6 +3922,13 @@ pp::foundation::Result make_animation_o if (args.kind == "move") { return pp::app::plan_animation_move_frame(args.frame_count, args.selected_frame, args.offset); } + if (args.kind == "select") { + return pp::app::plan_animation_select_frame( + args.frame_count, + args.layer_index, + args.layer_id, + args.selected_frame); + } if (args.kind == "goto") { return pp::app::plan_animation_goto_frame(args.total_duration, args.current_frame); } @@ -3917,6 +3938,9 @@ pp::foundation::Result make_animation_o if (args.kind == "prev") { return pp::app::plan_animation_step_frame(args.total_duration, args.current_frame, -1); } + if (args.kind == "playback") { + return pp::app::plan_animation_playback_step(args.total_duration, args.current_frame, args.offset); + } if (args.kind == "onion") { return pp::app::plan_animation_onion_size(args.onion_size); } @@ -3947,6 +3971,8 @@ int plan_animation_operation(int argc, char** argv) << ",\"totalDuration\":" << args.total_duration << ",\"currentFrame\":" << args.current_frame << ",\"selectedFrame\":" << args.selected_frame + << ",\"layerIndex\":" << args.layer_index + << ",\"layerId\":" << args.layer_id << ",\"currentDuration\":" << args.current_duration << ",\"delta\":" << args.delta << ",\"offset\":" << args.offset @@ -3956,6 +3982,8 @@ int plan_animation_operation(int argc, char** argv) << ",\"currentFrame\":" << value.current_frame << ",\"selectedFrame\":" << value.selected_frame << ",\"targetFrame\":" << value.target_frame + << ",\"layerIndex\":" << value.layer_index + << ",\"layerId\":" << value.layer_id << ",\"frameDuration\":" << value.frame_duration << ",\"durationDelta\":" << value.duration_delta << ",\"moveOffset\":" << value.move_offset