Tag Clash Detection

I am working on a script to detect clashes between tags and modeled elements. One struggle I have come across are duct and pipe tags. These types of tags have this imaginary reference leader that always point to the duct and pipe and therefore gets detected as a clash between whatever falls on that path of this line. what could be done to avoid this imaginary line from being detected as a leader?

this is boundary box around tags i am generating to compare for clashes

here is the bit of code i am working on:

# pyRevit script
# Detect Tag overlap with modeled elements in active view
# Highlights overlapping tags in bright red
# Now uses Tag's bounding box with vertical shrink to exclude ghost leaders
# User is prompted to select categories to compare against from actual model content

__title__ = 'Detect Tag Overlaps'

from Autodesk.Revit.DB import *
from pyrevit import revit, DB, forms, output
from System.Collections.Generic import List
from System import Enum


doc = revit.doc
uidoc = revit.uidoc
view = doc.ActiveView

# Start output
out = output.get_output()

# Get all elements in view and find their categories
element_collector = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType()
category_map = {}

for el in element_collector:
    cat = el.Category
    if cat and cat.Id.IntegerValue < 0:  # Built-in categories only
        if cat.Name not in category_map:
            category_map[cat.Name] = cat.BuiltInCategory

if not category_map:
    forms.alert("No valid model categories found in view.", title="Tag Overlap Detection", warn_icon=True)
    script.exit()

# Prompt user to select from only categories in the view
selected_names = forms.SelectFromList.show(sorted(category_map.keys()), title="Select Categories to Clash With Tags", multiselect=True)

if not selected_names:
    forms.alert("No categories selected. Exiting.", title="Tag Overlap Detection", warn_icon=True)
    script.exit()

selected_categories = [category_map[name] for name in selected_names]

# Collect all tags in active view
tag_collector = FilteredElementCollector(doc, view.Id)\
    .OfClass(IndependentTag)\
    .WhereElementIsNotElementType()

tags = tag_collector.ToElements()

# Create LogicalOrFilter for selected categories
category_filters = [ElementCategoryFilter(cat) for cat in selected_categories]
multi_category_filter = LogicalOrFilter(category_filters)

# Collect model elements matching selected categories
model_collector = FilteredElementCollector(doc, view.Id)\
    .WhereElementIsNotElementType()\
    .WherePasses(multi_category_filter)

models = model_collector.ToElements()

def bounding_boxes_intersect(bb1, bb2):
    """Check if two bounding boxes intersect in 2D (XY plane)."""
    if not bb1 or not bb2:
        return False

    # Check X overlap
    if bb1.Max.X < bb2.Min.X or bb1.Min.X > bb2.Max.X:
        return False
    # Check Y overlap
    if bb1.Max.Y < bb2.Min.Y or bb1.Min.Y > bb2.Max.Y:
        return False

    return True

def get_filtered_tag_boundingbox(tag, view, shrink_factor=0.1):
    """Get bounding box of tag and shrink Z axis to ignore ghost leader effects."""
    bb = tag.get_BoundingBox(view)
    if not bb:
        return None

    min_pt = bb.Min
    max_pt = bb.Max

    # Shrink Z axis tightly (ignore long ghost lines vertically)
    new_min = XYZ(min_pt.X, min_pt.Y, (min_pt.Z + max_pt.Z) / 2.0 - shrink_factor)
    new_max = XYZ(max_pt.X, max_pt.Y, (min_pt.Z + max_pt.Z) / 2.0 + shrink_factor)

    new_bb = BoundingBoxXYZ()
    new_bb.Min = new_min
    new_bb.Max = new_max

    return new_bb

# Store overlaps
overlapping_tags = []

for tag in tags:
    if not isinstance(tag, IndependentTag):
        continue

    tag_bb = get_filtered_tag_boundingbox(tag, view)
    if not tag_bb:
        continue

    for model in models:
        model_bb = model.get_BoundingBox(view)
        if not model_bb:
            continue

        if bounding_boxes_intersect(tag_bb, model_bb):
            overlapping_tags.append((tag, model))

