tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-anim/src/constraint.rs Rust · 182 siq'ikuna · 6.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Constraints de rigging sobre la jerarquía de huesos (estilo Rive): IK de 2
2//! huesos y aim. Corren **después** de que la animación posó el esqueleto,
3//! ajustando rotaciones para satisfacer un objetivo; luego el skinning deforma.
4//!
5//! Convención: los huesos apuntan a lo largo de su eje local **+x** (su hijo se
6//! traslada en +x, el efector está en +x) — la misma que arma `skel`. Todo en
7//! 2D, analítico (sin iteración), sobre `kurbo`.
8
9use kurbo::{Affine, Point, Vec2};
10
11use crate::skel::{BoneId, Pose, Skeleton};
12
13/// Ángulo de rotación de un `Affine` (asume escala ~uniforme y sin shear).
14fn affine_rotation(aff: Affine) -> f64 {
15    let c = aff.as_coeffs(); // [a, b, c, d, e, f]; a=cosθ·sx, b=sinθ·sx
16    c[1].atan2(c[0])
17}
18
19/// Origen (punto de pivote) de un hueso en mundo. La rotación del propio hueso
20/// no mueve su origen, así que sirve aunque vayamos a re-rotarlo.
21fn bone_origin(skel: &Skeleton, bone: BoneId) -> Point {
22    skel.world(bone) * Point::ZERO
23}
24
25fn parent_rotation(skel: &Skeleton, bone: BoneId) -> f64 {
26    match skel.parent(bone) {
27        Some(p) => affine_rotation(skel.world(p)),
28        None => 0.0,
29    }
30}
31
32/// Resuelve **IK de 2 huesos**: ajusta las rotaciones locales de `upper` (padre)
33/// y `lower` (hijo de `upper`) para que el efector — el punto `tip_local` en el
34/// frame de `lower` — alcance `target` (en espacio de mundo del esqueleto).
35/// `flip` elige la solución del codo (arriba/abajo). Si `target` está fuera de
36/// alcance, el brazo se estira hacia él (alcance máximo). Llama `update` al final.
37///
38/// Requiere que los world transforms estén actualizados antes (la animación ya
39/// posó el esqueleto). Las longitudes salen del rig: `L1` = offset `upper→lower`,
40/// `L2` = `|tip_local|`.
41pub fn solve_two_bone_ik(
42    skel: &mut Skeleton,
43    upper: BoneId,
44    lower: BoneId,
45    tip_local: Vec2,
46    target: Point,
47    flip: bool,
48) {
49    let base = bone_origin(skel, upper);
50    let l1 = skel.pose(lower).translation.hypot();
51    let l2 = tip_local.hypot();
52    if l1 < 1e-9 || l2 < 1e-9 {
53        return;
54    }
55    let s = if flip { -1.0 } else { 1.0 };
56
57    let d = target - base;
58    let dist = d.hypot().clamp((l1 - l2).abs() + 1e-6, l1 + l2 - 1e-6);
59    let alpha = d.y.atan2(d.x);
60    let cos_beta = ((dist * dist + l1 * l1 - l2 * l2) / (2.0 * dist * l1)).clamp(-1.0, 1.0);
61    let beta = cos_beta.acos();
62    let cos_elbow = ((l1 * l1 + l2 * l2 - dist * dist) / (2.0 * l1 * l2)).clamp(-1.0, 1.0);
63    let elbow = cos_elbow.acos();
64
65    let theta_upper_world = alpha - s * beta;
66    let theta_lower_local = s * (std::f64::consts::PI - elbow);
67
68    let up = skel.pose(upper);
69    skel.set_pose(
70        upper,
71        Pose {
72            rotation: theta_upper_world - parent_rotation(skel, upper),
73            ..up
74        },
75    );
76    let low = skel.pose(lower);
77    skel.set_pose(
78        lower,
79        Pose {
80            rotation: theta_lower_local,
81            ..low
82        },
83    );
84    skel.update();
85}
86
87/// **Aim**: rota `bone` para que su eje local `forward` apunte desde su origen
88/// hacia `target` (mundo). Llama `update`. Útil para que una cabeza/ojo/torreta
89/// siga a un objetivo.
90pub fn aim_at(skel: &mut Skeleton, bone: BoneId, forward: Vec2, target: Point) {
91    let origin = bone_origin(skel, bone);
92    let d = target - origin;
93    if d.hypot() < 1e-9 || forward.hypot() < 1e-9 {
94        return;
95    }
96    let desired_world = d.y.atan2(d.x) - forward.y.atan2(forward.x);
97    let pose = skel.pose(bone);
98    skel.set_pose(
99        bone,
100        Pose {
101            rotation: desired_world - parent_rotation(skel, bone),
102            ..pose
103        },
104    );
105    skel.update();
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::skel::Pose;
112
113    fn two_bone(l1: f64) -> (Skeleton, BoneId, BoneId) {
114        let mut s = Skeleton::new();
115        let a = s.add_bone(None, Pose::identity());
116        let b = s.add_bone(Some(a), Pose::translate(Vec2::new(l1, 0.0)));
117        s.bind();
118        s.update();
119        (s, a, b)
120    }
121
122    fn tip(skel: &Skeleton, lower: BoneId, tip_local: Vec2) -> Point {
123        skel.world(lower) * Point::new(tip_local.x, tip_local.y)
124    }
125
126    #[test]
127    fn ik_alcanza_un_objetivo_dentro_de_rango() {
128        let (mut s, a, b) = two_bone(50.0);
129        let tip_local = Vec2::new(50.0, 0.0); // L2 = 50, alcance 0..100
130        let target = Point::new(60.0, 40.0); // dist ≈ 72 < 100
131        solve_two_bone_ik(&mut s, a, b, tip_local, target, false);
132        let t = tip(&s, b, tip_local);
133        assert!(
134            (t.x - target.x).abs() < 1e-6 && (t.y - target.y).abs() < 1e-6,
135            "tip {t:?} debería alcanzar {target:?}"
136        );
137    }
138
139    #[test]
140    fn ik_fuera_de_rango_se_estira() {
141        let (mut s, a, b) = two_bone(50.0);
142        let tip_local = Vec2::new(50.0, 0.0);
143        let target = Point::new(300.0, 0.0); // muy lejos → estira a ~100 en x
144        solve_two_bone_ik(&mut s, a, b, tip_local, target, false);
145        let t = tip(&s, b, tip_local);
146        assert!((t.x - 100.0).abs() < 1e-3, "x del tip {} ~ 100", t.x);
147        assert!(t.y.abs() < 1e-3, "y del tip {} ~ 0 (en línea al objetivo)", t.y);
148    }
149
150    #[test]
151    fn ik_flip_da_el_otro_codo() {
152        let (mut s, a, b) = two_bone(50.0);
153        let tip_local = Vec2::new(50.0, 0.0);
154        let target = Point::new(60.0, 40.0);
155
156        solve_two_bone_ik(&mut s, a, b, tip_local, target, false);
157        let elbow_down = bone_origin(&s, b);
158        solve_two_bone_ik(&mut s, a, b, tip_local, target, true);
159        let elbow_up = bone_origin(&s, b);
160
161        // Ambas soluciones alcanzan el target, pero el codo cae en lados
162        // opuestos de la línea base→target.
163        assert!(
164            (elbow_down.y - elbow_up.y).abs() > 1.0,
165            "los codos deberían diferir: {elbow_down:?} vs {elbow_up:?}"
166        );
167        let t = tip(&s, b, tip_local);
168        assert!((t.x - target.x).abs() < 1e-6 && (t.y - target.y).abs() < 1e-6);
169    }
170
171    #[test]
172    fn aim_apunta_el_eje_forward_al_objetivo() {
173        let mut s = Skeleton::new();
174        let bone = s.add_bone(None, Pose::identity());
175        s.bind();
176        s.update();
177        aim_at(&mut s, bone, Vec2::new(1.0, 0.0), Point::new(0.0, 10.0));
178        // El eje +x del hueso ahora debe apuntar hacia +y (al objetivo).
179        let dir = s.world(bone) * Point::new(1.0, 0.0);
180        assert!(dir.x.abs() < 1e-9 && dir.y > 0.0, "dir {dir:?} debería ser +y");
181    }
182}