Booleans: a prism cut beside the target's other faces takes the exact 2D route in a column (#1629)
Thanks @phanoob.
This commit is contained in:
1 parent
18ec44d773
commit
9bb65b4d68
8 files changed
+579
-67
No files matched your search
@@ -90,6 +90,7 @@ mod polybool;
|
||||
mod polyhedron;
|
||||
mod prism;
|
||||
mod prism_curve;
|
||||
mod prism_local;
|
||||
mod prism_measure;
|
||||
pub mod provenance;
|
||||
mod record;
|
||||
|
||||
@@ -817,6 +817,19 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
return Ok(Some(body));
|
||||
}
|
||||
}
|
||||
// A prism tool whose target is a prism along its direction only round it (a pocket made
|
||||
// deeper in a plate with a chamfered rim, a ring cut round a counterbored hole in a
|
||||
// chamfered disc): the exact 2D route in a column cut round the tool. Its walls lying on
|
||||
// the target's are what the tries below fail on.
|
||||
if attempt == 1 && matches!(op, BoolOp::Cut | BoolOp::Intersect) {
|
||||
let (local, diagnostics) = capture_boolean_diagnostics(|| crate::prism_local::local_prism_boolean(a, b, op));
|
||||
if let Some(Ok(Some(body))) = local
|
||||
&& check(&body).0
|
||||
{
|
||||
merge(diagnostics);
|
||||
return Ok(Some(body));
|
||||
}
|
||||
}
|
||||
let shift = mt::Vector3::new(j[0], j[1], j[2]) * (size * 0.01);
|
||||
for k in [2e-3, 5e-4] {
|
||||
let tol = (size * k).max(1e-5);
|
||||
|
||||
@@ -527,79 +527,98 @@ pub(crate) fn sliced_planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Re
|
||||
}
|
||||
let pm = planar_polys(&middle.0, 0).map_err(|e| fail(&e.describe("middle piece")))?;
|
||||
let r = polys_boolean(pm, pb, size, op, size * 1e-9, &Sources::of(&[&middle.0, b]))?;
|
||||
let h_b = recorded::kept(b, 1, n);
|
||||
let counts = record::counts(&[a, b]);
|
||||
if op == BoolOp::Intersect {
|
||||
return Ok(r.map(|r| {
|
||||
let h = dropped(recorded::of_pair(&r, &middle.1, &h_b));
|
||||
r.with_checked_history(h, &counts)
|
||||
}));
|
||||
}
|
||||
// The rebuild leaves the cut faces' edges split where other polygons were cut; healed,
|
||||
// they match the pieces outside edge for edge.
|
||||
let pieces: Vec<Traced> = match r {
|
||||
Some(m) => {
|
||||
let healed = recorded::heal_body(&m, m.deep_copy(), size, a.split_keep(), &record::counts(&[&middle.0, b]))?;
|
||||
let h = recorded::of_pair(&healed, &middle.1, &h_b);
|
||||
vec![(healed, h)]
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
// Each piece in its lumps: a cut plane crossing a frame or a ring leaves the middle (or
|
||||
// a piece outside) in several parts, which meet the others along several cut faces.
|
||||
let lumps = |(whole, h): Traced| -> Result<Vec<Traced>> {
|
||||
if whole.solid.boundaries().len() == 1 {
|
||||
return Ok(vec![(whole, h)]);
|
||||
}
|
||||
let parts = whole.lumps()?;
|
||||
let histories = recorded::parts_of(&parts, &whole);
|
||||
Ok(parts
|
||||
.into_iter()
|
||||
.zip(histories)
|
||||
.map(|(p, hp)| {
|
||||
let h = match (hp, h.as_ref()) {
|
||||
(Some(hp), Some(h)) => recorded::composed(hp.substituted(0, h)),
|
||||
_ => None,
|
||||
};
|
||||
(p, h)
|
||||
})
|
||||
.collect())
|
||||
};
|
||||
let mut all: Vec<Traced> = Vec::new();
|
||||
for piece in pieces.into_iter().chain(outside) {
|
||||
all.extend(lumps(piece)?);
|
||||
}
|
||||
// Glue pieces that meet along their cut faces until one is left; every cut face pair
|
||||
// goes (a pair left behind would be a slit through the result).
|
||||
let mut joined: Vec<Traced> = Vec::new();
|
||||
while let Some(mut cur) = all.pop() {
|
||||
loop {
|
||||
let found = all.iter().enumerate().find_map(|(i, (o, _))| Some((i, crate::coplanar::glue(&cur.0, o)?.ok()?)));
|
||||
let Some((i, j)) = found else { break };
|
||||
let other = all.swap_remove(i);
|
||||
let h = recorded::of_pair(&j, &cur.1, &other.1);
|
||||
cur = (j, h);
|
||||
}
|
||||
joined.push(cur);
|
||||
}
|
||||
if joined.iter().any(|(x, _)| x.solid.boundaries().len() != 1) {
|
||||
return Err(fail("a piece in several parts"));
|
||||
}
|
||||
// A join onto one piece is one piece: anything apart is a cut face that did not glue.
|
||||
if op == BoolOp::Union && a.solid.boundaries().len() == 1 && joined.len() != 1 {
|
||||
return Err(fail("a piece does not join back"));
|
||||
}
|
||||
let pieces = joined;
|
||||
recorded::join(pieces.into_iter().map(|(p, h)| (p, dropped(h))).collect(), &counts)
|
||||
reassembled(a, b, op, &middle, r, outside, n, &fail)
|
||||
};
|
||||
Some(run())
|
||||
}
|
||||
|
||||
/// The middle piece's boolean with `b` (`r`, over `[middle, b]`) and the pieces of `a` split
|
||||
/// away from the middle (`outside`) glued back into one result over `[a, b]`: the operands past
|
||||
/// the second (`n` in all, the boxes a piece was cut with) are dropped. `fail` names the route
|
||||
/// in its errors. Shared with [`crate::prism_local`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn reassembled(
|
||||
a: &Body,
|
||||
b: &Body,
|
||||
op: BoolOp,
|
||||
middle: &Traced,
|
||||
r: Option<Body>,
|
||||
outside: Vec<Traced>,
|
||||
n: usize,
|
||||
fail: &dyn Fn(&str) -> KernelError,
|
||||
) -> Result<Option<Body>> {
|
||||
let size = a.size().max(b.size());
|
||||
let h_b = recorded::kept(b, 1, n);
|
||||
let counts = record::counts(&[a, b]);
|
||||
if op == BoolOp::Intersect {
|
||||
return Ok(r.map(|r| {
|
||||
let h = dropped(recorded::of_pair(&r, &middle.1, &h_b));
|
||||
r.with_checked_history(h, &counts)
|
||||
}));
|
||||
}
|
||||
// The rebuild leaves the cut faces' edges split where other polygons were cut; healed,
|
||||
// they match the pieces outside edge for edge.
|
||||
let pieces: Vec<Traced> = match r {
|
||||
Some(m) => {
|
||||
let healed = recorded::heal_body(&m, m.deep_copy(), size, a.split_keep(), &record::counts(&[&middle.0, b]))?;
|
||||
let h = recorded::of_pair(&healed, &middle.1, &h_b);
|
||||
vec![(healed, h)]
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
// Each piece in its lumps: a cut plane crossing a frame or a ring leaves the middle (or
|
||||
// a piece outside) in several parts, which meet the others along several cut faces.
|
||||
let lumps = |(whole, h): Traced| -> Result<Vec<Traced>> {
|
||||
if whole.solid.boundaries().len() == 1 {
|
||||
return Ok(vec![(whole, h)]);
|
||||
}
|
||||
let parts = whole.lumps()?;
|
||||
let histories = recorded::parts_of(&parts, &whole);
|
||||
Ok(parts
|
||||
.into_iter()
|
||||
.zip(histories)
|
||||
.map(|(p, hp)| {
|
||||
let h = match (hp, h.as_ref()) {
|
||||
(Some(hp), Some(h)) => recorded::composed(hp.substituted(0, h)),
|
||||
_ => None,
|
||||
};
|
||||
(p, h)
|
||||
})
|
||||
.collect())
|
||||
};
|
||||
let mut all: Vec<Traced> = Vec::new();
|
||||
for piece in pieces.into_iter().chain(outside) {
|
||||
all.extend(lumps(piece)?);
|
||||
}
|
||||
// Glue pieces that meet along their cut faces until one is left; every cut face pair
|
||||
// goes (a pair left behind would be a slit through the result).
|
||||
let mut joined: Vec<Traced> = Vec::new();
|
||||
while let Some(mut cur) = all.pop() {
|
||||
loop {
|
||||
let found = all.iter().enumerate().find_map(|(i, (o, _))| Some((i, crate::coplanar::glue(&cur.0, o)?.ok()?)));
|
||||
let Some((i, j)) = found else { break };
|
||||
let other = all.swap_remove(i);
|
||||
let h = recorded::of_pair(&j, &cur.1, &other.1);
|
||||
cur = (j, h);
|
||||
}
|
||||
joined.push(cur);
|
||||
}
|
||||
if joined.iter().any(|(x, _)| x.solid.boundaries().len() != 1) {
|
||||
return Err(fail("a piece in several parts"));
|
||||
}
|
||||
// A join onto one piece is one piece: anything apart is a cut face that did not glue.
|
||||
if op == BoolOp::Union && a.solid.boundaries().len() == 1 && joined.len() != 1 {
|
||||
return Err(fail("a piece does not join back"));
|
||||
}
|
||||
let pieces = joined;
|
||||
recorded::join(pieces.into_iter().map(|(p, h)| (p, dropped(h))).collect(), &counts)
|
||||
}
|
||||
|
||||
/// `piece` split by `plane` (at `x` along `axis`): the parts on the side away from the tool
|
||||
/// and the one part on its side (positive along `axis` when `keep_positive`), each with its
|
||||
/// history over the operands `piece`'s is over. `None` when the split leaves no part on either
|
||||
/// side, or several on the tool's.
|
||||
fn split_beside(piece: &Traced, plane: &solidcraft_geom::Plane, axis: Vec3, x: f64, keep_positive: bool) -> Option<(Vec<Traced>, Traced)> {
|
||||
pub(crate) fn split_beside(piece: &Traced, plane: &solidcraft_geom::Plane, axis: Vec3, x: f64, keep_positive: bool) -> Option<(Vec<Traced>, Traced)> {
|
||||
let parts = crate::splitface::split_body(&piece.0, &crate::splitface::SplitTool::Plane(*plane)).ok()?;
|
||||
if parts.len() < 2 {
|
||||
return None;
|
||||
|
||||
@@ -2217,6 +2217,34 @@ fn directions(a: &Body, b: &Body) -> Option<Vec<Vec3>> {
|
||||
Some(dirs)
|
||||
}
|
||||
|
||||
/// The direction `b` is a prism (or a stack of prisms) along: the first of its planar faces'
|
||||
/// normals along which it is one. `None` when there is none.
|
||||
pub(crate) fn prism_axis(b: &Body) -> Option<Vec3> {
|
||||
if b.face_count() > MAX_PLANAR_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
let tol = (b.size() * 1e-7).max(1e-9) * 10.0;
|
||||
let index = LazyIndex::new(b, 1);
|
||||
let mut tried: Vec<Vec3> = Vec::new();
|
||||
for f in b.solid.face_iter() {
|
||||
let mt::Surface::Plane(p) = f.oriented_surface() else { continue };
|
||||
let n = p.normal();
|
||||
let Some(n) = Vec3::new(n.x, n.y, n.z).normalized() else { continue };
|
||||
if tried.iter().any(|m| m.dot(n).abs() > 1.0 - 1e-9) {
|
||||
continue;
|
||||
}
|
||||
tried.push(n);
|
||||
if tried.len() > 16 {
|
||||
return None;
|
||||
}
|
||||
let plane = Plane::from_normal(Vec3::ZERO, n)?;
|
||||
if stack_tagged(b, n, &plane, tol, &index).is_some() {
|
||||
return Some(n);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether a boolean of sections left one of area `v` as it was (area `u` after): the same to
|
||||
/// within rounding, as the very same outline gives (a change is at least a sliver `tol` wide).
|
||||
fn same_area(u: f64, v: f64, tol: f64) -> bool {
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
//! 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.tessellate(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();
|
||||
!(along > 1.0 - 1e-9 || along < 1e-9)
|
||||
}
|
||||
_ => !crate::prism::wall_along(&surface, d, tol, &mut work),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mesh = body.tessellate(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))
|
||||
}
|
||||
Reference in new issue
Block a user