forked from 1b-agent/fieldcraft-upstream
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:
1 parent
8ca2dddeec
commit
b6ce7aee69
8 files changed
+1439
-81
No files matched your search
+117
-5
@@ -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()
|
||||
}
|
||||
@@ -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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user