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}