Compare commits

..

9 Commits

25 changed files with 3378 additions and 207 deletions

View File

@@ -355,6 +355,7 @@ if(PP_BUILD_APP)
pp_assets pp_assets
pp_document pp_document
pp_paint pp_paint
pp_paint_renderer
pp_renderer_api pp_renderer_api
pp_project_warnings) pp_project_warnings)
if(TARGET pp_renderer_gl) if(TARGET pp_renderer_gl)
@@ -392,6 +393,7 @@ if(PP_BUILD_APP)
pp_assets pp_assets
pp_document pp_document
pp_paint pp_paint
pp_paint_renderer
pp_renderer_api pp_renderer_api
pp_project_warnings) pp_project_warnings)
if(TARGET pp_renderer_gl) if(TARGET pp_renderer_gl)

View File

@@ -1,7 +1,7 @@
# PanoPainter Capability Map # PanoPainter Capability Map
Status: live Status: live
Last updated: 2026-06-02 Last updated: 2026-06-03
This map is the preservation checklist for the modernization. When a component This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label, is extracted, update the relevant rows with the owning component, test label,
@@ -32,12 +32,12 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests | | 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 | | 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 | | 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 | | Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden | | Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden |
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity | | Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke-feedback destination-copy coverage, and GPU parity |
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects | | Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation ## Layers And Animation

View File

@@ -40,7 +40,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing | | DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | | DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely | | DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, and the `BrushUiServices` boundary, but the live adapter still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings execution is owned by injected brush/app/asset/UI services with no legacy brush adapter | | DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, 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-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |
| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning and execution dispatch now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, and the `QuickUiServices` boundary, but the live adapter still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by injected app/brush/UI services with no legacy quick-panel adapter |
| DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter | | DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter |
@@ -53,6 +53,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter | | DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current stroke shader feedback decision, and live `Canvas::stroke_draw` uses it for main-brush, dual-brush, and stroke-pad destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, and thumbnail layer blending still use legacy OpenGL canvas execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
## Closed Debt ## Closed Debt

View File

