From 31d4c6eba4e899d24ddbc95de7e1811e4be5ea5f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 10:31:24 +0700 Subject: [PATCH] Batch r2-1 review: per-side strokes follow smoothed corners for every alignment --- crates/pathops/src/lib.rs | 22 ++++++++++++++----- .../pathops/src/rounded_side_strokes_tests.rs | 10 +++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/pathops/src/lib.rs b/crates/pathops/src/lib.rs index ca2ee0b6..324af4d4 100644 --- a/crates/pathops/src/lib.rs +++ b/crates/pathops/src/lib.rs @@ -456,8 +456,9 @@ pub fn rect_side_strokes(w: f64, h: f64, sides: [f64; 4], align: StrokeAlign) -> /// Per-side stroke of a rectangle with corner radii `[tl, tr, br, bl]`, like CSS borders: the outer /// edge is the rounded rectangle (center and outside grow its box by half or all of each side's /// weight, and each non-zero radius with it); the inner edge is inset by each side's weight, with -/// elliptical corners `max(0, R − weight)` across and down (square where either is 0). An even-odd -/// ring whose inner edge runs against the outer one, so it is a ring under non-zero too. +/// elliptical corners `max(0, R − weight)` across and down (square where either is 0). Corners +/// that stay circular are smoothed like the shape's. An even-odd ring whose inner edge runs against +/// the outer one, so it is a ring under non-zero too. pub fn rounded_side_strokes(w: f64, h: f64, radii: [f64; 4], smoothing: f64, sides: [f64; 4], align: StrokeAlign) -> BezPath { let [t, r, b, l] = sides.map(non_negative); if t + r + b + l <= 0.0 { @@ -477,7 +478,7 @@ pub fn rounded_side_strokes(w: f64, h: f64, radii: [f64; 4], smoothing: f64, sid figcraft_geom::rounded_rect(w, h, radii, smoothing) } else { let grown = corners.map(|(rr, x, y)| if rr > 0.0 { (rr + x * f, rr + y * f) } else { (0.0, 0.0) }); - elliptical_rect(figcraft_geom::Rect::new(-l * f, -t * f, w + r * f, h + b * f), grown) + corner_rect(figcraft_geom::Rect::new(-l * f, -t * f, w + r * f, h + b * f), grown, smoothing) }; let k = 1.0 - f; let inner = figcraft_geom::Rect::new(l * k, t * k, w - r * k, h - b * k); @@ -485,9 +486,18 @@ pub fn rounded_side_strokes(w: f64, h: f64, radii: [f64; 4], smoothing: f64, sid return outer; } let (inset, fit) = fit_corners(inner, corners.map(|(rr, x, y)| ((rr - x * k).max(0.0), (rr - y * k).max(0.0)))); - // The hole's corners sit on the outer ones, inside them, unless they were shrunk to fit or the - // outer ones are smoothed: then it may leave the outer edge. - ring(outer, &elliptical_rect(inner, inset), fit < 1.0 || (align == StrokeAlign::Inside && smoothing > 0.0)) + // The hole's corners sit on the outer ones, inside them, unless they were shrunk to fit or + // smoothed: then it may leave the outer edge. + ring(outer, &corner_rect(inner, inset, smoothing), fit < 1.0 || smoothing > 0.0) +} + +/// Rectangle `r` with `(rx, ry)` corners, smoothed like `figcraft_geom::rounded_rect` where every +/// corner is circular (smoothing is defined for those only). +fn corner_rect(r: figcraft_geom::Rect, corners: [(f64, f64); 4], smoothing: f64) -> BezPath { + if smoothing > 0.0 && corners.iter().all(|(rx, ry)| rx == ry) { + return kurbo::Affine::translate((r.x0, r.y0)) * figcraft_geom::rounded_rect(r.width(), r.height(), corners.map(|c| c.0), smoothing); + } + elliptical_rect(r, corners) } fn non_negative(v: f64) -> f64 { diff --git a/crates/pathops/src/rounded_side_strokes_tests.rs b/crates/pathops/src/rounded_side_strokes_tests.rs index ee0e8281..9c2f387e 100644 --- a/crates/pathops/src/rounded_side_strokes_tests.rs +++ b/crates/pathops/src/rounded_side_strokes_tests.rs @@ -100,6 +100,16 @@ fn smoothed_corners_keep_the_stroke_inside_the_fill() { assert!(painted(&smooth, 60.0, 59.5)); } +#[test] +fn center_and_outside_strokes_follow_smoothed_corners() { + for (align, out, inset) in [(StrokeAlign::Center, 1.0, 1.0), (StrokeAlign::Outside, 2.0, 0.0)] { + let a = area(&rounded_side_strokes(W, H, [16.0; 4], 0.6, [2.0; 4], align)); + let outer = area(&rounded_rect(W + 2.0 * out, H + 2.0 * out, [16.0 + out; 4], 0.6)); + let hole = area(&rounded_rect(W - 2.0 * inset, H - 2.0 * inset, [16.0 - inset; 4], 0.6)); + assert!((a - (outer - hole)).abs() < 1e-3, "{align:?}: {a} vs {}", outer - hole); + } +} + #[test] fn weights_wider_than_the_box_leave_the_outer_edge() { let full = rounded_side_strokes(20.0, 10.0, [4.0; 4], 0.0, [6.0; 4], StrokeAlign::Inside);