Merge pull request #1090 from vangel76/fix-1081-orient-along-path-flip

This commit is contained in:
Brandon Thomas committed 2026-10-13 04:29:05 -04:00
commit 5249ff8bad
2 files changed
+45 -5

No files matched your search

+22 -5
View File
@@ -212,18 +212,35 @@ pub fn rig_rotation(ctx: &EvalCtx, layer: &Layer) -> Mat4 {
// ---------------------------------------------------------------- auto-orient
/// Direction of travel of a layer's position (parent space) at the context time.
///
/// Where the position is not moving (at and after the last keyframe, before the first, or while
/// holding) the last valid direction of travel is kept, so the layer does not snap back to its
/// unrotated pose at the end of a path.
fn path_tangent(ctx: &EvalCtx, layer: &Layer) -> Option<Vec3> {
let tr = layer.transform()?;
// Position (or its separated X/Y/Z dimensions) must be animated.
let moving = ["position", "positionX", "positionY", "positionZ"].iter().any(|m| tr.get(m).is_some_and(|p| p.keys.len() > 1 || p.has_expression()));
let dims = ["position", "positionX", "positionY", "positionZ"];
let moving = dims.iter().any(|m| tr.get(m).is_some_and(|p| p.keys.len() > 1 || p.has_expression()));
if !moving {
return None;
}
let dt = effectcraft_time::Tick::from_seconds_f64(0.005);
let a = Vec3::from(ctx.at(ctx.time - dt).v3(layer, tr, "position", [0.0; 3]));
let b = Vec3::from(ctx.at(ctx.time + dt).v3(layer, tr, "position", [0.0; 3]));
let d = b - a;
(d.length() > 1e-9).then(|| d.normalize())
let pos = |t| Vec3::from(ctx.at(t).v3(layer, tr, "position", [0.0; 3]));
let dir = |d: Vec3| (d.length() > 1e-9 && d.length().is_finite()).then(|| d.normalize());
if let Some(d) = dir(pos(ctx.time + dt) - pos(ctx.time - dt)) {
return Some(d);
}
// No motion here: fall back on the nearest motion before this time (a backward difference at
// each earlier key, latest first), then on the first motion after it.
let mut keys: Vec<effectcraft_time::Tick> = dims.iter().filter_map(|m| tr.get(m)).flat_map(|p| p.keys.iter().map(|k| layer.comp_time(k.time))).collect();
keys.sort();
keys.dedup();
keys.iter()
.rev()
.filter(|k| **k <= ctx.time)
.find_map(|&k| dir(pos(k) - pos(k - dt)))
.or_else(|| keys.iter().filter(|k| **k > ctx.time).find_map(|&k| dir(pos(k + dt) - pos(k))))
}
/// Auto-Orient ▸ Orient Along Path for 2D layers: extra Z rotation in degrees.
+23
View File
@@ -55,6 +55,29 @@ fn orient_along_path_2d_follows_motion() {
assert!(x_axis.x.abs() < 1e-6 && (x_axis.y - 1.0).abs() < 1e-6, "{x_axis:?}");
}
/// #1081: a layer travelling left kept its heading until the last keyframe, then (no motion left
/// to measure) snapped back to 0 degrees and looked flipped. It must keep its last direction of
/// travel at the final key and after it, in 2D and 3D.
#[test]
fn orient_along_path_keeps_the_last_heading_at_and_after_the_end() {
for three_d in [false, true] {
let (mut p, cid, comp) = setup();
let mut l = solid(&mut p, &comp, three_d, [150.0, 50.0, 0.0]);
l.auto_orient = AutoOrient::AlongPath;
let end = Tick::from_seconds_f64(1.0);
l.props.prop_mut("transform/position").unwrap().keys =
vec![Keyframe::new(Tick::ZERO, Value::Vec3([150.0, 50.0, 0.0])), Keyframe::new(end, Value::Vec3([50.0, 50.0, 0.0]))];
p.comp_mut(cid).unwrap().layers.push(l);
let c = p.comp(cid).unwrap();
for secs in [0.5, 0.999, 1.0, 1.2, 1.9] {
let ctx = EvalCtx::new(&p, cid, c, Tick::from_seconds_f64(secs));
let x_axis = ctx.world_matrix(&c.layers[0]).apply_vec(vec3(1.0, 0.0, 0.0));
// Moving left (-x): the layer's x axis keeps pointing left.
assert!((x_axis.x + 1.0).abs() < 1e-6 && x_axis.y.abs() < 1e-6 && x_axis.z.abs() < 1e-6, "3d={three_d} t={secs}: {x_axis:?}");
}
}
}
#[test]
fn layers_parented_to_a_camera_follow_its_rotation() {
let (mut p, cid, comp) = setup();