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

Функции в программировании

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

Пользовательские функции — это функции, которые можем написать сами. По сути с помощью функции мы создаём свои собственные команды. Это удобно, когда нужно в коде производить какие-то однотипные операции. Чтобы не писать каждый раз весь код, можно один раз написать функцию и применять её. Это типа макросов для мышек: записал его, потом одним нажатием вызываешь алгоритм.

Далее под словом «функция» буду подразумевать именно пользовательскую функцию.

Синтаксис функций

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

def name_of_function(argument1, argument2, ...):
    тело функции

    return result

Далее делаем отступ и пишем тело функции — тот код, который будет что-то делать с аргументами, которые подаём в функцию.

Если нужно вернуть какой-то результат, то пишем ключевое слово return и далее имя переменной или переменных, которые возвращаем.

Самый банальный пример, который приводят — это функция для суммирования двух чисел. Она будет выглядеть так:

def summa(number1, number2):
    summ = number1 + number2
    return summ

В дальнейшем внутри кода можем не писать сложение, а подавать два числа в функцию: summa(число 1, число 2), в итоге будем получать их сумма. Пример не ахти какой, конечно, так как тут сомнительна какая-то экономия, но сейчас главное — принцип.

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

def algebra(a, b):
    summ = a + b
    diff = a - b
    multi = a * b
    div = a / b
    
    return summ, diff, multi, div

Тут у нас четыре переменных на выходе. Чтобы получить доступ к конкретному значению, можем обращаться к нему через индекс. Например, мне нужно получить произведение двух чисел. Тогда внутри кода буду писать так: m = algebra(num1, num2)[2]

В итоге в переменную m запишется результат произведения чисел num1 и num2.

Если нужно в отдельные переменные получить все значения из этой функции, то можно записать это так: sum, dif, mul, div = algebra(num1, num2)

В данном случае каждому выходному из функции значению я задал переменную через запятую. Так как функция возвращает четыре значения, то и переменных должно быть четыре. Если сделать три или пять, то будут ошибки.

Пример функции для длины списка

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

Итак, что имеем: есть какой-то список, я сгенерирую обычный список с числами, функция должна посчитать, сколько элементов в данном списке и вернуть целое число. Функция len() уже существует в языке программирования, поэтому использовать данное имя нам нельзя. Придумаем своё.

def list_length(given_list):
    length = 0
    for i in given_list:
        length += 1

    return length

Для проверки подал один и тот же список в свою функцию и во встроенную. Как видите, результат одинаковый. Теперь подробнее расскажу про код этой функции.

Объявил ключевое слово def — Питон понимает, что ща буит мясо функция. Даю ей имя, в скобках перечисляю аргументы. Так как функция должна считать длину одного списка, то и переменная в аргументах одна. В данном случае это имя — просто какое-то имя, внутри кода мы не обязаны подавать в функцию переменную с таким же именем.

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

Затем создаю цикл, который заходит в каждый элемент списка, а внутри цикла я прибавляю единицу к переменной length на каждой итерации. Именно поэтому я создал её вне цикла и приравнял к нулю — чтобы каждый элемент списка давал единицу для добавления.

В выход функции подаю переменную с длиной списка. Готово.

С функциями разобрались, теперь давайте посмотрим на коллекторы элементов.

Коллекторы элементов

Когда мы создаём скрипт, то чаще всего нужно получить элементы из модели и как-то их обработать. Как сделать это стандартными нодами в Динамо, показывал в отдельной статье:

Но мы тут в Питоне программируем, нам эти их ноды не хочется. Поэтому будем использовать специальный инструмент из Ревит АПИ — коллектор элементов. Более полно он называется FilteredElementCollector — коллектор отфильтрованных элементов.

Коллектор на одну категорию

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

  • FilteredElementCollector(doc) # тут получим доступ ко всем элементам во всем проекте, а дальше уже будем отфильтровывать и оставлять нужные
  • FilteredElementCollector(doc, doc.ActiveView.Id) # тут получим элементы только с активного вида и уже их будет отфильтровывать

В скобках doc — это переменная, которая содержит текущий проект.

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

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

Указываю метод OfCategory(), а в скобках пишу встроенное имя категории в Ревите после BuiltInCategory. Самый простой вариант узнать это имя — посмотреть через Ревит Лукап. Для этого выделяем элемент, нажимаем Snoop Selection и ищем свойство Category. Жмём по нему и увидим встроенное имя. Оно всегда начинается с OST.

Дальше нужно уточнить, что мы хотим получить — экземпляры или типы. Экземпляры — это размещенные в модели элементы, типы — это типоразмеры семейств. Для экземпляров делается это ещё одним методом: FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Встроенное имя категории).WhereElementIsNotElementType()

Если вам нужны типоразмеры, то пишете .WhereElementIsElementType()

И после всего этого надо преобразовать полученные данные в элементы или айди элементов. Делается это ещё одним методом — ToElements() или ToElementIds().

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

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

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

Коллектор на несколько категорий

Если нужно собрать элементы из разных категорий, то можно, конечно, сделать несколько раз коллектор по каждой категории и собрать всё в один список через extend(). Но можно сразу перечислить нужные категории и собрать разом элементы.

