diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index c6cdd98..3fc6dca 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -49,7 +49,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0029 | Open | Modernization | Image import route planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-image-import`, and the `DocumentImageImportServices` boundary, but the live adapter still loads images with legacy `Image`, calls legacy `Canvas::import_equirectangular`, or configures legacy import transform mode directly | Preserve current File > Import behavior while image import moves toward document/app/asset command services | `pp_app_core_document_import_tests`; `pano_cli plan-image-import --width 4096 --height 2048`; `pano_cli plan-image-import --width 1024 --height 1024`; `ctest --preset desktop-fast --build-config Debug` | Image loading, equirectangular import, transform-placement import, and failure reporting are owned by injected document/app/asset services with File-menu callbacks acting only as adapters and no legacy image-import adapter | | DEBT-0030 | Open | Modernization | File export menu action planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-export-menu`, and the `DocumentExportMenuServices` boundary, but the live adapter still opens legacy export dialogs and then reaches legacy canvas/render/video export code | Preserve current export menu behavior while export command execution moves toward document/app/renderer/video services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind png`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-menu --kind layers --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Export menu routing, license gating, target creation, image/layer/cube/depth/animation/timelapse execution, and error reporting are owned by injected document/app/renderer/video services with File-menu callbacks acting only as UI adapters and no legacy export adapter | | DEBT-0031 | Open | Modernization | Top-level File menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_file`, `pano_cli plan-file-menu`, and the `FileMenuServices` boundary, but the live adapter still invokes legacy dialogs, platform pickers, cloud code, share code, and canvas import/export paths directly | Preserve File menu behavior while app workflows move toward app/document/platform command services | `pp_app_core_file_menu_tests`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-file-menu --command import`; `pano_cli plan-file-menu --command cloud-upload`; `ctest --preset desktop-fast --build-config Debug` | File menu routing, picker dispatch, save/share/cloud/resize/export execution, and image/project import execution are owned by injected app/document/platform services with `App::init_menu_file` acting only as a UI adapter and no legacy File menu adapter | -| DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary, and Layer menu clear now reuses the `DocumentCanvasClearServices` executor, but the live adapter still calls `App::dialog_layer_rename`, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer rename, merge-down execution, animation gating, and selected-layer state are owned by injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu adapter | +| DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary, Layer menu clear reuses the `DocumentCanvasClearServices` executor, and Layer menu merge validates/dispatches through `DocumentLayerMergeServices`, but the live adapter still calls `App::dialog_layer_rename`, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1 --animation-duration 3`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer rename, merge-down execution, animation gating, and selected-layer state are owned by injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu adapter | | DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0af18cf..8c18db6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -501,6 +501,8 @@ rename, and merge-down labels/actions, and direct Layer menu commands now dispatch through `DocumentLayerMenuServices` before the legacy canvas/layer UI adapter continues execution. Layer menu clear now routes through the shared `DocumentCanvasClearServices` executor before the legacy canvas-clear adapter +continues, and Layer menu merge now validates and dispatches through +`DocumentLayerMergeServices` before the legacy layer-panel merge adapter continues. `pano_cli plan-animation-operation` exposes app-core planning for animation frame add, duplicate, remove, duration adjustment, timeline moves, timeline @@ -1249,8 +1251,9 @@ Results: layer operation side-effect dispatch, no-op operation preservation, malformed operation rejection, Layer menu labels/actions, merge-down routing, animated merge blocking, missing selection handling, bad Layer menu state - rejection, Layer menu executor dispatch, and no-op menu execution - preservation. + rejection, Layer menu executor dispatch, no-op menu execution preservation, + merge-plan validation, unsupported animated merge rejection, merge executor + dispatch, and malformed merge-plan rejection. - `pano_cli_plan_layer_rename_smoke`, `pano_cli_plan_layer_rename_no_op_smoke`, and `pano_cli_plan_layer_rename_rejects_empty_name` passed and expose live @@ -1261,6 +1264,9 @@ Results: `pano_cli_plan_layer_menu_missing_selection_smoke`, and `pano_cli_plan_layer_menu_rejects_bad_state` passed and expose live Layer menu planning as JSON automation. +- `pano_cli_plan_layer_merge_smoke` and + `pano_cli_plan_layer_merge_animated_rejected` passed and expose live + merge execution planning as JSON automation. - `pano_cli_plan_layer_operation_add_smoke`, `pano_cli_plan_layer_operation_reorder_no_op_smoke`, `pano_cli_plan_layer_operation_highlight_smoke`, and diff --git a/src/app_core/document_layer.h b/src/app_core/document_layer.h index 9765bcf..00e02fa 100644 --- a/src/app_core/document_layer.h +++ b/src/app_core/document_layer.h @@ -78,6 +78,12 @@ struct DocumentLayerMenuPlan { int to_index = 0; }; +struct DocumentLayerMergePlan { + int from_index = 0; + int to_index = 0; + bool create_history = true; +}; + class DocumentLayerMenuServices { public: virtual ~DocumentLayerMenuServices() = default; @@ -115,6 +121,13 @@ public: virtual void update_title() = 0; }; +class DocumentLayerMergeServices { +public: + virtual ~DocumentLayerMergeServices() = default; + + virtual void merge_layers(int from_index, int to_index, bool create_history) = 0; +}; + [[nodiscard]] inline pp::foundation::Status validate_layer_index( int layer_count, int index) noexcept @@ -447,6 +460,45 @@ public: return pp::foundation::Result::success(std::move(plan)); } +[[nodiscard]] inline pp::foundation::Result plan_document_layer_merge( + int layer_count, + int from_index, + int to_index, + int animation_duration, + bool create_history = true) +{ + auto index_status = validate_layer_index(layer_count, from_index); + if (!index_status.ok()) { + return pp::foundation::Result::failure(index_status); + } + + index_status = validate_layer_index(layer_count, to_index); + if (!index_status.ok()) { + return pp::foundation::Result::failure(index_status); + } + + if (animation_duration < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("animation duration must not be negative")); + } + + if (animation_duration > 1) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("animated layer merge is not supported")); + } + + if (from_index <= to_index) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("layer merge source must be above the destination")); + } + + DocumentLayerMergePlan plan; + plan.from_index = from_index; + plan.to_index = to_index; + plan.create_history = create_history; + return pp::foundation::Result::success(plan); +} + [[nodiscard]] inline pp::foundation::Status execute_document_layer_rename_plan( const DocumentLayerRenamePlan& plan, DocumentLayerRenameServices& services) @@ -545,6 +597,19 @@ inline void execute_document_layer_operation_side_effects( return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Status execute_document_layer_merge_plan( + const DocumentLayerMergePlan& plan, + DocumentLayerMergeServices& services) +{ + if (plan.from_index <= plan.to_index) { + return pp::foundation::Status::invalid_argument( + "layer merge source must be above the destination"); + } + + services.merge_layers(plan.from_index, plan.to_index, plan.create_history); + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan( const DocumentLayerMenuPlan& plan, DocumentLayerMenuServices& services) diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 1ee0d4d..9e09d14 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -167,6 +167,8 @@ void execute_document_canvas_clear_plan(App& app, const pp::app::DocumentCanvasC LOG("Canvas clear failed: %s", status.message); } +void execute_document_layer_merge_plan(App& app, const pp::app::DocumentLayerMergePlan& plan); + bool apply_document_export_menu_plan(App& app, pp::app::DocumentExportMenuKind kind) { class LegacyDocumentExportMenuServices final : public pp::app::DocumentExportMenuServices { @@ -605,8 +607,21 @@ public: void merge_with_lower_layer(int from_index, int to_index) override { - if (app_.layers) - app_.layers->merge(from_index, to_index, true); + const int layer_count = app_.canvas && app_.canvas->m_canvas + ? static_cast(app_.canvas->m_canvas->m_layers.size()) + : 0; + const int animation_duration = Canvas::I + ? Canvas::I->anim_duration() + : 0; + const auto plan = pp::app::plan_document_layer_merge( + layer_count, + from_index, + to_index, + animation_duration); + if (!plan) + return; + + execute_document_layer_merge_plan(app_, plan.value()); } void show_merge_animated_not_supported() override @@ -618,6 +633,23 @@ private: App& app_; }; +class LegacyDocumentLayerMergeServices final : public pp::app::DocumentLayerMergeServices { +public: + explicit LegacyDocumentLayerMergeServices(App& app) noexcept + : app_(app) + { + } + + void merge_layers(int from_index, int to_index, bool create_history) override + { + if (app_.layers) + app_.layers->merge(from_index, to_index, create_history); + } + +private: + App& app_; +}; + class LegacyDocumentLayerOperationServices final : public pp::app::DocumentLayerOperationServices { public: LegacyDocumentLayerOperationServices( @@ -786,6 +818,14 @@ void execute_document_layer_menu_plan(App& app, const pp::app::DocumentLayerMenu LOG("Layer menu action failed: %s", status.message); } +void execute_document_layer_merge_plan(App& app, const pp::app::DocumentLayerMergePlan& plan) +{ + LegacyDocumentLayerMergeServices services(app); + const auto status = pp::app::execute_document_layer_merge_plan(plan, services); + if (!status.ok()) + LOG("Layer merge failed: %s", status.message); +} + void execute_document_layer_operation_plan( App& app, const pp::app::DocumentLayerOperationPlan& plan, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1f5e2ac..f32a51c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1016,6 +1016,18 @@ if(TARGET pano_cli) LABELS "app;document;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_layer_merge_smoke + COMMAND pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1) + set_tests_properties(pano_cli_plan_layer_merge_smoke PROPERTIES + LABELS "app;document;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-layer-merge\".*\"layerCount\":3.*\"fromIndex\":2.*\"toIndex\":1.*\"createHistory\":true") + + add_test(NAME pano_cli_plan_layer_merge_animated_rejected + COMMAND pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1 --animation-duration 3) + set_tests_properties(pano_cli_plan_layer_merge_animated_rejected PROPERTIES + LABELS "app;document;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_layer_operation_add_smoke COMMAND pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint) set_tests_properties(pano_cli_plan_layer_operation_add_smoke PROPERTIES diff --git a/tests/app_core/document_layer_tests.cpp b/tests/app_core/document_layer_tests.cpp index a4c4142..ab7c2ee 100644 --- a/tests/app_core/document_layer_tests.cpp +++ b/tests/app_core/document_layer_tests.cpp @@ -152,6 +152,22 @@ public: std::string last_name; }; +class FakeDocumentLayerMergeServices final : public pp::app::DocumentLayerMergeServices { +public: + void merge_layers(int from_index, int to_index, bool create_history) override + { + merge_calls += 1; + last_from_index = from_index; + last_to_index = to_index; + last_create_history = create_history; + } + + int merge_calls = 0; + int last_from_index = -1; + int last_to_index = -1; + bool last_create_history = false; +}; + void layer_rename_records_changed_name(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_layer_rename("Base", "Paint"); @@ -724,6 +740,63 @@ void layer_menu_executor_preserves_no_op_actions(pp::tests::Harness& harness) PP_EXPECT(harness, services.total_calls() == 0); } +void layer_merge_plan_validates_supported_merge(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_document_layer_merge(3, 2, 1, 1); + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, plan.value().from_index == 2); + PP_EXPECT(harness, plan.value().to_index == 1); + PP_EXPECT(harness, plan.value().create_history); + } + + const auto no_history = pp::app::plan_document_layer_merge(3, 2, 0, 1, false); + PP_EXPECT(harness, no_history); + if (no_history) { + PP_EXPECT(harness, !no_history.value().create_history); + } +} + +void layer_merge_plan_rejects_bad_or_unsupported_state(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(0, 0, 0, 1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 3, 1, 1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 1, 3, 1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 1, 1, 1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 0, 1, 1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 2, 1, -1)); + PP_EXPECT(harness, !pp::app::plan_document_layer_merge(3, 2, 1, 2)); +} + +void layer_merge_executor_dispatches_merge(pp::tests::Harness& harness) +{ + FakeDocumentLayerMergeServices services; + + const auto plan = pp::app::plan_document_layer_merge(3, 2, 1, 1); + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, pp::app::execute_document_layer_merge_plan(plan.value(), services).ok()); + PP_EXPECT(harness, services.merge_calls == 1); + PP_EXPECT(harness, services.last_from_index == 2); + PP_EXPECT(harness, services.last_to_index == 1); + PP_EXPECT(harness, services.last_create_history); + } +} + +void layer_merge_executor_rejects_malformed_plan(pp::tests::Harness& harness) +{ + FakeDocumentLayerMergeServices services; + + pp::app::DocumentLayerMergePlan malformed; + malformed.from_index = 1; + malformed.to_index = 1; + + const auto status = pp::app::execute_document_layer_merge_plan(malformed, services); + PP_EXPECT(harness, !status.ok()); + PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, services.merge_calls == 0); +} + } int main() @@ -750,5 +823,9 @@ int main() harness.run("layer menu handles missing selection and bad state", layer_menu_handles_missing_selection_and_bad_state); harness.run("layer menu executor dispatches menu actions", layer_menu_executor_dispatches_menu_actions); harness.run("layer menu executor preserves no op actions", layer_menu_executor_preserves_no_op_actions); + harness.run("layer merge plan validates supported merge", layer_merge_plan_validates_supported_merge); + harness.run("layer merge plan rejects bad or unsupported state", layer_merge_plan_rejects_bad_or_unsupported_state); + harness.run("layer merge executor dispatches merge", layer_merge_executor_dispatches_merge); + harness.run("layer merge executor rejects malformed plan", layer_merge_executor_rejects_malformed_plan); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 4d48e15..d629e3b 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -287,6 +287,14 @@ struct PlanLayerMenuArgs { std::string lower_name = "Layer 0"; }; +struct PlanLayerMergeArgs { + int layer_count = 2; + int from_index = 1; + int to_index = 0; + int animation_duration = 1; + bool create_history = true; +}; + struct PlanAnimationOperationArgs { std::string kind = "goto"; int frame_count = 1; @@ -1779,6 +1787,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-menu --command clear|rename|merge [--no-current-layer] [--current-index N] [--animation-duration N] [--current-name NAME] [--lower-name NAME]\n" + << " plan-layer-merge [--layer-count N] [--from-index N] [--to-index N] [--animation-duration N] [--no-history]\n" << " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n" << " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n" @@ -3959,6 +3968,75 @@ int plan_layer_menu(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_layer_merge_args( + int argc, + char** argv, + PlanLayerMergeArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--layer-count" || key == "--from-index" || key == "--to-index" + || key == "--animation-duration") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--layer-count") { + args.layer_count = value.value(); + } else if (key == "--from-index") { + args.from_index = value.value(); + } else if (key == "--to-index") { + args.to_index = value.value(); + } else { + args.animation_duration = value.value(); + } + } else if (key == "--no-history") { + args.create_history = false; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_layer_merge(int argc, char** argv) +{ + PlanLayerMergeArgs args; + const auto status = parse_plan_layer_merge_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-layer-merge", status.message); + return 2; + } + + const auto plan = pp::app::plan_document_layer_merge( + args.layer_count, + args.from_index, + args.to_index, + args.animation_duration, + args.create_history); + if (!plan) { + print_error("plan-layer-merge", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-layer-merge\"" + << ",\"state\":{\"layerCount\":" << args.layer_count + << ",\"fromIndex\":" << args.from_index + << ",\"toIndex\":" << args.to_index + << ",\"animationDuration\":" << args.animation_duration + << ",\"createHistory\":" << json_bool(args.create_history) + << "},\"plan\":{\"fromIndex\":" << value.from_index + << ",\"toIndex\":" << value.to_index + << ",\"createHistory\":" << json_bool(value.create_history) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_layer_operation_args( int argc, char** argv, @@ -8215,6 +8293,10 @@ int main(int argc, char** argv) return plan_layer_menu(argc, argv); } + if (command == "plan-layer-merge") { + return plan_layer_merge(argc, argv); + } + if (command == "plan-layer-operation") { return plan_layer_operation(argc, argv); }