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}