Expose layer history intent in app core

This commit is contained in:
2026-06-12 19:07:19 +02:00
parent 8cd384012f
commit ae24285203
5 changed files with 192 additions and 15 deletions

View File

@@ -18,6 +18,18 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions
- 2026-06-12: DEBT-0050, DEBT-0053, and DEBT-0057 were narrowed. WebGL
exported-image publishing, persistent-storage flushing, prepared-file
handoff, and default canvas resolution now dispatch through injectable
`pp::platform::WebPlatformServices` in `pp_platform_api`; the retained Web
fallback implementation still lives in `src/platform_legacy/legacy_platform_services.cpp`
until a dedicated Web platform shell injects the service directly.
- 2026-06-12: DEBT-0021 was narrowed. Layer rename execution now exposes
app-core history intent explicitly, records undo before applying the new
layer name, and has focused call-order coverage. Layer operation and merge
plans also expose app-core history-intent helpers, while retained
`NodePanelLayer`/`Canvas` execution still owns the concrete `ActionManager`
entries for add/remove/property-change/clear paths.
- 2026-06-12: DEBT-0020 was narrowed. The shared document resize/canvas-clear
live bridge no longer includes `legacy_history_services.h`; resize history
clearing is now an explicit `DocumentResizeServices::clear_history()` app-core
@@ -887,7 +899,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0017 | Open | Modernization | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, working-directory picker/display-path policy, canvas input tip/pressure policy, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, SonarPen availability/startup, and VR mode start/stop now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; Windows render-platform hint and debug-output state token/enable sequencing now delegates to tested `pp_renderer_gl` helpers, leaving Windows with context, callback, console, and Win32 ownership; the retained macOS fallback render-platform hint enable sequence also delegates to the same tested `pp_renderer_gl` helper; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches, including retained iOS canvas tip behavior, retained macOS directory picker/display-path behavior, retained iOS SonarPen bridge, retained non-Windows VR unsupported/no-op behavior, and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell |
| DEBT-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings |
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands; resize history clearing is an explicit app-core execution output implemented directly by the retained `ActionManager`, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and owns the retained `ActionManager` call site | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or retained `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch, layer panel operation planning/execution dispatch, and layer panel selected-control/visibility view projection now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `NodePanelLayer::update_attributes()`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `pano_cli plan-layer-panel-view`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge and panel adapter still mutate legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | Preserve existing UI/canvas behavior while document layer commands, panel projection, and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution and panel state projection are owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch, layer panel operation planning/execution dispatch, layer panel selected-control/visibility view projection, and explicit layer history-intent helpers now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `NodePanelLayer::update_attributes()`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `pano_cli plan-layer-panel-view`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`; rename now records undo before applying the new name through separate app-core service calls, but the shared bridge and panel adapter still mutate legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and retained `ActionManager` undo entries for add/remove/property-change/clear paths | Preserve existing UI/canvas behavior while document layer commands, panel projection, and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution and panel state projection are owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, timeline scrub planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, stroke-panel view projection, app-level brush refresh view projection, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `App::brush_update()`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, `NodePanelStroke::update_controls()`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-refresh`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `pano_cli plan-brush-stroke-panel-view`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge and panel adapter still mutate/read legacy `Brush`/`Canvas::I`, load/save legacy brush texture images, apply retained legacy quick/stroke/color widget writes, own brush thumbnail paths and popup behavior, and use temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands and view projection move toward a brush/app/asset command boundary and asset-managed texture/preset selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `pano_cli plan-brush-refresh --floating-picker --tip-flow 0.8 --tip-size 48 --r 0.2 --g 0.3 --b 0.4 --a 1`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `pano_cli plan-brush-stroke-panel-view --tip-size 64 --jitter-scatter 0.4 --dual-disabled --tip-blend-mode 2 --pattern-blend-mode 5`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, stroke-control execution, stroke-panel projection, and brush refresh projection are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, brush thumbnail/popup ownership, legacy quick/stroke/color widget writes, or brush-panel friend access |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |
@@ -916,10 +928,10 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0047 | Open | Modernization | PPBR brush package export request validation, success-dialog metadata, 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, and mobile/Web completion 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 --path D:/Paint/clouds.ppbr`; `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 mobile/Web completion 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-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly |
| DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through `PlatformServices`; the iOS/Web policy decision lives in tested `pp_platform_api::platform_policy`, but non-Windows execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch |
| DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through platform service boundaries; the iOS/Web policy decision lives in tested `pp_platform_api::platform_policy`, and WebGL flushing now goes through injectable `pp::platform::WebPlatformServices`, but non-Windows execution still lives in retained fallback adapters and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch |
| DEBT-0051 | Open | Modernization | Document browser search roots and Browse dialog working-directory picker visibility/path formatting now dispatch through `PlatformServices`; iOS Inbox roots and working-directory picker availability live in tested `pp_platform_api::platform_policy`, but macOS directory picker/display-path execution still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch |
| DEBT-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`; Windows/macOS save policy lives in tested `pp_platform_api::platform_policy`, and Windows placement reads/writes now use `LegacyWindowPreferenceSnapshot` plus `src/legacy_preference_storage.*`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge while Windows still stores placement through retained `Settings` behind the adapter | Preserve current Windows/macOS UI persistence while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple package smoke once root Apple builds exist | UI/window state persistence is owned by injected platform services with no legacy adapter branch |
| DEBT-0053 | Open | Modernization | Prepared-file writable target selection and prepared-file export-dialog policy now dispatch through `PlatformServices`; iOS temporary-file and WebGL data-path target planning live in tested `pp_platform_api::platform_policy`, but retained iOS/Web save/download handoff execution still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection, export-dialog policy, and save/download handoff are owned by injected platform services with no legacy adapter branch |
| DEBT-0053 | Open | Modernization | Prepared-file writable target selection and prepared-file export-dialog policy now dispatch through platform service boundaries; iOS temporary-file and WebGL data-path target planning live in tested `pp_platform_api::platform_policy`, and WebGL prepared-file handoff now goes through injectable `pp::platform::WebPlatformServices`, but retained iOS/Web save/download handoff execution still lives in retained fallback adapters | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection, export-dialog policy, and save/download handoff are owned by injected platform services with no legacy adapter branch |
| DEBT-0054 | Open | Modernization | Layout XML file read/reload decisions now consume `pp_platform_api::plan_asset_file_load`; platform-family reload behavior lives in tested `pp_platform_api::platform_policy` and pure probed planning, but the live wrapper still performs direct `stat` probing for Windows/macOS mtime reload checks until platform storage/file-watch services exist | Preserve current layout hot-reload and mobile/Web single-load behavior while removing platform guards from the shared `LayoutManager` parser | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build | Layout reload decisions are owned by injected platform storage/file-watch services or an asset manager boundary with platform-specific file watching removed from compile-time helpers |
| DEBT-0055 | Open | Modernization | `src/app.h` now forward-declares retained iOS/macOS/Android/Linux/Web platform handles instead of including platform SDK headers, and full SDK includes are isolated in `src/platform_legacy/legacy_platform_services.cpp`, but the `App` singleton still stores those platform handles directly | Reduce central header platform coupling incrementally without rewriting non-Windows platform entrypoints before Phase 6 | Windows app build; Apple/Android/Linux/Web package smoke once platform root builds are active | Platform handles are owned by injected `pp_platform_*` shell state or services, and `App` has no platform SDK handle fields or platform conditional members |
| DEBT-0056 | Open | Modernization | `src/asset.h` is now Android-SDK-free and uses opaque Android asset handles behind `Asset::set_android_asset_manager`, but retained `Asset` still owns a static Android asset-manager bridge and `src/asset.cpp` still performs Android `AAssetManager` reads directly; the current `android-arm64` root preset is headless and does not expose `pp_legacy_assets_io`, though the retained Android standard package `native-lib` now builds through its refreshed C++23 CMake path | Reduce legacy asset I/O header coupling without rewriting Android asset loading before the asset manager/storage boundary exists | Windows app build; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 -Targets pp_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | Android asset loading is owned by injected asset storage/platform services or `pp_assets` file providers, with no static Android asset manager on `Asset` |
@@ -927,7 +939,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0061 | Open | Modernization | Desktop XR runtime selection now lives in tested `pp_platform_api` policy and prefers OpenXR, but `WindowsPlatformServices` still reports OpenXR unavailable and reaches the retained OpenVR SDK bridge as a legacy fallback; Windows runtime deployment copies `openvr_api.dll` beside `PanoPainter.exe` until that fallback is removed | Preserve current desktop VR behavior while replacing OpenVR with OpenXR behind the platform/renderer boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add an OpenXR SDK/package target, implement desktop OpenXR startup/shutdown/pose/controller submission behind `pp_platform_vr` or `PlatformServices`, validate parity with mocked/runtime smoke coverage, and remove `libs/openvr` plus the OpenVR link/include paths from root CMake |
| DEBT-0062 | Open | Modernization | VS 2026 builds generate a patched fmt `format.h` overlay in the build tree for `pp_legacy_vendor`, disabling the old `_SECURE_SCL` checked-array iterator branch while leaving the fmt submodule clean | VS 2026's STL no longer exposes the legacy checked-array iterator used by this old fmt release, but replacing fmt is part of the dependency migration rather than this platform unblock | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `cmake --build --preset windows-msvc-default --config Debug --target pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | Move fmt to a supported vcpkg/package version or update the vendored fmt release, then remove the generated fmt overlay from `pp_legacy_vendor` |
| DEBT-0063 | Open | Modernization | The retained UI tree still exposes `Node* m_parent`, public `std::vector<std::shared_ptr<Node>> m_children`, raw `find<T>()` lookup results, `add_child<T>()` allocation through `new`, callbacks/observers that take or capture raw `Node*`, and manual `destroy()`/`m_destroyed` semantics. `pp_ui_core` now owns a tested `NodeLifetimeTree` target model with checked node handles, scoped callback connections, subtree destruction, pointer/keyboard capture release, whole-tree clear for layout reload, and mutation-safe dispatch, plus a tested `UiOverlayLifetime` popup/dialog stack model. Retained app-dialog root insertion, app-menu popup template cloning/root attachment, quick/stroke/brush panel popup root attachment, combo-box popup insertion, Open/Browse delete-confirmation dialog insertion, popup tick decoration insertion, top-toolbar panel popup insertion, repeated retained popup activation flag setup, repeated retained popup close/release execution, popup tick-decoration close callback wiring, and popup-panel outside-click release/remove/callback dispatch are now centralized in `src/legacy_ui_overlay_services.*`, but retained `Node`/`NodePopupMenu`/`NodeDialog*` still have not adopted checked handles or scoped callback ownership | Preserve current UI behavior while making panel/dialog extraction safe instead of spreading lifetime hazards into the new architecture | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; future `pp_panopainter_ui_dialog_lifetime_tests`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Retained `Node` and `pp_panopainter_ui` adopt checked node handles or equivalent non-owning references, scoped callback connection/disconnect semantics, mutation-safe event dispatch, parent/child invariants hidden behind APIs, and destroy-during-callback/capture-release/popup-close/layout-reload tests; retained `Node*` APIs are removed or isolated behind compatibility adapters |
| DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api::platform_policy`, but the Web shell still reaches it through the legacy platform fallback until injected Web services own the policy | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback |
| DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api` policy behind injectable `pp::platform::WebPlatformServices`, but the Web shell still reaches the default implementation through the retained fallback until a dedicated Web service is injected directly | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback |
| DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata, including message-dialog OK/cancel captions, now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`; live execution is centralized in `src/legacy_app_dialog_services.*`, retained root insertion now routes through `src/legacy_ui_overlay_services.*`, and whats-new dialog state persistence routes through `src/legacy_preference_storage.*`, but the bridge still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances with raw callback/lifetime ownership | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters |
| DEBT-0059 | Open | Modernization | iOS root CMake headless builds assign generated bundle identifiers and disable code signing for executable test/tool targets | The current Apple gate is compile validation for shared component targets; signed iOS app/package validation is not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device`; `sh scripts/automation/platform-build.sh "ios-device"` on `panopainter-mac` | Root CMake owns the signed Apple app/package targets, package-smoke validates Apple bundles where signing material is available, and headless iOS test/tool targets are either excluded from signed package builds or use explicit test-runner signing policy |

