Automatic Dimension for Pipes / Ducts and Cable Tray

Hello i have a problem with my script it doesnt work… perfect.

Can anyone help me?

Below is a brief summary of what the script is designed to do:

  • Purpose:
    The script automates the dimensioning process in Revit 2025 (from a plan view) by associating system elements (pipes, ducts, and cable trays) with selected load-bearing walls.
  • How It Works:
    For each system element:
    • It obtains the center point from the element’s location curve (the axis).
    • It projects this point onto the nearest wall face to determine a reference point on the wall.
    • For rectangular ducts and cable trays, it subtracts half of the element’s width from the axis—in the direction from the wall to the element—to compute the exact wall-side outer edge. (For round pipes, the axis is used directly.)
    • It then creates a short detail line at the wall reference point and another at the computed outer edge.
    • Finally, it groups these detail lines by wall and sets up dimensions (either as individual or chain dimensions) based on the detail lines’ midpoints.
  • Goal:
    The aim is to dimension the distance from the wall directly to the outer edge of the duct (or cable tray) without any extra offset—resulting in an accurate measurement that reflects the true external boundary of the channel.

This approach ensures that, in plan view, ducts and cable trays are dimensioned based on their actual wall-side edge (calculated as the axis minus half the width), while pipes are dimensioned using their axis.

and This is my script for Pyrevit:

# -*- coding: utf-8 -*-
import clr
import math
from collections import defaultdict
from Autodesk.Revit.DB import (
    FilteredElementCollector, BuiltInCategory, Transaction, Line, DetailCurve,
    Dimension, XYZ, HostObjectUtils, ShellLayerType, ReferenceArray,
    OverrideGraphicSettings, Color, Options
)
from Autodesk.Revit.UI import TaskDialog

doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument

# Umrechnungsfaktor: Revit speichert intern in ft (1 ft = 304.8 mm)
FT_TO_MM = 304.8
MM_TO_FT = 1.0 / 304.8

def is_parallel_xy(vec1, vec2, max_angle_deg=2.0):
    """Prüft, ob zwei Vektoren in der XY-Ebene einen Winkel < max_angle_deg haben."""
    v1 = XYZ(vec1.X, vec1.Y, 0)
    v2 = XYZ(vec2.X, vec2.Y, 0)
    if v1.GetLength() < 1e-9 or v2.GetLength() < 1e-9:
        return False
    dot = v1.DotProduct(v2) / (v1.GetLength()*v2.GetLength())
    dot = max(-1.0, min(1.0, dot))
    angle_deg = math.degrees(math.acos(dot))
    return (angle_deg <= max_angle_deg or abs(angle_deg-180) <= max_angle_deg)

def get_nearest_wall_face(wall, point):
    """Ermittelt die Wandfläche (Innen oder Außen), die dem Punkt am nächsten liegt."""
    faces = []
    try:
        ext_refs = HostObjectUtils.GetSideFaces(wall, ShellLayerType.Exterior)
        for r in ext_refs:
            face_obj = doc.GetElement(r).GetGeometryObjectFromReference(r)
            if face_obj:
                faces.append(face_obj)
    except:
        pass
    try:
        int_refs = HostObjectUtils.GetSideFaces(wall, ShellLayerType.Interior)
        for r in int_refs:
            face_obj = doc.GetElement(r).GetGeometryObjectFromReference(r)
            if face_obj:
                faces.append(face_obj)
    except:
        pass
    nearest_face = None
    min_dist = float('inf')
    for f in faces:
        proj = f.Project(point)
        if proj:
            d = (proj.XYZPoint - point).GetLength()
            if d < min_dist:
                min_dist = d
                nearest_face = f
    return (nearest_face, min_dist) if nearest_face else (None, None)

def get_wall_direction(wall):
    """Liefert den normalisierten Richtungsvektor der Wand (aus der Location.Curve)."""
    return wall.Location.Curve.Direction.Normalize()