# Prepare override settings
highlight_ogs = OverrideGraphicSettings()
highlight_ogs.SetProjectionLineColor(Color(255, 0, 0))  # Bright red lines
highlight_ogs.SetCutLineColor(Color(255, 0, 0))          # Red cut lines
highlight_ogs.SetSurfaceForegroundPatternColor(Color(255, 0, 0))  # Red surface hatch
highlight_ogs.SetHalftone(False)

# Report
if overlapping_tags:
    out.print_md("## Overlapping Tags Found:\n")

    # Start a Transaction for highlighting
    with revit.Transaction("Highlight Overlapping Tags"):
        for tag, model in overlapping_tags:
            # Report
            out.print_md("Tag: {} overlaps with Model: {}".format(
                out.linkify(tag.Id),
                out.linkify(model.Id)
            ))
            out.print_md("---")

            # Highlight the tag
            view.SetElementOverrides(tag.Id, highlight_ogs)

else:
    forms.alert("No overlapping tags detected!", title="Tag Overlap Detection", warn_icon=False)```

Hi Ralph, this is what I used for overlapping dimensions, it dont believe it would be any different using the bounding box intersection filter:

def is_space_free_for_dimension(doc, line, view):
    start_pt = line.GetEndPoint(0)
    end_pt = line.GetEndPoint(1)
    min_point = DB.XYZ(min(start_pt.X, end_pt.X), min(start_pt.Y, end_pt.Y), min(start_pt.Z, end_pt.Z))
    max_point = DB.XYZ(max(start_pt.X, end_pt.X), max(start_pt.Y, end_pt.Y), max(start_pt.Z, end_pt.Z))
    outline = DB.Outline(min_point, max_point)
    intersecting_dimensions = (
        DB.FilteredElementCollector(doc, view.Id)
        .OfCategory(DB.BuiltInCategory.OST_Dimensions)
        .WherePasses(DB.BoundingBoxIntersectsFilter(outline))
        .WhereElementIsNotElementType()
        .ToElements()
    )
    return len(intersecting_dimensions) == 0

def offset_dimension_line(line, offset_value, perp_dir):
    """
    Offsets the dimension line in the direction perpendicular to the wall.
    """
    return line.CreateTransformed(DB.Transform.CreateTranslation(perp_dir.Multiply(offset_value)))

def find_free_space_for_dimension(doc, original_line, perp_dir, view):
    line = original_line
    offset_multiplier = 1
    max_attempts = 10  # Prevent infinite loops
    while not is_space_free_for_dimension(doc, line, view) and offset_multiplier < max_attempts:
        offset_multiplier += 1
        line = offset_dimension_line(original_line, OFFSET_MULTIPLIER_INCREMENT * offset_multiplier, perp_dir)
        logger.debug("Offsetting dimension line by {}mm to find free space.".format(OFFSET_MULTIPLIER_INCREMENT * offset_multiplier))
    if offset_multiplier >= max_attempts:
        logger.warning("Max attempts reached for finding free space for dimension.")
        return None
    return line

There’s an autodesk forum thread about this if you want to dive into that.
They suggest moving the tag to the host point and getting the bounding box in a temporary transaction
I assume you’d have to translate that bounding box back to where the tag is actually positioned.

They mention having to remove any rotations of the tag before doing this, which is probably true, but you might be able to get away with not doing that depending on how your duct/pipe/cabletray/conduit tags are built (rotate with component).

Tag Width/Height or Accurate BoundingBox of IndependentTag - Autodesk Community

1 Like

Thanks for sharing that thread. It got me a hell of a lot closer to the end goal. Still got a few bugs to work out but this was super helpful.

So i got a bit further into the script. i am encountering clashes between tags and duct elbows and i feel this has to do with trying to get the projection line of the curve:

what would i use to be able to get the boundary of the curve?

I would project the centerline + half the diameter in the same direction of the vector of the connection.
You can also use geometry instead of bounding boxes.
I only use bounding boxes in special cases (like labels) but no with parametric elements that have a complete face/mesh/geometry.

Using the element geometry is a good idea. It is much slower than computing bounding box intersections, though. I would recommend first testing for bounding box intersection, then throw out all the elements that don’t intersect by bounding box. Then, out of the remaining pool of elements, you can test for intersection by geometry.