mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 11:35:55 +00:00
sew: a cut-out touching its face's outline at a corner is one loop through it
Where a cut-out's corner touches the outline (a triangle on the sheet's edge in a bend), four walls meet along the corner's line. The touching sew paired them round the empty wedges and gave each fan at the corner its own vertex, so a cap kept its outline and the cut-out as two loops with a vertex each at one point: SMLib refuses that face (coincident vertices, loops touching: its face tests 32, 38 and 39), and the bend face's two corners, a hair apart in (u, v), left a boundary no triangulation follows. The walls are paired round the wedges of material, as SMLib keeps a solid touching itself along a line: an edge per wedge, the two sharing their end vertices. A face's loops that meet at a vertex are joined there into one loop through it twice, SMLib's form of such a face. With SMLib's face builder taking the same profile (an extrusion of the plate and its cut-out), fixes rig sheet-metal/1095. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 0bf765aa815a5552e5b090fd8854b1dc7a981e3f)
This commit is contained in:
+114
-75
@@ -731,9 +731,11 @@ pub fn sew(faces: &[FaceSpec]) -> Result<Body> {
|
||||
}
|
||||
|
||||
/// Sew the faces into a closed solid that may touch itself: where four faces meet along a
|
||||
/// straight edge (a cut-out's corner on the outline of a plate), each two of them get an edge of
|
||||
/// their own (see [`touching_sheets`]), and where faces meet at a point in fans joined by no edge
|
||||
/// (a hole's corner on its face's outline), each fan gets a vertex of its own.
|
||||
/// straight edge (a cut-out's corner on the outline of a plate), each two of them that bound one
|
||||
/// wedge of material get an edge of their own (see [`touching_sheets`]); where faces meet at a
|
||||
/// point in fans joined by no edge (two caps touching at a corner), each fan gets a vertex of its
|
||||
/// own; and a face whose loops meet at a vertex (a hole's corner on its face's outline) has them
|
||||
/// as one loop through it (see [`join_touching_loops`]).
|
||||
pub fn sew_touching(faces: &[FaceSpec]) -> Result<Body> {
|
||||
sew_impl(faces, true, None).map(|(body, _)| body)
|
||||
}
|
||||
@@ -761,12 +763,10 @@ const SAME_WAY: f64 = 1e-6;
|
||||
/// of extruded arcs and on ruled walls are looked at.
|
||||
///
|
||||
/// Each sheet bounds a wedge of material, so that pieces of material meeting along an edge stay
|
||||
/// apart, except where that would make a face touch itself: a hole touching the outline from
|
||||
/// inside (tangent, or at a corner) leaves its caps an outline and a hole meeting at the edge's
|
||||
/// end, which with the material kept apart would share a vertex, and a face can't have that.
|
||||
/// The material there goes round the hole, and the edges ending at such points get sheets that
|
||||
/// bound the empty wedges instead: the hole's wall on either side is one sheet, the outline's
|
||||
/// the other.
|
||||
/// apart, as SMLib keeps a solid touching itself along a line: an edge for each wedge of
|
||||
/// material, the two edges sharing their end vertices. A hole touching the outline from inside
|
||||
/// (tangent, or at a corner) leaves its caps an outline and a hole meeting at the edge's end, at
|
||||
/// one vertex: [`join_touching_loops`] makes them one loop through it.
|
||||
fn touching_sheets(faces: &[FaceSpec]) -> Result<(HashMap<EdgeUse, usize>, HashMap<Corner, usize>)> {
|
||||
let mut uses: HashMap<(Key, Key), Vec<(EdgeUse, Vec3, Vec3)>> = HashMap::new();
|
||||
for (fi, face) in faces.iter().enumerate() {
|
||||
@@ -783,69 +783,27 @@ fn touching_sheets(faces: &[FaceSpec]) -> Result<(HashMap<EdgeUse, usize>, HashM
|
||||
}
|
||||
}
|
||||
}
|
||||
// Each contact edge: its ends, both ways of pairing its faces, and whether the second is used.
|
||||
let mut contacts = Vec::new();
|
||||
for (ends, group) in uses.iter().filter(|(_, g)| g.len() > 2) {
|
||||
let [material, empty] = edge_sheets(faces, group)?;
|
||||
contacts.push((*ends, material, empty, false));
|
||||
}
|
||||
if contacts.is_empty() {
|
||||
return Ok((HashMap::new(), corner_fans(faces, &HashMap::new())));
|
||||
}
|
||||
let sheets_of = |contacts: &[((Key, Key), [[EdgeUse; 2]; 2], [[EdgeUse; 2]; 2], bool)]| -> HashMap<EdgeUse, usize> {
|
||||
let mut sheets = HashMap::new();
|
||||
for (_, material, empty, round) in contacts {
|
||||
for (sheet, pair) in (if *round { empty } else { material }).iter().enumerate() {
|
||||
for u in pair {
|
||||
sheets.insert(*u, sheet);
|
||||
}
|
||||
// Each contact edge's sheets: its faces in the pairs that bound a wedge of material.
|
||||
let mut sheets = HashMap::new();
|
||||
for group in uses.values().filter(|g| g.len() > 2) {
|
||||
for (sheet, pair) in edge_sheets(faces, group)?.iter().enumerate() {
|
||||
for u in pair {
|
||||
sheets.insert(*u, sheet);
|
||||
}
|
||||
}
|
||||
sheets
|
||||
};
|
||||
// Each pass turns at least one edge round, and none back.
|
||||
loop {
|
||||
let sheets = sheets_of(&contacts);
|
||||
let fans = corner_fans(faces, &sheets);
|
||||
let touching = self_touching(faces, &fans);
|
||||
let mut turned = false;
|
||||
for (ends, _, _, round) in &mut contacts {
|
||||
if !*round && (touching.contains(&ends.0) || touching.contains(&ends.1)) {
|
||||
*round = true;
|
||||
turned = true;
|
||||
}
|
||||
}
|
||||
if !turned {
|
||||
return Ok((sheets, fans));
|
||||
}
|
||||
}
|
||||
let fans = corner_fans(faces, &sheets);
|
||||
Ok((sheets, fans))
|
||||
}
|
||||
|
||||
/// The points where a face has two corners at one vertex (its loops would touch there).
|
||||
fn self_touching(faces: &[FaceSpec], fans: &HashMap<Corner, usize>) -> std::collections::HashSet<Key> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out = std::collections::HashSet::new();
|
||||
for (fi, f) in faces.iter().enumerate() {
|
||||
for (li, l) in f.loops.iter().enumerate() {
|
||||
for (ei, e) in l.iter().enumerate() {
|
||||
let at = key(e.ends().0);
|
||||
if !seen.insert((at, fans.get(&(fi, li, ei)).copied().unwrap_or(0), fi)) {
|
||||
out.insert(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The two ways of making sheets of an edge four faces meet along, as the pairs of faces
|
||||
/// bounding each: around the wedges of material between them, and around the empty ones.
|
||||
/// The sheets of an edge four faces meet along, as the pairs of faces bounding each: around the
|
||||
/// wedges of material between them.
|
||||
///
|
||||
/// Seen along the edge, each face leaves it as a curve; going round the edge, material and empty
|
||||
/// wedges between them alternate. Faces are ordered by the way they leave the edge and, where
|
||||
/// they leave it the same way (tangent faces), by how fast they turn from it: the order of
|
||||
/// points a short way into each face, as in 2D loop chaining (`prism::tightest_left`).
|
||||
fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[[EdgeUse; 2]; 2]; 2]> {
|
||||
fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[EdgeUse; 2]; 2]> {
|
||||
let fail = |s: &str| KernelError::Invalid(format!("sew: {s}"));
|
||||
let (Some((_, a0, b0)), 4) = (group.first(), group.len()) else { return Err(fail("unsupported edge contact")) };
|
||||
let t = (if key(*a0) < key(*b0) { *b0 - *a0 } else { *a0 - *b0 }).normalized().ok_or_else(|| fail("a degenerate contact"))?;
|
||||
@@ -885,14 +843,11 @@ fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[
|
||||
if wedges.iter().any(|(p, q)| p.3 == q.3 || p.1 == q.1 || (same_way(p.4, q.4) && alike(p.2, q.2))) {
|
||||
return Err(fail("ambiguous edge contact"));
|
||||
}
|
||||
let pairs = |material: bool| -> Result<[[EdgeUse; 2]; 2]> {
|
||||
let mut it = wedges.iter().filter(|(p, _)| p.3 == material).map(|(p, q)| [p.0, q.0]);
|
||||
match (it.next(), it.next()) {
|
||||
(Some(x), Some(y)) => Ok([x, y]),
|
||||
_ => Err(fail("ambiguous edge contact")),
|
||||
}
|
||||
};
|
||||
Ok([pairs(true)?, pairs(false)?])
|
||||
let mut it = wedges.iter().filter(|(p, _)| p.3).map(|(p, q)| [p.0, q.0]);
|
||||
match (it.next(), it.next()) {
|
||||
(Some(x), Some(y)) => Ok([x, y]),
|
||||
_ => Err(fail("ambiguous edge contact")),
|
||||
}
|
||||
}
|
||||
|
||||
/// How a face leaves the straight edge `a`→`b` it bounds (its use `u`), seen along the edge's
|
||||
@@ -968,11 +923,10 @@ fn circumcentre(a: Vec3, b: Vec3, c: Vec3) -> Option<Vec3> {
|
||||
/// A loop corner: face, loop, and the edge of the loop that starts there.
|
||||
type Corner = (usize, usize, usize);
|
||||
|
||||
/// Where the faces meeting at a point form several fans, joined to each other by no edge (a hole
|
||||
/// touching its face's outline at one point; two caps touching at a corner), the fan each corner
|
||||
/// there is in, from 1 for every fan past the first (corners not listed are in fan 0). Each fan
|
||||
/// gets its own vertex, so that a face's loops share no vertex and
|
||||
/// every vertex of the solid is a manifold one.
|
||||
/// Where the faces meeting at a point form several fans, joined to each other by no edge (two
|
||||
/// caps touching at a corner), the fan each corner there is in, from 1 for every fan past the
|
||||
/// first (corners not listed are in fan 0). Each fan gets its own vertex, so that pieces of
|
||||
/// material touching at a point share no vertex.
|
||||
fn corner_fans(faces: &[FaceSpec], sheets: &HashMap<EdgeUse, usize>) -> HashMap<Corner, usize> {
|
||||
type EdgeId = (Key, Key, Option<Key>, usize);
|
||||
let id = |fi: usize, li: usize, ei: usize, e: &EdgeSpec| -> EdgeId {
|
||||
@@ -1236,6 +1190,7 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
if !sheets.is_empty() {
|
||||
split_vertex_fans(&mut vertices, &mut edges, &out)?;
|
||||
}
|
||||
join_touching_loops(&mut out, &edges)?;
|
||||
// Each edge bounds two faces, and separate bodies share no vertex.
|
||||
if users.iter().any(|u| u.len() > 2) {
|
||||
return Err(KernelError::Invalid("sew: nonmanifold edge contact".into()));
|
||||
@@ -1282,6 +1237,51 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
})
|
||||
}
|
||||
|
||||
/// Each face's loops that meet at a vertex as one loop through it twice: the first loop up to the
|
||||
/// vertex, the other from the vertex round to it again, then the rest of the first. That is how
|
||||
/// SMLib keeps a face whose hole touches its outline, or whose holes touch each other, at a
|
||||
/// corner: a face's loops share no vertex (SMLib's face tests 38 and 39), and two vertices at
|
||||
/// one point are one (test 32). Each loop keeps its direction, so the joined loop keeps the face
|
||||
/// on its left, and the outer loop stays first.
|
||||
fn join_touching_loops(faces: &mut [PartFace], edges: &[PartEdge]) -> Result<()> {
|
||||
let start = |u: &PartUse| -> Result<u32> {
|
||||
let e = edges.get(u.edge as usize).ok_or_else(|| KernelError::Failed("sew: edge reference".into()))?;
|
||||
Ok(if u.forward { e.vertices[0] } else { e.vertices[1] })
|
||||
};
|
||||
for f in faces {
|
||||
// Each join takes one loop away.
|
||||
'join: while f.loops.len() > 1 {
|
||||
for a in 0..f.loops.len() {
|
||||
for b in a + 1..f.loops.len() {
|
||||
let (Some(la), Some(lb)) = (f.loops.get(a), f.loops.get(b)) else { continue };
|
||||
let mut at = None;
|
||||
'find: for (ia, ua) in la.iter().enumerate() {
|
||||
let v = start(ua)?;
|
||||
for (ib, ub) in lb.iter().enumerate() {
|
||||
if start(ub)? == v {
|
||||
at = Some((ia, ib));
|
||||
break 'find;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((ia, ib)) = at else { continue };
|
||||
let mut lb = f.loops.remove(b);
|
||||
if let Some(la) = f.loops.get_mut(a) {
|
||||
let rest = la.split_off(ia);
|
||||
let head: Vec<PartUse> = lb.drain(..ib).collect();
|
||||
la.extend(lb);
|
||||
la.extend(head);
|
||||
la.extend(rest);
|
||||
}
|
||||
continue 'join;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One face to [`assemble`].
|
||||
pub(crate) struct PartFace {
|
||||
pub surface: b::Surface,
|
||||
@@ -1807,6 +1807,45 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 10 x 6 x 2 plate with a triangular cut-out whose corner touches the plate's edge x = 0
|
||||
/// at (0, 3): the plate touches itself along the line of that corner, where four walls meet.
|
||||
/// As SMLib keeps such a solid, each cap is one loop through the corner twice, the line two
|
||||
/// edges (one per wedge of material) between the corner's two vertices: SMLib finds it valid.
|
||||
/// The caps had been an outline and a hole, each with its own vertex at the corner, which
|
||||
/// SMLib refuses (a face with coincident vertices, loops touching).
|
||||
#[test]
|
||||
fn a_cut_out_touching_the_outline_at_a_corner_caps_with_one_loop() {
|
||||
let at = |x: f64, y: f64, z: f64| Vec3::new(x, y, z);
|
||||
let outline = [(0.0, 0.0), (10.0, 0.0), (10.0, 6.0), (0.0, 6.0), (0.0, 3.0)];
|
||||
// Clockwise: a hole.
|
||||
let hole = [(0.0, 3.0), (2.0, 3.5), (2.0, 2.5)];
|
||||
let ring = |pts: &[(f64, f64)], z: f64| -> Vec<EdgeSpec> {
|
||||
(0..pts.len())
|
||||
.map(|i| EdgeSpec::Line { a: at(pts[i].0, pts[i].1, z), b: at(pts[(i + 1) % pts.len()].0, pts[(i + 1) % pts.len()].1, z) })
|
||||
.collect()
|
||||
};
|
||||
let down = |l: Vec<EdgeSpec>| -> Vec<EdgeSpec> { l.into_iter().rev().map(|e| e.reversed()).collect() };
|
||||
let mut faces = vec![
|
||||
FaceSpec { loops: vec![ring(&outline, 2.0), ring(&hole, 2.0)], surface: SurfSpec::Plane, probe: None },
|
||||
FaceSpec { loops: vec![down(ring(&outline, 0.0)), down(ring(&hole, 0.0))], surface: SurfSpec::Plane, probe: None },
|
||||
];
|
||||
for pts in [&outline[..], &hole[..]] {
|
||||
for i in 0..pts.len() {
|
||||
let ((ax, ay), (bx, by)) = (pts[i], pts[(i + 1) % pts.len()]);
|
||||
faces.push(quad(at(ax, ay, 0.0), at(bx, by, 0.0), at(bx, by, 2.0), at(ax, ay, 2.0)));
|
||||
}
|
||||
}
|
||||
let body = sew_touching(&faces).unwrap();
|
||||
assert!((crate::measure(&body).unwrap().volume - 118.0).abs() < 1e-9);
|
||||
let record = crate::brep_record(&body).unwrap();
|
||||
let caps: Vec<&b::Face> =
|
||||
record.faces.iter().filter(|f| f.loops.len() == 1 && matches!(&f.loops[0], b::Loop::Edges { edges } if edges.len() == 8)).collect();
|
||||
assert_eq!(caps.len(), 2, "each cap one loop of the outline's five edges and the cut-out's three");
|
||||
assert_eq!(record.vertices.len(), 14, "the corner one vertex on each cap");
|
||||
let v = crate::validity(&body);
|
||||
assert!(v.ok(), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sewn_box() {
|
||||
let p = |x: f64, y: f64, z: f64| Vec3::new(x, y, z);
|
||||
|
||||
Reference in New Issue
Block a user