Can you Center Room Tags in Host Model with Linked Arch Model?

General question before I go down a rabbit hole.

I have a linked model with all the rooms. I have tagged these rooms in my host model. I use the Tag All feature. The rooms tags however, aren’t nice and center. I have always had to move them manually in my host model.

I have another tool that is able to recenter tags/reference lines but only for those in my host model.

Would it be possible to center the room tags in my host model that are referencing rooms from a linked model? What would be the approach? Bounding Box from a linked model? something else?

I have never explored stuff on the linked model side of things….yet.

Getting elements from linked files is kind a tricky.

I’m working around in this using some pyM4B tools,

I have tried to modify the Center Rooms tool to work whit the snippet from SelectionBox Revit Link, and seems like a possible way, I don’t have enough time to figure out, but this can be a starting point.

#DEFINITIONS
tolist = lambda x : x if hasattr(x, '__iter__') else [x]

##Select linked elements
with forms.WarningBar(title='Select linked elements and then press Finish'):
try:
	elem_refs = tolist(uidoc.Selection.PickObjects(UI.Selection.ObjectType.LinkedElement, "Select linked elements"))
except:
	script.exit()

I have re-use the code from SelectionBox Revit Link to get elements from links in other custom tools and it worked.

Hope it helps.
Cheers mate.

hmm ok. so interacting with links is doable. I’ll take a closer look then. Thanks @AntonioRojas01

Just a clarification:

  • Collecting data and spatial information (and link transformation which will explain why stuff you place aren’t placed at the right location if you do not transform the position of elements from host to link and vice versa) is possible.

  • But modifying elements inside a link is not unless you open that document

1 Like

I played around with this last night for a project I had all the room tags deleted and it would have been to much to do by hand.

it does like 90% ish of room tag recentering. I had some rooms that are skewed in the linked model that did not move, for example
Before Script

After Script

but it got most of the job done. If anyone decides to further refine this and gets it working better, please share.

for now, this is my result

# -*- coding: utf-8 -*-
# pyRevit | IronPython 2.7
# Recenter Room Tags in Active View

import clr, math
clr.AddReference('RevitAPI')

from Autodesk.Revit.DB import *
from pyrevit import script

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
active_view = doc.ActiveView
output = script.get_output()


# ---------------- Helpers ----------------

def compute_2d_centroid(uv_points):
    """Shoelace formula centroid."""
    n = len(uv_points)
    if n < 3:
        return None
    area, cx, cy = 0.0, 0.0, 0.0
    for i in range(n):
        j = (i + 1) % n
        xi, yi = uv_points[i].U, uv_points[i].V
        xj, yj = uv_points[j].U, uv_points[j].V
        cross = xi * yj - xj * yi
        area += cross
        cx += (xi + xj) * cross
        cy += (yi + yj) * cross
    area *= 0.5
    if abs(area) < 1e-6:
        return None
    cx /= (6.0 * area)
    cy /= (6.0 * area)
    return UV(cx, cy)


def is_near_rectangle(uv_points, tol_ratio=0.95):
    """Check if polygon fills most of its bounding box (rectangle-ish)."""
    xs, ys = [p.U for p in uv_points], [p.V for p in uv_points]
    minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
    bb_area = (maxx - minx) * (maxy - miny)
    if bb_area < 1e-6:
        return False

    # Shoelace polygon area
    poly_area = 0.0
    for i in range(len(uv_points)):
        j = (i+1) % len(uv_points)
        poly_area += uv_points[i].U * uv_points[j].V - uv_points[j].U * uv_points[i].V
    poly_area = abs(poly_area) / 2.0

    return (poly_area / bb_area) >= tol_ratio


def get_room_center(room, transform):
    """Compute stable center point for tagging (ignores off-center refs)."""
    try:
        opts = SpatialElementBoundaryOptions()
        boundaries = room.GetBoundarySegments(opts)
        if not boundaries:
            return None

        uv_points, z_vals = [], []
        for blist in boundaries:
            for seg in blist:
                c = seg.GetCurve()
                p0, p1 = c.GetEndPoint(0), c.GetEndPoint(1)
                uv_points.append(UV(p0.X, p0.Y))
                z_vals.extend([p0.Z, p1.Z])

        # Compute shoelace centroid
        centroid_uv = compute_2d_centroid(uv_points)
        if not centroid_uv:
            return None
        avg_z = sum(z_vals) / len(z_vals)
        centroid = XYZ(centroid_uv.U, centroid_uv.V, avg_z)
        centroid = transform.OfPoint(centroid)

        # Compute bounding box center
        bb = room.get_BoundingBox(None)
        if bb:
            bb_center = (bb.Min + bb.Max) / 2.0
            bb_center = transform.OfPoint(bb_center)

            # Rectangle-ish override
            if is_near_rectangle(uv_points):
                return bb_center

            # Distance-based override
            tol = max(0.05 * math.sqrt(room.Area), 0.5)
            if centroid.DistanceTo(bb_center) > tol:
                return bb_center

        return centroid
    except Exception as ex:
        output.print_md("⚠️ Error computing center for Room {}: {}".format(room.Id, ex))
        return None


def get_all_linked_room_centers():
    results = []
    for linkInst in FilteredElementCollector(doc).OfClass(RevitLinkInstance):
        linkDoc = linkInst.GetLinkDocument()
        if not linkDoc:
            continue
        transform = linkInst.GetTotalTransform()
        rooms = FilteredElementCollector(linkDoc).OfCategory(
            BuiltInCategory.OST_Rooms).WhereElementIsNotElementType()
        for room in rooms:
            if not isinstance(room, SpatialElement):
                continue
            center = get_room_center(room, transform)
            if center:
                results.append((room, center))
    return results


def find_nearest_room_center(tag_pt, room_centers):
    nearest = None
    min_dist = 1e9
    for (room, center) in room_centers:
        d = tag_pt.DistanceTo(center)
        if d < min_dist:
            min_dist = d
            nearest = center
    return nearest, min_dist


# ---------------- Main ----------------

tags = FilteredElementCollector(doc, active_view.Id) \
    .OfCategory(BuiltInCategory.OST_RoomTags) \
    .WhereElementIsNotElementType()

room_centers = get_all_linked_room_centers()
output.print_md("### Found {} room tags".format(tags.GetElementCount()))

t = Transaction(doc, "Recenter Room Tags (Centroid + Rectangle Override)")
t.Start()

moved, skipped = 0, 0
for tag in tags:
    try:
        loc = tag.Location
        if not isinstance(loc, LocationPoint):
            continue

        tag_pt = loc.Point
        nearest, dist = find_nearest_room_center(tag_pt, room_centers)
        if not nearest:
            continue

        delta = nearest - tag_pt
        if dist > 0.001:
            orig_pt = loc.Point
            ElementTransformUtils.MoveElement(doc, tag.Id, delta)

            if tag.HasLeader:  # rollback if leader appears
                back_delta = orig_pt - loc.Point
                ElementTransformUtils.MoveElement(doc, tag.Id, back_delta)
                tag.HasLeader = False
                skipped += 1
            else:
                moved += 1
    except Exception as ex:
        output.print_md("⚠️ Error on tag {}: {}".format(tag.Id, ex))
        continue

t.Commit()

output.print_md("### ✅ Room Tags Recentered: {}".format(moved))
output.print_md("### ❌ Room Tags Skipped: {}".format(skipped))
output.print_md("### ℹ️ Skipped tags were ones that would have landed outside their rooms and caused leaders")

1 Like