diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fca4f6..474a7b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,6 +226,7 @@ add_library(pp_app_core STATIC src/app_core/app_preferences.h src/app_core/app_status.h src/app_core/brush_ui.h + src/app_core/canvas_tool_ui.h src/app_core/document_animation.h src/app_core/document_cloud.h src/app_core/document_export.cpp diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 984cd13..f1c785e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -44,6 +44,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | 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 app/brush/UI services with `NodePanelQuick` acting only as UI adapter | | DEBT-0026 | Open | Modernization | Toolbar and canvas history command planning now consumes pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, and `pano_cli plan-history-operation`, but live execution still mutates legacy `ActionManager` stacks and `Canvas::I` unsaved state directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by document/app history services with toolbar and canvas input acting only as adapters | +| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar planning now consumes pure `pp_app_core` through `App::init_toolbar_draw` and `pano_cli plan-canvas-tool`, but live execution still mutates legacy `Canvas` mode state, pen picking state, touch-lock state, and transform copy/cut action objects directly | Preserve current toolbar behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool --kind pick`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, picking, touch lock, and transform action execution are owned by app/document/canvas services with toolbar callbacks acting only as adapters | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index a04a3e4..33e10ab 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -499,6 +499,10 @@ before legacy `Canvas`/`Layer` frame execution continues. changes, tip/pattern/dual texture changes, preset brush replacement, and stroke settings refreshes used by the live brush, quick, color, and floating panel callbacks before legacy `Brush` mutation and resource loading continue. +`pano_cli plan-canvas-tool` exposes app-core planning for draw/erase/line, +camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar +commands before legacy `Canvas` mode, pen picking, touch-lock, and transform +state mutation continue. `pano_cli plan-grid-operation` exposes app-core planning for grid heightmap pick/load/reload/clear, lightmap render capability/limit checks, and heightmap commit used by the live grid panel before legacy image loading, OpenGL texture @@ -1158,6 +1162,15 @@ Results: `pano_cli_plan_grid_operation_rejects_empty_reload`, and `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live grid/heightmap/lightmap planning as JSON automation. +- `pp_app_core_canvas_tool_ui_tests` passed, covering toolbar mode selection, + copy/cut transform action planning, pick no-op outside draw mode, and + touch-lock toggling. +- `pano_cli_plan_canvas_tool_draw_smoke`, + `pano_cli_plan_canvas_tool_copy_smoke`, + `pano_cli_plan_canvas_tool_pick_noop_smoke`, + `pano_cli_plan_canvas_tool_touch_lock_smoke`, and + `pano_cli_plan_canvas_tool_rejects_unknown` passed and expose live draw + toolbar planning as JSON automation. - `pp_app_core_history_ui_tests` passed, covering undo/redo availability, no-op history commands, clear-history stack/memory state, memory-only clear, and negative metric rejection. diff --git a/src/app_core/canvas_tool_ui.h b/src/app_core/canvas_tool_ui.h new file mode 100644 index 0000000..9c38bb4 --- /dev/null +++ b/src/app_core/canvas_tool_ui.h @@ -0,0 +1,86 @@ +#pragma once + +#include "foundation/result.h" + +namespace pp::app { + +enum class CanvasToolOperation { + select_mode, + toggle_picking, + toggle_touch_lock, +}; + +enum class CanvasToolMode { + draw, + erase, + line, + camera, + grid, + copy, + cut, + fill, + mask_free, + mask_line, + flood_fill, +}; + +enum class CanvasToolTransformAction { + none, + copy, + cut, +}; + +struct CanvasToolPlan { + CanvasToolOperation operation = CanvasToolOperation::select_mode; + CanvasToolMode mode = CanvasToolMode::draw; + CanvasToolTransformAction transform_action = CanvasToolTransformAction::none; + bool selects_toolbar_button = false; + bool updates_canvas_mode = false; + bool toggles_picking = false; + bool toggles_touch_lock = false; + bool requires_draw_mode = false; + bool no_op = false; +}; + +[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept +{ + if (mode == CanvasToolMode::copy) { + return CanvasToolTransformAction::copy; + } + if (mode == CanvasToolMode::cut) { + return CanvasToolTransformAction::cut; + } + return CanvasToolTransformAction::none; +} + +[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept +{ + CanvasToolPlan plan; + plan.operation = CanvasToolOperation::select_mode; + plan.mode = mode; + plan.transform_action = transform_action_for_mode(mode); + plan.selects_toolbar_button = true; + plan.updates_canvas_mode = true; + return plan; +} + +[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept +{ + CanvasToolPlan plan; + plan.operation = CanvasToolOperation::toggle_picking; + plan.mode = CanvasToolMode::draw; + plan.requires_draw_mode = true; + plan.toggles_picking = current_mode_is_draw; + plan.no_op = !current_mode_is_draw; + return plan; +} + +[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept +{ + CanvasToolPlan plan; + plan.operation = CanvasToolOperation::toggle_touch_lock; + plan.toggles_touch_lock = true; + return plan; +} + +} // namespace pp::app diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 4acf63b..5bcb789 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -8,6 +8,7 @@ #include "node_panel_floating.h" #include "app_core/app_preferences.h" #include "app_core/brush_ui.h" +#include "app_core/canvas_tool_ui.h" #include "app_core/document_layer.h" #include "app_core/app_status.h" #include "app_core/history_ui.h" @@ -569,22 +570,75 @@ void select_button(Node* main, T* button) { button->set_active(false); }; +kCanvasMode canvas_mode_from_tool(pp::app::CanvasToolMode mode) +{ + switch (mode) { + case pp::app::CanvasToolMode::draw: + return kCanvasMode::Draw; + case pp::app::CanvasToolMode::erase: + return kCanvasMode::Erase; + case pp::app::CanvasToolMode::line: + return kCanvasMode::Line; + case pp::app::CanvasToolMode::camera: + return kCanvasMode::Camera; + case pp::app::CanvasToolMode::grid: + return kCanvasMode::Grid; + case pp::app::CanvasToolMode::copy: + return kCanvasMode::Copy; + case pp::app::CanvasToolMode::cut: + return kCanvasMode::Cut; + case pp::app::CanvasToolMode::fill: + return kCanvasMode::Fill; + case pp::app::CanvasToolMode::mask_free: + return kCanvasMode::MaskFree; + case pp::app::CanvasToolMode::mask_line: + return kCanvasMode::MaskLine; + case pp::app::CanvasToolMode::flood_fill: + return kCanvasMode::FloodFill; + } + return kCanvasMode::Draw; +} + +template +void apply_canvas_tool_select(App& app, T* button, pp::app::CanvasToolMode mode) +{ + const auto plan = pp::app::plan_canvas_tool_select(mode); + if (plan.selects_toolbar_button) + select_button(app.layout[app.main_id], button); + + if (plan.transform_action == pp::app::CanvasToolTransformAction::copy) { + auto* transform = static_cast( + app.canvas->m_canvas->modes[(int)kCanvasMode::Copy][0]); + transform->m_action = CanvasModeTransform::ActionType::Copy; + } else if (plan.transform_action == pp::app::CanvasToolTransformAction::cut) { + auto* transform = static_cast( + app.canvas->m_canvas->modes[(int)kCanvasMode::Cut][0]); + transform->m_action = CanvasModeTransform::ActionType::Cut; + } + + if (plan.updates_canvas_mode) + Canvas::set_mode(canvas_mode_from_tool(plan.mode)); +} + void App::init_toolbar_draw() { if (auto* button = layout[main_id]->find("btn-pen")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Draw); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::draw); }; //button->set_active(true); - Canvas::set_mode(kCanvasMode::Draw); + const auto plan = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw); + if (plan.updates_canvas_mode) + Canvas::set_mode(canvas_mode_from_tool(plan.mode)); } if (auto* button = layout[main_id]->find("btn-pick")) { button->on_click = [this](Node*) { CanvasModePen* mode = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]; - if (mode && canvas->m_canvas->m_current_mode == kCanvasMode::Draw) + const auto plan = pp::app::plan_canvas_tool_pick_toggle( + canvas->m_canvas->m_current_mode == kCanvasMode::Draw); + if (mode && plan.toggles_picking) { mode->m_picking = !mode->m_picking; } @@ -593,82 +647,70 @@ void App::init_toolbar_draw() if (auto* button = layout[main_id]->find("btn-touchlock")) { button->on_click = [this](Node*) { - canvas->m_canvas->m_touch_lock = !canvas->m_canvas->m_touch_lock; + const auto plan = pp::app::plan_canvas_tool_touch_lock_toggle(); + if (plan.toggles_touch_lock) + canvas->m_canvas->m_touch_lock = !canvas->m_canvas->m_touch_lock; }; } if (auto* button = layout[main_id]->find("btn-erase")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Erase); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::erase); }; } if (auto* button = layout[main_id]->find("btn-line")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Line); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::line); }; } if (auto* button = layout[main_id]->find("btn-cam")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Camera); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::camera); }; } if (auto* button = layout[main_id]->find("btn-grid")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Grid); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::grid); }; } if (auto* button = layout[main_id]->find("btn-copy")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - auto m = static_cast(canvas->m_canvas->modes[(int)kCanvasMode::Copy][0]); - m->m_action = CanvasModeTransform::ActionType::Copy; - Canvas::set_mode(kCanvasMode::Copy); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::copy); }; } if (auto* button = layout[main_id]->find("btn-cut")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - auto m = static_cast(canvas->m_canvas->modes[(int)kCanvasMode::Cut][0]); - m->m_action = CanvasModeTransform::ActionType::Cut; - Canvas::set_mode(kCanvasMode::Cut); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::cut); }; } if (auto* button = layout[main_id]->find("btn-fill")) { // polygon fill button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::Fill); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::fill); }; } if (auto* button = layout[main_id]->find("btn-mask-free")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::MaskFree); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_free); }; } if (auto* button = layout[main_id]->find("btn-mask-line")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::MaskLine); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_line); }; } if (auto* button = layout[main_id]->find("btn-bucket")) { button->on_click = [this, button](Node*) { - select_button(layout[main_id], button); - Canvas::set_mode(kCanvasMode::FloodFill); + apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::flood_fill); }; } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e0f8b34..56511b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -278,6 +278,16 @@ add_test(NAME pp_app_core_brush_ui_tests COMMAND pp_app_core_brush_ui_tests) set_tests_properties(pp_app_core_brush_ui_tests PROPERTIES LABELS "app;paint;desktop-fast;fuzz") +add_executable(pp_app_core_canvas_tool_ui_tests + app_core/canvas_tool_ui_tests.cpp) +target_link_libraries(pp_app_core_canvas_tool_ui_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_canvas_tool_ui_tests COMMAND pp_app_core_canvas_tool_ui_tests) +set_tests_properties(pp_app_core_canvas_tool_ui_tests PROPERTIES + LABELS "app;ui;desktop-fast;fuzz") + add_executable(pp_app_core_grid_ui_tests app_core/grid_ui_tests.cpp) target_link_libraries(pp_app_core_grid_ui_tests PRIVATE @@ -846,6 +856,36 @@ if(TARGET pano_cli) LABELS "app;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_tool_draw_smoke + COMMAND pano_cli plan-canvas-tool --kind draw) + set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool\".*\"operation\":\"select-mode\".*\"mode\":\"draw\".*\"selectsToolbarButton\":true.*\"updatesCanvasMode\":true") + + add_test(NAME pano_cli_plan_canvas_tool_copy_smoke + COMMAND pano_cli plan-canvas-tool --kind copy) + set_tests_properties(pano_cli_plan_canvas_tool_copy_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool\".*\"mode\":\"copy\".*\"transformAction\":\"copy\".*\"updatesCanvasMode\":true") + + add_test(NAME pano_cli_plan_canvas_tool_pick_noop_smoke + COMMAND pano_cli plan-canvas-tool --kind pick) + set_tests_properties(pano_cli_plan_canvas_tool_pick_noop_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool\".*\"operation\":\"toggle-picking\".*\"togglesPicking\":false.*\"requiresDrawMode\":true.*\"noOp\":true") + + add_test(NAME pano_cli_plan_canvas_tool_touch_lock_smoke + COMMAND pano_cli plan-canvas-tool --kind touch-lock) + set_tests_properties(pano_cli_plan_canvas_tool_touch_lock_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool\".*\"operation\":\"toggle-touch-lock\".*\"togglesTouchLock\":true.*\"noOp\":false") + + add_test(NAME pano_cli_plan_canvas_tool_rejects_unknown + COMMAND pano_cli plan-canvas-tool --kind warp) + set_tests_properties(pano_cli_plan_canvas_tool_rejects_unknown PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_grid_operation_pick_smoke COMMAND pano_cli plan-grid-operation --kind pick) set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES diff --git a/tests/app_core/canvas_tool_ui_tests.cpp b/tests/app_core/canvas_tool_ui_tests.cpp new file mode 100644 index 0000000..7a48606 --- /dev/null +++ b/tests/app_core/canvas_tool_ui_tests.cpp @@ -0,0 +1,62 @@ +#include "app_core/canvas_tool_ui.h" +#include "test_harness.h" + +namespace { + +void selection_plans_canvas_modes(pp::tests::Harness& harness) +{ + const auto draw = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, draw.operation == pp::app::CanvasToolOperation::select_mode); + PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, draw.transform_action == pp::app::CanvasToolTransformAction::none); + PP_EXPECT(harness, draw.selects_toolbar_button); + PP_EXPECT(harness, draw.updates_canvas_mode); + PP_EXPECT(harness, !draw.no_op); + + const auto mask_line = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_line); + PP_EXPECT(harness, mask_line.mode == pp::app::CanvasToolMode::mask_line); + PP_EXPECT(harness, mask_line.updates_canvas_mode); +} + +void transform_tools_plan_copy_and_cut_actions(pp::tests::Harness& harness) +{ + const auto copy = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy); + PP_EXPECT(harness, copy.mode == pp::app::CanvasToolMode::copy); + PP_EXPECT(harness, copy.transform_action == pp::app::CanvasToolTransformAction::copy); + PP_EXPECT(harness, copy.updates_canvas_mode); + + const auto cut = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::cut); + PP_EXPECT(harness, cut.mode == pp::app::CanvasToolMode::cut); + PP_EXPECT(harness, cut.transform_action == pp::app::CanvasToolTransformAction::cut); + PP_EXPECT(harness, cut.updates_canvas_mode); +} + +void pick_and_touch_lock_toggle_state(pp::tests::Harness& harness) +{ + const auto pick_active = pp::app::plan_canvas_tool_pick_toggle(true); + PP_EXPECT(harness, pick_active.operation == pp::app::CanvasToolOperation::toggle_picking); + PP_EXPECT(harness, pick_active.requires_draw_mode); + PP_EXPECT(harness, pick_active.toggles_picking); + PP_EXPECT(harness, !pick_active.no_op); + + const auto pick_noop = pp::app::plan_canvas_tool_pick_toggle(false); + PP_EXPECT(harness, pick_noop.requires_draw_mode); + PP_EXPECT(harness, !pick_noop.toggles_picking); + PP_EXPECT(harness, pick_noop.no_op); + + const auto touch_lock = pp::app::plan_canvas_tool_touch_lock_toggle(); + PP_EXPECT(harness, touch_lock.operation == pp::app::CanvasToolOperation::toggle_touch_lock); + PP_EXPECT(harness, touch_lock.toggles_touch_lock); + PP_EXPECT(harness, !touch_lock.no_op); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("selection plans canvas modes", selection_plans_canvas_modes); + harness.run("transform tools plan copy and cut actions", transform_tools_plan_copy_and_cut_actions); + harness.run("pick and touch lock toggle state", pick_and_touch_lock_toggle_state); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 1191c95..86ec0fe 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,6 +1,7 @@ #include "app_core/app_preferences.h" #include "app_core/app_status.h" #include "app_core/brush_ui.h" +#include "app_core/canvas_tool_ui.h" #include "app_core/document_animation.h" #include "app_core/document_export.h" #include "app_core/document_cloud.h" @@ -284,6 +285,11 @@ struct PlanHistoryOperationArgs { int memory_bytes = 0; }; +struct PlanCanvasToolArgs { + std::string kind = "draw"; + bool current_mode_draw = false; +}; + struct PlanQuickOperationArgs { std::string kind = "brush"; int current_index = 0; @@ -623,6 +629,64 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep return "stroke-settings-changed"; } +const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept +{ + switch (operation) { + case pp::app::CanvasToolOperation::select_mode: + return "select-mode"; + case pp::app::CanvasToolOperation::toggle_picking: + return "toggle-picking"; + case pp::app::CanvasToolOperation::toggle_touch_lock: + return "toggle-touch-lock"; + } + + return "select-mode"; +} + +const char* canvas_tool_mode_name(pp::app::CanvasToolMode mode) noexcept +{ + switch (mode) { + case pp::app::CanvasToolMode::draw: + return "draw"; + case pp::app::CanvasToolMode::erase: + return "erase"; + case pp::app::CanvasToolMode::line: + return "line"; + case pp::app::CanvasToolMode::camera: + return "camera"; + case pp::app::CanvasToolMode::grid: + return "grid"; + case pp::app::CanvasToolMode::copy: + return "copy"; + case pp::app::CanvasToolMode::cut: + return "cut"; + case pp::app::CanvasToolMode::fill: + return "fill"; + case pp::app::CanvasToolMode::mask_free: + return "mask-free"; + case pp::app::CanvasToolMode::mask_line: + return "mask-line"; + case pp::app::CanvasToolMode::flood_fill: + return "bucket"; + } + + return "draw"; +} + +const char* canvas_tool_transform_action_name(pp::app::CanvasToolTransformAction action) noexcept +{ + switch (action) { + case pp::app::CanvasToolTransformAction::none: + return "none"; + case pp::app::CanvasToolTransformAction::copy: + return "copy"; + case pp::app::CanvasToolTransformAction::cut: + return "cut"; + } + + return "none"; +} + const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept { switch (operation) { @@ -940,6 +1004,7 @@ void print_help() << " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|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-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" + << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" << " plan-quick-operation --kind brush|color|restore|reset [--current-index N] [--slot-index N] [--brush-index N] [--color-index N] [--slot-count N] [--fire-event]\n" @@ -2981,6 +3046,119 @@ int plan_brush_operation(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_canvas_tool_args( + int argc, + char** argv, + PlanCanvasToolArgs& 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 == "--current-mode-draw") { + args.current_mode_draw = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Result make_canvas_tool_plan(const PlanCanvasToolArgs& args) +{ + if (args.kind == "pick") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_pick_toggle(args.current_mode_draw)); + } + if (args.kind == "touch-lock") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_touch_lock_toggle()); + } + if (args.kind == "draw") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw)); + } + if (args.kind == "erase") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::erase)); + } + if (args.kind == "line") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::line)); + } + if (args.kind == "camera") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::camera)); + } + if (args.kind == "grid") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::grid)); + } + if (args.kind == "copy") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy)); + } + if (args.kind == "cut") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::cut)); + } + if (args.kind == "fill") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::fill)); + } + if (args.kind == "mask-free") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_free)); + } + if (args.kind == "mask-line") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_line)); + } + if (args.kind == "bucket") { + return pp::foundation::Result::success( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::flood_fill)); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown canvas tool kind")); +} + +int plan_canvas_tool(int argc, char** argv) +{ + PlanCanvasToolArgs args; + const auto status = parse_plan_canvas_tool_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-canvas-tool", status.message); + return 2; + } + + const auto plan = make_canvas_tool_plan(args); + if (!plan) { + print_error("plan-canvas-tool", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-canvas-tool\"" + << ",\"state\":{\"kind\":\"" << json_escape(args.kind) + << "\",\"currentModeDraw\":" << json_bool(args.current_mode_draw) + << "},\"plan\":{\"operation\":\"" << canvas_tool_operation_name(value.operation) + << "\",\"mode\":\"" << canvas_tool_mode_name(value.mode) + << "\",\"transformAction\":\"" << canvas_tool_transform_action_name(value.transform_action) + << "\",\"selectsToolbarButton\":" << json_bool(value.selects_toolbar_button) + << ",\"updatesCanvasMode\":" << json_bool(value.updates_canvas_mode) + << ",\"togglesPicking\":" << json_bool(value.toggles_picking) + << ",\"togglesTouchLock\":" << json_bool(value.toggles_touch_lock) + << ",\"requiresDrawMode\":" << json_bool(value.requires_draw_mode) + << ",\"noOp\":" << json_bool(value.no_op) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_grid_operation_args( int argc, char** argv, @@ -5725,6 +5903,10 @@ int main(int argc, char** argv) return plan_brush_operation(argc, argv); } + if (command == "plan-canvas-tool") { + return plan_canvas_tool(argc, argv); + } + if (command == "plan-grid-operation") { return plan_grid_operation(argc, argv); }