diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 219d5c6..bb18bc8 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -265,12 +265,16 @@ Known local toolchain state: color, texture, preset, stroke-refresh, brush texture-list, and stroke-control execution. `NodePanelBrushPreset` now consumes `pp_app_core` preset-list planning and `BrushPresetListServices` execution for add/select/move/remove/ - clear before the retained legacy bridge mutates child nodes. These paths stay - on the `pp_app_core` contracts while + clear before the retained legacy bridge mutates child nodes. + `NodePanelStroke::update_controls()` now consumes the tested `pp_app_core` + stroke-panel view model for brush float settings, toggles, blend modes, and + thumbnail paths, and `pano_cli plan-brush-stroke-panel-view` exposes that + projection for automation. These paths stay on the `pp_app_core` contracts + while legacy `Brush`, `Canvas::I`, image load/save, `NodePanelBrush`, `NodePanelStroke`, quick/color refreshes, direct preset child-node mutation, - and the temporary `NodePanelBrush` friend adapter remain tracked by - `DEBT-0023`. + brush thumbnail/popup ownership, and the temporary `NodePanelBrush` friend + adapter remain tracked by `DEBT-0023`. - `src/legacy_grid_ui_services.*` is the current UI-shell bridge for grid heightmap picker/load/reload/clear, lightmap render, and heightmap commit execution. It keeps those live paths on the `pp_app_core` contracts while diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 181650d..579185b 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -51,6 +51,12 @@ agent or engineer to remove them without reconstructing context from chat. app, and `pano_cli plan-layer-panel-view` exposes the same path for automation. Legacy layer mutation, UI node ownership, and undo wiring remain open under DEBT-0021. +- 2026-06-05: DEBT-0023 was narrowed again. Stroke-panel control projection now + goes through tested `pp_app_core` planning, `NodePanelStroke::update_controls()` + consumes that view model in the live app, and + `pano_cli plan-brush-stroke-panel-view` exposes the same state path for + automation. Legacy brush mutation, brush thumbnail ownership, popup behavior, + and preset child-node mutation remain open under DEBT-0023. - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, thumbnail, and object-draw history paths now query saved blend state through tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect @@ -87,7 +93,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, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history through the history bridge | 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 planning/execution dispatch, layer panel operation planning/execution dispatch, and layer panel selected-control/visibility view projection now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `NodePanelLayer::update_attributes()`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `pano_cli plan-layer-panel-view`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge and panel adapter still mutate legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | Preserve existing UI/canvas behavior while document layer commands, panel projection, 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`; `pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution and panel state projection are 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 view-model projection, timeline scrub 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`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | 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`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `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, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge still mutates legacy `Brush`/`Canvas::I`, loads/saves legacy brush texture images, refreshes legacy quick/stroke/color widgets, and uses temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands move toward a brush/app/asset command boundary and asset-managed texture/preset 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-preset-list --kind remove --item-count 1 --current-index 0`; `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, preset-list, and stroke-control execution are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, or brush-panel friend access | +| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, stroke-panel view projection, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, `NodePanelStroke::update_controls()`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `pano_cli plan-brush-stroke-panel-view`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge and panel adapter still mutate/read legacy `Brush`/`Canvas::I`, load/save legacy brush texture images, refresh legacy quick/stroke/color widgets, own brush thumbnail paths and popup behavior, and use temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands and view projection move toward a brush/app/asset command boundary and asset-managed texture/preset 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-preset-list --kind remove --item-count 1 --current-index 0`; `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`; `pano_cli plan-brush-stroke-panel-view --tip-size 64 --jitter-scatter 0.4 --dual-disabled --tip-blend-mode 2 --pattern-blend-mode 5`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, stroke-control execution, and stroke-panel projection are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, brush thumbnail/popup ownership, or brush-panel friend access | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | 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; live execution is centralized in `src/legacy_quick_ui_services.*`, but the bridge 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 canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge 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 369a300..92b0b4b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -555,6 +555,11 @@ stroke panel's slider, checkbox, blend-mode, tip-aspect reset, and default brush reset commands. `NodePanelStroke` now dispatches those controls through `BrushStrokeControlServices` in the shared brush bridge before the legacy `Canvas::I`/`Brush`/stroke-panel adapter continues. +`pano_cli plan-brush-stroke-panel-view` exposes the app-core stroke-panel view +projection for brush float settings, toggles, blend modes, and thumbnail paths, +and live `NodePanelStroke::update_controls()` now consumes that tested +projection before applying retained slider-curve, preview, and thumbnail UI +updates. `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` @@ -1750,6 +1755,10 @@ Results: `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. +- `pano_cli_plan_brush_stroke_panel_view_smoke`, + `pano_cli_plan_brush_stroke_panel_view_rejects_bad_float`, and + `pano_cli_plan_brush_stroke_panel_view_rejects_bad_blend` passed and expose + live stroke-panel state projection 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 426092b..fc1f26f 100644 --- a/src/app_core/brush_ui.h +++ b/src/app_core/brush_ui.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include namespace pp::app { @@ -175,6 +177,41 @@ struct BrushStrokeControlPlan { bool notifies_stroke_change = false; }; +struct BrushStrokeFloatValue { + BrushStrokeFloatSetting setting = BrushStrokeFloatSetting::tip_size; + float value = 0.0F; +}; + +struct BrushStrokeBoolValue { + BrushStrokeBoolSetting setting = BrushStrokeBoolSetting::tip_angle_init; + bool value = false; +}; + +struct BrushStrokeBlendValue { + BrushStrokeBlendSetting setting = BrushStrokeBlendSetting::tip; + int blend_mode = 0; +}; + +struct BrushStrokePanelInput { + std::vector float_values; + std::vector bool_values; + std::vector blend_values; + std::string tip_thumbnail_path; + std::string dual_thumbnail_path; + std::string pattern_thumbnail_path; +}; + +struct BrushStrokePanelView { + std::vector float_values; + std::vector bool_values; + std::vector blend_values; + std::string tip_thumbnail_path; + std::string dual_thumbnail_path; + std::string pattern_thumbnail_path; + bool updates_preview = true; + bool updates_thumbnails = true; +}; + class BrushUiServices { public: virtual ~BrushUiServices() = default; @@ -278,6 +315,33 @@ public: return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Result plan_brush_stroke_panel_view( + BrushStrokePanelInput input) +{ + for (const auto& value : input.float_values) { + const auto status = validate_brush_stroke_float(value.value); + if (!status.ok()) { + return pp::foundation::Result::failure(status); + } + } + + for (const auto& value : input.blend_values) { + const auto status = validate_brush_stroke_blend_mode(value.blend_mode); + if (!status.ok()) { + return pp::foundation::Result::failure(status); + } + } + + BrushStrokePanelView view; + view.float_values = std::move(input.float_values); + view.bool_values = std::move(input.bool_values); + view.blend_values = std::move(input.blend_values); + view.tip_thumbnail_path = std::move(input.tip_thumbnail_path); + view.dual_thumbnail_path = std::move(input.dual_thumbnail_path); + view.pattern_thumbnail_path = std::move(input.pattern_thumbnail_path); + return pp::foundation::Result::success(std::move(view)); +} + [[nodiscard]] inline pp::foundation::Result plan_brush_ui_color( float r, float g, diff --git a/src/node_panel_stroke.cpp b/src/node_panel_stroke.cpp index 714d80f..4404988 100644 --- a/src/node_panel_stroke.cpp +++ b/src/node_panel_stroke.cpp @@ -8,6 +8,115 @@ #include "app.h" #include "abr.h" +namespace { + +void add_stroke_float( + pp::app::BrushStrokePanelInput& input, + pp::app::BrushStrokeFloatSetting setting, + float value) +{ + input.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = setting, + .value = value, + }); +} + +void add_stroke_bool( + pp::app::BrushStrokePanelInput& input, + pp::app::BrushStrokeBoolSetting setting, + bool value) +{ + input.bool_values.push_back(pp::app::BrushStrokeBoolValue { + .setting = setting, + .value = value, + }); +} + +void add_stroke_blend( + pp::app::BrushStrokePanelInput& input, + pp::app::BrushStrokeBlendSetting setting, + int blend_mode) +{ + input.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = setting, + .blend_mode = blend_mode, + }); +} + +pp::app::BrushStrokePanelInput make_stroke_panel_input(const Brush& brush) +{ + pp::app::BrushStrokePanelInput input; + input.float_values.reserve(29); + input.bool_values.reserve(24); + input.blend_values.reserve(3); + + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_size, brush.m_tip_size); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_spacing, brush.m_tip_spacing); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_flow, brush.m_tip_flow); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_opacity, brush.m_tip_opacity); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_angle, brush.m_tip_angle); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_angle_smooth, brush.m_tip_angle_smooth); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_mix, brush.m_tip_mix); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_wet, brush.m_tip_wet); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_noise, brush.m_tip_noise); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_scale, brush.m_jitter_scale); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_angle, brush.m_jitter_angle); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_scatter, brush.m_jitter_scatter); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_flow, brush.m_jitter_flow); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_opacity, brush.m_jitter_opacity); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_hue, brush.m_jitter_hue); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_saturation, brush.m_jitter_sat); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_value, brush.m_jitter_val); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::jitter_aspect, brush.m_jitter_aspect); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_size, brush.m_dual_size); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_spacing, brush.m_dual_spacing); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_flow, brush.m_dual_flow); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_scatter, brush.m_dual_scatter); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::tip_aspect, brush.m_tip_aspect); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_opacity, brush.m_dual_opacity); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::dual_rotate, brush.m_dual_rotate); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::pattern_scale, brush.m_pattern_scale); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::pattern_brightness, brush.m_pattern_brightness); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::pattern_contrast, brush.m_pattern_contrast); + add_stroke_float(input, pp::app::BrushStrokeFloatSetting::pattern_depth, brush.m_pattern_depth); + + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample, brush.m_jitter_hsv_eachsample); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_angle_follow, brush.m_tip_angle_follow); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_angle_init, brush.m_tip_angle_init); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_flow_pressure, brush.m_tip_flow_pressure); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_opacity_pressure, brush.m_tip_opacity_pressure); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_size_pressure, brush.m_tip_size_pressure); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis, brush.m_jitter_aspect_bothaxis); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_invert, brush.m_tip_invert); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_flip_x, brush.m_tip_flipx); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_flip_y, brush.m_tip_flipy); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_enabled, brush.m_pattern_enabled); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_enabled, brush.m_dual_enabled); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis, brush.m_dual_scatter_bothaxis); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_invert, brush.m_dual_invert); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_flip_x, brush.m_dual_flipx); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_flip_y, brush.m_dual_flipy); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::dual_random_flip, brush.m_dual_randflip); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_random_flip_x, brush.m_tip_randflipx); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::tip_random_flip_y, brush.m_tip_randflipy); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_each_sample, brush.m_pattern_eachsample); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_invert, brush.m_pattern_invert); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_flip_x, brush.m_pattern_flipx); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_flip_y, brush.m_pattern_flipy); + add_stroke_bool(input, pp::app::BrushStrokeBoolSetting::pattern_random_offset, brush.m_pattern_rand_offset); + + add_stroke_blend(input, pp::app::BrushStrokeBlendSetting::tip, brush.m_blend_mode); + add_stroke_blend(input, pp::app::BrushStrokeBlendSetting::dual, brush.m_dual_blend_mode); + add_stroke_blend(input, pp::app::BrushStrokeBlendSetting::pattern, brush.m_pattern_blend_mode); + + input.tip_thumbnail_path = brush.m_brush_thumb_path; + input.dual_thumbnail_path = brush.m_dual_thumb_path; + input.pattern_thumbnail_path = brush.m_pattern_thumb_path; + return input; +} + +} // namespace + Node* NodePanelStroke::clone_instantiate() const { return new NodePanelStroke(); @@ -27,77 +136,106 @@ void NodePanelStroke::init() void NodePanelStroke::update_controls() { - const auto& b = Canvas::I->m_current_brush; - m_tip_size->m_value = m_curves[m_tip_size].to_slider(b->m_tip_size); - m_tip_spacing->m_value = m_curves[m_tip_spacing].to_slider(b->m_tip_spacing); - m_tip_flow->m_value = m_curves[m_tip_flow].to_slider(b->m_tip_flow); - m_tip_opacity->m_value = b->m_tip_opacity; - m_tip_angle->m_value = b->m_tip_angle; - m_tip_angle_smooth->m_value = b->m_tip_angle_smooth; - m_tip_mix->m_value = m_curves[m_tip_mix].to_slider(b->m_tip_mix); - m_tip_wet->m_value = b->m_tip_wet; - m_tip_noise->m_value = b->m_tip_noise; - m_jitter_scale->m_value = b->m_jitter_scale; - m_jitter_angle->m_value = b->m_jitter_angle; - m_jitter_scatter->m_value = m_curves[m_jitter_scatter].to_slider(b->m_jitter_scatter);; - m_jitter_flow->m_value = b->m_jitter_flow; - m_jitter_opacity->m_value = b->m_jitter_opacity; - m_jitter_hue->m_value = b->m_jitter_hue; - m_jitter_sat->m_value = b->m_jitter_sat; - m_jitter_val->m_value = b->m_jitter_val; - m_jitter_hsv_eachsample->set_value(b->m_jitter_hsv_eachsample); - m_jitter_aspect->m_value = b->m_jitter_aspect; - m_tip_angle_follow->set_value(b->m_tip_angle_follow); - m_tip_angle_init->set_value(b->m_tip_angle_init); - m_tip_flow_pressure->set_value(b->m_tip_flow_pressure); - m_tip_opacity_pressure->set_value(b->m_tip_opacity_pressure); - m_tip_size_pressure->set_value(b->m_tip_size_pressure); - m_jitter_aspect_bothaxis->set_value(b->m_jitter_aspect_bothaxis); + if (!Canvas::I || !Canvas::I->m_current_brush) { + return; + } - m_tip_invert->set_value(b->m_tip_invert); - m_tip_flipx->set_value(b->m_tip_flipx); - m_tip_flipy->set_value(b->m_tip_flipy); - m_pattern_enabled->set_value(b->m_pattern_enabled); - m_dual_enabled->set_value(b->m_dual_enabled); - m_dual_scatter_bothaxis->set_value(b->m_dual_scatter_bothaxis); - m_dual_invert->set_value(b->m_dual_invert); - m_dual_flipx->set_value(b->m_dual_flipx); - m_dual_flipy->set_value(b->m_dual_flipy); - m_dual_randflip->set_value(b->m_dual_randflip); - m_tip_randflipx->set_value(b->m_tip_randflipx); - m_tip_randflipy->set_value(b->m_tip_randflipy); - - m_dual_size->m_value = m_curves[m_dual_size].to_slider(b->m_dual_size); - m_dual_spacing->m_value = m_curves[m_dual_spacing].to_slider(b->m_dual_spacing); - m_dual_flow->m_value = m_curves[m_dual_flow].to_slider(b->m_dual_flow); - m_dual_scatter->m_value = m_curves[m_dual_scatter].to_slider(b->m_dual_scatter); - m_tip_aspect->m_value = b->m_tip_aspect; - m_dual_opacity->m_value = b->m_dual_opacity; - m_dual_rotate->m_value = b->m_dual_rotate; + const auto view = pp::app::plan_brush_stroke_panel_view( + make_stroke_panel_input(*Canvas::I->m_current_brush)); + if (!view) { + LOG("Brush stroke panel view failed: %s", view.status().message); + return; + } - m_pattern_eachsample->set_value(b->m_pattern_eachsample); - m_pattern_invert->set_value(b->m_pattern_invert); - m_pattern_flipx->set_value(b->m_pattern_flipx); - m_pattern_flipy->set_value(b->m_pattern_flipy); - m_pattern_rand_offset->set_value(b->m_pattern_rand_offset); - m_pattern_scale->m_value = m_curves[m_pattern_scale].to_slider(b->m_pattern_scale); - m_pattern_brightness->m_value = b->m_pattern_brightness; - m_pattern_contrast->m_value = b->m_pattern_contrast; - m_pattern_depth->m_value = b->m_pattern_depth; + const auto set_slider = [this](NodeSliderH* slider, float value) { + const auto curve = m_curves.find(slider); + slider->m_value = curve != m_curves.end() ? curve->second.to_slider(value) : value; + }; - m_blend_mode->set_index(b->m_blend_mode); - m_dual_blend_mode->set_index(b->m_dual_blend_mode); - m_pattern_blend_mode->set_index(b->m_pattern_blend_mode); + for (const auto& value : view.value().float_values) { + switch (value.setting) { + case pp::app::BrushStrokeFloatSetting::tip_size: set_slider(m_tip_size, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_spacing: set_slider(m_tip_spacing, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_flow: set_slider(m_tip_flow, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_opacity: set_slider(m_tip_opacity, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_angle: set_slider(m_tip_angle, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: set_slider(m_tip_angle_smooth, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_mix: set_slider(m_tip_mix, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_wet: set_slider(m_tip_wet, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_noise: set_slider(m_tip_noise, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_scale: set_slider(m_jitter_scale, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_angle: set_slider(m_jitter_angle, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_scatter: set_slider(m_jitter_scatter, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_flow: set_slider(m_jitter_flow, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_opacity: set_slider(m_jitter_opacity, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_hue: set_slider(m_jitter_hue, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_saturation: set_slider(m_jitter_sat, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_value: set_slider(m_jitter_val, value.value); break; + case pp::app::BrushStrokeFloatSetting::jitter_aspect: set_slider(m_jitter_aspect, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_size: set_slider(m_dual_size, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_spacing: set_slider(m_dual_spacing, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_flow: set_slider(m_dual_flow, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_scatter: set_slider(m_dual_scatter, value.value); break; + case pp::app::BrushStrokeFloatSetting::tip_aspect: set_slider(m_tip_aspect, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_opacity: set_slider(m_dual_opacity, value.value); break; + case pp::app::BrushStrokeFloatSetting::dual_rotate: set_slider(m_dual_rotate, value.value); break; + case pp::app::BrushStrokeFloatSetting::pattern_scale: set_slider(m_pattern_scale, value.value); break; + case pp::app::BrushStrokeFloatSetting::pattern_brightness: set_slider(m_pattern_brightness, value.value); break; + case pp::app::BrushStrokeFloatSetting::pattern_contrast: set_slider(m_pattern_contrast, value.value); break; + case pp::app::BrushStrokeFloatSetting::pattern_depth: set_slider(m_pattern_depth, value.value); break; + default: break; + } + } - m_preview->m_brush = b; - m_preview->draw_stroke(); + for (const auto& value : view.value().bool_values) { + switch (value.setting) { + case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: m_jitter_hsv_eachsample->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_angle_follow: m_tip_angle_follow->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_angle_init: m_tip_angle_init->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: m_tip_flow_pressure->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: m_tip_opacity_pressure->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_size_pressure: m_tip_size_pressure->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: m_jitter_aspect_bothaxis->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_invert: m_tip_invert->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_flip_x: m_tip_flipx->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_flip_y: m_tip_flipy->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_enabled: m_pattern_enabled->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_enabled: m_dual_enabled->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: m_dual_scatter_bothaxis->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_invert: m_dual_invert->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_flip_x: m_dual_flipx->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_flip_y: m_dual_flipy->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::dual_random_flip: m_dual_randflip->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: m_tip_randflipx->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: m_tip_randflipy->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_each_sample: m_pattern_eachsample->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_invert: m_pattern_invert->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_flip_x: m_pattern_flipx->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_flip_y: m_pattern_flipy->set_value(value.value); break; + case pp::app::BrushStrokeBoolSetting::pattern_random_offset: m_pattern_rand_offset->set_value(value.value); break; + default: break; + } + } - if (m_brush_thumb->m_path != b->m_brush_thumb_path) - m_brush_thumb->set_image(b->m_brush_thumb_path); - if (m_dual_brush_thumb->m_path != b->m_dual_thumb_path) - m_dual_brush_thumb->set_image(b->m_dual_thumb_path); - if (m_pattern_thumb->m_path != b->m_pattern_thumb_path) - m_pattern_thumb->set_image(b->m_pattern_thumb_path); + for (const auto& value : view.value().blend_values) { + switch (value.setting) { + case pp::app::BrushStrokeBlendSetting::tip: m_blend_mode->set_index(value.blend_mode); break; + case pp::app::BrushStrokeBlendSetting::dual: m_dual_blend_mode->set_index(value.blend_mode); break; + case pp::app::BrushStrokeBlendSetting::pattern: m_pattern_blend_mode->set_index(value.blend_mode); break; + } + } + + if (view.value().updates_preview) { + m_preview->m_brush = Canvas::I->m_current_brush; + m_preview->draw_stroke(); + } + + if (m_brush_thumb->m_path != view.value().tip_thumbnail_path) + m_brush_thumb->set_image(view.value().tip_thumbnail_path); + if (m_dual_brush_thumb->m_path != view.value().dual_thumbnail_path) + m_dual_brush_thumb->set_image(view.value().dual_thumbnail_path); + if (m_pattern_thumb->m_path != view.value().pattern_thumbnail_path) + m_pattern_thumb->set_image(view.value().pattern_thumbnail_path); } void NodePanelStroke::set_flow(float value, bool normalized, bool propagate) @@ -162,15 +300,15 @@ void NodePanelStroke::init_controls() const int br_idx = m_default_brush_index; // init main brush - 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; + auto default_brush = std::make_shared(); + default_brush->load_tip(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx)); + //default_brush->load_dual(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx)); + //default_brush->load_pattern(m_pattern_popup->get_texture_path(0), m_pattern_popup->get_thumb_path(0)); + default_brush->m_tip_size = 30; + default_brush->m_tip_flow = .9f; + default_brush->m_tip_spacing = .1f; + default_brush->m_tip_opacity = 1.f; + Canvas::I->m_current_brush = default_brush; // BRUSH PRESETS diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b5e4f7a..a050b50 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1448,6 +1448,24 @@ if(TARGET pano_cli) LABELS "app;paint;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_brush_stroke_panel_view_smoke + COMMAND pano_cli plan-brush-stroke-panel-view --tip-size 64 --jitter-scatter 0.4 --dual-disabled --tip-blend-mode 2 --pattern-blend-mode 5 --tip-thumb data/brushes/thumbs/ink.png) + set_tests_properties(pano_cli_plan_brush_stroke_panel_view_smoke PROPERTIES + LABELS "app;paint;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-stroke-panel-view\".*\"floatValues\":2.*\"boolValues\":1.*\"blendValues\":2.*\"tipSize\":64.*\"jitterScatter\":0.4.*\"dualEnabled\":false.*\"tipBlendMode\":2.*\"patternBlendMode\":5.*\"tipThumb\":\"data/brushes/thumbs/ink.png\".*\"updatesPreview\":true.*\"updatesThumbnails\":true") + + add_test(NAME pano_cli_plan_brush_stroke_panel_view_rejects_bad_float + COMMAND pano_cli plan-brush-stroke-panel-view --bad-float) + set_tests_properties(pano_cli_plan_brush_stroke_panel_view_rejects_bad_float PROPERTIES + LABELS "app;paint;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + + add_test(NAME pano_cli_plan_brush_stroke_panel_view_rejects_bad_blend + COMMAND pano_cli plan-brush-stroke-panel-view --bad-blend) + set_tests_properties(pano_cli_plan_brush_stroke_panel_view_rejects_bad_blend PROPERTIES + LABELS "app;paint;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_paint_feedback_framebuffer_fetch_smoke COMMAND pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only) set_tests_properties(pano_cli_plan_paint_feedback_framebuffer_fetch_smoke PROPERTIES diff --git a/tests/app_core/brush_ui_tests.cpp b/tests/app_core/brush_ui_tests.cpp index 6d119ff..9bad821 100644 --- a/tests/app_core/brush_ui_tests.cpp +++ b/tests/app_core/brush_ui_tests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace { @@ -409,6 +410,74 @@ void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests:: PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64)); } +void stroke_panel_view_projects_brush_state(pp::tests::Harness& harness) +{ + pp::app::BrushStrokePanelInput input; + input.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = pp::app::BrushStrokeFloatSetting::tip_size, + .value = 42.5F, + }); + input.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = pp::app::BrushStrokeFloatSetting::jitter_scatter, + .value = 0.75F, + }); + input.bool_values.push_back(pp::app::BrushStrokeBoolValue { + .setting = pp::app::BrushStrokeBoolSetting::dual_enabled, + .value = true, + }); + input.bool_values.push_back(pp::app::BrushStrokeBoolValue { + .setting = pp::app::BrushStrokeBoolSetting::pattern_random_offset, + .value = false, + }); + input.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = pp::app::BrushStrokeBlendSetting::tip, + .blend_mode = 4, + }); + input.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = pp::app::BrushStrokeBlendSetting::pattern, + .blend_mode = 8, + }); + input.tip_thumbnail_path = "data/brushes/thumbs/soft.png"; + input.dual_thumbnail_path = "data/brushes/thumbs/hard.png"; + input.pattern_thumbnail_path = "data/patterns/thumbs/noise.png"; + + const auto view = pp::app::plan_brush_stroke_panel_view(std::move(input)); + PP_EXPECT(harness, view); + if (view) { + PP_EXPECT(harness, view.value().float_values.size() == 2); + PP_EXPECT(harness, view.value().bool_values.size() == 2); + PP_EXPECT(harness, view.value().blend_values.size() == 2); + PP_EXPECT(harness, view.value().float_values[0].setting == pp::app::BrushStrokeFloatSetting::tip_size); + PP_EXPECT(harness, view.value().float_values[0].value == 42.5F); + PP_EXPECT(harness, view.value().bool_values[0].setting == pp::app::BrushStrokeBoolSetting::dual_enabled); + PP_EXPECT(harness, view.value().bool_values[0].value); + PP_EXPECT(harness, view.value().blend_values[1].setting == pp::app::BrushStrokeBlendSetting::pattern); + PP_EXPECT(harness, view.value().blend_values[1].blend_mode == 8); + PP_EXPECT(harness, view.value().tip_thumbnail_path == "data/brushes/thumbs/soft.png"); + PP_EXPECT(harness, view.value().dual_thumbnail_path == "data/brushes/thumbs/hard.png"); + PP_EXPECT(harness, view.value().pattern_thumbnail_path == "data/patterns/thumbs/noise.png"); + PP_EXPECT(harness, view.value().updates_preview); + PP_EXPECT(harness, view.value().updates_thumbnails); + } +} + +void stroke_panel_view_rejects_invalid_brush_state(pp::tests::Harness& harness) +{ + pp::app::BrushStrokePanelInput bad_float; + bad_float.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = pp::app::BrushStrokeFloatSetting::tip_flow, + .value = std::nanf(""), + }); + PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_float))); + + pp::app::BrushStrokePanelInput bad_blend; + bad_blend.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = pp::app::BrushStrokeBlendSetting::dual, + .blend_mode = 64, + }); + PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_blend))); +} + 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( @@ -852,6 +921,8 @@ int main() 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("stroke panel view projects brush state", stroke_panel_view_projects_brush_state); + harness.run("stroke panel view rejects invalid brush state", stroke_panel_view_rejects_invalid_brush_state); 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("preset list plans add select move remove and clear", preset_list_plans_add_select_move_remove_and_clear); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 3658b29..ec9e6ff 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -412,6 +412,19 @@ struct PlanBrushStrokeControlArgs { int blend_mode = 0; }; +struct PlanBrushStrokePanelViewArgs { + float tip_size = 42.5F; + float jitter_scatter = 0.75F; + bool dual_enabled = true; + int tip_blend_mode = 4; + int pattern_blend_mode = 8; + std::string tip_thumbnail_path = "data/brushes/thumbs/soft.png"; + std::string dual_thumbnail_path = "data/brushes/thumbs/hard.png"; + std::string pattern_thumbnail_path = "data/patterns/thumbs/noise.png"; + bool bad_float = false; + bool bad_blend = false; +}; + struct PlanPaintFeedbackArgs { int width = 64; int height = 32; @@ -1957,6 +1970,7 @@ void print_help() << " 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-preset-list --kind add|remove|move|up|down|select|clear [--item-count N] [--current-index N] [--offset N] [--no-current-brush]\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-brush-stroke-panel-view [--tip-size N] [--jitter-scatter N] [--dual-enabled|--dual-disabled] [--tip-blend-mode N] [--pattern-blend-mode N] [--tip-thumb FILE] [--dual-thumb FILE] [--pattern-thumb FILE] [--bad-float] [--bad-blend]\n" << " plan-paint-feedback [--width N] [--height N] [--simple|--complex] [--framebuffer-fetch] [--texture-copy] [--blit] [--explicit-transitions] [--render-only] [--depth]\n" << " plan-stroke-composite [--width N] [--height N] [--layer-blend N] [--stroke-blend N] [--dual-blend] [--pattern-blend] [--framebuffer-fetch] [--texture-copy] [--blit] [--explicit-transitions] [--render-only] [--depth]\n" << " plan-canvas-hotkey --event key-down|key-up|touch-tap --key e|z|s|tab|alt|android-back|bracket-left|bracket-right [--ctrl] [--shift] [--mouse-focus] [--undo-count N] [--redo-count N] [--touch-fingers N]\n" @@ -5773,6 +5787,154 @@ int plan_brush_stroke_control(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_brush_stroke_panel_view_args( + int argc, + char** argv, + PlanBrushStrokePanelViewArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--tip-size" || key == "--jitter-scatter") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--tip-size") { + args.tip_size = value.value(); + } else { + args.jitter_scatter = value.value(); + } + } else if (key == "--tip-blend-mode" || key == "--pattern-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(); + } + if (key == "--tip-blend-mode") { + args.tip_blend_mode = value.value(); + } else { + args.pattern_blend_mode = value.value(); + } + } else if (key == "--tip-thumb" || key == "--dual-thumb" || key == "--pattern-thumb") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + if (key == "--tip-thumb") { + args.tip_thumbnail_path = argv[++i]; + } else if (key == "--dual-thumb") { + args.dual_thumbnail_path = argv[++i]; + } else { + args.pattern_thumbnail_path = argv[++i]; + } + } else if (key == "--dual-enabled") { + args.dual_enabled = true; + } else if (key == "--dual-disabled") { + args.dual_enabled = false; + } else if (key == "--bad-float") { + args.bad_float = true; + } else if (key == "--bad-blend") { + args.bad_blend = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_brush_stroke_panel_view(int argc, char** argv) +{ + PlanBrushStrokePanelViewArgs args; + const auto status = parse_plan_brush_stroke_panel_view_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-brush-stroke-panel-view", status.message); + return 2; + } + + pp::app::BrushStrokePanelInput input; + input.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = pp::app::BrushStrokeFloatSetting::tip_size, + .value = args.bad_float ? std::nanf("") : args.tip_size, + }); + input.float_values.push_back(pp::app::BrushStrokeFloatValue { + .setting = pp::app::BrushStrokeFloatSetting::jitter_scatter, + .value = args.jitter_scatter, + }); + input.bool_values.push_back(pp::app::BrushStrokeBoolValue { + .setting = pp::app::BrushStrokeBoolSetting::dual_enabled, + .value = args.dual_enabled, + }); + input.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = pp::app::BrushStrokeBlendSetting::tip, + .blend_mode = args.tip_blend_mode, + }); + input.blend_values.push_back(pp::app::BrushStrokeBlendValue { + .setting = pp::app::BrushStrokeBlendSetting::pattern, + .blend_mode = args.bad_blend ? 64 : args.pattern_blend_mode, + }); + input.tip_thumbnail_path = args.tip_thumbnail_path; + input.dual_thumbnail_path = args.dual_thumbnail_path; + input.pattern_thumbnail_path = args.pattern_thumbnail_path; + + const auto view = pp::app::plan_brush_stroke_panel_view(std::move(input)); + if (!view) { + print_error("plan-brush-stroke-panel-view", view.status().message); + return 2; + } + + float tip_size = 0.0F; + float jitter_scatter = 0.0F; + bool dual_enabled = false; + int tip_blend_mode = 0; + int pattern_blend_mode = 0; + for (const auto& value : view.value().float_values) { + if (value.setting == pp::app::BrushStrokeFloatSetting::tip_size) { + tip_size = value.value; + } else if (value.setting == pp::app::BrushStrokeFloatSetting::jitter_scatter) { + jitter_scatter = value.value; + } + } + for (const auto& value : view.value().bool_values) { + if (value.setting == pp::app::BrushStrokeBoolSetting::dual_enabled) { + dual_enabled = value.value; + } + } + for (const auto& value : view.value().blend_values) { + if (value.setting == pp::app::BrushStrokeBlendSetting::tip) { + tip_blend_mode = value.blend_mode; + } else if (value.setting == pp::app::BrushStrokeBlendSetting::pattern) { + pattern_blend_mode = value.blend_mode; + } + } + + std::cout << "{\"ok\":true,\"command\":\"plan-brush-stroke-panel-view\"" + << ",\"state\":{\"tipSize\":" << args.tip_size + << ",\"jitterScatter\":" << args.jitter_scatter + << ",\"dualEnabled\":" << json_bool(args.dual_enabled) + << ",\"tipBlendMode\":" << args.tip_blend_mode + << ",\"patternBlendMode\":" << args.pattern_blend_mode + << "},\"view\":{\"floatValues\":" << view.value().float_values.size() + << ",\"boolValues\":" << view.value().bool_values.size() + << ",\"blendValues\":" << view.value().blend_values.size() + << ",\"tipSize\":" << tip_size + << ",\"jitterScatter\":" << jitter_scatter + << ",\"dualEnabled\":" << json_bool(dual_enabled) + << ",\"tipBlendMode\":" << tip_blend_mode + << ",\"patternBlendMode\":" << pattern_blend_mode + << ",\"tipThumb\":\"" << json_escape(view.value().tip_thumbnail_path) + << "\",\"dualThumb\":\"" << json_escape(view.value().dual_thumbnail_path) + << "\",\"patternThumb\":\"" << json_escape(view.value().pattern_thumbnail_path) + << "\",\"updatesPreview\":" << json_bool(view.value().updates_preview) + << ",\"updatesThumbnails\":" << json_bool(view.value().updates_thumbnails) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_paint_feedback_args( int argc, char** argv, @@ -9337,6 +9499,10 @@ int main(int argc, char** argv) return plan_brush_stroke_control(argc, argv); } + if (command == "plan-brush-stroke-panel-view") { + return plan_brush_stroke_panel_view(argc, argv); + } + if (command == "plan-paint-feedback") { return plan_paint_feedback(argc, argv); }