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