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

Popular posts from this blog

Revit area plans adding new types and references (Gross and rentable)

Revit CSV file manager for families and re-exporting to a CSV file

PDQ Sticky notification - Systray and MSG box notificaitons for install complete for users to close