Route export storage hooks through platform services

This commit is contained in:
2026-06-04 16:34:19 +02:00
parent 3c709f07e6
commit 6419645e03
12 changed files with 86 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
# Build And Platform Inventory # Build And Platform Inventory
Status: live Status: live
Last updated: 2026-06-03 Last updated: 2026-06-04
This inventory records the known build surfaces during the CMake migration. This inventory records the known build surfaces during the CMake migration.
Keep it updated as platform paths move to shared CMake targets. 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, app/window close, UI-thread lifecycle hooks, render-context lifecycle hooks,
render-target binding hooks, render platform hint hooks, render-capture frame render-target binding hooks, render platform hint hooks, render-capture frame
hooks, render debug callback hooks, per-frame platform hooks, picker hooks, render debug callback hooks, per-frame platform hooks, picker
callbacks, and recording cleanup, live asset/layout reload policy, diagnostic callbacks, recording cleanup, exported-image publishing, persistent storage
stacktrace/crash hooks, prepared-file save/download handoff; flushing, live asset/layout reload policy, diagnostic stacktrace/crash hooks,
prepared-file save/download handoff;
Windows Windows
live app execution now uses injected live app execution now uses injected
`WindowsPlatformServices` from `WindowsPlatformServices` from

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -1,7 +1,7 @@
# PanoPainter Modernization Roadmap # PanoPainter Modernization Roadmap
Status: live Status: live
Last updated: 2026-06-03 Last updated: 2026-06-04
This is the living roadmap for modernizing PanoPainter into independently This is the living roadmap for modernizing PanoPainter into independently
testable C++23 components while retaining all existing functionality. Keep this 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, Prepared-file save/download handoff is now also part of the service contract,
so iOS/Web export completion routes through `PlatformServices` after the app so iOS/Web export completion routes through `PlatformServices` after the app
writes the temporary/exported payload. 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::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, and
`App::hideKeyboard` now dispatch through the active service without local `App::hideKeyboard` now dispatch through the active service without local
platform guards; unsupported platforms rely on their service no-op behavior. 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 app/window close dispatch, UI-thread lifecycle dispatch, render-context
lifecycle dispatch, render-target binding dispatch, render platform hint lifecycle dispatch, render-target binding dispatch, render platform hint
dispatch, render debug callback dispatch, render-capture frame hook dispatch, 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 diagnostic hook dispatch, per-frame platform hook dispatch, picker callback
dispatch, and prepared-file save/download callback dispatch. The live Windows dispatch, and prepared-file save/download callback dispatch. The live Windows
app now app now

View File

@@ -204,6 +204,8 @@ public:
void end_render_capture_frame(); void end_render_capture_frame();
[[nodiscard]] bool platform_deletes_recorded_files_on_clear(); [[nodiscard]] bool platform_deletes_recorded_files_on_clear();
void clear_platform_recorded_files(std::string path); 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(); [[nodiscard]] bool platform_enables_live_asset_reloading();
void update_platform_frame(float delta_time_seconds); void update_platform_frame(float delta_time_seconds);
void report_rendered_frames(int frames); void report_rendered_frames(int frames);

View File

@@ -31,7 +31,6 @@
#include "oculus_vr.h" #include "oculus_vr.h"
#elif __WEB__ #elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback); void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_sync();
#endif #endif
namespace { namespace {

View File

@@ -277,6 +277,16 @@ void App::clear_platform_recorded_files(std::string path)
active_platform_services().clear_recorded_files(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() bool App::platform_enables_live_asset_reloading()
{ {
return active_platform_services().enables_live_asset_reloading(); return active_platform_services().enables_live_asset_reloading();

View File

@@ -13,9 +13,6 @@
#ifdef __APPLE__ #ifdef __APPLE__
#include <Foundation/Foundation.h> #include <Foundation/Foundation.h>
#import "objc_utils.h"
#elif __WEB__
void webgl_sync();
#endif #endif
namespace { namespace {
@@ -2021,9 +2018,7 @@ void Canvas::export_equirectangular_thread(std::string file_path)
data.save_png(file_path); data.save_png(file_path);
} }
#ifdef __IOS__ App::I->publish_exported_image(file_path);
save_image_library(file_path);
#endif
} }
void Canvas::inject_xmp(std::string jpg_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); face.save_png(path);
pb->increment(); pb->increment();
#ifdef __IOS__ App::I->publish_exported_image(path);
save_image_library(path);
#endif
#ifdef __OBJC__ #ifdef __OBJC__
[files addObject : [NSString stringWithUTF8String:path.c_str()] ]; [files addObject : [NSString stringWithUTF8String:path.c_str()] ];
#endif #endif
@@ -2575,9 +2568,7 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress)
if (!sw.save(lapse_path)) if (!sw.save(lapse_path))
LOG("cannot save timelase to %s", lapse_path.c_str()); LOG("cannot save timelase to %s", lapse_path.c_str());
} }
#if __WEB__ App::I->flush_platform_storage();
webgl_sync();
#endif
} }
if (show_progress) if (show_progress)

