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}