Add animation playback toggle boundary

This commit is contained in:
2026-06-03 16:47:02 +02:00
parent 5752bc6ae9
commit 603bb0c4e7
7 changed files with 217 additions and 26 deletions

View File

@@ -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-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-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-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, 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-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-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-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-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 | | 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 |

View File

@@ -498,11 +498,11 @@ dispatch through `DocumentLayerMenuServices` before the legacy canvas/layer UI
adapter continues execution. adapter continues execution.
`pano_cli plan-animation-operation` exposes app-core planning for animation `pano_cli plan-animation-operation` exposes app-core planning for animation
frame add, duplicate, remove, duration adjustment, timeline moves, timeline frame add, duplicate, remove, duration adjustment, timeline moves, timeline
goto/next/previous, and onion-size updates used by the live animation panel goto/next/previous, onion-size updates, frame selection, no-reload playback
plus frame selection and no-reload playback stepping. Panel-control, stepping, and play-mode toggles used by the live animation panel.
timeline, selected-frame click, and playback tick execution now dispatch Panel-control, timeline, selected-frame click, playback tick, and play-button
through `DocumentAnimationServices` before the legacy `Canvas`/`Layer` adapter toggle execution now dispatch through `DocumentAnimationServices` before the
continues. legacy `Canvas`/`Layer`/canvas-mode adapter continues.
`pano_cli plan-brush-operation` exposes app-core planning for brush color `pano_cli plan-brush-operation` exposes app-core planning for brush color
changes, tip/pattern/dual texture changes, preset brush replacement, and stroke changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel settings refreshes used by the live brush, quick, color, and floating panel
@@ -1205,12 +1205,15 @@ Results:
rejection, duration floor/overflow handling, timeline move edge behavior, rejection, duration floor/overflow handling, timeline move edge behavior,
goto/next/previous wrapping, onion-size rejection, service dispatch ordering, goto/next/previous wrapping, onion-size rejection, service dispatch ordering,
frame-click selection planning, no-reload playback step planning, frame-click selection planning, no-reload playback step planning,
non-mutating duration no-ops, and malformed execution payload rejection. playback toggle start/stop planning, non-mutating duration no-ops, and
malformed execution payload rejection.
- `pano_cli_plan_animation_operation_add_smoke`, - `pano_cli_plan_animation_operation_add_smoke`,
`pano_cli_plan_animation_operation_duration_floor_smoke`, `pano_cli_plan_animation_operation_duration_floor_smoke`,
`pano_cli_plan_animation_operation_next_wrap_smoke`, `pano_cli_plan_animation_operation_next_wrap_smoke`,
`pano_cli_plan_animation_operation_select_smoke`, `pano_cli_plan_animation_operation_select_smoke`,
`pano_cli_plan_animation_operation_playback_smoke`, `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_operation_rejects_remove_last_frame`, and `pano_cli_plan_animation_operation_rejects_remove_last_frame`, and
`pano_cli_plan_animation_operation_rejects_bad_selection` passed and expose `pano_cli_plan_animation_operation_rejects_bad_selection` passed and expose
live animation-panel planning as JSON automation. live animation-panel planning as JSON automation.

View File

