Skip to main content

runmat_plot/export/
cpu_surface.rs

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