Часть 1 здесь. Весь код — на GitHub.
Итак, продолжаем работу над нашим плагином. Задача оказалась довольно сложной, поэтому с выходом части 2 я чуть затянул. Материала будет много, располагайтесь поудобнее и включайте свои мозги — поехали.
Алгоритм работы
Написав плагин, я понял, что рассказать о нём в формате "пишем то, пишем это" — не выйдет. Опишу пошаговый алгоритм, а затем подробно распишу, что происходит.
- Просим пользователя указать точки разреза.
- Получаем точки, в которых мы разрежем существующую трубу.
- Разрезаем трубу.
- Находим получившийся центральный сегмент.
- Смещаем его в нужную сторону.
- Вычисляем, насколько его нужно подкоротить, если угол менее 90 градусов, и укорачиваем.
- Строим кусочки трубы от краёв центрального отсека до краёв старой трубы.
- На построенных кусочков находим коннекторы, которые совпадают по координате с коннекторами краёв старой трубы.
- Строим отводы на совпадающих коннекторах.
Реализация
Запрос точек
Точки мы получаем 2 способами: с привязкой и без, в зависимости от того, что указал пользователь в чекбоксе "работать с привязкой". Соответственно, 2 метода:
Нюансы:
- Переменные, в которые записаны значения — это приватные поля моего класса, чтобы я мог их использовать в других методах.
- На строке 113 я передаю в фильтр первый выбранный элемент, чтобы пользователь не мог выбрать другую трубу.
- В первом случае пользователь делает два клика, а во втором — три.
Получение точек
Это не одно и тоже с запросом точек, так как нам надо спроецировать полученные точки на ось трубы, и только тогда мы сможем её разрезать в этих точках. Смотрим код:
Нюансы:
- Переменная HasSnap как раз показывает, выбираем мы с привязкой или без.
- Возвращаем массив, а не список для экономии памяти.
- Далее просто проецируем на линию либо точку, либо GlobalPoint от Reference.
Разделение кода на короткие методы заметно упрощает его чтение, не забывайте делать так же.
Разрез трубы и поиск центрального сегмента
Один из самых сложных моментов. Я создал в своём классе 4 поля для коннекторов по такой логике:
То есть мы будем строить трубу от connector11 до 21, а затем от 22 до 32. Цифры означают: Первая труба, первый коннектор. Соответственно, нам надо порезать трубы, определить, какая является первой, второй, третьей, и записать их коннекторы в правильном порядке в нужные поля класса:
Контр-коннекторы — те, которые будут на новых трубах (если смотреть на картинку, то там они будут вертикальные), для построения отводов.
Итак, посмотрим код (не пугайтесь), а потом подробно разберём внутрянку, что и почему:
Первая проблема: после первого разреза мы не знаем, какую трубу делить дальше: со старым Id, или с новым. Поэтому написан метод BreakBrokenCurve (вызываем на строке 154):
Мы пробуем разделить или старую, или новую трубу.
Далее нам надо среди этих трёх id найти именно центральную трубу. Для этого используем метод FindNewPipe, который находит эту трубу и заодно заполняет её коннекторы в полях класса: номер 21 и 22.
Мы отбрасываем коннекторы, не являющиеся конечными (игнорируем врезки — строка 237), а затем проверяем их на совпадение с точками разреза трубы. Если у трубы оба коннектора совпали с этими точками, то это новая труба. Её коннекторы мы перезаписали в номер 21 и 22. Порядок коннекторов в принципе не важен (важен только относительный порядок), и с ним мы разберёмся позже в основном методе. Приведу его же код вновь:
Здесь на строках 162-176 мы находим коннекторы 11 и 32 на обрезках изначальной трубы.
И в самом конце мы сдвигаем центральный участок методом MovePipe (строка 177).
Смещение центрального сегмента
Мы уже знаем его и знаем дистанцию смещения, введённую пользователем. Определим направление (введённое пользователем с помощью переключателей в UI) с помощью перечисления:
Но у нас переключатели возвращают bool, а нам нужен enum. Что же делать? Воспользуемся конвертёрами значений и переведём одно в другое с помощью свойства ConverterParameter.
Так выглядит конвертёр:
А так часть настроенных привязок:
Теперь при изменении состояния переключателя у ViewModel меняется свойство Direction. Исходя из него, и напишем метод:
При смещении по вертикали мы меняем параметр "Смещение". При смещении по горизонтали уже нужно двигать трубу через статический класс ElementTransformUtils. Код длинный, но в основном из-за фигурных скобочек, само смещение занимает одну строку для каждого варианта.
Корректировка угла
Самая мозговыносящая часть. Допустим, нам нужно сделать обход не под углом 90, а под меньшим? И что делать? Правильно, вспоминать тригонометрию:
Нужно найти длину отрезка AB, противолежащего углу альфа, и перенести коннектор из точки A в точку B. Длина отрезка AB равна тангенсу альфа, умноженному на величину смещению. А теперь перенесём эти выкладки в код:
На строке 267 мы отбрасываем углы 90 градусов и выше (рассматриваем их как 90). Далее рассмотрим коннекторы. Точку A (270 строка) смещаем в сторону точки B (271 строка), и наоборот.
Выражение (B - A) / B.DistanceTo(A) даёт нам направление смещения: длину мы поделили, а направление в векторах осталось. Далее умножаем на offset и на тангенс введённого пользователем угла.
Перемещаем коннекторы на строках 277-278.
Построение новых участков труб
Эту часть я решил не выносить в отдельный метод, а сделать в основном. По сути, задача простая: соединяем 11 и 21 и 22 и 32:
Тут много кода, но в основном это мы вызываем предыдущие методы. На строках 70 и 71 мы берём из выбранной трубы её тип и уровень, а на 85 и 86 создаём новые трубы.
Поиск противоположных коннекторов
На новых трубах нам нужно найти коннекторы, которые геометрически совпадают с коннекторами на старых (разделённых) трубах, чтобы мы могли создать отводы. Это, разумеется, делает отдельный метод:
Мы передаём в метод новые трубы, там всегда только 2 коннектора. Если один не совпадает, значит это второй — всё просто.
Создание отводов
Самый простой и приятный метод статьи. Мы всё подготовили, осталось лишь создать отводы:
Цикличная работа плагина
Тут я воспользовался циклом do-while, который работает, если переменная IsCyclic переведена пользователем в true. Благодаря этому можно построить сразу несколько обходов с одинаковыми настройками. За счёт использования событий я управляю видимостью окна, чтобы можно было менять настройки и продолжать создание других обходов.
Код итогового метода занял 50 строчек, поэтому в 2 скрина, ориентируйтесь по номерам строк:
В общем-то, и всё. Плагин протестировал, в общем и целом — работает, особенно если не пытаться сместить большую трубу на маленькое расстояние, или сдвинуть горизонтальную трубу в направлениях для вертикальных (тут сработает, только если двигаем в перпендикулярном направлении).
Заключение
На этом с этой частью всё. В следующей части научимся создавать установочник msi и сделаем релиз на гитхабе. Пока можете смотреть код и вносить свои предложения по улучшению.
Код по результатам этой статьи доступен по ссылке на GitHub. Учтите, что он может измениться со временем, но основная логика, скорее всего, будет та же.
Не забывайте подписываться на мой телеграм-канал. До новых встреч!