1use std::cell::{OnceCell, RefCell};
100use std::collections::hash_map::Entry;
101use std::collections::{BTreeMap, BTreeSet, HashMap};
102
103use crate::catmull_clark::stencils::Sparse;
104use crate::closest_point::SearchIndex;
105use crate::limit::{SectorDerivatives, corner_limit_sector};
106use crate::patch::QuadClass;
107use crate::{
108 Adjacency, CornerRule, CreaseComputationMethod, KernelError, Mesh, PatchTable,
109 RefinementResult, Refiner, Scheme, SchemeOptions, UniformRefine, VertexOrigin,
110};
111
112pub const MAX_ISOLATION_DEPTH: u32 = 20;
114
115pub type LimitSample = ([f32; 3], [f32; 3], [f32; 3]);
118
119type IsolatedSample = ([f32; 3], [f64; 3], [f64; 3]);
121
122type WeightRow = Vec<(u32, f64)>;
125
126type CornerSector = (Sparse, SectorDerivatives);
132
133const CORNER_UV: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
135
136pub struct LimitEvaluator<'a> {
139 pub(crate) result: &'a RefinementResult,
140 pub(crate) positions: &'a [[f32; 3]],
141 pub(crate) table: PatchTable,
142 isolations: RefCell<HashMap<u32, IsolationNode>>,
143 corner_sectors: RefCell<HashMap<(u32, usize), CornerSector>>,
146 pub(crate) search: OnceCell<SearchIndex>,
149}
150
151impl RefinementResult {
152 pub fn limit_evaluator<'a>(
160 &'a self,
161 positions: &'a [[f32; 3]],
162 ) -> Result<LimitEvaluator<'a>, KernelError> {
163 let table = self.patch_table()?;
164 if positions.len() != self.topology.vertex_count as usize {
165 return Err(KernelError::InvalidTopology(
166 "positions length does not match the refined vertex count",
167 ));
168 }
169 Ok(LimitEvaluator {
170 result: self,
171 positions,
172 table,
173 isolations: RefCell::new(HashMap::new()),
174 corner_sectors: RefCell::new(HashMap::new()),
175 search: OnceCell::new(),
176 })
177 }
178}
179
180impl LimitEvaluator<'_> {
181 pub fn quad_class(&self, face: u32) -> QuadClass {
183 self.table.quad_class(face)
184 }
185
186 pub fn eval(&self, face: u32, uv: [f32; 2]) -> Result<[f32; 3], KernelError> {
189 match self.table.face_patch(face) {
190 Some(patch) => Ok(self.table.eval(
191 patch as usize,
192 [clamp01(uv[0] as f64) as f32, clamp01(uv[1] as f64) as f32],
193 self.positions,
194 )),
195 None => self.eval_with_derivatives(face, uv).map(|(p, _, _)| p),
196 }
197 }
198
199 pub fn eval_with_derivatives(
205 &self,
206 face: u32,
207 uv: [f32; 2],
208 ) -> Result<LimitSample, KernelError> {
209 let uv = [clamp01(uv[0] as f64), clamp01(uv[1] as f64)];
210 let (p, du, dv) = self.evaluate(face, uv)?;
211 Ok((
212 p,
213 [du[0] as f32, du[1] as f32, du[2] as f32],
214 [dv[0] as f32, dv[1] as f32, dv[2] as f32],
215 ))
216 }
217
218 pub fn weights_at(&self, face: u32, uv: [f32; 2]) -> Result<Vec<(u32, f32)>, KernelError> {
236 let mesh = &self.result.topology;
237 let adjacency = &self.result.adjacency;
238 let options = &self.result.options;
239 let uv = [clamp01(uv[0] as f64), clamp01(uv[1] as f64)];
240 let row = if let Some(patch) = self.table.face_patch(face) {
241 self.table
242 .position_weights(patch as usize, [uv[0] as f32, uv[1] as f32])
243 .to_vec()
244 } else if let Some(corner) = snap_target(uv, face, mesh, adjacency, options) {
245 let mut sectors = self.corner_sectors.borrow_mut();
246 let sector =
247 corner_sector_cached(&mut sectors, mesh, adjacency, options, face, corner)?;
248 corner_row(sector)
249 } else {
250 let mut isolations = self.isolations.borrow_mut();
251 let node = match isolations.entry(face) {
252 Entry::Occupied(occupied) => occupied.into_mut(),
253 Entry::Vacant(vacant) => vacant.insert(isolation_node(
254 mesh,
255 adjacency,
256 self.positions,
257 face,
258 options,
259 )?),
260 };
261 let inner = weights_isolated(node, uv, 0, options)?;
262 lift_row(&inner, node)
263 };
264 Ok(merge_row(row))
265 }
266
267 fn evaluate(&self, face: u32, uv: [f64; 2]) -> Result<IsolatedSample, KernelError> {
268 let mesh = &self.result.topology;
269 let adjacency = &self.result.adjacency;
270 let options = &self.result.options;
271 if let Some(patch) = self.table.face_patch(face) {
272 let (p, du, dv) = self.table.eval_with_derivatives(
273 patch as usize,
274 [uv[0] as f32, uv[1] as f32],
275 self.positions,
276 );
277 Ok((p, v3(du), v3(dv)))
278 } else if let Some(corner) = snap_target(uv, face, mesh, adjacency, options) {
279 let mut sectors = self.corner_sectors.borrow_mut();
280 let sector =
281 corner_sector_cached(&mut sectors, mesh, adjacency, options, face, corner)?;
282 Ok(snap_corner(sector, self.positions, corner))
283 } else {
284 let mut isolations = self.isolations.borrow_mut();
285 let node = match isolations.entry(face) {
286 Entry::Occupied(occupied) => occupied.into_mut(),
287 Entry::Vacant(vacant) => vacant.insert(isolation_node(
288 mesh,
289 adjacency,
290 self.positions,
291 face,
292 options,
293 )?),
294 };
295 eval_isolated(node, uv, 0, options)
296 }
297 }
298}
299
300struct IsolationNode {
302 refined: RefinementResult,
304 positions: Vec<[f32; 3]>,
306 table: PatchTable,
308 sub_vertices: Vec<u32>,
311 corner_sectors: HashMap<(u32, usize), CornerSector>,
314 children: [QuadrantChild; 4],
316}
317
318struct QuadrantChild {
322 face: u32,
324 origin: [f64; 2],
326 jacobian: [[f64; 2]; 2],
329 node: Option<Box<IsolationNode>>,
330}
331
332impl QuadrantChild {
333 fn child_uv(&self, uv: [f64; 2]) -> [f64; 2] {
334 let d = [uv[0] - self.origin[0], uv[1] - self.origin[1]];
335 [
336 self.jacobian[0][0] * d[0] + self.jacobian[0][1] * d[1],
337 self.jacobian[1][0] * d[0] + self.jacobian[1][1] * d[1],
338 ]
339 }
340
341 fn parent_derivatives(&self, du: [f64; 3], dv: [f64; 3]) -> ([f64; 3], [f64; 3]) {
343 let j = &self.jacobian;
344 let combine = |a: f64, b: f64| {
345 [
346 a * du[0] + b * dv[0],
347 a * du[1] + b * dv[1],
348 a * du[2] + b * dv[2],
349 ]
350 };
351 (combine(j[0][0], j[1][0]), combine(j[0][1], j[1][1]))
352 }
353}
354
355fn eval_isolated(
356 node: &mut IsolationNode,
357 uv: [f64; 2],
358 depth: u32,
359 options: &SchemeOptions,
360) -> Result<IsolatedSample, KernelError> {
361 let k = quadrant(uv);
362 let child_uv = node.children[k].child_uv(uv);
363 let face = node.children[k].face;
364 debug_assert!(
365 (-1e-12..=1.0 + 1e-12).contains(&child_uv[0])
366 && (-1e-12..=1.0 + 1e-12).contains(&child_uv[1]),
367 "quadrant map left the child quad: {child_uv:?}",
368 );
369 let mesh = &node.refined.topology;
370 let adjacency = &node.refined.adjacency;
371 let snap = |sectors: &mut HashMap<(u32, usize), CornerSector>,
372 positions: &[[f32; 3]],
373 corner: usize| {
374 let sector = corner_sector_cached(sectors, mesh, adjacency, options, face, corner)?;
375 Ok(snap_corner(sector, positions, corner))
376 };
377 let (p, du, dv) = if let Some(patch) = node.table.face_patch(face) {
378 let (p, du, dv) = node.table.eval_with_derivatives(
379 patch as usize,
380 [child_uv[0] as f32, child_uv[1] as f32],
381 &node.positions,
382 );
383 (p, v3(du), v3(dv))
384 } else if let Some(corner) = snap_target(child_uv, face, mesh, adjacency, options) {
385 snap(&mut node.corner_sectors, &node.positions, corner)?
386 } else if depth + 1 >= MAX_ISOLATION_DEPTH {
387 snap(
388 &mut node.corner_sectors,
389 &node.positions,
390 nearest_corner(child_uv),
391 )?
392 } else {
393 let child = match &mut node.children[k].node {
394 Some(child) => child,
395 vacant @ None => vacant.insert(Box::new(isolation_node(
396 &node.refined.topology,
397 &node.refined.adjacency,
398 &node.positions,
399 face,
400 options,
401 )?)),
402 };
403 eval_isolated(child, child_uv, depth + 1, options)?
404 };
405 let (du, dv) = node.children[k].parent_derivatives(du, dv);
406 Ok((p, du, dv))
407}
408
409fn weights_isolated(
415 node: &mut IsolationNode,
416 uv: [f64; 2],
417 depth: u32,
418 options: &SchemeOptions,
419) -> Result<WeightRow, KernelError> {
420 let k = quadrant(uv);
421 let child_uv = node.children[k].child_uv(uv);
422 let face = node.children[k].face;
423 let mesh = &node.refined.topology;
424 let adjacency = &node.refined.adjacency;
425 if let Some(patch) = node.table.face_patch(face) {
426 Ok(node
427 .table
428 .position_weights(patch as usize, [child_uv[0] as f32, child_uv[1] as f32])
429 .to_vec())
430 } else if let Some(corner) = snap_target(child_uv, face, mesh, adjacency, options) {
431 corner_sector_cached(
432 &mut node.corner_sectors,
433 mesh,
434 adjacency,
435 options,
436 face,
437 corner,
438 )
439 .map(corner_row)
440 } else if depth + 1 >= MAX_ISOLATION_DEPTH {
441 corner_sector_cached(
442 &mut node.corner_sectors,
443 mesh,
444 adjacency,
445 options,
446 face,
447 nearest_corner(child_uv),
448 )
449 .map(corner_row)
450 } else {
451 let child = match &mut node.children[k].node {
452 Some(child) => child,
453 vacant @ None => vacant.insert(Box::new(isolation_node(
454 &node.refined.topology,
455 &node.refined.adjacency,
456 &node.positions,
457 face,
458 options,
459 )?)),
460 };
461 let inner = weights_isolated(child, child_uv, depth + 1, options)?;
462 Ok(lift_row(&inner, child))
463 }
464}
465
466fn corner_row(sector: &CornerSector) -> WeightRow {
469 sector.0.iter().map(|&(i, w)| (i, w as f64)).collect()
470}
471
472fn lift_row(row: &WeightRow, node: &IsolationNode) -> WeightRow {
477 debug_assert_eq!(
478 node.refined.level_stencils.len(),
479 1,
480 "isolation refines exactly one level",
481 );
482 let stencils = &node.refined.level_stencils[0];
483 row.iter()
484 .fold(BTreeMap::new(), |mut acc: BTreeMap<u32, f64>, &(j, w)| {
485 let start = stencils.offsets[j as usize] as usize;
486 let end = stencils.offsets[j as usize + 1] as usize;
487 stencils.indices[start..end]
488 .iter()
489 .zip(&stencils.weights[start..end])
490 .for_each(|(&cage, &cw)| {
491 *acc.entry(node.sub_vertices[cage as usize]).or_insert(0.0) += w * cw as f64;
492 });
493 acc
494 })
495 .into_iter()
496 .collect()
497}
498
499fn merge_row(row: WeightRow) -> Vec<(u32, f32)> {
502 row.iter()
503 .fold(BTreeMap::new(), |mut acc: BTreeMap<u32, f64>, &(i, w)| {
504 *acc.entry(i).or_insert(0.0) += w;
505 acc
506 })
507 .into_iter()
508 .filter(|&(_, w)| w != 0.0)
509 .map(|(i, w)| (i, w as f32))
510 .collect()
511}
512
513fn isolation_node(
516 mesh: &Mesh,
517 adjacency: &Adjacency,
518 positions: &[[f32; 3]],
519 face: u32,
520 options: &SchemeOptions,
521) -> Result<IsolationNode, KernelError> {
522 let off = (face * 4) as usize;
523 let corners = &mesh.face_vertex_indices[off..off + 4];
524
525 let support_faces: Vec<u32> = corners
527 .iter()
528 .flat_map(|&corner| {
529 let start = adjacency.vertex_face_offsets[corner as usize] as usize;
530 let end = adjacency.vertex_face_offsets[corner as usize + 1] as usize;
531 adjacency.vertex_faces[start..end].iter().copied()
532 })
533 .collect::<BTreeSet<u32>>()
534 .into_iter()
535 .collect();
536 let central =
537 support_faces
538 .iter()
539 .position(|&f| f == face)
540 .ok_or(KernelError::InvalidTopology(
541 "central quad is not incident to its own corners",
542 ))? as u32;
543
544 let mut vertex_map: HashMap<u32, u32> = HashMap::new();
547 let mut sub_vertices: Vec<u32> = Vec::new();
548 let face_vertex_indices: Vec<u32> = support_faces
549 .iter()
550 .flat_map(|&f| mesh.face_vertex_indices[(f * 4) as usize..(f * 4) as usize + 4].iter())
551 .map(|&v| {
552 *vertex_map.entry(v).or_insert_with(|| {
553 sub_vertices.push(v);
554 sub_vertices.len() as u32 - 1
555 })
556 })
557 .collect();
558
559 let crease_edges: BTreeSet<u32> = support_faces
563 .iter()
564 .flat_map(|&f| adjacency.face_edges[(f * 4) as usize..(f * 4) as usize + 4].iter())
565 .copied()
566 .filter(|&e| mesh.edge_creases[e as usize] > 0.0)
567 .collect();
568 let (edge_vertices, edge_creases): (Vec<[u32; 2]>, Vec<f32>) = crease_edges
569 .iter()
570 .map(|&e| {
571 let [a, b] = mesh.edge_vertices[e as usize];
572 (
573 [vertex_map[&a], vertex_map[&b]],
574 mesh.edge_creases[e as usize],
575 )
576 })
577 .unzip();
578
579 let submesh = Mesh {
580 vertex_count: sub_vertices.len() as u32,
581 face_vertex_counts: vec![4; support_faces.len()],
582 face_vertex_indices,
583 edge_vertices,
584 edge_creases,
585 vertex_corners: sub_vertices
586 .iter()
587 .map(|&v| mesh.vertex_corners[v as usize])
588 .collect(),
589 };
590 let sub_positions: Vec<[f32; 3]> = sub_vertices
591 .iter()
592 .map(|&v| positions[v as usize])
593 .collect();
594 let central_corners: Vec<u32> = corners.iter().map(|&c| vertex_map[&c]).collect();
595
596 let refined = Refiner::new(submesh, Scheme::CatmullClark, *options)?
597 .refine_uniform(&UniformRefine::default())?;
598 let positions = refined.interpolate(&sub_positions);
599 let table = refined.patch_table()?;
600 let children = quadrant_children(&refined, central, ¢ral_corners)?;
601 Ok(IsolationNode {
602 refined,
603 positions,
604 table,
605 sub_vertices,
606 corner_sectors: HashMap::new(),
607 children,
608 })
609}
610
611fn quadrant_children(
617 refined: &RefinementResult,
618 central: u32,
619 central_corners: &[u32],
620) -> Result<[QuadrantChild; 4], KernelError> {
621 let mut children: [Option<QuadrantChild>; 4] = [None, None, None, None];
622 for (child_face, &parent) in refined.lineage.face_parent.iter().enumerate() {
623 if parent != central {
624 continue;
625 }
626 let child_corners =
627 &refined.topology.face_vertex_indices[child_face * 4..child_face * 4 + 4];
628 let (r, k) = child_corners
629 .iter()
630 .enumerate()
631 .find_map(|(r, &c)| match refined.lineage.vertex_origin[c as usize] {
632 VertexOrigin::Vertex(pv) => central_corners
633 .iter()
634 .position(|&cc| cc == pv)
635 .map(|k| (r, k)),
636 _ => None,
637 })
638 .ok_or(KernelError::InvalidTopology(
639 "central child quad has no central-corner vertex point",
640 ))?;
641 debug_assert!(
642 matches!(
643 refined.lineage.vertex_origin[child_corners[(r + 2) % 4] as usize],
644 VertexOrigin::Face(f) if f == central,
645 ),
646 "central child quad's diagonal is not the central face point",
647 );
648 debug_assert!(
649 matches!(
650 refined.lineage.vertex_origin[child_corners[(r + 1) % 4] as usize],
651 VertexOrigin::Edge(_),
652 ) && matches!(
653 refined.lineage.vertex_origin[child_corners[(r + 3) % 4] as usize],
654 VertexOrigin::Edge(_),
655 ),
656 "central child quad's off-corners are not edge points",
657 );
658
659 let mid = |a: [f64; 2], b: [f64; 2]| [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
660 let mut parent_uv = [[0.0f64; 2]; 4];
661 parent_uv[r] = CORNER_UV[k];
662 parent_uv[(r + 1) % 4] = mid(CORNER_UV[k], CORNER_UV[(k + 1) % 4]);
663 parent_uv[(r + 2) % 4] = [0.5, 0.5];
664 parent_uv[(r + 3) % 4] = mid(CORNER_UV[k], CORNER_UV[(k + 3) % 4]);
665
666 let e_u = [
667 parent_uv[1][0] - parent_uv[0][0],
668 parent_uv[1][1] - parent_uv[0][1],
669 ];
670 let e_v = [
671 parent_uv[3][0] - parent_uv[0][0],
672 parent_uv[3][1] - parent_uv[0][1],
673 ];
674 let det = e_u[0] * e_v[1] - e_v[0] * e_u[1];
675 debug_assert!(det.abs() > 1e-12, "degenerate child parameter frame");
676 children[k] = Some(QuadrantChild {
677 face: child_face as u32,
678 origin: parent_uv[0],
679 jacobian: [[e_v[1] / det, -e_v[0] / det], [-e_u[1] / det, e_u[0] / det]],
680 node: None,
681 });
682 }
683 debug_assert!(
686 (0..4).all(|k| {
687 children[k]
688 .as_ref()
689 .zip(children[(k + 1) % 4].as_ref())
690 .is_none_or(|(a, b)| {
691 let corners = |f: u32| {
692 &refined.topology.face_vertex_indices
693 [(f * 4) as usize..(f * 4) as usize + 4]
694 };
695 corners(a.face)
696 .iter()
697 .filter(|c| corners(b.face).contains(c))
698 .count()
699 == 2
700 })
701 }),
702 "quadrant children do not share their lead edge points",
703 );
704 let [a, b, c, d] = children;
705 a.zip(b)
706 .zip(c.zip(d))
707 .map(|((a, b), (c, d))| [a, b, c, d])
708 .ok_or(KernelError::InvalidTopology(
709 "central quad did not refine into four quadrant children",
710 ))
711}
712
713fn corner_sector_cached<'c>(
716 cache: &'c mut HashMap<(u32, usize), CornerSector>,
717 mesh: &Mesh,
718 adjacency: &Adjacency,
719 options: &SchemeOptions,
720 face: u32,
721 corner: usize,
722) -> Result<&'c CornerSector, KernelError> {
723 match cache.entry((face, corner)) {
724 Entry::Occupied(occupied) => Ok(occupied.into_mut()),
725 Entry::Vacant(vacant) => {
726 Ok(vacant.insert(corner_limit_sector(face, corner, mesh, adjacency, options)?))
727 }
728 }
729}
730
731fn snap_corner(sector: &CornerSector, positions: &[[f32; 3]], corner: usize) -> IsolatedSample {
737 let (position, derivatives) = sector;
738 let p = apply(position, positions);
739 let (du, dv) = match derivatives {
740 SectorDerivatives::Parametric { d_out, d_in } => {
741 let (d_out, d_in) = (apply(d_out, positions), apply(d_in, positions));
742 let neg = |d: [f64; 3]| [-d[0], -d[1], -d[2]];
743 match corner {
744 0 => (d_out, d_in),
745 1 => (neg(d_in), d_out),
746 2 => (neg(d_out), neg(d_in)),
747 _ => (d_in, neg(d_out)),
748 }
749 }
750 SectorDerivatives::Plane { tangent1, tangent2 } => {
751 (apply(tangent1, positions), apply(tangent2, positions))
752 }
753 };
754 ([p[0] as f32, p[1] as f32, p[2] as f32], du, dv)
755}
756
757fn persistent_feature_vertex(
764 vi: usize,
765 mesh: &Mesh,
766 adjacency: &Adjacency,
767 options: &SchemeOptions,
768) -> bool {
769 let start = adjacency.vertex_edge_offsets[vi] as usize;
770 let end = adjacency.vertex_edge_offsets[vi + 1] as usize;
771 let persistent_crease = |s: f32| persistent_sharp_edge(s, options);
772 let persistent_corner = |s: f32| {
773 s > 0.0
774 && (options.corner_normalize
775 || s.is_infinite()
776 || (options.corner_rule == CornerRule::OpenSubdivDeRose && s >= 10.0))
777 };
778 adjacency.vertex_is_boundary[vi]
779 || end - start != 4
780 || persistent_corner(mesh.vertex_corners[vi])
781 || adjacency.vertex_edges[start..end]
782 .iter()
783 .any(|&e| persistent_crease(mesh.edge_creases[e as usize]))
784}
785
786pub(crate) fn persistent_sharp_edge(sharpness: f32, options: &SchemeOptions) -> bool {
791 sharpness > 0.0
792 && (options.crease_normalize
793 || sharpness.is_infinite()
794 || (options.corner_rule == CornerRule::OpenSubdivDeRose
795 && options.crease_computation == CreaseComputationMethod::Uniform
796 && sharpness >= 10.0))
797}
798
799fn snap_target(
802 uv: [f64; 2],
803 face: u32,
804 mesh: &Mesh,
805 adjacency: &Adjacency,
806 options: &SchemeOptions,
807) -> Option<usize> {
808 exact_corner(uv).filter(|&corner| {
809 persistent_feature_vertex(
810 mesh.face_vertex_indices[(face * 4) as usize + corner] as usize,
811 mesh,
812 adjacency,
813 options,
814 )
815 })
816}
817
818fn exact_corner(uv: [f64; 2]) -> Option<usize> {
820 let bit = |t: f64| (t == 0.0).then_some(false).or((t == 1.0).then_some(true));
821 bit(uv[0]).zip(bit(uv[1])).map(|bits| match bits {
822 (false, false) => 0,
823 (true, false) => 1,
824 (true, true) => 2,
825 (false, true) => 3,
826 })
827}
828
829fn quadrant(uv: [f64; 2]) -> usize {
832 match (uv[0] > 0.5, uv[1] > 0.5) {
833 (false, false) => 0,
834 (true, false) => 1,
835 (true, true) => 2,
836 (false, true) => 3,
837 }
838}
839
840fn nearest_corner(uv: [f64; 2]) -> usize {
842 match (uv[0] >= 0.5, uv[1] >= 0.5) {
843 (false, false) => 0,
844 (true, false) => 1,
845 (true, true) => 2,
846 (false, true) => 3,
847 }
848}
849
850fn clamp01(t: f64) -> f64 {
851 t.clamp(0.0, 1.0)
852}
853
854fn v3(p: [f32; 3]) -> [f64; 3] {
855 [p[0] as f64, p[1] as f64, p[2] as f64]
856}
857
858fn apply(row: &Sparse, positions: &[[f32; 3]]) -> [f64; 3] {
860 row.iter().fold([0.0f64; 3], |acc, &(i, w)| {
861 let p = positions[i as usize];
862 [
863 acc[0] + w as f64 * p[0] as f64,
864 acc[1] + w as f64 * p[1] as f64,
865 acc[2] + w as f64 * p[2] as f64,
866 ]
867 })
868}
869
870#[cfg(test)]
871mod tests {
872 use core::num::NonZeroU8;
873
874 use crate::{KernelError, Mesh, Refiner, Scheme, SchemeOptions, UniformRefine};
875
876 fn grid() -> Mesh {
880 Mesh {
881 vertex_count: 9,
882 face_vertex_counts: vec![4; 4],
883 face_vertex_indices: vec![0, 3, 4, 1, 1, 4, 5, 2, 3, 6, 7, 4, 4, 7, 8, 5],
884 edge_vertices: Vec::new(),
885 edge_creases: Vec::new(),
886 vertex_corners: vec![0.0; 9],
887 }
888 }
889
890 #[test]
891 fn non_catmull_clark_scheme_is_rejected() {
892 let refiner =
893 Refiner::new(grid(), Scheme::DooSabin, SchemeOptions::default()).expect("refiner");
894 let result = refiner
895 .refine_uniform(&UniformRefine::default())
896 .expect("refinement");
897 let positions = vec![[0.0f32; 3]; result.topology.vertex_count as usize];
898 assert!(matches!(
899 result.limit_evaluator(&positions).err(),
900 Some(KernelError::NotImplemented(_)),
901 ));
902 }
903
904 #[test]
905 fn partial_face_selection_is_rejected() {
906 let req = UniformRefine {
907 levels: NonZeroU8::new(1).unwrap(),
909 selected_faces: Some(vec![true, true, true, false]),
910 ..Default::default()
911 };
912 let refiner =
913 Refiner::new(grid(), Scheme::CatmullClark, SchemeOptions::default()).expect("refiner");
914 let result = refiner.refine_uniform(&req).expect("refinement");
915 let positions = vec![[0.0f32; 3]; result.topology.vertex_count as usize];
916 assert!(matches!(
917 result.limit_evaluator(&positions).err(),
918 Some(KernelError::NotImplemented(_)),
919 ));
920 }
921
922 #[test]
923 fn mismatched_positions_length_is_rejected() {
924 let refiner =
925 Refiner::new(grid(), Scheme::CatmullClark, SchemeOptions::default()).expect("refiner");
926 let result = refiner
927 .refine_uniform(&UniformRefine::default())
928 .expect("refinement");
929 let positions = vec![[0.0f32; 3]; 3];
930 assert!(matches!(
931 result.limit_evaluator(&positions).err(),
932 Some(KernelError::InvalidTopology(_)),
933 ));
934 }
935}