Extract grid UI operation planning
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
145
src/app_core/grid_ui.h
Normal file
145
src/app_core/grid_ui.h
Normal 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
|
||||
@@ -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<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;
|
||||
}
|
||||
load_heightmap_file(path, true);
|
||||
});
|
||||
};
|
||||
|
||||
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_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<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;
|
||||
}
|
||||
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<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] {
|
||||
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<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
|
||||
{
|
||||
assert(App::I->is_render_thread());
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
99
tests/app_core/grid_ui_tests.cpp
Normal file
99
tests/app_core/grid_ui_tests.cpp
Normal 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();
|
||||
}
|
||||
@@ -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<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(
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user