Interference: a pin a hair off the axis of its hole gets its crescent (#1893)

Thanks @NOWN-DEV.
This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-17 04:08:31 -04:00
1 parent 713111d9df
commit 962070e955
4 files changed
+466 -2

No files matched your search

+12 -2
View File
@@ -73,7 +73,7 @@ pub static COMMANDS: &[CommandSpec] = &[
),
CommandSpec::new("joint.solve", "Solve Joints", solve_cmd).icon("compute").params("re-place the occurrences from the joints → conflicts, errors, dof"),
CommandSpec::new("inspect.interference", "Interference", interference).at("SOLID", "INSPECT").icon("interference").noundo().params(
"bodies?: [names] (default: all, in world placement) → interferences: pairs that overlap with the overlap volume, area, bounding box and centre; unchecked: pairs that could not be checked, with the reason. Bodies are pruned by bounding box first, so any number of bodies is fine; at most 20000 pairs whose boxes overlap are intersected per call (truncated: true and skipped_pairs: n say when more were left out)",
"bodies?: [names] (default: all, in world placement) → interferences: pairs that overlap with the overlap volume, area, bounding box and centre (a pin a hair off the axis of a hole of its size also has near_coaxial: {offset_mm, length_mm}, its volume the crescent the walls share); unchecked: pairs that could not be checked, with the reason. Bodies are pruned by bounding box first, so any number of bodies is fine; at most 20000 pairs whose boxes overlap are intersected per call (truncated: true and skipped_pairs: n say when more were left out)",
),
];
@@ -1156,6 +1156,16 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
let (mut hits, mut unchecked) = (Vec::new(), Vec::new());
for (i, j) in pairs {
let (Some(a), Some(b)) = (bodies.get(i), bodies.get(j)) else { continue };
let threshold = 1e-9 * a.body.size().max(1.0).powi(3);
// A pin a hair off the axis of its hole: the crescent the walls share, which the
// boolean can't intersect.
if let Some(o) = solidcraft_kernel::coaxial_overlap(&a.body, &b.body) {
if o.volume > threshold {
hits.push(json!({"a": a.name, "b": b.name, "volume_mm3": o.volume, "area_mm2": o.area, "center": o.centroid,
"bbox": {"min": o.bbox.min, "max": o.bbox.max}, "near_coaxial": {"offset_mm": o.offset, "length_mm": o.length}}));
}
continue;
}
// A pair whose shared part can't be worked out is reported, not passed as clear.
let (shared, report) =
solidcraft_kernel::capture_boolean_diagnostics(|| solidcraft_kernel::boolean(&a.body, &b.body, solidcraft_kernel::BoolOp::Intersect));
@@ -1178,7 +1188,7 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
continue;
}
};
if m.volume > 1e-9 * a.body.size().max(1.0).powi(3) {
if m.volume > threshold {
hits.push(json!({"a": a.name, "b": b.name, "volume_mm3": m.volume, "area_mm2": m.area, "center": m.centroid, "bbox": {"min": m.bbox.min, "max": m.bbox.max}}));
}
}
+28
View File
@@ -1452,3 +1452,31 @@ mod cam;
// Plots and interference of motion studies, on the four-bar and slider-crank above.
#[path = "motion_analysis_tests.rs"]
mod motion_analysis;
/// A pin through a hole of its own radius whose axis is a hair off the hole's (parts placed from
/// rounded coordinates): the walls are all but tangent along two lines, which the boolean can't
/// intersect. Each offset gets a checked answer: clear on the axis and within the kernel's
/// same-surface tolerance, and otherwise the crescent the pin shares with the plate, against
/// the textbook lens of two circles. The pin is chamfered, so it is not a plain prism.
#[test]
fn interference_of_a_pin_a_hair_off_its_hole() {
let crescent = |r: f64, e: f64| std::f64::consts::PI * r * r - (2.0 * r * r * (e / (2.0 * r)).acos() - e / 2.0 * (4.0 * r * r - e * e).sqrt());
for offset in [0.0, 1e-6, 5.9e-4, 1e-2] {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 5, "corner": [-10, -10, 0], "body_name": "Plate"}));
run(&mut s, "solid.cylinder", json!({"radius": 3, "height": 9, "base": [0, 0, -2], "operation": "cut", "targets": ["Plate"]}));
run(&mut s, "solid.cylinder", json!({"radius": 3, "height": 9, "base": [offset, 0, -2], "operation": "new", "body_name": "Pin"}));
run(&mut s, "solid.chamfer", json!({"edges": [[offset + 3.0, 0, 7]], "distance": 0.5, "body": "Pin"}));
let i = run(&mut s, "inspect.interference", json!({"bodies": ["Plate", "Pin"]}));
assert_eq!(i["unchecked"].as_array().map(Vec::len), Some(0), "{offset}: {i}");
if offset < 1e-5 {
assert_eq!(i["count"], 0, "{offset}: {i}");
continue;
}
assert_eq!(i["count"], 1, "{offset}: {i}");
let want = 5.0 * crescent(3.0, offset);
let got = i["interferences"][0]["volume_mm3"].as_f64().unwrap_or(0.0);
assert!((got - want).abs() < 1e-6 * want, "{offset}: {got} against {want}: {i}");
assert!((i["interferences"][0]["near_coaxial"]["offset_mm"].as_f64().unwrap_or(0.0) - offset).abs() < 1e-9, "{i}");
}
}
+424
View File
@@ -0,0 +1,424 @@
//! The shared volume of a pin in a hole of (nearly) its own size whose axis is a hair off the
//! hole's (parts placed from rounded coordinates).
//!
//! The pin's wall and the hole's wall then meet along two lines where they are all but tangent,
//! which the boolean cannot intersect, and the facets of the two walls pass through each other
//! by less than the boolean's sampled shortcuts can see. So the overlap is worked out in two
//! parts instead. The pin's wall is moved off the hole's by a clear step (`offset_body`) and the
//! bodies are intersected as they then are, which the boolean can do: narrowed, that gives the
//! overlap away from the walls (the core). In the band the move swept, what the two walls
//! share is a crescent in every section, whose area follows from the two circles alone. The
//! band is assumed to hold nothing but the two walls; that is checked by intersecting with the
//! pin widened by half the step and by the whole step, whose volumes must come out as the core
//! plus the crescents those wider circles give. Where they don't (a cross hole through the
//! band, a hole that is not round all the way, a pin end inside the hole), the overlap is not
//! answered here and the caller takes the general boolean.
use solidcraft_geom::{Aabb3, Vec3};
use crate::body::Body;
use crate::{BoolOp, FaceSurface};
/// The overlap of two bodies with a pin of one in a hole of the other, worked out as above.
#[derive(Clone, Debug)]
pub struct CoaxialOverlap {
pub volume: f64,
/// Approximate: the core's area plus the crescents' walls and ends.
pub area: f64,
pub centroid: Vec3,
pub bbox: Aabb3,
/// The largest offset between a pin's axis and its hole's, and the length they share.
pub offset: f64,
pub length: f64,
}
/// Largest axis offset plus radius difference handled, as a share of the radius.
const NEAR: f64 = 0.01;
/// The step the pin's wall is moved by, as a share of its radius.
const STEP: f64 = 0.1;
/// The faces of one body on one cylinder, the side its material is on and their extent along
/// the axis.
#[derive(Clone, Debug)]
struct Wall {
faces: Vec<usize>,
origin: Vec3,
axis: Vec3,
radius: f64,
/// Material inside the cylinder (a pin), not outside it (a hole).
boss: bool,
/// Intervals along `axis` from `origin`, one per face.
spans: Vec<(f64, f64)>,
}
/// A pin of one body (`pin_in_a`) in a hole of the other, and the length they share.
#[derive(Clone, Debug)]
struct Pair {
pin: Wall,
pin_in_a: bool,
hole: Wall,
/// The hole's axis seen from the pin's, across it, and how far.
toward: Vec3,
offset: f64,
/// Where they share length along the pin's axis.
shared: Vec<(f64, f64)>,
step: f64,
}
/// The overlap of `a` and `b` when a pin of one sits in a hole of the other on nearly the same
/// cylinder (axes parallel, offset plus radius difference within 1 % of the radius, but not on
/// the same surface), worked out as the module says. `None` when there is no such pin, or the
/// bodies hold more than the pin and the hole where the work assumes they don't: the caller
/// then takes the general boolean.
pub fn coaxial_overlap(a: &Body, b: &Body) -> Option<CoaxialOverlap> {
if a.is_mesh() || b.is_mesh() {
return None;
}
crate::guard("near-coaxial overlap", || Ok(overlap(a, b))).ok().flatten()
}
fn overlap(a: &Body, b: &Body) -> Option<CoaxialOverlap> {
let size = a.size().max(b.size());
if !(size.is_finite() && size > 0.0) {
return None;
}
// The same surface as the kernel takes it elsewhere: walls that close are one wall.
let same = (size * 1e-7).max(1e-9) * 10.0;
// Meshed only when two cylinders are close enough to be asked about.
let (ca, cb) = (cylinders(a), cylinders(b));
let close = |x: &Wall, y: &Wall| x.axis.dot(y.axis).abs() > 1.0 - 1e-9 && near(x, y, same).is_some();
if !ca.iter().any(|x| cb.iter().any(|y| close(x, y))) {
return None;
}
let (wa, wb) = (walls(a, ca, same)?, walls(b, cb, same)?);
let mut pairs = Vec::new();
for x in &wa {
for y in &wb {
if x.axis.dot(y.axis).abs() <= 1.0 - 1e-9 {
continue;
}
let (pin, hole, pin_in_a) = match (x.boss, y.boss) {
(true, false) => (x, y, true),
(false, true) => (y, x, false),
// Two pins or two holes on nearly one cylinder: not this case.
_ if near(x, y, same).is_some() => return None,
_ => continue,
};
let Some((toward, offset)) = near(pin, hole, same) else { continue };
let shared = intersect(&union(&pin.spans), &union(&project(hole, pin)));
if total(&shared) <= same {
continue;
}
let step = (pin.radius * STEP).min(size * 0.02);
pairs.push(Pair { pin: pin.clone(), pin_in_a, hole: hole.clone(), toward, offset, shared, step });
}
}
if pairs.is_empty() {
return None;
}
// A pin's faces move together: one step for each of them (the pin's walls do not repeat
// across pairs unless one pin is near two holes, which the check below then answers).
let shared_at = |k: f64| -> Option<(f64, Option<Body>)> {
let moved = |body: &Body, in_a: bool| -> Option<Body> {
let mine: Vec<&Pair> = pairs.iter().filter(|p| p.pin_in_a == in_a).collect();
if mine.is_empty() {
return Some(body.clone());
}
let shift = |i: usize, _| mine.iter().find(|p| p.pin.faces.contains(&i)).map(|p| p.step * k).unwrap_or(0.0);
let m = crate::offset::offset_body(body, shift).ok()?;
m.validity().is_empty().then_some(m)
};
let (ma, mb) = (moved(a, true)?, moved(b, false)?);
let (r, report) = crate::capture_boolean_diagnostics(|| crate::ops::boolean(&ma, &mb, BoolOp::Intersect));
if report.unverified().is_some() || report.warning().is_some() {
return None;
}
match r.ok()? {
None => Some((0.0, None)),
Some(body) => Some((crate::measure(&body).ok()?.volume, Some(body))),
}
};
// The crescents' volume with every pin grown by `k` steps.
let crescents =
|k: f64| -> f64 { pairs.iter().map(|p| total(&p.shared) * crescent(p.pin.radius + p.step * k, p.hole.radius, p.offset).area).sum() };
let (core, core_body) = shared_at(-1.0)?;
for k in [0.5, 1.0] {
let (v, _) = shared_at(k)?;
let gain = crescents(k) - crescents(-1.0);
if !((v - core - gain).abs() <= 0.02 * gain + 1e-4 * v.abs() + 1e-12 * size.powi(3)) {
return None;
}
}
// The band held the two walls only: the overlap is the core plus the crescents.
let mut volume = core;
let mut moment = Vec3::ZERO;
let mut area = 0.0;
let mut bbox = Aabb3::EMPTY;
if let Some(body) = &core_body
&& let Ok(m) = crate::measure(body)
{
moment = m.centroid * m.volume;
area = m.area;
bbox = m.bbox;
volume = m.volume;
}
for p in &pairs {
let c = crescent(p.pin.radius, p.hole.radius, p.offset);
let length = total(&p.shared);
// Less what the narrowed pin already reached (nothing, unless the hole is the smaller).
let v = length * c.area - length * crescent(p.pin.radius - p.step, p.hole.radius, p.offset).area;
let mid = p.shared.iter().map(|(s, t)| (s + t) * 0.5 * (t - s)).sum::<f64>() / length;
let side = p.toward.cross(p.pin.axis);
moment += (p.pin.origin + p.pin.axis * mid + p.toward * c.centroid) * v;
volume += v;
area += p.shared.len() as f64 * 2.0 * c.area + length * c.perimeter;
for (s, t) in &p.shared {
for q in crescent_outline(p.pin.radius, p.hole.radius, p.offset) {
let at = p.pin.origin + p.toward * q.0 + side * q.1;
bbox.add(at + p.pin.axis * *s);
bbox.add(at + p.pin.axis * *t);
}
}
}
let centroid = if volume > 0.0 { moment / volume } else { bbox.center() };
let offset = pairs.iter().map(|p| p.offset).fold(0.0, f64::max);
let length = pairs.iter().map(|p| total(&p.shared)).sum();
Some(CoaxialOverlap { volume: volume.max(0.0), area, centroid, bbox, offset, length })
}
/// The pin's offset to the hole, when the two are nearly on one cylinder but not on the same
/// surface: the unit direction from the pin's axis to the hole's (any, for one axis) and the
/// distance.
fn near(pin: &Wall, hole: &Wall, same: f64) -> Option<(Vec3, f64)> {
let d = hole.origin - pin.origin;
let across = d - pin.axis * d.dot(pin.axis);
let offset = across.len();
let gap = (pin.radius - hole.radius).abs();
if offset <= same && gap <= same {
return None;
}
if !(offset + gap <= NEAR * pin.radius.min(hole.radius)) {
return None;
}
let toward = across.normalized().unwrap_or_else(|| pin.axis.any_perp());
Some((toward, offset))
}
/// The body's cylindrical faces, each as a wall of its own with no side or extent yet.
fn cylinders(b: &Body) -> Vec<Wall> {
(0..b.face_count())
.filter_map(|i| match crate::face_surface(b, i)? {
FaceSurface::Cylinder { origin, axis, radius } if origin.is_finite() && radius.is_finite() && radius > 0.0 => {
Some(Wall { faces: vec![i], origin, axis: axis.normalized()?, radius, boss: false, spans: Vec::new() })
}
_ => None,
})
.collect()
}
/// The cylindrical walls of a body from its `cylinders`: its faces on each cylinder, which side
/// its material is on and where they run along the axis. `None` when the body can't be meshed.
fn walls(b: &Body, cylinders: Vec<Wall>, same: f64) -> Option<Vec<Wall>> {
let mut out: Vec<Wall> = Vec::new();
let mesh = b.tessellate_with((b.size() * 1e-3).max(1e-4), true).ok()?;
for Wall { faces, origin, axis, radius, .. } in cylinders {
let Some(&face) = faces.first() else { continue };
// Which way the face looks: out of the axis for a pin, toward it for a hole.
let (mut facing, mut lo, mut hi) = (0.0, f64::INFINITY, f64::NEG_INFINITY);
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
if *f as usize != face {
continue;
}
let Some([p, q, r]) = mesh.tri(t) else { continue };
let c = (p + q + r) / 3.0;
let radial = (c - origin) - axis * (c - origin).dot(axis);
facing += (q - p).cross(r - p).dot(radial);
for x in [p, q, r] {
let s = (x - origin).dot(axis);
lo = lo.min(s);
hi = hi.max(s);
}
}
if !(lo < hi) || facing == 0.0 {
continue;
}
let boss = facing > 0.0;
let wall = Wall { faces: vec![face], origin, axis, radius, boss, spans: vec![(lo, hi)] };
match out.iter_mut().find(|w| w.boss == boss && on_one_cylinder(w, &wall, same)) {
Some(w) => {
let s = (origin - w.origin).dot(w.axis) * if axis.dot(w.axis) < 0.0 { -1.0 } else { 1.0 };
let (a, b) = if axis.dot(w.axis) < 0.0 { (-hi, -lo) } else { (lo, hi) };
w.faces.push(face);
w.spans.push((a + s, b + s));
}
None => out.push(wall),
}
}
Some(out)
}
fn on_one_cylinder(x: &Wall, y: &Wall, same: f64) -> bool {
let d = y.origin - x.origin;
x.axis.dot(y.axis).abs() > 1.0 - 1e-9 && (x.radius - y.radius).abs() < same && (d - x.axis * d.dot(x.axis)).len() < same
}
/// `wall`'s spans along `onto`'s axis.
fn project(wall: &Wall, onto: &Wall) -> Vec<(f64, f64)> {
let flip = wall.axis.dot(onto.axis) < 0.0;
let s = (wall.origin - onto.origin).dot(onto.axis);
wall.spans.iter().map(|&(lo, hi)| if flip { (s - hi, s - lo) } else { (s + lo, s + hi) }).collect()
}
/// Sorted, disjoint intervals covering `spans`.
fn union(spans: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut v: Vec<(f64, f64)> = spans.iter().copied().filter(|(a, b)| a < b).collect();
v.sort_by(|x, y| x.0.total_cmp(&y.0));
let mut out: Vec<(f64, f64)> = Vec::with_capacity(v.len());
for (a, b) in v {
match out.last_mut() {
Some(last) if a <= last.1 => last.1 = last.1.max(b),
_ => out.push((a, b)),
}
}
out
}
fn intersect(x: &[(f64, f64)], y: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut out = Vec::new();
for &(a, b) in x {
for &(c, d) in y {
let (lo, hi) = (a.max(c), b.min(d));
if lo < hi {
out.push((lo, hi));
}
}
}
out
}
fn total(spans: &[(f64, f64)]) -> f64 {
spans.iter().map(|(a, b)| b - a).sum()
}
/// The part of a disc of radius `pin` outside a disc of radius `hole` whose centre is `offset`
/// away along +x: its area, its centroid's x (y is 0) and its perimeter.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Crescent {
pub(crate) area: f64,
pub(crate) centroid: f64,
pub(crate) perimeter: f64,
}
pub(crate) fn crescent(pin: f64, hole: f64, offset: f64) -> Crescent {
use std::f64::consts::PI;
let (r, q, e) = (pin.max(0.0), hole.max(0.0), offset.abs());
let disc = PI * r * r;
if e >= r + q {
return Crescent { area: disc, centroid: 0.0, perimeter: 2.0 * PI * r };
}
if e <= (r - q).abs() {
return if q >= r {
Crescent { area: 0.0, centroid: 0.0, perimeter: 0.0 }
} else {
let area = disc - PI * q * q;
Crescent { area, centroid: -PI * q * q * e / area, perimeter: 2.0 * PI * (r + q) }
};
}
// Half-angles of the lens's two segments, seen from each centre.
let alpha = ((e * e + r * r - q * q) / (2.0 * e * r)).clamp(-1.0, 1.0).acos();
let beta = ((e * e + q * q - r * r) / (2.0 * e * q)).clamp(-1.0, 1.0).acos();
let segment = |radius: f64, half: f64| {
let area = radius * radius * (half - half.sin() * half.cos());
let w = 2.0 * half - (2.0 * half).sin();
let from_centre = if w > 1e-12 { 4.0 * radius * half.sin().powi(3) / (3.0 * w) } else { radius };
(area, from_centre)
};
let (sa, da) = segment(r, alpha);
let (sb, db) = segment(q, beta);
let lens = sa + sb;
let area = (disc - lens).max(0.0);
let moment = -(sa * da + sb * (e - db));
let centroid = if area > 0.0 { moment / area } else { 0.0 };
Crescent { area, centroid, perimeter: r * (2.0 * PI - 2.0 * alpha) + q * 2.0 * beta }
}
/// Points around the crescent's outline (x toward the hole's centre, y across), for its box.
fn crescent_outline(pin: f64, hole: f64, offset: f64) -> Vec<(f64, f64)> {
let (r, q, e) = (pin, hole, offset);
let alpha = if e > 0.0 { ((e * e + r * r - q * q) / (2.0 * e * r)).clamp(-1.0, 1.0).acos() } else { 0.0 };
let n = 32;
(0..=n)
.map(|i| {
let t = alpha + (2.0 * std::f64::consts::PI - 2.0 * alpha) * f64::from(i) / f64::from(n);
(r * t.cos(), r * t.sin())
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// The crescent against the textbook lens of two equal circles, and its limits.
#[test]
fn crescent_matches_the_lens_formula() {
let (r, e) = (3.0f64, 0.01f64);
let lens = 2.0 * r * r * (e / (2.0 * r)).acos() - e / 2.0 * (4.0 * r * r - e * e).sqrt();
let c = crescent(r, r, e);
assert!((c.area - (std::f64::consts::PI * r * r - lens)).abs() < 1e-12, "{c:?}");
// Thin: twice the radius times the offset, its centroid π/4 of the radius away from
// the hole's centre, its outline nearly two half circles.
assert!((c.area - 2.0 * r * e).abs() < 1e-6, "{c:?}");
assert!((c.centroid + std::f64::consts::FRAC_PI_4 * r).abs() < 1e-2, "{c:?}");
assert!((c.perimeter - 2.0 * std::f64::consts::PI * r).abs() < 1e-3, "{c:?}");
assert_eq!(crescent(r, r, 0.0).area, 0.0);
// A pin with clearance in its hole shares nothing; a pin wider than its hole a ring.
assert_eq!(crescent(2.99, 3.0, 0.005).area, 0.0);
let ring = crescent(3.01, 3.0, 0.0);
assert!((ring.area - std::f64::consts::PI * (3.01f64.powi(2) - 9.0)).abs() < 1e-12, "{ring:?}");
}
/// A 20 x 20 x 5 plate with a hole of radius 3 through it, and a pin of radius `pin` through
/// the hole, `offset` off its axis along X.
fn plate_and_pin(pin: f64, offset: f64) -> (Body, Body) {
let block = crate::box_solid(Vec3::new(-10.0, -10.0, 0.0), Vec3::new(10.0, 10.0, 5.0)).unwrap();
let drill = crate::cylinder(Vec3::new(0.0, 0.0, -2.0), Vec3::Z, 3.0, 9.0).unwrap();
let plate = crate::boolean(&block, &drill, BoolOp::Cut).unwrap().unwrap();
(plate, crate::cylinder(Vec3::new(offset, 0.0, -2.0), Vec3::Z, pin, 9.0).unwrap())
}
/// The pin shares a crescent with the plate over the plate's thickness, in either order.
#[test]
fn a_pin_off_its_hole_shares_a_crescent() {
let (plate, pin) = plate_and_pin(3.0, 0.01);
let want = 5.0 * crescent(3.0, 3.0, 0.01).area;
for (x, y) in [(&plate, &pin), (&pin, &plate)] {
let o = coaxial_overlap(x, y).unwrap();
assert!((o.volume - want).abs() < 1e-6 * want, "{o:?} {want}");
assert!((o.offset - 0.01).abs() < 1e-9 && (o.length - 5.0).abs() < 1e-9, "{o:?}");
// The crescent is on the pin's side away from the hole's axis (+X).
assert!(o.centroid.x > 2.0 && o.bbox.min.z > -1e-9 && o.bbox.max.z < 5.0 + 1e-9, "{o:?}");
}
}
/// A pin with clearance all round, though off the axis, shares nothing.
#[test]
fn a_pin_with_clearance_shares_nothing() {
let (plate, pin) = plate_and_pin(2.995, 0.003);
let o = coaxial_overlap(&plate, &pin).unwrap();
assert!(o.volume < 1e-9, "{o:?}");
}
/// A slot through the hole's wall where the crescent would be: the band holds more than the
/// two walls, the widened pin says so, and the pair is left to the boolean.
#[test]
fn a_slot_through_the_band_is_left_to_the_boolean() {
let (plate, pin) = plate_and_pin(3.0, 0.01);
let slot = crate::box_solid(Vec3::new(2.5, -1.0, -1.0), Vec3::new(4.0, 1.0, 6.0)).unwrap();
let slotted = crate::boolean(&plate, &slot, BoolOp::Cut).unwrap().unwrap();
assert!(coaxial_overlap(&slotted, &pin).is_none());
// Pins on the very axis, or far off it, aren't asked about.
assert!(coaxial_overlap(&plate, &plate_and_pin(3.0, 0.0).1).is_none());
assert!(coaxial_overlap(&plate, &plate_and_pin(3.0, 0.5).1).is_none());
}
}
+2
View File
@@ -28,6 +28,7 @@ mod boundary;
mod bridge;
mod build;
mod clearance;
mod coaxial_overlap;
mod continuity;
mod coplanar;
mod corner_g2;
@@ -139,6 +140,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 coaxial_overlap::{CoaxialOverlap, coaxial_overlap};
pub use continuity::{
ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamVerdict, SeamWorst, pick_edges, seam_diagnostics,
};