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,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Small, boring helpers that remove the usual Revit API boilerplate.
|
||||
|
||||
Written for IronPython 2.7 (pyRevit's default engine) but 3.x-safe:
|
||||
no f-strings, no walrus.
|
||||
"""
|
||||
# pylint: disable=import-error,invalid-name,broad-except
|
||||
from contextlib import contextmanager
|
||||
|
||||
from Autodesk.Revit import DB, UI # noqa: F401
|
||||
from Autodesk.Revit.DB import (
|
||||
FilteredElementCollector, BuiltInCategory, BuiltInParameter,
|
||||
ElementId, StorageType, Transaction, TransactionGroup,
|
||||
UnitUtils,
|
||||
)
|
||||
|
||||
# UnitTypeId arrived in Revit 2021 and DisplayUnitType was removed in 2022, so
|
||||
# the two never overlap for long. Resolving the unit handles once here keeps a
|
||||
# host outside that window from failing the import of the whole library --
|
||||
# which would take every button down, not just the four conversions below.
|
||||
try:
|
||||
from Autodesk.Revit.DB import UnitTypeId
|
||||
_MILLIMETERS = UnitTypeId.Millimeters
|
||||
_METERS = UnitTypeId.Meters
|
||||
_SQUARE_METERS = UnitTypeId.SquareMeters
|
||||
except ImportError: # Revit 2020 and older
|
||||
from Autodesk.Revit.DB import DisplayUnitType
|
||||
_MILLIMETERS = DisplayUnitType.DUT_MILLIMETERS
|
||||
_METERS = DisplayUnitType.DUT_METERS
|
||||
_SQUARE_METERS = DisplayUnitType.DUT_SQUARE_METERS
|
||||
|
||||
from pyrevit import revit, script, HOST_APP
|
||||
|
||||
|
||||
# ------------------------------------------------------------ session handles
|
||||
#
|
||||
# revit.doc / revit.uidoc are LIVE @property accessors -- they always return
|
||||
# whichever document is active RIGHT NOW. Assigning one to a module global
|
||||
# snapshots its value ONCE, at the first import of `vibe`, and freezes it to
|
||||
# whatever document happened to be active then. With two models open and the
|
||||
# user switching between them that snapshot goes stale, and any helper that
|
||||
# read it would silently act on the wrong model -- a transaction opened on the
|
||||
# stale doc, a collector enumerating the stale doc's elements with no error.
|
||||
#
|
||||
# So the helpers below NEVER read these globals; they call _doc()/_uidoc()/
|
||||
# _uiapp(), which re-resolve on every call. The globals are kept only for
|
||||
# backward compatibility: `vibe/__init__.py` re-exports them, and some callers
|
||||
# do `from vibe.helpers import doc`. IronPython 2.7 has no module-level
|
||||
# __getattr__ (that is Python 3.7+), so these names cannot be made to
|
||||
# re-resolve on plain attribute access -- a caller that reads helpers.doc /
|
||||
# vibe.doc still gets the frozen snapshot. Any such caller in a
|
||||
# model-reading or transaction path must be repointed to _doc() (or read
|
||||
# revit.doc directly) instead of these names.
|
||||
uiapp = HOST_APP.uiapp
|
||||
uidoc = revit.uidoc
|
||||
doc = revit.doc
|
||||
|
||||
|
||||
def _uiapp():
|
||||
"""The live UIApplication, re-resolved on each call (never a snapshot)."""
|
||||
return HOST_APP.uiapp
|
||||
|
||||
|
||||
def _uidoc():
|
||||
"""The active UIDocument, re-resolved on each call (never a snapshot)."""
|
||||
return revit.uidoc
|
||||
|
||||
|
||||
def _doc():
|
||||
"""The active Document, re-resolved on each call (never a snapshot)."""
|
||||
return revit.doc
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- collecting
|
||||
|
||||
def _as_bic(category):
|
||||
"""Accept BuiltInCategory, 'OST_Walls', or 'Walls'."""
|
||||
if isinstance(category, BuiltInCategory):
|
||||
return category
|
||||
name = str(category)
|
||||
if not name.startswith('OST_'):
|
||||
name = 'OST_' + name.replace(' ', '')
|
||||
bic = getattr(BuiltInCategory, name, None)
|
||||
if bic is None:
|
||||
raise ValueError('unknown BuiltInCategory: ' + str(category))
|
||||
return bic
|
||||
|
||||
|
||||
def q(category=None, cls=None, view=None, types=False, document=None):
|
||||
"""One-liner element query. Returns a list.
|
||||
|
||||
q('OST_Walls') -> all wall instances
|
||||
q(cls=DB.Wall) -> same, by class
|
||||
q('OST_Doors', view=doc.ActiveView)-> only what's visible in the view
|
||||
q('OST_Walls', types=True) -> wall types
|
||||
"""
|
||||
d = document or _doc()
|
||||
col = FilteredElementCollector(d, view.Id) if view is not None \
|
||||
else FilteredElementCollector(d)
|
||||
if category is not None:
|
||||
col = col.OfCategory(_as_bic(category))
|
||||
if cls is not None:
|
||||
col = col.OfClass(cls)
|
||||
col = col.WhereElementIsElementType() if types \
|
||||
else col.WhereElementIsNotElementType()
|
||||
return list(col.ToElements())
|
||||
|
||||
|
||||
def all_of(category, document=None):
|
||||
"""All instances of a category."""
|
||||
return q(category, document=document)
|
||||
|
||||
|
||||
def types_of(category, document=None):
|
||||
"""All element types of a category."""
|
||||
return q(category, types=True, document=document)
|
||||
|
||||
|
||||
def by_name(elements, name):
|
||||
"""First element whose .Name matches, else None."""
|
||||
for el in elements:
|
||||
try:
|
||||
if el.Name == name:
|
||||
return el
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def selected(document=None):
|
||||
"""Currently selected elements as a list."""
|
||||
d = document or _doc()
|
||||
return [d.GetElement(i) for i in _uidoc().Selection.GetElementIds()]
|
||||
|
||||
|
||||
def pick(message='Pick elements', multiple=True):
|
||||
"""Interactive pick. Returns a list of elements (empty if cancelled)."""
|
||||
from Autodesk.Revit.UI.Selection import ObjectType
|
||||
_uidoc_ = _uidoc()
|
||||
_doc_ = _doc()
|
||||
try:
|
||||
if multiple:
|
||||
refs = _uidoc_.Selection.PickObjects(ObjectType.Element, message)
|
||||
return [_doc_.GetElement(r) for r in refs]
|
||||
ref = _uidoc_.Selection.PickObject(ObjectType.Element, message)
|
||||
return [_doc_.GetElement(ref)]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- parameters
|
||||
|
||||
def _find_param(el, name):
|
||||
"""Resolve a parameter by BuiltInParameter, exact name, then case-insensitive."""
|
||||
if isinstance(name, BuiltInParameter):
|
||||
return el.get_Parameter(name)
|
||||
prm = el.LookupParameter(str(name))
|
||||
if prm is not None:
|
||||
return prm
|
||||
lowered = str(name).lower()
|
||||
for candidate in el.Parameters:
|
||||
if candidate.Definition.Name.lower() == lowered:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def p(el, name, default=None):
|
||||
"""Read a parameter value, typed. Falls back to `default` if missing."""
|
||||
prm = _find_param(el, name)
|
||||
if prm is None:
|
||||
return default
|
||||
st = prm.StorageType
|
||||
if st == StorageType.String:
|
||||
return prm.AsString()
|
||||
if st == StorageType.Integer:
|
||||
return prm.AsInteger()
|
||||
if st == StorageType.Double:
|
||||
return prm.AsDouble()
|
||||
if st == StorageType.ElementId:
|
||||
return prm.AsElementId()
|
||||
return default
|
||||
|
||||
|
||||
def p_display(el, name, default=None):
|
||||
"""Read a parameter as the string Revit shows in the UI (with units)."""
|
||||
prm = _find_param(el, name)
|
||||
if prm is None:
|
||||
return default
|
||||
val = prm.AsValueString()
|
||||
return val if val is not None else prm.AsString()
|
||||
|
||||
|
||||
def set_p(el, name, value):
|
||||
"""Write a parameter. Must be called inside a transaction.
|
||||
|
||||
Returns True on success, False if the parameter is missing or read-only.
|
||||
"""
|
||||
prm = _find_param(el, name)
|
||||
if prm is None or prm.IsReadOnly:
|
||||
return False
|
||||
return bool(prm.Set(value))
|
||||
|
||||
|
||||
# -------------------------------------------------------------- transactions
|
||||
|
||||
@contextmanager
|
||||
def tx(name='vibe'):
|
||||
"""Transaction context manager - commits on success, rolls back on error.
|
||||
|
||||
with tx('Rename walls'):
|
||||
set_p(wall, 'Comments', 'checked')
|
||||
"""
|
||||
t = Transaction(_doc(), name)
|
||||
t.Start()
|
||||
try:
|
||||
yield t
|
||||
t.Commit()
|
||||
except Exception:
|
||||
if t.HasStarted() and not t.HasEnded():
|
||||
t.RollBack()
|
||||
raise
|
||||
|
||||
|
||||
@contextmanager
|
||||
def txg(name='vibe group'):
|
||||
"""Transaction group - lets you undo several transactions as one step."""
|
||||
g = TransactionGroup(_doc(), name)
|
||||
g.Start()
|
||||
try:
|
||||
yield g
|
||||
g.Assimilate()
|
||||
except Exception:
|
||||
if g.HasStarted() and not g.HasEnded():
|
||||
g.RollBack()
|
||||
raise
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- units
|
||||
# Revit stores lengths in decimal FEET internally, always.
|
||||
|
||||
def ft(millimeters):
|
||||
"""mm -> internal feet."""
|
||||
return UnitUtils.ConvertToInternalUnits(millimeters, _MILLIMETERS)
|
||||
|
||||
|
||||
def mm(internal_feet):
|
||||
"""internal feet -> mm."""
|
||||
return UnitUtils.ConvertFromInternalUnits(internal_feet, _MILLIMETERS)
|
||||
|
||||
|
||||
def m(internal_feet):
|
||||
"""internal feet -> meters."""
|
||||
return UnitUtils.ConvertFromInternalUnits(internal_feet, _METERS)
|
||||
|
||||
|
||||
def sqft_to_sqm(internal_area):
|
||||
"""internal square feet -> square meters."""
|
||||
return UnitUtils.ConvertFromInternalUnits(internal_area, _SQUARE_METERS)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- output
|
||||
|
||||
def out():
|
||||
"""The pyRevit output window for the running script."""
|
||||
return script.get_output()
|
||||
|
||||
|
||||
def log(*args):
|
||||
"""print() that always lands in the pyRevit output window."""
|
||||
print(' '.join(str(a) for a in args))
|
||||
|
||||
|
||||
def table(rows, columns=None, title=None):
|
||||
"""Render a list of lists as a table in the pyRevit output window."""
|
||||
output = script.get_output()
|
||||
if title:
|
||||
output.print_md('### ' + title)
|
||||
output.print_table(table_data=[[str(c) for c in r] for r in rows],
|
||||
columns=columns or [])
|
||||
Reference in New Issue
Block a user