use brodo::analysis::euler;
use brodo::analysis::{Connectivity, Label, Labels, label};
use brodo::tree::Coord;
use crate::convert::{Converted, Inside, Lattice};
use crate::distance::Point;
use crate::error::ReportError;
use crate::mesh::Mesh;
use crate::surface::Surface;
use crate::thin::{check, resolves, thin};
use crate::winding::Winding;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Betti {
pub components: i64,
pub tunnels: i64,
pub cavities: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Reference {
Closed {
surface_euler: i64,
solid_euler: i64,
},
None {
surface: Surface,
},
}
pub type Pair = (usize, usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Place {
Solid,
Complement {
component: u32,
outside: bool,
},
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Region {
pub name: String,
pub point: Point,
pub place: Place,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ThinKind {
Wall,
Gap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Invariants {
pub euler: i64,
pub solid: i64,
pub complement: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Check {
Same(Invariants),
Changed {
coarse: Invariants,
fine: Invariants,
},
Unchecked,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Thin {
pub kind: ThinKind,
pub lo: Point,
pub hi: Point,
pub least: f64,
pub points: u64,
pub check: Check,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Report {
pub solid: Betti,
pub complement: Betti,
pub euler: i64,
pub reference: Reference,
pub euler_change: Option<i64>,
pub regions: Vec<Region>,
pub together: Vec<Pair>,
pub apart: Vec<Pair>,
pub thin: Vec<Thin>,
pub located: bool,
}
fn lattice(c: &Converted) -> Result<Lattice, ReportError> {
c.lattice().ok_or(ReportError::Transform)
}
fn voxel_size(c: &Converted) -> Result<f64, ReportError> {
Ok(lattice(c)?.h.into_iter().fold(f64::INFINITY, f64::min))
}
fn solid(v: f32) -> bool {
v < 0.0
}
fn voxel(c: &Converted, p: Point) -> Result<Option<Coord>, ReportError> {
let l = lattice(c)?;
let mut out = [0_i32; 3];
for (k, o) in out.iter_mut().enumerate() {
let index = ((p[k] - l.origin[k]) / l.h[k]).floor();
if !(f64::from(i32::MIN)..=f64::from(i32::MAX)).contains(&index) {
return Ok(None);
}
#[expect(clippy::cast_possible_truncation, reason = "within i32, checked above")]
let i = index as i32;
*o = i;
}
Ok(Some(out))
}
fn place(c: &Converted, complement: &Labels, p: Point) -> Result<Place, ReportError> {
let Some(v) = voxel(c, p)? else {
return Ok(Place::Complement { component: outside_component(complement), outside: true });
};
Ok(match complement.label_at(&c.topology, v)? {
Label::Component(component) => Place::Complement {
component,
outside: complement.reaches_outside(Label::Component(component)),
},
Label::Excluded => Place::Solid,
})
}
fn outside_component(complement: &Labels) -> u32 {
(0..complement.count())
.find(|k| complement.reaches_outside(Label::Component(*k)))
.unwrap_or(complement.count())
}
fn reference(mesh: &Mesh) -> Result<Reference, ReportError> {
let surface = Surface::of(&mesh.welded()?)?;
Ok(if surface.closed_manifold() {
Reference::Closed { surface_euler: surface.euler(), solid_euler: surface.euler() / 2 }
} else {
Reference::None { surface }
})
}
fn pairs(regions: &[Region]) -> [Vec<Pair>; 2] {
let (mut together, mut apart) = (Vec::new(), Vec::new());
for (i, a) in regions.iter().enumerate() {
for (j, b) in regions.iter().enumerate().skip(i + 1) {
if let (
Place::Complement { component: x, .. },
Place::Complement { component: y, .. },
) = (a.place, b.place)
{
if x == y { together.push((i, j)) } else { apart.push((i, j)) }
}
}
}
[together, apart]
}
fn third_layer(mesh: &Mesh, converted: &Converted) -> Result<Vec<Thin>, ReportError> {
let winding = Winding::new(mesh)?;
let inside = Inside::new(&winding, converted.sign, converted.accuracy);
let h = voxel_size(converted)?;
if !resolves(winding.bvh(), h) {
return Err(ReportError::TooFar);
}
let mut places = thin(winding.bvh(), &inside, h);
for place in &mut places {
place.check = check(converted, &inside, place)?;
}
Ok(places)
}
pub fn report(
mesh: &Mesh,
converted: &Converted,
regions: &[(String, Point)],
) -> Result<Report, ReportError> {
let (t, f) = (&converted.topology, &converted.field);
let solid_labels = label(t, f, solid, Connectivity::TwentySix)?;
let complement = label(t, f, |v| !solid(v), Connectivity::Six)?;
let count = |n: u32| i64::from(n);
let reaching = (0..complement.count())
.filter(|k| complement.reaches_outside(Label::Component(*k)))
.count();
let components = count(solid_labels.count());
let cavities = count(complement.count()) - i64::try_from(reaching).unwrap_or(i64::MAX);
let chi = euler(t, f, solid, Connectivity::TwentySix)?;
let tunnels = components + cavities - chi;
let reference = reference(mesh)?;
let euler_change = match reference {
Reference::Closed { solid_euler, .. } => Some(chi - solid_euler),
Reference::None { .. } => None,
};
let regions = regions
.iter()
.enumerate()
.map(|(i, (name, point))| {
if !point.iter().all(|x| x.is_finite()) {
return Err(ReportError::Region(i));
}
Ok(Region {
name: name.clone(),
point: *point,
place: place(converted, &complement, *point)?,
})
})
.collect::<Result<Vec<_>, ReportError>>()?;
let [together, apart] = pairs(®ions);
let thin = third_layer(mesh, converted)?;
Ok(Report {
solid: Betti { components, tunnels, cavities },
complement: Betti {
components: count(complement.count()),
tunnels,
cavities: if components == 0 { 1 } else { components - 1 },
},
euler: chi,
reference,
euler_change,
regions,
together,
apart,
thin,
located: true,
})
}