hex: conforming multi-block meshes of partitioned parts

hex_mesh_cells meshes cells that meet face to face (SolidCraft split
bodies) as one conforming all-hex mesh: each cell a mapped block after
smoothly meeting faces are joined (virtual topology: the halves of a hole,
a plane running into a fillet), vertices, edges and faces shared by
identity, interval assignment over all cells, each shared face meshed
once, radial edges graded off curved faces. finish spans several cells:
tags per cell, hex20 mid-edge nodes shared across cells, a closed
2-manifold boundary and no free internal face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 04:56:46 -04:00
1 parent 8ca2dddeec
commit b6ce7aee69
8 files changed
+1439 -81

No files matched your search

+796
View File
@@ -0,0 +1,796 @@
//! Conforming structured hex meshes of a part partitioned into cells (multi-block meshing).
//!
//! An analyst meshes a part no single sweep fits by splitting it into cells that each are a
//! logical cube (six four-sided faces, possibly composite), meshing every cell as a mapped block
//! and joining the blocks so that they conform: the multi-block grids of J. F. Thompson, B. K.
//! Soni, N. P. Weatherill (eds.), *Handbook of Grid Generation*, CRC Press 1999, ch. 3 and 13.
//! [`hex_mesh_cells`] does it in five steps:
//!
//! 1. **Topology.** Each cell's B-rep is read with smoothly meeting faces joined
//! ([`super::brep::Brep::composite`]: the halves of a hole, a plane running into a fillet) and
//! recognized as a logical cube ([`super::mapped::recognize`]). Corners of different cells at
//! the same point (within the merge tolerance, the one geometric decision) are one vertex;
//! cell edges between the same two vertices along the same curve are one edge; cell faces
//! bounded by the same four edges are one face, shared by at most two cells (an interface).
//! Everything after this works on these identities, never on coordinates.
//! 2. **Interval assignment** over the edges of all cells together ([`super::interval`]): the
//! four edges along each direction of every cell are equal, so a count is the same on both
//! sides of every interface. Edges that run away from a curved face (radial lines off a hole,
//! the diagonal off a fillet) are then graded: the spacing at that end matches the spacing
//! along the curved face there and grows by at most [`GROWTH`] per element towards the target
//! size; their counts are reassigned with the grading's goals.
//! 3. **Edges** are divided once, on the geometry, and their nodes belong to the edge.
//! 4. **Faces** get one transfinite grid each (Gordon and Hall), from their four edges and
//! projected onto the face; an interface's grid is made once and both cells use its nodes.
//! 5. **Cells** fill their interior by trivariate transfinite interpolation (Gordon and Thiel)
//! of their six face grids, blended by the edges' arc-length distribution.
//!
//! Nodes are merged by identity (a vertex, an edge's k-th node, a face grid's node), so the
//! cells conform exactly: no node is duplicated on an interface and none hangs. The result goes
//! through the same finishing as a single body ([`super::finish`]): the certified validity gate,
//! the tiling and fidelity checks, a closed 2-manifold boundary, and no boundary face lying on
//! another cell (a free internal face, where cells touch without sharing a face).
use std::collections::BTreeMap;
use super::brep::Brep;
use super::geom::{TriangleTree, add, dist, scale};
use super::mapped::{self, Cube, face_ring, other};
use super::{Cells, Ctx, HexMesh, HexMethod, HexOptions, RELATIVE_TOLERANCE, Raw, Surface, finish, project_to_face, project_to_faces};
use crate::quad::{lattice_index, transfinite_grid, transfinite_volume};
use crate::size::{SizeField, divide_polyline, ideal_divisions};
use crate::surface::BoundarySurface;
use crate::vec3::{P, cross, dot, norm, sub};
use crate::{Error, Result};
/// One cell: its name (for messages), its boundary surface (face-tagged) and the analytic
/// surfaces of its faces by tag.
#[derive(Clone, Copy)]
pub struct Cell<'a> {
pub name: &'a str,
pub surface: &'a BoundarySurface,
pub faces: &'a [Option<Surface>],
}
/// A conforming mesh of several cells.
#[derive(Debug, Clone, PartialEq)]
pub struct CellsMesh {
/// The mesh; boundary faces are tagged with the B-rep faces of their element's cell.
pub mesh: HexMesh,
/// The cell of each element (index into the cells given).
pub element_cell: Vec<u32>,
/// Each cell's block: divisions along its three logical directions.
pub blocks: Vec<[usize; 3]>,
/// Faces shared by two cells.
pub interfaces: usize,
}
/// The most cells meshed together.
pub const MAX_CELLS: usize = 4096;
/// Spacing growth from one element to the next along a graded edge (1.2: each at most 20 %
/// longer than the one before).
pub const GROWTH: f64 = 1.2;
/// Segments a graded edge's polyline is cut into for the integration of its grading (the size
/// helpers take the size constant along a sample).
const GRADED_SEGMENTS: usize = 64;
/// An edge shared by cells: its end vertices (`a < b`), the cell that divides it and how.
struct Edge {
a: usize,
b: usize,
/// The owning cell, its edge and whether that edge runs from `a` to `b`.
owner: (usize, usize, bool),
length: f64,
}
/// A face shared by one or two cells.
struct Face {
/// The owner's ring of vertices (the order of [`face_ring`]).
ring: [usize; 4],
/// The cells, with the direction and side of the face in each; the first owns the grid.
uses: Vec<(usize, usize, usize)>,
}
/// Mesh `cells` (bodies that together make the part, meeting face to face) into one conforming
/// all-hex mesh, every cell a mapped block. `opts.method` must be [`HexMethod::Auto`] or
/// [`HexMethod::Mapped`]; a cell that is not a logical cube, or cells that meet without sharing
/// whole faces, are [`Error::Unsupported`] with the reason.
pub fn hex_mesh_cells(cells: &[Cell], field: &dyn SizeField, opts: &HexOptions) -> Result<CellsMesh> {
if !matches!(opts.method, HexMethod::Auto | HexMethod::Mapped) {
return Err(Error::Invalid(format!("conforming cells are meshed as mapped blocks; method `{}` does not apply", opts.method.name())));
}
if !(opts.chordal_tolerance >= 0.0 && opts.chordal_tolerance.is_finite()) {
return Err(Error::Invalid(format!("the chordal tolerance must be a non-negative number, got {}", opts.chordal_tolerance)));
}
if cells.is_empty() || cells.len() > MAX_CELLS {
return Err(Error::Invalid(format!("{} cells; 1 to {MAX_CELLS} are meshed together", cells.len())));
}
if let Some(c) = cells.iter().find(|c| c.surface.shells().len() != 1) {
return Err(Error::Unsupported(format!("cell `{}` has {} shells; a cell is one solid without voids", c.name, c.surface.shells().len())));
}
let surfaces: Vec<&BoundarySurface> = cells.iter().map(|c| c.surface).collect();
super::check_estimate(&surfaces, field, opts)?;
let breps: Vec<Brep> = cells.iter().map(|c| Brep::composite(c.surface, c.faces)).collect::<Result<_>>()?;
let trees: Vec<TriangleTree> = cells.iter().map(|c| TriangleTree::new(c.surface)).collect::<Result<_>>()?;
let ctxs: Vec<Ctx> =
breps.iter().zip(&trees).map(|(brep, tree)| Ctx { brep, tree, field, opts, tol: RELATIVE_TOLERANCE * brep.diagonal }).collect();
let names: Vec<&str> = cells.iter().map(|c| c.name).collect();
let cubes: Vec<Cube> = breps
.iter()
.zip(&names)
.map(|(b, name)| mapped::recognize(b).map_err(|e| Error::Unsupported(format!("cell `{name}`: {}", super::plain(&e)))))
.collect::<Result<_>>()?;
let topo = Topology::new(&breps, &cubes, &names, opts.chordal_tolerance)?;
let divisions = topo.intervals(&ctxs, &cubes, field)?;
let mut builder =
Builder { topo: &topo, ctxs: &ctxs, cubes: &cubes, divisions: &divisions, field, opts, nodes: Vec::new(), edge_nodes: Vec::new() };
let (raw, element_cell, blocks) = builder.build()?;
let shared = topo.interfaces();
let mesh = finish(&Cells { ctxs: &ctxs, of: &element_cell, names: &names }, raw, HexMethod::Mapped)?;
Ok(CellsMesh { mesh, element_cell, blocks, interfaces: shared })
}
/// Vertices, edges and faces shared by the cells.
struct Topology {
/// Shared vertices: where they are.
vertices: Vec<P>,
edges: Vec<Edge>,
faces: Vec<Face>,
/// Per cell: the vertex at each corner, `corner[i][j][k]`.
corners: Vec<[[[usize; 2]; 2]; 2]>,
/// Per cell, per direction: the four edges (in the order of [`Cube::groups`]).
cell_edges: Vec<[[usize; 4]; 3]>,
/// Per cell: the face on each side of each direction.
cell_faces: Vec<[[usize; 2]; 3]>,
}
/// Points at most this far apart (relative to the diagonal of all cells) are one vertex.
const RELATIVE_MERGE: f64 = 1e-6;
impl Topology {
fn new(breps: &[Brep], cubes: &[Cube], names: &[&str], chordal: f64) -> Result<Topology> {
let name = |c: usize| names.get(c).copied().unwrap_or("?");
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
for b in breps {
for p in b.surface.points() {
for k in 0..3 {
lo[k] = lo[k].min(p[k]);
hi[k] = hi[k].max(p[k]);
}
}
}
let merge = RELATIVE_MERGE * dist(lo, hi);
if !(merge > 0.0 && merge.is_finite()) {
return Err(Error::Invalid("the cells have no extent".into()));
}
// Vertices: corners within `merge` of each other, found through a grid of that spacing.
let mut vertices: Vec<P> = Vec::new();
let mut grid: BTreeMap<[i64; 3], Vec<usize>> = BTreeMap::new();
let cellkey = |p: P| p.map(|x| (x / merge).floor() as i64);
let mut corners = Vec::with_capacity(cubes.len());
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
let mut ids = [[[0usize; 2]; 2]; 2];
let mut seen: Vec<usize> = Vec::with_capacity(8);
for (i, plane) in ids.iter_mut().enumerate() {
for (j, row) in plane.iter_mut().enumerate() {
for (k, slot) in row.iter_mut().enumerate() {
let p = brep.point(cube.at(i, j, k));
let key = cellkey(p);
let mut found: Option<usize> = None;
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
let near = [key[0] + dx, key[1] + dy, key[2] + dz];
for &v in grid.get(&near).map(Vec::as_slice).unwrap_or(&[]) {
if vertices.get(v).is_some_and(|&x| dist(x, p) <= merge) {
if found.is_some_and(|f| f != v) {
return Err(Error::Unsupported(format!(
"cell `{}`: a corner at {p:?} is within the merge distance of two vertices",
name(c)
)));
}
found = Some(v);
}
}
}
}
}
let v = match found {
Some(v) => v,
None => {
vertices.push(p);
grid.entry(key).or_default().push(vertices.len() - 1);
vertices.len() - 1
}
};
if seen.contains(&v) {
return Err(Error::Unsupported(format!("cell `{}`: two corners at the same point {p:?}", name(c))));
}
seen.push(v);
*slot = v;
}
}
}
corners.push(ids);
}
// Edges: the same two vertices and the same curve (midpoints within the merge distance
// and the tessellation's chordal deviation).
let edge_tol = merge + 4.0 * chordal;
let mut edges: Vec<Edge> = Vec::new();
let mut by_ends: BTreeMap<(usize, usize), Vec<(usize, P)>> = BTreeMap::new();
let mut cell_edges = Vec::with_capacity(cubes.len());
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
let ids = corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
let mut out = [[0usize; 4]; 3];
for (d, row) in out.iter_mut().enumerate() {
for (m, slot) in row.iter_mut().enumerate() {
let (u, v) = edge_ends(&ids, d, m);
let (e, fwd) = cube.groups.get(d).and_then(|g| g.get(m)).copied().ok_or_else(|| Error::Failed("cube groups".into()))?;
let pts = brep.edge_points(e, fwd);
let (mid, length) = midpoint(&pts);
let (a, b) = (u.min(v), u.max(v));
let list = by_ends.entry((a, b)).or_default();
let id = match list.iter().find(|(_, q)| dist(*q, mid) <= edge_tol) {
Some(&(id, _)) => id,
None => {
edges.push(Edge { a, b, owner: (c, e, fwd == (u == a)), length });
list.push((edges.len() - 1, mid));
edges.len() - 1
}
};
*slot = id;
}
}
cell_edges.push(out);
}
// Faces: the same four edges, on the same surface, with the cells on opposite sides.
let mut faces: Vec<Face> = Vec::new();
let mut by_edges: BTreeMap<[usize; 4], usize> = BTreeMap::new();
let mut cell_faces = Vec::with_capacity(cubes.len());
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
let ids = corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
let mut out = [[0usize; 2]; 3];
for (d, row) in out.iter_mut().enumerate() {
for (side, slot) in row.iter_mut().enumerate() {
let ring = face_ring(d, side).map(|[i, j, k]| ids[i][j][k]);
let mut key = [0usize; 4];
for (m, k) in key.iter_mut().enumerate() {
let (u, v) = (ring[m], ring[(m + 1) % 4]);
let groups = cell_edges.get(c).ok_or_else(|| Error::Failed("cell edges".into()))?;
*k = edge_between(&edges, groups, u, v).ok_or_else(|| Error::Failed("a face side is no cell edge".into()))?;
}
key.sort_unstable();
let face = cube.faces.get(d).and_then(|r| r.get(side)).copied().ok_or_else(|| Error::Failed("cube faces".into()))?;
let id = match by_edges.get(&key) {
Some(&id) => {
let f = faces.get_mut(id).ok_or_else(|| Error::Failed("face index".into()))?;
if f.uses.len() >= 2 {
return Err(Error::Unsupported(format!("cell `{}`: a face is shared by more than two cells", name(c))));
}
if let Some(&(c0, d0, s0)) = f.uses.first() {
if c0 == c {
return Err(Error::Unsupported(format!("cell `{}`: two faces of the cell have the same four edges", name(c))));
}
let f0 = cubes.get(c0).and_then(|q| q.faces.get(d0)).and_then(|r| r.get(s0)).copied().unwrap_or(0);
let b0 = breps.get(c0).ok_or_else(|| Error::Failed("cell index".into()))?;
let (centre0, area0) = face_centre(b0, f0);
let (centre1, area1) = face_centre(brep, face);
if dist(centre0, centre1) > edge_tol + 1e-3 * norm(area0).sqrt() || dot(area0, area1) >= 0.0 {
return Err(Error::Unsupported(format!(
"cells `{}` and `{}` share the four edges of a face but not the face (different surfaces, or on the same side)",
name(c0),
name(c)
)));
}
}
f.uses.push((c, d, side));
id
}
None => {
faces.push(Face { ring, uses: vec![(c, d, side)] });
by_edges.insert(key, faces.len() - 1);
faces.len() - 1
}
};
*slot = id;
}
}
cell_faces.push(out);
}
Ok(Topology { vertices, edges, faces, corners, cell_edges, cell_faces })
}
/// Faces shared by two cells.
fn interfaces(&self) -> usize {
self.faces.iter().filter(|f| f.uses.len() == 2).count()
}
/// The edge between vertices `u` and `v` among cell `c`'s edges, and whether it runs from
/// `u` to `v`.
fn edge(&self, c: usize, u: usize, v: usize) -> Option<(usize, bool)> {
let e = edge_between(&self.edges, self.cell_edges.get(c)?, u, v)?;
Some((e, self.edges.get(e)?.a == u))
}
/// How every edge is divided, all cells together; graded edges get the counts their
/// grading asks for.
fn intervals(&self, ctxs: &[Ctx], cubes: &[Cube], field: &dyn SizeField) -> Result<Vec<Division>> {
let polylines: Vec<Vec<P>> = self.edges.iter().map(|e| self.polyline(ctxs, e)).collect::<Result<_>>()?;
let goals: Vec<f64> = polylines.iter().map(|p| ideal_divisions(field, p)).collect::<Result<_>>()?;
let first = self.solve(&goals)?;
// Ends of edges that leave a curved face: the spacing along that face there.
let ends = self.graded_ends(ctxs, cubes, &first);
let mut graded = goals;
let mut divisions = Vec::with_capacity(polylines.len());
for ((poly, ends), goal) in polylines.into_iter().zip(ends).zip(graded.iter_mut()) {
let poly = if ends.iter().any(Option::is_some) {
let dense = densify(&poly);
*goal = goal.max(ideal_divisions(&Grading { field, ends }, &dense)?);
dense
} else {
poly
};
divisions.push(Division { count: 1, ends, polyline: poly });
}
for (d, n) in divisions.iter_mut().zip(self.solve(&graded)?) {
d.count = n;
}
Ok(divisions)
}
fn solve(&self, goals: &[f64]) -> Result<Vec<usize>> {
let mut ia = super::interval::Intervals::default();
for &g in goals {
ia.var(g, 1);
}
for groups in &self.cell_edges {
for g in groups {
for &e in g.iter().skip(1) {
ia.equal.push((vec![g[0]], vec![e]));
}
}
}
ia.solve()
}
/// For every edge, at each end (`a`, `b`): the point and the spacing to start from when the
/// edge leaves a curved face there (ends on it without lying on it) — the smallest spacing,
/// by `counts`, of that face's edges at the end — or `None`.
fn graded_ends(&self, ctxs: &[Ctx], cubes: &[Cube], counts: &[usize]) -> Vec<GradedEnds> {
// The edges of every curved face, by the vertices they meet.
let mut curved: Vec<[usize; 4]> = Vec::new();
let mut curved_at: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for f in &self.faces {
let Some(&(c, d, side)) = f.uses.first() else { continue };
let planar = cubes
.get(c)
.and_then(|q| q.faces.get(d))
.and_then(|r| r.get(side))
.and_then(|&bf| ctxs.get(c).and_then(|x| x.brep.faces.get(bf)))
.is_none_or(|bf| bf.plane.is_some());
if planar {
continue;
}
let mut es = [usize::MAX; 4];
for (m, slot) in es.iter_mut().enumerate() {
if let Some((e, _)) = self.edge(c, f.ring[m], f.ring[(m + 1) % 4]) {
*slot = e;
}
}
for &v in &f.ring {
curved_at.entry(v).or_default().push(curved.len());
}
curved.push(es);
}
let spacing = |e: usize| -> f64 { self.edges.get(e).map_or(f64::INFINITY, |x| x.length / counts.get(e).copied().unwrap_or(1).max(1) as f64) };
self.edges
.iter()
.enumerate()
.map(|(e, edge)| {
[edge.a, edge.b].map(|v| {
let mut best: Option<f64> = None;
for face in curved_at.get(&v).map(Vec::as_slice).unwrap_or(&[]).iter().filter_map(|&k| curved.get(k)) {
if face.contains(&e) {
continue;
}
for &o in face {
if self.edges.get(o).is_some_and(|x| x.a == v || x.b == v) {
let s = spacing(o);
best = Some(best.map_or(s, |b| b.min(s)));
}
}
}
best.and_then(|s| self.vertices.get(v).map(|&x| (x, s)))
})
})
.collect()
}
/// The owner's polyline of an edge, from `a` to `b`.
fn polyline(&self, ctxs: &[Ctx], e: &Edge) -> Result<Vec<P>> {
let (c, be, fwd) = e.owner;
let ctx = ctxs.get(c).ok_or_else(|| Error::Failed("edge owner".into()))?;
let mut pts = ctx.brep.edge_points(be, fwd);
// The ends are the shared vertices.
if let (Some(first), Some(a)) = (pts.first_mut(), self.vertices.get(e.a)) {
*first = *a;
}
if let (Some(last), Some(b)) = (pts.last_mut(), self.vertices.get(e.b)) {
*last = *b;
}
if pts.len() < 2 {
return Err(Error::Failed("an edge without points".into()));
}
Ok(pts)
}
}
/// The edge among a cell's edges (`groups`) between vertices `u` and `v`.
fn edge_between(edges: &[Edge], groups: &[[usize; 4]; 3], u: usize, v: usize) -> Option<usize> {
let key = (u.min(v), u.max(v));
groups.iter().flatten().copied().find(|&e| edges.get(e).is_some_and(|x| (x.a, x.b) == key))
}
/// The corners (low, high) of edge `m` of direction `d` of a cell whose corners are `ids`, in
/// the order of [`Cube::groups`].
fn edge_ends(ids: &[[[usize; 2]; 2]; 2], d: usize, m: usize) -> (usize, usize) {
let (s, t) = ((m / 2) & 1, m & 1);
match d {
0 => (ids[0][s][t], ids[1][s][t]),
1 => (ids[s][0][t], ids[s][1][t]),
_ => (ids[s][t][0], ids[s][t][1]),
}
}
/// The point halfway along a polyline (by arc length) and its length.
fn midpoint(pts: &[P]) -> (P, f64) {
let length: f64 = pts.windows(2).map(|w| dist(w[0], w[1])).sum();
let mut walked = 0.0;
for w in pts.windows(2) {
let l = dist(w[0], w[1]);
if walked + l >= 0.5 * length && l > 0.0 {
let t = (0.5 * length - walked) / l;
return (add(w[0], scale(sub(w[1], w[0]), t)), length);
}
walked += l;
}
(pts.first().copied().unwrap_or([0.0; 3]), length)
}
/// A B-rep face's area-weighted centre and its area vector (outward).
fn face_centre(brep: &Brep, f: usize) -> (P, P) {
let (mut c, mut n, mut total) = ([0.0; 3], [0.0; 3], 0.0);
for &t in brep.faces.get(f).map(|x| x.triangles.as_slice()).unwrap_or(&[]) {
let Some(tri) = brep.surface.triangles().get(t as usize) else { continue };
let [a, b, d] = tri.map(|v| brep.point(v));
let area = cross(sub(b, a), sub(d, a));
let w = 0.5 * norm(area);
c = add(c, scale(add(add(a, b), d), w / 3.0));
n = add(n, scale(area, 0.5));
total += w;
}
(if total > 0.0 { scale(c, 1.0 / total) } else { c }, n)
}
/// A polyline cut into at least [`GRADED_SEGMENTS`] pieces (for the integration of a grading).
fn densify(pts: &[P]) -> Vec<P> {
let length: f64 = pts.windows(2).map(|w| dist(w[0], w[1])).sum();
let step = length / GRADED_SEGMENTS as f64;
let mut out = Vec::with_capacity(pts.len() + GRADED_SEGMENTS);
for w in pts.windows(2) {
let l = dist(w[0], w[1]);
let k = if step > 0.0 { ((l / step).ceil() as usize).clamp(1, GRADED_SEGMENTS) } else { 1 };
for s in 0..k {
out.push(add(w[0], scale(sub(w[1], w[0]), s as f64 / k as f64)));
}
}
if let Some(&last) = pts.last() {
out.push(last);
}
out
}
/// Each end of an edge (`a`, `b`): where grading starts from and the spacing there.
type GradedEnds = [Option<(P, f64)>; 2];
/// How an edge is divided: its count, its graded ends and the polyline (from `a` to `b`; cut
/// finer when graded) it is divided along.
struct Division {
count: usize,
ends: GradedEnds,
polyline: Vec<P>,
}
/// The size along a graded edge: the field's, capped near each graded end by the spacing there
/// growing by [`GROWTH`] per element (linearly with the distance from that end).
struct Grading<'a> {
field: &'a dyn SizeField,
ends: GradedEnds,
}
impl SizeField for Grading<'_> {
fn size(&self, p: [f64; 3]) -> f64 {
let mut h = self.field.size(p);
for (q, s) in self.ends.iter().flatten() {
h = h.min(s + (GROWTH - 1.0) * dist(p, *q));
}
h
}
}
/// The nodes of a cell's lattice, or of a face's grid, by identity.
struct Builder<'t, 'c, 'a> {
topo: &'t Topology,
ctxs: &'c [Ctx<'a>],
cubes: &'c [Cube],
divisions: &'c [Division],
field: &'c dyn SizeField,
opts: &'c HexOptions,
nodes: Vec<P>,
/// Per edge: its node ids from `a` to `b`.
edge_nodes: Vec<Vec<u32>>,
}
impl Builder<'_, '_, '_> {
fn count(&self, e: usize) -> usize {
self.divisions.get(e).map_or(1, |d| d.count)
}
fn push(&mut self, p: P) -> Result<u32> {
let id = u32::try_from(self.nodes.len()).map_err(|_| Error::Limit("too many nodes".into()))?;
self.nodes.push(p);
Ok(id)
}
fn build(&mut self) -> Result<(Raw, Vec<u32>, Vec<[usize; 3]>)> {
let (topo, field, max_elements) = (self.topo, self.field, self.opts.max_elements);
// Block sizes and the element budget before any node is made.
let mut blocks = Vec::with_capacity(topo.cell_edges.len());
let mut total = 0usize;
for groups in &topo.cell_edges {
let n = groups.map(|g| self.count(g[0]));
let nodes = (n[0] + 1).checked_mul(n[1] + 1).and_then(|x| x.checked_mul(n[2] + 1)).filter(|&x| x <= mapped::MAX_NODES);
if nodes.is_none() {
return Err(Error::Limit(format!("a {} × {} × {} block exceeds {} nodes", n[0], n[1], n[2], mapped::MAX_NODES)));
}
total = total.saturating_add(n[0] * n[1] * n[2]);
blocks.push(n);
}
if total > max_elements {
return Err(Error::Limit(format!("the mesh would have {total} hexahedra; the limit is {max_elements}")));
}
// Vertices.
for &v in &topo.vertices {
self.push(v)?;
}
// Edges: divided once, on the geometry.
for (edge, division) in topo.edges.iter().zip(self.divisions) {
let n = division.count;
let pts = if division.ends.iter().any(Option::is_some) {
divide_polyline(&Grading { field, ends: division.ends }, &division.polyline, n)?
} else {
divide_polyline(field, &division.polyline, n)?
};
let (c, be, _) = edge.owner;
let ctx = self.ctxs.get(c).ok_or_else(|| Error::Failed("edge owner".into()))?;
let faces = ctx.brep.edges.get(be).map(|x| x.faces.to_vec()).unwrap_or_default();
let mut ids = Vec::with_capacity(n + 1);
ids.push(edge.a as u32);
for q in pts.iter().take(n).skip(1) {
let p = project_to_faces(ctx, &faces, *q);
ids.push(self.push(p)?);
}
ids.push(edge.b as u32);
self.edge_nodes.push(ids);
}
// Faces: one grid each, from the owner's side.
let mut face_grids: Vec<FaceGrid> = Vec::with_capacity(topo.faces.len());
for face in &topo.faces {
face_grids.push(self.face_grid(face)?);
}
// Cells.
let mut hexes: Vec<[u32; 8]> = Vec::with_capacity(total);
let mut element_cell: Vec<u32> = Vec::with_capacity(total);
for (c, &n) in blocks.iter().enumerate() {
let ids = self.cell_lattice(c, n, &face_grids)?;
let [ni, nj, nk] = n;
let id = |i: usize, j: usize, k: usize| ids.get(lattice_index(n, [i, j, k])).copied().unwrap_or(0);
for k in 0..nk {
for j in 0..nj {
for i in 0..ni {
hexes.push([
id(i, j, k),
id(i + 1, j, k),
id(i + 1, j + 1, k),
id(i, j + 1, k),
id(i, j, k + 1),
id(i + 1, j, k + 1),
id(i + 1, j + 1, k + 1),
id(i, j + 1, k + 1),
]);
element_cell.push(c as u32);
}
}
}
}
let shared = topo.interfaces();
let description = format!(
"{} cell{} as conforming mapped blocks, {} shared face{}",
blocks.len(),
if blocks.len() == 1 { "" } else { "s" },
shared,
if shared == 1 { "" } else { "s" }
);
let nodes = std::mem::take(&mut self.nodes);
Ok((Raw { nodes, hexes, description }, element_cell, blocks))
}
/// The nodes of edge `e` from vertex `u`.
fn edge_from(&self, e: usize, u: usize) -> Result<Vec<u32>> {
let edge = self.topo.edges.get(e).ok_or_else(|| Error::Failed("edge index".into()))?;
let mut ids = self.edge_nodes.get(e).cloned().ok_or_else(|| Error::Failed("edge nodes".into()))?;
if edge.b == u {
ids.reverse();
} else if edge.a != u {
return Err(Error::Failed("an edge does not start at the vertex asked".into()));
}
Ok(ids)
}
/// A face's transfinite grid in its owner's frame.
fn face_grid(&mut self, face: &Face) -> Result<FaceGrid> {
let &(c, d, side) = face.uses.first().ok_or_else(|| Error::Failed("a face without a cell".into()))?;
let r = face.ring;
let side_nodes = |b: &Self, u: usize, v: usize| -> Result<Vec<u32>> {
let (e, _) = b.topo.edge(c, u, v).ok_or_else(|| Error::Failed("a face side is no edge".into()))?;
b.edge_from(e, u)
};
let bottom = side_nodes(self, r[0], r[1])?;
let right = side_nodes(self, r[1], r[2])?;
let top = side_nodes(self, r[3], r[2])?;
let left = side_nodes(self, r[0], r[3])?;
let (na, nb) = (bottom.len() - 1, left.len() - 1);
if top.len() != na + 1 || right.len() != nb + 1 {
return Err(Error::Failed("opposite sides of a face have different counts".into()));
}
let at = |b: &Self, ids: &[u32]| -> Vec<P> { ids.iter().map(|&i| b.nodes.get(i as usize).copied().unwrap_or([0.0; 3])).collect() };
let grid = transfinite_grid(&at(self, &bottom), &at(self, &right), &at(self, &top), &at(self, &left))?;
let ctx = self.ctxs.get(c).ok_or_else(|| Error::Failed("face owner".into()))?;
let brep_face = self.cubes.get(c).and_then(|q| q.faces.get(d)).and_then(|row| row.get(side)).copied();
let mut ids = vec![0u32; (na + 1) * (nb + 1)];
for b in 0..=nb {
for a in 0..=na {
let id = if b == 0 {
bottom.get(a).copied()
} else if b == nb {
top.get(a).copied()
} else if a == 0 {
left.get(b).copied()
} else if a == na {
right.get(b).copied()
} else {
let q = grid.get(b * (na + 1) + a).copied().ok_or_else(|| Error::Failed("face grid".into()))?;
let q = brep_face.and_then(|f| project_to_face(ctx, f, q)).unwrap_or(q);
Some(self.push(q)?)
};
if let Some(slot) = ids.get_mut(b * (na + 1) + a) {
*slot = id.ok_or_else(|| Error::Failed("face side node".into()))?;
}
}
}
Ok(FaceGrid { ring: r, na, nb, ids })
}
/// Cell `c`'s lattice of node ids: boundary from the shared face grids, interior new.
fn cell_lattice(&mut self, c: usize, n: [usize; 3], face_grids: &[FaceGrid]) -> Result<Vec<u32>> {
let topo = self.topo;
let corners = topo.corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
let faces = topo.cell_faces.get(c).copied().ok_or_else(|| Error::Failed("cell faces".into()))?;
let total = (n[0] + 1) * (n[1] + 1) * (n[2] + 1);
let mut ids: Vec<Option<u32>> = vec![None; total];
for d in 0..3 {
let (da, db) = other(d);
for side in 0..2 {
let gf = faces.get(d).and_then(|r| r.get(side)).copied().ok_or_else(|| Error::Failed("cell face".into()))?;
let grid = face_grids.get(gf).ok_or_else(|| Error::Failed("face grid".into()))?;
let ring = face_ring(d, side).map(|[i, j, k]| corners[i][j][k]);
let map = grid.map_from(ring, n[da], n[db])?;
for b in 0..=n[db] {
for a in 0..=n[da] {
let mut l = [0usize; 3];
l[d] = side * n[d];
l[da] = a;
l[db] = b;
if let Some(slot) = ids.get_mut(lattice_index(n, l)) {
*slot = Some(map(a, b)?);
}
}
}
}
}
// Interior: trivariate interpolation, blended by the edges' arc-length distribution.
let mut x: Vec<Option<P>> = ids.iter().map(|i| i.and_then(|i| self.nodes.get(i as usize).copied())).collect();
let groups = topo.cell_edges.get(c).copied().ok_or_else(|| Error::Failed("cell edges".into()))?;
let mut blend: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
for (d, g) in groups.iter().enumerate() {
let mut sum = vec![0.0; n[d] + 1];
for (m, &e) in g.iter().enumerate() {
let pts: Vec<P> = self
.edge_from(e, edge_ends(&corners, d, m).0)?
.iter()
.map(|&i| self.nodes.get(i as usize).copied().unwrap_or([0.0; 3]))
.collect();
let params = crate::quad::tfi::params(&pts, "edge")?;
if params.len() != sum.len() {
return Err(Error::Failed("an edge's count differs from its block's".into()));
}
for (acc, p) in sum.iter_mut().zip(params) {
*acc += 0.25 * p;
}
}
if let Some(first) = sum.first_mut() {
*first = 0.0;
}
if let Some(last) = sum.last_mut() {
*last = 1.0;
}
blend[d] = sum;
}
transfinite_volume(&mut x, n, [&blend[0], &blend[1], &blend[2]])?;
let mut out = Vec::with_capacity(total);
for (slot, p) in ids.iter().zip(&x) {
out.push(match (slot, p) {
(Some(id), _) => *id,
(None, Some(p)) => self.push(*p)?,
(None, None) => return Err(Error::Failed("a block node is missing".into())),
});
}
Ok(out)
}
}
/// A face's grid of node ids in its owner's frame: `(na + 1) × (nb + 1)`, node `(a, b)` at
/// `b·(na + 1) + a`, corners at the owner's ring.
struct FaceGrid {
ring: [usize; 4],
na: usize,
nb: usize,
ids: Vec<u32>,
}
impl FaceGrid {
/// The lookup of node `(a, b)` of a cell's view of this face, whose ring (the order of
/// [`face_ring`]) is `ring` and counts `na × nb`.
fn map_from(&self, ring: [usize; 4], na: usize, nb: usize) -> Result<impl Fn(usize, usize) -> Result<u32> + '_> {
// The owner's lattice position of each of the cell's corners.
let corner = |v: usize| -> Option<[i64; 2]> {
let k = self.ring.iter().position(|&x| x == v)?;
let (a, b) = [(0, 0), (self.na, 0), (self.na, self.nb), (0, self.nb)].get(k).copied()?;
Some([a as i64, b as i64])
};
let (Some(o), Some(pa), Some(pb)) = (corner(ring[0]), corner(ring[1]), corner(ring[3])) else {
return Err(Error::Failed("a cell's face does not have the shared face's corners".into()));
};
let step = |p: [i64; 2], n: usize| -> Option<[i64; 2]> {
let d = [p[0] - o[0], p[1] - o[1]];
let n = n as i64;
// One axis of the owner's lattice, `n` long.
(n > 0 && d.iter().filter(|&&x| x != 0).count() == 1 && d.iter().all(|&x| x == 0 || x.abs() == n)).then(|| d.map(|x| x / n))
};
let (Some(du), Some(dv)) = (step(pa, na), step(pb, nb)) else {
return Err(Error::Failed("a cell's view of a shared face does not match its grid".into()));
};
let (na_owner, ids) = (self.na, &self.ids);
Ok(move |a: usize, b: usize| -> Result<u32> {
let (a, b) = (a as i64, b as i64);
let u = o[0] + a * du[0] + b * dv[0];
let v = o[1] + a * du[1] + b * dv[1];
usize::try_from(v)
.ok()
.zip(usize::try_from(u).ok())
.and_then(|(v, u)| ids.get(v * (na_owner + 1) + u))
.copied()
.ok_or_else(|| Error::Failed("shared face grid index".into()))
})
}
}