В этой статье расширим скрипт, который делали в седьмой статье, чтобы он работал не только с воздуховодами, но и с соединительными деталями. Будем работать как с элементами модели, так и с их соединителями.

Алгоритм работы
До этого мы уже определили толщину стенки воздуховодов. Так как воздуховоды — это «главные» элементы, у которых относительно легко определять толщину стенки, то толщину для фитингов будем брать с воздуховодов.
Для этого нам нужно получить все фитинги из проекта. А дальше искать все подключённые к ним воздуховоды. Нашли воздуховоды → взяли с них толщину стенки → определили наибольшую → записали в фитинги.
Сложность возникает в местах, где у нас несколько фитингов подряд, такое нередко случается в реальных системах. Ну или не несколько фитингов, а фитинг — арматура — фитинг — воздуховод. Вместо арматуры может быть и оборудование, например канальных вентилятор. Таким образом нам нужно перебирать элементы в системе, пока не найдём подходящий — воздуховод.
У нас возможны такие варианты:
- Фитинг со всех сторон подключён к воздуховодам. Тогда анализируем все толщины стенок воздуховодов и берём наибольшую.
- Фитинг подключен хотя бы с одной стороны к воздуховоду. Берём толщину с этого воздуховода. Тут могут быть накладки на таких элементах, как переходы. Например, с одной стороны размер 400х400 и там отвод, а к нему примыкает переход на 200х200, после которого идёт воздуховод. В нашем алгоритме толщина будет браться с воздуховода 200х200.
- Фитинг подключён с любой стороны не к воздуховодам. В этом случае надо брать подключённые элементы и искать подключённые к ним воздуховоды, а дальше выбирать наибольшую толщину.
Таким образом получается, что мы должны проверять каждый фитинг, а если фитинг подключён не к воздуховодам, то проверять ещё и подключённые к нему фитинги, чтобы через них перейти к воздуховодам. Будем делать для этого функцию с рекурсией, чтобы функция работала и «снаружи», и «внутри» себя.
Код
Открываю существующий код в VSCode, добавлю сюда ещё одну функцию для получения подключённых воздуховодов. Назову её find_duct_thickness().
В функцию подаю два аргумента — сам фитинги и пустой объект с None. Это нужно для последующей проверки фитингов, и тут нам придётся забежать вперёд, чтобы пояснить логику. Сам я с ней не справился, обращался к ИИ за помощью. Мой вариант кода приводил к вылету Ревита, без ошибок, без зависаний, Ревит просто закрывался и всё. Потому что получалась бесконечная рекурсия. Поэтому давайте с этим разбираться.
Если у нас фитинги всегда подключены хотя бы к одному воздуховоды, то никаких проверок можно не делать. В ходе обработки мы всегда выйдем на воздуховод, возьмём его стенку и готово. Если у нас соединены подряд три фитинга, то тут появляется простая рекурсия для среднего фитинга.
Мы берём средний фитинг, берём подключенный к нему фитинг. Далее той же функцией анализируем этот подключённый фитинг и у него находим воздуховод. Всё, берём стенку с него, цикл завершается. Тут не будет ошибки, потому что пусть и на втором уровне проверки, но мы нашли воздуховод.
А вот если у нас ситуация, в которой подряд подключены четыре и более фитинга, то тут мы попадаем в бесконечную рекурсию. Вот иллюстрация, пронумеровал фитинги. Первый и четвертый фитинги работают нормально, там есть воздуховоды.

Для 2 и 3 фитинги начинаются проблемы. Мы берём в функцию фитинг 2. У него нет воздуховодов, поэтому внутри функции подаём в эту же функцию подключенные фитинги, тут это 1 и 3. С фитингом 1 нет проблем, там рядом воздуховод. Но вот фитинг 3 нас подводит. Когда мы начинаем его анализ, то проверяем элементы, которые подключены к нему. Пусть мы сначала прыгаем на фитинг 4, тут всё нормально, нашли воздуховоды. Но потом мы прыгаем на фитинг 2 — тот самый, с которого начинали обработку.
В итоге мы бесконечно бегаем между 2 и 3 фитингом, так как рекурсия начинает обработку фитинга 3, с него перепрыгивает на фитинг 2, а с него снова на 3 и бесконечно. Память переполняется, Ревит вылетает.
Поэтому нужно добавить проверку обработанных фитингов, останавливать код, если фитинг ранее уже был в обработке. Отсюда и возникает объект None. В записи кода эта проверка выглядит так:

