====== Теги динамических чертежей профилей ====== В программном комплексе [[http://topomatic.ru/|Топоматик Робур]] чертежи профилей генерируются на основе шаблонов. Шаблон чертежа профиля представляет из себя файл в формате [[https://ru.wikipedia.org/wiki/DXF|DXF (Drawing eXchange Format)]]. При генерации чертежа используются специальные динамические объекты называемые [[road:commons_tasks:archive_sections:format_template_drawing:basic_codes_rail_subobject:start|тегами]]. Теги это элементы [[road:commons_tasks:archive_sections:format_template_drawing_cross_profile:start|шаблона чертежа]], которые представляют необходимую информацию о модели в требуемом графическом виде. ==== Создания пользовательского тега ==== Для создания пользовательского тега необходимы следующие действия: - Создание класса тега, содержащего алгоритм отрисовки графической информации на чертеже - Создание класса провайдера тега, отвечающего за регистрацию его в системе - Объявление команды регистрирующей тег и добавление таска для регистрации тега при инициализации пользовательского модуля При необходимости, в plugin файле можно объявить action добавления тега в шаблон чертежа Программно, тег является классом наследником от [[developers:references:topomatic.plt.templates.prf.prffield|PrfField]] для продольного профиля и [[developers:references:topomatic.plt.templates.crs.crsfield|CrsField]] для поперечного профиля. Оба эти класса, в свою очередь, являются наследниками класса [[developers:references:topomatic.plt.templates.common.templatefield|TemplateField]]. Класс тега содержит его свойства и описание алгоритмов построения графических примитивов. Провайдер тега это класс наследник от [[developers:references:topomatic.plt.templates.templatefieldprovider|TemplateFieldProvider]], с помощью которого мы будем регистрировать наш тег в системе. ==== Подготовка модуля ==== Создайте и настройте новый [[developers:tutorial:module|модуль]] для подключения к программному комплексу [[http://topomatic.ru/|Топоматик Робур]]. С помощью диалогового окна Менеджер ссылок добавьте ссылки на следующие библиотеки: * [[developers:references:topomatic.alg|Topomatic.Alg.dll]] - базовые классы подобъектов * [[developers:references:topomatic.alg.runtime|Topomatic.Alg.Runtime.dll]] - возможность использования статического класса AlgCoreTools, для доступа к необходимым константам * [[developers:references:topomatic.cad.foundation|Topomatic.Cad.Foundation.dll]] - базовые математические типы и операции * [[developers:references:topomatic.cad.view|Topomatic.Cad.View.dll]] - элемент управления для отображения слоёв моделей * [[developers:references:topomatic.componentmodel|Topomatic.ComponentModel.dll]] - возможность использования статического класса TypeExplorer * [[developers:references:topomatic.controls|Topomatic.Controls.dll]] - возможность использования нативного диалогового окна программного комплекса Топоматик Робур (Класс [[developers:references:topomatic.controls.dialogs.messagedlg|MessageDlg]]). Данная библиотека не является обязательной, но она требуется для работоспособности кода примеров приведённых далее. * [[developers:references:topomatic.crs|Topomatic.Crs.dll]] - базовые классы поперечных профилей * [[developers:references:topomatic.dwg|Topomatic.Dwg.dll]] - работа с чертежами * [[developers:references:topomatic.plt|Topomatic.Plt.dll]] - шаблоны и теги ==== Добавление графических примитивов ==== Добавление графических примитивов в чертёж ([[developers:references:topomatic.dwg.drawing|Drawing]]) описано в главе руководства "[[developers:tutorial:drawing|Работа с примитивами чертежа]]". Доступ к чертежу ([[developers:references:topomatic.dwg.drawing|Drawing]]) осуществляется с помощью свойства [[developers:references:topomatic.plt.templates.common.templatefield.drawing|Drawing]] унаследованного нашим классом от [[developers:references:topomatic.plt.templates.prf.prffield|PrfField]]. В программном комплексе [[http://topomatic.ru/|Топоматик Робур]] реализован механизм динамических чертежей. Данный механизм позволяет изменять положение примитивов в [[road:commons_tasks:general_information_layouts_drawings:start|макете чертежа]], не теряя при этом связи с исходной моделью. Например, мы можем переместить группу примитивов объединённых одним ключом в макете чертежа. При изменении состояния исходной модели смещение ключа останется. Например: - Исходное состояние {{ :developers:tutorial:alignmentedit:tutdyndrawing1.png?nolink&400 |}} - После изменения положения группы примитивов объединённых общим ключом {{ :developers:tutorial:alignmentedit:tutdyndrawing2.png?nolink&400 |}} - После изменения модели {{ :developers:tutorial:alignmentedit:tutdyndrawing3.png?nolink&400 |}} Чтобы положение примитивов можно было изменять в пространстве макета, необходимо воспользоваться следующей конструкцией: ... var key = GenerateSimpleKey(this, station, "keyName"); // Генерируем динамический ключ для макета BeginMockup(key, position); // Добавление ключа в макет и привязка примитивов к динамическому ключу try { // В этом блоке нужно добавить примитивы в Drawing // Все эти примитивы будут привязаны к динамическому ключу drawing.ActiveSpace.AddText("foobar", position, 2.0, 1.0, 0, 0); } finally{ EndMockup(); } ... Так же можно воспользоваться методом [[developers:references:topomatic.plt.templates.common.templatefield.drawmockuptext|DrawMockupText()]], если необходимо создать текстовый примитив с возможностью изменять его местоположение в макете. ==== Масштабирование координат ==== Чертежи генерируются с учётом масштабов установленных пользователем. Расположение примитивов чертежа должно соответствовать этим масштабам. === Продольный профиль === В классическом представлении, координатами продольного профиля являются высотная отметка (Y) и пикетажное положение (X). При моделировании трассы, пикетажное положение является значением производным от расстояния до начала трассы. Для преобразования координат продольного профиля следует пользоваться специальными методами: * [[developers:references:topomatic.plt.templates.prf.prffield.scaleelevation|ScaleElevation()]] - пересчёт высотной отметки в соответствии с вертикальным масштабом чертежа * [[developers:references:topomatic.plt.templates.prf.prffield.scalestation|ScaleStation()]] - пересчёт расстояния от начала пути в соответствии с горизонтальным масштабом чертежа === Поперечный профиль === Координатами поперечного профиля являются высотная отметка (Y) и величина смещения от оси базовой трассы (X). Для преобразования координат поперечного профиля следует пользоваться специальными методами: * [[developers:references:topomatic.plt.templates.crs.crsfield.scaleelevation|ScaleElevation()]] - пересчёт высотной отметки в соответствии с вертикальным масштабом чертежа * [[developers:references:topomatic.plt.templates.crs.crsfield.scaleoffset|ScaleOffset()]] - пересчёт горизонтального смещения в соответствии с горизонтальным масштабом чертежа ==== Пользовательский тег динамического чертежа продольного профиля ==== В этом примере мы создадим тег для чертежа продольного профиля и зарегистрируем его. Тег будет отрисовывать отметки точек экстремума чёрного и красного профилей. Создадим класс тега. Так как тег предназначен для продольного профиля, то наследовать наш класс мы будем от [[developers:references:topomatic.plt.templates.prf.prffield|PrfField]]. Добавим нашему классу свойство **Precision**, которое будет определять количество знаков после запятой у отрисовываемых отметок. Свойство следует декорировать атрибутом [[https://docs.microsoft.com/ru-ru/dotnet/api/system.componentmodel.displaynameattribute?view=net-6.0|DisplayNameAttribute]] или его наследником, для получения имени свойства из ресурсов нашей сборки. В нашем случае напишем свой класс наследник [[https://docs.microsoft.com/ru-ru/dotnet/api/system.componentmodel.displaynameattribute?view=net-6.0|DisplayNameAttribute]] для удобства отладки. В конструкторе класса проинициализируем свойство нужным значением. Алгоритм отрисовки графических и текстовых примитивов описывается в методе [[developers:references:topomatic.plt.templates.prf.prffield.ondrawfield|OnDrawField()]], который необходимо перекрыть. Вернём словарь данных нашего тега через свойство [[developers:references:topomatic.plt.templates.common.templatefield.datamanager|DataManager]]. С его помощью мы получим указатель на активный продольный профиль ([[developers:references:topomatic.alg.prf.transition|Transition]]) по ключевому слову **"ActiveTransition"**. Для нашей задачи потребуются линии черного и красного профилей, которые мы получим через свойства [[developers:references:topomatic.alg.prf.transition.egprofile|EgProfile]] и [[developers:references:topomatic.alg.prf.transition.redprofile|RedProfile]] соответственно у активного профиля. Подробнее о классе [[developers:references:topomatic.alg.prf.transition|Transition]] можно узнать в разделе руководства "[[developers:tutorial:algedit|Редактирование плана и профиля]]" в подразделе **Редактирование профиля**. Отметку и расстояние узла профиля можно получить с помощью свойств [[developers:references:topomatic.alg.prf.profilenode.elevation|Elevation]] и [[developers:references:topomatic.alg.prf.profilenode.station|Station]] соответственно. В процессе вычислений, нам необходимо проверять, попадает ли текущий узел профиля в границы чертежа, чтобы тег не производил отрисовку примитивов за его вне границ и не тратил время на избыточные вычисления. Чтобы понять, находимся ли мы в границах чертежа, следует воспользоваться методом [[developers:references:topomatic.alg.alignmentvalueconverter.stationinlimits_system.double_system.double_system.double_system.boolean_system.boolean|StationInLimits()]] статического класса [[developers:references:topomatic.alg.alignmentvalueconverter|AlignmentValueConverter]]. В качестве аргументов метод принимает проверяемое расстояние от начала пути, а так же расстояния начала и конца границы чертежа. Значение расстояний начала и конца границ чертежа можно получить с помощью свойств [[developers:references:topomatic.plt.templates.prf.prffield.startsta|StartSta]] и [[developers:references:topomatic.plt.templates.prf.prffield.endsta|EndSta]] унаследованных нашим классом от [[developers:references:topomatic.plt.templates.prf.prffield|PrfField]]. Добавьте в программу новый класс со следующим содержанием: ... class TutorialProfileField : PrfField { private int _precision; /// /// Количество знаков после запятой /// [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}"; } } } /// /// Отрисовка текстового примитива /// /// /// /// /// 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(); } } } ... Теперь необходимо создать провайдер, главной функцией которого будет регистрация нашего тега. Провайдер должен наследоваться от класса [[developers:references:topomatic.plt.templates.templatefieldprovider|TemplateFieldProvider]] и перекрывать метод [[developers:references:topomatic.plt.templates.templatefieldprovider.provide_topomatic.plt.templates.common.templateprocessor|Provide()]]. Провайдер регистрирует тег с помощью метода [[developers:references:topomatic.plt.templates.common.templateprocessor.registerfieldalias_system.string_system.string|RegisterFieldAlias()]] обработчика шаблонов ([[developers:references:topomatic.plt.templates.common.templateprocessor|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"); } } ... {{ :developers:tutorial:alignmentedit:tutcustomprffield.png?nolink |}} ==== Пользовательский тег динамического чертежа поперечного профиля профиля ==== В этом примере мы создадим тег для чертежа поперечного профиля и зарегистрируем его. Конструктивно, создание тега поперечного профиля и тега продольного профиля во многом идентично. Аналогично примеру для продольного профиля, тег будет отрисовывать отметки точек экстремума контура сечения земли и контура проектной конструкции. Создадим класс тега. Так как тег предназначен для поперечного профиля, то наследовать наш класс мы будем от [[developers:references:topomatic.plt.templates.crs.crsfield|CrsField]]. Добавим нашему классу свойство **Precision**, которое будет определять количество знаков после запятой у отрисовываемых отметок. Свойство следует декорировать атрибутом [[https://docs.microsoft.com/ru-ru/dotnet/api/system.componentmodel.displaynameattribute?view=net-6.0|DisplayNameAttribute]] или его наследником, для получения имени свойства из ресурсов нашей сборки. В нашем случае напишем свой класс наследник [[https://docs.microsoft.com/ru-ru/dotnet/api/system.componentmodel.displaynameattribute?view=net-6.0|DisplayNameAttribute]] для удобства отладки. В конструкторе класса проинициализируем свойство нужным значением. Алгоритм отрисовки графических и текстовых примитивов описывается в методе [[developers:references:topomatic.plt.templates.prf.crsfield.ondrawfield|OnDrawField()]], который необходимо перекрыть. Вернём словарь данных нашего тега через свойство [[developers:references:topomatic.plt.templates.common.templatefield.datamanager|DataManager]]. С его помощью мы получим указатель на базовый подобъект ([[developers:references:topomatic.alg.alignment|Alignment]]) по ключевому слову **"Alignment"**. Для нашей задачи потребуются контур сечения земли и контур проектной конструкции, которые мы получим из контекста конструирования поперечного профиля ([[developers:references:topomatic.crs.templates.crsdesigncontext|CrsDesignContext]]). Подробнее о классе [[developers:references:topomatic.crs.templates.crsdesigncontext|CrsDesignContext]] и контурах поперечного профиля можно узнать в разделе руководства "[[developers:tutorial:crsselection|Выбор объектов на поперечном профиле]]" в подразделе **Контекст конструирования поперечного профиля**. Индекс поперечного профиля получается с помощью свойства [[developers:references:topomatic.plt.templates.crs.crsfield.sectionindex|SectionIndex]] унаследованного от [[developers:references:topomatic.plt.templates.crs.crsfield|CrsField]]. Получим контекст конструирования поперечного профиля по индексу поперечного профиля с помощью свойства [[developers:references:topomatic.alg.crs.corridor.item_system.int32|Item[Int32]]] класса [[developers:references:topomatic.alg.crs.corridor|Corridor]] (подробнее о получении [[developers:references:topomatic.alg.crs.corridor|Corridor]] можно узнать из раздела руководства "[[developers:tutorial:algstationing|Выбор модели подобъекта и преобразование координат]]"). Добавьте в программу новый класс со следующим содержанием: ... class TutorialCrossectionField : CrsField { private int _precision; /// /// Количество знаков после запятой /// [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}"; } } } /// /// Отрисовка текстового примитива /// /// /// /// /// 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"); } } ... {{ :developers:tutorial:alignmentedit:tutcustomcrsfield.png?nolink |}} ==== Регистрация тегов ==== Теги следует регистрировать в момент инициализации пользовательского модуля. Для этого в теле программного модуля объявите команды, и декорируйте её атрибутом «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); } ... Остаётся зарегистрировать соответствующие таски. Для этого в классе наследнике от [[developers:references:topomatic.applicationplatform.plugins.pluginhostinitializator|PluginHostInitializator]] нашего модуля следует перекрыть метод [[developers:references:topomatic.applicationplatform.plugins.pluginhostinitializator.initialize|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», с подпунктами, которые будут работать в соответствии с описанными выше алгоритмами. [[developers:tutorial:tutorialcode|Исходный код]] примера расположен в проекте **"TutorialFields"**.