mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user