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-mesh/src/lib.rs Rust · 297 siq'ikuna · 10.5 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//! `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}