1#[cfg(not(feature = "std"))]
16use alloc::vec::Vec;
17
18#[derive(Clone, Debug)]
24pub struct ScalarField3D {
25 pub data: Vec<f32>,
27 pub nx: usize,
29 pub ny: usize,
31 pub nz: usize,
33 pub min: (f32, f32, f32),
35 pub max: (f32, f32, f32),
37 cell_size: (f32, f32, f32),
39 inv_cell_size: (f32, f32, f32),
41 scratch: Vec<f32>,
43}
44
45impl ScalarField3D {
46 #[must_use]
48 pub fn new(
49 nx: usize,
50 ny: usize,
51 nz: usize,
52 min: (f32, f32, f32),
53 max: (f32, f32, f32),
54 ) -> Self {
55 let cx = if nx > 1 {
56 (max.0 - min.0) / (nx - 1) as f32
57 } else {
58 1.0
59 };
60 let cy = if ny > 1 {
61 (max.1 - min.1) / (ny - 1) as f32
62 } else {
63 1.0
64 };
65 let cz = if nz > 1 {
66 (max.2 - min.2) / (nz - 1) as f32
67 } else {
68 1.0
69 };
70
71 let n = nx * ny * nz;
72 Self {
73 data: vec![0.0; n],
74 nx,
75 ny,
76 nz,
77 min,
78 max,
79 cell_size: (cx, cy, cz),
80 inv_cell_size: (1.0 / cx, 1.0 / cy, 1.0 / cz),
81 scratch: vec![0.0; n],
82 }
83 }
84
85 #[must_use]
87 pub fn new_filled(
88 nx: usize,
89 ny: usize,
90 nz: usize,
91 min: (f32, f32, f32),
92 max: (f32, f32, f32),
93 value: f32,
94 ) -> Self {
95 let mut field = Self::new(nx, ny, nz, min, max);
96 field.data.fill(value);
97 field
98 }
99
100 #[inline]
102 #[must_use]
103 pub const fn cell_count(&self) -> usize {
104 self.nx * self.ny * self.nz
105 }
106
107 #[inline]
109 #[must_use]
110 pub const fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
111 iz * self.ny * self.nx + iy * self.nx + ix
112 }
113
114 #[inline]
116 #[must_use]
117 pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f32 {
118 let ix = ix.min(self.nx - 1);
119 let iy = iy.min(self.ny - 1);
120 let iz = iz.min(self.nz - 1);
121 self.data[self.index(ix, iy, iz)]
122 }
123
124 #[inline]
126 pub fn set(&mut self, ix: usize, iy: usize, iz: usize, value: f32) {
127 if ix < self.nx && iy < self.ny && iz < self.nz {
128 let idx = self.index(ix, iy, iz);
129 self.data[idx] = value;
130 }
131 }
132
133 #[inline]
135 pub fn add(&mut self, ix: usize, iy: usize, iz: usize, delta: f32) {
136 if ix < self.nx && iy < self.ny && iz < self.nz {
137 let idx = self.index(ix, iy, iz);
138 self.data[idx] += delta;
139 }
140 }
141
142 #[inline]
144 fn world_to_grid(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
145 let gx = (x - self.min.0) * self.inv_cell_size.0;
146 let gy = (y - self.min.1) * self.inv_cell_size.1;
147 let gz = (z - self.min.2) * self.inv_cell_size.2;
148 (gx, gy, gz)
149 }
150
151 #[inline]
153 #[must_use]
154 pub fn sample(&self, x: f32, y: f32, z: f32) -> f32 {
155 let (gx, gy, gz) = self.world_to_grid(x, y, z);
156
157 let max_x = (self.nx - 1) as f32;
159 let max_y = (self.ny - 1) as f32;
160 let max_z = (self.nz - 1) as f32;
161 let gx = crate::det_math::min(crate::det_math::max(gx, 0.0), max_x);
162 let gy = crate::det_math::min(crate::det_math::max(gy, 0.0), max_y);
163 let gz = crate::det_math::min(crate::det_math::max(gz, 0.0), max_z);
164
165 let ix0 = gx as usize;
166 let iy0 = gy as usize;
167 let iz0 = gz as usize;
168 let ix1 = (ix0 + 1).min(self.nx - 1);
169 let iy1 = (iy0 + 1).min(self.ny - 1);
170 let iz1 = (iz0 + 1).min(self.nz - 1);
171
172 let fx = gx - ix0 as f32;
173 let fy = gy - iy0 as f32;
174 let fz = gz - iz0 as f32;
175
176 let nx = self.nx;
178 let ny_nx = self.ny * nx;
179 let d = &self.data;
180 let i000 = iz0 * ny_nx + iy0 * nx + ix0;
181 let i100 = iz0 * ny_nx + iy0 * nx + ix1;
182 let i010 = iz0 * ny_nx + iy1 * nx + ix0;
183 let i110 = iz0 * ny_nx + iy1 * nx + ix1;
184 let i001 = iz1 * ny_nx + iy0 * nx + ix0;
185 let i101 = iz1 * ny_nx + iy0 * nx + ix1;
186 let i011 = iz1 * ny_nx + iy1 * nx + ix0;
187 let i111 = iz1 * ny_nx + iy1 * nx + ix1;
188
189 let c00 = (d[i100] - d[i000]).mul_add(fx, d[i000]);
190 let c10 = (d[i110] - d[i010]).mul_add(fx, d[i010]);
191 let c01 = (d[i101] - d[i001]).mul_add(fx, d[i001]);
192 let c11 = (d[i111] - d[i011]).mul_add(fx, d[i011]);
193
194 let c0 = (c10 - c00).mul_add(fy, c00);
195 let c1 = (c11 - c01).mul_add(fy, c01);
196
197 (c1 - c0).mul_add(fz, c0)
198 }
199
200 #[inline]
202 #[must_use]
203 pub fn gradient(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
204 let eps_x = self.cell_size.0 * 0.5;
205 let eps_y = self.cell_size.1 * 0.5;
206 let eps_z = self.cell_size.2 * 0.5;
207
208 let inv_2ex = self.inv_cell_size.0;
210 let inv_2ey = self.inv_cell_size.1;
211 let inv_2ez = self.inv_cell_size.2;
212
213 let dx = (self.sample(x + eps_x, y, z) - self.sample(x - eps_x, y, z)) * inv_2ex;
214 let dy = (self.sample(x, y + eps_y, z) - self.sample(x, y - eps_y, z)) * inv_2ey;
215 let dz = (self.sample(x, y, z + eps_z) - self.sample(x, y, z - eps_z)) * inv_2ez;
216
217 (dx, dy, dz)
218 }
219
220 pub fn splat(&mut self, x: f32, y: f32, z: f32, value: f32, radius: f32) {
222 let (gx, gy, gz) = self.world_to_grid(x, y, z);
223 let gr_x = radius * self.inv_cell_size.0;
225 let gr_y = radius * self.inv_cell_size.1;
226 let gr_z = radius * self.inv_cell_size.2;
227
228 let ix_min = ((gx - gr_x).floor() as i32).max(0) as usize;
229 let ix_max = ((gx + gr_x).ceil() as i32).min(self.nx as i32 - 1) as usize;
230 let iy_min = ((gy - gr_y).floor() as i32).max(0) as usize;
231 let iy_max = ((gy + gr_y).ceil() as i32).min(self.ny as i32 - 1) as usize;
232 let iz_min = ((gz - gr_z).floor() as i32).max(0) as usize;
233 let iz_max = ((gz + gr_z).ceil() as i32).min(self.nz as i32 - 1) as usize;
234
235 let inv_r = if radius > 1e-10 { 1.0 / radius } else { 1.0 };
236 let radius_sq = radius * radius;
237
238 for iz in iz_min..=iz_max {
239 for iy in iy_min..=iy_max {
240 for ix in ix_min..=ix_max {
241 let wx = (ix as f32).mul_add(self.cell_size.0, self.min.0);
242 let wy = (iy as f32).mul_add(self.cell_size.1, self.min.1);
243 let wz = (iz as f32).mul_add(self.cell_size.2, self.min.2);
244
245 let dx = wx - x;
246 let dy = wy - y;
247 let dz = wz - z;
248 let dist_sq = dz.mul_add(dz, dx.mul_add(dx, dy * dy));
249
250 if dist_sq < radius_sq {
252 let dist = dist_sq.sqrt();
253 let t = dist.mul_add(-inv_r, 1.0);
255 let weight = t * t * 2.0f32.mul_add(-t, 3.0); let idx = self.index(ix, iy, iz);
257 self.data[idx] += value * weight;
258 }
259 }
260 }
261 }
262 }
263
264 pub fn diffuse(&mut self, dt: f32, rate: f32) {
275 let (ix, iy, iz) = self.inv_cell_size;
276 let number = rate * dt * 2.0 * (ix * ix + iy * iy + iz * iz);
277 if !number.is_finite() || number <= 0.0 {
278 return;
279 }
280 let steps = if number <= 1.0 {
282 1
283 } else {
284 ((number * 2.0).ceil() as u32).clamp(1, 1_000_000)
285 };
286 let sub_dt = dt / steps as f32;
287 for _ in 0..steps {
288 self.diffuse_step(sub_dt, rate);
289 }
290 }
291
292 fn diffuse_step(&mut self, dt: f32, rate: f32) {
294 let inv_dx2 = self.inv_cell_size.0 * self.inv_cell_size.0;
295 let inv_dy2 = self.inv_cell_size.1 * self.inv_cell_size.1;
296 let inv_dz2 = self.inv_cell_size.2 * self.inv_cell_size.2;
297
298 let nx = self.nx;
299 let ny = self.ny;
300 let nz = self.nz;
301 let ny_nx = ny * nx;
302 let rate_dt = rate * dt;
303
304 for iz in 0..nz {
305 let iz_off = iz * ny_nx;
306 for iy in 0..ny {
307 let iy_off = iz_off + iy * nx;
308 for ix in 0..nx {
309 let idx = iy_off + ix;
310 let c = self.data[idx];
311
312 let xm = if ix > 0 { self.data[idx - 1] } else { c };
313 let xp = if ix + 1 < nx { self.data[idx + 1] } else { c };
314 let ym = if iy > 0 { self.data[idx - nx] } else { c };
315 let yp = if iy + 1 < ny { self.data[idx + nx] } else { c };
316 let zm = if iz > 0 { self.data[idx - ny_nx] } else { c };
317 let zp = if iz + 1 < nz {
318 self.data[idx + ny_nx]
319 } else {
320 c
321 };
322
323 let laplacian = 2.0f32.mul_add(-c, zm + zp).mul_add(
324 inv_dz2,
325 2.0f32
326 .mul_add(-c, xm + xp)
327 .mul_add(inv_dx2, 2.0f32.mul_add(-c, ym + yp) * inv_dy2),
328 );
329
330 self.scratch[idx] = rate_dt.mul_add(laplacian, c);
331 }
332 }
333 }
334
335 std::mem::swap(&mut self.data, &mut self.scratch);
336 }
337
338 pub fn decay(&mut self, rate: f32, dt: f32) {
340 let factor = crate::det_math::exp(-rate * dt);
341 for v in &mut self.data {
342 *v *= factor;
343 }
344 }
345
346 pub fn decay_toward(&mut self, target: f32, rate: f32, dt: f32) {
348 let factor = crate::det_math::exp(-rate * dt);
349 for v in &mut self.data {
350 *v = (*v - target).mul_add(factor, target);
351 }
352 }
353
354 pub fn clamp(&mut self, lo: f32, hi: f32) {
356 for v in &mut self.data {
357 if *v < lo {
358 *v = lo;
359 }
360 if *v > hi {
361 *v = hi;
362 }
363 }
364 }
365
366 pub fn clear(&mut self) {
368 self.data.fill(0.0);
369 }
370
371 pub fn max_value(&self) -> f32 {
373 self.data
374 .iter()
375 .copied()
376 .fold(f32::NEG_INFINITY, crate::det_math::max)
377 }
378
379 #[inline]
381 #[must_use]
382 pub fn contains(&self, x: f32, y: f32, z: f32) -> bool {
383 x >= self.min.0
384 && x <= self.max.0
385 && y >= self.min.1
386 && y <= self.max.1
387 && z >= self.min.2
388 && z <= self.max.2
389 }
390}
391
392#[derive(Clone, Debug)]
398pub struct VectorField3D {
399 pub x: ScalarField3D,
401 pub y: ScalarField3D,
403 pub z: ScalarField3D,
405}
406
407impl VectorField3D {
408 #[must_use]
410 pub fn new(
411 nx: usize,
412 ny: usize,
413 nz: usize,
414 min: (f32, f32, f32),
415 max: (f32, f32, f32),
416 ) -> Self {
417 Self {
418 x: ScalarField3D::new(nx, ny, nz, min, max),
419 y: ScalarField3D::new(nx, ny, nz, min, max),
420 z: ScalarField3D::new(nx, ny, nz, min, max),
421 }
422 }
423
424 #[must_use]
426 pub fn sample(&self, wx: f32, wy: f32, wz: f32) -> (f32, f32, f32) {
427 (
428 self.x.sample(wx, wy, wz),
429 self.y.sample(wx, wy, wz),
430 self.z.sample(wx, wy, wz),
431 )
432 }
433
434 #[allow(clippy::too_many_arguments)]
436 pub fn splat(&mut self, wx: f32, wy: f32, wz: f32, vx: f32, vy: f32, vz: f32, radius: f32) {
437 self.x.splat(wx, wy, wz, vx, radius);
438 self.y.splat(wx, wy, wz, vy, radius);
439 self.z.splat(wx, wy, wz, vz, radius);
440 }
441
442 pub fn decay(&mut self, rate: f32, dt: f32) {
444 self.x.decay(rate, dt);
445 self.y.decay(rate, dt);
446 self.z.decay(rate, dt);
447 }
448
449 pub fn clear(&mut self) {
451 self.x.clear();
452 self.y.clear();
453 self.z.clear();
454 }
455}
456
457#[cfg(test)]
462#[allow(clippy::float_cmp)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn test_scalar_field_get_set() {
468 let mut field = ScalarField3D::new(4, 4, 4, (0.0, 0.0, 0.0), (3.0, 3.0, 3.0));
469 field.set(1, 2, 3, 42.0);
470 assert_eq!(field.get(1, 2, 3), 42.0);
471 assert_eq!(field.get(0, 0, 0), 0.0);
472 }
473
474 #[test]
475 fn test_trilinear_interpolation() {
476 let mut field = ScalarField3D::new(2, 2, 2, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
477 field.set(0, 0, 0, 0.0);
479 field.set(1, 0, 0, 10.0);
480 field.set(0, 1, 0, 0.0);
481 field.set(1, 1, 0, 10.0);
482 field.set(0, 0, 1, 0.0);
483 field.set(1, 0, 1, 10.0);
484 field.set(0, 1, 1, 0.0);
485 field.set(1, 1, 1, 10.0);
486
487 let mid = field.sample(0.5, 0.0, 0.0);
489 assert!((mid - 5.0).abs() < 0.01, "Expected ~5.0, got {mid}");
490
491 let corner = field.sample(1.0, 0.0, 0.0);
493 assert!((corner - 10.0).abs() < 0.01, "Expected ~10.0, got {corner}");
494 }
495
496 #[test]
497 fn test_splat() {
498 let mut field = ScalarField3D::new(8, 8, 8, (-1.0, -1.0, -1.0), (1.0, 1.0, 1.0));
499 field.splat(0.0, 0.0, 0.0, 1.0, 0.5);
500
501 let center = field.sample(0.0, 0.0, 0.0);
503 assert!(
504 center > 0.5,
505 "Center should be high after splat, got {center}"
506 );
507
508 let far = field.sample(1.0, 1.0, 1.0);
510 assert!(far < 0.01, "Far corner should be near zero, got {far}");
511 }
512
513 #[test]
514 fn test_diffusion() {
515 let mut field = ScalarField3D::new(8, 8, 8, (-1.0, -1.0, -1.0), (1.0, 1.0, 1.0));
516 field.set(4, 4, 4, 100.0);
518
519 let initial_max = field.max_value();
520
521 for _ in 0..10 {
523 field.diffuse(0.01, 1.0);
524 }
525
526 let after_max = field.max_value();
528 assert!(after_max < initial_max, "Diffusion should spread heat");
529
530 let neighbor = field.get(3, 4, 4);
532 assert!(neighbor > 0.0, "Neighbor should have received heat");
533 }
534
535 #[test]
536 fn test_gradient() {
537 let mut field = ScalarField3D::new(8, 8, 8, (-1.0, -1.0, -1.0), (1.0, 1.0, 1.0));
538 for ix in 0..8 {
540 for iy in 0..8 {
541 for iz in 0..8 {
542 let x = (ix as f32).mul_add(2.0 / 7.0, -1.0);
543 field.set(ix, iy, iz, x);
544 }
545 }
546 }
547
548 let (gx, gy, gz) = field.gradient(0.0, 0.0, 0.0);
549 assert!(
550 (gx - 1.0).abs() < 0.2,
551 "X gradient should be ~1.0, got {gx}"
552 );
553 assert!(gy.abs() < 0.1, "Y gradient should be ~0, got {gy}");
554 assert!(gz.abs() < 0.1, "Z gradient should be ~0, got {gz}");
555 }
556
557 #[test]
558 fn test_decay() {
559 let mut field = ScalarField3D::new(4, 4, 4, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
560 field.set(1, 1, 1, 100.0);
561
562 field.decay(1.0, 1.0); let v = field.get(1, 1, 1);
564 assert!(
565 (v - 36.8).abs() < 1.0,
566 "After decay, expected ~36.8, got {v}"
567 );
568 }
569}