diff --git a/CMakeLists.txt b/CMakeLists.txt index 57ffef6..9f3765b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,7 +235,8 @@ add_library(pp_app_core STATIC src/app_core/document_resize.h src/app_core/document_route.cpp src/app_core/document_sharing.h - src/app_core/document_session.cpp) + src/app_core/document_session.cpp + src/app_core/grid_ui.h) target_include_directories(pp_app_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 9d427d0..a77b798 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -41,6 +41,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning now consumes pure `pp_app_core` through `NodePanelAnimation` and `pano_cli plan-animation-operation`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but live execution still mutates legacy `Canvas`/`Layer` frame state and animation playback state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by the document/app command boundary with legacy `Canvas`/`Layer`/UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely | +| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 021c8c8..269bf37 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -499,6 +499,10 @@ before legacy `Canvas`/`Layer` frame execution continues. changes, tip/pattern/dual texture changes, preset brush replacement, and stroke settings refreshes used by the live brush, quick, color, and floating panel callbacks before legacy `Brush` mutation and resource loading continue. +`pano_cli plan-grid-operation` exposes app-core planning for grid heightmap +pick/load/reload/clear, lightmap render capability/limit checks, and heightmap +commit used by the live grid panel before legacy image loading, OpenGL texture +updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. `pp_platform_api` now owns a headless `PlatformServices` interface for startup storage path preparation, clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread lifecycle hooks, render-context @@ -1137,6 +1141,16 @@ Results: `pano_cli_plan_brush_operation_rejects_bad_color`, and `pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live brush/color/preset UI planning as JSON automation. +- `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear + planning, lightmap capability and limit checks, missing-heightmap no-op + behavior, and commit canvas gating. +- `pano_cli_plan_grid_operation_pick_smoke`, + `pano_cli_plan_grid_operation_load_smoke`, + `pano_cli_plan_grid_operation_render_supported_smoke`, + `pano_cli_plan_grid_operation_render_unsupported_smoke`, + `pano_cli_plan_grid_operation_rejects_empty_reload`, and + `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live + grid/heightmap/lightmap planning as JSON automation. - `pp_app_core_document_sharing_tests` passed, covering saved-path gating before platform share execution. - `pano_cli_plan_share_file_unsaved_smoke` and diff --git a/src/app_core/grid_ui.h b/src/app_core/grid_ui.h new file mode 100644 index 0000000..934eac5 --- /dev/null +++ b/src/app_core/grid_ui.h @@ -0,0 +1,145 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include + +namespace pp::app { + +enum class GridUiOperation { + request_heightmap_pick, + load_heightmap, + clear_heightmap, + reload_heightmap, + render_lightmap, + commit_heightmap, +}; + +struct GridUiPlan { + GridUiOperation operation = GridUiOperation::request_heightmap_pick; + std::string path; + int texture_resolution = 0; + int sample_count = 0; + bool opens_picker = false; + bool loads_heightmap = false; + bool clears_heightmap = false; + bool renders_lightmap = false; + bool commits_heightmap = false; + bool updates_preview = false; + bool updates_ground_opacity = false; + bool updates_shading_mode = false; + bool shows_unsupported_message = false; + bool shows_progress = false; + bool mutates_grid_state = false; +}; + +[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept +{ + if (texture_resolution <= 0 || texture_resolution > 16384) { + return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept +{ + if (sample_count <= 0 || sample_count > 4096) { + return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept +{ + GridUiPlan plan; + plan.operation = GridUiOperation::request_heightmap_pick; + plan.opens_picker = true; + return plan; +} + +[[nodiscard]] inline pp::foundation::Result plan_grid_heightmap_load(std::string_view path) +{ + if (path.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("heightmap path must not be empty")); + } + + GridUiPlan plan; + plan.operation = GridUiOperation::load_heightmap; + plan.path = std::string(path); + plan.loads_heightmap = true; + plan.updates_preview = true; + plan.updates_ground_opacity = true; + plan.mutates_grid_state = true; + return pp::foundation::Result::success(std::move(plan)); +} + +[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept +{ + GridUiPlan plan; + plan.operation = GridUiOperation::clear_heightmap; + plan.clears_heightmap = true; + plan.updates_preview = has_heightmap; + plan.mutates_grid_state = has_heightmap; + return plan; +} + +[[nodiscard]] inline pp::foundation::Result plan_grid_heightmap_reload(std::string_view path) +{ + auto plan = plan_grid_heightmap_load(path); + if (!plan) { + return pp::foundation::Result::failure(plan.status()); + } + plan.value().operation = GridUiOperation::reload_heightmap; + plan.value().updates_ground_opacity = false; + return plan; +} + +[[nodiscard]] inline pp::foundation::Result plan_grid_lightmap_render( + bool has_heightmap, + bool supports_float32, + bool supports_float16, + int texture_resolution, + int sample_count) +{ + const auto texture_status = validate_grid_texture_resolution(texture_resolution); + if (!texture_status.ok()) { + return pp::foundation::Result::failure(texture_status); + } + + const auto sample_status = validate_grid_lightmap_samples(sample_count); + if (!sample_status.ok()) { + return pp::foundation::Result::failure(sample_status); + } + + GridUiPlan plan; + plan.operation = GridUiOperation::render_lightmap; + plan.texture_resolution = texture_resolution; + plan.sample_count = sample_count; + if (!supports_float32 && !supports_float16) { + plan.shows_unsupported_message = true; + return pp::foundation::Result::success(plan); + } + + plan.renders_lightmap = has_heightmap; + plan.shows_progress = has_heightmap; + plan.updates_shading_mode = has_heightmap; + plan.mutates_grid_state = has_heightmap; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept +{ + GridUiPlan plan; + plan.operation = GridUiOperation::commit_heightmap; + plan.commits_heightmap = has_canvas; + plan.updates_ground_opacity = has_canvas; + plan.mutates_grid_state = has_canvas; + return plan; +} + +} // namespace pp::app diff --git a/src/node_panel_grid.cpp b/src/node_panel_grid.cpp index ccc6c47..afdaf71 100644 --- a/src/node_panel_grid.cpp +++ b/src/node_panel_grid.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include "app_core/grid_ui.h" #include "log.h" #include "node_panel_grid.h" #include "canvas.h" @@ -77,28 +78,19 @@ void NodePanelGrid::init_controls() }; m_hm_load->on_click = [this](Node*) { + const auto plan = pp::app::plan_grid_heightmap_pick(); + if (!plan.opens_picker) + return; App::I->pick_image([this](std::string path) { - Image img; - if (img.load_file(path)) - { - m_file_path = path; - m_hm_image = img.resize(128, 128); - m_hm_preview->tex = std::make_shared(); - m_hm_preview->tex->create(m_hm_image); - m_hm_preview->tex->create_mipmaps(); - auto sz = m_hm_preview->tex->size(); - m_hm_preview->SetAspectRatio(sz.x / sz.y); - m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height()); - m_hm_preview->SetHeight(100); - if (m_groud_opacity->get_value() == 0.f) - m_groud_opacity->set_value(1.f); - m_rt_dirty = true; - } + load_heightmap_file(path, true); }); }; m_hm_clear->on_click = [this](Node*) { + const auto plan = pp::app::plan_grid_heightmap_clear(static_cast(m_hm_image.data())); + if (!plan.clears_heightmap) + return; m_hm_plane.create(1, 1, 100 * get_resolution()); m_hm_image.destroy(); m_hm_preview->tex.reset(); @@ -107,24 +99,26 @@ void NodePanelGrid::init_controls() m_hm_reload->on_click = [this](Node*) { - Image img; - if (img.load_file(m_file_path)) - { - m_hm_image = img.resize(128, 128); - m_hm_preview->tex = std::make_shared(); - m_hm_preview->tex->create(m_hm_image); - m_hm_preview->tex->create_mipmaps(); - auto sz = m_hm_preview->tex->size(); - m_hm_preview->SetAspectRatio(sz.x / sz.y); - m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height()); - m_hm_preview->SetHeight(100); - m_rt_dirty = true; - } + load_heightmap_file(m_file_path, false); }; m_render->on_click = [this](Node*) { - if (ShaderManager::ext_float32 || ShaderManager::ext_float16) + const auto plan = pp::app::plan_grid_lightmap_render( + static_cast(m_hm_image.data()), + ShaderManager::ext_float32, + ShaderManager::ext_float16, + get_texres(), + get_samples()); + if (!plan) + return; + if (plan.value().shows_unsupported_message) + { + App::I->message_box("Rendering failed", + "Your hardware does not support lightmap rendering."); + return; + } + if (plan.value().renders_lightmap) { std::thread([this] { BT_SetTerminate(); @@ -133,18 +127,17 @@ void NodePanelGrid::init_controls() m_shade_mode = ShadeMode::Textured; }).detach(); } - else - { - App::I->message_box("Rendering failed", - "Your hardware does not support lightmap rendering."); - } }; m_commit->on_click = [this](Node*) { + const auto plan = pp::app::plan_grid_heightmap_commit(Canvas::I != nullptr); + if (!plan.commits_heightmap) + return; Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) { draw_heightmap(proj, camera, true); }, Canvas::I->layer().m_frame_index, true); - m_groud_opacity->set_value(0); + if (plan.updates_ground_opacity) + m_groud_opacity->set_value(0); }; m_hm_texres->on_select = [this](Node*, int index) { int texres = get_texres(); @@ -226,6 +219,33 @@ float NodePanelGrid::get_offset() const return glm::pow(m_groud_offset->get_value() - 0.5f, 3); } +bool NodePanelGrid::load_heightmap_file(const std::string& path, bool raise_ground_opacity) +{ + const auto plan = raise_ground_opacity + ? pp::app::plan_grid_heightmap_load(path) + : pp::app::plan_grid_heightmap_reload(path); + if (!plan) + return false; + + Image img; + if (!img.load_file(plan.value().path)) + return false; + + m_file_path = plan.value().path; + m_hm_image = img.resize(128, 128); + m_hm_preview->tex = std::make_shared(); + m_hm_preview->tex->create(m_hm_image); + m_hm_preview->tex->create_mipmaps(); + auto sz = m_hm_preview->tex->size(); + m_hm_preview->SetAspectRatio(sz.x / sz.y); + m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height()); + m_hm_preview->SetHeight(100); + if (plan.value().updates_ground_opacity && m_groud_opacity->get_value() == 0.f) + m_groud_opacity->set_value(1.f); + m_rt_dirty = true; + return true; +} + void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const { assert(App::I->is_render_thread()); diff --git a/src/node_panel_grid.h b/src/node_panel_grid.h index f82dfab..fc917a2 100644 --- a/src/node_panel_grid.h +++ b/src/node_panel_grid.h @@ -86,6 +86,7 @@ public: float get_resolution() const; float get_height() const; float get_offset() const; + bool load_heightmap_file(const std::string& path, bool raise_ground_opacity); void draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const; void bake_uvs(); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a92f501..f17a897 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -278,6 +278,16 @@ add_test(NAME pp_app_core_brush_ui_tests COMMAND pp_app_core_brush_ui_tests) set_tests_properties(pp_app_core_brush_ui_tests PROPERTIES LABELS "app;paint;desktop-fast;fuzz") +add_executable(pp_app_core_grid_ui_tests + app_core/grid_ui_tests.cpp) +target_link_libraries(pp_app_core_grid_ui_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_grid_ui_tests COMMAND pp_app_core_grid_ui_tests) +set_tests_properties(pp_app_core_grid_ui_tests PROPERTIES + LABELS "app;ui;renderer;desktop-fast;fuzz") + add_executable(pp_app_core_document_route_tests app_core/document_route_tests.cpp) target_link_libraries(pp_app_core_document_route_tests PRIVATE @@ -816,6 +826,42 @@ if(TARGET pano_cli) LABELS "app;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_grid_operation_pick_smoke + COMMAND pano_cli plan-grid-operation --kind pick) + set_tests_properties(pano_cli_plan_grid_operation_pick_smoke PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-grid-operation\".*\"operation\":\"request-heightmap-pick\".*\"opensPicker\":true.*\"mutatesGridState\":false") + + add_test(NAME pano_cli_plan_grid_operation_load_smoke + COMMAND pano_cli plan-grid-operation --kind load --path D:/Paint/height.png) + set_tests_properties(pano_cli_plan_grid_operation_load_smoke PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-grid-operation\".*\"operation\":\"load-heightmap\".*\"path\":\"D:/Paint/height.png\".*\"loadsHeightmap\":true.*\"updatesPreview\":true.*\"updatesGroundOpacity\":true") + + add_test(NAME pano_cli_plan_grid_operation_render_supported_smoke + COMMAND pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32) + set_tests_properties(pano_cli_plan_grid_operation_render_supported_smoke PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-grid-operation\".*\"operation\":\"render-lightmap\".*\"textureResolution\":1024.*\"sampleCount\":32.*\"rendersLightmap\":true.*\"updatesShadingMode\":true.*\"showsProgress\":true") + + add_test(NAME pano_cli_plan_grid_operation_render_unsupported_smoke + COMMAND pano_cli plan-grid-operation --kind render) + set_tests_properties(pano_cli_plan_grid_operation_render_unsupported_smoke PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-grid-operation\".*\"operation\":\"render-lightmap\".*\"rendersLightmap\":false.*\"showsUnsupportedMessage\":true") + + add_test(NAME pano_cli_plan_grid_operation_rejects_empty_reload + COMMAND pano_cli plan-grid-operation --kind reload) + set_tests_properties(pano_cli_plan_grid_operation_rejects_empty_reload PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_grid_operation_rejects_bad_samples + COMMAND pano_cli plan-grid-operation --kind render --float32 --samples 0) + set_tests_properties(pano_cli_plan_grid_operation_rejects_bad_samples PROPERTIES + LABELS "app;ui;renderer;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_share_file_unsaved_smoke COMMAND pano_cli plan-share-file) set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES diff --git a/tests/app_core/grid_ui_tests.cpp b/tests/app_core/grid_ui_tests.cpp new file mode 100644 index 0000000..02cd75c --- /dev/null +++ b/tests/app_core/grid_ui_tests.cpp @@ -0,0 +1,99 @@ +#include "app_core/grid_ui.h" +#include "test_harness.h" + +namespace { + +void heightmap_load_reload_and_clear_plan_state(pp::tests::Harness& harness) +{ + const auto pick = pp::app::plan_grid_heightmap_pick(); + PP_EXPECT(harness, pick.operation == pp::app::GridUiOperation::request_heightmap_pick); + PP_EXPECT(harness, pick.opens_picker); + PP_EXPECT(harness, !pick.mutates_grid_state); + + const auto load = pp::app::plan_grid_heightmap_load("D:/Paint/height.png"); + PP_EXPECT(harness, load); + if (load) { + PP_EXPECT(harness, load.value().operation == pp::app::GridUiOperation::load_heightmap); + PP_EXPECT(harness, load.value().path == "D:/Paint/height.png"); + PP_EXPECT(harness, load.value().loads_heightmap); + PP_EXPECT(harness, load.value().updates_preview); + PP_EXPECT(harness, load.value().updates_ground_opacity); + } + + const auto reload = pp::app::plan_grid_heightmap_reload("D:/Paint/height.png"); + PP_EXPECT(harness, reload); + if (reload) { + PP_EXPECT(harness, reload.value().operation == pp::app::GridUiOperation::reload_heightmap); + PP_EXPECT(harness, reload.value().loads_heightmap); + PP_EXPECT(harness, !reload.value().updates_ground_opacity); + } + + const auto clear_existing = pp::app::plan_grid_heightmap_clear(true); + PP_EXPECT(harness, clear_existing.clears_heightmap); + PP_EXPECT(harness, clear_existing.updates_preview); + PP_EXPECT(harness, clear_existing.mutates_grid_state); + + const auto clear_empty = pp::app::plan_grid_heightmap_clear(false); + PP_EXPECT(harness, clear_empty.clears_heightmap); + PP_EXPECT(harness, !clear_empty.updates_preview); + PP_EXPECT(harness, !clear_empty.mutates_grid_state); + + PP_EXPECT(harness, !pp::app::plan_grid_heightmap_load("")); + PP_EXPECT(harness, !pp::app::plan_grid_heightmap_reload("")); +} + +void lightmap_render_validates_capabilities_and_limits(pp::tests::Harness& harness) +{ + const auto supported = pp::app::plan_grid_lightmap_render(true, true, false, 1024, 32); + PP_EXPECT(harness, supported); + if (supported) { + PP_EXPECT(harness, supported.value().operation == pp::app::GridUiOperation::render_lightmap); + PP_EXPECT(harness, supported.value().renders_lightmap); + PP_EXPECT(harness, supported.value().shows_progress); + PP_EXPECT(harness, supported.value().updates_shading_mode); + PP_EXPECT(harness, supported.value().texture_resolution == 1024); + PP_EXPECT(harness, supported.value().sample_count == 32); + } + + const auto missing_heightmap = pp::app::plan_grid_lightmap_render(false, true, false, 1024, 32); + PP_EXPECT(harness, missing_heightmap); + if (missing_heightmap) { + PP_EXPECT(harness, !missing_heightmap.value().renders_lightmap); + PP_EXPECT(harness, !missing_heightmap.value().shows_unsupported_message); + } + + const auto unsupported = pp::app::plan_grid_lightmap_render(true, false, false, 1024, 32); + PP_EXPECT(harness, unsupported); + if (unsupported) { + PP_EXPECT(harness, unsupported.value().shows_unsupported_message); + PP_EXPECT(harness, !unsupported.value().renders_lightmap); + } + + PP_EXPECT(harness, !pp::app::plan_grid_lightmap_render(true, true, false, 0, 32)); + PP_EXPECT(harness, !pp::app::plan_grid_lightmap_render(true, true, false, 1024, 0)); + PP_EXPECT(harness, !pp::app::plan_grid_lightmap_render(true, true, false, 20000, 32)); + PP_EXPECT(harness, !pp::app::plan_grid_lightmap_render(true, true, false, 1024, 4097)); +} + +void commit_plan_requires_canvas(pp::tests::Harness& harness) +{ + const auto live = pp::app::plan_grid_heightmap_commit(true); + PP_EXPECT(harness, live.operation == pp::app::GridUiOperation::commit_heightmap); + PP_EXPECT(harness, live.commits_heightmap); + PP_EXPECT(harness, live.updates_ground_opacity); + + const auto headless = pp::app::plan_grid_heightmap_commit(false); + PP_EXPECT(harness, !headless.commits_heightmap); + PP_EXPECT(harness, !headless.mutates_grid_state); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("heightmap load reload and clear plan state", heightmap_load_reload_and_clear_plan_state); + harness.run("lightmap render validates capabilities and limits", lightmap_render_validates_capabilities_and_limits); + harness.run("commit plan requires canvas", commit_plan_requires_canvas); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 0dc6533..3ecf4e4 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -11,6 +11,7 @@ #include "app_core/document_route.h" #include "app_core/document_sharing.h" #include "app_core/document_session.h" +#include "app_core/grid_ui.h" #include "assets/image_format.h" #include "assets/image_metadata.h" #include "assets/image_pixels.h" @@ -263,6 +264,17 @@ struct PlanBrushOperationArgs { bool has_brush = true; }; +struct PlanGridOperationArgs { + std::string kind = "pick"; + std::string path; + bool has_heightmap = true; + bool has_canvas = true; + bool supports_float32 = false; + bool supports_float16 = false; + int texture_resolution = 1024; + int sample_count = 32; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -592,6 +604,26 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep return "stroke-settings-changed"; } +const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept +{ + switch (operation) { + case pp::app::GridUiOperation::request_heightmap_pick: + return "request-heightmap-pick"; + case pp::app::GridUiOperation::load_heightmap: + return "load-heightmap"; + case pp::app::GridUiOperation::clear_heightmap: + return "clear-heightmap"; + case pp::app::GridUiOperation::reload_heightmap: + return "reload-heightmap"; + case pp::app::GridUiOperation::render_lightmap: + return "render-lightmap"; + case pp::app::GridUiOperation::commit_heightmap: + return "commit-heightmap"; + } + + return "request-heightmap-pick"; +} + const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept { switch (decision) { @@ -847,6 +879,7 @@ void print_help() << " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n" << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" + << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" @@ -2885,6 +2918,127 @@ int plan_brush_operation(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_grid_operation_args( + int argc, + char** argv, + PlanGridOperationArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind" || key == "--path") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + if (key == "--kind") { + args.kind = argv[++i]; + } else { + args.path = argv[++i]; + } + } else if (key == "--texture-resolution" || key == "--samples") { + 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(); + } + if (key == "--texture-resolution") { + args.texture_resolution = value.value(); + } else { + args.sample_count = value.value(); + } + } else if (key == "--no-heightmap") { + args.has_heightmap = false; + } else if (key == "--no-canvas") { + args.has_canvas = false; + } else if (key == "--float32") { + args.supports_float32 = true; + } else if (key == "--float16") { + args.supports_float16 = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Result make_grid_operation_plan(const PlanGridOperationArgs& args) +{ + if (args.kind == "pick") { + return pp::foundation::Result::success(pp::app::plan_grid_heightmap_pick()); + } + if (args.kind == "load") { + return pp::app::plan_grid_heightmap_load(args.path); + } + if (args.kind == "reload") { + return pp::app::plan_grid_heightmap_reload(args.path); + } + if (args.kind == "clear") { + return pp::foundation::Result::success( + pp::app::plan_grid_heightmap_clear(args.has_heightmap)); + } + if (args.kind == "render") { + return pp::app::plan_grid_lightmap_render( + args.has_heightmap, + args.supports_float32, + args.supports_float16, + args.texture_resolution, + args.sample_count); + } + if (args.kind == "commit") { + return pp::foundation::Result::success( + pp::app::plan_grid_heightmap_commit(args.has_canvas)); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown grid operation kind")); +} + +int plan_grid_operation(int argc, char** argv) +{ + PlanGridOperationArgs args; + const auto status = parse_plan_grid_operation_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-grid-operation", status.message); + return 2; + } + + const auto plan = make_grid_operation_plan(args); + if (!plan) { + print_error("plan-grid-operation", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-grid-operation\"" + << ",\"state\":{\"kind\":\"" << json_escape(args.kind) + << "\",\"path\":\"" << json_escape(args.path) + << "\",\"hasHeightmap\":" << json_bool(args.has_heightmap) + << ",\"hasCanvas\":" << json_bool(args.has_canvas) + << ",\"float32\":" << json_bool(args.supports_float32) + << ",\"float16\":" << json_bool(args.supports_float16) + << ",\"textureResolution\":" << args.texture_resolution + << ",\"samples\":" << args.sample_count + << "},\"plan\":{\"operation\":\"" << grid_ui_operation_name(value.operation) + << "\",\"path\":\"" << json_escape(value.path) + << "\",\"textureResolution\":" << value.texture_resolution + << ",\"sampleCount\":" << value.sample_count + << ",\"opensPicker\":" << json_bool(value.opens_picker) + << ",\"loadsHeightmap\":" << json_bool(value.loads_heightmap) + << ",\"clearsHeightmap\":" << json_bool(value.clears_heightmap) + << ",\"rendersLightmap\":" << json_bool(value.renders_lightmap) + << ",\"commitsHeightmap\":" << json_bool(value.commits_heightmap) + << ",\"updatesPreview\":" << json_bool(value.updates_preview) + << ",\"updatesGroundOpacity\":" << json_bool(value.updates_ground_opacity) + << ",\"updatesShadingMode\":" << json_bool(value.updates_shading_mode) + << ",\"showsUnsupportedMessage\":" << json_bool(value.shows_unsupported_message) + << ",\"showsProgress\":" << json_bool(value.shows_progress) + << ",\"mutatesGridState\":" << json_bool(value.mutates_grid_state) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_share_file_args( int argc, char** argv, @@ -5305,6 +5459,10 @@ int main(int argc, char** argv) return plan_brush_operation(argc, argv); } + if (command == "plan-grid-operation") { + return plan_grid_operation(argc, argv); + } + if (command == "plan-share-file") { return plan_share_file(argc, argv); }