M2.1: blend Align-to-Path smooth on curved spines

Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.

Fix: store the original PathData in Spine and compute the tangent from
the original cubic at the true arc-length position via ParamCurveDeriv,
proportionally mapping flattened arc-length s onto each segment's curve
arclength using anchor_length values, instead of using piecewise-constant
chord directions.

Updated segment-selection condition in Spine::at_length to skip segments
whose flattened end is not greater than start before checking whether s is
within the segment. Preserve the existing final-segment fallback so
at_length(0.0) selects a nonzero segment and uses its tangent.

Added zero-derivative fallback tangent for blend-step sampling to keep
Align to Path from passing a zero tangent to atan2.

Added missing docstrings for Spine::new and Spine::length to satisfy
docstring coverage requirements.

Co-authored-by: Craft Bot <bot@storytold.dev>
This commit is contained in:
adambelis
2026-11-09 04:42:48 +07:00
committed by bflatastic
co-authored by Craft Bot
parent cb5300415a
commit 2709a5191f
+16 -4
View File
@@ -1018,15 +1018,19 @@ impl Spine {
for i in 0..n {
let c = sp.segment(i);
let end = self.anchor_length(i + 1).unwrap_or(self.length());
if end <= start {
// Skip segments whose flattened end is not greater than start
continue;
}
if s <= end || i + 1 == n {
let local_s = (s - start).max(0.0) / (end - start).max(1e-12) * c.arclen(1e-9);
let t = c.inv_arclen(local_s, 1e-9).clamp(1e-9, 1.0 - 1e-9);
let e = c.eval(t);
let tv = c.deriv().eval(t);
// Handle zero curve derivative: use limiting tangent from nearby parameter
let tv = if tv.x.abs() < 1e-12 && tv.y.abs() < 1e-12 {
// Use tangent at adjacent parameter as fallback
let t2 = t.clamp(0.5, 0.5).max(t.min(1.0 - 1e-3).min(1.0 - 1e-3)).clamp(1e-3, 1.0 - 1e-3);
c.deriv().eval(t2)
} else {
tv
};
return (Point::new(e.x, e.y), tv.y.atan2(tv.x));
}
start = end;
@@ -1037,6 +1041,14 @@ impl Spine {
let t = c.inv_arclen(local_s, 1e-9).clamp(1e-9, 1.0 - 1e-9);
let e = c.eval(t);
let tv = c.deriv().eval(t);
// Handle zero curve derivative: use limiting tangent from nearby parameter
let tv = if tv.x.abs() < 1e-12 && tv.y.abs() < 1e-12 {
// Use tangent at adjacent parameter as fallback
let t2 = t.clamp(0.5, 0.5).max(t.min(1.0 - 1e-3).min(1.0 - 1e-3)).clamp(1e-3, 1.0 - 1e-3);
c.deriv().eval(t2)
} else {
tv
};
(Point::new(e.x, e.y), tv.y.atan2(tv.x))
}
}