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