воскресенье, 11 октября 2009 г.

Интеграция TDMS и AutoCAD

Известно, что TDMS использует и ориентируется на механизм захвата и возврата (CheckOut/CheckIn в терминах программиста и команды Сохранить, Сохранить и закрыть, Отменить редактирование в терминах пользователя в меню Редактировать->Редактируемые объекты) файлов, хранящихся в объектах, который универсален, но применим больше для распределенной работы. Однако такой механизм при работе в рамках единого информационного пространства одного офиса не устраивает. Было принято решение реализовать работу с файлами по-своему.

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

Во-вторых, захваченные файлы, следуя идеологии TDMS, предполагается размещать на локальном диске рабочей станции пользователя. Следовательно, ни о какой интерактивности в работе и речи быть не может, т.е. исключается возможность online режима работы с внешними ссылками в AutoCAD'е. Уведомления в AutoCAD'е об изменении файла внешней ссылки не появятся никогда. Обидно терять такой функционал.

Как сделать так, чтобы файлы хранились в TDMS, но работа с ними была прозрачной? Чтобы решить задачу были выработаны следующие критерии:

  1. Файлы должны именоваться предсказуемо, т.е. имя файла должен давать не пользователь, а TDMS и не позволять именовать иначе.
  2. Файл должен захватываться на сетевой ресурс (файловый сервер), а не на локальный диск рабочей станции.
  3. Пути к файлам и компонентам не должны меняться без серьезных оснований.
  4. Файл должен захватывать за собой все компоненты (внешние ссылки и растры).
  5. Файл после сохранения или закрытия сразу должен попадать в TDMS.
  6. Файл после сохранения или закрытия должен сохранять вместе с собой растровые изображения, если они были изменены.

Этап 1 – подготовка к захвату.

Практически любая операция, изменяющая состав файлов в объекте TDMS влечет за собой вызов кода отвечающего за именование файлов в объекте. Чтобы имена файлов не совпадали, был досконально изучен документ, определяющий обозначение проектной документации и алгоритм размещения файлов в TDMS. В итоге, был принят следующий принцип:

Nindex description.ext, где

index – порядковый номер файла в коллекции;

description – наименование объекта, которое опирается на обозначение документа и стремится к уникальности;

ext – расширение файла

Причем из имени файла сразу исключаются недопустимые символы: косая черта, двоеточие, кавычки.

Выглядит это примерно так:

Таким образом, выполнились требования пункта 1.

Этап 2 – место для захвата.

Сетевой ресурс для размещения файлов создать было несложно. Файловый сервер уже есть, выделили каталог, расшарили, дали полные права пользователям и подключили ко всем рабочим станциям как диск P.

Получили \\server\tdms подключенный как диск P:.

Кроме этого, сразу пришлось отказаться от того, как TDMS формирует путь к захваченным файлам. Пришлось написать функции возвращающую новый путь захвата файла.

Было: Vol:\Catalog\UserName – GUID\filename.ext.

Стало: Vol:\GUID\filename.ext

Отказать от GUID нельзя, т.к. только по нему можно определить, к какому объекту относится файл.

Таким образом, выполнились требования пунктов 2 и 3.

Этап 3 – собственно сам захват.

Открытие файла из TDMS для пользователя должно происходит предельно прозрачно, он не должен задумываться, что происходит в системе, но должен в итоге увидеть открытый файл в соответствующем приложении.

В TDMS с привязкой к типу файла были написаны две команды: Открыть и Просмотр. Открытие файла в AutoCAD предполагает:

  • открытие AutoCAD'а, если он закрыт;
  • поиск открываемого файла в открытом AutoCAD'е, вдруг он уже открыт;
  • проверка прав пользователя на открытие для внесения изменений;
  • проверка возможности открытия файла, вдруг он открыт другим пользователем;
  • открытие файла.

Открыть AutoCAD на VBScript, тривиальная задача. Была написана следующая функция:

Function InitApp
Set InitApp = Nothing
dim acadApp
On Error Resume Next
Set acadApp = GetObject(,"AutoCAD.Application")
if Err then
Err.Clear
Set acadApp = CreateObject("AutoCAD.Application")
end if

if Err then
Err.Clear
MsgBox "Невозможно открыть Autodesk AutoCAD! Попробуйте открыть его самостоятельно и повторите команду.", 64, "Ошибка"
end if

acadApp.Visible = true

if acadApp.Documents.Count > 0 then
dim i
for i = 0 to 5000
if acadApp.GetAcadState.IsQuiescent = true then exit for
next
if acadApp.GetAcadState.IsQuiescent = false then
MsgBox "Autodesk AutoCAD занят выполнением команды. Дождитесь завершения или отмените ее!", 64, "Ошибка"
exit function
end if
end if

Set InitApp = acadApp
end Function

Хоть функция и ожидает открытия AutoCAD'а, все-таки пользователям рекомендовано самостоятельно открывать AutoCAD, чтобы не создавать недоразумений. Некоторые категории пользователей не подождав и секунды начинают судорожно повторять вызов команды Открыть. Человеческий фактор.

Поиск открываемого файла в сеансе AutoCAD тоже несложен

dim found
dim doc
found = false
For Each doc In app.Documents
If doc.FullName = ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", main_file) Then
Set OpenFile = doc
doc.Activate
found = True
exit for
end if
Next

Функция WorkFileName_TechDoc из модуля MAIN как раз возвращает полный путь к захватываемому файлу.

Проверку прав пользователя на открытие файла написал мой коллега, кроме этого, код зависит от структуры прав и т.д., поэтому приводить ее код не буду. Функция просто возвращает readonly=true/false в зависимости от прав.

Проверка возможности открытия файла, если он вдруг открыт другим пользователем с сетевого ресурса, наиболее интересная задачка. Коллегой был написан скрипт, выполняемый на стороне сервера, который заполняет на шаре \\server\tdms файл access.log каждые 10 секунд. В файле формируется список вида:

С помощью регулярных выражений уже на стороне клиента в файле access.log ведется поиск записи о недоступности файла и если он действительно открыт другим пользователем, то пользователь получает соответствующее уведомление, а также предложение открыть его в режиме только для чтения.

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

Функция захвата компонент:

Function CheckOutComponents(Obj)
if Obj.Attributes.Has("TECHDOC_COMPONENTS") then
dim attr
Set attr = Obj.Attributes("TECHDOC_COMPONENTS")
dim row
on error resume next
For Each row in attr.Rows
dim file
if not row.Attributes("TECHDOC_COMPONENTS_LINK").Object is nothing then
For Each file In row.Attributes("TECHDOC_COMPONENTS_LINK").Object.Files
file.CheckOut WorkFileName_TechDoc(File)
if err then
err.clear
end if
Next
end if
Next
end if
End Function

Фрагмент кода открытия файла:

