forked from 1b-agent/fieldcraft-upstream
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
This commit is contained in:
1 parent
741a803785
commit
3587b0de74
12 files changed
+3676
-7
No files matched your search
@@ -0,0 +1,98 @@
|
||||
use super::*;
|
||||
use crate::octree::geom::tests::box_surface;
|
||||
use crate::size::UniformSize;
|
||||
|
||||
#[test]
|
||||
fn a_box_meshes_with_exact_volume_and_tagged_faces() {
|
||||
let s = box_surface([0.0; 3], [10.0, 8.0, 6.0]);
|
||||
let m = octree_hex_mesh(&s, &UniformSize(2.0), &OctreeOptions::default()).unwrap();
|
||||
let r = &m.report;
|
||||
assert!((r.volume - 480.0).abs() < 1e-9 * 480.0, "{r:?}");
|
||||
assert!(r.min_scaled_jacobian > 0.3, "{r:?}");
|
||||
let faces: BTreeSet<u32> = m.boundary.iter().map(|b| b.brep_face).collect();
|
||||
assert_eq!(faces.len(), 6);
|
||||
// Each boundary quad lies in the plane of its face.
|
||||
for b in &m.boundary {
|
||||
let hx = m.hexes[b.element as usize];
|
||||
let q = topo::FACES[(b.face - 1) as usize].map(|i| m.nodes[hx[i] as usize]);
|
||||
let (axis, value) = match b.brep_face {
|
||||
0 => (2, 0.0),
|
||||
1 => (2, 6.0),
|
||||
2 => (1, 0.0),
|
||||
3 => (1, 8.0),
|
||||
4 => (0, 0.0),
|
||||
_ => (0, 10.0),
|
||||
};
|
||||
assert!(q.iter().all(|p| (p[axis] - value).abs() < 1e-9), "{b:?} {q:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A closed cylinder of radius `r` and height `h` on the z axis, `n` segments: faces 0 (bottom
|
||||
/// cap), 1 (top cap), 2 (side).
|
||||
pub(crate) fn cylinder_surface(r: f64, h: f64, n: usize) -> BoundarySurface {
|
||||
let mut p = vec![[0.0, 0.0, 0.0], [0.0, 0.0, h]];
|
||||
for z in [0.0, h] {
|
||||
for i in 0..n {
|
||||
let a = std::f64::consts::TAU * i as f64 / n as f64;
|
||||
p.push([r * a.cos(), r * a.sin(), z]);
|
||||
}
|
||||
}
|
||||
let (b, t) = (|i: usize| (2 + i % n) as u32, |i: usize| (2 + n + i % n) as u32);
|
||||
let mut tris = Vec::new();
|
||||
let mut face = Vec::new();
|
||||
for i in 0..n {
|
||||
tris.push([0, b(i + 1), b(i)]);
|
||||
face.push(0);
|
||||
tris.push([1, t(i), t(i + 1)]);
|
||||
face.push(1);
|
||||
tris.push([b(i), b(i + 1), t(i + 1)]);
|
||||
tris.push([b(i), t(i + 1), t(i)]);
|
||||
face.extend([2, 2]);
|
||||
}
|
||||
BoundarySurface::new(&p, &tris, &face, &crate::surface::SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
/// A UV sphere of radius `r`, one face.
|
||||
pub(crate) fn sphere_surface(r: f64, n: usize) -> BoundarySurface {
|
||||
let rings = n / 2;
|
||||
let mut p = vec![[0.0, 0.0, -r], [0.0, 0.0, r]];
|
||||
for j in 1..rings {
|
||||
let th = std::f64::consts::PI * j as f64 / rings as f64 - std::f64::consts::FRAC_PI_2;
|
||||
for i in 0..n {
|
||||
let a = std::f64::consts::TAU * i as f64 / n as f64;
|
||||
p.push([r * th.cos() * a.cos(), r * th.cos() * a.sin(), r * th.sin()]);
|
||||
}
|
||||
}
|
||||
let id = |j: usize, i: usize| (2 + (j - 1) * n + i % n) as u32;
|
||||
let mut tris = Vec::new();
|
||||
for i in 0..n {
|
||||
tris.push([0, id(1, i + 1), id(1, i)]);
|
||||
tris.push([1, id(rings - 1, i), id(rings - 1, i + 1)]);
|
||||
}
|
||||
for j in 1..rings - 1 {
|
||||
for i in 0..n {
|
||||
tris.push([id(j, i), id(j, i + 1), id(j + 1, i + 1)]);
|
||||
tris.push([id(j, i), id(j + 1, i + 1), id(j + 1, i)]);
|
||||
}
|
||||
}
|
||||
let face = vec![0; tris.len()];
|
||||
BoundarySurface::new(&p, &tris, &face, &crate::surface::SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cylinder_meshes_with_its_rims_captured() {
|
||||
let s = cylinder_surface(5.0, 8.0, 64);
|
||||
let m = octree_hex_mesh(&s, &UniformSize(2.0), &OctreeOptions::default()).unwrap();
|
||||
let r = &m.report;
|
||||
assert!(((r.volume - r.surface_volume) / r.surface_volume).abs() < 0.01, "{r:?}");
|
||||
let faces: BTreeSet<u32> = m.boundary.iter().map(|b| b.brep_face).collect();
|
||||
assert_eq!(faces.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sphere_meshes() {
|
||||
let s = sphere_surface(5.0, 48);
|
||||
let m = octree_hex_mesh(&s, &UniformSize(2.0), &OctreeOptions::default()).unwrap();
|
||||
let r = &m.report;
|
||||
assert!(((r.volume - r.surface_volume) / r.surface_volume).abs() < 0.01, "{r:?}");
|
||||
}
|
||||
Reference in new issue
Block a user