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 #[allow(clippy::too_many_arguments)]
350 fn layout_2d_panel(
351 &self,
352 outer: Rect,
353 plot_renderer: &PlotRenderer,
354 axes_index: usize,
355 title: Option<&str>,
356 x_label: Option<&str>,
357 y_label: Option<&str>,
358 scale: f32,
359 ) -> PanelLayout {
360 let scale = scale.max(0.75);
361 let has_title = Self::has_visible_text(title);
362 let has_x_label = Self::has_visible_text(x_label);
363 let has_y_label = Self::has_visible_text(y_label);
364 let outer_w = outer.width().max(1.0);
365 let outer_h = outer.height().max(1.0);
366 let title_gap = if has_title { 4.0 * scale } else { 1.5 * scale };
367 let x_gap = 4.0 * scale;
368 let right_pad = 3.0 * scale;
369
370 let mut title_h = if has_title {
371 (28.0 * scale).min(outer_h * 0.16)
372 } else {
373 0.0
374 };
375 let mut x_h = ((24.0 + if has_x_label { 14.0 } else { 0.0 }) * scale).min(outer_h * 0.28);
376 let y_band_estimate =
377 self.estimate_y_axis_band_width(plot_renderer, axes_index, has_y_label, scale);
378 let mut y_w = y_band_estimate.min(outer_w * 0.30);
379
380 let min_plot_w = (outer_w * 0.56).max(44.0 * scale).min(outer_w);
381 let min_plot_h = (outer_h * 0.54).max(44.0 * scale).min(outer_h);
382
383 if outer_w - y_w < min_plot_w {
384 y_w = (outer_w - min_plot_w).max(0.0);
385 }
386
387 let available_h = outer_h - title_h - title_gap - x_h - x_gap;
388 if available_h < min_plot_h {
389 let deficit = min_plot_h - available_h;
390 let reducible = title_h + x_h;
391 if reducible > 0.0 {
392 let keep = ((reducible - deficit).max(0.0)) / reducible;
393 title_h *= keep;
394 x_h *= keep;
395 }
396 }
397
398 let plot_rect = Rect::from_min_max(
399 egui::pos2(outer.min.x + y_w, outer.min.y + title_h + title_gap),
400 egui::pos2(
401 (outer.max.x - right_pad).max(outer.min.x + y_w + 1.0),
402 outer.max.y - x_h - x_gap,
403 ),
404 );
405 let frame_rect = plot_rect;
406 let title_rect = Rect::from_min_max(
407 egui::pos2(outer.min.x, outer.min.y),
408 egui::pos2(outer.max.x, plot_rect.min.y),
409 );
410 let x_label_rect = Rect::from_min_max(
411 egui::pos2(outer.min.x, plot_rect.max.y),
412 egui::pos2(outer.max.x, outer.max.y),
413 );
414 let y_label_rect = Rect::from_min_max(
415 egui::pos2(outer.min.x, plot_rect.min.y),
416 egui::pos2(plot_rect.min.x, plot_rect.max.y),
417 );
418 PanelLayout {
419 plot_rect,
420 frame_rect,
421 title_rect,
422 x_label_rect,
423 y_label_rect,
424 }
425 }
426
427 fn layout_3d_panel(&self, outer: Rect, title: Option<&str>, scale: f32) -> PanelLayout {
428 let scale = scale.max(0.75);
429 let title_h = if Self::has_visible_text(title) {
430 (28.0 * scale).min(outer.height().max(1.0) * 0.16)
431 } else {
432 0.0
433 };
434 let plot_rect =
435 Rect::from_min_max(egui::pos2(outer.min.x, outer.min.y + title_h), outer.max);
436 let title_rect = Rect::from_min_max(outer.min, egui::pos2(outer.max.x, plot_rect.min.y));
437 PanelLayout {
438 plot_rect,
439 frame_rect: plot_rect,
440 title_rect,
441 x_label_rect: plot_rect,
442 y_label_rect: plot_rect,
443 }
444 }
445
446 fn axes_is_3d(plot_renderer: &PlotRenderer, axes_index: usize) -> bool {
447 let cam = plot_renderer
448 .axes_camera(axes_index)
449 .unwrap_or_else(|| plot_renderer.camera());
450 matches!(
451 cam.projection,
452 crate::core::camera::ProjectionType::Perspective { .. }
453 )
454 }
455
456 fn panel_layout_for_axes(
457 &self,
458 outer: Rect,
459 plot_renderer: &PlotRenderer,
460 axes_index: usize,
461 scale: f32,
462 ) -> PanelLayout {
463 if Self::axes_is_3d(plot_renderer, axes_index) {
464 self.layout_3d_panel(
465 outer,
466 plot_renderer
467 .overlay_title_for_axes(axes_index)
468 .map(|s| s.as_str()),
469 scale,
470 )
471 } else {
472 self.layout_2d_panel(
473 outer,
474 plot_renderer,
475 axes_index,
476 plot_renderer
477 .overlay_title_for_axes(axes_index)
478 .map(|s| s.as_str()),
479 plot_renderer
480 .overlay_x_label_for_axes(axes_index)
481 .map(|s| s.as_str()),
482 plot_renderer
483 .overlay_y_label_for_axes(axes_index)
484 .map(|s| s.as_str()),
485 scale,
486 )
487 }
488 }
489
490 pub fn compute_subplot_plot_rects(
491 &self,
492 outer: Rect,
493 plot_renderer: &PlotRenderer,
494 font_scale: f32,
495 ) -> Vec<Rect> {
496 let plot_area = Self::outer_plot_area_for_axes(outer, plot_renderer);
497 let (rows, cols) = plot_renderer.figure_axes_grid();
498 if rows * cols <= 1 {
499 vec![
500 self.panel_layout_for_axes(plot_area, plot_renderer, 0, font_scale)
501 .plot_rect,
502 ]
503 } else {
504 let rects = self.compute_subplot_rects(
505 plot_area,
506 rows,
507 cols,
508 Self::SUBPLOT_GAP_POINTS,
509 Self::SUBPLOT_GAP_POINTS,
510 );
511 rects
512 .into_iter()
513 .enumerate()
514 .map(|(axes_index, rect)| {
515 self.panel_layout_for_axes(rect, plot_renderer, axes_index, font_scale)
516 .plot_rect
517 })
518 .collect()
519 }
520 }
521
522 pub fn snap_rect_to_pixels(rect: Rect, pixels_per_point: f32) -> Rect {
523 let ppp = pixels_per_point.max(0.5);
524 let min_x = (rect.min.x * ppp).round() / ppp;
525 let min_y = (rect.min.y * ppp).round() / ppp;
526 let width = (rect.width() * ppp).round().max(1.0) / ppp;
527 let height = (rect.height() * ppp).round().max(1.0) / ppp;
528 Rect::from_min_size(egui::pos2(min_x, min_y), egui::vec2(width, height))
529 }
530
531 fn snap_coord(value: f32, pixels_per_point: f32) -> f32 {
532 let ppp = pixels_per_point.max(0.5);
533 (value * ppp).round() / ppp
534 }
535
536 fn border_centerline_edges(
537 plot_rect: Rect,
538 pixels_per_point: f32,
539 stroke_width: f32,
540 ) -> (f32, f32, f32, f32) {
541 let offset = stroke_width * 0.5;
542 let left = Self::snap_coord(plot_rect.min.x - offset, pixels_per_point);
543 let right = Self::snap_coord(plot_rect.max.x + offset, pixels_per_point);
544 let top = Self::snap_coord(plot_rect.min.y - offset, pixels_per_point);
545 let bottom = Self::snap_coord(plot_rect.max.y + offset, pixels_per_point);
546 (left, right, top, bottom)
547 }
548
549 fn draw_2d_border(&self, ui: &mut egui::Ui, plot_rect: Rect) {
550 let stroke = Stroke::new(1.5, self.theme_axis_color());
551 let ppp = ui.ctx().pixels_per_point();
552 let (left, right, top, bottom) =
553 Self::border_centerline_edges(plot_rect, ppp, stroke.width);
554 ui.painter()
555 .line_segment([Pos2::new(left, top), Pos2::new(right, top)], stroke);
556 ui.painter()
557 .line_segment([Pos2::new(left, bottom), Pos2::new(right, bottom)], stroke);
558 ui.painter()
559 .line_segment([Pos2::new(left, top), Pos2::new(left, bottom)], stroke);
560 ui.painter()
561 .line_segment([Pos2::new(right, top), Pos2::new(right, bottom)], stroke);
562 }
563
564 fn draw_plot_box_mask(&self, ui: &mut egui::Ui, plot_rect: Rect) {
565 let mask = 2.0;
566 let bg = self.theme_background_color();
567 let top = Rect::from_min_max(
568 Pos2::new(plot_rect.min.x - mask, plot_rect.min.y - mask),
569 Pos2::new(plot_rect.max.x + mask, plot_rect.min.y),
570 );
571 let bottom = Rect::from_min_max(
572 Pos2::new(plot_rect.min.x - mask, plot_rect.max.y),
573 Pos2::new(plot_rect.max.x + mask, plot_rect.max.y + mask),
574 );
575 let left = Rect::from_min_max(
576 Pos2::new(plot_rect.min.x - mask, plot_rect.min.y - mask),
577 Pos2::new(plot_rect.min.x, plot_rect.max.y + mask),
578 );
579 let right = Rect::from_min_max(
580 Pos2::new(plot_rect.max.x, plot_rect.min.y - mask),
581 Pos2::new(plot_rect.max.x + mask, plot_rect.max.y + mask),
582 );
583 ui.painter().rect_filled(top, 0.0, bg);
584 ui.painter().rect_filled(bottom, 0.0, bg);
585 ui.painter().rect_filled(left, 0.0, bg);
586 ui.painter().rect_filled(right, 0.0, bg);
587 }
588
589 pub fn compute_subplot_plot_rects_snapped(
590 &self,
591 outer: Rect,
592 plot_renderer: &PlotRenderer,
593 font_scale: f32,
594 pixels_per_point: f32,
595 ) -> Vec<Rect> {
596 self.compute_subplot_plot_rects(outer, plot_renderer, font_scale)
597 .into_iter()
598 .map(|rect| Self::snap_rect_to_pixels(rect, pixels_per_point))
599 .collect()
600 }
601
602 pub fn outer_plot_area_for_axes(available_rect: Rect, plot_renderer: &PlotRenderer) -> Rect {
603 let (rows, cols) = plot_renderer.figure_axes_grid();
604 let single_axes_is_3d = rows * cols <= 1 && Self::axes_is_3d(plot_renderer, 0);
605 if single_axes_is_3d || rows * cols > 1 {
606 available_rect
607 } else {
608 available_rect.shrink2(egui::vec2(8.0, 8.0))
609 }
610 }
611
612 fn theme_text_color(&self) -> Color32 {
613 let text = self.theme.build_theme().get_text_color();
614 Color32::from_rgba_premultiplied(
615 (text.x.clamp(0.0, 1.0) * 255.0) as u8,
616 (text.y.clamp(0.0, 1.0) * 255.0) as u8,
617 (text.z.clamp(0.0, 1.0) * 255.0) as u8,
618 (text.w.clamp(0.0, 1.0) * 255.0) as u8,
619 )
620 }
621
622 fn theme_axis_color(&self) -> Color32 {
623 let axis = self.theme.build_theme().get_axis_color();
624 Color32::from_rgba_premultiplied(
625 (axis.x.clamp(0.0, 1.0) * 255.0) as u8,
626 (axis.y.clamp(0.0, 1.0) * 255.0) as u8,
627 (axis.z.clamp(0.0, 1.0) * 255.0) as u8,
628 (axis.w.clamp(0.0, 1.0) * 255.0) as u8,
629 )
630 }
631
632 fn theme_background_color(&self) -> Color32 {
633 let bg = self.theme.build_theme().get_background_color();
634 Color32::from_rgba_premultiplied(
635 (bg.x.clamp(0.0, 1.0) * 255.0) as u8,
636 (bg.y.clamp(0.0, 1.0) * 255.0) as u8,
637 (bg.z.clamp(0.0, 1.0) * 255.0) as u8,
638 (bg.w.clamp(0.0, 1.0) * 255.0) as u8,
639 )
640 }
641
642 fn themed_grid_colors(&self) -> (Color32, Color32) {
643 let grid = self.theme.build_theme().get_grid_color();
644 let major = Color32::from_rgba_premultiplied(
645 (grid.x.clamp(0.0, 1.0) * 255.0) as u8,
646 (grid.y.clamp(0.0, 1.0) * 255.0) as u8,
647 (grid.z.clamp(0.0, 1.0) * 255.0) as u8,
648 ((grid.w.clamp(0.15, 0.55)) * 255.0) as u8,
649 );
650 let minor = Color32::from_rgba_premultiplied(
651 (grid.x.clamp(0.0, 1.0) * 255.0) as u8,
652 (grid.y.clamp(0.0, 1.0) * 255.0) as u8,
653 (grid.z.clamp(0.0, 1.0) * 255.0) as u8,
654 ((grid.w * 0.6).clamp(0.10, 0.34) * 255.0) as u8,
655 );
656 (major, minor)
657 }
658
659 pub fn apply_theme(&self, ctx: &Context) {
661 match self.theme.variant {
662 ThemeVariant::ModernDark => {
663 ModernDarkTheme::default().apply_to_egui(ctx);
664 }
665 ThemeVariant::ClassicLight => {
666 ctx.set_visuals(egui::Visuals::light());
667 }
668 ThemeVariant::HighContrast => {
669 let mut visuals = egui::Visuals::dark();
670 visuals.extreme_bg_color = egui::Color32::BLACK;
671 visuals.widgets.noninteractive.bg_fill = egui::Color32::BLACK;
672 visuals.widgets.noninteractive.fg_stroke.color = egui::Color32::WHITE;
673 ctx.set_visuals(visuals);
674 }
675 ThemeVariant::Custom => {
676 let mut visuals = egui::Visuals::light();
677 let bg = self.theme.build_theme().get_background_color();
678 if bg.x + bg.y + bg.z < 1.5 {
679 visuals = egui::Visuals::dark();
680 }
681 ctx.set_visuals(visuals);
682 }
683 }
684
685 let mut visuals = ctx.style().visuals.clone();
687 visuals.window_fill = Color32::TRANSPARENT;
688 visuals.panel_fill = Color32::TRANSPARENT;
689 visuals.extreme_bg_color = Color32::TRANSPARENT;
690 visuals.faint_bg_color = Color32::TRANSPARENT;
691 visuals.widgets.noninteractive.bg_fill = Color32::TRANSPARENT;
692 visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
693 visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
694 visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
695 visuals.widgets.open.bg_fill = Color32::TRANSPARENT;
696 ctx.set_visuals(visuals);
697 }
698
699 pub fn render(
701 &mut self,
702 ctx: &Context,
703 plot_renderer: &PlotRenderer,
704 config: &OverlayConfig,
705 metrics: OverlayMetrics,
706 ) -> FrameInfo {
707 let mut consumed_input = false;
708 let mut plot_area = None;
709
710 if config.show_sidebar {
712 consumed_input |= self.render_sidebar(ctx, plot_renderer, config, &metrics);
713 }
714
715 let central_response = egui::CentralPanel::default()
717 .frame(egui::Frame::none()) .show(ctx, |ui| {
719 if config.show_toolbar {
721 egui::TopBottomPanel::top("plot_toolbar")
722 .frame(egui::Frame::none())
723 .show_inside(ui, |ui| {
724 let padded = ui.max_rect().shrink2(egui::vec2(12.0, 6.0));
725 self.toolbar_rect = Some(padded);
726 ui.allocate_ui_at_rect(padded, |ui| {
727 ui.with_layout(
728 egui::Layout::right_to_left(egui::Align::Center),
729 |ui| {
730 ui.spacing_mut().item_spacing = egui::vec2(8.0, 4.0);
731 ui.spacing_mut().button_padding = egui::vec2(8.0, 6.0);
732 if ui.button("Save PNG").clicked() {
733 self.want_save_png = true;
734 }
735 if ui.button("Save SVG").clicked() {
736 self.want_save_svg = true;
737 }
738 if ui.button("Reset View").clicked() {
739 self.want_reset_view = true;
740 }
741 let mut grid = plot_renderer.overlay_show_grid();
742 if ui.toggle_value(&mut grid, "Grid").changed() {
743 self.want_toggle_grid = Some(grid);
744 }
745 let mut legend = plot_renderer.overlay_show_legend();
746 if ui.toggle_value(&mut legend, "Legend").changed() {
747 self.want_toggle_legend = Some(legend);
748 }
749 },
750 );
751 });
752 });
753 } else {
754 self.toolbar_rect = None;
755 }
756 plot_area = Some(self.render_plot_area(ui, plot_renderer, config));
757 });
758
759 consumed_input |= central_response.response.hovered();
760
761 if self.show_dystr_modal {
763 consumed_input |= self.render_dystr_modal(ctx);
764 }
765
766 self.plot_area = plot_area;
768
769 FrameInfo {
770 plot_area,
771 consumed_input,
772 metrics,
773 }
774 }
775
776 fn render_sidebar(
778 &mut self,
779 ctx: &Context,
780 plot_renderer: &PlotRenderer,
781 config: &OverlayConfig,
782 metrics: &OverlayMetrics,
783 ) -> bool {
784 let mut consumed_input = false;
785
786 let sidebar_response = egui::SidePanel::left("plot_controls")
787 .resizable(true)
788 .default_width(config.sidebar_width)
789 .min_width(200.0)
790 .show(ctx, |ui| {
791 ui.style_mut().visuals.widgets.noninteractive.bg_fill = Color32::from_gray(25);
792 ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::from_gray(35);
793 ui.style_mut().visuals.widgets.hovered.bg_fill = Color32::from_gray(45);
794
795 ui.horizontal(|ui| {
797 let logo_size = egui::Vec2::splat(32.0);
799 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::click()).0;
800
801 ui.painter().rect_filled(
803 logo_rect,
804 4.0, Color32::from_rgb(100, 100, 100),
806 );
807
808 ui.painter().text(
810 logo_rect.center(),
811 Align2::CENTER_CENTER,
812 "D",
813 FontId::proportional(20.0),
814 Color32::WHITE,
815 );
816
817 ui.vertical(|ui| {
818 ui.heading("RunMat");
819 ui.horizontal(|ui| {
820 ui.small("a community project by ");
821 if ui.small_button("dystr.com").clicked() {
822 self.show_dystr_modal = true;
823 }
824 });
825 });
826 });
827 ui.separator();
828 ui.label("GC Stats: [not available]");
829
830 ui.collapsing("📷 Camera", |ui| {
832 let camera = plot_renderer.camera();
833 ui.label(format!(
834 "Position: {:.2}, {:.2}, {:.2}",
835 camera.position.x, camera.position.y, camera.position.z
836 ));
837 ui.label(format!(
838 "Target: {:.2}, {:.2}, {:.2}",
839 camera.target.x, camera.target.y, camera.target.z
840 ));
841
842 if let Some(vb) = plot_renderer.view_bounds() {
843 ui.label(format!("View X: {:.2} to {:.2}", vb.0, vb.1));
844 ui.label(format!("View Y: {:.2} to {:.2}", vb.2, vb.3));
845 }
846 if let Some(db) = plot_renderer.data_bounds() {
847 ui.label(format!("Data X: {:.2} to {:.2}", db.0, db.1));
848 ui.label(format!("Data Y: {:.2} to {:.2}", db.2, db.3));
849 }
850 });
851
852 ui.collapsing("🎬 Scene", |ui| {
854 let stats = plot_renderer.scene_statistics();
855 ui.label(format!("Nodes: {}", stats.total_nodes));
856 ui.label(format!("Visible: {}", stats.visible_nodes));
857 ui.label(format!("Vertices: {}", stats.total_vertices));
858 ui.label(format!("Triangles: {}", stats.total_triangles));
859 });
860
861 ui.collapsing("⚡ Performance", |ui| {
863 ui.label(format!("FPS: {:.1}", metrics.fps));
864 ui.label(format!("Render: {:.2}ms", metrics.render_time_ms));
865 ui.label(format!("Vertices: {}", metrics.vertex_count));
866 ui.label(format!("Triangles: {}", metrics.triangle_count));
867 });
868
869 ui.collapsing("🎨 Theme", |ui| {
871 let label = match self.theme.variant {
872 ThemeVariant::ModernDark => "Modern Dark",
873 ThemeVariant::ClassicLight => "Classic Light",
874 ThemeVariant::HighContrast => "High Contrast",
875 ThemeVariant::Custom => "Custom",
876 };
877 ui.label(format!("{label} (Active)"));
878 ui.checkbox(&mut self.show_debug, "Show Debug Info");
879 });
880
881 ui.separator();
882
883 ui.collapsing("🔧 Controls", |ui| {
885 ui.label("🖱️ Orbit: MMB drag (or RMB drag)");
886 ui.label("🖱️ Pan: Shift + MMB drag (or Shift + RMB drag)");
887 ui.label("🖱️ Zoom: Scroll wheel (zooms to cursor)");
888 ui.label("🖱️ Alt + LMB/MMB/RMB: Orbit/Pan/Zoom");
889 ui.label("📱 Touch: Pinch to zoom");
890 });
891 });
892
893 consumed_input |= sidebar_response.response.hovered();
894 self.sidebar_rect = Some(sidebar_response.response.rect);
895 consumed_input
896 }
897
898 fn render_plot_area(
900 &mut self,
901 ui: &mut egui::Ui,
902 plot_renderer: &PlotRenderer,
903 config: &OverlayConfig,
904 ) -> Rect {
905 let available_rect = ui.available_rect_before_wrap();
906 let mut rendered_axes_rects: Vec<Rect> = Vec::new();
907
908 let (rows, cols) = plot_renderer.figure_axes_grid();
909 let plot_rect = Self::outer_plot_area_for_axes(available_rect, plot_renderer);
910
911 let plot_area_rect = plot_rect;
914
915 if rows * cols > 1 {
916 let rects = self.compute_subplot_rects(
917 plot_area_rect,
918 rows,
919 cols,
920 Self::SUBPLOT_GAP_POINTS,
921 Self::SUBPLOT_GAP_POINTS,
922 );
923 for (i, cell_rect) in rects.iter().enumerate() {
924 let cam = plot_renderer
925 .axes_camera(i)
926 .unwrap_or_else(|| plot_renderer.camera());
927 let panel_layout =
928 self.panel_layout_for_axes(*cell_rect, plot_renderer, i, config.font_scale);
929 let r =
930 Self::snap_rect_to_pixels(panel_layout.plot_rect, ui.ctx().pixels_per_point());
931 let frame_rect =
932 Self::snap_rect_to_pixels(panel_layout.frame_rect, ui.ctx().pixels_per_point());
933 rendered_axes_rects.push(r);
934 log::debug!(
935 target: "runmat_plot.axes_layout",
936 "computed axes panel layout axes_index={} rows={} cols={} is_3d={} cell=({}, {})..({}, {}) frame=({}, {})..({}, {}) content=({}, {})..({}, {})",
937 i,
938 rows,
939 cols,
940 Self::axes_is_3d(plot_renderer, i),
941 cell_rect.min.x,
942 cell_rect.min.y,
943 cell_rect.max.x,
944 cell_rect.max.y,
945 frame_rect.min.x,
946 frame_rect.min.y,
947 frame_rect.max.x,
948 frame_rect.max.y,
949 r.min.x,
950 r.min.y,
951 r.max.x,
952 r.max.y
953 );
954 if matches!(
955 cam.projection,
956 crate::core::camera::ProjectionType::Perspective { .. }
957 ) {
958 if config.show_title {
959 if let Some(title) = plot_renderer.overlay_title_for_axes(i) {
960 self.draw_title_in_rect(
961 ui,
962 panel_layout.title_rect,
963 title,
964 config.font_scale,
965 );
966 }
967 }
968 self.draw_3d_orientation_gizmo(ui, r, plot_renderer, i, config.font_scale);
969 self.draw_3d_origin_axis_ticks(ui, r, plot_renderer, i, config.font_scale);
970 self.draw_projected_world_texts(ui, r, plot_renderer, i, config.font_scale);
971 for (label, pos) in plot_renderer.pie_labels_for_axes(i) {
972 self.draw_pie_label(ui, r, &label, pos, config.font_scale);
973 }
974 if plot_renderer.overlay_show_legend_for_axes(i) {
975 let entries = plot_renderer.overlay_legend_entries_for_axes(i);
976 self.draw_legend(ui, r, &entries, config.font_scale);
977 }
978 continue;
979 }
980 if plot_renderer.overlay_show_box_for_axes(i) {
982 self.draw_plot_box_mask(ui, r);
983 self.draw_2d_border(ui, frame_rect);
984 }
985
986 if config.show_grid && plot_renderer.overlay_show_grid_for_axes(i) {
988 let b = plot_renderer.view_bounds_for_axes(i);
989 self.draw_grid(ui, r, plot_renderer, b, Some(i));
990 }
991 if config.show_axes {
993 let b = plot_renderer.view_bounds_for_axes(i);
994 self.draw_axes(ui, r, plot_renderer, config, b, Some(i));
995 }
996
997 if config.show_title {
998 if let Some(title) = plot_renderer.overlay_title_for_axes(i) {
999 self.draw_title_in_rect(
1000 ui,
1001 panel_layout.title_rect,
1002 title,
1003 config.font_scale,
1004 );
1005 }
1006 }
1007 if !matches!(
1008 cam.projection,
1009 crate::core::camera::ProjectionType::Perspective { .. }
1010 ) {
1011 if let Some(x_label) = plot_renderer.overlay_x_label_for_axes(i) {
1012 self.draw_x_label_in_rect(
1013 ui,
1014 panel_layout.x_label_rect,
1015 x_label,
1016 config.font_scale,
1017 );
1018 }
1019 }
1020 if !matches!(
1021 cam.projection,
1022 crate::core::camera::ProjectionType::Perspective { .. }
1023 ) {
1024 if let Some(y_label) = plot_renderer.overlay_y_label_for_axes(i) {
1025 self.draw_y_label_in_rect(
1026 ui,
1027 panel_layout.y_label_rect,
1028 y_label,
1029 config.font_scale,
1030 );
1031 }
1032 }
1033 self.draw_projected_world_texts(ui, r, plot_renderer, i, config.font_scale);
1034 for (label, pos) in plot_renderer.pie_labels_for_axes(i) {
1035 self.draw_pie_label(ui, r, &label, pos, config.font_scale);
1036 }
1037 if plot_renderer.overlay_show_legend_for_axes(i) {
1038 let entries = plot_renderer.overlay_legend_entries_for_axes(i);
1039 self.draw_legend(ui, r, &entries, config.font_scale);
1040 }
1041 }
1042 } else {
1043 let cam = plot_renderer.camera();
1044 let panel_layout =
1045 self.panel_layout_for_axes(plot_area_rect, plot_renderer, 0, config.font_scale);
1046 let centered_plot_rect =
1047 Self::snap_rect_to_pixels(panel_layout.plot_rect, ui.ctx().pixels_per_point());
1048 let centered_frame_rect =
1049 Self::snap_rect_to_pixels(panel_layout.frame_rect, ui.ctx().pixels_per_point());
1050 rendered_axes_rects.push(centered_plot_rect);
1051 log::debug!(
1052 target: "runmat_plot.axes_layout",
1053 "computed axes panel layout axes_index=0 rows={} cols={} is_3d={} cell=({}, {})..({}, {}) frame=({}, {})..({}, {}) content=({}, {})..({}, {})",
1054 rows,
1055 cols,
1056 Self::axes_is_3d(plot_renderer, 0),
1057 plot_area_rect.min.x,
1058 plot_area_rect.min.y,
1059 plot_area_rect.max.x,
1060 plot_area_rect.max.y,
1061 centered_frame_rect.min.x,
1062 centered_frame_rect.min.y,
1063 centered_frame_rect.max.x,
1064 centered_frame_rect.max.y,
1065 centered_plot_rect.min.x,
1066 centered_plot_rect.min.y,
1067 centered_plot_rect.max.x,
1068 centered_plot_rect.max.y
1069 );
1070 if config.show_title {
1071 if let Some(title) = plot_renderer
1072 .overlay_title_for_axes(0)
1073 .or(config.title.as_ref())
1074 {
1075 self.draw_title_in_rect(ui, panel_layout.title_rect, title, config.font_scale);
1076 }
1077 }
1078 if matches!(
1079 cam.projection,
1080 crate::core::camera::ProjectionType::Perspective { .. }
1081 ) {
1082 self.draw_3d_orientation_gizmo(
1083 ui,
1084 centered_plot_rect,
1085 plot_renderer,
1086 0,
1087 config.font_scale,
1088 );
1089 self.draw_3d_origin_axis_ticks(
1090 ui,
1091 centered_plot_rect,
1092 plot_renderer,
1093 0,
1094 config.font_scale,
1095 );
1096 self.draw_projected_world_texts(
1097 ui,
1098 centered_plot_rect,
1099 plot_renderer,
1100 0,
1101 config.font_scale,
1102 );
1103 } else {
1104 if plot_renderer.overlay_show_box() {
1106 self.draw_plot_box_mask(ui, centered_plot_rect);
1107 self.draw_2d_border(ui, centered_frame_rect);
1108 }
1109 if config.show_grid {
1111 self.draw_grid(ui, centered_plot_rect, plot_renderer, None, None);
1112 }
1113
1114 if config.show_axes {
1116 self.draw_axes(ui, centered_plot_rect, plot_renderer, config, None, None);
1117 if let Some((x_min, x_max, y_min, y_max)) = plot_renderer
1119 .view_bounds()
1120 .or_else(|| plot_renderer.data_bounds())
1121 {
1122 let axis_color = self.theme_axis_color();
1123 let zero_stroke = Stroke::new(1.5, axis_color);
1124 if y_min < 0.0 && y_max > 0.0 {
1125 let y_screen = centered_plot_rect.max.y
1126 - ((0.0 - y_min) / (y_max - y_min)) as f32
1127 * centered_plot_rect.height();
1128 ui.painter().line_segment(
1129 [
1130 Pos2::new(centered_plot_rect.min.x, y_screen),
1131 Pos2::new(centered_plot_rect.max.x, y_screen),
1132 ],
1133 zero_stroke,
1134 );
1135 }
1136 if x_min < 0.0 && x_max > 0.0 {
1137 let x_screen = centered_plot_rect.min.x
1138 + ((0.0 - x_min) / (x_max - x_min)) as f32
1139 * centered_plot_rect.width();
1140 ui.painter().line_segment(
1141 [
1142 Pos2::new(x_screen, centered_plot_rect.min.y),
1143 Pos2::new(x_screen, centered_plot_rect.max.y),
1144 ],
1145 zero_stroke,
1146 );
1147 }
1148 }
1149 }
1150 if let Some(x_label) = plot_renderer
1151 .overlay_x_label_for_axes(0)
1152 .or(config.x_label.as_ref())
1153 {
1154 self.draw_x_label_in_rect(
1155 ui,
1156 panel_layout.x_label_rect,
1157 x_label,
1158 config.font_scale,
1159 );
1160 }
1161 if let Some(y_label) = plot_renderer
1162 .overlay_y_label_for_axes(0)
1163 .or(config.y_label.as_ref())
1164 {
1165 self.draw_y_label_in_rect(
1166 ui,
1167 panel_layout.y_label_rect,
1168 y_label,
1169 config.font_scale,
1170 );
1171 }
1172 self.draw_projected_world_texts(
1173 ui,
1174 centered_plot_rect,
1175 plot_renderer,
1176 0,
1177 config.font_scale,
1178 );
1179 }
1180 }
1181 let centered_plot_rect = if rows * cols <= 1 {
1182 self.panel_layout_for_axes(plot_area_rect, plot_renderer, 0, config.font_scale)
1183 .plot_rect
1184 } else {
1185 plot_area_rect
1186 };
1187 for (label, pos) in if rows * cols <= 1 {
1188 plot_renderer.active_axes_pie_labels()
1189 } else {
1190 Vec::new()
1191 } {
1192 self.draw_pie_label(ui, centered_plot_rect, &label, pos, config.font_scale);
1193 }
1194
1195 if rows * cols <= 1 && plot_renderer.overlay_show_legend() {
1197 self.draw_legend(
1198 ui,
1199 centered_plot_rect,
1200 plot_renderer.overlay_legend_entries(),
1201 config.font_scale,
1202 );
1203 }
1204
1205 if plot_renderer.overlay_colorbar_enabled() {
1207 let bar_width = 12.0;
1209 let pad = 8.0;
1210 let bar_rect = Rect::from_min_max(
1211 egui::pos2(
1212 centered_plot_rect.max.x - bar_width - pad,
1213 centered_plot_rect.min.y + pad,
1214 ),
1215 egui::pos2(
1216 centered_plot_rect.max.x - pad,
1217 centered_plot_rect.max.y - pad,
1218 ),
1219 );
1220 let steps = 64;
1222 for i in 0..steps {
1223 let t0 = i as f32 / steps as f32;
1224 let t1 = (i + 1) as f32 / steps as f32;
1225 let y0 = bar_rect.min.y + (1.0 - t0) * bar_rect.height();
1226 let y1 = bar_rect.min.y + (1.0 - t1) * bar_rect.height();
1227 let cmap = plot_renderer.overlay_colormap();
1228 let c = cmap.map_value(t0);
1229 let col = Color32::from_rgb(
1230 (c.x * 255.0) as u8,
1231 (c.y * 255.0) as u8,
1232 (c.z * 255.0) as u8,
1233 );
1234 ui.painter().rect_filled(
1235 Rect::from_min_max(
1236 egui::pos2(bar_rect.min.x, y1),
1237 egui::pos2(bar_rect.max.x, y0),
1238 ),
1239 0.0,
1240 col,
1241 );
1242 }
1243 let bg = plot_renderer.theme.build_theme().get_background_color();
1244 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
1245 let border = if bg_luma > 0.62 {
1246 Color32::from_gray(60)
1247 } else {
1248 Color32::WHITE
1249 };
1250 ui.painter()
1251 .rect_stroke(bar_rect, 0.0, Stroke::new(1.0, border));
1252 }
1253
1254 self.axes_plot_rects = rendered_axes_rects;
1255 centered_plot_rect
1256 }
1257
1258 pub fn compute_subplot_rects(
1260 &self,
1261 outer: Rect,
1262 rows: usize,
1263 cols: usize,
1264 hgap: f32,
1265 vgap: f32,
1266 ) -> Vec<Rect> {
1267 let rows = rows.max(1) as f32;
1268 let cols = cols.max(1) as f32;
1269 let total_hgap = hgap * (cols - 1.0);
1270 let total_vgap = vgap * (rows - 1.0);
1271 let cell_w = ((outer.width()).max(1.0) - total_hgap).max(1.0) / cols;
1272 let cell_h = ((outer.height()).max(1.0) - total_vgap).max(1.0) / rows;
1273 let mut rects = Vec::new();
1274 for r in 0..rows as i32 {
1275 for c in 0..cols as i32 {
1276 let x = outer.min.x + c as f32 * (cell_w + hgap);
1277 let y = outer.min.y + r as f32 * (cell_h + vgap);
1278 rects.push(Rect::from_min_size(
1279 egui::pos2(x, y),
1280 egui::vec2(cell_w, cell_h),
1281 ));
1282 }
1283 }
1284 rects
1285 }
1286
1287 fn draw_grid(
1289 &self,
1290 ui: &mut egui::Ui,
1291 plot_rect: Rect,
1292 plot_renderer: &PlotRenderer,
1293 view_bounds_override: Option<(f64, f64, f64, f64)>,
1294 axes_index: Option<usize>,
1295 ) {
1296 let ppp = ui.ctx().pixels_per_point();
1297 let edge_eps = 0.51 / ppp.max(0.5);
1298 if let Some(data_bounds) = view_bounds_override
1299 .or_else(|| plot_renderer.view_bounds())
1300 .or_else(|| plot_renderer.data_bounds())
1301 {
1302 let (grid_color_major, _grid_color_minor) = self.themed_grid_colors();
1303
1304 let (x_min, x_max, y_min, y_max) = data_bounds;
1305 let x_range = x_max - x_min;
1306 let y_range = y_max - y_min;
1307
1308 let x_log = axes_index
1310 .map(|idx| plot_renderer.overlay_x_log_for_axes(idx))
1311 .unwrap_or_else(|| plot_renderer.overlay_x_log());
1312 let y_log = axes_index
1313 .map(|idx| plot_renderer.overlay_y_log_for_axes(idx))
1314 .unwrap_or_else(|| plot_renderer.overlay_y_log());
1315
1316 let x_ticks = if x_log {
1317 Vec::new()
1318 } else {
1319 plot_utils::generate_major_ticks(x_min, x_max)
1320 };
1321 let y_ticks = if y_log {
1322 Vec::new()
1323 } else {
1324 plot_utils::generate_major_ticks(y_min, y_max)
1325 };
1326
1327 if x_log {
1329 let start_decade = x_min.log10().floor() as i32;
1331 let end_decade = x_max.log10().ceil() as i32;
1332 for d in start_decade..=end_decade {
1333 let decade = 10f64.powi(d);
1334 for m in [1.0, 2.0, 5.0].iter() {
1335 let x_val = decade * m;
1336 if x_val < x_min || x_val > x_max {
1337 continue;
1338 }
1339 let x_screen = plot_rect.min.x
1340 + ((x_val.log10() - x_min.log10()) / (x_max.log10() - x_min.log10()))
1341 as f32
1342 * plot_rect.width();
1343 let x_screen = Self::snap_coord(x_screen, ppp);
1344 if (x_screen - plot_rect.min.x).abs() <= edge_eps
1345 || (x_screen - plot_rect.max.x).abs() <= edge_eps
1346 {
1347 continue;
1348 }
1349 ui.painter().line_segment(
1350 [
1351 Pos2::new(x_screen, plot_rect.min.y),
1352 Pos2::new(x_screen, plot_rect.max.y),
1353 ],
1354 Stroke::new(0.8, grid_color_major),
1355 );
1356 }
1357 }
1358 } else {
1359 for x_val in x_ticks {
1360 let x_screen =
1361 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1362 let x_screen = Self::snap_coord(x_screen, ppp);
1363 if (x_screen - plot_rect.min.x).abs() <= edge_eps
1364 || (x_screen - plot_rect.max.x).abs() <= edge_eps
1365 {
1366 continue;
1367 }
1368 ui.painter().line_segment(
1369 [
1370 Pos2::new(x_screen, plot_rect.min.y),
1371 Pos2::new(x_screen, plot_rect.max.y),
1372 ],
1373 Stroke::new(0.8, grid_color_major),
1374 );
1375 }
1376 }
1377
1378 if y_log {
1380 let start_decade = y_min.log10().floor() as i32;
1381 let end_decade = y_max.log10().ceil() as i32;
1382 for d in start_decade..=end_decade {
1383 let decade = 10f64.powi(d);
1384 for m in [1.0, 2.0, 5.0].iter() {
1385 let y_val = decade * m;
1386 if y_val < y_min || y_val > y_max {
1387 continue;
1388 }
1389 let y_screen = plot_rect.max.y
1390 - ((y_val.log10() - y_min.log10()) / (y_max.log10() - y_min.log10()))
1391 as f32
1392 * plot_rect.height();
1393 let y_screen = Self::snap_coord(y_screen, ppp);
1394 if (y_screen - plot_rect.min.y).abs() <= edge_eps
1395 || (y_screen - plot_rect.max.y).abs() <= edge_eps
1396 {
1397 continue;
1398 }
1399 ui.painter().line_segment(
1400 [
1401 Pos2::new(plot_rect.min.x, y_screen),
1402 Pos2::new(plot_rect.max.x, y_screen),
1403 ],
1404 Stroke::new(0.8, grid_color_major),
1405 );
1406 }
1407 }
1408 } else {
1409 for y_val in y_ticks {
1410 let y_screen =
1411 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1412 let y_screen = Self::snap_coord(y_screen, ppp);
1413 if (y_screen - plot_rect.min.y).abs() <= edge_eps
1414 || (y_screen - plot_rect.max.y).abs() <= edge_eps
1415 {
1416 continue;
1417 }
1418 ui.painter().line_segment(
1419 [
1420 Pos2::new(plot_rect.min.x, y_screen),
1421 Pos2::new(plot_rect.max.x, y_screen),
1422 ],
1423 Stroke::new(0.8, grid_color_major),
1424 );
1425 }
1426 }
1427 }
1428 }
1429
1430 fn draw_axes(
1432 &self,
1433 ui: &mut egui::Ui,
1434 plot_rect: Rect,
1435 plot_renderer: &PlotRenderer,
1436 config: &OverlayConfig,
1437 view_bounds_override: Option<(f64, f64, f64, f64)>,
1438 axes_index: Option<usize>,
1439 ) {
1440 let ppp = ui.ctx().pixels_per_point();
1441 if let Some(data_bounds) = view_bounds_override
1442 .or_else(|| plot_renderer.view_bounds())
1443 .or_else(|| plot_renderer.data_bounds())
1444 {
1445 let (x_min, x_max, y_min, y_max) = data_bounds;
1446 let x_range = x_max - x_min;
1447 let y_range = y_max - y_min;
1448 let scale = config.font_scale.max(0.75);
1449 let tick_length = 6.0 * scale;
1450 let label_offset = 15.0 * scale;
1451 let tick_font = FontId::proportional(10.0 * scale);
1452 let axis_color = self.theme_axis_color();
1453 let label_color = self.theme_text_color();
1454 let border_left = plot_rect.min.x;
1455 let border_bottom = plot_rect.max.y;
1456
1457 let x_log = axes_index
1458 .map(|idx| plot_renderer.overlay_x_log_for_axes(idx))
1459 .unwrap_or_else(|| plot_renderer.overlay_x_log());
1460 let y_log = axes_index
1461 .map(|idx| plot_renderer.overlay_y_log_for_axes(idx))
1462 .unwrap_or_else(|| plot_renderer.overlay_y_log());
1463
1464 let (mut cat_x, mut cat_y) = (false, false);
1466 let mut custom_hist_x = false;
1467 if let Some((true, edges)) =
1468 axes_index.and_then(|idx| plot_renderer.overlay_histogram_edges_for_axes(idx))
1469 {
1470 custom_hist_x = true;
1471 self.draw_histogram_axis_ticks(
1472 ui,
1473 plot_rect,
1474 ppp,
1475 axis_color,
1476 label_color,
1477 tick_length,
1478 label_offset,
1479 tick_font.clone(),
1480 border_bottom,
1481 x_min,
1482 x_max,
1483 &edges,
1484 );
1485 }
1486 if let Some((is_x, labels)) = axes_index
1487 .and_then(|idx| plot_renderer.overlay_categorical_labels_for_axes(idx))
1488 .or_else(|| {
1489 plot_renderer
1490 .overlay_categorical_labels()
1491 .map(|(is_x, labels)| (is_x, labels.clone()))
1492 })
1493 {
1494 if is_x {
1495 cat_x = true;
1496 } else {
1497 cat_y = true;
1498 }
1499 if is_x {
1500 let stride = Self::label_stride(&labels, plot_rect.width(), tick_font.size);
1501 for (label_idx, label) in labels.iter().enumerate() {
1503 if label_idx != 0
1504 && label_idx != labels.len() - 1
1505 && label_idx % stride != 0
1506 {
1507 continue;
1508 }
1509 let x_val = (label_idx + 1) as f64;
1510 if x_val < x_min || x_val > x_max {
1511 continue;
1512 }
1513 let x_screen = plot_rect.min.x
1514 + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1515 let x_screen = Self::snap_coord(x_screen, ppp);
1516 ui.painter().line_segment(
1518 [
1519 Pos2::new(x_screen, border_bottom),
1520 Pos2::new(x_screen, border_bottom + tick_length),
1521 ],
1522 Stroke::new(1.0, axis_color),
1523 );
1524 let text = truncate_label(label, 14);
1526 ui.painter().text(
1527 Pos2::new(x_screen, border_bottom + label_offset),
1528 Align2::CENTER_CENTER,
1529 text,
1530 tick_font.clone(),
1531 label_color,
1532 );
1533 }
1534 } else {
1535 let stride = Self::label_stride(&labels, plot_rect.height(), tick_font.size);
1536 for (label_idx, label) in labels.iter().enumerate() {
1538 if label_idx != 0
1539 && label_idx != labels.len() - 1
1540 && label_idx % stride != 0
1541 {
1542 continue;
1543 }
1544 let y_val = (label_idx + 1) as f64;
1545 if y_val < y_min || y_val > y_max {
1546 continue;
1547 }
1548 let y_screen = plot_rect.max.y
1549 - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1550 let y_screen = Self::snap_coord(y_screen, ppp);
1551 ui.painter().line_segment(
1553 [
1554 Pos2::new(border_left - tick_length, y_screen),
1555 Pos2::new(border_left, y_screen),
1556 ],
1557 Stroke::new(1.0, axis_color),
1558 );
1559 let text = truncate_label(label, 14);
1561 ui.painter().text(
1562 Pos2::new(border_left - label_offset, y_screen),
1563 Align2::CENTER_CENTER,
1564 text,
1565 tick_font.clone(),
1566 label_color,
1567 );
1568 }
1569 }
1570 }
1571
1572 if x_log {
1574 let start_decade = x_min.log10().floor() as i32;
1575 let end_decade = x_max.log10().ceil() as i32;
1576 for d in start_decade..=end_decade {
1577 let decade = 10f64.powi(d);
1578 let x_screen = plot_rect.min.x
1579 + ((decade.log10() - x_min.log10()) / (x_max.log10() - x_min.log10()))
1580 as f32
1581 * plot_rect.width();
1582 let x_screen = Self::snap_coord(x_screen, ppp);
1583 ui.painter().line_segment(
1585 [
1586 Pos2::new(x_screen, border_bottom),
1587 Pos2::new(x_screen, border_bottom + tick_length),
1588 ],
1589 Stroke::new(1.0, axis_color),
1590 );
1591 ui.painter().text(
1593 Pos2::new(x_screen, border_bottom + label_offset),
1594 Align2::CENTER_CENTER,
1595 format!("10^{}", d),
1596 tick_font.clone(),
1597 label_color,
1598 );
1599 }
1600 } else if !cat_x && !custom_hist_x {
1601 for x_val in plot_utils::generate_major_ticks(x_min, x_max) {
1602 let x_screen =
1603 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
1604 let x_screen = Self::snap_coord(x_screen, ppp);
1605 ui.painter().line_segment(
1606 [
1607 Pos2::new(x_screen, border_bottom),
1608 Pos2::new(x_screen, border_bottom + tick_length),
1609 ],
1610 Stroke::new(1.0, axis_color),
1611 );
1612 ui.painter().text(
1613 Pos2::new(x_screen, border_bottom + label_offset),
1614 Align2::CENTER_CENTER,
1615 plot_utils::format_tick_label(x_val),
1616 tick_font.clone(),
1617 label_color,
1618 );
1619 }
1620 }
1621
1622 if y_log {
1624 let start_decade = y_min.log10().floor() as i32;
1625 let end_decade = y_max.log10().ceil() as i32;
1626 for d in start_decade..=end_decade {
1627 let decade = 10f64.powi(d);
1628 let y_screen = plot_rect.max.y
1629 - ((decade.log10() - y_min.log10()) / (y_max.log10() - y_min.log10()))
1630 as f32
1631 * plot_rect.height();
1632 let y_screen = Self::snap_coord(y_screen, ppp);
1633 ui.painter().line_segment(
1634 [
1635 Pos2::new(border_left - tick_length, y_screen),
1636 Pos2::new(border_left, y_screen),
1637 ],
1638 Stroke::new(1.0, axis_color),
1639 );
1640 ui.painter().text(
1641 Pos2::new(border_left - label_offset, y_screen),
1642 Align2::CENTER_CENTER,
1643 format!("10^{}", d),
1644 tick_font.clone(),
1645 label_color,
1646 );
1647 }
1648 } else if !cat_y {
1649 for y_val in plot_utils::generate_major_ticks(y_min, y_max) {
1650 let y_screen =
1651 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
1652 let y_screen = Self::snap_coord(y_screen, ppp);
1653 ui.painter().line_segment(
1654 [
1655 Pos2::new(border_left - tick_length, y_screen),
1656 Pos2::new(border_left, y_screen),
1657 ],
1658 Stroke::new(1.0, axis_color),
1659 );
1660 ui.painter().text(
1661 Pos2::new(border_left - label_offset, y_screen),
1662 Align2::CENTER_CENTER,
1663 plot_utils::format_tick_label(y_val),
1664 tick_font.clone(),
1665 label_color,
1666 );
1667 }
1668 }
1669 }
1670 }
1671
1672 fn draw_3d_orientation_gizmo(
1675 &self,
1676 ui: &mut egui::Ui,
1677 plot_rect: Rect,
1678 plot_renderer: &PlotRenderer,
1679 axes_index: usize,
1680 font_scale: f32,
1681 ) {
1682 let cam_ref = plot_renderer
1683 .axes_camera(axes_index)
1684 .unwrap_or_else(|| plot_renderer.camera());
1685 let cam = cam_ref.clone();
1686
1687 let forward = (cam.target - cam.position).normalize_or_zero();
1688 if forward.length_squared() < 1e-9 {
1689 return;
1690 }
1691 let world_up = cam.up.normalize_or_zero();
1692 let right = forward.cross(world_up).normalize_or_zero();
1693 if right.length_squared() < 1e-9 {
1694 return;
1695 }
1696 let up = right.cross(forward).normalize_or_zero();
1697 if up.length_squared() < 1e-9 {
1698 return;
1699 }
1700
1701 let scale = font_scale.max(0.75);
1702 let base = plot_rect.width().min(plot_rect.height()).max(1.0);
1703 let gizmo_size = (base * 0.16).clamp(44.0, 110.0) * scale;
1704 let pad = (30.0 * scale).round();
1705 let origin = Pos2::new(plot_rect.min.x + pad, plot_rect.max.y - pad);
1706
1707 struct AxisItem {
1708 label: &'static str,
1709 dir_world: Vec3,
1710 color: Color32,
1711 z_sort: f32,
1712 }
1713
1714 let mut axes = [
1715 AxisItem {
1716 label: "X",
1717 dir_world: Vec3::X,
1718 color: Color32::from_rgb(235, 80, 80),
1719 z_sort: 0.0,
1720 },
1721 AxisItem {
1722 label: "Y",
1723 dir_world: Vec3::Y,
1724 color: Color32::from_rgb(90, 220, 120),
1725 z_sort: 0.0,
1726 },
1727 AxisItem {
1728 label: "Z",
1729 dir_world: Vec3::Z,
1730 color: Color32::from_rgb(90, 160, 255),
1731 z_sort: 0.0,
1732 },
1733 ];
1734
1735 for a in axes.iter_mut() {
1738 let x = a.dir_world.dot(right);
1739 let y = a.dir_world.dot(up);
1740 let z = a.dir_world.dot(-forward);
1741 a.z_sort = z;
1742 a.dir_world = Vec3::new(x, y, z);
1743 }
1744 axes.sort_by(|a, b| {
1745 a.z_sort
1746 .partial_cmp(&b.z_sort)
1747 .unwrap_or(std::cmp::Ordering::Equal)
1748 });
1749
1750 let painter = ui.painter();
1751
1752 painter.circle_filled(origin, 2.0 * scale, Color32::from_gray(210));
1753
1754 let axis_len = gizmo_size * 0.65;
1755 let head_len = (8.0 * scale).min(axis_len * 0.35);
1756 let head_w = 5.0 * scale;
1757 let font = FontId::proportional(11.0 * scale);
1758
1759 for a in axes.iter() {
1760 let dir2 = egui::Vec2::new(a.dir_world.x, -a.dir_world.y);
1761 let mag = dir2.length();
1762 if !mag.is_finite() || mag < 1e-4 {
1763 continue;
1764 }
1765 let d = dir2 / mag;
1766
1767 let end = origin + d * axis_len;
1768 let stroke = Stroke::new(2.0 * scale, a.color);
1769 painter.line_segment([origin, end], stroke);
1770
1771 let base = end - d * head_len;
1773 let perp = egui::Vec2::new(-d.y, d.x);
1774 painter.line_segment([end, base + perp * head_w], stroke);
1775 painter.line_segment([end, base - perp * head_w], stroke);
1776
1777 let label_pos = end + d * (10.0 * scale);
1779 painter.text(
1780 label_pos,
1781 Align2::CENTER_CENTER,
1782 a.label,
1783 font.clone(),
1784 a.color,
1785 );
1786 }
1787 }
1788
1789 fn draw_3d_origin_axis_ticks(
1792 &self,
1793 ui: &mut egui::Ui,
1794 plot_rect: Rect,
1795 plot_renderer: &PlotRenderer,
1796 axes_index: usize,
1797 font_scale: f32,
1798 ) {
1799 let cam_ref = plot_renderer
1800 .axes_camera(axes_index)
1801 .unwrap_or_else(|| plot_renderer.camera());
1802 let mut cam = cam_ref.clone();
1803 let w = plot_rect.width().max(1.0);
1804 let h = plot_rect.height().max(1.0);
1805 cam.update_aspect_ratio(w / h);
1806 let view_proj = cam.view_proj_matrix();
1807
1808 let project = |p: Vec3| -> Option<Pos2> {
1809 let clip: Vec4 = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
1810 if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
1811 return None;
1812 }
1813 let ndc = clip.truncate() / clip.w;
1814 if !(ndc.x.is_finite() && ndc.y.is_finite()) {
1815 return None;
1816 }
1817 let sx = plot_rect.min.x + ((ndc.x + 1.0) * 0.5) * plot_rect.width();
1818 let sy = plot_rect.min.y + ((1.0 - ndc.y) * 0.5) * plot_rect.height();
1819 Some(Pos2::new(sx, sy))
1820 };
1821
1822 let nice_step = |raw: f64| -> f64 {
1823 if !raw.is_finite() || raw <= 0.0 {
1824 return 1.0;
1825 }
1826 let pow10 = 10.0_f64.powf(raw.log10().floor());
1827 let norm = raw / pow10;
1828 let mult = if norm <= 1.0 {
1829 1.0
1830 } else if norm <= 2.0 {
1831 2.0
1832 } else if norm <= 5.0 {
1833 5.0
1834 } else {
1835 10.0
1836 };
1837 mult * pow10
1838 };
1839
1840 let origin = Vec3::ZERO;
1843 let px_per_world = match (project(origin), project(origin + Vec3::X)) {
1844 (Some(a), Some(b)) => ((b.x - a.x).hypot(b.y - a.y) as f64).max(1e-3),
1845 _ => 1.0,
1846 };
1847 let desired_major_px = 120.0_f64;
1848 let major_step = nice_step((desired_major_px / px_per_world).max(1e-6));
1849 if !(major_step.is_finite() && major_step > 0.0) {
1850 return;
1851 }
1852 let axis_len = (major_step as f32 * 5.0).max(0.5);
1853
1854 let scale = font_scale.max(0.75);
1855 let font = FontId::proportional(10.0 * scale);
1856 let painter = ui.painter();
1857 let col_x = Color32::from_rgb(235, 80, 80);
1858 let col_y = Color32::from_rgb(90, 220, 120);
1859 let col_z = Color32::from_rgb(90, 160, 255);
1860 let panel_center = plot_rect.center();
1861
1862 let outward_offset = |pos: Pos2, base: f32| {
1863 let dir = pos - panel_center;
1864 let len = dir.length().max(1.0);
1865 (dir / len) * base
1866 };
1867
1868 if let Some(pos) = project(Vec3::X * axis_len * 1.10) {
1869 if let Some(label) = plot_renderer.overlay_x_label_for_axes(axes_index) {
1870 let style = plot_renderer
1871 .overlay_x_label_style_for_axes(axes_index)
1872 .cloned()
1873 .unwrap_or_default();
1874 let offset = outward_offset(pos, 12.0 * scale) + egui::vec2(4.0 * scale, 0.0);
1875 Self::paint_styled_text(
1876 painter,
1877 pos + offset,
1878 Align2::LEFT_CENTER,
1879 label,
1880 Self::style_font_size(&style, 12.0, scale),
1881 Self::style_color(&style, col_x),
1882 Self::style_is_bold(&style),
1883 110,
1884 );
1885 }
1886 }
1887 if let Some(pos) = project(Vec3::Y * axis_len * 1.10) {
1888 if let Some(label) = plot_renderer.overlay_y_label_for_axes(axes_index) {
1889 let style = plot_renderer
1890 .overlay_y_label_style_for_axes(axes_index)
1891 .cloned()
1892 .unwrap_or_default();
1893 let offset =
1894 outward_offset(pos, 12.0 * scale) + egui::vec2(2.0 * scale, -2.0 * scale);
1895 Self::paint_styled_text(
1896 painter,
1897 pos + offset,
1898 Align2::LEFT_CENTER,
1899 label,
1900 Self::style_font_size(&style, 12.0, scale),
1901 Self::style_color(&style, col_y),
1902 Self::style_is_bold(&style),
1903 110,
1904 );
1905 }
1906 }
1907 if let Some(pos) = project(Vec3::Z * axis_len * 1.10) {
1908 if let Some(label) = plot_renderer.overlay_z_label_for_axes(axes_index) {
1909 let style = plot_renderer
1910 .overlay_z_label_style_for_axes(axes_index)
1911 .cloned()
1912 .unwrap_or_default();
1913 let offset = outward_offset(pos, 12.0 * scale) + egui::vec2(0.0, -4.0 * scale);
1914 Self::paint_styled_text(
1915 painter,
1916 pos + offset,
1917 Align2::LEFT_BOTTOM,
1918 label,
1919 Self::style_font_size(&style, 12.0, scale),
1920 Self::style_color(&style, col_z),
1921 Self::style_is_bold(&style),
1922 110,
1923 );
1924 }
1925 }
1926
1927 let draw_axis = |axis: Vec3, color: Color32| {
1928 for i in 1..=6 {
1929 let t = (i as f32) * (major_step as f32);
1930 if t >= axis_len * 0.999 {
1931 break;
1932 }
1933 let p = origin + axis * t;
1934 let Some(pos) = project(p) else { continue };
1935 let offset =
1937 outward_offset(pos, 7.0 * scale) + egui::Vec2::new(3.0 * scale, -3.0 * scale);
1938 painter.text(
1939 pos + offset + egui::vec2(1.0, 1.0),
1940 Align2::LEFT_CENTER,
1941 plot_utils::format_tick_label((i as f64) * major_step),
1942 font.clone(),
1943 Color32::from_rgba_premultiplied(0, 0, 0, 90),
1944 );
1945 painter.text(
1946 pos + offset,
1947 Align2::LEFT_CENTER,
1948 plot_utils::format_tick_label((i as f64) * major_step),
1949 font.clone(),
1950 color,
1951 );
1952 }
1953 };
1954 draw_axis(Vec3::X, col_x);
1955 draw_axis(Vec3::Y, col_y);
1956 draw_axis(Vec3::Z, col_z);
1957 }
1958
1959 fn draw_title_in_rect(&self, ui: &mut egui::Ui, rect: Rect, title: &str, scale: f32) {
1960 let scale = scale.max(0.75);
1961 let text_color = self.theme_text_color();
1962 ui.painter().text(
1963 rect.center(),
1964 Align2::CENTER_CENTER,
1965 title,
1966 FontId::proportional(16.0 * scale),
1967 text_color,
1968 );
1969 }
1970
1971 fn draw_legend(
1972 &self,
1973 ui: &mut egui::Ui,
1974 plot_rect: Rect,
1975 entries: &[crate::plots::figure::LegendEntry],
1976 scale: f32,
1977 ) {
1978 if entries.is_empty() {
1979 return;
1980 }
1981 let scale = scale.max(0.75);
1982 let theme = self.theme.build_theme();
1983 let bg = theme.get_background_color();
1984 let text = theme.get_text_color();
1985 let legend_text = Color32::from_rgb(
1986 (text.x.clamp(0.0, 1.0) * 255.0) as u8,
1987 (text.y.clamp(0.0, 1.0) * 255.0) as u8,
1988 (text.z.clamp(0.0, 1.0) * 255.0) as u8,
1989 );
1990 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
1991 let legend_bg = if bg_luma > 0.62 {
1992 Color32::from_rgba_premultiplied(255, 255, 255, 170)
1993 } else {
1994 Color32::from_rgba_premultiplied(0, 0, 0, 128)
1995 };
1996 let legend_stroke = if bg_luma > 0.62 {
1997 Color32::from_rgb(55, 55, 55)
1998 } else {
1999 Color32::BLACK
2000 };
2001 let pad = 8.0 * scale;
2002 let row_h = (16.0 * scale).clamp(13.0, 18.0);
2003 let swatch_w = 14.0 * scale;
2004 let text_x_gap = 18.0 * scale;
2005 let legend_w = (plot_rect.width() * 0.30).clamp(92.0, 132.0);
2006 let x = plot_rect.max.x - legend_w - pad;
2007 let mut y = plot_rect.min.y + pad + 4.0 * scale;
2008 let legend_rect = Rect::from_min_max(
2009 egui::pos2(x - pad, plot_rect.min.y + pad),
2010 egui::pos2(x + legend_w, y + entries.len() as f32 * row_h + pad),
2011 );
2012 ui.painter().rect_filled(legend_rect, 4.0, legend_bg);
2013 y += 10.0 * scale;
2014 for e in entries {
2015 let c = Color32::from_rgb(
2016 (e.color.x * 255.0) as u8,
2017 (e.color.y * 255.0) as u8,
2018 (e.color.z * 255.0) as u8,
2019 );
2020 let swatch_rect = Rect::from_min_size(
2021 egui::pos2(x, y - 5.0 * scale),
2022 egui::vec2(swatch_w, 7.0 * scale),
2023 );
2024 match e.plot_type {
2025 crate::plots::figure::PlotType::Line
2026 | crate::plots::figure::PlotType::Line3
2027 | crate::plots::figure::PlotType::Contour => {
2028 let ymid = swatch_rect.center().y;
2029 ui.painter().line_segment(
2030 [
2031 Pos2::new(swatch_rect.min.x, ymid),
2032 Pos2::new(swatch_rect.max.x, ymid),
2033 ],
2034 Stroke::new(2.0, c),
2035 );
2036 }
2037 crate::plots::figure::PlotType::Scatter
2038 | crate::plots::figure::PlotType::Scatter3 => {
2039 let center = swatch_rect.center();
2040 ui.painter().circle_filled(center, 3.5, c);
2041 ui.painter()
2042 .circle_stroke(center, 3.5, Stroke::new(1.0, legend_stroke));
2043 }
2044 crate::plots::figure::PlotType::Bar
2045 | crate::plots::figure::PlotType::Area
2046 | crate::plots::figure::PlotType::Surface
2047 | crate::plots::figure::PlotType::Pie
2048 | crate::plots::figure::PlotType::ContourFill => {
2049 ui.painter().rect_filled(swatch_rect, 2.0, c);
2050 ui.painter()
2051 .rect_stroke(swatch_rect, 2.0, Stroke::new(1.0, legend_stroke));
2052 }
2053 crate::plots::figure::PlotType::ErrorBar
2054 | crate::plots::figure::PlotType::Stairs
2055 | crate::plots::figure::PlotType::Stem
2056 | crate::plots::figure::PlotType::Quiver => {
2057 let ymid = swatch_rect.center().y;
2058 ui.painter().line_segment(
2059 [
2060 Pos2::new(swatch_rect.min.x, ymid),
2061 Pos2::new(swatch_rect.max.x - 4.0, ymid),
2062 ],
2063 Stroke::new(1.5, c),
2064 );
2065 ui.painter().line_segment(
2066 [
2067 Pos2::new(swatch_rect.max.x - 4.0, ymid - 3.0),
2068 Pos2::new(swatch_rect.max.x, ymid),
2069 ],
2070 Stroke::new(1.0, c),
2071 );
2072 }
2073 }
2074 ui.painter().text(
2075 egui::pos2(x + text_x_gap, y),
2076 Align2::LEFT_CENTER,
2077 &e.label,
2078 FontId::proportional(11.0 * scale),
2079 legend_text,
2080 );
2081 y += row_h;
2082 }
2083 }
2084
2085 fn draw_x_label_in_rect(&self, ui: &mut egui::Ui, rect: Rect, label: &str, scale: f32) {
2086 let scale = scale.max(0.75);
2087 let text_color = self.theme_text_color();
2088 ui.painter().text(
2089 Pos2::new(rect.center().x, rect.max.y - rect.height() * 0.24),
2090 Align2::CENTER_CENTER,
2091 label,
2092 FontId::proportional(14.0 * scale),
2093 text_color,
2094 );
2095 }
2096
2097 fn draw_y_label_in_rect(&self, ui: &mut egui::Ui, rect: Rect, label: &str, scale: f32) {
2098 let scale = scale.max(0.75);
2099 let text_color = self.theme_text_color();
2100 let galley = ui.fonts(|fonts| {
2101 fonts.layout_no_wrap(
2102 label.to_owned(),
2103 FontId::proportional(13.0 * scale),
2104 text_color,
2105 )
2106 });
2107 let size = galley.size();
2108 let center = Pos2::new(rect.min.x + rect.width() * 0.32, rect.center().y);
2109 let pos = Pos2::new(center.x - size.y * 0.5, center.y + size.x * 0.5);
2110 let mut shape = egui::epaint::TextShape::new(pos, galley, text_color);
2111 shape.angle = -std::f32::consts::FRAC_PI_2;
2112 shape.override_text_color = Some(text_color);
2113 ui.painter().add(shape);
2114 }
2115
2116 fn project_world_to_screen(
2117 &self,
2118 plot_rect: Rect,
2119 camera: &crate::core::Camera,
2120 point: glam::Vec3,
2121 ) -> Option<Pos2> {
2122 let mut cam = camera.clone();
2123 let clip = cam.view_proj_matrix() * point.extend(1.0);
2124 if !clip.x.is_finite() || !clip.y.is_finite() || !clip.z.is_finite() || !clip.w.is_finite()
2125 {
2126 return None;
2127 }
2128 if clip.w.abs() < 1.0e-6 {
2129 return None;
2130 }
2131 let ndc = clip.truncate() / clip.w;
2132 if ndc.z < -1.1 || ndc.z > 1.1 {
2133 return None;
2134 }
2135 if clip.w <= 0.0
2136 && matches!(
2137 camera.projection,
2138 crate::core::camera::ProjectionType::Perspective { .. }
2139 )
2140 {
2141 return None;
2142 }
2143 let x = plot_rect.min.x + (ndc.x + 1.0) * 0.5 * plot_rect.width();
2144 let y = plot_rect.min.y + (1.0 - (ndc.y + 1.0) * 0.5) * plot_rect.height();
2145 Some(Pos2::new(x, y))
2146 }
2147
2148 fn draw_projected_world_texts(
2149 &self,
2150 ui: &mut egui::Ui,
2151 plot_rect: Rect,
2152 plot_renderer: &PlotRenderer,
2153 axes_index: usize,
2154 scale: f32,
2155 ) {
2156 let Some(camera) = plot_renderer
2157 .axes_camera(axes_index)
2158 .or_else(|| Some(plot_renderer.camera()))
2159 else {
2160 return;
2161 };
2162 let annotations = plot_renderer.world_text_annotations_for_axes(axes_index);
2163 let is_3d = Self::axes_is_3d(plot_renderer, axes_index);
2164 for (position, text, style) in annotations {
2165 if !style.visible || text.trim().is_empty() {
2166 continue;
2167 }
2168 let Some(screen) = self.project_world_to_screen(plot_rect, camera, position) else {
2169 continue;
2170 };
2171 let color = Self::style_color(&style, self.theme_text_color());
2172 let font_size = Self::style_font_size(&style, 14.0, scale);
2173 let offset = if is_3d {
2174 egui::vec2(0.0, -8.0 * scale.max(0.75))
2175 } else {
2176 egui::vec2(0.0, 6.0 * scale.max(0.75))
2177 };
2178 Self::paint_styled_text(
2179 ui.painter(),
2180 screen + offset,
2181 Align2::CENTER_CENTER,
2182 &text,
2183 font_size,
2184 color,
2185 Self::style_is_bold(&style),
2186 if is_3d { 120 } else { 90 },
2187 );
2188 }
2189 }
2190
2191 fn draw_pie_label(
2192 &self,
2193 ui: &mut egui::Ui,
2194 plot_rect: Rect,
2195 label: &str,
2196 pos: glam::Vec2,
2197 scale: f32,
2198 ) {
2199 let center = plot_rect.center();
2200 let radius = plot_rect.width().min(plot_rect.height()) * 0.4;
2201 let screen = Pos2::new(center.x + pos.x * radius, center.y - pos.y * radius);
2202 ui.painter().text(
2203 screen,
2204 Align2::CENTER_CENTER,
2205 label,
2206 FontId::proportional(12.0 * scale.max(0.75)),
2207 self.theme_text_color(),
2208 );
2209 }
2210
2211 pub fn plot_area(&self) -> Option<Rect> {
2213 self.plot_area
2214 }
2215
2216 pub fn axes_plot_rects(&self) -> &[Rect] {
2218 &self.axes_plot_rects
2219 }
2220
2221 pub fn toolbar_rect(&self) -> Option<Rect> {
2223 self.toolbar_rect
2224 }
2225
2226 pub fn sidebar_rect(&self) -> Option<Rect> {
2228 self.sidebar_rect
2229 }
2230
2231 pub fn take_toolbar_actions(&mut self) -> (bool, bool, bool, Option<bool>, Option<bool>) {
2232 let out = (
2233 self.want_save_png,
2234 self.want_save_svg,
2235 self.want_reset_view,
2236 self.want_toggle_grid.take(),
2237 self.want_toggle_legend.take(),
2238 );
2239 self.want_save_png = false;
2240 self.want_save_svg = false;
2241 self.want_reset_view = false;
2242 out
2243 }
2244
2245 fn render_dystr_modal(&mut self, ctx: &Context) -> bool {
2247 let mut consumed_input = false;
2248
2249 egui::Window::new("About Dystr")
2250 .anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
2251 .collapsible(false)
2252 .resizable(false)
2253 .default_width(400.0)
2254 .show(ctx, |ui| {
2255 consumed_input = true;
2256
2257 ui.vertical_centered(|ui| {
2258 ui.add_space(10.0);
2259
2260 let logo_size = egui::Vec2::splat(64.0);
2262 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::hover()).0;
2263
2264 ui.painter().rect_filled(
2265 logo_rect,
2266 8.0, Color32::from_rgb(60, 130, 200), );
2269
2270 ui.painter().text(
2271 logo_rect.center(),
2272 Align2::CENTER_CENTER,
2273 "D",
2274 FontId::proportional(40.0),
2275 Color32::WHITE,
2276 );
2277
2278 ui.add_space(15.0);
2279
2280 ui.heading("Welcome to RunMat");
2281 ui.add_space(10.0);
2282
2283 ui.label("RunMat is a high-performance MATLAB-compatible");
2284 ui.label("numerical computing platform, built as part of");
2285 ui.label("the Dystr computation ecosystem.");
2286
2287 ui.add_space(15.0);
2288
2289 ui.label("🚀 V8-inspired JIT compilation");
2290 ui.label("⚡ BLAS/LAPACK acceleration");
2291 ui.label("🎯 Full MATLAB compatibility");
2292 ui.label("🔬 Advanced plotting & visualization");
2293
2294 ui.add_space(20.0);
2295
2296 ui.horizontal(|ui| {
2297 if ui.button("Visit dystr.com").clicked() {
2298 if let Err(e) = webbrowser::open("https://dystr.com") {
2300 eprintln!("Failed to open browser: {e}");
2301 }
2302 }
2303
2304 if ui.button("Close").clicked() {
2305 self.show_dystr_modal = false;
2306 }
2307 });
2308
2309 ui.add_space(10.0);
2310 });
2311 });
2312
2313 consumed_input
2314 }
2315}
2316
2317fn truncate_label(label: &str, max_len: usize) -> String {
2318 if label.chars().count() <= max_len {
2319 return label.to_string();
2320 }
2321 let mut out = String::new();
2322 for (i, ch) in label.chars().enumerate() {
2323 if i >= max_len - 1 {
2324 break;
2325 }
2326 out.push(ch);
2327 }
2328 out.push('…');
2329 out
2330}