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 let mut triangles = Vec::new();
111 for solid in scene.solids() {
112 if !solid.visible {
113 continue;
114 }
115 if ray_hits_aabb(&ray, &solid.bbox) {
116 let candidates = if solid.mesh.indices.len() / 3 > 128 {
120 triangles.clear();
121 scene.pick_triangles(solid, &ray, &mut triangles);
122 Some(triangles.as_slice())
123 } else {
124 None
125 };
126 pick_faces(solid, camera, &ray, forward, options, candidates, &mut hits);
127 }
128 pick_edges(solid, camera, x, y, forward, persp, options, &mut hits);
129 pick_vertices(solid, camera, x, y, forward, persp, options, &mut hits);
130 }
131
132 hits.sort_by(|a, b| {
133 (a.kind as u8)
134 .cmp(&(b.kind as u8))
135 .then(a.depth.total_cmp(&b.depth))
136 .then(a.screen_dist.total_cmp(&b.screen_dist))
137 });
138 hits.truncate(MAX_CANDIDATES);
139
140 let mut solids_seen: Vec<String> = Vec::new();
142 let mut solid_entries: Vec<PickCandidate> = Vec::new();
143 for hit in &hits {
144 if solids_seen.iter().any(|name| name == &hit.solid) {
145 continue;
146 }
147 solids_seen.push(hit.solid.clone());
148 solid_entries.push(PickCandidate {
149 kind: PickKind::Solid,
150 name: hit.solid.clone(),
151 solid: hit.solid.clone(),
152 depth: hit.depth,
153 screen_dist: hit.screen_dist,
154 position: hit.position,
155 });
156 }
157 hits.extend(solid_entries);
158 hits
159}
160
161pub fn pick_filtered(
170 scene: &RenderScene,
171 camera: &ViewCamera,
172 x: f64,
173 y: f64,
174 options: &PickOptions,
175 filter: &[String],
176) -> Option<PickCandidate> {
177 pick(scene, camera, x, y, options).into_iter().find(|c| {
178 filter.is_empty() || filter.iter().any(|f| f.eq_ignore_ascii_case(c.kind.as_str()))
179 })
180}
181
182fn view_depth(camera: &ViewCamera, forward: [f64; 3], point: [f64; 3]) -> f64 {
183 dot3(sub3(point, camera.eye), forward)
184}
185
186#[allow(clippy::too_many_arguments)]
187fn pick_faces(
188 solid: &SolidDisplay,
189 camera: &ViewCamera,
190 ray: &Ray,
191 forward: [f64; 3],
192 options: &PickOptions,
193 candidates: Option<&[usize]>,
194 out: &mut Vec<PickCandidate>,
195) {
196 if solid.mesh.indices.is_empty() {
197 return;
198 }
199 if !ray_hits_aabb(ray, &solid.bbox) {
200 return;
201 }
202 let positions = &solid.mesh.positions;
203 let indices = &solid.mesh.indices;
204 for (index, face) in solid.faces.iter().enumerate() {
205 if !solid.visibility.is_face_visible(index) {
208 continue;
209 }
210 if face.tri_count == 0 {
211 continue;
212 }
213 let mut best: Option<(f64, [f64; 3])> = None;
214 let start = face.tri_start as usize;
215 let end = start + face.tri_count as usize;
216 let end = end.min(indices.len() / 3);
217 let mut test_triangle = |tri: usize| {
218 let i0 = indices[tri * 3] as usize;
219 let i1 = indices[tri * 3 + 1] as usize;
220 let i2 = indices[tri * 3 + 2] as usize;
221 let a = to_f64(positions[i0]);
222 let b = to_f64(positions[i1]);
223 let c = to_f64(positions[i2]);
224 if let Some(t) = ray_triangle(ray, a, b, c, options.double_sided) {
225 let point = add3(ray.origin, scale3(ray.dir, t));
226 if best.map(|(bt, _)| t < bt).unwrap_or(true) {
227 best = Some((t, point));
228 }
229 }
230 };
231 match candidates {
232 Some(triangles) => {
233 let first = triangles.partition_point(|&tri| tri < start);
234 let last = triangles.partition_point(|&tri| tri < end);
235 for &tri in &triangles[first..last] {
236 test_triangle(tri);
237 }
238 }
239 None => {
240 for tri in start..end {
241 test_triangle(tri);
242 }
243 }
244 }
245 if let Some((_, point)) = best {
246 out.push(PickCandidate {
247 kind: PickKind::Face,
248 name: face.name.clone(),
249 solid: solid.name.clone(),
250 depth: view_depth(camera, forward, point),
251 screen_dist: 0.0,
252 position: point,
253 });
254 }
255 }
256}
257
258#[allow(clippy::too_many_arguments)]
259fn pick_edges(
260 solid: &SolidDisplay,
261 camera: &ViewCamera,
262 x: f64,
263 y: f64,
264 forward: [f64; 3],
265 persp: bool,
266 options: &PickOptions,
267 out: &mut Vec<PickCandidate>,
268) {
269 for (index, edge) in solid.edges.iter().enumerate() {
270 if edge.aux || !solid.visibility.is_edge_visible(index) {
274 continue;
275 }
276 let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
278 let mut a = to_f64(pair[0]);
279 let mut b = to_f64(pair[1]);
280 if persp {
281 let da = view_depth(camera, forward, a);
282 let db = view_depth(camera, forward, b);
283 const EPS: f64 = 1e-6;
284 if da <= EPS && db <= EPS {
285 continue;
286 }
287 if da <= EPS || db <= EPS {
288 let t = (EPS - da) / (db - da);
290 let clip = add3(a, scale3(sub3(b, a), t));
291 if da <= EPS {
292 a = clip;
293 } else {
294 b = clip;
295 }
296 }
297 }
298 let (ax, ay, _) = camera.project(a);
299 let (bx, by, _) = camera.project(b);
300 let (dist, t) = point_segment_distance((x, y), (ax, ay), (bx, by));
301 if dist <= options.edge_px {
302 let world = add3(a, scale3(sub3(b, a), t));
303 let depth = view_depth(camera, forward, world);
304 if best
305 .map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
306 .unwrap_or(true)
307 {
308 best = Some((dist, depth, world));
309 }
310 }
311 }
312 if let Some((screen_dist, depth, position)) = best {
313 out.push(PickCandidate {
314 kind: PickKind::Edge,
315 name: edge.name.clone(),
316 solid: solid.name.clone(),
317 depth,
318 screen_dist,
319 position,
320 });
321 }
322 }
323}
324
325#[allow(clippy::too_many_arguments)]
326fn pick_vertices(
327 solid: &SolidDisplay,
328 camera: &ViewCamera,
329 x: f64,
330 y: f64,
331 forward: [f64; 3],
332 persp: bool,
333 options: &PickOptions,
334 out: &mut Vec<PickCandidate>,
335) {
336 for (index, vertex) in solid.vertices.iter().enumerate() {
337 if !solid.visibility.is_vertex_visible(index) {
339 continue;
340 }
341 let depth = view_depth(camera, forward, vertex.position);
342 if persp && depth <= 1e-6 {
343 continue;
344 }
345 let (sx, sy, _) = camera.project(vertex.position);
346 let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
347 if dist <= options.vertex_px {
348 out.push(PickCandidate {
349 kind: PickKind::Vertex,
350 name: String::new(),
351 solid: solid.name.clone(),
352 depth,
353 screen_dist: dist,
354 position: vertex.position,
355 });
356 }
357 }
358}
359
360fn to_f64(p: [f32; 3]) -> [f64; 3] {
361 [p[0] as f64, p[1] as f64, p[2] as f64]
362}
363
364fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
366 let e1 = sub3(b, a);
367 let e2 = sub3(c, a);
368 let pvec = cross3(ray.dir, e2);
369 let det = dot3(e1, pvec);
370 const EPS: f64 = 1e-14;
371 if double_sided {
372 if det.abs() < EPS {
373 return None;
374 }
375 } else if det < EPS {
376 return None;
377 }
378 let inv_det = 1.0 / det;
379 let tvec = sub3(ray.origin, a);
380 let u = dot3(tvec, pvec) * inv_det;
381 if !(-1e-9..=1.0 + 1e-9).contains(&u) {
382 return None;
383 }
384 let qvec = cross3(tvec, e1);
385 let v = dot3(ray.dir, qvec) * inv_det;
386 if v < -1e-9 || u + v > 1.0 + 1e-9 {
387 return None;
388 }
389 let t = dot3(e2, qvec) * inv_det;
390 if t <= 0.0 {
391 return None;
392 }
393 Some(t)
394}
395
396fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
397 if bbox.is_empty() {
398 return false;
399 }
400 let mut t_min = f64::NEG_INFINITY;
401 let mut t_max = f64::INFINITY;
402 for axis in 0..3 {
403 let dir = ray.dir[axis];
404 let origin = ray.origin[axis];
405 if dir.abs() < 1e-15 {
406 if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
407 return false;
408 }
409 continue;
410 }
411 let inv = 1.0 / dir;
412 let t0 = (bbox.min[axis] - origin) * inv;
413 let t1 = (bbox.max[axis] - origin) * inv;
414 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
415 t_min = t_min.max(lo);
416 t_max = t_max.min(hi);
417 if t_min > t_max {
418 return false;
419 }
420 }
421 t_max > 0.0
422}
423
424pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
426 let list: Vec<serde_json::Value> = candidates
427 .iter()
428 .map(|c| {
429 serde_json::json!({
430 "kind": c.kind.as_str(),
431 "name": c.name,
432 "solid": c.solid,
433 "depth": c.depth,
434 "screenDist": c.screen_dist,
435 "position": c.position,
436 })
437 })
438 .collect();
439 serde_json::Value::Array(list).to_string()
440}
441