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 nearest_face_depth(scene: &RenderScene, camera: &ViewCamera, x: f64, y: f64, options: &PickOptions) -> Option<f64> {
98 pick(scene, camera, x, y, options)
99 .into_iter()
100 .filter(|candidate| candidate.kind == PickKind::Face)
101 .map(|candidate| candidate.depth)
102 .min_by(f64::total_cmp)
103}
104
105pub fn pick_unoccluded(
111 scene: &RenderScene,
112 context: &RenderScene,
113 camera: &ViewCamera,
114 x: f64,
115 y: f64,
116 options: &PickOptions,
117) -> Vec<PickCandidate> {
118 let mut hits = pick(scene, camera, x, y, options);
119 if context.solids().is_empty() {
120 return hits;
121 }
122 if let Some(front) = nearest_face_depth(context, camera, x, y, options) {
123 let tolerance = 1e-6 * front.abs().max(1.0) + 1e-4;
124 hits.retain(|candidate| candidate.depth <= front + tolerance);
125 }
126 hits
127}
128
129pub fn pick(
133 scene: &RenderScene,
134 camera: &ViewCamera,
135 x: f64,
136 y: f64,
137 options: &PickOptions,
138) -> Vec<PickCandidate> {
139 let ray = camera.pick_ray(x, y);
140 let (_, _, forward) = camera.basis();
141 let persp = matches!(camera.projection, Projection::Perspective { .. });
142
143 let mut hits: Vec<PickCandidate> = Vec::new();
144 let mut triangles = Vec::new();
145 for solid in scene.solids() {
146 if !solid.visible {
147 continue;
148 }
149 if ray_hits_aabb(&ray, &solid.bbox) {
150 let candidates = if solid.mesh.indices.len() / 3 > 128 {
154 triangles.clear();
155 scene.pick_triangles(solid, &ray, &mut triangles);
156 Some(triangles.as_slice())
157 } else {
158 None
159 };
160 pick_faces(solid, camera, &ray, forward, options, candidates, &mut hits);
161 }
162 pick_edges(solid, camera, x, y, forward, persp, options, &mut hits);
163 pick_vertices(solid, camera, x, y, forward, persp, options, &mut hits);
164 }
165
166 hits.sort_by(|a, b| {
167 (a.kind as u8)
168 .cmp(&(b.kind as u8))
169 .then(a.depth.total_cmp(&b.depth))
170 .then(a.screen_dist.total_cmp(&b.screen_dist))
171 });
172 hits.truncate(MAX_CANDIDATES);
173
174 let mut solids_seen: Vec<String> = Vec::new();
177 let mut solid_entries: Vec<PickCandidate> = Vec::new();
178 for hit in &hits {
179 if solids_seen.iter().any(|name| name == &hit.solid) {
180 continue;
181 }
182 solids_seen.push(hit.solid.clone());
183 solid_entries.push(PickCandidate {
184 kind: PickKind::Solid,
185 name: hit.solid.clone(),
186 solid: hit.solid.clone(),
187 depth: hit.depth,
188 screen_dist: hit.screen_dist,
189 position: hit.position,
190 });
191 }
192 hits.retain(|hit| hit.kind != PickKind::Solid);
193 hits.extend(solid_entries);
194 hits
195}
196
197pub fn pick_filtered(
206 scene: &RenderScene,
207 camera: &ViewCamera,
208 x: f64,
209 y: f64,
210 options: &PickOptions,
211 filter: &[String],
212) -> Option<PickCandidate> {
213 pick(scene, camera, x, y, options).into_iter().find(|c| {
214 filter.is_empty() || filter.iter().any(|f| f.eq_ignore_ascii_case(c.kind.as_str()))
215 })
216}
217
218fn view_depth(camera: &ViewCamera, forward: [f64; 3], point: [f64; 3]) -> f64 {
219 dot3(sub3(point, camera.eye), forward)
220}
221
222#[allow(clippy::too_many_arguments)]
223fn pick_faces(
224 solid: &SolidDisplay,
225 camera: &ViewCamera,
226 ray: &Ray,
227 forward: [f64; 3],
228 options: &PickOptions,
229 candidates: Option<&[usize]>,
230 out: &mut Vec<PickCandidate>,
231) {
232 if solid.mesh.indices.is_empty() {
233 return;
234 }
235 if !ray_hits_aabb(ray, &solid.bbox) {
236 return;
237 }
238 let positions = &solid.mesh.positions;
239 let indices = &solid.mesh.indices;
240 let first_hit = out.len();
241 let mut cap_depth: Option<f64> = None;
242 for (index, face) in solid.faces.iter().enumerate() {
243 if !solid.visibility.is_face_visible(index) {
246 continue;
247 }
248 if face.tri_count == 0 {
249 continue;
250 }
251 let mut best: Option<(f64, [f64; 3])> = None;
252 let start = face.tri_start as usize;
253 let end = start + face.tri_count as usize;
254 let end = end.min(indices.len() / 3);
255 let mut test_triangle = |tri: usize| {
256 let i0 = indices[tri * 3] as usize;
257 let i1 = indices[tri * 3 + 1] as usize;
258 let i2 = indices[tri * 3 + 2] as usize;
259 let a = to_f64(positions[i0]);
260 let b = to_f64(positions[i1]);
261 let c = to_f64(positions[i2]);
262 if let Some(t) = ray_triangle(ray, a, b, c, options.double_sided) {
263 let point = add3(ray.origin, scale3(ray.dir, t));
264 if best.map(|(bt, _)| t < bt).unwrap_or(true) {
265 best = Some((t, point));
266 }
267 }
268 };
269 match candidates {
270 Some(triangles) => {
271 let first = triangles.partition_point(|&tri| tri < start);
272 let last = triangles.partition_point(|&tri| tri < end);
273 for &tri in &triangles[first..last] {
274 test_triangle(tri);
275 }
276 }
277 None => {
278 for tri in start..end {
279 test_triangle(tri);
280 }
281 }
282 }
283 if let Some((_, point)) = best {
284 let depth = view_depth(camera, forward, point);
285 if face.kind == crate::scene::FaceKind::SectionCap {
288 cap_depth = Some(cap_depth.map_or(depth, |d| d.min(depth)));
289 out.push(PickCandidate {
290 kind: PickKind::Solid,
291 name: solid.name.clone(),
292 solid: solid.name.clone(),
293 depth,
294 screen_dist: 0.0,
295 position: point,
296 });
297 continue;
298 }
299 out.push(PickCandidate {
300 kind: PickKind::Face,
301 name: face.name.clone(),
302 solid: solid.name.clone(),
303 depth,
304 screen_dist: 0.0,
305 position: point,
306 });
307 }
308 }
309 if let Some(cap) = cap_depth {
312 let tolerance = 1e-6 * cap.abs().max(1.0);
313 let mut index = first_hit;
314 while index < out.len() {
315 if out[index].kind == PickKind::Face && out[index].depth > cap + tolerance {
316 out.remove(index);
317 } else {
318 index += 1;
319 }
320 }
321 }
322}
323
324#[allow(clippy::too_many_arguments)]
325fn pick_edges(
326 solid: &SolidDisplay,
327 camera: &ViewCamera,
328 x: f64,
329 y: f64,
330 forward: [f64; 3],
331 persp: bool,
332 options: &PickOptions,
333 out: &mut Vec<PickCandidate>,
334) {
335 for (index, edge) in solid.edges.iter().enumerate() {
336 if edge.aux || !solid.visibility.is_edge_visible(index) {
340 continue;
341 }
342 let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
344 let mut a = to_f64(pair[0]);
345 let mut b = to_f64(pair[1]);
346 if persp {
347 let da = view_depth(camera, forward, a);
348 let db = view_depth(camera, forward, b);
349 const EPS: f64 = 1e-6;
350 if da <= EPS && db <= EPS {
351 continue;
352 }
353 if da <= EPS || db <= EPS {
354 let t = (EPS - da) / (db - da);
356 let clip = add3(a, scale3(sub3(b, a), t));
357 if da <= EPS {
358 a = clip;
359 } else {
360 b = clip;
361 }
362 }
363 }
364 let (ax, ay, _) = camera.project(a);
365 let (bx, by, _) = camera.project(b);
366 let (dist, t) = point_segment_distance((x, y), (ax, ay), (bx, by));
367 if dist <= options.edge_px {
368 let world = add3(a, scale3(sub3(b, a), t));
369 let depth = view_depth(camera, forward, world);
370 if best
371 .map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
372 .unwrap_or(true)
373 {
374 best = Some((dist, depth, world));
375 }
376 }
377 }
378 if let Some((screen_dist, depth, position)) = best {
379 out.push(PickCandidate {
380 kind: PickKind::Edge,
381 name: edge.name.clone(),
382 solid: solid.name.clone(),
383 depth,
384 screen_dist,
385 position,
386 });
387 }
388 }
389}
390
391#[allow(clippy::too_many_arguments)]
392fn pick_vertices(
393 solid: &SolidDisplay,
394 camera: &ViewCamera,
395 x: f64,
396 y: f64,
397 forward: [f64; 3],
398 persp: bool,
399 options: &PickOptions,
400 out: &mut Vec<PickCandidate>,
401) {
402 for (index, vertex) in solid.vertices.iter().enumerate() {
403 if !solid.visibility.is_vertex_visible(index) {
405 continue;
406 }
407 let depth = view_depth(camera, forward, vertex.position);
408 if persp && depth <= 1e-6 {
409 continue;
410 }
411 let (sx, sy, _) = camera.project(vertex.position);
412 let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
413 if dist <= options.vertex_px {
414 out.push(PickCandidate {
415 kind: PickKind::Vertex,
416 name: String::new(),
417 solid: solid.name.clone(),
418 depth,
419 screen_dist: dist,
420 position: vertex.position,
421 });
422 }
423 }
424}
425
426fn to_f64(p: [f32; 3]) -> [f64; 3] {
427 [p[0] as f64, p[1] as f64, p[2] as f64]
428}
429
430fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
432 let e1 = sub3(b, a);
433 let e2 = sub3(c, a);
434 let pvec = cross3(ray.dir, e2);
435 let det = dot3(e1, pvec);
436 const EPS: f64 = 1e-14;
437 if double_sided {
438 if det.abs() < EPS {
439 return None;
440 }
441 } else if det < EPS {
442 return None;
443 }
444 let inv_det = 1.0 / det;
445 let tvec = sub3(ray.origin, a);
446 let u = dot3(tvec, pvec) * inv_det;
447 if !(-1e-9..=1.0 + 1e-9).contains(&u) {
448 return None;
449 }
450 let qvec = cross3(tvec, e1);
451 let v = dot3(ray.dir, qvec) * inv_det;
452 if v < -1e-9 || u + v > 1.0 + 1e-9 {
453 return None;
454 }
455 let t = dot3(e2, qvec) * inv_det;
456 if t <= 0.0 {
457 return None;
458 }
459 Some(t)
460}
461
462fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
463 if bbox.is_empty() {
464 return false;
465 }
466 let mut t_min = f64::NEG_INFINITY;
467 let mut t_max = f64::INFINITY;
468 for axis in 0..3 {
469 let dir = ray.dir[axis];
470 let origin = ray.origin[axis];
471 if dir.abs() < 1e-15 {
472 if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
473 return false;
474 }
475 continue;
476 }
477 let inv = 1.0 / dir;
478 let t0 = (bbox.min[axis] - origin) * inv;
479 let t1 = (bbox.max[axis] - origin) * inv;
480 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
481 t_min = t_min.max(lo);
482 t_max = t_max.min(hi);
483 if t_min > t_max {
484 return false;
485 }
486 }
487 t_max > 0.0
488}
489
490pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
492 let list: Vec<serde_json::Value> = candidates
493 .iter()
494 .map(|c| {
495 serde_json::json!({
496 "kind": c.kind.as_str(),
497 "name": c.name,
498 "solid": c.solid,
499 "depth": c.depth,
500 "screenDist": c.screen_dist,
501 "position": c.position,
502 })
503 })
504 .collect();
505 serde_json::Value::Array(list).to_string()
506}
507