Centralize quick and grid UI bridges

This commit is contained in:
2026-06-04 12:43:00 +02:00
parent bd2ee54617
commit 65e9fdf1b9
12 changed files with 574 additions and 252 deletions

View File

@@ -90,6 +90,10 @@ set(PP_PANOPAINTER_UI_SOURCES
src/legacy_brush_ui_services.h src/legacy_brush_ui_services.h
src/legacy_document_animation_services.cpp src/legacy_document_animation_services.cpp
src/legacy_document_animation_services.h src/legacy_document_animation_services.h
src/legacy_grid_ui_services.cpp
src/legacy_grid_ui_services.h
src/legacy_quick_ui_services.cpp
src/legacy_quick_ui_services.h
src/node_about.cpp src/node_about.cpp
src/node_canvas.cpp src/node_canvas.cpp
src/node_changelog.cpp src/node_changelog.cpp

View File

@@ -185,6 +185,16 @@ Known local toolchain state:
legacy `Brush`, `Canvas::I`, image load/save, `NodePanelBrush`, legacy `Brush`, `Canvas::I`, image load/save, `NodePanelBrush`,
`NodePanelStroke`, quick/color refreshes, and the temporary `NodePanelStroke`, quick/color refreshes, and the temporary
`NodePanelBrush` friend adapter remain tracked by `DEBT-0023`. `NodePanelBrush` friend adapter remain tracked by `DEBT-0023`.
- `src/legacy_grid_ui_services.*` is the current UI-shell bridge for grid
heightmap picker/load/reload/clear, lightmap render, and heightmap commit
execution. It keeps those live paths on the `pp_app_core` contracts while
legacy image loading, OpenGL texture updates, nanort baking/progress, and
`Canvas::draw_objects` execution remain tracked by `DEBT-0024`.
- `src/legacy_quick_ui_services.*` is the current UI-shell bridge for quick
brush/color slot selection, popup routing, mini-state restore, and mini-state
reset execution. It keeps those live paths on the `pp_app_core` contracts
while legacy quick widgets, brush previews, color picker state, and preset
popup execution remain tracked by `DEBT-0025`.
- `pano_cli simulate-image-import` decodes an embedded tiny PNG through - `pano_cli simulate-image-import` decodes an embedded tiny PNG through
`pp_assets`, attaches it to `pp_document`, and is covered by `pp_assets`, attaches it to `pp_document`, and is covered by
`pano_cli_simulate_image_import_smoke`. `pano_cli_simulate_image_import_smoke`.

View File

@@ -41,8 +41,8 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch and layer panel operation planning/execution dispatch now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | 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 planning/execution dispatch and layer panel operation planning/execution dispatch now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | 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, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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 select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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 select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*`, but the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses a temporary `NodePanelBrush` friend adapter to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset 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`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter or `NodePanelBrush` friend access | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*`, but the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses a temporary `NodePanelBrush` friend adapter to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset 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`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter or `NodePanelBrush` friend access |
| 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 | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | 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 |
| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary; live execution is centralized in `src/legacy_quick_ui_services.*`, but the bridge still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter |
| DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter | | DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter |
| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, active-state planning/execution dispatch, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, and live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, but the bridge still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter | | DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, active-state planning/execution dispatch, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, and live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, but the bridge still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter |
| DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter | | DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter |

View File

@@ -570,8 +570,10 @@ dispatch through `DocumentExportMenuServices` in the shared app-shell bridge
before legacy export dialogs and renderer/video execution continue. before legacy export dialogs and renderer/video execution continue.
`pano_cli plan-grid-operation` exposes app-core planning for grid heightmap `pano_cli plan-grid-operation` exposes app-core planning for grid heightmap
pick/load/reload/clear, lightmap render capability/limit checks, and 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 commit used by the live grid panel. Grid execution now dispatches through
updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. `GridUiServices` in `src/legacy_grid_ui_services.*` before legacy image loading,
OpenGL texture updates, nanort lightmap baking, and `Canvas::draw_objects`
execution continue.
`pano_cli plan-history-operation` exposes app-core planning for undo, redo, and `pano_cli plan-history-operation` exposes app-core planning for undo, redo, and
clear-history availability used by toolbar buttons and canvas shortcuts; live clear-history availability used by toolbar buttons and canvas shortcuts; live
toolbar and canvas-hotkey execution now dispatch through a shared app-shell toolbar and canvas-hotkey execution now dispatch through a shared app-shell
@@ -588,8 +590,8 @@ history/canvas adapters, and settings UI execution continue.
`pano_cli plan-quick-operation` exposes app-core planning for quick brush/color `pano_cli plan-quick-operation` exposes app-core planning for quick brush/color
slot selection versus popup opening, plus quick mini-state restore/reset slot selection versus popup opening, plus quick mini-state restore/reset
validation used by the live quick panel. Quick-panel execution now dispatches validation used by the live quick panel. Quick-panel execution now dispatches
through `QuickUiServices` before the legacy `Brush`, color picker, stroke through `QuickUiServices` in `src/legacy_quick_ui_services.*` before the legacy
preview, and preset popup adapter continues. `Brush`, color picker, stroke preview, and preset popup adapter continues.
`pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core `pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core
planning for top-level Tools commands and floating-panel requests, including planning for top-level Tools commands and floating-panel requests, including
already-visible no-ops, panel chrome metadata, shortcuts, camera reset, already-visible no-ops, panel chrome metadata, shortcuts, camera reset,

View File

@@ -35,6 +35,17 @@ struct GridUiPlan {
bool mutates_grid_state = false; bool mutates_grid_state = false;
}; };
class GridUiServices {
public:
virtual ~GridUiServices() = default;
virtual void request_heightmap_pick() = 0;
virtual pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) = 0;
virtual void clear_heightmap(bool updates_preview) = 0;
virtual void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) = 0;
virtual void commit_heightmap(bool updates_ground_opacity) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept [[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
{ {
if (texture_resolution <= 0 || texture_resolution > 16384) { if (texture_resolution <= 0 || texture_resolution > 16384) {
@@ -142,4 +153,57 @@ struct GridUiPlan {
return plan; return plan;
} }
[[nodiscard]] inline pp::foundation::Status execute_grid_ui_plan(
const GridUiPlan& plan,
GridUiServices& services)
{
switch (plan.operation) {
case GridUiOperation::request_heightmap_pick:
if (!plan.opens_picker) {
return pp::foundation::Status::invalid_argument("grid heightmap pick plan must open a picker");
}
services.request_heightmap_pick();
return pp::foundation::Status::success();
case GridUiOperation::load_heightmap:
case GridUiOperation::reload_heightmap:
if (!plan.loads_heightmap || plan.path.empty()) {
return pp::foundation::Status::invalid_argument("grid heightmap load plan must provide a path");
}
return services.load_heightmap(plan.path, plan.updates_ground_opacity);
case GridUiOperation::clear_heightmap:
if (!plan.clears_heightmap) {
return pp::foundation::Status::invalid_argument("grid heightmap clear plan must clear heightmap state");
}
services.clear_heightmap(plan.updates_preview);
return pp::foundation::Status::success();
case GridUiOperation::render_lightmap:
{
const auto texture_status = validate_grid_texture_resolution(plan.texture_resolution);
if (!texture_status.ok()) {
return texture_status;
}
const auto sample_status = validate_grid_lightmap_samples(plan.sample_count);
if (!sample_status.ok()) {
return sample_status;
}
if (!plan.shows_unsupported_message && !plan.renders_lightmap) {
return pp::foundation::Status::success();
}
services.render_lightmap(plan.shows_unsupported_message, plan.renders_lightmap);
return pp::foundation::Status::success();
}
case GridUiOperation::commit_heightmap:
if (plan.commits_heightmap) {
services.commit_heightmap(plan.updates_ground_opacity);
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown grid UI operation");
}
} // namespace pp::app } // namespace pp::app

View File

@@ -0,0 +1,101 @@
#include "pch.h"
#include "legacy_grid_ui_services.h"
#include "app.h"
#include "canvas.h"
#include "image.h"
#include "node_panel_grid.h"
namespace pp::panopainter {
namespace {
class LegacyGridUiServices final : public pp::app::GridUiServices {
public:
explicit LegacyGridUiServices(NodePanelGrid& panel) noexcept
: panel_(panel)
{
}
void request_heightmap_pick() override
{
auto* panel = &panel_;
App::I->pick_image([panel](std::string path) {
panel->load_heightmap_file(path, true);
});
}
pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) override
{
Image img;
if (!img.load_file(std::string(path)))
return pp::foundation::Status::invalid_argument("heightmap image could not be loaded");
panel_.m_file_path = std::string(path);
panel_.m_hm_image = img.resize(128, 128);
panel_.m_hm_preview->tex = std::make_shared<Texture2D>();
panel_.m_hm_preview->tex->create(panel_.m_hm_image);
panel_.m_hm_preview->tex->create_mipmaps();
auto sz = panel_.m_hm_preview->tex->size();
panel_.m_hm_preview->SetAspectRatio(sz.x / sz.y);
panel_.m_hm_plane.create(1, 1, panel_.m_hm_image, panel_.get_resolution(), panel_.get_height());
panel_.m_hm_preview->SetHeight(100);
if (raise_ground_opacity && panel_.m_groud_opacity->get_value() == 0.f)
panel_.m_groud_opacity->set_value(1.f);
panel_.m_rt_dirty = true;
return pp::foundation::Status::success();
}
void clear_heightmap(bool updates_preview) override
{
panel_.m_hm_plane.create(1, 1, 100 * panel_.get_resolution());
panel_.m_hm_image.destroy();
panel_.m_hm_preview->tex.reset();
panel_.m_hm_preview->SetHeight(0);
}
void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) override
{
if (shows_unsupported_message)
{
App::I->message_box("Rendering failed",
"Your hardware does not support lightmap rendering.");
return;
}
if (!renders_lightmap)
return;
auto* panel = &panel_;
std::thread([panel] {
BT_SetTerminate();
panel->bake_uvs();
panel->m_hm_shading->set_index(3);
panel->m_shade_mode = NodePanelGrid::ShadeMode::Textured;
}).detach();
}
void commit_heightmap(bool updates_ground_opacity) override
{
Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) {
panel_.draw_heightmap(proj, camera, true);
}, Canvas::I->layer().m_frame_index, true);
if (updates_ground_opacity)
panel_.m_groud_opacity->set_value(0);
}
private:
NodePanelGrid& panel_;
};
} // namespace
pp::foundation::Status execute_legacy_grid_ui_plan(
NodePanelGrid& panel,
const pp::app::GridUiPlan& plan)
{
LegacyGridUiServices services(panel);
return pp::app::execute_grid_ui_plan(plan, services);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,14 @@
#pragma once
#include "app_core/grid_ui.h"
#include "foundation/result.h"
class NodePanelGrid;
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_grid_ui_plan(
NodePanelGrid& panel,
const pp::app::GridUiPlan& plan);
} // namespace pp::panopainter

View File

@@ -0,0 +1,207 @@
#include "pch.h"
#include "legacy_quick_ui_services.h"
#include "app.h"
#include "node_image.h"
#include "node_stroke_preview.h"
namespace pp::panopainter {
namespace {
class LegacyQuickUiServices final : public pp::app::QuickUiServices {
public:
LegacyQuickUiServices(NodePanelQuick& panel, const NodePanelQuick::MiniState* restore_state = nullptr) noexcept
: panel_(panel)
, restore_state_(restore_state)
{
}
void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
panel_.set_selected_brush_index(slot_index, fire_event);
return;
}
panel_.set_selected_color_index(slot_index, fire_event);
}
void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
open_brush_popup(slot_index);
return;
}
open_color_picker(slot_index);
}
void restore_state(int brush_index, int color_index, bool fire_event) override
{
if (!restore_state_)
return;
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = restore_state_->brushes[i];
b->draw_stroke();
auto c = static_cast<NodeBorder*>(panel_.m_button_colors[i]->m_children[0].get());
c->m_color = restore_state_->colors[i];
}
panel_.set_selected_color_index(color_index, fire_event);
panel_.set_selected_brush_index(brush_index, fire_event);
}
void reset_state(bool fire_event) override
{
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = std::make_shared<Brush>();
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
b->draw_stroke();
}
static_cast<NodeBorder*>(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
panel_.set_selected_brush_index(0, fire_event);
panel_.set_selected_color_index(0, fire_event);
}
private:
void open_brush_popup(int slot_index)
{
auto button = panel_.m_button_brushes[slot_index];
if (!button)
return;
auto popup = App::I->presets;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
popup->SetWidth(350);
popup->SetHeight(glm::max(hh, 400.f));
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
popup->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
popup->on_brush_changed = [panel, button](Node*, std::shared_ptr<Brush>& b) {
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
*pr->m_brush = *b;
pr->m_brush->load();
pr->draw_stroke();
if (panel->on_brush_change)
panel->on_brush_change(button, pr->m_brush);
};
}
void open_color_picker(int slot_index)
{
auto target = panel_.m_button_colors[slot_index];
if (!target)
return;
auto popup = panel_.m_picker;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
panel_.m_picker->set_color(c->m_color);
panel_.m_picker->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
panel_.m_picker->on_color_change = [panel, c](Node*, glm::vec3 rgb) {
c->m_color = glm::vec4(rgb, 1.f);
if (panel->on_color_change)
panel->on_color_change(panel, rgb);
};
}
NodePanelQuick& panel_;
const NodePanelQuick::MiniState* restore_state_ = nullptr;
};
} // namespace
pp::foundation::Status execute_legacy_quick_ui_plan(
NodePanelQuick& panel,
const pp::app::QuickUiPlan& plan,
const NodePanelQuick::MiniState* restore_state)
{
LegacyQuickUiServices services(panel, restore_state);
return pp::app::execute_quick_ui_plan(plan, services);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,14 @@
#pragma once
#include "app_core/quick_ui.h"
#include "foundation/result.h"
#include "node_panel_quick.h"
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_quick_ui_plan(
NodePanelQuick& panel,
const pp::app::QuickUiPlan& plan,
const NodePanelQuick::MiniState* restore_state = nullptr);
} // namespace pp::panopainter

