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

View File

@@ -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`.

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

View File

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

View File

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

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 "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<bool>(m_hm_image.data()));
if (!plan.clears_heightmap)
return;
m_hm_plane.create(1, 1, 100 * get_resolution());
m_hm_image.destroy();
m_hm_preview->tex.reset();
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<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;
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

View File

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

View File

@@ -1,8 +1,67 @@
#include "app_core/grid_ui.h"
#include "test_harness.h"
#include <string>
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();
}