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

Методы и свойства
Посмотрим сначала через Ревит Лукап, как мы вообще можем узнать, есть ли у семейства общие вложенные. Найдите элемент, у которого точно есть вложенные, зайдите в лукап через Snoop Selection. Дальше просто ищите глазами название свойства, которое хоть как-то похоже на то, что ищем.
Если крутить до самого низа, то увидим, что у экземпляра семейства есть метод GetSubComponentIds() — получить айдишники вложенных компонентов. То, что надо. Здесь мы получаем список айдишников, а по ним можем вернуть и сами элементы из проекта.


Как я понял, что это именно метод, а не свойство? В Ревит Лукапе можно навести мышку на имя параметра и появится всплывающее окошко с подсказкой. Если написано Method, то это метод. Если нет, то это свойство.

Чтобы получить родительское семейство, нужно заглянуть в свойство SuperComponent. Если у семейства есть какое-то родительское, то есть само семейство вложенное, то свойство будет жирным и там будет имя семейства с айди. Если семейство не вложенное, то там будет null.



Итого по пункту
У экземпляров семейств есть метод GetSubComponentIds() и свойство SuperComponent. Метод вернёт список айдишников вложенных, если они есть. Свойство вернёт родительский экземпляр семейства, если семейство вложенное. Если нет, то свойство вернёт False.
Алгоритм
Теперь нужно определиться с логикой. Наша задача — найти родительские семейства, а потом к каждому найти все его вложенные. Причём желательно найти вложенные со всех уровней, так как семейства могут загружаться каскадом вложенных.
Поэтому задачу можем разделить на три шага.
Шаг 1
Поиск высших родительских семейств, то есть тех, кто не являются вложенными. Такое родительское — это то семейство, у которого свойство SuperComponent возвращает False.
В данном случае имеет смысл работать только с загружаемыми семействами, потому что семейства системных категорий не могут в принципе обладать вложенными. У стены или трубы не бывает вложенных общих. При этом есть риск, что в загружаемых будут как раз системные категории, потому что такой трюк провернуть можно. Как такое сделать, показываю в этом видео:
Мы такой случай рассматривать не будем, но по сути вам нужно добавить проверку категорий вложенных и пропускать элементы с системными категориями.
Шаг 2
Нужно у родительских найти все их вложенные. Для этого мы, естественно, будем использовать наш второй метод — GetSubComponentIds(). Но при этом нужно учитывать, что у этих вложенных могут быть ещё вложенные, и ещё вложенные, и ещё вложенные, и так далее.
Здесь у нас получается цикл, глубину которого мы не знаем. Когда мы получаем элемент из модели, мы не знаем, сколько в нём вложенных. Может быть вложенные на 1 уровне, а может — на 20 уровнях. Решить эту проблему нам поможем специальный цикл, который создаст рекурсию — повторение цикла столько раз, сколько нужно.
Шаг 3
Если задача — перенести значение параметра из родительского во вложенные, то у нас есть список родительских и список с подсписками вложенных. Берём значение у родительского, пишем во вложенные. Это несложно, такое делали в прошлых статьях.
Давайте для интереса сделаем нумерацию позиций так, чтобы родительское получало целое число N, а вложенные — дробные в виде N.i, где i — порядковый номер вложенного.
Сортировать элементы будет просто по их айди, то есть по времени создания элементов в модели. Получать строки из спецификации и обрабатывать их я не умею, поэтому рассмотрим более простой вариант.
Питонинг
Лёг, полежал.
Подготовка
Для примера создам пустой проект и загружу в него своё семейство фланцев с вложенными креплениями и трубы из сшитого полиэтилена. У фланцев есть по несколько вложенных, а может и не быть, а у фитингов в трубах сшитого полиэтилена есть вложенные семейства гильз. Таким образом создаю для себя контролируемую среду, в которой и разные варианты есть, и тестировать проще.
В качестве семейства с каскадами вложенных сделаю просто кубик, в который вложу другие кубики. При этом вложенные можно отключать, таким образом получаю каскады разной глубины. Каскад в данном случае — уровень вложенности общих семейств. Добавлю ещё параметр с позицией, куда будем писать значения.


