use crate::geometry2d::point_segment_distance;
use crate::scene::{RenderScene, SolidDisplay};
use crate::view::{add3, cross3, dot3, scale3, sub3, Projection, Ray, ViewCamera};
pub const EDGE_PICK_PX: f64 = 6.0;
pub const VERTEX_PICK_PX: f64 = 6.0;
const MAX_CANDIDATES: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PickKind {
Vertex = 0,
Edge = 1,
Face = 2,
Plane = 3,
Solid = 4,
Component = 5,
}
impl PickKind {
pub fn as_str(&self) -> &'static str {
match self {
PickKind::Vertex => "VERTEX",
PickKind::Edge => "EDGE",
PickKind::Face => "FACE",
PickKind::Plane => "PLANE",
PickKind::Solid => "SOLID",
PickKind::Component => "COMPONENT",
}
}
}
#[derive(Debug, Clone)]
pub struct PickCandidate {
pub kind: PickKind,
pub name: String,
pub solid: String,
pub depth: f64,
pub screen_dist: f64,
pub position: [f64; 3],
}
#[derive(Debug, Clone, Copy)]
pub struct PickOptions {
pub double_sided: bool,
pub edge_px: f64,
pub vertex_px: f64,
}
impl Default for PickOptions {
fn default() -> Self {
Self {
double_sided: true,
edge_px: EDGE_PICK_PX,
vertex_px: VERTEX_PICK_PX,
}
}
}
pub fn pick(
scene: &RenderScene,
camera: &ViewCamera,
x: f64,
y: f64,
options: &PickOptions,
) -> Vec<PickCandidate> {
let ray = camera.pick_ray(x, y);
let (_, _, forward) = camera.basis();
let persp = matches!(camera.projection, Projection::Perspective { .. });
let mut hits: Vec<PickCandidate> = Vec::new();
for solid in scene.solids() {
if !solid.visible {
continue;
}
pick_faces(solid, camera, &ray, forward, options, &mut hits);
pick_edges(solid, camera, x, y, forward, persp, options, &mut hits);
pick_vertices(solid, camera, x, y, forward, persp, options, &mut hits);
}
hits.sort_by(|a, b| {
(a.kind as u8)
.cmp(&(b.kind as u8))
.then(a.depth.total_cmp(&b.depth))
.then(a.screen_dist.total_cmp(&b.screen_dist))
});
hits.truncate(MAX_CANDIDATES);
let mut solids_seen: Vec<String> = Vec::new();
let mut solid_entries: Vec<PickCandidate> = Vec::new();
for hit in &hits {
if solids_seen.iter().any(|name| name == &hit.solid) {
continue;
}
solids_seen.push(hit.solid.clone());
solid_entries.push(PickCandidate {
kind: PickKind::Solid,
name: hit.solid.clone(),
solid: hit.solid.clone(),
depth: hit.depth,
screen_dist: hit.screen_dist,
position: hit.position,
});
}
hits.extend(solid_entries);
hits
}
pub fn pick_filtered(
scene: &RenderScene,
camera: &ViewCamera,
x: f64,
y: f64,
options: &PickOptions,
filter: &[String],
) -> Option<PickCandidate> {
pick(scene, camera, x, y, options).into_iter().find(|c| {
filter.is_empty() || filter.iter().any(|f| f.eq_ignore_ascii_case(c.kind.as_str()))
})
}
fn view_depth(camera: &ViewCamera, forward: [f64; 3], point: [f64; 3]) -> f64 {
dot3(sub3(point, camera.eye), forward)
}
fn pick_faces(
solid: &SolidDisplay,
camera: &ViewCamera,
ray: &Ray,
forward: [f64; 3],
options: &PickOptions,
out: &mut Vec<PickCandidate>,
) {
if solid.mesh.indices.is_empty() {
return;
}
if !ray_hits_aabb(ray, &solid.bbox) {
return;
}
let positions = &solid.mesh.positions;
let indices = &solid.mesh.indices;
for (index, face) in solid.faces.iter().enumerate() {
if !solid.visibility.is_face_visible(index) {
continue;
}
if face.tri_count == 0 {
continue;
}
let mut best: Option<(f64, [f64; 3])> = None;
let start = face.tri_start as usize;
let end = start + face.tri_count as usize;
for tri in start..end.min(indices.len() / 3) {
let i0 = indices[tri * 3] as usize;
let i1 = indices[tri * 3 + 1] as usize;
let i2 = indices[tri * 3 + 2] as usize;
let a = to_f64(positions[i0]);
let b = to_f64(positions[i1]);
let c = to_f64(positions[i2]);
if let Some(t) = ray_triangle(ray, a, b, c, options.double_sided) {
let point = add3(ray.origin, scale3(ray.dir, t));
if best.map(|(bt, _)| t < bt).unwrap_or(true) {
best = Some((t, point));
}
}
}
if let Some((_, point)) = best {
out.push(PickCandidate {
kind: PickKind::Face,
name: face.name.clone(),
solid: solid.name.clone(),
depth: view_depth(camera, forward, point),
screen_dist: 0.0,
position: point,
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn pick_edges(
solid: &SolidDisplay,
camera: &ViewCamera,
x: f64,
y: f64,
forward: [f64; 3],
persp: bool,
options: &PickOptions,
out: &mut Vec<PickCandidate>,
) {
for (index, edge) in solid.edges.iter().enumerate() {
if !solid.visibility.is_edge_visible(index) {
continue;
}
let mut best: Option<(f64, f64, [f64; 3])> = None; for pair in edge.polyline.windows(2) {
let mut a = to_f64(pair[0]);
let mut b = to_f64(pair[1]);
if persp {
let da = view_depth(camera, forward, a);
let db = view_depth(camera, forward, b);
const EPS: f64 = 1e-6;
if da <= EPS && db <= EPS {
continue;
}
if da <= EPS || db <= EPS {
let t = (EPS - da) / (db - da);
let clip = add3(a, scale3(sub3(b, a), t));
if da <= EPS {
a = clip;
} else {
b = clip;
}
}
}
let (ax, ay, _) = camera.project(a);
let (bx, by, _) = camera.project(b);
let (dist, t) = point_segment_distance((x, y), (ax, ay), (bx, by));
if dist <= options.edge_px {
let world = add3(a, scale3(sub3(b, a), t));
let depth = view_depth(camera, forward, world);
if best
.map(|(bd, bdepth, _)| dist < bd || (dist == bd && depth < bdepth))
.unwrap_or(true)
{
best = Some((dist, depth, world));
}
}
}
if let Some((screen_dist, depth, position)) = best {
out.push(PickCandidate {
kind: PickKind::Edge,
name: edge.name.clone(),
solid: solid.name.clone(),
depth,
screen_dist,
position,
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn pick_vertices(
solid: &SolidDisplay,
camera: &ViewCamera,
x: f64,
y: f64,
forward: [f64; 3],
persp: bool,
options: &PickOptions,
out: &mut Vec<PickCandidate>,
) {
for (index, vertex) in solid.vertices.iter().enumerate() {
if !solid.visibility.is_vertex_visible(index) {
continue;
}
let depth = view_depth(camera, forward, vertex.position);
if persp && depth <= 1e-6 {
continue;
}
let (sx, sy, _) = camera.project(vertex.position);
let dist = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
if dist <= options.vertex_px {
out.push(PickCandidate {
kind: PickKind::Vertex,
name: String::new(),
solid: solid.name.clone(),
depth,
screen_dist: dist,
position: vertex.position,
});
}
}
}
fn to_f64(p: [f32; 3]) -> [f64; 3] {
[p[0] as f64, p[1] as f64, p[2] as f64]
}
fn ray_triangle(ray: &Ray, a: [f64; 3], b: [f64; 3], c: [f64; 3], double_sided: bool) -> Option<f64> {
let e1 = sub3(b, a);
let e2 = sub3(c, a);
let pvec = cross3(ray.dir, e2);
let det = dot3(e1, pvec);
const EPS: f64 = 1e-14;
if double_sided {
if det.abs() < EPS {
return None;
}
} else if det < EPS {
return None;
}
let inv_det = 1.0 / det;
let tvec = sub3(ray.origin, a);
let u = dot3(tvec, pvec) * inv_det;
if !(-1e-9..=1.0 + 1e-9).contains(&u) {
return None;
}
let qvec = cross3(tvec, e1);
let v = dot3(ray.dir, qvec) * inv_det;
if v < -1e-9 || u + v > 1.0 + 1e-9 {
return None;
}
let t = dot3(e2, qvec) * inv_det;
if t <= 0.0 {
return None;
}
Some(t)
}
fn ray_hits_aabb(ray: &Ray, bbox: &crate::camera::Aabb) -> bool {
if bbox.is_empty() {
return false;
}
let mut t_min = f64::NEG_INFINITY;
let mut t_max = f64::INFINITY;
for axis in 0..3 {
let dir = ray.dir[axis];
let origin = ray.origin[axis];
if dir.abs() < 1e-15 {
if origin < bbox.min[axis] - 1e-9 || origin > bbox.max[axis] + 1e-9 {
return false;
}
continue;
}
let inv = 1.0 / dir;
let t0 = (bbox.min[axis] - origin) * inv;
let t1 = (bbox.max[axis] - origin) * inv;
let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
t_min = t_min.max(lo);
t_max = t_max.min(hi);
if t_min > t_max {
return false;
}
}
t_max > 0.0
}
pub fn candidates_to_json(candidates: &[PickCandidate]) -> String {
let list: Vec<serde_json::Value> = candidates
.iter()
.map(|c| {
serde_json::json!({
"kind": c.kind.as_str(),
"name": c.name,
"solid": c.solid,
"depth": c.depth,
"screenDist": c.screen_dist,
"position": c.position,
})
})
.collect();
serde_json::Value::Array(list).to_string()
}