Extract animation operation planning
This commit is contained in:
@@ -225,6 +225,7 @@ target_link_libraries(pp_platform_api
|
||||
add_library(pp_app_core STATIC
|
||||
src/app_core/app_preferences.h
|
||||
src/app_core/app_status.h
|
||||
src/app_core/document_animation.h
|
||||
src/app_core/document_cloud.h
|
||||
src/app_core/document_export.cpp
|
||||
src/app_core/document_layer.h
|
||||
@@ -411,6 +412,7 @@ if(PP_BUILD_APP)
|
||||
|
||||
target_link_libraries(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_engine
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
|
||||
@@ -39,6 +39,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0019 | Open | Modernization | MSVC warning C4100 is muted globally through `pp_project_warnings` with `/wd4100` | 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` | Remove `/wd4100`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app and headless tests pass without C4100 warnings |
|
||||
| DEBT-0020 | Open | Modernization | Document resize dialog state and selected-resolution planning now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, and `pano_cli plan-document-resize`, but live resize execution still calls legacy `Canvas::resize` and clears legacy `ActionManager` history directly | 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 a document/app boundary with legacy `Canvas` acting only as an adapter 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 now consumes pure `pp_app_core` through `NodePanelAnimation` and `pano_cli plan-animation-operation`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but live execution still mutates legacy `Canvas`/`Layer` frame state and animation playback state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by the document/app command boundary with legacy `Canvas`/`Layer`/UI nodes acting only as adapters or removed entirely |
|
||||
|
||||
## Closed Debt
|
||||
|
||||
|
||||
@@ -490,6 +490,10 @@ the live layer rename dialog before legacy `Canvas` layer mutation and
|
||||
duplicate, select, reorder, remove, opacity, visibility, alpha-lock, blend-mode,
|
||||
and highlight actions used by the live layer panel before legacy `Canvas` and
|
||||
UI layer execution continue.
|
||||
`pano_cli plan-animation-operation` exposes app-core planning for animation
|
||||
frame add, duplicate, remove, duration adjustment, timeline moves, timeline
|
||||
goto/next/previous, and onion-size updates used by the live animation panel
|
||||
before legacy `Canvas`/`Layer` frame execution continues.
|
||||
`pp_platform_api` now owns a headless `PlatformServices` interface for
|
||||
startup storage path preparation, clipboard text, cursor visibility,
|
||||
virtual-keyboard visibility, UI-thread lifecycle hooks, render-context
|
||||
@@ -1110,6 +1114,15 @@ Results:
|
||||
`pano_cli_plan_layer_operation_highlight_smoke`, and
|
||||
`pano_cli_plan_layer_operation_rejects_bad_opacity` passed and expose live
|
||||
layer-panel operation planning as JSON automation.
|
||||
- `pp_app_core_document_animation_tests` passed, covering animation frame
|
||||
add/duplicate/remove planning, selected-frame rejection, last-frame remove
|
||||
rejection, duration floor/overflow handling, timeline move edge behavior,
|
||||
goto/next/previous wrapping, and onion-size rejection.
|
||||
- `pano_cli_plan_animation_operation_add_smoke`,
|
||||
`pano_cli_plan_animation_operation_duration_floor_smoke`,
|
||||
`pano_cli_plan_animation_operation_next_wrap_smoke`, and
|
||||
`pano_cli_plan_animation_operation_rejects_remove_last_frame` passed and
|
||||
expose live animation-panel planning as JSON automation.
|
||||
- `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
|
||||
platform share execution.
|
||||
- `pano_cli_plan_share_file_unsaved_smoke` and
|
||||
|
||||
291
src/app_core/document_animation.h
Normal file
291
src/app_core/document_animation.h
Normal file
@@ -0,0 +1,291 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr int document_animation_default_frame_duration = 1;
|
||||
|
||||
enum class DocumentAnimationOperation {
|
||||
add_frame,
|
||||
duplicate_frame,
|
||||
remove_frame,
|
||||
adjust_duration,
|
||||
move_frame,
|
||||
goto_frame,
|
||||
goto_next,
|
||||
goto_previous,
|
||||
set_onion_size,
|
||||
};
|
||||
|
||||
struct DocumentAnimationOperationPlan {
|
||||
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int selected_frame = 0;
|
||||
int target_frame = 0;
|
||||
int frame_duration = document_animation_default_frame_duration;
|
||||
int duration_delta = 0;
|
||||
int move_offset = 0;
|
||||
int onion_size = 1;
|
||||
bool requires_selected_frame = false;
|
||||
bool mutates_document = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_canvas_animation = false;
|
||||
bool marks_unsaved = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
|
||||
{
|
||||
if (frame_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
|
||||
int frame_count,
|
||||
int index) noexcept
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= frame_count) {
|
||||
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
|
||||
{
|
||||
if (duration < 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
|
||||
int frame_count,
|
||||
int current_frame)
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
if (current_frame < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::add_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.current_frame = current_frame;
|
||||
plan.selected_frame = frame_count;
|
||||
plan.target_frame = current_frame;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::duplicate_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame + 1;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (frame_count <= 1) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::remove_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = std::min(selected_frame, frame_count - 2);
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int current_duration,
|
||||
int delta)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto duration_status = validate_animation_frame_duration(current_duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
|
||||
}
|
||||
|
||||
if (delta == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
|
||||
}
|
||||
|
||||
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::adjust_duration;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.frame_duration = std::max(current_duration + delta, 1);
|
||||
plan.duration_delta = delta;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.frame_duration != current_duration;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_canvas_animation = plan.mutates_document;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::move_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
|
||||
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
|
||||
plan.move_offset = offset;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.target_frame != selected_frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
|
||||
int total_duration,
|
||||
int frame)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (frame < 0 || frame >= total_duration) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::goto_frame;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = frame;
|
||||
plan.target_frame = frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
|
||||
if (!current_status) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = current_frame;
|
||||
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
|
||||
% static_cast<std::int64_t>(total_duration);
|
||||
if (target < 0) {
|
||||
target += total_duration;
|
||||
}
|
||||
plan.target_frame = static_cast<int>(target);
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
|
||||
{
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::set_onion_size;
|
||||
plan.onion_size = onion_size;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "app_core/document_animation.h"
|
||||
#include "node_button.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
@@ -45,62 +46,180 @@ void NodePanelAnimation::init_controls()
|
||||
m_frame_label = find<NodeText>("frame-index");
|
||||
|
||||
btn_add->on_click = [this](Node*) {
|
||||
const auto plan = pp::app::plan_animation_add_frame(
|
||||
Canvas::I->layer().frames_count(),
|
||||
Canvas::I->m_anim_frame);
|
||||
if (!plan)
|
||||
return;
|
||||
Canvas::I->layer().add_frame();
|
||||
load_layers();
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_duplicate->on_click = [this](Node*) {
|
||||
Canvas::I->layer().duplicate_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_duplicate_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->duplicate_frame(plan.value().selected_frame);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_remove->on_click = [this](Node*) {
|
||||
Canvas::I->layer_with_id(m_selected_frame_layer_id)->remove_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_remove_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->remove_frame(plan.value().selected_frame);
|
||||
m_selected_frame_index = plan.value().target_frame;
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_up->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) + 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto index_status = pp::app::validate_animation_frame_index(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!index_status.ok())
|
||||
return;
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->set_frame_duration(plan.value().selected_frame, plan.value().frame_duration);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_down->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) - 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto index_status = pp::app::validate_animation_frame_index(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!index_status.ok())
|
||||
return;
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->set_frame_duration(plan.value().selected_frame, plan.value().frame_duration);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_left->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, -1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
m_selected_frame_index = layer->move_frame_offset(plan.value().selected_frame, plan.value().move_offset);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_right->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, +1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
m_selected_frame_index = layer->move_frame_offset(plan.value().selected_frame, plan.value().move_offset);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
|
||||
m_onion->on_select = [this] (Node* target, int index) {
|
||||
m_timeline->m_onion_size = m_onion->get_int();
|
||||
Canvas::I->anim_update();
|
||||
const auto plan = pp::app::plan_animation_onion_size(m_onion->get_int());
|
||||
if (!plan)
|
||||
return;
|
||||
m_timeline->m_onion_size = plan.value().onion_size;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
};
|
||||
|
||||
m_timeline->on_frame_changed = [this] (NodeAnimationTimeline* target, int frame) {
|
||||
LOG("goto frame %d", frame);
|
||||
Canvas::I->anim_goto_frame(frame);
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_goto_frame(Canvas::I->anim_duration(), frame);
|
||||
if (!plan)
|
||||
return;
|
||||
LOG("goto frame %d", plan.value().target_frame);
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
|
||||
btn_next->on_click = [this] (Node* target) {
|
||||
Canvas::I->anim_goto_next();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, 1);
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_prev->on_click = [this](Node* target) {
|
||||
Canvas::I->anim_goto_prev();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, -1);
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_play->on_click = [this] (Node* target) {
|
||||
static auto mode = Canvas::I->m_current_mode;
|
||||
@@ -183,9 +302,13 @@ void NodePanelAnimation::on_tick(float dt)
|
||||
if (m_playback_timer > (1.f / m_fps->get_float()))
|
||||
{
|
||||
m_playback_timer = 0;
|
||||
Canvas::I->anim_goto_next();
|
||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
||||
update_frames();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, 1);
|
||||
if (plan)
|
||||
{
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
||||
update_frames();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,16 @@ add_test(NAME pp_app_core_document_recording_tests COMMAND pp_app_core_document_
|
||||
set_tests_properties(pp_app_core_document_recording_tests PROPERTIES
|
||||
LABELS "app;desktop-fast;fuzz")
|
||||
|
||||
add_executable(pp_app_core_document_animation_tests
|
||||
app_core/document_animation_tests.cpp)
|
||||
target_link_libraries(pp_app_core_document_animation_tests PRIVATE
|
||||
pp_app_core
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_app_core_document_animation_tests COMMAND pp_app_core_document_animation_tests)
|
||||
set_tests_properties(pp_app_core_document_animation_tests PROPERTIES
|
||||
LABELS "app;desktop-fast;fuzz")
|
||||
|
||||
add_executable(pp_app_core_document_layer_tests
|
||||
app_core/document_layer_tests.cpp)
|
||||
target_link_libraries(pp_app_core_document_layer_tests PRIVATE
|
||||
@@ -742,6 +752,30 @@ if(TARGET pano_cli)
|
||||
LABELS "app;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_animation_operation_add_smoke
|
||||
COMMAND pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0)
|
||||
set_tests_properties(pano_cli_plan_animation_operation_add_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"add-frame\".*\"selectedFrame\":2.*\"mutatesDocument\":true.*\"updatesCanvasAnimation\":true.*\"marksUnsaved\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_animation_operation_duration_floor_smoke
|
||||
COMMAND pano_cli plan-animation-operation --kind duration --frame-count 2 --selected-frame 1 --current-duration 1 --delta -1)
|
||||
set_tests_properties(pano_cli_plan_animation_operation_duration_floor_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast;fuzz"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"adjust-duration\".*\"selectedFrame\":1.*\"frameDuration\":1.*\"mutatesDocument\":false.*\"marksUnsaved\":false")
|
||||
|
||||
add_test(NAME pano_cli_plan_animation_operation_next_wrap_smoke
|
||||
COMMAND pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4)
|
||||
set_tests_properties(pano_cli_plan_animation_operation_next_wrap_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-animation-operation\".*\"operation\":\"goto-next\".*\"currentFrame\":4.*\"targetFrame\":0.*\"updatesCanvasAnimation\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_animation_operation_rejects_remove_last_frame
|
||||
COMMAND pano_cli plan-animation-operation --kind remove --frame-count 1 --selected-frame 0)
|
||||
set_tests_properties(pano_cli_plan_animation_operation_rejects_remove_last_frame PROPERTIES
|
||||
LABELS "app;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_share_file_unsaved_smoke
|
||||
COMMAND pano_cli plan-share-file)
|
||||
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES
|
||||
|
||||
133
tests/app_core/document_animation_tests.cpp
Normal file
133
tests/app_core/document_animation_tests.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "app_core/document_animation.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#define PP_REQUIRE(harness, expression) \
|
||||
do { \
|
||||
const bool pp_require_ok = static_cast<bool>(expression); \
|
||||
(harness).expect(pp_require_ok, #expression, __FILE__, __LINE__); \
|
||||
if (!pp_require_ok) { \
|
||||
return; \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
namespace {
|
||||
|
||||
void add_duplicate_and_remove_validate_frame_bounds(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto add = pp::app::plan_animation_add_frame(2, 0);
|
||||
PP_REQUIRE(harness, add);
|
||||
PP_EXPECT(harness, add.value().operation == pp::app::DocumentAnimationOperation::add_frame);
|
||||
PP_EXPECT(harness, add.value().selected_frame == 2);
|
||||
PP_EXPECT(harness, add.value().mutates_document);
|
||||
PP_EXPECT(harness, add.value().reloads_animation_layers);
|
||||
PP_EXPECT(harness, add.value().updates_canvas_animation);
|
||||
PP_EXPECT(harness, add.value().marks_unsaved);
|
||||
|
||||
const auto duplicate = pp::app::plan_animation_duplicate_frame(3, 1);
|
||||
PP_REQUIRE(harness, duplicate);
|
||||
PP_EXPECT(harness, duplicate.value().operation == pp::app::DocumentAnimationOperation::duplicate_frame);
|
||||
PP_EXPECT(harness, duplicate.value().target_frame == 2);
|
||||
PP_EXPECT(harness, duplicate.value().requires_selected_frame);
|
||||
|
||||
const auto remove = pp::app::plan_animation_remove_frame(3, 2);
|
||||
PP_REQUIRE(harness, remove);
|
||||
PP_EXPECT(harness, remove.value().operation == pp::app::DocumentAnimationOperation::remove_frame);
|
||||
PP_EXPECT(harness, remove.value().target_frame == 1);
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_add_frame(0, 0));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_add_frame(1, -1));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_duplicate_frame(2, 2));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_remove_frame(1, 0));
|
||||
}
|
||||
|
||||
void duration_plans_clamp_floor_and_reject_overflow(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto up = pp::app::plan_animation_adjust_duration(2, 1, 4, 1);
|
||||
PP_REQUIRE(harness, up);
|
||||
PP_EXPECT(harness, up.value().operation == pp::app::DocumentAnimationOperation::adjust_duration);
|
||||
PP_EXPECT(harness, up.value().frame_duration == 5);
|
||||
PP_EXPECT(harness, up.value().mutates_document);
|
||||
|
||||
const auto down = pp::app::plan_animation_adjust_duration(2, 1, 1, -1);
|
||||
PP_REQUIRE(harness, down);
|
||||
PP_EXPECT(harness, down.value().frame_duration == 1);
|
||||
PP_EXPECT(harness, !down.value().mutates_document);
|
||||
PP_EXPECT(harness, !down.value().marks_unsaved);
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_adjust_duration(2, 1, 0, 1));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_adjust_duration(2, 1, 2, 0));
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
!pp::app::plan_animation_adjust_duration(2, 1, std::numeric_limits<int>::max(), 1));
|
||||
}
|
||||
|
||||
void move_and_timeline_plans_handle_edges(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto left_edge = pp::app::plan_animation_move_frame(3, 0, -1);
|
||||
PP_REQUIRE(harness, left_edge);
|
||||
PP_EXPECT(harness, left_edge.value().operation == pp::app::DocumentAnimationOperation::move_frame);
|
||||
PP_EXPECT(harness, left_edge.value().target_frame == 0);
|
||||
PP_EXPECT(harness, !left_edge.value().mutates_document);
|
||||
PP_EXPECT(harness, left_edge.value().reloads_animation_layers);
|
||||
|
||||
const auto right = pp::app::plan_animation_move_frame(3, 1, 1);
|
||||
PP_REQUIRE(harness, right);
|
||||
PP_EXPECT(harness, right.value().target_frame == 2);
|
||||
PP_EXPECT(harness, right.value().mutates_document);
|
||||
|
||||
const auto huge_left = pp::app::plan_animation_move_frame(3, 1, std::numeric_limits<int>::min());
|
||||
PP_REQUIRE(harness, huge_left);
|
||||
PP_EXPECT(harness, huge_left.value().target_frame == 0);
|
||||
|
||||
const auto huge_right = pp::app::plan_animation_move_frame(3, 1, std::numeric_limits<int>::max());
|
||||
PP_REQUIRE(harness, huge_right);
|
||||
PP_EXPECT(harness, huge_right.value().target_frame == 2);
|
||||
|
||||
const auto go = pp::app::plan_animation_goto_frame(5, 3);
|
||||
PP_REQUIRE(harness, go);
|
||||
PP_EXPECT(harness, go.value().operation == pp::app::DocumentAnimationOperation::goto_frame);
|
||||
PP_EXPECT(harness, go.value().target_frame == 3);
|
||||
PP_EXPECT(harness, !go.value().mutates_document);
|
||||
PP_EXPECT(harness, go.value().updates_canvas_animation);
|
||||
|
||||
const auto next = pp::app::plan_animation_step_frame(5, 4, 1);
|
||||
PP_REQUIRE(harness, next);
|
||||
PP_EXPECT(harness, next.value().operation == pp::app::DocumentAnimationOperation::goto_next);
|
||||
PP_EXPECT(harness, next.value().target_frame == 0);
|
||||
|
||||
const auto prev = pp::app::plan_animation_step_frame(5, 0, -1);
|
||||
PP_REQUIRE(harness, prev);
|
||||
PP_EXPECT(harness, prev.value().operation == pp::app::DocumentAnimationOperation::goto_previous);
|
||||
PP_EXPECT(harness, prev.value().target_frame == 4);
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_move_frame(3, 1, 0));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_goto_frame(5, 5));
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_step_frame(0, 0, 1));
|
||||
}
|
||||
|
||||
void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_onion_size(2);
|
||||
PP_REQUIRE(harness, plan);
|
||||
PP_EXPECT(harness, plan.value().operation == pp::app::DocumentAnimationOperation::set_onion_size);
|
||||
PP_EXPECT(harness, plan.value().onion_size == 2);
|
||||
PP_EXPECT(harness, plan.value().updates_canvas_animation);
|
||||
PP_EXPECT(harness, !plan.value().mutates_document);
|
||||
PP_EXPECT(harness, !plan.value().marks_unsaved);
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_animation_onion_size(-1));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("add duplicate and remove validate frame bounds", add_duplicate_and_remove_validate_frame_bounds);
|
||||
harness.run("duration plans clamp floor and reject overflow", duration_plans_clamp_floor_and_reject_overflow);
|
||||
harness.run("move and timeline plans handle edges", move_and_timeline_plans_handle_edges);
|
||||
harness.run("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "app_core/app_status.h"
|
||||
#include "app_core/document_animation.h"
|
||||
#include "app_core/document_export.h"
|
||||
#include "app_core/document_cloud.h"
|
||||
#include "app_core/document_layer.h"
|
||||
@@ -238,6 +239,18 @@ struct PlanLayerOperationArgs {
|
||||
int blend_mode = 0;
|
||||
};
|
||||
|
||||
struct PlanAnimationOperationArgs {
|
||||
std::string kind = "goto";
|
||||
int frame_count = 1;
|
||||
int total_duration = 1;
|
||||
int current_frame = 0;
|
||||
int selected_frame = 0;
|
||||
int current_duration = 1;
|
||||
int delta = 1;
|
||||
int offset = 1;
|
||||
int onion_size = 1;
|
||||
};
|
||||
|
||||
struct SimulateAppSessionArgs {
|
||||
bool has_canvas = true;
|
||||
bool new_document = false;
|
||||
@@ -511,6 +524,32 @@ const char* document_layer_operation_name(pp::app::DocumentLayerOperation operat
|
||||
return "select";
|
||||
}
|
||||
|
||||
const char* document_animation_operation_name(pp::app::DocumentAnimationOperation operation) noexcept
|
||||
{
|
||||
switch (operation) {
|
||||
case pp::app::DocumentAnimationOperation::add_frame:
|
||||
return "add-frame";
|
||||
case pp::app::DocumentAnimationOperation::duplicate_frame:
|
||||
return "duplicate-frame";
|
||||
case pp::app::DocumentAnimationOperation::remove_frame:
|
||||
return "remove-frame";
|
||||
case pp::app::DocumentAnimationOperation::adjust_duration:
|
||||
return "adjust-duration";
|
||||
case pp::app::DocumentAnimationOperation::move_frame:
|
||||
return "move-frame";
|
||||
case pp::app::DocumentAnimationOperation::goto_frame:
|
||||
return "goto-frame";
|
||||
case pp::app::DocumentAnimationOperation::goto_next:
|
||||
return "goto-next";
|
||||
case pp::app::DocumentAnimationOperation::goto_previous:
|
||||
return "goto-previous";
|
||||
case pp::app::DocumentAnimationOperation::set_onion_size:
|
||||
return "set-onion-size";
|
||||
}
|
||||
|
||||
return "goto-frame";
|
||||
}
|
||||
|
||||
const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept
|
||||
{
|
||||
switch (decision) {
|
||||
@@ -724,6 +763,20 @@ pp::foundation::Result<float> parse_float_arg(std::string_view text)
|
||||
return pp::foundation::Result<float>::success(value);
|
||||
}
|
||||
|
||||
pp::foundation::Result<int> parse_i32_arg(std::string_view text)
|
||||
{
|
||||
int value = 0;
|
||||
const auto* begin = text.data();
|
||||
const auto* end = begin + text.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value);
|
||||
if (ec != std::errc {} || ptr != end) {
|
||||
return pp::foundation::Result<int>::failure(
|
||||
pp::foundation::Status::invalid_argument("invalid signed integer value"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<int>::success(value);
|
||||
}
|
||||
|
||||
void print_help()
|
||||
{
|
||||
std::cout
|
||||
@@ -750,6 +803,7 @@ void print_help()
|
||||
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n"
|
||||
<< " plan-layer-rename --old-name NAME --new-name NAME\n"
|
||||
<< " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n"
|
||||
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n"
|
||||
<< " plan-share-file [--path FILE]\n"
|
||||
<< " plan-picked-path [--path FILE]\n"
|
||||
<< " plan-display-file [--path FILE]\n"
|
||||
@@ -2535,6 +2589,136 @@ int plan_layer_operation(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_animation_operation_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
PlanAnimationOperationArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--kind") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
args.kind = argv[++i];
|
||||
} else if (key == "--frame-count" || key == "--total-duration" || key == "--current-frame"
|
||||
|| key == "--selected-frame" || key == "--current-duration" || key == "--delta"
|
||||
|| key == "--offset" || key == "--onion-size") {
|
||||
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 == "--frame-count") {
|
||||
args.frame_count = value.value();
|
||||
} else if (key == "--total-duration") {
|
||||
args.total_duration = value.value();
|
||||
} else if (key == "--current-frame") {
|
||||
args.current_frame = value.value();
|
||||
} else if (key == "--selected-frame") {
|
||||
args.selected_frame = value.value();
|
||||
} else if (key == "--current-duration") {
|
||||
args.current_duration = value.value();
|
||||
} else if (key == "--delta") {
|
||||
args.delta = value.value();
|
||||
} else if (key == "--offset") {
|
||||
args.offset = value.value();
|
||||
} else {
|
||||
args.onion_size = value.value();
|
||||
}
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<pp::app::DocumentAnimationOperationPlan> make_animation_operation_plan(
|
||||
const PlanAnimationOperationArgs& args)
|
||||
{
|
||||
if (args.kind == "add") {
|
||||
return pp::app::plan_animation_add_frame(args.frame_count, args.current_frame);
|
||||
}
|
||||
if (args.kind == "duplicate") {
|
||||
return pp::app::plan_animation_duplicate_frame(args.frame_count, args.selected_frame);
|
||||
}
|
||||
if (args.kind == "remove") {
|
||||
return pp::app::plan_animation_remove_frame(args.frame_count, args.selected_frame);
|
||||
}
|
||||
if (args.kind == "duration") {
|
||||
return pp::app::plan_animation_adjust_duration(
|
||||
args.frame_count,
|
||||
args.selected_frame,
|
||||
args.current_duration,
|
||||
args.delta);
|
||||
}
|
||||
if (args.kind == "move") {
|
||||
return pp::app::plan_animation_move_frame(args.frame_count, args.selected_frame, args.offset);
|
||||
}
|
||||
if (args.kind == "goto") {
|
||||
return pp::app::plan_animation_goto_frame(args.total_duration, args.current_frame);
|
||||
}
|
||||
if (args.kind == "next") {
|
||||
return pp::app::plan_animation_step_frame(args.total_duration, args.current_frame, 1);
|
||||
}
|
||||
if (args.kind == "prev") {
|
||||
return pp::app::plan_animation_step_frame(args.total_duration, args.current_frame, -1);
|
||||
}
|
||||
if (args.kind == "onion") {
|
||||
return pp::app::plan_animation_onion_size(args.onion_size);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<pp::app::DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown animation operation kind"));
|
||||
}
|
||||
|
||||
int plan_animation_operation(int argc, char** argv)
|
||||
{
|
||||
PlanAnimationOperationArgs args;
|
||||
const auto status = parse_plan_animation_operation_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("plan-animation-operation", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto plan = make_animation_operation_plan(args);
|
||||
if (!plan) {
|
||||
print_error("plan-animation-operation", plan.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto& value = plan.value();
|
||||
std::cout << "{\"ok\":true,\"command\":\"plan-animation-operation\""
|
||||
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
|
||||
<< "\",\"frameCount\":" << args.frame_count
|
||||
<< ",\"totalDuration\":" << args.total_duration
|
||||
<< ",\"currentFrame\":" << args.current_frame
|
||||
<< ",\"selectedFrame\":" << args.selected_frame
|
||||
<< ",\"currentDuration\":" << args.current_duration
|
||||
<< ",\"delta\":" << args.delta
|
||||
<< ",\"offset\":" << args.offset
|
||||
<< ",\"onionSize\":" << args.onion_size
|
||||
<< "},\"plan\":{\"operation\":\"" << document_animation_operation_name(value.operation)
|
||||
<< "\",\"frameCount\":" << value.frame_count
|
||||
<< ",\"currentFrame\":" << value.current_frame
|
||||
<< ",\"selectedFrame\":" << value.selected_frame
|
||||
<< ",\"targetFrame\":" << value.target_frame
|
||||
<< ",\"frameDuration\":" << value.frame_duration
|
||||
<< ",\"durationDelta\":" << value.duration_delta
|
||||
<< ",\"moveOffset\":" << value.move_offset
|
||||
<< ",\"onionSize\":" << value.onion_size
|
||||
<< ",\"requiresSelectedFrame\":" << json_bool(value.requires_selected_frame)
|
||||
<< ",\"mutatesDocument\":" << json_bool(value.mutates_document)
|
||||
<< ",\"reloadsAnimationLayers\":" << json_bool(value.reloads_animation_layers)
|
||||
<< ",\"updatesCanvasAnimation\":" << json_bool(value.updates_canvas_animation)
|
||||
<< ",\"marksUnsaved\":" << json_bool(value.marks_unsaved)
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_share_file_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
@@ -4947,6 +5131,10 @@ int main(int argc, char** argv)
|
||||
return plan_layer_operation(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-animation-operation") {
|
||||
return plan_animation_operation(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-share-file") {
|
||||
return plan_share_file(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user