1use std::collections::BinaryHeap;
20
21use axiolid_core::Scalar;
22
23use crate::{
24 morphology::radius_in_cells, FieldChannel, FieldConfig, LayeredField, LayeredFieldError,
25 PlanarMask,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct TraversalEnvelope {
31 pub agent_radius: Scalar,
33 pub agent_height: Scalar,
35 pub max_step: Scalar,
37 pub max_slope: Scalar,
39}
40
41impl TraversalEnvelope {
42 pub fn validate(&self) -> Result<(), LayeredFieldError> {
44 let values = [
45 self.agent_radius,
46 self.agent_height,
47 self.max_step,
48 self.max_slope,
49 ];
50 if values.iter().any(|v| !v.is_finite() || *v < 0.0) {
51 return Err(LayeredFieldError::InvalidEnvelope);
52 }
53 Ok(())
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct SupportNode {
60 pub x: usize,
62 pub y: usize,
64 pub w: Scalar,
66}
67
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct TraversalEvidence {
72 pub nodes: usize,
74 pub edges: usize,
76 pub rejected_by_height: usize,
78 pub rejected_by_radius: usize,
80 pub rejected_by_step: usize,
82 pub rejected_by_slope: usize,
84 pub components: usize,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum RouteOutcome {
91 Route {
93 nodes: Vec<SupportNode>,
95 length: Scalar,
97 evidence: TraversalEvidence,
99 },
100 NoRouteUnderEnvelope {
102 evidence: TraversalEvidence,
104 },
105}
106
107#[derive(Debug, Clone)]
109pub struct TraversalGraph {
110 width: usize,
111 cell_size: Scalar,
112 nodes: Vec<Option<SupportNode>>,
113 adjacency: Vec<Vec<usize>>,
114 evidence: TraversalEvidence,
115}
116
117impl TraversalGraph {
118 pub fn build(
124 field: &LayeredField,
125 config: &FieldConfig,
126 envelope: &TraversalEnvelope,
127 ) -> Result<Self, LayeredFieldError> {
128 envelope.validate()?;
129 let (width, height) = field.dimensions();
130 let linear = config.tolerance().linear();
131 let mut evidence = TraversalEvidence::default();
132
133 let blocking = PlanarMask::from_field(field, FieldChannel::SurfacePresence).inverted();
137 let inflated = blocking.dilate(config, envelope.agent_radius)?;
138 let reach = radius_in_cells(config, envelope.agent_radius)?;
139
140 let mut nodes: Vec<Option<SupportNode>> = vec![None; width * height];
141 for y in 0..height {
142 for x in 0..width {
143 let cell = field
144 .cell(x, y)
145 .ok_or(LayeredFieldError::NodeOutsideField)?;
146 let Some(support) = cell.surfaces().first() else {
147 continue;
148 };
149 let report = crate::clearance_above(field, config, x, y, support.w())?;
150 if report.distance + linear < envelope.agent_height {
151 evidence.rejected_by_height += 1;
152 continue;
153 }
154 if reach > 0 && inflated.get(x, y).unwrap_or(true) {
155 evidence.rejected_by_radius += 1;
156 continue;
157 }
158 nodes[y * width + x] = Some(SupportNode {
159 x,
160 y,
161 w: support.w(),
162 });
163 }
164 }
165 evidence.nodes = nodes.iter().filter(|node| node.is_some()).count();
166
167 let run = config.cell_size();
168 let mut adjacency = vec![Vec::new(); nodes.len()];
169 for y in 0..height {
170 for x in 0..width {
171 let index = y * width + x;
172 let Some(from) = nodes[index] else { continue };
173 for (nx, ny) in [(x + 1, y), (x, y + 1)] {
175 if nx >= width || ny >= height {
176 continue;
177 }
178 let neighbor = ny * width + nx;
179 let Some(to) = nodes[neighbor] else { continue };
180 let rise = (to.w - from.w).abs();
181 if rise > envelope.max_step + linear {
182 evidence.rejected_by_step += 1;
183 continue;
184 }
185 if rise / run > envelope.max_slope + linear {
186 evidence.rejected_by_slope += 1;
187 continue;
188 }
189 adjacency[index].push(neighbor);
190 adjacency[neighbor].push(index);
191 evidence.edges += 1;
192 }
193 }
194 }
195
196 let mut graph = Self {
197 width,
198 cell_size: config.cell_size(),
199 nodes,
200 adjacency,
201 evidence,
202 };
203 graph.evidence.components = graph.count_components();
204 Ok(graph)
205 }
206
207 pub const fn evidence(&self) -> TraversalEvidence {
209 self.evidence
210 }
211
212 pub fn node(&self, x: usize, y: usize) -> Option<SupportNode> {
214 self.nodes.get(y * self.width + x).copied().flatten()
215 }
216
217 pub fn connected(&self, from: (usize, usize), to: (usize, usize)) -> bool {
219 let (Some(start), Some(goal)) = (self.index_of(from), self.index_of(to)) else {
220 return false;
221 };
222 self.component_labels()[start]
223 .zip(self.component_labels()[goal])
224 .is_some_and(|(a, b)| a == b)
225 }
226
227 pub fn find_route(
232 &self,
233 from: (usize, usize),
234 to: (usize, usize),
235 ) -> Result<RouteOutcome, LayeredFieldError> {
236 let start = self
237 .index_of(from)
238 .ok_or(LayeredFieldError::NodeOutsideField)?;
239 let goal = self
240 .index_of(to)
241 .ok_or(LayeredFieldError::NodeOutsideField)?;
242
243 let mut best = vec![Scalar::INFINITY; self.nodes.len()];
244 let mut previous = vec![usize::MAX; self.nodes.len()];
245 let mut queue = BinaryHeap::new();
246 best[start] = 0.0;
247 queue.push(Candidate {
248 cost: 0.0,
249 index: start,
250 });
251
252 while let Some(Candidate { cost, index }) = queue.pop() {
253 if index == goal {
254 break;
255 }
256 if cost > best[index] {
257 continue;
258 }
259 for neighbor in &self.adjacency[index] {
260 let step = self.edge_length(index, *neighbor);
261 let candidate = cost + step;
262 let improves = candidate < best[*neighbor]
265 || (candidate == best[*neighbor] && index < previous[*neighbor]);
266 if improves {
267 best[*neighbor] = candidate;
268 previous[*neighbor] = index;
269 queue.push(Candidate {
270 cost: candidate,
271 index: *neighbor,
272 });
273 }
274 }
275 }
276
277 if !best[goal].is_finite() {
278 return Ok(RouteOutcome::NoRouteUnderEnvelope {
279 evidence: self.evidence,
280 });
281 }
282
283 let mut chain = vec![goal];
284 let mut cursor = goal;
285 while cursor != start {
286 cursor = previous[cursor];
287 if cursor == usize::MAX {
288 return Ok(RouteOutcome::NoRouteUnderEnvelope {
289 evidence: self.evidence,
290 });
291 }
292 chain.push(cursor);
293 }
294 chain.reverse();
295
296 Ok(RouteOutcome::Route {
297 nodes: chain
298 .iter()
299 .map(|index| self.nodes[*index].expect("route only visits retained nodes"))
300 .collect(),
301 length: best[goal],
302 evidence: self.evidence,
303 })
304 }
305
306 fn edge_length(&self, from: usize, to: usize) -> Scalar {
307 let (Some(a), Some(b)) = (self.nodes[from], self.nodes[to]) else {
308 return Scalar::INFINITY;
309 };
310 let run = self.run_between(a, b);
311 (run * run + (b.w - a.w) * (b.w - a.w)).sqrt()
312 }
313
314 fn run_between(&self, a: SupportNode, b: SupportNode) -> Scalar {
315 let dx = (a.x as Scalar) - (b.x as Scalar);
316 let dy = (a.y as Scalar) - (b.y as Scalar);
317 (dx * dx + dy * dy).sqrt() * self.cell_run()
318 }
319
320 fn cell_run(&self) -> Scalar {
321 self.cell_size
322 }
323
324 fn index_of(&self, cell: (usize, usize)) -> Option<usize> {
325 let index = cell.1 * self.width + cell.0;
326 (cell.0 < self.width && index < self.nodes.len() && self.nodes[index].is_some())
327 .then_some(index)
328 }
329
330 fn component_labels(&self) -> Vec<Option<usize>> {
331 let mut labels = vec![None; self.nodes.len()];
332 let mut next = 0usize;
333 let mut stack = Vec::new();
334 for start in 0..self.nodes.len() {
335 if self.nodes[start].is_none() || labels[start].is_some() {
336 continue;
337 }
338 let label = next;
339 next += 1;
340 labels[start] = Some(label);
341 stack.push(start);
342 while let Some(index) = stack.pop() {
343 for neighbor in &self.adjacency[index] {
344 if labels[*neighbor].is_none() {
345 labels[*neighbor] = Some(label);
346 stack.push(*neighbor);
347 }
348 }
349 }
350 }
351 labels
352 }
353
354 fn count_components(&self) -> usize {
355 self.component_labels()
356 .iter()
357 .filter_map(|label| *label)
358 .max()
359 .map_or(0, |max| max + 1)
360 }
361}
362
363struct Candidate {
364 cost: Scalar,
365 index: usize,
366}
367
368impl PartialEq for Candidate {
369 fn eq(&self, other: &Self) -> bool {
370 self.cost == other.cost && self.index == other.index
371 }
372}
373
374impl Eq for Candidate {}
375
376impl Ord for Candidate {
377 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
378 other
380 .cost
381 .total_cmp(&self.cost)
382 .then_with(|| other.index.cmp(&self.index))
383 }
384}
385
386impl PartialOrd for Candidate {
387 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
388 Some(self.cmp(other))
389 }
390}