WillTools: precast shop-ticket QC and panel handling tools
ci / test (push) Has been cancelled

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:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
@@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
#
# The takeoff, for the whole job: what it contains, per piece and in total.
#
# The counting lives in `vibe.takeoffcore` (classification and both totals),
# `vibe.takeoffcount` (the control-number multiplier) and `vibe.takeoffreport`
# (the two renderings), all three Revit-free and unit-tested on host CPython.
# `vibe.takeoffgather` is the only part that touches a document. This file is
# the button and nothing more.
#
# READ ONLY. No Transaction is opened here or in anything this calls. A takeoff
# reports what a model contains; it has no business changing it.
#
# WHAT A PRESS COSTS, AND WHY IT DOES NOT REFRESH THE INDEX
# -----------------------------------------------------------
# It reads the cached reinforcing index (~1.4 s, no geometry, no view) and
# makes one cheap pass over the assembly instances for the multiplier (~0.3 s).
# It deliberately does NOT rebuild the index. A takeoff off a stale index is a
# takeoff of the model as it was, and the report says when the index was built
# so a reader who has been editing can see the stamp predates his edits. A
# button that silently spends ten seconds rebuilding is a button that sometimes
# freezes Revit and never says why; refreshing is Reinforcing Suggestion's job
# and it has the progress bar for it.
#
# JUDGEMENT CALLS MADE HERE, for the owner to overrule
# -----------------------------------------------------
# 1. Whole job per press, not one piece. A takeoff is a job-level document --
# ordering is done once for the job, and a per-piece takeoff is already in
# the by-piece CSV this writes.
# 2. It multiplies by control number, and prints both figures. Per-mark and
# per-job answer different questions and a reader will want to check one
# against the other, so neither is hidden behind the other.
# 3. The multiplier comes from the control numbers on framing elements, via
# the job reconciliation's own gatherer -- not from assembly names, which
# were measured through the bridge and found to be 755 unique names for 755
# assemblies, i.e. no multiplier at all. An unmapped control-number role is
# printed as a refusal at the top of the multiplier section, never as a job
# where every mark happens to be one piece.
# 4. An empty index is a refusal, not an empty takeoff. A takeoff of nothing
# that looks like a takeoff of a job with nothing in it is the worst
# possible output, so it says the index has not been built and names the
# button that builds it.
# 5. The reports go beside the model, never into the repository. They name
# marks, part numbers and quantities for one client's job.
#
# WHAT THIS PAGE LOOKS LIKE, AND WHOSE RULE THAT IS
# --------------------------------------------------
# The owner's standing rule for the whole tab, and it lives in
# `vibe.foldreport` rather than here: the answer is the headline, biggest and
# first and never folded; everything else follows FOLDED; a section with a
# problem in it opens itself and says on its header why. The contract that
# buys is "if nothing below the headline is open, nothing needs me", and it
# is worth more than any one section being visible.
#
# Here the headline is the ORDER -- hardware by part number, reinforcing by
# bar mark, both figures on every line, with what multiplied them written
# above. Coverage, provenance, the piece-by-piece breakdown and the basis
# tallies fold. The refusal does not: an unmapped control-number role means
# the model was never asked how many pieces a mark is, every per-job figure
# is an undercount, and that renders OPEN as plain HTML.
#
# WHY THE FOLD IS IE5-ERA DOM AND IS DESIGNED TO FAIL SAFE
# ---------------------------------------------------------
# This output panel is a `System.Windows.Forms.WebBrowser` -- the IE engine,
# in IE7 document mode, since pyRevit sets no FEATURE_BROWSER_EMULATION.
# `<details>` does not exist there and fails SILENTLY. So the fold is an
# inline `onclick` plus `style.display`, and it has not been exercised inside
# Revit by the agent that wrote it. Two things make that survivable:
# a section with a problem renders open with no script at all, and the whole
# report, nothing folded, is the `.md` written beside the model every press
# -- which this page names. A dead toggle costs a file open, never a
# sentence.
# pylint: disable=import-error,invalid-name,broad-except
__title__ = 'Takeoff'
from pyrevit import revit, script, forms
from vibe import foldreport as fold
from vibe import knowledge
from vibe import takeoffgather as gather
from vibe import takeoffreport as report
doc = revit.doc
output = script.get_output()
output.close_others()
TITLE = 'Takeoff'
def not_judged(headline, message):
"""'Could not judge' is a RESULT and it gets printed, not swallowed into
a dialog that vanishes. It must never read as 'judged and found
nothing'."""
output.print_md('# {0}'.format(TITLE))
output.print_md('## {0}'.format(headline))
output.print_md(message)
script.exit()
def main():
know = knowledge.load(doc)
problems = knowledge.config_problems(doc, know)
if problems:
forms.alert('\n'.join(problems), title=TITLE, exitscript=True)
takeoff, counts, extras = gather.run(doc, know=know)
coverage = takeoff.get('coverage') or {}
if not coverage.get('indexed_marks'):
not_judged(
'The reinforcing index has not been built for this model',
'A takeoff is built from that index and there is nothing in it '
'yet. This is not a job with no hardware in it -- it is a '
'question nobody has asked the model.\n\n'
'Run **Reinforcing Suggestion** once; it builds the index. Then '
'press this again.')
sections = report.sections(takeoff, counts, title=TITLE, extras=extras)
# FOLDED IS NOT GONE, so the unfolded copy is written BEFORE anything is
# printed -- the file then exists even if the printing throws, and the
# page can name it. `takeoff.md` is that copy: `report.markdown` renders
# every section in full, folded or not, so it can never say less than
# this page does.
paths, write_error = {}, None
try:
paths = gather.write_reports(doc, takeoff, counts, extras=extras)
except Exception as exc:
write_error = exc
# ---- the answer, through pyRevit, big and first.
for block in sections[0].blocks:
if block[0] == fold.TABLE:
output.print_table(block[2], columns=block[1])
else:
output.print_md(block[1])
# ---- everything else, folded, and the one line that says whether any of
# it needs him. That line IS the contract; without it a page of shut
# headers looks the same whether or not something is wrong.
tail = sections[1:]
if tail:
output.add_style(fold.STYLE)
output.inject_script(fold.SCRIPT)
opened = fold.sections_with_problems(tail)
output.print_md('---')
if opened:
output.print_md(
'**{0} of the {1} section(s) below opened themselves &mdash; '
'they have something in them that needs you.** The rest are '
'folded; click a header to open one.'
.format(len(opened), len(tail)))
else:
output.print_md(
'**Nothing below needs you.** Every section is folded; click '
'a header to open one.')
if paths.get('.md'):
output.print_md('The whole takeoff, nothing folded, is in `{0}`.'
.format(paths['.md']))
output.print_html(fold.html(tail))
output.print_md('---')
if write_error is not None:
# The page is still complete, but only while the toggle works -- so
# this says which, rather than the old flat report's promise that
# what is on the page is all of it.
output.print_md(
'**The report could not be written to disk:** {0}. Every section '
'is still on this page, but the folded ones need the click to '
'open; there is no file copy of them this press.'.format(
write_error))
else:
output.print_md('Written beside the model:')
for suffix in sorted(paths):
output.print_md('- `{0}`'.format(paths[suffix]))
output.print_md('---')
output.print_md(
'_Read only. Nothing was written to the model. Index built '
'{0}._'.format(coverage.get('built_at') or 'never'))
main()