use std::f64::consts::PI;
use crate::catmull_clark::stencils::{Sparse, merge, pack};
use crate::{
Adjacency, BoundaryInterpolation, KernelError, Mesh, RefinementResult, Scheme, SchemeOptions,
StencilTable,
};
#[derive(Debug, Clone, PartialEq)]
pub struct LimitStencils {
pub position: StencilTable,
pub tangent1: StencilTable,
pub tangent2: StencilTable,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SectoredLimitStencils {
pub position: StencilTable,
pub tangent1: StencilTable,
pub tangent2: StencilTable,
pub corner_sector: Vec<u32>,
}
impl RefinementResult {
pub fn limit_stencils(&self) -> Result<LimitStencils, KernelError> {
build_limit_stencils(self)
}
pub fn compose_limit_stencils(
&self,
input_vertex_count: usize,
) -> Result<LimitStencils, KernelError> {
let limit = self.limit_stencils()?;
let cage_to_refined = self.compose_stencils(input_vertex_count);
Ok(LimitStencils {
position: cage_to_refined.compose(&limit.position),
tangent1: cage_to_refined.compose(&limit.tangent1),
tangent2: cage_to_refined.compose(&limit.tangent2),
})
}
pub fn sectored_limit_stencils(&self) -> Result<SectoredLimitStencils, KernelError> {
build_sectored_limit_stencils(self)
}
pub fn compose_sectored_limit_stencils(
&self,
input_vertex_count: usize,
) -> Result<SectoredLimitStencils, KernelError> {
let sectored = self.sectored_limit_stencils()?;
let cage_to_refined = self.compose_stencils(input_vertex_count);
Ok(SectoredLimitStencils {
position: cage_to_refined.compose(§ored.position),
tangent1: cage_to_refined.compose(§ored.tangent1),
tangent2: cage_to_refined.compose(§ored.tangent2),
corner_sector: sectored.corner_sector,
})
}
}
pub(crate) struct Ring {
pub(crate) neighbors: Vec<u32>,
pub(crate) edges: Vec<u32>,
pub(crate) diagonals: Vec<u32>,
pub(crate) faces: Vec<u32>,
pub(crate) boundary: bool,
}
impl Ring {
pub(crate) fn rotated(&self, by: usize) -> Self {
debug_assert!(!self.boundary, "boundary fans cannot be rotated");
let shift = |ring: &[u32]| -> Vec<u32> {
(0..ring.len())
.map(|i| ring[(i + by) % ring.len()])
.collect()
};
Self {
neighbors: shift(&self.neighbors),
edges: shift(&self.edges),
diagonals: shift(&self.diagonals),
faces: shift(&self.faces),
boundary: false,
}
}
}
enum LimitRule {
Smooth,
Crease([usize; 2]),
Corner(Vec<usize>),
}
struct FanStep {
out_edge: u32,
in_edge: u32,
diagonal: u32,
face: u32,
used: bool,
}
pub(crate) fn validate_refined_quads(result: &RefinementResult) -> Result<(), KernelError> {
if result.scheme != Scheme::CatmullClark {
return Err(KernelError::NotImplemented(
"limit-surface machinery is only implemented for Catmull-Clark",
));
}
if result
.selected_faces
.as_ref()
.is_some_and(|mask| mask.iter().any(|&selected| !selected))
{
return Err(KernelError::NotImplemented(
"limit-surface machinery is not defined for partially selected refinements",
));
}
if result.topology.face_vertex_counts.iter().any(|&c| c != 4) {
return Err(KernelError::NotImplemented(
"limit-surface machinery needs an all-quad refined level (refine the cage at least \
once)",
));
}
Ok(())
}
fn validate_limit_topology(result: &RefinementResult) -> Result<(), KernelError> {
validate_refined_quads(result)?;
if result.options.boundary_interpolation == BoundaryInterpolation::Natural
&& result.adjacency.edge_is_boundary.iter().any(|&b| b)
{
return Err(KernelError::NotImplemented(
"limit stencils for BoundaryInterpolation::Natural on open meshes",
));
}
Ok(())
}
fn build_limit_stencils(result: &RefinementResult) -> Result<LimitStencils, KernelError> {
validate_limit_topology(result)?;
let mesh = &result.topology;
let adjacency = &result.adjacency;
let vertex_count = mesh.vertex_count as usize;
let mut position_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
let mut tangent1_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
let mut tangent2_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
for vi in 0..vertex_count {
let ring = vertex_ring(vi, mesh, adjacency)?;
let masks = match classify(vi, &ring, mesh, adjacency, &result.options) {
LimitRule::Smooth => smooth_masks(vi as u32, &ring),
LimitRule::Crease(ends) => crease_masks(vi as u32, &ring, ends),
LimitRule::Corner(_) => corner_masks(vi as u32, &ring),
};
debug_assert!(
(masks.position.iter().map(|&(_, w)| w).sum::<f32>() - 1.0).abs() < 1e-4,
"limit position mask of vertex {vi} is not affine",
);
debug_assert!(
masks.tangent1.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
&& masks.tangent2.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4,
"limit tangent masks of vertex {vi} are not derivations",
);
position_rows.push(masks.position);
tangent1_rows.push(masks.tangent1);
tangent2_rows.push(masks.tangent2);
}
Ok(LimitStencils {
position: pack(&position_rows),
tangent1: pack(&tangent1_rows),
tangent2: pack(&tangent2_rows),
})
}
fn build_sectored_limit_stencils(
result: &RefinementResult,
) -> Result<SectoredLimitStencils, KernelError> {
validate_limit_topology(result)?;
let mesh = &result.topology;
let adjacency = &result.adjacency;
let vertex_count = mesh.vertex_count as usize;
let mut position_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
let mut tangent1_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
let mut tangent2_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
let mut corner_sector = vec![u32::MAX; mesh.face_vertex_indices.len()];
for vi in 0..vertex_count {
let ring = vertex_ring(vi, mesh, adjacency)?;
let rule = classify(vi, &ring, mesh, adjacency, &result.options);
let sectors = vertex_sectors(vi as u32, &ring, rule);
debug_assert!(
(sectors.position.iter().map(|&(_, w)| w).sum::<f32>() - 1.0).abs() < 1e-4,
"limit position mask of vertex {vi} is not affine",
);
debug_assert!(
sectors.tangents.iter().all(|(tangent1, tangent2)| {
tangent1.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
&& tangent2.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
}),
"a sector tangent mask of vertex {vi} is not a derivation",
);
let first_row = tangent1_rows.len() as u32;
for (fan_slot, &fi) in ring.faces.iter().enumerate() {
let off = (fi * 4) as usize;
let corner = mesh.face_vertex_indices[off..off + 4]
.iter()
.position(|&c| c == vi as u32)
.ok_or(KernelError::InvalidTopology(
"vertex-face adjacency references a face without that vertex",
))?;
corner_sector[off + corner] = first_row + sectors.fan_sector[fan_slot];
}
position_rows.push(sectors.position);
for (tangent1, tangent2) in sectors.tangents {
tangent1_rows.push(tangent1);
tangent2_rows.push(tangent2);
}
}
debug_assert!(
corner_sector.iter().all(|&row| row != u32::MAX),
"a refined face-corner was not covered by any vertex ring",
);
Ok(SectoredLimitStencils {
position: pack(&position_rows),
tangent1: pack(&tangent1_rows),
tangent2: pack(&tangent2_rows),
corner_sector,
})
}
struct VertexSectors {
position: Sparse,
tangents: Vec<(Sparse, Sparse)>,
fan_sector: Vec<u32>,
}
impl VertexSectors {
fn single(masks: LimitMasks, fan_count: usize) -> Self {
Self {
position: masks.position,
tangents: vec![(masks.tangent1, masks.tangent2)],
fan_sector: vec![0; fan_count],
}
}
}
fn vertex_sectors(vi: u32, ring: &Ring, rule: LimitRule) -> VertexSectors {
let fan_count = ring.faces.len();
match rule {
LimitRule::Smooth => VertexSectors::single(smooth_masks(vi, ring), fan_count),
LimitRule::Crease(ends) if ring.boundary => {
VertexSectors::single(crease_masks(vi, ring, ends), fan_count)
}
LimitRule::Crease([lead, trail]) => {
let near = crease_masks(vi, ring, [lead, trail]);
let far = crease_masks(vi, &ring.rotated(trail), [0, fan_count - (trail - lead)]);
VertexSectors {
position: near.position,
tangents: vec![(near.tangent1, near.tangent2), (far.tangent1, far.tangent2)],
fan_sector: (0..fan_count)
.map(|slot| u32::from(!(lead..trail).contains(&slot)))
.collect(),
}
}
LimitRule::Corner(sharp_slots) if sharp_slots.len() < 2 => {
VertexSectors::single(corner_masks(vi, ring), fan_count)
}
LimitRule::Corner(sharp_slots) => {
let wrap = (!ring.boundary).then(|| {
let first = sharp_slots[0];
let last = sharp_slots[sharp_slots.len() - 1];
(last, first + ring.edges.len())
});
let spans: Vec<(usize, usize)> = sharp_slots
.windows(2)
.map(|pair| (pair[0], pair[1]))
.chain(wrap)
.collect();
let tangents = spans
.iter()
.map(|&(lead, trail)| {
let masks = if ring.boundary {
crease_masks(vi, ring, [lead, trail])
} else {
crease_masks(vi, &ring.rotated(lead), [0, trail - lead])
};
(masks.tangent1, masks.tangent2)
})
.collect();
let fan_sector = (0..fan_count)
.map(|slot| {
let led_by = sharp_slots.partition_point(|&sharp| sharp <= slot);
(if led_by == 0 {
spans.len() - 1
} else {
led_by - 1
}) as u32
})
.collect();
VertexSectors {
position: vec![(vi, 1.0)],
tangents,
fan_sector,
}
}
}
}
pub(crate) enum SectorDerivatives {
Parametric { d_out: Sparse, d_in: Sparse },
Plane { tangent1: Sparse, tangent2: Sparse },
}
pub(crate) fn corner_limit_sector(
face: u32,
corner: usize,
mesh: &Mesh,
adjacency: &Adjacency,
options: &SchemeOptions,
) -> Result<(Sparse, SectorDerivatives), KernelError> {
let off = (face * 4) as usize;
let vi = mesh.face_vertex_indices[off + corner];
let ring = vertex_ring(vi as usize, mesh, adjacency)?;
if ring.boundary && options.boundary_interpolation == BoundaryInterpolation::Natural {
return Err(KernelError::NotImplemented(
"limit evaluation at boundary feature vertices under BoundaryInterpolation::Natural",
));
}
let rule = classify(vi as usize, &ring, mesh, adjacency, options);
let crease_ends = match &rule {
LimitRule::Crease(ends) => Some(*ends),
_ => None,
};
let fan_slot =
ring.faces
.iter()
.position(|&f| f == face)
.ok_or(KernelError::InvalidTopology(
"corner vertex ring does not contain the corner's face",
))?;
let mut sectors = vertex_sectors(vi, &ring, rule);
let row = sectors.fan_sector[fan_slot] as usize;
let (tangent1, tangent2) = sectors.tangents.swap_remove(row);
let span = crease_ends.map(|[lead, trail]| {
if ring.boundary || row == 0 {
(lead, trail - lead)
} else {
(trail, ring.edges.len() - (trail - lead))
}
});
let edge_derivative = |slot: usize| -> Option<Sparse> {
let (lead, len) = span?;
let rel = if ring.boundary {
slot.checked_sub(lead)?
} else {
(slot + ring.edges.len() - lead) % ring.edges.len()
};
if rel == 0 {
Some(tangent1.clone())
} else if rel == len {
Some(tangent1.iter().map(|&(i, w)| (i, -w)).collect())
} else if len == 2 && rel == 1 {
Some(tangent2.clone())
} else {
None
}
};
let slot_of = |edge: u32| ring.edges.iter().position(|&e| e == edge);
let out_slot = slot_of(adjacency.face_edges[off + corner]);
let in_slot = slot_of(adjacency.face_edges[off + (corner + 3) % 4]);
let derivatives = match (
out_slot.and_then(&edge_derivative),
in_slot.and_then(&edge_derivative),
) {
(Some(d_out), Some(d_in)) => SectorDerivatives::Parametric { d_out, d_in },
_ => SectorDerivatives::Plane { tangent1, tangent2 },
};
Ok((sectors.position, derivatives))
}
pub(crate) fn vertex_ring(
vi: usize,
mesh: &Mesh,
adjacency: &Adjacency,
) -> Result<Ring, KernelError> {
let face_start = adjacency.vertex_face_offsets[vi] as usize;
let face_end = adjacency.vertex_face_offsets[vi + 1] as usize;
let incident_faces = &adjacency.vertex_faces[face_start..face_end];
if incident_faces.is_empty() {
return Err(KernelError::InvalidTopology(
"vertex without incident faces has no limit ring",
));
}
let mut steps = incident_faces
.iter()
.map(|&fi| {
let off = (fi * 4) as usize;
let corners = &mesh.face_vertex_indices[off..off + 4];
corners
.iter()
.position(|&c| c == vi as u32)
.map(|j| FanStep {
out_edge: adjacency.face_edges[off + j],
in_edge: adjacency.face_edges[off + (j + 3) % 4],
diagonal: corners[(j + 2) % 4],
face: fi,
used: false,
})
.ok_or(KernelError::InvalidTopology(
"vertex-face adjacency references a face without that vertex",
))
})
.collect::<Result<Vec<_>, _>>()?;
let boundary = adjacency.vertex_is_boundary[vi];
let start = if boundary {
steps
.iter()
.find(|s| adjacency.edge_is_boundary[s.out_edge as usize])
.map(|s| s.out_edge)
.ok_or(KernelError::InvalidTopology(
"boundary vertex has no leading boundary edge (inconsistent face winding)",
))?
} else {
steps[0].out_edge
};
let face_count = steps.len();
let mut edges = Vec::with_capacity(face_count + 1);
let mut diagonals = Vec::with_capacity(face_count);
let mut faces = Vec::with_capacity(face_count);
let mut current = start;
edges.push(current);
for k in 0..face_count {
let step = steps
.iter_mut()
.find(|s| !s.used && s.out_edge == current)
.ok_or(KernelError::InvalidTopology(
"vertex ring is not a single oriented fan",
))?;
step.used = true;
diagonals.push(step.diagonal);
faces.push(step.face);
current = step.in_edge;
if boundary || k + 1 < face_count {
edges.push(current);
}
}
if !boundary && current != start {
return Err(KernelError::InvalidTopology(
"interior vertex ring does not close",
));
}
if boundary && !adjacency.edge_is_boundary[current as usize] {
return Err(KernelError::InvalidTopology(
"boundary vertex ring does not end on a boundary edge",
));
}
let incident_edge_count =
(adjacency.vertex_edge_offsets[vi + 1] - adjacency.vertex_edge_offsets[vi]) as usize;
if edges.len() != incident_edge_count {
return Err(KernelError::InvalidTopology(
"vertex ring does not cover all incident edges (non-manifold fan)",
));
}
let neighbors = edges
.iter()
.map(|&ei| {
let [a, b] = mesh.edge_vertices[ei as usize];
if a as usize == vi { b } else { a }
})
.collect();
Ok(Ring {
neighbors,
edges,
diagonals,
faces,
boundary,
})
}
fn classify(
vi: usize,
ring: &Ring,
mesh: &Mesh,
adjacency: &Adjacency,
options: &SchemeOptions,
) -> LimitRule {
let sharp_slots: Vec<usize> = ring
.edges
.iter()
.enumerate()
.filter(|&(_, &ei)| {
mesh.edge_creases[ei as usize] > 0.0 || adjacency.edge_is_boundary[ei as usize]
})
.map(|(slot, _)| slot)
.collect();
if mesh.vertex_corners[vi] > 0.0 {
return LimitRule::Corner(sharp_slots);
}
if ring.boundary
&& options.boundary_interpolation == BoundaryInterpolation::EdgesAndCorners
&& ring.diagonals.len() <= 1
{
return LimitRule::Corner(sharp_slots);
}
match sharp_slots.as_slice() {
[] | [_] => LimitRule::Smooth,
[lead, trail] => LimitRule::Crease([*lead, *trail]),
_ => LimitRule::Corner(sharp_slots),
}
}
struct LimitMasks {
position: Sparse,
tangent1: Sparse,
tangent2: Sparse,
}
fn push(row: &mut Sparse, index: u32, weight: f64) {
if weight != 0.0 {
merge(row, &[(index, weight as f32)], 1.0);
}
}
fn corner_masks(vi: u32, ring: &Ring) -> LimitMasks {
let mut tangent1 = Sparse::new();
push(&mut tangent1, vi, -1.0);
push(&mut tangent1, ring.neighbors[0], 1.0);
let mut tangent2 = Sparse::new();
push(&mut tangent2, vi, -1.0);
push(&mut tangent2, ring.neighbors[1], 1.0);
LimitMasks {
position: vec![(vi, 1.0)],
tangent1,
tangent2,
}
}
fn crease_masks(vi: u32, ring: &Ring, ends: [usize; 2]) -> LimitMasks {
let [lead, trail] = ends;
let mut position = Sparse::new();
push(&mut position, vi, 2.0 / 3.0);
push(&mut position, ring.neighbors[lead], 1.0 / 6.0);
push(&mut position, ring.neighbors[trail], 1.0 / 6.0);
let mut tangent1 = Sparse::new();
push(&mut tangent1, ring.neighbors[lead], 0.5);
push(&mut tangent1, ring.neighbors[trail], -0.5);
let mut tangent2 = Sparse::new();
let interior_edge_count = trail - lead - 1;
if interior_edge_count == 1 {
push(&mut tangent2, vi, -4.0 / 6.0);
push(&mut tangent2, ring.neighbors[lead], -1.0 / 6.0);
push(&mut tangent2, ring.neighbors[lead + 1], 4.0 / 6.0);
push(&mut tangent2, ring.neighbors[trail], -1.0 / 6.0);
push(&mut tangent2, ring.diagonals[lead], 1.0 / 6.0);
push(&mut tangent2, ring.diagonals[lead + 1], 1.0 / 6.0);
} else if interior_edge_count > 1 {
let k = (interior_edge_count + 1) as f64;
let theta = PI / k;
let cos_theta = theta.cos();
let sin_theta = theta.sin();
let common_denom = 1.0 / (k * (3.0 + cos_theta));
let r = (cos_theta + 1.0) / sin_theta;
push(
&mut tangent2,
vi,
4.0 * r * (cos_theta - 1.0) * common_denom,
);
let crease_weight = -r * (1.0 + 2.0 * cos_theta) * common_denom;
push(&mut tangent2, ring.neighbors[lead], crease_weight);
push(&mut tangent2, ring.neighbors[trail], crease_weight);
push(
&mut tangent2,
ring.diagonals[lead],
sin_theta * common_denom,
);
for i in 1..interior_edge_count + 1 {
let sin_theta_i = (i as f64 * theta).sin();
let sin_theta_i_plus_1 = ((i + 1) as f64 * theta).sin();
push(
&mut tangent2,
ring.neighbors[lead + i],
4.0 * sin_theta_i * common_denom,
);
push(
&mut tangent2,
ring.diagonals[lead + i],
(sin_theta_i + sin_theta_i_plus_1) * common_denom,
);
}
} else {
push(&mut tangent2, vi, -6.0);
push(&mut tangent2, ring.neighbors[lead], 3.0);
push(&mut tangent2, ring.neighbors[trail], 3.0);
}
LimitMasks {
position,
tangent1,
tangent2,
}
}
fn smooth_masks(vi: u32, ring: &Ring) -> LimitMasks {
let valence = ring.diagonals.len();
if valence == 2 {
return corner_masks(vi, ring);
}
let mut position = Sparse::new();
let mut tangent1 = Sparse::new();
let mut tangent2 = Sparse::new();
if valence == 4 {
push(&mut position, vi, 4.0 / 9.0);
let tan1_edge = [4.0, 0.0, -4.0, 0.0];
let tan1_face = [1.0, -1.0, -1.0, 1.0];
let tan2_edge = [0.0, 4.0, 0.0, -4.0];
let tan2_face = [1.0, 1.0, -1.0, -1.0];
for i in 0..4 {
push(&mut position, ring.neighbors[i], 1.0 / 9.0);
push(&mut position, ring.diagonals[i], 1.0 / 36.0);
push(&mut tangent1, ring.neighbors[i], tan1_edge[i]);
push(&mut tangent1, ring.diagonals[i], tan1_face[i]);
push(&mut tangent2, ring.neighbors[i], tan2_edge[i]);
push(&mut tangent2, ring.diagonals[i], tan2_face[i]);
}
} else {
let n = valence as f32;
let face_weight = 1.0 / (n * (n + 5.0));
let edge_weight = 4.0 * face_weight;
push(
&mut position,
vi,
(1.0 - n * (edge_weight + face_weight)) as f64,
);
let theta = 2.0 * PI / valence as f64;
let cos_theta = theta.cos();
let cos_half_theta = (theta * 0.5).cos();
let lambda = (5.0 / 16.0)
+ (1.0 / 16.0) * (cos_theta + cos_half_theta * (2.0 * (9.0 + cos_theta)).sqrt());
let face_weight_scale = 1.0 / (4.0 * lambda - 1.0);
let rotated = |i: usize| (i + valence - 1) % valence;
for i in 0..valence {
push(&mut position, ring.neighbors[i], edge_weight as f64);
push(&mut position, ring.diagonals[i], face_weight as f64);
let cos_theta_i = (i as f64 * theta).cos();
let cos_theta_i_plus_1 = ((i + 1) as f64 * theta).cos();
push(&mut tangent1, ring.neighbors[i], 4.0 * cos_theta_i);
push(
&mut tangent1,
ring.diagonals[i],
face_weight_scale * (cos_theta_i + cos_theta_i_plus_1),
);
let j = rotated(i);
let cos_theta_j = (j as f64 * theta).cos();
let cos_theta_j_plus_1 = ((j + 1) as f64 * theta).cos();
push(&mut tangent2, ring.neighbors[i], 4.0 * cos_theta_j);
push(
&mut tangent2,
ring.diagonals[i],
face_weight_scale * (cos_theta_j + cos_theta_j_plus_1),
);
}
}
LimitMasks {
position,
tangent1,
tangent2,
}
}
#[cfg(test)]
mod tests {
use core::num::NonZeroU8;
use crate::{KernelError, Mesh, Refiner, Scheme, SchemeOptions, UniformRefine};
fn grid() -> Mesh {
Mesh {
vertex_count: 9,
face_vertex_counts: vec![4; 4],
face_vertex_indices: vec![0, 3, 4, 1, 1, 4, 5, 2, 3, 6, 7, 4, 4, 7, 8, 5],
edge_vertices: Vec::new(),
edge_creases: Vec::new(),
vertex_corners: vec![0.0; 9],
}
}
fn refine(
scheme: Scheme,
options: SchemeOptions,
req: &UniformRefine,
) -> crate::RefinementResult {
let refiner = Refiner::new(grid(), scheme, options).expect("refiner");
refiner.refine_uniform(req).expect("refinement")
}
#[test]
fn non_catmull_clark_scheme_is_rejected() {
let result = refine(
Scheme::DooSabin,
SchemeOptions::default(),
&UniformRefine::default(),
);
assert!(matches!(
result.limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
assert!(matches!(
result.sectored_limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
}
#[test]
fn partial_face_selection_is_rejected() {
let req = UniformRefine {
levels: NonZeroU8::new(1).unwrap(),
selected_faces: Some(vec![true, true, true, false]),
..Default::default()
};
let result = refine(Scheme::CatmullClark, SchemeOptions::default(), &req);
assert!(matches!(
result.limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
assert!(matches!(
result.sectored_limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
}
#[test]
fn natural_boundary_on_open_mesh_is_rejected() {
let options = SchemeOptions {
boundary_interpolation: crate::BoundaryInterpolation::Natural,
..Default::default()
};
let result = refine(Scheme::CatmullClark, options, &UniformRefine::default());
assert!(matches!(
result.limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
assert!(matches!(
result.sectored_limit_stencils(),
Err(KernelError::NotImplemented(_)),
));
}
}