use crate::geom::{
add3, angle, bounds2, convex_hull, cross3, dot3, len3, min_area_rect_dir, normalize3, rotate,
scale3, signed_area, sub3, triangles_overlap, V2,
};
use crate::par;
use crate::progress::{Ctx, Stage};
use crate::solver::{least_squares, SparseRows};
use crate::topology::{Topology, NONE};
use std::collections::{HashMap, VecDeque};
pub(crate) struct Chart {
pub faces: Vec<u32>,
pub verts: Vec<u32>,
pub face_local: Vec<[u32; 3]>,
pub uvs: Vec<V2>,
pub size: V2,
pub area: f64,
}
struct Job<'a> {
topo: &'a Topology,
max_distortion: f64,
ctx: &'a Ctx<'a>,
total: usize,
}
impl Job<'_> {
fn done(
&self,
faces: Vec<u32>,
verts: Vec<u32>,
local: Vec<[u32; 3]>,
uvs: Vec<V2>,
) -> Vec<Chart> {
let _ = self
.ctx
.advance(Stage::Parameterizing, faces.len(), Some(self.total));
vec![finalize(self.topo, faces, verts, local, uvs)]
}
}
pub(crate) fn parameterize(
topo: &Topology,
groups: Vec<Vec<u32>>,
face_uvs: Option<&[[V2; 3]]>,
max_distortion: f64,
ctx: &Ctx,
total: usize,
) -> Vec<Chart> {
let job = Job {
topo,
max_distortion,
ctx,
total,
};
par::flat_map(groups, |g| match face_uvs {
Some(uvs) => island(&job, g, uvs),
None => parameterize_group(&job, g),
})
}
fn island(job: &Job, faces: Vec<u32>, face_uvs: &[[V2; 3]]) -> Vec<Chart> {
if job.ctx.is_cancelled() {
return Vec::new();
}
let mut map: HashMap<(u32, [u64; 2]), u32> = HashMap::with_capacity(faces.len());
let (mut verts, mut uvs) = (Vec::new(), Vec::new());
let local: Vec<[u32; 3]> = faces
.iter()
.map(|&f| {
let w = job.topo.faces[f as usize];
[0, 1, 2].map(|c| {
let uv = face_uvs[f as usize][c];
*map.entry((w[c], uv.map(|x| (x + 0.0).to_bits())))
.or_insert_with(|| {
verts.push(w[c]);
uvs.push(uv);
(verts.len() - 1) as u32
})
})
})
.collect();
if total_area(&local, &uvs) < 0.0 {
uvs.iter_mut().for_each(|p| p[0] = -p[0]);
}
if validate(job.topo, &faces, &local, &uvs, f64::INFINITY).is_ok() {
return job.done(faces, verts, local, uvs);
}
parameterize_group(job, faces)
}
enum Rejection {
Overlap(u32, u32),
Invalid,
}
fn total_area(local: &[[u32; 3]], uvs: &[V2]) -> f64 {
local
.iter()
.map(|t| signed_area(uvs[t[0] as usize], uvs[t[1] as usize], uvs[t[2] as usize]))
.sum()
}
const CURVATURE_CHECK_FACES: usize = 1000;
const MAX_CURVATURE: f64 = 3.0;
fn parameterize_group(job: &Job, faces: Vec<u32>) -> Vec<Chart> {
if job.ctx.is_cancelled() {
return Vec::new();
}
let (topo, max_distortion) = (job.topo, job.max_distortion);
let (verts, local) = build_local(topo, &faces);
let planar = planar_projection(topo, &faces, &verts);
if faces.len() == 1 || validate(topo, &faces, &local, &planar, 1.02).is_ok() {
return job.done(faces, verts, local, planar);
}
let hopeless = faces.len() >= CURVATURE_CHECK_FACES
&& total_curvature(topo, &verts, &local).abs() > MAX_CURVATURE;
let mut overlap = None;
if !hopeless {
let conformal = lscm(topo, &verts, &local, &planar);
if job.ctx.is_cancelled() {
return Vec::new(); }
match validate(topo, &faces, &local, &conformal, max_distortion) {
Ok(()) => return job.done(faces, verts, local, conformal),
Err(Rejection::Overlap(a, b)) => overlap = Some((a, b)),
Err(Rejection::Invalid) => {}
}
if validate(topo, &faces, &local, &planar, max_distortion).is_ok() {
return job.done(faces, verts, local, planar);
}
}
let (a, b) = split(topo, &faces, overlap);
let (mut a, b) = par::join(|| parameterize_group(job, a), || parameterize_group(job, b));
a.extend(b);
a
}
fn total_curvature(topo: &Topology, verts: &[u32], local: &[[u32; 3]]) -> f64 {
let n = verts.len();
let mut angle_sum = vec![0.0; n];
let mut edges: HashMap<(u32, u32), bool> = HashMap::with_capacity(local.len() * 2);
for t in local {
let p = t.map(|l| topo.positions[verts[l as usize] as usize]);
for k in 0..3 {
let a = sub3(p[(k + 1) % 3], p[k]);
let b = sub3(p[(k + 2) % 3], p[k]);
angle_sum[t[k] as usize] += angle(len3(cross3(a, b)), dot3(a, b));
let (u, v) = (t[k], t[(k + 1) % 3]);
let e = edges.entry((u.min(v), u.max(v))).or_insert(false);
*e = !*e;
}
}
let mut boundary = vec![false; n];
for (&(u, v), &odd) in &edges {
if odd {
boundary[u as usize] = true;
boundary[v as usize] = true;
}
}
(0..n)
.filter(|&i| !boundary[i])
.map(|i| std::f64::consts::TAU - angle_sum[i])
.sum()
}
fn build_local(topo: &Topology, faces: &[u32]) -> (Vec<u32>, Vec<[u32; 3]>) {
let mut map: HashMap<u32, u32> = HashMap::with_capacity(faces.len());
let mut verts = Vec::new();
let local = faces
.iter()
.map(|&f| {
topo.faces[f as usize].map(|v| {
*map.entry(v).or_insert_with(|| {
verts.push(v);
(verts.len() - 1) as u32
})
})
})
.collect();
(verts, local)
}
fn planar_projection(topo: &Topology, faces: &[u32], verts: &[u32]) -> Vec<V2> {
let mut nsum = [0.0; 3];
for &f in faces {
nsum = add3(
nsum,
scale3(topo.normals[f as usize], topo.areas[f as usize]),
);
}
let n = normalize3(nsum).unwrap_or(topo.normals[faces[0] as usize]);
let t = if n[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let u = normalize3(sub3(t, scale3(n, dot3(t, n)))).unwrap_or([0.0, 0.0, 1.0]);
let v = cross3(n, u); verts
.iter()
.map(|&w| {
let p = topo.positions[w as usize];
[dot3(p, u), dot3(p, v)]
})
.collect()
}
fn lscm(topo: &Topology, verts: &[u32], local: &[[u32; 3]], init: &[V2]) -> Vec<V2> {
let n = verts.len();
if n < 3 {
return init.to_vec();
}
let [lo, hi] = bounds2(init.iter().copied());
let axis = if hi[0] - lo[0] >= hi[1] - lo[1] { 0 } else { 1 };
let pin_a = (0..n)
.min_by(|&a, &b| init[a][axis].total_cmp(&init[b][axis]))
.unwrap();
let pin_b = (0..n)
.max_by(|&a, &b| init[a][axis].total_cmp(&init[b][axis]))
.unwrap();
if pin_a == pin_b {
return init.to_vec();
}
let mut col = vec![NONE; n];
let mut next = 0u32;
for (i, c) in col.iter_mut().enumerate() {
if i != pin_a && i != pin_b {
*c = next;
next += 2;
}
}
let ncols = next as usize;
let mut a = SparseRows::new(ncols);
let mut rhs = Vec::with_capacity(local.len() * 2);
for t in local {
let [p0, p1, p2] = t.map(|l| topo.positions[verts[l as usize] as usize]);
let e1 = sub3(p1, p0);
let e2 = sub3(p2, p0);
let nrm = cross3(e1, e2);
let two_area = len3(nrm);
let x1 = len3(e1);
if !(two_area > 0.0) || !(x1 > 0.0) {
continue;
}
let ax = scale3(e1, 1.0 / x1);
let ay = cross3(scale3(nrm, 1.0 / two_area), ax);
let (za, zb, zc, zd) = (x1, 0.0, dot3(e2, ax), dot3(e2, ay));
let w = 1.0 / two_area.sqrt();
let [i0, i1, i2] = t.map(|l| l as usize);
let rows: [[(usize, usize, f64); 6]; 2] = [
[
(i0, 0, zc - za),
(i0, 1, zb - zd),
(i1, 0, -zc),
(i1, 1, zd),
(i2, 0, za),
(i2, 1, -zb),
],
[
(i0, 0, zd - zb),
(i0, 1, zc - za),
(i1, 0, -zd),
(i1, 1, -zc),
(i2, 0, zb),
(i2, 1, za),
],
];
for row in rows {
let mut b = 0.0;
for (vi, comp, coef) in row {
if coef == 0.0 {
continue;
}
if col[vi] == NONE {
b -= w * coef * init[vi][comp];
} else {
a.push(col[vi] as usize + comp, w * coef);
}
}
a.end_row();
rhs.push(b);
}
}
let mut x = vec![0.0; ncols];
for i in 0..n {
if col[i] != NONE {
x[col[i] as usize] = init[i][0];
x[col[i] as usize + 1] = init[i][1];
}
}
least_squares(&mut a, &rhs, &mut x);
(0..n)
.map(|i| {
if col[i] == NONE {
init[i]
} else {
[x[col[i] as usize], x[col[i] as usize + 1]]
}
})
.collect()
}
fn validate(
topo: &Topology,
faces: &[u32],
local: &[[u32; 3]],
uv: &[V2],
max_distortion: f64,
) -> Result<(), Rejection> {
if uv.iter().any(|p| !p[0].is_finite() || !p[1].is_finite()) {
return Err(Rejection::Invalid);
}
let areas: Vec<f64> = local
.iter()
.map(|t| signed_area(uv[t[0] as usize], uv[t[1] as usize], uv[t[2] as usize]))
.collect();
let total_uv: f64 = areas.iter().sum();
let total_3d: f64 = faces.iter().map(|&f| topo.areas[f as usize]).sum();
if !(total_uv > 0.0) || !(total_3d > 0.0) {
return Err(Rejection::Invalid);
}
let min_area = total_uv * 1e-12;
let scale = total_3d / total_uv;
for (k, &f) in faces.iter().enumerate() {
if areas[k] <= min_area {
return Err(Rejection::Invalid);
}
let ratio = areas[k] * scale / topo.areas[f as usize];
if ratio > max_distortion || ratio * max_distortion < 1.0 {
return Err(Rejection::Invalid);
}
}
first_overlap(local, uv, total_uv).map_or(Ok(()), Err)
}
fn first_overlap(local: &[[u32; 3]], uv: &[V2], total_area: f64) -> Option<Rejection> {
let n = local.len();
if n < 2 {
return None;
}
let tris: Vec<[V2; 3]> = local.iter().map(|t| t.map(|l| uv[l as usize])).collect();
let boxes: Vec<[V2; 2]> = tris
.iter()
.map(|t| {
[
[
t[0][0].min(t[1][0]).min(t[2][0]),
t[0][1].min(t[1][1]).min(t[2][1]),
],
[
t[0][0].max(t[1][0]).max(t[2][0]),
t[0][1].max(t[1][1]).max(t[2][1]),
],
]
})
.collect();
let [lo, hi] = bounds2(uv.iter().copied());
let cell =
(2.0 * (total_area / n as f64).sqrt()).max((hi[0] - lo[0]).max(hi[1] - lo[1]) / 1024.0);
if !(cell > 0.0) {
return Some(Rejection::Invalid); }
let gx = (((hi[0] - lo[0]) / cell) as usize + 1).min(1024);
let gy = (((hi[1] - lo[1]) / cell) as usize + 1).min(1024);
let cell_of = |p: V2| {
(
(((p[0] - lo[0]) / cell) as usize).min(gx - 1),
(((p[1] - lo[1]) / cell) as usize).min(gy - 1),
)
};
let mut start = vec![0u32; gx * gy + 1];
for b in &boxes {
let ((x0, y0), (x1, y1)) = (cell_of(b[0]), cell_of(b[1]));
for y in y0..=y1 {
for x in x0..=x1 {
start[y * gx + x + 1] += 1;
}
}
}
for c in 0..gx * gy {
start[c + 1] += start[c];
}
let mut fill = start.clone();
let mut items = vec![0u32; start[gx * gy] as usize];
for (i, b) in boxes.iter().enumerate() {
let ((x0, y0), (x1, y1)) = (cell_of(b[0]), cell_of(b[1]));
for y in y0..=y1 {
for x in x0..=x1 {
items[fill[y * gx + x] as usize] = i as u32;
fill[y * gx + x] += 1;
}
}
}
let eps = total_area.sqrt() * 1e-9;
for y in 0..gy {
for x in 0..gx {
let c = &items[start[y * gx + x] as usize..start[y * gx + x + 1] as usize];
for (k, &a) in c.iter().enumerate() {
let ba = boxes[a as usize];
for &b in &c[k + 1..] {
let bb = boxes[b as usize];
if ba[1][0] < bb[0][0]
|| bb[1][0] < ba[0][0]
|| ba[1][1] < bb[0][1]
|| bb[1][1] < ba[0][1]
{
continue;
}
if cell_of([ba[0][0].max(bb[0][0]), ba[0][1].max(bb[0][1])]) != (x, y) {
continue;
}
if triangles_overlap(&tris[a as usize], &tris[b as usize], eps) {
return Some(Rejection::Overlap(a.min(b), a.max(b)));
}
}
}
}
}
None
}
fn split(topo: &Topology, faces: &[u32], seeds: Option<(u32, u32)>) -> (Vec<u32>, Vec<u32>) {
let index: HashMap<u32, usize> = faces.iter().enumerate().map(|(i, &f)| (f, i)).collect();
let bfs = |start: usize| -> Vec<u32> {
let mut dist = vec![u32::MAX; faces.len()];
let mut queue = VecDeque::from([start]);
dist[start] = 0;
while let Some(i) = queue.pop_front() {
for &nb in &topo.neighbors[faces[i] as usize] {
if let Some(&j) = index.get(&nb) {
if dist[j] == u32::MAX {
dist[j] = dist[i] + 1;
queue.push_back(j);
}
}
}
}
dist
};
let farthest = |d: &[u32]| {
(0..d.len())
.filter(|&i| d[i] != u32::MAX)
.max_by_key(|&i| d[i])
.unwrap_or(0)
};
let (da, db) = match seeds {
Some((a, b)) if a != b => (bfs(a as usize), bfs(b as usize)),
_ => {
let a = farthest(&bfs(0));
let da = bfs(a);
let b = farthest(&da);
let db = bfs(b);
(da, db)
}
};
let mut polygon_side: HashMap<u32, bool> = HashMap::new();
let (mut left, mut right) = (Vec::new(), Vec::new());
for (i, &f) in faces.iter().enumerate() {
let side = *polygon_side
.entry(topo.polygon[f as usize])
.or_insert(da[i] <= db[i]);
if side {
left.push(f);
} else {
right.push(f);
}
}
if left.is_empty() || right.is_empty() {
let mid = faces.len() / 2;
return (faces[..mid].to_vec(), faces[mid..].to_vec());
}
(left, right)
}
fn finalize(
topo: &Topology,
faces: Vec<u32>,
verts: Vec<u32>,
face_local: Vec<[u32; 3]>,
mut uvs: Vec<V2>,
) -> Chart {
let area: f64 = faces.iter().map(|&f| topo.areas[f as usize]).sum();
let uv_area = total_area(&face_local, &uvs);
let k = if uv_area > 0.0 {
(area / uv_area).sqrt()
} else {
1.0
};
let (c, s) = min_area_rect_dir(&convex_hull(&uvs));
for p in uvs.iter_mut() {
let r = rotate(*p, c, s);
*p = [r[0] * k, r[1] * k];
}
let [lo, hi] = bounds2(uvs.iter().copied());
for p in uvs.iter_mut() {
*p = [p[0] - lo[0], p[1] - lo[1]];
}
Chart {
faces,
verts,
face_local,
uvs,
size: [hi[0] - lo[0], hi[1] - lo[1]],
area,
}
}