WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
@@ -0,0 +1,12 @@
|
||||
title: Beam Analysis (WIP)
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# (WIP) is not decoration -- see GravityLoadChase. Added 2026-09-05 on the
|
||||
# owner's own instruction, alongside ConnectionAnalysis; nothing behind this
|
||||
# button is built yet, see script.py.
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Beam Analysis -- PLACEHOLDER. Nothing is implemented.
|
||||
#
|
||||
# Added 2026-09-05 alongside ConnectionAnalysis, on the owner's own
|
||||
# instruction to put a button here for it. Same posture as GravityLoadChase
|
||||
# and LateralExtract: a row that opens is still a row somebody clicks, so it
|
||||
# says plainly that nothing is built rather than staying silent about it.
|
||||
#
|
||||
# No scope has been settled -- not what a "beam" is in this model's terms,
|
||||
# not which code governs, not read-only or not. None of that is guessed
|
||||
# here; this button exists so there is somewhere to build it once it is.
|
||||
|
||||
# pylint: disable=import-error,invalid-name,broad-except
|
||||
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
|
||||
# pyRevit reads it only `if not self.meta`. It is kept in step so that
|
||||
# trimming a bundle file cannot bring an old name back. One line, no
|
||||
# `\n`: this button is a row in a `.pulldown`, and a row has no second
|
||||
# line to put one on.
|
||||
__title__ = 'Beam Analysis (WIP)'
|
||||
|
||||
from pyrevit import forms
|
||||
|
||||
forms.alert(
|
||||
'Beam Analysis is not built yet.\n\n'
|
||||
'This row exists so there is somewhere on the ribbon for it. Nothing '
|
||||
'has been read from the model and nothing has been calculated.',
|
||||
title='Beam Analysis', exitscript=True)
|
||||
@@ -0,0 +1,12 @@
|
||||
title: Connection Analysis (WIP)
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# (WIP) is not decoration -- see GravityLoadChase. Added 2026-09-05 on the
|
||||
# owner's own instruction, alongside BeamAnalysis; nothing behind this
|
||||
# button is built yet, see script.py.
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Connection Analysis -- PLACEHOLDER. Nothing is implemented.
|
||||
#
|
||||
# Added 2026-09-05 alongside BeamAnalysis, on the owner's own instruction to
|
||||
# put a button here for it. Same posture as GravityLoadChase and
|
||||
# LateralExtract: a row that opens is still a row somebody clicks, so it
|
||||
# says plainly that nothing is built rather than staying silent about it.
|
||||
#
|
||||
# No scope has been settled -- not which connections, not which code
|
||||
# governs, not read-only or not. None of that is guessed here; this button
|
||||
# exists so there is somewhere to build it once it is.
|
||||
|
||||
# pylint: disable=import-error,invalid-name,broad-except
|
||||
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
|
||||
# pyRevit reads it only `if not self.meta`. It is kept in step so that
|
||||
# trimming a bundle file cannot bring an old name back. One line, no
|
||||
# `\n`: this button is a row in a `.pulldown`, and a row has no second
|
||||
# line to put one on.
|
||||
__title__ = 'Connection Analysis (WIP)'
|
||||
|
||||
from pyrevit import forms
|
||||
|
||||
forms.alert(
|
||||
'Connection Analysis is not built yet.\n\n'
|
||||
'This row exists so there is somewhere on the ribbon for it. Nothing '
|
||||
'has been read from the model and nothing has been calculated.',
|
||||
title='Connection Analysis', exitscript=True)
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Gravity (WIP)
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# (WIP) is not decoration. This tool is buttons-and-docs only until the owner
|
||||
# provides worked examples; the suffix is what stops it being mistaken for a
|
||||
# finished tool by anyone who opens the DesignHub dropdown. It sits on the
|
||||
# title rather than the second line now because a pulldown row has no second
|
||||
# line.
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Gravity load chasing -- PLACEHOLDER. Nothing is implemented.
|
||||
#
|
||||
# It does not write to the Revit model and never will -- like Lateral
|
||||
# Extract, this is meant to be an extractor: read the model, write a file
|
||||
# elsewhere. This one's target is Excel -- it is meant to extract
|
||||
# information and write an Excel workbook. The owner's framing, verbatim
|
||||
# (2026-08-30):
|
||||
#
|
||||
# gravity-load-chasing 是设计,但是我一两句话说不清楚。这不是一个现在就要推的
|
||||
# 项目,先做个按钮跟占位符出来。这将是一个不小于 panel_lift 的功能性按钮。
|
||||
#
|
||||
# "I cannot say it in a sentence or two" is a statement about the problem, so
|
||||
# this file does not try to say it either. It states what is settled, states
|
||||
# that the rest is not, and prints the list of open questions -- above all the
|
||||
# ones only he can answer, because those are what the placeholder is FOR. A
|
||||
# placeholder that quietly starts making engineering assumptions is worse than
|
||||
# an empty one; a placeholder that collects the open questions is worth having.
|
||||
#
|
||||
# The full document is QNA/gravity-load-chasing.md. This script deliberately
|
||||
# carries the substance rather than only a pointer: the person who presses the
|
||||
# button is in Revit, not in a text editor, and "see the file" is how a
|
||||
# placeholder becomes a dead end.
|
||||
#
|
||||
# WHAT THIS FILE MUST NOT GROW INTO
|
||||
# ---------------------------------
|
||||
# No arithmetic. No load model, no tributary rule, no accumulation. No
|
||||
# knowledge key, no schema field, no config default, no data structure, no file
|
||||
# format. None of that has been chosen by anybody, and structure invented ahead
|
||||
# of the method is how a placeholder turns into a set of assumptions nobody
|
||||
# agreed to. If something looks obviously needed, it goes in the QNA document
|
||||
# as a PROPOSAL and waits.
|
||||
#
|
||||
# It also reads nothing. Not geometry, not parameters, not the knowledge
|
||||
# layers. It opens no Transaction, because there is nothing to write and there
|
||||
# never will be from this file. The only thing it touches is the model's title,
|
||||
# so the report says which session printed it.
|
||||
|
||||
# pylint: disable=import-error,invalid-name,broad-except
|
||||
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
|
||||
# pyRevit reads it only `if not self.meta`. It is kept in step so that
|
||||
# trimming a bundle file cannot bring an old name back. One line, no
|
||||
# `\n`: this button is a row in a `.pulldown`, and a row has no second
|
||||
# line to put one on.
|
||||
__title__ = 'Gravity (WIP)'
|
||||
|
||||
import os
|
||||
|
||||
from pyrevit import revit, script
|
||||
|
||||
doc = revit.doc
|
||||
output = script.get_output()
|
||||
output.close_others()
|
||||
|
||||
TITLE = 'Gravity Load Chasing'
|
||||
NOTES = 'QNA/gravity-load-chasing.md'
|
||||
|
||||
|
||||
def notes_path():
|
||||
"""Absolute path to the design-notes document, if it can be found.
|
||||
|
||||
Four levels up from this file is the extension folder; one more is the
|
||||
repo. Purely a convenience for the reader -- a miss costs a line, never
|
||||
the report.
|
||||
"""
|
||||
try:
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
repo = os.path.abspath(os.path.join(here, '..', '..', '..', '..'))
|
||||
found = os.path.join(repo, 'QNA', 'gravity-load-chasing.md')
|
||||
if os.path.isfile(found):
|
||||
return found
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------- the report
|
||||
|
||||
output.print_md('# {0} — NOT IMPLEMENTED'.format(TITLE))
|
||||
output.print_md(
|
||||
'**None of this feature is built.** There is no calculation behind this '
|
||||
'button, and nothing was read from the model to produce this page. That '
|
||||
'is deliberate and it is the whole state of the work: a button, and a '
|
||||
'document holding the questions.')
|
||||
|
||||
try:
|
||||
output.print_md('Model **{0}** — opened, not read.'.format(doc.Title))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
output.print_md('---')
|
||||
|
||||
output.print_md('## What it will be')
|
||||
output.print_md(
|
||||
'Gravity load chasing, as the owner put it on 2026-08-30: *"it is '
|
||||
'design, but I cannot say it in a sentence or two. It is not a project '
|
||||
'to push right now — make a button and a placeholder first. It '
|
||||
'will be a functional button no smaller than Panel Lift Check."*')
|
||||
output.print_md(
|
||||
'One thing beyond that quote is already settled: like Lateral Extract, '
|
||||
'this tool does not write to the Revit model and never will. Both are '
|
||||
'extractors — read the model, write a file elsewhere. This one\'s '
|
||||
'target is **Excel**: it is meant to extract information and write an '
|
||||
'Excel workbook. Nothing here does that yet.')
|
||||
output.print_md(
|
||||
'Only four things follow from the quote itself, and nothing here will '
|
||||
'pretend to more:')
|
||||
output.print_md(
|
||||
'1. It is **design**, not checking.\n'
|
||||
'2. It **cannot yet be specified**, and that is a fact about the '
|
||||
'problem, not about anybody\'s time.\n'
|
||||
'3. It is **not being pushed now**.\n'
|
||||
'4. When it is built it is a **full feature** — its own '
|
||||
'engineering core, its own report, its own tests — at least the '
|
||||
'size of Panel Lift Check.')
|
||||
|
||||
output.print_md('## Two boundaries that are already settled')
|
||||
|
||||
output.print_md(
|
||||
'**It is design, not checking, and the liability is different.** '
|
||||
'Everything else on this panel reviews work that already exists — '
|
||||
'Ticket QC compares a drawn ticket to the model, Panel Lift Check judges '
|
||||
'rigging somebody placed, Reinforcing Suggestion proposes donors for a '
|
||||
'human to accept. In each case the original work is still there to look '
|
||||
'at if the tool is wrong. Gravity load chasing produces the answer '
|
||||
'itself, and it goes out under an engineer\'s seal. That has to shape '
|
||||
'the design from the first commit; it cannot be added at the end.')
|
||||
|
||||
output.print_md(
|
||||
'**Its input is not the current model.** It needs old jobs and their '
|
||||
'spreadsheets as samples and controls — mass-layout among them. '
|
||||
'Nothing else in this extension works that way. Those files also carry '
|
||||
'client and job identity, which may never enter the repository, so where '
|
||||
'the controls live is a real constraint on the project layout and not a '
|
||||
'detail for later.')
|
||||
|
||||
output.print_md(
|
||||
'**Carried forward from the reinforcing work** (see '
|
||||
'`QNA/reinforcing-suggestion-brief.md`): the plant standard bands '
|
||||
'vertical reinforcing `0.2H / PER DESIGN / 0.2H`, and the middle band is '
|
||||
'explicitly the engineer\'s own calculation for that one piece. The '
|
||||
'reinforcing matcher is forbidden from proposing it, because presenting '
|
||||
'one panel\'s design calculation as another panel\'s answer would look '
|
||||
'exactly like an ordinary suggestion. Gravity load chasing stands on the '
|
||||
'same ground — its entire output is that class of quantity. So the '
|
||||
'rule here is the mirror image: **a result from one job must never '
|
||||
'become an input to another by any automatic path**, and nothing this '
|
||||
'tool produces may ever be presented with the confidence the checking '
|
||||
'tools use.')
|
||||
|
||||
output.print_md('## What it will need from you before it can be built')
|
||||
output.print_md(
|
||||
'This is the useful half of a placeholder. None of these have answers '
|
||||
'yet, and none will be invented:')
|
||||
output.print_md(
|
||||
'- **Old jobs, and the spreadsheets that go with them.** Which ones, and '
|
||||
'where they can be reached from — they cannot live in the '
|
||||
'repository.\n'
|
||||
'- **Mass-layout: what it actually is.** A spreadsheet, a tool, a stage '
|
||||
'of the job? Who produces it, when, in what format?\n'
|
||||
'- **Which of those jobs are the controls**, and what counts as passing '
|
||||
'— reproducing your number to within what tolerance, on which '
|
||||
'quantities?\n'
|
||||
'- **Whether the spreadsheets are machine-readable** — fixed '
|
||||
'columns and stable sheet names, or hand-formatted per job. This is a '
|
||||
'large fork: a parser, or an import UI.\n'
|
||||
'- **What is being chased, over what unit, producing what.** One panel, '
|
||||
'one wall line, a whole building; a number per piece, a table, a '
|
||||
'drawing.\n'
|
||||
'- **Which method governs** — which standard and edition, and '
|
||||
'whether the plant has its own that supersedes it.\n'
|
||||
'- **Which loads are in scope**, and what arrives as given rather than '
|
||||
'computed.\n'
|
||||
'- **Design, or a first pass an engineer edits.** Different products, '
|
||||
'not different wordings.\n'
|
||||
'- **Read-only, or not.** Everything in this extension is read-only '
|
||||
'today. Is this the feature that changes that?')
|
||||
|
||||
output.print_md('## The five that block everything else')
|
||||
output.print_md(
|
||||
'The rest can be discovered while building. These cannot — each '
|
||||
'one changes the shape of the whole thing, and getting one wrong is a '
|
||||
'rewrite rather than a fix:')
|
||||
output.print_md(
|
||||
'1. What is being chased, over what unit, producing what.\n'
|
||||
'2. What mass-layout is, and whether the sample spreadsheets can be read '
|
||||
'by a program.\n'
|
||||
'3. Where the controls live, given that they are client work.\n'
|
||||
'4. Design or first pass; read-only or not.\n'
|
||||
'5. Which method governs.')
|
||||
|
||||
output.print_md('## What was deliberately not built')
|
||||
output.print_md(
|
||||
'So it reads as a decision and not an oversight: no load model, no '
|
||||
'tributary logic, no accumulation rule, no arithmetic. No knowledge key, '
|
||||
'no schema field, no config default, no data structure, no file format. '
|
||||
'No library module on either side of the CPython boundary. Anything that '
|
||||
'looked obviously needed was written into the notes as a **proposal** '
|
||||
'instead, so that it gets chosen rather than inherited.')
|
||||
|
||||
output.print_md('---')
|
||||
found = notes_path()
|
||||
if found:
|
||||
output.print_md(
|
||||
'The full scope, every open question, and the proposals are in '
|
||||
'`{0}`.'.format(found))
|
||||
else:
|
||||
output.print_md(
|
||||
'The full scope, every open question, and the proposals are in '
|
||||
'`{0}` in the repository.'.format(NOTES))
|
||||
output.print_md(
|
||||
'Answers are welcome anywhere — in that file, or in '
|
||||
'`QNA/questions.md` the usual way. Until they arrive this button stays '
|
||||
'exactly as honest as it is now.')
|
||||
@@ -0,0 +1,10 @@
|
||||
title: Lateral (WIP)
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# (WIP) is not decoration -- see GravityLoadChase. Half-built, dropdown only.
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,351 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Extract the lateral-resisting members and write an analysis input file.
|
||||
#
|
||||
# WHICH MEMBERS? The user says. Not this tool. A member is lateral-resisting
|
||||
# when somebody has typed the marker into its identity description -- by
|
||||
# default 'lateral-resistent' (the owner's spelling) or 'lateral-resistant'.
|
||||
# Nothing here infers it from geometry, category or name.
|
||||
#
|
||||
# READ-ONLY. No Transaction is opened, nothing is written to the model, and
|
||||
# the output files go beside the model, never into the repository.
|
||||
#
|
||||
# WHAT IT WRITES: a JSON intermediate model, two CSVs (nodes, members), and a
|
||||
# RISA-3D draft that is deliberately named `.r3d.DRAFT.txt` so it cannot be
|
||||
# loaded by accident. See vibe.latrisa for what is established about the RISA
|
||||
# format and what is not.
|
||||
#
|
||||
# The selection idiom follows Ticket QC and Panel Lift Check: what is
|
||||
# preselected is what is checked; nothing preselected scans the configured
|
||||
# categories across the model. The difference from both is that this tool
|
||||
# never asks the user to pick pieces from a list -- the marker IS the pick,
|
||||
# and offering a second way to choose would let the two disagree.
|
||||
|
||||
# pylint: disable=import-error,invalid-name,broad-except
|
||||
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
|
||||
# pyRevit reads it only `if not self.meta`. It is kept in step so that
|
||||
# trimming a bundle file cannot bring an old name back. One line, no
|
||||
# `\n`: this button is a row in a `.pulldown`, and a row has no second
|
||||
# line to put one on.
|
||||
__title__ = 'Lateral (WIP)'
|
||||
|
||||
import io
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from pyrevit import revit, script, forms
|
||||
|
||||
from vibe import designspec, knowledge, latextract, latmodel, latrisa
|
||||
from vibe import runlog
|
||||
from vibe import scope as scopelib
|
||||
|
||||
doc = revit.doc
|
||||
uidoc = revit.uidoc
|
||||
output = script.get_output()
|
||||
output.close_others()
|
||||
|
||||
TITLE = 'Lateral Extract'
|
||||
|
||||
|
||||
def die(message):
|
||||
"""The user has not given us something yet. Not a refusal to extract."""
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title, outcome='refused',
|
||||
report_text=message)
|
||||
forms.alert(message, title=TITLE, exitscript=True)
|
||||
|
||||
|
||||
def not_run(message):
|
||||
"""'Could not judge' -- a RESULT, and it must survive on the report.
|
||||
|
||||
The distinction this whole tool turns on: NOT RUN is not the same as
|
||||
'ran and found nothing marked'. NOT RUN means the question could not be
|
||||
asked at all, so the absence of marked members says nothing. A run that
|
||||
asked and found nothing gets the ordinary report, with a nothing-found
|
||||
banner and no files written.
|
||||
"""
|
||||
output.print_md('# Lateral Extract — NOT RUN')
|
||||
output.print_md('**NOT RUN** — the extraction did not run.')
|
||||
output.print_md('> {0}'.format(message))
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title, outcome='not-run',
|
||||
report_text=message)
|
||||
script.exit()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- main
|
||||
# The main body is wrapped so an unexpected exception still leaves a run-log
|
||||
# record (outcome='error') before pyRevit shows its own error dialog.
|
||||
try:
|
||||
# -------------------------------------------------------------- context
|
||||
know = knowledge.load(doc)
|
||||
for problem in knowledge.config_problems(doc, know):
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title,
|
||||
outcome='refused', report_text=problem)
|
||||
forms.alert(problem, title=TITLE, exitscript=True)
|
||||
|
||||
selection = [doc.GetElement(i) for i in uidoc.Selection.GetElementIds()]
|
||||
scope = ('the {0} element(s) you selected'.format(len(selection))
|
||||
if selection else 'the whole model, in the configured categories')
|
||||
|
||||
# --------------------------------------------------------------------- run
|
||||
with forms.ProgressBar(title='Reading identity descriptions...') as pb:
|
||||
pb.update_progress(0, 2)
|
||||
model, found, why = latextract.gather(doc, selection=selection or None,
|
||||
know=know)
|
||||
pb.update_progress(2, 2)
|
||||
|
||||
cats = knowledge.get(know, 'lateral.categories', None) \
|
||||
or latextract.DEFAULT_CATEGORIES
|
||||
|
||||
if why == latextract.NOT_RUN_UNMAPPED:
|
||||
not_run(
|
||||
'The `parameters.{0}` role is not mapped, so this does not know '
|
||||
'which parameter holds the identity description the marker is '
|
||||
'typed into.\n\n'
|
||||
'It will not guess a name. A guessed name would work on the '
|
||||
'template it was guessed from and silently find nothing on every '
|
||||
'other one, and "found nothing" is indistinguishable from "nothing '
|
||||
'is marked" once it is on the screen.\n\n'
|
||||
'Map it in the plant knowledge file and run again. Self Check lists '
|
||||
'which layers are in play.'.format(latextract.DESC_ROLE))
|
||||
|
||||
if why == latextract.NOT_RUN_ABSENT:
|
||||
# A literal role path, not a variable, so knowledge_drift can see it.
|
||||
names = knowledge.param_names(know, 'member.identity_description')
|
||||
not_run(
|
||||
'The `parameters.{0}` role is mapped to **{1}**, but not one '
|
||||
'candidate element actually carries that parameter.\n\n'
|
||||
'That is not the same as "nothing is marked". The field is not '
|
||||
'there, so nothing could have been marked, and this tool has no '
|
||||
'opinion of its own about which members resist lateral load.\n\n'
|
||||
'Scope searched: {2}. Categories: `{3}`.\n\n'
|
||||
'Either add the parameter to these families, or point the role at '
|
||||
'whatever field this office really uses.'
|
||||
.format(latextract.DESC_ROLE, ', '.join(names), scope,
|
||||
', '.join(cats)))
|
||||
|
||||
# ------------------------------------------------------------------ report
|
||||
output.print_md('# Lateral extraction')
|
||||
output.print_md(
|
||||
'Model **{0}** · scope: {1} · {2} candidate element(s) read'
|
||||
.format(doc.Title, scope, found['candidates']))
|
||||
output.print_md(
|
||||
'Marker tokens accepted: {0}. Read from: {1}.'
|
||||
.format(', '.join(['`' + t + '`' for t in found['tokens']]),
|
||||
', '.join(['`' + p + '`' for p in found['params_tried']])))
|
||||
|
||||
# ---- the declared scope, and why it is NOT applied here ------------------
|
||||
#
|
||||
# Every other button on this panel narrows what it judges to the job's
|
||||
# declared scope (`vibe.scope`). This one deliberately does not, and says so
|
||||
# rather than leaving the reader to assume either way.
|
||||
#
|
||||
# The reason is that the marker IS the pick. A member is lateral-resisting
|
||||
# because somebody typed it into the identity description, one element at a
|
||||
# time -- that is a person choosing, not the tool choosing, and this whole
|
||||
# tool's rule is that a filter never overrides what the user picked. Beyond
|
||||
# that, a lateral system is columns, beams and shear walls by nature; scoping
|
||||
# it to one discipline's prefix would delete most of the frame and produce an
|
||||
# analysis model that looks complete and is not. The line below is printed
|
||||
# every run so the difference is visible on the page.
|
||||
_declared = scopelib.of(know)
|
||||
output.print_md(
|
||||
'> NOTHING WAS SET ASIDE FOR SCOPE. The declared scope is {0}, and it is '
|
||||
'deliberately NOT applied here: a member is in this extraction because '
|
||||
'somebody marked it, and a lateral system crosses every discipline on '
|
||||
'the job. Every marked member is exported whatever its mark, and the '
|
||||
'members another engineer owns are as much a part of the frame as '
|
||||
'yours.'.format(_declared.label()))
|
||||
|
||||
n_marked = len(found['marked'])
|
||||
n_amb = len(found['ambiguous'])
|
||||
|
||||
if not n_marked and not n_amb:
|
||||
# Asked, and the answer was nothing. Distinct from NOT RUN above, and
|
||||
# said in words rather than left as an empty table -- and critically,
|
||||
# NO FILES ARE WRITTEN. An empty RISA file on disk looks like an answer.
|
||||
output.print_md('## Nothing is marked')
|
||||
output.print_md(
|
||||
'> {0} element(s) were read and **not one** carries the marker. '
|
||||
'The identity-description field exists, so the question was asked '
|
||||
'properly and the answer is that no member has been marked as '
|
||||
'lateral-resisting yet.\n\n'
|
||||
'> **No files have been written.** An empty analysis file on disk '
|
||||
'would look like a result.\n\n'
|
||||
'> To mark a member, type `{1}` into its identity description '
|
||||
'(`{2}`) and run this again.'
|
||||
.format(found['candidates'], found['tokens'][0],
|
||||
found['params_tried'][0]))
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title,
|
||||
outcome='refused',
|
||||
report_text='Nothing is marked - no files written.')
|
||||
script.exit()
|
||||
|
||||
if n_amb:
|
||||
output.print_md('## Ambiguous marks — not counted either way')
|
||||
output.print_md(
|
||||
'The marker appears in these fields but is preceded by a negating '
|
||||
'word, so it may mean the opposite. They are **excluded** from the '
|
||||
'export and named here for you to settle.')
|
||||
output.print_table(
|
||||
[[str(r['element_id']),
|
||||
r['mark'] or '', r['category'] or '', r['desc'] or '']
|
||||
for r in found['ambiguous']],
|
||||
columns=['element id', 'mark', 'category', 'identity description'])
|
||||
|
||||
# ---- what was selected, element by element -------------------------------
|
||||
output.print_md('## Marked as lateral-resisting ({0})'.format(n_marked))
|
||||
output.print_table(
|
||||
[[str(r['element_id']), r['mark'] or '', r['category'] or '',
|
||||
r['desc_param'] or '', r['token'] or '', r['desc'] or '']
|
||||
for r in found['marked']],
|
||||
columns=['element id', 'mark', 'category', 'read from', 'matched',
|
||||
'identity description'])
|
||||
|
||||
# ---- what was skipped, element by element, with the reason ---------------
|
||||
output.print_md('## Skipped ({0})'.format(len(model['skipped'])))
|
||||
if model['skipped']:
|
||||
output.print_table(
|
||||
[[str(s.get('element_id')), s.get('mark') or '',
|
||||
s.get('category') or '', s.get('reason') or '']
|
||||
for s in model['skipped']],
|
||||
columns=['element id', 'mark', 'category', 'why it is not a member'])
|
||||
else:
|
||||
output.print_md('> Nothing was skipped.')
|
||||
|
||||
# ---- what became members -------------------------------------------------
|
||||
output.print_md('## Members written ({0})'.format(len(model['members'])))
|
||||
if model['members']:
|
||||
output.print_table(
|
||||
[[m['label'], m['i_node'], m['j_node'],
|
||||
'{0:.3f}'.format(m['length_ft']),
|
||||
(m.get('section') or {}).get('name') or '(none)',
|
||||
m.get('material') or '(none)',
|
||||
m.get('axis_source') or '',
|
||||
'; '.join(m['unresolved'])]
|
||||
for m in model['members']],
|
||||
columns=['member', 'i', 'j', 'length ft', 'section label',
|
||||
'material', 'axis from', 'NOT determined'])
|
||||
else:
|
||||
output.print_md(
|
||||
'> **No member survived extraction.** Every marked element is in the '
|
||||
'skipped table above with its reason. Nothing analysable came out of '
|
||||
'this run.')
|
||||
|
||||
# ---- what could not be determined ----------------------------------------
|
||||
rows = latmodel.unresolved_rows(model)
|
||||
output.print_md('## Not determined ({0} item(s))'.format(len(rows)))
|
||||
output.print_md(
|
||||
'Every one of these is **left empty** in the output. None of them is '
|
||||
'given a default value. A member with no section is not quietly assigned '
|
||||
'one; the draft file writes `{0}` for it, which no analysis program can '
|
||||
'resolve, so it fails to analyse rather than analysing wrongly.'
|
||||
.format(latrisa.UNRESOLVED_SHAPE))
|
||||
if rows:
|
||||
output.print_table(
|
||||
[[r['label'], str(r['element_id']), r['mark'] or '', r['missing']]
|
||||
for r in rows],
|
||||
columns=['member', 'element id', 'mark', 'what is missing'])
|
||||
|
||||
# ---- connectivity --------------------------------------------------------
|
||||
conn = model['connectivity']
|
||||
output.print_md('## Connectivity')
|
||||
output.print_md(
|
||||
'{0} node(s), {1} member(s), welded at {2} ft. **{3}** node(s) have only '
|
||||
'one member reaching them. This tool does not extract supports, so it '
|
||||
'cannot tell a support from a disconnected member -- both look like this.'
|
||||
.format(conn['node_count'], conn['member_count'], model['node_tol_ft'],
|
||||
len(conn['free_ends'])))
|
||||
if not conn.get('near_miss_checked'):
|
||||
output.print_md(
|
||||
'> The near-miss search was **not run** ({0} nodes is too many for an '
|
||||
'O(n^2) scan on Revit\'s UI thread). That is not a finding of "no '
|
||||
'near misses" -- nobody looked.'.format(conn['node_count']))
|
||||
elif conn['near_misses']:
|
||||
output.print_md(
|
||||
'**{0} pair(s) of nodes sit within 6 inches of each other but further '
|
||||
'apart than the weld tolerance.** These are almost certainly meant to '
|
||||
'be the same point. They are **not** merged here.'
|
||||
.format(conn['near_miss_total']))
|
||||
output.print_table(
|
||||
[[r['a'], r['b'], '{0:.4f}'.format(r['gap_ft'])]
|
||||
for r in conn['near_misses'][:40]],
|
||||
columns=['node', 'node', 'gap ft'])
|
||||
output.print_md(
|
||||
'> A member ending part-way along another member is **not** connected to '
|
||||
'it and no node is inserted to make it so.')
|
||||
|
||||
# ---- the standing caveats ------------------------------------------------
|
||||
output.print_md('## What is deliberately not in the output')
|
||||
for note in model['notes']:
|
||||
output.print_md('- ' + note)
|
||||
|
||||
# ------------------------------------------------------------ where it goes
|
||||
#
|
||||
# Beside the MODEL, never in the repository.
|
||||
start_dir = knowledge.get(know, 'lateral.output_dir', None) or \
|
||||
designspec.sub_dir(doc, 'lateral')
|
||||
try:
|
||||
if start_dir and not os.path.isdir(start_dir):
|
||||
os.makedirs(start_dir)
|
||||
except Exception:
|
||||
start_dir = ''
|
||||
|
||||
if not doc.PathName:
|
||||
output.print_md(
|
||||
'## Not written\n> This model has never been saved, so there is no '
|
||||
'folder beside it to write to. Save the model and run again.')
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title,
|
||||
outcome='refused',
|
||||
report_text='Model has never been saved - nothing written.')
|
||||
script.exit()
|
||||
|
||||
stem = os.path.splitext(os.path.basename(doc.PathName))[0]
|
||||
written = []
|
||||
for name, text in latrisa.file_set(model, stem):
|
||||
path = os.path.join(start_dir, name)
|
||||
try:
|
||||
f = io.open(path, 'w', encoding='utf-8')
|
||||
try:
|
||||
f.write(text if isinstance(text, type(u'')) else text.decode('utf-8'))
|
||||
finally:
|
||||
f.close()
|
||||
written.append(path)
|
||||
except Exception as ex:
|
||||
output.print_md('- **could not write** `{0}`: {1}'.format(name, ex))
|
||||
|
||||
output.print_md('## Files written')
|
||||
for path in written:
|
||||
output.print_md('- `{0}`'.format(path))
|
||||
output.print_md(
|
||||
'> The `.json` and the two `.csv` files are complete statements of what '
|
||||
'was extracted. The `.r3d.DRAFT.txt` is **not** a verified RISA input '
|
||||
'file -- its section keywords follow RISA\'s published Appendix D but '
|
||||
'the field order inside each record is unverified, so it is named so '
|
||||
'that it cannot be opened into RISA by accident. Read its header before '
|
||||
'renaming it.')
|
||||
if not latmodel.is_analysable(model):
|
||||
output.print_md(
|
||||
'> **This model would not analyse correctly as written**, and that '
|
||||
'is on purpose: it has unresolved sections, free ends, no supports '
|
||||
'and no releases. Fix those in the analysis package, not here.')
|
||||
|
||||
# The extraction ran and its files are on disk -- the record is 'ok' with
|
||||
# the report text kept short, since record() caps at 20000 characters.
|
||||
runlog.record('Lateral Extract', doc_title=doc.Title, outcome='ok',
|
||||
report_path=written[0] if written else None,
|
||||
report_text=('{0} marked member(s), {1} skipped, '
|
||||
'{2} node(s), {3} member(s) written; files: {4}'
|
||||
.format(n_marked, len(model['skipped']),
|
||||
conn['node_count'],
|
||||
conn['member_count'],
|
||||
'; '.join(written))))
|
||||
|
||||
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('Lateral Extract', doc_title=doc.Title,
|
||||
outcome='error', error=str(e),
|
||||
traceback_text=traceback.format_exc())
|
||||
raise
|
||||
@@ -0,0 +1,17 @@
|
||||
title: Panel Analysis (WIP)
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# (WIP) is not decoration, same as its siblings in this pulldown. This button
|
||||
# pops an Eriksson-Wall-shaped INPUT CONFIRMATION form -- one collapsed section
|
||||
# per selected panel, pre-filled from the model where readable and blank
|
||||
# otherwise, gated so a panel cannot run until its required fields (incl. >=2
|
||||
# supports) are filled -- then feeds the confirmed inputs to the Stage-0
|
||||
# self-weight solve. The in-place / wind / service engine behind the form is a
|
||||
# later stage. See script.py's own comments for exactly what it does and does
|
||||
# not do.
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,27 @@
|
||||
title: Panel Lift Check
|
||||
# DEV. A fresh IronPython engine for every press: `lib/vibe` is re-imported
|
||||
# on each click, so an edit to `liftexport` -- the Revit half of the
|
||||
# panellift boundary -- is visible without a Revit restart. It trades press
|
||||
# latency (engine construction instead of engine reuse) for restart-freedom.
|
||||
# The CPython half already needs no restart; this is for the half that does.
|
||||
#
|
||||
# Read docs/architecture-migration.md section 8 before trusting or removing
|
||||
# this. Two things it says that are not obvious: pyRevit's DEFAULT is already
|
||||
# a clean engine, and this extension gets one today only because
|
||||
# extension.json has no `url:` and so is never registered as a package --
|
||||
# add a url and every button silently switches to the shared engine. This key
|
||||
# is the pin against that, not a new capability.
|
||||
#
|
||||
# REMOVE IT PER BUTTON as that button stabilises: delete the two lines below.
|
||||
engine:
|
||||
clean: true
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# MOVED 2026-09-05 from Tools.panel, where it was one of four large icons
|
||||
# (see Tools.panel/bundle.yaml). One line, no `\n`: this button is now a row
|
||||
# in DesignHub's `.pulldown`, and a pulldown row renders as text beside a
|
||||
# 16px icon -- a `\n` has nowhere to go and the dropdown grows to fit the
|
||||
# longest row, so the title is free to say what it means. No (WIP): moving
|
||||
# panels did not change what this tool does, and it is not half-built.
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,36 @@
|
||||
# See Setup.panel/Config.pulldown/bundle.yaml for what a `.pulldown` is and
|
||||
# why a container may carry a `title:`.
|
||||
title: DesignHub
|
||||
|
||||
# REORGANISED 2026-09-05, on the owner's own instruction: "把 Panel Lift
|
||||
# 移到 Design Hub 里面去,再把 Beam Analysis 跟 Connection Analysis 也加
|
||||
# 进去" -- Panel Lift Check moved onto this pulldown from being one of
|
||||
# Tools.panel's four large icons (see that panel's own bundle comment), and
|
||||
# two new placeholder rows, BeamAnalysis and ConnectionAnalysis, joined
|
||||
# GravityLoadChase and LateralExtract as more (WIP) rows.
|
||||
#
|
||||
# PanelLiftCheck is NOT half-built -- it is the one row here that runs, end
|
||||
# to end, on a real piece, exactly like PanelAnalysis. It moved here because
|
||||
# the owner wants it grouped with the other analysis tools rather than
|
||||
# because it stopped working; nothing about its own behaviour changed.
|
||||
#
|
||||
# The other four rows are half-built and all four say so in their own
|
||||
# titles. Keep the (WIP) suffixes until the tool behind one actually runs:
|
||||
# the arrow hides them from the ribbon, but a row that opens is still a row
|
||||
# somebody clicks. PanelAnalysis differs from BeamAnalysis, ConnectionAnalysis,
|
||||
# GravityLoadChase and LateralExtract in one way worth naming: it DOES run,
|
||||
# end to end, and opens a real page -- what is unfinished is the analysis
|
||||
# pipeline behind the page it opens, not the button itself. See that
|
||||
# button's own script.py.
|
||||
#
|
||||
# Order: the two tools that run today, then the four that do not, in the
|
||||
# order the owner named them.
|
||||
#
|
||||
# Anything not listed here is not created. Add a button, add it here.
|
||||
layout:
|
||||
- PanelLiftCheck
|
||||
- PanelAnalysis
|
||||
- BeamAnalysis
|
||||
- ConnectionAnalysis
|
||||
- GravityLoadChase
|
||||
- LateralExtract
|
||||
|
After Width: | Height: | Size: 5.0 KiB |