WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
# -*- 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'
|
||||
|
||||
import traceback
|
||||
|
||||
from pyrevit import revit, script, forms
|
||||
|
||||
from vibe import foldreport as fold
|
||||
from vibe import knowledge
|
||||
from vibe import runlog
|
||||
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)
|
||||
runlog.record('Takeoff', doc_title=doc.Title, outcome='not-run',
|
||||
report_text='{0}: {1}'.format(headline, message))
|
||||
script.exit()
|
||||
|
||||
|
||||
def main():
|
||||
know = knowledge.load(doc)
|
||||
problems = knowledge.config_problems(doc, know)
|
||||
if problems:
|
||||
runlog.record('Takeoff', doc_title=doc.Title, outcome='refused',
|
||||
report_text='\n'.join(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 — '
|
||||
'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'))
|
||||
|
||||
# The takeoff is on the page and (usually) in the files beside the model
|
||||
# -- the record is 'ok' with the paths that were written.
|
||||
runlog.record('Takeoff', doc_title=doc.Title, outcome='ok',
|
||||
report_path=paths.get('.md'),
|
||||
report_text=('Takeoff ran; index built {0}. Reports: {1}'
|
||||
.format(coverage.get('built_at') or 'never',
|
||||
', '.join(sorted(paths)) or 'not '
|
||||
'written')))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
# The deliberate exits above never reach here (script.exit() raises
|
||||
# SystemExit, which Exception does not catch) -- this is the
|
||||
# unexpected-exception path, and pyRevit shows its own dialog after
|
||||
# the record is written.
|
||||
runlog.record('Takeoff', doc_title=doc.Title,
|
||||
outcome='error', error=str(e),
|
||||
traceback_text=traceback.format_exc())
|
||||
raise
|
||||
Reference in New Issue
Block a user