diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 928d266..3e376fd 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, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles 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 the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode 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`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | +| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode 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`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode 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 5dbea82..8aeeb2d 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -500,6 +500,9 @@ adapter continues execution. frame add, duplicate, remove, duration adjustment, timeline moves, timeline goto/next/previous, onion-size updates, frame selection, no-reload playback stepping, and play-mode toggles used by the live animation panel. +`pano_cli plan-animation-panel-action` exposes the higher-level animation panel +state/action planner for goto, next, previous, playback-step, and play-toggle +automation without requiring the legacy UI or canvas. Panel-control, timeline, selected-frame click, playback tick, and play-button toggle execution now dispatch through `DocumentAnimationServices` before the legacy `Canvas`/`Layer`/canvas-mode adapter continues. @@ -1205,7 +1208,8 @@ Results: 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, - playback toggle start/stop planning, non-mutating duration no-ops, and + playback toggle start/stop planning, animation panel action planning, + invalid panel timeline state rejection, 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`, @@ -1214,6 +1218,9 @@ Results: `pano_cli_plan_animation_operation_playback_smoke`, `pano_cli_plan_animation_operation_toggle_playback_start_smoke`, `pano_cli_plan_animation_operation_toggle_playback_stop_smoke`, + `pano_cli_plan_animation_panel_action_next_smoke`, + `pano_cli_plan_animation_panel_action_toggle_stop_smoke`, + `pano_cli_plan_animation_panel_action_rejects_bad_timeline`, `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. diff --git a/src/app_core/document_animation.h b/src/app_core/document_animation.h index a0eb41e..d73829a 100644 --- a/src/app_core/document_animation.h +++ b/src/app_core/document_animation.h @@ -25,6 +25,20 @@ enum class DocumentAnimationOperation { set_onion_size, }; +enum class DocumentAnimationPanelAction { + goto_frame, + next_frame, + previous_frame, + playback_step, + toggle_playback, +}; + +struct DocumentAnimationPanelState { + int total_duration = 1; + int current_frame = 0; + bool playback_active = false; +}; + struct DocumentAnimationOperationPlan { DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame; int frame_count = 1; @@ -380,6 +394,32 @@ public: return pp::foundation::Result::success(plan); } +[[nodiscard]] inline pp::foundation::Result plan_animation_panel_action( + DocumentAnimationPanelAction action, + const DocumentAnimationPanelState& state, + int target_frame = 0) +{ + switch (action) { + case DocumentAnimationPanelAction::goto_frame: + return plan_animation_goto_frame(state.total_duration, target_frame); + + case DocumentAnimationPanelAction::next_frame: + return plan_animation_step_frame(state.total_duration, state.current_frame, 1); + + case DocumentAnimationPanelAction::previous_frame: + return plan_animation_step_frame(state.total_duration, state.current_frame, -1); + + case DocumentAnimationPanelAction::playback_step: + return plan_animation_playback_step(state.total_duration, state.current_frame, 1); + + case DocumentAnimationPanelAction::toggle_playback: + return plan_animation_playback_toggle(state.playback_active); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown animation panel action")); +} + [[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan( const DocumentAnimationOperationPlan& plan) noexcept { diff --git a/src/node_panel_animation.cpp b/src/node_panel_animation.cpp index 5ecd033..1c0f91c 100644 --- a/src/node_panel_animation.cpp +++ b/src/node_panel_animation.cpp @@ -163,6 +163,15 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation LOG("Animation panel action failed: %s", status.message); } +pp::app::DocumentAnimationPanelState NodePanelAnimation::animation_panel_state() const +{ + return pp::app::DocumentAnimationPanelState { + .total_duration = Canvas::I->anim_duration(), + .current_frame = Canvas::I->m_anim_frame, + .playback_active = btn_play->is_active(), + }; +} + void NodePanelAnimation::init_controls() { m_layers_container = find("layers"); @@ -275,7 +284,10 @@ void NodePanelAnimation::init_controls() }; m_timeline->on_frame_changed = [this] (NodeAnimationTimeline* target, int frame) { - const auto plan = pp::app::plan_animation_goto_frame(Canvas::I->anim_duration(), frame); + const auto plan = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::goto_frame, + animation_panel_state(), + frame); if (!plan) return; LOG("goto frame %d", plan.value().target_frame); @@ -283,19 +295,25 @@ void NodePanelAnimation::init_controls() }; btn_next->on_click = [this] (Node* target) { - 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_panel_action( + pp::app::DocumentAnimationPanelAction::next_frame, + animation_panel_state()); if (!plan) return; execute_animation_plan(plan.value()); }; btn_prev->on_click = [this](Node* target) { - 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_panel_action( + pp::app::DocumentAnimationPanelAction::previous_frame, + animation_panel_state()); if (!plan) return; execute_animation_plan(plan.value()); }; btn_play->on_click = [this] (Node*) { - const auto plan = pp::app::plan_animation_playback_toggle(btn_play->is_active()); + const auto plan = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::toggle_playback, + animation_panel_state()); if (plan) execute_animation_plan(plan.value()); }; @@ -364,10 +382,9 @@ 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_playback_step( - Canvas::I->anim_duration(), - Canvas::I->m_anim_frame, - 1); + const auto plan = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::playback_step, + animation_panel_state()); if (plan) execute_animation_plan(plan.value()); } diff --git a/src/node_panel_animation.h b/src/node_panel_animation.h index 9aadc75..4b51027 100644 --- a/src/node_panel_animation.h +++ b/src/node_panel_animation.h @@ -10,6 +10,7 @@ class Layer; namespace pp::app { +struct DocumentAnimationPanelState; struct DocumentAnimationOperationPlan; } @@ -73,6 +74,7 @@ class NodePanelAnimation : public Node float m_playback_timer = 0; void execute_animation_plan(const pp::app::DocumentAnimationOperationPlan& plan, Layer* layer = nullptr); + [[nodiscard]] pp::app::DocumentAnimationPanelState animation_panel_state() const; public: using this_class = NodePanelAnimation; using parent = Node; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8f6e110..645d07b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1072,6 +1072,24 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"toggle-playback\".*\"playbackIdleMs\":100.*\"playbackWasActive\":true.*\"playbackActive\":false.*\"resetsPlaybackTimer\":false") + add_test(NAME pano_cli_plan_animation_panel_action_next_smoke + COMMAND pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4) + set_tests_properties(pano_cli_plan_animation_panel_action_next_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-panel-action\".*\"action\":\"next-frame\".*\"operation\":\"goto-next\".*\"targetFrame\":0") + + add_test(NAME pano_cli_plan_animation_panel_action_toggle_stop_smoke + COMMAND pano_cli plan-animation-panel-action --action toggle-playback --playing) + set_tests_properties(pano_cli_plan_animation_panel_action_toggle_stop_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-panel-action\".*\"action\":\"toggle-playback\".*\"operation\":\"toggle-playback\".*\"playbackIdleMs\":100.*\"playbackWasActive\":true.*\"playbackActive\":false") + + add_test(NAME pano_cli_plan_animation_panel_action_rejects_bad_timeline + COMMAND pano_cli plan-animation-panel-action --action next --total-duration 0 --current-frame 0) + set_tests_properties(pano_cli_plan_animation_panel_action_rejects_bad_timeline PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL 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 diff --git a/tests/app_core/document_animation_tests.cpp b/tests/app_core/document_animation_tests.cpp index fb0a3e9..2ecf51c 100644 --- a/tests/app_core/document_animation_tests.cpp +++ b/tests/app_core/document_animation_tests.cpp @@ -328,6 +328,91 @@ void playback_toggle_plans_capture_start_and_stop_intent(pp::tests::Harness& har PP_EXPECT(harness, stop.value().playback_idle_ms == 100); } +void panel_actions_plan_timeline_and_playback_intent(pp::tests::Harness& harness) +{ + const pp::app::DocumentAnimationPanelState state { + .total_duration = 5, + .current_frame = 4, + .playback_active = false, + }; + + const auto goto_frame = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::goto_frame, + state, + 2); + PP_REQUIRE(harness, goto_frame); + PP_EXPECT(harness, goto_frame.value().operation == pp::app::DocumentAnimationOperation::goto_frame); + PP_EXPECT(harness, goto_frame.value().target_frame == 2); + + const auto next = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::next_frame, + state); + PP_REQUIRE(harness, next); + PP_EXPECT(harness, next.value().operation == pp::app::DocumentAnimationOperation::goto_next); + PP_EXPECT(harness, next.value().target_frame == 0); + + const auto previous = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::previous_frame, + state); + PP_REQUIRE(harness, previous); + PP_EXPECT(harness, previous.value().operation == pp::app::DocumentAnimationOperation::goto_previous); + PP_EXPECT(harness, previous.value().target_frame == 3); + + const auto playback = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::playback_step, + state); + 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().reloads_animation_layers); + + const auto play_toggle = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::toggle_playback, + state); + PP_REQUIRE(harness, play_toggle); + PP_EXPECT(harness, play_toggle.value().operation == pp::app::DocumentAnimationOperation::toggle_playback); + PP_EXPECT(harness, play_toggle.value().playback_active); + PP_EXPECT(harness, play_toggle.value().playback_idle_ms == 10); +} + +void panel_actions_reject_invalid_timeline_state(pp::tests::Harness& harness) +{ + const pp::app::DocumentAnimationPanelState empty { + .total_duration = 0, + .current_frame = 0, + .playback_active = false, + }; + PP_EXPECT( + harness, + !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::goto_frame, empty, 0)); + PP_EXPECT( + harness, + !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::next_frame, empty)); + + const pp::app::DocumentAnimationPanelState out_of_range { + .total_duration = 3, + .current_frame = 3, + .playback_active = true, + }; + PP_EXPECT( + harness, + !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::playback_step, out_of_range)); + PP_EXPECT( + harness, + !pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::goto_frame, + out_of_range, + -1)); + + const auto stop_playback = pp::app::plan_animation_panel_action( + pp::app::DocumentAnimationPanelAction::toggle_playback, + out_of_range); + PP_REQUIRE(harness, stop_playback); + PP_EXPECT(harness, stop_playback.value().operation == pp::app::DocumentAnimationOperation::toggle_playback); + PP_EXPECT(harness, !stop_playback.value().playback_active); + PP_EXPECT(harness, stop_playback.value().playback_idle_ms == 100); +} + void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness) { const auto plan = pp::app::plan_animation_onion_size(2); @@ -502,6 +587,8 @@ int main() 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("playback toggle plans capture start and stop intent", playback_toggle_plans_capture_start_and_stop_intent); + harness.run("panel actions plan timeline and playback intent", panel_actions_plan_timeline_and_playback_intent); + harness.run("panel actions reject invalid timeline state", panel_actions_reject_invalid_timeline_state); 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 7ed0970..fbd84f3 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -297,6 +297,14 @@ struct PlanAnimationOperationArgs { bool playback_active = false; }; +struct PlanAnimationPanelActionArgs { + std::string action = "next"; + int total_duration = 1; + int current_frame = 0; + int target_frame = 0; + bool playback_active = false; +}; + struct PlanBrushOperationArgs { std::string kind = "settings"; std::string path; @@ -1003,6 +1011,52 @@ const char* document_animation_operation_name(pp::app::DocumentAnimationOperatio return "goto-frame"; } +const char* document_animation_panel_action_name(pp::app::DocumentAnimationPanelAction action) noexcept +{ + switch (action) { + case pp::app::DocumentAnimationPanelAction::goto_frame: + return "goto-frame"; + case pp::app::DocumentAnimationPanelAction::next_frame: + return "next-frame"; + case pp::app::DocumentAnimationPanelAction::previous_frame: + return "previous-frame"; + case pp::app::DocumentAnimationPanelAction::playback_step: + return "playback-step"; + case pp::app::DocumentAnimationPanelAction::toggle_playback: + return "toggle-playback"; + } + + return "goto-frame"; +} + +pp::foundation::Result parse_document_animation_panel_action( + std::string_view action) +{ + if (action == "goto" || action == "goto-frame") { + return pp::foundation::Result::success( + pp::app::DocumentAnimationPanelAction::goto_frame); + } + if (action == "next" || action == "next-frame") { + return pp::foundation::Result::success( + pp::app::DocumentAnimationPanelAction::next_frame); + } + if (action == "prev" || action == "previous" || action == "previous-frame") { + return pp::foundation::Result::success( + pp::app::DocumentAnimationPanelAction::previous_frame); + } + if (action == "playback" || action == "playback-step") { + return pp::foundation::Result::success( + pp::app::DocumentAnimationPanelAction::playback_step); + } + if (action == "toggle-playback" || action == "play-toggle") { + return pp::foundation::Result::success( + pp::app::DocumentAnimationPanelAction::toggle_playback); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown animation panel action")); +} + const char* brush_ui_texture_slot_name(pp::app::BrushUiTextureSlot slot) noexcept { switch (slot) { @@ -1557,6 +1611,7 @@ void print_help() << " 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|select|goto|next|prev|playback|toggle-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] [--playing]\n" + << " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\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" @@ -4010,6 +4065,101 @@ int plan_animation_operation(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_animation_panel_action_args( + int argc, + char** argv, + PlanAnimationPanelActionArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--action") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.action = argv[++i]; + } else if (key == "--playing") { + args.playback_active = true; + } else if (key == "--total-duration" || key == "--current-frame" || key == "--target-frame") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--total-duration") { + args.total_duration = value.value(); + } else if (key == "--current-frame") { + args.current_frame = value.value(); + } else { + args.target_frame = value.value(); + } + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_animation_panel_action(int argc, char** argv) +{ + PlanAnimationPanelActionArgs args; + const auto status = parse_plan_animation_panel_action_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-animation-panel-action", status.message); + return 2; + } + + const auto action = parse_document_animation_panel_action(args.action); + if (!action) { + print_error("plan-animation-panel-action", action.status().message); + return 2; + } + + const pp::app::DocumentAnimationPanelState state { + .total_duration = args.total_duration, + .current_frame = args.current_frame, + .playback_active = args.playback_active, + }; + const auto plan = pp::app::plan_animation_panel_action(action.value(), state, args.target_frame); + if (!plan) { + print_error("plan-animation-panel-action", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-animation-panel-action\"" + << ",\"state\":{\"action\":\"" << json_escape(args.action) + << "\",\"totalDuration\":" << args.total_duration + << ",\"currentFrame\":" << args.current_frame + << ",\"targetFrame\":" << args.target_frame + << ",\"playbackActive\":" << json_bool(args.playback_active) + << "},\"plan\":{\"action\":\"" << document_animation_panel_action_name(action.value()) + << "\",\"operation\":\"" << document_animation_operation_name(value.operation) + << "\",\"frameCount\":" << value.frame_count + << ",\"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 + << ",\"onionSize\":" << value.onion_size + << ",\"playbackIdleMs\":" << value.playback_idle_ms + << ",\"requiresSelectedFrame\":" << json_bool(value.requires_selected_frame) + << ",\"mutatesDocument\":" << json_bool(value.mutates_document) + << ",\"reloadsAnimationLayers\":" << json_bool(value.reloads_animation_layers) + << ",\"updatesCanvasAnimation\":" << json_bool(value.updates_canvas_animation) + << ",\"marksUnsaved\":" << json_bool(value.marks_unsaved) + << ",\"playbackWasActive\":" << json_bool(value.playback_was_active) + << ",\"playbackActive\":" << json_bool(value.playback_active) + << ",\"resetsPlaybackTimer\":" << json_bool(value.resets_playback_timer) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_brush_operation_args( int argc, char** argv, @@ -7247,6 +7397,10 @@ int main(int argc, char** argv) return plan_animation_operation(argc, argv); } + if (command == "plan-animation-panel-action") { + return plan_animation_panel_action(argc, argv); + } + if (command == "plan-brush-operation") { return plan_brush_operation(argc, argv); }