tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! Tests del fork tawasuyu: los Lotties que en velato 0.9 upstream
2//! **paniqueaban** (`todo!()`/`unimplemented!()` en el importador) ahora se
3//! importan con degradación graciosa, sin panic. Cada caso construye una
4//! `Composition` y exige que vuelva `Ok`.
5
6use foreign_lottie::Composition;
7
8/// Lottie con un layer cuyo transform NO trae campo de rotación (`r`).
9/// Upstream: `None => todo!("split rotation")`. Fork: rotación 0.
10#[test]
11fn transform_sin_rotacion_no_paniquea() {
12 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
13 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
14 "ks":{"p":{"a":0,"k":[0,0]}},"shapes":[]}]}"#;
15 assert!(Composition::from_slice(json).is_ok());
16}
17
18/// Transform con rotación **splitteada** (`r` = {x,y,z,or}). Upstream:
19/// `SplitRotation => todo!()`. Fork: usa la componente z.
20#[test]
21fn split_rotation_no_paniquea() {
22 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
23 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
24 "ks":{"p":{"a":0,"k":[0,0]},
25 "r":{"x":{"a":0,"k":0},"y":{"a":0,"k":0},"z":{"a":0,"k":45},
26 "or":{"a":0,"k":[0,0,0]}}},
27 "shapes":[]}]}"#;
28 assert!(Composition::from_slice(json).is_ok());
29}
30
31/// Transform con posición **splitteada** (`p` = {s:true,x,y}). Upstream del
32/// shape-transform: `SplitPosition => todo!("split position")`. Fork: la arma
33/// desde x/y. (Lo metemos dentro de un group-shape con su transform para
34/// ejercitar `conv_shape_transform`.)
35#[test]
36fn split_position_en_shape_no_paniquea() {
37 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
38 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
39 "ks":{"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0}},
40 "shapes":[{"ty":"gr","it":[
41 {"ty":"tr",
42 "p":{"s":true,"x":{"a":0,"k":10},"y":{"a":0,"k":20}},
43 "a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},
44 "r":{"a":0,"k":0},"o":{"a":0,"k":100}}
45 ]}]}]}"#;
46 assert!(Composition::from_slice(json).is_ok());
47}
48
49/// Blend mode `Add` (be=10) en un layer. Upstream: `Add => unimplemented!()`.
50/// Fork: compositing aditivo (Compose::Plus).
51#[test]
52fn blend_add_no_paniquea() {
53 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
54 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,"bm":10,
55 "ks":{"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0}},"shapes":[]}]}"#;
56 assert!(Composition::from_slice(json).is_ok());
57}
58
59/// Un Lottie sano sigue parseando (no rompimos el camino feliz).
60#[test]
61fn lottie_sano_sigue_ok() {
62 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
63 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
64 "ks":{"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0}},
65 "shapes":[
66 {"ty":"rc","p":{"a":0,"k":[50,50]},"s":{"a":0,"k":[80,80]},"r":{"a":0,"k":0}},
67 {"ty":"fl","c":{"a":0,"k":[0.8,0,0,1]},"o":{"a":0,"k":100}}
68 ]}]}"#;
69 let comp = Composition::from_slice(json).expect("lottie sano parsea");
70 assert_eq!((comp.width, comp.height), (100, 100));
71}
72
73// ── El deserializador: una capa que no entendemos no se lleva el archivo ──
74
75/// Un `ty` fuera de 0–4 (texto=5, cámara, luz, ajuste…) devolvía
76/// `Err("unknown layer type")`, y serde aborta el documento al primer error:
77/// **un solo layer de texto tumbaba el `.json` entero**. En un export de After
78/// Effects una cámara o una luz vienen de fábrica, así que el caso es el
79/// común, no el raro. Ahora la capa se reconoce como `Unsupported` y el resto
80/// del archivo se importa.
81///
82/// El diente del test es la SEGUNDA aserción: que no paniquee ni devuelva
83/// `Err` no alcanza si el documento vuelve sin capas — sería la misma pérdida
84/// con otro nombre.
85#[test]
86fn una_capa_de_tipo_desconocido_no_rechaza_el_documento() {
87 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
88 "layers":[
89 {"ty":5,"ip":0,"op":60,"st":0,"sr":1,
90 "ks":{"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0}}},
91 {"ty":4,"ip":0,"op":60,"st":0,"sr":1,
92 "ks":{"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0}},
93 "shapes":[
94 {"ty":"rc","p":{"a":0,"k":[50,50]},"s":{"a":0,"k":[80,80]},"r":{"a":0,"k":0}},
95 {"ty":"fl","c":{"a":0,"k":[0.8,0,0,1]},"o":{"a":0,"k":100}}
96 ]}
97 ]}"#;
98 let comp = Composition::from_slice(json).expect("una capa de texto no puede tumbar el archivo");
99 assert!(
100 !comp.layers.is_empty(),
101 "importó con Ok pero sin capas: la capa de forma se fue con la de texto"
102 );
103}
104
105/// Y el otro lado del mismo agujero: **omitir el campo `s` (escala) de un
106/// transform** es legal en Lottie y su default es 100. El default del
107/// importador era `[1,1,1]`, y la escala se divide por 100 al armar la
108/// `Affine`, así que la capa entraba al **1 %** — sin error, sin panic, sin
109/// nada que mirar salvo el resultado. Verificado a nivel píxel en
110/// `02_ruway/llimphi/llimphi-test/tests/lottie_degrada_pero_pinta.rs`
111/// (4 px sin `s`, 8.100 con `s:[100,100]`); acá se fija el número que lo
112/// causa, que es más barato y no necesita GPU.
113#[test]
114fn un_transform_sin_escala_entra_al_cien_por_ciento() {
115 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
116 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
117 "ks":{"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0}},"shapes":[]}]}"#;
118 let comp = Composition::from_slice(json).expect("parsea");
119 let capa = comp.layers.first().expect("hay una capa");
120 // `Transform::evaluate` devuelve un `ValueRef<kurbo::Affine>`.
121 let escala = capa.transform.evaluate(0.0).as_ref().as_coeffs();
122 // Los coeficientes 0 y 3 de la Affine son el escalado en x e y. Con el
123 // default viejo daban 0,01.
124 assert!(
125 (escala[0] - 1.0).abs() < 1e-6 && (escala[3] - 1.0).abs() < 1e-6,
126 "un transform sin `s` entró con escala {:?}, no 1,0 (= 100 %)",
127 (escala[0], escala[3])
128 );
129}