gioser

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definitions

⎇ main llimphi-wire-escena/src/forma.rs Rust · 277 lines · 9.8 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1// =============================================================================
2//  forma :: la geometría, en el ancho de banda mínimo
3// -----------------------------------------------------------------------------
4//  kurbo trabaja en f64 en todos lados, y con razón: es una biblioteca de
5//  geometría, y aplanar curvas en f32 acumula error visible. Pero acá la
6//  geometría ya está resuelta —el layout corrió, los radios se calcularon— y lo
7//  único que falta es CRUZAR. En ese tramo f64 es pagar el doble por precisión
8//  que muere en el rasterizador: el destino son píxeles enteros de una pantalla
9//  de 1280 columnas, no un plano continuo.
10//
11//  Por eso todo viaja en f32. A 500 widgets el plan midió 11-114 KB por
12//  fotograma; en f64 serían 22-228 KB por la misma imagen. La conversión de
13//  vuelta a f64 al reproducir es exacta (f32 → f64 nunca pierde), así que el
14//  único error posible es el del redondeo al empaquetar, que a escala de
15//  pantalla está muy por debajo del píxel.
16//
17//  Los tres casos de forma espejan los de `llimphi_cpu::Forma`, y por el mismo
18//  motivo: el compositor dibuja rectángulos y rectángulos redondeados en el 90%
19//  de los nodos, y aplanarlos a curvas sería pagar segmentos por nada.
20// =============================================================================
21
22use alloc::vec::Vec;
23
24use kurbo::{BezPath, PathEl, Point, Rect, RoundedRect, RoundedRectRadii, Shape};
25use serde::{Deserialize, Serialize};
26
27/// Tolerancia de aplanado, igual que la de `llimphi-cpu` y la que vello
28/// codifica de fábrica. Más fino no se nota y cuesta segmentos.
29const TOLERANCIA: f64 = 0.1;
30
31/// Un segmento de camino. Espeja `kurbo::PathEl` en f32.
32#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
33pub enum SegWire {
34    /// `PathEl::MoveTo`.
35    MoverA([f32; 2]),
36    /// `PathEl::LineTo`.
37    LineaA([f32; 2]),
38    /// `PathEl::QuadTo` — control, fin.
39    CuadA([f32; 2], [f32; 2]),
40    /// `PathEl::CurveTo` — control 1, control 2, fin.
41    CurvaA([f32; 2], [f32; 2], [f32; 2]),
42    /// `PathEl::ClosePath`.
43    Cerrar,
44}
45
46/// Una forma lista para viajar.
47#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
48pub enum FormaWire {
49    /// Rectángulo recto `[x0, y0, x1, y1]` — 16 bytes y el camino barato del
50    /// otro lado (`vello_cpu` tiene `fill_rect` dedicado).
51    Rect([f32; 4]),
52    /// Rectángulo redondeado: el rect + los cuatro radios en orden
53    /// `[sup-izq, sup-der, inf-der, inf-izq]`.
54    Redondeado {
55        rect: [f32; 4],
56        radios: [f32; 4],
57    },
58    /// Cualquier otra cosa, aplanada a curvas.
59    Camino(Vec<SegWire>),
60}
61
62impl FormaWire {
63    /// Graba una forma cualquiera quedándose con la representación más barata.
64    ///
65    /// Pregunta por `as_rect`/`as_rounded_rect` antes de aplanar, igual que
66    /// `llimphi_cpu::Forma::de`. Es la misma decisión y conviene que lo sea:
67    /// si los dos lados clasifican distinto, el mismo dibujo pesaría distinto
68    /// según por dónde pasó.
69    pub fn de(forma: &impl Shape) -> Self {
70        if let Some(r) = forma.as_rect() {
71            Self::Rect(rect_a_wire(r))
72        } else if let Some(rr) = forma.as_rounded_rect() {
73            let radios = rr.radii();
74            Self::Redondeado {
75                rect: rect_a_wire(rr.rect()),
76                radios: [
77                    radios.top_left as f32,
78                    radios.top_right as f32,
79                    radios.bottom_right as f32,
80                    radios.bottom_left as f32,
81                ],
82            }
83        } else {
84            Self::Camino(camino_a_wire(&forma.into_path(TOLERANCIA)))
85        }
86    }
87
88    /// Reconstruye la forma del lado nativo.
89    ///
90    /// Devuelve el `Rect` aparte cuando lo es, para que el reproductor pueda
91    /// elegir la primitiva rápida sin re-inspeccionar el camino.
92    pub fn a_kurbo(&self) -> FormaKurbo {
93        match self {
94            Self::Rect(r) => FormaKurbo::Rect(wire_a_rect(*r)),
95            Self::Redondeado { rect, radios } => FormaKurbo::Redondeado(RoundedRect::from_rect(
96                wire_a_rect(*rect),
97                RoundedRectRadii::new(
98                    radios[0] as f64,
99                    radios[1] as f64,
100                    radios[2] as f64,
101                    radios[3] as f64,
102                ),
103            )),
104            Self::Camino(segs) => FormaKurbo::Camino(wire_a_camino(segs)),
105        }
106    }
107
108    /// Cuántos segmentos ocupa. Sólo para medir escenas — un `Rect` cuenta 1.
109    pub fn segmentos(&self) -> usize {
110        match self {
111            Self::Rect(_) | Self::Redondeado { .. } => 1,
112            Self::Camino(s) => s.len(),
113        }
114    }
115}
116
117/// El resultado de reconstruir: los tipos de kurbo, sin perder cuál era.
118#[derive(Clone, Debug)]
119pub enum FormaKurbo {
120    Rect(Rect),
121    Redondeado(RoundedRect),
122    Camino(BezPath),
123}
124
125impl FormaKurbo {
126    /// La forma como camino, para donde sólo se acepta `BezPath`.
127    pub fn a_camino(&self) -> BezPath {
128        match self {
129            Self::Rect(r) => r.into_path(TOLERANCIA),
130            Self::Redondeado(rr) => rr.into_path(TOLERANCIA),
131            Self::Camino(c) => c.clone(),
132        }
133    }
134}
135
136fn rect_a_wire(r: Rect) -> [f32; 4] {
137    [r.x0 as f32, r.y0 as f32, r.x1 as f32, r.y1 as f32]
138}
139
140fn wire_a_rect(w: [f32; 4]) -> Rect {
141    Rect::new(w[0] as f64, w[1] as f64, w[2] as f64, w[3] as f64)
142}
143
144fn punto_a_wire(p: Point) -> [f32; 2] {
145    [p.x as f32, p.y as f32]
146}
147
148fn wire_a_punto(w: [f32; 2]) -> Point {
149    Point::new(w[0] as f64, w[1] as f64)
150}
151
152fn camino_a_wire(camino: &BezPath) -> Vec<SegWire> {
153    camino
154        .elements()
155        .iter()
156        .map(|el| match el {
157            PathEl::MoveTo(p) => SegWire::MoverA(punto_a_wire(*p)),
158            PathEl::LineTo(p) => SegWire::LineaA(punto_a_wire(*p)),
159            PathEl::QuadTo(c, p) => SegWire::CuadA(punto_a_wire(*c), punto_a_wire(*p)),
160            PathEl::CurveTo(c1, c2, p) => SegWire::CurvaA(
161                punto_a_wire(*c1),
162                punto_a_wire(*c2),
163                punto_a_wire(*p),
164            ),
165            PathEl::ClosePath => SegWire::Cerrar,
166        })
167        .collect()
168}
169
170fn wire_a_camino(segs: &[SegWire]) -> BezPath {
171    let mut camino = BezPath::new();
172    for s in segs {
173        match s {
174            SegWire::MoverA(p) => camino.move_to(wire_a_punto(*p)),
175            SegWire::LineaA(p) => camino.line_to(wire_a_punto(*p)),
176            SegWire::CuadA(c, p) => camino.quad_to(wire_a_punto(*c), wire_a_punto(*p)),
177            SegWire::CurvaA(c1, c2, p) => camino.curve_to(
178                wire_a_punto(*c1),
179                wire_a_punto(*c2),
180                wire_a_punto(*p),
181            ),
182            SegWire::Cerrar => camino.close_path(),
183        }
184    }
185    camino
186}
187
188/// Empaqueta un `Affine` (6 f64) en 6 f32.
189pub fn afin_a_wire(a: kurbo::Affine) -> [f32; 6] {
190    let c = a.as_coeffs();
191    [
192        c[0] as f32,
193        c[1] as f32,
194        c[2] as f32,
195        c[3] as f32,
196        c[4] as f32,
197        c[5] as f32,
198    ]
199}
200
201/// Reconstruye un `Affine` desde el wire.
202pub fn wire_a_afin(w: [f32; 6]) -> kurbo::Affine {
203    kurbo::Affine::new([
204        w[0] as f64,
205        w[1] as f64,
206        w[2] as f64,
207        w[3] as f64,
208        w[4] as f64,
209        w[5] as f64,
210    ])
211}
212
213/// Dónde va algo: un punto cuando alcanza, la matriz entera cuando hace falta.
214///
215/// -----------------------------------------------------------------------------
216///  Por qué dos variantes en vez de una
217/// -----------------------------------------------------------------------------
218///  Porque las dos opciones simples estaban mal, cada una a su manera.
219///
220///  Guardar sólo el **punto** es lo barato (8 B) y es lo que casi siempre
221///  alcanza: el compositor posiciona el texto con `transform_acumulada *
222///  translate(esquina)`, y esa acumulada es una traslación pura en el caso
223///  normal (scroll, offset del padre). Pero *puede* traer rotación o escala de un
224///  ancestro animado, y ahí un punto pierde la información **sin que nada avise**:
225///  el texto de un subárbol rotado se pintaría derecho.
226///
227///  Guardar siempre la **matriz** es correcto y cuesta 24 B. Medido sobre 500
228///  etiquetas: el fotograma pasa de 45,8 a 53,6 KB (+17%) para pagar un caso que
229///  casi nunca ocurre.
230///
231///  La salida es mirar el afín y elegir. Si la parte lineal es la identidad, es
232///  una traslación y viaja como punto; si no, viaja entera. El caso raro sigue
233///  siendo exacto y el caso común sigue siendo barato — no se degrada nada en
234///  silencio, que era el único requisito no negociable.
235#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
236pub enum UbicacionWire {
237    /// Traslación pura — el caso normal.
238    Punto([f32; 2]),
239    /// Afín completo, cuando hay rotación, escala o sesgo.
240    Afin([f32; 6]),
241}
242
243impl UbicacionWire {
244    /// Empaqueta un afín quedándose con la representación más barata que sea
245    /// exacta.
246    pub fn de(a: kurbo::Affine) -> Self {
247        let c = a.as_coeffs();
248        if c[0] == 1.0 && c[1] == 0.0 && c[2] == 0.0 && c[3] == 1.0 {
249            Self::Punto([c[4] as f32, c[5] as f32])
250        } else {
251            Self::Afin(afin_a_wire(a))
252        }
253    }
254
255    /// El afín del lado nativo.
256    pub fn a_kurbo(self) -> kurbo::Affine {
257        match self {
258            Self::Punto(p) => kurbo::Affine::translate((p[0] as f64, p[1] as f64)),
259            Self::Afin(m) => wire_a_afin(m),
260        }
261    }
262
263    /// Compone por izquierda, igual que `Affine::mul`. Vuelve a elegir la
264    /// representación: trasladar una traslación sigue siendo una traslación, y
265    /// perder eso engordaría el cable en cada `append`.
266    pub fn componer(self, t: kurbo::Affine) -> Self {
267        Self::de(t * self.a_kurbo())
268    }
269
270    /// La traslación resultante, para tests y para posicionar sin transformar.
271    pub fn traslacion(self) -> (f32, f32) {
272        match self {
273            Self::Punto(p) => (p[0], p[1]),
274            Self::Afin(m) => (m[4], m[5]),
275        }
276    }
277}