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::Voxels,
mesh::{Class, Fitting, Mesh, Tessellation},
ntree::{Balance, Balancing, CurvatureSizing, Dualization, Octree, Pairing},
segmentation::Segmentation,
},
math::Tensor,
units::Length,
};
use std::{collections::HashSet, path::Path, time::Instant};
#[derive(Subcommand)]
pub enum MeshSubcommand {
Hex(MeshArgs),
Hexdom(MeshArgs),
Poly(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)]
pub snap: bool,
#[arg(long, default_value_t = 1, short = 'l', value_name = "NUM")]
pub levels: usize,
#[arg(long, value_name = "FILE")]
pub metrics: Option<String>,
}
pub enum Element {
Hexahedra,
HexDominant,
Polyhedra,
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, true)?;
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 @ (Element::HexDominant | Element::Polyhedra), Some("stl")) => {
return cut(args, element, quiet);
}
(Element::HexDominant | Element::Polyhedra, 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::Triangles => {
crate::echo!(quiet, " \x1b[1;96mMeshing\x1b[0m voxels into triangles");
let voxels = remove_materials(voxels, args.remove.as_deref());
let mesh = Mesh::from(Tessellation::from(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());
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 = 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 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 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))
}