From f9b1c2504f50a66cb1746a5fbb3d3cc9d60750d7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:28:08 -0800 Subject: [PATCH] Smooth tool: measure twice the reach along a curve that leaves without a handle Near an anchor whose handle does not reach out, the curve starts more slowly than its control polygon, so the finely cut stretch could end before twice the reach and the turn there was measured along a long chord. The stretches past it are now cut as finely until the pieces reach three times the reach. docs/mcp.md says the one-eighth cap applies to a closed path the drag reaches all the way round. --- crates/engine/src/cmd/draw2/smooth.rs | 50 +++++++++++++++++++++++++-- docs/mcp.md | 4 +-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/draw2/smooth.rs b/crates/engine/src/cmd/draw2/smooth.rs index a2b4f61bfe..a460cb3ed2 100644 --- a/crates/engine/src/cmd/draw2/smooth.rs +++ b/crates/engine/src/cmd/draw2/smooth.rs @@ -31,7 +31,8 @@ fn polygon_len(c: CubicBez) -> f64 { /// The places (0 to 1, in order, without 0 and with 1) on the curved segment `c` that /// [`smooth_corners`] measures it at: up to [`CORNER_PIECES`] evenly spaced, and near each end, over /// a stretch at least three times `reach` long where the segment is that long, up to as many more -/// that are no more than `reach` / 8 apart along the control polygon. The turns over `reach` and +/// that are no more than `reach` / 8 apart along the control polygon, and as many again in each +/// stretch twice as long after it until the pieces reach three times `reach`. The turns over `reach` and /// twice `reach` from an anchor are then measured along the curve however long the segment is. fn corner_places(c: CubicBez, reach: f64) -> Vec { let piece = reach / 8.0; @@ -41,7 +42,8 @@ fn corner_places(c: CubicBez, reach: f64) -> Vec { if piece > 0.0 { for from_start in [true, false] { // The stretch from the end: halved until its control polygon is at most six reaches long. - let mut t = 1.0 / k as f64; + let first = 1.0 / k as f64; + let mut t = first; let part = |t: f64| if from_start { c.subsegment(0.0..t) } else { c.subsegment(1.0 - t..1.0) }; for _ in 0..60 { if polygon_len(part(t)) <= 6.0 * reach { @@ -49,8 +51,34 @@ fn corner_places(c: CubicBez, reach: f64) -> Vec { } t /= 2.0; } + let side = |u: f64| if from_start { u } else { 1.0 - u }; let m = pieces(polygon_len(part(t))); - places.extend((1..m).map(|j| t * j as f64 / m as f64).map(|u| if from_start { u } else { 1.0 - u })); + let mut fine: Vec = (1..=m).map(|j| t * j as f64 / m as f64).collect(); + // Then the stretches twice as long in turn, cut as finely, up to the first even place, until + // the pieces reach three reaches from the end, so twice the reach is measured along the curve + // where a handle that does not reach out makes the curve start more slowly than its polygon. + let mut covered = 0.0; + let mut at = c.eval(side(0.0)); + for u in &fine { + let q = c.eval(side(*u)); + covered += at.distance(q); + at = q; + } + let (mut from, mut levels) = (t, 0); + while covered < 3.0 * reach && from < first && levels < 60 { + let to = (2.0 * from).min(first); + let piece_len = polygon_len(if from_start { c.subsegment(from..to) } else { c.subsegment(1.0 - to..1.0 - from) }); + let m = pieces(piece_len); + for j in 1..=m { + let u = from + (to - from) * j as f64 / m as f64; + let q = c.eval(side(u)); + covered += at.distance(q); + at = q; + fine.push(u); + } + (from, levels) = (to, levels + 1); + } + places.extend(fine.into_iter().map(side)); } } places.retain(|u| *u > 0.0 && *u <= 1.0); @@ -969,6 +997,22 @@ mod tests { assert_eq!(at_v(&long), Some(true)); } + /// A line that meets a curve leaving with no handle, turning by 58° at the anchor and by about + /// 57.53° over twice a reach of 0.02 pt: the anchor is a corner, measured either way along the path, + /// as the stretch measured finely near the anchor covers twice the reach along the curve. + #[test] + fn a_corner_next_to_a_curve_without_a_handle_is_kept() { + let at = 58f64.to_radians(); + let prev = Anchor::corner(Point::new(-100.0 * at.cos(), -100.0 * at.sin())); + let v = Point::new(0.0, 0.0); + let a = Anchor::with_handles(v, v, v); + let end = Point::new(600.0, 600.0); + let next = Anchor::with_handles(end, Point::new(200.0, 0.0), end); + assert_eq!(smooth_corners(&[prev, a, next], false, 0.02), vec![false, true, false]); + let back = |x: Anchor| Anchor::with_handles(x.p, x.h_out, x.h_in); + assert_eq!(smooth_corners(&[back(next), back(a), back(prev)], false, 0.02), vec![false, true, false]); + } + /// A jagged closed path whose length is less than twice the reach: its jags are not corners, /// while a triangle that small keeps its three. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index d30f2bbf8a..13dd1919ed 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5296,8 +5296,8 @@ path, the curve keeps the direction of the part outside the drag. `object.path.s direction. `path.smoothRegion` keeps an anchor as a corner where the path turns by more than 57.5° (an angle inside the corner -under 122.5°) at the anchor and also over `radius` and over twice `radius` on either side of it (on a closed path, no -more than an eighth of its length), so a jag or a bump smaller than `radius` is still smoothed. The stretches on either side of a kept corner are smoothed and refitted on their +under 122.5°) at the anchor and also over `radius` and over twice `radius` on either side of it (on a closed path that the drag reaches all the +way round, no more than an eighth of its length), so a jag or a bump smaller than `radius` is still smoothed. The stretches on either side of a kept corner are smoothed and refitted on their own, and the corner keeps its place and the directions the path arrives and leaves along, on every call and at any `fidelity`. A call that changes no anchor returns `{ids: [], before: 0, after: 0}` and adds no undo step. The 57.5° threshold is VectorCraft's own value.