Skip to main content

subdiv_kernels/
limit.rs

1//! Analytic Catmull-Clark limit position and tangent stencils.
2//!
3//! [`RefinementResult::limit_stencils`] produces three [`StencilTable`]s
4//! over the refined level's own vertices: row `i` reads refined vertices
5//! and writes the limit position / tangents of refined vertex `i`. They
6//! compose with the refinement stencils
7//! ([`RefinementResult::compose_limit_stencils`]) and dispatch through
8//! the existing CPU/GPU evaluation paths unchanged. The surface normal
9//! is `tangent1 x tangent2`, oriented with the face winding.
10//!
11//! The mask weights are an exact port of OpenSubdiv's
12//! `Sdc::Scheme<SCHEME_CATMARK>::assign{Corner,Crease,Smooth}Limit{Mask,
13//! TangentMasks}` (`opensubdiv/sdc/catmarkScheme.h`), with face weights
14//! applied to the diagonal (opposite) vertex of each incident quad
15//! (`SetFaceWeightsForFaceCenters(false)`), which is why the level must
16//! have been refined at least once: every Catmull-Clark face is then a
17//! quad with a well-defined diagonal.
18//!
19//! # Rule selection (mirrors OpenSubdiv `Far`)
20//!
21//! OpenSubdiv selects limit masks from the subdivision rule *at the
22//! evaluated level* (`far/primvarRefiner.h` ->
23//! `Vtr::Level::getVertexRule` -> `Sdc::Crease::IsSharp`, i.e.
24//! `sharpness > 0`). Mirrored here on the refined level's stored
25//! crease/corner values:
26//!
27//! - An edge is sharp when its refined-level crease value is `> 0.0` or
28//!   it is a boundary edge (OpenSubdiv sharpens boundary edges
29//!   unconditionally; this crate's refiner applies the crease rule along
30//!   boundaries under `EdgesOnly`/`EdgesAndCorners`).
31//! - A vertex takes the corner masks when its refined-level corner
32//!   value is `> 0.0`, when more than two incident edges are sharp, or
33//!   -- under [`BoundaryInterpolation::EdgesAndCorners`] -- when it is a
34//!   boundary vertex with a single incident face (exactly the vertices
35//!   the refiner pins).
36//! - Exactly two sharp edges select the crease masks; zero or one
37//!   (dart) the smooth masks.
38//!
39//! Residual *finite* sharpness at the refined level therefore selects
40//! the sharp masks, exactly as OpenSubdiv far does; once refinement has
41//! decayed a semi-sharp value to zero, deeper results select the smooth
42//! masks. "Infinitely sharp" is whatever never decays under the
43//! refiner's configured propagation: `f32::INFINITY` always, the `10.0`
44//! sentinel under [`CornerRule::OpenSubdivDeRose`], and every positive
45//! value under `crease_normalize`/`corner_normalize`.
46//!
47//! One deliberate approximation against this crate's non-OpenSubdiv
48//! conventions, OpenSubdiv-exact otherwise:
49//!
50//! - [`BoundaryInterpolation::Natural`] has no closed-form limit mask
51//!   for boundary vertices (the refiner's natural rule averages a
52//!   partial neighborhood); open meshes are rejected under it.
53//!
54//! # Sectored tangents ([`RefinementResult::sectored_limit_stencils`])
55//!
56//! At an infinitely sharp crease the limit surface has one normal per
57//! *sector* -- the maximal contiguous fan of incident faces between
58//! consecutive sharp edges around a vertex (sharp per the rule
59//! selection above) -- so the single per-vertex tangent pair shades one
60//! side's normal onto every adjacent face. [`SectoredLimitStencils`]
61//! generalizes the tangent output to one row per sector:
62//!
63//! - Smooth and dart vertices keep one sector (the full ring, the
64//!   per-vertex smooth masks); all their corners share it.
65//! - A crease vertex has one sector per side: the counterclockwise span
66//!   between its two sharp edges and the complementary span, each
67//!   evaluated with the crease tangent masks ring-oriented so the
68//!   evaluated span *is* that sector. A boundary crease vertex has one
69//!   side. `tangent1` runs along each sector's own leading edge.
70//! - A corner-rule vertex has one sector per fan between consecutive
71//!   sharp edges, position pinned, tangents from the crease tangent
72//!   masks of the fan's bounding edges. On a single-face fan that
73//!   normal is exactly OpenSubdiv's (the corner-mask and crease-mask
74//!   tangent pairs span the same plane, oracle-verified); on a
75//!   multi-face fan the pinned limit is a cone point with no
76//!   convergent normal -- OpenSubdiv's own patch evaluation reports
77//!   per-face normals there that drift with the adaptive isolation
78//!   level (probed in `tests/limit_osd_oracle.rs`) -- and the crease
79//!   machinery is kept as a deterministic, fan-symmetric tangent plane
80//!   (the naive bounding-edge cross product would degenerate on
81//!   straight-through fans). A vertex pinned by vertex sharpness alone
82//!   (fewer than two sharp edges) keeps the whole ring as one sector
83//!   with the naive corner tangents, OpenSubdiv-far parity.
84//!
85//! Sector rows are deterministic: emitted vertex by vertex, ordered
86//! within a vertex by the ring slot of each sector's leading edge.
87//! [`SectoredLimitStencils::corner_sector`] maps every refined
88//! face-corner to its row.
89//!
90//! [`CornerRule::OpenSubdivDeRose`]: crate::CornerRule::OpenSubdivDeRose
91
92use std::f64::consts::PI;
93
94use crate::catmull_clark::stencils::{Sparse, merge, pack};
95use crate::{
96    Adjacency, BoundaryInterpolation, KernelError, Mesh, RefinementResult, Scheme, SchemeOptions,
97    StencilTable,
98};
99
100/// Limit-surface stencils over a refined level's vertices.
101///
102/// All three tables read the refined level's vertex data (or, when
103/// produced by [`RefinementResult::compose_limit_stencils`], the
104/// original control points) and write one value per refined vertex.
105/// Tangents are unnormalized; `tangent1 x tangent2` is the (winding
106/// oriented) surface normal and the tangent magnitudes match
107/// OpenSubdiv's limit-mask scales.
108#[derive(Debug, Clone, PartialEq)]
109pub struct LimitStencils {
110    /// Refined-vertex -> limit position.
111    pub position: StencilTable,
112    /// Refined-vertex -> limit tangent 1 (unnormalized).
113    pub tangent1: StencilTable,
114    /// Refined-vertex -> limit tangent 2 (unnormalized).
115    pub tangent2: StencilTable,
116}
117
118/// Per-sector limit-surface stencils over a refined level's vertices.
119///
120/// The position table matches [`LimitStencils`]'s row for row; the
121/// tangent tables have one row per *sector* (see the module docs), so
122/// each side of an infinitely sharp crease shades with its own normal.
123/// Tangents are unnormalized and `tangent1 x tangent2` is the (winding
124/// oriented) sector normal.
125#[derive(Debug, Clone, PartialEq)]
126pub struct SectoredLimitStencils {
127    /// Refined-vertex -> limit position (same as
128    /// [`LimitStencils::position`]).
129    pub position: StencilTable,
130    /// Per-sector limit tangents: row `s` is sector `s`'s tangent.
131    pub tangent1: StencilTable,
132    /// Per-sector limit tangents: row `s` is sector `s`'s tangent.
133    pub tangent2: StencilTable,
134    /// For each refined face-corner, in the CSR order of
135    /// [`Mesh::face_vertex_indices`], the sector row its
136    /// vertex's limit tangents live in. Smooth vertices have one sector
137    /// shared by all their corners; a crease vertex has two; a
138    /// corner-rule vertex one per fan between consecutive sharp edges.
139    pub corner_sector: Vec<u32>,
140}
141
142impl RefinementResult {
143    /// Limit masks over the refined level's own vertices (row `i` reads
144    /// refined vertices, writes limit data for refined vertex `i`).
145    ///
146    /// Catmull-Clark only, and the result must come from at least one
147    /// full (unselected) refinement -- see the module docs for the rule
148    /// conventions and restrictions.
149    pub fn limit_stencils(&self) -> Result<LimitStencils, KernelError> {
150        build_limit_stencils(self)
151    }
152
153    /// The cage -> limit composition: each [`LimitStencils`] table
154    /// composed onto [`compose_stencils`](Self::compose_stencils), so a
155    /// host can upload three GPU tables and evaluate limit position and
156    /// tangents straight from control points. The surface normal is
157    /// `tangent1 x tangent2`.
158    ///
159    /// `input_vertex_count` must match the number of vertices in the
160    /// original (pre-refinement) topology.
161    pub fn compose_limit_stencils(
162        &self,
163        input_vertex_count: usize,
164    ) -> Result<LimitStencils, KernelError> {
165        let limit = self.limit_stencils()?;
166        let cage_to_refined = self.compose_stencils(input_vertex_count);
167        Ok(LimitStencils {
168            position: cage_to_refined.compose(&limit.position),
169            tangent1: cage_to_refined.compose(&limit.tangent1),
170            tangent2: cage_to_refined.compose(&limit.tangent2),
171        })
172    }
173
174    /// Per-sector limit masks over the refined level's own vertices:
175    /// the position table is per refined vertex, the tangent tables per
176    /// sector, with [`SectoredLimitStencils::corner_sector`] mapping
177    /// each refined face-corner to its tangent row.
178    ///
179    /// Same scheme/option gating and errors as
180    /// [`limit_stencils`](Self::limit_stencils).
181    pub fn sectored_limit_stencils(&self) -> Result<SectoredLimitStencils, KernelError> {
182        build_sectored_limit_stencils(self)
183    }
184
185    /// The cage -> limit composition of
186    /// [`sectored_limit_stencils`](Self::sectored_limit_stencils): all
187    /// three tables composed onto
188    /// [`compose_stencils`](Self::compose_stencils), so a host uploads
189    /// the tables and evaluates per-sector limit tangents straight from
190    /// control points.
191    ///
192    /// `input_vertex_count` must match the number of vertices in the
193    /// original (pre-refinement) topology.
194    pub fn compose_sectored_limit_stencils(
195        &self,
196        input_vertex_count: usize,
197    ) -> Result<SectoredLimitStencils, KernelError> {
198        let sectored = self.sectored_limit_stencils()?;
199        let cage_to_refined = self.compose_stencils(input_vertex_count);
200        Ok(SectoredLimitStencils {
201            position: cage_to_refined.compose(&sectored.position),
202            tangent1: cage_to_refined.compose(&sectored.tangent1),
203            tangent2: cage_to_refined.compose(&sectored.tangent2),
204            corner_sector: sectored.corner_sector,
205        })
206    }
207}
208
209/// Counterclockwise one-ring of a refined vertex.
210///
211/// Slot `i` holds ring edge `e_i`; the quad between `e_i` and `e_(i+1)`
212/// contributes its diagonal vertex as face slot `i`. Interior rings are
213/// cyclic (`edges == faces`); boundary rings run from the leading
214/// boundary edge around the surface fan to the trailing one
215/// (`edges == faces + 1`), the orientation OpenSubdiv's crease tangent
216/// masks assume (tangent1 along the leading edge, `tan1 x tan2`
217/// outward).
218pub(crate) struct Ring {
219    /// Neighbor vertex at the far end of each ring edge.
220    pub(crate) neighbors: Vec<u32>,
221    /// Mesh edge index per ring slot, parallel to `neighbors`.
222    pub(crate) edges: Vec<u32>,
223    /// Diagonal vertex of the quad between ring edges `i` and `i + 1`.
224    pub(crate) diagonals: Vec<u32>,
225    /// Face index of the quad between ring edges `i` and `i + 1`,
226    /// parallel to `diagonals`.
227    pub(crate) faces: Vec<u32>,
228    /// Boundary fan vs interior cycle.
229    pub(crate) boundary: bool,
230}
231
232impl Ring {
233    /// The ring re-origined so slot `by` becomes slot 0. Interior rings
234    /// only -- a boundary fan's origin is its leading boundary edge.
235    pub(crate) fn rotated(&self, by: usize) -> Self {
236        debug_assert!(!self.boundary, "boundary fans cannot be rotated");
237        let shift = |ring: &[u32]| -> Vec<u32> {
238            (0..ring.len())
239                .map(|i| ring[(i + by) % ring.len()])
240                .collect()
241        };
242        Self {
243            neighbors: shift(&self.neighbors),
244            edges: shift(&self.edges),
245            diagonals: shift(&self.diagonals),
246            faces: shift(&self.faces),
247            boundary: false,
248        }
249    }
250}
251
252/// Limit rule for one vertex, per the module-doc selection.
253enum LimitRule {
254    Smooth,
255    /// Ring slots of the two sharp edges (leading, trailing).
256    Crease([usize; 2]),
257    /// Ring slots of all sharp edges (may be fewer than two when the
258    /// vertex is pinned by vertex sharpness or boundary policy alone).
259    Corner(Vec<usize>),
260}
261
262/// Per-incident-face corner data feeding the ring walk: within the face,
263/// `out_edge` runs from the vertex to its next corner, `in_edge` to its
264/// previous corner, and `diagonal` is the opposite quad corner. With
265/// consistent winding, sweeping from `out_edge` across the face interior
266/// to `in_edge` is the counterclockwise step around the vertex.
267struct FanStep {
268    out_edge: u32,
269    in_edge: u32,
270    diagonal: u32,
271    face: u32,
272    used: bool,
273}
274
275/// The shared gating of refined-level surface machinery (limit masks,
276/// regular-patch extraction): Catmull-Clark, full (unselected)
277/// refinement, and an all-quad refined level.
278pub(crate) fn validate_refined_quads(result: &RefinementResult) -> Result<(), KernelError> {
279    if result.scheme != Scheme::CatmullClark {
280        return Err(KernelError::NotImplemented(
281            "limit-surface machinery is only implemented for Catmull-Clark",
282        ));
283    }
284    // `selected_faces` is an all-true mask for unselected refinements;
285    // only an actually partial mask leaves unrefined n-gons behind.
286    if result
287        .selected_faces
288        .as_ref()
289        .is_some_and(|mask| mask.iter().any(|&selected| !selected))
290    {
291        return Err(KernelError::NotImplemented(
292            "limit-surface machinery is not defined for partially selected refinements",
293        ));
294    }
295    if result.topology.face_vertex_counts.iter().any(|&c| c != 4) {
296        return Err(KernelError::NotImplemented(
297            "limit-surface machinery needs an all-quad refined level (refine the cage at least \
298             once)",
299        ));
300    }
301    Ok(())
302}
303
304/// The gating of both limit builders: [`validate_refined_quads`] plus
305/// no open mesh under the natural boundary rule (which has no
306/// closed-form limit mask for boundary vertices).
307fn validate_limit_topology(result: &RefinementResult) -> Result<(), KernelError> {
308    validate_refined_quads(result)?;
309    if result.options.boundary_interpolation == BoundaryInterpolation::Natural
310        && result.adjacency.edge_is_boundary.iter().any(|&b| b)
311    {
312        return Err(KernelError::NotImplemented(
313            "limit stencils for BoundaryInterpolation::Natural on open meshes",
314        ));
315    }
316    Ok(())
317}
318
319fn build_limit_stencils(result: &RefinementResult) -> Result<LimitStencils, KernelError> {
320    validate_limit_topology(result)?;
321    let mesh = &result.topology;
322    let adjacency = &result.adjacency;
323
324    let vertex_count = mesh.vertex_count as usize;
325    let mut position_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
326    let mut tangent1_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
327    let mut tangent2_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
328
329    for vi in 0..vertex_count {
330        let ring = vertex_ring(vi, mesh, adjacency)?;
331        let masks = match classify(vi, &ring, mesh, adjacency, &result.options) {
332            LimitRule::Smooth => smooth_masks(vi as u32, &ring),
333            LimitRule::Crease(ends) => crease_masks(vi as u32, &ring, ends),
334            LimitRule::Corner(_) => corner_masks(vi as u32, &ring),
335        };
336        debug_assert!(
337            (masks.position.iter().map(|&(_, w)| w).sum::<f32>() - 1.0).abs() < 1e-4,
338            "limit position mask of vertex {vi} is not affine",
339        );
340        debug_assert!(
341            masks.tangent1.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
342                && masks.tangent2.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4,
343            "limit tangent masks of vertex {vi} are not derivations",
344        );
345        position_rows.push(masks.position);
346        tangent1_rows.push(masks.tangent1);
347        tangent2_rows.push(masks.tangent2);
348    }
349
350    Ok(LimitStencils {
351        position: pack(&position_rows),
352        tangent1: pack(&tangent1_rows),
353        tangent2: pack(&tangent2_rows),
354    })
355}
356
357fn build_sectored_limit_stencils(
358    result: &RefinementResult,
359) -> Result<SectoredLimitStencils, KernelError> {
360    validate_limit_topology(result)?;
361    let mesh = &result.topology;
362    let adjacency = &result.adjacency;
363
364    let vertex_count = mesh.vertex_count as usize;
365    let mut position_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
366    let mut tangent1_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
367    let mut tangent2_rows: Vec<Sparse> = Vec::with_capacity(vertex_count);
368    let mut corner_sector = vec![u32::MAX; mesh.face_vertex_indices.len()];
369
370    for vi in 0..vertex_count {
371        let ring = vertex_ring(vi, mesh, adjacency)?;
372        let rule = classify(vi, &ring, mesh, adjacency, &result.options);
373        let sectors = vertex_sectors(vi as u32, &ring, rule);
374        debug_assert!(
375            (sectors.position.iter().map(|&(_, w)| w).sum::<f32>() - 1.0).abs() < 1e-4,
376            "limit position mask of vertex {vi} is not affine",
377        );
378        debug_assert!(
379            sectors.tangents.iter().all(|(tangent1, tangent2)| {
380                tangent1.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
381                    && tangent2.iter().map(|&(_, w)| w).sum::<f32>().abs() < 1e-4
382            }),
383            "a sector tangent mask of vertex {vi} is not a derivation",
384        );
385
386        // Scatter this vertex's fan slots into the per-corner map; each
387        // face corner belongs to exactly one vertex ring.
388        let first_row = tangent1_rows.len() as u32;
389        for (fan_slot, &fi) in ring.faces.iter().enumerate() {
390            let off = (fi * 4) as usize;
391            let corner = mesh.face_vertex_indices[off..off + 4]
392                .iter()
393                .position(|&c| c == vi as u32)
394                .ok_or(KernelError::InvalidTopology(
395                    "vertex-face adjacency references a face without that vertex",
396                ))?;
397            corner_sector[off + corner] = first_row + sectors.fan_sector[fan_slot];
398        }
399
400        position_rows.push(sectors.position);
401        for (tangent1, tangent2) in sectors.tangents {
402            tangent1_rows.push(tangent1);
403            tangent2_rows.push(tangent2);
404        }
405    }
406    debug_assert!(
407        corner_sector.iter().all(|&row| row != u32::MAX),
408        "a refined face-corner was not covered by any vertex ring",
409    );
410
411    Ok(SectoredLimitStencils {
412        position: pack(&position_rows),
413        tangent1: pack(&tangent1_rows),
414        tangent2: pack(&tangent2_rows),
415        corner_sector,
416    })
417}
418
419/// Sector decomposition of one vertex: the shared position mask, one
420/// tangent-mask pair per sector (ordered by leading ring slot), and the
421/// local sector index per ring fan slot (the face between ring edges
422/// `i` and `i + 1`).
423struct VertexSectors {
424    position: Sparse,
425    tangents: Vec<(Sparse, Sparse)>,
426    fan_sector: Vec<u32>,
427}
428
429impl VertexSectors {
430    /// The whole ring as one sector.
431    fn single(masks: LimitMasks, fan_count: usize) -> Self {
432        Self {
433            position: masks.position,
434            tangents: vec![(masks.tangent1, masks.tangent2)],
435            fan_sector: vec![0; fan_count],
436        }
437    }
438}
439
440/// Split one vertex's ring into sectors and evaluate per-sector masks;
441/// see the module docs for the per-rule decomposition.
442fn vertex_sectors(vi: u32, ring: &Ring, rule: LimitRule) -> VertexSectors {
443    let fan_count = ring.faces.len();
444    match rule {
445        LimitRule::Smooth => VertexSectors::single(smooth_masks(vi, ring), fan_count),
446        // A boundary crease vertex has surface on one side only.
447        LimitRule::Crease(ends) if ring.boundary => {
448            VertexSectors::single(crease_masks(vi, ring, ends), fan_count)
449        }
450        // An interior crease vertex has one sector per side: the
451        // counterclockwise span between the sharp edges and its
452        // complement, the latter evaluated on the re-origined ring so
453        // the crease masks see a forward span.
454        LimitRule::Crease([lead, trail]) => {
455            let near = crease_masks(vi, ring, [lead, trail]);
456            let far = crease_masks(vi, &ring.rotated(trail), [0, fan_count - (trail - lead)]);
457            VertexSectors {
458                position: near.position,
459                tangents: vec![(near.tangent1, near.tangent2), (far.tangent1, far.tangent2)],
460                fan_sector: (0..fan_count)
461                    .map(|slot| u32::from(!(lead..trail).contains(&slot)))
462                    .collect(),
463            }
464        }
465        // Pinned with fewer than two sharp edges: no fan is bounded, so
466        // the whole ring stays one sector with OpenSubdiv far's naive
467        // corner tangents.
468        LimitRule::Corner(sharp_slots) if sharp_slots.len() < 2 => {
469            VertexSectors::single(corner_masks(vi, ring), fan_count)
470        }
471        // A pinned vertex has one sector per fan between consecutive
472        // sharp edges; the position interpolates the vertex and each
473        // fan's tangents come from the crease tangent masks of its
474        // bounding edges (see the module docs for why not the naive
475        // bounding-edge differences).
476        LimitRule::Corner(sharp_slots) => {
477            let wrap = (!ring.boundary).then(|| {
478                let first = sharp_slots[0];
479                // SAFETY-free indexing: this arm has at least two slots.
480                let last = sharp_slots[sharp_slots.len() - 1];
481                (last, first + ring.edges.len())
482            });
483            let spans: Vec<(usize, usize)> = sharp_slots
484                .windows(2)
485                .map(|pair| (pair[0], pair[1]))
486                .chain(wrap)
487                .collect();
488            let tangents = spans
489                .iter()
490                .map(|&(lead, trail)| {
491                    let masks = if ring.boundary {
492                        crease_masks(vi, ring, [lead, trail])
493                    } else {
494                        crease_masks(vi, &ring.rotated(lead), [0, trail - lead])
495                    };
496                    (masks.tangent1, masks.tangent2)
497                })
498                .collect();
499            let fan_sector = (0..fan_count)
500                .map(|slot| {
501                    // Fan slot `slot` belongs to the sector led by the
502                    // last sharp slot at or before it; earlier slots
503                    // wrap into the trailing sector.
504                    let led_by = sharp_slots.partition_point(|&sharp| sharp <= slot);
505                    (if led_by == 0 {
506                        spans.len() - 1
507                    } else {
508                        led_by - 1
509                    }) as u32
510                })
511                .collect();
512            VertexSectors {
513                position: vec![(vi, 1.0)],
514                tangents,
515                fan_sector,
516            }
517        }
518    }
519}
520
521/// Derivative payload of [`corner_limit_sector`].
522pub(crate) enum SectorDerivatives {
523    /// Parametric one-sided limit derivatives along the corner's two
524    /// quad edges, with respect to the quad's own unit
525    /// parameterization: `d_out` along the out-edge (corner -> next
526    /// CSR corner), `d_in` along the in-edge (corner -> previous CSR
527    /// corner). Available at crease-rule corners whose quad edges are
528    /// each a sharp sector bound (the crease-curve derivative,
529    /// `(lead - trail) / 2`) or the single interior edge of a regular
530    /// two-face sector (the crease patch's cross derivative -- the
531    /// `tangent2` mask is exactly `limit(row 1) - limit(row 0)` of the
532    /// patch's control rows).
533    Parametric { d_out: Sparse, d_in: Sparse },
534    /// In-sector tangent pair only (smooth, dart, pinned, or irregular
535    /// sector): `tangent1 x tangent2` is the winding-oriented sector
536    /// normal, but the pair has no quad-parametric alignment or scale.
537    Plane { tangent1: Sparse, tangent2: Sparse },
538}
539
540/// Limit position and in-sector derivative masks of one face-corner,
541/// evaluated directly from its ring -- the row [`SectoredLimitStencils`]
542/// would assign that corner, without building whole-mesh tables (the
543/// feature-isolation submeshes of `limit_eval` have artificially
544/// truncated border vertices whose rings must never be walked).
545///
546/// The corner's vertex must have a complete fan. Boundary vertices
547/// under [`BoundaryInterpolation::Natural`] are rejected (no
548/// closed-form limit mask; see the module docs).
549pub(crate) fn corner_limit_sector(
550    face: u32,
551    corner: usize,
552    mesh: &Mesh,
553    adjacency: &Adjacency,
554    options: &SchemeOptions,
555) -> Result<(Sparse, SectorDerivatives), KernelError> {
556    let off = (face * 4) as usize;
557    let vi = mesh.face_vertex_indices[off + corner];
558    let ring = vertex_ring(vi as usize, mesh, adjacency)?;
559    if ring.boundary && options.boundary_interpolation == BoundaryInterpolation::Natural {
560        return Err(KernelError::NotImplemented(
561            "limit evaluation at boundary feature vertices under BoundaryInterpolation::Natural",
562        ));
563    }
564    let rule = classify(vi as usize, &ring, mesh, adjacency, options);
565    let crease_ends = match &rule {
566        LimitRule::Crease(ends) => Some(*ends),
567        _ => None,
568    };
569    let fan_slot =
570        ring.faces
571            .iter()
572            .position(|&f| f == face)
573            .ok_or(KernelError::InvalidTopology(
574                "corner vertex ring does not contain the corner's face",
575            ))?;
576    let mut sectors = vertex_sectors(vi, &ring, rule);
577    let row = sectors.fan_sector[fan_slot] as usize;
578    let (tangent1, tangent2) = sectors.tangents.swap_remove(row);
579
580    // The quad's sector span in ring slots (interior row 0 = the span
581    // between the sharp edges, row 1 = its complement; a boundary
582    // crease vertex has the one surface-side span).
583    let span = crease_ends.map(|[lead, trail]| {
584        if ring.boundary || row == 0 {
585            (lead, trail - lead)
586        } else {
587            (trail, ring.edges.len() - (trail - lead))
588        }
589    });
590    // Parametric derivative along ring edge `slot`, when the slot is a
591    // sector bound (+-tangent1 along the crease curve) or the single
592    // interior edge of a regular sector (+tangent2, the cross
593    // derivative); `None` leaves the corner plane-only.
594    let edge_derivative = |slot: usize| -> Option<Sparse> {
595        let (lead, len) = span?;
596        let rel = if ring.boundary {
597            slot.checked_sub(lead)?
598        } else {
599            (slot + ring.edges.len() - lead) % ring.edges.len()
600        };
601        if rel == 0 {
602            Some(tangent1.clone())
603        } else if rel == len {
604            Some(tangent1.iter().map(|&(i, w)| (i, -w)).collect())
605        } else if len == 2 && rel == 1 {
606            Some(tangent2.clone())
607        } else {
608            None
609        }
610    };
611    let slot_of = |edge: u32| ring.edges.iter().position(|&e| e == edge);
612    let out_slot = slot_of(adjacency.face_edges[off + corner]);
613    let in_slot = slot_of(adjacency.face_edges[off + (corner + 3) % 4]);
614    let derivatives = match (
615        out_slot.and_then(&edge_derivative),
616        in_slot.and_then(&edge_derivative),
617    ) {
618        (Some(d_out), Some(d_in)) => SectorDerivatives::Parametric { d_out, d_in },
619        _ => SectorDerivatives::Plane { tangent1, tangent2 },
620    };
621    Ok((sectors.position, derivatives))
622}
623
624/// Walk the oriented fan around `vi` from the per-face corner data.
625pub(crate) fn vertex_ring(
626    vi: usize,
627    mesh: &Mesh,
628    adjacency: &Adjacency,
629) -> Result<Ring, KernelError> {
630    let face_start = adjacency.vertex_face_offsets[vi] as usize;
631    let face_end = adjacency.vertex_face_offsets[vi + 1] as usize;
632    let incident_faces = &adjacency.vertex_faces[face_start..face_end];
633    if incident_faces.is_empty() {
634        return Err(KernelError::InvalidTopology(
635            "vertex without incident faces has no limit ring",
636        ));
637    }
638
639    let mut steps = incident_faces
640        .iter()
641        .map(|&fi| {
642            // All faces are quads (validated by the caller), so corner
643            // offsets are 4 * face.
644            let off = (fi * 4) as usize;
645            let corners = &mesh.face_vertex_indices[off..off + 4];
646            corners
647                .iter()
648                .position(|&c| c == vi as u32)
649                .map(|j| FanStep {
650                    out_edge: adjacency.face_edges[off + j],
651                    in_edge: adjacency.face_edges[off + (j + 3) % 4],
652                    diagonal: corners[(j + 2) % 4],
653                    face: fi,
654                    used: false,
655                })
656                .ok_or(KernelError::InvalidTopology(
657                    "vertex-face adjacency references a face without that vertex",
658                ))
659        })
660        .collect::<Result<Vec<_>, _>>()?;
661
662    let boundary = adjacency.vertex_is_boundary[vi];
663    let start = if boundary {
664        // The leading boundary edge is the one face-outgoing boundary
665        // edge; the walk then ends on the trailing boundary edge.
666        steps
667            .iter()
668            .find(|s| adjacency.edge_is_boundary[s.out_edge as usize])
669            .map(|s| s.out_edge)
670            .ok_or(KernelError::InvalidTopology(
671                "boundary vertex has no leading boundary edge (inconsistent face winding)",
672            ))?
673    } else {
674        steps[0].out_edge
675    };
676
677    let face_count = steps.len();
678    let mut edges = Vec::with_capacity(face_count + 1);
679    let mut diagonals = Vec::with_capacity(face_count);
680    let mut faces = Vec::with_capacity(face_count);
681    let mut current = start;
682    edges.push(current);
683    for k in 0..face_count {
684        let step = steps
685            .iter_mut()
686            .find(|s| !s.used && s.out_edge == current)
687            .ok_or(KernelError::InvalidTopology(
688                "vertex ring is not a single oriented fan",
689            ))?;
690        step.used = true;
691        diagonals.push(step.diagonal);
692        faces.push(step.face);
693        current = step.in_edge;
694        if boundary || k + 1 < face_count {
695            edges.push(current);
696        }
697    }
698    if !boundary && current != start {
699        return Err(KernelError::InvalidTopology(
700            "interior vertex ring does not close",
701        ));
702    }
703    if boundary && !adjacency.edge_is_boundary[current as usize] {
704        return Err(KernelError::InvalidTopology(
705            "boundary vertex ring does not end on a boundary edge",
706        ));
707    }
708    let incident_edge_count =
709        (adjacency.vertex_edge_offsets[vi + 1] - adjacency.vertex_edge_offsets[vi]) as usize;
710    if edges.len() != incident_edge_count {
711        return Err(KernelError::InvalidTopology(
712            "vertex ring does not cover all incident edges (non-manifold fan)",
713        ));
714    }
715
716    let neighbors = edges
717        .iter()
718        .map(|&ei| {
719            let [a, b] = mesh.edge_vertices[ei as usize];
720            if a as usize == vi { b } else { a }
721        })
722        .collect();
723
724    Ok(Ring {
725        neighbors,
726        edges,
727        diagonals,
728        faces,
729        boundary,
730    })
731}
732
733/// Select the limit rule for one vertex; see the module docs.
734fn classify(
735    vi: usize,
736    ring: &Ring,
737    mesh: &Mesh,
738    adjacency: &Adjacency,
739    options: &SchemeOptions,
740) -> LimitRule {
741    let sharp_slots: Vec<usize> = ring
742        .edges
743        .iter()
744        .enumerate()
745        .filter(|&(_, &ei)| {
746            mesh.edge_creases[ei as usize] > 0.0 || adjacency.edge_is_boundary[ei as usize]
747        })
748        .map(|(slot, _)| slot)
749        .collect();
750
751    // Sharp vertex tag (Sdc::Crease::IsSharp on the refined value).
752    if mesh.vertex_corners[vi] > 0.0 {
753        return LimitRule::Corner(sharp_slots);
754    }
755    // EdgesAndCorners pins single-face boundary vertices, mirroring the
756    // refiner's vertex_point_stencil.
757    if ring.boundary
758        && options.boundary_interpolation == BoundaryInterpolation::EdgesAndCorners
759        && ring.diagonals.len() <= 1
760    {
761        return LimitRule::Corner(sharp_slots);
762    }
763
764    match sharp_slots.as_slice() {
765        // Smooth and dart vertices share the smooth masks.
766        [] | [_] => LimitRule::Smooth,
767        [lead, trail] => LimitRule::Crease([*lead, *trail]),
768        _ => LimitRule::Corner(sharp_slots),
769    }
770}
771
772/// The three sparse limit masks of one vertex.
773struct LimitMasks {
774    position: Sparse,
775    tangent1: Sparse,
776    tangent2: Sparse,
777}
778
779/// Accumulate one weight, dropping exact zeros (OpenSubdiv masks carry
780/// explicit zero entries; CSR rows need not).
781fn push(row: &mut Sparse, index: u32, weight: f64) {
782    if weight != 0.0 {
783        merge(row, &[(index, weight as f32)], 1.0);
784    }
785}
786
787/// `assignCornerLimitMask` + `assignCornerLimitTangentMasks`: the limit
788/// interpolates the vertex; tangents run along the first two ring edges.
789fn corner_masks(vi: u32, ring: &Ring) -> LimitMasks {
790    let mut tangent1 = Sparse::new();
791    push(&mut tangent1, vi, -1.0);
792    push(&mut tangent1, ring.neighbors[0], 1.0);
793
794    let mut tangent2 = Sparse::new();
795    push(&mut tangent2, vi, -1.0);
796    push(&mut tangent2, ring.neighbors[1], 1.0);
797
798    LimitMasks {
799        position: vec![(vi, 1.0)],
800        tangent1,
801        tangent2,
802    }
803}
804
805/// `assignCreaseLimitMask` + `assignCreaseLimitTangentMasks` with
806/// `creaseEnds = ends`: the B-spline crease-curve limit along the two
807/// sharp edges, and the cross tangent over the counterclockwise fan
808/// between them (regular B-spline, Biermann et al. irregular, or the
809/// single-face average).
810fn crease_masks(vi: u32, ring: &Ring, ends: [usize; 2]) -> LimitMasks {
811    let [lead, trail] = ends;
812
813    let mut position = Sparse::new();
814    push(&mut position, vi, 2.0 / 3.0);
815    push(&mut position, ring.neighbors[lead], 1.0 / 6.0);
816    push(&mut position, ring.neighbors[trail], 1.0 / 6.0);
817
818    // Tangent along the crease, oriented with the leading edge.
819    let mut tangent1 = Sparse::new();
820    push(&mut tangent1, ring.neighbors[lead], 0.5);
821    push(&mut tangent1, ring.neighbors[trail], -0.5);
822
823    // Cross tangent over the span; face slot f sits between ring edges
824    // f and f + 1, so the span's faces are slots lead .. trail - 1.
825    let mut tangent2 = Sparse::new();
826    let interior_edge_count = trail - lead - 1;
827    if interior_edge_count == 1 {
828        // The regular case: uniform B-spline cross-tangent.
829        push(&mut tangent2, vi, -4.0 / 6.0);
830        push(&mut tangent2, ring.neighbors[lead], -1.0 / 6.0);
831        push(&mut tangent2, ring.neighbors[lead + 1], 4.0 / 6.0);
832        push(&mut tangent2, ring.neighbors[trail], -1.0 / 6.0);
833        push(&mut tangent2, ring.diagonals[lead], 1.0 / 6.0);
834        push(&mut tangent2, ring.diagonals[lead + 1], 1.0 / 6.0);
835    } else if interior_edge_count > 1 {
836        // The irregular case: formulae from Biermann et al.
837        let k = (interior_edge_count + 1) as f64;
838        let theta = PI / k;
839        let cos_theta = theta.cos();
840        let sin_theta = theta.sin();
841        let common_denom = 1.0 / (k * (3.0 + cos_theta));
842        let r = (cos_theta + 1.0) / sin_theta;
843
844        push(
845            &mut tangent2,
846            vi,
847            4.0 * r * (cos_theta - 1.0) * common_denom,
848        );
849        let crease_weight = -r * (1.0 + 2.0 * cos_theta) * common_denom;
850        push(&mut tangent2, ring.neighbors[lead], crease_weight);
851        push(&mut tangent2, ring.neighbors[trail], crease_weight);
852        push(
853            &mut tangent2,
854            ring.diagonals[lead],
855            sin_theta * common_denom,
856        );
857        for i in 1..interior_edge_count + 1 {
858            let sin_theta_i = (i as f64 * theta).sin();
859            let sin_theta_i_plus_1 = ((i + 1) as f64 * theta).sin();
860            push(
861                &mut tangent2,
862                ring.neighbors[lead + i],
863                4.0 * sin_theta_i * common_denom,
864            );
865            push(
866                &mut tangent2,
867                ring.diagonals[lead + i],
868                (sin_theta_i + sin_theta_i_plus_1) * common_denom,
869            );
870        }
871    } else {
872        // The single-face special case: simple average of the boundary
873        // edges.
874        push(&mut tangent2, vi, -6.0);
875        push(&mut tangent2, ring.neighbors[lead], 3.0);
876        push(&mut tangent2, ring.neighbors[trail], 3.0);
877    }
878
879    LimitMasks {
880        position,
881        tangent1,
882        tangent2,
883    }
884}
885
886/// `assignSmoothLimitMask` + `assignSmoothLimitTangentMasks` for an
887/// interior vertex of face valence `n`; valence 2 falls back to the
888/// corner masks, valence 4 uses the regular specializations.
889fn smooth_masks(vi: u32, ring: &Ring) -> LimitMasks {
890    let valence = ring.diagonals.len();
891    if valence == 2 {
892        return corner_masks(vi, ring);
893    }
894
895    let mut position = Sparse::new();
896    let mut tangent1 = Sparse::new();
897    let mut tangent2 = Sparse::new();
898
899    if valence == 4 {
900        push(&mut position, vi, 4.0 / 9.0);
901        let tan1_edge = [4.0, 0.0, -4.0, 0.0];
902        let tan1_face = [1.0, -1.0, -1.0, 1.0];
903        let tan2_edge = [0.0, 4.0, 0.0, -4.0];
904        let tan2_face = [1.0, 1.0, -1.0, -1.0];
905        for i in 0..4 {
906            push(&mut position, ring.neighbors[i], 1.0 / 9.0);
907            push(&mut position, ring.diagonals[i], 1.0 / 36.0);
908            push(&mut tangent1, ring.neighbors[i], tan1_edge[i]);
909            push(&mut tangent1, ring.diagonals[i], tan1_face[i]);
910            push(&mut tangent2, ring.neighbors[i], tan2_edge[i]);
911            push(&mut tangent2, ring.diagonals[i], tan2_face[i]);
912        }
913    } else {
914        // Position weights in f32 like the C++ general case.
915        let n = valence as f32;
916        let face_weight = 1.0 / (n * (n + 5.0));
917        let edge_weight = 4.0 * face_weight;
918        push(
919            &mut position,
920            vi,
921            (1.0 - n * (edge_weight + face_weight)) as f64,
922        );
923
924        // Tangent weights in f64 like the C++ (double trig, float cast).
925        let theta = 2.0 * PI / valence as f64;
926        let cos_theta = theta.cos();
927        let cos_half_theta = (theta * 0.5).cos();
928        let lambda = (5.0 / 16.0)
929            + (1.0 / 16.0) * (cos_theta + cos_half_theta * (2.0 * (9.0 + cos_theta)).sqrt());
930        let face_weight_scale = 1.0 / (4.0 * lambda - 1.0);
931
932        // tangent2 is tangent1 rotated by one ring slot.
933        let rotated = |i: usize| (i + valence - 1) % valence;
934        for i in 0..valence {
935            push(&mut position, ring.neighbors[i], edge_weight as f64);
936            push(&mut position, ring.diagonals[i], face_weight as f64);
937
938            let cos_theta_i = (i as f64 * theta).cos();
939            let cos_theta_i_plus_1 = ((i + 1) as f64 * theta).cos();
940            push(&mut tangent1, ring.neighbors[i], 4.0 * cos_theta_i);
941            push(
942                &mut tangent1,
943                ring.diagonals[i],
944                face_weight_scale * (cos_theta_i + cos_theta_i_plus_1),
945            );
946
947            let j = rotated(i);
948            let cos_theta_j = (j as f64 * theta).cos();
949            let cos_theta_j_plus_1 = ((j + 1) as f64 * theta).cos();
950            push(&mut tangent2, ring.neighbors[i], 4.0 * cos_theta_j);
951            push(
952                &mut tangent2,
953                ring.diagonals[i],
954                face_weight_scale * (cos_theta_j + cos_theta_j_plus_1),
955            );
956        }
957    }
958
959    LimitMasks {
960        position,
961        tangent1,
962        tangent2,
963    }
964}
965
966#[cfg(test)]
967mod tests {
968    use core::num::NonZeroU8;
969
970    use crate::{KernelError, Mesh, Refiner, Scheme, SchemeOptions, UniformRefine};
971
972    /// A 2x2 quad grid (the mask gates live in `tests/limit_surface.rs`
973    /// and `tests/limit_osd_oracle.rs`; these unit tests cover the
974    /// error paths only).
975    fn grid() -> Mesh {
976        Mesh {
977            vertex_count: 9,
978            face_vertex_counts: vec![4; 4],
979            face_vertex_indices: vec![0, 3, 4, 1, 1, 4, 5, 2, 3, 6, 7, 4, 4, 7, 8, 5],
980            edge_vertices: Vec::new(),
981            edge_creases: Vec::new(),
982            vertex_corners: vec![0.0; 9],
983        }
984    }
985
986    fn refine(
987        scheme: Scheme,
988        options: SchemeOptions,
989        req: &UniformRefine,
990    ) -> crate::RefinementResult {
991        let refiner = Refiner::new(grid(), scheme, options).expect("refiner");
992        refiner.refine_uniform(req).expect("refinement")
993    }
994
995    #[test]
996    fn non_catmull_clark_scheme_is_rejected() {
997        let result = refine(
998            Scheme::DooSabin,
999            SchemeOptions::default(),
1000            &UniformRefine::default(),
1001        );
1002        assert!(matches!(
1003            result.limit_stencils(),
1004            Err(KernelError::NotImplemented(_)),
1005        ));
1006        assert!(matches!(
1007            result.sectored_limit_stencils(),
1008            Err(KernelError::NotImplemented(_)),
1009        ));
1010    }
1011
1012    #[test]
1013    fn partial_face_selection_is_rejected() {
1014        let req = UniformRefine {
1015            // SAFETY: 1 is non-zero.
1016            levels: NonZeroU8::new(1).unwrap(),
1017            selected_faces: Some(vec![true, true, true, false]),
1018            ..Default::default()
1019        };
1020        let result = refine(Scheme::CatmullClark, SchemeOptions::default(), &req);
1021        assert!(matches!(
1022            result.limit_stencils(),
1023            Err(KernelError::NotImplemented(_)),
1024        ));
1025        assert!(matches!(
1026            result.sectored_limit_stencils(),
1027            Err(KernelError::NotImplemented(_)),
1028        ));
1029    }
1030
1031    #[test]
1032    fn natural_boundary_on_open_mesh_is_rejected() {
1033        let options = SchemeOptions {
1034            boundary_interpolation: crate::BoundaryInterpolation::Natural,
1035            ..Default::default()
1036        };
1037        let result = refine(Scheme::CatmullClark, options, &UniformRefine::default());
1038        assert!(matches!(
1039            result.limit_stencils(),
1040            Err(KernelError::NotImplemented(_)),
1041        ));
1042        assert!(matches!(
1043            result.sectored_limit_stencils(),
1044            Err(KernelError::NotImplemented(_)),
1045        ));
1046    }
1047}