diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 1be9e96..f3cd794 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 and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter | | DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter still mutates legacy `ActionManager` stacks 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 injected document/app history services with no legacy `ActionManager` adapter | -| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, and active-state planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `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, stylus eraser, and keyboard draw/erase 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, stylus eraser/key mode switching, and transform action execution are owned by app/document/canvas services with toolbar/canvas callbacks acting only as adapters | +| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, and active-state planning/execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-state`, and the `CanvasToolServices` boundary, but live adapters still mutate or read legacy `Canvas` mode state, pen picking state, touch-lock state, and transform copy/cut action objects | Preserve current toolbar, stylus eraser, and keyboard draw/erase 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, stylus eraser/key mode switching, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter | | DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, but the live adapter still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter | | 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 | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 9ce41f1..822710c 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -507,12 +507,13 @@ callbacks. Brush UI execution now dispatches through `BrushUiServices` before the legacy `Brush`/panel adapter mutates brush state or loads brush resources. `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-canvas-tool-state` exposes the matching -toolbar active-state refresh used by `App::update` before legacy `Canvas` mode -state remains the source of truth. `NodeCanvas` stylus eraser and `E` key -draw/erase mode switching also consume the same app-core command planner before -legacy canvas mode execution continues. +commands. Canvas tool execution now dispatches through `CanvasToolServices` +before legacy toolbar selection, `Canvas` mode, pen picking, touch-lock, and +transform state adapters 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. `NodeCanvas` stylus eraser +and `E` key draw/erase mode switching also consume the same app-core executor +before legacy canvas mode execution continues. `pano_cli plan-canvas-clear` exposes app-core planning for the main toolbar clear-current-layer command, including clear color validation, no-canvas handling, undo recording intent, and dirty-state intent; live toolbar execution @@ -1229,7 +1230,8 @@ Results: - `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, plus toolbar active-state derivation for draw, copy, and - bucket modes. + bucket modes, service dispatch ordering, pick no-op execution, and malformed + execution payload rejection. - `pano_cli_plan_canvas_tool_draw_smoke`, `pano_cli_plan_canvas_tool_copy_smoke`, `pano_cli_plan_canvas_tool_pick_noop_smoke`, diff --git a/src/app_core/canvas_tool_ui.h b/src/app_core/canvas_tool_ui.h index 7be8b76..a9e1a52 100644 --- a/src/app_core/canvas_tool_ui.h +++ b/src/app_core/canvas_tool_ui.h @@ -59,6 +59,17 @@ struct CanvasToolButtonState { bool flood_fill_active = false; }; +class CanvasToolServices { +public: + virtual ~CanvasToolServices() = default; + + virtual void select_toolbar_button(CanvasToolMode mode) = 0; + virtual void set_transform_action(CanvasToolTransformAction action) = 0; + virtual void set_canvas_mode(CanvasToolMode mode) = 0; + virtual void toggle_picking() = 0; + virtual void toggle_touch_lock() = 0; +}; + [[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept { if (mode == CanvasToolMode::copy) { @@ -123,4 +134,47 @@ struct CanvasToolButtonState { return state; } +[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan( + const CanvasToolPlan& plan, + CanvasToolServices& services) +{ + switch (plan.operation) { + case CanvasToolOperation::select_mode: + if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) { + return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode"); + } + if (plan.transform_action != transform_action_for_mode(plan.mode)) { + return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action"); + } + services.select_toolbar_button(plan.mode); + if (plan.transform_action != CanvasToolTransformAction::none) { + services.set_transform_action(plan.transform_action); + } + services.set_canvas_mode(plan.mode); + return pp::foundation::Status::success(); + + case CanvasToolOperation::toggle_picking: + if (!plan.requires_draw_mode) { + return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode"); + } + if (plan.no_op) { + return pp::foundation::Status::success(); + } + if (!plan.toggles_picking) { + return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op"); + } + services.toggle_picking(); + return pp::foundation::Status::success(); + + case CanvasToolOperation::toggle_touch_lock: + if (!plan.toggles_touch_lock || plan.no_op) { + return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock"); + } + services.toggle_touch_lock(); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown canvas tool operation"); +} + } // namespace pp::app diff --git a/src/app_layout.cpp b/src/app_layout.cpp index fd81dc9..3b7ddbf 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -1136,8 +1136,19 @@ void App::init_sidebar() }; } } -template -void select_button(Node* main, T* button) { +void set_canvas_tool_button_active(Node* button, bool active) +{ + if (auto* custom = dynamic_cast(button)) { + custom->set_active(active); + return; + } + if (auto* regular = dynamic_cast(button)) { + regular->set_active(active); + } +} + +void select_canvas_tool_button(Node* main, Node* button) +{ main->find("btn-pen")->set_active(false); main->find("btn-erase")->set_active(false); main->find("btn-line")->set_active(false); @@ -1145,12 +1156,11 @@ void select_button(Node* main, T* button) { main->find("btn-grid")->set_active(false); main->find("btn-copy")->set_active(false); main->find("btn-cut")->set_active(false); - //main->find("btn-fill")->set_color(color_button_normal); main->find("btn-mask-free")->set_active(false); main->find("btn-mask-line")->set_active(false); main->find("btn-bucket")->set_active(false); - button->set_active(false); -}; + set_canvas_tool_button_active(button, false); +} kCanvasMode canvas_mode_from_tool(pp::app::CanvasToolMode mode) { @@ -1181,25 +1191,73 @@ kCanvasMode canvas_mode_from_tool(pp::app::CanvasToolMode mode) return kCanvasMode::Draw; } +class LegacyCanvasToolServices final : public pp::app::CanvasToolServices { +public: + LegacyCanvasToolServices(App& app, Node* toolbar_button = nullptr) noexcept + : app_(app) + , toolbar_button_(toolbar_button) + { + } + + void select_toolbar_button(pp::app::CanvasToolMode) override + { + if (toolbar_button_) + select_canvas_tool_button(app_.layout[app_.main_id], toolbar_button_); + } + + void set_transform_action(pp::app::CanvasToolTransformAction action) override + { + if (!app_.canvas || !app_.canvas->m_canvas) + return; + + if (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 (action == pp::app::CanvasToolTransformAction::cut) { + auto* transform = static_cast( + app_.canvas->m_canvas->modes[(int)kCanvasMode::Cut][0]); + transform->m_action = CanvasModeTransform::ActionType::Cut; + } + } + + void set_canvas_mode(pp::app::CanvasToolMode mode) override + { + Canvas::set_mode(canvas_mode_from_tool(mode)); + } + + void toggle_picking() override + { + if (!app_.canvas || !app_.canvas->m_canvas) + return; + + auto* mode = static_cast( + app_.canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]); + if (mode) + mode->m_picking = !mode->m_picking; + } + + void toggle_touch_lock() override + { + if (!app_.canvas || !app_.canvas->m_canvas) + return; + + app_.canvas->m_canvas->m_touch_lock = !app_.canvas->m_canvas->m_touch_lock; + } + +private: + App& app_; + Node* toolbar_button_ = nullptr; +}; + 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)); + LegacyCanvasToolServices services(app, button); + const auto status = pp::app::execute_canvas_tool_plan(plan, services); + if (!status.ok()) + LOG("Canvas tool select action failed: %s", status.message); } void App::init_toolbar_draw() @@ -1211,27 +1269,30 @@ void App::init_toolbar_draw() }; //button->set_active(true); 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)); + LegacyCanvasToolServices services(*this); + const auto status = pp::app::execute_canvas_tool_plan(plan, services); + if (!status.ok()) + LOG("Canvas default tool action failed: %s", status.message); } 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]; 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; - } + LegacyCanvasToolServices services(*this); + const auto status = pp::app::execute_canvas_tool_plan(plan, services); + if (!status.ok()) + LOG("Canvas pick action failed: %s", status.message); }; } if (auto* button = layout[main_id]->find("btn-touchlock")) { button->on_click = [this](Node*) { 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; + LegacyCanvasToolServices services(*this); + const auto status = pp::app::execute_canvas_tool_plan(plan, services); + if (!status.ok()) + LOG("Canvas touch-lock action failed: %s", status.message); }; } if (auto* button = layout[main_id]->find("btn-erase")) diff --git a/src/node_canvas.cpp b/src/node_canvas.cpp index 886f9e2..782f2fb 100644 --- a/src/node_canvas.cpp +++ b/src/node_canvas.cpp @@ -37,22 +37,46 @@ void run_history_redo_if_available() ActionManager::redo(); } +class LegacyNodeCanvasToolServices final : public pp::app::CanvasToolServices { +public: + void select_toolbar_button(pp::app::CanvasToolMode) override + { + } + + void set_transform_action(pp::app::CanvasToolTransformAction) override + { + } + + void set_canvas_mode(pp::app::CanvasToolMode mode) override + { + switch (mode) { + case pp::app::CanvasToolMode::draw: + Canvas::set_mode(kCanvasMode::Draw); + return; + case pp::app::CanvasToolMode::erase: + Canvas::set_mode(kCanvasMode::Erase); + return; + default: + return; + } + } + + void toggle_picking() override + { + } + + void toggle_touch_lock() override + { + } +}; + void run_canvas_tool_mode(pp::app::CanvasToolMode mode) { const auto plan = pp::app::plan_canvas_tool_select(mode); - if (!plan.updates_canvas_mode) - return; - - switch (plan.mode) { - case pp::app::CanvasToolMode::draw: - Canvas::set_mode(kCanvasMode::Draw); - return; - case pp::app::CanvasToolMode::erase: - Canvas::set_mode(kCanvasMode::Erase); - return; - default: - return; - } + LegacyNodeCanvasToolServices services; + const auto status = pp::app::execute_canvas_tool_plan(plan, services); + if (!status.ok()) + LOG("Canvas input tool action failed: %s", status.message); } } diff --git a/tests/app_core/canvas_tool_ui_tests.cpp b/tests/app_core/canvas_tool_ui_tests.cpp index ac7273b..99e40e6 100644 --- a/tests/app_core/canvas_tool_ui_tests.cpp +++ b/tests/app_core/canvas_tool_ui_tests.cpp @@ -1,8 +1,55 @@ #include "app_core/canvas_tool_ui.h" #include "test_harness.h" +#include + namespace { +class FakeCanvasToolServices final : public pp::app::CanvasToolServices { +public: + void select_toolbar_button(pp::app::CanvasToolMode mode) override + { + toolbar_selections += 1; + last_mode = mode; + call_order += "select;"; + } + + void set_transform_action(pp::app::CanvasToolTransformAction action) override + { + transform_sets += 1; + last_transform_action = action; + call_order += "transform;"; + } + + void set_canvas_mode(pp::app::CanvasToolMode mode) override + { + mode_sets += 1; + last_mode = mode; + call_order += "mode;"; + } + + void toggle_picking() override + { + picking_toggles += 1; + call_order += "pick;"; + } + + void toggle_touch_lock() override + { + touch_lock_toggles += 1; + call_order += "touch;"; + } + + int toolbar_selections = 0; + int transform_sets = 0; + int mode_sets = 0; + int picking_toggles = 0; + int touch_lock_toggles = 0; + pp::app::CanvasToolMode last_mode = pp::app::CanvasToolMode::draw; + pp::app::CanvasToolTransformAction last_transform_action = pp::app::CanvasToolTransformAction::none; + std::string call_order; +}; + void selection_plans_canvas_modes(pp::tests::Harness& harness) { const auto draw = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw); @@ -81,6 +128,74 @@ void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness) PP_EXPECT(harness, !bucket.mask_line_active); } +void executor_dispatches_tool_actions(pp::tests::Harness& harness) +{ + FakeCanvasToolServices services; + + PP_EXPECT(harness, pp::app::execute_canvas_tool_plan( + pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy), + services).ok()); + PP_EXPECT(harness, pp::app::execute_canvas_tool_plan( + pp::app::plan_canvas_tool_pick_toggle(true), + services).ok()); + PP_EXPECT(harness, pp::app::execute_canvas_tool_plan( + pp::app::plan_canvas_tool_touch_lock_toggle(), + services).ok()); + + PP_EXPECT(harness, services.toolbar_selections == 1); + PP_EXPECT(harness, services.transform_sets == 1); + PP_EXPECT(harness, services.mode_sets == 1); + PP_EXPECT(harness, services.picking_toggles == 1); + PP_EXPECT(harness, services.touch_lock_toggles == 1); + PP_EXPECT(harness, services.last_mode == pp::app::CanvasToolMode::copy); + PP_EXPECT(harness, services.last_transform_action == pp::app::CanvasToolTransformAction::copy); + PP_EXPECT(harness, services.call_order == "select;transform;mode;pick;touch;"); +} + +void executor_no_ops_pick_when_not_in_draw_mode(pp::tests::Harness& harness) +{ + FakeCanvasToolServices services; + + PP_EXPECT(harness, pp::app::execute_canvas_tool_plan( + pp::app::plan_canvas_tool_pick_toggle(false), + services).ok()); + + PP_EXPECT(harness, services.toolbar_selections == 0); + PP_EXPECT(harness, services.mode_sets == 0); + PP_EXPECT(harness, services.picking_toggles == 0); + PP_EXPECT(harness, services.call_order.empty()); +} + +void executor_rejects_malformed_plans(pp::tests::Harness& harness) +{ + FakeCanvasToolServices services; + + auto select = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy); + select.transform_action = pp::app::CanvasToolTransformAction::cut; + PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(select, services).ok()); + + auto missing_update = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw); + missing_update.updates_canvas_mode = false; + PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(missing_update, services).ok()); + + pp::app::CanvasToolPlan pick; + pick.operation = pp::app::CanvasToolOperation::toggle_picking; + pick.requires_draw_mode = false; + pick.toggles_picking = true; + PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(pick, services).ok()); + + pp::app::CanvasToolPlan touch; + touch.operation = pp::app::CanvasToolOperation::toggle_touch_lock; + touch.toggles_touch_lock = false; + PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(touch, services).ok()); + + PP_EXPECT(harness, services.toolbar_selections == 0); + PP_EXPECT(harness, services.transform_sets == 0); + PP_EXPECT(harness, services.mode_sets == 0); + PP_EXPECT(harness, services.picking_toggles == 0); + PP_EXPECT(harness, services.touch_lock_toggles == 0); +} + } // namespace int main() @@ -90,5 +205,8 @@ int main() 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); + harness.run("executor dispatches tool actions", executor_dispatches_tool_actions); + harness.run("executor no-ops pick when not in draw mode", executor_no_ops_pick_when_not_in_draw_mode); + harness.run("executor rejects malformed plans", executor_rejects_malformed_plans); return harness.finish(); }