Add brush stroke control boundary

This commit is contained in:
2026-06-03 17:42:09 +02:00
parent 9adfad9609
commit dc23a5648d
9 changed files with 1141 additions and 109 deletions

View File

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

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

View File

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

View File

@@ -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<std::string_view> 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<BrushUiPlan> plan_brush_ui_color(
float r,
float g,
@@ -189,6 +312,81 @@ public:
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
BrushStrokeFloatSetting setting,
float value)
{
const auto status = validate_brush_stroke_float(value);
if (!status.ok()) {
return pp::foundation::Result<BrushStrokeControlPlan>::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<BrushStrokeControlPlan>::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<BrushStrokeControlPlan> 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<BrushStrokeControlPlan>::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<BrushStrokeControlPlan>::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<BrushTextureListPlan> 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)

View File

@@ -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>();
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<Brush>();
@@ -331,73 +464,73 @@ void NodePanelStroke::init_controls()
m_blend_mode = find<NodeComboBox>("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<NodeButtonCustom>("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<NodeComboBox>("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<NodeComboBox>("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<NodeButton>("brush-settings-reset");
m_brush_settings_reset->on_click = [br_idx,this](Node*) {
auto b = std::make_shared<Brush>();
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<NodeSliderH>(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<NodeCheckBox>(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)

View File

@@ -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<NodeSliderH*, SliderCurve> 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);
};

View File

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

View File

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

View File

@@ -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<pp::app::BrushStrokeFloatSetting> parse_brush_stroke_float_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeFloatSetting>, 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<pp::app::BrushStrokeFloatSetting>::success(entry.second);
}
}
if (setting == "pattern-scale") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_scale);
}
if (setting == "pattern-brightness") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_brightness);
}
if (setting == "pattern-contrast") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_contrast);
}
if (setting == "pattern-depth") {
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::success(
pp::app::BrushStrokeFloatSetting::pattern_depth);
}
return pp::foundation::Result<pp::app::BrushStrokeFloatSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke float setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBoolSetting> parse_brush_stroke_bool_setting(
std::string_view setting)
{
static constexpr std::array<std::pair<std::string_view, pp::app::BrushStrokeBoolSetting>, 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<pp::app::BrushStrokeBoolSetting>::success(entry.second);
}
}
if (setting == "dual-random-flip" || setting == "dual-randflip") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::dual_random_flip);
}
if (setting == "tip-random-flip-x" || setting == "tip-randflipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_x);
}
if (setting == "tip-random-flip-y" || setting == "tip-randflipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::tip_random_flip_y);
}
if (setting == "pattern-each-sample" || setting == "pattern-eachsample") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_each_sample);
}
if (setting == "pattern-invert") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_invert);
}
if (setting == "pattern-flip-x" || setting == "pattern-flipx") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_x);
}
if (setting == "pattern-flip-y" || setting == "pattern-flipy") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_flip_y);
}
if (setting == "pattern-random-offset" || setting == "pattern-rand-offset") {
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::success(
pp::app::BrushStrokeBoolSetting::pattern_random_offset);
}
return pp::foundation::Result<pp::app::BrushStrokeBoolSetting>::failure(
pp::foundation::Status::invalid_argument("unknown brush stroke bool setting"));
}
pp::foundation::Result<pp::app::BrushStrokeBlendSetting> parse_brush_stroke_blend_setting(
std::string_view setting)
{
if (setting == "tip" || setting == "brush") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::tip);
}
if (setting == "dual") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::dual);
}
if (setting == "pattern") {
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::success(
pp::app::BrushStrokeBlendSetting::pattern);
}
return pp::foundation::Result<pp::app::BrushStrokeBlendSetting>::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<pp::app::BrushStrokeControlPlan> 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<pp::app::BrushStrokeControlPlan>::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<pp::app::BrushStrokeControlPlan>::failure(setting.status());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::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<pp::app::BrushStrokeControlPlan>::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<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_tip_aspect_reset());
}
if (args.kind == "default-reset" || args.kind == "reset") {
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::success(
pp::app::plan_brush_default_settings_reset());
}
return pp::foundation::Result<pp::app::BrushStrokeControlPlan>::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);
}