diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index e667f2972d..6c0e880ee1 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -612,6 +612,22 @@ mod tests { } } + #[test] + fn a_split_curve_keeps_the_top_object_patch_when_split_at_a_cusp() { + // The top curve stops at t = 0.25 (a cusp), and an anchor splits it there: both handles at + // the split lie on the anchor, and the patch with the corners given stays the unsplit + // curve's. + let cusp = CubicBez::new((0.0, 0.0), (30.0, -40.0), (-58.0, 24.0), (200.0, 0.0)); + let pieces = [cusp.subsegment(0.0..0.25), cusp.subsegment(0.25..1.0)]; + assert!(pieces[0].p2.distance(pieces[0].p3) < 1e-9 && pieces[1].p1.distance(pieces[1].p0) < 1e-9); + let whole = Coons::from_top_object(&top_of_pieces(&[cusp]), 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 5e90a56462..dbebf9a439 100644 --- a/crates/doc/src/live/mesh.rs +++ b/crates/doc/src/live/mesh.rs @@ -362,7 +362,9 @@ fn control_polygon(c: &CubicBez) -> f64 { /// between them, when each control point of each piece lies within [`ONE_CURVE`] of the matching /// piece of that curve; `None` otherwise. Split at parameter `t`, a curve's handles on either side /// of the split lie in line, their lengths in proportion to the spans of its parameter the two -/// pieces take. Those proportions give each piece's span, and the curve follows from the first +/// 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 /// 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 @@ -377,8 +379,18 @@ 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 next = spans.last().copied().unwrap_or(1.0) * out / into; - if !(into > 0.0 && out > 0.0 && next.is_finite() && next > 0.0) { + 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 + }; + let next = spans.last().copied().unwrap_or(1.0) * ratio; + if !(next.is_finite() && next > 0.0) { return None; } spans.push(next);