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_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")

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-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

View File

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

View File

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

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
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

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_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);
}