1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum Material {
92 Missing,
95 Excess,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum Form {
103 Layer,
105 Lump,
107}
108
109#[derive(Debug, Clone, PartialEq)]
111pub struct Region {
112 pub side: Material,
114 pub volume: f64,
116 pub depth: f64,
119 pub kind: Form,
121 pub bounds: Aabb,
123 pub unsure: bool,
126}
127
128const VERTEX_BATCH: usize = 4096;
130
131pub 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
208struct Closed {
210 mesh: Triangulation,
211 first_fill: usize,
214}
215
216fn 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 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 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
343struct 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 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 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 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
448struct 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 band: f64,
458 tol: Tolerances,
459}
460
461struct Read {
464 differ: Vec<(u64, Material)>,
465 doubtful: Vec<u64>,
466}
467
468#[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 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 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
589struct Regions {
591 of: Vec<u32>,
593 count: usize,
594 doubted: Vec<bool>,
596}
597
598fn 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
611fn 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 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 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 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 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 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 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}