Skip to main content

pineal_render/
llimphi_backend.rs

1//! Backend del trait [`crate::Canvas`] sobre un `vello::Scene` de Llimphi.
2//!
3//! Bajo el feature `llimphi`. Traduce los primitivos de pineal a las
4//! llamadas nativas de vello/peniko/kurbo. No introduce dependencia
5//! transitiva a `llimphi-ui` en los crates de visualización — éstos
6//! siguen hablando contra el trait abstracto; sólo la función `view`
7//! de cada widget enchufa este backend en un `View::paint_with`.
8//!
9//! Paridad con el backend GPUI:
10//! - `push_clip` / `pop_clip` quedan como no-op por ahora — el `View`
11//!   contenedor ya recorta vía taffy + `clip(true)`; los painters de
12//!   pineal respetan su `plot_rect` y no salen del bounds.
13//! - `fill_triangle_strip` arma un `BezPath` por triángulo y lo rellena
14//!   con `Fill::NonZero` + el color promedio de los tres vértices. Vello
15//!   no expone mesh con per-vertex color directo, así que el promedio es
16//!   el trade-off (igual que el exporter SVG). Suficiente para wedges
17//!   pie/donut, ribbons Sankey, polígono fan del radar y phosphor trail.
18//!
19//! Coordenadas: pineal trabaja en pixels absolutos del scene (mismo
20//! origen que `PaintRect.{x,y}`). No traduce — los callers ya
21//! construyen sus rects en términos del `PaintRect` recibido.
22//!
23//! El texto se rendea usando el `Typesetter` cacheado del runtime; no
24//! creamos `FontContext` por frame.
25
26use crate::{Canvas, Color, Point, Rect, StrokeStyle};
27use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Line, Rect as KurboRect, Stroke};
28use llimphi_ui::llimphi_raster::peniko::{Color as PenikoColor, Fill};
29use llimphi_ui::llimphi_raster::vello::Scene;
30use llimphi_ui::llimphi_text::{draw_block, TextBlock, Typesetter};
31
32/// Adapter que pinta sobre un `&mut vello::Scene` + `&mut Typesetter`.
33///
34/// Construir uno nuevo dentro del closure de [`llimphi_ui::View::paint_with`].
35/// Vida útil del borrow del scene iguala la del frame.
36pub struct SceneCanvas<'a> {
37    scene: &'a mut Scene,
38    typesetter: &'a mut Typesetter,
39}
40
41impl<'a> SceneCanvas<'a> {
42    pub fn new(scene: &'a mut Scene, typesetter: &'a mut Typesetter) -> Self {
43        Self { scene, typesetter }
44    }
45}
46
47/// Convierte el `Color` RGBA lineal de pineal al `Color` peniko. Mantiene
48/// la convención sin gamma del resto del codebase. peniko trabaja en
49/// `[0, 255]` enteros para el canal alfa también, así que clampeamos y
50/// multiplicamos.
51fn to_peniko(c: Color) -> PenikoColor {
52    let to_byte = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
53    PenikoColor::from_rgba8(to_byte(c.r), to_byte(c.g), to_byte(c.b), to_byte(c.a))
54}
55
56fn avg_color(cs: &[Option<Color>]) -> Color {
57    let mut acc = Color::rgba(0.0, 0.0, 0.0, 0.0);
58    let mut n = 0.0;
59    for c in cs.iter().flatten() {
60        acc.r += c.r;
61        acc.g += c.g;
62        acc.b += c.b;
63        acc.a += c.a;
64        n += 1.0;
65    }
66    if n == 0.0 {
67        return Color::TRANSPARENT;
68    }
69    Color::rgba(acc.r / n, acc.g / n, acc.b / n, acc.a / n)
70}
71
72fn to_kurbo_rect(r: Rect) -> KurboRect {
73    KurboRect::new(
74        r.x as f64,
75        r.y as f64,
76        (r.x + r.w) as f64,
77        (r.y + r.h) as f64,
78    )
79}
80
81impl<'a> Canvas for SceneCanvas<'a> {
82    fn push_clip(&mut self, _rect: Rect) {
83        // No-op por ahora. El View contenedor recorta vía taffy + clip(true).
84    }
85    fn pop_clip(&mut self) {}
86
87    fn fill_rect(&mut self, rect: Rect, color: Color) {
88        self.scene.fill(
89            Fill::NonZero,
90            Affine::IDENTITY,
91            to_peniko(color),
92            None,
93            &to_kurbo_rect(rect),
94        );
95    }
96
97    fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
98        self.scene.stroke(
99            &Stroke::new(stroke.width as f64),
100            Affine::IDENTITY,
101            to_peniko(stroke.color),
102            None,
103            &to_kurbo_rect(rect),
104        );
105    }
106
107    fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
108        let line = Line::new((a.x as f64, a.y as f64), (b.x as f64, b.y as f64));
109        self.scene.stroke(
110            &Stroke::new(stroke.width as f64),
111            Affine::IDENTITY,
112            to_peniko(stroke.color),
113            None,
114            &line,
115        );
116    }
117
118    fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
119        if coords.len() < 4 {
120            return; // <2 puntos → no hay segmento
121        }
122        let mut path = BezPath::new();
123        path.move_to((coords[0] as f64, coords[1] as f64));
124        let mut i = 2;
125        while i + 1 < coords.len() {
126            path.line_to((coords[i] as f64, coords[i + 1] as f64));
127            i += 2;
128        }
129        self.scene.stroke(
130            &Stroke::new(stroke.width as f64),
131            Affine::IDENTITY,
132            to_peniko(stroke.color),
133            None,
134            &path,
135        );
136    }
137
138    fn fill_triangle_strip(&mut self, coords: &[f32], colors: &[Color]) {
139        let n = coords.len() / 2;
140        if n < 3 {
141            return;
142        }
143        for t in 0..n - 2 {
144            let i0 = t;
145            let i1 = t + 1;
146            let i2 = t + 2;
147            let avg = avg_color(&[
148                colors.get(i0).copied(),
149                colors.get(i1).copied(),
150                colors.get(i2).copied(),
151            ]);
152            let mut path = BezPath::new();
153            path.move_to((coords[i0 * 2] as f64, coords[i0 * 2 + 1] as f64));
154            path.line_to((coords[i1 * 2] as f64, coords[i1 * 2 + 1] as f64));
155            path.line_to((coords[i2 * 2] as f64, coords[i2 * 2 + 1] as f64));
156            path.close_path();
157            self.scene.fill(
158                Fill::NonZero,
159                Affine::IDENTITY,
160                to_peniko(avg),
161                None,
162                &path,
163            );
164        }
165    }
166
167    fn draw_text(&mut self, p: Point, text: &str, color: Color, size_px: f32) {
168        if text.is_empty() {
169            return;
170        }
171        // Reutiliza el typesetter cacheado del runtime — `TextBlock::simple`
172        // pone el origen donde pineal lo pide. `draw_block` hace shape +
173        // glyph_run en una pasada.
174        let block = TextBlock::simple(text, size_px, to_peniko(color), (p.x as f64, p.y as f64));
175        draw_block(self.scene, self.typesetter, &block);
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn rojo_redondea_a_255() {
185        let c = to_peniko(Color::rgb(1.0, 0.0, 0.0)).to_rgba8();
186        assert_eq!((c.r, c.g, c.b, c.a), (255, 0, 0, 255));
187    }
188
189    #[test]
190    fn alpha_pasa_directo() {
191        let c = to_peniko(Color::rgba(0.0, 0.0, 1.0, 0.25)).to_rgba8();
192        assert_eq!(c.b, 255);
193        assert_eq!(c.a, 64); // 0.25 * 255 = 63.75 → 64
194    }
195
196    #[test]
197    fn fuera_de_rango_clampea() {
198        let c = to_peniko(Color::rgba(1.5, -0.2, 0.5, 1.0)).to_rgba8();
199        assert_eq!((c.r, c.g, c.b), (255, 0, 128));
200    }
201}