mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Merge pull request #7247 from dotCooCoo/fix/blend-spine-measured-once
Blend: spreading keys along a replaced spine measures it once
This commit is contained in:
+336
-28
@@ -1464,7 +1464,7 @@ pub fn pin_spine(keys: &[Arc<Node>], 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<Node>], spine: &Spine) -> bool {
|
||||
let k = keys.len();
|
||||
keys.iter().enumerate().all(|(i, key)| {
|
||||
@@ -1489,40 +1489,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, 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<Vec<usize>> {
|
||||
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<Vec<usize>> {
|
||||
let sp = path.subpaths.first_mut()?;
|
||||
let lengths = SpreadLengths::new(sp, &mut work);
|
||||
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];
|
||||
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<usize> {
|
||||
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<Item = f64>,
|
||||
work: &mut impl FnMut(SpreadWork),
|
||||
) -> Option<Vec<usize>> {
|
||||
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<Point>,
|
||||
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<Point>,
|
||||
/// 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<std::ops::Range<usize>>,
|
||||
/// 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<Measured>,
|
||||
}
|
||||
|
||||
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<usize> {
|
||||
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<f64> {
|
||||
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<usize> {
|
||||
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`]).
|
||||
@@ -2012,6 +2176,150 @@ 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<Vec<usize>> {
|
||||
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<Point> = (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. 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] {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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};
|
||||
|
||||
@@ -1853,7 +1853,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}`.
|
||||
|
||||
- **Objects on Path measures its path once:** placing the objects, finding one object's place and transforming the group flatten each path once and use it both for the path's length and for the points along it, where they flattened it two or three times before; a request for an object past the last one returns nothing without flattening the path, and a move of the group flattens no path. A path whose length adds up past the largest finite number no longer counts as one objects can be attached to. Evidence: `objects_on_path::tests::{stops_and_stop_flatten_the_path_once, transforming_the_group_flattens_each_path_once, placing_the_objects_flattens_the_path_once, a_path_too_long_to_measure_is_not_attachable}` in `crates/doc`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user