Сфера проектирования вышла на новый уровень с появлением BIM-технологий. Помимо стандартных «коробочных»‎ решений, во многих САПР реализована возможность использования API для расширения базового функционала инструментов.

Эта возможность изменила даже состав специальностей, участвующих в проектировании. В крупных компаниях существуют целые команды разработчиков, задача которых — создание плагинов. В небольших компаниях, благодаря доступности и развитому комьюнити, также любой из продвинутых специалистов (проектировщиков, BIM-координаторов) может написать свой собственный плагин для решения рутинных задач.

Основные цели автоматизации проектирования — это сокращение трудозатрат проектировщиков и улучшение качества финального продукта.

Внедрение автоматизации стало доступным, но как ее внедрять, чтобы она была поддерживаемой и расширяемой? Что делать, когда плагинов становится слишком много? А если проблемы качества кода и его структуры становятся актуальными? Если над разработкой таких плагинов теперь работает не один человек, а целая команда? И кто такой Октавиан?

Одна из причин этих сложностей ясна: при внедрении BIM нет общего видения, как должна выглядеть система целиком. Многие разработчики плагинов не обладают опытом разработки до того, как начинают их писать. Часто отсутствуют технически подкованные коллеги, которые смогут научить и спроектировать структуру «на берегу»‎. 

С такими проблемами мы и столкнулись в нашей команде проектной автоматизации ПИК — и пришли к созданию своего фреймворка ReactiveBIM, который уже несколько лет показывает себя с хорошей стороны. 

Чтобы не вводить в заблуждение: слово «Reactive»‎ не имеет отношения к реактивному программированию, а подчеркивает ускоренное погружение новых специалистов в разработку плагинов для автоматизации BIM-моделирования.

ReactiveBIM — это платформа с открытым кодом для разработки плагинов для CAD/BIM программного обеспечения. Эта платформа предлагает разработчикам надежную структуру проекта, внедрение зависимостей, легкую настройку, удобное ведение журналов, автоматизацию сборки пакетов и многое другое.

Хотим рассказать про наш опыт и про ReactiveBIM подробнее, а также спросить вас, читателей, мнение о нашей работе. Возможно, наш проект заинтересует вас, и вы захотите принять участие в развитии opensource продукта.

В статье будет рассматриваться только часть фреймворка для Revit, но функционал включает в себя также поддержку Autocad.

Приятного чтения!

Не это ли настоящее чудо?

Представьте небольшую проектную организацию в городе N. В ней трудится команда замечательных специалистов. И вот пришло время внедрения BIM.

В этой компании среди работников яркой звездочкой выделяется наш герой по имени Октавиан, одним из первых принявший вызов по изучению и использованию программы Autodesk Revit на текущих проектах.

Давайте немного понаблюдаем за Октавианом и его историей внедрения BIM и автоматизации в его естественной среде обитания.

«Стена понимает, что она стена! — воскликнул Октавиан, знакомясь с Revit. — Какие же прекрасные возможности он открывает для проектирования!»‎. 

Вдохновение, присущее пылкому, открытому ко всему новому уму, охватило все сознание нашего героя. Наступил новый этап работы с глубоким погружением в мир BIM и знакомством со всеми возможностями инструмента.

И вот несколько месяцев спустя стали появляться вопросы, на которые у самого инструмента «из коробки»‎ нет красивых ответов:

  • Как бы побыстрее считать отделку, не моделируя ее?

  • Тратится много времени на оформление чертежей, можно ли исключить рутинные операции?

  • Как выполнить гидравлический расчет, не используя кучу эксель-табличек и справочник проектировщика?

Октавиан после некоторых изысканий смог найти ответ на эти и подобные вопросы. Ответ звучит так: автоматизация. Revit API позволяет — и этим стоит пользоваться.

С этого момента для героя открылся прекрасный мир программирования.

Первые идеи автоматизации, первые потраченные нервы с восклицаниями «почему не работает-то!»‎, радость от получения работающего скрипта/плагина. Ощущение, что нет ничего невозможного, есть запрос — напишем что-то, что поможет.

Успех налицо — множество рутинных задач превращались в нажатие одной кнопки, трудозатраты проектировщиков уменьшались на глазах.

Не это ли настоящее чудо?

