Files
OneCAD-Simulation/crates/mesh/src/octree/templates.rs
T
jyvikoandClaude Opus 5.5 3587b0de74 mesh: unstructured all-hex octree mesher
`fieldcraft_mesh::octree::octree_hex_mesh` meshes any closed part with hex8 only:

- an adaptive 3-refinement grid (27-tree) with strong balance and closure, filled with
  conforming node-marking transition templates (Schneiders; checked over all 48 symmetries);
- a core of grid cells kept clear of the boundary (the HybridOctree buffer rule), with
  non-manifold cleanup and a genus check against the part;
- a feature graph from the B-rep (corners, curves, patches) and a partition-first map of the
  core's boundary onto it, then two boundary layers: a padding layer and a pillow per patch,
  so every face boundary and sharp edge is captured by element edges;
- a Garanzha-type untangling and fitting energy (shape term |J|^3/chi, a volume term and a
  soft fitting term, deterministic across platforms), a compass search on the worst
  elements and a certified repair; failures refine the grid near them and rebuild, within
  bounded attempts and growth.

The output passes the certified Jacobian check of every element and a gate that the
boundary encloses the elements' volume; boundary quads carry their B-rep faces. Quality is
reported with the same scaled Jacobian as mesh.quality. Literature notes for each step are in
docs/meshing/literature.md (B1.6-B1.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:38:11 -04:00

375 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The 3-refinement transition templates: how a leaf of the balanced tree is cut into
//! hexahedra so that it conforms to finer neighbours.
//!
//! Every vertex of a leaf is *marked* when a finer leaf touches it. Under strong balance the
//! marked set of a leaf is one of five patterns (after the closure in [`super::grid`] has split
//! every leaf that had another one), each with one template:
//!
//! | marks | template | hexahedra |
//! |---|---|---|
//! | none | the leaf itself | 1 |
//! | one corner | corner cube and three blocks fanning to the far corner | 4 |
//! | the two ends of an edge | three cubes along the edge and two four-hex slabs | 11 |
//! | the four corners of a face | nine cubes on the face, nine above them, four on top | 22 |
//! | all eight | the 3 × 3 × 3 split | 27 |
//!
//! A face of a leaf is cut according to its own marks only (none, one corner, one edge, all
//! four), and an edge into thirds at its marked ends, so two leaves sharing a face cut it the same
//! way: the mesh is conforming by construction. This is the node-marking form of Schneiders'
//! 3-refinement (R. Schneiders, *Refining quadrilateral and hexahedral element meshes*, NUMGRID
//! 1996; Y. Ito, A. M. Shih, B. K. Soni, *Octree-based reasonable-quality hexahedral mesh
//! generation using a new set of refinement templates*, IJNME 77(13):1809–1833, 2009). The
//! templates are our own construction from those papers' description: the face template's top
//! block is the 3 × 3 → 1 transition traced from its dual sheets, the edge template is two
//! copies of the planar 3 → 1 transition swept across the leaf, and the tests prove every
//! template's faces, volume and validity.
//!
//! Coordinates are integers in sixths of the leaf's edge, so every vertex shared with a neighbour
//! lies on the global integer lattice of [`super::grid`] and coincident vertices merge exactly.
/// A point of a leaf in sixths of its edge, `0..=6` per axis.
pub(crate) type Local = [i64; 3];
/// A hexahedron in the model's node order (corners 1-2-3-4 then 5-6-7-8 above them).
pub(crate) type Hex = [Local; 8];
/// The marked-corner pattern of a leaf, up to symmetry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pattern {
None,
Corner,
Edge,
Face,
Full,
}
/// A symmetry of the cube: coordinate `k` of the image is coordinate `perm[k]` of the point,
/// reflected (`6 − v`) when `flip[k]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Sym {
perm: [usize; 3],
flip: [bool; 3],
}
impl Sym {
pub(crate) fn apply(&self, p: Local) -> Local {
let mut q = [0; 3];
for (k, out) in q.iter_mut().enumerate() {
let v = p[self.perm[k]];
*out = if self.flip[k] { 6 - v } else { v };
}
q
}
}
/// The 48 symmetries in a fixed order.
pub(crate) fn symmetries() -> Vec<Sym> {
let perms = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
let mut out = Vec::with_capacity(48);
for perm in perms {
for bits in 0..8u8 {
out.push(Sym { perm, flip: [bits & 1 != 0, bits & 2 != 0, bits & 4 != 0] });
}
}
out
}
/// The leaf corner `c` (bit 0: x, bit 1: y, bit 2: z) in sixths.
pub(crate) fn corner(c: usize) -> Local {
[(c & 1) as i64 * 6, ((c >> 1) & 1) as i64 * 6, ((c >> 2) & 1) as i64 * 6]
}
/// The corner index of a corner point.
fn corner_index(p: Local) -> usize {
usize::from(p[0] == 6) | (usize::from(p[1] == 6) << 1) | (usize::from(p[2] == 6) << 2)
}
/// The image of a corner mask under a symmetry.
fn map_mask(sym: &Sym, mask: u8) -> u8 {
(0..8).filter(|c| mask & (1 << c) != 0).fold(0u8, |m, c| m | (1 << corner_index(sym.apply(corner(c)))))
}
/// The pattern of a corner mask, or `None` when no template fits it.
pub(crate) fn classify(mask: u8) -> Option<Pattern> {
let corners: Vec<Local> = (0..8).filter(|c| mask & (1 << c) != 0).map(corner).collect();
let shared = |n: usize| (0..3).filter(|&k| corners.iter().all(|p| p[k] == corners[0][k])).count() == n;
match corners.len() {
0 => Some(Pattern::None),
1 => Some(Pattern::Corner),
2 if shared(2) => Some(Pattern::Edge),
4 if shared(1) => Some(Pattern::Face),
8 => Some(Pattern::Full),
_ => None,
}
}
/// The canonical mask of a pattern: corner 0; edge 0–1 (along x); face z = 0.
fn canonical_mask(p: Pattern) -> u8 {
match p {
Pattern::None => 0,
Pattern::Corner => 0b1,
Pattern::Edge => 0b11,
Pattern::Face => 0b1111,
Pattern::Full => 0xff,
}
}
/// The first symmetry (in [`symmetries`] order) taking the pattern's canonical mask to `mask`.
pub(crate) fn orientation(syms: &[Sym], pattern: Pattern, mask: u8) -> Option<Sym> {
let c = canonical_mask(pattern);
syms.iter().find(|s| map_mask(s, c) == mask).copied()
}
/// Thirds to sixths.
fn t(x: i64, y: i64, z: i64) -> Local {
[2 * x, 2 * y, 2 * z]
}
/// A thirds-aligned cube with lower corner `(x, y, z)` (in thirds) and edge `e` thirds.
fn cube(x: i64, y: i64, z: i64, e: i64) -> Hex {
let (a, b) = (t(x, y, z), t(x + e, y + e, z + e));
[
[a[0], a[1], a[2]],
[b[0], a[1], a[2]],
[b[0], b[1], a[2]],
[a[0], b[1], a[2]],
[a[0], a[1], b[2]],
[b[0], a[1], b[2]],
[b[0], b[1], b[2]],
[a[0], b[1], b[2]],
]
}
/// The planar 3 → 1 transition in `(x, s)`: x in thirds, `s` in halves of the transition height,
/// three segments at `s = 0` and one at `s = 2`.
const THREE_TO_ONE: [[(i64, i64); 4]; 4] =
[[(0, 0), (1, 0), (1, 1), (0, 2)], [(1, 0), (2, 0), (2, 1), (1, 1)], [(2, 0), (3, 0), (3, 2), (2, 1)], [(1, 1), (2, 1), (3, 2), (0, 2)]];
/// The canonical template of a pattern, in sixths of the leaf.
pub(crate) fn template(p: Pattern) -> Vec<Hex> {
match p {
Pattern::None => vec![cube(0, 0, 0, 3)],
Pattern::Full => (0..27).map(|k| cube(k % 3, (k / 3) % 3, k / 9, 1)).collect(),
Pattern::Corner => vec![
cube(0, 0, 0, 1),
[t(1, 0, 0), t(3, 0, 0), t(3, 3, 0), t(1, 1, 0), t(1, 0, 1), t(3, 0, 3), t(3, 3, 3), t(1, 1, 1)],
[t(0, 1, 0), t(1, 1, 0), t(3, 3, 0), t(0, 3, 0), t(0, 1, 1), t(1, 1, 1), t(3, 3, 3), t(0, 3, 3)],
[t(0, 0, 1), t(1, 0, 1), t(1, 1, 1), t(0, 1, 1), t(0, 0, 3), t(3, 0, 3), t(3, 3, 3), t(0, 3, 3)],
],
Pattern::Edge => {
let mut out: Vec<Hex> = (0..3).map(|i| cube(i, 0, 0, 1)).collect();
// Two slabs over the edge column's faces y = 1 and z = 1, each the 3 → 1 transition
// swept from the leaf face to the diagonal plane y = z.
for q in THREE_TO_ONE {
let b = |(x, s): (i64, i64), w: i64| t(x, 1 + s, w * (1 + s));
let c = |(x, s): (i64, i64), w: i64| t(x, w * (1 + s), 1 + s);
out.push([b(q[0], 0), b(q[1], 0), b(q[2], 0), b(q[3], 0), b(q[0], 1), b(q[1], 1), b(q[2], 1), b(q[3], 1)]);
out.push([c(q[0], 0), c(q[1], 0), c(q[2], 0), c(q[3], 0), c(q[0], 1), c(q[1], 1), c(q[2], 1), c(q[3], 1)]);
}
out
}
Pattern::Face => {
let mut out: Vec<Hex> = (0..9).map(|k| cube(k % 3, k / 3, 0, 1)).collect();
// The layer above: its top is the corners raised to the leaf's top, the side nodes of
// the 3 → 1 transitions at two thirds, the inside nodes at half height.
let top = |a: i64, b: i64| -> Local {
let edge = |v: i64| v == 0 || v == 3;
match (edge(a), edge(b)) {
(true, true) => [2 * a, 2 * b, 6],
(false, false) => [2 * a, 2 * b, 3],
_ => [2 * a, 2 * b, 4],
}
};
for k in 0..9 {
let (i, j) = (k % 3, k / 3);
out.push([
t(i, j, 1),
t(i + 1, j, 1),
t(i + 1, j + 1, 1),
t(i, j + 1, 1),
top(i, j),
top(i + 1, j),
top(i + 1, j + 1),
top(i, j + 1),
]);
}
// The four on top: the y-arch (low), the x-arch (high) and the two side blocks.
out.push([[2, 2, 3], [4, 2, 3], [4, 4, 3], [2, 4, 3], [2, 0, 4], [4, 0, 4], [4, 6, 4], [2, 6, 4]]);
out.push([[2, 0, 4], [4, 0, 4], [4, 6, 4], [2, 6, 4], [0, 0, 6], [6, 0, 6], [6, 6, 6], [0, 6, 6]]);
out.push([[2, 2, 3], [2, 4, 3], [2, 6, 4], [2, 0, 4], [0, 2, 4], [0, 4, 4], [0, 6, 6], [0, 0, 6]]);
out.push([[4, 2, 3], [4, 4, 3], [4, 6, 4], [4, 0, 4], [6, 2, 4], [6, 4, 4], [6, 6, 6], [6, 0, 6]]);
out
}
}
}
/// Six times the signed volume of the corner tetrahedron at node 0 (exact on the lattice).
pub(crate) fn corner0_orientation(h: &[[i64; 3]; 8]) -> i128 {
let e = |j: usize| [0, 1, 2].map(|k| i128::from(h[j][k] - h[0][k]));
let (a, b, c) = (e(1), e(3), e(4));
a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0])
}
/// The hexahedron with its orientation reversed (the same cell, mirrored node order).
pub(crate) fn reversed<T: Copy>(h: [T; 8]) -> [T; 8] {
[h[0], h[3], h[2], h[1], h[4], h[7], h[6], h[5]]
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::quality::{Validity, element_validity};
use fieldcraft_model::ElementKind;
/// The face of the leaf a quad lies on (axis, value), if any.
fn on_leaf_face(q: &[Local; 4]) -> Option<(usize, i64)> {
(0..3).find_map(|k| [0, 6].into_iter().find(|&v| q.iter().all(|p| p[k] == v)).map(|v| (k, v)))
}
fn sorted(mut q: [Local; 4]) -> [Local; 4] {
q.sort();
q
}
/// The quads a leaf face must carry for its four corner marks, as sorted vertex sets, on the
/// face `(axis, value)`.
fn face_template(axis: usize, value: i64, marks: [bool; 4]) -> Vec<[Local; 4]> {
// Face coordinates (u, v) are the other two axes in increasing order; corners in order
// (0,0), (6,0), (0,6), (6,6).
let (ua, va) = match axis {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let lift = |u: i64, v: i64| {
let mut p = [0; 3];
p[axis] = value;
p[ua] = u;
p[va] = v;
p
};
// Build in a canonical frame, then map the 2D symmetry that takes the canonical marks to
// these marks.
let count = marks.iter().filter(|m| **m).count();
let canon: Vec<[(i64, i64); 4]> = match count {
0 => vec![[(0, 0), (6, 0), (6, 6), (0, 6)]],
1 => vec![[(0, 0), (2, 0), (2, 2), (0, 2)], [(2, 0), (6, 0), (6, 6), (2, 2)], [(0, 2), (2, 2), (6, 6), (0, 6)]],
2 => {
let mut v: Vec<[(i64, i64); 4]> = (0..3).map(|i| [(2 * i, 0), (2 * i + 2, 0), (2 * i + 2, 2), (2 * i, 2)]).collect();
v.extend([
[(0, 2), (2, 2), (2, 4), (0, 6)],
[(2, 2), (4, 2), (4, 4), (2, 4)],
[(4, 2), (6, 2), (6, 6), (4, 4)],
[(2, 4), (4, 4), (6, 6), (0, 6)],
]);
v
}
_ => (0..9)
.map(|k| {
[(2 * (k % 3), 2 * (k / 3)), (2 * (k % 3) + 2, 2 * (k / 3)), (2 * (k % 3) + 2, 2 * (k / 3) + 2), (2 * (k % 3), 2 * (k / 3) + 2)]
})
.collect(),
};
let canon_marks: [bool; 4] = match count {
0 => [false; 4],
1 => [true, false, false, false],
2 => [true, true, false, false],
_ => [true; 4],
};
let maps: [fn(i64, i64) -> (i64, i64); 8] = [
|u, v| (u, v),
|u, v| (6 - u, v),
|u, v| (u, 6 - v),
|u, v| (6 - u, 6 - v),
|u, v| (v, u),
|u, v| (6 - v, u),
|u, v| (v, 6 - u),
|u, v| (6 - v, 6 - u),
];
let corners = [(0, 0), (6, 0), (0, 6), (6, 6)];
let idx = |c: (i64, i64)| corners.iter().position(|&x| x == c).unwrap();
let m =
maps.iter().find(|m| (0..4).all(|c| canon_marks[c] == marks[idx(m(corners[c].0, corners[c].1))])).expect("a face symmetry for the marks");
canon.iter().map(|q| sorted(q.map(|(u, v)| m(u, v)).map(|(u, v)| lift(u, v)))).collect()
}
fn face_corner_marks(axis: usize, value: i64, mask: u8) -> [bool; 4] {
let (ua, va) = match axis {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
[(0, 0), (6, 0), (0, 6), (6, 6)].map(|(u, v)| {
let mut p = [0; 3];
p[axis] = value;
p[ua] = u;
p[va] = v;
mask & (1 << corner_index(p)) != 0
})
}
/// Every template under every symmetry: valid elements, the leaf's volume, closed (each inner
/// face shared by two elements) and, on each leaf face, exactly the face template of that
/// face's marks.
#[test]
fn templates_are_valid_closed_and_conform_to_their_face_marks() {
let syms = symmetries();
for pattern in [Pattern::None, Pattern::Corner, Pattern::Edge, Pattern::Face, Pattern::Full] {
for sym in &syms {
let mask = map_mask(sym, canonical_mask(pattern));
assert_eq!(classify(mask), Some(pattern));
let hexes: Vec<Hex> = template(pattern)
.into_iter()
.map(|h| {
let h = h.map(|p| sym.apply(p));
if corner0_orientation(&h) < 0 { reversed(h) } else { h }
})
.collect();
let mut volume = 0.0;
let mut faces: HashMap<[Local; 4], usize> = HashMap::new();
for h in &hexes {
let xyz: Vec<[f64; 3]> = h.iter().map(|p| p.map(|v| v as f64)).collect();
let check = element_validity(ElementKind::Hex8, &xyz).unwrap();
assert_eq!(check.validity, Validity::Valid, "{pattern:?} {sym:?} {h:?}");
volume += check.volume;
for f in ElementKind::Hex8.faces() {
*faces.entry(sorted([h[f[0]], h[f[1]], h[f[2]], h[f[3]]])).or_default() += 1;
}
}
assert!((volume - 216.0).abs() < 1e-9, "{pattern:?}: volume {volume}");
let mut outer: HashMap<(usize, i64), Vec<[Local; 4]>> = HashMap::new();
for (q, n) in &faces {
match on_leaf_face(q) {
Some(f) => {
assert_eq!(*n, 1);
outer.entry(f).or_default().push(*q);
}
None => assert_eq!(*n, 2, "{pattern:?} {sym:?}: inner face {q:?} used {n} times"),
}
}
for axis in 0..3 {
for value in [0, 6] {
let mut got = outer.remove(&(axis, value)).unwrap_or_default();
let mut want = face_template(axis, value, face_corner_marks(axis, value, mask));
got.sort();
want.sort();
assert_eq!(got, want, "{pattern:?} {sym:?} face {axis}={value}");
}
}
}
}
}
#[test]
fn patterns_without_a_template_are_recognised() {
assert_eq!(classify(0b1001), None, "diagonal of a face");
assert_eq!(classify(0b1000_0001), None, "opposite corners");
assert_eq!(classify(0b0111), None, "three of a face");
assert_eq!(classify(0b0110_1001), None, "four not on a face");
assert_eq!(classify(0b0111_1111), None);
assert_eq!(classify(0b0011_0011), Some(Pattern::Face));
assert_eq!(classify(0b0101_0000), Some(Pattern::Edge));
}
}