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 !solid.visibility.is_edge_visible(index) {
242 continue;
243 }
244 let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
246 let mut a = to_f64(pair[0]);
247 let mut b = to_f64(pair[1]);
248 if persp {
249 let da = view_depth(camera, forward, a);
250 let db = view_depth(camera, forward, b);
251 const EPS: f64 = 1e-6;
252 if da <= EPS && db <= EPS {
253 continue;
254 }
255 if da <= EPS || db <= EPS {
256 let t = (EPS - da) / (db - da);
258 let clip = add3(a, scale3(sub3(b, a), t));
259 if da <= EPS {
260 a = clip;
261 } else {
262 b = clip;
263 }
264 }
265 }
266 let (ax, ay, _) = camera.project(a);
267 let (bx, by, _) = camera.project(b);
268 let (dist, t) = point_segment_distance((x, y), (ax, ay), (bx, by));
269 if dist <= options.edge_px {
270 let world = add3(a, scale3(sub3(b, a), t));
271 let depth = view_depth(camera, forward, world);
272 if best
273 .map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
274 .unwrap_or(true)
275 {
276 best = Some((dist, depth, world));
277 }
278 }
279 }
280 if let Some((screen_dist, depth, position)) = best {
281 out.push(PickCandidate {
282 kind: PickKind::Edge,
283 name: edge.name.clone(),
284 solid: solid.name.clone(),
285 depth,
286 screen_dist,
287 position,
288 });
289 }
290 }
291}
292
293#[allow(clippy::too_many_arguments)]
294fn pick_vertices(
295 solid: &SolidDisplay,
296 camera: &ViewCamera,
297 x: f64,
298 y: f64,
299 forward: [f64; 3],
300 persp: bool,
301 options: &PickOptions,
302 out: &mut Vec<PickCandidate>,
303) {
304 for (index, vertex) in solid.vertices.iter().enumerate() {
305 if !solid.visibility.is_vertex_visible(index) {
307 continue;
308 }
309 let depth = view_depth(camera, forward, vertex.position);
310 if persp && depth <= 1e-6 {
311 continue;
312 }
313 let (sx, sy, _) = camera.project(vertex.position);
314 let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
315 if dist <= options.vertex_px {
316 out.push(PickCandidate {
317 kind: PickKind::Vertex,
318 name: String::new(),
319 solid: solid.name.clone(),
320 depth,
321 screen_dist: dist,
322 position: vertex.position,
323 });
324 }
325 }
326}
327
328fn to_f64(p: [f32; 3]) -> [f64; 3] {
329 [p[0] as f64, p[1] as f64, p[2] as f64]
330}
331
332fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
334 let e1 = sub3(b, a);
335 let e2 = sub3(c, a);
336 let pvec = cross3(ray.dir, e2);
337 let det = dot3(e1, pvec);
338 const EPS: f64 = 1e-14;
339 if double_sided {
340 if det.abs() < EPS {
341 return None;
342 }
343 } else if det < EPS {
344 return None;
345 }
346 let inv_det = 1.0 / det;
347 let tvec = sub3(ray.origin, a);
348 let u = dot3(tvec, pvec) * inv_det;
349 if !(-1e-9..=1.0 + 1e-9).contains(&u) {
350 return None;
351 }
352 let qvec = cross3(tvec, e1);
353 let v = dot3(ray.dir, qvec) * inv_det;
354 if v < -1e-9 || u + v > 1.0 + 1e-9 {
355 return None;
356 }
357 let t = dot3(e2, qvec) * inv_det;
358 if t <= 0.0 {
359 return None;
360 }
361 Some(t)
362}
363
364fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
365 if bbox.is_empty() {
366 return false;
367 }
368 let mut t_min = f64::NEG_INFINITY;
369 let mut t_max = f64::INFINITY;
370 for axis in 0..3 {
371 let dir = ray.dir[axis];
372 let origin = ray.origin[axis];
373 if dir.abs() < 1e-15 {
374 if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
375 return false;
376 }
377 continue;
378 }
379 let inv = 1.0 / dir;
380 let t0 = (bbox.min[axis] - origin) * inv;
381 let t1 = (bbox.max[axis] - origin) * inv;
382 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
383 t_min = t_min.max(lo);
384 t_max = t_max.min(hi);
385 if t_min > t_max {
386 return false;
387 }
388 }
389 t_max > 0.0
390}
391
392pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
394 let list: Vec<serde_json::Value> = candidates
395 .iter()
396 .map(|c| {
397 serde_json::json!({
398 "kind": c.kind.as_str(),
399 "name": c.name,
400 "solid": c.solid,
401 "depth": c.depth,
402 "screenDist": c.screen_dist,
403 "position": c.position,
404 })
405 })
406 .collect();
407 serde_json::Value::Array(list).to_string()
408}
409
410