Route canvas cursor visibility through app core
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Build And Platform Inventory
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-04
|
||||
Last updated: 2026-06-05
|
||||
|
||||
This inventory records the known build surfaces during the CMake migration.
|
||||
Keep it updated as platform paths move to shared CMake targets.
|
||||
@@ -245,9 +245,12 @@ Known local toolchain state:
|
||||
canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and
|
||||
canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core`
|
||||
contracts while canvas mode tip visibility and pressure remapping now ask
|
||||
`PlatformServices`; legacy `Canvas` mode state, transform actions, picking,
|
||||
touch-lock, save/UI/cursor calls, brush-size controls, and history execution
|
||||
remain tracked by `DEBT-0027`.
|
||||
`PlatformServices`; `NodeCanvas::update_cursor()` now consumes
|
||||
`pp_app_core` canvas cursor visibility planning exposed through
|
||||
`pano_cli plan-canvas-cursor` before retained platform cursor dispatch.
|
||||
Legacy `Canvas` mode state, transform actions, picking, touch-lock,
|
||||
save/UI/cursor calls, brush-size controls, and history execution remain
|
||||
tracked by `DEBT-0027`.
|
||||
- `src/legacy_document_animation_services.*` is the current UI-shell bridge for
|
||||
animation frame commands, timeline/selected-frame execution, playback ticks,
|
||||
onion-size updates, and play-mode toggles. It keeps those live paths on the
|
||||
|
||||
@@ -68,6 +68,13 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
`pano_cli plan-quick-slider-preview` exposes the path for automation. Legacy
|
||||
quick widgets, brush previews, popup state, and direct `CanvasMode*` field
|
||||
writes remain open under DEBT-0025.
|
||||
- 2026-06-05: DEBT-0027 was narrowed. Canvas cursor visibility policy now goes
|
||||
through tested `pp_app_core`, live `NodeCanvas::update_cursor()` consumes the
|
||||
planner before retained platform cursor dispatch, and
|
||||
`pano_cli plan-canvas-cursor` exposes draw/erase versus non-paint mode,
|
||||
small-brush, not-painting, modifier, and malformed-brush states for
|
||||
automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor
|
||||
execution remain open under DEBT-0027.
|
||||
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
|
||||
thumbnail, and object-draw history paths now query saved blend state through
|
||||
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
|
||||
@@ -108,7 +115,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | 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, mini-state, and size/flow slider preview planning now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, `pano_cli plan-quick-slider-preview`, and the `QuickUiServices` boundary; live slot/popup/restore/reset execution is centralized in `src/legacy_quick_ui_services.*`, and live slider callbacks now consume `pp_app_core` preview cursor/tip planning directly, but the bridge and panel adapter still mutate legacy quick UI widgets, `Brush` previews, color picker popup state, preset popup state, and direct legacy `CanvasModePen`/`CanvasModeLine` fields | Preserve quick-panel behavior while quick brush/color and slider preview 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`; `pano_cli plan-quick-slider-preview --slider-x 10 --slider-y 20 --slider-height 40 --zoom 2 --pen-mode --no-line-mode`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, color execution, and slider preview mode updates are owned by injected app/brush/UI/canvas services with no legacy quick-panel adapter, popup adapter, direct brush-preview mutation, or direct `CanvasMode*` field writes |
|
||||
| DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge 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, active-state planning/execution dispatch, and canvas keyboard/touch command planning 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`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter |
|
||||
| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, active-state planning/execution dispatch, canvas cursor visibility planning, and canvas keyboard/touch command planning 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`, `pano_cli plan-canvas-cursor`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, `NodeCanvas::update_cursor()` consumes the cursor planner before retained platform cursor dispatch, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, `CanvasModePen` drawing/resizing/picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, cursor visibility, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5`; `pano_cli plan-canvas-cursor --visibility small-brush --bad-size`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, cursor visibility planning/execution, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, 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`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge 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`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `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, and live File-menu import execution is centralized in `src/legacy_app_shell_services.*`, but the bridge 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, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge 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 |
|
||||
|
||||
@@ -579,7 +579,11 @@ touch command handling now consumes
|
||||
`pp_app_core` canvas-hotkey planning for E draw/erase, Ctrl+Z, Ctrl+Shift+Z,
|
||||
Ctrl+S, Ctrl+Shift+S, Tab UI toggle, brush-size brackets, Android back, Alt
|
||||
cursor reveal, and two-finger undo before the shared bridge delegates to legacy
|
||||
UI/canvas/history adapters.
|
||||
UI/canvas/history adapters. `pano_cli plan-canvas-cursor` exposes the
|
||||
canvas-specific cursor visibility policy for draw/erase versus non-paint
|
||||
modes, small-brush thresholds, active-stroke hiding, and modifier/tool forced
|
||||
visibility; live `NodeCanvas::update_cursor()` consumes that planner before
|
||||
retained `App::show_cursor`/`App::hide_cursor` platform dispatch.
|
||||
`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
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasToolOperation {
|
||||
@@ -30,6 +32,13 @@ enum class CanvasToolTransformAction {
|
||||
cut,
|
||||
};
|
||||
|
||||
enum class CanvasCursorVisibilityMode {
|
||||
never,
|
||||
small_brush,
|
||||
not_painting,
|
||||
always,
|
||||
};
|
||||
|
||||
struct CanvasToolPlan {
|
||||
CanvasToolOperation operation = CanvasToolOperation::select_mode;
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
@@ -59,6 +68,25 @@ struct CanvasToolButtonState {
|
||||
bool flood_fill_active = false;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityInput {
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never;
|
||||
bool has_current_brush = true;
|
||||
float brush_tip_size = 0.0F;
|
||||
bool pen_is_drawing = false;
|
||||
bool alt_down = false;
|
||||
bool pen_is_resizing = false;
|
||||
bool pen_is_picking = false;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityPlan {
|
||||
bool visible = true;
|
||||
bool paint_mode = false;
|
||||
bool uses_brush_size = false;
|
||||
bool uses_pen_state = false;
|
||||
bool forced_visible_by_modifier_or_tool = false;
|
||||
};
|
||||
|
||||
class CanvasToolServices {
|
||||
public:
|
||||
virtual ~CanvasToolServices() = default;
|
||||
@@ -134,6 +162,54 @@ public:
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool canvas_tool_mode_is_paint(CanvasToolMode mode) noexcept
|
||||
{
|
||||
return mode == CanvasToolMode::draw || mode == CanvasToolMode::erase;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasCursorVisibilityPlan> plan_canvas_cursor_visibility(
|
||||
const CanvasCursorVisibilityInput& input)
|
||||
{
|
||||
CanvasCursorVisibilityPlan plan;
|
||||
plan.paint_mode = canvas_tool_mode_is_paint(input.mode);
|
||||
if (!plan.paint_mode) {
|
||||
plan.visible = true;
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
switch (input.visibility_mode) {
|
||||
case CanvasCursorVisibilityMode::always:
|
||||
plan.visible = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::never:
|
||||
plan.visible = false;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::small_brush:
|
||||
if (!input.has_current_brush) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor small-brush mode requires a current brush"));
|
||||
}
|
||||
if (!std::isfinite(input.brush_tip_size) || input.brush_tip_size < 0.0F) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor brush size must be finite and non-negative"));
|
||||
}
|
||||
plan.visible = input.brush_tip_size < 10.0F;
|
||||
plan.uses_brush_size = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::not_painting:
|
||||
plan.visible = !input.pen_is_drawing;
|
||||
plan.uses_pen_state = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (input.alt_down || input.pen_is_resizing || input.pen_is_picking) {
|
||||
plan.visible = true;
|
||||
plan.forced_visible_by_modifier_or_tool = true;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
|
||||
const CanvasToolPlan& plan,
|
||||
CanvasToolServices& services)
|
||||
|
||||
@@ -258,6 +258,54 @@ pp::app::CanvasHotkeyKey canvas_hotkey_key(kKey key) noexcept
|
||||
}
|
||||
}
|
||||
|
||||
pp::app::CanvasToolMode canvas_tool_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;
|
||||
case kCanvasMode::COUNT:
|
||||
return pp::app::CanvasToolMode::draw;
|
||||
}
|
||||
|
||||
return pp::app::CanvasToolMode::draw;
|
||||
}
|
||||
|
||||
pp::app::CanvasCursorVisibilityMode canvas_cursor_visibility_mode(NodeCanvas::kCursorVisibility mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case NodeCanvas::kCursorVisibility::Never:
|
||||
return pp::app::CanvasCursorVisibilityMode::never;
|
||||
case NodeCanvas::kCursorVisibility::SmallBrush:
|
||||
return pp::app::CanvasCursorVisibilityMode::small_brush;
|
||||
case NodeCanvas::kCursorVisibility::NotPainting:
|
||||
return pp::app::CanvasCursorVisibilityMode::not_painting;
|
||||
case NodeCanvas::kCursorVisibility::Always:
|
||||
return pp::app::CanvasCursorVisibilityMode::always;
|
||||
}
|
||||
|
||||
return pp::app::CanvasCursorVisibilityMode::never;
|
||||
}
|
||||
|
||||
pp::app::CanvasHotkeyState canvas_hotkey_state(bool mouse_focused, int touch_finger_count = 0) noexcept
|
||||
{
|
||||
pp::app::CanvasHotkeyState state;
|
||||
@@ -961,24 +1009,24 @@ void NodeCanvas::set_cursor_visibility(kCursorVisibility mode)
|
||||
|
||||
void NodeCanvas::update_cursor()
|
||||
{
|
||||
bool visible = true;
|
||||
if (m_canvas->m_current_mode == kCanvasMode::Draw ||
|
||||
m_canvas->m_current_mode == kCanvasMode::Erase)
|
||||
{
|
||||
if (m_cursor_visibility == kCursorVisibility::Always)
|
||||
visible = true;
|
||||
if (m_cursor_visibility == kCursorVisibility::Never)
|
||||
visible = false;
|
||||
if (m_cursor_visibility == kCursorVisibility::SmallBrush)
|
||||
visible = m_canvas->m_current_brush->m_tip_size < 10;
|
||||
if (m_cursor_visibility == kCursorVisibility::NotPainting)
|
||||
visible = !m_canvas->get_mode<CanvasModePen>()->m_drawing;
|
||||
if (App::I->keys[(int)kKey::KeyAlt] ||
|
||||
m_canvas->get_mode<CanvasModePen>()->m_resizing ||
|
||||
m_canvas->get_mode<CanvasModePen>()->m_picking)
|
||||
visible = true;
|
||||
auto* pen_mode = m_canvas->get_mode<CanvasModePen>();
|
||||
const auto plan = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = canvas_tool_mode(m_canvas->m_current_mode),
|
||||
.visibility_mode = canvas_cursor_visibility_mode(m_cursor_visibility),
|
||||
.has_current_brush = m_canvas->m_current_brush != nullptr,
|
||||
.brush_tip_size = m_canvas->m_current_brush ? m_canvas->m_current_brush->m_tip_size : 0.0F,
|
||||
.pen_is_drawing = pen_mode && pen_mode->m_drawing,
|
||||
.alt_down = App::I && App::I->keys[(int)kKey::KeyAlt],
|
||||
.pen_is_resizing = pen_mode && pen_mode->m_resizing,
|
||||
.pen_is_picking = pen_mode && pen_mode->m_picking,
|
||||
});
|
||||
if (!plan) {
|
||||
LOG("Canvas cursor visibility planning failed: %s", plan.status().message);
|
||||
App::I->show_cursor();
|
||||
return;
|
||||
}
|
||||
visible ? App::I->show_cursor() : App::I->hide_cursor();
|
||||
|
||||
plan.value().visible ? App::I->show_cursor() : App::I->hide_cursor();
|
||||
}
|
||||
|
||||
void NodeCanvas::on_tick(float dt)
|
||||
|
||||
@@ -1628,6 +1628,42 @@ if(TARGET pano_cli)
|
||||
LABELS "app;ui;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_small_brush_smoke
|
||||
COMMAND pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_small_brush_smoke PROPERTIES
|
||||
LABELS "app;ui;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"draw\".*\"visibility\":\"small-brush\".*\"visible\":true.*\"paintMode\":true.*\"usesBrushSize\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_not_painting_hidden_smoke
|
||||
COMMAND pano_cli plan-canvas-cursor --mode erase --visibility not-painting --drawing)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_not_painting_hidden_smoke PROPERTIES
|
||||
LABELS "app;ui;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"erase\".*\"visibility\":\"not-painting\".*\"drawing\":true.*\"visible\":false.*\"usesPenState\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_alt_forces_visible_smoke
|
||||
COMMAND pano_cli plan-canvas-cursor --mode draw --visibility never --alt)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_alt_forces_visible_smoke PROPERTIES
|
||||
LABELS "app;ui;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"visibility\":\"never\".*\"visible\":true.*\"forcedVisibleByModifierOrTool\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_non_paint_smoke
|
||||
COMMAND pano_cli plan-canvas-cursor --mode camera --visibility never)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_non_paint_smoke PROPERTIES
|
||||
LABELS "app;ui;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-cursor\".*\"mode\":\"camera\".*\"visible\":true.*\"paintMode\":false")
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_rejects_missing_brush
|
||||
COMMAND pano_cli plan-canvas-cursor --visibility small-brush --no-brush)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_rejects_missing_brush PROPERTIES
|
||||
LABELS "app;ui;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_canvas_cursor_rejects_bad_size
|
||||
COMMAND pano_cli plan-canvas-cursor --visibility small-brush --bad-size)
|
||||
set_tests_properties(pano_cli_plan_canvas_cursor_rejects_bad_size 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "app_core/canvas_tool_ui.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
@@ -128,6 +129,96 @@ void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness)
|
||||
PP_EXPECT(harness, !bucket.mask_line_active);
|
||||
}
|
||||
|
||||
void cursor_visibility_projects_canvas_state(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto non_paint = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::camera,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
|
||||
});
|
||||
PP_EXPECT(harness, non_paint);
|
||||
if (non_paint) {
|
||||
PP_EXPECT(harness, non_paint.value().visible);
|
||||
PP_EXPECT(harness, !non_paint.value().paint_mode);
|
||||
}
|
||||
|
||||
const auto small = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::draw,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
|
||||
.has_current_brush = true,
|
||||
.brush_tip_size = 9.5F,
|
||||
});
|
||||
PP_EXPECT(harness, small);
|
||||
if (small) {
|
||||
PP_EXPECT(harness, small.value().visible);
|
||||
PP_EXPECT(harness, small.value().paint_mode);
|
||||
PP_EXPECT(harness, small.value().uses_brush_size);
|
||||
}
|
||||
|
||||
const auto large = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::erase,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
|
||||
.has_current_brush = true,
|
||||
.brush_tip_size = 10.0F,
|
||||
});
|
||||
PP_EXPECT(harness, large);
|
||||
if (large) {
|
||||
PP_EXPECT(harness, !large.value().visible);
|
||||
PP_EXPECT(harness, large.value().uses_brush_size);
|
||||
}
|
||||
|
||||
const auto painting = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::draw,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
|
||||
.pen_is_drawing = true,
|
||||
});
|
||||
PP_EXPECT(harness, painting);
|
||||
if (painting) {
|
||||
PP_EXPECT(harness, !painting.value().visible);
|
||||
PP_EXPECT(harness, painting.value().uses_pen_state);
|
||||
}
|
||||
}
|
||||
|
||||
void cursor_visibility_forces_visible_for_modifier_or_tool(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto alt = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::draw,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
|
||||
.alt_down = true,
|
||||
});
|
||||
PP_EXPECT(harness, alt);
|
||||
if (alt) {
|
||||
PP_EXPECT(harness, alt.value().visible);
|
||||
PP_EXPECT(harness, alt.value().forced_visible_by_modifier_or_tool);
|
||||
}
|
||||
|
||||
const auto picking = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::erase,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
|
||||
.pen_is_drawing = true,
|
||||
.pen_is_picking = true,
|
||||
});
|
||||
PP_EXPECT(harness, picking);
|
||||
if (picking) {
|
||||
PP_EXPECT(harness, picking.value().visible);
|
||||
PP_EXPECT(harness, picking.value().forced_visible_by_modifier_or_tool);
|
||||
}
|
||||
}
|
||||
|
||||
void cursor_visibility_rejects_invalid_brush_state(pp::tests::Harness& harness)
|
||||
{
|
||||
PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::draw,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
|
||||
.has_current_brush = false,
|
||||
}));
|
||||
PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = pp::app::CanvasToolMode::draw,
|
||||
.visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
|
||||
.has_current_brush = true,
|
||||
.brush_tip_size = std::nanf(""),
|
||||
}));
|
||||
}
|
||||
|
||||
void executor_dispatches_tool_actions(pp::tests::Harness& harness)
|
||||
{
|
||||
FakeCanvasToolServices services;
|
||||
@@ -205,6 +296,9 @@ 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("cursor visibility projects canvas state", cursor_visibility_projects_canvas_state);
|
||||
harness.run("cursor visibility forces visible for modifier or tool", cursor_visibility_forces_visible_for_modifier_or_tool);
|
||||
harness.run("cursor visibility rejects invalid brush state", cursor_visibility_rejects_invalid_brush_state);
|
||||
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);
|
||||
|
||||
@@ -515,6 +515,18 @@ struct PlanCanvasToolStateArgs {
|
||||
bool touch_lock = false;
|
||||
};
|
||||
|
||||
struct PlanCanvasCursorArgs {
|
||||
std::string mode = "draw";
|
||||
std::string visibility = "small-brush";
|
||||
bool has_brush = true;
|
||||
float brush_size = 9.5F;
|
||||
bool drawing = false;
|
||||
bool alt = false;
|
||||
bool resizing = false;
|
||||
bool picking = false;
|
||||
bool bad_size = false;
|
||||
};
|
||||
|
||||
struct PlanQuickOperationArgs {
|
||||
std::string kind = "brush";
|
||||
int current_index = 0;
|
||||
@@ -1456,6 +1468,22 @@ const char* canvas_tool_transform_action_name(pp::app::CanvasToolTransformAction
|
||||
return "none";
|
||||
}
|
||||
|
||||
const char* canvas_cursor_visibility_mode_name(pp::app::CanvasCursorVisibilityMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case pp::app::CanvasCursorVisibilityMode::never:
|
||||
return "never";
|
||||
case pp::app::CanvasCursorVisibilityMode::small_brush:
|
||||
return "small-brush";
|
||||
case pp::app::CanvasCursorVisibilityMode::not_painting:
|
||||
return "not-painting";
|
||||
case pp::app::CanvasCursorVisibilityMode::always:
|
||||
return "always";
|
||||
}
|
||||
|
||||
return "never";
|
||||
}
|
||||
|
||||
const char* canvas_hotkey_event_name(pp::app::CanvasHotkeyEvent event) noexcept
|
||||
{
|
||||
switch (event) {
|
||||
@@ -2003,6 +2031,7 @@ void print_help()
|
||||
<< " plan-canvas-hotkey --event key-down|key-up|touch-tap --key e|z|s|tab|alt|android-back|bracket-left|bracket-right [--ctrl] [--shift] [--mouse-focus] [--undo-count N] [--redo-count N] [--touch-fingers N]\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-canvas-cursor [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--visibility never|small-brush|not-painting|always] [--brush-size N] [--no-brush] [--drawing] [--alt] [--resizing] [--picking] [--bad-size]\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-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\n"
|
||||
@@ -6436,6 +6465,30 @@ pp::foundation::Result<pp::app::CanvasToolMode> parse_canvas_tool_mode(std::stri
|
||||
pp::foundation::Status::invalid_argument("unknown canvas tool mode"));
|
||||
}
|
||||
|
||||
pp::foundation::Result<pp::app::CanvasCursorVisibilityMode> parse_canvas_cursor_visibility_mode(
|
||||
std::string_view mode)
|
||||
{
|
||||
if (mode == "never") {
|
||||
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
|
||||
pp::app::CanvasCursorVisibilityMode::never);
|
||||
}
|
||||
if (mode == "small-brush") {
|
||||
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
|
||||
pp::app::CanvasCursorVisibilityMode::small_brush);
|
||||
}
|
||||
if (mode == "not-painting") {
|
||||
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
|
||||
pp::app::CanvasCursorVisibilityMode::not_painting);
|
||||
}
|
||||
if (mode == "always") {
|
||||
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::success(
|
||||
pp::app::CanvasCursorVisibilityMode::always);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<pp::app::CanvasCursorVisibilityMode>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown canvas cursor visibility mode"));
|
||||
}
|
||||
|
||||
int plan_canvas_tool(int argc, char** argv)
|
||||
{
|
||||
PlanCanvasToolArgs args;
|
||||
@@ -6693,6 +6746,105 @@ int plan_canvas_tool_state(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_canvas_cursor_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
PlanCanvasCursorArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--mode" || key == "--visibility") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
if (key == "--mode") {
|
||||
args.mode = argv[++i];
|
||||
} else {
|
||||
args.visibility = argv[++i];
|
||||
}
|
||||
} else if (key == "--brush-size") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
const auto value = parse_float_arg(argv[++i]);
|
||||
if (!value) {
|
||||
return value.status();
|
||||
}
|
||||
args.brush_size = value.value();
|
||||
} else if (key == "--no-brush") {
|
||||
args.has_brush = false;
|
||||
} else if (key == "--drawing") {
|
||||
args.drawing = true;
|
||||
} else if (key == "--alt") {
|
||||
args.alt = true;
|
||||
} else if (key == "--resizing") {
|
||||
args.resizing = true;
|
||||
} else if (key == "--picking") {
|
||||
args.picking = true;
|
||||
} else if (key == "--bad-size") {
|
||||
args.bad_size = true;
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
int plan_canvas_cursor(int argc, char** argv)
|
||||
{
|
||||
PlanCanvasCursorArgs args;
|
||||
const auto status = parse_plan_canvas_cursor_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("plan-canvas-cursor", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto mode = parse_canvas_tool_mode(args.mode);
|
||||
if (!mode) {
|
||||
print_error("plan-canvas-cursor", mode.status().message);
|
||||
return 2;
|
||||
}
|
||||
const auto visibility = parse_canvas_cursor_visibility_mode(args.visibility);
|
||||
if (!visibility) {
|
||||
print_error("plan-canvas-cursor", visibility.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto plan = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
|
||||
.mode = mode.value(),
|
||||
.visibility_mode = visibility.value(),
|
||||
.has_current_brush = args.has_brush,
|
||||
.brush_tip_size = args.bad_size ? std::nanf("") : args.brush_size,
|
||||
.pen_is_drawing = args.drawing,
|
||||
.alt_down = args.alt,
|
||||
.pen_is_resizing = args.resizing,
|
||||
.pen_is_picking = args.picking,
|
||||
});
|
||||
if (!plan) {
|
||||
print_error("plan-canvas-cursor", plan.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto& value = plan.value();
|
||||
std::cout << "{\"ok\":true,\"command\":\"plan-canvas-cursor\""
|
||||
<< ",\"state\":{\"mode\":\"" << canvas_tool_mode_name(mode.value())
|
||||
<< "\",\"visibility\":\"" << canvas_cursor_visibility_mode_name(visibility.value())
|
||||
<< "\",\"hasBrush\":" << json_bool(args.has_brush)
|
||||
<< ",\"brushSize\":" << args.brush_size
|
||||
<< ",\"drawing\":" << json_bool(args.drawing)
|
||||
<< ",\"alt\":" << json_bool(args.alt)
|
||||
<< ",\"resizing\":" << json_bool(args.resizing)
|
||||
<< ",\"picking\":" << json_bool(args.picking)
|
||||
<< "},\"plan\":{\"visible\":" << json_bool(value.visible)
|
||||
<< ",\"paintMode\":" << json_bool(value.paint_mode)
|
||||
<< ",\"usesBrushSize\":" << json_bool(value.uses_brush_size)
|
||||
<< ",\"usesPenState\":" << json_bool(value.uses_pen_state)
|
||||
<< ",\"forcedVisibleByModifierOrTool\":" << json_bool(value.forced_visible_by_modifier_or_tool)
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_grid_operation_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
@@ -9753,6 +9905,10 @@ int main(int argc, char** argv)
|
||||
return plan_canvas_tool_state(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-canvas-cursor") {
|
||||
return plan_canvas_cursor(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-grid-operation") {
|
||||
return plan_grid_operation(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user