diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 5b6d6b8..90e8103 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -1,7 +1,7 @@ # Build And Platform Inventory Status: live -Last updated: 2026-06-03 +Last updated: 2026-06-04 This inventory records the known build surfaces during the CMake migration. Keep it updated as platform paths move to shared CMake targets. @@ -542,8 +542,9 @@ Known local toolchain state: app/window close, UI-thread lifecycle hooks, render-context lifecycle hooks, render-target binding hooks, render platform hint hooks, render-capture frame hooks, render debug callback hooks, per-frame platform hooks, picker - callbacks, and recording cleanup, live asset/layout reload policy, diagnostic - stacktrace/crash hooks, prepared-file save/download handoff; + callbacks, recording cleanup, exported-image publishing, persistent storage + flushing, live asset/layout reload policy, diagnostic stacktrace/crash hooks, + prepared-file save/download handoff; Windows live app execution now uses injected `WindowsPlatformServices` from diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 0293911..8def995 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -67,6 +67,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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`, 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`; `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 | | DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | | DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly | +| DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through `PlatformServices`, but non-Windows execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index a84ebc7..e8df039 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1,7 +1,7 @@ # PanoPainter Modernization Roadmap Status: live -Last updated: 2026-06-03 +Last updated: 2026-06-04 This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this @@ -639,6 +639,10 @@ is preserved while their platform shell implementations are extracted. Prepared-file save/download handoff is now also part of the service contract, so iOS/Web export completion routes through `PlatformServices` after the app writes the temporary/exported payload. +Canvas image export publishing and explicit persistent-storage flushes now +dispatch through `PlatformServices` too, preserving iOS photo-library export +publication and WebGL filesystem sync behavior in the legacy adapter while +removing those direct platform calls from `Canvas` and brush preset storage. `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, and `App::hideKeyboard` now dispatch through the active service without local platform guards; unsupported platforms rely on their service no-op behavior. @@ -1679,7 +1683,8 @@ Results: app/window close dispatch, UI-thread lifecycle dispatch, render-context lifecycle dispatch, render-target binding dispatch, render platform hint dispatch, render debug callback dispatch, render-capture frame hook dispatch, - recording cleanup dispatch, live asset/layout reload policy dispatch, + recording cleanup dispatch, exported-image publish dispatch, persistent + storage flush dispatch, live asset/layout reload policy dispatch, diagnostic hook dispatch, per-frame platform hook dispatch, picker callback dispatch, and prepared-file save/download callback dispatch. The live Windows app now diff --git a/src/app.h b/src/app.h index 6367493..bef9f39 100644 --- a/src/app.h +++ b/src/app.h @@ -204,6 +204,8 @@ public: void end_render_capture_frame(); [[nodiscard]] bool platform_deletes_recorded_files_on_clear(); void clear_platform_recorded_files(std::string path); + void publish_exported_image(std::string path); + void flush_platform_storage(); [[nodiscard]] bool platform_enables_live_asset_reloading(); void update_platform_frame(float delta_time_seconds); void report_rendered_frames(int frames); diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 87a2fe2..c3839ef 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -31,7 +31,6 @@ #include "oculus_vr.h" #elif __WEB__ void webgl_pick_file(std::function callback); -void webgl_sync(); #endif namespace { diff --git a/src/app_events.cpp b/src/app_events.cpp index 74727fb..205c770 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -277,6 +277,16 @@ void App::clear_platform_recorded_files(std::string path) active_platform_services().clear_recorded_files(path); } +void App::publish_exported_image(std::string path) +{ + active_platform_services().publish_exported_image(path); +} + +void App::flush_platform_storage() +{ + active_platform_services().flush_persistent_storage(); +} + bool App::platform_enables_live_asset_reloading() { return active_platform_services().enables_live_asset_reloading(); diff --git a/src/canvas.cpp b/src/canvas.cpp index 091fec9..02b6656 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -13,9 +13,6 @@ #ifdef __APPLE__ #include -#import "objc_utils.h" -#elif __WEB__ -void webgl_sync(); #endif namespace { @@ -2021,9 +2018,7 @@ void Canvas::export_equirectangular_thread(std::string file_path) data.save_png(file_path); } -#ifdef __IOS__ - save_image_library(file_path); -#endif + App::I->publish_exported_image(file_path); } void Canvas::inject_xmp(std::string jpg_path) @@ -2305,9 +2300,7 @@ void Canvas::export_cube_faces_thread(std::string file_name) face.save_png(path); pb->increment(); -#ifdef __IOS__ - save_image_library(path); -#endif + App::I->publish_exported_image(path); #ifdef __OBJC__ [files addObject : [NSString stringWithUTF8String:path.c_str()] ]; #endif @@ -2575,9 +2568,7 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress) if (!sw.save(lapse_path)) LOG("cannot save timelase to %s", lapse_path.c_str()); } -#if __WEB__ - webgl_sync(); -#endif + App::I->flush_platform_storage(); } if (show_progress) diff --git a/src/node_panel_brush.cpp b/src/node_panel_brush.cpp index 9d10bac..9fabb57 100644 --- a/src/node_panel_brush.cpp +++ b/src/node_panel_brush.cpp @@ -9,8 +9,6 @@ #ifdef __APPLE__ #include -#elif __WEB__ -void webgl_sync(); #endif #include "canvas.h" @@ -244,9 +242,7 @@ bool NodePanelBrush::save() } f.write((char*)sw.m_data.data(), sw.m_data.size()); f.close(); -#if __WEB__ - webgl_sync(); -#endif + App::I->flush_platform_storage(); return true; } return false; diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h index a0ded6a..afb7fe5 100644 --- a/src/platform_api/platform_services.h +++ b/src/platform_api/platform_services.h @@ -41,6 +41,8 @@ public: virtual void end_render_capture_frame() = 0; [[nodiscard]] virtual bool deletes_recorded_files_on_clear() = 0; virtual void clear_recorded_files(std::string_view recording_path) = 0; + virtual void publish_exported_image(std::string_view path) = 0; + virtual void flush_persistent_storage() = 0; [[nodiscard]] virtual bool enables_live_asset_reloading() = 0; virtual void update_platform_frame(float delta_time_seconds) = 0; virtual void report_rendered_frames(int frames) = 0; diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index 9abb84e..09afc9d 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -18,6 +18,9 @@ std::string android_get_clipboard(); bool android_set_clipboard(const std::string& s); #elif __APPLE__ void delete_all_files_in_path(const std::string& source_path); +#ifdef __IOS__ +void save_image_library(const std::string& path); +#endif #elif __LINUX__ #include std::string linux_home_path(); @@ -279,6 +282,22 @@ public: #endif } + void publish_exported_image(std::string_view path) override + { +#ifdef __IOS__ + save_image_library(std::string(path)); +#else + (void)path; +#endif + } + + void flush_persistent_storage() override + { +#ifdef __WEB__ + webgl_sync(); +#endif + } + [[nodiscard]] bool enables_live_asset_reloading() override { #if defined(__OSX__) diff --git a/src/platform_windows/windows_platform_services.cpp b/src/platform_windows/windows_platform_services.cpp index 112e4eb..415ef64 100644 --- a/src/platform_windows/windows_platform_services.cpp +++ b/src/platform_windows/windows_platform_services.cpp @@ -369,6 +369,15 @@ public: (void)recording_path; } + void publish_exported_image(std::string_view path) override + { + (void)path; + } + + void flush_persistent_storage() override + { + } + [[nodiscard]] bool enables_live_asset_reloading() override { return true; diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index 3c69680..9d35ff1 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -123,6 +123,17 @@ public: cleared_recording_path.assign(recording_path); } + void publish_exported_image(std::string_view path) override + { + ++exported_image_publishes; + exported_image_path.assign(path); + } + + void flush_persistent_storage() override + { + ++persistent_storage_flushes; + } + [[nodiscard]] bool enables_live_asset_reloading() override { ++live_asset_reload_policy_checks; @@ -215,6 +226,8 @@ public: int crash_tests = 0; int recording_delete_policy_checks = 0; int recording_clears = 0; + int exported_image_publishes = 0; + int persistent_storage_flushes = 0; int live_asset_reload_policy_checks = 0; int platform_frame_updates = 0; int frame_reports = 0; @@ -238,6 +251,7 @@ public: std::string prepared_file_path; std::string prepared_file_name; std::string cleared_recording_path; + std::string exported_image_path; std::string picker_path = "D:/Paint/import.png"; std::string save_path = "D:/Paint/export.ppi"; std::string directory_path = "D:/Paint"; @@ -377,6 +391,19 @@ void platform_services_dispatch_recording_cleanup(pp::tests::Harness& harness) PP_EXPECT(harness, fake.cleared_recording_path == "D:/Paint/frames"); } +void platform_services_dispatch_exported_image_and_storage_flush(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + + services.publish_exported_image("D:/Paint/export.png"); + services.flush_persistent_storage(); + + PP_EXPECT(harness, fake.exported_image_publishes == 1); + PP_EXPECT(harness, fake.exported_image_path == "D:/Paint/export.png"); + PP_EXPECT(harness, fake.persistent_storage_flushes == 1); +} + void platform_services_dispatch_live_asset_reload_policy(pp::tests::Harness& harness) { FakePlatformServices fake("unused"); @@ -481,6 +508,7 @@ int main() harness.run("platform services dispatch render context lifecycle", platform_services_dispatch_render_context_lifecycle); harness.run("platform services dispatch render capture hooks", platform_services_dispatch_render_capture_hooks); harness.run("platform services dispatch recording cleanup", platform_services_dispatch_recording_cleanup); + harness.run("platform services dispatch exported image and storage flush", platform_services_dispatch_exported_image_and_storage_flush); harness.run("platform services dispatch live asset reload policy", platform_services_dispatch_live_asset_reload_policy); harness.run("platform services dispatch frame hooks", platform_services_dispatch_frame_hooks); harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions);