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

Оглавление цикла

Обработка нескольких элементов в Питоне

Напомню код из прошлой статьи, где мы собирали вместе соединители с тройника.

Elements = UnwrapElement(IN[0])

connset = Elements.MEPModel.ConnectorManager.Connectors

newlist = []

for con in connset:
    con_svoistvo = con.Shape
    newlist.append(con_svoistvo)

OUT = newlist

Теперь усложним ситуацию. Возьмём нод Select Model Elements — с его помощью можно выбрать сразу несколько элементов. Выберу тройник и переход — два загружаемых семейства с разным количеством соединителей. На данном этапе важно выбрать именно загружаемые семейства, потому что наш код работает только с ними. Дальше разберёмся и с этим.

Новый нод выделяет несколько элементов сразу

Получаю жёлтый нод Питона, и ошибку, что в четвёртой строке есть AttributeError: 'List[object]' object has no attribute 'MEPModel'. У списка — list — нет атрибута MEPModel. Список — это просто список, это не элемент модели, поэтому у него не может быть свойства MEPModel. Ошибка связана с тем, что теперь мы подаём не один элемент в обработку, а несколько элементов. Их несколько, поэтому программа собирает их в список.

Поэтому нужно залезть в список и обрабатывать элементы по очереди. За это отвечает всё тот же цикл for. Цепочка действий такая:

  1. Циклом for залезаем в список элементов, которые подаём в Питон-нод;
  2. Получаем набор соединителей с первого элемента списка. Это тоже список, поэтому нужен ещё один цикл.
  3. Вторым циклом for залезаем в набор соединителей и уже обрабатываем их — получаем свойства.
  4. Записываем значения свойств в специально созданный для этого пустой список.
  5. Список со свойствами записываем в ещё один пустой список, чтобы на выходе получить список со списком значений для каждого элемента.

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

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

Давайте реализуем это в коде. Я хочу собрать новый список с диаметрами каждого соединителя моих тройника и перехода. Сперва залезем в список наших элементов. Делаем цикл.

Elements = UnwrapElement(IN[0])

newlist = []

for element in Elements:
    connset = element.MEPModel.ConnectorManager.Connectors

Здесь element — новая переменная, в которую будем записывать наши элементы модели. Сперва цикл возьмёт тройник, обработает его, потом возьмёт переход и сделает с ним то же самое. Теперь в переменную connset подаю не Elements, потому что это список семейств, а отдельное семейство — переменную element. Пустой список newlist перенёс выше цикла, потому что это наш общий пустой список, в него потом будем складывать другие списки. Это наша большая коробка.

Таким образом добрался до набора соединителей одного семейства. Это тоже список, коробочка поменьше, значит нужно снова залезть в неё циклом. Поскольку во вложенном списке будет несколько элементов, то их нужно сложить в отдельную коробочку — создать новый пустой список до того, как циклом залезет в соединители. Пусть это будет новая переменная e_list.

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

Elements = UnwrapElement(IN[0])

newlist = []

for element in Elements:
    connset = element.MEPModel.ConnectorManager.Connectors

    e_list = []

    for con in connset:
        con_svoistvo = con.Radius*304.8*2
        e_list.append(con_svoistvo)

Взял все семейства, добрался до одного семейства, потом у него получил список соединителей, залез в соединители, получил свойство. Сложил значение свойства в отдельную маленькую коробочку. Теперь эту коробочку нужно упаковать в большую пустую коробку — в наш список newlist.

Здесь самое важное — где вы разместите команду. Всё, что происходит внутри цикла, сдвигается клавишей Таб или четырьмя пробелами. В итоге получается такая волнистая структура. Пока вы внутри отступа, операции проводятся внутри цикла. Если выйти наружу, то операция будет выполняться после цикла.

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

Это сложно для понимания через текст. Гораздо эффективнее усваивать на практике методом проб и ошибок, а также визуализацией через те же коробки. Если сейчас прочитали и не поняли, не огорчайтесь, я до сих пор путаюсь в этом, хотя вроде базовую логику понимаю.

Итак, наш итоговый код будет выглядеть так, в него я добавил только одну строку, в которой собираю списки со значениями в один общий список, ну и вывожу всё в OUT:

