diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 03927df..6514cf4 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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> m_children`, raw `find()` lookup results, `add_child()` 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 | diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 1de1285..6735b59 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -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 | diff --git a/src/app_core/document_layer.h b/src/app_core/document_layer.h index ee5296d..b0ec39c 100644 --- a/src/app_core/document_layer.h +++ b/src/app_core/document_layer.h @@ -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(); } diff --git a/src/legacy_document_layer_services.cpp b/src/legacy_document_layer_services.cpp index 3ac6391..bdc53b5 100644 --- a/src/legacy_document_layer_services.cpp +++ b/src/legacy_document_layer_services.cpp @@ -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(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; } diff --git a/tests/app_core/document_layer_tests.cpp b/tests/app_core/document_layer_tests.cpp index 84220d4..6ca4cca 100644 --- a/tests/app_core/document_layer_tests.cpp +++ b/tests/app_core/document_layer_tests.cpp @@ -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);