mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
kernel: planar regions stay one 2D arrangement; prism booleans are SMLib's boolean
The prism module kept two things: the 2D arrangement of planar regions (planar_regions_boolean, and extrude_planar_regions, which normalises the material with it, proves the pieces apart and extrudes them with crate::extrude, SMLib's extrusion with its face sources), and a second route to the solid boolean: prism operands read section by section, the result sewn and its history rebuilt from section tags (prism_boolean, prism_union_between_caps, the stacked and free-form variants, prism_lineage.rs) and the column-local variant (prism_local.rs). On SMLib a boolean is smlib_boolean, with SMLib's own history: the second route is deleted, with its tests (prism_spline_tests.rs, the stack/prism tests in planar_tests) of what no longer exists. The section tags shrink to what the arrangement uses: the free-form segment a fitted arc stands for. wall_along reads a record surface (cylinder, revolved line, spline net) for clearance.rs. The 2D tests and the wall test keep their assertions. lib.rs (coordinator): drop `mod prism_local;`. (not built: base in progress) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
+60
-1806
File diff suppressed because it is too large
Load Diff
@@ -1,538 +0,0 @@
|
||||
//! The history of a prism boolean's result (see [`super::prism_boolean`]), from what its faces
|
||||
//! were built over: each wall over a section segment whose [`Tag`] says which operand wall it
|
||||
//! was read beside, each cap over a section region lying on operand caps at its level. Edges
|
||||
//! follow from the same tags: a wall's edge with a cap is the operand's edge between them (or
|
||||
//! the meeting of the wall with the other operand's cap); an edge between walls at a section
|
||||
//! vertex is the operand's edge there (or the meeting of two operands' walls).
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use solidcraft_geom::{Plane, Region2, Vec2, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use super::{Index, Section, Tag, dist_to, inside};
|
||||
use crate::BoolOp;
|
||||
use crate::body::{Body, edge_order, from_p3};
|
||||
use crate::history::{EdgeOrigin, FaceOrigin, FaceOrigin::Face, FaceProv, Side};
|
||||
|
||||
/// A cap of an operand: a planar face square to the prism direction.
|
||||
struct Cap {
|
||||
origin: FaceOrigin,
|
||||
/// Its level and whether it faces along the direction.
|
||||
z: f64,
|
||||
up: bool,
|
||||
outline: Outline,
|
||||
}
|
||||
|
||||
/// A section's regions with the walls its outline was read beside and its segments' middles,
|
||||
/// to tell whether two sections overlap.
|
||||
struct Outline {
|
||||
regions: Vec<Region2>,
|
||||
walls: BTreeSet<(usize, usize)>,
|
||||
middles: Vec<Vec2>,
|
||||
}
|
||||
|
||||
impl Outline {
|
||||
fn of(s: &Section) -> Outline {
|
||||
let (segs, tags) = s.segments();
|
||||
Outline {
|
||||
regions: s.regions.clone(),
|
||||
walls: tags.iter().filter_map(|t| Some((t.operand, t.wall?))).collect(),
|
||||
middles: segs.iter().map(|g| g.point_at(0.5)).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a prism boolean's result is built from: the operands (target, tool), the direction and
|
||||
/// the plane square to it that sections are drawn in, and for simple prisms each operand's
|
||||
/// extent along the direction.
|
||||
pub(super) struct Context<'a> {
|
||||
pub op: BoolOp,
|
||||
pub operands: [&'a Body; 2],
|
||||
pub index: [&'a Index; 2],
|
||||
pub plane: Plane,
|
||||
pub tol: f64,
|
||||
pub heights: Option<[(f64, f64); 2]>,
|
||||
/// Both operands' caps, read once (see [`Context::caps`]).
|
||||
caps: std::cell::OnceCell<Option<Vec<Cap>>>,
|
||||
}
|
||||
|
||||
/// How one piece of the result was built.
|
||||
pub(super) enum Built {
|
||||
/// One body of `crate::extrude` of a section's one region through `layers`: successive
|
||||
/// slabs (the section as read in each, between its two levels), bottom first, all of one
|
||||
/// outline. Several where operands stacked end to end share an outline (one wall then lies
|
||||
/// over each operand's wall in turn, and is made from all of them).
|
||||
Extruded { layers: Vec<(Section, f64, f64)> },
|
||||
/// Sewn (`sew_connected_sections`) from these faces, the sewing saying which each face of
|
||||
/// the piece was built from (`None`: it could not).
|
||||
Sewn { specs: Vec<Spec>, faces: Option<Vec<usize>> },
|
||||
/// Built some other way, carrying its own history over the operands (none: not known).
|
||||
Traced,
|
||||
}
|
||||
|
||||
/// A face sewn into a stepped prism.
|
||||
pub(super) enum Spec {
|
||||
/// A wall over one section segment through successive slabs, between successive `levels`
|
||||
/// (one more than `slabs`): what it is over in each slab.
|
||||
Wall { levels: Vec<f64>, slabs: Vec<Slab> },
|
||||
/// A cap over `section` at level `z`, facing along the direction when `up`.
|
||||
Cap { section: Section, z: f64, up: bool },
|
||||
}
|
||||
|
||||
/// A wall in one slab: over a segment tagged `tag`; `next` is the wall over the next segment
|
||||
/// of its loop in that slab, which it meets at `at`.
|
||||
pub(super) struct Slab {
|
||||
pub tag: Tag,
|
||||
pub next: usize,
|
||||
pub at: Vec2,
|
||||
}
|
||||
|
||||
impl Spec {
|
||||
/// The wall's slab between levels `lo` and `hi` (`hi` above `lo`).
|
||||
fn slab(&self, lo: f64, hi: f64) -> Option<&Slab> {
|
||||
let Spec::Wall { levels, slabs } = self else { return None };
|
||||
let mid = (lo + hi) / 2.0;
|
||||
slabs.iter().zip(levels.windows(2)).find(|(_, z)| z[0] < mid && mid < z[1]).map(|(s, _)| s)
|
||||
}
|
||||
|
||||
/// The wall's slab at its bottom level, when that is `z`.
|
||||
fn bottom(&self, z: f64, tol: f64) -> Option<&Slab> {
|
||||
let Spec::Wall { levels, slabs } = self else { return None };
|
||||
((levels.first()? - z).abs() <= tol).then_some(slabs.first()?)
|
||||
}
|
||||
|
||||
/// The wall's slab at its top level, when that is `z`.
|
||||
fn top(&self, z: f64, tol: f64) -> Option<&Slab> {
|
||||
let Spec::Wall { levels, slabs } = self else { return None };
|
||||
((levels.last()? - z).abs() <= tol).then_some(slabs.last()?)
|
||||
}
|
||||
|
||||
/// The wall's slab at its bottom or top level `z`.
|
||||
fn end(&self, z: f64, tol: f64) -> Option<&Slab> {
|
||||
self.bottom(z, tol).or_else(|| self.top(z, tol))
|
||||
}
|
||||
}
|
||||
|
||||
/// A face of an extruded piece: a wall or a cap.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Kind {
|
||||
/// The wall over this segment of the section.
|
||||
Wall(usize),
|
||||
Cap(usize),
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub(super) fn new(op: BoolOp, operands: [&'a Body; 2], index: [&'a Index; 2], plane: Plane, tol: f64, heights: Option<[(f64, f64); 2]>) -> Self {
|
||||
Context { op, operands, index, plane, tol, heights, caps: std::cell::OnceCell::new() }
|
||||
}
|
||||
|
||||
/// `body`, made of `pieces` (their shells in order), with its history over the operands
|
||||
/// (none where it is not known).
|
||||
pub(super) fn attach<B: std::borrow::Borrow<Body>>(&self, body: Body, pieces: &[(B, Built)]) -> Body {
|
||||
let pieces: Vec<(&Body, &Built)> = pieces.iter().map(|(b, k)| (b.borrow(), k)).collect();
|
||||
let (faces, edges) = self.history(&pieces).unzip();
|
||||
crate::record::traced(body, &crate::record::counts(&self.operands), faces, edges.as_ref())
|
||||
}
|
||||
|
||||
/// The faces (each a region of operand faces, and whether it is one untouched) and the edges
|
||||
/// (by identity) of the pieces, in order; `None` where something is not known.
|
||||
pub(super) fn history(&self, pieces: &[(&Body, &Built)]) -> Option<(Vec<FaceProv>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
|
||||
let caps = self.caps.get_or_init(|| self.caps()).as_deref()?;
|
||||
let mut faces = Vec::new();
|
||||
let mut edges = HashMap::new();
|
||||
for (body, built) in pieces {
|
||||
let (f, e) = match built {
|
||||
Built::Extruded { layers } => self.extruded(caps, body, layers)?,
|
||||
Built::Sewn { specs, faces } => self.sewn(caps, body, specs, faces.as_deref()?)?,
|
||||
Built::Traced => {
|
||||
let h = body.history()?;
|
||||
faces.extend(h.faces.iter().cloned());
|
||||
edges.extend(crate::record::edges_by_identity(body, h));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
faces.extend(f.into_iter().map(|(origins, whole)| crate::record::region(origins, whole)));
|
||||
edges.extend(e);
|
||||
}
|
||||
// Pieces of one body can share an origin.
|
||||
crate::record::mark_shared_split(&mut faces);
|
||||
Some((faces, edges))
|
||||
}
|
||||
|
||||
/// Both operands' caps, their outlines tagged.
|
||||
fn caps(&self) -> Option<Vec<Cap>> {
|
||||
let d = self.plane.normal();
|
||||
let mut caps = Vec::new();
|
||||
let (mut work, mut segments) = (2_000_000usize, super::MAX_PLANAR_SEGMENTS * 2);
|
||||
for (operand, (body, index)) in self.operands.iter().zip(self.index).enumerate() {
|
||||
for (face, f) in body.solid.face_iter().enumerate() {
|
||||
let mt::Surface::Plane(p) = f.oriented_surface() else { continue };
|
||||
let n = p.normal();
|
||||
let n = Vec3::new(n.x, n.y, n.z).normalized()?;
|
||||
if n.dot(d).abs() < 1.0 - 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let section = Section::from_loops(index.loops(f, face, &self.plane, self.tol, &mut work, &mut segments)?)?;
|
||||
let (z, up) = (self.plane.height(from_p3(p.origin())), n.dot(d) > 0.0);
|
||||
caps.push(Cap { origin: Face { operand, face }, z, up, outline: Outline::of(§ion.oriented()) });
|
||||
}
|
||||
}
|
||||
Some(caps)
|
||||
}
|
||||
|
||||
/// The operand caps under a result cap over `section` at level `z` (facing along the
|
||||
/// direction when `up`): the target's caps there facing the same way, and the tool's facing
|
||||
/// the same way (a union or intersection) or the other way (a cut, whose tool's caps bound
|
||||
/// what it leaves), wherever they overlap the section.
|
||||
fn cap_origins(&self, caps: &[Cap], section: &Section, z: f64, up: bool) -> BTreeSet<FaceOrigin> {
|
||||
let outline = Outline::of(section);
|
||||
caps.iter()
|
||||
.filter(|c| {
|
||||
let facing = match c.origin {
|
||||
Face { operand: 1, .. } if self.op == BoolOp::Cut => c.up != up,
|
||||
_ => c.up == up,
|
||||
};
|
||||
facing && (c.z - z).abs() < self.tol && self.overlap(&outline, &c.outline)
|
||||
})
|
||||
.map(|c| c.origin)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Do two sections (at one level) overlap? Where their outlines share a wall, or a point of
|
||||
/// one's outline, clear of the other's, lies inside it.
|
||||
fn overlap(&self, x: &Outline, y: &Outline) -> bool {
|
||||
let within = |x: &Outline, y: &Outline| x.middles.iter().any(|m| dist_to(&y.regions, *m) > self.tol && inside(&y.regions, *m));
|
||||
!x.walls.is_disjoint(&y.walls) || within(x, y) || within(y, x)
|
||||
}
|
||||
|
||||
/// The operand wall a segment was read beside.
|
||||
fn wall(t: Tag) -> Option<FaceOrigin> {
|
||||
Some(Face { operand: t.operand, face: t.wall? })
|
||||
}
|
||||
|
||||
/// The operand's edges (by number) bounding exactly the faces `faces`.
|
||||
fn edges_between(&self, operand: usize, [f, g]: [usize; 2]) -> &[usize] {
|
||||
self.index.get(operand).and_then(|index| index.between.get(&(f.min(g), f.max(g)))).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Is the wall over a segment tagged `t` between `lo` and `hi` its operand wall untouched?
|
||||
/// The whole cap edge it was read off, the operand's whole height (simple prisms), and a
|
||||
/// wall with no other cap edges than its two.
|
||||
fn whole(&self, caps: &[Cap], t: Tag, lo: f64, hi: f64) -> bool {
|
||||
let (Some(heights), Some(wall), Some(_), Some(_)) = (self.heights, t.wall, t.start, t.end) else { return false };
|
||||
let Some((l, h)) = heights.get(t.operand) else { return false };
|
||||
let Some(index) = self.index.get(t.operand) else { return false };
|
||||
let cap_edges: usize = caps
|
||||
.iter()
|
||||
.filter_map(|c| match c.origin {
|
||||
Face { operand, face } if operand == t.operand => index.between.get(&(face.min(wall), face.max(wall))).map(Vec::len),
|
||||
_ => None,
|
||||
})
|
||||
.sum();
|
||||
(lo - l).abs() < self.tol && (hi - h).abs() < self.tol && cap_edges == 2
|
||||
}
|
||||
|
||||
/// The origins of the edge between the wall over a segment tagged `t` and a cap lying on the
|
||||
/// operand faces `cap`: the operand's edge between the wall and its own cap there (the very
|
||||
/// edge the segment was read off, or the one it runs along where its slab ends: the one
|
||||
/// running through `at`, a point of the edge, where the wall meets its caps along several),
|
||||
/// else where the wall meets the cap (all of the faces it lies on).
|
||||
fn cap_edge(&self, t: Tag, cap: &BTreeSet<FaceOrigin>, at: Vec3) -> Option<Vec<EdgeOrigin>> {
|
||||
let (x, w) = (t.operand, t.wall?);
|
||||
let own: Vec<usize> =
|
||||
cap.iter().filter_map(|o| if let Face { operand, face } = *o { (operand == x).then_some(face) } else { None }).collect();
|
||||
let index = self.index.get(x)?;
|
||||
let beside = |e: &usize| index.bounds.get(*e).is_some_and(|(fs, _, _)| fs.contains(&w) && fs.iter().any(|f| own.contains(f)));
|
||||
if let Some(e) = [t.edge, t.far].into_iter().flatten().find(beside) {
|
||||
return Some(vec![EdgeOrigin::Edge { operand: x, edge: e }]);
|
||||
}
|
||||
let along: Vec<usize> = own.iter().flat_map(|c| self.edges_between(x, [w, *c])).copied().collect();
|
||||
match along.as_slice() {
|
||||
[e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
|
||||
[] => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
|
||||
// One wall with openings meets its caps along several edges (a side wall with ports
|
||||
// cut through it meets the ports' floors at each port): the one this edge lies on,
|
||||
// else (a new opening beside them) where the wall meets the cap.
|
||||
many => match self.edge_through(x, many, at)? {
|
||||
Some(edge) => Some(vec![EdgeOrigin::Edge { operand: x, edge }]),
|
||||
None => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Of the operand's edges `edges`, the one the point `at` lies on (`Some(None)` when none
|
||||
/// does; `None` when several do).
|
||||
fn edge_through(&self, operand: usize, edges: &[usize], at: Vec3) -> Option<Option<usize>> {
|
||||
if edges.len() > 4096 {
|
||||
return None;
|
||||
}
|
||||
let body = self.operands.get(operand)?;
|
||||
let order = crate::body::edge_order(body.solid.face_iter()).0;
|
||||
let mut on = edges.iter().filter(|e| {
|
||||
order.get(**e).is_some_and(|edge| {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let c = edge.curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
// The edge as a fine polyline: a line exactly, an arc within its chords' sag.
|
||||
let n = if matches!(c, mt::Curve::Line(_)) { 1 } else { 64 };
|
||||
let pts: Vec<Vec3> = (0..=n).map(|k| from_p3(c.subs(t0 + (t1 - t0) * k as f64 / n as f64))).collect();
|
||||
let length: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
let near = self.tol * 10.0 + if n == 1 { 0.0 } else { length * 2e-3 };
|
||||
pts.windows(2).any(|w| at.dist_to_segment(w[0], w[1]) <= near)
|
||||
})
|
||||
});
|
||||
match (on.next(), on.next()) {
|
||||
(Some(e), None) => Some(Some(*e)),
|
||||
(None, _) => Some(None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The origins of the edge between the walls over two segments where the first (tagged
|
||||
/// `t1`) runs into the second (`t2`), between levels `lo` and `hi`: the operand's edge there
|
||||
/// between its walls (along the line through the operand vertex the segments meet at, else
|
||||
/// the one edge between them), where two operands' walls meet, or none within one wall (a
|
||||
/// segment split there).
|
||||
fn junction(&self, t1: Tag, t2: Tag, lo: f64, hi: f64) -> Option<Vec<EdgeOrigin>> {
|
||||
let (p, q) = (Self::wall(t1)?, Self::wall(t2)?);
|
||||
if t1.operand != t2.operand {
|
||||
return Some(vec![EdgeOrigin::meet(p, q)]);
|
||||
}
|
||||
let (x, w1, w2) = (t1.operand, t1.wall?, t2.wall?);
|
||||
match (t1.end, t2.start) {
|
||||
// At that vertex: the operand's edge there (a seam, within one wall), else two edges
|
||||
// of one wall's cap outline running on through it.
|
||||
(Some(v), Some(u)) if v == u => self.edge_over(x, [w1, w2], Some(v), lo, hi).or_else(|| (w1 == w2).then(Vec::new)),
|
||||
_ if w1 == w2 => Some(Vec::new()),
|
||||
// Elsewhere (a segment split off there, or read off another cap): only the operand's
|
||||
// walls are around the edge, so the operand has its edge between them there, and
|
||||
// that is the one edge between them over these levels where there is one.
|
||||
_ => self.edge_over(x, [w1, w2], None, lo, hi),
|
||||
}
|
||||
}
|
||||
|
||||
/// The operand's edge between the walls `faces` that runs over the levels `lo` to `hi`,
|
||||
/// when it has exactly one; with `at`, among the edges between them chained to that vertex.
|
||||
/// The segments' vertex was read at one level and carried through others, and the line
|
||||
/// between two walls is in several edges where vertices an earlier operation left (stations)
|
||||
/// split it: the levels say which of them a slab's edge runs along.
|
||||
fn edge_over(&self, operand: usize, faces: [usize; 2], at: Option<usize>, lo: f64, hi: f64) -> Option<Vec<EdgeOrigin>> {
|
||||
let index = self.index.get(operand)?;
|
||||
let between = self.edges_between(operand, faces);
|
||||
if between.len() > 4096 {
|
||||
return None;
|
||||
}
|
||||
let ends: Vec<(Option<usize>, Option<usize>)> =
|
||||
between.iter().map(|e| index.bounds.get(*e).map_or((None, None), |(_, front, back)| (*front, *back))).collect();
|
||||
// With `at`: the edges reached from it through shared vertices.
|
||||
let mut chained = vec![at.is_none(); between.len()];
|
||||
let mut reached: Vec<usize> = at.into_iter().collect();
|
||||
while let Some(p) = reached.pop() {
|
||||
for (k, (front, back)) in ends.iter().enumerate() {
|
||||
if !chained[k] && (*front == Some(p) || *back == Some(p)) {
|
||||
chained[k] = true;
|
||||
reached.extend(if *front == Some(p) { *back } else { *front });
|
||||
}
|
||||
}
|
||||
}
|
||||
let height = |v: Option<usize>| v.and_then(|v| index.points.get(v)).map(|p| self.plane.height(*p));
|
||||
let mut over = between.iter().zip(&ends).zip(chained).filter_map(|((e, (front, back)), chained)| {
|
||||
let (a, b) = (height(*front)?, height(*back)?);
|
||||
(chained && a.min(b) <= lo + self.tol && a.max(b) >= hi - self.tol).then_some(*e)
|
||||
});
|
||||
match (over.next(), over.next()) {
|
||||
(Some(edge), None) => Some(vec![EdgeOrigin::Edge { operand, edge }]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The origins of the edge between walls over one segment stacked at a level (tagged `t1`
|
||||
/// below, `t2` above): within one wall (none), along the operand's edge between two of its
|
||||
/// walls, or where two operands' walls meet.
|
||||
fn stacked(&self, t1: Tag, t2: Tag) -> Option<Vec<EdgeOrigin>> {
|
||||
let (p, q) = (Self::wall(t1)?, Self::wall(t2)?);
|
||||
if p == q {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
if t1.operand != t2.operand {
|
||||
return Some(vec![EdgeOrigin::meet(p, q)]);
|
||||
}
|
||||
// An edge across the direction at a level: the levels cannot tell edges apart here.
|
||||
self.one_edge(t1.operand, [t1.wall?, t2.wall?])
|
||||
}
|
||||
|
||||
/// The operand's edge between the faces `faces`, when it has exactly one.
|
||||
fn one_edge(&self, operand: usize, faces: [usize; 2]) -> Option<Vec<EdgeOrigin>> {
|
||||
match self.edges_between(operand, faces) {
|
||||
[edge] => Some(vec![EdgeOrigin::Edge { operand, edge: *edge }]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces and edges of a piece `crate::extrude` built from a section of one region through
|
||||
/// `layers` (see [`Built::Extruded`]). Its history says which faces are walls over which
|
||||
/// segment (numbered over the region's loops in order) and which are its caps; the start
|
||||
/// cap's wires run over the segments in loop order (as the extrude builds them), so the
|
||||
/// vertex after each wire edge is where one segment runs into the next, or splits. A wall
|
||||
/// through several layers is made from the wall each layer's segment was read beside, and
|
||||
/// an edge between two walls from the operand edges there in each layer.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn extruded(
|
||||
&self,
|
||||
caps: &[Cap],
|
||||
body: &Body,
|
||||
layers: &[(Section, f64, f64)],
|
||||
) -> Option<(Vec<(BTreeSet<FaceOrigin>, bool)>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
|
||||
let h = body.history()?;
|
||||
let ((first, lo, _), (last, _, hi)) = (layers.first()?, layers.last()?);
|
||||
let (lo, hi) = (*lo, *hi);
|
||||
let tags: Vec<Vec<Tag>> = layers.iter().map(|(section, ..)| section.segments().1).collect();
|
||||
let count = tags.first()?.len();
|
||||
if tags.iter().any(|t| t.len() != count) {
|
||||
return None;
|
||||
}
|
||||
let tag = |layer: usize, segment: usize| tags.get(layer).and_then(|t| t.get(segment)).copied();
|
||||
let tops = [self.cap_origins(caps, first, lo, false), self.cap_origins(caps, last, hi, true)];
|
||||
let kinds: Vec<Kind> = h
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| match f.origins.as_slice() {
|
||||
[FaceOrigin::Profile { segment }] => (*segment < count).then_some(Kind::Wall(*segment)),
|
||||
[FaceOrigin::Cap { end }] => Some(Kind::Cap(usize::from(*end))),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
let faces = kinds
|
||||
.iter()
|
||||
.map(|k| match *k {
|
||||
Kind::Wall(segment) => {
|
||||
let walls = (0..layers.len()).map(|layer| Self::wall(tag(layer, segment)?)).collect::<Option<BTreeSet<_>>>()?;
|
||||
let whole = match layers {
|
||||
[(_, lo, hi)] => self.whole(caps, tag(0, segment)?, *lo, *hi),
|
||||
_ => false,
|
||||
};
|
||||
Some((walls, whole))
|
||||
}
|
||||
Kind::Cap(end) => Some(tops.get(end).filter(|o| !o.is_empty())?.clone()).map(|o| (o, false)),
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let (edges, bounds) = edge_order(body.solid.face_iter());
|
||||
let position: HashMap<mt::EdgeID, usize> = edges.iter().enumerate().map(|(i, e)| (e.id(), i)).collect();
|
||||
// The segment of the wall beside an edge of the start cap.
|
||||
let segment_of = |e: &mt::Edge| -> Option<usize> {
|
||||
bounds.get(*position.get(&e.id())?)?.iter().find_map(|f| match h.faces.get(*f)?.origins.as_slice() {
|
||||
[FaceOrigin::Profile { segment }] => Some(*segment),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let start = kinds.iter().position(|k| matches!(k, Kind::Cap(0)))?;
|
||||
// At each vertex of the start cap: the segment running into it, the one running out,
|
||||
// and whether that is one segment the extrude split there.
|
||||
let mut junctions: HashMap<mt::VertexID, (usize, usize, bool)> = HashMap::new();
|
||||
for w in body.solid.face_iter().nth(start)?.absolute_boundaries() {
|
||||
let es: Vec<mt::Edge> = w.edge_iter().cloned().collect();
|
||||
let segs: Vec<usize> = es.iter().map(segment_of).collect::<Option<_>>()?;
|
||||
if segs.windows(2).any(|p| p.first() > p.get(1)) {
|
||||
return None;
|
||||
}
|
||||
for (k, e) in es.iter().enumerate() {
|
||||
let (s, next) = (*segs.get(k)?, *segs.get((k + 1) % segs.len())?);
|
||||
junctions.insert(e.back().id(), (s, next, s == next && k + 1 != segs.len()));
|
||||
}
|
||||
}
|
||||
let mut origins = HashMap::new();
|
||||
for (e, fs) in edges.iter().zip(&bounds) {
|
||||
let [f, g] = fs.as_slice() else { return None };
|
||||
let o = match (kinds.get(*f)?, kinds.get(*g)?) {
|
||||
(Kind::Wall(segment), Kind::Cap(end)) | (Kind::Cap(end), Kind::Wall(segment)) => {
|
||||
let layer = if *end == 0 { 0 } else { layers.len() - 1 };
|
||||
self.cap_edge(tag(layer, *segment)?, tops.get(*end)?, crate::heal::edge_points(e).get(2).copied()?)?
|
||||
}
|
||||
(Kind::Wall(..), Kind::Wall(..)) => {
|
||||
let (s, next, split) = *[e.absolute_front().id(), e.absolute_back().id()].iter().find_map(|v| junctions.get(v))?;
|
||||
let mut o = BTreeSet::new();
|
||||
for (layer, (_, lo, hi)) in layers.iter().enumerate() {
|
||||
let (t, u) = (tag(layer, s)?, tag(layer, next)?);
|
||||
// A segment the extrude split in two runs on through the vertex between.
|
||||
let (t, u) = if split { (Tag { end: None, ..t }, Tag { start: None, ..t }) } else { (t, u) };
|
||||
o.extend(self.junction(t, u, *lo, *hi)?);
|
||||
}
|
||||
o.into_iter().collect()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
origins.insert(e.id(), o);
|
||||
}
|
||||
Some((faces, origins))
|
||||
}
|
||||
|
||||
/// Faces and edges of a piece sewn from `specs`: the sewing reports which spec each face was
|
||||
/// built from. Walls meeting at a vertex of their loop (when two walls meet at both ends,
|
||||
/// the edge's vertex says which); walls over one segment meeting at a level; walls meeting
|
||||
/// caps.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn sewn(
|
||||
&self,
|
||||
caps: &[Cap],
|
||||
body: &Body,
|
||||
specs: &[Spec],
|
||||
built: &[usize],
|
||||
) -> Option<(Vec<(BTreeSet<FaceOrigin>, bool)>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
|
||||
let cap_of: Vec<Option<BTreeSet<FaceOrigin>>> = specs
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Spec::Cap { section, z, up } => Some(self.cap_origins(caps, section, *z, *up)).filter(|o| !o.is_empty()),
|
||||
Spec::Wall { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
let faces = built
|
||||
.iter()
|
||||
.map(|i| match specs.get(*i)? {
|
||||
Spec::Wall { slabs, .. } => Some((slabs.iter().map(|s| Self::wall(s.tag)).collect::<Option<_>>()?, false)),
|
||||
Spec::Cap { .. } => cap_of.get(*i)?.clone().map(|o| (o, false)),
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let (edges, bounds) = edge_order(body.solid.face_iter());
|
||||
let mut origins = HashMap::new();
|
||||
for (e, fs) in edges.iter().zip(&bounds) {
|
||||
let [f, g] = fs.as_slice() else { return None };
|
||||
let (i, j) = (*built.get(*f)?, *built.get(*g)?);
|
||||
let (x, y) = (specs.get(i)?, specs.get(j)?);
|
||||
// The edge's levels, lower first, and where it stands in plan.
|
||||
let (front, back) = (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()));
|
||||
let (h1, h2) = (self.plane.height(front), self.plane.height(back));
|
||||
let (lo, hi) = (h1.min(h2), h1.max(h2));
|
||||
let middle = || crate::heal::edge_points(e).get(2).copied();
|
||||
let o = match (x, y) {
|
||||
(Spec::Wall { .. }, Spec::Cap { .. }) => self.cap_edge(x.end(lo, self.tol)?.tag, cap_of.get(j)?.as_ref()?, middle()?)?,
|
||||
(Spec::Cap { .. }, Spec::Wall { .. }) => self.cap_edge(y.end(lo, self.tol)?.tag, cap_of.get(i)?.as_ref()?, middle()?)?,
|
||||
(Spec::Wall { .. }, Spec::Wall { .. }) if hi - lo <= self.tol => {
|
||||
// One over the other at a level: the lower first.
|
||||
let (below, above) = match (x.top(lo, self.tol), y.bottom(lo, self.tol)) {
|
||||
(Some(s), Some(t)) => (s, t),
|
||||
_ => (y.top(lo, self.tol)?, x.bottom(lo, self.tol)?),
|
||||
};
|
||||
self.stacked(below.tag, above.tag)?
|
||||
}
|
||||
(Spec::Wall { .. }, Spec::Wall { .. }) => {
|
||||
// Side by side within a slab: the vertex the edge stands on (in plan) says
|
||||
// which way round they meet.
|
||||
let (s, t) = (x.slab(lo, hi)?, y.slab(lo, hi)?);
|
||||
let p = self.plane.to_local(front);
|
||||
let at = |q: &Vec2| p.dist(*q) < self.tol * 10.0;
|
||||
if s.next == j && at(&s.at) {
|
||||
self.junction(s.tag, t.tag, lo, hi)?
|
||||
} else if t.next == i && at(&t.at) {
|
||||
self.junction(t.tag, s.tag, lo, hi)?
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
origins.insert(e.id(), o);
|
||||
}
|
||||
Some((faces, origins))
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
//! Booleans of a prism tool with a target that is a prism along the tool's direction only round
|
||||
//! the tool: a pocket made deeper in a plate whose rim is chamfered, a ring cut round a
|
||||
//! counterbored hole in a chamfered disc. The tool's walls lie on the target's there, which the
|
||||
//! B-rep boolean can't intersect, and the 2D route ([`crate::prism::prism_boolean`]) declines the
|
||||
//! target for a face far away (the chamfer) that is neither a wall along the direction nor a cap
|
||||
//! square to it.
|
||||
//!
|
||||
//! The target is split by planes into a column round the tool that holds none of those faces;
|
||||
//! the column takes the exact 2D boolean, and the pieces are glued back along the cut faces, as
|
||||
//! [`crate::polybool::sliced_planar_boolean`] does for an all-planar tool beside curved faces.
|
||||
//! The cuts are imprints (no boolean), each placed beside the tool on one of three axes: the
|
||||
//! tool's direction and two square to it. A cut that leaves every offending face of the middle
|
||||
//! piece beyond it goes first; otherwise one close beside the tool, which splits them (a
|
||||
//! chamfer all round a disc is split into two pieces apart, which the next cut leaves out).
|
||||
|
||||
use solidcraft_geom::{Plane, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::boolean_record::{self as recorded, Traced};
|
||||
use crate::{BoolOp, KernelError, Result};
|
||||
|
||||
/// Cuts at most, each splitting the middle piece once.
|
||||
const MAX_CUTS: usize = 6;
|
||||
|
||||
/// Cuts made with half boxes at most (two booleans each, with their retries): an imprint traced
|
||||
/// wrong across a curved face is rare, and the booleans cost seconds on curved parts.
|
||||
const MAX_BOX_CUTS: usize = 2;
|
||||
|
||||
/// Operands a piece's history is over: `a`, `b`, then the two half boxes each cut may be made
|
||||
/// with (dropped from the result, whose faces are none of theirs).
|
||||
const OPERANDS: usize = 2 + 2 * MAX_CUTS;
|
||||
|
||||
thread_local! {
|
||||
/// Set while [`local_prism_boolean`] runs: the booleans it makes a cut with (a piece and a
|
||||
/// half box) take the general path, not this one again.
|
||||
static RUNNING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// An extent along each of three axes.
|
||||
type Extent = [(f64, f64); 3];
|
||||
|
||||
/// [`crate::prism::prism_boolean`] of `a` and the prism `b`, taken in a column of `a` round `b`
|
||||
/// (see the module notes). `None` when `b` is not a prism, `a` is one along `b`'s direction
|
||||
/// already, or no column round `b` is clear of `a`'s other faces: the caller goes on as before.
|
||||
pub(crate) fn local_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
// A cut or an intersection: what a tool meeting the target's walls fails on. A union (a
|
||||
// shell's cavity joined with its opening) is left to the general path.
|
||||
if !matches!(op, BoolOp::Cut | BoolOp::Intersect) || a.face_count() > 4096 || RUNNING.with(std::cell::Cell::get) {
|
||||
return None;
|
||||
}
|
||||
/// Clears the flag however the boolean ends, a caught panic included.
|
||||
struct Reset;
|
||||
impl Drop for Reset {
|
||||
fn drop(&mut self) {
|
||||
RUNNING.with(|r| r.set(false));
|
||||
}
|
||||
}
|
||||
RUNNING.with(|r| r.set(true));
|
||||
let _reset = Reset;
|
||||
let d = crate::prism::prism_axis(b)?;
|
||||
let size = a.size().max(b.size());
|
||||
let tol = (size * 1e-7).max(1e-9) * 10.0;
|
||||
let frame = Plane::from_normal(Vec3::ZERO, d)?;
|
||||
let axes = [frame.x, frame.y, d];
|
||||
// Extents come from meshes this close to the faces, widened by as much.
|
||||
let mesh_tol = (size * 1e-3).max(1e-4);
|
||||
let tool = widened(extent(&b.mesh(&crate::MeshTolerance::chord(mesh_tol)).ok()?.positions, axes)?, mesh_tol);
|
||||
let gap = size * 1e-4 + 1e-6;
|
||||
let pieces = survey(a, d, tol, mesh_tol, axes)?.pieces;
|
||||
if pieces.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// A piece of an offending face within a hair of the tool's box: no column round the tool
|
||||
// can leave it out, so nothing is cut.
|
||||
let reach = size * 2e-3 + 2.0 * gap;
|
||||
if pieces.iter().any(|(_, e)| e.iter().zip(&tool).all(|((c0, c1), (t0, t1))| *c1 > t0 - reach && *c0 < t1 + reach)) {
|
||||
return None;
|
||||
}
|
||||
let run = || -> Result<Option<Body>> {
|
||||
let fail = |m: &str| KernelError::Failed(format!("prism boolean in a column round the tool: {m}"));
|
||||
let mut middle: Traced = (a.clone(), recorded::kept(a, 0, OPERANDS));
|
||||
let mut outside: Vec<Traced> = Vec::new();
|
||||
// The sides already cut (axis, positive side): each is cut once.
|
||||
let mut cut: Vec<(usize, bool)> = Vec::new();
|
||||
let mut box_cuts = 0;
|
||||
for _ in 0..MAX_CUTS {
|
||||
let survey = survey(&middle.0, d, tol, mesh_tol, axes).ok_or_else(|| fail("the middle piece"))?;
|
||||
let off = survey.faces();
|
||||
if off.is_empty() {
|
||||
break;
|
||||
}
|
||||
let (k, places, keep_positive) =
|
||||
next_cut(&off, &tool, &survey.here, &survey.flat, &cut, axes, size, gap).ok_or_else(|| fail("no cut leaves the other faces out"))?;
|
||||
cut.push((k, !keep_positive));
|
||||
let axis = axes[k];
|
||||
let (u, w) = (axes[(k + 1) % 3], axes[(k + 2) % 3]);
|
||||
// The first of the places where the piece splits into parts that add up to it (a
|
||||
// plane whose section through the piece doesn't close is passed over). The cut's
|
||||
// half boxes, if it takes them, are operands `slot` and `slot + 1`.
|
||||
let slot = 2 * cut.len();
|
||||
let (away, kept) = places
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, x)| {
|
||||
let plane = Plane::new(axis * *x, u, w)?;
|
||||
let with_boxes = i == 0 && box_cuts < MAX_BOX_CUTS;
|
||||
// Only a plane across an offending face can be traced wrong (a cone's piece
|
||||
// meshed past its bounds): the others are taken as they are.
|
||||
let verify = survey.pieces.iter().any(|(_, e)| e[k].0 < *x && e[k].1 > *x);
|
||||
let r = split(&middle, &plane, axis, *x, keep_positive, slot, with_boxes, verify)?;
|
||||
box_cuts += usize::from(r.2);
|
||||
Some((r.0, r.1))
|
||||
})
|
||||
.ok_or_else(|| fail("a cut beside the tool"))?;
|
||||
outside.extend(away);
|
||||
middle = kept;
|
||||
}
|
||||
if !survey(&middle.0, d, tol, mesh_tol, axes).is_some_and(|s| s.pieces.is_empty()) {
|
||||
return Err(fail("other faces are left round the tool"));
|
||||
}
|
||||
let r = match crate::prism::prism_boolean(&middle.0, b, op) {
|
||||
Some(r) => r?,
|
||||
None => return Err(fail("the column is not a prism along the tool")),
|
||||
};
|
||||
crate::polybool::reassembled(a, b, op, &middle, r, outside, OPERANDS, &fail)
|
||||
};
|
||||
Some(run())
|
||||
}
|
||||
|
||||
/// `piece` split by `plane` (at `x` along `axis`, square to it): the parts away from the tool
|
||||
/// and the one on its side (positive along `axis` when `keep_positive`), with their histories.
|
||||
/// An imprint of the plane, taken as it is unless `verify`; else where its parts don't add up to
|
||||
/// the piece (a plane across a cone can be traced wrong) and `with_boxes`, the piece's intersections with the half boxes
|
||||
/// either side (the cut's operands `slot` and `slot + 1`), and then whether it took them. `None`
|
||||
/// when neither adds up.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn split(
|
||||
piece: &Traced,
|
||||
plane: &Plane,
|
||||
axis: Vec3,
|
||||
x: f64,
|
||||
keep_positive: bool,
|
||||
slot: usize,
|
||||
with_boxes: bool,
|
||||
verify: bool,
|
||||
) -> Option<(Vec<Traced>, Traced, bool)> {
|
||||
if !verify {
|
||||
let (away, kept) = crate::polybool::split_beside(piece, plane, axis, x, keep_positive)?;
|
||||
return Some((away, kept, false));
|
||||
}
|
||||
let whole = crate::measure(&piece.0).ok()?.volume;
|
||||
let adds_up = |away: &[Traced], kept: &Traced| {
|
||||
let sum: Option<f64> = away.iter().chain([kept]).map(|(p, _)| crate::measure(p).ok().map(|m| m.volume)).sum();
|
||||
sum.is_some_and(|sum| (sum - whole).abs() <= whole.abs() * 5e-5 + 1e-9)
|
||||
};
|
||||
if let Some((away, kept)) = crate::polybool::split_beside(piece, plane, axis, x, keep_positive)
|
||||
&& adds_up(&away, &kept)
|
||||
{
|
||||
return Some((away, kept, false));
|
||||
}
|
||||
if !with_boxes {
|
||||
return None;
|
||||
}
|
||||
let boxes = (crate::ops::half_box(&piece.0, plane, !keep_positive).ok()?, crate::ops::half_box(&piece.0, plane, keep_positive).ok()?);
|
||||
let (h_away, h_kept) = (recorded::kept(&boxes.0, slot, OPERANDS), recorded::kept(&boxes.1, slot + 1, OPERANDS));
|
||||
let away = crate::ops::boolean(&piece.0, &boxes.0, BoolOp::Intersect).ok()?;
|
||||
let kept = crate::ops::boolean(&piece.0, &boxes.1, BoolOp::Intersect).ok()??;
|
||||
let away: Vec<Traced> = away
|
||||
.into_iter()
|
||||
.map(|o| {
|
||||
let h = recorded::of_pair(&o, &piece.1, &h_away);
|
||||
(o, h)
|
||||
})
|
||||
.collect();
|
||||
let kept: Traced = {
|
||||
let h = recorded::of_pair(&kept, &piece.1, &h_kept);
|
||||
(kept, h)
|
||||
};
|
||||
adds_up(&away, &kept).then_some((away, kept, true))
|
||||
}
|
||||
|
||||
/// The extent of `points` along each of `axes`; `None` for no points.
|
||||
fn extent(points: &[Vec3], axes: [Vec3; 3]) -> Option<Extent> {
|
||||
if points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut e = [(f64::INFINITY, f64::NEG_INFINITY); 3];
|
||||
for p in points {
|
||||
for (slot, axis) in e.iter_mut().zip(axes) {
|
||||
let t = p.dot(axis);
|
||||
*slot = (slot.0.min(t), slot.1.max(t));
|
||||
}
|
||||
}
|
||||
e.iter().all(|(lo, hi)| lo.is_finite() && hi.is_finite()).then_some(e)
|
||||
}
|
||||
|
||||
/// What a cut is chosen from: a piece's extent (`here`), the mesh triangles of its faces that
|
||||
/// are neither a wall along the direction nor a plane square to it (the faces that keep it from
|
||||
/// being a prism along it: each one's index and extent), and its planar faces (unit normal,
|
||||
/// offset along it) for a cut to keep out of. Extents are widened by the mesh's tolerance.
|
||||
struct Survey {
|
||||
here: Extent,
|
||||
pieces: Vec<(usize, Extent)>,
|
||||
flat: Vec<(Vec3, f64)>,
|
||||
}
|
||||
|
||||
impl Survey {
|
||||
/// The extent of each offending face.
|
||||
fn faces(&self) -> Vec<Extent> {
|
||||
let mut faces: Vec<(usize, Extent)> = Vec::new();
|
||||
for (i, e) in &self.pieces {
|
||||
match faces.iter_mut().find(|(j, _)| j == i) {
|
||||
Some((_, acc)) => {
|
||||
for (x, y) in acc.iter_mut().zip(e) {
|
||||
*x = (x.0.min(y.0), x.1.max(y.1));
|
||||
}
|
||||
}
|
||||
None => faces.push((*i, *e)),
|
||||
}
|
||||
}
|
||||
faces.into_iter().map(|(_, e)| e).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Survey`] of `body` along `d`, from one mesh of it. `None` when it can't be meshed.
|
||||
fn survey(body: &Body, d: Vec3, tol: f64, mesh_tol: f64, axes: [Vec3; 3]) -> Option<Survey> {
|
||||
let mut work = 262_144usize;
|
||||
let mut flat = Vec::new();
|
||||
let off: Vec<bool> = body
|
||||
.solid
|
||||
.face_iter()
|
||||
.map(|f| {
|
||||
let surface = f.oriented_surface();
|
||||
match &surface {
|
||||
mt::Surface::Plane(p) => {
|
||||
let n = p.normal();
|
||||
let Some(n) = Vec3::new(n.x, n.y, n.z).normalized() else { return true };
|
||||
let o = p.origin();
|
||||
flat.push((n, n.dot(Vec3::new(o.x, o.y, o.z))));
|
||||
let along = n.dot(d).abs();
|
||||
// NaN counts as inside, as the comparisons it replaces did.
|
||||
along.is_nan() || (1e-9..=1.0 - 1e-9).contains(&along)
|
||||
}
|
||||
_ => !crate::prism::wall_along(&surface, d, tol, &mut work),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(mesh_tol)).ok()?;
|
||||
let here = widened(extent(&mesh.positions, axes)?, mesh_tol);
|
||||
let mut pieces = Vec::new();
|
||||
if off.iter().any(|x| *x) {
|
||||
for (t, fi) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
let i = *fi as usize;
|
||||
if !off.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let Some(tri) = mesh.tri(t) else { continue };
|
||||
if let Some(e) = extent(&tri, axes) {
|
||||
pieces.push((i, widened(e, mesh_tol)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Survey { here, pieces, flat })
|
||||
}
|
||||
|
||||
/// `e` grown by `by` each way.
|
||||
fn widened(mut e: Extent, by: f64) -> Extent {
|
||||
for (lo, hi) in e.iter_mut() {
|
||||
*lo -= by;
|
||||
*hi += by;
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// The next cut of the middle piece (its extent `here`, its planar faces `flat`): the axis, the
|
||||
/// place along it and whether the tool's side is the positive one. A cut leaving the most
|
||||
/// offending faces (`off`) wholly beyond it, on a side not `cut` yet, placed halfway to the nearest of them; failing
|
||||
/// that, one beside the tool across the most of them (on an axis square to the direction
|
||||
/// first). `None` when no cut beside the tool has any offending face beyond it.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn next_cut(
|
||||
off: &[Extent],
|
||||
tool: &Extent,
|
||||
here: &Extent,
|
||||
flat: &[(Vec3, f64)],
|
||||
cut: &[(usize, bool)],
|
||||
axes: [Vec3; 3],
|
||||
size: f64,
|
||||
gap: f64,
|
||||
) -> Option<(usize, Vec<f64>, bool)> {
|
||||
// Places between `near` (beside the tool) and `far`, best first, out of the planes of the
|
||||
// piece's faces square to the axis.
|
||||
let place = |k: usize, near: f64, far: f64| -> Option<Vec<f64>> {
|
||||
let axis = axes[k];
|
||||
let places: Vec<f64> = [0.5, 0.37, 0.63, 0.25, 0.75, 0.1, 0.9]
|
||||
.iter()
|
||||
.map(|t| near + (far - near) * t)
|
||||
.filter(|x| !flat.iter().any(|(n, o)| n.dot(axis).abs() > 1.0 - 1e-9 && (o * n.dot(axis).signum() - x).abs() < gap))
|
||||
.collect();
|
||||
(!places.is_empty()).then_some(places)
|
||||
};
|
||||
let mut whole: Option<(usize, usize, Vec<f64>, bool)> = None;
|
||||
let mut across: Option<(usize, usize, Vec<f64>, bool)> = None;
|
||||
let beside = size * 2e-3;
|
||||
for k in [0usize, 1, 2] {
|
||||
let (t0, t1) = tool[k];
|
||||
let (h0, h1) = here[k];
|
||||
for positive_side in [true, false] {
|
||||
if cut.contains(&(k, positive_side)) {
|
||||
continue;
|
||||
}
|
||||
// Past the tool on this side: is there any of the piece to cut away?
|
||||
let (edge, room) = if positive_side { (t1 + gap, h1 - t1) } else { (t0 - gap, t0 - h0) };
|
||||
if room <= 2.0 * gap {
|
||||
continue;
|
||||
}
|
||||
let beyond: Vec<f64> = off
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
let (c0, c1) = e[k];
|
||||
if positive_side { (c0 > edge).then_some(c0) } else { (c1 < edge).then_some(c1) }
|
||||
})
|
||||
.collect();
|
||||
if let Some(nearest) = beyond.iter().copied().reduce(if positive_side { f64::min } else { f64::max }) {
|
||||
let Some(x) = place(k, edge, nearest) else { continue };
|
||||
if whole.as_ref().is_none_or(|(_, n, ..)| beyond.len() > *n) {
|
||||
whole = Some((k, beyond.len(), x, !positive_side));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if k == 2 && across.is_some() {
|
||||
continue;
|
||||
}
|
||||
let near = edge + if positive_side { beside } else { -beside };
|
||||
let crossing = off.iter().filter(|e| if positive_side { e[k].1 > near } else { e[k].0 < near }).count();
|
||||
let limit = if positive_side { h1 } else { h0 };
|
||||
if crossing == 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(x) = place(k, edge, near + (near - edge))
|
||||
.map(|p| p.into_iter().filter(|x| if positive_side { *x < limit - gap } else { *x > limit + gap }).collect::<Vec<_>>())
|
||||
.filter(|p| !p.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if across.as_ref().is_none_or(|(_, n, ..)| crossing > *n) {
|
||||
across = Some((k, crossing, x, !positive_side));
|
||||
}
|
||||
}
|
||||
}
|
||||
whole.or(across).map(|(k, _, x, keep_positive)| (k, x, keep_positive))
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
/// A rounded square of four quadratic curves (a font's kind) about (20, 15).
|
||||
fn rounded() -> Region2 {
|
||||
let p = Vec2::new;
|
||||
let q = |a: Vec2, apex: Vec2, b: Vec2| Seg2::Conic { a, apex, b, w: 1.0 };
|
||||
let segs = vec![
|
||||
q(p(10.0, 15.0), p(10.0, 5.0), p(20.0, 5.0)),
|
||||
q(p(20.0, 5.0), p(30.0, 5.0), p(30.0, 15.0)),
|
||||
q(p(30.0, 15.0), p(30.0, 25.0), p(20.0, 25.0)),
|
||||
q(p(20.0, 25.0), p(10.0, 25.0), p(10.0, 15.0)),
|
||||
];
|
||||
Region2 { outer: Loop2 { segs }, holes: vec![] }
|
||||
}
|
||||
|
||||
fn block() -> Body {
|
||||
crate::box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(40.0, 30.0, 10.0)).unwrap()
|
||||
}
|
||||
|
||||
/// Its free-form faces (the tool's walls kept as the curves they were).
|
||||
fn spline_faces(b: &Body) -> usize {
|
||||
b.solid.face_iter().filter(|f| matches!(f.surface(), mt::Surface::BSplineSurface(_) | mt::Surface::NurbsSurface(_))).count()
|
||||
}
|
||||
|
||||
/// A shape of a font's curves engraved into a block is decided in 2D, its walls the exact curves.
|
||||
#[test]
|
||||
fn an_engraving_of_quadratic_curves_is_exact_in_2d() {
|
||||
let region = rounded();
|
||||
let area = region.outer.signed_area().abs();
|
||||
let tool = crate::extrude(&Plane::XY, std::slice::from_ref(®ion), 9.0, 11.0).unwrap().remove(0);
|
||||
let out = prism_boolean(&block(), &tool, BoolOp::Cut).expect("the 2D route takes it").unwrap().unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// Half the tool, measured as the result is (meshes of curved faces are off by about 1e-4).
|
||||
let taken = 12000.0 - crate::measure(&out).unwrap().volume;
|
||||
let half = crate::measure(&tool).unwrap().volume / 2.0;
|
||||
assert!((taken - half).abs() < 1e-6 * half && (taken - area).abs() < 2e-4 * area, "{taken} vs {half} ({area})");
|
||||
assert_eq!(spline_faces(&out), 4);
|
||||
}
|
||||
|
||||
/// A shape running off the block's side is cut where its curves exactly cross the side.
|
||||
#[test]
|
||||
fn an_engraving_running_off_the_side_ends_on_its_exact_crossings() {
|
||||
let region = rounded();
|
||||
// Moved 14 to the left: the block's side x = 0 cuts its two left curves.
|
||||
let shift = |s: &Seg2| match *s {
|
||||
Seg2::Conic { a, apex, b, w } => {
|
||||
Seg2::Conic { a: a - Vec2::new(14.0, 0.0), apex: apex - Vec2::new(14.0, 0.0), b: b - Vec2::new(14.0, 0.0), w }
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let moved = Region2 { outer: Loop2 { segs: region.outer.segs.iter().map(shift).collect() }, holes: vec![] };
|
||||
let tool = crate::extrude(&Plane::XY, std::slice::from_ref(&moved), 9.0, 11.0).unwrap().remove(0);
|
||||
let out = prism_boolean(&block(), &tool, BoolOp::Cut).expect("the 2D route takes it").unwrap().unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// What the block keeps of the shape: the shape's part at x > 0.
|
||||
let inside = planar_regions_boolean(
|
||||
std::slice::from_ref(&moved),
|
||||
&[Region2 {
|
||||
outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0), Vec2::new(40.0, 30.0), Vec2::new(0.0, 30.0)]),
|
||||
holes: vec![],
|
||||
}],
|
||||
BoolOp::Intersect,
|
||||
1e-6,
|
||||
)
|
||||
.unwrap();
|
||||
let area: f64 = inside.iter().map(|r| r.outer.signed_area().abs()).sum();
|
||||
let taken = 12000.0 - crate::measure(&out).unwrap().volume;
|
||||
assert!((taken - area).abs() < 2e-4 * area, "{taken} vs {area}");
|
||||
assert!(spline_faces(&out) >= 2, "{}", spline_faces(&out));
|
||||
}
|
||||
|
||||
/// A tool that misses its target leaves it as it is: the 2D route declines it (the B-rep
|
||||
/// boolean then reuses the target itself). An engraving taking far less than a ten-thousandth
|
||||
/// of a large part is a change all the same, and the route keeps it: told from the exact
|
||||
/// sections, not from volumes measured off meshes, which could not tell it from no change.
|
||||
#[test]
|
||||
fn a_tool_that_misses_is_declined_and_a_fine_engraving_is_not() {
|
||||
let big = crate::box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(200.0, 200.0, 10.0)).unwrap();
|
||||
let region = rounded();
|
||||
let tool = |lo: f64, hi: f64| crate::extrude(&Plane::XY, std::slice::from_ref(®ion), lo, hi).unwrap().remove(0);
|
||||
assert!(prism_boolean(&big, &tool(10.0, 12.0), BoolOp::Cut).is_none(), "a tool above the part misses it");
|
||||
// 0.05 mm deep: 17 mm³ of a 400000 mm³ part.
|
||||
let out = prism_boolean(&big, &tool(9.95, 11.0), BoolOp::Cut).expect("an engraving is a change").unwrap().unwrap();
|
||||
let taken = 400_000.0 - crate::measure(&out).unwrap().volume;
|
||||
let want = region.outer.signed_area().abs() * 0.05;
|
||||
assert!((taken - want).abs() < 2e-3 * want, "{taken} vs {want}");
|
||||
}
|
||||
Reference in New Issue
Block a user