Пришла пора практиковаться. В этой статье посмотрим, как с помощью кода в Питоне определить толщину стенки у воздуховода и записать её в указанный параметр. Сначала обсудим алгоритм и начальные условия, а потом напишем код. В коде будем работать со словарями, функциями и Ревит АПИ.

Алгоритм работы

Для начала давайте определим, что влияет на толщину стенки.

Первое — это форма воздуховода, для кругляка и прямоуголки это будут немного разные алгоритмы. Овальные рассматривать смысла не вижу, так как это, как правило, пластиковые трассы, нас же интересует только металл.

Второе — это наличие изоляции, особенно огнезащитной. Соответственно, такие воздуховоды до каких-то пределов независимо от своего размера будут толще, чем без огнезащиты. При этом изоляция возможна ещё и тепловая, она на стенку не влияет.

Естественно, нам нужен какой-то источник данных. Если брать приложение К к СП 60.13330.2020, то там информация вот такая:

На основе этих данных составим словарь из ключей — сторон воздуховода, и значений — толщины стали. Вы можете скорректировать словарь под нужные вам значения. При этом примем, что воздуховод в огнезащите всегда будет толщиной 0.9 мм, однако для круглых воздуховодов с диаметром от 900 и выше, надо будет снова обращаться к таблице из СП, так как у них толщина должна быть выше по конструктивным соображениям.

Алгоритм в целом вижу таким.

  1. Пишем функцию для определения формы воздуховода.
  2. Пишем функцию для определения стороны воздуховода. У круглого это будет просто диаметр, у прямоугольного — большая сторона.
  3. Создаём словарь со значениями сторон и толщин стенки.
  4. Пишем функцию для определения толщины стенки.
  5. Пишем функцию для получения вида изоляции.
  6. Собираем из проекта все воздуховоды.
  7. Определяем форму и сторону. Получаем толщину стенки.
  8. Проверяем, есть ли изоляция и какая она. Если есть огнезащита, то сверяем, какая толщина больше — 0,9 мм или та, что получили ранее. Принимаем большую толщину.
  9. Пишем стенку в параметры воздуховодов.
  10. Чисто в теории можем дописывать информацию в наименование воздуховода.

Давайте пробовать. За основу буду брать свою модель вентиляции детского садика, которую собирал в прямых эфирах. Можете поискать плейлист на моих видеохостингах.

Код

Я буду писать код следующим образом: создам скрипт в Динамо, но сам код будут подгружать из текстового файла, потому что писать его буду в VS Code. Можете почитать статьи о том, как всё установить и настроить у меня в блоге:

Этот способ удобен тем, что кодить будем в удобной среде с нормальной подсветкой кода. В самом Динамо писать код — это боль. Выглядеть эта заготовка будет вот так:

Беру нод с указанием пути к файлу, подаю файл на нод для чтения текста в файле, а потом подаю на нод, который будет выполнять питоновский код из поданного текста. По сути это такая костыльная система отладки, чтобы проверять код на ходу, но при этом не копипастить его каждый раз.

Загрузка библиотек

Тут всё стандартно, как в прошлой статье:

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

# тут будет наш код

TM.Instance.EnsureInTransaction(doc) # Открытие транзакции

### Действия внутри транзакции ###

TM.Instance.TransactionTaskDone() # Закрытие транзакции

Функция: форма воздуховода

Тут всё довольно просто в том плане, что цель функция весьма очевидная. Но есть нюанс: как именно мы будем её определять.

У любого воздуховода есть соединители, а у них есть форма. Это встроенные параметры, они есть всегда. Можно определять по ним.

При этом у воздуховодов есть так же и встроенные параметры, например имя типа, Диаметр, Ширина и Высота. По идее, если у воздуховода есть параметр «Диаметр», то он круглый. Если нет, то прямоугольный или овальный. Получается, что тоже неидеально, так как потом надо ещё понять, овальный он или нет.

Имя типа у воздуховодов тоже системно задано, правда, тут есть влияние локализации Ревита, она не обязательно должна быть на русском или английском. Поэтому тут параметр более зыбкий.

Так что из всех вариантов первый — самый надёжный. Покажу, как это сделать.

Как добраться до коннекторов воздуховода и их свойств? Для этого посмотрим на воздуховод через Ревит Лукап. Выделяем любой воздуховод, делаем Snoop Selection, ищем строку ConnectorManager.

