1mod dyn_volume;
4mod scalar;
5
6pub use dyn_volume::DynVolume;
7pub use scalar::Scalar;
8
9use glam::{DMat3, DVec3, UVec3};
10use thiserror::Error;
11
12use crate::math::Aabb;
13
14#[derive(Debug, Error, PartialEq)]
18#[non_exhaustive]
19pub enum VolumeError {
20 #[error(
22 "data length {actual} does not match dimensions \
23 {dims_x}×{dims_y}×{dims_z}×{components} = {expected}"
24 )]
25 DimensionMismatch {
26 expected: usize,
28 actual: usize,
30 dims_x: u32,
32 dims_y: u32,
34 dims_z: u32,
36 components: u32,
38 },
39
40 #[error("dimensions must be non-zero, got ({x}, {y}, {z})")]
42 ZeroDimension {
43 x: u32,
45 y: u32,
47 z: u32,
49 },
50
51 #[error("spacing must be positive, got ({x}, {y}, {z})")]
53 InvalidSpacing {
54 x: f64,
56 y: f64,
58 z: f64,
60 },
61
62 #[error("components must be >= 1, got 0")]
64 ZeroComponents,
65
66 #[error("slice {index} has length {actual}, expected {expected} (width × height)")]
68 InconsistentSlice {
69 index: usize,
71 expected: usize,
73 actual: usize,
75 },
76
77 #[error("at least one slice must be provided")]
79 EmptySlices,
80}
81
82pub trait VolumeInfo {
89 fn dimensions(&self) -> UVec3;
91
92 fn spacing(&self) -> DVec3;
94
95 fn origin(&self) -> DVec3;
97
98 fn direction(&self) -> DMat3;
101
102 fn components(&self) -> u32;
104
105 fn index_to_world(&self, ijk: DVec3) -> DVec3 {
109 self.origin() + self.direction() * (ijk * self.spacing())
110 }
111
112 fn world_to_index(&self, xyz: DVec3) -> DVec3 {
116 let rel = xyz - self.origin();
117 let inv = self.direction().transpose();
119 inv * rel / self.spacing()
120 }
121
122 fn world_bounds(&self) -> Aabb {
124 let dims = self.dimensions().as_dvec3();
125 let corners = [
127 DVec3::ZERO,
128 DVec3::new(dims.x - 1.0, 0.0, 0.0),
129 DVec3::new(0.0, dims.y - 1.0, 0.0),
130 DVec3::new(0.0, 0.0, dims.z - 1.0),
131 DVec3::new(dims.x - 1.0, dims.y - 1.0, 0.0),
132 DVec3::new(dims.x - 1.0, 0.0, dims.z - 1.0),
133 DVec3::new(0.0, dims.y - 1.0, dims.z - 1.0),
134 dims - DVec3::ONE,
135 ];
136 let world_corners: Vec<DVec3> = corners.iter().map(|&c| self.index_to_world(c)).collect();
137
138 let min = world_corners
139 .iter()
140 .fold(DVec3::splat(f64::INFINITY), |a, &b| a.min(b));
141 let max = world_corners
142 .iter()
143 .fold(DVec3::splat(f64::NEG_INFINITY), |a, &b| a.max(b));
144 Aabb::new(min, max)
145 }
146}
147
148#[derive(Debug, Clone)]
161pub struct Volume<T: Scalar> {
162 data: Vec<T>,
163 dimensions: UVec3,
164 spacing: DVec3,
165 origin: DVec3,
166 direction: DMat3,
167 components: u32,
168 scalar_range_cache: std::cell::OnceCell<(f64, f64)>,
169}
170
171impl<T: Scalar> Volume<T> {
172 pub fn from_data(
181 data: Vec<T>,
182 dimensions: UVec3,
183 spacing: DVec3,
184 origin: DVec3,
185 direction: DMat3,
186 components: u32,
187 ) -> Result<Self, VolumeError> {
188 Self::validate(dimensions, spacing, components)?;
189 let expected = (dimensions.x as usize)
190 * (dimensions.y as usize)
191 * (dimensions.z as usize)
192 * (components as usize);
193 if data.len() != expected {
194 return Err(VolumeError::DimensionMismatch {
195 expected,
196 actual: data.len(),
197 dims_x: dimensions.x,
198 dims_y: dimensions.y,
199 dims_z: dimensions.z,
200 components,
201 });
202 }
203 Ok(Self {
204 data,
205 dimensions,
206 spacing,
207 origin,
208 direction,
209 components,
210 scalar_range_cache: std::cell::OnceCell::new(),
211 })
212 }
213
214 pub fn from_slices(
222 slices: &[&[T]],
223 width: u32,
224 height: u32,
225 spacing: DVec3,
226 origin: DVec3,
227 direction: DMat3,
228 ) -> Result<Self, VolumeError> {
229 if slices.is_empty() {
230 return Err(VolumeError::EmptySlices);
231 }
232 let expected_len = (width as usize) * (height as usize);
233 for (i, slice) in slices.iter().enumerate() {
234 if slice.len() != expected_len {
235 return Err(VolumeError::InconsistentSlice {
236 index: i,
237 expected: expected_len,
238 actual: slice.len(),
239 });
240 }
241 }
242 let depth = slices.len() as u32;
243 let mut data = Vec::with_capacity(expected_len * slices.len());
244 for slice in slices {
245 data.extend_from_slice(slice);
246 }
247 Self::from_data(
248 data,
249 UVec3::new(width, height, depth),
250 spacing,
251 origin,
252 direction,
253 1,
254 )
255 }
256
257 #[must_use]
263 pub fn get(&self, x: u32, y: u32, z: u32) -> Option<T> {
264 if x >= self.dimensions.x || y >= self.dimensions.y || z >= self.dimensions.z {
265 return None;
266 }
267 let idx = x as usize
268 + y as usize * self.dimensions.x as usize
269 + z as usize * (self.dimensions.x as usize * self.dimensions.y as usize);
270 self.data.get(idx).copied()
271 }
272
273 #[must_use]
278 pub fn sample_linear(&self, ijk: DVec3) -> Option<f64> {
279 let dims = self.dimensions.as_dvec3() - DVec3::ONE;
280 if ijk.x < 0.0 || ijk.y < 0.0 || ijk.z < 0.0 {
281 return None;
282 }
283 if ijk.x > dims.x || ijk.y > dims.y || ijk.z > dims.z {
284 return None;
285 }
286
287 let i0 = ijk.x.floor() as u32;
288 let j0 = ijk.y.floor() as u32;
289 let k0 = ijk.z.floor() as u32;
290 let i1 = (i0 + 1).min(self.dimensions.x - 1);
291 let j1 = (j0 + 1).min(self.dimensions.y - 1);
292 let k1 = (k0 + 1).min(self.dimensions.z - 1);
293
294 let tx = ijk.x - i0 as f64;
295 let ty = ijk.y - j0 as f64;
296 let tz = ijk.z - k0 as f64;
297
298 macro_rules! g {
299 ($x:expr, $y:expr, $z:expr) => {
300 self.get($x, $y, $z).unwrap_or(T::min_value()).to_f64()
301 };
302 }
303
304 let c000 = g!(i0, j0, k0);
305 let c100 = g!(i1, j0, k0);
306 let c010 = g!(i0, j1, k0);
307 let c110 = g!(i1, j1, k0);
308 let c001 = g!(i0, j0, k1);
309 let c101 = g!(i1, j0, k1);
310 let c011 = g!(i0, j1, k1);
311 let c111 = g!(i1, j1, k1);
312
313 let c00 = c000 * (1.0 - tx) + c100 * tx;
314 let c01 = c001 * (1.0 - tx) + c101 * tx;
315 let c10 = c010 * (1.0 - tx) + c110 * tx;
316 let c11 = c011 * (1.0 - tx) + c111 * tx;
317 let c0 = c00 * (1.0 - ty) + c10 * ty;
318 let c1 = c01 * (1.0 - ty) + c11 * ty;
319 Some(c0 * (1.0 - tz) + c1 * tz)
320 }
321
322 #[must_use]
326 pub fn sample_nearest(&self, ijk: DVec3) -> Option<T> {
327 let x = ijk.x.round() as i64;
328 let y = ijk.y.round() as i64;
329 let z = ijk.z.round() as i64;
330 if x < 0 || y < 0 || z < 0 {
331 return None;
332 }
333 self.get(x as u32, y as u32, z as u32)
334 }
335
336 #[must_use]
340 pub fn scalar_range(&self) -> (f64, f64) {
341 *self.scalar_range_cache.get_or_init(|| {
342 self.data
343 .iter()
344 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
345 let f = v.to_f64();
346 (lo.min(f), hi.max(f))
347 })
348 })
349 }
350
351 #[must_use]
353 pub fn as_bytes(&self) -> &[u8] {
354 bytemuck::cast_slice(&self.data)
355 }
356
357 #[must_use]
359 pub fn data(&self) -> &[T] {
360 &self.data
361 }
362
363 fn validate(dimensions: UVec3, spacing: DVec3, components: u32) -> Result<(), VolumeError> {
366 if dimensions.x == 0 || dimensions.y == 0 || dimensions.z == 0 {
367 return Err(VolumeError::ZeroDimension {
368 x: dimensions.x,
369 y: dimensions.y,
370 z: dimensions.z,
371 });
372 }
373 if spacing.x <= 0.0 || spacing.y <= 0.0 || spacing.z <= 0.0 {
374 return Err(VolumeError::InvalidSpacing {
375 x: spacing.x,
376 y: spacing.y,
377 z: spacing.z,
378 });
379 }
380 if components == 0 {
381 return Err(VolumeError::ZeroComponents);
382 }
383 Ok(())
384 }
385}
386
387impl<T: Scalar> VolumeInfo for Volume<T> {
388 fn dimensions(&self) -> UVec3 {
389 self.dimensions
390 }
391 fn spacing(&self) -> DVec3 {
392 self.spacing
393 }
394 fn origin(&self) -> DVec3 {
395 self.origin
396 }
397 fn direction(&self) -> DMat3 {
398 self.direction
399 }
400 fn components(&self) -> u32 {
401 self.components
402 }
403}
404
405#[cfg(test)]
408mod tests {
409 use super::*;
410 use approx::assert_abs_diff_eq;
411 use glam::{DMat3, DVec3, UVec3};
412
413 fn unit_volume() -> Volume<u8> {
414 let data = (0u8..=3)
415 .flat_map(|z| (0u8..=3).flat_map(move |y| (0u8..=3).map(move |x| x + y * 4 + z * 16)))
416 .collect();
417 Volume::from_data(
418 data,
419 UVec3::new(4, 4, 4),
420 DVec3::ONE,
421 DVec3::ZERO,
422 DMat3::IDENTITY,
423 1,
424 )
425 .unwrap()
426 }
427
428 #[test]
429 fn from_data_happy_path() {
430 let vol = unit_volume();
431 assert_eq!(vol.dimensions(), UVec3::new(4, 4, 4));
432 }
433
434 #[test]
435 fn from_data_wrong_length() {
436 let err = Volume::<u8>::from_data(
437 vec![0u8; 10],
438 UVec3::new(4, 4, 4),
439 DVec3::ONE,
440 DVec3::ZERO,
441 DMat3::IDENTITY,
442 1,
443 );
444 assert!(matches!(err, Err(VolumeError::DimensionMismatch { .. })));
445 }
446
447 #[test]
448 fn from_data_zero_dim() {
449 let err = Volume::<u8>::from_data(
450 vec![],
451 UVec3::new(0, 4, 4),
452 DVec3::ONE,
453 DVec3::ZERO,
454 DMat3::IDENTITY,
455 1,
456 );
457 assert!(matches!(err, Err(VolumeError::ZeroDimension { .. })));
458 }
459
460 #[test]
461 fn from_data_invalid_spacing() {
462 let err = Volume::<u8>::from_data(
463 vec![0u8; 64],
464 UVec3::new(4, 4, 4),
465 DVec3::new(0.0, 1.0, 1.0),
466 DVec3::ZERO,
467 DMat3::IDENTITY,
468 1,
469 );
470 assert!(matches!(err, Err(VolumeError::InvalidSpacing { .. })));
471 }
472
473 #[test]
474 fn get_voxel() {
475 let vol = unit_volume();
476 assert_eq!(vol.get(0, 0, 0), Some(0));
477 assert_eq!(vol.get(3, 3, 3), Some(3 + 3 * 4 + 3 * 16));
478 assert_eq!(vol.get(4, 0, 0), None);
479 }
480
481 #[test]
482 fn scalar_range() {
483 let vol = unit_volume();
484 let (lo, hi) = vol.scalar_range();
485 assert_abs_diff_eq!(lo, 0.0, epsilon = 1e-10);
486 assert_abs_diff_eq!(hi, 3.0 + 3.0 * 4.0 + 3.0 * 16.0, epsilon = 1e-10);
487 }
488
489 #[test]
490 fn sample_linear_at_integer_matches_get() {
491 let vol = unit_volume();
492 let v = vol.sample_linear(DVec3::new(1.0, 2.0, 3.0)).unwrap();
493 assert_abs_diff_eq!(v, vol.get(1, 2, 3).unwrap().to_f64(), epsilon = 1e-10);
494 }
495
496 #[test]
497 fn sample_linear_out_of_bounds_returns_none() {
498 let vol = unit_volume();
499 assert!(vol.sample_linear(DVec3::new(-0.1, 0.0, 0.0)).is_none());
500 assert!(vol.sample_linear(DVec3::new(3.1, 0.0, 0.0)).is_none());
501 }
502
503 #[test]
504 fn index_to_world_origin_aligned() {
505 let vol = unit_volume();
506 let world = vol.index_to_world(DVec3::new(1.0, 2.0, 3.0));
508 assert_abs_diff_eq!(world.x, 1.0, epsilon = 1e-10);
509 assert_abs_diff_eq!(world.y, 2.0, epsilon = 1e-10);
510 assert_abs_diff_eq!(world.z, 3.0, epsilon = 1e-10);
511 }
512
513 #[test]
514 fn index_world_round_trip() {
515 let vol = Volume::<f32>::from_data(
516 vec![0.0f32; 8 * 8 * 8],
517 UVec3::new(8, 8, 8),
518 DVec3::new(0.5, 0.75, 1.25),
519 DVec3::new(10.0, 20.0, 30.0),
520 DMat3::IDENTITY,
521 1,
522 )
523 .unwrap();
524 let ijk = DVec3::new(3.5, 2.0, 6.0);
525 let world = vol.index_to_world(ijk);
526 let back = vol.world_to_index(world);
527 assert_abs_diff_eq!(back.x, ijk.x, epsilon = 1e-10);
528 assert_abs_diff_eq!(back.y, ijk.y, epsilon = 1e-10);
529 assert_abs_diff_eq!(back.z, ijk.z, epsilon = 1e-10);
530 }
531
532 #[test]
533 fn from_slices_assembles_correctly() {
534 let slice0: Vec<u8> = (0..4).collect();
535 let slice1: Vec<u8> = (4..8).collect();
536 let vol = Volume::from_slices(
537 &[slice0.as_slice(), slice1.as_slice()],
538 2,
539 2,
540 DVec3::ONE,
541 DVec3::ZERO,
542 DMat3::IDENTITY,
543 )
544 .unwrap();
545 assert_eq!(vol.dimensions(), UVec3::new(2, 2, 2));
546 assert_eq!(vol.get(0, 0, 0), Some(0));
547 assert_eq!(vol.get(0, 0, 1), Some(4));
548 }
549
550 #[test]
551 fn from_slices_inconsistent_length() {
552 let s0 = vec![0u8; 4];
553 let s1 = vec![0u8; 3]; let err = Volume::<u8>::from_slices(
555 &[s0.as_slice(), s1.as_slice()],
556 2,
557 2,
558 DVec3::ONE,
559 DVec3::ZERO,
560 DMat3::IDENTITY,
561 );
562 assert!(matches!(
563 err,
564 Err(VolumeError::InconsistentSlice { index: 1, .. })
565 ));
566 }
567
568 #[test]
569 fn world_bounds_axis_aligned() {
570 let vol = Volume::<u8>::from_data(
571 vec![0u8; 4 * 4 * 4],
572 UVec3::new(4, 4, 4),
573 DVec3::new(2.0, 3.0, 4.0),
574 DVec3::new(1.0, 1.0, 1.0),
575 DMat3::IDENTITY,
576 1,
577 )
578 .unwrap();
579 let bounds = vol.world_bounds();
580 assert_abs_diff_eq!(bounds.min.x, 1.0, epsilon = 1e-10);
582 assert_abs_diff_eq!(bounds.max.x, 1.0 + 3.0 * 2.0, epsilon = 1e-10);
583 }
584}
585
586#[cfg(test)]
587mod proptests {
588 use super::*;
589 use glam::{DMat3, DVec3};
590 use proptest::prelude::*;
591
592 fn make_vol() -> Volume<u8> {
593 Volume::from_data(
594 vec![0u8; 4 * 4 * 4],
595 UVec3::new(4, 4, 4),
596 DVec3::new(1.0, 1.0, 1.0),
597 DVec3::ZERO,
598 DMat3::IDENTITY,
599 1,
600 )
601 .unwrap()
602 }
603
604 proptest! {
605 #[test]
606 fn index_to_world_round_trip(
607 ix in 0.0f64..3.0,
608 iy in 0.0f64..3.0,
609 iz in 0.0f64..3.0,
610 ) {
611 let vol = make_vol();
612 let idx = DVec3::new(ix, iy, iz);
613 let world = vol.index_to_world(idx);
614 let back = vol.world_to_index(world);
615 prop_assert!((back.x - idx.x).abs() < 1e-8, "x: {} vs {}", idx.x, back.x);
616 prop_assert!((back.y - idx.y).abs() < 1e-8, "y: {} vs {}", idx.y, back.y);
617 prop_assert!((back.z - idx.z).abs() < 1e-8, "z: {} vs {}", idx.z, back.z);
618 }
619
620 #[test]
621 fn world_to_index_round_trip(
622 wx in 0.0f64..3.0,
623 wy in 0.0f64..3.0,
624 wz in 0.0f64..3.0,
625 ) {
626 let vol = make_vol();
627 let world = DVec3::new(wx, wy, wz);
628 let idx = vol.world_to_index(world);
629 let back = vol.index_to_world(idx);
630 prop_assert!((back.x - world.x).abs() < 1e-8, "x: {} vs {}", world.x, back.x);
631 prop_assert!((back.y - world.y).abs() < 1e-8, "y: {} vs {}", world.y, back.y);
632 prop_assert!((back.z - world.z).abs() < 1e-8, "z: {} vs {}", world.z, back.z);
633 }
634 }
635}