mod internals;
#[cfg(test)]
mod tests;
use honeycomb_core::attributes::AttributeBind;
use honeycomb_core::cmap::{CMap2, CMap3, CMapBuilder};
use honeycomb_core::geometry::{CoordsFloat, Vertex2, Vertex3};
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum GridBuilderError {
#[error("invalid grid parameters - {0}")]
InvalidGridParameters(&'static str),
#[error("insufficient parameters - please specify at least 2")]
MissingGridParameters,
}
pub struct GridBuilder<const D: usize, T: CoordsFloat> {
pub(crate) map_builder: CMapBuilder<D>,
pub(crate) origin: [T; D],
pub(crate) n_cells: Option<[usize; D]>,
pub(crate) len_per_cell: Option<[T; D]>,
pub(crate) lens: Option<[T; D]>,
pub(crate) split_cells: bool,
}
impl<const D: usize, T: CoordsFloat> Default for GridBuilder<D, T> {
fn default() -> Self {
Self {
map_builder: CMapBuilder::<D>::from_n_darts(1),
origin: [T::zero(); D],
n_cells: None,
len_per_cell: None,
lens: None,
split_cells: false,
}
}
}
impl<const D: usize, T: CoordsFloat> GridBuilder<D, T> {
#[must_use = "unused builder object"]
pub fn n_cells(mut self, n_cells: [usize; D]) -> Self {
self.n_cells = Some(n_cells);
self
}
#[must_use = "unused builder object"]
pub fn len_per_cell(mut self, len_per_cell: [T; D]) -> Self {
self.len_per_cell = Some(len_per_cell);
self
}
#[must_use = "unused builder object"]
pub fn lens(mut self, lens: [T; D]) -> Self {
self.lens = Some(lens);
self
}
#[must_use = "unused builder object"]
pub fn origin(mut self, origin: [T; D]) -> Self {
self.origin = origin;
self
}
#[must_use = "unused builder object"]
pub fn split_cells(mut self, split: bool) -> Self {
self.split_cells = split;
self
}
#[must_use = "unused builder object"]
pub fn add_attribute<A: AttributeBind + 'static>(mut self) -> Self {
let tmp = self.map_builder.add_attribute::<A>();
self.map_builder = tmp;
self
}
}
macro_rules! check_parameters {
($id: ident, $msg: expr) => {
if $id.is_sign_negative() | $id.is_zero() {
return Err(GridBuilderError::InvalidGridParameters($msg));
}
};
}
impl<T: CoordsFloat> GridBuilder<2, T> {
#[allow(clippy::type_complexity)]
pub(crate) fn parse_2d(
self,
) -> Result<(CMapBuilder<2>, Vertex2<T>, [usize; 2], [T; 2]), GridBuilderError> {
match (self.n_cells, self.len_per_cell, self.lens) {
(Some([nx, ny]), Some([lpx, lpy]), lens) => {
if lens.is_some() {
eprintln!(
"W: All three grid parameters were specified, total lengths will be ignored"
);
}
#[rustfmt::skip]
check_parameters!(lpx, "length per x cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpy, "length per y cell is null or negative");
Ok((
self.map_builder,
Vertex2(self.origin[0], self.origin[1]),
[nx, ny],
[lpx, lpy],
))
}
(Some([nx, ny]), None, Some([lx, ly])) => {
#[rustfmt::skip]
check_parameters!(lx, "grid length along x is null or negative");
#[rustfmt::skip]
check_parameters!(ly, "grid length along y is null or negative");
Ok((
self.map_builder,
Vertex2(self.origin[0], self.origin[1]),
[nx, ny],
[lx / T::from(nx).unwrap(), ly / T::from(ny).unwrap()],
))
}
(None, Some([lpx, lpy]), Some([lx, ly])) => {
#[rustfmt::skip]
check_parameters!(lpx, "length per x cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpy, "length per y cell is null or negative");
#[rustfmt::skip]
check_parameters!(lx, "grid length along x is null or negative");
#[rustfmt::skip]
check_parameters!(ly, "grid length along y is null or negative");
Ok((
self.map_builder,
Vertex2(self.origin[0], self.origin[1]),
[
(lx / lpx).ceil().to_usize().unwrap(),
(ly / lpy).ceil().to_usize().unwrap(),
],
[lpx, lpy],
))
}
(_, _, _) => Err(GridBuilderError::MissingGridParameters),
}
}
}
impl<T: CoordsFloat> GridBuilder<3, T> {
#[allow(clippy::type_complexity)]
pub(crate) fn parse_3d(
self,
) -> Result<(CMapBuilder<3>, Vertex3<T>, [usize; 3], [T; 3]), GridBuilderError> {
match (self.n_cells, self.len_per_cell, self.lens) {
(Some([nx, ny, nz]), Some([lpx, lpy, lpz]), lens) => {
if lens.is_some() {
eprintln!(
"W: All three grid parameters were specified, total lengths will be ignored"
);
}
#[rustfmt::skip]
check_parameters!(lpx, "length per x cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpy, "length per y cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpz, "length per z cell is null or negative");
Ok((
self.map_builder,
Vertex3(self.origin[0], self.origin[1], self.origin[2]),
[nx, ny, nz],
[lpx, lpy, lpz],
))
}
(Some([nx, ny, nz]), None, Some([lx, ly, lz])) => {
#[rustfmt::skip]
check_parameters!(lx, "grid length along x is null or negative");
#[rustfmt::skip]
check_parameters!(ly, "grid length along y is null or negative");
#[rustfmt::skip]
check_parameters!(lz, "grid length along z is null or negative");
Ok((
self.map_builder,
Vertex3(self.origin[0], self.origin[1], self.origin[2]),
[nx, ny, nz],
[
lx / T::from(nx).unwrap(),
ly / T::from(ny).unwrap(),
lz / T::from(nz).unwrap(),
],
))
}
(None, Some([lpx, lpy, lpz]), Some([lx, ly, lz])) => {
#[rustfmt::skip]
check_parameters!(lpx, "length per x cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpy, "length per y cell is null or negative");
#[rustfmt::skip]
check_parameters!(lpz, "length per z cell is null or negative");
#[rustfmt::skip]
check_parameters!(lx, "grid length along x is null or negative");
#[rustfmt::skip]
check_parameters!(ly, "grid length along y is null or negative");
#[rustfmt::skip]
check_parameters!(lz, "grid length along z is null or negative");
Ok((
self.map_builder,
Vertex3(self.origin[0], self.origin[1], self.origin[2]),
[
(lx / lpx).ceil().to_usize().unwrap(),
(ly / lpy).ceil().to_usize().unwrap(),
(lz / lpz).ceil().to_usize().unwrap(),
],
[lpx, lpy, lpz],
))
}
(_, _, _) => Err(GridBuilderError::MissingGridParameters),
}
}
}
impl<T: CoordsFloat> GridBuilder<2, T> {
#[allow(clippy::missing_panics_doc)]
#[must_use = "unused builder object"]
pub fn unit_grid(n_cells_per_axis: usize) -> CMap2<T> {
GridBuilder::default()
.n_cells([n_cells_per_axis; 2])
.len_per_cell([T::one(); 2])
.build()
.expect("E: unreachable")
}
#[allow(clippy::missing_panics_doc)]
#[must_use = "unused builder object"]
pub fn unit_triangles(n_square: usize) -> CMap2<T> {
GridBuilder::default()
.n_cells([n_square; 2])
.len_per_cell([T::one(); 2])
.split_cells(true)
.build()
.expect("E: unreachable")
}
#[allow(clippy::missing_errors_doc)]
pub fn build(self) -> Result<CMap2<T>, GridBuilderError> {
let split = self.split_cells;
self.parse_2d().map(|(builder, origin, ns, lens)| {
if split {
internals::build_2d_splitgrid(builder, origin, ns, lens)
} else {
internals::build_2d_grid(builder, origin, ns, lens)
}
})
}
}
impl<T: CoordsFloat> GridBuilder<3, T> {
#[allow(clippy::missing_panics_doc)]
pub fn hex_grid(n_cells_per_axis: usize, cell_length: T) -> CMap3<T> {
GridBuilder::default()
.n_cells([n_cells_per_axis; 3])
.len_per_cell([cell_length; 3])
.build()
.expect("E: unreachable")
}
#[allow(clippy::missing_panics_doc)]
#[must_use = "unused builder object"]
pub fn tet_grid(n_cells_per_axis: usize, cell_length: T) -> CMap3<T> {
GridBuilder::default()
.n_cells([n_cells_per_axis; 3])
.len_per_cell([cell_length; 3])
.split_cells(true)
.build()
.expect("E: unreachable")
}
#[allow(clippy::missing_errors_doc)]
pub fn build(self) -> Result<CMap3<T>, GridBuilderError> {
let split = self.split_cells;
self.parse_3d().map(|(builder, origin, ns, lens)| {
if split {
internals::build_3d_tetgrid(builder, origin, ns, lens)
} else {
internals::build_3d_grid(builder, origin, ns, lens)
}
})
}
}