forked from 1b-agent/opencadstudio-upstream
Optimize InteractionIndex::build by eliminating per-wire vector allocation churn and combining wire handle / ordinal resolution: 1. Introduced WireIndexBatch to chunk wires into batches of 256 wires. Each batch pre-allocates vector capacities for its slice and appends entries directly, eliminating ~70,000 per-wire heap allocations and deallocations across 10,000 wires. 2. Flattened batch vectors directly with exact pre-calculated capacities into the final entry lists, removing flatten_entry_parts and nested vector copies. 3. Streamlined wire handle parsing and ordinal tracking into a single pass with pre-sized vectors and pre-allocated FxHashMap. Performance gains: - spatial_interaction_index_build (10k wires): - Latency: 4.88 ms -> 4.01 ms (-17.8%) - Mean latency: 4.93 ms -> 4.05 ms (-17.8%) - Throughput: 2.05M wires/s -> 2.53M wires/s (+23.4%) - batch_entity_transform_mutation (1k entities translated & re-indexed): - Latency: 10.92 ms -> 8.95 ms (-18.0%) - Mean latency: 11.28 ms -> 9.52 ms (-15.6%) - Throughput: 92.4k entities/s -> 111.3k entities/s (+20.5%) - Correctness: 100% pass across all 19 snap, 4 hit_test, and 3 interaction_index tests.
2136 lines
70 KiB
Rust
2136 lines
70 KiB
Rust
//! 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<T> {
|
||
aabb: [f64; 6],
|
||
value: T,
|
||
}
|
||
|
||
fn entry_aabb2<T>(entry: &Entry3<T>) -> [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<BvhNode2>,
|
||
order: Vec<u32>,
|
||
}
|
||
|
||
impl SpatialBvh2 {
|
||
fn build<T>(entries: &[Entry3<T>], mut order: Vec<u32>) -> Self {
|
||
let mut nodes = Vec::new();
|
||
if !order.is_empty() {
|
||
build_bvh2_node(entries, &mut order, 0, &mut nodes);
|
||
}
|
||
Self { nodes, order }
|
||
}
|
||
|
||
fn query<T>(&self, entries: &[Entry3<T>], query: [f64; 4], out: &mut Vec<u32>) {
|
||
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<T>(
|
||
entries: &[Entry3<T>],
|
||
order: &mut [u32],
|
||
base: usize,
|
||
nodes: &mut Vec<BvhNode2>,
|
||
) -> 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<BvhNode3>,
|
||
order: Vec<u32>,
|
||
}
|
||
|
||
impl SpatialBvh3 {
|
||
fn build<T>(entries: &[Entry3<T>]) -> Self {
|
||
let mut order: Vec<u32> = (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<T: Copy + Ord>(
|
||
&self,
|
||
entries: &[Entry3<T>],
|
||
screen_rect: [f32; 4],
|
||
view_rot: Mat4,
|
||
eye: DVec3,
|
||
bounds: Rectangle,
|
||
) -> Vec<T> {
|
||
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<T>(
|
||
entries: &[Entry3<T>],
|
||
order: &mut [u32],
|
||
base: usize,
|
||
nodes: &mut Vec<BvhNode3>,
|
||
) -> 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<u32>,
|
||
cell_entries: Vec<u32>,
|
||
oversized: SpatialBvh2,
|
||
}
|
||
|
||
impl SpatialGrid {
|
||
fn build<T: Sync>(entries: &[Entry3<T>]) -> 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<AtomicU32> =
|
||
(0..cell_count).map(|_| AtomicU32::new(0)).collect();
|
||
let oversized: Vec<u32> = 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::<Vec<u32>>(),
|
||
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<AtomicU32> = cell_offsets[..cell_count]
|
||
.iter()
|
||
.copied()
|
||
.map(AtomicU32::new)
|
||
.collect();
|
||
let slots: Vec<AtomicU32> = (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_unordered<T: Copy>(&self, entries: &[Entry3<T>], query: [f64; 4]) -> Vec<T> {
|
||
let mut indices = Vec::new();
|
||
self.oversized.query(entries, query, &mut indices);
|
||
let mut out: Vec<T> = indices
|
||
.into_iter()
|
||
.filter_map(|idx| {
|
||
let entry = entries.get(idx as usize)?;
|
||
aabb_overlaps(entry_aabb2(entry), query).then_some(entry.value)
|
||
})
|
||
.collect();
|
||
|
||
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
|
||
{
|
||
return out;
|
||
}
|
||
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
|
||
};
|
||
let (row_lo, row_hi) = (row(query[1]), row(query[3]));
|
||
let (col_lo, col_hi) = (col(query[0]), col(query[2]));
|
||
|
||
// One growth instead of a chain of reallocations: a box over the whole
|
||
// drawing lands nearly every entry here.
|
||
let spanned: usize = (row_lo..=row_hi)
|
||
.map(|r| {
|
||
let base = r as usize * self.cols as usize;
|
||
let start = self.cell_offsets[base + col_lo as usize] as usize;
|
||
let end = self.cell_offsets[base + col_hi as usize + 1] as usize;
|
||
end.saturating_sub(start)
|
||
})
|
||
.sum();
|
||
out.reserve(spanned);
|
||
|
||
for r in row_lo..=row_hi {
|
||
let base = r as usize * self.cols as usize;
|
||
for c in col_lo..=col_hi {
|
||
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;
|
||
out.extend(self.cell_entries[start..end].iter().filter_map(|&idx| {
|
||
let entry = entries.get(idx as usize)?;
|
||
aabb_overlaps(entry_aabb2(entry), query).then_some(entry.value)
|
||
}));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn query<T: Copy + Ord>(&self, entries: &[Entry3<T>], query: [f64; 4]) -> Vec<T> {
|
||
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<T> = 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<T> {
|
||
entries: Vec<Entry3<T>>,
|
||
xy: SpatialGrid,
|
||
xyz: std::sync::OnceLock<SpatialBvh3>,
|
||
}
|
||
|
||
impl<T: Copy + Ord + Sync> SpatialSet<T> {
|
||
fn build(entries: Vec<Entry3<T>>) -> Self {
|
||
let xy = SpatialGrid::build(&entries);
|
||
Self {
|
||
entries,
|
||
xy,
|
||
xyz: std::sync::OnceLock::new(),
|
||
}
|
||
}
|
||
|
||
fn query_xy(&self, aabb: [f64; 4]) -> Vec<T> {
|
||
self.xy.query(&self.entries, aabb)
|
||
}
|
||
|
||
/// See [`SpatialGrid::query_unordered`] — same set, no sort, duplicates in.
|
||
fn query_xy_unordered(&self, aabb: [f64; 4]) -> Vec<T> {
|
||
self.xy.query_unordered(&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<T> {
|
||
self.xyz
|
||
.get_or_init(|| SpatialBvh3::build(&self.entries))
|
||
.query_screen(&self.entries, screen_rect, view_rot, eye, bounds)
|
||
}
|
||
}
|
||
|
||
pub struct InteractionIndex {
|
||
wires: SpatialSet<u32>,
|
||
/// 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<Option<u64>>,
|
||
/// 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<Option<u32>>,
|
||
segments: SpatialSet<SegmentRef>,
|
||
snap_points: SpatialSet<SnapPointRef>,
|
||
key_vertices: SpatialSet<KeyVertexRef>,
|
||
key_segments: SpatialSet<KeySegmentRef>,
|
||
fill_triangles: SpatialSet<TriangleRef>,
|
||
pick_triangles: SpatialSet<TriangleRef>,
|
||
glyphs: SpatialSet<GlyphRef>,
|
||
unbounded_wires: Vec<u32>,
|
||
screen_unbounded_wires: Vec<u32>,
|
||
max_line_half_width_px: f32,
|
||
max_marker_radius_fraction: f32,
|
||
}
|
||
|
||
pub struct InteractionHandleIndex {
|
||
handles: SpatialSet<u64>,
|
||
}
|
||
|
||
struct WireIndexBatch {
|
||
wire_entries: Vec<Entry3<u32>>,
|
||
segments: Vec<Entry3<SegmentRef>>,
|
||
snap_points: Vec<Entry3<SnapPointRef>>,
|
||
key_vertices: Vec<Entry3<KeyVertexRef>>,
|
||
key_segments: Vec<Entry3<KeySegmentRef>>,
|
||
fill_triangles: Vec<Entry3<TriangleRef>>,
|
||
pick_triangles: Vec<Entry3<TriangleRef>>,
|
||
glyphs: Vec<Entry3<GlyphRef>>,
|
||
unbounded_wires: Vec<u32>,
|
||
screen_unbounded_wires: Vec<u32>,
|
||
max_line_half_width_px: f32,
|
||
max_marker_radius_fraction: f32,
|
||
}
|
||
|
||
impl WireIndexBatch {
|
||
fn with_capacity_for_chunk(chunk: &[WireModel]) -> Self {
|
||
let total_segs: usize = chunk.iter().map(|w| w.points.len().saturating_sub(1)).sum();
|
||
let total_snaps: usize = chunk.iter().map(|w| w.snap_pts.len()).sum();
|
||
let total_key_verts: usize = chunk.iter().map(|w| w.key_vertices.len()).sum();
|
||
let total_key_segs: usize = chunk
|
||
.iter()
|
||
.map(|w| w.key_vertices.len().saturating_sub(1))
|
||
.sum();
|
||
let total_fills: usize = chunk.iter().map(|w| w.fill_tris.len() / 3).sum();
|
||
let total_picks: usize = chunk.iter().map(|w| w.pick_tris.len() / 3).sum();
|
||
let total_glyphs: usize = chunk.iter().map(|w| w.text_verts.len() / 6).sum();
|
||
|
||
Self {
|
||
wire_entries: Vec::with_capacity(chunk.len()),
|
||
segments: Vec::with_capacity(total_segs),
|
||
snap_points: Vec::with_capacity(total_snaps),
|
||
key_vertices: Vec::with_capacity(total_key_verts),
|
||
key_segments: Vec::with_capacity(total_key_segs),
|
||
fill_triangles: Vec::with_capacity(total_fills),
|
||
pick_triangles: Vec::with_capacity(total_picks),
|
||
glyphs: Vec::with_capacity(total_glyphs),
|
||
unbounded_wires: Vec::new(),
|
||
screen_unbounded_wires: Vec::new(),
|
||
max_line_half_width_px: 0.0,
|
||
max_marker_radius_fraction: 0.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn append_wire_index_entries(wire_idx: u32, wire: &WireModel, batch: &mut WireIndexBatch) {
|
||
if let Some(aabb) = finite_wire_aabb3(wire) {
|
||
batch.wire_entries.push(Entry3 {
|
||
aabb,
|
||
value: wire_idx,
|
||
});
|
||
} else {
|
||
batch.unbounded_wires.push(wire_idx);
|
||
}
|
||
if wire.point_marker.is_some() {
|
||
batch.screen_unbounded_wires.push(wire_idx);
|
||
}
|
||
if wire.line_weight_px.is_finite() {
|
||
batch.max_line_half_width_px = batch
|
||
.max_line_half_width_px
|
||
.max((wire.line_weight_px * 0.5).max(0.0));
|
||
}
|
||
if let Some(marker) = wire.point_marker {
|
||
batch.max_marker_radius_fraction = batch.max_marker_radius_fraction.max(
|
||
marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2,
|
||
);
|
||
}
|
||
|
||
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;
|
||
};
|
||
batch.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() {
|
||
batch.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()) {
|
||
batch.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;
|
||
};
|
||
batch.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 batch.fill_triangles,
|
||
);
|
||
append_triangle_entries(
|
||
wire_idx,
|
||
&wire.pick_tris,
|
||
&wire.pick_tris_low,
|
||
&mut batch.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;
|
||
};
|
||
batch.glyphs.push(Entry3 {
|
||
aabb,
|
||
value: GlyphRef {
|
||
wire: wire_idx,
|
||
start: start as u32,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
impl InteractionHandleIndex {
|
||
pub fn build(entries: impl IntoIterator<Item = (u64, [f64; 6])>) -> 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<u64> {
|
||
self.handles.query_xy(aabb)
|
||
}
|
||
|
||
pub fn query_screen(
|
||
&self,
|
||
screen_rect: [f32; 4],
|
||
view_rot: Mat4,
|
||
eye: DVec3,
|
||
bounds: Rectangle,
|
||
) -> Vec<u64> {
|
||
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 n_wires = wires.len();
|
||
let mut wire_handles = Vec::with_capacity(n_wires);
|
||
let mut wire_ordinals = Vec::with_capacity(n_wires);
|
||
let mut next_ordinal: rustc_hash::FxHashMap<u64, u32> =
|
||
rustc_hash::FxHashMap::with_capacity_and_hasher(n_wires.min(4096), Default::default());
|
||
for wire in wires {
|
||
let handle = wire.name.parse::<u64>().ok();
|
||
wire_handles.push(handle);
|
||
wire_ordinals.push(handle.map(|h| {
|
||
let entry = next_ordinal.entry(h).or_default();
|
||
let ord = *entry;
|
||
*entry += 1;
|
||
ord
|
||
}));
|
||
}
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
let handles_elapsed = build_started.elapsed();
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
let collect_started = iced::time::Instant::now();
|
||
|
||
const CHUNK_SIZE: usize = 256;
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
let batches: Vec<WireIndexBatch> = {
|
||
use crate::par::prelude::*;
|
||
wires
|
||
.par_chunks(CHUNK_SIZE)
|
||
.enumerate()
|
||
.map(|(chunk_idx, chunk)| {
|
||
let mut batch = WireIndexBatch::with_capacity_for_chunk(chunk);
|
||
let base_idx = (chunk_idx * CHUNK_SIZE) as u32;
|
||
for (i, wire) in chunk.iter().enumerate() {
|
||
append_wire_index_entries(base_idx + i as u32, wire, &mut batch);
|
||
}
|
||
batch
|
||
})
|
||
.collect()
|
||
};
|
||
#[cfg(target_arch = "wasm32")]
|
||
let batches: Vec<WireIndexBatch> = wires
|
||
.chunks(CHUNK_SIZE)
|
||
.enumerate()
|
||
.map(|(chunk_idx, chunk)| {
|
||
let mut batch = WireIndexBatch::with_capacity_for_chunk(chunk);
|
||
let base_idx = (chunk_idx * CHUNK_SIZE) as u32;
|
||
for (i, wire) in chunk.iter().enumerate() {
|
||
append_wire_index_entries(base_idx + i as u32, wire, &mut batch);
|
||
}
|
||
batch
|
||
})
|
||
.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 total_wires: usize = batches.iter().map(|b| b.wire_entries.len()).sum();
|
||
let total_segments: usize = batches.iter().map(|b| b.segments.len()).sum();
|
||
let total_snaps: usize = batches.iter().map(|b| b.snap_points.len()).sum();
|
||
let total_key_verts: usize = batches.iter().map(|b| b.key_vertices.len()).sum();
|
||
let total_key_segs: usize = batches.iter().map(|b| b.key_segments.len()).sum();
|
||
let total_fills: usize = batches.iter().map(|b| b.fill_triangles.len()).sum();
|
||
let total_picks: usize = batches.iter().map(|b| b.pick_triangles.len()).sum();
|
||
let total_glyphs: usize = batches.iter().map(|b| b.glyphs.len()).sum();
|
||
let total_unbounded: usize = batches.iter().map(|b| b.unbounded_wires.len()).sum();
|
||
let total_screen_unbounded: usize =
|
||
batches.iter().map(|b| b.screen_unbounded_wires.len()).sum();
|
||
|
||
let mut wire_entries = Vec::with_capacity(total_wires);
|
||
let mut segment_entries = Vec::with_capacity(total_segments);
|
||
let mut snap_point_entries = Vec::with_capacity(total_snaps);
|
||
let mut key_vertex_entries = Vec::with_capacity(total_key_verts);
|
||
let mut key_segment_entries = Vec::with_capacity(total_key_segs);
|
||
let mut fill_triangle_entries = Vec::with_capacity(total_fills);
|
||
let mut pick_triangle_entries = Vec::with_capacity(total_picks);
|
||
let mut glyph_entries = Vec::with_capacity(total_glyphs);
|
||
let mut unbounded_wires = Vec::with_capacity(total_unbounded);
|
||
let mut screen_unbounded_wires = Vec::with_capacity(total_screen_unbounded);
|
||
let mut max_line_half_width_px = 0.0f32;
|
||
let mut max_marker_radius_fraction = 0.0f32;
|
||
|
||
for mut batch in batches {
|
||
max_line_half_width_px =
|
||
max_line_half_width_px.max(batch.max_line_half_width_px);
|
||
max_marker_radius_fraction =
|
||
max_marker_radius_fraction.max(batch.max_marker_radius_fraction);
|
||
wire_entries.append(&mut batch.wire_entries);
|
||
segment_entries.append(&mut batch.segments);
|
||
snap_point_entries.append(&mut batch.snap_points);
|
||
key_vertex_entries.append(&mut batch.key_vertices);
|
||
key_segment_entries.append(&mut batch.key_segments);
|
||
fill_triangle_entries.append(&mut batch.fill_triangles);
|
||
pick_triangle_entries.append(&mut batch.pick_triangles);
|
||
glyph_entries.append(&mut batch.glyphs);
|
||
unbounded_wires.append(&mut batch.unbounded_wires);
|
||
screen_unbounded_wires.append(&mut batch.screen_unbounded_wires);
|
||
}
|
||
|
||
#[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)),
|
||
) = crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| SpatialSet::build(wire_entries),
|
||
|| SpatialSet::build(segment_entries),
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::join(
|
||
|| SpatialSet::build(snap_point_entries),
|
||
|| SpatialSet::build(key_vertex_entries),
|
||
)
|
||
},
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| SpatialSet::build(key_segment_entries),
|
||
|| SpatialSet::build(fill_triangle_entries),
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::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"))]
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| crate::par::join(|| self.wires.prepare_screen(), || self.segments.prepare_screen()),
|
||
|| {
|
||
crate::par::join(
|
||
|| self.snap_points.prepare_screen(),
|
||
|| self.key_vertices.prepare_screen(),
|
||
)
|
||
},
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| self.key_segments.prepare_screen(),
|
||
|| self.fill_triangles.prepare_screen(),
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::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<u32>,
|
||
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<u32> {
|
||
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<u32>,
|
||
slots: &rustc_hash::FxHashMap<(u64, u32), u32>,
|
||
include_screen_unbounded: bool,
|
||
) -> Vec<u32> {
|
||
indices.extend_from_slice(&self.unbounded_wires);
|
||
if include_screen_unbounded {
|
||
indices.extend_from_slice(&self.screen_unbounded_wires);
|
||
}
|
||
let mut local: Vec<u32> = indices
|
||
.into_iter()
|
||
.filter_map(|index| self.remap_wire_index(index, slots))
|
||
.collect();
|
||
local.sort_unstable();
|
||
local.dedup();
|
||
local
|
||
}
|
||
|
||
fn remap_refs<T>(
|
||
&self,
|
||
values: Vec<T>,
|
||
slots: &rustc_hash::FxHashMap<(u64, u32), u32>,
|
||
wire_of: impl Fn(&T) -> u32,
|
||
set_wire: impl Fn(&mut T, u32),
|
||
) -> Vec<T> {
|
||
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_area(
|
||
&self,
|
||
wires: Arc<Vec<WireModel>>,
|
||
slots: &rustc_hash::FxHashMap<(u64, u32), u32>,
|
||
aabb: [f64; 4],
|
||
) -> InteractionCandidates {
|
||
InteractionCandidates {
|
||
wires,
|
||
wire_indices: Some(self.remap_wire_indices(
|
||
self.wires.query_xy_unordered(aabb),
|
||
slots,
|
||
false,
|
||
)),
|
||
segments: Some(self.remap_refs(
|
||
self.segments.query_xy_unordered(aabb),
|
||
slots,
|
||
|value| value.wire,
|
||
|value, wire| value.wire = wire,
|
||
)),
|
||
snap_points: None,
|
||
key_vertices: None,
|
||
key_segments: None,
|
||
fill_triangles: Some(self.remap_refs(
|
||
self.fill_triangles.query_xy_unordered(aabb),
|
||
slots,
|
||
|value| value.wire,
|
||
|value, wire| value.wire = wire,
|
||
)),
|
||
pick_triangles: Some(self.remap_refs(
|
||
self.pick_triangles.query_xy_unordered(aabb),
|
||
slots,
|
||
|value| value.wire,
|
||
|value, wire| value.wire = wire,
|
||
)),
|
||
glyphs: Some(self.remap_refs(
|
||
self.glyphs.query_xy_unordered(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_xy(
|
||
&self,
|
||
wires: Arc<Vec<WireModel>>,
|
||
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<Vec<WireModel>>,
|
||
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_area(
|
||
&self,
|
||
wires: Arc<Vec<WireModel>>,
|
||
aabb: [f64; 4],
|
||
) -> InteractionCandidates {
|
||
let ((wire_indices, segments), ((fill_triangles, pick_triangles), glyphs)) =
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| {
|
||
let mut indices = self.wires.query_xy_unordered(aabb);
|
||
indices.extend_from_slice(&self.unbounded_wires);
|
||
indices.sort_unstable();
|
||
indices.dedup();
|
||
indices
|
||
},
|
||
|| self.segments.query_xy_unordered(aabb),
|
||
)
|
||
},
|
||
|| {
|
||
crate::par::join(
|
||
|| {
|
||
crate::par::join(
|
||
|| self.fill_triangles.query_xy_unordered(aabb),
|
||
|| self.pick_triangles.query_xy_unordered(aabb),
|
||
)
|
||
},
|
||
|| self.glyphs.query_xy_unordered(aabb),
|
||
)
|
||
},
|
||
);
|
||
InteractionCandidates {
|
||
wires,
|
||
wire_indices: Some(wire_indices),
|
||
segments: Some(segments),
|
||
snap_points: None,
|
||
key_vertices: None,
|
||
key_segments: None,
|
||
fill_triangles: Some(fill_triangles),
|
||
pick_triangles: Some(pick_triangles),
|
||
glyphs: Some(glyphs),
|
||
query_aabb: Some(aabb),
|
||
screen_rect: None,
|
||
screen_view: None,
|
||
}
|
||
}
|
||
|
||
pub fn query_xy(&self, wires: Arc<Vec<WireModel>>, 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<Vec<WireModel>>,
|
||
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<Vec<WireModel>>,
|
||
wire_indices: Option<Vec<u32>>,
|
||
segments: Option<Vec<SegmentRef>>,
|
||
snap_points: Option<Vec<SnapPointRef>>,
|
||
key_vertices: Option<Vec<KeyVertexRef>>,
|
||
key_segments: Option<Vec<KeySegmentRef>>,
|
||
fill_triangles: Option<Vec<TriangleRef>>,
|
||
pick_triangles: Option<Vec<TriangleRef>>,
|
||
glyphs: Option<Vec<GlyphRef>>,
|
||
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<Vec<WireModel>>) -> 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<Vec<WireModel>>) -> 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_area(&mut self, other: Self) {
|
||
debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));
|
||
|
||
fn append<T>(target: &mut Option<Vec<T>>, incoming: Option<Vec<T>>) {
|
||
let (Some(target), Some(incoming)) = (target.as_mut(), incoming) else {
|
||
return;
|
||
};
|
||
target.extend(incoming);
|
||
}
|
||
|
||
if let (Some(target), Some(incoming)) =
|
||
(self.wire_indices.as_mut(), other.wire_indices)
|
||
{
|
||
target.extend(incoming);
|
||
target.sort_unstable();
|
||
target.dedup();
|
||
}
|
||
append(&mut self.segments, other.segments);
|
||
append(&mut self.fill_triangles, other.fill_triangles);
|
||
append(&mut self.pick_triangles, other.pick_triangles);
|
||
append(&mut self.glyphs, other.glyphs);
|
||
}
|
||
|
||
pub(crate) fn extend_indexed(&mut self, other: Self) {
|
||
debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));
|
||
|
||
fn extend<T: Ord>(target: &mut Option<Vec<T>>, incoming: Option<Vec<T>>) {
|
||
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<WireModel> {
|
||
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<const N: usize> 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<Self::Item> {
|
||
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<Item = [f64; 3]>) -> 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<Entry3<TriangleRef>>,
|
||
) {
|
||
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]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod area_query_tests {
|
||
use super::*;
|
||
use iced::{Point, Rectangle};
|
||
|
||
fn wire(name: &str, pts: Vec<[f32; 3]>, aabb: [f32; 4]) -> WireModel {
|
||
let mut w = WireModel::solid(name.to_string(), pts, [1.0; 4], false);
|
||
w.aabb = aabb;
|
||
w
|
||
}
|
||
|
||
fn sample() -> Vec<WireModel> {
|
||
vec![
|
||
wire("5", vec![[-0.5, -0.5, 0.0], [0.5, 0.5, 0.0]], [-0.5, -0.5, 0.5, 0.5]),
|
||
wire("9", vec![[0.1, -0.9, 0.0], [0.9, -0.1, 0.0]], [0.1, -0.9, 0.9, -0.1]),
|
||
wire("13", vec![[-0.9, 0.2, 0.0], [-0.2, 0.9, 0.0]], [-0.9, 0.2, -0.2, 0.9]),
|
||
]
|
||
}
|
||
|
||
#[test]
|
||
fn area_query_matches_full_query_where_it_is_read() {
|
||
let wires = sample();
|
||
let index = InteractionIndex::build(&wires);
|
||
let arc = Arc::new(wires);
|
||
let aabb = [-2.0, -2.0, 2.0, 2.0];
|
||
|
||
let full = index.query_xy(Arc::clone(&arc), aabb);
|
||
let area = index.query_xy_area(Arc::clone(&arc), aabb);
|
||
|
||
fn normalise<T: Copy + Ord>(v: &Option<Vec<T>>) -> Option<Vec<T>> {
|
||
v.as_ref().map(|items| {
|
||
let mut items = items.clone();
|
||
items.sort_unstable();
|
||
items.dedup();
|
||
items
|
||
})
|
||
}
|
||
assert_eq!(area.wire_indices, full.wire_indices);
|
||
assert_eq!(normalise(&area.segments), normalise(&full.segments));
|
||
assert_eq!(
|
||
normalise(&area.fill_triangles),
|
||
normalise(&full.fill_triangles),
|
||
);
|
||
assert_eq!(
|
||
normalise(&area.pick_triangles),
|
||
normalise(&full.pick_triangles),
|
||
);
|
||
assert_eq!(normalise(&area.glyphs), normalise(&full.glyphs));
|
||
assert_eq!(area.query_aabb, full.query_aabb);
|
||
|
||
// The sample carries no hatches or text, so the equality above is
|
||
// thin for those three. What matters is that they stay indexed at all:
|
||
// dropping one to `None` is what would silently unselect a hatch.
|
||
assert!(area.fill_triangles.is_some());
|
||
assert!(area.pick_triangles.is_some());
|
||
assert!(area.glyphs.is_some());
|
||
|
||
assert!(area.snap_points.is_none());
|
||
assert!(area.key_vertices.is_none());
|
||
assert!(area.key_segments.is_none());
|
||
assert!(full.snap_points.is_some(), "the full query still snaps");
|
||
}
|
||
|
||
#[test]
|
||
fn the_area_merge_dedups_wire_indices_and_nothing_else() {
|
||
let arc = Arc::new(sample());
|
||
let base = |indices: Vec<u32>, segments: Vec<SegmentRef>| InteractionCandidates {
|
||
wires: Arc::clone(&arc),
|
||
wire_indices: Some(indices),
|
||
segments: Some(segments),
|
||
snap_points: None,
|
||
key_vertices: None,
|
||
key_segments: None,
|
||
fill_triangles: Some(Vec::new()),
|
||
pick_triangles: Some(Vec::new()),
|
||
glyphs: Some(Vec::new()),
|
||
query_aabb: Some([-2.0, -2.0, 2.0, 2.0]),
|
||
screen_rect: None,
|
||
screen_view: None,
|
||
};
|
||
let seg = |wire: u32| SegmentRef { wire, start: 0 };
|
||
|
||
let mut merged = base(vec![2, 0], vec![seg(2), seg(0)]);
|
||
merged.extend_indexed_area(base(vec![1, 0], vec![seg(1), seg(0)]));
|
||
|
||
assert_eq!(merged.wire_indices, Some(vec![0, 1, 2]));
|
||
assert_eq!(
|
||
merged.segments,
|
||
Some(vec![seg(2), seg(0), seg(1), seg(0)]),
|
||
"the other categories are appended as they come",
|
||
);
|
||
}
|
||
|
||
// World (x,y) → screen ((x+1)*100, (1-y)*100) over a 200×200 viewport, the
|
||
// same identity ortho view the hit-test tests use.
|
||
#[test]
|
||
fn box_selection_picks_the_same_handles_from_either_query() {
|
||
let wires = sample();
|
||
let index = InteractionIndex::build(&wires);
|
||
let arc = Arc::new(wires);
|
||
let aabb = [-2.0, -2.0, 2.0, 2.0];
|
||
let bounds = Rectangle {
|
||
x: 0.0,
|
||
y: 0.0,
|
||
width: 200.0,
|
||
height: 200.0,
|
||
};
|
||
let eye = glam::DVec3::ZERO;
|
||
|
||
for crossing in [false, true] {
|
||
let full = index.query_xy(Arc::clone(&arc), aabb);
|
||
let area = index.query_xy_area(Arc::clone(&arc), aabb);
|
||
let hit = |c: &InteractionCandidates| {
|
||
let mut names = crate::scene::pick::hit_test::box_hit(
|
||
Point::new(0.0, 0.0),
|
||
Point::new(200.0, 200.0),
|
||
crossing,
|
||
c,
|
||
glam::Mat4::IDENTITY,
|
||
eye,
|
||
bounds,
|
||
)
|
||
.into_iter()
|
||
.map(str::to_string)
|
||
.collect::<Vec<_>>();
|
||
names.sort();
|
||
names
|
||
};
|
||
let from_full = hit(&full);
|
||
assert!(!from_full.is_empty(), "the box covers the whole sample");
|
||
assert_eq!(hit(&area), from_full, "crossing={crossing}");
|
||
}
|
||
}
|
||
}
|