AutoCAD и подобные ему САПР давно уже стали стандартом в области проектирования, и неудивительно что таким же стандартом стали широко используемые в них форматы файлов DWG/DXF. Так что если вы разрабатываете какое-то решение для архитекторов и проектировщиков, то умение работать с этими форматами (ну или хотя бы с одним из них) — must have фича вашего продукта.
В рамках своего вебсервиса для симуляции движения пешеходов пришлось и мне озаботиться импортом генпланов в этих форматах. Раньше с САПР я дела не имел, поэтому наивно думал «да что там, подумаешь — еще один формат чертежей, линии и многоугольники, что там может быть сложного?». Но в процессе работы выяснилось что сложного там может быть достаточно, некоторые нюансы вполне похожи на древние костыли, тянущиеся из глубин веков, при этом многие вещи толком не документированы в спецификациях самого формата (например работа с блоками или с кривыми). Видимо они считаются очевидными для любого чертежника, но что делать если вы родом из другой области, и таких знаний не имеете?
В общем под катом — перечисление граблей и решений, которые не удалось нагуглить и пришлось добывать полуночными бдениями над чертежами.
Решаемая задача
Мне для моего приложения на Java нужно было настроить импорт генпланов районов и конвертацию его во внутреннее упрощенное GeoJSON представление. При этом мне не нужна была полная информация и все виды сущностей, лишь некоторая их часть, которая бы использовалась в симуляции. Так что данный материал не охватывает все возможности и Нетривиальные Технические Решения DXF. А почему именно DXF, а не DWG? А про это ниже.
Выбор формата
Итак, что в первую очередь ассоциируется со словами «автокад» и «формат файла»? А это DWG. Бинарный закрытый формат, который изначально был создан AutoDesk'ом и его спецификации не раскрывались, однако в свое время он был удачно реверснут Open Design Alliance.
И вот тут следует разочарование №1: не существует актуальных бесплатных реализаций этого формата. Вообще.
Есть библиотека по работе с ним от AutoDesk. Есть популярная библиотека Teigha, созданная ODA. И… все. Обе они платные, причем хорошо платные (речь о сотнях и тысячах долларов). Не подходит.
Есть некоторое количество попыток реализовать стандарт в виде бесплатного Open-source решения. Например jdwglib. Но все они давно мертвы, обновлялись последний раз 5-10 лет назад. А прогресс не стоит на месте, новые версии автокада добавляют новые фичи и в DWG, в итоге с мечтой читать файлы современных версий можете попрощаться, как и с поддержкой и надеждой на фикс багов.
Альтернативой является DXF. Несколько менее популярный, но в то же время поддерживаемый всеми САПР, изначально открытый и поэтому, по идее, более распространенный.
Поиск библиотек поначалу тоже обескураживает — конкретно для Java нет ни одного живого проекта, везде та же картина: последние релизы 5-летней давности, заброшенные репозитории, грустно глядящие в вечность последние новости, полные неоправдавшегося оптимима и обещаний. Но сам по себе формат в отличие от DWG не так активно развивается, поэтому даже довольно старой библиотекой вполне можно открыть актуальные чертежи.
В итоге была выбрана библиотека Kabeja, последний релиз которой был в 2011 году. С помощью идущего в комплекте сэмпла (конвертация DXF в SVG) было проверено что все актуальные файлы чертежей корректно открываются, после чего я приступил к импорту. Слегка правда насторожил меня один комментарий к вопросу про парсинг DXF от некоего CAD-гуру на Stackoverflow что, мол, «DXF выглядит простым но на деле ты запаришься с ним работать».
Слои
DXF чертеж содержит в себе набор слоев (layer) и блоков (block). Там есть и другие сущности, но для того чтобы выдрать координаты геометрии в простейшем случае они не нужны.
Со слоями все очевидно, работают они так же как и в каком-нибудь Photoshop. Слои можно включать-выключать и можно задавать дефолтные параметры графики для слоя (то есть например все линии по умолчанию на данном слое будут иметь такую-то толщину). Поскольку моей задачей была только выжимка координат, вопросами отображения я не занимался.
Окей, все кажется просто: бежим по списку слоев, для каждого слоя — по списку объектов, преобразуем координаты. Впрочем уже тут я наступил на первые грабли: набор слоев который вы видите в CAD и который есть в файле — это не одно и то же. Я себе голову сломал, почему у меня вдруг пропадали куски дорог. В NanoCAD они есть, в моем экспорте — нет. Полез в отладчик — их и в возвращаемых Kabeja структурах нет. Зато если проэкспортировать файл целиком их семплом — они есть. В общем выяснилось что один слой из редактора в файле может представляться несколькими слоями, с именами вида «layerName», «layerName @ 1». Зачем это сделано и откуда оно берется — черт его знает, но факт — поиск на точное совпадение имени слоя (на который намекает даже структура кода библиотеки, хранящая слои в Map с ключом-именем) не работает.
Блоки
Блоки представляют собой шаблоны, которые единожды нарисовав можно многократно вставлять. При этом изменение базового блока изменит и все его вставки. Удобно. Еще круче то, что блок может содержать в себе объекты с нескольких слоев. При этом вставка тоже принадлежит какому-то слою. При этом блок может содержать в себе вставки других блоков. То есть можно сделать блоки «секция дома», затем составить из них блок «дом», который затем несколько раз вставить на карту. При этом итоговый объект будет иметь несколько слоев (отдельно заливка, отдельно контуры, отдельно специальные пометки), так же как исходные блоки. Все это очень круто с точки зрения пользователя, но добавляет работы программисту.
Более того, блок может вставляться не просто разово, а многократно в виде прямоугольной матрицы. Для этого у объекта вставки есть параметры с количеством рядов, столбцов и с расстоянием между ними.
В итоге пока что код обработки вставки выглядит как-то так:
for (int row = 0; row < insert.getRows(); ++row) {
for (int col = 0; col < insert.getColumns(); ++col) {
// Нам надо преобразовать координаты точек из локальных координат блока в глобальные координаты чертежа с учетом места вставки, угла поворота и т.п
AffineTransform transform = new AffineTransform();
transform.translate(
insert.getPoint().getX() - (insert.getColumns() - col) * insert.getColumnSpacing()
, insert.getPoint().getY() - (insert.getRows() - row) * insert.getRowSpacing());
transform.rotate(Math.toRadians(insert.getRotate()));
transform.scale(insert.getScaleX(), insert.getScaleY());
// Feature это уже наш объект, содержащий преобразованные в GeoJSON координаты
for (Feature f : block.features) {
// Копируем содержимое блока, преобразуем координаты. В итоге получаем реальное положение дома на плане
Feature inserted = cloneFeatureWithTransform(f, transform);
features.add(inserted);
}
}
}
return block.features.size();
При этом еще важно знать что блоки и слои в файле не упорядочены. То есть поочередно обрабатывая блоки можно наткнуться в нем на вставку из другого блока, который еще не был обработан. Честно говоря не знаю, можно ли умудриться сделать цикл и что в таком случае будет.
Линии
Напомню, что моя задача — преобразовать DXF в GeoJSON, который из всех видов геометрии признает лишь ломаную и многоугольник, никаких дуг и кривых.
DXF поддерживает кучу разных вариантов линий:
- Аж 2 типа ломаных — Polyline и LWPolyline. В моем случае простых 2д чертежей разницы между ними никакой
- Дуги, причем аж двух видов — эллиптические и круговые. К счастью в классах Kabeja уже есть готовые методы для получения координат точек на них, так что преобразовать дугу в ломаную с нужной точностью несложно
- Сплайны — опять же Kabeja сама умеет их преобразовывать в Polyline
- Просто линейные отрезки
Казалось бы все просто, но нет. Даже простой на первый взгляд тип Polyline может использоваться для отображения кривых второго порядка (а не просто ломаных). Для этого у вершины может быть задан параметр bulge. Если он указан то две вершины соединяются не прямой линией, а дугой окружности, проходящей через эти вершины и центр которой можно выразить через них и этот параметр.
Вот такой вот код позволяет определить центр окружности:
private Point getCenterByVerticesAndBulge(DXFVertex a, DXFVertex b, double bulge) {
double norm = Math.sqrt(Math.pow(b.getX() - a.getX(), 2) + Math.pow(b.getY() - a.getY(), 2));
double s = norm / 2;
double d = s * (1 - bulge * bulge) / (bulge * bulge);
// "direction"
double u = (b.getX() - a.getX()) / norm;
double v = (b.getY() - a.getY()) / norm;
//"center"
double c1 = Math.signum(bulge) * -v * d + (a.getX() + b.getX()) / 2;
double c2 = Math.signum(bulge) * u * d + (a.getY() + b.getY()) / 2;
return new Point(c1, c2, 0);
}
Над этими дугами я бился долго, да в итоге плюнул и отключил их, соединяя вершины напрямую. Так как в генпланах такие дуги обычно используют для закруглений на углах перекрестков, я мог на них вполне забить — разница с точки зрения симуляции невелика.
Заливки/штриховки (hatch)
Это то с чем непосредственно работает мой симулятор. Здания, дороги и генераторы я пока требую задавать именно заливками (иначе непонятно где в мешанине отдельных линий внутренняя часть, а где — внешняя).
И тут тоже есть нюансы:
- Границей заливки может быть любая комбинация объектов-линий. Часть границы может быть ломаной, затем несколько дуг, потом просто куча отрезков разных линий
- Один объект заливки может иметь произвольное число непересекающихся областей (что нехарактерно для многих других форматов где у многоугольника есть только одна внешняя граница), каждая из которых может иметь произвольное число дырок
- Заливки могут быть многократно вложенными: то есть внутри заливки дырка, в которой еще одна заливка, в которой снова дырка, причем все это в DXF задается одним объектом HATCH с несколькими границами.
- Есть и еще более экзотические варианты отношений заливки и ее границ (см картинку) но мне они пока слава богу не попадались
В общем из DXF для заливки мы получаем кучу границ, имеющих флаг «внешняя или внутренняя», а дальше сами должны как-то разбираться как они вообще сделаны и как их раскидать по GeoJSON-овским полигонам, которые могут иметь только одну внешнюю границу и не имеют вложенности.
Я сходил по нескольким путям, но на каждый алгоритм я довольно быстро получал чертеж, на котором этот алгоритм не работал. Например вот этот вот: схема улиц и проездов для жилого района Читы, в котором они все заданы буквально парой объектов HATCH с очень сложной структурой, в которой почему-то все границы были помечены как внешние (чую тут какой-то баг в Kabeja, так как DXF определяет сразу два схожих флага External и Outer, но в самой библиотеке есть только один):
В итоге единственный рабочий алгоритм выглядит так:
- Создать полигон для каждой внешней границы
- Вычесть из него все остальные границы, неважно отмечены ли они внешними или внутренними
- Исправить возможные проблемы (полигон получился пустой, дырки выходят за границы внешнего контура, полигон разбился на несвязанные области и т.п.)
Для третьего пункта (да и вообще для работы с геометрией уже внутри самого алгоритма симуляции) я использовал библиотеку JTS — Java Topology Suite. Она содержит довольно много всяких нужных примитивов и операций по работе с геометрией, начиная от операций типа построения буфера и заканчивая структурами данных типа квадродерева.
Победа?
После множества мучений и подперев все что можно костылями, я все-таки смог создать поддержку нужного мне подмножества DXF и загружать чертежи напрямую в свой симулятор, чтобы использовать их для определения косяков проектировщиков. Поскольку большую часть из указанной выше информацией мне пришлось добывать с боем и сидением до двух ночи над NanoCAD-ом (не реклама, но это единственный легкодоступный бесплатный и качественный редактор DXF который я нашел, тот же LibreCAD не осилил открыть правильно первый же чертеж что я ему передал), я решил поделиться ей и с читателим — вдруг мой опыт сэкономит кому-то время.
Ну и да, вот как-то так выглядит предсказание моего алгоритма для района со скриншота выше:
Очевидные выводы — не надо делать дорожки под прямым углом, но не надо и пытаться делать какие-то непонятные закругления там где не надо. Посмотрим через несколько лет, оправдается ли мой прогноз когда район этот построят.
За предоставленные планы районов спасибо Мастерской комплексного архитектурно-строительного проектирования
Автор: JediPhilosopher