ПИК начинал с похожего подхода, но, возможно, с большим размахом — в штате были BIM-координаторы и несколько программистов, которые по заданию писали плагины. Каждый плагин — произведение искусства определенного разработчика с гордо всплывающим на кнопках ToolTip с именами ответственных за плагин сотрудников.

В бочке меда ложка дегтя? Или все-таки ведро?

Мало-помалу Revit обрастал небольшими плагинами, и все вроде бы шло отлично. Октавиан стал главным парнем на деревне — знал, где и что лежит, как это работает, а как работать не будет. К нему начали обращаться разные отделы. Спрос в какой-то момент стал превышать предложение, и было принято решение расширить команду разработчиков.

Новые лица, свежие силы, новые стили написания кода, новое видение структуры проекта — все это неизбежно подсветит недостатки текущих наработок.

Вот пример одной из проблем, с которой столкнулся Октавиан. Пришла новая задача на доработку существующего плагина, а самому дописывать код не получится — не хватает на все рук. Так это хорошая возможность влиться в работу новенькому сотруднику! Задание отдано, ожидается результат. И этот результат приходит, но не вовремя и не тот. 

Дело в том, что у другого исполнителя есть свое понимание, как код должен выглядеть. И сложности с разбором уже реализованной логики.

Так что в результате был частично сломан существующий функционал, и на код ревью у Октавиана было много вопросов общего характера: «зачем тут так?»‎.

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

Когда все изначально строится несколько «кустарно»‎, со временем становится сложнее поддерживать технологию и расширять ее, не ломая предыдущее и не выбиваясь из сроков выполнения задач.

«Что-то не так!‎ — воскликнул Октавиан, — Надо всё исправлять…»‎

Но как это сделать? Вопрос оставался открытым. Октавиан был опечален.

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

А есть ли решение?

Наш герой и его команда не хотели существовать в такой степени неопределенности и стали думать о новом подходе, который помог бы навести порядок в их работе. На просторах интернета Октавиан наткнулся на библиотеку ReactiveBim и решил выяснить, что это за зверь такой.

Программистами ПИК из дирекции автоматизации проектного блока (ДАПБ) был заложен фундамент для разработки фреймворка под Autodesk Revit и Autocad (Civil).

В этой части статьи хотим рассказать об основных функциях фреймворка для Revit.

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

С чего начать?

ReactiveBIM предоставляет примеры команд. В них реализованы проекты с настроенным контейнером для внедрения зависимостей, файлом с командой и json файлом для хранения параметров и настроек плагина. Демонстрирует MVVM подход к архитектуре проекта на основе MVVMLight.

Пример структуры проекта
Пример структуры проекта

Метод ExecuteCommand является точкой входа в выполнение кода плагина. Пример того, как может выглядеть код файла Cmd.cs.

    [Regeneration(RegenerationOption.Manual)]
    [Transaction(TransactionMode.Manual)]
    public class Cmd : RxBimCommand
    {
        private static Window _win;
        public PluginResult ExecuteCommand(MainWindow mainWindow)
        {
            if (_win == null)
            {
                _win = mainWindow;
                _win.Closing += (_, _) => _win = null;
                mainWindow.Show();
            }
            else
            {
                _win.Activate();
            }

            return PluginResult.Succeeded;
        }
    }

Для реализации нашего подхода к написанию плагина предлагается наследоваться от RxBimCommand, который уже реализует IExternalCommand и  IExternalCommandAvailability.

Внедрение зависимостей

Многие наверняка знают и пользуются внедрением зависимостей (Dependency Injection) в своих проектах. Если в двух словах, то это:

Способ сделать так, чтобы объекты класса не создавали сами свои инструменты, и передать эту обязанность внешней системе. Например, если вы решили построить машину времени, то можно заказать из магазина уже готовые временной контур и плутониевый реактор, собранные кем-то еще, вместо того чтобы собирать их самостоятельно.

Использование ReactiveBIM упрощает настройку и последующую работу с внедрением зависимостей в проекте. Создание контейнера и регистрация происходит автоматически при запуске команды. 

Все, что нужно сделать, это прописать зависимости в специально для этого созданном шаблоном файле Config.cs. В своей работе RxBim.Di использует Microsoft.Extensions.DependencyInjection.

  public class Config : ICommandConfiguration
    {
        public void Configure(IContainer container)
        {
            container.AddRevitHelpers();

            container.AddSingleton<MainWindow>();
            container.AddSingleton<MainContext>();

            container.AddSingleton<ICalcWallsService, CalcWallsService>();
            container.AddSingleton<IUserInteractionService, UserInteractionService>();
            container.AddTransient<ISessionUserSettingsManager, SessionUserSettingsManager>();
        }
    }

