#![expect(
clippy::cast_sign_loss,
reason = "IJ/ST coordinate conversions — values always non-negative at point of cast"
)]
#![expect(
clippy::cast_possible_truncation,
reason = "IJ/ST coordinate conversions — bounded by cell level constants"
)]
#![cfg_attr(
test,
expect(
clippy::cast_possible_wrap,
reason = "u32 -> i32 for IJ coordinate test roundtrips — bounded by LIMIT_IJ"
)
)]
use std::fmt;
use crate::r3::{Axis, Vector};
pub const MAX_CELL_LEVEL: u8 = 30;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Level(u8);
impl Level {
pub const MIN: Level = Level(0);
pub const MAX: Level = Level(MAX_CELL_LEVEL);
#[track_caller]
pub const fn new(v: u8) -> Self {
assert!(v <= MAX_CELL_LEVEL, "level must be 0..=30");
Level(v)
}
pub const fn try_new(v: u8) -> Option<Self> {
if v <= MAX_CELL_LEVEL {
Some(Level(v))
} else {
None
}
}
pub const fn as_u8(self) -> u8 {
self.0
}
pub const fn as_usize(self) -> usize {
self.0 as usize
}
pub const fn as_u32(self) -> u32 {
self.0 as u32 }
pub const fn as_i32(self) -> i32 {
self.0 as i32 }
}
impl std::ops::Add<u8> for Level {
type Output = Level;
#[track_caller]
fn add(self, rhs: u8) -> Level {
Level::new(self.0 + rhs)
}
}
impl std::ops::Sub<u8> for Level {
type Output = Level;
#[track_caller]
fn sub(self, rhs: u8) -> Level {
Level::new(self.0 - rhs)
}
}
impl std::ops::Sub<Level> for Level {
type Output = u8;
fn sub(self, rhs: Level) -> u8 {
self.0 - rhs.0
}
}
impl PartialEq<u8> for Level {
fn eq(&self, other: &u8) -> bool {
self.0 == *other
}
}
impl PartialOrd<u8> for Level {
fn partial_cmp(&self, other: &u8) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Level> for u8 {
fn from(l: Level) -> u8 {
l.0
}
}
impl From<Level> for usize {
fn from(l: Level) -> usize {
l.0 as usize
}
}
impl From<Level> for u32 {
fn from(l: Level) -> u32 {
u32::from(l.0)
}
}
impl From<Level> for i32 {
fn from(l: Level) -> i32 {
i32::from(l.0)
}
}
impl From<u8> for Level {
#[track_caller]
fn from(v: u8) -> Self {
Level::new(v)
}
}
impl TryFrom<u16> for Level {
type Error = &'static str;
fn try_from(v: u16) -> Result<Self, Self::Error> {
if v <= u16::from(MAX_CELL_LEVEL) {
Ok(Level(v as u8))
} else {
Err("level must be 0..=30")
}
}
}
impl From<Level> for f64 {
fn from(l: Level) -> f64 {
f64::from(l.0)
}
}
impl TryFrom<i32> for Level {
type Error = &'static str;
fn try_from(v: i32) -> Result<Self, Self::Error> {
if v >= 0 && v <= i32::from(MAX_CELL_LEVEL) {
Ok(Level(v as u8))
} else {
Err("level must be 0..=30")
}
}
}
impl TryFrom<usize> for Level {
type Error = &'static str;
fn try_from(v: usize) -> Result<Self, Self::Error> {
if v <= MAX_CELL_LEVEL as usize {
Ok(Level(v as u8))
} else {
Err("level must be 0..=30")
}
}
}
pub const NUM_FACES: u8 = 6;
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum Face {
F0 = 0,
F1 = 1,
F2 = 2,
F3 = 3,
F4 = 4,
F5 = 5,
}
impl Face {
pub const ALL: [Face; 6] = [Face::F0, Face::F1, Face::F2, Face::F3, Face::F4, Face::F5];
#[inline]
pub fn from_u8(v: u8) -> Face {
Face::ALL[v as usize]
}
#[inline]
pub fn as_u8(self) -> u8 {
self as u8
}
pub fn iter() -> impl Iterator<Item = Face> {
Face::ALL.iter().copied()
}
#[inline]
pub fn axis(self) -> Axis {
Axis::from_index((self as u8 % 3) as usize)
}
#[inline]
pub fn opposite(self) -> Face {
Face::from_u8((self as u8 + 3) % 6)
}
#[inline]
pub fn is_positive(self) -> bool {
(self as u8) < 3
}
}
impl fmt::Display for Face {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_u8())
}
}
impl From<Face> for u8 {
#[inline]
fn from(f: Face) -> u8 {
f as u8
}
}
impl TryFrom<u8> for Face {
type Error = u8;
#[inline]
fn try_from(v: u8) -> Result<Face, u8> {
if v < 6 {
Ok(Face::ALL[v as usize])
} else {
Err(v)
}
}
}
pub const LIMIT_IJ: i32 = 1 << MAX_CELL_LEVEL;
pub const MAX_SI_TI: u32 = 1u32 << (MAX_CELL_LEVEL + 1);
pub const MAX_XYZ_TO_UV_ERROR: f64 = 0.5 * f64::EPSILON;
pub const DBL_EPSILON: f64 = f64::EPSILON;
#[inline]
pub fn st_to_uv(s: f64) -> f64 {
if s >= 0.5 {
(1.0 / 3.0) * (4.0 * s * s - 1.0)
} else {
(1.0 / 3.0) * (1.0 - 4.0 * (1.0 - s) * (1.0 - s))
}
}
#[inline]
pub fn uv_to_st(u: f64) -> f64 {
if u >= 0.0 {
0.5 * (1.0 + 3.0 * u).sqrt()
} else {
1.0 - 0.5 * (1.0 - 3.0 * u).sqrt()
}
}
#[inline]
pub fn ij_to_st_min(i: i32) -> f64 {
debug_assert!((0..=LIMIT_IJ).contains(&i));
(1.0 / f64::from(LIMIT_IJ)) * f64::from(i)
}
#[inline]
pub fn st_to_ij(s: f64) -> i32 {
debug_assert!(!s.is_nan());
if s <= 0.0 || s.is_nan() {
return 0;
}
(f64::from(LIMIT_IJ) * s).min(f64::from(LIMIT_IJ - 1)) as i32
}
#[inline]
pub fn si_ti_to_st(si: u32) -> f64 {
debug_assert!(si <= MAX_SI_TI);
(1.0 / f64::from(MAX_SI_TI)) * f64::from(si)
}
#[inline]
pub fn st_to_si_ti(s: f64) -> u32 {
(s * f64::from(MAX_SI_TI)).round() as u32
}
#[inline]
pub fn face_uv_to_xyz(face: Face, u: f64, v: f64) -> Vector {
match face {
Face::F0 => Vector { x: 1.0, y: u, z: v },
Face::F1 => Vector {
x: -u,
y: 1.0,
z: v,
},
Face::F2 => Vector {
x: -u,
y: -v,
z: 1.0,
},
Face::F3 => Vector {
x: -1.0,
y: -v,
z: -u,
},
Face::F4 => Vector {
x: v,
y: -1.0,
z: -u,
},
Face::F5 => Vector {
x: v,
y: u,
z: -1.0,
},
}
}
#[inline]
pub fn valid_face_xyz_to_uv(face: Face, p: &Vector) -> (f64, f64) {
debug_assert!(
p.dot(get_norm(face)) > 0.0,
"valid_face_xyz_to_uv: p does not belong to face {face}"
);
match face {
Face::F0 => (p.y / p.x, p.z / p.x),
Face::F1 => (-p.x / p.y, p.z / p.y),
Face::F2 => (-p.x / p.z, -p.y / p.z),
Face::F3 => (p.z / p.x, p.y / p.x),
Face::F4 => (p.z / p.y, -p.x / p.y),
Face::F5 => (-p.y / p.z, -p.x / p.z),
}
}
#[inline]
pub fn face_xyz_to_uv(face: Face, p: &Vector) -> Option<(f64, f64)> {
if face.is_positive() {
if p[face.axis()] <= 0.0 {
return None;
}
} else if p[face.axis()] >= 0.0 {
return None;
}
Some(valid_face_xyz_to_uv(face, p))
}
#[inline]
pub fn face_xyz_to_uvw(face: Face, p: &Vector) -> Vector {
match face {
Face::F0 => Vector {
x: p.y,
y: p.z,
z: p.x,
},
Face::F1 => Vector {
x: -p.x,
y: p.z,
z: p.y,
},
Face::F2 => Vector {
x: -p.x,
y: -p.y,
z: p.z,
},
Face::F3 => Vector {
x: -p.z,
y: -p.y,
z: -p.x,
},
Face::F4 => Vector {
x: -p.z,
y: p.x,
z: -p.y,
},
Face::F5 => Vector {
x: p.y,
y: p.x,
z: -p.z,
},
}
}
#[inline]
pub fn get_face(p: &Vector) -> Face {
let axis = p.largest_abs_component();
let positive = p[Axis::from_index(axis)] >= 0.0;
Face::from_u8(if positive { axis as u8 } else { axis as u8 + 3 })
}
#[inline]
pub fn xyz_to_face_uv(p: &Vector) -> (Face, f64, f64) {
let face = get_face(p);
let (u, v) = valid_face_xyz_to_uv(face, p);
(face, u, v)
}
#[inline]
pub fn xyz_to_face_si_ti(p: &Vector) -> (Face, u32, u32, Option<Level>) {
let (face, u, v) = xyz_to_face_uv(p);
let si = st_to_si_ti(uv_to_st(u));
let ti = st_to_si_ti(uv_to_st(v));
let trailing_si = (si | MAX_SI_TI).trailing_zeros();
let trailing_ti = (ti | MAX_SI_TI).trailing_zeros();
let level_si = u32::from(MAX_CELL_LEVEL).wrapping_sub(trailing_si);
let level_ti = u32::from(MAX_CELL_LEVEL).wrapping_sub(trailing_ti);
if level_si > u32::from(MAX_CELL_LEVEL) || level_si != level_ti {
return (face, si, ti, None);
}
let level = Level::new(level_si as u8);
let center = face_si_ti_to_xyz(face, si, ti).normalize();
if *p == center {
(face, si, ti, Some(level))
} else {
(face, si, ti, None)
}
}
#[inline]
pub fn face_si_ti_to_xyz(face: Face, si: u32, ti: u32) -> Vector {
let u = st_to_uv(si_ti_to_st(si));
let v = st_to_uv(si_ti_to_st(ti));
face_uv_to_xyz(face, u, v)
}
#[inline]
pub fn get_u_norm(face: Face, u: f64) -> Vector {
match face {
Face::F0 => Vector {
x: u,
y: -1.0,
z: 0.0,
},
Face::F1 => Vector {
x: 1.0,
y: u,
z: 0.0,
},
Face::F2 => Vector {
x: 1.0,
y: 0.0,
z: u,
},
Face::F3 => Vector {
x: -u,
y: 0.0,
z: 1.0,
},
Face::F4 => Vector {
x: 0.0,
y: -u,
z: 1.0,
},
Face::F5 => Vector {
x: 0.0,
y: -1.0,
z: -u,
},
}
}
#[inline]
pub fn get_v_norm(face: Face, v: f64) -> Vector {
match face {
Face::F0 => Vector {
x: -v,
y: 0.0,
z: 1.0,
},
Face::F1 => Vector {
x: 0.0,
y: -v,
z: 1.0,
},
Face::F2 => Vector {
x: 0.0,
y: -1.0,
z: -v,
},
Face::F3 => Vector {
x: v,
y: -1.0,
z: 0.0,
},
Face::F4 => Vector {
x: 1.0,
y: v,
z: 0.0,
},
Face::F5 => Vector {
x: 1.0,
y: 0.0,
z: v,
},
}
}
#[inline]
pub fn get_norm(face: Face) -> Vector {
get_uvw_axis(face, 2)
}
#[inline]
pub fn get_u_axis(face: Face) -> Vector {
get_uvw_axis(face, 0)
}
#[inline]
pub fn get_v_axis(face: Face) -> Vector {
get_uvw_axis(face, 1)
}
#[inline]
pub fn get_uvw_axis(face: Face, axis: u8) -> Vector {
let p = &FACE_UVW_AXES[face.as_u8() as usize][axis as usize];
Vector {
x: p[0],
y: p[1],
z: p[2],
}
}
#[inline]
pub fn get_uvw_face(face: Face, axis: u8, direction: u8) -> Face {
debug_assert!(axis < 3 && direction < 2);
Face::from_u8(FACE_UVW_FACES[face.as_u8() as usize][axis as usize][direction as usize])
}
pub const SWAP_MASK: u8 = 0x01;
pub const INVERT_MASK: u8 = 0x02;
pub const IJ_TO_POS: [[u8; 4]; 4] = [
[0, 1, 3, 2], [0, 3, 1, 2], [2, 3, 1, 0], [2, 1, 3, 0], ];
pub const POS_TO_IJ: [[u8; 4]; 4] = [
[0, 1, 3, 2], [0, 2, 3, 1], [3, 2, 0, 1], [3, 1, 0, 2], ];
pub const POS_TO_ORIENTATION: [u8; 4] = [SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK];
const FACE_UVW_AXES: [[[f64; 3]; 3]; 6] = [
[[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]],
[[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]],
[[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]],
[[0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [-1.0, 0.0, 0.0]],
[[0.0, 0.0, -1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]],
[[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]],
];
const FACE_UVW_FACES: [[[u8; 2]; 3]; 6] = [
[[4, 1], [5, 2], [3, 0]],
[[0, 3], [5, 2], [4, 1]],
[[0, 3], [1, 4], [5, 2]],
[[2, 5], [1, 4], [0, 3]],
[[2, 5], [3, 0], [1, 4]],
[[4, 1], [3, 0], [2, 5]],
];
#[cfg(test)]
mod tests {
use super::*;
fn float64_eq(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-15
}
fn float64_near(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() <= eps
}
fn swap_axes(ij: u8) -> u8 {
((ij >> 1) & 1) + ((ij & 1) << 1)
}
fn invert_bits(ij: u8) -> u8 {
ij ^ 3
}
#[test]
fn test_traversal_order() {
for r in 0..4u8 {
for i in 0..4u8 {
assert_eq!(
IJ_TO_POS[r as usize][i as usize],
IJ_TO_POS[(r ^ SWAP_MASK) as usize][swap_axes(i) as usize],
);
assert_eq!(
POS_TO_IJ[r as usize][i as usize],
swap_axes(POS_TO_IJ[(r ^ SWAP_MASK) as usize][i as usize]),
);
assert_eq!(
IJ_TO_POS[r as usize][i as usize],
IJ_TO_POS[(r ^ INVERT_MASK) as usize][invert_bits(i) as usize],
);
assert_eq!(
POS_TO_IJ[r as usize][i as usize],
invert_bits(POS_TO_IJ[(r ^ INVERT_MASK) as usize][i as usize]),
);
assert_eq!(
IJ_TO_POS[r as usize][POS_TO_IJ[r as usize][i as usize] as usize],
i,
);
assert_eq!(
POS_TO_IJ[r as usize][IJ_TO_POS[r as usize][i as usize] as usize],
i,
);
}
}
}
#[test]
fn test_st_uv_conversions() {
for &s in &[0.0, 0.5, 1.0] {
let u = st_to_uv(s);
let want = 2.0 * s - 1.0;
assert!(float64_eq(u, want), "st_to_uv({s}) = {u}, want {want}",);
}
for &u in &[-1.0, 0.0, 1.0] {
let s = uv_to_st(u);
let want = 0.5 * (u + 1.0);
assert!(float64_eq(s, want), "uv_to_st({u}) = {s}, want {want}",);
}
let mut x = 0.0;
while x <= 1.0 {
let got = uv_to_st(st_to_uv(x));
assert!(
float64_near(got, x, 1e-15),
"uv_to_st(st_to_uv({x})) = {got}, want {x}",
);
let u = 2.0 * x - 1.0;
let got2 = st_to_uv(uv_to_st(u));
assert!(
float64_near(got2, u, 1e-15),
"st_to_uv(uv_to_st({u})) = {got2}, want {u}",
);
x += 0.0001;
}
}
#[test]
fn test_st_to_ij_boundaries() {
assert_eq!(st_to_ij(0.0), 0);
assert_eq!(st_to_ij(1.0), LIMIT_IJ - 1);
}
#[test]
fn test_st_to_ij_halfway() {
let recip = 1.0 / f64::from(LIMIT_IJ);
assert_eq!(st_to_ij(0.5 * recip), 0);
assert_eq!(st_to_ij(1.0 * recip), 1);
assert_eq!(st_to_ij(1.5 * recip), 1);
assert_eq!(st_to_ij(2.0 * recip), 2);
assert_eq!(st_to_ij((f64::from(LIMIT_IJ) - 0.5) * recip), LIMIT_IJ - 1);
}
#[test]
fn test_si_ti_st_roundtrip() {
for si in [0u32, 1, 2, MAX_SI_TI / 2, MAX_SI_TI - 1, MAX_SI_TI] {
assert_eq!(
st_to_si_ti(si_ti_to_st(si)),
si,
"roundtrip failed for si={si}",
);
}
let mut s = 0.0;
while s <= 1.0 {
let got = si_ti_to_st(st_to_si_ti(s));
assert!(
float64_near(got, s, 1e-8),
"si_ti_to_st(st_to_si_ti({s})) = {got}, want ≈{s}",
);
s += 0.001;
}
}
#[test]
fn test_face_uv_to_xyz() {
let mut sum = Vector::default();
for face in Face::iter() {
let center = face_uv_to_xyz(face, 0.0, 0.0);
assert!(
center.aequal(get_norm(face), 1e-15),
"face_uv_to_xyz({face}, 0, 0) = {center:?}, want ≈ {:?}",
get_norm(face),
);
let abs_center = center.abs();
match center.largest_abs_component() {
0 => assert_eq!(abs_center.x, 1.0),
1 => assert_eq!(abs_center.y, 1.0),
_ => assert_eq!(abs_center.z, 1.0),
}
sum = sum + center.abs();
assert_eq!(
get_u_axis(face).cross(get_v_axis(face)).dot(get_norm(face)),
1.0,
"face {face} not right-handed",
);
let sign: f64 = if face.as_u8() & SWAP_MASK == 1 {
-1.0
} else {
1.0
};
let next_face = Face::from_u8((face.as_u8() + 1) % 6);
assert_eq!(
face_uv_to_xyz(face, sign, -sign),
face_uv_to_xyz(next_face, -1.0, -1.0),
"Hilbert curve discontinuity at face {face}",
);
}
assert!(sum.aequal(
Vector {
x: 2.0,
y: 2.0,
z: 2.0
},
1e-15
));
}
#[test]
fn test_face_xyz_to_uv() {
let point = Vector {
x: 1.1,
y: 1.2,
z: 1.3,
};
let point_neg = Vector {
x: -1.1,
y: -1.2,
z: -1.3,
};
let cases: Vec<(Face, Vector, Option<(f64, f64)>)> = vec![
(Face::F0, point, Some((1.0 + 1.0 / 11.0, 1.0 + 2.0 / 11.0))),
(Face::F0, point_neg, None),
(Face::F1, point, Some((-11.0 / 12.0, 1.0 + 1.0 / 12.0))),
(Face::F1, point_neg, None),
(Face::F2, point, Some((-11.0 / 13.0, -12.0 / 13.0))),
(Face::F2, point_neg, None),
(Face::F3, point, None),
(
Face::F3,
point_neg,
Some((1.0 + 2.0 / 11.0, 1.0 + 1.0 / 11.0)),
),
(Face::F4, point, None),
(
Face::F4,
point_neg,
Some((1.0 + 1.0 / 12.0, -(11.0 / 12.0))),
),
(Face::F5, point, None),
(Face::F5, point_neg, Some((-12.0 / 13.0, -11.0 / 13.0))),
];
for (face, p, expected) in &cases {
let got = face_xyz_to_uv(*face, p);
match (got, expected) {
(None, None) => {}
(Some((gu, gv)), Some((eu, ev))) => {
assert!(
float64_eq(gu, *eu) && float64_eq(gv, *ev),
"face_xyz_to_uv({face}, {p:?}) = ({gu}, {gv}), want ({eu}, {ev})",
);
}
_ => panic!("face_xyz_to_uv({face}, {p:?}) = {got:?}, want {expected:?}",),
}
}
}
#[test]
fn test_face_xyz_to_uvw() {
let origin = Vector::default();
let pos_x = Vector {
x: 1.0,
y: 0.0,
z: 0.0,
};
let neg_x = Vector {
x: -1.0,
y: 0.0,
z: 0.0,
};
let pos_y = Vector {
x: 0.0,
y: 1.0,
z: 0.0,
};
let neg_y = Vector {
x: 0.0,
y: -1.0,
z: 0.0,
};
let pos_z = Vector {
x: 0.0,
y: 0.0,
z: 1.0,
};
let neg_z = Vector {
x: 0.0,
y: 0.0,
z: -1.0,
};
for face in Face::iter() {
assert_eq!(face_xyz_to_uvw(face, &origin), origin);
assert_eq!(face_xyz_to_uvw(face, &get_u_axis(face)), pos_x);
assert_eq!(face_xyz_to_uvw(face, &(-get_u_axis(face))), neg_x);
assert_eq!(face_xyz_to_uvw(face, &get_v_axis(face)), pos_y);
assert_eq!(face_xyz_to_uvw(face, &(-get_v_axis(face))), neg_y);
assert_eq!(face_xyz_to_uvw(face, &get_norm(face)), pos_z);
assert_eq!(face_xyz_to_uvw(face, &(-get_norm(face))), neg_z);
}
}
#[test]
fn test_uvw_axis() {
for face in Face::iter() {
assert_eq!(
face_uv_to_xyz(face, 1.0, 0.0) - face_uv_to_xyz(face, 0.0, 0.0),
get_u_axis(face),
);
assert_eq!(
face_uv_to_xyz(face, 0.0, 1.0) - face_uv_to_xyz(face, 0.0, 0.0),
get_v_axis(face),
);
assert_eq!(face_uv_to_xyz(face, 0.0, 0.0), get_norm(face));
assert_eq!(
get_u_axis(face).cross(get_v_axis(face)).dot(get_norm(face)),
1.0,
);
assert_eq!(get_u_axis(face), get_uvw_axis(face, 0));
assert_eq!(get_v_axis(face), get_uvw_axis(face, 1));
assert_eq!(get_norm(face), get_uvw_axis(face, 2));
}
}
#[test]
fn test_uv_norms() {
let step = 1.0 / 1024.0;
for face in Face::iter() {
let mut x = -1.0;
while x <= 1.0 {
let u_angle = face_uv_to_xyz(face, x, -1.0)
.cross(face_uv_to_xyz(face, x, 1.0))
.angle(get_u_norm(face, x));
assert!(
float64_eq(u_angle, 0.0),
"UNorm not orthogonal at face={face}, x={x}: angle={u_angle}",
);
let v_angle = face_uv_to_xyz(face, -1.0, x)
.cross(face_uv_to_xyz(face, 1.0, x))
.angle(get_v_norm(face, x));
assert!(
float64_eq(v_angle, 0.0),
"VNorm not orthogonal at face={face}, x={x}: angle={v_angle}",
);
x += step;
}
}
}
#[test]
fn test_uvw_face() {
for face in Face::iter() {
for axis in 0..3u8 {
assert_eq!(
get_face(&(-get_uvw_axis(face, axis))),
get_uvw_face(face, axis, 0),
);
assert_eq!(
get_face(&get_uvw_axis(face, axis)),
get_uvw_face(face, axis, 1),
);
}
}
}
#[test]
fn test_get_face() {
let cases: Vec<(Vector, u8)> = vec![
(
Vector {
x: -1.0,
y: -1.0,
z: -1.0,
},
5,
),
(
Vector {
x: -1.0,
y: -1.0,
z: 0.0,
},
4,
),
(
Vector {
x: -1.0,
y: -1.0,
z: 1.0,
},
2,
),
(
Vector {
x: -1.0,
y: 0.0,
z: -1.0,
},
5,
),
(
Vector {
x: -1.0,
y: 0.0,
z: 0.0,
},
3,
),
(
Vector {
x: -1.0,
y: 0.0,
z: 1.0,
},
2,
),
(
Vector {
x: -1.0,
y: 1.0,
z: -1.0,
},
5,
),
(
Vector {
x: -1.0,
y: 1.0,
z: 0.0,
},
1,
),
(
Vector {
x: -1.0,
y: 1.0,
z: 1.0,
},
2,
),
(
Vector {
x: 0.0,
y: -1.0,
z: -1.0,
},
5,
),
(
Vector {
x: 0.0,
y: -1.0,
z: 0.0,
},
4,
),
(
Vector {
x: 0.0,
y: -1.0,
z: 1.0,
},
2,
),
(
Vector {
x: 0.0,
y: 0.0,
z: -1.0,
},
5,
),
(
Vector {
x: 0.0,
y: 0.0,
z: 0.0,
},
2,
), (
Vector {
x: 0.0,
y: 0.0,
z: 1.0,
},
2,
),
(
Vector {
x: 0.0,
y: 1.0,
z: -1.0,
},
5,
),
(
Vector {
x: 0.0,
y: 1.0,
z: 0.0,
},
1,
),
(
Vector {
x: 0.0,
y: 1.0,
z: 1.0,
},
2,
),
(
Vector {
x: 1.0,
y: -1.0,
z: -1.0,
},
5,
),
(
Vector {
x: 1.0,
y: -1.0,
z: 0.0,
},
4,
),
(
Vector {
x: 1.0,
y: -1.0,
z: 1.0,
},
2,
),
(
Vector {
x: 1.0,
y: 0.0,
z: -1.0,
},
5,
),
(
Vector {
x: 1.0,
y: 0.0,
z: 0.0,
},
0,
),
(
Vector {
x: 1.0,
y: 0.0,
z: 1.0,
},
2,
),
(
Vector {
x: 1.0,
y: 1.0,
z: -1.0,
},
5,
),
(
Vector {
x: 1.0,
y: 1.0,
z: 0.0,
},
1,
),
(
Vector {
x: 1.0,
y: 1.0,
z: 1.0,
},
2,
),
];
for (v, want) in &cases {
assert_eq!(
get_face(v),
Face::from_u8(*want),
"get_face({v:?}) = {}, want {want}",
get_face(v),
);
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_level_roundtrip() {
for l in 0..=MAX_CELL_LEVEL {
let level = Level::new(l);
let json = serde_json::to_string(&level).unwrap();
let back: Level = serde_json::from_str(&json).unwrap();
assert_eq!(level, back);
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_face_roundtrip() {
for f in Face::ALL {
let json = serde_json::to_string(&f).unwrap();
let back: Face = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
}
}
#[cfg(test)]
mod quickcheck_tests {
use super::*;
use quickcheck_macros::quickcheck;
fn clamp_finite(v: f64) -> f64 {
if v.is_finite() {
v.clamp(-1e10, 1e10)
} else {
0.0
}
}
#[quickcheck]
fn prop_uv_st_roundtrip(s: f64) -> bool {
let s = clamp_finite(s).clamp(0.0, 1.0);
let got = uv_to_st(st_to_uv(s));
(got - s).abs() < 1e-15
}
#[quickcheck]
fn prop_st_uv_roundtrip(u: f64) -> bool {
let u = clamp_finite(u).clamp(-1.0, 1.0);
let got = st_to_uv(uv_to_st(u));
(got - u).abs() < 1e-15
}
#[quickcheck]
fn prop_face_uv_xyz_roundtrip(x: f64, y: f64, z: f64) -> bool {
let x = clamp_finite(x);
let y = clamp_finite(y);
let z = clamp_finite(z);
if x == 0.0 && y == 0.0 && z == 0.0 {
return true;
}
let p = Vector { x, y, z };
let (face, u, v) = xyz_to_face_uv(&p);
let q = face_uv_to_xyz(face, u, v);
let scale = if p.x.abs() > p.y.abs().max(p.z.abs()) {
q.x / p.x
} else if p.y.abs() > p.z.abs() {
q.y / p.y
} else {
q.z / p.z
};
scale > 0.0
&& (q.x - scale * p.x).abs() < 1e-10
&& (q.y - scale * p.y).abs() < 1e-10
&& (q.z - scale * p.z).abs() < 1e-10
}
#[quickcheck]
fn prop_all_faces_reachable(x: f64, y: f64, z: f64) -> bool {
let x = clamp_finite(x);
let y = clamp_finite(y);
let z = clamp_finite(z);
let p = Vector { x, y, z };
get_face(&p).as_u8() < 6
}
#[quickcheck]
fn prop_ij_st_roundtrip(i: u32) -> bool {
let i = (i % (LIMIT_IJ as u32)) as i32;
let s = ij_to_st_min(i);
let j = st_to_ij(s);
(j - i).abs() <= 1
}
#[quickcheck]
fn prop_si_ti_st_roundtrip(si: u32) -> bool {
let si = si % (MAX_SI_TI + 1);
let s = si_ti_to_st(si);
let si2 = st_to_si_ti(s);
si == si2
}
#[quickcheck]
fn prop_st_to_uv_monotonic(a: f64, b: f64) -> bool {
let a = clamp_finite(a).clamp(0.0, 1.0);
let b = clamp_finite(b).clamp(0.0, 1.0);
if a <= b {
st_to_uv(a) <= st_to_uv(b)
} else {
st_to_uv(a) >= st_to_uv(b)
}
}
#[quickcheck]
fn prop_uv_to_st_range(u: f64) -> bool {
let u = clamp_finite(u).clamp(-1.0, 1.0);
let s = uv_to_st(u);
(0.0..=1.0).contains(&s)
}
#[quickcheck]
fn prop_face_axes_orthogonal(face: u8) -> bool {
let face = Face::from_u8(face % 6);
let u = get_u_axis(face);
let v = get_v_axis(face);
let n = get_norm(face);
u.dot(v).abs() < 1e-14 && u.dot(n).abs() < 1e-14 && v.dot(n).abs() < 1e-14
}
#[quickcheck]
fn prop_face_normals_unit(face: u8) -> bool {
let face = Face::from_u8(face % 6);
let n = get_norm(face);
(n.norm() - 1.0).abs() < 1e-14
}
#[test]
fn test_ij_to_st_to_ij_roundtrip() {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let mut rng = StdRng::seed_from_u64(42);
for _ in 0..1000 {
let i = (rng.r#gen::<u32>() % LIMIT_IJ as u32) as i32;
let s_min = ij_to_st_min(i);
let s_max = ij_to_st_min(i + 1);
let t: f64 = rng.r#gen();
let s = s_min + t * (s_max - s_min);
let s = s.min(f64::from_bits(s_max.to_bits() - 1)); assert_eq!(st_to_ij(s), i, "s={s}, i={i}");
assert_eq!(st_to_ij(s_min), i, "s_min={s_min}, i={i}");
let before_s_max = f64::from_bits(s_max.to_bits() - 1);
assert_eq!(
st_to_ij(before_s_max),
i,
"before_s_max={before_s_max}, i={i}"
);
}
}
#[test]
fn test_xyz_to_face_si_ti() {
use crate::s2::testing::random_cell_id_at_level;
use rand::SeedableRng;
use rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(123);
for level_u8 in 0..=MAX_CELL_LEVEL {
let level = Level::new(level_u8);
for _ in 0..100 {
let id = random_cell_id_at_level(&mut rng, level);
let p = id.to_point();
let (face, si, ti, actual_level) = xyz_to_face_si_ti(&p.0);
assert_eq!(
actual_level,
Some(level),
"level mismatch for {id:?} at level {level}"
);
let actual_id =
crate::s2::cell_id::from_face_ij(face, (si / 2) as i32, (ti / 2) as i32)
.parent_at_level(level);
assert_eq!(id, actual_id);
let p_moved = crate::s2::Point(p.0 + Vector::new(1e-13, 1e-13, 1e-13)).normalize();
let (face_moved, si_moved, ti_moved, level_moved) = xyz_to_face_si_ti(&p_moved.0);
assert_eq!(
level_moved, None,
"moved point should not match a cell center"
);
assert_eq!(face, face_moved);
assert_eq!(si, si_moved);
assert_eq!(ti, ti_moved);
}
}
}
}