WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
# Interference checking, and what it took to make it usable
|
||||
|
||||
Written down because every wrong version of this check looked right, and two
|
||||
of them reported "no interference" while being incapable of finding any.
|
||||
|
||||
The code is `check_member_interference` in
|
||||
`WillTools.extension/lib/vibe/ticketchecks.py`, with the geometry in
|
||||
`ticketqc.py`. This is the reasoning, not the API.
|
||||
|
||||
## The criterion
|
||||
|
||||
Two bodies interfere when the material they share is **more than 1/8" thick**.
|
||||
|
||||
For the *filtering* stages, which work on boxes, that is read as "the boxes
|
||||
overlap by more than the tolerance on every axis" — the same statement a
|
||||
detailer makes as "more than 1/8" of interference shows in each of the three
|
||||
views". A view is a dropped coordinate, so each view shows two axes;
|
||||
"overlaps in all three views" expands to `(0∧2) ∧ (0∧1) ∧ (1∧2)`, which is
|
||||
`0 ∧ 1 ∧ 2`. There is nothing to gain by writing it as three 2D tests; the
|
||||
gain is in not forming the pair at all.
|
||||
|
||||
For the *verdict*, boxes are not good enough, and neither is volume.
|
||||
|
||||
**Volume is a trap.** A bar grazing the face of a plate over a wide area has
|
||||
volume while being thousandths deep; one bar catching the corner of another
|
||||
has depth with almost no volume.
|
||||
|
||||
**A bounding box of the intersection is a worse trap**, and this one survived
|
||||
several rounds of looking right. When two round bars graze, the material they
|
||||
share is a *curved lens*, and a lens is no better described by its box than a
|
||||
bent bar is — measured on a real piece, these intersections fill **10 to 15
|
||||
percent** of their own boxes. Boxing them reported **0.51" of interference for
|
||||
a sliver of 0.0166 cubic inches**: a shape a thousandth of an inch thick would
|
||||
have measured exactly the same. The box was not even mis-oriented; the
|
||||
intersection's own box was already axis-aligned. It is simply the wrong tool
|
||||
for a curved shape, twice over.
|
||||
|
||||
So the verdict is a **mean thickness**. For a thin body, `Volume = Area × t`
|
||||
and `SurfaceArea = 2 × Area`, hence
|
||||
|
||||
```
|
||||
t = 2 × Volume / SurfaceArea
|
||||
```
|
||||
|
||||
which does not care how the sliver is oriented or how curved it is. On the
|
||||
piece where boxes claimed 44 interferences, every one measured **0.059″ to
|
||||
0.066″** thick — under half the tolerance, and the count went to zero.
|
||||
|
||||
Known bias: mean thickness **under-reports a chunky intersection**, giving
|
||||
`side/3` for a cube and `diameter/3` for two bars crossing dead centre. It
|
||||
errs towards calling an interference shallower than it is, so the tolerance it
|
||||
is compared against should be set knowing that.
|
||||
|
||||
## The pipeline, and why each stage exists
|
||||
|
||||
Revit's own assembly clash tests every pair and solves geometry for all of it.
|
||||
Five stages instead, each one cheap enough to earn the one after it:
|
||||
|
||||
| stage | rejects | cost |
|
||||
| --- | --- | --- |
|
||||
| sweep and prune along the length | pairs that never share a station | one sort |
|
||||
| **top in form**, then front, then right | pairs clear in the drawing plane | six comparisons |
|
||||
| one box per **solid** | air inside multi-part embeds | solves geometry |
|
||||
| boolean **intersection** | air inside bent bars | one boolean |
|
||||
| exemptions | what is deliberate | table lookup |
|
||||
|
||||
Measured on three real pieces:
|
||||
|
||||
```
|
||||
all pairs -> sweep -> top in form -> front -> solid boxes -> real
|
||||
12090 2656 1120 644 320 27
|
||||
8385 2246 980 555 187 47
|
||||
6670 1839 956 506 150 10
|
||||
```
|
||||
|
||||
Under a second per piece.
|
||||
|
||||
### The first view is fixed, not chosen
|
||||
|
||||
Top in form — the full 299" × 143" face, where members are spread out and most
|
||||
pairs separate at once. The other two views look along a long axis at an 8"
|
||||
thickness, where nearly everything overlaps nearly everything.
|
||||
|
||||
Which view is worth looking at first is a fact about how a wall panel is
|
||||
built. It does not change piece by piece, so it is not inferred from the data.
|
||||
|
||||
Read off the model rather than assumed: the view named `Elevation Top` on a
|
||||
real ticket looks along the panel's local thickness axis, dot product ±1.000
|
||||
on every piece checked. The sign varies because pieces are mirrored; the axis
|
||||
does not.
|
||||
|
||||
**The third view never rejects anything and cannot.** Top-in-form establishes
|
||||
length and width, front adds thickness, and right asks only about thickness
|
||||
and width — both already known. It passed 755 of 755, 596 of 596, 537 of 537.
|
||||
Kept because the code should read like the three drawings it describes; not
|
||||
load-bearing, and nobody should think it is.
|
||||
|
||||
## Three ways to build this wrong
|
||||
|
||||
### 1. Sampling edge vertices for a bounding box
|
||||
|
||||
A cylinder's edges are **circles**, and a circle's two "end" points are the
|
||||
same point on the seam. Sampling them collapses a strand's box to a
|
||||
zero-thickness sliver in two axes — and a sliver overlaps nothing.
|
||||
|
||||
The first version of this check reported **no interference anywhere**, on
|
||||
every piece, and looked entirely healthy doing it. Use
|
||||
`Solid.GetBoundingBox()`, and remember its box is stated in the solid's own
|
||||
coordinates, so its `Transform` applies before the panel's inverse.
|
||||
|
||||
### 2. One box per element
|
||||
|
||||
An embed drawn as a plate at one end and anchor bars at the other is **nine
|
||||
solids**, and one box around all of them claims the air between as occupied.
|
||||
Measured fill: **3%** steel by box volume. Per-solid boxes cut the reported
|
||||
count from 265 to 223 before any boolean ran.
|
||||
|
||||
### 3. Trusting boxes at all, once bars are bent
|
||||
|
||||
A box is the product of three intervals, so a **bent bar's box is the whole U
|
||||
including the hole**, and anything passing cleanly through that hole reads as
|
||||
an interference. How much of each box is actually steel, measured per family
|
||||
on one piece:
|
||||
|
||||
```
|
||||
3.05% a 9-solid embed 6.06% a bent bar, 1 solid
|
||||
3.2% a 3-solid embed 10.16% a bent bar, 1 solid
|
||||
12.85% a bent bar, 1 solid
|
||||
--------------------------------------------------------------------
|
||||
46.32% a straight bar 50.16% a strand
|
||||
```
|
||||
|
||||
The straight bar and the strand are close to what a cylinder in its own box
|
||||
should be. Everything above them is mostly air — and one of those bent bars
|
||||
is a 0.63" bar occupying a 13.63" × 6.06" box.
|
||||
|
||||
Boxes claimed 265 interferences on that piece; **86 were real**. Two thirds
|
||||
was air. The boolean pass that establishes this costs about 0.1 s.
|
||||
|
||||
This also produced a wrong conclusion that stood for a while: transverse bars
|
||||
were said to cross every bottom-row strand as a drafting convention, 70 such
|
||||
pairs on one piece. **Not one was real** — the bars and the strands miss each
|
||||
other through the thickness.
|
||||
|
||||
Boxing the **intersection** is sound where boxing the inputs was not. A bent
|
||||
bar's own box is mostly the hole; the intersection is the small piece of steel
|
||||
actually shared, and its box describes that piece fairly.
|
||||
|
||||
## What counts as deliberate
|
||||
|
||||
Left alone, the check reports several hundred per piece and nobody reads it.
|
||||
Everything below is configuration, not code, and lives in the knowledge layers.
|
||||
|
||||
| | where | why |
|
||||
| --- | --- | --- |
|
||||
| `same_family_ok_categories` | plant | Some hardware is placed as several overlapping instances of one family **on purpose** — the intersection is what makes Revit pick them up together when the assembly is created. |
|
||||
| `within_category_only` | plant | Reinforcing meeting an embed, or a bar running into a ledge, is how the piece goes together. Two things of one **kind** in one place is the defect. Blunt, and it does hide real cross-category problems. |
|
||||
| `insert_bars` | plant | A lifting insert is set out with a bar to either side. Those belong to the **insert**, not the reinforcing schedule — the plant places them and may move them freely, so there is nothing to hold them to. |
|
||||
| `component_groups` | either | Hardware that arrives as several families but is **one part** — see below. |
|
||||
| `exempt_families` | job | Hardware whose modelled solid sits inside other things and always will. |
|
||||
| `ignore_family_pairs` | either | A named pair, one at a time. The last resort, not the first. |
|
||||
|
||||
### Component groups
|
||||
|
||||
A connection is not one family. A top-of-panel-to-column connection is a
|
||||
plate, its rebar, its sleeve and a good deal else; an embed and the bar welded
|
||||
to it are one part installed together. They are modelled touching **because
|
||||
they are one thing**.
|
||||
|
||||
Revit records none of this. Checked on a real model: every component's family
|
||||
name is just its own part number, and no parameter, nested family or shared
|
||||
parameter points at the assembly it belongs to. So the ownership has to be
|
||||
stated, and `component_groups` is where.
|
||||
|
||||
Two strengths, and reaching for the wrong one goes blind:
|
||||
|
||||
- `interferes: within_group` — the components may overlap **each other**
|
||||
freely, but one of them hitting something **outside** the group is still
|
||||
reported. Reach for this first. It explains the overlaps that are by design
|
||||
without hiding the ones that are not, and on a real piece it kept a
|
||||
`P76 × P106` finding while dismissing `P76 × its own bar`.
|
||||
- `interferes: false` — nothing in the group is reported at all. For hardware
|
||||
set out as a unit that simply is not the detailer's to check.
|
||||
|
||||
### The insert-bar rule: found by symmetry, not by distance
|
||||
|
||||
An insert's own bars cannot be told apart from ordinary reinforcing by
|
||||
anything local. Same family, same size, same direction, same layer.
|
||||
|
||||
**Distance alone gets it backwards.** The first version of this rule exempted
|
||||
bars within 1.5" of a lifter, and on a real piece that exempted the ordinary
|
||||
#4s which happen to pass near one while leaving the actual lifter bars — #5s,
|
||||
sitting 4" away — fully checkable. It was wrong in both directions at once and
|
||||
the reported counts barely moved, so nothing gave it away.
|
||||
|
||||
What does distinguish them is **symmetry**. Reinforcing comes in layers
|
||||
through the thickness and the layers mirror each other, so an ordinary bar has
|
||||
a counterpart at the same station in another layer. An insert's bar does not.
|
||||
Measured on one piece:
|
||||
|
||||
```
|
||||
layer at 6.0" 20 x #4 at stations 2 4 14 32 50 68 86 104 122 140 ...
|
||||
layer at 2.5" 20 x #4 at exactly the same stations <- mirrored
|
||||
10 x #5 at 26 34 | 116 124 | 175 183 | 187 195 | 265 273
|
||||
<- unpaired
|
||||
lifter stations 30 | 120 | 179 | 191 | 269
|
||||
```
|
||||
|
||||
Five pairs, 8" apart, each centred **±4" on a lifter station**. Nothing else
|
||||
sets those ten apart.
|
||||
|
||||
So: a bar with no counterpart, within reach of an insert, belongs to it. A bar
|
||||
with no counterpart and **no** insert nearby is reported (`BAR-UNPAIRED`) —
|
||||
the pattern does not explain it and neither does a lifter.
|
||||
|
||||
**The mirror is never assumed to be at `thickness - depth`.** The rule looks
|
||||
for a counterpart in *some* other layer rather than at a computed depth, so a
|
||||
piece whose symmetry is broken — extra clear cover on one face, say — costs
|
||||
nothing and no layer pairing has to be worked out.
|
||||
|
||||
A measurement note that cost an afternoon: layer depths must be reported
|
||||
**raw**. Binning them to the nearest half inch turned 1.75 / 6.25 into
|
||||
2.0 / 6.5 and 2.25 / 5.75 into 2.5 / 6.0, which made a perfectly symmetric
|
||||
piece look like it had half an inch of extra cover on one face. The panel was
|
||||
symmetric to 0.000" all along, in every layer except the insert bars — which
|
||||
is what a wall panel should be, since the only thing that breaks the mirror is
|
||||
a lifter, and a lifter is not symmetric either.
|
||||
|
||||
## Open
|
||||
|
||||
- Every bar in `bar_families` is assumed to be layered reinforcing. Ledge bars
|
||||
are not, and they surface as `BAR-UNPAIRED` — correct as stated, but it is
|
||||
a category the rule does not really have.
|
||||
- If a lifter has no bars at all, nothing notices. Whether every face lifter
|
||||
must carry one either side is a *completeness* question, and no check asks
|
||||
it yet.
|
||||
- Exempting an insert's bars from **everything** is what the plant asked for,
|
||||
since it may move them freely — but it does suppress real geometry. On one
|
||||
piece it removed the deepest interference in the list, a #5 buried a full
|
||||
diameter into an embed.
|
||||
- Cross-category interference is not looked at when `within_category_only` is
|
||||
on, and some of it is certainly real.
|
||||
- AABBs remain a **conservative** approximation everywhere they survive into a
|
||||
result: they can invent an overlap but never hide one. That is the right
|
||||
direction for a filter and the wrong one for an exemption, which is why the
|
||||
insert-bar test refines geometry before measuring.
|
||||
Reference in New Issue
Block a user