From 72c8f58e11dff989ea95d8960fb9947e5ae2d26e Mon Sep 17 00:00:00 2001 From: David Mann Date: Tue, 13 Oct 2026 03:17:38 +0200 Subject: [PATCH] Shapes: a rectangle with a negative size extends from its origin like the other shapes (#4203) (#4204) rounded_rect took the absolute width/height without moving the origin, so shape.create {"kind":"rect","rect":[50,10,-20,30]} covered x 50..70 while an ellipse or polygon with the same rect covered 30..50. Normalise (x, w) and (y, h) for negative sizes instead. The Shape tool drag already normalises its rect, so interactive drawing is unchanged. Co-authored-by: Claude Opus 5.5 (1M context) --- crates/engine/src/vector_cmds/tests.rs | 25 +++++++++++++++++++++++++ crates/vector/src/shapes.rs | 20 ++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/vector_cmds/tests.rs b/crates/engine/src/vector_cmds/tests.rs index 11a4b511e..21207d3e9 100644 --- a/crates/engine/src/vector_cmds/tests.rs +++ b/crates/engine/src/vector_cmds/tests.rs @@ -796,6 +796,31 @@ fn shape_info_bounds_of_curved_shapes_are_exact() { } } +#[test] +fn shape_create_negative_size_rect_covers_the_ellipse_box() { + // Issue #4203: a rectangle with a negative width spanned x 50..70 while the ellipse and + // polygon with the same `rect` spanned 30..50. + for depth in [8, 16, 32] { + let mut s = session(100, 60, depth); + let ell = s.execute("shape.create", json!({"kind": "ellipse", "rect": [50, 10, -20, 30], "fill": "#0000ff"})).unwrap()["bounds"].clone(); + assert_eq!(ell, json!([30, 10, 20, 30]), "{depth}-bit"); + for params in [ + json!({"kind": "rect", "rect": [50, 10, -20, 30], "fill": "#0000ff"}), + json!({"kind": "roundedRect", "rect": [50, 10, -20, 30], "radii": 5, "fill": "#0000ff"}), + json!({"kind": "rect", "rect": [50, 40, -20, -30], "fill": "#0000ff"}), + ] { + let r = s.execute("shape.create", params.clone()).unwrap(); + assert_eq!(r["bounds"], ell, "{params} at {depth}-bit"); + let sh = shape(&s, r["layer"].as_u64().unwrap()); + let c = sh.cache.as_ref().unwrap(); + let fmt = c.format(); + let alpha = |x: i32, y: i32| photocraft_raster::to_rgba(&fmt, &c.read_region(photocraft_doc::Rect::from_xywh(x, y, 1, 1)))[3]; + assert!(alpha(40, 25) > 0.99, "{params} at {depth}-bit"); + assert!(alpha(60, 25) < 0.01, "{params} at {depth}-bit"); + } + } +} + /// Two overlapping 40×40 rectangle shape layers ("Rectangle 1" black below, "Rectangle 2" grey /// above, overlapping by 20×20) with both selected, like the Layers panel in #2400. fn two_selected_rects(depth: u32) -> (Session, u64, u64) { diff --git a/crates/vector/src/shapes.rs b/crates/vector/src/shapes.rs index a459ba816..b154a4732 100644 --- a/crates/vector/src/shapes.rs +++ b/crates/vector/src/shapes.rs @@ -29,9 +29,11 @@ pub fn clamp_radii(w: f64, h: f64, radii: [f64; 4]) -> [f64; 4] { r.map(|v| v * f) } -/// Rectangle with corner radii `[top-left, top-right, bottom-right, bottom-left]`. +/// Rectangle with corner radii `[top-left, top-right, bottom-right, bottom-left]`. A negative +/// width or height extends from the origin to the left or up, like [`ellipse`] and [`polygon`]. pub fn rounded_rect(x: f64, y: f64, w: f64, h: f64, radii: [f64; 4]) -> Path { - let (w, h) = (w.abs(), h.abs()); + let (x, w) = if w < 0.0 { (x + w, -w) } else { (x, w) }; + let (y, h) = if h < 0.0 { (y + h, -h) } else { (y, h) }; let [tl, tr, br, bl] = clamp_radii(w, h, radii); if tl <= 0.0 && tr <= 0.0 && br <= 0.0 && bl <= 0.0 { return rect(x, y, w, h); @@ -143,6 +145,20 @@ mod tests { assert_eq!(clamp_radii(10.0, 10.0, [10.0; 4]), [5.0; 4]); } + #[test] + fn negative_size_rect_extends_from_its_origin_like_the_ellipse() { + // Issue #4203: a negative width/height drew the rectangle on the far side of its origin. + let want = ellipse(50.0, 10.0, -20.0, 30.0).control_bounds().unwrap(); + let want_neg_h = ellipse(50.0, 40.0, -20.0, -30.0).control_bounds().unwrap(); + for radii in [[0.0; 4], [5.0; 4]] { + let b = rounded_rect(50.0, 10.0, -20.0, 30.0, radii).control_bounds().unwrap(); + assert_eq!(b, want, "radii {radii:?}"); + assert_eq!(b, (30.0, 10.0, 50.0, 40.0), "radii {radii:?}"); + let b = rounded_rect(50.0, 40.0, -20.0, -30.0, radii).control_bounds().unwrap(); + assert_eq!(b, want_neg_h, "radii {radii:?}"); + } + } + #[test] fn star_vertices() { assert_eq!(polygon(0.0, 0.0, 10.0, 10.0, 5, 0.5).subpaths[0].knots.len(), 10);