Skip to main content

wowlab_engine_spatial/
rstar.rs

1//! Selected rstar + robust implementation of the spatial-query port.
2
3use 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        // BOUNDS: write_entries returns an initialized prefix bounded by output length.
512
513        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        // BOUNDS: the preceding caller-storage assertion validates this output slot.
705        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    // BOUNDS: write_entries returns an initialized prefix bounded by output length.
719
720    output[..count].sort_unstable();
721
722    let mut unique = 0;
723
724    for index in 0..count {
725        // BOUNDS: count is the initialized prefix returned by write_entries.
726        let entry = output[index];
727
728        // BOUNDS: unique is at most index, and the zero guard avoids underflow.
729
730        if unique == 0 || output[unique - 1] != entry {
731            // BOUNDS: unique is at most the current initialized source index.
732            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        // BOUNDS: callers pass an initialized count returned for this same output slice.
749        let entry = output[index];
750
751        if keep(entry) {
752            // BOUNDS: retained cannot exceed the already-validated source index.
753            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;