Files
OneCAD/crates/ui-egui/src/pick_index.rs
T

103 lines
3.5 KiB
Rust

//! Picking without scanning every triangle and edge: each body mesh's [`MeshIndex`] (its
//! triangles and edge segments in hierarchies), made the first time a pick reaches the body and
//! kept while the mesh is. The viewport's picker asks it for the nearest triangle along the
//! cursor's ray and for the edge segments near that ray, then runs its exact screen tests on
//! those alone.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use egui::{Pos2, vec2};
use solidcraft_engine::geom::{Mesh, MeshIndex, Vec3};
use crate::viewport::Proj;
/// Indexes by mesh address, each with its mesh held (so the address stays its own).
#[derive(Default)]
pub(crate) struct PickIndexes(Mutex<HashMap<usize, (Arc<Mesh>, Arc<MeshIndex>)>>);
impl PickIndexes {
/// The index of `mesh`, made now when there is none yet.
pub(crate) fn of(&self, mesh: &Arc<Mesh>) -> Arc<MeshIndex> {
let key = Arc::as_ptr(mesh) as usize;
let Ok(mut map) = self.0.lock() else { return Arc::new(MeshIndex::new(mesh)) };
if let Some((_, index)) = map.get(&key) {
return index.clone();
}
let index = Arc::new(MeshIndex::new(mesh));
map.insert(key, (mesh.clone(), index.clone()));
index
}
/// Drops the indexes of meshes `keep` no longer names (a new model state).
pub(crate) fn retain(&self, keep: impl Fn(usize) -> bool) {
if let Ok(mut map) = self.0.lock() {
map.retain(|k, _| keep(*k));
}
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.0.lock().map_or(0, |m| m.len())
}
}
/// How far from the ray through `pos` a point may lie and still come within `px` points of it on
/// screen: `a + b·t` at distance `t` along the ray (constant for an orthographic view, widening
/// with depth in perspective), with room to spare.
pub(crate) fn reach(proj: &Proj, pos: Pos2, px: f32) -> (f64, f64) {
let (o, d) = proj.ray(pos);
let (mut a, mut b) = (0.0f64, 0.0f64);
for step in [vec2(px, 0.0), vec2(0.0, px), vec2(-px, 0.0), vec2(0.0, -px)] {
let (o2, d2) = proj.ray(pos + step);
a = a.max(o2.dist(o));
b = b.max(d2.dist(d));
}
// A screen offset of `px` along a diagonal, and rounding: half again.
(a * 1.5, b * 1.5)
}
/// The edge segments of `index` that may come within reach of the ray, as (edge, first point),
/// in edge order and then along each edge: the order a scan of every edge would meet them in.
pub(crate) fn segments_near(index: &MeshIndex, mesh: &Mesh, o: Vec3, d: Vec3, (a, b): (f64, f64)) -> Vec<(usize, usize)> {
let mut v = Vec::new();
if scanning() {
for (ei, e) in mesh.edges.iter().enumerate() {
v.extend((0..e.len().max(2) - 1).map(|si| (ei, si)));
}
return v;
}
index.segments_near(o, d, a, b, |e, s| v.push((e, s)));
v.sort_unstable();
v
}
/// The nearest triangle of `mesh` along the ray, through its index.
pub(crate) fn raycast(index: &MeshIndex, mesh: &Mesh, o: Vec3, d: Vec3) -> Option<(f64, usize)> {
if scanning() {
return mesh.raycast(o, d);
}
index.raycast(mesh, o, d)
}
#[cfg(test)]
thread_local! {
/// Tests: pick by scanning every triangle and edge, as before the indexes, to compare.
pub(crate) static SCAN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn scanning() -> bool {
#[cfg(test)]
{
SCAN.with(|s| s.get())
}
#[cfg(not(test))]
{
false
}
}
#[cfg(test)]
#[path = "pick_index_tests.rs"]
mod tests;