Route animation panel view through app core
This commit is contained in:
@@ -250,6 +250,9 @@ Known local toolchain state:
|
|||||||
consumes the tested `pp_app_core` onion frame range and alpha falloff helper,
|
consumes the tested `pp_app_core` onion frame range and alpha falloff helper,
|
||||||
and `NodeAnimationTimeline` mouse scrubbing consumes tested `pp_app_core`
|
and `NodeAnimationTimeline` mouse scrubbing consumes tested `pp_app_core`
|
||||||
cursor-to-frame planning exposed through `pano_cli plan-animation-timeline-scrub`,
|
cursor-to-frame planning exposed through `pano_cli plan-animation-timeline-scrub`,
|
||||||
|
and `NodePanelAnimation` layer/frame/timeline display state consumes the
|
||||||
|
tested `pp_app_core` panel view model exposed through
|
||||||
|
`pano_cli plan-animation-panel-view`,
|
||||||
while legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel
|
while legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel
|
||||||
timeline/playback fields, and the temporary `NodePanelAnimation` friend
|
timeline/playback fields, and the temporary `NodePanelAnimation` friend
|
||||||
adapter remain tracked by `DEBT-0022`.
|
adapter remain tracked by `DEBT-0022`.
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ agent or engineer to remove them without reconstructing context from chat.
|
|||||||
and opacity falloff in the render loop. Later on 2026-06-05, animation
|
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
|
timeline mouse scrubbing also moved to tested `pp_app_core` planning with
|
||||||
`pano_cli plan-animation-timeline-scrub` coverage, so `NodeAnimationTimeline`
|
`pano_cli plan-animation-timeline-scrub` coverage, so `NodeAnimationTimeline`
|
||||||
no longer owns cursor-to-frame clamp policy. Legacy canvas/layer/UI execution
|
no longer owns cursor-to-frame clamp policy. Animation panel layer/frame view
|
||||||
remains open under DEBT-0022.
|
projection now also uses a tested `pp_app_core` view model exposed by
|
||||||
|
`pano_cli plan-animation-panel-view`, including stale-selection behavior.
|
||||||
|
Legacy canvas/layer/UI execution remains open under DEBT-0022.
|
||||||
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
|
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
|
||||||
thumbnail, and object-draw history paths now query saved blend state through
|
thumbnail, and object-draw history paths now query saved blend state through
|
||||||
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
|
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
|
||||||
@@ -78,7 +80,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, 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-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-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, 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-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, 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-panel-view`, `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-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --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-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-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 |
|
| 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 |
|
||||||
|
|||||||
@@ -1699,8 +1699,9 @@ Results:
|
|||||||
invalid panel timeline state rejection, non-mutating duration no-ops, tested
|
invalid panel timeline state rejection, non-mutating duration no-ops, tested
|
||||||
onion-skin frame range/alpha falloff planning consumed by live `NodeCanvas`
|
onion-skin frame range/alpha falloff planning consumed by live `NodeCanvas`
|
||||||
panorama drawing, tested timeline mouse-scrub cursor-to-frame planning
|
panorama drawing, tested timeline mouse-scrub cursor-to-frame planning
|
||||||
consumed by live `NodeAnimationTimeline`, and malformed execution payload
|
consumed by live `NodeAnimationTimeline`, tested animation panel layer/frame
|
||||||
rejection.
|
view-model projection consumed by live `NodePanelAnimation`, stale selected
|
||||||
|
frame preservation, 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`,
|
||||||
@@ -1711,6 +1712,9 @@ Results:
|
|||||||
`pano_cli_plan_animation_panel_action_next_smoke`,
|
`pano_cli_plan_animation_panel_action_next_smoke`,
|
||||||
`pano_cli_plan_animation_panel_action_toggle_stop_smoke`,
|
`pano_cli_plan_animation_panel_action_toggle_stop_smoke`,
|
||||||
`pano_cli_plan_animation_panel_action_rejects_bad_timeline`,
|
`pano_cli_plan_animation_panel_action_rejects_bad_timeline`,
|
||||||
|
`pano_cli_plan_animation_panel_view_smoke`,
|
||||||
|
`pano_cli_plan_animation_panel_view_allows_stale_selection`,
|
||||||
|
`pano_cli_plan_animation_panel_view_rejects_empty_frames`,
|
||||||
`pano_cli_plan_animation_timeline_scrub_smoke`,
|
`pano_cli_plan_animation_timeline_scrub_smoke`,
|
||||||
`pano_cli_plan_animation_timeline_scrub_clamps_left`,
|
`pano_cli_plan_animation_timeline_scrub_clamps_left`,
|
||||||
`pano_cli_plan_animation_timeline_scrub_rejects_bad_duration`,
|
`pano_cli_plan_animation_timeline_scrub_rejects_bad_duration`,
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace pp::app {
|
namespace pp::app {
|
||||||
|
|
||||||
@@ -80,6 +84,39 @@ struct DocumentAnimationTimelineScrubPlan {
|
|||||||
int target_frame = 0;
|
int target_frame = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationLayerInput {
|
||||||
|
int layer_index = 0;
|
||||||
|
std::uint32_t layer_id = 0;
|
||||||
|
std::string name;
|
||||||
|
bool visible = true;
|
||||||
|
std::vector<int> frame_durations;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationFrameView {
|
||||||
|
int frame_index = 0;
|
||||||
|
int duration = document_animation_default_frame_duration;
|
||||||
|
bool selected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationLayerView {
|
||||||
|
int layer_index = 0;
|
||||||
|
std::uint32_t layer_id = 0;
|
||||||
|
std::string name;
|
||||||
|
bool visible = true;
|
||||||
|
bool current = false;
|
||||||
|
std::vector<DocumentAnimationFrameView> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DocumentAnimationPanelView {
|
||||||
|
int total_duration = 1;
|
||||||
|
int current_frame = 0;
|
||||||
|
int onion_size = 0;
|
||||||
|
std::uint32_t selected_layer_id = 0;
|
||||||
|
int selected_frame = -1;
|
||||||
|
bool has_selected_frame = false;
|
||||||
|
std::vector<DocumentAnimationLayerView> layers;
|
||||||
|
};
|
||||||
|
|
||||||
class DocumentAnimationServices {
|
class DocumentAnimationServices {
|
||||||
public:
|
public:
|
||||||
virtual ~DocumentAnimationServices() = default;
|
virtual ~DocumentAnimationServices() = default;
|
||||||
@@ -140,6 +177,86 @@ public:
|
|||||||
return pp::foundation::Status::success();
|
return pp::foundation::Status::success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationPanelView> plan_animation_panel_view(
|
||||||
|
const std::vector<DocumentAnimationLayerInput>& layers,
|
||||||
|
int total_duration,
|
||||||
|
int current_layer_index,
|
||||||
|
int current_frame,
|
||||||
|
std::uint32_t selected_layer_id,
|
||||||
|
int selected_frame,
|
||||||
|
int onion_size)
|
||||||
|
{
|
||||||
|
if (layers.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation panel requires at least one layer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto timeline_status = validate_animation_frame_index(total_duration, current_frame);
|
||||||
|
if (!timeline_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(timeline_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_layer_index < 0 || current_layer_index >= static_cast<int>(layers.size())) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("current animation layer index is outside the document"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onion_size < 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationPanelView view;
|
||||||
|
view.total_duration = total_duration;
|
||||||
|
view.current_frame = current_frame;
|
||||||
|
view.onion_size = onion_size;
|
||||||
|
view.selected_layer_id = selected_layer_id;
|
||||||
|
view.selected_frame = selected_frame;
|
||||||
|
view.layers.reserve(layers.size());
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < layers.size(); ++i) {
|
||||||
|
const auto& input = layers[i];
|
||||||
|
if (input.layer_index < 0) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||||
|
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||||
|
}
|
||||||
|
if (input.frame_durations.empty()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("animation layer must contain at least one frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAnimationLayerView layer;
|
||||||
|
layer.layer_index = input.layer_index;
|
||||||
|
layer.layer_id = input.layer_id;
|
||||||
|
layer.name = input.name;
|
||||||
|
layer.visible = input.visible;
|
||||||
|
layer.current = input.layer_index == current_layer_index;
|
||||||
|
layer.frames.reserve(input.frame_durations.size());
|
||||||
|
|
||||||
|
for (std::size_t frame_index = 0; frame_index < input.frame_durations.size(); ++frame_index) {
|
||||||
|
const int duration = input.frame_durations[frame_index];
|
||||||
|
const auto duration_status = validate_animation_frame_duration(duration);
|
||||||
|
if (!duration_status.ok()) {
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::failure(duration_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool selected = selected_frame >= 0
|
||||||
|
&& input.layer_id == selected_layer_id
|
||||||
|
&& static_cast<int>(frame_index) == selected_frame;
|
||||||
|
view.has_selected_frame = view.has_selected_frame || selected;
|
||||||
|
layer.frames.push_back(DocumentAnimationFrameView {
|
||||||
|
.frame_index = static_cast<int>(frame_index),
|
||||||
|
.duration = duration,
|
||||||
|
.selected = selected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view.layers.push_back(std::move(layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Result<DocumentAnimationPanelView>::success(std::move(view));
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
|
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
|
||||||
int frame_count,
|
int frame_count,
|
||||||
int current_frame,
|
int current_frame,
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimation
|
|||||||
pp::app::DocumentAnimationPanelState NodePanelAnimation::animation_panel_state() const
|
pp::app::DocumentAnimationPanelState NodePanelAnimation::animation_panel_state() const
|
||||||
{
|
{
|
||||||
return pp::app::DocumentAnimationPanelState {
|
return pp::app::DocumentAnimationPanelState {
|
||||||
.total_duration = Canvas::I->anim_duration(),
|
.total_duration = m_timeline ? m_timeline->m_frames_count : 0,
|
||||||
.current_frame = Canvas::I->m_anim_frame,
|
.current_frame = m_timeline ? m_timeline->m_frame : 0,
|
||||||
.playback_active = btn_play->is_active(),
|
.playback_active = btn_play->is_active(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -195,50 +195,91 @@ void NodePanelAnimation::load_layers()
|
|||||||
return;
|
return;
|
||||||
m_layers_container->remove_all_children();
|
m_layers_container->remove_all_children();
|
||||||
m_frames_container->remove_all_children();
|
m_frames_container->remove_all_children();
|
||||||
|
if (!Canvas::I)
|
||||||
|
return;
|
||||||
auto& layers = Canvas::I->m_layers;
|
auto& layers = Canvas::I->m_layers;
|
||||||
m_selected_frame = nullptr;
|
m_selected_frame = nullptr;
|
||||||
|
|
||||||
|
std::vector<pp::app::DocumentAnimationLayerInput> layer_inputs;
|
||||||
|
layer_inputs.reserve(layers.size());
|
||||||
for (int i = 0; i < layers.size(); i++)
|
for (int i = 0; i < layers.size(); i++)
|
||||||
{
|
{
|
||||||
auto l = m_layers_container->add_child<NodeAnimationLayer>();
|
pp::app::DocumentAnimationLayerInput input;
|
||||||
l->set_text(layers[i]->m_name);
|
input.layer_index = i;
|
||||||
l->set_selected(Canvas::I->m_current_layer_idx == i);
|
input.layer_id = layers[i]->id;
|
||||||
l->set_chekcbox(layers[i]->m_visible);
|
input.name = layers[i]->m_name;
|
||||||
auto film = m_frames_container->add_child_ref<NodeAnimationFilm>();
|
input.visible = layers[i]->m_visible;
|
||||||
|
input.frame_durations.reserve(layers[i]->frames_count());
|
||||||
for (int fi = 0; fi < layers[i]->frames_count(); fi++)
|
for (int fi = 0; fi < layers[i]->frames_count(); fi++)
|
||||||
{
|
input.frame_durations.push_back(layers[i]->frame_duration(fi));
|
||||||
auto b = film->add_frame(layers[i]->frame_duration(fi));
|
layer_inputs.push_back(std::move(input));
|
||||||
|
}
|
||||||
|
|
||||||
if (m_selected_frame_layer_id == layers[i]->id && m_selected_frame_index == fi)
|
const auto view_result = pp::app::plan_animation_panel_view(
|
||||||
|
layer_inputs,
|
||||||
|
Canvas::I->anim_duration(),
|
||||||
|
Canvas::I->m_current_layer_idx,
|
||||||
|
Canvas::I->m_anim_frame,
|
||||||
|
m_selected_frame_layer_id,
|
||||||
|
m_selected_frame_index,
|
||||||
|
m_onion->get_int());
|
||||||
|
if (!view_result)
|
||||||
|
{
|
||||||
|
LOG("Animation panel view failed: %s", view_result.status().message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& view = view_result.value();
|
||||||
|
|
||||||
|
for (const auto& layer_view : view.layers)
|
||||||
|
{
|
||||||
|
auto l = m_layers_container->add_child<NodeAnimationLayer>();
|
||||||
|
l->set_text(layer_view.name);
|
||||||
|
l->set_selected(layer_view.current);
|
||||||
|
l->set_chekcbox(layer_view.visible);
|
||||||
|
auto film = m_frames_container->add_child_ref<NodeAnimationFilm>();
|
||||||
|
const int frame_count = static_cast<int>(layer_view.frames.size());
|
||||||
|
for (const auto& frame_view : layer_view.frames)
|
||||||
|
{
|
||||||
|
auto b = film->add_frame(frame_view.duration);
|
||||||
|
|
||||||
|
if (frame_view.selected)
|
||||||
{
|
{
|
||||||
b->set_active(true);
|
b->set_active(true);
|
||||||
m_selected_frame = b.get();
|
m_selected_frame = b.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
b->on_click = [this, fi, lid=layers[i]->id, i] (Node* target) {
|
b->on_click = [this,
|
||||||
|
frame_index = frame_view.frame_index,
|
||||||
|
frame_count,
|
||||||
|
layer_id = layer_view.layer_id,
|
||||||
|
layer_index = layer_view.layer_index] (Node* target) {
|
||||||
auto frame = static_cast<NodeAnimationFrame*>(target);
|
auto frame = static_cast<NodeAnimationFrame*>(target);
|
||||||
if (m_selected_frame)
|
if (m_selected_frame)
|
||||||
m_selected_frame->set_active(false);
|
m_selected_frame->set_active(false);
|
||||||
frame->set_active(true);
|
frame->set_active(true);
|
||||||
m_selected_frame = frame;
|
m_selected_frame = frame;
|
||||||
const auto plan = pp::app::plan_animation_select_frame(
|
const auto plan = pp::app::plan_animation_select_frame(
|
||||||
Canvas::I->m_layers[i]->frames_count(),
|
frame_count,
|
||||||
i,
|
layer_index,
|
||||||
lid,
|
layer_id,
|
||||||
fi);
|
frame_index);
|
||||||
if (plan)
|
if (plan && Canvas::I && layer_index >= 0 && layer_index < static_cast<int>(Canvas::I->m_layers.size()))
|
||||||
execute_animation_plan(plan.value(), Canvas::I->m_layers[i].get());
|
execute_animation_plan(plan.value(), Canvas::I->m_layers[layer_index].get());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
m_timeline->m_frame = view.current_frame;
|
||||||
m_timeline->m_onion_size = m_onion->get_int();
|
m_timeline->m_frames_count = view.total_duration;
|
||||||
|
m_timeline->m_onion_size = view.onion_size;
|
||||||
update_frames();
|
update_frames();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodePanelAnimation::update_frames()
|
void NodePanelAnimation::update_frames()
|
||||||
{
|
{
|
||||||
int total_frames = Canvas::I->anim_duration();
|
if (!m_timeline || !m_frame_label)
|
||||||
|
return;
|
||||||
|
int total_frames = m_timeline ? m_timeline->m_frames_count : 1;
|
||||||
int digits = (int)floor(glm::log(total_frames));
|
int digits = (int)floor(glm::log(total_frames));
|
||||||
m_frame_label->set_text_format("%0*d/%d", digits, m_timeline->m_frame + 1, total_frames);
|
m_frame_label->set_text_format("%0*d/%d", digits, m_timeline->m_frame + 1, total_frames);
|
||||||
}
|
}
|
||||||
@@ -349,8 +390,7 @@ kEventResult NodeAnimationTimeline::handle_event(Event* e)
|
|||||||
auto me = static_cast<MouseEvent*>(e);
|
auto me = static_cast<MouseEvent*>(e);
|
||||||
auto update = [&](){
|
auto update = [&](){
|
||||||
auto loc = me->m_pos - m_pos;
|
auto loc = me->m_pos - m_pos;
|
||||||
const int total_duration = Canvas::I ? Canvas::I->anim_duration() : 0;
|
const auto scrub = pp::app::plan_animation_timeline_scrub(m_frames_count, loc.x);
|
||||||
const auto scrub = pp::app::plan_animation_timeline_scrub(total_duration, loc.x);
|
|
||||||
if (!scrub)
|
if (!scrub)
|
||||||
return;
|
return;
|
||||||
m_frame = scrub.value().target_frame;
|
m_frame = scrub.value().target_frame;
|
||||||
|
|||||||
@@ -1268,6 +1268,24 @@ if(TARGET pano_cli)
|
|||||||
LABELS "app;integration;desktop-fast;fuzz"
|
LABELS "app;integration;desktop-fast;fuzz"
|
||||||
WILL_FAIL TRUE)
|
WILL_FAIL TRUE)
|
||||||
|
|
||||||
|
add_test(NAME pano_cli_plan_animation_panel_view_smoke
|
||||||
|
COMMAND pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --frame-duration 2 --total-duration 6 --current-layer 1 --current-frame 4 --selected-layer-id 10 --selected-frame 2 --onion-size 2 --hidden-layer 1)
|
||||||
|
set_tests_properties(pano_cli_plan_animation_panel_view_smoke PROPERTIES
|
||||||
|
LABELS "app;ui;integration;desktop-fast"
|
||||||
|
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-panel-view\".*\"layers\":2.*\"visibleLayers\":1.*\"selectedFrames\":1.*\"hasSelectedFrame\":true.*\"currentLayerId\":20.*\"firstLayerFrames\":3")
|
||||||
|
|
||||||
|
add_test(NAME pano_cli_plan_animation_panel_view_allows_stale_selection
|
||||||
|
COMMAND pano_cli plan-animation-panel-view --layer-count 1 --frame-count 1 --total-duration 1 --current-layer 0 --current-frame 0 --selected-layer-id 999 --selected-frame 42)
|
||||||
|
set_tests_properties(pano_cli_plan_animation_panel_view_allows_stale_selection PROPERTIES
|
||||||
|
LABELS "app;ui;integration;desktop-fast;fuzz"
|
||||||
|
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-panel-view\".*\"layers\":1.*\"selectedFrames\":0.*\"hasSelectedFrame\":false")
|
||||||
|
|
||||||
|
add_test(NAME pano_cli_plan_animation_panel_view_rejects_empty_frames
|
||||||
|
COMMAND pano_cli plan-animation-panel-view --layer-count 1 --frame-count 0 --total-duration 1)
|
||||||
|
set_tests_properties(pano_cli_plan_animation_panel_view_rejects_empty_frames PROPERTIES
|
||||||
|
LABELS "app;ui;integration;desktop-fast;fuzz"
|
||||||
|
WILL_FAIL TRUE)
|
||||||
|
|
||||||
add_test(NAME pano_cli_plan_animation_timeline_scrub_smoke
|
add_test(NAME pano_cli_plan_animation_timeline_scrub_smoke
|
||||||
COMMAND pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99)
|
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
|
set_tests_properties(pano_cli_plan_animation_timeline_scrub_smoke PROPERTIES
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#define PP_REQUIRE(harness, expression) \
|
#define PP_REQUIRE(harness, expression) \
|
||||||
do { \
|
do { \
|
||||||
@@ -426,6 +427,114 @@ void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& har
|
|||||||
PP_EXPECT(harness, !pp::app::plan_animation_onion_size(-1));
|
PP_EXPECT(harness, !pp::app::plan_animation_onion_size(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void panel_view_projects_layers_frames_and_timeline_state(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
std::vector<pp::app::DocumentAnimationLayerInput> layers;
|
||||||
|
layers.push_back(pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 0,
|
||||||
|
.layer_id = 10,
|
||||||
|
.name = "Base",
|
||||||
|
.visible = true,
|
||||||
|
.frame_durations = { 1, 2 },
|
||||||
|
});
|
||||||
|
layers.push_back(pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 1,
|
||||||
|
.layer_id = 20,
|
||||||
|
.name = "Ink",
|
||||||
|
.visible = false,
|
||||||
|
.frame_durations = { 3 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto view = pp::app::plan_animation_panel_view(
|
||||||
|
layers,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
3);
|
||||||
|
PP_REQUIRE(harness, view);
|
||||||
|
PP_EXPECT(harness, view.value().total_duration == 4);
|
||||||
|
PP_EXPECT(harness, view.value().current_frame == 2);
|
||||||
|
PP_EXPECT(harness, view.value().onion_size == 3);
|
||||||
|
PP_EXPECT(harness, view.value().has_selected_frame);
|
||||||
|
PP_EXPECT(harness, view.value().layers.size() == 2);
|
||||||
|
PP_EXPECT(harness, !view.value().layers[0].current);
|
||||||
|
PP_EXPECT(harness, view.value().layers[1].current);
|
||||||
|
PP_EXPECT(harness, view.value().layers[0].name == "Base");
|
||||||
|
PP_EXPECT(harness, !view.value().layers[1].visible);
|
||||||
|
PP_EXPECT(harness, view.value().layers[0].frames.size() == 2);
|
||||||
|
PP_EXPECT(harness, view.value().layers[0].frames[0].duration == 1);
|
||||||
|
PP_EXPECT(harness, view.value().layers[0].frames[1].duration == 2);
|
||||||
|
PP_EXPECT(harness, !view.value().layers[0].frames[0].selected);
|
||||||
|
PP_EXPECT(harness, view.value().layers[0].frames[1].selected);
|
||||||
|
PP_EXPECT(harness, !view.value().layers[1].frames[0].selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void panel_view_preserves_stale_selection_as_no_active_frame(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const std::vector<pp::app::DocumentAnimationLayerInput> layers {
|
||||||
|
pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 0,
|
||||||
|
.layer_id = 10,
|
||||||
|
.name = "Base",
|
||||||
|
.visible = true,
|
||||||
|
.frame_durations = { 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto view = pp::app::plan_animation_panel_view(
|
||||||
|
layers,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
999,
|
||||||
|
42,
|
||||||
|
0);
|
||||||
|
PP_REQUIRE(harness, view);
|
||||||
|
PP_EXPECT(harness, !view.value().has_selected_frame);
|
||||||
|
PP_EXPECT(harness, !view.value().layers[0].frames[0].selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void panel_view_rejects_invalid_document_state(pp::tests::Harness& harness)
|
||||||
|
{
|
||||||
|
const std::vector<pp::app::DocumentAnimationLayerInput> valid_layers {
|
||||||
|
pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 0,
|
||||||
|
.layer_id = 10,
|
||||||
|
.name = "Base",
|
||||||
|
.visible = true,
|
||||||
|
.frame_durations = { 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const std::vector<pp::app::DocumentAnimationLayerInput> empty_frames {
|
||||||
|
pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 0,
|
||||||
|
.layer_id = 10,
|
||||||
|
.name = "Base",
|
||||||
|
.visible = true,
|
||||||
|
.frame_durations = {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const std::vector<pp::app::DocumentAnimationLayerInput> bad_duration {
|
||||||
|
pp::app::DocumentAnimationLayerInput {
|
||||||
|
.layer_index = 0,
|
||||||
|
.layer_id = 10,
|
||||||
|
.name = "Base",
|
||||||
|
.visible = true,
|
||||||
|
.frame_durations = { 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view({}, 1, 0, 0, 0, -1, 0));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 0, 0, 0, 0, -1, 0));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 1, 0, 0, -1, 0));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 0, 1, 0, -1, 0));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 0, 0, 0, -1, -1));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(empty_frames, 1, 0, 0, 0, -1, 0));
|
||||||
|
PP_EXPECT(harness, !pp::app::plan_animation_panel_view(bad_duration, 1, 0, 0, 0, -1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
void onion_frame_ranges_clamp_edges_and_alpha(pp::tests::Harness& harness)
|
void onion_frame_ranges_clamp_edges_and_alpha(pp::tests::Harness& harness)
|
||||||
{
|
{
|
||||||
const auto center = pp::app::plan_animation_onion_frame_range(5, 2, 1);
|
const auto center = pp::app::plan_animation_onion_frame_range(5, 2, 1);
|
||||||
@@ -666,6 +775,9 @@ int main()
|
|||||||
harness.run("panel actions plan timeline and playback intent", panel_actions_plan_timeline_and_playback_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("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("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation);
|
||||||
|
harness.run("panel view projects layers frames and timeline state", panel_view_projects_layers_frames_and_timeline_state);
|
||||||
|
harness.run("panel view preserves stale selection as no active frame", panel_view_preserves_stale_selection_as_no_active_frame);
|
||||||
|
harness.run("panel view rejects invalid document state", panel_view_rejects_invalid_document_state);
|
||||||
harness.run("onion frame ranges clamp edges and alpha", onion_frame_ranges_clamp_edges_and_alpha);
|
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("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 clamps cursor to document frame", timeline_scrub_clamps_cursor_to_document_frame);
|
||||||
|
|||||||
@@ -346,6 +346,19 @@ struct PlanAnimationPanelActionArgs {
|
|||||||
bool playback_active = false;
|
bool playback_active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PlanAnimationPanelViewArgs {
|
||||||
|
int layer_count = 2;
|
||||||
|
int frame_count = 2;
|
||||||
|
int frame_duration = 1;
|
||||||
|
int total_duration = 2;
|
||||||
|
int current_layer = 0;
|
||||||
|
int current_frame = 0;
|
||||||
|
std::uint32_t selected_layer_id = 10;
|
||||||
|
int selected_frame = 0;
|
||||||
|
int onion_size = 1;
|
||||||
|
int hidden_layer = -1;
|
||||||
|
};
|
||||||
|
|
||||||
struct PlanAnimationTimelineScrubArgs {
|
struct PlanAnimationTimelineScrubArgs {
|
||||||
int total_duration = 1;
|
int total_duration = 1;
|
||||||
float cursor_x = 0.0F;
|
float cursor_x = 0.0F;
|
||||||
@@ -1928,6 +1941,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-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-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-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n"
|
||||||
|
<< " plan-animation-panel-view [--layer-count N] [--frame-count N] [--frame-duration N] [--total-duration N] [--current-layer N] [--current-frame N] [--selected-layer-id N] [--selected-frame N] [--onion-size N] [--hidden-layer N]\n"
|
||||||
<< " plan-animation-timeline-scrub [--total-duration N] [--cursor-x N] [--frame-width N]\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-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-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n"
|
||||||
@@ -4834,6 +4848,133 @@ int plan_animation_panel_action(int argc, char** argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp::foundation::Status parse_plan_animation_panel_view_args(
|
||||||
|
int argc,
|
||||||
|
char** argv,
|
||||||
|
PlanAnimationPanelViewArgs& args)
|
||||||
|
{
|
||||||
|
for (int i = 2; i < argc; ++i) {
|
||||||
|
const std::string_view key(argv[i]);
|
||||||
|
if (key == "--layer-count" || key == "--frame-count" || key == "--frame-duration"
|
||||||
|
|| key == "--total-duration" || key == "--current-layer" || key == "--current-frame"
|
||||||
|
|| key == "--selected-layer-id" || key == "--selected-frame" || key == "--onion-size"
|
||||||
|
|| key == "--hidden-layer") {
|
||||||
|
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 == "--layer-count") {
|
||||||
|
args.layer_count = value.value();
|
||||||
|
} else if (key == "--frame-count") {
|
||||||
|
args.frame_count = value.value();
|
||||||
|
} else if (key == "--frame-duration") {
|
||||||
|
args.frame_duration = value.value();
|
||||||
|
} else if (key == "--total-duration") {
|
||||||
|
args.total_duration = value.value();
|
||||||
|
} else if (key == "--current-layer") {
|
||||||
|
args.current_layer = value.value();
|
||||||
|
} else if (key == "--current-frame") {
|
||||||
|
args.current_frame = value.value();
|
||||||
|
} else if (key == "--selected-layer-id") {
|
||||||
|
if (value.value() < 0) {
|
||||||
|
return pp::foundation::Status::out_of_range("selected animation layer id must not be negative");
|
||||||
|
}
|
||||||
|
args.selected_layer_id = static_cast<std::uint32_t>(value.value());
|
||||||
|
} else if (key == "--selected-frame") {
|
||||||
|
args.selected_frame = value.value();
|
||||||
|
} else if (key == "--onion-size") {
|
||||||
|
args.onion_size = value.value();
|
||||||
|
} else {
|
||||||
|
args.hidden_layer = value.value();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return pp::foundation::Status::invalid_argument("unknown option");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pp::foundation::Status::success();
|
||||||
|
}
|
||||||
|
|
||||||
|
int plan_animation_panel_view(int argc, char** argv)
|
||||||
|
{
|
||||||
|
PlanAnimationPanelViewArgs args;
|
||||||
|
const auto status = parse_plan_animation_panel_view_args(argc, argv, args);
|
||||||
|
if (!status.ok()) {
|
||||||
|
print_error("plan-animation-panel-view", status.message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pp::app::DocumentAnimationLayerInput> layers;
|
||||||
|
if (args.layer_count > 0) {
|
||||||
|
layers.reserve(static_cast<std::size_t>(args.layer_count));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < args.layer_count; ++i) {
|
||||||
|
pp::app::DocumentAnimationLayerInput layer;
|
||||||
|
layer.layer_index = i;
|
||||||
|
layer.layer_id = static_cast<std::uint32_t>((i + 1) * 10);
|
||||||
|
layer.name = "Layer " + std::to_string(i);
|
||||||
|
layer.visible = i != args.hidden_layer;
|
||||||
|
if (args.frame_count > 0) {
|
||||||
|
layer.frame_durations.assign(static_cast<std::size_t>(args.frame_count), args.frame_duration);
|
||||||
|
}
|
||||||
|
layers.push_back(std::move(layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto view = pp::app::plan_animation_panel_view(
|
||||||
|
layers,
|
||||||
|
args.total_duration,
|
||||||
|
args.current_layer,
|
||||||
|
args.current_frame,
|
||||||
|
args.selected_layer_id,
|
||||||
|
args.selected_frame,
|
||||||
|
args.onion_size);
|
||||||
|
if (!view) {
|
||||||
|
print_error("plan-animation-panel-view", view.status().message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& value = view.value();
|
||||||
|
int selected_count = 0;
|
||||||
|
int visible_count = 0;
|
||||||
|
for (const auto& layer : value.layers) {
|
||||||
|
if (layer.visible) {
|
||||||
|
visible_count += 1;
|
||||||
|
}
|
||||||
|
for (const auto& frame : layer.frames) {
|
||||||
|
if (frame.selected) {
|
||||||
|
selected_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "{\"ok\":true,\"command\":\"plan-animation-panel-view\""
|
||||||
|
<< ",\"state\":{\"layerCount\":" << args.layer_count
|
||||||
|
<< ",\"frameCount\":" << args.frame_count
|
||||||
|
<< ",\"frameDuration\":" << args.frame_duration
|
||||||
|
<< ",\"totalDuration\":" << args.total_duration
|
||||||
|
<< ",\"currentLayer\":" << args.current_layer
|
||||||
|
<< ",\"currentFrame\":" << args.current_frame
|
||||||
|
<< ",\"selectedLayerId\":" << args.selected_layer_id
|
||||||
|
<< ",\"selectedFrame\":" << args.selected_frame
|
||||||
|
<< ",\"onionSize\":" << args.onion_size
|
||||||
|
<< ",\"hiddenLayer\":" << args.hidden_layer
|
||||||
|
<< "},\"view\":{\"totalDuration\":" << value.total_duration
|
||||||
|
<< ",\"currentFrame\":" << value.current_frame
|
||||||
|
<< ",\"onionSize\":" << value.onion_size
|
||||||
|
<< ",\"layers\":" << value.layers.size()
|
||||||
|
<< ",\"visibleLayers\":" << visible_count
|
||||||
|
<< ",\"selectedFrames\":" << selected_count
|
||||||
|
<< ",\"hasSelectedFrame\":" << json_bool(value.has_selected_frame)
|
||||||
|
<< ",\"currentLayerId\":" << value.layers[static_cast<std::size_t>(args.current_layer)].layer_id
|
||||||
|
<< ",\"firstLayerName\":\"" << json_escape(value.layers.front().name)
|
||||||
|
<< "\",\"firstLayerFrames\":" << value.layers.front().frames.size()
|
||||||
|
<< "}}\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
pp::foundation::Status parse_plan_animation_timeline_scrub_args(
|
pp::foundation::Status parse_plan_animation_timeline_scrub_args(
|
||||||
int argc,
|
int argc,
|
||||||
char** argv,
|
char** argv,
|
||||||
@@ -9045,6 +9186,10 @@ int main(int argc, char** argv)
|
|||||||
return plan_animation_panel_action(argc, argv);
|
return plan_animation_panel_action(argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == "plan-animation-panel-view") {
|
||||||
|
return plan_animation_panel_view(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
if (command == "plan-animation-timeline-scrub") {
|
if (command == "plan-animation-timeline-scrub") {
|
||||||
return plan_animation_timeline_scrub(argc, argv);
|
return plan_animation_timeline_scrub(argc, argv);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user