def create_detail_line(view, start_pt, end_pt):
    """Erzeugt eine DetailLine zwischen start_pt und end_pt; setzt die Linienfarbe auf hellgrau."""
    t = Transaction(doc, "Create Detail Line")
    t.Start()
    try:
        curve = Line.CreateBound(start_pt, end_pt)
        detail_line = doc.Create.NewDetailCurve(view, curve)
    except Exception as e:
        TaskDialog.Show("Fehler", "DetailLine-Erstellung fehlgeschlagen:\n{}".format(e))
        t.RollBack()
        return None
    t.Commit()
    t2 = Transaction(doc, "Set DetailLine Override")
    t2.Start()
    try:
        ogs = OverrideGraphicSettings()
        ogs.SetProjectionLineColor(Color(230,230,230))
        view.SetElementOverrides(detail_line.Id, ogs)
    except Exception as e:
        TaskDialog.Show("Fehler", "SetElementOverrides fehlgeschlagen:\n{}".format(e))
        t2.RollBack()
    t2.Commit()
    return detail_line

def create_2d_dimension(view, line1, line2, results):
    """Erstellt eine 2D-Bemaßung zwischen den Mittelpunkten zweier DetailLines."""
    ref_array = ReferenceArray()
    try:
        ref_array.Append(line1.GeometryCurve.Reference)
        ref_array.Append(line2.GeometryCurve.Reference)
    except Exception as e:
        results["errors"].append("Referenzen nicht ermittelbar: {}".format(e))
        return
    mid1 = line1.GeometryCurve.Evaluate(0.5, True)
    mid2 = line2.GeometryCurve.Evaluate(0.5, True)
    if (mid2 - mid1).GetLength() < 1e-3:
        results["errors"].append("Einzelbemaßung abgebrochen: Distanz zu klein.")
        return
    dim_line = Line.CreateBound(mid1, mid2)
    t = Transaction(doc, "Create 2D Dimension")
    t.Start()
    try:
        doc.Create.NewDimension(view, dim_line, ref_array)
        results["count"] += 1
    except Exception as e:
        results["errors"].append("Einzelbemaßung fehlgeschlagen: {}".format(e))
    t.Commit()

def create_chain_dimension(view, detail_lines, results):
    """Erzeugt eine Kettenbemaßung (von der ersten zur letzten DetailLine)."""
    if len(detail_lines) < 2:
        return
    ref_array = ReferenceArray()
    for dl in detail_lines:
        try:
            ref_array.Append(dl.GeometryCurve.Reference)
        except Exception as e:
            results["errors"].append("Chain-Referenzproblem: {}".format(e))
    first_mid = detail_lines[0].GeometryCurve.Evaluate(0.5, True)
    last_mid = detail_lines[-1].GeometryCurve.Evaluate(0.5, True)
    if (last_mid - first_mid).GetLength() < 1e-3:
        results["errors"].append("Kettenbemaßung abgebrochen: Dimension-Linie zu kurz.")
        return
    dim_line = Line.CreateBound(first_mid, last_mid)
    t = Transaction(doc, "Create Chain Dimension")
    t.Start()
    try:
        doc.Create.NewDimension(view, dim_line, ref_array)
        results["count"] += 1
    except Exception as e:
        results["errors"].append("Chain-Dimension fehlgeschlagen: {}".format(e))
    t.Commit()

