jagua_rs/collision_detection/
cd_engine.rs

1use crate::collision_detection::hazards::HazKey;
2use crate::collision_detection::hazards::Hazard;
3use crate::collision_detection::hazards::HazardEntity;
4use crate::collision_detection::hazards::collector::HazardCollector;
5use crate::collision_detection::hazards::filter::HazardFilter;
6use crate::collision_detection::quadtree::{QTHazPresence, QTHazard, QTNode};
7use crate::entities::PItemKey;
8use crate::geometry::Transformation;
9use crate::geometry::fail_fast::{SPSurrogate, SPSurrogateConfig};
10use crate::geometry::geo_enums::{GeoPosition, GeoRelation};
11use crate::geometry::geo_traits::{CollidesWith, Transformable};
12use crate::geometry::primitives::Rect;
13use crate::geometry::primitives::SPolygon;
14use crate::util::assertions;
15use itertools::Itertools;
16use serde::{Deserialize, Serialize};
17use slotmap::SlotMap;
18
19/// The Collision Detection Engine (CDE).
20/// [`Hazard`]s can be (de)registered and collision queries can be performed.
21#[derive(Clone, Debug)]
22pub struct CDEngine {
23    /// Root node of the quadtree
24    pub quadtree: QTNode,
25    /// All hazards registered in the CDE (active and inactive)
26    pub hazards_map: SlotMap<HazKey, Hazard>,
27    /// Configuration of the CDE
28    pub config: CDEConfig,
29    /// The key of the hazard that represents the exterior of the container.
30    hkey_exterior: HazKey,
31}
32
33impl CDEngine {
34    pub fn new(bbox: Rect, static_hazards: Vec<Hazard>, config: CDEConfig) -> CDEngine {
35        let mut quadtree = QTNode::new(config.quadtree_depth, bbox, config.cd_threshold);
36        let mut hazards_map = SlotMap::with_key();
37
38        for haz in static_hazards.into_iter() {
39            let hkey = hazards_map.insert(haz);
40            let qt_haz = QTHazard::from_root(quadtree.bbox, &hazards_map[hkey], hkey);
41            quadtree.register_hazard(qt_haz, &hazards_map);
42        }
43
44        let hkey_exterior = hazards_map
45            .iter()
46            .find(|(_, h)| matches!(h.entity, HazardEntity::Exterior))
47            .map(|(hkey, _)| hkey)
48            .expect("No exterior hazard registered in the CDE");
49
50        CDEngine {
51            quadtree,
52            hazards_map,
53            config,
54            hkey_exterior,
55        }
56    }
57
58    /// Registers a new hazard in the CDE.
59    pub fn register_hazard(&mut self, hazard: Hazard) {
60        debug_assert!(
61            !self.hazards_map.values().any(|h| h.entity == hazard.entity),
62            "Hazard with an identical entity already registered"
63        );
64        let hkey = self.hazards_map.insert(hazard);
65        let qt_hazard = QTHazard::from_root(self.bbox(), &self.hazards_map[hkey], hkey);
66        self.quadtree.register_hazard(qt_hazard, &self.hazards_map);
67
68        debug_assert!(assertions::qt_contains_no_dangling_hazards(self));
69    }
70
71    /// Removes a hazard from the CDE.
72    pub fn deregister_hazard_by_entity(&mut self, hazard_entity: HazardEntity) -> Hazard {
73        let hkey = self
74            .hazards_map
75            .iter()
76            .find(|(_, h)| h.entity == hazard_entity)
77            .map(|(hkey, _)| hkey)
78            .expect("Cannot deregister hazard that is not registered");
79
80        self.quadtree.deregister_hazard(hkey);
81        let hazard = self.hazards_map.remove(hkey).unwrap();
82        debug_assert!(assertions::qt_contains_no_dangling_hazards(self));
83
84        hazard
85    }
86
87    pub fn deregister_hazard_by_key(&mut self, hkey: HazKey) -> Hazard {
88        let hazard = self
89            .hazards_map
90            .remove(hkey)
91            .expect("Cannot deregister hazard that is not registered");
92        self.quadtree.deregister_hazard(hkey);
93        debug_assert!(assertions::qt_contains_no_dangling_hazards(self));
94
95        hazard
96    }
97
98    pub fn save(&self) -> CDESnapshot {
99        let dynamic_hazards = self
100            .hazards_map
101            .values()
102            .filter(|h| h.dynamic)
103            .cloned()
104            .collect_vec();
105        CDESnapshot { dynamic_hazards }
106    }
107
108    /// Restores the CDE to a previous state, as described by the snapshot.
109    pub fn restore(&mut self, snapshot: &CDESnapshot) {
110        //Restore the quadtree, by doing a 'diff' between the current state and the snapshot
111        //Only dynamic hazards are considered
112
113        //Determine which dynamic hazards need to be removed and which need to be added
114        let mut hazards_to_remove = self
115            .hazards_map
116            .iter()
117            .filter(|(_, h)| h.dynamic)
118            .map(|(hkey, h)| (hkey, h.entity))
119            .collect_vec();
120        let mut hazards_to_add = vec![];
121
122        for hazard in snapshot.dynamic_hazards.iter() {
123            let present = hazards_to_remove
124                .iter()
125                .position(|(_, h)| h == &hazard.entity);
126            if let Some(idx) = present {
127                //the hazard is already present in the CDE, remove it from the hazards to remove
128                hazards_to_remove.swap_remove(idx);
129            } else {
130                //the hazard is not present in the CDE, add it to the list of hazards to add
131                hazards_to_add.push(hazard.clone());
132            }
133        }
134
135        //Remove all hazards currently in the CDE but not in the snapshot
136        for (hkey, _) in hazards_to_remove {
137            self.deregister_hazard_by_key(hkey);
138        }
139
140        //Add all hazards in the snapshot but not currently in the CDE
141        for hazard in hazards_to_add {
142            self.register_hazard(hazard);
143        }
144
145        debug_assert!(
146            self.hazards_map.values().filter(|h| h.dynamic).count()
147                == snapshot.dynamic_hazards.len()
148        );
149    }
150
151    /// Returns all hazards in the CDE
152    pub fn hazards(&self) -> impl Iterator<Item = &Hazard> {
153        self.hazards_map.values()
154    }
155
156    /// Checks whether a simple polygon collides with any of the (relevant) hazards
157    /// # Arguments
158    /// * `shape` - The shape (already transformed) to be checked for collisions
159    /// * `filter` - Hazard filter to be applied
160    pub fn detect_poly_collision(&self, shape: &SPolygon, filter: &impl HazardFilter) -> bool {
161        if self.bbox().relation_to(shape.bbox) != GeoRelation::Surrounding {
162            //The CDE does not capture the entire shape, so we can immediately return true
163            true
164        } else {
165            //Instead of each time starting from the quadtree root, we can use the virtual root (lowest level node which fully surrounds the shape)
166            let v_qt_root = self.get_virtual_root(shape.bbox);
167
168            // Check for edge intersections with the shape
169            for edge in shape.edge_iter() {
170                if v_qt_root.collides(&edge, filter).is_some() {
171                    return true;
172                }
173            }
174
175            // Check for containment of the shape in any of the hazards
176            for qt_hazard in v_qt_root.hazards.iter() {
177                match &qt_hazard.presence {
178                    QTHazPresence::None => {}
179                    QTHazPresence::Entire => unreachable!(
180                        "Entire hazards in the virtual root should have been caught by the edge intersection tests"
181                    ),
182                    QTHazPresence::Partial(_) => {
183                        if !filter.is_irrelevant(qt_hazard.hkey) {
184                            let haz_shape = &self.hazards_map[qt_hazard.hkey].shape;
185                            if self.detect_containment_collision(shape, haz_shape, qt_hazard.entity)
186                            {
187                                // The hazard is contained in the shape (or vice versa)
188                                return true;
189                            }
190                        }
191                    }
192                }
193            }
194
195            false
196        }
197    }
198
199    /// Checks whether a surrogate collides with any of the (relevant) hazards.
200    /// # Arguments
201    /// * `base_surrogate` - The (untransformed) surrogate to be checked for collisions
202    /// * `transform` - The transformation to be applied to the surrogate (on the fly)
203    /// * `filter` - Hazard filter to be applied
204    pub fn detect_surrogate_collision(
205        &self,
206        base_surrogate: &SPSurrogate,
207        transform: &Transformation,
208        filter: &impl HazardFilter,
209    ) -> bool {
210        for pole in base_surrogate.ff_poles() {
211            let t_pole = pole.transform_clone(transform);
212            if self.quadtree.collides(&t_pole, filter).is_some() {
213                return true;
214            }
215        }
216        for pier in base_surrogate.ff_piers() {
217            let t_pier = pier.transform_clone(transform);
218            if self.quadtree.collides(&t_pier, filter).is_some() {
219                return true;
220            }
221        }
222        false
223    }
224
225    /// Check for collision by containment between a shape and a hazard.
226    /// This only guarantees to detect collisions caused by full containment of one shape in another.
227    /// # Arguments
228    /// * `shape` - The shape to be checked for containment
229    /// * `haz_shape` - The shape of the respective hazard
230    /// * `haz_entity` - The entity inducing the hazard
231    pub fn detect_containment_collision(
232        &self,
233        shape: &SPolygon,
234        haz_shape: &SPolygon,
235        haz_entity: HazardEntity,
236    ) -> bool {
237        //Due to possible fp issues, we check if the bboxes are "almost" related --
238        //meaning that, when edges are very close together, they are considered equal.
239        //Some relations which would normally be seen as `Intersecting` are now being considered `Enclosed`/`Surrounding` (which triggers the containment check).
240        let haz_to_shape_bbox_relation = haz_shape.bbox.almost_relation_to(shape.bbox);
241
242        //If the bounding boxes are contained, we have to check the actual shapes for containment.
243        //This can be done by testing whether a single point of the smaller shape is contained in the larger shape.
244        let contained = match haz_to_shape_bbox_relation {
245            GeoRelation::Surrounding => haz_shape.collides_with(&shape.poi.center),
246            GeoRelation::Enclosed => shape.collides_with(&haz_shape.poi.center),
247            GeoRelation::Disjoint | GeoRelation::Intersecting => false,
248        };
249
250        //Depending on the scope of the hazard this results a collision or not
251        match (haz_entity.scope(), contained) {
252            (GeoPosition::Interior, true) | (GeoPosition::Exterior, false) => true,
253            (GeoPosition::Interior, false) | (GeoPosition::Exterior, true) => false,
254        }
255    }
256
257    /// Collects all hazards with which the polygon collides and reports them to the collector.
258    /// # Arguments
259    /// * `shape` - The shape to be checked for collisions
260    /// * `collector` - The collector to which the hazards are reported
261    pub fn collect_poly_collisions(&self, shape: &SPolygon, collector: &mut impl HazardCollector) {
262        if self.bbox().relation_to(shape.bbox) != GeoRelation::Surrounding {
263            collector.insert(self.hkey_exterior, HazardEntity::Exterior);
264        }
265
266        //Instead of each time starting from the quadtree root, we can use the virtual root (lowest level node which fully surrounds the shape)
267        let v_quadtree = self.get_virtual_root(shape.bbox);
268
269        //Collect all colliding entities due to edge intersection
270        shape
271            .edge_iter()
272            .for_each(|e| v_quadtree.collect_collisions(&e, collector));
273
274        //Check if there are any other collisions due to containment
275        for qt_haz in v_quadtree.hazards.iter() {
276            match &qt_haz.presence {
277                // No need to check these, guaranteed to be detected by edge intersection
278                QTHazPresence::None | QTHazPresence::Entire => {}
279                QTHazPresence::Partial(_) => {
280                    if !collector.contains_key(qt_haz.hkey) {
281                        let h_shape = &self.hazards_map[qt_haz.hkey].shape;
282                        if self.detect_containment_collision(shape, h_shape, qt_haz.entity) {
283                            collector.insert(qt_haz.hkey, qt_haz.entity);
284                        }
285                    }
286                }
287            }
288        }
289    }
290
291    /// Collects all hazards with which the surrogate collides and reports them to the collector.
292    /// # Arguments
293    /// * `base_surrogate` - The (untransformed) surrogate to be checked for collisions
294    /// * `transform` - The transformation to be applied to the surrogate (on the fly)
295    /// * `collector` - The collector to which the hazards are reported
296    pub fn collect_surrogate_collisions(
297        &self,
298        base_surrogate: &SPSurrogate,
299        transform: &Transformation,
300        collector: &mut impl HazardCollector,
301    ) {
302        for pole in base_surrogate.ff_poles() {
303            let t_pole = pole.transform_clone(transform);
304            self.quadtree.collect_collisions(&t_pole, collector)
305        }
306        for pier in base_surrogate.ff_piers() {
307            let t_pier = pier.transform_clone(transform);
308            self.quadtree.collect_collisions(&t_pier, collector);
309        }
310    }
311
312    /// Returns the lowest `QTNode` that completely surrounds the given bounding box.
313    /// Used to initiate collision checks from lower in the quadtree.
314    pub fn get_virtual_root(&self, bbox: Rect) -> &QTNode {
315        let mut v_root = &self.quadtree;
316        while let Some(children) = v_root.children.as_ref() {
317            // Keep going down the tree until we cannot find a child that fully surrounds the shape
318            let surrounding_child = children
319                .iter()
320                .find(|child| child.bbox.relation_to(bbox) == GeoRelation::Surrounding);
321            match surrounding_child {
322                Some(child) => v_root = child,
323                None => break,
324            }
325        }
326        v_root
327    }
328
329    pub fn bbox(&self) -> Rect {
330        self.quadtree.bbox
331    }
332
333    pub fn haz_key_from_pi_key(&self, pik: PItemKey) -> Option<HazKey> {
334        self.hazards_map
335            .iter()
336            .find(|(_, hazard)| match hazard.entity {
337                HazardEntity::PlacedItem { pk, .. } => pik == pk,
338                _ => false,
339            })
340            .map(|(key, _)| key)
341    }
342}
343
344///Configuration of the [`CDEngine`]
345#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
346pub struct CDEConfig {
347    ///Maximum depth of the quadtree
348    pub quadtree_depth: u8,
349    /// Stop traversing the quadtree and perform collision collection immediately when the total number of edges in a node falls below this number
350    pub cd_threshold: u8,
351    ///Configuration of the surrogate generation for items
352    pub item_surrogate_config: SPSurrogateConfig,
353}
354
355/// Snapshot of the state of [`CDEngine`]. Can be used to restore to a previous state.
356#[derive(Clone, Debug)]
357pub struct CDESnapshot {
358    pub dynamic_hazards: Vec<Hazard>,
359}