diff --git a/crates/kernel/src/display_mesh_tests.rs b/crates/kernel/src/display_mesh_tests.rs index 86e15341..f2b0a17f 100644 --- a/crates/kernel/src/display_mesh_tests.rs +++ b/crates/kernel/src/display_mesh_tests.rs @@ -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 = (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"); + } + } +} diff --git a/vendor/truck-meshalgo/src/tessellation/triangulation.rs b/vendor/truck-meshalgo/src/tessellation/triangulation.rs index cb7282de..0349d9bc 100644 --- a/vendor/truck-meshalgo/src/tessellation/triangulation.rs +++ b/vendor/truck-meshalgo/src/tessellation/triangulation.rs @@ -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>> = udiv .into_iter() @@ -970,6 +974,55 @@ fn balanced_grid(surface: &impl PreMeshableSurface, udiv: Vec, vdiv: Vec, polyline: &PolyBoundary, axis: usize, ruled: bool, other: usize, cap: usize) -> Vec { + 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 = 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<()>, ()>,