View File

@@ -9,8 +9,6 @@
#ifdef __APPLE__ #ifdef __APPLE__
#include <Foundation/Foundation.h> #include <Foundation/Foundation.h>
#elif __WEB__
void webgl_sync();
#endif #endif
#include "canvas.h" #include "canvas.h"
@@ -244,9 +242,7 @@ bool NodePanelBrush::save()
} }
f.write((char*)sw.m_data.data(), sw.m_data.size()); f.write((char*)sw.m_data.data(), sw.m_data.size());
f.close(); f.close();
#if __WEB__ App::I->flush_platform_storage();
webgl_sync();
#endif
return true; return true;
} }
return false; return false;

View File

@@ -41,6 +41,8 @@ public:
virtual void end_render_capture_frame() = 0; virtual void end_render_capture_frame() = 0;
[[nodiscard]] virtual bool deletes_recorded_files_on_clear() = 0; [[nodiscard]] virtual bool deletes_recorded_files_on_clear() = 0;
virtual void clear_recorded_files(std::string_view recording_path) = 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; [[nodiscard]] virtual bool enables_live_asset_reloading() = 0;
virtual void update_platform_frame(float delta_time_seconds) = 0; virtual void update_platform_frame(float delta_time_seconds) = 0;
virtual void report_rendered_frames(int frames) = 0; virtual void report_rendered_frames(int frames) = 0;

View File

@@ -18,6 +18,9 @@ std::string android_get_clipboard();
bool android_set_clipboard(const std::string& s); bool android_set_clipboard(const std::string& s);
#elif __APPLE__ #elif __APPLE__
void delete_all_files_in_path(const std::string& source_path); void delete_all_files_in_path(const std::string& source_path);
#ifdef __IOS__
void save_image_library(const std::string& path);
#endif
#elif __LINUX__ #elif __LINUX__
#include <tinyfiledialogs.h> #include <tinyfiledialogs.h>
std::string linux_home_path(); std::string linux_home_path();
@@ -279,6 +282,22 @@ public:
#endif #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 [[nodiscard]] bool enables_live_asset_reloading() override
{ {
#if defined(__OSX__) #if defined(__OSX__)

View File

@@ -369,6 +369,15 @@ public:
(void)recording_path; (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 [[nodiscard]] bool enables_live_asset_reloading() override
{ {
return true; return true;

View File

@@ -123,6 +123,17 @@ public:
cleared_recording_path.assign(recording_path); 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 [[nodiscard]] bool enables_live_asset_reloading() override
{ {
++live_asset_reload_policy_checks; ++live_asset_reload_policy_checks;
@@ -215,6 +226,8 @@ public:
int crash_tests = 0; int crash_tests = 0;
int recording_delete_policy_checks = 0; int recording_delete_policy_checks = 0;
int recording_clears = 0; int recording_clears = 0;
int exported_image_publishes = 0;
int persistent_storage_flushes = 0;
int live_asset_reload_policy_checks = 0; int live_asset_reload_policy_checks = 0;
int platform_frame_updates = 0; int platform_frame_updates = 0;
int frame_reports = 0; int frame_reports = 0;
@@ -238,6 +251,7 @@ public:
std::string prepared_file_path; std::string prepared_file_path;
std::string prepared_file_name; std::string prepared_file_name;
std::string cleared_recording_path; std::string cleared_recording_path;
std::string exported_image_path;
std::string picker_path = "D:/Paint/import.png"; std::string picker_path = "D:/Paint/import.png";
std::string save_path = "D:/Paint/export.ppi"; std::string save_path = "D:/Paint/export.ppi";
std::string directory_path = "D:/Paint"; 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"); 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) void platform_services_dispatch_live_asset_reload_policy(pp::tests::Harness& harness)
{ {
FakePlatformServices fake("unused"); 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 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 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 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 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 frame hooks", platform_services_dispatch_frame_hooks);
harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions); harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions);