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))
