Compare commits
9 Commits
cee5f141a3
...
2ac2c45b11
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac2c45b11 | |||
| b576143afb | |||
| bc5b39057d | |||
| 1369a9048e | |||
| a89f5e6cf2 | |||
| 2ec11e5099 | |||
| 94a6877e7c | |||
| dc23a5648d | |||
| 9adfad9609 |
@@ -355,6 +355,7 @@ if(PP_BUILD_APP)
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
@@ -392,6 +393,7 @@ if(PP_BUILD_APP)
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-02
|
||||
Last updated: 2026-06-03
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
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 |
|
||||
| --- | --- | --- | --- |
|
||||
| Brush settings serialization | `Brush`, `Serializer` | `pp_paint`, `pp_assets` | Round-trip and boundary values |
|
||||
| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests |
|
||||
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
|
||||
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
|
||||
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and 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 |
|
||||
| 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, 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 |
|
||||
|
||||
## Layers And Animation
|
||||
|
||||
@@ -40,7 +40,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, but the live adapter still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing |
|
||||
| DEBT-0021 | Open | Modernization | Layer rename and layer panel operation planning now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, and `pano_cli plan-layer-operation`, but live execution still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries directly | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
|
||||
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, and `DocumentAnimationServices`, and `pp_legacy_ui_core` temporarily links `pp_app_core`, but the live adapter still mutates or reads legacy `Canvas`/`Layer` frame state and canvas mode directly | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
|
||||
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning 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-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 |
|
||||
@@ -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-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-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
|
||||
|
||||
|
||||
@@ -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
|
||||
callbacks. Brush UI execution now dispatches through `BrushUiServices` before
|
||||
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,
|
||||
camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar
|
||||
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
|
||||
and shader-plus-mesh before draw within each render pass, and rejects invalid
|
||||
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
|
||||
behind the renderer interfaces.
|
||||
|
||||
@@ -1098,8 +1139,15 @@ Results:
|
||||
render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/
|
||||
upload/texture-copy/readback/frame-capture/blit command capture, draw
|
||||
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.
|
||||
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_layout_value_tests` passed.
|
||||
- `pp_ui_core_layout_xml_tests` passed.
|
||||
@@ -1226,15 +1274,29 @@ Results:
|
||||
live animation-panel planning as JSON automation.
|
||||
- `pp_app_core_brush_ui_tests` passed, covering brush color channel validation,
|
||||
invalid color rejection, texture-path validation, preset-brush availability,
|
||||
preserve-current-color intent, stroke-settings refresh intent, service
|
||||
dispatch ordering, texture/preset execution payloads, and invalid execution
|
||||
payload rejection.
|
||||
preserve-current-color intent, stroke-settings refresh intent, texture-list
|
||||
add target path planning, user-texture removal intent, clamped reorder intent,
|
||||
stroke-control slider/toggle/blend/reset planning, service dispatch ordering,
|
||||
texture/preset/list/stroke-control execution payloads, execution failure
|
||||
preservation, and invalid execution payload rejection.
|
||||
- `pano_cli_plan_brush_operation_color_smoke`,
|
||||
`pano_cli_plan_brush_operation_texture_smoke`,
|
||||
`pano_cli_plan_brush_operation_preset_smoke`,
|
||||
`pano_cli_plan_brush_operation_rejects_bad_color`, and
|
||||
`pano_cli_plan_brush_operation_rejects_empty_texture` passed and expose live
|
||||
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
|
||||
planning, lightmap capability and limit checks, missing-heightmap no-op
|
||||
behavior, and commit canvas gating.
|
||||
@@ -1552,6 +1614,25 @@ Results:
|
||||
- Canvas layer merge rendering and explicit layer-merge compositing now route
|
||||
depth/blend state, active texture units, fallback 2D texture unbinds, and
|
||||
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
|
||||
depth/blend state and active texture units through the renderer GL backend
|
||||
mapping.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -21,6 +22,89 @@ enum class BrushUiOperation {
|
||||
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 {
|
||||
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
|
||||
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
|
||||
@@ -37,6 +121,38 @@ struct BrushUiPlan {
|
||||
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 {
|
||||
public:
|
||||
virtual ~BrushUiServices() = default;
|
||||
@@ -47,6 +163,55 @@ public:
|
||||
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
|
||||
{
|
||||
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
|
||||
@@ -56,6 +221,24 @@ public:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
|
||||
{
|
||||
if (blend_mode < 0 || blend_mode > 63) {
|
||||
return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
|
||||
float r,
|
||||
float g,
|
||||
@@ -129,6 +312,168 @@ public:
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
|
||||
BrushStrokeFloatSetting setting,
|
||||
float value)
|
||||
{
|
||||
const auto status = validate_brush_stroke_float(value);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
||||
}
|
||||
|
||||
BrushStrokeControlPlan plan;
|
||||
plan.operation = BrushStrokeControlOperation::set_float;
|
||||
plan.float_setting = setting;
|
||||
plan.float_value = value;
|
||||
plan.mutates_brush = true;
|
||||
plan.refreshes_preview = true;
|
||||
plan.notifies_stroke_change = true;
|
||||
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
|
||||
BrushStrokeBoolSetting setting,
|
||||
bool value) noexcept
|
||||
{
|
||||
BrushStrokeControlPlan plan;
|
||||
plan.operation = BrushStrokeControlOperation::set_bool;
|
||||
plan.bool_setting = setting;
|
||||
plan.bool_value = value;
|
||||
plan.mutates_brush = true;
|
||||
plan.refreshes_preview = true;
|
||||
plan.notifies_stroke_change = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
|
||||
BrushStrokeBlendSetting setting,
|
||||
int blend_mode)
|
||||
{
|
||||
const auto status = validate_brush_stroke_blend_mode(blend_mode);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
|
||||
}
|
||||
|
||||
BrushStrokeControlPlan plan;
|
||||
plan.operation = BrushStrokeControlOperation::set_blend_mode;
|
||||
plan.blend_setting = setting;
|
||||
plan.blend_mode = blend_mode;
|
||||
plan.mutates_brush = true;
|
||||
plan.refreshes_preview = true;
|
||||
plan.notifies_stroke_change = true;
|
||||
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
|
||||
{
|
||||
BrushStrokeControlPlan plan;
|
||||
plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
|
||||
plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
|
||||
plan.float_value = value;
|
||||
plan.mutates_brush = true;
|
||||
plan.refreshes_preview = true;
|
||||
plan.notifies_stroke_change = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
|
||||
{
|
||||
BrushStrokeControlPlan plan;
|
||||
plan.operation = BrushStrokeControlOperation::reset_default_brush;
|
||||
plan.mutates_brush = true;
|
||||
plan.updates_controls = true;
|
||||
plan.refreshes_preview = true;
|
||||
plan.notifies_stroke_change = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
|
||||
std::string_view directory_name,
|
||||
std::string_view data_path,
|
||||
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(
|
||||
const BrushUiPlan& plan,
|
||||
BrushUiServices& services)
|
||||
@@ -168,4 +513,116 @@ public:
|
||||
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
|
||||
const BrushStrokeControlPlan& plan,
|
||||
BrushStrokeControlServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case BrushStrokeControlOperation::set_float:
|
||||
{
|
||||
const auto status = validate_brush_stroke_float(plan.float_value);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
services.set_float_setting(plan.float_setting, plan.float_value);
|
||||
break;
|
||||
}
|
||||
|
||||
case BrushStrokeControlOperation::set_bool:
|
||||
services.set_bool_setting(plan.bool_setting, plan.bool_value);
|
||||
break;
|
||||
|
||||
case BrushStrokeControlOperation::set_blend_mode:
|
||||
{
|
||||
const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
services.set_blend_mode(plan.blend_setting, plan.blend_mode);
|
||||
break;
|
||||
}
|
||||
|
||||
case BrushStrokeControlOperation::reset_tip_aspect:
|
||||
{
|
||||
const auto status = validate_brush_stroke_float(plan.float_value);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
services.reset_tip_aspect(plan.float_value);
|
||||
break;
|
||||
}
|
||||
|
||||
case BrushStrokeControlOperation::reset_default_brush:
|
||||
services.reset_default_brush();
|
||||
break;
|
||||
}
|
||||
|
||||
if (plan.updates_controls) {
|
||||
services.update_stroke_controls();
|
||||
}
|
||||
if (plan.refreshes_preview) {
|
||||
services.refresh_stroke_preview();
|
||||
}
|
||||
if (plan.notifies_stroke_change) {
|
||||
services.notify_stroke_changed();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
|
||||
const BrushTextureListPlan& plan,
|
||||
BrushTextureListServices& services)
|
||||
{
|
||||
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
|
||||
|
||||
@@ -16,6 +16,17 @@ namespace {
|
||||
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()
|
||||
@@ -55,6 +66,7 @@ void App::initShaders()
|
||||
ShaderManager::ext_float32 = capabilities.float32_textures;
|
||||
ShaderManager::ext_float32_linear = capabilities.float32_linear;
|
||||
ShaderManager::ext_float16 = capabilities.float16_textures;
|
||||
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
|
||||
});
|
||||
|
||||
#if __GL__
|
||||
@@ -63,6 +75,8 @@ void App::initShaders()
|
||||
ShaderManager::ext_float32 = true;
|
||||
ShaderManager::ext_float16 = true;
|
||||
#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");
|
||||
|
||||
|
||||
111
src/canvas.cpp
111
src/canvas.cpp
@@ -4,6 +4,7 @@
|
||||
#include "app.h"
|
||||
#include "texture.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "paint_renderer/compositor.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
@@ -43,6 +44,69 @@ GLenum 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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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::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));
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_stroke_destination)
|
||||
{
|
||||
glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.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();
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_stroke_destination)
|
||||
{
|
||||
set_active_texture_unit(1);
|
||||
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_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());
|
||||
ShaderManager::use(kShader::Stroke);
|
||||
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::TexPattern, 2); // pattern
|
||||
ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
|
||||
@@ -648,7 +718,7 @@ void Canvas::stroke_draw()
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, f.col);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
|
||||
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();
|
||||
|
||||
@@ -675,7 +745,7 @@ void Canvas::stroke_draw()
|
||||
// work on documents that doesn't have the padding, so on document loading.
|
||||
ShaderManager::use(kShader::StrokePad);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_stroke_destination)
|
||||
{
|
||||
set_active_texture_unit(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_tmp[i].bindFramebuffer();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_stroke_destination)
|
||||
{
|
||||
glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
|
||||
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_tmp[i].unbindFramebuffer();
|
||||
}
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_stroke_destination)
|
||||
{
|
||||
unbind_texture_2d();
|
||||
}
|
||||
@@ -747,7 +817,7 @@ void Canvas::stroke_draw()
|
||||
if (P.size() < 3)
|
||||
continue;
|
||||
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();
|
||||
|
||||
// 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);
|
||||
const auto& b = m_current_stroke->m_brush;
|
||||
|
||||
// check if any layer use blend, otherwise draw directly on main framebuffer
|
||||
bool use_blend = false;
|
||||
for (auto& l : m_layers)
|
||||
{
|
||||
use_blend |= l->m_blend_mode != 0;
|
||||
}
|
||||
if (Canvas::I->m_current_stroke)
|
||||
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
|
||||
const auto blend_gate = draw_merge_blend_gate_plan(
|
||||
m_width,
|
||||
m_height,
|
||||
m_layers,
|
||||
m_current_stroke ? m_current_stroke->m_brush.get() : nullptr);
|
||||
const bool use_blend = blend_gate.shader_blend;
|
||||
const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
|
||||
|
||||
// if not using shader blend, use gl rasterizer blend
|
||||
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_float(kShaderUniform::Alpha, 1.f);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_blend_destination)
|
||||
{
|
||||
m_sampler.bind(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);
|
||||
m_merge_rtt.bindTexture();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_blend_destination)
|
||||
{
|
||||
set_active_texture_unit(2);
|
||||
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();
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_blend_destination)
|
||||
{
|
||||
set_active_texture_unit(2);
|
||||
m_merge_tex.unbind();
|
||||
|
||||
@@ -205,7 +205,7 @@ public:
|
||||
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;
|
||||
// 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;
|
||||
void stroke_draw();
|
||||
void stroke_end();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "app_core/canvas_tool_ui.h"
|
||||
#include "app_core/history_ui.h"
|
||||
@@ -8,6 +11,7 @@
|
||||
#include "log.h"
|
||||
#include "node_canvas.h"
|
||||
#include "node_image_texture.h"
|
||||
#include "paint_renderer/compositor.h"
|
||||
#include "settings.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
@@ -23,6 +27,48 @@ void unbind_texture_2d()
|
||||
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()
|
||||
{
|
||||
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
|
||||
@@ -252,14 +298,13 @@ void NodeCanvas::draw()
|
||||
}
|
||||
else
|
||||
{
|
||||
// check if any layer use blend, otherwise draw directly on main framebuffer
|
||||
bool use_blend = false;
|
||||
for (size_t i = 0; i < m_canvas->m_layers.size(); i++)
|
||||
{
|
||||
use_blend |= m_canvas->m_layers[i]->m_blend_mode != 0;
|
||||
}
|
||||
if (Canvas::I->m_current_stroke)
|
||||
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
|
||||
const auto blend_gate = node_canvas_blend_gate_plan(
|
||||
m_cache_rtt.getWidth(),
|
||||
m_cache_rtt.getHeight(),
|
||||
m_canvas->m_layers,
|
||||
m_canvas->m_current_stroke ? m_canvas->m_current_stroke->m_brush.get() : nullptr);
|
||||
const bool use_blend = blend_gate.shader_blend;
|
||||
const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;
|
||||
|
||||
if (use_blend)
|
||||
{
|
||||
@@ -450,7 +495,7 @@ void NodeCanvas::draw()
|
||||
|
||||
ShaderManager::use(kShader::TextureBlend);
|
||||
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::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
|
||||
@@ -458,7 +503,7 @@ void NodeCanvas::draw()
|
||||
|
||||
set_active_texture_unit(0);
|
||||
m_blender_rtt.bindTexture();
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_blend_destination)
|
||||
{
|
||||
set_active_texture_unit(2);
|
||||
m_blender_bg.bind();
|
||||
@@ -468,7 +513,7 @@ void NodeCanvas::draw()
|
||||
|
||||
m_face_plane.draw_fill();
|
||||
|
||||
if (!ShaderManager::ext_framebuffer_fetch)
|
||||
if (copy_blend_destination)
|
||||
{
|
||||
set_active_texture_unit(2);
|
||||
m_blender_bg.unbind();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "node_panel_brush.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "asset.h"
|
||||
#include "texture.h"
|
||||
|
||||
@@ -75,6 +76,116 @@ Node* NodePanelBrush::clone_instantiate() const
|
||||
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()
|
||||
{
|
||||
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->on_click = [this](Node*) {
|
||||
App::I->pick_file({ "JPG", "PNG" }, [this](std::string path) {
|
||||
std::string name, base, ext;
|
||||
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
|
||||
std::smatch m;
|
||||
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();
|
||||
const auto plan = pp::app::plan_brush_texture_list_add(m_dir_name, App::I->data_path, path);
|
||||
if (plan) {
|
||||
execute_texture_list_plan(plan.value());
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -126,26 +205,13 @@ void NodePanelBrush::init()
|
||||
if (m_current)
|
||||
{
|
||||
int idx = m_container->get_child_index(m_current);
|
||||
if (m_current->m_user_brush)
|
||||
{
|
||||
// only delete user brushes
|
||||
Asset::delete_file(m_current->thumb_path);
|
||||
Asset::delete_file(m_current->high_path);
|
||||
const auto plan = pp::app::plan_brush_texture_list_remove(
|
||||
static_cast<int>(m_container->m_children.size()),
|
||||
idx,
|
||||
m_current->m_user_brush);
|
||||
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)
|
||||
{
|
||||
int idx = m_container->get_child_index(m_current);
|
||||
idx = std::max(0, std::min(idx - 1, (int)m_container->m_children.size() - 1));
|
||||
m_container->move_child(m_current, idx);
|
||||
save();
|
||||
const auto plan = pp::app::plan_brush_texture_list_move(
|
||||
static_cast<int>(m_container->m_children.size()),
|
||||
idx,
|
||||
-1);
|
||||
if (plan) {
|
||||
execute_texture_list_plan(plan.value());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,9 +235,13 @@ void NodePanelBrush::init()
|
||||
if (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));
|
||||
m_container->move_child(m_current, idx);
|
||||
save();
|
||||
const auto plan = pp::app::plan_brush_texture_list_move(
|
||||
static_cast<int>(m_container->m_children.size()),
|
||||
idx,
|
||||
1);
|
||||
if (plan) {
|
||||
execute_texture_list_plan(plan.value());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "serializer.h"
|
||||
#include "node_button.h"
|
||||
|
||||
namespace pp::app {
|
||||
struct BrushTextureListPlan;
|
||||
}
|
||||
|
||||
class NodeButtonBrush : public NodeButtonCustom, public Serializer::Type
|
||||
{
|
||||
public:
|
||||
@@ -38,6 +42,7 @@ class NodePanelBrush : public Node
|
||||
NodeButtonCustom* m_btn_down;
|
||||
NodeButtonCustom* m_btn_remove;
|
||||
bool m_interacted = false;
|
||||
void execute_texture_list_plan(const pp::app::BrushTextureListPlan& plan);
|
||||
public:
|
||||
NodeScroll* m_container;
|
||||
std::string m_dir_name;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "log.h"
|
||||
#include "node_panel_stroke.h"
|
||||
#include "canvas.h"
|
||||
@@ -6,6 +7,137 @@
|
||||
#include "app.h"
|
||||
#include "abr.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class LegacyBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
|
||||
public:
|
||||
explicit LegacyBrushStrokeControlServices(NodePanelStroke& panel) : panel_(panel) {}
|
||||
|
||||
void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeFloatSetting::tip_size: brush.m_tip_size = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_spacing: brush.m_tip_spacing = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_flow: brush.m_tip_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_opacity: brush.m_tip_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_angle: brush.m_tip_angle = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_angle_smooth: brush.m_tip_angle_smooth = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_mix: brush.m_tip_mix = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_wet: brush.m_tip_wet = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_noise: brush.m_tip_noise = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_hue: brush.m_tip_hue = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_saturation: brush.m_tip_sat = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_value: brush.m_tip_val = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_scale: brush.m_jitter_scale = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_angle: brush.m_jitter_angle = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_scatter: brush.m_jitter_scatter = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_flow: brush.m_jitter_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_opacity: brush.m_jitter_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_hue: brush.m_jitter_hue = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_saturation: brush.m_jitter_sat = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_value: brush.m_jitter_val = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::jitter_aspect: brush.m_jitter_aspect = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_size: brush.m_dual_size = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_spacing: brush.m_dual_spacing = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_scatter: brush.m_dual_scatter = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::tip_aspect: brush.m_tip_aspect = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_opacity: brush.m_dual_opacity = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_flow: brush.m_dual_flow = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::dual_rotate: brush.m_dual_rotate = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_scale: brush.m_pattern_scale = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_brightness: brush.m_pattern_brightness = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_contrast: brush.m_pattern_contrast = value; break;
|
||||
case pp::app::BrushStrokeFloatSetting::pattern_depth: brush.m_pattern_depth = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeBoolSetting::tip_angle_init: brush.m_tip_angle_init = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_angle_follow: brush.m_tip_angle_follow = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flow_pressure: brush.m_tip_flow_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_opacity_pressure: brush.m_tip_opacity_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_size_pressure: brush.m_tip_size_pressure = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis: brush.m_jitter_scatter_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis: brush.m_jitter_aspect_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample: brush.m_jitter_hsv_eachsample = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_invert: brush.m_tip_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flip_x: brush.m_tip_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_flip_y: brush.m_tip_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_enabled: brush.m_pattern_enabled = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_enabled: brush.m_dual_enabled = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis: brush.m_dual_scatter_bothaxis = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_invert: brush.m_dual_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_flip_x: brush.m_dual_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_flip_y: brush.m_dual_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::dual_random_flip: brush.m_dual_randflip = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_random_flip_x: brush.m_tip_randflipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::tip_random_flip_y: brush.m_tip_randflipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_each_sample: brush.m_pattern_eachsample = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_invert: brush.m_pattern_invert = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_flip_x: brush.m_pattern_flipx = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_flip_y: brush.m_pattern_flipy = value; break;
|
||||
case pp::app::BrushStrokeBoolSetting::pattern_random_offset: brush.m_pattern_rand_offset = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
|
||||
{
|
||||
auto& brush = *Canvas::I->m_current_brush;
|
||||
switch (setting) {
|
||||
case pp::app::BrushStrokeBlendSetting::tip: brush.m_blend_mode = blend_mode; break;
|
||||
case pp::app::BrushStrokeBlendSetting::dual: brush.m_dual_blend_mode = blend_mode; break;
|
||||
case pp::app::BrushStrokeBlendSetting::pattern: brush.m_pattern_blend_mode = blend_mode; break;
|
||||
}
|
||||
}
|
||||
|
||||
void reset_tip_aspect(float value) override
|
||||
{
|
||||
panel_.m_tip_aspect->set_value(value);
|
||||
Canvas::I->m_current_brush->m_tip_aspect = value;
|
||||
}
|
||||
|
||||
void reset_default_brush() override
|
||||
{
|
||||
auto brush = std::make_shared<Brush>();
|
||||
brush->load_tip(
|
||||
panel_.m_brush_popup->get_texture_path(panel_.m_default_brush_index),
|
||||
panel_.m_brush_popup->get_thumb_path(panel_.m_default_brush_index));
|
||||
brush->m_tip_size = 30;
|
||||
brush->m_tip_flow = .9f;
|
||||
brush->m_tip_spacing = .1f;
|
||||
brush->m_tip_opacity = 1.f;
|
||||
Canvas::I->m_current_brush = brush;
|
||||
}
|
||||
|
||||
void update_stroke_controls() override
|
||||
{
|
||||
panel_.update_controls();
|
||||
}
|
||||
|
||||
void refresh_stroke_preview() override
|
||||
{
|
||||
if (panel_.m_preview) {
|
||||
panel_.m_preview->m_brush = Canvas::I->m_current_brush;
|
||||
}
|
||||
}
|
||||
|
||||
void notify_stroke_changed() override
|
||||
{
|
||||
if (panel_.on_stroke_change) {
|
||||
panel_.on_stroke_change(&panel_);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
NodePanelStroke& panel_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Node* NodePanelStroke::clone_instantiate() const
|
||||
{
|
||||
return new NodePanelStroke();
|
||||
@@ -156,7 +288,8 @@ void NodePanelStroke::init_controls()
|
||||
//m_presets_popup->m_flood_events = true;
|
||||
//m_presets_popup->m_capture_children = false;
|
||||
|
||||
int br_idx = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
|
||||
m_default_brush_index = std::max(m_brush_popup->find_brush("Round-Hard"), 0);
|
||||
const int br_idx = m_default_brush_index;
|
||||
|
||||
// init main brush
|
||||
auto b = std::make_shared<Brush>();
|
||||
@@ -331,73 +464,73 @@ void NodePanelStroke::init_controls()
|
||||
|
||||
m_blend_mode = find<NodeComboBox>("blend-mode");
|
||||
m_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
init_slider(m_tip_size, "tip-size", &Brush::m_tip_size);
|
||||
init_slider(m_tip_spacing, "tip-spacing", &Brush::m_tip_spacing);
|
||||
init_slider(m_tip_flow, "tip-flow", &Brush::m_tip_flow);
|
||||
init_slider(m_tip_opacity, "tip-opacity", &Brush::m_tip_opacity);
|
||||
init_slider(m_tip_angle, "tip-angle", &Brush::m_tip_angle);
|
||||
init_slider(m_tip_angle_smooth, "tip-angle-smooth", &Brush::m_tip_angle_smooth);
|
||||
init_slider(m_tip_mix, "tip-mix", &Brush::m_tip_mix);
|
||||
init_slider(m_tip_wet, "tip-wet", &Brush::m_tip_wet);
|
||||
init_slider(m_tip_noise, "tip-noise", &Brush::m_tip_noise);
|
||||
init_slider(m_tip_hue, "tip-hue", &Brush::m_tip_hue);
|
||||
init_slider(m_tip_sat, "tip-sat", &Brush::m_tip_sat);
|
||||
init_slider(m_tip_val, "tip-val", &Brush::m_tip_val);
|
||||
init_slider(m_jitter_scale, "jitter-scale", &Brush::m_jitter_scale);
|
||||
init_slider(m_jitter_angle, "jitter-angle", &Brush::m_jitter_angle);
|
||||
init_slider(m_jitter_scatter, "jitter-scatter", &Brush::m_jitter_scatter);
|
||||
init_slider(m_jitter_flow, "jitter-flow", &Brush::m_jitter_flow);
|
||||
init_slider(m_jitter_opacity, "jitter-opacity", &Brush::m_jitter_opacity);
|
||||
init_slider(m_jitter_hue, "jitter-hue", &Brush::m_jitter_hue);
|
||||
init_slider(m_jitter_sat, "jitter-sat", &Brush::m_jitter_sat);
|
||||
init_slider(m_jitter_val, "jitter-val", &Brush::m_jitter_val);
|
||||
init_slider(m_jitter_aspect, "jitter-aspect", &Brush::m_jitter_aspect);
|
||||
init_slider(m_tip_size, "tip-size", pp::app::BrushStrokeFloatSetting::tip_size, &Brush::m_tip_size);
|
||||
init_slider(m_tip_spacing, "tip-spacing", pp::app::BrushStrokeFloatSetting::tip_spacing, &Brush::m_tip_spacing);
|
||||
init_slider(m_tip_flow, "tip-flow", pp::app::BrushStrokeFloatSetting::tip_flow, &Brush::m_tip_flow);
|
||||
init_slider(m_tip_opacity, "tip-opacity", pp::app::BrushStrokeFloatSetting::tip_opacity, &Brush::m_tip_opacity);
|
||||
init_slider(m_tip_angle, "tip-angle", pp::app::BrushStrokeFloatSetting::tip_angle, &Brush::m_tip_angle);
|
||||
init_slider(m_tip_angle_smooth, "tip-angle-smooth", pp::app::BrushStrokeFloatSetting::tip_angle_smooth, &Brush::m_tip_angle_smooth);
|
||||
init_slider(m_tip_mix, "tip-mix", pp::app::BrushStrokeFloatSetting::tip_mix, &Brush::m_tip_mix);
|
||||
init_slider(m_tip_wet, "tip-wet", pp::app::BrushStrokeFloatSetting::tip_wet, &Brush::m_tip_wet);
|
||||
init_slider(m_tip_noise, "tip-noise", pp::app::BrushStrokeFloatSetting::tip_noise, &Brush::m_tip_noise);
|
||||
init_slider(m_tip_hue, "tip-hue", pp::app::BrushStrokeFloatSetting::tip_hue, &Brush::m_tip_hue);
|
||||
init_slider(m_tip_sat, "tip-sat", pp::app::BrushStrokeFloatSetting::tip_saturation, &Brush::m_tip_sat);
|
||||
init_slider(m_tip_val, "tip-val", pp::app::BrushStrokeFloatSetting::tip_value, &Brush::m_tip_val);
|
||||
init_slider(m_jitter_scale, "jitter-scale", pp::app::BrushStrokeFloatSetting::jitter_scale, &Brush::m_jitter_scale);
|
||||
init_slider(m_jitter_angle, "jitter-angle", pp::app::BrushStrokeFloatSetting::jitter_angle, &Brush::m_jitter_angle);
|
||||
init_slider(m_jitter_scatter, "jitter-scatter", pp::app::BrushStrokeFloatSetting::jitter_scatter, &Brush::m_jitter_scatter);
|
||||
init_slider(m_jitter_flow, "jitter-flow", pp::app::BrushStrokeFloatSetting::jitter_flow, &Brush::m_jitter_flow);
|
||||
init_slider(m_jitter_opacity, "jitter-opacity", pp::app::BrushStrokeFloatSetting::jitter_opacity, &Brush::m_jitter_opacity);
|
||||
init_slider(m_jitter_hue, "jitter-hue", pp::app::BrushStrokeFloatSetting::jitter_hue, &Brush::m_jitter_hue);
|
||||
init_slider(m_jitter_sat, "jitter-sat", pp::app::BrushStrokeFloatSetting::jitter_saturation, &Brush::m_jitter_sat);
|
||||
init_slider(m_jitter_val, "jitter-val", pp::app::BrushStrokeFloatSetting::jitter_value, &Brush::m_jitter_val);
|
||||
init_slider(m_jitter_aspect, "jitter-aspect", pp::app::BrushStrokeFloatSetting::jitter_aspect, &Brush::m_jitter_aspect);
|
||||
|
||||
init_checkbox(m_tip_angle_init, "tip-angle-init", &Brush::m_tip_angle_init);
|
||||
init_checkbox(m_tip_angle_follow, "tip-angle-follow", &Brush::m_tip_angle_follow);
|
||||
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", &Brush::m_tip_flow_pressure);
|
||||
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", &Brush::m_tip_opacity_pressure);
|
||||
init_checkbox(m_tip_size_pressure, "tip-size-pressure", &Brush::m_tip_size_pressure);
|
||||
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", &Brush::m_jitter_scatter_bothaxis);
|
||||
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", &Brush::m_jitter_aspect_bothaxis);
|
||||
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", &Brush::m_jitter_hsv_eachsample);
|
||||
init_checkbox(m_tip_angle_init, "tip-angle-init", pp::app::BrushStrokeBoolSetting::tip_angle_init, &Brush::m_tip_angle_init);
|
||||
init_checkbox(m_tip_angle_follow, "tip-angle-follow", pp::app::BrushStrokeBoolSetting::tip_angle_follow, &Brush::m_tip_angle_follow);
|
||||
init_checkbox(m_tip_flow_pressure, "tip-flow-pressure", pp::app::BrushStrokeBoolSetting::tip_flow_pressure, &Brush::m_tip_flow_pressure);
|
||||
init_checkbox(m_tip_opacity_pressure, "tip-opacity-pressure", pp::app::BrushStrokeBoolSetting::tip_opacity_pressure, &Brush::m_tip_opacity_pressure);
|
||||
init_checkbox(m_tip_size_pressure, "tip-size-pressure", pp::app::BrushStrokeBoolSetting::tip_size_pressure, &Brush::m_tip_size_pressure);
|
||||
init_checkbox(m_jitter_scatter_bothaxis, "jitter-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_scatter_both_axis, &Brush::m_jitter_scatter_bothaxis);
|
||||
init_checkbox(m_jitter_aspect_bothaxis, "jitter-aspect-bothaxis", pp::app::BrushStrokeBoolSetting::jitter_aspect_both_axis, &Brush::m_jitter_aspect_bothaxis);
|
||||
init_checkbox(m_jitter_hsv_eachsample, "jitter-hsv-eachsample", pp::app::BrushStrokeBoolSetting::jitter_hsv_each_sample, &Brush::m_jitter_hsv_eachsample);
|
||||
|
||||
init_checkbox(m_tip_invert, "tip-invert", &Brush::m_tip_invert);
|
||||
init_checkbox(m_tip_flipx, "tip-flipx", &Brush::m_tip_flipx);
|
||||
init_checkbox(m_tip_flipy, "tip-flipy", &Brush::m_tip_flipy);
|
||||
init_checkbox(m_pattern_enabled, "pattern-enabled", &Brush::m_pattern_enabled);
|
||||
init_checkbox(m_dual_enabled, "dual-enabled", &Brush::m_dual_enabled);
|
||||
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", &Brush::m_dual_scatter_bothaxis);
|
||||
init_checkbox(m_dual_invert, "dual-invert", &Brush::m_dual_invert);
|
||||
init_checkbox(m_dual_flipx, "dual-flipx", &Brush::m_dual_flipx);
|
||||
init_checkbox(m_dual_flipy, "dual-flipy", &Brush::m_dual_flipy);
|
||||
init_checkbox(m_dual_randflip, "dual-randflip", &Brush::m_dual_randflip);
|
||||
init_checkbox(m_tip_randflipx, "tip-randflipx", &Brush::m_tip_randflipx);
|
||||
init_checkbox(m_tip_randflipy, "tip-randflipy", &Brush::m_tip_randflipy);
|
||||
init_checkbox(m_pattern_eachsample, "pattern-eachsample", &Brush::m_pattern_eachsample);
|
||||
init_checkbox(m_tip_invert, "tip-invert", pp::app::BrushStrokeBoolSetting::tip_invert, &Brush::m_tip_invert);
|
||||
init_checkbox(m_tip_flipx, "tip-flipx", pp::app::BrushStrokeBoolSetting::tip_flip_x, &Brush::m_tip_flipx);
|
||||
init_checkbox(m_tip_flipy, "tip-flipy", pp::app::BrushStrokeBoolSetting::tip_flip_y, &Brush::m_tip_flipy);
|
||||
init_checkbox(m_pattern_enabled, "pattern-enabled", pp::app::BrushStrokeBoolSetting::pattern_enabled, &Brush::m_pattern_enabled);
|
||||
init_checkbox(m_dual_enabled, "dual-enabled", pp::app::BrushStrokeBoolSetting::dual_enabled, &Brush::m_dual_enabled);
|
||||
init_checkbox(m_dual_scatter_bothaxis, "dual-scatter-bothaxis", pp::app::BrushStrokeBoolSetting::dual_scatter_both_axis, &Brush::m_dual_scatter_bothaxis);
|
||||
init_checkbox(m_dual_invert, "dual-invert", pp::app::BrushStrokeBoolSetting::dual_invert, &Brush::m_dual_invert);
|
||||
init_checkbox(m_dual_flipx, "dual-flipx", pp::app::BrushStrokeBoolSetting::dual_flip_x, &Brush::m_dual_flipx);
|
||||
init_checkbox(m_dual_flipy, "dual-flipy", pp::app::BrushStrokeBoolSetting::dual_flip_y, &Brush::m_dual_flipy);
|
||||
init_checkbox(m_dual_randflip, "dual-randflip", pp::app::BrushStrokeBoolSetting::dual_random_flip, &Brush::m_dual_randflip);
|
||||
init_checkbox(m_tip_randflipx, "tip-randflipx", pp::app::BrushStrokeBoolSetting::tip_random_flip_x, &Brush::m_tip_randflipx);
|
||||
init_checkbox(m_tip_randflipy, "tip-randflipy", pp::app::BrushStrokeBoolSetting::tip_random_flip_y, &Brush::m_tip_randflipy);
|
||||
init_checkbox(m_pattern_eachsample, "pattern-eachsample", pp::app::BrushStrokeBoolSetting::pattern_each_sample, &Brush::m_pattern_eachsample);
|
||||
|
||||
init_checkbox(m_pattern_invert, "pattern-invert", &Brush::m_pattern_invert);
|
||||
init_checkbox(m_pattern_flipx, "pattern-flipx", &Brush::m_pattern_flipx);
|
||||
init_checkbox(m_pattern_flipy, "pattern-flipy", &Brush::m_pattern_flipy);
|
||||
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", &Brush::m_pattern_rand_offset);
|
||||
init_checkbox(m_pattern_invert, "pattern-invert", pp::app::BrushStrokeBoolSetting::pattern_invert, &Brush::m_pattern_invert);
|
||||
init_checkbox(m_pattern_flipx, "pattern-flipx", pp::app::BrushStrokeBoolSetting::pattern_flip_x, &Brush::m_pattern_flipx);
|
||||
init_checkbox(m_pattern_flipy, "pattern-flipy", pp::app::BrushStrokeBoolSetting::pattern_flip_y, &Brush::m_pattern_flipy);
|
||||
init_checkbox(m_pattern_rand_offset, "pattern-rand-offset", pp::app::BrushStrokeBoolSetting::pattern_random_offset, &Brush::m_pattern_rand_offset);
|
||||
|
||||
init_slider(m_dual_size, "dual-size", &Brush::m_dual_size);
|
||||
init_slider(m_dual_spacing, "dual-spacing", &Brush::m_dual_spacing);
|
||||
init_slider(m_dual_scatter, "dual-scatter", &Brush::m_dual_scatter);
|
||||
init_slider(m_tip_aspect, "tip-aspect", &Brush::m_tip_aspect);
|
||||
init_slider(m_dual_opacity, "dual-opacity", &Brush::m_dual_opacity);
|
||||
init_slider(m_dual_flow, "dual-flow", &Brush::m_dual_flow);
|
||||
init_slider(m_dual_rotate, "dual-rotate", &Brush::m_dual_rotate);
|
||||
init_slider(m_pattern_scale, "pattern-scale", &Brush::m_pattern_scale);
|
||||
init_slider(m_pattern_brightness, "pattern-brightness", &Brush::m_pattern_brightness);
|
||||
init_slider(m_pattern_contrast, "pattern-contrast", &Brush::m_pattern_contrast);
|
||||
init_slider(m_pattern_depth, "pattern-depth", &Brush::m_pattern_depth);
|
||||
init_slider(m_dual_size, "dual-size", pp::app::BrushStrokeFloatSetting::dual_size, &Brush::m_dual_size);
|
||||
init_slider(m_dual_spacing, "dual-spacing", pp::app::BrushStrokeFloatSetting::dual_spacing, &Brush::m_dual_spacing);
|
||||
init_slider(m_dual_scatter, "dual-scatter", pp::app::BrushStrokeFloatSetting::dual_scatter, &Brush::m_dual_scatter);
|
||||
init_slider(m_tip_aspect, "tip-aspect", pp::app::BrushStrokeFloatSetting::tip_aspect, &Brush::m_tip_aspect);
|
||||
init_slider(m_dual_opacity, "dual-opacity", pp::app::BrushStrokeFloatSetting::dual_opacity, &Brush::m_dual_opacity);
|
||||
init_slider(m_dual_flow, "dual-flow", pp::app::BrushStrokeFloatSetting::dual_flow, &Brush::m_dual_flow);
|
||||
init_slider(m_dual_rotate, "dual-rotate", pp::app::BrushStrokeFloatSetting::dual_rotate, &Brush::m_dual_rotate);
|
||||
init_slider(m_pattern_scale, "pattern-scale", pp::app::BrushStrokeFloatSetting::pattern_scale, &Brush::m_pattern_scale);
|
||||
init_slider(m_pattern_brightness, "pattern-brightness", pp::app::BrushStrokeFloatSetting::pattern_brightness, &Brush::m_pattern_brightness);
|
||||
init_slider(m_pattern_contrast, "pattern-contrast", pp::app::BrushStrokeFloatSetting::pattern_contrast, &Brush::m_pattern_contrast);
|
||||
init_slider(m_pattern_depth, "pattern-depth", pp::app::BrushStrokeFloatSetting::pattern_depth, &Brush::m_pattern_depth);
|
||||
|
||||
SliderCurve curve_cubic {
|
||||
[](float v) { return glm::pow(v, 3.f); },
|
||||
@@ -469,27 +602,23 @@ void NodePanelStroke::init_controls()
|
||||
|
||||
m_tip_aspect_reset = find<NodeButtonCustom>("tip-aspect-reset");
|
||||
m_tip_aspect_reset->on_click = [this](Node*) {
|
||||
m_tip_aspect->set_value(0.5);
|
||||
Canvas::I->m_current_brush->m_tip_aspect = 0.5f;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
execute_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset());
|
||||
};
|
||||
|
||||
m_dual_blend_mode = find<NodeComboBox>("dual-blend-mode");
|
||||
m_dual_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_dual_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::dual, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
m_pattern_blend_mode = find<NodeComboBox>("pattern-blend-mode");
|
||||
m_pattern_blend_mode->on_select = [this](Node*, int index) {
|
||||
Canvas::I->m_current_brush->m_pattern_blend_mode = index;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, index);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
}
|
||||
};
|
||||
|
||||
m_preview->m_brush = Canvas::I->m_current_brush;
|
||||
@@ -518,54 +647,68 @@ void NodePanelStroke::init_controls()
|
||||
}
|
||||
|
||||
m_brush_settings_reset = find<NodeButton>("brush-settings-reset");
|
||||
m_brush_settings_reset->on_click = [br_idx,this](Node*) {
|
||||
auto b = std::make_shared<Brush>();
|
||||
b->load_tip(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
|
||||
//b->load_dual(m_brush_popup->get_texture_path(br_idx), m_brush_popup->get_thumb_path(br_idx));
|
||||
//b->load_pattern(m_pattern_popup->get_texture_path(0), m_pattern_popup->get_thumb_path(0));
|
||||
b->m_tip_size = 30;
|
||||
b->m_tip_flow = .9f;
|
||||
b->m_tip_spacing = .1f;
|
||||
b->m_tip_opacity = 1.f;
|
||||
Canvas::I->m_current_brush = b;
|
||||
update_controls();
|
||||
App::I->brush_update(true, true);
|
||||
m_brush_settings_reset->on_click = [this](Node*) {
|
||||
execute_stroke_control_plan(pp::app::plan_brush_default_settings_reset());
|
||||
};
|
||||
|
||||
update_controls();
|
||||
}
|
||||
|
||||
void NodePanelStroke::init_slider(NodeSliderH*& target, const char* id, float Brush::* prop)
|
||||
void NodePanelStroke::init_slider(
|
||||
NodeSliderH*& target,
|
||||
const char* id,
|
||||
pp::app::BrushStrokeFloatSetting setting,
|
||||
float Brush::* prop)
|
||||
{
|
||||
target = find<NodeSliderH>(id);
|
||||
target->on_value_changed = std::bind(&NodePanelStroke::handle_slide,
|
||||
this, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
this, setting, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
//m_canvas->m_brush->*prop = target->m_values;
|
||||
}
|
||||
|
||||
void NodePanelStroke::handle_slide(float Brush::* prop, Node* target, float value)
|
||||
void NodePanelStroke::handle_slide(
|
||||
pp::app::BrushStrokeFloatSetting setting,
|
||||
float Brush::* prop,
|
||||
Node* target,
|
||||
float value)
|
||||
{
|
||||
auto curve = m_curves.find((NodeSliderH*)target);
|
||||
Canvas::I->m_current_brush.get()->*prop = curve != m_curves.end() ? curve->second.to_value(value) : value;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto brush_value = curve != m_curves.end() ? curve->second.to_value(value) : value;
|
||||
const auto plan = pp::app::plan_brush_stroke_float_setting(setting, brush_value);
|
||||
if (plan) {
|
||||
execute_stroke_control_plan(plan.value());
|
||||
} else {
|
||||
Canvas::I->m_current_brush.get()->*prop = brush_value;
|
||||
}
|
||||
}
|
||||
|
||||
void NodePanelStroke::init_checkbox(NodeCheckBox*& target, const char* id, bool Brush::* prop)
|
||||
void NodePanelStroke::init_checkbox(
|
||||
NodeCheckBox*& target,
|
||||
const char* id,
|
||||
pp::app::BrushStrokeBoolSetting setting,
|
||||
bool Brush::* prop)
|
||||
{
|
||||
target = find<NodeCheckBox>(id);
|
||||
target->on_value_changed = std::bind(&NodePanelStroke::handle_checkbox,
|
||||
this, prop, std::placeholders::_1, std::placeholders::_2);
|
||||
this, setting, std::placeholders::_2);
|
||||
Canvas::I->m_current_brush.get()->*prop = target->checked;
|
||||
}
|
||||
|
||||
void NodePanelStroke::handle_checkbox(bool Brush::* prop, Node *target, bool value)
|
||||
void NodePanelStroke::handle_checkbox(
|
||||
pp::app::BrushStrokeBoolSetting setting,
|
||||
bool value)
|
||||
{
|
||||
Canvas::I->m_current_brush.get()->*prop = value;
|
||||
//m_preview->draw_stroke();
|
||||
if (on_stroke_change)
|
||||
on_stroke_change(this);
|
||||
const auto plan = pp::app::plan_brush_stroke_bool_setting(setting, value);
|
||||
execute_stroke_control_plan(plan);
|
||||
}
|
||||
|
||||
void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan)
|
||||
{
|
||||
LegacyBrushStrokeControlServices services(*this);
|
||||
const auto status = pp::app::execute_brush_stroke_control_plan(plan, services);
|
||||
if (!status.ok()) {
|
||||
LOG("Brush stroke control action failed: %s", status.message);
|
||||
}
|
||||
}
|
||||
|
||||
kEventResult NodePanelStroke::handle_event(Event* e)
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
#include "node_image.h"
|
||||
#include "node_panel_brush.h"
|
||||
|
||||
namespace pp::app {
|
||||
struct BrushStrokeControlPlan;
|
||||
enum class BrushStrokeBoolSetting;
|
||||
enum class BrushStrokeFloatSetting;
|
||||
enum class BrushStrokeBlendSetting;
|
||||
} // namespace pp::app
|
||||
|
||||
class NodePanelStroke : public Node
|
||||
{
|
||||
public:
|
||||
@@ -103,6 +110,7 @@ public:
|
||||
inline float to_slider(float v) { return m_inv(v); }
|
||||
};
|
||||
std::map<NodeSliderH*, SliderCurve> m_curves;
|
||||
int m_default_brush_index = 0;
|
||||
|
||||
virtual Node* clone_instantiate() const override;
|
||||
virtual void clone_finalize(Node* dest) const override;
|
||||
@@ -116,9 +124,10 @@ public:
|
||||
void set_size(float value, bool normalized, bool propagate);
|
||||
|
||||
void init_fold(const std::string& name);
|
||||
void init_slider(NodeSliderH*& slider, const char* id, float Brush::* prop);
|
||||
void handle_slide(float Brush::* prop, Node* target, float value);
|
||||
void init_slider(NodeSliderH*& slider, const char* id, pp::app::BrushStrokeFloatSetting setting, float Brush::* prop);
|
||||
void handle_slide(pp::app::BrushStrokeFloatSetting setting, float Brush::* prop, Node* target, float value);
|
||||
|
||||
void init_checkbox(NodeCheckBox*& slider, const char* id, bool Brush::* prop);
|
||||
void handle_checkbox(bool Brush::* prop, Node* target, bool value);
|
||||
void init_checkbox(NodeCheckBox*& slider, const char* id, pp::app::BrushStrokeBoolSetting setting, bool Brush::* prop);
|
||||
void handle_checkbox(pp::app::BrushStrokeBoolSetting setting, bool value);
|
||||
void execute_stroke_control_plan(const pp::app::BrushStrokeControlPlan& plan);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,70 @@ namespace pp::paint_renderer {
|
||||
|
||||
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
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
[[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(
|
||||
@@ -62,4 +176,193 @@ pp::foundation::Status composite_layer(
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,10 +4,17 @@
|
||||
#include "paint/blend.h"
|
||||
#include "renderer_api/renderer_api.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace pp::paint_renderer {
|
||||
|
||||
enum class StrokeCompositePath : std::uint8_t {
|
||||
fixed_function_blend,
|
||||
framebuffer_fetch,
|
||||
ping_pong_textures,
|
||||
};
|
||||
|
||||
struct LayerCompositeView {
|
||||
std::span<const pp::paint::Rgba> pixels;
|
||||
float opacity = 1.0F;
|
||||
@@ -15,9 +22,90 @@ struct LayerCompositeView {
|
||||
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(
|
||||
std::span<pp::paint::Rgba> destination,
|
||||
pp::renderer::Extent2D extent,
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@@ -823,6 +823,65 @@ pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc desti
|
||||
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
|
||||
{
|
||||
switch (format) {
|
||||
@@ -887,6 +946,20 @@ const char* blit_filter_name(BlitFilter filter) noexcept
|
||||
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
|
||||
{
|
||||
switch (factor) {
|
||||
|
||||
@@ -132,6 +132,12 @@ enum class BlitFilter : std::uint8_t {
|
||||
linear,
|
||||
};
|
||||
|
||||
enum class PaintFeedbackPath : std::uint8_t {
|
||||
none,
|
||||
framebuffer_fetch,
|
||||
ping_pong_textures,
|
||||
};
|
||||
|
||||
enum class BlendFactor : std::uint8_t {
|
||||
zero,
|
||||
one,
|
||||
@@ -236,6 +242,19 @@ struct RenderDeviceFeatures {
|
||||
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 {
|
||||
public:
|
||||
virtual ~ITexture2D() = default;
|
||||
@@ -393,10 +412,15 @@ public:
|
||||
[[nodiscard]] pp::foundation::Status validate_blit_descs(
|
||||
TextureDesc source,
|
||||
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_state_name(TextureState state) noexcept;
|
||||
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) 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_op_name(BlendOp op) noexcept;
|
||||
[[nodiscard]] const char* compare_op_name(CompareOp op) noexcept;
|
||||
|
||||
@@ -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;
|
||||
Shader* ShaderManager::m_current;
|
||||
pp::renderer::RenderDeviceFeatures ShaderManager::m_render_device_features {};
|
||||
bool ShaderManager::ext_framebuffer_fetch = false;
|
||||
bool ShaderManager::ext_float32 = 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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
m_shaders.clear();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "renderer_api/renderer_api.h"
|
||||
#include "util.h"
|
||||
|
||||
bool check_uniform_uniqueness();
|
||||
@@ -108,6 +109,7 @@ class ShaderManager
|
||||
{
|
||||
static std::map<kShader, Shader> m_shaders;
|
||||
static Shader* m_current;
|
||||
static pp::renderer::RenderDeviceFeatures m_render_device_features;
|
||||
public:
|
||||
static bool ext_framebuffer_fetch;
|
||||
static bool ext_float32;
|
||||
@@ -127,5 +129,7 @@ public:
|
||||
static void u_int(kShaderUniform id, int i);
|
||||
static void u_int(const char* name, int i);
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -1132,6 +1132,126 @@ if(TARGET pano_cli)
|
||||
LABELS "app;paint;integration;desktop-fast;fuzz"
|
||||
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
|
||||
COMMAND pano_cli plan-canvas-tool --kind draw)
|
||||
set_tests_properties(pano_cli_plan_canvas_tool_draw_smoke PROPERTIES
|
||||
|
||||
@@ -65,6 +65,150 @@ public:
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
FakeBrushUiServices services;
|
||||
@@ -197,6 +469,105 @@ void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness)
|
||||
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)
|
||||
{
|
||||
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.texture_sets == 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
|
||||
@@ -229,9 +614,14 @@ int main()
|
||||
harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot);
|
||||
harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush);
|
||||
harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
|
||||
harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points);
|
||||
harness.run("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input);
|
||||
harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges);
|
||||
harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh);
|
||||
harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset);
|
||||
harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
|
||||
harness.run("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure);
|
||||
harness.run("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads);
|
||||
harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
@@ -2,14 +2,27 @@
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::paint::BlendMode;
|
||||
using pp::paint::Rgba;
|
||||
using pp::paint::StrokeBlendMode;
|
||||
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::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::RenderDeviceFeatures;
|
||||
using pp::renderer::TextureFormat;
|
||||
using pp::renderer::TextureUsage;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -97,6 +110,337 @@ void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
|
||||
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()
|
||||
@@ -105,5 +449,12 @@ int main()
|
||||
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("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();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ using pp::renderer::IRenderTarget;
|
||||
using pp::renderer::IRenderTrace;
|
||||
using pp::renderer::IShaderProgram;
|
||||
using pp::renderer::MeshDesc;
|
||||
using pp::renderer::PaintFeedbackPath;
|
||||
using pp::renderer::PrimitiveTopology;
|
||||
using pp::renderer::ReadbackRegion;
|
||||
using pp::renderer::RecordedRenderCommandKind;
|
||||
@@ -66,6 +67,8 @@ using pp::renderer::max_texture_dimension;
|
||||
using pp::renderer::max_texture_slots;
|
||||
using pp::renderer::max_trace_label_bytes;
|
||||
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::readback_byte_size;
|
||||
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"));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
const TextureDesc rgba_desc {
|
||||
@@ -2696,6 +2787,7 @@ int main()
|
||||
harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes);
|
||||
harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes);
|
||||
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_blend_contract", validates_blend_contract);
|
||||
harness.run("validates_depth_contract", validates_depth_contract);
|
||||
|
||||
@@ -7,6 +7,7 @@ target_link_libraries(pano_cli PRIVATE
|
||||
pp_foundation
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint_renderer
|
||||
pp_renderer_api
|
||||
pp_ui_core)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "foundation/parse.h"
|
||||
#include "foundation/result.h"
|
||||
#include "paint/blend.h"
|
||||
#include "paint_renderer/compositor.h"
|
||||
#include "paint/stroke.h"
|
||||
#include "paint/stroke_script.h"
|
||||
#include "renderer_api/recording_renderer.h"
|
||||
@@ -316,6 +317,52 @@ struct PlanBrushOperationArgs {
|
||||
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 {
|
||||
std::string kind = "pick";
|
||||
std::string path;
|
||||
@@ -1087,6 +1134,125 @@ const char* brush_ui_operation_name(pp::app::BrushUiOperation operation) noexcep
|
||||
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
|
||||
{
|
||||
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-panel-action --action goto|next|prev|playback|toggle-playback [--total-duration N] [--current-frame N] [--target-frame N] [--playing]\n"
|
||||
<< " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n"
|
||||
<< " plan-brush-texture-list --kind add|remove|move [--dir NAME] [--data-path DIR] [--source FILE] [--item-count N] [--current-index N] [--offset N] [--user-texture]\n"
|
||||
<< " plan-brush-stroke-control --kind float|bool|blend|tip-aspect-reset|default-reset [--setting NAME] [--value N] [--enabled|--disabled] [--blend-mode N]\n"
|
||||
<< " plan-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-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"
|
||||
@@ -4283,6 +4453,634 @@ int plan_brush_operation(int argc, char** argv)
|
||||
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(
|
||||
int argc,
|
||||
char** argv,
|
||||
@@ -7405,6 +8203,22 @@ int main(int argc, char** 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") {
|
||||
return plan_canvas_tool(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user