Skip to main content

jagua_rs/collision_detection/
cd_engine.rs

1use crate::collision_detection::hazard_index::HazardIndex;
2use crate::collision_detection::hazards::HazKey;
3use crate::collision_detection::hazards::Hazard;
4use crate::collision_detection::hazards::HazardGeometry;
5use crate::collision_detection::hazards::collector::HazardCollector;
6use crate::collision_detection::hazards::filter::HazardFilter;
7use crate::collision_detection::hazards::{BasicHazardEntity, HazardEntity};
8use crate::collision_detection::narrow_phase::{self, EdgeChains};
9use crate::geometry::Transformation;
10use crate::geometry::fail_fast::{SPSurrogate, SPSurrogateConfig};
11use crate::geometry::geo_enums::{GeoPosition, GeoRelation};
12use crate::geometry::geo_traits::{CollidesWith, Transformable};
13use crate::geometry::primitives::{Circle, Rect, SPolygon};
14use crate::util::assertions;
15use itertools::Itertools;
16use serde::{Deserialize, Serialize};
17use slotmap::{SecondaryMap, SlotMap};
18
19/// The Collision Detection Engine (CDE).
20/// [`Hazard`]s can be (de)registered and collision queries can be performed.
21/// The entity type `E` defines what can induce a hazard, see [`HazardEntity`].
22#[derive(Clone, Debug)]
23pub struct CDEngine<E = BasicHazardEntity> {
24    /// Shapes reaching outside this rectangle collide with the exterior
25    bbox: Rect,
26    /// All hazards registered in the CDE (active and inactive)
27    pub(crate) hazards_map: SlotMap<HazKey, Hazard<E>>,
28    /// Configuration of the CDE
29    pub(crate) config: CDEConfig,
30    /// The key of the hazard that represents the exterior of the container.
31    hkey_exterior: HazKey,
32    /// Bounding boxes of all hazards except the exterior
33    pub(crate) index: HazardIndex,
34    /// Keys of the hazards the CDE was created with, which snapshots leave out
35    base_keys: SecondaryMap<HazKey, ()>,
36}
37
38impl<E: HazardEntity> CDEngine<E> {
39    /// Creates an engine with the static hazards, exactly one of which covers the exterior.
40    /// Snapshots hold only the hazards registered later.
41    #[must_use]
42    pub fn new(bbox: Rect, static_hazards: Vec<Hazard<E>>, config: CDEConfig) -> Self {
43        let mut hazards_map = SlotMap::with_key();
44        for haz in static_hazards {
45            hazards_map.insert(haz);
46        }
47        let base_keys = hazards_map.keys().map(|k| (k, ())).collect();
48
49        let mut exteriors = hazards_map
50            .iter()
51            .filter(|(_, h)| h.entity().scope() == GeoPosition::Exterior)
52            .map(|(hkey, _)| hkey);
53        let hkey_exterior = exteriors
54            .next()
55            .expect("No exterior hazard registered in the CDE");
56        assert!(
57            exteriors.next().is_none(),
58            "More than one exterior hazard registered in the CDE"
59        );
60
61        let mut index = HazardIndex::default();
62        for (hkey, haz) in &hazards_map {
63            if hkey != hkey_exterior {
64                index.insert(hkey, haz.shape().bbox);
65            }
66        }
67
68        let cde = CDEngine {
69            bbox,
70            hazards_map,
71            config,
72            hkey_exterior,
73            index,
74            base_keys,
75        };
76        debug_assert!(assertions::index_matches_hazards(&cde));
77        cde
78    }
79
80    /// Registers a new hazard in the CDE.
81    pub fn register_hazard(&mut self, hazard: Hazard<E>) {
82        debug_assert!(
83            !self
84                .hazards_map
85                .values()
86                .any(|h| h.entity() == hazard.entity()),
87            "Hazard with an identical entity already registered"
88        );
89        debug_assert!(
90            hazard.entity().scope() == GeoPosition::Interior,
91            "The exterior hazard can only be registered at construction"
92        );
93        let bbox = hazard.shape().bbox;
94        let hkey = self.hazards_map.insert(hazard);
95        self.index.insert(hkey, bbox);
96        debug_assert!(assertions::index_matches_hazards(self));
97    }
98
99    /// Removes a hazard from the CDE.
100    pub fn deregister_hazard_by_entity(&mut self, hazard_entity: E) -> Hazard<E> {
101        let hkey = self
102            .hazards_map
103            .iter()
104            .find(|(_, h)| h.entity() == hazard_entity)
105            .map(|(hkey, _)| hkey)
106            .expect("Cannot deregister hazard that is not registered");
107        self.deregister_hazard_by_key(hkey)
108    }
109
110    /// Removes a hazard registered after construction from the CDE.
111    pub fn deregister_hazard_by_key(&mut self, hkey: HazKey) -> Hazard<E> {
112        debug_assert!(
113            !self.base_keys.contains_key(hkey),
114            "Hazards the CDE was created with cannot be deregistered"
115        );
116        let hazard = self
117            .hazards_map
118            .remove(hkey)
119            .expect("Cannot deregister hazard that is not registered");
120        self.index.remove(hkey, hazard.shape().bbox);
121        debug_assert!(assertions::index_matches_hazards(self));
122
123        hazard
124    }
125
126    /// Saves the hazards registered after construction, to restore them later with
127    /// [`CDEngine::restore`].
128    #[must_use]
129    pub fn save(&self) -> CDESnapshot<E> {
130        let dynamic_hazards = self
131            .hazards_map
132            .iter()
133            .filter(|(hkey, _)| !self.base_keys.contains_key(*hkey))
134            .map(|(_, h)| h.clone())
135            .collect_vec();
136        CDESnapshot { dynamic_hazards }
137    }
138
139    /// Restores the hazards registered after construction to those of a snapshot, keeping
140    /// those present in both.
141    pub fn restore(&mut self, snapshot: &CDESnapshot<E>) {
142        //Restore the hazards, by doing a 'diff' between the current state and the snapshot
143
144        //Determine which hazards need to be removed and which need to be added
145        let mut hazards_to_remove = self
146            .hazards_map
147            .iter()
148            .filter(|(hkey, _)| !self.base_keys.contains_key(*hkey))
149            .map(|(hkey, h)| (hkey, h.entity()))
150            .collect_vec();
151        let mut hazards_to_add = vec![];
152
153        for hazard in &snapshot.dynamic_hazards {
154            let present = hazards_to_remove
155                .iter()
156                .position(|(_, h)| *h == hazard.entity());
157            if let Some(idx) = present {
158                //the hazard is already present in the CDE, remove it from the hazards to remove
159                hazards_to_remove.swap_remove(idx);
160            } else {
161                //the hazard is not present in the CDE, add it to the list of hazards to add
162                hazards_to_add.push(hazard.clone());
163            }
164        }
165
166        //Remove all hazards currently in the CDE but not in the snapshot
167        for (hkey, _) in hazards_to_remove {
168            self.deregister_hazard_by_key(hkey);
169        }
170
171        //Add all hazards in the snapshot but not currently in the CDE
172        for hazard in hazards_to_add {
173            self.register_hazard(hazard);
174        }
175
176        debug_assert_eq!(
177            self.hazards_map.len() - self.base_keys.len(),
178            snapshot.dynamic_hazards.len()
179        );
180    }
181
182    /// Returns all hazards in the CDE
183    pub fn hazards(&self) -> impl Iterator<Item = &Hazard<E>> {
184        self.hazards_map.values()
185    }
186
187    /// Checks whether a simple polygon collides with any of the (relevant) hazards
188    /// # Arguments
189    /// * `shape` - The shape (already transformed) to be checked for collisions
190    /// * `filter` - Hazard filter to be applied
191    pub fn detect_poly_collision(&self, shape: &SPolygon, filter: &impl HazardFilter<E>) -> bool {
192        if self.bbox.relation_to(shape.bbox) != GeoRelation::Surrounding {
193            //The CDE does not capture the entire shape, so we can immediately return true
194            return true;
195        }
196        let exterior = self.hazards_map[self.hkey_exterior].entity();
197        if !filter.is_irrelevant(self.hkey_exterior, &exterior)
198            && self.collides_with_exterior(shape)
199        {
200            return true;
201        }
202        self.index.any_overlapping(shape.bbox, |hkey| {
203            let haz = &self.hazards_map[hkey];
204            !filter.is_irrelevant(hkey, &haz.entity()) && Self::poly_collides_with(shape, haz)
205        })
206    }
207
208    /// Checks whether a surrogate collides with any of the (relevant) hazards.
209    /// A `true` result proves a collision for the represented polygon.
210    /// A `false` result is inconclusive; follow with [`Self::detect_poly_collision`].
211    /// # Arguments
212    /// * `base_surrogate` - The (untransformed) surrogate to be checked for collisions
213    /// * `transform` - The transformation to be applied to the surrogate (on the fly)
214    /// * `filter` - Hazard filter to be applied
215    pub fn detect_surrogate_collision(
216        &self,
217        base_surrogate: &SPSurrogate,
218        transform: &Transformation,
219        filter: &impl HazardFilter<E>,
220    ) -> bool {
221        let exterior = self.hazards_map[self.hkey_exterior].entity();
222        let check_exterior = !filter.is_irrelevant(self.hkey_exterior, &exterior);
223        for pole in base_surrogate.ff_poles() {
224            let pole = pole.transform_clone(transform);
225            if check_exterior && self.pole_collides_with_exterior(&pole) {
226                return true;
227            }
228            let hit = self.index.any_overlapping(pole.bbox(), |hkey| {
229                let haz = &self.hazards_map[hkey];
230                !filter.is_irrelevant(hkey, &haz.entity()) && Self::pole_collides_with(&pole, haz)
231            });
232            if hit {
233                return true;
234            }
235        }
236        false
237    }
238
239    /// Whether `shape` collides with the interior hazard `haz`, given that their bounding boxes overlap.
240    fn poly_collides_with(shape: &SPolygon, haz: &Hazard<E>) -> bool {
241        let r = narrow_phase::overlap(shape.bbox, haz.shape().bbox)
242            .expect("the index reports overlapping bounding boxes");
243        narrow_phase::edges_intersect(shape, r, haz)
244            || Self::detect_containment_collision(shape, haz)
245    }
246
247    /// Whether `pole` collides with the interior hazard `haz`.
248    fn pole_collides_with(pole: &Circle, haz: &Hazard<E>) -> bool {
249        // A pole touching no edge collides only if it lies inside the hazard,
250        // which requires its bounding box to lie inside the hazard's
251        narrow_phase::circle_touches_edges(pole, haz)
252            || (haz.shape().bbox.relation_to(pole.bbox()) == GeoRelation::Surrounding
253                && narrow_phase::poly_contains(haz.shape(), haz, pole.center))
254    }
255
256    /// Check for collision by containment between a shape and a hazard.
257    /// This only guarantees to detect collisions caused by full containment of one shape in another.
258    /// # Arguments
259    /// * `shape` - The shape to be checked for containment
260    /// * `haz` - The hazard
261    #[must_use]
262    fn detect_containment_collision(shape: &SPolygon, haz: &Hazard<E>) -> bool {
263        let (haz_shape, haz_entity) = (haz.shape(), haz.entity());
264        //Due to possible fp issues, we check if the bboxes are "almost" related --
265        //meaning that, when edges are very close together, they are considered equal.
266        //Some relations which would normally be seen as `Intersecting` are now being considered `Enclosed`/`Surrounding` (which triggers the containment check).
267        let haz_to_shape_bbox_relation = haz_shape.bbox.almost_relation_to(shape.bbox);
268
269        //If the bounding boxes are contained, we have to check the actual shapes for containment.
270        //This can be done by testing whether a single point of the smaller shape is contained in the larger shape.
271        let contained = match haz_to_shape_bbox_relation {
272            GeoRelation::Surrounding => {
273                narrow_phase::poly_contains(haz_shape, haz, shape.poi.center)
274            }
275            GeoRelation::Enclosed => shape.collides_with(&haz_shape.poi.center),
276            GeoRelation::Disjoint | GeoRelation::Intersecting => false,
277        };
278
279        //Depending on the scope of the hazard this results a collision or not
280        match (haz_entity.scope(), contained) {
281            (GeoPosition::Interior, true) | (GeoPosition::Exterior, false) => true,
282            (GeoPosition::Interior, false) | (GeoPosition::Exterior, true) => false,
283        }
284    }
285
286    /// Collects all hazards with which the polygon collides and reports them to the collector.
287    /// # Arguments
288    /// * `shape` - The shape to be checked for collisions
289    /// * `filter` - Hazards to ignore
290    /// * `collector` - The collector to which the hazards are reported
291    pub fn collect_poly_collisions(
292        &self,
293        shape: &SPolygon,
294        filter: &impl HazardFilter<E>,
295        collector: &mut impl HazardCollector<Entity = E>,
296    ) {
297        let _ = self.collect_poly_collisions_until(shape, filter, collector, |_| false);
298    }
299
300    /// Collects polygon collisions until `stop_after_collision` requests an early return.
301    ///
302    /// The callback receives each colliding hazard's entity after it is added to the collector for
303    /// the first time. Returning `true` stops traversal and leaves the collector with only the
304    /// hazards found up to that point. Collision order is unspecified.
305    #[must_use]
306    pub fn collect_poly_collisions_until<H, C, F>(
307        &self,
308        shape: &SPolygon,
309        filter: &H,
310        collector: &mut C,
311        mut stop_after_collision: F,
312    ) -> bool
313    where
314        H: HazardFilter<E>,
315        C: HazardCollector<Entity = E>,
316        F: FnMut(E) -> bool,
317    {
318        let exterior = self.hazards_map[self.hkey_exterior].entity();
319        if !collector.contains_key(self.hkey_exterior)
320            && !filter.is_irrelevant(self.hkey_exterior, &exterior)
321            && (self.bbox.relation_to(shape.bbox) != GeoRelation::Surrounding
322                || self.collides_with_exterior(shape))
323        {
324            collector.insert(self.hkey_exterior, exterior);
325            if stop_after_collision(exterior) {
326                return true;
327            }
328        }
329
330        // Every hazard whose bounding box overlaps the shape's is checked once
331        self.index.any_overlapping(shape.bbox, |hkey| {
332            if collector.contains_key(hkey) {
333                return false;
334            }
335            let haz = &self.hazards_map[hkey];
336            if filter.is_irrelevant(hkey, &haz.entity()) {
337                return false;
338            }
339            if Self::poly_collides_with(shape, haz) {
340                collector.insert(hkey, haz.entity());
341                return stop_after_collision(haz.entity());
342            }
343            false
344        })
345    }
346
347    /// Collects collisions found by the surrogate screening pass.
348    ///
349    /// This checks a prefix of the largest poles, not the complete polygon. Follow it with
350    /// [`Self::collect_poly_collisions`] when every collision must be collected.
351    pub fn collect_surrogate_collisions(
352        &self,
353        shape: &SPolygon,
354        filter: &impl HazardFilter<E>,
355        collector: &mut impl HazardCollector<Entity = E>,
356    ) {
357        let _ = self.collect_surrogate_collisions_until(shape, filter, collector, |_| false);
358    }
359
360    /// Collects collisions found by the surrogate screening pass until
361    /// `stop_after_collision` requests an early return.
362    ///
363    /// The callback contract matches [`Self::collect_poly_collisions_until`]. The configured
364    /// fail-fast poles are checked against the exterior first, then against the other hazards in
365    /// index order, not by pole size. This is not a complete polygon collision query; follow it
366    /// with [`Self::collect_poly_collisions_until`] if the callback does not stop the screening
367    /// pass.
368    #[must_use]
369    pub fn collect_surrogate_collisions_until<H, C, F>(
370        &self,
371        shape: &SPolygon,
372        filter: &H,
373        collector: &mut C,
374        mut stop_after_collision: F,
375    ) -> bool
376    where
377        H: HazardFilter<E>,
378        C: HazardCollector<Entity = E>,
379        F: FnMut(E) -> bool,
380    {
381        let Some(surrogate) = &shape.surrogate else {
382            return false;
383        };
384        let poles = surrogate.ff_poles();
385        let exterior = self.hazards_map[self.hkey_exterior].entity();
386        for pole in poles {
387            if !collector.contains_key(self.hkey_exterior)
388                && !filter.is_irrelevant(self.hkey_exterior, &exterior)
389                && self.pole_collides_with_exterior(pole)
390            {
391                collector.insert(self.hkey_exterior, exterior);
392                if stop_after_collision(exterior) {
393                    return true;
394                }
395            }
396        }
397
398        // One scan of the index with the poles' combined bounding box
399        let Some(union) = poles.iter().map(Circle::bbox).reduce(Rect::bounding_rect) else {
400            return false;
401        };
402        self.index
403            .any_overlapping_each(union, &poles.iter().map(Circle::bbox), |i, hkey| {
404                if collector.contains_key(hkey) {
405                    return false;
406                }
407                let haz = &self.hazards_map[hkey];
408                if filter.is_irrelevant(hkey, &haz.entity()) {
409                    return false;
410                }
411                if Self::pole_collides_with(&poles[i], haz) {
412                    collector.insert(hkey, haz.entity());
413                    return stop_after_collision(haz.entity());
414                }
415                false
416            })
417    }
418
419    /// The exterior hazard and its geometry.
420    fn exterior(&self) -> (&Hazard<E>, &EdgeChains, bool) {
421        let haz = &self.hazards_map[self.hkey_exterior];
422        match haz.geometry() {
423            HazardGeometry::Exterior { chains, is_rect } => (haz, chains, *is_rect),
424            HazardGeometry::Interior(_) => unreachable!("the exterior has exterior geometry"),
425        }
426    }
427
428    /// Whether `shape` collides with the exterior hazard.
429    fn collides_with_exterior(&self, shape: &SPolygon) -> bool {
430        let (haz, chains, is_rect) = self.exterior();
431        let ext_bbox = haz.shape().bbox;
432        if is_rect && strictly_inside(shape.bbox, ext_bbox) {
433            return false;
434        }
435        let Some(r) = narrow_phase::overlap(shape.bbox, ext_bbox) else {
436            return true;
437        };
438        narrow_phase::edges_intersect(shape, r, haz)
439            || match ext_bbox.almost_relation_to(shape.bbox) {
440                // No edges cross: the shape lies inside or outside the container as a whole
441                GeoRelation::Surrounding => !chains.contains(shape.poi.center),
442                _ => true,
443            }
444    }
445
446    /// Whether `pole` collides with the exterior hazard.
447    fn pole_collides_with_exterior(&self, pole: &Circle) -> bool {
448        let (haz, chains, is_rect) = self.exterior();
449        if is_rect && strictly_inside(pole.bbox(), haz.shape().bbox) {
450            return false;
451        }
452        narrow_phase::circle_touches_edges(pole, haz) || !chains.contains(pole.center)
453    }
454
455    /// Shapes reaching outside this rectangle collide with the exterior.
456    #[must_use]
457    pub fn bbox(&self) -> Rect {
458        self.bbox
459    }
460
461    /// Returns the key of the hazard induced by `entity`, if registered.
462    #[must_use]
463    pub fn haz_key(&self, entity: &E) -> Option<HazKey> {
464        self.hazards_map
465            .iter()
466            .find(|(_, hazard)| hazard.entity() == *entity)
467            .map(|(key, _)| key)
468    }
469
470    /// All hazards registered in the CDE (active and inactive)
471    #[must_use]
472    pub fn hazards_map(&self) -> &SlotMap<HazKey, Hazard<E>> {
473        &self.hazards_map
474    }
475
476    /// Returns the registered hazard, or `None` if the key is no longer valid.
477    #[inline]
478    #[must_use]
479    pub fn hazard(&self, key: HazKey) -> Option<&Hazard<E>> {
480        self.hazards_map.get(key)
481    }
482
483    /// Configuration of the CDE
484    #[must_use]
485    pub fn config(&self) -> CDEConfig {
486        self.config
487    }
488}
489
490/// Whether `inner` lies inside `outer` without touching its sides.
491fn strictly_inside(inner: Rect, outer: Rect) -> bool {
492    outer.x_min < inner.x_min
493        && inner.x_max < outer.x_max
494        && outer.y_min < inner.y_min
495        && inner.y_max < outer.y_max
496}
497
498///Configuration of the [`CDEngine`]
499#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
500pub struct CDEConfig {
501    ///Configuration of the surrogate generation for items
502    pub item_surrogate_config: SPSurrogateConfig,
503}
504
505/// Snapshot of the dynamic hazards of a [`CDEngine`], those registered after its construction,
506/// to restore them later.
507#[derive(Clone, Debug)]
508pub struct CDESnapshot<E = BasicHazardEntity> {
509    pub(crate) dynamic_hazards: Vec<Hazard<E>>,
510}
511
512impl<E> CDESnapshot<E> {
513    /// The saved hazards, those registered after the engine's construction.
514    #[must_use]
515    pub fn dynamic_hazards(&self) -> &[Hazard<E>] {
516        &self.dynamic_hazards
517    }
518}