Конструктор облика двухбалочного БПЛА
Author
using GeometryBasics
using FileIO, MeshIO
using LinearAlgebra
# ============================================================
# ПАРАМЕТРЫ МОДЕЛИ (все переменные для управления геометрией)
# ============================================================
include_landing_gear = true
# --- Фюзеляж ---
fuselage_offset_z = -0.2 # опускаем весь фюзеляж вниз
k_nose_x = 0.6 # сплющивание в носу (вдоль X)
k_tail_x = 0.95 # сплющивание в хвосте
k_bottom_z = 0.45 # сплющивание дна (по Z)
k_top_z = 1.0 # верх — круглый
# Профиль фюзеляжа (x, r) — от носа к хвосту
fuselage_profile = [
(-1.70, 0.00), # вершина носа (ближе к телу → нос тупее)
(-1.69, 0.06), # сразу большой радиус → скруглённый, а не острый
(-1.67, 0.12),
(-1.64, 0.18),
(-1.60, 0.23),
(-1.55, 0.27),
(-1.48, 0.31),
(-1.40, 0.34),
(-1.30, 0.37),
(-1.15, 0.39),
(-1.00, 0.40), # зона максимума
(-0.70, 0.41), # «пузо»
(-0.40, 0.41),
(-0.10, 0.41),
( 0.20, 0.41),
( 0.50, 0.41),
( 0.80, 0.41),
( 1.10, 0.41),
( 1.40, 0.41),
( 1.60, 0.41),
]
fuselage_lathe_N = 48 # число точек в кольце
# --- Крыло ---
wing_span = 6.8
wing_chord_root = 0.9
wing_chord_tip = 0.3
wing_thickness = 0.08
wing_root_x_front = -0.2 # положение передней кромки в корне
wing_sweep_tip = 0.2 # смещение передней кромки на конце (назад)
wing_offset = Vec3f(0.48, 0, -0.20)
# --- Пилоны (балки) ---
boom_radius = 0.05
boom_separation = 1.40
d_boom = boom_separation / 2
boom_x_start = 1.0
boom_z_start = -0.15
boom_length = 2.4
boom_angle = deg2rad(25) # наклон вверх
boom_x_end = boom_x_start + boom_length * cos(boom_angle)
boom_z_end = boom_z_start + boom_length * sin(boom_angle)
# --- Хвостовое оперение (V-tail) ---
tail_chord = 0.45
tail_height = 0.75 # ← увеличено, чтобы tail_height ≥ d_boom
tail_offset_x = 0.4
# --- Шасси ---
strut_radius = 0.03
wheel_radius = 0.10
wheel_width = 0.06
ground_z = -0.80
nose_strut_x = -1.0
main_strut_x = 0.5
main_wheel_y = 0.45
main_strut_y_root = 0.20
# Углы наклона стоек (положительный — колесо уходит назад, в -X)
nose_strut_angle = deg2rad(15) # передняя стойка наклонена назад
main_strut_angle = deg2rad(0) # основные — вертикально (можно тоже наклонить)
nose_strut_z_top = -0.20 # верхняя точка крепления носовой стойки
main_strut_z_top = -0.15 # верхняя точка основных стоек
# --- Трубка Пито ---
pitot_length = 0.25
pitot_radius = 0.008
pitot_x_start = -1.70 # начало (у вершины носа)
pitot_x_end = -1.70 - pitot_length
# --- Винглеты ---
include_winglets = true
winglet_length_up = 0.25 # длина вверх от плоскости крыла
winglet_length_down = 0.10 # длина вниз от плоскости крыла
# Наклоны передней кромки (смещение верхней/нижней точки назад по X)
winglet_up_sweep_front = -0.12 # верхняя часть: скос передней кромки
winglet_down_sweep_front = -0.03 # нижняя часть: скос передней кромки
# Наклоны задней кромки (смещение верхней/нижней точки назад по X)
winglet_up_sweep_back = -0.01 # верхняя часть: скос задней кромки
winglet_down_sweep_back = -0.01 # нижняя часть: скос задней кромки
# Отклонение в сторону (пока 0)
winglet_deflect_top = 0.0
winglet_deflect_bot = 0.0
winglet_thickness = 0.02
# ============================================================
# Вспомогательные функции
# ============================================================
to_mesh(prim) = GeometryBasics.mesh(prim)
rot_x(θ) = Float32[1 0 0; 0 cos(θ) -sin(θ); 0 sin(θ) cos(θ)]
function transform_mesh(mesh, translation::Vec3f, rotation_matrix::Matrix{Float32};
scale::Vec3f = Vec3f(1,1,1))
new_points = [Point3f(rotation_matrix * (p .* scale) + translation)
for p in coordinates(mesh)]
return Mesh(new_points, faces(mesh))
end
# ============================================================
# Прогрессивные сплющивания
# ============================================================
function flatten_factor_x(x; x_nose=-1.8, x_tail=1.6, k_nose=k_nose_x, k_tail=k_tail_x)
t = clamp((x - x_nose) / (x_tail - x_nose), 0.0, 1.0)
return k_nose + (k_tail - k_nose) * (1 - cos(π * t)) / 2
end
function flatten_factor_z(z_norm; k_bottom=k_bottom_z, k_top=k_top_z)
t = (z_norm + 1) / 2
return k_bottom + (k_top - k_bottom) * t
end
function flatten_fuselage(mesh)
pts = Point3f[]
for p in coordinates(mesh)
r_local = sqrt(p[2]^2 + p[3]^2)
z_norm = r_local > 1e-6 ? p[3] / r_local : 0.0
k_x = flatten_factor_x(p[1])
k_z = flatten_factor_z(z_norm)
z_new = p[3] * k_x * k_z
push!(pts, Point3f(p[1], p[2], z_new))
end
return Mesh(pts, faces(mesh))
end
# ============================================================
# Фюзеляж (тело вращения)
# ============================================================
function lathe_x(profile::Vector{Tuple{Float64,Float64}}, N::Int)
points = Point3f[]
faces = TriangleFace{Int}[]
ring_start = Int[]
for (x, r) in profile
push!(ring_start, length(points) + 1)
if r ≈ 0
push!(points, Point3f(x, 0, 0))
else
for k in 0:N-1
θ = 2π * k / N
push!(points, Point3f(x, r*cos(θ), r*sin(θ)))
end
end
end
for i in 1:length(profile)-1
s1 = ring_start[i]; s2 = ring_start[i+1]
r1 = profile[i][2]; r2 = profile[i+1][2]
if r1 ≈ 0 && r2 > 0
for k in 0:N-1
push!(faces, TriangleFace(s1, s2+k, s2+mod(k+1,N)))
end
elseif r1 > 0 && r2 ≈ 0
for k in 0:N-1
push!(faces, TriangleFace(s1+k, s1+mod(k+1,N), s2))
end
elseif r1 > 0 && r2 > 0
for k in 0:N-1
a1=s1+k; b1=s1+mod(k+1,N); a2=s2+k; b2=s2+mod(k+1,N)
push!(faces, TriangleFace(a1, b1, b2))
push!(faces, TriangleFace(a1, b2, a2))
end
end
end
return Mesh(points, faces)
end
function cap_disc(x::Float64, r::Float64, N::Int)
pts = Point3f[Point3f(x, 0, 0)]
faces = TriangleFace{Int}[]
for k in 0:N-1
θ = 2π * k / N
push!(pts, Point3f(x, r*cos(θ), r*sin(θ)))
end
for k in 0:N-1
push!(faces, TriangleFace(1, 2+mod(k+1,N), 2+k))
end
return Mesh(pts, faces)
end
# ============================================================
# 1. Фюзеляж
# ============================================================
fuselage_mesh = lathe_x(fuselage_profile, fuselage_lathe_N)
fuselage_mesh = flatten_fuselage(fuselage_mesh)
# Задняя крышка: координата и радиус берутся из профиля
x_tail, r_tail = fuselage_profile[end]
cap = cap_disc(x_tail, r_tail, fuselage_lathe_N)
cap = flatten_fuselage(cap)
# Опукаем фюзеляж (и крышку) вниз
fuselage_mesh = transform_mesh(fuselage_mesh, Vec3f(0, 0, fuselage_offset_z),
Matrix{Float32}(I, 3, 3))
cap = transform_mesh(cap, Vec3f(0, 0, fuselage_offset_z),
Matrix{Float32}(I, 3, 3))
# ============================================================
# 2. Крыло
# ============================================================
function make_wing_half(sign)
pts = Point3f[]
faces = TriangleFace{Int}[]
root_x_front = wing_root_x_front
root_x_back = root_x_front + wing_chord_root
tip_x_front = root_x_front + wing_sweep_tip
tip_x_back = tip_x_front + wing_chord_tip
root_y = 0.0
tip_y = sign * wing_span / 2
root_z_bot = -wing_thickness/2
root_z_top = wing_thickness/2
tip_z_bot = -wing_thickness/4
tip_z_top = wing_thickness/4
push!(pts, Point3f(root_x_front, root_y, root_z_bot))
push!(pts, Point3f(root_x_back, root_y, root_z_bot))
push!(pts, Point3f(root_x_back, root_y, root_z_top))
push!(pts, Point3f(root_x_front, root_y, root_z_top))
push!(pts, Point3f(tip_x_front, tip_y, tip_z_bot))
push!(pts, Point3f(tip_x_back, tip_y, tip_z_bot))
push!(pts, Point3f(tip_x_back, tip_y, tip_z_top))
push!(pts, Point3f(tip_x_front, tip_y, tip_z_top))
push!(faces, TriangleFace(1,2,6)); push!(faces, TriangleFace(1,6,5))
push!(faces, TriangleFace(4,8,7)); push!(faces, TriangleFace(4,7,3))
push!(faces, TriangleFace(1,5,8)); push!(faces, TriangleFace(1,8,4))
push!(faces, TriangleFace(2,3,7)); push!(faces, TriangleFace(2,7,6))
push!(faces, TriangleFace(1,4,3)); push!(faces, TriangleFace(1,3,2))
push!(faces, TriangleFace(5,6,7)); push!(faces, TriangleFace(5,7,8))
return Mesh(pts, faces)
end
wing_left = make_wing_half(-1)
wing_right = make_wing_half(+1)
wing_left = transform_mesh(wing_left, wing_offset, Matrix{Float32}(I, 3, 3))
wing_right = transform_mesh(wing_right, wing_offset, Matrix{Float32}(I, 3, 3))
function make_winglet(sign)
# sign = +1 для правого крыла, -1 для левого
pts = Point3f[]
faces = TriangleFace{Int}[]
# --- Концевое сечение крыла (корень винглета) ---
tip_y = sign * wing_span / 2
tip_x_front = wing_root_x_front + wing_sweep_tip
tip_x_back = tip_x_front + wing_chord_tip
tip_z_bot = -wing_thickness/4
tip_z_top = wing_thickness/4
# --- Верхняя кромка винглета ---
z_top = tip_z_top + winglet_length_up
x_front_top = tip_x_front - winglet_up_sweep_front
x_back_top = tip_x_back + winglet_up_sweep_back
y_top = tip_y + sign * winglet_deflect_top
# --- Нижняя кромка винглета ---
z_bot = tip_z_bot - winglet_length_down
x_front_bot = tip_x_front - winglet_down_sweep_front
x_back_bot = tip_x_back + winglet_down_sweep_back
y_bot = tip_y + sign * winglet_deflect_bot
# --- Корневое сечение (на уровне крыла) ---
z_root_front = (tip_z_bot + tip_z_top) / 2 # середина толщины крыла
x_front_root = tip_x_front
x_back_root = tip_x_back
y_root = tip_y
half_t = winglet_thickness / 2
# 12 вершин: 3 сечения × 2 стороны (внутренняя/наружная) × 2 кромки (перед/зад)
# Порядок:
# 1-2: нижняя кромка (перед, зад) внутренняя сторона
# 3-4: корневая (перед, зад) внутренняя сторона
# 5-6: верхняя кромка (перед, зад) внутренняя сторона
# 7-8: нижняя кромка (перед, зад) внешняя сторона
# 9-10: корневая (перед, зад) внешняя сторона
# 11-12: верхняя кромка (перед, зад) внешняя сторона
# Внутренняя сторона
push!(pts, Point3f(x_front_bot, y_bot - sign*half_t, z_bot)) # 1
push!(pts, Point3f(x_back_bot, y_bot - sign*half_t, z_bot)) # 2
push!(pts, Point3f(x_front_root, y_root - sign*half_t, z_root_front)) # 3
push!(pts, Point3f(x_back_root, y_root - sign*half_t, z_root_front)) # 4
push!(pts, Point3f(x_front_top, y_top - sign*half_t, z_top)) # 5
push!(pts, Point3f(x_back_top, y_top - sign*half_t, z_top)) # 6
# Внешняя сторона
push!(pts, Point3f(x_front_bot, y_bot + sign*half_t, z_bot)) # 7
push!(pts, Point3f(x_back_bot, y_bot + sign*half_t, z_bot)) # 8
push!(pts, Point3f(x_front_root, y_root + sign*half_t, z_root_front)) # 9
push!(pts, Point3f(x_back_root, y_root + sign*half_t, z_root_front)) # 10
push!(pts, Point3f(x_front_top, y_top + sign*half_t, z_top)) # 11
push!(pts, Point3f(x_back_top, y_top + sign*half_t, z_top)) # 12
# --- Грани ---
# Внутренняя сторона (разбита на 4 треугольника: нижняя половина + верхняя)
push!(faces, TriangleFace(1,2,4)); push!(faces, TriangleFace(1,4,3)) # нижняя внутренняя
push!(faces, TriangleFace(3,4,6)); push!(faces, TriangleFace(3,6,5)) # верхняя внутренняя
# Внешняя сторона
push!(faces, TriangleFace(7,9,10)); push!(faces, TriangleFace(7,10,8)) # нижняя внешняя
push!(faces, TriangleFace(9,11,12)); push!(faces, TriangleFace(9,12,10)) # верхняя внешняя
# Нижняя кромка
push!(faces, TriangleFace(1,7,8)); push!(faces, TriangleFace(1,8,2))
# Верхняя кромка
push!(faces, TriangleFace(5,6,12)); push!(faces, TriangleFace(5,12,11))
# Передняя кромка (нижняя половина + верхняя половина)
push!(faces, TriangleFace(1,3,9)); push!(faces, TriangleFace(1,9,7))
push!(faces, TriangleFace(3,5,11)); push!(faces, TriangleFace(3,11,9))
# Задняя кромка
push!(faces, TriangleFace(2,8,10)); push!(faces, TriangleFace(2,10,4))
push!(faces, TriangleFace(4,10,12)); push!(faces, TriangleFace(4,12,6))
return Mesh(pts, faces)
end
# ============================================================
# 3. Пилоны
# ============================================================
boom_left = to_mesh(Cylinder(Point3f(boom_x_start, -d_boom, boom_z_start),
Point3f(boom_x_end, -d_boom, boom_z_end),
boom_radius))
boom_right = to_mesh(Cylinder(Point3f(boom_x_start, d_boom, boom_z_start),
Point3f(boom_x_end, d_boom, boom_z_end),
boom_radius))
# ============================================================
# 4. V-tail (сдвинут назад на ширину крыла)
# ============================================================
tail_angle_v = asin(d_boom / tail_height)
# Базовая пластина
tail_base = Rect3f(Point3f(0, -0.02, 0), Vec3f(tail_chord, 0.04, tail_height))
tail_base_mesh = to_mesh(tail_base)
# Сдвиг назад: оперение начинается на boom_x_end - tail_chord + tail_offset_x
tail_x_start = boom_x_end + tail_offset_x + tail_chord # = boom_x_end + offset
# Фактически: передняя кромка на boom_x_end + tail_offset_x, задняя на +tail_chord
# Но чтобы «перенести назад на ширину крыла», используем tail_offset_x = tail_chord
# (это уже учтено в переменной tail_offset_x = 0.45 в параметрах)
tail_left_mesh = transform_mesh(
tail_base_mesh,
Vec3f(boom_x_end + tail_offset_x - tail_chord, -d_boom, boom_z_end),
rot_x(-tail_angle_v)
)
tail_right_mesh = transform_mesh(
tail_base_mesh,
Vec3f(boom_x_end + tail_offset_x - tail_chord, d_boom, boom_z_end),
rot_x( tail_angle_v)
)
# Перекладина — точно по верхним кромкам
top_z = boom_z_end + tail_height * cos(tail_angle_v)
top_bar = Rect3f(
Point3f(boom_x_end + tail_offset_x - tail_chord, -0.02, top_z - 0.03),
Vec3f(tail_chord, 0.04, 0.05)
)
top_bar_mesh = to_mesh(top_bar)
# ============================================================
# 5. Шасси (опционально)
# ============================================================
landing_gear_meshes = Any[]
if include_landing_gear
# ---- Носовая стойка: верх в (nose_strut_x, 0, nose_strut_z_top),
# колесо смещено назад на величину (z_top - ground_z) * tan(angle)
nose_dz = nose_strut_z_top - ground_z # положительная длина по Z
nose_wheel_x = nose_strut_x - nose_dz * tan(nose_strut_angle) # назад = -X
nose_strut = to_mesh(Cylinder(
Point3f(nose_strut_x, 0, nose_strut_z_top),
Point3f(nose_wheel_x, 0, ground_z),
strut_radius
))
nose_wheel = to_mesh(Cylinder(
Point3f(nose_wheel_x, -wheel_width/2, ground_z),
Point3f(nose_wheel_x, wheel_width/2, ground_z),
wheel_radius
))
push!(landing_gear_meshes, nose_strut, nose_wheel)
# ---- Левая основная стойка
main_dz = main_strut_z_top - ground_z
left_wheel_x = main_strut_x - main_dz * tan(main_strut_angle)
left_strut = to_mesh(Cylinder(
Point3f(main_strut_x, -main_strut_y_root, main_strut_z_top),
Point3f(left_wheel_x, -main_wheel_y, ground_z),
strut_radius
))
left_wheel = to_mesh(Cylinder(
Point3f(left_wheel_x, -main_wheel_y - wheel_width/2, ground_z),
Point3f(left_wheel_x, -main_wheel_y + wheel_width/2, ground_z),
wheel_radius
))
push!(landing_gear_meshes, left_strut, left_wheel)
# ---- Правая основная стойка
right_wheel_x = main_strut_x - main_dz * tan(main_strut_angle)
right_strut = to_mesh(Cylinder(
Point3f(main_strut_x, main_strut_y_root, main_strut_z_top),
Point3f(right_wheel_x, main_wheel_y, ground_z),
strut_radius
))
right_wheel = to_mesh(Cylinder(
Point3f(right_wheel_x, main_wheel_y - wheel_width/2, ground_z),
Point3f(right_wheel_x, main_wheel_y + wheel_width/2, ground_z),
wheel_radius
))
push!(landing_gear_meshes, right_strut, right_wheel)
end
# ============================================================
# Трубка Пито
# ============================================================
pitot_mesh = to_mesh(Cylinder(
Point3f(pitot_x_start, 0, 0),
Point3f(pitot_x_end, 0, 0),
pitot_radius
))
# Трубка должна быть на уровне оси фюзеляжа (с учётом его смещения вниз)
pitot_mesh = transform_mesh(pitot_mesh, Vec3f(0, 0, fuselage_offset_z),
Matrix{Float32}(I, 3, 3))
# ============================================================
# Винглеты
# ============================================================
winglet_meshes = Any[]
if include_winglets
wl_left = make_winglet(-1)
wl_right = make_winglet(+1)
# Сдвигаем вместе с крылом (тот же wing_offset)
wl_left = transform_mesh(wl_left, wing_offset, Matrix{Float32}(I, 3, 3))
wl_right = transform_mesh(wl_right, wing_offset, Matrix{Float32}(I, 3, 3))
push!(winglet_meshes, wl_left, wl_right)
end
# ============================================================
# 6. Объединение и экспорт
# ============================================================
all_meshes = [fuselage_mesh, cap, wing_left, wing_right,
boom_left, boom_right,
tail_left_mesh, tail_right_mesh, top_bar_mesh, pitot_mesh]
append!(all_meshes, winglet_meshes)
append!(all_meshes, landing_gear_meshes)
all_points = Point3f[]
all_faces = TriangleFace{Int}[]
offset = 0
for m in all_meshes
pts = coordinates(m)
fcs = faces(m)
append!(all_points, pts)
for f in fcs
push!(all_faces, TriangleFace(f[1]+offset, f[2]+offset, f[3]+offset))
end
global offset += length(pts)
end
final_mesh = Mesh(all_points, all_faces);
save("drone_model.stl", final_mesh);