Обсуждение на форуме DWG.RU
Пример блока с картограммой
Предыстория
С 2000-го по 2005-ый я работал в небольшой проектной фирме, где сначала был чертежником в отделе генплана. Иногда приходилось заниматься расчетом плана земляных масс. Из софта был Autodesk Land Desktop (LDD) и набор DOS-овских программ (sever.exe и plazma.exe, если не ошибаюсь, разработка Гипротюменнефтегаза), в которых был заложен некий алгоритм расчета осадок на болотах.
Процесс ввода данных был муторный - вручную вбивать координаты границ площадки. Потом sever рассчитывал координаты всех узловых и граничных точек. После этого нужно вручную ввести все отметки в узловых и граничных точках. На выходе получался DXF-файл c готовой картограммой.
Надо было как-то автоматизировать процесс. Методом научного тыка был определен формат файлов sever'a (оказалось, что это простые бинарные файлы с фиксированной длиной записи).
Программка на Delphi брала из LDD координаты полилинии (границ площадки) и записывала их в нужный файл расчетной программы. Затем вручную запускался sever и создавал файл с промежуточными точками. Далее снова Delphi, и в файле с промежуточными точками оказывались отметки поверхностей из LDD. Как-то так...
Чтобы исключить ручные шаги, добавил посылку нажатий клавиш в DOSовскую консоль. Стало еще проще. Но все равно это был набор костылей. К тому же нельзя было вмешаться в генерацию DXF-файла.
В итоге всей этой истории было решено написать программу с нуля под Land Desktop. Динамическая библиотека на Delphi вызывалась из Land Desktop (кстати с помощью самописного же модуля MtmdLoadDll на С++) и строила картограмму с осадкой по имеющимся цифровым моделям поверхностей.
Для тех кто не в курсе - вызовы COM-методов из DLL (т.е. в пределах одного процесса) на порядок быстрее чем межпроцессные COM-вызовы
С 2005-го года я долго занимался делами, далекими от генплана, пока в сентябре 2012 года не попал в Тюменьнефтегазпроект, где к тому времени успешно внедрили 3D-проектирование в технологическом отделе.
От меня требовалось перевести на рельсы 3D отдел генплана, учитывая мой некоторый опыт работы с Land Desktop. Всему отделу установили свеженький Civil 3D 2013, который оказался к тому же довольно глючноватым продуктом.
Собственно о Civil 3D
Первым делом я адаптировал свою старую программу на Delphi под новый Civil 3D, что оказалось не так уж и сложно. Но оставалась одна проблема - DLL у меня 32-битная, а у пользователей крутые новые компы с кучей оперативки и конечно с 64-битным Civil 3D. Правда программу можно было еще запускать в виде EXE-шника, но тогда дикие тормоза при общении с Civil 3D гарантированы. В общем, решил переписать на чем-то более правильном. Выбор небольшой - VisualLisp либо C# (ну или даже VB.NET). Учитывая мою любовь к первому и стойкое отвращение ко второму начал писать на лиспе. До сих пор не разочаровался в выборе, хотя под конец пришлось пару небольших функций переписать на C#.
Для справки: Civil 3D судя по всему унаследовал COM-интерфейс от Land Desktop, но в последнее время почти для всего есть и .NET-обертки. Плохо то, что некоторые новые функции появляются только в .NET, а COM-интерфейсов к ним нет. А это значит, что напрямую из лиспа к ним уже не обратиться и нужно писать лисп-функцию на C#.
Писать на лиспе под Civil 3D оказалось довольно приятно. Почувствуйте, как говорится разницу: в C# для отладки нужно каждый раз перезапускать Civil и ждать несколько минут, в случае с VisualLisp я в Civil набираю определенную мной же команду LPZM и вуаля - через секунду загружена свежая версия исходников.
В случае необходимости IDE VisualLisp можно запустить прямо на компьютере пользователя и посмотреть, что пошло не так. Выручало уже несколько раз. К тому же код на лиспе гораздо выразительнее и короче варианта на C#.
Далее попробую перечислить интересные моменты и подводные камни:
в VisualLisp есть возможность импорта tlb-файлов (Type library), но tlb-библиотека Civil 3D ему оказалась не по зубам. Так что пришлось отказаться от этой затеи и обращаться к функциям Civil'а через vlax-get-property, vlax-put-property и vlax-invoke-method
Интерфейсы и методы COM API Civil 3D можно посмотреть в онлайн-справке, значения констант я подглядывал в .pas-файле, созданном в Delphi. Можно также воспользоваться VBA-редактором любого продукта из MS Office
;; алиасы для длинных названий функций
(setq prop> vlax-get-property
putprop> vlax-put-property
m> vlax-invoke-method
d> vlax-dump-object
*pzmXdataAppName "TNGPPZM" ;; имя приложения для хранения XDATA
*pzmXdataVer "1.0" ;; версия расширенных данных блока картограммы
*pzmDebug nil ;; режим отладки (временные поверхности не удаляются)
*pzmProgIdTinVolHelper "TNGP.PZMTinVolumeHelper" )
(defun C:LPZM () ;; команда для быстрой перезагрузки локальной копии
(setq *pzmTestLocal T) ;; тестирование на локальной копии программы
(princ (load "D:/mtm/Projects/TNGP.Civil3D/2013/pzm-lib.lsp"
"\nОшибка загрузки pzm-lib.lsp") )
(princ (load "D:/mtm/Projects/TNGP.Civil3D/2013/pzm-initstyles.lsp"
"\nОшибка загрузки pzm-initstyles.lsp") )
(princ (load "D:/mtm/Projects/TNGP.Civil3D/2013/vertplan.lsp"
"\nОшибка загрузки vertplan.lsp") )
(princ (load "D:/mtm/Projects/TNGP.Civil3D/2013/pzm-xrecord.lsp"
"\nОшибка загрузки pzm-xrecord.lsp") )
(setq *pzmDebug T)
(princ)
)
;; в зависимости от флага *pzmTestLocal грузим локальную или сетевую копию
;; .NET-модуля и DCL-ресурса
;; к тому же программа работает как под Civil 3D 2012, так и под Civil 3D 2013
(cond
((= (acadver) "2012")
(setq
*pzmProgIdAeccApp "AeccXUiLand.AeccApplication.9.0"
*pzmProgIdTinCrData "AeccXLand.AeccTinCreationData.9.0"
*pzmProgIdTinVolCrData "AeccXLand.AeccTinVolumeCreationData.9.0"
*pzmDlgPath "TNGP.Civil3D/2013/pzm2012.dcl"
*pzmDotNetModule "TNGP.Civil3D/2012/TNGP_Civil3D.NET.dll" )
(if *pzmTestLocal
(setq
*pzmDlgPath "d:/TNGP.Civil3D/2013/pzm2012.dcl"
*pzmDotNetModule "d:/TNGP.Civil3D.NET/2012/TNGP_Civil3D.NET.dll" ) ) )
((= (acadver) "2013")
(setq
*pzmProgIdAeccApp "AeccXUiLand.AeccApplication.10.0"
*pzmProgIdTinCrData "AeccXLand.AeccTinCreationData.10.0"
*pzmProgIdTinVolCrData "AeccXLand.AeccTinVolumeCreationData.10.0"
*pzmDlgPath "TNGP.Civil3D/2013/pzm.dcl"
*pzmDotNetModule "TNGP.Civil3D/2013/TNGP_Civil3D.NET.dll" )
(if *pzmTestLocal
(setq
*pzmDlgPath "d:/TNGP.Civil3D/2013/pzm.dcl"
*pzmDotNetModule "d:/TNGP.Civil3D.NET/2013/TNGP_Civil3D.NET.dll" ) ) ) )
;; для корневых объектов используем несколько глобальных переменных
(defun pzm-initrootobjects ()
(cond
((not *pzm-TinVolHelper)
(regapp *pzmXdataAppName)
(setq *acadApp (vlax-get-Acad-object)
*acadDoc (vla-get-ActiveDocument *acadApp)
*aeccApp (vla-getInterfaceObject *acadApp *pzmProgIdAeccApp)
*aeccDoc (prop> *aeccApp 'ActiveDocument)
*aeccDb (prop> *aeccDoc 'Database)
*pzm-TinVolHelper (vla-getInterfaceObject *acadApp *pzmProgIdTinVolHelper) )
(pzm-initAllPointGrpCustProps)
) ) )
;; обнаружилась неприятная особенность встроенной функции rtos - отбрасывание
;; ведущего нуля в зависимости от переменной DIMZIN.
;; В просторах сети нашлась готовая обертка:
;; rtos wrapper - Lee Mac
;; A wrapper for the rtos function to negate the effect of DIMZIN
(defun LM:rtos ( real units prec / dimzin result )
(setq dimzin (getvar 'dimzin))
(setvar 'dimzin 0)
(setq result (vl-catch-all-apply 'rtos (list real units prec)))
(setvar 'dimzin dimzin)
(if (not (vl-catch-all-error-p result))
result ) )
;; как выяснилось, в Civil 3D только у коллекций IAeccSurfaces и IAeccWatershedDrains
;; есть свойство Item, у всех остальных - метод Item. С учетом этого была написана
;; универсальная функция для доступа к элементам коллекций:
(defun item> (collName Itm / coll res)
(setq coll
(cond
((eq (type collName) 'VLA-OBJECT) ;; (item> collObject "ItemName")
collName )
((eq (type collName) 'LIST) ;; (item> (cons *parentObj 'CollName) "ItemName")
(prop> (car collName) (cdr collName)) )
(T (prop> *aeccDb collName)) ) ) ;; (item> 'CollName "ItemName")
(cond
((vlax-method-applicable-p coll 'Item)
(setq res (vl-catch-all-apply 'm> (list coll 'Item Itm))) )
((vlax-property-available-p coll 'Item)
(setq res (vl-catch-all-apply 'prop> (list coll 'Item Itm))) ) )
(if (vl-catch-all-error-p res) nil res) )
;; Примеры вызова:
;; aeccDb.Surfaces("Surface 1") => (item> 'Surfaces "Surface 1")
;; aeccDb.Surfaces("Surface 1") => (item> (cons *aeccDb 'Surfaces) "Surface 1")
;; aeccDb.Surfaces("Surface 1") => (item> (prop> *aeccDb 'Surfaces) "Surface 1")
;; Обработка ошибок.
;; Так как я использую групповую отмену операций (vla-startundomark и vla-endundomark),
;; при возникновении ошибки проверяется состояние стека отмены операций
;; в случае необходимости добавляется маркер закрытия группы.
(defun pzm-ErrHandler (msg)
(if *pzm-error* (setq *error* *pzm-error* *pzm-error* nil))
(if (= 8 (logand 8 (getvar "UNDOCTL"))) (vla-endundomark *acadDoc))
(princ (if errmsg errmsg msg)) )
;; аналог функции assert: если первый аргумент вычисляется в nil,
;; программа завершается с внятным сообщением об ошибке
;; пример: (pzm-assert 'elev "\nОшибка: elev = nil")
(defun pzm-assert (expr msg)
(if (not (eval expr)) (pzm-ExitWithMsg msg)) )
;; функция завершения программы с сообщением об ошибке:
;; пример: (if (not var) (pzm-ExitWithMsg "\nError: var=nil"))
(defun pzm-ExitWithMsg (errmsg)
(if *error* (setq *pzm-error* *error*))
(setq *error* pzm-ErrHandler)
(quit) )
В ходе разработки возникли проблемы с созданием поверхности объема. Оказалось, что из лиспа не работает присвоение объектных свойств:
TinCreationData.BaseSurface = BaseSurface; TinCreationData.ComparisonSurface = CompSurface;
На помощь пришла чудесная вещь под названием WSC (Windows Script Components) - сценарий на VBScript/JScript, завернутый в специальный XML-формат и регистрируемый в системе как обычный COM-server. Как оказалось, эти сценарии отлично загружаются и в 64-битный процесс.
Файл tinvolume-helper.wsc
<?xml version='1.0' encoding='windows-1251' standalone='yes'?>
<component>
<registration
description="Tin volume creation helper"
progid="TNGP.PZMTinVolumeHelper"
version="1.00"
classid="{1AC5DCE8-0A61-4697-B1C6-62A62D6E5124}"
/>
<public>
<method name='AssignSurfaces'>
<parameter name="TinCreationData"/>
<parameter name="BaseSurface"/>
<parameter name="CompSurface"/>
</method>
</public>
<script language="JScript"><![CDATA[
function AssignSurfaces(TinCreationData, BaseSurface, CompSurface) {
TinCreationData.BaseSurface = BaseSurface;
TinCreationData.ComparisonSurface = CompSurface;
return 0;
}
]]></script>
</component>
Далее регистрируем сценарий: regsvr32 tinvolume-helper.wsc (кстати лучше вместо этого сделать обычный reg-файл, который может указывать на HKCU\SOFTWARE\Classes, тогда права администратора для регистрации компонента не потребуются. Теперь через vla-getInterfaceObject мы можем загрузить наш сценарий.
... (setq *pzm-TinVolHelper (vla-getInterfaceObject *acadApp "TNGP.PZMTinVolumeHelper")) ... (defun pzm-AddVolSurf (Name BaseSurf CompSurf / crData surf) (or (item> 'SurfaceStyles "NullWorkLines") (pzm-CheckSurfStyles)) (if (setq surf (pzm-getSurface Name)) (m> surf 'Delete)) (setq crData (vla-getInterfaceObject *acadApp *pzmProgIdTinVolCrData)) (putprop> crData 'Name Name) (putprop> crData 'Style "NullWorkLines") (putprop> crData 'Layer "0") (putprop> crData 'BaseLayer "0") (putprop> crData 'Description "Volume surface") (m> *pzm-TinVolHelper 'AssignSurfaces crData BaseSurf CompSurf nil nil) (m> (pzm-Surfaces) 'AddTinVolumeSurface crData) )
Для построения картограммы я использую обычный блок с автоматической генерацией имени. Настройки конкретного экземпляра картограммы хранятся в расширенных данных ссылки на блок (BlockReference). Для генерации блока есть два способа: классический (entmake) и COM. Я выбрал первый вариант. Построение картограммы (с учетом расчета осадок) занимает буквально несколько секунд.
Особенности entmake: при повторном перестроении блока старое содержимое заменяется автоматически. При использовании COM блок бы пришлось предварительно очистить. Но способ с COM позволяет сделать хитрый финт ушами: можно обновить отметки в блоке картограммы, не перестраивая и не удаляя графические элементы. У пользователя появляется возможность редактировать картограмму и после этого обновлять ее, не теряя результатов редактирования.
Однако в дальнейшем я столкнулся с трудностями при отрисовке мультивыноски (объект MLEADER): непонятно, как с помощью entmake создать объект MLEADER c прикрепленным к нему блоком да еще и с атрибутами. В итоге пришлось MLEADER добавлять через COM уже после создания блока.
Возможно кому-то пригодится информация о работе с пользовательскими свойствами точек (AeccPoint). Оказывается для точек можно определить дополнительные поля разных типов (строки, числа и т.п) и использовать их для хранения и отображения дополнительной информации. Нюанс в том, что для работы с этими свойствами нужно предварительно вызвать метод SetUserDefinedPropertyClassification для группы точек (AeccPointGroup). В коде ниже приведен пример вызова этого метода
Также можно программно инициализировать практически все стили и настройки чертежа. Примеры смотрите ниже. Правда дело это неблагодарное... всё же гораздо проще один раз настроить шаблон ручками. Преимущество программного метода только в том, что чертеж может быть создан на любом (или на более старом) шаблоне, в котором не оказалось нужных стилей/настроек.
;; Для доступа к custom-свойствам точек необходимо для группы точек
;; вызвать SetUserDefinedPropertyClassification
(defun pzm-initAllPointGrpCustProps ( / clName udp)
(setq
clName "ГП" ;; наша группа классификации (в шаблоне)
udp (item> 'PointUserDefinedPropertyClassifications clName) )
(if (not udp) (pzm-ExitWithMsg
(strcat "\nОшибка! Классификация пользовательских свойств \""
clName "\" не найдена") ) )
(vlax-for grp (prop> *aeccDb 'PointGroups)
;; aeccUDPClassificationApplyAll = 1
(m> grp 'SetUserDefinedPropertyClassification 1 udp) ) )
(defun GetOrCreateItem (collName Name / coll)
(setq curStyle ;; curStyle - глобальная переменная, указывает на текущий стиль
(cond
((item> (setq coll (prop> *aeccDb collName)) Name))
((m> coll 'Add Name)) ) ) )
(defun setitem (L Val / Itm)
(setq Itm curStyle)
(cond
((listp L)
(while (cdr L) (setq Itm (prop> Itm (car L)) L (cdr L) ))
(putprop> Itm (car L) Val) )
(T (putprop> Itm L Val)) ) )
(defun pzm-GeneralSettings ()
;aeccDrawingUnitMeters = 2,
;; *aeccDb.Settings.DrawingSettings.UnitZoneSettings.DrawingUnits = aeccDrawingUnitMeters
(setq curStyle *aeccDb)
(setitem '(Settings DrawingSettings UnitZoneSettings DrawingUnits) 2) )
;; настройка стилей точек
(defun pzm-CheckPointStyles ( /
curStyle add-udp udpc props grpPeat)
;--- local functions ---
;; aeccUDPPropertyFieldTypeString = 17
(defun add-udp (Name Desc)
(m> props 'Add Name Desc 17 :vlax-false :vlax-false 0
:vlax-false :vlax-false 0 :vlax-true "??,??" 0) )
;--- end local functions ---
;; создаем стиль точки
(GetOrCreateItem 'PointStyles "Точка планировки")
(setitem 'CustomMarkerStyle 2)
(setitem 'MarkerSize 0.001)
(GetOrCreateItem 'PointStyles "Точка болота")
(setitem 'CustomMarkerStyle 2)
(setitem 'CustomMarkerSuperimposeStyle 2)
(setitem 'MarkerSize 0.001)
;; стили отметок для точек
(GetOrCreateItem 'PointLabelStyles "Номер-Отметка-Описание")
(GetOrCreateItem 'PointLabelStyles "Точка планировки")
(GetOrCreateItem 'PointLabelStyles "Отметка болота")
(vlax-for itm (prop> curStyle 'TextComponents)
(cond
((= (prop> itm 'Name) "Описание точки")
(putprop> (prop> itm 'AnchorPoint) 'Value 5)
(putprop> (prop> itm 'Attachment) 'Value 3)
(putprop> (prop> itm 'AnchorComponent) 'Value "")
;(d> (prop> itm 'AnchorComponent))
;(princ (strcat "\n" (prop> itm 'Name)))
)
(T (putprop> (prop> itm 'Visibility) 'Value :vlax-false)) ) )
;; создаем пользовательские свойства для точек планировки
(setq udpc (prop> *aeccDb 'PointUserDefinedPropertyClassifications)
props (prop> (cond ((item> udpc "ГП")) ((m> udpc 'Add "ГП"))) 'UserDefinedProperties))
(or (item> props "DIFF") (add-udp "DIFF" "Рабочая отметка"))
(or (item> props "EG") (add-udp "EG" "Черная отметка")) )
Когда же мне понадобился C#? Пришлось на нем накатать пару функций:
(TNGP:SetVertexElevations surfename '((x y z) (x y z)... )), которая устанавливает отметки в нескольких точках поверхности (это потребовалось при генерации поверхности осадки);
(TNGP:PasteSurface dstsurfename srcsurfename), которая вставляет одну поверхность в другую.
Увы, разработчики Civil 3D не предоставили COM-интерфейс...
Комментариев нет:
Отправить комментарий