jagua_rs/util/
assertions.rs

1use crate::collision_detection::CDEngine;
2use crate::collision_detection::hazards::Hazard;
3use crate::collision_detection::hazards::HazardEntity;
4use crate::collision_detection::quadtree::QTHazPresence;
5use crate::collision_detection::quadtree::QTHazard;
6use crate::collision_detection::quadtree::QTNode;
7use crate::entities::Layout;
8use crate::entities::LayoutSnapshot;
9use crate::geometry::primitives::Rect;
10use itertools::Itertools;
11use log::error;
12use std::collections::HashSet;
13//Various checks to verify correctness of the state of the system
14//Used in debug_assertion!() blocks
15
16#[must_use]
17pub fn snapshot_matches_layout(layout: &Layout, layout_snapshot: &LayoutSnapshot) -> bool {
18    if layout.container.id != layout_snapshot.container.id {
19        return false;
20    }
21    for placed_item in layout_snapshot.placed_items.values() {
22        if !layout
23            .placed_items
24            .values()
25            .any(|pi| pi.item_id == placed_item.item_id && pi.d_transf == placed_item.d_transf)
26        {
27            return false;
28        }
29    }
30    true
31}
32
33#[must_use]
34pub fn collision_hazards_sorted_correctly(hazards: &[QTHazard]) -> bool {
35    let mut partial_hazard_detected = false;
36    for hazard in hazards {
37        match hazard.presence {
38            QTHazPresence::Partial(_) => {
39                partial_hazard_detected = true;
40            }
41            QTHazPresence::Entire => {
42                if partial_hazard_detected {
43                    return false;
44                }
45            }
46            QTHazPresence::None => {
47                panic!("None hazard should never be collision hazard vec");
48            }
49        }
50    }
51    true
52}
53
54#[must_use]
55pub fn qt_contains_no_dangling_hazards(cde: &CDEngine) -> bool {
56    if let Some(children) = &cde.quadtree.children {
57        for child in children.as_ref() {
58            if !qt_node_contains_no_dangling_hazards(child, &cde.quadtree) {
59                return false;
60            }
61        }
62    }
63    true
64}
65
66fn qt_node_contains_no_dangling_hazards(node: &QTNode, parent: &QTNode) -> bool {
67    let parent_h_entities = parent
68        .hazards
69        .iter()
70        .map(|h| &h.entity)
71        .unique()
72        .collect_vec();
73
74    let dangling_hazards = node
75        .hazards
76        .iter()
77        .any(|h| !parent_h_entities.contains(&&h.entity));
78    if dangling_hazards {
79        println!("Node contains dangling hazard");
80        return false;
81    }
82
83    if let Some(children) = &node.children {
84        for child in children.as_ref() {
85            if !qt_node_contains_no_dangling_hazards(child, node) {
86                return false;
87            }
88        }
89    }
90
91    true
92}
93
94#[must_use]
95pub fn layout_qt_matches_fresh_qt(layout: &Layout) -> bool {
96    //check if every placed item is correctly represented in the quadtree
97
98    //rebuild the quadtree
99    let container = &layout.container;
100    let mut fresh_cde = container.base_cde.as_ref().clone();
101    for (pk, pi) in &layout.placed_items {
102        let hazard = Hazard::new((pk, pi).into(), pi.shape.clone(), true);
103        fresh_cde.register_hazard(hazard);
104    }
105
106    qt_nodes_match(Some(&layout.cde().quadtree), Some(&fresh_cde.quadtree))
107        && hazards_match(layout.cde().hazards(), fresh_cde.hazards())
108}
109
110fn qt_nodes_match(qn1: Option<&QTNode>, qn2: Option<&QTNode>) -> bool {
111    let hashable = |h: &QTHazard| {
112        let p_sk = match h.presence {
113            QTHazPresence::None => 0,
114            QTHazPresence::Partial(_) => 1,
115            QTHazPresence::Entire => 2,
116        };
117        (h.entity, p_sk)
118    };
119    match (qn1, qn2) {
120        (Some(qn1), Some(qn2)) => {
121            //if both nodes exist
122            let hv1 = &qn1.hazards;
123            let hv2 = &qn2.hazards;
124
125            //collect active hazards to hashsets
126            let active_haz_1 = hv1
127                .iter()
128                .map(hashable)
129                .collect::<HashSet<(HazardEntity, u8)>>();
130
131            let active_haz_2 = hv2
132                .iter()
133                .map(hashable)
134                .collect::<HashSet<(HazardEntity, u8)>>();
135
136            let active_in_1_but_not_2 = active_haz_1
137                .difference(&active_haz_2)
138                .collect::<HashSet<_>>();
139            let active_in_2_but_not_1 = active_haz_2
140                .difference(&active_haz_1)
141                .collect::<HashSet<_>>();
142
143            if !(active_in_1_but_not_2.is_empty() && active_in_2_but_not_1.is_empty()) {
144                let from_1 = **active_in_1_but_not_2.iter().next().unwrap();
145                let from_2 = **active_in_2_but_not_1.iter().next().unwrap();
146                println!("{}", from_1 == from_2);
147                error!(
148                    "Active hazards don't match {active_in_1_but_not_2:?} vs {active_in_2_but_not_1:?}"
149                );
150                return false;
151            }
152        }
153        (Some(qn1), None) => {
154            if qn1.hazards.iter().next().is_some() {
155                error!("qn1 contains active hazards while other qn2 does not exist");
156                return false;
157            }
158        }
159        (None, Some(qn2)) => {
160            if qn2.hazards.iter().next().is_some() {
161                error!("qn2 contains active hazards while other qn1 does not exist");
162                return false;
163            }
164        }
165        (None, None) => panic!("Both nodes are none"),
166    }
167
168    //Check children
169    match (
170        qn1.map_or(&None, |qn| &qn.children),
171        qn2.map_or(&None, |qn| &qn.children),
172    ) {
173        (None, None) => true,
174        (Some(c1), None) => {
175            let qn1_has_partial_hazards = qn1.is_some_and(|qn| {
176                qn.hazards
177                    .iter()
178                    .any(|h| matches!(h.presence, QTHazPresence::Partial(_)))
179            });
180            if qn1_has_partial_hazards {
181                for child in c1.as_ref() {
182                    if !qt_nodes_match(Some(child), None) {
183                        return false;
184                    }
185                }
186            }
187            true
188        }
189        (None, Some(c2)) => {
190            let qn2_has_partial_hazards = qn2.is_some_and(|qn| {
191                qn.hazards
192                    .iter()
193                    .any(|h| matches!(h.presence, QTHazPresence::Partial(_)))
194            });
195            if qn2_has_partial_hazards {
196                for child in c2.as_ref() {
197                    if !qt_nodes_match(None, Some(child)) {
198                        return false;
199                    }
200                }
201            }
202            true
203        }
204        (Some(c1), Some(c2)) => {
205            for (child1, child2) in c1.as_ref().iter().zip(c2.as_ref().iter()) {
206                if !qt_nodes_match(Some(child1), Some(child2)) {
207                    return false;
208                }
209            }
210            true
211        }
212    }
213}
214
215fn hazards_match<'a>(
216    chv1: impl Iterator<Item = &'a Hazard>,
217    chv2: impl Iterator<Item = &'a Hazard>,
218) -> bool {
219    let chv1_active_hazards = chv1.map(|h| h.entity).collect::<HashSet<_>>();
220
221    let chv2_active_hazards = chv2.map(|h| h.entity).collect::<HashSet<_>>();
222
223    if chv1_active_hazards != chv2_active_hazards {
224        println!("Hazard vecs don't match");
225        return false;
226    }
227    true
228}
229
230/// Checks if the quadrants follow the layout set in [`Rect::QUADRANT_NEIGHBOR_LAYOUT`]
231#[must_use]
232pub fn quadrants_have_valid_layout(quadrants: &[Rect; 4]) -> bool {
233    let layout = Rect::QUADRANT_NEIGHBOR_LAYOUT;
234    for (idx, q) in quadrants.iter().enumerate() {
235        //make sure they share two points (an edge) with each neighbor
236        let [n_0, n_1] = layout[idx];
237        let q_corners = q.corners();
238        let n_0_corners = quadrants[n_0].corners();
239        let n_1_corners = quadrants[n_1].corners();
240
241        assert_eq!(
242            2,
243            n_0_corners
244                .iter()
245                .filter(|c| q_corners.iter().any(|qc| &qc == c))
246                .count()
247        );
248        assert_eq!(
249            2,
250            n_1_corners
251                .iter()
252                .filter(|c| q_corners.iter().any(|qc| &qc == c))
253                .count()
254        );
255    }
256    true
257}
258
259///Prints code to rebuild a layout. Intended for debugging purposes.
260pub fn print_layout(layout: &Layout) {
261    println!(
262        "let mut layout = Layout::new(0, instance.container({}).clone());",
263        layout.container.id
264    );
265    println!();
266
267    for pi in layout.placed_items.values() {
268        let transformation_str = {
269            let t_decomp = &pi.d_transf;
270            let (tr, (tx, ty)) = (t_decomp.rotation(), t_decomp.translation());
271            format!("&DTransformation::new({tr:.6},({tx:.6},{ty:.6}))")
272        };
273
274        println!(
275            "layout.place_item(instance.item({}), {});",
276            pi.item_id, transformation_str
277        );
278    }
279}