Skip to main content

pfx_editor_viewport/
overlay.rs

1use egui::{Align2, Color32, CornerRadius, Painter, Pos2, Rect, Stroke, StrokeKind, Vec2};
2use glam::DVec3;
3use pfx_editor_style::Theme;
4
5use crate::camera::Basis;
6use crate::gizmo::colours;
7use crate::lens::Lens;
8use crate::pick::EDGES;
9
10pub const SQUARE: f32 = 20.0;
11pub const GRID_LINES: i32 = 10;
12pub const GRID_STEP: f64 = 1.0;
13pub const DIM: f32 = 0.6;
14
15pub fn checker(painter: &Painter, image: Rect, theme: &Theme) {
16    painter.rect_filled(image, CornerRadius::ZERO, theme.well);
17    let columns = (image.width() / SQUARE).ceil() as i32;
18    let rows = (image.height() / SQUARE).ceil() as i32;
19    for row in 0..rows {
20        for column in (row % 2..columns).step_by(2) {
21            let min = image.min + Vec2::new(column as f32 * SQUARE, row as f32 * SQUARE);
22            let square = Rect::from_min_size(min, Vec2::splat(SQUARE)).intersect(image);
23            painter.rect_filled(square, CornerRadius::ZERO, theme.surface);
24        }
25    }
26}
27
28pub fn border(painter: &Painter, image: Rect, theme: &Theme, focused: bool) {
29    let colour = if focused { theme.accent } else { theme.line };
30    painter.rect_stroke(
31        image,
32        CornerRadius::ZERO,
33        Stroke::new(theme.stroke, colour),
34        StrokeKind::Inside,
35    );
36}
37
38pub fn bands(image: Rect, frame: Rect) -> Vec<Rect> {
39    let frame = frame.intersect(image);
40    [
41        Rect::from_min_max(image.min, Pos2::new(image.max.x, frame.min.y)),
42        Rect::from_min_max(Pos2::new(image.min.x, frame.max.y), image.max),
43        Rect::from_min_max(
44            Pos2::new(image.min.x, frame.min.y),
45            Pos2::new(frame.min.x, frame.max.y),
46        ),
47        Rect::from_min_max(
48            Pos2::new(frame.max.x, frame.min.y),
49            Pos2::new(image.max.x, frame.max.y),
50        ),
51    ]
52    .into_iter()
53    .filter(|band| band.width() > 0.0 && band.height() > 0.0)
54    .collect()
55}
56
57pub fn passepartout(painter: &Painter, image: Rect, frame: Rect, theme: &Theme) {
58    for band in bands(image, frame) {
59        painter.rect_filled(band, CornerRadius::ZERO, theme.bg.gamma_multiply(DIM));
60    }
61    painter.rect_stroke(
62        frame,
63        CornerRadius::ZERO,
64        Stroke::new(theme.stroke, theme.line),
65        StrokeKind::Outside,
66    );
67}
68
69pub fn grid(painter: &Painter, lens: &Lens, theme: &Theme) {
70    let reach = f64::from(GRID_LINES) * GRID_STEP;
71    let [x, _, z] = colours(theme);
72    let faint = Stroke::new(1.0_f32, theme.line.gamma_multiply(0.35));
73    for line in -GRID_LINES..=GRID_LINES {
74        let at = f64::from(line) * GRID_STEP;
75        for (a, b, axis) in [
76            (DVec3::new(-reach, 0.0, at), DVec3::new(reach, 0.0, at), x),
77            (DVec3::new(at, 0.0, -reach), DVec3::new(at, 0.0, reach), z),
78        ] {
79            let stroke = if line == 0 {
80                Stroke::new(1.0_f32, axis.gamma_multiply(0.45))
81            } else {
82                faint
83            };
84            if let Some(points) = lens.segment(a, b) {
85                painter.line_segment(points, stroke);
86            }
87        }
88    }
89}
90
91pub fn axes(painter: &Painter, image: Rect, basis: &Basis, theme: &Theme) {
92    let reach = (image.height() * 0.05).clamp(14.0, 32.0).round();
93    let centre = Pos2::new(image.left() + reach + 22.0, image.bottom() - reach - 22.0).round();
94    let tones = colours(theme);
95    let mut order: Vec<(f64, &str, Vec2, Color32)> = [DVec3::X, DVec3::Y, DVec3::Z]
96        .into_iter()
97        .zip(["x", "y", "z"])
98        .zip(tones)
99        .map(|((axis, name), colour)| {
100            let flat = Vec2::new(axis.dot(basis.right) as f32, -(axis.dot(basis.up) as f32));
101            (axis.dot(basis.forward), name, flat, colour)
102        })
103        .collect();
104    order.sort_by(|a, b| b.0.total_cmp(&a.0));
105    painter.circle_filled(centre, reach + 12.0, theme.well.gamma_multiply(0.8));
106    let font = theme.font(theme.size_code);
107    for (_, name, flat, colour) in order {
108        let end = centre + flat * reach;
109        painter.line_segment([centre, end], Stroke::new(1.5_f32, colour));
110        painter.circle_filled(end, 2.5, colour);
111        painter.text(
112            centre + flat * (reach + 9.0),
113            Align2::CENTER_CENTER,
114            name,
115            font.clone(),
116            colour,
117        );
118    }
119    painter.circle_filled(centre, 2.0, theme.accent);
120}
121
122pub fn outline(painter: &Painter, lens: &Lens, boxes: &[[DVec3; 8]], colour: Color32, width: f32) {
123    for corners in boxes {
124        for [a, b] in EDGES {
125            if let Some(points) = lens.segment(corners[a], corners[b]) {
126                painter.line_segment(points, Stroke::new(width, colour));
127            }
128        }
129    }
130}
131
132pub fn band(painter: &Painter, at: Pos2, text: &str, colour: Color32, wrap: f32, theme: &Theme) {
133    let galley = painter.layout(text.to_string(), theme.font(theme.size_small), colour, wrap);
134    let band = Rect::from_min_size(at, galley.size() + Vec2::new(16.0, 8.0));
135    painter.rect_filled(band, CornerRadius::same(theme.radius), theme.well);
136    painter.rect_stroke(
137        band,
138        CornerRadius::same(theme.radius),
139        Stroke::new(theme.stroke, colour),
140        StrokeKind::Inside,
141    );
142    painter.galley(at + Vec2::new(8.0, 4.0), galley, colour);
143}
144
145pub fn notice(painter: &Painter, image: Rect, text: &str, colour: Color32, theme: &Theme) {
146    let at = image.left_top() + Vec2::splat(theme.section_gap);
147    let wrap = (image.width() - 2.0 * theme.section_gap - 16.0).max(80.0);
148    band(painter, at, text, colour, wrap, theme);
149}
150
151pub fn linear(colour: Color32) -> [f32; 3] {
152    let channel = |v: u8| {
153        let c = f32::from(v) / 255.0;
154        if c <= 0.04045 {
155            c / 12.92
156        } else {
157            ((c + 0.055) / 1.055).powf(2.4)
158        }
159    };
160    [
161        channel(colour.r()),
162        channel(colour.g()),
163        channel(colour.b()),
164    ]
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn a_colour_turns_linear_for_the_engine() {
173        assert_eq!(linear(Color32::from_rgb(0, 255, 0)), [0.0, 1.0, 0.0]);
174        let [r, ..] = linear(Color32::from_rgb(0x80, 0, 0));
175        assert!((r - 0.2158605).abs() < 1e-6);
176    }
177
178    #[test]
179    fn the_bands_cover_the_panel_around_the_frame() {
180        let image = Rect::from_min_size(Pos2::new(5.0, 5.0), Vec2::new(400.0, 300.0));
181        let frame = Rect::from_center_size(image.center(), Vec2::new(200.0, 100.0));
182        let around = bands(image, frame);
183        assert_eq!(around.len(), 4);
184        let area = |rect: Rect| rect.width() * rect.height();
185        let covered: f32 = around.iter().map(|band| area(*band)).sum();
186        assert_eq!(covered + area(frame), area(image));
187        for (k, a) in around.iter().enumerate() {
188            assert!(!a.intersects(frame.shrink(0.5)));
189            for b in &around[k + 1..] {
190                assert!(!a.shrink(0.5).intersects(b.shrink(0.5)), "{a:?} {b:?}");
191            }
192        }
193        assert!(bands(image, image).is_empty());
194    }
195}