//! Mapped blocks: a structured mesh of one six-sided region given by its eight corners and,
//! optionally, curved edges (circular and elliptical arcs).
//!
//! The interior follows the edge-based transfinite interpolation of Gordon and Hall (1973,
//! "Construction of curvilinear co-ordinate systems and applications to mesh generation",
//! Int. J. Numer. Meth. Engng 7, 461–477): the twelve edge curves blended linearly across the
//! block, minus twice the trilinear interpolation of the corners. It reproduces every edge
//! exactly, gives Coons patches on the faces and the trilinear map for straight edges.
//!
//! Elements: hex8 and hex20 per cell; tet4 and tet10 by Kuhn's split of each cell into six
//! tetrahedra along its main diagonal (conforming between the cells of a block); wedge6 by
//! splitting each cell's first-second-direction face along its diagonal and extruding along the
//! third. Quadratic elements take their mid-side nodes from the same map, so they lie on the
//! curved edges.
//!
//! Every element is checked with the certified Jacobian test ([`crate::quality`]); a block
//! whose corners are not in hex8 order or whose curves fold it is an error.
use modelcraft_model::{ElementKind, MAX_COORD};
use crate::quality::{Validity, element_validity};
use crate::vec3::{P, cross, dot, norm, sub};
use crate::{Error, Result};
/// Most elements one block makes.
pub const MAX_BLOCK_ELEMENTS: usize = 4_000_000;
/// Most divisions along one direction.
pub const MAX_DIVISIONS: usize = 100_000;
/// The corners of the block's twelve edges, grouped by direction: four along 1→2, four along
/// 1→4, four along 1→5, each from the corner where its direction's parameter is 0 to the one
/// where it is 1. Block edge numbers in [`BlockMesh::edges`] index this table.
pub const EDGES: [[usize; 2]; 12] = [[0, 1], [3, 2], [4, 5], [7, 6], [0, 3], [1, 2], [4, 7], [5, 6], [0, 4], [1, 5], [2, 6], [3, 7]];
/// Corner `c` of the block in parameter space (hex8 order).
const CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
/// The shape of one block edge between its two corners.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Curve {
Line,
/// The shorter circular arc about `center` (both corners at the same distance from it).
Arc {
center: P,
},
/// The shorter arc of the ellipse `center + a cos t + b sin t` (`a` and `b` perpendicular)
/// between the two corners, which must lie on it.
Ellipse {
center: P,
axes: [P; 2],
},
}
/// One block to mesh.
#[derive(Debug, Clone, PartialEq)]
pub struct Block {
pub kind: ElementKind,
/// The corners in hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8's side.
pub corners: [P; 8],
/// Elements along the directions 1→2, 1→4 and 1→5.
pub divisions: [usize; 3],
/// Last element size over first along each direction (geometric grading); 1 is uniform.
pub bias: [f64; 3],
/// The curve of each edge, by corner pair (0-based corners).
pub curves: Vec<([usize; 2], Curve)>,
}
/// The mesh of a block: points, elements as point indices in the model's node order, and where
/// element faces and edges lie on the block's faces and edges.
#[derive(Debug, Clone, PartialEq)]
pub struct BlockMesh {
pub points: Vec
,
pub elements: Vec>,
/// `(element, face 1-based, block face 0..6)`: block faces in hex8 face order.
pub faces: Vec<(u32, u8, u8)>,
/// `(element, edge 1-based, block edge 0..12)`: block edges as in [`EDGES`].
pub edges: Vec<(u32, u8, u8)>,
/// The point at each block corner.
pub corners: [u32; 8],
}
impl Block {
/// The block edge ([`EDGES`] index) joining two 0-based corners, either way round.
pub fn edge_index(corners: [usize; 2]) -> Option {
EDGES.iter().position(|e| *e == corners || *e == [corners[1], corners[0]])
}
}
fn add_scaled(a: P, s: f64, b: P) -> P {
[a[0] + s * b[0], a[1] + s * b[1], a[2] + s * b[2]]
}
/// An edge curve resolved against its end points: `point(s)` for `s` in `[0, 1]`.
#[derive(Debug, Clone, Copy)]
enum Path {
Line { from: P, to: P },
Ellipse { center: P, a: P, b: P, start: f64, sweep: f64 },
}
impl Path {
fn new(curve: Curve, from: P, to: P) -> Result {
let (center, a, b) = match curve {
Curve::Line => return Ok(Path::Line { from, to }),
Curve::Arc { center } => {
let (p, q) = (sub(from, center), sub(to, center));
let r = norm(p);
if !(r > 0.0) || (norm(q) - r).abs() > 1e-9 * r.max(norm(q)) {
return Err(Error::Invalid(format!(
"an arc's corners {from:?} and {to:?} must be at the same distance from its centre {center:?}"
)));
}
let n = cross(p, q);
if !(norm(n) > 1e-12 * r * r) {
return Err(Error::Invalid(format!(
"the arc from {from:?} to {to:?} about {center:?} has no plane (it spans 0 or 180 degrees); split it"
)));
}
let b = cross(n, p);
(center, p, [b[0] * r / norm(b), b[1] * r / norm(b), b[2] * r / norm(b)])
}
Curve::Ellipse { center, axes: [a, b] } => (center, a, b),
};
let (la, lb) = (norm(a), norm(b));
if !(la > 0.0 && lb > 0.0) || dot(a, b).abs() > 1e-9 * la * lb {
return Err(Error::Invalid("an ellipse's two axes must be non-zero and perpendicular".into()));
}
let angle = |p: P| -> Result {
let d = sub(p, center);
let (c, s) = (dot(d, a) / (la * la), dot(d, b) / (lb * lb));
let back = add_scaled(add_scaled(center, c, a), s, b);
if norm(sub(back, p)) > 1e-9 * la.max(lb) || ((c * c + s * s).sqrt() - 1.0).abs() > 1e-9 {
return Err(Error::Invalid(format!("corner {p:?} is not on the edge's ellipse or arc about {center:?}")));
}
Ok(s.atan2(c))
};
let start = angle(from)?;
let mut sweep = angle(to)? - start;
if sweep > std::f64::consts::PI {
sweep -= 2.0 * std::f64::consts::PI;
} else if sweep <= -std::f64::consts::PI {
sweep += 2.0 * std::f64::consts::PI;
}
if !(sweep.abs() > 1e-12) || std::f64::consts::PI - sweep.abs() < 1e-9 {
return Err(Error::Invalid(format!("the arc from {from:?} to {to:?} spans 0 or 180 degrees; split it")));
}
Ok(Path::Ellipse { center, a, b, start, sweep })
}
fn point(&self, s: f64) -> P {
match *self {
Path::Line { from, to } => add_scaled(from, s, sub(to, from)),
Path::Ellipse { center, a, b, start, sweep } => {
let t = start + s * sweep;
add_scaled(add_scaled(center, t.cos(), a), t.sin(), b)
}
}
}
}
/// The parameters of `n` elements along one direction with last/first size ratio `bias`, at
/// `order` points per element (`order · n + 1` values from 0 to 1; mid-side values halfway).
fn parameters(n: usize, bias: f64, order: usize) -> Vec {
let q = if n > 1 { bias.powf(1.0 / (n - 1) as f64) } else { 1.0 };
let mut ends = Vec::with_capacity(n + 1);
let (mut sum, mut size) = (0.0, 1.0);
ends.push(0.0);
for _ in 0..n {
sum += size;
ends.push(sum);
size *= q;
}
let ends: Vec = ends.iter().map(|e| e / sum).collect();
let mut out = Vec::with_capacity(order * n + 1);
for w in ends.windows(2) {
let (a, b) = (w[0], w[1]);
out.push(a);
if order == 2 {
out.push(0.5 * (a + b));
}
}
out.push(1.0);
out
}
/// The map from the unit cube onto the block, on the lattice of `params`: each edge curve is
/// evaluated once per lattice parameter along its direction.
struct Map<'a> {
corners: [P; 8],
params: &'a [Vec; 3],
/// `edges[k][i]`: edge `k` at the `i`-th parameter of its direction (`k / 4`).
edges: [Vec
; 12],
}
impl<'a> Map<'a> {
fn new(corners: [P; 8], paths: &[Path; 12], params: &'a [Vec; 3]) -> Map<'a> {
let edges = std::array::from_fn(|k| params[k / 4].iter().map(|&s| paths[k].point(s)).collect());
Map { corners, params, edges }
}
/// The point at lattice coordinates `[i, j, k]`.
fn point(&self, at: [usize; 3]) -> P {
let [u, v, w] = [0, 1, 2].map(|d| self.params[d].get(at[d]).copied().unwrap_or(0.0));
let e = |k: usize| self.edges[k].get(at[k / 4]).copied().unwrap_or([f64::NAN; 3]);
let terms: [(f64, P); 12] = [
((1.0 - v) * (1.0 - w), e(0)),
(v * (1.0 - w), e(1)),
((1.0 - v) * w, e(2)),
(v * w, e(3)),
((1.0 - u) * (1.0 - w), e(4)),
(u * (1.0 - w), e(5)),
((1.0 - u) * w, e(6)),
(u * w, e(7)),
((1.0 - u) * (1.0 - v), e(8)),
(u * (1.0 - v), e(9)),
(u * v, e(10)),
((1.0 - u) * v, e(11)),
];
let mut x = [0.0; 3];
for (f, p) in terms {
x = add_scaled(x, f, p);
}
for (c, at) in self.corners.iter().zip(CORNERS) {
let f = |bit: usize, t: f64| if bit == 1 { t } else { 1.0 - t };
x = add_scaled(x, -2.0 * f(at[0], u) * f(at[1], v) * f(at[2], w), *c);
}
x
}
}
/// Lattice offsets of each element of one cell, in units of the lattice (two per cell for
/// quadratic kinds), in the model's node order.
fn cell_elements(kind: ElementKind) -> Vec> {
// Corners in units of the lattice, then (quadratic kinds) a mid-side node on each edge in
// the kind's edge order.
let nodes = |corners: &[[usize; 3]]| -> Vec<[usize; 3]> {
if !kind.is_quadratic() {
return corners.to_vec();
}
let mut out: Vec<[usize; 3]> = corners.iter().map(|c| c.map(|x| 2 * x)).collect();
for edge in kind.edges() {
if let (Some(a), Some(b)) = (edge.first().and_then(|&i| corners.get(i)), edge.get(1).and_then(|&i| corners.get(i))) {
out.push([a[0] + b[0], a[1] + b[1], a[2] + b[2]]);
}
}
out
};
match kind {
ElementKind::Hex8 | ElementKind::Hex20 => vec![nodes(&CORNERS)],
ElementKind::Tet4 | ElementKind::Tet10 => {
[[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
.iter()
.map(|perm| {
let mut v = [[0usize; 3]; 4];
for s in 0..3 {
v[s + 1] = v[s];
v[s + 1][perm[s]] = 1;
}
// Even permutations are positively oriented; swap two nodes of the others.
let odd = matches!(perm, [0, 2, 1] | [1, 0, 2] | [2, 1, 0]);
if odd {
v.swap(1, 2);
}
nodes(&v)
})
.collect()
}
ElementKind::Wedge6 => vec![
vec![[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1]],
vec![[0, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 1, 1], [0, 1, 1]],
],
_ => Vec::new(),
}
}
/// Mesh one block.
pub fn block(b: &Block) -> Result {
let kind = b.kind;
if !matches!(kind, ElementKind::Hex8 | ElementKind::Hex20 | ElementKind::Tet4 | ElementKind::Tet10 | ElementKind::Wedge6) {
return Err(Error::Invalid(format!("a block is meshed with hex8, hex20, tet4, tet10 or wedge6 elements, not {}", kind.name())));
}
if b.corners.iter().flatten().any(|c| !c.is_finite() || c.abs() > MAX_COORD) {
return Err(Error::Invalid(format!("block corners must be finite and within ±{MAX_COORD}")));
}
if b.divisions.iter().any(|&n| n == 0 || n > MAX_DIVISIONS) {
return Err(Error::Invalid(format!("divisions must be 1 to {MAX_DIVISIONS} in each direction")));
}
if b.bias.iter().any(|&r| !(r.is_finite() && (1e-6..=1e6).contains(&r))) {
return Err(Error::Invalid("bias must be between 1e-6 and 1e6 (1 is uniform)".into()));
}
let per_cell = cell_elements(kind).len();
let cells = b.divisions.iter().try_fold(1usize, |acc, &n| acc.checked_mul(n));
let count = cells.and_then(|c| c.checked_mul(per_cell)).filter(|&n| n <= MAX_BLOCK_ELEMENTS).ok_or_else(|| {
Error::Limit(format!("a block makes at most {MAX_BLOCK_ELEMENTS} elements; {:?} divisions of {} are more", b.divisions, kind.name()))
})?;
let mut curves: [Option; 12] = [None; 12];
for (pair, curve) in &b.curves {
let k = Block::edge_index(*pair)
.ok_or_else(|| Error::Invalid(format!("corners {} and {} are not joined by a block edge", pair[0] + 1, pair[1] + 1)))?;
if curves[k].replace(*curve).is_some() {
return Err(Error::Invalid(format!("the edge from corner {} to {} is given twice", pair[0] + 1, pair[1] + 1)));
}
}
let mut paths = [Path::Line { from: [0.0; 3], to: [0.0; 3] }; 12];
for (k, [from, to]) in EDGES.iter().enumerate() {
paths[k] = Path::new(curves[k].unwrap_or(Curve::Line), b.corners[*from], b.corners[*to])?;
}
let order = if kind.is_quadratic() { 2 } else { 1 };
let params: [Vec; 3] = [0, 1, 2].map(|d| parameters(b.divisions[d], b.bias[d], order));
let map = Map::new(b.corners, &paths, ¶ms);
let size = [0, 1, 2].map(|d| params[d].len());
let index = |c: [usize; 3]| c[0] + size[0] * (c[1] + size[1] * c[2]);
// Elements as lattice coordinates; then number the lattice points they use, in lattice order.
let pattern = cell_elements(kind);
let mut lattice: Vec> = Vec::with_capacity(count);
for k in 0..b.divisions[2] {
for j in 0..b.divisions[1] {
for i in 0..b.divisions[0] {
let base = [i * order, j * order, k * order];
for e in &pattern {
lattice.push(e.iter().map(|o| [base[0] + o[0], base[1] + o[1], base[2] + o[2]]).collect());
}
}
}
}
let mut used = vec![false; size.iter().product::()];
for c in lattice.iter().flatten() {
if let Some(slot) = used.get_mut(index(*c)) {
*slot = true;
}
}
let mut number: Vec