fast_mesh_query/lib.rs
1//! Queries on meshes stored in [`fast_mesh_store`], accelerated by [`fast_mesh_bvh`].
2//!
3//! Every query takes a mesh and a BVH built for it, works in the mesh's local
4//! `f32` coordinates, and fails with [`QueryError::StaleBvh`] when the mesh was
5//! edited after the BVH was last updated. Point, ray, region and pair queries
6//! allocate only into the output vector they are given. [`slice()`],
7//! [`fill()`], [`contour_gap`],
8//! [`triangulate`](triangulate::triangulate) and [`intersection_polylines`]
9//! also build per-call temporaries, all reserved fallibly, so that running
10//! out of memory is [`QueryError::OutOfMemory`].
11//!
12//! - Closest point: [`closest_point`] and [`closest_point_many`], with the
13//! [`Feature`] of the triangle the nearest point lies on.
14//! - Raycasts: [`raycast`] (nearest hit), [`raycast_any`] (occlusion),
15//! [`raycast_all`] (every hit, sorted) and [`raycast_many`] (a batch). The
16//! ray-triangle test is watertight: a ray through a shared edge or vertex
17//! never slips between the triangles meeting there. Ties go to the smallest
18//! triangle id.
19//! - Region queries: [`triangles_in`] and [`vertices_in`] find the triangles
20//! that touch, or the vertices inside, a [`Region`] (a box or a sphere). The
21//! box is an [`Aabb`], re-exported from `fast_mesh_bvh` so that callers need
22//! not depend on that crate to build a [`Region::Box`].
23//! - Relative placement: a [`Rigid`] transform (rotation and translation, no
24//! scale) maps one mesh's local coordinates into another's, for queries
25//! between two meshes. Whether two triangles intersect or touch is decided
26//! exactly, by exact orientation predicates on the transformed `f64`
27//! coordinates.
28//! - Mesh-vs-mesh queries, with mesh B placed in mesh A's space by a
29//! [`Rigid`]: [`intersects`], [`mesh_distance`] (a [`MeshDistance`] with
30//! both points in A's space), [`triangles_near`] (the triangles of each
31//! mesh within a margin of the other), [`intersecting_pairs`] and
32//! [`intersection_polylines`] (the intersecting pairs clipped and chained
33//! into [`Polyline`]s). Results are in A's space; passing the same mesh
34//! twice means two coincident copies.
35//! - Self-intersection: [`self_intersecting_pairs`] finds the triangle pairs
36//! of one mesh that intersect and share no vertex id, by the same exact
37//! test.
38//! - Cutting planes: a [`Plane`] (origin and unit normal, in `f64` mesh-local
39//! coordinates) with an orthonormal 2D frame `(u, v)`. A point is on its
40//! `+` side when its signed distance is zero or more.
41//! - Sections: [`slice()`] cuts a mesh with a [`Plane`] into a [`Slice`] of
42//! [`Contour`]s, chained by crossed edge, with flags for open, unoriented
43//! and non-manifold pieces.
44//! - Polygon fill: [`triangulate::triangulate`] splits a 2D polygon with
45//! holes into counter-clockwise triangles by ear clipping.
46//! - Section measurements on `(u, v)` polylines: [`contour_length`],
47//! [`signed_area`], [`fill`] (nesting into outers and holes, then a
48//! [`Fill`] of triangles) and [`contour_gap`] (the exact in-plane
49//! [`Gap2`] between two sets of polylines, found through a grid).
50
51mod closest;
52mod error;
53mod pair;
54mod pairs;
55mod plane;
56mod polylines;
57mod ray;
58mod region;
59mod rigid;
60mod section;
61mod selfpairs;
62mod slice;
63#[cfg(test)]
64mod test_util;
65mod tri;
66pub mod triangulate;
67mod tritri;
68
69use fast_mesh_bvh::Bvh;
70use fast_mesh_store::Mesh;
71
72pub use closest::*;
73pub use error::{QueryError, Result};
74/// The axis-aligned box type of [`Region::Box`], re-exported from `fast_mesh_bvh`.
75pub use fast_mesh_bvh::Aabb;
76pub use pairs::*;
77pub use plane::Plane;
78pub use polylines::{PairSegment, Polyline, intersecting_segments, intersection_polylines};
79pub use ray::*;
80pub use region::*;
81pub use rigid::Rigid;
82pub use section::{Fill, Gap2, contour_gap, contour_length, fill, signed_area};
83pub use selfpairs::self_intersecting_pairs;
84pub use selfpairs::self_intersects;
85pub use slice::{Contour, Slice, slice};
86pub use tri::{Cull, Feature};
87
88/// Checks that `bvh` was built for `mesh` and is up to date with it.
89pub(crate) fn check(mesh: &Mesh, bvh: &Bvh) -> Result<()> {
90 if bvh.mesh_uid() != mesh.uid() {
91 Err(QueryError::MeshMismatch)
92 } else if !bvh.is_current(mesh) {
93 Err(QueryError::StaleBvh)
94 } else {
95 Ok(())
96 }
97}
98
99#[cfg(any(test, feature = "fault-injection"))]
100thread_local! {
101 /// Test-only fault injection: number of fallible reservations that still succeed.
102 static ALLOCATIONS_LEFT: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
103}
104
105/// Makes the `n`-th next fallible reservation on this thread (0-based) and every one
106/// after it fail with `OutOfMemory`. `None` turns failures off. While armed,
107/// parallel work runs on this thread so the hook sees every reservation.
108///
109/// Test-only: compiled for this crate's own tests, and, with the
110/// `fault-injection` feature, as a public helper for dependents' tests.
111#[cfg(any(test, feature = "fault-injection"))]
112pub fn fail_allocations_after(n: Option<usize>) {
113 // Only on arming or disarming: the count must see one `true` per arm.
114 let was = ALLOCATIONS_LEFT.with(|c| c.replace(n)).is_some();
115 if was != n.is_some() {
116 fast_mesh_store::par::set_force_sequential(n.is_some());
117 }
118}
119
120fn allocation_allowed() -> Result<()> {
121 #[cfg(any(test, feature = "fault-injection"))]
122 {
123 if let Some(n) = ALLOCATIONS_LEFT.with(|c| c.get()) {
124 if n == 0 {
125 return Err(QueryError::OutOfMemory);
126 }
127 ALLOCATIONS_LEFT.with(|c| c.set(Some(n - 1)));
128 }
129 }
130 Ok(())
131}
132
133/// Appends to a caller's vector; on failure it restores the vector's original length.
134pub(crate) struct Appender<'a, T> {
135 out: &'a mut Vec<T>,
136 start: usize,
137}
138
139impl<'a, T> Appender<'a, T> {
140 pub(crate) fn new(out: &'a mut Vec<T>) -> Self {
141 let start = out.len();
142 Appender { out, start }
143 }
144
145 /// Pushes with a fallible reservation. On failure everything appended so far is
146 /// dropped and `OutOfMemory` is returned.
147 pub(crate) fn push(&mut self, v: T) -> Result<()> {
148 let reserved = allocation_allowed()
149 .and_then(|()| self.out.try_reserve(1).map_err(|_| QueryError::OutOfMemory));
150 if let Err(e) = reserved {
151 self.out.truncate(self.start);
152 return Err(e);
153 }
154 self.out.push(v);
155 Ok(())
156 }
157
158 /// Appends all of `items` with one fallible reservation, as [`Self::push`]
159 /// does for one.
160 pub(crate) fn extend(&mut self, items: &[T]) -> Result<()>
161 where
162 T: Copy,
163 {
164 let reserved = allocation_allowed().and_then(|()| {
165 self.out
166 .try_reserve(items.len())
167 .map_err(|_| QueryError::OutOfMemory)
168 });
169 if let Err(e) = reserved {
170 self.out.truncate(self.start);
171 return Err(e);
172 }
173 self.out.extend_from_slice(items);
174 Ok(())
175 }
176
177 /// The elements appended through this appender.
178 pub(crate) fn appended(&mut self) -> &mut [T] {
179 &mut self.out[self.start..]
180 }
181
182 /// Drops all but the first `n` appended elements.
183 pub(crate) fn truncate_appended(&mut self, n: usize) {
184 self.out.truncate(self.start + n);
185 }
186
187 /// How many elements were appended.
188 pub(crate) fn count(&self) -> usize {
189 self.out.len() - self.start
190 }
191
192 /// Sorts the appended elements and removes duplicates among them, in
193 /// place; elements from before stay as they are. Returns how many remain.
194 pub(crate) fn sort_dedup_appended(&mut self) -> usize
195 where
196 T: Ord + Copy,
197 {
198 let found = self.appended();
199 found.sort_unstable();
200 // Compact the sorted run in place, keeping the first of each value.
201 let mut kept = 0;
202 for i in 0..found.len() {
203 if kept == 0 || found[i] != found[kept - 1] {
204 found[kept] = found[i];
205 kept += 1;
206 }
207 }
208 self.truncate_appended(kept);
209 kept
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use fast_mesh_bvh::{BuildOptions, Bvh};
217 use fast_mesh_store::Mesh;
218
219 fn mesh() -> Mesh {
220 Mesh::from_buffers(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[0, 1, 2]).unwrap()
221 }
222
223 #[test]
224 fn check_reports_mismatch_then_staleness() {
225 let mut a = mesh();
226 let b = mesh();
227 let mut bvh = Bvh::build(&a, &BuildOptions::default()).unwrap();
228 assert_eq!(check(&b, &bvh), Err(QueryError::MeshMismatch));
229 assert_eq!(check(&a, &bvh), Ok(()));
230 let mut tx = a.begin_edit();
231 tx.set_positions(&[0], &[[0.0, 0.0, 1.0]]).unwrap();
232 let p = tx.commit().unwrap();
233 assert_eq!(check(&a, &bvh), Err(QueryError::StaleBvh));
234 bvh.update(&mut a, p.changes()).unwrap();
235 assert_eq!(check(&a, &bvh), Ok(()));
236 }
237
238 #[test]
239 fn appender_restores_length_on_oom() {
240 let mut out = vec![7u32];
241 {
242 let mut ap = Appender::new(&mut out);
243 fail_allocations_after(Some(1));
244 assert_eq!(ap.push(1), Ok(()));
245 assert_eq!(ap.count(), 1);
246 assert_eq!(ap.appended(), &[1]);
247 assert_eq!(ap.push(2), Err(QueryError::OutOfMemory));
248 fail_allocations_after(None);
249 }
250 assert_eq!(out, vec![7]);
251 }
252}