#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UvProjectionMode {
XY,
XZ,
YZ,
Spherical,
Cylindrical,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ParameterizeConfig {
pub projection: UvProjectionMode,
pub normalize: bool,
pub flip_v: bool,
pub pack_islands: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct UvParam {
pub u: f32,
pub v: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ParamResult {
pub uvs: Vec<UvParam>,
pub island_count: usize,
pub valid: bool,
pub seam_length: f32,
}
pub type IslandRect = (f32, f32, f32, f32);
#[allow(dead_code)]
fn len3(v: [f32; 3]) -> f32 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[allow(dead_code)]
fn dist2(a: UvParam, b: UvParam) -> f32 {
let du = a.u - b.u;
let dv = a.v - b.v;
(du * du + dv * dv).sqrt()
}
#[allow(dead_code)]
pub fn default_parameterize_config() -> ParameterizeConfig {
ParameterizeConfig {
projection: UvProjectionMode::XY,
normalize: true,
flip_v: false,
pack_islands: false,
}
}
#[allow(dead_code)]
pub fn planar_project_uv(positions: &[[f32; 3]], mode: UvProjectionMode) -> Vec<UvParam> {
positions
.iter()
.map(|&[x, y, z]| match mode {
UvProjectionMode::XY => UvParam { u: x, v: y },
UvProjectionMode::XZ => UvParam { u: x, v: z },
UvProjectionMode::YZ => UvParam { u: y, v: z },
UvProjectionMode::Spherical => {
let r = len3([x, y, z]);
if r < 1e-12 {
UvParam { u: 0.0, v: 0.5 }
} else {
let u = (z.atan2(x) / (2.0 * std::f32::consts::PI)) + 0.5;
let v = (y / r).clamp(-1.0, 1.0).acos() / std::f32::consts::PI;
UvParam { u, v }
}
}
UvProjectionMode::Cylindrical => {
let u = (z.atan2(x) / (2.0 * std::f32::consts::PI)) + 0.5;
UvParam { u, v: y }
}
})
.collect()
}
#[allow(dead_code)]
pub fn parameterize_patch(
positions: &[[f32; 3]],
patch_indices: &[usize],
mode: UvProjectionMode,
) -> Vec<UvParam> {
let patch_pos: Vec<[f32; 3]> = patch_indices
.iter()
.filter_map(|&i| positions.get(i).copied())
.collect();
planar_project_uv(&patch_pos, mode)
}
#[allow(dead_code)]
pub fn uv_distortion_metric(positions: &[[f32; 3]], uvs: &[UvParam], indices: &[u32]) -> f32 {
if indices.len() < 3 || uvs.len() != positions.len() {
return 0.0;
}
let tris = indices.len() / 3;
let mut total = 0.0f32;
let mut count = 0usize;
for t in 0..tris {
let base = t * 3;
let v = [
indices[base] as usize,
indices[base + 1] as usize,
indices[base + 2] as usize,
];
for k in 0..3 {
let a = v[k];
let b = v[(k + 1) % 3];
if a >= positions.len() || b >= positions.len() {
continue;
}
let p = positions[a];
let q = positions[b];
let d3 = ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2))
.sqrt();
let d2 = dist2(uvs[a], uvs[b]);
if d3 > 1e-12 {
total += ((d2 - d3) / d3).powi(2);
count += 1;
}
}
}
if count == 0 {
0.0
} else {
total / count as f32
}
}
#[allow(dead_code)]
pub fn uv_overlap_count(uvs: &[UvParam], indices: &[u32], tolerance: f32) -> usize {
let tris = indices.len() / 3;
let mut centroids: Vec<UvParam> = Vec::with_capacity(tris);
for t in 0..tris {
let base = t * 3;
let mut su = 0.0f32;
let mut sv = 0.0f32;
for k in 0..3 {
let vi = indices[base + k] as usize;
if vi < uvs.len() {
su += uvs[vi].u;
sv += uvs[vi].v;
}
}
centroids.push(UvParam { u: su / 3.0, v: sv / 3.0 });
}
let mut count = 0;
for i in 0..centroids.len() {
for j in (i + 1)..centroids.len() {
if dist2(centroids[i], centroids[j]) < tolerance {
count += 1;
}
}
}
count
}
#[allow(dead_code)]
pub fn normalize_uv_bounds(uvs: &[UvParam]) -> Vec<UvParam> {
if uvs.is_empty() {
return Vec::new();
}
let mut umin = f32::MAX;
let mut umax = f32::MIN;
let mut vmin = f32::MAX;
let mut vmax = f32::MIN;
for uv in uvs {
umin = umin.min(uv.u);
umax = umax.max(uv.u);
vmin = vmin.min(uv.v);
vmax = vmax.max(uv.v);
}
let urange = (umax - umin).max(1e-12);
let vrange = (vmax - vmin).max(1e-12);
uvs.iter()
.map(|uv| UvParam {
u: (uv.u - umin) / urange,
v: (uv.v - vmin) / vrange,
})
.collect()
}
#[allow(dead_code)]
pub fn flip_uv_v(uvs: &[UvParam]) -> Vec<UvParam> {
uvs.iter().map(|uv| UvParam { u: uv.u, v: 1.0 - uv.v }).collect()
}
#[allow(dead_code)]
pub fn rotate_uv_90(uvs: &[UvParam]) -> Vec<UvParam> {
uvs.iter().map(|uv| UvParam { u: uv.v, v: 1.0 - uv.u }).collect()
}
#[allow(dead_code)]
pub fn pack_uv_islands_stub(island_sizes: &[(f32, f32)]) -> Vec<IslandRect> {
let mut x = 0.0f32;
let mut result = Vec::with_capacity(island_sizes.len());
for &(w, h) in island_sizes {
result.push((x, 0.0, x + w, h));
x += w + 0.01; }
result
}
#[allow(dead_code)]
pub fn validate_uv_map(uvs: &[UvParam]) -> bool {
uvs.iter().all(|uv| {
uv.u.is_finite()
&& uv.v.is_finite()
&& uv.u >= -1.0
&& uv.u <= 3.0
&& uv.v >= -1.0
&& uv.v <= 3.0
})
}
#[allow(dead_code)]
pub fn uv_seam_length(positions: &[[f32; 3]], uvs: &[UvParam], indices: &[u32]) -> f32 {
use std::collections::HashMap;
if uvs.len() != positions.len() {
return 0.0;
}
let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
let tris = indices.len() / 3;
for t in 0..tris {
let base = t * 3;
for k in 0..3 {
let a = indices[base + k] as usize;
let b = indices[base + (k + 1) % 3] as usize;
let key = (a.min(b), a.max(b));
*edge_count.entry(key).or_insert(0) += 1;
}
}
let mut total = 0.0f32;
for ((a, b), count) in &edge_count {
if *count == 1 && *a < positions.len() && *b < positions.len() {
let p = positions[*a];
let q = positions[*b];
total += ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2))
.sqrt();
}
}
total
}
#[allow(dead_code)]
pub fn uv_vertex_count(uvs: &[UvParam]) -> usize {
uvs.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn unit_triangle_pos() -> Vec<[f32; 3]> {
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
}
fn unit_triangle_idx() -> Vec<u32> {
vec![0, 1, 2]
}
#[test]
fn test_default_config_projection_xy() {
let cfg = default_parameterize_config();
assert_eq!(cfg.projection, UvProjectionMode::XY);
}
#[test]
fn test_default_config_normalize_true() {
let cfg = default_parameterize_config();
assert!(cfg.normalize);
}
#[test]
fn test_planar_project_xy() {
let pos = unit_triangle_pos();
let uvs = planar_project_uv(&pos, UvProjectionMode::XY);
assert_eq!(uvs.len(), 3);
assert!((uvs[0].u - 0.0).abs() < 1e-6);
assert!((uvs[1].u - 1.0).abs() < 1e-6);
assert!((uvs[2].v - 1.0).abs() < 1e-6);
}
#[test]
fn test_planar_project_xz() {
let pos = vec![[1.0f32, 99.0, 2.0]];
let uvs = planar_project_uv(&pos, UvProjectionMode::XZ);
assert!((uvs[0].u - 1.0).abs() < 1e-6);
assert!((uvs[0].v - 2.0).abs() < 1e-6);
}
#[test]
fn test_planar_project_yz() {
let pos = vec![[99.0f32, 3.0, 4.0]];
let uvs = planar_project_uv(&pos, UvProjectionMode::YZ);
assert!((uvs[0].u - 3.0).abs() < 1e-6);
assert!((uvs[0].v - 4.0).abs() < 1e-6);
}
#[test]
fn test_spherical_u_in_range() {
let pos = vec![[1.0f32, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let uvs = planar_project_uv(&pos, UvProjectionMode::Spherical);
for uv in &uvs {
assert!(uv.u >= 0.0 && uv.u <= 1.0, "u={} not in [0,1]", uv.u);
}
}
#[test]
fn test_cylindrical_u_in_range() {
let pos = vec![[1.0f32, 2.0, 0.0], [-1.0, -1.0, 0.0]];
let uvs = planar_project_uv(&pos, UvProjectionMode::Cylindrical);
for uv in &uvs {
assert!(uv.u >= 0.0 && uv.u <= 1.0, "u={} not in [0,1]", uv.u);
}
}
#[test]
fn test_parameterize_patch_count() {
let pos = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let patch = vec![0, 2];
let uvs = parameterize_patch(&pos, &patch, UvProjectionMode::XY);
assert_eq!(uvs.len(), 2);
}
#[test]
fn test_distortion_zero_identity() {
let pos = unit_triangle_pos();
let idx = unit_triangle_idx();
let uvs = planar_project_uv(&pos, UvProjectionMode::XY);
let dist = uv_distortion_metric(&pos, &uvs, &idx);
assert!(dist < 1e-4, "distortion={dist}");
}
#[test]
fn test_overlap_count_overlapping() {
let uvs = vec![
UvParam { u: 0.0, v: 0.0 },
UvParam { u: 1.0, v: 0.0 },
UvParam { u: 0.0, v: 1.0 },
];
let idx = vec![0u32, 1, 2, 0, 1, 2];
let count = uv_overlap_count(&uvs, &idx, 0.01);
assert!(count >= 1, "should detect overlapping UV centroids");
}
#[test]
fn test_normalize_uv_bounds() {
let uvs = vec![
UvParam { u: 2.0, v: -1.0 },
UvParam { u: 4.0, v: 3.0 },
UvParam { u: 3.0, v: 1.0 },
];
let norm = normalize_uv_bounds(&uvs);
let umin = norm.iter().map(|uv| uv.u).fold(f32::MAX, f32::min);
let umax = norm.iter().map(|uv| uv.u).fold(f32::MIN, f32::max);
let vmin = norm.iter().map(|uv| uv.v).fold(f32::MAX, f32::min);
let vmax = norm.iter().map(|uv| uv.v).fold(f32::MIN, f32::max);
assert!((umin - 0.0).abs() < 1e-5);
assert!((umax - 1.0).abs() < 1e-5);
assert!((vmin - 0.0).abs() < 1e-5);
assert!((vmax - 1.0).abs() < 1e-5);
}
#[test]
fn test_normalize_uv_bounds_empty() {
let norm = normalize_uv_bounds(&[]);
assert!(norm.is_empty());
}
#[test]
fn test_flip_uv_v() {
let uvs = vec![UvParam { u: 0.3, v: 0.7 }];
let flipped = flip_uv_v(&uvs);
assert!((flipped[0].v - 0.3).abs() < 1e-6);
assert!((flipped[0].u - 0.3).abs() < 1e-6);
}
#[test]
fn test_rotate_uv_90_double() {
let uvs = vec![UvParam { u: 0.2, v: 0.8 }];
let r1 = rotate_uv_90(&uvs);
let r2 = rotate_uv_90(&r1);
assert!((r2[0].u - (1.0 - uvs[0].u)).abs() < 1e-5);
assert!((r2[0].v - (1.0 - uvs[0].v)).abs() < 1e-5);
}
#[test]
fn test_pack_islands_non_overlapping() {
let islands = vec![(0.3f32, 0.4), (0.2, 0.5), (0.1, 0.3)];
let packed = pack_uv_islands_stub(&islands);
assert_eq!(packed.len(), 3);
for (umin, vmin, umax, vmax) in &packed {
assert!(umin < umax, "umin >= umax");
assert!(vmin < vmax, "vmin >= vmax");
}
}
#[test]
fn test_validate_uv_map_valid() {
let uvs = vec![UvParam { u: 0.0, v: 0.0 }, UvParam { u: 1.0, v: 1.0 }];
assert!(validate_uv_map(&uvs));
}
#[test]
fn test_validate_uv_map_nan() {
let uvs = vec![UvParam { u: f32::NAN, v: 0.0 }];
assert!(!validate_uv_map(&uvs));
}
#[test]
fn test_uv_seam_length_non_negative() {
let pos = unit_triangle_pos();
let idx = unit_triangle_idx();
let uvs = planar_project_uv(&pos, UvProjectionMode::XY);
let seam = uv_seam_length(&pos, &uvs, &idx);
assert!(seam >= 0.0);
}
#[test]
fn test_uv_vertex_count() {
let pos = unit_triangle_pos();
let uvs = planar_project_uv(&pos, UvProjectionMode::XY);
assert_eq!(uv_vertex_count(&uvs), 3);
}
#[test]
fn test_planar_project_empty() {
let uvs = planar_project_uv(&[], UvProjectionMode::XY);
assert!(uvs.is_empty());
}
}