Dispatch cube export writes through app core

This commit is contained in:
2026-06-05 20:09:46 +02:00
parent af28da4e83
commit 2d33f9d928
7 changed files with 248 additions and 27 deletions

View File

@@ -282,10 +282,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
- Live equirectangular, layer, animation-frame, and cube-face export adapters - Live equirectangular, layer, animation-frame, and cube-face export adapters
now prepare and log the same payload-bearing canvas document snapshot plus now prepare and log the same payload-bearing canvas document snapshot plus
shared renderer-neutral active-frame upload and face-PNG export reports. shared renderer-neutral active-frame upload and face-PNG export reports.
Cube-face export writes the pure document/renderer PNG bytes to the retained Cube-face export writes the pure document/renderer PNG bytes through the
face filename set and falls back to `Canvas::export_cube_faces` if snapshot app-core write/publish executor to the planned retained face filename set and
capture, PNG generation, or file writing fails. Equirectangular, layer, falls back to `Canvas::export_cube_faces` if snapshot capture, PNG generation,
animation-frame, depth, and video export remain on retained writer paths. or file writing fails. Equirectangular, layer, animation-frame, depth, and
video export remain on retained writer paths.
- `pano_cli save-document-project` writes that pure document export to a PPI - `pano_cli save-document-project` writes that pure document export to a PPI
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`, file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
which inspects and loads the generated file. which inspects and loads the generated file.
@@ -999,7 +1000,8 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
- `pp_app_core_document_export_tests` covers export file targets, collection - `pp_app_core_document_export_tests` covers export file targets, collection
directory/stem targets, picked-directory stems, work-directory versus directory/stem targets, picked-directory stems, work-directory versus
picker-stem collection target planning, cube-face legacy work-path sets, MP4 picker-stem collection target planning, cube-face legacy work-path sets, MP4
suggested names, and invalid export naming inputs, plus export-start license/canvas availability decisions, suggested names, cube-face write/publish service execution order and failure
handling, and invalid export naming inputs, plus export-start license/canvas availability decisions,
export menu executor dispatch, file/stem/collection export execution export menu executor dispatch, file/stem/collection export execution
dispatch, failed directory creation preservation, named depth/cube export dispatch, failed directory creation preservation, named depth/cube export
dispatch, malformed export target rejection, video export dispatch for dispatch, malformed export target rejection, video export dispatch for
@@ -1125,8 +1127,9 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
Equirectangular, layer, animation-frame, and cube-face execution now prepare Equirectangular, layer, animation-frame, and cube-face execution now prepare
the document snapshot plus renderer-upload readiness report before those the document snapshot plus renderer-upload readiness report before those
retained calls; cube-face export writes pure face-PNG bytes to the app-core retained calls; cube-face export writes pure face-PNG bytes to the app-core
planned legacy work-directory face paths before falling back to the retained planned legacy work-directory face paths through the app-core write/publish
writer, and depth export remains on the older retained path. It executor before falling back to the retained writer, and depth export remains
on the older retained path. It
also bridges timelapse and animation MP4 export picker-selected paths while also bridges timelapse and animation MP4 export picker-selected paths while
preserving desktop worker-thread timelapse behavior, mobile/Web save preserving desktop worker-thread timelapse behavior, mobile/Web save
callbacks, `App::rec_export`, animation `Canvas::export_anim_mp4`, and callbacks, `App::rec_export`, animation `Canvas::export_anim_mp4`, and

View File

@@ -26,7 +26,7 @@ and validation command.
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file | | PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness | | PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness |
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload readiness | | Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload readiness |
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, pure face-PNG export helper, app-core face filename planning, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set | | Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, pure face-PNG export helper, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation | | Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
## Brush And Painting ## Brush And Painting

View File

@@ -516,6 +516,12 @@ agent or engineer to remove them without reconstructing context from chat.
`pano_cli plan-export-target --kind cube-faces`, and consumed by `pano_cli plan-export-target --kind cube-faces`, and consumed by
`src/legacy_document_export_services.*` before writing pure face-PNG bytes. `src/legacy_document_export_services.*` before writing pure face-PNG bytes.
Storage service ownership and non-cube retained export writers remain open. Storage service ownership and non-cube retained export writers remain open.
- 2026-06-05: DEBT-0043 was narrowed again. Cube-face export file writes and
exported-image publishing now dispatch through tested `pp_app_core`
`execute_document_cube_face_export_write`, so the live bridge only adapts the
retained filesystem write and `App::publish_exported_image` calls. Dedicated
storage/platform service ownership and non-cube retained export writers
remain open.
## Open Debt ## Open Debt
@@ -529,7 +535,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, and root CMake now exposes a focused `panopainter_platform_build_vcpkg_ui_core` target for the vcpkg-backed `pp_ui_core`/tinyxml2 boundary, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, and root CMake now exposes a focused `panopainter_platform_build_vcpkg_ui_core` target for the vcpkg-backed `pp_ui_core`/tinyxml2 boundary, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |
| DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection | | DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection |
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning with retained fallback, and live equirectangular/layer/animation-frame/cube-face export snapshot/render/export-readiness reporting, but action-command adoption, live save-writer replacement, broader renderer-owned export execution, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade | | DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning and write/publish service dispatch with retained fallback, and live equirectangular/layer/animation-frame/cube-face export snapshot/render/export-readiness reporting, but action-command adoption, live save-writer replacement, broader renderer-owned export execution, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade |
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, and reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, and retained Linux/WebGL CMake baseline metadata are reachable from package validation and root CMake package-readiness targets, but Windows AppX/APK/Linux/Apple/WebGL package outputs are still `blocked` because root CMake package targets do not exist yet | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, and reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, and retained Linux/WebGL CMake baseline metadata are reachable from package validation and root CMake package-readiness targets, but Windows AppX/APK/Linux/Apple/WebGL package outputs are still `blocked` because root CMake package targets do not exist yet | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present |
| DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception |
| DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, decoded pixel attachment to `pp_document`, live save-path snapshot-readiness reporting, and app-core canvas-snapshot-to-PPI export automation, but full legacy PPI round-trip parity and pure live save writer replacement are not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility | | DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, decoded pixel attachment to `pp_document`, live save-path snapshot-readiness reporting, and app-core canvas-snapshot-to-PPI export automation, but full legacy PPI round-trip parity and pure live save writer replacement are not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility |
@@ -561,7 +567,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0040 | Open | Modernization | Close request, document save, save-before-workflow planning/execution dispatch, and close/save-before/save-error prompt metadata now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `pano_cli plan-document-session-prompt`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; close/save-before prompt creation now uses `src/legacy_app_dialog_services.*`, Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, and existing-project save/save-before-workflow execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still opens retained save dialogs, wires prompt callbacks directly, delegates actual writing to `Canvas::project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-document-session-prompt --kind close-unsaved`; `pano_cli plan-document-session-prompt --kind save-before-workflow`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters | | DEBT-0040 | Open | Modernization | Close request, document save, save-before-workflow planning/execution dispatch, and close/save-before/save-error prompt metadata now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `pano_cli plan-document-session-prompt`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; close/save-before prompt creation now uses `src/legacy_app_dialog_services.*`, Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, and existing-project save/save-before-workflow execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still opens retained save dialogs, wires prompt callbacks directly, delegates actual writing to `Canvas::project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-document-session-prompt --kind close-unsaved`; `pano_cli plan-document-session-prompt --kind save-before-workflow`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters |
| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch and new-document overwrite prompt metadata now consume pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `pano_cli plan-document-session-prompt`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; new-document overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, wires overwrite callbacks directly, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-session-prompt --kind new-document-overwrite`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter | | DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch and new-document overwrite prompt metadata now consume pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `pano_cli plan-document-session-prompt`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; new-document overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, wires overwrite callbacks directly, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-session-prompt --kind new-document-overwrite`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter |
| DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch plus Save As overwrite prompt metadata now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-document-session-prompt`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`; Save As overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and accepted Save As/Save Version execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still wires overwrite callbacks directly, delegates actual writing to legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-session-prompt --kind file-overwrite --name demo`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters | | DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch plus Save As overwrite prompt metadata now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-document-session-prompt`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`; Save As overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and accepted Save As/Save Version execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still wires overwrite callbacks directly, delegates actual writing to legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-session-prompt --kind file-overwrite --name demo`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters |
| DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, export success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, equirectangular/layer/animation-frame/cube-face execution prepares a payload-bearing document snapshot plus renderer-neutral upload and face-PNG export reports, and cube-face export writes the pure face PNG bytes to `pp_app_core` planned work-directory face paths before falling back to retained Canvas execution on failure, but the bridge still calls legacy `Canvas` export methods for equirectangular/layers/animation/depth, creates export directories, handles picker-selected stems, performs Web prepared-file handoff directly, and leaves depth export on the retained path | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli plan-export-target --kind cube-faces --work-dir D:/Paint --doc-name demo`; `pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint`; `pano_cli plan-export-report --kind license-disabled`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and remaining retained export execution, export-directory creation, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, export success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, equirectangular/layer/animation-frame/cube-face execution prepares a payload-bearing document snapshot plus renderer-neutral upload and face-PNG export reports, and cube-face export writes the pure face PNG bytes to `pp_app_core` planned work-directory face paths through `execute_document_cube_face_export_write` before falling back to retained Canvas execution on failure, but the bridge still adapts retained filesystem writes/exported-image publishing locally, still calls legacy `Canvas` export methods for equirectangular/layers/animation/depth, creates export directories, handles picker-selected stems, performs Web prepared-file handoff directly, and leaves depth export on the retained path | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli plan-export-target --kind cube-faces --work-dir D:/Paint --doc-name demo`; `pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint`; `pano_cli plan-export-report --kind license-disabled`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and remaining retained export execution, export-directory creation, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `pano_cli plan-export-message`, `pano_cli plan-export-report`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, and success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, and owns mobile/Web save callbacks | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-export-report --kind animation-mp4 --message "video export path must not be empty"`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, and mobile/Web save callbacks are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `pano_cli plan-export-message`, `pano_cli plan-export-report`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, and success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, and owns mobile/Web save callbacks | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-export-report --kind animation-mp4 --message "video export path must not be empty"`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, and mobile/Web save callbacks are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and `Settings::save` directly; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, whose desktop runtime policy prefers OpenXR while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` under DEBT-0061 | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | | DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and `Settings::save` directly; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, whose desktop runtime policy prefers OpenXR while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` under DEBT-0061 | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters |
| DEBT-0046 | Open | Modernization | Startup preference/runtime execution and startup resource sequencing now consume pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `pano_cli plan-app-startup-resources`, `AppStartupServices`, `AppStartupResourceServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, message-box license warning execution, shader loading, asset initialization, layout creation, title updates, and UI render-target creation directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI/renderer services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `pano_cli plan-app-startup-resources --width 1280 --height 720`; `pano_cli plan-app-startup-resources --bad-size`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, startup resource initialization, title updates, and UI render-target allocation are owned by injected app/preferences/storage/recording/UI/renderer services with `App::init` acting only as orchestration | | DEBT-0046 | Open | Modernization | Startup preference/runtime execution and startup resource sequencing now consume pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `pano_cli plan-app-startup-resources`, `AppStartupServices`, `AppStartupResourceServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, message-box license warning execution, shader loading, asset initialization, layout creation, title updates, and UI render-target creation directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI/renderer services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `pano_cli plan-app-startup-resources --width 1280 --height 720`; `pano_cli plan-app-startup-resources --bad-size`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, startup resource initialization, title updates, and UI render-target allocation are owned by injected app/preferences/storage/recording/UI/renderer services with `App::init` acting only as orchestration |

View File

@@ -689,10 +689,11 @@ renderer-owned readback remain under
Live equirectangular, layer, animation-frame, and cube-face export adapters now Live equirectangular, layer, animation-frame, and cube-face export adapters now
prepare the same payload-bearing document snapshot and renderer-neutral upload prepare the same payload-bearing document snapshot and renderer-neutral upload
report helper plus pure face-PNG export report. Cube-face export writes those report helper plus pure face-PNG export report. Cube-face export writes those
document/renderer-owned PNG bytes to the app-core-planned legacy face filenames document/renderer-owned PNG bytes through a tested app-core write/publish
when available and falls back to retained `Canvas::export_cube_faces` on executor using the app-core-planned legacy face filenames when available and
snapshot/write failure; the other export workflows still delegate to retained falls back to retained `Canvas::export_cube_faces` on snapshot/write failure;
`Canvas` writers after readiness reporting. the other export workflows still delegate to retained `Canvas` writers after
readiness reporting.
`pano_cli plan-image-import` exposes app-core planning for File > Import image `pano_cli plan-image-import` exposes app-core planning for File > Import image
route decisions, including wide equirectangular images, legacy vertical cube route decisions, including wide equirectangular images, legacy vertical cube
strips, regular transform-placement images, and invalid image dimensions; live strips, regular transform-placement images, and invalid image dimensions; live
@@ -2532,10 +2533,11 @@ Results:
- Live image/collection/cube export adapters now prepare and log the same - Live image/collection/cube export adapters now prepare and log the same
document/canvas plus shared renderer-upload and face-PNG export readiness document/canvas plus shared renderer-upload and face-PNG export readiness
reports. Cube-face export now writes the pure document/renderer PNG bytes to reports. Cube-face export now writes the pure document/renderer PNG bytes to
the `pp_app_core` planned legacy face filenames before falling back to the `pp_app_core` planned legacy face filenames through a tested
retained `Canvas` execution on failure. Equirectangular, layer, write/publish service executor before falling back to retained `Canvas`
animation-frame, depth, and video export remain on their prior retained writer execution on failure. Equirectangular, layer, animation-frame, depth, and
paths; actual broader writer replacement remains tracked under export debt. video export remain on their prior retained writer paths; actual broader
writer replacement remains tracked under export debt.
- Snapshot creation now rejects invalid embedded RGBA8 face payloads before - Snapshot creation now rejects invalid embedded RGBA8 face payloads before
document export or history can persist malformed state. document export or history can persist malformed state.
- Package-smoke wrappers validate the Windows CMake app executable/runtime - Package-smoke wrappers validate the Windows CMake app executable/runtime

View File

@@ -6,6 +6,7 @@
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
@@ -36,6 +37,10 @@ struct DocumentCubeFaceExportTarget {
std::size_t face_count = 0; std::size_t face_count = 0;
}; };
struct DocumentCubeFaceExportPayload {
std::span<const std::byte> bytes;
};
struct DocumentExportSuggestedName { struct DocumentExportSuggestedName {
std::string name; std::string name;
}; };
@@ -175,6 +180,16 @@ public:
virtual void show_timelapse_export_success(std::string_view path) = 0; virtual void show_timelapse_export_success(std::string_view path) = 0;
}; };
class DocumentCubeFaceExportWriteServices {
public:
virtual ~DocumentCubeFaceExportWriteServices() = default;
virtual pp::foundation::Status write_binary_file(
std::string_view path,
std::span<const std::byte> bytes) = 0;
virtual void publish_exported_image(std::string_view path) = 0;
};
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start( [[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
bool requires_license, bool requires_license,
bool license_valid, bool license_valid,
@@ -609,6 +624,38 @@ document_cube_face_export_names() noexcept
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target)); return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
} }
[[nodiscard]] inline pp::foundation::Status execute_document_cube_face_export_write(
const DocumentCubeFaceExportTarget& target,
std::span<const DocumentCubeFaceExportPayload> face_payloads,
DocumentCubeFaceExportWriteServices& services)
{
if (target.face_count != pp::document::cube_face_count) {
return pp::foundation::Status::invalid_argument("cube face export target must contain all cube faces");
}
if (face_payloads.size() != target.face_count) {
return pp::foundation::Status::invalid_argument("cube face export payload count must match target face count");
}
for (std::size_t face_index = 0; face_index < target.face_count; ++face_index) {
const auto& face = target.faces[face_index];
if (face.path.empty()) {
return pp::foundation::Status::invalid_argument("cube face export path must not be empty");
}
if (face_payloads[face_index].bytes.empty()) {
return pp::foundation::Status::invalid_argument("cube face export payload must not be empty");
}
const auto write_status = services.write_binary_file(face.path, face_payloads[face_index].bytes);
if (!write_status.ok()) {
return write_status;
}
services.publish_exported_image(face.path);
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_document_export_file( [[nodiscard]] inline pp::foundation::Status execute_document_export_file(
const DocumentExportFileTarget& target, const DocumentExportFileTarget& target,
DocumentExportServices& services) DocumentExportServices& services)

View File

@@ -6,6 +6,7 @@
#include "legacy_document_canvas_services.h" #include "legacy_document_canvas_services.h"
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include <array>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <span> #include <span>
@@ -29,7 +30,7 @@ struct LegacyDocumentExportSnapshotReports {
pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs; pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs;
}; };
pp::foundation::Status write_binary_file(std::string_view path, std::span<const std::byte> bytes) pp::foundation::Status write_export_binary_file(std::string_view path, std::span<const std::byte> bytes)
{ {
if (path.empty()) { if (path.empty()) {
return pp::foundation::Status::invalid_argument("export path must not be empty"); return pp::foundation::Status::invalid_argument("export path must not be empty");
@@ -51,6 +52,29 @@ pp::foundation::Status write_binary_file(std::string_view path, std::span<const
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
class LegacyCubeFaceExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices {
public:
explicit LegacyCubeFaceExportWriteServices(App& app) noexcept
: app_(app)
{
}
pp::foundation::Status write_binary_file(
std::string_view path,
std::span<const std::byte> bytes) override
{
return write_export_binary_file(path, bytes);
}
void publish_exported_image(std::string_view path) override
{
app_.publish_exported_image(std::string(path));
}
private:
App& app_;
};
pp::foundation::Result<LegacyDocumentExportSnapshotReports> prepare_legacy_document_export_snapshot( pp::foundation::Result<LegacyDocumentExportSnapshotReports> prepare_legacy_document_export_snapshot(
App& app, App& app,
const char* context) const char* context)
@@ -149,16 +173,13 @@ pp::foundation::Status export_cube_faces_from_document_snapshot(
return pp::foundation::Status::invalid_argument("document snapshot did not produce all cube face PNGs"); return pp::foundation::Status::invalid_argument("document snapshot did not produce all cube face PNGs");
} }
for (std::size_t face_index = 0; face_index < target.value().face_count; ++face_index) { std::array<pp::app::DocumentCubeFaceExportPayload, pp::document::cube_face_count> payloads;
const auto& face = target.value().faces[face_index]; for (std::size_t face_index = 0; face_index < payloads.size(); ++face_index) {
const auto status = write_binary_file(face.path, reports.face_pngs.face_pngs[face_index]); payloads[face_index].bytes = std::span<const std::byte>(reports.face_pngs.face_pngs[face_index]);
if (!status.ok()) {
return status;
}
app.publish_exported_image(face.path);
} }
return pp::foundation::Status::success(); LegacyCubeFaceExportWriteServices services(app);
return pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
} }
class LegacyDocumentExportServices final : public pp::app::DocumentExportServices { class LegacyDocumentExportServices final : public pp::app::DocumentExportServices {

View File

@@ -1,6 +1,9 @@
#include "app_core/document_export.h" #include "app_core/document_export.h"
#include "test_harness.h" #include "test_harness.h"
#include <array>
#include <cstddef>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -149,6 +152,55 @@ public:
std::string call_order; std::string call_order;
}; };
class FakeDocumentCubeFaceExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices {
public:
pp::foundation::Status write_binary_file(
std::string_view path,
std::span<const std::byte> bytes) override
{
write_calls += 1;
last_path = std::string(path);
total_bytes += bytes.size();
call_order += "write:";
call_order += path;
call_order += ";";
if (fail_on_write_call == write_calls) {
return pp::foundation::Status::invalid_argument("write failed");
}
return pp::foundation::Status::success();
}
void publish_exported_image(std::string_view path) override
{
publish_calls += 1;
last_published_path = std::string(path);
call_order += "publish:";
call_order += path;
call_order += ";";
}
int write_calls = 0;
int publish_calls = 0;
int fail_on_write_call = -1;
std::size_t total_bytes = 0;
std::string last_path;
std::string last_published_path;
std::string call_order;
};
std::array<pp::app::DocumentCubeFaceExportPayload, 6> make_test_cube_face_payloads(
const std::array<std::array<std::byte, 4>, 6>& bytes)
{
return {
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[0]) },
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[1]) },
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[2]) },
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[3]) },
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[4]) },
pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[5]) },
};
}
void equirectangular_export_builds_file_target(pp::tests::Harness& harness) void equirectangular_export_builds_file_target(pp::tests::Harness& harness)
{ {
const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png"); const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
@@ -212,6 +264,89 @@ void cube_face_export_rejects_invalid_target_inputs(pp::tests::Harness& harness)
PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument);
} }
void cube_face_export_writer_writes_and_publishes_faces_in_order(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
const std::array<std::array<std::byte, 4>, 6> bytes {};
const auto payloads = make_test_cube_face_payloads(bytes);
FakeDocumentCubeFaceExportWriteServices services;
PP_EXPECT(harness, target);
PP_EXPECT(
harness,
pp::app::execute_document_cube_face_export_write(target.value(), payloads, services).ok());
PP_EXPECT(harness, services.write_calls == 6);
PP_EXPECT(harness, services.publish_calls == 6);
PP_EXPECT(harness, services.total_bytes == 24U);
PP_EXPECT(harness, services.last_path == "D:/Paint/demo-bottom.png");
PP_EXPECT(harness, services.last_published_path == "D:/Paint/demo-bottom.png");
PP_EXPECT(
harness,
services.call_order
== "write:D:/Paint/demo-front.png;publish:D:/Paint/demo-front.png;"
"write:D:/Paint/demo-right.png;publish:D:/Paint/demo-right.png;"
"write:D:/Paint/demo-back.png;publish:D:/Paint/demo-back.png;"
"write:D:/Paint/demo-left.png;publish:D:/Paint/demo-left.png;"
"write:D:/Paint/demo-top.png;publish:D:/Paint/demo-top.png;"
"write:D:/Paint/demo-bottom.png;publish:D:/Paint/demo-bottom.png;");
}
void cube_face_export_writer_stops_before_publish_on_write_failure(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
const std::array<std::array<std::byte, 4>, 6> bytes {};
const auto payloads = make_test_cube_face_payloads(bytes);
FakeDocumentCubeFaceExportWriteServices services;
services.fail_on_write_call = 2;
PP_EXPECT(harness, target);
const auto status = pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
PP_EXPECT(harness, !status.ok());
PP_EXPECT(harness, services.write_calls == 2);
PP_EXPECT(harness, services.publish_calls == 1);
PP_EXPECT(
harness,
services.call_order
== "write:D:/Paint/demo-front.png;publish:D:/Paint/demo-front.png;"
"write:D:/Paint/demo-right.png;");
}
void cube_face_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
{
auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
const std::array<std::array<std::byte, 4>, 6> bytes {};
auto payloads = make_test_cube_face_payloads(bytes);
FakeDocumentCubeFaceExportWriteServices services;
PP_EXPECT(harness, target);
auto malformed_target = target.value();
malformed_target.face_count = 5U;
PP_EXPECT(
harness,
!pp::app::execute_document_cube_face_export_write(malformed_target, payloads, services).ok());
PP_EXPECT(
harness,
!pp::app::execute_document_cube_face_export_write(
target.value(),
std::span<const pp::app::DocumentCubeFaceExportPayload>(payloads.data(), payloads.size() - 1U),
services)
.ok());
auto empty_path_target = target.value();
empty_path_target.faces[0].path.clear();
PP_EXPECT(
harness,
!pp::app::execute_document_cube_face_export_write(empty_path_target, payloads, services).ok());
payloads[0].bytes = {};
PP_EXPECT(
harness,
!pp::app::execute_document_cube_face_export_write(target.value(), payloads, services).ok());
PP_EXPECT(harness, services.write_calls == 0);
PP_EXPECT(harness, services.publish_calls == 0);
}
void video_export_builds_suggested_name(pp::tests::Harness& harness) void video_export_builds_suggested_name(pp::tests::Harness& harness)
{ {
const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse"); const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse");
@@ -749,6 +884,13 @@ int main()
harness.run("picked directory export builds stem", picked_directory_export_builds_stem); harness.run("picked directory export builds stem", picked_directory_export_builds_stem);
harness.run("cube face export builds legacy work paths", cube_face_export_builds_legacy_work_paths); harness.run("cube face export builds legacy work paths", cube_face_export_builds_legacy_work_paths);
harness.run("cube face export rejects invalid target inputs", cube_face_export_rejects_invalid_target_inputs); harness.run("cube face export rejects invalid target inputs", cube_face_export_rejects_invalid_target_inputs);
harness.run(
"cube face export writer writes and publishes faces in order",
cube_face_export_writer_writes_and_publishes_faces_in_order);
harness.run(
"cube face export writer stops before publish on write failure",
cube_face_export_writer_stops_before_publish_on_write_failure);
harness.run("cube face export writer rejects malformed inputs", cube_face_export_writer_rejects_malformed_inputs);
harness.run("video export builds suggested name", video_export_builds_suggested_name); harness.run("video export builds suggested name", video_export_builds_suggested_name);
harness.run("collection export target plan selects platform destination", collection_export_target_plan_selects_platform_destination); harness.run("collection export target plan selects platform destination", collection_export_target_plan_selects_platform_destination);
harness.run("export success dialog plans image destinations", export_success_dialog_plans_image_destinations); harness.run("export success dialog plans image destinations", export_success_dialog_plans_image_destinations);