Blend: a vanished spine tangent takes the curve's direction nearby; the unreachable copy after the loop goes

This commit is contained in:
bflatastic
2026-11-09 04:45:17 +07:00
parent 2709a5191f
commit 863023d7de
+22 -24
View File
@@ -1022,34 +1022,16 @@ impl Spine {
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
};
let tv = c.deriv().eval(t).to_vec2();
// Where a handle sits on its anchor the derivative vanishes there: take the
// direction between points a little before and after instead.
let tv = if tv.hypot2() < 1e-24 { c.eval((t + 1e-3).min(1.0)) - c.eval((t - 1e-3).max(0.0)) } else { tv };
return (Point::new(e.x, e.y), tv.y.atan2(tv.x));
}
start = end;
}
let c = sp.segment(n - 1);
let end = self.length();
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
};
(Point::new(e.x, e.y), tv.y.atan2(tv.x))
// Only a spine without segments gets here: the loop returns at the last one.
(self.pts.first().copied().unwrap_or_default(), 0.0)
}
}
@@ -1503,6 +1485,22 @@ mod tests {
}
}
/// Where the spine starts at an anchor whose handle sits on it, the first step still faces along
/// the curve (towards the next handle), not along a direction of zero length.
#[test]
fn a_handle_on_its_anchor_still_gives_the_curve_direction() {
let spine = PathData::single(SubPath::new(
vec![Anchor::corner(Point::new(0.0, 0.0)), Anchor::smooth(Point::new(100.0, 0.0), Point::new(150.0, -50.0))],
false,
));
let sp = Spine::new(&spine).unwrap();
let (p, angle) = sp.at_length(0.0);
assert!(p.x.abs() < 1e-6 && p.y.abs() < 1e-6, "{p:?}");
assert!((angle - std::f64::consts::FRAC_PI_4).abs() < 0.05, "faces the in handle at (50, 50): {angle}");
let (_, end) = sp.at_length(sp.length());
assert!(end.is_finite(), "{end}");
}
#[test]
fn easing_keeps_the_ends_and_stays_monotone() {
use crate::live::{BlendEase, BlendEaseType as K};