Skip to main content

ogeom_algo/
volume_difference.rs

1//! The volume between a scanned mesh and the solid it was made to: the
2//! material the solid has and the scan lacks, and the material the scan has
3//! beyond the solid, as connected regions.
4//!
5//! # The grid
6//!
7//! Space over both bodies' boxes is cut into cubic cells `cell` wide, and
8//! each cell is read at its centre, its node. A node is in the scan by the
9//! parity of the scan's crossings along the line through it parallel to
10//! `x` ([`parity`](ogeom_mesh::parity)), exactly, and on the scan counts as
11//! in it. A node is in the solid by the same parity against the solid
12//! meshed at a sixteenth of a cell. Where the node lies within that chord,
13//! plus the confusion distance, of the solid's mesh, the band where the
14//! mesh and the solid can disagree, it is read again against the solid's
15//! own surfaces by [`SolidBoundary::holds`], and on the solid counts as in
16//! it. A node in the solid and out of the scan is missing material; one in
17//! the scan and out of the solid is excess material.
18//!
19//! # Holes in the scan
20//!
21//! Vertices of the scan at one position are made one first, so a mesh
22//! stored as loose triangles reads as the surface they make. The parity
23//! needs every edge used an even number of times; a scan with an edge
24//! used an odd number of times, a hole's rim, is closed first, each rim
25//! filled flat with [`Editable::fill`] on its own vertices. A differing
26//! node whose nearest point on the closed scan lies on a fill was not seen
27//! by the scanner: it takes the solid's side, so a hole reports no
28//! material either way.
29//!
30//! Such a node is *doubtful*, and so is every node within a cell of a
31//! fill, where the fill may stand where the scan would have, and a node
32//! the solid's surfaces could not classify, every ray meeting a
33//! degeneracy, which keeps the reading of the solid's mesh.
34//!
35//! # Regions
36//!
37//! Nodes that differ the same way are joined across faces, edges and
38//! corners into regions. A region's volume is its nodes' count times a
39//! cell's volume. A node is counted where its centre lies, so the volume
40//! is off by at most the cells the region's boundary passes through: about
41//! the boundary's area times `√3` cells, and less where the boundary's
42//! errors cancel, as they do on a surface not aligned to the grid.
43//!
44//! A region's depth is how far apart the two surfaces stand across it: the
45//! greatest distance, from a vertex of either surface within a cell of the
46//! region's cells, to the other surface. A vertex of the scan is measured
47//! to the solid's mesh, within its chord of the solid, and a vertex of the
48//! solid's mesh to the closed scan. A boss standing off a face reads its
49//! height, from its top; a pocket its depth, from its floor. A region no
50//! vertex comes near reads the greatest distance from its nodes to either
51//! surface.
52//!
53//! A region shallower than a cell is below the grid's resolution, which
54//! puts at least one node into a slab a cell thick, wherever the grid
55//! falls and however the slab is turned, but not into a thinner one: it is
56//! not reported. A noisy scan
57//! lifts single nodes off its surface all over, and each is that shallow.
58//!
59//! # Layers and lumps
60//!
61//! A region `d` deep and `V` in volume is as wide as a square slab of that
62//! thickness, `w = √(V / d)`. It is a [`Form::Layer`], a face standing
63//! off, where the slab's two faces have more area than its four sides,
64//! `2 w² ≥ 4 w d`, so where `V ≥ 4 d³`; otherwise a [`Form::Lump`].
65//!
66//! # Unsure
67//!
68//! A region is [`Region::unsure`] where a doubtful node lies within a cell
69//! of one of its nodes, or where it is shallower than two cells. Then its
70//! extent rests on nodes nobody measured, or its volume error, about its
71//! area times `√3` cells against its area times its depth, can reach its
72//! whole volume.
73//!
74//! # Cost
75//!
76//! Each row of nodes along `x` is read in parallel, at a parity line
77//! through each body and an exact classification of the nodes in the
78//! solid's band, an eighth of those within a cell of its surface.
79//! Rows are read and regions numbered in the grid's order, so the answer
80//! is the same bit for bit on any thread count.
81
82use ogeom_core::{FastMap, OgeomError, OgeomResult, Tolerances, ogeom_bail, parallel};
83use ogeom_math::{Aabb, Point};
84use ogeom_mesh::{Bvh, Deflection, Editable, Fairing, Fill, Line, triangulate};
85use ogeom_topo::{Model, Shape, Triangulation};
86
87use crate::classify::{SolidBoundary, exact_ring_chord};
88
89/// Which way a region differs.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum Material {
92    /// In the solid and out of the scan: a boss the scan lacks, or a
93    /// pocket it has.
94    Missing,
95    /// In the scan and out of the solid: a boss the solid lacks, or a
96    /// pocket the scan fills.
97    Excess,
98}
99
100/// The shape of a region.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum Form {
103    /// Wider than twice its depth: a face standing off.
104    Layer,
105    /// Less than twice as wide as deep: a boss or a pocket.
106    Lump,
107}
108
109/// A connected volume where the scan and the solid differ the same way.
110#[derive(Debug, Clone, PartialEq)]
111pub struct Region {
112    /// Which way they differ.
113    pub side: Material,
114    /// Its nodes' count times a cell's volume.
115    pub volume: f64,
116    /// How far apart the two surfaces stand across it; see the
117    /// [module documentation](self).
118    pub depth: f64,
119    /// A layer or a lump.
120    pub kind: Form,
121    /// The box of its cells.
122    pub bounds: Aabb,
123    /// Whether it rests on a hole of the scan, or is too shallow for its
124    /// volume to be read; see the [module documentation](self).
125    pub unsure: bool,
126}
127
128/// How many vertices a worker measures at a time.
129const VERTEX_BATCH: usize = 4096;
130
131/// The regions where `mesh`, a scan, and the solid `shape` differ, on a
132/// grid of cells `cell` wide; see the [module documentation](self).
133///
134/// Regions come largest first, equal volumes in the grid's order.
135///
136/// # Errors
137///
138/// [`OgeomError::Construction`] if `cell` is not a positive distance, if
139/// `shape` is not a solid or a closed shell, or if a hole of the scan
140/// cannot be filled, as [`Editable::fill`] refuses.
141/// [`OgeomError::NullObject`] if the scan has no triangles.
142/// [`OgeomError::NotDone`] if the solid's mesh misses its chord.
143/// [`OgeomError::Range`] if the grid would have more than `u32::MAX`
144/// nodes along an axis. As [`Bvh::build`] for a scan with a vertex that is
145/// not finite.
146pub fn volume_difference(
147    mesh: &Triangulation,
148    model: &Model,
149    shape: &Shape,
150    cell: f64,
151    tol: Tolerances,
152) -> OgeomResult<Vec<Region>> {
153    if !(cell.is_finite() && cell > 0.0) {
154        ogeom_bail!(Construction, "a cell of {cell} is not a distance");
155    }
156    if mesh.triangles.is_empty() {
157        ogeom_bail!(NullObject, "the scan has no triangles");
158    }
159    let closed = Closed::of(mesh)?;
160    let scan = Bvh::build(&closed.mesh)?;
161
162    let chord = cell / 16.0;
163    let solid_mesh = triangulate(
164        model,
165        shape,
166        Deflection {
167            chord,
168            ..Deflection::default()
169        },
170        tol,
171    )?;
172    if solid_mesh.is_empty() || !solid_mesh.is_closed() {
173        ogeom_bail!(
174            Construction,
175            "the solid's boundary is not closed, so there is no inside to be in"
176        );
177    }
178    if !solid_mesh.deflection_met {
179        ogeom_bail!(NotDone, "the solid's mesh misses its chord of {chord}");
180    }
181    let solid = Bvh::build(&solid_mesh)?;
182    let boundary = SolidBoundary::of(model, shape, exact_ring_chord(tol), tol)?;
183    let reader = Reader {
184        grid: Grid::over(&scan.bounds().union(&solid.bounds()), cell)?,
185        scan: &scan,
186        first_fill: closed.first_fill,
187        solid: &solid,
188        boundary: &boundary,
189        model,
190        band: tol.confusion() + chord,
191        tol,
192    };
193
194    let mut read = reader.read()?;
195    read.doubtful.extend(closed.near_fills(&reader.grid)?);
196    read.doubtful.sort_unstable();
197    read.doubtful.dedup();
198    let (grid, differ) = (&reader.grid, &read.differ);
199    let mut regions = Regions::join(grid, differ);
200    regions.near_doubt(grid, differ, &read.doubtful);
201    let mut depth = vec![f64::NEG_INFINITY; regions.count];
202    regions.deepen(grid, differ, &mesh.positions, &solid, &mut depth);
203    regions.deepen(grid, differ, &solid_mesh.positions, &scan, &mut depth);
204    regions.deepen_by_nodes(&reader, differ, &mut depth);
205    Ok(regions.finish(grid, differ, &depth))
206}
207
208/// The scan, closed, and where the fills start among its triangles.
209struct Closed {
210    mesh: Triangulation,
211    /// The first triangle of the fills; every triangle where the scan was
212    /// closed.
213    first_fill: usize,
214}
215
216/// The mesh with its vertices at one position made one, and whether every
217/// edge joining two of them is then used an even number of times, as the
218/// parity asks.
219fn welded(mesh: &Triangulation) -> (Triangulation, bool) {
220    let key = |p: Point| [p.x, p.y, p.z].map(|c| (c + 0.0).to_bits());
221    let mut first: FastMap<[u64; 3], u32> = FastMap::default();
222    let mut out = Triangulation::new();
223    let mut to = Vec::with_capacity(mesh.positions.len());
224    for &p in &mesh.positions {
225        let next = index(out.positions.len());
226        let at = *first.entry(key(p)).or_insert(next);
227        if at == next {
228            out.positions.push(p);
229        }
230        to.push(at);
231    }
232    let mut odd: FastMap<(u32, u32), bool> = FastMap::default();
233    for t in &mesh.triangles {
234        let t = t.map(|v| to.get(v as usize).copied().unwrap_or(v));
235        for k in 0..3 {
236            let (a, b) = (t[k], t[(k + 1) % 3]);
237            if a != b {
238                let flag = odd.entry((a.min(b), a.max(b))).or_insert(false);
239                *flag = !*flag;
240            }
241        }
242        out.triangles.push(t);
243    }
244    out.deflection_met = mesh.deflection_met;
245    let even = odd.values().all(|&o| !o);
246    (out, even)
247}
248
249impl Closed {
250    fn of(mesh: &Triangulation) -> OgeomResult<Self> {
251        let (mesh, even) = welded(mesh);
252        if even {
253            return Ok(Self {
254                first_fill: mesh.triangles.len(),
255                mesh,
256            });
257        }
258        let mut editable = Editable::from(&mesh, 0.0)?;
259        let fill = Fill {
260            fairing: Fairing::Flat,
261            refine: false,
262        };
263        let mut made = 0;
264        // A fill that joins the vertices where two rims touch can rename
265        // the other rim; it is filled on the next pass, under its new name.
266        loop {
267            let rims = editable.rims();
268            if rims.is_empty() {
269                break;
270            }
271            let mut filled = false;
272            for rim in rims {
273                match editable.fill(rim.id(), &fill) {
274                    Ok(report) => {
275                        made += report.faces.len();
276                        filled = true;
277                    }
278                    Err(OgeomError::Dangling(_)) => {}
279                    Err(e) => ogeom_bail!(
280                        Construction,
281                        "a hole of the scan, rim {}, cannot be filled: {e}",
282                        rim.id().0
283                    ),
284                }
285            }
286            if !filled {
287                ogeom_bail!(Construction, "the scan's holes cannot be filled");
288            }
289        }
290        let (closed, even) = welded(&editable.to_triangulation());
291        if !even {
292            ogeom_bail!(
293                Construction,
294                "the scan is not closed once its holes are filled: an edge is used \
295                 an odd number of times"
296            );
297        }
298        Ok(Self {
299            first_fill: closed.triangles.len() - made,
300            mesh: closed,
301        })
302    }
303
304    /// The nodes within a cell of a fill, in the grid's order.
305    fn near_fills(&self, grid: &Grid) -> OgeomResult<Vec<u64>> {
306        let mut fills = Triangulation::new();
307        fills.positions.clone_from(&self.mesh.positions);
308        fills.triangles = self.mesh.triangles[self.first_fill..].to_vec();
309        if fills.triangles.is_empty() {
310            return Ok(Vec::new());
311        }
312        let bvh = Bvh::build(&fills)?;
313        let h = grid.cell;
314        let found = parallel::map_ordered(&fills.triangles, |_, t| {
315            let corners = t.map(|v| fills.positions[v as usize]);
316            let reach = Aabb::of_points(&corners).expanded(h);
317            let (Some(low), Some(high)) = (reach.low(), reach.high()) else {
318                return Vec::new();
319            };
320            let (from, to) = (grid.cell_of(low), grid.cell_of(high));
321            let mut out = Vec::new();
322            for k in from[2]..=to[2] {
323                for j in from[1]..=to[1] {
324                    for i in from[0]..=to[0] {
325                        let Some(n) = grid.on([i, j, k]) else {
326                            continue;
327                        };
328                        if bvh.nearest(grid.at(n), h).is_some() {
329                            out.push(grid.key(n));
330                        }
331                    }
332                }
333            }
334            out
335        });
336        let mut near: Vec<u64> = found.into_iter().flatten().collect();
337        near.sort_unstable();
338        near.dedup();
339        Ok(near)
340    }
341}
342
343/// The nodes: cell centres, `origin + (i + ½) cell` along each axis.
344struct Grid {
345    origin: Point,
346    cell: f64,
347    count: [u32; 3],
348}
349
350impl Grid {
351    fn over(bounds: &Aabb, cell: f64) -> OgeomResult<Self> {
352        let (Some(low), Some(high)) = (bounds.low(), bounds.high()) else {
353            ogeom_bail!(NullObject, "neither body has a triangle");
354        };
355        let size = high - low;
356        let along = |extent: f64| -> OgeomResult<u32> {
357            let count = (extent / cell).ceil().max(1.0);
358            if !(count.is_finite() && count <= f64::from(u32::MAX)) {
359                ogeom_bail!(
360                    Range,
361                    "a grid of {count} nodes along an axis, a cell of {cell} across {extent}"
362                );
363            }
364            #[allow(
365                clippy::cast_possible_truncation,
366                clippy::cast_sign_loss,
367                reason = "a whole number in 1..=u32::MAX, checked above"
368            )]
369            Ok(count as u32)
370        };
371        Ok(Self {
372            origin: low,
373            cell,
374            count: [along(size.x)?, along(size.y)?, along(size.z)?],
375        })
376    }
377
378    fn coordinate(&self, axis: usize, i: u32) -> f64 {
379        let o = [self.origin.x, self.origin.y, self.origin.z][axis];
380        (f64::from(i) + 0.5).mul_add(self.cell, o)
381    }
382
383    fn at(&self, [i, j, k]: [u32; 3]) -> Point {
384        Point::new(
385            self.coordinate(0, i),
386            self.coordinate(1, j),
387            self.coordinate(2, k),
388        )
389    }
390
391    fn key(&self, [i, j, k]: [u32; 3]) -> u64 {
392        (u64::from(k) * u64::from(self.count[1]) + u64::from(j)) * u64::from(self.count[0])
393            + u64::from(i)
394    }
395
396    fn node(&self, key: u64) -> [u32; 3] {
397        let nx = u64::from(self.count[0]);
398        let ny = u64::from(self.count[1]);
399        #[allow(
400            clippy::cast_possible_truncation,
401            reason = "each index is below its axis's u32 count"
402        )]
403        [
404            (key % nx) as u32,
405            ((key / nx) % ny) as u32,
406            (key / nx / ny) as u32,
407        ]
408    }
409
410    /// The node whose cell holds `p` along each axis, as a signed index
411    /// that may lie off the grid.
412    fn cell_of(&self, p: Point) -> [i64; 3] {
413        let o = [self.origin.x, self.origin.y, self.origin.z];
414        let q = [p.x, p.y, p.z];
415        core::array::from_fn(|a| {
416            let at = ((q[a] - o[a]) / self.cell).floor();
417            #[allow(
418                clippy::cast_possible_truncation,
419                reason = "clamped into i64 range first; a node off the grid is dropped"
420            )]
421            let at = at.clamp(-2.0, f64::from(u32::MAX) + 1.0) as i64;
422            at
423        })
424    }
425
426    /// The node at signed indices `n`, where it lies on the grid.
427    fn on(&self, n: [i64; 3]) -> Option<[u32; 3]> {
428        let on = (0..3).all(|a| n[a] >= 0 && n[a] < i64::from(self.count[a]));
429        #[allow(
430            clippy::cast_possible_truncation,
431            clippy::cast_sign_loss,
432            reason = "checked to lie in 0..count above"
433        )]
434        on.then(|| n.map(|v| v as u32))
435    }
436
437    /// The nodes within one step of `at` along every axis, `at` among them,
438    /// that lie on the grid.
439    fn around(&self, at: [i64; 3]) -> impl Iterator<Item = [u32; 3]> + '_ {
440        (-1..=1_i64).flat_map(move |dk| {
441            (-1..=1_i64).flat_map(move |dj| {
442                (-1..=1_i64).filter_map(move |di| self.on([at[0] + di, at[1] + dj, at[2] + dk]))
443            })
444        })
445    }
446}
447
448/// What reading the grid needs.
449struct Reader<'a> {
450    grid: Grid,
451    scan: &'a Bvh,
452    first_fill: usize,
453    solid: &'a Bvh,
454    boundary: &'a SolidBoundary,
455    model: &'a Model,
456    /// How near the solid's mesh a node is read against its surfaces.
457    band: f64,
458    tol: Tolerances,
459}
460
461/// The grid as read: the nodes that differ, in the grid's order, and the
462/// doubtful ones, in no order.
463struct Read {
464    differ: Vec<(u64, Material)>,
465    doubtful: Vec<u64>,
466}
467
468/// One row's reading.
469#[derive(Default)]
470struct Row {
471    differ: Vec<(u32, Material)>,
472    doubtful: Vec<u32>,
473}
474
475impl Reader<'_> {
476    fn read(&self) -> OgeomResult<Read> {
477        let [_, ny, nz] = self.grid.count;
478        let rows: Vec<u64> = (0..u64::from(ny) * u64::from(nz)).collect();
479        let read = parallel::map_ordered(&rows, |_, &r| {
480            #[allow(
481                clippy::cast_possible_truncation,
482                reason = "each index is below its axis's u32 count"
483            )]
484            let (j, k) = ((r % u64::from(ny)) as u32, (r / u64::from(ny)) as u32);
485            self.row(j, k)
486        });
487        let nx = u64::from(self.grid.count[0]);
488        let mut differ = Vec::new();
489        let mut doubtful = Vec::new();
490        for (r, row) in rows.iter().zip(read) {
491            let row = row?;
492            differ.extend(row.differ.iter().map(|&(i, m)| (r * nx + u64::from(i), m)));
493            doubtful.extend(row.doubtful.iter().map(|&i| r * nx + u64::from(i)));
494        }
495        self.unseen(&mut differ, &mut doubtful);
496        Ok(Read { differ, doubtful })
497    }
498
499    fn row(&self, j: u32, k: u32) -> OgeomResult<Row> {
500        let (y, z) = (self.grid.coordinate(1, j), self.grid.coordinate(2, k));
501        let nx = self.grid.count[0];
502        let scan = Line::through(self.scan, y, z);
503        let solid = Line::through(self.solid, y, z);
504        let near = self.near_band(y, z);
505        let mut row = Row::default();
506        for i in 0..nx {
507            let x = self.grid.coordinate(0, i);
508            let in_scan = scan.lies(x).is_inside_or_on();
509            let mut in_solid = solid.lies(x).is_inside_or_on();
510            if near[i as usize] {
511                let p = Point::new(x, y, z);
512                if self.solid.nearest(p, self.band).is_some() {
513                    match self.boundary.holds(self.model, p, self.tol) {
514                        Ok(c) => in_solid = c.is_inside_or_on(),
515                        Err(OgeomError::NotDone(_)) => row.doubtful.push(i),
516                        Err(e) => return Err(e),
517                    }
518                }
519            }
520            match (in_solid, in_scan) {
521                (true, false) => row.differ.push((i, Material::Missing)),
522                (false, true) => row.differ.push((i, Material::Excess)),
523                _ => {}
524            }
525        }
526        Ok(row)
527    }
528
529    /// Which nodes of the row through `(y, z)` lie within the band of a
530    /// triangle of the solid's mesh along `x`, a box about it: the only
531    /// ones that can be within the band of the mesh.
532    fn near_band(&self, y: f64, z: f64) -> Vec<bool> {
533        let band = self.band;
534        let nx = self.grid.count[0];
535        let mut near = vec![false; nx as usize];
536        for t in self.solid.triangles_where(|b| match (b.low(), b.high()) {
537            (Some(l), Some(h)) => {
538                l.y - band <= y && y <= h.y + band && l.z - band <= z && z <= h.z + band
539            }
540            _ => false,
541        }) {
542            let Some(c) = self.solid.triangle(t) else {
543                continue;
544            };
545            let low = c[0].x.min(c[1].x).min(c[2].x) - band;
546            let high = c[0].x.max(c[1].x).max(c[2].x) + band;
547            let first = ((low - self.grid.origin.x) / self.grid.cell - 0.5).ceil();
548            let last = ((high - self.grid.origin.x) / self.grid.cell - 0.5).floor();
549            let first = first.max(0.0);
550            let last = last.min(f64::from(nx) - 1.0);
551            if first > last {
552                continue;
553            }
554            #[allow(
555                clippy::cast_possible_truncation,
556                clippy::cast_sign_loss,
557                reason = "clamped into 0..nx above"
558            )]
559            for slot in &mut near[first as usize..=last as usize] {
560                *slot = true;
561            }
562        }
563        near
564    }
565
566    /// Give the solid's side to each differing node whose nearest point on
567    /// the closed scan lies on a fill, and doubt it.
568    fn unseen(&self, differ: &mut Vec<(u64, Material)>, doubtful: &mut Vec<u64>) {
569        if self.first_fill == self.scan.triangle_count() || differ.is_empty() {
570            return;
571        }
572        let points: Vec<Point> = differ
573            .iter()
574            .map(|&(key, _)| self.grid.at(self.grid.node(key)))
575            .collect();
576        let hits = self.scan.nearest_many(&points, f64::INFINITY);
577        let mut kept = Vec::with_capacity(differ.len());
578        for (&(key, m), hit) in differ.iter().zip(hits) {
579            if hit.is_some_and(|h| h.triangle as usize >= self.first_fill) {
580                doubtful.push(key);
581            } else {
582                kept.push((key, m));
583            }
584        }
585        *differ = kept;
586    }
587}
588
589/// The differing nodes joined into regions.
590struct Regions {
591    /// Each differing node's region, in the order of the nodes.
592    of: Vec<u32>,
593    count: usize,
594    /// Whether a doubtful node lies within a cell of each region.
595    doubted: Vec<bool>,
596}
597
598/// The root of `x`'s set, halving the path to it.
599fn find(parent: &mut [u32], mut x: u32) -> u32 {
600    while parent[x as usize] != x {
601        parent[x as usize] = parent[parent[x as usize] as usize];
602        x = parent[x as usize];
603    }
604    x
605}
606
607fn index(i: usize) -> u32 {
608    u32::try_from(i).unwrap_or(u32::MAX)
609}
610
611/// Where `key` stands among the sorted differing nodes.
612fn place(differ: &[(u64, Material)], key: u64) -> Option<usize> {
613    differ.binary_search_by_key(&key, |&(k, _)| k).ok()
614}
615
616impl Regions {
617    fn join(grid: &Grid, differ: &[(u64, Material)]) -> Self {
618        let mut parent: Vec<u32> = (0..differ.len()).map(index).collect();
619        for (at, &(key, side)) in differ.iter().enumerate() {
620            let node = grid.node(key);
621            let centre = node.map(i64::from);
622            for n in grid.around(centre) {
623                let other = grid.key(n);
624                if other <= key {
625                    continue;
626                }
627                if let Some(o) = place(differ, other)
628                    && differ[o].1 == side
629                {
630                    let (a, b) = (find(&mut parent, index(at)), find(&mut parent, index(o)));
631                    if a != b {
632                        parent[a.max(b) as usize] = a.min(b);
633                    }
634                }
635            }
636        }
637        // Regions numbered by their first node, in the grid's order.
638        let mut number = vec![u32::MAX; differ.len()];
639        let mut of = Vec::with_capacity(differ.len());
640        let mut count = 0;
641        for at in 0..differ.len() {
642            let root = find(&mut parent, index(at)) as usize;
643            if number[root] == u32::MAX {
644                number[root] = index(count);
645                count += 1;
646            }
647            of.push(number[root]);
648        }
649        Self {
650            of,
651            count,
652            doubted: vec![false; count],
653        }
654    }
655
656    /// The regions of the differing nodes within one step of node `at`.
657    fn near(&self, grid: &Grid, differ: &[(u64, Material)], at: [i64; 3], out: &mut Vec<u32>) {
658        out.clear();
659        for n in grid.around(at) {
660            if let Some(o) = place(differ, grid.key(n)) {
661                let r = self.of[o];
662                if !out.contains(&r) {
663                    out.push(r);
664                }
665            }
666        }
667    }
668
669    /// Mark the regions within a cell of a doubtful node.
670    fn near_doubt(&mut self, grid: &Grid, differ: &[(u64, Material)], doubtful: &[u64]) {
671        let mut out = Vec::new();
672        for &key in doubtful {
673            self.near(grid, differ, grid.node(key).map(i64::from), &mut out);
674            for &r in &out {
675                self.doubted[r as usize] = true;
676            }
677        }
678    }
679
680    /// Raise each region's depth to the distance, to `other`, of each of
681    /// `vertices` within a cell of its cells.
682    fn deepen(
683        &self,
684        grid: &Grid,
685        differ: &[(u64, Material)],
686        vertices: &[Point],
687        other: &Bvh,
688        depth: &mut [f64],
689    ) {
690        if differ.is_empty() {
691            return;
692        }
693        let starts: Vec<usize> = (0..vertices.len()).step_by(VERTEX_BATCH).collect();
694        let found = parallel::map_ordered(&starts, |_, &start| {
695            let mut out = Vec::new();
696            let mut near = Vec::new();
697            for &p in &vertices[start..(start + VERTEX_BATCH).min(vertices.len())] {
698                self.near(grid, differ, grid.cell_of(p), &mut near);
699                if near.is_empty() {
700                    continue;
701                }
702                if let Some(hit) = other.nearest(p, f64::INFINITY) {
703                    out.extend(near.iter().map(|&r| (r, hit.distance)));
704                }
705            }
706            out
707        });
708        for (r, d) in found.into_iter().flatten() {
709            depth[r as usize] = depth[r as usize].max(d);
710        }
711    }
712
713    /// Give each region no vertex came near the greatest distance from its
714    /// nodes to either body.
715    fn deepen_by_nodes(&self, reader: &Reader<'_>, differ: &[(u64, Material)], depth: &mut [f64]) {
716        let (asked, points): (Vec<u32>, Vec<Point>) = differ
717            .iter()
718            .zip(&self.of)
719            .filter(|&(_, &r)| depth[r as usize] == f64::NEG_INFINITY)
720            .map(|(&(key, _), &r)| (r, reader.grid.at(reader.grid.node(key))))
721            .unzip();
722        if points.is_empty() {
723            return;
724        }
725        let to_scan = reader.scan.nearest_many(&points, f64::INFINITY);
726        let to_solid = reader.solid.nearest_many(&points, f64::INFINITY);
727        for ((r, a), b) in asked.into_iter().zip(to_scan).zip(to_solid) {
728            let d = a
729                .map_or(0.0, |h| h.distance)
730                .max(b.map_or(0.0, |h| h.distance));
731            depth[r as usize] = depth[r as usize].max(d);
732        }
733    }
734
735    /// The regions as reported: deep enough to resolve, largest first.
736    fn finish(&self, grid: &Grid, differ: &[(u64, Material)], depth: &[f64]) -> Vec<Region> {
737        let mut low = vec![[u32::MAX; 3]; self.count];
738        let mut high = vec![[0_u32; 3]; self.count];
739        let mut cells = vec![0_u64; self.count];
740        let mut side = vec![Material::Missing; self.count];
741        for (&(key, m), &r) in differ.iter().zip(&self.of) {
742            let r = r as usize;
743            let n = grid.node(key);
744            for a in 0..3 {
745                low[r][a] = low[r][a].min(n[a]);
746                high[r][a] = high[r][a].max(n[a]);
747            }
748            cells[r] += 1;
749            side[r] = m;
750        }
751        let h = grid.cell;
752        let mut regions: Vec<(usize, Region)> = (0..self.count)
753            .filter(|&r| depth[r] >= h)
754            .map(|r| {
755                #[allow(
756                    clippy::cast_precision_loss,
757                    reason = "a count of nodes, exact below 2^53"
758                )]
759                let volume = cells[r] as f64 * h * h * h;
760                let d = depth[r];
761                let half = Point::new(h / 2.0, h / 2.0, h / 2.0);
762                let (a, b) = (grid.at(low[r]), grid.at(high[r]));
763                let region = Region {
764                    side: side[r],
765                    volume,
766                    depth: d,
767                    kind: if volume >= 4.0 * d * d * d {
768                        Form::Layer
769                    } else {
770                        Form::Lump
771                    },
772                    bounds: Aabb::of_corners(
773                        Point::new(a.x - half.x, a.y - half.y, a.z - half.z),
774                        Point::new(b.x + half.x, b.y + half.y, b.z + half.z),
775                    ),
776                    unsure: self.doubted[r] || d < 2.0 * h,
777                };
778                (r, region)
779            })
780            .collect();
781        regions.sort_by(|(ra, a), (rb, b)| b.volume.total_cmp(&a.volume).then(ra.cmp(rb)));
782        regions.into_iter().map(|(_, region)| region).collect()
783    }
784}