Extract canvas tool UI planning

This commit is contained in:
2026-06-03 11:20:56 +02:00
parent 58afa672c7
commit 2087505921
8 changed files with 456 additions and 29 deletions

View File

@@ -226,6 +226,7 @@ add_library(pp_app_core STATIC
src/app_core/app_preferences.h src/app_core/app_preferences.h
src/app_core/app_status.h src/app_core/app_status.h
src/app_core/brush_ui.h src/app_core/brush_ui.h
src/app_core/canvas_tool_ui.h
src/app_core/document_animation.h src/app_core/document_animation.h
src/app_core/document_cloud.h src/app_core/document_cloud.h
src/app_core/document_export.cpp src/app_core/document_export.cpp

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -499,6 +499,10 @@ before legacy `Canvas`/`Layer` frame execution continues.
changes, tip/pattern/dual texture changes, preset brush replacement, and stroke changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel settings refreshes used by the live brush, quick, color, and floating panel
callbacks before legacy `Brush` mutation and resource loading continue. 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 `pano_cli plan-grid-operation` exposes app-core planning for grid heightmap
pick/load/reload/clear, lightmap render capability/limit checks, and 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 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_empty_reload`, and
`pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live
grid/heightmap/lightmap planning as JSON automation. 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, - `pp_app_core_history_ui_tests` passed, covering undo/redo availability,
no-op history commands, clear-history stack/memory state, memory-only clear, no-op history commands, clear-history stack/memory state, memory-only clear,
and negative metric rejection. and negative metric rejection.

View File

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

View File

@@ -8,6 +8,7 @@
#include "node_panel_floating.h" #include "node_panel_floating.h"
#include "app_core/app_preferences.h" #include "app_core/app_preferences.h"
#include "app_core/brush_ui.h" #include "app_core/brush_ui.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_layer.h" #include "app_core/document_layer.h"
#include "app_core/app_status.h" #include "app_core/app_status.h"
#include "app_core/history_ui.h" #include "app_core/history_ui.h"
@@ -569,22 +570,75 @@ void select_button(Node* main, T* button) {
button->set_active(false); 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<class T>
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<CanvasModeTransform*>(
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<CanvasModeTransform*>(
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() void App::init_toolbar_draw()
{ {
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-pen")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-pen"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::draw);
Canvas::set_mode(kCanvasMode::Draw);
}; };
//button->set_active(true); //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<NodeButtonCustom>("btn-pick")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-pick"))
{ {
button->on_click = [this](Node*) { button->on_click = [this](Node*) {
CanvasModePen* mode = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]; 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; mode->m_picking = !mode->m_picking;
} }
@@ -593,82 +647,70 @@ void App::init_toolbar_draw()
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-touchlock")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-touchlock"))
{ {
button->on_click = [this](Node*) { 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<NodeButtonCustom>("btn-erase")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-erase"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::erase);
Canvas::set_mode(kCanvasMode::Erase);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-line")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-line"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::line);
Canvas::set_mode(kCanvasMode::Line);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-cam")) if (auto* button = layout[main_id]->find<NodeButton>("btn-cam"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::camera);
Canvas::set_mode(kCanvasMode::Camera);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-grid")) if (auto* button = layout[main_id]->find<NodeButton>("btn-grid"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::grid);
Canvas::set_mode(kCanvasMode::Grid);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-copy")) if (auto* button = layout[main_id]->find<NodeButton>("btn-copy"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::copy);
auto m = static_cast<CanvasModeTransform*>(canvas->m_canvas->modes[(int)kCanvasMode::Copy][0]);
m->m_action = CanvasModeTransform::ActionType::Copy;
Canvas::set_mode(kCanvasMode::Copy);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-cut")) if (auto* button = layout[main_id]->find<NodeButton>("btn-cut"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::cut);
auto m = static_cast<CanvasModeTransform*>(canvas->m_canvas->modes[(int)kCanvasMode::Cut][0]);
m->m_action = CanvasModeTransform::ActionType::Cut;
Canvas::set_mode(kCanvasMode::Cut);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-fill")) if (auto* button = layout[main_id]->find<NodeButton>("btn-fill"))
{ {
// polygon fill // polygon fill
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::fill);
Canvas::set_mode(kCanvasMode::Fill);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-mask-free")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-mask-free"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_free);
Canvas::set_mode(kCanvasMode::MaskFree);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-mask-line")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-mask-line"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_line);
Canvas::set_mode(kCanvasMode::MaskLine);
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-bucket")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-bucket"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
select_button(layout[main_id], button); apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::flood_fill);
Canvas::set_mode(kCanvasMode::FloodFill);
}; };
} }
} }

