use glam::Vec2;
use crate::{
cacl::affine::decompose_affine,
data::runtime::skeleton::{BoneIdx, Pose, Skeleton},
};
#[derive(Debug, Clone)]
pub struct IkConstraintData {
pub bones: Vec<BoneIdx>,
pub target: BoneIdx,
pub mix: f32,
pub bend_positive: bool,
pub compress: bool,
pub stretch: bool,
pub order: u32,
}
#[derive(Debug, Clone)]
pub struct TransformConstraintData {
pub bones: Vec<BoneIdx>,
pub target: BoneIdx,
pub mix_rotate: f32,
pub mix_x: f32,
pub mix_y: f32,
pub mix_scale_x: f32,
pub mix_scale_y: f32,
pub mix_shear_y: f32,
pub offset_rotation: f32,
pub offset_x: f32,
pub offset_y: f32,
pub offset_scale_x: f32,
pub offset_scale_y: f32,
pub offset_shear_y: f32,
pub order: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintIdx {
Ik(u32),
Transform(u32),
}
#[derive(Debug, Clone, Default)]
pub struct ConstraintSet {
pub ik: Vec<IkConstraintData>,
pub transform: Vec<TransformConstraintData>,
pub order: Vec<ConstraintIdx>,
}
impl ConstraintSet {
pub fn new_state(&self) -> ConstraintState {
ConstraintState {
ik: self
.ik
.iter()
.map(|c| IkState {
mix: c.mix,
bend_positive: c.bend_positive,
compress: c.compress,
stretch: c.stretch,
})
.collect(),
transform: self
.transform
.iter()
.map(|c| TransformState {
mix_rotate: c.mix_rotate,
mix_x: c.mix_x,
mix_y: c.mix_y,
mix_scale_x: c.mix_scale_x,
mix_scale_y: c.mix_scale_y,
mix_shear_y: c.mix_shear_y,
})
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IkState {
pub mix: f32,
pub bend_positive: bool,
pub compress: bool,
pub stretch: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TransformState {
pub mix_rotate: f32,
pub mix_x: f32,
pub mix_y: f32,
pub mix_scale_x: f32,
pub mix_scale_y: f32,
pub mix_shear_y: f32,
}
#[derive(Debug, Clone, Default)]
pub struct ConstraintState {
pub ik: Vec<IkState>,
pub transform: Vec<TransformState>,
}
pub fn apply_constraints(
pose: &mut Pose,
skeleton: &Skeleton,
set: &ConstraintSet,
state: &ConstraintState,
) {
for c in &set.order {
match c {
ConstraintIdx::Ik(i) => {
apply_ik(pose, skeleton, &set.ik[*i as usize], &state.ik[*i as usize])
}
ConstraintIdx::Transform(i) => apply_transform(
pose,
skeleton,
&set.transform[*i as usize],
&state.transform[*i as usize],
),
}
}
}
fn wrap_deg(a: f32) -> f32 {
let mut a = a % 360.0;
if a > 180.0 {
a -= 360.0;
} else if a < -180.0 {
a += 360.0;
}
a
}
fn apply_ik(pose: &mut Pose, skeleton: &Skeleton, ik: &IkConstraintData, state: &IkState) {
let alpha = state.mix;
if alpha <= 0.0 {
return;
}
let target_pos = pose.get_world_affine(skeleton, ik.target).translation;
let bend_dir = if state.bend_positive { 1.0f32 } else { -1.0f32 };
match ik.bones.len() {
1 => apply_ik_1(
pose,
skeleton,
ik.bones[0],
target_pos,
state.compress,
state.stretch,
alpha,
),
2 => apply_ik_2(
pose,
skeleton,
ik.bones[0],
ik.bones[1],
target_pos,
bend_dir,
state.stretch,
alpha,
),
_ => {}
}
}
fn apply_ik_1(
pose: &mut Pose,
skeleton: &Skeleton,
bone: BoneIdx,
target_pos: Vec2,
compress: bool,
stretch: bool,
alpha: f32,
) {
let b = pose.bone_data[bone].clone();
let parent_world = match skeleton.data_static[bone].parent_index {
Some(pi) => pose.get_world_affine(skeleton, pi),
None => glam::Affine2::IDENTITY,
};
let pa = parent_world.matrix2.x_axis;
let pb = parent_world.matrix2.y_axis;
let p = pa.x * pb.y - pa.y * pb.x; let x = target_pos.x - parent_world.translation.x;
let y = target_pos.y - parent_world.translation.y;
let (tx, ty) = if p.abs() <= 1e-4 {
(0.0, 0.0)
} else {
(
(x * pb.y - y * pb.x) / p - b.translate_x,
(y * pa.x - x * pa.y) / p - b.translate_y,
)
};
let mut rotation_ik = -b.shear_x_deg - b.rotation_deg + ty.atan2(tx).to_degrees();
if b.scale_x < 0.0 {
rotation_ik += 180.0;
}
rotation_ik = wrap_deg(rotation_ik);
let mut sx = b.scale_x;
let sy = b.scale_y;
if compress || stretch {
let b_len = skeleton.data_static[bone].length * sx;
if b_len > 1e-4 {
let tx_w = target_pos.x - pose.get_world_affine(skeleton, bone).translation.x;
let ty_w = target_pos.y - pose.get_world_affine(skeleton, bone).translation.y;
let dd = tx_w * tx_w + ty_w * ty_w;
if (compress && dd < b_len * b_len) || (stretch && dd > b_len * b_len) {
let s = ((dd.sqrt() / b_len - 1.0) * alpha + 1.0).max(1e-4);
sx *= s; }
}
}
let d = &mut pose.bone_data[bone];
d.rotation_deg += rotation_ik * alpha;
d.scale_x = sx;
d.scale_y = sy;
pose.invalidate_world(skeleton, bone);
}
#[allow(clippy::too_many_arguments)]
fn apply_ik_2(
pose: &mut Pose,
skeleton: &Skeleton,
parent: BoneIdx,
child: BoneIdx,
target_pos: Vec2,
bend_dir: f32,
stretch: bool,
alpha: f32,
) {
use crate::data::runtime::skeleton::InheritMode;
let parent_static = &skeleton.data_static[parent];
let child_static = &skeleton.data_static[child];
if parent_static.inherit_mode != InheritMode::Normal
|| child_static.inherit_mode != InheritMode::Normal
{
return;
}
let p = pose.bone_data[parent].clone();
let c = pose.bone_data[child].clone();
let px = p.translate_x;
let py = p.translate_y;
let mut psx = p.scale_x;
let mut psy = p.scale_y;
let mut sx = psx;
let sy = psy;
let csx = c.scale_x;
let (os1, mut s2) = if psx < 0.0 {
psx = -psx;
(180.0f32, -1.0f32)
} else {
(0.0, 1.0)
};
if psy < 0.0 {
psy = -psy;
s2 = -s2;
}
let os2 = if csx < 0.0 { 180.0 } else { 0.0 };
let cx = c.translate_x;
let parent_world = pose.get_world_affine(skeleton, parent);
let a = parent_world.matrix2.x_axis.x;
let b_ = parent_world.matrix2.y_axis.x;
let c_ = parent_world.matrix2.x_axis.y;
let d_ = parent_world.matrix2.y_axis.y;
let uniform = (psx - psy).abs() <= 1e-4;
let cy;
let (cwx, cwy) = if !uniform || stretch {
cy = 0.0;
(
a * cx + parent_world.translation.x,
c_ * cx + parent_world.translation.y,
)
} else {
cy = c.translate_y;
(
a * cx + b_ * c.translate_y + parent_world.translation.x,
c_ * cx + d_ * c.translate_y + parent_world.translation.y,
)
};
let pp = match parent_static.parent_index {
Some(pi) => pose.get_world_affine(skeleton, pi),
None => glam::Affine2::IDENTITY,
};
let pa = pp.matrix2.x_axis.x;
let pb = pp.matrix2.y_axis.x;
let pc = pp.matrix2.x_axis.y;
let pd = pp.matrix2.y_axis.y;
let det = pa * pd - pb * pc;
let id = if det.abs() <= 1e-4 { 0.0 } else { 1.0 / det };
let x = cwx - pp.translation.x;
let y = cwy - pp.translation.y;
let dx = (x * pd - y * pb) * id - px;
let dy = (y * pa - x * pc) * id - py;
let l1 = (dx * dx + dy * dy).sqrt();
let mut l2 = child_static.length * csx;
if l1 < 1e-4 {
apply_ik_1(pose, skeleton, parent, target_pos, false, stretch, alpha);
let cd = &mut pose.bone_data[child];
cd.rotation_deg = 0.0;
pose.invalidate_world(skeleton, child);
return;
}
let x = target_pos.x - pp.translation.x;
let y = target_pos.y - pp.translation.y;
let tx = (x * pd - y * pb) * id - px;
let ty = (y * pa - x * pc) * id - py;
let dd = tx * tx + ty * ty;
let rad = std::f32::consts::PI;
let (a1, a2, stretch_scale);
if uniform {
l2 *= psx;
let cos = (dd - l1 * l1 - l2 * l2) / (2.0 * l1 * l2);
if cos < -1.0 {
a2 = rad * bend_dir;
} else if cos > 1.0 {
a2 = 0.0;
} else {
a2 = cos.acos() * bend_dir;
}
let a = l1 + l2 * a2.cos();
let b = l2 * a2.sin();
a1 = (ty * a - tx * b).atan2(tx * a + ty * b);
stretch_scale = if stretch && cos > 1.0 {
(dd.sqrt() / (l1 + l2) - 1.0) * alpha + 1.0
} else {
1.0
};
} else {
let a = psx * l2;
let b = psy * l2;
let aa = a * a;
let bb = b * b;
let ta = ty.atan2(tx);
let c = bb * l1 * l1 + aa * dd - aa * bb;
let c1 = -2.0 * bb * l1;
let c2 = bb - aa;
let d = c1 * c1 - 4.0 * c2 * c;
let (mut a1v, mut a2v, mut solved) = (0.0f32, 0.0f32, false);
if d >= 0.0 {
let mut q = d.sqrt();
if c1 < 0.0 {
q = -q;
}
q = -(c1 + q) * 0.5;
let r0 = q / c2;
let r1 = c / q;
let r = if r0.abs() < r1.abs() { r0 } else { r1 };
let r0 = dd - r * r;
if r0 >= 0.0 {
let y = r0.sqrt() * bend_dir;
a1v = ta - y.atan2(r);
a2v = (y / psy).atan2((r - l1) / psx);
solved = true;
}
}
if !solved {
let mut min_angle = rad;
let mut min_x = l1 - a;
let mut min_dist = min_x * min_x;
let mut min_y = 0.0;
let mut max_angle = 0.0;
let mut max_x = l1 + a;
let mut max_dist = max_x * max_x;
let mut max_y = 0.0;
let cc = -a * l1 / (aa - bb);
if (-1.0..=1.0).contains(&cc) {
let cc = cc.acos();
let xv = a * cc.cos() + l1;
let yv = b * cc.sin();
let d2 = xv * xv + yv * yv;
if d2 < min_dist {
min_angle = cc;
min_dist = d2;
min_x = xv;
min_y = yv;
}
if d2 > max_dist {
max_angle = cc;
max_dist = d2;
max_x = xv;
max_y = yv;
}
}
if dd <= (min_dist + max_dist) * 0.5 {
a1v = ta - (min_y * bend_dir).atan2(min_x);
a2v = min_angle * bend_dir;
} else {
a1v = ta - (max_y * bend_dir).atan2(max_x);
a2v = max_angle * bend_dir;
}
}
a1 = a1v;
a2 = a2v;
stretch_scale = 1.0;
}
let os = cy.atan2(cx) * s2;
let mut a1_delta = (a1 - os).to_degrees() + os1 - p.rotation_deg;
a1_delta = wrap_deg(a1_delta);
if stretch_scale != 1.0 {
sx *= stretch_scale; }
let pd_ = &mut pose.bone_data[parent];
pd_.rotation_deg += a1_delta * alpha;
pd_.scale_x = sx;
pd_.scale_y = sy;
pd_.shear_x_deg = 0.0;
pd_.shear_y_deg = 0.0;
let mut a2_delta = ((a2 + os).to_degrees() - c.shear_x_deg) * s2 + os2 - c.rotation_deg;
a2_delta = wrap_deg(a2_delta);
let cd = &mut pose.bone_data[child];
cd.rotation_deg += a2_delta * alpha;
pose.invalidate_world(skeleton, parent);
pose.invalidate_world(skeleton, child);
}
fn apply_transform(
pose: &mut Pose,
skeleton: &Skeleton,
tc: &TransformConstraintData,
state: &TransformState,
) {
use crate::data::runtime::skeleton::InheritMode;
if state.mix_rotate == 0.0
&& state.mix_x == 0.0
&& state.mix_y == 0.0
&& state.mix_scale_x == 0.0
&& state.mix_scale_y == 0.0
&& state.mix_shear_y == 0.0
{
return;
}
let target_world = pose.get_world_affine(skeleton, tc.target);
let ta = target_world.matrix2.x_axis.x;
let tb = target_world.matrix2.y_axis.x;
let tc_ = target_world.matrix2.x_axis.y;
let td = target_world.matrix2.y_axis.y;
let reflect = if ta * td - tb * tc_ > 0.0 {
1.0f32
} else {
-1.0f32
};
let off_r = (tc.offset_rotation * reflect).to_radians();
let off_sy = (tc.offset_shear_y * reflect).to_radians();
for &bone in &tc.bones {
let b = pose.bone_data[bone].clone();
let mut w = pose.get_world_affine(skeleton, bone);
if state.mix_rotate != 0.0 {
let a = w.matrix2.x_axis.x;
let c = w.matrix2.x_axis.y;
let mut r = off_r + tc_.atan2(ta) - c.atan2(a);
wrap_rad(&mut r);
r *= state.mix_rotate;
let (cos, sin) = (r.cos(), r.sin());
let xa = w.matrix2.x_axis;
let ya = w.matrix2.y_axis;
w.matrix2.x_axis = Vec2::new(cos * xa.x - sin * xa.y, sin * xa.x + cos * xa.y);
w.matrix2.y_axis = Vec2::new(cos * ya.x - sin * ya.y, sin * ya.x + cos * ya.y);
}
if state.mix_x != 0.0 || state.mix_y != 0.0 {
let off_world = target_world.transform_point2(Vec2::new(tc.offset_x, tc.offset_y));
w.translation.x += (off_world.x - w.translation.x) * state.mix_x;
w.translation.y += (off_world.y - w.translation.y) * state.mix_y;
}
if state.mix_scale_x != 0.0 {
let s = w.matrix2.x_axis.length();
if s != 0.0 {
let ts = Vec2::new(ta, tc_).length();
let ns = (s + (ts - s + tc.offset_scale_x) * state.mix_scale_x) / s;
w.matrix2.x_axis *= ns;
}
}
if state.mix_scale_y != 0.0 {
let s = w.matrix2.y_axis.length();
if s != 0.0 {
let ts = Vec2::new(tb, td).length();
let ns = (s + (ts - s + tc.offset_scale_y) * state.mix_scale_y) / s;
w.matrix2.y_axis *= ns;
}
}
if state.mix_shear_y > 0.0 {
let by = w.matrix2.y_axis.y.atan2(w.matrix2.y_axis.x);
let mut r =
td.atan2(tb) - tc_.atan2(ta) - (by - w.matrix2.x_axis.y.atan2(w.matrix2.x_axis.x));
wrap_rad(&mut r);
r = by + (r + off_sy) * state.mix_shear_y;
let s = w.matrix2.y_axis.length();
w.matrix2.y_axis = Vec2::new(r.cos() * s, r.sin() * s);
}
let parent_world = match skeleton.data_static[bone].parent_index {
Some(pi) => pose.get_world_affine(skeleton, pi),
None => glam::Affine2::IDENTITY,
};
let d = &mut pose.bone_data[bone];
if b.inherit_mode != InheritMode::Normal {
let local = parent_world.inverse() * w;
let (lx, ly, lr, lsx, lsy, _, lshy) = decompose_affine(&local);
d.translate_x = lx;
d.translate_y = ly;
d.rotation_deg = lr;
d.scale_x = lsx;
d.scale_y = lsy;
d.shear_x_deg = 0.0;
d.shear_y_deg = lshy;
pose.invalidate_world(skeleton, bone);
continue;
}
let pa = parent_world.matrix2.x_axis.x;
let pb = parent_world.matrix2.y_axis.x;
let pc = parent_world.matrix2.x_axis.y;
let pd = parent_world.matrix2.y_axis.y;
let det = pa * pd - pb * pc;
if det.abs() <= 1e-4 {
continue;
}
let pid = 1.0 / det;
let dx = w.translation.x - parent_world.translation.x;
let dy = w.translation.y - parent_world.translation.y;
let lx = (dx * pd - dy * pb) * pid;
let ly = (dy * pa - dx * pc) * pid;
let a = w.matrix2.x_axis.x;
let b_ = w.matrix2.y_axis.x;
let c_ = w.matrix2.x_axis.y;
let dd_ = w.matrix2.y_axis.y;
let ia = pid * pd;
let ib = pid * pb;
let ic = pid * pc;
let id = pid * pa;
let ra = ia * a - ib * c_;
let rb = ia * b_ - ib * dd_;
let rc = id * c_ - ic * a;
let rd = id * dd_ - ic * b_;
d.translate_x = lx;
d.translate_y = ly;
d.rotation_deg = rc.atan2(ra).to_degrees();
let lsx = (ra * ra + rc * rc).sqrt();
if lsx > 1e-4 {
let det_l = ra * rd - rb * rc;
d.scale_x = lsx;
d.scale_y = det_l / lsx;
d.shear_x_deg = 0.0;
d.shear_y_deg = -(ra * rb + rc * rd).atan2(det_l).to_degrees();
} else {
d.scale_x = 0.0;
d.scale_y = (rb * rb + rd * rd).sqrt();
d.shear_x_deg = 0.0;
d.shear_y_deg = 0.0;
}
pose.invalidate_world(skeleton, bone);
}
}
fn wrap_rad(a: &mut f32) {
let mut a_ = *a % (std::f32::consts::PI * 2.0);
if a_ > std::f32::consts::PI {
a_ -= std::f32::consts::PI * 2.0;
} else if a_ < -std::f32::consts::PI {
a_ += std::f32::consts::PI * 2.0;
}
*a = a_;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
data::runtime::skeleton::{BoneDataStatic, Skeleton},
utils::default,
};
fn build_ik_skeleton() -> Skeleton {
let mut s = Skeleton::new();
s.insert_bone("root", default()).unwrap();
let p = BoneDataStatic {
length: 100.0,
parent_index: s.get_bone_index("root"),
..Default::default()
};
s.insert_bone("p", p).unwrap();
let c = BoneDataStatic {
length: 80.0,
translate_x: 100.0,
parent_index: s.get_bone_index("p"),
..Default::default()
};
s.insert_bone("c", c).unwrap();
let t = BoneDataStatic {
parent_index: s.get_bone_index("root"),
..Default::default()
};
s.insert_bone("t", t).unwrap();
s.build_children_map();
s.build_roots();
s
}
fn pose(skeleton: &Skeleton) -> Pose {
skeleton.default_pose()
}
#[test]
fn ik_two_bone_reaches_target() {
let skeleton = build_ik_skeleton();
let mut pose = pose(&skeleton);
pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_x = 150.0;
pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_y = 60.0;
let ik = IkConstraintData {
bones: vec![
skeleton.get_bone_index("p").unwrap(),
skeleton.get_bone_index("c").unwrap(),
],
target: skeleton.get_bone_index("t").unwrap(),
mix: 1.0,
bend_positive: true,
compress: false,
stretch: false,
order: 0,
};
let set = ConstraintSet {
ik: vec![ik],
transform: vec![],
order: vec![ConstraintIdx::Ik(0)],
};
let state = set.new_state();
apply_constraints(&mut pose, &skeleton, &set, &state);
let c_world = pose.get_world_affine(&skeleton, skeleton.get_bone_index("c").unwrap());
let end = c_world.transform_point2(Vec2::new(80.0, 0.0));
let target = pose.get_world_affine(&skeleton, skeleton.get_bone_index("t").unwrap());
let d = (end - target.translation).length();
assert!(
d < 1.0,
"IK 末端 {end:?} 应到达 target {:?},偏差 {d}",
target.translation
);
}
#[test]
fn ik_mix_zero_no_effect() {
let skeleton = build_ik_skeleton();
let mut pose = pose(&skeleton);
pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_x = 150.0;
let before = pose.bone_data[skeleton.get_bone_index("p").unwrap()].rotation_deg;
let ik = IkConstraintData {
bones: vec![
skeleton.get_bone_index("p").unwrap(),
skeleton.get_bone_index("c").unwrap(),
],
target: skeleton.get_bone_index("t").unwrap(),
mix: 0.0,
bend_positive: true,
compress: false,
stretch: false,
order: 0,
};
let set = ConstraintSet {
ik: vec![ik],
transform: vec![],
order: vec![ConstraintIdx::Ik(0)],
};
let state = set.new_state();
apply_constraints(&mut pose, &skeleton, &set, &state);
let after = pose.bone_data[skeleton.get_bone_index("p").unwrap()].rotation_deg;
assert_eq!(before, after);
}
#[test]
fn ik_reaches_various_targets() {
let skeleton = build_ik_skeleton();
let (pi, ci, ti) = (
skeleton.get_bone_index("p").unwrap(),
skeleton.get_bone_index("c").unwrap(),
skeleton.get_bone_index("t").unwrap(),
);
let cases: [(f32, f32, bool, bool, bool); 5] = [
(150.0, 60.0, true, true, false), (150.0, 60.0, true, false, true), (-120.0, -70.0, true, true, false), (60.0, -170.0, false, true, false), (300.0, 200.0, false, true, false), ];
for (tx, ty, reachable, bend_pos, expect_ccw) in cases {
let mut pose = pose(&skeleton);
pose.bone_data[ti].translate_x = tx;
pose.bone_data[ti].translate_y = ty;
let ik = IkConstraintData {
bones: vec![pi, ci],
target: ti,
mix: 1.0,
bend_positive: bend_pos,
compress: false,
stretch: false,
order: 0,
};
let set = ConstraintSet {
ik: vec![ik],
transform: vec![],
order: vec![ConstraintIdx::Ik(0)],
};
let state = set.new_state();
apply_constraints(&mut pose, &skeleton, &set, &state);
let c_world = pose.get_world_affine(&skeleton, ci);
let end = c_world.transform_point2(Vec2::new(80.0, 0.0));
let target = pose.get_world_affine(&skeleton, ti).translation;
let d = (end - target).length();
if reachable {
assert!(
d < 1.0,
"target ({tx},{ty}) bend={bend_pos}: 末端 {end:?} 应到达 {target:?},偏差 {d}"
);
let p_world = pose.get_world_affine(&skeleton, pi).translation;
let m = pose
.get_world_affine(&skeleton, pi)
.transform_point2(Vec2::new(100.0, 0.0));
let to_t = target - p_world;
let to_m = m - p_world;
let cross = to_t.x * to_m.y - to_t.y * to_m.x;
let ccw = cross > 0.0;
assert_eq!(
ccw, expect_ccw,
"target ({tx},{ty}) bend={bend_pos}: 弯曲侧不符 cross={cross}"
);
} else {
let from_p = end - pose.get_world_affine(&skeleton, pi).translation;
let dist = from_p.length();
assert!(
(dist - 180.0).abs() < 1.0,
"target ({tx},{ty}): 不可达应完全伸直,末端距 {dist}(期望 180)"
);
let dir_t = (target - pose.get_world_affine(&skeleton, pi).translation).normalize();
let dir_e = from_p.normalize();
assert!(
(dir_t - dir_e).length() < 1e-2,
"target ({tx},{ty}): 不可达方向应指向 target"
);
}
}
}
#[test]
fn ik_matches_official_runtime_values() {
let skeleton = build_ik_skeleton();
let (pi, ci, ti) = (
skeleton.get_bone_index("p").unwrap(),
skeleton.get_bone_index("c").unwrap(),
skeleton.get_bone_index("t").unwrap(),
);
for (bend_pos, exp_p, exp_c) in [(true, -1.388, 52.68), (false, 44.99, -52.68)] {
let mut pose = pose(&skeleton);
pose.bone_data[ti].translate_x = 150.0;
pose.bone_data[ti].translate_y = 60.0;
let ik = IkConstraintData {
bones: vec![pi, ci],
target: ti,
mix: 1.0,
bend_positive: bend_pos,
compress: false,
stretch: false,
order: 0,
};
let set = ConstraintSet {
ik: vec![ik],
transform: vec![],
order: vec![ConstraintIdx::Ik(0)],
};
let state = set.new_state();
apply_constraints(&mut pose, &skeleton, &set, &state);
let rp = pose.bone_data[pi].rotation_deg;
let rc = pose.bone_data[ci].rotation_deg;
assert!(
(rp - exp_p).abs() < 0.02 && (rc - exp_c).abs() < 0.02,
"bend_positive={bend_pos}: parent={rp} (期望 {exp_p}), child={rc} (期望 {exp_c})"
);
}
}
#[test]
fn transform_follows_target_rotation() {
let mut skeleton = Skeleton::new();
skeleton.insert_bone("root", default()).unwrap();
let target = BoneDataStatic {
parent_index: skeleton.get_bone_index("root"),
..Default::default()
};
skeleton.insert_bone("target", target).unwrap();
let bone = BoneDataStatic {
parent_index: skeleton.get_bone_index("root"),
rotation_deg: 10.0,
..Default::default()
};
skeleton.insert_bone("bone", bone).unwrap();
skeleton.build_children_map();
skeleton.build_roots();
let mut pose = skeleton.default_pose();
pose.bone_data[skeleton.get_bone_index("target").unwrap()].rotation_deg = 30.0;
let tc = TransformConstraintData {
bones: vec![skeleton.get_bone_index("bone").unwrap()],
target: skeleton.get_bone_index("target").unwrap(),
mix_rotate: 1.0,
mix_x: 0.0,
mix_y: 0.0,
mix_scale_x: 0.0,
mix_scale_y: 0.0,
mix_shear_y: 0.0,
offset_rotation: 0.0,
offset_x: 0.0,
offset_y: 0.0,
offset_scale_x: 1.0,
offset_scale_y: 1.0,
offset_shear_y: 0.0,
order: 0,
};
let set = ConstraintSet {
ik: vec![],
transform: vec![tc],
order: vec![ConstraintIdx::Transform(0)],
};
let state = set.new_state();
apply_constraints(&mut pose, &skeleton, &set, &state);
let r = pose.bone_data[skeleton.get_bone_index("bone").unwrap()].rotation_deg;
assert!((r - 30.0).abs() < 0.01, "bone 旋转应跟随 target: {r}");
}
}