Skip to main content

jagua_rs/entities/
layout.rs

1use crate::collision_detection::hazards::Hazard;
2use crate::collision_detection::hazards::filter::QualityZoneFilter;
3use crate::collision_detection::{CDESnapshot, CDEngine};
4use crate::entities::Container;
5use crate::entities::Item;
6use crate::entities::{PItemKey, PlacedItem};
7use crate::geometry::DTransformation;
8use crate::util::assertions;
9use itertools::Itertools;
10use slotmap::SlotMap;
11use std::sync::Arc;
12
13/// A [`Layout`] is a dynamic representation of items that have been placed in a container at specific positions.
14/// Items can be placed and removed. The container can be swapped. Snapshots can be taken and restored to.
15/// Each layout maintains a [`CDEngine`], which can be used to check for collisions before placing items.
16#[derive(Clone)]
17pub struct Layout {
18    /// The container used for this layout
19    pub(crate) container: Container,
20    /// All the items that have been placed in this layout, indexed by a unique key
21    pub(crate) placed_items: SlotMap<PItemKey, PlacedItem>,
22    /// The collision detection engine for this layout
23    cde: CDEngine,
24}
25
26impl Layout {
27    #[must_use]
28    pub fn new(container: Container) -> Self {
29        let cde = container.base_cde.as_ref().clone();
30        Layout {
31            container,
32            placed_items: SlotMap::with_key(),
33            cde,
34        }
35    }
36
37    #[must_use]
38    pub fn from_snapshot(ls: &LayoutSnapshot) -> Self {
39        let mut layout = Layout::new(ls.container.clone());
40        layout
41            .restore(ls)
42            .expect("snapshot shares the container's static base");
43        layout
44    }
45
46    /// Replaces the current container with a new one, rebuilding the collision detection engine accordingly.
47    pub fn swap_container(&mut self, container: Container) {
48        let cde_snapshot = self.cde.save();
49        // rebuild the CDE
50        self.container = container;
51        self.cde = self.container.base_cde.as_ref().clone();
52        for hazard in cde_snapshot.dynamic_hazards {
53            // re-register all dynamic hazards from the previous CDE snapshot
54            self.cde.register_hazard(hazard);
55        }
56    }
57
58    /// Saves the current state of the layout to be potentially restored to later.
59    #[must_use]
60    pub fn save(&self) -> LayoutSnapshot {
61        LayoutSnapshot {
62            container: self.container.clone(),
63            placed_items: self.placed_items.clone(),
64            cde_snapshot: self.cde.save(),
65        }
66    }
67
68    /// Restores the layout from a snapshot.
69    ///
70    /// # Errors
71    /// Returns [`ContainerMismatch`] if the snapshot uses a different container,
72    /// leaving the layout unchanged.
73    /// Use [`Layout::swap_container`] first or [`Layout::from_snapshot`] to
74    /// restore a snapshot with a different container.
75    pub fn restore(&mut self, layout_snapshot: &LayoutSnapshot) -> Result<(), ContainerMismatch> {
76        if !Arc::ptr_eq(
77            &self.container.base_cde,
78            &layout_snapshot.container.base_cde,
79        ) {
80            return Err(ContainerMismatch);
81        }
82        self.container.clone_from(&layout_snapshot.container);
83
84        self.placed_items.clone_from(&layout_snapshot.placed_items);
85        self.cde.restore(&layout_snapshot.cde_snapshot);
86
87        debug_assert!(assertions::layout_cde_matches_fresh(self));
88        debug_assert!(assertions::snapshot_matches_layout(self, layout_snapshot));
89        Ok(())
90    }
91
92    /// Places an item in the layout at a specific position by applying a transformation.
93    /// Returns the unique key for the placed item.
94    /// Does not check collisions or orientation permissions before placement.
95    pub fn place_item(&mut self, item: &Arc<Item>, d_transformation: DTransformation) -> PItemKey {
96        let pk = self
97            .placed_items
98            .insert(PlacedItem::new(item, d_transformation));
99        let pi = &self.placed_items[pk];
100        let hazard = Hazard::new((pk, pi).into(), pi.shape.clone());
101
102        self.cde.register_hazard(hazard);
103
104        debug_assert!(assertions::layout_cde_matches_fresh(self));
105
106        pk
107    }
108
109    /// Removes an item from the layout by its unique key and returns the removed [`PlacedItem`].
110    pub fn remove_item(&mut self, pk: PItemKey) -> PlacedItem {
111        let pi = self
112            .placed_items
113            .remove(pk)
114            .expect("key is not valid anymore");
115
116        // update the collision detection engine
117        self.cde.deregister_hazard_by_entity((pk, &pi).into());
118
119        debug_assert!(assertions::layout_cde_matches_fresh(self));
120
121        pi
122    }
123
124    /// True if no items are placed
125    #[must_use]
126    pub fn is_empty(&self) -> bool {
127        self.placed_items.is_empty()
128    }
129
130    /// The current density of the layout defined as the ratio of the area of the items placed to the area of the container.
131    /// Uses the original shapes of items and container to calculate the area.
132    #[must_use]
133    pub fn density(&self) -> f32 {
134        self.placed_item_area() / self.container.area()
135    }
136
137    /// The sum of the areas of the items placed in the layout (using the original shapes of the items).
138    #[must_use]
139    pub fn placed_item_area(&self) -> f32 {
140        self.placed_items
141            .values()
142            .map(|pi| pi.item.area())
143            .sum::<f32>()
144    }
145
146    /// Distinct item types present in this layout.
147    pub fn items(&self) -> impl Iterator<Item = &Item> {
148        self.placed_items
149            .values()
150            .map(|pi| pi.item.as_ref())
151            .unique_by(|item| item.idx)
152    }
153
154    /// Returns the collision detection engine for this layout
155    #[must_use]
156    pub fn cde(&self) -> &CDEngine {
157        &self.cde
158    }
159
160    /// Returns true if no placed item collides with the container or another item.
161    /// Quality zones meeting each item's minimum quality are ignored; holes remain blocking.
162    #[must_use]
163    pub fn is_collision_free(&self) -> bool {
164        self.placed_items.iter().all(|(pk, pi)| {
165            let hkey = self
166                .cde
167                .haz_key(&(pk, pi).into())
168                .expect("all placed items should be registered in the CDE");
169            match pi.item.min_quality {
170                Some(min_quality) => {
171                    let filter = (QualityZoneFilter { min_quality }, hkey);
172                    !self.cde.detect_poly_collision(&pi.shape, &filter)
173                }
174                None => !self.cde.detect_poly_collision(&pi.shape, &hkey),
175            }
176        })
177    }
178
179    /// The container used for this layout
180    #[must_use]
181    pub fn container(&self) -> &Container {
182        &self.container
183    }
184
185    /// All the items that have been placed in this layout, indexed by a unique key
186    #[must_use]
187    pub fn placed_items(&self) -> &SlotMap<PItemKey, PlacedItem> {
188        &self.placed_items
189    }
190}
191
192/// The layout and snapshot do not share the same static collision base.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
194#[error("layout and snapshot have different static collision bases")]
195pub struct ContainerMismatch;
196
197/// Immutable and compact representation of a [`Layout`].
198/// Can be used to restore a [`Layout`] back to a previous state.
199#[derive(Clone, Debug)]
200pub struct LayoutSnapshot {
201    /// A copy of the container used in the layout
202    pub(crate) container: Container,
203    /// A copy of the placed items in the layout
204    pub(crate) placed_items: SlotMap<PItemKey, PlacedItem>,
205    /// Snapshot of the collision detection engine
206    pub(crate) cde_snapshot: CDESnapshot,
207}
208
209impl LayoutSnapshot {
210    /// The container captured in this snapshot.
211    #[must_use]
212    pub fn container(&self) -> &Container {
213        &self.container
214    }
215
216    /// The placements captured in this snapshot.
217    #[must_use]
218    pub fn placed_items(&self) -> &SlotMap<PItemKey, PlacedItem> {
219        &self.placed_items
220    }
221
222    /// Equivalent to [`Layout::density`]
223    #[must_use]
224    pub fn density(&self) -> f32 {
225        self.placed_item_area() / self.container.area()
226    }
227
228    /// Equivalent to [`Layout::placed_item_area`]
229    #[must_use]
230    pub fn placed_item_area(&self) -> f32 {
231        self.placed_items
232            .values()
233            .map(|pi| pi.item.area())
234            .sum::<f32>()
235    }
236}