# Cadquery Interference Check

> Audit a CadQuery assembly for interferences (unintended overlaps) and unintended gaps at every mating interface. Use when the orchestrator (`cadquery-assembly`) finishes laying out parts, when the user says "check for clashes", "verify there are no interferences", "interference check", or before exporting a final STEP. Walks every part pair, computes pairwise intersections, reports clashes with volume and bounding box, and proposes fixes.

- Skill: `leoai-org/cadquery-interference-check` (Agent Skill)
- Install (CLI): `npx skillmds@latest add leoai-org/cadquery-interference-check`
- Raw SKILL.md: https://api.skillmd.com/api/skills/leoai-org/cadquery-interference-check/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: leoai-org (https://skillmd.com/u/leoai-org)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/leoai-org/cadquery-interference-check

---


# CadQuery interference check

## Purpose

Catch **unintended solid overlap** and **unintended gaps** before STEP export. Run after functional geometry (before detailing) and again after DFM and full assembly integration.

## Audit script (write once per project)

Create `<project>/tools/interference_audit.py`:

```python
"""Pairwise interference audit for assembly.py. Run: python tools/interference_audit.py"""
import runpy
import cadquery as cq
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ns = runpy.run_path(str(ROOT / "assembly.py"))
assy = ns["result"]

def iter_parts(node, parent_loc=cq.Location()):
    loc = parent_loc
    if getattr(node, "loc", None) is not None:
        loc = parent_loc * node.loc
    name = getattr(node, "name", None) or "?"
    if getattr(node, "obj", None) is not None:
        obj = node.obj
        shape = obj.val() if hasattr(obj, "val") else obj
        yield name, shape.moved(loc)
    for child in getattr(node, "children", []) or []:
        yield from iter_parts(child, loc)

def collect(assy):
  if isinstance(assy, cq.Assembly):
    return list(iter_parts(assy))
  return [("result", assy.val())]

parts = collect(assy)
VOL_EPS = 1e-1  # mm³ — ignore numerical noise below this

clashes = []
for i, (na, sa) in enumerate(parts):
    for nb, sb in parts[i + 1 :]:
        try:
            inter = sa.intersect(sb)
            vol = inter.Volume() if inter is not None else 0.0
        except Exception as e:
            clashes.append({"a": na, "b": nb, "error": str(e)})
            continue
        if vol > VOL_EPS:
            bb = inter.BoundingBox()
            clashes.append({
                "a": na, "b": nb,
                "volume_mm3": round(vol, 3),
                "bbox": (bb.xmin, bb.ymin, bb.zmin, bb.xmax, bb.ymax, bb.zmax),
            })

print(f"Pairs checked: {len(parts) * (len(parts) - 1) // 2}")
print(f"Clashes above {VOL_EPS} mm³: {len(clashes)}")
for c in clashes:
    print(c)
```

For **local** checks during Phase B, temporarily `runpy.run_path` a scratch script that builds only the new part + neighbor envelopes — same pairwise logic.

Run:

```bash
cd <project> && python tools/interference_audit.py
```

## Classify each clash

| Type | Expected? | Action |
|------|-----------|--------|
| Press-fit spigot ↔ socket | Often yes | Verify volume matches comment; else resize spigot in `common.py` |
| Weld penetration | Small yes | Document |
| Flush housing faces | **No** | Fix `loc=` or use `constrain().solve()` |
| Rib ↔ neighbor wall | **No** | Trim rib in detailing or relieve wall |
| Fastener shank in clearance hole | No (hole should be open) | DFM clearance diameter |

**Never** fix clashes by shrinking a functional mate dimension without updating `common.py` and the partner part.

## Gap check (flush mates)

From `common.py` `# DECOMPOSITION`, for each **flush** or **butt** mate:

1. Identify the two faces that should touch (e.g. `lower_housing` top @ `BASE_TOP_Z`, `upper_housing` bottom).
2. Compare bounding-box faces or use distance between shapes along the mate normal.
3. Gap > 0.05 mm on a flush mate → fix placement in sub-assembly `loc=` or constraints.

Document intentional gaps (snap clearance, thermal gap) in `common.py` as `MATE_GAP_*`.

## Pair pruning (performance)

For assemblies with >30 solids, still check all pairs for export gate; during development, prioritize:

- New part vs each declared mating partner
- Siblings in the same sub-assembly
- Skip pairs > 50 mm apart (bbox center distance) unless mates declare otherwise

## Report format

```text
INTERFERENCE AUDIT — <project>
Pairs checked: N
Unintended clashes: <count>
  - part_a ↔ part_b: <vol> mm³ bbox (...)
Intentional overlaps: <count>
  - part_a ↔ part_b: <vol> mm³ [press-fit per common MATE_*]
Unintended gaps: <count>
  - part_a ↔ part_b: <d> mm (expected flush @ Z=...)
```

Hand back to orchestrator only when **unintended clashes = 0** and **unintended gaps = 0** for declared flush mates.

## Gotchas

- Compose **parent × child** `loc` when walking nested `cq.Assembly` trees.
- `intersect` may throw on near-tangent contact — record as `error` and inspect in render view.
- Duplicate instance names make reports ambiguous — enforce unique `name=` in `assy.add()`.
- Envelope-phase overlaps are OK only if documented as intentional interference fits; otherwise fix in `high_level_assembly.py` before Phase B.

## Fix loop

1. Identify owning part (usually the later-added feature).
2. Edit `parts/<name>.py` in the correct pass (functional vs detailing vs DFM).
3. Re-exec + `cadquery-render` + re-run audit.

