//! Mapped blocks: a structured mesh of one six-sided region given by its eight corners and,
//! optionally, curved edges (circular and elliptical arcs).
//!
//! The lattice follows the transfinite interpolation shared with the structured hex mesher
//! ([`crate::quad::transfinite_faces`], Gordon and Hall 1973, for the faces;
//! [`crate::quad::transfinite_volume`], Gordon and Thiel 1982, for the interior): the edge curves
//! at their (graded) parameters, each face the Coons patch of its four edges and the volume
//! blended from the faces, both by the same lattice parameters. It reproduces every edge exactly
//! and gives the trilinear map for straight edges. A 2D patch is the Coons patch of its four
//! 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 fieldcraft_model::{ElementKind, MAX_COORD};
use crate::quad::{lattice_index, transfinite_faces, transfinite_grid_blended, transfinite_volume};
use crate::quality::{Validity, element_validity, planar_quad_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 (a patch fills the first four).
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,
},
/// The ends are kept so the curve passes through the corners exactly (not to rounding).
Ellipse {
center: P,
a: P,
b: P,
start: f64,
sweep: f64,
ends: [P; 2],
},
}
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(crate::vec3::atan2(s, 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, ends: [from, to] })
}
fn point(&self, s: f64) -> P {
match *self {
// The ends are the corners exactly (not to rounding): shared faces and the
// transfinite grids, which need equal corners, rely on it.
Path::Line { from, .. } | Path::Ellipse { ends: [from, _], .. } if s == 0.0 => from,
Path::Line { to, .. } | Path::Ellipse { ends: [_, to], .. } if s == 1.0 => to,
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, libm::cos(t), a), libm::sin(t), 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 { libm::pow(bias, 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 block's lattice of `params.len()` points per direction (node `(i, j, k)` at
/// `(k·n1 + j)·n0 + i`): the twelve edge curves at their parameters, the six faces as transfinite
/// grids of their edges ([`transfinite_grid`]) and the interior by the trivariate interpolation of
/// the faces ([`transfinite_volume`]), blended by the lattice parameters.
fn lattice_points(paths: &[Path; 12], params: &[Vec; 3]) -> Result> {
let n = [0, 1, 2].map(|d| params[d].len() - 1);
let mut x: Vec