Files
OneCAD-Simulation/crates/mesh/src/octree/tests.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

99 lines
3.7 KiB
Rust

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:?}");
}