Simplify: the join at the start of a smoothed loop turns by at most 0.5°

A smoothed loop with no corner is refitted along one direction across
its start. The first and the last fitted curve were each compared with
that direction, so when both became lines, the two lines could turn by
up to 1° where they meet at the loop's start.

`refit_part` now compares the last curve of such a loop with the
direction the loop's first segment leaves along. When the line would
turn by more than 0.5° from it, the last piece stays a flat curve.

Test: `simplify_keeps_the_join_at_the_start_of_a_smoothed_loop_smooth`
(`crates/pathops/tests/it/ops.rs`).
This commit is contained in:
dotCooCoo
2026-11-13 04:33:23 -08:00
parent eee96556e6
commit ab75b67f85
2 changed files with 34 additions and 2 deletions
+8 -2
View File
@@ -329,12 +329,18 @@ fn refit_part(part: &[(&Seg, usize)], opts: &SimplifyOptions, tol: f64, ends: Op
// The fit may be `tol * 0.98` from the lines between the samples, those lines may be
// `SAMPLE_GAP` of `tol` from the path, and the curve the test below turns into a line moves by up
// to 1.5% of `tol`: 3/4 of how far its handles may be from the line. The fitted curves join
// smoothly, so a curve becomes a line only when the line keeps both of its joins smooth.
// smoothly, so a curve becomes a line only when the line keeps both of its joins smooth. The last
// curve of a loop fitted across its start leaves along the loop's first segment.
let (t0, t1) = ends.map_or((start_tangent(&first.c), end_tangent(&last.c)), |d| (d, d));
let fitted = fit_cubics(&pts, t0, t1, tol * 0.98);
let from = result.len();
let mut arrive = t0;
for (k, &c) in fitted.iter().enumerate() {
let leave = fitted.get(k + 1).map_or(t1, start_tangent);
let leave = match fitted.get(k + 1) {
Some(next) => start_tangent(next),
None if ends.is_some() => result.get(from).map_or(t1, |s| start_tangent(&s.c)),
None => t1,
};
let line = is_straight(&c, tol * 0.02) && line_keeps_joins(&c, arrive, leave);
arrive = if line { unit(c.p3 - c.p0).unwrap_or(arrive) } else { end_tangent(&c) };
result.push(if line { Seg::line(c.p0, c.p3) } else { Seg { c, line } });
+26
View File
@@ -462,6 +462,32 @@ fn simplify_turns_a_curve_into_a_line_only_where_the_joins_stay_smooth() {
assert!(!has_curves(&s), "{s:?}");
}
/// A smoothed loop with no corner is fitted across its start, and the join there also turns by at
/// most 0.5°. Each loop is a flat side that bends by 0.6° at the loop's start, closed by an arc of
/// `n` lines `height` tall, with `inner` anchors along each half of the side.
#[test]
fn simplify_keeps_the_join_at_the_start_of_a_smoothed_loop_smooth() {
for (half, height, n, inner, tolerance, smooth) in
[(40.0, 20.0, 8, 3, 2.5, 2.0), (80.0, 60.0, 8, 3, 5.0, 0.5), (160.0, 200.0, 24, 3, 10.0, 2.0), (160.0, 20.0, 24, 8, 10.0, 5.0)]
{
let rise = half * 0.3f64.to_radians().tan();
let along = |i: i32| Point::new(half * f64::from(i) / f64::from(inner), -rise * f64::from(i.abs()) / f64::from(inner));
let mut pts: Vec<Point> = (0..=inner).map(along).collect();
pts.extend((1..n).map(|i| {
let a = f64::from(i) / f64::from(n) * PI;
Point::new(half * a.cos(), -rise - height * a.sin())
}));
pts.extend((-inner..0).map(along));
let path = PathData::single(SubPath::polyline(&pts, true));
let s = simplify_with(&path, &SimplifyOptions { tolerance, corner_angle_deg: 179.0, smooth, ..Default::default() });
let sp = &s.subpaths[0];
assert!(sp.closed && sp.anchors.len() < pts.len(), "{tolerance} pt: {s:?}");
assert!(sp.segment_is_line(0), "{tolerance} pt: the first segment is a curve: {s:?}");
let turn = largest_join_turn(sp);
assert!(turn <= 0.5 + 1e-9, "{tolerance} pt: an anchor turns {turn}°: {s:?}");
}
}
#[test]
fn simplify_straight_lines_option() {
let c = dense_circle(50.0, 200);