# -*- 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 [])