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 candidates = self.candidates_in_envelope(layer, envelope);
276        let broad_phase_candidates = candidates.len();
277        let result = candidates
278            .into_iter()
279            .filter(|enemy| actor_is_allowed(*enemy, allowed))
280            .filter(|enemy| {
281                self.indexed_transforms
282                    .get(enemy.as_usize())
283                    .and_then(|transform| *transform)
284                    .is_some_and(|transform| {
285                        transform.layer == layer
286                            && center.distance(transform.position) <= radius + GEOMETRY_EPSILON
287                    })
288            })
289            .collect();
290
291        SpatialQueryObservation {
292            result,
293            broad_phase_candidates,
294        }
295    }
296
297    fn diagnose_actors_in_cone(
298        &self,
299        query: ConeQuery,
300        allowed: Option<&[EnemyIdx]>,
301    ) -> SpatialQueryObservation<Vec<EnemyIdx>> {
302        let ConeQuery {
303            layer,
304            apex,
305            heading,
306            half_angle,
307            range,
308        } = query;
309        let candidates =
310            self.candidates_in_envelope(layer, cone_envelope(apex, heading, half_angle, range));
311        let broad_phase_candidates = candidates.len();
312        let result = candidates
313            .into_iter()
314            .filter(|enemy| actor_is_allowed(*enemy, allowed))
315            .filter(|enemy| {
316                self.indexed_transforms
317                    .get(enemy.as_usize())
318                    .and_then(|transform| *transform)
319                    .is_some_and(|transform| {
320                        transform.layer == layer
321                            && in_cone(apex, heading, half_angle, range, transform.position)
322                    })
323            })
324            .collect();
325
326        SpatialQueryObservation {
327            result,
328            broad_phase_candidates,
329        }
330    }
331
332    fn diagnose_segment_contacts(
333        &self,
334        layer: SpatialLayerId,
335        start: Position2,
336        end: Position2,
337    ) -> SpatialQueryObservation<Vec<usize>> {
338        let Some(index) = self.layer(layer) else {
339            return SpatialQueryObservation {
340                result: Vec::new(),
341                broad_phase_candidates: 0,
342            };
343        };
344        let envelope = AABB::from_corners(
345            [
346                start.x.min(end.x) - GEOMETRY_EPSILON,
347                start.y.min(end.y) - GEOMETRY_EPSILON,
348            ],
349            [
350                start.x.max(end.x) + GEOMETRY_EPSILON,
351                start.y.max(end.y) + GEOMETRY_EPSILON,
352            ],
353        );
354        let mut candidates: Vec<_> = index
355            .obstacles
356            .locate_in_envelope_intersecting(envelope)
357            .map(|entry| entry.data)
358            .collect();
359
360        candidates.sort_unstable();
361        candidates.dedup();
362        let broad_phase_candidates = candidates.len();
363
364        candidates.retain(|id| {
365            index
366                .segments
367                .get(*id)
368                .is_some_and(|obstacle| los_contact((start, end), *obstacle))
369        });
370
371        SpatialQueryObservation {
372            result: candidates,
373            broad_phase_candidates,
374        }
375    }
376}
377
378impl SpatialQuery for RstarSpatialQuery {
379    fn candidates_on_layer(&self, layer: SpatialLayerId) -> Vec<EnemyIdx> {
380        let Some(index) = self.layer(layer) else {
381            return Vec::new();
382        };
383        let mut ids: Vec<_> = index.actors.iter().map(|entry| entry.data).collect();
384
385        ids.sort_unstable();
386        ids.dedup();
387
388        ids
389    }
390
391    fn candidates_in_envelope(
392        &self,
393        layer: SpatialLayerId,
394        envelope: SpatialEnvelope,
395    ) -> Vec<EnemyIdx> {
396        let Some(index) = self.layer(layer) else {
397            return Vec::new();
398        };
399        let mut ids: Vec<_> = index
400            .actors
401            .locate_in_envelope_intersecting(AABB::from_corners(
402                point(envelope.min),
403                point(envelope.max),
404            ))
405            .map(|entry| entry.data)
406            .collect();
407
408        ids.sort_unstable();
409        ids.dedup();
410
411        ids
412    }
413
414    fn actors_in_radius(&self, query: RadiusQuery, allowed: Option<&[EnemyIdx]>) -> Vec<EnemyIdx> {
415        self.diagnose_actors_in_radius(query, allowed).result
416    }
417
418    fn actors_in_cone(&self, query: ConeQuery, allowed: Option<&[EnemyIdx]>) -> Vec<EnemyIdx> {
419        self.diagnose_actors_in_cone(query, allowed).result
420    }
421
422    fn actors_by_distance(
423        &self,
424        layer: SpatialLayerId,
425        origin: Position2,
426        allowed: Option<&[EnemyIdx]>,
427    ) -> Vec<EnemyIdx> {
428        let Some(index) = self.layer(layer) else {
429            return Vec::new();
430        };
431        let mut actors: Vec<_> = if let Some(allowed) = allowed {
432            allowed
433                .iter()
434                .filter_map(|actor| {
435                    let transform = self
436                        .indexed_transforms
437                        .get(actor.as_usize())
438                        .and_then(|transform| *transform)
439                        .filter(|transform| transform.layer == layer)?;
440                    let dx = transform.position.x - origin.x;
441                    let dy = transform.position.y - origin.y;
442
443                    Some((*actor, dx.mul_add(dx, dy * dy)))
444                })
445                .collect()
446        } else {
447            index
448                .actors
449                .nearest_neighbor_iter_with_distance_2(point(origin))
450                .map(|(entry, distance)| (entry.data, distance))
451                .collect()
452        };
453
454        actors.sort_unstable_by(|left, right| {
455            left.1
456                .total_cmp(&right.1)
457                .then_with(|| left.0.cmp(&right.0))
458        });
459
460        actors.into_iter().map(|(actor, _)| actor).collect()
461    }
462
463    fn nearest_actor_matching(
464        &self,
465        layer: SpatialLayerId,
466        origin: Position2,
467        is_eligible: &mut dyn FnMut(EnemyIdx) -> bool,
468    ) -> Option<EnemyIdx> {
469        let index = self.layer(layer)?;
470        let mut nearest = index
471            .actors
472            .nearest_neighbor_iter_with_distance_2(point(origin))
473            .peekable();
474        let mut distance_band = Vec::new();
475
476        while let Some((entry, distance)) = nearest.next() {
477            distance_band.clear();
478            distance_band.push(entry.data);
479
480            while nearest
481                .peek()
482                .is_some_and(|(_, next_distance)| distance.total_cmp(next_distance).is_eq())
483            {
484                if let Some((entry, _)) = nearest.next() {
485                    distance_band.push(entry.data);
486                }
487            }
488
489            distance_band.sort_unstable();
490            distance_band.dedup();
491
492            if let Some(actor) = distance_band
493                .iter()
494                .copied()
495                .find(|actor| is_eligible(*actor))
496            {
497                return Some(actor);
498            }
499        }
500
501        None
502    }
503
504    fn segment_visible(&self, layer: SpatialLayerId, start: Position2, end: Position2) -> bool {
505        let Some(index) = self.layer(layer) else {
506            return false;
507        };
508        let envelope = AABB::from_corners(
509            [
510                start.x.min(end.x) - GEOMETRY_EPSILON,
511                start.y.min(end.y) - GEOMETRY_EPSILON,
512            ],
513            [
514                start.x.max(end.x) + GEOMETRY_EPSILON,
515                start.y.max(end.y) + GEOMETRY_EPSILON,
516            ],
517        );
518
519        !index
520            .obstacles
521            .locate_in_envelope_intersecting(envelope)
522            .any(|entry| {
523                index
524                    .segments
525                    .get(entry.data)
526                    .is_some_and(|obstacle| los_contact((start, end), *obstacle))
527            })
528    }
529
530    fn validate_transform(&self, transform: SpatialTransform) -> Result<(), SpatialQueryError> {
531        if !transform.position.x.is_finite()
532            || !transform.position.y.is_finite()
533            || !transform.heading.is_finite()
534        {
535            return Err(SpatialQueryError::non_finite_transform());
536        }
537
538        let index = self
539            .layer(transform.layer)
540            .ok_or_else(|| SpatialQueryError::missing_layer(transform.layer))?;
541
542        if transform_segment_candidates(index, transform.position).any(|entry| {
543            index.segments.get(entry.data).is_some_and(|segment| {
544                point_segment_distance_sq(transform.position, *segment)
545                    <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
546            })
547        }) || transform_polygon_candidates(index, transform.position).any(|entry| {
548            index
549                .polygons
550                .get(entry.data)
551                .is_some_and(|polygon| point_in_blocker(transform.position, polygon))
552        }) {
553            return Err(SpatialQueryError::blocked_transform());
554        }
555
556        Ok(())
557    }
558
559    fn insert_actor(&mut self, actor: SpatialActor) -> Result<(), SpatialQueryError> {
560        self.validate_transform(actor.transform)?;
561
562        if self
563            .indexed_transforms
564            .get(actor.id.as_usize())
565            .is_some_and(Option::is_some)
566        {
567            return Err(SpatialQueryError::duplicate_actor(actor.id));
568        }
569
570        if actor.id.as_usize() >= self.indexed_transforms.len() {
571            self.indexed_transforms
572                .resize(actor.id.as_usize() + 1, None);
573        }
574
575        self.layer_mut(actor.transform.layer)
576            .ok_or_else(|| SpatialQueryError::missing_layer(actor.transform.layer))?
577            .actors
578            .insert(ActorEntry::new(point(actor.transform.position), actor.id));
579        let Some(indexed_transform) = self.indexed_transforms.get_mut(actor.id.as_usize()) else {
580            return Err(SpatialQueryError::missing_actor(actor.id));
581        };
582
583        *indexed_transform = Some(actor.transform);
584
585        Ok(())
586    }
587
588    fn remove_actor(&mut self, actor: SpatialActor) -> Result<(), SpatialQueryError> {
589        let indexed = self
590            .indexed_transforms
591            .get(actor.id.as_usize())
592            .and_then(|transform| *transform)
593            .ok_or_else(|| SpatialQueryError::missing_actor(actor.id))?;
594
595        if indexed != actor.transform {
596            return Err(SpatialQueryError::transform_mismatch(actor.id));
597        }
598
599        let removed = self
600            .layer_mut(indexed.layer)
601            .ok_or_else(|| SpatialQueryError::missing_layer(indexed.layer))?
602            .actors
603            .remove(&ActorEntry::new(point(indexed.position), actor.id));
604
605        if removed.is_none() {
606            return Err(SpatialQueryError::missing_actor(actor.id));
607        }
608
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 = None;
614
615        Ok(())
616    }
617
618    fn relocate_actor(
619        &mut self,
620        actor: EnemyIdx,
621        from: SpatialTransform,
622        to: SpatialTransform,
623    ) -> Result<(), SpatialQueryError> {
624        self.validate_transform(to)?;
625        let indexed = self
626            .indexed_transforms
627            .get(actor.as_usize())
628            .and_then(|transform| *transform)
629            .ok_or_else(|| SpatialQueryError::missing_actor(actor))?;
630
631        if indexed != from {
632            return Err(SpatialQueryError::transform_mismatch(actor));
633        }
634
635        let from_entry = ActorEntry::new(point(from.position), actor);
636
637        if self
638            .layer_mut(from.layer)
639            .ok_or_else(|| SpatialQueryError::missing_layer(from.layer))?
640            .actors
641            .remove(&from_entry)
642            .is_none()
643        {
644            return Err(SpatialQueryError::missing_actor(actor));
645        }
646
647        self.layer_mut(to.layer)
648            .ok_or_else(|| SpatialQueryError::missing_layer(to.layer))?
649            .actors
650            .insert(ActorEntry::new(point(to.position), actor));
651        let Some(indexed_transform) = self.indexed_transforms.get_mut(actor.as_usize()) else {
652            return Err(SpatialQueryError::missing_actor(actor));
653        };
654
655        *indexed_transform = Some(to);
656
657        Ok(())
658    }
659}
660
661fn actor_is_allowed(actor: EnemyIdx, allowed: Option<&[EnemyIdx]>) -> bool {
662    allowed.is_none_or(|ids| ids.binary_search(&actor).is_ok())
663}
664
665fn layer_index_mut(
666    layers: &mut [LayerIndex],
667    layer: SpatialLayerId,
668) -> Result<&mut LayerIndex, SpatialQueryError> {
669    layers
670        .get_mut(usize::from(layer.0))
671        .ok_or_else(|| SpatialQueryError::missing_layer(layer))
672}
673
674fn push_segment(index: &mut LayerIndex, start: Position2, end: Position2) {
675    index.segments.push((start, end));
676}
677
678fn push_ring_segments(index: &mut LayerIndex, ring: &[Position2]) {
679    for edge in ring.windows(EDGE_POINT_COUNT) {
680        if let [start, end] = edge {
681            push_segment(index, *start, *end);
682        }
683    }
684}
685
686fn point_epsilon_envelope(position: Position2) -> AABB<[f64; POINT_DIMENSIONS]> {
687    AABB::from_corners(
688        [position.x - GEOMETRY_EPSILON, position.y - GEOMETRY_EPSILON],
689        [position.x + GEOMETRY_EPSILON, position.y + GEOMETRY_EPSILON],
690    )
691}
692
693fn transform_segment_candidates(
694    index: &LayerIndex,
695    position: Position2,
696) -> impl Iterator<Item = &SegmentEntry> {
697    index
698        .obstacles
699        .locate_in_envelope_intersecting(point_epsilon_envelope(position))
700}
701
702fn transform_polygon_candidates(
703    index: &LayerIndex,
704    position: Position2,
705) -> impl Iterator<Item = &PolygonEntry> {
706    index
707        .polygon_envelopes
708        .locate_in_envelope_intersecting(point_epsilon_envelope(position))
709}
710
711fn polygon_envelope(polygon: &PolygonBlocker) -> Option<Rectangle<[f64; POINT_DIMENSIONS]>> {
712    let first = polygon.exterior.first()?;
713    let mut min = *first;
714    let mut max = *first;
715
716    for position in polygon.exterior.iter().skip(1) {
717        min.x = min.x.min(position.x);
718        min.y = min.y.min(position.y);
719        max.x = max.x.max(position.x);
720        max.y = max.y.max(position.y);
721    }
722
723    Some(Rectangle::from_corners(point(min), point(max)))
724}
725
726const fn point(position: Position2) -> [f64; POINT_DIMENSIONS] {
727    [position.x, position.y]
728}
729
730const fn coord(position: Position2) -> Coord<f64> {
731    Coord {
732        x: position.x,
733        y: position.y,
734    }
735}
736
737fn exact_intersection(a: (Position2, Position2), b: (Position2, Position2)) -> bool {
738    let o1 = orient2d(coord(a.0), coord(a.1), coord(b.0));
739    let o2 = orient2d(coord(a.0), coord(a.1), coord(b.1));
740    let o3 = orient2d(coord(b.0), coord(b.1), coord(a.0));
741    let o4 = orient2d(coord(b.0), coord(b.1), coord(a.1));
742
743    if opposite_signs(o1, o2) && opposite_signs(o3, o4) {
744        return true;
745    }
746
747    exact_zero(o1) && on_closed_segment(b.0, a.0, a.1)
748        || exact_zero(o2) && on_closed_segment(b.1, a.0, a.1)
749        || exact_zero(o3) && on_closed_segment(a.0, b.0, b.1)
750        || exact_zero(o4) && on_closed_segment(a.1, b.0, b.1)
751}
752
753const fn opposite_signs(left: f64, right: f64) -> bool {
754    (left > 0.0 && right < 0.0) || (left < 0.0 && right > 0.0)
755}
756
757fn exact_zero(value: f64) -> bool {
758    value.total_cmp(&0.0).is_eq() || value.total_cmp(&-0.0).is_eq()
759}
760
761fn on_closed_segment(point: Position2, start: Position2, end: Position2) -> bool {
762    point.x >= start.x.min(end.x)
763        && point.x <= start.x.max(end.x)
764        && point.y >= start.y.min(end.y)
765        && point.y <= start.y.max(end.y)
766}
767
768fn point_segment_distance_sq(point: Position2, segment: (Position2, Position2)) -> f64 {
769    let dx = segment.1.x - segment.0.x;
770    let dy = segment.1.y - segment.0.y;
771    let length_sq = dx.mul_add(dx, dy * dy);
772
773    if exact_zero(length_sq) {
774        let ex = point.x - segment.0.x;
775        let ey = point.y - segment.0.y;
776
777        return ex.mul_add(ex, ey * ey);
778    }
779
780    let t =
781        (((point.x - segment.0.x) * dx + (point.y - segment.0.y) * dy) / length_sq).clamp(0.0, 1.0);
782    let qx = segment.0.x + t * dx;
783    let qy = segment.0.y + t * dy;
784    let ex = point.x - qx;
785    let ey = point.y - qy;
786
787    ex.mul_add(ex, ey * ey)
788}
789
790fn los_contact(a: (Position2, Position2), b: (Position2, Position2)) -> bool {
791    exact_intersection(a, b)
792        || point_segment_distance_sq(a.0, b) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
793        || point_segment_distance_sq(a.1, b) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
794        || point_segment_distance_sq(b.0, a) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
795        || point_segment_distance_sq(b.1, a) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
796}
797
798fn angle_diff(left: f64, right: f64) -> f64 {
799    let mut delta = (left - right) % std::f64::consts::TAU;
800
801    if delta > std::f64::consts::PI {
802        delta -= std::f64::consts::TAU;
803    }
804
805    if delta < -std::f64::consts::PI {
806        delta += std::f64::consts::TAU;
807    }
808
809    delta.abs()
810}
811
812fn in_cone(apex: Position2, heading: f64, half_angle: f64, range: f64, point: Position2) -> bool {
813    let distance = apex.distance(point);
814
815    if distance > range + GEOMETRY_EPSILON {
816        return false;
817    }
818
819    if distance <= GEOMETRY_EPSILON {
820        return true;
821    }
822
823    let angle = (point.y - apex.y).atan2(point.x - apex.x);
824
825    angle_diff(angle, heading) <= half_angle + GEOMETRY_EPSILON
826}
827
828fn cone_envelope(apex: Position2, heading: f64, half_angle: f64, range: f64) -> SpatialEnvelope {
829    if half_angle >= std::f64::consts::PI - GEOMETRY_EPSILON {
830        return SpatialEnvelope {
831            min: Position2 {
832                x: apex.x - range - GEOMETRY_EPSILON,
833                y: apex.y - range - GEOMETRY_EPSILON,
834            },
835            max: Position2 {
836                x: apex.x + range + GEOMETRY_EPSILON,
837                y: apex.y + range + GEOMETRY_EPSILON,
838            },
839        };
840    }
841
842    let mut min = apex;
843    let mut max = apex;
844    let mut include = |angle: f64| {
845        let position = Position2 {
846            x: apex.x + range * angle.cos(),
847            y: apex.y + range * angle.sin(),
848        };
849
850        min.x = min.x.min(position.x);
851        min.y = min.y.min(position.y);
852        max.x = max.x.max(position.x);
853        max.y = max.y.max(position.y);
854    };
855
856    include(heading - half_angle);
857    include(heading + half_angle);
858
859    for quadrant in 0..QUADRANT_COUNT {
860        let angle = f64::from(quadrant) * std::f64::consts::FRAC_PI_2;
861
862        if angle_diff(angle, heading) <= half_angle + GEOMETRY_EPSILON {
863            include(angle);
864        }
865    }
866
867    SpatialEnvelope {
868        min: Position2 {
869            x: min.x - GEOMETRY_EPSILON,
870            y: min.y - GEOMETRY_EPSILON,
871        },
872        max: Position2 {
873            x: max.x + GEOMETRY_EPSILON,
874            y: max.y + GEOMETRY_EPSILON,
875        },
876    }
877}
878
879fn point_in_blocker(point: Position2, polygon: &PolygonBlocker) -> bool {
880    ring_contains(point, &polygon.exterior)
881        && !polygon
882            .holes
883            .iter()
884            .any(|hole| ring_contains_strict(point, hole))
885}
886
887fn ring_contains(point: Position2, ring: &[Position2]) -> bool {
888    ring.windows(EDGE_POINT_COUNT).any(|edge| {
889        let [start, end] = edge else {
890            return false;
891        };
892
893        point_segment_distance_sq(point, (*start, *end)) <= GEOMETRY_EPSILON * GEOMETRY_EPSILON
894    }) || ring_contains_strict(point, ring)
895}
896
897fn ring_contains_strict(point: Position2, ring: &[Position2]) -> bool {
898    let mut inside = false;
899
900    for edge in ring.windows(EDGE_POINT_COUNT) {
901        let [a, b] = edge else {
902            continue;
903        };
904
905        if (a.y > point.y) != (b.y > point.y) {
906            let crossing_x = (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x;
907
908            if point.x < crossing_x {
909                inside = !inside;
910            }
911        }
912    }
913
914    inside
915}
916
917#[cfg(test)]
918#[path = "../benches/support/mod.rs"]
919mod frozen_oracle;
920
921#[cfg(test)]
922#[path = "rstar/tests.rs"]
923mod tests;