Теги динамических чертежей профилей

В программном комплексе Топоматик Робур чертежи профилей генерируются на основе шаблонов. Шаблон чертежа профиля представляет из себя файл в формате DXF (Drawing eXchange Format). При генерации чертежа используются специальные динамические объекты называемые тегами. Теги это элементы шаблона чертежа, которые представляют необходимую информацию о модели в требуемом графическом виде.

Алгоритм создания пользовательского тега

Для создания пользовательского тега необходимы следующие действия:

Создание класса тега, содержащего алгоритм отрисовки графической информации на чертеже

Создание класса провайдера тега, отвечающего за регистрацию его в системе

Добавление команды регистрирующей тег в теле пользовательского модуля

Добавление таска для регистрации тега при инициализации пользовательского модуля

Создание в plugin файле action'a добавления тега в шаблон чертежа

Программно, тег является классом наследником от PrfField для продольного профиля и CrsField для поперечного профиля. Оба эти класса, в свою очередь, являются наследниками класса TemplateField. Класс тега содержит его свойства и описание алгоритмов построения графических примитивов.

Провайдер тега это класс наследник от TemplateFieldProvider, с помощью которого мы будем регистрировать наш тег в системе.

Подготовка модуля

Создайте и настройте новый модуль для подключения к программному комплексу Топоматик Робур.

С помощью диалогового окна Менеджер ссылок добавьте ссылки на следующие библиотеки:

Topomatic.Alg.dll - базовые классы подобъектов

Topomatic.Alg.Runtime.dll - возможность использования статического класса AlgCoreTools, для доступа к необходимым константам

Topomatic.Cad.Foundation.dll - базовые математические типы и операции

Topomatic.Cad.View.dll - элемент управления для отображения слоёв моделей

Topomatic.ComponentModel.dll - возможность использования статического класса TypeExplorer

Topomatic.Controls.dll - возможность использования нативного диалогового окна программного комплекса Топоматик Робур (Класс MessageDlg). Данная библиотека не является обязательной, но она требуется для работоспособности кода примеров приведённых далее.

Topomatic.Crs.dll - базовые классы поперечных профилей

Topomatic.Dwg.dll - работа с чертежами

Topomatic.Plt.dll - шаблоны и теги

Добавление графических примитивов

Добавление графических примитивов в чертёж (Drawing) описано в главе руководства «Работа с примитивами чертежа». Доступ к чертежу (Drawing) осуществляется с помощью свойства Drawing унаследованного нашим классом от PrfField. В программном комплексе Топоматик Робур реализован механизм динамических чертежей. Данный механизм позволяет изменять положение примитивов в макете чертежа, не теряя при этом связи с исходной моделью. Например, мы можем переместить группу примитивов объединённых одним ключом в макете чертежа. При изменении состояния исходной модели смещение ключа останется. Например:

- Исходное состояние - После изменения положения группы примитивов объединённых общим ключом - После изменения модели Чтобы положение примитивов можно было изменять в пространстве макета, необходимо воспользоваться следующей конструкцией:

... var key = GenerateSimpleKey(this, station, "keyName"); // Генерируем динамический ключ для макета BeginMockup(key, position); // Добавление ключа в макет и привязка примитивов к динамическому ключу try { // В этом блоке нужно добавить примитивы в Drawing // Все эти примитивы будут привязаны к динамическому ключу drawing.ActiveSpace.AddText("foobar", position, 2.0, 1.0, 0, 0); } finally{ EndMockup(); } ...

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

Масштабирование координат

Чертежи генерируются с учётом масштабов установленных пользователем. Расположение примитивов чертежа должно соответствовать этим масштабам.

Продольный профиль

В классическом представлении, координатами продольного профиля являются высотная отметка (Y) и пикетажное положение (X). При моделировании трассы, пикетажное положение является значением производным от расстояния до начала трассы. Для преобразования координат продольного профиля следует пользоваться специальными методами:

ScaleElevation() - пересчёт высотной отметки в соответствии с вертикальным масштабом чертежа

ScaleStation() - пересчёт расстояния от начала пути в соответствии с горизонтальным масштабом чертежа

Поперечный профиль

