Tessellation: a whole sphere's loop starts off its poles when its seam has two segments

A whole sphere as one face has its seam as its only edge, used both ways. In a body many times
its size the seam gets two segments at the body's chord (pole, equator, pole), so no node off
the poles follows a node off them, and the loop kept its first node, a pole: its closing step
crossed the other pole, which the drift unwrapped to a whole turn ("boundary winds around the
surface"). The sphere was left out of the mesh (cut_beside_a_sealed_void_keeps_the_body: a void
r 2 in a 75 mm part, "1 face(s) don't mesh"). The loop now starts, failing a node with an
off-pole predecessor, at a node whose predecessor is a pole on the same edge, so the step into
it is that edge's own and the step into the pole is unwrapped in the loop like any other.

Regression test a_small_sphere_void_in_a_large_block_meshes (tess tests): watertight, no face
left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 01:31:58 -04:00
committed by Irakli Kourtis
parent 9e97bc4703
commit 47b1d716d0
2 changed files with 28 additions and 1 deletions
+8 -1
View File
@@ -356,7 +356,14 @@ fn boundary(
}
let n = uvs.len();
let pole = |q: &[f64; 2]| (0..2).any(|d| pole_along(*q, d));
if let Some(k) = (0..n).find(|&k| uvs.get(k).is_some_and(|q| !pole(q)) && uvs.get((k + n - 1) % n).is_some_and(|q| !pole(q))) {
let off = |k: usize| uvs.get(k).is_some_and(|q| !pole(q));
// A node off the poles whose predecessor is off them too; failing that (a whole sphere's
// seam in two segments: pole, equator, pole), one whose predecessor is a pole on the same
// edge, so the step into it is the edge's own and the step into that pole, from the
// edge before, is in the loop where it is unwrapped like any other.
let same_edge = |k: usize| segs.get((k + n - 1) % n).zip(segs.get(k)).is_some_and(|(a, b)| a.0 == b.0);
let start = (0..n).find(|&k| off(k) && off((k + n - 1) % n)).or_else(|| (0..n).find(|&k| off(k) && same_edge(k)));
if let Some(k) = start {
uvs.rotate_left(k);
pts.rotate_left(k);
segs.rotate_left(k);
+20
View File
@@ -888,3 +888,23 @@ fn a_loop_corner_within_its_tolerance_of_a_pole_is_a_pole() {
let Err(e) = plan(&pyramid_past_a_near_pole(1e-8), 0.01, None) else { panic!("no pole without the tolerance") };
assert!(e.to_string().contains("winds around the surface"), "{e}");
}
/// A whole sphere as one face (its seam the only edge) in a body many times its size: at the
/// body's chord the seam gets two segments, pole to equator to pole, and every node off the poles
/// follows a pole. The loop used to start at a pole and close across the other, which unwrapped
/// to a whole turn: "boundary winds around the surface", and the sphere was left out (a sealed
/// void r 2 in a 60 mm block). It meshes closed.
#[test]
fn a_small_sphere_void_in_a_large_block_meshes() {
let block = ok(crate::box_solid(Vec3::new(-30.0, -30.0, 0.0), Vec3::new(30.0, 30.0, 60.0)));
let ball = ok(crate::sphere(Vec3::new(0.0, 0.0, 30.0), 2.0));
let hollow = cut(&block, &ball);
let tol = hollow.size() * 0.01;
let (m, stats) = match plan(&record(&hollow), tol, None) {
Ok((p, stats)) => (p.evaluate(), stats),
Err(e) => panic!("plan: {e}"),
};
assert!(stats.unmeshed.is_empty(), "{:?}", stats.unmeshed.iter().map(|u| &u.reason).collect::<Vec<_>>());
assert_watertight(&m, "block with a sphere void");
assert_eq!(hollow.unmeshed_faces(), 0);
}