Коннектор Менеджер — это объект, который содержит внутри себя все соединители элемента. У него есть свойство Connectors, это такой типизированный список с соединителями — ConnectorSet. Мы в него зайдём и возьмём соединитель. А уже у соединителя есть параметр с формой.

У любого воздуховода всегда только два соединителя и они одной формы. Значит, нам подходит вообще любой соединитель. Ну всё, пишем код функции.

Так код выглядит в VS Code
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"

Объявляю функцию и даю ей имя duct_shape. В скобках — переменная duct, в неё будем подавать воздуховоды.

Дальше небольшой трюк. Так как у нас коннекторы хранятся в объекте ConnectorSet, то для обращения по индексам мне нужно преобразовать его в список. Поэтому пишу list(), а в скобках duct.ConnectorManager.Connectors → то есть беру воздуховод, с него получаю Коннектор Менеджер, а с менеджера коннекторы. И так как мне подойдёт любой соединитель воздуховода из двух, то беру первый попавшийся через обращение по индексу [0].

Строкой connector_shape = duct_connector.Shape получаю форму соединителя.

Вот дальше нифига не очевидный момент. Когда я получаю форму коннектора, я получаю её не текстом или числом (в зависимости от версии Ревита там разные значения), а специальным типом данных — ConnectorProfileType. Как я это узнал? Я навёл мышку в Ревит Лукапе на параметр Shape и увидел это во всплывающем меню.

Поэтому формирую свои условные выражения и форму коннектора пишут в формате ConnectorProfileType.Round, где после точки идёт форма. Уже не помню, как и когда нашёл такой способ, но иногда оно вот так неочевидно работает.

При том что даже Динамо нам вернёт тут текст, если проведём операцию не в функции, а просто над рандомным воздуховодом. Но если посмотреть на тип хранения объекта, то увидим, что это не просто текст. Вот такая путаница.

duct_connector = list(all_ducts[0].ConnectorManager.Connectors)[0]
connector_shape = duct_connector.Shape

OUT = connector_shape

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

И в зависимости от формы воздуховода возвращаю три значения: round для круглых, rect — для прямоугольных, oval — для всех остальных, то есть овальных. Rect — это от слова rectangular, прямоугольный, не от слова ректальный.

Чтобы сразу проверить, работает ли функция, давайте получим воздуховоды из проекта и применим функцию к рандомному воздуховоду. Делаю это так:

all_ducts = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_DuctCurves).WhereElementIsNotElementType().ToElements()

OUT = duct_shape(all_ducts[0])

Собираю коллектором все воздуховоды из проекта. Это будет список. А далее беру первый попавшийся через индекс [0] и отправляю в функцию. Переменная OUT выведет мне результат из Питон-скрипта. Вот такая система отладки получается.

Функция: определяющая сторона воздуховода

Тут всё просто. Беру воздуховод, беру предыдущую функцию по определению стороны. Если воздуховод круглый, то беру у него параметр «Диаметр» и значение из него. Если нет, то определяю значения из «Ширина» и «Высота» и беру наибольшее. Так как параметры встроенные, то буду их брать по внутренним именам. Как такое делать, показывал в шестой статье цикла.

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)
		if w >= h:
			size = w 
		else: size = h
	
	return size

В данном случае выражение size = round(size * 304.8, 2) — это перевод в мм из футов и удаление дробных хвостов.

Вот эту запись в Питоне:

if w >= h:
    size = w 
else: size = h

можно укоротить до одной строки: size = w if w >= h else h. Это такой короткий формат записи условного выражения. Однако сильно им не балуйтесь, он усложняет чтение кода.

Функция: определение вида изоляции

В шаблонах АДСК я делал скрипт, в котором воздуховоды с изоляцией, чьё имя содержит текст «Огнезащита», автоматически считались как воздуховоды в огнезащите. Это нормальный способ, но он накладывает ограничение на имя типа изоляции. Поэтому сейчас думаю, что для этого лучше использовать какой-нибудь встроенный параметр изоляции, в котором будет указано, огнезащитный это материал или нет.

Пусть для примера это будет параметр «Группа модели», он же «Модель» в Ревитах 2025+.

Надо дать возможность пользователю указать толщину стенки воздуховода при наличии на нём огнезащиты. Удобнее сделать вход в Питон-скрипт с числовым значением, которое пользователь может вбить в Проигрывателе Динамо.

