1use 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
71pub const CLAMP: f64 = 1.0 / 1024.0;
79
80#[derive(Debug, Clone, Copy, PartialEq, Default)]
82pub struct Grid {
83 pub cell: Option<f64>,
85}
86
87#[derive(Debug, Clone, PartialEq)]
89pub struct Hollowed {
90 pub mesh: Triangulation,
94 pub first_cavity_triangle: usize,
96 pub shells: usize,
100 pub cell: f64,
102 pub deviation: Option<Deviation>,
105}
106
107pub 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
193fn 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
211fn 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
220fn 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
240struct Lattice {
242 origin: P3,
243 cell: f64,
244 count: [u32; 3],
245 wall: f64,
246 reach: f64,
248 outside_counts: bool,
250}
251
252fn coords(p: Point) -> P3 {
253 [p.x, p.y, p.z]
254}
255
256fn 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
273const 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
285struct Marching {
287 out: Triangulation,
288 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 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 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 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 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 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 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 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;