Extract grid UI operation planning

This commit is contained in:
2026-06-03 10:52:51 +02:00
parent a487b0ba48
commit 73fac0f8e4
9 changed files with 522 additions and 37 deletions

View File

@@ -235,7 +235,8 @@ add_library(pp_app_core STATIC
src/app_core/document_resize.h src/app_core/document_resize.h
src/app_core/document_route.cpp src/app_core/document_route.cpp
src/app_core/document_sharing.h 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 target_include_directories(pp_app_core
PUBLIC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -499,6 +499,10 @@ before legacy `Canvas`/`Layer` frame execution continues.
changes, tip/pattern/dual texture changes, preset brush replacement, and stroke changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel settings refreshes used by the live brush, quick, color, and floating panel
callbacks before legacy `Brush` mutation and resource loading continue. 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 `pp_platform_api` now owns a headless `PlatformServices` interface for
startup storage path preparation, clipboard text, cursor visibility, startup storage path preparation, clipboard text, cursor visibility,
virtual-keyboard visibility, UI-thread lifecycle hooks, render-context 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_bad_color`, and
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live `pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
brush/color/preset UI planning as JSON automation. 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 - `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
platform share execution. platform share execution.
- `pano_cli_plan_share_file_unsaved_smoke` and - `pano_cli_plan_share_file_unsaved_smoke` and

145
src/app_core/grid_ui.h Normal file
View File

@@ -0,0 +1,145 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
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<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
{
if (path.empty()) {
return pp::foundation::Result<GridUiPlan>::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<GridUiPlan>::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<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
{
auto plan = plan_grid_heightmap_load(path);
if (!plan) {
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
}
plan.value().operation = GridUiOperation::reload_heightmap;
plan.value().updates_ground_opacity = false;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> 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<GridUiPlan>::failure(texture_status);
}
const auto sample_status = validate_grid_lightmap_samples(sample_count);
if (!sample_status.ok()) {
return pp::foundation::Result<GridUiPlan>::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<GridUiPlan>::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<GridUiPlan>::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

View File

@@ -1,4 +1,5 @@
#include "pch.h" #include "pch.h"
#include "app_core/grid_ui.h"
#include "log.h" #include "log.h"
#include "node_panel_grid.h" #include "node_panel_grid.h"
#include "canvas.h" #include "canvas.h"
@@ -77,28 +78,19 @@ void NodePanelGrid::init_controls()
}; };
m_hm_load->on_click = [this](Node*) { 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) { App::I->pick_image([this](std::string path) {
Image img; load_heightmap_file(path, true);
if (img.load_file(path))
{
m_file_path = path;
m_hm_image = img.resize(128, 128);
m_hm_preview->tex = std::make_shared<Texture2D>();
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;
}
}); });
}; };
m_hm_clear->on_click = [this](Node*) m_hm_clear->on_click = [this](Node*)
{ {
const auto plan = pp::app::plan_grid_heightmap_clear(static_cast<bool>(m_hm_image.data()));
if (!plan.clears_heightmap)
return;
m_hm_plane.create(1, 1, 100 * get_resolution()); m_hm_plane.create(1, 1, 100 * get_resolution());
m_hm_image.destroy(); m_hm_image.destroy();
m_hm_preview->tex.reset(); m_hm_preview->tex.reset();
@@ -107,24 +99,26 @@ void NodePanelGrid::init_controls()
m_hm_reload->on_click = [this](Node*) m_hm_reload->on_click = [this](Node*)
{ {
Image img; load_heightmap_file(m_file_path, false);
if (img.load_file(m_file_path))
{
m_hm_image = img.resize(128, 128);
m_hm_preview->tex = std::make_shared<Texture2D>();
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;
}
}; };
m_render->on_click = [this](Node*) m_render->on_click = [this](Node*)
{ {
if (ShaderManager::ext_float32 || ShaderManager::ext_float16) const auto plan = pp::app::plan_grid_lightmap_render(
static_cast<bool>(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] { std::thread([this] {
BT_SetTerminate(); BT_SetTerminate();
@@ -133,17 +127,16 @@ void NodePanelGrid::init_controls()
m_shade_mode = ShadeMode::Textured; m_shade_mode = ShadeMode::Textured;
}).detach(); }).detach();
} }
else
{
App::I->message_box("Rendering failed",
"Your hardware does not support lightmap rendering.");
}
}; };
m_commit->on_click = [this](Node*) 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) { Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) {
draw_heightmap(proj, camera, true); draw_heightmap(proj, camera, true);
}, Canvas::I->layer().m_frame_index, true); }, Canvas::I->layer().m_frame_index, true);
if (plan.updates_ground_opacity)
m_groud_opacity->set_value(0); m_groud_opacity->set_value(0);
}; };
m_hm_texres->on_select = [this](Node*, int index) { m_hm_texres->on_select = [this](Node*, int index) {
@@ -226,6 +219,33 @@ float NodePanelGrid::get_offset() const
return glm::pow(m_groud_offset->get_value() - 0.5f, 3); 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<Texture2D>();
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 void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const
{ {
assert(App::I->is_render_thread()); assert(App::I->is_render_thread());

View File

@@ -86,6 +86,7 @@ public:
float get_resolution() const; float get_resolution() const;
float get_height() const; float get_height() const;
float get_offset() 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 draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const;
void bake_uvs(); void bake_uvs();
}; };

View File

@@ -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 set_tests_properties(pp_app_core_brush_ui_tests PROPERTIES
LABELS "app;paint;desktop-fast;fuzz") 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 add_executable(pp_app_core_document_route_tests
app_core/document_route_tests.cpp) app_core/document_route_tests.cpp)
target_link_libraries(pp_app_core_document_route_tests PRIVATE 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" LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) 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 add_test(NAME pano_cli_plan_share_file_unsaved_smoke
COMMAND pano_cli plan-share-file) COMMAND pano_cli plan-share-file)
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES

View File

@@ -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();
}

View File

@@ -11,6 +11,7 @@
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_sharing.h" #include "app_core/document_sharing.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "app_core/grid_ui.h"
#include "assets/image_format.h" #include "assets/image_format.h"
#include "assets/image_metadata.h" #include "assets/image_metadata.h"
#include "assets/image_pixels.h" #include "assets/image_pixels.h"
@@ -263,6 +264,17 @@ struct PlanBrushOperationArgs {
bool has_brush = true; 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 { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -592,6 +604,26 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
return "stroke-settings-changed"; 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 const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept
{ {
switch (decision) { 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-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-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-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-share-file [--path FILE]\n"
<< " plan-picked-path [--path FILE]\n" << " plan-picked-path [--path FILE]\n"
<< " plan-display-file [--path FILE]\n" << " plan-display-file [--path FILE]\n"
@@ -2885,6 +2918,127 @@ int plan_brush_operation(int argc, char** argv)
return 0; 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<pp::app::GridUiPlan> make_grid_operation_plan(const PlanGridOperationArgs& args)
{
if (args.kind == "pick") {
return pp::foundation::Result<pp::app::GridUiPlan>::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<pp::app::GridUiPlan>::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<pp::app::GridUiPlan>::success(
pp::app::plan_grid_heightmap_commit(args.has_canvas));
}
return pp::foundation::Result<pp::app::GridUiPlan>::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( pp::foundation::Status parse_plan_share_file_args(
int argc, int argc,
char** argv, char** argv,
@@ -5305,6 +5459,10 @@ int main(int argc, char** argv)
return plan_brush_operation(argc, argv); return plan_brush_operation(argc, argv);
} }
if (command == "plan-grid-operation") {
return plan_grid_operation(argc, argv);
}
if (command == "plan-share-file") { if (command == "plan-share-file") {
return plan_share_file(argc, argv); return plan_share_file(argc, argv);
} }