@@ -21,6 +21,7 @@ enum class DocumentAnimationOperation {
goto_next, goto_next,
goto_previous, goto_previous,
playback_step, playback_step,
toggle_playback,
set_onion_size, set_onion_size,
}; };
@@ -36,11 +37,15 @@ struct DocumentAnimationOperationPlan {
int onion_size = 1; int onion_size = 1;
int layer_index = 0; int layer_index = 0;
std::uint32_t layer_id = 0; std::uint32_t layer_id = 0;
int playback_idle_ms = 100;
bool requires_selected_frame = false; bool requires_selected_frame = false;
bool mutates_document = false; bool mutates_document = false;
bool reloads_animation_layers = false; bool reloads_animation_layers = false;
bool updates_canvas_animation = false; bool updates_canvas_animation = false;
bool marks_unsaved = false; bool marks_unsaved = false;
bool playback_was_active = false;
bool playback_active = false;
bool resets_playback_timer = false;
}; };
class DocumentAnimationServices { class DocumentAnimationServices {
@@ -57,6 +62,12 @@ public:
virtual void goto_frame(int target_frame) = 0; virtual void goto_frame(int target_frame) = 0;
virtual void set_timeline_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 set_onion_size(int onion_size) = 0;
virtual void capture_playback_restore_mode() = 0;
virtual void enter_playback_camera_mode() = 0;
virtual void restore_playback_canvas_mode() = 0;
virtual void set_playback_active(bool active) = 0;
virtual void reset_playback_timer() = 0;
virtual void set_playback_idle_ms(int idle_ms) = 0;
virtual void update_canvas_animation() = 0; virtual void update_canvas_animation() = 0;
virtual void update_frame_status() = 0; virtual void update_frame_status() = 0;
virtual void reload_animation_layers() = 0; virtual void reload_animation_layers() = 0;
@@ -343,6 +354,18 @@ public:
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan); return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
} }
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
bool playback_active)
{
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::toggle_playback;
plan.playback_was_active = playback_active;
plan.playback_active = !playback_active;
plan.playback_idle_ms = playback_active ? 100 : 10;
plan.resets_playback_timer = !playback_active;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size) [[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
{ {
if (onion_size < 0) { if (onion_size < 0) {
@@ -419,6 +442,18 @@ public:
} }
return validate_animation_frame_index(plan.frame_count, plan.target_frame); return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active == plan.playback_was_active) {
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
}
if (plan.playback_idle_ms <= 0) {
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
}
if (plan.playback_active && !plan.resets_playback_timer) {
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size: case DocumentAnimationOperation::set_onion_size:
if (plan.onion_size < 0) { if (plan.onion_size < 0) {
return pp::foundation::Status::invalid_argument("animation onion size must not be negative"); return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
@@ -517,6 +552,20 @@ public:
services.update_frame_status(); services.update_frame_status();
return pp::foundation::Status::success(); return pp::foundation::Status::success();
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active) {
services.capture_playback_restore_mode();
services.enter_playback_camera_mode();
if (plan.resets_playback_timer) {
services.reset_playback_timer();
}
} else {
services.restore_playback_canvas_mode();
}
services.set_playback_active(plan.playback_active);
services.set_playback_idle_ms(plan.playback_idle_ms);
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size: case DocumentAnimationOperation::set_onion_size:
services.set_onion_size(plan.onion_size); services.set_onion_size(plan.onion_size);
if (plan.updates_canvas_animation) { if (plan.updates_canvas_animation) {

View File

@@ -96,6 +96,36 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation
panel_.m_timeline->m_onion_size = onion_size; panel_.m_timeline->m_onion_size = onion_size;
} }
void capture_playback_restore_mode() override
{
playback_restore_mode() = Canvas::I->m_current_mode;
}
void enter_playback_camera_mode() override
{
Canvas::set_mode(kCanvasMode::Camera);
}
void restore_playback_canvas_mode() override
{
Canvas::set_mode(playback_restore_mode());
}
void set_playback_active(bool active) override
{
panel_.btn_play->set_active(active);
}
void reset_playback_timer() override
{
panel_.m_playback_timer = 0;
}
void set_playback_idle_ms(int idle_ms) override
{
App::I->idle_ms = idle_ms;
}
void update_canvas_animation() override void update_canvas_animation() override
{ {
Canvas::I->anim_update(); Canvas::I->anim_update();
@@ -117,6 +147,12 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation
} }
private: private:
static kCanvasMode& playback_restore_mode()
{
static auto mode = Canvas::I->m_current_mode;
return mode;
}
NodePanelAnimation& panel_; NodePanelAnimation& panel_;
Layer* layer_ = nullptr; Layer* layer_ = nullptr;
}; };
@@ -258,23 +294,10 @@ void NodePanelAnimation::init_controls()
return; return;
execute_animation_plan(plan.value()); execute_animation_plan(plan.value());
}; };
btn_play->on_click = [this] (Node* target) { btn_play->on_click = [this] (Node*) {
static auto mode = Canvas::I->m_current_mode; const auto plan = pp::app::plan_animation_playback_toggle(btn_play->is_active());
auto b = static_cast<NodeButtonCustom*>(target); if (plan)
if (b->is_active()) execute_animation_plan(plan.value());
{
Canvas::set_mode(mode);
b->set_active(false);
App::I->idle_ms = 100;
}
else
{
mode = Canvas::I->m_current_mode;
Canvas::set_mode(kCanvasMode::Camera);
m_playback_timer = 0;
b->set_active(true);
App::I->idle_ms = 10;
}
}; };
} }

View File

