# -*- 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. # `
` 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 — ' '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()