1use std::cmp::Ordering;
40use std::collections::BinaryHeap;
41
42use crate::continuous::{PolygonPath, PolygonPathfinder, PolygonSearchResult};
43use crate::polygonal::{Point2, PolygonScene, PolygonSearchRequest};
44
45const EPSILON: f64 = 1e-9;
46
47#[derive(Debug, Clone, Copy, Default)]
58pub struct VisibilityGraph;
59
60impl PolygonPathfinder for VisibilityGraph {
61 fn name(&self) -> &'static str {
62 "visibility-graph"
63 }
64
65 fn search(&self, scene: &PolygonScene, request: PolygonSearchRequest) -> PolygonSearchResult {
69 if !scene.is_walkable(request.start) {
70 return Err(crate::continuous::PolygonSearchError::InvalidStart {
71 point: request.start,
72 });
73 }
74 if !scene.is_walkable(request.goal) {
75 return Err(crate::continuous::PolygonSearchError::InvalidGoal {
76 point: request.goal,
77 });
78 }
79 if scene.validate(request).is_err() {
80 return crate::continuous::not_found(0);
81 }
82
83 if points_equal(request.start, request.goal) {
84 return crate::continuous::found(
85 PolygonPath::from_points(vec![request.start])
86 .expect("polygon path contains at least one point"),
87 1,
88 );
89 }
90
91 let nodes = collect_nodes(scene, request);
92 let adjacency = build_visibility_edges(scene, &nodes);
93 let (cost, predecessors, visited_nodes) =
94 match shortest_path(&adjacency, 0, 1, request.budget) {
95 Ok(outcome) => outcome,
96 Err(reason) => return Err(crate::continuous::budget_error(reason)),
97 };
98
99 match cost {
100 Some(goal_cost) => {
101 let points = reconstruct_path(&nodes, &predecessors, 1);
102 crate::continuous::found(
103 PolygonPath::from_points_with_cost(points, goal_cost)
104 .expect("polygon path contains at least one point"),
105 visited_nodes,
106 )
107 }
108 None => crate::continuous::not_found(visited_nodes),
109 }
110 }
111}
112
113fn collect_nodes(scene: &PolygonScene, request: PolygonSearchRequest) -> Vec<Point2> {
114 let mut nodes = vec![request.start, request.goal];
115 for obstacle in &scene.obstacles {
116 for &vertex in obstacle.vertices() {
117 if !nodes.iter().any(|point| points_equal(*point, vertex)) {
118 nodes.push(vertex);
119 }
120 }
121 }
122 nodes
123}
124
125fn build_visibility_edges(scene: &PolygonScene, nodes: &[Point2]) -> Vec<Vec<(usize, f64)>> {
126 let mut adjacency = vec![Vec::new(); nodes.len()];
127
128 for left_index in 0..nodes.len() {
129 for right_index in (left_index + 1)..nodes.len() {
130 let start = nodes[left_index];
131 let end = nodes[right_index];
132 if scene.segment_is_walkable(start, end) {
133 let cost = start.distance_to(end);
134 adjacency[left_index].push((right_index, cost));
135 adjacency[right_index].push((left_index, cost));
136 }
137 }
138 }
139
140 adjacency
141}
142
143type ShortestPathOutcome = (Option<f64>, Vec<Option<usize>>, usize);
144
145fn shortest_path(
146 adjacency: &[Vec<(usize, f64)>],
147 start_index: usize,
148 goal_index: usize,
149 budget: condor_core::SearchBudget,
150) -> Result<ShortestPathOutcome, condor_core::BudgetExhausted> {
151 let mut distances = vec![f64::INFINITY; adjacency.len()];
152 let mut predecessors = vec![None; adjacency.len()];
153 let mut closed = vec![false; adjacency.len()];
154 let mut frontier = BinaryHeap::new();
155 let mut visited_nodes = 0usize;
156 let watch = condor_core::BudgetWatch::start(budget);
157
158 distances[start_index] = 0.0;
159 frontier.push(HeapEntry {
160 node_index: start_index,
161 cost: 0.0,
162 });
163
164 while let Some(entry) = frontier.pop() {
165 if closed[entry.node_index] {
166 continue;
167 }
168
169 closed[entry.node_index] = true;
170 visited_nodes += 1;
171
172 if entry.node_index == goal_index {
173 return Ok((Some(entry.cost), predecessors, visited_nodes));
174 }
175
176 watch.check(visited_nodes)?;
177
178 for &(neighbor_index, edge_cost) in &adjacency[entry.node_index] {
179 if closed[neighbor_index] {
180 continue;
181 }
182
183 let next_cost = entry.cost + edge_cost;
184 if next_cost + EPSILON < distances[neighbor_index] {
185 distances[neighbor_index] = next_cost;
186 predecessors[neighbor_index] = Some(entry.node_index);
187 frontier.push(HeapEntry {
188 node_index: neighbor_index,
189 cost: next_cost,
190 });
191 }
192 }
193 }
194
195 Ok((None, predecessors, visited_nodes))
196}
197
198fn reconstruct_path(
199 nodes: &[Point2],
200 predecessors: &[Option<usize>],
201 goal_index: usize,
202) -> Vec<Point2> {
203 let mut reversed = Vec::new();
204 let mut current = Some(goal_index);
205
206 while let Some(index) = current {
207 reversed.push(nodes[index]);
208 current = predecessors[index];
209 }
210
211 reversed.reverse();
212 reversed
213}
214
215fn points_equal(left: Point2, right: Point2) -> bool {
216 (left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
217}
218
219#[derive(Debug, Clone, Copy, PartialEq)]
220struct HeapEntry {
221 node_index: usize,
222 cost: f64,
223}
224
225impl Eq for HeapEntry {}
226
227impl Ord for HeapEntry {
228 fn cmp(&self, other: &Self) -> Ordering {
229 other
230 .cost
231 .total_cmp(&self.cost)
232 .then_with(|| other.node_index.cmp(&self.node_index))
233 }
234}
235
236impl PartialOrd for HeapEntry {
237 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
238 Some(self.cmp(other))
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::VisibilityGraph;
245 use crate::continuous::PolygonPathfinder;
246 use crate::polygonal::{Point2, Polygon, PolygonScene, PolygonSearchRequest, WorldBounds};
247
248 #[test]
249 fn visibility_graph_finds_direct_path_in_open_space() {
250 let scene = PolygonScene {
251 world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
252 obstacles: Vec::new(),
253 };
254 let request = PolygonSearchRequest::new(Point2::new(1.0, 1.0), Point2::new(9.0, 1.0));
255
256 let result = VisibilityGraph.search(&scene, request);
257
258 assert!(result.as_ref().expect("valid search request").is_found());
259 let path = result
260 .as_ref()
261 .expect("valid search request")
262 .path()
263 .expect("path should be present");
264 assert_eq!(path.points(), &[request.start, request.goal]);
265 assert!((path.cost() - 8.0).abs() <= 1e-9);
266 }
267
268 #[test]
269 fn visibility_graph_reports_no_path_for_separator() {
270 let scene = PolygonScene {
271 world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
272 obstacles: vec![Polygon::new(vec![
273 Point2::new(4.0, 0.0),
274 Point2::new(6.0, 0.0),
275 Point2::new(6.0, 10.0),
276 Point2::new(4.0, 10.0),
277 ])],
278 };
279 let request = PolygonSearchRequest::new(Point2::new(2.0, 5.0), Point2::new(8.0, 5.0));
280
281 let result = VisibilityGraph.search(&scene, request);
282
283 assert!(!result.as_ref().expect("valid search request").is_found());
284 assert!(
285 result
286 .as_ref()
287 .expect("valid search request")
288 .path()
289 .is_none()
290 );
291 assert_eq!(result.as_ref().expect("valid search request").cost(), None);
292 }
293}