forked from 1b-agent/fieldcraft-upstream
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
797 lines
37 KiB
Rust
797 lines
37 KiB
Rust
//! 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()))
|
||
})
|
||
}
|
||
}
|