Координатами поперечного профиля являются высотная отметка (Y) и величина смещения от оси базовой трассы (X). Для преобразования координат поперечного профиля следует пользоваться специальными методами:

ScaleElevation() - пересчёт высотной отметки в соответствии с вертикальным масштабом чертежа

ScaleOffset() - пересчёт горизонтального смещения в соответствии с горизонтальным масштабом чертежа

Пользовательский тег динамического чертежа продольного профиля

В этом примере мы создадим тег для чертежа продольного профиля и зарегистрируем его. Тег будет отрисовывать отметки точек экстремума чёрного и красного профилей.

Создадим класс тега. Так как тег предназначен для продольного профиля, то наследовать наш класс мы будем от PrfField. Добавим нашему классу свойство Precision, которое будет определять количество знаков после запятой у отрисовываемых отметок. Свойство следует декорировать атрибутом DisplayNameAttribute или его наследником, для получения имени свойства из ресурсов нашей сборки. В нашем случае напишем свой класс наследник DisplayNameAttribute для удобства отладки. В конструкторе класса проинициализируем свойство нужным значением.

Алгоритм отрисовки графических и текстовых примитивов описывается в методе OnDrawField(), который необходимо перекрыть. Вернём словарь данных нашего тега через свойство DataManager. С его помощью мы получим указатель на активный продольный профиль (Transition) по ключевому слову «ActiveTransition». Для нашей задачи потребуются линии черного и красного профилей, которые мы получим через свойства EgProfile и RedProfile соответственно у активного профиля. Подробнее о классе Transition можно узнать в разделе руководства «Редактирование плана и профиля» в подразделе Редактирование профиля.

Отметку и расстояние узла профиля можно получить с помощью свойств Elevation и Station соответственно.

В процессе вычислений, нам необходимо проверять, попадает ли текущий узел профиля в границы чертежа, чтобы тег не производил отрисовку примитивов за его вне границ и не тратил время на избыточные вычисления. Чтобы понять, находимся ли мы в границах чертежа, следует воспользоваться методом StationInLimits() статического класса AlignmentValueConverter. В качестве аргументов метод принимает проверяемое расстояние от начала пути, а так же расстояния начала и конца границы чертежа. Значение расстояний начала и конца границ чертежа можно получить с помощью свойств StartSta и EndSta унаследованных нашим классом от PrfField.

Добавьте в программу новый класс со следующим содержанием:

