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}");
}
}