Tracing electrical circuits through cable trays

Hello everyone, maybe someone can help. I am trying to write a script for laying electrical circuits on cable trays. But I can’t solve the problem with the diagonal of the chain segments, and for some reason, with the short lengths of the cable trays, it doesn’t build correctly.

When I try to script a circuit from SW1–>1, it builds like this

When I build a circuit from SW2–>4, it builds correctly.

But at the same time, if I add a small branch to the tray, I immediately get errors related to the diagonality of the chain segments. SW3–>5

Unfortunately, I can’t upload many screenshots, so I’m giving you a link to the file.

Revit 2022

Maybe someone has come across this and can help me, thank you.

# -*- coding: utf-8 -*-
from Autodesk.Revit.DB import (
    FilteredElementCollector,
    Transaction,
    BuiltInCategory,
    FamilyInstance,
    LocationCurve,
    XYZ,
    SpotDimension,
    Reference,
    View,
    Line,
)
from Autodesk.Revit.UI import TaskDialog
from pyrevit import revit, forms
import math


# Получение имени кабельной системы от пользователя через диалоговое окно
def get_tray_name_from_user():
    tray_name = forms.ask_for_string(
        title="Имя кабельной системы",
        prompt="Введите имя кабельной системы:",
        default=""
    )
    return tray_name


# Получение кабельных лотков по имени
def get_cable_trays_by_name(doc, tray_name):
    trays = FilteredElementCollector(doc) \
        .OfCategory(BuiltInCategory.OST_CableTray) \
        .WhereElementIsNotElementType() \
        .ToElements()

    return [tray for tray in trays if
            tray.LookupParameter("mS_Имя системы") and tray.LookupParameter("mS_Имя системы").AsString() == tray_name]


# Получение всех электрических цепей в проекте
def get_all_circuits(doc):
    return FilteredElementCollector(doc) \
        .OfCategory(BuiltInCategory.OST_ElectricalCircuit) \
        .WhereElementIsNotElementType() \
        .ToElements()


# Выбор электрических цепей через диспетчер инженерных систем
def select_circuits_from_list(doc):
    all_circuits = get_all_circuits(doc)
    circuit_names = [
        "{0} (ID: {1})".format(circuit.Name, circuit.Id.IntegerValue)
        for circuit in all_circuits
    ]

    selected_names = forms.SelectFromList.show(
        circuit_names,
        title="Выберите электрические цепи",
        multiselect=True
    )

    if not selected_names:
        return []

    # Найти соответствующие цепи по имени
    selected_circuits = [
        circuit for circuit in all_circuits
        if "{0} (ID: {1})".format(circuit.Name, circuit.Id.IntegerValue) in selected_names
    ]
    return selected_circuits


def interpolate_points(point1, point2):
    intermediate1 = XYZ(point1.X, point2.Y, point1.Z)
    intermediate2 = XYZ(point2.X, point2.Y, point1.Z)
    return [intermediate1, intermediate2]


def find_nearest_end(tray_curve, equipment_point):
    start = tray_curve.GetEndPoint(0)
    end = tray_curve.GetEndPoint(1)

    distance_to_start = equipment_point.DistanceTo(start)
    distance_to_end = equipment_point.DistanceTo(end)

    return start if distance_to_start < distance_to_end else end


# Получение оконечного оборудования
def get_load_equipment(circuit):
    for elem in circuit.Elements:
        if isinstance(elem, FamilyInstance):
            return elem
    return None


# Получение соединительных деталей кабельных лотков
def get_cable_tray_fittings(doc):
    return FilteredElementCollector(doc) \
        .OfCategory(BuiltInCategory.OST_CableTrayFitting) \
        .WhereElementIsNotElementType() \
        .ToElements()



