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//! **Un Lottie que el fork «degrada con gracia» sigue dibujando algo.**
2//!
3//! Paso 7 del runbook `[METAL]` del `PLAN-MOTOR-PROFUNDO`, que anotaba
4//! «`llimphi-lottie` sigue sin probarse con archivos Lottie reales — el puente
5//! a `velato` está, los archivos no».
6//!
7//! Lo que había en `shared/foreign-lottie/tests/no_panic_fork.rs` es la mitad
8//! de la pregunta: cada caso que en velato 0.9 upstream **paniqueaba** ahora
9//! importa con `Ok`. Perfecto para lo que ese test se propone —que el
10//! importador no tumbe el hilo de UI— y **ciego** a lo que pasa después: un
11//! `Ok` que devuelve una composición vacía, o una capa que se descarta entera
12//! al no entender un campo, da exactamente el mismo `is_ok()`. «No paniquea» y
13//! «se ve» son dos afirmaciones distintas, y sólo estaba probada la primera.
14//!
15//! La diferencia importa porque los consumidores son reales y silenciosos:
16//! `mirada-greeter` pinta un Lottie de fondo en la pantalla de login y
17//! `media-app` lo acepta como fondo. Un `.json` de diseñador que degrada a
18//! nada no da error en ningún lado — da un fondo negro.
19//!
20//! Acá cada caso lleva **una figura visible** además de la característica que
21//! el fork degrada, y se exige que el render tenga tinta. El control negativo
22//! es el mismo JSON sin la figura: tiene que dar cero, o el test estaría
23//! midiendo el fondo del arnés.
24//!
25//! Certificado por texto (§8): conteo de píxeles con tinta.
26
27use llimphi_compositor::peniko::Color;
28use llimphi_lottie::LottieAsset;
29use llimphi_test::{with_harness, Harness, Rendered};
30
31const W: u32 = 128;
32const H: u32 = 128;
33const FONDO: Color = Color::from_rgba8(0, 0, 0, 255);
34
35#[derive(Clone)]
36enum Msg {}
37
38/// La figura que tiene que sobrevivir a cada degradación: un cuadrado rojo
39/// grande y opaco, centrado. Se inyecta en el `shapes` de la capa.
40const FIGURA: &str = r#"{"ty":"gr","it":[
41 {"ty":"rc","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[70,70]},"r":{"a":0,"k":0}},
42 {"ty":"fl","c":{"a":0,"k":[0.9,0.15,0.15]},"o":{"a":0,"k":100}},
43 {"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},
44 "s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}"#;
45
46fn tinta(r: &Rendered) -> usize {
47 r.rgba.chunks_exact(4).filter(|p| p[0] > 8 || p[1] > 8 || p[2] > 8).count()
48}
49
50fn pintar(h: &mut Harness, json: &str) -> usize {
51 let asset = LottieAsset::from_str(json).expect("el fork importa esto sin paniquear");
52 tinta(&h.render(asset.view_at_time::<Msg>(0.0), W, H, FONDO))
53}
54
55/// Arma un Lottie con `ks` (el transform de la capa) dado y, si `con_figura`,
56/// el cuadrado rojo adentro.
57fn lottie(ks: &str, con_figura: bool, extra_capa: &str) -> String {
58 let shapes = if con_figura { FIGURA } else { "" };
59 format!(
60 r#"{{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
61 "layers":[{{"ty":4,"ip":0,"op":60,"st":0,"sr":1{extra_capa},
62 "ks":{ks},"shapes":[{shapes}]}}]}}"#
63 )
64}
65
66/// El transform sano, como referencia de los demás.
67const KS_SANO: &str = r#"{"o":{"a":0,"k":100},"r":{"a":0,"k":0},
68 "p":{"a":0,"k":[50,50]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]}}"#;
69
70#[test]
71fn el_control_negativo_da_cero() {
72 // Sin figura no hay tinta. Si esto pintara, todo lo de abajo sería ruido
73 // del arnés y no del importador.
74 with_harness(|h| {
75 assert_eq!(pintar(h, &lottie(KS_SANO, false, "")), 0, "una capa vacía pintó algo");
76 });
77}
78
79#[test]
80fn el_camino_feliz_pinta() {
81 with_harness(|h| {
82 let n = pintar(h, &lottie(KS_SANO, true, ""));
83 assert!(n > 2000, "el Lottie sano pintó {n} px");
84 println!("sano: {n} px");
85 });
86}
87
88/// Cada degradación del fork, con la figura adentro. Upstream paniqueaba en
89/// todas; lo que se exige acá es que además **se sigan viendo**.
90#[test]
91fn cada_degradacion_del_fork_sigue_pintando() {
92 // (nombre, transform de capa, extra en el objeto de la capa)
93 let casos: &[(&str, &str, &str)] = &[
94 (
95 "transform sin campo de rotación",
96 r#"{"p":{"a":0,"k":[50,50]}}"#,
97 "",
98 ),
99 (
100 "rotación splitteada (x/y/z)",
101 r#"{"p":{"a":0,"k":[50,50]},
102 "r":{"x":{"a":0,"k":0},"y":{"a":0,"k":0},"z":{"a":0,"k":45},
103 "or":{"a":0,"k":[0,0,0]}}}"#,
104 "",
105 ),
106 (
107 "blend Add",
108 KS_SANO,
109 r#","bm":10"#,
110 ),
111 (
112 "blend HardMix",
113 KS_SANO,
114 r#","bm":15"#,
115 ),
116 ];
117 with_harness(|h| {
118 for (nombre, ks, extra) in casos {
119 let n = pintar(h, &lottie(ks, true, extra));
120 assert!(
121 n > 2000,
122 "«{nombre}» importó sin paniquear pero pintó {n} px: la \
123 degradación se comió la capa entera"
124 );
125 println!("{nombre}: {n} px");
126 }
127 });
128}
129
130/// Posición splitteada dentro del transform de un **shape group** — el otro
131/// sitio del importador que paniqueaba. La figura va adentro del mismo grupo,
132/// así que si la degradación descarta el grupo, no queda nada que pintar.
133#[test]
134fn split_position_en_shape_sigue_pintando() {
135 let json = r#"{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
136 "layers":[{"ty":4,"ip":0,"op":60,"st":0,"sr":1,
137 "ks":{"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0}},
138 "shapes":[{"ty":"gr","it":[
139 {"ty":"rc","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[70,70]},"r":{"a":0,"k":0}},
140 {"ty":"fl","c":{"a":0,"k":[0.9,0.15,0.15]},"o":{"a":0,"k":100}},
141 {"ty":"tr",
142 "p":{"s":true,"x":{"a":0,"k":50},"y":{"a":0,"k":50}},
143 "a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},
144 "r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}]}]}"#;
145 with_harness(|h| {
146 let n = pintar(h, json);
147 assert!(n > 2000, "split position en shape pintó {n} px");
148 println!("split position en shape: {n} px");
149 });
150}
151
152/// Los gaps que el fork **no** cierra porque son de velato (capas de texto,
153/// effects, expresiones) degradan a **omisión**. Este test no pide que se
154/// vean: pide que la omisión sea de la característica y no del archivo — o
155/// sea, que una capa de texto al lado de una figura no se lleve la figura.
156#[test]
157fn una_capa_de_texto_no_se_lleva_puesta_a_la_figura() {
158 let json = format!(
159 r#"{{"v":"5.5.2","fr":30,"ip":0,"op":60,"w":100,"h":100,
160 "layers":[
161 {{"ty":5,"ip":0,"op":60,"st":0,"sr":1,"ks":{KS_SANO},
162 "t":{{"d":{{"k":[{{"s":{{"t":"hola","s":24,"f":"Inter","lh":28,"tr":0,
163 "j":0,"fc":[1,1,1]}},"t":0}}]}}}}}},
164 {{"ty":4,"ip":0,"op":60,"st":0,"sr":1,"ks":{KS_SANO},"shapes":[{FIGURA}]}}
165 ]}}"#
166 );
167 with_harness(|h| {
168 let n = pintar(h, &json);
169 assert!(
170 n > 2000,
171 "con una capa de texto al lado, la figura pintó {n} px: la \
172 característica no soportada se llevó puesto el archivo entero"
173 );
174 println!("figura junto a capa de texto: {n} px");
175 });
176}