1#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub struct Position3 {
6 pub x: f32,
8 pub y: f32,
10 pub z: f32,
12}
13
14impl Position3 {
15 pub const fn new(x: f32, y: f32, z: f32) -> Self {
17 Self { x, y, z }
18 }
19
20 pub fn distance_squared(self, other: Self) -> f32 {
22 let dx = self.x - other.x;
23 let dy = self.y - other.y;
24 let dz = self.z - other.z;
25 dx.mul_add(dx, dy.mul_add(dy, dz * dz))
26 }
27
28 pub const fn to_vec3(self) -> Vec3 {
30 Vec3::new(self.x, self.y, self.z)
31 }
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
36pub struct Vec3 {
37 pub x: f32,
39 pub y: f32,
41 pub z: f32,
43}
44
45impl Vec3 {
46 pub const fn new(x: f32, y: f32, z: f32) -> Self {
48 Self { x, y, z }
49 }
50
51 pub fn dot(self, other: Self) -> f32 {
53 self.x
54 .mul_add(other.x, self.y.mul_add(other.y, self.z * other.z))
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct Aabb3 {
61 pub min: Position3,
63 pub max: Position3,
65}
66
67impl Aabb3 {
68 pub const fn new(min: Position3, max: Position3) -> Self {
70 Self { min, max }
71 }
72
73 pub fn from_center_half_extents(center: Position3, half_extents: Vec3) -> Self {
75 Self {
76 min: Position3::new(
77 center.x - half_extents.x,
78 center.y - half_extents.y,
79 center.z - half_extents.z,
80 ),
81 max: Position3::new(
82 center.x + half_extents.x,
83 center.y + half_extents.y,
84 center.z + half_extents.z,
85 ),
86 }
87 }
88
89 pub fn overlaps(self, other: Self) -> bool {
91 self.min.x <= other.max.x
92 && self.max.x >= other.min.x
93 && self.min.y <= other.max.y
94 && self.max.y >= other.min.y
95 && self.min.z <= other.max.z
96 && self.max.z >= other.min.z
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct Plane3 {
103 pub normal: Vec3,
105 pub distance: f32,
107}
108
109impl Plane3 {
110 pub const fn new(normal: Vec3, distance: f32) -> Self {
112 Self { normal, distance }
113 }
114
115 pub fn from_normal_and_point(normal: Vec3, point: Position3) -> Self {
117 Self {
118 normal,
119 distance: -normal.dot(point.to_vec3()),
120 }
121 }
122
123 pub fn signed_distance_to_position(self, position: Position3) -> f32 {
125 self.normal.dot(position.to_vec3()) + self.distance
126 }
127
128 pub fn intersects_aabb(self, aabb: Aabb3) -> bool {
130 let positive = Position3::new(
131 if self.normal.x >= 0.0 {
132 aabb.max.x
133 } else {
134 aabb.min.x
135 },
136 if self.normal.y >= 0.0 {
137 aabb.max.y
138 } else {
139 aabb.min.y
140 },
141 if self.normal.z >= 0.0 {
142 aabb.max.z
143 } else {
144 aabb.min.z
145 },
146 );
147 self.signed_distance_to_position(positive) >= 0.0
148 }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct Frustum3 {
154 pub planes: [Plane3; 6],
156}
157
158impl Frustum3 {
159 pub const fn from_planes(planes: [Plane3; 6]) -> Self {
161 Self { planes }
162 }
163
164 pub fn from_aabb(aabb: Aabb3) -> Self {
166 Self::from_planes([
167 Plane3::new(Vec3::new(1.0, 0.0, 0.0), -aabb.min.x),
168 Plane3::new(Vec3::new(-1.0, 0.0, 0.0), aabb.max.x),
169 Plane3::new(Vec3::new(0.0, 1.0, 0.0), -aabb.min.y),
170 Plane3::new(Vec3::new(0.0, -1.0, 0.0), aabb.max.y),
171 Plane3::new(Vec3::new(0.0, 0.0, 1.0), -aabb.min.z),
172 Plane3::new(Vec3::new(0.0, 0.0, -1.0), aabb.max.z),
173 ])
174 }
175
176 pub fn intersects_aabb(self, aabb: Aabb3) -> bool {
178 self.planes.iter().all(|plane| plane.intersects_aabb(aabb))
179 }
180
181 pub fn intersects_bounds(self, position: Position3, bounds: Bounds) -> bool {
183 self.intersects_aabb(bounds.to_aabb(position))
184 }
185}
186
187#[derive(Clone, Copy, Debug, Default, PartialEq)]
189pub enum Bounds {
190 #[default]
192 Point,
193 Sphere {
195 radius: f32,
197 },
198 Aabb {
200 half_extents: Vec3,
202 },
203}
204
205impl Bounds {
206 pub fn to_aabb(self, position: Position3) -> Aabb3 {
208 match self {
209 Self::Point => Aabb3::new(position, position),
210 Self::Sphere { radius } => {
211 Aabb3::from_center_half_extents(position, Vec3::new(radius, radius, radius))
212 }
213 Self::Aabb { half_extents } => Aabb3::from_center_half_extents(position, half_extents),
214 }
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct CellCoord3 {
221 pub x: i32,
223 pub y: i32,
225 pub z: i32,
227}
228
229impl CellCoord3 {
230 pub const fn new(x: i32, y: i32, z: i32) -> Self {
232 Self { x, y, z }
233 }
234}
235
236#[derive(Clone, Copy, Debug, PartialEq)]
238pub struct GridSpec {
239 cell_size: f32,
240}
241
242impl GridSpec {
243 pub fn new(cell_size: f32) -> Result<Self, GridSpecError> {
245 if cell_size.is_finite() && cell_size > 0.0 {
246 Ok(Self { cell_size })
247 } else {
248 Err(GridSpecError::InvalidCellSize)
249 }
250 }
251
252 pub const fn cell_size(self) -> f32 {
254 self.cell_size
255 }
256
257 pub fn cell_at(self, position: Position3) -> CellCoord3 {
259 let inv = 1.0 / self.cell_size;
260 CellCoord3::new(
261 floor_to_cell(position.x * inv),
262 floor_to_cell(position.y * inv),
263 floor_to_cell(position.z * inv),
264 )
265 }
266
267 pub fn cells_for_aabb(self, aabb: Aabb3) -> Vec<CellCoord3> {
269 let min = self.cell_at(aabb.min);
270 let max = self.cell_at(aabb.max);
271 let capacity = axis_cell_count(min.x, max.x)
272 .saturating_mul(axis_cell_count(min.y, max.y))
273 .saturating_mul(axis_cell_count(min.z, max.z));
274 let mut cells = Vec::with_capacity(capacity);
275
276 for x in min.x..=max.x {
277 for y in min.y..=max.y {
278 for z in min.z..=max.z {
279 cells.push(CellCoord3::new(x, y, z));
280 }
281 }
282 }
283
284 cells
285 }
286
287 pub fn cells_for_bounds(self, position: Position3, bounds: Bounds) -> Vec<CellCoord3> {
289 self.cells_for_aabb(bounds.to_aabb(position))
290 }
291}
292
293#[allow(clippy::cast_possible_truncation)]
294fn floor_to_cell(value: f32) -> i32 {
295 value.floor() as i32
297}
298
299fn axis_cell_count(min: i32, max: i32) -> usize {
300 if max < min {
301 return 0;
302 }
303 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(usize::MAX)
304}
305
306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub enum GridSpecError {
309 InvalidCellSize,
311}
312
313impl core::fmt::Display for GridSpecError {
314 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
315 match self {
316 Self::InvalidCellSize => f.write_str("cell size must be finite and positive"),
317 }
318 }
319}
320
321impl std::error::Error for GridSpecError {}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn frustum_from_aabb_accepts_intersecting_bounds_and_rejects_outside() {
329 let frustum = Frustum3::from_aabb(Aabb3::new(
330 Position3::new(0.0, -10.0, -10.0),
331 Position3::new(50.0, 10.0, 10.0),
332 ));
333
334 assert!(frustum.intersects_bounds(Position3::new(25.0, 0.0, 0.0), Bounds::Point));
335 assert!(frustum.intersects_bounds(
336 Position3::new(55.0, 0.0, 0.0),
337 Bounds::Sphere { radius: 8.0 }
338 ));
339 assert!(!frustum.intersects_bounds(Position3::new(-5.0, 0.0, 0.0), Bounds::Point));
340 assert!(!frustum.intersects_bounds(Position3::new(25.0, 20.0, 0.0), Bounds::Point));
341 }
342}