Skip to main content

subdiv_kernels/
closest_point.rs

1//! Closest point and signed distance to the Catmull-Clark limit
2//! surface (limit-surface SDF design s3, per design sections 4-4/4-5).
3//!
4//! [`LimitEvaluator::closest_point`] answers point queries against the
5//! exact limit surface the s1/s2 machinery evaluates: a best-first
6//! descent of a per-refined-quad AABB hierarchy proposes candidate
7//! quads, a safeguarded Gauss-Newton iteration on each candidate's
8//! `(u, v)` polishes the foot point -- walking across refined-quad
9//! seams through the [`RefinementResult`] adjacency when the minimizer
10//! pins to a quad edge or corner -- and an angle-weighted pseudonormal
11//! signs the distance on closed surfaces.
12//!
13//! The query lives on [`LimitEvaluator`] itself rather than a wrapping
14//! query struct: the acceleration index depends on exactly the
15//! topology + positions pair the evaluator already binds, shares its
16//! lifetime and invalidation story with the s2 isolation cache, and
17//! the s4 SDF sampler and s5 amendment drag both hold an evaluator
18//! anyway. The index is built lazily on the first query (a `OnceCell`
19//! next to the isolation `RefCell`) and reused by every later query --
20//! like the evaluator, cheap to build per thread but not `Sync`.
21//!
22//! # Acceleration: conservative per-quad AABBs
23//!
24//! One axis-aligned box per refined quad, each guaranteed to contain
25//! the quad's entire limit patch, so box-distance pruning can never
26//! cut off the true minimizer:
27//!
28//! - **Regular quads**: the box of the patch's 16 B-spline control
29//!   points -- the bicubic basis is nonnegative and partitions unity,
30//!   so the patch lies in the control points' convex hull.
31//! - **Feature quads**: the box of the quad's *support submesh*
32//!   vertices (the quad plus every face sharing a vertex with it, the
33//!   exact point set s2's recursive isolation refines). Every
34//!   Catmull-Clark refinement rule and every limit mask the isolation
35//!   applies is a convex combination -- nonnegative weights summing to
36//!   one -- and by the s2 support contract each isolation level's
37//!   central children read only points derived from the previous
38//!   level's support, so by induction the central quad's limit lies in
39//!   the convex hull of the support cage, hence in its box.
40//!
41//! The boxes feed a median-split binary BVH (built once, O(n log n),
42//! flat nodes). A BVH over patch boxes was chosen over a point
43//! kd-tree/grid because the boxes bound the *continuous* patches --
44//! pruning is exact rather than sample-resolution-limited -- and over
45//! no index at all because the SDF sampler and amendment drags are
46//! many-query consumers (design section 6).
47//!
48//! # Candidates -> Gauss-Newton, and the seam walk
49//!
50//! Best-first descent orders nodes by box distance in a min-heap and
51//! stops when the nearest unvisited box is no closer than the best
52//! foot point so far. Each surviving candidate quad runs a damped
53//! Gauss-Newton minimization of `|S(u, v) - q|^2` from five starts
54//! (center plus the four corners) using
55//! [`eval_with_derivatives`](LimitEvaluator::eval_with_derivatives)'s
56//! first derivatives -- the normal-equations step; the curvature term
57//! of the true Hessian is unavailable through the s2 interface and
58//! unnecessary, since the step-halving line search only ever accepts
59//! strictly improving iterates. Steps are clamped to the unit square,
60//! and when the full step fails the line search against a pinned
61//! bound, the active-set reduction retries with the 1D Gauss-Newton
62//! step along the free coordinate (quadratic along edge minimizers
63//! where the clamped full step would creep linearly). When an
64//! accepted step pins to a quad edge with the unclamped step
65//! pointing outside, the iteration *transfers* across the seam to the
66//! adjacent refined quad (winding-consistent parameter remap) and
67//! continues, and a run that converges pinned to an edge or corner
68//! seeds the adjacent quad -- the whole vertex fan at a corner -- so a
69//! minimizer on a seam is polished from every side instead of being
70//! accepted as a clamped one-sided local minimum. Strict-improvement
71//! acceptance rules out non-improving walk cycles; hop and seed caps
72//! are backstops only.
73//!
74//! *Feature lines* -- open boundaries and persistent sharp creases --
75//! get special treatment: derivatives evaluated exactly on such a
76//! line are depth-cap-degraded (the s2 module docs), so a run pinned
77//! against one minimizes along the edge on positions only (a coarse
78//! scan plus dyadic resolution doubling), continuing through endpoint
79//! corners via the fan seeds; Gauss-Newton never iterates on degraded
80//! on-line gradients.
81//!
82//! Because every evaluated point (all seeds and every accepted
83//! iterate) updates the running best, the returned point is never
84//! farther than the best brute-force candidate the search encountered.
85//!
86//! Near the medial axis the box pruning necessarily degrades (many
87//! quads tie), so per-query dedup keeps the candidate cost flat:
88//! corner starts run once per corner *vertex* (they are shared surface
89//! points; skipped repeats are still evaluated and offered), a vertex
90//! fan expands once, and a feature edge is slid along once.
91//!
92//! # Convergence tolerances
93//!
94//! A run stops when no halved step improves, when it exhausts
95//! [`MAX_NEWTON_ITERATIONS`](self), or when an accepted step moves the
96//! foot point by less than `1e-6` of the root box diagonal -- positions
97//! are f32, so ~`1e-7` relative is the noise floor and the stop
98//! criterion sits one decade above it. `(u, v)` quantizes to f32 at
99//! evaluation, matching the s1/s2 interfaces.
100//!
101//! # Sign (design section 4-5): angle-weighted pseudonormal
102//!
103//! `signed_distance` is `Some` exactly when the refined topology is
104//! closed (no boundary edge); open meshes keep the documented-unsigned
105//! status quo. The sign tests `query - position` against:
106//!
107//! - **quad interior**: the sector-correct limit normal `du x dv`.
108//! - **on a seam edge** (within `1e-4` in parameter): the two incident
109//!   quads' unit normals, equal-weighted. Across a smooth seam they
110//!   agree; across an infinitely sharp crease this is the classic
111//!   two-face edge pseudonormal.
112//! - **on a corner**: the corner vertex's full face fan, each face's
113//!   sector normal weighted by its wedge angle (the angle between the
114//!   one-sided derivatives along the quad's two corner edges) -- s2's
115//!   corner snap supplies per-sector tangents at persistent features.
116//!
117//! Cone-point caveat: at a multi-sector pinned vertex the limit has no
118//! convergent normal, so the fan normals entering the pseudonormal are
119//! s2's deterministic per-sector tangent planes -- the sign is
120//! deterministic but the pseudonormal is a fan aggregate, not a limit
121//! of surface normals. Should the aggregate degenerate outright
122//! (antipodal fan normals), the reported normal is zero and the sign
123//! falls back to positive.
124
125use std::cmp::Ordering;
126use std::collections::{BTreeSet, BinaryHeap};
127
128use crate::limit_eval::persistent_sharp_edge;
129use crate::{KernelError, LimitEvaluator};
130
131/// Faces per BVH leaf.
132const LEAF_SIZE: usize = 4;
133/// Gauss-Newton iterations per run (one seed, transfers included).
134const MAX_NEWTON_ITERATIONS: usize = 32;
135/// Seam transfers per run.
136const MAX_SEAM_HOPS: usize = 8;
137/// Seeds per candidate quad (five starts plus walk/fan seeds).
138const MAX_SEEDS: usize = 24;
139/// Step halvings per line search.
140const MAX_HALVINGS: usize = 12;
141/// Converged-step threshold relative to the root box diagonal.
142const STEP_TOLERANCE_REL: f64 = 1e-6;
143/// In-parameter snap classifying a foot point as on-edge/on-corner
144/// for the pseudonormal (an interior minimizer this close to a seam
145/// is normal-indistinguishable from the seam at f32 resolution).
146const EDGE_SNAP: f64 = 1e-4;
147
148/// In-quad `(u, v)` of CSR corner `k`, the `PatchTable` convention.
149const CORNER_UV: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
150
151/// One f64 limit sample: `(position, dp/du, dp/dv)`.
152type SampleF64 = ([f64; 3], [f64; 3], [f64; 3]);
153
154/// An accepted line-search step:
155/// `(raw uv, clamped uv, position, dp/du, dp/dv, dist2)`.
156type AcceptedStep = ([f64; 2], [f64; 2], [f64; 3], [f64; 3], [f64; 3], f64);
157
158/// Result of [`LimitEvaluator::closest_point`].
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct ClosestPoint {
161    /// The foot point on the limit surface.
162    pub position: [f32; 3],
163    /// The refined quad of the closest point.
164    pub face: u32,
165    /// In-quad `(u, v)` of the closest point
166    /// (`position == eval(face, uv)`).
167    pub uv: [f32; 2],
168    /// Euclidean distance from the query to `position`.
169    pub distance: f32,
170    /// Signed variant of `distance` when sign is available (closed
171    /// surface), negative inside (the anti-winding-normal side).
172    pub signed_distance: Option<f32>,
173    /// The limit normal at the closest point (sector-correct at
174    /// creases): the surface normal in quad interiors, the
175    /// angle-weighted pseudonormal on seam edges/corners. Unit length,
176    /// or zero if it degenerates (see the module docs).
177    pub normal: [f32; 3],
178}
179
180// -- The acceleration index --------------------------------------------------
181
182/// Conservative axis-aligned box (f32 bounds are exact min/max of f32
183/// points; distances are computed in f64).
184#[derive(Debug, Clone, Copy)]
185struct Aabb {
186    min: [f32; 3],
187    max: [f32; 3],
188}
189
190impl Aabb {
191    const EMPTY: Self = Aabb {
192        min: [f32::INFINITY; 3],
193        max: [f32::NEG_INFINITY; 3],
194    };
195
196    fn add(&mut self, p: [f32; 3]) {
197        for (c, &coord) in p.iter().enumerate() {
198            self.min[c] = self.min[c].min(coord);
199            self.max[c] = self.max[c].max(coord);
200        }
201    }
202
203    fn union(self, other: Self) -> Self {
204        Aabb {
205            min: [
206                self.min[0].min(other.min[0]),
207                self.min[1].min(other.min[1]),
208                self.min[2].min(other.min[2]),
209            ],
210            max: [
211                self.max[0].max(other.max[0]),
212                self.max[1].max(other.max[1]),
213                self.max[2].max(other.max[2]),
214            ],
215        }
216    }
217
218    fn centroid(&self) -> [f64; 3] {
219        [
220            (self.min[0] as f64 + self.max[0] as f64) * 0.5,
221            (self.min[1] as f64 + self.max[1] as f64) * 0.5,
222            (self.min[2] as f64 + self.max[2] as f64) * 0.5,
223        ]
224    }
225
226    /// Squared distance from `q` to the box (zero inside) -- a lower
227    /// bound on the squared distance to anything the box bounds.
228    fn distance2(&self, q: [f64; 3]) -> f64 {
229        (0..3)
230            .map(|c| {
231                let t = q[c].clamp(self.min[c] as f64, self.max[c] as f64) - q[c];
232                t * t
233            })
234            .sum()
235    }
236
237    fn diagonal(&self) -> f64 {
238        (0..3)
239            .map(|c| ((self.max[c] - self.min[c]) as f64).powi(2))
240            .sum::<f64>()
241            .sqrt()
242    }
243}
244
245enum NodeKind {
246    Internal {
247        left: u32,
248        right: u32,
249    },
250    /// Faces `order[start..start + len]`.
251    Leaf {
252        start: u32,
253        len: u32,
254    },
255}
256
257struct Node {
258    aabb: Aabb,
259    kind: NodeKind,
260}
261
262/// The per-evaluator closest-point index: a BVH over conservative
263/// per-refined-quad boxes, plus the closed-surface flag and the
264/// tolerance scale. Built once by [`LimitEvaluator::closest_point`]
265/// and cached on the evaluator.
266pub(crate) struct SearchIndex {
267    nodes: Vec<Node>,
268    root: u32,
269    /// Refined-face indices permuted into leaf order.
270    order: Vec<u32>,
271    /// Conservative box per refined face (indexed by face).
272    boxes: Vec<Aabb>,
273    /// No boundary edge at the refined level: sign is available.
274    closed: bool,
275    /// Root box diagonal -- the physical tolerance scale.
276    diag: f64,
277}
278
279fn build_index(evaluator: &LimitEvaluator) -> Result<SearchIndex, KernelError> {
280    let mesh = &evaluator.result.topology;
281    let adjacency = &evaluator.result.adjacency;
282    let face_count = mesh.face_vertex_counts.len();
283    if face_count == 0 {
284        return Err(KernelError::InvalidTopology(
285            "refined level has no faces to run closest-point queries against",
286        ));
287    }
288    let boxes: Vec<Aabb> = (0..face_count as u32)
289        .map(|face| face_box(evaluator, face))
290        .collect();
291    let centroids: Vec<[f64; 3]> = boxes.iter().map(Aabb::centroid).collect();
292    let mut faces: Vec<u32> = (0..face_count as u32).collect();
293    let mut nodes = Vec::new();
294    let mut order = Vec::with_capacity(face_count);
295    let root = build_node(&mut faces, &boxes, &centroids, &mut nodes, &mut order);
296    let diag = nodes[root as usize].aabb.diagonal();
297    Ok(SearchIndex {
298        nodes,
299        root,
300        order,
301        boxes,
302        closed: !adjacency.edge_is_boundary.iter().any(|&b| b),
303        diag,
304    })
305}
306
307/// Conservative box of one refined quad's limit patch; see the module
308/// docs for why each variant bounds the limit.
309fn face_box(evaluator: &LimitEvaluator, face: u32) -> Aabb {
310    let mut aabb = Aabb::EMPTY;
311    match evaluator.table.face_patch(face) {
312        Some(patch) => {
313            for &cp in &evaluator.table.control_points[patch as usize] {
314                aabb.add(evaluator.positions[cp as usize]);
315            }
316        }
317        None => {
318            let mesh = &evaluator.result.topology;
319            let adjacency = &evaluator.result.adjacency;
320            let off = (face * 4) as usize;
321            let support: BTreeSet<u32> = mesh.face_vertex_indices[off..off + 4]
322                .iter()
323                .flat_map(|&corner| {
324                    let start = adjacency.vertex_face_offsets[corner as usize] as usize;
325                    let end = adjacency.vertex_face_offsets[corner as usize + 1] as usize;
326                    adjacency.vertex_faces[start..end].iter().copied()
327                })
328                .collect();
329            for f in support {
330                for &v in &mesh.face_vertex_indices[(f * 4) as usize..(f * 4) as usize + 4] {
331                    aabb.add(evaluator.positions[v as usize]);
332                }
333            }
334        }
335    }
336    aabb
337}
338
339/// Median-split build over box centroids; returns the node index.
340fn build_node(
341    faces: &mut [u32],
342    boxes: &[Aabb],
343    centroids: &[[f64; 3]],
344    nodes: &mut Vec<Node>,
345    order: &mut Vec<u32>,
346) -> u32 {
347    let aabb = faces
348        .iter()
349        .fold(Aabb::EMPTY, |acc, &f| acc.union(boxes[f as usize]));
350    let kind = if faces.len() <= LEAF_SIZE {
351        let start = order.len() as u32;
352        order.extend_from_slice(faces);
353        NodeKind::Leaf {
354            start,
355            len: faces.len() as u32,
356        }
357    } else {
358        // Split the centroid bounds on their widest axis.
359        let (lo, hi) = faces.iter().fold(
360            ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]),
361            |(mut lo, mut hi), &f| {
362                for c in 0..3 {
363                    lo[c] = lo[c].min(centroids[f as usize][c]);
364                    hi[c] = hi[c].max(centroids[f as usize][c]);
365                }
366                (lo, hi)
367            },
368        );
369        let axis = (0..3).fold(0, |best, c| {
370            if hi[c] - lo[c] > hi[best] - lo[best] {
371                c
372            } else {
373                best
374            }
375        });
376        let mid = faces.len() / 2;
377        faces.select_nth_unstable_by(mid, |&a, &b| {
378            centroids[a as usize][axis].total_cmp(&centroids[b as usize][axis])
379        });
380        let (left_faces, right_faces) = faces.split_at_mut(mid);
381        let left = build_node(left_faces, boxes, centroids, nodes, order);
382        let right = build_node(right_faces, boxes, centroids, nodes, order);
383        NodeKind::Internal { left, right }
384    };
385    nodes.push(Node { aabb, kind });
386    (nodes.len() - 1) as u32
387}
388
389/// Min-heap entry: smallest box distance pops first.
390#[derive(Clone, Copy)]
391struct HeapEntry {
392    d2: f64,
393    node: u32,
394}
395
396impl PartialEq for HeapEntry {
397    fn eq(&self, other: &Self) -> bool {
398        self.cmp(other) == Ordering::Equal
399    }
400}
401
402impl Eq for HeapEntry {}
403
404impl PartialOrd for HeapEntry {
405    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
406        Some(self.cmp(other))
407    }
408}
409
410impl Ord for HeapEntry {
411    fn cmp(&self, other: &Self) -> Ordering {
412        // Reversed: `BinaryHeap` is a max-heap, we pop the nearest box.
413        other
414            .d2
415            .total_cmp(&self.d2)
416            .then_with(|| other.node.cmp(&self.node))
417    }
418}
419
420// -- The query ----------------------------------------------------------------
421
422/// Running best foot point.
423struct Best {
424    dist2: f64,
425    face: u32,
426    uv: [f64; 2],
427    position: [f64; 3],
428}
429
430impl LimitEvaluator<'_> {
431    /// Closest point on the limit surface to `query`, signed on closed
432    /// surfaces. See the module docs of `closest_point.rs` for the
433    /// search, walk, tolerance, and sign semantics. The acceleration
434    /// index is built on the first call and reused afterwards.
435    pub fn closest_point(&self, query: [f32; 3]) -> Result<ClosestPoint, KernelError> {
436        let index = self.search_index()?;
437        let q = v3(query);
438        let mut minimizer = Minimizer {
439            evaluator: self,
440            q,
441            tolerance: STEP_TOLERANCE_REL * index.diag,
442            best: Best {
443                dist2: f64::INFINITY,
444                face: 0,
445                uv: [0.0; 2],
446                position: [0.0; 3],
447            },
448            seeds: Vec::new(),
449            seeded: Vec::new(),
450            corner_runs: Vec::new(),
451            fanned: Vec::new(),
452            polished: Vec::new(),
453        };
454        let mut heap = BinaryHeap::new();
455        heap.push(HeapEntry {
456            d2: index.nodes[index.root as usize].aabb.distance2(q),
457            node: index.root,
458        });
459        while let Some(entry) = heap.pop() {
460            if entry.d2 >= minimizer.best.dist2 {
461                // Min-heap order: every remaining box is farther still.
462                break;
463            }
464            match index.nodes[entry.node as usize].kind {
465                NodeKind::Internal { left, right } => {
466                    for child in [left, right] {
467                        let d2 = index.nodes[child as usize].aabb.distance2(q);
468                        if d2 < minimizer.best.dist2 {
469                            heap.push(HeapEntry { d2, node: child });
470                        }
471                    }
472                }
473                NodeKind::Leaf { start, len } => {
474                    for &face in &index.order[start as usize..(start + len) as usize] {
475                        if index.boxes[face as usize].distance2(q) < minimizer.best.dist2 {
476                            minimizer.search_quad(face)?;
477                        }
478                    }
479                }
480            }
481        }
482        self.finish(q, index, minimizer.best)
483    }
484
485    /// The cached index, built on first use.
486    fn search_index(&self) -> Result<&SearchIndex, KernelError> {
487        if self.search.get().is_none() {
488            // Not Sync, so no concurrent set; an existing value is
489            // impossible on this path.
490            let _ = self.search.set(build_index(self)?);
491        }
492        // SAFETY: populated just above when it was empty.
493        Ok(self.search.get().expect("search index populated"))
494    }
495
496    /// f64 view of [`eval_with_derivatives`](Self::eval_with_derivatives).
497    fn sample(&self, face: u32, uv: [f64; 2]) -> Result<SampleF64, KernelError> {
498        let (p, du, dv) = self.eval_with_derivatives(face, [uv[0] as f32, uv[1] as f32])?;
499        Ok((v3(p), v3(du), v3(dv)))
500    }
501
502    /// Whether the edge at `slot` of `face` is a *feature line* --
503    /// open boundary or persistent sharp crease -- where exactly-on-
504    /// line derivatives are depth-cap-degraded (the s2 module docs):
505    /// the walk minimizes along such an edge on positions only
506    /// instead of Gauss-Newton-ing on it.
507    fn feature_line(&self, face: u32, slot: usize) -> bool {
508        let adjacency = &self.result.adjacency;
509        let edge = adjacency.face_edges[face as usize * 4 + slot] as usize;
510        adjacency.edge_is_boundary[edge]
511            || persistent_sharp_edge(
512                self.result.topology.edge_creases[edge],
513                &self.result.options,
514            )
515    }
516
517    /// The neighbor across edge slot `slot` of `face`, with the
518    /// in-edge parameter `x` remapped into the neighbor's frame
519    /// (winding-consistent meshes traverse a shared edge oppositely).
520    /// `None` on boundary edges or inconsistent adjacency.
521    fn across_edge(&self, face: u32, slot: usize, x: f64) -> Option<(u32, [f64; 2])> {
522        let adjacency = &self.result.adjacency;
523        let edge = adjacency.face_edges[face as usize * 4 + slot];
524        let [fa, fb] = adjacency.edge_faces[edge as usize];
525        let neighbor = if fa == face { fb } else { fa };
526        (neighbor != u32::MAX)
527            .then(|| {
528                adjacency.face_edges[neighbor as usize * 4..neighbor as usize * 4 + 4]
529                    .iter()
530                    .position(|&e| e == edge)
531                    .map(|t| (neighbor, edge_uv(t, 1.0 - x)))
532            })
533            .flatten()
534    }
535
536    /// Package the best foot point: pseudonormal, sign, f32 narrowing.
537    fn finish(
538        &self,
539        q: [f64; 3],
540        index: &SearchIndex,
541        best: Best,
542    ) -> Result<ClosestPoint, KernelError> {
543        let distance = best.dist2.sqrt() as f32;
544        let pseudo = self.pseudonormal(best.face, best.uv)?;
545        let len = length(pseudo);
546        let normal = if len > 1e-12 {
547            scale(pseudo, 1.0 / len)
548        } else {
549            [0.0; 3]
550        };
551        let signed_distance = index.closed.then(|| {
552            if dot(sub(q, best.position), normal) < 0.0 {
553                -distance
554            } else {
555                distance
556            }
557        });
558        Ok(ClosestPoint {
559            position: [
560                best.position[0] as f32,
561                best.position[1] as f32,
562                best.position[2] as f32,
563            ],
564            face: best.face,
565            uv: [best.uv[0] as f32, best.uv[1] as f32],
566            distance,
567            signed_distance,
568            normal: [normal[0] as f32, normal[1] as f32, normal[2] as f32],
569        })
570    }
571
572    /// Unnormalized pseudonormal at a foot point, classified by the
573    /// [`EDGE_SNAP`] parameter snap; see the module docs.
574    fn pseudonormal(&self, face: u32, uv: [f64; 2]) -> Result<[f64; 3], KernelError> {
575        let pin = |t: f64| {
576            if t <= EDGE_SNAP {
577                Some(false)
578            } else if t >= 1.0 - EDGE_SNAP {
579                Some(true)
580            } else {
581                None
582            }
583        };
584        match (pin(uv[0]), pin(uv[1])) {
585            (None, None) => self.face_normal(face, uv),
586            (Some(u_hi), Some(v_hi)) => {
587                let corner = match (u_hi, v_hi) {
588                    (false, false) => 0,
589                    (true, false) => 1,
590                    (true, true) => 2,
591                    (false, true) => 3,
592                };
593                self.corner_pseudonormal(face, corner)
594            }
595            (u_pin, v_pin) => {
596                // Exactly one coordinate pinned: an on-edge foot point.
597                let (slot, x) = match (u_pin, v_pin) {
598                    (None, Some(false)) => (0, uv[0]),
599                    (Some(true), None) => (1, uv[1]),
600                    (None, Some(true)) => (2, 1.0 - uv[0]),
601                    _ => (3, 1.0 - uv[1]),
602                };
603                let own = self.face_normal(face, edge_uv(slot, x))?;
604                let other = match self.across_edge(face, slot, x) {
605                    Some((neighbor, neighbor_uv)) => self.face_normal(neighbor, neighbor_uv)?,
606                    None => [0.0; 3],
607                };
608                Ok(add(own, other))
609            }
610        }
611    }
612
613    /// Unit surface normal `du x dv`, or zero where it degenerates.
614    fn face_normal(&self, face: u32, uv: [f64; 2]) -> Result<[f64; 3], KernelError> {
615        let (_, du, dv) = self.sample(face, uv)?;
616        Ok(normalize_or_zero(cross(du, dv)))
617    }
618
619    /// Angle-weighted pseudonormal over the full face fan of the quad
620    /// corner's vertex: per incident face, the sector normal weighted
621    /// by the wedge angle between the one-sided derivatives along the
622    /// quad's two corner edges.
623    fn corner_pseudonormal(&self, face: u32, corner: usize) -> Result<[f64; 3], KernelError> {
624        let mesh = &self.result.topology;
625        let adjacency = &self.result.adjacency;
626        let vi = mesh.face_vertex_indices[face as usize * 4 + corner];
627        let start = adjacency.vertex_face_offsets[vi as usize] as usize;
628        let end = adjacency.vertex_face_offsets[vi as usize + 1] as usize;
629        let mut acc = [0.0; 3];
630        for &fan_face in &adjacency.vertex_faces[start..end] {
631            let off = fan_face as usize * 4;
632            let k = mesh.face_vertex_indices[off..off + 4]
633                .iter()
634                .position(|&c| c == vi)
635                .ok_or(KernelError::InvalidTopology(
636                    "vertex fan face does not contain the fan vertex",
637                ))?;
638            let (_, du, dv) = self.sample(fan_face, CORNER_UV[k])?;
639            // Wedge directions toward the next/previous CSR corners.
640            let (toward_next, toward_prev) = match k {
641                0 => (du, dv),
642                1 => (dv, neg(du)),
643                2 => (neg(du), neg(dv)),
644                _ => (neg(dv), du),
645            };
646            let normal = normalize_or_zero(cross(du, dv));
647            acc = add(acc, scale(normal, wedge_angle(toward_next, toward_prev)));
648        }
649        Ok(acc)
650    }
651}
652
653/// Per-query Gauss-Newton state: the running best plus the seed queue
654/// of the candidate quad being searched.
655struct Minimizer<'e, 'a> {
656    evaluator: &'e LimitEvaluator<'a>,
657    q: [f64; 3],
658    /// Physical converged-step threshold.
659    tolerance: f64,
660    best: Best,
661    /// Pending `(face, uv)` seeds of the current candidate quad.
662    seeds: Vec<(u32, [f64; 2])>,
663    /// `(face, uv bits)` starts already seeded for the current
664    /// candidate quad -- keyed by the exact start, so a fan seed at a
665    /// different corner of an already-seeded face still runs.
666    seeded: Vec<(u32, [u64; 2])>,
667    /// Corner vertices already Newton-run this query. Corner seeds are
668    /// shared surface points between incident quads; near the medial
669    /// axis (where box pruning cannot cut candidates) running each one
670    /// once instead of once per quad is the difference between ~2 and
671    /// 5 runs per candidate. Skipped seeds are still evaluated and
672    /// offered, so the brute-candidate guarantee is untouched.
673    corner_runs: Vec<u32>,
674    /// Corner vertices whose full fan was already seeded this query
675    /// (the fan starts are identical wherever they are triggered from).
676    fanned: Vec<u32>,
677    /// Feature edges (by refined edge index) already slid along this
678    /// query: every seed converging onto the same boundary or crease
679    /// edge -- from either side -- shares one polish.
680    polished: Vec<u32>,
681}
682
683impl Minimizer<'_, '_> {
684    /// Multi-start search of one candidate quad: center + corners,
685    /// plus whatever walk/fan seeds the runs enqueue. Corner starts
686    /// whose vertex already ran this query are evaluated and offered
687    /// but not re-run (see [`corner_runs`](Self::corner_runs)).
688    fn search_quad(&mut self, face: u32) -> Result<(), KernelError> {
689        self.seeds.clear();
690        self.seeded.clear();
691        self.enqueue(face, [0.5, 0.5]);
692        let off = face as usize * 4;
693        for (k, &uv) in CORNER_UV.iter().enumerate() {
694            let vertex = self.evaluator.result.topology.face_vertex_indices[off + k];
695            if self.corner_runs.contains(&vertex) {
696                let (p, _, _) = self.evaluator.sample(face, uv)?;
697                let dist2 = norm2(sub(p, self.q));
698                self.offer(face, uv, p, dist2);
699            } else {
700                self.corner_runs.push(vertex);
701                self.enqueue(face, uv);
702            }
703        }
704        let mut next = 0;
705        while next < self.seeds.len() {
706            let (seed_face, seed_uv) = self.seeds[next];
707            next += 1;
708            self.run(seed_face, seed_uv)?;
709        }
710        Ok(())
711    }
712
713    fn offer(&mut self, face: u32, uv: [f64; 2], position: [f64; 3], dist2: f64) {
714        if dist2 < self.best.dist2 {
715            self.best = Best {
716                dist2,
717                face,
718                uv,
719                position,
720            };
721        }
722    }
723
724    /// One damped Gauss-Newton run with in-run seam transfers; pinned
725    /// convergence seeds the across-seam neighbors.
726    fn run(&mut self, face: u32, uv: [f64; 2]) -> Result<(), KernelError> {
727        let (mut face, mut uv) = (face, uv);
728        let (mut p, mut du, mut dv) = self.evaluator.sample(face, uv)?;
729        let mut dist2 = norm2(sub(p, self.q));
730        self.offer(face, uv, p, dist2);
731        let mut hops = 0;
732        for _ in 0..MAX_NEWTON_ITERATIONS {
733            let d = sub(p, self.q);
734            let full = gauss_newton_step(d, du, dv);
735            // The full step first; when it fails against a pinned
736            // bound, the reduced active-set step (1D Newton along the
737            // free coordinate) -- the clamped full step would only
738            // creep linearly along an edge minimizer.
739            let mut accepted = self.line_search(face, uv, full, dist2)?;
740            if accepted.is_none()
741                && let Some(reduced) = reduced_step(uv, full, d, du, dv)
742            {
743                accepted = self.line_search(face, uv, reduced, dist2)?;
744            }
745            let Some((raw, cand, cp, cdu, cdv, cd2)) = accepted else {
746                // Local (possibly constrained) minimum: slide along
747                // any pinned feature edge (Gauss-Newton cannot -- see
748                // `polish_feature_edge`) and polish any pinned seam
749                // from the other side too.
750                self.polish_pinned_feature_edges(face, uv)?;
751                self.enqueue_pinned(face, uv);
752                break;
753            };
754            let moved2 = norm2(sub(cp, p));
755            (uv, p, du, dv, dist2) = (cand, cp, cdu, cdv, cd2);
756            self.offer(face, uv, p, dist2);
757            if moved2 <= self.tolerance * self.tolerance {
758                self.polish_pinned_feature_edges(face, uv)?;
759                self.enqueue_pinned(face, uv);
760                break;
761            }
762            // Transfer across the seam when the step was clamped at an
763            // edge; strict improvement above rules out ping-pong.
764            let pin_u = pinned_crossing(uv[0], raw[0]);
765            let pin_v = pinned_crossing(uv[1], raw[1]);
766            match (pin_u, pin_v) {
767                (Some(_), Some(_)) => {
768                    self.enqueue_corner_fan(face, uv);
769                    break;
770                }
771                (Some(side), None) | (None, Some(side)) => {
772                    let (slot, x) = if pin_u.is_some() {
773                        if side { (1, uv[1]) } else { (3, 1.0 - uv[1]) }
774                    } else if side {
775                        (2, 1.0 - uv[0])
776                    } else {
777                        (0, uv[0])
778                    };
779                    if self.evaluator.feature_line(face, slot) {
780                        // Boundary or sharp crease: Gauss-Newton on
781                        // the line is derivative-degraded -- slide on
782                        // positions only. No across-seed: a foot
783                        // beyond the crease lives in the neighbor's
784                        // interior, which stays an unpruned BVH
785                        // candidate of its own, and an on-line seed
786                        // would only grind deep snapped evals.
787                        self.polish_feature_edge(face, slot)?;
788                        break;
789                    }
790                    hops += 1;
791                    if hops > MAX_SEAM_HOPS {
792                        break;
793                    }
794                    match self.evaluator.across_edge(face, slot, x) {
795                        Some((neighbor, neighbor_uv)) => {
796                            (face, uv) = (neighbor, neighbor_uv);
797                            (p, du, dv) = self.evaluator.sample(face, uv)?;
798                            dist2 = norm2(sub(p, self.q));
799                            self.offer(face, uv, p, dist2);
800                        }
801                        // Unreachable for manifold seams; stand pat.
802                        None => break,
803                    }
804                }
805                (None, None) => {}
806            }
807        }
808        Ok(())
809    }
810
811    /// [`polish_feature_edge`](Self::polish_feature_edge) for every
812    /// feature edge the converged point pins to (both incident edges
813    /// at a pinned corner).
814    fn polish_pinned_feature_edges(&mut self, face: u32, uv: [f64; 2]) -> Result<(), KernelError> {
815        let pin = |t: f64| (t == 0.0).then_some(false).or((t == 1.0).then_some(true));
816        let slots: &[usize] = match (pin(uv[0]), pin(uv[1])) {
817            (Some(false), Some(false)) => &[3, 0],
818            (Some(true), Some(false)) => &[0, 1],
819            (Some(true), Some(true)) => &[1, 2],
820            (Some(false), Some(true)) => &[2, 3],
821            (Some(false), None) => &[3],
822            (Some(true), None) => &[1],
823            (None, Some(false)) => &[0],
824            (None, Some(true)) => &[2],
825            (None, None) => &[],
826        };
827        for &slot in slots {
828            if self.evaluator.feature_line(face, slot) {
829                self.polish_feature_edge(face, slot)?;
830            }
831        }
832        Ok(())
833    }
834
835    /// Constrained 1D minimization along feature edge `slot` of
836    /// `face`, on positions only: *derivatives* exactly on a feature
837    /// line are depth-cap-degraded (the s2 module docs), so
838    /// Gauss-Newton cannot slide along a boundary or sharp-crease
839    /// minimizer. A coarse scan plus resolution-doubling descent on
840    /// *dyadic* parameters replaces it -- a dyadic `x` with `k` bits
841    /// snaps as an exact depth-`k` corner in the s2 isolation, so the
842    /// probes stay shallow and share ancestor nodes, where arbitrary
843    /// `x` would build a fresh chain to the depth cap each. An
844    /// endpoint minimizer seeds the corner fan so the slide continues
845    /// into the next quad along the feature.
846    fn polish_feature_edge(&mut self, face: u32, slot: usize) -> Result<(), KernelError> {
847        let edge = self.evaluator.result.adjacency.face_edges[face as usize * 4 + slot];
848        if self.polished.contains(&edge) {
849            return Ok(());
850        }
851        self.polished.push(edge);
852        // Depth-3 scan (the distance along one feature segment need
853        // not be unimodal over [0, 1])...
854        let mut best = (f64::INFINITY, 0.0f64);
855        let mut step = 1.0 / 8.0;
856        for k in 0..=8 {
857            let x = k as f64 * step;
858            let d2 = self.edge_distance2(face, slot, x)?;
859            if d2 < best.0 {
860                best = (d2, x);
861            }
862        }
863        // ...then double the dyadic resolution around the running
864        // argmin down to 2^-18 of the edge (well under the foot-point
865        // tolerances; deeper would out-resolve f32 positions anyway).
866        for _ in 0..15 {
867            step *= 0.5;
868            for x in [best.1 - step, best.1 + step] {
869                if (0.0..=1.0).contains(&x) {
870                    let d2 = self.edge_distance2(face, slot, x)?;
871                    if d2 < best.0 {
872                        best = (d2, x);
873                    }
874                }
875            }
876        }
877        if best.1 <= 1e-3 {
878            self.enqueue_corner_fan(face, edge_uv(slot, 0.0));
879        } else if best.1 >= 1.0 - 1e-3 {
880            self.enqueue_corner_fan(face, edge_uv(slot, 1.0));
881        }
882        Ok(())
883    }
884
885    /// One on-line probe of [`polish_feature_edge`]: squared distance
886    /// at parameter `x` along edge `slot`, offered to the running best.
887    fn edge_distance2(&mut self, face: u32, slot: usize, x: f64) -> Result<f64, KernelError> {
888        let uv = edge_uv(slot, x);
889        let (p, _, _) = self.evaluator.sample(face, uv)?;
890        let d2 = norm2(sub(p, self.q));
891        self.offer(face, uv, p, d2);
892        Ok(d2)
893    }
894
895    /// Clamped backtracking line search: the first halving that
896    /// strictly improves, as `(raw, clamped, position, du, dv, dist2)`.
897    fn line_search(
898        &self,
899        face: u32,
900        uv: [f64; 2],
901        step: [f64; 2],
902        dist2: f64,
903    ) -> Result<Option<AcceptedStep>, KernelError> {
904        let mut alpha = 1.0;
905        for _ in 0..MAX_HALVINGS {
906            let raw = [uv[0] + alpha * step[0], uv[1] + alpha * step[1]];
907            let cand = [raw[0].clamp(0.0, 1.0), raw[1].clamp(0.0, 1.0)];
908            if cand != uv {
909                let (cp, cdu, cdv) = self.evaluator.sample(face, cand)?;
910                let cd2 = norm2(sub(cp, self.q));
911                if cd2 < dist2 {
912                    return Ok(Some((raw, cand, cp, cdu, cdv, cd2)));
913                }
914            }
915            alpha *= 0.5;
916        }
917        Ok(None)
918    }
919
920    /// Seed the neighbors of an exactly pinned converged point: the
921    /// whole vertex fan at a corner, the across-edge quad on a smooth
922    /// seam (feature lines are polished instead -- an on-line seed
923    /// would start on degraded derivatives).
924    fn enqueue_pinned(&mut self, face: u32, uv: [f64; 2]) {
925        let pin = |t: f64| (t == 0.0).then_some(false).or((t == 1.0).then_some(true));
926        match (pin(uv[0]), pin(uv[1])) {
927            (Some(_), Some(_)) => self.enqueue_corner_fan(face, uv),
928            (u_pin @ Some(side), None) | (u_pin @ None, Some(side)) => {
929                let (slot, x) = if u_pin.is_some() {
930                    if side { (1, uv[1]) } else { (3, 1.0 - uv[1]) }
931                } else if side {
932                    (2, 1.0 - uv[0])
933                } else {
934                    (0, uv[0])
935                };
936                if !self.evaluator.feature_line(face, slot)
937                    && let Some((neighbor, neighbor_uv)) = self.evaluator.across_edge(face, slot, x)
938                {
939                    self.enqueue(neighbor, neighbor_uv);
940                }
941            }
942            (None, None) => {}
943        }
944    }
945
946    /// Seed every face around the corner vertex at its own corner
947    /// parameter (the corner walk: the true minimizer may live in any
948    /// fan face, including across the diagonal). Each vertex fans at
949    /// most once per query -- the starts are identical wherever the
950    /// fan is triggered from.
951    fn enqueue_corner_fan(&mut self, face: u32, uv: [f64; 2]) {
952        let corner = match (uv[0] == 1.0, uv[1] == 1.0) {
953            (false, false) => 0,
954            (true, false) => 1,
955            (true, true) => 2,
956            (false, true) => 3,
957        };
958        let mesh = &self.evaluator.result.topology;
959        let adjacency = &self.evaluator.result.adjacency;
960        let vi = mesh.face_vertex_indices[face as usize * 4 + corner];
961        if self.fanned.contains(&vi) {
962            return;
963        }
964        self.fanned.push(vi);
965        let start = adjacency.vertex_face_offsets[vi as usize] as usize;
966        let end = adjacency.vertex_face_offsets[vi as usize + 1] as usize;
967        for &fan_face in &adjacency.vertex_faces[start..end] {
968            let off = fan_face as usize * 4;
969            if let Some(k) = mesh.face_vertex_indices[off..off + 4]
970                .iter()
971                .position(|&c| c == vi)
972            {
973                self.enqueue(fan_face, CORNER_UV[k]);
974            }
975        }
976    }
977
978    fn enqueue(&mut self, face: u32, uv: [f64; 2]) {
979        let key = (face, [uv[0].to_bits(), uv[1].to_bits()]);
980        if self.seeds.len() < MAX_SEEDS && !self.seeded.contains(&key) {
981            self.seeded.push(key);
982            self.seeds.push((face, uv));
983        }
984    }
985}
986
987/// Gauss-Newton step for `|S - q|^2/2` from the first-derivative
988/// normal equations; falls back to scaled steepest descent when the
989/// tangent frame degenerates (sector-plane corner tangents,
990/// crease-crease corners) -- the line search safeguards either way.
991fn gauss_newton_step(d: [f64; 3], du: [f64; 3], dv: [f64; 3]) -> [f64; 2] {
992    let (a, b, c) = (dot(du, du), dot(dv, dv), dot(du, dv));
993    let (gu, gv) = (dot(d, du), dot(d, dv));
994    let det = a * b - c * c;
995    if det > 1e-12 * a * b {
996        [(c * gv - b * gu) / det, (c * gu - a * gv) / det]
997    } else {
998        let scale = (a + b).max(1e-30);
999        [-gu / scale, -gv / scale]
1000    }
1001}
1002
1003/// Active-set reduction when the full step pushes outward through
1004/// exactly one pinned bound: 1D Gauss-Newton along the free
1005/// coordinate (quadratic along an edge minimizer where the clamped
1006/// full step would creep linearly). `None` when nothing is pinned
1007/// outward, the frame degenerates, or both bounds pin (a corner --
1008/// the fan seeding owns that case).
1009fn reduced_step(
1010    uv: [f64; 2],
1011    full: [f64; 2],
1012    d: [f64; 3],
1013    du: [f64; 3],
1014    dv: [f64; 3],
1015) -> Option<[f64; 2]> {
1016    let outward = |t: f64, s: f64| (t == 0.0 && s < 0.0) || (t == 1.0 && s > 0.0);
1017    let one_d = |tangent: [f64; 3]| {
1018        let scale = dot(tangent, tangent);
1019        (scale > 1e-30).then(|| -dot(d, tangent) / scale)
1020    };
1021    match (outward(uv[0], full[0]), outward(uv[1], full[1])) {
1022        (true, false) => one_d(dv).map(|s| [0.0, s]),
1023        (false, true) => one_d(du).map(|s| [s, 0.0]),
1024        _ => None,
1025    }
1026}
1027
1028/// Whether a clamped coordinate is pinned at 0/1 with the raw step
1029/// strictly outside (the seam-crossing test).
1030fn pinned_crossing(clamped: f64, raw: f64) -> Option<bool> {
1031    (clamped == 0.0 && raw < 0.0)
1032        .then_some(false)
1033        .or((clamped == 1.0 && raw > 1.0).then_some(true))
1034}
1035
1036/// `(u, v)` at parameter `x` along CSR edge slot `slot` (corner `slot`
1037/// toward corner `slot + 1`).
1038fn edge_uv(slot: usize, x: f64) -> [f64; 2] {
1039    match slot {
1040        0 => [x, 0.0],
1041        1 => [1.0, x],
1042        2 => [1.0 - x, 1.0],
1043        _ => [0.0, 1.0 - x],
1044    }
1045}
1046
1047/// Angle between two wedge tangents; a neutral right angle when either
1048/// degenerates.
1049fn wedge_angle(a: [f64; 3], b: [f64; 3]) -> f64 {
1050    let (la, lb) = (length(a), length(b));
1051    if la > 1e-12 && lb > 1e-12 {
1052        (dot(a, b) / (la * lb)).clamp(-1.0, 1.0).acos()
1053    } else {
1054        std::f64::consts::FRAC_PI_2
1055    }
1056}
1057
1058// -- f64 vector helpers --------------------------------------------------------
1059
1060fn v3(p: [f32; 3]) -> [f64; 3] {
1061    [p[0] as f64, p[1] as f64, p[2] as f64]
1062}
1063
1064fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
1065    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
1066}
1067
1068fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
1069    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
1070}
1071
1072fn neg(a: [f64; 3]) -> [f64; 3] {
1073    [-a[0], -a[1], -a[2]]
1074}
1075
1076fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
1077    [a[0] * s, a[1] * s, a[2] * s]
1078}
1079
1080fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
1081    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
1082}
1083
1084fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
1085    [
1086        a[1] * b[2] - a[2] * b[1],
1087        a[2] * b[0] - a[0] * b[2],
1088        a[0] * b[1] - a[1] * b[0],
1089    ]
1090}
1091
1092fn norm2(a: [f64; 3]) -> f64 {
1093    dot(a, a)
1094}
1095
1096fn length(a: [f64; 3]) -> f64 {
1097    norm2(a).sqrt()
1098}
1099
1100fn normalize_or_zero(a: [f64; 3]) -> [f64; 3] {
1101    let len = length(a);
1102    if len > 1e-12 {
1103        scale(a, 1.0 / len)
1104    } else {
1105        [0.0; 3]
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::{Aabb, edge_uv, pinned_crossing};
1112
1113    /// The geometry gates live in `tests/closest_point.rs`; these unit
1114    /// tests pin the pure helpers.
1115    #[test]
1116    fn aabb_distance_is_zero_inside_and_exact_outside() {
1117        let mut aabb = Aabb::EMPTY;
1118        aabb.add([0.0, 0.0, 0.0]);
1119        aabb.add([2.0, 1.0, 3.0]);
1120        assert_eq!(aabb.distance2([1.0, 0.5, 1.5]), 0.0);
1121        assert_eq!(aabb.distance2([3.0, 0.5, 1.5]), 1.0);
1122        assert_eq!(aabb.distance2([-1.0, -1.0, 4.0]), 3.0);
1123    }
1124
1125    #[test]
1126    fn edge_parameterizations_traverse_csr_corners() {
1127        // Slot `s` runs corner `s` -> corner `s + 1`.
1128        assert_eq!(edge_uv(0, 0.0), [0.0, 0.0]);
1129        assert_eq!(edge_uv(0, 1.0), [1.0, 0.0]);
1130        assert_eq!(edge_uv(1, 0.25), [1.0, 0.25]);
1131        assert_eq!(edge_uv(2, 0.25), [0.75, 1.0]);
1132        assert_eq!(edge_uv(3, 0.25), [0.0, 0.75]);
1133    }
1134
1135    #[test]
1136    fn pinned_crossing_requires_clamp_and_overshoot() {
1137        assert_eq!(pinned_crossing(0.0, -0.5), Some(false));
1138        assert_eq!(pinned_crossing(1.0, 1.5), Some(true));
1139        // Pinned but moving along the edge: no crossing.
1140        assert_eq!(pinned_crossing(0.0, 0.0), None);
1141        assert_eq!(pinned_crossing(0.5, 0.5), None);
1142    }
1143}