Route canvas view execution through app core

This commit is contained in:
2026-06-05 05:47:42 +02:00
parent 9373e07d3e
commit e42afcc83f
15 changed files with 456 additions and 33 deletions

View File

@@ -62,6 +62,8 @@ set(PP_LEGACY_APP_SOURCES
src/legacy_app_shell_services.h src/legacy_app_shell_services.h
src/legacy_canvas_tool_services.cpp src/legacy_canvas_tool_services.cpp
src/legacy_canvas_tool_services.h 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.cpp
src/legacy_document_canvas_services.h src/legacy_document_canvas_services.h
src/legacy_document_layer_services.cpp src/legacy_document_layer_services.cpp

View File

@@ -226,9 +226,14 @@ Known local toolchain state:
`pp_app_core` contracts while legacy dialogs, pickers, cloud/share/export, `pp_app_core` contracts while legacy dialogs, pickers, cloud/share/export,
Tools, About, history, canvas-clear, settings, and platform SonarPen startup Tools, About, history, canvas-clear, settings, and platform SonarPen startup
execution remain tracked by `DEBT-0029`, `DEBT-0030`, `DEBT-0031`, execution remain tracked by `DEBT-0029`, `DEBT-0030`, `DEBT-0031`,
`DEBT-0033`, `DEBT-0034`, and `DEBT-0035`. `NodeCanvas::reset_camera()` `DEBT-0033`, `DEBT-0034`, and `DEBT-0035`. `src/legacy_canvas_view_services.*`
now consumes the tested `pp_app_core` reset-camera state exposed through is the shared bridge for reset-camera, viewport-density, and cursor-mode
`pano_cli plan-canvas-camera-reset` before retained canvas camera mutation. 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 - `src/legacy_app_preference_services.*` is the current app-shell bridge for
options-menu preference execution. It keeps UI scale, viewport scale, RTL, options-menu preference execution. It keeps UI scale, viewport scale, RTL,
VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks on VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks on

View File

@@ -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 small-brush, not-painting, modifier, and malformed-brush states for
automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor
execution remain open under DEBT-0027. execution remain open under DEBT-0027.
- 2026-06-05: DEBT-0033 was narrowed. Canvas reset-camera defaults now go - 2026-06-05: DEBT-0033 was narrowed again. Canvas reset-camera defaults,
through tested `pp_app_core`, live `NodeCanvas::reset_camera()` consumes the viewport density, and cursor mode now go through tested `pp_app_core` plans
planner before retained canvas mutation, and `pano_cli plan-canvas-camera-reset` and shared `src/legacy_canvas_view_services.*` execution. Live Tools
exposes the exact identity rotation, zero position/pan, and 85-degree field reset-camera, document open/new-document reset, cloud download reset, and
of view for automation. Legacy Tools/document/cloud callers still reach the options viewport/cursor callbacks consume that bridge, while
legacy `NodeCanvas` adapter until canvas view services exist. `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, - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
thumbnail, and object-draw history paths now query saved blend state through thumbnail, and object-draw history paths now query saved blend state through
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect 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-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-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-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-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-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 | | 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-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-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-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-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-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 | | 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 |

View File

@@ -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 legacy UI/panel/canvas/platform adapters continue execution. The live animation
panel route now also checks animation panel visibility and applies animation panel route now also checks animation panel visibility and applies animation
panel layout state instead of using the grid panel by mistake. panel layout state instead of using the grid panel by mistake.
`pano_cli plan-canvas-camera-reset` exposes the shared app-core reset-camera `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, and
state used by live `NodeCanvas::reset_camera()` before retained legacy canvas `pano_cli plan-canvas-view-cursor-mode` expose shared app-core canvas-view
camera mutation, keeping document-open/session/cloud/tools reset defaults under state used by live reset-camera, viewport-density, and cursor-mode paths.
the same tested policy. 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 The live SonarPen menu action now asks the active `PlatformServices` instance
for availability and startup, removing the local iOS branch from the Tools menu 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 and shared Tools executor while preserving the retained iOS bridge in the

View File

@@ -1,9 +1,19 @@
#pragma once #pragma once
#include "foundation/result.h"
#include <array> #include <array>
#include <cmath>
namespace pp::app { namespace pp::app {
enum class CanvasViewCursorMode {
never = 0,
small_brush = 1,
not_painting = 2,
always = 3,
};
struct CanvasCameraState { struct CanvasCameraState {
std::array<float, 16> rotation {}; std::array<float, 16> rotation {};
std::array<float, 3> position {}; std::array<float, 3> position {};
@@ -11,6 +21,24 @@ struct CanvasCameraState {
std::array<float, 2> pan {}; std::array<float, 2> 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 [[nodiscard]] constexpr CanvasCameraState plan_canvas_camera_reset() noexcept
{ {
CanvasCameraState state; CanvasCameraState state;
@@ -26,4 +54,62 @@ struct CanvasCameraState {
return state; return state;
} }
[[nodiscard]] inline pp::foundation::Result<CanvasViewDensityPlan> plan_canvas_view_density(float density)
{
if (!std::isfinite(density) || density <= 0.0F) {
return pp::foundation::Result<CanvasViewDensityPlan>::failure(
pp::foundation::Status::invalid_argument("canvas view density must be finite and positive"));
}
return pp::foundation::Result<CanvasViewDensityPlan>::success(CanvasViewDensityPlan {
.density = density,
.recreates_buffers = true,
});
}
[[nodiscard]] inline pp::foundation::Result<CanvasViewCursorModePlan> plan_canvas_view_cursor_mode(int mode)
{
if (mode < static_cast<int>(CanvasViewCursorMode::never)
|| mode > static_cast<int>(CanvasViewCursorMode::always)) {
return pp::foundation::Result<CanvasViewCursorModePlan>::failure(
pp::foundation::Status::out_of_range("canvas cursor mode is out of range"));
}
return pp::foundation::Result<CanvasViewCursorModePlan>::success(CanvasViewCursorModePlan {
.mode = static_cast<CanvasViewCursorMode>(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 } // namespace pp::app

View File

@@ -3,7 +3,7 @@
#include "legacy_app_preference_services.h" #include "legacy_app_preference_services.h"
#include "app.h" #include "app.h"
#include "node_canvas.h" #include "legacy_canvas_view_services.h"
#include "serializer.h" #include "serializer.h"
#include "settings.h" #include "settings.h"
@@ -24,12 +24,9 @@ public:
void apply_viewport_scale(const pp::app::ScaleApplicationPlan& plan) override void apply_viewport_scale(const pp::app::ScaleApplicationPlan& plan) override
{ {
if (!app_.canvas) const auto status = execute_legacy_canvas_view_density(app_, plan.scale);
return; if (!status.ok())
LOG("Viewport scale preference failed: %s", status.message);
app_.canvas->set_density(plan.scale);
Settings::set("vp-scale", Serializer::Float(plan.scale));
Settings::save();
} }
void apply_interface_direction(const pp::app::InterfaceDirectionPlan& plan) override 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 void apply_canvas_cursor_mode(const pp::app::StoredIntegerPreferencePlan& plan) override
{ {
if (!app_.canvas) const auto status = execute_legacy_canvas_cursor_mode(app_, plan.value);
return; if (!status.ok())
LOG("Canvas cursor mode preference failed: %s", status.message);
app_.canvas->set_cursor_visibility(static_cast<NodeCanvas::kCursorVisibility>(plan.value));
Settings::set("show-cursor", Serializer::Integer(plan.value));
Settings::save();
} }
private: private:

View File

@@ -4,6 +4,7 @@
#include "app.h" #include "app.h"
#include "app_core/document_import.h" #include "app_core/document_import.h"
#include "legacy_canvas_view_services.h"
#include "legacy_document_canvas_services.h" #include "legacy_document_canvas_services.h"
#include "legacy_history_services.h" #include "legacy_history_services.h"
@@ -304,8 +305,9 @@ public:
void reset_camera() override void reset_camera() override
{ {
if (app_.canvas) const auto status = execute_legacy_canvas_camera_reset(app_);
app_.canvas->reset_camera(); if (!status.ok())
LOG("Canvas camera reset failed: %s", status.message);
} }
void show_shortcuts_dialog() override void show_shortcuts_dialog() override

View File

@@ -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<int>(plan.mode);
app_.canvas->set_cursor_visibility(static_cast<NodeCanvas::kCursorVisibility>(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

View File

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

View File

@@ -4,6 +4,7 @@
#include "app.h" #include "app.h"
#include "canvas.h" #include "canvas.h"
#include "legacy_canvas_view_services.h"
#include "node_dialog_cloud.h" #include "node_dialog_cloud.h"
#include "node_progress_bar.h" #include "node_progress_bar.h"
#include "util.h" #include "util.h"
@@ -122,7 +123,9 @@ public:
m->m_message->set_text(progress); 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->layers->clear();
app->canvas->m_canvas->project_open_thread(request.selected_path); app->canvas->m_canvas->project_open_thread(request.selected_path);

View File

@@ -4,6 +4,7 @@
#include "app.h" #include "app.h"
#include "legacy_brush_package_import_services.h" #include "legacy_brush_package_import_services.h"
#include "legacy_canvas_view_services.h"
#include "legacy_history_services.h" #include "legacy_history_services.h"
#include "log.h" #include "log.h"
#include "node_panel_brush.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_name = route.name;
app.doc_dir = route.directory; app.doc_dir = route.directory;
app.doc_path = route.path; 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.layers->clear();
app.canvas->m_canvas->project_open(route.path, [&app](bool success) { app.canvas->m_canvas->project_open(route.path, [&app](bool success) {
if (success) if (success)

View File

@@ -3,6 +3,7 @@
#include "legacy_document_session_services.h" #include "legacy_document_session_services.h"
#include "app.h" #include "app.h"
#include "legacy_canvas_view_services.h"
#include "legacy_history_services.h" #include "legacy_history_services.h"
#include "node_dialog_open.h" #include "node_dialog_open.h"
@@ -24,7 +25,9 @@ void create_legacy_new_document(
app.layers->clear(); app.layers->clear();
app.canvas->m_canvas->m_layers.clear(); app.canvas->m_canvas->m_layers.clear();
app.canvas->m_canvas->resize(plan.resolution, plan.resolution); 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(); pp::panopainter::clear_legacy_history();
app.layers->add_layer("Default", false, true); app.layers->add_layer("Default", false, true);

View File

@@ -1650,6 +1650,30 @@ if(TARGET pano_cli)
LABELS "app;ui;integration;desktop-fast;fuzz" LABELS "app;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) 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 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) 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 set_tests_properties(pano_cli_plan_canvas_cursor_small_brush_smoke PROPERTIES

View File

@@ -1,10 +1,46 @@
#include "app_core/canvas_view.h" #include "app_core/canvas_view.h"
#include "test_harness.h" #include "test_harness.h"
#include <cmath>
#include <cstddef> #include <cstddef>
#include <string>
namespace { 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) void camera_reset_projects_legacy_defaults(pp::tests::Harness& harness)
{ {
const auto state = pp::app::plan_canvas_camera_reset(); 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); 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 } // namespace
int main() int main()
{ {
pp::tests::Harness harness; pp::tests::Harness harness;
harness.run("camera reset projects legacy defaults", camera_reset_projects_legacy_defaults); 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(); return harness.finish();
} }

View File

@@ -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 --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-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-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-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-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"
@@ -6770,6 +6772,78 @@ int plan_canvas_camera_reset(int argc, char** argv)
return 0; 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<int>(plan.value().mode)
<< "}\n";
return 0;
}
pp::foundation::Status parse_plan_canvas_cursor_args( pp::foundation::Status parse_plan_canvas_cursor_args(
int argc, int argc,
char** argv, char** argv,
@@ -9933,6 +10007,14 @@ int main(int argc, char** argv)
return plan_canvas_camera_reset(argc, 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") { if (command == "plan-canvas-cursor") {
return plan_canvas_cursor(argc, argv); return plan_canvas_cursor(argc, argv);
} }