View File

@@ -1,5 +1,6 @@
#include "pch.h" #include "pch.h"
#include "app_core/grid_ui.h" #include "app_core/grid_ui.h"
#include "legacy_grid_ui_services.h"
#include "log.h" #include "log.h"
#include "node_panel_grid.h" #include "node_panel_grid.h"
#include "canvas.h" #include "canvas.h"
@@ -79,22 +80,17 @@ 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(); const auto plan = pp::app::plan_grid_heightmap_pick();
if (!plan.opens_picker) const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan);
return; if (!status.ok())
App::I->pick_image([this](std::string path) { LOG("Grid heightmap pick action failed: %s", status.message);
load_heightmap_file(path, 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())); const auto plan = pp::app::plan_grid_heightmap_clear(static_cast<bool>(m_hm_image.data()));
if (!plan.clears_heightmap) const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan);
return; if (!status.ok())
m_hm_plane.create(1, 1, 100 * get_resolution()); LOG("Grid heightmap clear action failed: %s", status.message);
m_hm_image.destroy();
m_hm_preview->tex.reset();
m_hm_preview->SetHeight(0);
}; };
m_hm_reload->on_click = [this](Node*) m_hm_reload->on_click = [this](Node*)
@@ -112,32 +108,16 @@ void NodePanelGrid::init_controls()
get_samples()); get_samples());
if (!plan) if (!plan)
return; return;
if (plan.value().shows_unsupported_message) const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan.value());
{ if (!status.ok())
App::I->message_box("Rendering failed", LOG("Grid lightmap render action failed: %s", status.message);
"Your hardware does not support lightmap rendering.");
return;
}
if (plan.value().renders_lightmap)
{
std::thread([this] {
BT_SetTerminate();
bake_uvs();
m_hm_shading->set_index(3);
m_shade_mode = ShadeMode::Textured;
}).detach();
}
}; };
m_commit->on_click = [this](Node*) m_commit->on_click = [this](Node*)
{ {
const auto plan = pp::app::plan_grid_heightmap_commit(Canvas::I != nullptr); const auto plan = pp::app::plan_grid_heightmap_commit(Canvas::I != nullptr);
if (!plan.commits_heightmap) const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan);
return; if (!status.ok())
Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) { LOG("Grid heightmap commit action failed: %s", status.message);
draw_heightmap(proj, camera, true);
}, Canvas::I->layer().m_frame_index, true);
if (plan.updates_ground_opacity)
m_groud_opacity->set_value(0);
}; };
m_hm_texres->on_select = [this](Node*, int index) { m_hm_texres->on_select = [this](Node*, int index) {
int texres = get_texres(); int texres = get_texres();
@@ -227,23 +207,10 @@ bool NodePanelGrid::load_heightmap_file(const std::string& path, bool raise_grou
if (!plan) if (!plan)
return false; return false;
Image img; const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan.value());
if (!img.load_file(plan.value().path)) if (!status.ok())
return false; LOG("Grid heightmap load action failed: %s", status.message);
return status.ok();
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

View File

@@ -1,198 +1,11 @@
#include "pch.h" #include "pch.h"
#include "app_core/quick_ui.h" #include "app_core/quick_ui.h"
#include "legacy_quick_ui_services.h"
#include "node_panel_quick.h" #include "node_panel_quick.h"
#include "node_stroke_preview.h" #include "node_stroke_preview.h"
#include "node_image.h" #include "node_image.h"
#include "app.h" #include "app.h"
namespace {
class LegacyQuickUiServices final : public pp::app::QuickUiServices {
public:
LegacyQuickUiServices(NodePanelQuick& panel, const NodePanelQuick::MiniState* restore_state = nullptr) noexcept
: panel_(panel)
, restore_state_(restore_state)
{
}
void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
panel_.set_selected_brush_index(slot_index, fire_event);
return;
}
panel_.set_selected_color_index(slot_index, fire_event);
}
void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
open_brush_popup(slot_index);
return;
}
open_color_picker(slot_index);
}
void restore_state(int brush_index, int color_index, bool fire_event) override
{
if (!restore_state_)
return;
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = restore_state_->brushes[i];
b->draw_stroke();
auto c = static_cast<NodeBorder*>(panel_.m_button_colors[i]->m_children[0].get());
c->m_color = restore_state_->colors[i];
}
panel_.set_selected_color_index(color_index, fire_event);
panel_.set_selected_brush_index(brush_index, fire_event);
}
void reset_state(bool fire_event) override
{
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = std::make_shared<Brush>();
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
b->draw_stroke();
}
static_cast<NodeBorder*>(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
panel_.set_selected_brush_index(0, fire_event);
panel_.set_selected_color_index(0, fire_event);
}
private:
void open_brush_popup(int slot_index)
{
auto button = panel_.m_button_brushes[slot_index];
if (!button)
return;
auto popup = App::I->presets;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
popup->SetWidth(350);
popup->SetHeight(glm::max(hh, 400.f));
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
popup->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
popup->on_brush_changed = [panel, button](Node*, std::shared_ptr<Brush>& b) {
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
*pr->m_brush = *b;
pr->m_brush->load();
pr->draw_stroke();
if (panel->on_brush_change)
panel->on_brush_change(button, pr->m_brush);
};
}
void open_color_picker(int slot_index)
{
auto target = panel_.m_button_colors[slot_index];
if (!target)
return;
auto popup = panel_.m_picker;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
panel_.m_picker->set_color(c->m_color);
panel_.m_picker->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
panel_.m_picker->on_color_change = [panel, c](Node*, glm::vec3 rgb) {
c->m_color = glm::vec4(rgb, 1.f);
if (panel->on_color_change)
panel->on_color_change(panel, rgb);
};
}
NodePanelQuick& panel_;
const NodePanelQuick::MiniState* restore_state_ = nullptr;
};
} // namespace
Node* NodePanelQuick::clone_instantiate() const Node* NodePanelQuick::clone_instantiate() const
{ {
return new this_class; return new this_class;
@@ -278,8 +91,7 @@ void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false
if (!plan) if (!plan)
return; return;
LegacyQuickUiServices services(*this, &state); const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value(), &state);
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok()) if (!status.ok())
LOG("Quick restore action failed: %s", status.message); LOG("Quick restore action failed: %s", status.message);
} }
@@ -290,8 +102,7 @@ void NodePanelQuick::reset_state(bool fire_event /*= false*/)
if (!plan) if (!plan)
return; return;
LegacyQuickUiServices services(*this); const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value());
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok()) if (!status.ok())
LOG("Quick reset action failed: %s", status.message); LOG("Quick reset action failed: %s", status.message);
} }
@@ -400,8 +211,7 @@ void NodePanelQuick::handle_button_brush_click(Node* button)
if (!plan) if (!plan)
return; return;
LegacyQuickUiServices services(*this); const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value());
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok()) if (!status.ok())
LOG("Quick brush action failed: %s", status.message); LOG("Quick brush action failed: %s", status.message);
} }
@@ -418,8 +228,7 @@ void NodePanelQuick::handle_button_color_click(Node* target)
if (!plan) if (!plan)
return; return;
LegacyQuickUiServices services(*this); const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value());
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok()) if (!status.ok())
LOG("Quick color action failed: %s", status.message); LOG("Quick color action failed: %s", status.message);
} }