def create_detail_lines_for_wall_and_elem(view, wall, elem):
    """
    Erzeugt für eine Wand und ein Systemelement (Pipe, Duct, Cable Tray) jeweils eine DetailLine.
    
    - Für Pipes wird der exakte Achsenmittelpunkt verwendet.
    - Für Ducts und Cable Trays wird der halbe Width-Wert (in ft) ermittelt und vom Achsenmittelpunkt in Richtung der Wand subtrahiert.
    
    Rückgabe: (wall_dl, elem_dl)
    """
    # Ermittele den Mittel­punkt der Elementachse (in 3D) und projiziere auf XY (View-Ebene)
    loc_curve = elem.Location.Curve
    if not loc_curve:
        return None, None
    p_mid_3d = loc_curve.Evaluate(0.5, True)
    view_z = view.Origin.Z
    axis_center = XYZ(p_mid_3d.X, p_mid_3d.Y, view_z)
    
    # Bestimme den nächstgelegenen Wandpunkt anhand der Elementachse
    face, _ = get_nearest_wall_face(wall, p_mid_3d)
    if not face:
        return None, None
    proj = face.Project(p_mid_3d)
    if not proj:
        return None, None
    wall_pt = XYZ(proj.XYZPoint.X, proj.XYZPoint.Y, view_z)
    
    # Berechne den Vektor von der Wand zum Achsenmittelpunkt
    vec = axis_center - wall_pt
    if vec.GetLength() < 1e-9:
        unit_vec = XYZ(0,0,0)
    else:
        unit_vec = vec.Normalize()
    
    cat_name = elem.Category.Name.lower()
    if "pipe" in cat_name:
        # Für Pipes: Verwende den Achsenmittelpunkt
        final_pt = axis_center
    elif "duct" in cat_name or "kanal" in cat_name or "cable" in cat_name:
        # Für Ducts/Kabeltrassen: Subtrahiere half width in Richtung Wand
        width_param = elem.LookupParameter("Width")
        if width_param and width_param.AsDouble():
            half_width = width_param.AsDouble() / 2.0  # in ft (intern)
            final_pt = axis_center - unit_vec.Multiply(half_width)
        else:
            final_pt = axis_center
    else:
        final_pt = axis_center

    # Erzeuge Wandreferenz-DetailLine: an der projizierten Wand (wall_pt)
    wall_dl = create_detail_line(view, wall_pt, wall_pt + get_wall_direction(wall).Multiply(0.1))
    # Erzeuge Element-DetailLine: an final_pt (die errechnete Außenkante)
    elem_dl = create_detail_line(view, final_pt, final_pt + get_wall_direction(wall).Multiply(0.1))
    
    return wall_dl, elem_dl

