diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index f81df2c..f7e1e66 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -649,6 +649,10 @@ Known local toolchain state: for image file exports, layer/frame collection directories, picked-directory stems, and MP4 suggested names as JSON and is covered for file, collection, and suggested-name states. +- `pano_cli plan-export-message` exposes `pp_app_core` export completion + dialog metadata for equirectangular image, layer/frame collection, + depth/cube, animation MP4, and timelapse success paths as JSON, including + platform-style destinations and suppressed/no-message paths. - `pano_cli plan-export-start` exposes `pp_app_core` export availability planning for license-gated, demo-blocked, and missing-canvas states as JSON; the live image, layer, animation-frame, depth, and cube-face export dialogs diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 6f25478..ccf683c 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -193,7 +193,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `App::rec_loop`, `App::update_ui_observer`, `App::render_task*`, `App::ui_task*`, `App::render_thread_*`, `App::ui_thread_*`, file-menu save actions, `NodeCanvas` canvas hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress/worker decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display and renderer diagnostic decisions, app dialog metadata decisions, app frame/UI-observer decisions, app thread/task orchestration decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-app-dialog`, `pano_cli plan-app-thread`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-canvas-hotkey`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/dialog/thread/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar/canvas-command contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, app dialog node creation, status/display UI rendering, renderer diagnostic capability adaptation, app task/thread execution, UI observer parent walking/callback execution, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4/PBO execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_frame_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_app_dialog_tests`; `pp_app_core_app_thread_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --running --no-encoder`; `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-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12 --framebuffer-fetch --float32 --float32-linear --float16`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-frame`; `pano_cli plan-app-thread --kind ui-loop --live-reload`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `App::rec_loop`, `App::update_ui_observer`, `App::render_task*`, `App::ui_task*`, `App::render_thread_*`, `App::ui_thread_*`, file-menu save actions, `NodeCanvas` canvas hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path/message decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress/worker decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display and renderer diagnostic decisions, app dialog metadata decisions, app frame/UI-observer decisions, app thread/task orchestration decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-export-message`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-app-dialog`, `pano_cli plan-app-thread`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-canvas-hotkey`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/dialog/thread/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar/canvas-command contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, app dialog node creation, status/display UI rendering, renderer diagnostic capability adaptation, app task/thread execution, UI observer parent walking/callback execution, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4/PBO execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_frame_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_app_dialog_tests`; `pp_app_core_app_thread_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --running --no-encoder`; `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-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12 --framebuffer-fetch --float32 --float32-linear --float16`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-frame`; `pano_cli plan-app-thread --kind ui-loop --live-reload`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, 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 | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | @@ -231,8 +231,8 @@ 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.*`; Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still opens retained `NodeMessageBox`/save dialogs, wires callbacks directly, calls `Canvas::I->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 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, creates retained `NodeMessageBox` overwrite prompts, 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.*`, but the bridge still creates retained `NodeMessageBox` overwrite prompts, calls legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-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, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | -| DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | +| DEBT-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, and export success dialog metadata now comes from `plan_document_export_success_dialog`/`pano_cli plan-export-message`, but the bridge still calls legacy `Canvas` export methods, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube 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`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, and success-message metadata now comes from `plan_document_export_success_dialog`, 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`; `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`, while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | | DEBT-0046 | Open | Modernization | Startup preference/runtime execution 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-0047 | Open | Modernization | PPBR brush package export request validation and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, and the macOS data-directory override now routes through `PlatformServices`, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, owns desktop worker-thread dispatch, dialog destruction, mobile/Web completion, and success-message behavior directly | Preserve current PPBR export behavior while brush assets, PPBR serialization, picker completion, and UI lifetime move toward asset/storage/UI/platform services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_export_tests`; `pp_platform_api_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --author Artist --dest-path D:/Paint/BrushPreviews --export-data --header-image`; `pano_cli plan-brush-package-export`; `pano_cli plan-brush-package-export --path clouds`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --dest-path D:/Paint/BrushPreviews --no-export-data`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | PPBR metadata collection, header-image ownership, serialization, picker-selected path execution, desktop threading, dialog lifetime, and success UI are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 1b4ad4b..f1c598a 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -519,6 +519,10 @@ stems, and MP4 suggested names used by the live export dialogs. used by live image, layer, animation-frame, depth, and cube-face export dialogs plus MP4 animation and timelapse export dialogs before they call legacy canvas/recording export execution. +`pano_cli plan-export-message` exposes the app-core export completion dialog +metadata now consumed by the live legacy export bridge for equirectangular, +layer/frame, depth/cube, animation MP4, and timelapse success reporting, +including platform-style destinations and no-message/suppressed branches. `pano_cli plan-recording-session` exposes the app-core recording start, stop, clear, platform recorded-file cleanup, frame reset, and export progress-total decisions used by the live recording controls. Recording lifecycle and MP4 @@ -655,6 +659,9 @@ choices, including image, layer, cube-face, depth, animation-frame, MP4, and timelapse dialog routing plus license/canvas gating. Export menu commands now dispatch through `DocumentExportMenuServices` in the shared app-shell bridge before legacy export dialogs and renderer/video execution continue. +Export success-message metadata now also comes from `pp_app_core` through +`pano_cli plan-export-message` and the legacy document-export bridge, reducing +the bridge to export execution, platform handoff, and retained threading. `pano_cli plan-grid-operation` exposes app-core planning for grid heightmap pick/load/reload/clear, lightmap render capability/limit checks, and heightmap commit used by the live grid panel. Grid execution now dispatches through diff --git a/src/app_core/document_export.h b/src/app_core/document_export.h index f5dd66a..eae0c46 100644 --- a/src/app_core/document_export.h +++ b/src/app_core/document_export.h @@ -1,5 +1,6 @@ #pragma once +#include "app_core/app_dialog.h" #include "foundation/result.h" #include @@ -71,6 +72,26 @@ enum class DocumentVideoExportKind { timelapse, }; +enum class DocumentExportSuccessKind { + equirectangular, + layers, + animation_frames, + depth, + cube_faces, + animation_mp4, + timelapse, +}; + +enum class DocumentExportSuccessDestination { + suppressed, + photos, + pictures_panopainter, + files_panopainter, + work_directory, + path, + generic_success, +}; + struct DocumentExportMenuPlan { DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg; DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog; @@ -83,6 +104,13 @@ struct DocumentExportCollectionTargetPlan { std::string_view suffix; }; +struct DocumentExportSuccessDialogPlan { + DocumentExportSuccessKind kind = DocumentExportSuccessKind::equirectangular; + DocumentExportSuccessDestination destination = DocumentExportSuccessDestination::suppressed; + AppMessageDialogPlan dialog; + bool show_dialog = false; +}; + class DocumentExportMenuServices { public: virtual ~DocumentExportMenuServices() = default; @@ -180,6 +208,32 @@ public: return DocumentExportMenuAction::show_jpeg_dialog; } +[[nodiscard]] constexpr DocumentExportSuccessDestination document_export_equirectangular_platform_destination() noexcept +{ +#if defined(__IOS__) + return DocumentExportSuccessDestination::photos; +#elif defined(__OSX__) + return DocumentExportSuccessDestination::pictures_panopainter; +#elif defined(_WIN32) + return DocumentExportSuccessDestination::work_directory; +#else + return DocumentExportSuccessDestination::suppressed; +#endif +} + +[[nodiscard]] constexpr DocumentExportSuccessDestination document_export_media_platform_destination() noexcept +{ +#if defined(__IOS__) + return DocumentExportSuccessDestination::files_panopainter; +#elif defined(__OSX__) + return DocumentExportSuccessDestination::pictures_panopainter; +#elif defined(_WIN32) + return DocumentExportSuccessDestination::work_directory; +#else + return DocumentExportSuccessDestination::suppressed; +#endif +} + [[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action( DocumentExportMenuKind kind, bool has_canvas, @@ -230,6 +284,99 @@ public: }; } +[[nodiscard]] inline DocumentExportSuccessDialogPlan plan_document_export_success_dialog( + DocumentExportSuccessKind kind, + DocumentExportSuccessDestination destination, + std::string_view detail = {}) +{ + DocumentExportSuccessDialogPlan plan; + plan.kind = kind; + plan.destination = destination; + if (destination == DocumentExportSuccessDestination::suppressed) { + return plan; + } + + std::string message; + switch (kind) { + case DocumentExportSuccessKind::equirectangular: + plan.dialog.title = "Export Equirectangular"; + switch (destination) { + case DocumentExportSuccessDestination::photos: + message = "Image exported to Photos"; + break; + case DocumentExportSuccessDestination::pictures_panopainter: + message = "Image exported to Pictures/PanoPainter folder"; + break; + case DocumentExportSuccessDestination::work_directory: + message = "Image exported to "; + message += detail; + break; + case DocumentExportSuccessDestination::suppressed: + case DocumentExportSuccessDestination::files_panopainter: + case DocumentExportSuccessDestination::path: + case DocumentExportSuccessDestination::generic_success: + break; + } + break; + case DocumentExportSuccessKind::layers: + case DocumentExportSuccessKind::animation_frames: + plan.dialog.title = "Export Layers"; + if (destination == DocumentExportSuccessDestination::files_panopainter) { + message = "Image layers exported to Files/PanoPainter"; + } else if (destination == DocumentExportSuccessDestination::path) { + message = "Layers exported to: "; + message += detail; + } + break; + case DocumentExportSuccessKind::depth: + plan.dialog.title = "Export 3D View + Depth"; + if (destination == DocumentExportSuccessDestination::files_panopainter) { + message = "Image and depth exported to Files/PanoPainter"; + } else if (destination == DocumentExportSuccessDestination::pictures_panopainter) { + message = "Image and depth exported to Pictures/PanoPainter folder"; + } else if (destination == DocumentExportSuccessDestination::work_directory) { + message = "Image and depth exported to "; + message += detail; + } + break; + case DocumentExportSuccessKind::cube_faces: + plan.dialog.title = "Export Cube Faces"; + if (destination == DocumentExportSuccessDestination::files_panopainter) { + message = "Image and depth exported to Files/PanoPainter"; + } else if (destination == DocumentExportSuccessDestination::pictures_panopainter) { + message = "Image and depth exported to Pictures/PanoPainter folder"; + } else if (destination == DocumentExportSuccessDestination::work_directory) { + message = "Image and depth exported to "; + message += detail; + } + break; + case DocumentExportSuccessKind::animation_mp4: + plan.dialog.title = "Export Animation"; + if (destination == DocumentExportSuccessDestination::path) { + message = "Animation exported to: "; + message += detail; + } else if (destination == DocumentExportSuccessDestination::generic_success) { + message = "Animation exported successfully."; + } + break; + case DocumentExportSuccessKind::timelapse: + plan.dialog.title = "Export Timelapse"; + if (destination == DocumentExportSuccessDestination::path) { + message = "Timelapse exported to: "; + message += detail; + } else if (destination == DocumentExportSuccessDestination::generic_success) { + message = "Timelapse exported successfully."; + } + break; + } + + if (!plan.dialog.title.empty() && !message.empty()) { + plan.dialog.message = std::move(message); + plan.show_dialog = true; + } + return plan; +} + [[nodiscard]] inline pp::foundation::Result make_document_export_file_target( std::string_view work_directory, std::string_view document_name, diff --git a/src/legacy_document_export_services.cpp b/src/legacy_document_export_services.cpp index 6baa088..6b70fd6 100644 --- a/src/legacy_document_export_services.cpp +++ b/src/legacy_document_export_services.cpp @@ -10,6 +10,15 @@ namespace pp::panopainter { namespace { +void show_export_success_dialog( + App& app, + const pp::app::DocumentExportSuccessDialogPlan& plan) +{ + if (plan.show_dialog) { + app.message_box(plan.dialog.title, plan.dialog.message, plan.dialog.show_cancel); + } +} + class LegacyDocumentExportServices final : public pp::app::DocumentExportServices { public: explicit LegacyDocumentExportServices(App& app) noexcept @@ -26,20 +35,18 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_equirectangular(target.path, [app, target] { -#if defined(__IOS__) - app->message_box("Export Equirectangular", "Image exported to Photos"); -#elif defined(__OSX__) - app->message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - app->message_box("Export Equirectangular", "Image exported to " + app->work_path); -#elif defined(__QUEST__) - (void)target; -#elif __WEB__ +#if __WEB__ app->ui_task([app, target] { app->save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { }); }); #else (void)target; + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::equirectangular, + pp::app::document_export_equirectangular_platform_destination(), + app->work_path)); #endif }); } @@ -48,7 +55,12 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_layers(target.stem_path, [app, target] { - app->message_box("Export Layers", "Layers exported to: " + target.stem_path); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::layers, + pp::app::DocumentExportSuccessDestination::path, + target.stem_path)); }); } @@ -56,7 +68,11 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_layers(target.stem_path, [app] { - app->message_box("Export Layers", "Image layers exported to Files/PanoPainter"); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::layers, + pp::app::DocumentExportSuccessDestination::files_panopainter)); }); } @@ -64,7 +80,12 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app, target] { - app->message_box("Export Layers", "Layers exported to: " + target.stem_path); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_frames, + pp::app::DocumentExportSuccessDestination::path, + target.stem_path)); }); } @@ -72,7 +93,11 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app] { - app->message_box("Export Layers", "Image layers exported to Files/PanoPainter"); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_frames, + pp::app::DocumentExportSuccessDestination::files_panopainter)); }); } @@ -80,13 +105,12 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_depth(std::string(document_name), [app] { -#if defined(__IOS__) - app->message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter"); -#elif defined(__OSX__) - app->message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - app->message_box("Export 3D View + Depth", "Image and depth exported to " + app->work_path); -#endif + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::document_export_media_platform_destination(), + app->work_path)); }); } @@ -94,13 +118,12 @@ public: { auto* app = &app_; app_.canvas->m_canvas->export_cube_faces(std::string(document_name), [app] { -#if defined(__IOS__) - app->message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter"); -#elif defined(__OSX__) - app->message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - app->message_box("Export Cube Faces", "Image and depth exported to " + app->work_path); -#endif + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::cube_faces, + pp::app::document_export_media_platform_destination(), + app->work_path)); }); } @@ -122,13 +145,22 @@ public: auto path_string = std::string(path); if (asynchronous_) { Canvas::I->export_anim_mp4(path_string, [app, path_string] { - app->message_box("Export Animation", "Animation exported to: " + path_string); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_mp4, + pp::app::DocumentExportSuccessDestination::path, + path_string)); }); return; } Canvas::I->export_anim_mp4(path_string, [app] { - app->message_box("Export Animation", "Animation exported successfully."); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_mp4, + pp::app::DocumentExportSuccessDestination::generic_success)); }); } @@ -140,7 +172,12 @@ public: std::thread([app, path_string] { BT_SetTerminate(); app->rec_export(path_string); - app->message_box("Export Timelapse", "Timelapse exported to: " + path_string); + show_export_success_dialog( + *app, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::timelapse, + pp::app::DocumentExportSuccessDestination::path, + path_string)); }).detach(); return; } @@ -158,7 +195,12 @@ public: if (asynchronous_) { (void)path; } else { - app_.message_box("Export Timelapse", "Timelapse exported successfully."); + (void)path; + show_export_success_dialog( + app_, + pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::timelapse, + pp::app::DocumentExportSuccessDestination::generic_success)); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 400b71b..b4a4092 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -899,6 +899,37 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-target\".*\"kind\":\"name\".*\"suggestedName\":\"demo-timelapse\"") + add_test(NAME pano_cli_plan_export_message_equirect_work_smoke + COMMAND pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint) + set_tests_properties(pano_cli_plan_export_message_equirect_work_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-message\".*\"kind\":\"equirectangular\".*\"destination\":\"work-directory\".*\"showDialog\":true.*\"title\":\"Export Equirectangular\".*\"message\":\"Image exported to D:/Paint\"") + + add_test(NAME pano_cli_plan_export_message_layers_files_smoke + COMMAND pano_cli plan-export-message --kind layers --destination files) + set_tests_properties(pano_cli_plan_export_message_layers_files_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-message\".*\"kind\":\"layers\".*\"destination\":\"files-panopainter\".*\"showDialog\":true.*\"message\":\"Image layers exported to Files/PanoPainter\"") + + add_test(NAME pano_cli_plan_export_message_timelapse_success_smoke + COMMAND pano_cli plan-export-message --kind timelapse --destination success) + set_tests_properties(pano_cli_plan_export_message_timelapse_success_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-message\".*\"kind\":\"timelapse\".*\"destination\":\"generic-success\".*\"showDialog\":true.*\"message\":\"Timelapse exported successfully\\.\"") + + add_test(NAME pano_cli_plan_export_message_suppressed_smoke + COMMAND pano_cli plan-export-message --kind depth --destination suppressed) + set_tests_properties(pano_cli_plan_export_message_suppressed_smoke PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-message\".*\"kind\":\"depth\".*\"destination\":\"suppressed\".*\"showDialog\":false.*\"title\":\"\".*\"message\":\"\"") + + add_test(NAME pano_cli_plan_export_message_rejects_unknown + COMMAND pano_cli plan-export-message --kind nope) + set_tests_properties(pano_cli_plan_export_message_rejects_unknown PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-message\".*\"message\":\"unknown export message kind\"") + add_test(NAME pano_cli_plan_cloud_upload_clean_smoke COMMAND pano_cli plan-cloud-upload) set_tests_properties(pano_cli_plan_cloud_upload_clean_smoke PROPERTIES diff --git a/tests/app_core/document_export_tests.cpp b/tests/app_core/document_export_tests.cpp index 1131492..cae9952 100644 --- a/tests/app_core/document_export_tests.cpp +++ b/tests/app_core/document_export_tests.cpp @@ -218,6 +218,115 @@ void collection_export_target_plan_selects_platform_destination(pp::tests::Harne PP_EXPECT(harness, picked_layers.suffix == "_layers"); } +void export_success_dialog_plans_image_destinations(pp::tests::Harness& harness) +{ + const auto photos = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::equirectangular, + pp::app::DocumentExportSuccessDestination::photos); + const auto pictures = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::equirectangular, + pp::app::DocumentExportSuccessDestination::pictures_panopainter); + const auto work = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::equirectangular, + pp::app::DocumentExportSuccessDestination::work_directory, + "D:/Paint"); + + PP_EXPECT(harness, photos.show_dialog); + PP_EXPECT(harness, photos.dialog.title == "Export Equirectangular"); + PP_EXPECT(harness, photos.dialog.message == "Image exported to Photos"); + PP_EXPECT(harness, pictures.dialog.message == "Image exported to Pictures/PanoPainter folder"); + PP_EXPECT(harness, work.dialog.message == "Image exported to D:/Paint"); +} + +void export_success_dialog_plans_collection_destinations(pp::tests::Harness& harness) +{ + const auto layers_path = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::layers, + pp::app::DocumentExportSuccessDestination::path, + "D:/Paint/demo_layers/demo"); + const auto frames_path = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_frames, + pp::app::DocumentExportSuccessDestination::path, + "D:/Paint/demo_frames/demo"); + const auto layers_collection = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::layers, + pp::app::DocumentExportSuccessDestination::files_panopainter); + const auto frames_collection = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_frames, + pp::app::DocumentExportSuccessDestination::files_panopainter); + + PP_EXPECT(harness, layers_path.dialog.title == "Export Layers"); + PP_EXPECT(harness, layers_path.dialog.message == "Layers exported to: D:/Paint/demo_layers/demo"); + PP_EXPECT(harness, frames_path.dialog.message == "Layers exported to: D:/Paint/demo_frames/demo"); + PP_EXPECT(harness, layers_collection.dialog.message == "Image layers exported to Files/PanoPainter"); + PP_EXPECT(harness, frames_collection.dialog.message == "Image layers exported to Files/PanoPainter"); +} + +void export_success_dialog_plans_depth_and_cube_destinations(pp::tests::Harness& harness) +{ + const auto depth_files = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::DocumentExportSuccessDestination::files_panopainter); + const auto depth_pictures = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::DocumentExportSuccessDestination::pictures_panopainter); + const auto depth_work = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::DocumentExportSuccessDestination::work_directory, + "D:/Paint"); + const auto cube_work = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::cube_faces, + pp::app::DocumentExportSuccessDestination::work_directory, + "D:/Paint"); + + PP_EXPECT(harness, depth_files.dialog.title == "Export 3D View + Depth"); + PP_EXPECT(harness, depth_files.dialog.message == "Image and depth exported to Files/PanoPainter"); + PP_EXPECT(harness, depth_pictures.dialog.message == "Image and depth exported to Pictures/PanoPainter folder"); + PP_EXPECT(harness, depth_work.dialog.message == "Image and depth exported to D:/Paint"); + PP_EXPECT(harness, cube_work.dialog.title == "Export Cube Faces"); + PP_EXPECT(harness, cube_work.dialog.message == "Image and depth exported to D:/Paint"); +} + +void export_success_dialog_plans_video_destinations(pp::tests::Harness& harness) +{ + const auto animation_path = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_mp4, + pp::app::DocumentExportSuccessDestination::path, + "D:/Paint/animation.mp4"); + const auto animation_success = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::animation_mp4, + pp::app::DocumentExportSuccessDestination::generic_success); + const auto timelapse_path = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::timelapse, + pp::app::DocumentExportSuccessDestination::path, + "D:/Paint/timelapse.mp4"); + const auto timelapse_success = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::timelapse, + pp::app::DocumentExportSuccessDestination::generic_success); + + PP_EXPECT(harness, animation_path.dialog.title == "Export Animation"); + PP_EXPECT(harness, animation_path.dialog.message == "Animation exported to: D:/Paint/animation.mp4"); + PP_EXPECT(harness, animation_success.dialog.message == "Animation exported successfully."); + PP_EXPECT(harness, timelapse_path.dialog.title == "Export Timelapse"); + PP_EXPECT(harness, timelapse_path.dialog.message == "Timelapse exported to: D:/Paint/timelapse.mp4"); + PP_EXPECT(harness, timelapse_success.dialog.message == "Timelapse exported successfully."); +} + +void export_success_dialog_suppresses_unsupported_destinations(pp::tests::Harness& harness) +{ + const auto suppressed = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::depth, + pp::app::DocumentExportSuccessDestination::suppressed); + const auto invalid_combo = pp::app::plan_document_export_success_dialog( + pp::app::DocumentExportSuccessKind::equirectangular, + pp::app::DocumentExportSuccessDestination::files_panopainter); + + PP_EXPECT(harness, !suppressed.show_dialog); + PP_EXPECT(harness, suppressed.dialog.title.empty()); + PP_EXPECT(harness, !invalid_combo.show_dialog); + PP_EXPECT(harness, invalid_combo.dialog.message.empty()); +} + void export_start_allows_valid_canvas_state(pp::tests::Harness& harness) { PP_EXPECT( @@ -552,6 +661,11 @@ int main() harness.run("picked directory export builds stem", picked_directory_export_builds_stem); 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("export success dialog plans image destinations", export_success_dialog_plans_image_destinations); + harness.run("export success dialog plans collection destinations", export_success_dialog_plans_collection_destinations); + harness.run("export success dialog plans depth and cube destinations", export_success_dialog_plans_depth_and_cube_destinations); + harness.run("export success dialog plans video destinations", export_success_dialog_plans_video_destinations); + harness.run("export success dialog suppresses unsupported destinations", export_success_dialog_suppresses_unsupported_destinations); harness.run("export start allows valid canvas state", export_start_allows_valid_canvas_state); harness.run("export start blocks demo only when license required", export_start_blocks_demo_only_when_license_required); harness.run("export start reports missing canvas after license gate", export_start_reports_missing_canvas_after_license_gate); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 0ba33af..863f103 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -168,6 +168,12 @@ struct PlanExportTargetArgs { std::string suffix; }; +struct PlanExportMessageArgs { + std::string kind = "equirectangular"; + std::string destination = "work"; + std::string detail = "D:/Paint"; +}; + struct PlanExportStartArgs { bool requires_license = false; bool license_valid = true; @@ -1943,6 +1949,123 @@ pp::foundation::Result parse_document_export_me pp::foundation::Status::invalid_argument("unknown export menu kind")); } +const char* document_export_success_kind_name(pp::app::DocumentExportSuccessKind kind) noexcept +{ + switch (kind) { + case pp::app::DocumentExportSuccessKind::equirectangular: + return "equirectangular"; + case pp::app::DocumentExportSuccessKind::layers: + return "layers"; + case pp::app::DocumentExportSuccessKind::animation_frames: + return "animation-frames"; + case pp::app::DocumentExportSuccessKind::depth: + return "depth"; + case pp::app::DocumentExportSuccessKind::cube_faces: + return "cube-faces"; + case pp::app::DocumentExportSuccessKind::animation_mp4: + return "animation-mp4"; + case pp::app::DocumentExportSuccessKind::timelapse: + return "timelapse"; + } + + return "equirectangular"; +} + +const char* document_export_success_destination_name( + pp::app::DocumentExportSuccessDestination destination) noexcept +{ + switch (destination) { + case pp::app::DocumentExportSuccessDestination::suppressed: + return "suppressed"; + case pp::app::DocumentExportSuccessDestination::photos: + return "photos"; + case pp::app::DocumentExportSuccessDestination::pictures_panopainter: + return "pictures-panopainter"; + case pp::app::DocumentExportSuccessDestination::files_panopainter: + return "files-panopainter"; + case pp::app::DocumentExportSuccessDestination::work_directory: + return "work-directory"; + case pp::app::DocumentExportSuccessDestination::path: + return "path"; + case pp::app::DocumentExportSuccessDestination::generic_success: + return "generic-success"; + } + + return "suppressed"; +} + +pp::foundation::Result parse_document_export_success_kind( + std::string_view kind) +{ + if (kind == "equirectangular" || kind == "equirect" || kind == "image") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::equirectangular); + } + if (kind == "layers") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::layers); + } + if (kind == "animation-frames" || kind == "frames") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::animation_frames); + } + if (kind == "depth") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::depth); + } + if (kind == "cube-faces" || kind == "cube") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::cube_faces); + } + if (kind == "animation-mp4" || kind == "mp4") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::animation_mp4); + } + if (kind == "timelapse") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessKind::timelapse); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown export message kind")); +} + +pp::foundation::Result parse_document_export_success_destination( + std::string_view destination) +{ + if (destination == "suppressed" || destination == "none") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::suppressed); + } + if (destination == "photos") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::photos); + } + if (destination == "pictures" || destination == "pictures-panopainter") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::pictures_panopainter); + } + if (destination == "files" || destination == "files-panopainter") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::files_panopainter); + } + if (destination == "work" || destination == "work-directory") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::work_directory); + } + if (destination == "path") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::path); + } + if (destination == "success" || destination == "generic-success") { + return pp::foundation::Result::success( + pp::app::DocumentExportSuccessDestination::generic_success); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown export message destination")); +} + const char* cloud_upload_action_name(pp::app::CloudUploadAction action) noexcept { switch (action) { @@ -2234,6 +2357,7 @@ void print_help() << " plan-export-start [--requires-license] [--demo] [--no-canvas]\n" << " plan-export-menu --kind jpeg|png|layers|cube-faces|depth|animation-frames|animation-mp4|timelapse [--demo] [--no-canvas]\n" << " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n" + << " plan-export-message --kind equirectangular|layers|animation-frames|depth|cube-faces|animation-mp4|timelapse --destination photos|pictures|files|work|path|success|suppressed [--detail TEXT]\n" << " plan-cloud-upload [--no-canvas] [--new-document] [--unsaved]\n" << " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n" << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" @@ -3588,6 +3712,74 @@ int plan_export_menu(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_export_message_args( + int argc, + char** argv, + PlanExportMessageArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.kind = argv[++i]; + } else if (key == "--destination") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.destination = argv[++i]; + } else if (key == "--detail") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.detail = argv[++i]; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_export_message(int argc, char** argv) +{ + PlanExportMessageArgs args; + const auto status = parse_plan_export_message_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-export-message", status.message); + return 2; + } + + const auto kind = parse_document_export_success_kind(args.kind); + if (!kind) { + print_error("plan-export-message", kind.status().message); + return 2; + } + + const auto destination = parse_document_export_success_destination(args.destination); + if (!destination) { + print_error("plan-export-message", destination.status().message); + return 2; + } + + const auto plan = pp::app::plan_document_export_success_dialog( + kind.value(), + destination.value(), + args.detail); + std::cout << "{\"ok\":true,\"command\":\"plan-export-message\"" + << ",\"state\":{\"kind\":\"" << json_escape(args.kind) + << "\",\"destination\":\"" << json_escape(args.destination) + << "\",\"detail\":\"" << json_escape(args.detail) + << "\"},\"plan\":{\"kind\":\"" << document_export_success_kind_name(plan.kind) + << "\",\"destination\":\"" << document_export_success_destination_name(plan.destination) + << "\",\"showDialog\":" << json_bool(plan.show_dialog) + << ",\"title\":\"" << json_escape(plan.dialog.title) + << "\",\"message\":\"" << json_escape(plan.dialog.message) + << "\"}}\n"; + return 0; +} + pp::foundation::Status parse_plan_cloud_upload_args( int argc, char** argv, @@ -11187,6 +11379,10 @@ int main(int argc, char** argv) return plan_export_target(argc, argv); } + if (command == "plan-export-message") { + return plan_export_message(argc, argv); + } + if (command == "plan-cloud-upload") { return plan_cloud_upload(argc, argv); }