use std::collections::BinaryHeap;
use axiolid_core::Scalar;
use crate::{
morphology::radius_in_cells, FieldChannel, FieldConfig, LayeredField, LayeredFieldError,
PlanarMask,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TraversalEnvelope {
pub agent_radius: Scalar,
pub agent_height: Scalar,
pub max_step: Scalar,
pub max_slope: Scalar,
}
impl TraversalEnvelope {
pub fn validate(&self) -> Result<(), LayeredFieldError> {
let values = [
self.agent_radius,
self.agent_height,
self.max_step,
self.max_slope,
];
if values.iter().any(|v| !v.is_finite() || *v < 0.0) {
return Err(LayeredFieldError::InvalidEnvelope);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SupportNode {
pub x: usize,
pub y: usize,
pub w: Scalar,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TraversalEvidence {
pub nodes: usize,
pub edges: usize,
pub rejected_by_height: usize,
pub rejected_by_radius: usize,
pub rejected_by_step: usize,
pub rejected_by_slope: usize,
pub components: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RouteOutcome {
Route {
nodes: Vec<SupportNode>,
length: Scalar,
evidence: TraversalEvidence,
},
NoRouteUnderEnvelope {
evidence: TraversalEvidence,
},
}
#[derive(Debug, Clone)]
pub struct TraversalGraph {
width: usize,
cell_size: Scalar,
nodes: Vec<Option<SupportNode>>,
adjacency: Vec<Vec<usize>>,
evidence: TraversalEvidence,
}
impl TraversalGraph {
pub fn build(
field: &LayeredField,
config: &FieldConfig,
envelope: &TraversalEnvelope,
) -> Result<Self, LayeredFieldError> {
envelope.validate()?;
let (width, height) = field.dimensions();
let linear = config.tolerance().linear();
let mut evidence = TraversalEvidence::default();
let blocking = PlanarMask::from_field(field, FieldChannel::SurfacePresence).inverted();
let inflated = blocking.dilate(config, envelope.agent_radius)?;
let reach = radius_in_cells(config, envelope.agent_radius)?;
let mut nodes: Vec<Option<SupportNode>> = vec![None; width * height];
for y in 0..height {
for x in 0..width {
let cell = field
.cell(x, y)
.ok_or(LayeredFieldError::NodeOutsideField)?;
let Some(support) = cell.surfaces().first() else {
continue;
};
let report = crate::clearance_above(field, config, x, y, support.w())?;
if report.distance + linear < envelope.agent_height {
evidence.rejected_by_height += 1;
continue;
}
if reach > 0 && inflated.get(x, y).unwrap_or(true) {
evidence.rejected_by_radius += 1;
continue;
}
nodes[y * width + x] = Some(SupportNode {
x,
y,
w: support.w(),
});
}
}
evidence.nodes = nodes.iter().filter(|node| node.is_some()).count();
let run = config.cell_size();
let mut adjacency = vec![Vec::new(); nodes.len()];
for y in 0..height {
for x in 0..width {
let index = y * width + x;
let Some(from) = nodes[index] else { continue };
for (nx, ny) in [(x + 1, y), (x, y + 1)] {
if nx >= width || ny >= height {
continue;
}
let neighbor = ny * width + nx;
let Some(to) = nodes[neighbor] else { continue };
let rise = (to.w - from.w).abs();
if rise > envelope.max_step + linear {
evidence.rejected_by_step += 1;
continue;
}
if rise / run > envelope.max_slope + linear {
evidence.rejected_by_slope += 1;
continue;
}
adjacency[index].push(neighbor);
adjacency[neighbor].push(index);
evidence.edges += 1;
}
}
}
let mut graph = Self {
width,
cell_size: config.cell_size(),
nodes,
adjacency,
evidence,
};
graph.evidence.components = graph.count_components();
Ok(graph)
}
pub const fn evidence(&self) -> TraversalEvidence {
self.evidence
}
pub fn node(&self, x: usize, y: usize) -> Option<SupportNode> {
self.nodes.get(y * self.width + x).copied().flatten()
}
pub fn connected(&self, from: (usize, usize), to: (usize, usize)) -> bool {
let (Some(start), Some(goal)) = (self.index_of(from), self.index_of(to)) else {
return false;
};
self.component_labels()[start]
.zip(self.component_labels()[goal])
.is_some_and(|(a, b)| a == b)
}
pub fn find_route(
&self,
from: (usize, usize),
to: (usize, usize),
) -> Result<RouteOutcome, LayeredFieldError> {
let start = self
.index_of(from)
.ok_or(LayeredFieldError::NodeOutsideField)?;
let goal = self
.index_of(to)
.ok_or(LayeredFieldError::NodeOutsideField)?;
let mut best = vec![Scalar::INFINITY; self.nodes.len()];
let mut previous = vec![usize::MAX; self.nodes.len()];
let mut queue = BinaryHeap::new();
best[start] = 0.0;
queue.push(Candidate {
cost: 0.0,
index: start,
});
while let Some(Candidate { cost, index }) = queue.pop() {
if index == goal {
break;
}
if cost > best[index] {
continue;
}
for neighbor in &self.adjacency[index] {
let step = self.edge_length(index, *neighbor);
let candidate = cost + step;
let improves = candidate < best[*neighbor]
|| (candidate == best[*neighbor] && index < previous[*neighbor]);
if improves {
best[*neighbor] = candidate;
previous[*neighbor] = index;
queue.push(Candidate {
cost: candidate,
index: *neighbor,
});
}
}
}
if !best[goal].is_finite() {
return Ok(RouteOutcome::NoRouteUnderEnvelope {
evidence: self.evidence,
});
}
let mut chain = vec![goal];
let mut cursor = goal;
while cursor != start {
cursor = previous[cursor];
if cursor == usize::MAX {
return Ok(RouteOutcome::NoRouteUnderEnvelope {
evidence: self.evidence,
});
}
chain.push(cursor);
}
chain.reverse();
Ok(RouteOutcome::Route {
nodes: chain
.iter()
.map(|index| self.nodes[*index].expect("route only visits retained nodes"))
.collect(),
length: best[goal],
evidence: self.evidence,
})
}
fn edge_length(&self, from: usize, to: usize) -> Scalar {
let (Some(a), Some(b)) = (self.nodes[from], self.nodes[to]) else {
return Scalar::INFINITY;
};
let run = self.run_between(a, b);
(run * run + (b.w - a.w) * (b.w - a.w)).sqrt()
}
fn run_between(&self, a: SupportNode, b: SupportNode) -> Scalar {
let dx = (a.x as Scalar) - (b.x as Scalar);
let dy = (a.y as Scalar) - (b.y as Scalar);
(dx * dx + dy * dy).sqrt() * self.cell_run()
}
fn cell_run(&self) -> Scalar {
self.cell_size
}
fn index_of(&self, cell: (usize, usize)) -> Option<usize> {
let index = cell.1 * self.width + cell.0;
(cell.0 < self.width && index < self.nodes.len() && self.nodes[index].is_some())
.then_some(index)
}
fn component_labels(&self) -> Vec<Option<usize>> {
let mut labels = vec![None; self.nodes.len()];
let mut next = 0usize;
let mut stack = Vec::new();
for start in 0..self.nodes.len() {
if self.nodes[start].is_none() || labels[start].is_some() {
continue;
}
let label = next;
next += 1;
labels[start] = Some(label);
stack.push(start);
while let Some(index) = stack.pop() {
for neighbor in &self.adjacency[index] {
if labels[*neighbor].is_none() {
labels[*neighbor] = Some(label);
stack.push(*neighbor);
}
}
}
}
labels
}
fn count_components(&self) -> usize {
self.component_labels()
.iter()
.filter_map(|label| *label)
.max()
.map_or(0, |max| max + 1)
}
}
struct Candidate {
cost: Scalar,
index: usize,
}
impl PartialEq for Candidate {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost && self.index == other.index
}
}
impl Eq for Candidate {}
impl Ord for Candidate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.cost
.total_cmp(&self.cost)
.then_with(|| other.index.cmp(&self.index))
}
}
impl PartialOrd for Candidate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}