mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
Envelopes: a top object's side split at a cusp still follows its curve
Split where the curve stops (a cusp), both handles at the split lie on the anchor, so their lengths say nothing of how the curve's parameter divides between the two pieces, and the side fell back to the polygon shares. When both handles lie within ONE_CURVE of the anchor, the spans now come from how far each piece bends away from it (the second derivatives, in proportion to the squares of the spans). The existing check of every piece against the rebuilt curve is unchanged. Test: a_split_curve_keeps_the_top_object_patch_when_split_at_a_cusp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
728fd9ef14
commit
6ed3935e59
@@ -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
|
||||
|
||||
@@ -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<CubicBez> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user