@@ -1060,6 +1060,18 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"playback-step\".*\"currentFrame\":4.*\"targetFrame\":0.*\"reloadsAnimationLayers\":false.*\"updatesCanvasAnimation\":true") 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_toggle_playback_start_smoke
COMMAND pano_cli plan-animation-operation --kind toggle-playback)
set_tests_properties(pano_cli_plan_animation_operation_toggle_playback_start_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"toggle-playback\".*\"playbackIdleMs\":10.*\"playbackWasActive\":false.*\"playbackActive\":true.*\"resetsPlaybackTimer\":true")
add_test(NAME pano_cli_plan_animation_operation_toggle_playback_stop_smoke
COMMAND pano_cli plan-animation-operation --kind toggle-playback --playing)
set_tests_properties(pano_cli_plan_animation_operation_toggle_playback_stop_smoke PROPERTIES
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_operation_rejects_remove_last_frame 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) 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 set_tests_properties(pano_cli_plan_animation_operation_rejects_remove_last_frame PROPERTIES

View File

@@ -92,6 +92,44 @@ public:
call_order += "onion;"; call_order += "onion;";
} }
void capture_playback_restore_mode() override
{
playback_restore_captures += 1;
call_order += "capture-mode;";
}
void enter_playback_camera_mode() override
{
playback_camera_entries += 1;
call_order += "camera-mode;";
}
void restore_playback_canvas_mode() override
{
playback_mode_restores += 1;
call_order += "restore-mode;";
}
void set_playback_active(bool active) override
{
playback_active_sets += 1;
last_playback_active = active;
call_order += active ? "play-active;" : "play-inactive;";
}
void reset_playback_timer() override
{
playback_timer_resets += 1;
call_order += "reset-timer;";
}
void set_playback_idle_ms(int idle_ms) override
{
playback_idle_sets += 1;
last_playback_idle_ms = idle_ms;
call_order += "idle;";
}
void update_canvas_animation() override void update_canvas_animation() override
{ {
canvas_updates += 1; canvas_updates += 1;
@@ -126,6 +164,12 @@ public:
int gotos = 0; int gotos = 0;
int timeline_sets = 0; int timeline_sets = 0;
int onion_sets = 0; int onion_sets = 0;
int playback_restore_captures = 0;
int playback_camera_entries = 0;
int playback_mode_restores = 0;
int playback_active_sets = 0;
int playback_timer_resets = 0;
int playback_idle_sets = 0;
int canvas_updates = 0; int canvas_updates = 0;
int frame_status_updates = 0; int frame_status_updates = 0;
int reloads = 0; int reloads = 0;
@@ -137,7 +181,9 @@ public:
int last_move_offset = 0; int last_move_offset = 0;
int last_onion_size = -1; int last_onion_size = -1;
int last_layer_index = -1; int last_layer_index = -1;
int last_playback_idle_ms = 0;
std::uint32_t last_layer_id = 0; std::uint32_t last_layer_id = 0;
bool last_playback_active = false;
int move_result = 0; int move_result = 0;
std::string call_order; std::string call_order;
}; };
@@ -263,6 +309,25 @@ void frame_selection_and_playback_plans_keep_ui_refresh_scoped(pp::tests::Harnes
PP_EXPECT(harness, !pp::app::plan_animation_playback_step(3, 0, 0)); PP_EXPECT(harness, !pp::app::plan_animation_playback_step(3, 0, 0));
} }
void playback_toggle_plans_capture_start_and_stop_intent(pp::tests::Harness& harness)
{
const auto start = pp::app::plan_animation_playback_toggle(false);
PP_REQUIRE(harness, start);
PP_EXPECT(harness, start.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
PP_EXPECT(harness, !start.value().playback_was_active);
PP_EXPECT(harness, start.value().playback_active);
PP_EXPECT(harness, start.value().resets_playback_timer);
PP_EXPECT(harness, start.value().playback_idle_ms == 10);
const auto stop = pp::app::plan_animation_playback_toggle(true);
PP_REQUIRE(harness, stop);
PP_EXPECT(harness, stop.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
PP_EXPECT(harness, stop.value().playback_was_active);
PP_EXPECT(harness, !stop.value().playback_active);
PP_EXPECT(harness, !stop.value().resets_playback_timer);
PP_EXPECT(harness, stop.value().playback_idle_ms == 100);
}
void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness) void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness)
{ {
const auto plan = pp::app::plan_animation_onion_size(2); const auto plan = pp::app::plan_animation_onion_size(2);
@@ -339,6 +404,14 @@ void executor_dispatches_timeline_and_parameter_operations(pp::tests::Harness& h
PP_REQUIRE(harness, playback); PP_REQUIRE(harness, playback);
PP_EXPECT(harness, pp::app::execute_animation_operation_plan(playback.value(), services).ok()); PP_EXPECT(harness, pp::app::execute_animation_operation_plan(playback.value(), services).ok());
const auto play_start = pp::app::plan_animation_playback_toggle(false);
PP_REQUIRE(harness, play_start);
PP_EXPECT(harness, pp::app::execute_animation_operation_plan(play_start.value(), services).ok());
const auto play_stop = pp::app::plan_animation_playback_toggle(true);
PP_REQUIRE(harness, play_stop);
PP_EXPECT(harness, pp::app::execute_animation_operation_plan(play_stop.value(), services).ok());
PP_EXPECT(harness, services.duration_sets == 1); PP_EXPECT(harness, services.duration_sets == 1);
PP_EXPECT(harness, services.last_duration == 5); PP_EXPECT(harness, services.last_duration == 5);
PP_EXPECT(harness, services.moves == 1); PP_EXPECT(harness, services.moves == 1);
@@ -357,11 +430,20 @@ void executor_dispatches_timeline_and_parameter_operations(pp::tests::Harness& h
PP_EXPECT(harness, services.canvas_updates == 2); PP_EXPECT(harness, services.canvas_updates == 2);
PP_EXPECT(harness, services.frame_status_updates == 1); PP_EXPECT(harness, services.frame_status_updates == 1);
PP_EXPECT(harness, services.reloads == 3); PP_EXPECT(harness, services.reloads == 3);
PP_EXPECT(harness, services.playback_restore_captures == 1);
PP_EXPECT(harness, services.playback_camera_entries == 1);
PP_EXPECT(harness, services.playback_mode_restores == 1);
PP_EXPECT(harness, services.playback_active_sets == 2);
PP_EXPECT(harness, services.playback_timer_resets == 1);
PP_EXPECT(harness, services.playback_idle_sets == 2);
PP_EXPECT(harness, !services.last_playback_active);
PP_EXPECT(harness, services.last_playback_idle_ms == 100);
PP_EXPECT( PP_EXPECT(
harness, harness,
services.call_order services.call_order
== "duration;unsaved;update;reload;move;unsaved;goto;reload;goto;reload;onion;update;" == "duration;unsaved;update;reload;move;unsaved;goto;reload;goto;reload;onion;update;"
"select-frame;goto;select-layer;goto;timeline;frame-status;"); "select-frame;goto;select-layer;goto;timeline;frame-status;"
"capture-mode;camera-mode;reset-timer;play-active;idle;restore-mode;play-inactive;idle;");
} }
void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness) void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness)
@@ -396,6 +478,14 @@ void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness)
playback.move_offset = 0; playback.move_offset = 0;
PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(playback, services).ok()); PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(playback, services).ok());
auto toggle = pp::app::plan_animation_playback_toggle(false).value();
toggle.playback_active = false;
PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(toggle, services).ok());
toggle = pp::app::plan_animation_playback_toggle(false).value();
toggle.playback_idle_ms = 0;
PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(toggle, services).ok());
PP_EXPECT(harness, services.adds == 0); PP_EXPECT(harness, services.adds == 0);
PP_EXPECT(harness, services.duration_sets == 0); PP_EXPECT(harness, services.duration_sets == 0);
PP_EXPECT(harness, services.gotos == 0); PP_EXPECT(harness, services.gotos == 0);
@@ -411,6 +501,7 @@ int main()
harness.run("duration plans clamp floor and reject overflow", duration_plans_clamp_floor_and_reject_overflow); 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("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("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("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation); 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 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 dispatches timeline and parameter operations", executor_dispatches_timeline_and_parameter_operations);

View File

@@ -294,6 +294,7 @@ struct PlanAnimationOperationArgs {
int delta = 1; int delta = 1;
int offset = 1; int offset = 1;
int onion_size = 1; int onion_size = 1;
bool playback_active = false;
}; };
struct PlanBrushOperationArgs { struct PlanBrushOperationArgs {
@@ -993,6 +994,8 @@ const char* document_animation_operation_name(pp::app::DocumentAnimationOperatio
return "goto-previous"; return "goto-previous";
case pp::app::DocumentAnimationOperation::playback_step: case pp::app::DocumentAnimationOperation::playback_step:
return "playback-step"; return "playback-step";
case pp::app::DocumentAnimationOperation::toggle_playback:
return "toggle-playback";
case pp::app::DocumentAnimationOperation::set_onion_size: case pp::app::DocumentAnimationOperation::set_onion_size:
return "set-onion-size"; return "set-onion-size";
} }
@@ -1553,7 +1556,7 @@ void print_help()
<< " plan-layer-rename --old-name NAME --new-name NAME\n" << " 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-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-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|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-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-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-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 --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" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n"
@@ -3857,6 +3860,8 @@ pp::foundation::Status parse_plan_animation_operation_args(
return pp::foundation::Status::invalid_argument("missing value for option"); return pp::foundation::Status::invalid_argument("missing value for option");
} }
args.kind = argv[++i]; args.kind = argv[++i];
} else if (key == "--playing") {
args.playback_active = true;
} else if (key == "--frame-count" || key == "--total-duration" || key == "--current-frame" } else if (key == "--frame-count" || key == "--total-duration" || key == "--current-frame"
|| key == "--selected-frame" || key == "--current-duration" || key == "--delta" || key == "--selected-frame" || key == "--current-duration" || key == "--delta"
|| key == "--offset" || key == "--onion-size" || key == "--layer-index" || key == "--offset" || key == "--onion-size" || key == "--layer-index"
@@ -3941,6 +3946,9 @@ pp::foundation::Result<pp::app::DocumentAnimationOperationPlan> make_animation_o
if (args.kind == "playback") { if (args.kind == "playback") {
return pp::app::plan_animation_playback_step(args.total_duration, args.current_frame, args.offset); return pp::app::plan_animation_playback_step(args.total_duration, args.current_frame, args.offset);
} }
if (args.kind == "toggle-playback" || args.kind == "play-toggle") {
return pp::app::plan_animation_playback_toggle(args.playback_active);
}
if (args.kind == "onion") { if (args.kind == "onion") {
return pp::app::plan_animation_onion_size(args.onion_size); return pp::app::plan_animation_onion_size(args.onion_size);
} }
@@ -3977,6 +3985,7 @@ int plan_animation_operation(int argc, char** argv)
<< ",\"delta\":" << args.delta << ",\"delta\":" << args.delta
<< ",\"offset\":" << args.offset << ",\"offset\":" << args.offset
<< ",\"onionSize\":" << args.onion_size << ",\"onionSize\":" << args.onion_size
<< ",\"playbackActive\":" << json_bool(args.playback_active)
<< "},\"plan\":{\"operation\":\"" << document_animation_operation_name(value.operation) << "},\"plan\":{\"operation\":\"" << document_animation_operation_name(value.operation)
<< "\",\"frameCount\":" << value.frame_count << "\",\"frameCount\":" << value.frame_count
<< ",\"currentFrame\":" << value.current_frame << ",\"currentFrame\":" << value.current_frame
@@ -3988,11 +3997,15 @@ int plan_animation_operation(int argc, char** argv)
<< ",\"durationDelta\":" << value.duration_delta << ",\"durationDelta\":" << value.duration_delta
<< ",\"moveOffset\":" << value.move_offset << ",\"moveOffset\":" << value.move_offset
<< ",\"onionSize\":" << value.onion_size << ",\"onionSize\":" << value.onion_size
<< ",\"playbackIdleMs\":" << value.playback_idle_ms
<< ",\"requiresSelectedFrame\":" << json_bool(value.requires_selected_frame) << ",\"requiresSelectedFrame\":" << json_bool(value.requires_selected_frame)
<< ",\"mutatesDocument\":" << json_bool(value.mutates_document) << ",\"mutatesDocument\":" << json_bool(value.mutates_document)
<< ",\"reloadsAnimationLayers\":" << json_bool(value.reloads_animation_layers) << ",\"reloadsAnimationLayers\":" << json_bool(value.reloads_animation_layers)
<< ",\"updatesCanvasAnimation\":" << json_bool(value.updates_canvas_animation) << ",\"updatesCanvasAnimation\":" << json_bool(value.updates_canvas_animation)
<< ",\"marksUnsaved\":" << json_bool(value.marks_unsaved) << ",\"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"; << "}}\n";
return 0; return 0;
} }