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) <[email protected]>
This commit is contained in:
David MannandClaude Opus 5.5 authored and GitHub committed 2026-10-12 21:17:38 -04:00
1 parent e4f15eecdf
commit 72c8f58e11
2 files changed
+43 -2

No files matched your search

+25
View File
@@ -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) {
+18 -2
View File
@@ -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);