Route command conversion through app core
This commit is contained in:
@@ -251,6 +251,7 @@ add_library(pp_app_core STATIC
|
|||||||
src/app_core/canvas_hotkey.h
|
src/app_core/canvas_hotkey.h
|
||||||
src/app_core/canvas_tool_ui.h
|
src/app_core/canvas_tool_ui.h
|
||||||
src/app_core/canvas_view.h
|
src/app_core/canvas_view.h
|
||||||
|
src/app_core/command_convert.h
|
||||||
src/app_core/document_animation.h
|
src/app_core/document_animation.h
|
||||||
src/app_core/document_canvas.h
|
src/app_core/document_canvas.h
|
||||||
src/app_core/document_cloud.h
|
src/app_core/document_cloud.h
|
||||||
|
|||||||
@@ -844,6 +844,10 @@ Known local toolchain state:
|
|||||||
UI-state save, stroke-preview renderer shutdown, recording stop,
|
UI-state save, stroke-preview renderer shutdown, recording stop,
|
||||||
texture/shader invalidation, layout unload, render-target/mesh destruction,
|
texture/shader invalidation, layout unload, render-target/mesh destruction,
|
||||||
panel-node release, and quick-mode cleanup.
|
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,
|
- `pp_platform_api_tests` covers service dispatch for clipboard read/write,
|
||||||
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
|
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
|
||||||
external file display, file sharing, VR lifecycle, layout/asset file load
|
external file display, file sharing, VR lifecycle, layout/asset file load
|
||||||
|
|||||||
@@ -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
|
tested `pp_app_core` plans consumed by `App::terminate` and
|
||||||
`pano_cli plan-app-shutdown`; retained cleanup execution remains in the
|
`pano_cli plan-app-shutdown`; retained cleanup execution remains in the
|
||||||
legacy app.
|
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,
|
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
|
||||||
thumbnail, and object-draw history paths now query saved blend state through
|
thumbnail, and object-draw history paths now query saved blend state through
|
||||||
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
|
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# PanoPainter Modernization Roadmap
|
# PanoPainter Modernization Roadmap
|
||||||
|
|
||||||
Status: live
|
Status: live
|
||||||
Last updated: 2026-06-04
|
Last updated: 2026-06-05
|
||||||
|
|
||||||
This is the living roadmap for modernizing PanoPainter into independently
|
This is the living roadmap for modernizing PanoPainter into independently
|
||||||
testable C++23 components while retaining all existing functionality. Keep this
|
testable C++23 components while retaining all existing functionality. Keep this
|
||||||
@@ -209,6 +209,11 @@ recording stop, texture/shader invalidation, layout unload, render-target
|
|||||||
destruction, panel-node release, and quick-mode cleanup now lives in
|
destruction, panel-node release, and quick-mode cleanup now lives in
|
||||||
`pp_app_core`; `App::terminate` and `pano_cli plan-app-shutdown` consume that
|
`pp_app_core`; `App::terminate` and `pano_cli plan-app-shutdown` consume that
|
||||||
plan while retained cleanup execution stays in the legacy app.
|
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
|
`panopainter_app` is now a real static target that owns app orchestration
|
||||||
sources, app version metadata, and version-header generation.
|
sources, app version metadata, and version-header generation.
|
||||||
`pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas,
|
`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`,
|
- Focused shutdown CTest coverage passed for `pp_app_core_app_shutdown_tests`,
|
||||||
`pano_cli_plan_app_shutdown_smoke`, and
|
`pano_cli_plan_app_shutdown_smoke`, and
|
||||||
`pano_cli_plan_app_shutdown_rejects_unknown_option`.
|
`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
|
- `PanoPainter`, `pp_app_core_brush_package_export_tests`, and `pano_cli` built
|
||||||
after PPBR brush package export request validation and dispatch moved behind
|
after PPBR brush package export request validation and dispatch moved behind
|
||||||
app-core brush package services.
|
app-core brush package services.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ param(
|
|||||||
"pp_app_core_app_shutdown_tests",
|
"pp_app_core_app_shutdown_tests",
|
||||||
"pp_app_core_app_startup_tests",
|
"pp_app_core_app_startup_tests",
|
||||||
"pp_app_core_app_status_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_export_tests",
|
||||||
"pp_app_core_brush_package_import_tests",
|
"pp_app_core_brush_package_import_tests",
|
||||||
"pp_app_core_brush_ui_tests",
|
"pp_app_core_brush_ui_tests",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ set -u
|
|||||||
|
|
||||||
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
|
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
|
||||||
shift || true
|
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)"
|
start="$(date +%s)"
|
||||||
|
|
||||||
overall_exit=0
|
overall_exit=0
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
#include "app_core/command_convert.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "canvas.h"
|
#include "canvas.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
@@ -39,15 +40,50 @@ void apply_convert_command_state()
|
|||||||
LOG("OpenGL convert command state failed: %s", status.message);
|
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)
|
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;
|
LegacyCommandConvertServices services;
|
||||||
const int canvas_resolution = default_canvas_resolution();
|
const auto status = pp::app::execute_command_convert_plan(plan.value(), services);
|
||||||
command_canvas->create(canvas_resolution, canvas_resolution);
|
if (!status.ok())
|
||||||
command_canvas->project_open_thread(pano_path);
|
LOG("Convert command failed: %s", status.message);
|
||||||
command_canvas->export_equirectangular_thread(out_path);
|
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/app_core/command_convert.h
Normal file
97
src/app_core/command_convert.h
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "foundation/result.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<CommandConvertStep> 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<CommandConvertPlan> plan_command_convert(
|
||||||
|
std::string_view project_path,
|
||||||
|
std::string_view output_path,
|
||||||
|
int canvas_resolution)
|
||||||
|
{
|
||||||
|
if (project_path.empty()) {
|
||||||
|
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("convert project path must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output_path.empty()) {
|
||||||
|
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||||
|
pp::foundation::Status::invalid_argument("convert output path must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas_resolution < 1) {
|
||||||
|
return pp::foundation::Result<CommandConvertPlan>::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<CommandConvertPlan>::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
|
||||||
@@ -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
|
set_tests_properties(pp_app_core_app_shutdown_tests PROPERTIES
|
||||||
LABELS "app;desktop-fast")
|
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
|
add_executable(pp_app_core_document_sharing_tests
|
||||||
app_core/document_sharing_tests.cpp)
|
app_core/document_sharing_tests.cpp)
|
||||||
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
|
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
|
||||||
@@ -1005,6 +1015,26 @@ if(TARGET pano_cli)
|
|||||||
WILL_FAIL TRUE
|
WILL_FAIL TRUE
|
||||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-shutdown\".*\"message\":\"unknown option\"")
|
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
|
add_test(NAME pano_cli_plan_brush_package_import_ppbr_smoke
|
||||||
COMMAND pano_cli plan-brush-package-import
|
COMMAND pano_cli plan-brush-package-import
|
||||||
--kind ppbr
|
--kind ppbr
|
||||||
|
|||||||
103
tests/app_core/command_convert_tests.cpp
Normal file
103
tests/app_core/command_convert_tests.cpp
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#include "app_core/command_convert.h"
|
||||||
|
#include "test_harness.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "app_core/canvas_hotkey.h"
|
#include "app_core/canvas_hotkey.h"
|
||||||
#include "app_core/canvas_tool_ui.h"
|
#include "app_core/canvas_tool_ui.h"
|
||||||
#include "app_core/canvas_view.h"
|
#include "app_core/canvas_view.h"
|
||||||
|
#include "app_core/command_convert.h"
|
||||||
#include "app_core/document_animation.h"
|
#include "app_core/document_animation.h"
|
||||||
#include "app_core/document_canvas.h"
|
#include "app_core/document_canvas.h"
|
||||||
#include "app_core/document_export.h"
|
#include "app_core/document_export.h"
|
||||||
@@ -262,6 +263,12 @@ struct PlanAppFrameArgs {
|
|||||||
bool bad_resize = false;
|
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 {
|
struct PlanBrushPackageExportArgs {
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string author;
|
std::string author;
|
||||||
@@ -1968,6 +1975,22 @@ const char* timelapse_recording_action_name(pp::app::TimelapseRecordingAction ac
|
|||||||
return "no-op";
|
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<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;
|
||||||
@@ -2029,6 +2052,7 @@ void print_help()
|
|||||||
<< " plan-app-startup-resources [--width N] [--height N] [--bad-size]\n"
|
<< " 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-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-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-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-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"
|
<< " 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;
|
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(
|
pp::foundation::Status parse_plan_brush_package_import_args(
|
||||||
int argc,
|
int argc,
|
||||||
char** argv,
|
char** argv,
|
||||||
@@ -10115,6 +10206,10 @@ int main(int argc, char** argv)
|
|||||||
return plan_app_shutdown(argc, argv);
|
return plan_app_shutdown(argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == "plan-command-convert") {
|
||||||
|
return plan_command_convert(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
if (command == "plan-brush-package-import") {
|
if (command == "plan-brush-package-import") {
|
||||||
return plan_brush_package_import(argc, argv);
|
return plan_brush_package_import(argc, argv);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user