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