use super::{
ErrorWrapper,
io::{extension, invalid_input, read_segmentation, write_mesh},
metrics::write_metrics,
remesh::apply_remesh_subcommand,
smooth::{MeshSmoothCommands, apply_smoothing_method},
};
use clap::Subcommand;
use conspire::{
geometry::{
Coordinate, Coordinates,
grid::{Gradient, MarchingCubes, Voxels},
mesh::{
Class, Connectivity, Finish, Fitting, Freedom, Marching, Mesh, Placement, Tessellation,
},
ntree::{Balance, Balancing, CurvatureSizing, Dualization, Octree, Pairing},
segmentation::Segmentation,
},
math::{Tensor, TensorVec},
units::Length,
};
use std::{collections::HashSet, path::Path, time::Instant};
#[derive(Subcommand)]
pub enum MeshSubcommand {
Hex(MeshArgs),
Hexdom(MeshArgs),
Poly(MeshArgs),
Tet(MeshArgs),
Tri(MeshArgs),
}
#[derive(clap::Args)]
pub struct MeshArgs {
#[command(subcommand)]
pub smoothing: Option<MeshSmoothCommands>,
#[arg(long, short, value_name = "FILE")]
pub input: String,
#[arg(long, short, value_name = "FILE")]
pub output: String,
#[arg(long, short, value_name = "NUM")]
pub defeature: Option<usize>,
#[arg(long, short = 'x', value_name = "NEL")]
pub nelx: Option<usize>,
#[arg(long, short = 'y', value_name = "NEL")]
pub nely: Option<usize>,
#[arg(long, short = 'z', value_name = "NEL")]
pub nelz: Option<usize>,
#[arg(long, num_args = 1.., short, value_delimiter = ' ', value_name = "ID")]
pub remove: Option<Vec<usize>>,
#[arg(default_value_t = 1.0, long, value_name = "SCALE")]
pub xscale: f64,
#[arg(default_value_t = 1.0, long, value_name = "SCALE")]
pub yscale: f64,
#[arg(default_value_t = 1.0, long, value_name = "SCALE")]
pub zscale: f64,
#[arg(
long,
default_value_t = 0.0,
allow_negative_numbers = true,
value_name = "VAL"
)]
pub xtranslate: f64,
#[arg(
long,
default_value_t = 0.0,
allow_negative_numbers = true,
value_name = "VAL"
)]
pub ytranslate: f64,
#[arg(
long,
default_value_t = 0.0,
allow_negative_numbers = true,
value_name = "VAL"
)]
pub ztranslate: f64,
#[arg(long, default_value_t = 5.0, short = 's', value_name = "SCALE")]
pub scale: f64,
#[arg(long, short = 'u', value_name = "SPACING")]
pub uniform: Option<f64>,
#[arg(long, short = 't', value_name = "TOL")]
pub tolerance: Option<f64>,
#[arg(action, long)]
pub strong: bool,
#[arg(action, long, requires = "uniform")]
pub inflate: bool,
#[arg(action, conflicts_with = "inflate", long, requires = "uniform")]
pub marching: bool,
#[arg(action, long)]
pub snap: bool,
#[arg(conflicts_with_all = ["inflate", "marching"], long, value_name = "MSJ")]
pub pyramids: Option<f64>,
#[arg(long, default_value_t = 1, short = 'l', value_name = "NUM")]
pub levels: usize,
#[arg(long, value_name = "FILE")]
pub metrics: Option<String>,
#[arg(long, value_enum, default_value_t = Cubes::Cuberille, value_name = "METHOD")]
pub cubes: Cubes,
}
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum Cubes {
Cuberille,
Marching,
}
pub enum Element {
Hexahedra,
HexDominant,
Polyhedra,
Tetrahedra,
Triangles,
}
fn read_voxels(args: &MeshArgs, quiet: bool) -> Result<Voxels<u8>, ErrorWrapper> {
match extension(&args.input) {
Some("npy") | Some("spn") => {
let mut voxels =
read_segmentation(&args.input, args.nelx, args.nely, args.nelz, quiet)?;
if let Some(min) = args.defeature {
let time = Instant::now();
crate::echo!(
quiet,
" \x1b[1;96mDefeaturing\x1b[0m clusters of {min} voxels or less"
);
voxels = voxels.defeature(min)?;
crate::echo!(quiet, " \x1b[1;92mDone\x1b[0m {:?}", time.elapsed());
}
Ok(voxels)
}
extension => Err(invalid_input(&args.input, extension)),
}
}
fn finish(mut mesh: Mesh<3>, args: MeshArgs, quiet: bool) -> Result<(), ErrorWrapper> {
if let Some(MeshSmoothCommands::Smooth {
remeshing,
iterations,
method,
pass_band,
scale,
hierarchical,
}) = args.smoothing
{
apply_smoothing_method(
&mut mesh,
iterations,
method,
pass_band,
scale,
hierarchical,
quiet,
)?;
if let Some(subcommand) = remeshing {
mesh = apply_remesh_subcommand(mesh, subcommand, quiet)?;
}
}
if let Some(file) = &args.metrics {
write_metrics(&mesh, file, quiet)?;
}
write_mesh(&args.output, mesh, quiet)
}
pub fn mesh(element: Element, args: MeshArgs, quiet: bool) -> Result<(), ErrorWrapper> {
match (&element, extension(&args.input)) {
(Element::Hexahedra, Some("stl")) => return hexahedralize(args, quiet),
(Element::Tetrahedra, Some("stl")) => return tetrahedralize(args, quiet),
(element @ (Element::HexDominant | Element::Polyhedra), Some("stl")) => {
return cut(args, element, quiet);
}
(Element::HexDominant | Element::Polyhedra | Element::Tetrahedra, extension) => {
return Err(invalid_input(&args.input, extension));
}
_ => {}
}
if args.uniform.is_some() {
return Err(ErrorWrapper::from(
"Uniform lattice meshing applies to tessellation (stl) inputs only",
));
}
let voxels = read_voxels(&args, quiet)?;
let time = Instant::now();
let mesh = match element {
Element::Hexahedra => {
crate::echo!(quiet, " \x1b[1;96mMeshing\x1b[0m voxels into hexahedra");
let remove: Option<Vec<u8>> = args
.remove
.as_ref()
.map(|ids| ids.iter().map(|&id| id as u8).collect());
let scale = Coordinate::from([args.xscale, args.yscale, args.zscale]);
let translate = Coordinate::from([args.xtranslate, args.ytranslate, args.ztranslate]);
let segmentation = Segmentation::new(voxels, scale, translate);
Mesh::from_segmentation(segmentation, remove.as_deref())
}
Element::HexDominant | Element::Polyhedra => {
unreachable!("cutting requires a tessellation input")
}
Element::Tetrahedra => unreachable!("tetrahedralizing requires a tessellation input"),
Element::Triangles => {
crate::echo!(quiet, " \x1b[1;96mMeshing\x1b[0m voxels into triangles");
let voxels = remove_materials(voxels, args.remove.as_deref());
let mesh = match args.cubes {
Cubes::Cuberille => Mesh::from(Tessellation::from(voxels)),
Cubes::Marching => marching(&voxels)?,
};
scaled(
mesh,
[args.xscale, args.yscale, args.zscale],
[args.xtranslate, args.ytranslate, args.ztranslate],
)
}
};
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
finish(mesh, args, quiet)
}
fn hexahedralize(args: MeshArgs, quiet: bool) -> Result<(), ErrorWrapper> {
crate::echo!(quiet, " \x1b[1;96mReading\x1b[0m {}", args.input);
let mut time = Instant::now();
let tessellation = Tessellation::try_from(Path::new(&args.input))?;
crate::echo!(quiet, " \x1b[1;92mDone\x1b[0m {:?}", time.elapsed());
if args.inflate || args.marching {
return marching_hex(args, tessellation, quiet);
}
let fitting = if args.snap {
Fitting::Snap
} else {
Fitting::Soft
};
crate::echo!(
quiet,
" \x1b[1;96mMeshing\x1b[0m hexahedra {}",
if args.uniform.is_some() {
"uniformly"
} else {
"adaptively"
}
);
time = Instant::now();
let mut mesh = if let Some(spacing) = args.uniform {
tessellation.lattice_background(Length::meters(spacing))?.0
} else {
let balancing = if args.strong {
Balancing::Strong(1)
} else {
Balancing::Weak(1)
};
let mut octree = Octree::<u16, usize>::from_features(
&tessellation,
args.scale,
CurvatureSizing {
tolerance: args.tolerance.map(Length::meters),
..Default::default()
},
0,
)?;
octree.equilibrate(balancing, Pairing::Regular)?;
octree.dualize()
};
tessellation.trim(&mut mesh)?;
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
crate::echo!(
quiet,
" \x1b[1;96mBuffering\x1b[0m hexahedra onto geometry"
);
time = Instant::now();
let mesh = if let Some(threshold) = args.pyramids {
mesh.buffer_targeted(&tessellation, fitting, threshold)?
} else {
mesh.buffer(&tessellation, fitting)?
};
let mesh = scaled(
mesh,
[args.xscale, args.yscale, args.zscale],
[args.xtranslate, args.ytranslate, args.ztranslate],
);
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
finish(mesh, args, quiet)
}
fn marching_hex(
args: MeshArgs,
tessellation: Tessellation,
quiet: bool,
) -> Result<(), ErrorWrapper> {
if args.marching && args.snap {
return Err(ErrorWrapper::from(
"Snapping applies to an inflated mesh, not one left as cut",
));
}
let size = args
.uniform
.expect("marching cubes requires a uniform element size");
crate::echo!(quiet, " \x1b[1;96mMeshing\x1b[0m hexahedra uniformly");
let mut time = Instant::now();
let mut mesh = tessellation.marching_hex(
Length::meters(size * 2.0),
Marching {
placement: Placement::Crossing(0.2),
finish: Finish::Cut,
},
)?;
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
if args.inflate {
crate::echo!(
quiet,
" \x1b[1;96mInflating\x1b[0m hexahedra onto geometry"
);
time = Instant::now();
mesh.inflate(
&tessellation,
Freedom::Whole,
if args.snap {
Fitting::Snap
} else {
Fitting::Soft
},
)?;
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
}
let mesh = scaled(
mesh,
[args.xscale, args.yscale, args.zscale],
[args.xtranslate, args.ytranslate, args.ztranslate],
);
finish(mesh, args, quiet)
}
fn tetrahedralize(args: MeshArgs, quiet: bool) -> Result<(), ErrorWrapper> {
crate::echo!(quiet, " \x1b[1;96mReading\x1b[0m {}", args.input);
let mut time = Instant::now();
let tessellation = Tessellation::try_from(Path::new(&args.input))?;
crate::echo!(quiet, " \x1b[1;92mDone\x1b[0m {:?}", time.elapsed());
let fitting = if args.snap {
Fitting::Snap
} else {
Fitting::Soft
};
crate::echo!(
quiet,
" \x1b[1;96mMeshing\x1b[0m tetrahedra {}",
if args.uniform.is_some() {
"uniformly"
} else {
"adaptively"
}
);
time = Instant::now();
let (mut mesh, _classes) = if let Some(spacing) = args.uniform {
tessellation.lattice_tet_background(Length::meters(spacing))?
} else {
tessellation.octree_tet_background(
Balancing::Strong(1),
Pairing::None,
args.scale,
args.tolerance.map(Length::meters),
)?
};
tessellation.trim(&mut mesh)?;
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
crate::echo!(
quiet,
" \x1b[1;96mBuffering\x1b[0m tetrahedra onto geometry"
);
time = Instant::now();
let mesh = mesh.buffer_tets(&tessellation, fitting)?;
let mesh = scaled(
mesh,
[args.xscale, args.yscale, args.zscale],
[args.xtranslate, args.ytranslate, args.ztranslate],
);
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
finish(mesh, args, quiet)
}
fn cut(args: MeshArgs, element: &Element, quiet: bool) -> Result<(), ErrorWrapper> {
crate::echo!(quiet, " \x1b[1;96mReading\x1b[0m {}", args.input);
let mut time = Instant::now();
let tessellation = Tessellation::try_from(Path::new(&args.input))?;
crate::echo!(quiet, " \x1b[1;92mDone\x1b[0m {:?}", time.elapsed());
let polyhedral = matches!(element, Element::Polyhedra);
if polyhedral && args.uniform.is_some() {
return Err(ErrorWrapper::from(
"Uniform lattice meshing applies to mesh hex and mesh hexdom only",
));
}
if !polyhedral && args.levels != 1 {
return Err(ErrorWrapper::from(
"Dualization requires 2:1 balancing, so levels applies to mesh poly only",
));
}
crate::echo!(
quiet,
" \x1b[1;96mMeshing\x1b[0m {} {}",
if polyhedral { "polyhedra" } else { "hexahedra" },
if args.uniform.is_some() {
"uniformly"
} else {
"adaptively"
}
);
time = Instant::now();
let (background, classes) = if let Some(spacing) = args.uniform {
tessellation.lattice_background(Length::meters(spacing))
} else {
let balancing = if args.strong {
Balancing::Strong(args.levels)
} else {
Balancing::Weak(args.levels)
};
if polyhedral {
tessellation.octree_background(balancing, args.scale)
} else {
tessellation.dual_background(balancing, args.scale)
}
}?;
let (elements, nodes) = retained(&background, &classes);
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{elements} elements, {nodes} nodes]\x1b[0m",
time.elapsed()
);
crate::echo!(
quiet,
" \x1b[1;96mBuffering\x1b[0m polyhedra onto geometry"
);
time = Instant::now();
let mesh = if polyhedral {
tessellation.cut_polyhedral(background, &classes)
} else {
tessellation.cut(background, &classes)
}?;
let mesh = scaled(
mesh,
[args.xscale, args.yscale, args.zscale],
[args.xtranslate, args.ytranslate, args.ztranslate],
);
crate::echo!(
quiet,
" \x1b[1;92mDone\x1b[0m {:?} \x1b[2m[{} elements, {} nodes]\x1b[0m",
time.elapsed(),
mesh.number_of_elements(),
mesh.number_of_nodes()
);
finish(mesh, args, quiet)
}
fn retained(background: &Mesh<3>, classes: &[Class]) -> (usize, usize) {
let mut nodes = HashSet::new();
let elements = background
.connectivities()
.iter()
.flatten()
.zip(classes)
.filter(|(_, class)| !matches!(class, Class::Outside))
.inspect(|(element, _)| nodes.extend(element.iter().copied()))
.count();
(elements, nodes.len())
}
fn remove_materials(voxels: Voxels<u8>, remove: Option<&[usize]>) -> Voxels<u8> {
match remove {
Some(remove) if !remove.is_empty() => {
let nel = *voxels.nel();
let data = voxels
.data()
.iter()
.map(|&block| {
if remove.contains(&(block as usize)) {
0
} else {
block
}
})
.collect();
Voxels::new(data, nel)
}
_ => voxels,
}
}
fn marching(voxels: &Voxels<u8>) -> Result<Mesh<3>, ErrorWrapper> {
let [nx, ny, nz] = *voxels.nel();
let padded = [nz + 2, ny + 2, nx + 2];
let mut materials: Vec<u8> = voxels.data().iter().copied().filter(|&m| m != 0).collect();
materials.sort_unstable();
materials.dedup();
let extractor = MarchingCubes {
gradient: Gradient::Ascent,
degenerate: false,
..Default::default()
};
let mut coordinates = Coordinates::new();
let mut connectivities = Vec::with_capacity(materials.len());
for material in materials {
let mut data = vec![0.0; padded.iter().product()];
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
if voxels.data()[voxels.flat([i, j, k])] == material {
data[((k + 1) * padded[1] + j + 1) * padded[2] + i + 1] = 1.0;
}
}
}
}
let surface = extractor
.extract(&Voxels::new_row_major(data, padded), None)
.map_err(ErrorWrapper::from)?;
let offset = coordinates.len();
for vertex in surface.vertices.iter() {
coordinates.push(Coordinate::from([
vertex[0] + Length::meters(-0.5),
vertex[1] + Length::meters(-0.5),
vertex[2] + Length::meters(-0.5),
]));
}
let faces: Vec<[usize; 3]> = surface
.faces
.iter()
.map(|face| face.map(|node| node + offset))
.collect();
connectivities.push(Connectivity::Triangular(faces.into()));
}
Ok(Mesh::from((connectivities, coordinates)))
}
fn scaled(mesh: Mesh<3>, scale: [f64; 3], translate: [f64; 3]) -> Mesh<3> {
if scale == [1.0, 1.0, 1.0] && translate == [0.0, 0.0, 0.0] {
return mesh;
}
let (connectivities, coordinates) = mesh.into();
let coordinates: Coordinates<3> = coordinates
.iter()
.map(|coordinate| {
Coordinate::from([
coordinate[0] * scale[0] + Length::meters(translate[0]),
coordinate[1] * scale[1] + Length::meters(translate[1]),
coordinate[2] * scale[2] + Length::meters(translate[2]),
])
})
.collect();
Mesh::from((connectivities.into_members(), coordinates))
}