fix: a cut takes only its sketch, a join adds only it (#1310)

Thanks @NOWN-DEV.
This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-17 08:42:17 -04:00
1 parent 0a66697da8
commit 8a2d142333
5 files changed
+569 -128

No files matched your search

+377 -124
View File
@@ -1838,144 +1838,266 @@ fn fill_void_holes(st: &ModelState, r: &Region2, op: Operation, targets: &[Strin
Region2 { outer: r.outer.clone(), holes }
}
/// Grow profile edges that lie on a face of a target body away from the profile, by a small
/// amount, when the space they grow into is already material (join) or empty (cut). `probe`
/// maps a sketch point to the world points where the tool would be (along the extrude, around
/// the revolve). Loops of lines and arcs are adjusted: a line moves out, an arc keeps its centre
/// and changes its radius (a rounded corner of a face cut whole, #1275). Arcs move only where
/// both of their ends meet tangent neighbours that move with them; otherwise the loop is left
/// as it is.
fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, targets: &[String], probe: &dyn Fn(Vec2) -> Vec<Vec3>) -> Region2 {
/// The bodies a cut or join works on (its targets, else every body), asked what its tool would
/// take or add if it reached past its sketch.
struct Surroundings<'a> {
/// Each body with its display mesh and, once something near it is asked, its faces.
bodies: Vec<(&'a ModelBody, Arc<Mesh>, std::cell::OnceCell<Option<kernel::Clearance<'a>>>)>,
op: Operation,
/// The largest body's size.
size: f64,
/// Faces this close to a plane lie in it (the kernel's measure for coincident faces).
skin: f64,
/// How far off a round face a space must stay to be asked about: the face's triangles may
/// stand off it by twice the chord they are made with, and the check pads them as far again.
round: f64,
}
impl<'a> Surroundings<'a> {
/// The chord of the meshes the bodies' faces are asked on, as a share of each one's size.
const CHORD: f64 = 2e-4;
/// `None` for an operation other than a cut or a join, or without a body to work on.
fn of(st: &'a ModelState, op: Operation, targets: &[String]) -> Option<Surroundings<'a>> {
if !matches!(op, Operation::Cut | Operation::Join) {
return None;
}
let bodies: Vec<_> =
st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).map(|b| (b, b.mesh(), std::cell::OnceCell::new())).collect();
let size = bodies.iter().map(|(b, _, _)| b.body.size()).fold(0.0, f64::max);
(!bodies.is_empty()).then(|| Surroundings { bodies, op, size, skin: (size * 1e-6).max(1e-8), round: 4.0 * Self::CHORD * size })
}
/// Does the space the triangles `pieces` of `plane` sweep along its normal, from the height
/// `lo` to `hi`, hold just what the tool may gain without changing the result: nothing of
/// the bodies (a cut), or one body's material (a join)? Decided on the bodies' faces (see
/// [`kernel::Clearance`]), so a wall, a rib, a gap or a hole anywhere in it counts.
/// `margin`: how far the region reaches past the triangles sideways (negative: their sides
/// are drawn in, so that faces lying against them don't count).
fn harmless(&self, plane: &Plane, pieces: &[[Vec2; 3]], margin: f64, lo: f64, hi: f64) -> bool {
let n = plane.normal();
let faces: Vec<([Vec3; 3], f64)> = pieces.iter().map(|t| (t.map(|p| plane.to_world(p)), margin)).collect();
let mut space = solidcraft_geom::Aabb3::EMPTY;
for p in faces.iter().flat_map(|(t, _)| t) {
space.add(*p + n * lo);
space.add(*p + n * hi);
}
let mut full = false;
for (body, mesh, faces_of) in &self.bodies {
// A body well away from that space holds none of it (its faces lie within the
// display mesh's chord tolerance of the mesh).
let (b, pad) = (mesh.bounds(), margin.abs() + 2.0 * body.display_tol());
let away = |lo: Vec3, hi: Vec3| lo.x > hi.x + pad || lo.y > hi.y + pad || lo.z > hi.z + pad;
if !b.is_empty() && (away(b.min, space.max) || away(space.min, b.max)) {
continue;
}
let Some(clearance) = faces_of.get_or_init(|| kernel::Clearance::new(&body.body, body.body.size() * Self::CHORD)) else { return false };
match clearance.swept(&faces, n, lo, hi) {
kernel::Occupancy::Mixed => return false,
kernel::Occupancy::Full => full = true,
kernel::Occupancy::Empty => {}
}
}
full == (self.op == Operation::Join)
}
}
/// The outward normal (away from the profile) at parameter `t` of a segment of a
/// counterclockwise loop: the tangent turned clockwise.
fn outward(s: &solidcraft_geom::Seg2, t: f64) -> Option<Vec2> {
use solidcraft_geom::Seg2;
if !matches!(op, Operation::Cut | Operation::Join) {
return r.clone();
match *s {
Seg2::Line { a, b } => (b - a).normalized().map(|d| Vec2::new(d.y, -d.x)),
Seg2::Arc { start, sweep, .. } => Some(Vec2::from_angle(start + sweep * t) * sweep.signum()),
_ => None,
}
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).collect();
if bodies.is_empty() {
return r.clone();
}
/// A counterclockwise loop of lines and arcs, each moved out by `by` where `grow` says so: a
/// line moves out and meets its neighbours again, an arc keeps its centre and changes its
/// radius. `None` when an edge would turn round (too short for the move), edges in one line
/// would part, a segment would leave a tangent neighbour behind, or an arc would move at a
/// sharp corner.
fn offset_segs(segs: &[solidcraft_geom::Seg2], grow: &[bool], by: f64) -> Option<Vec<solidcraft_geom::Seg2>> {
use solidcraft_geom::Seg2;
let n = segs.len();
let tangent = |a: &Seg2, b: &Seg2| a.end_tangent().cross(b.start_tangent()).abs() < 1e-6 && a.end_tangent().dot(b.start_tangent()) > 0.0;
let step = |g: bool| if g { by } else { 0.0 };
// Where segment `i` starts once moved: the corner of it and the one before.
let mut starts = Vec::with_capacity(n);
for i in 0..n {
let (prev, cur, g0, g1) = (segs.get((i + n - 1) % n)?, segs.get(i)?, *grow.get((i + n - 1) % n)?, *grow.get(i)?);
let corner = cur.start();
if !g0 && !g1 {
starts.push(corner);
} else if tangent(prev, cur) {
// Moved together, a tangent joint moves along the common normal and stays tangent.
if g0 != g1 {
return None;
}
starts.push(corner + outward(cur, 0.0)? * by);
} else {
let (Seg2::Line { a: a0, b: b0 }, Seg2::Line { a: a1, b: b1 }) = (*prev, *cur) else { return None };
let (d0, d1) = ((b0 - a0).normalized()?, (b1 - a1).normalized()?);
let (p0, p1) = (a0 + Vec2::new(d0.y, -d0.x) * step(g0), a1 + Vec2::new(d1.y, -d1.x) * step(g1));
let den = d0.cross(d1);
if den.abs() >= 1e-12 {
starts.push(p0 + d0 * ((p1 - p0).cross(d1) / den));
} else if g0 == g1 {
starts.push(p1);
} else {
return None;
}
}
}
let meshes: Vec<Arc<Mesh>> = bodies.iter().map(|b| b.mesh()).collect();
let size = bodies.iter().map(|b| b.body.size()).fold(0.0, f64::max);
let delta = (size * 0.005).max(1e-3);
let mut out = Vec::with_capacity(n);
for (i, seg) in segs.iter().enumerate() {
let (a, b, g) = (*starts.get(i)?, *starts.get((i + 1) % n)?, *grow.get(i)?);
out.push(match *seg {
Seg2::Line { a: a0, b: b0 } if (b - a).dot(b0 - a0) > 0.0 => Seg2::Line { a, b },
Seg2::Arc { center, radius, start, sweep } if radius + step(g) * sweep.signum() > 0.0 => {
Seg2::Arc { center, radius: radius + step(g) * sweep.signum(), start, sweep }
}
_ => return None,
});
}
Some(out)
}
/// Grow profile edges that lie on a face of a body away from the profile, by a small amount,
/// so that the boolean meets no coincident face there. `probe` maps a sketch point to the
/// world points where the tool would be (along the extrude, around the revolve): an edge with
/// material on one side and room on the other at three points along it may grow. It grows
/// only where `gains` finds the whole strip it would add (triangles in the sketch) harmless,
/// asked a quarter wider than it grows, so that the moved side comes near nothing either:
/// three points miss the walls at the edge's ends, a neighbour across a gap, or a wall thinner
/// than the step. Elsewhere the edge stays where it was drawn. Loops of lines and arcs are
/// adjusted: a line moves out, an arc keeps its centre and changes its radius (a rounded corner
/// of a face cut whole, #1275); an arc's strip, and a line's where it meets one, is asked from
/// a little off the round face the arc lies on (see [`Surroundings`]'s `round`). Edges in one
/// line move together or not at all; arcs move only where both of their ends meet tangent
/// neighbours that move with them, otherwise the loop is left as it is.
fn grow_profile_for_coplanar(sur: &Surroundings, r: &Region2, probe: &dyn Fn(Vec2) -> Vec<Vec3>, gains: &dyn Fn(&[[Vec2; 3]]) -> bool) -> Region2 {
use solidcraft_geom::Seg2;
let delta = (sur.size * 0.005).max(1e-3);
let lp = r.outer.ccw();
let segs = &lp.segs;
if segs.is_empty() || !segs.iter().all(|s| matches!(s, Seg2::Line { .. } | Seg2::Arc { .. })) {
return r.clone();
}
// The outward normal (away from the profile) at parameter `t` of a segment of the
// counterclockwise loop: the tangent turned clockwise.
let out_at = |s: &Seg2, t: f64| -> Option<Vec2> {
match *s {
Seg2::Line { a, b } => (b - a).normalized().map(|d| Vec2::new(d.y, -d.x)),
Seg2::Arc { start, sweep, .. } => Some(Vec2::from_angle(start + sweep * t) * sweep.signum()),
let n = segs.len();
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
let inside: Vec<_> = sur.bodies.iter().map(|(_, m, _)| m.inside_index()).collect();
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
let want_outer = sur.op == Operation::Join;
let mut grow: Vec<bool> = segs
.iter()
.map(|seg| {
if let Seg2::Arc { radius, sweep, .. } = *seg
&& sweep < 0.0
&& radius <= 2.0 * delta
{
// A concave arc too small to shrink by the step.
return false;
}
// Is the edge on a body face? Material on exactly one side of it.
let mids: Vec<(Vec2, Vec2)> = [0.25, 0.5, 0.75].iter().filter_map(|t| Some((seg.point_at(*t), outward(seg, *t)?))).collect();
if mids.len() < 3 {
return false;
}
let side = |k: f64| mids.iter().flat_map(|(m, out)| probe(*m + *out * (delta * k))).map(&inside_any).collect::<Vec<bool>>();
let (outer, inner) = (side(0.5), side(-0.5));
let on_face = outer.iter().all(|x| *x == want_outer) && inner.iter().any(|x| *x != want_outer);
on_face && side(1.0).iter().all(|x| *x == want_outer)
})
.collect();
let in_line = |i: usize| match (segs.get(i % n), segs.get((i + 1) % n)) {
(Some(Seg2::Line { a: a0, b: b0 }), Some(Seg2::Line { a: a1, b: b1 })) => {
matches!(((*b0 - *a0).normalized(), (*b1 - *a1).normalized()), (Some(d0), Some(d1)) if d0.cross(d1).abs() < 1e-12)
}
_ => false,
};
let round = |k: usize| matches!(segs.get(k % n), Some(Seg2::Arc { .. }));
// What segment `i` gains moving out to `moved`, as triangles in the sketch; `None` when
// nothing of it can be asked.
let strip = |i: usize, seg: &Seg2, moved: &Seg2| -> Option<Vec<[Vec2; 3]>> {
match (*seg, *moved) {
(Seg2::Line { a, b }, Seg2::Line { a: a2, b: b2 }) => {
// Its ends drawn back from the round faces of arcs it runs into.
let d = (b - a).normalized()?;
let s0 = if round(i + n - 1) { sur.round } else { 0.0 };
let s1 = if round(i + 1) { sur.round } else { 0.0 };
if (b - a).len() <= s0 + s1 {
return None;
}
let (a, a2, b, b2) = (a + d * s0, a2 + d * s0, b - d * s1, b2 - d * s1);
Some(vec![[a, b, b2], [a, b2, a2]])
}
(Seg2::Arc { center, radius, start, sweep }, Seg2::Arc { radius: far, .. }) => {
// From `round` off the face out to the moved arc, in pieces whose chords stand off
// the circles by less than `round`: on circles drawn round those circles' chords
// where the arc is convex (the chords then stay off the face and reach past the
// moved arc), on the circles themselves where it is concave.
let near = radius + sur.round * sweep.signum();
let steps = ((sweep.abs() / (8.0 * sur.round / radius.max(far)).sqrt()).ceil() as usize).clamp(1, 256);
let scale = if sweep > 0.0 { 1.0 / (sweep.abs() / steps as f64 / 2.0).cos() } else { 1.0 };
let at = |k: usize, rad: f64| center + Vec2::from_angle(start + sweep * k as f64 / steps as f64) * (rad * scale);
Some((0..steps).flat_map(|k| [[at(k, near), at(k + 1, near), at(k + 1, far)], [at(k, near), at(k + 1, far), at(k, far)]]).collect())
}
_ => None,
}
};
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
let inside: Vec<_> = meshes.iter().map(|m| m.inside_index()).collect();
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
let mut grow = vec![0.0; segs.len()];
let mut any = false;
for (i, seg) in segs.iter().enumerate() {
if let Seg2::Arc { radius, sweep, .. } = *seg
&& sweep < 0.0
&& radius <= 2.0 * delta
{
// A concave arc too small to shrink by the step.
continue;
for _ in 0..=n {
// Edges in one line move together or not at all.
while let Some(i) = (0..n).find(|&i| grow.get(i) != grow.get((i + 1) % n) && in_line(i)) {
grow.iter_mut().enumerate().filter(|(k, _)| *k == i || *k == (i + 1) % n).for_each(|(_, g)| *g = false);
}
// Is the edge on a body face? Material on exactly one side of it.
let mids: Vec<(Vec2, Vec2)> = [0.25, 0.5, 0.75].iter().filter_map(|t| Some((seg.point_at(*t), out_at(seg, *t)?))).collect();
if mids.len() < 3 {
continue;
}
let side = |k: f64| mids.iter().flat_map(|(m, out)| probe(*m + *out * (delta * k))).map(inside_any).collect::<Vec<bool>>();
let (outer, inner) = (side(0.5), side(-0.5));
let want_outer = op == Operation::Join;
let on_face = outer.iter().all(|x| *x == want_outer) && inner.iter().any(|x| *x != want_outer);
let room = side(1.0).iter().all(|x| *x == want_outer);
if on_face
&& room
&& let Some(g) = grow.get_mut(i)
{
*g = delta;
any = true;
}
}
if !any {
return r.clone();
}
let n = segs.len();
let tangent = |a: &Seg2, b: &Seg2| a.end_tangent().cross(b.start_tangent()).abs() < 1e-6 && a.end_tangent().dot(b.start_tangent()) > 0.0;
// Where segment `i` starts once grown: the corner of it and the one before.
let mut starts = Vec::with_capacity(n);
for i in 0..n {
let (Some(prev), Some(cur), Some(&g0), Some(&g1)) = (segs.get((i + n - 1) % n), segs.get(i), grow.get((i + n - 1) % n), grow.get(i)) else {
if !grow.contains(&true) {
return r.clone();
};
let corner = cur.start();
if g0 == 0.0 && g1 == 0.0 {
starts.push(corner);
continue;
}
if tangent(prev, cur) {
// Moved together, a tangent joint moves along the common normal and stays tangent.
if g0 != g1 {
return r.clone();
let Some(reach) = offset_segs(segs, &grow, delta * 1.25) else { return r.clone() };
let mut dropped = false;
for (i, (seg, moved)) in segs.iter().zip(&reach).enumerate() {
if grow.get(i) == Some(&true)
&& !strip(i, seg, moved).is_some_and(|pieces| gains(&pieces))
&& let Some(g) = grow.get_mut(i)
{
*g = false;
dropped = true;
}
let Some(out) = out_at(cur, 0.0) else { return r.clone() };
starts.push(corner + out * g1);
continue;
}
let (Seg2::Line { a: a0, b: b0 }, Seg2::Line { a: a1, b: b1 }) = (*prev, *cur) else { return r.clone() };
let (Some(d0), Some(d1)) = ((b0 - a0).normalized(), (b1 - a1).normalized()) else { return r.clone() };
let (p0, p1) = (a0 + Vec2::new(d0.y, -d0.x) * g0, a1 + Vec2::new(d1.y, -d1.x) * g1);
let den = d0.cross(d1);
if den.abs() < 1e-12 {
starts.push(p1);
continue;
if !dropped {
return match offset_segs(segs, &grow, delta) {
Some(segs) => Region2 { outer: solidcraft_geom::Loop2 { segs }, holes: r.holes.clone() },
None => r.clone(),
};
}
let t = (p1 - p0).cross(d1) / den;
starts.push(p0 + d0 * t);
}
let mut out = Vec::with_capacity(n);
for (i, seg) in segs.iter().enumerate() {
let (Some(&a), Some(&b), Some(&g)) = (starts.get(i), starts.get((i + 1) % n), grow.get(i)) else { return r.clone() };
out.push(match *seg {
Seg2::Line { .. } => Seg2::Line { a, b },
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center, radius: radius + g * sweep.signum(), start, sweep },
_ => return r.clone(),
});
}
Region2 { outer: solidcraft_geom::Loop2 { segs: out }, holes: r.holes.clone() }
r.clone()
}
/// Booleans struggle with coincident faces (a cut starting on the face it cuts, a boss
/// starting on the face it joins). Extending the tool past such an end changes nothing when the
/// space beyond is empty (cut) or already material (join), so do exactly that. Only an end
/// that lies on a target face parallel to it moves, and by less than half the way to the next
/// such face level: the samples can miss material beyond the end (a thin floor between a recess
/// above and a ring pocket below), which a longer tool would cut through.
fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi: f64, op: Operation, targets: &[String]) -> (f64, f64) {
if !matches!(op, Operation::Cut | Operation::Join) || st.bodies.is_empty() {
return (lo, hi);
}
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).collect();
if bodies.is_empty() {
return (lo, hi);
}
let size = bodies.iter().map(|b| b.body.size()).fold(0.0, f64::max);
/// that lies on a body face parallel to it moves, by less than half the way to the next such
/// face level, and only when the whole slab it gains (asked a quarter further than it moves)
/// is harmless on the bodies' faces (see [`Surroundings::harmless`]): sample points miss a rib
/// beyond the end, a thin floor between a recess above and a ring pocket below, or a hole or a
/// gap behind it.
fn extend_for_coplanar(sur: &Surroundings, plane: &Plane, r: &Region2, lo: f64, hi: f64) -> (f64, f64) {
let size = sur.size;
let delta = (size * 0.01).max(1e-3).min((hi - lo).abs().max(1e-3));
let meshes: Vec<Arc<Mesh>> = bodies.iter().map(|b| b.mesh()).collect();
let samples = region_samples(r);
let n = plane.normal();
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
let inside: Vec<_> = meshes.iter().map(|m| m.inside_index()).collect();
let inside: Vec<_> = sur.bodies.iter().map(|(_, m, _)| m.inside_index()).collect();
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
// Heights of the targets' faces parallel to the profile plane.
// Heights of the bodies' faces parallel to the profile plane.
let tol = (size * 1e-7).max(1e-9);
let levels: Vec<f64> = meshes
let levels: Vec<f64> = sur
.bodies
.iter()
.map(|(_, m, _)| m)
.flat_map(|m| {
m.triangles.iter().filter_map(|t| {
let [a, b, c] = m.tri(t)?;
@@ -1993,15 +2115,25 @@ fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi:
let gap = levels.iter().map(|z| (z - h) * dir).filter(|g| *g > tol).fold(f64::INFINITY, f64::min);
delta.min(gap * 0.5)
};
// The profile's triangles; curved sides stand off their chords by up to `chord`.
let chord = (size * 2e-4).max(1e-6);
let straight = std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).all(|s| matches!(s, solidcraft_geom::Seg2::Line { .. }));
let ok_beyond = |h: f64, dir: f64, step: f64| {
samples.iter().all(|s| {
let sampled = samples.iter().all(|s| {
let base = plane.to_world(*s);
[0.5, 1.0].iter().all(|k| {
let p = base + n * (h + dir * step * k);
let inside = inside_any(p);
if op == Operation::Cut { !inside } else { inside }
})
})
[0.5, 1.0].iter().all(|k| inside_any(base + n * (h + dir * step * k)) == (sur.op == Operation::Join))
});
if !sampled {
return false;
}
let pieces = r.triangulate(chord);
// Triangles that leave part of a straight profile uncovered leave it unasked.
let covered: f64 = pieces.iter().map(|[a, b, c]| (*b - *a).cross(*c - *a).abs() * 0.5).sum();
if pieces.is_empty() || (straight && (covered - r.area()).abs() > 1e-7 * r.area()) {
return false;
}
let (h0, h1) = if dir > 0.0 { (h + sur.skin, h + step * 1.25) } else { (h - step * 1.25, h - sur.skin) };
sur.harmless(plane, &pieces, if straight { -sur.skin } else { 2.0 * chord }, h0, h1)
};
let (down, up) = (room(lo, -1.0), room(hi, 1.0));
let lo2 = if down > tol && ok_beyond(lo, -1.0, down) { lo - down } else { lo };
@@ -2932,7 +3064,9 @@ fn feature_tools_with_constructor(
// How far a tool grows past its profile at either end, at most.
let mut growth: f64 = 0.0;
// A face of the targets in the profile's plane (the same for every profile).
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, &crate::plain(targets));
let plain_targets: Vec<String> = crate::plain(targets);
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, &plain_targets);
let surroundings = Surroundings::of(st, *operation, &plain_targets);
let tools: Vec<Traced> = regions
.iter()
.map(|r| {
@@ -2956,14 +3090,14 @@ fn feature_tools_with_constructor(
return Ok(roles.tool(exact, None));
}
let (lo, hi) = if sign < 0.0 { (from - len, from) } else { (from, from + len) };
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, &crate::plain(targets));
let (l2, h2) = surroundings.as_ref().map_or((lo, hi), |sur| extend_for_coplanar(sur, &ss.plane, r, lo, hi));
let (mut a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
// A taper that narrows the tool away from the face widens it as it runs on into it, past
// the profile (where the profile is the whole face, out through the body's sides, #1534):
// then it starts on the face instead.
if a < 0.0 && a * taper.tan() > 0.0 {
let dir = n * sign;
if !taper_run_on_fits(st, &start_plane, dir, r, -a, a * taper.tan(), *operation, &crate::plain(targets)) {
if !taper_run_on_fits(st, &start_plane, dir, r, -a, a * taper.tan(), *operation, &plain_targets) {
a = 0.0;
}
}
@@ -3027,6 +3161,8 @@ fn feature_tools_with_constructor(
let mut footprints = Vec::with_capacity(regions.len());
// Each tool's extent along the normal (they differ where one is run on into a face).
let mut extents: Vec<(f64, f64)> = Vec::with_capacity(regions.len());
let plain_targets: Vec<String> = crate::plain(targets);
let surroundings = Surroundings::of(st, *operation, &plain_targets);
for r in &regions {
if *operation == Operation::Join {
let exact = centered_extrude(&ss.plane, r, lo + off, hi + off, Some(&ss))?;
@@ -3044,14 +3180,32 @@ fn feature_tools_with_constructor(
continue;
}
}
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, &crate::plain(targets));
let (l2, h2) = surroundings.as_ref().map_or((lo + off, hi + off), |sur| extend_for_coplanar(sur, &ss.plane, r, lo + off, hi + off));
let probe = |p: Vec2| (1..4).map(|k| ss.plane.to_world(p) + n * (lo + off + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
let original = r;
let plain_targets = crate::plain(targets);
let r = grow_profile_for_coplanar(st, r, *operation, &plain_targets, &probe);
let r = fill_void_holes(st, &r, *operation, &plain_targets, &ss.plane, (l2, h2));
let grown = |gains: &dyn Fn(&[[Vec2; 3]]) -> bool| match &surroundings {
Some(sur) => grow_profile_for_coplanar(sur, r, &probe, gains),
None => r.clone(),
};
// A hole of the profile over empty space (a cut) or material (a join) is taken in.
let filled = |p: Region2| fill_void_holes(st, &p, *operation, &plain_targets, &ss.plane, (l2, h2));
// A side's strip runs the tool's height, drawn back by a hair from its ends.
let mut r = filled(grown(&|pieces| {
surroundings.as_ref().is_some_and(|sur| sur.harmless(&ss.plane, pieces, -sur.skin, l2 + sur.skin, h2 - sur.skin))
}));
let mut built = centered_extrude(&ss.plane, &r, l2, h2, Some(&ss));
if built.is_err() {
// A profile the kernel cannot build as drawn (a square some hundredths of a
// micrometre across, away from the origin, defeats truck's plane fit) grows
// wherever the samples find room, as it used to.
let loose = filled(grown(&|_| true));
if loose != r {
built = centered_extrude(&ss.plane, &loose, l2, h2, Some(&ss));
r = loose;
}
}
untrimmed &= l2 == lo + off && h2 == hi + off && &r == original;
tools.extend(centered_extrude(&ss.plane, &r, l2, h2, Some(&ss))?);
tools.extend(built?);
footprints.push(r);
extents.push((l2, h2));
}
@@ -3180,6 +3334,7 @@ fn feature_tools_with_constructor(
let (operation, targets) = feature_op(f);
let axis_w = ss.plane.dir_to_world(d);
let origin_w = ss.plane.to_world(o);
let surroundings = Surroundings::of(st, operation, &targets);
let regions: Vec<Region2> = regions
.iter()
.zip(&region_extents)
@@ -3199,7 +3354,11 @@ fn feature_tools_with_constructor(
})
.collect::<Vec<_>>()
};
Ok(grow_profile_for_coplanar(st, r, operation, &targets, &probe))
// Around the axis the strips are rings, which the faces are not asked about yet.
Ok(match &surroundings {
Some(sur) => grow_profile_for_coplanar(sur, r, &probe, &|_| true),
None => r.clone(),
})
})
.collect::<Result<Vec<_>>>()?;
// To a curved face: turn past the whole target body, then cut back to its surface
@@ -5554,6 +5713,100 @@ mod body_name_tests {
}
}
#[cfg(test)]
mod coplanar_growth_tests {
use super::*;
fn state(bodies: Vec<Body>) -> ModelState {
ModelState { bodies: bodies.into_iter().enumerate().map(|(i, b)| ModelBody::new(format!("Body{i}"), b, 0)).collect(), ..Default::default() }
}
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Region2 {
let p = Vec2::new;
Region2 { outer: solidcraft_geom::Loop2::polygon(&[p(x0, y0), p(x1, y0), p(x1, y1), p(x0, y1)]), holes: Vec::new() }
}
/// The rectangle `x0` … `x1` by `y0` … `y1` with its corners rounded to `rad`, counterclockwise.
fn rounded(x0: f64, y0: f64, x1: f64, y1: f64, rad: f64) -> Region2 {
use solidcraft_geom::Seg2;
let p = Vec2::new;
let quarter = std::f64::consts::FRAC_PI_2;
let arc = |cx: f64, cy: f64, k: f64| Seg2::Arc { center: p(cx, cy), radius: rad, start: k * quarter, sweep: quarter };
let segs = vec![
Seg2::Line { a: p(x0 + rad, y0), b: p(x1 - rad, y0) },
arc(x1 - rad, y0 + rad, 3.0),
Seg2::Line { a: p(x1, y0 + rad), b: p(x1, y1 - rad) },
arc(x1 - rad, y1 - rad, 0.0),
Seg2::Line { a: p(x1 - rad, y1), b: p(x0 + rad, y1) },
arc(x0 + rad, y1 - rad, 1.0),
Seg2::Line { a: p(x0, y1 - rad), b: p(x0, y0 + rad) },
arc(x0 + rad, y0 + rad, 2.0),
];
Region2 { outer: solidcraft_geom::Loop2 { segs }, holes: Vec::new() }
}
/// The extrude's growth of `r` (a cut from the plane z = `z`, `lo` … `hi` along +z), as
/// `feature_tools_with_constructor` asks for it.
fn grown(st: &ModelState, z: f64, r: &Region2, lo: f64, hi: f64) -> (Region2, f64, f64) {
let sur = Surroundings::of(st, Operation::Cut, &[]).unwrap();
let plane = Plane { origin: Vec3::new(0.0, 0.0, z), ..Plane::XY };
let (l2, h2) = extend_for_coplanar(&sur, &plane, r, lo, hi);
let probe = |p: Vec2| (1..4).map(|k| plane.to_world(p) + Vec3::Z * (lo + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
let gains = |pieces: &[[Vec2; 3]]| sur.harmless(&plane, pieces, -sur.skin, l2 + sur.skin, h2 - sur.skin);
(grow_profile_for_coplanar(&sur, r, &probe, &gains), l2, h2)
}
/// A cut's tool still grows past the faces it lies on where all it gains is empty space: a
/// notch 10 deep into the corner of a 100 × 60 × 30 block, flush with two of its sides,
/// grows out of those sides by 0.5 % of the block's size and up out of its top by 1 %.
/// Cut into the end wall of a tray of that size, flush with the wall's inner face, the
/// strip its side would gain holds the start of the long walls: there it stays as drawn.
#[test]
fn the_tool_grows_only_where_it_gains_nothing() {
let block = kernel::box_solid(Vec3::ZERO, Vec3::new(100.0, 60.0, 30.0)).unwrap();
let st = state(vec![block.clone()]);
let notch = rect(0.0, 0.0, 20.0, 20.0);
let (r, lo, hi) = grown(&st, 30.0, &notch, -10.0, 0.0);
let step = block.size() * 0.005;
for p in [Vec2::new(-0.9 * step, 10.0), Vec2::new(10.0, -0.9 * step), Vec2::new(-0.9 * step, -0.9 * step)] {
assert!(r.contains(p) && !notch.contains(p), "{p:?}: {r:?}");
}
assert!(!r.contains(Vec2::new(20.0 + 0.1 * step, 10.0)) && !r.contains(Vec2::new(10.0, 20.0 + 0.1 * step)), "{r:?}");
assert_eq!(lo, -10.0);
assert!((hi - block.size() * 0.01).abs() < 1e-9, "{hi}");
let inner = kernel::box_solid(Vec3::new(2.0, 2.0, 2.0), Vec3::new(98.0, 58.0, 40.0)).unwrap();
let tray = kernel::boolean(&block, &inner, BoolOp::Cut).unwrap().unwrap();
let st = state(vec![tray]);
let end_wall = rect(-1.0, -1.0, 2.0, 70.0);
assert_eq!(grown(&st, 2.0, &end_wall, 0.0, 40.0), (end_wall, 0.0, 40.0));
}
/// Arcs grow too (#1275), on the same terms. A cut of the whole top face of a 40 × 40 × 20
/// block with corners rounded to 8 grows out of every side, its corners' arcs by the same step
/// as its edges. A post standing 0.1 off one corner's arc, between the points the samples
/// look at, would lose a sliver to the grown arc: there the loop stays as drawn.
#[test]
fn arcs_grow_only_where_they_gain_nothing() {
let face = rounded(0.0, 0.0, 40.0, 40.0, 8.0);
let block = kernel::extrude(&Plane::XY, std::slice::from_ref(&face), 0.0, 20.0).unwrap().remove(0);
let step = block.size() * 0.005;
let st = state(vec![block.clone()]);
let (r, _, _) = grown(&st, 20.0, &face, -5.0, 0.0);
let corner = |k: f64| Vec2::new(8.0, 8.0) + Vec2::from_angle(1.25 * std::f64::consts::PI) * (8.0 + k * step);
for q in [Vec2::new(-0.9 * step, 20.0), Vec2::new(20.0, 40.0 + 0.9 * step), corner(0.9)] {
assert!(r.contains(q) && !face.contains(q), "{q:?}: {r:?}");
}
assert!(!r.contains(corner(1.1)), "{r:?}");
// The post's corner nearest the arc's centre (8, 8) lies 8.1 from it, at 190 degrees; the
// samples look at 202.5, 225 and 247.5 degrees.
let post = kernel::box_solid(Vec3::new(-2.0, 5.0, 0.0), Vec3::new(8.0 - 7.977, 8.0 - 1.407, 20.0)).unwrap();
let st = state(vec![block, post]);
assert_eq!(grown(&st, 20.0, &face, -5.0, 0.0).0, face);
}
}
/// The parts of a body split by a plane, on its positive side first (only the non-empty ones).
/// Each part's cut face is named `section` after the half-space box that cut it, so it takes an
/// attachment that follows the plane (#565); the box's other faces never reach the part.
+176
View File
@@ -0,0 +1,176 @@
//! A cut takes exactly the prism its sketch and extent define, and a join adds exactly that,
//! whatever stands beside the tool or beyond its ends. The tool is still grown past a face it
//! lies on (to keep coincident faces out of the boolean), but only where what it gains is empty
//! (a cut) or material (a join) on the bodies' faces, not just at a few sample points.
use serde_json::{Value, json};
use solidcraft_geom::Vec3;
use crate::Session;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
match s.execute(id, &p) {
Ok(v) => v,
Err(e) => panic!("{id} {p}: {e}"),
}
}
fn volume(s: &Session, body: &str) -> f64 {
s.model.state().body(body).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(f64::NAN)
}
fn has(s: &Session, body: &str, at: [f64; 3]) -> bool {
s.model.state().body(body).is_some_and(|b| b.body.contains(Vec3::new(at[0], at[1], at[2])))
}
fn bodies(s: &Session) -> Vec<String> {
s.model.state().bodies.iter().map(|b| b.name.clone()).collect()
}
fn same(got: f64, want: f64) -> bool {
(got - want).abs() <= 1e-9 * want.abs().max(1.0)
}
/// A rectangle sketched on `plane`, extruded by `distance` along its normal (the other way
/// when negative).
fn rectangle(s: &mut Session, plane: Value, p0: [f64; 2], p1: [f64; 2], distance: f64, operation: &str, targets: Value) {
run(s, "sketch.create", json!({"plane": plane, "name": "Tool"}));
run(s, "sketch.rectangle.two_point", json!({"p0": p0, "p1": p1}));
run(s, "sketch.finish", json!({}));
let direction = if distance < 0.0 { "negative" } else { "positive" };
let mut p = json!({"sketch": "Tool", "distance": distance.abs(), "direction": direction, "operation": operation});
if !targets.is_null() {
p["targets"] = targets;
}
run(s, "solid.extrude", p);
}
/// The plane z = `z`.
fn at_height(z: f64) -> Value {
json!({"origin": [0, 0, z], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]})
}
/// An open tray, `l` × `w` × `h` outside, with a 2 mm floor and 2 mm walls.
fn tray(l: f64, w: f64, h: f64) -> Session {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": l, "width": w, "height": h, "body_name": "Tray"}));
run(&mut s, "solid.box", json!({"corner": [2, 2, 2], "length": l - 4.0, "width": w - 4.0, "height": h, "operation": "cut", "targets": ["Tray"]}));
assert!(same(volume(&s, "Tray"), l * w * h - (l - 4.0) * (w - 4.0) * (h - 2.0)));
s
}
/// A cut from the tray's floor up, over x −1 … X and all of y, takes the end wall x = 0 … 2
/// and, past x = 2, the start of the two long walls. At X = 2 its side lies in the end wall's
/// inner face, at 1.9 it leaves 0.1 mm of that wall, at 2.1 it reaches 0.1 mm past it. The
/// tray then starts at x = X and has lost min(X, 2) · W · (H − 2) + max(0, X − 2) · 2 · 2 ·
/// (H − 2), whatever its size. (The tool's side used to be grown by 0.5 % of the tray's size
/// where three points along it found room: 0.6 mm of the long walls went with it on a 100 mm
/// tray.)
#[test]
fn a_cut_beside_a_wall_takes_only_its_sketch() {
for (l, w, h, x) in [(100.0, 60.0, 30.0, 1.9), (100.0, 60.0, 30.0, 2.0), (100.0, 60.0, 30.0, 2.1), (1000.0, 600.0, 300.0, 2.0)] {
let mut s = tray(l, w, h);
let before = volume(&s, "Tray");
rectangle(&mut s, at_height(2.0), [-1.0, -1.0], [x, w + 10.0], h + 10.0, "cut", json!(["Tray"]));
let removed = x.min(2.0) * w * (h - 2.0) + (x - 2.0).max(0.0) * 2.0 * 2.0 * (h - 2.0);
let took = before - volume(&s, "Tray");
assert!(same(took, removed), "{l} × {w} × {h} cut to x = {x}: took {took} mm³, its sketch {removed} mm³");
// The long wall starts at the cut; what is left of the end wall and the floor stay.
assert!(has(&s, "Tray", [x + 0.05, 1.0, h / 2.0]) && !has(&s, "Tray", [x - 0.05, 1.0, h / 2.0]), "{l} × {w} × {h} cut to x = {x}");
assert_eq!(has(&s, "Tray", [(x + 2.0) / 2.0, w / 2.0, h / 2.0]), x < 2.0, "{l} × {w} × {h} cut to x = {x}");
assert!(has(&s, "Tray", [x / 2.0, w / 2.0, 1.0]), "{l} × {w} × {h} cut to x = {x}: the floor under the cut is gone");
}
}
/// What stands beside a cut whose side lies in a face stays: a 0.2 mm web between a slot and
/// the plate's edge, and a 1 mm plate standing 0.8 mm from the face (which the grown tool used
/// to cut in two).
#[test]
fn a_cut_leaves_what_stands_beside_it() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 200, "width": 100, "height": 10, "body_name": "Plate"}));
rectangle(&mut s, at_height(0.0), [0.2, 20.0], [10.0, 80.0], 10.0, "cut", Value::Null);
assert!(same(volume(&s, "Plate"), 200.0 * 100.0 * 10.0 - 9.8 * 60.0 * 10.0), "{}", volume(&s, "Plate"));
assert!(has(&s, "Plate", [0.1, 50.0, 5.0]) && !has(&s, "Plate", [0.3, 50.0, 5.0]));
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 1000, "width": 600, "height": 200, "body_name": "A"}));
run(&mut s, "solid.box", json!({"corner": [1000.8, 0, 0], "length": 1, "width": 600, "height": 200, "operation": "new_body", "body_name": "B"}));
rectangle(&mut s, at_height(0.0), [900.0, 100.0], [1000.0, 500.0], 200.0, "cut", Value::Null);
assert_eq!(bodies(&s), ["A", "B"], "the plate must not be cut in two");
assert!(same(volume(&s, "A"), 1000.0 * 600.0 * 200.0 - 100.0 * 400.0 * 200.0), "{}", volume(&s, "A"));
assert!(same(volume(&s, "B"), 600.0 * 200.0), "{}", volume(&s, "B"));
}
/// A join adds only its sketch: a block against a wall narrower than the block adds nothing
/// beside the wall, a block 0.1 mm from a wall leaves the gap, and a pad flush with a block's
/// side does not reach a neighbour 0.8 mm from that side.
#[test]
fn a_join_adds_only_its_sketch() {
let wall = || {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"corner": [0, 10, 0], "length": 2, "width": 40, "height": 30, "body_name": "Wall"}));
s
};
let mut s = wall();
rectangle(&mut s, at_height(0.0), [2.0, 0.0], [20.0, 60.0], 30.0, "join", Value::Null);
assert!(same(volume(&s, "Wall"), 2.0 * 40.0 * 30.0 + 18.0 * 60.0 * 30.0), "{}", volume(&s, "Wall"));
assert!(!has(&s, "Wall", [1.9, 5.0, 15.0]) && !has(&s, "Wall", [1.9, 55.0, 15.0]) && has(&s, "Wall", [1.9, 30.0, 15.0]));
let mut s = wall();
rectangle(&mut s, at_height(0.0), [2.1, 10.0], [20.0, 50.0], 30.0, "join", Value::Null);
assert!(same(volume(&s, "Wall"), 2.0 * 40.0 * 30.0 + 17.9 * 40.0 * 30.0), "{}", volume(&s, "Wall"));
assert!(!has(&s, "Wall", [2.05, 30.0, 15.0]), "the 0.1 mm gap is filled");
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 300, "width": 200, "height": 100, "body_name": "A"}));
run(
&mut s,
"solid.box",
json!({"corner": [300.8, 0, 0], "length": 99.2, "width": 200, "height": 150, "operation": "new_body", "body_name": "B"}),
);
rectangle(&mut s, at_height(100.0), [280.0, 50.0], [300.0, 150.0], 20.0, "join", Value::Null);
assert_eq!(bodies(&s), ["A", "B"], "the pad must not bridge the gap");
assert!(same(volume(&s, "A"), 300.0 * 200.0 * 100.0 + 20.0 * 100.0 * 20.0), "{}", volume(&s, "A"));
}
/// A window cut down through a plate ends on its underside, where a 4 mm rib hangs below
/// between the points the end used to be checked at: the rib stays whole.
#[test]
fn a_cut_ending_on_a_face_leaves_the_rib_beyond_it() {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "Section"}));
let tee = [[0, 20], [88, 20], [88, 0], [92, 0], [92, 20], [200, 20], [200, 30], [0, 30]];
run(&mut s, "sketch.line", json!({"points": tee, "closed": true}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Section", "distance": 200, "body_name": "Tee"}));
let before = volume(&s, "Tee");
assert!(same(before, (200.0 * 10.0 + 4.0 * 20.0) * 200.0), "{before}");
rectangle(&mut s, at_height(30.0), [50.0, -150.0], [150.0, -50.0], -10.0, "cut", Value::Null);
assert!(same(before - volume(&s, "Tee"), 100.0 * 100.0 * 10.0), "{}", before - volume(&s, "Tee"));
assert!(has(&s, "Tee", [90.0, -100.0, 19.5]), "the rib under the window is cut");
}
/// A hole cut up through a ring from its flat underside (the reference case `60-pipe-flange`).
/// The ring is revolved, so that face is a line swept round the axis, not a plane, but flat
/// all the same: the tool still starts past it and the hole opens it, leaving no film of zero
/// thickness over the hole (two discs of its area more surface).
#[test]
fn a_hole_from_a_revolved_rings_flat_side_opens_it() {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "Profile"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [25, 0], "p1": [75, -16]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.revolve", json!({"sketch": "Profile", "axis": {"origin": [0, 0, 0], "dir": [0, 0, 1]}, "angle": 360, "body_names": ["Ring"]}));
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Hole"}));
run(&mut s, "sketch.circle.center", json!({"center": [60, 0], "radius": 7}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Hole", "through_all": true, "direction": "positive", "operation": "cut"}));
let pi = std::f64::consts::PI;
let ring = s.model.state().body("Ring").map(|b| solidcraft_kernel::measure(&b.body).unwrap()).unwrap();
let want = 2.0 * pi * (75.0 * 75.0 - 25.0 * 25.0 - 7.0 * 7.0) + 2.0 * pi * (75.0 + 25.0 + 7.0) * 16.0;
assert!((ring.area - want).abs() < 1e-3 * want, "area {} instead of {want}", ring.area);
let volume = pi * (75.0 * 75.0 - 25.0 * 25.0 - 7.0 * 7.0) * 16.0;
assert!((ring.volume / volume - 1.0).abs() < 1e-3, "volume {} instead of {volume}", ring.volume);
assert!(!has(&s, "Ring", [60.0, 0.0, 8.0]), "the hole is not cut");
}
+2
View File
@@ -1019,6 +1019,8 @@ mod derived_units_tests;
#[cfg(test)]
mod display_mesh_tests;
#[cfg(test)]
mod exact_tool_tests;
#[cfg(test)]
mod extent_tests;
#[cfg(test)]
mod face_name_tests;
+13 -4
View File
@@ -5,6 +5,10 @@
//! body (a cut), or nothing but its material (a join). A [`Clearance`] decides that on the
//! other body's faces: every triangle of its mesh is clipped against that space, so a wall, a
//! rib or a hole anywhere in it is found. Sample points miss what lies between them.
//!
//! A cut's or join's tool grown past the faces it lies on, to keep coincident faces out of its
//! boolean, is asked the same way (in the document's evaluation): it may gain only empty space
//! (a cut) or material (a join).
use std::collections::HashMap;
@@ -16,7 +20,7 @@ use crate::offset::{Surf, surf_of};
/// How a body's material sits in a region of space.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Occupancy {
pub enum Occupancy {
/// The region holds none of it.
Empty,
/// The region lies wholly in it.
@@ -104,7 +108,7 @@ enum Reach {
}
/// A body's faces, for telling how its material sits in the space a face moves through.
pub(crate) struct Clearance<'a> {
pub struct Clearance<'a> {
body: &'a Body,
mesh: Mesh,
/// How far a curved face may stand off its triangles.
@@ -119,7 +123,7 @@ const MAX_PAIRS: usize = 2_000_000;
impl<'a> Clearance<'a> {
/// `chord`: the chord error of the mesh the body is asked on.
pub(crate) fn new(body: &'a Body, chord: f64) -> Option<Clearance<'a>> {
pub fn new(body: &'a Body, chord: f64) -> Option<Clearance<'a>> {
let mesh = body.tessellate_with(chord, true).ok()?;
(!mesh.triangles.is_empty()).then(|| Clearance { body, mesh, sag: 2.0 * chord, tol: (body.size() * 1e-7).max(1e-9), exact: false })
}
@@ -140,7 +144,7 @@ impl<'a> Clearance<'a> {
/// planar faces square to `along`, each with how far its face reaches past it sideways
/// (negative: the triangle's sides are drawn in, so that faces lying against them don't
/// count). `Mixed` as soon as a face of the body reaches into that space.
pub(crate) fn swept(&self, faces: &[([Vec3; 3], f64)], along: Vec3, lo: f64, hi: f64) -> Occupancy {
pub fn swept(&self, faces: &[([Vec3; 3], f64)], along: Vec3, lo: f64, hi: f64) -> Occupancy {
let Some((first, _)) = faces.first() else { return Occupancy::Mixed };
let base = along.dot(first[0]);
// The body's triangles near that space.
@@ -264,6 +268,11 @@ impl<'a> Clearance<'a> {
return Ok(Reach::Never);
}
let round = surf_of(f, self.tol * 100.0);
// A flat face stored otherwise (a revolve's end: a line swept round its axis) is
// a plane all the same: its triangles lie in it.
if matches!(round, Some(Surf::Plane { .. })) {
return Ok(Reach::Exact);
}
let straight = matches!(round, Some(Surf::Cylinder { a, .. }) if a.dot(along).abs() > 1.0 - 1e-12)
|| crate::prism::wall_along(&surface, along, self.tol * 10.0, &mut 262_144);
Ok(if straight {
+1
View File
@@ -148,6 +148,7 @@ pub use build::{
PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, loft, path_polyline, revolve,
revolve_whole, sphere, sweep, sweep_with, torus,
};
pub use clearance::{Clearance, Occupancy};
pub use coaxial_overlap::{CoaxialOverlap, coaxial_overlap};
pub use continuity::{
ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamVerdict, SeamWorst, pick_edges, seam_diagnostics,