tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
llimphi-voxel/src/rig.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
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}