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 <[email protected]>
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

+117 -5
View File
@@ -6,7 +6,16 @@
//! The hex meshers reason on this topology the way B-rep sweepers do (Blacker, *The Cooper
//! tool*, Proc. 5th International Meshing Roundtable, 1996): which faces are sources, targets and
//! linking faces, and which edges bound them.
//!
//! **Virtual topology.** [`Brep::composite`] joins faces that meet smoothly along all of their
//! common boundary (the two halves of a cylinder the kernel splits at a seam, a plane running
//! tangentially into a fillet, coplanar pieces) into one composite face, so their seam is no edge
//! of the mesher's topology: the composite surfaces of virtual topology (A. Sheffer, M. Bercovier,
//! T. Blacker, J. Clements, *Virtual topology operators for meshing*, IJCGA 10(3):309–331, 2000).
//! A composite face keeps an analytic surface when every member lies on the first member's;
//! otherwise nodes go onto its triangles.
use std::borrow::Cow;
use std::collections::BTreeMap;
use super::geom::{Surface, add, dist, scale, unit};
@@ -59,6 +68,9 @@ impl Edge {
pub(crate) struct Brep<'a> {
pub surface: &'a BoundarySurface,
/// The face tag of each triangle: the surface's own tags, or for composite faces the smallest
/// tag of the composite.
tags: Cow<'a, [u32]>,
pub faces: Vec<Face>,
pub edges: Vec<Edge>,
/// Whether each surface vertex is a B-rep vertex.
@@ -72,9 +84,29 @@ pub(crate) struct Brep<'a> {
/// Relative geometric tolerance (of the bounding-box diagonal).
pub(crate) const RELATIVE_EPS: f64 = 1e-7;
/// Largest angle (radians) between the surface normals of two faces along their common boundary
/// for them to join into a composite face: 1°.
const SMOOTH_ANGLE: f64 = 0.017_453_292_519_943_295;
/// Distance from a member's vertices to the first member's analytic surface (relative to the
/// bounding-box diagonal) within which a composite face keeps that surface.
const RELATIVE_ON_SURFACE: f64 = 1e-6;
impl<'a> Brep<'a> {
/// Recover the topology. `surfaces[tag]` is the analytic surface of B-rep face `tag`, if any.
pub(crate) fn new(surface: &'a BoundarySurface, surfaces: &[Option<Surface>]) -> Result<Self> {
Self::build(surface, surfaces, Cow::Borrowed(surface.tri_face()), false)
}
/// [`Brep::new`] with smoothly meeting faces joined into composite faces (virtual topology).
pub(crate) fn composite(surface: &'a BoundarySurface, surfaces: &[Option<Surface>]) -> Result<Self> {
let tags = composite_tags(surface, surfaces);
Self::build(surface, surfaces, Cow::Owned(tags), true)
}
fn build(surface: &'a BoundarySurface, surfaces: &[Option<Surface>], tags: Cow<'a, [u32]>, composite: bool) -> Result<Self> {
if tags.len() != surface.triangles().len() {
return Err(Error::Failed("a face tag per triangle is needed".into()));
}
let pts = surface.points();
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
@@ -91,7 +123,7 @@ impl<'a> Brep<'a> {
let eps = RELATIVE_EPS * diagonal;
// Faces in tag order.
let mut by_tag: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
for (t, &f) in surface.tri_face().iter().enumerate() {
for (t, &f) in tags.iter().enumerate() {
by_tag.entry(f).or_default().push(t as u32);
}
let index: BTreeMap<u32, usize> = by_tag.keys().enumerate().map(|(i, &f)| (f, i)).collect();
@@ -109,12 +141,13 @@ impl<'a> Brep<'a> {
}
verts.sort_unstable();
verts.dedup();
let analytic = surfaces.get(id as usize).copied().flatten().and_then(|s| s.checked());
let analytic =
if composite { composite_surface(surface, surfaces, tris, RELATIVE_ON_SURFACE * diagonal) } else { own_surface(surfaces, id) };
let normal = unit(nsum).unwrap_or([0.0; 3]);
let plane = planarity(pts, &verts, normal, analytic, eps);
faces.push(Face { id, triangles: tris.clone(), vertices: verts, area, surface: analytic, plane, loops: Vec::new() });
}
let face_of = |t: u32| surface.tri_face().get(t as usize).and_then(|f| index.get(f)).copied();
let face_of = |t: u32| tags.get(t as usize).and_then(|f| index.get(f)).copied();
// Oriented boundary segments: `from → to` with face `left` traversing it.
struct Seg {
from: u32,
@@ -128,6 +161,10 @@ impl<'a> Brep<'a> {
let (Some(left), Some(right)) = (face_of(fe.triangles[0]), face_of(fe.triangles[1])) else {
return Err(Error::Failed("feature edge without faces".into()));
};
// The seam inside a composite face is no edge.
if left == right {
continue;
}
// Orient each segment as the lower-indexed face traverses it, so that the segments of
// one edge chain head to tail.
let [lo, hi] = fe.vertices;
@@ -236,7 +273,7 @@ impl<'a> Brep<'a> {
face.loops = loops;
}
}
Ok(Brep { surface, faces, edges, is_vertex, edge_triangles, diagonal })
Ok(Brep { surface, tags, faces, edges, is_vertex, edge_triangles, diagonal })
}
pub(crate) fn point(&self, v: u32) -> P {
@@ -276,9 +313,14 @@ impl<'a> Brep<'a> {
/// The faces that use the triangle's tag (index into `faces`).
pub(crate) fn face_of_triangle(&self, t: u32) -> Option<usize> {
let tag = *self.surface.tri_face().get(t as usize)?;
let tag = *self.tags.get(t as usize)?;
self.faces.binary_search_by_key(&tag, |f| f.id).ok()
}
/// The face tag of triangle `t` in this topology (a composite face's tag for its members).
pub(crate) fn tag(&self, t: u32) -> Option<u32> {
self.tags.get(t as usize).copied()
}
}
/// A point and outward normal of a planar face: the analytic plane, or the plane fitted through
@@ -300,3 +342,73 @@ fn planarity(pts: &[P], verts: &[u32], normal: P, analytic: Option<Surface>, eps
// tolerance, not to ours.
(flat || matches!(analytic, Some(Surface::Plane { .. }))).then_some((point, n))
}
/// The analytic surface of face `tag` from the geometry, checked.
fn own_surface(surfaces: &[Option<Surface>], tag: u32) -> Option<Surface> {
surfaces.get(tag as usize).copied().flatten().and_then(|s| s.checked())
}
/// The analytic surface of a composite face (its triangles `tris`): the first member's, when the
/// vertices of every member lie on it within `tol`.
fn composite_surface(surface: &BoundarySurface, surfaces: &[Option<Surface>], tris: &[u32], tol: f64) -> Option<Surface> {
let tri_face = surface.tri_face();
let first = tris.iter().filter_map(|&t| tri_face.get(t as usize)).min()?;
let s = own_surface(surfaces, *first)?;
let pts = surface.points();
let on = |v: u32| pts.get(v as usize).is_some_and(|&p| s.project(p).is_some_and(|q| dist(p, q) <= tol));
tris.iter().all(|&t| surface.triangles().get(t as usize).is_some_and(|tri| tri.iter().all(|&v| on(v)))).then_some(s)
}
/// Face tags with smoothly meeting faces joined: two faces join when, at every boundary segment
/// between them, their analytic surfaces' normals are within [`SMOOTH_ANGLE`] of parallel and the
/// two triangles there face the same side. Each composite face takes its smallest member tag.
fn composite_tags(surface: &BoundarySurface, surfaces: &[Option<Surface>]) -> Vec<u32> {
let tri_face = surface.tri_face();
let pts = surface.points();
let point = |v: u32| pts.get(v as usize).copied().unwrap_or([0.0; 3]);
let tri_normal = |t: u32| {
surface.triangles().get(t as usize).and_then(|tri| {
let [a, b, c] = tri.map(point);
unit(cross(sub(b, a), sub(c, a)))
})
};
let cos_min = SMOOTH_ANGLE.cos();
// Per pair of faces (low tag, high tag): whether every segment between them is smooth.
let mut pairs: BTreeMap<(u32, u32), bool> = BTreeMap::new();
for fe in surface.feature_edges().iter().filter(|e| e.kind == FeatureKind::FaceBoundary) {
let (Some(&a), Some(&b)) = (tri_face.get(fe.triangles[0] as usize), tri_face.get(fe.triangles[1] as usize)) else { continue };
if a == b {
continue;
}
let m = scale(add(point(fe.vertices[0]), point(fe.vertices[1])), 0.5);
let normals = (own_surface(surfaces, a).and_then(|s| s.normal(m)), own_surface(surfaces, b).and_then(|s| s.normal(m)));
let same_side = match (tri_normal(fe.triangles[0]), tri_normal(fe.triangles[1])) {
(Some(x), Some(y)) => dot(x, y) > 0.0,
_ => false,
};
let smooth = same_side && matches!(normals, (Some(x), Some(y)) if dot(x, y).abs() >= cos_min);
let entry = pairs.entry((a.min(b), a.max(b))).or_insert(true);
*entry = *entry && smooth;
}
// Union–find over tags, the root always the smaller tag.
let mut parent: BTreeMap<u32, u32> = BTreeMap::new();
fn root(parent: &BTreeMap<u32, u32>, mut x: u32) -> u32 {
// Each step goes to a smaller tag, so this ends.
while let Some(&p) = parent.get(&x) {
if p == x {
break;
}
x = p;
}
x
}
for (&(a, b), &smooth) in &pairs {
if smooth {
let (ra, rb) = (root(&parent, a), root(&parent, b));
if ra != rb {
parent.insert(ra.max(rb), ra.min(rb));
}
}
}
tri_face.iter().map(|&f| root(&parent, f)).collect()
}
+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()))
})
}
}
+34 -1
View File
@@ -115,6 +115,29 @@ impl Surface {
};
q.iter().all(|x| x.is_finite()).then_some(q)
}
/// The unit normal of the surface at its point closest to `p`, up to sign, or `None` where
/// the closest point is not unique.
pub(crate) fn normal(&self, p: P) -> Option<P> {
match *self {
Surface::Plane { normal, .. } => unit(normal),
Surface::Cylinder { origin, axis, .. } => {
let w = sub(p, origin);
unit(sub(w, scale(axis, dot(w, axis))))
}
Surface::Cone { apex, axis, half_angle } => {
let w = sub(p, apex);
let radial = unit(sub(w, scale(axis, dot(w, axis))))?;
unit(sub(scale(radial, half_angle.cos()), scale(axis, half_angle.sin())))
}
Surface::Sphere { center, .. } => unit(sub(p, center)),
Surface::Torus { center, axis, major, .. } => {
let w = sub(p, center);
let ring = add(center, scale(unit(sub(w, scale(axis, dot(w, axis))))?, major));
unit(sub(p, ring))
}
}
}
}
/// The closest point to `p` on triangle `abc`.
@@ -274,8 +297,18 @@ impl<'a> TriangleTree<'a> {
/// The closest triangle to `p` among those `accept` admits (by triangle index).
pub(crate) fn closest(&self, p: P, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
self.search(p, f64::INFINITY, accept)
}
/// [`TriangleTree::closest`] among the triangles within distance `r` of `p` (the search
/// skips everything farther).
pub(crate) fn closest_within(&self, p: P, r: f64, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
self.search(p, r * r, accept).filter(|h| h.dist <= r)
}
fn search(&self, p: P, limit2: f64, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
let mut best: Option<Hit> = None;
let mut best2 = f64::INFINITY;
let mut best2 = limit2;
let mut stack = vec![0usize];
let mut visits = 0usize;
while let Some(id) = stack.pop() {
+83 -36
View File
@@ -12,6 +12,7 @@
use std::collections::BTreeMap;
use super::brep::Brep;
use super::interval::Intervals;
use super::{Ctx, Raw, project_to_face, project_to_faces};
use crate::quad::{lattice_index, transfinite_faces, transfinite_volume};
@@ -20,10 +21,30 @@ use crate::vec3::{P, det, sub};
use crate::{Error, Result};
/// The most nodes of a mapped block.
const MAX_NODES: usize = 1 << 26;
pub(crate) const MAX_NODES: usize = 1 << 26;
pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
let brep = ctx.brep;
/// A logical cube found in a B-rep: its corners, its edges by logical direction and its faces.
pub(crate) struct Cube {
/// The surface vertex at each corner, `corner[i][j][k]`, right-handed.
pub corner: [[[u32; 2]; 2]; 2],
/// The four edges along each logical direction `d`, oriented from the low corner to the high
/// one, at the other two coordinates `(s, t) ∈ {0, 1}²` in the order (0, 0), (0, 1), (1, 0),
/// (1, 1).
pub groups: [[(usize, bool); 4]; 3],
/// The B-rep face (index into `faces`) on each side of each direction: `faces[d][side]`.
pub faces: [[usize; 2]; 3],
}
impl Cube {
/// The corner at logical coordinates `(i, j, k)` (each 0 or 1).
pub(crate) fn at(&self, i: usize, j: usize, k: usize) -> u32 {
self.corner[i & 1][j & 1][k & 1]
}
}
/// Recognize a logical cube: six faces with one loop of four edges each, three edges at each of
/// eight corners. Anything else is [`Error::Unsupported`] with the reason.
pub(crate) fn recognize(brep: &Brep) -> Result<Cube> {
if brep.faces.len() != 6 || brep.edges.len() != 12 {
return Err(Error::Unsupported(format!("not a logical cube: {} faces and {} edges (6 and 12 needed)", brep.faces.len(), brep.edges.len())));
}
@@ -61,7 +82,6 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
};
let c111 = common(c110, c101, a).filter(|&x| neighbours(c011).contains(&x));
let Some(c111) = c111 else { return Err(Error::Unsupported("not a logical cube: the corner graph is not a cube".into())) };
// corner[i][j][k]
let corner = [[[c000, c], [b, c011]], [[a, c101], [c110, c111]]];
let at = |i: usize, j: usize, k: usize| -> u32 { corner[i][j][k] };
let mut distinct: Vec<u32> = corner.iter().flatten().flatten().copied().collect();
@@ -74,20 +94,65 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
let edge_between = |u: u32, v: u32| -> Option<(usize, bool)> {
adj.get(&u)?.iter().find(|x| x.0 == v).map(|x| (x.1, brep.edges.get(x.1).is_some_and(|e| e.first() == u)))
};
// The four edges along each direction, from the low corner to the high one.
let mut groups: [Vec<(usize, bool)>; 3] = [Vec::new(), Vec::new(), Vec::new()];
let mut groups = [[(0usize, true); 4]; 3];
for (d, group) in groups.iter_mut().enumerate() {
for s in 0..2 {
for t in 0..2 {
let (lo, hi) = match d {
0 => (at(0, s, t), at(1, s, t)),
1 => (at(s, 0, t), at(s, 1, t)),
_ => (at(s, t, 0), at(s, t, 1)),
};
group.push(edge_between(lo, hi).ok_or_else(|| Error::Unsupported("not a logical cube: a corner pair has no edge".into()))?);
}
for (m, slot) in group.iter_mut().enumerate() {
let (s, t) = (m / 2, m % 2);
let (lo, hi) = match d {
0 => (at(0, s, t), at(1, s, t)),
1 => (at(s, 0, t), at(s, 1, t)),
_ => (at(s, t, 0), at(s, t, 1)),
};
*slot = edge_between(lo, hi).ok_or_else(|| Error::Unsupported("not a logical cube: a corner pair has no edge".into()))?;
}
}
// The face bounded by the four edges of each side.
let mut faces = [[0usize; 2]; 3];
for (d, row) in faces.iter_mut().enumerate() {
for (side, slot) in row.iter_mut().enumerate() {
let ring = face_ring(d, side).map(|[i, j, k]| at(i, j, k));
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
bounding.sort_unstable();
*slot = brep
.faces
.iter()
.position(|f| {
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
es.sort_unstable();
es == bounding
})
.ok_or_else(|| Error::Unsupported("not a logical cube: a side's four edges bound no face".into()))?;
}
}
Ok(Cube { corner, groups, faces })
}
/// The corners of side `side` of direction `d` in the order (0, 0), (1, 0), (1, 1), (0, 1) of its
/// two other directions (lower first).
pub(crate) fn face_ring(d: usize, side: usize) -> [[usize; 3]; 4] {
let (da, db) = other(d);
[(0, 0), (1, 0), (1, 1), (0, 1)].map(|(a, b)| {
let mut c = [0usize; 3];
c[d] = side;
c[da] = a;
c[db] = b;
c
})
}
/// The two directions other than `d`, lower first.
pub(crate) fn other(d: usize) -> (usize, usize) {
match d {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
}
}
pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
let brep = ctx.brep;
let cube = recognize(brep)?;
let groups = cube.groups;
let mut ia = Intervals::default();
for e in 0..brep.edges.len() {
ia.var(ideal_divisions(ctx.field, &brep.edge_points(e, true))?, 1);
@@ -99,7 +164,7 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
}
}
let counts = ia.solve()?;
let count = |g: &Vec<(usize, bool)>| g.first().and_then(|x| counts.get(x.0)).copied().unwrap_or(1);
let count = |g: &[(usize, bool); 4]| g.first().and_then(|x| counts.get(x.0)).copied().unwrap_or(1);
let (ni, nj, nk) = (count(&groups[0]), count(&groups[1]), count(&groups[2]));
let total = (ni + 1).checked_mul(nj + 1).and_then(|x| x.checked_mul(nk + 1)).filter(|&x| x <= MAX_NODES);
let total = total.ok_or_else(|| Error::Limit(format!("a {ni} × {nj} × {nk} block exceeds {MAX_NODES} nodes")))?;
@@ -135,26 +200,8 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
// Faces: grids from their edges, projected onto the B-rep face their four edges bound.
let n = [ni, nj, nk];
transfinite_faces(&mut x, n, None, |d, side, grid| {
let (da, db) = match d {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let logical = |a: usize, b: usize| {
let mut c = [0usize; 3];
c[d] = side;
c[da] = a;
c[db] = b;
at(c[0], c[1], c[2])
};
let ring = [logical(0, 0), logical(1, 0), logical(1, 1), logical(0, 1)];
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
bounding.sort_unstable();
let face = brep.faces.iter().position(|f| {
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
es.sort_unstable();
es == bounding
});
let (da, db) = other(d);
let face = cube.faces.get(d).and_then(|r| r.get(side)).copied();
let (na, nb) = (n[da], n[db]);
for b in 1..nb {
for a in 1..na {
+208 -36
View File
@@ -19,6 +19,11 @@
//! A body none of these fits is [`Error::Unsupported`] with each method's reason: the input for
//! decomposition into sweepable and mappable regions.
//!
//! **Partitioned parts.** [`hex_mesh_cells`] ([`cells`]) meshes a part split into cells that
//! meet face to face (SolidCraft split bodies) as one conforming mesh: every cell a mapped block,
//! interval assignment over all cells together, each shared face meshed once and its nodes
//! merged by identity.
//!
//! **Common steps.** Interval assignment ([`interval`]) picks the division counts of shared curves
//! together. Boundary nodes on analytic faces are placed on the exact surface; quadratic hexahedra
//! (hex20) get their mid-edge nodes projected onto the faces (and onto both faces of a B-rep
@@ -31,6 +36,7 @@
mod arrangement;
mod brep;
mod cells;
mod geom;
mod interval;
mod layered;
@@ -43,6 +49,7 @@ mod tests;
use std::collections::HashMap;
pub use cells::{Cell, CellsMesh, GROWTH, MAX_CELLS, hex_mesh_cells};
pub use geom::Surface;
use brep::Brep;
@@ -198,16 +205,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
surface.shells().len()
)));
}
// A mesh far beyond the element limit is refused before any work: the solid's volume over
// the cube of the smallest size the field asks for at the boundary.
let mut h_min = f64::INFINITY;
for p in surface.points() {
h_min = h_min.min(crate::size::checked_size(field, *p)?);
}
let estimate = surface.volume().abs() / h_min.powi(3);
if !(estimate <= 8.0 * opts.max_elements as f64) {
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
}
check_estimate(&[surface], field, opts)?;
let brep = Brep::new(surface, faces)?;
let tree = TriangleTree::new(surface)?;
let ctx = Ctx { brep: &brep, tree: &tree, field, opts, tol: RELATIVE_TOLERANCE * brep.diagonal };
@@ -253,7 +251,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
Err(e @ Error::Limit(_)) => return Err(e),
Err(e) => last = Some(e),
},
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&ctx, raw, method)) {
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&Cells::one(&ctx), raw, method)) {
Ok(mesh) => return Ok(mesh),
Err(e @ Error::Limit(_)) => return Err(e),
Err(e) => last = Some(e),
@@ -266,6 +264,23 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
Err(Error::Unsupported(format!("no structured hex method fits this body ({})", reasons.join("; "))))
}
/// Refuse a mesh far beyond the element limit before any work: the solids' volume over the cube
/// of the smallest size the field asks for on their boundaries.
fn check_estimate(surfaces: &[&BoundarySurface], field: &dyn SizeField, opts: &HexOptions) -> Result<()> {
let (mut h_min, mut volume) = (f64::INFINITY, 0.0);
for s in surfaces {
for p in s.points() {
h_min = h_min.min(crate::size::checked_size(field, *p)?);
}
volume += s.volume().abs();
}
let estimate = volume / h_min.powi(3);
if !(estimate <= 8.0 * opts.max_elements as f64) {
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
}
Ok(())
}
/// The frames a layered method can try, fewest levels first, or the first reason none fits.
fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<layered::Setup>, String> {
let frames = if method == HexMethod::Extrude { layered::translations(ctx.brep) } else { layered::rotations(ctx.brep) };
@@ -292,7 +307,7 @@ fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<lay
fn with_refinement(ctx: &Ctx, method: HexMethod, run: impl Fn(u32) -> Result<Raw>) -> Result<HexMesh> {
let mut last = Error::Failed("no attempt".into());
for refine in 0..=MAX_REFINE {
match run(refine).and_then(|raw| finish(ctx, raw, method)) {
match run(refine).and_then(|raw| finish(&Cells::one(ctx), raw, method)) {
Ok(mesh) => return Ok(mesh),
Err(e @ Error::Failed(_)) => last = e,
Err(e) => return Err(e),
@@ -314,14 +329,130 @@ fn hex_face(k: usize) -> [usize; 4] {
ElementKind::Hex8.faces().get(k).and_then(|f| <[usize; 4]>::try_from(*f).ok()).unwrap_or([0, 1, 2, 3])
}
/// Tag, check, upgrade to hex20 when asked, gate and measure.
fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
/// The cells a mesh spans, for [`finish`]: one context per cell and the cell of each element.
pub(crate) struct Cells<'c, 'a> {
pub ctxs: &'c [Ctx<'a>],
/// The cell of each hexahedron (index into `ctxs`); empty: all in the first.
pub of: &'c [u32],
/// The cells' names, for messages.
pub names: &'c [&'c str],
}
impl<'c, 'a> Cells<'c, 'a> {
pub(crate) fn one(ctx: &'c Ctx<'a>) -> Self {
Cells { ctxs: std::slice::from_ref(ctx), of: &[], names: &[] }
}
fn name(&self, c: usize) -> String {
self.names.get(c).map_or_else(|| c.to_string(), |n| format!("`{n}`"))
}
fn cell(&self, e: usize) -> usize {
self.of.get(e).map_or(0, |&c| c as usize)
}
fn ctx(&self, e: usize) -> Result<&Ctx<'a>> {
self.ctxs.get(self.cell(e)).ok_or_else(|| Error::Failed(format!("element {e} has no cell")))
}
}
/// Buckets of a uniform grid over the cells' bounding boxes: which cells may be near a point.
struct CellGrid {
lo: P,
step: f64,
dims: [usize; 3],
buckets: Vec<Vec<usize>>,
}
/// Cosine of the largest angle between a boundary face's normal and the reversed normal of
/// another cell's triangle for the face to lie on that triangle (about 25°): a face on another
/// cell faces it, while faces that merely meet at an edge (a fillet beside a split) do not.
const FACING: f64 = 0.9;
/// Buckets per axis of a [`CellGrid`] at most.
const MAX_GRID_DIM: usize = 64;
impl CellGrid {
/// The cells' boxes grown by `gap`.
fn new(ctxs: &[Ctx], gap: f64) -> CellGrid {
let boxes: Vec<(P, P)> = ctxs
.iter()
.map(|c| {
c.brep.surface.points().iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), p| {
for k in 0..3 {
lo[k] = lo[k].min(p[k] - gap);
hi[k] = hi[k].max(p[k] + gap);
}
(lo, hi)
})
})
.collect();
let (lo, hi) = boxes.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), (a, b)| {
for k in 0..3 {
lo[k] = lo[k].min(a[k]);
hi[k] = hi[k].max(b[k]);
}
(lo, hi)
});
let per_axis = ((ctxs.len() as f64).cbrt().ceil() as usize * 2).clamp(1, MAX_GRID_DIM);
let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
let step = if extent > 0.0 && extent.is_finite() { extent / per_axis as f64 } else { 1.0 };
let dim = |k: usize| (((hi[k] - lo[k]) / step).ceil() as usize).clamp(1, MAX_GRID_DIM);
let dims = [dim(0), dim(1), dim(2)];
let mut grid = CellGrid { lo, step, dims, buckets: vec![Vec::new(); dims[0] * dims[1] * dims[2]] };
for (c, (a, b)) in boxes.iter().enumerate() {
let (i0, i1) = (grid.index(*a), grid.index(*b));
for i in i0[0]..=i1[0] {
for j in i0[1]..=i1[1] {
for k in i0[2]..=i1[2] {
if let Some(bucket) = grid.buckets.get_mut((k * dims[1] + j) * dims[0] + i) {
bucket.push(c);
}
}
}
}
}
grid
}
fn index(&self, p: P) -> [usize; 3] {
[0, 1, 2].map(|k| {
let x = ((p[k] - self.lo[k]) / self.step).floor();
if x.is_finite() && x > 0.0 { (x as usize).min(self.dims[k] - 1) } else { 0 }
})
}
fn near(&self, p: P) -> &[usize] {
let [i, j, k] = self.index(p);
self.buckets.get((k * self.dims[1] + j) * self.dims[0] + i).map(Vec::as_slice).unwrap_or(&[])
}
}
/// A boundary quadrilateral of a mesh being finished: the element face, its cell and the B-rep
/// face (index into the cell's faces) it lies on.
struct Skin {
element: u32,
face: u8,
cell: usize,
brep: usize,
}
/// Tag, check, upgrade to hex20 when asked, gate and measure. Besides the gate, the tiling and
/// the fidelity checks, no boundary face of a mesh of several cells may lie on another cell's
/// boundary (a free internal face: cells that do not conform there), and the mesh boundary must
/// be a closed 2-manifold of quadrilaterals (every edge on exactly two).
fn finish(cells: &Cells, raw: Raw, method: HexMethod) -> Result<HexMesh> {
let Raw { mut nodes, hexes, description } = raw;
let first = cells.ctxs.first().ok_or_else(|| Error::Failed("no cell to finish".into()))?;
let opts = first.opts;
if hexes.is_empty() {
return Err(Error::Failed("the method produced no elements".into()));
}
if hexes.len() > ctx.opts.max_elements {
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), ctx.opts.max_elements)));
if hexes.len() > opts.max_elements {
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), opts.max_elements)));
}
if !cells.of.is_empty() && cells.of.len() != hexes.len() {
return Err(Error::Failed("a cell is needed for every element".into()));
}
fieldcraft_model::check_coordinates(nodes.as_flattened())?;
let at = |nodes: &[P], i: u32| nodes.get(i as usize).copied().ok_or_else(|| Error::Failed(format!("node {i} out of range")));
@@ -337,8 +468,14 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
if let Some(n) = uses.values().copied().max().filter(|&n| n > 2) {
return Err(Error::Failed(format!("a hexahedron face is shared by {n} elements (the mesh is not a manifold)")));
}
// A boundary face this close to another cell's boundary lies on it.
let gap = 2.0 * opts.chordal_tolerance + cells.ctxs.iter().map(|c| c.tol).fold(0.0, f64::max);
let others = (cells.ctxs.len() > 1).then(|| CellGrid::new(cells.ctxs, gap));
let mut skin: Vec<Skin> = Vec::new();
let mut boundary = Vec::new();
let mut fidelity = 0.0;
// Boundary edges of the quadrilaterals: each must be on exactly two of them.
let mut rim: HashMap<(u32, u32), u32> = HashMap::new();
for (e, h) in hexes.iter().enumerate() {
for k in 0..6 {
let corners = hex_face(k).map(|i| h.get(i).copied().unwrap_or(0));
@@ -347,6 +484,11 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
if uses.get(&key) != Some(&1) {
continue;
}
for m in 0..4 {
let (a, b) = (corners[m], corners[(m + 1) % 4]);
*rim.entry((a.min(b), a.max(b))).or_insert(0) += 1;
}
let ctx = cells.ctx(e)?;
let [a, b, c, d] = [at(&nodes, corners[0])?, at(&nodes, corners[1])?, at(&nodes, corners[2])?, at(&nodes, corners[3])?];
let centre = scale(add(add(a, b), add(c, d)), 0.25);
let n = cross(sub(c, a), sub(d, b));
@@ -357,14 +499,35 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
.closest(centre, &|t| dot(ctx.tree.normal(t), n) > 0.0)
.ok_or_else(|| Error::Failed(format!("no boundary face faces the same way as element {e}'s face {}", k + 1)))?;
let f = ctx.brep.face_of_triangle(hit.triangle).ok_or_else(|| Error::Failed("triangle without a face".into()))?;
fidelity += area * (hit.dist + ctx.opts.chordal_tolerance);
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: ctx.brep.face_tag(f) });
let cell = cells.cell(e);
if let Some(grid) = &others {
for &o in grid.near(centre) {
let Some(other) = cells.ctxs.get(o).filter(|_| o != cell) else { continue };
if other.tree.closest_within(centre, gap, &|t| dot(other.tree.normal(t), n) < -FACING).is_some() {
return Err(Error::Failed(format!(
"element {e}'s face {} lies on cell {}'s boundary without a neighbour there: cells {} and {} do not conform (split them so that they meet face to face)",
k + 1,
cells.name(o),
cells.name(cell),
cells.name(o)
)));
}
}
}
// The geometry's own face tag (a member of a composite face keeps its own).
let tag = ctx.brep.surface.tri_face().get(hit.triangle as usize).copied().unwrap_or_else(|| ctx.brep.face_tag(f));
fidelity += area * (hit.dist + opts.chordal_tolerance);
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: tag });
skin.push(Skin { element: e as u32, face: k as u8 + 1, cell, brep: f });
}
}
let kind = if ctx.opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
if let Some((&(a, b), &n)) = rim.iter().filter(|(_, n)| **n != 2).min_by_key(|(k, _)| **k) {
return Err(Error::Failed(format!("the mesh boundary is not closed: the edge between nodes {a} and {b} is on {n} boundary faces")));
}
let kind = if opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
let mut connectivity: Vec<u32> = Vec::with_capacity(hexes.len() * kind.node_count());
if ctx.opts.quadratic {
let mids = mid_edge_nodes(ctx, &mut nodes, &hexes, &boundary)?;
if opts.quadratic {
let mids = mid_edge_nodes(cells, &mut nodes, &hexes, &skin)?;
for (h, m) in hexes.iter().zip(&mids) {
connectivity.extend_from_slice(h);
connectivity.extend_from_slice(m);
@@ -398,7 +561,7 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
let gate = validity_gate_checks(checks.iter().map(|(l, c)| (*l, c)), &GateOptions { expected_volume: Some(enclosed), volume_tolerance: 1e-9 })?
.into_result()
.map_err(|e| Error::Failed(format!("{} ({description})", plain(&e))))?;
let surface_volume = ctx.brep.surface.volume();
let surface_volume: f64 = cells.ctxs.iter().map(|c| c.brep.surface.volume()).sum();
let slack = 2.0 * fidelity + 1e-9 * surface_volume.abs();
if (gate.volume - surface_volume).abs() > slack {
return Err(Error::Failed(format!(
@@ -478,46 +641,52 @@ pub(crate) fn project_to_face(ctx: &Ctx, f: usize, p: P) -> Option<P> {
return Some(q);
}
let tag = face.id;
let tri_face = ctx.brep.surface.tri_face();
ctx.tree.closest(p, &|t| tri_face.get(t as usize) == Some(&tag)).map(|h| h.point)
ctx.tree.closest(p, &|t| ctx.brep.tag(t) == Some(tag)).map(|h| h.point)
}
/// The point on every listed face (their common curve or corner) nearest `p`, by alternating
/// projections.
pub(crate) fn project_to_faces(ctx: &Ctx, faces: &[usize], p: P) -> P {
let pairs: Vec<(&Ctx, usize)> = faces.iter().map(|&f| (ctx, f)).collect();
project_alternating(&pairs, ctx.tol, p)
}
/// The point on every listed face of the listed cells nearest `p`, by alternating projections
/// until a round moves it less than a thousandth of `tol`.
fn project_alternating(faces: &[(&Ctx, usize)], tol: f64, p: P) -> P {
let mut x = p;
for _ in 0..PROJECTION_ROUNDS {
let before = x;
for &f in faces {
for &(ctx, f) in faces {
if let Some(q) = project_to_face(ctx, f, x) {
x = q;
}
}
if faces.len() < 2 || dist(before, x) <= ctx.tol * 1e-3 {
if faces.len() < 2 || dist(before, x) <= tol * 1e-3 {
break;
}
}
x
}
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements;
/// boundary edges get theirs on the B-rep faces of the boundary quads around them.
fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &[BoundaryFace]) -> Result<Vec<[u32; 12]>> {
// B-rep faces at each boundary edge.
let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
for b in boundary {
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements
/// (of any cell); boundary edges get theirs on the B-rep faces of the boundary quads around them.
fn mid_edge_nodes(cells: &Cells, nodes: &mut Vec<P>, hexes: &[[u32; 8]], skin: &[Skin]) -> Result<Vec<[u32; 12]>> {
// B-rep faces (cell, face) at each boundary edge.
let mut edge_faces: HashMap<(u32, u32), Vec<(usize, usize)>> = HashMap::new();
for b in skin {
let Some(h) = hexes.get(b.element as usize) else { continue };
let c = hex_face(b.face as usize - 1).map(|i| h.get(i).copied().unwrap_or(0));
let f = ctx.brep.faces.binary_search_by_key(&b.brep_face, |x| x.id).map_err(|_| Error::Failed("boundary tag".into()))?;
for k in 0..4 {
let (a, z) = (c[k], c[(k + 1) % 4]);
let list = edge_faces.entry((a.min(z), a.max(z))).or_default();
if !list.contains(&f) {
list.push(f);
if !list.contains(&(b.cell, b.brep)) {
list.push((b.cell, b.brep));
}
}
}
let edges = ElementKind::Hex20.edges();
let tol = cells.ctxs.iter().map(|c| c.tol).fold(f64::INFINITY, f64::min);
let mut mid: HashMap<(u32, u32), u32> = HashMap::new();
let mut out = Vec::with_capacity(hexes.len());
for h in hexes {
@@ -533,7 +702,10 @@ fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &
let (a, b) = (nodes.get(i as usize).copied().unwrap_or([0.0; 3]), nodes.get(j as usize).copied().unwrap_or([0.0; 3]));
let p = scale(add(a, b), 0.5);
let p = match edge_faces.get(&key) {
Some(fs) => project_to_faces(ctx, fs, p),
Some(fs) => {
let pairs: Vec<(&Ctx, usize)> = fs.iter().filter_map(|&(c, f)| cells.ctxs.get(c).map(|ctx| (ctx, f))).collect();
project_alternating(&pairs, tol, p)
}
None => p,
};
nodes.push(p);
+188
View File
@@ -292,3 +292,191 @@ fn unsweepable_bodies_say_why_and_hostile_input_is_an_error() {
let m = hex_mesh(&s, &garbage, &UniformSize(1.0), &HexOptions::default()).unwrap();
assert_eq!(m.element_count(), 24);
}
// ---- Conforming cells ----
/// A prism over a counter-clockwise polygon from z0 to z1 whose side from vertex k to k + 1 gets
/// tag `side_tags[k]` (bottom 0, top 1); quads of one tag make one face.
fn tagged_prism(poly: &[[f64; 2]], side_tags: &[u32], z0: f64, z1: f64) -> BoundarySurface {
let n = poly.len();
let mut pts: Vec<[f64; 3]> = poly.iter().map(|p| [p[0], p[1], z0]).collect();
pts.extend(poly.iter().map(|p| [p[0], p[1], z1]));
let mut tris = Vec::new();
let mut tags = Vec::new();
for t in ears(&pts, &(0..n as u32).rev().collect::<Vec<_>>()) {
tris.push(t);
tags.push(0);
}
for t in ears(&pts, &(n as u32..2 * n as u32).collect::<Vec<_>>()) {
tris.push(t);
tags.push(1);
}
for (k, &tag) in side_tags.iter().enumerate().take(n) {
let (a, b) = (k as u32, ((k + 1) % n) as u32);
let (c, d) = (b + n as u32, a + n as u32);
tris.push([a, b, c]);
tris.push([a, c, d]);
tags.extend([tag, tag]);
}
BoundarySurface::new(&pts, &tris, &tags, &SurfaceOptions::default()).unwrap()
}
fn cells_of<'a>(surfaces: &'a [BoundarySurface], faces: &'a [Vec<Option<Surface>>]) -> Vec<Cell<'a>> {
surfaces.iter().zip(faces).map(|(surface, faces)| Cell { name: "cell", surface, faces }).collect()
}
/// Boxes `[x0, x1] × [y0, y1] × [0, h]` as prisms (faces 0 bottom, 1 top, 2..6 the sides).
fn boxes(spans: &[([f64; 2], [f64; 2])], h: f64) -> Vec<BoundarySurface> {
spans.iter().map(|(x, y)| prism(&[[x[0], y[0]], [x[1], y[0]], [x[1], y[1]], [x[0], y[1]]], 0.0, h)).collect()
}
/// Every node of the mesh at a distinct position: the cells' nodes were merged.
fn distinct_positions(m: &HexMesh) -> bool {
let mut keys: Vec<[u64; 3]> = m.nodes.iter().map(|p| p.map(|x| (x + 0.0).to_bits())).collect();
keys.sort_unstable();
keys.windows(2).all(|w| w[0] != w[1])
}
#[test]
fn boxes_side_by_side_conform() {
// Two boxes sharing x = 2, then a 2 × 2 arrangement whose middle edge four cells share.
let pairs = boxes(&[([0.0, 2.0], [0.0, 1.0]), ([2.0, 3.0], [0.0, 1.0])], 1.0);
let none = vec![Vec::new(); 2];
let m = hex_mesh_cells(&cells_of(&pairs, &none), &UniformSize(0.5), &HexOptions::default()).unwrap();
assert_eq!(m.interfaces, 1);
assert_eq!(m.blocks.iter().map(|b| b[0] * b[1] * b[2]).sum::<usize>(), m.mesh.element_count());
assert_eq!(m.mesh.element_count(), 6 * 2 * 2);
// One structured grid: 7 × 3 × 3 nodes, none duplicated on the interface.
assert_eq!(m.mesh.nodes.len(), 7 * 3 * 3);
assert!(distinct_positions(&m.mesh));
assert!((m.mesh.report.volume - 3.0).abs() < 1e-12);
assert!(m.mesh.report.gate.passed());
// The boundary is the outside only: 2·(6·2 + 6·2 + 2·2) quads.
assert_eq!(m.mesh.boundary.len(), 56);
assert_eq!(m.element_cell.iter().filter(|&&c| c == 0).count(), 16);
let grid = boxes(&[([0.0, 1.0], [0.0, 1.0]), ([1.0, 2.0], [0.0, 1.0]), ([0.0, 1.0], [1.0, 2.0]), ([1.0, 2.0], [1.0, 2.0])], 1.0);
let none = vec![Vec::new(); 4];
let a = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions::default()).unwrap();
assert_eq!(a.interfaces, 4);
assert_eq!(a.mesh.nodes.len(), 9 * 9 * 5);
assert!(distinct_positions(&a.mesh));
// Deterministic, and hex20 shares its mid-edge nodes across the interfaces too.
let b = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions::default()).unwrap();
assert_eq!(a, b);
let q = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions { quadratic: true, ..HexOptions::default() }).unwrap();
assert_eq!(q.mesh.kind, ElementKind::Hex20);
// Corner nodes plus one node per lattice edge: 9·9·5 + (8·9·5)·2 + 9·9·4.
assert_eq!(q.mesh.nodes.len(), 405 + 720 + 324);
assert!(distinct_positions(&q.mesh));
}
#[test]
fn cells_that_do_not_meet_face_to_face_are_refused() {
// One box against two half-height boxes: its face at x = 1 meets two faces (a T-junction).
let t = vec![
prism(&[[0.0, 0.0], [1.0, 0.0], [1.0, 2.0], [0.0, 2.0]], 0.0, 1.0),
prism(&[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]], 0.0, 1.0),
prism(&[[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0]], 0.0, 1.0),
];
let none = vec![Vec::new(); 3];
match hex_mesh_cells(&cells_of(&t, &none), &UniformSize(0.5), &HexOptions::default()) {
Err(Error::Failed(why)) => assert!(why.contains("interface") || why.contains("conform"), "{why}"),
other => panic!("{other:?}"),
}
// A cell that is no logical cube says which.
let l = vec![prism(&[[0.0, 0.0], [6.0, 0.0], [6.0, 2.0], [2.0, 2.0], [2.0, 6.0], [0.0, 6.0]], 0.0, 2.0)];
match hex_mesh_cells(&cells_of(&l, &[Vec::new()]), &UniformSize(1.0), &HexOptions::default()) {
Err(Error::Unsupported(why)) => assert!(why.starts_with("cell `cell`: not a logical cube"), "{why}"),
other => panic!("{other:?}"),
}
}
#[test]
fn hostile_cell_input_is_an_error() {
let two = boxes(&[([0.0, 1.0], [0.0, 1.0]), ([1.0, 2.0], [0.0, 1.0])], 1.0);
let none = vec![Vec::new(); 2];
let cells = cells_of(&two, &none);
assert!(hex_mesh_cells(&[], &UniformSize(1.0), &HexOptions::default()).is_err());
for method in [HexMethod::Sweep, HexMethod::Extrude, HexMethod::Revolve, HexMethod::Swept] {
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(1.0), &HexOptions { method, ..HexOptions::default() }), Err(Error::Invalid(_))));
}
let bad_sizes: [&dyn crate::size::SizeField; 3] = [&UniformSize(f64::NAN), &UniformSize(0.0), &UniformSize(-1.0)];
for f in bad_sizes {
assert!(hex_mesh_cells(&cells, f, &HexOptions::default()).is_err());
}
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(1e-4), &HexOptions::default()), Err(Error::Limit(_))));
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(0.1), &HexOptions { max_elements: 100, ..HexOptions::default() }), Err(Error::Limit(_))));
assert!(hex_mesh_cells(&cells, &UniformSize(1.0), &HexOptions { chordal_tolerance: -1.0, ..HexOptions::default() }).is_err());
// The same cell twice: two cells on the same side of every face.
let same = [two[0].clone(), two[0].clone()];
assert!(hex_mesh_cells(&cells_of(&same, &none), &UniformSize(0.5), &HexOptions::default()).is_err());
// Garbage analytic surfaces are not trusted.
let garbage = vec![vec![Some(Surface::Cylinder { origin: [f64::NAN; 3], axis: [0.0; 3], radius: -1.0 }); 6]; 2];
let m = hex_mesh_cells(&cells_of(&two, &garbage), &UniformSize(0.5), &HexOptions::default()).unwrap();
assert_eq!(m.mesh.element_count(), 16);
}
/// The four cells of an O-grid round a hole of radius 1 in the square [−2, 2]², height `h`: each
/// cell the hole's quarter between two diagonals and a side of the square. The hole's quarter is
/// one face (tag 5) or two halves of the cylinder (tags 5 and 6: virtual topology joins them).
fn o_grid(h: f64, arc_segments: usize, split_arcs: bool) -> (Vec<BoundarySurface>, Vec<Vec<Option<Surface>>>) {
use std::f64::consts::FRAC_PI_4;
let mut surfaces = Vec::new();
let mut faces = Vec::new();
for q in 0..4 {
let turn = q as f64 * 2.0 * FRAC_PI_4;
let rot = |p: [f64; 2]| [p[0] * turn.cos() - p[1] * turn.sin(), p[0] * turn.sin() + p[1] * turn.cos()];
let mut poly = vec![rot([FRAC_PI_4.cos(), -FRAC_PI_4.sin()]), rot([2.0, -2.0]), rot([2.0, 2.0])];
let mut tags = vec![2, 3, 4];
for k in 0..arc_segments {
let t = FRAC_PI_4 - 2.0 * FRAC_PI_4 * k as f64 / arc_segments as f64;
poly.push(rot([t.cos(), t.sin()]));
tags.push(if split_arcs && 2 * k >= arc_segments { 6 } else { 5 });
}
// Exact corners where the cells meet.
let snap = |p: [f64; 2]| p.map(|x| if (x.abs() - 2.0).abs() < 1e-12 { 2.0 * x.signum() } else { x });
let poly: Vec<[f64; 2]> = poly.into_iter().map(snap).collect();
surfaces.push(tagged_prism(&poly, &tags, 0.0, h));
let cyl = Some(Surface::Cylinder { origin: [0.0; 3], axis: [0.0, 0.0, 1.0], radius: 1.0 });
faces.push(vec![None, None, None, None, None, cyl, cyl]);
}
(surfaces, faces)
}
#[test]
fn an_o_grid_round_a_hole_conforms_with_graded_radial_lines() {
for split_arcs in [false, true] {
let (surfaces, faces) = o_grid(0.5, 24, split_arcs);
let opts = HexOptions { chordal_tolerance: 1.0 - (std::f64::consts::PI / 48.0).cos(), ..HexOptions::default() };
let m = hex_mesh_cells(&cells_of(&surfaces, &faces), &UniformSize(0.5), &opts).unwrap();
assert_eq!(m.interfaces, 4, "{split_arcs}");
assert!(m.mesh.report.gate.passed());
assert!(m.mesh.report.min_scaled_jacobian > 0.3, "{}", m.mesh.report.min_scaled_jacobian);
assert!(distinct_positions(&m.mesh));
// A ring of quads round the hole: every block is (around) × (radial) × (through).
let sorted = |b: [usize; 3]| {
let mut b = b;
b.sort_unstable();
b
};
assert!(m.blocks.iter().all(|&b| sorted(b) == sorted(m.blocks[0])), "{:?}", m.blocks);
let per_block = m.blocks[0].iter().product::<usize>();
assert_eq!(m.mesh.element_count(), 4 * per_block);
// Nodes on the hole lie on it.
let hole: Vec<u32> = if split_arcs { vec![5, 6] } else { vec![5] };
assert!(on_radius(&m.mesh, &hole, 1.0), "{split_arcs}");
let on_hole = m.mesh.boundary.iter().filter(|b| hole.contains(&b.brep_face)).count();
assert!(on_hole > 0);
// Radial grading: the first spacing off the hole is smaller than the last at the square.
let radii: std::collections::BTreeSet<u64> =
m.mesh.nodes.iter().filter(|p| p[2] == 0.0 && p[1].abs() < 1e-12 && p[0] > 0.0).map(|p| p[0].to_bits()).collect();
let r: Vec<f64> = radii.into_iter().map(f64::from_bits).collect();
assert!(r.len() >= 3, "{r:?}");
let first = r[1] - r[0];
let last = r[r.len() - 1] - r[r.len() - 2];
assert!(first < 0.75 * last, "{r:?}");
// Spacing grows by at most the growth rate plus the rounding of the count.
assert!(r.windows(3).all(|w| (w[2] - w[1]) <= 1.35 * (w[1] - w[0])), "{r:?}");
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ fn dist<const D: usize>(a: &[f64; D], b: &[f64; D]) -> f64 {
}
/// Normalized cumulative arc length of a side, from 0 to 1.
fn params<const D: usize>(side: &[[f64; D]], name: &str) -> Result<Vec<f64>> {
pub(crate) fn params<const D: usize>(side: &[[f64; D]], name: &str) -> Result<Vec<f64>> {
let mut acc = Vec::with_capacity(side.len());
let mut s = 0.0;
acc.push(0.0);
+12 -2
View File
@@ -880,7 +880,7 @@ input points), Steiner points kept half the local spacing from the boundary.
### B8. Mapped and swept hexahedral meshing (implemented in `fieldcraft-mesh::hex`)
`mesh.hex.generate` (and `mesh.hex.sweep`, `mesh.hex.mapped`). Every method below is implemented from
`mesh.hex.generate` (and `mesh.hex.sweep`, `mesh.hex.mapped`, `mesh.hex.blocks`). Every method below is implemented from
its paper; no code was read. DOIs marked "checked" were confirmed on 2026-10-13 through a repository
record or the publisher; the others are cited by venue only.
@@ -932,7 +932,17 @@ record or the publisher; the others are cited by venue only.
Applied Mathematics and Computation 10–11:171–233, 1982.
- **Use.** Interior nodes of a logical cube from its six face grids (`hex::mapped`).
**B8.8 Support.** Generalized winding number for the inside test (A. Jacobson, L. Kavan, O. Sorkine-Hornung,
**B8.8 Multi-block meshing of partitioned parts.** **Verdict: BUILD ON (conforming cells).**
- **Citations.** J. F. Thompson, B. K. Soni, N. P. Weatherill (eds.). *Handbook of Grid Generation.* CRC Press,
1999, ch. 3 (transfinite and algebraic grids) and ch. 13 (multiblock grids). A. Sheffer, M. Bercovier,
T. Blacker, J. Clements. *Virtual topology operators for meshing.* IJCGA 10(3):309–331, 2000.
- **Use.** `hex::cells` (`mesh.hex.blocks`): cells that meet face to face, each a logical cube after smoothly
meeting faces are joined into composite faces (virtual topology); vertices, edges and faces shared by
identity; interval assignment over all cells (B8.5); each edge divided once, each face's transfinite grid
made once and used by both cells; edges that leave a curved face graded from the spacing along it with a
growth of 1.2 per element; interiors by B8.7. The merged mesh is checked for free internal faces.
**B8.9 Support.** Generalized winding number for the inside test (A. Jacobson, L. Kavan, O. Sorkine-Hornung,
*Robust inside-outside segmentation using generalized winding numbers*, ACM TOG 32(4):33, 2013; solid angle
of a triangle: Van Oosterom and Strackee 1983); closest point on a triangle (C. Ericson, *Real-Time
Collision Detection*, 2005, §5.1.5).