# Прокладка цепи вдоль кабельного лотка с учетом соединительных деталей
def route_circuit_along_tray(circuit, tray, doc):
    tray_location = tray.Location
    if not isinstance(tray_location, LocationCurve):
        forms.alert("Ошибка: Кабельный лоток не содержит геометрической кривой.")
        return

    tray_curve = tray_location.Curve

    base_equipment = circuit.BaseEquipment
    if not base_equipment:
        forms.alert("Ошибка: Базовое оборудование цепи не найдено.")
        return

    # Используем местоположение оборудования напрямую (фиктивные соединители)
    if hasattr(base_equipment, "Location") and base_equipment.Location is not None:
        if hasattr(base_equipment.Location, "Point"):
            start_point = base_equipment.Location.Point
        else:
            forms.alert("Ошибка: Location базового оборудования не содержит точки.")
            return
    else:
        forms.alert("Ошибка: Не удалось получить Location оборудования.")
        return

    load_equipment = get_load_equipment(circuit)
    if not load_equipment:
        forms.alert("Ошибка: Оконечное оборудование цепи не найдено.")
        return

    # Используем местоположение оборудования напрямую (фиктивные соединители)
    if hasattr(load_equipment, "Location") and load_equipment.Location is not None:
        if hasattr(load_equipment.Location, "Point"):
            end_point = load_equipment.Location.Point
        else:
            forms.alert("Ошибка: Location оконечного оборудования не содержит точки.")
            return
    else:
        forms.alert("Ошибка: Не удалось получить Location оконечного оборудования.")
        return

    nearest_start = find_nearest_end(tray_curve, start_point)
    nearest_end = find_nearest_end(tray_curve, end_point)

    # Создаем список точек маршрута
    path_points = [start_point]

    # Добавляем промежуточные точки для корректного соединения
    # Разбиваем маршрут на горизонтальные и вертикальные сегменты
    intermediate_points = create_orthogonal_path(start_point, nearest_start)
    path_points += intermediate_points
    path_points.append(nearest_start)

    # Добавляем точки соединительных деталей
    fittings = get_cable_tray_fittings(doc)
    for fitting in fittings:
        if hasattr(fitting, "Location") and fitting.Location is not None:
            if hasattr(fitting.Location, "Point"):
                fitting_point = fitting.Location.Point
                # Проверяем, что точка находится между началом и концом лотка
                if (tray_curve.GetEndPoint(0).DistanceTo(fitting_point) < tray_curve.Length and
                        tray_curve.GetEndPoint(1).DistanceTo(fitting_point) < tray_curve.Length):
                    path_points.append(fitting_point)

    # Добавляем конечные точки
    path_points.append(nearest_end)
    intermediate_points = create_orthogonal_path(nearest_end, end_point)
    path_points += intermediate_points
    path_points.append(end_point)

    # Фильтруем точки, чтобы избежать дублирования
    min_distance = 1e-3
    filtered_points = [path_points[0]]
    for point in path_points[1:]:
        if point.DistanceTo(filtered_points[-1]) > min_distance:
            filtered_points.append(point)

    # Проверяем, что все сегменты строго горизонтальные или вертикальные
    for i in range(1, len(filtered_points)):
        prev_point = filtered_points[i - 1]
        curr_point = filtered_points[i]
        if not (math.isclose(prev_point.X, curr_point.X, abs_tol=1e-3) or
                math.isclose(prev_point.Y, curr_point.Y, abs_tol=1e-3)):
            # Выводим координаты точек, где обнаружен диагональный сегмент
            print("Обнаружен диагональный сегмент между точками:")
            print(f"Точка 1: X={prev_point.X}, Y={prev_point.Y}, Z={prev_point.Z}")
            print(f"Точка 2: X={curr_point.X}, Y={curr_point.Y}, Z={curr_point.Z}")

            forms.alert(f"Ошибка: Обнаружен диагональный сегмент между точками:\n"
                        f"Точка 1: X={prev_point.X}, Y={prev_point.Y}, Z={prev_point.Z}\n"
                        f"Точка 2: X={curr_point.X}, Y={curr_point.Y}, Z={curr_point.Z}")
            return

    try:
        with Transaction(doc, "Маршрутизация цепи") as t:
            t.Start()
            circuit.SetCircuitPath(filtered_points)
            t.Commit()
    except Exception as ex:
        forms.alert(f"Ошибка при установке пути цепи: {ex}")


def create_orthogonal_path(point1, point2):
    """
    Создает промежуточные точки для маршрута, чтобы он состоял только из горизонтальных и вертикальных сегментов.
    """
    intermediate_points = []

    # Горизонтальный сегмент
    if not math.isclose(point1.X, point2.X, abs_tol=1e-3):
        intermediate1 = XYZ(point2.X, point1.Y, point1.Z)
        intermediate_points.append(intermediate1)

    # Вертикальный сегмент
    if not math.isclose(point1.Y, point2.Y, abs_tol=1e-3):
        intermediate2 = XYZ(point2.X, point2.Y, point1.Z)
        intermediate_points.append(intermediate2)

    return intermediate_points


# Запуск кода
if __name__ == "__main__":
    tray_name = get_tray_name_from_user()
    if tray_name:
        doc = revit.doc
        trays = get_cable_trays_by_name(doc, tray_name)
        if not trays:
            forms.alert(f"Кабельные лотки с именем '{tray_name}' не найдены.")
        else:
            selected_circuits = select_circuits_from_list(doc)
            if not selected_circuits:
                forms.alert("Не выбраны электрические цепи.")
            else:
                for circuit in selected_circuits:
                    for tray in trays:
                        route_circuit_along_tray(circuit, tray, doc)