mod common;
use std::collections::BTreeMap;
use std::num::NonZeroU8;
use common::{Case, cube_case, grid_case, length, spoked_cube_case, sub, v3};
use subdiv_kernels::{
LimitEvaluator, QuadClass, RefinementResult, Refiner, Scheme, UniformRefine,
};
const IDENTITY_TOLERANCE: f64 = 1e-5;
const UNITY_TOLERANCE: f64 = 1e-6;
const UV_SAMPLES: [[f32; 2]; 8] = [
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
[0.5, 0.0],
[0.5, 0.5],
[0.3, 0.7],
[0.85, 0.15],
];
const CORNER_UV: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
struct EvaluatedCase {
result: RefinementResult,
refined: Vec<[f32; 3]>,
}
fn evaluated_case(case: &Case, level: u8) -> EvaluatedCase {
let refiner =
Refiner::new(case.topology(), Scheme::CatmullClark, case.options).expect("refiner");
let result = refiner
.refine_uniform(&UniformRefine::from(
NonZeroU8::new(level).expect("non-zero"),
))
.expect("refinement");
let refined = result.interpolate(&case.positions);
EvaluatedCase { result, refined }
}
fn apply(row: &[(u32, f32)], positions: &[[f32; 3]]) -> [f64; 3] {
row.iter().fold([0.0f64; 3], |acc, &(i, w)| {
let p = positions[i as usize];
[
acc[0] + w as f64 * p[0] as f64,
acc[1] + w as f64 * p[1] as f64,
acc[2] + w as f64 * p[2] as f64,
]
})
}
fn assert_row_gates(
evaluator: &LimitEvaluator,
refined: &[[f32; 3]],
face: u32,
uv: [f32; 2],
) -> Vec<(u32, f32)> {
let row = evaluator.weights_at(face, uv).expect("weights_at");
assert!(!row.is_empty(), "face {face} at {uv:?}: empty weight row");
let unique: BTreeMap<u32, f32> = row.iter().copied().collect();
assert_eq!(
unique.len(),
row.len(),
"face {face} at {uv:?}: duplicate indices in {row:?}",
);
assert!(
row.iter().all(|&(i, _)| (i as usize) < refined.len()),
"face {face} at {uv:?}: index out of range in {row:?}",
);
let total: f64 = row.iter().map(|&(_, w)| w as f64).sum();
assert!(
(total - 1.0).abs() <= UNITY_TOLERANCE,
"face {face} at {uv:?}: weights sum to {total}, not 1",
);
let evaluated = v3(evaluator.eval(face, uv).expect("eval"));
let folded = apply(&row, refined);
let distance = length(sub(folded, evaluated));
assert!(
distance <= IDENTITY_TOLERANCE,
"face {face} at {uv:?}: row gives {folded:?}, eval gives {evaluated:?} \
({distance} apart)",
);
row
}
fn sweep_case(case: &Case, level: u8) -> (usize, usize) {
let ours = evaluated_case(case, level);
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
let face_count = ours.result.topology.face_vertex_counts.len() as u32;
let (mut regular, mut feature) = (0, 0);
for face in 0..face_count {
match evaluator.quad_class(face) {
QuadClass::Regular => regular += 1,
QuadClass::Feature => feature += 1,
}
for &uv in &UV_SAMPLES {
assert_row_gates(&evaluator, &ours.refined, face, uv);
}
}
(regular, feature)
}
#[test]
fn cube_rows_match_eval() {
let (regular, feature) = sweep_case(&cube_case(false), 1);
assert_eq!((regular, feature), (0, 24));
}
#[test]
fn creased_cube_rows_match_eval() {
let (regular, feature) = sweep_case(&cube_case(true), 2);
assert!(regular > 0 && feature > 0);
}
#[test]
fn warped_grid_rows_match_eval() {
let case = grid_case(6, |i, j| {
0.25 * (i as f32) * (i as f32) - 0.2 * (j as f32) * (i as f32 + 1.0)
});
let (regular, feature) = sweep_case(&case, 1);
assert!(regular > 0 && feature > 0);
}
#[test]
fn spoked_cube_rows_match_eval() {
let (case, _) = spoked_cube_case();
let (_, feature) = sweep_case(&case, 2);
assert!(feature > 0);
}
#[test]
fn cage_fold_reproduces_eval_from_cage_positions() {
let case = cube_case(true);
let ours = evaluated_case(&case, 2);
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
let cage_to_refined = ours.result.compose_stencils(case.positions.len());
let face_count = ours.result.topology.face_vertex_counts.len() as u32;
for face in 0..face_count {
for &uv in &[[0.5f32, 0.5], [0.3, 0.7], [0.0, 0.0]] {
let row = evaluator.weights_at(face, uv).expect("weights_at");
let cage_row: BTreeMap<u32, f64> =
row.iter().fold(BTreeMap::new(), |mut acc, &(i, w)| {
let start = cage_to_refined.offsets[i as usize] as usize;
let end = cage_to_refined.offsets[i as usize + 1] as usize;
cage_to_refined.indices[start..end]
.iter()
.zip(&cage_to_refined.weights[start..end])
.for_each(|(&ci, &cw)| {
*acc.entry(ci).or_insert(0.0) += w as f64 * cw as f64;
});
acc
});
let total: f64 = cage_row.values().sum();
assert!(
(total - 1.0).abs() <= UNITY_TOLERANCE,
"face {face} at {uv:?}: cage fold sums to {total}, not 1",
);
let folded = cage_row.iter().fold([0.0f64; 3], |acc, (&ci, &w)| {
let p = case.positions[ci as usize];
[
acc[0] + w * p[0] as f64,
acc[1] + w * p[1] as f64,
acc[2] + w * p[2] as f64,
]
});
let evaluated = v3(evaluator.eval(face, uv).expect("eval"));
let distance = length(sub(folded, evaluated));
assert!(
distance <= IDENTITY_TOLERANCE,
"face {face} at {uv:?}: cage fold gives {folded:?}, eval gives \
{evaluated:?} ({distance} apart)",
);
}
}
}
fn cube_ev_corner() -> (EvaluatedCase, u32, usize) {
let case = cube_case(false);
let ours = evaluated_case(&case, 1);
let valence = |vi: u32| {
ours.result.adjacency.vertex_edge_offsets[vi as usize + 1]
- ours.result.adjacency.vertex_edge_offsets[vi as usize]
};
let (face, k) = (0..ours.result.topology.face_vertex_counts.len() as u32)
.find_map(|face| {
ours.result.topology.face_vertex_indices[face as usize * 4..face as usize * 4 + 4]
.iter()
.position(|&c| valence(c) == 3)
.map(|k| (face, k))
})
.expect("a quad touching an extraordinary vertex");
(ours, face, k)
}
fn off_corner(k: usize, inset: f32) -> [f32; 2] {
[
if CORNER_UV[k][0] == 0.0 { inset } else { 1.0 - inset },
if CORNER_UV[k][1] == 0.0 { inset } else { 1.0 - inset },
]
}
#[test]
fn deep_isolation_and_depth_cap_rows_match_eval() {
let (ours, face, k) = cube_ev_corner();
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
assert_row_gates(&evaluator, &ours.refined, face, off_corner(k, 1e-4));
assert_row_gates(&evaluator, &ours.refined, face, off_corner(k, 1e-9));
}
#[test]
fn out_of_range_uv_rows_are_clamped() {
let ours = evaluated_case(&cube_case(false), 1);
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
assert_eq!(
evaluator.weights_at(0, [-1.0, 2.0]).expect("clamped row"),
evaluator.weights_at(0, [0.0, 1.0]).expect("corner row"),
);
}
#[test]
fn repeated_rows_are_bit_identical() {
let ours = evaluated_case(&cube_case(true), 2);
let evaluator = ours
.result
.limit_evaluator(&ours.refined)
.expect("limit evaluator");
let face_count = ours.result.topology.face_vertex_counts.len() as u32;
let samples: Vec<(u32, [f32; 2])> = (0..face_count)
.filter(|&f| evaluator.quad_class(f) == QuadClass::Feature)
.flat_map(|f| {
[[0.5f32, 0.5], [0.3, 0.7], [1e-4, 1e-4]]
.into_iter()
.map(move |uv| (f, uv))
})
.collect();
let first: Vec<_> = samples
.iter()
.map(|&(f, uv)| evaluator.weights_at(f, uv).expect("first pass"))
.collect();
let second: Vec<_> = samples
.iter()
.map(|&(f, uv)| evaluator.weights_at(f, uv).expect("second pass"))
.collect();
assert_eq!(first, second, "cached isolation changed a repeated row");
}