Строка duct_category_Id = ElementId(BuiltInCategory.OST_DuctCurves) содержит айди категории воздуховодов, это нам пригодится дальше.
Объявляю функцию, в ней два аргумента: фитинг для обработки и переменная visited, которая в момент начала равна None, ну типа пустой объект.
Дальше делаю условие. Если переменная visited является пустым объектом, тогда создаю в ней пустоt множество. Множество создаётся ключевым словом set и скобками. Фишка множества в том, что оно хранит в себе неповторяющиеся элементы в рандомном порядке. От рандомного порядка нам ни пользы, ни вреда, потому что множество это нужно, чтобы проверять, есть там наш элемент или нет. Поэтому порядок тут не важен.
И далее в это множество добавляем наш фитинг, который начали обрабатывать. Таким образом он исключается из повторной обработки внутри цикла. Добавляем не сам элемент, а айди, это более надёжный способ, с самими элементами напрямую работать не стоит.
Без такой проверки в ситуациях 4 и более соединённых элементах без воздуховода будем получать вылет Ревита в космос.
Далее создаю три переменные. В первую получу все соединители с фитинга. Во второй приравняю толщину фитинга нулю, а третьей будет переменная с пустым списком.

С помощью кода fitting.MEPModel.ConnectorManager.Connectors я беру фитинг, обращаюсь к его свойству MEPModel. Это свойство есть у всех инженерных элементов, которые являются загружаемыми семействами. У воздуховодов или труб такого параметра нет, так как это системные семейства. Далее захожу в свойство ConnectorManager, а оттуда в Connectors — в типизированный список, содержащий все соединители семейства.
Всё это можно посмотреть через Ревит Лукап. Коннектор Менеджер нужен, чтобы добраться до соединителей.

Соединители «сидят» внутри объекта ConnectorSet, мы можем залезать в него циклом for. Внутри цикла будем перебирать каждый соединитель и смотреть в его свойства.
В данном случае нас интересует свойство соединителя «AllRefs» — all references, все подключенные к данному соединителю соединители. Второе нужное нам свойство — Owner, то есть владелец соединителя. С его помощью мы с соединителя получаем его владельца, в нашем случае другой фитинг.



Переменная fitting_thickness нужна, чтобы в неё записывать толщину стенки и выводить её из функции. Список connected_elements нужен, чтобы в дальнешем класть в него подключенные фитинги и анализировать их.
Создаю цикл, в котором перебираю соединители: for connector in fitting_connectors
Получаю все подключённые соединители к соединителю нашего фитинга: all_refs = connector.AllRefs. Так как это тоже список, то создаю новый цикл внутри цикла. Внутри второго цикла беру каждый подключенный соединитель и получаю его владельца — элемент модели: ref_owner = ref_con.Owner.
Тут же проверяю, есть ли этот элемент в списке обработанных. Если айди элемента есть в множестве visited, то использую оператор continue. Если условие выполняется, то этот оператор завершает текущую итерацию и отправляет на следующую. То есть если элемент уже обрабатывался ранее, то цикл переходит к следующему подключённому соединителю и анализирует уже его. Это и есть наша защита от бесконечного перебора элементов.
Если же элемента ещё не было в обработке, нам нужно его проверить: это воздуховод или нет? Если это воздуховод, то беру из его параметра значение толщины стенки. Но этого может быть недостаточно, если к фитингу подключены несколько воздуховодов с разной толщиной стенки. Нам нужны выбрать наибольшую.
Именно поэтому ранее я создал параметр fitting_thickness и приравнял его к нулю. Внутри той ветки цикла, которая берёт толщину стенки с воздуховода, я буду сравнивать толщину стенки воздуховода и значение в параметре. Если толщина стенки больше, чем значение параметра, то приравниваем эту стенку в значение параметра. Каждая проходка по каждому подключённому воздуховоду будет брать с него стенку и сравнивать с тем, что уже есть в параметре. Если значение больше, то параметр будет обновляться.
Например, вот у нас есть тройник, к которому подключены три воздуховода. У одного стенка 0,5, у другого 0,7, у третьего 0,9 мм. Цикл берёт первый воздуховод, сравнивать стенку 0,5 и значение из параметра, которое в начале работы цикла равно нулю. 0,5 больше нуля, поэтому пишем в параметр fitting_thickness значение 0,5. Потом берём воздуховод с толщиной 0,9 мм. Это больше, чем 0,5, поэтому обновляем значение до 0,9. Потом берём воздуховод 0,7 мм, это меньше, чем 0,9, поэтому не меняем значение. На выходе получим максимум — 0,9 мм.

