Help with Grid Bubble Clash Offset

I started to make this “Fix Grid Bubble” clash offset tool just now. I’d like to ask for help to make it work better. i am able to get the elbow added but not get the elbow moved after its been added. it stays at this point which is the default placement by clicking on the add elbow. I’d like to make it move further so it’s no longer clashing.

# -*- coding: utf-8 -*-
"""_________________________________________________________________
Description:
Detects clashing grid bubbles and resolves by offsetting
the bubble leader with an elbow jog.
Rules:
- For vertical grids → move the LEFTMOST bubble.
- For horizontal grids → move the TOPMOST bubble.
- Offset distance is auto-calculated based on the bubble size,
  view scale, and the actual overlap distance.
- Elbow is placed relative to the grid anchor point, so it
  always jogs outward instead of overlapping on the grid line.
_________________________________________________________________
"""

import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')

import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import TaskDialog
from Autodesk.Revit.UI.Selection import ObjectType, ISelectionFilter
from pyrevit import revit, forms
import sys

# Setup
doc = revit.doc
uidoc = revit.uidoc
view = doc.ActiveView


# ---------------- Selection Filter ----------------
class GridSelectionFilter(ISelectionFilter):
    def AllowElement(self, e):
        return e.Category.Id.IntegerValue == int(BuiltInCategory.OST_Grids)
    def AllowReference(self, ref, point):
        return True


# ---------------- Helpers ----------------
def get_bubble_point(grid, end, view):
    """Return bubble location via leader if it exists, else endpoint."""
    leader = grid.GetLeader(end, view)
    if leader:
        return leader.End
    else:
        return grid.Curve.GetEndPoint(0 if end == DatumEnds.End0 else 1)


def move_bubble(grid, clash_end, view, jog_offset, end_offset):
    """Move a grid bubble by adjusting its leader elbow + end."""
    if not grid.IsBubbleVisibleInView(clash_end, view):
        grid.ShowBubbleInView(clash_end, view)

    leader = grid.GetLeader(clash_end, view)
    if not leader:
        print("No leader found on {0}, adding one...".format(grid.Name))
        leader = grid.AddLeader(clash_end, view)

    if leader:
        anchor = leader.Anchor
        old_end = leader.End

        # Place elbow explicitly relative to anchor
        leader.Elbow = anchor + jog_offset

        # Move bubble outward from old end
        leader.End = old_end + end_offset

        print("Bubble for {0} moved. Anchor={1}, Elbow={2}, End={3}".format(
            grid.Name, anchor, leader.Elbow, leader.End))


def get_clash_end_and_points(g1, g2, view, bubble_radius):
    """Check both ends and return clash info if found."""
    for end in [DatumEnds.End0, DatumEnds.End1]:
        if not (g1.HasBubbleInView(end, view) and g1.IsBubbleVisibleInView(end, view)):
            continue
        if not (g2.HasBubbleInView(end, view) and g2.IsBubbleVisibleInView(end, view)):
            continue

        pt1_tmp = get_bubble_point(g1, end, view)
        pt2_tmp = get_bubble_point(g2, end, view)

        if pt1_tmp and pt2_tmp:
            dist = pt1_tmp.DistanceTo(pt2_tmp)
            print("Distance between bubble centers at {0} = {1}".format(end, dist))

            if dist < (bubble_radius * 2.0):
                return True, end, pt1_tmp, pt2_tmp, dist
    return False, None, None, None, None


# ---------------- Main ----------------
try:
    refs = uidoc.Selection.PickObjects(ObjectType.Element, GridSelectionFilter(),
                                       "Select two grids with clashing bubbles")
    if len(refs) < 2:
        forms.alert("Please select at least two grids.", exitscript=True)

    grids = [doc.GetElement(r) for r in refs]
    g1, g2 = grids[0], grids[1]

    print("Two grids selected: {0} and {1}".format(g1.Name, g2.Name))

    # Bubble radius auto-scales with view
    BUBBLE_RADIUS_INCHES = 0.25  # 1/4" radius for 1/2" bubble diameter
    BUBBLE_RADIUS = (BUBBLE_RADIUS_INCHES / 12.0) * view.Scale
    bubble_diameter_ft = BUBBLE_RADIUS * 2.0

    print("Bubble diameter on sheet = {0}\"; in model space = {1} ft".format(
        BUBBLE_RADIUS_INCHES * 2.0, round(bubble_diameter_ft, 2)))

    clash_detected, clash_end, pt1, pt2, dist = get_clash_end_and_points(g1, g2, view, BUBBLE_RADIUS)

    if clash_detected:
        print("Selected grid bubbles clash detected at {0}".format(clash_end))

        with Transaction(doc, "Resolve Grid Bubble Clash") as t:
            t.Start()

            curve = g1.Curve
            pt0 = curve.GetEndPoint(0)
            pt1_curve = curve.GetEndPoint(1)
            dir_vec = (pt1_curve - pt0).Normalize()

            # Determine movement direction by grid orientation
            if abs(dir_vec.X) < 0.1:  # Vertical grids
                if pt1.X < pt2.X:
                    grid_to_move = g1
                else:
                    grid_to_move = g2
                move_dir = XYZ(-1, 0, 0)  # always left
            else:  # Horizontal grids
                if pt1.Y > pt2.Y:
                    grid_to_move = g1
                else:
                    grid_to_move = g2
                move_dir = XYZ(0, 1, 0)  # always up

            # Calculate overlap and move amount
            overlap = bubble_diameter_ft - dist if dist < bubble_diameter_ft else 0
            clearance = bubble_diameter_ft * 0.25  # 25% extra
            move_amount = overlap + clearance

            jog_offset = move_dir * (move_amount / 2.0)
            end_offset = move_dir * move_amount

            print("Moving bubble for {0} by {1} ft".format(grid_to_move.Name, round(move_amount, 2)))
            move_bubble(grid_to_move, clash_end, view, jog_offset, end_offset)

            t.Commit()

        forms.alert("Clash resolved: Bubble moved on {0}".format(grid_to_move.Name), title="Success")

    else:
        print("Selected grid bubbles do not clash.")
        forms.alert("No clash detected between selected grid bubbles.", title="Info")

