Plan document share decisions in app core

This commit is contained in:
2026-06-02 23:53:09 +02:00
parent cc3490d9d8
commit 777723b68c
10 changed files with 148 additions and 3 deletions

View File

@@ -215,6 +215,7 @@ add_library(pp_app_core STATIC
src/app_core/document_export.cpp src/app_core/document_export.cpp
src/app_core/document_recording.h src/app_core/document_recording.h
src/app_core/document_route.cpp src/app_core/document_route.cpp
src/app_core/document_sharing.h
src/app_core/document_session.cpp) src/app_core/document_session.cpp)
target_include_directories(pp_app_core target_include_directories(pp_app_core
PUBLIC PUBLIC

View File

@@ -422,6 +422,10 @@ Known local toolchain state:
stop, clear, platform cleanup, frame-count reset, and export progress-total stop, clear, platform cleanup, frame-count reset, and export progress-total
planning as JSON; the live recording controls consume those contracts before planning as JSON; the live recording controls consume those contracts before
reaching legacy recording threads, PBO readback, and MP4 encoder execution. reaching legacy recording threads, PBO readback, and MP4 encoder execution.
- `pano_cli plan-share-file` exposes `pp_app_core` share availability planning
as JSON for unsaved and saved document paths; the live platform share command
consumes the same contract before reaching iOS/macOS sharing bridges or
retained no-op platform branches.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON; new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before the live cloud upload command consumes the same start contract before
@@ -450,6 +454,8 @@ Known local toolchain state:
- `pp_app_core_document_recording_tests` covers recording start/stop, clear, - `pp_app_core_document_recording_tests` covers recording start/stop, clear,
platform recorded-file cleanup, frame-count reset, export progress totals, platform recorded-file cleanup, frame-count reset, export progress totals,
and oversized progress-total clamping. and oversized progress-total clamping.
- `pp_app_core_document_sharing_tests` covers saved-path gating before platform
share execution.
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
new-document warning, clean publish prompt, and dirty save-before-upload new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download decisions, plus cloud browse no-canvas/show-browser and selected-download

View File

@@ -67,7 +67,7 @@ and validation command.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback | | Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback |
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback | | Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
| Clipboard/file picker/share | `App` platform methods | `pp_platform_*` | Platform smoke or mocked service | | Clipboard/file picker/share | `App` platform methods | `pp_app_core`, `pp_platform_*` | Share saved-path decision tests, platform smoke or mocked service |
| Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke | | Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke |
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests | | OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate | | Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |

View File

@@ -22,7 +22,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-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-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::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save 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/target naming/path decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `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/cloud contracts, but document creation/loading, brush import execution, saving, export execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `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-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `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`, file-menu save actions, `NodeCanvas` save 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/target naming/path decisions, share-file saved-path decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-share-file`, `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/share/cloud contracts, but document creation/loading, brush import execution, saving, export execution, platform share execution, recording/MP4 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_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `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-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `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-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-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 | | 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 |

View File

@@ -448,6 +448,9 @@ canvas/recording export execution.
clear, platform recorded-file cleanup, frame reset, and export progress-total clear, platform recorded-file cleanup, frame reset, and export progress-total
decisions used by the live recording controls before legacy recording threads, decisions used by the live recording controls before legacy recording threads,
PBO readback, and MP4 encoder execution continue. PBO readback, and MP4 encoder execution continue.
`pano_cli plan-share-file` exposes the app-core saved-path decision used by the
live platform share command before iOS/macOS sharing bridges or retained no-op
platform branches execute.
`pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by
the live cloud upload command for missing-canvas, new-document warning, publish the live cloud upload command for missing-canvas, new-document warning, publish
prompt, and dirty-document save-before-upload states before legacy UI, canvas, prompt, and dirty-document save-before-upload states before legacy UI, canvas,
@@ -929,6 +932,11 @@ Results:
`pano_cli_plan_recording_session_running_smoke`, and `pano_cli_plan_recording_session_running_smoke`, and
`pano_cli_plan_recording_session_platform_cleanup_smoke` passed and expose `pano_cli_plan_recording_session_platform_cleanup_smoke` passed and expose
app-core recording lifecycle/export decisions as JSON. app-core recording lifecycle/export decisions as JSON.
- `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
platform share execution.
- `pano_cli_plan_share_file_unsaved_smoke` and
`pano_cli_plan_share_file_saved_smoke` passed and expose app-core share
decisions as JSON.
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7 - `panopainter_validate_shaders` passed, validating 25 shader programs and 7
shader includes for stage markers and include graph integrity. shader includes for stage markers and include graph integrity.
- `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless,

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class DocumentShareAction {
show_save_required_warning,
share_now,
};
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
{
return path.empty()
? DocumentShareAction::show_save_required_warning
: DocumentShareAction::share_now;
}
}

View File

@@ -1,5 +1,6 @@
#include "pch.h" #include "pch.h"
#include "app.h" #include "app.h"
#include "app_core/document_sharing.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
namespace { namespace {
@@ -331,7 +332,8 @@ void App::display_file(std::string path)
void App::share_file(std::string path) void App::share_file(std::string path)
{ {
if (path.empty()) const auto plan = pp::app::plan_document_share(path);
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
{ {
message_box("Sharing failed", "Please save the document before sharing it."); message_box("Sharing failed", "Please save the document before sharing it.");
return; return;

View File

@@ -298,6 +298,16 @@ add_test(NAME pp_app_core_document_recording_tests COMMAND pp_app_core_document_
set_tests_properties(pp_app_core_document_recording_tests PROPERTIES set_tests_properties(pp_app_core_document_recording_tests PROPERTIES
LABELS "app;desktop-fast;fuzz") LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_document_sharing_tests
app_core/document_sharing_tests.cpp)
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_document_sharing_tests COMMAND pp_app_core_document_sharing_tests)
set_tests_properties(pp_app_core_document_sharing_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_document_session_tests add_executable(pp_app_core_document_session_tests
app_core/document_session_tests.cpp) app_core/document_session_tests.cpp)
target_link_libraries(pp_app_core_document_session_tests PRIVATE target_link_libraries(pp_app_core_document_session_tests PRIVATE
@@ -571,6 +581,18 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast;fuzz" LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-recording-session\".*\"platformDeletesRecordedFiles\":true.*\"deleteRecordedFiles\":true.*\"frameCountAfterClear\":0") PASS_REGULAR_EXPRESSION "\"command\":\"plan-recording-session\".*\"platformDeletesRecordedFiles\":true.*\"deleteRecordedFiles\":true.*\"frameCountAfterClear\":0")
add_test(NAME pano_cli_plan_share_file_unsaved_smoke
COMMAND pano_cli plan-share-file)
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-share-file\".*\"path\":\"\".*\"decision\":\"show-save-required-warning\"")
add_test(NAME pano_cli_plan_share_file_saved_smoke
COMMAND pano_cli plan-share-file --path D:/Paint/demo.ppi)
set_tests_properties(pano_cli_plan_share_file_saved_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-share-file\".*\"path\":\"D:/Paint/demo.ppi\".*\"decision\":\"share-now\"")
add_test(NAME pano_cli_simulate_app_session_clean_smoke add_test(NAME pano_cli_simulate_app_session_clean_smoke
COMMAND pano_cli simulate-app-session) COMMAND pano_cli simulate-app-session)
set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES

View File

@@ -0,0 +1,28 @@
#include "app_core/document_sharing.h"
#include "test_harness.h"
namespace {
void share_requires_saved_document_path(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_share("") == pp::app::DocumentShareAction::show_save_required_warning);
}
void share_allows_nonempty_document_path(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_share("D:/Paint/demo.ppi") == pp::app::DocumentShareAction::share_now);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("share requires saved document path", share_requires_saved_document_path);
harness.run("share allows nonempty document path", share_allows_nonempty_document_path);
return harness.finish();
}

View File

@@ -2,6 +2,7 @@
#include "app_core/document_cloud.h" #include "app_core/document_cloud.h"
#include "app_core/document_recording.h" #include "app_core/document_recording.h"
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_sharing.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "assets/image_format.h" #include "assets/image_format.h"
#include "assets/image_metadata.h" #include "assets/image_metadata.h"
@@ -160,6 +161,10 @@ struct PlanRecordingSessionArgs {
bool platform_deletes_recorded_files = false; bool platform_deletes_recorded_files = false;
}; };
struct PlanShareFileArgs {
std::string path;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -481,6 +486,18 @@ const char* recording_stop_action_name(pp::app::RecordingStopAction action) noex
return "no-op-not-running"; return "no-op-not-running";
} }
const char* document_share_action_name(pp::app::DocumentShareAction action) noexcept
{
switch (action) {
case pp::app::DocumentShareAction::show_save_required_warning:
return "show-save-required-warning";
case pp::app::DocumentShareAction::share_now:
return "share-now";
}
return "show-save-required-warning";
}
pp::foundation::Result<float> parse_float_arg(std::string_view text) pp::foundation::Result<float> parse_float_arg(std::string_view text)
{ {
float value = 0.0F; float value = 0.0F;
@@ -521,6 +538,7 @@ void print_help()
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n" << " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n"
<< " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n" << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n"
<< " plan-share-file [--path FILE]\n"
<< " load-project --path FILE\n" << " load-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
<< " record-render [--width N] [--height N] [--exercise-clear]\n" << " record-render [--width N] [--height N] [--exercise-clear]\n"
@@ -1854,6 +1872,43 @@ int plan_recording_session(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_share_file_args(
int argc,
char** argv,
PlanShareFileArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_share_file(int argc, char** argv)
{
PlanShareFileArgs args;
const auto status = parse_plan_share_file_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-share-file", status.message);
return 2;
}
const auto decision = pp::app::plan_document_share(args.path);
std::cout << "{\"ok\":true,\"command\":\"plan-share-file\""
<< ",\"state\":{\"path\":\"" << json_escape(args.path)
<< "\"},\"decision\":\"" << document_share_action_name(decision)
<< "\"}\n";
return 0;
}
pp::foundation::Status parse_plan_export_target_args( pp::foundation::Status parse_plan_export_target_args(
int argc, int argc,
char** argv, char** argv,
@@ -4021,6 +4076,10 @@ int main(int argc, char** argv)
return plan_recording_session(argc, argv); return plan_recording_session(argc, argv);
} }
if (command == "plan-share-file") {
return plan_share_file(argc, argv);
}
if (command == "load-project") { if (command == "load-project") {
return load_project(argc, argv); return load_project(argc, argv);
} }