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}