Kernel: a sweep along a smooth path names every wall, and a closed one has no caps

The smooth-path sweep named walls only by the profile's edges in the start plane and took any
planar face nothing claimed for the end cap. A wall the start plane meets along no edge got
no name or became a cap: round a closed path, a square tube's planar top and bottom (whole
annuli, no seam) were both called end caps; along a line then an arc, the walls SMLib makes
along the arc were unnamed or caps. Like the cornered and controlled sweeps, it now also names
walls by the profile carried half way along, takes the end cap from where SMLib carries the
profile last, and round a closed path has no caps at all.

The issue #2005 tube check counted V - E + F without the ring term: SMLib's annular planar
faces have two loops each, so it checks V - E + F - (L - F) = 2 - 2g (Euler-Poincare). Along a
line then an arc SMLib makes a wall per profile segment per path segment (two, where truck
made three).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:16:42 -04:00
committed by Irakli Kourtis
parent d91d3f2a67
commit c1ead0fef7
3 changed files with 17 additions and 6 deletions
+4 -2
View File
@@ -31,8 +31,10 @@ fn circle_path() -> [PathSeg; 2] {
fn tube(body: &Body, volume: f64, genus: usize) {
let m = measure(body).unwrap();
assert!((m.volume - volume).abs() < volume * 0.01, "{m:?}, expected {volume}");
// A closed orientable tube has Euler characteristic 2 - 2g. Coincident caps gave 2.
assert_eq!(m.vertices + m.faces + 2 * genus, m.edges + 2, "{m:?}");
// A closed orientable tube of genus g: V - E + F - (L - F) = 2 - 2g (Euler-Poincare, L the
// loops: an annular face has two). Coincident caps gave 2 - 2g + 2.
let loops: usize = body.record().faces.iter().map(|f| f.loops.len()).sum();
assert_eq!(m.vertices + 2 * m.faces + 2 * genus, m.edges + loops + 2, "{m:?}, {loops} loops");
// Every shell closed: each edge bounds two faces.
assert!(body.record().edges.iter().all(|e| e.uses.len() >= 2));
let h = body.history().expect("sweep provenance");
+2 -2
View File
@@ -439,13 +439,13 @@ fn check_sweep(b: &Body, plane: &Plane, r: &Region2, faces_per_segment: Option<u
#[test]
fn sweeps_along_paths() {
let r = Region2 { outer: rotated(&rect(-2.0, -2.0, 2.0, 2.0).reversed(), 1), holes: vec![] };
// A line then a quarter turn (two parts): three faces per segment.
// A line then a quarter turn: a face per segment along each path segment.
let path = [
PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 10.0) },
PathSeg::Arc { a: Vec3::new(0.0, 0.0, 10.0), center: Vec3::new(20.0, 0.0, 10.0), axis: Vec3::Y, angle: FRAC_PI_2 },
];
let b = crate::sweep(&Plane::XY, &r, &path).unwrap();
check_sweep(&b, &Plane::XY, &r, Some(3));
check_sweep(&b, &Plane::XY, &r, Some(2));
// Along a smooth 3D path: one wall per segment.
let curve: Vec<Vec3> = (0..=40)
.map(|i| {
+11 -2
View File
@@ -565,9 +565,18 @@ mod imp {
if runs.len() > 1 {
return cornered(plane, pieces, path, &runs, translate);
}
let (brep, _) = swept(plane, pieces, path, translate)?;
let (brep, [_, middle, last]) = swept(plane, pieces, path, translate)?;
let body = body_of(&brep, "sweep")?;
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], true)?;
// The walls known by the profile carried half way (past the start's edges: a wall the
// start touches by no edge, as round a closed path, and walls along later path segments).
let inside = seeds(plane, pieces, |q| apply(&middle, q));
let start = path.first().map(crate::PathSeg::start);
let end = path.last().map(seg_ends).transpose()?.map(|(_, b)| b);
let roles = match (start, end) {
// Round a closed path there are no caps, and the start holds seams only.
(Some(a), Some(b)) if a.dist(b) <= 1e-6 => roles(&body, &plane.offset(body.size().max(1e-9) * 10.0), None, &[], &inside, false)?,
_ => roles(&body, plane, Some(&placed(plane, &last)), &seeds(plane, pieces, |q| q), &inside, false)?,
};
with_history(body, &roles, pieces)
}