diff --git a/hcie-engine-api/src/stroke_brush.rs b/hcie-engine-api/src/stroke_brush.rs index 5f397f0..286b025 100644 --- a/hcie-engine-api/src/stroke_brush.rs +++ b/hcie-engine-api/src/stroke_brush.rs @@ -270,10 +270,6 @@ impl Engine { let w = self.document.canvas_width as f32; let h = self.document.canvas_height as f32; let inside = x >= 0.0 && y >= 0.0 && x < w && y < h; - if !inside { - self.last_stroke_pos = None; - return; - } let tip = self.brush_tip_with_time_dynamics(x, y); let color = self.apply_cyclic_color(self.current_color); @@ -287,10 +283,14 @@ impl Engine { let is_eraser = self.is_eraser; let dirty_r = Self::brush_dirty_radius(&tip); - self.document.expand_dirty(lx as u32, ly as u32, dirty_r); - self.document.expand_dirty(x as u32, y as u32, dirty_r); - self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r); - self.expand_stroke_bounds(x as u32, y as u32, dirty_r); + if lx >= 0.0 && ly >= 0.0 && lx < w && ly < h { + self.document.expand_dirty(lx as u32, ly as u32, dirty_r); + self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r); + } + if inside { + self.document.expand_dirty(x as u32, y as u32, dirty_r); + self.expand_stroke_bounds(x as u32, y as u32, dirty_r); + } self.draw_target_pixels_or_mask( layer_id, @@ -348,13 +348,15 @@ impl Engine { let dirty_r = tip.size.max(2.0); if let Some((lx, ly, _)) = self.last_stroke_pos { - self.document.expand_dirty(lx as u32, ly as u32, dirty_r); + if lx >= 0.0 && ly >= 0.0 && lx < w && ly < h { + self.document.expand_dirty(lx as u32, ly as u32, dirty_r); + self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r); + } } - self.document.expand_dirty(x as u32, y as u32, dirty_r); - if let Some((lx, ly, _)) = self.last_stroke_pos { - self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r); + if inside { + self.document.expand_dirty(x as u32, y as u32, dirty_r); + self.expand_stroke_bounds(x as u32, y as u32, dirty_r); } - self.expand_stroke_bounds(x as u32, y as u32, dirty_r); if self.editing_mask { self.draw_target_pixels_or_mask( diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs index da528ac..f9c8afb 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs @@ -1024,8 +1024,9 @@ pub struct CanvasShaderProgram { /// Convert a viewport-local point to canvas-space coordinates. /// /// `local_pos` is relative to the shader widget's top-left corner. -/// `viewport_size` is the widget's size. Returns `(Some(x), Some(y))` -/// when inside the canvas image bounds, otherwise `(None, None)`. +/// `viewport_size` is the widget's size. Returns the full signed canvas +/// coordinates even when the cursor is outside the visible canvas image, +/// so brush strokes can continue mathematically beyond the canvas edges. fn screen_to_canvas_local( local_pos: Point, viewport_w: f32, @@ -1053,17 +1054,7 @@ fn screen_to_canvas_local( let canvas_x = (local_pos.x - origin_x) / zoom; let canvas_y = (local_pos.y - origin_y) / zoom; - let cx = if canvas_x >= 0.0 && canvas_x < engine_w { - Some(canvas_x) - } else { - None - }; - let cy = if canvas_y >= 0.0 && canvas_y < engine_h { - Some(canvas_y) - } else { - None - }; - (cx, cy) + (Some(canvas_x), Some(canvas_y)) } impl shader::Program for CanvasShaderProgram { @@ -1106,8 +1097,8 @@ impl shader::Program for CanvasShaderProgram { // Left mouse drag (drawing) if state.left_pressed { - let cx = canvas_x.unwrap_or(0.0).clamp(0.0, self.engine_w as f32); - let cy = canvas_y.unwrap_or(0.0).clamp(0.0, self.engine_h as f32); + let cx = canvas_x.unwrap_or(0.0); + let cy = canvas_y.unwrap_or(0.0); return ( iced::event::Status::Captured, Some(Message::CanvasPointerMoved { diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs index 5edbd2a..e8485d3 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs @@ -200,6 +200,9 @@ impl Program for CanvasViewportProgram { } /// Convert viewport-space coordinates to canvas-space coordinates. +/// +/// Returns signed canvas coordinates even when the cursor is outside the canvas +/// image so brush strokes can start or continue beyond the visible edges. fn viewport_to_canvas(viewport_pos: Point, bounds: Rectangle, config: &CanvasViewportConfig) -> Option<(f32, f32)> { let display_w = config.engine_w as f32 * config.zoom; let display_h = config.engine_h as f32 * config.zoom; @@ -210,13 +213,7 @@ fn viewport_to_canvas(viewport_pos: Point, bounds: Rectangle, config: &CanvasVie let canvas_x = (viewport_pos.x - origin_x) / config.zoom; let canvas_y = (viewport_pos.y - origin_y) / config.zoom; - if canvas_x >= 0.0 && canvas_x < config.engine_w as f32 - && canvas_y >= 0.0 && canvas_y < config.engine_h as f32 - { - Some((canvas_x, canvas_y)) - } else { - None - } + Some((canvas_x, canvas_y)) } /// Build the canvas viewport widget.