egui_gizmo/
painter.rs

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}