From ab75b67f852878f19c7cec2c94abeeb308565a17 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 04:33:23 -0800 Subject: [PATCH] =?UTF-8?q?Simplify:=20the=20join=20at=20the=20start=20of?= =?UTF-8?q?=20a=20smoothed=20loop=20turns=20by=20at=20most=200.5=C2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`). --- crates/pathops/src/edit.rs | 10 ++++++++-- crates/pathops/tests/it/ops.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/pathops/src/edit.rs b/crates/pathops/src/edit.rs index 19089dfd2e..bc1d54cba9 100644 --- a/crates/pathops/src/edit.rs +++ b/crates/pathops/src/edit.rs @@ -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 } }); diff --git a/crates/pathops/tests/it/ops.rs b/crates/pathops/tests/it/ops.rs index d48e1c5dd8..93f6d4c29c 100644 --- a/crates/pathops/tests/it/ops.rs +++ b/crates/pathops/tests/it/ops.rs @@ -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 = (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);