Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/raster_tool.rs

759 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Unified raster tool interface.
//!
//! Every raster tool operates on three GPU textures of identical dimensions:
//!
//! | Buffer | Access | Purpose |
//! |--------|--------|---------|
//! | **A** | Read-only | Source pixels, uploaded from layer/float at mousedown. |
//! | **B** | Write-only | Output / display. Compositor shows B while the tool is active. |
//! | **C** | Read+Write | Scratch. Dabs accumulate here across the stroke; composite A+C→B each frame. |
//!
//! All three are `Rgba8Unorm` with the same pixel dimensions. The framework
//! allocates and validates them in [`begin_raster_workspace`]; tools only
//! dispatch shaders.
use std::sync::Arc;
use uuid::Uuid;
use eframe::egui;
// ── WorkspaceSource ──────────────────────────────────────────────────────────
/// Describes whether the tool is operating on a raster layer or a floating selection.
#[derive(Clone, Debug)]
pub enum WorkspaceSource {
/// Operating on the full raster layer.
Layer {
layer_id: Uuid,
time: f64,
/// The keyframe's own UUID (the A-canvas key in `GpuBrushEngine`).
kf_id: Uuid,
/// Full canvas dimensions (may differ from workspace dims for floating selections).
canvas_w: u32,
canvas_h: u32,
},
/// Operating on the floating selection.
Float,
}
// ── RasterWorkspace ───────────────────────────────────────────────────────────
/// GPU buffer IDs and metadata for a single tool operation.
///
/// Created by [`begin_raster_workspace`] on mousedown. All three canvas UUIDs
/// index into `GpuBrushEngine::canvases` and are valid for the lifetime of the
/// active tool. They are queued for removal in `pending_canvas_removals` after
/// commit or cancel.
#[derive(Debug)]
pub struct RasterWorkspace {
/// A canvas (Rgba8Unorm) — source pixels, uploaded at mousedown, read-only for tools.
pub a_canvas_id: Uuid,
/// B canvas (Rgba8Unorm) — output / display; compositor shows this while active.
pub b_canvas_id: Uuid,
/// C canvas (Rgba8Unorm) — scratch; tools accumulate dabs here across the stroke.
pub c_canvas_id: Uuid,
/// Optional R8Unorm selection mask (same pixel dimensions as A/B/C).
/// `None` means the entire workspace is selected.
pub mask_texture: Option<Arc<wgpu::Texture>>,
/// Pixel dimensions. A, B, C, and mask are all guaranteed to be this size.
pub width: u32,
pub height: u32,
/// Top-left position in document-pixel space.
/// `(0, 0)` for a layer workspace; `(float.x, float.y)` for a float workspace.
pub x: i32,
pub y: i32,
/// Where the workspace came from — drives commit behaviour.
pub source: WorkspaceSource,
/// CPU snapshot taken at mousedown for undo / cancel.
/// Length is always `width * height * 4` (sRGB premultiplied RGBA).
pub before_pixels: Vec<u8>,
}
impl RasterWorkspace {
/// Panic-safe bounds check. Asserts that every GPU canvas exists and has
/// the dimensions declared by this workspace. Called by the framework
/// before `begin()` and before each `update()`.
pub fn validate(&self, gpu: &crate::gpu_brush::GpuBrushEngine) {
for (name, id) in [
("A", self.a_canvas_id),
("B", self.b_canvas_id),
("C", self.c_canvas_id),
] {
let canvas = gpu.canvases.get(&id).unwrap_or_else(|| {
panic!(
"RasterWorkspace::validate: buffer '{}' (id={}) not found in GpuBrushEngine",
name, id
)
});
assert_eq!(
canvas.width, self.width,
"RasterWorkspace::validate: buffer '{}' width {} != workspace width {}",
name, canvas.width, self.width
);
assert_eq!(
canvas.height, self.height,
"RasterWorkspace::validate: buffer '{}' height {} != workspace height {}",
name, canvas.height, self.height
);
}
let expected = (self.width * self.height * 4) as usize;
assert_eq!(
self.before_pixels.len(), expected,
"RasterWorkspace::validate: before_pixels.len()={} != expected {}",
self.before_pixels.len(), expected
);
}
/// Returns the three canvas UUIDs as an array (convenient for bulk removal).
pub fn canvas_ids(&self) -> [Uuid; 3] {
[self.a_canvas_id, self.b_canvas_id, self.c_canvas_id]
}
}
// ── WorkspaceInitPacket ───────────────────────────────────────────────────────
/// Data sent to `prepare()` on the first frame to create and upload the A/B/C canvases.
///
/// The canvas UUIDs are pre-allocated in `begin_raster_workspace()` (UI thread).
/// The actual `wgpu::Texture` creation and pixel upload happens in `prepare()`.
pub struct WorkspaceInitPacket {
/// A canvas UUID (already in `RasterWorkspace::a_canvas_id`).
pub a_canvas_id: Uuid,
/// Pixel data to upload to A. Length must equal `width * height * 4`.
pub a_pixels: Vec<u8>,
/// B canvas UUID.
pub b_canvas_id: Uuid,
/// C canvas UUID.
pub c_canvas_id: Uuid,
pub width: u32,
pub height: u32,
}
// ── ActiveToolRender ──────────────────────────────────────────────────────────
/// Passed to `VelloRenderContext` so the compositor can blit the tool's B output
/// in the correct position in the layer stack.
///
/// While an `ActiveToolRender` is set:
/// - If `layer_id == Some(id)`: blit B at that layer's compositor slot.
/// - If `layer_id == None`: blit B at the float's compositor slot.
#[derive(Clone, Debug)]
pub struct ActiveToolRender {
/// B canvas to blit.
pub b_canvas_id: Uuid,
/// Position of the B canvas in document space.
pub x: i32,
pub y: i32,
/// Pixel dimensions of the B canvas.
pub width: u32,
pub height: u32,
/// `Some(layer_id)` → B replaces this layer's render slot.
/// `None` → B replaces the float render slot.
pub layer_id: Option<Uuid>,
}
// ── PendingGpuWork ────────────────────────────────────────────────────────────
/// GPU work to execute in `VelloCallback::prepare()`.
///
/// Tools compute dab lists and other CPU-side data in `update()` (UI thread),
/// store them as a `Box<dyn PendingGpuWork>`, and return that work through
/// `RasterTool::take_pending_gpu_work()` each frame. `prepare()` then calls
/// `execute()` with the render-thread `device`/`queue`/`gpu`.
///
/// `execute()` takes `&self` so the work object need not be consumed; it lives
/// in the `VelloRenderContext` (which is immutable in `prepare()`).
pub trait PendingGpuWork: Send + Sync {
fn execute(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
gpu: &mut crate::gpu_brush::GpuBrushEngine,
);
}
// ── RasterTool trait ──────────────────────────────────────────────────────────
/// Unified interface for all raster tools.
///
/// All methods run on the UI thread. They update the tool's internal state
/// and store pending GPU op descriptors in fields that `StagePane` forwards
/// to `VelloRenderContext` for execution by `VelloCallback::prepare()`.
pub trait RasterTool: Send + Sync {
/// Called on **mousedown** after [`begin_raster_workspace`] has allocated and
/// validated A, B, and C. The tool should initialise its internal state and
/// optionally queue an initial GPU dispatch (e.g. identity composite for
/// transform so the handle frame appears immediately).
fn begin(
&mut self,
ws: &RasterWorkspace,
pos: egui::Vec2,
dt: f32,
settings: &crate::tools::RasterToolSettings,
);
/// Called every frame while the pointer is held (including the first drag frame).
/// The tool should accumulate new work into C and queue a composite A+C→B pass.
/// `dt` is the elapsed time in seconds since the previous call; used by time-based
/// brushes (airbrush, etc.) to fire dabs at the correct rate when stationary.
fn update(
&mut self,
ws: &RasterWorkspace,
pos: egui::Vec2,
dt: f32,
settings: &crate::tools::RasterToolSettings,
);
/// Called on **pointer release**. Returns `true` if a GPU readback of B should
/// be performed and the result committed to the document. Returns `false` if
/// the operation was a no-op (e.g. the pointer never moved).
fn finish(&mut self, ws: &RasterWorkspace) -> bool;
/// Called on **Escape** or tool switch mid-stroke. The caller restores the
/// source pixels from `ws.before_pixels` without creating an undo entry; the
/// tool just cleans up internal state.
fn cancel(&mut self, ws: &RasterWorkspace);
/// Called once per frame (in the VelloCallback construction, UI thread) to
/// extract pending GPU work accumulated by `begin()` / `update()`.
///
/// The tool clears its internal pending work and returns it. `prepare()` on
/// the render thread then calls `work.execute()`. Default: no GPU work.
fn take_pending_gpu_work(&mut self) -> Option<Box<dyn PendingGpuWork>> {
None
}
}
// ── BrushRasterTool ───────────────────────────────────────────────────────────
use lightningbeam_core::brush_engine::{BrushEngine, GpuDab, StrokeState};
use lightningbeam_core::brush_settings::BrushSettings;
use lightningbeam_core::raster_layer::{RasterBlendMode, StrokePoint, StrokeRecord};
/// GPU work for one frame of a brush stroke: dispatch dabs into C, then composite A+C→B.
struct PendingBrushWork {
dabs: Vec<GpuDab>,
bbox: (i32, i32, i32, i32),
a_id: Uuid,
b_id: Uuid,
c_id: Uuid,
canvas_w: u32,
canvas_h: u32,
}
impl PendingGpuWork for PendingBrushWork {
fn execute(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
gpu: &mut crate::gpu_brush::GpuBrushEngine,
) {
// 1. Accumulate this frame's dabs into C (if any).
if !self.dabs.is_empty() {
gpu.render_dabs(device, queue, self.c_id, &self.dabs, self.bbox, self.canvas_w, self.canvas_h);
}
// 2. Always composite A + C → B so B shows A's content even with no dabs this frame.
// On begin() with empty C this initialises B = A, avoiding a transparent flash.
gpu.composite_a_c_to_b(device, queue, self.a_id, self.c_id, self.b_id, self.canvas_w, self.canvas_h);
}
}
/// Raster tool for paint brushes (Normal blend mode).
///
/// Each `update()` call computes new dabs for that frame and stores them as
/// `PendingBrushWork`. `take_pending_gpu_work()` hands the work to `prepare()`
/// which dispatches the dab and composite shaders on the render thread.
pub struct BrushRasterTool {
color: [f32; 4],
brush: BrushSettings,
blend_mode: RasterBlendMode,
stroke_state: StrokeState,
last_point: Option<StrokePoint>,
pending: Option<Box<PendingBrushWork>>,
/// True after at least one non-empty frame (so finish() knows a commit is needed).
has_dabs: bool,
/// Offset to convert world coordinates to canvas-local coordinates.
canvas_offset_x: i32,
canvas_offset_y: i32,
}
impl BrushRasterTool {
/// Create a new brush tool.
///
/// `color` — linear premultiplied RGBA, matches the format expected by `GpuDab`.
pub fn new(
color: [f32; 4],
brush: BrushSettings,
blend_mode: RasterBlendMode,
) -> Self {
Self {
color,
brush,
blend_mode,
stroke_state: StrokeState::new(),
last_point: None,
pending: None,
has_dabs: false,
canvas_offset_x: 0,
canvas_offset_y: 0,
}
}
fn make_stroke_point(pos: egui::Vec2, off_x: i32, off_y: i32) -> StrokePoint {
StrokePoint {
x: pos.x - off_x as f32,
y: pos.y - off_y as f32,
pressure: 1.0,
tilt_x: 0.0,
tilt_y: 0.0,
timestamp: 0.0,
}
}
fn dispatch_dabs(
&mut self,
ws: &RasterWorkspace,
pt: StrokePoint,
dt: f32,
) {
// Use a 2-point segment when we have a previous point so the engine
// interpolates dabs along the path. First mousedown uses a single point.
let points = match self.last_point.take() {
Some(prev) => vec![prev, pt.clone()],
None => vec![pt.clone()],
};
let record = StrokeRecord {
brush_settings: self.brush.clone(),
color: self.color,
blend_mode: self.blend_mode,
tool_params: [0.0; 4],
points,
};
let (dabs, bbox) = BrushEngine::compute_dabs(&record, &mut self.stroke_state, dt);
if !dabs.is_empty() {
self.has_dabs = true;
self.pending = Some(Box::new(PendingBrushWork {
dabs,
bbox,
a_id: ws.a_canvas_id,
b_id: ws.b_canvas_id,
c_id: ws.c_canvas_id,
canvas_w: ws.width,
canvas_h: ws.height,
}));
}
self.last_point = Some(pt);
}
}
impl RasterTool for BrushRasterTool {
fn begin(&mut self, ws: &RasterWorkspace, pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.canvas_offset_x = ws.x;
self.canvas_offset_y = ws.y;
let pt = Self::make_stroke_point(pos, ws.x, ws.y);
self.dispatch_dabs(ws, pt, 0.0);
// Always ensure a composite is queued on begin() so B is initialised from A
// on the first frame even if no dabs fired (large spacing, etc.).
if self.pending.is_none() {
self.pending = Some(Box::new(PendingBrushWork {
dabs: vec![],
bbox: (0, 0, ws.width as i32, ws.height as i32),
a_id: ws.a_canvas_id,
b_id: ws.b_canvas_id,
c_id: ws.c_canvas_id,
canvas_w: ws.width,
canvas_h: ws.height,
}));
}
}
fn update(&mut self, ws: &RasterWorkspace, pos: egui::Vec2, dt: f32, _settings: &crate::tools::RasterToolSettings) {
let pt = Self::make_stroke_point(pos, ws.x, ws.y);
self.dispatch_dabs(ws, pt, dt);
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool {
self.has_dabs
}
fn cancel(&mut self, _ws: &RasterWorkspace) {
self.pending = None;
self.has_dabs = false;
}
fn take_pending_gpu_work(&mut self) -> Option<Box<dyn PendingGpuWork>> {
self.pending.take().map(|w| w as Box<dyn PendingGpuWork>)
}
}
// ── EffectBrushTool ───────────────────────────────────────────────────────────
/// Raster tool for effect brushes (Blur, Sharpen, Dodge, Burn, Sponge, Desaturate).
///
/// C accumulates a per-pixel influence weight (R channel, 0255).
/// The composite pass applies the effect to A, scaled by C.r, writing to B:
/// `B = lerp(A, effect(A), C.r)`
///
/// Using C as an influence map (rather than accumulating modified pixels) prevents
/// overlapping dabs from compounding the effect beyond the C.r cap (255).
///
/// # GPU implementation (TODO)
/// Requires a dedicated `effect_brush_composite.wgsl` shader that reads A and C,
/// applies the blend-mode-specific filter to A, and blends by C.r → B.
pub struct EffectBrushTool {
brush: BrushSettings,
blend_mode: RasterBlendMode,
has_dabs: bool,
}
impl EffectBrushTool {
pub fn new(brush: BrushSettings, blend_mode: RasterBlendMode) -> Self {
Self { brush, blend_mode, has_dabs: false }
}
}
impl RasterTool for EffectBrushTool {
fn begin(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.has_dabs = true; // placeholder
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dabs }
fn cancel(&mut self, _ws: &RasterWorkspace) { self.has_dabs = false; }
// GPU shaders not yet implemented; take_pending_gpu_work returns None (default).
}
// ── SmudgeTool ────────────────────────────────────────────────────────────────
/// Raster tool for the smudge brush.
///
/// `begin()`: copy A → C so C starts with the source pixels for color pickup.
/// `update()`: dispatch smudge dabs using `blend_mode=2` (reads C as source,
/// writes smear to C); then composite C over A → B.
/// Because the smudge shader reads from `canvas_src` (C.src) and writes to
/// `canvas_dst` (C.dst), existing dabs are preserved in the smear history.
///
/// # GPU implementation (TODO)
/// Requires an initial A → C copy in `begin()` (via GPU copy command).
/// The smudge dab dispatch then uses `render_dabs(c_id, smudge_dabs, ...)`.
/// The composite pass is `composite_a_c_to_b` (same as BrushRasterTool).
pub struct SmudgeTool {
brush: BrushSettings,
has_dabs: bool,
}
impl SmudgeTool {
pub fn new(brush: BrushSettings) -> Self {
Self { brush, has_dabs: false }
}
}
impl RasterTool for SmudgeTool {
fn begin(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.has_dabs = true; // placeholder
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dabs }
fn cancel(&mut self, _ws: &RasterWorkspace) { self.has_dabs = false; }
// GPU shaders not yet implemented; take_pending_gpu_work returns None (default).
}
// ── GradientRasterTool ────────────────────────────────────────────────────────
use crate::gpu_brush::GpuGradientStop;
use lightningbeam_core::gradient::{GradientExtend, GradientType, ShapeGradient};
fn gradient_stops_to_gpu(gradient: &ShapeGradient) -> Vec<GpuGradientStop> {
gradient.stops.iter().map(|s| {
GpuGradientStop::from_srgb_u8(s.position, s.color.r, s.color.g, s.color.b, s.color.a)
}).collect()
}
fn gradient_extend_to_u32(extend: GradientExtend) -> u32 {
match extend {
GradientExtend::Pad => 0,
GradientExtend::Reflect => 1,
GradientExtend::Repeat => 2,
}
}
fn gradient_kind_to_u32(kind: GradientType) -> u32 {
match kind {
GradientType::Linear => 0,
GradientType::Radial => 1,
}
}
struct PendingGradientWork {
a_id: Uuid,
b_id: Uuid,
stops: Vec<GpuGradientStop>,
start: (f32, f32),
end: (f32, f32),
opacity: f32,
extend_mode: u32,
kind: u32,
}
impl PendingGpuWork for PendingGradientWork {
fn execute(&self, device: &wgpu::Device, queue: &wgpu::Queue, gpu: &mut crate::gpu_brush::GpuBrushEngine) {
gpu.apply_gradient_fill(
device, queue,
&self.a_id, &self.b_id,
&self.stops,
self.start, self.end,
self.opacity, self.extend_mode, self.kind,
);
}
}
/// Raster tool for gradient fills.
///
/// `begin()` records the canvas-local start position.
/// `update()` recomputes gradient parameters from settings and queues a
/// `PendingGradientWork` that calls `apply_gradient_fill` in `prepare()`.
/// `finish()` returns whether any gradient was dispatched.
pub struct GradientRasterTool {
start_canvas: egui::Vec2,
end_canvas: egui::Vec2,
pending: Option<Box<PendingGradientWork>>,
has_dispatched: bool,
}
impl GradientRasterTool {
pub fn new() -> Self {
Self {
start_canvas: egui::Vec2::ZERO,
end_canvas: egui::Vec2::ZERO,
pending: None,
has_dispatched: false,
}
}
}
impl RasterTool for GradientRasterTool {
fn begin(&mut self, ws: &RasterWorkspace, pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
let canvas_pos = pos - egui::vec2(ws.x as f32, ws.y as f32);
self.start_canvas = canvas_pos;
self.end_canvas = canvas_pos;
}
fn update(&mut self, ws: &RasterWorkspace, pos: egui::Vec2, _dt: f32, settings: &crate::tools::RasterToolSettings) {
self.end_canvas = pos - egui::vec2(ws.x as f32, ws.y as f32);
let gradient = &settings.gradient;
self.pending = Some(Box::new(PendingGradientWork {
a_id: ws.a_canvas_id,
b_id: ws.b_canvas_id,
stops: gradient_stops_to_gpu(gradient),
start: (self.start_canvas.x, self.start_canvas.y),
end: (self.end_canvas.x, self.end_canvas.y),
opacity: settings.gradient_opacity,
extend_mode: gradient_extend_to_u32(gradient.extend),
kind: gradient_kind_to_u32(gradient.kind),
}));
self.has_dispatched = true;
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dispatched }
fn cancel(&mut self, _ws: &RasterWorkspace) {
self.pending = None;
self.has_dispatched = false;
}
fn take_pending_gpu_work(&mut self) -> Option<Box<dyn PendingGpuWork>> {
self.pending.take().map(|w| w as Box<dyn PendingGpuWork>)
}
}
// ── TransformRasterTool ───────────────────────────────────────────────────────
use crate::gpu_brush::RasterTransformGpuParams;
struct PendingTransformWork {
a_id: Uuid,
b_id: Uuid,
params: RasterTransformGpuParams,
}
impl PendingGpuWork for PendingTransformWork {
fn execute(&self, device: &wgpu::Device, queue: &wgpu::Queue, gpu: &mut crate::gpu_brush::GpuBrushEngine) {
gpu.render_transform(device, queue, &self.a_id, &self.b_id, self.params);
}
}
/// Raster tool for affine transforms (move, scale, rotate, shear).
///
/// `begin()` stores the initial canvas dimensions and queues an identity
/// transform so B is initialised on the first frame.
/// `update()` recomputes the inverse affine matrix from the current handle
/// positions and queues a new `PendingTransformWork`.
///
/// The inverse matrix maps output pixel coordinates back to source pixel
/// coordinates: `src = M_inv * dst + b`
/// where `M_inv = [[a00, a01], [a10, a11]]` and `b = [b0, b1]`.
///
/// # GPU implementation
/// Fully wired — uses `GpuBrushEngine::render_transform`. Handle interaction
/// logic (drag, rotate, scale) is handled by the tool's `update()` caller in
/// `stage.rs` which computes and passes in the `RasterTransformGpuParams`.
pub struct TransformRasterTool {
pending: Option<Box<PendingTransformWork>>,
has_dispatched: bool,
canvas_w: u32,
canvas_h: u32,
}
impl TransformRasterTool {
pub fn new() -> Self {
Self {
pending: None,
has_dispatched: false,
canvas_w: 0,
canvas_h: 0,
}
}
/// Queue a transform with the given inverse-affine matrix.
/// Called by the stage handler after computing handle positions.
pub fn set_transform(
&mut self,
ws: &RasterWorkspace,
params: RasterTransformGpuParams,
) {
self.pending = Some(Box::new(PendingTransformWork {
a_id: ws.a_canvas_id,
b_id: ws.b_canvas_id,
params,
}));
self.has_dispatched = true;
}
}
impl RasterTool for TransformRasterTool {
fn begin(&mut self, ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.canvas_w = ws.width;
self.canvas_h = ws.height;
// Queue identity transform so B shows the source immediately.
let identity = RasterTransformGpuParams {
a00: 1.0, a01: 0.0,
a10: 0.0, a11: 1.0,
b0: 0.0, b1: 0.0,
src_w: ws.width, src_h: ws.height,
dst_w: ws.width, dst_h: ws.height,
_pad0: 0, _pad1: 0,
};
self.set_transform(ws, identity);
}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
// Handle interaction and matrix updates are driven from stage.rs via set_transform().
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dispatched }
fn cancel(&mut self, _ws: &RasterWorkspace) {
self.pending = None;
self.has_dispatched = false;
}
fn take_pending_gpu_work(&mut self) -> Option<Box<dyn PendingGpuWork>> {
self.pending.take().map(|w| w as Box<dyn PendingGpuWork>)
}
}
// ── WarpRasterTool ────────────────────────────────────────────────────────────
/// Raster tool for warp / mesh deformation.
///
/// Uses a displacement buffer (managed by `GpuBrushEngine`) that maps each
/// output pixel to a source offset. The displacement grid is updated by
/// dragging control points; the warp shader reads anchor pixels + displacement
/// → B each frame.
///
/// # GPU implementation (TODO)
/// Requires: `create_displacement_buf`, `apply_warp` already exist in
/// `GpuBrushEngine`. Wire brush-drag interaction to update displacement
/// entries and call `apply_warp`.
pub struct WarpRasterTool {
has_dispatched: bool,
}
impl WarpRasterTool {
pub fn new() -> Self { Self { has_dispatched: false } }
}
impl RasterTool for WarpRasterTool {
fn begin(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.has_dispatched = true; // placeholder
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dispatched }
fn cancel(&mut self, _ws: &RasterWorkspace) { self.has_dispatched = false; }
// take_pending_gpu_work: default (None) — full GPU wiring is TODO.
}
// ── LiquifyRasterTool ─────────────────────────────────────────────────────────
/// Raster tool for liquify (per-pixel displacement painting).
///
/// Similar to `WarpRasterTool` but uses a full per-pixel displacement map
/// (grid_cols = grid_rows = 0 in `apply_warp`) painted by brush strokes.
/// Each dab accumulates displacement in the push/pull/swirl direction.
///
/// # GPU implementation (TODO)
/// Requires: a dab-to-displacement shader that accumulates per-pixel offsets
/// into the displacement buffer, then `apply_warp` reads it → B.
pub struct LiquifyRasterTool {
has_dispatched: bool,
}
impl LiquifyRasterTool {
pub fn new() -> Self { Self { has_dispatched: false } }
}
impl RasterTool for LiquifyRasterTool {
fn begin(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.has_dispatched = true; // placeholder
}
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { self.has_dispatched }
fn cancel(&mut self, _ws: &RasterWorkspace) { self.has_dispatched = false; }
// take_pending_gpu_work: default (None) — full GPU wiring is TODO.
}
// ── SelectionTool ─────────────────────────────────────────────────────────────
/// Raster selection tool (Magic Wand / Quick Select).
///
/// C (RGBA8) acts as the growing selection; C.r = mask value (0 or 255).
/// Each `update()` frame a flood-fill / region-grow shader extends C.r.
/// The composite pass draws A + a tinted overlay from C.r → B so the user
/// sees the growing selection boundary.
///
/// `finish()` returns false (commit does not write pixels back to the layer;
/// instead the caller extracts C.r into the standalone `R8Unorm` selection
/// texture via `shared.raster_selection`).
///
/// # GPU implementation (TODO)
/// Requires: a flood-fill compute shader seeded by the click position that
/// grows the selection in C.r; and a composite shader that tints selected
/// pixels blue/cyan for preview.
pub struct SelectionTool {
has_selection: bool,
}
impl SelectionTool {
pub fn new() -> Self { Self { has_selection: false } }
}
impl RasterTool for SelectionTool {
fn begin(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {}
fn update(&mut self, _ws: &RasterWorkspace, _pos: egui::Vec2, _dt: f32, _settings: &crate::tools::RasterToolSettings) {
self.has_selection = true; // placeholder
}
/// Selection tools never trigger a pixel readback/commit on mouseup.
/// The caller reads C.r directly into the selection mask texture.
fn finish(&mut self, _ws: &RasterWorkspace) -> bool { false }
fn cancel(&mut self, _ws: &RasterWorkspace) { self.has_selection = false; }
// take_pending_gpu_work: default (None) — full GPU wiring is TODO.
}