2026-04-10 20:28:55 +03:00
|
|
|
|
use bevy::prelude::*;
|
2026-04-11 18:09:30 +03:00
|
|
|
|
use crate::states::level::*;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
use bevy::window::PrimaryWindow;
|
2026-04-12 23:34:36 +03:00
|
|
|
|
use std::collections::HashSet;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
pub fn setup_cannon(mut commands: Commands, asset_server: Res<AssetServer>) {
|
2026-04-10 20:28:55 +03:00
|
|
|
|
let texture: Handle<Image> = asset_server.load("cannon/cannon.png");
|
|
|
|
|
|
|
|
|
|
|
|
commands.spawn((
|
|
|
|
|
|
LevelMarker,
|
|
|
|
|
|
Cannon,
|
2026-04-11 18:09:30 +03:00
|
|
|
|
Transform::from_xyz(579.0 - (1280.0 / 2.0), (768.0 / 2.0) - 150.0, CANNON_Z_INDEX),
|
2026-04-10 20:28:55 +03:00
|
|
|
|
Sprite::from_image(texture),
|
|
|
|
|
|
Visibility::Visible,
|
|
|
|
|
|
));
|
2026-04-11 18:09:30 +03:00
|
|
|
|
println!("Cannon visuals is set up");
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn rotate_cannon(
|
|
|
|
|
|
mut query: Query<&mut Transform, With<Cannon>>,
|
|
|
|
|
|
window_query: Query<&Window, With<PrimaryWindow>>,
|
|
|
|
|
|
camera_query: Query<(&Camera, &GlobalTransform)>,
|
|
|
|
|
|
) {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let Ok(mut cannon_tf) = query.single_mut() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
let Ok(window) = window_query.single() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(cursor_pos) = window.cursor_position() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
let Ok((camera, camera_tf)) = camera_query.single() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
|
|
|
|
|
// Конвертируем экранные координаты курсора в мировые
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let Ok(world_cursor_pos) = camera.viewport_to_world_2d(camera_tf, cursor_pos) else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
|
|
|
|
|
// Вектор направления от пушки к курсору
|
|
|
|
|
|
let cannon_pos = Vec2::new(cannon_tf.translation.x, cannon_tf.translation.y);
|
|
|
|
|
|
let direction = world_cursor_pos - cannon_pos;
|
|
|
|
|
|
|
|
|
|
|
|
// Применяем вращение по оси Z
|
|
|
|
|
|
let angle = direction.to_angle() - std::f32::consts::FRAC_PI_2;
|
|
|
|
|
|
cannon_tf.rotation = Quat::from_rotation_z(angle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn spawn_projectile_from_cannon(
|
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
|
cannon_query: Query<&Transform, With<Cannon>>,
|
|
|
|
|
|
mouse_input: Res<ButtonInput<MouseButton>>,
|
|
|
|
|
|
asset_server: Res<AssetServer>,
|
2026-04-11 18:09:30 +03:00
|
|
|
|
mut cannon_state: ResMut<CannonState>
|
2026-04-10 20:28:55 +03:00
|
|
|
|
) {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
if !mouse_input.just_pressed(MouseButton::Left) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let Ok(cannon_tf) = cannon_query.single() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-11 18:09:30 +03:00
|
|
|
|
let ball_type = cannon_state.current_type;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
let image = asset_server.load(ball_type.asset_path());
|
|
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let offset = 100.0; // сдвиг от центра к дулу пушки
|
2026-04-10 20:28:55 +03:00
|
|
|
|
let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize();
|
|
|
|
|
|
let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset;
|
2026-04-11 18:09:30 +03:00
|
|
|
|
let spawn_pos = spawn_pos_2d.extend(PROJECTILE_Z_INDEX);
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
|
|
|
|
|
commands.spawn((
|
|
|
|
|
|
LevelMarker,
|
|
|
|
|
|
Transform::from_translation(spawn_pos),
|
|
|
|
|
|
Sprite {
|
|
|
|
|
|
image,
|
|
|
|
|
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
|
|
|
|
|
..default()
|
|
|
|
|
|
},
|
|
|
|
|
|
Visibility::Visible,
|
|
|
|
|
|
BallProjectile {
|
|
|
|
|
|
velocity: direction * 800.0,
|
|
|
|
|
|
previous_position: spawn_pos_2d,
|
|
|
|
|
|
ball_type,
|
|
|
|
|
|
},
|
|
|
|
|
|
));
|
2026-04-11 18:09:30 +03:00
|
|
|
|
cannon_state.fire();
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn move_projectiles(
|
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
|
mut projectiles: Query<(Entity, &mut BallProjectile, &mut Transform)>,
|
|
|
|
|
|
time: Res<Time>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// обновляем позицию
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let delta = proj.velocity * time.delta_secs();
|
|
|
|
|
|
let new_pos = proj.previous_position + delta;
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-10 20:28:55 +03:00
|
|
|
|
transform.translation = new_pos.extend(transform.translation.z);
|
|
|
|
|
|
proj.previous_position = new_pos;
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// удаляем снаряды, улетевшие за экран
|
2026-04-10 20:28:55 +03:00
|
|
|
|
if new_pos.x.abs() > 2000.0 || new_pos.y.abs() > 2000.0 {
|
|
|
|
|
|
commands.entity(entity).despawn();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
pub fn cycle_cannon_type(
|
|
|
|
|
|
mut cannon_state: ResMut<CannonState>,
|
|
|
|
|
|
mouse_input: Res<ButtonInput<MouseButton>>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if mouse_input.just_pressed(MouseButton::Right) {
|
|
|
|
|
|
cannon_state.cycle_next();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn update_cannon_preview(
|
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
|
cannon_state: Res<CannonState>,
|
|
|
|
|
|
asset_server: Res<AssetServer>,
|
|
|
|
|
|
cannon_query: Query<Entity, With<Cannon>>,
|
|
|
|
|
|
muzzle_query: Query<Entity, With<CurrentPreviewMarker>>,
|
|
|
|
|
|
next_query: Query<Entity, With<NextPreviewMarker>>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let Ok(cannon_entity) = cannon_query.single() else { return };
|
|
|
|
|
|
|
|
|
|
|
|
// шарик у дула
|
|
|
|
|
|
let muzzle_image = asset_server.load(cannon_state.current_type.asset_path());
|
|
|
|
|
|
|
|
|
|
|
|
if let Ok(muzzle_entity) = muzzle_query.single() {
|
|
|
|
|
|
// усли шарик у дула есть, то обновляем спрайт
|
|
|
|
|
|
commands.entity(muzzle_entity).insert(Sprite::from_image(muzzle_image));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
//иначе создаем
|
|
|
|
|
|
let preview_entity = commands.spawn((
|
|
|
|
|
|
CurrentPreviewMarker,
|
|
|
|
|
|
Sprite{
|
|
|
|
|
|
image: muzzle_image,
|
|
|
|
|
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
|
|
|
|
|
..default()
|
|
|
|
|
|
},
|
|
|
|
|
|
Transform::from_xyz(0.0, 100.0, CURRENT_SHOT_Z_INDEX),
|
|
|
|
|
|
Visibility::Visible,
|
|
|
|
|
|
)).id();
|
|
|
|
|
|
commands.entity(cannon_entity).add_child(preview_entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// следующий шарик в хвосте
|
|
|
|
|
|
let next_image = asset_server.load(cannon_state.next_type.asset_path());
|
|
|
|
|
|
|
|
|
|
|
|
if let Ok(next_entity) = next_query.single() {
|
|
|
|
|
|
commands.entity(next_entity).insert(Sprite::from_image(next_image));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let preview_entity = commands.spawn((
|
|
|
|
|
|
NextPreviewMarker,
|
|
|
|
|
|
Sprite{
|
|
|
|
|
|
image: next_image,
|
|
|
|
|
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
|
|
|
|
|
..default()
|
|
|
|
|
|
},
|
|
|
|
|
|
Transform::from_xyz(0.0, -50.0, NEXT_SHOT_Z_INDEX),
|
|
|
|
|
|
Visibility::Visible,
|
|
|
|
|
|
)).id();
|
|
|
|
|
|
commands.entity(cannon_entity).add_child(preview_entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn build_cannon_state() -> CannonState {
|
|
|
|
|
|
CannonState {
|
|
|
|
|
|
current_type: BallType::random(),
|
|
|
|
|
|
next_type: BallType::random(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const HITBOX_TYPE: HitboxType = HitboxType::Circle;
|
|
|
|
|
|
const HIT_THRESHOLD: f32 = SLOT_SIZE * 0.9; // Радиус/половина стороны хитбокса
|
|
|
|
|
|
|
|
|
|
|
|
fn project_point_to_segment(point: Vec2, p1: Vec2, p2: Vec2) -> Vec2 {
|
|
|
|
|
|
// вспомогательная функция для детекта попадания
|
|
|
|
|
|
// проекция точки на отрезок, возвращает ближайшую точку на отрезке [p1, p2] к точке point
|
|
|
|
|
|
let segment = p2 - p1;
|
|
|
|
|
|
let len_sq = segment.length_squared();
|
|
|
|
|
|
|
|
|
|
|
|
if len_sq < 0.0001 { return p1; } // защита от деления на ноль
|
|
|
|
|
|
|
|
|
|
|
|
// проекция: t = 0 -> p1, t = 1 -> p2, t < 0 или > 1 -> за пределами отрезка
|
|
|
|
|
|
let t = ((point - p1).dot(segment)) / len_sq;
|
|
|
|
|
|
let t_clamped = t.clamp(0.0, 1.0);
|
|
|
|
|
|
|
|
|
|
|
|
p1 + segment * t_clamped
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 20:28:55 +03:00
|
|
|
|
pub fn detect_projectile_hit(
|
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
|
track: Res<TrackPath>,
|
2026-04-11 15:12:11 +03:00
|
|
|
|
mut wave: ResMut<WaveState>,
|
2026-04-10 20:28:55 +03:00
|
|
|
|
mut projectiles: Query<(Entity, &mut BallProjectile)>,
|
2026-04-11 15:12:11 +03:00
|
|
|
|
mut balls: Query<(Entity, &mut Ball)>,
|
2026-04-10 20:28:55 +03:00
|
|
|
|
) {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
if wave.is_queue_locked {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 15:14:27 +03:00
|
|
|
|
for (proj_entity, proj) in projectiles.iter_mut() {
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// ищем ближайшую точку на треке
|
|
|
|
|
|
// TODO почистить старые неиспользуемые вещи
|
|
|
|
|
|
let mut nearest_proj_point = Vec2::ZERO;
|
|
|
|
|
|
let mut nearest_segment_idx = 0;
|
|
|
|
|
|
let mut min_dist = f32::MAX;
|
|
|
|
|
|
let mut t_along_segment = 0.0;
|
|
|
|
|
|
|
|
|
|
|
|
for i in 0..track.points.len() - 1 {
|
|
|
|
|
|
let p1 = track.points[i];
|
|
|
|
|
|
let p2 = track.points[i + 1];
|
|
|
|
|
|
|
|
|
|
|
|
let proj_point = project_point_to_segment(proj.previous_position, p1, p2);
|
|
|
|
|
|
let dist = proj.previous_position.distance(proj_point);
|
|
|
|
|
|
|
|
|
|
|
|
if dist < min_dist {
|
|
|
|
|
|
min_dist = dist;
|
|
|
|
|
|
nearest_proj_point = proj_point;
|
|
|
|
|
|
nearest_segment_idx = i;
|
|
|
|
|
|
|
|
|
|
|
|
let segment = p2 - p1;
|
|
|
|
|
|
t_along_segment = if segment.length_squared() > 0.0001 {
|
|
|
|
|
|
((proj_point - p1).dot(segment)) / segment.length_squared()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
};
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// проверяем попадание
|
|
|
|
|
|
let hit = match HITBOX_TYPE {
|
|
|
|
|
|
HitboxType::Circle => {
|
|
|
|
|
|
min_dist < HIT_THRESHOLD
|
|
|
|
|
|
}
|
|
|
|
|
|
HitboxType::Square => {
|
|
|
|
|
|
let p1 = track.points[nearest_segment_idx];
|
|
|
|
|
|
let p2 = track.points[nearest_segment_idx + 1];
|
|
|
|
|
|
is_point_in_oriented_rect(proj.previous_position, p1, p2, HIT_THRESHOLD)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if !hit {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// определяем индекс для вставки
|
|
|
|
|
|
let target_slot_idx = if t_along_segment < 0.5 {
|
|
|
|
|
|
nearest_segment_idx
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nearest_segment_idx + 1
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let insert_idx;
|
|
|
|
|
|
let target_progress;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
if let Some((_, target_ball)) = balls.iter().find(|(_, b)| b.slot_index == target_slot_idx) {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
target_progress = target_ball.slot_progress;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
let proj_before_ball = t_along_segment < target_progress;
|
|
|
|
|
|
insert_idx = if proj_before_ball {
|
|
|
|
|
|
if target_slot_idx > 0 { target_slot_idx - 1 } else { 0 }
|
2026-04-10 20:28:55 +03:00
|
|
|
|
} else {
|
2026-04-12 23:34:36 +03:00
|
|
|
|
target_slot_idx
|
2026-04-11 15:12:11 +03:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// лочим очередь для мутаций
|
2026-04-11 15:12:11 +03:00
|
|
|
|
wave.is_queue_locked = true;
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// ищем конец непрерывной очереди начиная с insert_idx
|
|
|
|
|
|
// строим набор занятых индексов (обязательно до вставки)
|
|
|
|
|
|
let occupied: HashSet<usize> = balls.iter()
|
|
|
|
|
|
.map(|(_, b)| b.slot_index)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
let mut continuous_block_end = insert_idx;
|
|
|
|
|
|
loop {
|
|
|
|
|
|
let next_idx = continuous_block_end + 1;
|
|
|
|
|
|
if occupied.contains(&next_idx) {
|
|
|
|
|
|
continuous_block_end = next_idx;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// разрыв найден, остановка
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// сдвигаем шарики внутри непрерывного блока
|
2026-04-11 15:12:11 +03:00
|
|
|
|
let max_idx = track.points.len() - 1;
|
|
|
|
|
|
let mut to_despawn = Vec::new();
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
for (entity, mut ball) in balls.iter_mut() {
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// сдвигаем только если шарик в непрерывном блоке
|
|
|
|
|
|
if ball.slot_index >= insert_idx && ball.slot_index <= continuous_block_end {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
if ball.slot_index < max_idx {
|
|
|
|
|
|
ball.slot_index += 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
to_despawn.push(entity);
|
2026-04-12 23:34:36 +03:00
|
|
|
|
}
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// шарики за разрывом ball.slot_index > continuous_block_end не двигаем
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-11 15:12:11 +03:00
|
|
|
|
for entity in to_despawn {
|
|
|
|
|
|
commands.entity(entity).despawn();
|
|
|
|
|
|
}
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// вставляем шарик-снаряд в очередь
|
|
|
|
|
|
commands.entity(proj_entity)
|
|
|
|
|
|
.insert(Ball {
|
2026-04-11 15:12:11 +03:00
|
|
|
|
ball_type: proj.ball_type,
|
|
|
|
|
|
slot_index: insert_idx,
|
|
|
|
|
|
slot_progress: target_progress,
|
2026-04-12 23:34:36 +03:00
|
|
|
|
})
|
|
|
|
|
|
.remove::<BallProjectile>();
|
2026-04-12 16:39:23 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// сразу визуально синхронизируем
|
2026-04-12 16:39:23 +03:00
|
|
|
|
if let Some(&curr_pos) = track.points.get(insert_idx) {
|
|
|
|
|
|
let visual_pos = if let Some(&next_pos) = track.points.get(insert_idx + 1) {
|
|
|
|
|
|
curr_pos.lerp(next_pos, target_progress)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
curr_pos
|
|
|
|
|
|
};
|
|
|
|
|
|
commands.entity(proj_entity).insert(
|
|
|
|
|
|
Transform::from_translation(visual_pos.extend(10.0))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-12 23:34:36 +03:00
|
|
|
|
|
2026-04-11 19:40:22 +03:00
|
|
|
|
wave.last_insert_index = Some(insert_idx);
|
2026-04-12 23:34:36 +03:00
|
|
|
|
wave.is_queue_locked = false;
|
2026-04-10 20:28:55 +03:00
|
|
|
|
}
|
2026-04-11 15:12:11 +03:00
|
|
|
|
}
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
fn is_point_in_oriented_rect(point: Vec2, p1: Vec2, p2: Vec2, half_width: f32) -> bool {
|
|
|
|
|
|
// вспомогательная функция для квадратного хитбокса
|
|
|
|
|
|
// проверяет находится ли point внутри прямоугольника на отрезке [p1, p2] с половинной толщиной
|
|
|
|
|
|
let segment = p2 - p1;
|
|
|
|
|
|
let segment_len = segment.length();
|
|
|
|
|
|
|
|
|
|
|
|
if segment_len < 0.0001 {
|
|
|
|
|
|
return point.distance(p1) < half_width; // вырожденный сегмент
|
2026-04-11 18:09:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// локальная система координат: ось x вдоль сегмента, ось y перпендикуляр
|
|
|
|
|
|
let dir = segment.normalize();
|
|
|
|
|
|
let perp = Vec2::new(-dir.y, dir.x); // перпендикуляр
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// вектор от начала сегмента до точки
|
|
|
|
|
|
let to_point = point - p1;
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// проекции на локальные оси
|
|
|
|
|
|
let along = to_point.dot(dir); // координата вдоль сегмента
|
|
|
|
|
|
let across = to_point.dot(perp); // координата поперёк сегмента
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
// проверка границ прямоугольника
|
|
|
|
|
|
let within_length = along >= -half_width && along <= segment_len + half_width;
|
|
|
|
|
|
let within_width = across.abs() <= half_width;
|
2026-04-11 18:09:30 +03:00
|
|
|
|
|
2026-04-12 23:34:36 +03:00
|
|
|
|
within_length && within_width
|
|
|
|
|
|
}
|