View File

@@ -1,8 +1,67 @@
#include "app_core/grid_ui.h" #include "app_core/grid_ui.h"
#include "test_harness.h" #include "test_harness.h"
#include <string>
namespace { namespace {
class FakeGridUiServices final : public pp::app::GridUiServices {
public:
void request_heightmap_pick() override
{
picks += 1;
call_order += "pick;";
}
pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) override
{
loads += 1;
last_path = std::string(path);
last_raise_ground_opacity = raise_ground_opacity;
call_order += "load;";
if (fail_load) {
return pp::foundation::Status::invalid_argument("fake load failed");
}
return pp::foundation::Status::success();
}
void clear_heightmap(bool updates_preview) override
{
clears += 1;
last_updates_preview = updates_preview;
call_order += "clear;";
}
void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) override
{
renders += 1;
last_shows_unsupported_message = shows_unsupported_message;
last_renders_lightmap = renders_lightmap;
call_order += "render;";
}
void commit_heightmap(bool updates_ground_opacity) override
{
commits += 1;
last_updates_ground_opacity = updates_ground_opacity;
call_order += "commit;";
}
int picks = 0;
int loads = 0;
int clears = 0;
int renders = 0;
int commits = 0;
bool fail_load = false;
bool last_raise_ground_opacity = false;
bool last_updates_preview = false;
bool last_shows_unsupported_message = false;
bool last_renders_lightmap = false;
bool last_updates_ground_opacity = false;
std::string last_path;
std::string call_order;
};
void heightmap_load_reload_and_clear_plan_state(pp::tests::Harness& harness) void heightmap_load_reload_and_clear_plan_state(pp::tests::Harness& harness)
{ {
const auto pick = pp::app::plan_grid_heightmap_pick(); const auto pick = pp::app::plan_grid_heightmap_pick();
@@ -87,6 +146,75 @@ void commit_plan_requires_canvas(pp::tests::Harness& harness)
PP_EXPECT(harness, !headless.mutates_grid_state); PP_EXPECT(harness, !headless.mutates_grid_state);
} }
void executor_dispatches_grid_operations(pp::tests::Harness& harness)
{
FakeGridUiServices services;
PP_EXPECT(harness, pp::app::execute_grid_ui_plan(pp::app::plan_grid_heightmap_pick(), services).ok());
const auto load = pp::app::plan_grid_heightmap_load("D:/Paint/height.png");
PP_EXPECT(harness, load);
if (load) {
PP_EXPECT(harness, pp::app::execute_grid_ui_plan(load.value(), services).ok());
}
const auto clear = pp::app::plan_grid_heightmap_clear(true);
PP_EXPECT(harness, pp::app::execute_grid_ui_plan(clear, services).ok());
const auto render = pp::app::plan_grid_lightmap_render(true, true, false, 1024, 32);
PP_EXPECT(harness, render);
if (render) {
PP_EXPECT(harness, pp::app::execute_grid_ui_plan(render.value(), services).ok());
}
const auto commit = pp::app::plan_grid_heightmap_commit(true);
PP_EXPECT(harness, pp::app::execute_grid_ui_plan(commit, services).ok());
PP_EXPECT(harness, services.picks == 1);
PP_EXPECT(harness, services.loads == 1);
PP_EXPECT(harness, services.clears == 1);
PP_EXPECT(harness, services.renders == 1);
PP_EXPECT(harness, services.commits == 1);
PP_EXPECT(harness, services.last_path == "D:/Paint/height.png");
PP_EXPECT(harness, services.last_raise_ground_opacity);
PP_EXPECT(harness, services.last_updates_preview);
PP_EXPECT(harness, services.last_renders_lightmap);
PP_EXPECT(harness, services.last_updates_ground_opacity);
PP_EXPECT(harness, services.call_order == "pick;load;clear;render;commit;");
}
void executor_rejects_malformed_grid_plans(pp::tests::Harness& harness)
{
FakeGridUiServices services;
pp::app::GridUiPlan pick;
pick.operation = pp::app::GridUiOperation::request_heightmap_pick;
PP_EXPECT(harness, !pp::app::execute_grid_ui_plan(pick, services).ok());
pp::app::GridUiPlan load;
load.operation = pp::app::GridUiOperation::load_heightmap;
load.loads_heightmap = true;
PP_EXPECT(harness, !pp::app::execute_grid_ui_plan(load, services).ok());
pp::app::GridUiPlan render;
render.operation = pp::app::GridUiOperation::render_lightmap;
render.renders_lightmap = true;
render.texture_resolution = 0;
render.sample_count = 32;
PP_EXPECT(harness, !pp::app::execute_grid_ui_plan(render, services).ok());
const auto failing_load = pp::app::plan_grid_heightmap_load("D:/Paint/missing.png");
PP_EXPECT(harness, failing_load);
if (failing_load) {
services.fail_load = true;
PP_EXPECT(harness, !pp::app::execute_grid_ui_plan(failing_load.value(), services).ok());
}
PP_EXPECT(harness, services.picks == 0);
PP_EXPECT(harness, services.renders == 0);
PP_EXPECT(harness, services.loads == 1);
}
} // namespace } // namespace
int main() int main()
@@ -95,5 +223,7 @@ int main()
harness.run("heightmap load reload and clear plan state", heightmap_load_reload_and_clear_plan_state); 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("lightmap render validates capabilities and limits", lightmap_render_validates_capabilities_and_limits);
harness.run("commit plan requires canvas", commit_plan_requires_canvas); harness.run("commit plan requires canvas", commit_plan_requires_canvas);
harness.run("executor dispatches grid operations", executor_dispatches_grid_operations);
harness.run("executor rejects malformed grid plans", executor_rejects_malformed_grid_plans);
return harness.finish(); return harness.finish();
} }