use std::error::Error;
use std::path::{Path, PathBuf};
use brodo::slice::{Axis, ColorMap, Slice, render};
use colino::convert::{Converted, Options, convert};
use colino::mesh::{Limits, Mesh};
use colino::report::{Report, report};
type BoxError = Box<dyn Error + Send + Sync>;
const RING: f64 = 20.0;
const TUBE: f64 = 7.0;
const STEPS: u32 = 48;
const H: f64 = 1.0;
const LIMITS: Limits = Limits::new(1 << 24, 1 << 24, 1 << 30);
fn torus_obj() -> String {
let angle = |i: u32| 2.0 * std::f64::consts::PI * f64::from(i % STEPS) / f64::from(STEPS);
let mut text = String::new();
for i in 0..STEPS {
for j in 0..STEPS {
let (u, v) = (angle(i), angle(j));
let ring = RING + TUBE * v.cos();
text.push_str(&format!("v {} {} {}\n", ring * u.cos(), ring * u.sin(), TUBE * v.sin()));
}
}
let at = |i: u32, j: u32| (i % STEPS) * STEPS + j % STEPS + 1;
for i in 0..STEPS {
for j in 0..STEPS {
let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
text.push_str(&format!("f {a} {b} {c}\nf {a} {c} {d}\n"));
}
}
text
}
fn exact(p: [f64; 3]) -> f64 {
((p[0].hypot(p[1]) - RING).powi(2) + p[2] * p[2]).sqrt() - TUBE
}
fn worst(c: &Converted) -> Result<f64, BoxError> {
let width = 3.0 * H;
let mut worst: f64 = 0.0;
for n in c.topology.leaf_numbers() {
for (v, x) in c.topology.leaf_voxels(n)?.zip(c.field.leaf(&c.topology, n)?) {
let d = exact(v.map(|k| (f64::from(k) + 0.5) * H));
if d.abs() < width - 0.5 {
worst = worst.max((f64::from(*x) - d).abs());
}
}
}
Ok(worst)
}
fn faceting() -> f64 {
let sag = 1.0 - (std::f64::consts::PI / f64::from(STEPS)).cos();
(RING + 2.0 * TUBE) * sag + 3.0 * H / f64::from(1_u32 << 24)
}
fn out_dir() -> Result<PathBuf, BoxError> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../out/examples/colino");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
fn write(name: &str, c: &Converted, r: &Report, half: i32) -> Result<(), BoxError> {
let dir = out_dir()?;
let slice = Slice::new(Axis::Z, 0, [-half, -half], [half - 1, half - 1]);
let image = render(&c.topology, &c.field, slice, ColorMap::Diverging { limit: 3.0 })?;
brodo::slice::write(&dir.join(format!("{name}_z0.png")), &image)?;
std::fs::write(dir.join(format!("{name}_report.txt")), format!("{r:#?}\n"))?;
Ok(())
}
fn run() -> Result<(Report, f64), BoxError> {
let mesh = colino::obj::from_bytes(torus_obj().as_bytes(), LIMITS)?;
let options = Options::new(H).with_unit(Some("millimeter".into()));
let c = convert(&mesh, &options)?;
let r = report(&mesh, &c, &[("hole".into(), [0.0; 3]), ("tube".into(), [RING, 0.0, 0.0])])?;
write("torus", &c, &r, 32)?;
Ok((r, worst(&c)?))
}
fn read(path: &Path) -> Result<Mesh, BoxError> {
let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
Ok(match ext.as_deref() {
Some("stl") => colino::stl::read(path, LIMITS)?,
Some("obj") => colino::obj::read(path, LIMITS)?,
Some("3mf") => colino::threemf::read(path, LIMITS)?,
_ => return Err(format!("{}: not .stl, .obj or .3mf", path.display()).into()),
})
}
fn convert_file(path: &Path, h: f64) -> Result<(), BoxError> {
let mesh = read(path)?;
let options = Options::new(h).with_unit(Some("millimeter".into()));
let c = convert(&mesh, &options)?;
let r = report(&mesh, &c, &[])?;
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("mesh");
std::fs::write(out_dir()?.join(format!("{name}_report.txt")), format!("{r:#?}\n"))?;
Ok(())
}
fn main() -> Result<(), BoxError> {
let args: Vec<String> = std::env::args().skip(1).collect();
if let [path, h] = args.as_slice() {
return convert_file(Path::new(path), h.parse()?);
}
let (r, worst) = run()?;
check(&r, worst)
}
fn check(r: &Report, worst: f64) -> Result<(), BoxError> {
let betti = [r.solid.components, r.solid.tunnels, r.solid.cavities];
if betti != [1, 1, 0] || r.euler_change != Some(0) {
return Err(format!("β {betti:?}, χ change {:?}", r.euler_change).into());
}
if worst > faceting() {
return Err(format!("a band value {worst} mm from the exact distance").into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use colino::report::Place;
#[test]
fn the_torus_keeps_its_tunnel_and_its_distances() {
let (r, worst) = run().unwrap();
check(&r, worst).unwrap();
assert!(matches!(r.regions[0].place, Place::Complement { outside: true, .. }));
assert_eq!(r.regions[1].place, Place::Solid);
assert!(worst > 0.0, "the faceting shows");
}
}