tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-test/tests/lottie_degrada_pero_pinta.rs Rust · 176 lines · 6.9 KB History Who touched what raw

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

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}