@@ -511,6 +511,15 @@ changes, tip/pattern/dual texture changes, preset brush replacement, and stroke
settings refreshes used by the live brush, quick, color, and floating panel settings refreshes used by the live brush, quick, color, and floating panel
callbacks. Brush UI execution now dispatches through `BrushUiServices` before callbacks. Brush UI execution now dispatches through `BrushUiServices` before
the legacy `Brush`/panel adapter mutates brush state or loads brush resources. the legacy `Brush`/panel adapter mutates brush state or loads brush resources.
`pano_cli plan-brush-texture-list` exposes app-core planning for brush/pattern
texture add, remove, and reorder actions, and `NodePanelBrush` now dispatches
those actions through `BrushTextureListServices` before the legacy image
load/save and UI-list adapter continues.
`pano_cli plan-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, `pano_cli plan-canvas-tool` exposes app-core planning for draw/erase/line,
camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar
commands. Canvas tool execution now dispatches through `CanvasToolServices` commands. Canvas tool execution now dispatches through `CanvasToolServices`
@@ -853,6 +862,38 @@ nested passes, texture I/O or blits inside a pass, and unclosed passes. It
also validates executable command dependencies, including shader-before-uniform also validates executable command dependencies, including shader-before-uniform
and shader-plus-mesh before draw within each render pass, and rejects invalid and shader-plus-mesh before draw within each render pass, and rejects invalid
texture/sampler bind slots in malformed recorded streams. texture/sampler bind slots in malformed recorded streams.
The renderer-neutral API now also plans complex paint feedback strategies for
future stroke/layer compositing work: framebuffer-fetch-capable backends can
read destination color directly, while other backends must use ping-pong render
targets backed by texture copy or render-target blit support. This is exposed
through `pano_cli plan-paint-feedback` and tracked by DEBT-0036 until the live
paint renderer consumes the plan.
`pp_paint_renderer` now consumes that lower-level feedback planner through a
stroke composite plan that decides whether a stroke/layer blend can use
fixed-function blending or needs framebuffer-fetch/ping-pong destination
feedback. `pano_cli plan-stroke-composite` exposes the same decision for
automation, including layer blend, stroke blend, dual-brush, and pattern-blend
inputs. Live `Canvas::draw_merge` now uses this planner for its existing
shader-blend gate for layer and primary-brush blend modes while preserving the
legacy trigger policy; actual canvas stroke execution, dual-brush feedback, and
pattern feedback are still legacy OpenGL and remain tracked by DEBT-0036 until
the app calls through renderer services for the whole compositing path.
`pp_paint_renderer::plan_canvas_blend_gate` now also owns the compatibility
mapping from persisted layer and brush blend indices to that planner, including
fallback behavior for unknown nonzero indices. Both `Canvas::draw_merge` and
`NodeCanvas` panorama rendering consume that shared gate, so the live app no
longer has duplicate local blend-trigger logic or duplicate destination-copy
versus framebuffer-fetch decisions in those paths.
The OpenGL shader initialization path now stores a renderer-neutral
`RenderDeviceFeatures` snapshot converted by `pp_renderer_gl`, and those live
canvas gates consume that snapshot instead of rebuilding feature flags from
individual `ShaderManager` extension booleans.
`pp_paint_renderer::plan_canvas_stroke_feedback` now models the current stroke
shader's required destination feedback without changing the legacy shader math.
Live `Canvas::stroke_draw` consumes that plan for main-brush, dual-brush, and
stroke-pad destination-copy versus framebuffer-fetch decisions; thumbnail layer
blending is the remaining direct canvas feedback branch before a fuller live
paint-renderer execution boundary can take over.
The existing renderer classes are not yet fully The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.
@@ -1098,8 +1139,15 @@ Results:
render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/
upload/texture-copy/readback/frame-capture/blit command capture, draw upload/texture-copy/readback/frame-capture/blit command capture, draw
mesh-input capture, explicit draw-range capture, and invalid catalog mesh-input capture, explicit draw-range capture, and invalid catalog
rejection. rejection. The same suite now covers complex paint feedback planning for
framebuffer-fetch backends, ping-pong texture-copy/blit fallbacks, simple
no-feedback blends, invalid render-target usage, unsupported backends, and
depth-target rejection.
- `pp_paint_renderer_compositor_tests` passed. - `pp_paint_renderer_compositor_tests` passed.
The suite now covers fixed-function stroke composite planning,
framebuffer-fetch planning, ping-pong texture-copy/blit fallback planning,
dual/pattern blend feedback detection, invalid blend mode rejection,
unsupported backend rejection, and invalid render-target rejection.
- `pp_ui_core_color_tests` passed. - `pp_ui_core_color_tests` passed.
- `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_value_tests` passed.
- `pp_ui_core_layout_xml_tests` passed. - `pp_ui_core_layout_xml_tests` passed.
@@ -1226,15 +1274,29 @@ Results:
live animation-panel planning as JSON automation. live animation-panel planning as JSON automation.
- `pp_app_core_brush_ui_tests` passed, covering brush color channel validation, - `pp_app_core_brush_ui_tests` passed, covering brush color channel validation,
invalid color rejection, texture-path validation, preset-brush availability, invalid color rejection, texture-path validation, preset-brush availability,
preserve-current-color intent, stroke-settings refresh intent, service preserve-current-color intent, stroke-settings refresh intent, texture-list
dispatch ordering, texture/preset execution payloads, and invalid execution add target path planning, user-texture removal intent, clamped reorder intent,
payload rejection. 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_color_smoke`,
`pano_cli_plan_brush_operation_texture_smoke`, `pano_cli_plan_brush_operation_texture_smoke`,
`pano_cli_plan_brush_operation_preset_smoke`, `pano_cli_plan_brush_operation_preset_smoke`,
`pano_cli_plan_brush_operation_rejects_bad_color`, and `pano_cli_plan_brush_operation_rejects_bad_color`, and
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live `pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
brush/color/preset UI planning as JSON automation. brush/color/preset UI planning as JSON automation.
- `pano_cli_plan_brush_texture_list_add_smoke`,
`pano_cli_plan_brush_texture_list_remove_user_smoke`,
`pano_cli_plan_brush_texture_list_move_edge_smoke`, and
`pano_cli_plan_brush_texture_list_rejects_bad_source` passed and expose live
brush/pattern texture-list planning as JSON automation.
- `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 - `pp_app_core_grid_ui_tests` passed, covering heightmap pick/load/reload/clear
planning, lightmap capability and limit checks, missing-heightmap no-op planning, lightmap capability and limit checks, missing-heightmap no-op
behavior, and commit canvas gating. behavior, and commit canvas gating.
@@ -1552,6 +1614,25 @@ Results:
- Canvas layer merge rendering and explicit layer-merge compositing now route - Canvas layer merge rendering and explicit layer-merge compositing now route
depth/blend state, active texture units, fallback 2D texture unbinds, and depth/blend state, active texture units, fallback 2D texture unbinds, and
merge framebuffer copy targets through the renderer GL backend mapping. merge framebuffer copy targets through the renderer GL backend mapping.
- Canvas draw-merge shader-blend selection now consumes the extracted
`pp_paint_renderer` stroke composite planner for current layer and primary
brush blend modes, while preserving legacy OpenGL compositing execution under
DEBT-0036.
- `NodeCanvas` panorama rendering now consumes the same tested
`pp_paint_renderer` canvas blend-gate planner as `Canvas::draw_merge`, so
layer and primary-brush blend-trigger compatibility is centralized.
- Shader initialization now publishes the OpenGL backend's renderer-neutral
feature snapshot through the legacy shader manager, and live canvas blend
gates consume that `RenderDeviceFeatures` value instead of hand-built
framebuffer-fetch/texture-copy flags.
- Canvas draw-merge and `NodeCanvas` panorama shader-blend paths now use the
shared canvas blend-gate plan to decide whether they can read destination
color through framebuffer fetch or must copy the destination texture before
the legacy OpenGL blend draw.
- Canvas main-brush, dual-brush, and stroke-pad draw paths now use the tested
`pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch
supplies destination color or the legacy OpenGL path must copy the target
texture before drawing.
- Canvas equirectangular import drawing and depth export rendering now route - Canvas equirectangular import drawing and depth export rendering now route
depth/blend state and active texture units through the renderer GL backend depth/blend state and active texture units through the renderer GL backend
mapping. mapping.

View File

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

View File

@@ -16,6 +16,17 @@ namespace {
return static_cast<GLenum>(pp::renderer::gl::extension_string_name()); return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
} }
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
{
pp::renderer::gl::OpenGlCapabilities capabilities;
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
capabilities.float32_textures = ShaderManager::ext_float32;
capabilities.float32_linear = ShaderManager::ext_float32_linear;
capabilities.float16_textures = ShaderManager::ext_float16;
return capabilities;
}
} }
void App::initShaders() void App::initShaders()
@@ -55,6 +66,7 @@ void App::initShaders()
ShaderManager::ext_float32 = capabilities.float32_textures; ShaderManager::ext_float32 = capabilities.float32_textures;
ShaderManager::ext_float32_linear = capabilities.float32_linear; ShaderManager::ext_float32_linear = capabilities.float32_linear;
ShaderManager::ext_float16 = capabilities.float16_textures; ShaderManager::ext_float16 = capabilities.float16_textures;
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
}); });
#if __GL__ #if __GL__
@@ -63,6 +75,8 @@ void App::initShaders()
ShaderManager::ext_float32 = true; ShaderManager::ext_float32 = true;
ShaderManager::ext_float16 = true; ShaderManager::ext_float16 = true;
#endif #endif
ShaderManager::set_render_device_features(
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled"); LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");

View File

@@ -4,6 +4,7 @@
#include "app.h" #include "app.h"
#include "texture.h" #include "texture.h"
#include "node_progress_bar.h" #include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#include <thread> #include <thread>
#include <algorithm> #include <algorithm>
@@ -43,6 +44,69 @@ GLenum rgba_pixel_format()
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format()); return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format());
} }
pp::renderer::RenderDeviceFeatures canvas_stroke_composite_features() noexcept
{
return ShaderManager::render_device_features();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_stroke_feedback_plan(
int width,
int height) noexcept
{
const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
canvas_stroke_composite_features(),
pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
return fallback;
}
pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
int width,
int height,
const std::vector<std::shared_ptr<Layer>>& layers,
const Brush* brush) noexcept
{
std::vector<int> layer_blend_modes;
layer_blend_modes.reserve(layers.size());
for (const auto& layer : layers) {
if (!layer) {
continue;
}
layer_blend_modes.push_back(layer->m_blend_mode);
}
const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
canvas_stroke_composite_features(),
pp::paint_renderer::CanvasBlendGateRequest {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
},
.layer_blend_modes = layer_blend_modes,
.has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0,
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasBlendGatePlan fallback;
fallback.shader_blend = true;
fallback.complex_blend = true;
fallback.compatibility_fallback = true;
return fallback;
}
GLenum unsigned_byte_component_type() GLenum unsigned_byte_component_type()
{ {
return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type()); return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type());
@@ -421,9 +485,12 @@ std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vert
return ret; return ret;
} }
glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P) glm::vec4 Canvas::stroke_draw_samples(
int i,
std::vector<vertex_t>& P,
bool copy_stroke_destination)
{ {
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
set_active_texture_unit(1); set_active_texture_unit(1);
m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing) m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
@@ -441,7 +508,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
glm::vec2 pad(1); glm::vec2 pad(1);
glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height }); glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height });
glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos)); glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y, glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y,
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y); tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
@@ -469,7 +536,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
} }
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
set_active_texture_unit(1); set_active_texture_unit(1);
m_tex[i].unbind(); m_tex[i].unbind();
@@ -587,10 +654,13 @@ void Canvas::stroke_draw()
if (brush->m_pattern_flipx) patt_scale.x *= -1.f; if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
if (brush->m_pattern_flipy) patt_scale.y *= -1.f; if (brush->m_pattern_flipy) patt_scale.y *= -1.f;
const auto stroke_feedback = canvas_stroke_feedback_plan(m_width, m_height);
const bool copy_stroke_destination = !stroke_feedback.reads_destination_color;
glDisable(blend_state()); glDisable(blend_state());
ShaderManager::use(kShader::Stroke); ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg
ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
@@ -648,7 +718,7 @@ void Canvas::stroke_draw()
ShaderManager::u_vec4(kShaderUniform::Col, f.col); ShaderManager::u_vec4(kShaderUniform::Col, f.col);
ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
auto box_sample = stroke_draw_samples(i, P); auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
@@ -675,7 +745,7 @@ void Canvas::stroke_draw()
// work on documents that doesn't have the padding, so on document loading. // work on documents that doesn't have the padding, so on document loading.
ShaderManager::use(kShader::StrokePad); ShaderManager::use(kShader::StrokePad);
ShaderManager::u_vec4(kShaderUniform::Col, pad_color); ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
set_active_texture_unit(1); set_active_texture_unit(1);
ShaderManager::u_int(kShaderUniform::TexBG, 1); ShaderManager::u_int(kShaderUniform::TexBG, 1);
@@ -705,7 +775,7 @@ void Canvas::stroke_draw()
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
m_tmp[i].bindFramebuffer(); m_tmp[i].bindFramebuffer();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
glm::vec2 o = glm::max({0, 0}, xy(b) - pad); glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o; glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
@@ -716,7 +786,7 @@ void Canvas::stroke_draw()
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
} }
if (!ShaderManager::ext_framebuffer_fetch) if (copy_stroke_destination)
{ {
unbind_texture_2d(); unbind_texture_2d();
} }
@@ -747,7 +817,7 @@ void Canvas::stroke_draw()
if (P.size() < 3) if (P.size() < 3)
continue; continue;
m_tmp_dual[i].bindFramebuffer(); m_tmp_dual[i].bindFramebuffer();
auto box_sample = stroke_draw_samples(i, P); auto box_sample = stroke_draw_samples(i, P, copy_stroke_destination);
m_tmp_dual[i].unbindFramebuffer(); m_tmp_dual[i].unbindFramebuffer();
// this mode overflows the main brush boundries // this mode overflows the main brush boundries
@@ -1086,14 +1156,13 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f); auto ortho = glm::ortho<float>(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f);
const auto& b = m_current_stroke->m_brush; const auto& b = m_current_stroke->m_brush;
// check if any layer use blend, otherwise draw directly on main framebuffer const auto blend_gate = draw_merge_blend_gate_plan(
bool use_blend = false; m_width,
for (auto& l : m_layers) m_height,
{ m_layers,
use_blend |= l->m_blend_mode != 0; m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
} const bool use_blend = blend_gate.shader_blend;
if (Canvas::I->m_current_stroke) const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
// if not using shader blend, use gl rasterizer blend // if not using shader blend, use gl rasterizer blend
glDisable(depth_test_state()); glDisable(depth_test_state());
@@ -1257,7 +1326,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode); ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
ShaderManager::u_mat4(kShaderUniform::MVP, ortho); ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
m_sampler.bind(2); m_sampler.bind(2);
ShaderManager::u_int(kShaderUniform::TexBG, 2); ShaderManager::u_int(kShaderUniform::TexBG, 2);
@@ -1265,7 +1334,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
set_active_texture_unit(0); set_active_texture_unit(0);
m_merge_rtt.bindTexture(); m_merge_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_merge_tex.bind(); m_merge_tex.bind();
@@ -1274,7 +1343,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
m_plane.draw_fill(); m_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_merge_tex.unbind(); m_merge_tex.unbind();

View File

@@ -205,7 +205,7 @@ public:
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz); void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const; std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
// return rect {origin, size} // return rect {origin, size}
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P); glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P, bool copy_stroke_destination);
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const; std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
void stroke_draw(); void stroke_draw();
void stroke_end(); void stroke_end();

View File

@@ -1,6 +1,9 @@
#include "pch.h" #include "pch.h"
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <memory>
#include <vector>
#include "app_core/canvas_tool_ui.h" #include "app_core/canvas_tool_ui.h"
#include "app_core/history_ui.h" #include "app_core/history_ui.h"
@@ -8,6 +11,7 @@
#include "log.h" #include "log.h"
#include "node_canvas.h" #include "node_canvas.h"
#include "node_image_texture.h" #include "node_image_texture.h"
#include "paint_renderer/compositor.h"
#include "settings.h" #include "settings.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
@@ -23,6 +27,48 @@ void unbind_texture_2d()
glBindTexture(pp::renderer::gl::texture_2d_target(), 0); glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
} }
pp::renderer::RenderDeviceFeatures node_canvas_stroke_composite_features() noexcept
{
return ShaderManager::render_device_features();
}
pp::paint_renderer::CanvasBlendGatePlan node_canvas_blend_gate_plan(
int width,
int height,
const std::vector<std::shared_ptr<Layer>>& layers,
const Brush* brush) noexcept
{
std::vector<int> layer_blend_modes;
layer_blend_modes.reserve(layers.size());
for (const auto& layer : layers) {
if (!layer) {
continue;
}
layer_blend_modes.push_back(layer->m_blend_mode);
}
const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
node_canvas_stroke_composite_features(),
pp::paint_renderer::CanvasBlendGateRequest {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
},
.layer_blend_modes = layer_blend_modes,
.has_stroke_blend_mode = brush != nullptr,
.stroke_blend_mode = brush ? brush->m_blend_mode : 0,
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasBlendGatePlan fallback;
fallback.shader_blend = true;
fallback.complex_blend = true;
fallback.compatibility_fallback = true;
return fallback;
}
void run_history_undo_if_available() void run_history_undo_if_available()
{ {
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size())); const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
@@ -252,14 +298,13 @@ void NodeCanvas::draw()
} }
else else
{ {
// check if any layer use blend, otherwise draw directly on main framebuffer const auto blend_gate = node_canvas_blend_gate_plan(
bool use_blend = false; m_cache_rtt.getWidth(),
for (size_t i = 0; i < m_canvas->m_layers.size(); i++) m_cache_rtt.getHeight(),
{ m_canvas->m_layers,
use_blend |= m_canvas->m_layers[i]->m_blend_mode != 0; m_canvas->m_current_stroke ? m_canvas->m_current_stroke->m_brush.get() : nullptr);
} const bool use_blend = blend_gate.shader_blend;
if (Canvas::I->m_current_stroke) const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
if (use_blend) if (use_blend)
{ {
@@ -450,7 +495,7 @@ void NodeCanvas::draw()
ShaderManager::use(kShader::TextureBlend); ShaderManager::use(kShader::TextureBlend);
ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::Tex, 0);
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
ShaderManager::u_int(kShaderUniform::TexBG, 2); ShaderManager::u_int(kShaderUniform::TexBG, 2);
ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode); ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
@@ -458,7 +503,7 @@ void NodeCanvas::draw()
set_active_texture_unit(0); set_active_texture_unit(0);
m_blender_rtt.bindTexture(); m_blender_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_blender_bg.bind(); m_blender_bg.bind();
@@ -468,7 +513,7 @@ void NodeCanvas::draw()
m_face_plane.draw_fill(); m_face_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch) if (copy_blend_destination)
{ {
set_active_texture_unit(2); set_active_texture_unit(2);
m_blender_bg.unbind(); m_blender_bg.unbind();

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
#include "pch.h" #include "pch.h"
#include "app_core/brush_ui.h"
#include "log.h" #include "log.h"
#include "node_panel_stroke.h" #include "node_panel_stroke.h"
#include "canvas.h" #include "canvas.h"
@@ -6,6 +7,137 @@
#include "app.h" #include "app.h"
#include "abr.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 Node* NodePanelStroke::clone_instantiate() const
{ {
return new NodePanelStroke(); return new NodePanelStroke();
@@ -156,7 +288,8 @@ void NodePanelStroke::init_controls()
//m_presets_popup->m_flood_events = true; //m_presets_popup->m_flood_events = true;
//m_presets_popup->m_capture_children = false; //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 // init main brush
auto b = std::make_shared<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 = find<NodeComboBox>("blend-mode");
m_blend_mode->on_select = [this](Node*, int index) { m_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_blend_mode = index; const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, index);
//m_preview->draw_stroke(); if (plan) {
if (on_stroke_change) execute_stroke_control_plan(plan.value());
on_stroke_change(this); }
}; };
init_slider(m_tip_size, "tip-size", &Brush::m_tip_size); init_slider(m_tip_size, "tip-size", pp::app::BrushStrokeFloatSetting::tip_size, &Brush::m_tip_size);
init_slider(m_tip_spacing, "tip-spacing", &Brush::m_tip_spacing); init_slider(m_tip_spacing, "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing, &Brush::m_tip_spacing);
init_slider(m_tip_flow, "tip-flow", &Brush::m_tip_flow); init_slider(m_tip_flow, "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow, &Brush::m_tip_flow);
init_slider(m_tip_opacity, "tip-opacity", &Brush::m_tip_opacity); init_slider(m_tip_opacity, "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity, &Brush::m_tip_opacity);
init_slider(m_tip_angle, "tip-angle", &Brush::m_tip_angle); 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", &Brush::m_tip_angle_smooth); 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", &Brush::m_tip_mix); init_slider(m_tip_mix, "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix, &Brush::m_tip_mix);
init_slider(m_tip_wet, "tip-wet", &Brush::m_tip_wet); init_slider(m_tip_wet, "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet, &Brush::m_tip_wet);
init_slider(m_tip_noise, "tip-noise", &Brush::m_tip_noise); init_slider(m_tip_noise, "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise, &Brush::m_tip_noise);
init_slider(m_tip_hue, "tip-hue", &Brush::m_tip_hue); init_slider(m_tip_hue, "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue, &Brush::m_tip_hue);
init_slider(m_tip_sat, "tip-sat", &Brush::m_tip_sat); init_slider(m_tip_sat, "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation, &Brush::m_tip_sat);
init_slider(m_tip_val, "tip-val", &Brush::m_tip_val); init_slider(m_tip_val, "tip-val", pp::app::BrushStrokeFloatSetting::tip_value, &Brush::m_tip_val);
init_slider(m_jitter_scale, "jitter-scale", &Brush::m_jitter_scale); init_slider(m_jitter_scale, "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale, &Brush::m_jitter_scale);
init_slider(m_jitter_angle, "jitter-angle", &Brush::m_jitter_angle); init_slider(m_jitter_angle, "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle, &Brush::m_jitter_angle);
init_slider(m_jitter_scatter, "jitter-scatter", &Brush::m_jitter_scatter); init_slider(m_jitter_scatter, "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter, &Brush::m_jitter_scatter);
init_slider(m_jitter_flow, "jitter-flow", &Brush::m_jitter_flow); init_slider(m_jitter_flow, "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow, &Brush::m_jitter_flow);
init_slider(m_jitter_opacity, "jitter-opacity", &Brush::m_jitter_opacity); init_slider(m_jitter_opacity, "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity, &Brush::m_jitter_opacity);
init_slider(m_jitter_hue, "jitter-hue", &Brush::m_jitter_hue); init_slider(m_jitter_hue, "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue, &Brush::m_jitter_hue);
init_slider(m_jitter_sat, "jitter-sat", &Brush::m_jitter_sat); init_slider(m_jitter_sat, "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation, &Brush::m_jitter_sat);
init_slider(m_jitter_val, "jitter-val", &Brush::m_jitter_val); init_slider(m_jitter_val, "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value, &Brush::m_jitter_val);
init_slider(m_jitter_aspect, "jitter-aspect", &Brush::m_jitter_aspect); 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_init, "tip-angle-init", pp::app::BrushStrokeBoolSetting::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_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", &Brush::m_tip_flow_pressure); 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", &Brush::m_tip_opacity_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", &Brush::m_tip_size_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", &Brush::m_jitter_scatter_bothaxis); 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", &Brush::m_jitter_aspect_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", &Brush::m_jitter_hsv_eachsample); 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_invert, "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert, &Brush::m_tip_invert);
init_checkbox(m_tip_flipx, "tip-flipx", &Brush::m_tip_flipx); init_checkbox(m_tip_flipx, "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x, &Brush::m_tip_flipx);
init_checkbox(m_tip_flipy, "tip-flipy", &Brush::m_tip_flipy); init_checkbox(m_tip_flipy, "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y, &Brush::m_tip_flipy);
init_checkbox(m_pattern_enabled, "pattern-enabled", &Brush::m_pattern_enabled); init_checkbox(m_pattern_enabled, "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled, &Brush::m_pattern_enabled);
init_checkbox(m_dual_enabled, "dual-enabled", &Brush::m_dual_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", &Brush::m_dual_scatter_bothaxis); 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", &Brush::m_dual_invert); init_checkbox(m_dual_invert, "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert, &Brush::m_dual_invert);
init_checkbox(m_dual_flipx, "dual-flipx", &Brush::m_dual_flipx); init_checkbox(m_dual_flipx, "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x, &Brush::m_dual_flipx);
init_checkbox(m_dual_flipy, "dual-flipy", &Brush::m_dual_flipy); init_checkbox(m_dual_flipy, "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y, &Brush::m_dual_flipy);
init_checkbox(m_dual_randflip, "dual-randflip", &Brush::m_dual_randflip); init_checkbox(m_dual_randflip, "dual-randflip", pp::app::BrushStrokeBoolSetting::dual_random_flip, &Brush::m_dual_randflip);
init_checkbox(m_tip_randflipx, "tip-randflipx", &Brush::m_tip_randflipx); 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", &Brush::m_tip_randflipy); 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", &Brush::m_pattern_eachsample); 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_invert, "pattern-invert", pp::app::BrushStrokeBoolSetting::pattern_invert, &Brush::m_pattern_invert);
init_checkbox(m_pattern_flipx, "pattern-flipx", &Brush::m_pattern_flipx); init_checkbox(m_pattern_flipx, "pattern-flipx", pp::app::BrushStrokeBoolSetting::pattern_flip_x, &Brush::m_pattern_flipx);
init_checkbox(m_pattern_flipy, "pattern-flipy", &Brush::m_pattern_flipy); 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", &Brush::m_pattern_rand_offset); 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_size, "dual-size", pp::app::BrushStrokeFloatSetting::dual_size, &Brush::m_dual_size);
init_slider(m_dual_spacing, "dual-spacing", &Brush::m_dual_spacing); init_slider(m_dual_spacing, "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing, &Brush::m_dual_spacing);
init_slider(m_dual_scatter, "dual-scatter", &Brush::m_dual_scatter); init_slider(m_dual_scatter, "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter, &Brush::m_dual_scatter);
init_slider(m_tip_aspect, "tip-aspect", &Brush::m_tip_aspect); init_slider(m_tip_aspect, "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect, &Brush::m_tip_aspect);
init_slider(m_dual_opacity, "dual-opacity", &Brush::m_dual_opacity); init_slider(m_dual_opacity, "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity, &Brush::m_dual_opacity);
init_slider(m_dual_flow, "dual-flow", &Brush::m_dual_flow); init_slider(m_dual_flow, "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow, &Brush::m_dual_flow);
init_slider(m_dual_rotate, "dual-rotate", &Brush::m_dual_rotate); init_slider(m_dual_rotate, "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate, &Brush::m_dual_rotate);
init_slider(m_pattern_scale, "pattern-scale", &Brush::m_pattern_scale); init_slider(m_pattern_scale, "pattern-scale", pp::app::BrushStrokeFloatSetting::pattern_scale, &Brush::m_pattern_scale);
init_slider(m_pattern_brightness, "pattern-brightness", &Brush::m_pattern_brightness); init_slider(m_pattern_brightness, "pattern-brightness", pp::app::BrushStrokeFloatSetting::pattern_brightness, &Brush::m_pattern_brightness);
init_slider(m_pattern_contrast, "pattern-contrast", &Brush::m_pattern_contrast); init_slider(m_pattern_contrast, "pattern-contrast", pp::app::BrushStrokeFloatSetting::pattern_contrast, &Brush::m_pattern_contrast);
init_slider(m_pattern_depth, "pattern-depth", &Brush::m_pattern_depth); init_slider(m_pattern_depth, "pattern-depth", pp::app::BrushStrokeFloatSetting::pattern_depth, &Brush::m_pattern_depth);
SliderCurve curve_cubic { SliderCurve curve_cubic {
[](float v) { return glm::pow(v, 3.f); }, [](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 = find<NodeButtonCustom>("tip-aspect-reset");
m_tip_aspect_reset->on_click = [this](Node*) { m_tip_aspect_reset->on_click = [this](Node*) {
m_tip_aspect->set_value(0.5); execute_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset());
Canvas::I->m_current_brush->m_tip_aspect = 0.5f;
//m_preview->draw_stroke();
if (on_stroke_change)
on_stroke_change(this);
}; };
m_dual_blend_mode = find<NodeComboBox>("dual-blend-mode"); m_dual_blend_mode = find<NodeComboBox>("dual-blend-mode");
m_dual_blend_mode->on_select = [this](Node*, int index) { m_dual_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_dual_blend_mode = index; const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::dual, index);
//m_preview->draw_stroke(); if (plan) {
if (on_stroke_change) execute_stroke_control_plan(plan.value());
on_stroke_change(this); }
}; };
m_pattern_blend_mode = find<NodeComboBox>("pattern-blend-mode"); m_pattern_blend_mode = find<NodeComboBox>("pattern-blend-mode");
m_pattern_blend_mode->on_select = [this](Node*, int index) { m_pattern_blend_mode->on_select = [this](Node*, int index) {
Canvas::I->m_current_brush->m_pattern_blend_mode = index; const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, index);
//m_preview->draw_stroke(); if (plan) {
if (on_stroke_change) execute_stroke_control_plan(plan.value());
on_stroke_change(this); }
}; };
m_preview->m_brush = Canvas::I->m_current_brush; 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 = find<NodeButton>("brush-settings-reset");
m_brush_settings_reset->on_click = [br_idx,this](Node*) { m_brush_settings_reset->on_click = [this](Node*) {
auto b = std::make_shared<Brush>(); execute_stroke_control_plan(pp::app::plan_brush_default_settings_reset());
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);
}; };
update_controls(); 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 = find<NodeSliderH>(id);
target->on_value_changed = std::bind(&NodePanelStroke::handle_slide, 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; //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); auto curve = m_curves.find((NodeSliderH*)target);
Canvas::I->m_current_brush.get()->*prop = curve != m_curves.end() ? curve->second.to_value(value) : value; const auto brush_value = curve != m_curves.end() ? curve->second.to_value(value) : value;
//m_preview->draw_stroke(); const auto plan = pp::app::plan_brush_stroke_float_setting(setting, brush_value);
if (on_stroke_change) if (plan) {
on_stroke_change(this); 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 = find<NodeCheckBox>(id);
target->on_value_changed = std::bind(&NodePanelStroke::handle_checkbox, 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; 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; const auto plan = pp::app::plan_brush_stroke_bool_setting(setting, value);
//m_preview->draw_stroke(); execute_stroke_control_plan(plan);
if (on_stroke_change) }
on_stroke_change(this);
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) kEventResult NodePanelStroke::handle_event(Event* e)

View File

@@ -9,6 +9,13 @@
#include "node_image.h" #include "node_image.h"
#include "node_panel_brush.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 class NodePanelStroke : public Node
{ {
public: public:
@@ -103,6 +110,7 @@ public:
inline float to_slider(float v) { return m_inv(v); } inline float to_slider(float v) { return m_inv(v); }
}; };
std::map<NodeSliderH*, SliderCurve> m_curves; std::map<NodeSliderH*, SliderCurve> m_curves;
int m_default_brush_index = 0;
virtual Node* clone_instantiate() const override; virtual Node* clone_instantiate() const override;
virtual void clone_finalize(Node* dest) 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 set_size(float value, bool normalized, bool propagate);
void init_fold(const std::string& name); void init_fold(const std::string& name);
void init_slider(NodeSliderH*& slider, const char* id, float Brush::* prop); void init_slider(NodeSliderH*& slider, const char* id, pp::app::BrushStrokeFloatSetting setting, float Brush::* prop);
void handle_slide(float Brush::* prop, Node* target, float value); 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 init_checkbox(NodeCheckBox*& slider, const char* id, pp::app::BrushStrokeBoolSetting setting, bool Brush::* prop);
void handle_checkbox(bool Brush::* prop, Node* target, bool value); void handle_checkbox(pp::app::BrushStrokeBoolSetting setting, bool value);
void execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan);
}; };

View File

@@ -6,6 +6,70 @@ namespace pp::paint_renderer {
namespace { namespace {
[[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept
{
switch (mode) {
case pp::paint::BlendMode::normal:
case pp::paint::BlendMode::multiply:
case pp::paint::BlendMode::screen:
case pp::paint::BlendMode::color_dodge:
case pp::paint::BlendMode::overlay:
return true;
}
return false;
}
[[nodiscard]] bool is_valid_stroke_blend_mode(pp::paint::StrokeBlendMode mode) noexcept
{
switch (mode) {
case pp::paint::StrokeBlendMode::normal:
case pp::paint::StrokeBlendMode::multiply:
case pp::paint::StrokeBlendMode::subtract:
case pp::paint::StrokeBlendMode::darken:
case pp::paint::StrokeBlendMode::overlay:
case pp::paint::StrokeBlendMode::color_dodge:
case pp::paint::StrokeBlendMode::color_burn:
case pp::paint::StrokeBlendMode::linear_burn:
case pp::paint::StrokeBlendMode::hard_mix:
case pp::paint::StrokeBlendMode::linear_height:
case pp::paint::StrokeBlendMode::height:
return true;
}
return false;
}
[[nodiscard]] bool paint_blend_mode_from_persisted_index(int value, pp::paint::BlendMode& out) noexcept
{
switch (value) {
case 0: out = pp::paint::BlendMode::normal; return true;
case 1: out = pp::paint::BlendMode::multiply; return true;
case 2: out = pp::paint::BlendMode::screen; return true;
case 3: out = pp::paint::BlendMode::color_dodge; return true;
case 4: out = pp::paint::BlendMode::overlay; return true;
default: return false;
}
}
[[nodiscard]] bool stroke_blend_mode_from_persisted_index(int value, pp::paint::StrokeBlendMode& out) noexcept
{
switch (value) {
case 0: out = pp::paint::StrokeBlendMode::normal; return true;
case 1: out = pp::paint::StrokeBlendMode::multiply; return true;
case 2: out = pp::paint::StrokeBlendMode::subtract; return true;
case 3: out = pp::paint::StrokeBlendMode::darken; return true;
case 4: out = pp::paint::StrokeBlendMode::overlay; return true;
case 5: out = pp::paint::StrokeBlendMode::color_dodge; return true;
case 6: out = pp::paint::StrokeBlendMode::color_burn; return true;
case 7: out = pp::paint::StrokeBlendMode::linear_burn; return true;
case 8: out = pp::paint::StrokeBlendMode::hard_mix; return true;
case 9: out = pp::paint::StrokeBlendMode::linear_height; return true;
case 10: out = pp::paint::StrokeBlendMode::height; return true;
default: return false;
}
}
[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept [[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
{ {
const auto extent_status = pp::renderer::validate_extent(extent); const auto extent_status = pp::renderer::validate_extent(extent);
@@ -29,6 +93,56 @@ namespace {
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count)); return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count));
} }
[[nodiscard]] StrokeCompositePath composite_path_from_feedback(pp::renderer::PaintFeedbackPath path) noexcept
{
switch (path) {
case pp::renderer::PaintFeedbackPath::none:
return StrokeCompositePath::fixed_function_blend;
case pp::renderer::PaintFeedbackPath::framebuffer_fetch:
return StrokeCompositePath::framebuffer_fetch;
case pp::renderer::PaintFeedbackPath::ping_pong_textures:
return StrokeCompositePath::ping_pong_textures;
}
return StrokeCompositePath::fixed_function_blend;
}
void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept
{
gate.path = stroke.path;
gate.reads_destination_color = stroke.path == StrokeCompositePath::framebuffer_fetch;
gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture;
gate.requires_texture_copy = stroke.requires_texture_copy;
gate.requires_render_target_blit = stroke.requires_render_target_blit;
}
void apply_feedback_plan(CanvasStrokeFeedbackPlan& plan, const pp::renderer::PaintFeedbackPlan& feedback) noexcept
{
plan.path = composite_path_from_feedback(feedback.path);
plan.reads_destination_color = plan.path == StrokeCompositePath::framebuffer_fetch;
plan.requires_auxiliary_texture = feedback.requires_auxiliary_texture;
plan.requires_texture_copy = feedback.requires_texture_copy;
plan.requires_render_target_blit = feedback.requires_render_target_blit;
}
void mark_shader_blend_fallback(
CanvasBlendGatePlan& gate,
pp::renderer::RenderDeviceFeatures features) noexcept
{
gate.shader_blend = true;
gate.complex_blend = true;
gate.compatibility_fallback = true;
if (features.framebuffer_fetch) {
gate.path = StrokeCompositePath::framebuffer_fetch;
gate.reads_destination_color = true;
} else if (features.texture_copy || features.render_target_blit) {
gate.path = StrokeCompositePath::ping_pong_textures;
gate.requires_auxiliary_texture = true;
gate.requires_texture_copy = features.texture_copy;
gate.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
}
}
} }
pp::foundation::Status composite_layer( pp::foundation::Status composite_layer(
@@ -62,4 +176,193 @@ pp::foundation::Status composite_layer(
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
bool stroke_composite_requires_feedback(
pp::paint::BlendMode layer_blend_mode,
pp::paint::StrokeBlendMode stroke_blend_mode,
bool dual_brush_blend,
bool pattern_blend) noexcept
{
return layer_blend_mode != pp::paint::BlendMode::normal
|| stroke_blend_mode != pp::paint::StrokeBlendMode::normal
|| dual_brush_blend
|| pattern_blend;
}
pp::foundation::Result<StrokeCompositePlan> plan_stroke_composite(
pp::renderer::RenderDeviceFeatures features,
StrokeCompositeRequest request) noexcept
{
if (!is_valid_blend_mode(request.layer_blend_mode)) {
return pp::foundation::Result<StrokeCompositePlan>::failure(
pp::foundation::Status::invalid_argument("unknown layer blend mode"));
}
if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) {
return pp::foundation::Result<StrokeCompositePlan>::failure(
pp::foundation::Status::invalid_argument("unknown stroke blend mode"));
}
const pp::renderer::TextureDesc target_desc {
.extent = request.extent,
.format = request.target_format,
.usage = request.target_usage,
.debug_name = "stroke-composite-target",
};
const auto complex_blend = stroke_composite_requires_feedback(
request.layer_blend_mode,
request.stroke_blend_mode,
request.dual_brush_blend,
request.pattern_blend);
const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, complex_blend);
if (!feedback) {
return pp::foundation::Result<StrokeCompositePlan>::failure(feedback.status());
}
StrokeCompositePlan plan;
plan.path = composite_path_from_feedback(feedback.value().path);
plan.feedback = feedback.value();
plan.target_desc = target_desc;
plan.target_bytes = feedback.value().target_bytes;
plan.auxiliary_bytes = feedback.value().requires_auxiliary_texture
? feedback.value().target_bytes
: 0U;
plan.estimated_working_bytes = plan.target_bytes + plan.auxiliary_bytes;
plan.complex_blend = complex_blend;
plan.reads_destination_color = feedback.value().reads_destination_color;
plan.requires_auxiliary_texture = feedback.value().requires_auxiliary_texture;
plan.requires_texture_copy = feedback.value().requires_texture_copy;
plan.requires_render_target_blit = feedback.value().requires_render_target_blit;
plan.requires_explicit_transition = feedback.value().requires_explicit_transition;
return pp::foundation::Result<StrokeCompositePlan>::success(plan);
}
pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept
{
CanvasBlendGatePlan gate;
for (std::size_t i = 0; i < request.layer_blend_modes.size(); ++i) {
pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal;
if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) {
if (request.layer_blend_modes[i] != 0) {
gate.first_complex_layer_index = static_cast<int>(i);
mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
continue;
}
if (layer_blend == pp::paint::BlendMode::normal) {
continue;
}
gate.shader_blend = true;
gate.complex_blend = true;
gate.first_complex_layer_index = static_cast<int>(i);
const auto stroke = plan_stroke_composite(
features,
StrokeCompositeRequest {
.extent = request.extent,
.layer_blend_mode = layer_blend,
});
if (stroke) {
apply_stroke_plan(gate, stroke.value());
} else {
gate.compatibility_fallback = true;
}
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal;
if (request.has_stroke_blend_mode) {
if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) {
if (request.stroke_blend_mode != 0) {
gate.stroke_complex = true;
mark_shader_blend_fallback(gate, features);
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
} else if (stroke_blend != pp::paint::StrokeBlendMode::normal) {
gate.stroke_complex = true;
}
}
gate.dual_brush_complex = request.dual_brush_blend;
gate.pattern_complex = request.pattern_blend;
if (!gate.stroke_complex && !gate.dual_brush_complex && !gate.pattern_complex) {
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
gate.shader_blend = true;
gate.complex_blend = true;
const auto stroke = plan_stroke_composite(
features,
StrokeCompositeRequest {
.extent = request.extent,
.stroke_blend_mode = stroke_blend,
.dual_brush_blend = request.dual_brush_blend,
.pattern_blend = request.pattern_blend,
});
if (stroke) {
apply_stroke_plan(gate, stroke.value());
} else {
gate.compatibility_fallback = true;
}
return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}
pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
pp::renderer::RenderDeviceFeatures features,
pp::renderer::Extent2D extent) noexcept
{
const auto extent_status = pp::renderer::validate_extent(extent);
if (!extent_status.ok()) {
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::failure(extent_status);
}
const pp::renderer::TextureDesc target_desc {
.extent = extent,
.format = pp::renderer::TextureFormat::rgba8,
.usage = pp::renderer::TextureUsage::render_target
| pp::renderer::TextureUsage::sampled
| pp::renderer::TextureUsage::copy_source
| pp::renderer::TextureUsage::copy_destination,
.debug_name = "canvas-stroke-feedback-target",
};
const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, true);
if (feedback) {
CanvasStrokeFeedbackPlan plan;
apply_feedback_plan(plan, feedback.value());
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(plan);
}
CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
if (features.framebuffer_fetch) {
fallback.path = StrokeCompositePath::framebuffer_fetch;
fallback.reads_destination_color = true;
} else {
fallback.path = StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
fallback.requires_texture_copy = features.texture_copy;
fallback.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
}
return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(fallback);
}
const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
{
switch (path) {
case StrokeCompositePath::fixed_function_blend:
return "fixed_function_blend";
case StrokeCompositePath::framebuffer_fetch:
return "framebuffer_fetch";
case StrokeCompositePath::ping_pong_textures:
return "ping_pong_textures";
}
return "unknown";
}
} }

View File

@@ -4,10 +4,17 @@
#include "paint/blend.h" #include "paint/blend.h"
#include "renderer_api/renderer_api.h" #include "renderer_api/renderer_api.h"
#include <cstdint>
#include <span> #include <span>
namespace pp::paint_renderer { namespace pp::paint_renderer {
enum class StrokeCompositePath : std::uint8_t {
fixed_function_blend,
framebuffer_fetch,
ping_pong_textures,
};
struct LayerCompositeView { struct LayerCompositeView {
std::span<const pp::paint::Rgba> pixels; std::span<const pp::paint::Rgba> pixels;
float opacity = 1.0F; float opacity = 1.0F;
@@ -15,9 +22,90 @@ struct LayerCompositeView {
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal; pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
}; };
struct StrokeCompositeRequest {
pp::renderer::Extent2D extent {};
pp::renderer::TextureFormat target_format = pp::renderer::TextureFormat::rgba8;
pp::renderer::TextureUsage target_usage = pp::renderer::TextureUsage::render_target
| pp::renderer::TextureUsage::sampled
| pp::renderer::TextureUsage::copy_source
| pp::renderer::TextureUsage::copy_destination;
pp::paint::BlendMode layer_blend_mode = pp::paint::BlendMode::normal;
pp::paint::StrokeBlendMode stroke_blend_mode = pp::paint::StrokeBlendMode::normal;
bool dual_brush_blend = false;
bool pattern_blend = false;
};
struct StrokeCompositePlan {
StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
pp::renderer::PaintFeedbackPlan feedback {};
pp::renderer::TextureDesc target_desc {};
std::uint64_t target_bytes = 0;
std::uint64_t auxiliary_bytes = 0;
std::uint64_t estimated_working_bytes = 0;
bool complex_blend = false;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
bool requires_explicit_transition = false;
};
struct CanvasBlendGateRequest {
pp::renderer::Extent2D extent {};
std::span<const int> layer_blend_modes;
bool has_stroke_blend_mode = false;
int stroke_blend_mode = 0;
bool dual_brush_blend = false;
bool pattern_blend = false;
};
struct CanvasBlendGatePlan {
bool shader_blend = false;
bool complex_blend = false;
bool compatibility_fallback = false;
bool stroke_complex = false;
bool dual_brush_complex = false;
bool pattern_complex = false;
int first_complex_layer_index = -1;
StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
};
struct CanvasStrokeFeedbackPlan {
StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
bool compatibility_fallback = false;
};
[[nodiscard]] pp::foundation::Status composite_layer( [[nodiscard]] pp::foundation::Status composite_layer(
std::span<pp::paint::Rgba> destination, std::span<pp::paint::Rgba> destination,
pp::renderer::Extent2D extent, pp::renderer::Extent2D extent,
LayerCompositeView layer) noexcept; LayerCompositeView layer) noexcept;
[[nodiscard]] bool stroke_composite_requires_feedback(
pp::paint::BlendMode layer_blend_mode,
pp::paint::StrokeBlendMode stroke_blend_mode,
bool dual_brush_blend,
bool pattern_blend) noexcept;
[[nodiscard]] pp::foundation::Result<StrokeCompositePlan> plan_stroke_composite(
pp::renderer::RenderDeviceFeatures features,
StrokeCompositeRequest request) noexcept;
[[nodiscard]] pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
pp::renderer::RenderDeviceFeatures features,
CanvasBlendGateRequest request) noexcept;
[[nodiscard]] pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
pp::renderer::RenderDeviceFeatures features,
pp::renderer::Extent2D extent) noexcept;
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
} }

View File

@@ -823,6 +823,65 @@ pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc desti
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
pp::foundation::Result<PaintFeedbackPlan> plan_paint_feedback(
RenderDeviceFeatures features,
TextureDesc target_desc,
bool complex_blend) noexcept
{
const auto target_status = validate_texture_desc(target_desc);
if (!target_status.ok()) {
return pp::foundation::Result<PaintFeedbackPlan>::failure(target_status);
}
if (!has_texture_usage(target_desc.usage, TextureUsage::render_target)) {
return pp::foundation::Result<PaintFeedbackPlan>::failure(
pp::foundation::Status::invalid_argument("paint feedback target must allow render_target usage"));
}
if (target_desc.format == TextureFormat::depth24_stencil8) {
return pp::foundation::Result<PaintFeedbackPlan>::failure(
pp::foundation::Status::invalid_argument("paint feedback target must be a color texture"));
}
const auto target_bytes = texture_byte_size(target_desc);
if (!target_bytes.ok()) {
return pp::foundation::Result<PaintFeedbackPlan>::failure(target_bytes.status());
}
PaintFeedbackPlan plan;
plan.target_desc = target_desc;
plan.target_bytes = target_bytes.value();
plan.complex_blend = complex_blend;
if (!complex_blend) {
return pp::foundation::Result<PaintFeedbackPlan>::success(plan);
}
plan.reads_destination_color = true;
if (features.framebuffer_fetch) {
plan.path = PaintFeedbackPath::framebuffer_fetch;
plan.requires_explicit_transition = features.explicit_texture_transitions;
return pp::foundation::Result<PaintFeedbackPlan>::success(plan);
}
const bool can_ping_pong = has_texture_usage(target_desc.usage, TextureUsage::sampled)
&& has_texture_usage(target_desc.usage, TextureUsage::copy_source)
&& has_texture_usage(target_desc.usage, TextureUsage::copy_destination)
&& (features.texture_copy || features.render_target_blit);
if (!can_ping_pong) {
return pp::foundation::Result<PaintFeedbackPlan>::failure(
pp::foundation::Status::invalid_argument(
"complex paint feedback requires framebuffer fetch or sampled copy-capable render targets"));
}
plan.path = PaintFeedbackPath::ping_pong_textures;
plan.requires_auxiliary_texture = true;
plan.requires_texture_copy = features.texture_copy;
plan.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
plan.requires_explicit_transition = features.explicit_texture_transitions;
plan.auxiliary_desc = target_desc;
return pp::foundation::Result<PaintFeedbackPlan>::success(plan);
}
const char* texture_format_name(TextureFormat format) noexcept const char* texture_format_name(TextureFormat format) noexcept
{ {
switch (format) { switch (format) {
@@ -887,6 +946,20 @@ const char* blit_filter_name(BlitFilter filter) noexcept
return "unknown"; return "unknown";
} }
const char* paint_feedback_path_name(PaintFeedbackPath path) noexcept
{
switch (path) {
case PaintFeedbackPath::none:
return "none";
case PaintFeedbackPath::framebuffer_fetch:
return "framebuffer_fetch";
case PaintFeedbackPath::ping_pong_textures:
return "ping_pong_textures";
}
return "unknown";
}
const char* blend_factor_name(BlendFactor factor) noexcept const char* blend_factor_name(BlendFactor factor) noexcept
{ {
switch (factor) { switch (factor) {

View File

@@ -132,6 +132,12 @@ enum class BlitFilter : std::uint8_t {
linear, linear,
}; };
enum class PaintFeedbackPath : std::uint8_t {
none,
framebuffer_fetch,
ping_pong_textures,
};
enum class BlendFactor : std::uint8_t { enum class BlendFactor : std::uint8_t {
zero, zero,
one, one,
@@ -236,6 +242,19 @@ struct RenderDeviceFeatures {
bool float32_render_targets = false; bool float32_render_targets = false;
}; };
struct PaintFeedbackPlan {
PaintFeedbackPath path = PaintFeedbackPath::none;
TextureDesc target_desc {};
TextureDesc auxiliary_desc {};
std::uint64_t target_bytes = 0;
bool complex_blend = false;
bool reads_destination_color = false;
bool requires_auxiliary_texture = false;
bool requires_texture_copy = false;
bool requires_render_target_blit = false;
bool requires_explicit_transition = false;
};
class ITexture2D { class ITexture2D {
public: public:
virtual ~ITexture2D() = default; virtual ~ITexture2D() = default;
@@ -393,10 +412,15 @@ public:
[[nodiscard]] pp::foundation::Status validate_blit_descs( [[nodiscard]] pp::foundation::Status validate_blit_descs(
TextureDesc source, TextureDesc source,
TextureDesc destination) noexcept; TextureDesc destination) noexcept;
[[nodiscard]] pp::foundation::Result<PaintFeedbackPlan> plan_paint_feedback(
RenderDeviceFeatures features,
TextureDesc target_desc,
bool complex_blend) noexcept;
[[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept;
[[nodiscard]] const char* texture_state_name(TextureState state) noexcept; [[nodiscard]] const char* texture_state_name(TextureState state) noexcept;
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
[[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept; [[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept;
[[nodiscard]] const char* paint_feedback_path_name(PaintFeedbackPath path) noexcept;
[[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept; [[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept;
[[nodiscard]] const char* blend_op_name(BlendOp op) noexcept; [[nodiscard]] const char* blend_op_name(BlendOp op) noexcept;
[[nodiscard]] const char* compare_op_name(CompareOp op) noexcept; [[nodiscard]] const char* compare_op_name(CompareOp op) noexcept;

View File

@@ -211,6 +211,7 @@ std::int32_t get_opengl_uniform_location(std::uint32_t program, const char* name
std::map<kShader, Shader> ShaderManager::m_shaders; std::map<kShader, Shader> ShaderManager::m_shaders;
Shader* ShaderManager::m_current; Shader* ShaderManager::m_current;
pp::renderer::RenderDeviceFeatures ShaderManager::m_render_device_features {};
bool ShaderManager::ext_framebuffer_fetch = false; bool ShaderManager::ext_framebuffer_fetch = false;
bool ShaderManager::ext_float32 = false; bool ShaderManager::ext_float32 = false;
bool ShaderManager::ext_float32_linear = false; bool ShaderManager::ext_float32_linear = false;
@@ -816,6 +817,16 @@ void ShaderManager::u_float(kShaderUniform id, float f)
m_current->u_float(id, f); m_current->u_float(id, f);
} }
void ShaderManager::set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept
{
m_render_device_features = features;
}
pp::renderer::RenderDeviceFeatures ShaderManager::render_device_features() noexcept
{
return m_render_device_features;
}
void ShaderManager::invalidate() void ShaderManager::invalidate()
{ {
m_shaders.clear(); m_shaders.clear();

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include "renderer_api/renderer_api.h"
#include "util.h" #include "util.h"
bool check_uniform_uniqueness(); bool check_uniform_uniqueness();
@@ -108,6 +109,7 @@ class ShaderManager
{ {
static std::map<kShader, Shader> m_shaders; static std::map<kShader, Shader> m_shaders;
static Shader* m_current; static Shader* m_current;
static pp::renderer::RenderDeviceFeatures m_render_device_features;
public: public:
static bool ext_framebuffer_fetch; static bool ext_framebuffer_fetch;
static bool ext_float32; static bool ext_float32;
@@ -127,5 +129,7 @@ public:
static void u_int(kShaderUniform id, int i); static void u_int(kShaderUniform id, int i);
static void u_int(const char* name, int i); static void u_int(const char* name, int i);
static void u_float(kShaderUniform id, float f); static void u_float(kShaderUniform id, float f);
static void set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept;
static pp::renderer::RenderDeviceFeatures render_device_features() noexcept;
static void invalidate(); static void invalidate();
}; };

View File

@@ -1132,6 +1132,126 @@ if(TARGET pano_cli)
LABELS "app;paint;integration;desktop-fast;fuzz" LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_brush_texture_list_add_smoke
COMMAND pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png)
set_tests_properties(pano_cli_plan_brush_texture_list_add_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"add-texture\".*\"source\":\"C:/Temp/soft.png\".*\"path\":\"data/brushes/soft.png\".*\"thumb\":\"data/brushes/thumbs/soft.png\".*\"brushName\":\"soft\".*\"userTexture\":true.*\"convertsBrushAlpha\":true")
add_test(NAME pano_cli_plan_brush_texture_list_remove_user_smoke
COMMAND pano_cli plan-brush-texture-list --kind remove --item-count 3 --current-index 2 --user-texture)
set_tests_properties(pano_cli_plan_brush_texture_list_remove_user_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"remove-texture\".*\"itemCount\":3.*\"currentIndex\":2.*\"targetIndex\":1.*\"deletesTextureFiles\":true.*\"notifiesSelection\":true")
add_test(NAME pano_cli_plan_brush_texture_list_move_edge_smoke
COMMAND pano_cli plan-brush-texture-list --kind move --item-count 3 --current-index 0 --offset -1)
set_tests_properties(pano_cli_plan_brush_texture_list_move_edge_smoke PROPERTIES
LABELS "app;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-texture-list\".*\"operation\":\"move-texture\".*\"currentIndex\":0.*\"targetIndex\":0.*\"moveOffset\":-1.*\"noOp\":true")
add_test(NAME pano_cli_plan_brush_texture_list_rejects_bad_source
COMMAND pano_cli plan-brush-texture-list --kind add --source no-extension)
set_tests_properties(pano_cli_plan_brush_texture_list_rejects_bad_source PROPERTIES
LABELS "app;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_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_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
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"framebuffer_fetch\".*\"readsDestinationColor\":true.*\"requiresAuxiliaryTexture\":false.*\"requiresExplicitTransition\":true")
add_test(NAME pano_cli_plan_paint_feedback_ping_pong_copy_smoke
COMMAND pano_cli plan-paint-feedback --texture-copy)
set_tests_properties(pano_cli_plan_paint_feedback_ping_pong_copy_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"ping_pong_textures\".*\"requiresAuxiliaryTexture\":true.*\"requiresTextureCopy\":true.*\"requiresRenderTargetBlit\":false")
add_test(NAME pano_cli_plan_paint_feedback_simple_smoke
COMMAND pano_cli plan-paint-feedback --simple --render-only)
set_tests_properties(pano_cli_plan_paint_feedback_simple_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-paint-feedback\".*\"path\":\"none\".*\"complexBlend\":false.*\"readsDestinationColor\":false")
add_test(NAME pano_cli_plan_paint_feedback_rejects_unsupported
COMMAND pano_cli plan-paint-feedback)
set_tests_properties(pano_cli_plan_paint_feedback_rejects_unsupported PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_paint_feedback_rejects_depth
COMMAND pano_cli plan-paint-feedback --texture-copy --depth)
set_tests_properties(pano_cli_plan_paint_feedback_rejects_depth PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_stroke_composite_fixed_smoke
COMMAND pano_cli plan-stroke-composite --render-only)
set_tests_properties(pano_cli_plan_stroke_composite_fixed_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"path\":\"fixed_function_blend\".*\"feedbackPath\":\"none\".*\"complexBlend\":false.*\"readsDestinationColor\":false")
add_test(NAME pano_cli_plan_stroke_composite_framebuffer_fetch_smoke
COMMAND pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only)
set_tests_properties(pano_cli_plan_stroke_composite_framebuffer_fetch_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"strokeBlend\":\"height\".*\"path\":\"framebuffer_fetch\".*\"feedbackPath\":\"framebuffer_fetch\".*\"complexBlend\":true.*\"requiresExplicitTransition\":true")
add_test(NAME pano_cli_plan_stroke_composite_ping_pong_copy_smoke
COMMAND pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy)
set_tests_properties(pano_cli_plan_stroke_composite_ping_pong_copy_smoke PROPERTIES
LABELS "renderer;paint;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-stroke-composite\".*\"layerBlend\":\"overlay\".*\"path\":\"ping_pong_textures\".*\"estimatedWorkingBytes\":16384.*\"requiresAuxiliaryTexture\":true.*\"requiresTextureCopy\":true")
add_test(NAME pano_cli_plan_stroke_composite_rejects_unsupported
COMMAND pano_cli plan-stroke-composite --layer-blend 1)
set_tests_properties(pano_cli_plan_stroke_composite_rejects_unsupported PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_stroke_composite_rejects_bad_stroke_blend
COMMAND pano_cli plan-stroke-composite --stroke-blend 99 --texture-copy)
set_tests_properties(pano_cli_plan_stroke_composite_rejects_bad_stroke_blend PROPERTIES
LABELS "renderer;paint;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_canvas_tool_draw_smoke add_test(NAME pano_cli_plan_canvas_tool_draw_smoke
COMMAND pano_cli plan-canvas-tool --kind draw) COMMAND pano_cli plan-canvas-tool --kind draw)
set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES

View File

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

View File

@@ -2,14 +2,27 @@
#include "test_harness.h" #include "test_harness.h"
#include <cmath> #include <cmath>
#include <string_view>
#include <vector> #include <vector>
using pp::foundation::StatusCode; using pp::foundation::StatusCode;
using pp::paint::BlendMode; using pp::paint::BlendMode;
using pp::paint::Rgba; using pp::paint::Rgba;
using pp::paint::StrokeBlendMode;
using pp::paint_renderer::LayerCompositeView; using pp::paint_renderer::LayerCompositeView;
using pp::paint_renderer::CanvasBlendGateRequest;
using pp::paint_renderer::StrokeCompositePath;
using pp::paint_renderer::StrokeCompositeRequest;
using pp::paint_renderer::composite_layer; using pp::paint_renderer::composite_layer;
using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_feedback;
using pp::paint_renderer::plan_stroke_composite;
using pp::paint_renderer::stroke_composite_path_name;
using pp::paint_renderer::stroke_composite_requires_feedback;
using pp::renderer::Extent2D; using pp::renderer::Extent2D;
using pp::renderer::RenderDeviceFeatures;
using pp::renderer::TextureFormat;
using pp::renderer::TextureUsage;
namespace { namespace {
@@ -97,6 +110,337 @@ void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument); PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument);
} }
void detects_feedback_requirements(pp::tests::Harness& h)
{
PP_EXPECT(h, !stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::multiply,
StrokeBlendMode::normal,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::overlay,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
true,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
false,
true));
}
void plans_stroke_composite_paths(pp::tests::Harness& h)
{
const StrokeCompositeRequest simple {
.extent = Extent2D { .width = 64, .height = 32 },
.target_usage = TextureUsage::render_target,
};
const auto fixed = plan_stroke_composite(
RenderDeviceFeatures {},
simple);
PP_EXPECT(h, fixed);
if (fixed) {
PP_EXPECT(h, fixed.value().path == StrokeCompositePath::fixed_function_blend);
PP_EXPECT(h, !fixed.value().complex_blend);
PP_EXPECT(h, !fixed.value().reads_destination_color);
PP_EXPECT(h, fixed.value().target_bytes == 8192U);
PP_EXPECT(h, fixed.value().estimated_working_bytes == 8192U);
}
const StrokeCompositeRequest complex_fetch {
.extent = Extent2D { .width = 32, .height = 16 },
.target_usage = TextureUsage::render_target,
.stroke_blend_mode = StrokeBlendMode::height,
};
const auto fetch = plan_stroke_composite(
RenderDeviceFeatures {
.framebuffer_fetch = true,
.explicit_texture_transitions = true,
},
complex_fetch);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().complex_blend);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, fetch.value().requires_explicit_transition);
PP_EXPECT(h, fetch.value().target_bytes == 2048U);
}
const StrokeCompositeRequest complex_copy {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = BlendMode::overlay,
.dual_brush_blend = true,
};
const auto copy = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
complex_copy);
PP_EXPECT(h, copy);
if (copy) {
PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, copy.value().requires_auxiliary_texture);
PP_EXPECT(h, copy.value().requires_texture_copy);
PP_EXPECT(h, !copy.value().requires_render_target_blit);
PP_EXPECT(h, copy.value().target_bytes == 2048U);
PP_EXPECT(h, copy.value().auxiliary_bytes == 2048U);
PP_EXPECT(h, copy.value().estimated_working_bytes == 4096U);
}
const auto blit = plan_stroke_composite(
RenderDeviceFeatures { .render_target_blit = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.pattern_blend = true,
});
PP_EXPECT(h, blit);
if (blit) {
PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !blit.value().requires_texture_copy);
PP_EXPECT(h, blit.value().requires_render_target_blit);
}
}
void rejects_bad_stroke_composite_plans(pp::tests::Harness& h)
{
const auto unsupported = plan_stroke_composite(
RenderDeviceFeatures {},
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = BlendMode::multiply,
});
const auto missing_usage = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.target_usage = TextureUsage::render_target,
.layer_blend_mode = BlendMode::multiply,
});
const auto depth = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.target_format = TextureFormat::depth24_stencil8,
.layer_blend_mode = BlendMode::multiply,
});
const auto bad_blend = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = static_cast<BlendMode>(255),
});
const auto bad_stroke_blend = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.stroke_blend_mode = static_cast<StrokeBlendMode>(255),
});
PP_EXPECT(h, !unsupported.ok());
PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_usage.ok());
PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !depth.ok());
PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_blend.ok());
PP_EXPECT(h, bad_blend.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_stroke_blend.ok());
PP_EXPECT(h, bad_stroke_blend.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::fixed_function_blend) == std::string_view("fixed_function_blend"));
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::framebuffer_fetch) == std::string_view("framebuffer_fetch"));
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::ping_pong_textures) == std::string_view("ping_pong_textures"));
PP_EXPECT(h, stroke_composite_path_name(static_cast<StrokeCompositePath>(255)) == std::string_view("unknown"));
}
void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{
const std::vector<int> normal_layers { 0, 0, 0 };
const auto normal = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 0, .height = 0 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 0,
});
PP_EXPECT(h, normal);
if (normal) {
PP_EXPECT(h, !normal.value().shader_blend);
PP_EXPECT(h, !normal.value().complex_blend);
PP_EXPECT(h, !normal.value().compatibility_fallback);
}
const std::vector<int> layer_blend { 0, 4 };
const auto layer = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = layer_blend,
});
PP_EXPECT(h, layer);
if (layer) {
PP_EXPECT(h, layer.value().shader_blend);
PP_EXPECT(h, layer.value().complex_blend);
PP_EXPECT(h, layer.value().first_complex_layer_index == 1);
PP_EXPECT(h, layer.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, layer.value().reads_destination_color);
}
const auto stroke = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, stroke);
if (stroke) {
PP_EXPECT(h, stroke.value().shader_blend);
PP_EXPECT(h, stroke.value().stroke_complex);
PP_EXPECT(h, stroke.value().first_complex_layer_index == -1);
PP_EXPECT(h, stroke.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !stroke.value().reads_destination_color);
PP_EXPECT(h, stroke.value().requires_texture_copy);
}
}
void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{
const std::vector<int> unknown_layer { 0, 99 };
const auto unknown = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown);
if (unknown) {
PP_EXPECT(h, unknown.value().shader_blend);
PP_EXPECT(h, unknown.value().complex_blend);
PP_EXPECT(h, unknown.value().compatibility_fallback);
PP_EXPECT(h, unknown.value().first_complex_layer_index == 1);
PP_EXPECT(h, unknown.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, unknown.value().requires_auxiliary_texture);
PP_EXPECT(h, unknown.value().requires_texture_copy);
}
const std::vector<int> normal_layers { 0 };
const auto unsupported = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, unsupported);
if (unsupported) {
PP_EXPECT(h, unsupported.value().shader_blend);
PP_EXPECT(h, unsupported.value().stroke_complex);
PP_EXPECT(h, unsupported.value().compatibility_fallback);
PP_EXPECT(h, !unsupported.value().requires_texture_copy);
}
const auto unknown_fetch = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown_fetch);
if (unknown_fetch) {
PP_EXPECT(h, unknown_fetch.value().compatibility_fallback);
PP_EXPECT(h, unknown_fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, unknown_fetch.value().reads_destination_color);
PP_EXPECT(h, !unknown_fetch.value().requires_texture_copy);
}
const auto dual_pattern = plan_canvas_blend_gate(
RenderDeviceFeatures { .render_target_blit = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 16, .height = 16 },
.layer_blend_modes = normal_layers,
.dual_brush_blend = true,
.pattern_blend = true,
});
PP_EXPECT(h, dual_pattern);
if (dual_pattern) {
PP_EXPECT(h, dual_pattern.value().shader_blend);
PP_EXPECT(h, dual_pattern.value().dual_brush_complex);
PP_EXPECT(h, dual_pattern.value().pattern_complex);
PP_EXPECT(h, dual_pattern.value().requires_render_target_blit);
}
}
void plans_canvas_stroke_feedback_paths(pp::tests::Harness& h)
{
const Extent2D extent { .width = 32, .height = 16 };
const auto fetch = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .framebuffer_fetch = true },
extent);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, !fetch.value().compatibility_fallback);
}
const auto copy = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
extent);
PP_EXPECT(h, copy);
if (copy) {
PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !copy.value().reads_destination_color);
PP_EXPECT(h, copy.value().requires_auxiliary_texture);
PP_EXPECT(h, copy.value().requires_texture_copy);
PP_EXPECT(h, !copy.value().requires_render_target_blit);
}
const auto blit = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .render_target_blit = true },
extent);
PP_EXPECT(h, blit);
if (blit) {
PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, blit.value().requires_auxiliary_texture);
PP_EXPECT(h, !blit.value().requires_texture_copy);
PP_EXPECT(h, blit.value().requires_render_target_blit);
}
}
void canvas_stroke_feedback_preserves_legacy_fallback(pp::tests::Harness& h)
{
const auto fallback = plan_canvas_stroke_feedback(
RenderDeviceFeatures {},
Extent2D { .width = 32, .height = 16 });
PP_EXPECT(h, fallback);
if (fallback) {
PP_EXPECT(h, fallback.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, fallback.value().requires_auxiliary_texture);
PP_EXPECT(h, !fallback.value().requires_texture_copy);
PP_EXPECT(h, fallback.value().compatibility_fallback);
}
const auto invalid = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
Extent2D { .width = 0, .height = 16 });
PP_EXPECT(h, !invalid.ok());
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
}
} }
int main() int main()
@@ -105,5 +449,12 @@ int main()
harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity); harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity);
harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops); harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops);
harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity); harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity);
harness.run("detects_feedback_requirements", detects_feedback_requirements);
harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans);
harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
return harness.finish(); return harness.finish();
} }

View File

@@ -34,6 +34,7 @@ using pp::renderer::IRenderTarget;
using pp::renderer::IRenderTrace; using pp::renderer::IRenderTrace;
using pp::renderer::IShaderProgram; using pp::renderer::IShaderProgram;
using pp::renderer::MeshDesc; using pp::renderer::MeshDesc;
using pp::renderer::PaintFeedbackPath;
using pp::renderer::PrimitiveTopology; using pp::renderer::PrimitiveTopology;
using pp::renderer::ReadbackRegion; using pp::renderer::ReadbackRegion;
using pp::renderer::RecordedRenderCommandKind; using pp::renderer::RecordedRenderCommandKind;
@@ -66,6 +67,8 @@ using pp::renderer::max_texture_dimension;
using pp::renderer::max_texture_slots; using pp::renderer::max_texture_slots;
using pp::renderer::max_trace_label_bytes; using pp::renderer::max_trace_label_bytes;
using pp::renderer::panopainter_shader_catalog; using pp::renderer::panopainter_shader_catalog;
using pp::renderer::paint_feedback_path_name;
using pp::renderer::plan_paint_feedback;
using pp::renderer::primitive_topology_name; using pp::renderer::primitive_topology_name;
using pp::renderer::readback_byte_size; using pp::renderer::readback_byte_size;
using pp::renderer::recorded_render_command_kind_name; using pp::renderer::recorded_render_command_kind_name;
@@ -1210,6 +1213,94 @@ void validates_blit_contract(pp::tests::Harness& h)
PP_EXPECT(h, blit_filter_name(static_cast<BlitFilter>(255)) == std::string_view("unknown")); PP_EXPECT(h, blit_filter_name(static_cast<BlitFilter>(255)) == std::string_view("unknown"));
} }
void plans_paint_feedback_paths(pp::tests::Harness& h)
{
const TextureDesc render_target {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.usage = TextureUsage::render_target
| TextureUsage::sampled
| TextureUsage::copy_source
| TextureUsage::copy_destination,
.debug_name = "paint-target",
};
const TextureDesc render_only_target {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.usage = TextureUsage::render_target,
.debug_name = "paint-render-only",
};
const TextureDesc depth_target {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::depth24_stencil8,
.usage = TextureUsage::render_target
| TextureUsage::sampled
| TextureUsage::copy_source
| TextureUsage::copy_destination,
.debug_name = "paint-depth",
};
const RenderDeviceFeatures framebuffer_fetch_features {
.framebuffer_fetch = true,
.explicit_texture_transitions = true,
};
const RenderDeviceFeatures copy_features {
.texture_copy = true,
};
const RenderDeviceFeatures blit_features {
.render_target_blit = true,
};
const auto simple = plan_paint_feedback(copy_features, render_only_target, false);
PP_EXPECT(h, simple);
if (simple) {
PP_EXPECT(h, simple.value().path == PaintFeedbackPath::none);
PP_EXPECT(h, !simple.value().reads_destination_color);
PP_EXPECT(h, simple.value().target_bytes == 8192U);
}
const auto fetch = plan_paint_feedback(framebuffer_fetch_features, render_only_target, true);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == PaintFeedbackPath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, !fetch.value().requires_texture_copy);
PP_EXPECT(h, fetch.value().requires_explicit_transition);
}
const auto ping_pong_copy = plan_paint_feedback(copy_features, render_target, true);
PP_EXPECT(h, ping_pong_copy);
if (ping_pong_copy) {
PP_EXPECT(h, ping_pong_copy.value().path == PaintFeedbackPath::ping_pong_textures);
PP_EXPECT(h, ping_pong_copy.value().requires_auxiliary_texture);
PP_EXPECT(h, ping_pong_copy.value().requires_texture_copy);
PP_EXPECT(h, !ping_pong_copy.value().requires_render_target_blit);
PP_EXPECT(h, ping_pong_copy.value().auxiliary_desc.extent.width == render_target.extent.width);
}
const auto ping_pong_blit = plan_paint_feedback(blit_features, render_target, true);
PP_EXPECT(h, ping_pong_blit);
if (ping_pong_blit) {
PP_EXPECT(h, ping_pong_blit.value().path == PaintFeedbackPath::ping_pong_textures);
PP_EXPECT(h, !ping_pong_blit.value().requires_texture_copy);
PP_EXPECT(h, ping_pong_blit.value().requires_render_target_blit);
}
const auto unsupported = plan_paint_feedback(RenderDeviceFeatures {}, render_target, true);
const auto missing_usage = plan_paint_feedback(copy_features, render_only_target, true);
const auto depth = plan_paint_feedback(copy_features, depth_target, true);
PP_EXPECT(h, !unsupported.ok());
PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_usage.ok());
PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !depth.ok());
PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, paint_feedback_path_name(PaintFeedbackPath::framebuffer_fetch) == std::string_view("framebuffer_fetch"));
PP_EXPECT(h, paint_feedback_path_name(PaintFeedbackPath::ping_pong_textures) == std::string_view("ping_pong_textures"));
PP_EXPECT(h, paint_feedback_path_name(static_cast<PaintFeedbackPath>(255)) == std::string_view("unknown"));
}
void validates_texture_copy_contract(pp::tests::Harness& h) void validates_texture_copy_contract(pp::tests::Harness& h)
{ {
const TextureDesc rgba_desc { const TextureDesc rgba_desc {
@@ -2696,6 +2787,7 @@ int main()
harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes); harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes);
harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes);
harness.run("validates_blit_contract", validates_blit_contract); harness.run("validates_blit_contract", validates_blit_contract);
harness.run("plans_paint_feedback_paths", plans_paint_feedback_paths);
harness.run("validates_texture_copy_contract", validates_texture_copy_contract); harness.run("validates_texture_copy_contract", validates_texture_copy_contract);
harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_blend_contract", validates_blend_contract);
harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_depth_contract", validates_depth_contract);

View File

@@ -7,6 +7,7 @@ target_link_libraries(pano_cli PRIVATE
pp_foundation pp_foundation
pp_assets pp_assets
pp_document pp_document
pp_paint_renderer
pp_renderer_api pp_renderer_api
pp_ui_core) pp_ui_core)

View File

@@ -31,6 +31,7 @@
#include "foundation/parse.h" #include "foundation/parse.h"
#include "foundation/result.h" #include "foundation/result.h"
#include "paint/blend.h" #include "paint/blend.h"
#include "paint_renderer/compositor.h"
#include "paint/stroke.h" #include "paint/stroke.h"
#include "paint/stroke_script.h" #include "paint/stroke_script.h"
#include "renderer_api/recording_renderer.h" #include "renderer_api/recording_renderer.h"
@@ -316,6 +317,52 @@ struct PlanBrushOperationArgs {
bool has_brush = true; bool has_brush = true;
}; };
struct PlanBrushTextureListArgs {
std::string kind = "add";
std::string directory_name = "brushes";
std::string data_path = "data";
std::string source_path = "source.png";
int item_count = 1;
int current_index = 0;
int offset = 1;
bool current_is_user_texture = false;
};
struct PlanBrushStrokeControlArgs {
std::string kind = "float";
std::string setting = "tip-size";
float value = 1.0F;
bool enabled = true;
int blend_mode = 0;
};
struct PlanPaintFeedbackArgs {
int width = 64;
int height = 32;
bool complex_blend = true;
bool framebuffer_fetch = false;
bool explicit_texture_transitions = false;
bool texture_copy = false;
bool render_target_blit = false;
bool render_only_target = false;
bool depth_target = false;
};
struct PlanStrokeCompositeArgs {
int width = 64;
int height = 32;
int layer_blend_mode = 0;
int stroke_blend_mode = 0;
bool dual_brush_blend = false;
bool pattern_blend = false;
bool framebuffer_fetch = false;
bool explicit_texture_transitions = false;
bool texture_copy = false;
bool render_target_blit = false;
bool render_only_target = false;
bool depth_target = false;
};
struct PlanGridOperationArgs { struct PlanGridOperationArgs {
std::string kind = "pick"; std::string kind = "pick";
std::string path; std::string path;
@@ -1087,6 +1134,125 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
return "stroke-settings-changed"; return "stroke-settings-changed";
} }
const char* brush_texture_list_operation_name(pp::app::BrushTextureListOperation operation) noexcept
{
switch (operation) {
case pp::app::BrushTextureListOperation::add_texture:
return "add-texture";
case pp::app::BrushTextureListOperation::remove_texture:
return "remove-texture";
case pp::app::BrushTextureListOperation::move_texture:
return "move-texture";
}
return "add-texture";
}
const char* 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 const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) noexcept
{ {
switch (operation) { switch (operation) {
@@ -1613,6 +1779,10 @@ void print_help()
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|select|goto|next|prev|playback|toggle-playback|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--layer-index N] [--layer-id N] [--current-duration N] [--delta N] [--offset N] [--onion-size N] [--playing]\n"
<< " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n" << " plan-animation-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n"
<< " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n"
<< " plan-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n"
<< " plan-brush-stroke-control --kind float|bool|blend|tip-aspect-reset|default-reset [--setting NAME] [--value N] [--enabled|--disabled] [--blend-mode N]\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-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n"
<< " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n"
<< " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n"
@@ -4283,6 +4453,634 @@ int plan_brush_operation(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_brush_texture_list_args(
int argc,
char** argv,
PlanBrushTextureListArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind" || key == "--dir" || key == "--data-path" || key == "--source") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
if (key == "--kind") {
args.kind = argv[++i];
} else if (key == "--dir") {
args.directory_name = argv[++i];
} else if (key == "--data-path") {
args.data_path = argv[++i];
} else {
args.source_path = argv[++i];
}
} else if (key == "--item-count" || key == "--current-index" || key == "--offset") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
if (key == "--item-count") {
args.item_count = value.value();
} else if (key == "--current-index") {
args.current_index = value.value();
} else {
args.offset = value.value();
}
} else if (key == "--user-texture") {
args.current_is_user_texture = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::BrushTextureListPlan> make_brush_texture_list_plan(
const PlanBrushTextureListArgs& args)
{
if (args.kind == "add") {
return pp::app::plan_brush_texture_list_add(args.directory_name, args.data_path, args.source_path);
}
if (args.kind == "remove") {
return pp::app::plan_brush_texture_list_remove(
args.item_count,
args.current_index,
args.current_is_user_texture);
}
if (args.kind == "move" || args.kind == "up" || args.kind == "down") {
const int offset = args.kind == "up" ? -1 : (args.kind == "down" ? 1 : args.offset);
return pp::app::plan_brush_texture_list_move(args.item_count, args.current_index, offset);
}
return pp::foundation::Result<pp::app::BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("unknown brush texture list operation kind"));
}
int plan_brush_texture_list(int argc, char** argv)
{
PlanBrushTextureListArgs args;
const auto status = parse_plan_brush_texture_list_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-brush-texture-list", status.message);
return 2;
}
const auto plan = make_brush_texture_list_plan(args);
if (!plan) {
print_error("plan-brush-texture-list", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-brush-texture-list\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"dir\":\"" << json_escape(args.directory_name)
<< "\",\"dataPath\":\"" << json_escape(args.data_path)
<< "\",\"source\":\"" << json_escape(args.source_path)
<< "\",\"itemCount\":" << args.item_count
<< ",\"currentIndex\":" << args.current_index
<< ",\"offset\":" << args.offset
<< ",\"currentIsUserTexture\":" << json_bool(args.current_is_user_texture)
<< "},\"plan\":{\"operation\":\"" << brush_texture_list_operation_name(value.operation)
<< "\",\"itemCount\":" << value.item_count
<< ",\"currentIndex\":" << value.current_index
<< ",\"targetIndex\":" << value.target_index
<< ",\"moveOffset\":" << value.move_offset
<< ",\"source\":\"" << json_escape(value.source_path)
<< "\",\"path\":\"" << json_escape(value.high_path)
<< "\",\"thumb\":\"" << json_escape(value.thumbnail_path)
<< "\",\"brushName\":\"" << json_escape(value.brush_name)
<< "\",\"userTexture\":" << json_bool(value.user_texture)
<< ",\"deletesTextureFiles\":" << json_bool(value.deletes_texture_files)
<< ",\"savesList\":" << json_bool(value.saves_list)
<< ",\"notifiesSelection\":" << json_bool(value.notifies_selection)
<< ",\"convertsBrushAlpha\":" << json_bool(value.converts_brush_alpha)
<< ",\"noOp\":" << json_bool(value.no_op)
<< "}}\n";
return 0;
}
pp::foundation::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_paint_feedback_args(
int argc,
char** argv,
PlanPaintFeedbackArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--width" || key == "--height") {
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 (value.value() <= 0) {
return pp::foundation::Status::invalid_argument("paint feedback extent must be greater than zero");
}
if (key == "--width") {
args.width = value.value();
} else {
args.height = value.value();
}
} else if (key == "--simple") {
args.complex_blend = false;
} else if (key == "--complex") {
args.complex_blend = true;
} else if (key == "--framebuffer-fetch") {
args.framebuffer_fetch = true;
} else if (key == "--explicit-transitions") {
args.explicit_texture_transitions = true;
} else if (key == "--texture-copy") {
args.texture_copy = true;
} else if (key == "--blit") {
args.render_target_blit = true;
} else if (key == "--render-only") {
args.render_only_target = true;
} else if (key == "--depth") {
args.depth_target = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_paint_feedback(int argc, char** argv)
{
PlanPaintFeedbackArgs args;
const auto status = parse_plan_paint_feedback_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-paint-feedback", status.message);
return 2;
}
pp::renderer::TextureUsage usage = pp::renderer::TextureUsage::render_target;
if (!args.render_only_target) {
usage |= pp::renderer::TextureUsage::sampled;
usage |= pp::renderer::TextureUsage::copy_source;
usage |= pp::renderer::TextureUsage::copy_destination;
}
const pp::renderer::RenderDeviceFeatures features {
.framebuffer_fetch = args.framebuffer_fetch,
.explicit_texture_transitions = args.explicit_texture_transitions,
.texture_copy = args.texture_copy,
.render_target_blit = args.render_target_blit,
};
const pp::renderer::TextureDesc target {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(args.width),
.height = static_cast<std::uint32_t>(args.height),
},
.format = args.depth_target
? pp::renderer::TextureFormat::depth24_stencil8
: pp::renderer::TextureFormat::rgba8,
.usage = usage,
.debug_name = "paint-feedback-target",
};
const auto plan = pp::renderer::plan_paint_feedback(features, target, args.complex_blend);
if (!plan) {
print_error("plan-paint-feedback", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-paint-feedback\""
<< ",\"state\":{\"width\":" << args.width
<< ",\"height\":" << args.height
<< ",\"complexBlend\":" << json_bool(args.complex_blend)
<< ",\"framebufferFetch\":" << json_bool(args.framebuffer_fetch)
<< ",\"explicitTransitions\":" << json_bool(args.explicit_texture_transitions)
<< ",\"textureCopy\":" << json_bool(args.texture_copy)
<< ",\"blit\":" << json_bool(args.render_target_blit)
<< ",\"renderOnlyTarget\":" << json_bool(args.render_only_target)
<< ",\"depthTarget\":" << json_bool(args.depth_target)
<< "},\"plan\":{\"path\":\"" << pp::renderer::paint_feedback_path_name(value.path)
<< "\",\"targetFormat\":\"" << pp::renderer::texture_format_name(value.target_desc.format)
<< "\",\"targetBytes\":" << value.target_bytes
<< ",\"complexBlend\":" << json_bool(value.complex_blend)
<< ",\"readsDestinationColor\":" << json_bool(value.reads_destination_color)
<< ",\"requiresAuxiliaryTexture\":" << json_bool(value.requires_auxiliary_texture)
<< ",\"requiresTextureCopy\":" << json_bool(value.requires_texture_copy)
<< ",\"requiresRenderTargetBlit\":" << json_bool(value.requires_render_target_blit)
<< ",\"requiresExplicitTransition\":" << json_bool(value.requires_explicit_transition)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_stroke_composite_args(
int argc,
char** argv,
PlanStrokeCompositeArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--width" || key == "--height" || key == "--layer-blend" || key == "--stroke-blend") {
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 == "--width" || key == "--height") {
if (value.value() <= 0) {
return pp::foundation::Status::invalid_argument("stroke composite extent must be greater than zero");
}
if (key == "--width") {
args.width = value.value();
} else {
args.height = value.value();
}
} else if (key == "--layer-blend") {
args.layer_blend_mode = value.value();
} else {
args.stroke_blend_mode = value.value();
}
} else if (key == "--dual-blend") {
args.dual_brush_blend = true;
} else if (key == "--pattern-blend") {
args.pattern_blend = true;
} else if (key == "--framebuffer-fetch") {
args.framebuffer_fetch = true;
} else if (key == "--explicit-transitions") {
args.explicit_texture_transitions = true;
} else if (key == "--texture-copy") {
args.texture_copy = true;
} else if (key == "--blit") {
args.render_target_blit = true;
} else if (key == "--render-only") {
args.render_only_target = true;
} else if (key == "--depth") {
args.depth_target = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.layer_blend_mode < 0 || args.layer_blend_mode > 4) {
return pp::foundation::Status::out_of_range("layer blend mode must be in the range [0, 4]");
}
if (args.stroke_blend_mode < 0 || args.stroke_blend_mode > 10) {
return pp::foundation::Status::out_of_range("stroke blend mode must be in the range [0, 10]");
}
return pp::foundation::Status::success();
}
int plan_stroke_composite(int argc, char** argv)
{
PlanStrokeCompositeArgs args;
const auto status = parse_plan_stroke_composite_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-stroke-composite", status.message);
return 2;
}
pp::renderer::TextureUsage usage = pp::renderer::TextureUsage::render_target;
if (!args.render_only_target) {
usage |= pp::renderer::TextureUsage::sampled;
usage |= pp::renderer::TextureUsage::copy_source;
usage |= pp::renderer::TextureUsage::copy_destination;
}
const pp::paint_renderer::StrokeCompositeRequest request {
.extent = pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(args.width),
.height = static_cast<std::uint32_t>(args.height),
},
.target_format = args.depth_target
? pp::renderer::TextureFormat::depth24_stencil8
: pp::renderer::TextureFormat::rgba8,
.target_usage = usage,
.layer_blend_mode = static_cast<pp::paint::BlendMode>(args.layer_blend_mode),
.stroke_blend_mode = static_cast<pp::paint::StrokeBlendMode>(args.stroke_blend_mode),
.dual_brush_blend = args.dual_brush_blend,
.pattern_blend = args.pattern_blend,
};
const pp::renderer::RenderDeviceFeatures features {
.framebuffer_fetch = args.framebuffer_fetch,
.explicit_texture_transitions = args.explicit_texture_transitions,
.texture_copy = args.texture_copy,
.render_target_blit = args.render_target_blit,
};
const auto plan = pp::paint_renderer::plan_stroke_composite(features, request);
if (!plan) {
print_error("plan-stroke-composite", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-stroke-composite\""
<< ",\"state\":{\"width\":" << args.width
<< ",\"height\":" << args.height
<< ",\"layerBlend\":\"" << pp::paint::blend_mode_name(request.layer_blend_mode)
<< "\",\"strokeBlend\":\"" << pp::paint::stroke_blend_mode_name(request.stroke_blend_mode)
<< "\",\"dualBrushBlend\":" << json_bool(args.dual_brush_blend)
<< ",\"patternBlend\":" << json_bool(args.pattern_blend)
<< ",\"framebufferFetch\":" << json_bool(args.framebuffer_fetch)
<< ",\"explicitTransitions\":" << json_bool(args.explicit_texture_transitions)
<< ",\"textureCopy\":" << json_bool(args.texture_copy)
<< ",\"blit\":" << json_bool(args.render_target_blit)
<< ",\"renderOnlyTarget\":" << json_bool(args.render_only_target)
<< ",\"depthTarget\":" << json_bool(args.depth_target)
<< "},\"plan\":{\"path\":\"" << pp::paint_renderer::stroke_composite_path_name(value.path)
<< "\",\"feedbackPath\":\"" << pp::renderer::paint_feedback_path_name(value.feedback.path)
<< "\",\"targetBytes\":" << value.target_bytes
<< ",\"auxiliaryBytes\":" << value.auxiliary_bytes
<< ",\"estimatedWorkingBytes\":" << value.estimated_working_bytes
<< ",\"complexBlend\":" << json_bool(value.complex_blend)
<< ",\"readsDestinationColor\":" << json_bool(value.reads_destination_color)
<< ",\"requiresAuxiliaryTexture\":" << json_bool(value.requires_auxiliary_texture)
<< ",\"requiresTextureCopy\":" << json_bool(value.requires_texture_copy)
<< ",\"requiresRenderTargetBlit\":" << json_bool(value.requires_render_target_blit)
<< ",\"requiresExplicitTransition\":" << json_bool(value.requires_explicit_transition)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_canvas_tool_args( pp::foundation::Status parse_plan_canvas_tool_args(
int argc, int argc,
char** argv, char** argv,
@@ -7405,6 +8203,22 @@ int main(int argc, char** argv)
return plan_brush_operation(argc, argv); return plan_brush_operation(argc, argv);
} }
if (command == "plan-brush-texture-list") {
return plan_brush_texture_list(argc, argv);
}
if (command == "plan-brush-stroke-control") {
return plan_brush_stroke_control(argc, argv);
}
if (command == "plan-paint-feedback") {
return plan_paint_feedback(argc, argv);
}
if (command == "plan-stroke-composite") {
return plan_stroke_composite(argc, argv);
}
if (command == "plan-canvas-tool") { if (command == "plan-canvas-tool") {
return plan_canvas_tool(argc, argv); return plan_canvas_tool(argc, argv);
} }