Skip to main content

r/interpreter/grid/
render.rs

1//! Grid renderer trait and display list replay.
2//!
3//! The `GridRenderer` trait defines the abstract drawing API that backends
4//! (SVG, egui, PDF, etc.) must implement. The `replay` function walks a
5//! display list and calls the renderer methods with resolved coordinates.
6
7use super::display::{DisplayItem, DisplayList};
8use super::gpar::Gpar;
9use super::grob::{Grob, GrobStore};
10use super::viewport::{ViewportStack, ViewportTransform};
11
12// region: GridRenderer trait
13
14/// Abstract renderer for grid graphics.
15///
16/// All coordinates are in centimeters from the device origin (bottom-left).
17/// The renderer converts these to whatever coordinate system the backend uses.
18pub trait GridRenderer {
19    /// Draw a single line segment.
20    fn line(&mut self, x0_cm: f64, y0_cm: f64, x1_cm: f64, y1_cm: f64, gp: &Gpar);
21
22    /// Draw a connected polyline (multiple segments sharing endpoints).
23    fn polyline(&mut self, x_cm: &[f64], y_cm: &[f64], gp: &Gpar);
24
25    /// Draw a rectangle.
26    fn rect(&mut self, x_cm: f64, y_cm: f64, w_cm: f64, h_cm: f64, gp: &Gpar);
27
28    /// Draw a circle.
29    fn circle(&mut self, x_cm: f64, y_cm: f64, r_cm: f64, gp: &Gpar);
30
31    /// Draw a filled polygon.
32    fn polygon(&mut self, x_cm: &[f64], y_cm: &[f64], gp: &Gpar);
33
34    /// Draw a text label.
35    fn text(&mut self, x_cm: f64, y_cm: f64, label: &str, rot: f64, gp: &Gpar);
36
37    /// Draw a point (plotting symbol).
38    fn point(&mut self, x_cm: f64, y_cm: f64, pch: u8, size_cm: f64, gp: &Gpar);
39
40    /// Set a clipping rectangle. All subsequent drawing is clipped to this region.
41    fn clip(&mut self, x_cm: f64, y_cm: f64, w_cm: f64, h_cm: f64);
42
43    /// Remove the most recently set clipping rectangle.
44    fn unclip(&mut self);
45
46    /// Return the device size in centimeters (width, height).
47    fn device_size_cm(&self) -> (f64, f64);
48}
49
50// endregion
51
52// region: Replay
53
54/// Replay a display list through a renderer, resolving all units to device cm.
55///
56/// This walks the display list items in order:
57/// - `PushViewport` pushes a viewport and optionally clips
58/// - `PopViewport` pops and unclips
59/// - `Draw` resolves the grob's units against the current viewport transform
60///   and calls the appropriate renderer method
61pub fn replay(list: &DisplayList, store: &GrobStore, renderer: &mut dyn GridRenderer) {
62    let (dev_w, dev_h) = renderer.device_size_cm();
63    let mut vp_stack = ViewportStack::new(dev_w, dev_h);
64    let mut transform = ViewportTransform::root(dev_w, dev_h);
65    let mut transform_stack: Vec<ViewportTransform> = vec![transform.clone()];
66    let mut clip_depth: usize = 0;
67
68    for item in list.items() {
69        match item {
70            DisplayItem::PushViewport(vp) => {
71                transform = ViewportTransform::from_viewport(vp, &transform);
72                transform_stack.push(transform.clone());
73                vp_stack.push((**vp).clone());
74
75                if vp.clip {
76                    renderer.clip(
77                        transform.x_offset_cm,
78                        transform.y_offset_cm,
79                        transform.width_cm,
80                        transform.height_cm,
81                    );
82                    clip_depth += 1;
83                }
84            }
85            DisplayItem::PopViewport => {
86                let popped = vp_stack.pop();
87                if transform_stack.len() > 1 {
88                    transform_stack.pop();
89                }
90                transform = transform_stack
91                    .last()
92                    .expect("transform stack always has at least the root")
93                    .clone();
94
95                if let Some(vp) = popped {
96                    if vp.clip && clip_depth > 0 {
97                        renderer.unclip();
98                        clip_depth -= 1;
99                    }
100                }
101            }
102            DisplayItem::Draw(grob_id) => {
103                if let Some(grob) = store.get(*grob_id) {
104                    render_grob(grob, &transform, store, renderer);
105                }
106            }
107        }
108    }
109
110    // Clean up any unclosed clips
111    for _ in 0..clip_depth {
112        renderer.unclip();
113    }
114}
115
116/// Render a single grob using the current viewport transform.
117fn render_grob(
118    grob: &Grob,
119    transform: &ViewportTransform,
120    store: &GrobStore,
121    renderer: &mut dyn GridRenderer,
122) {
123    let ctx = transform.unit_context();
124
125    match grob {
126        Grob::Lines { x, y, gp } => {
127            let n = x.len().min(y.len());
128            if n < 2 {
129                return;
130            }
131            let x_cm: Vec<f64> = (0..n)
132                .map(|i| transform.x_offset_cm + ctx.resolve_x(x, i))
133                .collect();
134            let y_cm: Vec<f64> = (0..n)
135                .map(|i| transform.y_offset_cm + ctx.resolve_y(y, i))
136                .collect();
137            renderer.polyline(&x_cm, &y_cm, gp);
138        }
139
140        Grob::Segments { x0, y0, x1, y1, gp } => {
141            let n = x0.len().min(y0.len()).min(x1.len()).min(y1.len());
142            for i in 0..n {
143                renderer.line(
144                    transform.x_offset_cm + ctx.resolve_x(x0, i),
145                    transform.y_offset_cm + ctx.resolve_y(y0, i),
146                    transform.x_offset_cm + ctx.resolve_x(x1, i),
147                    transform.y_offset_cm + ctx.resolve_y(y1, i),
148                    gp,
149                );
150            }
151        }
152
153        Grob::Points {
154            x,
155            y,
156            pch,
157            size,
158            gp,
159        } => {
160            let n = x.len().min(y.len());
161            for i in 0..n {
162                let size_cm = ctx.resolve_size(size, i.min(size.len().saturating_sub(1)));
163                renderer.point(
164                    transform.x_offset_cm + ctx.resolve_x(x, i),
165                    transform.y_offset_cm + ctx.resolve_y(y, i),
166                    *pch,
167                    size_cm,
168                    gp,
169                );
170            }
171        }
172
173        Grob::Rect {
174            x,
175            y,
176            width,
177            height,
178            just,
179            gp,
180        } => {
181            let n = x.len().min(y.len());
182            for i in 0..n {
183                let cx = transform.x_offset_cm + ctx.resolve_x(x, i);
184                let cy = transform.y_offset_cm + ctx.resolve_y(y, i);
185                let w = ctx.resolve_x(width, i.min(width.len().saturating_sub(1)));
186                let h = ctx.resolve_y(height, i.min(height.len().saturating_sub(1)));
187                let rx = cx - just.0.as_fraction() * w;
188                let ry = cy - just.1.as_fraction() * h;
189                renderer.rect(rx, ry, w, h, gp);
190            }
191        }
192
193        Grob::Circle { x, y, r, gp } => {
194            let n = x.len().min(y.len());
195            for i in 0..n {
196                let r_cm = ctx.resolve_size(r, i.min(r.len().saturating_sub(1)));
197                renderer.circle(
198                    transform.x_offset_cm + ctx.resolve_x(x, i),
199                    transform.y_offset_cm + ctx.resolve_y(y, i),
200                    r_cm,
201                    gp,
202                );
203            }
204        }
205
206        Grob::Polygon { x, y, gp } => {
207            let n = x.len().min(y.len());
208            if n < 3 {
209                return;
210            }
211            let x_cm: Vec<f64> = (0..n)
212                .map(|i| transform.x_offset_cm + ctx.resolve_x(x, i))
213                .collect();
214            let y_cm: Vec<f64> = (0..n)
215                .map(|i| transform.y_offset_cm + ctx.resolve_y(y, i))
216                .collect();
217            renderer.polygon(&x_cm, &y_cm, gp);
218        }
219
220        Grob::Text {
221            label,
222            x,
223            y,
224            just: _just,
225            rot,
226            gp,
227        } => {
228            let n = label.len().min(x.len()).min(y.len());
229            for (i, lbl) in label.iter().enumerate().take(n) {
230                renderer.text(
231                    transform.x_offset_cm + ctx.resolve_x(x, i),
232                    transform.y_offset_cm + ctx.resolve_y(y, i),
233                    lbl,
234                    *rot,
235                    gp,
236                );
237            }
238        }
239
240        Grob::Collection { children } => {
241            for &child_id in children {
242                if let Some(child) = store.get(child_id) {
243                    render_grob(child, transform, store, renderer);
244                }
245            }
246        }
247    }
248}
249
250// endregion