*Пример регистрации сервисов в Config.cs.

Метод AddSingleton добавит в контейнер единую для всего приложения имплементацию данного интерфейса, в то время как AddTransient предоставит при каждом запросе новый экземпляр. Метод расширения AddRevitHelpers уже содержит регистрацию некоторых полезных сервисов из RxBim.Tools (например, сервис для удобного проведения транзакций ITransactionService). Для использования этих реализаций далее достаточно добавить их в конструктор класса, в котором они будут использоваться.

Пример использования DI в коде:

public class CalcWallsService : ICalcWallsService
{
    private readonly ITransactionService _transactionService;
    private readonly IUserInteractionService _userInteractionService;
    private readonly Document _document;

    public CalcWallsService(
        ITransactionService transactionService,
        IUserInteractionService userInteractionService,
        Document document)
    {
        _transactionService = transactionService;
        _userInteractionService = userInteractionService;
        _document = document;
    }

    public void CalcWallsLength()
    {
        _transactionService.RunInTransaction(
            () => SaveWallsProperties("Wall"),
            "Writing walls properties.");

        _userInteractionService.PromptUserToSelectDirectory("./new");
     }

В этом примере в конструкторе класса прописаны аргументами ITransactionService и IUserInteractionService. Система внедрения зависимостей создаст экземпляры согласно зарегистрированным типам и передаст их в конструктор.

RxBim.Tools

В процессе работы над плагинами были написаны методы, упрощающие разработку. Для Revit 2019 такие методы объединили в пакет RxBim.Tools.

Для регистрации вспомогательных сервисов в контейнер DI пропишите в Config.cs

container.AddRevitHelpers();

Transaction Service

Любое изменение в модели Revit проходит с помощью транзакции. Сделано это для того, чтобы при появлении ошибок во время работы состояние модели можно было бы откатить до начального состояния.

Работа с транзакциями реализована в transaction service в удобном для разработчика виде. Методы RunInTransaction(...) и RunInTransactionGroup(...) оборачивают переданную функцию в транзакцию (или группу транзакций) и фиксирует изменения после выполнения.

 _transactionService.RunInTransaction(
            () => UsfullFunc(),
            "Writing walls properties.");

Существует перегрузка с доступом к транзакции для возможности реализации дополнительной логики (например, отмены изменений).

_transactionService.RunInTransactionGroup(
            transaction =>
            {
                if (!SaveWallsProperties("Wall"))
                    transaction.RollBack();
            },
            "Writing walls properties.");

RevitTask

В Revit существует понятие контекста, которое подразумевает под собой абстракцию для взаимодействия с Revit-моделью. Во время работы приложения вне основного потока Revit, например, при отрисовке UI, немодального окна плагина или запуска задачи в отдельном потоке, теряется этот контекст.

Чтобы вы не мучились с этими вещами, мы используем логику взаимодействия Revit контекста и UI-потока, под названием RevitTask.

using RxBim.Tools.Revit.Extensions;

public class SampleTestService : ISampleTestService
{
    private readonly ITransactionService _transactionService;
    private readonly RevitTask _revitTask;
    
    public CalcWallsService(
        ITransactionService transactionService,
        RevitTask revitTask)
    {
        _transactionService = transactionService;
        _revitTask = revitTask;
    }

    public async void ExampleMethod()
    {
        await _revitTask.Run(app =>
        {
            _transactionService.RunInTransaction(
                transaction =>
                {
                    if (!SaveWallsProperties("Wall"))
                        transaction.RollBack();
                },
                "Writing walls properties.");
        });
    }
}

Пример сервиса, использующего RevitTask и DI. Метод Run принимает функцию, предоставляя ей доступ к контексту Revit.

Утилитарные методы

Кроме того, в RxBim.Tools присутствуют полезные методы расширения. Например:

  • группа методов для сравнения чисел с погрешностью — IsEqualTo(...), IsEqualOrLess(...), IsEqualOrGreater(...). По умолчанию погрешность 10e-6;

