use crate::prelude::*;
use beet_core::prelude::*;
use std::f32::consts::PI;
use std::f32::consts::TAU;
pub type Radians = f32;
#[derive(Debug, Clone, Copy, Reflect)]
pub struct IkArm4Dof {
pub base_offset_angle: Radians,
pub segment1: IkSegment,
pub segment2: IkSegment,
pub segment3: IkSegment,
pub arm_style: IkArmStyle,
}
impl Default for IkArm4Dof {
fn default() -> Self {
Self {
base_offset_angle: 0.0,
segment1: IkSegment::DEG_360,
segment2: IkSegment::DEG_360,
segment3: IkSegment::DEG_360.with_len(0.2),
arm_style: default(),
}
}
}
#[derive(Debug, Default, Clone, Copy, Reflect)]
pub enum IkArmStyle {
#[default]
Overarm,
Underarm,
}
impl IkArm4Dof {
pub fn new(
base_offset_angle: Radians,
segment1: IkSegment,
segment2: IkSegment,
segment3: IkSegment,
) -> Self {
Self {
base_offset_angle,
segment1,
segment2,
segment3,
arm_style: default(),
}
}
pub fn reach(&self) -> f32 { self.segment1.len + self.segment2.len }
pub fn solve4d(&self, delta_pos: Vec3) -> (f32, f32, f32, f32) {
let angles = self.solve3d(delta_pos);
let theta4 = TAU - angles.1 - angles.2;
(angles.0, angles.1, angles.2, theta4)
}
pub fn solve3d(&self, delta_pos: Vec3) -> (f32, f32, f32) {
let delta_pos_flat = Vec2::new(delta_pos.x, delta_pos.z);
let angle_base = f32::atan2(delta_pos_flat.y, delta_pos_flat.x);
let hypotenuse = delta_pos_flat.length();
let (angle_segment1, angle_segment2) =
self.solve2d(Vec2::new(hypotenuse, delta_pos.y));
(
angle_base + self.base_offset_angle,
angle_segment1,
angle_segment2,
)
}
pub fn solve2d(&self, mut target: Vec2) -> (f32, f32) {
let (l1, l2) = (self.segment1.len, self.segment2.len);
let reach = self.reach();
if target.length_squared() > reach.powi(2) {
target = target.normalize_or_zero() * (reach * 0.999);
}
let is_neg = target.x < 0.0;
if is_neg {
target.x = -target.x;
target.y = -target.y;
};
let cos_angle2 =
(target.x.powi(2) + target.y.powi(2) - l1.powi(2) - l2.powi(2))
/ (2.0 * l1 * l2);
let angle2 = match self.arm_style {
IkArmStyle::Overarm => -cos_angle2.acos(),
IkArmStyle::Underarm => cos_angle2.acos(),
}
.clamp(self.segment2.min_angle, self.segment2.max_angle);
let k1 = l1 + l2 * angle2.cos();
let k2 = l2 * angle2.sin();
let angle1 = (target.y.atan2(target.x) - k2.atan2(k1))
.clamp(self.segment1.min_angle, self.segment1.max_angle);
if is_neg {
(angle1 + PI, angle2)
} else {
(angle1, angle2)
}
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use std::f32::consts::PI;
#[test]
fn test_reachable_target() {
let ik_solver = IkArm4Dof::default();
let target = Vec2::new(1.0, 1.0);
let (angle1, angle2) = ik_solver.solve2d(target);
assert!((angle1).abs() <= PI);
assert!((angle2).abs() <= PI);
}
#[test]
fn test_unreachable_target_too_far() {
let ik_solver = IkArm4Dof::default();
let target = Vec2::new(3.0, 3.0);
let (angle1, angle2) = ik_solver.solve2d(target);
assert_eq!((angle1, angle2), (0.8301215, -0.089446634));
}
}