Skip to main content

ogeom_mesh/
hollow.rs

1//! Hollowing a closed mesh: a sealed cavity inside it, its wall a set
2//! distance from the part's surface.
3//!
4//! # The distance field
5//!
6//! The part's distance field is sampled on a grid of cubic cells, a third
7//! of the wall wide unless the caller says otherwise, over the part's box.
8//! Each node is inside, outside or on the part by the crossings of the line
9//! along `x` through it: the triangles that line crosses are found exactly,
10//! a line through an edge or a vertex resolved by simulation of
11//! simplicity, as [`boolean`](crate::boolean()) resolves its rays, and a
12//! node is inside where an odd number of them lie ahead of it, each
13//! decided by the exact side of the node on the triangle's plane. A node
14//! inside takes its exact distance to the nearest triangle, through the
15//! part's [`Bvh`], negated.
16//!
17//! Only distances near the wall decide anything. The field is 1-Lipschitz,
18//! so a node deeper than the wall by more than the longest edge of the
19//! tetrahedra below, a cell's diagonal `√3 h`, has every neighbour deeper
20//! than the wall: a node with no triangle within `wall + 2h` takes that
21//! distance and loses nothing. For the same reason a node outside needs no
22//! distance unless `√3 h` reaches across the wall.
23//!
24//! # The wall
25//!
26//! Each cell is split into six tetrahedra about its diagonal along `x + y +
27//! z`, the same in every cell, so neighbours share their faces' splits. The
28//! cavity is where the field is below `-wall`; a node exactly at it counts
29//! outside the cavity, so no node lies on the surface. In each tetrahedron
30//! the surface crosses the edges joining a node in the cavity to one out of
31//! it, at the point the field interpolates linearly to `-wall`, held at
32//! least [`CLAMP`] of the edge from either end. Points anywhere inside the
33//! edges give triangles that meet only along their shared edges, so the
34//! cavity's surface is closed, 2-manifold and crosses itself nowhere; each
35//! triangle is wound by the exact side of a node in the cavity, so that it
36//! faces into the cavity, out of the part's material.
37//!
38//! The surface is then decimated with [`Target::Verified`] within a
39//! sixteenth of a cell, every point of each side measured.
40//!
41//! # Checked, not assumed
42//!
43//! The cavity is added to the part as a second shell, with no boolean, so
44//! the result is checked exactly as [`boolean`](crate::boolean()) checks
45//! an operand: closed, 2-manifold, no degenerate triangle, no triangle of
46//! the cavity meeting another triangle but at shared corners, and every
47//! shell wound outward of the material, which puts each cavity strictly
48//! inside the part. A decimated cavity that fails, or that misses its
49//! tolerance, is replaced by the one marched; one that fails too is an
50//! error.
51//!
52//! # Error
53//!
54//! A point of the cavity's wall, before decimation, is where the linear
55//! interpolation of the field is `-wall`; the field differs from its
56//! interpolation within a tetrahedron by at most the tetrahedron's
57//! diameter, `√3 h`, and where the field is smooth with curvature `κ`, by
58//! about `3 h² κ / 2`. Holding the point off the ends moves it by at most
59//! `√3 h` [`CLAMP`], and the decimation by `h / 16`.
60
61use ogeom_core::parallel;
62use ogeom_core::{Exact, FastMap, OgeomResult, P3, Predicates, Sign, Tolerances, ogeom_bail};
63use ogeom_math::Point;
64use ogeom_topo::Triangulation;
65
66use crate::Bvh;
67use crate::boolean::check_solid_kept;
68use crate::parity::{Lies, Line};
69use crate::simplify::{Deviation, Target, simplify};
70
71/// The share of a grid edge's length a crossing is held from either end.
72///
73/// It moves a crossing by at most `√3 h / 1024`, under a five-hundredth of
74/// the decimation's tolerance, and keeps the corners of every triangle at
75/// least `h / 1024` apart along their edges, far above the rounding of any
76/// coordinate a grid of under `2^40` cells along an axis holds, so no
77/// triangle's corners round onto one line.
78pub const CLAMP: f64 = 1.0 / 1024.0;
79
80/// The grid a hollow's distance field is sampled on.
81#[derive(Debug, Clone, Copy, PartialEq, Default)]
82pub struct Grid {
83    /// The spacing of the nodes; `None` for a third of the wall.
84    pub cell: Option<f64>,
85}
86
87/// A hollowed mesh, and what made it.
88#[derive(Debug, Clone, PartialEq)]
89pub struct Hollowed {
90    /// The part and its cavities: the input's vertices and triangles first,
91    /// as they were, then each cavity's, wound into the cavity. It carries
92    /// no normals and no parameters.
93    pub mesh: Triangulation,
94    /// The first triangle of the cavities in [`Hollowed::mesh`].
95    pub first_cavity_triangle: usize,
96    /// How many closed shells the cavities add: one for a cavity like a
97    /// ball's, two for one about a hole of the part's own. Zero where no
98    /// point of the part is deeper than the wall.
99    pub shells: usize,
100    /// The grid's spacing.
101    pub cell: f64,
102    /// The decimation's measurement against the marched cavity, or `None`
103    /// where the marched cavity was kept as it was.
104    pub deviation: Option<Deviation>,
105}
106
107/// Hollow a closed mesh, leaving a wall `wall` thick.
108///
109/// The part must bound a solid as [`boolean`](crate::boolean()) asks of an
110/// operand: closed, 2-manifold, no degenerate triangle, no triangle meeting
111/// another but at shared corners, wound outward; it may have cavities of
112/// its own, and the wall keeps clear of them. The cavity is sampled on
113/// `grid` and decimated within a sixteenth of a cell, with `tol`'s
114/// confusion as the decimation's; see the [module documentation](self).
115///
116/// The field costs a nearest-triangle query for each node inside the part,
117/// about `V / h³` of them, and the checks one exact pass over the part;
118/// the decimation, which checks every collapse against the marched cavity
119/// and the triangles near it, costs most on a curved cavity.
120///
121/// # Errors
122///
123/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
124/// `wall` or the cell is not a positive distance, and as
125/// [`boolean`](crate::boolean()) for an operand if the part does not bound
126/// a solid, named "part". [`OgeomError::Range`](ogeom_core::OgeomError::Range)
127/// if the grid would have more than `u32::MAX` nodes along an axis or the
128/// cavity more than `u32::MAX` vertices.
129/// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if the
130/// marched cavity fails the check of the result, named "hollowed".
131pub fn hollow(
132    mesh: &Triangulation,
133    wall: f64,
134    grid: &Grid,
135    tol: Tolerances,
136) -> OgeomResult<Hollowed> {
137    if !(wall.is_finite() && wall > 0.0) {
138        ogeom_bail!(Construction, "a wall of {wall} is not a distance");
139    }
140    let cell = grid.cell.unwrap_or(wall / 3.0);
141    if !(cell.is_finite() && cell > 0.0) {
142        ogeom_bail!(Construction, "a cell of {cell} is not a distance");
143    }
144    let part = check_solid_kept(mesh, "part")?;
145    let bvh = part.bvh();
146    let lattice = Lattice::over(bvh, cell, wall)?;
147    let marched = lattice.march(bvh)?;
148    let first = mesh.triangles.len();
149    if marched.triangles.is_empty() {
150        return Ok(Hollowed {
151            mesh: join(mesh, &marched)?,
152            first_cavity_triangle: first,
153            shells: 0,
154            cell,
155            deviation: None,
156        });
157    }
158
159    let decimated = simplify(
160        &marched,
161        Target::Verified {
162            tolerance: cell / 16.0,
163            share: 1.0,
164            max_triangles: None,
165        },
166        tol,
167    )?;
168    if decimated.target_met {
169        let joined = join(mesh, &decimated.mesh)?;
170        if part.check_cavity(&decimated.mesh, "hollowed").is_ok() {
171            return Ok(Hollowed {
172                shells: shells(&decimated.mesh),
173                mesh: joined,
174                first_cavity_triangle: first,
175                cell,
176                deviation: decimated.deviation,
177            });
178        }
179    }
180    let joined = join(mesh, &marched)?;
181    if let Err(e) = part.check_cavity(&marched, "hollowed") {
182        ogeom_bail!(Invariant, "the marched cavity does not seal the part: {e}");
183    }
184    Ok(Hollowed {
185        shells: shells(&marched),
186        mesh: joined,
187        first_cavity_triangle: first,
188        cell,
189        deviation: None,
190    })
191}
192
193/// The part's vertices and triangles, then the cavity's.
194fn join(part: &Triangulation, cavity: &Triangulation) -> OgeomResult<Triangulation> {
195    let Ok(base) = u32::try_from(part.positions.len()) else {
196        ogeom_bail!(Range, "a part of more than four billion vertices");
197    };
198    if u32::try_from(part.positions.len() + cavity.positions.len()).is_err() {
199        ogeom_bail!(Range, "a hollowed part of more than four billion vertices");
200    }
201    let mut out = Triangulation::new();
202    out.positions = part.positions.clone();
203    out.positions.extend_from_slice(&cavity.positions);
204    out.triangles = part.triangles.clone();
205    out.triangles
206        .extend(cavity.triangles.iter().map(|t| t.map(|v| v + base)));
207    out.deflection_met = part.deflection_met;
208    Ok(out)
209}
210
211/// The root of `x`'s set, halving the path to it.
212fn find(parent: &mut [u32], mut x: u32) -> u32 {
213    while parent[x as usize] != x {
214        parent[x as usize] = parent[parent[x as usize] as usize];
215        x = parent[x as usize];
216    }
217    x
218}
219
220/// How many connected pieces a mesh's triangles make, joined at vertices.
221fn shells(mesh: &Triangulation) -> usize {
222    let n = u32::try_from(mesh.positions.len()).unwrap_or(u32::MAX);
223    let mut parent: Vec<u32> = (0..n).collect();
224    let mut used = vec![false; parent.len()];
225    for t in &mesh.triangles {
226        let r = find(&mut parent, t[0]);
227        for &v in t {
228            used[v as usize] = true;
229            let s = find(&mut parent, v);
230            if s != r {
231                parent[s as usize] = r;
232            }
233        }
234    }
235    (0..n)
236        .filter(|&v| used[v as usize] && find(&mut parent, v) == v)
237        .count()
238}
239
240/// The grid: its first node, spacing and node counts.
241struct Lattice {
242    origin: P3,
243    cell: f64,
244    count: [u32; 3],
245    wall: f64,
246    /// The distance beyond which a node's own distance does not matter.
247    reach: f64,
248    /// Whether nodes outside the part need their distances.
249    outside_counts: bool,
250}
251
252fn coords(p: Point) -> P3 {
253    [p.x, p.y, p.z]
254}
255
256/// How many nodes of a grid fit `extent` at `cell`, the last at or past it.
257fn nodes_along(extent: f64, cell: f64) -> OgeomResult<u32> {
258    let count = (extent / cell).ceil() + 1.0;
259    if !(count.is_finite() && count <= f64::from(u32::MAX)) {
260        ogeom_bail!(
261            Range,
262            "a grid of {count} nodes along an axis, a cell of {cell} across {extent}"
263        );
264    }
265    #[allow(
266        clippy::cast_possible_truncation,
267        clippy::cast_sign_loss,
268        reason = "a whole number in 1..=u32::MAX, checked above"
269    )]
270    Ok(count as u32)
271}
272
273/// The six tetrahedra of a cell, as corners numbered by their offsets'
274/// bits, `x` the lowest: each runs from the low corner to the high one,
275/// one axis at a time.
276const TETRAHEDRA: [[u8; 4]; 6] = [
277    [0, 1, 3, 7],
278    [0, 1, 5, 7],
279    [0, 2, 3, 7],
280    [0, 2, 6, 7],
281    [0, 4, 5, 7],
282    [0, 4, 6, 7],
283];
284
285/// The cavity's surface while it is being marched.
286struct Marching {
287    out: Triangulation,
288    /// The vertex on each grid edge crossed, keyed by the edge's low node
289    /// times eight plus the bits of its offset.
290    on_edge: FastMap<u64, u32>,
291}
292
293impl Lattice {
294    fn over(bvh: &Bvh, cell: f64, wall: f64) -> OgeomResult<Self> {
295        let bounds = bvh.bounds();
296        let (Some(low), Some(high)) = (bounds.low(), bounds.high()) else {
297            ogeom_bail!(NullObject, "the part has no triangles");
298        };
299        let size = high - low;
300        Ok(Self {
301            origin: coords(low),
302            cell,
303            count: [
304                nodes_along(size.x, cell)?,
305                nodes_along(size.y, cell)?,
306                nodes_along(size.z, cell)?,
307            ],
308            wall,
309            reach: 2.0f64.mul_add(cell, wall),
310            outside_counts: 3.0f64.sqrt() * cell >= wall,
311        })
312    }
313
314    fn at(&self, i: u32, j: u32, k: u32) -> P3 {
315        [
316            f64::from(i).mul_add(self.cell, self.origin[0]),
317            f64::from(j).mul_add(self.cell, self.origin[1]),
318            f64::from(k).mul_add(self.cell, self.origin[2]),
319        ]
320    }
321
322    /// Where each node of row `j` of layer `k` lies against the part.
323    fn row(&self, bvh: &Bvh, j: u32, k: u32) -> Vec<Lies> {
324        let [_, y, z] = self.at(0, j, k);
325        let line = Line::through(bvh, y, z);
326        (0..self.count[0])
327            .map(|i| line.lies(self.at(i, j, k)[0]))
328            .collect()
329    }
330
331    /// The field at each node of layer `k`, row by row: the wall plus the
332    /// node's signed distance, so the cavity is where it is negative.
333    fn layer(&self, bvh: &Bvh, k: u32) -> Vec<f64> {
334        let rows: Vec<u32> = (0..self.count[1]).collect();
335        let lies: Vec<Lies> = parallel::map_ordered(&rows, |_, &j| self.row(bvh, j, k))
336            .into_iter()
337            .flatten()
338            .collect();
339        let nx = self.count[0];
340        let mut asked = Vec::new();
341        let mut points = Vec::new();
342        let nx = nx as usize;
343        for (at, &l) in lies.iter().enumerate() {
344            if l == Lies::Inside || (l == Lies::Outside && self.outside_counts) {
345                let (i, j) = (at % nx, at / nx);
346                let [x, y, z] = self.at(
347                    u32::try_from(i).unwrap_or(u32::MAX),
348                    u32::try_from(j).unwrap_or(u32::MAX),
349                    k,
350                );
351                asked.push(at);
352                points.push(Point::new(x, y, z));
353            }
354        }
355        let mut field = vec![self.wall; lies.len()];
356        for (at, hit) in asked.into_iter().zip(bvh.nearest_many(&points, self.reach)) {
357            let d = hit.map_or(self.reach, |h| h.distance);
358            field[at] = if lies[at] == Lies::Inside {
359                self.wall - d
360            } else {
361                self.wall + d
362            };
363        }
364        field
365    }
366
367    /// The cavity's surface, wound into the cavity.
368    fn march(&self, bvh: &Bvh) -> OgeomResult<Triangulation> {
369        let mut m = Marching {
370            out: Triangulation::new(),
371            on_edge: FastMap::default(),
372        };
373        let [nx, ny, nz] = self.count;
374        if nx < 2 || ny < 2 || nz < 2 {
375            return Ok(m.out);
376        }
377        // The layers are sampled a few at a time, each on one worker, so the
378        // field is held for those layers only.
379        let chunk = u32::try_from(4 * parallel::threads()).unwrap_or(u32::MAX);
380        let mut ahead = std::collections::VecDeque::new();
381        let mut next: u32 = 0;
382        let mut layer = |k: u32| {
383            if ahead.is_empty() {
384                let ks: Vec<u32> = (next..nz.min(next.saturating_add(chunk))).collect();
385                next = ks.last().map_or(next, |&last| last + 1);
386                ahead.extend(parallel::map_ordered(&ks, |_, &k| self.layer(bvh, k)));
387            }
388            debug_assert!(next > k);
389            ahead.pop_front().unwrap_or_default()
390        };
391        let mut below = layer(0);
392        for k in 1..nz {
393            let above = layer(k);
394            for j in 0..ny - 1 {
395                for i in 0..nx - 1 {
396                    let mut f = [0.0; 8];
397                    for (bits, value) in f.iter_mut().enumerate() {
398                        let layer = if bits & 4 == 0 { &below } else { &above };
399                        let (di, dj) = (usize::from(bits & 1 == 1), usize::from(bits & 2 == 2));
400                        *value = layer[(j as usize + dj) * nx as usize + i as usize + di];
401                    }
402                    let cavity = f.iter().filter(|&&v| v < 0.0).count();
403                    if cavity == 0 || cavity == 8 {
404                        continue;
405                    }
406                    for tet in TETRAHEDRA {
407                        self.tetrahedron(&mut m, [i, j, k - 1], tet, &f)?;
408                    }
409                }
410            }
411            below = above;
412        }
413        m.out.deflection_met = false;
414        Ok(m.out)
415    }
416
417    fn corner(&self, cube: [u32; 3], bits: u8) -> [u32; 3] {
418        [
419            cube[0] + u32::from(bits & 1),
420            cube[1] + u32::from((bits >> 1) & 1),
421            cube[2] + u32::from((bits >> 2) & 1),
422        ]
423    }
424
425    fn point(&self, n: [u32; 3]) -> P3 {
426        self.at(n[0], n[1], n[2])
427    }
428
429    /// The vertex where the surface crosses the edge from corner `inside`,
430    /// in the cavity, to corner `outside`.
431    fn vertex(
432        &self,
433        m: &mut Marching,
434        cube: [u32; 3],
435        (inside, outside): (u8, u8),
436        f: &[f64; 8],
437    ) -> OgeomResult<u32> {
438        let low = self.corner(cube, inside & outside);
439        let node = (u64::from(low[2]) * u64::from(self.count[1]) + u64::from(low[1]))
440            * u64::from(self.count[0])
441            + u64::from(low[0]);
442        let key = node * 8 + u64::from(inside ^ outside);
443        if let Some(&v) = m.on_edge.get(&key) {
444            return Ok(v);
445        }
446        let (a, b) = (f[inside as usize], f[outside as usize]);
447        let t = (a / (a - b)).clamp(CLAMP, 1.0 - CLAMP);
448        let p = self.point(self.corner(cube, inside));
449        let q = self.point(self.corner(cube, outside));
450        let at = Point::new(
451            t.mul_add(q[0] - p[0], p[0]),
452            t.mul_add(q[1] - p[1], p[1]),
453            t.mul_add(q[2] - p[2], p[2]),
454        );
455        let Ok(v) = u32::try_from(m.out.positions.len()) else {
456            ogeom_bail!(Range, "a cavity of more than four billion vertices");
457        };
458        m.out.positions.push(at);
459        m.on_edge.insert(key, v);
460        Ok(v)
461    }
462
463    /// Add the triangle on three vertices, wound so that the corner
464    /// `inside` of the cell, in the cavity, is in front of it.
465    fn triangle(
466        &self,
467        m: &mut Marching,
468        cube: [u32; 3],
469        mut t: [u32; 3],
470        inside: u8,
471    ) -> OgeomResult<()> {
472        let [a, b, c] = t.map(|v| coords(m.out.positions[v as usize]));
473        match Exact::orient3d(a, b, c, self.point(self.corner(cube, inside))) {
474            Sign::Negative => {}
475            Sign::Positive => t.swap(1, 2),
476            Sign::Zero => ogeom_bail!(
477                Invariant,
478                "a cavity triangle's plane passes through a node it separates"
479            ),
480        }
481        m.out.triangles.push(t);
482        Ok(())
483    }
484
485    fn tetrahedron(
486        &self,
487        m: &mut Marching,
488        cube: [u32; 3],
489        tet: [u8; 4],
490        f: &[f64; 8],
491    ) -> OgeomResult<()> {
492        let (inside, outside): (Vec<u8>, Vec<u8>) = tet.iter().partition(|&&c| f[c as usize] < 0.0);
493        match (inside.as_slice(), outside.as_slice()) {
494            (&[a], &[b, c, d]) => {
495                let t = [
496                    self.vertex(m, cube, (a, b), f)?,
497                    self.vertex(m, cube, (a, c), f)?,
498                    self.vertex(m, cube, (a, d), f)?,
499                ];
500                self.triangle(m, cube, t, a)
501            }
502            (&[a, b, c], &[d]) => {
503                let t = [
504                    self.vertex(m, cube, (a, d), f)?,
505                    self.vertex(m, cube, (b, d), f)?,
506                    self.vertex(m, cube, (c, d), f)?,
507                ];
508                self.triangle(m, cube, t, a)
509            }
510            (&[a, b], &[c, d]) => {
511                let ac = self.vertex(m, cube, (a, c), f)?;
512                let ad = self.vertex(m, cube, (a, d), f)?;
513                let bd = self.vertex(m, cube, (b, d), f)?;
514                let bc = self.vertex(m, cube, (b, c), f)?;
515                // Each triangle holds two edges from one corner in the
516                // cavity, so its plane parts that corner from the corners
517                // out of it.
518                self.triangle(m, cube, [ac, ad, bd], a)?;
519                self.triangle(m, cube, [ac, bd, bc], b)
520            }
521            _ => Ok(()),
522        }
523    }
524}
525
526#[cfg(test)]
527#[path = "hollow/tests.rs"]
528mod tests;