feat: allow brush strokes to extend beyond canvas edges in viewport and shader calculations
mandatory-regression-gate / deterministic-tests (push) Waiting to run
mandatory-regression-gate / protected-performance-path (push) Waiting to run

This commit is contained in:
Your Name
2026-07-25 16:34:51 +03:00
parent eb17bf4205
commit 45a231a456
3 changed files with 25 additions and 35 deletions
+10 -8
View File
@@ -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);
if lx >= 0.0 && ly >= 0.0 && lx < w && ly < h {
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);
}
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 {
if lx >= 0.0 && ly >= 0.0 && lx < w && ly < h {
self.document.expand_dirty(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);
}
if self.editing_mask {
self.draw_target_pixels_or_mask(
@@ -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<Message> for CanvasShaderProgram {
@@ -1106,8 +1097,8 @@ impl shader::Program<Message> 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 {
@@ -200,6 +200,9 @@ impl Program<Message> 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
}
}
/// Build the canvas viewport widget.