Elements = UnwrapElement(IN[0])

newlist = []

for element in Elements:
    connset = element.MEPModel.ConnectorManager.Connectors

    e_list = []

    for con in connset:
        con_svoistvo = con.Radius*304.8*2
        e_list.append(con_svoistvo)

    newlist.append(e_list)

OUT = newlist
Реализация кода в Динамо

Как видите, получаем список со вложенными списками, в которые записали значения диаметров каждого соединителя.

Обработка системных и загружаемых категорий вместе

Следующий случай — когда выделяем одновременно и системные категории и загружаемые. Например, я выберу и фитинги, и воздуховоды, и арматуру воздуховодов. В Питоне сразу получаю ошибку, что 'Duct' object has no attribute 'MEPModel'.

Ошибка связана с тем, что у системных категорий нет свойства MEPModel. Значит, нам нужно как-то разводить наши потоки кода. Это можно сделать разными способами.

Операторы try и except

Самый простой, но менее надёжный, — пара операторов try и except. Они работают так: если в коде, который идёт внутри try появляется ошибка, то Питон пробует выполнить тот код, что идёт после except. Не получилось одним способом, ну попробуем другим. Всё, что выполняется внутри этих операторов, нужно сдвигать табом или четырьмя пробелами правее операторов.

В нашем случае это сработает, потому что мы точно знаем, что именно подаём в Питон и какая ошибка ждёт впереди. Но в общем случае это не очень надёжно, потому что кто его знает, какие элементы могут прийти в Питон. Вдруг выделим семейство, у которого вообще нет свойства MEPModel ни в каком виде? Тогда оба варианта кода выдадут ошибку и всё.

Помним, что у системных категорий нет свойства MEPModel, мы можем сразу обращаться к Коннектор Менеджеру. Поэтому нам нужно по сути добавить эту пару операторов и скопировать с небольшими корректировками код. Получится вот так, после знака # в Питоне идут комментарии, это текст для человека, программа его не выполняет:

Elements = UnwrapElement(IN[0]) # список всех элементов, которые подаём в Питон

newlist = [] # общий пустой список для значений

for element in Elements: # обращаемся к конкретному семейству в списке
	try: #оператор, которым пробуем первый вариант кода
		connset = element.MEPModel.ConnectorManager.Connectors # первый вариант для обращения через MEPModel к загружаемым семействам
		
		e_list = [] # пустой список для значений диаметра
		
		for con in connset: # залезаем в набор соединителей
			con_svoistvo = con.Radius*304.8*2 # получаем диаметр с соединителя
			e_list.append(con_svoistvo) # записываем значение в список
	except: # вариант кода, если первый вариант выдал ошибку
		connset = element.ConnectorManager.Connectors # второй вариант для обращения к системным семействам
		
		e_list = []
		
		for con in connset:
			con_svoistvo = con.Radius*304.8*2
			e_list.append(con_svoistvo)		
	
	newlist.append(e_list) # записываем списки со значениями в общий список

OUT = newlist # выводим значения из Питон-нода
Работает

Теперь, когда Питон берёт воздуховод, натыкается на ошибку в первом варианте кода, он перебрасывает воздуховод на второй вариант кода, а там всё нормально.

Условные операторы if — else

Логика простая, это обычное условие ЕСЛИ и ИНАЧЕ. То же, что в Экселе или в формулах в семействах Ревита. Вопрос лишь в том, какое условие задать.

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

Скопирую нод через перетаскивание с зажатым Ctrl. И буду менять код.

Сперва разберёмся, как узнать имя категории элемента. Выделяем воздуховод, лезет в Ревит Лукап. Тут есть жирным Category, если нажать, то откроется окно со свойствами категории, одно из них — Name. То есть наш путь к имени — Element.Category.Name.

Свойства воздуховода в Ревит Лукапе
Проваливаюсь в свойства категории

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

Elements = UnwrapElement(IN[0]) # список всех элементов, которые подаём в Питон

newlist = [] # общий пустой список для значений

