Skip to main content

runmat_plot/export/
cpu_surface.rs

1use crate::core::{Camera, PipelineType, ProjectionType, RenderData};
2use crate::plots::{Figure, PlotElement};
3use crate::styling::PlotThemeConfig;
4use font8x8::{UnicodeFonts, BASIC_FONTS};
5use glam::{Vec2, Vec3, Vec4};
6
7#[derive(Clone, Debug)]
8struct AxesView {
9    viewport: (u32, u32, u32, u32),
10    plot_rect: (u32, u32, u32, u32),
11    bounds_2d: (f32, f32, f32, f32),
12    bounds_3d: (Vec3, Vec3),
13    camera_3d: Option<Camera>,
14    has_3d_content: bool,
15    title: Option<String>,
16    x_label: Option<String>,
17    y_label: Option<String>,
18    z_label: Option<String>,
19    title_scale: u32,
20    label_scale: u32,
21    tick_scale: u32,
22    show_grid: bool,
23    show_box: bool,
24}
25
26#[derive(Clone, Copy, Debug)]
27struct ScreenVertex {
28    x: f32,
29    y: f32,
30    z: f32,
31    color: [u8; 4],
32}
33
34struct Canvas {
35    width: u32,
36    height: u32,
37    pixels: Vec<u8>,
38    depth: Vec<f32>,
39}
40
41impl Canvas {
42    fn new(width: u32, height: u32, background: [u8; 4]) -> Self {
43        let mut pixels = vec![0u8; (width.max(1) * height.max(1) * 4) as usize];
44        for px in pixels.chunks_exact_mut(4) {
45            px.copy_from_slice(&background);
46        }
47        let depth = vec![f32::INFINITY; (width.max(1) * height.max(1)) as usize];
48        Self {
49            width: width.max(1),
50            height: height.max(1),
51            pixels,
52            depth,
53        }
54    }
55
56    fn rgba(self) -> Vec<u8> {
57        self.pixels
58    }
59
60    fn blend_pixel(&mut self, x: i32, y: i32, rgba: [u8; 4], depth: f32, use_depth: bool) {
61        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
62            return;
63        }
64        let idx = (y as u32 * self.width + x as u32) as usize;
65        if use_depth {
66            if !depth.is_finite() || depth >= self.depth[idx] {
67                return;
68            }
69            self.depth[idx] = depth;
70        }
71
72        let p = idx * 4;
73        let src_a = rgba[3] as f32 / 255.0;
74        let dst_a = self.pixels[p + 3] as f32 / 255.0;
75        let out_a = src_a + dst_a * (1.0 - src_a);
76        if out_a <= f32::EPSILON {
77            self.pixels[p..p + 4].copy_from_slice(&[0, 0, 0, 0]);
78            return;
79        }
80        for (i, src_u8) in rgba.iter().take(3).enumerate() {
81            let src = *src_u8 as f32 / 255.0;
82            let dst = self.pixels[p + i] as f32 / 255.0;
83            let out = (src * src_a + dst * dst_a * (1.0 - src_a)) / out_a;
84            self.pixels[p + i] = (out.clamp(0.0, 1.0) * 255.0) as u8;
85        }
86        self.pixels[p + 3] = (out_a.clamp(0.0, 1.0) * 255.0) as u8;
87    }
88
89    fn draw_disc(&mut self, center: Vec2, radius: f32, rgba: [u8; 4], depth: f32, use_depth: bool) {
90        let r = radius.max(0.5);
91        let min_x = (center.x - r).floor() as i32;
92        let max_x = (center.x + r).ceil() as i32;
93        let min_y = (center.y - r).floor() as i32;
94        let max_y = (center.y + r).ceil() as i32;
95        let rr = r * r;
96        for y in min_y..=max_y {
97            for x in min_x..=max_x {
98                let dx = x as f32 + 0.5 - center.x;
99                let dy = y as f32 + 0.5 - center.y;
100                if dx * dx + dy * dy <= rr {
101                    self.blend_pixel(x, y, rgba, depth, use_depth);
102                }
103            }
104        }
105    }
106
107    fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, rgba: [u8; 4]) {
108        if w <= 0 || h <= 0 {
109            return;
110        }
111        let x0 = x.max(0);
112        let y0 = y.max(0);
113        let x1 = (x + w).min(self.width as i32);
114        let y1 = (y + h).min(self.height as i32);
115        for yy in y0..y1 {
116            for xx in x0..x1 {
117                self.blend_pixel(xx, yy, rgba, 0.0, false);
118            }
119        }
120    }
121
122    fn stroke_rect(&mut self, x: i32, y: i32, w: i32, h: i32, rgba: [u8; 4], width_px: f32) {
123        let l = x as f32;
124        let r = (x + w - 1) as f32;
125        let t = y as f32;
126        let b = (y + h - 1) as f32;
127        let c = rgba;
128        self.draw_line(
129            ScreenVertex {
130                x: l,
131                y: t,
132                z: 0.0,
133                color: c,
134            },
135            ScreenVertex {
136                x: r,
137                y: t,
138                z: 0.0,
139                color: c,
140            },
141            width_px,
142            0,
143            false,
144        );
145        self.draw_line(
146            ScreenVertex {
147                x: r,
148                y: t,
149                z: 0.0,
150                color: c,
151            },
152            ScreenVertex {
153                x: r,
154                y: b,
155                z: 0.0,
156                color: c,
157            },
158            width_px,
159            0,
160            false,
161        );
162        self.draw_line(
163            ScreenVertex {
164                x: r,
165                y: b,
166                z: 0.0,
167                color: c,
168            },
169            ScreenVertex {
170                x: l,
171                y: b,
172                z: 0.0,
173                color: c,
174            },
175            width_px,
176            0,
177            false,
178        );
179        self.draw_line(
180            ScreenVertex {
181                x: l,
182                y: b,
183                z: 0.0,
184                color: c,
185            },
186            ScreenVertex {
187                x: l,
188                y: t,
189                z: 0.0,
190                color: c,
191            },
192            width_px,
193            0,
194            false,
195        );
196    }
197
198    fn draw_line(
199        &mut self,
200        a: ScreenVertex,
201        b: ScreenVertex,
202        width_px: f32,
203        style_code: i32,
204        use_depth: bool,
205    ) {
206        let radius = width_px.max(1.0) * 0.5;
207        let segments = dash_segments(a, b, style_code, radius.max(1.0));
208        for (s0, s1) in segments {
209            self.draw_capsule_segment(s0, s1, radius, use_depth);
210        }
211    }
212
213    fn draw_capsule_segment(
214        &mut self,
215        a: ScreenVertex,
216        b: ScreenVertex,
217        radius: f32,
218        use_depth: bool,
219    ) {
220        let min_x = (a.x.min(b.x) - radius - 1.0).floor() as i32;
221        let max_x = (a.x.max(b.x) + radius + 1.0).ceil() as i32;
222        let min_y = (a.y.min(b.y) - radius - 1.0).floor() as i32;
223        let max_y = (a.y.max(b.y) + radius + 1.0).ceil() as i32;
224
225        let av = Vec2::new(a.x, a.y);
226        let bv = Vec2::new(b.x, b.y);
227        let ab = bv - av;
228        let ab_len2 = ab.length_squared().max(1e-8);
229
230        for y in min_y..=max_y {
231            for x in min_x..=max_x {
232                let p = Vec2::new(x as f32 + 0.5, y as f32 + 0.5);
233                let t = ((p - av).dot(ab) / ab_len2).clamp(0.0, 1.0);
234                let closest = av + ab * t;
235                let dist = p.distance(closest);
236                if dist > radius + 1.0 {
237                    continue;
238                }
239                let coverage = (radius + 1.0 - dist).clamp(0.0, 1.0);
240                if coverage <= 0.0 {
241                    continue;
242                }
243
244                let depth = a.z + (b.z - a.z) * t;
245                let mut color = lerp_rgba(a.color, b.color, t);
246                color[3] = ((color[3] as f32) * coverage).round().clamp(0.0, 255.0) as u8;
247                self.blend_pixel(x, y, color, depth, use_depth);
248            }
249        }
250    }
251
252    fn fill_triangle(
253        &mut self,
254        v0: ScreenVertex,
255        v1: ScreenVertex,
256        v2: ScreenVertex,
257        use_depth: bool,
258    ) {
259        let min_x = v0.x.min(v1.x).min(v2.x).floor() as i32;
260        let max_x = v0.x.max(v1.x).max(v2.x).ceil() as i32;
261        let min_y = v0.y.min(v1.y).min(v2.y).floor() as i32;
262        let max_y = v0.y.max(v1.y).max(v2.y).ceil() as i32;
263
264        let p0 = Vec2::new(v0.x, v0.y);
265        let p1 = Vec2::new(v1.x, v1.y);
266        let p2 = Vec2::new(v2.x, v2.y);
267        let area = edge_fn(p0, p1, p2);
268        if area.abs() <= f32::EPSILON {
269            return;
270        }
271
272        for y in min_y..=max_y {
273            for x in min_x..=max_x {
274                let p = Vec2::new(x as f32 + 0.5, y as f32 + 0.5);
275                let w0 = edge_fn(p1, p2, p) / area;
276                let w1 = edge_fn(p2, p0, p) / area;
277                let w2 = edge_fn(p0, p1, p) / area;
278                if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
279                    continue;
280                }
281                let depth = w0 * v0.z + w1 * v1.z + w2 * v2.z;
282                let color = blend_barycentric_rgba(v0.color, v1.color, v2.color, w0, w1, w2);
283                self.blend_pixel(x, y, color, depth, use_depth);
284            }
285        }
286    }
287}
288
289fn edge_fn(a: Vec2, b: Vec2, c: Vec2) -> f32 {
290    (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)
291}
292
293fn blend_barycentric_rgba(
294    c0: [u8; 4],
295    c1: [u8; 4],
296    c2: [u8; 4],
297    w0: f32,
298    w1: f32,
299    w2: f32,
300) -> [u8; 4] {
301    let mix = |a: u8, b: u8, c: u8| -> u8 {
302        (a as f32 * w0 + b as f32 * w1 + c as f32 * w2)
303            .round()
304            .clamp(0.0, 255.0) as u8
305    };
306    [
307        mix(c0[0], c1[0], c2[0]),
308        mix(c0[1], c1[1], c2[1]),
309        mix(c0[2], c1[2], c2[2]),
310        mix(c0[3], c1[3], c2[3]),
311    ]
312}
313
314fn lerp_rgba(a: [u8; 4], b: [u8; 4], t: f32) -> [u8; 4] {
315    let mix = |x: u8, y: u8| -> u8 {
316        (x as f32 + (y as f32 - x as f32) * t)
317            .round()
318            .clamp(0.0, 255.0) as u8
319    };
320    [
321        mix(a[0], b[0]),
322        mix(a[1], b[1]),
323        mix(a[2], b[2]),
324        mix(a[3], b[3]),
325    ]
326}
327
328fn with_alpha(color: [u8; 4], alpha_scale: f32) -> [u8; 4] {
329    let mut out = color;
330    out[3] = ((out[3] as f32) * alpha_scale).round().clamp(0.0, 255.0) as u8;
331    out
332}
333
334fn dash_segments(
335    a: ScreenVertex,
336    b: ScreenVertex,
337    style_code: i32,
338    width_px: f32,
339) -> Vec<(ScreenVertex, ScreenVertex)> {
340    let dx = b.x - a.x;
341    let dy = b.y - a.y;
342    let len = (dx * dx + dy * dy).sqrt();
343    if len <= 1e-5 {
344        return vec![(a, b)];
345    }
346    let pattern = match style_code {
347        1 => vec![(6.0 * width_px, true), (6.0 * width_px, false)],
348        2 => vec![(1.5 * width_px, true), (5.0 * width_px, false)],
349        3 => vec![
350            (6.0 * width_px, true),
351            (4.0 * width_px, false),
352            (1.5 * width_px, true),
353            (4.0 * width_px, false),
354        ],
355        _ => return vec![(a, b)],
356    };
357
358    let mut out = Vec::new();
359    let mut s = 0.0f32;
360    let mut pi = 0usize;
361    while s < len {
362        let (step, draw) = pattern[pi % pattern.len()];
363        let e = (s + step.max(1.0)).min(len);
364        if draw {
365            let t0 = s / len;
366            let t1 = e / len;
367            out.push((lerp_screen(a, b, t0), lerp_screen(a, b, t1)));
368        }
369        s = e;
370        pi += 1;
371    }
372    out
373}
374
375fn lerp_screen(a: ScreenVertex, b: ScreenVertex, t: f32) -> ScreenVertex {
376    ScreenVertex {
377        x: a.x + (b.x - a.x) * t,
378        y: a.y + (b.y - a.y) * t,
379        z: a.z + (b.z - a.z) * t,
380        color: lerp_rgba(a.color, b.color, t),
381    }
382}
383
384fn to_u8_rgba(color: [f32; 4]) -> [u8; 4] {
385    [
386        (color[0].clamp(0.0, 1.0) * 255.0) as u8,
387        (color[1].clamp(0.0, 1.0) * 255.0) as u8,
388        (color[2].clamp(0.0, 1.0) * 255.0) as u8,
389        (color[3].clamp(0.0, 1.0) * 255.0) as u8,
390    ]
391}
392
393fn is_default_figure_bg(bg: Vec4) -> bool {
394    const EPS: f32 = 1e-3;
395    (bg.x - 1.0).abs() <= EPS
396        && (bg.y - 1.0).abs() <= EPS
397        && (bg.z - 1.0).abs() <= EPS
398        && (bg.w - 1.0).abs() <= EPS
399}
400
401fn compute_tiled_viewports(
402    width: u32,
403    height: u32,
404    rows: usize,
405    cols: usize,
406) -> Vec<(u32, u32, u32, u32)> {
407    if rows == 0 || cols == 0 {
408        return vec![(0, 0, width.max(1), height.max(1))];
409    }
410    let rows_u = rows as u32;
411    let cols_u = cols as u32;
412    let cell_w = (width / cols_u).max(1);
413    let cell_h = (height / rows_u).max(1);
414    let mut out = Vec::with_capacity(rows * cols);
415    for r in 0..rows_u {
416        for c in 0..cols_u {
417            let x = c * cell_w;
418            let y = r * cell_h;
419            let w = if c + 1 == cols_u {
420                width.saturating_sub(x).max(1)
421            } else {
422                cell_w
423            };
424            let h = if r + 1 == rows_u {
425                height.saturating_sub(y).max(1)
426            } else {
427                cell_h
428            };
429            out.push((x, y, w, h));
430        }
431    }
432    out
433}
434
435fn compute_plot_rect(viewport: (u32, u32, u32, u32), has_3d: bool) -> (u32, u32, u32, u32) {
436    let (vx, vy, vw, vh) = viewport;
437    let left = if has_3d { 48 } else { 62 };
438    let right = 24;
439    let top = if has_3d { 34 } else { 40 };
440    let bottom = if has_3d { 48 } else { 54 };
441
442    let px = vx + left.min(vw.saturating_sub(2));
443    let py = vy + top.min(vh.saturating_sub(2));
444    let pw = vw
445        .saturating_sub(left + right)
446        .max(vw.saturating_sub(2).max(1));
447    let ph = vh
448        .saturating_sub(top + bottom)
449        .max(vh.saturating_sub(2).max(1));
450    (px, py, pw.max(1), ph.max(1))
451}
452
453fn project_2d(
454    pos: Vec3,
455    plot_rect: (u32, u32, u32, u32),
456    bounds: (f32, f32, f32, f32),
457    color: [u8; 4],
458) -> ScreenVertex {
459    let (x_min, x_max, y_min, y_max) = bounds;
460    let xr = (x_max - x_min).max(1e-6);
461    let yr = (y_max - y_min).max(1e-6);
462    let tx = ((pos.x - x_min) / xr).clamp(0.0, 1.0);
463    let ty = ((pos.y - y_min) / yr).clamp(0.0, 1.0);
464    let sx = plot_rect.0 as f32 + tx * plot_rect.2.max(1) as f32;
465    let sy = plot_rect.1 as f32 + (1.0 - ty) * plot_rect.3.max(1) as f32;
466    ScreenVertex {
467        x: sx,
468        y: sy,
469        z: 0.0,
470        color,
471    }
472}
473
474fn project_3d(
475    pos: Vec3,
476    plot_rect: (u32, u32, u32, u32),
477    camera: &Camera,
478    color: [u8; 4],
479) -> Option<ScreenVertex> {
480    let mut cam = camera.clone();
481    cam.update_aspect_ratio((plot_rect.2.max(1) as f32) / (plot_rect.3.max(1) as f32));
482    let vp = cam.view_proj_matrix();
483    let clip = vp * pos.extend(1.0);
484    if clip.w.abs() <= 1e-6 {
485        return None;
486    }
487    let ndc = clip.truncate() / clip.w;
488    if ndc.z < -1.2 || ndc.z > 1.2 {
489        return None;
490    }
491    let sx = plot_rect.0 as f32 + (ndc.x * 0.5 + 0.5) * plot_rect.2.max(1) as f32;
492    let sy = plot_rect.1 as f32 + (1.0 - (ndc.y * 0.5 + 0.5)) * plot_rect.3.max(1) as f32;
493    let depth = ndc.z * 0.5 + 0.5;
494    Some(ScreenVertex {
495        x: sx,
496        y: sy,
497        z: depth,
498        color,
499    })
500}
501
502fn axes_has_3d_content(figure: &Figure, axes_index: usize) -> bool {
503    figure
504        .plots()
505        .zip(figure.plot_axes_indices().iter().copied())
506        .any(|(plot, idx)| {
507            idx == axes_index
508                && match plot {
509                    PlotElement::Surface(surface) => !surface.image_mode,
510                    PlotElement::Patch(patch) => {
511                        patch.force_3d() || patch.vertices().iter().any(|p| p.z.abs() > 1e-6)
512                    }
513                    PlotElement::Line3(_) | PlotElement::Scatter3(_) => true,
514                    _ => false,
515                }
516        })
517}
518
519fn choose_axes_bounds(
520    figure: &Figure,
521    axes_index: usize,
522    render_data: &[(usize, RenderData)],
523) -> (f32, f32, f32, f32) {
524    let mut min_x = f32::INFINITY;
525    let mut max_x = f32::NEG_INFINITY;
526    let mut min_y = f32::INFINITY;
527    let mut max_y = f32::NEG_INFINITY;
528
529    for (ax, rd) in render_data {
530        if *ax != axes_index {
531            continue;
532        }
533        if let Some(bounds) = rd.bounds {
534            min_x = min_x.min(bounds.min.x);
535            max_x = max_x.max(bounds.max.x);
536            min_y = min_y.min(bounds.min.y);
537            max_y = max_y.max(bounds.max.y);
538        }
539    }
540
541    if !min_x.is_finite() || !max_x.is_finite() || !min_y.is_finite() || !max_y.is_finite() {
542        min_x = -1.0;
543        max_x = 1.0;
544        min_y = -1.0;
545        max_y = 1.0;
546    }
547
548    if let Some(meta) = figure.axes_metadata(axes_index) {
549        if let Some((l, r)) = meta.x_limits {
550            min_x = l as f32;
551            max_x = r as f32;
552        }
553        if let Some((b, t)) = meta.y_limits {
554            min_y = b as f32;
555            max_y = t as f32;
556        }
557        if meta.axis_equal {
558            let cx = (min_x + max_x) * 0.5;
559            let cy = (min_y + max_y) * 0.5;
560            let size = (max_x - min_x).abs().max((max_y - min_y).abs()).max(0.1);
561            min_x = cx - size * 0.5;
562            max_x = cx + size * 0.5;
563            min_y = cy - size * 0.5;
564            max_y = cy + size * 0.5;
565        }
566    }
567
568    (min_x, max_x, min_y, max_y)
569}
570
571fn choose_axes_bounds_3d(
572    axes_index: usize,
573    render_data: &[(usize, RenderData)],
574    bounds_2d: (f32, f32, f32, f32),
575) -> (Vec3, Vec3) {
576    let mut min = Vec3::splat(f32::INFINITY);
577    let mut max = Vec3::splat(f32::NEG_INFINITY);
578
579    for (ax, rd) in render_data {
580        if *ax != axes_index {
581            continue;
582        }
583        if let Some(bounds) = rd.bounds {
584            min = min.min(bounds.min);
585            max = max.max(bounds.max);
586        }
587    }
588
589    if !min.x.is_finite() || !max.x.is_finite() {
590        (
591            Vec3::new(bounds_2d.0, bounds_2d.2, -1.0),
592            Vec3::new(bounds_2d.1, bounds_2d.3, 1.0),
593        )
594    } else {
595        if (max.z - min.z).abs() < 1e-6 {
596            min.z -= 0.5;
597            max.z += 0.5;
598        }
599        (min, max)
600    }
601}
602
603fn default_3d_camera_for_bounds(min: Vec3, max: Vec3) -> Camera {
604    let center = (min + max) * 0.5;
605    let extent = (max - min).abs();
606    let radius = extent.length().max(1e-3) * 0.5;
607
608    let mut cam = Camera::new();
609    let fov = match cam.projection {
610        ProjectionType::Perspective { fov, .. } => fov.max(0.2),
611        _ => 45.0f32.to_radians(),
612    };
613    let distance = (radius / (fov * 0.5).tan()).max(radius * 2.5) * 1.05;
614
615    let dir = Vec3::new(1.0, -1.0, 0.8).normalize_or_zero();
616    cam.target = center;
617    cam.position = center + dir * distance;
618    cam.up = Vec3::Z;
619    cam
620}
621
622fn choose_axes_camera(
623    figure: &Figure,
624    axes_index: usize,
625    axes_cameras: Option<&[Camera]>,
626    min: Vec3,
627    max: Vec3,
628) -> Camera {
629    if let Some(cams) = axes_cameras {
630        if let Some(cam) = cams.get(axes_index) {
631            return cam.clone();
632        }
633    }
634
635    let mut cam = default_3d_camera_for_bounds(min, max);
636
637    if let Some(meta) = figure.axes_metadata(axes_index) {
638        if let (Some(az), Some(el)) = (meta.view_azimuth_deg, meta.view_elevation_deg) {
639            cam.set_view_angles_deg(az, el);
640        }
641    }
642
643    cam
644}
645
646fn get_axes_title_and_labels(
647    figure: &Figure,
648    axes_index: usize,
649) -> (
650    Option<String>,
651    Option<String>,
652    Option<String>,
653    Option<String>,
654) {
655    let meta = figure.axes_metadata(axes_index);
656    let title = meta
657        .and_then(|m| m.title.as_ref())
658        .or(figure.title.as_ref())
659        .map(|s| s.trim().to_string())
660        .filter(|s| !s.is_empty());
661    let x_label = meta
662        .and_then(|m| m.x_label.as_ref())
663        .or(figure.x_label.as_ref())
664        .map(|s| s.trim().to_string())
665        .filter(|s| !s.is_empty());
666    let y_label = meta
667        .and_then(|m| m.y_label.as_ref())
668        .or(figure.y_label.as_ref())
669        .map(|s| s.trim().to_string())
670        .filter(|s| !s.is_empty());
671    let z_label = meta
672        .and_then(|m| m.z_label.as_ref())
673        .or(figure.z_label.as_ref())
674        .map(|s| s.trim().to_string())
675        .filter(|s| !s.is_empty());
676    (title, x_label, y_label, z_label)
677}
678
679fn text_scale_from_font_size(font_size: Option<f32>, default_scale: u32) -> u32 {
680    let base = font_size.unwrap_or((default_scale.max(1) * 8) as f32);
681    ((base / 8.0).round() as i32).clamp(1, 4) as u32
682}
683
684fn get_axes_style_and_display_prefs(
685    figure: &Figure,
686    axes_index: usize,
687) -> (u32, u32, u32, bool, bool) {
688    let Some(meta) = figure.axes_metadata(axes_index) else {
689        return (2, 2, 1, figure.grid_enabled, figure.box_enabled);
690    };
691
692    let title_scale = text_scale_from_font_size(meta.title_style.font_size, 2);
693    let label_font = meta
694        .x_label_style
695        .font_size
696        .or(meta.y_label_style.font_size)
697        .or(meta.z_label_style.font_size);
698    let label_scale = text_scale_from_font_size(label_font, 2);
699    let tick_scale = text_scale_from_font_size(label_font.map(|s| (s - 2.0).max(8.0)), 1);
700
701    (
702        title_scale,
703        label_scale,
704        tick_scale,
705        meta.grid_enabled,
706        meta.box_enabled,
707    )
708}
709
710fn project_vertex(vertex: &crate::core::Vertex, axes: &AxesView) -> Option<ScreenVertex> {
711    let pos = Vec3::from_array(vertex.position);
712    let color = to_u8_rgba(vertex.color);
713    if axes.has_3d_content {
714        axes.camera_3d
715            .as_ref()
716            .and_then(|camera| project_3d(pos, axes.plot_rect, camera, color))
717    } else {
718        Some(project_2d(pos, axes.plot_rect, axes.bounds_2d, color))
719    }
720}
721
722fn draw_bitmap_text(canvas: &mut Canvas, x: i32, y: i32, text: &str, scale: u32, color: [u8; 4]) {
723    let mut cursor_x = x;
724    let sc = scale.max(1) as i32;
725    let fallback = BASIC_FONTS.get('?').unwrap_or([0u8; 8]);
726
727    for ch in text.chars() {
728        let glyph = BASIC_FONTS
729            .get(ch)
730            .or_else(|| BASIC_FONTS.get(' '))
731            .unwrap_or(fallback);
732        for (row, bits) in glyph.iter().enumerate() {
733            for col in 0..8i32 {
734                if ((bits >> col) & 1) == 0 {
735                    continue;
736                }
737                for sy in 0..sc {
738                    for sx in 0..sc {
739                        canvas.blend_pixel(
740                            cursor_x + col * sc + sx,
741                            y + row as i32 * sc + sy,
742                            color,
743                            0.0,
744                            false,
745                        );
746                    }
747                }
748            }
749        }
750        cursor_x += 8 * sc + sc;
751    }
752}
753
754fn draw_text_centered(
755    canvas: &mut Canvas,
756    center_x: i32,
757    y: i32,
758    text: &str,
759    scale: u32,
760    color: [u8; 4],
761) {
762    let sc = scale.max(1) as i32;
763    let text_w = (text.chars().count() as i32) * (8 * sc + sc);
764    let x = center_x - text_w / 2;
765    draw_bitmap_text(canvas, x, y, text, scale, color);
766}
767
768fn format_tick(v: f32) -> String {
769    if !v.is_finite() {
770        return "nan".to_string();
771    }
772    let abs = v.abs();
773    if abs >= 1000.0 || (abs > 0.0 && abs < 0.01) {
774        format!("{v:.2e}")
775    } else {
776        format!("{v:.3}")
777    }
778}
779
780fn draw_2d_axes_decorations(canvas: &mut Canvas, axes: &AxesView) {
781    let frame_color = [162, 170, 184, 255];
782    let grid_color = [104, 114, 130, 110];
783    let text_color = [212, 220, 234, 255];
784
785    let (px, py, pw, ph) = axes.plot_rect;
786    let left = px as i32;
787    let right = (px + pw.saturating_sub(1)) as i32;
788    let top = py as i32;
789    let bottom = (py + ph.saturating_sub(1)) as i32;
790
791    if axes.show_grid {
792        for i in 0..=6 {
793            let t = i as f32 / 6.0;
794            let x = (left as f32 + t * (right - left) as f32).round() as i32;
795            let y = (top as f32 + t * (bottom - top) as f32).round() as i32;
796
797            let gv = ScreenVertex {
798                x: x as f32,
799                y: top as f32,
800                z: 0.0,
801                color: grid_color,
802            };
803            let gv2 = ScreenVertex {
804                x: x as f32,
805                y: bottom as f32,
806                z: 0.0,
807                color: grid_color,
808            };
809            canvas.draw_line(gv, gv2, 1.0, 0, false);
810
811            let gh = ScreenVertex {
812                x: left as f32,
813                y: y as f32,
814                z: 0.0,
815                color: grid_color,
816            };
817            let gh2 = ScreenVertex {
818                x: right as f32,
819                y: y as f32,
820                z: 0.0,
821                color: grid_color,
822            };
823            canvas.draw_line(gh, gh2, 1.0, 0, false);
824        }
825    }
826
827    if axes.show_box {
828        let corners = [
829            (left as f32, top as f32),
830            (right as f32, top as f32),
831            (right as f32, bottom as f32),
832            (left as f32, bottom as f32),
833        ];
834        for i in 0..4 {
835            let a = corners[i];
836            let b = corners[(i + 1) % 4];
837            canvas.draw_line(
838                ScreenVertex {
839                    x: a.0,
840                    y: a.1,
841                    z: 0.0,
842                    color: frame_color,
843                },
844                ScreenVertex {
845                    x: b.0,
846                    y: b.1,
847                    z: 0.0,
848                    color: frame_color,
849                },
850                1.2,
851                0,
852                false,
853            );
854        }
855    }
856
857    let (x_min, x_max, y_min, y_max) = axes.bounds_2d;
858    let tick_sc = axes.tick_scale as i32;
859    for i in 0..=4 {
860        let t = i as f32 / 4.0;
861        let x = (left as f32 + t * (right - left) as f32).round() as i32;
862        let y = (top as f32 + t * (bottom - top) as f32).round() as i32;
863
864        let xv = x_min + t * (x_max - x_min);
865        let yv = y_max - t * (y_max - y_min);
866
867        draw_bitmap_text(
868            canvas,
869            x - 12 * tick_sc,
870            bottom + 6 + tick_sc,
871            &format_tick(xv),
872            axes.tick_scale,
873            with_alpha(text_color, 0.9),
874        );
875        draw_bitmap_text(
876            canvas,
877            left - 56 * tick_sc,
878            y - 4 * tick_sc,
879            &format_tick(yv),
880            axes.tick_scale,
881            with_alpha(text_color, 0.9),
882        );
883    }
884
885    if let Some(title) = &axes.title {
886        draw_text_centered(
887            canvas,
888            (axes.viewport.0 + axes.viewport.2 / 2) as i32,
889            axes.viewport.1 as i32 + 6,
890            title,
891            axes.title_scale,
892            text_color,
893        );
894    }
895    if let Some(x_label) = &axes.x_label {
896        let label_sc = axes.label_scale as i32;
897        draw_text_centered(
898            canvas,
899            (axes.viewport.0 + axes.viewport.2 / 2) as i32,
900            (axes.viewport.1 + axes.viewport.3).saturating_sub((12 + 10 * label_sc) as u32) as i32,
901            x_label,
902            axes.label_scale,
903            text_color,
904        );
905    }
906    if let Some(y_label) = &axes.y_label {
907        let label_sc = axes.label_scale as i32;
908        draw_bitmap_text(
909            canvas,
910            axes.viewport.0 as i32 + 6,
911            (axes.viewport.1 + axes.viewport.3 / 2).saturating_sub((8 * label_sc) as u32) as i32,
912            y_label,
913            axes.label_scale,
914            text_color,
915        );
916    }
917}
918
919fn draw_3d_axes_decorations(canvas: &mut Canvas, axes: &AxesView) {
920    let floor_grid_minor = [44, 54, 70, 68];
921    let axis_x_color = [235, 80, 80, 230];
922    let axis_y_color = [90, 220, 120, 230];
923    let axis_z_color = [90, 160, 255, 230];
924    let text_color = [212, 220, 234, 255];
925
926    let (bmin, bmax) = axes.bounds_3d;
927    let Some(cam) = axes.camera_3d.as_ref() else {
928        return;
929    };
930
931    let origin_component = |lo: f32, hi: f32| -> f32 {
932        if lo <= 0.0 && hi >= 0.0 {
933            0.0
934        } else {
935            lo
936        }
937    };
938    let ox = origin_component(bmin.x, bmax.x);
939    let oy = origin_component(bmin.y, bmax.y);
940    let oz = origin_component(bmin.z, bmax.z);
941    let floor_z = oz;
942
943    if axes.show_grid {
944        let divisions = 28usize;
945        for i in 0..=divisions {
946            let t = i as f32 / divisions as f32;
947            let x = bmin.x + t * (bmax.x - bmin.x);
948            let y = bmin.y + t * (bmax.y - bmin.y);
949
950            let gx0 = Vec3::new(x, bmin.y, floor_z);
951            let gx1 = Vec3::new(x, bmax.y, floor_z);
952            let gy0 = Vec3::new(bmin.x, y, floor_z);
953            let gy1 = Vec3::new(bmax.x, y, floor_z);
954
955            let Some(a0) = project_3d(gx0, axes.plot_rect, cam, floor_grid_minor) else {
956                continue;
957            };
958            let Some(a1) = project_3d(gx1, axes.plot_rect, cam, floor_grid_minor) else {
959                continue;
960            };
961            canvas.draw_line(a0, a1, 0.9, 0, false);
962
963            let Some(b0) = project_3d(gy0, axes.plot_rect, cam, floor_grid_minor) else {
964                continue;
965            };
966            let Some(b1) = project_3d(gy1, axes.plot_rect, cam, floor_grid_minor) else {
967                continue;
968            };
969            canvas.draw_line(b0, b1, 0.9, 0, false);
970        }
971    }
972
973    let x_end = if bmax.x >= ox {
974        Vec3::new(bmax.x, oy, floor_z)
975    } else {
976        Vec3::new(bmin.x, oy, floor_z)
977    };
978    let y_end = if bmax.y >= oy {
979        Vec3::new(ox, bmax.y, floor_z)
980    } else {
981        Vec3::new(ox, bmin.y, floor_z)
982    };
983    let z_end = if bmax.z >= oz {
984        Vec3::new(ox, oy, bmax.z)
985    } else {
986        Vec3::new(ox, oy, bmin.z)
987    };
988    let origin = Vec3::new(ox, oy, oz);
989
990    if let (Some(o), Some(xp)) = (
991        project_3d(origin, axes.plot_rect, cam, axis_x_color),
992        project_3d(x_end, axes.plot_rect, cam, axis_x_color),
993    ) {
994        canvas.draw_line(o, xp, 1.8, 0, false);
995        draw_bitmap_text(
996            canvas,
997            xp.x as i32 + 6,
998            xp.y as i32 + 2,
999            axes.x_label.as_deref().unwrap_or("x"),
1000            axes.label_scale,
1001            axis_x_color,
1002        );
1003    }
1004    if let (Some(o), Some(yp)) = (
1005        project_3d(origin, axes.plot_rect, cam, axis_y_color),
1006        project_3d(y_end, axes.plot_rect, cam, axis_y_color),
1007    ) {
1008        canvas.draw_line(o, yp, 1.8, 0, false);
1009        draw_bitmap_text(
1010            canvas,
1011            yp.x as i32 + 6,
1012            yp.y as i32 + 2,
1013            axes.y_label.as_deref().unwrap_or("y"),
1014            axes.label_scale,
1015            axis_y_color,
1016        );
1017    }
1018    if let (Some(o), Some(zp)) = (
1019        project_3d(origin, axes.plot_rect, cam, axis_z_color),
1020        project_3d(z_end, axes.plot_rect, cam, axis_z_color),
1021    ) {
1022        canvas.draw_line(o, zp, 1.8, 0, false);
1023        draw_bitmap_text(
1024            canvas,
1025            zp.x as i32 + 6,
1026            zp.y as i32 + 2,
1027            axes.z_label.as_deref().unwrap_or("z"),
1028            axes.label_scale,
1029            axis_z_color,
1030        );
1031    }
1032
1033    if let Some(title) = &axes.title {
1034        draw_text_centered(
1035            canvas,
1036            (axes.viewport.0 + axes.viewport.2 / 2) as i32,
1037            axes.viewport.1 as i32 + 6,
1038            title,
1039            axes.title_scale,
1040            text_color,
1041        );
1042    }
1043}
1044
1045fn draw_3d_orientation_gizmo(canvas: &mut Canvas, axes: &AxesView) {
1046    let Some(cam) = axes.camera_3d.as_ref() else {
1047        return;
1048    };
1049    let forward = (cam.target - cam.position).normalize_or_zero();
1050    if forward.length_squared() < 1e-9 {
1051        return;
1052    }
1053    let world_up = cam.up.normalize_or_zero();
1054    let right = forward.cross(world_up).normalize_or_zero();
1055    if right.length_squared() < 1e-9 {
1056        return;
1057    }
1058    let up = right.cross(forward).normalize_or_zero();
1059    if up.length_squared() < 1e-9 {
1060        return;
1061    }
1062
1063    #[derive(Clone, Copy)]
1064    struct AxisItem {
1065        label: &'static str,
1066        dir_world: Vec3,
1067        color: [u8; 4],
1068        z_sort: f32,
1069    }
1070
1071    let mut axis_items = [
1072        AxisItem {
1073            label: "X",
1074            dir_world: Vec3::X,
1075            color: [235, 80, 80, 255],
1076            z_sort: 0.0,
1077        },
1078        AxisItem {
1079            label: "Y",
1080            dir_world: Vec3::Y,
1081            color: [90, 220, 120, 255],
1082            z_sort: 0.0,
1083        },
1084        AxisItem {
1085            label: "Z",
1086            dir_world: Vec3::Z,
1087            color: [90, 160, 255, 255],
1088            z_sort: 0.0,
1089        },
1090    ];
1091
1092    for a in &mut axis_items {
1093        let x = a.dir_world.dot(right);
1094        let y = a.dir_world.dot(up);
1095        let z = a.dir_world.dot(-forward);
1096        a.z_sort = z;
1097        a.dir_world = Vec3::new(x, y, z);
1098    }
1099    axis_items.sort_by(|a, b| a.z_sort.total_cmp(&b.z_sort));
1100
1101    let scale = ((axes.viewport.2.min(axes.viewport.3) as f32) / 720.0).clamp(0.8, 1.6);
1102    let gizmo_size =
1103        ((axes.viewport.2.min(axes.viewport.3) as f32) * 0.16).clamp(44.0, 110.0) * scale;
1104    let pad = (30.0 * scale).round() as i32;
1105    let origin = Vec2::new(
1106        (axes.viewport.0 as i32 + pad) as f32,
1107        ((axes.viewport.1 + axes.viewport.3) as i32 - pad) as f32,
1108    );
1109    canvas.draw_disc(
1110        origin,
1111        (2.0 * scale).max(1.0),
1112        [210, 214, 224, 255],
1113        0.0,
1114        false,
1115    );
1116
1117    let axis_len = gizmo_size * 0.65;
1118    let head_len = (8.0 * scale).min(axis_len * 0.35);
1119    let head_w = 5.0 * scale;
1120    for a in &axis_items {
1121        let dir2 = Vec2::new(a.dir_world.x, -a.dir_world.y);
1122        let mag = dir2.length();
1123        if !mag.is_finite() || mag < 1e-4 {
1124            continue;
1125        }
1126        let d = dir2 / mag;
1127        let end = origin + d * axis_len;
1128        canvas.draw_line(
1129            ScreenVertex {
1130                x: origin.x,
1131                y: origin.y,
1132                z: 0.0,
1133                color: a.color,
1134            },
1135            ScreenVertex {
1136                x: end.x,
1137                y: end.y,
1138                z: 0.0,
1139                color: a.color,
1140            },
1141            (2.0 * scale).max(1.2),
1142            0,
1143            false,
1144        );
1145
1146        let base = end - d * head_len;
1147        let perp = Vec2::new(-d.y, d.x);
1148        canvas.draw_line(
1149            ScreenVertex {
1150                x: end.x,
1151                y: end.y,
1152                z: 0.0,
1153                color: a.color,
1154            },
1155            ScreenVertex {
1156                x: (base + perp * head_w).x,
1157                y: (base + perp * head_w).y,
1158                z: 0.0,
1159                color: a.color,
1160            },
1161            (2.0 * scale).max(1.2),
1162            0,
1163            false,
1164        );
1165        canvas.draw_line(
1166            ScreenVertex {
1167                x: end.x,
1168                y: end.y,
1169                z: 0.0,
1170                color: a.color,
1171            },
1172            ScreenVertex {
1173                x: (base - perp * head_w).x,
1174                y: (base - perp * head_w).y,
1175                z: 0.0,
1176                color: a.color,
1177            },
1178            (2.0 * scale).max(1.2),
1179            0,
1180            false,
1181        );
1182
1183        let label_pos = end + d * (10.0 * scale);
1184        draw_bitmap_text(
1185            canvas,
1186            label_pos.x as i32 - 3,
1187            label_pos.y as i32 - 3,
1188            a.label,
1189            1,
1190            a.color,
1191        );
1192    }
1193}
1194
1195fn draw_legend_for_axes(canvas: &mut Canvas, figure: &Figure, axes: &AxesView) {
1196    if !figure.legend_enabled {
1197        return;
1198    }
1199    let entries = figure.legend_entries();
1200    if entries.is_empty() {
1201        return;
1202    }
1203
1204    let max_entries = entries.len().min(8);
1205    let pad = 10i32;
1206    let row_h = 20i32;
1207    let legend_w = ((axes.viewport.2 as f32 * 0.30).clamp(92.0, 148.0)).round() as i32;
1208    let legend_h = row_h * max_entries as i32 + 10;
1209    let x = (axes.viewport.0 + axes.viewport.2) as i32 - legend_w - pad;
1210    let y = axes.viewport.1 as i32 + 12;
1211
1212    canvas.fill_rect(x, y, legend_w, legend_h, [8, 14, 24, 220]);
1213    canvas.stroke_rect(x, y, legend_w, legend_h, [36, 52, 74, 245], 1.0);
1214
1215    for (i, entry) in entries.into_iter().take(max_entries).enumerate() {
1216        let yy = y + 6 + i as i32 * row_h + row_h / 2;
1217        let swatch_x0 = x + 10;
1218        let swatch_x1 = swatch_x0 + 18;
1219        let swatch_color = to_u8_rgba(entry.color.to_array());
1220        canvas.draw_line(
1221            ScreenVertex {
1222                x: swatch_x0 as f32,
1223                y: yy as f32,
1224                z: 0.0,
1225                color: swatch_color,
1226            },
1227            ScreenVertex {
1228                x: swatch_x1 as f32,
1229                y: yy as f32,
1230                z: 0.0,
1231                color: swatch_color,
1232            },
1233            2.0,
1234            0,
1235            false,
1236        );
1237
1238        let label = if entry.label.is_empty() {
1239            "Series".to_string()
1240        } else {
1241            entry.label
1242        };
1243        draw_bitmap_text(canvas, x + 34, yy - 4, &label, 1, [220, 228, 239, 255]);
1244    }
1245}
1246
1247pub async fn render_figure_rgba_bytes(
1248    mut figure: Figure,
1249    width: u32,
1250    height: u32,
1251    theme: Option<PlotThemeConfig>,
1252    camera: Option<&Camera>,
1253    axes_cameras: Option<&[Camera]>,
1254    _textmark: Option<&str>,
1255) -> Result<Vec<u8>, String> {
1256    let width = width.max(1);
1257    let height = height.max(1);
1258    let bg = if is_default_figure_bg(figure.background_color) {
1259        theme
1260            .as_ref()
1261            .map(|cfg| cfg.build_theme().get_background_color())
1262            .unwrap_or_else(|| Vec4::new(1.0, 1.0, 1.0, 1.0))
1263    } else {
1264        figure.background_color
1265    };
1266    let mut canvas = Canvas::new(width, height, to_u8_rgba(bg.to_array()));
1267
1268    let (rows, cols) = figure.axes_grid();
1269    let viewports = compute_tiled_viewports(width, height, rows.max(1), cols.max(1));
1270    let axes_count = rows.max(1) * cols.max(1);
1271
1272    let has_3d_flags: Vec<bool> = (0..axes_count)
1273        .map(|axes_index| axes_has_3d_content(&figure, axes_index))
1274        .collect();
1275    let axes_sizes: Vec<(u32, u32)> = viewports
1276        .iter()
1277        .zip(has_3d_flags.iter())
1278        .map(|(vp, has_3d)| {
1279            let rect = compute_plot_rect(*vp, *has_3d);
1280            (rect.2.max(1), rect.3.max(1))
1281        })
1282        .collect();
1283
1284    let render_items = figure.render_data_with_axes_with_viewport_and_gpu(
1285        Some((width, height)),
1286        Some(&axes_sizes),
1287        None,
1288        None,
1289    );
1290
1291    let mut axes_views = Vec::with_capacity(axes_count);
1292    for axes_index in 0..axes_count {
1293        let has_3d = has_3d_flags[axes_index];
1294        let viewport = viewports[axes_index];
1295        let plot_rect = compute_plot_rect(viewport, has_3d);
1296        let bounds_2d = choose_axes_bounds(&figure, axes_index, &render_items);
1297        let (bmin, bmax) = choose_axes_bounds_3d(axes_index, &render_items, bounds_2d);
1298        let camera_3d = if has_3d {
1299            Some(if axes_count == 1 {
1300                camera.cloned().unwrap_or_else(|| {
1301                    choose_axes_camera(&figure, axes_index, axes_cameras, bmin, bmax)
1302                })
1303            } else {
1304                choose_axes_camera(&figure, axes_index, axes_cameras, bmin, bmax)
1305            })
1306        } else {
1307            None
1308        };
1309
1310        let (title, x_label, y_label, z_label) = get_axes_title_and_labels(&figure, axes_index);
1311        let (title_scale, label_scale, tick_scale, show_grid, show_box) =
1312            get_axes_style_and_display_prefs(&figure, axes_index);
1313
1314        axes_views.push(AxesView {
1315            viewport,
1316            plot_rect,
1317            bounds_2d,
1318            bounds_3d: (bmin, bmax),
1319            camera_3d,
1320            has_3d_content: has_3d,
1321            title,
1322            x_label,
1323            y_label,
1324            z_label,
1325            title_scale,
1326            label_scale,
1327            tick_scale,
1328            show_grid,
1329            show_box,
1330        });
1331    }
1332
1333    for axes in &axes_views {
1334        if axes.has_3d_content {
1335            draw_3d_axes_decorations(&mut canvas, axes);
1336        } else {
1337            draw_2d_axes_decorations(&mut canvas, axes);
1338        }
1339    }
1340
1341    for (axes_index, rd) in render_items.iter() {
1342        if rd.vertices.is_empty() {
1343            continue;
1344        }
1345        let Some(axes) = axes_views.get(*axes_index) else {
1346            continue;
1347        };
1348        draw_render_data(&mut canvas, rd, axes);
1349    }
1350    for axes in &axes_views {
1351        if !axes.has_3d_content {
1352            continue;
1353        }
1354        draw_3d_orientation_gizmo(&mut canvas, axes);
1355        if axes_views.len() == 1 {
1356            draw_legend_for_axes(&mut canvas, &figure, axes);
1357        }
1358    }
1359
1360    Ok(canvas.rgba())
1361}
1362
1363fn draw_render_data(canvas: &mut Canvas, render_data: &RenderData, axes: &AxesView) {
1364    let width_px = render_data.material.roughness.max(1.0);
1365    let style_code = render_data.material.metallic as i32;
1366
1367    match render_data.pipeline_type {
1368        PipelineType::Lines => {
1369            for segment in render_data.vertices.chunks_exact(2) {
1370                let Some(a) = project_vertex(&segment[0], axes) else {
1371                    continue;
1372                };
1373                let Some(b) = project_vertex(&segment[1], axes) else {
1374                    continue;
1375                };
1376                canvas.draw_line(a, b, width_px, style_code, axes.has_3d_content);
1377            }
1378        }
1379        PipelineType::Points | PipelineType::Scatter3 => {
1380            for v in &render_data.vertices {
1381                let Some(p) = project_vertex(v, axes) else {
1382                    continue;
1383                };
1384                let marker_radius = (v.normal[2].max(1.0) * 0.5).max(1.0);
1385                canvas.draw_disc(
1386                    Vec2::new(p.x, p.y),
1387                    marker_radius,
1388                    p.color,
1389                    p.z,
1390                    axes.has_3d_content,
1391                );
1392            }
1393        }
1394        PipelineType::Triangles => {
1395            if axes.has_3d_content && render_data.indices.is_none() {
1396                return;
1397            }
1398            if let Some(indices) = &render_data.indices {
1399                for tri in indices.chunks_exact(3) {
1400                    let (Some(v0), Some(v1), Some(v2)) = (
1401                        render_data.vertices.get(tri[0] as usize),
1402                        render_data.vertices.get(tri[1] as usize),
1403                        render_data.vertices.get(tri[2] as usize),
1404                    ) else {
1405                        continue;
1406                    };
1407                    let (Some(p0), Some(p1), Some(p2)) = (
1408                        project_vertex(v0, axes),
1409                        project_vertex(v1, axes),
1410                        project_vertex(v2, axes),
1411                    ) else {
1412                        continue;
1413                    };
1414                    canvas.fill_triangle(p0, p1, p2, axes.has_3d_content);
1415                }
1416            } else {
1417                for tri in render_data.vertices.chunks_exact(3) {
1418                    let (Some(p0), Some(p1), Some(p2)) = (
1419                        project_vertex(&tri[0], axes),
1420                        project_vertex(&tri[1], axes),
1421                        project_vertex(&tri[2], axes),
1422                    ) else {
1423                        continue;
1424                    };
1425                    canvas.fill_triangle(p0, p1, p2, axes.has_3d_content);
1426                }
1427            }
1428        }
1429        PipelineType::Textured => {
1430            for tri in render_data.vertices.chunks_exact(3) {
1431                let (Some(p0), Some(p1), Some(p2)) = (
1432                    project_vertex(&tri[0], axes),
1433                    project_vertex(&tri[1], axes),
1434                    project_vertex(&tri[2], axes),
1435                ) else {
1436                    continue;
1437                };
1438                canvas.fill_triangle(p0, p1, p2, axes.has_3d_content);
1439            }
1440        }
1441    }
1442}
1443
1444pub fn encode_png_bytes(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
1445    use image::{ImageBuffer, ImageFormat, Rgba};
1446
1447    let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width.max(1), height.max(1), rgba.to_vec())
1448        .ok_or_else(|| "Failed to create image buffer for CPU PNG encoding".to_string())?;
1449    let mut out = std::io::Cursor::new(Vec::new());
1450    image
1451        .write_to(&mut out, ImageFormat::Png)
1452        .map_err(|err| format!("Failed to encode CPU PNG bytes: {err}"))?;
1453    Ok(out.into_inner())
1454}
1455
1456pub async fn render_figure_png_bytes(
1457    figure: Figure,
1458    width: u32,
1459    height: u32,
1460    theme: Option<PlotThemeConfig>,
1461    camera: Option<&Camera>,
1462    axes_cameras: Option<&[Camera]>,
1463    textmark: Option<&str>,
1464) -> Result<Vec<u8>, String> {
1465    let rgba =
1466        render_figure_rgba_bytes(figure, width, height, theme, camera, axes_cameras, textmark)
1467            .await?;
1468    encode_png_bytes(width.max(1), height.max(1), &rgba)
1469}