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