mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
kernel: planar sheets are built one simple loop at a time
Seven sheet-metal cases failed in smlib_planar_faces (status 1001/1006). Every refused
curve list, in the rig's dumps, has curves of which more than two meet at one end point:
flange regions of one flat pattern touching at their corners (405, 349 x2, 436, 1481, 725,
723) and an outline passing a point twice where a cut-out's corner lies on the sheet's edge
(1095). SmBrep::CreatePlanarFacesWith3DCurves, behind SmApiCreatePlanarFaces, documents
that it fails for such "spine points (where more than two curves share a common
end-point)" (SmBrep.cpp); the sheet handed it all of an operand's loops at once.
A sheet is now built one simple loop at a time: each loop (outer counter-clockwise, holes
clockwise) split where it passes a point twice, each simple loop its own planar face, the
counter-clockwise ones united and the clockwise ones taken away with SMLib's 2D boolean,
which takes faces touching at a point. extrude_planar_regions uses the same sheet.
The read-back keeps the contract the regions had: a face's loop that passes a point twice
(a cut-out touching the outline) comes back as simple loops, counter-clockwise pieces
material and clockwise ones holes of the piece that holds them.
Release CLI, SOLIDCRAFT_KERNEL=smlib: 405, 349 (72 and 90 degrees), 436, 1481, 723 and 725
pass. 1095 now gets past the planar regions; its extrusion of a plate with a cut-out
touching its edge at one point fails in SMLib's export ("a closed shell bounds two regions
and encloses one"). Every other sheet-metal case fails or passes as in rig-1, and 529
(a hang in rig-1) passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
+92
-40
@@ -33,7 +33,7 @@ pub fn planar_regions_boolean(a: &[Region2], b: &[Region2], op: BoolOp, toleranc
|
||||
(false, true, _) => return Ok(a.to_vec()),
|
||||
_ => {}
|
||||
}
|
||||
let r = sheets::combine(&sheets::sheet(a)?, &sheets::sheet(b)?, op)?;
|
||||
let r = sheets::combine(&sheets::sheet(a, tolerance)?, &sheets::sheet(b, tolerance)?, op)?;
|
||||
Ok(match r {
|
||||
Some(sheet) => sheets::regions(&sheet, tolerance)?.regions,
|
||||
None => Vec::new(),
|
||||
@@ -80,8 +80,8 @@ fn check_regions<'a>(regions: impl Iterator<Item = &'a Region2>, tol: f64) -> Re
|
||||
}
|
||||
|
||||
/// Extrude normalized planar material as one logical compound, retaining every disjoint
|
||||
/// region and its holes. The regions are united in the plane first (each its own operand, so
|
||||
/// overlapping ones merge), and pieces of the union that touch each other are refused: apart,
|
||||
/// region and its holes. The regions are united in the plane first (overlapping ones merge),
|
||||
/// and pieces of the union that touch each other are refused: apart,
|
||||
/// each is extruded on its own and the solids joined as one body.
|
||||
pub fn extrude_planar_regions(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Result<Body> {
|
||||
crate::guard("planar material extrusion", || {
|
||||
@@ -89,20 +89,8 @@ pub fn extrude_planar_regions(plane: &Plane, regions: &[Region2], start: f64, en
|
||||
if regions.is_empty() {
|
||||
return Err(KernelError::Invalid("empty planar material extrusion".into()));
|
||||
}
|
||||
// Balanced unions: each operand a sheet of regions apart from each other.
|
||||
let mut level = regions.iter().map(|r| sheets::sheet(std::slice::from_ref(r))).collect::<Result<Vec<_>>>()?;
|
||||
while level.len() > 1 {
|
||||
let mut next = Vec::with_capacity(level.len().div_ceil(2));
|
||||
let mut iter = level.into_iter();
|
||||
while let Some(a) = iter.next() {
|
||||
match iter.next() {
|
||||
Some(b) => next.push(sheets::combine(&a, &b, BoolOp::Union)?.ok_or_else(|| KernelError::Failed("empty planar union".into()))?),
|
||||
None => next.push(a),
|
||||
}
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
let union = level.pop().ok_or_else(|| KernelError::Invalid("empty planar material extrusion".into()))?;
|
||||
// One sheet of all the regions: their material united, so overlapping ones merge.
|
||||
let union = sheets::sheet(regions, 1e-7)?;
|
||||
let united = sheets::regions(&union, 1e-7)?;
|
||||
if united.touching {
|
||||
return Err(KernelError::Invalid("planar material pieces touch at a boundary".into()));
|
||||
@@ -134,17 +122,74 @@ mod sheets {
|
||||
KernelError::Failed(format!("SMLib planar regions: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `regions` (in the plane's own coordinates) as a sheet of planar faces on z = 0.
|
||||
pub(super) fn sheet(regions: &[Region2]) -> Result<Brep> {
|
||||
let curves = regions
|
||||
.iter()
|
||||
.flat_map(|r| std::iter::once(&r.outer).chain(&r.holes))
|
||||
.flat_map(|l| &l.segs)
|
||||
.map(|s| crate::smlib_sweeps::curve(&Plane::XY, s))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
/// `regions` (in the plane's own coordinates) as one sheet on z = 0. SMLib's face builder
|
||||
/// refuses curves of which more than two meet at one end point (`CreatePlanarFacesWith3DCurves`,
|
||||
/// SmBrep.cpp: "spine points"), as where pieces of material touch at a corner or an outline
|
||||
/// passes a point twice. So each loop, oriented (outer counter-clockwise, holes clockwise), is
|
||||
/// split where it passes a point twice into simple loops, and each simple loop is a face of its
|
||||
/// own; those running counter-clockwise are material, the others holes. The sheet is the
|
||||
/// material united less the holes united (SMLib's 2D boolean, which takes touching faces).
|
||||
/// `tol` (mm): how close a loop's passes through one point are.
|
||||
pub(super) fn sheet(regions: &[Region2], tol: f64) -> Result<Brep> {
|
||||
let (mut material, mut holes) = (Vec::new(), Vec::new());
|
||||
for r in regions {
|
||||
let loops = std::iter::once(r.outer.ccw()).chain(r.holes.iter().map(|h| h.ccw().reversed()));
|
||||
for piece in loops.flat_map(|lp| simple_loops(lp, tol * 10.0)) {
|
||||
let face = face(&piece)?;
|
||||
if piece.signed_area() > 0.0 { material.push(face) } else { holes.push(face) }
|
||||
}
|
||||
}
|
||||
let material = united(material)?.ok_or_else(|| failed("sheet", "the regions hold no material"))?;
|
||||
match united(holes)? {
|
||||
Some(holes) => combine(&material, &holes, BoolOp::Cut)?.ok_or_else(|| failed("sheet", "the holes take all the material")),
|
||||
None => Ok(material),
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple closed loop as one planar face.
|
||||
fn face(lp: &Loop2) -> Result<Brep> {
|
||||
let curves = lp.segs.iter().map(|s| crate::smlib_sweeps::curve(&Plane::XY, s)).collect::<Result<Vec<_>>>()?;
|
||||
Brep::planar_faces(&curves).map_err(|e| failed("sheet", e))
|
||||
}
|
||||
|
||||
/// `sheets` united in balanced rounds (each union of two sheets of about the same size);
|
||||
/// `None` for none.
|
||||
fn united(mut sheets: Vec<Brep>) -> Result<Option<Brep>> {
|
||||
while sheets.len() > 1 {
|
||||
let mut next = Vec::with_capacity(sheets.len().div_ceil(2));
|
||||
let mut iter = sheets.into_iter();
|
||||
while let Some(a) = iter.next() {
|
||||
next.push(match iter.next() {
|
||||
Some(b) => combine(&a, &b, BoolOp::Union)?.ok_or_else(|| failed("sheet", "a union of faces is empty"))?,
|
||||
None => a,
|
||||
});
|
||||
}
|
||||
sheets = next;
|
||||
}
|
||||
Ok(sheets.pop())
|
||||
}
|
||||
|
||||
/// A closed loop split where it passes a point twice (its segments' starts within `near`)
|
||||
/// into loops that pass every point once.
|
||||
fn simple_loops(lp: Loop2, near: f64) -> Vec<Loop2> {
|
||||
let mut todo = vec![lp.segs];
|
||||
let mut out = Vec::new();
|
||||
// Each split leaves two shorter loops, so the work is bounded by the segment count.
|
||||
while let Some(segs) = todo.pop() {
|
||||
let starts: Vec<Vec2> = segs.iter().map(Seg2::start).collect();
|
||||
let repeat = (0..starts.len()).find_map(|i| (i + 1..starts.len()).find(|&j| starts[i].dist(starts[j]) < near).map(|j| (i, j)));
|
||||
match repeat {
|
||||
Some((i, j)) => {
|
||||
let (inner, rest) = (segs[i..j].to_vec(), [&segs[j..], &segs[..i]].concat());
|
||||
todo.push(inner);
|
||||
todo.push(rest);
|
||||
}
|
||||
None => out.push(Loop2 { segs }),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `a` and `b` combined by `op`; `None` when nothing is left.
|
||||
pub(super) fn combine(a: &Brep, b: &Brep, op: BoolOp) -> Result<Option<Brep>> {
|
||||
let op = match op {
|
||||
@@ -171,9 +216,11 @@ mod sheets {
|
||||
if up.abs() < 1.0 - 1e-9 {
|
||||
return Err(failed("face", format!("face {f} is not in the plane of the regions")));
|
||||
}
|
||||
let mut outer = None;
|
||||
let mut holes = Vec::new();
|
||||
for (l, lp) in face.loops.iter().enumerate() {
|
||||
// Each loop as it runs in the plane, then split where it passes a point twice (a
|
||||
// cut-out touching the outline, material meeting at a corner): counter-clockwise
|
||||
// pieces are material, the others holes in the material that holds them.
|
||||
let mut pieces = Vec::new();
|
||||
for lp in &face.loops {
|
||||
let Loop::Edges { edges } = lp else { return Err(failed("face", format!("face {f} has a vertex loop"))) };
|
||||
let mut segs = Vec::new();
|
||||
for e in edges {
|
||||
@@ -187,18 +234,23 @@ mod sheets {
|
||||
touching |= *owner.entry(v).or_insert(f) != f;
|
||||
}
|
||||
}
|
||||
let lp = Loop2 { segs };
|
||||
// Seen from the face's normal the outer loop runs counter-clockwise.
|
||||
if lp.signed_area() * up > 0.0 {
|
||||
if outer.replace(lp.ccw()).is_some() {
|
||||
return Err(failed("face", format!("face {f} has two outer loops (loop {l})")));
|
||||
}
|
||||
} else {
|
||||
holes.push(lp.ccw().reversed());
|
||||
}
|
||||
// Seen from the face's normal, the outer loop runs counter-clockwise and holes clockwise.
|
||||
let lp = if up > 0.0 { Loop2 { segs } } else { Loop2 { segs }.reversed() };
|
||||
pieces.extend(simple_loops(lp, tol * 10.0));
|
||||
}
|
||||
let (outers, holes): (Vec<Loop2>, Vec<Loop2>) = pieces.into_iter().partition(|lp| lp.signed_area() > 0.0);
|
||||
let first = out.len();
|
||||
out.extend(outers.into_iter().map(|outer| Region2 { outer, holes: Vec::new() }));
|
||||
for hole in holes {
|
||||
// The smallest material piece of this face holding the hole's middle.
|
||||
let mid = hole.segs.first().map(|s| s.point_at(0.5)).ok_or_else(|| failed("face", format!("face {f} has an empty loop")))?;
|
||||
let owner = out[first..]
|
||||
.iter_mut()
|
||||
.filter(|r| r.outer.contains(mid))
|
||||
.min_by(|a, b| a.outer.signed_area().total_cmp(&b.outer.signed_area()))
|
||||
.ok_or_else(|| failed("face", format!("face {f} has a hole outside its material")))?;
|
||||
owner.holes.push(hole);
|
||||
}
|
||||
let outer = outer.ok_or_else(|| failed("face", format!("face {f} has no outer loop")))?;
|
||||
out.push(Region2 { outer, holes });
|
||||
}
|
||||
Ok(Regions { regions: out, touching })
|
||||
}
|
||||
@@ -225,7 +277,7 @@ mod sheets {
|
||||
/// A sheet stands for nothing without SMLib.
|
||||
pub(super) enum Sheet {}
|
||||
|
||||
pub(super) fn sheet(_: &[Region2]) -> Result<Sheet> {
|
||||
pub(super) fn sheet(_: &[Region2], _: f64) -> Result<Sheet> {
|
||||
refused()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user