View File

@@ -34,11 +34,11 @@ auditable steps rather than by subjective estimates.
| Build and CMake ownership | 15 | 11 | Root CMake owns active source lists, app/tool targets, and retained package entrypoints. |
| Test and automation coverage | 15 | 9 | Headless, platform, package, and focused validation commands exist and are current. |
| Pure component behavior ownership | 15 | 8 | Behavior lives in `pp_*` components and is consumed by live adapters. |
| Legacy adapter retirement | 20 | 3 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. |
| Legacy adapter retirement | 20 | 4 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. |
| Renderer boundary and OpenGL parity | 15 | 3 | Live render/export/readback paths execute through renderer interfaces with parity checks. |
| Platform and package parity | 10 | 2 | Required platforms have root CMake/package validation and injected platform services. |
| Platform and package parity | 10 | 4 | Required platforms have root CMake/package validation and injected platform services. |
| Hardening and future backend readiness | 10 | 0 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. |
| **Total** | **100** | **36** | Only completed tasks below may change this number. |
| **Total** | **100** | **39** | Only completed tasks below may change this number. |
When updating `Current`, add a dated note under "Completed Task Log" with the
task id, points moved, validation command, and commit hash.
@@ -110,7 +110,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
### ADP-002 - Remove History Bridge From Layer Operations
Status: Ready
Status: Done
Score: +1 legacy adapter retirement
Debt: `DEBT-0021`
Scope: `src/legacy_document_layer_services.*`,
@@ -142,7 +142,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
### ADP-003 - Remove History Bridge From Document Open And Session Save
Status: Ready
Status: Done
Score: +1 legacy adapter retirement
Debt: `DEBT-0039`, `DEBT-0040`, `DEBT-0042`
Scope: `src/legacy_document_open_services.*`,
@@ -527,6 +527,8 @@ Done Checks:
| Date | Task | Score Change | Validation | Commit |
| --- | --- | ---: | --- | --- |
| 2026-06-12 | PLT-002 | +2 platform and package parity | `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | 8cd38401 |
| 2026-06-12 | ADP-002 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer" --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | pending |
| 2026-06-12 | ADP-001 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_resize\|pp_app_core_document_canvas\|pano_cli_plan_document_resize\|pano_cli_plan_canvas_clear" --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core\|pano_cli_plan"` | e489b1e2 |
| 2026-06-12 | MT-001 | 0 | `git diff -- docs\modernization\roadmap.md docs\modernization\tasks.md` | same docs slice |

