tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Sutichaykuna

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-voxel/src/rig.rs Rust · 543 siq'ikuna · 21.4 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//! `rig` — **cuerpo arbitrario articulado** para los Seres: un árbol de partes
2//! (cajas) unidas por articulaciones, que generaliza el muñeco humanoide fijo de
3//! [`Actor`](crate::Actor) a cualquier morfología (cuadrúpedo, ave, serpiente, bicho).
4//! Es la **capa 1** del movimiento; encima montan los *andares* (capa 2, animación
5//! procedural por articulación — ya esbozada aquí con [`Andar`]) y la *conducta*
6//! (capa 3, locomoción) — y, más adelante, movimiento coreografiado/complejo.
7//!
8//! Un [`Segmento`] es una caja que cuelga de un **pivote** (su articulación) en el
9//! marco de su **padre**, y rota sobre un [`Eje`]. El árbol se hornea a una malla
10//! ([`Rig::mesh`]) acumulando las transformaciones padre→hijo: rotar el muslo
11//! arrastra la pantorrilla y el pie. Como el [`Actor`], produce `(Vec<Vertex3d>,
12//! Vec<u16>)` en espacio local (pies en el origen, mirando a `+Z`); no toca GPU.
13//!
14//! Todo es **dato serializable**: una criatura nueva es un `Rig`, no código nuevo.
15
16use llimphi_3d::glam::{Mat4, Vec3};
17use llimphi_3d::{push_cube, Vertex3d};
18use serde::{Deserialize, Serialize};
19
20/// Eje de rotación de una articulación (en el marco local del segmento).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Eje {
23    /// Cabeceo: balanceo hacia adelante/atrás (miembros que caminan).
24    X,
25    /// Giro: rotación horizontal (serpenteo de una serpiente).
26    Y,
27    /// Alabeo: hacia el costado/arriba (aletear de un ave).
28    Z,
29}
30
31impl Eje {
32    fn mat(self, a: f32) -> Mat4 {
33        match self {
34            Eje::X => Mat4::from_rotation_x(a),
35            Eje::Y => Mat4::from_rotation_y(a),
36            Eje::Z => Mat4::from_rotation_z(a),
37        }
38    }
39}
40
41/// De qué color se pinta un segmento. Las tres ranuras mapean a los colores del
42/// [`Sere`](crate::CharSpec) (piel/cuerpo/patas); `Custom` lleva el suyo.
43#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
44pub enum SlotColor {
45    Piel,
46    Cuerpo,
47    Patas,
48    Custom([f32; 3]),
49}
50
51impl SlotColor {
52    fn resolve(self, piel: [f32; 3], cuerpo: [f32; 3], patas: [f32; 3]) -> [f32; 3] {
53        match self {
54            SlotColor::Piel => piel,
55            SlotColor::Cuerpo => cuerpo,
56            SlotColor::Patas => patas,
57            SlotColor::Custom(c) => c,
58        }
59    }
60}
61
62/// Una **parte** del cuerpo: una caja articulada en el árbol.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Segmento {
65    /// Nombre (define el rol para los andares: "pata", "brazo", "ala", "cola"…).
66    pub nombre: String,
67    /// Índice del padre en [`Rig::segmentos`]; `-1` = raíz (anclado a los pies).
68    pub padre: i32,
69    /// Posición de la **articulación** `[x,y,z]` relativa al marco del padre.
70    /// (`[f32;3]` y no `Vec3` para que serialice sin features extra de glam.)
71    pub pivote: [f32; 3],
72    /// Desplazamiento del **centro de la caja** desde el pivote (p.ej. una pata
73    /// cuelga con `centro = [0, -largo/2, 0]`).
74    pub centro: [f32; 3],
75    /// Tamaño de la caja.
76    pub tamaño: [f32; 3],
77    /// Eje sobre el que rota la articulación.
78    pub eje: Eje,
79    /// Color de la caja.
80    pub color: SlotColor,
81}
82
83/// Pose: el **ángulo de cada articulación** (paralelo a [`Rig::segmentos`]).
84#[derive(Debug, Clone, Default)]
85pub struct RigPose {
86    pub angulos: Vec<f32>,
87}
88
89impl RigPose {
90    /// Pose neutra (todos los ángulos en 0) para `n` segmentos.
91    pub fn neutra(n: usize) -> Self {
92        Self { angulos: vec![0.0; n] }
93    }
94}
95
96/// Un **cuerpo**: el árbol de segmentos (ordenado padre-antes-que-hijo).
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Rig {
99    pub nombre: String,
100    pub segmentos: Vec<Segmento>,
101}
102
103impl Rig {
104    /// Cantidad de segmentos.
105    pub fn len(&self) -> usize {
106        self.segmentos.len()
107    }
108    pub fn is_empty(&self) -> bool {
109        self.segmentos.is_empty()
110    }
111
112    /// Transformación de mundo de cada segmento para una pose (acumula padre→hijo):
113    /// `world[s] = world[padre] · T(pivote) · R(eje, ángulo)`. Requiere orden
114    /// topológico (padre antes que hijo), que los presets respetan.
115    fn world_transforms(&self, pose: &RigPose) -> Vec<Mat4> {
116        let mut world = vec![Mat4::IDENTITY; self.segmentos.len()];
117        for (i, s) in self.segmentos.iter().enumerate() {
118            let ang = pose.angulos.get(i).copied().unwrap_or(0.0);
119            let local = Mat4::from_translation(Vec3::from_array(s.pivote)) * s.eje.mat(ang);
120            world[i] = if s.padre < 0 {
121                local
122            } else {
123                world[s.padre as usize] * local
124            };
125        }
126        world
127    }
128
129    /// Hornea la **malla** del cuerpo en la pose dada, con los tres colores del ser.
130    /// Espacio local: pies en el origen, mirando a `+Z` (igual que [`Actor::mesh`]).
131    pub fn mesh(
132        &self,
133        pose: &RigPose,
134        piel: [f32; 3],
135        cuerpo: [f32; 3],
136        patas: [f32; 3],
137    ) -> (Vec<Vertex3d>, Vec<u16>) {
138        let world = self.world_transforms(pose);
139        let mut v = Vec::with_capacity(8 * self.segmentos.len());
140        let mut i = Vec::with_capacity(36 * self.segmentos.len());
141        for (s, w) in self.segmentos.iter().zip(&world) {
142            let m = *w
143                * Mat4::from_translation(Vec3::from_array(s.centro))
144                * Mat4::from_scale(Vec3::from_array(s.tamaño));
145            push_cube(&mut v, &mut i, m, s.color.resolve(piel, cuerpo, patas));
146        }
147        (v, i)
148    }
149}
150
151/// Helper para declarar un segmento conciso (toma `Vec3`, guarda `[f32;3]`).
152fn seg(nombre: &str, padre: i32, pivote: Vec3, centro: Vec3, tamaño: Vec3, eje: Eje, color: SlotColor) -> Segmento {
153    Segmento {
154        nombre: nombre.into(),
155        padre,
156        pivote: pivote.to_array(),
157        centro: centro.to_array(),
158        tamaño: tamaño.to_array(),
159        eje,
160        color,
161    }
162}
163
164/// Caja que **cuelga** de su pivote (miembro): centro a `-largo/2` en Y.
165fn miembro(nombre: &str, padre: i32, pivote: Vec3, largo: f32, grosor: f32, eje: Eje, color: SlotColor) -> Segmento {
166    seg(
167        nombre,
168        padre,
169        pivote,
170        Vec3::new(0.0, -largo * 0.5, 0.0),
171        Vec3::new(grosor, largo, grosor),
172        eje,
173        color,
174    )
175}
176
177impl Rig {
178    /// **Humanoide**: torso + cabeza + 2 brazos + 2 piernas (versión rig del muñeco).
179    pub fn humanoide() -> Rig {
180        let torso_y = 0.8; // cadera (pies en 0)
181        let torso = Vec3::new(0.55, 0.60, 0.30);
182        Rig {
183            nombre: "humanoide".into(),
184            segmentos: vec![
185                // 0: torso (raíz), centrado entre la cadera y los hombros.
186                seg("torso", -1, Vec3::new(0.0, torso_y, 0.0), Vec3::new(0.0, torso.y * 0.5, 0.0), torso, Eje::X, SlotColor::Cuerpo),
187                // 1: cabeza (hija del torso).
188                seg("cabeza", 0, Vec3::new(0.0, torso.y, 0.0), Vec3::new(0.0, 0.22, 0.0), Vec3::new(0.42, 0.40, 0.42), Eje::X, SlotColor::Piel),
189                // 2,3: brazos (hijos del torso, en los hombros).
190                miembro("brazo_r", 0, Vec3::new(0.36, torso.y, 0.0), 0.60, 0.18, Eje::X, SlotColor::Cuerpo),
191                miembro("brazo_l", 0, Vec3::new(-0.36, torso.y, 0.0), 0.60, 0.18, Eje::X, SlotColor::Cuerpo),
192                // 4,5: piernas (raíz, ancladas a la cadera, no heredan el torso).
193                miembro("pierna_r", -1, Vec3::new(0.14, torso_y, 0.0), 0.80, 0.22, Eje::X, SlotColor::Patas),
194                miembro("pierna_l", -1, Vec3::new(-0.14, torso_y, 0.0), 0.80, 0.22, Eje::X, SlotColor::Patas),
195            ],
196        }
197    }
198
199    /// **Cuadrúpedo**: lomo horizontal + cabeza + 4 patas + cola.
200    pub fn cuadrupedo() -> Rig {
201        let alto = 0.7; // altura del lomo (patas de 0.7)
202        let lomo = Vec3::new(0.45, 0.40, 1.10); // largo en Z
203        Rig {
204            nombre: "cuadrúpedo".into(),
205            segmentos: vec![
206                // 0: lomo (raíz), centrado a la altura del lomo.
207                seg("lomo", -1, Vec3::new(0.0, alto, 0.0), Vec3::ZERO, lomo, Eje::X, SlotColor::Cuerpo),
208                // 1: cabeza adelante (+Z) y un poco arriba.
209                seg("cabeza", 0, Vec3::new(0.0, 0.12, lomo.z * 0.5), Vec3::new(0.0, 0.0, 0.22), Vec3::new(0.34, 0.34, 0.40), Eje::X, SlotColor::Piel),
210                // 2..5: cuatro patas desde las esquinas del lomo.
211                miembro("pata_fr", 0, Vec3::new(0.30, -lomo.y * 0.5, lomo.z * 0.4), alto, 0.16, Eje::X, SlotColor::Patas),
212                miembro("pata_fl", 0, Vec3::new(-0.30, -lomo.y * 0.5, lomo.z * 0.4), alto, 0.16, Eje::X, SlotColor::Patas),
213                miembro("pata_br", 0, Vec3::new(0.30, -lomo.y * 0.5, -lomo.z * 0.4), alto, 0.16, Eje::X, SlotColor::Patas),
214                miembro("pata_bl", 0, Vec3::new(-0.30, -lomo.y * 0.5, -lomo.z * 0.4), alto, 0.16, Eje::X, SlotColor::Patas),
215                // 6: cola atrás (-Z), serpentea en Y.
216                miembro("cola", 0, Vec3::new(0.0, 0.10, -lomo.z * 0.5), 0.55, 0.12, Eje::Y, SlotColor::Cuerpo),
217            ],
218        }
219    }
220
221    /// **Ave**: cuerpo + cabeza + 2 alas (aletean en Z) + 2 patitas + cola.
222    pub fn ave() -> Rig {
223        let alto = 0.45;
224        let cuerpo = Vec3::new(0.40, 0.46, 0.60);
225        Rig {
226            nombre: "ave".into(),
227            segmentos: vec![
228                seg("cuerpo", -1, Vec3::new(0.0, alto, 0.0), Vec3::ZERO, cuerpo, Eje::X, SlotColor::Cuerpo),
229                seg("cabeza", 0, Vec3::new(0.0, 0.20, cuerpo.z * 0.5), Vec3::new(0.0, 0.10, 0.10), Vec3::new(0.28, 0.28, 0.30), Eje::X, SlotColor::Piel),
230                // Alas: cajas anchas que cuelgan poco y aletean en Z.
231                seg("ala_r", 0, Vec3::new(cuerpo.x * 0.5, 0.10, 0.0), Vec3::new(0.30, 0.0, 0.0), Vec3::new(0.60, 0.08, 0.40), Eje::Z, SlotColor::Cuerpo),
232                seg("ala_l", 0, Vec3::new(-cuerpo.x * 0.5, 0.10, 0.0), Vec3::new(-0.30, 0.0, 0.0), Vec3::new(0.60, 0.08, 0.40), Eje::Z, SlotColor::Cuerpo),
233                miembro("pata_r", -1, Vec3::new(0.12, alto - 0.05, 0.0), alto - 0.05, 0.07, Eje::X, SlotColor::Patas),
234                miembro("pata_l", -1, Vec3::new(-0.12, alto - 0.05, 0.0), alto - 0.05, 0.07, Eje::X, SlotColor::Patas),
235                miembro("cola", 0, Vec3::new(0.0, 0.05, -cuerpo.z * 0.5), 0.35, 0.10, Eje::X, SlotColor::Cuerpo),
236            ],
237        }
238    }
239
240    /// **Serpiente**: cadena de segmentos que serpentea (cada uno gira en Y respecto
241    /// del anterior). La cabeza es el primero.
242    pub fn serpiente() -> Rig {
243        let n = 8;
244        let alto = 0.25;
245        let paso = 0.42; // largo de cada segmento (en -Z, hacia atrás)
246        let mut segmentos = Vec::with_capacity(n);
247        // 0: cabeza (raíz).
248        segmentos.push(seg(
249            "cabeza",
250            -1,
251            Vec3::new(0.0, alto, 0.0),
252            Vec3::new(0.0, 0.0, 0.10),
253            Vec3::new(0.34, 0.30, 0.40),
254            Eje::Y,
255            SlotColor::Piel,
256        ));
257        // 1..n: cuerpo, cada uno colgando hacia atrás del anterior, girando en Y.
258        for k in 1..n {
259            let grosor = 0.34 - 0.02 * k as f32; // se afina hacia la cola
260            segmentos.push(seg(
261                "cuerpo",
262                (k - 1) as i32,
263                Vec3::new(0.0, 0.0, -paso),
264                Vec3::new(0.0, 0.0, -paso * 0.5),
265                Vec3::new(grosor, 0.26, paso),
266                Eje::Y,
267                SlotColor::Cuerpo,
268            ));
269        }
270        Rig { nombre: "serpiente".into(), segmentos }
271    }
272
273    /// Los cuatro presets (para un editor que cicle entre ellos).
274    pub fn presets() -> Vec<Rig> {
275        vec![Rig::humanoide(), Rig::cuadrupedo(), Rig::ave(), Rig::serpiente()]
276    }
277}
278
279/// Oscilador de **una articulación** en un andar: balancea `amplitud · sin(t·cadencia
280/// + desfase)`.
281#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
282pub struct Osc {
283    pub amplitud: f32,
284    pub desfase: f32,
285}
286
287/// **Andar** (capa 2): animación procedural por articulación a partir del tiempo.
288/// Cada segmento oscila según su [`Osc`] a una `cadencia` común (rad/seg). La amplitud
289/// y el desfase iniciales salen del **rol** del segmento (nombre+posición), así un
290/// `Andar::caminar` arranca razonable para cualquier rig; después se editan.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Andar {
293    /// Ritmo del ciclo (rad/seg): mayor = pasos más rápidos.
294    pub cadencia: f32,
295    /// Oscilador por segmento (paralelo a [`Rig::segmentos`]).
296    pub osc: Vec<Osc>,
297}
298
299impl Andar {
300    /// Caminata heurística para `rig`: patas/piernas en oposición (izq/der y
301    /// delante/atrás), brazos contra su pierna, alas aleteando, serpiente/cola con
302    /// desfase creciente a lo largo de la cadena. `cadencia` de caminata.
303    pub fn caminar(rig: &Rig) -> Andar {
304        use std::f32::consts::PI;
305        let osc = rig
306            .segmentos
307            .iter()
308            .enumerate()
309            .map(|(i, s)| {
310                let nom = s.nombre.as_str();
311                let lado = if s.pivote[0] < -1e-3 { PI } else { 0.0 };
312                let tren = if s.pivote[2] < -1e-3 { PI } else { 0.0 };
313                let (amplitud, desfase) = if nom.contains("pierna") || nom.contains("pata") {
314                    (0.6, lado + tren)
315                } else if nom.contains("brazo") {
316                    (0.5, lado + PI)
317                } else if nom.contains("ala") {
318                    (1.0, lado)
319                } else if nom.contains("cola") {
320                    (0.4, 0.0)
321                } else if nom.contains("cuerpo") && s.eje == Eje::Y {
322                    (0.5, i as f32 * 0.9) // serpenteo: onda que viaja
323                } else {
324                    (0.0, 0.0)
325                };
326                Osc { amplitud, desfase }
327            })
328            .collect();
329        Andar { cadencia: 8.0, osc }
330    }
331
332    /// Reposo: el andar de caminata muy atenuado y lento (un balanceo apenas vivo).
333    pub fn quieto(rig: &Rig) -> Andar {
334        let mut a = Andar::caminar(rig);
335        a.cadencia = 2.2;
336        for o in &mut a.osc {
337            o.amplitud *= 0.12;
338        }
339        a
340    }
341
342    /// Trote: la caminata más amplia y rápida.
343    pub fn correr(rig: &Rig) -> Andar {
344        let mut a = Andar::caminar(rig);
345        a.cadencia = 13.0;
346        for o in &mut a.osc {
347            o.amplitud = (o.amplitud * 1.4).min(1.4);
348        }
349        a
350    }
351
352    /// **Saludar**: un miembro delantero (o la cabeza si no hay) oscila amplio; el
353    /// resto quieto. Gesto genérico para cualquier rig.
354    pub fn saludar(rig: &Rig) -> Andar {
355        let mut osc = vec![Osc { amplitud: 0.0, desfase: 0.0 }; rig.len()];
356        if let Some(i) = miembro_expresivo(rig) {
357            osc[i] = Osc { amplitud: 1.0, desfase: 0.0 };
358        }
359        Andar { cadencia: 9.0, osc }
360    }
361
362    /// **Señalar**: un miembro delantero extendido y *sostenido* (cadencia 0 → pose
363    /// fija; `sin(π/2)=1` lo deja en su máxima extensión).
364    pub fn senalar(rig: &Rig) -> Andar {
365        let mut osc = vec![Osc { amplitud: 0.0, desfase: 0.0 }; rig.len()];
366        if let Some(i) = miembro_expresivo(rig) {
367            osc[i] = Osc { amplitud: 0.9, desfase: std::f32::consts::FRAC_PI_2 };
368        }
369        Andar { cadencia: 0.0, osc }
370    }
371
372    /// **Festejar**: todos los miembros (o el cuerpo, si no hay) rebotan en fase.
373    pub fn festejar(rig: &Rig) -> Andar {
374        let osc = rig
375            .segmentos
376            .iter()
377            .map(|s| {
378                let nom = s.nombre.as_str();
379                let activo = es_miembro(nom)
380                    || nom.contains("cabeza")
381                    || (nom.contains("cuerpo") && s.eje == Eje::Y);
382                Osc { amplitud: if activo { 0.7 } else { 0.0 }, desfase: 0.0 }
383            })
384            .collect();
385        Andar { cadencia: 8.0, osc }
386    }
387
388    /// La pose del andar en el instante `t` (seg).
389    pub fn pose(&self, t: f32) -> RigPose {
390        let angulos = self
391            .osc
392            .iter()
393            .map(|o| o.amplitud * (t * self.cadencia + o.desfase).sin())
394            .collect();
395        RigPose { angulos }
396    }
397}
398
399/// `true` si el segmento es un miembro (brazo/pata/ala).
400fn es_miembro(nom: &str) -> bool {
401    nom.contains("brazo") || nom.contains("pata") || nom.contains("ala")
402}
403
404/// Índice del miembro **delantero-derecho** para los gestos (brazo/pata/ala del
405/// frente y a la derecha); si no hay miembros, la cabeza; si tampoco, `None`.
406fn miembro_expresivo(rig: &Rig) -> Option<usize> {
407    let miembros: Vec<usize> = (0..rig.len()).filter(|&i| es_miembro(&rig.segmentos[i].nombre)).collect();
408    miembros
409        .iter()
410        .copied()
411        .find(|&i| rig.segmentos[i].pivote[2] >= -1e-3 && rig.segmentos[i].pivote[0] > 1e-3)
412        .or_else(|| miembros.first().copied())
413        .or_else(|| rig.segmentos.iter().position(|s| s.nombre.contains("cabeza")))
414}
415
416/// Los **andares por estado** de una criatura (capa 2): reposo, caminar, correr.
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct Andares {
419    pub quieto: Andar,
420    pub caminar: Andar,
421    pub correr: Andar,
422}
423
424impl Andares {
425    /// Andares por defecto (heurísticos) para un rig.
426    pub fn default_for(rig: &Rig) -> Self {
427        Self { quieto: Andar::quieto(rig), caminar: Andar::caminar(rig), correr: Andar::correr(rig) }
428    }
429
430    /// Rótulos de los estados, en orden de índice.
431    pub const LABELS: [&'static str; 3] = ["quieto", "caminar", "correr"];
432
433    /// El andar del estado `i` (0=quieto,1=caminar,2=correr).
434    pub fn estado(&self, i: usize) -> &Andar {
435        match i {
436            0 => &self.quieto,
437            1 => &self.caminar,
438            _ => &self.correr,
439        }
440    }
441    pub fn estado_mut(&mut self, i: usize) -> &mut Andar {
442        match i {
443            0 => &mut self.quieto,
444            1 => &mut self.caminar,
445            _ => &mut self.correr,
446        }
447    }
448}
449
450/// El **movimiento** de un cuerpo: su [`Rig`] (morfología) + sus [`Andares`]
451/// (animación). Es lo que distingue a una criatura no-humanoide; los andares quedan
452/// siempre en sincronía con el rig (misma cantidad de segmentos).
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct Movimiento {
455    pub rig: Rig,
456    pub andares: Andares,
457}
458
459impl Movimiento {
460    /// Un movimiento a partir de un rig, con andares por defecto.
461    pub fn preset(rig: Rig) -> Self {
462        let andares = Andares::default_for(&rig);
463        Self { rig, andares }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn presets_tienen_las_partes_esperadas() {
473        assert_eq!(Rig::humanoide().len(), 6);
474        assert_eq!(Rig::cuadrupedo().len(), 7);
475        assert_eq!(Rig::ave().len(), 7);
476        assert_eq!(Rig::serpiente().len(), 8);
477        // Todos en orden topológico: el padre aparece antes que el hijo.
478        for rig in Rig::presets() {
479            for (i, s) in rig.segmentos.iter().enumerate() {
480                assert!((s.padre as i32) < i as i32, "{}::{} padre {} no es anterior", rig.nombre, s.nombre, s.padre);
481            }
482        }
483    }
484
485    #[test]
486    fn la_malla_no_es_vacia_y_escala_con_las_partes() {
487        let rig = Rig::cuadrupedo();
488        let pose = RigPose::neutra(rig.len());
489        let (v, idx) = rig.mesh(&pose, [0.9, 0.7, 0.6], [0.4, 0.5, 0.7], [0.3, 0.3, 0.3]);
490        // 8 vértices y 36 índices por caja.
491        assert_eq!(v.len(), 8 * rig.len());
492        assert_eq!(idx.len(), 36 * rig.len());
493    }
494
495    #[test]
496    fn rotar_el_padre_arrastra_al_hijo() {
497        // En la serpiente, girar la cabeza mueve el primer segmento del cuerpo.
498        let rig = Rig::serpiente();
499        let mut pose = RigPose::neutra(rig.len());
500        let world0 = rig.world_transforms(&pose);
501        pose.angulos[0] = 0.8; // gira la cabeza (raíz) en Y
502        let world1 = rig.world_transforms(&pose);
503        let p0 = world0[1].transform_point3(Vec3::ZERO);
504        let p1 = world1[1].transform_point3(Vec3::ZERO);
505        assert!((p0 - p1).length() > 0.05, "el hijo siguió a la rotación del padre");
506    }
507
508    #[test]
509    fn los_gestos_mueven_lo_esperado() {
510        // Saludar: exactamente un miembro con amplitud (el delantero-derecho).
511        let rig = Rig::cuadrupedo();
512        let s = Andar::saludar(&rig);
513        let activos = s.osc.iter().filter(|o| o.amplitud > 0.0).count();
514        assert_eq!(activos, 1, "saludar mueve un solo miembro");
515        // Señalar: sostenido (cadencia 0) y la pose no es nula.
516        let n = Andar::senalar(&rig);
517        assert_eq!(n.cadencia, 0.0);
518        assert!(n.pose(3.7).angulos.iter().any(|a| a.abs() > 0.1), "señalar deja una pose fija no nula");
519        // Festejar: varios miembros activos (las 4 patas + cola).
520        let f = Andar::festejar(&rig);
521        assert!(f.osc.iter().filter(|o| o.amplitud > 0.0).count() >= 4, "festejar mueve varias partes");
522        // Una serpiente (sin miembros) igual saluda con la cabeza.
523        let serp = Rig::serpiente();
524        assert!(Andar::saludar(&serp).osc.iter().any(|o| o.amplitud > 0.0), "la serpiente saluda con la cabeza");
525    }
526
527    #[test]
528    fn el_andar_balancea_las_patas_en_oposicion() {
529        use std::f32::consts::PI;
530        let rig = Rig::cuadrupedo();
531        let andar = Andar::caminar(&rig);
532        // Patas delanteras der/izq en oposición (~π de desfase).
533        let fr = rig.segmentos.iter().position(|s| s.nombre == "pata_fr").unwrap();
534        let fl = rig.segmentos.iter().position(|s| s.nombre == "pata_fl").unwrap();
535        let p = andar.pose(0.3);
536        // En oposición: signos opuestos en casi toda la fase.
537        let a = (0.3_f32).sin();
538        let b = (0.3_f32 + PI).sin();
539        assert!(a * b < 0.0, "patas opuestas baten en contrafase");
540        assert!(p.angulos[fr].abs() > 0.0 && p.angulos[fl].abs() > 0.0);
541        let _ = (fr, fl);
542    }
543}