  • методы для перевода чисел из футов в мм и наоборот — MmToFt(), FtToMm() или из градусов в радианы — DegreeToRadian();

  • методы для упрощения работы с геометрией — GetVectorLength(...), GetSolid(...) (получение Solid передаваемого элемента), IsParallelTo(...) проверяет параллельность двух векторов;

  • SharedParameterService — Сервис для работы с общими параметрами. Позволяет значительно упростить и обезопасить программное добавление и обновление общих параметров проекта без потери данных.

и много другое.

Отображение плагина в Revit

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

Фреймворк ReactiveBim предоставляет для этого удобную абстракцию. Вы можете легко добавить свои собственные элементы управления, используя конфигурацию JSON или Fluent API.

Fluent API

Остановимся подробнее на варианте Fluent API. Вам нужно добавить конфигурацию своего меню в уже реализованный метод конфигурации контейнера. С помощью методов нашей абстракции вы сможете настроить всплывающие подсказки, описания, изображения, а также их расположение внутри Revit и т.д.

public class Config : IApplicationConfiguration
{
    public void Configure(IContainer container)
    {
        container.AddRevitMenu(ribbon => ribbon
            .EnableDisplayVersion()
            .SetVersionPrefix("Version: ")
            .Tab(
                title: "RxBim_Tab_FromAction", 
                tab => tab
                    .Panel(
                        title: "RxBim_Panel_1",
                        panel => panel
                            .PullDownButton(
                                "Pulldown1",
                                pulldown => pulldown
                                    .CommandButton(
                                        "Command1_Pulldown1",
                                        typeof(Cmd1),
                                        button => button
                                            .ToolTip("Tooltip: I'm run command #1. Push me!")
                                            .Text("Command\n#1")
                                            .Description("Description: This is command #1")
                                            .LargeImage(@"img\num1_32.png")
                                            .HelpUrl("https://github.com/ReactiveBIM/RxBim"))
									.Separator()
                                    .CommandButton(
                                        "Command2_Pulldown1",
                                        typeof(Cmd2),
                                        button => button
                                            .ToolTip("Tooltip: I'm run command #2. Push me!")
                                            .Text("Command\n#2")
                                            .Description("Description: This is command #2")
                                            .LargeImage(@"img\num2_32.bmp")
                                            .HelpUrl("https://github.com/ReactiveBIM/RxBim"))
                                    .LargeImage(@"img\command_32.ico")
                                    .Text("Pulldown\n#1")))),
            Assembly.GetExecutingAssembly());
    }
}

Также существуют способы конфигурации при помощи JSON и атрибутов для стартового класса команды.

Все варианты эквивалентны, вы можете использовать любой из них.

Тестирование плагинов

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

ReactiveBim предоставляет API для запуска интеграционных тестов Autodesk Revit, используя библиотеку RxBim.AcadTests.

Создание тестового проекта

Для начала создайте новый тестовый проект с NUnit:

dotnet new nunit -f <project version>.

Затем установите дополнительные пакеты:

  • RxBim.RevitTests.DI

  • FluentAssertions 

  • Moq

  • NUnit

Создайте тестовую модель Revit, на которой будет происходить тестирование функционала, например model.rvt, и поместите ее в тестовый проект. Настройте проект для копирования модели в выходную директорию:

<ItemGroup>
	<Content Include="model.rvt">
		<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
	</Content>
</ItemGroup>

Далее можно писать тесты, используя NUnit. Также вы можете настроить контейнер DI с фиктивной конфигурацией, используя TestDiConfigurator, и зарегистрировать все необходимые для тестирования сервисы:

[TestFixture]
public class Tests
{
    private IContainer _container;

    [SetUp]
    public void Setup()
    {
       var testingDiConfigurator = new TestingDiConfigurator();
       testingDiConfigurator.Configure(Assembly.GetExecutingAssembly());
       _container = testingDiConfigurator.Container;
       var uiApp = _container.GetService<UIApplication>();
       var modelPath = Path.Combine(Environment.CurrentDirectory, «model.rvt»‎);
       uiApp.OpenAndActivateDocument(modelPath);
       _document = _container.GetService<Document>();
    }

