fix(geom): Trim straight sides exactly for Corner Options

Straight sides were trimmed through inv_arclen at 1e-3 accuracy, so a
rectangle's corner points landed up to about 1e-4 pt off. Lines are now
measured and trimmed along the chord, exactly; curves still use arc length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer 2026-10-11 23:57:34 +02:00 committed by paaspaas00
parent a175245f95
commit 5eda7cebbc
1 changed files with 29 additions and 2 deletions

View File

@ -107,7 +107,9 @@ fn apply_subpath(sp: &SubPath, opts: &CornerOptions, out: &mut BezPath) {
// Segment `i` runs from anchor `i` to anchor `i + 1`; the corner at anchor `i` joins segments
// `i - 1` and `i`, and takes at most half of each.
let segs: Vec<CubicBez> = (0..n).map(|i| sp.segment(i)).collect();
let lens: Vec<f64> = segs.iter().map(|c| c.arclen(ARC_ACCURACY)).collect();
let lines: Vec<bool> = (0..n).map(|i| sp.segment_is_line(i)).collect();
// Straight sides are measured (and trimmed below) exactly; curves by arc length.
let lens: Vec<f64> = segs.iter().zip(&lines).map(|(c, line)| if *line { (c.p3 - c.p0).hypot() } else { c.arclen(ARC_ACCURACY) }).collect();
let size: Vec<f64> = (0..n)
.map(|i| {
let c = opts.corners[i % 4];
@ -126,6 +128,12 @@ fn apply_subpath(sp: &SubPath, opts: &CornerOptions, out: &mut BezPath) {
let trimmed: Vec<CubicBez> = (0..n)
.map(|i| {
let (s0, s1) = (size[i], size[(i + 1) % n]);
if lines[i] {
let c = &segs[i];
let u = if lens[i] > 1e-12 { (c.p3 - c.p0) / lens[i] } else { Vec2::ZERO };
let (a, b) = (c.p0 + u * s0, c.p3 - u * s1);
return CubicBez::new(a, a, b, b);
}
let t0 = if s0 > 0.0 { segs[i].inv_arclen(s0, ARC_ACCURACY) } else { 0.0 };
let t1 = if s1 > 0.0 { segs[i].inv_arclen(lens[i] - s1, ARC_ACCURACY) } else { 1.0 };
segs[i].subsegment(t0..t1.max(t0))
@ -134,7 +142,7 @@ fn apply_subpath(sp: &SubPath, opts: &CornerOptions, out: &mut BezPath) {
out.move_to(trimmed[0].p0);
for i in 0..n {
let seg = &trimmed[i];
if sp.segment_is_line(i) {
if lines[i] {
out.line_to(seg.p3);
} else {
out.curve_to(seg.p1, seg.p2, seg.p3);
@ -246,6 +254,25 @@ mod tests {
}
}
#[test]
fn straight_sides_are_trimmed_exactly() {
let r = rectangle(Rect::new(0.0, 0.0, 100.0, 60.0));
let bp = apply(&r, &CornerOptions::uniform(CornerShape::Bevel, 10.0));
// Every point a bevelled rectangle goes through sits exactly 10 pt from a corner.
let pts: Vec<Point> = bp
.elements()
.iter()
.filter_map(|e| match e {
kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) => Some(*p),
_ => None,
})
.collect();
assert_eq!(pts.len(), 9, "{pts:?}");
for p in pts {
assert!([0.0, 10.0, 90.0, 100.0].contains(&p.x) && [0.0, 10.0, 50.0, 60.0].contains(&p.y), "{p:?}");
}
}
#[test]
fn smooth_points_are_not_corners() {
let e = crate::shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 60.0));