Extract canvas toolbar state planning

This commit is contained in:
2026-06-03 11:26:58 +02:00
parent 2087505921
commit 91e1c2c9a3
7 changed files with 263 additions and 18 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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<GLenum>(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<GLenum>(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<NodeButtonCustom>("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking);
layout[main_id]->find<NodeButtonCustom>("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<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(mode == kCanvasMode::Draw);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(mode == kCanvasMode::Erase);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(mode == kCanvasMode::FloodFill);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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<pp::app::CanvasToolPlan> make_canvas_tool_plan(const Plan
pp::foundation::Status::invalid_argument("unknown canvas tool kind"));
}
pp::foundation::Result<pp::app::CanvasToolMode> parse_canvas_tool_mode(std::string_view mode)
{
if (mode == "draw") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::draw);
}
if (mode == "erase") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::erase);
}
if (mode == "line") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::line);
}
if (mode == "camera") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::camera);
}
if (mode == "grid") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::grid);
}
if (mode == "copy") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::copy);
}
if (mode == "cut") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::cut);
}
if (mode == "fill") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::fill);
}
if (mode == "mask-free") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::mask_free);
}
if (mode == "mask-line") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::mask_line);
}
if (mode == "bucket") {
return pp::foundation::Result<pp::app::CanvasToolMode>::success(pp::app::CanvasToolMode::flood_fill);
}
return pp::foundation::Result<pp::app::CanvasToolMode>::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);
}