step: a loop through its surface's pole has its seam

A cone face bounded by its base circle and its seam out to the apex and back
turns once round the axis and closes at the apex. add_seams counted the turn
as a loop without a seam and added another, out and back from the same
vertex: the drill point of a turned pin read back from STEP had a sliver
whose boundary crossed itself, and the body would not tessellate (#5193). A
winding loop with a vertex on a pole is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
This commit is contained in:
jyviko
2026-10-25 19:51:02 -04:00
parent 903e371fa7
commit cfe1b8b929
2 changed files with 46 additions and 1 deletions
+17 -1
View File
@@ -167,6 +167,7 @@ impl Builder<'_, '_> {
if periods.iter().all(Option::is_none) {
continue;
}
let found = axis(&surface).or_else(|| spline_axis(&surface, &eval));
let mut up = Vec::new();
let mut down = Vec::new();
for (li, l) in face.loops.iter().enumerate() {
@@ -175,6 +176,12 @@ impl Builder<'_, '_> {
if w[1] != 0 || w[0].abs() > 1 {
return Err(format!("face #{}: {}", face.step, unsupported("a loop winding around a surface more than once or in v")));
}
// A loop through a pole turns once round the axis and closes there, where every
// angle is one point: a cone's base circle with its seam out to the apex and
// back. It has its seam; another one doubled it, a sliver the face crossed.
if w[0] != 0 && found.as_ref().is_some_and(|ax| self.through_pole(es, ax)) {
continue;
}
match w[0] {
1 => up.push(li),
-1 => down.push(li),
@@ -185,7 +192,7 @@ impl Builder<'_, '_> {
continue;
}
let step = face.step;
let Some(ax) = axis(&surface).or_else(|| spline_axis(&surface, &eval)) else {
let Some(ax) = found else {
return Err(format!("face #{step}: {}", unsupported("a face winding around a torus or a spline closed in v without a seam edge")));
};
match (up.as_slice(), down.as_slice()) {
@@ -226,6 +233,15 @@ impl Builder<'_, '_> {
Ok(total.map(|x| x.round() as i64))
}
/// A vertex of the loop is on one of the axis's poles (within its tolerance).
fn through_pole(&self, es: &[(u32, bool)], ax: &Axis) -> bool {
es.iter().any(|&u| {
self.start(u).ok().and_then(|v| self.vertices.get(v as usize)).is_some_and(|x| {
ax.poles.iter().flatten().any(|p| Vec3::from(x.point).dist(*p) <= self.tol.max(x.tolerance))
})
})
}
/// The vertex where use `k` of a loop starts.
fn start(&self, (e, forward): (u32, bool)) -> R<u32> {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
+29
View File
@@ -445,6 +445,35 @@ fn a_cone_reaches_its_apex_through_a_vertex_loop() {
faces_away(&s.brep, 0, |p| [0.0, 0.0, p[2].clamp(1.0, 2.0)]);
}
/// The same cone written with its seam: the base circle, then the seam out to the apex and back.
/// The loop turns once round the axis and closes at the apex, so it has its seam; reading it as
/// a loop without one gave the face a second seam, a sliver out and back the face's boundary
/// crossed (#5193: a turned pin's drill point after a STEP round trip did not tessellate).
#[test]
fn a_cone_with_its_seam_to_the_apex_gets_no_other() {
let mut w = W::default();
let (p, top) = ([5.0, 0.0, 0.0], [0.0, 0.0, 10.0]);
let (v, apex) = (w.vertex(p), w.vertex(top));
let k = w.circle([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], 5.0);
let e = w.add(format!("EDGE_CURVE('',#{v},#{v},#{k},.T.)"));
let seam = w.line_edge((v, p), (apex, top), true, len3(sub3(top, p)));
let a = w.axis([0.0; 3], [0.0, 0.0, -1.0], [1.0, 0.0, 0.0]);
let cone = w.add(format!("CONICAL_SURFACE('',#{a},5.,{})", real(0.5f64.atan())));
let uses = [w.oe(e, true), w.oe(seam, true), w.oe(seam, false)];
let outer = w.bound(&uses, true, true);
let side = w.face(&[outer], cone, true);
let o = w.oe(e, false);
let base_bound = w.bound(&[o], true, true);
let plane = w.plane([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
let base = w.face(&[base_bound], plane, false);
let shell = closed_shell(&mut w, &[side, base]);
let solid = w.add(format!("MANIFOLD_SOLID_BREP('cone',#{shell})"));
let s = one_solid(&w.file(&[solid]));
assert_eq!(counts(&s.brep), [2, 1, 2, 2, 2]);
assert!(!s.notes.iter().any(|n| n.contains("seam edges added")), "{:?}", s.notes);
faces_away(&s.brep, 0, |p| [0.0, 0.0, p[2].clamp(1.0, 2.0)]);
}
/// A tube (radius 5 outside, 3 inside, from z = 0 to 4) whose cylinders carry no seam edges:
/// each side is bounded by two circles whose vertices sit at different angles.
#[test]