Temporary Graphical Link (Mechanical Equipment to Thermostats)

i am working on creating a script with the idea being to show some sort of visual between terminal units (mechanical equipment) and thermostats (communication devices) that share the same ‘ID Instance’ text shared parameter". We use this instead of the Mark parameter. so essentially, when the user turns on the script, it shows an analytical line, like a duct system where Revit displays graphical warning markers and web lines, or something graphical that is temporary so that when the user exits the script, nothing is shown anymore.

I am essentially aiming to create a temporary graphical link (analytical line, similar visual cue, screen overlay, model line) between mechanical equipment and thermostats that share a custom ID Instance parameter.

i keep getting this message and i am not sure how to get around it.

“A transaction or sub-transaction was opened but not closed. All changes to the active document made by External Command will be discarded.”

this is what my script looks like so far

import clr
clr.AddReference("RevitServices")
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

clr.AddReference("RevitAPI")
clr.AddReference("RevitAPIUI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *

# Get document and active view
uidoc = __revit__.ActiveUIDocument
if uidoc is None:
    TaskDialog.Show("Error", "No active Revit document open.")
    raise Exception("No ActiveUIDocument found.")

doc = uidoc.Document
view = doc.ActiveView

# --- SETTINGS ---
ID_PARAM_NAME = 'ID Instance'
LINE_STYLE_NAME = '<Wide Lines>'

# --- HELPER: Get parameter value ---
def get_id_value(elem):
    param = elem.LookupParameter(ID_PARAM_NAME)
    if param and param.HasValue:
        return param.AsString()
    return None

# --- HELPER: Project point to 2D plane ---
def project_to_plane(pt, ref_z):
    return XYZ(pt.X, pt.Y, ref_z)

# --- COLLECT ELEMENTS ---
mech_elems = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_MechanicalEquipment)\
    .WhereElementIsNotElementType().ToElements()

thermo_elems = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_CommunicationDevices)\
    .WhereElementIsNotElementType().ToElements()

# --- INDEX MECHANICAL EQUIPMENT BY ID ---
mech_by_id = {}
for m in mech_elems:
    id_val = get_id_value(m)
    if id_val:
        mech_by_id.setdefault(id_val, []).append(m)

# --- ESTABLISH Z-PLANE REFERENCE ---
ref_z = 0.0
try:
    if hasattr(view, 'SketchPlane') and view.SketchPlane:
        ref_z = view.SketchPlane.GetPlane().Origin.Z
    elif hasattr(view, 'Origin'):
        ref_z = view.Origin.Z
except:
    ref_z = 0.0

# --- CREATE LINES ---
lines_to_draw = []
for t in thermo_elems:
    id_val = get_id_value(t)
    if id_val and id_val in mech_by_id:
        t_loc = t.Location
        if not isinstance(t_loc, LocationPoint):
            continue
        t_point = project_to_plane(t_loc.Point, ref_z)
        for m in mech_by_id[id_val]:
            m_loc = m.Location
            if not isinstance(m_loc, LocationPoint):
                continue
            m_point = project_to_plane(m_loc.Point, ref_z)
            line = Line.CreateBound(t_point, m_point)
            lines_to_draw.append(line)

# --- FIND <Wide Lines> STYLE ---
graphics_styles = FilteredElementCollector(doc).OfClass(GraphicsStyle).ToElements()
wide_line_style = None
for gs in graphics_styles:
    if gs.GraphicsStyleCategory.Name == LINE_STYLE_NAME and gs.GraphicsStyleType == GraphicsStyleType.Projection:
        wide_line_style = gs
        break

if not wide_line_style:
    TaskDialog.Show("Error", "Could not find line style: " + LINE_STYLE_NAME)
    raise Exception("Missing graphics style.")

# --- DRAW TEMPORARY LINES ---
detail_lines = []
TransactionManager.Instance.EnsureInTransaction(doc)
for line in lines_to_draw:
    try:
        detail_line = doc.Create.NewDetailCurve(view, line)
        detail_line.LineStyle = wide_line_style
        detail_lines.append(detail_line)
    except Exception as e:
        print("Skipping line: {}".format(str(e)))
