//! 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>, /// 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, } 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, /// 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, } // ── 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`, 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> { 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, 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, pending: Option>, /// 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> { self.pending.take().map(|w| w as Box) } } // ── EffectBrushTool ─────────────────────────────────────────────────────────── /// Raster tool for effect brushes (Blur, Sharpen, Dodge, Burn, Sponge, Desaturate). /// /// C accumulates a per-pixel influence weight (R channel, 0–255). /// 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 { 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, 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>, 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> { self.pending.take().map(|w| w as Box) } } // ── 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>, 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> { self.pending.take().map(|w| w as Box) } } // ── 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. }