use glam::{DVec2, DVec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SlicePlane {
pub origin: DVec3,
pub right: DVec3,
pub up: DVec3,
pub width: f64,
pub height: f64,
}
impl SlicePlane {
#[must_use]
pub fn new(origin: DVec3, right: DVec3, up: DVec3, width: f64, height: f64) -> Self {
let r = right.normalize_or(DVec3::X);
let u = (up - r * up.dot(r)).normalize_or(DVec3::Y);
Self {
origin,
right: r,
up: u,
width,
height,
}
}
#[must_use]
pub fn axial(z_coord: f64, extent: f64) -> Self {
Self::new(
DVec3::new(0.0, 0.0, z_coord),
DVec3::X,
DVec3::Y,
extent,
extent,
)
}
#[must_use]
pub fn coronal(y_coord: f64, extent: f64) -> Self {
Self::new(
DVec3::new(0.0, y_coord, 0.0),
DVec3::X,
DVec3::Z,
extent,
extent,
)
}
#[must_use]
pub fn sagittal(x_coord: f64, extent: f64) -> Self {
Self::new(
DVec3::new(x_coord, 0.0, 0.0),
DVec3::Y,
DVec3::Z,
extent,
extent,
)
}
#[must_use]
pub fn normal(&self) -> DVec3 {
self.right.cross(self.up)
}
#[must_use]
pub fn to_world(&self, u: f64, v: f64) -> DVec3 {
self.origin + self.right * u + self.up * v
}
#[must_use]
pub fn point_to_world(&self, uv: DVec2) -> DVec3 {
let u = (uv.x - 0.5) * self.width;
let v = (uv.y - 0.5) * self.height;
self.origin + self.right * u + self.up * v
}
#[must_use]
pub fn world_to_point(&self, world: DVec3) -> (DVec2, f64) {
let rel = world - self.origin;
let n = self.normal();
let dist = rel.dot(n);
let u = rel.dot(self.right) / self.width + 0.5;
let v = rel.dot(self.up) / self.height + 0.5;
(DVec2::new(u, v), dist)
}
#[must_use]
pub fn offset_along_normal(&self, delta: f64) -> Self {
Self {
origin: self.origin + self.normal() * delta,
..*self
}
}
pub fn translate_along_normal(&mut self, distance: f64) {
self.origin += self.normal() * distance;
}
#[must_use]
pub fn rotated(&self, axis: DVec3, angle_rad: f64) -> Self {
let rot = glam::DQuat::from_axis_angle(axis.normalize_or(DVec3::Z), angle_rad);
Self::new(
self.origin,
rot * self.right,
rot * self.up,
self.width,
self.height,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ThickSlabMode {
#[default]
Mip,
MinIp,
Mean,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThickSlabParams {
pub half_thickness: f64,
pub mode: ThickSlabMode,
pub num_samples: u32,
}
impl Default for ThickSlabParams {
fn default() -> Self {
Self {
half_thickness: 1.0,
mode: ThickSlabMode::Mip,
num_samples: 10,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn axial_normal_is_z() {
let p = SlicePlane::axial(0.0, 100.0);
let n = p.normal();
assert_abs_diff_eq!(n.x, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(n.y, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(n.z, 1.0, epsilon = 1e-10);
}
#[test]
fn coronal_normal_is_neg_y() {
let p = SlicePlane::coronal(0.0, 100.0);
let n = p.normal();
assert_abs_diff_eq!(n.x, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(n.y.abs(), 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(n.z, 0.0, epsilon = 1e-10);
}
#[test]
fn to_world_origin_is_identity() {
let p = SlicePlane::axial(5.0, 100.0);
let w = p.to_world(0.0, 0.0);
assert_abs_diff_eq!(w.x, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.y, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.z, 5.0, epsilon = 1e-10);
}
#[test]
fn to_world_displaces_correctly() {
let p = SlicePlane::axial(0.0, 100.0);
let w = p.to_world(3.0, 4.0);
assert_abs_diff_eq!(w.x, 3.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.y, 4.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.z, 0.0, epsilon = 1e-10);
}
#[test]
fn offset_along_normal() {
let p = SlicePlane::axial(0.0, 100.0);
let q = p.offset_along_normal(5.0);
assert_abs_diff_eq!(q.origin.z, 5.0, epsilon = 1e-10);
}
#[test]
fn new_orthogonalises_up() {
let p = SlicePlane::new(
DVec3::ZERO,
DVec3::X,
DVec3::new(0.5, 0.866, 0.0),
10.0,
10.0,
);
let dot = p.right.dot(p.up);
assert_abs_diff_eq!(dot, 0.0, epsilon = 1e-10);
}
#[test]
fn right_and_up_are_unit_vectors() {
let p = SlicePlane::sagittal(10.0, 100.0);
assert_abs_diff_eq!(p.right.length(), 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(p.up.length(), 1.0, epsilon = 1e-10);
}
#[test]
fn point_to_world_center() {
let p = SlicePlane::axial(5.0, 100.0);
let w = p.point_to_world(DVec2::new(0.5, 0.5));
assert_abs_diff_eq!(w.x, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.y, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(w.z, 5.0, epsilon = 1e-10);
}
#[test]
fn point_to_world_world_to_point_round_trip() {
let p = SlicePlane::axial(5.0, 100.0);
let uv = DVec2::new(0.3, 0.7);
let world = p.point_to_world(uv);
let (back_uv, dist) = p.world_to_point(world);
assert_abs_diff_eq!(back_uv.x, uv.x, epsilon = 1e-8);
assert_abs_diff_eq!(back_uv.y, uv.y, epsilon = 1e-8);
assert_abs_diff_eq!(dist, 0.0, epsilon = 1e-8);
}
#[test]
fn world_to_point_distance() {
let p = SlicePlane::axial(0.0, 100.0);
let (_, dist) = p.world_to_point(DVec3::new(0.0, 0.0, 3.0));
assert_abs_diff_eq!(dist, 3.0, epsilon = 1e-10);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn world_to_point_round_trip(
u_val in 0.01f64..0.99,
v_val in 0.01f64..0.99,
z_offset in -50.0f64..50.0,
) {
let plane = SlicePlane::axial(z_offset, 100.0);
let uv = DVec2::new(u_val, v_val);
let world = plane.point_to_world(uv);
let (back_uv, dist) = plane.world_to_point(world);
prop_assert!((back_uv.x - uv.x).abs() < 1e-8, "u: {} vs {}", uv.x, back_uv.x);
prop_assert!((back_uv.y - uv.y).abs() < 1e-8, "v: {} vs {}", uv.y, back_uv.y);
prop_assert!(dist.abs() < 1e-8, "dist = {dist}");
}
}
}