From 6326b17b0ee75a8cc414cdf5394488b7d74d5db8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:09:01 -0800 Subject: [PATCH 1/2] Blend: spreading keys along a replaced spine measures it once Giving the keys of a blend whose spine came from Replace Spine anchors of their own flattened and measured the whole spine again for every key, then walked its segments one by one. 1,000 keys on a spine of 4,096 segments flattened it 999 times. The spine is now flattened once into one buffer. A split flattens only its two halves and measures the segments after it again, in order, only as far as the next key, so the arc lengths are summed as before and the keys get the same anchors. A binary search finds each key's segment. A spine so large that its length overflows no longer gets an anchor split at a parameter that is not a number: spreading keys along it returns nothing and leaves it as it was. --- crates/doc/src/blend.rs | 361 +++++++++++++++++++++++++++++++++++--- docs/target-app-parity.md | 4 +- 2 files changed, 336 insertions(+), 29 deletions(-) diff --git a/crates/doc/src/blend.rs b/crates/doc/src/blend.rs index d170bd606f..462786b93d 100644 --- a/crates/doc/src/blend.rs +++ b/crates/doc/src/blend.rs @@ -1455,7 +1455,7 @@ pub fn pin_spine(keys: &[Arc], spec: &BlendSpec) -> Option<(PathData, Vec< } /// Do `keys` sit where a spine from Replace Spine spreads them: evenly along `spine` by arc length -/// (within a hundredth of a point, as [`anchor_at_length`] matches anchors)? +/// (within a hundredth of a point, as [`SpreadLengths::anchor_at_length`] matches anchors)? fn keys_spread_along(keys: &[Arc], spine: &Spine) -> bool { let k = keys.len(); keys.iter().enumerate().all(|(i, key)| { @@ -1480,40 +1480,204 @@ fn open_at_start(sp: &mut SubPath, ka: &mut [usize]) { } /// The anchors of `path`'s first subpath that `k` keys spread evenly along it by arc length sit -/// on, splitting segments where none is. +/// on, splitting segments where none is. `None`, leaving `path` as it was, when the subpath has no +/// length to spread along, or when an arc length it would split at is not finite (a path so large +/// its length overflows). fn spread_anchors(path: &mut PathData, k: usize) -> Option> { - let total = Spine::new(path)?.length(); - let closed = path.subpaths.first().is_some_and(|s| s.closed); + spread_anchors_with(path, k, |_| {}) +} + +/// [`spread_anchors`], calling `work` for each step of work it does. +fn spread_anchors_with(path: &mut PathData, k: usize, mut work: impl FnMut(SpreadWork)) -> Option> { + let sp = path.subpaths.first_mut()?; + let lengths = SpreadLengths::new(sp, &mut work); + let total = lengths.total()?; + let targets = (1..k.saturating_sub(1)).map(|i| total * i as f64 / (k - 1) as f64); let mut out = vec![0usize]; - for i in 1..k.saturating_sub(1) { - let s = total * i as f64 / (k - 1) as f64; - out.push(anchor_at_length(path, s)?); - } - let last = path.subpaths.first().map_or(0, |sp| if closed { 0 } else { sp.anchors.len().saturating_sub(1) }); - out.push(last); + out.extend(anchors_at_lengths(sp, lengths, targets, &mut work)?); + out.push(if sp.closed { 0 } else { sp.anchors.len().saturating_sub(1) }); Some(out) } -/// The anchor of the first subpath at arc length `s`: an existing one within a hundredth of a -/// point, else a new one splitting the segment there. -fn anchor_at_length(path: &mut PathData, s: f64) -> Option { - let spine = Spine::new(path)?; - let sp = path.subpaths.first_mut()?; - let n = sp.segment_count(); - let seg = (0..n).rev().find(|i| spine.anchor_length(*i).is_some_and(|l| l <= s)).unwrap_or(0); - let from = spine.anchor_length(seg).unwrap_or(0.0); - let to = spine.anchor_length(seg + 1).unwrap_or(spine.length()); - if s - from < 0.01 { - return Some(seg); +/// The anchors of `sp` at each arc length in `targets`, in order, splitting segments where none is +/// (see [`SpreadLengths::anchor_at_length`]); `lengths` measures `sp`. The splits are made on a copy +/// of `sp`, which replaces it only when every target has its anchor: `None` leaves `sp` as it was. +fn anchors_at_lengths( + sp: &mut SubPath, + mut lengths: SpreadLengths, + targets: impl IntoIterator, + work: &mut impl FnMut(SpreadWork), +) -> Option> { + let mut split = sp.clone(); + let mut out = Vec::new(); + for s in targets { + out.push(lengths.anchor_at_length(&mut split, s, work)?); } - if to - s < 0.01 { - return Some((seg + 1) % sp.anchors.len().max(1)); + *sp = split; + Some(out) +} + +/// A step of the work [`spread_anchors_with`] does. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SpreadWork { + /// Flattening every segment of the subpath. + WholePath, + /// Flattening one segment after a split. + Segment, + /// Adding one segment's flattened points to the arc lengths. + Measure, + /// Comparing an anchor's arc length with the one looked for. + Compare, +} + +/// Where the measurement of a subpath's flattened points stands at the start of a segment: the last +/// point kept, the arc length there (0 before any point), and how many points were kept. +#[derive(Clone, Copy)] +struct Measured { + last: Option, + len: f64, + kept: usize, +} + +impl Measured { + const START: Self = Self { last: None, len: 0.0, kept: 0 }; + + /// The measurement after `pts`, keeping each point more than 1e-9 from the last one kept, as + /// [`Spine::new`] does. + fn then(mut self, pts: &[Point]) -> Self { + for &p in pts { + if self.last.is_none_or(|q| q.distance(p) > 1e-9) { + self.len = match self.last { + Some(q) => self.len + q.distance(p), + None => 0.0, + }; + self.last = Some(p); + self.kept += 1; + } + } + self + } +} + +/// The arc lengths of a subpath's anchors, as [`Spine::new`] measures them, kept up to date while +/// [`spread_anchors`] splits its segments. Each segment is flattened once, and again only when it +/// is split. A split leaves the arc lengths of the anchors before it as they are; those after it +/// are measured again, in order from the split, only as far as the next arc length looked for, so +/// the sums are the same as [`Spine::new`]'s on the split subpath. +struct SpreadLengths { + /// The finite points the segments flatten to, each segment's in one run. + pts: Vec, + /// Where each segment's run is in `pts`. A split segment's halves get runs at the end, and its + /// old run is not used again. + flat: Vec>, + /// The measurement at the start of each segment, up to the first one not measured since the + /// last split; after the last segment too once every segment is measured. + marks: Vec, +} + +impl SpreadLengths { + /// Flatten and measure every segment of `sp`. + fn new(sp: &SubPath, work: &mut impl FnMut(SpreadWork)) -> Self { + work(SpreadWork::WholePath); + let n = sp.segment_count(); + let mut lengths = Self { pts: Vec::new(), flat: Vec::with_capacity(n), marks: Vec::with_capacity(n + 1) }; + for i in 0..n { + let run = lengths.flatten(sp, i); + lengths.flat.push(run); + } + lengths.marks.push(Measured::START); + lengths.measure_to(f64::INFINITY, work); + lengths + } + + /// Flatten segment `i` of `sp` as [`Spine::new`] does, adding its finite points to `pts`, and + /// return where they are. + fn flatten(&mut self, sp: &SubPath, i: usize) -> std::ops::Range { + let start = self.pts.len(); + let c = sp.segment(i); + let mut bp = BezPath::new(); + bp.move_to(c.p0); + if sp.segment_is_line(i) { + bp.line_to(c.p3); + } else { + bp.curve_to(c.p1, c.p2, c.p3); + } + vectorcraft_geom::kurbo::flatten(bp.iter(), 0.1, |el| { + if let vectorcraft_geom::PathEl::MoveTo(p) | vectorcraft_geom::PathEl::LineTo(p) = el + && p.x.is_finite() + && p.y.is_finite() + { + self.pts.push(p); + } + }); + start..self.pts.len() + } + + /// The subpath's flattened length, `None` when it flattens to fewer than two points. + fn total(&self) -> Option { + self.marks.last().filter(|m| self.marks.len() > self.flat.len() && m.kept >= 2).map(|m| m.len) + } + + /// Measure the segments that are not measured, from the first, until one starts past `s` (with + /// two points kept before it) or every one is measured. + fn measure_to(&mut self, s: f64, work: &mut impl FnMut(SpreadWork)) { + while let Some(run) = self.flat.get(self.marks.len() - 1) { + let Some(&m) = self.marks.last() else { return }; + if self.marks.len() >= 2 && m.kept >= 2 && m.len > s { + return; + } + work(SpreadWork::Measure); + self.marks.push(m.then(self.pts.get(run.clone()).unwrap_or_default())); + } + } + + /// The anchor of `sp` at arc length `s`: an existing one within a hundredth of a point, else a + /// new one splitting the segment there. The segment is the last one starting at or before `s` + /// (the first one when none does), found by a binary search. `None`, leaving `sp` as it was, + /// when `s` or the arc length at the end of the segment is not finite, or when `sp` flattens + /// to fewer than two points. + fn anchor_at_length(&mut self, sp: &mut SubPath, s: f64, work: &mut impl FnMut(SpreadWork)) -> Option { + if !s.is_finite() { + return None; + } + self.measure_to(s, work); + let n = self.flat.len(); + if self.marks.last().is_none_or(|m| m.kept < 2) { + return None; + } + let starts = &self.marks[..self.marks.len().min(n)]; + let seg = starts + .partition_point(|m| { + work(SpreadWork::Compare); + m.len <= s + }) + .saturating_sub(1); + let from = self.marks.get(seg)?.len; + let to = self.marks.get(seg + 1)?.len; + if s - from < 0.01 { + return Some(seg); + } + if to - s < 0.01 { + return Some((seg + 1) % sp.anchors.len().max(1)); + } + if !from.is_finite() || !to.is_finite() { + return None; + } + let c = sp.segment(seg); + // The flattened length and the curve's own differ slightly: scale into the curve's. + let arc = c.arclen(1e-4).max(1e-12); + let t = c.inv_arclen((s - from) / (to - from).max(1e-12) * arc, 1e-4).clamp(1e-4, 1.0 - 1e-4); + if !t.is_finite() { + return None; + } + let at = sp.insert_anchor(seg, t); + work(SpreadWork::Segment); + work(SpreadWork::Segment); + let halves = [self.flatten(sp, seg), self.flatten(sp, seg + 1)]; + self.flat.splice(seg..=seg, halves); + self.marks.truncate(seg + 1); + Some(at) } - let c = sp.segment(seg); - // The flattened length and the curve's own differ slightly: scale into the curve's. - let arc = c.arclen(1e-4).max(1e-12); - let t = c.inv_arclen((s - from) / (to - from).max(1e-12) * arc, 1e-4).clamp(1e-4, 1.0 - 1e-4); - Some(sp.insert_anchor(seg, t)) } /// Where a blend's keys and steps go along its spine (see [`blend_spine`]). @@ -2003,6 +2167,147 @@ mod tests { } } + /// [`spread_anchors`] by measuring the whole spine again for each key and walking its segments + /// from the last, as a reference for the measurement kept up to date between keys. + fn rebuilt_spread_anchors(path: &mut PathData, k: usize) -> Option> { + let total = Spine::new(path)?.length(); + let closed = path.subpaths.first().is_some_and(|s| s.closed); + let mut out = vec![0usize]; + for i in 1..k.saturating_sub(1) { + let s = total * i as f64 / (k - 1) as f64; + let spine = Spine::new(path)?; + let sp = path.subpaths.first_mut()?; + let n = sp.segment_count(); + let seg = (0..n).rev().find(|i| spine.anchor_length(*i).is_some_and(|l| l <= s)).unwrap_or(0); + let from = spine.anchor_length(seg).unwrap_or(0.0); + let to = spine.anchor_length(seg + 1).unwrap_or(spine.length()); + out.push(if s - from < 0.01 { + seg + } else if to - s < 0.01 { + (seg + 1) % sp.anchors.len().max(1) + } else { + let c = sp.segment(seg); + let arc = c.arclen(1e-4).max(1e-12); + let t = c.inv_arclen((s - from) / (to - from).max(1e-12) * arc, 1e-4).clamp(1e-4, 1.0 - 1e-4); + sp.insert_anchor(seg, t) + }); + } + let last = path.subpaths.first().map_or(0, |sp| if closed { 0 } else { sp.anchors.len().saturating_sub(1) }); + out.push(last); + Some(out) + } + + /// Spreading keys along a spine measured once and kept up to date gives the same anchors and + /// the same spine as measuring it again for each key: on open and closed polylines and curves, + /// zero-length segments, keys exactly on anchors, keys within, at and just past a hundredth of + /// a point from an anchor, a closed curve whose keys go round past its first anchor, many keys + /// in one segment, and paths with no segment. The closed spines open as [`pin_spine`] opens + /// them. + #[test] + fn spreading_keys_matches_measuring_the_spine_for_each_key() { + let p = Point::new; + let bow = SubPath::new( + vec![Anchor::smooth(p(0.0, 0.0), p(50.0, 50.0)), Anchor::smooth(p(100.0, 0.0), p(150.0, -50.0)), Anchor::corner(p(200.0, 0.0))], + false, + ); + let mut spines = vec![ + PathData::default(), + PathData::single(SubPath::default()), + PathData::single(SubPath::polyline(&[p(5.0, 5.0)], false)), + PathData::single(SubPath::polyline(&[p(5.0, 5.0), p(5.0, 5.0)], false)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(100.0, 0.0)], false)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(100.0, 0.0)], true)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(10.0, 0.0), p(20.0, 0.0), p(30.0, 0.0)], false)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(10.0, 0.0), p(10.0, 0.0), p(10.0, 0.0), p(30.0, 5.0), p(30.0, 5.0)], false)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(0.0, 0.0), p(40.0, 0.0), p(40.0, 30.0)], true)), + PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(40.0, 0.0), p(40.0, 30.0), p(0.0, 0.0)], true)), + PathData::single(bow.clone()), + PathData::single(SubPath { closed: true, ..bow }), + shapes::ellipse(Rect::new(0.0, 0.0, 120.0, 80.0)), + ]; + // An anchor near where the middle of three keys goes, 10 along a spine 20 long. + for d in [0.0, 0.005, 0.0099, 0.01, 0.0101, 0.02] { + for x in [10.0 - d, 10.0 + d] { + spines.push(PathData::single(SubPath::polyline(&[p(0.0, 0.0), p(x, 0.0), p(20.0, 0.0)], false))); + } + } + let mut compared = 0; + for spine in &spines { + for k in (0..=12).chain([17, 50, 200]) { + let (mut a, mut b) = (spine.clone(), spine.clone()); + let (ka, kb) = (spread_anchors(&mut a, k), rebuilt_spread_anchors(&mut b, k)); + assert_eq!(ka, kb, "{k} keys on {spine:?}"); + assert_eq!(a, b, "{k} keys on {spine:?}"); + if let (Some(mut ka), Some(mut kb)) = (ka, kb) { + let (mut sa, mut sb) = (a.subpaths[0].clone(), b.subpaths[0].clone()); + open_at_start(&mut sa, &mut ka); + open_at_start(&mut sb, &mut kb); + assert_eq!((sa, ka), (sb, kb), "{k} keys on {spine:?}, opened"); + compared += 1; + } + } + } + assert!(compared > 200, "{compared} spreads compared"); + } + + /// Spreading 1,000 keys along a spine of 4,096 segments flattens the spine once and then only + /// the two halves of each split segment, finds each key's segment in a few comparisons, and + /// measures about as many segments as the spine ends up with, not the whole spine per key. + #[test] + fn spreading_keys_measures_the_spine_once() { + let pts: Vec = (0..=4096).map(|i| Point::new(i as f64, if i % 2 == 0 { 0.0 } else { 1.0 })).collect(); + let mut path = PathData::single(SubPath::polyline(&pts, false)); + let (mut whole, mut segments, mut measured, mut compared) = (0, 0, 0, 0); + let ka = spread_anchors_with(&mut path, 1000, |w| match w { + SpreadWork::WholePath => whole += 1, + SpreadWork::Segment => segments += 1, + SpreadWork::Measure => measured += 1, + SpreadWork::Compare => compared += 1, + }) + .unwrap(); + assert_eq!(ka.len(), 1000); + let n = path.subpaths[0].segment_count(); + assert!(n > 4096 + 900, "{n} segments"); + assert_eq!(whole, 1); + assert!(segments <= 2 * 998, "{segments} segments flattened"); + assert!(compared <= 998 * 14, "{compared} comparisons"); + assert!(measured <= 4096 + n + 3 * 1000, "{measured} segments measured for {n}"); + let mut reference = PathData::single(SubPath::polyline(&pts, false)); + assert_eq!(rebuilt_spread_anchors(&mut reference, 1000), Some(ka)); + assert_eq!(reference, path); + } + + /// A spine so large that its length overflows spreads no keys, and is left as it was, rather + /// than getting an anchor split at a parameter that is not a number. + #[test] + fn spreading_keys_along_a_spine_whose_length_overflows_changes_nothing() { + let p = Point::new; + for closed in [false, true] { + let spine = PathData::single(SubPath::polyline(&[p(-1e308, 0.0), p(0.0, 0.0), p(1e308, 0.0)], closed)); + let mut path = spine.clone(); + assert_eq!(spread_anchors(&mut path, 3), None); + assert_eq!(path, spine); + assert!(path.subpaths[0].anchors.iter().all(|a| a.p.x.is_finite() && a.h_in.x.is_finite() && a.h_out.x.is_finite())); + } + } + + /// When a later arc length has no anchor (here one that is not finite, as when a spine's length + /// overflows only after earlier splits), the splits already made are dropped with it. + #[test] + fn a_failed_anchor_keeps_none_of_the_earlier_splits() { + let sp = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0)], false); + for bad in [f64::NAN, f64::INFINITY] { + let mut split = sp.clone(); + let lengths = SpreadLengths::new(&split, &mut |_| {}); + assert_eq!(anchors_at_lengths(&mut split, lengths, [25.0, 50.0, bad], &mut |_| {}), None); + assert_eq!(split, sp); + } + let mut split = sp.clone(); + let lengths = SpreadLengths::new(&split, &mut |_| {}); + assert_eq!(anchors_at_lengths(&mut split, lengths, [25.0, 50.0], &mut |_| {}), Some(vec![1, 2])); + assert_eq!(split.anchors.len(), 4); + } + #[test] fn easing_keeps_the_ends_and_stays_monotone() { use crate::live::{BlendEase, BlendEaseType as K}; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f7782bdfcd..d3906e911b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1838,7 +1838,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Blend widgets without a spine:** a blend made with Create Spine set to Never shows the Appearance widget alone with the Selection and Blend tools, and its drag sets Shift as on a blend with a spine; there is no Easing widget, as the [Blend panel help page](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blend-panel-overview.html) makes Easing available only on a blend with a spine. The widget sits 16 px above the halfway point, by length, of the straight lines between the key centers that the steps are laid out along, and a drag follows those lines' direction there; the lines are not drawn. Showing the widget without a spine and its placement are VectorCraft's own rules. Evidence: `blendwidget::tests::{a_blend_without_a_spine_shows_the_appearance_widget_alone, dragging_the_appearance_widget_of_a_blend_without_a_spine}`, `tests::blendopts::dragging_the_appearance_widget_of_a_blend_without_a_spine_is_one_undo_step`, `blend::tests::a_blend_without_a_spine_has_a_layout_line_through_its_key_centers`. -- **Positions along a blend spine:** blend steps, keys spread along a spine, the blend widgets and Objects on Path find the spine segment for each arc length with a binary search over the anchors' arc lengths, so a spine of 4,096 segments takes at most 13 comparisons per position instead of one per segment before it. Points and tangents are unchanged, including at anchors shared by zero-length segments. Evidence: `blend::tests::{spine_lookup_compares_a_few_segment_ends, spine_lookup_matches_walking_the_segments}`. +- **Positions along a blend spine:** blend steps, the blend widgets and Objects on Path find the spine segment for each arc length with a binary search over the anchors' arc lengths, so a spine of 4,096 segments takes at most 13 comparisons per position instead of one per segment before it. Points and tangents are unchanged, including at anchors shared by zero-length segments. Evidence: `blend::tests::{spine_lookup_compares_a_few_segment_ends, spine_lookup_matches_walking_the_segments}`. + +- **Keys spread along a replaced spine:** when the keys of a blend whose spine came from Replace Spine get anchors of their own (a key moved off the spine, or an edit of the spine), the spine is flattened and measured once. Each split measures again only the segments from the split to the next key, and a binary search finds each key's segment. 1,000 keys on a spine of 4,096 segments flatten the spine once instead of 999 times. The keys' anchors and the spine are the same as before. A spine so large that its length overflows gets no anchors and is left as it was. Evidence: `blend::tests::{spreading_keys_matches_measuring_the_spine_for_each_key, spreading_keys_measures_the_spine_once, spreading_keys_along_a_spine_whose_length_overflows_changes_nothing, a_failed_anchor_keeps_none_of_the_earlier_splits}`. - **Blend, mesh and envelope numbers given as text:** `object.blend.make` and `object.blend.options` (`steps`, `distance`, `value`), `object.envelope.makeWithWarp`, `resetWithWarp` and `options` (`bend`, `h`, `v`, `horizontalDistortion`, `verticalDistortion`), `object.envelope.makeWithMesh` and `resetWithMesh` (`rows`, `cols`, `columns`), `object.envelope.options` (`fidelity`), `object.mesh.create` (`rows`, `cols`, `columns`, `highlight`) and `object.mesh.setPointColor` (`opacity`) refuse a value other than a number or null, such as `"8"`, and change nothing. `object.mesh.create` refuses an `at` that is not `[x, y]`. From 8091e4861e1ce9ee05ba2622d3d8b8d164caf394 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:47:15 -0800 Subject: [PATCH 2/2] Blend: a spine whose length overflows gives two keys no anchors Spreading two keys along a spine whose length overflows splits nothing, so it returned the end anchors, and a closed spine was then opened at its start. spread_anchors now rejects a length that is not finite before it makes any target, so two keys get no anchors and the path stays as it was, as for three or more keys. --- crates/doc/src/blend.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/doc/src/blend.rs b/crates/doc/src/blend.rs index 462786b93d..3f41bf38c3 100644 --- a/crates/doc/src/blend.rs +++ b/crates/doc/src/blend.rs @@ -1481,8 +1481,8 @@ fn open_at_start(sp: &mut SubPath, ka: &mut [usize]) { /// The anchors of `path`'s first subpath that `k` keys spread evenly along it by arc length sit /// on, splitting segments where none is. `None`, leaving `path` as it was, when the subpath has no -/// length to spread along, or when an arc length it would split at is not finite (a path so large -/// its length overflows). +/// length to spread along, when its length is not finite (a path so large its length overflows), +/// or when an arc length it would split at is not finite. fn spread_anchors(path: &mut PathData, k: usize) -> Option> { spread_anchors_with(path, k, |_| {}) } @@ -1491,7 +1491,7 @@ fn spread_anchors(path: &mut PathData, k: usize) -> Option> { fn spread_anchors_with(path: &mut PathData, k: usize, mut work: impl FnMut(SpreadWork)) -> Option> { let sp = path.subpaths.first_mut()?; let lengths = SpreadLengths::new(sp, &mut work); - let total = lengths.total()?; + let total = lengths.total().filter(|t| t.is_finite())?; let targets = (1..k.saturating_sub(1)).map(|i| total * i as f64 / (k - 1) as f64); let mut out = vec![0usize]; out.extend(anchors_at_lengths(sp, lengths, targets, &mut work)?); @@ -2278,16 +2278,19 @@ mod tests { } /// A spine so large that its length overflows spreads no keys, and is left as it was, rather - /// than getting an anchor split at a parameter that is not a number. + /// than getting an anchor split at a parameter that is not a number. Two keys, which split + /// nothing, get no anchors either. #[test] fn spreading_keys_along_a_spine_whose_length_overflows_changes_nothing() { let p = Point::new; for closed in [false, true] { - let spine = PathData::single(SubPath::polyline(&[p(-1e308, 0.0), p(0.0, 0.0), p(1e308, 0.0)], closed)); - let mut path = spine.clone(); - assert_eq!(spread_anchors(&mut path, 3), None); - assert_eq!(path, spine); - assert!(path.subpaths[0].anchors.iter().all(|a| a.p.x.is_finite() && a.h_in.x.is_finite() && a.h_out.x.is_finite())); + for k in [2, 3, 5] { + let spine = PathData::single(SubPath::polyline(&[p(-1e308, 0.0), p(0.0, 0.0), p(1e308, 0.0)], closed)); + let mut path = spine.clone(); + assert_eq!(spread_anchors(&mut path, k), None, "closed {closed}, {k} keys"); + assert_eq!(path, spine); + assert!(path.subpaths[0].anchors.iter().all(|a| a.p.x.is_finite() && a.h_in.x.is_finite() && a.h_out.x.is_finite())); + } } }