parry3d/shape/voxels/
voxels_edition.rs

1use crate::bounding_volume::Aabb;
2use crate::math::{Point, Real, Vector, DIM};
3use crate::shape::voxels::voxels_chunk::{VoxelsChunk, VoxelsChunkHeader};
4use crate::shape::{VoxelState, Voxels};
5use crate::utils::hashmap::Entry;
6use alloc::vec;
7
8impl Voxels {
9    /// Sets the size of each voxel along each local coordinate axis.
10    ///
11    /// Since the internal spatial acceleration structure needs to be updated, this
12    /// operation runs in `O(n)` time, where `n` is the number of voxels.
13    pub fn set_voxel_size(&mut self, new_size: Vector<Real>) {
14        let scale = new_size.component_div(&self.voxel_size);
15        self.chunk_bvh.scale(&scale);
16        self.voxel_size = new_size;
17    }
18
19    /// Adds or removes a voxel at the specified grid coordinates.
20    ///
21    /// This is the primary method for dynamically modifying a voxel shape. It can:
22    /// - Add a new voxel by setting `is_filled = true`
23    /// - Remove an existing voxel by setting `is_filled = false`
24    ///
25    /// The method automatically updates the neighborhood information for the affected voxel
26    /// and all its neighbors to maintain correct collision detection behavior.
27    ///
28    /// # Returns
29    ///
30    /// The previous [`VoxelState`] of the voxel before modification. This allows you to
31    /// detect whether the operation actually changed anything.
32    ///
33    /// # Examples
34    ///
35    /// ## Adding Voxels
36    ///
37    /// ```
38    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
39    /// use parry3d::shape::Voxels;
40    /// use nalgebra::{Point3, Vector3};
41    ///
42    /// let mut voxels = Voxels::new(Vector3::new(1.0, 1.0, 1.0), &[]);
43    ///
44    /// // Add a voxel at (0, 0, 0)
45    /// let prev_state = voxels.set_voxel(Point3::new(0, 0, 0), true);
46    /// assert!(prev_state.is_empty()); // Was empty before
47    ///
48    /// // Verify it was added
49    /// let state = voxels.voxel_state(Point3::new(0, 0, 0)).unwrap();
50    /// assert!(!state.is_empty());
51    /// # }
52    /// ```
53    ///
54    /// ## Removing Voxels
55    ///
56    /// ```
57    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
58    /// use parry3d::shape::Voxels;
59    /// use nalgebra::{Point3, Vector3};
60    ///
61    /// let mut voxels = Voxels::new(
62    ///     Vector3::new(1.0, 1.0, 1.0),
63    ///     &[Point3::new(0, 0, 0), Point3::new(1, 0, 0)],
64    /// );
65    ///
66    /// // Remove the voxel at (0, 0, 0)
67    /// voxels.set_voxel(Point3::new(0, 0, 0), false);
68    ///
69    /// // Verify it was removed
70    /// let state = voxels.voxel_state(Point3::new(0, 0, 0)).unwrap();
71    /// assert!(state.is_empty());
72    /// # }
73    /// ```
74    ///
75    /// ## Building Shapes Dynamically
76    ///
77    /// ```
78    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
79    /// use parry3d::shape::Voxels;
80    /// use nalgebra::{Point3, Vector3};
81    ///
82    /// let mut voxels = Voxels::new(Vector3::new(1.0, 1.0, 1.0), &[]);
83    ///
84    /// // Build a 3×3 floor
85    /// for x in 0..3 {
86    ///     for z in 0..3 {
87    ///         voxels.set_voxel(Point3::new(x, 0, z), true);
88    ///     }
89    /// }
90    ///
91    /// // Count filled voxels
92    /// let filled = voxels.voxels()
93    ///     .filter(|v| !v.state.is_empty())
94    ///     .count();
95    /// assert_eq!(filled, 9);
96    /// # }
97    /// ```
98    ///
99    /// ## Detecting Changes
100    ///
101    /// ```
102    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
103    /// use parry3d::shape::Voxels;
104    /// use nalgebra::{Point3, Vector3};
105    ///
106    /// let mut voxels = Voxels::new(
107    ///     Vector3::new(1.0, 1.0, 1.0),
108    ///     &[Point3::new(0, 0, 0)],
109    /// );
110    ///
111    /// // Try to add a voxel that already exists
112    /// let prev = voxels.set_voxel(Point3::new(0, 0, 0), true);
113    /// if !prev.is_empty() {
114    ///     println!("Voxel was already filled!");
115    /// }
116    /// # }
117    /// ```
118    pub fn set_voxel(&mut self, key: Point<i32>, is_filled: bool) -> VoxelState {
119        let (chunk_key, id_in_chunk) = Self::chunk_key_and_id_in_chunk(key);
120        let header_entry = self.chunk_headers.entry(chunk_key);
121
122        if !is_filled && matches!(header_entry, Entry::Vacant(_)) {
123            // The voxel is already empty (it doesn’t exist at all).
124            // Nothing more to do.
125            return VoxelState::EMPTY;
126        }
127
128        let chunk_header = header_entry.or_insert_with(|| {
129            let id = self.free_chunks.pop().unwrap_or_else(|| {
130                self.chunks.push(VoxelsChunk::default());
131                self.chunk_keys.push(chunk_key);
132                self.chunks.len() - 1
133            });
134
135            self.chunk_keys[id] = chunk_key;
136            self.chunk_bvh
137                .insert(VoxelsChunk::aabb(&chunk_key, &self.voxel_size), id as u32);
138            VoxelsChunkHeader { id, len: 0 }
139        });
140        let chunk_id = chunk_header.id;
141
142        let prev = self.chunks[chunk_id].states[id_in_chunk];
143        let new_is_empty = !is_filled;
144
145        if prev.is_empty() ^ new_is_empty {
146            let can_remove_chunk = if new_is_empty {
147                chunk_header.len -= 1;
148                chunk_header.len == 0
149            } else {
150                chunk_header.len += 1;
151                false
152            };
153
154            self.chunks[chunk_id].states[id_in_chunk] =
155                self.update_neighbors_state(key, new_is_empty);
156
157            if can_remove_chunk {
158                self.chunk_bvh.remove(chunk_id as u32);
159
160                #[cfg(feature = "enhanced-determinism")]
161                let _ = self.chunk_headers.swap_remove(&chunk_key);
162                #[cfg(not(feature = "enhanced-determinism"))]
163                let _ = self.chunk_headers.remove(&chunk_key);
164
165                self.free_chunks.push(chunk_id);
166                self.chunk_keys[chunk_id] = VoxelsChunk::INVALID_CHUNK_KEY;
167            }
168        }
169
170        prev
171    }
172
173    /// Crops the voxel shape in-place to a rectangular region.
174    ///
175    /// Removes all voxels outside the specified grid coordinate bounds `[domain_mins, domain_maxs]`
176    /// (inclusive on both ends). This is useful for:
177    /// - Extracting a sub-region of a larger voxel world
178    /// - Removing voxels outside a region of interest
179    /// - Implementing chunk-based world management
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
185    /// use parry3d::shape::Voxels;
186    /// use nalgebra::{Point3, Vector3};
187    ///
188    /// let mut voxels = Voxels::new(
189    ///     Vector3::new(1.0, 1.0, 1.0),
190    ///     &[
191    ///         Point3::new(0, 0, 0),
192    ///         Point3::new(1, 0, 0),
193    ///         Point3::new(2, 0, 0),
194    ///         Point3::new(3, 0, 0),
195    ///     ],
196    /// );
197    ///
198    /// // Keep only voxels in the range [1, 2]
199    /// voxels.crop(Point3::new(1, 0, 0), Point3::new(2, 0, 0));
200    ///
201    /// // Only two voxels remain
202    /// let count = voxels.voxels()
203    ///     .filter(|v| !v.state.is_empty())
204    ///     .count();
205    /// assert_eq!(count, 2);
206    /// # }
207    /// ```
208    pub fn crop(&mut self, domain_mins: Point<i32>, domain_maxs: Point<i32>) {
209        // TODO PERF: this could be done more efficiently.
210        if let Some(new_shape) = self.cropped(domain_mins, domain_maxs) {
211            *self = new_shape;
212        }
213    }
214
215    /// Returns a cropped version of this voxel shape with a rectangular domain.
216    ///
217    /// This removes every voxels out of the `[domain_mins, domain_maxs]` bounds.
218    pub fn cropped(&self, domain_mins: Point<i32>, domain_maxs: Point<i32>) -> Option<Self> {
219        // TODO PERF: can be optimized significantly.
220        let mut in_box = vec![];
221        for vox in self.voxels() {
222            if !vox.state.is_empty()
223                && grid_aabb_contains_point(&domain_mins, &domain_maxs, &vox.grid_coords)
224            {
225                in_box.push(vox.grid_coords);
226            }
227        }
228
229        if !in_box.is_empty() {
230            Some(Voxels::new(self.voxel_size, &in_box))
231        } else {
232            None
233        }
234    }
235
236    /// Splits this voxel shape into two separate shapes based on an AABB.
237    ///
238    /// Partitions the voxels into two groups:
239    /// - **Inside**: Voxels whose centers fall inside the given `aabb`
240    /// - **Outside**: All remaining voxels
241    ///
242    /// Returns `(Some(inside), Some(outside))`, or `None` for either if that partition is empty.
243    ///
244    /// # Use Cases
245    ///
246    /// - Spatial partitioning for physics simulation
247    /// - Implementing destructible objects (remove the "inside" part on explosion)
248    /// - Chunk-based world management
249    /// - Level-of-detail systems
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
255    /// use parry3d::shape::Voxels;
256    /// use parry3d::bounding_volume::Aabb;
257    /// use nalgebra::{Point3, Vector3};
258    ///
259    /// let voxels = Voxels::new(
260    ///     Vector3::new(1.0, 1.0, 1.0),
261    ///     &[
262    ///         Point3::new(0, 0, 0),  // Center at (0.5, 0.5, 0.5)
263    ///         Point3::new(2, 0, 0),  // Center at (2.5, 0.5, 0.5)
264    ///         Point3::new(4, 0, 0),  // Center at (4.5, 0.5, 0.5)
265    ///     ],
266    /// );
267    ///
268    /// // Split at X = 3.0
269    /// let split_box = Aabb::new(
270    ///     Point3::new(-10.0, -10.0, -10.0),
271    ///     Point3::new(3.0, 10.0, 10.0),
272    /// );
273    ///
274    /// let (inside, outside) = voxels.split_with_box(&split_box);
275    ///
276    /// // First two voxels inside, last one outside
277    /// assert!(inside.is_some());
278    /// assert!(outside.is_some());
279    /// # }
280    /// ```
281    pub fn split_with_box(&self, aabb: &Aabb) -> (Option<Self>, Option<Self>) {
282        // TODO PERF: can be optimized significantly.
283        let mut in_box = vec![];
284        let mut rest = vec![];
285        for vox in self.voxels() {
286            if !vox.state.is_empty() {
287                if aabb.contains_local_point(&vox.center) {
288                    in_box.push(vox.grid_coords);
289                } else {
290                    rest.push(vox.grid_coords);
291                }
292            }
293        }
294
295        let in_box = if !in_box.is_empty() {
296            Some(Voxels::new(self.voxel_size, &in_box))
297        } else {
298            None
299        };
300
301        let rest = if !rest.is_empty() {
302            Some(Voxels::new(self.voxel_size, &rest))
303        } else {
304            None
305        };
306
307        (in_box, rest)
308    }
309}
310
311fn grid_aabb_contains_point(mins: &Point<i32>, maxs: &Point<i32>, point: &Point<i32>) -> bool {
312    for i in 0..DIM {
313        if point[i] < mins[i] || point[i] > maxs[i] {
314            return false;
315        }
316    }
317
318    true
319}