mesh: tag each projected quad with the face it lies on, slivers included

project_trimmed accepted the exact surface's point up to a fifth of the
nearest triangle's median edge off the face: on a tall fillet piece
(triangles 30 mm long) that let a point on a sibling piece of the same
cylinder count as on it, so every quad round the corner took the first
piece's name. The slack is now the triangle's own chordal gap to the
surface, doubled.

A face narrower than the elements (the sliver a corner boss leaves of the
inner fillet) gets one row of the quads it runs through, from the
neighbour with the most quads left. Tessellation::nearest_face finds the
face nearest a point.

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 10:38:31 -04:00
1 parent fcc068bce9
commit 18dbc29357
2 files changed
+108 -21

No files matched your search

+61
View File
@@ -136,6 +136,63 @@ pub struct ProjectedMesh {
pub report: ProjectReport, pub report: ProjectReport,
} }
/// Give each target face no boundary quad's centre landed on (a face narrower than the elements:
/// a fillet's sliver between a cut and the walls) one row of the quads it runs through (of a
/// target holding the face, centre within half the quad's shortest side of it): one quad of
/// those beside each other across the face (projecting onto it at nearly the same point), all
/// along it. A quad keeps its face when it is that face's
/// last, so no face loses its only quad to a neighbour.
fn claim_narrow_faces(boundary: &mut [BoundaryFace], centres: &[(P, f64, usize)], targets: &[Vec<u32>], g: &dyn Geometry) {
let mut count: BTreeMap<u32, usize> = BTreeMap::new();
for b in boundary.iter() {
*count.entry(b.brep_face).or_insert(0) += 1;
}
let targeted: BTreeSet<u32> = targets.iter().flatten().copied().collect();
for f in targeted {
if count.contains_key(&f) {
continue;
}
// (distance to the face, quad, its centre, the centre's foot on the face, shortest side)
let mut near: Vec<(f64, usize, P, P, f64)> = centres
.iter()
.enumerate()
.filter(|(_, (_, _, tg))| targets.get(*tg).is_some_and(|set| set.contains(&f)))
.filter_map(|(i, (centre, shortest, _))| {
let foot = g.project(f, *centre)?;
let d = dist(foot, *centre);
(d <= 0.5 * shortest).then_some((d, i, *centre, foot, *shortest))
})
.collect();
near.sort_by(|a, b| a.0.total_cmp(&b.0));
// The rows across the face: quads beside each other that project onto it at nearly the
// same point.
let mut rows: Vec<Vec<(usize, P, P, f64)>> = Vec::new();
for (_, i, centre, foot, shortest) in near {
let across = |(_, c, q, s): &(usize, P, P, f64)| {
let apart = dist(*c, centre);
apart < 1.5 * s.max(shortest) && dist(*q, foot) < 0.5 * apart
};
match rows.iter_mut().find(|row| row.iter().any(across)) {
Some(row) => row.push((i, centre, foot, shortest)),
None => rows.push(vec![(i, centre, foot, shortest)]),
}
}
// One quad per row: from the neighbour with the most quads left (the nearest of equals),
// so neighbours on both sides give alike.
for row in rows {
let left = |i: usize| boundary.get(i).and_then(|b| count.get(&b.brep_face)).copied().unwrap_or(0);
let Some(&(i, ..)) = row.iter().max_by_key(|(i, ..)| left(*i)) else { continue };
let Some(b) = boundary.get_mut(i) else { continue };
let Some(left) = count.get_mut(&b.brep_face) else { continue };
if *left > 1 {
*left -= 1;
b.brep_face = f;
*count.entry(f).or_insert(0) += 1;
}
}
}
}
/// The face groups joined across smooth B-rep edges. /// The face groups joined across smooth B-rep edges.
fn smooth_groups(surface: &BoundarySurface, faces: usize) -> Vec<usize> { fn smooth_groups(surface: &BoundarySurface, faces: usize) -> Vec<usize> {
let mut parent: Vec<usize> = (0..faces).collect(); let mut parent: Vec<usize> = (0..faces).collect();
@@ -626,6 +683,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
} }
} }
let mut boundary = Vec::with_capacity(quads.len()); let mut boundary = Vec::with_capacity(quads.len());
let mut centres = Vec::with_capacity(quads.len());
let mut enclosed = 0.0; let mut enclosed = 0.0;
for (q, e, f, tg) in &quads { for (q, e, f, tg) in &quads {
let pts = q.map(|v| nodes.get(v as usize).copied().unwrap_or_default()); let pts = q.map(|v| nodes.get(v as usize).copied().unwrap_or_default());
@@ -634,7 +692,10 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
let set = targets.get(*tg).map_or(&[][..], Vec::as_slice); let set = targets.get(*tg).map_or(&[][..], Vec::as_slice);
let brep_face = on_target(g, set, centre).map_or_else(|| set.first().copied().unwrap_or(0), |h| h.2); let brep_face = on_target(g, set, centre).map_or_else(|| set.first().copied().unwrap_or(0), |h| h.2);
boundary.push(BoundaryFace { element: *e, face: *f, brep_face }); boundary.push(BoundaryFace { element: *e, face: *f, brep_face });
let shortest = (0..4).map(|i| dist(pts[i], pts[(i + 1) % 4])).fold(f64::INFINITY, f64::min);
centres.push((centre, shortest, *tg));
} }
claim_narrow_faces(&mut boundary, &centres, &targets, g);
let surface_volume = surface.volume(); let surface_volume = surface.volume();
let report = ProjectReport { let report = ProjectReport {
blocks: t.blocks.len(), blocks: t.blocks.len(),
+47 -21
View File
@@ -507,6 +507,19 @@ impl Tessellation {
self.face_tree(face).nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3])) self.face_tree(face).nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3]))
} }
/// The B-rep face whose triangles lie nearest to `p`.
pub fn nearest_face(&self, p: P) -> Option<u32> {
let dist = |t: u32| {
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
let d = sub(closest_on_triangle(p, self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2])), p);
(dot(d, d), [0.0, 0.0, 0.0, f64::from(t)])
};
let r = self.tri_tree.nearest4(p, &|_| true, &dist)?;
// The triangle index travels through the f64 exactly (triangle counts are capped far
// below 2^53).
self.tri_face.get(r[3] as usize).copied()
}
/// The tree over `face`'s triangles (the whole boundary's for a face out of range, which /// The tree over `face`'s triangles (the whole boundary's for a face out of range, which
/// the queries' filter then finds nothing in). /// the queries' filter then finds nothing in).
fn face_tree(&self, face: u32) -> &Tree { fn face_tree(&self, face: u32) -> &Tree {
@@ -528,38 +541,51 @@ impl Tessellation {
} }
} }
/// How far off a plane face's triangles, in triangle sizes, a point of the plane may lie and /// How far off a face's triangles, in triangle sizes, a point of its surface may lie and still
/// still be on the face (rounding only: the triangles lie on the plane). /// be on the face, beyond the triangles' own gap to the surface (rounding only).
const PLANE_SLACK: f64 = 1e-6; const PLANE_SLACK: f64 = 1e-6;
impl Tessellation { impl Tessellation {
/// The closest point to `p` on the trimmed face `face`: the exact surface's closest point /// The closest point to `p` on the trimmed face `face`: the exact surface's closest point
/// when it lies on the face (within a fifth of the local triangle size of the face's /// when it lies on the face (no further off the face's nearest triangle than twice that
/// triangles, or on them for a plane, whose triangles can span the whole face), else the closest point of the face's triangles. Unlike /// triangle's own chordal gap to the surface, which is rounding only for a plane), else the
/// [`BoundaryGeometry::project_face`] it never returns a point of the surface's extension /// closest point of the face's triangles. Unlike [`BoundaryGeometry::project_face`] it never
/// beyond the face's boundary, whatever the distance from `p`. /// returns a point of the surface's extension beyond the face's boundary, whatever the
/// distance from `p` — nor of the same surface's other faces: the pieces of one fillet split
/// by a cut share their cylinder, and a point on one piece is off the others.
pub fn project_trimmed(&self, face: u32, p: P) -> Option<P> { pub fn project_trimmed(&self, face: u32, p: P) -> Option<P> {
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face); let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
let nearest = |x: P| { let corners = |t: u32| {
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
(self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]))
};
let near = {
let dist = |t: u32| { let dist = |t: u32| {
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]); let (a, b, c) = corners(t);
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2])); let q = closest_on_triangle(p, a, b, c);
let q = closest_on_triangle(x, a, b, c); let d = sub(q, p);
let d = sub(q, x); (dot(d, d), [q[0], q[1], q[2], 0.0])
let mut e = [measure::dist(a, b), measure::dist(b, c), measure::dist(c, a)];
e.sort_by(f64::total_cmp);
(dot(d, d), [q[0], q[1], q[2], e[1]])
}; };
self.face_tree(face).nearest4(x, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3])) let r = self.face_tree(face).nearest4(p, &accept, &dist)?;
[r[0], r[1], r[2]]
}; };
let (near, _) = nearest(p)?;
let Some(surface) = self.analytic(face) else { return Some(near) }; let Some(surface) = self.analytic(face) else { return Some(near) };
let Some(q) = surface.project(p) else { return Some(near) }; let Some(q) = surface.project(p) else { return Some(near) };
let (back, size) = nearest(q)?; // The nearest triangle to the surface's point, with its slack: the largest gap between
// A plane's triangles lie on it, so a point of the plane off them is off the face; a // the triangle and the surface (at its edges' midpoints and centre: a chord's deepest
// curved face's triangles are chords, a fraction of their size off the surface. // points), doubled, and a millionth of its size for rounding.
let slack = if matches!(surface, AnalyticSurface::Plane { .. }) { PLANE_SLACK } else { 0.2 }; let gap = |x: P| surface.project(x).map_or(0.0, |y| measure::dist(x, y));
Some(if measure::dist(back, q) <= slack * size { q } else { near }) let dist = |t: u32| {
let (a, b, c) = corners(t);
let o = closest_on_triangle(q, a, b, c);
let d = sub(o, q);
let mid = |u: P, v: P| scale(add(u, v), 0.5);
let deepest = [mid(a, b), mid(b, c), mid(c, a), scale(add(add(a, b), c), 1.0 / 3.0)].into_iter().map(gap).fold(0.0, f64::max);
let size = measure::dist(a, b).max(measure::dist(b, c)).max(measure::dist(c, a));
(dot(d, d), [o[0], o[1], o[2], 2.0 * deepest + PLANE_SLACK * size])
};
let r = self.face_tree(face).nearest4(q, &accept, &dist)?;
Some(if measure::dist([r[0], r[1], r[2]], q) <= r[3] { q } else { near })
} }
/// The outward unit normal of B-rep face `face` near `p`: the exact surface's (oriented like /// The outward unit normal of B-rep face `face` near `p`: the exact surface's (oriented like