use std::num::NonZeroU8;
use subdiv_kernels::{
QuadClass, RefinementResult, Refiner, Scheme, SchemeOptions, Mesh, UniformRefine,
};
const CORNER_TOLERANCE: f32 = 1e-5;
fn quad_grid(n: u32, height: impl Fn(u32, u32) -> f32) -> (Mesh, Vec<[f32; 3]>) {
let stride = n + 1;
let height = &height;
let positions: Vec<[f32; 3]> = (0..stride)
.flat_map(|i| (0..stride).map(move |j| [i as f32, height(i, j), j as f32]))
.collect();
let vid = |i: u32, j: u32| i * stride + j;
let face_vertex_indices: Vec<u32> = (0..n)
.flat_map(|i| {
(0..n).flat_map(move |j| [vid(i, j), vid(i + 1, j), vid(i + 1, j + 1), vid(i, j + 1)])
})
.collect();
let topo = Mesh {
vertex_count: stride * stride,
face_vertex_counts: vec![4; (n * n) as usize],
face_vertex_indices,
edge_vertices: Vec::new(),
edge_creases: Vec::new(),
vertex_corners: vec![0.0; (stride * stride) as usize],
};
(topo, positions)
}
fn cube() -> (Mesh, Vec<[f32; 3]>) {
let positions: Vec<[f32; 3]> = vec![
[-1.0, -1.0, -1.0],
[1.0, -1.0, -1.0],
[1.0, 1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
];
let face_vertex_indices: Vec<u32> = vec![
0, 3, 2, 1, 4, 5, 6, 7, 0, 1, 5, 4, 1, 2, 6, 5, 2, 3, 7, 6, 3, 0, 4, 7, ];
let topo = Mesh {
vertex_count: 8,
face_vertex_counts: vec![4; 6],
face_vertex_indices,
edge_vertices: Vec::new(),
edge_creases: Vec::new(),
vertex_corners: vec![0.0; 8],
};
(topo, positions)
}
fn refine(topo: Mesh, levels: u8) -> RefinementResult {
let refiner = Refiner::new(topo, Scheme::CatmullClark, SchemeOptions::default())
.expect("refiner");
refiner
.refine_uniform(&UniformRefine::from(
NonZeroU8::new(levels).expect("non-zero"),
))
.expect("refinement")
}
fn valence_three(result: &RefinementResult) -> Vec<bool> {
(0..result.topology.vertex_count as usize)
.map(|vi| {
let start = result.adjacency.vertex_edge_offsets[vi];
let end = result.adjacency.vertex_edge_offsets[vi + 1];
end - start == 3
})
.collect()
}
#[test]
fn grid_interior_quads_are_regular_boundary_adjacent_are_feature() {
let (topo, _) = quad_grid(6, |_, _| 0.0);
let result = refine(topo, 1);
let table = result.patch_table().expect("patch table");
let face_count = result.topology.face_vertex_counts.len();
assert_eq!(face_count, 144);
for face in 0..face_count {
let corners = &result.topology.face_vertex_indices[face * 4..face * 4 + 4];
let touches_boundary = corners
.iter()
.any(|&c| result.adjacency.vertex_is_boundary[c as usize]);
let expected = if touches_boundary {
QuadClass::Feature
} else {
QuadClass::Regular
};
assert_eq!(
table.quad_class(face as u32),
expected,
"refined quad {face}",
);
assert_eq!(
table.face_patch(face as u32).is_some(),
expected == QuadClass::Regular,
"refined quad {face}",
);
}
assert_eq!(table.control_points.len(), 100);
assert_eq!(table.faces.len(), 100);
for (patch, &face) in table.faces.iter().enumerate() {
assert_eq!(table.face_patch(face), Some(patch as u32));
}
}
#[test]
fn once_refined_cube_has_no_regular_patches() {
let (topo, _) = cube();
let result = refine(topo, 1);
let table = result.patch_table().expect("patch table");
assert!(table.control_points.is_empty());
assert!(table.faces.is_empty());
assert_eq!(result.topology.face_vertex_counts.len(), 24);
for face in 0..24 {
assert_eq!(table.quad_class(face), QuadClass::Feature);
}
}
#[test]
fn twice_refined_cube_isolates_extraordinary_vertices() {
let (topo, _) = cube();
let result = refine(topo, 2);
let table = result.patch_table().expect("patch table");
let face_count = result.topology.face_vertex_counts.len();
assert_eq!(face_count, 96);
assert_eq!(table.control_points.len(), 72);
let extraordinary = valence_three(&result);
for face in 0..face_count {
let corners = &result.topology.face_vertex_indices[face * 4..face * 4 + 4];
let touches_ev = corners.iter().any(|&c| extraordinary[c as usize]);
let expected = if touches_ev {
QuadClass::Feature
} else {
QuadClass::Regular
};
assert_eq!(
table.quad_class(face as u32),
expected,
"refined quad {face}",
);
}
}
#[test]
fn flat_grid_patch_is_the_bilinear_chart_of_its_quad() {
let (topo, positions) = quad_grid(6, |_, _| 0.0);
let result = refine(topo, 1);
let table = result.patch_table().expect("patch table");
let refined = result.interpolate(&positions);
let (patch, face) = table
.faces
.iter()
.enumerate()
.find(|&(_, &face)| {
let corners = &result.topology.face_vertex_indices[face as usize * 4..face as usize * 4 + 4];
corners.iter().all(|&c| {
let p = refined[c as usize];
(3.0..=3.5).contains(&p[0]) && (3.0..=3.5).contains(&p[2])
})
})
.map(|(patch, &face)| (patch, face))
.expect("central patch");
let corners = &result.topology.face_vertex_indices[face as usize * 4..face as usize * 4 + 4];
let c0 = refined[corners[0] as usize];
let c1 = refined[corners[1] as usize];
let c3 = refined[corners[3] as usize];
for &(u, v) in &[(0.0f32, 0.0f32), (1.0, 0.0), (0.5, 0.5), (0.3, 0.7), (0.85, 0.15)] {
let (position, du, dv) = table.eval_with_derivatives(patch, [u, v], &refined);
for c in 0..3 {
let expected = c0[c] + u * (c1[c] - c0[c]) + v * (c3[c] - c0[c]);
assert!(
(position[c] - expected).abs() < 1e-6,
"patch {patch} at ({u}, {v}): position {position:?} is not the bilinear chart",
);
assert!(
(du[c] - (c1[c] - c0[c])).abs() < 1e-6,
"patch {patch} at ({u}, {v}): du {du:?} is not corner 0 -> 1",
);
assert!(
(dv[c] - (c3[c] - c0[c])).abs() < 1e-6,
"patch {patch} at ({u}, {v}): dv {dv:?} is not corner 0 -> 3",
);
}
}
}
fn assert_corners_match_limit_stencils(result: &RefinementResult, positions: &[[f32; 3]]) {
let table = result.patch_table().expect("patch table");
assert!(!table.faces.is_empty(), "case has no regular patches");
let refined = result.interpolate(positions);
let limit = result.limit_stencils().expect("limit stencils");
let limit_positions = limit.position.interpolate(&refined);
for (patch, &face) in table.faces.iter().enumerate() {
let corners = &result.topology.face_vertex_indices[face as usize * 4..face as usize * 4 + 4];
for (k, &(u, v)) in [(0.0f32, 0.0f32), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
.iter()
.enumerate()
{
let evaluated = table.eval(patch, [u, v], &refined);
let stencil = limit_positions[corners[k] as usize];
for c in 0..3 {
assert!(
(evaluated[c] - stencil[c]).abs() < CORNER_TOLERANCE,
"patch {patch} corner {k}: eval {evaluated:?} vs limit stencil {stencil:?}",
);
}
}
}
}
#[test]
fn cube_patch_corners_match_limit_stencils() {
let (topo, positions) = cube();
let result = refine(topo, 2);
assert_corners_match_limit_stencils(&result, &positions);
}
#[test]
fn warped_grid_patch_corners_match_limit_stencils() {
let (topo, positions) = quad_grid(6, |i, j| {
0.25 * (i as f32) * (i as f32) - 0.2 * (j as f32) * (i as f32 + 1.0)
});
let result = refine(topo, 1);
assert_corners_match_limit_stencils(&result, &positions);
}