feat(kernel): every boolean path reports exact face and edge history
ops::boolean's result carries its history over [a, b] whichever path made it, recorded by the code that builds the faces: - truck path (incl. jitter retries): shapeops history mapped through the order-preserving copies and the cut's turned tool, then through healing - heal records which input faces each healed face merges and which input edges each joined or exact edge replaces (HealMap); opt-in, so heal_keep callers pay nothing - trivial shortcuts, the void, glue, faces pushed apart (push_faces records its own history; the slab cut back on a union maps to the operands' faces in that plane), the nudged retry, by_lumps and join composition - prism booleans: section segments carry tags (operand cap edge, the wall beside it, its vertices) through the 2D boolean; walls, caps and edges of the extruded or sewn result follow from them - planar and faceted polygon booleans: polygons carry their operand face; faces join the pieces across edges, edges come from the pieces either side History gains renumber/over (and consuming forms) for composition; record gains regions/traced/build helpers. truck-shapeops *_with_history now return the solid with an Option history. Paths that cannot report (bore narrowing via offset, inexpressible lineage) leave the result without history. Boolean results, tolerances and fallback order are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
1 parent
d69ebbee1b
commit
d5cb276965
16 files changed
+2169
-312
No files matched your search
+461
-179
@@ -12,6 +12,9 @@ use truck_modeling as mt;
|
||||
use crate::body::{Body, from_p3};
|
||||
use crate::{BoolOp, KernelError, Result};
|
||||
|
||||
#[path = "prism_lineage.rs"]
|
||||
mod lineage;
|
||||
|
||||
const MAX_PLANAR_SEGMENTS: usize = 8192;
|
||||
const MAX_PLANAR_EVENTS: usize = 131_072;
|
||||
const MAX_PLANAR_PAIRS: usize = 16_000_000;
|
||||
@@ -113,11 +116,222 @@ pub fn extrude_planar_regions(plane: &Plane, regions: &[Region2], start: f64, en
|
||||
})
|
||||
}
|
||||
|
||||
/// A prism: the plane of its bottom cap (normal along the walls), its height and section.
|
||||
/// A prism: the plane of its bottom cap (normal along the walls), its height and section (its
|
||||
/// segments' tags alongside).
|
||||
struct Prism {
|
||||
lo: f64,
|
||||
hi: f64,
|
||||
regions: Vec<Region2>,
|
||||
tags: Vec<Vec<Vec<Tag>>>,
|
||||
}
|
||||
|
||||
impl Prism {
|
||||
fn new(lo: f64, hi: f64, section: Section) -> Prism {
|
||||
Prism { lo, hi, regions: section.regions, tags: section.tags }
|
||||
}
|
||||
|
||||
fn section(&self) -> Section {
|
||||
Section { regions: self.regions.clone(), tags: self.tags.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a section's segment was made from, for the history of what is built over it: an
|
||||
/// operand's cap edge (`edge`, by its place in the operand's edge order), the wall face beside it
|
||||
/// (`wall`), and the operand's vertices (by [`Index`]) the segment starts and ends at (`None`
|
||||
/// where it was split off a longer one). `None` fields: not known (no history then).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Tag {
|
||||
pub operand: usize,
|
||||
pub wall: Option<usize>,
|
||||
pub edge: Option<usize>,
|
||||
pub start: Option<usize>,
|
||||
pub end: Option<usize>,
|
||||
}
|
||||
|
||||
/// A segment nothing is known of.
|
||||
const BLANK: Tag = Tag { operand: 0, wall: None, edge: None, start: None, end: None };
|
||||
|
||||
impl Tag {
|
||||
/// The segment run the other way.
|
||||
fn reversed(self) -> Tag {
|
||||
Tag { start: self.end, end: self.start, ..self }
|
||||
}
|
||||
|
||||
/// Piece `k` of `n` the segment was split into, in its direction.
|
||||
fn piece(self, k: usize, n: usize) -> Tag {
|
||||
Tag { start: if k == 0 { self.start } else { None }, end: if k + 1 == n { self.end } else { None }, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// An operand's [`Index`], built when first needed (only bodies that are prisms are read).
|
||||
pub(crate) struct LazyIndex<'a> {
|
||||
body: &'a Body,
|
||||
operand: usize,
|
||||
index: std::cell::OnceCell<Index>,
|
||||
}
|
||||
|
||||
impl<'a> LazyIndex<'a> {
|
||||
fn new(body: &'a Body, operand: usize) -> LazyIndex<'a> {
|
||||
LazyIndex { body, operand, index: std::cell::OnceCell::new() }
|
||||
}
|
||||
|
||||
fn get(&self) -> &Index {
|
||||
self.index.get_or_init(|| Index::new(self.body, self.operand))
|
||||
}
|
||||
}
|
||||
|
||||
/// Regions with a tag for each segment: per region, per loop (outer, then holes), per segment.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct Section {
|
||||
pub regions: Vec<Region2>,
|
||||
pub tags: Vec<Vec<Vec<Tag>>>,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
/// Regions whose segments nothing is known of.
|
||||
fn untagged(regions: &[Region2]) -> Section {
|
||||
let tags = regions.iter().map(|r| std::iter::once(&r.outer).chain(&r.holes).map(|l| vec![BLANK; l.segs.len()]).collect()).collect();
|
||||
Section { regions: regions.to_vec(), tags }
|
||||
}
|
||||
|
||||
/// The tag of segment `s` of loop `l` of region `r` (blank where missing).
|
||||
fn tag(&self, r: usize, l: usize, s: usize) -> Tag {
|
||||
self.tags.get(r).and_then(|t| t.get(l)).and_then(|t| t.get(s)).copied().unwrap_or(BLANK)
|
||||
}
|
||||
|
||||
/// The tags of region `r`'s loops, as long as its loops (blank where missing).
|
||||
fn region_tags(&self, r: usize) -> Vec<Vec<Tag>> {
|
||||
self.regions
|
||||
.get(r)
|
||||
.map(|reg| {
|
||||
std::iter::once(®.outer)
|
||||
.chain(®.holes)
|
||||
.enumerate()
|
||||
.map(|(l, lp)| (0..lp.segs.len()).map(|s| self.tag(r, l, s)).collect())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// One region from its loops (each segment tagged): the loop with the largest area is the
|
||||
/// outer one.
|
||||
fn from_loops(loops: Vec<(Loop2, Vec<Tag>)>) -> Option<Section> {
|
||||
let ((outer, outer_tags), holes) = split_outer(loops)?;
|
||||
let (holes, hole_tags): (Vec<Loop2>, Vec<Vec<Tag>>) = holes.into_iter().unzip();
|
||||
Some(Section { regions: vec![Region2 { outer, holes }], tags: vec![std::iter::once(outer_tags).chain(hole_tags).collect()] })
|
||||
}
|
||||
|
||||
/// Region `r` alone.
|
||||
fn region(&self, r: usize) -> Section {
|
||||
Section { regions: self.regions.get(r).cloned().into_iter().collect(), tags: vec![self.region_tags(r)] }
|
||||
}
|
||||
|
||||
/// Every segment in order (regions, then loops) with its tag.
|
||||
fn segments(&self) -> (Vec<Seg2>, Vec<Tag>) {
|
||||
let mut out = (Vec::new(), Vec::new());
|
||||
for (r, reg) in self.regions.iter().enumerate() {
|
||||
for (l, lp) in std::iter::once(®.outer).chain(®.holes).enumerate() {
|
||||
for (s, seg) in lp.segs.iter().enumerate() {
|
||||
out.0.push(*seg);
|
||||
out.1.push(self.tag(r, l, s));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Loops oriented: outers counter-clockwise, holes clockwise; the tags following their
|
||||
/// segments.
|
||||
fn oriented(&self) -> Section {
|
||||
let turn = |lp: &Loop2, tags: Vec<Tag>, reverse: bool| -> (Loop2, Vec<Tag>) {
|
||||
if reverse { (lp.reversed(), tags.into_iter().rev().map(Tag::reversed).collect()) } else { (lp.clone(), tags) }
|
||||
};
|
||||
let mut out = Section::default();
|
||||
for (r, reg) in self.regions.iter().enumerate() {
|
||||
let mut tags = self.region_tags(r).into_iter();
|
||||
let (outer, outer_tags) = turn(®.outer, tags.next().unwrap_or_default(), reg.outer.signed_area() < 0.0);
|
||||
let mut loop_tags = vec![outer_tags];
|
||||
let mut holes = Vec::new();
|
||||
for h in ®.holes {
|
||||
let (hole, hole_tags) = turn(h, tags.next().unwrap_or_default(), h.signed_area() > 0.0);
|
||||
holes.push(hole);
|
||||
loop_tags.push(hole_tags);
|
||||
}
|
||||
out.regions.push(Region2 { outer, holes });
|
||||
out.tags.push(loop_tags);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// An operand's faces, edges and vertices by identity, to tag the section segments read off its
|
||||
/// caps ([`Tag`]).
|
||||
pub(crate) struct Index {
|
||||
operand: usize,
|
||||
/// Each edge's place in the edge order.
|
||||
edges: std::collections::HashMap<mt::EdgeID, usize>,
|
||||
/// Per edge (edge order): the faces it bounds, and its ends (by vertex number).
|
||||
bounds: Vec<(Vec<usize>, Option<usize>, Option<usize>)>,
|
||||
/// The edges between each pair of faces (lesser first; a face twice for a seam).
|
||||
between: std::collections::HashMap<(usize, usize), Vec<usize>>,
|
||||
/// Each vertex's number (first seen along the edge order).
|
||||
vertices: std::collections::HashMap<mt::VertexID, usize>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
fn new(b: &Body, operand: usize) -> Index {
|
||||
let (edges, bounds) = crate::body::edge_order(b.solid.face_iter());
|
||||
let mut vertices = std::collections::HashMap::new();
|
||||
for e in &edges {
|
||||
for v in [e.absolute_front(), e.absolute_back()] {
|
||||
let next = vertices.len();
|
||||
vertices.entry(v.id()).or_insert(next);
|
||||
}
|
||||
}
|
||||
let mut between: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, fs) in bounds.iter().enumerate() {
|
||||
match fs.as_slice() {
|
||||
[f] => between.entry((*f, *f)).or_default().push(i),
|
||||
[f, g] => between.entry((*f.min(g), *f.max(g))).or_default().push(i),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let bounds = edges
|
||||
.iter()
|
||||
.zip(bounds)
|
||||
.map(|(e, fs)| (fs, vertices.get(&e.absolute_front().id()).copied(), vertices.get(&e.absolute_back().id()).copied()))
|
||||
.collect();
|
||||
Index { operand, edges: crate::record::edge_index(&b.solid), bounds, between, vertices }
|
||||
}
|
||||
|
||||
/// The tag of the segments read off edge `e` (as it runs in a wire) of cap face `cap`.
|
||||
fn tag(&self, e: &mt::Edge, cap: usize) -> Tag {
|
||||
let edge = self.edges.get(&e.id()).copied();
|
||||
let wall = edge.and_then(|i| self.bounds.get(i)).and_then(|(fs, _, _)| match fs.as_slice() {
|
||||
[x, y] if *x == cap => Some(*y),
|
||||
[x, y] if *y == cap => Some(*x),
|
||||
_ => None,
|
||||
});
|
||||
Tag { operand: self.operand, wall, edge, start: self.vertices.get(&e.front().id()).copied(), end: self.vertices.get(&e.back().id()).copied() }
|
||||
}
|
||||
|
||||
/// The loops of cap face `f` (number `cap`) as segments, each with its tag; `None` where an
|
||||
/// edge isn't a line or an arc in the plane.
|
||||
fn loops(&self, f: &mt::Face, cap: usize, plane: &Plane, tol: f64, work: &mut usize, segments: &mut usize) -> Option<Vec<(Loop2, Vec<Tag>)>> {
|
||||
let mut loops = Vec::new();
|
||||
for w in f.boundaries() {
|
||||
let (mut segs, mut tags) = (Vec::new(), Vec::new());
|
||||
for e in w.edge_iter() {
|
||||
let pieces = crate::prism_curve::segment(e, plane, tol, work)?;
|
||||
*segments = segments.checked_sub(pieces.len())?;
|
||||
let tag = self.tag(e, cap);
|
||||
tags.extend((0..pieces.len()).map(|k| tag.piece(k, pieces.len())));
|
||||
segs.extend(pieces);
|
||||
}
|
||||
loops.push((Loop2 { segs }, tags));
|
||||
}
|
||||
Some(loops)
|
||||
}
|
||||
}
|
||||
|
||||
/// Certify that a curved wall is swept along `d` from its surface representation.
|
||||
@@ -209,19 +423,25 @@ fn wall_along(surface: &mt::Surface, d: Vec3, tol: f64, remaining: &mut usize) -
|
||||
}
|
||||
|
||||
/// The body as a prism along `d` (unit), seen in `plane` (normal `d`).
|
||||
#[cfg(test)]
|
||||
fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Prism> {
|
||||
prism_tagged(b, d, plane, tol, &LazyIndex::new(b, 0))
|
||||
}
|
||||
|
||||
/// [`prism_of`], its section's segments tagged from `index` (the body's).
|
||||
fn prism_tagged(b: &Body, d: Vec3, plane: &Plane, tol: f64, index: &LazyIndex) -> Option<Prism> {
|
||||
// A simple extrusion with N section edges has N walls plus its cap faces.
|
||||
// Even disconnected two-arc sections need at most 2N faces. Keep that
|
||||
// complete planar arrangement envelope, independently of stepped limits.
|
||||
let mut remaining_faces = MAX_PLANAR_SEGMENTS * 2;
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = f64::NEG_INFINITY;
|
||||
let mut tops: Vec<&mt::Face> = Vec::new();
|
||||
let mut tops: Vec<(usize, &mt::Face)> = Vec::new();
|
||||
let mut top_h = Vec::new();
|
||||
let mut bottom_h = Vec::new();
|
||||
let mut wall_work = 262_144;
|
||||
let mut curve_work = 2_000_000usize;
|
||||
for f in b.solid.face_iter() {
|
||||
for (fi, f) in b.solid.face_iter().enumerate() {
|
||||
remaining_faces = remaining_faces.checked_sub(1)?;
|
||||
curve_work = curve_work.checked_sub(1)?;
|
||||
let surf = f.oriented_surface();
|
||||
@@ -230,7 +450,7 @@ fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Prism> {
|
||||
let n = Vec3::new(n.x, n.y, n.z).normalized()?;
|
||||
let h = plane.height(from_p3(p.origin()));
|
||||
if n.dot(d) > 1.0 - 1e-9 {
|
||||
tops.push(f);
|
||||
tops.push((fi, f));
|
||||
top_h.push(h);
|
||||
hi = hi.max(h);
|
||||
continue;
|
||||
@@ -253,30 +473,22 @@ fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Prism> {
|
||||
if top_h.iter().any(|h| (h - hi).abs() > tol) || bottom_h.iter().any(|h| (h - lo).abs() > tol) || !(lo.is_finite() && hi > lo + tol) {
|
||||
return None;
|
||||
}
|
||||
let mut regions = Vec::new();
|
||||
let mut section = Section::default();
|
||||
let mut remaining_segments = MAX_PLANAR_SEGMENTS;
|
||||
let mut remaining_loops = MAX_PLANAR_SEGMENTS;
|
||||
for f in tops {
|
||||
let mut loops = Vec::new();
|
||||
for w in f.boundaries() {
|
||||
remaining_loops = remaining_loops.checked_sub(1)?;
|
||||
let mut segs = Vec::new();
|
||||
for e in w.edge_iter() {
|
||||
let segments = crate::prism_curve::segment(e, plane, tol, &mut curve_work)?;
|
||||
remaining_segments = remaining_segments.checked_sub(segments.len())?;
|
||||
segs.extend(segments);
|
||||
}
|
||||
loops.push(Loop2 { segs });
|
||||
}
|
||||
let (outer, holes) = split_outer(loops)?;
|
||||
regions.push(Region2 { outer, holes });
|
||||
for (fi, f) in tops {
|
||||
remaining_loops = remaining_loops.checked_sub(f.boundaries().len())?;
|
||||
let loops = index.get().loops(f, fi, plane, tol, &mut curve_work, &mut remaining_segments)?;
|
||||
let one = Section::from_loops(loops)?;
|
||||
section.regions.extend(one.regions);
|
||||
section.tags.extend(one.tags);
|
||||
}
|
||||
Some(Prism { lo, hi, regions })
|
||||
Some(Prism::new(lo, hi, section))
|
||||
}
|
||||
|
||||
/// The loop with the largest area is the outer one.
|
||||
fn split_outer(mut loops: Vec<Loop2>) -> Option<(Loop2, Vec<Loop2>)> {
|
||||
let i = (0..loops.len()).max_by(|a, b| loops[*a].signed_area().abs().total_cmp(&loops[*b].signed_area().abs()))?;
|
||||
fn split_outer<T>(mut loops: Vec<(Loop2, T)>) -> Option<((Loop2, T), Vec<(Loop2, T)>)> {
|
||||
let i = (0..loops.len()).max_by(|a, b| loops[*a].0.signed_area().abs().total_cmp(&loops[*b].0.signed_area().abs()))?;
|
||||
let outer = loops.remove(i);
|
||||
Some((outer, loops))
|
||||
}
|
||||
@@ -435,29 +647,23 @@ fn dir_at(regions: &[Region2], p: Vec2, tol: f64) -> Option<Vec2> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Loops oriented: outers counter-clockwise, holes clockwise.
|
||||
fn oriented(regions: &[Region2]) -> Vec<Region2> {
|
||||
regions
|
||||
.iter()
|
||||
.map(|r| Region2 {
|
||||
outer: if r.outer.signed_area() < 0.0 { r.outer.reversed() } else { r.outer.clone() },
|
||||
holes: r.holes.iter().map(|h| if h.signed_area() > 0.0 { h.reversed() } else { h.clone() }).collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 2D boolean of regions bounded by lines and arcs.
|
||||
fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<Region2>> {
|
||||
boolean2d_tagged(&Section::untagged(a), &Section::untagged(b), op, tol).map(|s| s.regions)
|
||||
}
|
||||
|
||||
/// [`boolean2d`] of tagged sections: each segment of the result tagged as the segment of `a` or
|
||||
/// `b` it is (a piece of).
|
||||
fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option<Section> {
|
||||
let count =
|
||||
|rs: &[Region2]| rs.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).fold(0usize, |n, lp| n.saturating_add(lp.segs.len()));
|
||||
let (na, nb) = (count(a), count(b));
|
||||
let (na, nb) = (count(&a.regions), count(&b.regions));
|
||||
if na.saturating_add(nb) > MAX_PLANAR_SEGMENTS || na.saturating_mul(nb) > MAX_PLANAR_PAIRS {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (oriented(a), oriented(b));
|
||||
let segs =
|
||||
|rs: &[Region2]| -> Vec<Seg2> { rs.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).flat_map(|l| l.segs.clone()).collect() };
|
||||
let (sa, sb) = (segs(&a), segs(&b));
|
||||
let (a, b) = (a.oriented(), b.oriented());
|
||||
let ((sa, ta), (sb, tb)) = (a.segments(), b.segments());
|
||||
let (a, b) = (a.regions, b.regions);
|
||||
if sa.iter().chain(&sb).any(|s| !matches!(s, Seg2::Line { .. } | Seg2::Arc { .. })) {
|
||||
return None;
|
||||
}
|
||||
@@ -526,13 +732,15 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
out.push(rest);
|
||||
out
|
||||
};
|
||||
let mut kept: Vec<Seg2> = Vec::new();
|
||||
let mut kept: Vec<(Seg2, Tag)> = Vec::new();
|
||||
// A's pieces.
|
||||
for (i, s) in sa.iter().enumerate() {
|
||||
if cut_a.get(i)?.len() > MAX_PLANAR_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
for p in pieces(s, cut_a.get_mut(i)?) {
|
||||
let split = pieces(s, cut_a.get_mut(i)?);
|
||||
let (n, tag) = (split.len(), ta.get(i).copied().unwrap_or(BLANK));
|
||||
for (k, p) in split.into_iter().enumerate() {
|
||||
let m = p.point_at(0.5);
|
||||
let on = dist_to(&b, m) < tol;
|
||||
let keep = if on {
|
||||
@@ -552,7 +760,7 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
if kept.len() >= MAX_PLANAR_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
kept.push(p);
|
||||
kept.push((p, tag.piece(k, n)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,7 +769,9 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
if cut_b.get(j)?.len() > MAX_PLANAR_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
for p in pieces(t, cut_b.get_mut(j)?) {
|
||||
let split = pieces(t, cut_b.get_mut(j)?);
|
||||
let (n, tag) = (split.len(), tb.get(j).copied().unwrap_or(BLANK));
|
||||
for (k, p) in split.into_iter().enumerate() {
|
||||
let m = p.point_at(0.5);
|
||||
if dist_to(&a, m) < tol {
|
||||
continue;
|
||||
@@ -570,26 +780,28 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
if kept.len() >= MAX_PLANAR_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
let tag = tag.piece(k, n);
|
||||
match op {
|
||||
BoolOp::Union if !ins => kept.push(p),
|
||||
BoolOp::Intersect if ins => kept.push(p),
|
||||
BoolOp::Cut if ins => kept.push(p.reversed()),
|
||||
BoolOp::Union if !ins => kept.push((p, tag)),
|
||||
BoolOp::Intersect if ins => kept.push((p, tag)),
|
||||
BoolOp::Cut if ins => kept.push((p.reversed(), tag.reversed())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chain into loops.
|
||||
let mut loops: Vec<Loop2> = Vec::new();
|
||||
let mut loops: Vec<(Loop2, Vec<Tag>)> = Vec::new();
|
||||
let mut used = vec![false; kept.len()];
|
||||
for start in 0..kept.len() {
|
||||
if used.get(start).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let mut segs = vec![*kept.get(start)?];
|
||||
let (seg, tag) = *kept.get(start)?;
|
||||
let (mut segs, mut tags) = (vec![seg], vec![tag]);
|
||||
if let Some(u) = used.get_mut(start) {
|
||||
*u = true;
|
||||
}
|
||||
let first = kept.get(start)?.start();
|
||||
let first = seg.start();
|
||||
let mut guard = 0;
|
||||
loop {
|
||||
guard += 1;
|
||||
@@ -604,8 +816,9 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
}
|
||||
let next = (0..kept.len())
|
||||
.filter(|k| !used.get(*k).copied().unwrap_or(true))
|
||||
.min_by(|x, y| kept[*x].start().dist(end).total_cmp(&kept[*y].start().dist(end)))?;
|
||||
if kept.get(next)?.start().dist(end) > tol * 10.0 {
|
||||
.min_by(|x, y| kept[*x].0.start().dist(end).total_cmp(&kept[*y].0.start().dist(end)))?;
|
||||
let (seg, tag) = *kept.get(next)?;
|
||||
if seg.start().dist(end) > tol * 10.0 {
|
||||
if end.dist(first) < tol * 10.0 {
|
||||
break;
|
||||
}
|
||||
@@ -614,23 +827,29 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
|
||||
if let Some(u) = used.get_mut(next) {
|
||||
*u = true;
|
||||
}
|
||||
segs.push(*kept.get(next)?);
|
||||
segs.push(seg);
|
||||
tags.push(tag);
|
||||
}
|
||||
let l = Loop2 { segs };
|
||||
if l.signed_area().abs() > tol * tol {
|
||||
loops.push(l);
|
||||
loops.push((l, tags));
|
||||
}
|
||||
}
|
||||
// Outers (counter-clockwise) take the holes they contain, the smallest first.
|
||||
let (mut outers, holes): (Vec<Loop2>, Vec<Loop2>) = loops.into_iter().partition(|l| l.signed_area() > 0.0);
|
||||
outers.sort_by(|x, y| x.signed_area().abs().total_cmp(&y.signed_area().abs()));
|
||||
let mut regions: Vec<Region2> = outers.into_iter().map(|o| Region2 { outer: o, holes: Vec::new() }).collect();
|
||||
for h in holes {
|
||||
let p = h.segs.first()?.point_at(0.5);
|
||||
let r = regions.iter_mut().find(|r| inside_loop(&r.outer, p) && r.outer.signed_area().abs() > h.signed_area().abs())?;
|
||||
r.holes.push(h);
|
||||
let (mut outers, holes): (Vec<(Loop2, Vec<Tag>)>, Vec<(Loop2, Vec<Tag>)>) = loops.into_iter().partition(|(l, _)| l.signed_area() > 0.0);
|
||||
outers.sort_by(|x, y| x.0.signed_area().abs().total_cmp(&y.0.signed_area().abs()));
|
||||
let mut out = Section::default();
|
||||
for (o, tags) in outers {
|
||||
out.regions.push(Region2 { outer: o, holes: Vec::new() });
|
||||
out.tags.push(vec![tags]);
|
||||
}
|
||||
Some(regions)
|
||||
for (h, tags) in holes {
|
||||
let p = h.segs.first()?.point_at(0.5);
|
||||
let r = out.regions.iter().position(|r| inside_loop(&r.outer, p) && r.outer.signed_area().abs() > h.signed_area().abs())?;
|
||||
out.regions.get_mut(r)?.holes.push(h);
|
||||
out.tags.get_mut(r)?.push(tags);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The boolean done on prism sections, or `None` when it doesn't apply.
|
||||
@@ -667,27 +886,35 @@ fn split_seg(seg: &Seg2, pts: &[Vec2], tol: f64) -> Option<Vec<Seg2>> {
|
||||
/// a face down over the one above less the one below. Every outline is split where any other
|
||||
/// meets it, so the faces share their edges. `None` when a slab is empty or a curve isn't a
|
||||
/// line or an arc.
|
||||
fn sew_stack(plane: &Plane, d: Vec3, pa: &Prism, pb: &Prism, tol: f64) -> Option<Result<Body>> {
|
||||
fn sew_stack(plane: &Plane, d: Vec3, pa: &Prism, pb: &Prism, tol: f64, history: &lineage::Context) -> Option<Result<Body>> {
|
||||
let mut levels = vec![pa.lo, pa.hi, pb.lo, pb.hi];
|
||||
levels.sort_by(f64::total_cmp);
|
||||
levels.dedup_by(|x, y| (*x - *y).abs() < tol);
|
||||
let covers = |p: &Prism, z0: f64, z1: f64| p.lo <= z0 + tol && p.hi >= z1 - tol;
|
||||
let mut slabs: Vec<Vec<Region2>> = Vec::new();
|
||||
let mut slabs: Vec<Section> = Vec::new();
|
||||
for w in levels.windows(2) {
|
||||
let (z0, z1) = (*w.first()?, *w.get(1)?);
|
||||
let s = match (covers(pa, z0, z1), covers(pb, z0, z1)) {
|
||||
(true, true) => boolean2d(&pa.regions, &pb.regions, BoolOp::Union, tol)?,
|
||||
(true, false) => pa.regions.clone(),
|
||||
(false, true) => pb.regions.clone(),
|
||||
(true, true) => boolean2d_tagged(&pa.section(), &pb.section(), BoolOp::Union, tol)?,
|
||||
(true, false) => pa.section(),
|
||||
(false, true) => pb.section(),
|
||||
(false, false) => return None,
|
||||
};
|
||||
slabs.push(oriented(&s));
|
||||
slabs.push(s.oriented());
|
||||
}
|
||||
sew_sections(plane, d, &levels, &slabs, tol, false)
|
||||
sew_sections(plane, d, &levels, &slabs, tol, false, history)
|
||||
}
|
||||
|
||||
/// Sew the exact sections between successive levels, including empty gaps.
|
||||
fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>], tol: f64, coalesce: bool) -> Option<Result<Body>> {
|
||||
fn sew_sections(
|
||||
plane: &Plane,
|
||||
d: Vec3,
|
||||
levels: &[f64],
|
||||
slabs: &[Section],
|
||||
tol: f64,
|
||||
coalesce: bool,
|
||||
history: &lineage::Context,
|
||||
) -> Option<Result<Body>> {
|
||||
if levels.len() != slabs.len() + 1
|
||||
|| slabs.len() > 128
|
||||
|| plane.normal().dot(d) < 1. - 1e-12
|
||||
@@ -699,7 +926,7 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>],
|
||||
let mut blocks = Vec::new();
|
||||
let mut start = None;
|
||||
for (i, slab) in slabs.iter().enumerate() {
|
||||
if slab.is_empty() {
|
||||
if slab.regions.is_empty() {
|
||||
if let Some(first) = start.take() {
|
||||
blocks.push((first, i));
|
||||
}
|
||||
@@ -718,8 +945,9 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>],
|
||||
let mut remaining_constructed_faces = MAX_PLANAR_SEGMENTS * 2;
|
||||
let mut remaining_faces = MAX_PLANAR_SEGMENTS * 2;
|
||||
for (first, end) in blocks {
|
||||
let section = slabs.get(first).ok_or_else(|| KernelError::Failed("missing prism section".into()))?;
|
||||
if slabs[first..end].iter().all(|s| s == section) {
|
||||
let tagged = slabs.get(first).ok_or_else(|| KernelError::Failed("missing prism section".into()))?;
|
||||
let section = &tagged.regions;
|
||||
if slabs[first..end].iter().all(|s| s.regions == *section) {
|
||||
let edge_count = section.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).flat_map(|l| &l.segs).fold(0usize, |n, s| {
|
||||
n.saturating_add(match s {
|
||||
Seg2::Arc { sweep, .. } if sweep.abs() > 1.5 * std::f64::consts::PI => 2,
|
||||
@@ -731,23 +959,23 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>],
|
||||
.ok_or_else(|| KernelError::Failed("prism construction face budget".into()))?;
|
||||
// This is a NEW exact extrusion of the current Boolean section,
|
||||
// not copied lineage from either operand's old footprint.
|
||||
for piece in crate::extrude(plane, section, levels[first], levels[end])? {
|
||||
for (r, piece) in crate::extrude(plane, section, levels[first], levels[end])?.into_iter().enumerate() {
|
||||
remaining_faces =
|
||||
remaining_faces.checked_sub(piece.face_count()).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?;
|
||||
pieces.push(piece);
|
||||
pieces.push((piece, lineage::Built::Extruded { section: tagged.region(r), lo: levels[first], hi: levels[end] }));
|
||||
}
|
||||
} else {
|
||||
let piece = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce)
|
||||
let (piece, specs, faces) = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce)
|
||||
.ok_or_else(|| KernelError::Failed("stepped prism construction failed".into()))??;
|
||||
remaining_faces =
|
||||
remaining_faces.checked_sub(piece.face_count()).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?;
|
||||
pieces.push(piece);
|
||||
pieces.push((piece, lineage::Built::Sewn { specs, faces }));
|
||||
}
|
||||
}
|
||||
let mut old = std::collections::HashMap::new();
|
||||
let mut shells = Vec::new();
|
||||
let mut remaining_map_faces = MAX_PLANAR_SEGMENTS * 2;
|
||||
for piece in &pieces {
|
||||
for (piece, _) in &pieces {
|
||||
for (index, face) in piece.solid.face_iter().enumerate() {
|
||||
remaining_map_faces = remaining_map_faces.checked_sub(1).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?;
|
||||
let source = piece.face_sources().and_then(|s| s.get(index)).and_then(Option::as_ref);
|
||||
@@ -766,7 +994,8 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>],
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Ok(body.with_face_sources(Some(std::sync::Arc::new(sources))))
|
||||
let body = body.with_face_sources(Some(std::sync::Arc::new(sources)));
|
||||
Ok(history.attach(body, &pieces))
|
||||
})())
|
||||
}
|
||||
|
||||
@@ -781,20 +1010,29 @@ fn wall_key(s: &Seg2) -> Option<[u64; 6]> {
|
||||
}
|
||||
|
||||
/// A genuinely stepped connected axial block has no single extrusion footprint.
|
||||
fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<Region2>], tol: f64, coalesce: bool) -> Option<Result<Body>> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn sew_connected_sections(
|
||||
plane: &Plane,
|
||||
d: Vec3,
|
||||
levels: &[f64],
|
||||
slabs: &[Section],
|
||||
tol: f64,
|
||||
coalesce: bool,
|
||||
) -> Option<Result<(Body, Vec<lineage::Spec>, Option<Vec<usize>>)>> {
|
||||
use crate::sew::{EdgeSpec, FaceSpec, SurfSpec};
|
||||
// Faces at each level: up where the slab below sticks out, down where the one above does.
|
||||
let diff = |x: &[Region2], y: &[Region2]| -> Option<Vec<Region2>> {
|
||||
match (x.is_empty(), y.is_empty()) {
|
||||
(true, _) => Some(Vec::new()),
|
||||
(false, true) => Some(x.to_vec()),
|
||||
(false, false) => boolean2d(x, y, BoolOp::Cut, tol).map(|r| oriented(&r)),
|
||||
let diff = |x: &Section, y: &Section| -> Option<Section> {
|
||||
match (x.regions.is_empty(), y.regions.is_empty()) {
|
||||
(true, _) => Some(Section::default()),
|
||||
(false, true) => Some(x.clone()),
|
||||
(false, false) => boolean2d_tagged(x, y, BoolOp::Cut, tol).map(|r| r.oriented()),
|
||||
}
|
||||
};
|
||||
let mut caps: Vec<(f64, bool, Vec<Region2>)> = Vec::new();
|
||||
let empty = Section::default();
|
||||
let mut caps: Vec<(f64, bool, Section)> = Vec::new();
|
||||
for (k, z) in levels.iter().enumerate() {
|
||||
let below: &[Region2] = if k == 0 { &[] } else { slabs.get(k - 1)? };
|
||||
let above: &[Region2] = slabs.get(k).map(Vec::as_slice).unwrap_or(&[]);
|
||||
let below: &Section = if k == 0 { &empty } else { slabs.get(k - 1)? };
|
||||
let above: &Section = slabs.get(k).unwrap_or(&empty);
|
||||
caps.push((*z, true, diff(below, above)?));
|
||||
caps.push((*z, false, diff(above, below)?));
|
||||
}
|
||||
@@ -802,7 +1040,7 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<R
|
||||
let loops_of =
|
||||
|rs: &[Region2]| -> Vec<Loop2> { rs.iter().flat_map(|r| std::iter::once(r.outer.clone()).chain(r.holes.iter().cloned())).collect() };
|
||||
let mut pts: Vec<Vec2> = Vec::new();
|
||||
for lp in slabs.iter().flat_map(|s| loops_of(s)).chain(caps.iter().flat_map(|c| loops_of(&c.2))) {
|
||||
for lp in slabs.iter().flat_map(|s| loops_of(&s.regions)).chain(caps.iter().flat_map(|c| loops_of(&c.2.regions))) {
|
||||
for sg in &lp.segs {
|
||||
pts.push(sg.start());
|
||||
pts.push(sg.end());
|
||||
@@ -820,28 +1058,43 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<R
|
||||
let mut runs: Vec<(usize, usize, Seg2)> = Vec::new();
|
||||
let mut last = std::collections::BTreeMap::new();
|
||||
let mut remaining = MAX_PLANAR_SEGMENTS * 2;
|
||||
// What each face is over, for the result's history. Coalesced walls span several slabs
|
||||
// and are not described: such a piece carries no history.
|
||||
let mut specs: Vec<lineage::Spec> = Vec::new();
|
||||
for (i, slab) in slabs.iter().enumerate() {
|
||||
for lp in loops_of(slab) {
|
||||
for sg0 in &lp.segs {
|
||||
for sg in split_seg(sg0, &pts, tol)? {
|
||||
remaining = remaining.checked_sub(1)?;
|
||||
let key = wall_key(&sg)?;
|
||||
if coalesce {
|
||||
if let Some(&index) = last.get(&key) {
|
||||
let run: &mut (usize, usize, Seg2) = runs.get_mut(index)?;
|
||||
if run.1 == i {
|
||||
run.1 = i + 1;
|
||||
continue;
|
||||
}
|
||||
// Duplicate oriented boundaries in one slab are not
|
||||
// a valid reason to reconstruct a larger wall.
|
||||
if run.1 == i + 1 {
|
||||
return None;
|
||||
let z0 = *levels.get(i)?;
|
||||
for (r, reg) in slab.regions.iter().enumerate() {
|
||||
for (l, lp) in std::iter::once(®.outer).chain(®.holes).enumerate() {
|
||||
let first = specs.len();
|
||||
for (si, sg0) in lp.segs.iter().enumerate() {
|
||||
let pieces = split_seg(sg0, &pts, tol)?;
|
||||
let (n, tag) = (pieces.len(), slab.tag(r, l, si));
|
||||
for (k, sg) in pieces.into_iter().enumerate() {
|
||||
remaining = remaining.checked_sub(1)?;
|
||||
let key = wall_key(&sg)?;
|
||||
if coalesce {
|
||||
if let Some(&index) = last.get(&key) {
|
||||
let run: &mut (usize, usize, Seg2) = runs.get_mut(index)?;
|
||||
if run.1 == i {
|
||||
run.1 = i + 1;
|
||||
continue;
|
||||
}
|
||||
// Duplicate oriented boundaries in one slab are not
|
||||
// a valid reason to reconstruct a larger wall.
|
||||
if run.1 == i + 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
last.insert(key, runs.len());
|
||||
} else {
|
||||
specs.push(lineage::Spec::Wall { tag: tag.piece(k, n), z0, next: specs.len() + 1, at: sg.end() });
|
||||
}
|
||||
last.insert(key, runs.len());
|
||||
runs.push((i, i + 1, sg));
|
||||
}
|
||||
runs.push((i, i + 1, sg));
|
||||
}
|
||||
// The loop's last wall runs into its first.
|
||||
if !coalesce && let Some(lineage::Spec::Wall { next, .. }) = specs.last_mut() {
|
||||
*next = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -902,7 +1155,7 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<R
|
||||
}));
|
||||
}
|
||||
for (z, up, rs) in &caps {
|
||||
for r in rs {
|
||||
for (ri, r) in rs.regions.iter().enumerate() {
|
||||
let mut loops = Vec::new();
|
||||
for lp in std::iter::once(&r.outer).chain(&r.holes) {
|
||||
let mut segs: Vec<Seg2> = Vec::new();
|
||||
@@ -914,16 +1167,24 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec<R
|
||||
}
|
||||
faces.push(FaceSpec { loops, surface: SurfSpec::Plane, probe: None });
|
||||
sources.push(None);
|
||||
specs.push(lineage::Spec::Cap { section: rs.region(ri), z: *z, up: *up });
|
||||
}
|
||||
}
|
||||
Some(crate::sew::sew_touching_with_sources(&faces, &sources).and_then(
|
||||
|body| {
|
||||
if coalesce { body.regularize_generated_planar_faces() } else { Ok(body) }
|
||||
},
|
||||
))
|
||||
let sewn = crate::sew::sew_touching_with_sources(&faces, &sources);
|
||||
Some(if coalesce {
|
||||
sewn.and_then(|(body, _)| Ok((body.regularize_generated_planar_faces()?, specs, None)))
|
||||
} else {
|
||||
sewn.map(|(body, from)| (body, specs, from))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
simple_prism_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)])
|
||||
}
|
||||
|
||||
/// [`prism_boolean`] of single prisms, the operands' indexes given.
|
||||
fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option<Result<Option<Body>>> {
|
||||
let size = a.size().max(b.size());
|
||||
let tol = (size * 1e-7).max(1e-9) * 10.0;
|
||||
// Candidate directions: a's planar face normals (each line once).
|
||||
@@ -945,11 +1206,11 @@ fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<
|
||||
}
|
||||
let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| {
|
||||
let plane = Plane::from_normal(Vec3::ZERO, d)?;
|
||||
let pa = prism_of(a, d, &plane, tol)?;
|
||||
let pb = prism_of(b, d, &plane, tol)?;
|
||||
let pa = prism_tagged(a, d, &plane, tol, ia)?;
|
||||
let pb = prism_tagged(b, d, &plane, tol, ib)?;
|
||||
Some((d, plane, pa, pb))
|
||||
})?;
|
||||
let _ = d;
|
||||
let history = lineage::Context { op, operands: [a, b], index: [ia.get(), ib.get()], plane, tol, heights: Some([(pa.lo, pa.hi), (pb.lo, pb.hi)]) };
|
||||
let (blo, bhi) = heights_of(b, &plane);
|
||||
let spans = blo <= pa.lo + tol && bhi >= pa.hi - tol;
|
||||
let same = (pb.lo - pa.lo).abs() < tol && (pb.hi - pa.hi).abs() < tol;
|
||||
@@ -964,41 +1225,55 @@ fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
let regions = boolean2d(&pa.regions, &pb.regions, op, tol)?;
|
||||
let section = boolean2d_tagged(&pa.section(), &pb.section(), op, tol)?;
|
||||
let regions = §ion.regions;
|
||||
if regions.is_empty() {
|
||||
return Some(Ok(None));
|
||||
}
|
||||
let (lo, hi) = if stacked {
|
||||
// Different sections (a boss hanging over a plate's edge): sewn slab by slab.
|
||||
let (u, x, y) = (area(®ions), area(&pa.regions), area(&pb.regions));
|
||||
let (u, x, y) = (area(regions), area(&pa.regions), area(&pb.regions));
|
||||
if (u - x).abs() > tol * (1.0 + u) || (u - y).abs() > tol * (1.0 + u) {
|
||||
return sew_stack(&plane, d, &pa, &pb, tol).map(|r| r.map(Some));
|
||||
return sew_stack(&plane, d, &pa, &pb, tol, &history).map(|r| r.map(Some));
|
||||
}
|
||||
(pa.lo.min(pb.lo), pa.hi.max(pb.hi))
|
||||
} else {
|
||||
(pa.lo, pa.hi)
|
||||
};
|
||||
let base = plane.offset(lo);
|
||||
let mut bodies = match crate::extrude(&base, ®ions, 0.0, hi - lo) {
|
||||
let bodies = match crate::extrude(&base, regions, 0.0, hi - lo) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let first = bodies.pop()?;
|
||||
if !bodies.is_empty() {
|
||||
// Several pieces: one body of several lumps.
|
||||
let mut shells = first.solid.boundaries().clone();
|
||||
for b in bodies {
|
||||
shells.extend(b.solid.boundaries().iter().cloned());
|
||||
}
|
||||
return Some(crate::body::Solid::try_new(shells).map_err(|e| crate::KernelError::Failed(e.to_string())).and_then(Body::new).map(Some));
|
||||
// Each body over its region.
|
||||
let mut pieces: Vec<(Body, lineage::Built)> =
|
||||
bodies.into_iter().enumerate().map(|(r, b)| (b, lineage::Built::Extruded { section: section.region(r), lo, hi })).collect();
|
||||
let last = pieces.pop()?;
|
||||
if !pieces.is_empty() {
|
||||
// Several pieces: one body of several lumps (the last first).
|
||||
pieces.push(last);
|
||||
pieces.rotate_right(1);
|
||||
let shells: Vec<mt::Shell> = pieces.iter().flat_map(|(b, _)| b.solid.boundaries().iter().cloned()).collect();
|
||||
return Some(
|
||||
crate::body::Solid::try_new(shells)
|
||||
.map_err(|e| crate::KernelError::Failed(e.to_string()))
|
||||
.and_then(Body::new)
|
||||
.map(|body| Some(history.attach(body, &pieces))),
|
||||
);
|
||||
}
|
||||
Some(Ok(Some(first)))
|
||||
Some(Ok(Some(history.attach(last.0.clone(), &[last]))))
|
||||
}
|
||||
|
||||
/// A stepped extrusion (case floor plus walls), recovered from its cap events.
|
||||
/// Downward caps add a section above their level; upward caps remove one.
|
||||
#[cfg(test)]
|
||||
fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Vec<Prism>> {
|
||||
if let Some(p) = prism_of(b, d, plane, tol) {
|
||||
stack_tagged(b, d, plane, tol, &LazyIndex::new(b, 0))
|
||||
}
|
||||
|
||||
/// [`stack_of`], its sections' segments tagged from `index` (the body's).
|
||||
fn stack_tagged(b: &Body, d: Vec3, plane: &Plane, tol: f64, index: &LazyIndex) -> Option<Vec<Prism>> {
|
||||
if let Some(p) = prism_tagged(b, d, plane, tol, index) {
|
||||
return Some(vec![p]);
|
||||
}
|
||||
if b.face_count() > 2048 {
|
||||
@@ -1007,23 +1282,15 @@ fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Vec<Prism>> {
|
||||
let mut events = Vec::new();
|
||||
let mut wall_work = 262_144;
|
||||
let mut curve_work = 2_000_000;
|
||||
for f in b.solid.face_iter() {
|
||||
let mut segments = usize::MAX;
|
||||
for (fi, f) in b.solid.face_iter().enumerate() {
|
||||
let surf = f.oriented_surface();
|
||||
if let mt::Surface::Plane(p) = &surf {
|
||||
let n = p.normal();
|
||||
let n = Vec3::new(n.x, n.y, n.z).normalized()?;
|
||||
if n.dot(d).abs() > 1.0 - 1e-9 {
|
||||
let mut loops = Vec::new();
|
||||
for w in f.boundaries() {
|
||||
let mut segs = Vec::new();
|
||||
for e in w.edge_iter() {
|
||||
segs.extend(crate::prism_curve::segment(e, plane, tol, &mut curve_work)?);
|
||||
}
|
||||
loops.push(Loop2 { segs });
|
||||
}
|
||||
let (outer, holes) = split_outer(loops)?;
|
||||
let region = Region2 { outer, holes };
|
||||
events.push((plane.height(from_p3(p.origin())), n.dot(d) > 0.0, oriented(&[region])));
|
||||
let section = Section::from_loops(index.get().loops(f, fi, plane, tol, &mut curve_work, &mut segments)?)?;
|
||||
events.push((plane.height(from_p3(p.origin())), n.dot(d) > 0.0, section.oriented()));
|
||||
continue;
|
||||
}
|
||||
if n.dot(d).abs() > 1e-9 {
|
||||
@@ -1041,29 +1308,35 @@ fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Vec<Prism>> {
|
||||
if levels.len() < 2 || levels.len() > 64 || levels.iter().any(|z| !z.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let mut current = Vec::new();
|
||||
let mut current = Section::default();
|
||||
let mut stack = Vec::new();
|
||||
for (i, z) in levels.iter().enumerate() {
|
||||
// Remove terminating material before adding any material starting here.
|
||||
for up in [true, false] {
|
||||
for (_, _, section) in events.iter().filter(|(h, u, _)| (*h - *z).abs() < tol && *u == up) {
|
||||
current = match (current.is_empty(), up) {
|
||||
current = match (current.regions.is_empty(), up) {
|
||||
(true, false) => section.clone(),
|
||||
(true, true) => return None,
|
||||
(false, _) => boolean2d(¤t, section, if up { BoolOp::Cut } else { BoolOp::Union }, tol)?,
|
||||
(false, _) => boolean2d_tagged(¤t, section, if up { BoolOp::Cut } else { BoolOp::Union }, tol)?,
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(next) = levels.get(i + 1)
|
||||
&& !current.is_empty()
|
||||
&& !current.regions.is_empty()
|
||||
{
|
||||
stack.push(Prism { lo: *z, hi: *next, regions: oriented(¤t) });
|
||||
stack.push(Prism::new(*z, *next, current.oriented()));
|
||||
}
|
||||
}
|
||||
(current.is_empty() && !stack.is_empty()).then_some(stack)
|
||||
(current.regions.is_empty() && !stack.is_empty()).then_some(stack)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
stack_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)])
|
||||
}
|
||||
|
||||
/// [`prism_boolean`] of stepped prisms, the operands' indexes given.
|
||||
fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option<Result<Option<Body>>> {
|
||||
// Common-cap joins and overlapping partial-depth cuts of aligned stepped prisms can
|
||||
// retain their exact section boundaries instead of intersecting meshes.
|
||||
// Nonaligned bodies retain their existing B-rep path.
|
||||
@@ -1073,8 +1346,8 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>>
|
||||
if op == BoolOp::Cut && a.face_count().saturating_add(b.face_count()) > 2048 {
|
||||
return None;
|
||||
}
|
||||
fn section(ps: &[Prism], lo: f64, hi: f64, tol: f64) -> &[Region2] {
|
||||
ps.iter().find(|p| p.lo <= lo + tol && p.hi >= hi - tol).map(|p| p.regions.as_slice()).unwrap_or(&[])
|
||||
fn section(ps: &[Prism], lo: f64, hi: f64, tol: f64) -> Section {
|
||||
ps.iter().find(|p| p.lo <= lo + tol && p.hi >= hi - tol).map(Prism::section).unwrap_or_default()
|
||||
}
|
||||
let tol = (a.size().max(b.size()) * 1e-7).max(1e-9) * 10.0;
|
||||
let mut dirs: Vec<Vec3> = Vec::new();
|
||||
@@ -1091,8 +1364,8 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>>
|
||||
let ends = |ps: &[Prism]| ps.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.lo), hi.max(p.hi)));
|
||||
let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| {
|
||||
let plane = Plane::from_normal(Vec3::ZERO, d)?;
|
||||
let pa = stack_of(a, d, &plane, tol)?;
|
||||
let pb = stack_of(b, d, &plane, tol)?;
|
||||
let pa = stack_tagged(a, d, &plane, tol, ia)?;
|
||||
let pb = stack_tagged(b, d, &plane, tol, ib)?;
|
||||
let (ea, eb) = (ends(&pa), ends(&pb));
|
||||
match op {
|
||||
BoolOp::Union if (ea.0 - eb.0).abs() >= tol && (ea.1 - eb.1).abs() >= tol => return None,
|
||||
@@ -1114,48 +1387,46 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>>
|
||||
let (lo, hi) = (*w.first()?, *w.get(1)?);
|
||||
let (x, y) = (section(&pa, lo, hi, tol), section(&pb, lo, hi, tol));
|
||||
if op == BoolOp::Cut {
|
||||
let count = edges(x).saturating_add(edges(y));
|
||||
let count = edges(&x.regions).saturating_add(edges(&y.regions));
|
||||
work = work.saturating_add(count.saturating_mul(count));
|
||||
if work > 32_000_000 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let s = match (x.is_empty(), y.is_empty(), op) {
|
||||
(true, _, BoolOp::Cut | BoolOp::Intersect) | (_, true, BoolOp::Intersect) => Vec::new(),
|
||||
(true, _, BoolOp::Union) => y.to_vec(),
|
||||
(_, true, _) => x.to_vec(),
|
||||
_ => boolean2d(x, y, op, tol)?,
|
||||
let s = match (x.regions.is_empty(), y.regions.is_empty(), op) {
|
||||
(true, _, BoolOp::Cut | BoolOp::Intersect) | (_, true, BoolOp::Intersect) => Section::default(),
|
||||
(true, _, BoolOp::Union) => y,
|
||||
(_, true, _) => x,
|
||||
_ => boolean2d_tagged(&x, &y, op, tol)?,
|
||||
};
|
||||
if op == BoolOp::Cut {
|
||||
output_edges = output_edges.saturating_add(edges(&s));
|
||||
output_edges = output_edges.saturating_add(edges(&s.regions));
|
||||
if output_edges > 8192 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
slabs.push(oriented(&s));
|
||||
slabs.push(s.oriented());
|
||||
}
|
||||
if slabs.iter().all(Vec::is_empty) {
|
||||
if slabs.iter().all(|s| s.regions.is_empty()) {
|
||||
return Some(Ok(None));
|
||||
}
|
||||
sew_sections(
|
||||
&plane,
|
||||
d,
|
||||
&levels,
|
||||
&slabs,
|
||||
tol,
|
||||
op == BoolOp::Cut
|
||||
&& !a.regularization_protected
|
||||
&& !b.regularization_protected
|
||||
&& a.split_keep().is_empty()
|
||||
&& b.split_keep().is_empty()
|
||||
&& a.paint().is_none_or(|p| p.faces.is_empty())
|
||||
&& b.paint().is_none_or(|p| p.faces.is_empty()),
|
||||
)
|
||||
.map(|r| r.map(Some))
|
||||
let history = lineage::Context { op, operands: [a, b], index: [ia.get(), ib.get()], plane, tol, heights: None };
|
||||
let coalesce = op == BoolOp::Cut
|
||||
&& !a.regularization_protected
|
||||
&& !b.regularization_protected
|
||||
&& a.split_keep().is_empty()
|
||||
&& b.split_keep().is_empty()
|
||||
&& a.paint().is_none_or(|p| p.faces.is_empty())
|
||||
&& b.paint().is_none_or(|p| p.faces.is_empty());
|
||||
sew_sections(&plane, d, &levels, &slabs, tol, coalesce, &history).map(|r| r.map(Some))
|
||||
}
|
||||
|
||||
/// Boolean of prisms along a common direction, in 2D (see the module notes); `None` when it
|
||||
/// doesn't apply. The result carries its history over `[a, b]` (see [`lineage`]).
|
||||
pub(crate) fn prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
crate::guard("prism boolean", || Ok(simple_prism_boolean(a, b, op).or_else(|| stack_boolean(a, b, op)))).ok().flatten()
|
||||
// Each operand's index is built once, and only if it is read as a prism.
|
||||
let index = [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)];
|
||||
crate::guard("prism boolean", || Ok(simple_prism_indexed(a, b, op, index).or_else(|| stack_indexed(a, b, op, index)))).ok().flatten()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1323,9 +1594,20 @@ mod planar_tests {
|
||||
} else {
|
||||
rectangle(Vec2::new(-5., -5.), Vec2::new(5., 5.)).outer.reversed()
|
||||
});
|
||||
let slabs = vec![vec![outer], vec![upper]];
|
||||
let old = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, false).unwrap().unwrap();
|
||||
let merged = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, true).unwrap().unwrap();
|
||||
let slabs = vec![Section::untagged(&[outer]), Section::untagged(&[upper])];
|
||||
// Sections nothing is known of: no history, the construction alone is under test.
|
||||
let operand = crate::extrude(&Plane::XY, &[rectangle(Vec2::new(-1., -1.), Vec2::new(1., 1.))], 0., 1.).unwrap().pop().unwrap();
|
||||
let index = LazyIndex::new(&operand, 0);
|
||||
let history = lineage::Context {
|
||||
op: BoolOp::Cut,
|
||||
operands: [&operand, &operand],
|
||||
index: [index.get(), index.get()],
|
||||
plane: Plane::XY,
|
||||
tol: 1e-7,
|
||||
heights: None,
|
||||
};
|
||||
let old = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, false, &history).unwrap().unwrap();
|
||||
let merged = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, true, &history).unwrap().unwrap();
|
||||
let count = if curved { 2 } else { 4 };
|
||||
assert_eq!(old.face_count() - merged.face_count(), count);
|
||||
let marked: Vec<_> = merged.solid.face_iter().enumerate().filter(|(i, _)| merged.face_was_coalesced(*i)).collect();
|
||||
@@ -1344,7 +1626,7 @@ mod planar_tests {
|
||||
assert!(mesh.contains(Vec3::new(0., 0., 1.)));
|
||||
assert!(!mesh.contains(Vec3::new(0., 0., 10.)));
|
||||
assert!(merged.validity().is_empty());
|
||||
assert_ne!(wall_key(&slabs[0][0].outer.segs[0]), wall_key(&slabs[0][0].outer.segs[0].reversed()));
|
||||
assert_ne!(wall_key(&slabs[0].regions[0].outer.segs[0]), wall_key(&slabs[0].regions[0].outer.segs[0].reversed()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user