1use std::cmp::Ordering;
10
11use geop_core_math::{
12 plain::{add, cross, dot, normalize, scale, sub},
13 scalars::Scalar,
14 vector::Vector3,
15};
16
17use serde::Serialize;
18
19use super::{Pointer, Reach, Shape, Style, Visual};
20use geop_core_program::element::PickedElement;
21
22pub const HANDLE: f64 = 0.8;
25pub const LABEL: f64 = 1.2;
27
28pub fn nearer(a: Scalar, b: Scalar) -> Ordering {
32 a.to_f64().total_cmp(&b.to_f64())
33}
34
35pub trait Scene {
40 fn under(&self, pointer: &Pointer, accepts: &[String]) -> Vec<ElementHit>;
46
47 fn pick(&self, pointer: &Pointer, accepts: &[String]) -> Option<ElementHit> {
50 cycled(self.under(pointer, accepts), pointer.cycle)
51 }
52
53 fn hit_instance(&self, name: &str, pointer: &Pointer) -> Option<Scalar>;
56}
57
58pub fn cycled<T>(candidates: Vec<T>, cycle: usize) -> Option<T> {
61 let count = candidates.len();
62 (count > 0)
63 .then(|| candidates.into_iter().nth(cycle % count))
64 .flatten()
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
71pub struct Cycle {
72 pub at: usize,
73 pub of: usize,
74}
75
76impl Cycle {
77 pub fn of(of: usize, pointer: &Pointer) -> Option<Self> {
79 (of > 0).then(|| Cycle {
80 at: pointer.cycle % of,
81 of,
82 })
83 }
84}
85
86#[derive(Clone, Debug, PartialEq)]
89pub struct ElementHit {
90 pub element: PickedElement,
91 pub point: Vector3,
92 pub t: Scalar,
93}
94
95#[derive(Clone, Copy, Debug)]
97pub struct VisualHit<'a> {
98 pub visual: &'a Visual,
99 pub t: Scalar,
101}
102
103fn distance(
110 pointer: &Pointer,
111 visual: &Visual,
112 instance: &impl Fn(&str, &Pointer) -> Option<Scalar>,
113) -> Option<(u8, Scalar, Scalar)> {
114 let ray = &pointer.ray;
115 let within = |dist: Scalar, t: Scalar, reaches: f64| {
116 pointer
117 .within(dist, t, reaches)
118 .then(|| (dist.div(pointer.reach.at(t)).unwrap_or(Scalar::ZERO), t))
119 };
120 match &visual.shape {
121 Shape::Handle { at, direction } => {
122 let radius = pointer.reach_at(HANDLE, ray.closest_to_point(at));
123 let (dist, t) = match direction.normalize() {
124 Ok(d) => {
125 let reach = d.prod_scalar(radius.mul(Scalar::from_f64(3.0)));
126 ray.distance_to_segment(&at.sub(&reach), &at.add(&reach))
127 }
128 Err(_) => ray.distance_to_point(at),
129 };
130 within(dist, t, HANDLE).map(|(p, t)| (0, p, t))
131 }
132 Shape::Label { at, offset, .. } => {
133 let center = at.add(&offset.prod_scalar(pointer.reach.at(ray.closest_to_point(at))));
134 let (dist, t) = ray.distance_to_point(¢er);
135 within(dist, t, LABEL).map(|(p, t)| (0, p, t))
136 }
137 Shape::Point { at } => {
138 let (dist, t) = ray.distance_to_point(at);
139 within(dist, t, 1.0).map(|(p, t)| (1, p, t))
140 }
141 Shape::Polyline { points } => points
142 .windows(2)
143 .filter_map(|w| match w {
144 [a, b] => {
145 let (dist, t) = ray.distance_to_segment(a, b);
146 within(dist, t, 1.0)
147 }
148 _ => None,
149 })
150 .min_by(|a, b| nearer(a.0, b.0))
151 .map(|(p, t)| {
152 let rank = if matches!(visual.style, Style::Construction | Style::Reference) {
153 3
154 } else {
155 2
156 };
157 (rank, p, t)
158 }),
159 Shape::Triangles { triangles } => triangles
160 .iter()
161 .filter_map(|[a, b, c]| ray.intersect_triangle(a, b, c))
162 .min_by(|&a, &b| nearer(a, b))
163 .map(|t| (4, Scalar::ZERO, t)),
164 Shape::Instance { name } => instance(name, pointer).map(|t| (4, Scalar::ZERO, t)),
165 Shape::Triad { .. } => None,
166 }
167}
168
169pub fn visuals_under<'a>(
176 visuals: &'a [Visual],
177 pointer: &Pointer,
178 instance: impl Fn(&str, &Pointer) -> Option<Scalar>,
179 accept: impl Fn(&Visual) -> bool,
180) -> Vec<VisualHit<'a>> {
181 let mut hits: Vec<_> = (visuals.iter())
182 .filter(|v| accept(v))
183 .filter_map(|v| {
184 distance(pointer, v, &instance).map(|(rank, reaches, t)| (rank, reaches, t, v))
185 })
186 .collect();
187 let first = (hits.iter().enumerate())
188 .min_by(|(_, a), (_, b)| a.0.cmp(&b.0).then(nearer(a.1, b.1)).then(nearer(a.2, b.2)))
189 .map(|(at, _)| at);
190 let first = first.map(|at| hits.remove(at));
191 hits.sort_by(|a, b| nearer(a.2, b.2));
192 (first.into_iter().chain(hits))
193 .map(|(_, _, t, visual)| VisualHit { visual, t })
194 .collect()
195}
196
197pub fn hit_visuals<'a>(
200 visuals: &'a [Visual],
201 pointer: &Pointer,
202 instance: impl Fn(&str, &Pointer) -> Option<Scalar>,
203 accept: impl Fn(&Visual) -> bool,
204) -> Option<VisualHit<'a>> {
205 cycled(
206 visuals_under(visuals, pointer, instance, accept),
207 pointer.cycle,
208 )
209}
210
211pub fn no_instances(_: &str, _: &Pointer) -> Option<Scalar> {
213 None
214}
215
216#[derive(Clone, Debug)]
223pub struct Window {
224 origin: [f64; 3],
226 sight: [f64; 3],
228 sides: [[f64; 3]; 2],
230 perspective: bool,
232 low: [f64; 2],
234 high: [f64; 2],
235}
236
237impl Window {
238 pub fn new(from: &Pointer, to: &Pointer) -> Option<Self> {
240 let sight = normalize(from.ray.dir().to_f64_array())?;
241 let axis = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
243 .into_iter()
244 .min_by(|a, b| dot(*a, sight).abs().total_cmp(&dot(*b, sight).abs()))?;
245 let v = normalize(sub(axis, scale(sight, dot(axis, sight))))?;
246 let u = cross(v, sight);
247 let perspective = matches!(from.reach, Reach::Cone { .. });
248 let mut window = Self {
249 origin: from.ray.origin().to_f64_array(),
250 sight,
251 sides: [u, v],
252 perspective,
253 low: [0.0; 2],
254 high: [0.0; 2],
255 };
256 let [x, y] = match perspective {
257 true => window.screen(&to.ray.at(Scalar::ONE).to_f64_array())?,
258 false => window.screen(&to.ray.origin().to_f64_array())?,
259 };
260 window.low = [x.min(0.0), y.min(0.0)];
261 window.high = [x.max(0.0), y.max(0.0)];
262 Some(window)
263 }
264
265 fn screen(&self, p: &[f64; 3]) -> Option<[f64; 2]> {
268 let r = sub(*p, self.origin);
269 let [u, v] = self.sides;
270 let (x, y) = (dot(r, u), dot(r, v));
271 if !self.perspective {
272 return Some([x, y]);
273 }
274 let depth = dot(r, self.sight);
275 (depth > 0.0).then(|| [x / depth, y / depth])
276 }
277
278 pub fn contains(&self, p: &Vector3) -> bool {
280 let [low_x, low_y] = self.low;
281 let [high_x, high_y] = self.high;
282 self.screen(&p.to_f64_array())
283 .is_some_and(|[x, y]| low_x <= x && x <= high_x && low_y <= y && y <= high_y)
284 }
285
286 pub fn depth(&self, p: &Vector3) -> f64 {
288 dot(sub(p.to_f64_array(), self.origin), self.sight)
289 }
290
291 pub fn corners(&self, depth: f64) -> [Vector3; 4] {
294 let [u, v] = self.sides;
295 let size = if self.perspective { depth } else { 1.0 };
296 let centre = add(self.origin, scale(self.sight, depth));
297 let at = |x: f64, y: f64| {
298 let across = add(scale(u, x), scale(v, y));
299 Vector3::from_array(add(centre, scale(across, size)).map(Scalar::from_f64))
300 };
301 let [low_x, low_y] = self.low;
302 let [high_x, high_y] = self.high;
303 [
304 at(low_x, low_y),
305 at(high_x, low_y),
306 at(high_x, high_y),
307 at(low_x, high_y),
308 ]
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use geop_core_math::{primitives::Ray, scalars::Scalar, vector::Vector3};
315
316 use super::*;
317 use crate::editing::Reach;
318
319 fn v(x: f64, y: f64, z: f64) -> Vector3 {
320 Vector3::from_array([x, y, z].map(Scalar::from_f64))
321 }
322
323 fn down(x: f64, y: f64) -> Pointer {
326 Pointer {
327 cycle: 0,
328 ray: Ray::try_new(v(x, y, 10.0), v(0.0, 0.0, -1.0)).unwrap(),
329 reach: Reach::Tube {
330 radius: Scalar::from_f64(0.1),
331 },
332 }
333 }
334
335 fn point(key: &str, at: Vector3) -> Visual {
336 Visual::new(key, Shape::Point { at }, Style::Free)
337 }
338
339 #[test]
342 fn points_win_over_curves() {
343 let visuals = [
344 Visual::new(
345 "line",
346 Shape::Polyline {
347 points: vec![v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0)],
348 },
349 Style::Free,
350 ),
351 point("end", v(1.0, 0.0, 0.0)),
352 ];
353 let key = |x: f64, y: f64| {
354 hit_visuals(&visuals, &down(x, y), no_instances, |_| true).map(|h| h.visual.key.clone())
355 };
356 assert_eq!(key(0.95, 0.0).as_deref(), Some("end"));
357 assert_eq!(key(0.5, 0.05).as_deref(), Some("line"));
358 assert_eq!(key(0.5, 0.2), None);
359 }
360
361 #[test]
364 fn the_area_in_front_is_hit() {
365 let square = |key: &str, z: f64| {
366 Visual::new(
367 key,
368 Shape::Triangles {
369 triangles: vec![
370 [v(-1.0, -1.0, z), v(1.0, -1.0, z), v(1.0, 1.0, z)],
371 [v(-1.0, -1.0, z), v(1.0, 1.0, z), v(-1.0, 1.0, z)],
372 ],
373 },
374 Style::Region,
375 )
376 };
377 let visuals = [square("below", -1.0), square("above", 1.0)];
378 let hit = hit_visuals(&visuals, &down(0.2, 0.3), no_instances, |_| true);
379 assert_eq!(hit.map(|h| h.visual.key.as_str()), Some("above"));
380 }
381
382 #[test]
385 fn labels_are_hit_where_they_are_drawn() {
386 let visuals = [Visual::new(
387 "k1",
388 Shape::Label {
389 at: v(0.0, 0.0, 0.0),
390 text: "H".into(),
391 offset: v(2.0, 1.0, 0.0),
392 },
393 Style::Free,
394 )];
395 assert!(hit_visuals(&visuals, &down(0.2, 0.1), no_instances, |_| true).is_some());
396 assert!(hit_visuals(&visuals, &down(0.0, 0.0), no_instances, |_| true).is_none());
397 }
398
399 #[test]
405 fn windows_take_what_is_seen_within_them() {
406 let window = Window::new(&down(0.0, 0.0), &down(2.0, 1.0)).unwrap();
407 assert!(window.contains(&v(1.0, 0.5, -3.0)));
408 assert!(window.contains(&v(1.0, 0.5, 7.0)));
409 assert!(!window.contains(&v(3.0, 0.5, 0.0)));
410 assert!(!window.contains(&v(1.0, -0.5, 0.0)));
411 let corners = window.corners(10.0);
412 assert!(corners[2].could_be_equal(&v(2.0, 1.0, 0.0)), "{corners:?}");
413
414 let eye = |x: f64, y: f64| Pointer {
415 cycle: 0,
416 ray: Ray::try_new(v(0.0, 0.0, 10.0), v(x, y, -10.0)).unwrap(),
417 reach: Reach::Cone {
418 slope: Scalar::from_f64(0.01),
419 },
420 };
421 let window = Window::new(&eye(0.0, 0.0), &eye(1.0, 1.0)).unwrap();
422 assert!(window.contains(&v(0.5, 0.5, 0.0)));
423 assert!(!window.contains(&v(0.5, 0.5, 8.0)));
424 }
425
426 #[test]
428 fn cones_widen_with_distance() {
429 let visuals = [point("p", v(0.3, 0.0, 0.0))];
430 let from = |z: f64| Pointer {
431 cycle: 0,
432 ray: Ray::try_new(v(0.0, 0.0, z), v(0.0, 0.0, -1.0)).unwrap(),
433 reach: Reach::Cone {
434 slope: Scalar::from_f64(0.01),
435 },
436 };
437 assert!(hit_visuals(&visuals, &from(10.0), no_instances, |_| true).is_none());
438 assert!(hit_visuals(&visuals, &from(40.0), no_instances, |_| true).is_some());
439 }
440}