1use glam::Vec2;
10
11use crate::geometry::{closest_point_on_segment, segment_intersection};
12use crate::graph::{EdgeId, NodeId, RoadGraph};
13
14#[derive(Clone, Default)]
15struct HashCell {
16 nodes: Vec<NodeId>,
17 edges: Vec<EdgeId>,
18}
19
20pub struct SpatialHash {
22 cell_size: f32,
23 cols: usize,
24 rows: usize,
25 cells: Vec<HashCell>,
26}
27
28impl SpatialHash {
29 const MAX_CELLS: usize = 4_000_000;
32
33 pub fn new(world_width: f32, world_depth: f32, cell_size: f32) -> Self {
37 assert!(
38 world_width.is_finite() && world_depth.is_finite() && cell_size.is_finite(),
39 "SpatialHash dimensions must be finite (got {world_width} x {world_depth}, cell {cell_size})"
40 );
41 assert!(
42 world_width > 0.0 && world_depth > 0.0 && cell_size > 0.0,
43 "SpatialHash dimensions must be positive (got {world_width} x {world_depth}, cell {cell_size})"
44 );
45 let mut cs = cell_size;
46 loop {
47 let cols = (world_width / cs).ceil() as usize;
48 let rows = (world_depth / cs).ceil() as usize;
49 if cols.saturating_mul(rows) <= Self::MAX_CELLS {
50 return Self {
51 cell_size: cs,
52 cols,
53 rows,
54 cells: vec![HashCell::default(); cols * rows],
55 };
56 }
57 cs *= 2.0;
58 }
59 }
60
61 fn coords_to_index(&self, pos: Vec2) -> Option<usize> {
62 let col = (pos.x / self.cell_size).floor() as isize;
63 let row = (pos.y / self.cell_size).floor() as isize;
64
65 if col >= 0 && col < self.cols as isize && row >= 0 && row < self.rows as isize {
66 Some(row as usize * self.cols + col as usize)
67 } else {
68 None
69 }
70 }
71
72 pub fn insert_node(&mut self, id: NodeId, pos: Vec2) {
74 if let Some(idx) = self.coords_to_index(pos) {
75 self.cells[idx].nodes.push(id);
76 }
77 }
78
79 pub fn remove_edge(&mut self, id: EdgeId, start: Vec2, end: Vec2) {
81 let min_col = ((start.x.min(end.x) / self.cell_size).floor() as isize).max(0);
82 let max_col =
83 ((start.x.max(end.x) / self.cell_size).floor() as isize).min(self.cols as isize - 1);
84 let min_row = ((start.y.min(end.y) / self.cell_size).floor() as isize).max(0);
85 let max_row =
86 ((start.y.max(end.y) / self.cell_size).floor() as isize).min(self.rows as isize - 1);
87
88 for r in min_row..=max_row {
89 for c in min_col..=max_col {
90 let idx = r as usize * self.cols + c as usize;
91 self.cells[idx].edges.retain(|&e| e != id);
92 }
93 }
94 }
95
96 pub fn insert_edge(&mut self, id: EdgeId, start: Vec2, end: Vec2) {
98 let min_col = ((start.x.min(end.x) / self.cell_size).floor() as isize).max(0);
99 let max_col =
100 ((start.x.max(end.x) / self.cell_size).floor() as isize).min(self.cols as isize - 1);
101 let min_row = ((start.y.min(end.y) / self.cell_size).floor() as isize).max(0);
102 let max_row =
103 ((start.y.max(end.y) / self.cell_size).floor() as isize).min(self.rows as isize - 1);
104
105 for r in min_row..=max_row {
106 for c in min_col..=max_col {
107 let idx = r as usize * self.cols + c as usize;
108 self.cells[idx].edges.push(id);
109 }
110 }
111 }
112
113 pub(crate) fn edges_in_region(&self, start: Vec2, end: Vec2, padding: f32) -> Vec<EdgeId> {
115 let cells = self.cells_for_region(start, end, padding);
116 let mut ids = Vec::new();
117 for cell in &cells {
118 for &eid in &cell.edges {
119 if !ids.contains(&eid) {
120 ids.push(eid);
121 }
122 }
123 }
124 ids
125 }
126
127 fn cells_for_region(&self, start: Vec2, end: Vec2, padding: f32) -> Vec<&HashCell> {
129 let min_col = (((start.x.min(end.x) - padding) / self.cell_size).floor() as isize).max(0);
130 let max_col = (((start.x.max(end.x) + padding) / self.cell_size).floor() as isize)
131 .min(self.cols as isize - 1);
132 let min_row = (((start.y.min(end.y) - padding) / self.cell_size).floor() as isize).max(0);
133 let max_row = (((start.y.max(end.y) + padding) / self.cell_size).floor() as isize)
134 .min(self.rows as isize - 1);
135
136 let mut result = Vec::new();
137 for r in min_row..=max_row {
138 for c in min_col..=max_col {
139 result.push(&self.cells[r as usize * self.cols + c as usize]);
140 }
141 }
142 result
143 }
144}
145
146pub enum TraceResult {
148 Clear(Vec2),
150 SnappedToNode(NodeId),
152 SnappedToEdge {
154 edge_id: EdgeId,
155 intersection_pos: Vec2,
156 },
157}
158
159pub fn resolve_trace_step(
166 graph: &RoadGraph,
167 spatial: &SpatialHash,
168 start_pos: Vec2,
169 proposed_pos: Vec2,
170 snap_radius: f32,
171 current_node_id: NodeId,
172) -> TraceResult {
173 let cells = spatial.cells_for_region(start_pos, proposed_pos, snap_radius);
174 let snap_sq = snap_radius * snap_radius;
175
176 let current_neighbours: Vec<NodeId> = graph.nodes[current_node_id as usize]
179 .edges
180 .iter()
181 .filter(|&&eid| graph.edges[eid as usize].active)
182 .map(|&eid| graph.opposite(eid, current_node_id))
183 .collect();
184
185 let mut closest_crossing: Option<(EdgeId, Vec2)> = None;
190 let mut closest_crossing_dist = f32::MAX;
191
192 for cell in &cells {
193 for &e_id in &cell.edges {
194 let edge = &graph.edges[e_id as usize];
195 if !edge.active {
196 continue;
197 }
198 if edge.start == current_node_id || edge.end == current_node_id {
199 continue;
200 }
201
202 let e_start = graph.nodes[edge.start as usize].position;
203 let e_end = graph.nodes[edge.end as usize].position;
204
205 if let Some(intersect) = segment_intersection(start_pos, proposed_pos, e_start, e_end) {
206 let dist = start_pos.distance_squared(intersect);
207 if dist < closest_crossing_dist {
208 closest_crossing_dist = dist;
209 closest_crossing = Some((e_id, intersect));
210 }
211 }
212 }
213 }
214
215 if let Some((edge_id, intersection_pos)) = closest_crossing {
216 let edge = &graph.edges[edge_id as usize];
217 let e_start = graph.nodes[edge.start as usize].position;
218 let e_end = graph.nodes[edge.end as usize].position;
219
220 let dist_to_start = intersection_pos.distance_squared(e_start);
221 let dist_to_end = intersection_pos.distance_squared(e_end);
222
223 if dist_to_start < snap_sq && dist_to_start <= dist_to_end {
224 let nid = edge.start;
225 if nid != current_node_id {
226 return TraceResult::SnappedToNode(nid);
227 }
228 return TraceResult::SnappedToEdge {
229 edge_id,
230 intersection_pos,
231 };
232 } else if dist_to_end < snap_sq {
233 let nid = edge.end;
234 if nid != current_node_id {
235 return TraceResult::SnappedToNode(nid);
236 }
237 return TraceResult::SnappedToEdge {
238 edge_id,
239 intersection_pos,
240 };
241 } else {
242 return TraceResult::SnappedToEdge {
243 edge_id,
244 intersection_pos,
245 };
246 }
247 }
248
249 let mut closest_node: Option<NodeId> = None;
251 let mut closest_node_dist = f32::MAX;
252
253 for cell in &cells {
254 for &n_id in &cell.nodes {
255 if n_id == current_node_id || current_neighbours.contains(&n_id) {
256 continue;
257 }
258 let dist_sq = graph.nodes[n_id as usize]
259 .position
260 .distance_squared(proposed_pos);
261 if dist_sq < snap_sq && dist_sq < closest_node_dist {
262 closest_node_dist = dist_sq;
263 closest_node = Some(n_id);
264 }
265 }
266 }
267
268 if let Some(n_id) = closest_node {
269 return TraceResult::SnappedToNode(n_id);
270 }
271
272 let mut closest_edge_hit: Option<(EdgeId, Vec2)> = None;
274 let mut closest_edge_dist = f32::MAX;
275
276 for cell in &cells {
277 for &e_id in &cell.edges {
278 let edge = &graph.edges[e_id as usize];
279 if !edge.active {
280 continue;
281 }
282 if edge.start == current_node_id || edge.end == current_node_id {
283 continue;
284 }
285 if current_neighbours.contains(&edge.start) || current_neighbours.contains(&edge.end) {
286 continue;
287 }
288
289 let e_start = graph.nodes[edge.start as usize].position;
290 let e_end = graph.nodes[edge.end as usize].position;
291
292 let proj = closest_point_on_segment(proposed_pos, e_start, e_end);
293 let dist_to_edge_sq = proposed_pos.distance_squared(proj);
294
295 if dist_to_edge_sq < snap_sq && dist_to_edge_sq < closest_edge_dist {
296 closest_edge_dist = dist_to_edge_sq;
297 closest_edge_hit = Some((e_id, proj));
298 }
299 }
300 }
301
302 if let Some((edge_id, intersection_pos)) = closest_edge_hit {
303 let edge = &graph.edges[edge_id as usize];
304 let e_start = graph.nodes[edge.start as usize].position;
305 let e_end = graph.nodes[edge.end as usize].position;
306
307 let dist_to_start = intersection_pos.distance_squared(e_start);
308 let dist_to_end = intersection_pos.distance_squared(e_end);
309
310 if dist_to_start < snap_sq && dist_to_start <= dist_to_end {
311 let nid = edge.start;
312 if nid != current_node_id {
313 return TraceResult::SnappedToNode(nid);
314 }
315 return TraceResult::SnappedToEdge {
316 edge_id,
317 intersection_pos,
318 };
319 } else if dist_to_end < snap_sq {
320 let nid = edge.end;
321 if nid != current_node_id {
322 return TraceResult::SnappedToNode(nid);
323 }
324 return TraceResult::SnappedToEdge {
325 edge_id,
326 intersection_pos,
327 };
328 } else {
329 return TraceResult::SnappedToEdge {
330 edge_id,
331 intersection_pos,
332 };
333 }
334 }
335
336 TraceResult::Clear(proposed_pos)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::graph::RoadType;
343
344 #[test]
345 fn insert_and_find_node() {
346 let mut graph = RoadGraph::default();
347 let origin = graph.add_node(Vec2::new(0.0, 0.0)); let target = graph.add_node(Vec2::new(5.0, 5.0));
349
350 let mut sh = SpatialHash::new(100.0, 100.0, 10.0);
351 sh.insert_node(origin, Vec2::new(0.0, 0.0));
352 sh.insert_node(target, Vec2::new(5.0, 5.0));
353
354 let result = resolve_trace_step(
355 &graph,
356 &sh,
357 Vec2::new(0.0, 0.0),
358 Vec2::new(5.5, 5.5),
359 2.0,
360 origin,
361 );
362 assert!(matches!(result, TraceResult::SnappedToNode(1)));
363 }
364
365 #[test]
366 fn crossing_edge_detected() {
367 let mut graph = RoadGraph::default();
368 let origin = graph.add_node(Vec2::new(5.0, 0.0)); let a = graph.add_node(Vec2::new(0.0, 5.0));
370 let b = graph.add_node(Vec2::new(10.0, 5.0));
371 let e = graph.add_edge(a, b, RoadType::Major);
372
373 let mut sh = SpatialHash::new(100.0, 100.0, 10.0);
374 sh.insert_node(origin, graph.node_pos(origin));
375 sh.insert_node(a, graph.node_pos(a));
376 sh.insert_node(b, graph.node_pos(b));
377 sh.insert_edge(e, graph.node_pos(a), graph.node_pos(b));
378
379 let result = resolve_trace_step(
380 &graph,
381 &sh,
382 Vec2::new(5.0, 0.0),
383 Vec2::new(5.0, 10.0),
384 1.0,
385 origin,
386 );
387 match result {
388 TraceResult::SnappedToEdge {
389 edge_id,
390 intersection_pos,
391 } => {
392 assert_eq!(edge_id, e);
393 assert!((intersection_pos - Vec2::new(5.0, 5.0)).length() < 1e-4);
394 }
395 _ => panic!("expected SnappedToEdge"),
396 }
397 }
398
399 #[test]
400 fn clear_when_no_obstacles() {
401 let mut graph = RoadGraph::default();
402 let origin = graph.add_node(Vec2::new(40.0, 40.0));
403 let sh = SpatialHash::new(100.0, 100.0, 10.0);
404
405 let target = Vec2::new(50.0, 50.0);
406 let result = resolve_trace_step(&graph, &sh, Vec2::new(40.0, 40.0), target, 1.0, origin);
407 match result {
408 TraceResult::Clear(pos) => assert!((pos - target).length() < 1e-5),
409 _ => panic!("expected Clear"),
410 }
411 }
412}