mesh: pyramid transition surface and frozen faces in the tet mesher

transition::pyramid_surface puts a pyramid on every hex interface quad and
builds the tet region's surface closed by the pyramid sides, re-stitching the
neighbouring faces to the hex rim nodes. QualityOptions::frozen_faces keeps
those sides exactly (TetComplex refuses changes that remove or add a frozen
face; their vertices are corners), and apex_points checks the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 14:57:41 -04:00
1 parent 3da332be77
commit 68e9bcde92
4 files changed
+392 -4

No files matched your search

+20 -4
View File
@@ -195,6 +195,8 @@ pub struct TetComplex {
/// only at it, so stars never miss a piece).
vtets: Vec<Vec<u32>>,
curves: IdMap<(u32, u32), u32>,
/// Boundary face tags no change may remove or add a face of ([`Self::freeze_faces`]).
frozen: Vec<u32>,
mark: Vec<u32>,
epoch: u32,
live: usize,
@@ -276,6 +278,7 @@ impl TetComplex {
tags: vec![[NONE; 4]; tets.len()],
free: Vec::new(),
curves: IdMap::default(),
frozen: Vec::new(),
mark: vec![0; tets.len()],
epoch: 0,
live: tets.len(),
@@ -406,6 +409,19 @@ impl TetComplex {
e
}
/// Keep the boundary faces tagged with `tags` exactly as they are: [`Self::replace`] refuses
/// every change that would remove one of them (a split, flip or collapse) or add one. Their
/// vertices must also be [`VertexKind::Corner`]s, so nothing moves them.
pub fn freeze_faces(&mut self, tags: &[u32]) {
self.frozen = tags.to_vec();
self.frozen.sort_unstable();
self.frozen.dedup();
}
fn is_frozen(&self, tag: u32) -> bool {
self.frozen.binary_search(&tag).is_ok()
}
pub fn set_curve(&mut self, a: u32, b: u32, curve: Option<u32>) {
let key = sorted2(a, b);
let before = match curve {
@@ -881,13 +897,13 @@ impl TetComplex {
used_declared += usize::from(declared_face.is_some());
if let Some((n, back, tag, seen)) = outside.remove(&key) {
// The new tetrahedron lies on the side of the face the old one did.
if !same_cycle(face, seen) {
if !same_cycle(face, seen) || (n == NONE && self.is_frozen(tag) && declared_face.is_some_and(|d| d.1 != tag)) {
return Ok(None);
}
links.push((i, k, if n == NONE { Link::Boundary(declared_face.map_or(tag, |d| d.1)) } else { Link::Old(n, back) }));
} else if let Some((oriented, tag)) = declared_face {
// A new boundary face, oriented as declared (into the mesh).
if !same_cycle(face, oriented) {
if !same_cycle(face, oriented) || self.is_frozen(tag) {
return Ok(None);
}
links.push((i, k, Link::Boundary(tag)));
@@ -898,8 +914,8 @@ impl TetComplex {
if used_declared != declared.len() {
return Ok(None);
}
// What is left outside must be boundary faces the change removes.
if outside.values().any(|(n, _, _, _)| *n != NONE) || (!outside.is_empty() && change.new_boundary.is_empty()) {
// What is left outside must be boundary faces the change removes, and none frozen.
if outside.values().any(|(n, _, tag, _)| *n != NONE || self.is_frozen(*tag)) || (!outside.is_empty() && change.new_boundary.is_empty()) {
return Ok(None);
}
// Feature edges strictly inside the old region must survive (or be the split one).
+14
View File
@@ -97,6 +97,11 @@ pub struct QualityOptions {
/// grows from it at the grading rate. A seed finer than the global size refines along its
/// edge; one coarser leaves it to the global size.
pub seeds: Vec<crate::seeds::EdgeSeed>,
/// B-rep face tags whose boundary triangles are kept exactly: their vertices stay where they
/// are and no triangle of theirs is split, flipped or removed. The faces a pyramid
/// transition shares with the tetrahedra ([`crate::transition`]) are frozen, so the two
/// meshes stay conforming.
pub frozen_faces: Vec<u32>,
}
impl Default for QualityOptions {
@@ -117,6 +122,7 @@ impl Default for QualityOptions {
max_tets: 5_000_000,
max_points: 1_000_000,
seeds: Vec::new(),
frozen_faces: Vec::new(),
}
}
}
@@ -359,6 +365,8 @@ pub fn complex_from_cdt(surface: &BoundarySurface, cdt: &TetMesh, o: &QualityOpt
// Vertex kinds from the boundary faces and curve edges at each vertex.
let mut face_tag: Vec<Option<u32>> = vec![None; cdt.points.len()];
let mut mixed = vec![false; cdt.points.len()];
let mut frozen = vec![false; cdt.points.len()];
cx.freeze_faces(&o.frozen_faces);
for (t, vs) in cx.live_tets() {
let (ns, tg) = (cx.adj(t)?, cx.tags(t)?);
for k in 0..4 {
@@ -367,6 +375,11 @@ pub fn complex_from_cdt(surface: &BoundarySurface, cdt: &TetMesh, o: &QualityOpt
}
for v in face_toward(vs, k) {
let i = v as usize;
if o.frozen_faces.contains(&tg[k])
&& let Some(f) = frozen.get_mut(i)
{
*f = true;
}
match face_tag.get(i).copied().flatten() {
None => {
if let Some(s) = face_tag.get_mut(i) {
@@ -390,6 +403,7 @@ pub fn complex_from_cdt(surface: &BoundarySurface, cdt: &TetMesh, o: &QualityOpt
}
for v in 0..cdt.points.len() as u32 {
let kind = match (face_tag.get(v as usize).copied().flatten(), curve_count.get(&v).map(Vec::as_slice)) {
_ if frozen.get(v as usize).copied().unwrap_or(false) => VertexKind::Corner,
(None, _) => VertexKind::Interior,
(Some(tag), None) if !mixed.get(v as usize).copied().unwrap_or(true) => VertexKind::Face(tag),
(Some(_), Some(&[c1, c2])) if c1 == c2 => VertexKind::Curve(c1),