Логика будет такая: берём воздуховод, проверяем наличии изоляции на нём. Если изоляции нет, то функция возвращает False. Если есть, то проверяем параметр «Группа модели» («Модель» в 2025+). Если он содержит текст «Огнезащита», то возвращаем True. В остальных случаях тоже False.

Чтобы получить изоляцию с воздуховода, воспользуемся методом GetDependentElements(). В скобки нужно подать фильтр, чтобы вернулись только элементы нужного вида. Возвращаться будет список.

Функция выглядит так:

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

Мне нужна только изоляция с воздуховода. Поэтому фильтр создам через ElementMulticategoryFilter(), но вместо нескольких категорий подам только одну. Как создавать такие фильтры, показывал в предыдущей статье о коллекторах, так что подробно останавливаться не буду.

Далее создаю переменную insulation_id_list и получаю в неё списком зависимые элементы методом GetDependentElements(filter). В скобках тут как раз мой фильтр на одну категорию.

Если у воздуховода есть изоляция, то в списке она будет. Причём на воздуховоде может быть только одна изоляция, поэтому можно взять индексом самый первый элемент и не проходится циклом по всему списку. Далее получаю тип, заглядываю в параметр, ищу там слово «Огнезащита».

Если нашёл — возвращаю из функции истину, True. Если список пустой, то условие вернёт False, поэтому и в выход функции тоже подаю False.

Словарь с толщинами стенок

Про словари рассказывал в отдельной статье, поищите в рубрикаторе блога, если не знаете, что это.

По сути сейчас перебью данные из СП в формате словаря. Для кругляка и для прямоуголки буду делать разные словари. Форму я узнаю легко благодаря функции, а если делать единый словарь, то придётся как-то разделять кругляк и прямоуголку, скорее всего это будут текстовые ключи, а значит надо будет ещё и сторону воздуховода переводить в текст. Зачем эта морока, проще сделать два словаря.

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
}

Чтобы получить значение из словаря, надо обратиться методом dict.get(ключ). Позже увидим.

При этом для определения, к какому диапазону относится реальная сторона воздуховода, мне нужны списки только ключей, то есть размеров, без толщин стенок. Ключи списком я могу получить следующим выражением, на примере словаря для круглых воздуховодов: dict_round_duct.keys()

Особенность словарей такая, что данные в них перемешиваются. Если выдать этот список на выход Питона, то увидим следующее:

Хоть я и писал всё по порядку, в итоге Питон перемешал данные. Для работы словаря это не страшно, а для нас неудобно. Поэтому дополнительно применяю функцию sorted() для списка. В итоге все значения упорядочатся от меньшего к большему: sorted(dict_round_duct.keys()).

round_keys = sorted(dict_round_duct.keys())
rect_keys = sorted(dict_rect_duct.keys())

Определение толщины стенки

У нас есть функции, чтобы определить форму и сторону воздуховода, наличие огнезащиты, есть таблица для определения толщины стенки. Осталось понять, к какому диапазону из СП относится сторона воздуховода, чтобы получить стенку из словаря. Потом сравнить с толщиной в огнезащите, если она есть, и выбрать большую.

Сперва создам два нода для ввода данных и две переменные в Питоне для них. И получу все воздуховоды из проекта через коллектор элементов. Значения толщин стенки хочу собрать в отдельный список, его буду выводить из Питон-скрипта, чтобы потом пользователь мог при желании изучить список обработанных воздуховодов и толщин стенок для них.

Так как внутри цикла по анализу воздуховодов мы будем записывать в каждый воздуховод его толщину стенки, то перед циклом надо открыть транзакцию.

Далее создаю цикл For, о нём тоже была статья, и буду пробегаться по каждому воздуховоду:

  1. Получу функцией большую сторону воздуховода.
  2. Получу форму воздуховода.
  3. Создам переменную для толщины и приравняю к нулю. Потом буду менять это значение в зависимости от результатов анализа.
  4. Потом анализ: если воздуховод круглый, то сверяю его размер со списком круглых воздуховодов. Если размер воздуховода окажется меньше или равен какому-то значению из списка, значит беру это значение из списка и по нему из словаря получаю толщину стенки.
  5. Если прямоугольный, то тоже самое, но для списка прямоугольных.
  6. Далее беру огнезащиту. Если она есть, то сравниваю толщину стенки из словаря и толщину для воздуховода в огнезащите. Если стенка из словаря больше, то оставляю её, если нет, то беру толщину для воздуховода в огнезащите.
  7. Всё, пишу стенку в воздуховод. Финиш.

