tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! `llimphi-mesh` — render de mallas deformables a una `vello::Scene`.
2//!
3//! La matemática de huesos y skinning vive en [`llimphi_anim::skel`]; este crate
4//! sólo **pinta** la malla ya deformada. Recibe la malla ([`Mesh`]) + las
5//! posiciones deformadas de sus vértices (lo que devuelve [`Mesh::deform`]) y un
6//! `xform` model→pantalla, y ofrece las dos rutas que el spike validó contra
7//! vello 0.7:
8//!
9//! - [`paint_solid`] — rellena cada triángulo deformado con un color (malla
10//! vectorial / debug). Trivial.
11//! - [`paint_textured`] — malla **texturizada**: por triángulo, recorta al
12//! triángulo deformado y dibuja la imagen con el afín que mapea sus UV a la
13//! posición deformada (warp piecewise-affine). Costo = un clip-layer por
14//! triángulo; cuidado con mallas de miles de triángulos.
15//! - [`paint_wireframe`] — traza los bordes de los triángulos (ver la
16//! deformación; ideal para demos/debug).
17//!
18//! Helpers de encuadre: [`rest_bounds`] (bbox de la malla en reposo) +
19//! [`fit_transform`] (afín que encaja esos bounds, centrados, en un `PaintRect`).
20
21#![forbid(unsafe_code)]
22
23use llimphi_anim::skel::Mesh;
24use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Point, Rect, Stroke};
25use llimphi_ui::llimphi_raster::peniko::{Brush, Color, Fill, ImageBrush};
26use llimphi_ui::llimphi_raster::vello::Scene;
27use llimphi_ui::PaintRect;
28
29/// Afín que mapea el triángulo `src` al triángulo `dst` (3 correspondencias).
30/// Es la base del warp piecewise-affine de la malla texturizada. Devuelve la
31/// identidad si `src` es degenerado (área nula).
32pub fn affine_from_tri(src: [Point; 3], dst: [Point; 3]) -> Affine {
33 let e1 = (src[1].x - src[0].x, src[1].y - src[0].y);
34 let e2 = (src[2].x - src[0].x, src[2].y - src[0].y);
35 let f1 = (dst[1].x - dst[0].x, dst[1].y - dst[0].y);
36 let f2 = (dst[2].x - dst[0].x, dst[2].y - dst[0].y);
37 let det = e1.0 * e2.1 - e2.0 * e1.1;
38 if det.abs() < 1e-9 {
39 return Affine::IDENTITY;
40 }
41 let inv = 1.0 / det;
42 let l00 = (f1.0 * e2.1 - f2.0 * e1.1) * inv;
43 let l01 = (-f1.0 * e2.0 + f2.0 * e1.0) * inv;
44 let l10 = (f1.1 * e2.1 - f2.1 * e1.1) * inv;
45 let l11 = (-f1.1 * e2.0 + f2.1 * e1.0) * inv;
46 let tx = dst[0].x - (l00 * src[0].x + l01 * src[0].y);
47 let ty = dst[0].y - (l10 * src[0].x + l11 * src[0].y);
48 Affine::new([l00, l10, l01, l11, tx, ty])
49}
50
51/// Bounding box de las posiciones de **reposo** de la malla. Útil para
52/// `fit_transform`. Vacío (`Rect::ZERO`) si la malla no tiene vértices.
53pub fn rest_bounds(mesh: &Mesh) -> Rect {
54 let mut it = mesh.vertices.iter();
55 let Some(first) = it.next() else {
56 return Rect::ZERO;
57 };
58 let (mut x0, mut y0, mut x1, mut y1) = (first.rest.x, first.rest.y, first.rest.x, first.rest.y);
59 for v in it {
60 x0 = x0.min(v.rest.x);
61 y0 = y0.min(v.rest.y);
62 x1 = x1.max(v.rest.x);
63 y1 = y1.max(v.rest.y);
64 }
65 Rect::new(x0, y0, x1, y1)
66}
67
68/// Afín que encaja `bounds` (espacio de la malla) dentro de `rect` (pantalla),
69/// escalando uniforme al mínimo lado y centrando (preserva aspecto).
70pub fn fit_transform(bounds: Rect, rect: PaintRect) -> Affine {
71 let bw = bounds.width();
72 let bh = bounds.height();
73 if bw <= 0.0 || bh <= 0.0 || rect.w <= 0.0 || rect.h <= 0.0 {
74 return Affine::IDENTITY;
75 }
76 let s = (rect.w as f64 / bw).min(rect.h as f64 / bh);
77 let used_w = bw * s;
78 let used_h = bh * s;
79 let tx = rect.x as f64 + (rect.w as f64 - used_w) * 0.5 - bounds.x0 * s;
80 let ty = rect.y as f64 + (rect.h as f64 - used_h) * 0.5 - bounds.y0 * s;
81 Affine::translate((tx, ty)) * Affine::scale(s)
82}
83
84/// Posiciones deformadas (model space) → pantalla, en un buffer reusable.
85fn to_screen(positions: &[Point], xform: Affine, out: &mut Vec<Point>) {
86 out.clear();
87 out.extend(positions.iter().map(|p| xform * *p));
88}
89
90fn tri_path(a: Point, b: Point, c: Point) -> BezPath {
91 let mut p = BezPath::new();
92 p.move_to(a);
93 p.line_to(b);
94 p.line_to(c);
95 p.close_path();
96 p
97}
98
99/// Rellena cada triángulo deformado con un color sólido. `positions` son las
100/// posiciones deformadas (de `Mesh::deform`), `xform` las lleva a pantalla.
101pub fn paint_solid(
102 scene: &mut Scene,
103 mesh: &Mesh,
104 positions: &[Point],
105 xform: Affine,
106 color: Color,
107) {
108 let mut screen = Vec::new();
109 to_screen(positions, xform, &mut screen);
110 let brush = Brush::Solid(color);
111 for t in &mesh.triangles {
112 let (Some(&a), Some(&b), Some(&c)) = (
113 screen.get(t[0] as usize),
114 screen.get(t[1] as usize),
115 screen.get(t[2] as usize),
116 ) else {
117 continue;
118 };
119 scene.fill(Fill::NonZero, Affine::IDENTITY, &brush, None, &tri_path(a, b, c));
120 }
121}
122
123/// Malla texturizada: por triángulo, recorta al triángulo deformado y dibuja la
124/// imagen con el afín que mapea las UV (espacio de imagen) a la posición
125/// deformada en pantalla — warp piecewise-affine. `image` es la textura; las UV
126/// de cada vértice (`0..1`) indexan en ella.
127pub fn paint_textured(
128 scene: &mut Scene,
129 mesh: &Mesh,
130 positions: &[Point],
131 xform: Affine,
132 image: &ImageBrush,
133) {
134 let mut screen = Vec::new();
135 to_screen(positions, xform, &mut screen);
136 let iw = image.image.width as f64;
137 let ih = image.image.height as f64;
138 for t in &mesh.triangles {
139 let idx = [t[0] as usize, t[1] as usize, t[2] as usize];
140 let (Some(&da), Some(&db), Some(&dc)) =
141 (screen.get(idx[0]), screen.get(idx[1]), screen.get(idx[2]))
142 else {
143 continue;
144 };
145 // src = UV·tamaño-de-imagen (espacio de imagen); dst = pantalla deformada.
146 let uv = |i: usize| {
147 let v = &mesh.vertices[idx[i]];
148 Point::new(v.uv.0 * iw, v.uv.1 * ih)
149 };
150 let aff = affine_from_tri([uv(0), uv(1), uv(2)], [da, db, dc]);
151 scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &tri_path(da, db, dc));
152 scene.draw_image(image.as_ref(), aff);
153 scene.pop_layer();
154 }
155}
156
157/// Traza los bordes de los triángulos (para ver la deformación / debug).
158pub fn paint_wireframe(
159 scene: &mut Scene,
160 mesh: &Mesh,
161 positions: &[Point],
162 xform: Affine,
163 color: Color,
164 width: f64,
165) {
166 let mut screen = Vec::new();
167 to_screen(positions, xform, &mut screen);
168 let brush = Brush::Solid(color);
169 let stroke = Stroke::new(width);
170 for t in &mesh.triangles {
171 let (Some(&a), Some(&b), Some(&c)) = (
172 screen.get(t[0] as usize),
173 screen.get(t[1] as usize),
174 screen.get(t[2] as usize),
175 ) else {
176 continue;
177 };
178 scene.stroke(&stroke, Affine::IDENTITY, &brush, None, &tri_path(a, b, c));
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use llimphi_anim::skel::{Mesh, Pose, Skeleton, Vertex};
186 use llimphi_ui::llimphi_raster::peniko::{Blob, ImageAlphaType, ImageData, ImageFormat};
187 use std::sync::Arc;
188
189 /// Una malla quad (2 triángulos) atada a un hueso, con UVs de esquina.
190 fn quad_mesh(bone: usize) -> Mesh {
191 let mut m = Mesh::new();
192 m.vertices = vec![
193 Vertex::rigid(Point::new(0.0, 0.0), (0.0, 0.0), bone),
194 Vertex::rigid(Point::new(100.0, 0.0), (1.0, 0.0), bone),
195 Vertex::rigid(Point::new(100.0, 100.0), (1.0, 1.0), bone),
196 Vertex::rigid(Point::new(0.0, 100.0), (0.0, 1.0), bone),
197 ];
198 m.triangles = vec![[0, 1, 2], [0, 2, 3]];
199 m
200 }
201
202 fn skel_one_bone() -> (Skeleton, usize) {
203 let mut s = Skeleton::new();
204 let b = s.add_bone(None, Pose::identity());
205 s.bind();
206 s.update();
207 (s, b)
208 }
209
210 fn img_2x2() -> ImageBrush {
211 let px: Vec<u8> = vec![
212 200, 60, 60, 255, 60, 200, 60, 255, 60, 60, 200, 255, 220, 200, 60, 255,
213 ];
214 ImageBrush::new(ImageData {
215 data: Blob::new(Arc::new(px)),
216 format: ImageFormat::Rgba8,
217 alpha_type: ImageAlphaType::Alpha,
218 width: 2,
219 height: 2,
220 })
221 }
222
223 const RECT: PaintRect = PaintRect {
224 x: 0.0,
225 y: 0.0,
226 w: 200.0,
227 h: 200.0,
228 };
229
230 #[test]
231 fn fit_transform_centra_y_escala() {
232 let m = quad_mesh(0);
233 let xf = fit_transform(rest_bounds(&m), RECT);
234 // El quad 100×100 en un rect 200×200 escala ×2 y centra (sin offset, ya
235 // que llena el lado). Esquina (0,0) → (0,0), (100,100) → (200,200).
236 let p0 = xf * Point::new(0.0, 0.0);
237 let p1 = xf * Point::new(100.0, 100.0);
238 assert!((p0.x).abs() < 1e-9 && (p0.y).abs() < 1e-9, "{p0:?}");
239 assert!((p1.x - 200.0).abs() < 1e-9 && (p1.y - 200.0).abs() < 1e-9, "{p1:?}");
240 }
241
242 #[test]
243 fn paint_solid_emite_geometria() {
244 let (s, b) = skel_one_bone();
245 let m = quad_mesh(b);
246 let pos = m.deform(&s);
247 let xf = fit_transform(rest_bounds(&m), RECT);
248 let mut sc = Scene::new();
249 paint_solid(&mut sc, &m, &pos, xf, Color::from_rgba8(200, 100, 50, 255));
250 assert!(!sc.encoding().is_empty());
251 }
252
253 #[test]
254 fn paint_textured_emite_geometria() {
255 let (s, b) = skel_one_bone();
256 let m = quad_mesh(b);
257 let pos = m.deform(&s);
258 let xf = fit_transform(rest_bounds(&m), RECT);
259 let img = img_2x2();
260 let mut sc = Scene::new();
261 paint_textured(&mut sc, &m, &pos, xf, &img);
262 assert!(!sc.encoding().is_empty(), "la malla texturizada debe emitir geometría");
263 }
264
265 #[test]
266 fn paint_wireframe_emite_geometria() {
267 let (s, b) = skel_one_bone();
268 let m = quad_mesh(b);
269 let pos = m.deform(&s);
270 let xf = fit_transform(rest_bounds(&m), RECT);
271 let mut sc = Scene::new();
272 paint_wireframe(&mut sc, &m, &pos, xf, Color::from_rgba8(255, 255, 255, 255), 1.5);
273 assert!(!sc.encoding().is_empty());
274 }
275
276 /// Deformar un hueso mueve la geometría pintada: las posiciones deformadas
277 /// cambian respecto al reposo (certifica que el render usa la deformación).
278 #[test]
279 fn deformar_cambia_las_posiciones() {
280 let mut s = Skeleton::new();
281 let b = s.add_bone(None, Pose::identity());
282 s.bind();
283 s.update();
284 let m = quad_mesh(b);
285 let reposo = m.deform(&s);
286
287 s.set_pose(b, Pose::rotate(0.5));
288 s.update();
289 let deformado = m.deform(&s);
290
291 let movido = reposo
292 .iter()
293 .zip(&deformado)
294 .any(|(a, d)| (a.x - d.x).abs() > 1e-6 || (a.y - d.y).abs() > 1e-6);
295 assert!(movido, "rotar el hueso debe mover los vértices");
296 }
297}