View File

@@ -126,7 +126,8 @@ class DocumentLayerRenameServices {
public:
virtual ~DocumentLayerRenameServices() = default;
virtual void rename_layer(std::string_view old_name, std::string_view new_name) = 0;
virtual void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) = 0;
virtual void set_current_layer_name(std::string_view new_name) = 0;
virtual void finish_layer_rename() = 0;
};
@@ -156,6 +157,40 @@ public:
virtual void merge_layers(int from_index, int to_index, bool create_history) = 0;
};
[[nodiscard]] inline bool document_layer_rename_records_history(
const DocumentLayerRenamePlan& plan) noexcept
{
return plan.action == DocumentLayerRenameAction::rename_and_record_undo;
}
[[nodiscard]] inline bool document_layer_operation_records_history(
const DocumentLayerOperationPlan& plan) noexcept
{
switch (plan.operation) {
case DocumentLayerOperation::add:
case DocumentLayerOperation::duplicate:
case DocumentLayerOperation::remove:
case DocumentLayerOperation::set_opacity:
case DocumentLayerOperation::set_visibility:
case DocumentLayerOperation::set_alpha_lock:
case DocumentLayerOperation::set_blend_mode:
return plan.mutates_document;
case DocumentLayerOperation::reorder:
return plan.mutates_document;
case DocumentLayerOperation::select:
case DocumentLayerOperation::set_highlight:
return false;
}
return false;
}
[[nodiscard]] inline bool document_layer_merge_records_history(
const DocumentLayerMergePlan& plan) noexcept
{
return plan.create_history;
}
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
int layer_count,
int index) noexcept
@@ -596,7 +631,12 @@ public:
if (plan.old_name == plan.new_name) {
return pp::foundation::Status::invalid_argument("layer rename plan must change the name");
}
services.rename_layer(plan.old_name, plan.new_name);
if (!document_layer_rename_records_history(plan)) {
return pp::foundation::Status::invalid_argument(
"layer rename plan must record history when the name changes");
}
services.record_layer_rename_undo(plan.old_name, plan.new_name);
services.set_current_layer_name(plan.new_name);
services.finish_layer_rename();
return pp::foundation::Status::success();
}

