From 1df93c23f7fb770df5ce0e32acb9cfce9f01acab Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 06:31:38 +0200 Subject: [PATCH] Route command conversion through app core --- CMakeLists.txt | 1 + docs/modernization/build-inventory.md | 4 + docs/modernization/debt.md | 6 ++ docs/modernization/roadmap.md | 14 ++- scripts/automation/platform-build.ps1 | 1 + scripts/automation/platform-build.sh | 2 +- src/app_commands.cpp | 48 +++++++++-- src/app_core/command_convert.h | 97 +++++++++++++++++++++ tests/CMakeLists.txt | 30 +++++++ tests/app_core/command_convert_tests.cpp | 103 +++++++++++++++++++++++ tools/pano_cli/main.cpp | 95 +++++++++++++++++++++ 11 files changed, 393 insertions(+), 8 deletions(-) create mode 100644 src/app_core/command_convert.h create mode 100644 tests/app_core/command_convert_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5aac493..5ef003a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -251,6 +251,7 @@ add_library(pp_app_core STATIC src/app_core/canvas_hotkey.h src/app_core/canvas_tool_ui.h src/app_core/canvas_view.h + src/app_core/command_convert.h src/app_core/document_animation.h src/app_core/document_canvas.h src/app_core/document_cloud.h diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index d1a5d9f..e00c62e 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -844,6 +844,10 @@ Known local toolchain state: UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target/mesh destruction, panel-node release, and quick-mode cleanup. +- `pp_app_core_command_convert_tests` covers command-line panorama conversion + sequencing for renderer-state setup, temporary canvas allocation, project + open, equirectangular export, malformed input rejection, malformed-plan + rejection, and exact executor dispatch order. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, external file display, file sharing, VR lifecycle, layout/asset file load diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 02ea335..92ed478 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -105,6 +105,12 @@ agent or engineer to remove them without reconstructing context from chat. tested `pp_app_core` plans consumed by `App::terminate` and `pano_cli plan-app-shutdown`; retained cleanup execution remains in the legacy app. +- 2026-06-05: DEBT-0003 was narrowed again. Command-line panorama conversion + sequencing for renderer-state setup, temporary canvas allocation, project + open, and equirectangular export now goes through tested `pp_app_core` + planning consumed by `App::cmd_convert` and `pano_cli plan-command-convert`; + retained OpenGL state dispatch and legacy `Canvas` project open/export + execution remain in the legacy app. - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, thumbnail, and object-draw history paths now query saved blend state through tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index a12b930..78e146f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1,7 +1,7 @@ # PanoPainter Modernization Roadmap Status: live -Last updated: 2026-06-04 +Last updated: 2026-06-05 This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this @@ -209,6 +209,11 @@ recording stop, texture/shader invalidation, layout unload, render-target destruction, panel-node release, and quick-mode cleanup now lives in `pp_app_core`; `App::terminate` and `pano_cli plan-app-shutdown` consume that plan while retained cleanup execution stays in the legacy app. +Command-line panorama conversion planning for renderer-state setup, temporary +canvas allocation, project open, and equirectangular export now lives in +`pp_app_core`; `App::cmd_convert` and `pano_cli plan-command-convert` consume +that sequence while retained OpenGL state dispatch and legacy `Canvas` +open/export execution stay in the legacy app. `panopainter_app` is now a real static target that owns app orchestration sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, @@ -1667,6 +1672,13 @@ Results: - Focused shutdown CTest coverage passed for `pp_app_core_app_shutdown_tests`, `pano_cli_plan_app_shutdown_smoke`, and `pano_cli_plan_app_shutdown_rejects_unknown_option`. +- `PanoPainter`, `pp_app_core_command_convert_tests`, and `pano_cli` built + after command-line panorama conversion planning moved into `pp_app_core`. +- Focused command-convert CTest coverage passed for + `pp_app_core_command_convert_tests`, + `pano_cli_plan_command_convert_smoke`, + `pano_cli_plan_command_convert_rejects_empty_project`, and + `pano_cli_plan_command_convert_rejects_bad_resolution`. - `PanoPainter`, `pp_app_core_brush_package_export_tests`, and `pano_cli` built after PPBR brush package export request validation and dispatch moved behind app-core brush package services. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 1471b36..5cf445d 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -46,6 +46,7 @@ param( "pp_app_core_app_shutdown_tests", "pp_app_core_app_startup_tests", "pp_app_core_app_status_tests", + "pp_app_core_command_convert_tests", "pp_app_core_brush_package_export_tests", "pp_app_core_brush_package_import_tests", "pp_app_core_brush_ui_tests", diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 8e04d14..d353beb 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" start="$(date +%s)" overall_exit=0 diff --git a/src/app_commands.cpp b/src/app_commands.cpp index 0a08ca1..f0a5ebf 100644 --- a/src/app_commands.cpp +++ b/src/app_commands.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include "app_core/command_convert.h" #include "app.h" #include "canvas.h" #include "log.h" @@ -39,15 +40,50 @@ void apply_convert_command_state() LOG("OpenGL convert command state failed: %s", status.message); } +class LegacyCommandConvertServices final : public pp::app::CommandConvertServices { +public: + void apply_renderer_state() override + { + apply_convert_command_state(); + } + + void create_canvas(int canvas_resolution) override + { + command_canvas = new Canvas; + command_canvas->create(canvas_resolution, canvas_resolution); + } + + void open_project(std::string_view project_path) override + { + if (command_canvas) + command_canvas->project_open_thread(std::string(project_path)); + } + + void export_equirectangular(std::string_view output_path) override + { + if (command_canvas) + command_canvas->export_equirectangular_thread(std::string(output_path)); + } + +private: + Canvas* command_canvas = nullptr; +}; + } void App::cmd_convert(std::string pano_path, std::string out_path) { - apply_convert_command_state(); + const auto plan = pp::app::plan_command_convert( + pano_path, + out_path, + default_canvas_resolution()); + if (!plan) { + LOG("Convert command rejected: %s", plan.status().message); + return; + } - Canvas* command_canvas = new Canvas; - const int canvas_resolution = default_canvas_resolution(); - command_canvas->create(canvas_resolution, canvas_resolution); - command_canvas->project_open_thread(pano_path); - command_canvas->export_equirectangular_thread(out_path); + LegacyCommandConvertServices services; + const auto status = pp::app::execute_command_convert_plan(plan.value(), services); + if (!status.ok()) + LOG("Convert command failed: %s", status.message); } diff --git a/src/app_core/command_convert.h b/src/app_core/command_convert.h new file mode 100644 index 0000000..57e50cd --- /dev/null +++ b/src/app_core/command_convert.h @@ -0,0 +1,97 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include + +namespace pp::app { + +enum class CommandConvertStep { + apply_renderer_state, + create_canvas, + open_project, + export_equirectangular, +}; + +struct CommandConvertPlan { + std::string project_path; + std::string output_path; + int canvas_resolution = 0; + std::vector steps; +}; + +class CommandConvertServices { +public: + virtual ~CommandConvertServices() = default; + + virtual void apply_renderer_state() = 0; + virtual void create_canvas(int canvas_resolution) = 0; + virtual void open_project(std::string_view project_path) = 0; + virtual void export_equirectangular(std::string_view output_path) = 0; +}; + +[[nodiscard]] inline pp::foundation::Result plan_command_convert( + std::string_view project_path, + std::string_view output_path, + int canvas_resolution) +{ + if (project_path.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("convert project path must not be empty")); + } + + if (output_path.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("convert output path must not be empty")); + } + + if (canvas_resolution < 1) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("convert canvas resolution must be positive")); + } + + CommandConvertPlan plan; + plan.project_path = std::string(project_path); + plan.output_path = std::string(output_path); + plan.canvas_resolution = canvas_resolution; + plan.steps = { + CommandConvertStep::apply_renderer_state, + CommandConvertStep::create_canvas, + CommandConvertStep::open_project, + CommandConvertStep::export_equirectangular, + }; + return pp::foundation::Result::success(std::move(plan)); +} + +[[nodiscard]] inline pp::foundation::Status execute_command_convert_plan( + const CommandConvertPlan& plan, + CommandConvertServices& services) +{ + if (plan.project_path.empty() || plan.output_path.empty() || plan.canvas_resolution < 1) { + return pp::foundation::Status::invalid_argument("convert plan is malformed"); + } + + for (const auto step : plan.steps) { + switch (step) { + case CommandConvertStep::apply_renderer_state: + services.apply_renderer_state(); + break; + case CommandConvertStep::create_canvas: + services.create_canvas(plan.canvas_resolution); + break; + case CommandConvertStep::open_project: + services.open_project(plan.project_path); + break; + case CommandConvertStep::export_equirectangular: + services.export_equirectangular(plan.output_path); + break; + } + } + + return pp::foundation::Status::success(); +} + +} // namespace pp::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82a57f6..8b2d7c7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -575,6 +575,16 @@ add_test(NAME pp_app_core_app_shutdown_tests COMMAND pp_app_core_app_shutdown_te set_tests_properties(pp_app_core_app_shutdown_tests PROPERTIES LABELS "app;desktop-fast") +add_executable(pp_app_core_command_convert_tests + app_core/command_convert_tests.cpp) +target_link_libraries(pp_app_core_command_convert_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_command_convert_tests COMMAND pp_app_core_command_convert_tests) +set_tests_properties(pp_app_core_command_convert_tests PROPERTIES + 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 @@ -1005,6 +1015,26 @@ if(TARGET pano_cli) WILL_FAIL TRUE PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-shutdown\".*\"message\":\"unknown option\"") + add_test(NAME pano_cli_plan_command_convert_smoke + COMMAND pano_cli plan-command-convert --project D:/Paint/demo.ppi --output D:/Paint/demo.png --canvas-resolution 2048) + set_tests_properties(pano_cli_plan_command_convert_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-command-convert\".*\"project\":\"D:/Paint/demo.ppi\".*\"output\":\"D:/Paint/demo.png\".*\"canvasResolution\":2048.*\"steps\":\\[\"apply-renderer-state\",\"create-canvas\",\"open-project\",\"export-equirectangular\"\\]") + + add_test(NAME pano_cli_plan_command_convert_rejects_empty_project + COMMAND pano_cli plan-command-convert --project "" --output D:/Paint/demo.png) + set_tests_properties(pano_cli_plan_command_convert_rejects_empty_project PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-command-convert\".*\"message\":\"convert project path must not be empty\"") + + add_test(NAME pano_cli_plan_command_convert_rejects_bad_resolution + COMMAND pano_cli plan-command-convert --canvas-resolution 0) + set_tests_properties(pano_cli_plan_command_convert_rejects_bad_resolution PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-command-convert\".*\"message\":\"convert canvas resolution must be positive\"") + add_test(NAME pano_cli_plan_brush_package_import_ppbr_smoke COMMAND pano_cli plan-brush-package-import --kind ppbr diff --git a/tests/app_core/command_convert_tests.cpp b/tests/app_core/command_convert_tests.cpp new file mode 100644 index 0000000..7d200d3 --- /dev/null +++ b/tests/app_core/command_convert_tests.cpp @@ -0,0 +1,103 @@ +#include "app_core/command_convert.h" +#include "test_harness.h" + +#include +#include + +namespace { + +class FakeCommandConvertServices final : public pp::app::CommandConvertServices { +public: + void apply_renderer_state() override + { + call_order += "state;"; + } + + void create_canvas(int canvas_resolution) override + { + created_canvas_resolution = canvas_resolution; + call_order += "canvas;"; + } + + void open_project(std::string_view project_path) override + { + opened_project = std::string(project_path); + call_order += "open;"; + } + + void export_equirectangular(std::string_view output_path) override + { + exported_output = std::string(output_path); + call_order += "export;"; + } + + int created_canvas_resolution = 0; + std::string opened_project; + std::string exported_output; + std::string call_order; +}; + +void convert_plan_preserves_legacy_step_order(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_command_convert("D:/Paint/demo.ppi", "D:/Paint/demo.png", 2048); + + PP_EXPECT(harness, plan); + if (!plan) { + return; + } + + PP_EXPECT(harness, plan.value().project_path == "D:/Paint/demo.ppi"); + PP_EXPECT(harness, plan.value().output_path == "D:/Paint/demo.png"); + PP_EXPECT(harness, plan.value().canvas_resolution == 2048); + PP_EXPECT(harness, plan.value().steps.size() == 4); + PP_EXPECT(harness, plan.value().steps[0] == pp::app::CommandConvertStep::apply_renderer_state); + PP_EXPECT(harness, plan.value().steps[1] == pp::app::CommandConvertStep::create_canvas); + PP_EXPECT(harness, plan.value().steps[2] == pp::app::CommandConvertStep::open_project); + PP_EXPECT(harness, plan.value().steps[3] == pp::app::CommandConvertStep::export_equirectangular); +} + +void convert_plan_rejects_missing_inputs(pp::tests::Harness& harness) +{ + const auto missing_project = pp::app::plan_command_convert("", "D:/Paint/demo.png", 2048); + const auto missing_output = pp::app::plan_command_convert("D:/Paint/demo.ppi", "", 2048); + const auto bad_resolution = pp::app::plan_command_convert("D:/Paint/demo.ppi", "D:/Paint/demo.png", 0); + + PP_EXPECT(harness, !missing_project); + PP_EXPECT(harness, missing_project.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, !missing_output); + PP_EXPECT(harness, missing_output.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, !bad_resolution); + PP_EXPECT(harness, bad_resolution.status().code == pp::foundation::StatusCode::invalid_argument); +} + +void convert_executor_dispatches_valid_plan(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_command_convert("D:/Paint/demo.ppi", "D:/Paint/demo.png", 1024); + FakeCommandConvertServices services; + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, pp::app::execute_command_convert_plan(plan.value(), services).ok()); + PP_EXPECT(harness, services.created_canvas_resolution == 1024); + PP_EXPECT(harness, services.opened_project == "D:/Paint/demo.ppi"); + PP_EXPECT(harness, services.exported_output == "D:/Paint/demo.png"); + PP_EXPECT(harness, services.call_order == "state;canvas;open;export;"); +} + +void convert_executor_rejects_malformed_plan(pp::tests::Harness& harness) +{ + FakeCommandConvertServices services; + PP_EXPECT(harness, !pp::app::execute_command_convert_plan(pp::app::CommandConvertPlan {}, services).ok()); + PP_EXPECT(harness, services.call_order.empty()); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("convert plan preserves legacy step order", convert_plan_preserves_legacy_step_order); + harness.run("convert plan rejects missing inputs", convert_plan_rejects_missing_inputs); + harness.run("convert executor dispatches valid plan", convert_executor_dispatches_valid_plan); + harness.run("convert executor rejects malformed plan", convert_executor_rejects_malformed_plan); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 396af60..e53256c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -10,6 +10,7 @@ #include "app_core/canvas_hotkey.h" #include "app_core/canvas_tool_ui.h" #include "app_core/canvas_view.h" +#include "app_core/command_convert.h" #include "app_core/document_animation.h" #include "app_core/document_canvas.h" #include "app_core/document_export.h" @@ -262,6 +263,12 @@ struct PlanAppFrameArgs { bool bad_resize = false; }; +struct PlanCommandConvertArgs { + std::string project_path = "D:/Paint/demo.ppi"; + std::string output_path = "D:/Paint/demo.png"; + int canvas_resolution = 1024; +}; + struct PlanBrushPackageExportArgs { std::string path; std::string author; @@ -1968,6 +1975,22 @@ const char* timelapse_recording_action_name(pp::app::TimelapseRecordingAction ac return "no-op"; } +const char* command_convert_step_name(pp::app::CommandConvertStep step) noexcept +{ + switch (step) { + case pp::app::CommandConvertStep::apply_renderer_state: + return "apply-renderer-state"; + case pp::app::CommandConvertStep::create_canvas: + return "create-canvas"; + case pp::app::CommandConvertStep::open_project: + return "open-project"; + case pp::app::CommandConvertStep::export_equirectangular: + return "export-equirectangular"; + } + + return "unknown"; +} + pp::foundation::Result parse_float_arg(std::string_view text) { float value = 0.0F; @@ -2029,6 +2052,7 @@ void print_help() << " plan-app-startup-resources [--width N] [--height N] [--bad-size]\n" << " plan-app-frame [--redraw] [--animate] [--no-designer-layout] [--no-main-layout] [--no-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only] [--resize-width N] [--resize-height N] [--bad-resize]\n" << " plan-app-shutdown\n" + << " plan-command-convert [--project FILE] [--output FILE] [--canvas-resolution N]\n" << " plan-app-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N] [--framebuffer-fetch] [--float32] [--float32-linear] [--float16]\n" << " plan-brush-package-import --kind abr|ppbr --path FILE\n" << " plan-brush-package-export --path FILE [--author NAME] [--email EMAIL] [--url URL] [--description TEXT] [--dest-path DIR] [--export-data|--no-export-data] [--header-image]\n" @@ -3860,6 +3884,73 @@ int plan_app_shutdown(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_command_convert_args( + int argc, + char** argv, + PlanCommandConvertArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--project") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.project_path = argv[++i]; + } else if (key == "--output") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.output_path = argv[++i]; + } else if (key == "--canvas-resolution") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + args.canvas_resolution = value.value(); + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_command_convert(int argc, char** argv) +{ + PlanCommandConvertArgs args; + const auto status = parse_plan_command_convert_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-command-convert", status.message); + return 2; + } + + const auto plan = pp::app::plan_command_convert( + args.project_path, + args.output_path, + args.canvas_resolution); + if (!plan) { + print_error("plan-command-convert", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-command-convert\"" + << ",\"project\":\"" << json_escape(plan.value().project_path) + << "\",\"output\":\"" << json_escape(plan.value().output_path) + << "\",\"canvasResolution\":" << plan.value().canvas_resolution + << ",\"steps\":["; + for (std::size_t i = 0; i < plan.value().steps.size(); ++i) { + if (i > 0) { + std::cout << ","; + } + std::cout << "\"" << command_convert_step_name(plan.value().steps[i]) << "\""; + } + std::cout << "]}\n"; + return 0; +} + pp::foundation::Status parse_plan_brush_package_import_args( int argc, char** argv, @@ -10115,6 +10206,10 @@ int main(int argc, char** argv) return plan_app_shutdown(argc, argv); } + if (command == "plan-command-convert") { + return plan_command_convert(argc, argv); + } + if (command == "plan-brush-package-import") { return plan_brush_package_import(argc, argv); }