Skip to main content

fast_mesh_query/
region.rs

1//! Box and sphere region queries.
2
3use fast_mesh_bvh::{Aabb, Bvh, NodeKind};
4use fast_mesh_store::{Mesh, TriangleId, VertexId};
5
6use crate::ray::points;
7use crate::tri::{box_triangle, sphere_triangle};
8use crate::{Appender, QueryError, Result, check};
9
10/// A region of space, in mesh-local coordinates. Both shapes are closed:
11/// points on the boundary are inside.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Region {
14    /// An axis-aligned box. An empty box ([`Aabb::EMPTY`]) contains nothing.
15    Box(Aabb),
16    /// A ball. The center must be finite and the radius finite and not negative.
17    Sphere {
18        /// The center of the ball.
19        center: [f32; 3],
20        /// The radius of the ball.
21        radius: f32,
22    },
23}
24
25/// Absolute pad on a sphere's pruning radius, on top of the `4 * EPSILON`
26/// relative pad in [`Region::bounds`].
27///
28/// When `r * r` underflows to a subnormal or zero (below about 1e-19), the
29/// containment test `d <= r * r` keeps almost no relative precision: with
30/// `r = 1e-23`, `r * r` is zero and a vertex at `2e-23` is contained. The
31/// relative pad and the one-ulp bound widening are both far below that, so
32/// without an absolute pad such a vertex is pruned.
33///
34/// `1e-22` covers every radius whose square underflows. Then `r * r` is at
35/// most a few ulps of `2^-149`, and `d` carries at most a few more from its
36/// own rounding, so an accepted vertex is within about `2 * r` of the
37/// center for the tiniest radii (below about `4e-23`, where `2 * r` is below
38/// the pad) and within `r + 2^-150 / r` above them (where `2^-150 / r` is
39/// below the pad for every `r` above about `4e-24`).
40const SPHERE_PAD: f32 = 1e-22;
41
42impl Region {
43    fn validate(&self) -> Result<()> {
44        if let Region::Sphere { center, radius } = self {
45            if !center.iter().all(|c| c.is_finite()) {
46                return Err(QueryError::InvalidInput("sphere center must be finite"));
47            }
48            if !(radius.is_finite() && *radius >= 0.0) {
49                return Err(QueryError::InvalidInput(
50                    "sphere radius must be finite and not negative",
51                ));
52            }
53        }
54        Ok(())
55    }
56
57    /// A box that contains the whole region.
58    ///
59    /// The `f32` containment test `d <= r * r` can accept points a few ulp
60    /// outside `center +- radius`, so the sphere's box is padded outward: the
61    /// radius grows by a relative `4 * EPSILON` plus [`SPHERE_PAD`], and each
62    /// bound then moves one more ulp out. Pruning is then conservative with
63    /// respect to the containment tests, whatever the tree layout.
64    ///
65    /// The same box is conservative against `sphere_triangle`'s
66    /// `closest_on_triangle(..).dist2 <= r * r` test as well (issue #19). A
67    /// pruned triangle's corners all lie strictly outside the box on some
68    /// axis, so its true closest point is at least the box gap beyond the
69    /// sphere, and acceptance would need downward rounding to bridge it. A
70    /// vertex closest point carries only relative rounding, covered by the
71    /// `4 * EPSILON` like the vertex test. An interpolated (edge or face)
72    /// one rounds by half an ulp at the corners' scale; corners that can
73    /// flip sit just outside the box, at the box's own scale, so that is
74    /// covered by the full ulp each bound moves out. Corners spread far
75    /// along the separating axis instead push the true foot out of the
76    /// segment (a vertex feature, no interpolation) or the true distance
77    /// far beyond the sphere. The sliver path interpolates along segments
78    /// the same way. `sphere_pruning_box_covers_sphere_triangle_at_ulp_scale`
79    /// sweeps the boundary with margin to spare, and fails if the box is
80    /// shrunk (checked by mutation).
81    fn bounds(&self) -> Aabb {
82        match *self {
83            Region::Box(b) => b,
84            Region::Sphere { center, radius } => {
85                let r = radius * (1.0 + 4.0 * f32::EPSILON) + SPHERE_PAD;
86                Aabb::new(
87                    center.map(|c| (c - r).next_down()),
88                    center.map(|c| (c + r).next_up()),
89                )
90            }
91        }
92    }
93
94    fn contains_point(&self, q: [f32; 3]) -> bool {
95        match *self {
96            Region::Box(b) => (0..3).all(|k| b.min[k] <= q[k] && q[k] <= b.max[k]),
97            Region::Sphere { center, radius } => {
98                let d: f32 = (0..3)
99                    .map(|k| (q[k] - center[k]) * (q[k] - center[k]))
100                    .sum();
101                d <= radius * radius
102            }
103        }
104    }
105
106    fn meets_triangle(&self, p: [[f32; 3]; 3]) -> bool {
107        match *self {
108            Region::Box(b) => box_triangle(&b, p),
109            Region::Sphere { center, radius } => sphere_triangle(center, radius, p),
110        }
111    }
112}
113
114/// Calls `leaf` for every triangle of every leaf whose bounds meet the region's
115/// bounding box, until `leaf` reports an error.
116fn candidates(
117    bvh: &Bvh,
118    region: &Region,
119    mut leaf: impl FnMut(TriangleId) -> Result<()>,
120) -> Result<()> {
121    let bounds = region.bounds();
122    let mut failed = None;
123    bvh.traverse(|_, node| {
124        if failed.is_some() || !node.bounds.overlaps(&bounds) {
125            return false;
126        }
127        if let NodeKind::Leaf { triangles } = node.kind {
128            for &id in triangles {
129                if let Err(e) = leaf(id) {
130                    failed = Some(e);
131                    return false;
132                }
133            }
134        }
135        true
136    });
137    failed.map_or(Ok(()), Err)
138}
139
140/// Appends to `out` every alive triangle that touches `region` (shares a point
141/// with it), and returns how many were appended. The order is unspecified.
142/// Elements already in `out` are kept.
143///
144/// The test is exact for the triangle's surface, not its bounding box. Sphere
145/// and box overlap are both closed, so a triangle that only touches the region
146/// counts.
147///
148/// # Errors
149/// [`QueryError::InvalidInput`] for a sphere with a non-finite center or a
150/// radius that is negative or not finite; then [`QueryError::MeshMismatch`] or
151/// [`QueryError::StaleBvh`] when `bvh` does not match the current `mesh`; and
152/// [`QueryError::OutOfMemory`] when `out` cannot grow. On any error `out` keeps
153/// its original length.
154pub fn triangles_in(
155    mesh: &Mesh,
156    bvh: &Bvh,
157    region: &Region,
158    out: &mut Vec<TriangleId>,
159) -> Result<usize> {
160    region.validate()?;
161    check(mesh, bvh)?;
162    let mut ap = Appender::new(out);
163    candidates(bvh, region, |id| {
164        if region.meets_triangle(points(mesh, id)) {
165            ap.push(id)?;
166        }
167        Ok(())
168    })?;
169    Ok(ap.count())
170}
171
172/// Appends to `out`, in ascending order and without duplicates, every vertex
173/// inside `region` that belongs to an alive triangle, and returns how many were
174/// appended. Elements already in `out` are kept and stay in front; only the
175/// appended part is sorted.
176///
177/// A vertex that no triangle uses is not found: the search runs over triangles.
178///
179/// During the traversal the appended range is compacted (sorted and
180/// deduplicated in place) whenever it reaches twice its size at the last
181/// compaction, so it never holds much more than twice the final result, and
182/// needs no extra memory; `out` keeps that capacity afterwards.
183///
184/// # Errors
185/// As for [`triangles_in`].
186pub fn vertices_in(
187    mesh: &Mesh,
188    bvh: &Bvh,
189    region: &Region,
190    out: &mut Vec<VertexId>,
191) -> Result<usize> {
192    region.validate()?;
193    check(mesh, bvh)?;
194    let mut ap = Appender::new(out);
195    let positions = mesh.positions();
196    // Appended elements at which the range is next compacted. Below this
197    // floor compaction costs more than it saves.
198    let mut compact_at = 64;
199    candidates(bvh, region, |id| {
200        for v in mesh.triangles()[id as usize] {
201            if region.contains_point(positions[v as usize]) {
202                ap.push(v)?;
203            }
204        }
205        if ap.count() >= compact_at {
206            compact_at = 2 * ap.sort_dedup_appended().max(1);
207        }
208        Ok(())
209    })?;
210    Ok(ap.sort_dedup_appended())
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::ray::points;
217    use crate::test_util::{Lcg, build, wavy};
218    use crate::tri::{box_triangle, sphere_triangle};
219    use crate::{QueryError, fail_allocations_after};
220    use fast_mesh_bvh::{Aabb, Bvh, Quality};
221    use fast_mesh_store::{Mesh, TriangleId, VertexId};
222
223    fn contains(region: &Region, q: [f32; 3]) -> bool {
224        match region {
225            Region::Box(b) => (0..3).all(|k| b.min[k] <= q[k] && q[k] <= b.max[k]),
226            Region::Sphere { center, radius } => {
227                let d: f32 = (0..3)
228                    .map(|k| (q[k] - center[k]) * (q[k] - center[k]))
229                    .sum();
230                d <= radius * radius
231            }
232        }
233    }
234
235    fn brute_triangles(mesh: &Mesh, region: &Region) -> Vec<TriangleId> {
236        (0..mesh.triangle_slots() as TriangleId)
237            .filter(|&id| mesh.is_triangle_alive(id))
238            .filter(|&id| match region {
239                Region::Box(b) => box_triangle(b, points(mesh, id)),
240                Region::Sphere { center, radius } => {
241                    sphere_triangle(*center, *radius, points(mesh, id))
242                }
243            })
244            .collect()
245    }
246
247    fn brute_vertices(mesh: &Mesh, region: &Region) -> Vec<VertexId> {
248        let mut v: Vec<VertexId> = (0..mesh.triangle_slots() as TriangleId)
249            .filter(|&id| mesh.is_triangle_alive(id))
250            .flat_map(|id| mesh.triangles()[id as usize])
251            .filter(|&v| mesh.is_vertex_alive(v) && contains(region, mesh.positions()[v as usize]))
252            .collect();
253        v.sort_unstable();
254        v.dedup();
255        v
256    }
257
258    /// 200 boxes and 200 spheres over and around a `wavy(16)` grid.
259    fn regions() -> Vec<Region> {
260        let mut rng = Lcg(0x4e61);
261        let mut out = Vec::new();
262        for _ in 0..200 {
263            let c = [
264                rng.next() * 18.0 - 1.0,
265                rng.next() * 18.0 - 1.0,
266                rng.next() * 4.0 - 1.0,
267            ];
268            let h = [
269                0.1 + rng.next() * 3.0,
270                0.1 + rng.next() * 3.0,
271                0.1 + rng.next() * 2.0,
272            ];
273            out.push(Region::Box(Aabb::new(
274                std::array::from_fn(|k| c[k] - h[k]),
275                std::array::from_fn(|k| c[k] + h[k]),
276            )));
277            out.push(Region::Sphere {
278                center: c,
279                radius: 0.1 + rng.next() * 3.0,
280            });
281        }
282        out
283    }
284
285    fn tri_mesh() -> (Mesh, Bvh) {
286        let p = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
287        let mesh = Mesh::from_buffers(&p, &[0, 1, 2]).unwrap();
288        let bvh = build(&mesh, Quality::Sah);
289        (mesh, bvh)
290    }
291
292    #[test]
293    fn triangles_match_brute_force() {
294        let mesh = wavy(16);
295        for quality in [Quality::Sah, Quality::Fast] {
296            let bvh = build(&mesh, quality);
297            let mut nonempty = 0;
298            for region in regions() {
299                let mut out = Vec::new();
300                let n = triangles_in(&mesh, &bvh, &region, &mut out).unwrap();
301                assert_eq!(n, out.len());
302                out.sort_unstable();
303                assert_eq!(out, brute_triangles(&mesh, &region), "{region:?}");
304                nonempty += usize::from(n > 0);
305            }
306            assert!(nonempty > 100, "only {nonempty} regions hit");
307        }
308    }
309
310    #[test]
311    fn bounding_box_overlap_alone_is_not_enough() {
312        let (mesh, bvh) = tri_mesh();
313        // Both regions overlap the triangle's bounding box but miss the
314        // triangle, which lies below the line x + y = 1.
315        let regions = [
316            Region::Box(Aabb::new([0.6, 0.6, -1.0], [1.0, 1.0, 1.0])),
317            Region::Sphere {
318                center: [0.9, 0.9, 0.0],
319                radius: 0.5,
320            },
321        ];
322        for region in regions {
323            let mut out = Vec::new();
324            assert_eq!(triangles_in(&mesh, &bvh, &region, &mut out), Ok(0));
325            assert!(out.is_empty());
326        }
327    }
328
329    #[test]
330    fn vertices_match_brute_force() {
331        let mesh = wavy(16);
332        for quality in [Quality::Sah, Quality::Fast] {
333            let bvh = build(&mesh, quality);
334            let mut nonempty = 0;
335            for region in regions() {
336                let mut out = Vec::new();
337                let n = vertices_in(&mesh, &bvh, &region, &mut out).unwrap();
338                assert_eq!(n, out.len());
339                assert!(out.windows(2).all(|w| w[0] < w[1]), "{region:?}");
340                assert_eq!(out, brute_vertices(&mesh, &region), "{region:?}");
341                nonempty += usize::from(n > 0);
342            }
343            assert!(nonempty > 100, "only {nonempty} regions hit");
344        }
345    }
346
347    #[test]
348    fn sphere_pruning_box_covers_f32_containment_at_ulp_scale() {
349        // Rounding in `q - center` and in the squares lets the containment test
350        // accept a point a few ulp beyond `center + radius`. Search seeded
351        // values for such a point, a few ulp outward along +x.
352        let mut rng = Lcg(0x51de);
353        let mut found = None;
354        'search: for _ in 0..20_000 {
355            let center = [rng.next() * 10.0 - 5.0, 0.0, 0.0];
356            let radius = 0.1 + rng.next() * 3.0;
357            let region = Region::Sphere { center, radius };
358            let plain_hi = center[0] + radius;
359            let mut x = plain_hi;
360            for _ in 0..4 {
361                x = x.next_up();
362                if contains(&region, [x, 0.0, 0.0]) {
363                    found = Some((region, x));
364                    break 'search;
365                }
366            }
367        }
368        let (region, x) = found.expect("no point just outside the unpadded box is contained");
369        let Region::Sphere { center, radius } = region else {
370            unreachable!()
371        };
372        assert!(x > center[0] + radius);
373        assert!(contains(&region, [x, 0.0, 0.0]));
374        assert!(region.bounds().max[0] >= x);
375
376        // A triangle whose bounds start at that vertex, so only the padded box
377        // reaches it.
378        let p = [x, 0.0, 0.0, x + 1.0, 5.0, 0.0, x + 1.0, 0.0, 5.0];
379        let mesh = Mesh::from_buffers(&p, &[0, 1, 2]).unwrap();
380        for quality in [Quality::Sah, Quality::Fast] {
381            let bvh = build(&mesh, quality);
382            let mut out = Vec::new();
383            assert_eq!(vertices_in(&mesh, &bvh, &region, &mut out), Ok(1));
384            assert_eq!(out, vec![0]);
385        }
386    }
387
388    #[test]
389    fn sphere_with_underflowing_square_keeps_contained_vertices() {
390        // `r * r` underflows to zero, so every vertex whose squared distance
391        // also underflows to zero is contained; the pruning box must still
392        // reach them (issue #19).
393        let center = [0.0, 0.0, 0.0];
394        let radius = 1e-23f32;
395        assert_eq!(radius * radius, 0.0);
396        let region = Region::Sphere { center, radius };
397        let q = [2e-23f32, 0.0, 0.0];
398        assert!(contains(&region, q));
399        let p = [q[0], q[1], q[2], q[0], 1e-23, 0.0, q[0], 0.0, 1e-23];
400        let mesh = Mesh::from_buffers(&p, &[0, 1, 2]).unwrap();
401        for quality in [Quality::Sah, Quality::Fast] {
402            let bvh = build(&mesh, quality);
403            let mut out = Vec::new();
404            assert_eq!(vertices_in(&mesh, &bvh, &region, &mut out), Ok(3));
405            assert_eq!(out, vec![0, 1, 2]);
406        }
407    }
408
409    #[test]
410    fn sphere_pruning_box_covers_sphere_triangle_at_ulp_scale() {
411        // Pruning must be conservative against `sphere_triangle` too, not
412        // just the vertex test: a triangle all of whose corners lie strictly
413        // outside the padded box (so a node box around them is disjoint from
414        // it) must still be rejected. Corners sit from just outside the box
415        // (a few ulps, where the interpolation rounding bites) to a radius
416        // beyond it, at center scales where that rounding is far above the
417        // relative pad (issue #19).
418        let mut rng = Lcg(0x7a19);
419        for scale in [1.0f32, 10.0, 100.0, 1000.0] {
420            let center = [scale, -0.5 * scale, 0.25 * scale];
421            for radius in [0.5, 1.0, 3.0] {
422                let region = Region::Sphere { center, radius };
423                let bounds = region.bounds();
424                for ax in 0..3 {
425                    for _ in 0..1500 {
426                        let span = [1.0f32, 3.0, 10.0, 30.0][(rng.next() * 4.0) as usize % 4];
427                        let mut p = [[0.0f32; 3]; 3];
428                        for v in p.iter_mut() {
429                            for k in 0..3 {
430                                v[k] = if k == ax {
431                                    // Just outside the box, or up to a radius
432                                    // beyond it (spread corners exercise the
433                                    // interpolation-parameter rounding).
434                                    let t = rng.next();
435                                    bounds.max[ax] + t * t * radius
436                                } else {
437                                    center[k] + (rng.next() * 2.0 - 1.0) * span * radius
438                                };
439                            }
440                        }
441                        if rng.next() < 0.5 {
442                            // A sliver: the third corner near the first edge.
443                            let s = rng.next();
444                            let (a, b) = (p[0], p[1]);
445                            for (c, (&x, &y)) in p[2].iter_mut().zip(a.iter().zip(b.iter())) {
446                                *c = x + s * (y - x);
447                            }
448                        }
449                        if !p.iter().all(|v| v[ax] > bounds.max[ax]) {
450                            continue;
451                        }
452                        assert!(
453                            !sphere_triangle(center, radius, p),
454                            "pruned but accepted: c={center:?} r={radius} ax={ax} p={p:?}"
455                        );
456                    }
457                }
458            }
459        }
460    }
461
462    #[test]
463    fn vertices_in_peak_stays_near_the_result() {
464        // Issue #20: periodic compaction during the traversal bounds the
465        // temporary peak at about twice the result, instead of once per
466        // incident triangle (about 6 times). The capacity pins the peak
467        // within vector doubling of it.
468        let mesh = wavy(64);
469        let bvh = build(&mesh, Quality::Sah);
470        let region = Region::Box(Aabb::new([-1.0; 3], [70.0; 3]));
471        let mut out = Vec::new();
472        let n = vertices_in(&mesh, &bvh, &region, &mut out).unwrap();
473        assert_eq!(n, 65 * 65);
474        assert_eq!(out.len(), n);
475        assert!(
476            out.capacity() <= 5 * n,
477            "capacity {} for {n} vertices",
478            out.capacity()
479        );
480    }
481
482    #[test]
483    fn vertices_in_sorts_only_the_appended_range() {
484        let mesh = wavy(4);
485        let bvh = build(&mesh, Quality::Sah);
486        let region = Region::Box(Aabb::new([-1.0; 3], [5.0; 3]));
487        let mut out = vec![999];
488        let n = vertices_in(&mesh, &bvh, &region, &mut out).unwrap();
489        assert_eq!(n, 25);
490        assert_eq!(out[0], 999);
491        assert_eq!(out[1..], (0..25).collect::<Vec<_>>()[..]);
492        // The first element is larger than the rest, so a sort of all of
493        // `out` would have moved it.
494    }
495
496    #[test]
497    fn isolated_vertex_is_not_found() {
498        let (mut mesh, mut bvh) = tri_mesh();
499        let mut tx = mesh.begin_edit();
500        let ids = tx.add_vertices(&[[0.5, 0.25, 0.0]]).unwrap();
501        let p = tx.commit().unwrap();
502        bvh.update(&mut mesh, p.changes()).unwrap();
503        let region = Region::Box(Aabb::new([0.4, 0.2, -1.0], [0.6, 0.3, 1.0]));
504        let mut out = Vec::new();
505        assert_eq!(vertices_in(&mesh, &bvh, &region, &mut out), Ok(0));
506        assert!(!out.contains(&ids[0]));
507        // Its triangle neighbors are found, though.
508        let region = Region::Box(Aabb::new([-1.0; 3], [2.0; 3]));
509        assert_eq!(vertices_in(&mesh, &bvh, &region, &mut out), Ok(3));
510        assert_eq!(out, vec![0, 1, 2]);
511    }
512
513    #[test]
514    fn empty_box_returns_nothing() {
515        let mesh = wavy(4);
516        let bvh = build(&mesh, Quality::Sah);
517        let region = Region::Box(Aabb::EMPTY);
518        let mut out = Vec::new();
519        assert_eq!(triangles_in(&mesh, &bvh, &region, &mut out), Ok(0));
520        let mut verts = Vec::new();
521        assert_eq!(vertices_in(&mesh, &bvh, &region, &mut verts), Ok(0));
522        assert!(out.is_empty() && verts.is_empty());
523    }
524
525    #[test]
526    fn empty_mesh_returns_nothing() {
527        let check_empty = |mesh: &Mesh, bvh: &Bvh| {
528            for region in [
529                Region::Box(Aabb::new([-9.0; 3], [9.0; 3])),
530                Region::Sphere {
531                    center: [0.0; 3],
532                    radius: 9.0,
533                },
534            ] {
535                let mut t = Vec::new();
536                assert_eq!(triangles_in(mesh, bvh, &region, &mut t), Ok(0));
537                let mut v = Vec::new();
538                assert_eq!(vertices_in(mesh, bvh, &region, &mut v), Ok(0));
539                assert!(t.is_empty() && v.is_empty());
540            }
541        };
542        let mesh = Mesh::new();
543        let bvh = build(&mesh, Quality::Sah);
544        check_empty(&mesh, &bvh);
545
546        let mut mesh = wavy(4);
547        let mut bvh = build(&mesh, Quality::Sah);
548        let all: Vec<TriangleId> = (0..mesh.triangle_slots() as TriangleId).collect();
549        let mut tx = mesh.begin_edit();
550        tx.remove_triangles(&all).unwrap();
551        let p = tx.commit().unwrap();
552        bvh.update(&mut mesh, p.changes()).unwrap();
553        check_empty(&mesh, &bvh);
554    }
555
556    #[test]
557    fn invalid_input() {
558        let (mesh, bvh) = tri_mesh();
559        let bad = [
560            Region::Sphere {
561                center: [0.0; 3],
562                radius: -1.0,
563            },
564            Region::Sphere {
565                center: [0.0; 3],
566                radius: f32::NAN,
567            },
568            Region::Sphere {
569                center: [f32::NAN, 0.0, 0.0],
570                radius: 1.0,
571            },
572        ];
573        for region in bad {
574            let mut t = vec![5];
575            assert!(matches!(
576                triangles_in(&mesh, &bvh, &region, &mut t),
577                Err(QueryError::InvalidInput(_))
578            ));
579            let mut v = vec![5];
580            assert!(matches!(
581                vertices_in(&mesh, &bvh, &region, &mut v),
582                Err(QueryError::InvalidInput(_))
583            ));
584            assert_eq!((t, v), (vec![5], vec![5]));
585        }
586    }
587
588    #[test]
589    fn stale_and_mismatch_are_rejected() {
590        let (mut mesh, bvh) = tri_mesh();
591        let (other, _) = tri_mesh();
592        let region = Region::Box(Aabb::new([-1.0; 3], [2.0; 3]));
593        let mut out = Vec::new();
594        assert_eq!(
595            triangles_in(&other, &bvh, &region, &mut out),
596            Err(QueryError::MeshMismatch)
597        );
598        let mut tx = mesh.begin_edit();
599        tx.set_positions(&[0], &[[0.0, 0.0, 1.0]]).unwrap();
600        tx.commit().unwrap();
601        assert_eq!(
602            triangles_in(&mesh, &bvh, &region, &mut out),
603            Err(QueryError::StaleBvh)
604        );
605        let mut verts = Vec::new();
606        assert_eq!(
607            vertices_in(&mesh, &bvh, &region, &mut verts),
608            Err(QueryError::StaleBvh)
609        );
610    }
611
612    #[test]
613    fn oom_restores_out() {
614        let (mesh, bvh) = tri_mesh();
615        let region = Region::Box(Aabb::new([-1.0; 3], [2.0; 3]));
616        let mut t = vec![7];
617        fail_allocations_after(Some(0));
618        let r = triangles_in(&mesh, &bvh, &region, &mut t);
619        fail_allocations_after(None);
620        assert_eq!(r, Err(QueryError::OutOfMemory));
621        assert_eq!(t, vec![7]);
622
623        let mut v = vec![7];
624        fail_allocations_after(Some(0));
625        let r = vertices_in(&mesh, &bvh, &region, &mut v);
626        fail_allocations_after(None);
627        assert_eq!(r, Err(QueryError::OutOfMemory));
628        assert_eq!(v, vec![7]);
629    }
630}