Duct Splitting with Pyton

Hello everyone,

I am trying to create a script that splits rectangular ducts when they exceed 1500 mm, but it isn’t working. I get the following error:
Warning: Could not access Id of selected element – 'Line' object has no attribute 'Id'.

-- coding: utf-8 --

“”"
Cuts selected duct elements in the active Revit model if their length exceeds 1500mm.
Handles both LocationCurve and direct Line geometries for duct paths.
Includes ultra-robust error handling for unexpected input types.

Requires:

  • Revit 2023
  • Dynamo 2.13 (or higher, compatible with Revit 2023’s Python 3)

Input:

  • IN[0]: A list or single Revit element representing duct(s) to be checked and cut.

Output:

  • A list of processed duct element IDs.
  • A list of warning messages for ducts that couldn’t be processed or invalid selections.

Author: Your Name (or leave blank)
Date: 2025-04-29
Location: Mondercange, Esch-sur-Alzette, Luxembourg

import clr

# Import Revit API
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import Transaction, Line, XYZ, BuiltInCategory, ElementTransformUtils, CopyPasteOptions, TransactionStatus, LocationCurve
from Autodesk.Revit.DB.Mechanical import Duct

# Import Revit Services for Dynamo interaction
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

# Get the current Revit document
doc = DocumentManager.Instance.CurrentDBDocument

# Get the input element(s)
elements_to_process = UnwrapElement(IN[0]) if isinstance(IN[0], list) else [UnwrapElement(IN[0])] if IN[0] else []

# Define the threshold length in millimeters
max_length_mm = 1500.0

# Define potential parameter names for Length
length_param_names = ["Length", "Longueur"]

processed_duct_ids = []
warning_messages = []

# Start a new transaction
TransactionManager.Instance.EnsureInTransaction(doc)
transaction = None  # Initialize transaction

try:
    transaction = Transaction(doc, "Cut Long Ducts")
    for item in elements_to_process:
        if item is None:
            warning_messages.append("Warning: Received a None object as input. Skipping.")
            continue

        if isinstance(item, Line):
            warning_messages.append("Warning: Received a Line object as input (before Id check). Skipping.")
            continue

        try:
            element_id = item.Id.IntegerValue
            if isinstance(item, Duct):
                duct = item
                if duct and (duct.Category.BuiltInCategory == BuiltInCategory.OST_DuctCurves or duct.Category.BuiltInCategory == BuiltInCategory.OST_DuctFitting):
                    length_param = None
                    for name in length_param_names:
                        temp_param = duct.LookupParameter(name)
                        if temp_param:
                            length_param = temp_param
                            break  # Found a valid length parameter

                    if length_param:
                        try:
                            length_value = length_param.AsDouble()  # Length is stored in feet in Revit API
                            length_mm = length_value * 304.8  # Convert feet to millimeters

                            if length_mm > max_length_mm and duct.Category.BuiltInCategory == BuiltInCategory.OST_DuctCurves and hasattr(duct, 'MEPModel'):
                                location = duct.Location
                                curve = None

                                if isinstance(location, LocationCurve):
                                    curve = location.Curve
                                elif isinstance(location, Line):
                                    curve = location

                                if curve:
                                    total_length = curve.Length if isinstance(curve, Line) else curve.Length

                                    split_param = 0.5
                                    split_point = curve.Evaluate(split_param, True) if isinstance(curve, LocationCurve) else XYZ(
                                        (curve.GetEndPoint(0).X + curve.GetEndPoint(1).X) / 2.0,
                                        (curve.GetEndPoint(0).Y + curve.GetEndPoint(1).Y) / 2.0,
                                        (curve.GetEndPoint(0).Z + curve.GetEndPoint(1).Z) / 2.0
                                    )

                                    start_point = curve.GetEndPoint(0)
                                    end_point = curve.GetEndPoint(1)

                                    line1 = Line.CreateBound(start_point, split_point)
                                    line2 = Line.CreateBound(split_point, end_point)

                                    connectors = [c for c in duct.MEPModel.ConnectorManager.Connectors]
                                    if len(connectors) == 2:
                                        new_duct1_id = doc.Create.NewDuct(connectors[0], connectors[1])
                                        new_duct1 = doc.GetElement(new_duct1_id)
                                        new_duct1.Location.Curve = line1

                                        new_duct2_id = doc.Create.NewDuct(connectors[0], connectors[1])
                                        new_duct2 = doc.GetElement(new_duct2_id)
                                        new_duct2.Location.Curve = line2

                                        for param in duct.Parameters:
                                            if not param.IsReadOnly:
                                                try:
                                                    if new_duct1.LookupParameter(param.Definition.Name):
                                                        new_duct1.LookupParameter(param.Definition.Name).Set(param.AsValueString())
                                                    if new_duct2.LookupParameter(param.Definition.Name):
                                                        new_duct2.LookupParameter(param.Definition.Name).Set(param.AsValueString())
                                                except Exception as e_param:
                                                    warning_messages.append(f"Warning: Could not copy parameter '{param.Definition.Name}' from duct ID {duct.Id} - {e_param}")

                                        doc.Delete(duct.Id)
                                        processed_duct_ids.append(new_duct1_id)
                                        processed_duct_ids.append(new_duct2_id)
                                    else:
                                        warning_messages.append(f"Warning: Duct with ID {duct.Id} has {len(connectors)} connectors. Expected 2 for simple split.")
                                else:
                                    warning_messages.append(f"Warning: Duct with ID {duct.Id} has an unsupported location type for cutting.")

                            elif length_mm > max_length_mm and duct.Category.BuiltInCategory == BuiltInCategory.OST_DuctFitting and hasattr(duct, 'LookupParameter'):
                                warning_messages.append(f"Warning: Duct fitting with ID {duct.Id} exceeds {max_length_mm}mm and cannot be directly cut.")
                            else:
                                processed_duct_ids.append(duct.Id)  # Duct within limit or not a duct curve/fitting
                        except Exception as e:
                            warning_messages.append(f"Error processing duct with ID {element_id}: {e}")
                    else:
                        warning_messages.append(f"Warning: Selected element with ID {element_id} is not a valid duct or duct fitting.")
            else:
                warning_messages.append(f"Warning: Selected element with ID {element_id} is not a duct element and will be skipped.")
        except Exception as main_loop_error:
            if isinstance(item, Line):
                # This should ideally prevent the 'Line' error, but we're still seeing it.
                warning_messages.append(f"Warning: Could not access Id (again) - {main_loop_error}")
            else:
                warning_messages.append(f"Warning: Could not access Id of selected element - {main_loop_error}")

    if transaction and transaction.GetStatus() == TransactionStatus.Started:
        transaction.Commit()
except Exception as main_error:
    if transaction and transaction.GetStatus() == TransactionStatus.Started:
        transaction.RollBack()
    warning_messages.append(f"An unexpected error occurred: {main_error}")

# Assign output
OUT = (processed_duct_ids, warning_messages)

You’re passing the location curve to python instead of the actual duct element. You just need to supply the duct.

When doing that the outcome of “Watch” Is Empty List

are you passing a dynamo locationline?

lf yes then this doesn’t:
element_id = item.Id.IntegerValue

You need to show us. You need to be passing the element if you’re using duct methods from the API. Something else is likely the problem.