use std::collections::HashMap;
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 opp_corner = pos_corner_table.opposite(c);
if opp_corner.is_none() {
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>> {
attributes
.into_iter()
.map(|att| build_single_attribute_ds(ds, pos_corner_table, att))
.collect()
}
fn build_single_attribute_ds<'a>(
ds: &'a DS,
pos_corner_table: &'a CornerTable,
att: Attribute,
) -> AttributeDS<'a> {
let num_corners = ds.num_corners();
let num_points = ds.num_points();
let is_edge_on_seam = compute_seam_edges(pos_corner_table, num_corners, |c| {
att.get_unique_val_idx(ds.point_idx(c))
});
let corner_table = AttributeCornerTable::new(pos_corner_table, is_edge_on_seam);
let mut point_to_vertex_map = vec![VertexIdx::from(usize::MAX); 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;
loop {
let l = pos_corner_table.swing_left(pos_left_most);
if l.is_some() {
if l == start {
break;
} else {
pos_left_most = l;
}
} else {
break;
}
}
let mut first_c = pos_left_most;
loop {
let l = corner_table.swing_left(first_c);
if l.is_some() {
if l == pos_left_most {
break;
} else {
first_c = l;
}
} else {
break;
}
}
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::from(usize::MAX)
|| 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 maybe_curr = pos_corner_table.swing_right(first_c);
while maybe_curr.is_some() {
let curr = maybe_curr;
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::from(usize::MAX)
|| 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;
maybe_curr = pos_corner_table.swing_right(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]>>();
sort_mesh(&pos_faces, &mut mesh_faces, attributes);
let corner_table = compute_corner_table(&pos_faces);
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]],
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 corners in edge_coboundary(pos_faces).values() {
if corners.len() != 2 {
continue;
}
let c1 = CornerIdx::from(corners[0]);
let c2 = CornerIdx::from(corners[1]);
if !edge_orientation_consistent(pos_faces, c1, c2) {
continue;
}
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 mut first_root: HashMap<PointIdx, usize> = HashMap::new();
let mut minted: HashMap<(PointIdx, usize), PointIdx> = HashMap::new();
for c in 0..num_corners {
let root = uf_find(&mut parent, c);
let p = mesh_faces[c / 3][c % 3];
let owner = *first_root.entry(p).or_insert(root);
if owner == root {
continue;
}
let np = *minted.entry((p, root)).or_insert_with(|| {
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),
}
}
np.expect("attributes is non-empty")
});
mesh_faces[c / 3][c % 3] = np;
}
}
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 edge_coboundary(pos_faces: &[[VertexIdx; 3]]) -> HashMap<(usize, usize), Vec<usize>> {
let pos_vertex = |c: CornerIdx| usize::from(pos_faces[usize::from(c) / 3][usize::from(c) % 3]);
let mut coboundary: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for c in 0..pos_faces.len() * 3 {
let c = CornerIdx::from(c);
let next = pos_vertex(c.next());
let prev = pos_vertex(c.previous());
let entry = if next < prev {
(next, prev)
} else {
(prev, next)
};
coboundary.entry(entry).or_default().push(c.into());
}
coboundary
}
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(pos_faces: &[[VertexIdx; 3]]) -> CornerTable {
let num_corners = pos_faces.len() * 3;
let mut opposite: VecCornerIdx<CornerIdx> = vec![CornerIdx::none(); num_corners].into();
for corners in edge_coboundary(pos_faces).values() {
if corners.len() != 2 {
continue;
}
let c1 = CornerIdx::from(corners[0]);
let c2 = CornerIdx::from(corners[1]);
opposite[c1] = c2;
opposite[c2] = c1;
}
cut_orientation_seams(&mut opposite, pos_faces);
CornerTable::from_raw_data(opposite)
}
fn cut_orientation_seams(corner_table: &mut VecCornerIdx<CornerIdx>, pos_faces: &[[VertexIdx; 3]]) {
for c in 0..pos_faces.len() * 3 {
let c = CornerIdx::from(c);
let opp_c = corner_table[c];
if opp_c.is_none() || usize::from(opp_c) < usize::from(c) {
continue;
}
if !edge_orientation_consistent(pos_faces, c, opp_c) {
corner_table[c] = CornerIdx::none();
corner_table[opp_c] = CornerIdx::none();
}
}
}
#[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)), CornerIdx::from(5));
assert_eq!(ct.opposite(CornerIdx::from(5)), 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);
}
}