Plan export start decisions in app core

This commit is contained in:
2026-06-02 23:24:44 +02:00
parent 8de9dadf1d
commit 561193b2ab
9 changed files with 244 additions and 94 deletions

View File

@@ -413,6 +413,11 @@ Known local toolchain state:
for image file exports, layer/frame collection directories, picked-directory for image file exports, layer/frame collection directories, picked-directory
stems, and MP4 suggested names as JSON and is covered for file, collection, stems, and MP4 suggested names as JSON and is covered for file, collection,
and suggested-name states. and suggested-name states.
- `pano_cli plan-export-start` exposes `pp_app_core` export availability
planning for license-gated, demo-blocked, and missing-canvas states as JSON;
the live image, layer, animation-frame, depth, and cube-face export dialogs
consume the same start contract before reaching legacy canvas export
execution.
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open, - `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
app-close, save, save-as, save-version, and save-before-workflow decisions app-close, save, save-as, save-version, and save-before-workflow decisions
as JSON and is covered for clean, dirty, already-prompting, missing-canvas, as JSON and is covered for clean, dirty, already-prompting, missing-canvas,
@@ -423,7 +428,8 @@ Known local toolchain state:
legacy canvas work. legacy canvas work.
- `pp_app_core_document_export_tests` covers export file targets, collection - `pp_app_core_document_export_tests` covers export file targets, collection
directory/stem targets, picked-directory stems, MP4 suggested names, and directory/stem targets, picked-directory stems, MP4 suggested names, and
invalid export naming inputs. invalid export naming inputs, plus export-start license/canvas availability
decisions.
- `pp_app_core_document_session_tests` covers clean and dirty app session, - `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning, save-request, save-before-workflow, document-open action planning, save-request, save-before-workflow,
new-document target/resolution/overwrite planning, document file target, new-document target/resolution/overwrite planning, document file target,

View File

@@ -23,7 +23,7 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests | | Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file | | PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export target planning tests | | PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests |
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests | | Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests |
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set | | Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation | | Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |

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`, 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 target naming/path 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-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document creation/loading, brush import execution, saving, and export execution still reach legacy `Canvas::I`/UI 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_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-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `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`, 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, `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`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document creation/loading, brush import execution, saving, and export execution still reach legacy `Canvas::I`/UI 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_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 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

@@ -440,6 +440,9 @@ the live save-version dialog.
`pano_cli plan-export-target` exposes app-core export target planning for `pano_cli plan-export-target` exposes app-core export target planning for
equirectangular image files, layer/frame collection stems, picked-directory equirectangular image files, layer/frame collection stems, picked-directory
stems, and MP4 suggested names used by the live export dialogs. stems, and MP4 suggested names used by the live export dialogs.
`pano_cli plan-export-start` exposes the app-core export availability decision
used by live image, layer, animation-frame, depth, and cube-face export dialogs
before they call legacy canvas export execution.
`pano_cli parse-layout` exercises the XML layout path. Continue expanding `pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary. behind the renderer boundary.

View File