Код
Теперь пора открывать Динамо и VSCode. Буду писать код в ВСКоде, а в Динамо закидывать его через ноды для чтения кода из текстового файла. Ну и сразу закидываю стандартный текст для импорта библиотек.

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
Функции
Сначала напишу функцию для определения того, является ли семейство родительским. Здесь достаточно проверить на то, что вернёт свойство SuperComponent. Если будет False, то это родительское, если вернёт какое-то иное значение, то это вложенное, его пропускаем. Из функции буду возвращать True и False. В дальнейшем смогу циклом проходиться по списку элементов, быстро проверять, родительское ли семейство, и если да, то закидывать в отдельный список.
def get_parent(fam_inst):
if fam_inst.SuperComponent:
return False
else: return True
Написал функцию и вижу, что это какая-то шляпа. По сути я засунул в неё одно действие. Так как функция будет мне нужна всего один раз, то тут особой экономии нет, поэтому и смысла от неё ноль. Так что функцию упраздняем, возьмём список семейств из проекта и прямо из него получим родительские.
Теперь напишу функцию, которая берёт родительское семейство и проверяет все вложенные на всех уровнях и запаковывает их в один список. Сначала покажу код, а потом пройдёмся по логике. Жирным выделил место, где появляется рекурсия.

# Функция для получения вложенных со всех уровней
def get_children(parent):
nested_list = []
if parent.GetSubComponentIds():
nested_id_list = parent.GetSubComponentIds()
for n_id in nested_id_list:
nested = doc.GetElement(n_id)
nested_list.append(nested)
subnested = get_children(nested)
nested_list.extend(subnested)
return nested_list
Объявил имя функции, подавать будем одну переменную — родительское семейство. Далее создам пустой список nested_list, в него буду складывать вложенные. Так как их может быть много, то нужен именно список, а не просто переменная.
После запускаю проверку условием if. Если применяю метод GetSubComponentIds() к семейству и возвращается False, значит, вложенных нет. Если вернётся True, то вложенные есть, надо их получать. Что я и делаю в переменную nested_id_list.
Так как это список, в котором сидят айди элементов, то нужно залезть в этот список циклом for, пробежаться по каждому айди, получить по нему элемент из проекта и закинуть в список вложенных.
В принципе, всё, это рабочая функция, она собирает вложенные. Но она нам соберёт вложенные только первого уровня, так как метод GetSubComponentIds() возвращает только вложенные первого уровня из семейства. А нам нужно и эти вложенные проверить на наличие вложенных.
Поэтому мы берём наше вложенное и уже к нему применяем ту же самую функцию, в которой это вложенное получили. Так можно делать в Питоне, это удобно, потому что в итоге мы получили рекурсию — мы будем проходиться по вложенным столько, сколько их есть на любом количестве уровней вложенности.
Как только мы дойдём до последнего вложенного, то «упрёмся» в условие if parent.GetSubComponentIds(). У последнего нет вложенных, условие выполняться не будет, и работа рекурсии прекратится.
Так как метод возвращает список вложенных, даже если вложенное одно или их нет, то нам нужно содержимое этого списка прибавить к тому списку вложенных, что получили на первом уровне, ещё до рекурсии. Поэтому беру список nested_list и расширяю его методом extend(). Этот метод добавит в исходный список тот список, что засунем внутрь метода, без создания подсписка. Пустые списки добавляться не будут.
В результате действия функции мы получим двухуровневый список. У родительских без вложенных получим пустые списки, так как внутри функции мы начинаем код с того, что создаём этот самый пустой список. Если нет вложенных, то и добавить в него нечего. Вот и возвращается он таким, каким был создан первоначально — пустым.

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

fam_instances = ( FilteredElementCollector(doc)
.OfClass(FamilyInstance)
.WhereElementIsNotElementType()
.ToElements() )
Здесь объявляю переменную, потом открываю скобки. Это нужно, чтобы написать не одну длинную макаронину, а абзацами и отступами переносить строку так, чтобы её было удобно читать. Дальше пишу обычный текст для коллектора, но вместо категории беру OfClass(). Внутри пишу класс, который мне нужен, — это все загружаемые семейства, они называются экземплярами семейства — FamilyInstance. У системных категорий свои классы, например Wall или MEPCurve.
Дальше пишу, что мне не нужны типоразмеры и преобразую всё к элементам. Получил семейства.
Теперь создам пустой список, куда запишу родительские семейства, а потом циклом пройдусь по списку экземпляров.

parent_list = []
for ins in fam_instances:
if not ins.SuperComponent:
parent_list.append(ins)
Как видите, код довольно короткий, так что тут можно обойтись без функции.
Теперь делаю почти то же самое, но для вложенных. Для этого создам пустой список для них, а потом циклом пройдусь по родительским семействам и функцией вытащу вложенные.

