Оглавление
Введение
Представьте: летательный аппарат следует по заданному маршруту на постоянной высоте. Курс выдержан, скорость стабильна. Но впереди — следующая точка маршрута, и она в стороне от текущего направления. Нужно повернуть.
Казалось бы, что тут сложного? Повернул — и летишь дальше. Но у летательного аппарата фиксированного типа есть одно жёсткое ограничение: минимальный радиус разворота. Он не может крутануться на месте. Любой манёвр — это дуга с конкретным радиусом, продиктованным физикой: скоростью, аэродинамикой, конструкцией.
Отсюда возникает задача, которую система управления должна решить заранее: как именно проложить траекторию разворота? Где заканчивается прямолинейный полёт и начинается дуга? Где дуга переходит обратно в прямую, ведущую к цели? Какова длина этой дуги — чтобы автопилот знал, сколько лететь по ней?
Именно эту задачу мы и разберём. Для её решения не понадобится ничего сверхъестественного — только геометрия 9–11 класса: касательная к окружности, теорема Пифагора, подобие треугольников. Весь необходимый аппарат вы уже проходили — просто, возможно, не думали, что он управляет реальными летательными аппаратами.
И вот что интересно: задача достаточно простая, чтобы школьник старших классов не только разобрался в математике, но и самостоятельно построил модель в среде динамического моделирования. Именно это мы и сделаем в конце статьи — разберём реализацию в Engee, с которой вполне справится любой, кто знаком с основами программирования.
В статье мы пройдём путь от постановки задачи через математику — к реализации модели и выбору оптимальной траектории манёвра.
Немного истории: задача Дубинса
В 1957 году американский математик Лестер Дубинс (Lester Eli Dubins) опубликовал работу «О кривых минимальной длины при ограничении на кривизну и заданных граничных условиях на положение и направление».

Звучит сухо, но суть проста: Какой кратчайший путь существует между двумя точками, если у объекта есть фиксированный минимальный радиус поворота и заданное направление движения в начале и конце пути?
Дубинс доказал, что оптимальный путь всегда состоит не более чем из трёх сегментов, каждый из которых — либо дуга окружности, либо прямая. Отсюда появилась классификация путей Дубинса:
Тип | Расшифровка |
RSR | Правый поворот → Прямая → Правый поворот |
LSL | Левый поворот → Прямая → Левый поворот |
RSL | Правый поворот → Прямая → Левый поворот |
LSR | Левый поворот → Прямая → Правый поворот |
RLR | Правый поворот → Левый поворот → Правый поворот |
LRL | Левый поворот → Правый поворот → Левый поворот |
(R — right, L — left, S — straight)
Система управления перебирает все применимые варианты и выбирает кратчайший. Именно поэтому алгоритм Дубинса до сих пор живёт в системах управления летательными аппаратами, в планировщиках маршрутов автономных автомобилей и в морской навигации.
Наша задача — упрощённый частный случай: один разворот, одна дуга, известный радиус. Но геометрия та же самая. И именно с неё удобно начинать, если вы впервые сталкиваетесь с темой планирования манёвров.
Постановка задачи
Итак, у нас есть:
Точка A — откуда летим;
Точка B — точка начала разворота;
Точка C — куда нужно попасть после разворота;
Радиус разворота R — задан физическими характеристиками летательного аппарата: скоростью, аэродинамикой, допустимой перегрузкой;
Линейная скорость V — постоянная на всей траектории.
Нужно найти:
Точку D — где дуга заканчивается и начинается прямолинейный полёт к C;
Длину дуги BD — чтобы система управления знала, сколько лететь по дуге;
Время манёвра от точки B до точки C — складывается из времен прохождения дуги BD и прямолинейного участка DC.
Траектория выглядит так: A → B → (дуга) → D → C (рис.2).

Обратите внимание на ключевое геометрическое свойство: прямая AB касается окружности разворота в точке B, а прямая DC касается той же окружности в точке D. Касательность обеспечивает плавный вход и выход из разворота без резкого излома курса — аппарат плавно переходит с прямой на дугу и обратно.
Всё остальное — следствие этого факта и школьной геометрии.
Математика: как это решается?
Шаг первый: Находим центр окружности O
Раз AB — касательная, а OB — радиус, то по свойству касательной: OB ⊥ AB. Значит, центр O лежит на перпендикуляре к AB, проведённом через точку B.
Коэффициенты нормали к прямой AB:
Масштабный коэффициент:
Два кандидата на центр (рис.3):