В коде это выглядит так, давайте разбирать код пошагово. Открываю транзакцию.

Создаю цикл и в нём получаю большую сторону в переменную real_size. В переменную shape — форму воздуховода. Переменная thickness равна нулю.

Затем делаю условное выражение if shape == "round":, то есть проверяю, круглый ли воздуховод. Если да, то делаю цикл. В нём я захожу в список круглых воздуховодов из словаря. Каждый диаметр там соответствует какой-либо толщине стенки из СП. С этими значениями я и сравниваю диаметр реального воздуховода из модели.

Как только реальный диаметр окажется меньше или равен диаметру из СП, значит, это и есть наша искомая сторона и соответственно стенка. В этом случае приравниваю данное значение в переменную thickness. И дальше прерываю выполнение цикла ключевым словом break.

Вот пример. Пусть диаметр воздуховода из модели будет 630 мм. Это соответствует диапазону от 500 до 800 включительно и толщине стенки 0,7 мм.

Что происходит внутри цикла. Питон берёт список диаметров круглых воздуховодов (200, 450, 800, 1250, 1600, 2000) и сравнивает диаметр реального воздуховода с этими значениями. Сначала условие не выполняется, так как 630 <= 200 логически неверное выражение. То же с 450 мм, а вот для 800 мм условие выполняется, так как 630 <= 800 мм.

Значит, моему воздуховоду соответствует диапазон с максимальной стороной 800 мм, а это толщина 0,7 мм. Эту толщину мы и пишем в переменную thickness.

Фишка в том, что если цикл не завершить, то мы получим неверное значение стенки. Ведь дальше будет идти 1250 мм и условие 630 <= 1250 тоже выполняется. Все последующие значения будут всегда больше, чем 630, а значит, условие в цикле будет выполняться. Если не остановить цикл после первого «попадания», то Питон пробежится по всему списку до конца, и мы всегда будет получать значение стенки для воздуховода стороной 2000 мм вместо правильного.

Именно поэтому используем прерывание цикла ключевым словом break. Как только условие выполнилось, действие внутри цикла выполняется, а после цикл завершается.

В результате выполнения этого куска кода мы получаем толщину по СП без учёта огнезащиты. Её мы проверим позже.

Ровно то же самое надо сделать для прямоугольных воздуховодов:

В итоге этой проверки получаем толщину по СП для круглых и прямоугольных воздуховодов. Осталось посмотреть, что там с огнезащитой.

Так как функция по огнезащите возвращает истину или ложь, то сразу формирую условное выражение с функцией. Если у воздуховода есть огнезащита, то вернётся истина, условие выполнится. В этом случае проверяем, что больше: толщина стенки по СП или та толщина, что должна быть минимальной для воздуховода в огнезащиту. Это значение пользователь укажет сам перед запуском скрипта. Обычно это 0,9 мм.

Если толщина для огнезащиты больше, чем толщина по СП, то берём толщину для огнезащиты. Если толщина по СП уже больше, чем толщина для огнезащиты, то оставляем ту толщину, что была.

И дальше закидываю толщину в список толщин, чтобы потом вывести его пользователю.

Теперь осталось записать стенку в параметр воздуховода, имя параметр указывает пользователь. Важно, чтобы это был параметр экземпляра с типом данных «Длина», иначе будет неприятно.

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

Если писать в числовой параметр, то делить на 304.8 не надо.

После записи вне цикла закрываю транзакцию и вывожу список всех воздуховодов и толщин в списки Динамо. Если какой-то воздуховод не попал в обработку, например у него сторона больше 2000 мм, то толщина запишется ему нулевая. Это будет индикатором, что надо что-то делать. Можно отфильтровать такие воздуховоды в спецификации Ревита и проверять в модели.

Итоги

В этой статье мы с вами:

  1. Поработали с функциями в Питоне. Применяли циклы FOR и условные выражения IF-ELIF-ELSE.
  2. Использовали словари.
  3. Получили зависимые элементы, в частности изоляцию с воздуховодов.
  4. Добавили новенькое — ключевое слово break в цикле.
  5. Вспомнили работу с коллектором элементов.
  6. Записали данные в элементы модели внутри транзакции.
  7. Собрали готовое решение для записи толщины стенки воздуховода.

Итоговый код ровно на 100 строк выглядит так:

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

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 = []

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)

TM.Instance.TransactionTaskDone() # Закрытие транзакции

OUT = all_ducts, duct_thickness_list