except Autodesk.Revit.Exceptions.OperationCanceledException:
    print("User canceled operation.")
    sys.exit()
except Exception as ex:
    TaskDialog.Show("Error", "Warning: {0}".format(ex))
    print("Error: {0}".format(ex))

2 Likes

after exploring this some more, i was not able to move the anchor/elbows/leader endpoints.

This is what i came up with so far. If anyone knows how to take this further to make the bubbles move further apart to avoid overlaps, give it a whirl and share :upside_down_face:.

GridBubbleElbow_script.py

# -*- coding: utf-8 -*-
__title__ = "Fix Grid Bubble Overlaps (Active View)"
__doc__ = """Checks visible grid bubbles in the active view and, if overlaps are found,
adds a leader (elbow) to one of the grids to stagger bubbles apart.
If both already have leaders, the overlap is only reported.

Shift+Click this tool to configure the bubble radius (bubble_config.py).
"""

import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')

from Autodesk.Revit.DB import *
from pyrevit import revit, forms
import bubble_config   # shared config file

# Revit context
doc = revit.doc
view = doc.ActiveView


# ---------------- Helper: distance ----------------
def distance(pt1, pt2):
    return pt1.DistanceTo(pt2)


# ---------------- Main ----------------
def fix_grid_bubble_overlaps():
    # Load bubble radius from config (default = 0.25")
    bubble_radius_inches = bubble_config.load_radius()
    bubble_radius_ft = (bubble_radius_inches / 12.0) * view.Scale
    bubble_diameter_ft = bubble_radius_ft * 2.0

    grids = FilteredElementCollector(doc, view.Id).OfClass(Grid).ToElements()

    # Collect bubble centers
    bubble_points = []   # list of (grid, end, XYZ)
    for g in grids:
        curve = g.Curve
        for end in [DatumEnds.End0, DatumEnds.End1]:
            if g.HasBubbleInView(end, view) and g.IsBubbleVisibleInView(end, view):
                pt = curve.GetEndPoint(0) if end == DatumEnds.End0 else curve.GetEndPoint(1)
                bubble_points.append((g, end, pt))

    # Compare pairwise and fix overlaps
    overlaps = []
    with Transaction(doc, "Fix Grid Bubble Overlaps") as t:
        t.Start()
        for i in range(len(bubble_points)):
            g1, e1, p1 = bubble_points[i]
            for j in range(i + 1, len(bubble_points)):
                g2, e2, p2 = bubble_points[j]

                # Only compare bubbles at the same end type (End0 vs End0, End1 vs End1)
                if e1 != e2:
                    continue

                d = distance(p1, p2)
                if d < bubble_diameter_ft:
                    overlaps.append((g1.Name, g2.Name, str(e1), round(d, 3)))

                    # Try to add elbow intelligently
                    try:
                        g2.AddLeader(e2, view)
                    except:
                        try:
                            g1.AddLeader(e1, view)
                        except:
                            # both already had leaders → skip
                            pass

        t.Commit()

    # Report
    if overlaps:
        msg_lines = ["Bubble diameter per view scale ≈ {0:.2f} ft\nOverlaps fixed :".format(bubble_diameter_ft)]
        for g1, g2, end, d in overlaps:
            msg_lines.append(" - {0} and {1}".format(g1, g2))
        forms.alert("\n".join(msg_lines), title="Grid Bubble Elbows Added")
    else:
        forms.alert("No overlapping grid bubbles detected.", title="Grid Bubble Check")


# Run
fix_grid_bubble_overlaps()

This is the corresponding bubble_config.py

# -*- coding: utf-8 -*-
# bubble_config.py
# Configuration module for bubble radius
# Runs when user Shift+Clicks the tool button

from pyrevit import forms, script

cfg = script.get_config("grid_bubble_config")

DEFAULT_RADIUS = 0.25  # inches


def load_radius():
    """Load saved bubble radius or default"""
    return getattr(cfg, "bubble_radius_inches", DEFAULT_RADIUS)


def save_radius(val):
    """Save bubble radius"""
    cfg.bubble_radius_inches = val
    script.save_config()


def configure_radius():
    """Ask user to set bubble radius"""
    current = load_radius()
    s = forms.ask_for_string(
        prompt="Enter grid bubble's radius in inches as found in the family:",
        title="Grid Bubble Radius",
        default=str(current)
    )
    try:
        val = float(s)
        if val <= 0:
            raise ValueError
        save_radius(val)
        forms.alert("Bubble radius saved: {:.4f} in".format(val),
                    title="Grid Bubble Config")
    except:
        forms.alert("Invalid input. Keeping existing radius: {:.4f} in".format(current),
                    title="Grid Bubble Config")


# If Shift+Clicked directly, run configurator
if __name__ == "__main__":
    configure_radius()

1 Like