#![allow(dead_code)]
use std::f32::consts::{PI, TAU};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum TextureProjMode {
Planar,
Cylindrical,
Spherical,
Box,
}
#[allow(dead_code)]
pub fn planar_project(pos: [f32; 3], scale: [f32; 2], offset: [f32; 2]) -> [f32; 2] {
[pos[0] * scale[0] + offset[0], pos[1] * scale[1] + offset[1]]
}
#[allow(dead_code)]
pub fn cylindrical_project(pos: [f32; 3], radius: f32, height: f32) -> [f32; 2] {
let u = (pos[2].atan2(pos[0]) + PI) / TAU;
let v = (pos[1] / height.max(1e-9)).clamp(0.0, 1.0);
let _ = radius;
[u, v]
}
#[allow(dead_code)]
pub fn spherical_project(pos: [f32; 3]) -> [f32; 2] {
let len = (pos[0].powi(2) + pos[1].powi(2) + pos[2].powi(2))
.sqrt()
.max(1e-9);
let u = (pos[2].atan2(pos[0]) + PI) / TAU;
let v = (pos[1] / len).clamp(-1.0, 1.0).acos() / PI;
[u, v]
}
#[allow(dead_code)]
pub fn box_project(pos: [f32; 3]) -> [f32; 2] {
let ax = pos[0].abs();
let ay = pos[1].abs();
let az = pos[2].abs();
if ax >= ay && ax >= az {
[pos[1] * 0.5 + 0.5, pos[2] * 0.5 + 0.5]
} else if ay >= ax && ay >= az {
[pos[0] * 0.5 + 0.5, pos[2] * 0.5 + 0.5]
} else {
[pos[0] * 0.5 + 0.5, pos[1] * 0.5 + 0.5]
}
}
#[allow(dead_code)]
pub fn project_all(positions: &[[f32; 3]], mode: TextureProjMode) -> Vec<[f32; 2]> {
positions
.iter()
.map(|&p| match mode {
TextureProjMode::Planar => planar_project(p, [1.0, 1.0], [0.0, 0.0]),
TextureProjMode::Cylindrical => cylindrical_project(p, 1.0, 2.0),
TextureProjMode::Spherical => spherical_project(p),
TextureProjMode::Box => box_project(p),
})
.collect()
}
#[allow(dead_code)]
pub fn uvs_in_unit_range(uvs: &[[f32; 2]]) -> bool {
uvs.iter()
.all(|uv| (0.0..=1.0).contains(&uv[0]) && (0.0..=1.0).contains(&uv[1]))
}
#[allow(dead_code)]
pub fn tile_uvs_proj(uvs: &[[f32; 2]], tile_u: f32, tile_v: f32) -> Vec<[f32; 2]> {
uvs.iter()
.map(|uv| [uv[0] * tile_u, uv[1] * tile_v])
.collect()
}
#[allow(dead_code)]
pub fn normalize_uvs_proj(uvs: &[[f32; 2]]) -> Vec<[f32; 2]> {
if uvs.is_empty() {
return vec![];
}
let (mut mnu, mut mxu) = (f32::MAX, f32::MIN);
let (mut mnv, mut mxv) = (f32::MAX, f32::MIN);
for uv in uvs {
mnu = mnu.min(uv[0]);
mxu = mxu.max(uv[0]);
mnv = mnv.min(uv[1]);
mxv = mxv.max(uv[1]);
}
let ru = (mxu - mnu).max(1e-9);
let rv = (mxv - mnv).max(1e-9);
uvs.iter()
.map(|uv| [(uv[0] - mnu) / ru, (uv[1] - mnv) / rv])
.collect()
}
#[allow(dead_code)]
pub fn uv_bounds_proj(uvs: &[[f32; 2]]) -> ([f32; 2], [f32; 2]) {
let mut mn = [f32::MAX; 2];
let mut mx = [f32::MIN; 2];
for uv in uvs {
mn[0] = mn[0].min(uv[0]);
mn[1] = mn[1].min(uv[1]);
mx[0] = mx[0].max(uv[0]);
mx[1] = mx[1].max(uv[1]);
}
(mn, mx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn planar_project_basic() {
let uv = planar_project([0.5, 0.5, 0.0], [1.0, 1.0], [0.0, 0.0]);
assert!((uv[0] - 0.5).abs() < 1e-6);
}
#[test]
fn cylindrical_u_range() {
let pos = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, 0.0]];
let uvs = project_all(&pos, TextureProjMode::Cylindrical);
assert!(uvs.iter().all(|uv| (0.0..=1.0).contains(&uv[0])));
}
#[test]
fn spherical_on_unit_sphere() {
let pos = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let uvs = project_all(&pos, TextureProjMode::Spherical);
assert!(uvs.iter().all(|uv| (0.0..=1.0).contains(&uv[1])));
}
#[test]
fn box_project_in_range() {
let pos = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let uvs: Vec<_> = pos.iter().map(|&p| box_project(p)).collect();
assert!(uvs_in_unit_range(&uvs));
}
#[test]
fn tile_uvs_scales() {
let uvs = vec![[0.5, 0.5]];
let t = tile_uvs_proj(&uvs, 2.0, 3.0);
assert!((t[0][0] - 1.0).abs() < 1e-6);
assert!((t[0][1] - 1.5).abs() < 1e-6);
}
#[test]
fn normalize_uvs_range() {
let uvs = vec![[0.0, 2.0], [0.5, 4.0], [1.0, 6.0]];
let n = normalize_uvs_proj(&uvs);
assert!((n[0][1] - 0.0).abs() < 1e-6);
assert!((n[2][1] - 1.0).abs() < 1e-6);
}
#[test]
fn uv_bounds_test() {
let uvs = vec![[0.1, 0.2], [0.9, 0.8]];
let (mn, mx) = uv_bounds_proj(&uvs);
assert!((mn[0] - 0.1).abs() < 1e-6);
assert!((mx[1] - 0.8).abs() < 1e-6);
}
#[test]
fn empty_uvs_normalize() {
let n = normalize_uvs_proj(&[]);
assert!(n.is_empty());
}
#[test]
fn pi_tau_used() {
assert!((TAU - 2.0 * PI).abs() < 1e-5);
}
#[test]
fn projection_mode_planar_all() {
let pos = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
let uvs = project_all(&pos, TextureProjMode::Planar);
assert_eq!(uvs.len(), 2);
}
}