//! Shared spatial broad phase for cursor and area interactions. //! //! Exact snap and selection rules remain in their consumers. This index only //! narrows large resident wire sets to local wire/sub-primitive candidates: //! compact world-XY grids for plan views and projected 3D BVHs for tilted or //! perspective views. use crate::scene::model::wire_model::WireModel; use glam::{DVec3, Mat4}; use iced::Rectangle; use std::sync::Arc; const TARGET_ENTRIES_PER_CELL: usize = 8; const MAX_SPAN_CELLS: u64 = 64; const MAX_AXIS_CELLS: u32 = 8_192; const BVH_LEAF_SIZE: usize = 16; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct SegmentRef { pub wire: u32, pub start: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct SnapPointRef { pub wire: u32, pub index: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct KeyVertexRef { pub wire: u32, pub index: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct KeySegmentRef { pub wire: u32, pub start: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TriangleRef { pub wire: u32, pub start: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct GlyphRef { pub wire: u32, pub start: u32, } #[derive(Clone, Copy)] struct Entry3 { aabb: [f64; 6], value: T, } fn entry_aabb2(entry: &Entry3) -> [f64; 4] { [entry.aabb[0], entry.aabb[1], entry.aabb[3], entry.aabb[4]] } #[derive(Clone, Copy)] struct BvhNode2 { aabb: [f64; 4], left: u32, right: u32, start: u32, len: u32, } struct SpatialBvh2 { nodes: Vec, order: Vec, } impl SpatialBvh2 { fn build(entries: &[Entry3], mut order: Vec) -> Self { let mut nodes = Vec::new(); if !order.is_empty() { build_bvh2_node(entries, &mut order, 0, &mut nodes); } Self { nodes, order } } fn query(&self, entries: &[Entry3], query: [f64; 4], out: &mut Vec) { if self.nodes.is_empty() { return; } let mut stack = vec![0u32]; while let Some(node_idx) = stack.pop() { let node = self.nodes[node_idx as usize]; if !aabb_overlaps(node.aabb, query) { continue; } if node.left == u32::MAX { for &entry_idx in &self.order[node.start as usize..(node.start + node.len) as usize] { if aabb_overlaps(entry_aabb2(&entries[entry_idx as usize]), query) { out.push(entry_idx); } } } else { stack.push(node.left); stack.push(node.right); } } } } fn build_bvh2_node( entries: &[Entry3], order: &mut [u32], base: usize, nodes: &mut Vec, ) -> u32 { let mut bounds = [ f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, ]; for &idx in order.iter() { let aabb = entry_aabb2(&entries[idx as usize]); bounds[0] = bounds[0].min(aabb[0]); bounds[1] = bounds[1].min(aabb[1]); bounds[2] = bounds[2].max(aabb[2]); bounds[3] = bounds[3].max(aabb[3]); } let node_idx = nodes.len() as u32; nodes.push(BvhNode2 { aabb: bounds, left: u32::MAX, right: u32::MAX, start: base as u32, len: order.len() as u32, }); if order.len() <= BVH_LEAF_SIZE { return node_idx; } let axis = usize::from(bounds[3] - bounds[1] > bounds[2] - bounds[0]); let mid = order.len() / 2; order.select_nth_unstable_by(mid, |&a, &b| { let aa = entry_aabb2(&entries[a as usize]); let bb = entry_aabb2(&entries[b as usize]); (aa[axis] + aa[axis + 2]).total_cmp(&(bb[axis] + bb[axis + 2])) }); let (left_order, right_order) = order.split_at_mut(mid); let left = build_bvh2_node(entries, left_order, base, nodes); let right = build_bvh2_node(entries, right_order, base + mid, nodes); nodes[node_idx as usize].left = left; nodes[node_idx as usize].right = right; node_idx } #[derive(Clone, Copy)] struct BvhNode3 { aabb: [f64; 6], left: u32, right: u32, start: u32, len: u32, } struct SpatialBvh3 { nodes: Vec, order: Vec, } impl SpatialBvh3 { fn build(entries: &[Entry3]) -> Self { let mut order: Vec = (0..entries.len() as u32).collect(); let mut nodes = Vec::new(); if !order.is_empty() { build_bvh3_node(entries, &mut order, 0, &mut nodes); } Self { nodes, order } } fn query_screen( &self, entries: &[Entry3], screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> Vec { if self.nodes.is_empty() { return Vec::new(); } let mut stack = vec![0u32]; let mut entry_indices = Vec::new(); while let Some(node_idx) = stack.pop() { let node = self.nodes[node_idx as usize]; if !aabb3_projects_into(node.aabb, screen_rect, view_rot, eye, bounds) { continue; } if node.left == u32::MAX { for &entry_idx in &self.order[node.start as usize..(node.start + node.len) as usize] { let entry = entries[entry_idx as usize]; if aabb3_projects_into(entry.aabb, screen_rect, view_rot, eye, bounds) { entry_indices.push(entry.value); } } } else { stack.push(node.left); stack.push(node.right); } } entry_indices.sort_unstable(); entry_indices.dedup(); entry_indices } } fn build_bvh3_node( entries: &[Entry3], order: &mut [u32], base: usize, nodes: &mut Vec, ) -> u32 { let mut bounds = [ f64::INFINITY, f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, ]; for &idx in order.iter() { let aabb = entries[idx as usize].aabb; for axis in 0..3 { bounds[axis] = bounds[axis].min(aabb[axis]); bounds[axis + 3] = bounds[axis + 3].max(aabb[axis + 3]); } } let node_idx = nodes.len() as u32; nodes.push(BvhNode3 { aabb: bounds, left: u32::MAX, right: u32::MAX, start: base as u32, len: order.len() as u32, }); if order.len() <= BVH_LEAF_SIZE { return node_idx; } let mut axis = 0; for candidate in 1..3 { if bounds[candidate + 3] - bounds[candidate] > bounds[axis + 3] - bounds[axis] { axis = candidate; } } let mid = order.len() / 2; order.select_nth_unstable_by(mid, |&a, &b| { let aa = entries[a as usize].aabb; let bb = entries[b as usize].aabb; (aa[axis] + aa[axis + 3]).total_cmp(&(bb[axis] + bb[axis + 3])) }); let (left_order, right_order) = order.split_at_mut(mid); let left = build_bvh3_node(entries, left_order, base, nodes); let right = build_bvh3_node(entries, right_order, base + mid, nodes); nodes[node_idx as usize].left = left; nodes[node_idx as usize].right = right; node_idx } struct SpatialGrid { min: [f64; 2], cell: f64, cols: u32, rows: u32, cell_offsets: Vec, cell_entries: Vec, oversized: SpatialBvh2, } impl SpatialGrid { fn build(entries: &[Entry3]) -> Self { if entries.is_empty() { return Self { min: [0.0; 2], cell: 1.0, cols: 1, rows: 1, cell_offsets: vec![0, 0], cell_entries: Vec::new(), oversized: SpatialBvh2 { nodes: Vec::new(), order: Vec::new(), }, }; } let mut min = [f64::INFINITY; 2]; let mut max = [f64::NEG_INFINITY; 2]; for entry in entries { let aabb = entry_aabb2(entry); min[0] = min[0].min(aabb[0]); min[1] = min[1].min(aabb[1]); max[0] = max[0].max(aabb[2]); max[1] = max[1].max(aabb[3]); } let ext_x = (max[0] - min[0]).max(1e-9); let ext_y = (max[1] - min[1]).max(1e-9); let target_cells = ((entries.len() / TARGET_ENTRIES_PER_CELL).max(1)) as f64; let cell = ((ext_x * ext_y) / target_cells) .sqrt() .max(ext_x / (MAX_AXIS_CELLS - 1) as f64) .max(ext_y / (MAX_AXIS_CELLS - 1) as f64) .max(1e-9); let cols = (((ext_x / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32; let rows = (((ext_y / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32; let cell_count = cols as usize * rows as usize; let col_of = |x: f64| (((x - min[0]) / cell).floor()).clamp(0.0, (cols - 1) as f64) as u32; let row_of = |y: f64| (((y - min[1]) / cell).floor()).clamp(0.0, (rows - 1) as f64) as u32; #[cfg(not(target_arch = "wasm32"))] let (counts, oversized) = { use crate::par::prelude::*; use std::sync::atomic::{AtomicU32, Ordering}; let counts: Vec = (0..cell_count).map(|_| AtomicU32::new(0)).collect(); let oversized: Vec = entries .par_iter() .enumerate() .filter_map(|(idx, entry)| { let aabb = entry_aabb2(entry); let c0 = col_of(aabb[0]); let c1 = col_of(aabb[2]); let r0 = row_of(aabb[1]); let r1 = row_of(aabb[3]); let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64; if span > MAX_SPAN_CELLS { return Some(idx as u32); } for row in r0..=r1 { let base = row as usize * cols as usize; for col in c0..=c1 { counts[base + col as usize].fetch_add(1, Ordering::Relaxed); } } None }) .collect(); ( counts .into_iter() .map(AtomicU32::into_inner) .collect::>(), oversized, ) }; #[cfg(target_arch = "wasm32")] let (counts, oversized) = { let mut counts = vec![0u32; cell_count]; let mut oversized = Vec::new(); for (idx, entry) in entries.iter().enumerate() { let aabb = entry_aabb2(entry); let c0 = col_of(aabb[0]); let c1 = col_of(aabb[2]); let r0 = row_of(aabb[1]); let r1 = row_of(aabb[3]); let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64; if span > MAX_SPAN_CELLS { oversized.push(idx as u32); continue; } for row in r0..=r1 { let base = row as usize * cols as usize; for col in c0..=c1 { counts[base + col as usize] += 1; } } } (counts, oversized) }; let mut cell_offsets = Vec::with_capacity(cell_count + 1); cell_offsets.push(0); for count in counts { cell_offsets.push(cell_offsets.last().copied().unwrap_or(0) + count); } #[cfg(not(target_arch = "wasm32"))] let cell_entries = { use crate::par::prelude::*; use std::sync::atomic::{AtomicU32, Ordering}; let cursors: Vec = cell_offsets[..cell_count] .iter() .copied() .map(AtomicU32::new) .collect(); let slots: Vec = (0..*cell_offsets.last().unwrap_or(&0) as usize) .map(|_| AtomicU32::new(0)) .collect(); entries.par_iter().enumerate().for_each(|(idx, entry)| { let aabb = entry_aabb2(entry); let c0 = col_of(aabb[0]); let c1 = col_of(aabb[2]); let r0 = row_of(aabb[1]); let r1 = row_of(aabb[3]); let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64; if span > MAX_SPAN_CELLS { return; } for row in r0..=r1 { let base = row as usize * cols as usize; for col in c0..=c1 { let cell_idx = base + col as usize; let cursor = cursors[cell_idx].fetch_add(1, Ordering::Relaxed); slots[cursor as usize].store(idx as u32, Ordering::Relaxed); } } }); slots.into_iter().map(AtomicU32::into_inner).collect() }; #[cfg(target_arch = "wasm32")] let cell_entries = { let mut cell_entries = vec![0u32; *cell_offsets.last().unwrap_or(&0) as usize]; let mut cursors = cell_offsets[..cell_count].to_vec(); for (idx, entry) in entries.iter().enumerate() { let aabb = entry_aabb2(entry); let c0 = col_of(aabb[0]); let c1 = col_of(aabb[2]); let r0 = row_of(aabb[1]); let r1 = row_of(aabb[3]); let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64; if span > MAX_SPAN_CELLS { continue; } for row in r0..=r1 { let base = row as usize * cols as usize; for col in c0..=c1 { let cell_idx = base + col as usize; let cursor = &mut cursors[cell_idx]; cell_entries[*cursor as usize] = idx as u32; *cursor += 1; } } } cell_entries }; let oversized = SpatialBvh2::build(&entries, oversized); Self { min, cell, cols, rows, cell_offsets, cell_entries, oversized, } } fn query(&self, entries: &[Entry3], query: [f64; 4]) -> Vec { let mut entry_indices = Vec::new(); self.oversized.query(entries, query, &mut entry_indices); let grid_max_x = self.min[0] + self.cols as f64 * self.cell; let grid_max_y = self.min[1] + self.rows as f64 * self.cell; if query[2] >= self.min[0] && query[3] >= self.min[1] && query[0] <= grid_max_x && query[1] <= grid_max_y { let col = |x: f64| { (((x - self.min[0]) / self.cell).floor()).clamp(0.0, (self.cols - 1) as f64) as u32 }; let row = |y: f64| { (((y - self.min[1]) / self.cell).floor()).clamp(0.0, (self.rows - 1) as f64) as u32 }; for r in row(query[1])..=row(query[3]) { let base = r as usize * self.cols as usize; for c in col(query[0])..=col(query[2]) { let cell_idx = base + c as usize; let start = self.cell_offsets[cell_idx] as usize; let end = self.cell_offsets[cell_idx + 1] as usize; entry_indices.extend_from_slice(&self.cell_entries[start..end]); } } } entry_indices.sort_unstable(); entry_indices.dedup(); let mut out: Vec = entry_indices .into_iter() .filter_map(|idx| { let entry = entries.get(idx as usize)?; aabb_overlaps(entry_aabb2(entry), query).then_some(entry.value) }) .collect(); out.sort_unstable(); out.dedup(); out } } struct SpatialSet { entries: Vec>, xy: SpatialGrid, xyz: std::sync::OnceLock, } impl SpatialSet { fn build(entries: Vec>) -> Self { let xy = SpatialGrid::build(&entries); Self { entries, xy, xyz: std::sync::OnceLock::new(), } } fn query_xy(&self, aabb: [f64; 4]) -> Vec { self.xy.query(&self.entries, aabb) } fn prepare_screen(&self) { self.xyz .get_or_init(|| SpatialBvh3::build(&self.entries)); } fn query_screen( &self, screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> Vec { self.xyz .get_or_init(|| SpatialBvh3::build(&self.entries)) .query_screen(&self.entries, screen_rect, view_rot, eye, bounds) } } pub struct InteractionIndex { wires: SpatialSet, /// Stable entity handle for each source wire. Lets a stale-but-valid base /// index feed an incremental overlay after small edits without retaining /// the old heavyweight wire set or trusting shifted vector indices. wire_handles: Vec>, /// Stable occurrence number of each wire inside its owning entity run. /// Small-edit overlays use `(handle, ordinal)` to recover only the exact /// nearby wires from the current resident memo instead of cloning every /// wire emitted by a large INSERT. wire_ordinals: Vec>, segments: SpatialSet, snap_points: SpatialSet, key_vertices: SpatialSet, key_segments: SpatialSet, fill_triangles: SpatialSet, pick_triangles: SpatialSet, glyphs: SpatialSet, unbounded_wires: Vec, screen_unbounded_wires: Vec, max_line_half_width_px: f32, max_marker_radius_fraction: f32, } pub struct InteractionHandleIndex { handles: SpatialSet, } struct WireIndexEntries { wire: Option>, segments: Vec>, snap_points: Vec>, key_vertices: Vec>, key_segments: Vec>, fill_triangles: Vec>, pick_triangles: Vec>, glyphs: Vec>, unbounded: bool, screen_unbounded: bool, max_line_half_width_px: f32, max_marker_radius_fraction: f32, } fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries { let mut entries = WireIndexEntries { wire: finite_wire_aabb3(wire).map(|aabb| Entry3 { aabb, value: wire_idx, }), segments: Vec::with_capacity(wire.points.len().saturating_sub(1)), snap_points: Vec::with_capacity(wire.snap_pts.len()), key_vertices: Vec::with_capacity(wire.key_vertices.len()), key_segments: Vec::with_capacity(wire.key_vertices.len().saturating_sub(1)), fill_triangles: Vec::with_capacity(wire.fill_tris.len() / 3), pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3), glyphs: Vec::with_capacity(wire.text_verts.len() / 6), unbounded: false, screen_unbounded: wire.point_marker.is_some(), max_line_half_width_px: if wire.line_weight_px.is_finite() { (wire.line_weight_px * 0.5).max(0.0) } else { 0.0 }, max_marker_radius_fraction: wire.point_marker.map_or(0.0, |marker| { marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2 }), }; entries.unbounded = entries.wire.is_none(); if wire.point_marker.is_none() { for start in 0..wire.points.len().saturating_sub(1) { let Some(aabb) = points_aabb3([ wire_point(wire, start), wire_point(wire, start + 1), ]) else { continue; }; entries.segments.push(Entry3 { aabb, value: SegmentRef { wire: wire_idx, start: start as u32, }, }); } } for (index, (point, _)) in wire.snap_pts.iter().enumerate() { if point.is_finite() { entries.snap_points.push(Entry3 { aabb: [point.x, point.y, point.z, point.x, point.y, point.z], value: SnapPointRef { wire: wire_idx, index: index as u32, }, }); } } for (index, &point) in wire.key_vertices.iter().enumerate() { if point.iter().all(|value| value.is_finite()) { entries.key_vertices.push(Entry3 { aabb: [point[0], point[1], point[2], point[0], point[1], point[2]], value: KeyVertexRef { wire: wire_idx, index: index as u32, }, }); } } for start in 0..wire.key_vertices.len().saturating_sub(1) { let Some(aabb) = points_aabb3([wire.key_vertices[start], wire.key_vertices[start + 1]]) else { continue; }; entries.key_segments.push(Entry3 { aabb, value: KeySegmentRef { wire: wire_idx, start: start as u32, }, }); } append_triangle_entries( wire_idx, &wire.fill_tris, &wire.fill_tris_low, &mut entries.fill_triangles, ); append_triangle_entries( wire_idx, &wire.pick_tris, &wire.pick_tris_low, &mut entries.pick_triangles, ); for start in (0..wire.text_verts.len()).step_by(6) { let Some(quad) = wire.text_verts.get(start..start + 6) else { break; }; let Some(aabb) = points_aabb3(quad.iter().map(|vertex| { [ vertex.pos[0] as f64 + vertex.pos_low[0] as f64, vertex.pos[1] as f64 + vertex.pos_low[1] as f64, vertex.pos[2] as f64 + vertex.pos_low[2] as f64, ] })) else { continue; }; entries.glyphs.push(Entry3 { aabb, value: GlyphRef { wire: wire_idx, start: start as u32, }, }); } entries } fn flatten_entry_parts(mut parts: Vec>) -> Vec { let Some((largest, _)) = parts.iter().enumerate().max_by_key(|(_, part)| part.len()) else { return Vec::new(); }; let mut output = parts.swap_remove(largest); let remaining: usize = parts.iter().map(Vec::len).sum(); output.reserve(remaining); for mut part in parts { output.append(&mut part); } output } impl InteractionHandleIndex { pub fn build(entries: impl IntoIterator) -> Self { Self { handles: SpatialSet::build( entries .into_iter() .filter(|(_, aabb)| { aabb.iter().all(|value| value.is_finite()) && aabb[0] <= aabb[3] && aabb[1] <= aabb[4] && aabb[2] <= aabb[5] }) .map(|(value, aabb)| Entry3 { aabb, value }) .collect(), ), } } pub fn query_xy(&self, aabb: [f64; 4]) -> Vec { self.handles.query_xy(aabb) } pub fn query_screen( &self, screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> Vec { self.handles .query_screen(screen_rect, view_rot, eye, bounds) } } impl InteractionIndex { pub fn estimated_work(wires: &[WireModel]) -> usize { wires.iter().fold(0usize, |total, wire| { total .saturating_add(wire.points.len()) .saturating_add(wire.key_vertices.len()) .saturating_add(wire.snap_pts.len() * 2) .saturating_add(wire.fill_tris.len()) .saturating_add(wire.pick_tris.len()) .saturating_add(wire.text_verts.len() / 2) }) } pub fn build(wires: &[WireModel]) -> Self { #[cfg(not(target_arch = "wasm32"))] let perf = crate::perf::enabled(); #[cfg(not(target_arch = "wasm32"))] let build_started = iced::time::Instant::now(); let wire_handles: Vec> = wires .iter() .map(|wire| wire.name.parse::().ok()) .collect(); let mut next_ordinal: rustc_hash::FxHashMap = rustc_hash::FxHashMap::default(); let wire_ordinals: Vec> = wire_handles .iter() .map(|handle| { let handle = (*handle)?; let ordinal = next_ordinal.entry(handle).or_default(); let current = *ordinal; *ordinal += 1; Some(current) }) .collect(); #[cfg(not(target_arch = "wasm32"))] let handles_elapsed = build_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] let collect_started = iced::time::Instant::now(); let mut wire_entries = Vec::with_capacity(wires.len()); let mut unbounded_wires = Vec::new(); let mut screen_unbounded_wires = Vec::new(); let mut max_line_half_width_px = 0.0f32; let mut max_marker_radius_fraction = 0.0f32; #[cfg(not(target_arch = "wasm32"))] let per_wire: Vec = { use crate::par::prelude::*; wires .par_iter() .enumerate() .map(|(index, wire)| collect_wire_index_entries(index as u32, wire)) .collect() }; #[cfg(target_arch = "wasm32")] let per_wire: Vec = wires .iter() .enumerate() .map(|(index, wire)| collect_wire_index_entries(index as u32, wire)) .collect(); #[cfg(not(target_arch = "wasm32"))] let collect_elapsed = collect_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] let flatten_started = iced::time::Instant::now(); let mut segment_parts = Vec::with_capacity(per_wire.len()); let mut snap_point_parts = Vec::with_capacity(per_wire.len()); let mut key_vertex_parts = Vec::with_capacity(per_wire.len()); let mut key_segment_parts = Vec::with_capacity(per_wire.len()); let mut fill_triangle_parts = Vec::with_capacity(per_wire.len()); let mut pick_triangle_parts = Vec::with_capacity(per_wire.len()); let mut glyph_parts = Vec::with_capacity(per_wire.len()); for (wire_idx, entries) in per_wire.into_iter().enumerate() { max_line_half_width_px = max_line_half_width_px.max(entries.max_line_half_width_px); max_marker_radius_fraction = max_marker_radius_fraction.max(entries.max_marker_radius_fraction); if let Some(entry) = entries.wire { wire_entries.push(entry); } if entries.unbounded { unbounded_wires.push(wire_idx as u32); } if entries.screen_unbounded { screen_unbounded_wires.push(wire_idx as u32); } segment_parts.push(entries.segments); snap_point_parts.push(entries.snap_points); key_vertex_parts.push(entries.key_vertices); key_segment_parts.push(entries.key_segments); fill_triangle_parts.push(entries.fill_triangles); pick_triangle_parts.push(entries.pick_triangles); glyph_parts.push(entries.glyphs); } #[cfg(not(target_arch = "wasm32"))] let ( ((segment_entries, snap_point_entries), (key_vertex_entries, key_segment_entries)), ((fill_triangle_entries, pick_triangle_entries), glyph_entries), ) = rayon::join( || { rayon::join( || { rayon::join( || flatten_entry_parts(segment_parts), || flatten_entry_parts(snap_point_parts), ) }, || { rayon::join( || flatten_entry_parts(key_vertex_parts), || flatten_entry_parts(key_segment_parts), ) }, ) }, || { let (fill, pick) = rayon::join( || flatten_entry_parts(fill_triangle_parts), || flatten_entry_parts(pick_triangle_parts), ); ((fill, pick), flatten_entry_parts(glyph_parts)) }, ); #[cfg(target_arch = "wasm32")] let ( segment_entries, snap_point_entries, key_vertex_entries, key_segment_entries, fill_triangle_entries, pick_triangle_entries, glyph_entries, ) = ( flatten_entry_parts(segment_parts), flatten_entry_parts(snap_point_parts), flatten_entry_parts(key_vertex_parts), flatten_entry_parts(key_segment_parts), flatten_entry_parts(fill_triangle_parts), flatten_entry_parts(pick_triangle_parts), flatten_entry_parts(glyph_parts), ); #[cfg(not(target_arch = "wasm32"))] let flatten_elapsed = flatten_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] let spatial_started = iced::time::Instant::now(); #[cfg(not(target_arch = "wasm32"))] let ( ((wires, segments), (snap_points, key_vertices)), ((key_segments, fill_triangles), (pick_triangles, glyphs)), ) = rayon::join( || { rayon::join( || { rayon::join( || SpatialSet::build(wire_entries), || SpatialSet::build(segment_entries), ) }, || { rayon::join( || SpatialSet::build(snap_point_entries), || SpatialSet::build(key_vertex_entries), ) }, ) }, || { rayon::join( || { rayon::join( || SpatialSet::build(key_segment_entries), || SpatialSet::build(fill_triangle_entries), ) }, || { rayon::join( || SpatialSet::build(pick_triangle_entries), || SpatialSet::build(glyph_entries), ) }, ) }, ); #[cfg(target_arch = "wasm32")] let ( wires, segments, snap_points, key_vertices, key_segments, fill_triangles, pick_triangles, glyphs, ) = ( SpatialSet::build(wire_entries), SpatialSet::build(segment_entries), SpatialSet::build(snap_point_entries), SpatialSet::build(key_vertex_entries), SpatialSet::build(key_segment_entries), SpatialSet::build(fill_triangle_entries), SpatialSet::build(pick_triangle_entries), SpatialSet::build(glyph_entries), ); #[cfg(not(target_arch = "wasm32"))] if perf { crate::perf_record!( "[perf] interaction-index-detail total={:.1}ms handles={:.1} collect={:.1} flatten={:.1} spatial={:.1}", build_started.elapsed().as_secs_f64() * 1000.0, handles_elapsed.as_secs_f64() * 1000.0, collect_elapsed.as_secs_f64() * 1000.0, flatten_elapsed.as_secs_f64() * 1000.0, spatial_started.elapsed().as_secs_f64() * 1000.0, ); } Self { wires, wire_handles, wire_ordinals, segments, snap_points, key_vertices, key_segments, fill_triangles, pick_triangles, glyphs, unbounded_wires, screen_unbounded_wires, max_line_half_width_px, max_marker_radius_fraction, } } /// Build every projected 3D broad phase before the index reaches the UI /// thread. Perspective/orbit hover must never pay this one-time cost. pub fn prepare_screen(&self) { #[cfg(not(target_arch = "wasm32"))] rayon::join( || { rayon::join( || rayon::join(|| self.wires.prepare_screen(), || self.segments.prepare_screen()), || { rayon::join( || self.snap_points.prepare_screen(), || self.key_vertices.prepare_screen(), ) }, ) }, || { rayon::join( || { rayon::join( || self.key_segments.prepare_screen(), || self.fill_triangles.prepare_screen(), ) }, || { rayon::join( || self.pick_triangles.prepare_screen(), || self.glyphs.prepare_screen(), ) }, ) }, ); #[cfg(target_arch = "wasm32")] { self.wires.prepare_screen(); self.segments.prepare_screen(); self.snap_points.prepare_screen(); self.key_vertices.prepare_screen(); self.key_segments.prepare_screen(); self.fill_triangles.prepare_screen(); self.pick_triangles.prepare_screen(); self.glyphs.prepare_screen(); } } pub fn pick_radius_px( &self, base_radius_px: f32, viewport_height_px: f32, include_line_weight: bool, ) -> f32 { let radius = base_radius_px .max(self.max_marker_radius_fraction * viewport_height_px.max(0.0)); if include_line_weight { radius.max(self.max_line_half_width_px) } else { radius } } fn queried_wire_keys( &self, mut indices: Vec, include_screen_unbounded: bool, ) -> Vec<(u64, u32)> { indices.extend_from_slice(&self.unbounded_wires); if include_screen_unbounded { indices.extend_from_slice(&self.screen_unbounded_wires); } let mut keys: Vec<(u64, u32)> = indices .into_iter() .filter_map(|index| { self.wire_handles .get(index as usize) .copied() .flatten() .zip( self.wire_ordinals .get(index as usize) .copied() .flatten(), ) }) .collect(); keys.sort_unstable(); keys.dedup(); keys } pub fn query_wire_keys_xy(&self, aabb: [f64; 4]) -> Vec<(u64, u32)> { self.queried_wire_keys(self.wires.query_xy(aabb), false) } pub fn query_wire_keys_screen( &self, screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> Vec<(u64, u32)> { self.queried_wire_keys( self.wires.query_screen(screen_rect, view_rot, eye, bounds), true, ) } fn remap_wire_index( &self, index: u32, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, ) -> Option { let handle = self .wire_handles .get(index as usize) .copied() .flatten()?; let ordinal = self .wire_ordinals .get(index as usize) .copied() .flatten()?; slots.get(&(handle, ordinal)).copied() } fn remap_wire_indices( &self, mut indices: Vec, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, include_screen_unbounded: bool, ) -> Vec { indices.extend_from_slice(&self.unbounded_wires); if include_screen_unbounded { indices.extend_from_slice(&self.screen_unbounded_wires); } let mut local: Vec = indices .into_iter() .filter_map(|index| self.remap_wire_index(index, slots)) .collect(); local.sort_unstable(); local.dedup(); local } fn remap_refs( &self, values: Vec, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, wire_of: impl Fn(&T) -> u32, set_wire: impl Fn(&mut T, u32), ) -> Vec { values .into_iter() .filter_map(|mut value| { let wire = self.remap_wire_index(wire_of(&value), slots)?; set_wire(&mut value, wire); Some(value) }) .collect() } pub(crate) fn query_remapped_xy( &self, wires: Arc>, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, aabb: [f64; 4], ) -> InteractionCandidates { InteractionCandidates { wires, wire_indices: Some(self.remap_wire_indices( self.wires.query_xy(aabb), slots, false, )), segments: Some(self.remap_refs( self.segments.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), snap_points: Some(self.remap_refs( self.snap_points.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), key_vertices: Some(self.remap_refs( self.key_vertices.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), key_segments: Some(self.remap_refs( self.key_segments.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), fill_triangles: Some(self.remap_refs( self.fill_triangles.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), pick_triangles: Some(self.remap_refs( self.pick_triangles.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), glyphs: Some(self.remap_refs( self.glyphs.query_xy(aabb), slots, |value| value.wire, |value, wire| value.wire = wire, )), query_aabb: Some(aabb), screen_rect: None, screen_view: None, } } pub(crate) fn query_remapped_screen( &self, wires: Arc>, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> InteractionCandidates { InteractionCandidates { wires, wire_indices: Some(self.remap_wire_indices( self.wires .query_screen(screen_rect, view_rot, eye, bounds), slots, true, )), segments: Some(self.remap_refs( self.segments .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), snap_points: Some(self.remap_refs( self.snap_points .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), key_vertices: Some(self.remap_refs( self.key_vertices .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), key_segments: Some(self.remap_refs( self.key_segments .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), fill_triangles: Some(self.remap_refs( self.fill_triangles .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), pick_triangles: Some(self.remap_refs( self.pick_triangles .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), glyphs: Some(self.remap_refs( self.glyphs .query_screen(screen_rect, view_rot, eye, bounds), slots, |value| value.wire, |value, wire| value.wire = wire, )), query_aabb: None, screen_rect: Some(screen_rect), screen_view: Some((view_rot, eye, bounds)), } } pub fn query_xy(&self, wires: Arc>, aabb: [f64; 4]) -> InteractionCandidates { let mut wire_indices = self.wires.query_xy(aabb); wire_indices.extend_from_slice(&self.unbounded_wires); wire_indices.sort_unstable(); wire_indices.dedup(); InteractionCandidates { wires, wire_indices: Some(wire_indices), segments: Some(self.segments.query_xy(aabb)), snap_points: Some(self.snap_points.query_xy(aabb)), key_vertices: Some(self.key_vertices.query_xy(aabb)), key_segments: Some(self.key_segments.query_xy(aabb)), fill_triangles: Some(self.fill_triangles.query_xy(aabb)), pick_triangles: Some(self.pick_triangles.query_xy(aabb)), glyphs: Some(self.glyphs.query_xy(aabb)), query_aabb: Some(aabb), screen_rect: None, screen_view: None, } } pub fn query_screen( &self, wires: Arc>, screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> InteractionCandidates { let mut wire_indices = self.wires.query_screen(screen_rect, view_rot, eye, bounds); wire_indices.extend_from_slice(&self.unbounded_wires); wire_indices.extend_from_slice(&self.screen_unbounded_wires); wire_indices.sort_unstable(); wire_indices.dedup(); InteractionCandidates { wires, wire_indices: Some(wire_indices), segments: Some( self.segments .query_screen(screen_rect, view_rot, eye, bounds), ), snap_points: Some( self.snap_points .query_screen(screen_rect, view_rot, eye, bounds), ), key_vertices: Some( self.key_vertices .query_screen(screen_rect, view_rot, eye, bounds), ), key_segments: Some( self.key_segments .query_screen(screen_rect, view_rot, eye, bounds), ), fill_triangles: Some(self.fill_triangles.query_screen( screen_rect, view_rot, eye, bounds, )), pick_triangles: Some(self.pick_triangles.query_screen( screen_rect, view_rot, eye, bounds, )), glyphs: Some(self.glyphs.query_screen(screen_rect, view_rot, eye, bounds)), query_aabb: None, screen_rect: Some(screen_rect), screen_view: Some((view_rot, eye, bounds)), } } } impl std::fmt::Debug for InteractionIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InteractionIndex") .field("wires", &self.wire_handles.len()) .field("segments", &self.segments.entries.len()) .field("fill_triangles", &self.fill_triangles.entries.len()) .field("pick_triangles", &self.pick_triangles.entries.len()) .finish() } } pub struct InteractionCandidates { wires: Arc>, wire_indices: Option>, segments: Option>, snap_points: Option>, key_vertices: Option>, key_segments: Option>, fill_triangles: Option>, pick_triangles: Option>, glyphs: Option>, query_aabb: Option<[f64; 4]>, screen_rect: Option<[f32; 4]>, screen_view: Option<(Mat4, DVec3, Rectangle)>, } pub trait WireSource { fn iter(&self) -> WireIter<'_>; fn len(&self) -> usize; fn get(&self, index: usize) -> Option<&WireModel>; fn segments(&self) -> Option<&[SegmentRef]> { None } fn snap_points(&self) -> Option<&[SnapPointRef]> { None } fn key_vertices(&self) -> Option<&[KeyVertexRef]> { None } fn key_segments(&self) -> Option<&[KeySegmentRef]> { None } fn fill_triangles(&self) -> Option<&[TriangleRef]> { None } fn pick_triangles(&self) -> Option<&[TriangleRef]> { None } fn glyphs(&self) -> Option<&[GlyphRef]> { None } fn source_wire(&self, index: u32) -> Option<&WireModel>; } impl InteractionCandidates { pub fn all(wires: Arc>) -> Self { Self { wires, wire_indices: None, segments: None, snap_points: None, key_vertices: None, key_segments: None, fill_triangles: None, pick_triangles: None, glyphs: None, query_aabb: None, screen_rect: None, screen_view: None, } } /// Empty but explicitly indexed candidate set. Used while a large source /// is still being prepared off-thread so UI input never falls back to a /// full-scene scan. pub fn pending(wires: Arc>) -> Self { Self { wires, wire_indices: Some(Vec::new()), segments: Some(Vec::new()), snap_points: Some(Vec::new()), key_vertices: Some(Vec::new()), key_segments: Some(Vec::new()), fill_triangles: Some(Vec::new()), pick_triangles: Some(Vec::new()), glyphs: Some(Vec::new()), query_aabb: None, screen_rect: None, screen_view: None, } } pub fn is_indexed(&self) -> bool { self.wire_indices.is_some() } pub fn iter(&self) -> WireIter<'_> { match &self.wire_indices { Some(indices) => WireIter::Indexed { wires: &self.wires, indices: indices.iter(), }, None => WireIter::All(self.wires.as_slice().iter()), } } pub fn len(&self) -> usize { self.wire_indices .as_ref() .map_or(self.wires.len(), Vec::len) } pub fn get(&self, index: usize) -> Option<&WireModel> { match &self.wire_indices { Some(indices) => indices .get(index) .and_then(|&wire| self.wires.get(wire as usize)), None => self.wires.get(index), } } pub fn segments(&self) -> Option<&[SegmentRef]> { self.segments.as_deref() } pub fn snap_points(&self) -> Option<&[SnapPointRef]> { self.snap_points.as_deref() } pub fn key_vertices(&self) -> Option<&[KeyVertexRef]> { self.key_vertices.as_deref() } pub fn key_segments(&self) -> Option<&[KeySegmentRef]> { self.key_segments.as_deref() } pub fn fill_triangles(&self) -> Option<&[TriangleRef]> { self.fill_triangles.as_deref() } pub fn pick_triangles(&self) -> Option<&[TriangleRef]> { self.pick_triangles.as_deref() } pub fn glyphs(&self) -> Option<&[GlyphRef]> { self.glyphs.as_deref() } pub fn source_wire(&self, index: u32) -> Option<&WireModel> { self.wires.get(index as usize) } pub fn query_aabb(&self) -> Option<[f64; 4]> { self.query_aabb } pub fn screen_query(&self) -> Option<([f32; 4], Mat4, DVec3, Rectangle)> { self.screen_rect .zip(self.screen_view) .map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds)) } pub(crate) fn extend_indexed(&mut self, other: Self) { debug_assert!(Arc::ptr_eq(&self.wires, &other.wires)); fn extend(target: &mut Option>, incoming: Option>) { let (Some(target), Some(incoming)) = (target.as_mut(), incoming) else { return; }; target.extend(incoming); target.sort_unstable(); target.dedup(); } extend(&mut self.wire_indices, other.wire_indices); extend(&mut self.segments, other.segments); extend(&mut self.snap_points, other.snap_points); extend(&mut self.key_vertices, other.key_vertices); extend(&mut self.key_segments, other.key_segments); extend(&mut self.fill_triangles, other.fill_triangles); extend(&mut self.pick_triangles, other.pick_triangles); extend(&mut self.glyphs, other.glyphs); } } impl WireSource for InteractionCandidates { fn iter(&self) -> WireIter<'_> { self.iter() } fn len(&self) -> usize { self.len() } fn get(&self, index: usize) -> Option<&WireModel> { self.get(index) } fn segments(&self) -> Option<&[SegmentRef]> { self.segments() } fn snap_points(&self) -> Option<&[SnapPointRef]> { self.snap_points() } fn key_vertices(&self) -> Option<&[KeyVertexRef]> { self.key_vertices() } fn key_segments(&self) -> Option<&[KeySegmentRef]> { self.key_segments() } fn fill_triangles(&self) -> Option<&[TriangleRef]> { self.fill_triangles() } fn pick_triangles(&self) -> Option<&[TriangleRef]> { self.pick_triangles() } fn glyphs(&self) -> Option<&[GlyphRef]> { self.glyphs() } fn source_wire(&self, index: u32) -> Option<&WireModel> { self.source_wire(index) } } impl WireSource for [WireModel] { fn iter(&self) -> WireIter<'_> { WireIter::All(<[WireModel]>::iter(self)) } fn len(&self) -> usize { <[WireModel]>::len(self) } fn get(&self, index: usize) -> Option<&WireModel> { <[WireModel]>::get(self, index) } fn source_wire(&self, index: u32) -> Option<&WireModel> { self.get(index as usize) } } impl WireSource for Vec { fn iter(&self) -> WireIter<'_> { WireIter::All(self.as_slice().iter()) } fn len(&self) -> usize { Vec::len(self) } fn get(&self, index: usize) -> Option<&WireModel> { self.as_slice().get(index) } fn source_wire(&self, index: u32) -> Option<&WireModel> { self.get(index as usize) } } impl WireSource for [WireModel; N] { fn iter(&self) -> WireIter<'_> { WireIter::All(self.as_slice().iter()) } fn len(&self) -> usize { N } fn get(&self, index: usize) -> Option<&WireModel> { self.as_slice().get(index) } fn source_wire(&self, index: u32) -> Option<&WireModel> { self.get(index as usize) } } #[derive(Clone)] pub enum WireIter<'a> { All(std::slice::Iter<'a, WireModel>), Indexed { wires: &'a [WireModel], indices: std::slice::Iter<'a, u32>, }, } impl<'a> Iterator for WireIter<'a> { type Item = &'a WireModel; fn next(&mut self) -> Option { match self { Self::All(iter) => iter.next(), Self::Indexed { wires, indices } => { indices.next().and_then(|&idx| wires.get(idx as usize)) } } } } fn finite_wire_aabb3(wire: &WireModel) -> Option<[f64; 6]> { if let Some(marker) = wire.point_marker { if !marker.origin.is_finite() { return None; } let normal = marker.normal.try_normalize().unwrap_or(DVec3::Z); let mut min_axial = 0.0f64; let mut max_axial = 0.0f64; for index in 0..wire.points.len() { let point = DVec3::from_array(wire_point(wire, index)); if point.is_finite() { let axial = (point - marker.origin).dot(normal); min_axial = min_axial.min(axial); max_axial = max_axial.max(axial); } } return points_aabb3([ (marker.origin + normal * min_axial).to_array(), (marker.origin + normal * max_axial).to_array(), ]); } let [min_x, min_y, max_x, max_y] = wire.aabb; if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() { return None; } let mag = wire.aabb.iter().fold(0.0f32, |m, c| m.max(c.abs())); let pad = (mag * f32::EPSILON * 2.0) as f64; let mut aabb = [ min_x as f64 - pad, min_y as f64 - pad, f64::INFINITY, max_x as f64 + pad, max_y as f64 + pad, f64::NEG_INFINITY, ]; let mut include = |point: [f64; 3]| { if point.iter().all(|value| value.is_finite()) { aabb[0] = aabb[0].min(point[0]); aabb[1] = aabb[1].min(point[1]); aabb[2] = aabb[2].min(point[2]); aabb[3] = aabb[3].max(point[0]); aabb[4] = aabb[4].max(point[1]); aabb[5] = aabb[5].max(point[2]); } }; for index in 0..wire.points.len() { include(wire_point(wire, index)); } for &(point, _) in &wire.snap_pts { include(point.to_array()); } for &point in &wire.key_vertices { include(point); } for (index, &point) in wire.fill_tris.iter().enumerate() { include(point_with_low(point, &wire.fill_tris_low, index)); } for (index, &point) in wire.pick_tris.iter().enumerate() { include(point_with_low(point, &wire.pick_tris_low, index)); } for vertex in &wire.text_verts { include([ vertex.pos[0] as f64 + vertex.pos_low[0] as f64, vertex.pos[1] as f64 + vertex.pos_low[1] as f64, vertex.pos[2] as f64 + vertex.pos_low[2] as f64, ]); } if !aabb[2].is_finite() { aabb[2] = 0.0; aabb[5] = 0.0; } Some(aabb) } fn wire_point(wire: &WireModel, index: usize) -> [f64; 3] { let high = wire.points[index]; let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); [ high[0] as f64 + low[0] as f64, high[1] as f64 + low[1] as f64, high[2] as f64 + low[2] as f64, ] } fn point_with_low(point: [f32; 3], low: &[[f32; 3]], index: usize) -> [f64; 3] { let residual = low.get(index).copied().unwrap_or([0.0; 3]); [ point[0] as f64 + residual[0] as f64, point[1] as f64 + residual[1] as f64, point[2] as f64 + residual[2] as f64, ] } fn points_aabb3(points: impl IntoIterator) -> Option<[f64; 6]> { let mut aabb = [ f64::INFINITY, f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, ]; let mut any = false; for point in points { if !point.iter().all(|value| value.is_finite()) { return None; } any = true; for axis in 0..3 { aabb[axis] = aabb[axis].min(point[axis]); aabb[axis + 3] = aabb[axis + 3].max(point[axis]); } } any.then_some(aabb) } fn append_triangle_entries( wire: u32, points: &[[f32; 3]], low: &[[f32; 3]], out: &mut Vec>, ) { for start in (0..points.len()).step_by(3) { if start + 2 >= points.len() { break; } let Some(aabb) = points_aabb3((start..start + 3).map(|index| point_with_low(points[index], low, index))) else { continue; }; out.push(Entry3 { aabb, value: TriangleRef { wire, start: start as u32, }, }); } } fn aabb3_projects_into( aabb: [f64; 6], screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, bounds: Rectangle, ) -> bool { let mut projected = [ f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, ]; for x in [aabb[0], aabb[3]] { for y in [aabb[1], aabb[4]] { for z in [aabb[2], aabb[5]] { let clip = view_rot * (DVec3::new(x, y, z) - eye).as_vec3().extend(1.0); if !clip.is_finite() || clip.w <= 1e-6 { return true; } let ndc = clip.truncate() / clip.w; let sx = (ndc.x + 1.0) * 0.5 * bounds.width; let sy = (1.0 - ndc.y) * 0.5 * bounds.height; projected[0] = projected[0].min(sx); projected[1] = projected[1].min(sy); projected[2] = projected[2].max(sx); projected[3] = projected[3].max(sy); } } } projected[2] >= screen_rect[0] && projected[0] <= screen_rect[2] && projected[3] >= screen_rect[1] && projected[1] <= screen_rect[3] } fn aabb_overlaps(a: [f64; 4], b: [f64; 4]) -> bool { a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] && a[1] <= b[3] }