fix(kernel): a small overlap is not taken for bodies apart or inside (#484)

When sample points say two bodies are apart or one is inside the other, the facetings are now checked for surfaces passing through each other; if they do, the boolean is computed instead of shortcut. If it still can't be intersected, the sampled answer is kept as a flagged, unverified result, and Interference lists such pairs as "could not be checked" with the reason. Fixes #442 and #443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Scott BowdenandClaude Opus 5.5 authored and GitHub committed 2026-10-13 00:13:16 -04:00
1 parent 340ac0313a
commit 3159fc0dd0
8 files changed
+406 -25

No files matched your search

+25 -5
View File
@@ -37,7 +37,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("joint.list", "List Joints", list).noundo().params("→ joints (type, occurrences, values, limits), origins, links, degrees of freedom, conflicts"),
CommandSpec::new("joint.solve", "Solve Joints", solve_cmd).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) → pairs that overlap with the overlap volume, area, bounding box and centre",
"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",
),
];
@@ -531,7 +531,7 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
if bodies.len() > 200 {
return Err(bad(cmd, "too many bodies (200 at most)"));
}
let mut hits = Vec::new();
let (mut hits, mut unchecked) = (Vec::new(), Vec::new());
for (i, a) in bodies.iter().enumerate() {
for b in bodies.iter().skip(i + 1) {
let (ba, bb) = (a.mesh().bounds(), b.mesh().bounds());
@@ -544,14 +544,34 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
if apart {
continue;
}
let Ok(Some(o)) = solidcraft_kernel::boolean(&a.body, &b.body, solidcraft_kernel::BoolOp::Intersect) else { continue };
let Ok(m) = solidcraft_kernel::measure(&o) else { 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));
let mut skip = |reason: String| unchecked.push(json!({"a": a.name, "b": b.name, "reason": reason}));
let shared = match shared {
Ok(o) => o,
Err(e) => {
skip(e.to_string());
continue;
}
};
if let Some(reason) = report.unverified() {
skip(reason);
}
let Some(o) = shared else { continue };
let m = match solidcraft_kernel::measure(&o) {
Ok(m) => m,
Err(e) => {
skip(e.to_string());
continue;
}
};
if m.volume > 1e-9 * a.body.size().max(1.0).powi(3) {
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}}));
}
}
}
Ok(json!({"interferences": hits, "count": hits.len(), "checked": bodies.len()}))
Ok(json!({"interferences": hits, "count": hits.len(), "checked": bodies.len(), "unchecked": unchecked}))
}
#[cfg(test)]