1use robust::{Coord, orient2d};
4use rstar::{
5 AABB, RTree,
6 primitives::{GeomWithData, Line, Rectangle},
7};
8use wowlab_engine_ports::{
9 ConeQuery, RadiusQuery, SpatialActor, SpatialEnvelope, SpatialQuery, SpatialQueryError,
10};
11use wowlab_types::sim::{
12 EnemyIdx, GEOMETRY_EPSILON, Position2, SpatialLayerId, SpatialTransform, StaticObstacle,
13 StaticSpatialScene,
14};
15
16use crate::{SpatialQueryDiagnostics, SpatialQueryObservation};
17
18const POINT_DIMENSIONS: usize = 2;
19const EDGE_POINT_COUNT: usize = 2;
20const QUADRANT_COUNT: i32 = 4;
21
22type ActorEntry = GeomWithData<[f64; POINT_DIMENSIONS], EnemyIdx>;
23type SegmentEntry = GeomWithData<Line<[f64; POINT_DIMENSIONS]>, usize>;
24type PolygonEntry = GeomWithData<Rectangle<[f64; POINT_DIMENSIONS]>, usize>;
25
26#[derive(Clone, Debug)]
27struct PolygonBlocker {
28 exterior: Vec<Position2>,
29 holes: Vec<Box<[Position2]>>,
30}
31
32impl PolygonBlocker {
33 fn from_rings(exterior: &[Position2], holes: &[Vec<Position2>]) -> Self {
34 Self {
35 exterior: exterior.to_vec(),
36 holes: holes
37 .iter()
38 .map(|ring| ring.clone().into_boxed_slice())
39 .collect(),
40 }
41 }
42}
43
44#[derive(Debug)]
45struct LayerIndex {
46 actors: RTree<ActorEntry>,
47 obstacles: RTree<SegmentEntry>,
48 segments: Vec<(Position2, Position2)>,
49 polygon_envelopes: RTree<PolygonEntry>,
50 polygons: Vec<PolygonBlocker>,
51}
52
53impl LayerIndex {
54 fn new() -> Self {
55 Self {
56 actors: RTree::new(),
57 obstacles: RTree::new(),
58 segments: Vec::new(),
59 polygon_envelopes: RTree::new(),
60 polygons: Vec::new(),
61 }
62 }
63}
64
65#[derive(Debug)]
66pub(crate) struct RstarSpatialQuery {
67 layers: Vec<LayerIndex>,
68 indexed_transforms: Vec<Option<SpatialTransform>>,
69}
70
71impl RstarSpatialQuery {
72 pub(crate) fn build(
73 scene: &StaticSpatialScene,
74 actors: &[SpatialActor],
75 ) -> Result<Self, SpatialQueryError> {
76 let mut layers: Vec<_> = scene.layers.iter().map(|_| LayerIndex::new()).collect();
77
78 for obstacle in &scene.obstacles {
79 match obstacle {
80 StaticObstacle::Segment { layer, start, end } => {
81 push_segment(layer_index_mut(&mut layers, *layer)?, *start, *end);
82 }
83 StaticObstacle::Polygon {
84 layer,
85 exterior,
86 holes,
87 } => {
88 let index = layer_index_mut(&mut layers, *layer)?;
89
90 push_ring_segments(index, exterior);
91
92 for hole in holes {
93 push_ring_segments(index, hole);
94 }
95
96 index
97 .polygons
98 .push(PolygonBlocker::from_rings(exterior, holes));
99 }
100 }
101 }
102
103 for layer in &mut layers {
104 let entries = layer
105 .segments
106 .iter()
107 .enumerate()
108 .map(|(id, segment)| {
109 SegmentEntry::new(Line::new(point(segment.0), point(segment.1)), id)
110 })
111 .collect();
112
113 layer.obstacles = RTree::bulk_load(entries);
114 let polygon_entries = layer
115 .polygons
116 .iter()
117 .enumerate()
118 .filter_map(|(id, polygon)| {
119 polygon_envelope(polygon).map(|envelope| PolygonEntry::new(envelope, id))
120 })
121 .collect();
122
123 layer.polygon_envelopes = RTree::bulk_load(polygon_entries);
124 }
125
126 let capacity = actors
127 .iter()
128 .map(|actor| actor.id.as_usize() + 1)
129 .max()
130 .unwrap_or(0);
131 let mut query = Self {
132 layers,
133 indexed_transforms: vec![None; capacity],
134 };
135 let mut actor_entries: Vec<Vec<ActorEntry>> =
136 query.layers.iter().map(|_| Vec::new()).collect();
137
138 for actor in actors {
139 query.validate_transform(actor.transform)?;
140 let indexed_transform = query
141 .indexed_transforms
142 .get_mut(actor.id.as_usize())
143 .ok_or_else(|| SpatialQueryError::missing_actor(actor.id))?;
144
145 if indexed_transform.is_some() {
146 return Err(SpatialQueryError::duplicate_actor(actor.id));
147 }
148
149 *indexed_transform = Some(actor.transform);
150 actor_entries
151 .get_mut(usize::from(actor.transform.layer.0))
152 .ok_or_else(|| SpatialQueryError::missing_layer(actor.transform.layer))?
153 .push(ActorEntry::new(point(actor.transform.position), actor.id));
154 }
155
156 for (layer, entries) in query.layers.iter_mut().zip(actor_entries) {
157 layer.actors = RTree::bulk_load(entries);
158 }
159
160 Ok(query)
161 }
162
163 fn layer(&self, layer: SpatialLayerId) -> Option<&LayerIndex> {
164 self.layers.get(usize::from(layer.0))
165 }
166
167 fn layer_mut(&mut self, layer: SpatialLayerId) -> Option<&mut LayerIndex> {
168 self.layers.get_mut(usize::from(layer.0))
169 }
170
171 #[cfg(test)]
172 fn diagnose_transform_validation(
173 &self,
174 transform: SpatialTransform,
175 ) -> TransformValidationObservation {
176 if !transform.position.x.is_finite()
177 || !transform.position.y.is_finite()
178 || !transform.heading.is_finite()
179 {
180 return TransformValidationObservation::error(SpatialQueryError::non_finite_transform());
181 }
182
183 let Some(index) = self.layer(transform.layer) else {
184 return TransformValidationObservation::error(SpatialQueryError::missing_layer(
185 transform.layer,
186 ));
187 };
188
189 let segment_candidates: Vec<_> = transform_segment_candidates(index, transform.position)
190 .map(|entry| entry.data)
191 .collect();
192 let polygon_candidates: Vec<_> = transform_polygon_candidates(index, transform.position)
193 .map(|entry| entry.data)
194 .collect();
195 let mut segment_blocked = false;
196 let mut segment_exact_checks = 0;
197
198 for id in &segment_candidates {
199 segment_exact_checks += 1;
200 segment_blocked |= index.segments.get(*id).is_some_and(|segment| {
201 point_segment_distance_sq(transform.position, *segment)
202 <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
203 });
204 }
205
206 let mut polygon_blocked = false;
207 let mut polygon_exact_checks = 0;
208
209 for id in &polygon_candidates {
210 polygon_exact_checks += 1;
211 polygon_blocked |= index
212 .polygons
213 .get(*id)
214 .is_some_and(|polygon| point_in_blocker(transform.position, polygon));
215 }
216
217 TransformValidationObservation {
218 result: if segment_blocked || polygon_blocked {
219 Err(SpatialQueryError::blocked_transform())
220 } else {
221 Ok(())
222 },
223 segment_candidates: segment_candidates.len(),
224 polygon_candidates: polygon_candidates.len(),
225 segment_exact_checks,
226 polygon_exact_checks,
227 }
228 }
229}
230
231#[derive(Debug, Eq, PartialEq)]
232#[cfg(test)]
233struct TransformValidationObservation {
234 result: Result<(), SpatialQueryError>,
235 segment_candidates: usize,
236 polygon_candidates: usize,
237 segment_exact_checks: usize,
238 polygon_exact_checks: usize,
239}
240
241#[cfg(test)]
242impl TransformValidationObservation {
243 const fn error(error: SpatialQueryError) -> Self {
244 Self {
245 result: Err(error),
246 segment_candidates: 0,
247 polygon_candidates: 0,
248 segment_exact_checks: 0,
249 polygon_exact_checks: 0,
250 }
251 }
252}
253
254impl SpatialQueryDiagnostics for RstarSpatialQuery {
255 fn diagnose_actors_in_radius(
256 &self,
257 query: RadiusQuery,
258 allowed: Option<&[EnemyIdx]>,
259 ) -> SpatialQueryObservation<Vec<EnemyIdx>> {
260 let RadiusQuery {
261 layer,
262 center,
263 radius,
264 } = query;
265 let envelope = SpatialEnvelope {
266 min: Position2 {
267 x: center.x - radius - GEOMETRY_EPSILON,
268 y: center.y - radius - GEOMETRY_EPSILON,
269 },
270 max: Position2 {
271 x: center.x + radius + GEOMETRY_EPSILON,
272 y: center.y + radius + GEOMETRY_EPSILON,
273 },
274 };
275 let mut candidates = vec![EnemyIdx::PRIMARY; self.indexed_transforms.len()];
276 let broad_phase_candidates =
277 self.candidates_in_envelope_into(layer, envelope, &mut candidates);
278 let result = candidates
279 .into_iter()
280 .take(broad_phase_candidates)
281 .filter(|enemy| actor_is_allowed(*enemy, allowed))
282 .filter(|enemy| {
283 self.indexed_transforms
284 .get(enemy.as_usize())
285 .and_then(|transform| *transform)
286 .is_some_and(|transform| {
287 transform.layer == layer
288 && center.distance(transform.position) <= radius + GEOMETRY_EPSILON
289 })
290 })
291 .collect();
292
293 SpatialQueryObservation {
294 result,
295 broad_phase_candidates,
296 }
297 }
298
299 fn diagnose_actors_in_cone(
300 &self,
301 query: ConeQuery,
302 allowed: Option<&[EnemyIdx]>,
303 ) -> SpatialQueryObservation<Vec<EnemyIdx>> {
304 let ConeQuery {
305 layer,
306 apex,
307 heading,
308 half_angle,
309 range,
310 } = query;
311 let mut candidates = vec![EnemyIdx::PRIMARY; self.indexed_transforms.len()];
312 let broad_phase_candidates = self.candidates_in_envelope_into(
313 layer,
314 cone_envelope(apex, heading, half_angle, range),
315 &mut candidates,
316 );
317 let result = candidates
318 .into_iter()
319 .take(broad_phase_candidates)
320 .filter(|enemy| actor_is_allowed(*enemy, allowed))
321 .filter(|enemy| {
322 self.indexed_transforms
323 .get(enemy.as_usize())
324 .and_then(|transform| *transform)
325 .is_some_and(|transform| {
326 transform.layer == layer
327 && in_cone(apex, heading, half_angle, range, transform.position)
328 })
329 })
330 .collect();
331
332 SpatialQueryObservation {
333 result,
334 broad_phase_candidates,
335 }
336 }
337
338 fn diagnose_segment_contacts(
339 &self,
340 layer: SpatialLayerId,
341 start: Position2,
342 end: Position2,
343 ) -> SpatialQueryObservation<Vec<usize>> {
344 let Some(index) = self.layer(layer) else {
345 return SpatialQueryObservation {
346 result: Vec::new(),
347 broad_phase_candidates: 0,
348 };
349 };
350 let envelope = AABB::from_corners(
351 [
352 start.x.min(end.x) - GEOMETRY_EPSILON,
353 start.y.min(end.y) - GEOMETRY_EPSILON,
354 ],
355 [
356 start.x.max(end.x) + GEOMETRY_EPSILON,
357 start.y.max(end.y) + GEOMETRY_EPSILON,
358 ],
359 );
360 let mut candidates: Vec<_> = index
361 .obstacles
362 .locate_in_envelope_intersecting(envelope)
363 .map(|entry| entry.data)
364 .collect();
365
366 candidates.sort_unstable();
367 candidates.dedup();
368 let broad_phase_candidates = candidates.len();
369
370 candidates.retain(|id| {
371 index
372 .segments
373 .get(*id)
374 .is_some_and(|obstacle| los_contact((start, end), *obstacle))
375 });
376
377 SpatialQueryObservation {
378 result: candidates,
379 broad_phase_candidates,
380 }
381 }
382}
383
384impl SpatialQuery for RstarSpatialQuery {
385 fn actor_capacity(&self) -> usize {
386 self.indexed_transforms.len()
387 }
388
389 fn candidates_on_layer_into(&self, layer: SpatialLayerId, output: &mut [EnemyIdx]) -> usize {
390 let Some(index) = self.layer(layer) else {
391 return 0;
392 };
393
394 write_sorted_unique(index.actors.iter().map(|entry| entry.data), output)
395 }
396
397 fn candidates_in_envelope_into(
398 &self,
399 layer: SpatialLayerId,
400 envelope: SpatialEnvelope,
401 output: &mut [EnemyIdx],
402 ) -> usize {
403 let Some(index) = self.layer(layer) else {
404 return 0;
405 };
406
407 write_sorted_unique(
408 index
409 .actors
410 .locate_in_envelope_intersecting(AABB::from_corners(
411 point(envelope.min),
412 point(envelope.max),
413 ))
414 .map(|entry| entry.data),
415 output,
416 )
417 }
418
419 fn actors_in_radius_into(
420 &self,
421 query: RadiusQuery,
422 allowed: Option<&[EnemyIdx]>,
423 output: &mut [EnemyIdx],
424 ) -> usize {
425 let RadiusQuery {
426 layer,
427 center,
428 radius,
429 } = query;
430 let envelope = SpatialEnvelope {
431 min: Position2 {
432 x: center.x - radius - GEOMETRY_EPSILON,
433 y: center.y - radius - GEOMETRY_EPSILON,
434 },
435 max: Position2 {
436 x: center.x + radius + GEOMETRY_EPSILON,
437 y: center.y + radius + GEOMETRY_EPSILON,
438 },
439 };
440 let count = self.candidates_in_envelope_into(layer, envelope, output);
441
442 retain_prefix(output, count, |enemy| {
443 actor_is_allowed(enemy, allowed)
444 && self
445 .indexed_transforms
446 .get(enemy.as_usize())
447 .and_then(|transform| *transform)
448 .is_some_and(|transform| {
449 transform.layer == layer
450 && center.distance(transform.position) <= radius + GEOMETRY_EPSILON
451 })
452 })
453 }
454
455 fn actors_in_cone_into(
456 &self,
457 query: ConeQuery,
458 allowed: Option<&[EnemyIdx]>,
459 output: &mut [EnemyIdx],
460 ) -> usize {
461 let ConeQuery {
462 layer,
463 apex,
464 heading,
465 half_angle,
466 range,
467 } = query;
468 let count = self.candidates_in_envelope_into(
469 layer,
470 cone_envelope(apex, heading, half_angle, range),
471 output,
472 );
473
474 retain_prefix(output, count, |enemy| {
475 actor_is_allowed(enemy, allowed)
476 && self
477 .indexed_transforms
478 .get(enemy.as_usize())
479 .and_then(|transform| *transform)
480 .is_some_and(|transform| {
481 transform.layer == layer
482 && in_cone(apex, heading, half_angle, range, transform.position)
483 })
484 })
485 }
486
487 fn actors_by_distance_into(
488 &self,
489 layer: SpatialLayerId,
490 origin: Position2,
491 allowed: Option<&[EnemyIdx]>,
492 output: &mut [EnemyIdx],
493 ) -> usize {
494 let Some(index) = self.layer(layer) else {
495 return 0;
496 };
497 let count = if let Some(allowed) = allowed {
498 write_entries(
499 allowed.iter().copied().filter(|actor| {
500 self.indexed_transforms
501 .get(actor.as_usize())
502 .and_then(|transform| *transform)
503 .is_some_and(|transform| transform.layer == layer)
504 }),
505 output,
506 )
507 } else {
508 write_entries(index.actors.iter().map(|entry| entry.data), output)
509 };
510
511 output[..count].sort_unstable_by(|left, right| {
514 let distance = |actor: EnemyIdx| {
515 self.indexed_transforms
516 .get(actor.as_usize())
517 .and_then(|transform| *transform)
518 .map_or(f64::INFINITY, |transform| {
519 let dx = transform.position.x - origin.x;
520 let dy = transform.position.y - origin.y;
521
522 dx.mul_add(dx, dy * dy)
523 })
524 };
525
526 distance(*left)
527 .total_cmp(&distance(*right))
528 .then_with(|| left.cmp(right))
529 });
530
531 count
532 }
533
534 fn segment_visible(&self, layer: SpatialLayerId, start: Position2, end: Position2) -> bool {
535 let Some(index) = self.layer(layer) else {
536 return false;
537 };
538 let envelope = AABB::from_corners(
539 [
540 start.x.min(end.x) - GEOMETRY_EPSILON,
541 start.y.min(end.y) - GEOMETRY_EPSILON,
542 ],
543 [
544 start.x.max(end.x) + GEOMETRY_EPSILON,
545 start.y.max(end.y) + GEOMETRY_EPSILON,
546 ],
547 );
548
549 !index
550 .obstacles
551 .locate_in_envelope_intersecting(envelope)
552 .any(|entry| {
553 index
554 .segments
555 .get(entry.data)
556 .is_some_and(|obstacle| los_contact((start, end), *obstacle))
557 })
558 }
559
560 fn validate_transform(&self, transform: SpatialTransform) -> Result<(), SpatialQueryError> {
561 if !transform.position.x.is_finite()
562 || !transform.position.y.is_finite()
563 || !transform.heading.is_finite()
564 {
565 return Err(SpatialQueryError::non_finite_transform());
566 }
567
568 let index = self
569 .layer(transform.layer)
570 .ok_or_else(|| SpatialQueryError::missing_layer(transform.layer))?;
571
572 if transform_segment_candidates(index, transform.position).any(|entry| {
573 index.segments.get(entry.data).is_some_and(|segment| {
574 point_segment_distance_sq(transform.position, *segment)
575 <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
576 })
577 }) || transform_polygon_candidates(index, transform.position).any(|entry| {
578 index
579 .polygons
580 .get(entry.data)
581 .is_some_and(|polygon| point_in_blocker(transform.position, polygon))
582 }) {
583 return Err(SpatialQueryError::blocked_transform());
584 }
585
586 Ok(())
587 }
588
589 fn insert_actor(&mut self, actor: SpatialActor) -> Result<(), SpatialQueryError> {
590 self.validate_transform(actor.transform)?;
591
592 if self
593 .indexed_transforms
594 .get(actor.id.as_usize())
595 .is_some_and(Option::is_some)
596 {
597 return Err(SpatialQueryError::duplicate_actor(actor.id));
598 }
599
600 if actor.id.as_usize() >= self.indexed_transforms.len() {
601 self.indexed_transforms
602 .resize(actor.id.as_usize() + 1, None);
603 }
604
605 self.layer_mut(actor.transform.layer)
606 .ok_or_else(|| SpatialQueryError::missing_layer(actor.transform.layer))?
607 .actors
608 .insert(ActorEntry::new(point(actor.transform.position), actor.id));
609 let Some(indexed_transform) = self.indexed_transforms.get_mut(actor.id.as_usize()) else {
610 return Err(SpatialQueryError::missing_actor(actor.id));
611 };
612
613 *indexed_transform = Some(actor.transform);
614
615 Ok(())
616 }
617
618 fn remove_actor(&mut self, actor: SpatialActor) -> Result<(), SpatialQueryError> {
619 let indexed = self
620 .indexed_transforms
621 .get(actor.id.as_usize())
622 .and_then(|transform| *transform)
623 .ok_or_else(|| SpatialQueryError::missing_actor(actor.id))?;
624
625 if indexed != actor.transform {
626 return Err(SpatialQueryError::transform_mismatch(actor.id));
627 }
628
629 let removed = self
630 .layer_mut(indexed.layer)
631 .ok_or_else(|| SpatialQueryError::missing_layer(indexed.layer))?
632 .actors
633 .remove(&ActorEntry::new(point(indexed.position), actor.id));
634
635 if removed.is_none() {
636 return Err(SpatialQueryError::missing_actor(actor.id));
637 }
638
639 let Some(indexed_transform) = self.indexed_transforms.get_mut(actor.id.as_usize()) else {
640 return Err(SpatialQueryError::missing_actor(actor.id));
641 };
642
643 *indexed_transform = None;
644
645 Ok(())
646 }
647
648 fn relocate_actor(
649 &mut self,
650 actor: EnemyIdx,
651 from: SpatialTransform,
652 to: SpatialTransform,
653 ) -> Result<(), SpatialQueryError> {
654 self.validate_transform(to)?;
655 let indexed = self
656 .indexed_transforms
657 .get(actor.as_usize())
658 .and_then(|transform| *transform)
659 .ok_or_else(|| SpatialQueryError::missing_actor(actor))?;
660
661 if indexed != from {
662 return Err(SpatialQueryError::transform_mismatch(actor));
663 }
664
665 let from_entry = ActorEntry::new(point(from.position), actor);
666
667 if self
668 .layer_mut(from.layer)
669 .ok_or_else(|| SpatialQueryError::missing_layer(from.layer))?
670 .actors
671 .remove(&from_entry)
672 .is_none()
673 {
674 return Err(SpatialQueryError::missing_actor(actor));
675 }
676
677 self.layer_mut(to.layer)
678 .ok_or_else(|| SpatialQueryError::missing_layer(to.layer))?
679 .actors
680 .insert(ActorEntry::new(point(to.position), actor));
681 let Some(indexed_transform) = self.indexed_transforms.get_mut(actor.as_usize()) else {
682 return Err(SpatialQueryError::missing_actor(actor));
683 };
684
685 *indexed_transform = Some(to);
686
687 Ok(())
688 }
689}
690
691fn actor_is_allowed(actor: EnemyIdx, allowed: Option<&[EnemyIdx]>) -> bool {
692 allowed.is_none_or(|ids| ids.binary_search(&actor).is_ok())
693}
694
695fn write_entries(entries: impl IntoIterator<Item = EnemyIdx>, output: &mut [EnemyIdx]) -> usize {
696 let mut count = 0;
697
698 for entry in entries {
699 assert!(
700 count < output.len(),
701 "spatial output capacity {} is smaller than the finalized actor set",
702 output.len()
703 );
704 output[count] = entry;
706 count += 1;
707 }
708
709 count
710}
711
712fn write_sorted_unique(
713 entries: impl IntoIterator<Item = EnemyIdx>,
714 output: &mut [EnemyIdx],
715) -> usize {
716 let count = write_entries(entries, output);
717
718 output[..count].sort_unstable();
721
722 let mut unique = 0;
723
724 for index in 0..count {
725 let entry = output[index];
727
728 if unique == 0 || output[unique - 1] != entry {
731 output[unique] = entry;
733 unique += 1;
734 }
735 }
736
737 unique
738}
739
740fn retain_prefix(
741 output: &mut [EnemyIdx],
742 count: usize,
743 mut keep: impl FnMut(EnemyIdx) -> bool,
744) -> usize {
745 let mut retained = 0;
746
747 for index in 0..count {
748 let entry = output[index];
750
751 if keep(entry) {
752 output[retained] = entry;
754 retained += 1;
755 }
756 }
757
758 retained
759}
760
761fn layer_index_mut(
762 layers: &mut [LayerIndex],
763 layer: SpatialLayerId,
764) -> Result<&mut LayerIndex, SpatialQueryError> {
765 layers
766 .get_mut(usize::from(layer.0))
767 .ok_or_else(|| SpatialQueryError::missing_layer(layer))
768}
769
770fn push_segment(index: &mut LayerIndex, start: Position2, end: Position2) {
771 index.segments.push((start, end));
772}
773
774fn push_ring_segments(index: &mut LayerIndex, ring: &[Position2]) {
775 for edge in ring.windows(EDGE_POINT_COUNT) {
776 if let [start, end] = edge {
777 push_segment(index, *start, *end);
778 }
779 }
780}
781
782fn point_epsilon_envelope(position: Position2) -> AABB<[f64; POINT_DIMENSIONS]> {
783 AABB::from_corners(
784 [position.x - GEOMETRY_EPSILON, position.y - GEOMETRY_EPSILON],
785 [position.x + GEOMETRY_EPSILON, position.y + GEOMETRY_EPSILON],
786 )
787}
788
789fn transform_segment_candidates(
790 index: &LayerIndex,
791 position: Position2,
792) -> impl Iterator<Item = &SegmentEntry> {
793 index
794 .obstacles
795 .locate_in_envelope_intersecting(point_epsilon_envelope(position))
796}
797
798fn transform_polygon_candidates(
799 index: &LayerIndex,
800 position: Position2,
801) -> impl Iterator<Item = &PolygonEntry> {
802 index
803 .polygon_envelopes
804 .locate_in_envelope_intersecting(point_epsilon_envelope(position))
805}
806
807fn polygon_envelope(polygon: &PolygonBlocker) -> Option<Rectangle<[f64; POINT_DIMENSIONS]>> {
808 let first = polygon.exterior.first()?;
809 let mut min = *first;
810 let mut max = *first;
811
812 for position in polygon.exterior.iter().skip(1) {
813 min.x = min.x.min(position.x);
814 min.y = min.y.min(position.y);
815 max.x = max.x.max(position.x);
816 max.y = max.y.max(position.y);
817 }
818
819 Some(Rectangle::from_corners(point(min), point(max)))
820}
821
822const fn point(position: Position2) -> [f64; POINT_DIMENSIONS] {
823 [position.x, position.y]
824}
825
826const fn coord(position: Position2) -> Coord<f64> {
827 Coord {
828 x: position.x,
829 y: position.y,
830 }
831}
832
833fn exact_intersection(a: (Position2, Position2), b: (Position2, Position2)) -> bool {
834 let o1 = orient2d(coord(a.0), coord(a.1), coord(b.0));
835 let o2 = orient2d(coord(a.0), coord(a.1), coord(b.1));
836 let o3 = orient2d(coord(b.0), coord(b.1), coord(a.0));
837 let o4 = orient2d(coord(b.0), coord(b.1), coord(a.1));
838
839 if opposite_signs(o1, o2) && opposite_signs(o3, o4) {
840 return true;
841 }
842
843 exact_zero(o1) && on_closed_segment(b.0, a.0, a.1)
844 || exact_zero(o2) && on_closed_segment(b.1, a.0, a.1)
845 || exact_zero(o3) && on_closed_segment(a.0, b.0, b.1)
846 || exact_zero(o4) && on_closed_segment(a.1, b.0, b.1)
847}
848
849const fn opposite_signs(left: f64, right: f64) -> bool {
850 (left > 0.0 && right < 0.0) || (left < 0.0 && right > 0.0)
851}
852
853fn exact_zero(value: f64) -> bool {
854 value.total_cmp(&0.0).is_eq() || value.total_cmp(&-0.0).is_eq()
855}
856
857fn on_closed_segment(point: Position2, start: Position2, end: Position2) -> bool {
858 point.x >= start.x.min(end.x)
859 && point.x <= start.x.max(end.x)
860 && point.y >= start.y.min(end.y)
861 && point.y <= start.y.max(end.y)
862}
863
864fn point_segment_distance_sq(point: Position2, segment: (Position2, Position2)) -> f64 {
865 let dx = segment.1.x - segment.0.x;
866 let dy = segment.1.y - segment.0.y;
867 let length_sq = dx.mul_add(dx, dy * dy);
868
869 if exact_zero(length_sq) {
870 let ex = point.x - segment.0.x;
871 let ey = point.y - segment.0.y;
872
873 return ex.mul_add(ex, ey * ey);
874 }
875
876 let t =
877 (((point.x - segment.0.x) * dx + (point.y - segment.0.y) * dy) / length_sq).clamp(0.0, 1.0);
878 let qx = segment.0.x + t * dx;
879 let qy = segment.0.y + t * dy;
880 let ex = point.x - qx;
881 let ey = point.y - qy;
882
883 ex.mul_add(ex, ey * ey)
884}
885
886fn los_contact(a: (Position2, Position2), b: (Position2, Position2)) -> bool {
887 exact_intersection(a, b)
888 || point_segment_distance_sq(a.0, b) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
889 || point_segment_distance_sq(a.1, b) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
890 || point_segment_distance_sq(b.0, a) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
891 || point_segment_distance_sq(b.1, a) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
892}
893
894fn angle_diff(left: f64, right: f64) -> f64 {
895 let mut delta = (left - right) % std::f64::consts::TAU;
896
897 if delta > std::f64::consts::PI {
898 delta -= std::f64::consts::TAU;
899 }
900
901 if delta < -std::f64::consts::PI {
902 delta += std::f64::consts::TAU;
903 }
904
905 delta.abs()
906}
907
908fn in_cone(apex: Position2, heading: f64, half_angle: f64, range: f64, point: Position2) -> bool {
909 let distance = apex.distance(point);
910
911 if distance > range + GEOMETRY_EPSILON {
912 return false;
913 }
914
915 if distance <= GEOMETRY_EPSILON {
916 return true;
917 }
918
919 let angle = (point.y - apex.y).atan2(point.x - apex.x);
920
921 angle_diff(angle, heading) <= half_angle + GEOMETRY_EPSILON
922}
923
924fn cone_envelope(apex: Position2, heading: f64, half_angle: f64, range: f64) -> SpatialEnvelope {
925 if half_angle >= std::f64::consts::PI - GEOMETRY_EPSILON {
926 return SpatialEnvelope {
927 min: Position2 {
928 x: apex.x - range - GEOMETRY_EPSILON,
929 y: apex.y - range - GEOMETRY_EPSILON,
930 },
931 max: Position2 {
932 x: apex.x + range + GEOMETRY_EPSILON,
933 y: apex.y + range + GEOMETRY_EPSILON,
934 },
935 };
936 }
937
938 let mut min = apex;
939 let mut max = apex;
940 let mut include = |angle: f64| {
941 let position = Position2 {
942 x: apex.x + range * angle.cos(),
943 y: apex.y + range * angle.sin(),
944 };
945
946 min.x = min.x.min(position.x);
947 min.y = min.y.min(position.y);
948 max.x = max.x.max(position.x);
949 max.y = max.y.max(position.y);
950 };
951
952 include(heading - half_angle);
953 include(heading + half_angle);
954
955 for quadrant in 0..QUADRANT_COUNT {
956 let angle = f64::from(quadrant) * std::f64::consts::FRAC_PI_2;
957
958 if angle_diff(angle, heading) <= half_angle + GEOMETRY_EPSILON {
959 include(angle);
960 }
961 }
962
963 SpatialEnvelope {
964 min: Position2 {
965 x: min.x - GEOMETRY_EPSILON,
966 y: min.y - GEOMETRY_EPSILON,
967 },
968 max: Position2 {
969 x: max.x + GEOMETRY_EPSILON,
970 y: max.y + GEOMETRY_EPSILON,
971 },
972 }
973}
974
975fn point_in_blocker(point: Position2, polygon: &PolygonBlocker) -> bool {
976 ring_contains(point, &polygon.exterior)
977 && !polygon
978 .holes
979 .iter()
980 .any(|hole| ring_contains_strict(point, hole))
981}
982
983fn ring_contains(point: Position2, ring: &[Position2]) -> bool {
984 ring.windows(EDGE_POINT_COUNT).any(|edge| {
985 let [start, end] = edge else {
986 return false;
987 };
988
989 point_segment_distance_sq(point, (*start, *end)) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
990 }) || ring_contains_strict(point, ring)
991}
992
993fn ring_contains_strict(point: Position2, ring: &[Position2]) -> bool {
994 let mut inside = false;
995
996 for edge in ring.windows(EDGE_POINT_COUNT) {
997 let [a, b] = edge else {
998 continue;
999 };
1000
1001 if (a.y > point.y) != (b.y > point.y) {
1002 let crossing_x = (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x;
1003
1004 if point.x < crossing_x {
1005 inside = !inside;
1006 }
1007 }
1008 }
1009
1010 inside
1011}
1012
1013#[cfg(test)]
1014#[path = "../benches/support/mod.rs"]
1015mod frozen_oracle;
1016
1017#[cfg(test)]
1018#[path = "rstar/tests.rs"]
1019mod tests;