Skip to main content

ogeom_mesh/editable/
offset.rs

1//! Offsetting an editable mesh: every face moved along its outward normal
2//! by one distance, the folds that leaves repaired.
3//!
4//! # Placing a vertex
5//!
6//! A face's outward normal is the way its corners wind counter-clockwise.
7//! About each vertex, the faces of its fan are taken in turn, and an edge
8//! between two of them whose normals turn by more than [`OFFSET_EDGE_ANGLE`]
9//! (30°) is an *edge* of the offset. The edges, and the rims, part the fan
10//! into sectors; a closed fan with fewer than two edges is one sector. Each
11//! sector's normal is the mean of its faces' unit normals weighted by the
12//! faces' angles at the vertex: the normals smoothed within the sector and
13//! never across an edge.
14//!
15//! The sectors' normals fix the directions the vertex is held in: the
16//! heaviest sector's normal, then, of the sectors whose normals turn by
17//! more than the edge angle from each normal taken, the one whose normal
18//! has the largest part square to the directions taken, that part's
19//! direction, up to three. Within the directions taken, the
20//! vertex moves by the step `x` that minimises `Σ α (n · x − d)²` over its
21//! faces, `n` a face's unit normal and `α` its angle at the vertex: least
22//! squares weighted by corner angle, so that each face ends as near
23//! `distance` `d` from where it was as the vertex can put it. A vertex on a
24//! flat stretch moves by `d` along the normal, one on an edge between faces
25//! square to each other by `√2 d`, and the corner of a box by `√3 d`, to
26//! where the offset faces' planes meet. No vertex moves farther than
27//! [`OFFSET_FARTHEST`] (3) times `|d|`: a longer step is shortened along
28//! its line, as computed.
29//!
30//! # Folds
31//!
32//! Where the offset faces' planes meet before the faces end, as beside a
33//! convex edge offset inward or a concave one offset outward, the vertices
34//! within reach of the edge overshoot it and their faces fold over. Folds
35//! are repaired in two steps.
36//!
37//! First, overshooting vertices are pushed back. A vertex moved in one or
38//! two directions that ends nearer than `|d|` to the mesh as it was, to a
39//! face it stands in front of, on the side the offset goes, and that turns
40//! by at least the edge angle from the plane or line the vertex may still
41//! slide in, slides in it onto that face's offset plane, and is then held
42//! to that plane too. Faces are looked for about where the vertex was
43//! placed, the nearest first, and again for a second push along the line
44//! the first leaves. A slide that would take the vertex farther than the
45//! cap from where it started is not made. A strip that overshot an edge
46//! so lands on the edge, and a vertex in reach of two edges on the corner
47//! where they meet. Faces of one smooth stretch never push each other, so
48//! the residuals least squares leaves on a curved surface stay; nor do
49//! faces the vertex stands behind, so the offset of a piece thinner than
50//! twice the distance, whose sides pass through each other, is not
51//! repaired. These vertices are counted in [`OffsetReport::eased`].
52//!
53//! Then folded faces are collapsed. A face is *folded* when it turns from
54//! the way it faced before the offset by more than the edge angle, over
55//! included, or is a *sliver*, its height within the rounding of its
56//! corners' coordinates; a face that was a sliver before is passed over.
57//! Each folded face, lowest-numbered first, has an edge collapsed: one of
58//! its corners merged into another, which keeps its position and takes
59//! in the merged one's source vertices. The corner merged is the one held
60//! in the fewest directions, so a vertex of a flat stretch merges into one
61//! on an edge and that into a corner; ties go to
62//! the highest-numbered corner. It merges into the corner held in the most
63//! directions, ties to the nearest, then the lowest-numbered. A collapse is
64//! made only where it keeps the mesh a manifold, the two vertices sharing
65//! no neighbour but the corners across the edge, no face about one closing
66//! over the edge with a face about the other, and a vertex on a rim
67//! merging only along the rim; and only where it turns no face it moves by
68//! more than the edge angle short of a sliver. The faces about the vertex
69//! merged into are then looked at again. A strip pushed onto an edge so
70//! merges into it, and the faces left lie on the offset planes.
71//!
72//! Each collapse takes a vertex away, so the repair ends. A folded face no
73//! collapse may repair is left, and counted in [`OffsetReport::folded`].
74//!
75//! # A region
76//!
77//! With a region, the faces whose corners are all in it move and the rest
78//! stay. A vertex the region shares with faces outside it is parted in
79//! two: the vertex keeps its number and moves with the region's faces,
80//! placed from them alone, and a new vertex, with its source, stays with
81//! the faces outside. Each edge between a face of the region and one
82//! outside becomes a wall of two faces joining the two copies of its ends,
83//! so the region moves as a step and a closed mesh stays closed.
84//!
85//! # What stays
86//!
87//! Vertex and face numbers stay, but for the vertices merged, which are
88//! gone, the faces collapsed, which are removed, and the vertices and faces
89//! a region's wall adds, numbered after every one there is. A vertex with
90//! no face does not move. A vertex [`Editable::non_manifold`] reports moves
91//! with its own fan like any other, so the copies of a point several fans
92//! meet part, but is never merged; edges of three or more faces are joined
93//! again as they then stand.
94
95use core::f64::consts::FRAC_PI_6;
96use std::collections::BTreeSet;
97
98use super::smooth::capped;
99use super::{Editable, NONE, Scope, index, prev};
100use crate::bvh::Bvh;
101use crate::curvature::in_parallel;
102use ogeom_core::{OgeomResult, ogeom_bail};
103use ogeom_math::{Point, Vector};
104use ogeom_topo::Triangulation;
105
106/// The angle, in radians, beyond which the normals of two faces meeting at
107/// an edge turn for the edge to be one the offset keeps: 30°.
108pub const OFFSET_EDGE_ANGLE: f64 = FRAC_PI_6;
109
110/// The farthest a vertex moves, as a multiple of the offset distance.
111pub const OFFSET_FARTHEST: f64 = 3.0;
112
113/// A face's height, in units of the rounding of its corners' largest
114/// coordinate, at or below which it is a sliver. A vertex is placed and
115/// pushed by solving for the meeting of at most three planes, each pair
116/// turning by at least the edge angle, so each solve loses at most
117/// `1 / sin 30° = 2` of a few units of rounding: sixteen covers three.
118const SLIVER: f64 = 16.0;
119
120/// What an offset did besides moving the vertices.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct OffsetReport {
123    /// How many vertices were merged into a neighbour to repair folds.
124    pub merged: usize,
125    /// How many vertices that overshot an edge were pushed back onto it.
126    pub eased: usize,
127    /// How many vertices' steps were shortened to [`OFFSET_FARTHEST`] times
128    /// the distance.
129    pub capped: usize,
130    /// How many faces are left folded, no collapse being able to repair
131    /// them while keeping the mesh a manifold.
132    pub folded: usize,
133}
134
135/// Where a vertex goes: its step, and how many directions it is held in.
136#[derive(Clone, Copy)]
137struct Placed {
138    step: Vector,
139    rank: u8,
140    /// For a vertex moved in one direction, that direction, square to the
141    /// plane it may still slide in; in two, the line it may slide along.
142    free: Vector,
143    capped: bool,
144}
145
146/// A sector of a vertex's fan: its faces' unit normals and angles there.
147#[derive(Default)]
148struct Sector {
149    faces: Vec<(Vector, f64)>,
150}
151
152impl Sector {
153    fn weight(&self) -> f64 {
154        self.faces.iter().map(|&(_, a)| a).sum()
155    }
156
157    fn normal(&self) -> Option<Vector> {
158        let sum = self
159            .faces
160            .iter()
161            .fold(Vector::new(0.0, 0.0, 0.0), |s, &(n, a)| s + n * a);
162        unit(sum)
163    }
164}
165
166fn unit(v: Vector) -> Option<Vector> {
167    let length = v.magnitude();
168    (length > 0.0 && length.is_finite()).then(|| v / length)
169}
170
171impl Editable {
172    /// Offset the mesh by `distance` along its outward normals, outward
173    /// where positive and inward where negative, and repair the folds
174    /// that leaves, as the [module documentation](crate::editable::offset)
175    /// describes. With `region`, one entry per vertex number, only the
176    /// faces whose corners are all in it move, as a step joined to the
177    /// rest by a wall.
178    ///
179    /// # Errors
180    ///
181    /// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if
182    /// `distance` is not finite;
183    /// [`OgeomError::Range`](ogeom_core::OgeomError::Range) if `region` does
184    /// not have one entry per vertex number, or the wall's vertices and
185    /// faces would pass `u32` numbering.
186    pub fn offset(&mut self, distance: f64, region: Option<&[bool]>) -> OgeomResult<OffsetReport> {
187        if !distance.is_finite() {
188            ogeom_bail!(Numeric, "an offset of {distance} is not a distance");
189        }
190        let n = self.positions.len();
191        if let Some(region) = region
192            && region.len() != n
193        {
194            ogeom_bail!(
195                Range,
196                "a region of {} entries for {n} vertices",
197                region.len()
198            );
199        }
200        let mut report = OffsetReport {
201            merged: 0,
202            eased: 0,
203            capped: 0,
204            folded: 0,
205        };
206        if distance == 0.0 {
207            return Ok(report);
208        }
209        let faces_before = self.corners.len();
210        let area: Vec<Vector> = (0..faces_before).map(|f| self.area_vector(f)).collect();
211        let normal: Vec<Option<Vector>> = area
212            .iter()
213            .zip(&self.live)
214            .map(|(&a, &live)| if live { unit(a) } else { None })
215            .collect();
216        let was_sliver: Vec<bool> = (0..faces_before)
217            .map(|f| {
218                let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
219                shape(a, b, c, area[f]) == Shape::Sliver
220            })
221            .collect();
222        let moves: Vec<bool> = self
223            .corners
224            .iter()
225            .zip(&self.live)
226            .map(|(c, &live)| live && region.is_none_or(|r| c.iter().all(|&v| r[v as usize])))
227            .collect();
228
229        let candidates: Vec<u32> = (0..n)
230            .filter(|&v| self.out[v] != NONE && region.is_none_or(|r| r[v]))
231            .map(index)
232            .collect();
233        let placed = in_parallel(&candidates, |v| self.place(v, distance, &normal, &moves));
234        if region.is_some() {
235            self.raise_walls(&candidates, &moves)?;
236        }
237
238        let before = self.positions.clone();
239        let stuck = self.non_manifold();
240        let mut rank = vec![0_u8; self.positions.len()];
241        let mut movable = vec![false; self.positions.len()];
242        let mut sliding: Vec<(u32, Placed)> = Vec::new();
243        for (&v, p) in candidates.iter().zip(&placed) {
244            let k = v as usize;
245            if p.rank > 0 {
246                self.positions[k] += p.step;
247                rank[k] = p.rank;
248                movable[k] = true;
249                report.capped += usize::from(p.capped);
250                if p.rank < 3 {
251                    sliding.push((v, *p));
252                }
253            }
254        }
255        for v in stuck {
256            movable[v as usize] = false;
257        }
258        self.rejoin_crowded_edges();
259
260        let pushed = self.push_back(&before, faces_before, &normal, distance, &sliding)?;
261        report.eased = pushed.len();
262        for (v, to, r) in pushed {
263            self.positions[v as usize] = to;
264            rank[v as usize] = r;
265        }
266
267        let folded = |me: &Self, f: usize| {
268            f < faces_before && me.live[f] && !was_sliver[f] && me.folded(f, area[f])
269        };
270        let mut work: BTreeSet<u32> = (0..faces_before)
271            .filter(|&f| folded(self, f))
272            .map(index)
273            .collect();
274        while let Some(f) = work.pop_first() {
275            if !folded(self, f as usize) {
276                continue;
277            }
278            if let Some(w) = self.repair(f, &rank, &movable, &area) {
279                report.merged += 1;
280                work.extend(
281                    self.fan(w)
282                        .into_iter()
283                        .map(|h| h / 3)
284                        .filter(|&g| (g as usize) < faces_before),
285                );
286            }
287        }
288        report.folded = (0..faces_before).filter(|&f| folded(self, f)).count();
289        Ok(report)
290    }
291
292    /// Face `f`'s area vector, twice its area along its normal.
293    fn area_vector(&self, f: usize) -> Vector {
294        let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
295        (b - a).cross(c - a)
296    }
297
298    /// The half-edges leaving vertex `v`, turning about it from its `out`.
299    fn fan(&self, v: u32) -> Vec<u32> {
300        let start = self.out[v as usize];
301        let mut fan = Vec::new();
302        if start == NONE {
303            return fan;
304        }
305        let mut h = start;
306        loop {
307            fan.push(h);
308            h = self.twin[prev(h) as usize];
309            if h == NONE || h == start {
310                return fan;
311            }
312        }
313    }
314
315    /// Where vertex `v` goes, offset `d` from the faces `moves` marks.
316    fn place(&self, v: u32, d: f64, normal: &[Option<Vector>], moves: &[bool]) -> Placed {
317        let still = Placed {
318            step: Vector::new(0.0, 0.0, 0.0),
319            rank: 0,
320            free: Vector::new(0.0, 0.0, 0.0),
321            capped: false,
322        };
323        let fan = self.fan(v);
324        let closed = fan.first().is_some_and(|&h| self.twin[h as usize] != NONE);
325        let p = self.positions[v as usize];
326        let cos_edge = OFFSET_EDGE_ANGLE.cos();
327        let mut sectors: Vec<Sector> = vec![Sector::default()];
328        // Whether the fan was parted where it starts: always for an open
329        // fan, and for a closed one by an edge or a face left out there.
330        let mut parted_at_start = !closed;
331        let mut last: Option<Vector> = None;
332        let mut first: Option<Vector> = None;
333        for (i, &h) in fan.iter().enumerate() {
334            let f = (h / 3) as usize;
335            if !moves[f] {
336                if i == 0 {
337                    parted_at_start = true;
338                }
339                if !sectors.last().is_some_and(|s| s.faces.is_empty()) {
340                    sectors.push(Sector::default());
341                }
342                last = None;
343                continue;
344            }
345            let Some(nf) = normal[f] else { continue };
346            if last.is_some_and(|m| m.dot(nf) < cos_edge)
347                && !sectors.last().is_some_and(|s| s.faces.is_empty())
348            {
349                sectors.push(Sector::default());
350            }
351            if first.is_none() && sectors.len() == 1 {
352                first = Some(nf);
353            }
354            last = Some(nf);
355            let [a, b] = [self.head(h), self.tail(prev(h))].map(|w| self.positions[w as usize] - p);
356            let angle = a.cross(b).magnitude().atan2(a.dot(b));
357            if let Some(s) = sectors.last_mut() {
358                s.faces.push((nf, angle));
359            }
360        }
361        sectors.retain(|s| !s.faces.is_empty());
362        if closed
363            && !parted_at_start
364            && sectors.len() > 1
365            && let (Some(m), Some(nf)) = (last, first)
366            && m.dot(nf) >= cos_edge
367            && let Some(tail) = sectors.pop()
368        {
369            sectors[0].faces.extend(tail.faces);
370        }
371
372        let groups: Vec<(Vector, f64)> = sectors
373            .iter()
374            .filter_map(|s| s.normal().map(|m| (m, s.weight())))
375            .collect();
376        let Some(&(heaviest, _)) = groups.iter().reduce(|a, b| if b.1 > a.1 { b } else { a })
377        else {
378            return still;
379        };
380        let mut basis = vec![heaviest];
381        let mut taken = vec![heaviest];
382        while basis.len() < 3 {
383            let across = groups
384                .iter()
385                .filter(|&&(m, _)| taken.iter().all(|&t| m.dot(t) < cos_edge))
386                .map(|&(m, _)| (m, basis.iter().fold(m, |r, &e| r - e * r.dot(e))))
387                .reduce(|a, b| {
388                    if b.1.magnitude() > a.1.magnitude() {
389                        b
390                    } else {
391                        a
392                    }
393                });
394            match across.and_then(|(m, r)| unit(r).map(|e| (m, e))) {
395                Some((m, e)) => {
396                    taken.push(m);
397                    basis.push(e);
398                }
399                None => break,
400            }
401        }
402
403        let faces = sectors.iter().flat_map(|s| s.faces.iter());
404        let k = basis.len();
405        let mut m = [[0.0; 3]; 3];
406        let mut r = [0.0; 3];
407        for &(nf, a) in faces {
408            let mut c = [0.0; 3];
409            for (ci, &e) in c.iter_mut().zip(&basis) {
410                *ci = nf.dot(e);
411            }
412            for i in 0..k {
413                r[i] += a * d * c[i];
414                for j in 0..k {
415                    m[i][j] += a * c[i] * c[j];
416                }
417            }
418        }
419        let Some(alpha) = solve(m, r, k) else {
420            return still;
421        };
422        let step = basis
423            .iter()
424            .zip(alpha)
425            .fold(Vector::new(0.0, 0.0, 0.0), |s, (&e, t)| s + e * t);
426        if !step.magnitude().is_finite() {
427            return still;
428        }
429        let (to, held) = capped(p, p + step, OFFSET_FARTHEST * d.abs());
430        let free = match k {
431            1 => Some(basis[0]),
432            2 => unit(basis[0].cross(basis[1])),
433            _ => None,
434        };
435        Placed {
436            step: to - p,
437            rank: u8::try_from(k).unwrap_or(u8::MAX),
438            free: free.unwrap_or(Vector::new(0.0, 0.0, 0.0)),
439            capped: held,
440        }
441    }
442
443    /// Part each vertex among `candidates` that has faces both moving and
444    /// not, as `moves` marks, into itself on the moving faces and a new
445    /// vertex on the rest, and join the two by a wall along every edge
446    /// between a moving face and one that is not.
447    fn raise_walls(&mut self, candidates: &[u32], moves: &[bool]) -> OgeomResult<()> {
448        let mut foot = vec![NONE; self.positions.len()];
449        let mut parted: Vec<u32> = Vec::new();
450        for &v in candidates {
451            let fan = self.fan(v);
452            let mixed = fan.iter().any(|&h| moves[(h / 3) as usize])
453                && fan.iter().any(|&h| !moves[(h / 3) as usize]);
454            if mixed {
455                parted.push(v);
456            }
457        }
458        if parted.is_empty() {
459            return Ok(());
460        }
461        let walls: Vec<u32> = parted
462            .iter()
463            .flat_map(|&v| self.fan(v))
464            .filter(|&h| {
465                let g = self.twin[h as usize];
466                moves[(h / 3) as usize] && g != NONE && !moves[(g / 3) as usize]
467            })
468            .collect();
469        let added = parted.len().saturating_add(self.positions.len());
470        if added >= NONE as usize || (self.corners.len() + 2 * walls.len()) * 3 >= NONE as usize {
471            ogeom_bail!(Range, "a wall past u32 numbering");
472        }
473        let mut points: Vec<u32> = parted.iter().map(|&v| self.point[v as usize]).collect();
474        points.sort_unstable();
475        points.dedup();
476        let mut faces = self.about(&points);
477        for &v in &parted {
478            let w = self.add_vertex(self.positions[v as usize]);
479            self.copy_sources(w, v);
480            foot[v as usize] = w;
481            points.push(w);
482        }
483        for &f in &faces {
484            if !moves[f as usize] {
485                for c in &mut self.corners[f as usize] {
486                    let w = foot[*c as usize];
487                    if w != NONE {
488                        *c = w;
489                    }
490                }
491            }
492        }
493        for h in walls {
494            let (a, b) = (self.tail(h), self.head(h));
495            let (fa, fb) = (foot[a as usize], foot[b as usize]);
496            faces.push(self.add_face([fa, fb, b]));
497            faces.push(self.add_face([fa, b, a]));
498        }
499        faces.sort_unstable();
500        points.sort_unstable();
501        self.relink(&Scope::Points(points), &faces);
502        Ok(())
503    }
504
505    /// Push back each vertex of `sliding` that has come nearer than
506    /// `|d|` to the mesh as it stood at `before`, its first
507    /// `faces_before` faces with their unit normals `normal`: slid along
508    /// the plane or line it may still move in onto the offset plane of the
509    /// face it came nearest, then held to that plane too, while it stays on
510    /// the same side of that plane and within the cap. Returns each vertex
511    /// pushed, where it goes, and in how many directions it is then held.
512    fn push_back(
513        &self,
514        before: &[Point],
515        faces_before: usize,
516        normal: &[Option<Vector>],
517        d: f64,
518        sliding: &[(u32, Placed)],
519    ) -> OgeomResult<Vec<(u32, Point, u8)>> {
520        if sliding.is_empty() {
521            return Ok(Vec::new());
522        }
523        let mut faces: Vec<u32> = Vec::new();
524        let mut triangles: Vec<[u32; 3]> = Vec::new();
525        for (f, n) in normal.iter().enumerate().take(faces_before) {
526            if n.is_some() {
527                faces.push(index(f));
528                triangles.push(self.corners[f]);
529            }
530        }
531        let mesh = Triangulation {
532            positions: before.to_vec(),
533            triangles,
534            ..Triangulation::new()
535        };
536        let bvh = Bvh::build(&mesh)?;
537        let reach = d.abs();
538        let least = OFFSET_EDGE_ANGLE.sin();
539        let movers: Vec<u32> = sliding.iter().map(|&(v, _)| v).collect();
540        let pushed = in_parallel(&movers, |v| {
541            let slot = movers.binary_search(&v).ok()?;
542            let placed = sliding[slot].1;
543            let start = before[v as usize];
544            let placed_at = self.positions[v as usize];
545            let mut x = placed_at;
546            let mut rank = placed.rank;
547            let mut free = placed.free;
548            let mut moved = false;
549            while rank < 3 {
550                // The face must turn by the edge angle from the plane or
551                // line the vertex slides in, or it is of the same smooth
552                // stretch.
553                let across = |nf: Vector| {
554                    let slant = if rank == 1 {
555                        (1.0 - nf.dot(free).powi(2)).max(0.0).sqrt()
556                    } else {
557                        nf.dot(free).abs()
558                    };
559                    slant >= least
560                };
561                // Faces are looked for about where the vertex was placed,
562                // and it must stand there on the side of the face it is
563                // offset to.
564                let side = |f: usize, nf: Vector, at: Point| {
565                    nf.dot(at - before[self.corners[f][0] as usize])
566                };
567                let Some(hit) = bvh.nearest_where(placed_at, reach, |t| {
568                    let f = faces[t as usize] as usize;
569                    normal[f].is_some_and(|nf| across(nf) && side(f, nf, placed_at) * d > 0.0)
570                }) else {
571                    break;
572                };
573                if hit.distance >= reach {
574                    break;
575                }
576                let f = faces[hit.triangle as usize] as usize;
577                let Some(nf) = normal[f] else { break };
578                let side = side(f, nf, x);
579                let along = if rank == 1 {
580                    nf - free * nf.dot(free)
581                } else {
582                    free
583                };
584                let Some(along) = unit(along) else { break };
585                let t = (d - side) / nf.dot(along);
586                let to = x + along * t;
587                if !t.is_finite() || (to - start).magnitude() > OFFSET_FARTHEST * reach {
588                    break;
589                }
590                x = to;
591                moved = true;
592                if rank == 1 {
593                    let Some(line) = unit(free.cross(nf)) else {
594                        break;
595                    };
596                    free = line;
597                }
598                rank += 1;
599            }
600            moved.then_some((v, x, rank))
601        });
602        Ok(pushed.into_iter().flatten().collect())
603    }
604
605    /// Whether face `f`, its area vector `was` before the offset, is
606    /// folded.
607    fn folded(&self, f: usize, was: Vector) -> bool {
608        let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
609        shape(a, b, c, was) != Shape::Kept
610    }
611
612    /// Repair folded face `f` by merging one of its corners into another,
613    /// as the module documentation orders them; the vertex merged into, or
614    /// `None` where no collapse keeps the mesh a manifold.
615    fn repair(&mut self, f: u32, rank: &[u8], movable: &[bool], area: &[Vector]) -> Option<u32> {
616        let corners = self.corners[f as usize];
617        let mut from: Vec<u32> = corners
618            .iter()
619            .copied()
620            .filter(|&v| movable[v as usize])
621            .collect();
622        from.sort_by(|&a, &b| rank[a as usize].cmp(&rank[b as usize]).then(b.cmp(&a)));
623        for &u in &from {
624            let p = self.positions[u as usize];
625            let mut to: Vec<u32> = from.iter().copied().filter(|&w| w != u).collect();
626            to.sort_by(|&a, &b| {
627                rank[b as usize]
628                    .cmp(&rank[a as usize])
629                    .then(
630                        self.positions[a as usize]
631                            .distance(p)
632                            .total_cmp(&self.positions[b as usize].distance(p)),
633                    )
634                    .then(a.cmp(&b))
635            });
636            for w in to {
637                if self.collapse(u, w, area) {
638                    return Some(w);
639                }
640            }
641        }
642        None
643    }
644
645    /// Merge vertex `u` into its neighbour `w`, removing the faces on the
646    /// edge between them, if that keeps the mesh a manifold; whether it did.
647    fn collapse(&mut self, u: u32, w: u32, area: &[Vector]) -> bool {
648        let lone = |me: &Self, v: u32| {
649            me.ring(me.point[v as usize])
650                .into_iter()
651                .all(|c| c == v || me.out[c as usize] == NONE)
652        };
653        if !lone(self, u) || !lone(self, w) {
654            return false;
655        }
656        let (fan_u, fan_w) = (self.fan(u), self.fan(w));
657        let on_rim = |fan: &[u32]| fan.first().is_some_and(|&h| self.twin[h as usize] == NONE);
658        let neighbours = |fan: &[u32]| {
659            let mut n: Vec<u32> = fan
660                .iter()
661                .flat_map(|&h| [self.head(h), self.tail(prev(h))])
662                .collect();
663            n.sort_unstable();
664            n.dedup();
665            n
666        };
667        let link = |fan: &[u32]| {
668            let mut l: Vec<(u32, u32)> = fan
669                .iter()
670                .map(|&h| {
671                    let (a, b) = (self.head(h), self.tail(prev(h)));
672                    (a.min(b), a.max(b))
673                })
674                .collect();
675            l.sort_unstable();
676            l
677        };
678        let shared: Vec<u32> = fan_u
679            .iter()
680            .copied()
681            .filter(|&h| self.corners[(h / 3) as usize].contains(&w))
682            .collect();
683        if shared.is_empty() || shared.len() > 2 {
684            return false;
685        }
686        if on_rim(&fan_u) && shared.len() != 1 {
687            return false;
688        }
689        let mut across: Vec<u32> = shared
690            .iter()
691            .flat_map(|&h| self.corners[(h / 3) as usize])
692            .filter(|&c| c != u && c != w)
693            .collect();
694        across.sort_unstable();
695        across.dedup();
696        let (nu, nw) = (neighbours(&fan_u), neighbours(&fan_w));
697        let common: Vec<u32> = nu
698            .iter()
699            .copied()
700            .filter(|c| nw.binary_search(c).is_ok())
701            .collect();
702        if common != across {
703            return false;
704        }
705        let lw = link(&fan_w);
706        if link(&fan_u).iter().any(|e| lw.binary_search(e).is_ok()) {
707            return false;
708        }
709
710        let p = self.positions[w as usize];
711        for &h in &fan_u {
712            let f = (h / 3) as usize;
713            if f >= area.len() || shared.contains(&h) {
714                continue;
715            }
716            let [a, b] = [self.head(h), self.tail(prev(h))].map(|v| self.positions[v as usize]);
717            if shape(p, a, b, area[f]) == Shape::Turned {
718                return false;
719            }
720        }
721
722        let mut points: Vec<u32> = [u, w]
723            .into_iter()
724            .chain(across.iter().copied())
725            .map(|v| self.point[v as usize])
726            .collect();
727        points.sort_unstable();
728        points.dedup();
729        let around = self.about(&points);
730        for &h in &shared {
731            let g = h / 3;
732            self.live[g as usize] = false;
733            for e in 3 * g..3 * g + 3 {
734                self.twin[e as usize] = NONE;
735            }
736        }
737        let kept: Vec<u32> = around
738            .into_iter()
739            .filter(|&g| self.live[g as usize])
740            .collect();
741        self.absorb_sources(w, u);
742        for &g in &kept {
743            for c in &mut self.corners[g as usize] {
744                if *c == u {
745                    *c = w;
746                }
747            }
748        }
749        self.relink(&Scope::Points(points), &kept);
750        true
751    }
752}
753
754/// How a face stands against the way it faced before the offset.
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
756enum Shape {
757    /// Turned from its normal before by no more than the edge angle.
758    Kept,
759    /// Turned by more, short of a sliver.
760    Turned,
761    /// A sliver: its height is within the rounding of its corners.
762    Sliver,
763}
764
765/// How the face on `a`, `b` and `c`, its area vector `was` before the
766/// offset, stands.
767fn shape(a: Point, b: Point, c: Point, was: Vector) -> Shape {
768    let now = (b - a).cross(c - a);
769    let longest = (b - a)
770        .magnitude()
771        .max((c - b).magnitude())
772        .max((a - c).magnitude());
773    let size = [a, b, c]
774        .iter()
775        .flat_map(|p| [p.x.abs(), p.y.abs(), p.z.abs()])
776        .fold(0.0, f64::max);
777    if now.magnitude() <= SLIVER * f64::EPSILON * size * longest {
778        Shape::Sliver
779    } else if now.dot(was) < OFFSET_EDGE_ANGLE.cos() * now.magnitude() * was.magnitude() {
780        Shape::Turned
781    } else {
782        Shape::Kept
783    }
784}
785
786/// The solution of the first `k` rows and columns of `m x = r`, by
787/// elimination with partial pivoting; `None` where a pivot is zero.
788fn solve(mut m: [[f64; 3]; 3], mut r: [f64; 3], k: usize) -> Option<[f64; 3]> {
789    for col in 0..k {
790        let pivot = (col..k).max_by(|&a, &b| m[a][col].abs().total_cmp(&m[b][col].abs()))?;
791        if m[pivot][col] == 0.0 {
792            return None;
793        }
794        m.swap(col, pivot);
795        r.swap(col, pivot);
796        for row in col + 1..k {
797            let s = m[row][col] / m[col][col];
798            let above = m[col];
799            for (mj, &aj) in m[row].iter_mut().zip(&above).take(k).skip(col) {
800                *mj -= s * aj;
801            }
802            r[row] -= s * r[col];
803        }
804    }
805    let mut x = [0.0; 3];
806    for row in (0..k).rev() {
807        let mut s = r[row];
808        for j in row + 1..k {
809            s -= m[row][j] * x[j];
810        }
811        x[row] = s / m[row][row];
812    }
813    Some(x)
814}