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
16pub fn layouts_match(layout: &Layout, layout_snapshot: &LayoutSnapshot) -> bool {
17    if layout.container.id != layout_snapshot.container.id {
18        return false;
19    }
20    for placed_item in layout_snapshot.placed_items.values() {
21        if !layout
22            .placed_items()
23            .values()
24            .any(|pi| pi.item_id == placed_item.item_id && pi.d_transf == placed_item.d_transf)
25        {
26            return false;
27        }
28    }
29    //TODO: add dotgrid check, check if quadtree does not contain any more uncommitted removals
30    true
31}
32
33pub fn collision_hazards_sorted_correctly(hazards: &[QTHazard]) -> bool {
34    let mut partial_hazard_detected = false;
35    for hazard in hazards.iter() {
36        match hazard.presence {
37            QTHazPresence::Partial(_) => {
38                partial_hazard_detected = true;
39            }
40            QTHazPresence::Entire => {
41                if partial_hazard_detected {
42                    return false;
43                }
44            }
45            QTHazPresence::None => {
46                panic!("None hazard should never be collision hazard vec");
47            }
48        };
49    }
50    true
51}
52
53pub fn qt_node_contains_no_deactivated_hazards<'a>(
54    node: &'a QTNode,
55    mut stacktrace: Vec<&'a QTNode>,
56) -> (bool, Vec<&'a QTNode>) {
57    stacktrace.push(node);
58    let deactivated_hazard = node.hazards.all_hazards().iter().find(|h| !h.active);
59    if deactivated_hazard.is_some() {
60        println!("Deactivated hazard found");
61        dbg!(&stacktrace);
62        return (false, stacktrace);
63    }
64
65    if let Some(children) = &node.children {
66        for child in children.as_ref() {
67            let result = qt_node_contains_no_deactivated_hazards(child, stacktrace);
68            stacktrace = result.1;
69            let contains_no_deactivated_hazards = result.0;
70            if !contains_no_deactivated_hazards {
71                return (false, stacktrace);
72            }
73        }
74    }
75
76    stacktrace.pop();
77    (true, stacktrace)
78}
79
80pub fn qt_contains_no_dangling_hazards(cde: &CDEngine) -> bool {
81    if let Some(children) = &cde.quadtree().children {
82        for child in children.as_ref() {
83            if !qt_node_contains_no_dangling_hazards(child, cde.quadtree()) {
84                return false;
85            }
86        }
87    }
88    true
89}
90
91fn qt_node_contains_no_dangling_hazards(node: &QTNode, parent: &QTNode) -> bool {
92    let parent_h_entities = parent
93        .hazards
94        .all_hazards()
95        .iter()
96        .map(|h| &h.entity)
97        .unique()
98        .collect_vec();
99
100    let dangling_hazards = node
101        .hazards
102        .all_hazards()
103        .iter()
104        .any(|h| !parent_h_entities.contains(&&h.entity));
105    if dangling_hazards {
106        println!("Node contains dangling hazard");
107        return false;
108    }
109
110    if let Some(children) = &node.children {
111        for child in children.as_ref() {
112            if !qt_node_contains_no_dangling_hazards(child, node) {
113                return false;
114            }
115        }
116    }
117
118    true
119}
120
121pub fn qt_hz_entity_activation_consistent(cde: &CDEngine) -> bool {
122    for (active, hz_entity) in cde
123        .quadtree()
124        .hazards
125        .all_hazards()
126        .iter()
127        .map(|h| (h.active, &h.entity))
128        .unique()
129    {
130        if !hz_entity_same_everywhere(cde.quadtree(), hz_entity, active) {
131            return false;
132        }
133    }
134    true
135}
136
137pub fn hz_entity_same_everywhere(qt_node: &QTNode, hz_entity: &HazardEntity, active: bool) -> bool {
138    if let Some(h) = qt_node
139        .hazards
140        .all_hazards()
141        .iter()
142        .find(|h| &h.entity == hz_entity)
143    {
144        if h.active != active {
145            println!("Hazard entity activation inconsistent");
146            return false;
147        }
148    }
149    if let Some(children) = &qt_node.children {
150        for child in children.as_ref() {
151            if !hz_entity_same_everywhere(child, hz_entity, active) {
152                return false;
153            }
154        }
155    }
156
157    true
158}
159
160pub fn layout_qt_matches_fresh_qt(layout: &Layout) -> bool {
161    //check if every placed item is correctly represented in the quadtree
162
163    //rebuild the quadtree
164    let container = &layout.container;
165    let mut fresh_cde = container.base_cde.as_ref().clone();
166    for (pk, pi) in layout.placed_items().iter() {
167        let hazard = Hazard::new((pk, pi).into(), pi.shape.clone());
168        fresh_cde.register_hazard(hazard);
169    }
170
171    qt_nodes_match(Some(layout.cde().quadtree()), Some(fresh_cde.quadtree()))
172        && hazards_match(layout.cde().dynamic_hazards(), fresh_cde.dynamic_hazards())
173}
174
175fn qt_nodes_match(qn1: Option<&QTNode>, qn2: Option<&QTNode>) -> bool {
176    let hashable = |h: &QTHazard| {
177        let p_sk = match h.presence {
178            QTHazPresence::None => 0,
179            QTHazPresence::Partial(_) => 1,
180            QTHazPresence::Entire => 2,
181        };
182        (h.entity, h.active, p_sk)
183    };
184    match (qn1, qn2) {
185        (Some(qn1), Some(qn2)) => {
186            //if both nodes exist
187            let hv1 = &qn1.hazards;
188            let hv2 = &qn2.hazards;
189
190            //collect active hazards to hashsets
191            let active_haz_1 = hv1
192                .active_hazards()
193                .iter()
194                .map(hashable)
195                .collect::<HashSet<(HazardEntity, bool, u8)>>();
196
197            let active_haz_2 = hv2
198                .active_hazards()
199                .iter()
200                .map(hashable)
201                .collect::<HashSet<(HazardEntity, bool, u8)>>();
202
203            let active_in_1_but_not_2 = active_haz_1
204                .difference(&active_haz_2)
205                .collect::<HashSet<_>>();
206            let active_in_2_but_not_1 = active_haz_2
207                .difference(&active_haz_1)
208                .collect::<HashSet<_>>();
209
210            if !(active_in_1_but_not_2.is_empty() && active_in_2_but_not_1.is_empty()) {
211                let from_1 = **active_in_1_but_not_2.iter().next().unwrap();
212                let from_2 = **active_in_2_but_not_1.iter().next().unwrap();
213                println!("{}", from_1 == from_2);
214                error!(
215                    "Active hazards don't match {active_in_1_but_not_2:?} vs {active_in_2_but_not_1:?}"
216                );
217                return false;
218            }
219        }
220        (Some(qn1), None) => {
221            if qn1.hazards.active_hazards().iter().next().is_some() {
222                error!("qn1 contains active hazards while other qn2 does not exist");
223                return false;
224            }
225        }
226        (None, Some(qn2)) => {
227            if qn2.hazards.active_hazards().iter().next().is_some() {
228                error!("qn2 contains active hazards while other qn1 does not exist");
229                return false;
230            }
231        }
232        (None, None) => panic!("Both nodes are none"),
233    }
234
235    //Check children
236    match (
237        qn1.map_or(&None, |qn| &qn.children),
238        qn2.map_or(&None, |qn| &qn.children),
239    ) {
240        (None, None) => true,
241        (Some(c1), None) => {
242            let qn1_has_partial_hazards = qn1.is_some_and(|qn| {
243                qn.hazards
244                    .active_hazards()
245                    .iter()
246                    .any(|h| matches!(h.presence, QTHazPresence::Partial(_)))
247            });
248            if qn1_has_partial_hazards {
249                for child in c1.as_ref() {
250                    if !qt_nodes_match(Some(child), None) {
251                        return false;
252                    }
253                }
254            }
255            true
256        }
257        (None, Some(c2)) => {
258            let qn2_has_partial_hazards = qn2.is_some_and(|qn| {
259                qn.hazards
260                    .active_hazards()
261                    .iter()
262                    .any(|h| matches!(h.presence, QTHazPresence::Partial(_)))
263            });
264            if qn2_has_partial_hazards {
265                for child in c2.as_ref() {
266                    if !qt_nodes_match(None, Some(child)) {
267                        return false;
268                    }
269                }
270            }
271            true
272        }
273        (Some(c1), Some(c2)) => {
274            for (child1, child2) in c1.as_ref().iter().zip(c2.as_ref().iter()) {
275                if !qt_nodes_match(Some(child1), Some(child2)) {
276                    return false;
277                }
278            }
279            true
280        }
281    }
282}
283
284fn hazards_match(chv1: &[Hazard], chv2: &[Hazard]) -> bool {
285    let chv1_active_hazards = chv1
286        .iter()
287        .filter(|h| h.active)
288        .map(|h| &h.entity)
289        .collect::<HashSet<_>>();
290
291    let chv2_active_hazards = chv2
292        .iter()
293        .filter(|h| h.active)
294        .map(|h| &h.entity)
295        .collect::<HashSet<_>>();
296
297    if chv1_active_hazards != chv2_active_hazards {
298        println!("Hazard vecs don't match");
299        return false;
300    }
301    true
302}
303
304/// Checks if the quadrants follow the layout set in [Rect::QUADRANT_NEIGHBOR_LAYOUT]
305pub fn quadrants_have_valid_layout(quadrants: &[Rect; 4]) -> bool {
306    let layout = Rect::QUADRANT_NEIGHBOR_LAYOUT;
307    for (idx, q) in quadrants.iter().enumerate() {
308        //make sure they share two points (an edge) with each neighbor
309        let [n_0, n_1] = layout[idx];
310        let q_corners = q.corners();
311        let n_0_corners = quadrants[n_0].corners();
312        let n_1_corners = quadrants[n_1].corners();
313
314        assert_eq!(
315            2,
316            n_0_corners
317                .iter()
318                .filter(|c| q_corners.iter().any(|qc| &qc == c))
319                .count()
320        );
321        assert_eq!(
322            2,
323            n_1_corners
324                .iter()
325                .filter(|c| q_corners.iter().any(|qc| &qc == c))
326                .count()
327        );
328    }
329    true
330}
331
332///Prints code to rebuild a layout. Intended for debugging purposes.
333pub fn print_layout(layout: &Layout) {
334    println!(
335        "let mut layout = Layout::new(0, instance.container({}).clone());",
336        layout.container.id
337    );
338    println!();
339
340    for pi in layout.placed_items().values() {
341        let transformation_str = {
342            let t_decomp = &pi.d_transf;
343            let (tr, (tx, ty)) = (t_decomp.rotation(), t_decomp.translation());
344            format!("&DTransformation::new({tr:.6},({tx:.6},{ty:.6}))")
345        };
346
347        println!(
348            "layout.place_item(instance.item({}), {});",
349            pi.item_id, transformation_str
350        );
351    }
352}