@@ -26,6 +26,26 @@ struct DocumentExportSuggestedName {
std::string name; std::string name;
}; };
enum class DocumentExportStartDecision {
start_now,
show_license_disabled,
unavailable_no_canvas,
};
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
bool requires_license,
bool license_valid,
bool has_canvas) noexcept
{
if (requires_license && !license_valid) {
return DocumentExportStartDecision::show_license_disabled;
}
return has_canvas
? DocumentExportStartDecision::start_now
: DocumentExportStartDecision::unavailable_no_canvas;
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target( [[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
std::string_view work_directory, std::string_view work_directory,
std::string_view document_name, std::string_view document_name,

View File

@@ -28,6 +28,30 @@ void webgl_pick_file_save(const std::string& path,
void webgl_sync(); void webgl_sync();
#endif #endif
namespace {
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
{
const auto decision = pp::app::plan_document_export_start(
requires_license,
!requires_license || app.check_license(),
app.canvas != nullptr);
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return true;
case pp::app::DocumentExportStartDecision::show_license_disabled:
app.message_box("License", "This function is disabled in demo mode.");
return false;
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return false;
}
return false;
}
}
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/) std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
{ {
auto pb = std::make_shared<NodeProgressBar>(); auto pb = std::make_shared<NodeProgressBar>();
@@ -414,138 +438,114 @@ void App::dialog_save()
void App::dialog_export(std::string ext) void App::dialog_export(std::string ext)
{ {
if (!check_license()) if (!can_start_document_export(*this, true))
{ return;
message_box("License", "This function is disabled in demo mode.");
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
if (!target) {
message_box("Export Equirectangular", target.status().message);
return; return;
} }
if (canvas) canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
{
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
if (!target) {
message_box("Export Equirectangular", target.status().message);
return;
}
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
#if defined(__IOS__) #if defined(__IOS__)
message_box("Export Equirectangular", "Image exported to Photos"); message_box("Export Equirectangular", "Image exported to Photos");
#elif defined(__OSX__) #elif defined(__OSX__)
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder"); message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
#elif defined(_WIN32) #elif defined(_WIN32)
message_box("Export Equirectangular", "Image exported to " + work_path); message_box("Export Equirectangular", "Image exported to " + work_path);
#elif defined(__QUEST__) #elif defined(__QUEST__)
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo); //auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
#elif __WEB__ #elif __WEB__
ui_task([=]{ ui_task([=]{
webgl_pick_file_save(target.path, target.suggested_name, [](bool success){ }); webgl_pick_file_save(target.path, target.suggested_name, [](bool success){ });
});
#endif
}); });
} #endif
});
} }
void App::dialog_export_layers() void App::dialog_export_layers()
{ {
if (!check_license()) if (!can_start_document_export(*this, true))
{ return;
message_box("License", "This function is disabled in demo mode.");
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
if (!target) {
message_box("Export Layers", target.status().message);
return; return;
} }
if (canvas) if (Asset::create_dir(target.value().directory))
{ {
#if defined(__IOS__) canvas->m_canvas->export_layers(target.value().stem_path, [this] {
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers"); message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) { if (!target) {
message_box("Export Layers", target.status().message); message_box("Export Layers", target.status().message);
return; return;
} }
if (Asset::create_dir(target.value().directory)) canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
{ message_box("Export Layers", "Layers exported to: " + target.stem_path);
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
message_box("Export Layers", "Layers exported to: " + target.stem_path);
});
}); });
});
#endif #endif
}
} }
void App::dialog_export_anim_frames() void App::dialog_export_anim_frames()
{ {
if (!check_license()) if (!can_start_document_export(*this, true))
{ return;
message_box("License", "This function is disabled in demo mode.");
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
if (!target) {
message_box("Export Layers", target.status().message);
return; return;
} }
if (canvas) if (Asset::create_dir(target.value().directory))
{ {
#if defined(__IOS__) canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames"); message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) { if (!target) {
message_box("Export Layers", target.status().message); message_box("Export Layers", target.status().message);
return; return;
} }
if (Asset::create_dir(target.value().directory)) canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] {
{ message_box("Export Layers", "Layers exported to: " + target.stem_path);
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
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);
});
}); });
});
#endif #endif
}
} }
void App::dialog_export_depth() void App::dialog_export_depth()
{ {
if (!check_license()) if (!can_start_document_export(*this, true))
{
message_box("License", "This function is disabled in demo mode.");
return; return;
}
if (canvas) // TODO: use picker
{ canvas->m_canvas->export_depth(doc_name, [this] {
// TODO: use picker
canvas->m_canvas->export_depth(doc_name, [this] {
#if defined(__IOS__) #if defined(__IOS__)
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter"); message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
#elif defined(__OSX__) #elif defined(__OSX__)
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder"); message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
#elif defined(_WIN32) #elif defined(_WIN32)
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path); message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
#endif #endif
}); });
}
} }
void App::dialog_resize() void App::dialog_resize()
@@ -571,18 +571,18 @@ void App::dialog_resize()
void App::dialog_export_cube_faces() void App::dialog_export_cube_faces()
{ {
if (canvas) if (!can_start_document_export(*this, false))
{ return;
canvas->m_canvas->export_cube_faces(doc_name, [this] {
canvas->m_canvas->export_cube_faces(doc_name, [this] {
#if defined(__IOS__) #if defined(__IOS__)
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter"); message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
#elif defined(__OSX__) #elif defined(__OSX__)
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder"); message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
#elif defined(_WIN32) #elif defined(_WIN32)
message_box("Export Cube Faces", "Image and depth exported to " + work_path); message_box("Export Cube Faces", "Image and depth exported to " + work_path);
#endif #endif
}); });
}
} }
void App::dialog_layer_rename() void App::dialog_layer_rename()

View File

