use draco_oxide_core::attribute::{Attribute, AttributeType};
use draco_oxide_core::mesh::ds::{
AttributeCornerTable, AttributeDS, CornerTable, GenericCornerTable, DS,
};
use draco_oxide_core::safety_assert;
use draco_oxide_core::types::{
AttributeValueIdx, CornerIdx, PointIdx, VecCornerIdx, VecPointIdx, VecVertexIdx, VertexIdx,
};
fn compute_seam_edges<F>(
pos_corner_table: &CornerTable,
num_corners: usize,
att_val: F,
) -> VecCornerIdx<bool>
where
F: Fn(CornerIdx) -> AttributeValueIdx,
{
let mut is_edge_on_seam: VecCornerIdx<bool> = vec![false; num_corners].into();
for c in 0..num_corners {
let c = CornerIdx::from(c);
let Some(opp_corner) = pos_corner_table.opposite(c) else {
is_edge_on_seam[c] = true;
continue;
};
if usize::from(opp_corner) < usize::from(c) {
continue;
}
let mut c1 = c;
let mut c2 = opp_corner;
for _ in 0..2 {
c1 = c1.next();
c2 = c2.previous();
if att_val(c1) != att_val(c2) {
is_edge_on_seam[c] = true;
is_edge_on_seam[opp_corner] = true;
break;
}
}
}
is_edge_on_seam
}
pub(crate) fn build_attribute_ds<'a>(
ds: &'a DS,
pos_corner_table: &'a CornerTable,
attributes: Vec<Attribute>,
) -> Vec<AttributeDS<'a>> {
let pos_reference = attributes
.iter()
.position(|att| att.get_attribute_type() == AttributeType::Position);
attributes
.into_iter()
.enumerate()
.map(|(i, att)| {
build_single_attribute_ds(ds, pos_corner_table, att, pos_reference == Some(i))
})
.collect()
}
fn build_single_attribute_ds<'a>(
ds: &'a DS,
pos_corner_table: &'a CornerTable,
att: Attribute,
is_pos_reference: bool,
) -> AttributeDS<'a> {
let num_corners = ds.num_corners();
let num_points = ds.num_points();
let corner_table = if is_pos_reference {
AttributeCornerTable::shared(pos_corner_table)
} else {
let is_edge_on_seam = compute_seam_edges(pos_corner_table, num_corners, |c| {
att.get_unique_val_idx(ds.point_idx(c))
});
AttributeCornerTable::new(pos_corner_table, is_edge_on_seam)
};
let mut point_to_vertex_map = vec![VertexIdx::INVALID; num_points];
let mut vertex_to_left_most_corner: Vec<CornerIdx> = Vec::new();
let mut visited = vec![false; num_corners];
for start in 0..num_corners {
if visited[start] {
continue;
}
let start = CornerIdx::from(start);
let mut pos_left_most = start;
while let Some(l) = pos_corner_table.swing_left(pos_left_most) {
if l == start {
break;
}
pos_left_most = l;
}
let mut first_c = pos_left_most;
while let Some(l) = corner_table.swing_left(first_c) {
if l == pos_left_most {
break;
}
first_c = l;
}
let mut cur_vert_id = VertexIdx::from(vertex_to_left_most_corner.len());
vertex_to_left_most_corner.push(first_c);
let p = usize::from(ds.point_idx(first_c));
safety_assert!(
point_to_vertex_map[p] == VertexIdx::INVALID || point_to_vertex_map[p] == cur_vert_id,
"point {} spans multiple attribute sectors; sort_mesh must have split it",
p
);
point_to_vertex_map[p] = cur_vert_id;
visited[usize::from(first_c)] = true;
let mut prev_c = first_c;
while let Some(curr) = pos_corner_table.swing_right(prev_c) {
if curr == first_c {
break;
}
visited[usize::from(curr)] = true;
if corner_table.is_corner_opposite_to_seam_edge(curr.next()) {
cur_vert_id = VertexIdx::from(vertex_to_left_most_corner.len());
vertex_to_left_most_corner.push(curr);
}
let p = usize::from(ds.point_idx(curr));
safety_assert!(
point_to_vertex_map[p] == VertexIdx::INVALID
|| point_to_vertex_map[p] == cur_vert_id,
"point {} spans multiple attribute sectors; sort_mesh must have split it",
p
);
point_to_vertex_map[p] = cur_vert_id;
prev_c = curr;
}
}
let vertex_to_left_most_corner_map: VecVertexIdx<CornerIdx> = vertex_to_left_most_corner.into();
let point_to_vertex_map: VecPointIdx<VertexIdx> = point_to_vertex_map.into();
AttributeDS::new(
ds,
corner_table,
vertex_to_left_most_corner_map,
point_to_vertex_map,
att,
)
}
pub(crate) fn build_global_ds(
mut mesh_faces: Vec<[PointIdx; 3]>,
attributes: &mut [Attribute],
) -> (DS, CornerTable) {
let pos_att = attributes
.iter()
.find(|att| att.get_attribute_type() == AttributeType::Position)
.expect("position attribute must be present");
let pos_faces = mesh_faces
.iter()
.map(|face| {
[
usize::from(pos_att.get_unique_val_idx(face[0])).into(),
usize::from(pos_att.get_unique_val_idx(face[1])).into(),
usize::from(pos_att.get_unique_val_idx(face[2])).into(),
]
})
.collect::<Vec<[VertexIdx; 3]>>();
let edge_pairs = manifold_edge_pairs(&pos_faces);
sort_mesh(&pos_faces, &edge_pairs, &mut mesh_faces, attributes);
let corner_table = compute_corner_table(pos_faces.len() * 3, &edge_pairs);
let corner_to_point_map: VecCornerIdx<PointIdx> = mesh_faces
.iter()
.flat_map(|face| face.iter().copied())
.collect::<Vec<_>>()
.into();
let ds = DS::new(corner_to_point_map);
(ds, corner_table)
}
fn sort_mesh(
pos_faces: &[[VertexIdx; 3]],
edge_pairs: &[[CornerIdx; 2]],
mesh_faces: &mut [[PointIdx; 3]],
attributes: &mut [Attribute],
) {
let num_corners = pos_faces.len() * 3;
let mut parent: Vec<usize> = (0..num_corners).collect();
for &[c1, c2] in edge_pairs {
let point = |c: CornerIdx| mesh_faces[usize::from(c) / 3][usize::from(c) % 3];
if point(c1.next()) != point(c2.previous()) || point(c1.previous()) != point(c2.next()) {
continue;
}
uf_union(
&mut parent,
usize::from(c1.next()),
usize::from(c2.previous()),
);
uf_union(
&mut parent,
usize::from(c1.previous()),
usize::from(c2.next()),
);
}
debug_assert!(
attributes.windows(2).all(|w| w[0].len() == w[1].len()),
"attributes must share one point space"
);
let num_points = attributes.first().expect("attributes is non-empty").len();
let mut owner_root: Vec<usize> = vec![usize::MAX; num_points];
let mut minted: Vec<PointIdx> = vec![PointIdx::INVALID; num_corners];
for c in 0..num_corners {
let root = uf_find(&mut parent, c);
let p = mesh_faces[c / 3][c % 3];
let owner = &mut owner_root[usize::from(p)];
if *owner == usize::MAX {
*owner = root;
}
if *owner == root {
continue;
}
if minted[root] == PointIdx::INVALID {
let mut np: Option<PointIdx> = None;
for att in attributes.iter_mut() {
let m = att.mint(p);
match np {
Some(prev) => debug_assert_eq!(
prev, m,
"attributes must mint in lockstep so point spaces stay equal"
),
None => np = Some(m),
}
}
minted[root] = np.expect("attributes is non-empty");
}
mesh_faces[c / 3][c % 3] = minted[root];
}
}
fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
fn uf_union(parent: &mut [usize], a: usize, b: usize) {
let ra = uf_find(parent, a);
let rb = uf_find(parent, b);
if ra != rb {
parent[ra] = rb;
}
}
fn manifold_edge_pairs(pos_faces: &[[VertexIdx; 3]]) -> Vec<[CornerIdx; 2]> {
let num_corners = pos_faces.len() * 3;
let edge_of = |f: usize, i: usize| -> [u32; 2] {
let face = &pos_faces[f];
let a = usize::from(face[(i + 1) % 3]) as u32;
let b = usize::from(face[(i + 2) % 3]) as u32;
if a < b {
[a, b]
} else {
[b, a]
}
};
let mut num_vertices = 0usize;
for face in pos_faces {
for v in face {
num_vertices = num_vertices.max(usize::from(*v) + 1);
}
}
let mut head_hi = vec![0u32; num_vertices + 1];
for f in 0..pos_faces.len() {
for i in 0..3 {
head_hi[edge_of(f, i)[1] as usize + 1] += 1;
}
}
counts_to_offsets(&mut head_hi);
let mut by_hi = vec![[0u32; 2]; num_corners];
let mut cursor = head_hi.clone();
for f in 0..pos_faces.len() {
for i in 0..3 {
let [lo, hi] = edge_of(f, i);
let slot = &mut cursor[hi as usize];
by_hi[*slot as usize] = [lo, (f * 3 + i) as u32];
*slot += 1;
}
}
let mut head_lo = vec![0u32; num_vertices + 1];
for entry in &by_hi {
head_lo[entry[0] as usize + 1] += 1;
}
counts_to_offsets(&mut head_lo);
let mut by_lo = vec![[0u32; 2]; num_corners];
let mut cursor = head_lo.clone();
for hi in 0..num_vertices {
for &[lo, c] in &by_hi[head_hi[hi] as usize..head_hi[hi + 1] as usize] {
let slot = &mut cursor[lo as usize];
by_lo[*slot as usize] = [hi as u32, c];
*slot += 1;
}
}
let mut pairs = Vec::new();
for v in 0..num_vertices {
let bucket = &by_lo[head_lo[v] as usize..head_lo[v + 1] as usize];
let mut i = 0;
while i < bucket.len() {
let mut j = i + 1;
while j < bucket.len() && bucket[j][0] == bucket[i][0] {
j += 1;
}
if j - i == 2 {
let c1 = CornerIdx::from(bucket[i][1] as usize);
let c2 = CornerIdx::from(bucket[i + 1][1] as usize);
if edge_orientation_consistent(pos_faces, c1, c2) {
pairs.push([c1, c2]);
}
}
i = j;
}
}
pairs
}
fn counts_to_offsets(counts: &mut [u32]) {
for i in 1..counts.len() {
counts[i] += counts[i - 1];
}
}
fn edge_orientation_consistent(pos_faces: &[[VertexIdx; 3]], c: CornerIdx, opp: CornerIdx) -> bool {
let pos_vertex = |c: CornerIdx| pos_faces[usize::from(c) / 3][usize::from(c) % 3];
pos_vertex(c.next()) == pos_vertex(opp.previous())
&& pos_vertex(c.previous()) == pos_vertex(opp.next())
}
fn compute_corner_table(num_corners: usize, edge_pairs: &[[CornerIdx; 2]]) -> CornerTable {
let mut opposite: Vec<CornerIdx> = vec![CornerIdx::INVALID; num_corners];
for &[c1, c2] in edge_pairs {
opposite[usize::from(c1)] = c2;
opposite[usize::from(c2)] = c1;
}
CornerTable::from_opposite_sentinels(opposite)
}
#[cfg(test)]
mod tests {
use super::*;
use draco_oxide_core::attribute::{Attribute, AttributeDomain, AttributeType};
use draco_oxide_core::types::{FaceIdx, NdVector};
fn pos_attribute(vals: Vec<[f32; 3]>) -> Attribute {
Attribute::new(
vals.into_iter().map(NdVector::<3, f32>::from).collect(),
AttributeType::Position,
AttributeDomain::Position,
Vec::new(),
)
}
fn pos_attribute_2d(vals: Vec<[f32; 2]>) -> Attribute {
Attribute::new(
vals.into_iter().map(NdVector::<2, f32>::from).collect(),
AttributeType::Position,
AttributeDomain::Position,
Vec::new(),
)
}
fn faces(raw: Vec<[usize; 3]>) -> Vec<[PointIdx; 3]> {
raw.into_iter()
.map(|f| {
[
PointIdx::from(f[0]),
PointIdx::from(f[1]),
PointIdx::from(f[2]),
]
})
.collect()
}
#[test]
fn build_global_ds_does_not_inflate_points_on_closed_tetrahedron() {
let faces: Vec<[PointIdx; 3]> = vec![[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]]
.into_iter()
.map(|f| {
[
PointIdx::from(f[0]),
PointIdx::from(f[1]),
PointIdx::from(f[2]),
]
})
.collect();
let mut attributes = vec![pos_attribute(vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
])];
let (ds, pos_corner_table) = build_global_ds(faces, &mut attributes);
assert_eq!(
ds.num_points(),
4,
"closed manifold tetrahedron must not mint points"
);
let _adss = build_attribute_ds(&ds, &pos_corner_table, attributes);
}
#[test]
fn corner_table_two_triangles() {
let mut attributes = vec![pos_attribute_2d(vec![
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
])];
let (ds, ct) = build_global_ds(faces(vec![[0, 1, 2], [2, 1, 3]]), &mut attributes);
let adss = build_attribute_ds(&ds, &ct, attributes);
let pos = &adss[0];
assert_eq!(ds.num_faces(), 2);
assert_eq!(ds.num_corners(), 6);
assert_eq!(ds.num_points(), 4);
assert_eq!(pos.num_vertices(), 4);
for c in 0..6 {
assert_eq!(CornerIdx::from(c).face_idx(), FaceIdx::from(c / 3));
}
assert_eq!(ct.opposite(CornerIdx::from(0)), Some(CornerIdx::from(5)));
assert_eq!(ct.opposite(CornerIdx::from(5)), Some(CornerIdx::from(0)));
for c in [1, 2, 3, 4] {
assert!(ct.opposite(CornerIdx::from(c)).is_none());
}
}
#[test]
fn corner_table_no_attribute_seam() {
let mut attributes = vec![pos_attribute(vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 0.5, 0.0],
[1.0, 0.5, 0.0],
])];
let (ds, ct) = build_global_ds(
faces(vec![[0, 1, 2], [1, 3, 2], [2, 3, 4], [2, 4, 5]]),
&mut attributes,
);
let adss = build_attribute_ds(&ds, &ct, attributes);
assert_eq!(ds.num_faces(), 4);
assert_eq!(ds.num_corners(), 12);
assert_eq!(ds.num_points(), 6);
assert_eq!(adss[0].num_vertices(), 6);
}
#[test]
fn corner_table_triangle() {
let mut attributes = vec![pos_attribute_2d(vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])];
let (ds, ct) = build_global_ds(faces(vec![[0, 1, 2]]), &mut attributes);
let adss = build_attribute_ds(&ds, &ct, attributes);
let pos = &adss[0];
assert_eq!(ds.num_faces(), 1);
assert_eq!(ds.num_corners(), 3);
assert_eq!(ds.num_points(), 3);
assert_eq!(pos.num_vertices(), 3);
for v in 0..pos.num_vertices() {
let v = VertexIdx::from(v);
assert_eq!(pos.vertex_idx(pos.left_most_corner(v)), v);
}
}
#[test]
fn corner_table_non_manifold_vertex() {
let mut attributes = vec![pos_attribute_2d(vec![
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[-1.0, 1.0],
[0.0, -1.0],
])];
let (ds, ct) = build_global_ds(faces(vec![[0, 1, 2], [0, 3, 4]]), &mut attributes);
let adss = build_attribute_ds(&ds, &ct, attributes);
let pos = &adss[0];
assert_eq!(ds.num_faces(), 2);
assert_eq!(ds.num_corners(), 6);
assert_eq!(ds.num_points(), 6);
assert_eq!(pos.num_vertices(), 6);
for v in 0..pos.num_vertices() {
let v = VertexIdx::from(v);
assert_eq!(pos.vertex_idx(pos.left_most_corner(v)), v);
}
}
#[test]
fn corner_table_non_manifold_edge_is_boundary() {
let mut attributes = vec![pos_attribute_2d(vec![
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[-1.0, 0.0],
])];
let (ds, ct) = build_global_ds(
faces(vec![[0, 1, 2], [1, 3, 2], [2, 1, 4]]),
&mut attributes,
);
let _adss = build_attribute_ds(&ds, &ct, attributes);
for c in [0, 4, 8] {
assert!(
ct.opposite(CornerIdx::from(c)).is_none(),
"corner {c} across the non-manifold edge must be a boundary"
);
}
}
fn mobius_band() -> Vec<[PointIdx; 3]> {
faces(vec![[0, 1, 2], [2, 1, 3], [2, 3, 4], [4, 3, 0], [4, 0, 1]])
}
fn mobius_positions() -> Vec<Attribute> {
vec![pos_attribute(vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
])]
}
#[test]
fn build_global_ds_cuts_twist_edge_of_non_orientable_surface() {
let mut attributes = mobius_positions();
let (ds, ct) = build_global_ds(mobius_band(), &mut attributes);
assert_eq!(ds.num_faces(), 5);
assert_eq!(ds.num_corners(), 15);
let linked = (0..ds.num_corners())
.filter(|&c| ct.opposite(CornerIdx::from(c)).is_some())
.count();
assert_eq!(linked, 8, "one core (twist) edge should be cut");
}
#[test]
fn build_attribute_ds_handles_non_orientable_surface() {
let mut attributes = mobius_positions();
let (ds, ct) = build_global_ds(mobius_band(), &mut attributes);
assert_eq!(ds.num_points(), 7, "two aliasing points from the cut");
let adss = build_attribute_ds(&ds, &ct, attributes);
let pos = &adss[0];
assert_eq!(pos.att_data().len(), ds.num_points());
assert_eq!(pos.num_vertices(), ds.num_points());
for v in 0..pos.num_vertices() {
let v = VertexIdx::from(v);
assert_eq!(pos.vertex_idx(pos.left_most_corner(v)), v);
}
}
#[test]
fn build_global_ds_cuts_inconsistent_orientable_quad() {
let mut attributes = vec![pos_attribute_2d(vec![
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
])];
let (ds, ct) = build_global_ds(faces(vec![[0, 1, 2], [1, 2, 3]]), &mut attributes);
assert_eq!(ds.num_points(), 6);
let linked = (0..ds.num_corners())
.filter(|&c| ct.opposite(CornerIdx::from(c)).is_some())
.count();
assert_eq!(linked, 0);
let adss = build_attribute_ds(&ds, &ct, attributes);
assert_eq!(adss[0].num_vertices(), 6);
}
#[test]
fn build_global_ds_cuts_inconsistent_orientable_strip() {
let mut attributes = vec![pos_attribute_2d(vec![
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[2.0, 0.0],
])];
let (ds, ct) = build_global_ds(
faces(vec![[0, 1, 2], [2, 1, 3], [3, 2, 4]]),
&mut attributes,
);
assert_eq!(ds.num_points(), 7);
let linked = (0..ds.num_corners())
.filter(|&c| ct.opposite(CornerIdx::from(c)).is_some())
.count();
assert_eq!(linked, 2);
let adss = build_attribute_ds(&ds, &ct, attributes);
assert_eq!(adss[0].num_vertices(), 7);
}
}