Add brush UI service boundary

This commit is contained in:
2026-06-03 13:28:50 +02:00
parent 6427f218e7
commit de9bca8bb5
5 changed files with 301 additions and 37 deletions

View File

@@ -40,7 +40,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing | | DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | | DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning now consumes pure `pp_app_core` through `NodePanelAnimation` and `pano_cli plan-animation-operation`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but live execution still mutates legacy `Canvas`/`Layer` frame state and animation playback state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by the document/app command boundary with legacy `Canvas`/`Layer`/UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning now consumes pure `pp_app_core` through `NodePanelAnimation` and `pano_cli plan-animation-operation`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but live execution still mutates legacy `Canvas`/`Layer` frame state and animation playback state directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline execution is owned by the document/app command boundary with legacy `Canvas`/`Layer`/UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, and the `BrushUiServices` boundary, but the live adapter still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings execution is owned by injected brush/app/asset/UI services with no legacy brush adapter |
| 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 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 now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | 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 app/brush/UI services with `NodePanelQuick` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | 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 app/brush/UI services with `NodePanelQuick` acting only as UI adapter |
| DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter 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 execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter 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 |

View File

@@ -503,7 +503,8 @@ before legacy `Canvas`/`Layer` frame execution continues.
`pano_cli plan-brush-operation` exposes app-core planning for brush color `pano_cli plan-brush-operation` exposes app-core planning for brush color
changes, tip/pattern/dual texture changes, preset brush replacement, and stroke changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel settings refreshes used by the live brush, quick, color, and floating panel
callbacks before legacy `Brush` mutation and resource loading continue. callbacks. Brush UI execution now dispatches through `BrushUiServices` before
the legacy `Brush`/panel adapter mutates brush state or loads brush resources.
`pano_cli plan-canvas-tool` exposes app-core planning for draw/erase/line, `pano_cli plan-canvas-tool` exposes app-core planning for draw/erase/line,
camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar
commands before legacy `Canvas` mode, pen picking, touch-lock, and transform commands before legacy `Canvas` mode, pen picking, touch-lock, and transform
@@ -1205,7 +1206,9 @@ Results:
expose live animation-panel planning as JSON automation. expose live animation-panel planning as JSON automation.
- `pp_app_core_brush_ui_tests` passed, covering brush color channel validation, - `pp_app_core_brush_ui_tests` passed, covering brush color channel validation,
invalid color rejection, texture-path validation, preset-brush availability, invalid color rejection, texture-path validation, preset-brush availability,
preserve-current-color intent, and stroke-settings refresh intent. preserve-current-color intent, stroke-settings refresh intent, service
dispatch ordering, texture/preset execution payloads, and invalid execution
payload rejection.
- `pano_cli_plan_brush_operation_color_smoke`, - `pano_cli_plan_brush_operation_color_smoke`,
`pano_cli_plan_brush_operation_texture_smoke`, `pano_cli_plan_brush_operation_texture_smoke`,
`pano_cli_plan_brush_operation_preset_smoke`, `pano_cli_plan_brush_operation_preset_smoke`,

View File