@@ -443,6 +443,24 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-version\".*\"documentName\":\"demo.01\".*\"existingPaths\":1.*\"name\":\"demo.03\".*\"path\":\"D:/Paint/demo.03.ppi\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-version\".*\"documentName\":\"demo.01\".*\"existingPaths\":1.*\"name\":\"demo.03\".*\"path\":\"D:/Paint/demo.03.ppi\"")
add_test(NAME pano_cli_plan_export_start_allowed_smoke
COMMAND pano_cli plan-export-start --requires-license)
set_tests_properties(pano_cli_plan_export_start_allowed_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-start\".*\"requiresLicense\":true.*\"licenseValid\":true.*\"hasCanvas\":true.*\"decision\":\"start-now\"")
add_test(NAME pano_cli_plan_export_start_demo_blocked_smoke
COMMAND pano_cli plan-export-start --requires-license --demo)
set_tests_properties(pano_cli_plan_export_start_demo_blocked_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-start\".*\"requiresLicense\":true.*\"licenseValid\":false.*\"decision\":\"show-license-disabled\"")
add_test(NAME pano_cli_plan_export_start_no_canvas_smoke
COMMAND pano_cli plan-export-start --no-canvas)
set_tests_properties(pano_cli_plan_export_start_no_canvas_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-start\".*\"requiresLicense\":false.*\"hasCanvas\":false.*\"decision\":\"unavailable-no-canvas\"")
add_test(NAME pano_cli_plan_export_target_file_smoke add_test(NAME pano_cli_plan_export_target_file_smoke
COMMAND pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png) COMMAND pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png)
set_tests_properties(pano_cli_plan_export_target_file_smoke PROPERTIES set_tests_properties(pano_cli_plan_export_target_file_smoke PROPERTIES

View File

@@ -46,6 +46,38 @@ void video_export_builds_suggested_name(pp::tests::Harness& harness)
PP_EXPECT(harness, animation.value().name == "demo-animation"); PP_EXPECT(harness, animation.value().name == "demo-animation");
} }
void export_start_allows_valid_canvas_state(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_export_start(true, true, true)
== pp::app::DocumentExportStartDecision::start_now);
PP_EXPECT(
harness,
pp::app::plan_document_export_start(false, false, true)
== pp::app::DocumentExportStartDecision::start_now);
}
void export_start_blocks_demo_only_when_license_required(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_export_start(true, false, true)
== pp::app::DocumentExportStartDecision::show_license_disabled);
}
void export_start_reports_missing_canvas_after_license_gate(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_export_start(false, true, false)
== pp::app::DocumentExportStartDecision::unavailable_no_canvas);
PP_EXPECT(
harness,
pp::app::plan_document_export_start(true, false, false)
== pp::app::DocumentExportStartDecision::show_license_disabled);
}
} }
int main() int main()
@@ -56,5 +88,8 @@ int main()
harness.run("collection export builds directory and stem", collection_export_builds_directory_and_stem); harness.run("collection export builds directory and stem", collection_export_builds_directory_and_stem);
harness.run("picked directory export builds stem", picked_directory_export_builds_stem); harness.run("picked directory export builds stem", picked_directory_export_builds_stem);
harness.run("video export builds suggested name", video_export_builds_suggested_name); harness.run("video export builds suggested name", video_export_builds_suggested_name);
harness.run("export start allows valid canvas state", export_start_allows_valid_canvas_state);
harness.run("export start blocks demo only when license required", export_start_blocks_demo_only_when_license_required);
harness.run("export start reports missing canvas after license gate", export_start_reports_missing_canvas_after_license_gate);
return harness.finish(); return harness.finish();
} }

View File

@@ -130,6 +130,12 @@ struct PlanExportTargetArgs {
std::string suffix; std::string suffix;
}; };
struct PlanExportStartArgs {
bool requires_license = false;
bool license_valid = true;
bool has_canvas = true;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -375,6 +381,20 @@ const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision
return "save-now"; return "save-now";
} }
const char* document_export_start_decision_name(pp::app::DocumentExportStartDecision decision) noexcept
{
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return "start-now";
case pp::app::DocumentExportStartDecision::show_license_disabled:
return "show-license-disabled";
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return "unavailable-no-canvas";
}
return "unavailable-no-canvas";
}
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;
@@ -409,6 +429,7 @@ void print_help()
<< " plan-new-document --work-dir DIR --name NAME [--resolution-index N] [--target-exists]\n" << " plan-new-document --work-dir DIR --name NAME [--resolution-index N] [--target-exists]\n"
<< " plan-document-file --work-dir DIR --name NAME [--target-exists]\n" << " plan-document-file --work-dir DIR --name NAME [--target-exists]\n"
<< " plan-document-version --directory DIR --doc-name NAME [--existing-path FILE]\n" << " plan-document-version --directory DIR --doc-name NAME [--existing-path FILE]\n"
<< " plan-export-start [--requires-license] [--demo] [--no-canvas]\n"
<< " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n" << " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n"
<< " load-project --path FILE\n" << " load-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
@@ -1510,6 +1531,49 @@ int plan_document_version(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_export_start_args(
int argc,
char** argv,
PlanExportStartArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--requires-license") {
args.requires_license = true;
} else if (key == "--demo") {
args.license_valid = false;
} else if (key == "--no-canvas") {
args.has_canvas = false;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_export_start(int argc, char** argv)
{
PlanExportStartArgs args;
const auto status = parse_plan_export_start_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-export-start", status.message);
return 2;
}
const auto decision = pp::app::plan_document_export_start(
args.requires_license,
args.license_valid,
args.has_canvas);
std::cout << "{\"ok\":true,\"command\":\"plan-export-start\""
<< ",\"state\":{\"requiresLicense\":" << json_bool(args.requires_license)
<< ",\"licenseValid\":" << json_bool(args.license_valid)
<< ",\"hasCanvas\":" << json_bool(args.has_canvas)
<< "},\"decision\":\"" << document_export_start_decision_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,
@@ -3653,6 +3717,10 @@ int main(int argc, char** argv)
return plan_document_version(argc, argv); return plan_document_version(argc, argv);
} }
if (command == "plan-export-start") {
return plan_export_start(argc, argv);
}
if (command == "plan-export-target") { if (command == "plan-export-target") {
return plan_export_target(argc, argv); return plan_export_target(argc, argv);
} }