children_list = []
for parent in parent_list:
children_list.append(get_children(parent))
Если вывести оба этих списка, то получим такую картину:

В первом списке у меня 13 родительских семейство. А во втором — список из тринадцати подсписков, внутри которых — вложенных со всех уровней. Где их нет, там пустой список. Это не лишняя информация, потому что она нам пригодится для нумерации.
Нумерация позиций
У нас два списка, в котором есть строгое соответствие. Каждому элементу первого списка соответствует подсписок во втором списке. Значит, можем воспользоваться моей любимой функцией zip().
Нам нужно указать параметр, в который будем писать позицию и первый номер для позиции. Поскольку мы берём все элементы из проекта, то может случиться такая проблема: если родительские семейства одинаковые или в семействе несколько одинаковых вложенных, то каждому из них присвоится разная позиция, ведь для Ревита это разные элементы.
Авторы плагинов обычно избегают этого с помощью того, что анализируют спецификацию, но это сложная тема, я такое не умею.
Поэтому вижу тут два варианта относительно несложных. Первый — анализировать наименования вложенных. Если они одинаковые, то и элементы считаем одинаковыми, позицию им пишем одну и ту же. Проблема в том, что наименования могут быть одинаковыми, а марки разными.
Второй вариант — анализировать типы. Если у элементов айди типов, которые уже обрабатывали, то считаем эти элементы одинаковыми и даём одинаковый номер. Но тут тоже проблема, те же самые фитинги трубопроводов часто идут одним типоразмером, а наименования по экземпляру и вариаций может быть десятки.
Как видите, тут везде есть недостатки. По значению марки анализировать тоже такое себе, она может быть пустой. Поэтому давайте скомбинируем: если у элементов один и тот же тип и одинаковые наименования, то считаем их одинаковыми, поэтому и позицию даём им одну и ту же.
Это несколько искусственный подход, но что поделать.
Как будем проводить анализ. Берём элемент, получаем его айди, получаем значение из параметра с наименованием, у меня это будет «ADSK_Наименование». Переводим айди в текст и соединяем с наименованием в формате «Id_Наименование». Это наше значение для проверки.
При обработке семейств создаём пустой список, куда закидываем это значение. Так как мы проходимся циклом, то для каждого элемента получаем значение для проверки и заносим в список. При этом мы можем сделать проверку: если значение уже есть в списке, то номер позиции не приращиваем, а оставляем тем же. Если значения нет — надо добавить единичку к позиции.
Так как внутри цикла будем записывать номер позиции в семейства, то нужно делать цикл внутри транзакции. Поэтому давайте откроем транзакцию, создадим набор параметров на входе в ноды, обозначим их в коде.

TM.Instance.EnsureInTransaction(doc)
param_position = IN[0]
first_position = IN[1]
param_name = IN[2]

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

def create_combination(element):
elem_type_Id = str(element.GetTypeId())
elem_type = doc.GetElement(element.GetTypeId())
try:
elem_name_from_instance = element.LookupParameter(param_name).AsString()
out_value = elem_type_Id + "_" + elem_name_from_instance
except:
try:
elem_name_from_type = elem_type.LookupParameter(param_name).AsString()
out_value = elem_type_Id + "_" + elem_name_from_type
except: out_value = elem_type_Id
return out_value
Чувствую, что код кривоват, но он работает. Внутри функции получаю айди типоразмера и типоразмер. Далее конструкцией try-except получаю наименование. У нас возможны три ситуации: параметр с наименование по экземпляру, по типу или нет такого параметра вообще.
Поэтому в первом try я получаю значение из экземпляра, а потом формирую значение для проверки в переменной out_value. Если у элемента нет параметра, то программа выдаст ошибку. Мы её как раз и отлавливаем с помощью этого try. Будет ошибка — значит делаем то же самое, но для типоразмера. Если и там ошибка, то конструкция try-except сработает уже внутри первого except и так мы избежим ошибок при выполнении, так как если параметр нет, то получим просто айди типа.
Проверяю функцию в специальном кусочке кода, чтобы посмотреть в Динамо, что получается:


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

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

Теперь делаем условие для проверки, это записывается в формате значение in список.