... class TutorialProfileField : PrfField { private int _precision; /// <summary> /// Количество знаков после запятой /// </summary> [SRDisplayName("sTutorialFieldPrecisionProperty")] public int Precision { get => _precision; set => _precision = value > 0 ? value : 0; } public TutorialProfileField() : base() { Precision = 2; } protected override void OnDrawField() { base.OnDrawField(); // Получаем активную ось профиля и её линии земли и проектного профиля var transition = this.DataManager["ActiveTransition"] as Transition; if (transition == null) return; var drawing = Drawing; var egProfile = transition.EgProfile; var redProfile = transition.RedProfile; // Рассчитываем и отрисовываем минимальные и максимальные отметки линий земли и проектного профиля if (egProfile.Count > 0) { var minEgElev = double.MaxValue; var minEgElevSta = 0.0; var maxEgElev = double.MinValue; var maxEgElevSta = 0.0; for (var i = 0; i < egProfile.Count; i++) { var node = egProfile[i]; // Проверяем, попадает ли узел в текущий разрыв if (!AlignmentValueConverter.StationInLimits(node.Station, StartSta, EndSta, true, true)) continue; if (node.Elevation < minEgElev) { minEgElev = node.Elevation; minEgElevSta = node.Station; } if (node.Elevation > maxEgElev) { maxEgElev = node.Elevation; maxEgElevSta = node.Station; } } var minEgEnt = DrawElevation(drawing, minEgElev, minEgElevSta, CadColor.Green); if (ValueConverter.CompValues(minEgElev, maxEgElev) != 0) { var maxEgEnt = DrawElevation(drawing, maxEgElev, maxEgElevSta, CadColor.Green); minEgEnt.Content = $"E(L): {minEgEnt.Content}"; maxEgEnt.Content = $"E(H): {maxEgEnt.Content}"; } } if (redProfile.Count > 0) { var minRedElev = double.MaxValue; var minRedElevSta = 0.0; var maxRedElev = double.MinValue; var maxRedElevSta = 0.0; foreach (var node in redProfile) { if (node.Elevation < minRedElev) { minRedElev = node.Elevation; minRedElevSta = node.Station; } if (node.Elevation > maxRedElev) { maxRedElev = node.Elevation; maxRedElevSta = node.Station; } } var minRedEnt = DrawElevation(drawing, minRedElev, minRedElevSta, CadColor.Red); if (ValueConverter.CompValues(minRedElev, maxRedElev) != 0) { var maxRedEnt = DrawElevation(drawing, maxRedElev, maxRedElevSta, CadColor.Red); minRedEnt.Content = $"R(L): {minRedEnt.Content}"; maxRedEnt.Content = $"R(H): {maxRedEnt.Content}"; } } } /// <summary> /// Отрисовка текстового примитива /// </summary> /// <param name="value"></param> /// <param name="station"></param> /// <param name="color"></param> /// <returns></returns> private DwgText DrawElevation(Drawing drawing, double value, double station, CadColor color) { var textStyle = drawing.ActiveStyle; // Рассчитываем положение примитива с учётом масштаба макета var scaleStation = ScaleStation(station); var position = new Vector3D(scaleStation, 0.0, 0.0); // Генерируем динамический ключ для макета и создаём примитив var key = GenerateSimpleKey(this, scaleStation, "elevation"); BeginMockup(key, position.Pos); try { var ent = drawing.ActiveSpace.AddText(ValueConverter.FloatToStr(value, Precision), position, textStyle.Height, textStyle.Ratio, Math.PI * 0.5, textStyle.Oblique); ent.Color = color; ent.Justify = this.DefaultTextJustify; return ent; } finally{ EndMockup(); } } } ...

Теперь необходимо создать провайдер, главной функцией которого будет регистрация нашего тега. Провайдер должен наследоваться от класса TemplateFieldProvider и перекрывать метод Provide(). Провайдер регистрирует тег с помощью метода RegisterFieldAlias() обработчика шаблонов (TemplateProcessor). Этот метод вернёт дескриптор зарегистрированного тега, в который следует добавить свойства, которые в последствии будут отображаться на панели свойств во время редактирования шаблона чертежа.

Добавьте в программу новый класс со следующим содержанием:

... class TutorialProfileFieldProvider : TemplateFieldProvider { public override void Provide(TemplateProcessor templateProcessor) { // Регистрация тега продольного профиля var profileFieldDescriptor = templateProcessor.RegisterFieldAlias( "TutorialProfileField_MaxMinElevation", TypeExplorer.GetSerializableString(typeof(TutorialProfileField)), Resources.sTutorialProfileField); profileFieldDescriptor.Add("Precision"); } } ...

Пользовательский тег динамического чертежа поперечного профиля профиля

В этом примере мы создадим тег для чертежа поперечного профиля и зарегистрируем его. Конструктивно, создание тега поперечного профиля и тега продольного профиля во многом идентично. Аналогично примеру для продольного профиля, тег будет отрисовывать отметки точек экстремума контура сечения земли и контура проектной конструкции.

Создадим класс тега. Так как тег предназначен для поперечного профиля, то наследовать наш класс мы будем от CrsField. Добавим нашему классу свойство Precision, которое будет определять количество знаков после запятой у отрисовываемых отметок. Свойство следует декорировать атрибутом DisplayNameAttribute или его наследником, для получения имени свойства из ресурсов нашей сборки. В нашем случае напишем свой класс наследник DisplayNameAttribute для удобства отладки. В конструкторе класса проинициализируем свойство нужным значением.

Алгоритм отрисовки графических и текстовых примитивов описывается в методе OnDrawField(), который необходимо перекрыть. Вернём словарь данных нашего тега через свойство DataManager. С его помощью мы получим указатель на базовый подобъект (Alignment) по ключевому слову «Alignment». Для нашей задачи потребуются контур сечения земли и контур проектной конструкции, которые мы получим из контекста конструирования поперечного профиля (CrsDesignContext). Подробнее о классе CrsDesignContext и контурах поперечного профиля можно узнать в разделе руководства «Выбор объектов на поперечном профиле» в подразделе Контекст конструирования поперечного профиля.

Индекс поперечного профиля получается с помощью свойства SectionIndex унаследованного от CrsField. Получим контекст конструирования поперечного профиля по индексу поперечного профиля с помощью свойства Item[Int32] класса Corridor (подробнее о получении Corridor можно узнать из раздела руководства «Выбор модели подобъекта и преобразование координат»).

Добавьте в программу новый класс со следующим содержанием:

... class TutorialCrossectionField : CrsField { private int _precision; /// <summary> /// Количество знаков после запятой /// </summary> [SRDisplayName("sTutorialFieldPrecisionProperty")] public int Precision { get => _precision; set => _precision = value > 0 ? value : 0; } public TutorialCrossectionField() : base() { Precision = 2; } protected override void OnDrawField() { base.OnDrawField(); // Получаем активную ось профиля и её линии земли и конструкции проектного поперечника var alignment = DataManager["Alignment"] as Alignment; if (alignment == null) return; var drawing = Drawing; var corridor = alignment.Corridor; var sectionId = this.SectionIndex; var context = corridor[sectionId]; var egContour = context.GetEgContour(); var redContour = context.GetRedLineContour(); // Рассчитываем и отрисовываем минимальные и максимальные отметки линий земли и конструкции проектного поперечника if (egContour.Count > 0) { var minEgElev = double.MaxValue; var minEgElevOffset = 0.0; var maxEgElev = double.MinValue; var maxEgElevOffset = 0.0; for (var i = 0; i < egContour.Count; i++) { var node = egContour[i]; if (node.Y < minEgElev) { minEgElev = node.Y; minEgElevOffset = node.X; } if (node.Y > maxEgElev) { maxEgElev = node.Y; maxEgElevOffset = node.X; } } var minEgEnt = DrawElevation(drawing, minEgElev, minEgElevOffset, CadColor.Green); if (ValueConverter.CompValues(minEgElev, maxEgElev) != 0) { var maxEgEnt = DrawElevation(drawing, maxEgElev, maxEgElevOffset, CadColor.Green); minEgEnt.Content = $"E(L): {minEgEnt.Content}"; maxEgEnt.Content = $"E(H): {maxEgEnt.Content}"; } } if (redContour.Count > 0) { var minRedElev = double.MaxValue; var minRedElevOffset = 0.0; var maxRedElev = double.MinValue; var maxRedElevOffset = 0.0; for (var i = 0; i < redContour.Count; i++) { var node = redContour[i]; if (node.Y < minRedElev) { minRedElev = node.Y; minRedElevOffset = node.X; } if (node.Y > maxRedElev) { maxRedElev = node.Y; maxRedElevOffset = node.X; } } var minRedEnt = DrawElevation(drawing, minRedElev, minRedElevOffset, CadColor.Red); if (ValueConverter.CompValues(minRedElev, maxRedElev) != 0) { var maxRedEnt = DrawElevation(drawing, maxRedElev, maxRedElevOffset, CadColor.Red); minRedEnt.Content = $"R(L): {minRedEnt.Content}"; maxRedEnt.Content = $"R(H): {maxRedEnt.Content}"; } } } /// <summary> /// Отрисовка текстового примитива /// </summary> /// <param name="value"></param> /// <param name="offset"></param> /// <param name="color"></param> /// <returns></returns> private DwgText DrawElevation(Drawing drawing, double value, double offset, CadColor color) { var textStyle = drawing.ActiveStyle; // Рассчитываем положение примитива с учётом масштаба макета var scaleOffset = ScaleOffset(offset); var position = new Vector3D(scaleOffset, 0.0, 0.0); // Генерируем динамический ключ для макета и создаём примитив var key = GenerateSimpleKey(this, scaleOffset, 0); BeginMockup(key, position.Pos); try { var ent = drawing.ActiveSpace.AddText(ValueConverter.FloatToStr(value, Precision), position, textStyle.Height, textStyle.Ratio, Math.PI * 0.5, textStyle.Oblique); ent.Color = color; ent.Justify = this.DefaultTextJustify; return ent; } finally { EndMockup(); } } } ...

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

... internal class TutorialCrossectionFieldProvider : TemplateFieldProvider { public override void Provide(TemplateProcessor templateProcessor) { // Регистрация тега поперечного профиля var crossectionFieldDescriptor = templateProcessor.RegisterFieldAlias( "TutorialCrossectionField_MaxMinElevation", TypeExplorer.GetSerializableString(typeof(TutorialCrossectionField)), Resources.sTutorialCrossectionField); crossectionFieldDescriptor.Add("Precision"); } } ...

Регистрация тегов

Теги следует регистрировать в момент инициализации пользовательского модуля. Для этого в теле программного модуля объявите команды, и декорируйте её атрибутом «cmd». Команды «provide_tutorial_profile_fields» и «provide_tutorial_crossection_fields» будут создавать провайдеры соответствующих типов и выполнять регистрацию тегов.

... // Команда вызова процесса регистрации тега продольного профиля [cmd("provide_tutorial_profile_fields")] private void ProvideProfileFields(TemplateProcessor templateProcessor) { var provider = new TutorialProfileFieldProvider(); provider.Provide(templateProcessor); } // Команда вызова процесса регистрации тега поперечного профиля [cmd("provide_tutorial_crossection_fields")] private void ProvideCrossectionFields(TemplateProcessor templateProcessor) { var provider = new TutorialCrossectionFieldProvider(); provider.Provide(templateProcessor); } ...

Остаётся зарегистрировать соответствующие таски. Для этого в классе наследнике от PluginHostInitializator нашего модуля следует перекрыть метод Initialize() и добавить в него соответствующие инструкции. Пример такого класса будет выглядеть так:

... public class ModulePluginHost : PluginHostInitializator { protected override Type[] GetTypes() { return new Type[] { typeof(Module) }; } public override void Initialize(PluginFactory factory) { base.Initialize(factory); factory.RegisterTask(AlgCoreTools.TASK_PLT_FIELDS, $"{TemplateDwgGenerator.ID_PRF_RAIL}:provide_tutorial_profile_fields"); factory.RegisterTask(AlgCoreTools.TASK_PLT_FIELDS, $"{TemplateDwgGenerator.ID_CRS_RAIL}:provide_tutorial_crossection_fields"); } } ...

Action добавления тега в шаблон чертежа

Редактировать шаблон чертежа можно изменяя содержимое текстовых примитивов посредством DXF-редактора, но более удобно воспользоваться редактором шаблонов программного комплекса Топоматик Робур.

Для добавление тега в редакторе шаблонов нужно создать новый action в plugin файле. Action должен вызывать команду в формате

"insert_template_field \"<Выравнивание> <Индекс_подобъекта> <Идентификатор_тега> <Атрибуты_через_пробел>\""

В шаблоне чертежа тег является текстовым примитивом, содержание которого представлено в формате «$<Имя_тега>». Так же после имени тега, в тексте могут содержаться атрибуты разделённые пробелами. Атрибуты определяют значения свойств тега по порядку. Порядок свойств тега соответствует порядку свойств добавленных при регистрации тега.

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

Теперь необходимо сформировать наш файл .plugin. Заполните его следующим образом.

{ "assemblies": { "TutorialFields": { "assembly": "TutorialFields.dll, TutorialFields.ModulePluginHost" } }, "actions": { "id_insert_tutorial_profile_tag": { "cmd": "insert_template_field \"MiddleCenter 0 TutorialProfileField_MaxMinElevation 0;1\"", "title": "Мой тег продольного профиля", "description": "Мой тег продольного профиля", "flags": "$(prf_rail_field)" }, "id_insert_tutorial_crossection_tag": { "cmd": "insert_template_field \"MiddleCenter 0 TutorialCrossectionField_MaxMinElevation\"", "title": "Мой тег поперечного профиля", "description": "Мой тег поперечного профиля", "flags": "$(crs_rail_field)" } }, "menubars": { "rbproj": { "items": [ { "id": "tutorial_menu", "title": "Tutorial", "items": [ "id_insert_tutorial_profile_tag", "id_insert_tutorial_crossection_tag" ] } ] } } }

Результатом запуска проекта будет появление в главном меню пункта «Tutorial», с подпунктами, которые будут работать в соответствии с описанными выше алгоритмами.

Исходный код примера расположен в проекте «TutorialFields».