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:
1 parent
5f7ce80a93
commit
7135407268
2 files changed
+93
-1
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-1
@@ -755,7 +755,11 @@ fn insert_surface(
|
||||
(division(range.0), division(range.1))
|
||||
} else {
|
||||
let (u, v) = bounded_surface_division(&surface, range, tol, cap);
|
||||
balanced_grid(&surface, u, v, cap)
|
||||
let (u_ruled, v_ruled) = (u.len() <= 3, v.len() <= 3);
|
||||
let (u, v) = balanced_grid(&surface, u, v, cap);
|
||||
let v = with_ruling_vertices(v, polyline, 1, v_ruled, u.len(), cap);
|
||||
let u = with_ruling_vertices(u, polyline, 0, u_ruled, v.len(), cap);
|
||||
(u, v)
|
||||
};
|
||||
let insert_res: Vec<Vec<Option<_>>> = udiv
|
||||
.into_iter()
|
||||
@@ -970,6 +974,55 @@ fn balanced_grid(surface: &impl PreMeshableSurface, udiv: Vec<f64>, vdiv: Vec<f6
|
||||
(thin(u2), thin(v2))
|
||||
}
|
||||
|
||||
/// SolidCraft: most grid lines added for vertices on a ruling edge.
|
||||
const MAX_RULING_LINES: usize = 64;
|
||||
|
||||
/// SolidCraft: `div` (the grid along coordinate `axis`, 0 for u and 1 for v) with a line through
|
||||
/// every vertex that splits a boundary segment running along a ruling (a straight edge of a
|
||||
/// cylinder or cone split by a boolean). Without one, the triangulation fans out from such a
|
||||
/// vertex to the far ends of the face, and its triangles cut through the surface (the outer
|
||||
/// wall of a shelled cylinder came out cut off). Only a direction that needs no division of
|
||||
/// its own (`ruled`) gets them, while the grid stays within `cap`.
|
||||
fn with_ruling_vertices(div: Vec<f64>, polyline: &PolyBoundary, axis: usize, ruled: bool, other: usize, cap: usize) -> Vec<f64> {
|
||||
let (Some(&first), Some(&last)) = (div.first(), div.last()) else { return div };
|
||||
let span = last - first;
|
||||
if !ruled || !(span.is_finite() && span > 0.0) {
|
||||
return div;
|
||||
}
|
||||
let across = 1 - axis;
|
||||
// The extent of the face across the rulings, to tell a segment along one.
|
||||
let (lo, hi) = polyline.0.iter().flatten().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p[across]), hi.max(p[across])));
|
||||
let width = hi - lo;
|
||||
if !(width.is_finite() && width > 0.0) {
|
||||
return div;
|
||||
}
|
||||
let (same, apart) = (width * 1e-9, span * 1e-6);
|
||||
let mut extra: Vec<f64> = Vec::new();
|
||||
for w in &polyline.0 {
|
||||
for (a, b) in w.iter().circular_tuple_windows() {
|
||||
if (a[across] - b[across]).abs() > same {
|
||||
continue;
|
||||
}
|
||||
for t in [a[axis], b[axis]] {
|
||||
let inside = t > first + apart && t < last - apart;
|
||||
if inside && t.is_finite() && div.iter().chain(&extra).all(|d| (d - t).abs() > apart) {
|
||||
extra.push(t);
|
||||
}
|
||||
}
|
||||
if extra.len() > MAX_RULING_LINES {
|
||||
return div;
|
||||
}
|
||||
}
|
||||
}
|
||||
if extra.is_empty() || (div.len() + extra.len()).saturating_mul(other) > cap {
|
||||
return div;
|
||||
}
|
||||
let mut out = div;
|
||||
out.extend(extra);
|
||||
out.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
out
|
||||
}
|
||||
|
||||
/// Converts triangulation into `PolygonMesh`.
|
||||
fn triangulation_into_polymesh<'a>(
|
||||
vertices: VertexIterator<'a, SPoint2, (), CdtEdge<()>, ()>,
|
||||
|
||||
Reference in new issue
Block a user