25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
247 lines
12 KiB
Markdown
247 lines
12 KiB
Markdown
# 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.
|