use glam::Vec2;
#[derive(Clone, Copy, Debug)]
pub struct AvoidanceConfig {
pub neighbor_distance: f32,
pub max_neighbors: usize,
pub radius: f32,
pub time_horizon: f32,
pub angle_samples: u32,
}
impl Default for AvoidanceConfig {
fn default() -> Self {
Self {
neighbor_distance: 4.0,
max_neighbors: 8,
radius: 0.4,
time_horizon: 2.0,
angle_samples: 16,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Neighbor2 {
pub position: Vec2,
pub velocity: Vec2,
pub radius: f32,
}
fn earliest_collision(
position: Vec2,
candidate: Vec2,
neighbor: &Neighbor2,
radius_sum: f32,
horizon: f32,
) -> f32 {
let d = neighbor.position - position;
let relative = (candidate - neighbor.velocity) * 0.5;
let c = d.dot(d) - radius_sum * radius_sum;
if c < 0.0 {
return 0.0;
}
let a = relative.dot(relative);
if a <= 1e-12 {
return f32::INFINITY;
}
let b = 2.0 * d.dot(relative);
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return f32::INFINITY;
}
let t = (-b - discriminant.sqrt()) / (2.0 * a);
if t >= 0.0 && t <= horizon { t } else { f32::INFINITY }
}
fn consider(
candidate: Vec2,
index: u32,
desired: Vec2,
position: Vec2,
near: &[(f32, Neighbor2)],
config: &AvoidanceConfig,
best: &mut Option<(bool, f32, u32, Vec2)>,
) {
let mut earliest = f32::INFINITY;
for (_, neighbor) in near {
let t = earliest_collision(
position,
candidate,
neighbor,
config.radius + neighbor.radius,
config.time_horizon,
);
if t < earliest {
earliest = t;
}
}
let feasible = earliest.is_infinite();
let key = if feasible { (candidate - desired).length() } else { -earliest };
let better = match *best {
None => true,
Some((best_feasible, best_key, best_index, _)) => {
(feasible && !best_feasible)
|| (feasible == best_feasible
&& (key < best_key || (key == best_key && index < best_index)))
}
};
if better {
*best = Some((feasible, key, index, candidate));
}
}
pub fn resolve(desired: Vec2, position: Vec2, neighbors: &[Neighbor2], config: &AvoidanceConfig) -> Vec2 {
let mut near: Vec<(f32, Neighbor2)> = neighbors
.iter()
.filter(|n| position.distance(n.position) - n.radius - config.radius <= config.neighbor_distance)
.map(|n| (position.distance(n.position), *n))
.collect();
near.sort_by(|a, b| {
a.0.total_cmp(&b.0)
.then(a.1.position.x.to_bits().cmp(&b.1.position.x.to_bits()))
.then(a.1.position.y.to_bits().cmp(&b.1.position.y.to_bits()))
.then(a.1.velocity.x.to_bits().cmp(&b.1.velocity.x.to_bits()))
.then(a.1.velocity.y.to_bits().cmp(&b.1.velocity.y.to_bits()))
});
near.truncate(config.max_neighbors);
if near.is_empty() {
return desired;
}
let speed = desired.length();
if speed < 1e-6 {
return Vec2::ZERO;
}
let base = desired / speed;
let speeds = [1.0f32, 0.7, 0.4];
let mut best: Option<(bool, f32, u32, Vec2)> = None;
let mut index = 0u32;
for sample in 0..config.angle_samples {
let angle = std::f32::consts::TAU * sample as f32 / config.angle_samples as f32;
let (sin, cos) = angle.sin_cos();
let rotated = Vec2::new(base.x * cos - base.y * sin, base.x * sin + base.y * cos);
for scale in speeds {
consider(rotated * (speed * scale), index, desired, position, &near, config, &mut best);
index += 1;
}
}
consider(Vec2::ZERO, index, desired, position, &near, config, &mut best);
best.map(|(_, _, _, velocity)| velocity).unwrap_or(desired)
}