categs = ["Воздуховоды", "Гибкие воздуховоды", "Материалы изоляции воздуховодов", "Материалы внутренней изоляции воздуховодов"] # список системных категорий для вентиляции

for element in Elements: # обращаемся к конкретному семейству в списке
	
	e_categ = element.Category.Name
Если добавить внутри цикла строку newlist.append(e_categ) и в OUT подать newlist, то можно увидеть имена категорий

Теперь нужно проверить условие: если имя категории элемента находится в списке системных категорий, то выполняем вариант кода для системных категорий, а если вне списка, то вариант для загружаемых.

Сделать это можно с помощью такого кода: if element.Category.Name in categs. Здесь if — условный оператор, element.Category.Name — получение имени категории семейства, in — оператор для «заглядывания» в список, categs — мой список категорий, который создал вручную. Это выражение будет возвращаться true, если имя категории в списке и false, если нет.

Поэтому пишу вот такой код, обратите внимание, что теперь мы сперва обрабатываем системные категории, а потом загружаемые, поэтому варианты кода поменялись местами. Если не хотите таких преобразований, то можно вместо in использовать оператор not in — не в.

Elements = UnwrapElement(IN[0]) # список всех элементов, которые подаём в Питон

newlist = [] # общий пустой список для значений
categs = ["Воздуховоды", "Гибкие воздуховоды", "Материалы изоляции воздуховодов", "Материалы внутренней изоляции воздуховодов"] # список системных категорий для вентиляции

for element in Elements: # обращаемся к конкретному семейству в списке

    if element.Category.Name in categs: # проверяем, находится ли категория элемента в списке системных категорий
        connset = element.ConnectorManager.Connectors # второй вариант для обращения к системным семействам

        e_list = [] # пустой список для значений диаметра

        for con in connset:  # залезаем в набор соединителей
            con_svoistvo = con.Radius*304.8*2 # получаем диаметр с соединителя
            e_list.append(con_svoistvo)    # записываем значение в список    

        newlist.append(e_list) # записываем список значений в общий список

    else: # если категория не системная, то выполняем другой вариант кода

        connset = element.MEPModel.ConnectorManager.Connectors # первый вариант для обращения через MEPModel к загружаемым семействам

        e_list = [] # пустой список для значений диаметра

        for con in connset: # залезаем в набор соединителей
            con_svoistvo = con.Radius*304.8*2 # получаем диаметр с соединителя
            e_list.append(con_svoistvo) # записываем значение в список

        newlist.append(e_list) # записываем список значений в общий список

OUT = newlist # выводим значения из Питон-нода
Результат выполнения кода одинаковый

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

Обработка всех элементов определённой категории

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

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

FilteredElementCollector(doc).OfCategory(BuiltInCategory.Встроенное имя категории).WhereElementIsNotElementType().ToElements()

Этот код возьмёт из модели все элементы указанной категории. Чтобы он сработал, нужно указать на текущий документ (переменная doc), указать встроенное имя категории, а также указать, что работаем именно с экземплярами, а не типами — WhereElementIsNotElementType().

Такое выражение недостаточно просто взять и вставить в наш код. Для этого нужно подключить библиотеки. Это отдельная тема, вдаваться в неё не будем, просто покажу, что нужно написать в Питоне, чтобы всё заработало. Сперва грузим библиотеки, код такой:

import clr

clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager

doc = DocumentManager.Instance.CurrentDBDocument

Дальше посмотрим на примере воздуховодов. Встроенное имя категории для них называется OST_DuctCurves. Полный список можете посмотреть в специальной Гугл-табличке на вкладке «BuilInCategory Enumeration». Её подготовили Максим Похомов и Сергей Ковылин, спасибо им.

Пишу код, если видите перенос, то это просто браузер перенёс текст, потому что запись не влезла:

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

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

Если не выводить ducts сразу в OUT, а начать обрабатывать этот список воздуховодов, то можно получить доступ к соединителям и развлекаться, как хочется. Наш предыдущий код тоже можно адаптировать, однако не забывайте, что у нас воздуховоды бывают разной формы, и это тоже нужно учитывать в коде отдельными условиями. Будет, на чём попрактиковаться.

Удачи с Динамо, Питоном и Ревит АПИ.