Skip to main content

geop_core_editor/editing/
hit.rs

1//! Hit tests: which [`Visual`] a [`Pointer`] is over, shared by every
2//! operation so a click means the same thing whatever is being edited.
3//!
4//! What counts as over something is measured in the pointer's reach (see
5//! [`super::Reach`]), so "near" means near on screen however far away or
6//! zoomed in the view is. The geometry itself — how near a ray passes to a
7//! point, a segment, a triangle — is [`geop_core_math::primitives::Ray`]'s.
8
9use 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
22/// A handle's radius, in reaches; its arrows reach three times as far along
23/// its direction.
24pub const HANDLE: f64 = 0.8;
25/// How near a label's center a click has to land, in reaches.
26pub const LABEL: f64 = 1.2;
27
28/// Orders two distances for picking the nearest. Which of two overlapping
29/// enclosures is "nearer" is no geometric claim, only which of two things
30/// under the pointer to prefer, so their midpoints decide.
31pub fn nearer(a: Scalar, b: Scalar) -> Ordering {
32    a.to_f64().total_cmp(&b.to_f64())
33}
34
35/// The part as an editor draws it, which picks are made in: what an editor
36/// needs to know of it beyond a step's own visuals, and what an operation
37/// is given to find what the pointer is over (see
38/// [`crate::editable::Context::scene`]).
39pub trait Scene {
40    /// Every element of the kinds `accepts` under `pointer`, each once: the
41    /// one a click picks first — the smallest, in front, nearest the eye
42    /// (see [`crate::view::PartView::under`]) — then the rest front to back
43    /// along the ray, those hidden behind the first included. Its
44    /// [`Pointer::cycle`] is not looked at.
45    fn under(&self, pointer: &Pointer, accepts: &[String]) -> Vec<ElementHit>;
46
47    /// What a click with `pointer` picks: of what lies [`Scene::under`]
48    /// it, the one its [`Pointer::cycle`] names, and where it is hit.
49    fn pick(&self, pointer: &Pointer, accepts: &[String]) -> Option<ElementHit> {
50        cycled(self.under(pointer, accepts), pointer.cycle)
51    }
52
53    /// How far along `pointer`'s ray the placed part `name` is hit, if it
54    /// is (see [`Shape::Instance`]).
55    fn hit_instance(&self, name: &str, pointer: &Pointer) -> Option<Scalar>;
56}
57
58/// The one of `candidates` a pointer's [`Pointer::cycle`] `cycle` names:
59/// counted from the first, wrapping round. None of none.
60pub 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/// Which of the candidates under the pointer is lit: the `at`-th of `of`,
68/// front to back, counted from 0 — what a front end says while its user
69/// steps through them (see [`Pointer::cycle`]).
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
71pub struct Cycle {
72    pub at: usize,
73    pub of: usize,
74}
75
76impl Cycle {
77    /// Of `of` candidates, the one `pointer` names: none of none.
78    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/// An element of the part a pointer is over, as drawn: where, and how far
87/// along its ray (see [`Scene::pick`]).
88#[derive(Clone, Debug, PartialEq)]
89pub struct ElementHit {
90    pub element: PickedElement,
91    pub point: Vector3,
92    pub t: Scalar,
93}
94
95/// A visual the pointer is over.
96#[derive(Clone, Copy, Debug)]
97pub struct VisualHit<'a> {
98    pub visual: &'a Visual,
99    /// How far along the ray.
100    pub t: Scalar,
101}
102
103/// How near the pointer is to `visual`, in reaches, and where along the
104/// ray — `None` if not near enough to count. `rank` orders kinds of shapes:
105/// what is drawn small and on top wins over what is drawn large, and a
106/// curve that is part of the result over construction or reference geometry
107/// it lies on — a line drawn along a sketch's axis is that line. A placed part is
108/// hit where `instance` says, by its name: how far along the ray, if at all.
109fn 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(&center);
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
169/// The visuals among those `accept` takes that the pointer is over: first
170/// the one a click takes — handles and labels first, then points, curves
171/// and areas; within each, the one nearest the pointer on screen, and of
172/// those as near, the one nearest the eye: of two areas the pointer is
173/// over, the one in front — then the rest front to back along the ray.
174/// Placed parts are hit where `instance` says (see [`no_instances`]).
175pub 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
197/// The visual among those `accept` takes that the pointer is over: of
198/// those [`visuals_under`] it, the one its [`Pointer::cycle`] names.
199pub 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
211/// For [`hit_visuals`] where no placed part is drawn: none is hit.
212pub fn no_instances(_: &str, _: &Pointer) -> Option<Scalar> {
213    None
214}
215
216/// The window a drag from one pointer to another sweeps over the view: the
217/// rectangle between the two on the plane facing the eye, for selecting
218/// everything within it. Its sides run along the world's axes as the view
219/// shows them — the two least along the line of sight — which are the
220/// screen's own in a view along an axis. Seen in perspective it is the
221/// frustum through that rectangle.
222#[derive(Clone, Debug)]
223pub struct Window {
224    /// The eye, or where the first pointer's ray starts.
225    origin: [f64; 3],
226    /// The line of sight, unit.
227    sight: [f64; 3],
228    /// The window's two sides, unit and square to the line of sight.
229    sides: [[f64; 3]; 2],
230    /// Whether the view is in perspective: rays from one eye.
231    perspective: bool,
232    /// The window's corners in its sides' coordinates, low and high.
233    low: [f64; 2],
234    high: [f64; 2],
235}
236
237impl Window {
238    /// The window swept from `from` to `to`.
239    pub fn new(from: &Pointer, to: &Pointer) -> Option<Self> {
240        let sight = normalize(from.ray.dir().to_f64_array())?;
241        // The axis least along the line of sight, made square to it.
242        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    /// Where `p` is on the screen, in the window's sides: `None` behind the
266    /// eye.
267    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    /// Whether `p` is seen within the window.
279    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    /// How far `p` is along the line of sight.
287    pub fn depth(&self, p: &Vector3) -> f64 {
288        dot(sub(p.to_f64_array(), self.origin), self.sight)
289    }
290
291    /// The window's corners, around, `depth` along the line of sight: to
292    /// draw it there.
293    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    /// Looking straight down `-z` from `(x, y, 10)`, reaching a tenth of a
324    /// unit wherever it looks.
325    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    /// A point wins over the curve it lies on, within reach of it; further
340    /// along, the curve is hit.
341    #[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    /// Of two areas one behind the other, the one in front is hit, whichever
362    /// is listed first.
363    #[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    /// A label is hit where it is drawn: moved from its point by its
383    /// offset, in reaches.
384    #[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    /// A window dragged looking down from `(0, 0)` to `(2, 1)` takes a
400    /// point at `(1, 0.5)` at any depth and not one at `(3, 0.5)`; seen in
401    /// perspective from `z = 10` towards `(1, 1, 0)`, the window widens
402    /// with depth: a point at `(0.5, 0.5, 0)` is within it, one at
403    /// `(0.5, 0.5, 8)`, nearer the eye, is not.
404    #[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    /// In perspective, what is further away is hit from further off.
427    #[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}