From 78003923ca9d793bb3cf74191b47516daaf385bb Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 13:57:32 +0200 Subject: [PATCH] Centralize legacy document image exports --- cmake/PanoPainterSources.cmake | 2 + docs/modernization/build-inventory.md | 10 +- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 16 ++ src/app_core/document_export.h | 101 +++++++++++++ src/app_dialogs.cpp | 86 +++++------ src/legacy_document_export_services.cpp | 152 +++++++++++++++++++ src/legacy_document_export_services.h | 32 ++++ tests/app_core/document_export_tests.cpp | 183 +++++++++++++++++++++++ 9 files changed, 530 insertions(+), 53 deletions(-) create mode 100644 src/legacy_document_export_services.cpp create mode 100644 src/legacy_document_export_services.h diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index 8aa5eb1..24d509f 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -84,6 +84,8 @@ set(PP_PANOPAINTER_APP_SOURCES src/app_vr.cpp src/legacy_cloud_services.cpp src/legacy_cloud_services.h + src/legacy_document_export_services.cpp + src/legacy_document_export_services.h src/legacy_document_open_services.cpp src/legacy_document_open_services.h src/legacy_document_session_services.cpp diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 70abc15..253543f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -561,7 +561,9 @@ Known local toolchain state: - `pp_app_core_document_export_tests` covers export file targets, collection directory/stem targets, picked-directory stems, MP4 suggested names, and invalid export naming inputs, plus export-start license/canvas availability - decisions. + decisions, export menu executor dispatch, file/stem/collection export + execution dispatch, failed directory creation preservation, named depth/cube + export dispatch, and malformed export target rejection. - `pp_app_core_document_recording_tests` covers recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, and oversized progress-total clamping. @@ -605,6 +607,12 @@ Known local toolchain state: overwrite prompts, document field updates, title updates, and keyboard/dialog cleanup. Retained legacy UI/canvas execution remains tracked by `DEBT-0040`, `DEBT-0041`, and `DEBT-0042`. +- `src/legacy_document_export_services.*` is the current app-shell bridge + between `pp_app_core` document export execution plans and live equirectangular, + layers, animation-frame, depth, and cube-face export calls. It preserves + platform-specific export messages, directory creation, picker-selected stem + exports, Web prepared-file handoff, and legacy `Canvas` export execution while + retained renderer/document/platform ownership is tracked by `DEBT-0043`. - `src/legacy_history_services.*` is the current app-shell bridge between `pp_app_core` history plans and legacy `ActionManager`; toolbar and `NodeCanvas` hotkeys share it while document-history extraction remains diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 140353b..02ae38d 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -60,6 +60,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch 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`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy message boxes/save dialogs, 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`; `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 now consumes pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `NewDocumentServices`, and `src/legacy_document_session_services.*`, 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, 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`; `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 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 now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy overwrite prompts, calls legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters | +| DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`, 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`; `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, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index d803361..b383259 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -750,6 +750,14 @@ Save Version plans through app-core document file/version save executors and legacy `Canvas::project_save`, app document field updates, title updates, and keyboard/dialog cleanup while retained execution remains tracked under `DEBT-0042`. +`App::dialog_export`, `App::dialog_export_layers`, +`App::dialog_export_anim_frames`, `App::dialog_export_depth`, and +`App::dialog_export_cube_faces` now route accepted file/stem/collection and +named export work through app-core document export executors and +`src/legacy_document_export_services.*`, preserving existing platform messages, +directory creation, picker-selected stems, Web prepared-file handoff, and legacy +`Canvas` export calls while retained execution remains tracked under +`DEBT-0043`. Implementation tasks: @@ -1314,6 +1322,14 @@ Results: `pp_app_core_document_session_tests`, `pano_cli_plan_document_file_*`, `pano_cli_plan_document_version_*`, and `pano_cli_simulate_app_session_*` smoke tests after the live bridge split. +- `PanoPainter`, `pp_app_core_document_export_tests`, and `pano_cli` built + after equirectangular, layers, animation-frame, depth, and cube-face export + execution moved behind document export services. A clean rebuild was required + once because MSVC reported the known Debug PDB `LNK1103` corruption, after + which the build passed. +- Focused export CTest coverage passed for `pp_app_core_document_export_tests`, + `pano_cli_plan_export_start/menu/target_*`, and + `pano_cli_simulate_document_export_smoke` after the live bridge split. - `pp_app_core_document_recording_tests` passed, covering recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, and oversized progress-total clamping. diff --git a/src/app_core/document_export.h b/src/app_core/document_export.h index e0b9dc1..961321a 100644 --- a/src/app_core/document_export.h +++ b/src/app_core/document_export.h @@ -56,6 +56,11 @@ enum class DocumentExportMenuAction { unavailable_no_canvas, }; +enum class DocumentExportCollectionKind { + layers, + animation_frames, +}; + struct DocumentExportMenuPlan { DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg; DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog; @@ -77,6 +82,20 @@ public: virtual void show_license_disabled() = 0; }; +class DocumentExportServices { +public: + virtual ~DocumentExportServices() = default; + + virtual bool create_directory(std::string_view directory) = 0; + virtual void export_equirectangular(const DocumentExportFileTarget& target) = 0; + virtual void export_layers_to_stem(const DocumentExportStemTarget& target) = 0; + virtual void export_layers_to_collection(const DocumentExportCollectionTarget& target) = 0; + virtual void export_animation_frames_to_stem(const DocumentExportStemTarget& target) = 0; + virtual void export_animation_frames_to_collection(const DocumentExportCollectionTarget& target) = 0; + virtual void export_depth(std::string_view document_name) = 0; + virtual void export_cube_faces(std::string_view document_name) = 0; +}; + [[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start( bool requires_license, bool license_valid, @@ -241,6 +260,88 @@ public: return pp::foundation::Result::success(std::move(target)); } +[[nodiscard]] inline pp::foundation::Status execute_document_export_file( + const DocumentExportFileTarget& target, + DocumentExportServices& services) +{ + if (target.path.empty() || target.suggested_name.empty()) { + return pp::foundation::Status::invalid_argument("export file target requires a path and suggested name"); + } + + services.export_equirectangular(target); + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_document_export_stem( + DocumentExportCollectionKind kind, + const DocumentExportStemTarget& target, + DocumentExportServices& services) +{ + if (target.stem_path.empty()) { + return pp::foundation::Status::invalid_argument("export stem target requires a stem path"); + } + + switch (kind) { + case DocumentExportCollectionKind::layers: + services.export_layers_to_stem(target); + return pp::foundation::Status::success(); + case DocumentExportCollectionKind::animation_frames: + services.export_animation_frames_to_stem(target); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown document export collection kind"); +} + +[[nodiscard]] inline pp::foundation::Status execute_document_export_collection( + DocumentExportCollectionKind kind, + const DocumentExportCollectionTarget& target, + DocumentExportServices& services) +{ + if (target.directory.empty() || target.stem_path.empty()) { + return pp::foundation::Status::invalid_argument("export collection target requires a directory and stem path"); + } + + if (!services.create_directory(target.directory)) { + return pp::foundation::Status::success(); + } + + switch (kind) { + case DocumentExportCollectionKind::layers: + services.export_layers_to_collection(target); + return pp::foundation::Status::success(); + case DocumentExportCollectionKind::animation_frames: + services.export_animation_frames_to_collection(target); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown document export collection kind"); +} + +[[nodiscard]] inline pp::foundation::Status execute_document_export_depth( + std::string_view document_name, + DocumentExportServices& services) +{ + if (document_name.empty()) { + return pp::foundation::Status::invalid_argument("document name must not be empty"); + } + + services.export_depth(document_name); + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_document_export_cube_faces( + std::string_view document_name, + DocumentExportServices& services) +{ + if (document_name.empty()) { + return pp::foundation::Status::invalid_argument("document name must not be empty"); + } + + services.export_cube_faces(document_name); + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan( const DocumentExportMenuPlan& plan, DocumentExportMenuServices& services) diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index d20ede2..76342a2 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -5,6 +5,7 @@ #include "app_core/document_export.h" #include "app_core/document_session.h" #include "legacy_document_canvas_services.h" +#include "legacy_document_export_services.h" #include "legacy_document_layer_services.h" #include "legacy_document_session_services.h" #include "settings.h" @@ -356,21 +357,9 @@ void App::dialog_export(std::string ext) return; } - canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{ -#if defined(__IOS__) - message_box("Export Equirectangular", "Image exported to Photos"); -#elif defined(__OSX__) - message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - message_box("Export Equirectangular", "Image exported to " + work_path); -#elif defined(__QUEST__) - //auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo); -#elif __WEB__ - ui_task([=]{ - save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { }); - }); -#endif - }); + const auto status = pp::panopainter::execute_legacy_document_export_file(*this, target.value()); + if (!status.ok()) + LOG("Document export file action failed: %s", status.message); } void App::dialog_export_layers() @@ -385,12 +374,12 @@ void App::dialog_export_layers() return; } - if (Asset::create_dir(target.value().directory)) - { - canvas->m_canvas->export_layers(target.value().stem_path, [this] { - message_box("Export Layers", "Image layers exported to Files/PanoPainter"); - }); - } + const auto status = pp::panopainter::execute_legacy_document_export_collection( + *this, + pp::app::DocumentExportCollectionKind::layers, + target.value()); + if (!status.ok()) + LOG("Document layer collection export failed: %s", status.message); #else pick_dir([this](std::string path) { const auto target = pp::app::make_document_export_stem_target(path, doc_name); @@ -399,9 +388,12 @@ void App::dialog_export_layers() return; } - canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] { - message_box("Export Layers", "Layers exported to: " + target.stem_path); - }); + const auto status = pp::panopainter::execute_legacy_document_export_stem( + *this, + pp::app::DocumentExportCollectionKind::layers, + target.value()); + if (!status.ok()) + LOG("Document layer stem export failed: %s", status.message); }); #endif } @@ -418,12 +410,12 @@ void App::dialog_export_anim_frames() return; } - if (Asset::create_dir(target.value().directory)) - { - canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] { - message_box("Export Layers", "Image layers exported to Files/PanoPainter"); - }); - } + const auto status = pp::panopainter::execute_legacy_document_export_collection( + *this, + pp::app::DocumentExportCollectionKind::animation_frames, + target.value()); + if (!status.ok()) + LOG("Document animation frame collection export failed: %s", status.message); #else pick_dir([this](std::string path) { const auto target = pp::app::make_document_export_stem_target(path, doc_name); @@ -432,9 +424,12 @@ void App::dialog_export_anim_frames() return; } - canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] { - message_box("Export Layers", "Layers exported to: " + target.stem_path); - }); + const auto status = pp::panopainter::execute_legacy_document_export_stem( + *this, + pp::app::DocumentExportCollectionKind::animation_frames, + target.value()); + if (!status.ok()) + LOG("Document animation frame stem export failed: %s", status.message); }); #endif } @@ -444,16 +439,9 @@ void App::dialog_export_depth() if (!can_start_document_export(*this, true)) return; - // TODO: use picker - canvas->m_canvas->export_depth(doc_name, [this] { -#if defined(__IOS__) - message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter"); -#elif defined(__OSX__) - message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - message_box("Export 3D View + Depth", "Image and depth exported to " + work_path); -#endif - }); + const auto status = pp::panopainter::execute_legacy_document_export_depth(*this, doc_name); + if (!status.ok()) + LOG("Document depth export failed: %s", status.message); } void App::dialog_resize() @@ -487,15 +475,9 @@ void App::dialog_export_cube_faces() if (!can_start_document_export(*this, false)) return; - canvas->m_canvas->export_cube_faces(doc_name, [this] { -#if defined(__IOS__) - message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter"); -#elif defined(__OSX__) - message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder"); -#elif defined(_WIN32) - message_box("Export Cube Faces", "Image and depth exported to " + work_path); -#endif - }); + const auto status = pp::panopainter::execute_legacy_document_export_cube_faces(*this, doc_name); + if (!status.ok()) + LOG("Document cube-face export failed: %s", status.message); } void App::dialog_layer_rename() diff --git a/src/legacy_document_export_services.cpp b/src/legacy_document_export_services.cpp new file mode 100644 index 0000000..e21dad6 --- /dev/null +++ b/src/legacy_document_export_services.cpp @@ -0,0 +1,152 @@ +#include "pch.h" + +#include "legacy_document_export_services.h" + +#include "app.h" + +namespace pp::panopainter { +namespace { + +class LegacyDocumentExportServices final : public pp::app::DocumentExportServices { +public: + explicit LegacyDocumentExportServices(App& app) noexcept + : app_(app) + { + } + + bool create_directory(std::string_view directory) override + { + return Asset::create_dir(std::string(directory)); + } + + void export_equirectangular(const pp::app::DocumentExportFileTarget& target) override + { + 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__ + app->ui_task([app, target] { + app->save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { }); + }); +#else + (void)target; +#endif + }); + } + + void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override + { + 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); + }); + } + + void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override + { + auto* app = &app_; + app_.canvas->m_canvas->export_layers(target.stem_path, [app] { + app->message_box("Export Layers", "Image layers exported to Files/PanoPainter"); + }); + } + + void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override + { + 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); + }); + } + + void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override + { + 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"); + }); + } + + void export_depth(std::string_view document_name) override + { + 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 + }); + } + + void export_cube_faces(std::string_view document_name) override + { + 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 + }); + } + +private: + App& app_; +}; + +} // namespace + +pp::foundation::Status execute_legacy_document_export_file( + App& app, + const pp::app::DocumentExportFileTarget& target) +{ + LegacyDocumentExportServices services(app); + return pp::app::execute_document_export_file(target, services); +} + +pp::foundation::Status execute_legacy_document_export_stem( + App& app, + pp::app::DocumentExportCollectionKind kind, + const pp::app::DocumentExportStemTarget& target) +{ + LegacyDocumentExportServices services(app); + return pp::app::execute_document_export_stem(kind, target, services); +} + +pp::foundation::Status execute_legacy_document_export_collection( + App& app, + pp::app::DocumentExportCollectionKind kind, + const pp::app::DocumentExportCollectionTarget& target) +{ + LegacyDocumentExportServices services(app); + return pp::app::execute_document_export_collection(kind, target, services); +} + +pp::foundation::Status execute_legacy_document_export_depth( + App& app, + std::string_view document_name) +{ + LegacyDocumentExportServices services(app); + return pp::app::execute_document_export_depth(document_name, services); +} + +pp::foundation::Status execute_legacy_document_export_cube_faces( + App& app, + std::string_view document_name) +{ + LegacyDocumentExportServices services(app); + return pp::app::execute_document_export_cube_faces(document_name, services); +} + +} // namespace pp::panopainter diff --git a/src/legacy_document_export_services.h b/src/legacy_document_export_services.h new file mode 100644 index 0000000..d7619a3 --- /dev/null +++ b/src/legacy_document_export_services.h @@ -0,0 +1,32 @@ +#pragma once + +#include "app_core/document_export.h" +#include "foundation/result.h" + +class App; + +namespace pp::panopainter { + +[[nodiscard]] pp::foundation::Status execute_legacy_document_export_file( + App& app, + const pp::app::DocumentExportFileTarget& target); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_export_stem( + App& app, + pp::app::DocumentExportCollectionKind kind, + const pp::app::DocumentExportStemTarget& target); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_export_collection( + App& app, + pp::app::DocumentExportCollectionKind kind, + const pp::app::DocumentExportCollectionTarget& target); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_export_depth( + App& app, + std::string_view document_name); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_export_cube_faces( + App& app, + std::string_view document_name); + +} // namespace pp::panopainter diff --git a/tests/app_core/document_export_tests.cpp b/tests/app_core/document_export_tests.cpp index 8a33eef..effe01f 100644 --- a/tests/app_core/document_export_tests.cpp +++ b/tests/app_core/document_export_tests.cpp @@ -1,6 +1,9 @@ #include "app_core/document_export.h" #include "test_harness.h" +#include +#include + namespace { class FakeDocumentExportMenuServices final : public pp::app::DocumentExportMenuServices { @@ -33,6 +36,80 @@ public: int license_messages = 0; }; +class FakeDocumentExportServices final : public pp::app::DocumentExportServices { +public: + bool create_directory(std::string_view directory) override + { + create_dir_calls += 1; + last_directory = std::string(directory); + return create_directory_result; + } + + void export_equirectangular(const pp::app::DocumentExportFileTarget& target) override + { + equirectangular_exports += 1; + last_path = target.path; + call_order += "equirect;"; + } + + void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override + { + layer_stem_exports += 1; + last_stem = target.stem_path; + call_order += "layers-stem;"; + } + + void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override + { + layer_collection_exports += 1; + last_stem = target.stem_path; + call_order += "layers-collection;"; + } + + void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override + { + frame_stem_exports += 1; + last_stem = target.stem_path; + call_order += "frames-stem;"; + } + + void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override + { + frame_collection_exports += 1; + last_stem = target.stem_path; + call_order += "frames-collection;"; + } + + void export_depth(std::string_view document_name) override + { + depth_exports += 1; + last_name = std::string(document_name); + call_order += "depth;"; + } + + void export_cube_faces(std::string_view document_name) override + { + cube_exports += 1; + last_name = std::string(document_name); + call_order += "cube;"; + } + + bool create_directory_result = true; + int create_dir_calls = 0; + int equirectangular_exports = 0; + int layer_stem_exports = 0; + int layer_collection_exports = 0; + int frame_stem_exports = 0; + int frame_collection_exports = 0; + int depth_exports = 0; + int cube_exports = 0; + std::string last_directory; + std::string last_path; + std::string last_stem; + std::string last_name; + std::string call_order; +}; + void equirectangular_export_builds_file_target(pp::tests::Harness& harness) { const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png"); @@ -253,6 +330,107 @@ void export_menu_executor_preserves_blocked_and_unavailable_actions(pp::tests::H PP_EXPECT(harness, services.total_calls() == 1); } +void export_executor_dispatches_file_stem_collection_and_named_exports(pp::tests::Harness& harness) +{ + FakeDocumentExportServices services; + const auto file = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png"); + const auto collection = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers"); + const auto stem = pp::app::make_document_export_stem_target("D:/Exports", "demo"); + + PP_EXPECT(harness, file); + PP_EXPECT(harness, collection); + PP_EXPECT(harness, stem); + PP_EXPECT(harness, pp::app::execute_document_export_file(file.value(), services).ok()); + PP_EXPECT( + harness, + pp::app::execute_document_export_stem( + pp::app::DocumentExportCollectionKind::layers, + stem.value(), + services) + .ok()); + PP_EXPECT( + harness, + pp::app::execute_document_export_stem( + pp::app::DocumentExportCollectionKind::animation_frames, + stem.value(), + services) + .ok()); + PP_EXPECT( + harness, + pp::app::execute_document_export_collection( + pp::app::DocumentExportCollectionKind::layers, + collection.value(), + services) + .ok()); + PP_EXPECT( + harness, + pp::app::execute_document_export_collection( + pp::app::DocumentExportCollectionKind::animation_frames, + collection.value(), + services) + .ok()); + PP_EXPECT(harness, pp::app::execute_document_export_depth("demo", services).ok()); + PP_EXPECT(harness, pp::app::execute_document_export_cube_faces("demo", services).ok()); + + PP_EXPECT(harness, services.create_dir_calls == 2); + PP_EXPECT(harness, services.equirectangular_exports == 1); + PP_EXPECT(harness, services.layer_stem_exports == 1); + PP_EXPECT(harness, services.layer_collection_exports == 1); + PP_EXPECT(harness, services.frame_stem_exports == 1); + PP_EXPECT(harness, services.frame_collection_exports == 1); + PP_EXPECT(harness, services.depth_exports == 1); + PP_EXPECT(harness, services.cube_exports == 1); + PP_EXPECT( + harness, + services.call_order + == "equirect;layers-stem;frames-stem;layers-collection;frames-collection;depth;cube;"); +} + +void export_executor_preserves_failed_directory_creation(pp::tests::Harness& harness) +{ + FakeDocumentExportServices services; + services.create_directory_result = false; + const auto collection = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers"); + + PP_EXPECT(harness, collection); + PP_EXPECT( + harness, + pp::app::execute_document_export_collection( + pp::app::DocumentExportCollectionKind::layers, + collection.value(), + services) + .ok()); + PP_EXPECT(harness, services.create_dir_calls == 1); + PP_EXPECT(harness, services.layer_collection_exports == 0); + PP_EXPECT(harness, services.call_order.empty()); +} + +void export_executor_rejects_malformed_targets(pp::tests::Harness& harness) +{ + FakeDocumentExportServices services; + + PP_EXPECT( + harness, + !pp::app::execute_document_export_file(pp::app::DocumentExportFileTarget {}, services).ok()); + PP_EXPECT( + harness, + !pp::app::execute_document_export_stem( + pp::app::DocumentExportCollectionKind::layers, + pp::app::DocumentExportStemTarget {}, + services) + .ok()); + PP_EXPECT( + harness, + !pp::app::execute_document_export_collection( + pp::app::DocumentExportCollectionKind::animation_frames, + pp::app::DocumentExportCollectionTarget {}, + services) + .ok()); + PP_EXPECT(harness, !pp::app::execute_document_export_depth("", services).ok()); + PP_EXPECT(harness, !pp::app::execute_document_export_cube_faces("", services).ok()); + PP_EXPECT(harness, services.call_order.empty()); +} + } int main() @@ -271,5 +449,10 @@ int main() harness.run("export menu reports missing canvas for unlicensed image exports", export_menu_reports_missing_canvas_for_unlicensed_image_exports); harness.run("export menu executor dispatches all dialog actions", export_menu_executor_dispatches_all_dialog_actions); harness.run("export menu executor preserves blocked and unavailable actions", export_menu_executor_preserves_blocked_and_unavailable_actions); + harness.run( + "export executor dispatches file stem collection and named exports", + export_executor_dispatches_file_stem_collection_and_named_exports); + harness.run("export executor preserves failed directory creation", export_executor_preserves_failed_directory_creation); + harness.run("export executor rejects malformed targets", export_executor_rejects_malformed_targets); return harness.finish(); }