Skip to main content

subdiv_kernels/
limit_eval.rs

1//! Arbitrary-`(u, v)` limit evaluation over *any* refined
2//! Catmull-Clark quad (limit-surface SDF design s2, route a2).
3//!
4//! [`RefinementResult::limit_evaluator`] wraps the s1
5//! [`PatchTable`] in a uniform per-quad interface: regular quads
6//! evaluate their bicubic B-spline patch directly; feature quads
7//! (EV/crease/corner/boundary contact) evaluate by *recursive local
8//! isolation* -- no eigenbasis machinery, exact to the depth the
9//! query needs, reusing the ordinary [`Refiner`].
10//!
11//! # Feature isolation
12//!
13//! One isolation step extracts the feature quad's *support submesh* --
14//! the quad plus every face sharing a vertex with it, carrying the
15//! refined-level crease/corner sharpness of the support faces' edges
16//! and vertices -- as a standalone [`Mesh`], refines it one
17//! level with the same [`SchemeOptions`] (so semi-sharp decay matches
18//! the original refinement exactly; design §10-3 v1 semantics), and
19//! descends into the child quad containing the query (the quadrant of
20//! `(u, v)`, with the parameter map recovered from the children's
21//! vertex lineage). The recursion repeats until the containing child
22//! is [`QuadClass::Regular`] on its submesh, then evaluates the s1
23//! patch.
24//!
25//! The support contract makes each step exact for the central quad:
26//! every control point of a central child's 4x4 patch neighborhood is
27//! the vertex/edge/face point of a simplex *incident to a central
28//! corner*, and the support carries those simplices' positions,
29//! sharpness, and complete fans -- so the children's neighborhoods are
30//! bit-faithful to a full-mesh refinement, and the central children's
31//! own corners come out fully ringed (the extraction can recurse).
32//! Submesh-*border* quads are artificially Feature (truncated outer
33//! rings), but the recursion only ever evaluates central children.
34//!
35//! # Corner snapping and the persistent-feature predicate
36//!
37//! A query exactly at a quad corner whose vertex is a *persistent*
38//! feature -- boundary, extraordinary valence, or never-decaying
39//! sharpness under the scheme options -- would recurse forever, so it
40//! snaps to the analytic per-sector limit masks of `limit.rs` at that
41//! vertex instead (the same masks as
42//! [`SectoredLimitStencils`](crate::SectoredLimitStencils), applied to
43//! the current isolation level's positions). *Decaying* semi-sharp
44//! corners keep descending until the sharpness hits zero and the
45//! regular patch takes over. Because each descent doubles `(u, v)`
46//! exactly, any query that sits on a feature line (a crease edge, a
47//! boundary edge) becomes an exact corner once its dyadic bits
48//! exhaust, so f32 queries on feature lines terminate too -- unless
49//! the bits outlast the depth cap below.
50//!
51//! # Depth cap
52//!
53//! Isolation is capped at [`MAX_ISOLATION_DEPTH`] (20) levels below
54//! the evaluated refinement. The cap is a backstop for queries the
55//! snap cannot catch -- e.g. points on a crease line whose dyadic
56//! expansion exceeds the cap -- and falls back to the sector masks at
57//! the *nearest* corner of the deepest central child, at most
58//! `2^-21` of the root quad away in parameter, so positions stay well
59//! inside the oracle tolerances while derivatives degrade (f32
60//! position differences at that depth are noise-dominated).
61//!
62//! # Weight rows
63//!
64//! [`LimitEvaluator::weights_at`] runs the same descent in the weight
65//! domain: the terminal patch-basis (or corner-mask) row composes
66//! through each isolation level's refinement stencils back to the
67//! evaluated level's vertices, exposing the limit position's sparse
68//! linear dependence on the refined positions -- the per-grab-point
69//! `W` row of the amendment limit-surface oracle (its design §3).
70//!
71//! # Derivative conventions
72//!
73//! Derivatives are with respect to the *evaluated quad's* own
74//! `[0, 1]^2`, exactly as in [`PatchTable`] (an extra `2^level`
75//! against root-face/ptex parameterization); the recursion
76//! chain-rules each level's 2x2 quadrant map (a rotation times 2) on
77//! the way back up. Snapped corners return parametric one-sided
78//! derivatives when the sector machinery can supply them
79//! (crease-rule corners whose quad edges are sector bounds or the
80//! regular cross direction; see
81//! [`SectorDerivatives`](crate::limit::SectorDerivatives)) and
82//! otherwise the raw in-sector tangent pair -- correct tangent plane
83//! and winding-oriented normal (`du x dv`), but without parametric
84//! alignment or scale. The latter is unavoidable at cone points and
85//! smooth extraordinary vertices, where the parametric derivative
86//! itself does not converge (`2 lambda != 1`).
87//!
88//! # Caching
89//!
90//! Isolation submeshes are cached per feature quad, and each level's
91//! children lazily within its node, so repeated nearby queries
92//! (the s3 Newton iterations) refine each support at most once.
93//! Corner sector masks are memoized per `(face, corner)` at every
94//! level (the s4 perf lever: feature-line feet re-snap the same
95//! corners per sample; the masks depend only on topology + sharpness).
96//! The caches sit behind `RefCell`s -- the evaluator is cheap to build
97//! per thread but not `Sync`.
98
99use 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
112/// Isolation backstop below the evaluated level; see the module docs.
113pub const MAX_ISOLATION_DEPTH: u32 = 20;
114
115/// One limit sample: `(position, dp/du, dp/dv)`, the
116/// [`PatchTable::eval_with_derivatives`] shape.
117pub type LimitSample = ([f32; 3], [f32; 3], [f32; 3]);
118
119/// Internal sample with f64 derivatives, chain-ruled up the recursion.
120type IsolatedSample = ([f32; 3], [f64; 3], [f64; 3]);
121
122/// Sparse f64 weight row over the current vertex set -- the
123/// weight-domain counterpart of [`IsolatedSample`].
124type WeightRow = Vec<(u32, f64)>;
125
126/// Memoized [`corner_limit_sector`] output of one `(face, corner)`:
127/// the position mask and the derivative payload. Feature-line queries
128/// (crease feet, the s3 slide) re-snap the same corners per sample;
129/// the masks depend only on topology + sharpness, so they are computed
130/// once per evaluation context.
131type CornerSector = (Sparse, SectorDerivatives);
132
133/// Parent `(u, v)` of CSR corner `k`, the [`PatchTable`] convention.
134const CORNER_UV: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
135
136/// Uniform per-quad limit evaluation over a refined level; built by
137/// [`RefinementResult::limit_evaluator`]. See the module docs.
138pub 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    /// Memoized evaluated-level corner sector masks (see
144    /// [`CornerSector`]).
145    corner_sectors: RefCell<HashMap<(u32, usize), CornerSector>>,
146    /// Closest-point acceleration index (s3), built on the first
147    /// [`closest_point`](Self::closest_point) query; `closest_point.rs`.
148    pub(crate) search: OnceCell<SearchIndex>,
149}
150
151impl RefinementResult {
152    /// Build a [`LimitEvaluator`] over this refined level.
153    ///
154    /// `positions` is one position per refined vertex (the
155    /// [`PatchTable`] evaluation input -- CPU-interpolated or read
156    /// back from the GPU stencil path). Same gating as
157    /// [`patch_table`](Self::patch_table): Catmull-Clark, at least one
158    /// full (unselected) refinement.
159    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    /// Classification of refined face `face` (the s1 table's).
182    pub fn quad_class(&self, face: u32) -> QuadClass {
183        self.table.quad_class(face)
184    }
185
186    /// Limit position of refined quad `face` at in-quad `(u, v)`
187    /// (clamped to `[0, 1]^2`).
188    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    /// Limit position and first derivatives `(p, dp/du, dp/dv)` of
200    /// refined quad `face` at in-quad `(u, v)` (clamped to
201    /// `[0, 1]^2`); see the module docs for the derivative
202    /// conventions. `du x dv` is the winding-oriented surface normal
203    /// wherever it does not degenerate.
204    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    /// The sparse position stencil of [`eval`](Self::eval) at
219    /// `(face, uv)`, over the refined level's vertices (amendment
220    /// limit-oracle design §3): unique `(vertex, weight)` pairs
221    /// partitioning unity, with `eval(face, uv) == sum_i w_i *
222    /// positions[i]` up to f32 rounding. The limit point is linear in
223    /// the refined positions, so the row is exactly
224    /// `d eval(face, uv) / d positions`; folding it through
225    /// [`RefinementResult::compose_stencils`] yields the cage stencil
226    /// (the host-side split of
227    /// [`RefinementResult::compose_limit_stencils`]).
228    ///
229    /// `(u, v)` is clamped like `eval` and the row follows the same
230    /// branch structure -- regular patch basis, persistent-feature
231    /// corner masks, recursive isolation with the identical depth-cap
232    /// fallback -- composed in f64 and rounded once on return, so a
233    /// row exists wherever `eval` succeeds and shares its isolation
234    /// cache.
235    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
300/// One isolation level: the once-refined support submesh of a quad.
301struct IsolationNode {
302    /// One-level refinement of the support submesh.
303    refined: RefinementResult,
304    /// Refined submesh vertex positions.
305    positions: Vec<[f32; 3]>,
306    /// s1 patches over the refined submesh.
307    table: PatchTable,
308    /// Submesh cage vertex -> parent-level vertex (the support
309    /// extraction's selection; [`lift_row`] maps rows through it).
310    sub_vertices: Vec<u32>,
311    /// Memoized corner sector masks at this isolation level (see
312    /// [`CornerSector`]).
313    corner_sectors: HashMap<(u32, usize), CornerSector>,
314    /// The central quad's child per quadrant (parent corner slot).
315    children: [QuadrantChild; 4],
316}
317
318/// One quadrant child of an isolation node's central quad, with the
319/// affine in-parent -> in-child parameter map and its lazily isolated
320/// own node.
321struct QuadrantChild {
322    /// Refined-submesh face index.
323    face: u32,
324    /// Parent `(u, v)` of the child's CSR corner 0.
325    origin: [f64; 2],
326    /// `child_i = sum_j jacobian[i][j] * (parent - origin)_j`; entries
327    /// in `{0, +-2}`, so the map is exact on dyadic parameters.
328    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    /// Chain-rule child-parameter derivatives back to the parent's.
342    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
409/// The weight-domain [`eval_isolated`]: the position row of `(u, v)`
410/// over `node`'s once-refined submesh vertices, by the identical
411/// descent (terminal patch basis, persistent-corner masks, depth-cap
412/// fallback). Recursive rows come back over the child's refined
413/// vertices and are lifted one level by [`lift_row`].
414fn 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
466/// The position row of one snapped corner (the weight-domain
467/// [`snap_corner`], position mask only).
468fn corner_row(sector: &CornerSector) -> WeightRow {
469    sector.0.iter().map(|&(i, w)| (i, w as f64)).collect()
470}
471
472/// Lift a row over `node`'s once-refined submesh vertices to a row
473/// over the vertex set `node` was extracted from: expand each entry
474/// through the submesh's one-level refinement stencils, then map the
475/// submesh cage indices back through the extraction selection.
476fn 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
499/// Merge duplicate indices, drop exact zeros (corner/edge basis rows
500/// carry structurally zero columns), and round once to f32.
501fn 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
513/// Build one isolation node: extract the support submesh of `face`,
514/// refine it one level, and locate the central quad's children.
515fn 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    // The support: every face sharing a vertex with the central quad.
526    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    // Dense submesh vertex order: first appearance across the support
545    // faces' CSR corners.
546    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    // Sharpness carried verbatim: creased edges of the support faces
560    // (every edge incident to a central corner is one) and the
561    // vertices' corner values.
562    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, &central_corners)?;
601    Ok(IsolationNode {
602        refined,
603        positions,
604        table,
605        sub_vertices,
606        corner_sectors: HashMap::new(),
607        children,
608    })
609}
610
611/// Locate the central quad's four children in the refined submesh and
612/// recover each child's parent-parameter frame from its vertex
613/// lineage: the child holding the vertex point of central corner `k`
614/// covers quadrant `k`, its CSR corners sit (winding-preserved) at the
615/// corner, the two adjacent edge midpoints, and the face center.
616fn 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    // Winding sanity: consecutive quadrant children share the edge
684    // point on the parent edge between them.
685    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
713/// Memoized lookup of one `(face, corner)`'s sector masks; computes
714/// [`corner_limit_sector`] on the first request only.
715fn 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
731/// Sector-mask evaluation at one quad corner from its memoized masks:
732/// position plus either parametric one-sided `(du, dv)` (mapped from
733/// the corner's out/in edge derivatives by the corner's orientation in
734/// the quad frame) or the raw in-sector tangent plane. See
735/// [`SectorDerivatives`].
736fn 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
757/// Whether the vertex stays a feature at every deeper isolation level:
758/// boundary, extraordinary valence, or sharpness that never decays
759/// under `options` (infinite always; the OpenSubdiv `10.0` sentinel
760/// where the decay rule preserves it; any positive value under the
761/// normalize flags). Decaying semi-sharpness returns `false` -- deeper
762/// isolation resolves it.
763fn 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
786/// Whether an edge's stored sharpness never decays under `options`
787/// (the crease half of [`persistent_feature_vertex`]; the s3
788/// closest-point walk uses it to recognize feature lines whose
789/// on-line derivatives are depth-cap-degraded).
790pub(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
799/// The persistent-feature corner `(u, v)` snaps to, if any -- the
800/// shared snap branch of the value and weight evaluations.
801fn 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
818/// The CSR corner at `(u, v)` when both parameters are exactly 0 or 1.
819fn 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
829/// Quadrant (= parent corner slot) containing `(u, v)`; ties toward
830/// corner 0.
831fn 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
840/// CSR corner nearest to `(u, v)` (the depth-cap fallback target).
841fn 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
858/// Apply one sparse mask row to a positions buffer, accumulating f64.
859fn 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    /// A 2x2 quad grid (the geometry gates live in
877    /// `tests/limit_eval.rs` and `tests/limit_eval_osd_oracle.rs`;
878    /// these unit tests cover the error paths only).
879    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            // SAFETY: 1 is non-zero.
908            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}