From 91e1c2c9a3e1239685dca03fd3ea2c4f6ae1e6b6 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 11:26:58 +0200 Subject: [PATCH] Extract canvas toolbar state planning --- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 11 ++- src/app.cpp | 62 ++++++++++--- src/app_core/canvas_tool_ui.h | 40 ++++++++ tests/CMakeLists.txt | 18 ++++ tests/app_core/canvas_tool_ui_tests.cpp | 32 +++++++ tools/pano_cli/main.cpp | 116 ++++++++++++++++++++++++ 7 files changed, 263 insertions(+), 18 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index f1c785e..83dc545 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -44,7 +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 | +| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command and active-state planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `pano_cli plan-canvas-tool`, and `pano_cli plan-canvas-tool-state`, but live execution/state storage still mutates or reads 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-state --mode draw --picking --touch-lock`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, 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 33e10ab..ad93be6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -502,7 +502,9 @@ 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. +state mutation continue. `pano_cli plan-canvas-tool-state` exposes the matching +toolbar active-state refresh used by `App::update` before legacy `Canvas` mode +state remains the source of truth. `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 @@ -1164,13 +1166,18 @@ Results: 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. + touch-lock toggling, plus toolbar active-state derivation for draw, copy, and + bucket modes. - `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. +- `pano_cli_plan_canvas_tool_state_draw_smoke`, + `pano_cli_plan_canvas_tool_state_copy_smoke`, and + `pano_cli_plan_canvas_tool_state_rejects_unknown` passed and expose draw + toolbar active-state refresh 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.cpp b/src/app.cpp index 45ba764..868fdd2 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -6,6 +6,7 @@ #include "node_progress_bar.h" #include "mp4enc.h" #include "app_core/app_status.h" +#include "app_core/canvas_tool_ui.h" #include "app_core/document_recording.h" #include "app_core/document_route.h" #include "app_core/document_session.h" @@ -36,6 +37,36 @@ void enable_opengl_state(std::uint32_t state) noexcept glEnable(static_cast(state)); } +pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept +{ + switch (mode) { + case kCanvasMode::Draw: + return pp::app::CanvasToolMode::draw; + case kCanvasMode::Erase: + return pp::app::CanvasToolMode::erase; + case kCanvasMode::Line: + return pp::app::CanvasToolMode::line; + case kCanvasMode::Camera: + return pp::app::CanvasToolMode::camera; + case kCanvasMode::Grid: + return pp::app::CanvasToolMode::grid; + case kCanvasMode::Copy: + return pp::app::CanvasToolMode::copy; + case kCanvasMode::Cut: + return pp::app::CanvasToolMode::cut; + case kCanvasMode::Fill: + return pp::app::CanvasToolMode::fill; + case kCanvasMode::MaskFree: + return pp::app::CanvasToolMode::mask_free; + case kCanvasMode::MaskLine: + return pp::app::CanvasToolMode::mask_line; + case kCanvasMode::FloodFill: + return pp::app::CanvasToolMode::flood_fill; + default: + return pp::app::CanvasToolMode::draw; + } +} + void disable_opengl_state(std::uint32_t state) noexcept { glDisable(static_cast(state)); @@ -631,25 +662,26 @@ void App::update(float dt) main->update(width, height, zoom); { - static glm::vec4 color_button_normal{ .1, .1, .1, 1 }; - static glm::vec4 color_button_hlight{ 1, .0, .0, 1 }; - auto mode = Canvas::I->m_current_mode; CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]; - layout[main_id]->find("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking); - layout[main_id]->find("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock); + const auto toolbar = pp::app::plan_canvas_tool_button_state( + canvas_tool_mode_from_canvas_mode(mode), + pm && pm->m_picking, + canvas->m_canvas->m_touch_lock); + layout[main_id]->find("btn-pick")->set_active(toolbar.pick_active); + layout[main_id]->find("btn-touchlock")->set_active(toolbar.touch_lock_active); - layout[main_id]->find("btn-pen")->set_active(mode == kCanvasMode::Draw); - layout[main_id]->find("btn-erase")->set_active(mode == kCanvasMode::Erase); - layout[main_id]->find("btn-cam")->set_active(mode == kCanvasMode::Camera); - layout[main_id]->find("btn-line")->set_active(mode == kCanvasMode::Line); - layout[main_id]->find("btn-grid")->set_active(mode == kCanvasMode::Grid); - layout[main_id]->find("btn-copy")->set_active(mode == kCanvasMode::Copy); - layout[main_id]->find("btn-cut")->set_active(mode == kCanvasMode::Cut); - layout[main_id]->find("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree); - layout[main_id]->find("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine); - layout[main_id]->find("btn-bucket")->set_active(mode == kCanvasMode::FloodFill); + layout[main_id]->find("btn-pen")->set_active(toolbar.pen_active); + layout[main_id]->find("btn-erase")->set_active(toolbar.erase_active); + layout[main_id]->find("btn-cam")->set_active(toolbar.camera_active); + layout[main_id]->find("btn-line")->set_active(toolbar.line_active); + layout[main_id]->find("btn-grid")->set_active(toolbar.grid_active); + layout[main_id]->find("btn-copy")->set_active(toolbar.copy_active); + layout[main_id]->find("btn-cut")->set_active(toolbar.cut_active); + layout[main_id]->find("btn-mask-free")->set_active(toolbar.mask_free_active); + layout[main_id]->find("btn-mask-line")->set_active(toolbar.mask_line_active); + layout[main_id]->find("btn-bucket")->set_active(toolbar.flood_fill_active); } } diff --git a/src/app_core/canvas_tool_ui.h b/src/app_core/canvas_tool_ui.h index 9c38bb4..7be8b76 100644 --- a/src/app_core/canvas_tool_ui.h +++ b/src/app_core/canvas_tool_ui.h @@ -42,6 +42,23 @@ struct CanvasToolPlan { bool no_op = false; }; +struct CanvasToolButtonState { + CanvasToolMode mode = CanvasToolMode::draw; + bool pick_active = false; + bool touch_lock_active = false; + bool pen_active = false; + bool erase_active = false; + bool line_active = false; + bool camera_active = false; + bool grid_active = false; + bool copy_active = false; + bool cut_active = false; + bool fill_active = false; + bool mask_free_active = false; + bool mask_line_active = false; + bool flood_fill_active = false; +}; + [[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept { if (mode == CanvasToolMode::copy) { @@ -83,4 +100,27 @@ struct CanvasToolPlan { return plan; } +[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state( + CanvasToolMode mode, + bool picking, + bool touch_lock) noexcept +{ + CanvasToolButtonState state; + state.mode = mode; + state.pick_active = mode == CanvasToolMode::draw && picking; + state.touch_lock_active = touch_lock; + state.pen_active = mode == CanvasToolMode::draw; + state.erase_active = mode == CanvasToolMode::erase; + state.line_active = mode == CanvasToolMode::line; + state.camera_active = mode == CanvasToolMode::camera; + state.grid_active = mode == CanvasToolMode::grid; + state.copy_active = mode == CanvasToolMode::copy; + state.cut_active = mode == CanvasToolMode::cut; + state.fill_active = mode == CanvasToolMode::fill; + state.mask_free_active = mode == CanvasToolMode::mask_free; + state.mask_line_active = mode == CanvasToolMode::mask_line; + state.flood_fill_active = mode == CanvasToolMode::flood_fill; + return state; +} + } // namespace pp::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 56511b2..cd0c582 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -886,6 +886,24 @@ if(TARGET pano_cli) LABELS "app;ui;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_tool_state_draw_smoke + COMMAND pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock) + set_tests_properties(pano_cli_plan_canvas_tool_state_draw_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool-state\".*\"mode\":\"draw\".*\"pickActive\":true.*\"touchLockActive\":true.*\"penActive\":true") + + add_test(NAME pano_cli_plan_canvas_tool_state_copy_smoke + COMMAND pano_cli plan-canvas-tool-state --mode copy --picking) + set_tests_properties(pano_cli_plan_canvas_tool_state_copy_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool-state\".*\"mode\":\"copy\".*\"pickActive\":false.*\"penActive\":false.*\"copyActive\":true") + + add_test(NAME pano_cli_plan_canvas_tool_state_rejects_unknown + COMMAND pano_cli plan-canvas-tool-state --mode warp) + set_tests_properties(pano_cli_plan_canvas_tool_state_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 index 7a48606..ac7273b 100644 --- a/tests/app_core/canvas_tool_ui_tests.cpp +++ b/tests/app_core/canvas_tool_ui_tests.cpp @@ -50,6 +50,37 @@ void pick_and_touch_lock_toggle_state(pp::tests::Harness& harness) PP_EXPECT(harness, !touch_lock.no_op); } +void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness) +{ + const auto draw = pp::app::plan_canvas_tool_button_state( + pp::app::CanvasToolMode::draw, + true, + true); + PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, draw.pick_active); + PP_EXPECT(harness, draw.touch_lock_active); + PP_EXPECT(harness, draw.pen_active); + PP_EXPECT(harness, !draw.erase_active); + PP_EXPECT(harness, !draw.copy_active); + PP_EXPECT(harness, !draw.flood_fill_active); + + const auto copy = pp::app::plan_canvas_tool_button_state( + pp::app::CanvasToolMode::copy, + true, + false); + PP_EXPECT(harness, copy.copy_active); + PP_EXPECT(harness, !copy.pick_active); + PP_EXPECT(harness, !copy.touch_lock_active); + PP_EXPECT(harness, !copy.pen_active); + + const auto bucket = pp::app::plan_canvas_tool_button_state( + pp::app::CanvasToolMode::flood_fill, + false, + false); + PP_EXPECT(harness, bucket.flood_fill_active); + PP_EXPECT(harness, !bucket.mask_line_active); +} + } // namespace int main() @@ -58,5 +89,6 @@ int main() 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); + harness.run("button state tracks active mode and toggles", button_state_tracks_active_mode_and_toggles); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 86ec0fe..ff516bb 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -290,6 +290,12 @@ struct PlanCanvasToolArgs { bool current_mode_draw = false; }; +struct PlanCanvasToolStateArgs { + std::string mode = "draw"; + bool picking = false; + bool touch_lock = false; +}; + struct PlanQuickOperationArgs { std::string kind = "brush"; int current_index = 0; @@ -1005,6 +1011,7 @@ void print_help() << " 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-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\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" @@ -3127,6 +3134,46 @@ pp::foundation::Result make_canvas_tool_plan(const Plan pp::foundation::Status::invalid_argument("unknown canvas tool kind")); } +pp::foundation::Result parse_canvas_tool_mode(std::string_view mode) +{ + if (mode == "draw") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::draw); + } + if (mode == "erase") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::erase); + } + if (mode == "line") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::line); + } + if (mode == "camera") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::camera); + } + if (mode == "grid") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::grid); + } + if (mode == "copy") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::copy); + } + if (mode == "cut") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::cut); + } + if (mode == "fill") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::fill); + } + if (mode == "mask-free") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::mask_free); + } + if (mode == "mask-line") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::mask_line); + } + if (mode == "bucket") { + return pp::foundation::Result::success(pp::app::CanvasToolMode::flood_fill); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown canvas tool mode")); +} + int plan_canvas_tool(int argc, char** argv) { PlanCanvasToolArgs args; @@ -3159,6 +3206,71 @@ int plan_canvas_tool(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_canvas_tool_state_args( + int argc, + char** argv, + PlanCanvasToolStateArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--mode") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.mode = argv[++i]; + } else if (key == "--picking") { + args.picking = true; + } else if (key == "--touch-lock") { + args.touch_lock = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_canvas_tool_state(int argc, char** argv) +{ + PlanCanvasToolStateArgs args; + const auto status = parse_plan_canvas_tool_state_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-canvas-tool-state", status.message); + return 2; + } + + const auto mode = parse_canvas_tool_mode(args.mode); + if (!mode) { + print_error("plan-canvas-tool-state", mode.status().message); + return 2; + } + + const auto state = pp::app::plan_canvas_tool_button_state( + mode.value(), + args.picking, + args.touch_lock); + std::cout << "{\"ok\":true,\"command\":\"plan-canvas-tool-state\"" + << ",\"state\":{\"mode\":\"" << json_escape(args.mode) + << "\",\"picking\":" << json_bool(args.picking) + << ",\"touchLock\":" << json_bool(args.touch_lock) + << "},\"toolbar\":{\"mode\":\"" << canvas_tool_mode_name(state.mode) + << "\",\"pickActive\":" << json_bool(state.pick_active) + << ",\"touchLockActive\":" << json_bool(state.touch_lock_active) + << ",\"penActive\":" << json_bool(state.pen_active) + << ",\"eraseActive\":" << json_bool(state.erase_active) + << ",\"lineActive\":" << json_bool(state.line_active) + << ",\"cameraActive\":" << json_bool(state.camera_active) + << ",\"gridActive\":" << json_bool(state.grid_active) + << ",\"copyActive\":" << json_bool(state.copy_active) + << ",\"cutActive\":" << json_bool(state.cut_active) + << ",\"fillActive\":" << json_bool(state.fill_active) + << ",\"maskFreeActive\":" << json_bool(state.mask_free_active) + << ",\"maskLineActive\":" << json_bool(state.mask_line_active) + << ",\"bucketActive\":" << json_bool(state.flood_fill_active) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_grid_operation_args( int argc, char** argv, @@ -5907,6 +6019,10 @@ int main(int argc, char** argv) return plan_canvas_tool(argc, argv); } + if (command == "plan-canvas-tool-state") { + return plan_canvas_tool_state(argc, argv); + } + if (command == "plan-grid-operation") { return plan_grid_operation(argc, argv); }