TransactionManager.Instance.TransactionTaskDone()

# --- SAFE OK-ONLY DIALOG (PREVENTS CANCEL) ---
td = TaskDialog("Links Drawn")
td.MainInstruction = "Temporary lines shown"
td.MainContent = "Click OK to remove them."
td.CommonButtons = TaskDialogCommonButtons.Ok
td.DefaultButton = TaskDialogResult.Ok
td.Show()

# --- REMOVE TEMPORARY LINES ---
TransactionManager.Instance.EnsureInTransaction(doc)
for dl in detail_lines:
    doc.Delete(dl.Id)
TransactionManager.Instance.TransactionTaskDone()

any suggests on how to get this working? Code changes? I feel like it is 99% of the way there.


so after playing around with it more, I have got to this far

import clr
clr.AddReference("RevitServices")
from RevitServices.Persistence import DocumentManager

clr.AddReference("RevitAPI")
clr.AddReference("RevitAPIUI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *

clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
import System.Windows.Forms as WinForms
import System.Drawing

# Get doc + view
uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
view = doc.ActiveView

ID_PARAM_NAME = 'ID Instance'
LINE_STYLE_NAME = '<Wide Lines>'

def get_id_value(elem):
    param = elem.LookupParameter(ID_PARAM_NAME)
    if param and param.HasValue:
        return param.AsString()
    return None

def project_to_plane(pt, ref_z):
    return XYZ(pt.X, pt.Y, ref_z)

# --- COLLECT ELEMENTS ---
mech_elems = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_MechanicalEquipment)\
    .WhereElementIsNotElementType().ToElements()

thermo_elems = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_CommunicationDevices)\
    .WhereElementIsNotElementType().ToElements()

# --- INDEX MECHANICAL BY ID ---
mech_by_id = {}
for m in mech_elems:
    id_val = get_id_value(m)
    if id_val:
        mech_by_id.setdefault(id_val, []).append(m)

# --- ESTABLISH Z REF ---
ref_z = 0.0
try:
    if hasattr(view, 'SketchPlane') and view.SketchPlane:
        ref_z = view.SketchPlane.GetPlane().Origin.Z
    elif hasattr(view, 'Origin'):
        ref_z = view.Origin.Z
except:
    ref_z = 0.0

# --- FIND LINE STYLE ---
graphics_styles = FilteredElementCollector(doc).OfClass(GraphicsStyle).ToElements()
wide_line_style = None
for gs in graphics_styles:
    if gs.GraphicsStyleCategory.Name == LINE_STYLE_NAME and gs.GraphicsStyleType == GraphicsStyleType.Projection:
        wide_line_style = gs
        break

if not wide_line_style:
    WinForms.MessageBox.Show("Could not find line style: " + LINE_STYLE_NAME)
    raise Exception("Missing graphics style.")

# --- CREATE LINES ---
lines_to_draw = []
for t in thermo_elems:
    id_val = get_id_value(t)
    if id_val and id_val in mech_by_id:
        t_loc = t.Location
        if not isinstance(t_loc, LocationPoint):
            continue
        t_point = project_to_plane(t_loc.Point, ref_z)
        for m in mech_by_id[id_val]:
            m_loc = m.Location
            if not isinstance(m_loc, LocationPoint):
                continue
            m_point = project_to_plane(m_loc.Point, ref_z)
            line = Line.CreateBound(t_point, m_point)
            lines_to_draw.append(line)

# --- DRAW TEMPORARY DETAIL LINES ---
detail_lines = []
t = Transaction(doc, "Draw Temporary Lines")
t.Start()
for line in lines_to_draw:
    try:
        detail_line = doc.Create.NewDetailCurve(view, line)
        detail_line.LineStyle = wide_line_style
        detail_lines.append(detail_line)
    except Exception as e:
        print("Skipping line: {}".format(str(e)))
t.Commit()

now I am wondering if its possible to delete the lines within the same script?

1 Like

You can make a tadkdialog using

forms.alert("delete")

Then

t.RollBack()

which will cancel the transaction once you validated the form

1 Like

Thanks for the suggestion. This led me to exploring some other ideas and i got it working.

1 Like