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;