Проверяю, является ли воздуховодом наш подключенный элемент. Перед функцией создал параметр с айдишником категории воздуховодов. Поэтому беру с элемента категорию и её айди кодом ref_owner.Category.Id и сравниваю с айди категории. Если условие выполняется, то получаю с помощью метода LookupParameter() значение параметра. Его мы ранее добавляли для воздуховодов, для фитингов будет тот же. Это числовой параметр, поэтому добавляю метод AsDouble().
Строкой fitting_thickness = max(fitting_thickness, thickness) записываю в параметр стенку воздуховода, сравнивая её с текущим значением и выбираю максимальное.
Если элемент оказался не воздуховодом, то нужно закинуть его в наш пустой список connected_elements. При этом есть такой подвох: изоляция воздуховодов тоже цепляется на соединитель элемента и тоже является ссылкой, то есть может быть ситуация, когда мы добавим в список не подключённый элемент, а изоляцию самого фитинга. Тогда весь алгоритм пойдёт не туда.
Соответственно, нужно добавить проверку, чтобы в список попадали только загружаемые семейства. Мы не знаем, что там будет, там может быть другой фитинг, арматура, оборудование или воздухораспределитель. То есть это всё загружаемые категории. А вот изоляция или гибкие воздуховоды не нужны.
Поэтому делаю условие с проверкой isinstance(ref_owner, FamilyInstance). Функция isinstance() получает два аргумента — какой-то элемент и название класса. Если элемент принадлежит этому классу, то вернётся истина, если нет, то ложь. Например, если взять список spisok = [1, 2, 3] и проверить его функцией isinstance(spisok, list), то получим True, так как список — это объект класса list.
В нашем случае мы хотим получить загружаемые семейства. Они все объединяются классом FamilyInstance. В итоге после такой проверки мы засунем в список только загружаемые семейства вроде других фитингов или арматуры, а всякую изоляцию, гибкие воздуховоды отбросим.
После этого прошла первая ступень проверки. Если к фитингу подключён хотя бы один воздуховод, то с него получится стенка, если несколько — то выбирается самая толстая. Но вот если никаких воздуховодов не было подключено, то нужно идти дальше и проверять подключённые элементы, то есть идём на вторую ступень, на картинке ниже она с 76 строки.

Делаю условие. Если в результате всех проверок толщина так и не изменилась и осталась равной нулю, а так же в списке подключённых элементов что-то есть, то перебираю эти подключённые элементы. Кстати, толщину стенки изначально я записал в виде 0.0, чтобы тип хранения у меня был дробным, а не целым.
Если оба условия выполняются, то создаю цикл, которым залезаю в список подключённых к фитингу элементов. Далее использую конструкцию try—except, чтобы в случае ошибки просто пропускать элемент ключевым словом pass. Например, у элемента может быть параметр с толщиной стенки, но по типу вместо экземпляра. Такого быть не должно, но мало ли.
И вот тут начинается рекурсия. Беру снова параметр thickness и в нём применяю функцию, внутри которой мы сейчас находимся. Но уже подаём не изначальный фитинг, а подключённый к нему элементу. При этом копируем множество, чтобы оно не пересекалось с исходным множеством. Ну вот так ИИ говорит, типа надёжнее. Я запускал и с копированием и без, вроде работает одинаково.
Функция возьмёт подключённый элемент, проверит, что там подключёно уже к нему, если воздуховод — возьмёт стенку, если нет, то пойдёт проваливаться дальше. Потом вновь сравниваем полученную стенку с нашим параметром fitting_thickness и пишем в него значение, если оно больше текущего. При этом уже обработанные элементы пропускаются, в итоге Ревит не вылетает.
И на выходе из функции возвращаем нашу толщину стенки. Если по какой-то причине стенка не определится, то вернётся нулевое значение.
Всё функция готова, осталось получить из проекта все фитинги и обработать их. Код внутри функции большой, а вот обработка будет совсем небольшой, так как основную работу мы сделали внутри функции.
Мне нужно найти место, где мы уже обработали воздуховоды и записали в них толщину стенки. Так как программа выполняется последовательно, то нам важно, чтобы значение толщины уже было в параметре. Поэтому нахожу строчку, в которой записывал толщину стенки воздуховода. После неё начну обработку фитингов.
До этого нужно фитинги получить. Чтобы не делать это внутри транзакции, получу их перед ней. И заодно создам список, куда буду складывать полученные толщины стенок — так проще будет потом находить фитинги с нулевой толщиной, если такие будут.

Теперь внутри транзакции будут получать стенку и тут же её записывать в фитинг. Так как внутри функции я не переводил толщину стенки в мм из футов, то при записи в параметр не делю на 304,8. Ревит запишет значение во внутренних единицах — в футах, а потом сам сконвертирует значение для вывода в окне свойств. А вот внутри Динамо мне нужны миллиметры, а не футы, чтобы вывести значения в список. Там домножу на 304,8 и округлю до двух знаков.

В OUT вывожу две группы списков, в первой группе список из воздуховодов и их толщин стенок, во второй — список фитингов и их стенок.
Алгоритм работает, но проверка специалиста всё равно нужна, потому что местами работает не идеально, например вот на этом переходе всё же лучше делать толщину 0,7 мм. Конечно, можно получить размер наибольшего соединителя, сравнить его с таблицей для воздуховодов и писать значения оттуда, или делать отдельную проверку именно для переходов, но давайте это оставлю вам как домашнее творчество.

