Skip to main content

subdiv_kernels/
refiner.rs

1//! Topology refiner with cached analysis.
2//!
3//! The refiner is two-phase for all four schemes:
4//! [`Refiner::refine_topology`] builds and caches per-level topology
5//! in a [`Refinement`]; stencils are extracted on demand via
6//! [`Refinement::vertex_stencils`] or
7//! [`Refinement::face_varying_stencils`] without rebuilding
8//! topology.
9//!
10//! Per-level cached data is kept in a scheme-specific struct
11//! (`CcLevelData`, `LoopLevelData`, `Sqrt3LevelData`, or
12//! `DooSabinLevelData`) behind a single owning
13//! [`LevelData`] enum. A [`LevelDataCommon`] trait exposes the
14//! always-present fields (`mesh`, `lineage`, `face_selected`,
15//! `adjacency`) via enum-dispatch-generated impls, so accessors on
16//! [`Refinement`] can read them without manual match arms while
17//! scheme-specific stencil extraction still matches on the enum
18//! variant to pick the right per-scheme function.
19
20use enum_dispatch::enum_dispatch;
21
22use crate::catmull_clark::stencils::{
23    CcLevelData, base_level_data as cc_base_level_data,
24    refine_topology_once as cc_refine_topology_once,
25    vertex_stencils_from_level as cc_vertex_stencils_from_level,
26};
27use crate::doo_sabin::stencils::{
28    DooSabinLevelData, base_level_data as doo_sabin_base_level_data,
29    refine_topology_once as doo_sabin_refine_topology_once,
30    vertex_stencils_from_level as doo_sabin_vertex_stencils_from_level,
31};
32use crate::loop_subdivision::stencils::{
33    LoopLevelData, base_level_data as loop_base_level_data,
34    refine_topology_once as loop_refine_topology_once,
35    vertex_stencils_from_level as loop_vertex_stencils_from_level,
36};
37use crate::sqrt3::stencils::{
38    Sqrt3LevelData, base_level_data as sqrt3_base_level_data,
39    refine_topology_once as sqrt3_refine_topology_once,
40    vertex_stencils_from_level as sqrt3_vertex_stencils_from_level,
41};
42use crate::{
43    Adjacency, FaceVaryingChannel, FaceVaryingInterpolation, KernelError, LineageMaps, Mesh,
44    RefinementResult, Scheme, SchemeOptions, StencilTable, UniformRefine,
45};
46
47/// Per-scheme level data: shared accessor interface for the fields
48/// that every scheme's level cache holds. Scheme-specific fields
49/// (notably the internal `topo` analysis struct and crease/corner
50/// scratch) are read directly from the concrete variant in places
51/// that need them.
52#[enum_dispatch]
53pub(crate) trait LevelDataCommon {
54    fn mesh(&self) -> &Mesh;
55    fn lineage(&self) -> &LineageMaps;
56    fn face_selected(&self) -> &[bool];
57    fn adjacency(&self) -> &Adjacency;
58}
59
60/// Owning enum over per-scheme level caches.
61///
62/// Variants are type-distinct so the compiler can enforce "this
63/// `LevelData::Cc(_)` holds a `CcLevelData`, not a `LoopLevelData`".
64/// Per-element overhead is one discriminant byte plus alignment
65/// padding — negligible compared to the kilobytes/megabytes of cached
66/// topology inside each variant.
67#[enum_dispatch(LevelDataCommon)]
68pub(crate) enum LevelData {
69    Cc(CcLevelData),
70    Loop(LoopLevelData),
71    Sqrt3(Sqrt3LevelData),
72    DooSabin(DooSabinLevelData),
73}
74
75/// Subdivision refiner with validated topology and cached analysis.
76///
77/// Create a `Refiner` once for a given topology + scheme, then call
78/// [`refine_topology`](Self::refine_topology) for the cached two-phase
79/// API or [`refine_uniform`](Self::refine_uniform) for the one-shot
80/// `RefinementResult`-returning API.
81///
82/// # Example
83///
84/// ```ignore
85/// use core::num::NonZeroU8;
86/// let refiner = Refiner::new(topology, Scheme::CatmullClark, SchemeOptions::default())?;
87/// let req = UniformRefine { levels: NonZeroU8::new(2).unwrap(), ..Default::default() };
88/// let result = refiner.refine_uniform(&req)?;
89/// let positions = result.interpolate(&my_positions);
90/// ```
91pub struct Refiner {
92    topology: Mesh,
93    scheme: Scheme,
94    options: SchemeOptions,
95}
96
97/// Cached multi-level refinement topology.
98///
99/// Created by [`Refiner::refine_topology`]. Use
100/// [`vertex_stencils`](Self::vertex_stencils) and
101/// [`face_varying_stencils`](Self::face_varying_stencils) to compute
102/// stencils from the cached topology without redundant edge discovery,
103/// and [`level_lineage`](Self::level_lineage) /
104/// [`refinement_steps`](Self::refinement_steps) to walk multi-level
105/// ancestry by reference.
106///
107/// The shape mirrors OpenSubdiv's `Far::TopologyRefiner` — all per-level
108/// queries return borrowed slices into cached storage so adapters can
109/// fold across levels without cloning per-level state.
110#[must_use]
111pub struct Refinement {
112    /// Base level + all refined levels. `levels[0]` is the base mesh
113    /// (lineage is [`LineageMaps::default`]); `levels[1..=N]` hold
114    /// each refinement step's cached state.
115    levels: Vec<LevelData>,
116    scheme: Scheme,
117    options: SchemeOptions,
118    /// Polyline tracking (populated when the request asked for it).
119    edge_polylines: Option<Vec<Vec<u32>>>,
120}
121
122/// Owned outputs of [`Refinement::into_final_parts`].
123///
124/// Mirrors the public bits of [`RefinementResult`] without the per-level
125/// stencil tables — those must be computed via
126/// [`vertex_stencils`](Refinement::vertex_stencils) and
127/// [`face_varying_stencils`](Refinement::face_varying_stencils)
128/// *before* consuming the handle.
129///
130/// For multi-level ancestry, walk
131/// [`Refinement::level_lineage`] before calling
132/// [`into_final_parts`](Refinement::into_final_parts). Those
133/// accessors return borrowed slices and cost nothing; `into_final_parts`
134/// then consumes the handle and moves out the final-level owned state
135/// with zero clones.
136#[non_exhaustive]
137pub struct RefinedFinalParts {
138    /// Final-level control mesh (faces, edges, creases).
139    pub topology: Mesh,
140    /// Per-level vertex/edge/face lineage back to the input.
141    pub lineage: LineageMaps,
142    /// Final-level adjacency (edges, vertex rings, boundary flags).
143    pub adjacency: Adjacency,
144    /// Final-level face-selection mask, if face-selective refinement was used.
145    pub selected_faces: Option<Vec<bool>>,
146    /// Per input edge, the refined vertices lying on it, if requested.
147    pub edge_polylines: Option<Vec<Vec<u32>>>,
148}
149
150impl Refinement {
151    /// The subdivision scheme this refinement was produced with.
152    #[must_use]
153    pub fn scheme(&self) -> Scheme {
154        self.scheme
155    }
156
157    /// Compute per-level vertex stencils from cached topology.
158    ///
159    /// Returns one `StencilTable` per refinement step (length equals
160    /// [`refinement_steps`](Self::refinement_steps)). Apply with the
161    /// chaining pattern from
162    /// [`RefinementResult::interpolate`](crate::RefinementResult::interpolate):
163    ///
164    /// ```ignore
165    /// let tables = refined.vertex_stencils();
166    /// let final_positions = tables
167    ///     .iter()
168    ///     .fold(input_positions, |data, t| t.interpolate(&data));
169    /// ```
170    pub fn vertex_stencils(&self) -> Vec<StencilTable> {
171        // The parent of level k is `levels[k]` (level 0 = base).
172        // Skip the last element because it has no "next level" output.
173        let parent_count = self.levels.len().saturating_sub(1);
174        (0..parent_count)
175            .map(|k| match &self.levels[k] {
176                LevelData::Cc(parent) => cc_vertex_stencils_from_level(parent, &self.options),
177                LevelData::Loop(parent) => loop_vertex_stencils_from_level(parent, &self.options),
178                LevelData::Sqrt3(parent) => sqrt3_vertex_stencils_from_level(parent, &self.options),
179                LevelData::DooSabin(parent) => {
180                    doo_sabin_vertex_stencils_from_level(parent, &self.options)
181                }
182            })
183            .collect()
184    }
185
186    /// Compute per-level face-varying stencils from cached topology.
187    ///
188    /// All four [`FaceVaryingInterpolation`] modes are implemented for CC,
189    /// Loop and √3; Doo-Sabin's face-local smooth rule makes its three smooth
190    /// modes coincide.
191    pub fn face_varying_stencils(
192        &self,
193        channel: &FaceVaryingChannel,
194        mode: FaceVaryingInterpolation,
195    ) -> Result<Vec<StencilTable>, KernelError> {
196        let level_count = self.levels.len().saturating_sub(1);
197        let mut tables = Vec::with_capacity(level_count);
198        let mut current_fvar = channel.clone();
199
200        for i in 0..level_count {
201            let table = match (&self.levels[i], &self.levels[i + 1]) {
202                (LevelData::Cc(parent), LevelData::Cc(child)) => {
203                    crate::catmull_clark::face_varying::fvar_stencils_once(
204                        parent,
205                        child,
206                        &current_fvar,
207                        mode,
208                        &self.options,
209                    )?
210                }
211                (LevelData::Loop(parent), LevelData::Loop(child)) => {
212                    crate::loop_subdivision::face_varying::fvar_stencils_once(
213                        parent,
214                        child,
215                        &current_fvar,
216                        mode,
217                        &self.options,
218                    )?
219                }
220                (LevelData::Sqrt3(parent), LevelData::Sqrt3(child)) => {
221                    crate::sqrt3::face_varying::fvar_stencils_once(
222                        parent,
223                        child,
224                        &current_fvar,
225                        mode,
226                        &self.options,
227                    )?
228                }
229                (LevelData::DooSabin(parent), LevelData::DooSabin(child)) => {
230                    crate::doo_sabin::face_varying::fvar_stencils_once(
231                        parent,
232                        child,
233                        &current_fvar,
234                        mode,
235                        &self.options,
236                    )?
237                }
238                _ => unreachable!("a refinement chain holds one scheme's level data throughout"),
239            };
240            tables.push(table);
241
242            current_fvar = identity_channel(self.levels[i + 1].mesh());
243        }
244
245        Ok(tables)
246    }
247
248    /// Final refined topology.
249    pub fn final_topology(&self) -> &Mesh {
250        self.levels.last().expect("at least one level").mesh()
251    }
252
253    /// Lineage maps (last level, relative to level N-1).
254    pub fn lineage(&self) -> &LineageMaps {
255        self.levels.last().expect("at least one level").lineage()
256    }
257
258    /// Adjacency (last level).
259    pub fn adjacency(&self) -> &Adjacency {
260        self.levels.last().expect("at least one level").adjacency()
261    }
262
263    /// Edge polylines (original parent-edge slots → refined vertex
264    /// index sequences), if requested at refinement time.
265    pub fn edge_polylines(&self) -> Option<&[Vec<u32>]> {
266        self.edge_polylines.as_deref()
267    }
268
269    /// Selected faces at final level.
270    pub fn selected_faces(&self) -> Option<&[bool]> {
271        // Only meaningful when at least one refinement level ran AND
272        // the request carried a selection mask; the base level stores
273        // `face_selected = vec![true; face_count]` when no selection
274        // was supplied, so we need to distinguish those cases. For
275        // the CC cached path we mirror the earlier accessor's check
276        // and return `None` when `levels.len() <= 1` (base-only =
277        // refinement did not run).
278        if self.levels.len() <= 1 {
279            return None;
280        }
281        Some(
282            self.levels
283                .last()
284                .expect("at least one level")
285                .face_selected(),
286        )
287    }
288
289    /// Number of refinement steps (levels beyond the base mesh).
290    pub fn refinement_steps(&self) -> usize {
291        self.levels.len().saturating_sub(1)
292    }
293
294    /// Per-level lineage view by reference, zero allocation.
295    ///
296    /// `level_lineage(step)` returns the lineage of the refinement step
297    /// indexed from 0 (first refinement level relative to the base
298    /// mesh). Valid range is `0..refinement_steps()`.
299    ///
300    /// Adapters fold over this to chain ancestry from the final level
301    /// back to the original input mesh, mirroring OpenSubdiv's
302    /// `TopologyLevel::face_parent_face` / per-level walk pattern.
303    pub fn level_lineage(&self, step: usize) -> Option<&LineageMaps> {
304        // levels[0] is the base (empty lineage); refinement steps
305        // start at levels[1].
306        self.levels.get(step + 1).map(|level| level.lineage())
307    }
308
309    /// Consume the handle and yield its owned final-level outputs.
310    ///
311    /// Call this *after* any stencil or per-level-lineage borrows have
312    /// ended ([`vertex_stencils`](Self::vertex_stencils),
313    /// [`face_varying_stencils`](Self::face_varying_stencils),
314    /// [`level_lineage`](Self::level_lineage)), since those read from
315    /// cached level data that this method moves out.
316    ///
317    /// Zero clones: topology / lineage / adjacency / polylines are
318    /// moved out of the final level's scheme-specific struct.
319    pub fn into_final_parts(mut self) -> RefinedFinalParts {
320        let last = self.levels.pop().expect("at least one level");
321        // After the pop, `levels.len() >= 1` means refinement ran
322        // (base still present). Preserve the selection mask only when
323        // refinement actually happened.
324        let refinement_ran = !self.levels.is_empty();
325        match last {
326            LevelData::Cc(level) => RefinedFinalParts {
327                topology: level.mesh,
328                lineage: level.lineage,
329                adjacency: level.adjacency,
330                selected_faces: refinement_ran.then_some(level.face_selected),
331                edge_polylines: self.edge_polylines,
332            },
333            LevelData::Loop(level) => RefinedFinalParts {
334                topology: level.mesh,
335                lineage: level.lineage,
336                adjacency: level.adjacency,
337                selected_faces: refinement_ran.then_some(level.face_selected),
338                edge_polylines: self.edge_polylines,
339            },
340            LevelData::Sqrt3(level) => RefinedFinalParts {
341                topology: level.mesh,
342                lineage: level.lineage,
343                adjacency: level.adjacency,
344                selected_faces: refinement_ran.then_some(level.face_selected),
345                edge_polylines: self.edge_polylines,
346            },
347            LevelData::DooSabin(level) => RefinedFinalParts {
348                topology: level.mesh,
349                lineage: level.lineage,
350                adjacency: level.adjacency,
351                selected_faces: refinement_ran.then_some(level.face_selected),
352                edge_polylines: self.edge_polylines,
353            },
354        }
355    }
356}
357
358impl Refiner {
359    /// Create a new refiner, validating the input topology.
360    pub fn new(
361        topology: Mesh,
362        scheme: Scheme,
363        options: SchemeOptions,
364    ) -> Result<Self, KernelError> {
365        topology.validate()?;
366        Ok(Self {
367            topology,
368            scheme,
369            options,
370        })
371    }
372
373    /// Access the input topology.
374    pub fn topology(&self) -> &Mesh {
375        &self.topology
376    }
377
378    /// Access the scheme.
379    pub fn scheme(&self) -> Scheme {
380        self.scheme
381    }
382
383    /// Access the scheme options.
384    pub fn options(&self) -> &SchemeOptions {
385        &self.options
386    }
387
388    /// Build and cache per-level topology for all refinement levels.
389    ///
390    /// This is the expensive phase (edge discovery, adjacency
391    /// construction). Call [`Refinement::vertex_stencils`] and
392    /// [`Refinement::face_varying_stencils`] to compute stencils
393    /// from the cached topology without redundant edge rebuilds.
394    pub fn refine_topology(&self, req: &UniformRefine) -> Result<Refinement, KernelError> {
395        match self.scheme {
396            Scheme::CatmullClark => self.refine_topology_cc(req),
397            Scheme::Loop => self.refine_topology_loop(req),
398            Scheme::Sqrt3 => self.refine_topology_sqrt3(req),
399            Scheme::DooSabin => self.refine_topology_doo_sabin(req),
400        }
401    }
402
403    fn active_selection(&self, req: &UniformRefine) -> Result<Vec<bool>, KernelError> {
404        let initial_face_count = self.topology.face_vertex_counts.len();
405        req.selected_faces
406            .as_ref()
407            .map(|m| {
408                (m.len() == initial_face_count).then(|| m.clone()).ok_or(
409                    KernelError::InvalidTopology(
410                        "selected-face mask length does not match face count",
411                    ),
412                )
413            })
414            .transpose()
415            .map(|opt| opt.unwrap_or_else(|| vec![true; initial_face_count]))
416    }
417
418    fn refine_topology_cc(&self, req: &UniformRefine) -> Result<Refinement, KernelError> {
419        let active_sel = self.active_selection(req)?;
420        let base = cc_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
421
422        let mut levels: Vec<LevelData> = Vec::with_capacity(req.levels.get() as usize + 1);
423        levels.push(LevelData::Cc(base));
424
425        let mut polylines = req.edge_polylines.then(|| {
426            self.topology
427                .edge_vertices
428                .iter()
429                .map(|&[v0, v1]| vec![v0, v1])
430                .collect::<Vec<_>>()
431        });
432
433        for _ in 0..req.levels.get() {
434            let parent = match levels.last().unwrap() {
435                LevelData::Cc(p) => p,
436                _ => unreachable!("CC refine push-chain only touches CC variants"),
437            };
438            let child =
439                cc_refine_topology_once(parent, &self.options, req.selection_boundary_crease)?;
440
441            if let Some(ref mut polys) = polylines {
442                Self::refine_polylines(polys, &child.lineage, &parent.mesh);
443            }
444
445            levels.push(LevelData::Cc(child));
446        }
447
448        Ok(Refinement {
449            levels,
450            scheme: self.scheme,
451            options: self.options,
452            edge_polylines: polylines,
453        })
454    }
455
456    fn refine_topology_loop(&self, req: &UniformRefine) -> Result<Refinement, KernelError> {
457        let active_sel = self.active_selection(req)?;
458        let base = loop_base_level_data(
459            &self.topology,
460            active_sel,
461            &self.options,
462            req.selection_boundary_crease,
463        )?;
464
465        let mut levels: Vec<LevelData> = Vec::with_capacity(req.levels.get() as usize + 1);
466        levels.push(LevelData::Loop(base));
467
468        let mut polylines = req.edge_polylines.then(|| {
469            self.topology
470                .edge_vertices
471                .iter()
472                .map(|&[v0, v1]| vec![v0, v1])
473                .collect::<Vec<_>>()
474        });
475
476        for _ in 0..req.levels.get() {
477            let parent = match levels.last().unwrap() {
478                LevelData::Loop(p) => p,
479                _ => unreachable!("Loop refine push-chain only touches Loop variants"),
480            };
481            let child =
482                loop_refine_topology_once(parent, &self.options, req.selection_boundary_crease)?;
483
484            if let Some(ref mut polys) = polylines {
485                Self::refine_polylines(polys, &child.lineage, &parent.mesh);
486            }
487
488            levels.push(LevelData::Loop(child));
489        }
490
491        Ok(Refinement {
492            levels,
493            scheme: self.scheme,
494            options: self.options,
495            edge_polylines: polylines,
496        })
497    }
498
499    fn refine_topology_sqrt3(&self, req: &UniformRefine) -> Result<Refinement, KernelError> {
500        let active_sel = self.active_selection(req)?;
501        let base =
502            sqrt3_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
503
504        let mut levels: Vec<LevelData> = Vec::with_capacity(req.levels.get() as usize + 1);
505        levels.push(LevelData::Sqrt3(base));
506
507        let mut polylines = req.edge_polylines.then(|| {
508            self.topology
509                .edge_vertices
510                .iter()
511                .map(|&[v0, v1]| vec![v0, v1])
512                .collect::<Vec<_>>()
513        });
514
515        for _ in 0..req.levels.get() {
516            let parent = match levels.last().unwrap() {
517                LevelData::Sqrt3(p) => p,
518                _ => unreachable!("Sqrt3 refine push-chain only touches Sqrt3 variants"),
519            };
520            let child =
521                sqrt3_refine_topology_once(parent, &self.options, req.selection_boundary_crease)?;
522
523            if let Some(ref mut polys) = polylines {
524                Self::refine_polylines(polys, &child.lineage, &parent.mesh);
525            }
526
527            levels.push(LevelData::Sqrt3(child));
528        }
529
530        Ok(Refinement {
531            levels,
532            scheme: self.scheme,
533            options: self.options,
534            edge_polylines: polylines,
535        })
536    }
537
538    fn refine_topology_doo_sabin(&self, req: &UniformRefine) -> Result<Refinement, KernelError> {
539        let active_sel = self.active_selection(req)?;
540        let base =
541            doo_sabin_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
542
543        let mut levels: Vec<LevelData> = Vec::with_capacity(req.levels.get() as usize + 1);
544        levels.push(LevelData::DooSabin(base));
545
546        // Doo-Sabin is a dual scheme: parent vertices do not survive as
547        // `VertexOrigin::Vertex`, and parent edges do not survive as edges
548        // (they become faces). `Self::refine_polylines` relies on
549        // vertex-origin / edge-origin lineage to advance polylines, so its
550        // output is meaningless for Doo-Sabin. Always return `None`, even if
551        // the caller set `edge_polylines: true`.
552        let polylines: Option<Vec<Vec<u32>>> = None;
553
554        for _ in 0..req.levels.get() {
555            let parent = match levels.last().unwrap() {
556                LevelData::DooSabin(p) => p,
557                _ => unreachable!("DooSabin refine push-chain only touches DooSabin variants"),
558            };
559            let child = doo_sabin_refine_topology_once(
560                parent,
561                &self.options,
562                req.selection_boundary_crease,
563            )?;
564
565            levels.push(LevelData::DooSabin(child));
566        }
567
568        Ok(Refinement {
569            levels,
570            scheme: self.scheme,
571            options: self.options,
572            edge_polylines: polylines,
573        })
574    }
575
576    /// Perform uniform refinement, producing refined topology + vertex stencils.
577    ///
578    /// The returned [`StencilTable`]s map input vertex data to refined
579    /// vertex data. Apply them with [`StencilTable::interpolate`] to
580    /// any buffer, or use [`RefinementResult::interpolate`] to chain
581    /// through all levels in one call.
582    pub fn refine_uniform(&self, req: &UniformRefine) -> Result<RefinementResult, KernelError> {
583        let refined = self.refine_topology(req)?;
584        let level_stencils = refined.vertex_stencils();
585        // Pre-fold the per-level face lineage to the base mesh, so adapters
586        // get refined-face -> input-face directly instead of re-walking the
587        // levels (which `RefinementResult` does not carry).
588        let face_root = {
589            let base_faces = self.topology.face_vertex_counts.len() as u32;
590            let mut root: Vec<u32> = (0..base_faces).collect();
591            for step in 0..refined.refinement_steps() {
592                let lineage = refined
593                    .level_lineage(step)
594                    .expect("refinement_steps bounds level_lineage");
595                root = lineage
596                    .face_parent
597                    .iter()
598                    .map(|&parent| root[parent as usize])
599                    .collect();
600            }
601            root
602        };
603        Ok(RefinementResult {
604            topology: refined.final_topology().clone(),
605            level_stencils,
606            lineage: refined.lineage().clone(),
607            face_root,
608            selected_faces: refined.selected_faces().map(|s| s.to_vec()),
609            edge_polylines: refined.edge_polylines().map(|p| p.to_vec()),
610            adjacency: refined.adjacency().clone(),
611            scheme: self.scheme,
612            options: self.options,
613        })
614    }
615
616    /// For each parent-edge polyline, insert the edge-point vertex at
617    /// each split position in the polyline. After refinement, a polyline
618    /// segment `[A, B]` where edge A-B was split becomes `[A, edge_pt, B]`.
619    fn refine_polylines(polylines: &mut [Vec<u32>], lineage: &LineageMaps, parent_topo: &Mesh) {
620        use crate::output::VertexOrigin;
621
622        // Build a map: parent edge index → refined vertex index (the edge-point).
623        let edge_point_for_parent: Vec<Option<u32>> = {
624            let edge_count = parent_topo.edge_vertices.len();
625            let mut map = vec![None; edge_count];
626            lineage
627                .vertex_origin
628                .iter()
629                .enumerate()
630                .for_each(|(vi, origin)| {
631                    if let VertexOrigin::Edge(parent_ei) = *origin {
632                        map[parent_ei as usize] = Some(vi as u32);
633                    }
634                });
635            map
636        };
637
638        // Build a map: (v0, v1) canonical pair → parent edge index.
639        let edge_key = |a: u32, b: u32| if a <= b { (a, b) } else { (b, a) };
640        let edge_key_to_idx: rustc_hash::FxHashMap<(u32, u32), usize> = parent_topo
641            .edge_vertices
642            .iter()
643            .enumerate()
644            .map(|(ei, &[v0, v1])| (edge_key(v0, v1), ei))
645            .collect();
646
647        // For vertex-point vertices, find the mapping from parent vertex → refined vertex.
648        let vertex_point_for_parent: Vec<Option<u32>> = {
649            let vert_count = parent_topo.vertex_count as usize;
650            let mut map = vec![None; vert_count];
651            lineage
652                .vertex_origin
653                .iter()
654                .enumerate()
655                .for_each(|(vi, origin)| {
656                    if let VertexOrigin::Vertex(parent_vi) = *origin {
657                        map[parent_vi as usize] = Some(vi as u32);
658                    }
659                });
660            map
661        };
662
663        polylines.iter_mut().for_each(|poly| {
664            let mut new_poly = Vec::with_capacity(poly.len() * 2);
665
666            poly.windows(2).for_each(|pair| {
667                let a = pair[0];
668                let b = pair[1];
669
670                let ra = vertex_point_for_parent
671                    .get(a as usize)
672                    .copied()
673                    .flatten()
674                    .unwrap_or(a);
675
676                new_poly.push(ra);
677
678                let key = edge_key(a, b);
679                if let Some(&ei) = edge_key_to_idx.get(&key) {
680                    if let Some(ep) = edge_point_for_parent[ei] {
681                        new_poly.push(ep);
682                    }
683                }
684            });
685
686            if let Some(&last) = poly.last() {
687                let rl = vertex_point_for_parent
688                    .get(last as usize)
689                    .copied()
690                    .flatten()
691                    .unwrap_or(last);
692                new_poly.push(rl);
693            }
694
695            *poly = new_poly;
696        });
697    }
698
699    /// Compute per-level face-varying stencil tables for a channel.
700    ///
701    /// Returns one [`StencilTable`] per refinement level. Use the same
702    /// chaining pattern as [`RefinementResult::interpolate`]:
703    ///
704    /// ```ignore
705    /// let fvar_tables = refiner.face_varying_stencils(&req, &channel, mode)?;
706    /// let mut uvs = my_uvs.to_vec();
707    /// for table in &fvar_tables {
708    ///     uvs = table.interpolate(&uvs);
709    /// }
710    /// ```
711    pub fn face_varying_stencils(
712        &self,
713        req: &UniformRefine,
714        channel: &FaceVaryingChannel,
715        mode: FaceVaryingInterpolation,
716    ) -> Result<Vec<StencilTable>, KernelError> {
717        match self.scheme {
718            Scheme::CatmullClark => self.face_varying_stencils_cc(req, channel, mode),
719            Scheme::Loop => self.face_varying_stencils_loop(req, channel, mode),
720            Scheme::Sqrt3 => self.face_varying_stencils_sqrt3(req, channel, mode),
721            Scheme::DooSabin => self.face_varying_stencils_doo_sabin(req, channel, mode),
722        }
723    }
724
725    fn face_varying_stencils_loop(
726        &self,
727        req: &UniformRefine,
728        channel: &FaceVaryingChannel,
729        mode: FaceVaryingInterpolation,
730    ) -> Result<Vec<StencilTable>, KernelError> {
731        use crate::loop_subdivision::face_varying::fvar_stencils_once;
732
733        let active_sel = self.active_selection(req)?;
734        let mut parent = loop_base_level_data(
735            &self.topology,
736            active_sel,
737            &self.options,
738            req.selection_boundary_crease,
739        )?;
740        let mut current_fvar = channel.clone();
741        let mut tables = Vec::with_capacity(req.levels.get() as usize);
742
743        for _ in 0..req.levels.get() {
744            let child =
745                loop_refine_topology_once(&parent, &self.options, req.selection_boundary_crease)?;
746            tables.push(fvar_stencils_once(
747                &parent,
748                &child,
749                &current_fvar,
750                mode,
751                &self.options,
752            )?);
753            current_fvar = identity_channel(&child.mesh);
754            parent = child;
755        }
756
757        Ok(tables)
758    }
759
760    fn face_varying_stencils_sqrt3(
761        &self,
762        req: &UniformRefine,
763        channel: &FaceVaryingChannel,
764        mode: FaceVaryingInterpolation,
765    ) -> Result<Vec<StencilTable>, KernelError> {
766        use crate::sqrt3::face_varying::fvar_stencils_once;
767
768        let active_sel = self.active_selection(req)?;
769        let mut parent =
770            sqrt3_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
771        let mut current_fvar = channel.clone();
772        let mut tables = Vec::with_capacity(req.levels.get() as usize);
773
774        for _ in 0..req.levels.get() {
775            let child =
776                sqrt3_refine_topology_once(&parent, &self.options, req.selection_boundary_crease)?;
777            tables.push(fvar_stencils_once(
778                &parent,
779                &child,
780                &current_fvar,
781                mode,
782                &self.options,
783            )?);
784            current_fvar = identity_channel(&child.mesh);
785            parent = child;
786        }
787
788        Ok(tables)
789    }
790
791    fn face_varying_stencils_doo_sabin(
792        &self,
793        req: &UniformRefine,
794        channel: &FaceVaryingChannel,
795        mode: FaceVaryingInterpolation,
796    ) -> Result<Vec<StencilTable>, KernelError> {
797        use crate::doo_sabin::face_varying::fvar_stencils_once;
798
799        let active_sel = self.active_selection(req)?;
800        let mut parent =
801            doo_sabin_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
802        let mut current_fvar = channel.clone();
803        let mut tables = Vec::with_capacity(req.levels.get() as usize);
804
805        for _ in 0..req.levels.get() {
806            let child = doo_sabin_refine_topology_once(
807                &parent,
808                &self.options,
809                req.selection_boundary_crease,
810            )?;
811            tables.push(fvar_stencils_once(
812                &parent,
813                &child,
814                &current_fvar,
815                mode,
816                &self.options,
817            )?);
818            current_fvar = identity_channel(&child.mesh);
819            parent = child;
820        }
821
822        Ok(tables)
823    }
824
825    fn face_varying_stencils_cc(
826        &self,
827        req: &UniformRefine,
828        channel: &FaceVaryingChannel,
829        mode: FaceVaryingInterpolation,
830    ) -> Result<Vec<StencilTable>, KernelError> {
831        use crate::catmull_clark::face_varying::fvar_stencils_once;
832
833        let active_sel = self.active_selection(req)?;
834        let mut current_level =
835            cc_base_level_data(&self.topology, active_sel, req.selection_boundary_crease)?;
836        let mut current_fvar = channel.clone();
837        let mut tables = Vec::with_capacity(req.levels.get() as usize);
838
839        for _ in 0..req.levels.get() {
840            let child = cc_refine_topology_once(
841                &current_level,
842                &self.options,
843                req.selection_boundary_crease,
844            )?;
845
846            tables.push(fvar_stencils_once(
847                &current_level,
848                &child,
849                &current_fvar,
850                mode,
851                &self.options,
852            )?);
853
854            current_fvar = identity_channel(&child.mesh);
855            current_level = child;
856        }
857
858        Ok(tables)
859    }
860}
861
862/// Identity face-varying channel for a refined mesh: every refined corner is
863/// its own distinct value. Used to reset the channel between per-level
864/// face-varying stencil tables, mirroring the per-vertex stencil chaining.
865fn identity_channel(mesh: &Mesh) -> FaceVaryingChannel {
866    let n: u32 = mesh.face_vertex_counts.iter().sum();
867    FaceVaryingChannel {
868        indices: (0..n).collect(),
869        value_count: n,
870    }
871}