Plan app export targets in app core

This commit is contained in:
2026-06-02 22:50:42 +02:00
parent 5841878df9
commit b349f24931
11 changed files with 434 additions and 24 deletions

View File

@@ -211,6 +211,7 @@ target_link_libraries(pp_ui_core
pp_project_warnings) pp_project_warnings)
add_library(pp_app_core STATIC add_library(pp_app_core STATIC
src/app_core/document_export.cpp
src/app_core/document_route.cpp src/app_core/document_route.cpp
src/app_core/document_session.cpp) src/app_core/document_session.cpp)
target_include_directories(pp_app_core target_include_directories(pp_app_core

View File

@@ -397,6 +397,10 @@ Known local toolchain state:
- `pano_cli plan-document-file` exposes `pp_app_core` document-name - `pano_cli plan-document-file` exposes `pp_app_core` document-name
validation, legacy `.ppi` path construction, and overwrite-prompt decisions validation, legacy `.ppi` path construction, and overwrite-prompt decisions
as JSON and is covered for save-now and existing-target overwrite states. as JSON and is covered for save-now and existing-target overwrite states.
- `pano_cli plan-export-target` exposes `pp_app_core` export target planning
for image file exports, layer/frame collection directories, picked-directory
stems, and MP4 suggested names as JSON and is covered for file, collection,
and suggested-name states.
- `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,
@@ -405,6 +409,9 @@ Known local toolchain state:
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or and malformed paths before the live `App::open_document` performs UI or
legacy canvas work. legacy canvas work.
- `pp_app_core_document_export_tests` covers export file targets, collection
directory/stem targets, picked-directory stems, MP4 suggested names, and
invalid export naming inputs.
- `pp_app_core_document_session_tests` covers clean and dirty app session, - `pp_app_core_document_session_tests` covers clean and dirty app session,
save-request, save-before-workflow, document file target, and overwrite save-request, save-before-workflow, document file target, and overwrite
decisions without requiring a window, canvas, or message box. decisions without requiring a window, canvas, or message box.

View File

@@ -23,8 +23,8 @@ 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` | `pp_assets`, `pp_paint_renderer` | Golden output tolerance | | PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export target planning tests |
| Equirectangular import/export | `Canvas`, shaders, RTT | `pp_paint_renderer` | Tiny cube/equirect golden | | 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 |
@@ -48,7 +48,7 @@ and validation command.
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden | | Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases | | Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek | | Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
| MP4/timelapse export | `MP4Encoder`, recording thread | `pp_assets`, `pp_paint_renderer`, app | Smoke export and cancellation | | MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Smoke export, cancellation, suggested-name tests |
## UI And Workflow ## UI And Workflow

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/save-as document file naming and overwrite decisions, `pano_cli classify-open`, `pano_cli plan-document-file`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session contracts, but document loading and saving still reach legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `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/save-as document file naming and overwrite decisions, export target naming/path decisions, `pano_cli classify-open`, `pano_cli plan-document-file`, `pano_cli plan-export-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document loading, saving, and export execution still reach legacy `Canvas::I` and 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-document-file --work-dir D:/Paint --name demo --target-exists`; `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

@@ -429,6 +429,9 @@ save-version flows, plus the save-before-continue workflow gate used by
new-document/open/browse dialogs. `pano_cli plan-document-file` exposes the new-document/open/browse dialogs. `pano_cli plan-document-file` exposes the
same app-core document-name validation, legacy `.ppi` path construction, and same app-core document-name validation, legacy `.ppi` path construction, and
overwrite prompt decision used by new-document and save-as dialogs. overwrite prompt decision used by new-document and save-as dialogs.
`pano_cli plan-export-target` exposes app-core export target planning for
equirectangular image files, layer/frame collection stems, picked-directory
stems, and MP4 suggested names used by the live export dialogs.
`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

@@ -0,0 +1 @@
#include "app_core/document_export.h"

View File

@@ -0,0 +1,111 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
struct DocumentExportFileTarget {
std::string path;
std::string suggested_name;
};
struct DocumentExportCollectionTarget {
std::string directory;
std::string stem_path;
};
struct DocumentExportStemTarget {
std::string stem_path;
};
struct DocumentExportSuggestedName {
std::string name;
};
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view extension)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
if (extension.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("extension must not be empty"));
}
DocumentExportFileTarget target;
target.suggested_name.reserve(document_name.size() + extension.size());
target.suggested_name += document_name;
target.suggested_name += extension;
target.path.reserve(work_directory.size() + target.suggested_name.size() + 1);
target.path += work_directory;
target.path += "/";
target.path += target.suggested_name;
return pp::foundation::Result<DocumentExportFileTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportCollectionTarget> make_document_export_collection_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportCollectionTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportCollectionTarget target;
target.directory.reserve(work_directory.size() + document_name.size() + suffix.size() + 1);
target.directory += work_directory;
target.directory += "/";
target.directory += document_name;
target.directory += suffix;
target.stem_path.reserve(target.directory.size() + document_name.size() + 1);
target.stem_path += target.directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportCollectionTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportStemTarget> make_document_export_stem_target(
std::string_view directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportStemTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportStemTarget target;
target.stem_path.reserve(directory.size() + document_name.size() + 1);
target.stem_path += directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportStemTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportSuggestedName>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportSuggestedName target;
target.name.reserve(document_name.size() + suffix.size());
target.name += document_name;
target.name += suffix;
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
}
}

View File

@@ -1,6 +1,7 @@
#include "pch.h" #include "pch.h"
#include "app.h" #include "app.h"
#include "action.h" #include "action.h"
#include "app_core/document_export.h"
#include "settings.h" #include "settings.h"
#include "node_dialog_open.h" #include "node_dialog_open.h"
#include "node_dialog_browse.h" #include "node_dialog_browse.h"
@@ -421,9 +422,13 @@ void App::dialog_export(std::string ext)
if (canvas) if (canvas)
{ {
// TODO: use picker // TODO: use picker
auto path = work_path + "/" + doc_name + ext; const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
auto name = doc_name + ext; if (!target) {
canvas->m_canvas->export_equirectangular(path, [this, path, name]{ 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__)
@@ -434,7 +439,7 @@ void App::dialog_export(std::string ext)
//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(path, name, [](bool success){ }); webgl_pick_file_save(target.path, target.suggested_name, [](bool success){ });
}); });
#endif #endif
}); });
@@ -452,19 +457,28 @@ void App::dialog_export_layers()
if (canvas) if (canvas)
{ {
#if defined(__IOS__) #if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_layers"; const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
if (Asset::create_dir(dir)) if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (Asset::create_dir(target.value().directory))
{ {
auto p = dir + "/" + doc_name; canvas->m_canvas->export_layers(target.value().stem_path, [this] {
canvas->m_canvas->export_layers(p, [this, p] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter"); message_box("Export Layers", "Image layers exported to Files/PanoPainter");
}); });
} }
#else #else
pick_dir([this](std::string path) { pick_dir([this](std::string path) {
auto p = path + "/" + doc_name; const auto target = pp::app::make_document_export_stem_target(path, doc_name);
canvas->m_canvas->export_layers(p, [this, p] { if (!target) {
message_box("Export Layers", "Layers exported to: " + p); 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
@@ -482,19 +496,28 @@ void App::dialog_export_anim_frames()
if (canvas) if (canvas)
{ {
#if defined(__IOS__) #if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_frames"; const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
if (Asset::create_dir(dir)) if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (Asset::create_dir(target.value().directory))
{ {
auto p = dir + "/" + doc_name; canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
canvas->m_canvas->export_anim_frames(p, [this, p] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter"); message_box("Export Layers", "Image layers exported to Files/PanoPainter");
}); });
} }
#else #else
pick_dir([this](std::string path) { pick_dir([this](std::string path) {
auto p = path + "/" + doc_name; const auto target = pp::app::make_document_export_stem_target(path, doc_name);
canvas->m_canvas->export_anim_frames(p, [this, p] { if (!target) {
message_box("Export Layers", "Layers exported to: " + p); 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
@@ -657,7 +680,13 @@ void App::dialog_ppbr_export()
void App::dialog_timelapse_export() void App::dialog_timelapse_export()
{ {
#if __IOS__ || __WEB__ #if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-timelapse", const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
if (!target) {
message_box("Export Timelapse", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) { [this](std::string path) {
rec_export(path); rec_export(path);
}, },
@@ -679,7 +708,13 @@ void App::dialog_timelapse_export()
void App::dialog_export_mp4() void App::dialog_export_mp4()
{ {
#if __IOS__ || __WEB__ #if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-animation", const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
if (!target) {
message_box("Export Animation", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) { [this](std::string path) {
export_anim_mp4(path); export_anim_mp4(path);
}, },

View File

@@ -268,6 +268,16 @@ add_test(NAME pp_app_core_document_route_tests COMMAND pp_app_core_document_rout
set_tests_properties(pp_app_core_document_route_tests PROPERTIES set_tests_properties(pp_app_core_document_route_tests PROPERTIES
LABELS "app;desktop-fast;fuzz") LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_document_export_tests
app_core/document_export_tests.cpp)
target_link_libraries(pp_app_core_document_export_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_document_export_tests COMMAND pp_app_core_document_export_tests)
set_tests_properties(pp_app_core_document_export_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
@@ -383,6 +393,24 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-file\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":true.*\"decision\":\"prompt-overwrite\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-file\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":true.*\"decision\":\"prompt-overwrite\"")
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)
set_tests_properties(pano_cli_plan_export_target_file_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-target\".*\"kind\":\"file\".*\"path\":\"D:/Paint/demo.png\".*\"suggestedName\":\"demo.png\"")
add_test(NAME pano_cli_plan_export_target_collection_smoke
COMMAND pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers)
set_tests_properties(pano_cli_plan_export_target_collection_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-target\".*\"kind\":\"collection\".*\"directory\":\"D:/Paint/demo_layers\".*\"stemPath\":\"D:/Paint/demo_layers/demo\"")
add_test(NAME pano_cli_plan_export_target_name_smoke
COMMAND pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse)
set_tests_properties(pano_cli_plan_export_target_name_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-export-target\".*\"kind\":\"name\".*\"suggestedName\":\"demo-timelapse\"")
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,60 @@
#include "app_core/document_export.h"
#include "test_harness.h"
namespace {
void equirectangular_export_builds_file_target(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
PP_EXPECT(harness, target);
PP_EXPECT(harness, target.value().path == "D:/Paint/demo.png");
PP_EXPECT(harness, target.value().suggested_name == "demo.png");
}
void export_file_target_rejects_invalid_input(pp::tests::Harness& harness)
{
const auto missing_name = pp::app::make_document_export_file_target("D:/Paint", "", ".jpg");
const auto missing_extension = pp::app::make_document_export_file_target("D:/Paint", "demo", "");
PP_EXPECT(harness, !missing_name);
PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument);
PP_EXPECT(harness, !missing_extension);
PP_EXPECT(harness, missing_extension.status().code == pp::foundation::StatusCode::invalid_argument);
}
void collection_export_builds_directory_and_stem(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");
PP_EXPECT(harness, target);
PP_EXPECT(harness, target.value().directory == "D:/Paint/demo_layers");
PP_EXPECT(harness, target.value().stem_path == "D:/Paint/demo_layers/demo");
}
void picked_directory_export_builds_stem(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_export_stem_target("D:/Exports", "demo");
PP_EXPECT(harness, target);
PP_EXPECT(harness, target.value().stem_path == "D:/Exports/demo");
}
void video_export_builds_suggested_name(pp::tests::Harness& harness)
{
const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse");
const auto animation = pp::app::make_document_export_suggested_name("demo", "-animation");
PP_EXPECT(harness, timelapse);
PP_EXPECT(harness, animation);
PP_EXPECT(harness, timelapse.value().name == "demo-timelapse");
PP_EXPECT(harness, animation.value().name == "demo-animation");
}
}
int main()
{
pp::tests::Harness harness;
harness.run("equirectangular export builds file target", equirectangular_export_builds_file_target);
harness.run("export file target rejects invalid input", export_file_target_rejects_invalid_input);
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("video export builds suggested name", video_export_builds_suggested_name);
return harness.finish();
}

View File

@@ -1,3 +1,4 @@
#include "app_core/document_export.h"
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "assets/image_format.h" #include "assets/image_format.h"
@@ -102,6 +103,15 @@ struct PlanDocumentFileArgs {
bool target_exists = false; bool target_exists = false;
}; };
struct PlanExportTargetArgs {
std::string kind;
std::string work_directory;
std::string directory;
std::string document_name;
std::string extension;
std::string suffix;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -362,6 +372,7 @@ void print_help()
<< " inspect-project --path FILE\n" << " inspect-project --path FILE\n"
<< " classify-open --path FILE\n" << " classify-open --path FILE\n"
<< " plan-document-file --work-dir DIR --name NAME [--target-exists]\n" << " plan-document-file --work-dir DIR --name NAME [--target-exists]\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"
<< " record-render [--width N] [--height N] [--exercise-clear]\n" << " record-render [--width N] [--height N] [--exercise-clear]\n"
@@ -1261,6 +1272,155 @@ int plan_document_file(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_export_target_args(
int argc,
char** argv,
PlanExportTargetArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
auto read_value = [&](std::string& output) -> pp::foundation::Status {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
output = argv[++i];
return pp::foundation::Status::success();
};
if (key == "--kind") {
const auto status = read_value(args.kind);
if (!status.ok()) {
return status;
}
} else if (key == "--work-dir") {
const auto status = read_value(args.work_directory);
if (!status.ok()) {
return status;
}
} else if (key == "--directory") {
const auto status = read_value(args.directory);
if (!status.ok()) {
return status;
}
} else if (key == "--doc-name") {
const auto status = read_value(args.document_name);
if (!status.ok()) {
return status;
}
} else if (key == "--extension") {
const auto status = read_value(args.extension);
if (!status.ok()) {
return status;
}
} else if (key == "--suffix") {
const auto status = read_value(args.suffix);
if (!status.ok()) {
return status;
}
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.kind.empty()) {
return pp::foundation::Status::invalid_argument("kind must not be empty");
}
if (args.document_name.empty()) {
return pp::foundation::Status::invalid_argument("document name must not be empty");
}
if ((args.kind == "file" || args.kind == "collection") && args.work_directory.empty()) {
return pp::foundation::Status::invalid_argument("work directory must not be empty");
}
if (args.kind == "stem" && args.directory.empty()) {
return pp::foundation::Status::invalid_argument("directory must not be empty");
}
if (args.kind == "file" && args.extension.empty()) {
return pp::foundation::Status::invalid_argument("extension must not be empty");
}
if ((args.kind == "collection" || args.kind == "name") && args.suffix.empty()) {
return pp::foundation::Status::invalid_argument("suffix must not be empty");
}
if (args.kind != "file" && args.kind != "collection" && args.kind != "stem" && args.kind != "name") {
return pp::foundation::Status::invalid_argument("unknown export target kind");
}
return pp::foundation::Status::success();
}
int plan_export_target(int argc, char** argv)
{
PlanExportTargetArgs args;
const auto status = parse_plan_export_target_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-export-target", status.message);
return 2;
}
if (args.kind == "file") {
const auto target = pp::app::make_document_export_file_target(
args.work_directory,
args.document_name,
args.extension);
if (!target) {
print_error("plan-export-target", target.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-export-target\""
<< ",\"kind\":\"file\",\"target\":{\"path\":\"" << json_escape(target.value().path)
<< "\",\"suggestedName\":\"" << json_escape(target.value().suggested_name)
<< "\"}}\n";
return 0;
}
if (args.kind == "collection") {
const auto target = pp::app::make_document_export_collection_target(
args.work_directory,
args.document_name,
args.suffix);
if (!target) {
print_error("plan-export-target", target.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-export-target\""
<< ",\"kind\":\"collection\",\"target\":{\"directory\":\"" << json_escape(target.value().directory)
<< "\",\"stemPath\":\"" << json_escape(target.value().stem_path)
<< "\"}}\n";
return 0;
}
if (args.kind == "stem") {
const auto target = pp::app::make_document_export_stem_target(args.directory, args.document_name);
if (!target) {
print_error("plan-export-target", target.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-export-target\""
<< ",\"kind\":\"stem\",\"target\":{\"stemPath\":\"" << json_escape(target.value().stem_path)
<< "\"}}\n";
return 0;
}
const auto target = pp::app::make_document_export_suggested_name(args.document_name, args.suffix);
if (!target) {
print_error("plan-export-target", target.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-export-target\""
<< ",\"kind\":\"name\",\"target\":{\"suggestedName\":\"" << json_escape(target.value().name)
<< "\"}}\n";
return 0;
}
pp::foundation::Status parse_simulate_app_session_args( pp::foundation::Status parse_simulate_app_session_args(
int argc, int argc,
char** argv, char** argv,
@@ -3243,6 +3403,10 @@ int main(int argc, char** argv)
return plan_document_file(argc, argv); return plan_document_file(argc, argv);
} }
if (command == "plan-export-target") {
return plan_export_target(argc, argv);
}
if (command == "load-project") { if (command == "load-project") {
return load_project(argc, argv); return load_project(argc, argv);
} }