//! CPU-side hit-testing for wire geometry. //! //! All tests are performed in **screen space** — wire vertices are projected //! to 2-D pixel coordinates, then compared against the cursor or selection box. //! This matches the visual result the user sees. use std::collections::HashMap; use acadrust::Handle; use glam::{Mat4, Vec3}; use iced::{Point, Rectangle}; use super::hatch_model::HatchModel; use super::mesh_model::MeshModel; use super::wire_model::WireModel; /// Pixel radius used for single-click wire detection. const CLICK_THRESHOLD_PX: f32 = 8.0; // ── Single-click hit test ───────────────────────────────────────────────── /// Return the `name` of the closest wire whose screen-space segments pass /// within `CLICK_THRESHOLD_PX` pixels of `cursor`. /// /// Returns `None` when no wire is close enough. pub fn click_hit<'a>( cursor: Point, wires: &'a [WireModel], view_proj: Mat4, bounds: Rectangle, ) -> Option<&'a str> { let mut best_dist = CLICK_THRESHOLD_PX; let mut best: Option<&str> = None; // Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain. for wire in wires { let mut prev: Option = None; for &[px, py, pz] in &wire.points { if px.is_nan() { prev = None; continue; } let cur = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds); if let Some(p0) = prev { let d = dist_point_to_segment(cursor, p0, cur); if d < best_dist { best_dist = d; best = Some(&wire.name); } } prev = Some(cur); } } best } /// Pick a 3D solid by clicking anywhere on its shaded body: project each /// mesh triangle to screen space and test whether the cursor lands inside it. /// Returns the front-most hit (smallest projected depth). Lets meshed solids /// (whose only wire geometry is thin edges) be selected on their faces. pub fn mesh_click_hit<'a>( cursor: Point, meshes: impl Iterator, view_proj: Mat4, bounds: Rectangle, ) -> Option { let mut best: Option<(f32, Handle)> = None; for (handle, mesh) in meshes { let v = &mesh.verts; let idx = &mesh.indices; let mut t = 0; while t + 2 < idx.len() { let tri = [ v[idx[t] as usize], v[idx[t + 1] as usize], v[idx[t + 2] as usize], ]; t += 3; let mut sp = [Point::ORIGIN; 3]; let mut depth = 0.0f32; for (j, w) in tri.iter().enumerate() { let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2])); sp[j] = Point::new( (ndc.x + 1.0) * 0.5 * bounds.width, (1.0 - ndc.y) * 0.5 * bounds.height, ); depth += ndc.z; } if point_in_polygon(cursor, &sp) { let d = depth / 3.0; if best.map_or(true, |(bd, _)| d < bd) { best = Some((d, handle)); } break; // one hit per mesh is enough } } } best.map(|(_, h)| h) } // ── Box / window selection ──────────────────────────────────────────────── /// Return the names of wires selected by a completed rectangular selection box. /// /// - **Window mode** (`crossing = false`, left→right drag): /// ALL projected points must lie inside the box. /// - **Crossing mode** (`crossing = true`, right→left drag): /// ANY projected point inside the box, OR any wire segment crosses the box /// boundary (so large entities like viewport frames are caught even when /// no corner falls inside the selection rectangle). pub fn box_hit<'a>( corner_a: Point, corner_b: Point, crossing: bool, wires: &'a [WireModel], view_proj: Mat4, bounds: Rectangle, ) -> Vec<&'a str> { let min_x = corner_a.x.min(corner_b.x); let max_x = corner_a.x.max(corner_b.x); let min_y = corner_a.y.min(corner_b.y); let max_y = corner_a.y.max(corner_b.y); // Ignore zero-area boxes. if (max_x - min_x) < 1.0 || (max_y - min_y) < 1.0 { return vec![]; } let inside = |sp: Point| sp.x >= min_x && sp.x <= max_x && sp.y >= min_y && sp.y <= max_y; // Box corners for segment-intersection tests (crossing mode only). let box_tl = Point { x: min_x, y: min_y }; let box_tr = Point { x: max_x, y: min_y }; let box_bl = Point { x: min_x, y: max_y }; let box_br = Point { x: max_x, y: max_y }; // Q: lazy projection — accumulate screen points without allocating per-wire Vec. wires .iter() .filter_map(|wire| { // Fallback: when wire has no line geometry (e.g. greek text emits // only fill_tris) treat the AABB rectangle as the hit-test shape // so low-LOD text stays selectable. See #19. let aabb_pts: Vec<[f32; 3]>; let pts: &[[f32; 3]] = if !wire.points.is_empty() { &wire.points } else if wire.aabb != WireModel::UNBOUNDED_AABB { let [ax, ay, bx, by] = wire.aabb; aabb_pts = vec![ [ax, ay, 0.0], [bx, ay, 0.0], [bx, by, 0.0], [ax, by, 0.0], [ax, ay, 0.0], ]; &aabb_pts } else { return None; }; let mut hit = false; let mut all_inside = true; let mut prev: Option = None; for &[px, py, pz] in pts { if px.is_nan() { prev = None; continue; } let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds); if crossing { if inside(sp) { hit = true; } if let Some(p0) = prev { if !hit { hit = segments_intersect(p0, sp, box_tl, box_tr) || segments_intersect(p0, sp, box_tr, box_br) || segments_intersect(p0, sp, box_br, box_bl) || segments_intersect(p0, sp, box_bl, box_tl); } } } else { if !inside(sp) { all_inside = false; } } prev = Some(sp); } let result = if crossing { hit } else { all_inside && prev.is_some() }; if result { Some(wire.name.as_str()) } else { None } }) .collect() } // ── Polygon / lasso selection ───────────────────────────────────────────── /// Return the names of wires selected by a freehand polygon lasso. /// /// - **Window mode** (`crossing = false`): ALL projected points inside polygon. /// - **Crossing mode** (`crossing = true`): ANY point inside OR any wire /// segment crosses a polygon edge. pub fn poly_hit<'a>( poly: &[Point], crossing: bool, wires: &'a [WireModel], view_proj: Mat4, bounds: Rectangle, ) -> Vec<&'a str> { if poly.len() < 3 { return vec![]; } // Q: lazy projection — no Vec allocation per wire. wires .iter() .filter_map(|wire| { // Same AABB fallback as `box_hit`: when a wire has no line // geometry (e.g. greek-LOD text emits only fill_tris) treat the // AABB rectangle as the hit-test shape so low-LOD text stays // selectable. See #19. let aabb_pts: Vec<[f32; 3]>; let pts: &[[f32; 3]] = if !wire.points.is_empty() { &wire.points } else if wire.aabb != WireModel::UNBOUNDED_AABB { let [ax, ay, bx, by] = wire.aabb; aabb_pts = vec![ [ax, ay, 0.0], [bx, ay, 0.0], [bx, by, 0.0], [ax, by, 0.0], [ax, ay, 0.0], ]; &aabb_pts } else { return None; }; let mut hit = false; let mut all_inside = true; let mut prev: Option = None; for &[px, py, pz] in pts { if px.is_nan() { prev = None; continue; } let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds); if crossing { if point_in_polygon(sp, poly) { hit = true; } if !hit { if let Some(p0) = prev { if segment_crosses_polygon(p0, sp, poly) { hit = true; } } } } else { if !point_in_polygon(sp, poly) { all_inside = false; } } prev = Some(sp); } let result = if crossing { hit } else { all_inside && prev.is_some() }; if result { Some(wire.name.as_str()) } else { None } }) .collect() } // ── Helpers ─────────────────────────────────────────────────────────────── fn world_to_screen(world: Vec3, view_proj: Mat4, bounds: Rectangle) -> Point { let ndc = view_proj.project_point3(world); Point::new( (ndc.x + 1.0) * 0.5 * bounds.width, (1.0 - ndc.y) * 0.5 * bounds.height, ) } /// Even-odd ray-casting test: is `p` inside the polygon? /// /// Handles multi-path boundaries: NaN points (used as path separators by /// hatches with islands / holes) reset the previous-vertex tracking so /// that the ray-cast doesn't draw a spurious closing edge between the /// end of one sub-path and the start of the next. Each sub-path with at /// least 2 finite vertices contributes its segments to the parity flip. fn point_in_polygon(p: Point, poly: &[Point]) -> bool { let mut inside = false; let mut prev: Option = None; let mut path_start: Option = None; for &pt in poly { if !pt.x.is_finite() || !pt.y.is_finite() { // Close the just-finished sub-path before starting the next. if let (Some(prev_v), Some(start_v)) = (prev, path_start) { if ((prev_v.y > p.y) != (start_v.y > p.y)) && (p.x < (start_v.x - prev_v.x) * (p.y - prev_v.y) / (start_v.y - prev_v.y) + prev_v.x) { inside = !inside; } } prev = None; path_start = None; continue; } if let Some(prev_v) = prev { if ((prev_v.y > p.y) != (pt.y > p.y)) && (p.x < (pt.x - prev_v.x) * (p.y - prev_v.y) / (pt.y - prev_v.y) + prev_v.x) { inside = !inside; } } else { path_start = Some(pt); } prev = Some(pt); } // Close the final sub-path. if let (Some(prev_v), Some(start_v)) = (prev, path_start) { if ((prev_v.y > p.y) != (start_v.y > p.y)) && (p.x < (start_v.x - prev_v.x) * (p.y - prev_v.y) / (start_v.y - prev_v.y) + prev_v.x) { inside = !inside; } } inside } /// Does segment `[a, b]` cross any edge of the polygon? fn segment_crosses_polygon(a: Point, b: Point, poly: &[Point]) -> bool { let n = poly.len(); for i in 0..n { let c = poly[i]; let d = poly[(i + 1) % n]; if segments_intersect(a, b, c, d) { return true; } } false } /// Do segments `[a,b]` and `[c,d]` intersect? fn segments_intersect(a: Point, b: Point, c: Point, d: Point) -> bool { let cross = |o: Point, p: Point, q: Point| -> f32 { (p.x - o.x) * (q.y - o.y) - (p.y - o.y) * (q.x - o.x) }; let d1 = cross(c, d, a); let d2 = cross(c, d, b); let d3 = cross(a, b, c); let d4 = cross(a, b, d); if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0)) { return true; } false } // ── Hatch hit-testing ───────────────────────────────────────────────────── /// Return the Handle of the first hatch whose screen-space boundary polygon /// contains `cursor`. pub fn click_hit_hatch( cursor: Point, hatches: &HashMap, view_proj: Mat4, bounds: Rectangle, ) -> Option { for (&handle, hatch) in hatches { if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) { return Some(handle); } } None } /// Same as `click_hit_hatch` but iterates `(Handle, HatchModel)` pairs /// where the Handle is the parent Insert handle (block-internal /// hatches). The first matching pair returns its Insert handle so /// clicking a sub-hatch of a block selects the Insert, matching /// AutoCAD's behaviour for block sub-entities. pub fn click_hit_insert_hatch( cursor: Point, insert_hatches: &[(Handle, HatchModel)], view_proj: Mat4, bounds: Rectangle, ) -> Option { for (handle, hatch) in insert_hatches { if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) { return Some(*handle); } } None } fn hatch_contains_screen_point( hatch: &HatchModel, cursor: Point, view_proj: Mat4, bounds: Rectangle, ) -> bool { // boundary verts are stored as small f32 offsets from // `world_origin` (f64). Reconstruct offset-rel WCS before // projecting to screen. let ox = hatch.world_origin[0] as f32; let oy = hatch.world_origin[1] as f32; let screen: Vec = hatch .boundary .iter() .map(|&[x, y]| { if x.is_finite() && y.is_finite() { world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds) } else { // Preserve path separators for the NaN-aware // point_in_polygon ray-cast. Point::new(f32::NAN, f32::NAN) } }) .collect(); screen.len() >= 3 && point_in_polygon(cursor, &screen) } /// Return Handles of hatches selected by a completed rectangular selection box. pub fn box_hit_hatch( corner_a: Point, corner_b: Point, crossing: bool, hatches: &HashMap, view_proj: Mat4, bounds: Rectangle, ) -> Vec { let min_x = corner_a.x.min(corner_b.x); let max_x = corner_a.x.max(corner_b.x); let min_y = corner_a.y.min(corner_b.y); let max_y = corner_a.y.max(corner_b.y); if (max_x - min_x) < 1.0 || (max_y - min_y) < 1.0 { return vec![]; } let inside = |sp: Point| sp.x >= min_x && sp.x <= max_x && sp.y >= min_y && sp.y <= max_y; hatches .iter() .filter_map(|(&handle, hatch)| { if hatch.boundary.is_empty() { return None; } let ox = hatch.world_origin[0] as f32; let oy = hatch.world_origin[1] as f32; let screen: Vec = hatch .boundary .iter() .map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds)) .collect(); let hit = if crossing { screen.iter().any(|&sp| inside(sp)) } else { screen.iter().all(|&sp| inside(sp)) }; if hit { Some(handle) } else { None } }) .collect() } /// Return Handles of hatches selected by a freehand polygon lasso. pub fn poly_hit_hatch( poly: &[Point], crossing: bool, hatches: &HashMap, view_proj: Mat4, bounds: Rectangle, ) -> Vec { if poly.len() < 3 { return vec![]; } hatches .iter() .filter_map(|(&handle, hatch)| { if hatch.boundary.is_empty() { return None; } let ox = hatch.world_origin[0] as f32; let oy = hatch.world_origin[1] as f32; let screen: Vec = hatch .boundary .iter() .map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds)) .collect(); let hit = if crossing { screen.iter().any(|&sp| point_in_polygon(sp, poly)) || screen .windows(2) .any(|seg| segment_crosses_polygon(seg[0], seg[1], poly)) } else { screen.iter().all(|&sp| point_in_polygon(sp, poly)) }; if hit { Some(handle) } else { None } }) .collect() } /// Minimum distance from point `p` to line segment `[a, b]` in 2-D. fn dist_point_to_segment(p: Point, a: Point, b: Point) -> f32 { let abx = b.x - a.x; let aby = b.y - a.y; let len2 = abx * abx + aby * aby; let t = if len2 < 1e-6 { 0.0 } else { let apx = p.x - a.x; let apy = p.y - a.y; ((apx * abx + apy * aby) / len2).clamp(0.0, 1.0) }; let cx = a.x + t * abx; let cy = a.y + t * aby; let dx = p.x - cx; let dy = p.y - cy; (dx * dx + dy * dy).sqrt() }