Выбираем O с меньшим расстоянием до C — он соответствует развороту в нужную сторону.
Шаг 2. Находим точку касания D
Из точки C проводим две касательные к окружности. Точки касания D₁ и D₂ симметричны относительно OC, а отрезок D₁D₂ перпендикулярен OC и пересекает его в точке H.
Находим H — проекцию O на прямую OC:
Длина отрезка DH из подобия треугольников и теоремы Пифагора:
Нормаль к прямой OC:
Два кандидата на точку D (рис.4):

Шаг 3. Выбираем правильную точку D
Какую из двух точек выбрать — зависит от угла ∠ABC:
Условие | Выбор |
∠ABC > 90° | D с меньшим BD |
∠ABC < 90° | D с большим BD |
∠ABC = 90° | D с большим AD |
Угол оцениваем через теорему Пифагора — без тригонометрии:
AC2>AB2+BC2 - угол тупой
AC2<AB2+BC2 - угол острый

Шаг 4. Считаем длину дуги BD
По теореме косинусов из треугольника OBD:
Расстояние от C до прямой AB:
Если L1<2R и угол острый — большая дуга:
Иначе — меньшая:
Шаг 5. Считаем характеристики траектории
Зная координаты всех точек и длину дуги, посчитать характеристики траектории несложно. Разобьём путь на два участка:
Дуга B → D — разворот по окружности. Длина дуги уже найдена на шаге 4. В таком случае время её прохождения:
Участок D → C — прямолинейный полёт к цели:
Итого расстояние и время:
Реализация в Engee
Всю описанную математику мы реализовали в среде Engee на языке Julia. Engee — это облачная среда динамического моделирования, доступная бесплатно прямо в браузере. Никакой установки, никаких зависимостей — открыл и считаешь. Именно поэтому с ней вполне справится школьник старших классов: порог входа минимальный, а результат — полноценная инженерная модель.
Структура кода
Скрипт разбит на шесть логических блоков. Разберём каждый подробно.
Блок 1. Структура Waypoint
struct Waypoint x::Float64 y::Float64 end
Первое, что бросается в глаза — мы не используем название Point, хотя логично было бы. Причина проста: в Engee уже есть встроенный тип Point из графических пакетов (Makie, GeometryBasics), и попытка объявить свой Point приведёт к ошибке. Поэтому называем нашу структуру Waypoint — навигационная точка маршрута. Заодно название точнее отражает смысл.
Важный момент для Julia: struct обязан быть объявлен до первого использования. Поэтому он стоит самым первым блоком, ещё до входных параметров.
Блок 2. Входные параметры
A = Waypoint(0.0, 0.0) # Начальная точка маршрута B = Waypoint(2000.0, 0.0) # Точка начала разворота C = Waypoint(2400.0, -1800.0) # Целевая точка R = 300.0 # Радиус разворота, м V = 35.0 # Скорость, м/с
Все параметры задачи собраны в одном месте — в самом начале файла. Это сделано намеренно: чтобы поменять задачу, не нужно лезть вглубь кода. Изменили координаты точек, радиус или скорость — и запускаете заново.
Параметры подобраны так, чтобы соответствовать реальному лёгкому летательному аппарату:
Маршрут порядка 2–3 км — типично для тактической задачи;
Скорость 35 м/с (~126 км/ч) — крейсерский режим;
Радиус 300 м — соответствует крену ~30° на данной скорости.
Блок 3. Вспомогательная функция distance
distance(p1::Waypoint, p2::Waypoint) = sqrt((p1.x - p2.x)^2 + (p1.y - p2.y)^2)
Евклидово расстояние между двумя точками — формула из школьного курса геометрии. Используется повсюду в алгоритме: при выборе центра O, при вычислении хорды BD, при подсчёте длин участков маршрута.
Блок 4. Функция compute_turn — ядро алгоритма
function compute_turn(A::Waypoint, B::Waypoint, C::Waypoint, R::Float64) # Шаг 1. Коэффициенты нормали к прямой AB A_ab = B.y - A.y B_ab = A.x - B.x t = R / sqrt(A_ab^2 + B_ab^2) # Шаг 2. Два кандидата на центр окружности O O1 = Waypoint(B.x + A_ab * t, B.y + B_ab * t) O2 = Waypoint(B.x - A_ab * t, B.y - B_ab * t) O = distance(C, O1) < distance(C, O2) ? O1 : O2 # Шаг 3. Находим точку H CO = distance(C, O) t1 = R^2 / CO^2 H = Waypoint(O.x + t1 * (C.x - O.x), O.y + t1 * (C.y - O.y)) # Шаг 4. Длина DH и нормаль к OC DH = R * sqrt(max(0.0, 1 - t1)) A_oc = O.y - C.y B_oc = C.x - O.x t2 = DH / sqrt(A_oc^2 + B_oc^2) # Шаг 5. Два кандидата на точку D D1 = Waypoint(H.x + A_oc * t2, H.y + B_oc * t2) D2 = Waypoint(H.x - A_oc * t2, H.y - B_oc * t2) # Шаг 6. Выбираем D по углу ABC AC² = distance(A, C)^2 AB² = distance(A, B)^2 BC² = distance(B, C)^2 BD1 = distance(B, D1) BD2 = distance(B, D2) if AC² ≈ AB² + BC² D = distance(A, D1) > distance(A, D2) ? D1 : D2 elseif AC² > AB² + BC² D = BD1 < BD2 ? D1 : D2 else D = BD1 > BD2 ? D1 : D2 end # Шаг 7. Центральный угол φ BD = distance(B, D) cos_φ = clamp(1 - BD^2 / (2 * R^2), -1.0, 1.0) φ = acos(cos_φ) # Шаг 8. Расстояние от C до прямой AB L1 = abs(A_ab * C.x + B_ab * C.y - A_ab * A.x - B_ab * A.y) / sqrt(A_ab^2 + B_ab^2) # Шаг 9. Длина дуги BD arc_BD = if L1 < 2R && AC² < AB² + BC² (2π - φ) * R else φ * R end return D, O, arc_BD, φ end
Это главная функция скрипта. Принимает три точки и радиус, возвращает четыре значения: точку касания D, центр окружности O, длину дуги и центральный угол φ. Внутри — прямая реализация шагов 1–4 из математического раздела.
Никаких итераций, никакого численного поиска — только последовательные арифметические операции. Каждый шаг снабжён комментарием, поэтому код читается почти так же, как математический вывод выше.
Одна строка заслуживает отдельного внимания:
cos_φ = clamp(1 - BD^2 / (2 * R^2), -1.0, 1.0)
Блок 5. Вывод результатов
После вызова compute_turn считаются все метрики траектории и выводятся в консоль:
============================================= Входные параметры: A = (0.0, 0.0) B = (1500.0, 0.0) C = (2000.0, -1800.0) R = 300.0 м V = 35.0 м/с ============================================= Результаты: Центр окружности O = (1500.0, -300.0) Точка касания D = (1797.4, -260.9) Центральный угол = 82.5° Угловая скорость = 0.117 рад/с --------------------------------------------- Участок A → B: 1500.0 м | 42.9 с Дуга B → D: 432.0 м | 12.3 с Участок D → C: 1552.4 м | 44.4 с --------------------------------------------- Итого расстояние: 3484.4 м Итого время: 99.6 с =============================================
Блок 6. Визуализация
angle_B = atan(B.y - O.y, B.x - O.x) angle_D = atan(D.y - O.y, D.x - O.x) angles = range(angle_B, angle_D, length=200) arc_x = O.x .+ R .* cos.(angles) arc_y = O.y .+ R .* sin.(angles) θ = range(0, 2π, length=300) circle_x = O.x .+ R .* cos.(θ) circle_y = O.y .+ R .* sin.(θ) plt = plot(circle_x, circle_y, linestyle=:dash, color=:lightgray, label="Окружность разворота", aspect_ratio=:equal) plot!(plt, [A.x, B.x], [A.y, B.y], color=:blue, linewidth=2, label="A → B") plot!(plt, arc_x, arc_y, color=:red, linewidth=2, label="Дуга B → D") plot!(plt, [D.x, C.x], [D.y, C.y], color=:green, linewidth=2, label="D → C") scatter!(plt, [A.x, B.x, C.x, D.x, O.x], [A.y, B.y, C.y, D.y, O.y], color=:black, markersize=5, label="") off = R * 0.08 annotate!(plt, A.x, A.y + off, text("A", 10, :blue)) annotate!(plt, B.x, B.y + off, text("B", 10, :blue)) annotate!(plt, C.x, C.y + off, text("C", 10, :green)) annotate!(plt, D.x, D.y + off, text("D", 10, :red)) annotate!(plt, O.x, O.y + off, text("O", 10, :gray)) title!(plt, "Траектория A → B → (дуга) → D → C") xlabel!(plt, "x, м") ylabel!(plt, "y, м") xlims!(plt, -100, 2500) ylims!(plt, -2500, 100) display(plt)
График траектории
На графике (рис. 6) отображается:
Синяя линия — прямой участок A → B, полёт по исходному курсу
Красная дуга — разворот B → D, траектория манёвра
Зелёная линия — прямой участок D → C, выход на курс к цели
Серый пунктир — окружность разворота целиком, радиуса R с центром O
Чёрные точки — ключевые точки траектории: A, B, C, D, O
Точка O — центр окружности разворота, находится на перпендикуляре к AB в точке B на расстоянии R. Точка D — место выхода из разворота, где аппарат плавно переходит с дуги на прямую к C.

