use std::error::Error;
use std::path::Path;
use brodo::slice::{Axis, ColorMap, Slice, render};
use colino::convert::{Options, convert};
use colino::mesh::Mesh;
use colino::report::{Report, report};
type BoxError = Box<dyn Error + Send + Sync>;
const CUBE: f64 = 2.0;
const SIDE: i32 = 20;
const WALL: i32 = 3;
fn cells() -> Vec<[i32; 3]> {
let mut out = Vec::new();
for z in 0..SIDE {
for y in 0..SIDE {
for x in 0..SIDE {
let cavity = [x, y, z].iter().all(|c| (WALL..SIDE - WALL).contains(c));
let vent = x < WALL && y == SIDE / 2 && z == SIDE / 2;
if !cavity && !vent {
out.push([x, y, z]);
}
}
}
}
out
}
fn boundary(cells: &[[i32; 3]]) -> Result<Mesh, BoxError> {
let set: std::collections::BTreeSet<[i32; 3]> = cells.iter().copied().collect();
let mut m = Mesh::new(Vec::new(), Vec::new()).with_unit(Some("millimeter".into()));
for c in cells {
for axis in 0..3 {
for side in [0, 1] {
let mut n = *c;
n[axis] += 2 * side - 1;
if set.contains(&n) {
continue;
}
let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
let corner = |a: i32, b: i32| {
let mut p = c.map(f64::from);
p[axis] += f64::from(side);
p[u] += f64::from(a);
p[v] += f64::from(b);
p.map(|x| x * CUBE)
};
let base = u32::try_from(m.vertices.len())?;
m.vertices.extend([corner(0, 0), corner(1, 0), corner(1, 1), corner(0, 1)]);
let q = if side == 1 { [0, 1, 2, 3] } else { [0, 3, 2, 1] }.map(|i| base + i);
m.triangles.extend([[q[0], q[1], q[2]], [q[0], q[2], q[3]]]);
}
}
}
Ok(m.welded()?)
}
fn run(mesh: &Mesh, h: f64, tag: &str) -> Result<Report, BoxError> {
let c = convert(mesh, &Options::new(h))?;
let middle = f64::from(SIDE) * CUBE / 2.0;
let points = [("cavity".into(), [middle; 3]), ("outside".into(), [-10.0, middle, middle])];
let r = report(mesh, &c, &points)?;
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../out/examples/colino");
std::fs::create_dir_all(&dir)?;
#[expect(clippy::cast_possible_truncation, reason = "a voxel index within the box")]
let (vent, end) = (((middle + 1.0) / h) as i32, ((f64::from(SIDE) * CUBE) / h) as i32 + 4);
let slice = Slice::new(Axis::Z, vent, [-4, -4], [end, end]);
let image = render(&c.topology, &c.field, slice, ColorMap::Diverging { limit: 3.0 * h })?;
brodo::slice::write(&dir.join(format!("vent_h{tag}.png")), &image)?;
std::fs::write(dir.join(format!("vent_h{tag}_report.txt")), format!("{r:#?}\n"))?;
Ok(r)
}
fn both() -> Result<[Report; 2], BoxError> {
let mesh = boundary(&cells())?;
Ok([run(&mesh, 1.0, "1")?, run(&mesh, 3.0, "3")?])
}
fn check([fine, coarse]: &[Report; 2]) -> Result<(), BoxError> {
let betti = |r: &Report| [r.solid.components, r.solid.tunnels, r.solid.cavities];
if fine.together != [(0, 1)] || fine.euler_change != Some(0) || betti(fine) != [1, 0, 0] {
return Err(format!("at 1 mm: {fine:?}").into());
}
if coarse.apart != [(0, 1)] || coarse.euler_change != Some(1) || betti(coarse) != [1, 0, 1] {
return Err(format!("at 3 mm: {coarse:?}").into());
}
Ok(())
}
fn main() -> Result<(), BoxError> {
check(&both()?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_vent_closes_when_narrower_than_a_voxel() {
check(&both().unwrap()).unwrap();
}
}