From dc23a5648d64f7fcabff7b63fc1dcdf42f571987 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 17:42:09 +0200 Subject: [PATCH] Add brush stroke control boundary --- docs/modernization/capability-map.md | 2 +- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 17 +- src/app_core/brush_ui.h | 255 +++++++++++++++++ src/node_panel_stroke.cpp | 345 ++++++++++++++++------- src/node_panel_stroke.h | 17 +- tests/CMakeLists.txt | 36 +++ tests/app_core/brush_ui_tests.cpp | 182 +++++++++++++ tools/pano_cli/main.cpp | 394 +++++++++++++++++++++++++++ 9 files changed, 1141 insertions(+), 109 deletions(-) diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 1044ddc..0806c6f 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -32,7 +32,7 @@ and validation command. | Capability | Current Area | Target Owner | Required Tests | | --- | --- | --- | --- | -| Brush settings serialization | `Brush`, `Serializer` | `pp_paint`, `pp_assets` | Round-trip and boundary values | +| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests | | ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR | | PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture | | Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index afe35bf..de80819 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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-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-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-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, and `BrushStrokeControlServices`, but the live adapter still mutates legacy `Brush`/`Canvas::I`, 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`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter | | 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-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 | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index ab82b02..87f37e9 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -515,6 +515,11 @@ the legacy `Brush`/panel adapter mutates brush state or loads brush resources. 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-brush-stroke-control` exposes app-core planning for the live +stroke panel's slider, checkbox, blend-mode, tip-aspect reset, and default +brush reset commands. `NodePanelStroke` now dispatches those controls through +`BrushStrokeControlServices` before the legacy `Canvas::I`/`Brush`/stroke-panel +adapter continues. `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 commands. Canvas tool execution now dispatches through `CanvasToolServices` @@ -1232,8 +1237,9 @@ Results: invalid color rejection, texture-path validation, preset-brush availability, preserve-current-color intent, stroke-settings refresh intent, texture-list add target path planning, user-texture removal intent, clamped reorder intent, - service dispatch ordering, texture/preset/list execution payloads, execution - failure preservation, and invalid execution payload rejection. + stroke-control slider/toggle/blend/reset planning, service dispatch ordering, + texture/preset/list/stroke-control execution payloads, execution failure + preservation, and invalid execution payload rejection. - `pano_cli_plan_brush_operation_color_smoke`, `pano_cli_plan_brush_operation_texture_smoke`, `pano_cli_plan_brush_operation_preset_smoke`, @@ -1245,6 +1251,13 @@ Results: `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. +- `pano_cli_plan_brush_stroke_control_float_smoke`, + `pano_cli_plan_brush_stroke_control_toggle_smoke`, + `pano_cli_plan_brush_stroke_control_blend_smoke`, + `pano_cli_plan_brush_stroke_control_reset_smoke`, + `pano_cli_plan_brush_stroke_control_rejects_bad_setting`, and + `pano_cli_plan_brush_stroke_control_rejects_bad_blend` passed and expose live + stroke-panel slider/toggle/blend/reset planning as JSON automation. - `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear planning, lightmap capability and limit checks, missing-heightmap no-op behavior, and commit canvas gating. diff --git a/src/app_core/brush_ui.h b/src/app_core/brush_ui.h index 135365b..fd26f9a 100644 --- a/src/app_core/brush_ui.h +++ b/src/app_core/brush_ui.h @@ -28,6 +28,83 @@ enum class BrushTextureListOperation { move_texture, }; +enum class BrushStrokeControlOperation { + set_float, + set_bool, + set_blend_mode, + reset_tip_aspect, + reset_default_brush, +}; + +enum class BrushStrokeFloatSetting { + tip_size, + tip_spacing, + tip_flow, + tip_opacity, + tip_angle, + tip_angle_smooth, + tip_mix, + tip_wet, + tip_noise, + tip_hue, + tip_saturation, + tip_value, + jitter_scale, + jitter_angle, + jitter_scatter, + jitter_flow, + jitter_opacity, + jitter_hue, + jitter_saturation, + jitter_value, + jitter_aspect, + dual_size, + dual_spacing, + dual_scatter, + tip_aspect, + dual_opacity, + dual_flow, + dual_rotate, + pattern_scale, + pattern_brightness, + pattern_contrast, + pattern_depth, +}; + +enum class BrushStrokeBoolSetting { + tip_angle_init, + tip_angle_follow, + tip_flow_pressure, + tip_opacity_pressure, + tip_size_pressure, + jitter_scatter_both_axis, + jitter_aspect_both_axis, + jitter_hsv_each_sample, + tip_invert, + tip_flip_x, + tip_flip_y, + pattern_enabled, + dual_enabled, + dual_scatter_both_axis, + dual_invert, + dual_flip_x, + dual_flip_y, + dual_random_flip, + tip_random_flip_x, + tip_random_flip_y, + pattern_each_sample, + pattern_invert, + pattern_flip_x, + pattern_flip_y, + pattern_random_offset, +}; + +enum class BrushStrokeBlendSetting { + tip, + dual, + pattern, +}; + struct BrushUiPlan { BrushUiOperation operation = BrushUiOperation::stroke_settings_changed; BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip; @@ -62,6 +139,20 @@ struct BrushTextureListPlan { bool no_op = false; }; +struct BrushStrokeControlPlan { + BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float; + BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size; + BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init; + BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip; + float float_value = 0.0F; + bool bool_value = false; + int blend_mode = 0; + bool mutates_brush = false; + bool updates_controls = false; + bool refreshes_preview = false; + bool notifies_stroke_change = false; +}; + class BrushUiServices { public: virtual ~BrushUiServices() = default; @@ -88,6 +179,20 @@ public: virtual void save_texture_list() = 0; }; +class BrushStrokeControlServices { +public: + virtual ~BrushStrokeControlServices() = default; + + virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0; + virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0; + virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0; + virtual void reset_tip_aspect(float value) = 0; + virtual void reset_default_brush() = 0; + virtual void update_stroke_controls() = 0; + virtual void refresh_stroke_preview() = 0; + virtual void notify_stroke_changed() = 0; +}; + [[nodiscard]] inline pp::foundation::Result brush_texture_source_stem( std::string_view source_path) noexcept { @@ -116,6 +221,24 @@ public: return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept +{ + if (!std::isfinite(value)) { + return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept +{ + if (blend_mode < 0 || blend_mode > 63) { + return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63"); + } + + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Result plan_brush_ui_color( float r, float g, @@ -189,6 +312,81 @@ public: return plan; } +[[nodiscard]] inline pp::foundation::Result plan_brush_stroke_float_setting( + BrushStrokeFloatSetting setting, + float value) +{ + const auto status = validate_brush_stroke_float(value); + if (!status.ok()) { + return pp::foundation::Result::failure(status); + } + + BrushStrokeControlPlan plan; + plan.operation = BrushStrokeControlOperation::set_float; + plan.float_setting = setting; + plan.float_value = value; + plan.mutates_brush = true; + plan.refreshes_preview = true; + plan.notifies_stroke_change = true; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting( + BrushStrokeBoolSetting setting, + bool value) noexcept +{ + BrushStrokeControlPlan plan; + plan.operation = BrushStrokeControlOperation::set_bool; + plan.bool_setting = setting; + plan.bool_value = value; + plan.mutates_brush = true; + plan.refreshes_preview = true; + plan.notifies_stroke_change = true; + return plan; +} + +[[nodiscard]] inline pp::foundation::Result plan_brush_stroke_blend_mode( + BrushStrokeBlendSetting setting, + int blend_mode) +{ + const auto status = validate_brush_stroke_blend_mode(blend_mode); + if (!status.ok()) { + return pp::foundation::Result::failure(status); + } + + BrushStrokeControlPlan plan; + plan.operation = BrushStrokeControlOperation::set_blend_mode; + plan.blend_setting = setting; + plan.blend_mode = blend_mode; + plan.mutates_brush = true; + plan.refreshes_preview = true; + plan.notifies_stroke_change = true; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept +{ + BrushStrokeControlPlan plan; + plan.operation = BrushStrokeControlOperation::reset_tip_aspect; + plan.float_setting = BrushStrokeFloatSetting::tip_aspect; + plan.float_value = value; + plan.mutates_brush = true; + plan.refreshes_preview = true; + plan.notifies_stroke_change = true; + return plan; +} + +[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept +{ + BrushStrokeControlPlan plan; + plan.operation = BrushStrokeControlOperation::reset_default_brush; + plan.mutates_brush = true; + plan.updates_controls = true; + plan.refreshes_preview = true; + plan.notifies_stroke_change = true; + return plan; +} + [[nodiscard]] inline pp::foundation::Result plan_brush_texture_list_add( std::string_view directory_name, std::string_view data_path, @@ -315,6 +513,63 @@ public: return pp::foundation::Status::invalid_argument("unknown brush UI operation"); } +[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan( + const BrushStrokeControlPlan& plan, + BrushStrokeControlServices& services) +{ + switch (plan.operation) { + case BrushStrokeControlOperation::set_float: + { + const auto status = validate_brush_stroke_float(plan.float_value); + if (!status.ok()) { + return status; + } + services.set_float_setting(plan.float_setting, plan.float_value); + break; + } + + case BrushStrokeControlOperation::set_bool: + services.set_bool_setting(plan.bool_setting, plan.bool_value); + break; + + case BrushStrokeControlOperation::set_blend_mode: + { + const auto status = validate_brush_stroke_blend_mode(plan.blend_mode); + if (!status.ok()) { + return status; + } + services.set_blend_mode(plan.blend_setting, plan.blend_mode); + break; + } + + case BrushStrokeControlOperation::reset_tip_aspect: + { + const auto status = validate_brush_stroke_float(plan.float_value); + if (!status.ok()) { + return status; + } + services.reset_tip_aspect(plan.float_value); + break; + } + + case BrushStrokeControlOperation::reset_default_brush: + services.reset_default_brush(); + break; + } + + if (plan.updates_controls) { + services.update_stroke_controls(); + } + if (plan.refreshes_preview) { + services.refresh_stroke_preview(); + } + if (plan.notifies_stroke_change) { + services.notify_stroke_changed(); + } + + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan( const BrushTextureListPlan& plan, BrushTextureListServices& services) diff --git a/src/node_panel_stroke.cpp b/src/node_panel_stroke.cpp index 1876df0..428d2b9 100644 --- a/src/node_panel_stroke.cpp +++ b/src/node_panel_stroke.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include "app_core/brush_ui.h" #include "log.h" #include "node_panel_stroke.h" #include "canvas.h" @@ -6,6 +7,137 @@ #include "app.h" #include "abr.h" +namespace { + +class LegacyBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices { +public: + explicit LegacyBrushStrokeControlServices(NodePanelStroke& panel) : panel_(panel) {} + + void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override + { + auto& brush = *Canvas::I->m_current_brush; + switch (setting) { + case pp::app::BrushStrokeFloatSetting::tip_size: brush.m_tip_size = value; break; + case pp::app::BrushStrokeFloatSetting::tip_spacing: brush.m_tip_spacing = value; break; + case pp::app::BrushStrokeFloatSetting::tip_flow: brush.m_tip_flow = value; break; + case pp::app::BrushStrokeFloatSetting::tip_opacity: brush.m_tip_opacity = value; break; + case pp::app::BrushStrokeFloatSetting::tip_angle: brush.m_tip_angle = value; break; + case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: brush.m_tip_angle_smooth = value; break; + case pp::app::BrushStrokeFloatSetting::tip_mix: brush.m_tip_mix = value; break; + case pp::app::BrushStrokeFloatSetting::tip_wet: brush.m_tip_wet = value; break; + case pp::app::BrushStrokeFloatSetting::tip_noise: brush.m_tip_noise = value; break; + case pp::app::BrushStrokeFloatSetting::tip_hue: brush.m_tip_hue = value; break; + case pp::app::BrushStrokeFloatSetting::tip_saturation: brush.m_tip_sat = value; break; + case pp::app::BrushStrokeFloatSetting::tip_value: brush.m_tip_val = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_scale: brush.m_jitter_scale = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_angle: brush.m_jitter_angle = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_scatter: brush.m_jitter_scatter = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_flow: brush.m_jitter_flow = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_opacity: brush.m_jitter_opacity = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_hue: brush.m_jitter_hue = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_saturation: brush.m_jitter_sat = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_value: brush.m_jitter_val = value; break; + case pp::app::BrushStrokeFloatSetting::jitter_aspect: brush.m_jitter_aspect = value; break; + case pp::app::BrushStrokeFloatSetting::dual_size: brush.m_dual_size = value; break; + case pp::app::BrushStrokeFloatSetting::dual_spacing: brush.m_dual_spacing = value; break; + case pp::app::BrushStrokeFloatSetting::dual_scatter: brush.m_dual_scatter = value; break; + case pp::app::BrushStrokeFloatSetting::tip_aspect: brush.m_tip_aspect = value; break; + case pp::app::BrushStrokeFloatSetting::dual_opacity: brush.m_dual_opacity = value; break; + case pp::app::BrushStrokeFloatSetting::dual_flow: brush.m_dual_flow = value; break; + case pp::app::BrushStrokeFloatSetting::dual_rotate: brush.m_dual_rotate = value; break; + case pp::app::BrushStrokeFloatSetting::pattern_scale: brush.m_pattern_scale = value; break; + case pp::app::BrushStrokeFloatSetting::pattern_brightness: brush.m_pattern_brightness = value; break; + case pp::app::BrushStrokeFloatSetting::pattern_contrast: brush.m_pattern_contrast = value; break; + case pp::app::BrushStrokeFloatSetting::pattern_depth: brush.m_pattern_depth = value; break; + } + } + + void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override + { + auto& brush = *Canvas::I->m_current_brush; + switch (setting) { + case pp::app::BrushStrokeBoolSetting::tip_angle_init: brush.m_tip_angle_init = value; break; + case pp::app::BrushStrokeBoolSetting::tip_angle_follow: brush.m_tip_angle_follow = value; break; + case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: brush.m_tip_flow_pressure = value; break; + case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: brush.m_tip_opacity_pressure = value; break; + case pp::app::BrushStrokeBoolSetting::tip_size_pressure: brush.m_tip_size_pressure = value; break; + case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: brush.m_jitter_scatter_bothaxis = value; break; + case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: brush.m_jitter_aspect_bothaxis = value; break; + case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: brush.m_jitter_hsv_eachsample = value; break; + case pp::app::BrushStrokeBoolSetting::tip_invert: brush.m_tip_invert = value; break; + case pp::app::BrushStrokeBoolSetting::tip_flip_x: brush.m_tip_flipx = value; break; + case pp::app::BrushStrokeBoolSetting::tip_flip_y: brush.m_tip_flipy = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_enabled: brush.m_pattern_enabled = value; break; + case pp::app::BrushStrokeBoolSetting::dual_enabled: brush.m_dual_enabled = value; break; + case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: brush.m_dual_scatter_bothaxis = value; break; + case pp::app::BrushStrokeBoolSetting::dual_invert: brush.m_dual_invert = value; break; + case pp::app::BrushStrokeBoolSetting::dual_flip_x: brush.m_dual_flipx = value; break; + case pp::app::BrushStrokeBoolSetting::dual_flip_y: brush.m_dual_flipy = value; break; + case pp::app::BrushStrokeBoolSetting::dual_random_flip: brush.m_dual_randflip = value; break; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: brush.m_tip_randflipx = value; break; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: brush.m_tip_randflipy = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_each_sample: brush.m_pattern_eachsample = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_invert: brush.m_pattern_invert = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_flip_x: brush.m_pattern_flipx = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_flip_y: brush.m_pattern_flipy = value; break; + case pp::app::BrushStrokeBoolSetting::pattern_random_offset: brush.m_pattern_rand_offset = value; break; + } + } + + void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override + { + auto& brush = *Canvas::I->m_current_brush; + switch (setting) { + case pp::app::BrushStrokeBlendSetting::tip: brush.m_blend_mode = blend_mode; break; + case pp::app::BrushStrokeBlendSetting::dual: brush.m_dual_blend_mode = blend_mode; break; + case pp::app::BrushStrokeBlendSetting::pattern: brush.m_pattern_blend_mode = blend_mode; break; + } + } + + void reset_tip_aspect(float value) override + { + panel_.m_tip_aspect->set_value(value); + Canvas::I->m_current_brush->m_tip_aspect = value; + } + + void reset_default_brush() override + { + auto brush = std::make_shared(); + brush->load_tip( + panel_.m_brush_popup->get_texture_path(panel_.m_default_brush_index), + panel_.m_brush_popup->get_thumb_path(panel_.m_default_brush_index)); + brush->m_tip_size = 30; + brush->m_tip_flow = .9f; + brush->m_tip_spacing = .1f; + brush->m_tip_opacity = 1.f; + Canvas::I->m_current_brush = brush; + } + + void update_stroke_controls() override + { + panel_.update_controls(); + } + + void refresh_stroke_preview() override + { + if (panel_.m_preview) { + panel_.m_preview->m_brush = Canvas::I->m_current_brush; + } + } + + void notify_stroke_changed() override + { + if (panel_.on_stroke_change) { + panel_.on_stroke_change(&panel_); + } + } + +private: + NodePanelStroke& panel_; +}; + +} // namespace + Node* NodePanelStroke::clone_instantiate() const { return new NodePanelStroke(); @@ -156,7 +288,8 @@ void NodePanelStroke::init_controls() //m_presets_popup->m_flood_events = true; //m_presets_popup->m_capture_children = false; - int br_idx = std::max(m_brush_popup->find_brush("Round-Hard"), 0); + m_default_brush_index = std::max(m_brush_popup->find_brush("Round-Hard"), 0); + const int br_idx = m_default_brush_index; // init main brush auto b = std::make_shared(); @@ -331,73 +464,73 @@ void NodePanelStroke::init_controls() m_blend_mode = find("blend-mode"); m_blend_mode->on_select = [this](Node*, int index) { - Canvas::I->m_current_brush->m_blend_mode = index; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, index); + if (plan) { + execute_stroke_control_plan(plan.value()); + } }; - init_slider(m_tip_size, "tip-size", &Brush::m_tip_size); - init_slider(m_tip_spacing, "tip-spacing", &Brush::m_tip_spacing); - init_slider(m_tip_flow, "tip-flow", &Brush::m_tip_flow); - init_slider(m_tip_opacity, "tip-opacity", &Brush::m_tip_opacity); - init_slider(m_tip_angle, "tip-angle", &Brush::m_tip_angle); - init_slider(m_tip_angle_smooth, "tip-angle-smooth", &Brush::m_tip_angle_smooth); - init_slider(m_tip_mix, "tip-mix", &Brush::m_tip_mix); - init_slider(m_tip_wet, "tip-wet", &Brush::m_tip_wet); - init_slider(m_tip_noise, "tip-noise", &Brush::m_tip_noise); - init_slider(m_tip_hue, "tip-hue", &Brush::m_tip_hue); - init_slider(m_tip_sat, "tip-sat", &Brush::m_tip_sat); - init_slider(m_tip_val, "tip-val", &Brush::m_tip_val); - init_slider(m_jitter_scale, "jitter-scale", &Brush::m_jitter_scale); - init_slider(m_jitter_angle, "jitter-angle", &Brush::m_jitter_angle); - init_slider(m_jitter_scatter, "jitter-scatter", &Brush::m_jitter_scatter); - init_slider(m_jitter_flow, "jitter-flow", &Brush::m_jitter_flow); - init_slider(m_jitter_opacity, "jitter-opacity", &Brush::m_jitter_opacity); - init_slider(m_jitter_hue, "jitter-hue", &Brush::m_jitter_hue); - init_slider(m_jitter_sat, "jitter-sat", &Brush::m_jitter_sat); - init_slider(m_jitter_val, "jitter-val", &Brush::m_jitter_val); - init_slider(m_jitter_aspect, "jitter-aspect", &Brush::m_jitter_aspect); + init_slider(m_tip_size, "tip-size", pp::app::BrushStrokeFloatSetting::tip_size, &Brush::m_tip_size); + init_slider(m_tip_spacing, "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing, &Brush::m_tip_spacing); + init_slider(m_tip_flow, "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow, &Brush::m_tip_flow); + init_slider(m_tip_opacity, "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity, &Brush::m_tip_opacity); + init_slider(m_tip_angle, "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle, &Brush::m_tip_angle); + init_slider(m_tip_angle_smooth, "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth, &Brush::m_tip_angle_smooth); + init_slider(m_tip_mix, "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix, &Brush::m_tip_mix); + init_slider(m_tip_wet, "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet, &Brush::m_tip_wet); + init_slider(m_tip_noise, "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise, &Brush::m_tip_noise); + init_slider(m_tip_hue, "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue, &Brush::m_tip_hue); + init_slider(m_tip_sat, "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation, &Brush::m_tip_sat); + init_slider(m_tip_val, "tip-val", pp::app::BrushStrokeFloatSetting::tip_value, &Brush::m_tip_val); + init_slider(m_jitter_scale, "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale, &Brush::m_jitter_scale); + init_slider(m_jitter_angle, "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle, &Brush::m_jitter_angle); + init_slider(m_jitter_scatter, "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter, &Brush::m_jitter_scatter); + init_slider(m_jitter_flow, "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow, &Brush::m_jitter_flow); + init_slider(m_jitter_opacity, "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity, &Brush::m_jitter_opacity); + init_slider(m_jitter_hue, "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue, &Brush::m_jitter_hue); + init_slider(m_jitter_sat, "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation, &Brush::m_jitter_sat); + init_slider(m_jitter_val, "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value, &Brush::m_jitter_val); + init_slider(m_jitter_aspect, "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect, &Brush::m_jitter_aspect); - init_checkbox(m_tip_angle_init, "tip-angle-init", &Brush::m_tip_angle_init); - init_checkbox(m_tip_angle_follow, "tip-angle-follow", &Brush::m_tip_angle_follow); - init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", &Brush::m_tip_flow_pressure); - init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", &Brush::m_tip_opacity_pressure); - init_checkbox(m_tip_size_pressure, "tip-size-pressure", &Brush::m_tip_size_pressure); - init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", &Brush::m_jitter_scatter_bothaxis); - init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", &Brush::m_jitter_aspect_bothaxis); - init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", &Brush::m_jitter_hsv_eachsample); + init_checkbox(m_tip_angle_init, "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init, &Brush::m_tip_angle_init); + init_checkbox(m_tip_angle_follow, "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow, &Brush::m_tip_angle_follow); + init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure, &Brush::m_tip_flow_pressure); + init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure, &Brush::m_tip_opacity_pressure); + init_checkbox(m_tip_size_pressure, "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure, &Brush::m_tip_size_pressure); + init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis, &Brush::m_jitter_scatter_bothaxis); + init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis, &Brush::m_jitter_aspect_bothaxis); + init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample, &Brush::m_jitter_hsv_eachsample); - init_checkbox(m_tip_invert, "tip-invert", &Brush::m_tip_invert); - init_checkbox(m_tip_flipx, "tip-flipx", &Brush::m_tip_flipx); - init_checkbox(m_tip_flipy, "tip-flipy", &Brush::m_tip_flipy); - init_checkbox(m_pattern_enabled, "pattern-enabled", &Brush::m_pattern_enabled); - init_checkbox(m_dual_enabled, "dual-enabled", &Brush::m_dual_enabled); - init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", &Brush::m_dual_scatter_bothaxis); - init_checkbox(m_dual_invert, "dual-invert", &Brush::m_dual_invert); - init_checkbox(m_dual_flipx, "dual-flipx", &Brush::m_dual_flipx); - init_checkbox(m_dual_flipy, "dual-flipy", &Brush::m_dual_flipy); - init_checkbox(m_dual_randflip, "dual-randflip", &Brush::m_dual_randflip); - init_checkbox(m_tip_randflipx, "tip-randflipx", &Brush::m_tip_randflipx); - init_checkbox(m_tip_randflipy, "tip-randflipy", &Brush::m_tip_randflipy); - init_checkbox(m_pattern_eachsample, "pattern-eachsample", &Brush::m_pattern_eachsample); + init_checkbox(m_tip_invert, "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert, &Brush::m_tip_invert); + init_checkbox(m_tip_flipx, "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x, &Brush::m_tip_flipx); + init_checkbox(m_tip_flipy, "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y, &Brush::m_tip_flipy); + init_checkbox(m_pattern_enabled, "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled, &Brush::m_pattern_enabled); + init_checkbox(m_dual_enabled, "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled, &Brush::m_dual_enabled); + init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis, &Brush::m_dual_scatter_bothaxis); + init_checkbox(m_dual_invert, "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert, &Brush::m_dual_invert); + init_checkbox(m_dual_flipx, "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x, &Brush::m_dual_flipx); + init_checkbox(m_dual_flipy, "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y, &Brush::m_dual_flipy); + init_checkbox(m_dual_randflip, "dual-randflip", pp::app::BrushStrokeBoolSetting::dual_random_flip, &Brush::m_dual_randflip); + init_checkbox(m_tip_randflipx, "tip-randflipx", pp::app::BrushStrokeBoolSetting::tip_random_flip_x, &Brush::m_tip_randflipx); + init_checkbox(m_tip_randflipy, "tip-randflipy", pp::app::BrushStrokeBoolSetting::tip_random_flip_y, &Brush::m_tip_randflipy); + init_checkbox(m_pattern_eachsample, "pattern-eachsample", pp::app::BrushStrokeBoolSetting::pattern_each_sample, &Brush::m_pattern_eachsample); - init_checkbox(m_pattern_invert, "pattern-invert", &Brush::m_pattern_invert); - init_checkbox(m_pattern_flipx, "pattern-flipx", &Brush::m_pattern_flipx); - init_checkbox(m_pattern_flipy, "pattern-flipy", &Brush::m_pattern_flipy); - init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", &Brush::m_pattern_rand_offset); + init_checkbox(m_pattern_invert, "pattern-invert", pp::app::BrushStrokeBoolSetting::pattern_invert, &Brush::m_pattern_invert); + init_checkbox(m_pattern_flipx, "pattern-flipx", pp::app::BrushStrokeBoolSetting::pattern_flip_x, &Brush::m_pattern_flipx); + init_checkbox(m_pattern_flipy, "pattern-flipy", pp::app::BrushStrokeBoolSetting::pattern_flip_y, &Brush::m_pattern_flipy); + init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", pp::app::BrushStrokeBoolSetting::pattern_random_offset, &Brush::m_pattern_rand_offset); - init_slider(m_dual_size, "dual-size", &Brush::m_dual_size); - init_slider(m_dual_spacing, "dual-spacing", &Brush::m_dual_spacing); - init_slider(m_dual_scatter, "dual-scatter", &Brush::m_dual_scatter); - init_slider(m_tip_aspect, "tip-aspect", &Brush::m_tip_aspect); - init_slider(m_dual_opacity, "dual-opacity", &Brush::m_dual_opacity); - init_slider(m_dual_flow, "dual-flow", &Brush::m_dual_flow); - init_slider(m_dual_rotate, "dual-rotate", &Brush::m_dual_rotate); - init_slider(m_pattern_scale, "pattern-scale", &Brush::m_pattern_scale); - init_slider(m_pattern_brightness, "pattern-brightness", &Brush::m_pattern_brightness); - init_slider(m_pattern_contrast, "pattern-contrast", &Brush::m_pattern_contrast); - init_slider(m_pattern_depth, "pattern-depth", &Brush::m_pattern_depth); + init_slider(m_dual_size, "dual-size", pp::app::BrushStrokeFloatSetting::dual_size, &Brush::m_dual_size); + init_slider(m_dual_spacing, "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing, &Brush::m_dual_spacing); + init_slider(m_dual_scatter, "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter, &Brush::m_dual_scatter); + init_slider(m_tip_aspect, "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect, &Brush::m_tip_aspect); + init_slider(m_dual_opacity, "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity, &Brush::m_dual_opacity); + init_slider(m_dual_flow, "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow, &Brush::m_dual_flow); + init_slider(m_dual_rotate, "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate, &Brush::m_dual_rotate); + init_slider(m_pattern_scale, "pattern-scale", pp::app::BrushStrokeFloatSetting::pattern_scale, &Brush::m_pattern_scale); + init_slider(m_pattern_brightness, "pattern-brightness", pp::app::BrushStrokeFloatSetting::pattern_brightness, &Brush::m_pattern_brightness); + init_slider(m_pattern_contrast, "pattern-contrast", pp::app::BrushStrokeFloatSetting::pattern_contrast, &Brush::m_pattern_contrast); + init_slider(m_pattern_depth, "pattern-depth", pp::app::BrushStrokeFloatSetting::pattern_depth, &Brush::m_pattern_depth); SliderCurve curve_cubic { [](float v) { return glm::pow(v, 3.f); }, @@ -469,27 +602,23 @@ void NodePanelStroke::init_controls() m_tip_aspect_reset = find("tip-aspect-reset"); m_tip_aspect_reset->on_click = [this](Node*) { - m_tip_aspect->set_value(0.5); - Canvas::I->m_current_brush->m_tip_aspect = 0.5f; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + execute_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset()); }; m_dual_blend_mode = find("dual-blend-mode"); m_dual_blend_mode->on_select = [this](Node*, int index) { - Canvas::I->m_current_brush->m_dual_blend_mode = index; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::dual, index); + if (plan) { + execute_stroke_control_plan(plan.value()); + } }; m_pattern_blend_mode = find("pattern-blend-mode"); m_pattern_blend_mode->on_select = [this](Node*, int index) { - Canvas::I->m_current_brush->m_pattern_blend_mode = index; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, index); + if (plan) { + execute_stroke_control_plan(plan.value()); + } }; m_preview->m_brush = Canvas::I->m_current_brush; @@ -518,54 +647,68 @@ void NodePanelStroke::init_controls() } m_brush_settings_reset = find("brush-settings-reset"); - m_brush_settings_reset->on_click = [br_idx,this](Node*) { - auto b = std::make_shared(); - b->load_tip(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx)); - //b->load_dual(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx)); - //b->load_pattern(m_pattern_popup->get_texture_path(0), m_pattern_popup->get_thumb_path(0)); - b->m_tip_size = 30; - b->m_tip_flow = .9f; - b->m_tip_spacing = .1f; - b->m_tip_opacity = 1.f; - Canvas::I->m_current_brush = b; - update_controls(); - App::I->brush_update(true, true); + m_brush_settings_reset->on_click = [this](Node*) { + execute_stroke_control_plan(pp::app::plan_brush_default_settings_reset()); }; update_controls(); } -void NodePanelStroke::init_slider(NodeSliderH*& target, const char* id, float Brush::* prop) +void NodePanelStroke::init_slider( + NodeSliderH*& target, + const char* id, + pp::app::BrushStrokeFloatSetting setting, + float Brush::* prop) { target = find(id); target->on_value_changed = std::bind(&NodePanelStroke::handle_slide, - this, prop, std::placeholders::_1, std::placeholders::_2); + this, setting, prop, std::placeholders::_1, std::placeholders::_2); //m_canvas->m_brush->*prop = target->m_values; } -void NodePanelStroke::handle_slide(float Brush::* prop, Node* target, float value) +void NodePanelStroke::handle_slide( + pp::app::BrushStrokeFloatSetting setting, + float Brush::* prop, + Node* target, + float value) { auto curve = m_curves.find((NodeSliderH*)target); - Canvas::I->m_current_brush.get()->*prop = curve != m_curves.end() ? curve->second.to_value(value) : value; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + const auto brush_value = curve != m_curves.end() ? curve->second.to_value(value) : value; + const auto plan = pp::app::plan_brush_stroke_float_setting(setting, brush_value); + if (plan) { + execute_stroke_control_plan(plan.value()); + } else { + Canvas::I->m_current_brush.get()->*prop = brush_value; + } } -void NodePanelStroke::init_checkbox(NodeCheckBox*& target, const char* id, bool Brush::* prop) +void NodePanelStroke::init_checkbox( + NodeCheckBox*& target, + const char* id, + pp::app::BrushStrokeBoolSetting setting, + bool Brush::* prop) { target = find(id); target->on_value_changed = std::bind(&NodePanelStroke::handle_checkbox, - this, prop, std::placeholders::_1, std::placeholders::_2); + this, setting, std::placeholders::_2); Canvas::I->m_current_brush.get()->*prop = target->checked; } -void NodePanelStroke::handle_checkbox(bool Brush::* prop, Node *target, bool value) +void NodePanelStroke::handle_checkbox( + pp::app::BrushStrokeBoolSetting setting, + bool value) { - Canvas::I->m_current_brush.get()->*prop = value; - //m_preview->draw_stroke(); - if (on_stroke_change) - on_stroke_change(this); + const auto plan = pp::app::plan_brush_stroke_bool_setting(setting, value); + execute_stroke_control_plan(plan); +} + +void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan) +{ + LegacyBrushStrokeControlServices services(*this); + const auto status = pp::app::execute_brush_stroke_control_plan(plan, services); + if (!status.ok()) { + LOG("Brush stroke control action failed: %s", status.message); + } } kEventResult NodePanelStroke::handle_event(Event* e) diff --git a/src/node_panel_stroke.h b/src/node_panel_stroke.h index 5705b37..13af997 100644 --- a/src/node_panel_stroke.h +++ b/src/node_panel_stroke.h @@ -9,6 +9,13 @@ #include "node_image.h" #include "node_panel_brush.h" +namespace pp::app { +struct BrushStrokeControlPlan; +enum class BrushStrokeBoolSetting; +enum class BrushStrokeFloatSetting; +enum class BrushStrokeBlendSetting; +} // namespace pp::app + class NodePanelStroke : public Node { public: @@ -103,6 +110,7 @@ public: inline float to_slider(float v) { return m_inv(v); } }; std::map m_curves; + int m_default_brush_index = 0; virtual Node* clone_instantiate() const override; virtual void clone_finalize(Node* dest) const override; @@ -116,9 +124,10 @@ public: void set_size(float value, bool normalized, bool propagate); void init_fold(const std::string& name); - void init_slider(NodeSliderH*& slider, const char* id, float Brush::* prop); - void handle_slide(float Brush::* prop, Node* target, float value); + void init_slider(NodeSliderH*& slider, const char* id, pp::app::BrushStrokeFloatSetting setting, float Brush::* prop); + void handle_slide(pp::app::BrushStrokeFloatSetting setting, float Brush::* prop, Node* target, float value); - void init_checkbox(NodeCheckBox*& slider, const char* id, bool Brush::* prop); - void handle_checkbox(bool Brush::* prop, Node* target, bool value); + void init_checkbox(NodeCheckBox*& slider, const char* id, pp::app::BrushStrokeBoolSetting setting, bool Brush::* prop); + void handle_checkbox(pp::app::BrushStrokeBoolSetting setting, bool value); + void execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index feb4255..51af0c1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1156,6 +1156,42 @@ if(TARGET pano_cli) LABELS "app;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_brush_stroke_control_float_smoke + COMMAND pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5) + set_tests_properties(pano_cli_plan_brush_stroke_control_float_smoke PROPERTIES + LABELS "app;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-float\".*\"floatSetting\":\"tip-size\".*\"floatValue\":42.5.*\"mutatesBrush\":true.*\"notifiesStrokeChange\":true") + + add_test(NAME pano_cli_plan_brush_stroke_control_toggle_smoke + COMMAND pano_cli plan-brush-stroke-control --kind bool --setting dual-enabled --enabled) + set_tests_properties(pano_cli_plan_brush_stroke_control_toggle_smoke PROPERTIES + LABELS "app;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-bool\".*\"boolSetting\":\"dual-enabled\".*\"boolValue\":true.*\"refreshesPreview\":true") + + add_test(NAME pano_cli_plan_brush_stroke_control_blend_smoke + COMMAND pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3) + set_tests_properties(pano_cli_plan_brush_stroke_control_blend_smoke PROPERTIES + LABELS "app;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"set-blend-mode\".*\"blendSetting\":\"pattern\".*\"blendMode\":3") + + add_test(NAME pano_cli_plan_brush_stroke_control_reset_smoke + COMMAND pano_cli plan-brush-stroke-control --kind default-reset) + set_tests_properties(pano_cli_plan_brush_stroke_control_reset_smoke PROPERTIES + LABELS "app;paint;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-control\".*\"operation\":\"reset-default-brush\".*\"updatesControls\":true.*\"notifiesStrokeChange\":true") + + add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_setting + COMMAND pano_cli plan-brush-stroke-control --kind float --setting imaginary --value 1) + set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_setting PROPERTIES + LABELS "app;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_brush_stroke_control_rejects_bad_blend + COMMAND pano_cli plan-brush-stroke-control --kind blend --setting tip --blend-mode 99) + set_tests_properties(pano_cli_plan_brush_stroke_control_rejects_bad_blend PROPERTIES + LABELS "app;paint;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_tool_draw_smoke COMMAND pano_cli plan-canvas-tool --kind draw) set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES diff --git a/tests/app_core/brush_ui_tests.cpp b/tests/app_core/brush_ui_tests.cpp index 5e4def5..a32f6e6 100644 --- a/tests/app_core/brush_ui_tests.cpp +++ b/tests/app_core/brush_ui_tests.cpp @@ -135,6 +135,80 @@ public: std::string call_order; }; +class FakeBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices { +public: + void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override + { + float_sets += 1; + last_float_setting = setting; + last_float_value = value; + call_order += "float;"; + } + + void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override + { + bool_sets += 1; + last_bool_setting = setting; + last_bool_value = value; + call_order += "bool;"; + } + + void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override + { + blend_sets += 1; + last_blend_setting = setting; + last_blend_mode = blend_mode; + call_order += "blend;"; + } + + void reset_tip_aspect(float value) override + { + tip_aspect_resets += 1; + last_float_value = value; + call_order += "tip-aspect;"; + } + + void reset_default_brush() override + { + default_resets += 1; + call_order += "default;"; + } + + void update_stroke_controls() override + { + control_updates += 1; + call_order += "controls;"; + } + + void refresh_stroke_preview() override + { + preview_refreshes += 1; + call_order += "preview;"; + } + + void notify_stroke_changed() override + { + stroke_notifications += 1; + call_order += "notify;"; + } + + int float_sets = 0; + int bool_sets = 0; + int blend_sets = 0; + int tip_aspect_resets = 0; + int default_resets = 0; + int control_updates = 0; + int preview_refreshes = 0; + int stroke_notifications = 0; + pp::app::BrushStrokeFloatSetting last_float_setting = pp::app::BrushStrokeFloatSetting::tip_size; + pp::app::BrushStrokeBoolSetting last_bool_setting = pp::app::BrushStrokeBoolSetting::tip_angle_init; + pp::app::BrushStrokeBlendSetting last_blend_setting = pp::app::BrushStrokeBlendSetting::tip; + float last_float_value = 0.0F; + bool last_bool_value = false; + int last_blend_mode = 0; + std::string call_order; +}; + 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); @@ -202,6 +276,58 @@ void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness) PP_EXPECT(harness, !plan.loads_brush_resources); } +void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests::Harness& harness) +{ + const auto slider = pp::app::plan_brush_stroke_float_setting( + pp::app::BrushStrokeFloatSetting::tip_size, + 42.5F); + PP_EXPECT(harness, slider); + if (slider) { + PP_EXPECT(harness, slider.value().operation == pp::app::BrushStrokeControlOperation::set_float); + PP_EXPECT(harness, slider.value().float_setting == pp::app::BrushStrokeFloatSetting::tip_size); + PP_EXPECT(harness, slider.value().float_value == 42.5F); + PP_EXPECT(harness, slider.value().mutates_brush); + PP_EXPECT(harness, slider.value().refreshes_preview); + PP_EXPECT(harness, slider.value().notifies_stroke_change); + } + + const auto checkbox = pp::app::plan_brush_stroke_bool_setting( + pp::app::BrushStrokeBoolSetting::pattern_enabled, + true); + PP_EXPECT(harness, checkbox.operation == pp::app::BrushStrokeControlOperation::set_bool); + PP_EXPECT(harness, checkbox.bool_setting == pp::app::BrushStrokeBoolSetting::pattern_enabled); + PP_EXPECT(harness, checkbox.bool_value); + + const auto blend = pp::app::plan_brush_stroke_blend_mode( + pp::app::BrushStrokeBlendSetting::dual, + 7); + PP_EXPECT(harness, blend); + if (blend) { + PP_EXPECT(harness, blend.value().operation == pp::app::BrushStrokeControlOperation::set_blend_mode); + PP_EXPECT(harness, blend.value().blend_setting == pp::app::BrushStrokeBlendSetting::dual); + PP_EXPECT(harness, blend.value().blend_mode == 7); + } + + const auto tip_aspect = pp::app::plan_brush_tip_aspect_reset(); + PP_EXPECT(harness, tip_aspect.operation == pp::app::BrushStrokeControlOperation::reset_tip_aspect); + PP_EXPECT(harness, tip_aspect.float_setting == pp::app::BrushStrokeFloatSetting::tip_aspect); + PP_EXPECT(harness, tip_aspect.float_value == 0.5F); + + const auto defaults = pp::app::plan_brush_default_settings_reset(); + PP_EXPECT(harness, defaults.operation == pp::app::BrushStrokeControlOperation::reset_default_brush); + PP_EXPECT(harness, defaults.updates_controls); + PP_EXPECT(harness, defaults.refreshes_preview); + PP_EXPECT(harness, defaults.notifies_stroke_change); + + PP_EXPECT( + harness, + !pp::app::plan_brush_stroke_float_setting( + pp::app::BrushStrokeFloatSetting::tip_flow, + std::nanf(""))); + PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, -1)); + PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64)); +} + 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( @@ -388,6 +514,60 @@ void texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& PP_EXPECT(harness, failing_services.call_order == "add-failed;"); } +void stroke_control_executor_dispatches_and_rejects_bad_payloads(pp::tests::Harness& harness) +{ + FakeBrushStrokeControlServices services; + + const auto slider = pp::app::plan_brush_stroke_float_setting( + pp::app::BrushStrokeFloatSetting::pattern_depth, + 0.75F); + PP_EXPECT(harness, slider); + if (slider) { + PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(slider.value(), services).ok()); + } + + const auto checkbox = pp::app::plan_brush_stroke_bool_setting( + pp::app::BrushStrokeBoolSetting::dual_enabled, + true); + PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(checkbox, services).ok()); + + const auto blend = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 3); + PP_EXPECT(harness, blend); + if (blend) { + PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(blend.value(), services).ok()); + } + + PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset(), services).ok()); + PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_default_settings_reset(), services).ok()); + + PP_EXPECT(harness, services.float_sets == 1); + PP_EXPECT(harness, services.last_float_setting == pp::app::BrushStrokeFloatSetting::pattern_depth); + PP_EXPECT(harness, services.last_bool_setting == pp::app::BrushStrokeBoolSetting::dual_enabled); + PP_EXPECT(harness, services.last_blend_setting == pp::app::BrushStrokeBlendSetting::pattern); + PP_EXPECT(harness, services.last_blend_mode == 3); + PP_EXPECT(harness, services.tip_aspect_resets == 1); + PP_EXPECT(harness, services.default_resets == 1); + PP_EXPECT(harness, services.preview_refreshes == 5); + PP_EXPECT(harness, services.control_updates == 1); + PP_EXPECT(harness, services.stroke_notifications == 5); + PP_EXPECT( + harness, + services.call_order == "float;preview;notify;bool;preview;notify;blend;preview;notify;tip-aspect;" + "preview;notify;default;controls;preview;notify;"); + + FakeBrushStrokeControlServices bad_services; + pp::app::BrushStrokeControlPlan bad_float; + bad_float.operation = pp::app::BrushStrokeControlOperation::set_float; + bad_float.float_value = std::nanf(""); + PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_float, bad_services).ok()); + + pp::app::BrushStrokeControlPlan bad_blend; + bad_blend.operation = pp::app::BrushStrokeControlOperation::set_blend_mode; + bad_blend.blend_mode = 99; + PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_blend, bad_services).ok()); + PP_EXPECT(harness, bad_services.call_order.empty()); +} + void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness) { FakeBrushUiServices services; @@ -434,12 +614,14 @@ int main() 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); + harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points); 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 texture and preset", executor_dispatches_texture_and_preset); 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("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads); harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 608bb3d..8ddb119 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -327,6 +327,14 @@ struct PlanBrushTextureListArgs { bool current_is_user_texture = false; }; +struct PlanBrushStrokeControlArgs { + std::string kind = "float"; + std::string setting = "tip-size"; + float value = 1.0F; + bool enabled = true; + int blend_mode = 0; +}; + struct PlanGridOperationArgs { std::string kind = "pick"; std::string path; @@ -1112,6 +1120,111 @@ const char* brush_texture_list_operation_name(pp::app::BrushTextureListOperation return "add-texture"; } +const char* brush_stroke_control_operation_name(pp::app::BrushStrokeControlOperation operation) noexcept +{ + switch (operation) { + case pp::app::BrushStrokeControlOperation::set_float: + return "set-float"; + case pp::app::BrushStrokeControlOperation::set_bool: + return "set-bool"; + case pp::app::BrushStrokeControlOperation::set_blend_mode: + return "set-blend-mode"; + case pp::app::BrushStrokeControlOperation::reset_tip_aspect: + return "reset-tip-aspect"; + case pp::app::BrushStrokeControlOperation::reset_default_brush: + return "reset-default-brush"; + } + + return "set-float"; +} + +const char* brush_stroke_float_setting_name(pp::app::BrushStrokeFloatSetting setting) noexcept +{ + switch (setting) { + case pp::app::BrushStrokeFloatSetting::tip_size: return "tip-size"; + case pp::app::BrushStrokeFloatSetting::tip_spacing: return "tip-spacing"; + case pp::app::BrushStrokeFloatSetting::tip_flow: return "tip-flow"; + case pp::app::BrushStrokeFloatSetting::tip_opacity: return "tip-opacity"; + case pp::app::BrushStrokeFloatSetting::tip_angle: return "tip-angle"; + case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: return "tip-angle-smooth"; + case pp::app::BrushStrokeFloatSetting::tip_mix: return "tip-mix"; + case pp::app::BrushStrokeFloatSetting::tip_wet: return "tip-wet"; + case pp::app::BrushStrokeFloatSetting::tip_noise: return "tip-noise"; + case pp::app::BrushStrokeFloatSetting::tip_hue: return "tip-hue"; + case pp::app::BrushStrokeFloatSetting::tip_saturation: return "tip-saturation"; + case pp::app::BrushStrokeFloatSetting::tip_value: return "tip-value"; + case pp::app::BrushStrokeFloatSetting::jitter_scale: return "jitter-scale"; + case pp::app::BrushStrokeFloatSetting::jitter_angle: return "jitter-angle"; + case pp::app::BrushStrokeFloatSetting::jitter_scatter: return "jitter-scatter"; + case pp::app::BrushStrokeFloatSetting::jitter_flow: return "jitter-flow"; + case pp::app::BrushStrokeFloatSetting::jitter_opacity: return "jitter-opacity"; + case pp::app::BrushStrokeFloatSetting::jitter_hue: return "jitter-hue"; + case pp::app::BrushStrokeFloatSetting::jitter_saturation: return "jitter-saturation"; + case pp::app::BrushStrokeFloatSetting::jitter_value: return "jitter-value"; + case pp::app::BrushStrokeFloatSetting::jitter_aspect: return "jitter-aspect"; + case pp::app::BrushStrokeFloatSetting::dual_size: return "dual-size"; + case pp::app::BrushStrokeFloatSetting::dual_spacing: return "dual-spacing"; + case pp::app::BrushStrokeFloatSetting::dual_scatter: return "dual-scatter"; + case pp::app::BrushStrokeFloatSetting::tip_aspect: return "tip-aspect"; + case pp::app::BrushStrokeFloatSetting::dual_opacity: return "dual-opacity"; + case pp::app::BrushStrokeFloatSetting::dual_flow: return "dual-flow"; + case pp::app::BrushStrokeFloatSetting::dual_rotate: return "dual-rotate"; + case pp::app::BrushStrokeFloatSetting::pattern_scale: return "pattern-scale"; + case pp::app::BrushStrokeFloatSetting::pattern_brightness: return "pattern-brightness"; + case pp::app::BrushStrokeFloatSetting::pattern_contrast: return "pattern-contrast"; + case pp::app::BrushStrokeFloatSetting::pattern_depth: return "pattern-depth"; + } + + return "tip-size"; +} + +const char* brush_stroke_bool_setting_name(pp::app::BrushStrokeBoolSetting setting) noexcept +{ + switch (setting) { + case pp::app::BrushStrokeBoolSetting::tip_angle_init: return "tip-angle-init"; + case pp::app::BrushStrokeBoolSetting::tip_angle_follow: return "tip-angle-follow"; + case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: return "tip-flow-pressure"; + case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: return "tip-opacity-pressure"; + case pp::app::BrushStrokeBoolSetting::tip_size_pressure: return "tip-size-pressure"; + case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: return "jitter-scatter-both-axis"; + case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: return "jitter-aspect-both-axis"; + case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: return "jitter-hsv-each-sample"; + case pp::app::BrushStrokeBoolSetting::tip_invert: return "tip-invert"; + case pp::app::BrushStrokeBoolSetting::tip_flip_x: return "tip-flip-x"; + case pp::app::BrushStrokeBoolSetting::tip_flip_y: return "tip-flip-y"; + case pp::app::BrushStrokeBoolSetting::pattern_enabled: return "pattern-enabled"; + case pp::app::BrushStrokeBoolSetting::dual_enabled: return "dual-enabled"; + case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: return "dual-scatter-both-axis"; + case pp::app::BrushStrokeBoolSetting::dual_invert: return "dual-invert"; + case pp::app::BrushStrokeBoolSetting::dual_flip_x: return "dual-flip-x"; + case pp::app::BrushStrokeBoolSetting::dual_flip_y: return "dual-flip-y"; + case pp::app::BrushStrokeBoolSetting::dual_random_flip: return "dual-random-flip"; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: return "tip-random-flip-x"; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: return "tip-random-flip-y"; + case pp::app::BrushStrokeBoolSetting::pattern_each_sample: return "pattern-each-sample"; + case pp::app::BrushStrokeBoolSetting::pattern_invert: return "pattern-invert"; + case pp::app::BrushStrokeBoolSetting::pattern_flip_x: return "pattern-flip-x"; + case pp::app::BrushStrokeBoolSetting::pattern_flip_y: return "pattern-flip-y"; + case pp::app::BrushStrokeBoolSetting::pattern_random_offset: return "pattern-random-offset"; + } + + return "tip-angle-init"; +} + +const char* brush_stroke_blend_setting_name(pp::app::BrushStrokeBlendSetting setting) noexcept +{ + switch (setting) { + case pp::app::BrushStrokeBlendSetting::tip: + return "tip"; + case pp::app::BrushStrokeBlendSetting::dual: + return "dual"; + case pp::app::BrushStrokeBlendSetting::pattern: + return "pattern"; + } + + return "tip"; +} + const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept { switch (operation) { @@ -1639,6 +1752,7 @@ void print_help() << " 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-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-brush-stroke-control --kind float|bool|blend|tip-aspect-reset|default-reset [--setting NAME] [--value N] [--enabled|--disabled] [--blend-mode N]\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-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" @@ -4419,6 +4533,282 @@ int plan_brush_texture_list(int argc, char** argv) return 0; } +pp::foundation::Result parse_brush_stroke_float_setting( + std::string_view setting) +{ + static constexpr std::array, 32> settings{ { + { "tip-size", pp::app::BrushStrokeFloatSetting::tip_size }, + { "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing }, + { "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow }, + { "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity }, + { "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle }, + { "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth }, + { "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix }, + { "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet }, + { "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise }, + { "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue }, + { "tip-saturation", pp::app::BrushStrokeFloatSetting::tip_saturation }, + { "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation }, + { "tip-value", pp::app::BrushStrokeFloatSetting::tip_value }, + { "tip-val", pp::app::BrushStrokeFloatSetting::tip_value }, + { "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale }, + { "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle }, + { "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter }, + { "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow }, + { "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity }, + { "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue }, + { "jitter-saturation", pp::app::BrushStrokeFloatSetting::jitter_saturation }, + { "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation }, + { "jitter-value", pp::app::BrushStrokeFloatSetting::jitter_value }, + { "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value }, + { "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect }, + { "dual-size", pp::app::BrushStrokeFloatSetting::dual_size }, + { "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing }, + { "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter }, + { "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect }, + { "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity }, + { "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow }, + { "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate }, + } }; + for (const auto& entry : settings) { + if (setting == entry.first) { + return pp::foundation::Result::success(entry.second); + } + } + if (setting == "pattern-scale") { + return pp::foundation::Result::success( + pp::app::BrushStrokeFloatSetting::pattern_scale); + } + if (setting == "pattern-brightness") { + return pp::foundation::Result::success( + pp::app::BrushStrokeFloatSetting::pattern_brightness); + } + if (setting == "pattern-contrast") { + return pp::foundation::Result::success( + pp::app::BrushStrokeFloatSetting::pattern_contrast); + } + if (setting == "pattern-depth") { + return pp::foundation::Result::success( + pp::app::BrushStrokeFloatSetting::pattern_depth); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown brush stroke float setting")); +} + +pp::foundation::Result parse_brush_stroke_bool_setting( + std::string_view setting) +{ + static constexpr std::array, 25> settings{ { + { "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init }, + { "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow }, + { "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure }, + { "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure }, + { "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure }, + { "jitter-scatter-both-axis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis }, + { "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis }, + { "jitter-aspect-both-axis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis }, + { "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis }, + { "jitter-hsv-each-sample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample }, + { "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample }, + { "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert }, + { "tip-flip-x", pp::app::BrushStrokeBoolSetting::tip_flip_x }, + { "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x }, + { "tip-flip-y", pp::app::BrushStrokeBoolSetting::tip_flip_y }, + { "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y }, + { "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled }, + { "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled }, + { "dual-scatter-both-axis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis }, + { "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis }, + { "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert }, + { "dual-flip-x", pp::app::BrushStrokeBoolSetting::dual_flip_x }, + { "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x }, + { "dual-flip-y", pp::app::BrushStrokeBoolSetting::dual_flip_y }, + { "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y }, + } }; + for (const auto& entry : settings) { + if (setting == entry.first) { + return pp::foundation::Result::success(entry.second); + } + } + if (setting == "dual-random-flip" || setting == "dual-randflip") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::dual_random_flip); + } + if (setting == "tip-random-flip-x" || setting == "tip-randflipx") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::tip_random_flip_x); + } + if (setting == "tip-random-flip-y" || setting == "tip-randflipy") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::tip_random_flip_y); + } + if (setting == "pattern-each-sample" || setting == "pattern-eachsample") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::pattern_each_sample); + } + if (setting == "pattern-invert") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::pattern_invert); + } + if (setting == "pattern-flip-x" || setting == "pattern-flipx") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::pattern_flip_x); + } + if (setting == "pattern-flip-y" || setting == "pattern-flipy") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::pattern_flip_y); + } + if (setting == "pattern-random-offset" || setting == "pattern-rand-offset") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBoolSetting::pattern_random_offset); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown brush stroke bool setting")); +} + +pp::foundation::Result parse_brush_stroke_blend_setting( + std::string_view setting) +{ + if (setting == "tip" || setting == "brush") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBlendSetting::tip); + } + if (setting == "dual") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBlendSetting::dual); + } + if (setting == "pattern") { + return pp::foundation::Result::success( + pp::app::BrushStrokeBlendSetting::pattern); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown brush stroke blend setting")); +} + +pp::foundation::Status parse_plan_brush_stroke_control_args( + int argc, + char** argv, + PlanBrushStrokeControlArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind" || key == "--setting") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + if (key == "--kind") { + args.kind = argv[++i]; + } else { + args.setting = argv[++i]; + } + } else if (key == "--value") { + 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(); + } + args.value = value.value(); + } else if (key == "--blend-mode") { + 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(); + } + args.blend_mode = value.value(); + } else if (key == "--enabled") { + args.enabled = true; + } else if (key == "--disabled") { + args.enabled = false; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Result make_brush_stroke_control_plan( + const PlanBrushStrokeControlArgs& args) +{ + if (args.kind == "float" || args.kind == "slider") { + const auto setting = parse_brush_stroke_float_setting(args.setting); + if (!setting) { + return pp::foundation::Result::failure(setting.status()); + } + return pp::app::plan_brush_stroke_float_setting(setting.value(), args.value); + } + if (args.kind == "bool" || args.kind == "toggle" || args.kind == "checkbox") { + const auto setting = parse_brush_stroke_bool_setting(args.setting); + if (!setting) { + return pp::foundation::Result::failure(setting.status()); + } + return pp::foundation::Result::success( + pp::app::plan_brush_stroke_bool_setting(setting.value(), args.enabled)); + } + if (args.kind == "blend" || args.kind == "blend-mode") { + const auto setting = parse_brush_stroke_blend_setting(args.setting); + if (!setting) { + return pp::foundation::Result::failure(setting.status()); + } + return pp::app::plan_brush_stroke_blend_mode(setting.value(), args.blend_mode); + } + if (args.kind == "tip-aspect-reset") { + return pp::foundation::Result::success( + pp::app::plan_brush_tip_aspect_reset()); + } + if (args.kind == "default-reset" || args.kind == "reset") { + return pp::foundation::Result::success( + pp::app::plan_brush_default_settings_reset()); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown brush stroke control kind")); +} + +int plan_brush_stroke_control(int argc, char** argv) +{ + PlanBrushStrokeControlArgs args; + const auto status = parse_plan_brush_stroke_control_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-brush-stroke-control", status.message); + return 2; + } + + const auto plan = make_brush_stroke_control_plan(args); + if (!plan) { + print_error("plan-brush-stroke-control", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-brush-stroke-control\"" + << ",\"state\":{\"kind\":\"" << json_escape(args.kind) + << "\",\"setting\":\"" << json_escape(args.setting) + << "\",\"value\":" << args.value + << ",\"enabled\":" << json_bool(args.enabled) + << ",\"blendMode\":" << args.blend_mode + << "},\"plan\":{\"operation\":\"" << brush_stroke_control_operation_name(value.operation) + << "\",\"floatSetting\":\"" << brush_stroke_float_setting_name(value.float_setting) + << "\",\"boolSetting\":\"" << brush_stroke_bool_setting_name(value.bool_setting) + << "\",\"blendSetting\":\"" << brush_stroke_blend_setting_name(value.blend_setting) + << "\",\"floatValue\":" << value.float_value + << ",\"boolValue\":" << json_bool(value.bool_value) + << ",\"blendMode\":" << value.blend_mode + << ",\"mutatesBrush\":" << json_bool(value.mutates_brush) + << ",\"updatesControls\":" << json_bool(value.updates_controls) + << ",\"refreshesPreview\":" << json_bool(value.refreshes_preview) + << ",\"notifiesStrokeChange\":" << json_bool(value.notifies_stroke_change) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_canvas_tool_args( int argc, char** argv, @@ -7545,6 +7935,10 @@ int main(int argc, char** argv) return plan_brush_texture_list(argc, argv); } + if (command == "plan-brush-stroke-control") { + return plan_brush_stroke_control(argc, argv); + } + if (command == "plan-canvas-tool") { return plan_canvas_tool(argc, argv); }