Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
# The Revit bridge, and the two languages either side of it
|
||||
|
||||
Context for anyone asked to read the live model. Read this before writing a
|
||||
script that talks to Revit.
|
||||
|
||||
## What the bridge is
|
||||
|
||||
Revit runs a pyRevit routes server. `tools/rvt.py` posts a snippet of Python to
|
||||
it, that snippet executes **inside Revit's own process**, and whatever it leaves
|
||||
in a variable named `result` comes back as JSON.
|
||||
|
||||
PYTHONIOENCODING=utf-8 <python3.12> tools/rvt.py exec <script.py>
|
||||
<python3.12> tools/rvt.py status # which document is open
|
||||
<python3.12> tools/rvt.py exec -c "..." # one-liner
|
||||
|
||||
`python` is not on PATH. `<python3.12>` is the Python 3.12 install under the
|
||||
current user's `AppData\Local\Programs`; `CLAUDE.md` spells out the full path,
|
||||
and so does whoever hands you a task.
|
||||
|
||||
`PYTHONIOENCODING=utf-8` is not optional. Ticket text in this job contains
|
||||
`U+200E`, an invisible mark, and printing a result that carries it raises
|
||||
`UnicodeEncodeError` on a cp1252 console — the bridge succeeded and the client
|
||||
crashed on the way out.
|
||||
|
||||
## Two languages, and they are not the same one
|
||||
|
||||
**Inside Revit: IronPython 2.7.** No f-strings, no `pathlib`, no `dataclasses`,
|
||||
no `subprocess` worth using. `unicode` and `basestring` exist; `vibe.compat`
|
||||
exists for the differences. Write code you would have written in 2015.
|
||||
|
||||
**Outside: CPython 3.12.** `panellift/` and `tools/` are this side. The two
|
||||
halves never import each other; they communicate through a JSON job file.
|
||||
|
||||
## Rules that are not negotiable
|
||||
|
||||
- **Read only, unless you have been told otherwise in this specific task.** No
|
||||
`Transaction`, no `doc.Delete`, no parameter writes. If the script you are
|
||||
about to write would modify the document, do not run it.
|
||||
|
||||
Measured 2026-08-30: **no button and no check writes to the model.** Ticket
|
||||
QC, Panel Lift, Extract Design and Self Check all read the document and
|
||||
write only to disk. `helpers.transaction()` exists and nothing calls it;
|
||||
`startup.py` wraps bridge execs in a transaction, which is why a snippet run
|
||||
through the bridge *can* write. The tool does not modify models because
|
||||
nobody has made it, not because it cannot.
|
||||
|
||||
That is a property worth keeping deliberately rather than by accident. A
|
||||
read-only tool's worst failure is a wrong report, which the reader can
|
||||
disbelieve. Once it writes, its worst failure is wrong geometry in a model
|
||||
somebody builds from, discovered days later.
|
||||
|
||||
**The first feature with a real reason to write is reinforcing-suggestion**,
|
||||
and the engineer's call on 08-30 was to build it read-only first: find the
|
||||
closest donor and show it, leave the copying to a person. The reason is not
|
||||
timidity. Picking the right donor is the only hard part -- copying is
|
||||
mechanical -- and if both ship together, a bad result cannot be attributed:
|
||||
eighty-four elements in the wrong place look the same whether the donor was
|
||||
wrong or the transform was.
|
||||
- **Never use pyRevit's Reload.** It leaves the routes handler pointing at a
|
||||
dead scripting engine and the next request kills Revit outright — an
|
||||
unhandled exception on a non-main thread, which Revit cannot catch. Restart
|
||||
Revit instead. `rvt.py` refuses to talk to a bridge that has survived a
|
||||
reload, which is the only protection there is.
|
||||
- **Your script freezes Revit for exactly as long as it runs.** The bridge
|
||||
executes on Revit's API thread. There is no background about it: while your
|
||||
snippet is running, the person at the keyboard has a frozen application.
|
||||
Assume that cost before you write the loop, batch the work into small pieces,
|
||||
and ask before running anything long against a model somebody is using.
|
||||
|
||||
The expensive move is enumerating a view's contents. `FilteredElementCollector(doc, view.Id)`
|
||||
on a view nobody has opened **forces Revit to generate that view**, which is
|
||||
what "generating graphics" in the status bar means. Walking every placed view
|
||||
of twelve sheets froze a cloud model for five minutes. If all you need is
|
||||
which views are on a sheet, `GetAllPlacedViews()` gives you their names
|
||||
without touching their contents — reading the contents is the part that costs.
|
||||
|
||||
- **The second person to run something measures the cache, not the code.**
|
||||
Most of what a view or a schedule costs is paid once per session, by
|
||||
whoever asks for it first; everyone after that gets it for a hundredth of
|
||||
the price. Measured here on one design: the first ticket read took 47.2 s,
|
||||
the next two took 0.04 s and 0.05 s, and the reading half was all of it --
|
||||
the checking half was 0.19 s. A bigger design cost minutes on its first
|
||||
ticket and seconds on every one after.
|
||||
|
||||
So a benchmark that runs second proves nothing, and "I cannot reproduce
|
||||
your slow run" usually means "you warmed it up for me". Before believing a
|
||||
timing, ask whether anything touched the same views earlier in the session.
|
||||
Only a restart restores the cold state, and a restart is the user's to
|
||||
make -- ask, do not engineer a way around it.
|
||||
|
||||
This matters most for anything a person clicks. Their first click of the
|
||||
day pays the warm-up; every benchmark you ran pays nothing.
|
||||
|
||||
- **A client timeout is not a rollback.** The default timeout is 300 s. A
|
||||
request that times out on this side may still have executed and committed on
|
||||
the other. Re-probe the state before doing anything else, always.
|
||||
- **A bridge that does not answer is working, until the CPU says otherwise.**
|
||||
The default explanation for silence is that your snippet is still running,
|
||||
not that anything broke. The evidence is not on the bridge side, it is on
|
||||
the process:
|
||||
|
||||
Revit's CPU time still climbing -> it is working. Wait.
|
||||
CPU flat and silent for a while -> now consider other explanations.
|
||||
|
||||
Measured: a batch of 57 pieces was declared "a dead exec holding the API
|
||||
queue, only a restart will clear it". Nothing was dead. The same process
|
||||
went on to burn another hour of CPU and finished on its own, and the person
|
||||
who was asked to restart had simply slept instead. A restart would have
|
||||
thrown away every warm view and solved geometry in the session -- the most
|
||||
expensive thing this model has -- to fix a fault that did not exist.
|
||||
|
||||
**Never ask someone to restart Revit before checking whether its CPU is
|
||||
climbing.** And when a diagnosis arrives with the symptom, verify the
|
||||
diagnosis, not just the symptom.
|
||||
|
||||
- **A timeout does not stop the work.** The 300 s limit belongs to the client.
|
||||
Revit keeps executing your snippet to the end, still frozen. Re-sending the
|
||||
same script after a timeout starts a second run while the first is still
|
||||
going, and freezes the model again for its whole duration. Wait, then probe.
|
||||
|
||||
- **Check that the response is yours, and make that possible to check.**
|
||||
Result files can carry another request's payload into your output file. It
|
||||
has happened twice in one afternoon, and not from timeouts: **there is one
|
||||
routes server on one port, and anyone working on this model shares it.** Two
|
||||
people debugging at once trade results.
|
||||
|
||||
This is the worst failure mode on the bridge, because a foreign payload is
|
||||
well-formed JSON that answers a question you did not ask, and the wrong
|
||||
conclusion it produces looks exactly like a bug in your own code.
|
||||
|
||||
So put a signature key in every result and check it before reading anything
|
||||
else:
|
||||
|
||||
result = {'MYNAME_WHAT_THIS_IS': True, ...}
|
||||
|
||||
If the key is missing, the answer belongs to somebody else. Send it again;
|
||||
do not squint at it for clues.
|
||||
|
||||
## Units
|
||||
|
||||
Revit's internal unit is **decimal feet**. `XYZ()` takes feet. Every length,
|
||||
every bounding box, every location point is feet. Feeding an inch value into an
|
||||
`XYZ` is a 12x error, and the most expensive incident on this project was
|
||||
exactly that: copies landed 6,500 ft away and produced a week of false symptoms
|
||||
that all looked like API problems.
|
||||
|
||||
Areas are ft2, volumes ft3, and `unit_weight_pcf` is per cubic foot. Only
|
||||
lengths are ambiguous; be explicit in every variable name you write.
|
||||
|
||||
## The library, in `WillTools.extension/lib/vibe/`
|
||||
|
||||
- `knowledge.load(doc)` — the merged configuration: shipped defaults, then the
|
||||
plant file, then the job file, then the project config. Later layers win, and
|
||||
**lists replace rather than merge**.
|
||||
- `ticketqc` — reading a ticket. `collect_context(doc)` returns
|
||||
`(summaries, sheets)`; `assemblies_by_design(doc, know=know)` groups pieces;
|
||||
`read_ticket(doc, asm, sheets, know=know)` gathers one ticket;
|
||||
`name_of(el)` because `.Name` throws in IronPython on some elements.
|
||||
- `ticketchecks.run_all` — the computed checks.
|
||||
- `liftexport` — measuring a piece for the handling calculation.
|
||||
- `designspec.list_specs(doc)` — reviewed design specs by design number.
|
||||
|
||||
A worked read-only example that gets insert stations right:
|
||||
`_scratch/claude_wp199_verify.py`.
|
||||
|
||||
## Two shapes that have caused wrong answers
|
||||
|
||||
**Element ids.** `ticketqc.eid(el)` returns an int. `doc.GetElement` wants an
|
||||
`ElementId`, so `doc.GetElement(ElementId(int(the_int)))`. Passing the int
|
||||
raises `TypeError: expected Reference, got int`.
|
||||
|
||||
**Insert positions.** `read_ticket` gives each lifter a `local` tuple — `(X, Y,
|
||||
Z)` in the panel's own frame. There is no `x` or `y` key; asking for one returns
|
||||
`None` and looks exactly like missing data. `local[0]` is measured from the
|
||||
panel **centre**:
|
||||
|
||||
station_ft = ticket['length'] / 2.0 + lifter['local'][0]
|
||||
|
||||
Checked: WP200's four P136 sit at `local[0] = ±7.208` on a 22.917 ft piece,
|
||||
giving 4.25 ft from each end, and its ticket prints `4'-3"`.
|
||||
|
||||
**A bar mark is a configuration, not an object.** `SR612` says what the bar
|
||||
IS -- #6, 60 ksi, a 17A bend, A/B/C to these dimensions -- and says nothing
|
||||
about which one. The same mark turning up on eight different tickets is eight
|
||||
different bars built to one recipe, and that is the normal, wanted case.
|
||||
|
||||
The reason is not a modelling convention, it is how the shop works. **Every
|
||||
distinct mark is a SKU**: the bending machine has to be set up for it, by hand,
|
||||
once. So a plant works to keep the number of marks down and bend each one in a
|
||||
big batch. Reusing a mark across many pieces is good practice, not a mistake to
|
||||
be reported.
|
||||
|
||||
What follows for anything that names a bar: **the mark and the element id are
|
||||
both required, and neither substitutes for the other.** The id identifies the
|
||||
object; the mark is what a person recognises -- shown "24227001 overlaps
|
||||
17599760" an engineer asks what those are, shown "SR701 x P106" he answers in
|
||||
one sentence. A report that drops the mark is unreadable; a report that drops
|
||||
the id cannot be acted on, because the mark does not point at anything in
|
||||
particular.
|
||||
|
||||
**Nested families.** An element whose `SuperComponent` is not None belongs to
|
||||
another member. It still appears in `GetMemberIds()`, so counting members
|
||||
without filtering double-counts: `SR5602` sits inside the `P76` embed and
|
||||
`RL_OVAL_PATCH_4T` inside `QLPD4T5WG`.
|
||||
|
||||
## Scratch
|
||||
|
||||
Put scripts in `_scratch/` with a prefix of your own. It is gitignored. Do not
|
||||
write the job name, the client's name, a model filename or an absolute user
|
||||
path into anything you create.
|
||||
|
||||
## What a running session picks up, and what it does not
|
||||
|
||||
Measured 2026-08-30, and more precise than the rule this file carried before.
|
||||
|
||||
brand-new modules import fine
|
||||
already-imported ones stay on the code they were loaded with
|
||||
YAML knowledge re-read on every run
|
||||
|
||||
Ten modules created that day all imported through the bridge with no restart.
|
||||
In the same session `liftexport.weight_volumes` -- added to a module that had
|
||||
already been imported -- was still absent, and knowledge roles added to the
|
||||
plant YAML minutes earlier took effect immediately.
|
||||
|
||||
So the rule is not "a code change needs a restart". It is **a change to a
|
||||
module this session has already imported needs a restart.** New work is
|
||||
verifiable straight away; edits to existing modules are not.
|
||||
|
||||
The trap: importing a module through the bridge to look at it CACHES it. A
|
||||
module read early and edited later is then stuck for the rest of the session,
|
||||
and the bridge will keep reporting the old behaviour convincingly. If a fix
|
||||
seems not to have taken, check whether the session imported that module before
|
||||
the edit -- that is more likely than the fix being wrong.
|
||||
|
||||
Never solve this with pyRevit -> Reload. It kills Revit. Restarting Revit is
|
||||
the owner's action, never an agent's.
|
||||
Reference in New Issue
Block a user