From 65e9fdf1b9cbb8433e9acd303e87f865d6e21a2a Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 12:43:00 +0200 Subject: [PATCH] Centralize quick and grid UI bridges --- cmake/PanoPainterSources.cmake | 4 + docs/modernization/build-inventory.md | 10 ++ docs/modernization/debt.md | 4 +- docs/modernization/roadmap.md | 10 +- src/app_core/grid_ui.h | 64 ++++++++ src/legacy_grid_ui_services.cpp | 101 +++++++++++++ src/legacy_grid_ui_services.h | 14 ++ src/legacy_quick_ui_services.cpp | 207 ++++++++++++++++++++++++++ src/legacy_quick_ui_services.h | 14 ++ src/node_panel_grid.cpp | 67 +++------ src/node_panel_quick.cpp | 201 +------------------------ tests/app_core/grid_ui_tests.cpp | 130 ++++++++++++++++ 12 files changed, 574 insertions(+), 252 deletions(-) create mode 100644 src/legacy_grid_ui_services.cpp create mode 100644 src/legacy_grid_ui_services.h create mode 100644 src/legacy_quick_ui_services.cpp create mode 100644 src/legacy_quick_ui_services.h diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index a2b31f6..2fb3433 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -90,6 +90,10 @@ set(PP_PANOPAINTER_UI_SOURCES src/legacy_brush_ui_services.h src/legacy_document_animation_services.cpp 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_canvas.cpp src/node_changelog.cpp diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index c2887f5..76e8f41 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -185,6 +185,16 @@ Known local toolchain state: legacy `Brush`, `Canvas::I`, image load/save, `NodePanelBrush`, `NodePanelStroke`, quick/color refreshes, and the temporary `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 `pp_assets`, attaches it to `pp_document`, and is covered by `pano_cli_simulate_image_import_smoke`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 04ec4e8..0df8e5e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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-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-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-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-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; 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-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 | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index f81441d..1d51569 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -570,8 +570,10 @@ dispatch through `DocumentExportMenuServices` in the shared app-shell bridge before legacy export dialogs and renderer/video execution 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. +commit used by the live grid panel. Grid execution now dispatches through +`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 clear-history availability used by toolbar buttons and canvas shortcuts; live 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 slot selection versus popup opening, plus quick mini-state restore/reset validation used by the live quick panel. Quick-panel execution now dispatches -through `QuickUiServices` before the legacy `Brush`, color picker, stroke -preview, and preset popup adapter continues. +through `QuickUiServices` in `src/legacy_quick_ui_services.*` before the legacy +`Brush`, color picker, stroke preview, and preset popup adapter continues. `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 already-visible no-ops, panel chrome metadata, shortcuts, camera reset, diff --git a/src/app_core/grid_ui.h b/src/app_core/grid_ui.h index 934eac5..77c13c9 100644 --- a/src/app_core/grid_ui.h +++ b/src/app_core/grid_ui.h @@ -35,6 +35,17 @@ struct GridUiPlan { 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 { if (texture_resolution <= 0 || texture_resolution > 16384) { @@ -142,4 +153,57 @@ struct GridUiPlan { 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 diff --git a/src/legacy_grid_ui_services.cpp b/src/legacy_grid_ui_services.cpp new file mode 100644 index 0000000..0b6f86c --- /dev/null +++ b/src/legacy_grid_ui_services.cpp @@ -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(); + 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 diff --git a/src/legacy_grid_ui_services.h b/src/legacy_grid_ui_services.h new file mode 100644 index 0000000..d3ebf1f --- /dev/null +++ b/src/legacy_grid_ui_services.h @@ -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 diff --git a/src/legacy_quick_ui_services.cpp b/src/legacy_quick_ui_services.cpp new file mode 100644 index 0000000..af9597a --- /dev/null +++ b/src/legacy_quick_ui_services.cpp @@ -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(panel_.m_button_brushes.size()); i++) + { + auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); + b->m_brush = restore_state_->brushes[i]; + b->draw_stroke(); + auto c = static_cast(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(panel_.m_button_brushes.size()); i++) + { + auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); + b->m_brush = std::make_shared(); + b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png"); + b->draw_stroke(); + } + static_cast(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1); + static_cast(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1); + static_cast(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(); + 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& b) { + auto pr = static_cast(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(); + 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(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 diff --git a/src/legacy_quick_ui_services.h b/src/legacy_quick_ui_services.h new file mode 100644 index 0000000..c66c99b --- /dev/null +++ b/src/legacy_quick_ui_services.h @@ -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 diff --git a/src/node_panel_grid.cpp b/src/node_panel_grid.cpp index afdaf71..b007312 100644 --- a/src/node_panel_grid.cpp +++ b/src/node_panel_grid.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "app_core/grid_ui.h" +#include "legacy_grid_ui_services.h" #include "log.h" #include "node_panel_grid.h" #include "canvas.h" @@ -79,22 +80,17 @@ 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) { - load_heightmap_file(path, true); - }); + const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan); + if (!status.ok()) + LOG("Grid heightmap pick action failed: %s", status.message); }; m_hm_clear->on_click = [this](Node*) { const auto plan = pp::app::plan_grid_heightmap_clear(static_cast(m_hm_image.data())); - if (!plan.clears_heightmap) - return; - m_hm_plane.create(1, 1, 100 * get_resolution()); - m_hm_image.destroy(); - m_hm_preview->tex.reset(); - m_hm_preview->SetHeight(0); + const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan); + if (!status.ok()) + LOG("Grid heightmap clear action failed: %s", status.message); }; m_hm_reload->on_click = [this](Node*) @@ -112,32 +108,16 @@ void NodePanelGrid::init_controls() 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(); - bake_uvs(); - m_hm_shading->set_index(3); - m_shade_mode = ShadeMode::Textured; - }).detach(); - } + const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan.value()); + if (!status.ok()) + LOG("Grid lightmap render action failed: %s", status.message); }; 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); - if (plan.updates_ground_opacity) - m_groud_opacity->set_value(0); + const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan); + if (!status.ok()) + LOG("Grid heightmap commit action failed: %s", status.message); }; m_hm_texres->on_select = [this](Node*, int index) { int texres = get_texres(); @@ -227,23 +207,10 @@ bool NodePanelGrid::load_heightmap_file(const std::string& path, bool raise_grou if (!plan) return false; - Image img; - if (!img.load_file(plan.value().path)) - return false; - - m_file_path = plan.value().path; - m_hm_image = img.resize(128, 128); - m_hm_preview->tex = std::make_shared(); - m_hm_preview->tex->create(m_hm_image); - m_hm_preview->tex->create_mipmaps(); - auto sz = m_hm_preview->tex->size(); - m_hm_preview->SetAspectRatio(sz.x / sz.y); - m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height()); - m_hm_preview->SetHeight(100); - if (plan.value().updates_ground_opacity && m_groud_opacity->get_value() == 0.f) - m_groud_opacity->set_value(1.f); - m_rt_dirty = true; - return true; + const auto status = pp::panopainter::execute_legacy_grid_ui_plan(*this, plan.value()); + if (!status.ok()) + LOG("Grid heightmap load action failed: %s", status.message); + return status.ok(); } void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const diff --git a/src/node_panel_quick.cpp b/src/node_panel_quick.cpp index 62c270a..6081dc8 100644 --- a/src/node_panel_quick.cpp +++ b/src/node_panel_quick.cpp @@ -1,198 +1,11 @@ #include "pch.h" #include "app_core/quick_ui.h" +#include "legacy_quick_ui_services.h" #include "node_panel_quick.h" #include "node_stroke_preview.h" #include "node_image.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(panel_.m_button_brushes.size()); i++) - { - auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); - b->m_brush = restore_state_->brushes[i]; - b->draw_stroke(); - auto c = static_cast(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(panel_.m_button_brushes.size()); i++) - { - auto b = static_cast(panel_.m_button_brushes[i]->m_children[0].get()); - b->m_brush = std::make_shared(); - b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png"); - b->draw_stroke(); - } - static_cast(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1); - static_cast(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1); - static_cast(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(); - 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& b) { - auto pr = static_cast(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(); - 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(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 { return new this_class; @@ -278,8 +91,7 @@ void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false if (!plan) return; - LegacyQuickUiServices services(*this, &state); - const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value(), &state); if (!status.ok()) LOG("Quick restore action failed: %s", status.message); } @@ -290,8 +102,7 @@ void NodePanelQuick::reset_state(bool fire_event /*= false*/) if (!plan) return; - LegacyQuickUiServices services(*this); - const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value()); if (!status.ok()) LOG("Quick reset action failed: %s", status.message); } @@ -400,8 +211,7 @@ void NodePanelQuick::handle_button_brush_click(Node* button) if (!plan) return; - LegacyQuickUiServices services(*this); - const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value()); if (!status.ok()) LOG("Quick brush action failed: %s", status.message); } @@ -418,8 +228,7 @@ void NodePanelQuick::handle_button_color_click(Node* target) if (!plan) return; - LegacyQuickUiServices services(*this); - const auto status = pp::app::execute_quick_ui_plan(plan.value(), services); + const auto status = pp::panopainter::execute_legacy_quick_ui_plan(*this, plan.value()); if (!status.ok()) LOG("Quick color action failed: %s", status.message); } diff --git a/tests/app_core/grid_ui_tests.cpp b/tests/app_core/grid_ui_tests.cpp index 02cd75c..1aa7fc7 100644 --- a/tests/app_core/grid_ui_tests.cpp +++ b/tests/app_core/grid_ui_tests.cpp @@ -1,8 +1,67 @@ #include "app_core/grid_ui.h" #include "test_harness.h" +#include + 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) { 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); } +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 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("lightmap render validates capabilities and limits", lightmap_render_validates_capabilities_and_limits); 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(); }