Add brush texture list boundary

This commit is contained in:
2026-06-03 17:21:49 +02:00
parent cee5f141a3
commit 9adfad9609
8 changed files with 727 additions and 64 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, 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`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode 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 select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode 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 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 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-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `BrushUiServices`, and `BrushTextureListServices`, but the live adapter still mutates legacy `Brush`, loads/saves legacy brush texture images, and refreshes legacy quick/stroke/color widgets | 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`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings and texture-list execution are 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 and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, 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-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

@@ -511,6 +511,10 @@ 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. Brush UI execution now dispatches through `BrushUiServices` before callbacks. Brush UI execution now dispatches through `BrushUiServices` before
the legacy `Brush`/panel adapter mutates brush state or loads brush resources. the legacy `Brush`/panel adapter mutates brush state or loads brush resources.
`pano_cli plan-brush-texture-list` exposes app-core planning for brush/pattern
texture add, remove, and reorder actions, and `NodePanelBrush` now dispatches
those actions through `BrushTextureListServices` before the legacy image
load/save and UI-list adapter continues.
`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. Canvas tool execution now dispatches through `CanvasToolServices` commands. Canvas tool execution now dispatches through `CanvasToolServices`
@@ -1226,15 +1230,21 @@ Results:
live animation-panel planning as JSON automation. 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, stroke-settings refresh intent, service preserve-current-color intent, stroke-settings refresh intent, texture-list
dispatch ordering, texture/preset execution payloads, and invalid execution add target path planning, user-texture removal intent, clamped reorder intent,
payload rejection. service dispatch ordering, texture/preset/list execution payloads, execution
failure preservation, 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`,
`pano_cli_plan_brush_operation_rejects_bad_color`, and `pano_cli_plan_brush_operation_rejects_bad_color`, and
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live `pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
brush/color/preset UI planning as JSON automation. brush/color/preset UI planning as JSON automation.
- `pano_cli_plan_brush_texture_list_add_smoke`,
`pano_cli_plan_brush_texture_list_remove_user_smoke`,
`pano_cli_plan_brush_texture_list_move_edge_smoke`, and
`pano_cli_plan_brush_texture_list_rejects_bad_source` passed and expose live
brush/pattern texture-list planning as JSON automation.
- `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear - `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear
planning, lightmap capability and limit checks, missing-heightmap no-op planning, lightmap capability and limit checks, missing-heightmap no-op
behavior, and commit canvas gating. behavior, and commit canvas gating.

View File

@@ -2,6 +2,7 @@
#include "foundation/result.h" #include "foundation/result.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -21,6 +22,12 @@ enum class BrushUiOperation {
stroke_settings_changed, stroke_settings_changed,
}; };
enum class BrushTextureListOperation {
add_texture,
remove_texture,
move_texture,
};
struct BrushUiPlan { struct BrushUiPlan {
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed; BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip; BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
@@ -37,6 +44,24 @@ struct BrushUiPlan {
bool update_brush_ui = false; bool update_brush_ui = false;
}; };
struct BrushTextureListPlan {
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
int item_count = 0;
int current_index = -1;
int target_index = -1;
int move_offset = 0;
std::string source_path;
std::string high_path;
std::string thumbnail_path;
std::string brush_name;
bool user_texture = false;
bool deletes_texture_files = false;
bool saves_list = false;
bool notifies_selection = false;
bool converts_brush_alpha = false;
bool no_op = false;
};
class BrushUiServices { class BrushUiServices {
public: public:
virtual ~BrushUiServices() = default; virtual ~BrushUiServices() = default;
@@ -47,6 +72,41 @@ public:
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0; virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
}; };
class BrushTextureListServices {
public:
virtual ~BrushTextureListServices() = default;
virtual pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) = 0;
virtual void remove_texture(int index, bool delete_texture_files) = 0;
virtual void move_texture(int from_index, int to_index) = 0;
virtual void select_texture(int index) = 0;
virtual void save_texture_list() = 0;
};
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
std::string_view source_path) noexcept
{
const auto slash = source_path.find_last_of("/\\");
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
if (name_begin >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
}
const auto dot = source_path.find_last_of('.');
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
}
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
}
[[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) {
@@ -129,6 +189,93 @@ public:
return plan; return plan;
} }
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
std::string_view directory_name,
std::string_view data_path,
std::string_view source_path)
{
if (directory_name.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
}
if (data_path.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
}
const auto stem = brush_texture_source_stem(source_path);
if (!stem) {
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::add_texture;
plan.source_path = std::string(source_path);
plan.brush_name = std::string(stem.value());
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
+ plan.brush_name + ".png";
plan.user_texture = true;
plan.saves_list = true;
plan.converts_brush_alpha = directory_name == "brushes";
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
int item_count,
int current_index,
bool current_is_user_texture)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::remove_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
plan.user_texture = current_is_user_texture;
plan.deletes_texture_files = current_is_user_texture;
plan.saves_list = true;
plan.notifies_selection = plan.target_index >= 0;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
int item_count,
int current_index,
int offset)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
if (offset == 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::move_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
plan.move_offset = offset;
plan.saves_list = true;
plan.no_op = plan.target_index == current_index;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan( [[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan, const BrushUiPlan& plan,
BrushUiServices& services) BrushUiServices& services)
@@ -168,4 +315,59 @@ public:
return pp::foundation::Status::invalid_argument("unknown brush UI operation"); return pp::foundation::Status::invalid_argument("unknown brush UI operation");
} }
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
const BrushTextureListPlan& plan,
BrushTextureListServices& services)
{
switch (plan.operation) {
case BrushTextureListOperation::add_texture:
{
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|| plan.brush_name.empty()) {
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
}
const auto add_status = services.add_texture_from_source(
plan.source_path,
plan.high_path,
plan.thumbnail_path,
plan.brush_name,
plan.converts_brush_alpha);
if (!add_status.ok()) {
return add_status;
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
case BrushTextureListOperation::remove_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
}
services.remove_texture(plan.current_index, plan.deletes_texture_files);
if (plan.notifies_selection && plan.target_index >= 0) {
services.select_texture(plan.target_index);
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
case BrushTextureListOperation::move_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
}
services.move_texture(plan.current_index, plan.target_index);
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
}
} // namespace pp::app } // namespace pp::app

View File

@@ -1,6 +1,7 @@
#include "pch.h" #include "pch.h"
#include "log.h" #include "log.h"
#include "node_panel_brush.h" #include "node_panel_brush.h"
#include "app_core/brush_ui.h"
#include "asset.h" #include "asset.h"
#include "texture.h" #include "texture.h"
@@ -75,6 +76,116 @@ Node* NodePanelBrush::clone_instantiate() const
return new NodePanelBrush(); return new NodePanelBrush();
} }
void NodePanelBrush::execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan)
{
class LegacyBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
explicit LegacyBrushTextureListServices(NodePanelBrush& panel) noexcept
: panel_(panel)
{
}
pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) override
{
Image img;
if (!img.load_file(std::string(source_path))) {
return pp::foundation::Status::invalid_argument("brush texture source could not be loaded");
}
if (converts_brush_alpha) {
img.gayscale_alpha();
}
auto thumbnail_image = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumbnail_image.save_png(std::string(thumbnail_path));
img.save_png(std::string(high_path));
NodeButtonBrush* brush = new NodeButtonBrush;
panel_.m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
const auto thumbnail_path_string = std::string(thumbnail_path);
brush->set_icon(thumbnail_path_string.c_str());
brush->thumb_path = std::string(thumbnail_path);
brush->high_path = std::string(high_path);
brush->brush_name = std::string(brush_name);
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, &panel_, std::placeholders::_1);
return pp::foundation::Status::success();
}
void remove_texture(int index, bool delete_texture_files) override
{
auto* brush = brush_at(index);
if (!brush) {
return;
}
if (delete_texture_files) {
Asset::delete_file(brush->thumb_path);
Asset::delete_file(brush->high_path);
}
if (panel_.m_current == brush) {
panel_.m_current = nullptr;
}
panel_.m_container->remove_child(brush);
}
void move_texture(int from_index, int to_index) override
{
if (auto* brush = brush_at(from_index)) {
panel_.m_container->move_child(brush, to_index);
}
}
void select_texture(int index) override
{
if (panel_.m_current) {
panel_.m_current->m_selected = false;
}
panel_.m_current = brush_at(index);
if (!panel_.m_current) {
return;
}
panel_.m_current->m_selected = true;
if (panel_.on_brush_changed) {
panel_.on_brush_changed(&panel_, index);
}
}
void save_texture_list() override
{
panel_.save();
}
private:
NodeButtonBrush* brush_at(int index) const
{
if (index < 0 || index >= static_cast<int>(panel_.m_container->m_children.size())) {
return nullptr;
}
return static_cast<NodeButtonBrush*>(panel_.m_container->m_children[index].get());
}
NodePanelBrush& panel_;
};
LegacyBrushTextureListServices services(*this);
const auto status = pp::app::execute_brush_texture_list_plan(plan, services);
if (!status.ok()) {
LOG("Brush texture list action failed: %s", status.message);
}
}
void NodePanelBrush::init() void NodePanelBrush::init()
{ {
init_template_file("data/dialogs/panel-brushes.xml", "tpl-panel-brushes"); init_template_file("data/dialogs/panel-brushes.xml", "tpl-panel-brushes");
@@ -82,41 +193,9 @@ void NodePanelBrush::init()
m_btn_add = find<NodeButtonCustom>("btn-add"); m_btn_add = find<NodeButtonCustom>("btn-add");
m_btn_add->on_click = [this](Node*) { m_btn_add->on_click = [this](Node*) {
App::I->pick_file({ "JPG", "PNG" }, [this](std::string path) { App::I->pick_file({ "JPG", "PNG" }, [this](std::string path) {
std::string name, base, ext; const auto plan = pp::app::plan_brush_texture_list_add(m_dir_name, App::I->data_path, path);
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)"); if (plan) {
std::smatch m; execute_texture_list_plan(plan.value());
if (!std::regex_search(path, m, r))
return;
base = m[1].str();
name = m[2].str();
ext = m[3].str();
Image img;
if (!m_dir_name.empty() && img.load_file(path))
{
std::string path_high = App::I->data_path + "/" + m_dir_name + "/" + name + ".png";
std::string path_thumb = App::I->data_path + "/" + m_dir_name + "/thumbs/" + name + ".png";
//img = img.resize_squared(glm::u8vec4(255));
if (m_dir_name == "brushes")
img.gayscale_alpha();
auto thumb = img.resize(64, 64).resize_squared(glm::u8vec4(255));
thumb.save_png(path_thumb);
//auto po2 = img.resize_power2();
img.save_png(path_high);
NodeButtonBrush* brush = new NodeButtonBrush;
m_container->add_child(brush);
brush->init();
brush->create();
brush->loaded();
brush->set_icon(path_thumb.c_str());
brush->thumb_path = path_thumb;
brush->high_path = path_high;
brush->brush_name = name;
brush->m_user_brush = true;
brush->on_click = std::bind(&NodePanelBrush::handle_click, this, std::placeholders::_1);
save();
} }
}); });
}; };
@@ -126,26 +205,13 @@ void NodePanelBrush::init()
if (m_current) if (m_current)
{ {
int idx = m_container->get_child_index(m_current); int idx = m_container->get_child_index(m_current);
if (m_current->m_user_brush) const auto plan = pp::app::plan_brush_texture_list_remove(
{ static_cast<int>(m_container->m_children.size()),
// only delete user brushes idx,
Asset::delete_file(m_current->thumb_path); m_current->m_user_brush);
Asset::delete_file(m_current->high_path); if (plan) {
execute_texture_list_plan(plan.value());
} }
m_container->remove_child(m_current);
if (m_container->m_children.size() > 0)
{
idx = std::max(0, std::min(idx, (int)m_container->m_children.size() - 1));
m_current = (NodeButtonBrush*)m_container->m_children[idx].get();
m_current->m_selected = true;
if (on_brush_changed)
on_brush_changed(this, idx);
}
else
{
m_current = nullptr;
}
save();
} }
}; };
@@ -154,9 +220,13 @@ void NodePanelBrush::init()
if (m_current) if (m_current)
{ {
int idx = m_container->get_child_index(m_current); int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx - 1, (int)m_container->m_children.size() - 1)); const auto plan = pp::app::plan_brush_texture_list_move(
m_container->move_child(m_current, idx); static_cast<int>(m_container->m_children.size()),
save(); idx,
-1);
if (plan) {
execute_texture_list_plan(plan.value());
}
} }
}; };
@@ -165,9 +235,13 @@ void NodePanelBrush::init()
if (m_current) if (m_current)
{ {
int idx = m_container->get_child_index(m_current); int idx = m_container->get_child_index(m_current);
idx = std::max(0, std::min(idx + 1, (int)m_container->m_children.size() - 1)); const auto plan = pp::app::plan_brush_texture_list_move(
m_container->move_child(m_current, idx); static_cast<int>(m_container->m_children.size()),
save(); idx,
1);
if (plan) {
execute_texture_list_plan(plan.value());
}
} }
}; };

View File

@@ -9,6 +9,10 @@
#include "serializer.h" #include "serializer.h"
#include "node_button.h" #include "node_button.h"
namespace pp::app {
struct BrushTextureListPlan;
}
class NodeButtonBrush : public NodeButtonCustom, public Serializer::Type class NodeButtonBrush : public NodeButtonCustom, public Serializer::Type
{ {
public: public:
@@ -38,6 +42,7 @@ class NodePanelBrush : public Node
NodeButtonCustom* m_btn_down; NodeButtonCustom* m_btn_down;
NodeButtonCustom* m_btn_remove; NodeButtonCustom* m_btn_remove;
bool m_interacted = false; bool m_interacted = false;
void execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan);
public: public:
NodeScroll* m_container; NodeScroll* m_container;
std::string m_dir_name; std::string m_dir_name;

View File

@@ -1132,6 +1132,30 @@ if(TARGET pano_cli)
LABELS "app;paint;integration;desktop-fast;fuzz" LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_texture_list_add_smoke
COMMAND pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png)
set_tests_properties(pano_cli_plan_brush_texture_list_add_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"add-texture\".*\"source\":\"C:/Temp/soft.png\".*\"path\":\"data/brushes/soft.png\".*\"thumb\":\"data/brushes/thumbs/soft.png\".*\"brushName\":\"soft\".*\"userTexture\":true.*\"convertsBrushAlpha\":true")
add_test(NAME pano_cli_plan_brush_texture_list_remove_user_smoke
COMMAND pano_cli plan-brush-texture-list --kind remove --item-count 3 --current-index 2 --user-texture)
set_tests_properties(pano_cli_plan_brush_texture_list_remove_user_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"remove-texture\".*\"itemCount\":3.*\"currentIndex\":2.*\"targetIndex\":1.*\"deletesTextureFiles\":true.*\"notifiesSelection\":true")
add_test(NAME pano_cli_plan_brush_texture_list_move_edge_smoke
COMMAND pano_cli plan-brush-texture-list --kind move --item-count 3 --current-index 0 --offset -1)
set_tests_properties(pano_cli_plan_brush_texture_list_move_edge_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"move-texture\".*\"currentIndex\":0.*\"targetIndex\":0.*\"moveOffset\":-1.*\"noOp\":true")
add_test(NAME pano_cli_plan_brush_texture_list_rejects_bad_source
COMMAND pano_cli plan-brush-texture-list --kind add --source no-extension)
set_tests_properties(pano_cli_plan_brush_texture_list_rejects_bad_source PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_canvas_tool_draw_smoke add_test(NAME pano_cli_plan_canvas_tool_draw_smoke
COMMAND pano_cli plan-canvas-tool --kind draw) COMMAND pano_cli plan-canvas-tool --kind draw)
set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES

View File

@@ -65,6 +65,76 @@ public:
std::string call_order; std::string call_order;
}; };
class FakeBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) override
{
if (fail_add) {
call_order += "add-failed;";
return pp::foundation::Status::invalid_argument("fake add failure");
}
adds += 1;
last_source_path = std::string(source_path);
last_high_path = std::string(high_path);
last_thumbnail_path = std::string(thumbnail_path);
last_brush_name = std::string(brush_name);
last_converts_brush_alpha = converts_brush_alpha;
call_order += "add;";
return pp::foundation::Status::success();
}
void remove_texture(int index, bool delete_texture_files) override
{
removes += 1;
last_index = index;
last_deletes_texture_files = delete_texture_files;
call_order += "remove;";
}
void move_texture(int from_index, int to_index) override
{
moves += 1;
last_index = from_index;
last_target_index = to_index;
call_order += "move;";
}
void select_texture(int index) override
{
selections += 1;
last_target_index = index;
call_order += "select;";
}
void save_texture_list() override
{
saves += 1;
call_order += "save;";
}
int adds = 0;
int removes = 0;
int moves = 0;
int selections = 0;
int saves = 0;
int last_index = -1;
int last_target_index = -1;
std::string last_source_path;
std::string last_high_path;
std::string last_thumbnail_path;
std::string last_brush_name;
bool last_converts_brush_alpha = false;
bool last_deletes_texture_files = false;
bool fail_add = 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);
@@ -132,6 +202,82 @@ 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 texture_list_add_plans_target_paths_and_rejects_bad_input(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_brush_texture_list_add(
"brushes",
"D:/Paint/data",
"C:/Users/artist/My Brush.JPG");
PP_EXPECT(harness, plan);
if (plan) {
PP_EXPECT(harness, plan.value().operation == pp::app::BrushTextureListOperation::add_texture);
PP_EXPECT(harness, plan.value().source_path == "C:/Users/artist/My Brush.JPG");
PP_EXPECT(harness, plan.value().brush_name == "My Brush");
PP_EXPECT(harness, plan.value().high_path == "D:/Paint/data/brushes/My Brush.png");
PP_EXPECT(harness, plan.value().thumbnail_path == "D:/Paint/data/brushes/thumbs/My Brush.png");
PP_EXPECT(harness, plan.value().user_texture);
PP_EXPECT(harness, plan.value().saves_list);
PP_EXPECT(harness, plan.value().converts_brush_alpha);
}
const auto pattern = pp::app::plan_brush_texture_list_add(
"patterns",
"D:/Paint/data",
R"(C:\Textures\noise.png)");
PP_EXPECT(harness, pattern);
if (pattern) {
PP_EXPECT(harness, !pattern.value().converts_brush_alpha);
PP_EXPECT(harness, pattern.value().brush_name == "noise");
}
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("", "D:/Paint/data", "a.png"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "", "a.png"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "no-extension"));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/dir/"));
}
void texture_list_remove_and_move_plans_handle_edges(pp::tests::Harness& harness)
{
const auto remove_middle = pp::app::plan_brush_texture_list_remove(3, 1, true);
PP_EXPECT(harness, remove_middle);
if (remove_middle) {
PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushTextureListOperation::remove_texture);
PP_EXPECT(harness, remove_middle.value().current_index == 1);
PP_EXPECT(harness, remove_middle.value().target_index == 1);
PP_EXPECT(harness, remove_middle.value().deletes_texture_files);
PP_EXPECT(harness, remove_middle.value().notifies_selection);
PP_EXPECT(harness, remove_middle.value().saves_list);
}
const auto remove_last = pp::app::plan_brush_texture_list_remove(1, 0, false);
PP_EXPECT(harness, remove_last);
if (remove_last) {
PP_EXPECT(harness, remove_last.value().target_index == -1);
PP_EXPECT(harness, !remove_last.value().deletes_texture_files);
PP_EXPECT(harness, !remove_last.value().notifies_selection);
}
const auto move_up_edge = pp::app::plan_brush_texture_list_move(3, 0, -1);
PP_EXPECT(harness, move_up_edge);
if (move_up_edge) {
PP_EXPECT(harness, move_up_edge.value().operation == pp::app::BrushTextureListOperation::move_texture);
PP_EXPECT(harness, move_up_edge.value().target_index == 0);
PP_EXPECT(harness, move_up_edge.value().no_op);
}
const auto move_down = pp::app::plan_brush_texture_list_move(3, 1, 1);
PP_EXPECT(harness, move_down);
if (move_down) {
PP_EXPECT(harness, move_down.value().target_index == 2);
PP_EXPECT(harness, !move_down.value().no_op);
}
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(0, 0, true));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(2, 2, true));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, 0, 0));
PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, -1, 1));
}
void executor_dispatches_color_and_refresh(pp::tests::Harness& harness) void executor_dispatches_color_and_refresh(pp::tests::Harness& harness)
{ {
FakeBrushUiServices services; FakeBrushUiServices services;
@@ -197,6 +343,51 @@ void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness)
PP_EXPECT(harness, services.call_order == "refresh;"); PP_EXPECT(harness, services.call_order == "refresh;");
} }
void texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness)
{
FakeBrushTextureListServices services;
const auto add = pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/Temp/soft.png");
PP_EXPECT(harness, add);
if (add) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(add.value(), services).ok());
}
const auto remove = pp::app::plan_brush_texture_list_remove(3, 2, true);
PP_EXPECT(harness, remove);
if (remove) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(remove.value(), services).ok());
}
const auto move = pp::app::plan_brush_texture_list_move(3, 1, -1);
PP_EXPECT(harness, move);
if (move) {
PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(move.value(), services).ok());
}
PP_EXPECT(harness, services.adds == 1);
PP_EXPECT(harness, services.last_source_path == "C:/Temp/soft.png");
PP_EXPECT(harness, services.last_high_path == "D:/Paint/data/brushes/soft.png");
PP_EXPECT(harness, services.last_thumbnail_path == "D:/Paint/data/brushes/thumbs/soft.png");
PP_EXPECT(harness, services.last_brush_name == "soft");
PP_EXPECT(harness, services.last_converts_brush_alpha);
PP_EXPECT(harness, services.removes == 1);
PP_EXPECT(harness, services.moves == 1);
PP_EXPECT(harness, services.selections == 1);
PP_EXPECT(harness, services.saves == 3);
PP_EXPECT(harness, services.last_deletes_texture_files);
PP_EXPECT(harness, services.call_order == "add;save;remove;select;save;move;save;");
FakeBrushTextureListServices failing_services;
failing_services.fail_add = true;
PP_EXPECT(harness, add);
if (add) {
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add.value(), failing_services).ok());
}
PP_EXPECT(harness, failing_services.saves == 0);
PP_EXPECT(harness, failing_services.call_order == "add-failed;");
}
void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness) void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
{ {
FakeBrushUiServices services; FakeBrushUiServices services;
@@ -218,6 +409,20 @@ void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
PP_EXPECT(harness, services.color_sets == 0); PP_EXPECT(harness, services.color_sets == 0);
PP_EXPECT(harness, services.texture_sets == 0); PP_EXPECT(harness, services.texture_sets == 0);
PP_EXPECT(harness, services.refreshes == 0); PP_EXPECT(harness, services.refreshes == 0);
FakeBrushTextureListServices list_services;
pp::app::BrushTextureListPlan add;
add.operation = pp::app::BrushTextureListOperation::add_texture;
add.source_path = "source.png";
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add, list_services).ok());
pp::app::BrushTextureListPlan move;
move.operation = pp::app::BrushTextureListOperation::move_texture;
move.item_count = 1;
move.current_index = 0;
move.target_index = 1;
PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(move, list_services).ok());
PP_EXPECT(harness, list_services.call_order.empty());
} }
} // namespace } // namespace
@@ -229,9 +434,12 @@ 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("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input);
harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges);
harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh); 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 texture and preset", executor_dispatches_texture_and_preset);
harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only); harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
harness.run("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure);
harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads); harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
return harness.finish(); return harness.finish();
} }

View File

@@ -316,6 +316,17 @@ struct PlanBrushOperationArgs {
bool has_brush = true; bool has_brush = true;
}; };
struct PlanBrushTextureListArgs {
std::string kind = "add";
std::string directory_name = "brushes";
std::string data_path = "data";
std::string source_path = "source.png";
int item_count = 1;
int current_index = 0;
int offset = 1;
bool current_is_user_texture = false;
};
struct PlanGridOperationArgs { struct PlanGridOperationArgs {
std::string kind = "pick"; std::string kind = "pick";
std::string path; std::string path;
@@ -1087,6 +1098,20 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
return "stroke-settings-changed"; return "stroke-settings-changed";
} }
const char* brush_texture_list_operation_name(pp::app::BrushTextureListOperation operation) noexcept
{
switch (operation) {
case pp::app::BrushTextureListOperation::add_texture:
return "add-texture";
case pp::app::BrushTextureListOperation::remove_texture:
return "remove-texture";
case pp::app::BrushTextureListOperation::move_texture:
return "move-texture";
}
return "add-texture";
}
const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept
{ {
switch (operation) { switch (operation) {
@@ -1613,6 +1638,7 @@ void print_help()
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n"
<< " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n" << " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\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-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-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n"
<< " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n"
<< " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n"
<< " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n"
@@ -4283,6 +4309,116 @@ int plan_brush_operation(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_brush_texture_list_args(
int argc,
char** argv,
PlanBrushTextureListArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind" || key == "--dir" || key == "--data-path" || key == "--source") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--kind") {
args.kind = argv[++i];
} else if (key == "--dir") {
args.directory_name = argv[++i];
} else if (key == "--data-path") {
args.data_path = argv[++i];
} else {
args.source_path = argv[++i];
}
} else if (key == "--item-count" || key == "--current-index" || key == "--offset") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
if (key == "--item-count") {
args.item_count = value.value();
} else if (key == "--current-index") {
args.current_index = value.value();
} else {
args.offset = value.value();
}
} else if (key == "--user-texture") {
args.current_is_user_texture = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::BrushTextureListPlan> make_brush_texture_list_plan(
const PlanBrushTextureListArgs& args)
{
if (args.kind == "add") {
return pp::app::plan_brush_texture_list_add(args.directory_name, args.data_path, args.source_path);
}
if (args.kind == "remove") {
return pp::app::plan_brush_texture_list_remove(
args.item_count,
args.current_index,
args.current_is_user_texture);
}
if (args.kind == "move" || args.kind == "up" || args.kind == "down") {
const int offset = args.kind == "up" ? -1 : (args.kind == "down" ? 1 : args.offset);
return pp::app::plan_brush_texture_list_move(args.item_count, args.current_index, offset);
}
return pp::foundation::Result<pp::app::BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("unknown brush texture list operation kind"));
}
int plan_brush_texture_list(int argc, char** argv)
{
PlanBrushTextureListArgs args;
const auto status = parse_plan_brush_texture_list_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-brush-texture-list", status.message);
return 2;
}
const auto plan = make_brush_texture_list_plan(args);
if (!plan) {
print_error("plan-brush-texture-list", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-brush-texture-list\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"dir\":\"" << json_escape(args.directory_name)
<< "\",\"dataPath\":\"" << json_escape(args.data_path)
<< "\",\"source\":\"" << json_escape(args.source_path)
<< "\",\"itemCount\":" << args.item_count
<< ",\"currentIndex\":" << args.current_index
<< ",\"offset\":" << args.offset
<< ",\"currentIsUserTexture\":" << json_bool(args.current_is_user_texture)
<< "},\"plan\":{\"operation\":\"" << brush_texture_list_operation_name(value.operation)
<< "\",\"itemCount\":" << value.item_count
<< ",\"currentIndex\":" << value.current_index
<< ",\"targetIndex\":" << value.target_index
<< ",\"moveOffset\":" << value.move_offset
<< ",\"source\":\"" << json_escape(value.source_path)
<< "\",\"path\":\"" << json_escape(value.high_path)
<< "\",\"thumb\":\"" << json_escape(value.thumbnail_path)
<< "\",\"brushName\":\"" << json_escape(value.brush_name)
<< "\",\"userTexture\":" << json_bool(value.user_texture)
<< ",\"deletesTextureFiles\":" << json_bool(value.deletes_texture_files)
<< ",\"savesList\":" << json_bool(value.saves_list)
<< ",\"notifiesSelection\":" << json_bool(value.notifies_selection)
<< ",\"convertsBrushAlpha\":" << json_bool(value.converts_brush_alpha)
<< ",\"noOp\":" << json_bool(value.no_op)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_canvas_tool_args( pp::foundation::Status parse_plan_canvas_tool_args(
int argc, int argc,
char** argv, char** argv,
@@ -7405,6 +7541,10 @@ int main(int argc, char** argv)
return plan_brush_operation(argc, argv); return plan_brush_operation(argc, argv);
} }
if (command == "plan-brush-texture-list") {
return plan_brush_texture_list(argc, argv);
}
if (command == "plan-canvas-tool") { if (command == "plan-canvas-tool") {
return plan_canvas_tool(argc, argv); return plan_canvas_tool(argc, argv);
} }