#[allow(dead_code)]
fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[allow(dead_code)]
fn vec3_add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[allow(dead_code)]
fn vec3_scale(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[allow(dead_code)]
fn vec3_len(v: [f32; 3]) -> f32 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[allow(dead_code)]
fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
let len = vec3_len(v);
if len < 1e-8 {
[0.0, 1.0, 0.0]
} else {
vec3_scale(v, 1.0 / len)
}
}
#[allow(dead_code)]
fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[allow(dead_code)]
fn vec3_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[derive(Debug, Clone)]
pub struct IkResult {
pub mid_pos: [f32; 3],
pub end_pos: [f32; 3],
pub reached: bool,
pub root_angle: f32,
pub mid_angle: f32,
}
pub fn solve_2bone_ik(
root: [f32; 3],
target: [f32; 3],
upper_len: f32,
lower_len: f32,
hint: Option<[f32; 3]>,
) -> IkResult {
let to_target = vec3_sub(target, root);
let original_d = vec3_len(to_target);
let total_len = upper_len + lower_len;
let min_len = (upper_len - lower_len).abs();
let d = original_d.clamp(min_len, total_len);
let reached = original_d <= total_len;
let dir = if original_d < 1e-8 {
[0.0, 0.0, 1.0]
} else {
vec3_normalize(to_target)
};
let cos_alpha = ((upper_len * upper_len + d * d - lower_len * lower_len)
/ (2.0 * upper_len * d))
.clamp(-1.0, 1.0);
let alpha = cos_alpha.acos();
let cos_mid = ((upper_len * upper_len + lower_len * lower_len - d * d)
/ (2.0 * upper_len * lower_len))
.clamp(-1.0, 1.0);
let mid_angle = cos_mid.acos();
let pole = hint.unwrap_or([0.0, 1.0, 0.0]);
let pole_norm = vec3_normalize(pole);
let dot = vec3_dot(pole_norm, dir);
let perp_raw = vec3_sub(pole_norm, vec3_scale(dir, dot));
let perp = if vec3_len(perp_raw) < 1e-6 {
let alt = if dir[0].abs() < 0.9 {
[1.0_f32, 0.0, 0.0]
} else {
[0.0_f32, 1.0, 0.0]
};
let dot2 = vec3_dot(alt, dir);
vec3_normalize(vec3_sub(alt, vec3_scale(dir, dot2)))
} else {
vec3_normalize(perp_raw)
};
let mid_along = vec3_scale(dir, upper_len * cos_alpha);
let mid_perp = vec3_scale(perp, upper_len * alpha.sin());
let mid_pos = vec3_add(root, vec3_add(mid_along, mid_perp));
let end_pos = if reached {
target
} else {
vec3_add(root, vec3_scale(dir, total_len))
};
IkResult {
mid_pos,
end_pos,
reached,
root_angle: alpha,
mid_angle,
}
}
#[derive(Debug, Clone)]
pub struct IkJoint {
pub position: [f32; 3],
pub bone_length: f32,
}
pub fn fabrik_solve(
joints: &[IkJoint],
target: [f32; 3],
iterations: usize,
tolerance: f32,
) -> Vec<[f32; 3]> {
if joints.is_empty() {
return Vec::new();
}
if joints.len() == 1 {
return vec![joints[0].position];
}
let n = joints.len();
let mut positions: Vec<[f32; 3]> = joints.iter().map(|j| j.position).collect();
let root = positions[0];
let bone_lengths: Vec<f32> = joints.iter().map(|j| j.bone_length).collect();
let total_len: f32 = bone_lengths[..n - 1].iter().sum();
let to_target = vec3_sub(target, root);
let dist_to_target = vec3_len(to_target);
if dist_to_target > total_len {
let dir = vec3_normalize(to_target);
let mut acc = 0.0_f32;
positions[0] = root;
for i in 1..n {
acc += bone_lengths[i - 1];
positions[i] = vec3_add(root, vec3_scale(dir, acc));
}
return positions;
}
for _iter in 0..iterations {
positions[n - 1] = target;
for i in (0..n - 1).rev() {
let dir = vec3_normalize(vec3_sub(positions[i], positions[i + 1]));
positions[i] = vec3_add(positions[i + 1], vec3_scale(dir, bone_lengths[i]));
}
positions[0] = root;
for i in 0..n - 1 {
let dir = vec3_normalize(vec3_sub(positions[i + 1], positions[i]));
positions[i + 1] = vec3_add(positions[i], vec3_scale(dir, bone_lengths[i]));
}
let end_dist = vec3_len(vec3_sub(positions[n - 1], target));
if end_dist < tolerance {
break;
}
}
positions
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-3;
#[test]
fn two_bone_ik_straight_reach() {
let root = [0.0, 0.0, 0.0];
let target = [5.0, 0.0, 0.0];
let result = solve_2bone_ik(root, target, 3.0, 2.0, None);
assert!(result.reached, "should be reachable");
let dx = result.end_pos[0] - target[0];
let dy = result.end_pos[1] - target[1];
let dz = result.end_pos[2] - target[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
assert!(dist < EPS, "end_pos should match target; dist={dist}");
}
#[test]
fn two_bone_ik_reachable_target() {
let root = [0.0, 0.0, 0.0];
let target = [2.0, 0.0, 0.0];
let result = solve_2bone_ik(root, target, 3.0, 2.0, None);
assert!(result.reached, "target at distance 2 should be reachable");
}
#[test]
fn two_bone_ik_unreachable_target() {
let root = [0.0, 0.0, 0.0];
let target = [100.0, 0.0, 0.0]; let result = solve_2bone_ik(root, target, 3.0, 2.0, None);
assert!(!result.reached, "target at 100 should be unreachable");
let d = vec3_len(vec3_sub(result.end_pos, root));
assert!(
(d - 5.0).abs() < EPS,
"end_pos should be at total length (5); d={d}"
);
}
#[test]
fn two_bone_ik_end_pos_near_target_when_reachable() {
let root = [0.0, 0.0, 0.0];
let target = [3.0, 2.0, 1.0];
let result = solve_2bone_ik(root, target, 4.0, 3.0, None);
assert!(result.reached);
let dist = vec3_len(vec3_sub(result.end_pos, target));
assert!(
dist < EPS,
"end_pos should equal target when reachable; dist={dist}"
);
}
#[test]
fn two_bone_ik_mid_pos_correct_distance_from_root() {
let root = [0.0, 0.0, 0.0];
let target = [3.0, 0.0, 0.0];
let upper = 3.0_f32;
let lower = 2.0_f32;
let result = solve_2bone_ik(root, target, upper, lower, None);
let d_root = vec3_len(vec3_sub(result.mid_pos, root));
assert!(
(d_root - upper).abs() < EPS,
"mid_pos should be upper_len from root; d={d_root}"
);
}
#[test]
fn two_bone_ik_mid_pos_correct_distance_from_end() {
let root = [0.0, 0.0, 0.0];
let target = [3.0, 0.0, 0.0];
let upper = 3.0_f32;
let lower = 2.0_f32;
let result = solve_2bone_ik(root, target, upper, lower, None);
let d_end = vec3_len(vec3_sub(result.mid_pos, result.end_pos));
assert!(
(d_end - lower).abs() < EPS,
"mid_pos should be lower_len from end; d={d_end}"
);
}
#[test]
fn two_bone_ik_equal_bones() {
let root = [0.0, 0.0, 0.0];
let target = [3.0, 0.0, 0.0];
let result = solve_2bone_ik(root, target, 2.0, 2.0, None);
assert!(result.reached, "distance 3 should be reachable with 2+2=4");
let d_root = vec3_len(vec3_sub(result.mid_pos, root));
let d_end = vec3_len(vec3_sub(result.mid_pos, result.end_pos));
assert!(
(d_root - 2.0).abs() < EPS,
"upper bone length wrong; d={d_root}"
);
assert!(
(d_end - 2.0).abs() < EPS,
"lower bone length wrong; d={d_end}"
);
}
#[test]
fn two_bone_ik_with_hint_bends_correctly() {
let root = [0.0, 0.0, 0.0];
let target = [4.0, 0.0, 0.0];
let result_up = solve_2bone_ik(root, target, 3.0, 2.0, Some([0.0, 1.0, 0.0]));
let result_fwd = solve_2bone_ik(root, target, 3.0, 2.0, Some([0.0, 0.0, 1.0]));
assert!(result_up.reached);
assert!(result_fwd.reached);
assert!(
result_up.mid_pos[1] > 0.0,
"Y-hint should push elbow in +Y; mid_pos={:?}",
result_up.mid_pos
);
assert!(
result_fwd.mid_pos[2] > 0.0,
"Z-hint should push elbow in +Z; mid_pos={:?}",
result_fwd.mid_pos
);
}
fn make_joints(positions: &[[f32; 3]], bone_len: f32) -> Vec<IkJoint> {
positions
.iter()
.map(|&position| IkJoint {
position,
bone_length: bone_len,
})
.collect()
}
#[test]
fn fabrik_solve_two_joints_reaches_target() {
let joints = make_joints(&[[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [1.0, 0.0, 0.0]], 0.5);
let target = [0.6, 0.4, 0.0];
let result = fabrik_solve(&joints, target, 30, 1e-4);
let end_dist = vec3_len(vec3_sub(*result.last().expect("should succeed"), target));
assert!(
end_dist < 0.01,
"2-segment FABRIK should reach target; dist={end_dist}"
);
}
#[test]
fn fabrik_solve_three_joints_near_target() {
let joints = make_joints(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], 1.0);
let target = [1.5, 0.5, 0.0];
let result = fabrik_solve(&joints, target, 20, 1e-4);
let end_dist = vec3_len(vec3_sub(*result.last().expect("should succeed"), target));
assert!(
end_dist < 0.01,
"3-joint FABRIK should reach target; dist={end_dist}"
);
}
#[test]
fn fabrik_solve_unreachable_target_returns_stretched() {
let joints = make_joints(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], 1.0);
let target = [100.0, 0.0, 0.0];
let result = fabrik_solve(&joints, target, 10, 1e-4);
let end = result.last().expect("should succeed");
let d_from_root = vec3_len(vec3_sub(*end, result[0]));
assert!(
(d_from_root - 2.0).abs() < EPS,
"stretched chain should reach total length from root; d={d_from_root}"
);
assert!(end[0] > 0.0, "stretched chain should point toward target");
}
#[test]
fn fabrik_solve_preserves_bone_lengths() {
let joints = make_joints(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
],
1.0,
);
let target = [1.5, 1.5, 0.0];
let result = fabrik_solve(&joints, target, 20, 1e-5);
for i in 0..result.len() - 1 {
let d = vec3_len(vec3_sub(result[i + 1], result[i]));
assert!(
(d - 1.0).abs() < EPS,
"bone length between joints {i} and {} should be 1.0; d={d}",
i + 1
);
}
}
#[test]
fn fabrik_solve_result_length_matches_joints() {
let joints = make_joints(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
],
1.0,
);
let target = [2.0, 2.0, 0.0];
let result = fabrik_solve(&joints, target, 10, 1e-4);
assert_eq!(
result.len(),
joints.len(),
"result length should match joints length"
);
}
}