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