    [Test]
    public void CommentShouldBeSetAndNotThrowsException()
    {
         var testService = _container.GetService<ITestService>();
        var element = new FilteredElementCollector(_container.GetService<Document>())
            .WhereElementIsNotElementType()
            .OfClass(typeof(Wall))
            .FirstOrDefault();
        Assert.NotNull(element, "element != null");
        Assert.DoesNotThrow(() => testService.SetComment(element));

        var comment = element.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_COMMENTS)
            .AsValueString();
        Assert.NotNull(comment, "comment != null");
        Assert.IsNotEmpty(comment);
    }
}

Запуск тестов

Для запуска тестов реализован Nuke-таргет IntegrationTests. С помощью него можно запустить тестирование локально. Для этого в консоле пропишите:

nuke integrationtests --test-tool-name revit --OnlySelectedProjects

Nuke таргеты для публикации и сборки плагинов

Nuke используется для создания шагов по сборке и публикации приложений для CI/CD.

ReactiveBIM как раз использует Nuke.Build именно для этого.

Подготовка окружения

Для подготовки окружения требуется:

  1. Инициализировать решение с помощью Nuke.Build.

  2. Установить пакет RxBim.Nuke.Revit: dotnet add package RxBim.Nuke.Revit.

  3. Обновить Build класс — он должен наследовать RevitRxBimBuild.

Использования таргетов

RevitRxBimBuild предоставляет конвейер таргетов для сборки, подписи и публикации пакетов. Вы можете запустить их с помощью nuke <targetName> команды, например nuke BuildMsi .

С помощью таргетов для публикации ReactiveBIM вы можете быстро компилировать и собирать свой код в .exe или .msi файлы, не думая о том как это происходит под капотом.

В таргетах для публикации BuildMsi и BuildInnoExe есть механизм для подписи сборок и .exe файлов. Например, если того требует политика ИТ-безопасности вашей компании.


Также можно писать свои таргеты и имплементировать их под свои задачи.

Таргеты для публикации

  • CopyOutput — копирует бинарные файлы в папку плагинов программного обеспечения CAD/BIM. Полезно для быстрой отладки.

  • BuildMsi — собирает MSI-пакет с помощью WixToolset .

  • BuildInnoExe — создает EXE-пакет через InnoSetup .

Таргеты для сборки

  • Clean — очищает каталог решений, удаляя bin и obj папки.

  • Restore — восстанавливает все пакеты из Nuget.

  • Compile — компилирует решение.

  • GenerateProjectProps — генерирует свойства для сборки пакетов.

Таргеты для тестирования

  • Test — запускает все модульные тесты из решения.

  • IntegrationTests — запускает все интеграционные тесты из решения.

Кульминация

И как же мы видим жизнь Октавиана сейчас, дорогие читатели?

Если сможем прочитать его мысли, то узнаем, что стало намного лучше. Со временем он стал контрибьютором ReactiveBIM, принимает активное участие в его развитии. На работе он стал тем паровозиком сотрудником, который смог.

За него искренне радуешься!

Благодаря разработке и использованию общего фреймворка, в ПИКе получилось добиться таких результатов:

  • снижение порога вхождения новых специалистов (в компании практикуется не только внешний рекрутинг, но и внутреннее обучение BIM-специалистов для перехода в разработчики и даже подключение подрядных организаций);

  • облегчение понимания чужого кода благодаря четкой, единообразной структуре проектов;

  • уменьшение трудозатрат на разработку плагинов (возможность повторно использовать части кода, общая библиотека с полезными, постоянно используемыми методами, характерными для работы с Revit.);

  • стабильность работы плагинов (тестирование).

Развязка

Хочется поблагодарить тех, кто дошел до финальной части статьи. 

В ПИКе процесс активного внедрения BIM насчитывает 8-й год. На этом пути было набито немало шишек и проведено множество экспериментов, чтобы ответить на вопрос: «как лучше?»‎. Мы не можем сказать, что решили все-все проблемы, но точно утверждаем, что многое получилось реализовать.

На своем опыте можем подтвердить, что ReactiveBIM — рабочее, хорошо себя показавшее решение.

Он пока что далек от идеала, есть много моментов, которые хотелось бы отшлифовать, улучшить. 

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

Надеемся, что для кого-то из вас, читателей, ReactiveBIM окажется полезным, и вы попробуете применить его для своих задач.

Проект является opensource-продуктом не только по названию, но и по нашему подходу к его развитию. Ждем новых пулл реквестов! Будем рады обратной связи и расширению сообщества!

Комментарии (0)