From a9e12f2219c5476a9738db73cc80b17fbef47456 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 00:28:06 +0200 Subject: [PATCH] Route animation timeline scrubbing through app core --- docs/modernization/build-inventory.md | 2 + docs/modernization/debt.md | 7 +- docs/modernization/roadmap.md | 7 +- src/app_core/document_animation.h | 41 +++++++++++ src/node_panel_animation.cpp | 7 +- tests/CMakeLists.txt | 18 +++++ tests/app_core/document_animation_tests.cpp | 38 ++++++++++ tools/pano_cli/main.cpp | 79 +++++++++++++++++++++ 8 files changed, 194 insertions(+), 5 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 37ab759..346ad79 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -248,6 +248,8 @@ Known local toolchain state: onion-size updates, and play-mode toggles. It keeps those live paths on the `pp_app_core` contracts. `NodeCanvas` onion-skin panorama drawing now also consumes the tested `pp_app_core` onion frame range and alpha falloff helper, + and `NodeAnimationTimeline` mouse scrubbing consumes tested `pp_app_core` + cursor-to-frame planning exposed through `pano_cli plan-animation-timeline-scrub`, while legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and the temporary `NodePanelAnimation` friend adapter remain tracked by `DEBT-0022`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 10bb0ca..6de1c96 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -38,7 +38,10 @@ agent or engineer to remove them without reconstructing context from chat. - 2026-06-05: DEBT-0022 was narrowed. `pp_app_core` now owns tested onion-skin frame range and alpha falloff planning, and live `NodeCanvas` panorama drawing consumes that helper instead of open-coding frame clamping - and opacity falloff in the render loop. Legacy canvas/layer/UI execution + and opacity falloff in the render loop. Later on 2026-06-05, animation + timeline mouse scrubbing also moved to tested `pp_app_core` planning with + `pano_cli plan-animation-timeline-scrub` coverage, so `NodeAnimationTimeline` + no longer owns cursor-to-frame clamp policy. Legacy canvas/layer/UI execution remains open under DEBT-0022. - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, thumbnail, and object-draw history paths now query saved blend state through @@ -75,7 +78,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, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history through the history bridge | 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 planning/execution dispatch and layer panel operation planning/execution dispatch now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | 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 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`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, timeline scrub 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`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `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, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset command boundary and asset-managed texture/preset 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`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, or brush-panel friend access | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | 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; live execution is centralized in `src/legacy_quick_ui_services.*`, but the bridge 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 23aa77b..089b722 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1698,7 +1698,9 @@ Results: playback toggle start/stop planning, animation panel action planning, invalid panel timeline state rejection, non-mutating duration no-ops, tested onion-skin frame range/alpha falloff planning consumed by live `NodeCanvas` - panorama drawing, and malformed execution payload rejection. + panorama drawing, tested timeline mouse-scrub cursor-to-frame planning + consumed by live `NodeAnimationTimeline`, 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`, @@ -1709,6 +1711,9 @@ Results: `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_timeline_scrub_smoke`, + `pano_cli_plan_animation_timeline_scrub_clamps_left`, + `pano_cli_plan_animation_timeline_scrub_rejects_bad_duration`, `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 96e42f1..9473187 100644 --- a/src/app_core/document_animation.h +++ b/src/app_core/document_animation.h @@ -3,6 +3,7 @@ #include "foundation/result.h" #include +#include #include #include @@ -70,6 +71,15 @@ struct DocumentAnimationOnionFrameRange { int last_frame = 0; }; +inline constexpr float document_animation_timeline_frame_width = 35.0F; + +struct DocumentAnimationTimelineScrubPlan { + int total_duration = 1; + float cursor_x = 0.0F; + float frame_width = document_animation_timeline_frame_width; + int target_frame = 0; +}; + class DocumentAnimationServices { public: virtual ~DocumentAnimationServices() = default; @@ -162,6 +172,37 @@ public: }); } +[[nodiscard]] inline pp::foundation::Result plan_animation_timeline_scrub( + int total_duration, + float cursor_x, + float frame_width = document_animation_timeline_frame_width) +{ + if (total_duration <= 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("animation timeline duration must be greater than zero")); + } + + if (!std::isfinite(cursor_x)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("animation timeline cursor position must be finite")); + } + + if (!std::isfinite(frame_width) || frame_width <= 0.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("animation timeline frame width must be positive and finite")); + } + + const auto raw_frame = static_cast(std::floor(cursor_x / frame_width)); + const auto target_frame = std::clamp(raw_frame, 0, total_duration - 1); + return pp::foundation::Result::success( + DocumentAnimationTimelineScrubPlan { + .total_duration = total_duration, + .cursor_x = cursor_x, + .frame_width = frame_width, + .target_frame = static_cast(target_frame), + }); +} + [[nodiscard]] inline float animation_onion_frame_alpha( const DocumentAnimationOnionFrameRange& range, int frame) noexcept diff --git a/src/node_panel_animation.cpp b/src/node_panel_animation.cpp index 27d55cc..833d8fe 100644 --- a/src/node_panel_animation.cpp +++ b/src/node_panel_animation.cpp @@ -347,10 +347,13 @@ kEventResult NodeAnimationTimeline::handle_event(Event* e) parent::handle_event(e); static int signaled_frame = -1; auto me = static_cast(e); - auto ge = static_cast(e); auto update = [&](){ auto loc = me->m_pos - m_pos; - m_frame = glm::clamp((int)glm::floor(loc.x / 35.f), 0, Canvas::I->anim_duration() - 1); + const int total_duration = Canvas::I ? Canvas::I->anim_duration() : 0; + const auto scrub = pp::app::plan_animation_timeline_scrub(total_duration, loc.x); + if (!scrub) + return; + m_frame = scrub.value().target_frame; if (on_frame_changed && signaled_frame != m_frame) on_frame_changed(this, m_frame); signaled_frame = m_frame; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2c7c69b..76c84a4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1268,6 +1268,24 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_animation_timeline_scrub_smoke + COMMAND pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99) + set_tests_properties(pano_cli_plan_animation_timeline_scrub_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-timeline-scrub\".*\"totalDuration\":5.*\"cursorX\":174.99.*\"frameWidth\":35.*\"targetFrame\":4") + + add_test(NAME pano_cli_plan_animation_timeline_scrub_clamps_left + COMMAND pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x -1) + set_tests_properties(pano_cli_plan_animation_timeline_scrub_clamps_left PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-timeline-scrub\".*\"cursorX\":-1.*\"targetFrame\":0") + + add_test(NAME pano_cli_plan_animation_timeline_scrub_rejects_bad_duration + COMMAND pano_cli plan-animation-timeline-scrub --total-duration 0 --cursor-x 0) + set_tests_properties(pano_cli_plan_animation_timeline_scrub_rejects_bad_duration PROPERTIES + LABELS "app;ui;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 4d88255..e2e6312 100644 --- a/tests/app_core/document_animation_tests.cpp +++ b/tests/app_core/document_animation_tests.cpp @@ -466,6 +466,42 @@ void onion_frame_ranges_reject_invalid_inputs(pp::tests::Harness& harness) PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, 1, -1)); } +void timeline_scrub_clamps_cursor_to_document_frame(pp::tests::Harness& harness) +{ + const auto left = pp::app::plan_animation_timeline_scrub(5, -0.01F); + PP_REQUIRE(harness, left); + PP_EXPECT(harness, left.value().target_frame == 0); + + const auto first = pp::app::plan_animation_timeline_scrub(5, 0.0F); + PP_REQUIRE(harness, first); + PP_EXPECT(harness, first.value().target_frame == 0); + + const auto second = pp::app::plan_animation_timeline_scrub(5, 35.0F); + PP_REQUIRE(harness, second); + PP_EXPECT(harness, second.value().target_frame == 1); + + const auto inside_last = pp::app::plan_animation_timeline_scrub(5, 174.99F); + PP_REQUIRE(harness, inside_last); + PP_EXPECT(harness, inside_last.value().target_frame == 4); + + const auto far_right = pp::app::plan_animation_timeline_scrub(5, 10000.0F); + PP_REQUIRE(harness, far_right); + PP_EXPECT(harness, far_right.value().target_frame == 4); + + const auto custom_width = pp::app::plan_animation_timeline_scrub(4, 21.0F, 10.0F); + PP_REQUIRE(harness, custom_width); + PP_EXPECT(harness, custom_width.value().target_frame == 2); +} + +void timeline_scrub_rejects_invalid_inputs(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(0, 0.0F)); + PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, std::numeric_limits::infinity())); + PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, 0.0F)); + PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, -1.0F)); + PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, std::numeric_limits::infinity())); +} + void executor_dispatches_mutating_frame_operations(pp::tests::Harness& harness) { FakeDocumentAnimationServices services; @@ -632,6 +668,8 @@ int main() harness.run("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation); harness.run("onion frame ranges clamp edges and alpha", onion_frame_ranges_clamp_edges_and_alpha); harness.run("onion frame ranges reject invalid inputs", onion_frame_ranges_reject_invalid_inputs); + harness.run("timeline scrub clamps cursor to document frame", timeline_scrub_clamps_cursor_to_document_frame); + harness.run("timeline scrub rejects invalid inputs", timeline_scrub_rejects_invalid_inputs); 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); harness.run("executor rejects malformed animation plans", executor_rejects_malformed_animation_plans); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 5835d72..1da03d8 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -346,6 +346,12 @@ struct PlanAnimationPanelActionArgs { bool playback_active = false; }; +struct PlanAnimationTimelineScrubArgs { + int total_duration = 1; + float cursor_x = 0.0F; + float frame_width = pp::app::document_animation_timeline_frame_width; +}; + struct PlanBrushOperationArgs { std::string kind = "settings"; std::string path; @@ -1922,6 +1928,7 @@ void print_help() << " 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-animation-timeline-scrub [--total-duration N] [--cursor-x N] [--frame-width 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-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n" << " plan-brush-preset-list --kind add|remove|move|up|down|select|clear [--item-count N] [--current-index N] [--offset N] [--no-current-brush]\n" @@ -4827,6 +4834,74 @@ int plan_animation_panel_action(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_animation_timeline_scrub_args( + int argc, + char** argv, + PlanAnimationTimelineScrubArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--total-duration") { + 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(); + } + args.total_duration = value.value(); + } else if (key == "--cursor-x" || key == "--frame-width") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--cursor-x") { + args.cursor_x = value.value(); + } else { + args.frame_width = value.value(); + } + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_animation_timeline_scrub(int argc, char** argv) +{ + PlanAnimationTimelineScrubArgs args; + const auto status = parse_plan_animation_timeline_scrub_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-animation-timeline-scrub", status.message); + return 2; + } + + const auto plan = pp::app::plan_animation_timeline_scrub( + args.total_duration, + args.cursor_x, + args.frame_width); + if (!plan) { + print_error("plan-animation-timeline-scrub", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-animation-timeline-scrub\"" + << ",\"state\":{\"totalDuration\":" << args.total_duration + << ",\"cursorX\":" << args.cursor_x + << ",\"frameWidth\":" << args.frame_width + << "},\"plan\":{\"totalDuration\":" << value.total_duration + << ",\"cursorX\":" << value.cursor_x + << ",\"frameWidth\":" << value.frame_width + << ",\"targetFrame\":" << value.target_frame + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_brush_operation_args( int argc, char** argv, @@ -8970,6 +9045,10 @@ int main(int argc, char** argv) return plan_animation_panel_action(argc, argv); } + if (command == "plan-animation-timeline-scrub") { + return plan_animation_timeline_scrub(argc, argv); + } + if (command == "plan-brush-operation") { return plan_brush_operation(argc, argv); }