Extract brush UI operation planning
This commit is contained in:
@@ -225,6 +225,7 @@ target_link_libraries(pp_platform_api
|
||||
add_library(pp_app_core STATIC
|
||||
src/app_core/app_preferences.h
|
||||
src/app_core/app_status.h
|
||||
src/app_core/brush_ui.h
|
||||
src/app_core/document_animation.h
|
||||
src/app_core/document_cloud.h
|
||||
src/app_core/document_export.cpp
|
||||
|
||||
@@ -40,6 +40,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0020 | Open | Modernization | Document resize dialog state and selected-resolution planning now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, and `pano_cli plan-document-resize`, but live resize execution still calls legacy `Canvas::resize` and clears legacy `ActionManager` history directly | 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 a document/app boundary with legacy `Canvas` acting only as an adapter 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-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 |
|
||||
|
||||
## Closed Debt
|
||||
|
||||
|
||||
@@ -494,6 +494,10 @@ UI layer execution continue.
|
||||
frame add, duplicate, remove, duration adjustment, timeline moves, timeline
|
||||
goto/next/previous, and onion-size updates used by the live animation panel
|
||||
before legacy `Canvas`/`Layer` frame execution continues.
|
||||
`pano_cli plan-brush-operation` exposes app-core planning for brush color
|
||||
changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
|
||||
settings refreshes used by the live brush, quick, color, and floating panel
|
||||
callbacks before legacy `Brush` mutation and resource loading continue.
|
||||
`pp_platform_api` now owns a headless `PlatformServices` interface for
|
||||
startup storage path preparation, clipboard text, cursor visibility,
|
||||
virtual-keyboard visibility, UI-thread lifecycle hooks, render-context
|
||||
@@ -1123,6 +1127,15 @@ Results:
|
||||
`pano_cli_plan_animation_operation_next_wrap_smoke`, and
|
||||
`pano_cli_plan_animation_operation_rejects_remove_last_frame` passed and
|
||||
expose live animation-panel planning as JSON automation.
|
||||
- `pp_app_core_brush_ui_tests` passed, covering brush color channel validation,
|
||||
invalid color rejection, texture-path validation, preset-brush availability,
|
||||
preserve-current-color intent, and stroke-settings refresh intent.
|
||||
- `pano_cli_plan_brush_operation_color_smoke`,
|
||||
`pano_cli_plan_brush_operation_texture_smoke`,
|
||||
`pano_cli_plan_brush_operation_preset_smoke`,
|
||||
`pano_cli_plan_brush_operation_rejects_bad_color`, and
|
||||
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
|
||||
brush/color/preset UI planning as JSON automation.
|
||||
- `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
|
||||
platform share execution.
|
||||
- `pano_cli_plan_share_file_unsaved_smoke` and
|
||||
|
||||
122
src/app_core/brush_ui.h
Normal file
122
src/app_core/brush_ui.h
Normal file
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class BrushUiTextureSlot {
|
||||
tip,
|
||||
pattern,
|
||||
dual,
|
||||
};
|
||||
|
||||
enum class BrushUiOperation {
|
||||
set_tip_color,
|
||||
set_texture,
|
||||
replace_brush_from_preset,
|
||||
stroke_settings_changed,
|
||||
};
|
||||
|
||||
struct BrushUiPlan {
|
||||
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
|
||||
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
|
||||
std::string path;
|
||||
std::string thumbnail_path;
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 1.0F;
|
||||
bool mutates_brush = false;
|
||||
bool preserves_existing_color = false;
|
||||
bool loads_brush_resources = false;
|
||||
bool update_color_ui = false;
|
||||
bool update_brush_ui = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
|
||||
float r,
|
||||
float g,
|
||||
float b,
|
||||
float a)
|
||||
{
|
||||
for (const auto value : { r, g, b, a }) {
|
||||
const auto channel_status = validate_brush_ui_color_channel(value);
|
||||
if (!channel_status.ok()) {
|
||||
return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
|
||||
}
|
||||
}
|
||||
|
||||
BrushUiPlan plan;
|
||||
plan.operation = BrushUiOperation::set_tip_color;
|
||||
plan.r = r;
|
||||
plan.g = g;
|
||||
plan.b = b;
|
||||
plan.a = a;
|
||||
plan.mutates_brush = true;
|
||||
plan.update_color_ui = true;
|
||||
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
|
||||
BrushUiTextureSlot slot,
|
||||
std::string_view path,
|
||||
std::string_view thumbnail_path)
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Result<BrushUiPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
|
||||
}
|
||||
|
||||
BrushUiPlan plan;
|
||||
plan.operation = BrushUiOperation::set_texture;
|
||||
plan.texture_slot = slot;
|
||||
plan.path = std::string(path);
|
||||
plan.thumbnail_path = std::string(thumbnail_path);
|
||||
plan.mutates_brush = true;
|
||||
plan.loads_brush_resources = true;
|
||||
plan.update_color_ui = true;
|
||||
plan.update_brush_ui = true;
|
||||
return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
|
||||
{
|
||||
if (!has_preset_brush) {
|
||||
return pp::foundation::Result<BrushUiPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("preset brush must be available"));
|
||||
}
|
||||
|
||||
BrushUiPlan plan;
|
||||
plan.operation = BrushUiOperation::replace_brush_from_preset;
|
||||
plan.mutates_brush = true;
|
||||
plan.preserves_existing_color = true;
|
||||
plan.loads_brush_resources = true;
|
||||
plan.update_color_ui = true;
|
||||
plan.update_brush_ui = true;
|
||||
return pp::foundation::Result<BrushUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
|
||||
{
|
||||
BrushUiPlan plan;
|
||||
plan.operation = BrushUiOperation::stroke_settings_changed;
|
||||
plan.mutates_brush = true;
|
||||
plan.update_color_ui = true;
|
||||
plan.update_brush_ui = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "node_dialog_picker.h"
|
||||
#include "node_panel_floating.h"
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "app_core/document_layer.h"
|
||||
#include "app_core/app_status.h"
|
||||
#include "settings.h"
|
||||
@@ -17,6 +18,64 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
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);
|
||||
if (!plan)
|
||||
return false;
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(
|
||||
plan.value().r,
|
||||
plan.value().g,
|
||||
plan.value().b,
|
||||
plan.value().a);
|
||||
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)
|
||||
{
|
||||
const auto plan = pp::app::plan_brush_ui_texture(slot, path, thumb);
|
||||
if (!plan)
|
||||
return false;
|
||||
switch (plan.value().texture_slot)
|
||||
{
|
||||
case pp::app::BrushUiTextureSlot::tip:
|
||||
Canvas::I->m_current_brush->load_tip(plan.value().path, plan.value().thumbnail_path);
|
||||
break;
|
||||
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)
|
||||
{
|
||||
const auto plan = pp::app::plan_brush_ui_preset_replace(static_cast<bool>(brush));
|
||||
if (!plan)
|
||||
return false;
|
||||
auto color = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *brush;
|
||||
if (plan.value().preserves_existing_color)
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
if (plan.value().loads_brush_resources)
|
||||
Canvas::I->m_current_brush->load();
|
||||
app.brush_update(plan.value().update_color_ui, plan.value().update_brush_ui);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void App::title_update()
|
||||
{
|
||||
if (auto docname = layout[main_id]->find<NodeText>("txt-docname"))
|
||||
@@ -144,30 +203,25 @@ void App::init_sidebar()
|
||||
brush_update(true, true);
|
||||
};
|
||||
color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
quick->set_color(color);
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, color, true, false);
|
||||
};
|
||||
|
||||
stroke->on_brush_changed = [this](Node* target, const std::string& path, const std::string& thumb) {
|
||||
Canvas::I->m_current_brush->load_tip(path, thumb);
|
||||
brush_update(true, true);
|
||||
apply_brush_texture_plan(pp::app::BrushUiTextureSlot::tip, path, thumb);
|
||||
};
|
||||
stroke->on_pattern_changed = [this](Node*target, const std::string& path, const std::string& thumb) {
|
||||
Canvas::I->m_current_brush->load_pattern(path, thumb);
|
||||
brush_update(true, true);
|
||||
apply_brush_texture_plan(pp::app::BrushUiTextureSlot::pattern, path, thumb);
|
||||
};
|
||||
stroke->on_dual_changed = [this](Node*target, const std::string& path, const std::string& thumb) {
|
||||
Canvas::I->m_current_brush->load_dual(path, thumb);
|
||||
brush_update(true, true);
|
||||
apply_brush_texture_plan(pp::app::BrushUiTextureSlot::dual, path, thumb);
|
||||
};
|
||||
stroke->on_stroke_change = [this](Node*) {
|
||||
brush_update(true, true);
|
||||
const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
|
||||
brush_update(plan.update_color_ui, plan.update_brush_ui);
|
||||
};
|
||||
|
||||
quick->on_color_change = [this](Node*, glm::vec3 c) {
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(c, 1.f);
|
||||
color->set_color(c);
|
||||
apply_brush_color_plan(*this, glm::vec4(c, 1.f), false, true);
|
||||
};
|
||||
quick->on_flow_change = [this](Node*, float value) {
|
||||
stroke->set_flow(value, true, true);
|
||||
@@ -176,10 +230,7 @@ void App::init_sidebar()
|
||||
stroke->set_size(value, true, true);
|
||||
};
|
||||
quick->on_brush_change = [this](Node*, std::shared_ptr<Brush> b) {
|
||||
auto c = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *b;
|
||||
Canvas::I->m_current_brush->m_tip_color = c;
|
||||
brush_update(true, true);
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
|
||||
layers->on_layer_add = [this](Node*, std::shared_ptr<class Layer> layer, int index) {
|
||||
@@ -853,11 +904,7 @@ void App::init_menu_tools()
|
||||
//floating_presets->SetFlexGrow(1);
|
||||
//floating_presets->find("toolbar")->destroy();
|
||||
floating_presets->on_brush_changed = [this](Node* target, std::shared_ptr<Brush>& b) {
|
||||
auto c = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *b;
|
||||
Canvas::I->m_current_brush->m_tip_color = c;
|
||||
Canvas::I->m_current_brush->load();
|
||||
brush_update(true, true);
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -883,8 +930,7 @@ void App::init_menu_tools()
|
||||
//floating_color->SetMinHeight(300);
|
||||
floating_color->find("title")->SetVisibility(false);
|
||||
floating_color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, color, false, false);
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -910,8 +956,7 @@ void App::init_menu_tools()
|
||||
//floating_picker->SetWidth(250);
|
||||
floating_picker->m_autohide = false;
|
||||
floating_picker->on_color_change = [this](Node* target, glm::vec3 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(color, 1.f);
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, glm::vec4(color, 1.f), false, false);
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -1624,11 +1669,7 @@ void App::ui_restore()
|
||||
floating_presets->SetHeightP(100);
|
||||
//floating_presets->find("toolbar")->destroy();
|
||||
floating_presets->on_brush_changed = [this](Node* target, std::shared_ptr<Brush>& b) {
|
||||
auto c = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *b;
|
||||
Canvas::I->m_current_brush->m_tip_color = c;
|
||||
Canvas::I->m_current_brush->load();
|
||||
brush_update(true, true);
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1638,8 +1679,7 @@ void App::ui_restore()
|
||||
floating_color->SetHeightP(100);
|
||||
floating_color->find("title")->SetVisibility(false);
|
||||
floating_color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, color, false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1648,8 +1688,7 @@ void App::ui_restore()
|
||||
floating_picker = f->m_container->add_child_ref<NodeColorPicker>();
|
||||
floating_picker->m_autohide = false;
|
||||
floating_picker->on_color_change = [this](Node* target, glm::vec3 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(color, 1.f);
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, glm::vec4(color, 1.f), false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1717,11 +1756,7 @@ void App::ui_restore()
|
||||
floating_presets->SetHeightP(100);
|
||||
//floating_presets->find("toolbar")->destroy();
|
||||
floating_presets->on_brush_changed = [this](Node* target, std::shared_ptr<Brush>& b) {
|
||||
auto c = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *b;
|
||||
Canvas::I->m_current_brush->m_tip_color = c;
|
||||
Canvas::I->m_current_brush->load();
|
||||
brush_update(true, true);
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1731,8 +1766,7 @@ void App::ui_restore()
|
||||
floating_color->SetHeightP(100);
|
||||
floating_color->find("title")->destroy();
|
||||
floating_color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, color, false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1741,8 +1775,7 @@ void App::ui_restore()
|
||||
floating_picker = f->m_container->add_child_ref<NodeColorPicker>();
|
||||
floating_picker->m_autohide = false;
|
||||
floating_picker->on_color_change = [this](Node* target, glm::vec3 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(color, 1.f);
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, glm::vec4(color, 1.f), false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1797,11 +1830,7 @@ void App::ui_restore()
|
||||
floating_presets->SetHeightP(100);
|
||||
//floating_presets->find("toolbar")->destroy();
|
||||
floating_presets->on_brush_changed = [this](Node* target, std::shared_ptr<Brush>& b) {
|
||||
auto c = Canvas::I->m_current_brush->m_tip_color;
|
||||
*Canvas::I->m_current_brush = *b;
|
||||
Canvas::I->m_current_brush->m_tip_color = c;
|
||||
Canvas::I->m_current_brush->load();
|
||||
brush_update(true, true);
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1811,8 +1840,7 @@ void App::ui_restore()
|
||||
floating_color->SetHeightP(100);
|
||||
floating_color->find("title")->destroy();
|
||||
floating_color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = color;
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, color, false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1821,8 +1849,7 @@ void App::ui_restore()
|
||||
floating_picker = f->m_container->add_child_ref<NodeColorPicker>();
|
||||
floating_picker->m_autohide = false;
|
||||
floating_picker->on_color_change = [this](Node* target, glm::vec3 color) {
|
||||
Canvas::I->m_current_brush->m_tip_color = glm::vec4(color, 1.f);
|
||||
brush_update(true, false);
|
||||
apply_brush_color_plan(*this, glm::vec4(color, 1.f), false, false);
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,16 @@ add_test(NAME pp_ui_core_layout_xml_tests COMMAND pp_ui_core_layout_xml_tests)
|
||||
set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES
|
||||
LABELS "ui;desktop-fast;fuzz")
|
||||
|
||||
add_executable(pp_app_core_brush_ui_tests
|
||||
app_core/brush_ui_tests.cpp)
|
||||
target_link_libraries(pp_app_core_brush_ui_tests PRIVATE
|
||||
pp_app_core
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_app_core_brush_ui_tests COMMAND pp_app_core_brush_ui_tests)
|
||||
set_tests_properties(pp_app_core_brush_ui_tests PROPERTIES
|
||||
LABELS "app;paint;desktop-fast;fuzz")
|
||||
|
||||
add_executable(pp_app_core_document_route_tests
|
||||
app_core/document_route_tests.cpp)
|
||||
target_link_libraries(pp_app_core_document_route_tests PRIVATE
|
||||
@@ -776,6 +786,36 @@ if(TARGET pano_cli)
|
||||
LABELS "app;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_brush_operation_color_smoke
|
||||
COMMAND pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1)
|
||||
set_tests_properties(pano_cli_plan_brush_operation_color_smoke PROPERTIES
|
||||
LABELS "app;paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-operation\".*\"operation\":\"set-tip-color\".*\"r\":0.25.*\"g\":0.5.*\"b\":0.75.*\"updateColorUi\":true.*\"updateBrushUi\":false")
|
||||
|
||||
add_test(NAME pano_cli_plan_brush_operation_texture_smoke
|
||||
COMMAND pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png)
|
||||
set_tests_properties(pano_cli_plan_brush_operation_texture_smoke PROPERTIES
|
||||
LABELS "app;paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-operation\".*\"operation\":\"set-texture\".*\"textureSlot\":\"pattern\".*\"path\":\"data/patterns/noise.png\".*\"loadsBrushResources\":true.*\"updateBrushUi\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_brush_operation_preset_smoke
|
||||
COMMAND pano_cli plan-brush-operation --kind preset)
|
||||
set_tests_properties(pano_cli_plan_brush_operation_preset_smoke PROPERTIES
|
||||
LABELS "app;paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-operation\".*\"operation\":\"replace-brush-from-preset\".*\"preservesExistingColor\":true.*\"loadsBrushResources\":true")
|
||||
|
||||
add_test(NAME pano_cli_plan_brush_operation_rejects_bad_color
|
||||
COMMAND pano_cli plan-brush-operation --kind color --r 1.5)
|
||||
set_tests_properties(pano_cli_plan_brush_operation_rejects_bad_color PROPERTIES
|
||||
LABELS "app;paint;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_brush_operation_rejects_empty_texture
|
||||
COMMAND pano_cli plan-brush-operation --kind tip)
|
||||
set_tests_properties(pano_cli_plan_brush_operation_rejects_empty_texture PROPERTIES
|
||||
LABELS "app;paint;integration;desktop-fast;fuzz"
|
||||
WILL_FAIL TRUE)
|
||||
|
||||
add_test(NAME pano_cli_plan_share_file_unsaved_smoke
|
||||
COMMAND pano_cli plan-share-file)
|
||||
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES
|
||||
|
||||
85
tests/app_core/brush_ui_tests.cpp
Normal file
85
tests/app_core/brush_ui_tests.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
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);
|
||||
PP_EXPECT(harness, plan);
|
||||
if (plan) {
|
||||
PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_tip_color);
|
||||
PP_EXPECT(harness, plan.value().r == 0.25F);
|
||||
PP_EXPECT(harness, plan.value().g == 0.5F);
|
||||
PP_EXPECT(harness, plan.value().b == 0.75F);
|
||||
PP_EXPECT(harness, plan.value().a == 1.0F);
|
||||
PP_EXPECT(harness, plan.value().mutates_brush);
|
||||
PP_EXPECT(harness, plan.value().update_color_ui);
|
||||
PP_EXPECT(harness, !plan.value().update_brush_ui);
|
||||
}
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_brush_ui_color(-0.01F, 0.5F, 0.5F, 1.0F));
|
||||
PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 1.01F, 0.5F, 1.0F));
|
||||
PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 0.5F, std::nanf(""), 1.0F));
|
||||
}
|
||||
|
||||
void texture_plan_validates_path_and_slot(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto plan = pp::app::plan_brush_ui_texture(
|
||||
pp::app::BrushUiTextureSlot::pattern,
|
||||
"data/patterns/noise.png",
|
||||
"data/patterns/thumbs/noise.png");
|
||||
PP_EXPECT(harness, plan);
|
||||
if (plan) {
|
||||
PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_texture);
|
||||
PP_EXPECT(harness, plan.value().texture_slot == pp::app::BrushUiTextureSlot::pattern);
|
||||
PP_EXPECT(harness, plan.value().path == "data/patterns/noise.png");
|
||||
PP_EXPECT(harness, plan.value().thumbnail_path == "data/patterns/thumbs/noise.png");
|
||||
PP_EXPECT(harness, plan.value().loads_brush_resources);
|
||||
PP_EXPECT(harness, plan.value().update_color_ui);
|
||||
PP_EXPECT(harness, plan.value().update_brush_ui);
|
||||
}
|
||||
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
!pp::app::plan_brush_ui_texture(pp::app::BrushUiTextureSlot::tip, "", "thumb.png"));
|
||||
}
|
||||
|
||||
void preset_plan_preserves_color_and_requires_brush(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto plan = pp::app::plan_brush_ui_preset_replace(true);
|
||||
PP_EXPECT(harness, plan);
|
||||
if (plan) {
|
||||
PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::replace_brush_from_preset);
|
||||
PP_EXPECT(harness, plan.value().preserves_existing_color);
|
||||
PP_EXPECT(harness, plan.value().loads_brush_resources);
|
||||
PP_EXPECT(harness, plan.value().update_color_ui);
|
||||
PP_EXPECT(harness, plan.value().update_brush_ui);
|
||||
}
|
||||
|
||||
PP_EXPECT(harness, !pp::app::plan_brush_ui_preset_replace(false));
|
||||
}
|
||||
|
||||
void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
|
||||
PP_EXPECT(harness, plan.operation == pp::app::BrushUiOperation::stroke_settings_changed);
|
||||
PP_EXPECT(harness, plan.mutates_brush);
|
||||
PP_EXPECT(harness, plan.update_color_ui);
|
||||
PP_EXPECT(harness, plan.update_brush_ui);
|
||||
PP_EXPECT(harness, !plan.loads_brush_resources);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("color plan validates all channels", color_plan_validates_all_channels);
|
||||
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("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "app_core/app_status.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "app_core/document_animation.h"
|
||||
#include "app_core/document_export.h"
|
||||
#include "app_core/document_cloud.h"
|
||||
@@ -251,6 +252,17 @@ struct PlanAnimationOperationArgs {
|
||||
int onion_size = 1;
|
||||
};
|
||||
|
||||
struct PlanBrushOperationArgs {
|
||||
std::string kind = "settings";
|
||||
std::string path;
|
||||
std::string thumbnail_path;
|
||||
float r = 1.0F;
|
||||
float g = 1.0F;
|
||||
float b = 1.0F;
|
||||
float a = 1.0F;
|
||||
bool has_brush = true;
|
||||
};
|
||||
|
||||
struct SimulateAppSessionArgs {
|
||||
bool has_canvas = true;
|
||||
bool new_document = false;
|
||||
@@ -550,6 +562,36 @@ const char* document_animation_operation_name(pp::app::DocumentAnimationOperatio
|
||||
return "goto-frame";
|
||||
}
|
||||
|
||||
const char* brush_ui_texture_slot_name(pp::app::BrushUiTextureSlot slot) noexcept
|
||||
{
|
||||
switch (slot) {
|
||||
case pp::app::BrushUiTextureSlot::tip:
|
||||
return "tip";
|
||||
case pp::app::BrushUiTextureSlot::pattern:
|
||||
return "pattern";
|
||||
case pp::app::BrushUiTextureSlot::dual:
|
||||
return "dual";
|
||||
}
|
||||
|
||||
return "tip";
|
||||
}
|
||||
|
||||
const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcept
|
||||
{
|
||||
switch (operation) {
|
||||
case pp::app::BrushUiOperation::set_tip_color:
|
||||
return "set-tip-color";
|
||||
case pp::app::BrushUiOperation::set_texture:
|
||||
return "set-texture";
|
||||
case pp::app::BrushUiOperation::replace_brush_from_preset:
|
||||
return "replace-brush-from-preset";
|
||||
case pp::app::BrushUiOperation::stroke_settings_changed:
|
||||
return "stroke-settings-changed";
|
||||
}
|
||||
|
||||
return "stroke-settings-changed";
|
||||
}
|
||||
|
||||
const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept
|
||||
{
|
||||
switch (decision) {
|
||||
@@ -804,6 +846,7 @@ void print_help()
|
||||
<< " plan-layer-rename --old-name NAME --new-name NAME\n"
|
||||
<< " plan-layer-operation --kind add|duplicate|select|reorder|remove|opacity|visibility|alpha-lock|blend-mode|highlight [--layer-count N] [--index N] [--from-index N] [--to-index N] [--source-index N] [--name NAME] [--opacity N] [--blend-mode N] [--enabled]\n"
|
||||
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n"
|
||||
<< " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n"
|
||||
<< " plan-share-file [--path FILE]\n"
|
||||
<< " plan-picked-path [--path FILE]\n"
|
||||
<< " plan-display-file [--path FILE]\n"
|
||||
@@ -2719,6 +2762,129 @@ int plan_animation_operation(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_brush_operation_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
PlanBrushOperationArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--kind" || key == "--path" || key == "--thumb") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
if (key == "--kind") {
|
||||
args.kind = argv[++i];
|
||||
} else if (key == "--path") {
|
||||
args.path = argv[++i];
|
||||
} else {
|
||||
args.thumbnail_path = argv[++i];
|
||||
}
|
||||
} else if (key == "--r" || key == "--g" || key == "--b" || key == "--a") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
const auto value = parse_float_arg(argv[++i]);
|
||||
if (!value) {
|
||||
return value.status();
|
||||
}
|
||||
if (key == "--r") {
|
||||
args.r = value.value();
|
||||
} else if (key == "--g") {
|
||||
args.g = value.value();
|
||||
} else if (key == "--b") {
|
||||
args.b = value.value();
|
||||
} else {
|
||||
args.a = value.value();
|
||||
}
|
||||
} else if (key == "--no-brush") {
|
||||
args.has_brush = false;
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<pp::app::BrushUiPlan> make_brush_operation_plan(
|
||||
const PlanBrushOperationArgs& args)
|
||||
{
|
||||
if (args.kind == "color") {
|
||||
return pp::app::plan_brush_ui_color(args.r, args.g, args.b, args.a);
|
||||
}
|
||||
if (args.kind == "tip") {
|
||||
return pp::app::plan_brush_ui_texture(
|
||||
pp::app::BrushUiTextureSlot::tip,
|
||||
args.path,
|
||||
args.thumbnail_path);
|
||||
}
|
||||
if (args.kind == "pattern") {
|
||||
return pp::app::plan_brush_ui_texture(
|
||||
pp::app::BrushUiTextureSlot::pattern,
|
||||
args.path,
|
||||
args.thumbnail_path);
|
||||
}
|
||||
if (args.kind == "dual") {
|
||||
return pp::app::plan_brush_ui_texture(
|
||||
pp::app::BrushUiTextureSlot::dual,
|
||||
args.path,
|
||||
args.thumbnail_path);
|
||||
}
|
||||
if (args.kind == "preset") {
|
||||
return pp::app::plan_brush_ui_preset_replace(args.has_brush);
|
||||
}
|
||||
if (args.kind == "settings") {
|
||||
return pp::foundation::Result<pp::app::BrushUiPlan>::success(
|
||||
pp::app::plan_brush_ui_stroke_settings_changed());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<pp::app::BrushUiPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown brush operation kind"));
|
||||
}
|
||||
|
||||
int plan_brush_operation(int argc, char** argv)
|
||||
{
|
||||
PlanBrushOperationArgs args;
|
||||
const auto status = parse_plan_brush_operation_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("plan-brush-operation", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto plan = make_brush_operation_plan(args);
|
||||
if (!plan) {
|
||||
print_error("plan-brush-operation", plan.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto& value = plan.value();
|
||||
std::cout << "{\"ok\":true,\"command\":\"plan-brush-operation\""
|
||||
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
|
||||
<< "\",\"path\":\"" << json_escape(args.path)
|
||||
<< "\",\"thumb\":\"" << json_escape(args.thumbnail_path)
|
||||
<< "\",\"r\":" << args.r
|
||||
<< ",\"g\":" << args.g
|
||||
<< ",\"b\":" << args.b
|
||||
<< ",\"a\":" << args.a
|
||||
<< ",\"hasBrush\":" << json_bool(args.has_brush)
|
||||
<< "},\"plan\":{\"operation\":\"" << brush_ui_operation_name(value.operation)
|
||||
<< "\",\"textureSlot\":\"" << brush_ui_texture_slot_name(value.texture_slot)
|
||||
<< "\",\"path\":\"" << json_escape(value.path)
|
||||
<< "\",\"thumb\":\"" << json_escape(value.thumbnail_path)
|
||||
<< "\",\"r\":" << value.r
|
||||
<< ",\"g\":" << value.g
|
||||
<< ",\"b\":" << value.b
|
||||
<< ",\"a\":" << value.a
|
||||
<< ",\"mutatesBrush\":" << json_bool(value.mutates_brush)
|
||||
<< ",\"preservesExistingColor\":" << json_bool(value.preserves_existing_color)
|
||||
<< ",\"loadsBrushResources\":" << json_bool(value.loads_brush_resources)
|
||||
<< ",\"updateColorUi\":" << json_bool(value.update_color_ui)
|
||||
<< ",\"updateBrushUi\":" << json_bool(value.update_brush_ui)
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_share_file_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
@@ -5135,6 +5301,10 @@ int main(int argc, char** argv)
|
||||
return plan_animation_operation(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-brush-operation") {
|
||||
return plan_brush_operation(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-share-file") {
|
||||
return plan_share_file(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user