1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::f64::consts::TAU;

use egui::layers::ShapeIdx;
use egui::{Color32, Pos2, Rect, Shape, Stroke};
use glam::{DMat4, DVec3};

use crate::math::world_to_screen;

const STEPS_PER_RAD: f64 = 20.0;

pub struct Painter3d {
    painter: egui::Painter,
    mvp: DMat4,
    viewport: Rect,
}

impl Painter3d {
    pub const fn new(painter: egui::Painter, mvp: DMat4, viewport: Rect) -> Self {
        Self {
            painter,
            mvp,
            viewport,
        }
    }

    fn arc_points(&self, radius: f64, start_angle: f64, end_angle: f64) -> Vec<Pos2> {
        let angle = f64::clamp(end_angle - start_angle, -TAU, TAU);

        let step_count = steps(angle);
        let mut points = Vec::with_capacity(step_count);

        let step_size = angle / (step_count - 1) as f64;

        for step in (0..step_count).map(|i| step_size * i as f64) {
            let x = f64::cos(start_angle + step) * radius;
            let z = f64::sin(start_angle + step) * radius;

            points.push(DVec3::new(x, 0.0, z));
        }

        points
            .into_iter()
            .filter_map(|point| self.vec3_to_pos2(point))
            .collect::<Vec<_>>()
    }

    pub fn arc(
        &self,
        radius: f64,
        start_angle: f64,
        end_angle: f64,
        stroke: impl Into<Stroke>,
    ) -> ShapeIdx {
        let mut points = self.arc_points(radius, start_angle, end_angle);

        let closed = points
            .first()
            .zip(points.last())
            .filter(|(first, last)| first.distance(**last) < 1e-2)
            .is_some();

        if closed {
            points.pop();
            self.painter.add(Shape::closed_line(points, stroke))
        } else {
            self.painter.add(Shape::line(points, stroke))
        }
    }

    pub fn circle(&self, radius: f64, stroke: impl Into<Stroke>) -> ShapeIdx {
        self.arc(radius, 0.0, TAU, stroke)
    }

    pub fn filled_circle(&self, radius: f64, color: Color32) -> ShapeIdx {
        let mut points = self.arc_points(radius, 0.0, TAU);
        points.pop();

        self.painter
            .add(Shape::convex_polygon(points, color, Stroke::NONE))
    }

    pub fn line_segment(&self, from: DVec3, to: DVec3, stroke: impl Into<Stroke>) {
        let mut points: [Pos2; 2] = Default::default();

        for (i, point) in points.iter_mut().enumerate() {
            if let Some(pos) = world_to_screen(self.viewport, self.mvp, [from, to][i]) {
                *point = pos;
            } else {
                return;
            }
        }

        self.painter.line_segment(points, stroke);
    }

    pub fn arrow(&self, from: DVec3, to: DVec3, stroke: impl Into<Stroke>) {
        let stroke = stroke.into();
        let arrow_start = world_to_screen(self.viewport, self.mvp, from);
        let arrow_end = world_to_screen(self.viewport, self.mvp, to);

        if let Some((start, end)) = arrow_start.zip(arrow_end) {
            let cross = (end - start).normalized().rot90() * stroke.width;

            self.painter.add(Shape::convex_polygon(
                vec![start - cross, start + cross, end],
                stroke.color,
                Stroke::NONE,
            ));
        }
    }

    pub fn polygon(&self, points: &[DVec3], fill: impl Into<Color32>, stroke: impl Into<Stroke>) {
        let points = points
            .iter()
            .filter_map(|pos| world_to_screen(self.viewport, self.mvp, *pos))
            .collect::<Vec<_>>();

        if points.len() > 2 {
            self.painter
                .add(Shape::convex_polygon(points, fill, stroke));
        }
    }

    pub fn polyline(&self, points: &[DVec3], stroke: impl Into<Stroke>) {
        let points = points
            .iter()
            .filter_map(|pos| world_to_screen(self.viewport, self.mvp, *pos))
            .collect::<Vec<_>>();

        if points.len() > 1 {
            self.painter.add(Shape::line(points, stroke));
        }
    }

    fn vec3_to_pos2(&self, vec: DVec3) -> Option<Pos2> {
        world_to_screen(self.viewport, self.mvp, vec)
    }
}

fn steps(angle: f64) -> usize {
    (STEPS_PER_RAD * angle.abs()).ceil().max(1.0) as usize
}