Итоги
В этой статье мы с вами:
- Создали функцию с рекурсией и внутренней защитой от бесконечной рекурсии.
- Расширили функционал существующего скрипта, чтобы он обрабатывал все элементы.
- Ну и всё, наверное.
Итоговый код на 144 строки выглядит так:
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager as DM
from RevitServices.Transactions import TransactionManager as TM
import System
from System.Collections.Generic import List
doc = DM.Instance.CurrentDBDocument
def duct_shape(duct):
duct_connector = list(duct.ConnectorManager.Connectors)[0]
connector_shape = duct_connector.Shape
if connector_shape == ConnectorProfileType.Round:
return "round"
elif connector_shape == ConnectorProfileType.Rectangular:
return "rect"
else: return "oval"
def bigger_duct_size(duct):
if duct_shape(duct) == "round":
size = duct.get_Parameter(BuiltInParameter.RBS_CURVE_DIAMETER_PARAM).AsDouble()
size = round(size * 304.8, 2)
else:
w = duct.get_Parameter(BuiltInParameter.RBS_CURVE_WIDTH_PARAM).AsDouble()
w = round(w * 304.8, 2)
h = duct.get_Parameter(BuiltInParameter.RBS_CURVE_HEIGHT_PARAM).AsDouble()
h = round(h * 304.8, 2)
size = w if w >= h else h
return size
def duct_insulation_influence(duct):
category = List[BuiltInCategory]([BuiltInCategory.OST_DuctInsulations])
filter = ElementMulticategoryFilter(category)
insulation_id_list = duct.GetDependentElements(filter)
if insulation_id_list:
insulation = doc.GetElement(insulation_id_list[0])
insulation_type = doc.GetElement(insulation.GetTypeId())
ins_model = insulation_type.get_Parameter(BuiltInParameter.ALL_MODEL_MODEL).AsString()
if "Огнезащита" in ins_model:
return True
else: return False
duct_category_Id = ElementId(BuiltInCategory.OST_DuctCurves)
def find_duct_thickness(fitting, visited = None):
if visited is None:
visited = set()
visited.add(fitting.Id)
fitting_connectors = fitting.MEPModel.ConnectorManager.Connectors
fitting_thickness = 0.0
connected_elements = []
for connector in fitting_connectors:
all_refs = connector.AllRefs
for ref_con in all_refs:
ref_owner = ref_con.Owner
if ref_owner.Id in visited:
continue
if ref_owner.Category.Id == duct_category_Id:
thickness = ref_owner.LookupParameter(param_duct_thickness).AsDouble()
fitting_thickness = max(fitting_thickness, thickness)
else:
if isinstance(ref_owner, FamilyInstance):
connected_elements.append(ref_owner)
if fitting_thickness == 0.0 and connected_elements:
for elem in connected_elements:
try:
thickness = find_duct_thickness(elem, visited.copy())
fitting_thickness = max(fitting_thickness, thickness)
except: pass
return fitting_thickness
dict_round_duct = {
200 : 0.5,
450 : 0.6,
800 : 0.7,
1250 : 1,
1600 : 1.2,
2000: 1.4,
}
dict_rect_duct = {
250 : 0.5,
1000 : 0.7,
2000 : 0.9
}
round_keys = sorted(dict_round_duct.keys())
rect_keys = sorted(dict_rect_duct.keys())
duct_fireprotect_thickness = IN[0]
param_duct_thickness = IN[1]
all_ducts = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_DuctCurves).WhereElementIsNotElementType().ToElements()
duct_thickness_list = []
all_duct_fittings = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_DuctFitting).WhereElementIsNotElementType().ToElements()
duct_fittings_thickness_list = []
TM.Instance.EnsureInTransaction(doc) # Открытие транзакции
for duct in all_ducts:
real_size = bigger_duct_size(duct)
shape = duct_shape(duct)
thickness = 0
if shape == "round":
for size in round_keys:
if real_size <= size:
thickness = dict_round_duct.get(size)
break
else:
for size in rect_keys:
if real_size <= size:
thickness = dict_rect_duct.get(size)
break
if duct_insulation_influence(duct):
if duct_fireprotect_thickness > thickness:
thickness = duct_fireprotect_thickness
duct_thickness_list.append(thickness)
duct.LookupParameter(param_duct_thickness).Set(thickness/304.8)
for fitting in all_duct_fittings:
thickness = find_duct_thickness(fitting)
fitting.LookupParameter(param_duct_thickness).Set(thickness)
duct_fittings_thickness_list.append(round(thickness*304.8, 2))
TM.Instance.TransactionTaskDone() # Закрытие транзакции
OUT = [all_ducts, duct_thickness_list], [all_duct_fittings, duct_fittings_thickness_list]