Для этого используются специальные фильтры на несколько категорий — ElementMulticategoryFilter(). В него нужно подать специальный вид списка — типизированный список. Чтобы такое сделать, нужно в Питон подгрузить библиотеку:

import System
from System.Collections.Generic import List

Типизированный список содержит элементы только одного типа. В нём нельзя хранить одновременно разные типы, например числа и текст, категории и элементы. Только что-то одно. Тип данных в списке объявляется в квадратных скобках после слова List[тип данных].

Создаём обычный список со встроенными именами категорий. Для примера сделаю список для воздуховодов и их фитингов:

categs = [BuiltInCategory.OST_DuctCurves, BuiltInCategory.OST_DuctFitting]

Теперь создам новую переменную и в неё запишу типизированный список. После этого можно подавать такой список в фильтр на несколько категорий:

cats = List[BuiltInCategory](categs)
filter = ElementMulticategoryFilter(cats)

Почему список должен быть типизированным, почему оно работает именно так — не знаю, но оно работает вот так.

Теперь пишем коллектор. Начало такое же, как в прошлом варианте, получаем документ и говорим, что нужны экземпляры, а после этого применяем специальный метод WherePasses() и в скобках указываем наш фильтр на несколько категорий. Ну и в конце говорим, что нужны элементы, а не айди элементов.

elements = FilteredElementCollector(doc).WhereElementIsNotElementType().WherePasses(filter).ToElements()

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

В этот WherePasses() можно подавать разные фильтры. Например, есть фильтр ElementLevelFilter() — в скобки подаётся ElementId для уровня, в итоге получим все элементы, которые основаны на данном уровне.

level = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Levels).WhereElementIsNotElementType().ToElements()[0]

level_id = level.Id

filter = ElementLevelFilter(level_id)

elements = FilteredElementCollector(doc).WhereElementIsNotElementType().WherePasses(filter).ToElements()

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

Коллектор на класс

Есть ещё вариант получения элементов — по классу методом OfClass(). Я им не пользуюсь, как-то не было нужды, но пригодится такое тоже может.

Запись выглядит вот так на примере труб:

elements = FilteredElementCollector(doc).OfClass(Plumbing.Pipe).ToElements()

Объявляю коллектор, далее пишу метод OfClass(), а в скобках указываю, какой класс элементов нужен. Где посмотреть классы? А вот прям на сайте Ревит АПИ, классы ведь подписаны.

В скобках пишу какой именно класс нужен, при этом указываю, из какого пространства имён нужен класс — Plumbing.Pipe. Ну и дальше говорю, что хочу элементы, а не айдишники. В итоге в переменной elements буду все экземпляры в проекте указанного класса.

Очевидно, что если указать класс труб или воздуховодов, то по сути получим то же самое, что для категории на трубы или воздуховоды. Но среди классов есть как бы более общие классы, например MEPCurve или FamilyInstance.

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

Если указать FamilyInstance, то разом получаем все-все загружаемые семейства, которые используются в проекте. Конечно, сюда залетят и всякие листы и прочее лишнее, но тем не менее, за один приём получим всё, кроме системных категорий. Иногда такое может быть удобно.

Алиасы для коллектора и сужение импорта

Команда FilteredElementCollector довольно длинная, поэтому её можно сократить — создать алиас. Для этого мы должны подгрузить отдельно класс FilteredElementCollector и дать ему обозначение. Об алиасах рассказывал в статье номер 5 цикла.

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

В данном примере я отдельно подгружаю класс для коллектора и обозначаю его FEC. Можете называть FECALII, тут хозяин-барин.

Перед этим я написал import * — это означает, что я загрузил вообще все классы из пространства имён. Это может быть излишним, если работаем только с конкретными классами, например, только с трубами.

Поэтому экономнее будет загружать конкретные классы из конкретных пространств имён. Будет выглядеть вот так:

clr.AddReference('RevitAPI')
from Autodesk.Revit.DB.Mechanical import Duct
from Autodesk.Revit.DB import FilteredElementCollector as FEC

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

У такого конкретного импорта есть небольшой плюс: можно писать в коллекторе сразу класс без указания пространства имён. Вместо OfClass(Mechanical.Duct) можно будет написать просто OfClass(Duct).

Сумасшедшая экономия времени!

Практическая задача

Давайте сделаем следующее: соберём из проекта все воздуховоды, а потом сформируем для них наименование по шаблону: Префикс + Размер + Длина в мм. Например: Воздуховод круглый из оцинкованной стали ø250, L=870 мм.

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

С размером всё проще, но напишем функцию всё равно.

Начинаем с функций, первой будет функция для получения размера. По факту у воздуховодов уже есть параметр «Размер», который возвращает текстовое значение размера. Нам такое очень удобно, так как текст можно будет сразу использовать в наименовании.

Смотрим в Ревит Лукапе, как получить этот параметр и каким методом получить его в виде текста. Выделяю воздуховод, жму Snoop Selection, далее Parameters и ищу параметр «Размер». Смотрю в описание и вижу: тип данных «строка», значение можно получить и методом AsString() и методом AsValueString(). Заодно подсмотрим на внутреннее имя параметра.

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

def get_duct_size(duct):
    duct_size = duct.get_Parameter(BuiltInParameter.RBS_CALCULATED_SIZE).AsString()
    
    return duct_size

Теперь функция для длины. Тут всё то же самое, сначала смотрим параметры в Лукапе, потом пишем код.

Здесь мы видим, что тип хранения — Double, дробное число. При этом можно получить значение в виде строки, но там будет то же значение, что и в свойствах. А я бы хотел округлить длину до целых. Для этого есть функция round(число). Если округляем до целых, то она возвращает целое число. Если округляем до какого-то разряда, то запись будет round(число, N), где N — количество разрядов. В этом случае получим дробное.

Так как мне для наименования достаточно целого числа, то округляю до целых, а они очень просто переводятся в текст — функцией str(). Если же вам нужны разряды после запятой, то нужно будет дополнительно обработать число после перевода в текст. Об этом писал в статье про работу со строками.

def get_duct_len(duct):
    duct_len_feet = duct.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble()
    duct_len_mm = round( duct_len_feet * 304.8)
    duct_len_string = str(duct_len_mm)
    
    return duct_len_string

Теперь пишу функцию для формирования наименования. В ней мне нужно получить префикс, потом добавить размер и длину. Префикс будем брать из параметра типа для воздуховода, пусть это будут «Комментарии к типоразмеру».

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

def get_element_type(element):
    element_type = doc.GetElement(element.GetTypeId())
    
    return element_type

Чтобы получить тип элемента, я обращаюсь к методу GetTypeId(). Как видно из его названия, он возвращает айди типоразмера. А далее с помощью метода GetElement() я получаю элемент по его айди из проекта. Чем-то напоминает обращение к элементу через айди командой «Выбрать по коду».

И вот функция для наименования:

def duct_naming(duct):
    duct_type = get_element_type(duct)
    prefix = duct_type.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_COMMENTS).AsString()
    size = get_duct_size(duct)
    length = get_duct_len(duct)
    
    name = prefix + " " + size + ", L=" + length + " мм"
    
    return name

Сначала получаю тип функцией, которую создал ранее. С типа получаю значение параметра. Параметр текстовый, поэтому в конце применяю метод AsString().

После вновь использую функции, которые написал выше, чтобы получить размер и длину. И в итоговой переменной конкатенацией строк формирую текст наименование. Можно было бы использовать форматирование строк, но и так тоже нормально.

Можно было бы внутри функции ещё и сразу записывать наименование в воздуховод, но давайте сделаем это отдельно. Сперва нужно получить все воздуховоды, а затем циклом пройтись по ним, сформировать и записать наименование в каждый. Ещё создам пустой список, куда занесу все наименования, чтобы вывести их из скрипта в виде списка.

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

name_list = []

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

for duct in all_ducts:
    name = duct_naming(duct)
    name_list.append(name)
    duct.LookupParameter("ADSK_Наименование").Set(name)    

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

OUT = name_list, all_duct

В воздуховоды наименование пишу через метод LookupParameter() в параметр «ADSK_Наименование». Можно было бы вывести для пользователя строковый нод, куда он может внести нужное ему имя. Тогда внутри метода писали бы переменную с этим именем параметра.

Заметьте, какой короткий цикл — мы проводим большинство операций внутри функций, поэтому в цикле минимум действий. Таким образом циклы и сокращают код, особенно если одна и та же операция повторяется несколько раз, и позволяют в одном месте исправлять ошибки, и гораздо удобнее для копирования между разными скриптами. Рекомендую вообще завести отдельный файлик, куда будете складывать полезные функции.

Вот весь код целиком:

import clr

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

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_duct_size(duct):
    duct_size = duct.get_Parameter(BuiltInParameter.RBS_CALCULATED_SIZE).AsString()

    return duct_size

def get_duct_len(duct):
    duct_len_feet = duct.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble()
    duct_len_mm = round( duct_len_feet * 304.8)
    duct_len_string = str(duct_len_mm)

    return duct_len_string

def get_element_type(element):
    element_type = doc.GetElement(element.GetTypeId())

    return element_type

def duct_naming(duct):
    duct_type = get_element_type(duct)
    prefix = duct_type.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_COMMENTS).AsString()
    size = get_duct_size(duct)
    length = get_duct_len(duct)

    name = prefix + " " + size + ", L=" + length + " мм"

    return name

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

name_list = []

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

for duct in all_ducts:

    name = duct_naming(duct)
    name_list.append(name)
    duct.LookupParameter("ADSK_Наименование").Set(name) 


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

OUT = name_list, all_ducts

Итоги

Мы посмотрели на функции в Питоне, как их создавать и применять внутри Питона в Динамо.

Разобрались с коллекторами, чтобы получать из проекта нужные нам элементы.

Попрактиковались на простой, но реальной задаче, применили знания и о функциях, и о коллекторах.

Мы молодцы. Дальше в новых статьях будет ещё практика, ждите. Можете подписаться по ссылкам ниже на Телеграм, чтобы не пропустить уведомления о новых статьях.