fix: shell a body whose curved edge is rounded smaller than the wall (#476)
Moved in by more than its radius, a round collapses and its neighbours meet along a new edge. That edge was always built as a straight line keyed by its ends, so on a disc whose circular edge is rounded the four arcs where the floor meets the side became one chord used by four faces, and the shell failed with "This shell is not oriented and closed" (#319). The new edge now runs where the edge's face meets the face across the round (the round's far side, crossed as a strip), through the edge's middle moved onto both: a line or a circular arc, checked to lie on both moved faces, and refused otherwise. Twins are the new edges with the same ends through the same middle. Fixes #319 Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0fe032438f
commit
ccc36dc1f9
2 files changed
+117
-24
No files matched your search
+91
-24
@@ -757,6 +757,56 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
// Across the collapsed rounds next to edge `e` (its faces `fs`): a round is a strip, crossed
|
||||
// to its side that shares no vertex with the edge, through further collapsed rounds.
|
||||
let opposite_side = |e: &mt::Edge, fs: &[usize]| -> Vec<usize> {
|
||||
let mut out: Vec<usize> = Vec::new();
|
||||
let mut seen: Vec<usize> = Vec::new();
|
||||
let mut queue: Vec<(usize, [mt::VertexID; 2])> =
|
||||
fs.iter().copied().filter(|i| is_collapsed(*i)).map(|i| (i, [e.front().id(), e.back().id()])).collect();
|
||||
while let Some((cf, avoid)) = queue.pop() {
|
||||
if seen.contains(&cf) {
|
||||
continue;
|
||||
}
|
||||
seen.push(cf);
|
||||
let Some(face) = faces.get(cf) else { continue };
|
||||
for side in face.edge_iter() {
|
||||
if avoid.contains(&side.front().id()) || avoid.contains(&side.back().id()) {
|
||||
continue;
|
||||
}
|
||||
for nb in efaces.get(&side.id()).into_iter().flatten().copied() {
|
||||
if nb == cf || fs.contains(&nb) {
|
||||
continue;
|
||||
}
|
||||
if is_collapsed(nb) {
|
||||
queue.push((nb, [side.front().id(), side.back().id()]));
|
||||
} else if !out.contains(&nb) {
|
||||
out.push(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
// For a point on a collapsed round touching the faces `at`: the faces it keeps (`at`'s
|
||||
// own, not collapsed), and the faces along the rounds it is on, nearest `p` first.
|
||||
let across = |at: &[usize], p: Vec3| -> (Vec<usize>, Vec<usize>) {
|
||||
let own: Vec<usize> = at.iter().copied().filter(|i| !is_collapsed(*i)).collect();
|
||||
let mut cand: Vec<(f64, usize)> = expand(at)
|
||||
.into_iter()
|
||||
.filter(|nb| !own.contains(nb))
|
||||
.map(|nb| {
|
||||
let d = edge_pts
|
||||
.iter()
|
||||
.filter(|(fs, _)| fs.contains(&nb) && fs.iter().any(|i| is_collapsed(*i)))
|
||||
.flat_map(|(_, q)| q.iter().map(|x| x.dist(p)))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
(d, nb)
|
||||
})
|
||||
.collect();
|
||||
cand.sort_by(|x, y| x.0.total_cmp(&y.0));
|
||||
(own, cand.into_iter().map(|(_, nb)| nb).collect())
|
||||
};
|
||||
let mut newpos: HashMap<mt::VertexID, Vec3> = HashMap::new();
|
||||
// Corners whose moved faces meet at several points (see `split_corner`).
|
||||
let mut splits: HashMap<mt::VertexID, Vec<(Vec3, Vec<usize>)>> = HashMap::new();
|
||||
@@ -766,22 +816,7 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
let fs: Vec<usize> = if at.iter().any(|i| is_collapsed(*i)) {
|
||||
// On a collapsed round: its own faces, then the faces along the rounds it is on,
|
||||
// nearest first, until they fix a point.
|
||||
let group = expand(at);
|
||||
let mut own: Vec<usize> = at.iter().copied().filter(|i| !is_collapsed(*i)).collect();
|
||||
let mut cand: Vec<(f64, usize)> = group
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|nb| !own.contains(nb))
|
||||
.map(|nb| {
|
||||
let d = edge_pts
|
||||
.iter()
|
||||
.filter(|(fs, _)| fs.contains(&nb) && fs.iter().any(|i| is_collapsed(*i)))
|
||||
.flat_map(|(_, q)| q.iter().map(|x| x.dist(p)))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
(d, nb)
|
||||
})
|
||||
.collect();
|
||||
cand.sort_by(|x, y| x.0.total_cmp(&y.0));
|
||||
let (mut own, cand) = across(at, p);
|
||||
let rank = |ids: &[usize]| -> usize {
|
||||
let mut basis: Vec<Vec3> = Vec::new();
|
||||
for i in ids {
|
||||
@@ -796,7 +831,7 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
}
|
||||
basis.len()
|
||||
};
|
||||
for (_, c) in cand {
|
||||
for c in cand {
|
||||
if rank(&own) >= 3 {
|
||||
break;
|
||||
}
|
||||
@@ -904,7 +939,8 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
let mut edges: HashMap<mt::EdgeID, (mt::Edge, bool)> = HashMap::new();
|
||||
// Moved faces and edges; an edge where neighbours of a collapsed face meet is new.
|
||||
let mut rec = crate::record::Rebuild::default();
|
||||
let mut merged: HashMap<(mt::VertexID, mt::VertexID), mt::Edge> = HashMap::new();
|
||||
// New edges along collapsed rounds: ends and points along them, for their twins.
|
||||
let mut merged: Vec<(mt::VertexID, mt::VertexID, Vec<Vec3>, mt::Edge)> = Vec::new();
|
||||
for e in b.solid.edge_iter() {
|
||||
if edges.contains_key(&e.id()) {
|
||||
continue;
|
||||
@@ -921,13 +957,44 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
continue;
|
||||
}
|
||||
if touches {
|
||||
// Along a collapsed round: a straight edge, shared with its twin from the
|
||||
// round's other side.
|
||||
let ne = match merged.get(&(a.id(), c.id())).or_else(|| merged.get(&(c.id(), a.id()))) {
|
||||
Some(x) => x.clone(),
|
||||
// Along a collapsed round: where the edge's own face meets the face across the
|
||||
// round, between the edge's moved ends, through the edge's middle moved onto
|
||||
// both: a line (planes, or a plane along a cylinder's axis) or an arc (a plane
|
||||
// square to a coaxial cylinder); faces that meet in another curve are refused.
|
||||
// Shared with its twin from the round's other side: the new edge with the same
|
||||
// ends through the same middle (two arcs of one circle have the same ends).
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let fs = efaces.get(&e.id()).ok_or_else(|| fail("edge faces"))?;
|
||||
let cv = e.curve();
|
||||
let (t0, t1) = cv.range_tuple();
|
||||
let mid = from_p3(cv.subs((t0 + t1) / 2.0));
|
||||
// The face across: the strip's far side (`opposite_side`), the nearest of them
|
||||
// when several; the nearest along the rounds when the strip has none.
|
||||
let (mut meet, cand) = across(fs, mid);
|
||||
let far = opposite_side(&e, fs);
|
||||
meet.extend(cand.iter().find(|c| far.contains(c)).or(cand.first()));
|
||||
if meet.len() < 2 {
|
||||
return Err(fail("an edge along a collapsed round with no face across it"));
|
||||
}
|
||||
let (o, s, n) = pick(&meet);
|
||||
let m = move_point(mid, &o, &s, &n, size * 1e-6).ok_or_else(|| fail("faces that don't meet along a collapsed round"))?;
|
||||
let near = size * 1e-5;
|
||||
let twin = merged.iter().find(|(x, y, pts, _)| {
|
||||
((*x == a.id() && *y == c.id()) || (*x == c.id() && *y == a.id()))
|
||||
&& pts.windows(2).any(|w| w.first().zip(w.get(1)).is_some_and(|(p, q)| m.dist_to_segment(*p, *q) < near))
|
||||
});
|
||||
let ne = match twin {
|
||||
Some((_, _, _, x)) => x.clone(),
|
||||
None => {
|
||||
let x = builder::line(a, c);
|
||||
merged.insert((a.id(), c.id()), x.clone());
|
||||
let (pa, pc) = (from_p3(a.point()), from_p3(c.point()));
|
||||
let x = if m.dist_to_segment(pa, pc) < near { builder::line(a, c) } else { builder::circle_arc(a, c, p3(m)) };
|
||||
let xc = x.curve();
|
||||
let (u0, u1) = xc.range_tuple();
|
||||
let pts: Vec<Vec3> = (0..=16).map(|j| from_p3(xc.subs(u0 + (u1 - u0) * j as f64 / 16.0))).collect();
|
||||
if !pts.iter().all(|p| n.iter().all(|sf| sf.dist(*p).abs() < near)) {
|
||||
return Err(fail("not supported yet: faces meeting along a collapsed round in a curve other than a line or circle"));
|
||||
}
|
||||
merged.push((a.id(), c.id(), pts, x.clone()));
|
||||
x
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2123,3 +2123,29 @@ fn shell_open_beside_a_round_as_large_as_the_wall_is_refused() {
|
||||
let f = fillet(&b, &loop_, 1.5).unwrap();
|
||||
assert!(shell(&f, &bottom, 2.0).unwrap().validity().is_empty());
|
||||
}
|
||||
|
||||
/// A disc whose circular edge is rounded smaller than the wall (#319): moved in by the
|
||||
/// thickness the round collapses to a sharp edge, where the floor meets the side along a
|
||||
/// circle (two arcs with the same ends), not a straight line.
|
||||
#[test]
|
||||
fn shell_of_a_disc_rounded_smaller_than_the_wall() {
|
||||
use std::f64::consts::PI;
|
||||
let (rd, h, r) = (15.0, 5.0, 1.5);
|
||||
let disc = cylinder(Vec3::new(0.0, 0.0, -h), Vec3::Z, rd, h).unwrap();
|
||||
// The round takes a corner of area r²(1 − π/4) whose centroid is r(10 − 3π)/(3(4 − π))
|
||||
// in from the rim, swept round the axis (Pappus).
|
||||
let corner = r * r * (1.0 - PI / 4.0) * 2.0 * PI * (rd - r * (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI)));
|
||||
let (top, bottom) = ([Vec3::ZERO], [Vec3::new(0.0, 0.0, -h)]);
|
||||
for (edge_z, open) in [(-h, &top), (-h, &bottom), (0.0, &bottom)] {
|
||||
let rounded = fillet(&disc, &[Vec3::new(0.0, rd, edge_z), Vec3::new(0.0, -rd, edge_z)], r).unwrap();
|
||||
let v0 = PI * rd * rd * h - corner;
|
||||
for t in [1.6, 2.0] {
|
||||
let s = shell(&rounded, open, t).unwrap_or_else(|e| panic!("round at z {edge_z}, t {t}: {e}"));
|
||||
assert!(s.validity().is_empty(), "round at z {edge_z}, t {t}: {:?}", s.validity());
|
||||
// The cavity: a plain cylinder the thickness in from the side and the closed end.
|
||||
let want = v0 - PI * (rd - t).powi(2) * (h - t);
|
||||
let v = measure(&s).unwrap().volume;
|
||||
assert!(rel(v, want) < 1e-3, "round at z {edge_z}, t {t}: {v} vs {want}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user