View File

@@ -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 set_tests_properties(pp_app_core_brush_ui_tests PROPERTIES
LABELS "app;paint;desktop-fast;fuzz") 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 add_executable(pp_app_core_grid_ui_tests
app_core/grid_ui_tests.cpp) app_core/grid_ui_tests.cpp)
target_link_libraries(pp_app_core_grid_ui_tests PRIVATE 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" LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) 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 add_test(NAME pano_cli_plan_grid_operation_pick_smoke
COMMAND pano_cli plan-grid-operation --kind pick) COMMAND pano_cli plan-grid-operation --kind pick)
set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES

View File

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

View File

@@ -1,6 +1,7 @@
#include "app_core/app_preferences.h" #include "app_core/app_preferences.h"
#include "app_core/app_status.h" #include "app_core/app_status.h"
#include "app_core/brush_ui.h" #include "app_core/brush_ui.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_animation.h" #include "app_core/document_animation.h"
#include "app_core/document_export.h" #include "app_core/document_export.h"
#include "app_core/document_cloud.h" #include "app_core/document_cloud.h"
@@ -284,6 +285,11 @@ struct PlanHistoryOperationArgs {
int memory_bytes = 0; int memory_bytes = 0;
}; };
struct PlanCanvasToolArgs {
std::string kind = "draw";
bool current_mode_draw = false;
};
struct PlanQuickOperationArgs { struct PlanQuickOperationArgs {
std::string kind = "brush"; std::string kind = "brush";
int current_index = 0; int current_index = 0;
@@ -623,6 +629,64 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
return "stroke-settings-changed"; 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 const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept
{ {
switch (operation) { 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-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-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-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-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-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" << " 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; 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<pp::app::CanvasToolPlan> make_canvas_tool_plan(const PlanCanvasToolArgs& args)
{
if (args.kind == "pick") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_pick_toggle(args.current_mode_draw));
}
if (args.kind == "touch-lock") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_touch_lock_toggle());
}
if (args.kind == "draw") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw));
}
if (args.kind == "erase") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::erase));
}
if (args.kind == "line") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::line));
}
if (args.kind == "camera") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::camera));
}
if (args.kind == "grid") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::grid));
}
if (args.kind == "copy") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy));
}
if (args.kind == "cut") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::cut));
}
if (args.kind == "fill") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::fill));
}
if (args.kind == "mask-free") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_free));
}
if (args.kind == "mask-line") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_line));
}
if (args.kind == "bucket") {
return pp::foundation::Result<pp::app::CanvasToolPlan>::success(
pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::flood_fill));
}
return pp::foundation::Result<pp::app::CanvasToolPlan>::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( pp::foundation::Status parse_plan_grid_operation_args(
int argc, int argc,
char** argv, char** argv,
@@ -5725,6 +5903,10 @@ int main(int argc, char** argv)
return plan_brush_operation(argc, argv); return plan_brush_operation(argc, argv);
} }
if (command == "plan-canvas-tool") {
return plan_canvas_tool(argc, argv);
}
if (command == "plan-grid-operation") { if (command == "plan-grid-operation") {
return plan_grid_operation(argc, argv); return plan_grid_operation(argc, argv);
} }