diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index e81c4e2..be749f1 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -62,6 +62,8 @@ set(PP_LEGACY_APP_SOURCES src/legacy_app_shell_services.h src/legacy_canvas_tool_services.cpp src/legacy_canvas_tool_services.h + src/legacy_canvas_view_services.cpp + src/legacy_canvas_view_services.h src/legacy_document_canvas_services.cpp src/legacy_document_canvas_services.h src/legacy_document_layer_services.cpp diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 272a498..991e5fa 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -226,9 +226,14 @@ Known local toolchain state: `pp_app_core` contracts while legacy dialogs, pickers, cloud/share/export, Tools, About, history, canvas-clear, settings, and platform SonarPen startup execution remain tracked by `DEBT-0029`, `DEBT-0030`, `DEBT-0031`, - `DEBT-0033`, `DEBT-0034`, and `DEBT-0035`. `NodeCanvas::reset_camera()` - now consumes the tested `pp_app_core` reset-camera state exposed through - `pano_cli plan-canvas-camera-reset` before retained canvas camera mutation. + `DEBT-0033`, `DEBT-0034`, and `DEBT-0035`. `src/legacy_canvas_view_services.*` + is the shared bridge for reset-camera, viewport-density, and cursor-mode + execution; live Tools reset-camera, document open/new-document reset, cloud + download reset, and options viewport/cursor callbacks consume the tested + `pp_app_core` canvas-view plans exposed through + `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, + and `pano_cli plan-canvas-view-cursor-mode` before retained canvas mutation + and settings writes. - `src/legacy_app_preference_services.*` is the current app-shell bridge for options-menu preference execution. It keeps UI scale, viewport scale, RTL, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks on diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index cf4e11b..0f70235 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -75,12 +75,16 @@ agent or engineer to remove them without reconstructing context from chat. 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-05: DEBT-0033 was narrowed. Canvas reset-camera defaults now go - through tested `pp_app_core`, live `NodeCanvas::reset_camera()` consumes the - planner before retained canvas mutation, and `pano_cli plan-canvas-camera-reset` - exposes the exact identity rotation, zero position/pan, and 85-degree field - of view for automation. Legacy Tools/document/cloud callers still reach the - legacy `NodeCanvas` adapter until canvas view services exist. +- 2026-06-05: DEBT-0033 was narrowed again. Canvas reset-camera defaults, + viewport density, and cursor mode now go through tested `pp_app_core` plans + and shared `src/legacy_canvas_view_services.*` execution. Live Tools + reset-camera, document open/new-document reset, cloud download reset, and + options viewport/cursor callbacks consume that bridge, while + `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, and + `pano_cli plan-canvas-view-cursor-mode` expose the paths for automation. + This also narrows DEBT-0045 for viewport-density and cursor-mode preference + execution, though preference persistence remains retained in the legacy + canvas-view bridge. - 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 @@ -127,7 +131,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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 | | DEBT-0031 | Open | Modernization | Top-level File menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_file`, `pano_cli plan-file-menu`, and the `FileMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still invokes legacy dialogs, platform pickers, cloud code, share code, and canvas import/export paths directly | Preserve File menu behavior while app workflows move toward app/document/platform command services | `pp_app_core_file_menu_tests`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-file-menu --command import`; `pano_cli plan-file-menu --command cloud-upload`; `ctest --preset desktop-fast --build-config Debug` | File menu routing, picker dispatch, save/share/cloud/resize/export execution, and image/project import execution are owned by injected app/document/platform services with `App::init_menu_file` acting only as a UI adapter and no legacy File menu adapter | | DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary; Layer menu clear reuses the `DocumentCanvasClearServices` executor; and Layer menu rename/clear/merge now share `src/legacy_document_layer_services.*`, but the bridge still calls the legacy rename dialog path, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1 --animation-duration 3`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer rename, merge-down execution, animation gating, and selected-layer state are owned by injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu adapter | -| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-canvas-camera-reset`, and the `ToolsMenuServices` boundary, direct command execution is centralized in `src/legacy_app_shell_services.*`, SonarPen availability/startup now routes through `PlatformServices`, and `NodeCanvas::reset_camera()` consumes tested app-core reset defaults before mutating legacy canvas camera state, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, open legacy shortcuts UI, and rely on the legacy platform adapter for the retained iOS SonarPen bridge | Preserve current Tools menu and reset-camera behavior while UI shell actions move toward app/UI/platform/canvas services | `pp_app_core_tools_menu_tests`; `pp_app_core_canvas_view_tests`; `pp_platform_api_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `pano_cli plan-canvas-camera-reset`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform/canvas services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter | +| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, `pano_cli plan-canvas-view-cursor-mode`, and the `ToolsMenuServices` boundary, direct command execution is centralized in `src/legacy_app_shell_services.*`, SonarPen availability/startup now routes through `PlatformServices`, and reset-camera, viewport-density, and cursor-mode execution now share `src/legacy_canvas_view_services.*`, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, open legacy shortcuts UI, mutate retained `Canvas` camera/density/cursor state, write retained `Settings`, and rely on the legacy platform adapter for the retained iOS SonarPen bridge | Preserve current Tools menu and canvas-view behavior while UI shell actions move toward app/UI/platform/canvas services | `pp_app_core_tools_menu_tests`; `pp_app_core_canvas_view_tests`; `pp_platform_api_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `pano_cli plan-canvas-camera-reset`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, viewport density, cursor mode, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform/canvas services with `App::init_menu_tools` and options callbacks acting only as UI adapters and no legacy Tools/canvas-view adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | | DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::query_opengl_capability_detection`, `detect_opengl_feature_state`, and `render_device_features` as the backend conversion point; that feature snapshot now includes float32-linear filtering, so canvas stroke texture format selection, renderer diagnostics, grid lightmap render planning, and grid bake target selection no longer read `ShaderManager::ext_*` flags directly. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. The retained `copy_framebuffer_to_texture_2d` utility bridge now routes 2D framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch, retained `RTT::create`/`RTT::destroy` render-target texture parameter setup, optional depth renderbuffer allocation, framebuffer allocation/attachment/status checks, binding restore, and resource deletion now route through tested `pp_renderer_gl` dispatch, retained RTT clear, masked clear with color-write-mask restore, texture bind/unbind, and RGBA8 dirty-region texture writes now route through tested `pp_renderer_gl` dispatch, retained Canvas, NodeCanvas, and NodeStrokePreview texture-unit switches now route through tested active-texture dispatch, retained Canvas, NodeCanvas, NodeStrokePreview, and desktop HMD viewport/scissor/capability execution now route through tested `pp_renderer_gl` dispatch adapters, retained NodeCanvas, CanvasMode, and NodePanelGrid capability-state snapshots now route through tested `pp_renderer_gl` query dispatch, CanvasLayer cube/equirect generation plus frame clears now route blend state, active texture units, viewport execution, color clears, and cube-face framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch adapters, `NodePanelGrid` live heightmap draw and bake setup now route depth/blend state, depth clears, color-write-mask toggles, active texture selection, bake viewport execution, sun-overlay viewport query, and desktop texture-resize readback through tested `pp_renderer_gl` dispatch adapters, retained CanvasMode overlay/mask/transform paths now route active texture, depth/blend state, transform/cut viewport execution, paint-mode blend/depth state snapshots, and canvas-tip pick framebuffer readback through tested `pp_renderer_gl` dispatch adapters, retained simple UI draw paths now share `legacy_ui_gl_dispatch` for blend-state execution, fallback 2D texture unbinds, `NodeViewport` viewport query/restore, color-buffer clears, and clear-color restore, retained `NodeCanvas` plus `NodeStrokePreview` draw-state paths now route viewport query, clear-color query, color-buffer clear, and clear-color restore through tested `pp_renderer_gl` dispatch helpers, and retained `Canvas` plus `CanvasLayer` stroke/object/thumbnail/frame-clear draw-state paths now route saved viewport or clear-color query and restore through the same tested helpers, but actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, brush-preview compositing, and the retained `ShaderManager::ext_*` compatibility fields still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior | @@ -139,7 +143,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy overwrite prompts, calls legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters | | DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | -| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`, but the bridge still calls legacy `App::set_ui_scale`, `App::set_ui_rtl`, `NodeCanvas::set_density`, `NodeCanvas::set_cursor_visibility`, `App::rec_start`, `App::rec_stop`, and `Settings::save` directly; its VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | +| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and `Settings::save` directly; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | | DEBT-0046 | Open | Modernization | Startup preference/runtime execution now consumes pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `AppStartupServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, and message-box license warning execution directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, and startup UI/runtime side effects are owned by injected app/preferences/storage/recording/UI services with `App::init` acting only as orchestration | | DEBT-0047 | Open | Modernization | PPBR brush package export request validation and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, and the macOS data-directory override now routes through `PlatformServices`, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, owns desktop worker-thread dispatch, dialog destruction, mobile/Web completion, and success-message behavior directly | Preserve current PPBR export behavior while brush assets, PPBR serialization, picker completion, and UI lifetime move toward asset/storage/UI/platform services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_export_tests`; `pp_platform_api_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --author Artist --dest-path D:/Paint/BrushPreviews --export-data --header-image`; `pano_cli plan-brush-package-export`; `pano_cli plan-brush-package-export --path clouds`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --dest-path D:/Paint/BrushPreviews --no-export-data`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | PPBR metadata collection, header-image ownership, serialization, picker-selected path execution, desktop threading, dialog lifetime, and success UI are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter | | DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6d389c0..84f6ed4 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -646,10 +646,13 @@ dispatch through `ToolsMenuServices` in the shared app-shell bridge before the legacy UI/panel/canvas/platform adapters continue execution. The live animation panel route now also checks animation panel visibility and applies animation panel layout state instead of using the grid panel by mistake. -`pano_cli plan-canvas-camera-reset` exposes the shared app-core reset-camera -state used by live `NodeCanvas::reset_camera()` before retained legacy canvas -camera mutation, keeping document-open/session/cloud/tools reset defaults under -the same tested policy. +`pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, and +`pano_cli plan-canvas-view-cursor-mode` expose shared app-core canvas-view +state used by live reset-camera, viewport-density, and cursor-mode paths. +Tools reset-camera, document open/new-document reset, cloud download reset, and +options viewport/cursor callbacks now dispatch through +`src/legacy_canvas_view_services.*` before retained legacy canvas mutation and +settings writes. The live SonarPen menu action now asks the active `PlatformServices` instance for availability and startup, removing the local iOS branch from the Tools menu and shared Tools executor while preserving the retained iOS bridge in the diff --git a/src/app_core/canvas_view.h b/src/app_core/canvas_view.h index 47bbbcc..36fae86 100644 --- a/src/app_core/canvas_view.h +++ b/src/app_core/canvas_view.h @@ -1,9 +1,19 @@ #pragma once +#include "foundation/result.h" + #include +#include namespace pp::app { +enum class CanvasViewCursorMode { + never = 0, + small_brush = 1, + not_painting = 2, + always = 3, +}; + struct CanvasCameraState { std::array rotation {}; std::array position {}; @@ -11,6 +21,24 @@ struct CanvasCameraState { std::array pan {}; }; +struct CanvasViewDensityPlan { + float density = 1.0F; + bool recreates_buffers = true; +}; + +struct CanvasViewCursorModePlan { + CanvasViewCursorMode mode = CanvasViewCursorMode::never; +}; + +class CanvasViewServices { +public: + virtual ~CanvasViewServices() = default; + + virtual void reset_camera(const CanvasCameraState& state) = 0; + virtual void set_density(const CanvasViewDensityPlan& plan) = 0; + virtual void set_cursor_mode(const CanvasViewCursorModePlan& plan) = 0; +}; + [[nodiscard]] constexpr CanvasCameraState plan_canvas_camera_reset() noexcept { CanvasCameraState state; @@ -26,4 +54,62 @@ struct CanvasCameraState { return state; } +[[nodiscard]] inline pp::foundation::Result plan_canvas_view_density(float density) +{ + if (!std::isfinite(density) || density <= 0.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("canvas view density must be finite and positive")); + } + + return pp::foundation::Result::success(CanvasViewDensityPlan { + .density = density, + .recreates_buffers = true, + }); +} + +[[nodiscard]] inline pp::foundation::Result plan_canvas_view_cursor_mode(int mode) +{ + if (mode < static_cast(CanvasViewCursorMode::never) + || mode > static_cast(CanvasViewCursorMode::always)) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("canvas cursor mode is out of range")); + } + + return pp::foundation::Result::success(CanvasViewCursorModePlan { + .mode = static_cast(mode), + }); +} + +[[nodiscard]] inline pp::foundation::Status execute_canvas_camera_reset(CanvasViewServices& services) +{ + services.reset_camera(plan_canvas_camera_reset()); + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_canvas_view_density( + float density, + CanvasViewServices& services) +{ + const auto plan = plan_canvas_view_density(density); + if (!plan) { + return plan.status(); + } + + services.set_density(plan.value()); + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_canvas_view_cursor_mode( + int mode, + CanvasViewServices& services) +{ + const auto plan = plan_canvas_view_cursor_mode(mode); + if (!plan) { + return plan.status(); + } + + services.set_cursor_mode(plan.value()); + return pp::foundation::Status::success(); +} + } // namespace pp::app diff --git a/src/legacy_app_preference_services.cpp b/src/legacy_app_preference_services.cpp index 2d58972..f23e2c3 100644 --- a/src/legacy_app_preference_services.cpp +++ b/src/legacy_app_preference_services.cpp @@ -3,7 +3,7 @@ #include "legacy_app_preference_services.h" #include "app.h" -#include "node_canvas.h" +#include "legacy_canvas_view_services.h" #include "serializer.h" #include "settings.h" @@ -24,12 +24,9 @@ public: void apply_viewport_scale(const pp::app::ScaleApplicationPlan& plan) override { - if (!app_.canvas) - return; - - app_.canvas->set_density(plan.scale); - Settings::set("vp-scale", Serializer::Float(plan.scale)); - Settings::save(); + const auto status = execute_legacy_canvas_view_density(app_, plan.scale); + if (!status.ok()) + LOG("Viewport scale preference failed: %s", status.message); } void apply_interface_direction(const pp::app::InterfaceDirectionPlan& plan) override @@ -68,12 +65,9 @@ public: void apply_canvas_cursor_mode(const pp::app::StoredIntegerPreferencePlan& plan) override { - if (!app_.canvas) - return; - - app_.canvas->set_cursor_visibility(static_cast(plan.value)); - Settings::set("show-cursor", Serializer::Integer(plan.value)); - Settings::save(); + const auto status = execute_legacy_canvas_cursor_mode(app_, plan.value); + if (!status.ok()) + LOG("Canvas cursor mode preference failed: %s", status.message); } private: diff --git a/src/legacy_app_shell_services.cpp b/src/legacy_app_shell_services.cpp index 287c6a9..ac15d0a 100644 --- a/src/legacy_app_shell_services.cpp +++ b/src/legacy_app_shell_services.cpp @@ -4,6 +4,7 @@ #include "app.h" #include "app_core/document_import.h" +#include "legacy_canvas_view_services.h" #include "legacy_document_canvas_services.h" #include "legacy_history_services.h" @@ -304,8 +305,9 @@ public: void reset_camera() override { - if (app_.canvas) - app_.canvas->reset_camera(); + const auto status = execute_legacy_canvas_camera_reset(app_); + if (!status.ok()) + LOG("Canvas camera reset failed: %s", status.message); } void show_shortcuts_dialog() override diff --git a/src/legacy_canvas_view_services.cpp b/src/legacy_canvas_view_services.cpp new file mode 100644 index 0000000..98ff840 --- /dev/null +++ b/src/legacy_canvas_view_services.cpp @@ -0,0 +1,90 @@ +#include "pch.h" + +#include "legacy_canvas_view_services.h" + +#include "app.h" +#include "node_canvas.h" +#include "serializer.h" +#include "settings.h" + +namespace pp::panopainter { +namespace { + +class LegacyCanvasViewServices final : public pp::app::CanvasViewServices { +public: + explicit LegacyCanvasViewServices(App& app) noexcept + : app_(app) + { + } + + void reset_camera(const pp::app::CanvasCameraState& state) override + { + if (!app_.canvas || !app_.canvas->m_canvas) { + return; + } + + app_.canvas->m_canvas->m_cam_rot = glm::mat4( + state.rotation[0], state.rotation[1], state.rotation[2], state.rotation[3], + state.rotation[4], state.rotation[5], state.rotation[6], state.rotation[7], + state.rotation[8], state.rotation[9], state.rotation[10], state.rotation[11], + state.rotation[12], state.rotation[13], state.rotation[14], state.rotation[15]); + app_.canvas->m_canvas->m_cam_pos = { + state.position[0], + state.position[1], + state.position[2], + }; + app_.canvas->m_canvas->m_cam_fov = state.field_of_view_degrees; + app_.canvas->m_canvas->m_pan = { + state.pan[0], + state.pan[1], + }; + } + + void set_density(const pp::app::CanvasViewDensityPlan& plan) override + { + if (!app_.canvas) { + return; + } + + app_.canvas->set_density(plan.density); + Settings::set("vp-scale", Serializer::Float(plan.density)); + Settings::save(); + } + + void set_cursor_mode(const pp::app::CanvasViewCursorModePlan& plan) override + { + if (!app_.canvas) { + return; + } + + const auto mode = static_cast(plan.mode); + app_.canvas->set_cursor_visibility(static_cast(mode)); + Settings::set("show-cursor", Serializer::Integer(mode)); + Settings::save(); + } + +private: + App& app_; +}; + +} // namespace + +pp::foundation::Status execute_legacy_canvas_camera_reset(App& app) +{ + LegacyCanvasViewServices services(app); + return pp::app::execute_canvas_camera_reset(services); +} + +pp::foundation::Status execute_legacy_canvas_view_density(App& app, float density) +{ + LegacyCanvasViewServices services(app); + return pp::app::execute_canvas_view_density(density, services); +} + +pp::foundation::Status execute_legacy_canvas_cursor_mode(App& app, int mode) +{ + LegacyCanvasViewServices services(app); + return pp::app::execute_canvas_view_cursor_mode(mode, services); +} + +} // namespace pp::panopainter diff --git a/src/legacy_canvas_view_services.h b/src/legacy_canvas_view_services.h new file mode 100644 index 0000000..bd5fc02 --- /dev/null +++ b/src/legacy_canvas_view_services.h @@ -0,0 +1,13 @@ +#pragma once + +#include "app_core/canvas_view.h" + +class App; + +namespace pp::panopainter { + +[[nodiscard]] pp::foundation::Status execute_legacy_canvas_camera_reset(App& app); +[[nodiscard]] pp::foundation::Status execute_legacy_canvas_view_density(App& app, float density); +[[nodiscard]] pp::foundation::Status execute_legacy_canvas_cursor_mode(App& app, int mode); + +} // namespace pp::panopainter diff --git a/src/legacy_cloud_services.cpp b/src/legacy_cloud_services.cpp index 01e1357..98c1b5f 100644 --- a/src/legacy_cloud_services.cpp +++ b/src/legacy_cloud_services.cpp @@ -4,6 +4,7 @@ #include "app.h" #include "canvas.h" +#include "legacy_canvas_view_services.h" #include "node_dialog_cloud.h" #include "node_progress_bar.h" #include "util.h" @@ -122,7 +123,9 @@ public: m->m_message->set_text(progress); }); - app->canvas->reset_camera(); + const auto reset_status = execute_legacy_canvas_camera_reset(*app); + if (!reset_status.ok()) + LOG("Cloud download camera reset failed: %s", reset_status.message); app->layers->clear(); app->canvas->m_canvas->project_open_thread(request.selected_path); diff --git a/src/legacy_document_open_services.cpp b/src/legacy_document_open_services.cpp index bc60b2e..ddbe399 100644 --- a/src/legacy_document_open_services.cpp +++ b/src/legacy_document_open_services.cpp @@ -4,6 +4,7 @@ #include "app.h" #include "legacy_brush_package_import_services.h" +#include "legacy_canvas_view_services.h" #include "legacy_history_services.h" #include "log.h" #include "node_panel_brush.h" @@ -17,7 +18,9 @@ void open_legacy_project(App& app, const pp::app::DocumentOpenRoute& route) app.doc_name = route.name; app.doc_dir = route.directory; app.doc_path = route.path; - app.canvas->reset_camera(); + const auto reset_status = execute_legacy_canvas_camera_reset(app); + if (!reset_status.ok()) + LOG("Project open camera reset failed: %s", reset_status.message); app.layers->clear(); app.canvas->m_canvas->project_open(route.path, [&app](bool success) { if (success) diff --git a/src/legacy_document_session_services.cpp b/src/legacy_document_session_services.cpp index ce08465..16ad27a 100644 --- a/src/legacy_document_session_services.cpp +++ b/src/legacy_document_session_services.cpp @@ -3,6 +3,7 @@ #include "legacy_document_session_services.h" #include "app.h" +#include "legacy_canvas_view_services.h" #include "legacy_history_services.h" #include "node_dialog_open.h" @@ -24,7 +25,9 @@ void create_legacy_new_document( app.layers->clear(); app.canvas->m_canvas->m_layers.clear(); app.canvas->m_canvas->resize(plan.resolution, plan.resolution); - app.canvas->reset_camera(); + const auto reset_status = execute_legacy_canvas_camera_reset(app); + if (!reset_status.ok()) + LOG("New document camera reset failed: %s", reset_status.message); pp::panopainter::clear_legacy_history(); app.layers->add_layer("Default", false, true); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b9d9be8..09ab427 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1650,6 +1650,30 @@ if(TARGET pano_cli) LABELS "app;ui;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_view_density_smoke + COMMAND pano_cli plan-canvas-view-density --density 1.5) + set_tests_properties(pano_cli_plan_canvas_view_density_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-view-density\".*\"density\":1.5.*\"recreatesBuffers\":true") + + add_test(NAME pano_cli_plan_canvas_view_density_rejects_bad_float + COMMAND pano_cli plan-canvas-view-density --bad-float) + set_tests_properties(pano_cli_plan_canvas_view_density_rejects_bad_float PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_canvas_view_cursor_mode_smoke + COMMAND pano_cli plan-canvas-view-cursor-mode --mode 3) + set_tests_properties(pano_cli_plan_canvas_view_cursor_mode_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-view-cursor-mode\".*\"mode\":3") + + add_test(NAME pano_cli_plan_canvas_view_cursor_mode_rejects_invalid + COMMAND pano_cli plan-canvas-view-cursor-mode --mode 4) + set_tests_properties(pano_cli_plan_canvas_view_cursor_mode_rejects_invalid PROPERTIES + 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 diff --git a/tests/app_core/canvas_view_tests.cpp b/tests/app_core/canvas_view_tests.cpp index 52c8935..f074449 100644 --- a/tests/app_core/canvas_view_tests.cpp +++ b/tests/app_core/canvas_view_tests.cpp @@ -1,10 +1,46 @@ #include "app_core/canvas_view.h" #include "test_harness.h" +#include #include +#include namespace { +class FakeCanvasViewServices final : public pp::app::CanvasViewServices { +public: + void reset_camera(const pp::app::CanvasCameraState& state) override + { + camera_state = state; + reset_calls += 1; + call_order += "reset;"; + } + + void set_density(const pp::app::CanvasViewDensityPlan& plan) override + { + density = plan.density; + recreates_buffers = plan.recreates_buffers; + density_calls += 1; + call_order += "density;"; + } + + void set_cursor_mode(const pp::app::CanvasViewCursorModePlan& plan) override + { + cursor_mode = plan.mode; + cursor_calls += 1; + call_order += "cursor;"; + } + + pp::app::CanvasCameraState camera_state; + float density = 0.0F; + bool recreates_buffers = false; + pp::app::CanvasViewCursorMode cursor_mode = pp::app::CanvasViewCursorMode::never; + int reset_calls = 0; + int density_calls = 0; + int cursor_calls = 0; + std::string call_order; +}; + void camera_reset_projects_legacy_defaults(pp::tests::Harness& harness) { const auto state = pp::app::plan_canvas_camera_reset(); @@ -22,11 +58,84 @@ void camera_reset_projects_legacy_defaults(pp::tests::Harness& harness) PP_EXPECT(harness, state.pan[1] == 0.0F); } +void density_rejects_non_positive_or_non_finite_values(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_canvas_view_density(0.0F)); + PP_EXPECT(harness, !pp::app::plan_canvas_view_density(-1.0F)); + PP_EXPECT(harness, !pp::app::plan_canvas_view_density(std::nanf(""))); +} + +void density_projects_buffer_recreation(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_canvas_view_density(1.5F); + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, plan.value().density == 1.5F); + PP_EXPECT(harness, plan.value().recreates_buffers); + } +} + +void cursor_mode_rejects_out_of_range_values(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_canvas_view_cursor_mode(-1)); + PP_EXPECT(harness, !pp::app::plan_canvas_view_cursor_mode(4)); +} + +void cursor_mode_projects_legacy_integer_modes(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_canvas_view_cursor_mode(0).value().mode == pp::app::CanvasViewCursorMode::never); + PP_EXPECT( + harness, + pp::app::plan_canvas_view_cursor_mode(1).value().mode == pp::app::CanvasViewCursorMode::small_brush); + PP_EXPECT( + harness, + pp::app::plan_canvas_view_cursor_mode(2).value().mode == pp::app::CanvasViewCursorMode::not_painting); + PP_EXPECT( + harness, + pp::app::plan_canvas_view_cursor_mode(3).value().mode == pp::app::CanvasViewCursorMode::always); +} + +void executor_dispatches_canvas_view_services(pp::tests::Harness& harness) +{ + FakeCanvasViewServices services; + + PP_EXPECT(harness, pp::app::execute_canvas_camera_reset(services).ok()); + PP_EXPECT(harness, pp::app::execute_canvas_view_density(2.0F, services).ok()); + PP_EXPECT(harness, pp::app::execute_canvas_view_cursor_mode(3, services).ok()); + + PP_EXPECT(harness, services.reset_calls == 1); + PP_EXPECT(harness, services.density_calls == 1); + PP_EXPECT(harness, services.cursor_calls == 1); + PP_EXPECT(harness, services.camera_state.field_of_view_degrees == 85.0F); + PP_EXPECT(harness, services.density == 2.0F); + PP_EXPECT(harness, services.recreates_buffers); + PP_EXPECT(harness, services.cursor_mode == pp::app::CanvasViewCursorMode::always); + PP_EXPECT(harness, services.call_order == "reset;density;cursor;"); +} + +void executor_rejects_invalid_canvas_view_requests(pp::tests::Harness& harness) +{ + FakeCanvasViewServices services; + + PP_EXPECT(harness, !pp::app::execute_canvas_view_density(0.0F, services).ok()); + PP_EXPECT(harness, !pp::app::execute_canvas_view_cursor_mode(99, services).ok()); + PP_EXPECT(harness, services.density_calls == 0); + PP_EXPECT(harness, services.cursor_calls == 0); +} + } // namespace int main() { pp::tests::Harness harness; harness.run("camera reset projects legacy defaults", camera_reset_projects_legacy_defaults); + harness.run("density rejects non-positive or non-finite values", density_rejects_non_positive_or_non_finite_values); + harness.run("density projects buffer recreation", density_projects_buffer_recreation); + harness.run("cursor mode rejects out of range values", cursor_mode_rejects_out_of_range_values); + harness.run("cursor mode projects legacy integer modes", cursor_mode_projects_legacy_integer_modes); + harness.run("executor dispatches canvas view services", executor_dispatches_canvas_view_services); + harness.run("executor rejects invalid canvas view requests", executor_rejects_invalid_canvas_view_requests); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 13a4890..bbbb84c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2033,6 +2033,8 @@ void print_help() << " 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-camera-reset\n" + << " plan-canvas-view-density [--density N] [--bad-float]\n" + << " plan-canvas-view-cursor-mode [--mode N]\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" @@ -6770,6 +6772,78 @@ int plan_canvas_camera_reset(int argc, char** argv) return 0; } +int plan_canvas_view_density(int argc, char** argv) +{ + float density = 1.0F; + bool bad_float = false; + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--density") { + if (i + 1 >= argc) { + print_error("plan-canvas-view-density", "missing value for option"); + return 2; + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + print_error("plan-canvas-view-density", value.status().message); + return 2; + } + density = value.value(); + } else if (key == "--bad-float") { + bad_float = true; + } else { + print_error("plan-canvas-view-density", "unknown option"); + return 2; + } + } + + const auto plan = pp::app::plan_canvas_view_density(bad_float ? std::nanf("") : density); + if (!plan) { + print_error("plan-canvas-view-density", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-canvas-view-density\"" + << ",\"density\":" << plan.value().density + << ",\"recreatesBuffers\":" << json_bool(plan.value().recreates_buffers) + << "}\n"; + return 0; +} + +int plan_canvas_view_cursor_mode(int argc, char** argv) +{ + int mode = 0; + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--mode") { + if (i + 1 >= argc) { + print_error("plan-canvas-view-cursor-mode", "missing value for option"); + return 2; + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + print_error("plan-canvas-view-cursor-mode", value.status().message); + return 2; + } + mode = value.value(); + } else { + print_error("plan-canvas-view-cursor-mode", "unknown option"); + return 2; + } + } + + const auto plan = pp::app::plan_canvas_view_cursor_mode(mode); + if (!plan) { + print_error("plan-canvas-view-cursor-mode", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-canvas-view-cursor-mode\"" + << ",\"mode\":" << static_cast(plan.value().mode) + << "}\n"; + return 0; +} + pp::foundation::Status parse_plan_canvas_cursor_args( int argc, char** argv, @@ -9933,6 +10007,14 @@ int main(int argc, char** argv) return plan_canvas_camera_reset(argc, argv); } + if (command == "plan-canvas-view-density") { + return plan_canvas_view_density(argc, argv); + } + + if (command == "plan-canvas-view-cursor-mode") { + return plan_canvas_view_cursor_mode(argc, argv); + } + if (command == "plan-canvas-cursor") { return plan_canvas_cursor(argc, argv); }