From e11155c692084aafed6d2339462907b19d50d0e7 Mon Sep 17 00:00:00 2001 From: jmverges Date: Fri, 13 Nov 2026 14:42:07 +0000 Subject: [PATCH] Envelopes: a top object's split goes by the longer of handles or bends The previous commit took the spans from how far each piece bends away from the split whenever both handles there were within ONE_CURVE of the anchor. A curve that barely moves at the split but doesn't bend there (a straight one, say) has short handles and no bend, so that gave 0/0 and the side fell back to the polygon shares. Now the spans come from whichever pair is the longer: the handles, or the bends (which go with the squares of the spans). The check of every piece against the rebuilt curve is unchanged. Test: a_split_curve_keeps_the_top_object_patch_where_it_barely_moves_and_runs_straight. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/live.rs | 17 +++++++++++++++++ crates/doc/src/live/mesh.rs | 19 ++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index 6c0e880ee1..cf7a4d2fa0 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -628,6 +628,23 @@ mod tests { } } + #[test] + fn a_split_curve_keeps_the_top_object_patch_where_it_barely_moves_and_runs_straight() { + // A straight top curve that barely moves at t = 0.25 and doesn't bend there: split there, + // the handles at the split are a few thousandths of a point long and the pieces don't bend + // at all, and the patch with the corners given stays the unsplit curve's. + let slow = CubicBez::new((0.0, 0.0), (28.578125, 0.0), (-57.109375, 0.0), (200.0, 0.0)); + let pieces = [slow.subsegment(0.0..0.25), slow.subsegment(0.25..1.0)]; + let (into, out) = (pieces[0].p3.distance(pieces[0].p2), pieces[1].p0.distance(pieces[1].p1)); + assert!(into > 0.0 && out > 0.0 && into.max(out) < 0.01, "{into} {out}"); + let whole = Coons::from_top_object(&top_of_pieces(&[slow]), Some([0, 1, 2, 3])).unwrap(); + let c = Coons::from_top_object(&top_of_pieces(&pieces), Some([0, 2, 3, 4])).unwrap(); + for (u, v) in [(0.1, 0.0), (0.25, 0.0), (0.3, 0.2), (0.5, 0.5), (0.62, 0.0), (0.9, 0.7), (0.97, 1.0)] { + let (p, q) = (c.eval(u, v), whole.eval(u, v)); + assert!(p.distance(q) < 1e-6, "({u}, {v}) → {p:?}, unsplit {q:?}"); + } + } + #[test] fn a_top_object_joins_a_split_curve_saved_with_rounded_coordinates() { // A file saves the pieces of a curve split into thirds, or into tenths, with its diff --git a/crates/doc/src/live/mesh.rs b/crates/doc/src/live/mesh.rs index dbebf9a439..b6d5bbc88d 100644 --- a/crates/doc/src/live/mesh.rs +++ b/crates/doc/src/live/mesh.rs @@ -364,7 +364,7 @@ fn control_polygon(c: &CubicBez) -> f64 { /// of the split lie in line, their lengths in proportion to the spans of its parameter the two /// pieces take; split where the curve stops (a cusp), both handles lie on the anchor, and the /// pieces' second derivatives there, in proportion to the squares of the spans, give the proportion -/// instead. Those proportions give each piece's span, and the curve follows from the first +/// instead (whichever of the two pairs is the longer). Those proportions give each piece's span, and the curve follows from the first /// piece's start and its handle, and the last piece's end and its handle, scaled up by their spans. /// Every piece is then checked against that curve's piece over its span, so error can't build up /// from one split to the next (the first piece's start and handle, and the last piece's end and @@ -379,16 +379,13 @@ fn one_curve(pieces: &[CubicBez]) -> Option { for pair in pieces.windows(2) { let [a, b] = pair else { return None }; let (into, out) = (a.p3.distance(a.p2), b.p0.distance(b.p1)); - let ratio = if into.max(out) <= ONE_CURVE { - // Split where the curve stops (a cusp), both handles lie on the anchor and say nothing - // of the spans; how far each piece bends away from its anchor, in proportion to the - // squares of the spans, says it instead. - let bend_in = ((a.p3 - a.p2) - (a.p2 - a.p1)).hypot(); - let bend_out = ((b.p2 - b.p1) - (b.p1 - b.p0)).hypot(); - (bend_out / bend_in).sqrt() - } else { - out / into - }; + // How far each piece bends away from the anchor goes with the squares of the spans. Split + // where the curve stops (a cusp), both handles lie on the anchor and say nothing of the + // spans, and the bends say it; where the curve runs straight through the split, the bends + // are nothing and the handles say it. Whichever pair is the longer is the one to go by. + let bend_in = ((a.p3 - a.p2) - (a.p2 - a.p1)).hypot(); + let bend_out = ((b.p2 - b.p1) - (b.p1 - b.p0)).hypot(); + let ratio = if into.min(out) >= bend_in.min(bend_out) { out / into } else { (bend_out / bend_in).sqrt() }; let next = spans.last().copied().unwrap_or(1.0) * ratio; if !(next.is_finite() && next > 0.0) { return None;