pt.move-titleblock.py
# SUMMARY
##NOTE THERE ARE NOT INTERNAL ORIGINS IN FAMILIES SO DON"T TRY!
# One-shot move per sheet (single undo):
# 1) Optional run gate (IN[0]).
# 2) Target sheets from IN[1] (supports wrapped ViewSheet(s), DB.ViewSheet(s), ElementId/int(s), or None/"").
# 3) For each sheet, compute move vector = - (largest title block bbox Min) so TB bbox Min becomes (0,0).
# 4) Collect ALL paper-space elements on the sheet.
# 5) Exclude ONLY:
# - Origin CAD marker imports (ImportInstance) whose name/type contains "origin" or "0,0,0"/"0,0-origin"
# - ALL ScheduleSheetInstance elements (includes revision schedules) so they are NOT touched (prevents snap-back drift)
# 6) Record pinned elements in the move set, unpin them, move in one shot, then re-pin.
# 7) Try batch MoveElements; on binding failure, fall back to per-element MoveElement (still in same Transaction).
#
# INPUTS
# IN[0] run (bool, optional): True/None/omitted -> run, False -> do nothing
# IN[1] sheets filter (optional):
# - None / "" -> all sheets
# - ViewSheet or list[ViewSheet] (wrapped/DB) -> only those sheets
# - ElementId/int/list -> only those sheets
#
# OUTPUT
# OUT = report list of dicts per sheet
import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')
from Autodesk.Revit.DB import (
FilteredElementCollector, ViewSheet, FamilyInstance, BuiltInCategory,
ImportInstance, ElementTransformUtils, XYZ, ElementId,
BuiltInParameter, ScheduleSheetInstance
)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
# Optional: .NET List[T] for MoveElements binding in CPython
try:
from System.Collections.Generic import List as ClrList
HAS_CLR_LIST = True
except:
HAS_CLR_LIST = False
doc = DocumentManager.Instance.CurrentDBDocument
# ---------------- inputs ----------------
run = True if (len(IN) == 0 or IN[0] in (True, None)) else False
input_sheets = IN[1] if len(IN) > 1 else None
# ---------------- sheet filtering (accept sheet elements + unwrap) ----------------
def _to_elemid(x):
# Accept ElementId, int-like, or Revit element (wrapped/DB) with .Id
if isinstance(x, ElementId):
return x
# Try Dynamo unwrap
try:
ux = UnwrapElement(x)
if ux is not None and hasattr(ux, "Id") and isinstance(ux.Id, ElementId):
return ux.Id
except:
pass
# Try direct .Id
try:
if hasattr(x, "Id") and isinstance(x.Id, ElementId):
return x.Id
except:
pass
# Try int conversion
try:
return ElementId(int(x))
except:
return None
def _as_sheet_id_set(x):
# None/"" -> None (all sheets)
# invalid/empty list -> empty set (no sheets)
if x is None or x == "":
return None
seq = x if isinstance(x, (list, tuple)) else [x]
ids = []
for item in seq:
eid = _to_elemid(item)
if eid:
ids.append(eid)
return set(ids) if ids else set()
target_sheet_ids = _as_sheet_id_set(input_sheets)
# ---------------- helpers ----------------
def tblocks_on_sheet(sheet):
return list(
FilteredElementCollector(doc, sheet.Id)
.OfCategory(BuiltInCategory.OST_TitleBlocks)
.OfClass(FamilyInstance)
.WhereElementIsNotElementType()
)
def bbox_area_in_view(elem, view):
bb = elem.get_BoundingBox(view)
if not bb:
return 0.0
return abs((bb.Max.X - bb.Min.X) * (bb.Max.Y - bb.Min.Y))
def largest_titleblock_bbox(sheet):
ranked = []
for tb in tblocks_on_sheet(sheet):
bb = tb.get_BoundingBox(sheet)
if bb:
ranked.append((bbox_area_in_view(tb, sheet), bb))
if not ranked:
return None
ranked.sort(key=lambda x: x[0], reverse=True)
return ranked[0][1]
def paper_elems(sheet):
return list(FilteredElementCollector(doc, sheet.Id).WhereElementIsNotElementType())
def _safe_str(x):
try:
return (x or "").strip()
except:
return ""
def _get_import_best_name(imp):
# ImportInstance.Name can be "Import Symbol"; the type name often carries the DWG name
parts = []
try:
parts.append(_safe_str(imp.Name))
except:
pass
try:
t = doc.GetElement(imp.GetTypeId())
if t:
try:
parts.append(_safe_str(t.Name))
except:
pass
for bip in (BuiltInParameter.ALL_MODEL_TYPE_NAME, BuiltInParameter.SYMBOL_NAME_PARAM):
try:
p = t.get_Parameter(bip)
if p:
parts.append(_safe_str(p.AsString()))
except:
pass
except:
pass
return " | ".join([p for p in parts if p])
def is_origin_cad_import(elem):
# Exclude ONLY origin marker CAD imports
if not isinstance(elem, ImportInstance):
return False
s = _get_import_best_name(elem).lower().replace(" ", "")
return ("origin" in s) or ("0,0,0" in s) or ("0,0-origin" in s) or ("00,0-origin" in s)
def is_excluded(elem):
# Exclude origin CAD markers
if is_origin_cad_import(elem):
return True
# Exclude ALL sheet schedule instances (revision schedules included)
if isinstance(elem, ScheduleSheetInstance):
return True
return False
def _try_set_pinned(elem, state):
try:
if hasattr(elem, "Pinned"):
elem.Pinned = state
return True
except:
pass
return False
# ---------------- main ----------------
report = []
if not run:
OUT = ["Set IN[0]=True to run."]
else:
sheets = list(FilteredElementCollector(doc).OfClass(ViewSheet))
# Apply sheet filter
if target_sheet_ids is not None:
sheets = [s for s in sheets if s.Id in target_sheet_ids]
if not sheets:
OUT = ["No target sheets."]
else:
TransactionManager.Instance.EnsureInTransaction(doc)
for s in sheets:
tb_bb = largest_titleblock_bbox(s)
if not tb_bb:
report.append({"sheet": s.SheetNumber, "name": s.Name, "status": "no title block"})
continue
move = XYZ(-tb_bb.Min.X, -tb_bb.Min.Y, 0.0)
if move.IsZeroLength():
report.append({"sheet": s.SheetNumber, "name": s.Name, "status": "already at (0,0)"})
continue
all_elems = paper_elems(s)
# Filter: move everything except excluded items
movables = []
skipped_origin_cad = 0
skipped_schedules = 0
for e in all_elems:
if is_origin_cad_import(e):
skipped_origin_cad += 1
continue
if isinstance(e, ScheduleSheetInstance):
skipped_schedules += 1
continue
movables.append(e)
if not movables:
report.append({
"sheet": s.SheetNumber,
"name": s.Name,
"status": "no movable paper elements",
"skipped_origin_cad": skipped_origin_cad,
"skipped_schedules": skipped_schedules
})
continue
# Record pinned state and unpin only what we intend to move
unpinned_ids = []
for e in movables:
try:
if getattr(e, "Pinned", False):
if _try_set_pinned(e, False):
unpinned_ids.append(e.Id.IntegerValue)
except:
pass
moved = 0
tried_batch = False
batch_ok = False
failed_ids = []
# ONE-SHOT: MoveElements once per sheet
if HAS_CLR_LIST:
try:
id_list = ClrList[ElementId]()
for e in movables:
id_list.Add(e.Id)
ElementTransformUtils.MoveElements(doc, id_list, move)
moved = len(movables)
tried_batch = True
batch_ok = True
except:
tried_batch = True
batch_ok = False
# Fallback: per-element move (still same transaction)
if not batch_ok:
for e in movables:
try:
ElementTransformUtils.MoveElement(doc, e.Id, move)
moved += 1
except:
try:
failed_ids.append(e.Id.IntegerValue)
except:
pass
# Re-pin exactly the elements we unpinned
repinned = 0
for iid in unpinned_ids:
try:
el = doc.GetElement(ElementId(iid))
if el and _try_set_pinned(el, True):
repinned += 1
except:
pass
report.append({
"sheet": s.SheetNumber,
"name": s.Name,
"status": "moved",
"moved_count": moved,
"total_candidates": len(movables),
"skipped_origin_cad": skipped_origin_cad,
"skipped_schedules": skipped_schedules,
"unpinned_count": len(unpinned_ids),
"repinned_count": repinned,
"batch_tried": tried_batch,
"batch_ok": batch_ok,
"failed_count": len(failed_ids),
"failed_ids": failed_ids[:50], # cap output size
"delta": (round(move.X, 6), round(move.Y, 6), 0.0)
})
TransactionManager.Instance.TransactionTaskDone()
OUT = report
Comments
Post a Comment