В этой статье я хочу изложить основы, на базе которых запущу ветку статей по оцифровке чертежей из большого числа примитивов AutoCAD (так сказать, один из процессов "обратного bim". Так вышло, что до этого передо мной не стоял вопрос (хотя чего я лукавлю, стоит такой вопрос уже 2 месяца, который я откладываю на потом) по организации алгоритма выборки данных чертежа среди большого числа объектов как то:
- попадание объектов в контур;
- нахождение ближайших объектов к данному;
Доселе я решал задачу путем перебора координат точек границы линий и сравнения их друг с другом. Как только число данных стало возрастать, ожидание результата стало логично затягиваться, а недавно я получил пример топосъемки где абсолютно все элементы включая типы линий, блоки представляли собой набор отрезков, и стояла задача -- как-то упорядочить этот хаос.
Сперва я пошел по пути стандартного метода AutoCAD SelectCrossingPolygon (см. здесь). Но его специфика заключалась в том, что необходимо было "приближать" экран к месту позиционирования данных, а также он плохо работал со скученными объектами (у меня не получилось даже сделать рабочий вариант).
Затем я стал искать примеры кода с данным методом и раз за разом натыкался на отсылки к методу поиска через деревья (конкретно, RTree), см. например, тут. Осознав, что это остался единственный путь (о котором хотя бы говорят как о рабочем варианте), я сел за его изучение. Осознание к слову не заняло больше пары часов, а как только увидел результат его применения загорелся идеей написать эту статью и реализовать этот функционал в Dynamo.
И эта реализация заняла сутки реального времени суммарно 😂.
Базовая библиотека для работы с RTree была выбрана такая (она же тут).
Библиотеку я немножко подкорректировал, нагло убрав отладчик через интерфейс log4net.Log (на работе вроде бы это не сказывается). К слову. автор на GitHub'е опубликовал версию этого на .NET Standard (где используется встроенный интерфейс Log), но так как .NET Standard не совместим с .NET Framework в этой части, я оставил как есть (свой "костыль").
Материал по работе с самим кодом я искал тут и тут.
1. Идентификаторы объектов в AutoCAD
Любой объект чертежа имеет внутренний идентификационный код. Их в AutoCAD по крайней мере 3 типа - это ObjectId, Handle и Instance Pointer. Для внутренних транзакций и в целом работы с API без Dynamo мне лично всегда было достаточно только ObjectId, но вот в Dynamo объекты чертежа вызываются при помощи Handle.
Принципиальное отличие этих идентификаторов в том, что ObjectId - это идентификаторы объектов чертежа только для данной сессии документа (пока он не закрыт), а Handle остаются для одних и тех же объектов постоянными при открытии/закрытии чертежа. Instance Pointer мне не встречался вообще, да я и не уверен что это официальный термин.
Object ids are created for each entity each time the drawing database is opened, thus they are only unique until the current drawing is closed. Once it is reopened, all ObjectIDs will be completely different. Handles, however, persist between sessions and, as mavious stated, can be converted to a long and back. (https://stackoverflow.com/a/46699660)
Во всех реализация деревьев RTree использовал именно ObjectId. Поэтому для возможности возврата в интерфейс Dynamo самих объектов был использован метод из класса CustomNodes.Selection.GetAcadObjectsByObjectsId(), который и преобразует список ObjectId в объекты пространства чертежа (причем объекты не только AutoCAD, но и Civil 3D).
Но так как пока в реализации RTree я не учитывал объекты Civil 3D эта поправка о включении в список объектов чертежа не нужна
Здесь стоит отметить использование библиотеки AutoCADNodes.dll для возможности обращения к классу Autodesk.AutoCAD.DynamoNodes.
Также я вывел присоединение объектов из-под транзакции, так как при этом выбивалось исключение "Operation is not valid due to the current state of the object".
Для преобразования же объектов Dynamo (вернее, объектов AutoCAD, выбранных в Dynamo другими нодами), есть метод GetObjectIdsByObjects_AcadObj() из этого же класса:
2. Предварительная выборка объектов
Все знают о прекрасном инструментарии "Быстрый выбор" в AutoCAD:
Так вот, в API есть похожая конструкция, принимающая на вход сразу несколько условий (а тут - интерактивно, можно лишь задавать условия последовательно изменяя набор объектов).
Application.DocumentManager.MdiActiveDocument.Editor.SelectAll(<<SelectionFilter>>);
где SelectionFilter - это тот самый набор условий поиска.
В Dynamo я реализовал это тремя методами. Первый -- CustomNodes.Selection.GetDxfCodesToTypedValues содержит словарь (Dictionary), возвращающий коды свойств объектов, по которым производится поиск (такие как, имя слоя, уровень, толщина линии и пр.).
Полный список свойств см. в документации для класса DxfCode через отладку либо вот так (вообще странно, что не гуглятся эти коды где-то в Справке).
Теперь необходимо составить словарь из наших условий для фильтрации - это можно сделать незатейливо вот так:
Если для LayerName всё ясно -- просто наименование слоя, то отдельного пояснения требует указание типа объекта (DxfCode.Start).
Перечень доступных объектов для выбора перечислен здесь. Вносить именно прописными буквами, как там указано (например, при желании выбрать все отрезки и полилинии надо будет подать условие 0,"LINE, POLYLINE".
Итоговый словарь я делаю через PythonScript преобразуя целочисленные ключи DxfCode в строку (так как здесь имеет место быть отвратительное ограничение Dynamo, не позволяющее работать с ключами словарей отличных от string)
Далее фильтр подается на вход ноду SelectObjectsByConditions(), который возвращает в случае наличия объектов, удовлетворяющих условиям выборки, список из ObjectId. В противном случае -- возвращает null.
Примечание: выборку объектов, опять же - вы можете делать и другими средствами и нодами Dynamo, только для последующего использования с методами для RTree необходимо получить от них OBjectId методом GetObjectIdsByObjects_AcadObj():
3. Немного о деревьях в целом и реализация в Dynamo методов по работе с ними и отдельно по боли работы в Dynamo с ними
3.1 О создании дерева
Не вдаваясь в тонкости (так как сам едва-едва начал понимать этот функционал), деревья - это такие структуры данных (как знакомые вам списки, словари, массивы), допускающие вложенность (ветвистость) элементов и позволяющие пространственно индексировать данные (по ближайшему полоожению).
Именно из-за возможности пространственной индексации мы и будем их использовать, а храниться элементы в них будут как структура подобная словарю/двумерному списку - сам объект (его ObjectId) и его границы (BoundingBox), хранящиеся как элементы Rtree.Rectangle - фактически, та же граница.
Сперва я просто создал класс с методами для RTree для своего "нод-пака" Civil3D.CustomNodes, но потом подумал ... что держать все яйца в одной корзине столь обособленный функционал в своём базовом пакете не логично, и вывел его в отдельный пакет DynamoRTree (также с открытым исходным кодом, на GitHub тут).
Сразу оговорюсь, это не последняя версия пакета, там есть ещё ряд методов (RTree.Node) для реализации вложенных "листьев", которые я ещё не делал, а также реализация под Revit.
Деревья я решил создавать на базе ObjectId элементов чертежа. Как получить ObjectId элементов я рассмотрел в разделе 2 ранее, поэтому сразу перейду к сути.
В силу того, что ObjectId есть понятие свойственное в данном контекте AutoCAD (в Revit он свой), то для реализации RTree в AutoCAD/Revit будут свои классы. Вернее, для Revit он будет позже.
Итак, упомянутый пакет DynamoRTree содержит рабочий класс RTree_acad, реализующий основные методы по работе с объектами дерева - его созданием и индексацией, а также пару методов по приведению геометрии Dynamo к типам RTree.
Работа начинается с генерации объектов RTree.Rectangle по данным объектам (введенным пользователем). Это может быть как раз выборка объектов (рассмотренная в разделе 2 раннее) либо дополнительные условия к этой выборке. За этот процесс отвечает нод RTree_acad.GetRTReeRectangleByObjects().
Дерево RTree создается из коллекции ObjectId и связанных с ними RTree.Rectangle. Здесь из-за принципиального ограничения Dynamo о невозможности использовать в качестве ключа словаря не-string элементы я заменяю коллекцию словарем типа <string,object>, где помещаю две записи - где object = спискам.
Да, этот говно-Dynamo даже такие записи отказывается воспроизводить 😓.
public static void TestSmth (int Smth1, out Smth2, out Smth2) {}
Вот и приходиться делать костыли со словарями 😞.
К слову, Dynamo не хочет также знать про ссылочные значения (я хотел передавать деревья как ссылочный тип (ссылка на область в памяти, где оно хранится)), но **** тоже инструкции ref не читает.
3.2 О работе с деревом
Работа с деревом заключается в пространственных запросах к нему - поиск ближайших объектов и объектов в рамках данного контура.
Как таковых методов 3 - это Nearest, Intersects и Contains. Intersects и Contains принимают в качестве аргумента RTree.Rectangle по области вокруг нужной точки (или объекта сразу), а Nearest - объект RTree.Point (получаемый из обычной точки Dynamo).
Расписывать методы не буду, там всё в целом понятно. Лучше покажу их реализацию на отдельных примерах в рамках дальнейшей серии статей (упомянул во введении).
4. Небольшие итоги
Данная статья задумывалась как вводная для последующего погружения в индексацию данных чертежа и не содержит как таковых, боевых примеров скриптов намеренно. Я приведу их далее в цикле статей.
Мы зафиксировали особенности интерпретации данных в среде Dynamo (Handle/ObjectId), привели методы по конвертации объектов чертежа в объекты Dynamo и обратно, а также описали отдельные методы и структуру RTree в составе Dynamo-пакета DynamoRTree.
Не пропускайте публикации, подписывайтесь на Telegram-канал с тизерами статей.
#autodesk #autocad civil3d #autocad api #autodesk api #adn-cis.org #programming #gis #rtree