dim file
For Each file In real_obj.Files
file.CheckOut ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", File)
Next
Set doc = app.Documents.Open(ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", main_file), mode)

Из кода видно, что при захвате файла вместе с ним захватываются и все файлы объекта, среди которых могут также оказаться компоненты (в основном растровые файлы).

Таким образом, выполнились требования пункта 4.

Этап 4 – сохранить и закрыть.

Наиболее сложным и интересным оказалось создание кода, обрабатывающего сохранение и закрытие файла в AutoCAD'е. Ответственность кода и необходимость его оперативного изменения в случае обнаружения ошибок, а также возможность корректировки другими сотрудниками во время моего отсутствия или занятости привело к выбору Lisp'а в роли платформы для реализации кода.

Код условно разделен на три составляющие:

  • сохранение растровых компонент и информации о них;
  • сохранение информации о внешних ссылках;
  • сохранение самого файла.

Объектная модель AutoCAD и информация о примитивах, предоставляемая в Lisp, очень скупы при работе с растровыми изображениями, поэтому написание соответствующего кода отняло некоторое время.

Фрагмент кода работы с растровыми изображениями:

(defun hasImage( / dict ret)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
ret nil
)

(if (not (vl-catch-all-error-p dict))
(setq ret (> (vlax-get-property dict 'Count) 0))
)

ret
)

(defun listImage( / dict objimage ret)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
ret nil
)

(if (not (vl-catch-all-error-p dict))
(vlax-for objimage dict
(setq ret (append ret (list (cdr (assoc 1 (entget (handent (vlax-get-property objimage 'Handle))))))))
)
)
ret
)

Фрагмент кода отвечающего за сохранение растрового изображения средствами Raster Design:

(defun try_save_image_by_co ( fullname pathlist / coList coFileName coWrite ic cc imID )
;fullname must be is raster image file name
(if (/= (member "aeciui42.arx" (arx)) nil)
(progn
(princ "\nRaster Design загружен.")
(setq coList (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecImageObjectList"))
(setq ic 0 cc (vlax-get-property coList 'CurrentSpaceCount))
(if (> cc 0)
(progn
(setq coFileName (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecCoImageInfo"))
(while (<>
(setq imID (vlax-invoke-method coList 'CurrentSpaceObjectID ic))
(vlax-put-property coFileName 'ImageObjectID imID)
(if (= (vlax-get-property coFileName 'SavedImageFilePath) fullname)
(progn
(setq coWrite (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecImageWrite"))
(vlax-put-property coWrite 'ImageObjectID imID)
(vlax-invoke-method coWrite 'SaveAs fullname)
(vlax-release-object coWrite)
(princ (strcat "\n" (nth 2 pathlist) "\tсохранен."))
)
)

(setq ic (1+ ic))
)))

(vlax-release-object coFileName)
(vlax-release-object coList)
)

(princ "\nRaster Design не загружен.")
);if member
T
)

Помимо простого сохранения и сдачи растровых изображений хранимых в чертеже в TDMS, код распознает изображения подключенные не с диска P: и автоматически помещает их в объект, в котором хранится чертеж. Таким образом, пользователь избавлен от необходимости импортировать растр в TDMS, а потом организовывать на него ссылку.

Фрагмент кода отвечающего за изменение имени файла растрового изображения:

(defun changeImageFileName ( oldfileName newFileName / dict objimage obj)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
)

(if (not (vl-catch-all-error-p dict))
(vlax-for objimage dict
(setq obj (entget (handent (vlax-get-property objimage 'Handle))))
(if (= (cdr (assoc 1 obj)) oldFileName)
(progn
(setq obj (subst (cons 1 newFileName) (assoc 1 obj) obj))
(entmod obj)
)))))

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

Фрагмент кода работы с внешними ссылками:

(defun hasXref( / ablocks i cnt ret)
(setq
ablocks (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Blocks)
cnt (vlax-get-property ablocks 'Count)
i 0
ret nil
)

(while (and (<>
(setq
ret (if (= (vlax-get-property (vlax-invoke-method ablocks 'Item i) 'IsXRef) :vlax-true) T)
i (1+ i)
))

(vlax-release-object ablocks)
ret
)

(defun listXref( / ablocks ablock ret)
(setq
ablocks (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Blocks)
ret nil
)

(vlax-for ablock ablocks
(if (= (vlax-get-property ablock 'IsXRef) :vlax-true)
(setq ret (append ret (list (vlax-get-property ablock 'Path))))
))

(vlax-release-object ablocks)
ret
)

Фрагмент кода очистки табличного атрибута:

(setq
attrRows (vlax-get-property TDMSAttribute 'Rows)
)

(while (> (vlax-get-property attrRows 'Count) 0)
(vlax-invoke-method attrRows 'Remove 0)
)

Фрагмент кода заполнения табличного атрибута:

(setq
attrRows (vlax-get-property TDMSAttribute 'Rows)
)

(setq
attrRow (vlax-invoke-method attrRows 'Create)
attrRow (vlax-get-property (vlax-get-property attrRow 'Attributes) 'Item 0)
)

(vlax-put-property attrRow 'Object (car component))

Перехват событий при сохранении и закрытии осуществлен с помощью реакторов: :vlr-saveComplete и :vlr-beginClose. Закрытие перехватывается еще и для создания файла индексов, о котором было сказано в блоге TDMS и NormaCS.

Фрагмент кода отвечающего за сдачу файла:

;pathlist – список с разбитым на части полным именем файла vol, guid, filename
(defun file_checkout ( fullname pathlist / TDMSObj TDMSFiles TDMSFile )
(setq TDMSObj (vlax-invoke-method TDMS 'GetObjectByGUID (nth 1 pathlist)))
(if (not TDMSObj)
(progn
(princ (strcat "\n" (nth 2 pathlist) "\tне сдан. Объект для файла не найден."))
(vl-exit-with-value 3)
))

(setq TDMSFiles (vlax-get-property TDMSObj 'Files))
(vlax-for TDMSFile TDMSFiles
(if (= (nth 2 pathlist) (vlax-get-property TDMSFile 'FileName))
(progn
(vlax-invoke-method TDMSFile 'CheckIn fullname)
(princ (strcat "\n" (nth 2 pathlist) "\tсдан."))
)))

(vlax-release-object TDMSFiles)
(vlax-release-object TDMSObj)
T
)

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

Таким образом, выполнились требования пунктов 5 и 6.

Кроме этого, попутно было написано небольшое приложение на ObjectARX под названием AlxdIntercept.arx, которое отслеживает попытку подключить внешнюю ссылку в чертеж из стандартного менеджера внешних ссылок и, в случае, если открытый файл размещен в TDMS, подменяет стандартное окно выбора файла на окно выбора объекта из TDMS.

Преимущества решения для инженера:

  • исключение встроенного механизма захвата/сдачи из работы;
  • практически полное исключение операций помещения растровых изображений в TDMS вручную;
  • прозрачная работа с файлами, т.е. пользователь, как будто работает с файловой системой;
  • режим online для внешних ссылок, т.к. они все ссылаются на общий сетевой ресурс, и каждый пользователь мгновенно получает сообщение об изменении внешней ссылки средствами AutoCAD'а;
  • вставка внешних ссылок не требует дополнительных команд и выполняется стандартными средствами AutoCAD'а с минимальным вмешательством.

Недостатки:

  • если растровое изображение уже размещено на P:, но удалено из объекта, то код не отслеживает этого факта и файл может быть утерян;
  • изменение путей к растрам при сохранении срабатывает только после "двойного" сохранения, что при сохранении и так выполняется, а при закрытии не срабатывает, поэтому путь к растру при первом закрытии с сохранением не изменится;
  • код опирается на именование файлов.

Нехватка:

  • отсутствие методов или свойств управления порядком файлов в коллекции файлов (например, методов Up и Down или Swap) объекта TDMS не позволяет написать код, отслеживающий вредительство пользователей, например, удаление востребованного растра из объекта, изменение индекса файла в коллекции файлов являющегося компонентом документа и т.п.
Применение:

1 комментарий: