В прошлой статье из этого цикла рассказывал про определение уровня у точечно вставляемых загружаемых семейств. В этом материале рассмотрю работу с линейными объектами: трубами, воздуховодами и их изоляции.
Такие объекты в Ревите называется MEPCurve — MEP кривая. К ним же относятся и изоляция, и гибкие трубы и воздуховоды, а также лотки и короба электриков. Лотки и короба рассматривать не буду, но в целом логика будет та же. Единственное, их нельзя разделить на куски прямым методом из Ревит АПИ. Поэтому, если нужно делить, пользуйтесь МодПлюсом, плагин «MEP Разделение». Для труб и воздуховодов он тоже подходит, можете сначала поделить плагином, а потом скриптом записать этажи.
Работать буду со стандартными нодами в Динамо для Ревита 2023 и кодом в Питоне. Версия Динамо — 2.16.1.
Скрипт из статьи выложу в сообществе ВКонтакте для платных подписчиков.
Алгоритм
Для начала проговорю последовательность действий и основные идеи. Разбирать будем всё на примере трубопроводов, но логика будет та же для любых системных линейных категорий.
- Получаю все трубы из модели. Их нужно условно поделить на вертикальные и горизонтальные трубы. Горизонтальными считаю как строго горизонтальные ветки, так и ветки с уклоном.
- С горизонтальными всё достаточно просто — можно взять любую точку, например середину трубы, определить ближайший уровень снизу, он и будет уровнем элемента. То есть тут будет алгоритм тот же, что и в первой статье цикла.
- Для вертикальных нужно провести дополнительный анализ. В случае, когда труба проходит в пределах одного этажа, всё просто — взяли точку посередине и по ней определяем уровень.
- Если труба пересекает уровень или уровни, то в рамках нашей задачи выглядит логичным поделить её на части, чтобы каждый кусок трубы лежал в пределах одного этажа. Дальше снова берём середину трубы и определяем уровень. Тут есть свои сложности, о них ниже.
- Для гибких труб запариваться не будем, примем, что все они лежат в рамках одного этажа, их всё равно не разделить на части. Берём середину и по ней определяем этаж.
- Изоляцию обрабатываем отдельно, так как сначала нужно разделить трубы. В Динамо это, кстати, создаёт нам дополнительные трудности, так как в Динамо нельзя задать последовательность действий нодов. Они выполняются все разом.
Для разделения труб на части можно воспользоваться пакетными нодами, но я не такой человек. Поэтому покажу вам комбинацию из нодов и кода Питона для решения этой задачи.
Получение линейных элементов
В моём примере проект ВК на 9 этажей. Если собрать все трубы, изоляцию и гибкие трубы, то получается 8804 элементов. Сначала я их собрал с помощью нода Element Classes (в более ранних версиях Динамо это было нод Element Types) и All Elements of Class (ранее All Elements of Element Type). В выпадающем списке выбрал MEPCurve.
Уровни собираю так же, как в прошлой статье.
Но по ходу дела понял, что обрабатывать все МЕП кривые разом не получается. Гибкие трубы практически не обрабатываю, потому что с них нужно получить точку посередине и всё. Изоляцию обрабатываю, но её нужно обработать после разделения труб. Соответственно, большая часть работы происходит с трубами, ведь их нужно проанализировать и поделить на отрезки.
Поэтому в итоге вернулся к «классике» — получению элементов по категории.
Обработка уровней
Тут всё так же, как в первой статье, только не буду выкидывать самый верхний уровень, там есть элементы. Цель обработки — получить список имён уровней и список отметок этих уровней так, чтобы они шли сверху вниз, от самого верхнего к самому нижнему.
Работа с линейными элементами
Сперва получаю геометрию нодом Element.GetLocation. Локейшен можно перевести как способ вставки. Для оборудования или фитингов локейшен — это точка, точка вставки семейства в модель. Вы один раз тыкнули по плану этажа — семейство вставилось по точке. Для линейных элементов локейшен образуется двумя кликами, то есть указанием координат каждой точки, в итоге у них способ вставки — отрезок или кривая. При получении способа вставки у труб, воздуховодов и их изоляции будут отрезки, а у гибких труб и воздуховодов будет NURBS-кривая.
Что такое Nurbs-кривая? Для инженеров сетей ответ такой:
При написании статьи заметил, что Динамо может выдавать ошибки на ноде для получения способа вставки. Есть такая тема в Динамо — масштабирование геометрии, насколько большим делать пространство в Динамо для размещения геометрии. Когда я ставил масштаб «Очень большой», то получал ошибки, якобы Динамо не может построить геометрию отрезка. Когда переключал на «Большой» — всё было в порядке. Следите за этим у себя.
Гибкие трубы
Геометрия гибкой трубы — это кривая линия. Она может как-то изгибаться, но это неважно, мы просто получим точку на середине этой кривой, по ней и определим принадлежность к уровню. С таким же успехом можно брать точку начала или конца кривой, разница несущественная.
Чтобы получить точку ровно по середине кривой, воспользуемся нодом Curve.PointAtParameter. Параметром кривой называют число от 0 до 1 включительно, где 0 соответствует началу кривой, а 1 — концу. Все промежуточные значения будут лежать внутри длины кривой. Если мне нужна середина, то логично, что беру параметр 0.5.
Дальше мы получим точки со всех элементов и разом определим их уровень и запишем его имя в выбранный параметр. Поэтому идём дальше.
Трубопроводы
Теперь нужно проанализировать трубы. Сперва определим ориентацию: горизонтальная она или вертикальная. Можно выбирать разные подходы, вот несколько вариантов, которые придумал, наверняка можно придумать что-то ещё:
- анализ координат Z для начала и конца отрезков. Трубы, у которых координата отличается мало или одинаковая, — горизонтальные. Если отличается сильно, то это вертикальные трубы. Вся проблема тут в том, что достаточно длинная труба с уклоном может иметь перепад в те же 200—300 мм, как и опуски.
- работа с параметром «Уклон». У трубы по этому параметру можно определить направление, но и тут есть недостатки. У изоляции и гибких труб такого параметра нет. Иногда стояки может «повести» и у них получается очень большой уклон в процентах. Если нет цели исправлять модель, то этим можно пренебречь и считать, что это вертикальные трубы. Тем не менее, изоляция и гибкие в пролёте.
- аналитическое вычисление уклона. Можем сами взять длину элемента или перепад высот по вертикали, длину его проекции на плоскость уровня, отсюда узнаем угол наклона. Неплохой вариант.
- анализ координат X и Y. Если эти координаты у начала и конца отрезка совпадают, то перед нами — вертикальная труба. Всё остальное — это уже какие-то вариации горизонталей. В каком-то смысле это частный случай предыдущего варианта. Потому что можно всё также построить отрезок-проекцию на плоскость уровня, она же плоскость XOY, и получить длину отрезка. Нулевая длина = вертикальная труба.
Мне нравится последний вариант, поэтому покажу, как реализовать его. Для этого нам нужно получить геометрию трубы — отрезок. Взять его начало и конец, получить координаты X и Y, а потом сравнить. Если одновременно координаты попарно равны, то перед нами вертикальная труба. Всё остальное принимаем как горизонтальные, значит, у них просто берём середину.
Вертикальные же нужно проанализировать дальше на пересечения с уровнями. Те, что пересекаются, будем делить по плоскости уровня. И после разделения снова берём точку на середине трубы. То есть в итоге всё сведётся к тому, чтобы брать точку на середине трубы.
Алгоритм для разделения труб на вертикальные и горизонтальные такой:
- Получил геометрию труб, то есть отрезки.
- Получил точки начала и конца отрезков, это делается нодами Curve.StartPoint и Curve.EndPoint.
- В Код Блоке пишу p.X и p.Y. Это такая короткая запись, чтобы получить у точки одну её координату, в нашем случае X или Y. Буква p в данном случае просто переменная, куда подаём данные.
- Дальше в Код блоках пишу код для сравнения, сравнение всегда двумя знаками равно. Сравниваю координаты Х начала и конца, координаты Y начала и конца.
- Вертикальными будут те трубы, у которых совпадают и X и Y начала и конца.
Тут надо быть внимательным. Ревит переводит футы в миллиметры, отсюда могут возникать погрешности. Соответственно, не всегда будет так, что Ревит правильно определяет стояк. Кроме того, опять же бывают трубы с неровной геометрией, у них обычно очень большое значение уклона. Чтобы такие трубы тоже обрабатывать, можно предварительно координаты начала и конца отрезка округлить, чтобы убрать эти погрешности.
Я на скриншоте ниже округление не делал, но вы можете либо применить нод Math.Ceiling, это округление до целого вверх, либо написать более сложные формулы, чтобы округлять до кратности 10, например. - Чтобы взять трубы, у которых совпадают и X и Y начала и конца, подаю оба сравнения на нод && — логическое И. Где оба true получим true, в остальных случаях будут false — это и есть наши горизонтальные трубы с уклоном или без.
- Дальше фильтрую.
Самое паршивое тут — разделение трубопроводов на две отдельные ветки алгоритма. Потенциально это может привести к тому, что в скрипте появятся пустые списки. Ну вот мало ли у вас не будет вертикальных труб, это крайне маловероятно, но тут важнее концептуальная составляющая. Не все ноды в Динамо могут переварить пустые списки.
Тут, скорее, вопросы к разработчикам, но факт есть факт: пустые списки будут вызывать ошибки в некоторых нодах, соответственно, вы не будете понимать, откуда взялась ошибка, пока не залезете в Динамо. Причём, такие ошибки могут и не влиять на правильность работы скрипта, то есть в проекте всё будет корректно, но вот скрипт выдаст ошибку. Ладно, если вы автор и можете это проверить, но пользователь зачастую не понимает, что происходит, и любая ошибка будет вызывать вопросы.
Это большой минус Динамо, с которым приходится мириться до тех пор, пока вы не осилите программирование хотя бы на Питоне, где такой проблемы не будет. Поэтому в идеале трубы лучше обрабатывать сразу в Питоне, если речь про Динамо.
Анализ вертикальных труб
Теперь нужно определить, какие вертикальные трубы пересекают уровни. И в этих местах «распилить» их на части. Какие-то трубы могут не пересекать уровни, какие-то могут пересекать один уровень, а некоторые — несколько уровней.
Тут тоже наверняка можно придумать разные способы, мой вариант такой: берём координаты Z у точки начала и конца трубы. Определяем имя ближайшего уровня к этим точкам. Если имена одинаковые, то труба находится в пределах одного уровня, её делить не надо. Всё остальное — надо.
Алгоритм следующий:
- Беру отрезки вертикальных труб. Получаю точки начала и конца.
- Получаю координаты Z для начала и конца.
- Способом из первой статье цикла определяю имена ближайших уровней.
- Сравниваю имена уровней.
- Фильтрую.
Здесь у нас снова фильтрация и разделение элементов на два потока обработки. Это тоже не очень хорошо по причинам, что озвучил выше. Но так как трубы приходится обрабатывать по-разному, то приходится делить.
Поиск пересечений труб с уровнями
Чтобы разделить трубу, нам нужно получить точку деления трубы там, где она пересекает уровень. Для этого воспользуемся нодом Geometry.Intersect — он принимает два списка геометрией, а возвращает геометрию пересечения. В нашем случае отрезки будут пересекаться с плоскостями, а это всегда — точка. По ней и будем «пилить» трубы.
Но тут есть один нюанс. Геометрию трубы мы получаем через локейшен, а вот у уровня нет геометрии, хоть визуально мы и видим её в интерфейсе. Фактически, уровень — это аннотация, поэтому мы не можем получить его геометрию через нод Element.Geometry.
Поэтому будем сами воссоздавать внутри Динамо объект, который можно пересекать с другой геометрией. В случае уровня самое логичное создать плоскость. Для этого у нас есть набор нодов для работы с Plane — плоскостями. Нам нужно создать плоскость XOY, а потом её накопировать на высоты уровней.
Для этого берём нод Plane.XY — он создаёт плоскость XOY, но она лежит на нулевой отметке. Чтобы накопировать и сместить плоскость на отметки уровней, воспользуемся нодом Geometry.Translate — это нод для перемещения геометрии. На него нужно подать геометрию, нашу плоскость, направление смещения и расстояние. Направление у нас совпадает с осью Z, для этого есть нод Vector.ZAxis, он даёт вектор, совпадающий с направлением оси Z.
Расстояние — это наши отметки уровней. Те, что выше нуля, будут с положительными значениями, что ниже — с отрицательными.
Теперь у нас есть геометрия и труб, и уровней. Пересекаем их в ноде Geometry.Intersect с векторным переплетением, или Декартово произведение в новых версиях Динамо. На первых вход подаём геометрию труб, на второй — уровней. Это нужно, чтобы в итоговом списке получились подсписки для каждой отдельной трубы.
Так как в этом списке у нас точно все трубы пересекают уровни, то в каждом подсписке будет как минимум одна точка. Поэтому можем нодом List.Flatten убрать все пустые списки. Пустые списки мы получаем с теми уровнями, которые не пересекают трубу.
Теперь у нас есть список трубопроводов и список с точками их пересечения с уровнями. Этого достаточно, чтобы разделить трубы на отрезки.
Разделение трубопроводов по уровням
Важный момент! Следующий код будет делить трубы на части, но это всё, что он делает. То есть код буквально распилит трубу на куски, но соединять их ничем не будет. Таким образом у вас на стояках будут разрывы. Система останется та же, что и была, но всё же образуются разрывы. Их можно «зашивать», вставляя элементы-разделители, но это уже отдельный код и отдельная работа, их я рассматривать не буду.
Подобную вставку разделителей делаю в своём платном скрипте по делению труб и воздуховодов.
-
Скрипты Dynamo: деление трубопроводов на отрезки3 700 ₽ – 12 000 ₽
-
Скрипты Dynamo: деление воздуховодов на отрезки3 200 ₽
Создаём нод для Питон-кода Python Script. У него нужно сделать два входа, один будет для труб, второй — для точек деления. Не стал адаптировать код под обработку труб и воздуховодов, этот код обрабатывает только трубы, но его можно переделать для воздуховодов. Те, кто скачает скрипт, увидят в коде подсказку для этого. Так как получал элементы по категории, то уверен, что в код будут приходить только трубы. Для воздуховодов можно сделать копию нода и чуть его откорректировать для обработки воздуховодов.
Код в Питоне следующий:
import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')
from Autodesk.Revit.DB import Plumbing, Mechanical
from RevitServices.Persistence import DocumentManager as DM
from RevitServices.Transactions import TransactionManager as TM
clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)
doc = DM.Instance.CurrentDBDocument
pipe_list = UnwrapElement(IN[0])
point_list = IN[1]
TM.Instance.EnsureInTransaction(doc)
new_pipes = [] # список для обработанных и вновь созданных труб
errors = [] # список для труб, которые не обработались
for pipe, point_sublist in zip(pipe_list, point_list):
for point in point_sublist:
try:
new_pipe_id = Plumbing.PlumbingUtils.BreakCurve(doc, pipe.Id, point.ToXyz())
new_pipes.append(pipe)
if new_pipe_id is not Null:
new_pipes.append(doc.GetElement(new_pipe_id))
except:
errors.append(pipe)
TM.Instance.TransactionTaskDone()
OUT = new_pipes, errors
На выходе из нода будут два списка, в первом обработанные и новые трубы, а во втором — трубы, которые по какой-то причине не поделились. Тут надо изучать уже отдельно, почему именно. Как пример, труба так идёт через уровень, что при её разделении образуется слишком короткий участок, который Ревит не может построить. Или трубу уже ранее распилили, и теперь она формально пересекает уровень, так как одним концом отрезка лежит на уровне, но по факту там резать уже нечего.
После всего этого мы получаем четыре списка труб: горизонтальные, вертикальные без пересечения уровней, вертикальные, которые поделили, и трубы с ошибками. Объединяем всё это в один список, из него надо получить изоляцию. Уже после можно будет всё это объединить в один список из труб, изоляции и гибких труб, чтобы получить геометрию и определить ближайший уровень.
Получаем изоляцию с труб
Здесь нам снова нужен код Питона, потому что в стандартном Динамо нет нода для получения именно изоляции с трубопроводов. Опять же, мой код адаптирован под трубы, но поменять его на воздуховоды несложно, в самом коде будет комментарий с подсказкой.
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager as DM
doc = DM.Instance.CurrentDBDocument
pipe_list = UnwrapElement(IN[0])
insulation_list = []
filter = ElementClassFilter(Plumbing.PipeInsulation)
for pipe in pipe_list:
try:
dependent_elements_id = pipe.GetDependentElements(filter)
for id in dependent_elements_id:
dependent_elements = doc.GetElement(id)
insulation_list.append(dependent_elements)
except:
pass
OUT = insulation_list
Теперь осталось определить имя уровня и записать его в указанный параметр. Тут всё то же самое, что в первой статье, поэтому просто покажу ноды.
Соберите скрипт сами по скриншотами или оформите платную подписку — станьте доном — в моём сообществе в ВК и скачивайте готовый скрипт. Сможете открыть скрипт в любом Динамо в Ревитах от 2023 и более новых.