1use crate::core::{plot_utils, PlotRenderer};
7use crate::plots::TextStyle;
8use crate::styling::{ModernDarkTheme, PlotThemeConfig, ThemeVariant};
9use egui::{Align2, Color32, Context, FontId, Pos2, Rect, Stroke};
10use glam::{Vec3, Vec4};
11
12pub struct PlotOverlay {
14 theme: PlotThemeConfig,
16
17 plot_area: Option<Rect>,
19 axes_plot_rects: Vec<Rect>,
21 toolbar_rect: Option<Rect>,
23 sidebar_rect: Option<Rect>,
25
26 show_debug: bool,
28
29 show_dystr_modal: bool,
31
32 want_save_png: bool,
34 want_save_svg: bool,
35 want_reset_view: bool,
36 want_toggle_grid: Option<bool>,
37 want_toggle_legend: Option<bool>,
38}
39
40#[derive(Debug, Clone)]
42pub struct OverlayConfig {
43 pub show_sidebar: bool,
45
46 pub show_toolbar: bool,
48
49 pub font_scale: f32,
51
52 pub show_grid: bool,
54
55 pub show_axes: bool,
57
58 pub show_title: bool,
60
61 pub title: Option<String>,
63
64 pub x_label: Option<String>,
66 pub y_label: Option<String>,
67
68 pub sidebar_width: f32,
70
71 pub plot_margins: PlotMargins,
73}
74
75#[derive(Debug, Clone)]
76pub struct PlotMargins {
77 pub left: f32,
78 pub right: f32,
79 pub top: f32,
80 pub bottom: f32,
81}
82
83#[derive(Debug, Clone, Copy)]
84struct PanelLayout {
85 plot_rect: Rect,
86 frame_rect: Rect,
87 title_rect: Rect,
88 x_label_rect: Rect,
89 y_label_rect: Rect,
90}
91
92impl Default for OverlayConfig {
93 fn default() -> Self {
94 Self {
95 show_sidebar: true,
96 show_toolbar: true,
97 font_scale: 1.0,
98 show_grid: true,
99 show_axes: true,
100 show_title: true,
101 title: Some("Plot".to_string()),
102 x_label: Some("X".to_string()),
103 y_label: Some("Y".to_string()),
104 sidebar_width: 280.0,
105 plot_margins: PlotMargins {
106 left: 60.0,
107 right: 20.0,
108 top: 40.0,
109 bottom: 60.0,
110 },
111 }
112 }
113}
114
115#[derive(Debug)]
117pub struct FrameInfo {
118 pub plot_area: Option<Rect>,
120
121 pub consumed_input: bool,
123
124 pub metrics: OverlayMetrics,
126}
127
128#[derive(Debug, Default)]
129pub struct OverlayMetrics {
130 pub vertex_count: usize,
131 pub triangle_count: usize,
132 pub render_time_ms: f64,
133 pub fps: f32,
134}
135
136impl Default for PlotOverlay {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl PlotOverlay {
143 const SUBPLOT_GAP_POINTS: f32 = 6.0;
144
145 fn style_color(style: &TextStyle, fallback: Color32) -> Color32 {
146 style
147 .color
148 .map(|c| {
149 Color32::from_rgb(
150 (c.x.clamp(0.0, 1.0) * 255.0) as u8,
151 (c.y.clamp(0.0, 1.0) * 255.0) as u8,
152 (c.z.clamp(0.0, 1.0) * 255.0) as u8,
153 )
154 })
155 .unwrap_or(fallback)
156 }
157
158 fn style_font_size(style: &TextStyle, default_size: f32, scale: f32) -> f32 {
159 style.font_size.unwrap_or(default_size) * scale.max(0.75)
160 }
161
162 fn style_is_bold(style: &TextStyle) -> bool {
163 style
164 .font_weight
165 .as_deref()
166 .map(|weight| weight.eq_ignore_ascii_case("bold"))
167 .unwrap_or(false)
168 }
169
170 #[allow(clippy::too_many_arguments)]
171 fn paint_styled_text(
172 painter: &egui::Painter,
173 pos: Pos2,
174 align: Align2,
175 text: &str,
176 font_size: f32,
177 color: Color32,
178 bold: bool,
179 shadow_alpha: u8,
180 ) {
181 let font = FontId::proportional(font_size);
182 painter.text(
183 pos + egui::vec2(1.0, 1.0),
184 align,
185 text,
186 font.clone(),
187 Color32::from_rgba_premultiplied(0, 0, 0, shadow_alpha),
188 );
189 painter.text(pos, align, text, font.clone(), color);
190 if bold {
191 painter.text(pos + egui::vec2(0.6, 0.0), align, text, font.clone(), color);
192 painter.text(pos + egui::vec2(0.0, 0.6), align, text, font.clone(), color);
193 painter.text(pos + egui::vec2(0.6, 0.6), align, text, font, color);
194 }
195 }
196
197 fn label_stride(labels: &[String], axis_span_px: f32, font_size_px: f32) -> usize {
198 if labels.len() <= 1 || axis_span_px <= 1.0 {
199 return 1;
200 }
201 let max_chars = labels
202 .iter()
203 .map(|label| truncate_label(label, 14).chars().count())
204 .max()
205 .unwrap_or(0) as f32;
206 let estimated_label_width = (max_chars * font_size_px * 0.55).max(font_size_px * 2.0);
207 let slot_width = (axis_span_px / labels.len() as f32).max(1.0);
208 ((estimated_label_width / slot_width).ceil().max(1.0)) as usize
209 }
210
211 #[allow(clippy::too_many_arguments)]
212 fn draw_histogram_axis_ticks(
213 &self,
214 ui: &mut egui::Ui,
215 plot_rect: Rect,
216 ppp: f32,
217 axis_color: Color32,
218 label_color: Color32,
219 tick_length: f32,
220 label_offset: f32,
221 tick_font: FontId,
222 border_bottom: f32,
223 x_min: f64,
224 x_max: f64,
225 edges: &[f64],
226 ) {
227 if edges.len() < 2 || (x_max - x_min).abs() <= f64::EPSILON {
228 return;
229 }
230 let labels: Vec<String> = edges
231 .iter()
232 .map(|value| plot_utils::format_tick_label(*value))
233 .collect();
234 let stride = Self::label_stride(&labels, plot_rect.width(), tick_font.size);
235 let denom = (edges.len() - 1) as f64;
236 for (idx, label) in labels.iter().enumerate() {
237 if idx != 0 && idx != labels.len() - 1 && idx % stride != 0 {
238 continue;
239 }
240 let frac = idx as f64 / denom;
241 let x_val = x_min + frac * (x_max - x_min);
242 let x_screen =
243 plot_rect.min.x + ((x_val - x_min) / (x_max - x_min)) as f32 * plot_rect.width();
244 let x_screen = Self::snap_coord(x_screen, ppp);
245 ui.painter().line_segment(
246 [
247 Pos2::new(x_screen, border_bottom),
248 Pos2::new(x_screen, border_bottom + tick_length),
249 ],
250 Stroke::new(1.0, axis_color),
251 );
252 ui.painter().text(
253 Pos2::new(x_screen, border_bottom + label_offset),
254 Align2::CENTER_CENTER,
255 label,
256 tick_font.clone(),
257 label_color,
258 );
259 }
260 }
261
262 pub fn new() -> Self {
264 Self {
265 theme: PlotThemeConfig::default(),
266 plot_area: None,
267 axes_plot_rects: Vec::new(),
268 toolbar_rect: None,
269 sidebar_rect: None,
270 show_debug: false,
271 show_dystr_modal: false,
272 want_save_png: false,
273 want_save_svg: false,
274 want_reset_view: false,
275 want_toggle_grid: None,
276 want_toggle_legend: None,
277 }
278 }
279
280 pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
281 self.theme = theme;
282 }
283
284 fn has_visible_text(text: Option<&str>) -> bool {
285 text.map(|s| !s.trim().is_empty()).unwrap_or(false)
286 }
287
288 fn approx_text_width_points(text: &str, font_size: f32) -> f32 {
289 (text.chars().count() as f32) * font_size * 0.56
290 }
291
292 fn estimate_y_axis_band_width(
293 &self,
294 plot_renderer: &PlotRenderer,
295 axes_index: usize,
296 has_y_label: bool,
297 scale: f32,
298 ) -> f32 {
299 let tick_font_size = 10.0 * scale;
300 let label_offset = 15.0 * scale;
301
302 let y_log = plot_renderer.overlay_y_log_for_axes(axes_index);
303 let categorical = plot_renderer
304 .overlay_categorical_labels_for_axes(axes_index)
305 .filter(|(is_x, _)| !*is_x)
306 .map(|(_, labels)| labels)
307 .or_else(|| {
308 plot_renderer
309 .overlay_categorical_labels()
310 .and_then(|(is_x, labels)| if !is_x { Some(labels.clone()) } else { None })
311 });
312
313 let max_label_width = if let Some(labels) = categorical {
314 labels
315 .iter()
316 .map(|label| {
317 Self::approx_text_width_points(&truncate_label(label, 14), tick_font_size)
318 })
319 .fold(0.0_f32, f32::max)
320 } else if let Some((_x_min, _x_max, y_min, y_max)) =
321 plot_renderer.overlay_display_bounds_for_axes(axes_index)
322 {
323 if y_log && y_min > 0.0 && y_max > 0.0 {
324 let start_decade = y_min.log10().floor() as i32;
325 let end_decade = y_max.log10().ceil() as i32;
326 (start_decade..=end_decade)
327 .map(|d| Self::approx_text_width_points(&format!("10^{d}"), tick_font_size))
328 .fold(0.0_f32, f32::max)
329 } else {
330 plot_utils::generate_major_ticks(y_min, y_max)
331 .into_iter()
332 .map(plot_utils::format_tick_label)
333 .map(|label| Self::approx_text_width_points(&label, tick_font_size))
334 .fold(0.0_f32, f32::max)
335 }
336 } else {
337 Self::approx_text_width_points("-1.00", tick_font_size)
338 };
339
340 let y_tick_zone = label_offset + max_label_width * 0.5 + 4.0 * scale;
341 let y_label_zone = if has_y_label {
342 11.0 * scale
343 } else {
344 4.0 * scale
345 };
346 (y_tick_zone + y_label_zone).max(24.0 * scale)
347 }
348
349 fn estimate_x_axis_edge_padding(
350 &self,
351 plot_renderer: &PlotRenderer,
352 axes_index: usize,
353 scale: f32,
354 ) -> f32 {
355 let tick_font_size = 10.0 * scale;
356 let x_log = plot_renderer.overlay_x_log_for_axes(axes_index);
357
358 let explicit_tick_labels = plot_renderer
359 .overlay_x_tick_labels_for_axes(axes_index)
360 .filter(|labels| !labels.is_empty());
361 let categorical_tick_labels = plot_renderer
362 .overlay_categorical_labels_for_axes(axes_index)
363 .and_then(|(is_x, labels)| if is_x { Some(labels) } else { None })
364 .or_else(|| {
365 plot_renderer
366 .overlay_categorical_labels()
367 .and_then(|(is_x, labels)| if is_x { Some(labels.clone()) } else { None })
368 });
369
370 let labels: Vec<String> = if let Some(labels) = explicit_tick_labels {
371 labels
372 .iter()
373 .map(|label| truncate_label(label, 14))
374 .collect()
375 } else if let Some(labels) = categorical_tick_labels {
376 labels
377 .iter()
378 .map(|label| truncate_label(label, 14))
379 .collect()
380 } else if let Some((x_min, x_max, _y_min, _y_max)) =
381 plot_renderer.overlay_display_bounds_for_axes(axes_index)
382 {
383 if x_log && x_min > 0.0 && x_max > 0.0 {
384 let start_decade = x_min.log10().floor() as i32;
385 let end_decade = x_max.log10().ceil() as i32;
386 (start_decade..=end_decade)
387 .map(|d| format!("10^{d}"))
388 .collect()
389 } else {
390 plot_utils::generate_major_ticks(x_min, x_max)
391 .into_iter()
392 .map(plot_utils::format_tick_label)
393 .collect()
394 }
395 } else {
396 vec!["-1.00".to_string(), "1.00".to_string()]
397 };
398
399 let max_edge_label_width = if labels.is_empty() {
400 Self::approx_text_width_points("-1.00", tick_font_size)
401 } else if labels.len() == 1 {
402 Self::approx_text_width_points(&labels[0], tick_font_size)
403 } else {
404 let left = Self::approx_text_width_points(&labels[0], tick_font_size);
405 let right = Self::approx_text_width_points(&labels[labels.len() - 1], tick_font_size);
406 left.max(right)
407 };
408
409 (max_edge_label_width * 0.5 + 3.0 * scale).max(6.0 * scale)
410 }
411
412 #[allow(clippy::too_many_arguments)]
413 fn layout_2d_panel(
414 &self,
415 outer: Rect,
416 plot_renderer: &PlotRenderer,
417 axes_index: usize,
418 title: Option<&str>,
419 x_label: Option<&str>,
420 y_label: Option<&str>,
421 scale: f32,
422 ) -> PanelLayout {
423 let scale = scale.max(0.75);
424 let has_title = Self::has_visible_text(title);
425 let has_x_label = Self::has_visible_text(x_label);
426 let has_y_label = Self::has_visible_text(y_label);
427 let outer_w = outer.width().max(1.0);
428 let outer_h = outer.height().max(1.0);
429 let title_gap = if has_title { 4.0 * scale } else { 1.5 * scale };
430 let x_gap = 4.0 * scale;
431 let x_edge_pad = self.estimate_x_axis_edge_padding(plot_renderer, axes_index, scale);
432 let mut right_pad = (3.0 * scale).max(x_edge_pad + 1.0 * scale);
433
434 let mut title_h = if has_title {
435 (28.0 * scale).min(outer_h * 0.16)
436 } else {
437 0.0
438 };
439 let mut x_h = ((24.0 + if has_x_label { 14.0 } else { 0.0 }) * scale).min(outer_h * 0.28);
440 let y_band_estimate =
441 self.estimate_y_axis_band_width(plot_renderer, axes_index, has_y_label, scale);
442 let mut y_w = y_band_estimate.min(outer_w * 0.30);
443
444 let min_plot_w = (outer_w * 0.56).max(44.0 * scale).min(outer_w);
445 let min_plot_h = (outer_h * 0.54).max(44.0 * scale).min(outer_h);
446
447 if outer_w - y_w - right_pad < min_plot_w {
448 y_w = (outer_w - right_pad - min_plot_w).max(0.0);
449 }
450 if outer_w - y_w - right_pad < min_plot_w {
451 right_pad = (outer_w - y_w - min_plot_w).max(0.0);
452 }
453
454 let available_h = outer_h - title_h - title_gap - x_h - x_gap;
455 if available_h < min_plot_h {
456 let deficit = min_plot_h - available_h;
457 let reducible = title_h + x_h;
458 if reducible > 0.0 {
459 let keep = ((reducible - deficit).max(0.0)) / reducible;
460 title_h *= keep;
461 x_h *= keep;
462 }
463 }
464
465 let plot_rect = Rect::from_min_max(
466 egui::pos2(outer.min.x + y_w, outer.min.y + title_h + title_gap),
467 egui::pos2(
468 (outer.max.x - right_pad).max(outer.min.x + y_w + 1.0),
469 outer.max.y - x_h - x_gap,
470 ),
471 );
472 let frame_rect = plot_rect;
473 let title_rect = Rect::from_min_max(
474 egui::pos2(outer.min.x, outer.min.y),
475 egui::pos2(outer.max.x, plot_rect.min.y),
476 );
477 let x_label_rect = Rect::from_min_max(
478 egui::pos2(outer.min.x, plot_rect.max.y),
479 egui::pos2(outer.max.x, outer.max.y),
480 );
481 let y_label_rect = Rect::from_min_max(
482 egui::pos2(outer.min.x, plot_rect.min.y),
483 egui::pos2(plot_rect.min.x, plot_rect.max.y),
484 );
485 PanelLayout {
486 plot_rect,
487 frame_rect,
488 title_rect,
489 x_label_rect,
490 y_label_rect,
491 }
492 }
493
494 fn layout_3d_panel(&self, outer: Rect, title: Option<&str>, scale: f32) -> PanelLayout {
495 let scale = scale.max(0.75);
496 let title_h = if Self::has_visible_text(title) {
497 (28.0 * scale).min(outer.height().max(1.0) * 0.16)
498 } else {
499 0.0
500 };
501 let plot_rect =
502 Rect::from_min_max(egui::pos2(outer.min.x, outer.min.y + title_h), outer.max);
503 let title_rect = Rect::from_min_max(outer.min, egui::pos2(outer.max.x, plot_rect.min.y));
504 PanelLayout {
505 plot_rect,
506 frame_rect: plot_rect,
507 title_rect,
508 x_label_rect: plot_rect,
509 y_label_rect: plot_rect,
510 }
511 }
512
513 fn axes_is_3d(plot_renderer: &PlotRenderer, axes_index: usize) -> bool {
514 let cam = plot_renderer
515 .axes_camera(axes_index)
516 .unwrap_or_else(|| plot_renderer.camera());
517 matches!(
518 cam.projection,
519 crate::core::camera::ProjectionType::Perspective { .. }
520 )
521 }
522
523 fn panel_layout_for_axes(
524 &self,
525 outer: Rect,
526 plot_renderer: &PlotRenderer,
527 axes_index: usize,
528 scale: f32,
529 ) -> PanelLayout {
530 if Self::axes_is_3d(plot_renderer, axes_index) {
531 self.layout_3d_panel(
532 outer,
533 plot_renderer
534 .overlay_title_for_axes(axes_index)
535 .map(|s| s.as_str()),
536 scale,
537 )
538 } else {
539 self.layout_2d_panel(
540 outer,
541 plot_renderer,
542 axes_index,
543 plot_renderer
544 .overlay_title_for_axes(axes_index)
545 .map(|s| s.as_str()),
546 plot_renderer
547 .overlay_x_label_for_axes(axes_index)
548 .map(|s| s.as_str()),
549 plot_renderer
550 .overlay_y_label_for_axes(axes_index)
551 .map(|s| s.as_str()),
552 scale,
553 )
554 }
555 }
556
557 fn reserve_sg_title_band(
558 &self,
559 outer: Rect,
560 plot_renderer: &PlotRenderer,
561 scale: f32,
562 ) -> (Rect, Option<Rect>) {
563 let Some(title) = plot_renderer.overlay_sg_title().map(String::as_str) else {
564 return (outer, None);
565 };
566 if !Self::has_visible_text(Some(title)) {
567 return (outer, None);
568 }
569
570 let scale = scale.max(0.75);
571 let band_h = (30.0 * scale).min(outer.height().max(1.0) * 0.14);
572 let gap = 4.0 * scale;
573 let title_max_y = (outer.min.y + band_h).min(outer.max.y);
574 let content_min_y = (title_max_y + gap).min(outer.max.y);
575 (
576 Rect::from_min_max(egui::pos2(outer.min.x, content_min_y), outer.max),
577 Some(Rect::from_min_max(
578 outer.min,
579 egui::pos2(outer.max.x, title_max_y),
580 )),
581 )
582 }
583
584 fn compute_subplot_plot_rects_impl(
585 &self,
586 outer: Rect,
587 plot_renderer: &PlotRenderer,
588 font_scale: f32,
589 show_title: bool,
590 ) -> Vec<Rect> {
591 let plot_area = Self::outer_plot_area_for_axes(outer, plot_renderer);
592 let (plot_area, _) = if show_title {
593 self.reserve_sg_title_band(plot_area, plot_renderer, font_scale)
594 } else {
595 (plot_area, None)
596 };
597 let (rows, cols) = plot_renderer.figure_axes_grid();
598 if rows * cols <= 1 {
599 vec![
600 self.panel_layout_for_axes(plot_area, plot_renderer, 0, font_scale)
601 .plot_rect,
602 ]
603 } else {
604 let rects = self.compute_subplot_rects(
605 plot_area,
606 rows,
607 cols,
608 Self::SUBPLOT_GAP_POINTS,
609 Self::SUBPLOT_GAP_POINTS,
610 );
611 rects
612 .into_iter()
613 .enumerate()
614 .map(|(axes_index, rect)| {
615 self.panel_layout_for_axes(rect, plot_renderer, axes_index, font_scale)
616 .plot_rect
617 })
618 .collect()
619 }
620 }
621
622 pub fn compute_subplot_plot_rects(
623 &self,
624 outer: Rect,
625 plot_renderer: &PlotRenderer,
626 font_scale: f32,
627 ) -> Vec<Rect> {
628 self.compute_subplot_plot_rects_impl(outer, plot_renderer, font_scale, true)
629 }
630
631 pub fn compute_subplot_plot_rects_explicit(
635 &self,
636 outer: Rect,
637 plot_renderer: &PlotRenderer,
638 font_scale: f32,
639 show_title: bool,
640 ) -> Vec<Rect> {
641 self.compute_subplot_plot_rects_impl(outer, plot_renderer, font_scale, show_title)
642 }
643
644 pub fn snap_rect_to_pixels(rect: Rect, pixels_per_point: f32) -> Rect {
645 let ppp = pixels_per_point.max(0.5);
646 let min_x = (rect.min.x * ppp).round() / ppp;
647 let min_y = (rect.min.y * ppp).round() / ppp;
648 let width = (rect.width() * ppp).round().max(1.0) / ppp;
649 let height = (rect.height() * ppp).round().max(1.0) / ppp;
650 Rect::from_min_size(egui::pos2(min_x, min_y), egui::vec2(width, height))
651 }
652
653 fn snap_coord(value: f32, pixels_per_point: f32) -> f32 {
654 let ppp = pixels_per_point.max(0.5);
655 (value * ppp).round() / ppp
656 }
657
658 fn border_centerline_edges(
659 plot_rect: Rect,
660 pixels_per_point: f32,
661 stroke_width: f32,
662 ) -> (f32, f32, f32, f32) {
663 let offset = stroke_width * 0.5;
664 let left = Self::snap_coord(plot_rect.min.x - offset, pixels_per_point);
665 let right = Self::snap_coord(plot_rect.max.x + offset, pixels_per_point);
666 let top = Self::snap_coord(plot_rect.min.y - offset, pixels_per_point);
667 let bottom = Self::snap_coord(plot_rect.max.y + offset, pixels_per_point);
668 (left, right, top, bottom)
669 }
670
671 fn draw_2d_border(&self, ui: &mut egui::Ui, plot_rect: Rect) {
672 let stroke = Stroke::new(1.5, self.theme_axis_color());
673 let ppp = ui.ctx().pixels_per_point();
674 let (left, right, top, bottom) =
675 Self::border_centerline_edges(plot_rect, ppp, stroke.width);
676 ui.painter()
677 .line_segment([Pos2::new(left, top), Pos2::new(right, top)], stroke);
678 ui.painter()
679 .line_segment([Pos2::new(left, bottom), Pos2::new(right, bottom)], stroke);
680 ui.painter()
681 .line_segment([Pos2::new(left, top), Pos2::new(left, bottom)], stroke);
682 ui.painter()
683 .line_segment([Pos2::new(right, top), Pos2::new(right, bottom)], stroke);
684 }
685
686 fn draw_plot_box_mask(&self, ui: &mut egui::Ui, plot_rect: Rect) {
687 let mask = 2.0;
688 let bg = self.theme_background_color();
689 let top = Rect::from_min_max(
690 Pos2::new(plot_rect.min.x - mask, plot_rect.min.y - mask),
691 Pos2::new(plot_rect.max.x + mask, plot_rect.min.y),
692 );
693 let bottom = Rect::from_min_max(
694 Pos2::new(plot_rect.min.x - mask, plot_rect.max.y),
695 Pos2::new(plot_rect.max.x + mask, plot_rect.max.y + mask),
696 );
697 let left = Rect::from_min_max(
698 Pos2::new(plot_rect.min.x - mask, plot_rect.min.y - mask),
699 Pos2::new(plot_rect.min.x, plot_rect.max.y + mask),
700 );
701 let right = Rect::from_min_max(
702 Pos2::new(plot_rect.max.x, plot_rect.min.y - mask),
703 Pos2::new(plot_rect.max.x + mask, plot_rect.max.y + mask),
704 );
705 ui.painter().rect_filled(top, 0.0, bg);
706 ui.painter().rect_filled(bottom, 0.0, bg);
707 ui.painter().rect_filled(left, 0.0, bg);
708 ui.painter().rect_filled(right, 0.0, bg);
709 }
710
711 pub fn compute_subplot_plot_rects_snapped(
712 &self,
713 outer: Rect,
714 plot_renderer: &PlotRenderer,
715 font_scale: f32,
716 pixels_per_point: f32,
717 ) -> Vec<Rect> {
718 self.compute_subplot_plot_rects_impl(outer, plot_renderer, font_scale, true)
719 .into_iter()
720 .map(|rect| Self::snap_rect_to_pixels(rect, pixels_per_point))
721 .collect()
722 }
723
724 pub fn compute_subplot_plot_rects_snapped_explicit(
728 &self,
729 outer: Rect,
730 plot_renderer: &PlotRenderer,
731 font_scale: f32,
732 pixels_per_point: f32,
733 show_title: bool,
734 ) -> Vec<Rect> {
735 self.compute_subplot_plot_rects_impl(outer, plot_renderer, font_scale, show_title)
736 .into_iter()
737 .map(|rect| Self::snap_rect_to_pixels(rect, pixels_per_point))
738 .collect()
739 }
740
741 pub fn outer_plot_area_for_axes(available_rect: Rect, plot_renderer: &PlotRenderer) -> Rect {
742 let (rows, cols) = plot_renderer.figure_axes_grid();
743 let single_axes_is_3d = rows * cols <= 1 && Self::axes_is_3d(plot_renderer, 0);
744 if single_axes_is_3d || rows * cols > 1 {
745 available_rect
746 } else {
747 available_rect.shrink2(egui::vec2(8.0, 8.0))
748 }
749 }
750
751 fn theme_text_color(&self) -> Color32 {
752 let text = self.theme.build_theme().get_text_color();
753 Color32::from_rgba_premultiplied(
754 (text.x.clamp(0.0, 1.0) * 255.0) as u8,
755 (text.y.clamp(0.0, 1.0) * 255.0) as u8,
756 (text.z.clamp(0.0, 1.0) * 255.0) as u8,
757 (text.w.clamp(0.0, 1.0) * 255.0) as u8,
758 )
759 }
760
761 fn theme_axis_color(&self) -> Color32 {
762 let axis = self.theme.build_theme().get_axis_color();
763 Color32::from_rgba_premultiplied(
764 (axis.x.clamp(0.0, 1.0) * 255.0) as u8,
765 (axis.y.clamp(0.0, 1.0) * 255.0) as u8,
766 (axis.z.clamp(0.0, 1.0) * 255.0) as u8,
767 (axis.w.clamp(0.0, 1.0) * 255.0) as u8,
768 )
769 }
770
771 fn theme_background_color(&self) -> Color32 {
772 let bg = self.theme.build_theme().get_background_color();
773 Color32::from_rgba_premultiplied(
774 (bg.x.clamp(0.0, 1.0) * 255.0) as u8,
775 (bg.y.clamp(0.0, 1.0) * 255.0) as u8,
776 (bg.z.clamp(0.0, 1.0) * 255.0) as u8,
777 (bg.w.clamp(0.0, 1.0) * 255.0) as u8,
778 )
779 }
780
781 fn themed_grid_colors(&self) -> (Color32, Color32) {
782 let grid = self.theme.build_theme().get_grid_color();
783 let major = Color32::from_rgba_premultiplied(
784 (grid.x.clamp(0.0, 1.0) * 255.0) as u8,
785 (grid.y.clamp(0.0, 1.0) * 255.0) as u8,
786 (grid.z.clamp(0.0, 1.0) * 255.0) as u8,
787 ((grid.w.clamp(0.15, 0.55)) * 255.0) as u8,
788 );
789 let minor = Color32::from_rgba_premultiplied(
790 (grid.x.clamp(0.0, 1.0) * 255.0) as u8,
791 (grid.y.clamp(0.0, 1.0) * 255.0) as u8,
792 (grid.z.clamp(0.0, 1.0) * 255.0) as u8,
793 ((grid.w * 0.6).clamp(0.10, 0.34) * 255.0) as u8,
794 );
795 (major, minor)
796 }
797
798 pub fn apply_theme(&self, ctx: &Context) {
800 match self.theme.variant {
801 ThemeVariant::ModernDark => {
802 ModernDarkTheme::default().apply_to_egui(ctx);
803 }
804 ThemeVariant::ClassicLight => {
805 ctx.set_visuals(egui::Visuals::light());
806 }
807 ThemeVariant::HighContrast => {
808 let mut visuals = egui::Visuals::dark();
809 visuals.extreme_bg_color = egui::Color32::BLACK;
810 visuals.widgets.noninteractive.bg_fill = egui::Color32::BLACK;
811 visuals.widgets.noninteractive.fg_stroke.color = egui::Color32::WHITE;
812 ctx.set_visuals(visuals);
813 }
814 ThemeVariant::Custom => {
815 let mut visuals = egui::Visuals::light();
816 let bg = self.theme.build_theme().get_background_color();
817 if bg.x + bg.y + bg.z < 1.5 {
818 visuals = egui::Visuals::dark();
819 }
820 ctx.set_visuals(visuals);
821 }
822 }
823
824 let mut visuals = ctx.style().visuals.clone();
826 visuals.window_fill = Color32::TRANSPARENT;
827 visuals.panel_fill = Color32::TRANSPARENT;
828 visuals.extreme_bg_color = Color32::TRANSPARENT;
829 visuals.faint_bg_color = Color32::TRANSPARENT;
830 visuals.widgets.noninteractive.bg_fill = Color32::TRANSPARENT;
831 visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
832 visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
833 visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
834 visuals.widgets.open.bg_fill = Color32::TRANSPARENT;
835 ctx.set_visuals(visuals);
836 }
837
838 pub fn render(
840 &mut self,
841 ctx: &Context,
842 plot_renderer: &PlotRenderer,
843 config: &OverlayConfig,
844 metrics: OverlayMetrics,
845 ) -> FrameInfo {
846 let mut consumed_input = false;
847 let mut plot_area = None;
848
849 if config.show_sidebar {
851 consumed_input |= self.render_sidebar(ctx, plot_renderer, config, &metrics);
852 }
853
854 let central_response = egui::CentralPanel::default()
856 .frame(egui::Frame::none()) .show(ctx, |ui| {
858 if config.show_toolbar {
860 egui::TopBottomPanel::top("plot_toolbar")
861 .frame(egui::Frame::none())
862 .show_inside(ui, |ui| {
863 let padded = ui.max_rect().shrink2(egui::vec2(12.0, 6.0));
864 self.toolbar_rect = Some(padded);
865 ui.allocate_ui_at_rect(padded, |ui| {
866 ui.with_layout(
867 egui::Layout::right_to_left(egui::Align::Center),
868 |ui| {
869 ui.spacing_mut().item_spacing = egui::vec2(8.0, 4.0);
870 ui.spacing_mut().button_padding = egui::vec2(8.0, 6.0);
871 if ui.button("Save PNG").clicked() {
872 self.want_save_png = true;
873 }
874 if ui.button("Save SVG").clicked() {
875 self.want_save_svg = true;
876 }
877 if ui.button("Reset View").clicked() {
878 self.want_reset_view = true;
879 }
880 let mut grid = plot_renderer.overlay_show_grid();
881 if ui.toggle_value(&mut grid, "Grid").changed() {
882 self.want_toggle_grid = Some(grid);
883 }
884 let mut legend = plot_renderer.overlay_show_legend();
885 if ui.toggle_value(&mut legend, "Legend").changed() {
886 self.want_toggle_legend = Some(legend);
887 }
888 },
889 );
890 });
891 });
892 } else {
893 self.toolbar_rect = None;
894 }
895 plot_area = Some(self.render_plot_area(ui, plot_renderer, config));
896 });
897
898 consumed_input |= central_response.response.hovered();
899
900 if self.show_dystr_modal {
902 consumed_input |= self.render_dystr_modal(ctx);
903 }
904
905 self.plot_area = plot_area;
907
908 FrameInfo {
909 plot_area,
910 consumed_input,
911 metrics,
912 }
913 }
914
915 fn render_sidebar(
917 &mut self,
918 ctx: &Context,
919 plot_renderer: &PlotRenderer,
920 config: &OverlayConfig,
921 metrics: &OverlayMetrics,
922 ) -> bool {
923 let mut consumed_input = false;
924
925 let sidebar_response = egui::SidePanel::left("plot_controls")
926 .resizable(true)
927 .default_width(config.sidebar_width)
928 .min_width(200.0)
929 .show(ctx, |ui| {
930 ui.style_mut().visuals.widgets.noninteractive.bg_fill = Color32::from_gray(25);
931 ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::from_gray(35);
932 ui.style_mut().visuals.widgets.hovered.bg_fill = Color32::from_gray(45);
933
934 ui.horizontal(|ui| {
936 let logo_size = egui::Vec2::splat(32.0);
938 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::click()).0;
939
940 ui.painter().rect_filled(
942 logo_rect,
943 4.0, Color32::from_rgb(100, 100, 100),
945 );
946
947 ui.painter().text(
949 logo_rect.center(),
950 Align2::CENTER_CENTER,
951 "D",
952 FontId::proportional(20.0),
953 Color32::WHITE,
954 );
955
956 ui.vertical(|ui| {
957 ui.heading("RunMat");
958 ui.horizontal(|ui| {
959 ui.small("a community project by ");
960 if ui.small_button("dystr.com").clicked() {
961 self.show_dystr_modal = true;
962 }
963 });
964 });
965 });
966 ui.separator();
967 ui.label("GC Stats: [not available]");
968
969 ui.collapsing("📷 Camera", |ui| {
971 let camera = plot_renderer.camera();
972 ui.label(format!(
973 "Position: {:.2}, {:.2}, {:.2}",
974 camera.position.x, camera.position.y, camera.position.z
975 ));
976 ui.label(format!(
977 "Target: {:.2}, {:.2}, {:.2}",
978 camera.target.x, camera.target.y, camera.target.z
979 ));
980
981 if let Some(vb) = plot_renderer.view_bounds() {
982 ui.label(format!("View X: {:.2} to {:.2}", vb.0, vb.1));
983 ui.label(format!("View Y: {:.2} to {:.2}", vb.2, vb.3));
984 }
985 if let Some(db) = plot_renderer.data_bounds() {
986 ui.label(format!("Data X: {:.2} to {:.2}", db.0, db.1));
987 ui.label(format!("Data Y: {:.2} to {:.2}", db.2, db.3));
988 }
989 });
990
991 ui.collapsing("🎬 Scene", |ui| {
993 let stats = plot_renderer.scene_statistics();
994 ui.label(format!("Nodes: {}", stats.total_nodes));
995 ui.label(format!("Visible: {}", stats.visible_nodes));
996 ui.label(format!("Vertices: {}", stats.total_vertices));
997 ui.label(format!("Triangles: {}", stats.total_triangles));
998 });
999
1000 ui.collapsing("⚡ Performance", |ui| {
1002 ui.label(format!("FPS: {:.1}", metrics.fps));
1003 ui.label(format!("Render: {:.2}ms", metrics.render_time_ms));
1004 ui.label(format!("Vertices: {}", metrics.vertex_count));
1005 ui.label(format!("Triangles: {}", metrics.triangle_count));
1006 });
1007
1008 ui.collapsing("🎨 Theme", |ui| {
1010 let label = match self.theme.variant {
1011 ThemeVariant::ModernDark => "Modern Dark",
1012 ThemeVariant::ClassicLight => "Classic Light",
1013 ThemeVariant::HighContrast => "High Contrast",
1014 ThemeVariant::Custom => "Custom",
1015 };
1016 ui.label(format!("{label} (Active)"));
1017 ui.checkbox(&mut self.show_debug, "Show Debug Info");
1018 });
1019
1020 ui.separator();
1021
1022 ui.collapsing("🔧 Controls", |ui| {
1024 ui.label("🖱️ Orbit: MMB drag (or RMB drag)");
1025 ui.label("🖱️ Pan: Shift + MMB drag (or Shift + RMB drag)");
1026 ui.label("🖱️ Zoom: Scroll wheel (zooms to cursor)");
1027 ui.label("🖱️ Alt + LMB/MMB/RMB: Orbit/Pan/Zoom");
1028 ui.label("📱 Touch: Pinch to zoom");
1029 });
1030 });
1031
1032 consumed_input |= sidebar_response.response.hovered();
1033 self.sidebar_rect = Some(sidebar_response.response.rect);
1034 consumed_input
1035 }
1036
1037 fn render_plot_area(
1039 &mut self,
1040 ui: &mut egui::Ui,
1041 plot_renderer: &PlotRenderer,
1042 config: &OverlayConfig,
1043 ) -> Rect {
1044 let available_rect = ui.available_rect_before_wrap();
1045 let mut rendered_axes_rects: Vec<Rect> = Vec::new();
1046
1047 let (rows, cols) = plot_renderer.figure_axes_grid();
1048 let plot_rect = Self::outer_plot_area_for_axes(available_rect, plot_renderer);
1049 let (plot_rect, sg_title_rect) = if config.show_title {
1050 self.reserve_sg_title_band(plot_rect, plot_renderer, config.font_scale)
1051 } else {
1052 (plot_rect, None)
1053 };
1054
1055 let plot_area_rect = plot_rect;
1058
1059 if let Some(title_rect) = sg_title_rect {
1060 if let Some(title) = plot_renderer.overlay_sg_title() {
1061 self.draw_title_in_rect(
1062 ui,
1063 title_rect,
1064 title,
1065 Some(plot_renderer.overlay_sg_title_style()),
1066 config.font_scale,
1067 );
1068 }
1069 }
1070
1071 if rows * cols > 1 {
1072 let rects = self.compute_subplot_rects(
1073 plot_area_rect,
1074 rows,
1075 cols,
1076 Self::SUBPLOT_GAP_POINTS,
1077 Self::SUBPLOT_GAP_POINTS,
1078 );
1079 for (i, cell_rect) in rects.iter().enumerate() {
1080 let cam = plot_renderer
1081 .axes_camera(i)
1082 .unwrap_or_else(|| plot_renderer.camera());
1083 let panel_layout =
1084 self.panel_layout_for_axes(*cell_rect, plot_renderer, i, config.font_scale);
1085 let r =
1086 Self::snap_rect_to_pixels(panel_layout.plot_rect, ui.ctx().pixels_per_point());
1087 let frame_rect =
1088 Self::snap_rect_to_pixels(panel_layout.frame_rect, ui.ctx().pixels_per_point());
1089 rendered_axes_rects.push(r);
1090 log::debug!(
1091 target: "runmat_plot.axes_layout",
1092 "computed axes panel layout axes_index={} rows={} cols={} is_3d={} cell=({}, {})..({}, {}) frame=({}, {})..({}, {}) content=({}, {})..({}, {})",
1093 i,
1094 rows,
1095 cols,
1096 Self::axes_is_3d(plot_renderer, i),
1097 cell_rect.min.x,
1098 cell_rect.min.y,
1099 cell_rect.max.x,
1100 cell_rect.max.y,
1101 frame_rect.min.x,
1102 frame_rect.min.y,
1103 frame_rect.max.x,
1104 frame_rect.max.y,
1105 r.min.x,
1106 r.min.y,
1107 r.max.x,
1108 r.max.y
1109 );
1110 if matches!(
1111 cam.projection,
1112 crate::core::camera::ProjectionType::Perspective { .. }
1113 ) {
1114 if config.show_title {
1115 if let Some(title) = plot_renderer.overlay_title_for_axes(i) {
1116 self.draw_title_in_rect(
1117 ui,
1118 panel_layout.title_rect,
1119 title,
1120 None,
1121 config.font_scale,
1122 );
1123 }
1124 }
1125 self.draw_3d_orientation_gizmo(ui, r, plot_renderer, i, config.font_scale);
1126 self.draw_3d_origin_axis_ticks(ui, r, plot_renderer, i, config.font_scale);
1127 self.draw_projected_world_texts(ui, r, plot_renderer, i, config.font_scale);
1128 for (label, pos) in plot_renderer.pie_labels_for_axes(i) {
1129 self.draw_pie_label(ui, r, &label, pos, config.font_scale);
1130 }
1131 if plot_renderer.overlay_show_legend_for_axes(i) {
1132 let entries = plot_renderer.overlay_legend_entries_for_axes(i);
1133 self.draw_legend(ui, r, &entries, config.font_scale);
1134 }
1135 continue;
1136 }
1137 if plot_renderer.overlay_show_box_for_axes(i) {
1139 self.draw_plot_box_mask(ui, r);
1140 self.draw_2d_border(ui, frame_rect);
1141 }
1142
1143 if config.show_grid && plot_renderer.overlay_show_grid_for_axes(i) {
1145 let b = plot_renderer.view_bounds_for_axes(i);
1146 self.draw_grid(ui, r, plot_renderer, b, Some(i));
1147 }
1148 if config.show_axes {
1150 let b = plot_renderer.view_bounds_for_axes(i);
1151 self.draw_axes(ui, r, plot_renderer, config, b, Some(i));
1152 }
1153
1154 if config.show_title {
1155 if let Some(title) = plot_renderer.overlay_title_for_axes(i) {
1156 self.draw_title_in_rect(
1157 ui,
1158 panel_layout.title_rect,
1159 title,
1160 None,
1161 config.font_scale,
1162 );
1163 }
1164 }
1165 if !matches!(
1166 cam.projection,
1167 crate::core::camera::ProjectionType::Perspective { .. }
1168 ) {
1169 if let Some(x_label) = plot_renderer.overlay_x_label_for_axes(i) {
1170 self.draw_x_label_in_rect(
1171 ui,
1172 panel_layout.x_label_rect,
1173 x_label,
1174 config.font_scale,
1175 );
1176 }
1177 }
1178 if !matches!(
1179 cam.projection,
1180 crate::core::camera::ProjectionType::Perspective { .. }
1181 ) {
1182 if let Some(y_label) = plot_renderer.overlay_y_label_for_axes(i) {
1183 self.draw_y_label_in_rect(
1184 ui,
1185 panel_layout.y_label_rect,
1186 y_label,
1187 config.font_scale,
1188 );
1189 }
1190 }
1191 self.draw_projected_world_texts(ui, r, plot_renderer, i, config.font_scale);
1192 for (label, pos) in plot_renderer.pie_labels_for_axes(i) {
1193 self.draw_pie_label(ui, r, &label, pos, config.font_scale);
1194 }
1195 if plot_renderer.overlay_show_legend_for_axes(i) {
1196 let entries = plot_renderer.overlay_legend_entries_for_axes(i);
1197 self.draw_legend(ui, r, &entries, config.font_scale);
1198 }
1199 }
1200 } else {
1201 let cam = plot_renderer.camera();
1202 let panel_layout =
1203 self.panel_layout_for_axes(plot_area_rect, plot_renderer, 0, config.font_scale);
1204 let centered_plot_rect =
1205 Self::snap_rect_to_pixels(panel_layout.plot_rect, ui.ctx().pixels_per_point());
1206 let centered_frame_rect =
1207 Self::snap_rect_to_pixels(panel_layout.frame_rect, ui.ctx().pixels_per_point());
1208 rendered_axes_rects.push(centered_plot_rect);
1209 log::debug!(
1210 target: "runmat_plot.axes_layout",
1211 "computed axes panel layout axes_index=0 rows={} cols={} is_3d={} cell=({}, {})..({}, {}) frame=({}, {})..({}, {}) content=({}, {})..({}, {})",
1212 rows,
1213 cols,
1214 Self::axes_is_3d(plot_renderer, 0),
1215 plot_area_rect.min.x,
1216 plot_area_rect.min.y,
1217 plot_area_rect.max.x,
1218 plot_area_rect.max.y,
1219 centered_frame_rect.min.x,
1220 centered_frame_rect.min.y,
1221 centered_frame_rect.max.x,
1222 centered_frame_rect.max.y,
1223 centered_plot_rect.min.x,
1224 centered_plot_rect.min.y,
1225 centered_plot_rect.max.x,
1226 centered_plot_rect.max.y
1227 );
1228 if config.show_title {
1229 if let Some(title) = plot_renderer
1230 .overlay_title_for_axes(0)
1231 .or(config.title.as_ref())
1232 {
1233 self.draw_title_in_rect(
1234 ui,
1235 panel_layout.title_rect,
1236 title,
1237 None,
1238 config.font_scale,
1239 );
1240 }
1241 }
1242 if matches!(
1243 cam.projection,
1244 crate::core::camera::ProjectionType::Perspective { .. }
1245 ) {
1246 self.draw_3d_orientation_gizmo(
1247 ui,
1248 centered_plot_rect,
1249 plot_renderer,
1250 0,
1251 config.font_scale,
1252 );
1253 self.draw_3d_origin_axis_ticks(
1254 ui,
1255 centered_plot_rect,
1256 plot_renderer,
1257 0,
1258 config.font_scale,
1259 );
1260 self.draw_projected_world_texts(
1261 ui,
1262 centered_plot_rect,
1263 plot_renderer,
1264 0,
1265 config.font_scale,
1266 );
1267 } else {
1268 if plot_renderer.overlay_show_box() {
1270 self.draw_plot_box_mask(ui, centered_plot_rect);
1271 self.draw_2d_border(ui, centered_frame_rect);
1272 }
1273 if config.show_grid {
1275 self.draw_grid(ui, centered_plot_rect, plot_renderer, None, None);
1276 }
1277
1278 if config.show_axes {
1280 self.draw_axes(ui, centered_plot_rect, plot_renderer, config, None, None);
1281 if let Some((x_min, x_max, y_min, y_max)) = plot_renderer
1283 .view_bounds()
1284 .or_else(|| plot_renderer.data_bounds())
1285 {
1286 let axis_color = self.theme_axis_color();
1287 let zero_stroke = Stroke::new(1.5, axis_color);
1288 if y_min < 0.0 && y_max > 0.0 {
1289 let y_screen = centered_plot_rect.max.y
1290 - ((0.0 - y_min) / (y_max - y_min)) as f32
1291 * centered_plot_rect.height();
1292 ui.painter().line_segment(
1293 [
1294 Pos2::new(centered_plot_rect.min.x, y_screen),
1295 Pos2::new(centered_plot_rect.max.x, y_screen),
1296 ],
1297 zero_stroke,
1298 );
1299 }
1300 if x_min < 0.0 && x_max > 0.0 {
1301 let x_screen = centered_plot_rect.min.x
1302 + ((0.0 - x_min) / (x_max - x_min)) as f32
1303 * centered_plot_rect.width();
1304 ui.painter().line_segment(
1305 [
1306 Pos2::new(x_screen, centered_plot_rect.min.y),
1307 Pos2::new(x_screen, centered_plot_rect.max.y),
1308 ],
1309 zero_stroke,
1310 );
1311 }
1312 }
1313 }
1314 if let Some(x_label) = plot_renderer
1315 .overlay_x_label_for_axes(0)
1316 .or(config.x_label.as_ref())
1317 {
1318 self.draw_x_label_in_rect(
1319 ui,
1320 panel_layout.x_label_rect,
1321 x_label,
1322 config.font_scale,
1323 );
1324 }
1325 if let Some(y_label) = plot_renderer
1326 .overlay_y_label_for_axes(0)
1327 .or(config.y_label.as_ref())
1328 {
1329 self.draw_y_label_in_rect(
1330 ui,
1331 panel_layout.y_label_rect,
1332 y_label,
1333 config.font_scale,
1334 );
1335 }
1336 self.draw_projected_world_texts(
1337 ui,
1338 centered_plot_rect,
1339 plot_renderer,
1340 0,
1341 config.font_scale,
1342 );
1343 }
1344 }
1345 let centered_plot_rect = if rows * cols <= 1 {
1346 self.panel_layout_for_axes(plot_area_rect, plot_renderer, 0, config.font_scale)
1347 .plot_rect
1348 } else {
1349 plot_area_rect
1350 };
1351 for (label, pos) in if rows * cols <= 1 {
1352 plot_renderer.active_axes_pie_labels()
1353 } else {
1354 Vec::new()
1355 } {
1356 self.draw_pie_label(ui, centered_plot_rect, &label, pos, config.font_scale);
1357 }
1358
1359 if rows * cols <= 1 && plot_renderer.overlay_show_legend() {
1361 self.draw_legend(
1362 ui,
1363 centered_plot_rect,
1364 plot_renderer.overlay_legend_entries(),
1365 config.font_scale,
1366 );
1367 }
1368
1369 if plot_renderer.overlay_colorbar_enabled() {
1371 let bar_width = 12.0;
1373 let pad = 8.0;
1374 let bar_rect = Rect::from_min_max(
1375 egui::pos2(
1376 centered_plot_rect.max.x - bar_width - pad,
1377 centered_plot_rect.min.y + pad,
1378 ),
1379 egui::pos2(
1380 centered_plot_rect.max.x - pad,
1381 centered_plot_rect.max.y - pad,
1382 ),
1383 );
1384 let steps = 64;
1386 for i in 0..steps {
1387 let t0 = i as f32 / steps as f32;
1388 let t1 = (i + 1) as f32 / steps as f32;
1389 let y0 = bar_rect.min.y + (1.0 - t0) * bar_rect.height();
1390 let y1 = bar_rect.min.y + (1.0 - t1) * bar_rect.height();
1391 let cmap = plot_renderer.overlay_colormap();
1392 let c = cmap.map_value(t0);
1393 let col = Color32::from_rgb(
1394 (c.x * 255.0) as u8,
1395 (c.y * 255.0) as u8,
1396 (c.z * 255.0) as u8,
1397 );
1398 ui.painter().rect_filled(
1399 Rect::from_min_max(
1400 egui::pos2(bar_rect.min.x, y1),
1401 egui::pos2(bar_rect.max.x, y0),
1402 ),
1403 0.0,
1404 col,
1405 );
1406 }
1407 let bg = plot_renderer.theme.build_theme().get_background_color();
1408 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
1409 let border = if bg_luma > 0.62 {
1410 Color32::from_gray(60)
1411 } else {
1412 Color32::WHITE
1413 };
1414 ui.painter()
1415 .rect_stroke(bar_rect, 0.0, Stroke::new(1.0, border));
1416 }
1417
1418 self.axes_plot_rects = rendered_axes_rects;
1419 centered_plot_rect
1420 }
1421
1422 pub fn compute_subplot_rects(
1424 &self,
1425 outer: Rect,
1426 rows: usize,
1427 cols: usize,
1428 hgap: f32,
1429 vgap: f32,
1430 ) -> Vec<Rect> {
1431 let rows = rows.max(1) as f32;
1432 let cols = cols.max(1) as f32;
1433 let total_hgap = hgap * (cols - 1.0);
1434 let total_vgap = vgap * (rows - 1.0);
1435 let cell_w = ((outer.width()).max(1.0) - total_hgap).max(1.0) / cols;
1436 let cell_h = ((outer.height()).max(1.0) - total_vgap).max(1.0) / rows;
1437 let mut rects = Vec::new();
1438 for r in 0..rows as i32 {
1439 for c in 0..cols as i32 {
1440 let x = outer.min.x + c as f32 * (cell_w + hgap);
1441 let y = outer.min.y + r as f32 * (cell_h + vgap);
1442 rects.push(Rect::from_min_size(
1443 egui::pos2(x, y),
1444 egui::vec2(cell_w, cell_h),
1445 ));
1446 }
1447 }
1448 rects
1449 }
1450
1451 fn draw_grid(
1453 &self,
1454 ui: &mut egui::Ui,
1455 plot_rect: Rect,
1456 plot_renderer: &PlotRenderer,
1457 view_bounds_override: Option<(f64, f64, f64, f64)>,
1458 axes_index: Option<usize>,
1459 ) {
1460 let ppp = ui.ctx().pixels_per_point();
1461 let edge_eps = 0.51 / ppp.max(0.5);
1462 if let Some(data_bounds) = view_bounds_override
1463 .or_else(|| plot_renderer.view_bounds())
1464 .or_else(|| plot_renderer.data_bounds())
1465 {
1466 let (grid_color_major, _grid_color_minor) = self.themed_grid_colors();
1467
1468 let (x_min, x_max, y_min, y_max) = data_bounds;
1469 let x_range = x_max - x_min;
1470 let y_range = y_max - y_min;
1471
1472 let x_log = axes_index
1474 .map(|idx| plot_renderer.overlay_x_log_for_axes(idx))
1475 .unwrap_or_else(|| plot_renderer.overlay_x_log());
1476 let y_log = axes_index
1477 .map(|idx| plot_renderer.overlay_y_log_for_axes(idx))
1478 .unwrap_or_else(|| plot_renderer.overlay_y_log());
1479
1480 let x_ticks = if x_log {
1481 Vec::new()
1482 } else {
1483 plot_utils::generate_major_ticks(x_min, x_max)
1484 };
1485 let y_ticks = if y_log {
1486 Vec::new()
1487 } else {
1488 plot_utils::generate_major_ticks(y_min, y_max)
1489 };
1490
1491 if x_log {
1493 let start_decade = x_min.log10().floor() as i32;
1495 let end_decade = x_max.log10().ceil() as i32;
1496 for d in start_decade..=end_decade {
1497 let decade = 10f64.powi(d);
1498 for m in [1.0, 2.0, 5.0].iter() {
1499 let x_val = decade * m;
1500 if x_val < x_min || x_val > x_max {
1501 continue;
1502 }
1503 let x_screen = plot_rect.min.x
1504 + ((x_val.log10() - x_min.log10()) / (x_max.log10() - x_min.log10()))
1505 as f32
1506 * plot_rect.width();
1507 let x_screen = Self::snap_coord(x_screen, ppp);
1508 if (x_screen - plot_rect.min.x).abs() <= edge_eps
1509 || (x_screen - plot_rect.max.x).abs() <= edge_eps
1510 {
1511 continue;
1512 }
1513 ui.painter().line_segment(
1514 [
1515 Pos2::new(x_screen, plot_rect.min.y),
1516 Pos2::new(x_screen, plot_rect.max.y),
1517 ],
1518 Stroke::new(0.8, grid_color_major),
1519 );
1520 }
1521 }
1522 } else {
1523 for x_val in x_ticks {
1524 let x_screen =
1525 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1526 let x_screen = Self::snap_coord(x_screen, ppp);
1527 if (x_screen - plot_rect.min.x).abs() <= edge_eps
1528 || (x_screen - plot_rect.max.x).abs() <= edge_eps
1529 {
1530 continue;
1531 }
1532 ui.painter().line_segment(
1533 [
1534 Pos2::new(x_screen, plot_rect.min.y),
1535 Pos2::new(x_screen, plot_rect.max.y),
1536 ],
1537 Stroke::new(0.8, grid_color_major),
1538 );
1539 }
1540 }
1541
1542 if y_log {
1544 let start_decade = y_min.log10().floor() as i32;
1545 let end_decade = y_max.log10().ceil() as i32;
1546 for d in start_decade..=end_decade {
1547 let decade = 10f64.powi(d);
1548 for m in [1.0, 2.0, 5.0].iter() {
1549 let y_val = decade * m;
1550 if y_val < y_min || y_val > y_max {
1551 continue;
1552 }
1553 let y_screen = plot_rect.max.y
1554 - ((y_val.log10() - y_min.log10()) / (y_max.log10() - y_min.log10()))
1555 as f32
1556 * plot_rect.height();
1557 let y_screen = Self::snap_coord(y_screen, ppp);
1558 if (y_screen - plot_rect.min.y).abs() <= edge_eps
1559 || (y_screen - plot_rect.max.y).abs() <= edge_eps
1560 {
1561 continue;
1562 }
1563 ui.painter().line_segment(
1564 [
1565 Pos2::new(plot_rect.min.x, y_screen),
1566 Pos2::new(plot_rect.max.x, y_screen),
1567 ],
1568 Stroke::new(0.8, grid_color_major),
1569 );
1570 }
1571 }
1572 } else {
1573 for y_val in y_ticks {
1574 let y_screen =
1575 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1576 let y_screen = Self::snap_coord(y_screen, ppp);
1577 if (y_screen - plot_rect.min.y).abs() <= edge_eps
1578 || (y_screen - plot_rect.max.y).abs() <= edge_eps
1579 {
1580 continue;
1581 }
1582 ui.painter().line_segment(
1583 [
1584 Pos2::new(plot_rect.min.x, y_screen),
1585 Pos2::new(plot_rect.max.x, y_screen),
1586 ],
1587 Stroke::new(0.8, grid_color_major),
1588 );
1589 }
1590 }
1591 }
1592 }
1593
1594 fn draw_axes(
1596 &self,
1597 ui: &mut egui::Ui,
1598 plot_rect: Rect,
1599 plot_renderer: &PlotRenderer,
1600 config: &OverlayConfig,
1601 view_bounds_override: Option<(f64, f64, f64, f64)>,
1602 axes_index: Option<usize>,
1603 ) {
1604 let ppp = ui.ctx().pixels_per_point();
1605 if let Some(data_bounds) = view_bounds_override
1606 .or_else(|| plot_renderer.view_bounds())
1607 .or_else(|| plot_renderer.data_bounds())
1608 {
1609 let (x_min, x_max, y_min, y_max) = data_bounds;
1610 let x_range = x_max - x_min;
1611 let y_range = y_max - y_min;
1612 let scale = config.font_scale.max(0.75);
1613 let tick_length = 6.0 * scale;
1614 let label_offset = 15.0 * scale;
1615 let tick_font = FontId::proportional(10.0 * scale);
1616 let axis_color = self.theme_axis_color();
1617 let label_color = self.theme_text_color();
1618 let border_left = plot_rect.min.x;
1619 let border_bottom = plot_rect.max.y;
1620
1621 let x_log = axes_index
1622 .map(|idx| plot_renderer.overlay_x_log_for_axes(idx))
1623 .unwrap_or_else(|| plot_renderer.overlay_x_log());
1624 let y_log = axes_index
1625 .map(|idx| plot_renderer.overlay_y_log_for_axes(idx))
1626 .unwrap_or_else(|| plot_renderer.overlay_y_log());
1627
1628 let (mut cat_x, mut cat_y) = (false, false);
1630 let mut custom_hist_x = false;
1631 if let Some((true, edges)) =
1632 axes_index.and_then(|idx| plot_renderer.overlay_histogram_edges_for_axes(idx))
1633 {
1634 custom_hist_x = true;
1635 self.draw_histogram_axis_ticks(
1636 ui,
1637 plot_rect,
1638 ppp,
1639 axis_color,
1640 label_color,
1641 tick_length,
1642 label_offset,
1643 tick_font.clone(),
1644 border_bottom,
1645 x_min,
1646 x_max,
1647 &edges,
1648 );
1649 }
1650 if let Some(labels) = axes_index.and_then(|idx| {
1651 plot_renderer
1652 .overlay_x_tick_labels_for_axes(idx)
1653 .filter(|labels| !labels.is_empty())
1654 }) {
1655 cat_x = true;
1656 let stride = Self::label_stride(&labels, plot_rect.width(), tick_font.size);
1657 for (label_idx, label) in labels.iter().enumerate() {
1658 if label_idx != 0 && label_idx != labels.len() - 1 && label_idx % stride != 0 {
1659 continue;
1660 }
1661 let x_val = (label_idx + 1) as f64;
1662 if x_val < x_min || x_val > x_max {
1663 continue;
1664 }
1665 let x_screen =
1666 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1667 let x_screen = Self::snap_coord(x_screen, ppp);
1668 ui.painter().line_segment(
1669 [
1670 Pos2::new(x_screen, border_bottom),
1671 Pos2::new(x_screen, border_bottom + tick_length),
1672 ],
1673 Stroke::new(1.0, axis_color),
1674 );
1675 let text = truncate_label(label, 14);
1676 ui.painter().text(
1677 Pos2::new(x_screen, border_bottom + label_offset),
1678 Align2::CENTER_CENTER,
1679 text,
1680 tick_font.clone(),
1681 label_color,
1682 );
1683 }
1684 }
1685 if let Some(labels) = axes_index.and_then(|idx| {
1686 plot_renderer
1687 .overlay_y_tick_labels_for_axes(idx)
1688 .filter(|labels| !labels.is_empty())
1689 }) {
1690 cat_y = true;
1691 let stride = Self::label_stride(&labels, plot_rect.height(), tick_font.size);
1692 for (label_idx, label) in labels.iter().enumerate() {
1693 if label_idx != 0 && label_idx != labels.len() - 1 && label_idx % stride != 0 {
1694 continue;
1695 }
1696 let y_val = (label_idx + 1) as f64;
1697 if y_val < y_min || y_val > y_max {
1698 continue;
1699 }
1700 let y_screen =
1701 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1702 let y_screen = Self::snap_coord(y_screen, ppp);
1703 ui.painter().line_segment(
1704 [
1705 Pos2::new(border_left - tick_length, y_screen),
1706 Pos2::new(border_left, y_screen),
1707 ],
1708 Stroke::new(1.0, axis_color),
1709 );
1710 let text = truncate_label(label, 14);
1711 ui.painter().text(
1712 Pos2::new(border_left - label_offset, y_screen),
1713 Align2::CENTER_CENTER,
1714 text,
1715 tick_font.clone(),
1716 label_color,
1717 );
1718 }
1719 }
1720 if let Some((is_x, labels)) = axes_index
1721 .and_then(|idx| plot_renderer.overlay_categorical_labels_for_axes(idx))
1722 .or_else(|| {
1723 plot_renderer
1724 .overlay_categorical_labels()
1725 .map(|(is_x, labels)| (is_x, labels.clone()))
1726 })
1727 {
1728 if (is_x && cat_x) || (!is_x && cat_y) {
1729 } else if is_x {
1731 cat_x = true;
1732 let stride = Self::label_stride(&labels, plot_rect.width(), tick_font.size);
1733 for (label_idx, label) in labels.iter().enumerate() {
1735 if label_idx != 0
1736 && label_idx != labels.len() - 1
1737 && label_idx % stride != 0
1738 {
1739 continue;
1740 }
1741 let x_val = (label_idx + 1) as f64;
1742 if x_val < x_min || x_val > x_max {
1743 continue;
1744 }
1745 let x_screen = plot_rect.min.x
1746 + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1747 let x_screen = Self::snap_coord(x_screen, ppp);
1748 ui.painter().line_segment(
1750 [
1751 Pos2::new(x_screen, border_bottom),
1752 Pos2::new(x_screen, border_bottom + tick_length),
1753 ],
1754 Stroke::new(1.0, axis_color),
1755 );
1756 let text = truncate_label(label, 14);
1758 ui.painter().text(
1759 Pos2::new(x_screen, border_bottom + label_offset),
1760 Align2::CENTER_CENTER,
1761 text,
1762 tick_font.clone(),
1763 label_color,
1764 );
1765 }
1766 } else {
1767 cat_y = true;
1768 let stride = Self::label_stride(&labels, plot_rect.height(), tick_font.size);
1769 for (label_idx, label) in labels.iter().enumerate() {
1771 if label_idx != 0
1772 && label_idx != labels.len() - 1
1773 && label_idx % stride != 0
1774 {
1775 continue;
1776 }
1777 let y_val = (label_idx + 1) as f64;
1778 if y_val < y_min || y_val > y_max {
1779 continue;
1780 }
1781 let y_screen = plot_rect.max.y
1782 - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1783 let y_screen = Self::snap_coord(y_screen, ppp);
1784 ui.painter().line_segment(
1786 [
1787 Pos2::new(border_left - tick_length, y_screen),
1788 Pos2::new(border_left, y_screen),
1789 ],
1790 Stroke::new(1.0, axis_color),
1791 );
1792 let text = truncate_label(label, 14);
1794 ui.painter().text(
1795 Pos2::new(border_left - label_offset, y_screen),
1796 Align2::CENTER_CENTER,
1797 text,
1798 tick_font.clone(),
1799 label_color,
1800 );
1801 }
1802 }
1803 }
1804
1805 if x_log {
1807 let start_decade = x_min.log10().floor() as i32;
1808 let end_decade = x_max.log10().ceil() as i32;
1809 for d in start_decade..=end_decade {
1810 let decade = 10f64.powi(d);
1811 let x_screen = plot_rect.min.x
1812 + ((decade.log10() - x_min.log10()) / (x_max.log10() - x_min.log10()))
1813 as f32
1814 * plot_rect.width();
1815 let x_screen = Self::snap_coord(x_screen, ppp);
1816 ui.painter().line_segment(
1818 [
1819 Pos2::new(x_screen, border_bottom),
1820 Pos2::new(x_screen, border_bottom + tick_length),
1821 ],
1822 Stroke::new(1.0, axis_color),
1823 );
1824 ui.painter().text(
1826 Pos2::new(x_screen, border_bottom + label_offset),
1827 Align2::CENTER_CENTER,
1828 format!("10^{}", d),
1829 tick_font.clone(),
1830 label_color,
1831 );
1832 }
1833 } else if !cat_x && !custom_hist_x {
1834 for x_val in plot_utils::generate_major_ticks(x_min, x_max) {
1835 let x_screen =
1836 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1837 let x_screen = Self::snap_coord(x_screen, ppp);
1838 ui.painter().line_segment(
1839 [
1840 Pos2::new(x_screen, border_bottom),
1841 Pos2::new(x_screen, border_bottom + tick_length),
1842 ],
1843 Stroke::new(1.0, axis_color),
1844 );
1845 ui.painter().text(
1846 Pos2::new(x_screen, border_bottom + label_offset),
1847 Align2::CENTER_CENTER,
1848 plot_utils::format_tick_label(x_val),
1849 tick_font.clone(),
1850 label_color,
1851 );
1852 }
1853 }
1854
1855 if y_log {
1857 let start_decade = y_min.log10().floor() as i32;
1858 let end_decade = y_max.log10().ceil() as i32;
1859 for d in start_decade..=end_decade {
1860 let decade = 10f64.powi(d);
1861 let y_screen = plot_rect.max.y
1862 - ((decade.log10() - y_min.log10()) / (y_max.log10() - y_min.log10()))
1863 as f32
1864 * plot_rect.height();
1865 let y_screen = Self::snap_coord(y_screen, ppp);
1866 ui.painter().line_segment(
1867 [
1868 Pos2::new(border_left - tick_length, y_screen),
1869 Pos2::new(border_left, y_screen),
1870 ],
1871 Stroke::new(1.0, axis_color),
1872 );
1873 ui.painter().text(
1874 Pos2::new(border_left - label_offset, y_screen),
1875 Align2::CENTER_CENTER,
1876 format!("10^{}", d),
1877 tick_font.clone(),
1878 label_color,
1879 );
1880 }
1881 } else if !cat_y {
1882 for y_val in plot_utils::generate_major_ticks(y_min, y_max) {
1883 let y_screen =
1884 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1885 let y_screen = Self::snap_coord(y_screen, ppp);
1886 ui.painter().line_segment(
1887 [
1888 Pos2::new(border_left - tick_length, y_screen),
1889 Pos2::new(border_left, y_screen),
1890 ],
1891 Stroke::new(1.0, axis_color),
1892 );
1893 ui.painter().text(
1894 Pos2::new(border_left - label_offset, y_screen),
1895 Align2::CENTER_CENTER,
1896 plot_utils::format_tick_label(y_val),
1897 tick_font.clone(),
1898 label_color,
1899 );
1900 }
1901 }
1902 }
1903 }
1904
1905 fn draw_3d_orientation_gizmo(
1908 &self,
1909 ui: &mut egui::Ui,
1910 plot_rect: Rect,
1911 plot_renderer: &PlotRenderer,
1912 axes_index: usize,
1913 font_scale: f32,
1914 ) {
1915 let cam_ref = plot_renderer
1916 .axes_camera(axes_index)
1917 .unwrap_or_else(|| plot_renderer.camera());
1918 let cam = cam_ref.clone();
1919
1920 let forward = (cam.target - cam.position).normalize_or_zero();
1921 if forward.length_squared() < 1e-9 {
1922 return;
1923 }
1924 let world_up = cam.up.normalize_or_zero();
1925 let right = forward.cross(world_up).normalize_or_zero();
1926 if right.length_squared() < 1e-9 {
1927 return;
1928 }
1929 let up = right.cross(forward).normalize_or_zero();
1930 if up.length_squared() < 1e-9 {
1931 return;
1932 }
1933
1934 let scale = font_scale.max(0.75);
1935 let base = plot_rect.width().min(plot_rect.height()).max(1.0);
1936 let gizmo_size = (base * 0.16).clamp(44.0, 110.0) * scale;
1937 let pad = (30.0 * scale).round();
1938 let origin = Pos2::new(plot_rect.min.x + pad, plot_rect.max.y - pad);
1939
1940 struct AxisItem {
1941 label: &'static str,
1942 dir_world: Vec3,
1943 color: Color32,
1944 z_sort: f32,
1945 }
1946
1947 let mut axes = [
1948 AxisItem {
1949 label: "X",
1950 dir_world: Vec3::X,
1951 color: Color32::from_rgb(235, 80, 80),
1952 z_sort: 0.0,
1953 },
1954 AxisItem {
1955 label: "Y",
1956 dir_world: Vec3::Y,
1957 color: Color32::from_rgb(90, 220, 120),
1958 z_sort: 0.0,
1959 },
1960 AxisItem {
1961 label: "Z",
1962 dir_world: Vec3::Z,
1963 color: Color32::from_rgb(90, 160, 255),
1964 z_sort: 0.0,
1965 },
1966 ];
1967
1968 for a in axes.iter_mut() {
1971 let x = a.dir_world.dot(right);
1972 let y = a.dir_world.dot(up);
1973 let z = a.dir_world.dot(-forward);
1974 a.z_sort = z;
1975 a.dir_world = Vec3::new(x, y, z);
1976 }
1977 axes.sort_by(|a, b| {
1978 a.z_sort
1979 .partial_cmp(&b.z_sort)
1980 .unwrap_or(std::cmp::Ordering::Equal)
1981 });
1982
1983 let painter = ui.painter();
1984
1985 painter.circle_filled(origin, 2.0 * scale, Color32::from_gray(210));
1986
1987 let axis_len = gizmo_size * 0.65;
1988 let head_len = (8.0 * scale).min(axis_len * 0.35);
1989 let head_w = 5.0 * scale;
1990 let font = FontId::proportional(11.0 * scale);
1991
1992 for a in axes.iter() {
1993 let dir2 = egui::Vec2::new(a.dir_world.x, -a.dir_world.y);
1994 let mag = dir2.length();
1995 if !mag.is_finite() || mag < 1e-4 {
1996 continue;
1997 }
1998 let d = dir2 / mag;
1999
2000 let end = origin + d * axis_len;
2001 let stroke = Stroke::new(2.0 * scale, a.color);
2002 painter.line_segment([origin, end], stroke);
2003
2004 let base = end - d * head_len;
2006 let perp = egui::Vec2::new(-d.y, d.x);
2007 painter.line_segment([end, base + perp * head_w], stroke);
2008 painter.line_segment([end, base - perp * head_w], stroke);
2009
2010 let label_pos = end + d * (10.0 * scale);
2012 painter.text(
2013 label_pos,
2014 Align2::CENTER_CENTER,
2015 a.label,
2016 font.clone(),
2017 a.color,
2018 );
2019 }
2020 }
2021
2022 fn draw_3d_origin_axis_ticks(
2025 &self,
2026 ui: &mut egui::Ui,
2027 plot_rect: Rect,
2028 plot_renderer: &PlotRenderer,
2029 axes_index: usize,
2030 font_scale: f32,
2031 ) {
2032 let cam_ref = plot_renderer
2033 .axes_camera(axes_index)
2034 .unwrap_or_else(|| plot_renderer.camera());
2035 let mut cam = cam_ref.clone();
2036 let w = plot_rect.width().max(1.0);
2037 let h = plot_rect.height().max(1.0);
2038 cam.update_aspect_ratio(w / h);
2039 let view_proj = cam.view_proj_matrix();
2040
2041 let project = |p: Vec3| -> Option<Pos2> {
2042 let clip: Vec4 = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
2043 if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
2044 return None;
2045 }
2046 let ndc = clip.truncate() / clip.w;
2047 if !(ndc.x.is_finite() && ndc.y.is_finite()) {
2048 return None;
2049 }
2050 let sx = plot_rect.min.x + ((ndc.x + 1.0) * 0.5) * plot_rect.width();
2051 let sy = plot_rect.min.y + ((1.0 - ndc.y) * 0.5) * plot_rect.height();
2052 Some(Pos2::new(sx, sy))
2053 };
2054
2055 let nice_step = |raw: f64| -> f64 {
2056 if !raw.is_finite() || raw <= 0.0 {
2057 return 1.0;
2058 }
2059 let pow10 = 10.0_f64.powf(raw.log10().floor());
2060 let norm = raw / pow10;
2061 let mult = if norm <= 1.0 {
2062 1.0
2063 } else if norm <= 2.0 {
2064 2.0
2065 } else if norm <= 5.0 {
2066 5.0
2067 } else {
2068 10.0
2069 };
2070 mult * pow10
2071 };
2072
2073 let origin = Vec3::ZERO;
2076 let px_per_world = match (project(origin), project(origin + Vec3::X)) {
2077 (Some(a), Some(b)) => ((b.x - a.x).hypot(b.y - a.y) as f64).max(1e-3),
2078 _ => 1.0,
2079 };
2080 let desired_major_px = 120.0_f64;
2081 let major_step = nice_step((desired_major_px / px_per_world).max(1e-6));
2082 if !(major_step.is_finite() && major_step > 0.0) {
2083 return;
2084 }
2085 let axis_len = (major_step as f32 * 5.0).max(0.5);
2086
2087 let scale = font_scale.max(0.75);
2088 let font = FontId::proportional(10.0 * scale);
2089 let painter = ui.painter();
2090 let col_x = Color32::from_rgb(235, 80, 80);
2091 let col_y = Color32::from_rgb(90, 220, 120);
2092 let col_z = Color32::from_rgb(90, 160, 255);
2093 let panel_center = plot_rect.center();
2094
2095 let outward_offset = |pos: Pos2, base: f32| {
2096 let dir = pos - panel_center;
2097 let len = dir.length().max(1.0);
2098 (dir / len) * base
2099 };
2100
2101 if let Some(pos) = project(Vec3::X * axis_len * 1.10) {
2102 if let Some(label) = plot_renderer.overlay_x_label_for_axes(axes_index) {
2103 let style = plot_renderer
2104 .overlay_x_label_style_for_axes(axes_index)
2105 .cloned()
2106 .unwrap_or_default();
2107 let offset = outward_offset(pos, 12.0 * scale) + egui::vec2(4.0 * scale, 0.0);
2108 Self::paint_styled_text(
2109 painter,
2110 pos + offset,
2111 Align2::LEFT_CENTER,
2112 label,
2113 Self::style_font_size(&style, 12.0, scale),
2114 Self::style_color(&style, col_x),
2115 Self::style_is_bold(&style),
2116 110,
2117 );
2118 }
2119 }
2120 if let Some(pos) = project(Vec3::Y * axis_len * 1.10) {
2121 if let Some(label) = plot_renderer.overlay_y_label_for_axes(axes_index) {
2122 let style = plot_renderer
2123 .overlay_y_label_style_for_axes(axes_index)
2124 .cloned()
2125 .unwrap_or_default();
2126 let offset =
2127 outward_offset(pos, 12.0 * scale) + egui::vec2(2.0 * scale, -2.0 * scale);
2128 Self::paint_styled_text(
2129 painter,
2130 pos + offset,
2131 Align2::LEFT_CENTER,
2132 label,
2133 Self::style_font_size(&style, 12.0, scale),
2134 Self::style_color(&style, col_y),
2135 Self::style_is_bold(&style),
2136 110,
2137 );
2138 }
2139 }
2140 if let Some(pos) = project(Vec3::Z * axis_len * 1.10) {
2141 if let Some(label) = plot_renderer.overlay_z_label_for_axes(axes_index) {
2142 let style = plot_renderer
2143 .overlay_z_label_style_for_axes(axes_index)
2144 .cloned()
2145 .unwrap_or_default();
2146 let offset = outward_offset(pos, 12.0 * scale) + egui::vec2(0.0, -4.0 * scale);
2147 Self::paint_styled_text(
2148 painter,
2149 pos + offset,
2150 Align2::LEFT_BOTTOM,
2151 label,
2152 Self::style_font_size(&style, 12.0, scale),
2153 Self::style_color(&style, col_z),
2154 Self::style_is_bold(&style),
2155 110,
2156 );
2157 }
2158 }
2159
2160 let draw_axis = |axis: Vec3, color: Color32| {
2161 for i in 1..=6 {
2162 let t = (i as f32) * (major_step as f32);
2163 if t >= axis_len * 0.999 {
2164 break;
2165 }
2166 let p = origin + axis * t;
2167 let Some(pos) = project(p) else { continue };
2168 let offset =
2170 outward_offset(pos, 7.0 * scale) + egui::Vec2::new(3.0 * scale, -3.0 * scale);
2171 painter.text(
2172 pos + offset + egui::vec2(1.0, 1.0),
2173 Align2::LEFT_CENTER,
2174 plot_utils::format_tick_label((i as f64) * major_step),
2175 font.clone(),
2176 Color32::from_rgba_premultiplied(0, 0, 0, 90),
2177 );
2178 painter.text(
2179 pos + offset,
2180 Align2::LEFT_CENTER,
2181 plot_utils::format_tick_label((i as f64) * major_step),
2182 font.clone(),
2183 color,
2184 );
2185 }
2186 };
2187 draw_axis(Vec3::X, col_x);
2188 draw_axis(Vec3::Y, col_y);
2189 draw_axis(Vec3::Z, col_z);
2190 }
2191
2192 fn draw_title_in_rect(
2193 &self,
2194 ui: &mut egui::Ui,
2195 rect: Rect,
2196 title: &str,
2197 style: Option<&TextStyle>,
2198 scale: f32,
2199 ) {
2200 let scale = scale.max(0.75);
2201 let style = style.cloned().unwrap_or_default();
2202 Self::paint_styled_text(
2203 ui.painter(),
2204 rect.center(),
2205 Align2::CENTER_CENTER,
2206 title,
2207 Self::style_font_size(&style, 16.0, scale),
2208 Self::style_color(&style, self.theme_text_color()),
2209 Self::style_is_bold(&style),
2210 110,
2211 );
2212 }
2213
2214 fn draw_legend(
2215 &self,
2216 ui: &mut egui::Ui,
2217 plot_rect: Rect,
2218 entries: &[crate::plots::figure::LegendEntry],
2219 scale: f32,
2220 ) {
2221 if entries.is_empty() {
2222 return;
2223 }
2224 let scale = scale.max(0.75);
2225 let theme = self.theme.build_theme();
2226 let bg = theme.get_background_color();
2227 let text = theme.get_text_color();
2228 let legend_text = Color32::from_rgb(
2229 (text.x.clamp(0.0, 1.0) * 255.0) as u8,
2230 (text.y.clamp(0.0, 1.0) * 255.0) as u8,
2231 (text.z.clamp(0.0, 1.0) * 255.0) as u8,
2232 );
2233 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
2234 let legend_bg = if bg_luma > 0.62 {
2235 Color32::from_rgba_premultiplied(255, 255, 255, 170)
2236 } else {
2237 Color32::from_rgba_premultiplied(0, 0, 0, 128)
2238 };
2239 let legend_stroke = if bg_luma > 0.62 {
2240 Color32::from_rgb(55, 55, 55)
2241 } else {
2242 Color32::BLACK
2243 };
2244 let pad = 8.0 * scale;
2245 let row_h = (16.0 * scale).clamp(13.0, 18.0);
2246 let swatch_w = 14.0 * scale;
2247 let text_x_gap = 18.0 * scale;
2248 let legend_w = (plot_rect.width() * 0.30).clamp(92.0, 132.0);
2249 let x = plot_rect.max.x - legend_w - pad;
2250 let mut y = plot_rect.min.y + pad + 4.0 * scale;
2251 let legend_rect = Rect::from_min_max(
2252 egui::pos2(x - pad, plot_rect.min.y + pad),
2253 egui::pos2(x + legend_w, y + entries.len() as f32 * row_h + pad),
2254 );
2255 ui.painter().rect_filled(legend_rect, 4.0, legend_bg);
2256 y += 10.0 * scale;
2257 for e in entries {
2258 let c = Color32::from_rgb(
2259 (e.color.x * 255.0) as u8,
2260 (e.color.y * 255.0) as u8,
2261 (e.color.z * 255.0) as u8,
2262 );
2263 let swatch_rect = Rect::from_min_size(
2264 egui::pos2(x, y - 5.0 * scale),
2265 egui::vec2(swatch_w, 7.0 * scale),
2266 );
2267 match e.plot_type {
2268 crate::plots::figure::PlotType::Line
2269 | crate::plots::figure::PlotType::Line3
2270 | crate::plots::figure::PlotType::Contour
2271 | crate::plots::figure::PlotType::ReferenceLine => {
2272 let ymid = swatch_rect.center().y;
2273 ui.painter().line_segment(
2274 [
2275 Pos2::new(swatch_rect.min.x, ymid),
2276 Pos2::new(swatch_rect.max.x, ymid),
2277 ],
2278 Stroke::new(2.0, c),
2279 );
2280 }
2281 crate::plots::figure::PlotType::Scatter
2282 | crate::plots::figure::PlotType::Scatter3 => {
2283 let center = swatch_rect.center();
2284 ui.painter().circle_filled(center, 3.5, c);
2285 ui.painter()
2286 .circle_stroke(center, 3.5, Stroke::new(1.0, legend_stroke));
2287 }
2288 crate::plots::figure::PlotType::Bar
2289 | crate::plots::figure::PlotType::Area
2290 | crate::plots::figure::PlotType::Surface
2291 | crate::plots::figure::PlotType::Patch
2292 | crate::plots::figure::PlotType::Pie
2293 | crate::plots::figure::PlotType::ContourFill => {
2294 ui.painter().rect_filled(swatch_rect, 2.0, c);
2295 ui.painter()
2296 .rect_stroke(swatch_rect, 2.0, Stroke::new(1.0, legend_stroke));
2297 }
2298 crate::plots::figure::PlotType::ErrorBar
2299 | crate::plots::figure::PlotType::Stairs
2300 | crate::plots::figure::PlotType::Stem
2301 | crate::plots::figure::PlotType::Quiver => {
2302 let ymid = swatch_rect.center().y;
2303 ui.painter().line_segment(
2304 [
2305 Pos2::new(swatch_rect.min.x, ymid),
2306 Pos2::new(swatch_rect.max.x - 4.0, ymid),
2307 ],
2308 Stroke::new(1.5, c),
2309 );
2310 ui.painter().line_segment(
2311 [
2312 Pos2::new(swatch_rect.max.x - 4.0, ymid - 3.0),
2313 Pos2::new(swatch_rect.max.x, ymid),
2314 ],
2315 Stroke::new(1.0, c),
2316 );
2317 }
2318 }
2319 ui.painter().text(
2320 egui::pos2(x + text_x_gap, y),
2321 Align2::LEFT_CENTER,
2322 &e.label,
2323 FontId::proportional(11.0 * scale),
2324 legend_text,
2325 );
2326 y += row_h;
2327 }
2328 }
2329
2330 fn draw_x_label_in_rect(&self, ui: &mut egui::Ui, rect: Rect, label: &str, scale: f32) {
2331 let scale = scale.max(0.75);
2332 let text_color = self.theme_text_color();
2333 ui.painter().text(
2334 Pos2::new(rect.center().x, rect.max.y - rect.height() * 0.24),
2335 Align2::CENTER_CENTER,
2336 label,
2337 FontId::proportional(14.0 * scale),
2338 text_color,
2339 );
2340 }
2341
2342 fn draw_y_label_in_rect(&self, ui: &mut egui::Ui, rect: Rect, label: &str, scale: f32) {
2343 let scale = scale.max(0.75);
2344 let text_color = self.theme_text_color();
2345 let galley = ui.fonts(|fonts| {
2346 fonts.layout_no_wrap(
2347 label.to_owned(),
2348 FontId::proportional(13.0 * scale),
2349 text_color,
2350 )
2351 });
2352 let size = galley.size();
2353 let center = Pos2::new(rect.min.x + rect.width() * 0.32, rect.center().y);
2354 let pos = Pos2::new(center.x - size.y * 0.5, center.y + size.x * 0.5);
2355 let mut shape = egui::epaint::TextShape::new(pos, galley, text_color);
2356 shape.angle = -std::f32::consts::FRAC_PI_2;
2357 shape.override_text_color = Some(text_color);
2358 ui.painter().add(shape);
2359 }
2360
2361 fn project_world_to_screen(
2362 &self,
2363 plot_rect: Rect,
2364 camera: &crate::core::Camera,
2365 point: glam::Vec3,
2366 ) -> Option<Pos2> {
2367 let mut cam = camera.clone();
2368 let clip = cam.view_proj_matrix() * point.extend(1.0);
2369 if !clip.x.is_finite() || !clip.y.is_finite() || !clip.z.is_finite() || !clip.w.is_finite()
2370 {
2371 return None;
2372 }
2373 if clip.w.abs() < 1.0e-6 {
2374 return None;
2375 }
2376 let ndc = clip.truncate() / clip.w;
2377 if ndc.z < -1.1 || ndc.z > 1.1 {
2378 return None;
2379 }
2380 if clip.w <= 0.0
2381 && matches!(
2382 camera.projection,
2383 crate::core::camera::ProjectionType::Perspective { .. }
2384 )
2385 {
2386 return None;
2387 }
2388 let x = plot_rect.min.x + (ndc.x + 1.0) * 0.5 * plot_rect.width();
2389 let y = plot_rect.min.y + (1.0 - (ndc.y + 1.0) * 0.5) * plot_rect.height();
2390 Some(Pos2::new(x, y))
2391 }
2392
2393 fn draw_projected_world_texts(
2394 &self,
2395 ui: &mut egui::Ui,
2396 plot_rect: Rect,
2397 plot_renderer: &PlotRenderer,
2398 axes_index: usize,
2399 scale: f32,
2400 ) {
2401 let Some(camera) = plot_renderer
2402 .axes_camera(axes_index)
2403 .or_else(|| Some(plot_renderer.camera()))
2404 else {
2405 return;
2406 };
2407 let annotations = plot_renderer.world_text_annotations_for_axes(axes_index);
2408 let is_3d = Self::axes_is_3d(plot_renderer, axes_index);
2409 for (position, text, style) in annotations {
2410 if !style.visible || text.trim().is_empty() {
2411 continue;
2412 }
2413 let Some(screen) = self.project_world_to_screen(plot_rect, camera, position) else {
2414 continue;
2415 };
2416 let color = Self::style_color(&style, self.theme_text_color());
2417 let font_size = Self::style_font_size(&style, 14.0, scale);
2418 let offset = if is_3d {
2419 egui::vec2(0.0, -8.0 * scale.max(0.75))
2420 } else {
2421 egui::vec2(0.0, 6.0 * scale.max(0.75))
2422 };
2423 Self::paint_styled_text(
2424 ui.painter(),
2425 screen + offset,
2426 Align2::CENTER_CENTER,
2427 &text,
2428 font_size,
2429 color,
2430 Self::style_is_bold(&style),
2431 if is_3d { 120 } else { 90 },
2432 );
2433 }
2434 }
2435
2436 fn draw_pie_label(
2437 &self,
2438 ui: &mut egui::Ui,
2439 plot_rect: Rect,
2440 label: &str,
2441 pos: glam::Vec2,
2442 scale: f32,
2443 ) {
2444 let center = plot_rect.center();
2445 let radius = plot_rect.width().min(plot_rect.height()) * 0.4;
2446 let screen = Pos2::new(center.x + pos.x * radius, center.y - pos.y * radius);
2447 ui.painter().text(
2448 screen,
2449 Align2::CENTER_CENTER,
2450 label,
2451 FontId::proportional(12.0 * scale.max(0.75)),
2452 self.theme_text_color(),
2453 );
2454 }
2455
2456 pub fn plot_area(&self) -> Option<Rect> {
2458 self.plot_area
2459 }
2460
2461 pub fn axes_plot_rects(&self) -> &[Rect] {
2463 &self.axes_plot_rects
2464 }
2465
2466 pub fn toolbar_rect(&self) -> Option<Rect> {
2468 self.toolbar_rect
2469 }
2470
2471 pub fn sidebar_rect(&self) -> Option<Rect> {
2473 self.sidebar_rect
2474 }
2475
2476 pub fn take_toolbar_actions(&mut self) -> (bool, bool, bool, Option<bool>, Option<bool>) {
2477 let out = (
2478 self.want_save_png,
2479 self.want_save_svg,
2480 self.want_reset_view,
2481 self.want_toggle_grid.take(),
2482 self.want_toggle_legend.take(),
2483 );
2484 self.want_save_png = false;
2485 self.want_save_svg = false;
2486 self.want_reset_view = false;
2487 out
2488 }
2489
2490 fn render_dystr_modal(&mut self, ctx: &Context) -> bool {
2492 let mut consumed_input = false;
2493
2494 egui::Window::new("About Dystr")
2495 .anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
2496 .collapsible(false)
2497 .resizable(false)
2498 .default_width(400.0)
2499 .show(ctx, |ui| {
2500 consumed_input = true;
2501
2502 ui.vertical_centered(|ui| {
2503 ui.add_space(10.0);
2504
2505 let logo_size = egui::Vec2::splat(64.0);
2507 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::hover()).0;
2508
2509 ui.painter().rect_filled(
2510 logo_rect,
2511 8.0, Color32::from_rgb(60, 130, 200), );
2514
2515 ui.painter().text(
2516 logo_rect.center(),
2517 Align2::CENTER_CENTER,
2518 "D",
2519 FontId::proportional(40.0),
2520 Color32::WHITE,
2521 );
2522
2523 ui.add_space(15.0);
2524
2525 ui.heading("Welcome to RunMat");
2526 ui.add_space(10.0);
2527
2528 ui.label("RunMat is a high-performance MATLAB-compatible");
2529 ui.label("numerical computing platform, built as part of");
2530 ui.label("the Dystr computation ecosystem.");
2531
2532 ui.add_space(15.0);
2533
2534 ui.label("🚀 V8-inspired JIT compilation");
2535 ui.label("⚡ BLAS/LAPACK acceleration");
2536 ui.label("🎯 Full MATLAB compatibility");
2537 ui.label("🔬 Advanced plotting & visualization");
2538
2539 ui.add_space(20.0);
2540
2541 ui.horizontal(|ui| {
2542 if ui.button("Visit dystr.com").clicked() {
2543 if let Err(e) = webbrowser::open("https://dystr.com") {
2545 eprintln!("Failed to open browser: {e}");
2546 }
2547 }
2548
2549 if ui.button("Close").clicked() {
2550 self.show_dystr_modal = false;
2551 }
2552 });
2553
2554 ui.add_space(10.0);
2555 });
2556 });
2557
2558 consumed_input
2559 }
2560}
2561
2562fn truncate_label(label: &str, max_len: usize) -> String {
2563 if label.chars().count() <= max_len {
2564 return label.to_string();
2565 }
2566 let mut out = String::new();
2567 for (i, ch) in label.chars().enumerate() {
2568 if i >= max_len - 1 {
2569 break;
2570 }
2571 out.push(ch);
2572 }
2573 out.push('…');
2574 out
2575}