1use std::f64::consts::TAU;
2
3use egui::layers::ShapeIdx;
4use egui::{Color32, Pos2, Rect, Shape, Stroke};
5use glam::{DMat4, DVec3};
6
7use crate::math::world_to_screen;
8
9const STEPS_PER_RAD: f64 = 20.0;
10
11pub struct Painter3d {
12 painter: egui::Painter,
13 mvp: DMat4,
14 viewport: Rect,
15}
16
17impl Painter3d {
18 pub fn new(painter: egui::Painter, mvp: DMat4, viewport: Rect) -> Self {
19 Self {
20 painter: painter.with_clip_rect(viewport),
21 mvp,
22 viewport,
23 }
24 }
25
26 fn arc_points(&self, radius: f64, start_angle: f64, end_angle: f64) -> Vec<Pos2> {
27 let angle = f64::clamp(end_angle - start_angle, -TAU, TAU);
28
29 let step_count = steps(angle);
30 let mut points = Vec::with_capacity(step_count);
31
32 let step_size = angle / (step_count - 1) as f64;
33
34 for step in (0..step_count).map(|i| step_size * i as f64) {
35 let x = f64::cos(start_angle + step) * radius;
36 let z = f64::sin(start_angle + step) * radius;
37
38 points.push(DVec3::new(x, 0.0, z));
39 }
40
41 points
42 .into_iter()
43 .filter_map(|point| self.vec3_to_pos2(point))
44 .collect::<Vec<_>>()
45 }
46
47 pub fn arc(
48 &self,
49 radius: f64,
50 start_angle: f64,
51 end_angle: f64,
52 stroke: impl Into<Stroke>,
53 ) -> ShapeIdx {
54 let mut points = self.arc_points(radius, start_angle, end_angle);
55
56 let closed = points
57 .first()
58 .zip(points.last())
59 .filter(|(first, last)| first.distance(**last) < 1e-2)
60 .is_some();
61
62 if closed {
63 points.pop();
64 self.painter.add(Shape::closed_line(points, stroke))
65 } else {
66 self.painter.add(Shape::line(points, stroke))
67 }
68 }
69
70 pub fn circle(&self, radius: f64, stroke: impl Into<Stroke>) -> ShapeIdx {
71 self.arc(radius, 0.0, TAU, stroke)
72 }
73
74 pub fn filled_circle(&self, radius: f64, color: Color32) -> ShapeIdx {
75 let mut points = self.arc_points(radius, 0.0, TAU);
76 points.pop();
77
78 self.painter
79 .add(Shape::convex_polygon(points, color, Stroke::NONE))
80 }
81
82 pub fn line_segment(&self, from: DVec3, to: DVec3, stroke: impl Into<Stroke>) {
83 let mut points: [Pos2; 2] = Default::default();
84
85 for (i, point) in points.iter_mut().enumerate() {
86 if let Some(pos) = world_to_screen(self.viewport, self.mvp, [from, to][i]) {
87 *point = pos;
88 } else {
89 return;
90 }
91 }
92
93 self.painter.line_segment(points, stroke);
94 }
95
96 pub fn arrow(&self, from: DVec3, to: DVec3, stroke: impl Into<Stroke>) {
97 let stroke = stroke.into();
98 let arrow_start = world_to_screen(self.viewport, self.mvp, from);
99 let arrow_end = world_to_screen(self.viewport, self.mvp, to);
100
101 if let Some((start, end)) = arrow_start.zip(arrow_end) {
102 let cross = (end - start).normalized().rot90() * stroke.width;
103
104 self.painter.add(Shape::convex_polygon(
105 vec![start - cross, start + cross, end],
106 stroke.color,
107 Stroke::NONE,
108 ));
109 }
110 }
111
112 pub fn polygon(&self, points: &[DVec3], fill: impl Into<Color32>, stroke: impl Into<Stroke>) {
113 let points = points
114 .iter()
115 .filter_map(|pos| world_to_screen(self.viewport, self.mvp, *pos))
116 .collect::<Vec<_>>();
117
118 if points.len() > 2 {
119 self.painter
120 .add(Shape::convex_polygon(points, fill, stroke));
121 }
122 }
123
124 pub fn polyline(&self, points: &[DVec3], stroke: impl Into<Stroke>) {
125 let points = points
126 .iter()
127 .filter_map(|pos| world_to_screen(self.viewport, self.mvp, *pos))
128 .collect::<Vec<_>>();
129
130 if points.len() > 1 {
131 self.painter.add(Shape::line(points, stroke));
132 }
133 }
134
135 fn vec3_to_pos2(&self, vec: DVec3) -> Option<Pos2> {
136 world_to_screen(self.viewport, self.mvp, vec)
137 }
138}
139
140fn steps(angle: f64) -> usize {
141 (STEPS_PER_RAD * angle.abs()).ceil().max(1.0) as usize
142}