jagua_rs/collision_detection/
cd_engine.rs

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