use crate::math::{fm, Quat, Vec3};
pub fn one_bone(base: Vec3, tip: Vec3, target: Vec3) -> Vec3 {
let len = base.distance(tip);
let d = (target - base).normalized();
if d == Vec3::ZERO {
tip
} else {
base + d * len
}
}
pub fn two_bone(
base: Vec3,
mid: Vec3,
tip: Vec3,
target: Vec3,
pole: Option<Vec3>,
) -> (Vec3, Vec3) {
let upper = base.distance(mid); let lower = mid.distance(tip); let to_target = target - base;
let dist = to_target.length();
if dist < 1e-9 || upper < 1e-9 || lower < 1e-9 {
return (mid, tip);
}
let dir = to_target * (1.0 / dist);
if dist >= upper + lower {
let m = base + dir * upper;
return (m, m + dir * lower);
}
let min_reach = (upper - lower).abs().max(1e-6);
let d = dist.max(min_reach);
let cos_a = ((upper * upper + d * d - lower * lower) / (2.0 * upper * d)).clamp(-1.0, 1.0);
let sin_a = fm::sqrt((1.0 - cos_a * cos_a).max(0.0));
let hint = match pole {
Some(p) => p - base,
None => mid - base,
};
let mut side = hint - dir * hint.dot(dir);
if side.length_squared() < 1e-12 {
side = dir.any_perpendicular();
}
let side = side.normalized();
let m = base + dir * (upper * cos_a) + side * (upper * sin_a);
let t = if dist >= min_reach {
target
} else {
m + (target - m).normalized() * lower
};
(m, t)
}
pub fn segment_rotation(from: Vec3, to: Vec3) -> Quat {
Quat::from_to(from, to)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_bone_keeps_length() {
let tip = one_bone(
Vec3::ZERO,
Vec3::new(0.0, 2.0, 0.0),
Vec3::new(10.0, 0.0, 0.0),
);
assert!((tip - Vec3::new(2.0, 0.0, 0.0)).length() < 1e-5);
}
#[test]
fn two_bone_reaches_target_and_keeps_lengths() {
let (base, mid, tip) = (
Vec3::ZERO,
Vec3::new(0.0, 2.0, 0.0),
Vec3::new(0.0, 5.0, 0.0),
);
let target = Vec3::new(2.0, 2.0, 1.0);
let (m, t) = two_bone(base, mid, tip, target, None);
assert!((t - target).length() < 1e-4);
assert!((base.distance(m) - 2.0).abs() < 1e-4);
assert!((m.distance(t) - 3.0).abs() < 1e-4);
}
#[test]
fn two_bone_unreachable_is_straight() {
let (m, t) = two_bone(
Vec3::ZERO,
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 2.0, 0.0),
Vec3::new(0.0, 0.0, 9.0),
None,
);
assert!((m - Vec3::new(0.0, 0.0, 1.0)).length() < 1e-5);
assert!((t - Vec3::new(0.0, 0.0, 2.0)).length() < 1e-5);
}
#[test]
fn two_bone_pole_picks_bend_side() {
let (base, mid, tip) = (
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
);
let target = Vec3::new(1.2, 0.0, 0.0); let (up, _) = two_bone(base, mid, tip, target, Some(Vec3::new(0.0, 5.0, 0.0)));
let (down, _) = two_bone(base, mid, tip, target, Some(Vec3::new(0.0, -5.0, 0.0)));
assert!(up.y > 0.1 && down.y < -0.1);
assert!((up.y + down.y).abs() < 1e-4);
}
#[test]
fn two_bone_collinear_input_does_not_nan() {
let (m, t) = two_bone(
Vec3::ZERO,
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 2.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
None,
);
assert!(m.x.is_finite() && m.y.is_finite() && m.z.is_finite());
assert!(t.x.is_finite() && t.y.is_finite() && t.z.is_finite());
}
}