8e6ee31596
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.
280 lines
9.0 KiB
Python
280 lines
9.0 KiB
Python
# -*- 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 [])
|