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

Powerpoint countdown and current time in slides VBA

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

PS1 search for existing edge/opened link before opening