Если наше проверочное значение есть в списке проверочных значений, то идём в этот список и функцией index() получаем индекс существующего значения. Этот индекс нужен, чтобы пойти в список позиций и вытащить оттуда существующую позицию. И вот уже её пишем в параметр родительского семейства.
Давайте на примере. Пусть у нас уже прошло несколько итераций цикла. В списке
check_listу нас будут значения «111_Угольник» и «112_Тройник», а в спискеposition_listбудут значения 1 и 2. Цикл идёт дальше и берёт ещё один угольник, получает его проверочное значение, там снова «111_Угольник». Тогда идём и ищем индекс этого значения в спискеcheck_list.Находим — это 0. По этому индексу обращаемся в список позиций и получаем из него значение — 1. Значит эту единицу пишем в свойства очередного угольника.Цикл продолжает и получает семейство фланца. У него проверочное значение будет «121_Фланец». Такого значения нет в
check_list. Тогда добавляем значение в этот список, чтобы в будущем проверять другие элементы. Переводим номер позиции в строку, так как пишем значение в текстовый параметр. Записываем значение, новую позицию добавляем в список позиций. И добавляем счётчику единицу, чтобы следующий новый элемент получил последовательный номер.
Внутри одного родительского все вложенные также должны получить порядковые номера в соответствии с проверкой. В отличие от родительских, тут проверка будет идти на уровне списка вложенных, а не вообще всех вложенных. Если делать на уровне всех вложенных вообще, то потеряем распределение вложенных по родительским. Например, все гильзы из проекта запишутся как вложенные к угольнику, остальным фитингам ничего «не достанется».

Поэтому по сути тут всё то же самое, но сначала мы задаём позицию первого вложенного, это всегда единица. Затем проверяем, есть ли вообще вложенные, это делается условием if child_list. В Питоне, если список пустой, то такое условие вернёт False. Таким образом мы будем пропускать элементы без вложенных. Если вложенные есть, то запустится цикл проверки.
Перед этим создадим опять же два списка, для проверочных значений и позиций. Дальше повторяем те же циклы, что с родительскими с одним исключением — здесь мы получаем списки элементов, а не элемент, поэтому создаём дополнительный цикл for child in child_list.
После этого закрываем транзакцию и всё, наш код готов. Вот результат из Ревита в спецификации:

Разные родительские получили разные номера, а вложенные упорядочились по своим родительским. Объекты просуммировались, и мы получили деление по позициям. Оно неидеально, но если вам такое подходит, то можно пользоваться.
Вот весь код из статьи:
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 get_children(parent):
nested_list = []
if parent.GetSubComponentIds():
nested_id_list = parent.GetSubComponentIds()
for n_id in nested_id_list:
nested = doc.GetElement(n_id)
nested_list.append(nested)
subnested = get_children(nested)
nested_list.extend(subnested)
return nested_list
def create_combination(element):
elem_type_Id = str(element.GetTypeId())
elem_type = doc.GetElement(element.GetTypeId())
try:
elem_name_from_instance = element.LookupParameter(param_name).AsString()
out_value = elem_type_Id + "_" + elem_name_from_instance
except:
try:
elem_name_from_type = elem_type.LookupParameter(param_name).AsString()
out_value = elem_type_Id + "_" + elem_name_from_type
except: out_value = elem_type_Id
return out_value
fam_instances = ( FilteredElementCollector(doc)
.OfClass(FamilyInstance)
.WhereElementIsNotElementType()
.ToElements() )
parent_list = []
for ins in fam_instances:
if not ins.SuperComponent:
parent_list.append(ins)
children_list = []
for parent in parent_list:
children_list.append(get_children(parent))
TM.Instance.EnsureInTransaction(doc)
param_position = IN[0] # type: ignore
first_position = IN[1] # type: ignore
param_name = IN[2] # type: ignore
if first_position >= 0:
i = first_position
else: i = 1
check_list = []
position_list = []
for parent, child_list in zip(parent_list, children_list):
parent_check_value = create_combination(parent)
if parent_check_value in check_list:
index = check_list.index(parent_check_value)
parent_position = position_list[index]
parent.LookupParameter(param_position).Set(parent_position)
else:
check_list.append(parent_check_value)
parent_position = str(i)
parent.LookupParameter(param_position).Set(parent_position)
position_list.append(parent_position)
i += 1
j = 1
if child_list:
child_check_list = []
child_position_list = []
for child in child_list:
child_check_value = create_combination(child)
if child_check_value in child_check_list:
index = child_check_list.index(child_check_value)
child_position = child_position_list[index]
child.LookupParameter(param_position).Set(child_position)
else:
child_check_list.append(child_check_value)
child_position = parent_position + "." + str(j)
child.LookupParameter(param_position).Set(child_position)
child_position_list.append(child_position)
j += 1
TM.Instance.TransactionTaskDone()
OUT = parent_list, children_list
Надеюсь, было полезно.