@@ -37,6 +37,16 @@ struct BrushUiPlan {
bool update_brush_ui = false; bool update_brush_ui = false;
}; };
class BrushUiServices {
public:
virtual ~BrushUiServices() = default;
virtual void set_tip_color(float r, float g, float b, float a) = 0;
virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept [[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{ {
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) { if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
@@ -119,4 +129,43 @@ struct BrushUiPlan {
return plan; return plan;
} }
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan,
BrushUiServices& services)
{
switch (plan.operation) {
case BrushUiOperation::set_tip_color:
{
for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
const auto channel_status = validate_brush_ui_color_channel(value);
if (!channel_status.ok()) {
return channel_status;
}
}
services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
case BrushUiOperation::set_texture:
if (plan.path.empty()) {
return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
}
services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::replace_brush_from_preset:
services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::stroke_settings_changed:
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
}
} // namespace pp::app } // namespace pp::app

View File

@@ -29,22 +29,91 @@
namespace { namespace {
class LegacyBrushUiServices final : public pp::app::BrushUiServices {
public:
LegacyBrushUiServices(
App& app,
bool update_quick = false,
bool update_color_panel = false,
const std::shared_ptr<Brush>& preset_brush = nullptr) noexcept
: app_(app)
, update_quick_(update_quick)
, update_color_panel_(update_color_panel)
, preset_brush_(preset_brush)
{
}
void set_tip_color(float r, float g, float b, float a) override
{
if (!Canvas::I || !Canvas::I->m_current_brush)
return;
Canvas::I->m_current_brush->m_tip_color = glm::vec4(r, g, b, a);
if (update_quick_ && app_.quick)
app_.quick->set_color(Canvas::I->m_current_brush->m_tip_color);
if (update_color_panel_ && app_.color)
app_.color->set_color(Canvas::I->m_current_brush->m_tip_color);
}
void set_texture(
pp::app::BrushUiTextureSlot slot,
std::string_view path,
std::string_view thumbnail_path) override
{
if (!Canvas::I || !Canvas::I->m_current_brush)
return;
const std::string texture_path(path);
const std::string thumbnail(thumbnail_path);
switch (slot)
{
case pp::app::BrushUiTextureSlot::tip:
Canvas::I->m_current_brush->load_tip(texture_path, thumbnail);
break;
case pp::app::BrushUiTextureSlot::pattern:
Canvas::I->m_current_brush->load_pattern(texture_path, thumbnail);
break;
case pp::app::BrushUiTextureSlot::dual:
Canvas::I->m_current_brush->load_dual(texture_path, thumbnail);
break;
}
}
void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) override
{
if (!Canvas::I || !Canvas::I->m_current_brush || !preset_brush_)
return;
const auto color = Canvas::I->m_current_brush->m_tip_color;
*Canvas::I->m_current_brush = *preset_brush_;
if (preserve_existing_color)
Canvas::I->m_current_brush->m_tip_color = color;
if (load_resources)
Canvas::I->m_current_brush->load();
}
void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) override
{
app_.brush_update(update_color_ui, update_brush_ui);
}
private:
App& app_;
bool update_quick_ = false;
bool update_color_panel_ = false;
std::shared_ptr<Brush> preset_brush_;
};
bool apply_brush_color_plan(App& app, glm::vec4 color, bool update_quick, bool update_color_panel) bool apply_brush_color_plan(App& app, glm::vec4 color, bool update_quick, bool update_color_panel)
{ {
const auto plan = pp::app::plan_brush_ui_color(color.r, color.g, color.b, color.a); const auto plan = pp::app::plan_brush_ui_color(color.r, color.g, color.b, color.a);
if (!plan) if (!plan)
return false; return false;
Canvas::I->m_current_brush->m_tip_color = glm::vec4( LegacyBrushUiServices services(app, update_quick, update_color_panel);
plan.value().r, const auto status = pp::app::execute_brush_ui_plan(plan.value(), services);
plan.value().g, if (!status.ok())
plan.value().b, LOG("Brush color action failed: %s", status.message);
plan.value().a); return status.ok();
if (update_quick)
app.quick->set_color(Canvas::I->m_current_brush->m_tip_color);
if (update_color_panel)
app.color->set_color(Canvas::I->m_current_brush->m_tip_color);
app.brush_update(plan.value().update_color_ui, plan.value().update_brush_ui);
return true;
} }
bool apply_brush_texture_plan(pp::app::BrushUiTextureSlot slot, const std::string& path, const std::string& thumb) bool apply_brush_texture_plan(pp::app::BrushUiTextureSlot slot, const std::string& path, const std::string& thumb)
@@ -52,20 +121,11 @@ bool apply_brush_texture_plan(pp::app::BrushUiTextureSlot slot, const std::strin
const auto plan = pp::app::plan_brush_ui_texture(slot, path, thumb); const auto plan = pp::app::plan_brush_ui_texture(slot, path, thumb);
if (!plan) if (!plan)
return false; return false;
switch (plan.value().texture_slot) LegacyBrushUiServices services(*App::I);
{ const auto status = pp::app::execute_brush_ui_plan(plan.value(), services);
case pp::app::BrushUiTextureSlot::tip: if (!status.ok())
Canvas::I->m_current_brush->load_tip(plan.value().path, plan.value().thumbnail_path); LOG("Brush texture action failed: %s", status.message);
break; return status.ok();
case pp::app::BrushUiTextureSlot::pattern:
Canvas::I->m_current_brush->load_pattern(plan.value().path, plan.value().thumbnail_path);
break;
case pp::app::BrushUiTextureSlot::dual:
Canvas::I->m_current_brush->load_dual(plan.value().path, plan.value().thumbnail_path);
break;
}
App::I->brush_update(plan.value().update_color_ui, plan.value().update_brush_ui);
return true;
} }
bool apply_brush_preset_plan(App& app, const std::shared_ptr<Brush>& brush) bool apply_brush_preset_plan(App& app, const std::shared_ptr<Brush>& brush)
@@ -73,14 +133,11 @@ bool apply_brush_preset_plan(App& app, const std::shared_ptr<Brush>& brush)
const auto plan = pp::app::plan_brush_ui_preset_replace(static_cast<bool>(brush)); const auto plan = pp::app::plan_brush_ui_preset_replace(static_cast<bool>(brush));
if (!plan) if (!plan)
return false; return false;
auto color = Canvas::I->m_current_brush->m_tip_color; LegacyBrushUiServices services(app, false, false, brush);
*Canvas::I->m_current_brush = *brush; const auto status = pp::app::execute_brush_ui_plan(plan.value(), services);
if (plan.value().preserves_existing_color) if (!status.ok())
Canvas::I->m_current_brush->m_tip_color = color; LOG("Brush preset action failed: %s", status.message);
if (plan.value().loads_brush_resources) return status.ok();
Canvas::I->m_current_brush->load();
app.brush_update(plan.value().update_color_ui, plan.value().update_brush_ui);
return true;
} }
bool apply_document_export_menu_plan(App& app, pp::app::DocumentExportMenuKind kind) bool apply_document_export_menu_plan(App& app, pp::app::DocumentExportMenuKind kind)
@@ -750,7 +807,10 @@ void App::init_sidebar()
}; };
stroke->on_stroke_change = [this](Node*) { stroke->on_stroke_change = [this](Node*) {
const auto plan = pp::app::plan_brush_ui_stroke_settings_changed(); const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
brush_update(plan.update_color_ui, plan.update_brush_ui); LegacyBrushUiServices services(*this);
const auto status = pp::app::execute_brush_ui_plan(plan, services);
if (!status.ok())
LOG("Brush stroke settings action failed: %s", status.message);
}; };
quick->on_color_change = [this](Node*, glm::vec3 c) { quick->on_color_change = [this](Node*, glm::vec3 c) {

View File

@@ -2,9 +2,69 @@
#include "test_harness.h" #include "test_harness.h"
#include <cmath> #include <cmath>
#include <string>
#include <string_view>
namespace { namespace {
class FakeBrushUiServices final : public pp::app::BrushUiServices {
public:
void set_tip_color(float r, float g, float b, float a) override
{
color_sets += 1;
last_r = r;
last_g = g;
last_b = b;
last_a = a;
call_order += "color;";
}
void set_texture(
pp::app::BrushUiTextureSlot slot,
std::string_view path,
std::string_view thumbnail_path) override
{
texture_sets += 1;
last_slot = slot;
last_path = std::string(path);
last_thumbnail_path = std::string(thumbnail_path);
call_order += "texture;";
}
void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) override
{
preset_replacements += 1;
preserved_color = preserve_existing_color;
loaded_resources = load_resources;
call_order += "preset;";
}
void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) override
{
refreshes += 1;
last_update_color_ui = update_color_ui;
last_update_brush_ui = update_brush_ui;
call_order += "refresh;";
}
int color_sets = 0;
int texture_sets = 0;
int preset_replacements = 0;
int refreshes = 0;
float last_r = 0.0F;
float last_g = 0.0F;
float last_b = 0.0F;
float last_a = 0.0F;
pp::app::BrushUiTextureSlot last_slot = pp::app::BrushUiTextureSlot::tip;
std::string last_path;
std::string last_thumbnail_path;
bool preserved_color = false;
bool loaded_resources = false;
bool last_update_color_ui = false;
bool last_update_brush_ui = false;
std::string call_order;
};
void color_plan_validates_all_channels(pp::tests::Harness& harness) void color_plan_validates_all_channels(pp::tests::Harness& harness)
{ {
const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F); const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
@@ -72,6 +132,94 @@ void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness)
PP_EXPECT(harness, !plan.loads_brush_resources); PP_EXPECT(harness, !plan.loads_brush_resources);
} }
void executor_dispatches_color_and_refresh(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
PP_EXPECT(harness, plan);
if (plan) {
PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan.value(), services).ok());
}
PP_EXPECT(harness, services.color_sets == 1);
PP_EXPECT(harness, services.last_r == 0.25F);
PP_EXPECT(harness, services.last_g == 0.5F);
PP_EXPECT(harness, services.last_b == 0.75F);
PP_EXPECT(harness, services.last_a == 1.0F);
PP_EXPECT(harness, services.refreshes == 1);
PP_EXPECT(harness, services.last_update_color_ui);
PP_EXPECT(harness, !services.last_update_brush_ui);
PP_EXPECT(harness, services.call_order == "color;refresh;");
}
void executor_dispatches_texture_and_preset(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
const auto texture = pp::app::plan_brush_ui_texture(
pp::app::BrushUiTextureSlot::dual,
"data/brushes/dual.png",
"data/brushes/thumbs/dual.png");
PP_EXPECT(harness, texture);
if (texture) {
PP_EXPECT(harness, pp::app::execute_brush_ui_plan(texture.value(), services).ok());
}
const auto preset = pp::app::plan_brush_ui_preset_replace(true);
PP_EXPECT(harness, preset);
if (preset) {
PP_EXPECT(harness, pp::app::execute_brush_ui_plan(preset.value(), services).ok());
}
PP_EXPECT(harness, services.texture_sets == 1);
PP_EXPECT(harness, services.last_slot == pp::app::BrushUiTextureSlot::dual);
PP_EXPECT(harness, services.last_path == "data/brushes/dual.png");
PP_EXPECT(harness, services.last_thumbnail_path == "data/brushes/thumbs/dual.png");
PP_EXPECT(harness, services.preset_replacements == 1);
PP_EXPECT(harness, services.preserved_color);
PP_EXPECT(harness, services.loaded_resources);
PP_EXPECT(harness, services.refreshes == 2);
PP_EXPECT(harness, services.call_order == "texture;refresh;preset;refresh;");
}
void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan, services).ok());
PP_EXPECT(harness, services.color_sets == 0);
PP_EXPECT(harness, services.texture_sets == 0);
PP_EXPECT(harness, services.preset_replacements == 0);
PP_EXPECT(harness, services.refreshes == 1);
PP_EXPECT(harness, services.last_update_color_ui);
PP_EXPECT(harness, services.last_update_brush_ui);
PP_EXPECT(harness, services.call_order == "refresh;");
}
void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
{
FakeBrushUiServices services;
pp::app::BrushUiPlan color;
color.operation = pp::app::BrushUiOperation::set_tip_color;
color.r = std::nanf("");
color.g = 0.0F;
color.b = 0.0F;
color.a = 1.0F;
PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(color, services).ok());
pp::app::BrushUiPlan texture;
texture.operation = pp::app::BrushUiOperation::set_texture;
texture.texture_slot = pp::app::BrushUiTextureSlot::tip;
texture.path.clear();
PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(texture, services).ok());
PP_EXPECT(harness, services.color_sets == 0);
PP_EXPECT(harness, services.texture_sets == 0);
PP_EXPECT(harness, services.refreshes == 0);
}
} // namespace } // namespace
int main() int main()
@@ -81,5 +229,9 @@ int main()
harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot); harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot);
harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush); harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush);
harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview); harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh);
harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset);
harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
return harness.finish(); return harness.finish();
} }