1use dotloom_geometry::{FlattenTolerance, Point};
4use dotloom_scene::{MarkerKind, Overlay};
5use serde::{Deserialize, Serialize};
6
7use crate::color::{Theme, premul_bytes, with_alpha};
8use crate::tess::{Mesh, dash4, effective_tolerance};
9use crate::view::View;
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase", default)]
14pub struct Grid {
15 pub visible: bool,
17 pub spacing: f64,
19 pub major_every: u32,
21 pub min_px: f64,
24}
25
26impl Default for Grid {
27 fn default() -> Self {
28 Self { visible: true, spacing: 10.0, major_every: 10, min_px: 8.0 }
29 }
30}
31
32pub const MARKER_PX: f64 = 10.0;
34
35#[must_use]
38pub fn effective_spacing(grid: &Grid, view: &View) -> Option<f64> {
39 if !(grid.spacing.is_finite() && grid.spacing > 0.0) {
40 return None;
41 }
42 let step = f64::from(grid.major_every.max(2));
43 let mut s = grid.spacing;
44 for _ in 0..64 {
45 if s * view.scale >= grid.min_px.max(2.0) {
46 return Some(s);
47 }
48 s *= step;
49 }
50 None
51}
52
53pub(crate) fn grid_mesh(grid: &Grid, view: &View, size: (u32, u32), theme: &Theme) -> Mesh {
55 let mut m = Mesh::at([0.0, 0.0]);
56 if !grid.visible {
57 return m;
58 }
59 let Some(sp) = effective_spacing(grid, view) else { return m };
60 let major = i64::from(grid.major_every.max(2));
61 let (w, h) = (f64::from(size.0), f64::from(size.1));
62 let s = view.device_scale();
63 let to_x = |x: f64| ((x - view.center[0]) * s + w * 0.5).floor() + 0.5;
64 let to_y = |y: f64| (h * 0.5 - (y - view.center[1]) * s).floor() + 0.5;
65 let half_w = w * 0.5 / s;
66 let half_h = h * 0.5 / s;
67 let (x0, x1) = ((view.center[0] - half_w) / sp, (view.center[0] + half_w) / sp);
68 let (y0, y1) = ((view.center[1] - half_h) / sp, (view.center[1] + half_h) / sp);
69 if !(x0.is_finite() && x1.is_finite() && y0.is_finite() && y1.is_finite())
71 || (x1 - x0) > 4096.0
72 || (y1 - y0) > 4096.0
73 {
74 return m;
75 }
76 let base_ratio = (sp / grid.spacing).round();
79 let level_major = |k: i64| -> bool {
80 let n = (k as f64 * base_ratio).round() as i64;
81 n % major == 0
82 };
83 let mut lines: Vec<(f64, bool, u32)> = Vec::new();
84 for k in (x0.ceil() as i64)..=(x1.floor() as i64) {
85 let color = if k == 0 {
86 theme.grid_axis
87 } else if level_major(k) {
88 theme.grid_major
89 } else {
90 theme.grid_minor
91 };
92 lines.push((to_x(k as f64 * sp), true, color));
93 }
94 for k in (y0.ceil() as i64)..=(y1.floor() as i64) {
95 let color = if k == 0 {
96 theme.grid_axis
97 } else if level_major(k) {
98 theme.grid_major
99 } else {
100 theme.grid_minor
101 };
102 lines.push((to_y(k as f64 * sp), false, color));
103 }
104 lines.sort_by_key(|(_, _, c)| u8::from(*c == theme.grid_major) + 2 * u8::from(*c == theme.grid_axis));
106 for (v, vertical, color) in lines {
107 let (a, b) =
108 if vertical { (Point::new(v, 0.0), Point::new(v, h)) } else { (Point::new(0.0, v), Point::new(w, v)) };
109 m.polyline(&[a, b], false, premul_bytes(color), 1.0, [0.0; 4]);
110 }
111 m
112}
113
114fn marker(m: &mut Mesh, kind: MarkerKind, c: [f64; 2], dpr: f64, theme: &Theme) {
115 let r = MARKER_PX * 0.5 * dpr;
116 let p = |dx: f64, dy: f64| Point::new(c[0] + dx * r, c[1] + dy * r);
117 let color = premul_bytes(theme.marker);
118 let w = 1.5;
119 match kind {
120 MarkerKind::Handle => {
121 let k = 0.7;
122 let pts = [p(-k, -k), p(k, -k), p(k, k), p(-k, k)];
123 m.fill_rings(&[pts.to_vec()], premul_bytes(theme.background));
124 m.polyline(&pts, true, premul_bytes(theme.selection), 1.5, [0.0; 4]);
125 }
126 MarkerKind::Endpoint => {
127 m.polyline(&[p(-1.0, -1.0), p(1.0, -1.0), p(1.0, 1.0), p(-1.0, 1.0)], true, color, w, [0.0; 4])
128 }
129 MarkerKind::Midpoint => m.polyline(&[p(0.0, -1.1), p(1.0, 0.8), p(-1.0, 0.8)], true, color, w, [0.0; 4]),
130 MarkerKind::Center => {
131 let pts: Vec<Point> = (0..24)
132 .map(|i| {
133 let a = f64::from(i) / 24.0 * core::f64::consts::TAU;
134 p(dotloom_geometry::math::cos(a), dotloom_geometry::math::sin(a))
135 })
136 .collect();
137 m.polyline(&pts, true, color, w, [0.0; 4]);
138 }
139 MarkerKind::Intersection => {
140 m.polyline(&[p(-1.0, -1.0), p(1.0, 1.0)], false, color, w + 0.5, [0.0; 4]);
141 m.polyline(&[p(-1.0, 1.0), p(1.0, -1.0)], false, color, w + 0.5, [0.0; 4]);
142 }
143 MarkerKind::Nearest => {
144 m.polyline(&[p(-1.0, -1.0), p(1.0, -1.0), p(-1.0, 1.0), p(1.0, 1.0)], true, color, w, [0.0; 4]);
145 }
146 MarkerKind::Grid => {
147 m.polyline(&[p(-1.0, 0.0), p(1.0, 0.0)], false, color, w, [0.0; 4]);
148 m.polyline(&[p(0.0, -1.0), p(0.0, 1.0)], false, color, w, [0.0; 4]);
149 }
150 MarkerKind::Anchor => {
151 m.polyline(&[p(0.0, -1.2), p(1.2, 0.0), p(0.0, 1.2), p(-1.2, 0.0)], true, color, w, [0.0; 4])
152 }
153 }
154}
155
156pub(crate) fn screen_overlay(o: &Overlay, view: &View, size: (u32, u32), theme: &Theme) -> Mesh {
158 let mut m = Mesh::at([0.0, 0.0]);
159 let (w, h) = (f64::from(size.0), f64::from(size.1));
160 let s = view.device_scale();
161 let to_dev = |p: Point| [(p.x - view.center[0]) * s + w * 0.5, h * 0.5 - (p.y - view.center[1]) * s];
162 if let Some(r) = o.marquee
163 && !r.is_empty()
164 && r.min.is_finite()
165 && r.max.is_finite()
166 {
167 let a = to_dev(r.min);
168 let b = to_dev(r.max);
169 let pts = [Point::new(a[0], a[1]), Point::new(b[0], a[1]), Point::new(b[0], b[1]), Point::new(a[0], b[1])];
170 let base = if o.crossing { theme.marquee_crossing } else { theme.marquee_window };
171 m.fill_rings(&[pts.to_vec()], premul_bytes(with_alpha(base, 0.1)));
172 let dash = if o.crossing { dash4(&[5.0, 4.0]) } else { [0.0; 4] };
173 m.polyline(&pts, true, premul_bytes(base), 1.0, dash);
174 }
175 for mk in &o.markers {
176 if mk.at.is_finite() {
177 marker(&mut m, mk.kind, to_dev(mk.at), view.dpr, theme);
178 }
179 }
180 m
181}
182
183pub(crate) fn world_overlay(o: &Overlay, view: &View, lod_tol: f64, theme: &Theme) -> Mesh {
185 let mut m = Mesh::at(view.center);
186 let guide = premul_bytes(theme.guide);
187 for g in &o.guides {
188 m.polyline(&[g.a, g.b], false, guide, 1.0, dash4(&[6.0, 4.0]));
189 }
190 let sketch = premul_bytes(theme.sketch);
191 for s in &o.sketch {
192 let tol = effective_tolerance(lod_tol, s.bbox());
193 for fp in s.flatten(FlattenTolerance(tol)) {
194 m.polyline(&fp.points, fp.closed, sketch, 1.5, dash4(&[4.0, 3.0]));
195 }
196 }
197 m
198}
199
200#[cfg(test)]
201mod tests {
202 use dotloom_geometry::{Aabb, Segment, Shape};
203 use dotloom_scene::Marker;
204
205 use super::*;
206
207 fn view(scale: f64) -> View {
208 View { center: [0.0, 0.0], scale, width: 400.0, height: 300.0, dpr: 2.0 }
209 }
210
211 #[test]
212 fn grid_steps_up_when_dense() {
213 let g = Grid::default();
214 assert_eq!(effective_spacing(&g, &view(1.0)), Some(10.0));
215 assert_eq!(effective_spacing(&g, &view(0.5)), Some(100.0));
216 assert_eq!(effective_spacing(&g, &view(0.01)), Some(1000.0));
217 let m = grid_mesh(&g, &view(1.0), (800, 600), &Theme::light());
218 assert!((70..=74).contains(&m.lines.len()), "{}", m.lines.len());
220 assert!(m.lines.iter().all(|l| (l.p0[0].fract() - 0.5).abs() < 1e-6 || (l.p0[1].fract() - 0.5).abs() < 1e-6));
222 let hidden = Grid { visible: false, ..g };
223 assert!(grid_mesh(&hidden, &view(1.0), (800, 600), &Theme::light()).lines.is_empty());
224 let bad = Grid { spacing: f64::NAN, ..g };
225 assert!(grid_mesh(&bad, &view(1.0), (800, 600), &Theme::light()).lines.is_empty());
226 }
227
228 #[test]
229 fn markers_have_constant_screen_size() {
230 let o = Overlay {
231 markers: vec![Marker { at: Point::new(10.0, 10.0), kind: MarkerKind::Endpoint }],
232 ..Overlay::default()
233 };
234 let a = screen_overlay(&o, &view(1.0), (800, 600), &Theme::light());
235 let b = screen_overlay(&o, &view(100.0), (800, 600), &Theme::light());
236 let span = |m: &Mesh| m.lines[0].p1[0] - m.lines[0].p0[0];
237 assert_eq!(span(&a), span(&b));
238 assert!((f64::from(span(&a)) - MARKER_PX * 2.0).abs() < 1e-3, "dpr 2 → 20 device px");
239 }
240
241 #[test]
242 fn marquee_and_world_overlay() {
243 let o = Overlay {
244 marquee: Some(Aabb::from_corners(Point::new(-10.0, -10.0), Point::new(10.0, 10.0))),
245 crossing: true,
246 guides: vec![Segment::new(Point::new(0.0, 0.0), Point::new(100.0, 0.0))],
247 sketch: vec![Shape::Line(Segment::new(Point::new(0.0, 0.0), Point::new(0.0, 50.0)))],
248 ..Overlay::default()
249 };
250 let s = screen_overlay(&o, &view(1.0), (800, 600), &Theme::light());
251 assert_eq!(s.lines.len(), 4);
252 assert!(s.lines[0].dash[0] > 0.0);
253 assert_eq!(s.fill_indices.len(), 6);
254 let w = world_overlay(&o, &view(1.0), 0.1, &Theme::light());
255 assert_eq!(w.lines.len(), 2);
256 }
257}