1use crate::scene::{RenderScene, SolidDisplay};
14use crate::view::{add3, cross3, dot3, scale3, sub3, Projection, Ray, ViewCamera};
15
16pub const EDGE_PICK_PX: f64 = 6.0;
18pub const VERTEX_PICK_PX: f64 = 6.0;
19const MAX_CANDIDATES: usize = 16;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum PickKind {
23 Vertex = 0,
24 Edge = 1,
25 Face = 2,
26 Solid = 3,
27}
28
29impl PickKind {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 PickKind::Vertex => "VERTEX",
33 PickKind::Edge => "EDGE",
34 PickKind::Face => "FACE",
35 PickKind::Solid => "SOLID",
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct PickCandidate {
42 pub kind: PickKind,
43 pub name: String,
46 pub solid: String,
48 pub depth: f64,
50 pub screen_dist: f64,
52 pub position: [f64; 3],
54}
55
56#[derive(Debug, Clone, Copy)]
57pub struct PickOptions {
58 pub double_sided: bool,
59 pub edge_px: f64,
60 pub vertex_px: f64,
61}
62
63impl Default for PickOptions {
64 fn default() -> Self {
65 Self {
66 double_sided: true,
69 edge_px: EDGE_PICK_PX,
70 vertex_px: VERTEX_PICK_PX,
71 }
72 }
73}
74
75pub fn pick(
79 scene: &RenderScene,
80 camera: &ViewCamera,
81 x: f64,
82 y: f64,
83 options: &PickOptions,
84) -> Vec<PickCandidate> {
85 let ray = camera.pick_ray(x, y);
86 let (_, _, forward) = camera.basis();
87 let persp = matches!(camera.projection, Projection::Perspective { .. });
88
89 let mut hits: Vec<PickCandidate> = Vec::new();
90 for solid in scene.solids() {
91 if !solid.visible {
92 continue;
93 }
94 pick_faces(solid, camera, &ray, forward, options, &mut hits);
95 pick_edges(solid, camera, x, y, forward, persp, options, &mut hits);
96 pick_vertices(solid, camera, x, y, forward, persp, options, &mut hits);
97 }
98
99 hits.sort_by(|a, b| {
100 (a.kind as u8)
101 .cmp(&(b.kind as u8))
102 .then(a.depth.total_cmp(&b.depth))
103 .then(a.screen_dist.total_cmp(&b.screen_dist))
104 });
105 hits.truncate(MAX_CANDIDATES);
106
107 let mut solids_seen: Vec<String> = Vec::new();
109 let mut solid_entries: Vec<PickCandidate> = Vec::new();
110 for hit in &hits {
111 if solids_seen.iter().any(|name| name == &hit.solid) {
112 continue;
113 }
114 solids_seen.push(hit.solid.clone());
115 solid_entries.push(PickCandidate {
116 kind: PickKind::Solid,
117 name: hit.solid.clone(),
118 solid: hit.solid.clone(),
119 depth: hit.depth,
120 screen_dist: hit.screen_dist,
121 position: hit.position,
122 });
123 }
124 hits.extend(solid_entries);
125 hits
126}
127
128pub fn pick_filtered(
137 scene: &RenderScene,
138 camera: &ViewCamera,
139 x: f64,
140 y: f64,
141 options: &PickOptions,
142 filter: &[String],
143) -> Option<PickCandidate> {
144 pick(scene, camera, x, y, options).into_iter().find(|c| {
145 filter.is_empty() || filter.iter().any(|f| f.eq_ignore_ascii_case(c.kind.as_str()))
146 })
147}
148
149fn view_depth(camera: &ViewCamera, forward: [f64; 3], point: [f64; 3]) -> f64 {
150 dot3(sub3(point, camera.eye), forward)
151}
152
153fn pick_faces(
154 solid: &SolidDisplay,
155 camera: &ViewCamera,
156 ray: &Ray,
157 forward: [f64; 3],
158 options: &PickOptions,
159 out: &mut Vec<PickCandidate>,
160) {
161 if solid.mesh.indices.is_empty() {
162 return;
163 }
164 if !ray_hits_aabb(ray, &solid.bbox) {
165 return;
166 }
167 let positions = &solid.mesh.positions;
168 let indices = &solid.mesh.indices;
169 for (index, face) in solid.faces.iter().enumerate() {
170 if !solid.visibility.is_face_visible(index) {
173 continue;
174 }
175 if face.tri_count == 0 {
176 continue;
177 }
178 let mut best: Option<(f64, [f64; 3])> = None;
179 let start = face.tri_start as usize;
180 let end = start + face.tri_count as usize;
181 for tri in start..end.min(indices.len() / 3) {
182 let i0 = indices[tri * 3] as usize;
183 let i1 = indices[tri * 3 + 1] as usize;
184 let i2 = indices[tri * 3 + 2] as usize;
185 let a = to_f64(positions[i0]);
186 let b = to_f64(positions[i1]);
187 let c = to_f64(positions[i2]);
188 if let Some(t) = ray_triangle(ray, a, b, c, options.double_sided) {
189 let point = add3(ray.origin, scale3(ray.dir, t));
190 if best.map(|(bt, _)| t < bt).unwrap_or(true) {
191 best = Some((t, point));
192 }
193 }
194 }
195 if let Some((_, point)) = best {
196 out.push(PickCandidate {
197 kind: PickKind::Face,
198 name: face.name.clone(),
199 solid: solid.name.clone(),
200 depth: view_depth(camera, forward, point),
201 screen_dist: 0.0,
202 position: point,
203 });
204 }
205 }
206}
207
208#[allow(clippy::too_many_arguments)]
209fn pick_edges(
210 solid: &SolidDisplay,
211 camera: &ViewCamera,
212 x: f64,
213 y: f64,
214 forward: [f64; 3],
215 persp: bool,
216 options: &PickOptions,
217 out: &mut Vec<PickCandidate>,
218) {
219 for (index, edge) in solid.edges.iter().enumerate() {
220 if !solid.visibility.is_edge_visible(index) {
222 continue;
223 }
224 let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
226 let mut a = to_f64(pair[0]);
227 let mut b = to_f64(pair[1]);
228 if persp {
229 let da = view_depth(camera, forward, a);
230 let db = view_depth(camera, forward, b);
231 const EPS: f64 = 1e-6;
232 if da <= EPS && db <= EPS {
233 continue;
234 }
235 if da <= EPS || db <= EPS {
236 let t = (EPS - da) / (db - da);
238 let clip = add3(a, scale3(sub3(b, a), t));
239 if da <= EPS {
240 a = clip;
241 } else {
242 b = clip;
243 }
244 }
245 }
246 let (ax, ay, _) = camera.project(a);
247 let (bx, by, _) = camera.project(b);
248 let (dist, t) = point_segment_distance_2d(x, y, ax, ay, bx, by);
249 if dist <= options.edge_px {
250 let world = add3(a, scale3(sub3(b, a), t));
251 let depth = view_depth(camera, forward, world);
252 if best
253 .map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
254 .unwrap_or(true)
255 {
256 best = Some((dist, depth, world));
257 }
258 }
259 }
260 if let Some((screen_dist, depth, position)) = best {
261 out.push(PickCandidate {
262 kind: PickKind::Edge,
263 name: edge.name.clone(),
264 solid: solid.name.clone(),
265 depth,
266 screen_dist,
267 position,
268 });
269 }
270 }
271}
272
273#[allow(clippy::too_many_arguments)]
274fn pick_vertices(
275 solid: &SolidDisplay,
276 camera: &ViewCamera,
277 x: f64,
278 y: f64,
279 forward: [f64; 3],
280 persp: bool,
281 options: &PickOptions,
282 out: &mut Vec<PickCandidate>,
283) {
284 for (index, vertex) in solid.vertices.iter().enumerate() {
285 if !solid.visibility.is_vertex_visible(index) {
287 continue;
288 }
289 let depth = view_depth(camera, forward, vertex.position);
290 if persp && depth <= 1e-6 {
291 continue;
292 }
293 let (sx, sy, _) = camera.project(vertex.position);
294 let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
295 if dist <= options.vertex_px {
296 out.push(PickCandidate {
297 kind: PickKind::Vertex,
298 name: String::new(),
299 solid: solid.name.clone(),
300 depth,
301 screen_dist: dist,
302 position: vertex.position,
303 });
304 }
305 }
306}
307
308fn to_f64(p: [f32; 3]) -> [f64; 3] {
309 [p[0] as f64, p[1] as f64, p[2] as f64]
310}
311
312fn point_segment_distance_2d(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> (f64, f64) {
315 let abx = bx - ax;
316 let aby = by - ay;
317 let len2 = abx * abx + aby * aby;
318 let t = if len2 <= 1e-18 {
319 0.0
320 } else {
321 (((px - ax) * abx + (py - ay) * aby) / len2).clamp(0.0, 1.0)
322 };
323 let cx = ax + abx * t;
324 let cy = ay + aby * t;
325 (((px - cx).powi(2) + (py - cy).powi(2)).sqrt(), t)
326}
327
328fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
330 let e1 = sub3(b, a);
331 let e2 = sub3(c, a);
332 let pvec = cross3(ray.dir, e2);
333 let det = dot3(e1, pvec);
334 const EPS: f64 = 1e-14;
335 if double_sided {
336 if det.abs() < EPS {
337 return None;
338 }
339 } else if det < EPS {
340 return None;
341 }
342 let inv_det = 1.0 / det;
343 let tvec = sub3(ray.origin, a);
344 let u = dot3(tvec, pvec) * inv_det;
345 if !(-1e-9..=1.0 + 1e-9).contains(&u) {
346 return None;
347 }
348 let qvec = cross3(tvec, e1);
349 let v = dot3(ray.dir, qvec) * inv_det;
350 if v < -1e-9 || u + v > 1.0 + 1e-9 {
351 return None;
352 }
353 let t = dot3(e2, qvec) * inv_det;
354 if t <= 0.0 {
355 return None;
356 }
357 Some(t)
358}
359
360fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
361 if bbox.is_empty() {
362 return false;
363 }
364 let mut t_min = f64::NEG_INFINITY;
365 let mut t_max = f64::INFINITY;
366 for axis in 0..3 {
367 let dir = ray.dir[axis];
368 let origin = ray.origin[axis];
369 if dir.abs() < 1e-15 {
370 if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
371 return false;
372 }
373 continue;
374 }
375 let inv = 1.0 / dir;
376 let t0 = (bbox.min[axis] - origin) * inv;
377 let t1 = (bbox.max[axis] - origin) * inv;
378 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
379 t_min = t_min.max(lo);
380 t_max = t_max.min(hi);
381 if t_min > t_max {
382 return false;
383 }
384 }
385 t_max > 0.0
386}
387
388pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
390 let list: Vec<serde_json::Value> = candidates
391 .iter()
392 .map(|c| {
393 serde_json::json!({
394 "kind": c.kind.as_str(),
395 "name": c.name,
396 "solid": c.solid,
397 "depth": c.depth,
398 "screenDist": c.screen_dist,
399 "position": c.position,
400 })
401 })
402 .collect();
403 serde_json::Value::Array(list).to_string()
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use crate::pipeline::scene_from_history_json;
410
411 fn cube_scene() -> RenderScene {
412 let request = serde_json::json!({
413 "expressions": "",
414 "configurator": {},
415 "features": [{
416 "type": "P.CU",
417 "inputParams": {
418 "id": "PickCube",
419 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
420 "transform": {
421 "position": [0.0, 0.0, 0.0],
422 "rotationEuler": [0.0, 0.0, 0.0],
423 "scale": [1.0, 1.0, 1.0]
424 },
425 "boolean": { "targets": [], "operation": "NONE" }
426 },
427 "persistentData": {}
428 }]
429 })
430 .to_string();
431 let (scene, report) = scene_from_history_json(&request).unwrap();
432 assert!(report.feature_errors.is_empty(), "{:?}", report.feature_errors);
433 scene
434 }
435
436 fn front_camera() -> ViewCamera {
437 ViewCamera {
439 eye: [5.0, 5.0, 50.0],
440 target: [5.0, 5.0, 5.0],
441 up: [0.0, 1.0, 0.0],
442 projection: Projection::Orthographic { half_height: 10.0 },
443 width: 800.0,
444 height: 600.0,
445 near: -1000.0,
446 far: 1000.0,
447 }
448 }
449
450 #[test]
451 fn face_pick_at_center_is_deterministic() {
452 let scene = cube_scene();
453 let camera = front_camera();
454 let first = pick(&scene, &camera, 400.0, 300.0, &PickOptions::default());
455 let second = pick(&scene, &camera, 400.0, 300.0, &PickOptions::default());
456 assert!(!first.is_empty());
457 assert_eq!(first.len(), second.len());
458 for (a, b) in first.iter().zip(second.iter()) {
459 assert_eq!(a.kind, b.kind);
460 assert_eq!(a.name, b.name);
461 assert_eq!(a.depth.to_bits(), b.depth.to_bits(), "deterministic depth");
462 }
463 assert_eq!(first[0].kind, PickKind::Face);
466 assert!(!first[0].name.is_empty(), "face has a kernel name");
467 assert_eq!(first.last().unwrap().kind, PickKind::Solid);
468 assert_eq!(first.last().unwrap().name, "PickCube");
469 assert!(first[0].depth < first[1].depth);
471 assert!((first[0].position[2] - 10.0).abs() < 1e-9);
472 }
473
474 #[test]
475 fn edge_and_vertex_priority_ranking() {
476 let scene = cube_scene();
477 let camera = front_camera();
478 let (cx, cy, _) = camera.project([10.0, 10.0, 10.0]);
480 let hits = pick(&scene, &camera, cx, cy, &PickOptions::default());
481 assert!(!hits.is_empty());
482 assert_eq!(hits[0].kind, PickKind::Vertex);
484 assert!(crate::view::len3(crate::view::sub3(hits[0].position, [10.0, 10.0, 10.0])) < 1e-9);
485 assert!(hits.iter().any(|h| h.kind == PickKind::Edge));
486 assert!(hits.iter().any(|h| h.kind == PickKind::Face));
487 assert_eq!(hits.last().unwrap().kind, PickKind::Solid);
488
489 let (ex, ey, _) = camera.project([5.0, 10.0, 10.0]);
491 let hits = pick(&scene, &camera, ex, ey, &PickOptions::default());
492 assert_eq!(hits[0].kind, PickKind::Edge);
493 assert!(!hits[0].name.is_empty(), "edge has a kernel name");
494 }
495
496 #[test]
497 fn filtered_pick_constrains_to_kind() {
498 let scene = cube_scene();
499 let camera = front_camera();
500 let face = pick_filtered(&scene, &camera, 400.0, 300.0, &PickOptions::default(), &["FACE".into()]);
503 assert_eq!(face.as_ref().unwrap().kind, PickKind::Face);
504 let solid = pick_filtered(&scene, &camera, 400.0, 300.0, &PickOptions::default(), &["SOLID".into()]);
505 assert_eq!(solid.as_ref().unwrap().kind, PickKind::Solid);
506 assert_eq!(solid.unwrap().name, "PickCube");
507 let any = pick_filtered(&scene, &camera, 400.0, 300.0, &PickOptions::default(), &[]);
509 assert_eq!(any.unwrap().kind, PickKind::Face);
510 let lower = pick_filtered(&scene, &camera, 400.0, 300.0, &PickOptions::default(), &["solid".into()]);
511 assert_eq!(lower.unwrap().kind, PickKind::Solid);
512 assert!(pick_filtered(&scene, &camera, 10.0, 10.0, &PickOptions::default(), &["SOLID".into()]).is_none());
514 }
515
516 #[test]
517 fn miss_returns_empty() {
518 let scene = cube_scene();
519 let camera = front_camera();
520 let hits = pick(&scene, &camera, 10.0, 10.0, &PickOptions::default());
521 assert!(hits.is_empty(), "{hits:?}");
522 }
523
524 #[test]
528 fn hidden_entities_are_not_pickable() {
529 use crate::visibility::EntityKind;
530 let mut scene = cube_scene();
531 let camera = front_camera();
532
533 let before = pick(&scene, &camera, 400.0, 300.0, &PickOptions::default());
536 assert_eq!(before[0].kind, PickKind::Face);
537 let front_name = before[0].name.clone();
538 let front_index = scene
539 .solids()
540 .iter()
541 .find(|s| s.name == "PickCube")
542 .unwrap()
543 .faces
544 .iter()
545 .position(|f| f.name == front_name)
546 .unwrap();
547 scene
548 .solid_mut("PickCube")
549 .unwrap()
550 .visibility
551 .set_visible(EntityKind::Face, front_index, false);
552 let after = pick(&scene, &camera, 400.0, 300.0, &PickOptions::default());
553 assert!(
554 after
555 .iter()
556 .all(|c| !(c.kind == PickKind::Face && c.name == front_name)),
557 "hidden face still picked: {after:?}"
558 );
559 assert_eq!(after[0].kind, PickKind::Face, "the back face is still pickable");
560 assert!((after[0].position[2] - 0.0).abs() < 1e-9, "back face at z=0");
561
562 let vertex_count = scene.solids()[0].vertices.len();
565 scene
566 .solid_mut("PickCube")
567 .unwrap()
568 .visibility
569 .set_group_visible(EntityKind::Vertex, vertex_count, false);
570 let (cx, cy, _) = camera.project([10.0, 10.0, 10.0]);
571 let corner = pick(&scene, &camera, cx, cy, &PickOptions::default());
572 assert!(
573 corner.iter().all(|c| c.kind != PickKind::Vertex),
574 "hidden vertices still picked: {corner:?}"
575 );
576 assert_eq!(corner[0].kind, PickKind::Edge, "edges now outrank the hidden vertex");
577
578 let edge_count = scene.solids()[0].edges.len();
580 scene
581 .solid_mut("PickCube")
582 .unwrap()
583 .visibility
584 .set_group_visible(EntityKind::Edge, edge_count, false);
585 let corner = pick(&scene, &camera, cx, cy, &PickOptions::default());
586 assert!(corner.iter().all(|c| c.kind != PickKind::Edge));
587 assert!(corner.iter().any(|c| c.kind == PickKind::Face));
588 }
589
590 #[test]
591 fn hidden_solid_is_not_pickable() {
592 let mut scene = cube_scene();
593 scene.solid_mut("PickCube").unwrap().visible = false;
594 let camera = front_camera();
595 let hits = pick(&scene, &camera, 400.0, 300.0, &PickOptions::default());
596 assert!(hits.is_empty());
597 }
598}