1use std::cmp::Ordering;
10
11use geop_core_math::scalars::Scalar;
12
13use super::{Pointer, Shape, Visual};
14
15pub const HANDLE: f64 = 0.8;
18pub const LABEL: f64 = 1.2;
20
21pub fn nearer<S: Scalar>(a: S, b: S) -> Ordering {
25 a.to_f64().total_cmp(&b.to_f64())
26}
27
28#[derive(Clone, Copy, Debug)]
30pub struct VisualHit<'a, S: Scalar> {
31 pub visual: &'a Visual<S>,
32 pub t: S,
34}
35
36fn distance<S: Scalar>(pointer: &Pointer<S>, visual: &Visual<S>) -> Option<(u8, S, S)> {
40 let ray = &pointer.ray;
41 let within = |dist: S, t: S, reaches: f64| {
42 pointer
43 .within(dist, t, reaches)
44 .then(|| (dist.div(pointer.reach.at(t)).unwrap_or(S::ZERO), t))
45 };
46 match &visual.shape {
47 Shape::Handle { at, direction } => {
48 let radius = pointer.reach_at(HANDLE, ray.closest_to_point(at));
49 let (dist, t) = match direction.normalize() {
50 Ok(d) => {
51 let reach = d.prod_scalar(radius.mul(S::from_f64(3.0)));
52 ray.distance_to_segment(&at.sub(&reach), &at.add(&reach))
53 }
54 Err(_) => ray.distance_to_point(at),
55 };
56 within(dist, t, HANDLE).map(|(p, t)| (0, p, t))
57 }
58 Shape::Label { at, offset, .. } => {
59 let center = at.add(&offset.prod_scalar(pointer.reach.at(ray.closest_to_point(at))));
60 let (dist, t) = ray.distance_to_point(¢er);
61 within(dist, t, LABEL).map(|(p, t)| (0, p, t))
62 }
63 Shape::Point { at } => {
64 let (dist, t) = ray.distance_to_point(at);
65 within(dist, t, 1.0).map(|(p, t)| (1, p, t))
66 }
67 Shape::Polyline { points } => points
68 .windows(2)
69 .filter_map(|w| {
70 let (dist, t) = ray.distance_to_segment(&w[0], &w[1]);
71 within(dist, t, 1.0)
72 })
73 .min_by(|a, b| nearer(a.0, b.0))
74 .map(|(p, t)| (2, p, t)),
75 Shape::Triangles { triangles } => triangles
76 .iter()
77 .filter_map(|[a, b, c]| ray.intersect_triangle(a, b, c))
78 .min_by(|&a, &b| nearer(a, b))
79 .map(|t| (3, S::ZERO, t)),
80 }
81}
82
83pub fn hit_visuals<'a, S: Scalar>(
87 visuals: &'a [Visual<S>],
88 pointer: &Pointer<S>,
89 accept: impl Fn(&Visual<S>) -> bool,
90) -> Option<VisualHit<'a, S>> {
91 visuals
92 .iter()
93 .filter(|v| accept(v))
94 .filter_map(|v| distance(pointer, v).map(|(rank, reaches, t)| (rank, reaches, t, v)))
95 .min_by(|a, b| a.0.cmp(&b.0).then(nearer(a.1, b.1)))
96 .map(|(_, _, t, visual)| VisualHit { visual, t })
97}
98
99#[cfg(test)]
100mod tests {
101 use geop_core_math::{primitives::Ray, scalars::ScalInF64, vector::Vector3};
102
103 use super::*;
104 use crate::ui::{Reach, Style};
105
106 type S = ScalInF64;
107
108 fn v(x: f64, y: f64, z: f64) -> Vector3<S> {
109 Vector3::from_array([x, y, z].map(S::from_f64))
110 }
111
112 fn down(x: f64, y: f64) -> Pointer<S> {
115 Pointer {
116 ray: Ray::try_new(v(x, y, 10.0), v(0.0, 0.0, -1.0)).unwrap(),
117 reach: Reach::Tube {
118 radius: S::from_f64(0.1),
119 },
120 }
121 }
122
123 fn point(key: &str, at: Vector3<S>) -> Visual<S> {
124 Visual::new(key, Shape::Point { at }, Style::Free)
125 }
126
127 #[test]
130 fn points_win_over_curves() {
131 let visuals = [
132 Visual::new(
133 "line",
134 Shape::Polyline {
135 points: vec![v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0)],
136 },
137 Style::Free,
138 ),
139 point("end", v(1.0, 0.0, 0.0)),
140 ];
141 let key = |x: f64, y: f64| {
142 hit_visuals(&visuals, &down(x, y), |_| true).map(|h| h.visual.key.clone())
143 };
144 assert_eq!(key(0.95, 0.0).as_deref(), Some("end"));
145 assert_eq!(key(0.5, 0.05).as_deref(), Some("line"));
146 assert_eq!(key(0.5, 0.2), None);
147 }
148
149 #[test]
152 fn labels_are_hit_where_they_are_drawn() {
153 let visuals = [Visual::new(
154 "k1",
155 Shape::Label {
156 at: v(0.0, 0.0, 0.0),
157 text: "H".into(),
158 offset: v(2.0, 1.0, 0.0),
159 },
160 Style::Free,
161 )];
162 assert!(hit_visuals(&visuals, &down(0.2, 0.1), |_| true).is_some());
163 assert!(hit_visuals(&visuals, &down(0.0, 0.0), |_| true).is_none());
164 }
165
166 #[test]
168 fn cones_widen_with_distance() {
169 let visuals = [point("p", v(0.3, 0.0, 0.0))];
170 let from = |z: f64| Pointer {
171 ray: Ray::try_new(v(0.0, 0.0, z), v(0.0, 0.0, -1.0)).unwrap(),
172 reach: Reach::Cone {
173 slope: S::from_f64(0.01),
174 },
175 };
176 assert!(hit_visuals(&visuals, &from(10.0), |_| true).is_none());
177 assert!(hit_visuals(&visuals, &from(40.0), |_| true).is_some());
178 }
179}