def main():
    TaskDialog.Show("Info", "Starte Skript: Automatische Bemaßung – Halbe Width von Ducts abziehen (Draufsicht)")
    view = doc.ActiveView
    view_z = view.Origin.Z
    
    selected_ids = uidoc.Selection.GetElementIds()
    if not selected_ids:
        TaskDialog.Show("Hinweis", "Bitte tragende Wände auswählen.")
        return
    
    # Wandselektion: Nur tragende Wände
    selected_walls = []
    for el_id in selected_ids:
        wall = doc.GetElement(el_id)
        if wall and wall.Category:
            if ("wände" in wall.Category.Name.lower() or "walls" in wall.Category.Name.lower()):
                if str(wall.StructuralUsage).lower() == "bearing":
                    selected_walls.append(wall)
    if not selected_walls:
        TaskDialog.Show("Hinweis", "Keine tragende Wand gefunden.")
        return
    
    # Systemelemente: Pipes, Ducts und Cable Trays im aktiven View
    pipes = list(FilteredElementCollector(doc, view.Id)
                 .OfCategory(BuiltInCategory.OST_PipeCurves)
                 .WhereElementIsNotElementType().ToElements())
    ducts = list(FilteredElementCollector(doc, view.Id)
                 .OfCategory(BuiltInCategory.OST_DuctCurves)
                 .WhereElementIsNotElementType().ToElements())
    cable_trays = list(FilteredElementCollector(doc, view.Id)
                 .OfCategory(BuiltInCategory.OST_CableTray)
                 .WhereElementIsNotElementType().ToElements())
    system_elements = pipes + ducts + cable_trays
    if not system_elements:
        TaskDialog.Show("Hinweis", "Keine Rohre, Kanäle oder Kabeltrassen im View gefunden.")
        return
    
    results = {"count": 0, "errors": []}
    chain_map = defaultdict(list)
    for elem in system_elements:
        if not hasattr(elem.Location, "Curve"):
            continue
        loc_curve = elem.Location.Curve
        if not loc_curve:
            continue
        p_mid_3d = loc_curve.Evaluate(0.5, True)
        cat_name = elem.Category.Name.lower()
        tol = 2.0
        if "duct" in cat_name or "kanal" in cat_name or "cable" in cat_name:
            tol = 5.0
        best_wall = None
        min_dist = float('inf')
        for wall in selected_walls:
            if not is_parallel_xy(wall.Location.Curve.Direction, loc_curve.Direction, max_angle_deg=tol):
                continue
            face, dist = get_nearest_wall_face(wall, p_mid_3d)
            if face and dist < min_dist:
                min_dist = dist
                best_wall = wall
        if not best_wall:
            continue
        wall_dl, elem_dl = create_detail_lines_for_wall_and_elem(view, best_wall, elem)
        if not wall_dl or not elem_dl:
            continue
        # Gruppiere nach Wand (verwende den projizierten Wandpunkt als Schlüssel)
        face, _ = get_nearest_wall_face(best_wall, p_mid_3d)
        proj = face.Project(p_mid_3d)
        if not proj:
            continue
        wall_pt = XYZ(proj.XYZPoint.X, proj.XYZPoint.Y, view_z)
        key = (best_wall.Id, round(wall_pt.X, 2), round(wall_pt.Y, 2))
        chain_map[key].append((elem_dl, wall_dl))
    
    for key, tup_list in chain_map.items():
        if len(tup_list) < 1:
            continue
        if len(tup_list) == 1:
            elem_dl, wall_dl = tup_list[0]
            create_2d_dimension(view, elem_dl, wall_dl, results)
        else:
            wall_elem = doc.GetElement(key[0])
            wall_dir = get_wall_direction(wall_elem)
            base_pt = tup_list[0][0].GeometryCurve.Evaluate(0.5, True)
            tup_list.sort(key=lambda tup: (tup[0].GeometryCurve.Evaluate(0.5, True) - base_pt).DotProduct(wall_dir))
            elem_dl_list = [tup[0] for tup in tup_list]
            create_chain_dimension(view, elem_dl_list, results)
    
    msg = "Es wurden {} Dimensionen/Kettenbemaßungen erstellt.".format(results["count"])
    if results["errors"]:
        msg += "\n\nFehler:\n" + "\n".join(results["errors"])
    TaskDialog.Show("Ergebnis", msg)

main()

I am experiencing an issue where the dimension for the outer edge of a channel is not being generated in my drawing. When I try to create a measurement for this edge, no dimension appears. This suggests that there might be an issue with the dimensioning setup or the way the channel’s outer edge is defined within the software.

Additionally, chain dimensions are not being created. When I attempt to generate a chain dimension (a series of connected measurements) in the drawing, nothing happens. I am seeking help to understand why neither the outer edge dimension nor the chain dimensions are being generated, and I would appreciate any guidance or solutions to resolve these issues.

Hi Patrick,

ill happily take a look at it if you can provide any demo project as I barely work with mep.

I have created a script for auto dimensioning floorplans that you might want to take a look at for chain dimensioning. https://discourse.pyrevitlabs.io/t/auto-dimension-floor-plans/2893/6

Hey, here is a Test Project for you: Unique Download Link | WeTransfer

The script now works with chain dimensions, which is great. However, I’d like to add an additional option to use a detail line as a reference instead of a wall. Also, when creating the chain dimension, it would be ideal to exclude dimension segments that result in a value of 0.

Is there a way to implement this in the script? Any ideas or suggestions would be very much appreciated. Thanks in advance!

Currently, the detail lines are working flawlessly as a selection. I would also like to avoid outputting a result of 0 in a chained dimension. Additionally, for my tool, I need to treat a specific layer named “Wand” as a wall when working with referenced DWGs in Revit. Could someone please help me with this?