Mesh a cylinder wall whose seam edges a cut split without fans across it (#921)

A boolean that cuts a pocket into the top of a cylinder (Shell does this) splits the
outer wall's straight seam edges at the pocket's floor. The wall's parameter grid has no
division along its rulings, so the triangulation fanned out from the two split vertices
to the far rims and its triangles cut through the part: the shelled Bolted Flange sample
drew and exported its rim cut off (mesh volume 29201 mm3 for a 40796 mm3 body).

Give the grid a line through each vertex that splits a boundary segment running along a
ruling, in a direction that needs no division of its own and within the grid budget.

Fixes #819.

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 20:10:55 -04:00
1 parent 5f7ce80a93
commit 7135407268
2 files changed
+93 -1

No files matched your search

+39
View File
@@ -348,3 +348,42 @@ fn round_faces_of_every_kind_are_divided_by_the_angle() {
check(name, b);
}
}
/// The signed volume a closed mesh encloses.
fn mesh_volume(m: &Mesh) -> f64 {
m.triangles.iter().filter_map(|t| m.tri(t)).map(|[a, b, c]| a.dot(b.cross(c)) / 6.0).sum()
}
#[test]
fn a_cylinder_wall_split_along_its_seam_meshes_round() {
// A cup: a Ø100 × 10 disc with a Ø96 pocket 8 deep cut into its top, as Shell leaves it.
// The cut splits the outer wall's straight seam edges at the pocket's floor (z = 2); the
// wall used to be meshed as fans from those two vertices to the far rims, triangles that cut
// through the disc (the shelled Bolted Flange sample showed its rim cut off).
let disc = |r: f64, z0: f64, z1: f64| {
extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(Vec2::ZERO, r), holes: vec![] }], z0, z1).unwrap().pop().unwrap()
};
let cup = crate::ops::boolean(&disc(50.0, 0.0, 10.0), &disc(48.0, 2.0, 20.0), crate::BoolOp::Cut).unwrap().unwrap();
let exact = PI * (50.0 * 50.0 * 10.0 - 48.0 * 48.0 * 8.0);
let outer: Vec<bool> = (0..cup.face_count())
.map(|i| matches!(face_surface(&cup, i), Some(FaceSurface::Cylinder { radius, .. }) if (radius - 50.0).abs() < 1e-6))
.collect();
assert_eq!(outer.iter().filter(|o| **o).count(), 2);
for tol in [display_tol(&cup), 0.1, 0.01] {
let m = cup.tessellate(tol).unwrap();
assert_eq!(open_sides(&m, 1e-6), 0);
let v = mesh_volume(&m);
assert!((v - exact).abs() < exact * 0.01, "tolerance {tol}: mesh volume {v:.1}, exact {exact:.1}");
// Every vertex of the outer wall's triangles lies on it, and none spans its height
// from one of the split vertices across the rim.
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
if !outer.get(*f as usize).copied().unwrap_or(false) {
continue;
}
let [a, b, c] = m.tri(t).unwrap();
let centre = (a + b + c) * (1.0 / 3.0);
let r = Vec2::new(centre.x, centre.y).len();
assert!(r > 50.0 - 10.0 * tol.max(0.05), "tolerance {tol}: an outer wall triangle passes {r:.2} mm from the axis");
}
}
}