View File

@@ -51,7 +51,7 @@ public:
{
}
void rename_layer(std::string_view old_name, std::string_view new_name) override
void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) override
{
if (!app_.layers || !app_.layers->m_current_layer || !app_.canvas || !app_.canvas->m_canvas)
return;
@@ -65,6 +65,16 @@ public:
new_name_copy,
layer_node,
layer));
}
void set_current_layer_name(std::string_view new_name) override
{
if (!app_.layers || !app_.layers->m_current_layer || !app_.canvas || !app_.canvas->m_canvas)
return;
auto layer_node = std::static_pointer_cast<NodeLayer>(app_.layers->m_current_layer->shared_from_this());
auto* layer = app_.canvas->m_canvas->m_layers[app_.canvas->m_canvas->m_current_layer_idx].get();
const std::string new_name_copy(new_name);
layer_node->set_name(new_name_copy.c_str());
layer->m_name = new_name_copy;
}

View File

@@ -34,19 +34,33 @@ public:
class FakeDocumentLayerRenameServices final : public pp::app::DocumentLayerRenameServices {
public:
void rename_layer(std::string_view old_name, std::string_view new_name) override
void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) override
{
rename_calls += 1;
history_calls += 1;
last_old_name = std::string(old_name);
last_new_name = std::string(new_name);
call_order += "history;";
}
void finish_layer_rename() override { finish_calls += 1; }
void set_current_layer_name(std::string_view new_name) override
{
rename_calls += 1;
last_new_name = std::string(new_name);
call_order += "rename;";
}
void finish_layer_rename() override
{
finish_calls += 1;
call_order += "finish;";
}
int history_calls = 0;
int rename_calls = 0;
int finish_calls = 0;
std::string last_old_name;
std::string last_new_name;
std::string call_order;
};
class FakeDocumentLayerOperationServices final : public pp::app::DocumentLayerOperationServices {
@@ -222,10 +236,12 @@ void layer_rename_executor_dispatches_changed_name(pp::tests::Harness& harness)
PP_EXPECT(harness, plan);
if (plan) {
PP_EXPECT(harness, pp::app::execute_document_layer_rename_plan(plan.value(), services).ok());
PP_EXPECT(harness, services.history_calls == 1);
PP_EXPECT(harness, services.rename_calls == 1);
PP_EXPECT(harness, services.finish_calls == 1);
PP_EXPECT(harness, services.last_old_name == "Base");
PP_EXPECT(harness, services.last_new_name == "Paint");
PP_EXPECT(harness, services.call_order == "history;rename;finish;");
}
}
@@ -237,8 +253,10 @@ void layer_rename_executor_finishes_no_op(pp::tests::Harness& harness)
PP_EXPECT(harness, plan);
if (plan) {
PP_EXPECT(harness, pp::app::execute_document_layer_rename_plan(plan.value(), services).ok());
PP_EXPECT(harness, services.history_calls == 0);
PP_EXPECT(harness, services.rename_calls == 0);
PP_EXPECT(harness, services.finish_calls == 1);
PP_EXPECT(harness, services.call_order == "finish;");
}
}
@@ -253,10 +271,26 @@ void layer_rename_executor_rejects_malformed_changed_plan(pp::tests::Harness& ha
const auto status = pp::app::execute_document_layer_rename_plan(malformed, services);
PP_EXPECT(harness, !status.ok());
PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument);
PP_EXPECT(harness, services.history_calls == 0);
PP_EXPECT(harness, services.rename_calls == 0);
PP_EXPECT(harness, services.finish_calls == 0);
}
void layer_rename_history_requirement_matches_action(pp::tests::Harness& harness)
{
const auto rename = pp::app::plan_document_layer_rename("Base", "Paint");
const auto no_op = pp::app::plan_document_layer_rename("Ink", "Ink");
PP_EXPECT(harness, rename);
PP_EXPECT(harness, no_op);
if (rename) {
PP_EXPECT(harness, pp::app::document_layer_rename_records_history(rename.value()));
}
if (no_op) {
PP_EXPECT(harness, !pp::app::document_layer_rename_records_history(no_op.value()));
}
}
void layer_add_validates_insert_index_and_name(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_document_layer_add(2, 1, "Paint");
@@ -526,6 +560,67 @@ void layer_operation_executor_dispatches_document_mutations(pp::tests::Harness&
}
}
void layer_operation_history_requirement_matches_operation(pp::tests::Harness& harness)
{
const auto add = pp::app::plan_document_layer_add(2, 1, "Paint");
const auto duplicate = pp::app::plan_document_layer_duplicate(3, 1);
const auto select = pp::app::plan_document_layer_select(3, 2);
const auto reorder = pp::app::plan_document_layer_reorder(3, 2, 0);
const auto no_op_reorder = pp::app::plan_document_layer_reorder(3, 1, 1);
const auto remove = pp::app::plan_document_layer_remove(3, 1);
const auto opacity = pp::app::plan_document_layer_opacity(3, 1, 0.25F);
const auto visibility = pp::app::plan_document_layer_visibility(3, 1, false);
const auto alpha_lock = pp::app::plan_document_layer_alpha_lock(3, 1, true);
const auto blend = pp::app::plan_document_layer_blend_mode(3, 1, 4);
const auto highlight = pp::app::plan_document_layer_highlight(3, 1, true);
PP_EXPECT(harness, add);
PP_EXPECT(harness, duplicate);
PP_EXPECT(harness, select);
PP_EXPECT(harness, reorder);
PP_EXPECT(harness, no_op_reorder);
PP_EXPECT(harness, remove);
PP_EXPECT(harness, opacity);
PP_EXPECT(harness, visibility);
PP_EXPECT(harness, alpha_lock);
PP_EXPECT(harness, blend);
PP_EXPECT(harness, highlight);
if (add) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(add.value()));
}
if (duplicate) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(duplicate.value()));
}
if (select) {
PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(select.value()));
}
if (reorder) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(reorder.value()));
}
if (no_op_reorder) {
PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(no_op_reorder.value()));
}
if (remove) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(remove.value()));
}
if (opacity) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(opacity.value()));
}
if (visibility) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(visibility.value()));
}
if (alpha_lock) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(alpha_lock.value()));
}
if (blend) {
PP_EXPECT(harness, pp::app::document_layer_operation_records_history(blend.value()));
}
if (highlight) {
PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(highlight.value()));
}
}
void layer_operation_executor_dispatches_selection_and_metadata(pp::tests::Harness& harness)
{
FakeDocumentLayerOperationServices services;
@@ -848,6 +943,21 @@ void layer_merge_plan_validates_supported_merge(pp::tests::Harness& harness)
}
}
void layer_merge_history_requirement_matches_flag(pp::tests::Harness& harness)
{
const auto merge = pp::app::plan_document_layer_merge(3, 2, 1, 1);
const auto no_history = pp::app::plan_document_layer_merge(3, 2, 0, 1, false);
PP_EXPECT(harness, merge);
PP_EXPECT(harness, no_history);
if (merge) {
PP_EXPECT(harness, pp::app::document_layer_merge_records_history(merge.value()));
}
if (no_history) {
PP_EXPECT(harness, !pp::app::document_layer_merge_records_history(no_history.value()));
}
}
void layer_merge_plan_rejects_bad_or_unsupported_state(pp::tests::Harness& harness)
{
PP_EXPECT(harness, !pp::app::plan_document_layer_merge(0, 0, 0, 1));
@@ -900,6 +1010,7 @@ int main()
harness.run("layer rename executor dispatches changed name", layer_rename_executor_dispatches_changed_name);
harness.run("layer rename executor finishes no op", layer_rename_executor_finishes_no_op);
harness.run("layer rename executor rejects malformed changed plan", layer_rename_executor_rejects_malformed_changed_plan);
harness.run("layer rename history requirement matches action", layer_rename_history_requirement_matches_action);
harness.run("layer add validates insert index and name", layer_add_validates_insert_index_and_name);
harness.run("layer duplicate select and reorder validate indices", layer_duplicate_select_and_reorder_validate_indices);
harness.run("layer remove keeps at least one layer", layer_remove_keeps_at_least_one_layer);
@@ -908,6 +1019,7 @@ int main()
harness.run("layer panel view projects current controls and visibility", layer_panel_view_projects_current_controls_and_visibility);
harness.run("layer panel view rejects invalid document state", layer_panel_view_rejects_invalid_document_state);
harness.run("layer operation executor dispatches document mutations", layer_operation_executor_dispatches_document_mutations);
harness.run("layer operation history requirement matches operation", layer_operation_history_requirement_matches_operation);
harness.run("layer operation executor dispatches selection and metadata", layer_operation_executor_dispatches_selection_and_metadata);
harness.run("layer operation executor preserves no op and transient actions", layer_operation_executor_preserves_no_op_and_transient_actions);
harness.run("layer operation executor rejects malformed mutation plans", layer_operation_executor_rejects_malformed_mutation_plans);
@@ -917,6 +1029,7 @@ int main()
harness.run("layer menu executor dispatches menu actions", layer_menu_executor_dispatches_menu_actions);
harness.run("layer menu executor preserves no op actions", layer_menu_executor_preserves_no_op_actions);
harness.run("layer merge plan validates supported merge", layer_merge_plan_validates_supported_merge);
harness.run("layer merge history requirement matches flag", layer_merge_history_requirement_matches_flag);
harness.run("layer merge plan rejects bad or unsupported state", layer_merge_plan_rejects_bad_or_unsupported_state);
harness.run("layer merge executor dispatches merge", layer_merge_executor_dispatches_merge);
harness.run("layer merge executor rejects malformed plan", layer_merge_executor_rejects_malformed_plan);