1use crate::geometry2d::point_segment_distance;
14
15use crate::scene::{RenderScene, SolidDisplay};
16use crate::view::{add3, cross3, dot3, scale3, sub3, Projection, Ray, ViewCamera};
17
18pub const EDGE_PICK_PX: f64 = 6.0;
20pub const VERTEX_PICK_PX: f64 = 6.0;
21const MAX_CANDIDATES: usize = 16;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum PickKind {
25 Vertex = 0,
26 Edge = 1,
27 Face = 2,
28 Plane = 3,
39 Solid = 4,
40 Component = 5,
45}
46
47impl PickKind {
48 pub fn as_str(&self) -> &'static str {
49 match self {
50 PickKind::Vertex => "VERTEX",
51 PickKind::Edge => "EDGE",
52 PickKind::Face => "FACE",
53 PickKind::Plane => "PLANE",
54 PickKind::Solid => "SOLID",
55 PickKind::Component => "COMPONENT",
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct PickCandidate {
62 pub kind: PickKind,
63 pub name: String,
66 pub solid: String,
68 pub depth: f64,
70 pub screen_dist: f64,
72 pub position: [f64; 3],
74}
75
76#[derive(Debug, Clone, Copy)]
77pub struct PickOptions {
78 pub double_sided: bool,
79 pub edge_px: f64,
80 pub vertex_px: f64,
81}
82
83impl Default for PickOptions {
84 fn default() -> Self {
85 Self {
86 double_sided: true,
89 edge_px: EDGE_PICK_PX,
90 vertex_px: VERTEX_PICK_PX,
91 }
92 }
93}
94
95pub fn pick(
99 scene: &RenderScene,
100 camera: &ViewCamera,
101 x: f64,
102 y: f64,
103 options: &PickOptions,
104) -> Vec<PickCandidate> {
105 let ray = camera.pick_ray(x, y);
106 let (_, _, forward) = camera.basis();
107 let persp = matches!(camera.projection, Projection::Perspective { .. });
108
109 let mut hits: Vec<PickCandidate> = Vec::new();
110 for solid in scene.solids() {
111 if !solid.visible {
112 continue;
113 }
114 pick_faces(solid, camera, &ray, forward, options, &mut hits);
115 pick_edges(solid, camera, x, y, forward, persp, options, &mut hits);
116 pick_vertices(solid, camera, x, y, forward, persp, options, &mut hits);
117 }
118
119 hits.sort_by(|a, b| {
120 (a.kind as u8)
121 .cmp(&(b.kind as u8))
122 .then(a.depth.total_cmp(&b.depth))
123 .then(a.screen_dist.total_cmp(&b.screen_dist))
124 });
125 hits.truncate(MAX_CANDIDATES);
126
127 let mut solids_seen: Vec<String> = Vec::new();
129 let mut solid_entries: Vec<PickCandidate> = Vec::new();
130 for hit in &hits {
131 if solids_seen.iter().any(|name| name == &hit.solid) {
132 continue;
133 }
134 solids_seen.push(hit.solid.clone());
135 solid_entries.push(PickCandidate {
136 kind: PickKind::Solid,
137 name: hit.solid.clone(),
138 solid: hit.solid.clone(),
139 depth: hit.depth,
140 screen_dist: hit.screen_dist,
141 position: hit.position,
142 });
143 }
144 hits.extend(solid_entries);
145 hits
146}
147
148pub fn pick_filtered(
157 scene: &RenderScene,
158 camera: &ViewCamera,
159 x: f64,
160 y: f64,
161 options: &PickOptions,
162 filter: &[String],
163) -> Option<PickCandidate> {
164 pick(scene, camera, x, y, options).into_iter().find(|c| {
165 filter.is_empty() || filter.iter().any(|f| f.eq_ignore_ascii_case(c.kind.as_str()))
166 })
167}
168
169fn view_depth(camera: &ViewCamera, forward: [f64; 3], point: [f64; 3]) -> f64 {
170 dot3(sub3(point, camera.eye), forward)
171}
172
173fn pick_faces(
174 solid: &SolidDisplay,
175 camera: &ViewCamera,
176 ray: &Ray,
177 forward: [f64; 3],
178 options: &PickOptions,
179 out: &mut Vec<PickCandidate>,
180) {
181 if solid.mesh.indices.is_empty() {
182 return;
183 }
184 if !ray_hits_aabb(ray, &solid.bbox) {
185 return;
186 }
187 let positions = &solid.mesh.positions;
188 let indices = &solid.mesh.indices;
189 for (index, face) in solid.faces.iter().enumerate() {
190 if !solid.visibility.is_face_visible(index) {
193 continue;
194 }
195 if face.tri_count == 0 {
196 continue;
197 }
198 let mut best: Option<(f64, [f64; 3])> = None;
199 let start = face.tri_start as usize;
200 let end = start + face.tri_count as usize;
201 for tri in start..end.min(indices.len() / 3) {
202 let i0 = indices[tri * 3] as usize;
203 let i1 = indices[tri * 3 + 1] as usize;
204 let i2 = indices[tri * 3 + 2] as usize;
205 let a = to_f64(positions[i0]);
206 let b = to_f64(positions[i1]);
207 let c = to_f64(positions[i2]);
208 if let Some(t) = ray_triangle(ray, a, b, c, options.double_sided) {
209 let point = add3(ray.origin, scale3(ray.dir, t));
210 if best.map(|(bt, _)| t < bt).unwrap_or(true) {
211 best = Some((t, point));
212 }
213 }
214 }
215 if let Some((_, point)) = best {
216 out.push(PickCandidate {
217 kind: PickKind::Face,
218 name: face.name.clone(),
219 solid: solid.name.clone(),
220 depth: view_depth(camera, forward, point),
221 screen_dist: 0.0,
222 position: point,
223 });
224 }
225 }
226}
227
228#[allow(clippy::too_many_arguments)]
229fn pick_edges(
230 solid: &SolidDisplay,
231 camera: &ViewCamera,
232 x: f64,
233 y: f64,
234 forward: [f64; 3],
235 persp: bool,
236 options: &PickOptions,
237 out: &mut Vec<PickCandidate>,
238) {
239 for (index, edge) in solid.edges.iter().enumerate() {
240 if edge.aux || !solid.visibility.is_edge_visible(index) {
244 continue;
245 }
246 let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
248 let mut a = to_f64(pair[0]);
249 let mut b = to_f64(pair[1]);
250 if persp {
251 let da = view_depth(camera, forward, a);
252 let db = view_depth(camera, forward, b);
253 const EPS: f64 = 1e-6;
254 if da <= EPS && db <= EPS {
255 continue;
256 }
257 if da <= EPS || db <= EPS {
258 let t = (EPS - da) / (db - da);
260 let clip = add3(a, scale3(sub3(b, a), t));
261 if da <= EPS {
262 a = clip;
263 } else {
264 b = clip;
265 }
266 }
267 }
268 let (ax, ay, _) = camera.project(a);
269 let (bx, by, _) = camera.project(b);
270 let (dist, t) = point_segment_distance((x, y), (ax, ay), (bx, by));
271 if dist <= options.edge_px {
272 let world = add3(a, scale3(sub3(b, a), t));
273 let depth = view_depth(camera, forward, world);
274 if best
275 .map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
276 .unwrap_or(true)
277 {
278 best = Some((dist, depth, world));
279 }
280 }
281 }
282 if let Some((screen_dist, depth, position)) = best {
283 out.push(PickCandidate {
284 kind: PickKind::Edge,
285 name: edge.name.clone(),
286 solid: solid.name.clone(),
287 depth,
288 screen_dist,
289 position,
290 });
291 }
292 }
293}
294
295#[allow(clippy::too_many_arguments)]
296fn pick_vertices(
297 solid: &SolidDisplay,
298 camera: &ViewCamera,
299 x: f64,
300 y: f64,
301 forward: [f64; 3],
302 persp: bool,
303 options: &PickOptions,
304 out: &mut Vec<PickCandidate>,
305) {
306 for (index, vertex) in solid.vertices.iter().enumerate() {
307 if !solid.visibility.is_vertex_visible(index) {
309 continue;
310 }
311 let depth = view_depth(camera, forward, vertex.position);
312 if persp && depth <= 1e-6 {
313 continue;
314 }
315 let (sx, sy, _) = camera.project(vertex.position);
316 let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
317 if dist <= options.vertex_px {
318 out.push(PickCandidate {
319 kind: PickKind::Vertex,
320 name: String::new(),
321 solid: solid.name.clone(),
322 depth,
323 screen_dist: dist,
324 position: vertex.position,
325 });
326 }
327 }
328}
329
330fn to_f64(p: [f32; 3]) -> [f64; 3] {
331 [p[0] as f64, p[1] as f64, p[2] as f64]
332}
333
334fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
336 let e1 = sub3(b, a);
337 let e2 = sub3(c, a);
338 let pvec = cross3(ray.dir, e2);
339 let det = dot3(e1, pvec);
340 const EPS: f64 = 1e-14;
341 if double_sided {
342 if det.abs() < EPS {
343 return None;
344 }
345 } else if det < EPS {
346 return None;
347 }
348 let inv_det = 1.0 / det;
349 let tvec = sub3(ray.origin, a);
350 let u = dot3(tvec, pvec) * inv_det;
351 if !(-1e-9..=1.0 + 1e-9).contains(&u) {
352 return None;
353 }
354 let qvec = cross3(tvec, e1);
355 let v = dot3(ray.dir, qvec) * inv_det;
356 if v < -1e-9 || u + v > 1.0 + 1e-9 {
357 return None;
358 }
359 let t = dot3(e2, qvec) * inv_det;
360 if t <= 0.0 {
361 return None;
362 }
363 Some(t)
364}
365
366fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
367 if bbox.is_empty() {
368 return false;
369 }
370 let mut t_min = f64::NEG_INFINITY;
371 let mut t_max = f64::INFINITY;
372 for axis in 0..3 {
373 let dir = ray.dir[axis];
374 let origin = ray.origin[axis];
375 if dir.abs() < 1e-15 {
376 if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
377 return false;
378 }
379 continue;
380 }
381 let inv = 1.0 / dir;
382 let t0 = (bbox.min[axis] - origin) * inv;
383 let t1 = (bbox.max[axis] - origin) * inv;
384 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
385 t_min = t_min.max(lo);
386 t_max = t_max.min(hi);
387 if t_min > t_max {
388 return false;
389 }
390 }
391 t_max > 0.0
392}
393
394pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
396 let list: Vec<serde_json::Value> = candidates
397 .iter()
398 .map(|c| {
399 serde_json::json!({
400 "kind": c.kind.as_str(),
401 "name": c.name,
402 "solid": c.solid,
403 "depth": c.depth,
404 "screenDist": c.screen_dist,
405 "position": c.position,
406 })
407 })
408 .collect();
409 serde_json::Value::Array(list).to_string()
410}
411