Практическое применение
Задача, которую мы разобрали — не учебный пример. Она встречается везде, где есть объект с фиксированным радиусом поворота и заданным маршрутом.
Летательные аппараты. Самолёт не может мгновенно сменить курс — он обязан выполнить разворот с определённым радиусом, который зависит от скорости и допустимого крена. Именно поэтому в бортовых системах управления расчёт точки начала манёвра выполняется заранее, до того как аппарат её достигнет. Та же логика используется при построении схем захода на посадку и процедурных разворотов в инструментальных полётах.
Наземные роботы. Любой робот с дифференциальным приводом имеет минимальный радиус поворота. Планировщики пути для таких роботов используют ровно ту же геометрию — она лежит в основе алгоритмов семейства путей Дубинса, которые применяются в мобильной робототехнике повсеместно.
Автономные автомобили. Расчёт траектории поворота на перекрёстке, перестроения на трассе, выезда с парковки — везде присутствует ограничение на кривизну пути. Алгоритмы планирования траектории в системах типа Apollo и Autoware опираются на схожий математический аппарат.
Морские суда. Радиус циркуляции крупного танкера или контейнеровоза может достигать нескольких километров. Капитан обязан начать манёвр задолго до нужной точки — иначе судно просто не впишется в фарватер. Здесь цена ошибки в расчёте точки начала поворота измеряется не метрами, а посадкой на мель.
Общее во всех случаях одно: объект с кинематическим ограничением на радиус поворота должен заранее знать, где начать манёвр. Геометрия, которую мы разобрали, даёт на этот вопрос точный ответ — и, как выяснилось, для этого достаточно школьного курса математики.
Заключение
Мы прошли путь от простой инженерной задачи — где начать разворот — до полного геометрического решения с реализацией в среде моделирования Engee.
Главный вывод: за системами управления реальных летательных аппаратов стоит математика, которую проходят в школе. Касательная, теорема Пифагора, подобие треугольников — этого достаточно, чтобы рассчитать траекторию манёвра.
Задача Дубинса, сформулированная в 1957 году, до сих пор остаётся актуальной — и наше решение является её частным случаем: один разворот, одна дуга, фиксированный радиус, известная точка начала манёвра. Если тема заинтересовала, следующий шаг — кривые Ридса-Шеппа. Это обобщение задачи Дубинса для объектов, которые умеют двигаться назад. Казалось бы, небольшое добавление — но оно кардинально меняет пространство решений: задний ход иногда позволяет найти путь значительно короче, чем только вперёд. Особенно актуально для автономных автомобилей и роботов на парковке — там манёвр задним ходом часто единственный способ вписаться в узкое пространство.
И напоследок: если вы читаете это в старших классах школы — у вас уже есть весь необходимый математический аппарат. Осталось только попробовать.
