fix(acis): fill a planar face's holes in the fallback sampler too
A face with a hole came out with the hole solid and the material around it missing. Handle 176 of Annotation-2D-Mesh-Solid-BIM.dwg is one. `tess_plane_face` read `face.first_loop()` and fan-triangulated it, on the grounds that holes were the kernel path's job. Two things make that wrong. Nothing says the outer loop is listed first, so when a hole came first the hole got filled and the boundary was ignored. And the kernel path does not always win: when its mesh is incomplete, `tessellate_acis` runs both and keeps whichever produced more triangles — which is not a measure of being right. Every loop is read now, flattened into the plane's own frame, and triangulated with the widest ring bounding and the rest cutting into it — the same rule the kernel uses, and by area rather than by listing order. This is interim. The fallback should not exist at all: the kernel already lifts these bodies with no loss and meshes ten of handle 176's fourteen faces. The four it drops are planar rectangles whose boundary projects with one edge's `v` mirrored, so the ring self-crosses and ear clipping declines it — a `pcurve` fault, not a missing feature. Fixing that makes the mesh complete, and a complete mesh means the fallback never runs and can go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
13d3c7d803
commit
5a2aa13b74
1 changed files with 59 additions and 26 deletions
|
|
@ -2017,8 +2017,13 @@ pub(crate) fn tess_plane_face(
|
||||||
normals: &mut Vec<[f32; 3]>,
|
normals: &mut Vec<[f32; 3]>,
|
||||||
indices: &mut Vec<u32>,
|
indices: &mut Vec<u32>,
|
||||||
) {
|
) {
|
||||||
let mut poly = collect_face_polygon(sat, face, chord_frac);
|
// Every loop, not just the first. A face with a hole in it carries the
|
||||||
if poly.len() < 3 {
|
// hole on a second loop, and nothing says the outer one is listed first —
|
||||||
|
// so reading `first_loop` alone drew whichever came first and ignored the
|
||||||
|
// rest. When that was a hole, the hole came out solid and the material
|
||||||
|
// around it came out missing.
|
||||||
|
let rings = collect_face_loops(sat, face, chord_frac);
|
||||||
|
if rings.iter().all(|ring| ring.len() < 3) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2031,21 +2036,63 @@ pub(crate) fn tess_plane_face(
|
||||||
};
|
};
|
||||||
let nf = [nx as f32, ny as f32, nz as f32];
|
let nf = [nx as f32, ny as f32, nz as f32];
|
||||||
|
|
||||||
if dot3(newell_normal(&poly), [nx, ny, nz]) < 0.0 {
|
// Flattened into the plane's own frame, where "inside" is a question with
|
||||||
poly.reverse();
|
// an answer. The kernel's ear clipping bridges the holes into the outer
|
||||||
|
// ring; a fan from one vertex cannot express a hole at all.
|
||||||
|
let (px, py, pz) = plane.root_point();
|
||||||
|
let seed = if nx.abs() < 0.9 {
|
||||||
|
[1.0, 0.0, 0.0]
|
||||||
|
} else {
|
||||||
|
[0.0, 1.0, 0.0]
|
||||||
|
};
|
||||||
|
let Some(frame) =
|
||||||
|
acadrust::kernel::space::Plane::orthonormal([px, py, pz], seed, [nx, ny, nz])
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let flat: Vec<Vec<[f64; 2]>> = rings
|
||||||
|
.iter()
|
||||||
|
.filter(|ring| ring.len() >= 3)
|
||||||
|
.map(|ring| ring.iter().filter_map(|p| frame.project(*p)).collect())
|
||||||
|
.collect();
|
||||||
|
let Some(widest) = flat
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by(|a, b| {
|
||||||
|
acadrust::kernel::geom2d::signed_area(a.1)
|
||||||
|
.abs()
|
||||||
|
.total_cmp(&acadrust::kernel::geom2d::signed_area(b.1).abs())
|
||||||
|
})
|
||||||
|
.map(|(index, _)| index)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let holes: Vec<Vec<[f64; 2]>> = flat
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(index, _)| *index != widest)
|
||||||
|
.map(|(_, ring)| ring.clone())
|
||||||
|
.collect();
|
||||||
|
let (points, triangles) =
|
||||||
|
acadrust::kernel::geom2d::triangulate::polygon(&flat[widest], &holes);
|
||||||
|
if triangles.is_empty() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let base = verts.len() as u32;
|
let base = verts.len() as u32;
|
||||||
for &pt in &poly {
|
for point in &points {
|
||||||
verts.push(pt);
|
verts.push(frame.point_at(*point));
|
||||||
normals.push(nf);
|
normals.push(nf);
|
||||||
}
|
}
|
||||||
|
// Ear clipping hands back counter-clockwise triangles in the frame's own
|
||||||
// Fan triangulation from vertex 0 (outer loop only; holes are handled by
|
// coordinates; the frame was built around the outward normal, so that is
|
||||||
// the kernel B-rep path in `acis_kernel`).
|
// already the outward winding.
|
||||||
let n = poly.len() as u32;
|
for triangle in triangles {
|
||||||
for i in 1..(n - 1) {
|
indices.extend_from_slice(&[
|
||||||
indices.extend_from_slice(&[base, base + i, base + i + 1]);
|
base + triangle[0] as u32,
|
||||||
|
base + triangle[1] as u32,
|
||||||
|
base + triangle[2] as u32,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2632,20 +2679,6 @@ fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Area-weighted polygon normal via Newell's method. Robust for non-planar or
|
|
||||||
/// slightly noisy loops; its sign encodes the winding direction.
|
|
||||||
fn newell_normal(poly: &[[f64; 3]]) -> [f64; 3] {
|
|
||||||
let mut n = [0.0f64; 3];
|
|
||||||
let len = poly.len();
|
|
||||||
for i in 0..len {
|
|
||||||
let a = poly[i];
|
|
||||||
let b = poly[(i + 1) % len];
|
|
||||||
n[0] += (a[1] - b[1]) * (a[2] + b[2]);
|
|
||||||
n[1] += (a[2] - b[2]) * (a[0] + b[0]);
|
|
||||||
n[2] += (a[0] - b[0]) * (a[1] + b[1]);
|
|
||||||
}
|
|
||||||
n
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue