Skip to main content

ogeom_algo/
mesh_solid.rs

1//! A B-rep solid from a triangle mesh.
2//!
3//! A mesh already says which triangles meet along which edges: once its
4//! repeated vertices are welded, two triangles that share two vertices
5//! share an edge. So the topology is built from that connectivity
6//! directly (one vertex per mesh vertex, one edge per mesh edge), and no
7//! geometric sewing search is needed, which is what lets a printable file
8//! of hundreds of thousands of triangles convert in seconds.
9//!
10//! Adjacent triangles that lie in one plane merge into one planar face,
11//! bounded by the region's outer loop and its holes, and a run of boundary
12//! segments along one straight line between the same two faces becomes
13//! one edge: a cube's twelve triangles become six faces and twelve edges,
14//! and the faces take fillets and chamfers as a modelled box's do.
15//!
16//! Curved regions are recognized: triangles across which the surface
17//! turns smoothly are grown into regions for as long as their vertices lie
18//! on one cylinder, cone, sphere or torus, verified at the stated
19//! tolerance, and the region is rebuilt on that surface. Its boundary with
20//! each neighbour is placed on the surface exactly (a parallel circle or
21//! a ruling of it), so a meshed bore comes back as a cylinder between two
22//! circles, and a band all the way round its axis gets a seam as a
23//! modelled one has. A sphere or torus with no boundary is one face, a
24//! sphere bounded by one circle a cap, and a band round a torus's tube
25//! runs between two of its meridians. A region whose boundary is no such
26//! curve, or that nothing canonical fits, stays faceted.
27//!
28//! Windings are made consistent across each connected piece and turned
29//! outward. A mesh that does not close (a hole, an edge three triangles
30//! share, a piece that cannot be oriented) does not fail: it comes back
31//! as open shells, with the report saying why.
32
33use std::collections::HashMap;
34
35use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
36use ogeom_geom::{Curve, LineCurve, PlanarCurve, PlaneSurface};
37use ogeom_math::{Cone, Cylinder, Direction, Frame, Plane, Point, Point2, Sphere, Torus, Vector};
38
39use crate::recognize::{Canonical, recognize_curved, worst_deviation};
40use ogeom_topo::{EdgeData, EdgeRepr, FaceData, Location, Model, Shape, Triangulation, VertexData};
41
42/// How [`solid_from_mesh`] builds.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct MeshSolidOptions {
45    /// Merge adjacent coplanar triangles into one planar face, and collinear
46    /// boundary segments into one edge. Without it every triangle is a face
47    /// and every mesh edge an edge.
48    pub merge_coplanar: bool,
49    /// The angle, in radians, below which two triangles' normals count as
50    /// parallel for merging.
51    pub coplanar_angle: f64,
52    /// How far a triangle's corner may sit off a face's plane, or a
53    /// merged edge's intermediate vertex off its line, and still be merged.
54    /// `None` takes the resolution a mesh stored in single precision has:
55    /// a millionth of its bounding box's diagonal, never below the weld
56    /// distance. The vertices and edges of a merged face widen their
57    /// tolerances to cover what they stand off it.
58    pub coplanar_distance: Option<f64>,
59    /// Vertices closer than this are welded into one before building; an
60    /// STL repeats every vertex once per triangle that uses it. `None` is
61    /// the confusion tolerance.
62    pub weld: Option<f64>,
63    /// Recognize curved regions as cylinders, cones, spheres and tori, at
64    /// the coplanar distance. Needs `merge_coplanar`.
65    pub recognize: bool,
66    /// The angle, in radians, from which a turn between two triangles is a
67    /// crease (an edge of the model) rather than the surface curving on:
68    /// thirty degrees by default. A mesh drawn coarser than this round a
69    /// curve reads as facets.
70    pub crease: f64,
71}
72
73impl Default for MeshSolidOptions {
74    fn default() -> Self {
75        Self {
76            merge_coplanar: true,
77            coplanar_angle: 1e-3,
78            coplanar_distance: None,
79            weld: None,
80            recognize: true,
81            crease: core::f64::consts::FRAC_PI_6,
82        }
83    }
84}
85
86/// What [`solid_from_mesh`] did, and where the mesh does not close.
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct MeshSolidReport {
89    /// Triangles built from, after the dropped ones.
90    pub triangles: usize,
91    /// Faces built: the triangles, or their coplanar groups and recognized
92    /// regions.
93    pub faces: usize,
94    /// Faces built on a recognized curved surface.
95    pub curved_faces: usize,
96    /// Regions recognized as curved whose boundary could not be placed on
97    /// the surface exactly, and which were faceted instead.
98    pub curved_faceted: usize,
99    /// Mesh vertices that welded onto another.
100    pub vertices_welded: usize,
101    /// Triangles dropped for having no area.
102    pub degenerate_dropped: usize,
103    /// Triangles dropped for repeating another's three vertices.
104    pub duplicates_dropped: usize,
105    /// Triangles whose winding was reversed to agree with their neighbours
106    /// and face outward.
107    pub windings_flipped: usize,
108    /// Mesh edges only one triangle uses: the rims of holes.
109    pub edges_used_once: usize,
110    /// Mesh edges three or more triangles use: non-manifold.
111    pub edges_used_more: usize,
112    /// Shared edges whose windings cannot be made to agree, in a piece that
113    /// is not orientable.
114    pub orientation_conflicts: usize,
115    /// Connected pieces, each one shell.
116    pub shells: usize,
117}
118
119/// The result of [`solid_from_mesh`].
120#[derive(Debug, Clone)]
121pub struct MeshSolid {
122    /// A solid when the mesh closes (a compound of solids when it is
123    /// several disjoint closed pieces), and otherwise a shell, or a
124    /// compound of shells.
125    pub shape: Shape,
126    /// Whether every piece closed and became a solid.
127    pub closed: bool,
128    /// What was built, and where the mesh does not close.
129    pub report: MeshSolidReport,
130}
131
132/// Seal the cracks flat slivers leave when they are dropped.
133///
134/// A triangle with three distinct corners and no area has one corner on the
135/// segment between the other two. In a closed mesh the triangle across that
136/// long side fills the other side of it, and the two short sides are shared
137/// with the neighbours beyond: dropped alone, the sliver leaves all three
138/// used once. The triangle across the long side is split at the middle
139/// corner instead, so each short side is shared again. Slivers against
140/// slivers resolve as their neighbours are split, round by round.
141fn split_across_slivers(points: &[Point], triangles: &mut Vec<[u32; 3]>, slivers: &[[u32; 3]]) {
142    let mut pending: Vec<[u32; 3]> = slivers
143        .iter()
144        .map(|&[a, b, c]| {
145            // The middle corner is the one opposite the longest side.
146            let long = |x: u32, y: u32| points[x as usize].distance(points[y as usize]);
147            let sides = [
148                (long(b, c), a, [b, c]),
149                (long(c, a), b, [c, a]),
150                (long(a, b), c, [a, b]),
151            ];
152            let (_, middle, [p, q]) =
153                sides.into_iter().fold(
154                    sides[0],
155                    |best, side| if side.0 > best.0 { side } else { best },
156                );
157            [p, middle, q]
158        })
159        .collect();
160    loop {
161        let mut progress = false;
162        let mut left = Vec::new();
163        for [p, middle, q] in pending {
164            let across = triangles.iter().position(|t| {
165                (0..3).any(|k| {
166                    let (x, y) = (t[k], t[(k + 1) % 3]);
167                    (x == p && y == q) || (x == q && y == p)
168                }) && !t.contains(&middle)
169            });
170            let Some(index) = across else {
171                left.push([p, middle, q]);
172                continue;
173            };
174            let t = triangles[index];
175            let Some(k) = (0..3).find(|&k| {
176                let (x, y) = (t[k], t[(k + 1) % 3]);
177                (x == p && y == q) || (x == q && y == p)
178            }) else {
179                continue;
180            };
181            let (x, y, z) = (t[k], t[(k + 1) % 3], t[(k + 2) % 3]);
182            triangles[index] = [x, middle, z];
183            triangles.push([middle, y, z]);
184            progress = true;
185        }
186        pending = left;
187        if pending.is_empty() || !progress {
188            break;
189        }
190    }
191}
192
193/// Build a B-rep from a triangle mesh.
194///
195/// See the module documentation for the construction. A closed piece
196/// inside another becomes a void of the solid around it.
197///
198/// # Errors
199///
200/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
201/// mesh has no triangle with area, an index names a vertex that is not
202/// there, or an option is not finite and positive.
203pub fn solid_from_mesh(
204    model: &mut Model,
205    mesh: &Triangulation,
206    options: &MeshSolidOptions,
207    tol: Tolerances,
208) -> OgeomResult<MeshSolid> {
209    let weld = options.weld.unwrap_or_else(|| tol.confusion());
210    if !(weld.is_finite() && weld > 0.0) {
211        ogeom_bail!(
212            Construction,
213            "the weld distance {weld} is not finite and positive"
214        );
215    }
216    if !(options.coplanar_angle.is_finite() && options.coplanar_angle >= 0.0) {
217        ogeom_bail!(Construction, "the coplanar angle is not finite");
218    }
219    let count = mesh.positions.len();
220    if mesh
221        .triangles
222        .iter()
223        .flatten()
224        .any(|&v| v as usize >= count)
225    {
226        ogeom_bail!(
227            Construction,
228            "a triangle names a vertex past the {count} the mesh has"
229        );
230    }
231    let mut report = MeshSolidReport::default();
232
233    let (points, remap) = weld_points(&mesh.positions, weld);
234    report.vertices_welded = count - points.len();
235    let mut triangles = Vec::with_capacity(mesh.triangles.len());
236    let mut seen: HashMap<[u32; 3], ()> = HashMap::with_capacity(mesh.triangles.len());
237    let mut flat_slivers: Vec<[u32; 3]> = Vec::new();
238    for t in &mesh.triangles {
239        let [a, b, c] = t.map(|v| remap[v as usize]);
240        if a == b || b == c || c == a || !has_area(&points, [a, b, c], weld) {
241            report.degenerate_dropped += 1;
242            if a != b && b != c && c != a {
243                flat_slivers.push([a, b, c]);
244            }
245            continue;
246        }
247        let mut key = [a, b, c];
248        key.sort_unstable();
249        if seen.insert(key, ()).is_some() {
250            report.duplicates_dropped += 1;
251            continue;
252        }
253        triangles.push([a, b, c]);
254    }
255    if triangles.is_empty() {
256        ogeom_bail!(Construction, "the mesh has no triangle with area");
257    }
258    split_across_slivers(&points, &mut triangles, &flat_slivers);
259    report.triangles = triangles.len();
260
261    // Orient each piece consistently, then outward where it closes.
262    let adjacency = Adjacency::new(&triangles);
263    report.edges_used_once = adjacency.used_once;
264    report.edges_used_more = adjacency.used_more;
265    let pieces = orient(&points, &mut triangles, &adjacency, &mut report);
266    // A closed piece nested an odd number of pieces deep bounds a void of
267    // the one around it, and faces into the void, out of the material.
268    let all_closed = pieces.iter().all(|p| p.closed);
269    let depth: Vec<usize> = if all_closed {
270        (0..pieces.len())
271            .map(|i| {
272                (0..pieces.len())
273                    .filter(|&j| j != i && inside(&points, &triangles, &pieces[j], &pieces[i]))
274                    .count()
275            })
276            .collect()
277    } else {
278        vec![0; pieces.len()]
279    };
280    for (piece, d) in pieces.iter().zip(&depth) {
281        if d % 2 == 1 {
282            for &t in &piece.triangles {
283                triangles[t as usize].swap(1, 2);
284                report.windings_flipped += 1;
285            }
286        }
287    }
288    // Flipping renumbers every triangle's edges; the twins are found again.
289    let adjacency = Adjacency::new(&triangles);
290    report.shells = pieces.len();
291
292    let diagonal = diagonal(&points);
293    let flat = options
294        .coplanar_distance
295        .unwrap_or(1e-6 * diagonal)
296        .max(weld);
297    let mut groups = segment(&points, &triangles, &adjacency, options, flat, tol)?;
298    // Plan until every curved face's boundary is exact, faceting the ones
299    // whose boundary is not.
300    let mut pinned: std::collections::HashSet<u32> = std::collections::HashSet::new();
301    let plan = loop {
302        let planner = Planner {
303            points: &points,
304            triangles: &triangles,
305            adjacency: &adjacency,
306            groups: &groups,
307            merge: options.merge_coplanar,
308            pinned: &pinned,
309            flat,
310            tol,
311        };
312        let planned = planner.plan()?;
313
314        match planned {
315            Ok(plan) => break plan,
316            Err(Replan::Pin(vertices)) => pinned.extend(vertices),
317            Err(Replan::Facet(failed)) => {
318                for g in failed {
319                    groups.carriers[g] = Carrier::Gone;
320                    report.curved_faceted += 1;
321                    for of in &mut groups.of {
322                        if *of == g {
323                            *of = usize::MAX;
324                        }
325                    }
326                }
327                coplanar_groups(
328                    &points,
329                    &triangles,
330                    &adjacency,
331                    options.coplanar_angle,
332                    flat,
333                    &mut groups,
334                    tol,
335                )?;
336            }
337        }
338    };
339
340    model.begin_operation();
341    let built = Builder {
342        model,
343        points: &points,
344        triangles: &triangles,
345        groups: &groups,
346        plan: &plan,
347        tol,
348    }
349    .build()?;
350    report.faces = built.iter().flatten().count();
351    report.curved_faces = groups
352        .carriers
353        .iter()
354        .zip(&built)
355        .filter(|(c, b)| matches!(c, Carrier::Curved(_)) && b.is_some())
356        .count();
357
358    // Faces into one shell per piece; closed pieces into solids, a piece
359    // nested an odd number of times deep being a void of the one around it.
360    let mut shells = Vec::with_capacity(pieces.len());
361    for piece in &pieces {
362        let mut faces: Vec<Shape> = Vec::new();
363        let mut taken = vec![false; groups.carriers.len()];
364        for &t in &piece.triangles {
365            let g = groups.of[t as usize];
366            if !taken[g] {
367                taken[g] = true;
368                if let Some(face) = &built[g] {
369                    faces.push(face.clone());
370                }
371            }
372        }
373        shells.push(model.add_shell(&faces)?);
374    }
375    let shape = if all_closed {
376        let mut solids = Vec::new();
377        for (i, shell) in shells.iter().enumerate() {
378            if depth[i] % 2 == 1 {
379                continue;
380            }
381            let mut members = vec![shell.clone()];
382            for (j, void) in shells.iter().enumerate() {
383                if depth[j] == depth[i] + 1 && inside(&points, &triangles, &pieces[i], &pieces[j]) {
384                    members.push(void.clone());
385                }
386            }
387            solids.push(model.add_solid(&members)?);
388        }
389        if solids.len() == 1 {
390            solids.swap_remove(0)
391        } else {
392            model.add_compound(&solids)?
393        }
394    } else if shells.len() == 1 {
395        shells.swap_remove(0)
396    } else {
397        model.add_compound(&shells)?
398    };
399    Ok(MeshSolid {
400        shape,
401        closed: all_closed,
402        report,
403    })
404}
405
406/// Weld points on a grid of the weld distance, looking in the neighbouring
407/// cells too, so two points either side of a cell wall still meet.
408fn weld_points(positions: &[Point], weld: f64) -> (Vec<Point>, Vec<u32>) {
409    // A cast saturates, so a coordinate past `i64`'s cells shares the last
410    // one and is still compared by distance.
411    #[allow(clippy::cast_possible_truncation, reason = "saturating")]
412    let cell = |p: Point| {
413        (
414            (p.x / weld).floor() as i64,
415            (p.y / weld).floor() as i64,
416            (p.z / weld).floor() as i64,
417        )
418    };
419    let mut grid: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::with_capacity(positions.len());
420    let mut kept: Vec<Point> = Vec::with_capacity(positions.len());
421    let mut remap = Vec::with_capacity(positions.len());
422    for p in positions {
423        let (x, y, z) = cell(*p);
424        let mut found = None;
425        'search: for dx in -1..=1_i64 {
426            for dy in -1..=1_i64 {
427                for dz in -1..=1_i64 {
428                    let key = (
429                        x.saturating_add(dx),
430                        y.saturating_add(dy),
431                        z.saturating_add(dz),
432                    );
433                    if let Some(list) = grid.get(&key)
434                        && let Some(&k) = list
435                            .iter()
436                            .find(|&&k| kept[k as usize].distance(*p) <= weld)
437                    {
438                        found = Some(k);
439                        break 'search;
440                    }
441                }
442            }
443        }
444        let index = found.unwrap_or_else(|| {
445            let k = u32::try_from(kept.len()).unwrap_or(u32::MAX);
446            kept.push(*p);
447            grid.entry((x, y, z)).or_default().push(k);
448            k
449        });
450        remap.push(index);
451    }
452    (kept, remap)
453}
454
455/// Whether a triangle stands more than the weld distance tall over its
456/// longest side.
457fn has_area(points: &[Point], [a, b, c]: [u32; 3], weld: f64) -> bool {
458    let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
459    let twice_area = (b - a).cross(c - a).magnitude();
460    let longest = a.distance(b).max(b.distance(c)).max(c.distance(a));
461    twice_area > weld * longest
462}
463
464fn diagonal(points: &[Point]) -> f64 {
465    let (lo, hi) = points
466        .iter()
467        .fold(([f64::MAX; 3], [f64::MIN; 3]), |(lo, hi), p| {
468            (
469                [lo[0].min(p.x), lo[1].min(p.y), lo[2].min(p.z)],
470                [hi[0].max(p.x), hi[1].max(p.y), hi[2].max(p.z)],
471            )
472        });
473    Point::new(lo[0], lo[1], lo[2]).distance(Point::new(hi[0], hi[1], hi[2]))
474}
475
476/// A half-edge: side `k` of triangle `t`, as `3 t + k`, running from the
477/// triangle's corner `k` to corner `k + 1`.
478type Half = usize;
479
480fn from_to(triangles: &[[u32; 3]], h: Half) -> (u32, u32) {
481    let t = triangles[h / 3];
482    (t[h % 3], t[(h % 3 + 1) % 3])
483}
484
485fn next(h: Half) -> Half {
486    h - h % 3 + (h % 3 + 1) % 3
487}
488
489/// Which half-edges pair across a mesh edge exactly two triangles share.
490struct Adjacency {
491    /// The other triangle's half-edge on the same mesh edge, where exactly
492    /// one other triangle uses it.
493    twin: Vec<Option<Half>>,
494    used_once: usize,
495    used_more: usize,
496}
497
498impl Adjacency {
499    fn new(triangles: &[[u32; 3]]) -> Self {
500        let mut keyed: Vec<(u64, Half)> = (0..triangles.len() * 3)
501            .map(|h| {
502                let (a, b) = from_to(triangles, h);
503                ((u64::from(a.min(b)) << 32) | u64::from(a.max(b)), h)
504            })
505            .collect();
506        keyed.sort_unstable();
507        let mut twin = vec![None; keyed.len()];
508        let (mut used_once, mut used_more) = (0, 0);
509        let mut i = 0;
510        while i < keyed.len() {
511            let mut j = i;
512            while j < keyed.len() && keyed[j].0 == keyed[i].0 {
513                j += 1;
514            }
515            let n = j - i;
516            match n {
517                1 => used_once += 1,
518                2 => {
519                    twin[keyed[i].1] = Some(keyed[i + 1].1);
520                    twin[keyed[i + 1].1] = Some(keyed[i].1);
521                }
522                _ => used_more += 1,
523            }
524            i = j;
525        }
526        Self {
527            twin,
528            used_once,
529            used_more,
530        }
531    }
532}
533
534/// One connected piece: its triangles, and whether it closes.
535struct Piece {
536    triangles: Vec<u32>,
537    closed: bool,
538}
539
540/// Make windings agree across every shared edge, piece by piece; turn a
541/// closed piece outward and leave an open one the way most of its
542/// triangles already were.
543fn orient(
544    points: &[Point],
545    triangles: &mut [[u32; 3]],
546    adjacency: &Adjacency,
547    report: &mut MeshSolidReport,
548) -> Vec<Piece> {
549    let n = triangles.len();
550    let mut flip: Vec<Option<bool>> = vec![None; n];
551    let mut pieces = Vec::new();
552    for seed in 0..n {
553        if flip[seed].is_some() {
554            continue;
555        }
556        flip[seed] = Some(false);
557        let mut members = vec![seed];
558        let mut stack = vec![seed];
559        let mut conflicts = 0;
560        let mut open = false;
561        while let Some(t) = stack.pop() {
562            let mine = flip[t].unwrap_or(false);
563            for h in 3 * t..3 * t + 3 {
564                let Some(g) = adjacency.twin[h] else {
565                    open = true;
566                    continue;
567                };
568                let other = g / 3;
569                // Agreeing windings run a shared edge opposite ways.
570                let same_way = from_to(triangles, h) == from_to(triangles, g);
571                let wanted = mine ^ same_way;
572                match flip[other] {
573                    None => {
574                        flip[other] = Some(wanted);
575                        members.push(other);
576                        stack.push(other);
577                    }
578                    Some(have) if have != wanted => conflicts += 1,
579                    Some(_) => {}
580                }
581            }
582        }
583        // Each conflicting edge was met from both sides.
584        conflicts /= 2;
585        report.orientation_conflicts += conflicts;
586        let flipped = members.iter().filter(|&&t| flip[t] == Some(true)).count();
587        let closed = !open && conflicts == 0;
588        let turn_all = if closed {
589            let volume: f64 = members
590                .iter()
591                .map(|&t| {
592                    let mut tri = triangles[t];
593                    if flip[t] == Some(true) {
594                        tri.swap(1, 2);
595                    }
596                    signed_volume(points, tri)
597                })
598                .sum();
599            volume < 0.0
600        } else {
601            flipped * 2 > members.len()
602        };
603        for &t in &members {
604            if flip[t].unwrap_or(false) ^ turn_all {
605                triangles[t].swap(1, 2);
606                report.windings_flipped += 1;
607            }
608        }
609        let mut members: Vec<u32> = members
610            .into_iter()
611            .map(|t| u32::try_from(t).unwrap_or(u32::MAX))
612            .collect();
613        members.sort_unstable();
614        pieces.push(Piece {
615            triangles: members,
616            closed,
617        });
618    }
619    pieces
620}
621
622fn signed_volume(points: &[Point], [a, b, c]: [u32; 3]) -> f64 {
623    let [a, b, c] = [a, b, c].map(|i| points[i as usize] - Point::ORIGIN);
624    a.dot(b.cross(c)) / 6.0
625}
626
627/// Whether piece `inner` lies inside closed piece `outer`: a ray from one
628/// of its vertices crosses `outer` an odd number of times.
629fn inside(points: &[Point], triangles: &[[u32; 3]], outer: &Piece, inner: &Piece) -> bool {
630    let Some(&first) = inner.triangles.first() else {
631        return false;
632    };
633    let origin = points[triangles[first as usize][0] as usize];
634    // An off-axis direction, so a ray along a mesh's grid lines is unlikely.
635    let direction = Vector::new(0.577_215_664_9, 0.618_033_988_7, 0.533_751_168_7);
636    let mut crossings = 0;
637    for &t in &outer.triangles {
638        let [a, b, c] = triangles[t as usize].map(|i| points[i as usize]);
639        let (e1, e2) = (b - a, c - a);
640        let p = direction.cross(e2);
641        let det = e1.dot(p);
642        if det.abs() < 1e-300 {
643            continue;
644        }
645        let s = origin - a;
646        let u = s.dot(p) / det;
647        if !(0.0..=1.0).contains(&u) {
648            continue;
649        }
650        let q = s.cross(e1);
651        let v = direction.dot(q) / det;
652        if v < 0.0 || u + v > 1.0 {
653            continue;
654        }
655        if e2.dot(q) / det > 0.0 {
656            crossings += 1;
657        }
658    }
659    crossings % 2 == 1
660}
661
662/// What a face is built on.
663#[derive(Debug, Clone)]
664enum Carrier {
665    /// A plane, its normal outward.
666    Plane(Plane),
667    /// A surface recognition decided the region is.
668    Curved(Curved),
669    /// A region demoted to faceted, whose triangles went to other faces.
670    Gone,
671}
672
673#[derive(Debug, Clone)]
674struct Curved {
675    shape: Canonical,
676    /// How far the region's vertices stand off it.
677    deviation: f64,
678    /// The chart point every pcurve on it is unwrapped around, so they all
679    /// read on one branch.
680    centre: (f64, f64),
681    /// Whether the region runs all the way round its axis, and so needs a
682    /// seam across it.
683    wraps: bool,
684    /// Whether it runs all the way round a torus's tube, and so needs a
685    /// seam along it.
686    wraps_v: bool,
687    /// Whether the frame was set from the region's own boundary: a sphere
688    /// bounded by circles in parallel planes turns about their normal, so
689    /// they are its latitudes.
690    fixed: bool,
691    /// The region's mesh vertices.
692    vertices: Vec<u32>,
693}
694
695/// How a curved face's boundary lies in its chart.
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697enum Layout {
698    /// A patch inside one branch of the chart.
699    Open,
700    /// A band between two full circles, joined by a seam: round the axis,
701    /// or round a torus's tube.
702    Band { round_tube: bool },
703    /// A sphere's cap: one latitude circle, a seam to the pole, and the pole.
704    Cap,
705    /// The whole surface, with no boundary of its own.
706    Whole,
707    /// Round the axis between two rims of any shape, with holes: a seam
708    /// joins a vertex of each rim, straight in the chart and clear of the
709    /// holes.
710    Wrapped,
711    /// A sphere or a torus whole but for holes, its seams and poles clear
712    /// of them: the whole surface's face, the holes its inner wires.
713    Holed,
714}
715
716/// Triangles gathered into faces: which face each triangle is in, and what
717/// each face is built on.
718struct Groups {
719    of: Vec<usize>,
720    carriers: Vec<Carrier>,
721}
722
723/// The plane of a triangle, its normal by the winding, its `x` axis along
724/// the first side. The triangle has area, so both are defined.
725fn plane_of(points: &[Point], [a, b, c]: [u32; 3], tol: Tolerances) -> OgeomResult<Plane> {
726    let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
727    // Scaled to unit length before the kernel's own normalization, which
728    // refuses a vector shorter than the confusion distance, as the cross
729    // product of a small triangle's sides may be.
730    let normal = (b - a).cross(c - a);
731    let z = Direction::new(normal / normal.magnitude(), tol)?;
732    let x = Direction::new((b - a) / (b - a).magnitude(), tol)?;
733    Ok(Plane::new(Frame::new(a, z, x, tol)?))
734}
735
736fn unit_normal(points: &[Point], [a, b, c]: [u32; 3]) -> Vector {
737    let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
738    let n = (b - a).cross(c - a);
739    n / n.magnitude()
740}
741
742/// Grow planar faces across shared edges from the largest triangles down,
743/// among the triangles no face holds yet, taking a neighbour whose normal
744/// is within the angle of the face's and whose corners all lie within the
745/// distance of its plane. Measured against the face's own plane, not the
746/// last triangle's, so a gently curved surface does not drift into one
747/// face.
748fn coplanar_groups(
749    points: &[Point],
750    triangles: &[[u32; 3]],
751    adjacency: &Adjacency,
752    angle: f64,
753    flat: f64,
754    groups: &mut Groups,
755    tol: Tolerances,
756) -> OgeomResult<()> {
757    let area = |t: usize| {
758        let [a, b, c] = triangles[t].map(|i| points[i as usize]);
759        (b - a).cross(c - a).magnitude()
760    };
761    let mut order: Vec<usize> = (0..triangles.len())
762        .filter(|&t| groups.of[t] == usize::MAX)
763        .collect();
764    order.sort_by(|&x, &y| area(y).total_cmp(&area(x)));
765    let cos = angle.cos();
766    let mut stack = Vec::new();
767    for seed in order {
768        if groups.of[seed] != usize::MAX {
769            continue;
770        }
771        let g = groups.carriers.len();
772        let plane = plane_of(points, triangles[seed], tol)?;
773        let (origin, normal) = (plane.frame().origin(), plane.frame().z().vector());
774        groups.carriers.push(Carrier::Plane(plane));
775        groups.of[seed] = g;
776        stack.push(seed);
777        while let Some(t) = stack.pop() {
778            for h in 3 * t..3 * t + 3 {
779                let Some(twin) = adjacency.twin[h] else {
780                    continue;
781                };
782                let other = twin / 3;
783                if groups.of[other] != usize::MAX {
784                    continue;
785                }
786                let [a, b, c] = triangles[other].map(|i| points[i as usize]);
787                let n = (b - a).cross(c - a);
788                if n.dot(normal) < cos * n.magnitude() {
789                    continue;
790                }
791                if [a, b, c]
792                    .iter()
793                    .all(|p| (*p - origin).dot(normal).abs() <= flat)
794                {
795                    groups.of[other] = g;
796                    stack.push(other);
797                }
798            }
799        }
800    }
801    Ok(())
802}
803
804/// One face per triangle, for the triangles no face holds yet.
805fn one_each(
806    points: &[Point],
807    triangles: &[[u32; 3]],
808    groups: &mut Groups,
809    tol: Tolerances,
810) -> OgeomResult<()> {
811    for (t, triangle) in triangles.iter().enumerate() {
812        if groups.of[t] == usize::MAX {
813            groups.of[t] = groups.carriers.len();
814            groups
815                .carriers
816                .push(Carrier::Plane(plane_of(points, *triangle, tol)?));
817        }
818    }
819    Ok(())
820}
821
822/// The direction of a canonical surface's own normal at a point near it,
823/// not normalized: the gradient of its distance.
824/// How far a sample's normals must turn before the smallest sample stage
825/// is fitted: twenty degrees.
826const SMALL_STAGE_TURN: f64 = 0.35;
827
828/// The widest angle between any two of a sample's normals.
829fn turn_of(normals: &[Vector]) -> f64 {
830    let mut widest: f64 = 0.0;
831    for (i, a) in normals.iter().enumerate() {
832        for b in &normals[i + 1..] {
833            widest = widest.max(a.dot(*b).clamp(-1.0, 1.0).acos());
834        }
835    }
836    widest
837}
838
839/// Whether a facet whose corners lie on `shape` leans only as the surface
840/// turns under it: its normal within the spread of the surface's normals
841/// at its corners (and [`FACET_LEAN`]) of their mean, the surface turning
842/// no more than sixty degrees under it. A facet of a coarse mesh over a
843/// tight bend passes however far it turns from its neighbours; a flat
844/// cap's triangle with its corners on a cylinder's rim, square to every
845/// one of those normals, does not.
846fn leans_as_the_surface(shape: &Canonical, corners: [Point; 3], normal: Vector) -> bool {
847    let mut at = [Vector::ZERO; 3];
848    for (n, p) in at.iter_mut().zip(corners) {
849        let g = gradient(shape, p);
850        let m = g.magnitude();
851        if m == 0.0 {
852            return false;
853        }
854        *n = if g.dot(normal) < 0.0 { -g / m } else { g / m };
855    }
856    let angle = |a: Vector, b: Vector| a.dot(b).clamp(-1.0, 1.0).acos();
857    let spread = angle(at[0], at[1])
858        .max(angle(at[1], at[2]))
859        .max(angle(at[0], at[2]));
860    // A facet of any mesh turns far less than this across itself; one that
861    // spans more is a chord across the surface, not a piece of it.
862    if spread > FACET_TURN {
863        return false;
864    }
865    let mean = at[0] + at[1] + at[2];
866    let m = mean.magnitude();
867    m > 0.0 && angle(mean / m, normal) <= spread + FACET_LEAN
868}
869
870/// How far a facet's normal may lean past the spread of the surface's
871/// normals at its corners: a skewed triangle across a cylinder, joining
872/// points at different heights and angles, tilts along the axis out of the
873/// plane its corners' normals span. About eleven degrees.
874const FACET_LEAN: f64 = 0.2;
875
876/// The most a surface may turn under one facet: sixty degrees.
877const FACET_TURN: f64 = core::f64::consts::FRAC_PI_3;
878
879fn gradient(shape: &Canonical, p: Point) -> Vector {
880    let radial = |o: Point, z: Vector| {
881        let w = p - o;
882        let r = w - z * w.dot(z);
883        let m = r.magnitude();
884        (if m > 0.0 { r / m } else { Vector::ZERO }, w.dot(z))
885    };
886    match shape {
887        Canonical::Plane(plane) => plane.frame().z().vector(),
888        Canonical::Cylinder(c) => radial(c.frame().origin(), c.frame().z().vector()).0,
889        Canonical::Cone(c) => {
890            let z = c.frame().z().vector();
891            let (out, _) = radial(c.frame().origin(), z);
892            out - z * c.half_angle().tan()
893        }
894        Canonical::Sphere(s) => p - s.centre(),
895        Canonical::Torus(t) => {
896            let z = t.frame().z().vector();
897            let (out, _) = radial(t.frame().origin(), z);
898            p - (t.frame().origin() + out * t.major_radius())
899        }
900    }
901}
902
903/// The axis of a surface of revolution: its frame.
904fn axis_frame(shape: &Canonical) -> Option<Frame> {
905    match shape {
906        Canonical::Cylinder(c) => Some(c.frame()),
907        Canonical::Cone(c) => Some(c.frame()),
908        Canonical::Torus(t) => Some(t.frame()),
909        Canonical::Sphere(s) => Some(s.frame()),
910        Canonical::Plane(_) => None,
911    }
912}
913
914/// The same surface on another frame whose axis is the same line: its
915/// radii kept, a cone's reference radius carried to the new origin.
916fn on_frame(shape: &Canonical, frame: Frame, tol: Tolerances) -> Option<Canonical> {
917    Some(match shape {
918        Canonical::Cylinder(c) => Canonical::Cylinder(Cylinder::new(frame, c.radius(), tol).ok()?),
919        Canonical::Cone(c) => {
920            // The new origin's height on the old axis, and which way the
921            // new axis runs against the old.
922            let old = c.frame();
923            let shift = (frame.origin() - old.origin()).dot(old.z().vector());
924            let same = frame.z().vector().dot(old.z().vector()) > 0.0;
925            if !same {
926                return None;
927            }
928            let r0 = c.radius_at(shift);
929            Canonical::Cone(Cone::new(frame, r0.max(tol.confusion()), c.half_angle(), tol).ok()?)
930        }
931        Canonical::Torus(t) => {
932            Canonical::Torus(Torus::new(frame, t.major_radius(), t.minor_radius(), tol).ok()?)
933        }
934        Canonical::Sphere(s) => Canonical::Sphere(Sphere::new(frame, s.radius(), tol).ok()?),
935        Canonical::Plane(_) => return None,
936    })
937}
938
939/// A point's raw chart coordinates on a canonical surface.
940fn chart(shape: &Canonical, p: Point, tol: Tolerances) -> Option<(f64, f64)> {
941    use ogeom_math::elementary as e;
942    match shape {
943        Canonical::Plane(plane) => Some(e::plane_parameters(plane, p)),
944        Canonical::Cylinder(c) => e::cylinder_parameters(c, p, tol).ok(),
945        Canonical::Cone(c) => e::cone_parameters(c, p, tol).ok(),
946        Canonical::Sphere(s) => e::sphere_parameters(s, p, tol).ok(),
947        Canonical::Torus(t) => e::torus_parameters(t, p, tol).ok(),
948    }
949}
950
951fn evaluate(shape: &Canonical, (u, v): (f64, f64)) -> Point {
952    use ogeom_math::elementary as e;
953    match shape {
954        Canonical::Plane(plane) => e::plane_at(plane, u, v).point,
955        Canonical::Cylinder(c) => e::cylinder_at(c, u, v).point,
956        Canonical::Cone(c) => e::cone_at(c, u, v).point,
957        Canonical::Sphere(s) => e::sphere_at(s, u, v).point,
958        Canonical::Torus(t) => e::torus_at(t, u, v).point,
959    }
960}
961
962/// Which chart directions are angles, and so wrap.
963fn periodic(shape: &Canonical) -> (bool, bool) {
964    match shape {
965        Canonical::Plane(_) => (false, false),
966        Canonical::Cylinder(_) | Canonical::Cone(_) | Canonical::Sphere(_) => (true, false),
967        Canonical::Torus(_) => (true, true),
968    }
969}
970
971/// Chart coordinates on the branch around `centre`.
972fn unwrapped(curved: &Curved, p: Point, tol: Tolerances) -> Option<(f64, f64)> {
973    let (u, v) = chart(&curved.shape, p, tol)?;
974    let (pu, pv) = periodic(&curved.shape);
975    let near = |x: f64, c: f64, wraps: bool| {
976        if wraps {
977            c + ogeom_math::elementary::wrap_signed_angle(x - c)
978        } else {
979            x
980        }
981    };
982    Some((near(u, curved.centre.0, pu), near(v, curved.centre.1, pv)))
983}
984
985/// The circular mean of angles, and the widest gap between them.
986fn angular_spread(angles: &mut [f64]) -> (f64, f64) {
987    let (s, c) = angles
988        .iter()
989        .fold((0.0, 0.0), |(s, c), a| (s + a.sin(), c + a.cos()));
990    angles.sort_by(f64::total_cmp);
991    let mut gap: f64 = 0.0;
992    for w in angles.windows(2) {
993        gap = gap.max(w[1] - w[0]);
994    }
995    if let (Some(first), Some(last)) = (angles.first(), angles.last()) {
996        gap = gap.max(first + core::f64::consts::TAU - last);
997    }
998    (s.atan2(c), gap)
999}
1000
1001/// Grow regions of triangles that recognition says lie on one curved
1002/// canonical surface, then planar faces over the rest.
1003///
1004/// A region starts at a triangle with a curved edge (one across which the
1005/// surface turns by less than the crease angle but more than the coplanar
1006/// angle) and first grows across such edges only, which keeps a flat face
1007/// tangent to a fillet out of the fillet's first samples. Once it holds
1008/// enough vertices it is recognized, and from then on grows across any
1009/// smooth edge to a triangle whose corners lie on the surface and whose
1010/// normal agrees with it, the surface refitted to everything held as the
1011/// region doubles. A region whose samples are free-form, or also flat,
1012/// returns its triangles to the planar pass.
1013#[allow(clippy::too_many_arguments, reason = "the segmentation's inputs")]
1014fn segment(
1015    points: &[Point],
1016    triangles: &[[u32; 3]],
1017    adjacency: &Adjacency,
1018    options: &MeshSolidOptions,
1019    flat: f64,
1020    tol: Tolerances,
1021) -> OgeomResult<Groups> {
1022    let n = triangles.len();
1023    let mut groups = Groups {
1024        of: vec![usize::MAX; n],
1025        carriers: Vec::new(),
1026    };
1027    if !options.merge_coplanar {
1028        one_each(points, triangles, &mut groups, tol)?;
1029        return Ok(groups);
1030    }
1031    if options.recognize {
1032        recognized_regions(
1033            points,
1034            triangles,
1035            adjacency,
1036            options,
1037            flat,
1038            &mut groups,
1039            tol,
1040        );
1041        sphere_axes(points, triangles, adjacency, &mut groups, flat, tol);
1042        align_axes(points, &mut groups, flat, tol);
1043        hole_frames(points, triangles, adjacency, &mut groups, tol);
1044        slit_bands(points, triangles, adjacency, &mut groups, tol);
1045    }
1046    coplanar_groups(
1047        points,
1048        triangles,
1049        adjacency,
1050        options.coplanar_angle,
1051        flat,
1052        &mut groups,
1053        tol,
1054    )?;
1055    Ok(groups)
1056}
1057
1058/// What a seed's first samples came to: the triangles and vertices
1059/// gathered, those fitted, how many triangles the smallest sample held, the
1060/// fit if one held, with which of the fitted vertices it kept.
1061struct FirstFit {
1062    region: Vec<usize>,
1063    vertices: Vec<u32>,
1064    shared: Vec<u32>,
1065    first_sample: usize,
1066    /// Whether the first sample already turned through a tight bend: on a
1067    /// coarse mesh it can span several surfaces, and a failed fit says
1068    /// nothing about the triangles in it but the seed.
1069    wide: bool,
1070    found: Option<(crate::recognize::Recognized, Vec<bool>)>,
1071}
1072
1073/// The mesh as recognition reads it.
1074struct Surfaces<'a> {
1075    points: &'a [Point],
1076    triangles: &'a [[u32; 3]],
1077    adjacency: &'a Adjacency,
1078    normals: Vec<Vector>,
1079    cos_crease: f64,
1080    cos_flat: f64,
1081    flat: f64,
1082    tol: Tolerances,
1083}
1084
1085impl Surfaces<'_> {
1086    fn turn(&self, h: Half) -> Option<f64> {
1087        self.adjacency.twin[h].map(|g| self.normals[h / 3].dot(self.normals[g / 3]))
1088    }
1089
1090    fn curved(&self, h: Half) -> bool {
1091        self.turn(h)
1092            .is_some_and(|c| c >= self.cos_crease && c < self.cos_flat)
1093    }
1094
1095    fn smooth(&self, h: Half) -> bool {
1096        self.turn(h).is_some_and(|c| c >= self.cos_crease)
1097    }
1098
1099    fn bends(&self, t: usize) -> bool {
1100        (3 * t..3 * t + 3).any(|h| self.curved(h))
1101    }
1102
1103    /// Sample points with normals averaged over the region's triangles.
1104    fn samples(&self, vertices: &[u32], region: &[usize]) -> (Vec<Point>, Vec<Vector>) {
1105        let mut sum: HashMap<u32, Vector> = HashMap::with_capacity(vertices.len());
1106        for &t in region {
1107            for &v in &self.triangles[t] {
1108                *sum.entry(v).or_insert(Vector::ZERO) += self.normals[t];
1109            }
1110        }
1111        let pts = vertices.iter().map(|&v| self.points[v as usize]).collect();
1112        let nrm = vertices
1113            .iter()
1114            .map(|v| {
1115                let s = sum.get(v).copied().unwrap_or(Vector::Z);
1116                let m = s.magnitude();
1117                if m > 0.0 { s / m } else { Vector::Z }
1118            })
1119            .collect();
1120        (pts, nrm)
1121    }
1122
1123    /// The region's edges as segments, a few hundred at most, evenly
1124    /// through the region.
1125    fn chords(&self, region: &[usize]) -> Vec<(Point, Point)> {
1126        let stride = region.len().div_ceil(100).max(1);
1127        region
1128            .iter()
1129            .step_by(stride)
1130            .flat_map(|&t| {
1131                let [a, b, c] = self.triangles[t].map(|v| self.points[v as usize]);
1132                [(a, b), (b, c), (c, a)]
1133            })
1134            .collect()
1135    }
1136
1137    /// A seed's first samples and their fit, read from where the faces and
1138    /// the retired seeds stand; it changes nothing.
1139    ///
1140    /// Samples grow across smooth edges into triangles where the surface
1141    /// is seen to bend, each with an edge across which it turns. Within a
1142    /// strip of a cylinder or a torus the two triangles of a cell meet
1143    /// flat, and each still bends across its other side; a flat face met
1144    /// tangentially (a fillet's run-out) lends only its triangles along
1145    /// the tangent line, whose far corners the trimmed fit drops. The fit
1146    /// is tried as the sample grows: a narrow fillet fits from a few dozen
1147    /// vertices and would take in its neighbours' by a hundred; a patch of a
1148    /// thick torus needs the hundred to show its tube.
1149    fn first_fit(&self, seed: usize, of: &[usize], tried: &[bool]) -> FirstFit {
1150        const STAGES: [usize; 4] = [12, 24, 60, 150];
1151        let mut held: std::collections::HashSet<usize> = std::collections::HashSet::from([seed]);
1152        let mut seen: std::collections::HashSet<u32> = std::collections::HashSet::new();
1153        let mut region = vec![seed];
1154        let mut vertices: Vec<u32> = Vec::new();
1155        let take =
1156            |t: usize, vertices: &mut Vec<u32>, seen: &mut std::collections::HashSet<u32>| {
1157                for &v in &self.triangles[t] {
1158                    if seen.insert(v) {
1159                        vertices.push(v);
1160                    }
1161                }
1162            };
1163        take(seed, &mut vertices, &mut seen);
1164        let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::from([seed]);
1165        let mut found = None;
1166        let mut shared = Vec::new();
1167        let mut first_sample = usize::MAX;
1168        let mut wide = false;
1169        for target in STAGES {
1170            while vertices.len() < target {
1171                let Some(next) = queue.pop_front() else {
1172                    break;
1173                };
1174                for h in 3 * next..3 * next + 3 {
1175                    let Some(g) = self.adjacency.twin[h] else {
1176                        continue;
1177                    };
1178                    let other = g / 3;
1179                    if !self.smooth(h) || held.contains(&other) {
1180                        continue;
1181                    }
1182                    if of[other] != usize::MAX
1183                        || tried[other]
1184                        || !(self.curved(h) || self.bends(other))
1185                    {
1186                        continue;
1187                    }
1188                    held.insert(other);
1189                    region.push(other);
1190                    take(other, &mut vertices, &mut seen);
1191                    queue.push_back(other);
1192                }
1193            }
1194            // Fitted on the vertices two or more of the sampled triangles
1195            // share: a corner of a flat face across a tangent line is
1196            // touched by the one triangle that reached it, and a single
1197            // such corner far off the surface decides a least-squares axis.
1198            shared = {
1199                let mut count: HashMap<u32, u32> = HashMap::with_capacity(vertices.len());
1200                for &t in &region {
1201                    for &v in &self.triangles[t] {
1202                        *count.entry(v).or_insert(0) += 1;
1203                    }
1204                }
1205                vertices
1206                    .iter()
1207                    .copied()
1208                    .filter(|v| count[v] >= 2)
1209                    .collect::<Vec<u32>>()
1210            };
1211            first_sample = first_sample.min(region.len());
1212            if target == STAGES[0] {
1213                let normals: Vec<Vector> = region.iter().map(|&t| self.normals[t]).collect();
1214                wide = turn_of(&normals) >= SMALL_STAGE_TURN;
1215            }
1216            if shared.len() < 8 {
1217                if queue.is_empty() {
1218                    break;
1219                }
1220                continue;
1221            }
1222            let (pts, nrm) = self.samples(&shared, &region);
1223            // The first, smallest stage is for a coarse mesh, where a dozen
1224            // vertices already span a tight bend; on a fine one they lie
1225            // nearly flat and say little about the surface they are on.
1226            if target == STAGES[0] && !wide {
1227                continue;
1228            }
1229            let chords = self.chords(&region);
1230            match crate::recognize::recognize_trimmed(&pts, &nrm, &chords, self.flat, self.tol) {
1231                Ok(fit) => {
1232                    found = Some(fit);
1233                    break;
1234                }
1235                // A larger sample is worth fitting only where this one came
1236                // near for its size (within a hundredth of its own span),
1237                // as a small patch of a thick torus does, its tube not yet
1238                // seen; a free-form patch misses by more, and is left.
1239                Err(closest) if closest > span(&pts) * 1e-2 || queue.is_empty() => break,
1240                Err(_) => {}
1241            }
1242        }
1243        FirstFit {
1244            region,
1245            vertices,
1246            shared,
1247            first_sample,
1248            wide,
1249            found,
1250        }
1251    }
1252}
1253
1254/// Grow the recognized regions, seed by seed in triangle order.
1255///
1256/// A seed's first fit is the costly part, and it only reads: so seeds are
1257/// fitted a batch at a time in parallel against where things stand, then
1258/// taken in order, and a seed whose gathering took a triangle an earlier
1259/// seed of the same batch has since claimed or retired is fitted again.
1260/// The answer is the one seed-by-seed order gives, at any thread count.
1261#[allow(clippy::too_many_lines, reason = "one growth, read in one place")]
1262fn recognized_regions(
1263    points: &[Point],
1264    triangles: &[[u32; 3]],
1265    adjacency: &Adjacency,
1266    options: &MeshSolidOptions,
1267    flat: f64,
1268    groups: &mut Groups,
1269    tol: Tolerances,
1270) {
1271    let n = triangles.len();
1272    let mesh = Surfaces {
1273        points,
1274        triangles,
1275        adjacency,
1276        normals: triangles.iter().map(|t| unit_normal(points, *t)).collect(),
1277        cos_crease: options.crease.cos(),
1278        cos_flat: options.coplanar_angle.cos(),
1279        flat,
1280        tol,
1281    };
1282    // A facet of a coarse mesh leans from the surface at its centre by up
1283    // to half the turn between facets, which the crease bounds.
1284    let agree = options.crease.cos();
1285    let mut tried = vec![false; n];
1286    // The batch in which each triangle's standing last changed.
1287    let mut changed = vec![0_u32; n];
1288    let batch_size = ogeom_core::parallel::threads().max(1) * 4;
1289    let mut batch = 0_u32;
1290    let eligible =
1291        |t: usize, of: &[usize], tried: &[bool]| of[t] == usize::MAX && !tried[t] && mesh.bends(t);
1292    // Seeds are taken in a fixed stride through the triangles rather than
1293    // one after another: a mesh lists neighbours together, and a batch of
1294    // neighbouring seeds would mostly gather what the first of them took.
1295    // The stride is a prime not dividing the count, so every triangle comes
1296    // up once.
1297    let stride = [7919_usize, 7907, 7901]
1298        .into_iter()
1299        .find(|p| !n.is_multiple_of(*p))
1300        .unwrap_or(1);
1301    let order: Vec<usize> = (0..n).map(|i| (i * stride) % n).collect();
1302    let mut next = 0;
1303    while next < n {
1304        batch += 1;
1305        let mut seeds = Vec::with_capacity(batch_size);
1306        while next < n && seeds.len() < batch_size {
1307            if eligible(order[next], &groups.of, &tried) {
1308                seeds.push(order[next]);
1309            }
1310            next += 1;
1311        }
1312        let fits = {
1313            let (of, tried) = (&groups.of, &tried);
1314            ogeom_core::parallel::map_ordered(&seeds, |_, &seed| mesh.first_fit(seed, of, tried))
1315        };
1316        for (seed, fit) in seeds.into_iter().zip(fits) {
1317            if !eligible(seed, &groups.of, &tried) {
1318                continue;
1319            }
1320            // A triangle's standing only moves one way (free to taken), so
1321            // a triangle the gathering passed over stays passed over, and
1322            // only one it took can have changed what it gathers.
1323            let fit = if fit.region.iter().any(|&t| changed[t] == batch) {
1324                mesh.first_fit(seed, &groups.of, &tried)
1325            } else {
1326                fit
1327            };
1328            let FirstFit {
1329                mut region,
1330                mut vertices,
1331                shared,
1332                first_sample,
1333                wide,
1334                found,
1335                ..
1336            } = fit;
1337            let Some((found, keep)) = found else {
1338                // A fine sample that fits nothing lies on nothing canonical,
1339                // and its triangles are not seeded again; a wide one may
1340                // have straddled a fillet and its neighbours, and only the
1341                // seed is retired.
1342                let retired = if wide { 1 } else { first_sample };
1343                for &t in &region[..retired.min(region.len())] {
1344                    tried[t] = true;
1345                    changed[t] = batch;
1346                }
1347                continue;
1348            };
1349            // What the fit dropped, and what it never saw but misses the
1350            // fit: those vertices go, and the triangles that brought them.
1351            let kept: HashMap<u32, bool> = shared.iter().copied().zip(keep).collect();
1352            let dropped: std::collections::HashSet<u32> = vertices
1353                .iter()
1354                .copied()
1355                .filter(|v| {
1356                    !kept
1357                        .get(v)
1358                        .copied()
1359                        .unwrap_or_else(|| found.surface.distance_to(points[*v as usize]) <= flat)
1360                })
1361                .collect();
1362            if !dropped.is_empty() {
1363                let gathered = region.clone();
1364                region.retain(|&t| triangles[t].iter().all(|v| !dropped.contains(v)));
1365                if region.is_empty() {
1366                    for &t in &gathered[..first_sample.min(gathered.len())] {
1367                        tried[t] = true;
1368                        changed[t] = batch;
1369                    }
1370                    continue;
1371                }
1372                let mut seen = std::collections::HashSet::new();
1373                vertices.clear();
1374                for &t in &region {
1375                    for &v in &triangles[t] {
1376                        if seen.insert(v) {
1377                            vertices.push(v);
1378                        }
1379                    }
1380                }
1381            }
1382            let mut shape = found.surface;
1383            let mut mine: std::collections::HashSet<usize> = region.iter().copied().collect();
1384            let mut seen: std::collections::HashSet<u32> = vertices.iter().copied().collect();
1385
1386            // Then across any smooth edge, while the surface holds. A fit
1387            // from a small patch extrapolates only so far; when the growth
1388            // stalls with vertices gained since the last fit, the surface is
1389            // refitted to everything held and the rim tried again.
1390            let mut fitted_at = vertices.len();
1391            loop {
1392                let mut i = 0;
1393                while i < region.len() {
1394                    let t = region[i];
1395                    i += 1;
1396                    for h in 3 * t..3 * t + 3 {
1397                        let Some(g) = adjacency.twin[h] else {
1398                            continue;
1399                        };
1400                        let other = g / 3;
1401                        if mine.contains(&other) || groups.of[other] != usize::MAX {
1402                            continue;
1403                        }
1404                        let corners = triangles[other].map(|v| points[v as usize]);
1405                        if corners.iter().any(|p| shape.distance_to(*p) > flat) {
1406                            continue;
1407                        }
1408                        // Across a smooth edge, the facet's normal agrees
1409                        // with the surface's. Across a sharper one (a coarse
1410                        // mesh spanning two rows of a small fillet in one
1411                        // triangle), the surface must account for the whole
1412                        // lean.
1413                        if mesh.smooth(h) {
1414                            let centroid = Point::from_vector(
1415                                (corners[0].to_vector()
1416                                    + corners[1].to_vector()
1417                                    + corners[2].to_vector())
1418                                    / 3.0,
1419                            );
1420                            let direction = gradient(&shape, centroid);
1421                            let m = direction.magnitude();
1422                            if m == 0.0 || (direction.dot(mesh.normals[other]) / m).abs() < agree {
1423                                continue;
1424                            }
1425                        } else if !leans_as_the_surface(&shape, corners, mesh.normals[other])
1426                            || !sags_as_the_surface(&shape, corners, flat)
1427                        {
1428                            continue;
1429                        }
1430                        mine.insert(other);
1431                        region.push(other);
1432                        for &v in &triangles[other] {
1433                            if seen.insert(v) {
1434                                vertices.push(v);
1435                            }
1436                        }
1437                    }
1438                }
1439                if vertices.len() <= fitted_at {
1440                    break;
1441                }
1442                fitted_at = vertices.len();
1443                let (pts, nrm) = mesh.samples(&vertices, &region);
1444                match recognize_curved(&pts, &nrm, &mesh.chords(&region), flat, tol) {
1445                    Some(better) => shape = better.surface,
1446                    None => break,
1447                }
1448            }
1449            // A few triangles the region surrounds on every side, with their
1450            // corners on the surface, belong to it whatever their normals:
1451            // a sliver's plane through three nearly collinear points on the
1452            // surface tilts far from the surface's normal, as in the fans
1453            // round a sphere's pole, where several such slivers touch.
1454            let mut claimed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1455            let rim: Vec<usize> = region
1456                .iter()
1457                .flat_map(|&t| (3 * t..3 * t + 3).filter_map(|h| adjacency.twin[h]))
1458                .map(|g| g / 3)
1459                .filter(|&other| !mine.contains(&other) && groups.of[other] == usize::MAX)
1460                .collect();
1461            for start in rim {
1462                if claimed.contains(&start) {
1463                    continue;
1464                }
1465                let mut cluster = vec![start];
1466                let mut inside: std::collections::HashSet<usize> =
1467                    std::collections::HashSet::from([start]);
1468                let mut surrounded = true;
1469                let mut i = 0;
1470                while i < cluster.len() && surrounded {
1471                    let t = cluster[i];
1472                    i += 1;
1473                    for h in 3 * t..3 * t + 3 {
1474                        let Some(g) = adjacency.twin[h] else {
1475                            surrounded = false;
1476                            break;
1477                        };
1478                        let next = g / 3;
1479                        if mine.contains(&next) || inside.contains(&next) {
1480                            continue;
1481                        }
1482                        if groups.of[next] != usize::MAX || cluster.len() >= ENCLOSED_CLUSTER {
1483                            surrounded = false;
1484                            break;
1485                        }
1486                        inside.insert(next);
1487                        cluster.push(next);
1488                    }
1489                }
1490                let on_surface = cluster.iter().all(|&t| {
1491                    let corners = triangles[t].map(|v| points[v as usize]);
1492                    corners.iter().all(|p| shape.distance_to(*p) <= flat)
1493                        && sags_as_the_surface(&shape, corners, flat)
1494                        && (is_sliver(corners)
1495                            || leans_as_the_surface(&shape, corners, mesh.normals[t]))
1496                });
1497                if surrounded && on_surface {
1498                    for t in cluster {
1499                        claimed.insert(t);
1500                        if mine.insert(t) {
1501                            region.push(t);
1502                        }
1503                    }
1504                }
1505            }
1506            let pts: Vec<Point> = vertices.iter().map(|&v| points[v as usize]).collect();
1507            let deviation = worst_deviation(&shape, &pts);
1508            let flat_too = crate::recognize::is_flat(&pts, flat, tol);
1509            if deviation > flat || flat_too || region.len() < 2 {
1510                for &t in &region {
1511                    tried[t] = true;
1512                    changed[t] = batch;
1513                }
1514                continue;
1515            }
1516            let g = groups.carriers.len();
1517            for &t in &region {
1518                groups.of[t] = g;
1519                changed[t] = batch;
1520            }
1521            groups.carriers.push(Carrier::Curved(Curved {
1522                shape,
1523                deviation,
1524                centre: (0.0, 0.0),
1525                wraps: false,
1526                wraps_v: false,
1527                fixed: false,
1528                vertices,
1529            }));
1530        }
1531    }
1532}
1533
1534/// The largest distance between two of the points, from the first.
1535fn span(points: &[Point]) -> f64 {
1536    points.first().map_or(0.0, |a| {
1537        points.iter().map(|p| p.distance(*a)).fold(0.0, f64::max)
1538    })
1539}
1540
1541/// Turn each recognized sphere whose boundary is circles in parallel planes
1542/// about their common normal, so the circles are latitudes and the face a
1543/// cap or a zone about a pole; the axis points into a cap, so its pole is
1544/// the frame's north one. A sphere with no boundary is whole and keeps its
1545/// frame, and one bounded otherwise keeps it for the re-framing that moves
1546/// its poles away.
1547fn sphere_axes(
1548    points: &[Point],
1549    triangles: &[[u32; 3]],
1550    adjacency: &Adjacency,
1551    groups: &mut Groups,
1552    flat: f64,
1553    tol: Tolerances,
1554) {
1555    let reach = flat * REACH;
1556    for g in 0..groups.carriers.len() {
1557        let Carrier::Curved(curved) = &groups.carriers[g] else {
1558            continue;
1559        };
1560        let Canonical::Sphere(sphere) = curved.shape else {
1561            continue;
1562        };
1563        let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1564            continue;
1565        };
1566        let mut axis: Option<Vector> = None;
1567        let mut planar = true;
1568        for ring in &loops {
1569            let pts: Vec<Point> = ring.iter().map(|&v| points[v as usize]).collect();
1570            let Some((through, normal)) =
1571                (pts.len() >= 3).then(|| plane_through(&pts, tol)).flatten()
1572            else {
1573                planar = false;
1574                break;
1575            };
1576            let n = normal.vector();
1577            if pts.iter().any(|p| (*p - through).dot(n).abs() > reach) {
1578                planar = false;
1579                break;
1580            }
1581            match axis {
1582                None => axis = Some(n),
1583                Some(a) if a.cross(n).magnitude() <= 1e-3 => {}
1584                Some(_) => {
1585                    planar = false;
1586                    break;
1587                }
1588            }
1589        }
1590        let (true, Some(mut z)) = (planar, axis) else {
1591            continue;
1592        };
1593        // Into the region: the pole a cap covers is the north one.
1594        let side: f64 = curved
1595            .vertices
1596            .iter()
1597            .map(|&v| (points[v as usize] - sphere.centre()).dot(z))
1598            .sum();
1599        if side < 0.0 {
1600            z = -z;
1601        }
1602        let Ok(z) = Direction::new(z, tol) else {
1603            continue;
1604        };
1605        let Ok(frame) = Frame::new(sphere.centre(), z, z.any_perpendicular(), tol) else {
1606            continue;
1607        };
1608        let Ok(turned) = Sphere::new(frame, sphere.radius(), tol) else {
1609            continue;
1610        };
1611        if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1612            curved.shape = Canonical::Sphere(turned);
1613            curved.fixed = true;
1614        }
1615    }
1616}
1617
1618/// A region's boundary, walked into loops of mesh vertices: each border
1619/// half-edge leads to the one leaving its end. `None` where a vertex has
1620/// more than one way on.
1621fn border_loops(
1622    triangles: &[[u32; 3]],
1623    adjacency: &Adjacency,
1624    of: &[usize],
1625    g: usize,
1626) -> Option<Vec<Vec<u32>>> {
1627    let mut leaving: HashMap<u32, Vec<u32>> = HashMap::new();
1628    for (t, tri) in triangles.iter().enumerate() {
1629        if of[t] != g {
1630            continue;
1631        }
1632        for k in 0..3 {
1633            let inside = adjacency.twin[3 * t + k].is_some_and(|o| of[o / 3] == g);
1634            if !inside {
1635                leaving.entry(tri[k]).or_default().push(tri[(k + 1) % 3]);
1636            }
1637        }
1638    }
1639    if leaving.is_empty() || leaving.values().any(|to| to.len() != 1) {
1640        return None;
1641    }
1642    let mut loops: Vec<Vec<u32>> = Vec::new();
1643    let mut done: std::collections::HashSet<u32> = std::collections::HashSet::new();
1644    let mut starts: Vec<u32> = leaving.keys().copied().collect();
1645    starts.sort_unstable();
1646    for start in starts {
1647        if !done.insert(start) {
1648            continue;
1649        }
1650        let mut ring = vec![start];
1651        let mut at = leaving[&start][0];
1652        while at != start && ring.len() <= leaving.len() {
1653            done.insert(at);
1654            ring.push(at);
1655            at = leaving.get(&at).map_or(start, |to| to[0]);
1656        }
1657        loops.push(ring);
1658    }
1659    Some(loops)
1660}
1661
1662/// A frame for each closed surface its boundary only makes holes in: a
1663/// sphere round its axis whose rings are not the parallels of one axis, or
1664/// a torus round both ways. Its seams are placed clear of every ring, the
1665/// sphere's poles as far from them as any axis puts them, so the whole
1666/// surface's face can carry the rings as holes.
1667fn hole_frames(
1668    points: &[Point],
1669    triangles: &[[u32; 3]],
1670    adjacency: &Adjacency,
1671    groups: &mut Groups,
1672    tol: Tolerances,
1673) {
1674    for g in 0..groups.carriers.len() {
1675        let Carrier::Curved(curved) = &groups.carriers[g] else {
1676            continue;
1677        };
1678        let wanted = match curved.shape {
1679            Canonical::Sphere(_) => curved.wraps && !curved.fixed,
1680            Canonical::Torus(_) => curved.wraps && curved.wraps_v,
1681            _ => false,
1682        };
1683        if !wanted {
1684            continue;
1685        }
1686        let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1687            continue;
1688        };
1689        let ring_points: Vec<Point> = loops
1690            .iter()
1691            .flatten()
1692            .map(|&v| points[v as usize])
1693            .collect();
1694        let shape = match curved.shape {
1695            Canonical::Sphere(sphere) => {
1696                // The axis whose poles stand farthest from every ring point,
1697                // of those no ring goes round: a pole inside a hole is off
1698                // the face, however far it stands from the hole's edge.
1699                let unit = |p: Point| {
1700                    let d = p - sphere.centre();
1701                    let m = d.magnitude();
1702                    (m > 0.0).then(|| d / m)
1703                };
1704                let directions: Vec<Vector> = ring_points.iter().filter_map(|p| unit(*p)).collect();
1705                let rings: Vec<Vec<Vector>> = loops
1706                    .iter()
1707                    .map(|ring| {
1708                        ring.iter()
1709                            .filter_map(|&v| unit(points[v as usize]))
1710                            .collect()
1711                    })
1712                    .collect();
1713                let mut ranked: Vec<(f64, Vector)> = spread_directions(POLE_CANDIDATES)
1714                    .into_iter()
1715                    .map(|z| {
1716                        let nearest = directions
1717                            .iter()
1718                            .map(|d| d.dot(z).abs())
1719                            .fold(0.0_f64, f64::max);
1720                        (nearest, z)
1721                    })
1722                    .collect();
1723                ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
1724                // And both poles on the region itself: a loop no axis goes
1725                // round has both poles to one side of it, which may be the
1726                // hole's.
1727                let on_region = |p: Point| {
1728                    triangles
1729                        .iter()
1730                        .enumerate()
1731                        .map(|(t, tri)| {
1732                            let c = Point::from_vector(
1733                                (points[tri[0] as usize].to_vector()
1734                                    + points[tri[1] as usize].to_vector()
1735                                    + points[tri[2] as usize].to_vector())
1736                                    / 3.0,
1737                            );
1738                            (c.distance(p), t)
1739                        })
1740                        .min_by(|a, b| a.0.total_cmp(&b.0))
1741                        .is_some_and(|(_, t)| groups.of[t] == g)
1742                };
1743                let best = ranked.into_iter().find(|(_, z)| {
1744                    rings.iter().all(|ring| turns_about(ring, *z) == 0)
1745                        && on_region(sphere.centre() + *z * sphere.radius())
1746                        && on_region(sphere.centre() - *z * sphere.radius())
1747                });
1748                let Some((nearest, z)) = best else {
1749                    continue;
1750                };
1751                // A pole within a few degrees of a ring has no room round it.
1752                if nearest > POLE_CLEARANCE.cos() {
1753                    continue;
1754                }
1755                let Ok(z) = Direction::new(z, tol) else {
1756                    continue;
1757                };
1758                let Ok(frame) = Frame::new(sphere.centre(), z, z.any_perpendicular(), tol) else {
1759                    continue;
1760                };
1761                let Ok(turned) = Sphere::new(frame, sphere.radius(), tol) else {
1762                    continue;
1763                };
1764                Canonical::Sphere(turned)
1765            }
1766            other => other,
1767        };
1768        // Then the seam, turned about the axis into the widest angle the
1769        // rings leave free.
1770        let angles: Vec<Vec<(f64, f64)>> = vec![
1771            ring_points
1772                .iter()
1773                .filter_map(|p| chart(&shape, *p, tol))
1774                .collect(),
1775        ];
1776        let Some(free) = free_angle(&angles) else {
1777            continue;
1778        };
1779        let Some(frame) = axis_frame(&shape) else {
1780            continue;
1781        };
1782        let (x, y) = (frame.x().vector(), frame.y().vector());
1783        let Ok(x) = Direction::new(x * free.cos() + y * free.sin(), tol) else {
1784            continue;
1785        };
1786        let Ok(turned) = Frame::new(frame.origin(), frame.z(), x, tol) else {
1787            continue;
1788        };
1789        let Some(shape) = on_frame(&shape, turned, tol) else {
1790            continue;
1791        };
1792        if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1793            curved.shape = shape;
1794            curved.fixed = true;
1795            curved.centre = (core::f64::consts::PI, curved.centre.1);
1796        }
1797    }
1798}
1799
1800/// How many times a loop of directions goes round an axis.
1801fn turns_about(ring: &[Vector], z: Vector) -> i32 {
1802    let x = if z.x.abs() < 0.9 {
1803        Vector::X
1804    } else {
1805        Vector::Y
1806    };
1807    let x = x - z * x.dot(z);
1808    let y = z.cross(x);
1809    let angle = |d: &Vector| d.dot(y).atan2(d.dot(x));
1810    let mut turned = 0.0;
1811    for (i, d) in ring.iter().enumerate() {
1812        let next = &ring[(i + 1) % ring.len()];
1813        turned += ogeom_math::elementary::wrap_signed_angle(angle(next) - angle(d));
1814    }
1815    #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
1816    let turns = (turned / core::f64::consts::TAU).round() as i32;
1817    turns
1818}
1819
1820/// A region round its axis whose boundary is one loop going round it no
1821/// times is a band cut open along a slit (a few triangles its growth left
1822/// across it): no seam can join rims it does not have. It is built as a
1823/// patch instead, its chart cut placed in the slit, the widest gap in the
1824/// angles its vertices stand at.
1825fn slit_bands(
1826    points: &[Point],
1827    triangles: &[[u32; 3]],
1828    adjacency: &Adjacency,
1829    groups: &mut Groups,
1830    tol: Tolerances,
1831) {
1832    for g in 0..groups.carriers.len() {
1833        let Carrier::Curved(curved) = &groups.carriers[g] else {
1834            continue;
1835        };
1836        if !curved.wraps
1837            || curved.wraps_v
1838            || !matches!(curved.shape, Canonical::Cylinder(_) | Canonical::Cone(_))
1839        {
1840            continue;
1841        }
1842        let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1843            continue;
1844        };
1845        let [ring] = &loops[..] else {
1846            continue;
1847        };
1848        let Some(frame) = axis_frame(&curved.shape) else {
1849            continue;
1850        };
1851        let directions: Vec<Vector> = ring
1852            .iter()
1853            .map(|&v| points[v as usize] - frame.origin())
1854            .collect();
1855        if turns_about(&directions, frame.z().vector()) != 0 {
1856            continue;
1857        }
1858        let mut angles: Vec<f64> = curved
1859            .vertices
1860            .iter()
1861            .filter_map(|&v| chart(&curved.shape, points[v as usize], tol).map(|c| c.0))
1862            .collect();
1863        let Some(gap) = widest_gap(&mut angles) else {
1864            continue;
1865        };
1866        if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1867            curved.wraps = false;
1868            curved.centre = (
1869                ogeom_math::elementary::wrap_angle(gap + core::f64::consts::PI),
1870                curved.centre.1,
1871            );
1872        }
1873    }
1874}
1875
1876/// The middle of the widest gap between angles.
1877fn widest_gap(angles: &mut [f64]) -> Option<f64> {
1878    let tau = core::f64::consts::TAU;
1879    if angles.is_empty() {
1880        return None;
1881    }
1882    for a in angles.iter_mut() {
1883        *a = a.rem_euclid(tau);
1884    }
1885    angles.sort_by(f64::total_cmp);
1886    let mut best = (
1887        angles[0] + tau - angles[angles.len() - 1],
1888        angles[angles.len() - 1],
1889    );
1890    for pair in angles.windows(2) {
1891        if pair[1] - pair[0] > best.0 {
1892            best = (pair[1] - pair[0], pair[0]);
1893        }
1894    }
1895    Some(best.1 + best.0 / 2.0)
1896}
1897
1898/// How many axes a sphere's poles are tried along.
1899const POLE_CANDIDATES: usize = 400;
1900
1901/// How near a ring a sphere's pole may stand: five degrees.
1902const POLE_CLEARANCE: f64 = 0.087;
1903
1904/// Directions spread evenly over the sphere: a Fibonacci lattice.
1905fn spread_directions(count: usize) -> Vec<Vector> {
1906    let golden = core::f64::consts::PI * (3.0 - 5.0_f64.sqrt());
1907    (0..count)
1908        .map(|i| {
1909            #[allow(clippy::cast_precision_loss, reason = "a few hundred directions")]
1910            let (i, n) = (i as f64, count as f64);
1911            let z = 1.0 - 2.0 * (i + 0.5) / n;
1912            let r = (1.0 - z * z).max(0.0).sqrt();
1913            let a = golden * i;
1914            Vector::new(r * a.cos(), r * a.sin(), z)
1915        })
1916        .collect()
1917}
1918
1919/// Put coaxial surfaces on one axis and one angular origin, so bands that
1920/// meet along a circle meet at their seams too; then fix each curved
1921/// region's chart branch and whether it wraps.
1922fn align_axes(points: &[Point], groups: &mut Groups, flat: f64, tol: Tolerances) {
1923    let mut leaders: Vec<Frame> = Vec::new();
1924    for carrier in &mut groups.carriers {
1925        let Carrier::Curved(curved) = carrier else {
1926            continue;
1927        };
1928        let Some(frame) = axis_frame(&curved.shape) else {
1929            continue;
1930        };
1931        let sphere = matches!(curved.shape, Canonical::Sphere(_));
1932        let lead = leaders.iter().find(|l| {
1933            let parallel = l.z().vector().cross(frame.z().vector()).magnitude() <= 1e-3;
1934            let w = frame.origin() - l.origin();
1935            let off = (w - l.z().vector() * w.dot(l.z().vector())).magnitude();
1936            !sphere && parallel && off <= flat * 10.0
1937        });
1938        if let Some(lead) = lead {
1939            let z = lead.z().vector();
1940            let w = frame.origin() - lead.origin();
1941            let origin = lead.origin() + z * w.dot(z);
1942            let axis = if frame.z().vector().dot(z) >= 0.0 {
1943                lead.z()
1944            } else {
1945                -lead.z()
1946            };
1947            if let Ok(snapped) = Frame::new(origin, axis, lead.x(), tol)
1948                && let Some(shape) = on_frame(&curved.shape, snapped, tol)
1949            {
1950                let pts: Vec<Point> = curved
1951                    .vertices
1952                    .iter()
1953                    .map(|&v| points[v as usize])
1954                    .collect();
1955                let deviation = worst_deviation(&shape, &pts);
1956                if deviation <= flat {
1957                    curved.shape = shape;
1958                    curved.deviation = deviation;
1959                }
1960            }
1961        } else if !sphere {
1962            leaders.push(frame);
1963        }
1964        // The branch: the region's mean angle, and whether it wraps.
1965        let charts: Vec<(f64, f64)> = curved
1966            .vertices
1967            .iter()
1968            .filter_map(|&v| chart(&curved.shape, points[v as usize], tol))
1969            .collect();
1970        let mut us: Vec<f64> = charts.iter().map(|c| c.0).collect();
1971        let (_, gap_u) = angular_spread(&mut us);
1972        let (_, pv) = periodic(&curved.shape);
1973        if pv {
1974            let mut vs: Vec<f64> = charts.iter().map(|c| c.1).collect();
1975            curved.wraps_v = angular_spread(&mut vs).1 < core::f64::consts::FRAC_PI_2;
1976        }
1977        let wraps_u = gap_u < core::f64::consts::FRAC_PI_2;
1978        curved.wraps = wraps_u;
1979        if !curved.wraps
1980            && !curved.wraps_v
1981            && !curved.fixed
1982            && let Some(reframed) = away_from(curved, points, tol)
1983        {
1984            curved.shape = reframed;
1985        }
1986        // The branch every pcurve is read on: the region's own mean chart
1987        // point, which after the re-framing sits half a turn from the cut.
1988        let charts: Vec<(f64, f64)> = curved
1989            .vertices
1990            .iter()
1991            .filter_map(|&v| chart(&curved.shape, points[v as usize], tol))
1992            .collect();
1993        let mut us: Vec<f64> = charts.iter().map(|c| c.0).collect();
1994        let (mean_u, _) = angular_spread(&mut us);
1995        let mean_v = if curved.wraps_v {
1996            core::f64::consts::PI
1997        } else if pv {
1998            let mut vs: Vec<f64> = charts.iter().map(|c| c.1).collect();
1999            angular_spread(&mut vs).0
2000        } else {
2001            #[allow(
2002                clippy::cast_precision_loss,
2003                reason = "vertex counts are far below 2^52"
2004            )]
2005            let count = charts.len().max(1) as f64;
2006            charts.iter().map(|c| c.1).sum::<f64>() / count
2007        };
2008        let centre_u = if wraps_u {
2009            core::f64::consts::PI
2010        } else {
2011            ogeom_math::elementary::wrap_angle(mean_u)
2012        };
2013        curved.centre = (centre_u, mean_v);
2014    }
2015}
2016
2017/// The surface on a frame that puts the region half a turn from its
2018/// chart's cut (and a sphere's poles a quarter turn to either side of it),
2019/// so every image of its boundary reads in one piece.
2020fn away_from(curved: &Curved, points: &[Point], tol: Tolerances) -> Option<Canonical> {
2021    let mut mean = Vector::ZERO;
2022    match curved.shape {
2023        Canonical::Sphere(s) => {
2024            for &v in &curved.vertices {
2025                let d = points[v as usize] - s.centre();
2026                let m = d.magnitude();
2027                if m > 0.0 {
2028                    mean += d / m;
2029                }
2030            }
2031            let facing = Direction::new(mean, tol).ok()?;
2032            let frame = Frame::new(s.centre(), facing.any_perpendicular(), -facing, tol).ok()?;
2033            Some(Canonical::Sphere(Sphere::new(frame, s.radius(), tol).ok()?))
2034        }
2035        _ => {
2036            let frame = axis_frame(&curved.shape)?;
2037            let z = frame.z().vector();
2038            for &v in &curved.vertices {
2039                let w = points[v as usize] - frame.origin();
2040                let r = w - z * w.dot(z);
2041                let m = r.magnitude();
2042                if m > 0.0 {
2043                    mean += r / m;
2044                }
2045            }
2046            let facing = Direction::new(mean, tol).ok()?;
2047            on_frame(
2048                &curved.shape,
2049                Frame::new(frame.origin(), frame.z(), -facing, tol).ok()?,
2050                tol,
2051            )
2052        }
2053    }
2054}
2055
2056/// A vertex as built: a mesh vertex, or a point the construction placed,
2057/// where a closed circle is bounded.
2058#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2059enum Corner {
2060    Mesh(u32),
2061    Placed(usize),
2062}
2063
2064/// An edge as planned.
2065struct EdgeSpec {
2066    curve: Curve,
2067    range: (f64, f64),
2068    ends: [Corner; 2],
2069    tolerance: f64,
2070    closed_circle: bool,
2071}
2072
2073/// Everything the build needs, decided before anything is built.
2074struct Plan {
2075    edges: Vec<EdgeSpec>,
2076    /// Each boundary mesh edge, by its lower vertex first: its planned
2077    /// edge, and whether walking it from the lower vertex runs the edge
2078    /// forward.
2079    edge_of: HashMap<(u32, u32), (usize, bool)>,
2080    /// Each face's loops as half-edges, walked with the face on the left.
2081    loops: Vec<Vec<Vec<Half>>>,
2082    placed: Vec<Point>,
2083    /// Each curved face's surface, windowed to its boundary.
2084    surfaces: Vec<Option<ogeom_geom::SurfaceGeometry>>,
2085    /// Each edge's image on each curved face it bounds, by (edge, face),
2086    /// with how far the image strays from the edge.
2087    pcurves: HashMap<(usize, usize), (PlanarCurve, f64)>,
2088    /// How each curved face's boundary lies in its chart.
2089    layouts: Vec<Layout>,
2090}
2091
2092/// Plans the edges and loops.
2093struct Planner<'a> {
2094    points: &'a [Point],
2095    triangles: &'a [[u32; 3]],
2096    adjacency: &'a Adjacency,
2097    groups: &'a Groups,
2098    merge: bool,
2099    /// Vertices kept as edge ends whatever lies either side of them.
2100    pinned: &'a std::collections::HashSet<u32>,
2101    flat: f64,
2102    tol: Tolerances,
2103}
2104
2105/// Why a plan was refused: curved faces to facet, or vertices to keep.
2106enum Replan {
2107    Facet(Vec<usize>),
2108    Pin(Vec<u32>),
2109}
2110
2111/// Whether a triangle with its corners on `shape` stands off it at its
2112/// centroid no farther than a facet of its size over that much turn of the
2113/// surface would: its longest side times the turn of the surface's normals
2114/// across its corners, over six. A triangle spanning a recess, its
2115/// corners on the rim and its middle over the hollow, stands off more.
2116fn sags_as_the_surface(shape: &Canonical, corners: [Point; 3], flat: f64) -> bool {
2117    let unit = |p: Point| {
2118        let g = gradient(shape, p);
2119        let m = g.magnitude();
2120        (m > 0.0).then(|| g / m)
2121    };
2122    let (Some(a), Some(b), Some(c)) = (unit(corners[0]), unit(corners[1]), unit(corners[2])) else {
2123        return false;
2124    };
2125    let angle = |x: Vector, y: Vector| x.dot(y).clamp(-1.0, 1.0).acos();
2126    let turn = angle(a, b).max(angle(b, c)).max(angle(a, c));
2127    let longest = corners[0]
2128        .distance(corners[1])
2129        .max(corners[1].distance(corners[2]))
2130        .max(corners[0].distance(corners[2]));
2131    let centroid = Point::from_vector(
2132        (corners[0].to_vector() + corners[1].to_vector() + corners[2].to_vector()) / 3.0,
2133    );
2134    shape.distance_to(centroid) <= longest * turn / 6.0 + flat
2135}
2136
2137/// Whether a triangle is a sliver: no taller across its longest side than
2138/// a tenth of that side, so its normal says little.
2139fn is_sliver(corners: [Point; 3]) -> bool {
2140    let [a, b, c] = corners;
2141    let sides = [(a, b, c), (b, c, a), (c, a, b)];
2142    let (p, q, r) = sides
2143        .into_iter()
2144        .max_by(|x, y| x.0.distance(x.1).total_cmp(&y.0.distance(y.1)))
2145        .unwrap_or((a, b, c));
2146    let base = p.distance(q);
2147    base > 0.0 && distance_to_line(r, p, q) <= base * 0.1
2148}
2149
2150/// How far a chord taken for an edge may stand off the curved face it
2151/// bounds, against its length: a twentieth.
2152const CHORD_SAG: f64 = 0.05;
2153
2154/// How much wider than the gap it was measured from an edge's tolerance is
2155/// recorded: a millionth.
2156const TOLERANCE_MARGIN: f64 = 1e-6;
2157
2158/// The most triangles a cluster the region surrounds may hold and still
2159/// be taken into it whatever its normals.
2160const ENCLOSED_CLUSTER: usize = 16;
2161
2162/// How far a snapped curve may stand off its chain or its faces.
2163const REACH: f64 = 20.0;
2164
2165impl Planner<'_> {
2166    fn border(&self, h: Half) -> bool {
2167        match self.adjacency.twin[h] {
2168            None => true,
2169            Some(g) => self.groups.of[g / 3] != self.groups.of[h / 3],
2170        }
2171    }
2172
2173    fn curved(&self, g: usize) -> Option<&Curved> {
2174        match &self.groups.carriers[g] {
2175            Carrier::Curved(c) => Some(c),
2176            _ => None,
2177        }
2178    }
2179
2180    /// The plan, or the curved faces whose boundary could not be built
2181    /// exactly and are to be faceted instead.
2182    #[allow(clippy::too_many_lines, reason = "one pass over the boundary")]
2183    fn plan(&self) -> OgeomResult<Result<Plan, Replan>> {
2184        let halves = self.triangles.len() * 3;
2185        let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
2186        for h in 0..halves {
2187            if self.border(h) {
2188                let (a, b) = from_to(self.triangles, h);
2189                edge_faces
2190                    .entry((a.min(b), a.max(b)))
2191                    .or_default()
2192                    .push(self.groups.of[h / 3]);
2193            }
2194        }
2195        for faces in edge_faces.values_mut() {
2196            faces.sort_unstable();
2197        }
2198        let mut incident: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
2199        for &(a, b) in edge_faces.keys() {
2200            incident.entry(a).or_default().push((a, b));
2201            incident.entry(b).or_default().push((a, b));
2202        }
2203        let any_curved = |faces: &[usize]| faces.iter().any(|&g| self.curved(g).is_some());
2204        // A vertex between two boundary edges with the same faces either
2205        // side is inside one edge: between planes only where the two are
2206        // on one line; along a curved face always, the curve deciding.
2207        let removable = |v: u32| -> bool {
2208            if !self.merge || self.pinned.contains(&v) {
2209                return false;
2210            }
2211            let Some(list) = incident.get(&v) else {
2212                return false;
2213            };
2214            let [e1, e2] = list[..] else {
2215                return false;
2216            };
2217            let faces = &edge_faces[&e1];
2218            if *faces != edge_faces[&e2] {
2219                return false;
2220            }
2221            if any_curved(faces) {
2222                return true;
2223            }
2224            let far = |(a, b): (u32, u32)| if a == v { b } else { a };
2225            let (p, q) = (self.points[far(e1) as usize], self.points[far(e2) as usize]);
2226            let at = self.points[v as usize];
2227            (at - p).dot(q - at) > 0.0 && distance_to_line(at, p, q) <= self.flat
2228        };
2229        let is_kept: HashMap<u32, bool> = incident.keys().map(|&v| (v, !removable(v))).collect();
2230
2231        let mut plan = Plan {
2232            edges: Vec::new(),
2233            edge_of: HashMap::new(),
2234            loops: vec![Vec::new(); self.groups.carriers.len()],
2235            placed: Vec::new(),
2236            surfaces: vec![None; self.groups.carriers.len()],
2237            pcurves: HashMap::new(),
2238            layouts: vec![Layout::Open; self.groups.carriers.len()],
2239        };
2240        let mut failed: Vec<usize> = Vec::new();
2241        let mut keys: Vec<(u32, u32)> = edge_faces.keys().copied().collect();
2242        keys.sort_unstable();
2243        for pass in 0..2 {
2244            for &key in &keys {
2245                if plan.edge_of.contains_key(&key) {
2246                    continue;
2247                }
2248                let (a, b) = key;
2249                let start = if is_kept[&a] {
2250                    a
2251                } else if is_kept[&b] {
2252                    b
2253                } else if pass == 1 {
2254                    a
2255                } else {
2256                    continue;
2257                };
2258                let mut chain = vec![start];
2259                let mut edges = vec![key];
2260                let mut at = if start == a { b } else { a };
2261                chain.push(at);
2262                while !is_kept[&at] && at != start {
2263                    let Some(&following) = incident[&at].iter().find(|e| !edges.contains(e)) else {
2264                        break;
2265                    };
2266                    edges.push(following);
2267                    at = if following.0 == at {
2268                        following.1
2269                    } else {
2270                        following.0
2271                    };
2272                    chain.push(at);
2273                }
2274                let faces = edge_faces[&key].clone();
2275                if any_curved(&faces) {
2276                    match self.snapped(&chain, &faces) {
2277                        Some(spec) => {
2278                            let index = plan.edges.len();
2279                            let (spec, forward, images) = spec;
2280                            for (g, pcurve, deviation) in images {
2281                                plan.pcurves.insert((index, g), (pcurve, deviation));
2282                            }
2283                            let spec = match spec {
2284                                Snapped::Open(curve, range, tolerance) => EdgeSpec {
2285                                    curve,
2286                                    range,
2287                                    ends: if forward {
2288                                        [
2289                                            Corner::Mesh(chain[0]),
2290                                            Corner::Mesh(*chain.last().unwrap_or(&chain[0])),
2291                                        ]
2292                                    } else {
2293                                        [
2294                                            Corner::Mesh(*chain.last().unwrap_or(&chain[0])),
2295                                            Corner::Mesh(chain[0]),
2296                                        ]
2297                                    },
2298                                    tolerance,
2299                                    closed_circle: false,
2300                                },
2301                                Snapped::Loop(curve, range, tolerance) => EdgeSpec {
2302                                    curve,
2303                                    range,
2304                                    ends: [Corner::Mesh(chain[0]), Corner::Mesh(chain[0])],
2305                                    tolerance,
2306                                    closed_circle: false,
2307                                },
2308                                Snapped::Closed(curve, tolerance) => {
2309                                    use ogeom_geom::Curve3d as _;
2310                                    let at = curve.point_at(0.0, self.tol)?;
2311                                    plan.placed.push(at);
2312                                    let corner = Corner::Placed(plan.placed.len() - 1);
2313                                    EdgeSpec {
2314                                        curve,
2315                                        range: (0.0, core::f64::consts::TAU),
2316                                        ends: [corner, corner],
2317                                        tolerance,
2318                                        closed_circle: true,
2319                                    }
2320                                }
2321                            };
2322                            plan.edges.push(spec);
2323                            for (i, e) in edges.iter().enumerate() {
2324                                plan.edge_of
2325                                    .insert(*e, (index, (chain[i] == e.0) == forward));
2326                            }
2327                        }
2328                        None => {
2329                            for g in faces {
2330                                if self.curved(g).is_some() && !failed.contains(&g) {
2331                                    failed.push(g);
2332                                }
2333                            }
2334                            for e in edges {
2335                                plan.edge_of.insert(e, (usize::MAX, true));
2336                            }
2337                        }
2338                    }
2339                    continue;
2340                }
2341                let end = at;
2342                let (p, q) = (self.points[start as usize], self.points[end as usize]);
2343                let straight = start != end
2344                    && chain[1..chain.len() - 1]
2345                        .iter()
2346                        .all(|&v| distance_to_line(self.points[v as usize], p, q) <= self.flat);
2347                // Each piece: its vertices in order, and its mesh edges.
2348                type Piece = (Vec<u32>, Vec<(u32, u32)>);
2349                let pieces: Vec<Piece> = if straight {
2350                    vec![(chain.clone(), edges.clone())]
2351                } else {
2352                    edges.iter().map(|&e| (vec![e.0, e.1], vec![e])).collect()
2353                };
2354                for (piece, piece_edges) in pieces {
2355                    let (from, to) = (piece[0], piece[piece.len() - 1]);
2356                    let (p, q) = (self.points[from as usize], self.points[to as usize]);
2357                    let mut reach = self.tol.confusion();
2358                    for &v in &piece {
2359                        let at = self.points[v as usize];
2360                        reach = reach.max(distance_to_line(at, p, q));
2361                        for &g in &faces {
2362                            if let Carrier::Plane(plane) = &self.groups.carriers[g] {
2363                                reach = reach.max(plane.signed_distance_to(at).abs());
2364                            }
2365                        }
2366                    }
2367                    let index = plan.edges.len();
2368                    plan.edges.push(EdgeSpec {
2369                        curve: LineCurve::segment(p, q, self.tol)?.into(),
2370                        range: (0.0, p.distance(q)),
2371                        ends: [Corner::Mesh(from), Corner::Mesh(to)],
2372                        tolerance: reach,
2373                        closed_circle: false,
2374                    });
2375                    for (i, e) in piece_edges.iter().enumerate() {
2376                        plan.edge_of.insert(*e, (index, piece[i] == e.0));
2377                    }
2378                }
2379            }
2380        }
2381
2382        // Each face's loops, walked with the face on the left: from a
2383        // boundary half-edge to the next one at its end, turning through the
2384        // face's own triangles around the vertex, which keeps a loop that
2385        // touches itself at a vertex on its own side.
2386        let mut walked = vec![false; halves];
2387        for h in 0..halves {
2388            if walked[h] || !self.border(h) {
2389                continue;
2390            }
2391            let mut ring = Vec::new();
2392            let mut at = h;
2393            loop {
2394                walked[at] = true;
2395                ring.push(at);
2396                let mut step = next(at);
2397                let mut guard = 0;
2398                while !self.border(step) {
2399                    let Some(twin) = self.adjacency.twin[step] else {
2400                        break;
2401                    };
2402                    step = next(twin);
2403                    guard += 1;
2404                    if guard > halves {
2405                        ogeom_bail!(Construction, "a face's boundary does not close");
2406                    }
2407                }
2408                if step == h {
2409                    break;
2410                }
2411                if walked[step] {
2412                    ogeom_bail!(Construction, "a face's boundary runs into itself");
2413                }
2414                at = step;
2415            }
2416            plan.loops[self.groups.of[h / 3]].push(ring);
2417        }
2418
2419        // A face whose boundary would run out and back along one line (a
2420        // strip of slivers merged into two straight edges between the same
2421        // two vertices) encloses nothing; its runs keep their vertices. Two
2422        // edges of which one is curved (a flat face cut from a ball by a
2423        // second plane: an arc and a line) enclose a face like any other.
2424        let mut pin: Vec<u32> = Vec::new();
2425        for (g, rings) in plan.loops.iter().enumerate() {
2426            if !matches!(self.groups.carriers[g], Carrier::Plane(_)) {
2427                continue;
2428            }
2429            for ring in rings {
2430                let mut entries: Vec<usize> = Vec::new();
2431                for &h in ring {
2432                    let (edge, _) = self.entry(&plan, h);
2433                    if entries.last() != Some(&edge) {
2434                        entries.push(edge);
2435                    }
2436                }
2437                if entries.len() > 1 && entries.first() == entries.last() {
2438                    entries.pop();
2439                }
2440                if entries.len() < 3
2441                    && entries.iter().all(|&e| {
2442                        e != usize::MAX
2443                            && !plan.edges[e].closed_circle
2444                            && matches!(plan.edges[e].curve, Curve::Line(_))
2445                    })
2446                {
2447                    for &h in ring {
2448                        let (a, b) = from_to(self.triangles, h);
2449                        for v in [a, b] {
2450                            if !pin.contains(&v) && !self.pinned.contains(&v) {
2451                                pin.push(v);
2452                            }
2453                        }
2454                    }
2455                }
2456            }
2457        }
2458        if !pin.is_empty() && failed.is_empty() {
2459            return Ok(Err(Replan::Pin(pin)));
2460        }
2461
2462        // A face round its axis, or round a torus's tube, is built as a band
2463        // between two full circles joined by a seam; a sphere's cap as one
2464        // circle, a seam and the pole; a sphere or torus with no boundary
2465        // whole. A face round its axis between two rims of any other shape,
2466        // holed or not, gets a seam of its own between them.
2467        for (g, carrier) in self.groups.carriers.iter().enumerate() {
2468            let Carrier::Curved(curved) = carrier else {
2469                continue;
2470            };
2471            if failed.contains(&g) || !(curved.wraps || curved.wraps_v) {
2472                continue;
2473            }
2474            let sphere = matches!(curved.shape, Canonical::Sphere(_));
2475            let torus = matches!(curved.shape, Canonical::Torus(_));
2476            let rings = &plan.loops[g];
2477            let circles = rings.iter().all(|ring| {
2478                let first = self.entry(&plan, ring[0]);
2479                first.0 != usize::MAX
2480                    && plan.edges[first.0].closed_circle
2481                    && ring.iter().all(|&h| self.entry(&plan, h).0 == first.0)
2482            });
2483            let wrapped = || {
2484                let resolved = rings
2485                    .iter()
2486                    .all(|ring| ring.iter().all(|&h| self.entry(&plan, h).0 != usize::MAX));
2487                if sphere || !curved.wraps || curved.wraps_v || !resolved {
2488                    return None;
2489                }
2490                let windings: Option<Vec<i32>> = rings
2491                    .iter()
2492                    .map(|ring| winding(&curved.shape, ring, self.triangles, self.points, self.tol))
2493                    .collect();
2494                let windings = windings?;
2495                let rims = windings.iter().filter(|w| w.abs() == 1).count();
2496                let holes = windings.iter().filter(|w| **w == 0).count();
2497                (rims == 2 && rims + holes == windings.len()).then_some(Layout::Wrapped)
2498            };
2499            let holed = || {
2500                let closed_round = sphere || (torus && curved.wraps && curved.wraps_v);
2501                let resolved = rings
2502                    .iter()
2503                    .all(|ring| ring.iter().all(|&h| self.entry(&plan, h).0 != usize::MAX));
2504                if !closed_round || !resolved {
2505                    return false;
2506                }
2507                let tau = core::f64::consts::TAU;
2508                let (_, wraps_v) = periodic(&curved.shape);
2509                rings.iter().all(|ring| {
2510                    let turns =
2511                        windings(&curved.shape, ring, self.triangles, self.points, self.tol);
2512                    // Round neither way, and clear of the seams.
2513                    let Some((0, 0)) = turns else {
2514                        return false;
2515                    };
2516                    let Some(polygon) = hole_polygons(
2517                        &curved.shape,
2518                        &[ring.as_slice()],
2519                        self.triangles,
2520                        self.points,
2521                        self.tol,
2522                    )
2523                    .pop() else {
2524                        return false;
2525                    };
2526                    let clear = |values: &mut dyn Iterator<Item = f64>| {
2527                        let (lo, hi) = values
2528                            .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| {
2529                                (a.min(x), b.max(x))
2530                            });
2531                        (lo / tau).floor() == (hi / tau).floor()
2532                    };
2533                    clear(&mut polygon.iter().map(|p| p.0))
2534                        && (!wraps_v || clear(&mut polygon.iter().map(|p| p.1)))
2535                })
2536            };
2537            let layout = if rings.is_empty() {
2538                (sphere || torus).then_some(Layout::Whole)
2539            } else if holed() {
2540                Some(Layout::Holed)
2541            } else if (curved.wraps && curved.wraps_v) || !circles {
2542                wrapped()
2543            } else if sphere && rings.len() == 1 && curved.fixed {
2544                Some(Layout::Cap)
2545            } else if rings.len() == 2 && (!sphere || curved.fixed) {
2546                Some(Layout::Band {
2547                    round_tube: curved.wraps_v,
2548                })
2549            } else {
2550                wrapped()
2551            };
2552            let layout = match layout {
2553                Some(Layout::Wrapped) => {
2554                    let rings = plan.loops[g].clone();
2555                    self.seat_seam(&mut plan, curved, &rings)
2556                        .then_some(Layout::Wrapped)
2557                }
2558                other => other,
2559            };
2560            match layout {
2561                Some(layout) => plan.layouts[g] = layout,
2562                None => failed.push(g),
2563            }
2564        }
2565
2566        // Every curved face's images of its edges, held to the reach: the
2567        // straight image in the chart where a parallel or a ruling has one,
2568        // a fit by projection where it has not.
2569        let reach = self.flat * REACH;
2570        for (g, carrier) in self.groups.carriers.iter().enumerate() {
2571            let Carrier::Curved(curved) = carrier else {
2572                continue;
2573            };
2574            if failed.contains(&g) || plan.loops[g].is_empty() {
2575                continue;
2576            }
2577            let surface = surface_of(curved, self.points, self.tol)?;
2578            if matches!(
2579                plan.layouts[g],
2580                Layout::Open | Layout::Wrapped | Layout::Holed
2581            ) {
2582                let mut held = true;
2583                'rings: for ring in &plan.loops[g] {
2584                    for &h in ring {
2585                        let (edge, _) = self.entry(&plan, h);
2586                        if edge == usize::MAX {
2587                            held = false;
2588                            break 'rings;
2589                        }
2590                        if plan.pcurves.contains_key(&(edge, g)) {
2591                            continue;
2592                        }
2593                        let spec = &plan.edges[edge];
2594                        // A chord taken for an edge is imaged as loosely as
2595                        // it stands off the face.
2596                        let reach = reach.max(spec.tolerance * 2.0);
2597                        let image =
2598                            image_on(curved, &surface, &spec.curve, spec.range, reach, self.tol);
2599                        match image {
2600                            Some(found) => {
2601                                plan.pcurves.insert((edge, g), found);
2602                            }
2603                            None => {
2604                                held = false;
2605                                break 'rings;
2606                            }
2607                        }
2608                    }
2609                }
2610                if !held {
2611                    failed.push(g);
2612                    continue;
2613                }
2614            }
2615            plan.surfaces[g] = Some(surface);
2616        }
2617        if failed.is_empty() {
2618            Ok(Ok(plan))
2619        } else {
2620            Ok(Err(Replan::Facet(failed)))
2621        }
2622    }
2623
2624    /// Whether a face round its axis has a seam clear of its holes, turning
2625    /// a rim that is one full circle so its vertex stands in the widest
2626    /// stretch the holes leave free where the rims' own vertices offer none.
2627    fn seat_seam(&self, plan: &mut Plan, curved: &Curved, rings: &[Vec<Half>]) -> bool {
2628        use ogeom_geom::Curve3d as _;
2629        let shape = &curved.shape;
2630        let windings: Vec<i32> = rings
2631            .iter()
2632            .map(|ring| winding(shape, ring, self.triangles, self.points, self.tol).unwrap_or(0))
2633            .collect();
2634        let rims: Vec<usize> = (0..rings.len())
2635            .filter(|&k| windings[k].abs() == 1)
2636            .collect();
2637        let [low, high] = rims[..] else {
2638            return false;
2639        };
2640        let holes: Vec<&[Half]> = (0..rings.len())
2641            .filter(|&k| windings[k] == 0)
2642            .map(|k| rings[k].as_slice())
2643            .collect();
2644        let hole_rings = hole_polygons(shape, &holes, self.triangles, self.points, self.tol);
2645        let entries = |plan: &Plan, ring: &[Half]| -> Vec<(usize, bool)> {
2646            let mut out: Vec<(usize, bool)> = Vec::new();
2647            for &h in ring {
2648                let entry = self.entry(plan, h);
2649                if out.last() != Some(&entry) {
2650                    out.push(entry);
2651                }
2652            }
2653            if out.len() > 1 && out.first() == out.last() {
2654                out.pop();
2655            }
2656            out
2657        };
2658        let starts = |plan: &Plan, ring: &[Half]| -> Vec<Point> {
2659            entries(plan, ring)
2660                .into_iter()
2661                .map(
2662                    |(edge, forward)| match plan.edges[edge].ends[usize::from(!forward)] {
2663                        Corner::Mesh(v) => self.points[v as usize],
2664                        Corner::Placed(k) => plan.placed[k],
2665                    },
2666                )
2667                .collect()
2668        };
2669        let clear = |plan: &Plan| {
2670            choose_seam(
2671                shape,
2672                &starts(plan, &rings[low]),
2673                &starts(plan, &rings[high]),
2674                &hole_rings,
2675                self.tol,
2676            )
2677            .is_some()
2678        };
2679        if clear(plan) {
2680            return true;
2681        }
2682        let Some(angle) = free_angle(&hole_rings) else {
2683            return false;
2684        };
2685        for rim in [low, high] {
2686            let list = entries(plan, &rings[rim]);
2687            let [(edge, _)] = list[..] else {
2688                continue;
2689            };
2690            let spec = &plan.edges[edge];
2691            let (Curve::Circle(c), Corner::Placed(k)) = (&spec.curve, spec.ends[0]) else {
2692                continue;
2693            };
2694            let circle = c.circle();
2695            let Some((_, v)) = chart(shape, plan.placed[k], self.tol) else {
2696                continue;
2697            };
2698            let target = evaluate(shape, (angle, v));
2699            let Ok(x) = Direction::new(target - circle.centre(), self.tol) else {
2700                continue;
2701            };
2702            let Ok(frame) = Frame::new(circle.centre(), circle.frame().z(), x, self.tol) else {
2703                continue;
2704            };
2705            let Ok(turned) = ogeom_math::Circle::new(frame, circle.radius(), self.tol) else {
2706                continue;
2707            };
2708            let curve: Curve = ogeom_geom::CircleCurve::new(turned).into();
2709            let Ok(at) = curve.point_at(0.0, self.tol) else {
2710                continue;
2711            };
2712            plan.edges[edge].curve = curve;
2713            plan.placed[k] = at;
2714        }
2715        clear(plan)
2716    }
2717
2718    /// A half-edge's planned edge, and whether the half-edge runs it
2719    /// forward.
2720    fn entry(&self, plan: &Plan, h: Half) -> (usize, bool) {
2721        let (a, b) = from_to(self.triangles, h);
2722        let (edge, along) = plan.edge_of[&(a.min(b), a.max(b))];
2723        (edge, along == (a < b))
2724    }
2725
2726    /// The exact curve a chain between two faces, one of them curved,
2727    /// lies on: a parallel circle or a ruling of a curved face, placed on
2728    /// that face itself so its pcurve there is exact. `None` when no such
2729    /// curve holds the chain and the faces both.
2730    fn snapped(&self, chain: &[u32], faces: &[usize]) -> Option<(Snapped, bool, Images)> {
2731        if faces.len() > 2 {
2732            return None;
2733        }
2734        let closed = chain.len() > 2 && chain[0] == chain[chain.len() - 1];
2735        let pts: Vec<Point> = chain[..chain.len() - usize::from(closed)]
2736            .iter()
2737            .map(|&v| self.points[v as usize])
2738            .collect();
2739        let reach = self.flat * REACH;
2740        // A band round its axis bounds itself with its own rims, so its
2741        // seam meets their vertices; its candidates go first.
2742        let mut order: Vec<usize> = faces.to_vec();
2743        order.sort_by_key(|&g| !self.curved(g).is_some_and(|c| c.wraps || c.wraps_v));
2744        // A recognized plane across the chain is where the chain lies,
2745        // exactly: better than a plane fitted to the chain's own points.
2746        let across = faces.iter().find_map(|&g| match &self.groups.carriers[g] {
2747            Carrier::Plane(plane) => Some(*plane),
2748            _ => None,
2749        });
2750        for &g in &order {
2751            let Some(curved) = self.curved(g) else {
2752                continue;
2753            };
2754            for candidate in candidates(&curved.shape, &pts, across, reach, self.tol) {
2755                if let Some((snapped, forward)) = self.fitted(candidate, &pts, closed, faces, reach)
2756                {
2757                    return Some((snapped, forward, Vec::new()));
2758                }
2759            }
2760        }
2761        self.section(&pts, closed, faces, reach)
2762            .or_else(|| self.chord(&pts, closed, faces, reach))
2763    }
2764
2765    /// The last resort between two faces that meet all but tangentially (a
2766    /// fillet running on into a corner ball, or into a patch the mesh
2767    /// leaves faceted): no curve the two surfaces share can be solved for
2768    /// along the chain. The chain's own vertices lie on both, and a curve
2769    /// is threaded through them, a line for a single span; its tolerance is
2770    /// how far it strays from either surface between them, up to a
2771    /// twentieth of its longest span. A face bounded by such a curve is
2772    /// good to its tolerance, where it would otherwise fall to facets
2773    /// whole.
2774    fn chord(
2775        &self,
2776        pts: &[Point],
2777        closed: bool,
2778        faces: &[usize],
2779        reach: f64,
2780    ) -> Option<(Snapped, bool, Images)> {
2781        use ogeom_geom::Curve3d as _;
2782        let [a, b] = faces[..] else {
2783            return None;
2784        };
2785        let (fa, fb) = (self.signed(a)?, self.signed(b)?);
2786        let mut on: Vec<Point> = pts.to_vec();
2787        if closed {
2788            on.push(pts[0]);
2789        }
2790        let longest = on
2791            .windows(2)
2792            .map(|w| w[0].distance(w[1]))
2793            .fold(0.0_f64, f64::max);
2794        if longest <= self.tol.confusion() {
2795            return None;
2796        }
2797        let (curve, samples): (Curve, Vec<f64>) = if on.len() == 2 {
2798            let length = on[0].distance(on[1]);
2799            let line: Curve = LineCurve::segment(on[0], on[1], self.tol).ok()?.into();
2800            (
2801                line,
2802                (0..=16).map(|k| length * f64::from(k) / 16.0).collect(),
2803            )
2804        } else {
2805            // A loop is carried on past its ends and cut back, as a fitted
2806            // section is.
2807            let n = on.len();
2808            let pad = if closed { 3.min(n / 3) } else { 0 };
2809            let padded: Vec<Point> = if pad > 0 {
2810                on[n - 1 - pad..n - 1]
2811                    .iter()
2812                    .chain(&on)
2813                    .chain(&on[1..=pad])
2814                    .copied()
2815                    .collect()
2816            } else {
2817                on.clone()
2818            };
2819            let parameters =
2820                crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
2821            let mut spline = crate::fit::interpolate_at(&padded, &parameters, 3, self.tol).ok()?;
2822            if pad > 0 {
2823                spline = spline.split_at(parameters[pad], self.tol).ok()?.1;
2824                spline = spline.split_at(parameters[pad + n - 1], self.tol).ok()?.0;
2825            }
2826            let own = &parameters[pad..pad + n];
2827            let samples = own
2828                .windows(2)
2829                .flat_map(|w| {
2830                    [
2831                        w[0],
2832                        w[0] + (w[1] - w[0]) * 0.25,
2833                        w[0] + (w[1] - w[0]) * 0.5,
2834                        w[0] + (w[1] - w[0]) * 0.75,
2835                    ]
2836                })
2837                .chain(std::iter::once(own[n - 1]))
2838                .collect();
2839            (spline.into(), samples)
2840        };
2841        let range = curve.domain();
2842        let mut tolerance = self.tol.confusion();
2843        for t in samples {
2844            let p = curve.point_at(t.clamp(range.0, range.1), self.tol).ok()?;
2845            tolerance = tolerance.max(fa(p).abs()).max(fb(p).abs());
2846        }
2847        if tolerance > reach.max(longest * CHORD_SAG) {
2848            return None;
2849        }
2850        Some((
2851            if closed {
2852                Snapped::Loop(curve, range, tolerance)
2853            } else {
2854                Snapped::Open(curve, range, tolerance)
2855            },
2856            true,
2857            Vec::new(),
2858        ))
2859    }
2860
2861    /// The curve two faces meet along, where it is no parallel or ruling of
2862    /// either: vertices of the chain, and points between them, solved onto
2863    /// both surfaces, and the curve interpolated through them. Its image on
2864    /// each curved face is interpolated through the same points' chart
2865    /// positions at the same parameters, so the two run together. `None`
2866    /// where the surfaces meet tangentially, the solve does not settle, or
2867    /// the curve strays from either face past the reach.
2868    fn section(
2869        &self,
2870        pts: &[Point],
2871        closed: bool,
2872        faces: &[usize],
2873        reach: f64,
2874    ) -> Option<(Snapped, bool, Images)> {
2875        // Made through more points until it keeps to the surfaces to a
2876        // fiftieth of a micron's worth of confusion distances, as close as
2877        // an exact edge's image would, or the points run out.
2878        let close = self.tol.confusion() * 50.0;
2879        let mut best: Option<(f64, (Snapped, bool, Images))> = None;
2880        let mut count = SECTION_POINTS;
2881        while count <= SECTION_POINTS * 8 {
2882            let Some((worst, found)) = self.section_through(pts, closed, faces, reach, count)
2883            else {
2884                break;
2885            };
2886            let done = worst <= close;
2887            if best.as_ref().is_none_or(|(held, _)| worst < *held) {
2888                best = Some((worst, found));
2889            }
2890            if done {
2891                break;
2892            }
2893            count *= 2;
2894        }
2895        best.map(|(_, found)| found)
2896    }
2897
2898    /// One fitted section through at least `count` points, with the worst
2899    /// of its own stray and its images'.
2900    fn section_through(
2901        &self,
2902        pts: &[Point],
2903        closed: bool,
2904        faces: &[usize],
2905        reach: f64,
2906        count: usize,
2907    ) -> Option<(f64, (Snapped, bool, Images))> {
2908        use ogeom_geom::Curve3d as _;
2909        let [a, b] = faces[..] else {
2910            return None;
2911        };
2912        let (fa, fb) = (self.signed(a)?, self.signed(b)?);
2913        let steps = if closed { pts.len() } else { pts.len() - 1 };
2914        // A long chain is taken a few vertices at a time: the curve needs
2915        // its shape, not every vertex.
2916        let stride = steps.div_ceil(SECTION_SPANS).max(1);
2917        let split = count.div_ceil(steps.div_ceil(stride)).max(SECTION_SPLIT);
2918        let mut on = Vec::new();
2919        let mut i = 0;
2920        while i < steps {
2921            let next = (i + stride).min(steps);
2922            let (p, q) = (pts[i], pts[next % pts.len()]);
2923            for k in 0..split {
2924                #[allow(clippy::cast_precision_loss, reason = "a handful of splits")]
2925                let f = k as f64 / split as f64;
2926                let limit = if k == 0 { reach } else { p.distance(q) + reach };
2927                on.push(onto_both(&fa, &fb, p + (q - p) * f, reach, limit)?);
2928            }
2929            i = next;
2930        }
2931        on.push(if closed {
2932            on[0]
2933        } else {
2934            onto_both(&fa, &fb, pts[pts.len() - 1], reach, reach)?
2935        });
2936        // A loop is interpolated with a few of its points carried on past
2937        // each end, then cut back to its own: an open interpolation left
2938        // free at its ends wanders where the loop meets itself.
2939        let pad = if closed {
2940            SECTION_PAD.min(on.len() / 4)
2941        } else {
2942            0
2943        };
2944        let n = on.len();
2945        let padded: Vec<Point> = if pad > 0 {
2946            on[n - 1 - pad..n - 1]
2947                .iter()
2948                .chain(&on)
2949                .chain(&on[1..=pad])
2950                .copied()
2951                .collect()
2952        } else {
2953            on.clone()
2954        };
2955        let parameters =
2956            crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
2957        let cut = (parameters[pad], parameters[pad + n - 1]);
2958        let trimmed = |spline: ogeom_geom::BSplineCurve| -> Option<ogeom_geom::BSplineCurve> {
2959            if pad == 0 {
2960                return Some(spline);
2961            }
2962            let (_, after) = spline.split_at(cut.0, self.tol).ok()?;
2963            Some(after.split_at(cut.1, self.tol).ok()?.0)
2964        };
2965        let curve: Curve =
2966            trimmed(crate::fit::interpolate_at(&padded, &parameters, 3, self.tol).ok()?)?.into();
2967        let range = curve.domain();
2968        let parameters = parameters[pad..pad + n].to_vec();
2969        // Between the points it was made through, how far it strays from
2970        // either surface.
2971        let between = |k: usize, f: f64| parameters[k] + (parameters[k + 1] - parameters[k]) * f;
2972        // Its ends are the chain's ends solved onto both surfaces, a hair
2973        // from the vertices they meet.
2974        let last = if closed { pts[0] } else { pts[pts.len() - 1] };
2975        let mut tolerance = self
2976            .tol
2977            .confusion()
2978            .max(on[0].distance(pts[0]))
2979            .max(on[on.len() - 1].distance(last));
2980        for k in 0..on.len() - 1 {
2981            for f in [0.25, 0.5, 0.75] {
2982                let p = curve.point_at(between(k, f), self.tol).ok()?;
2983                tolerance = tolerance.max(fa(p).abs()).max(fb(p).abs());
2984            }
2985        }
2986        if tolerance > reach {
2987            return None;
2988        }
2989        let mut images = Vec::new();
2990        for &g in faces {
2991            let Some(curved) = self.curved(g) else {
2992                continue;
2993            };
2994            let (pu, pv) = periodic(&curved.shape);
2995            let mut uv: Vec<Point> = Vec::with_capacity(padded.len());
2996            for p in &padded {
2997                let (u, v) = match uv.last() {
2998                    None => unwrapped(curved, *p, self.tol)?,
2999                    Some(last) => {
3000                        let (u, v) = chart(&curved.shape, *p, self.tol)?;
3001                        let near = |x: f64, c: f64, wraps: bool| {
3002                            if wraps {
3003                                c + ogeom_math::elementary::wrap_signed_angle(x - c)
3004                            } else {
3005                                x
3006                            }
3007                        };
3008                        (near(u, last.x, pu), near(v, last.y, pv))
3009                    }
3010                };
3011                uv.push(Point::new(u, v, 0.0));
3012            }
3013            let all =
3014                crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
3015            let flat = trimmed(crate::fit::interpolate_at(&uv, &all, 3, self.tol).ok()?)?;
3016            let control: Vec<Point2> = flat
3017                .control_points()
3018                .iter()
3019                .map(|c| Point2::new(c.scaled.x, c.scaled.y))
3020                .collect();
3021            let pcurve: PlanarCurve =
3022                ogeom_geom::BSpline2d::new(flat.knots().clone(), control, self.tol)
3023                    .ok()?
3024                    .into();
3025            let mut deviation = self.tol.confusion();
3026            for k in 0..on.len() - 1 {
3027                for f in [0.0, 0.25, 0.5, 0.75] {
3028                    use ogeom_geom::Curve2d as _;
3029                    let t = between(k, f);
3030                    let at = pcurve.point_at(t, self.tol).ok()?;
3031                    let lifted = evaluate(&curved.shape, (at.x, at.y));
3032                    deviation = deviation.max(lifted.distance(curve.point_at(t, self.tol).ok()?));
3033                }
3034            }
3035            if deviation > reach {
3036                return None;
3037            }
3038            images.push((g, pcurve, deviation));
3039        }
3040        let worst = images.iter().map(|(_, _, d)| *d).fold(tolerance, f64::max);
3041        Some((
3042            worst,
3043            (
3044                if closed {
3045                    Snapped::Loop(curve, range, tolerance)
3046                } else {
3047                    Snapped::Open(curve, range, tolerance)
3048                },
3049                true,
3050                images,
3051            ),
3052        ))
3053    }
3054
3055    /// A face's surface as a signed distance, where it has one.
3056    fn signed(&self, g: usize) -> Option<Box<dyn Fn(Point) -> f64>> {
3057        match &self.groups.carriers[g] {
3058            Carrier::Plane(plane) => {
3059                let plane = *plane;
3060                Some(Box::new(move |p: Point| plane.signed_distance_to(p)))
3061            }
3062            Carrier::Curved(c) => {
3063                let shape = c.shape;
3064                Some(Box::new(move |p: Point| shape.signed_distance_to(p)))
3065            }
3066            Carrier::Gone => None,
3067        }
3068    }
3069
3070    /// A candidate curve held against the chain and the faces: its range,
3071    /// which way it runs along the chain, and its tolerance.
3072    fn fitted(
3073        &self,
3074        curve: Curve,
3075        pts: &[Point],
3076        closed: bool,
3077        faces: &[usize],
3078        reach: f64,
3079    ) -> Option<(Snapped, bool)> {
3080        use ogeom_geom::Curve3d as _;
3081        let tau = core::f64::consts::TAU;
3082        let parameter = |p: Point| -> Option<f64> {
3083            match &curve {
3084                Curve::Line(l) => {
3085                    let axis = l.axis();
3086                    Some((p - axis.location).dot(axis.direction.vector()))
3087                }
3088                Curve::Circle(c) => {
3089                    ogeom_math::elementary::circle_parameter(&c.circle(), p, self.tol).ok()
3090                }
3091                _ => None,
3092            }
3093        };
3094        let mut tolerance = self.tol.confusion();
3095        let mut ts = Vec::with_capacity(pts.len());
3096        for p in pts {
3097            let t = parameter(*p)?;
3098            let on = curve.point_at(t, self.tol).ok()?;
3099            tolerance = tolerance.max(on.distance(*p));
3100            ts.push(t);
3101        }
3102        // The sweep along the chain, unwrapped for a circle.
3103        let mut sweep = 0.0;
3104        let steps = if closed { ts.len() } else { ts.len() - 1 };
3105        for i in 0..steps {
3106            let (a, b) = (ts[i], ts[(i + 1) % ts.len()]);
3107            let d = b - a;
3108            sweep += if matches!(curve, Curve::Circle(_)) {
3109                ogeom_math::elementary::wrap_signed_angle(d)
3110            } else {
3111                d
3112            };
3113        }
3114        let (snapped, forward) = if closed {
3115            if !matches!(curve, Curve::Circle(_)) || (sweep.abs() - tau).abs() > 1e-3 {
3116                return None;
3117            }
3118            (None, sweep > 0.0)
3119        } else if sweep > 0.0 {
3120            (Some((ts[0], ts[0] + sweep)), true)
3121        } else {
3122            let last = ts[ts.len() - 1];
3123            (Some((last, last - sweep)), false)
3124        };
3125        let range = snapped.unwrap_or((0.0, tau));
3126        if range.1 - range.0 <= self.tol.parametric() {
3127            return None;
3128        }
3129        // Drawn in every plane it bounds: a circle is imaged in a plane by
3130        // projection, which a circle standing across the plane has not.
3131        for &g in faces {
3132            if let Carrier::Plane(plane) = &self.groups.carriers[g] {
3133                let surface: ogeom_geom::SurfaceGeometry = PlaneSurface::new(*plane).into();
3134                ogeom_intersect::exact_pcurve_of(&curve, &surface, self.tol)?;
3135            }
3136        }
3137        // Standing on every face it bounds.
3138        for k in 0..=16 {
3139            let t = range.0 + (range.1 - range.0) * f64::from(k) / 16.0;
3140            let p = curve.point_at(t, self.tol).ok()?;
3141            for &g in faces {
3142                let off = match &self.groups.carriers[g] {
3143                    Carrier::Plane(plane) => plane.signed_distance_to(p).abs(),
3144                    Carrier::Curved(c) => c.shape.distance_to(p),
3145                    Carrier::Gone => return None,
3146                };
3147                tolerance = tolerance.max(off);
3148            }
3149        }
3150        if tolerance > reach {
3151            return None;
3152        }
3153        Some((
3154            match snapped {
3155                Some(range) => Snapped::Open(curve, range, tolerance),
3156                None => Snapped::Closed(curve, tolerance),
3157            },
3158            forward,
3159        ))
3160    }
3161}
3162
3163enum Snapped {
3164    Open(Curve, (f64, f64), f64),
3165    Closed(Curve, f64),
3166    /// A closed curve that is no circle, starting and ending at the
3167    /// chain's first vertex.
3168    Loop(Curve, (f64, f64), f64),
3169}
3170
3171/// How many pieces each span of a chain is cut into for a fitted section:
3172/// its vertices alone leave a coarse chain's curve unconstrained between
3173/// them.
3174const SECTION_SPLIT: usize = 4;
3175
3176/// The most spans a chain is taken in for a fitted section.
3177const SECTION_SPANS: usize = 40;
3178
3179/// How many points a closed section is carried on past each end while it
3180/// is interpolated.
3181const SECTION_PAD: usize = 8;
3182
3183/// How many points a fitted section is interpolated through, at least:
3184/// a short chain's spans are cut finer to reach it.
3185const SECTION_POINTS: usize = 160;
3186
3187/// A fitted section's images on the curved faces it bounds: the face, the
3188/// image, and how far the image strays from the curve.
3189type Images = Vec<(usize, PlanarCurve, f64)>;
3190
3191/// A point solved onto where two surfaces meet, from a start near both:
3192/// Newton's step, the least one that zeroes both signed distances to first
3193/// order. `None` where the surfaces meet tangentially there, where the
3194/// solve does not settle within a thousandth of `reach` of both, or where
3195/// it lands farther than `limit` from its start.
3196fn onto_both(
3197    fa: &dyn Fn(Point) -> f64,
3198    fb: &dyn Fn(Point) -> f64,
3199    start: Point,
3200    reach: f64,
3201    limit: f64,
3202) -> Option<Point> {
3203    let mut p = start;
3204    let h = 1e-7 * (1.0 + p.to_vector().magnitude());
3205    let gradient = |f: &dyn Fn(Point) -> f64, p: Point| {
3206        let d = |v: Vector| (f(p + v * h) - f(p - v * h)) / (2.0 * h);
3207        Vector::new(d(Vector::X), d(Vector::Y), d(Vector::Z))
3208    };
3209    for _ in 0..40 {
3210        let (va, vb) = (fa(p), fb(p));
3211        if va.abs().max(vb.abs()) <= 1e-13 * (1.0 + p.to_vector().magnitude()) {
3212            break;
3213        }
3214        let (ga, gb) = (gradient(fa, p), gradient(fb, p));
3215        let (aa, ab, bb) = (ga.dot(ga), ga.dot(gb), gb.dot(gb));
3216        let det = aa.mul_add(bb, -(ab * ab));
3217        if det <= 1e-12 * aa * bb {
3218            return None;
3219        }
3220        let la = (va * bb - vb * ab) / det;
3221        let lb = (vb * aa - va * ab) / det;
3222        p = p - ga * la - gb * lb;
3223    }
3224    (fa(p).abs().max(fb(p).abs()) <= reach * 1e-3 && p.distance(start) <= limit).then_some(p)
3225}
3226
3227/// The curves a chain on a curved surface may be: the parallel circle
3228/// through its mean height, when every point sits at one height and one
3229/// distance from the axis; and, on a cylinder or a cone, the ruling
3230/// through its mean angle, when the chain is straight along it. Each is
3231/// placed on the surface exactly, its angle measured from the surface's
3232/// own origin.
3233fn candidates(
3234    shape: &Canonical,
3235    pts: &[Point],
3236    across: Option<Plane>,
3237    reach: f64,
3238    tol: Tolerances,
3239) -> Vec<Curve> {
3240    let mut out = Vec::new();
3241    let plane_of = |pts: &[Point]| match across {
3242        Some(plane) => Some((plane.project(pts[0]), plane.normal())),
3243        None => plane_through(pts, tol),
3244    };
3245    if let Canonical::Sphere(sphere) = shape {
3246        // The section of the sphere by the chain's plane. Where that plane
3247        // is square to the sphere's axis the section is a latitude, and is
3248        // built on the sphere's own frame, so its parameter is the sphere's
3249        // angle and it starts where the sphere's seam does.
3250        let axis = sphere.frame().z();
3251        if pts.len() >= 3
3252            && let Some((centre, normal)) = plane_of(pts)
3253            && normal.vector().cross(axis.vector()).magnitude()
3254                <= if across.is_some() { 1e-12 } else { 1e-3 }
3255        {
3256            let h = (centre - sphere.centre()).dot(axis.vector());
3257            let r2 = sphere.radius().powi(2) - h * h;
3258            if r2 > 0.0
3259                && let Ok(frame) = Frame::new(
3260                    sphere.centre() + axis.vector() * h,
3261                    axis,
3262                    sphere.frame().x(),
3263                    tol,
3264                )
3265                && let Ok(circle) = ogeom_math::Circle::new(frame, r2.sqrt(), tol)
3266            {
3267                out.push(ogeom_geom::CircleCurve::new(circle).into());
3268                return out;
3269            }
3270        }
3271        if pts.len() >= 3
3272            && let Some((centre, normal)) = plane_of(pts)
3273        {
3274            let n = normal.vector();
3275            let d = (centre - sphere.centre()).dot(n);
3276            let r2 = sphere.radius().powi(2) - d * d;
3277            if r2 > 0.0
3278                && let Ok(frame) = Frame::new(
3279                    sphere.centre() + n * d,
3280                    normal,
3281                    normal.any_perpendicular(),
3282                    tol,
3283                )
3284                && let Ok(circle) = ogeom_math::Circle::new(frame, r2.sqrt(), tol)
3285            {
3286                out.push(ogeom_geom::CircleCurve::new(circle).into());
3287            }
3288        }
3289        return out;
3290    }
3291    let Some(frame) = axis_frame(shape) else {
3292        return out;
3293    };
3294    let (o, z) = (frame.origin(), frame.z().vector());
3295    let heights: Vec<f64> = pts.iter().map(|p| (*p - o).dot(z)).collect();
3296    let radii: Vec<f64> = pts
3297        .iter()
3298        .zip(&heights)
3299        .map(|(p, h)| ((*p - o) - z * *h).magnitude())
3300        .collect();
3301    #[allow(
3302        clippy::cast_precision_loss,
3303        reason = "chain lengths are far below 2^52"
3304    )]
3305    let count = pts.len() as f64;
3306    let mean_h = heights.iter().sum::<f64>() / count;
3307    let mean_r = radii.iter().sum::<f64>() / count;
3308    let level = heights.iter().all(|h| (h - mean_h).abs() <= reach)
3309        && radii.iter().all(|r| (r - mean_r).abs() <= reach);
3310    if level {
3311        let radius = match shape {
3312            Canonical::Cylinder(c) => c.radius(),
3313            Canonical::Cone(c) => c.radius_at(mean_h),
3314            Canonical::Torus(t) => {
3315                let (big, small) = (t.major_radius(), t.minor_radius());
3316                let off = (small * small - mean_h * mean_h).max(0.0).sqrt();
3317                if (big + off - mean_r).abs() <= (big - off - mean_r).abs() {
3318                    big + off
3319                } else {
3320                    big - off
3321                }
3322            }
3323            _ => mean_r,
3324        };
3325        if let Ok(at) = Frame::new(o + z * mean_h, frame.z(), frame.x(), tol)
3326            && let Ok(circle) = ogeom_math::Circle::new(at, radius, tol)
3327        {
3328            out.push(ogeom_geom::CircleCurve::new(circle).into());
3329        }
3330    }
3331    // A circle of a torus's tube: every point in one plane through the
3332    // axis, at the angle the chain stands at. Built with the tube's own
3333    // angle as its parameter, starting at the outer equator.
3334    if let Canonical::Torus(torus) = shape
3335        && pts.len() >= 2
3336    {
3337        let mut angles: Vec<f64> = pts
3338            .iter()
3339            .filter_map(|x| chart(shape, *x, tol).map(|c| c.0))
3340            .collect();
3341        if !angles.is_empty() {
3342            let (u, _) = angular_spread(&mut angles);
3343            let (x, y) = (frame.x().vector(), frame.y().vector());
3344            let out_u = x * u.cos() + y * u.sin();
3345            let across = y * u.cos() - x * u.sin();
3346            let in_plane = pts.iter().all(|p| (*p - o).dot(across).abs() <= reach);
3347            if in_plane
3348                && let Ok(radial) = Direction::new(out_u, tol)
3349                && let Ok(normal) = Direction::new(out_u.cross(z), tol)
3350                && let Ok(at) = Frame::new(o + out_u * torus.major_radius(), normal, radial, tol)
3351                && let Ok(circle) = ogeom_math::Circle::new(at, torus.minor_radius(), tol)
3352            {
3353                out.push(ogeom_geom::CircleCurve::new(circle).into());
3354            }
3355        }
3356    }
3357    if matches!(shape, Canonical::Cylinder(_) | Canonical::Cone(_)) && pts.len() >= 2 {
3358        let (p, q) = (pts[0], pts[pts.len() - 1]);
3359        let straight = pts.iter().all(|x| distance_to_line(*x, p, q) <= reach);
3360        let mut angles: Vec<f64> = pts
3361            .iter()
3362            .filter_map(|x| chart(shape, *x, tol).map(|c| c.0))
3363            .collect();
3364        if straight && !angles.is_empty() {
3365            let (u, _) = angular_spread(&mut angles);
3366            let (lo, hi) = (
3367                heights.iter().copied().fold(f64::INFINITY, f64::min),
3368                heights.iter().copied().fold(f64::NEG_INFINITY, f64::max),
3369            );
3370            let (a, b) = (evaluate(shape, (u, lo)), evaluate(shape, (u, hi)));
3371            if let Ok(line) = LineCurve::segment(a, b, tol) {
3372                // Measured from the chain's first end, so its parameter is
3373                // the chain's distance along it.
3374                let _ = line;
3375                let (a, b) = if (pts[0] - a).magnitude() <= (pts[0] - b).magnitude() {
3376                    (a, b)
3377                } else {
3378                    (b, a)
3379                };
3380                if let Ok(line) = LineCurve::segment(a, b, tol) {
3381                    out.push(line.into());
3382                }
3383            }
3384        }
3385    }
3386    out
3387}
3388
3389/// The plane nearest a set of points: their centroid and the covariance's
3390/// smallest direction.
3391fn plane_through(points: &[Point], tol: Tolerances) -> Option<(Point, Direction)> {
3392    #[allow(
3393        clippy::cast_precision_loss,
3394        reason = "chain lengths are far below 2^52"
3395    )]
3396    let count = points.len() as f64;
3397    let c = points.iter().fold(Vector::ZERO, |s, p| s + p.to_vector()) / count;
3398    let mut m = nalgebra::Matrix3::<f64>::zeros();
3399    for p in points {
3400        let d = p.to_vector() - c;
3401        let v = nalgebra::Vector3::new(d.x, d.y, d.z);
3402        m += v * v.transpose();
3403    }
3404    let eigen = nalgebra::SymmetricEigen::new(m);
3405    let mut best = 0;
3406    for i in 1..3 {
3407        if eigen.eigenvalues[i] < eigen.eigenvalues[best] {
3408            best = i;
3409        }
3410    }
3411    let v = eigen.eigenvectors.column(best);
3412    Some((
3413        Point::from_vector(c),
3414        Direction::new(Vector::new(v[0], v[1], v[2]), tol).ok()?,
3415    ))
3416}
3417
3418/// The surface a curved face is built on, windowed along its axis to hold
3419/// every point its boundary reaches.
3420fn surface_of(
3421    curved: &Curved,
3422    points: &[Point],
3423    tol: Tolerances,
3424) -> OgeomResult<ogeom_geom::SurfaceGeometry> {
3425    use ogeom_geom::{ConeSurface, CylinderSurface, SphereSurface, TorusSurface};
3426    let heights = || {
3427        let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3428        for &v in &curved.vertices {
3429            if let Some((_, h)) = chart(&curved.shape, points[v as usize], tol) {
3430                lo = lo.min(h);
3431                hi = hi.max(h);
3432            }
3433        }
3434        let margin = (hi - lo).mul_add(0.25, tol.confusion() * 10.0);
3435        (lo - margin, hi + margin)
3436    };
3437    Ok(match curved.shape {
3438        Canonical::Cylinder(c) => CylinderSurface::new(c, heights())?.into(),
3439        Canonical::Cone(c) => {
3440            // Short of the apex, where the cone's radius runs out.
3441            let (lo, hi) = heights();
3442            let apex = -c.reference_radius() / c.half_angle().tan();
3443            let lo = lo.max(apex + (hi - apex) * 1e-6);
3444            ConeSurface::new(c, (lo, hi))?.into()
3445        }
3446        Canonical::Sphere(s) => SphereSurface::new(s).into(),
3447        Canonical::Torus(t) => TorusSurface::new(t).into(),
3448        Canonical::Plane(p) => PlaneSurface::new(p).into(),
3449    })
3450}
3451
3452/// An edge's image on a curved face, and how far it strays from the edge:
3453/// the straight chart segment where the edge is a parallel or a ruling of
3454/// the face, otherwise a fit by projection; `None` past the reach.
3455fn image_on(
3456    curved: &Curved,
3457    surface: &ogeom_geom::SurfaceGeometry,
3458    curve: &Curve,
3459    range: (f64, f64),
3460    reach: f64,
3461    tol: Tolerances,
3462) -> Option<(PlanarCurve, f64)> {
3463    if let Some((pcurve, deviation)) = straight_image(curved, curve, range, tol)
3464        && deviation <= reach
3465    {
3466        return Some((pcurve, deviation));
3467    }
3468    if let Some((pcurve, deviation)) = interpolated_image(curved, curve, range, tol)
3469        && deviation <= reach
3470    {
3471        return Some((pcurve, deviation));
3472    }
3473    let (pcurve, error, _, off, _) =
3474        crate::pcurve_fit::fit_projected_pcurve_capped(curve, range, surface, reach, tol).ok()?;
3475    let deviation = error.max(off);
3476    (deviation <= reach).then_some((pcurve, deviation))
3477}
3478
3479/// An edge's image interpolated through the chart positions of points
3480/// along it, at the edge's own parameters: the curve and its image agree
3481/// at every sample by construction, and between them to the fourth power
3482/// of the spacing, which is doubled until the image keeps to the curve
3483/// within a hundredth of the confusion distance. `None` where a sample
3484/// has no chart position (a pole).
3485fn interpolated_image(
3486    curved: &Curved,
3487    curve: &Curve,
3488    range: (f64, f64),
3489    tol: Tolerances,
3490) -> Option<(PlanarCurve, f64)> {
3491    use ogeom_geom::{Curve2d as _, Curve3d as _};
3492    let (pu, pv) = periodic(&curved.shape);
3493    let near = |x: f64, c: f64, wraps: bool| {
3494        if wraps {
3495            c + ogeom_math::elementary::wrap_signed_angle(x - c)
3496        } else {
3497            x
3498        }
3499    };
3500    let mut best: Option<(PlanarCurve, f64)> = None;
3501    let mut count: u32 = 32;
3502    while count <= 1024 {
3503        let parameters: Vec<f64> = (0..=count)
3504            .map(|k| range.0 + (range.1 - range.0) * f64::from(k) / f64::from(count))
3505            .collect();
3506        let mut uv: Vec<Point> = Vec::with_capacity(parameters.len());
3507        for &t in &parameters {
3508            let p = curve.point_at(t, tol).ok()?;
3509            let (u, v) = match uv.last() {
3510                None => unwrapped(curved, p, tol)?,
3511                Some(last) => {
3512                    let (u, v) = chart(&curved.shape, p, tol)?;
3513                    (near(u, last.x, pu), near(v, last.y, pv))
3514                }
3515            };
3516            uv.push(Point::new(u, v, 0.0));
3517        }
3518        let flat = crate::fit::interpolate_at(&uv, &parameters, 3, tol).ok()?;
3519        let control: Vec<Point2> = flat
3520            .control_points()
3521            .iter()
3522            .map(|c| Point2::new(c.scaled.x, c.scaled.y))
3523            .collect();
3524        let pcurve: PlanarCurve = ogeom_geom::BSpline2d::new(flat.knots().clone(), control, tol)
3525            .ok()?
3526            .into();
3527        let mut deviation = tol.confusion() * 1e-2;
3528        for pair in parameters.windows(2) {
3529            for f in [0.25, 0.5, 0.75] {
3530                let t = pair[0] + (pair[1] - pair[0]) * f;
3531                let at = pcurve.point_at(t, tol).ok()?;
3532                let lifted = evaluate(&curved.shape, (at.x, at.y));
3533                deviation = deviation.max(lifted.distance(curve.point_at(t, tol).ok()?));
3534            }
3535        }
3536        let done = deviation <= tol.confusion() * 1e-2;
3537        if best.as_ref().is_none_or(|(_, held)| deviation < *held) {
3538            best = Some((pcurve, deviation));
3539        }
3540        if done {
3541            break;
3542        }
3543        count *= 2;
3544    }
3545    best
3546}
3547
3548/// A degree-one pcurve over the edge's range, from the chart points of its
3549/// ends on the face's branch, and how far it strays along its length.
3550fn straight_image(
3551    curved: &Curved,
3552    curve: &Curve,
3553    range: (f64, f64),
3554    tol: Tolerances,
3555) -> Option<(PlanarCurve, f64)> {
3556    use ogeom_geom::Curve3d as _;
3557    let at =
3558        |t: f64| -> Option<(f64, f64)> { unwrapped(curved, curve.point_at(t, tol).ok()?, tol) };
3559    let start = at(range.0)?;
3560    // Carried along the edge a quarter at a time, so a full turn ends a
3561    // whole period from where it began.
3562    let (pu, pv) = periodic(&curved.shape);
3563    let step = |a: f64, b: f64, wraps: bool| {
3564        if wraps {
3565            a + ogeom_math::elementary::wrap_signed_angle(b - a)
3566        } else {
3567            b
3568        }
3569    };
3570    let mut end = start;
3571    for k in 1..=4 {
3572        let next = at(range.0 + (range.1 - range.0) * f64::from(k) / 4.0)?;
3573        end = (step(end.0, next.0, pu), step(end.1, next.1, pv));
3574    }
3575    let pcurve = linear(start, end, range, tol).ok()?;
3576    let mut deviation: f64 = 0.0;
3577    for k in 0..=16 {
3578        let f = f64::from(k) / 16.0;
3579        let t = range.0 + (range.1 - range.0) * f;
3580        let uv = (
3581            (end.0 - start.0).mul_add(f, start.0),
3582            (end.1 - start.1).mul_add(f, start.1),
3583        );
3584        deviation =
3585            deviation.max(evaluate(&curved.shape, uv).distance(curve.point_at(t, tol).ok()?));
3586    }
3587    Some((pcurve, deviation))
3588}
3589
3590/// Builds the planned vertices, edges and faces.
3591struct Builder<'a> {
3592    model: &'a mut Model,
3593    points: &'a [Point],
3594    triangles: &'a [[u32; 3]],
3595    groups: &'a Groups,
3596    plan: &'a Plan,
3597    tol: Tolerances,
3598}
3599
3600impl Builder<'_> {
3601    fn entry(&self, h: Half) -> (usize, bool) {
3602        let (a, b) = from_to(self.triangles, h);
3603        let (edge, along) = self.plan.edge_of[&(a.min(b), a.max(b))];
3604        (edge, along == (a < b))
3605    }
3606
3607    /// The faces, one per live group, by group index.
3608    fn build(mut self) -> OgeomResult<Vec<Option<Shape>>> {
3609        let mut corners: HashMap<Corner, Shape> = HashMap::new();
3610        let mut edges: Vec<Shape> = Vec::with_capacity(self.plan.edges.len());
3611        for spec in &self.plan.edges {
3612            for corner in spec.ends {
3613                corners.entry(corner).or_insert_with(|| {
3614                    let at = match corner {
3615                        Corner::Mesh(v) => self.points[v as usize],
3616                        Corner::Placed(i) => self.plan.placed[i],
3617                    };
3618                    self.model.add_vertex(VertexData::new(at))
3619                });
3620            }
3621            let id = self.model.geometry_mut().add_curve(spec.curve.clone());
3622            let mut data = EdgeData::on_curve(id, Location::identity(), spec.range);
3623            // A tolerance measured as a gap is held a millionth wider: the
3624            // checker measures the same gap again, a rounding apart.
3625            data.tolerance = Tolerance::new(spec.tolerance * (1.0 + TOLERANCE_MARGIN))?;
3626            let bounds = if spec.ends[0] == spec.ends[1] {
3627                vec![
3628                    corners[&spec.ends[0]].clone(),
3629                    corners[&spec.ends[0]].clone(),
3630                ]
3631            } else {
3632                vec![
3633                    corners[&spec.ends[0]].clone(),
3634                    corners[&spec.ends[1]].clone(),
3635                ]
3636            };
3637            edges.push(self.model.add_edge(data, &bounds)?);
3638        }
3639
3640        let mut faces = Vec::with_capacity(self.groups.carriers.len());
3641        for (g, carrier) in self.groups.carriers.iter().enumerate() {
3642            let rings = &self.plan.loops[g];
3643            let face = match carrier {
3644                Carrier::Gone => None,
3645                Carrier::Curved(curved) if self.plan.layouts[g] == Layout::Whole => {
3646                    Some(self.whole_face(curved, g)?)
3647                }
3648                _ if rings.is_empty() => None,
3649                Carrier::Plane(plane) => Some(self.plane_face(*plane, rings, &edges)?),
3650                Carrier::Curved(curved) => match self.plan.layouts[g] {
3651                    Layout::Band { round_tube } => {
3652                        Some(self.band_face(curved, rings, &edges, round_tube)?)
3653                    }
3654                    Layout::Cap => Some(self.cap_face(curved, rings, &edges)?),
3655                    Layout::Wrapped => Some(self.wrapped_face(curved, g, rings, &edges)?),
3656                    Layout::Holed => Some(self.holed_face(curved, g, rings, &edges)?),
3657                    Layout::Open | Layout::Whole => {
3658                        Some(self.curved_face(curved, g, rings, &edges)?)
3659                    }
3660                },
3661            };
3662            faces.push(face);
3663        }
3664        Ok(faces)
3665    }
3666
3667    /// A ring's planned edges in walking order, repeats run together.
3668    fn entries(&self, ring: &[Half]) -> Vec<(usize, bool)> {
3669        let mut entries: Vec<(usize, bool)> = Vec::new();
3670        for &h in ring {
3671            let entry = self.entry(h);
3672            if entries.last() != Some(&entry) {
3673                entries.push(entry);
3674            }
3675        }
3676        if entries.len() > 1 && entries.first() == entries.last() {
3677            entries.pop();
3678        }
3679        entries
3680    }
3681
3682    fn has_pcurve(&self, edge: &Shape, surface: ogeom_topo::SurfaceId) -> bool {
3683        self.model.node(edge).and_then(|n| n.data().as_edge()).is_some_and(|d| {
3684            d.representations.iter().any(|rep| {
3685                matches!(rep, EdgeRepr::PCurve { surface: s, .. } | EdgeRepr::Seam { surface: s, .. } if *s == surface)
3686            })
3687        })
3688    }
3689
3690    fn plane_face(
3691        &mut self,
3692        plane: Plane,
3693        rings: &[Vec<Half>],
3694        edges: &[Shape],
3695    ) -> OgeomResult<Shape> {
3696        let geometry: ogeom_geom::SurfaceGeometry = PlaneSurface::new(plane).into();
3697        let surface = self.model.geometry_mut().add_surface(geometry.clone());
3698        let local = |p: Point| {
3699            let l = plane.frame().to_local(p);
3700            Point2::new(l.x, l.y)
3701        };
3702        let mut wires: Vec<(f64, Shape)> = Vec::with_capacity(rings.len());
3703        for ring in rings {
3704            let area = ring_area(ring, self.triangles, |p| Some(local(p)), self.points);
3705            let mut ring_edges = Vec::new();
3706            for (edge, forward) in self.entries(ring) {
3707                let spec = &self.plan.edges[edge];
3708                if !self.has_pcurve(&edges[edge], surface) {
3709                    let Some(pcurve) =
3710                        ogeom_intersect::exact_pcurve_of(&spec.curve, &geometry, self.tol)
3711                    else {
3712                        ogeom_bail!(Construction, "an edge has no image in its face's plane");
3713                    };
3714                    crate::build::attach_pcurve(
3715                        self.model,
3716                        &edges[edge],
3717                        pcurve,
3718                        surface,
3719                        Location::identity(),
3720                        spec.range,
3721                    )?;
3722                }
3723                ring_edges.push(oriented(&edges[edge], forward));
3724            }
3725            wires.push((area, self.model.add_wire(&ring_edges)?));
3726        }
3727        wires.sort_by(|a, b| b.0.total_cmp(&a.0));
3728        let wires: Vec<Shape> = wires.into_iter().map(|(_, w)| w).collect();
3729        self.model
3730            .add_face(FaceData::new(surface, Location::identity()), &wires)
3731    }
3732
3733    /// Whether the region's outward side is the surface's own normal side.
3734    fn outward(&self, curved: &Curved, g: usize) -> bool {
3735        let mut vote = 0.0;
3736        for (t, tri) in self.triangles.iter().enumerate() {
3737            if self.groups.of[t] != g {
3738                continue;
3739            }
3740            let corners = tri.map(|v| self.points[v as usize]);
3741            let centroid = Point::from_vector(
3742                (corners[0].to_vector() + corners[1].to_vector() + corners[2].to_vector()) / 3.0,
3743            );
3744            let n = unit_normal(self.points, *tri);
3745            vote += n.dot(self.surface_normal(curved, centroid));
3746        }
3747        vote >= 0.0
3748    }
3749
3750    /// The surface's own normal (the chart's `du × dv`) near a point.
3751    fn surface_normal(&self, curved: &Curved, p: Point) -> Vector {
3752        let Some(at) = chart(&curved.shape, p, self.tol) else {
3753            return Vector::ZERO;
3754        };
3755        let e = 1e-6;
3756        let o = evaluate(&curved.shape, at);
3757        let du = evaluate(&curved.shape, (at.0 + e, at.1)) - o;
3758        let dv = evaluate(&curved.shape, (at.0, at.1 + e)) - o;
3759        let n = du.cross(dv);
3760        let m = n.magnitude();
3761        if m > 0.0 { n / m } else { Vector::ZERO }
3762    }
3763
3764    fn curved_face(
3765        &mut self,
3766        curved: &Curved,
3767        g: usize,
3768        rings: &[Vec<Half>],
3769        edges: &[Shape],
3770    ) -> OgeomResult<Shape> {
3771        let Some(geometry) = self.plan.surfaces[g].clone() else {
3772            ogeom_bail!(
3773                Construction,
3774                "a curved face was planned without its surface"
3775            );
3776        };
3777        let surface = self.model.geometry_mut().add_surface(geometry);
3778        let outward = self.outward(curved, g);
3779        let mut wires: Vec<(f64, Shape)> = Vec::with_capacity(rings.len());
3780        for ring in rings {
3781            let area = ring_area(
3782                ring,
3783                self.triangles,
3784                |p| unwrapped(curved, p, self.tol).map(|(u, v)| Point2::new(u, v)),
3785                self.points,
3786            );
3787            let mut ring_edges = Vec::new();
3788            for (edge, forward) in self.entries(ring) {
3789                let spec = &self.plan.edges[edge];
3790                if !self.has_pcurve(&edges[edge], surface) {
3791                    let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned()
3792                    else {
3793                        ogeom_bail!(
3794                            Construction,
3795                            "an edge was planned without its image on a face"
3796                        );
3797                    };
3798                    self.model.widen(
3799                        &edges[edge],
3800                        Tolerance::new(deviation.max(self.tol.confusion()))?,
3801                    )?;
3802                    crate::build::attach_pcurve(
3803                        self.model,
3804                        &edges[edge],
3805                        pcurve,
3806                        surface,
3807                        Location::identity(),
3808                        spec.range,
3809                    )?;
3810                }
3811                ring_edges.push(oriented(&edges[edge], forward));
3812            }
3813            let sign = if outward { 1.0 } else { -1.0 };
3814            wires.push((area * sign, self.model.add_wire(&ring_edges)?));
3815        }
3816        wires.sort_by(|a, b| b.0.total_cmp(&a.0));
3817        let wires: Vec<Shape> = wires.into_iter().map(|(_, w)| w).collect();
3818        // Fitted images answer on whichever branch their projection chose.
3819        crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
3820        let mut data = FaceData::new(surface, Location::identity());
3821        data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
3822        let face = self.model.add_face(data, &wires)?;
3823        Ok(if outward { face } else { face.reversed() })
3824    }
3825
3826    /// A face round its axis between two rims of any shape, with holes.
3827    ///
3828    /// The seam joins a vertex of one rim to a vertex of the other along a
3829    /// straight line in the chart: the pair turning least between them
3830    /// whose line crosses no hole. It is a ruling where the pair stand at
3831    /// one angle on a cylinder or a cone, and otherwise the curve that line
3832    /// traces on the surface, interpolated. The outer wire walks the seam
3833    /// down, one rim round, the seam up a whole turn over, and the other rim
3834    /// back; each hole is its own wire.
3835    fn wrapped_face(
3836        &mut self,
3837        curved: &Curved,
3838        g: usize,
3839        rings: &[Vec<Half>],
3840        edges: &[Shape],
3841    ) -> OgeomResult<Shape> {
3842        use ogeom_geom::Curve3d as _;
3843        let tau = core::f64::consts::TAU;
3844        let Some(geometry) = self.plan.surfaces[g].clone() else {
3845            ogeom_bail!(
3846                Construction,
3847                "a face round its axis was planned without its surface"
3848            );
3849        };
3850        let surface = self.model.geometry_mut().add_surface(geometry);
3851        let outward = self.outward(curved, g);
3852        // Every edge's image, as the plan made it.
3853        for ring in rings {
3854            for (edge, _) in self.entries(ring) {
3855                if self.has_pcurve(&edges[edge], surface) {
3856                    continue;
3857                }
3858                let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned() else {
3859                    ogeom_bail!(
3860                        Construction,
3861                        "an edge was planned without its image on a face"
3862                    );
3863                };
3864                self.model.widen(
3865                    &edges[edge],
3866                    Tolerance::new(deviation.max(self.tol.confusion()))?,
3867                )?;
3868                crate::build::attach_pcurve(
3869                    self.model,
3870                    &edges[edge],
3871                    pcurve,
3872                    surface,
3873                    Location::identity(),
3874                    self.plan.edges[edge].range,
3875                )?;
3876            }
3877        }
3878        let windings: Vec<i32> = rings
3879            .iter()
3880            .map(|ring| {
3881                winding(&curved.shape, ring, self.triangles, self.points, self.tol).unwrap_or(0)
3882            })
3883            .collect();
3884        let rims: Vec<usize> = (0..rings.len())
3885            .filter(|&k| windings[k].abs() == 1)
3886            .collect();
3887        let [low, high] = rims[..] else {
3888            ogeom_bail!(Construction, "a face round its axis has two rims");
3889        };
3890        if windings[low] != -windings[high] {
3891            ogeom_bail!(
3892                Construction,
3893                "a face round its axis has its rims turning one way"
3894            );
3895        }
3896        let holes: Vec<usize> = (0..rings.len()).filter(|&k| windings[k] == 0).collect();
3897
3898        // Each rim's entries, and the vertex each one starts from.
3899        let starts = |this: &Self, ring: &[Half]| -> OgeomResult<Vec<RimStart>> {
3900            let mut out = Vec::new();
3901            for (edge, forward) in this.entries(ring) {
3902                let ends = this.model.children_of(&edges[edge])?;
3903                let vertex = if forward { ends.first() } else { ends.last() };
3904                let Some(vertex) = vertex.cloned() else {
3905                    ogeom_bail!(Construction, "a rim edge has no vertex");
3906                };
3907                let Some(ogeom_topo::NodeData::Vertex(data)) =
3908                    this.model.node(&vertex).map(|n| n.data())
3909                else {
3910                    ogeom_bail!(Construction, "a rim vertex has no position");
3911                };
3912                out.push(((edge, forward), vertex, data.point));
3913            }
3914            Ok(out)
3915        };
3916        let from = starts(self, &rings[low])?;
3917        let to = starts(self, &rings[high])?;
3918        let hole_rings = hole_polygons(
3919            &curved.shape,
3920            &holes
3921                .iter()
3922                .map(|&k| rings[k].as_slice())
3923                .collect::<Vec<_>>(),
3924            self.triangles,
3925            self.points,
3926            self.tol,
3927        );
3928        let from_at: Vec<Point> = from.iter().map(|x| x.2).collect();
3929        let to_at: Vec<Point> = to.iter().map(|x| x.2).collect();
3930        let Some((i, j, a, b)) =
3931            choose_seam(&curved.shape, &from_at, &to_at, &hole_rings, self.tol)
3932        else {
3933            ogeom_bail!(Construction, "no seam joins the rims clear of the holes");
3934        };
3935        let (pa, pb) = (from[i].2, to[j].2);
3936        let straight = (b.0 - a.0).abs() <= 1e-12
3937            && matches!(curved.shape, Canonical::Cylinder(_) | Canonical::Cone(_));
3938        let (seam_curve, range, deviation): (Curve, (f64, f64), f64) = if straight {
3939            let line = LineCurve::segment(pa, pb, self.tol)?;
3940            (line.into(), (0.0, pa.distance(pb)), self.tol.confusion())
3941        } else {
3942            const SAMPLES: u32 = 96;
3943            let along = |f: f64| (a.0 + (b.0 - a.0) * f, a.1 + (b.1 - a.1) * f);
3944            let mut pts: Vec<Point> = (0..=SAMPLES)
3945                .map(|k| evaluate(&curved.shape, along(f64::from(k) / f64::from(SAMPLES))))
3946                .collect();
3947            pts[0] = pa;
3948            pts[SAMPLES as usize] = pb;
3949            let curve: Curve =
3950                crate::fit::interpolate(&pts, 3, crate::fit::Spacing::Uniform, self.tol)?.into();
3951            let range = curve.domain();
3952            let mut deviation = self.tol.confusion();
3953            for k in 0..=(SAMPLES * 4) {
3954                let f = f64::from(k) / f64::from(SAMPLES * 4);
3955                let t = range.0 + (range.1 - range.0) * f;
3956                deviation = deviation.max(
3957                    curve
3958                        .point_at(t, self.tol)?
3959                        .distance(evaluate(&curved.shape, along(f))),
3960                );
3961            }
3962            (curve, range, deviation)
3963        };
3964        let id = self.model.geometry_mut().add_curve(seam_curve);
3965        let mut data = EdgeData::on_curve(id, Location::identity(), range);
3966        data.tolerance = Tolerance::new(deviation)?;
3967        let seam = self
3968            .model
3969            .add_edge(data, &[from[i].1.clone(), to[j].1.clone()])?;
3970        // Down its near side where the wire leaves the high rim, up its far
3971        // side a whole turn over, where the low rim's walk comes round to.
3972        let over = tau * f64::from(windings[low]);
3973        let back = linear(a, b, range, self.tol)?;
3974        let forward = linear((a.0 + over, a.1), (b.0 + over, b.1), range, self.tol)?;
3975        crate::build::attach_seam(
3976            self.model,
3977            &seam,
3978            forward,
3979            back,
3980            surface,
3981            Location::identity(),
3982            range,
3983        )?;
3984        let rotated = |list: &[RimStart], at: usize| -> Vec<Shape> {
3985            (0..list.len())
3986                .map(|k| {
3987                    let ((edge, forward), _, _) = list[(at + k) % list.len()];
3988                    oriented(&edges[edge], forward)
3989                })
3990                .collect()
3991        };
3992        let mut outer = vec![seam.reversed()];
3993        outer.extend(rotated(&from, i));
3994        outer.push(seam.clone());
3995        outer.extend(rotated(&to, j));
3996        let mut wires = vec![self.model.add_wire(&outer)?];
3997        for &k in &holes {
3998            let ring_edges: Vec<Shape> = self
3999                .entries(&rings[k])
4000                .into_iter()
4001                .map(|(edge, forward)| oriented(&edges[edge], forward))
4002                .collect();
4003            wires.push(self.model.add_wire(&ring_edges)?);
4004        }
4005        crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
4006        let mut data = FaceData::new(surface, Location::identity());
4007        data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4008        let face = self.model.add_face(data, &wires)?;
4009        Ok(if outward { face } else { face.reversed() })
4010    }
4011
4012    /// A band round the axis: the two rim circles, and a seam at the
4013    /// surface's angle zero joining their vertices.
4014    fn band_face(
4015        &mut self,
4016        curved: &Curved,
4017        rings: &[Vec<Half>],
4018        edges: &[Shape],
4019        round_tube: bool,
4020    ) -> OgeomResult<Shape> {
4021        use ogeom_geom::Curve3d as _;
4022        let tau = core::f64::consts::TAU;
4023        let g = self.groups.of[rings[0][0] / 3];
4024        let Some(geometry) = self.plan.surfaces[g].clone() else {
4025            ogeom_bail!(Construction, "a band was planned without its surface");
4026        };
4027        let surface = self.model.geometry_mut().add_surface(geometry);
4028        let outward = self.outward(curved, g);
4029        let Some(frame) = axis_frame(&curved.shape) else {
4030            ogeom_bail!(Construction, "a band has no axis");
4031        };
4032        // Each rim: its edge, its chart height, and whether its parameter
4033        // runs with the surface's angle.
4034        let mut rims = Vec::with_capacity(2);
4035        for ring in rings {
4036            let (edge, _) = self.entry(ring[0]);
4037            let spec = &self.plan.edges[edge];
4038            let Curve::Circle(c) = &spec.curve else {
4039                ogeom_bail!(Construction, "a band's rim is not a circle");
4040            };
4041            let circle = c.circle();
4042            let start = spec.curve.point_at(0.0, self.tol)?;
4043            let Some((u, v)) = unwrapped(curved, start, self.tol) else {
4044                ogeom_bail!(Construction, "a band's rim has no chart position");
4045            };
4046            // Whether the rim's own parameter runs with the chart's angle
4047            // it goes round: the axis's for a band round it, the tube's for
4048            // a band round the tube.
4049            let turning = if round_tube {
4050                let radial = circle.centre() - frame.origin();
4051                radial.cross(frame.z().vector())
4052            } else {
4053                frame.z().vector()
4054            };
4055            let with = circle.frame().z().vector().dot(turning) > 0.0;
4056            rims.push((edge, if round_tube { u } else { v }, with, start));
4057        }
4058        rims.sort_by(|a, b| a.1.total_cmp(&b.1));
4059        let [
4060            (low, v_low, low_with, low_at),
4061            (high, v_high, high_with, high_at),
4062        ] = rims[..]
4063        else {
4064            ogeom_bail!(Construction, "a band has two rims");
4065        };
4066        for (edge, at, with) in [(low, v_low, low_with), (high, v_high, high_with)] {
4067            let (a, b) = if with { (0.0, tau) } else { (tau, 0.0) };
4068            let (from, to) = if round_tube {
4069                ((at, a), (at, b))
4070            } else {
4071                ((a, at), (b, at))
4072            };
4073            let pcurve = linear(from, to, (0.0, tau), self.tol)?;
4074            crate::build::attach_pcurve(
4075                self.model,
4076                &edges[edge],
4077                pcurve,
4078                surface,
4079                Location::identity(),
4080                (0.0, tau),
4081            )?;
4082        }
4083        // The seam, low rim to high, on the surface along angle zero: a
4084        // ruling, a meridian of a sphere, a circle of a torus's tube, or
4085        // for a band round the tube, an arc of its outer equator.
4086        let seam_curve: Curve = match curved.shape {
4087            Canonical::Torus(t) if round_tube => ogeom_geom::CircleCurve::new(
4088                ogeom_math::Circle::new(frame, t.major_radius() + t.minor_radius(), self.tol)?,
4089            )
4090            .into(),
4091            Canonical::Sphere(s) => {
4092                let normal =
4093                    Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4094                ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4095                    Frame::new(s.centre(), normal, frame.x(), self.tol)?,
4096                    s.radius(),
4097                    self.tol,
4098                )?)
4099                .into()
4100            }
4101            Canonical::Torus(t) => {
4102                let spine = frame.origin() + frame.x().vector() * t.major_radius();
4103                let normal =
4104                    Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4105                ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4106                    Frame::new(spine, normal, frame.x(), self.tol)?,
4107                    t.minor_radius(),
4108                    self.tol,
4109                )?)
4110                .into()
4111            }
4112            _ => LineCurve::segment(low_at, high_at, self.tol)?.into(),
4113        };
4114        let seam_range = match curved.shape {
4115            Canonical::Torus(_) | Canonical::Sphere(_) => (v_low, v_high),
4116            _ => (0.0, low_at.distance(high_at)),
4117        };
4118        // The seam runs between the rims' own vertices, placed where each
4119        // circle's parameter starts: the surface's angle zero.
4120        let placed = |at: Point| {
4121            self.plan
4122                .placed
4123                .iter()
4124                .any(|p| p.distance(at) <= self.tol.confusion())
4125        };
4126        if !placed(low_at) || !placed(high_at) {
4127            ogeom_bail!(
4128                Construction,
4129                "a band's rim vertex is not where its seam starts"
4130            );
4131        }
4132        let vertex = |edge: usize| -> OgeomResult<Shape> {
4133            match self.model.children_of(&edges[edge])?.first() {
4134                Some(v) => Ok(v.clone()),
4135                None => ogeom_bail!(Construction, "a rim has no vertex"),
4136            }
4137        };
4138        let (from_vertex, to_vertex) = (vertex(low)?, vertex(high)?);
4139        let id = self.model.geometry_mut().add_curve(seam_curve);
4140        let data = EdgeData::on_curve(id, Location::identity(), seam_range);
4141        let seam = self.model.add_edge(data, &[from_vertex, to_vertex])?;
4142        // The seam's two images: where the wire walks it forward, then back.
4143        let (forward, back) = if round_tube {
4144            (
4145                linear((v_low, 0.0), (v_high, 0.0), seam_range, self.tol)?,
4146                linear((v_low, tau), (v_high, tau), seam_range, self.tol)?,
4147            )
4148        } else {
4149            (
4150                linear((tau, v_low), (tau, v_high), seam_range, self.tol)?,
4151                linear((0.0, v_low), (0.0, v_high), seam_range, self.tol)?,
4152            )
4153        };
4154        crate::build::attach_seam(
4155            self.model,
4156            &seam,
4157            forward,
4158            back,
4159            surface,
4160            Location::identity(),
4161            seam_range,
4162        )?;
4163        // Counter-clockwise in the chart, then the whole ring the other way
4164        // for a face that faces against the surface. Round the axis: along
4165        // the low rim, up the seam's far side, back along the high rim, down
4166        // its near side. Round the tube: along the seam's near side, up the
4167        // high rim, back along the seam's far side, down the low rim.
4168        let mut ring = if round_tube {
4169            vec![
4170                seam.clone(),
4171                oriented(&edges[high], high_with),
4172                seam.reversed(),
4173                oriented(&edges[low], !low_with),
4174            ]
4175        } else {
4176            vec![
4177                oriented(&edges[low], low_with),
4178                seam.clone(),
4179                oriented(&edges[high], !high_with),
4180                seam.reversed(),
4181            ]
4182        };
4183        if !outward {
4184            ring.reverse();
4185            ring = ring.iter().map(Shape::reversed).collect();
4186        }
4187        let wire = self.model.add_wire(&ring)?;
4188        let mut data = FaceData::new(surface, Location::identity());
4189        data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4190        let face = self.model.add_face(data, std::slice::from_ref(&wire))?;
4191        Ok(if outward { face } else { face.reversed() })
4192    }
4193}
4194
4195impl Builder<'_> {
4196    /// A sphere's cap: its rim, a meridian seam from the rim to the pole,
4197    /// and the pole as an edge of no length, the frame's axis pointing into
4198    /// the cap so the pole is the north one.
4199    fn cap_face(
4200        &mut self,
4201        curved: &Curved,
4202        rings: &[Vec<Half>],
4203        edges: &[Shape],
4204    ) -> OgeomResult<Shape> {
4205        use ogeom_geom::Curve3d as _;
4206        let tau = core::f64::consts::TAU;
4207        let north = core::f64::consts::FRAC_PI_2;
4208        let g = self.groups.of[rings[0][0] / 3];
4209        let Canonical::Sphere(sphere) = curved.shape else {
4210            ogeom_bail!(Construction, "a cap is a sphere's");
4211        };
4212        let Some(geometry) = self.plan.surfaces[g].clone() else {
4213            ogeom_bail!(Construction, "a cap was planned without its surface");
4214        };
4215        let surface = self.model.geometry_mut().add_surface(geometry);
4216        let outward = self.outward(curved, g);
4217        let frame = sphere.frame();
4218        let (rim, _) = self.entry(rings[0][0]);
4219        let spec = &self.plan.edges[rim];
4220        let Curve::Circle(c) = &spec.curve else {
4221            ogeom_bail!(Construction, "a cap's rim is not a circle");
4222        };
4223        let with = c.circle().frame().z().vector().dot(frame.z().vector()) > 0.0;
4224        let start = spec.curve.point_at(0.0, self.tol)?;
4225        let Some((_, v_rim)) = unwrapped(curved, start, self.tol) else {
4226            ogeom_bail!(Construction, "a cap's rim has no chart position");
4227        };
4228        let (a, b) = if with { (0.0, tau) } else { (tau, 0.0) };
4229        crate::build::attach_pcurve(
4230            self.model,
4231            &edges[rim],
4232            linear((a, v_rim), (b, v_rim), (0.0, tau), self.tol)?,
4233            surface,
4234            Location::identity(),
4235            (0.0, tau),
4236        )?;
4237        let Some(rim_vertex) = self.model.children_of(&edges[rim])?.first().cloned() else {
4238            ogeom_bail!(Construction, "a rim has no vertex");
4239        };
4240        let pole = self.model.add_vertex(VertexData::new(
4241            sphere.centre() + frame.z().vector() * sphere.radius(),
4242        ));
4243        let normal = Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4244        let meridian: Curve = ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4245            Frame::new(sphere.centre(), normal, frame.x(), self.tol)?,
4246            sphere.radius(),
4247            self.tol,
4248        )?)
4249        .into();
4250        let id = self.model.geometry_mut().add_curve(meridian);
4251        let seam_range = (v_rim, north);
4252        let seam = self.model.add_edge(
4253            EdgeData::on_curve(id, Location::identity(), seam_range),
4254            &[rim_vertex, pole.clone()],
4255        )?;
4256        crate::build::attach_seam(
4257            self.model,
4258            &seam,
4259            linear((tau, v_rim), (tau, north), seam_range, self.tol)?,
4260            linear((0.0, v_rim), (0.0, north), seam_range, self.tol)?,
4261            surface,
4262            Location::identity(),
4263            seam_range,
4264        )?;
4265        let mut data = EdgeData::new();
4266        data.degenerate = true;
4267        let tip = self.model.add_edge(data, &[pole.clone(), pole])?;
4268        crate::build::attach_pcurve(
4269            self.model,
4270            &tip,
4271            linear((0.0, north), (tau, north), (0.0, tau), self.tol)?,
4272            surface,
4273            Location::identity(),
4274            (0.0, tau),
4275        )?;
4276        // Counter-clockwise in the chart: along the rim, up the seam's far
4277        // side, back along the pole, down the seam's near side.
4278        let mut ring = vec![
4279            oriented(&edges[rim], with),
4280            seam.clone(),
4281            tip.reversed(),
4282            seam.reversed(),
4283        ];
4284        if !outward {
4285            ring.reverse();
4286            ring = ring.iter().map(Shape::reversed).collect();
4287        }
4288        let wire = self.model.add_wire(&ring)?;
4289        let mut data = FaceData::new(surface, Location::identity());
4290        data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4291        let face = self.model.add_face(data, std::slice::from_ref(&wire))?;
4292        Ok(if outward { face } else { face.reversed() })
4293    }
4294
4295    /// A whole sphere or torus, a piece with no boundary: the face the
4296    /// primitive builds on the recognized surface.
4297    /// A sphere or torus whole but for holes: the whole surface's face, as
4298    /// the primitive builds it, with each ring an inner wire. The rings
4299    /// are turned to run as the primitive's own wires do, so the face
4300    /// flips as one where the region faces against its surface.
4301    fn holed_face(
4302        &mut self,
4303        curved: &Curved,
4304        g: usize,
4305        rings: &[Vec<Half>],
4306        edges: &[Shape],
4307    ) -> OgeomResult<Shape> {
4308        let outward = self.outward(curved, g);
4309        let whole = self.whole_face(curved, g)?;
4310        let whole = if outward { whole } else { whole.reversed() };
4311        let Some(ogeom_topo::NodeData::Face(data)) =
4312            self.model.node(&whole).map(|n| n.data().clone())
4313        else {
4314            ogeom_bail!(Construction, "a whole surface's face has no data");
4315        };
4316        let surface = data.surface;
4317        let mut wires = self.model.ordered_children_of(&whole)?;
4318        for ring in rings {
4319            let mut ring_edges = Vec::new();
4320            for (edge, forward) in self.entries(ring) {
4321                if !self.has_pcurve(&edges[edge], surface) {
4322                    let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned()
4323                    else {
4324                        ogeom_bail!(
4325                            Construction,
4326                            "an edge was planned without its image on a face"
4327                        );
4328                    };
4329                    self.model.widen(
4330                        &edges[edge],
4331                        Tolerance::new(deviation.max(self.tol.confusion()))?,
4332                    )?;
4333                    crate::build::attach_pcurve(
4334                        self.model,
4335                        &edges[edge],
4336                        pcurve,
4337                        surface,
4338                        Location::identity(),
4339                        self.plan.edges[edge].range,
4340                    )?;
4341                }
4342                ring_edges.push(oriented(&edges[edge], forward));
4343            }
4344            if !outward {
4345                ring_edges.reverse();
4346                ring_edges = ring_edges.iter().map(Shape::reversed).collect();
4347            }
4348            wires.push(self.model.add_wire(&ring_edges)?);
4349        }
4350        crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
4351        let mut face_data = FaceData::new(surface, Location::identity());
4352        face_data.tolerance = data.tolerance;
4353        let face = self.model.add_face(face_data, &wires)?;
4354        Ok(if outward { face } else { face.reversed() })
4355    }
4356
4357    fn whole_face(&mut self, curved: &Curved, g: usize) -> OgeomResult<Shape> {
4358        let outward = self.outward(curved, g);
4359        let built = match curved.shape {
4360            Canonical::Sphere(s) => {
4361                crate::primitive::make_sphere(self.model, s.frame(), s.radius(), self.tol)?
4362            }
4363            Canonical::Torus(t) => crate::primitive::make_torus(
4364                self.model,
4365                t.frame(),
4366                t.major_radius(),
4367                t.minor_radius(),
4368                self.tol,
4369            )?,
4370            _ => ogeom_bail!(Construction, "only a sphere or a torus is whole"),
4371        };
4372        let Some(face) =
4373            ogeom_topo::explore_unique(self.model, &built.shape, ogeom_topo::ShapeType::Face)?
4374                .into_iter()
4375                .next()
4376        else {
4377            ogeom_bail!(Construction, "a primitive came back with no face");
4378        };
4379        if let Some(node) = self.model.node_mut(&face)
4380            && let ogeom_topo::NodeData::Face(data) = node.data_mut()
4381        {
4382            data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4383        }
4384        Ok(if outward { face } else { face.reversed() })
4385    }
4386}
4387
4388fn oriented(edge: &Shape, forward: bool) -> Shape {
4389    if forward {
4390        edge.clone()
4391    } else {
4392        edge.reversed()
4393    }
4394}
4395
4396/// A straight chart segment from `a` to `b`, linear in the edge's own
4397/// parameter over `range`.
4398fn linear(
4399    a: (f64, f64),
4400    b: (f64, f64),
4401    range: (f64, f64),
4402    tol: Tolerances,
4403) -> OgeomResult<PlanarCurve> {
4404    let knots = ogeom_math::KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
4405    Ok(ogeom_geom::BSpline2d::new(
4406        knots,
4407        vec![Point2::new(a.0, a.1), Point2::new(b.0, b.1)],
4408        tol,
4409    )?
4410    .into())
4411}
4412
4413/// Where a rim's entry starts: the entry, its vertex, and where that
4414/// stands.
4415type RimStart = ((usize, bool), Shape, Point);
4416
4417/// A seam's two rim vertices, by their places in each rim, and the ends of
4418/// its line in the chart.
4419type SeamChoice = (usize, usize, (f64, f64), (f64, f64));
4420
4421/// A face's holes in its chart: each a polygon of its mesh vertices,
4422/// carried round continuously.
4423fn hole_polygons(
4424    shape: &Canonical,
4425    holes: &[&[Half]],
4426    triangles: &[[u32; 3]],
4427    points: &[Point],
4428    tol: Tolerances,
4429) -> Vec<Vec<(f64, f64)>> {
4430    holes
4431        .iter()
4432        .filter_map(|ring| {
4433            let mut out: Vec<(f64, f64)> = Vec::new();
4434            for &h in *ring {
4435                let (a, _) = from_to(triangles, h);
4436                let (u, v) = chart(shape, points[a as usize], tol)?;
4437                let u = match out.last() {
4438                    Some(&(last, _)) => last + ogeom_math::elementary::wrap_signed_angle(u - last),
4439                    None => u,
4440                };
4441                out.push((u, v));
4442            }
4443            Some(out)
4444        })
4445        .collect()
4446}
4447
4448/// The seam for a face round its axis: of the pairs of a vertex on one
4449/// rim and a vertex on the other, the one turning least between them whose
4450/// straight chart line crosses no hole, a whole turn either way included.
4451/// Its indices and the line's ends in the chart.
4452fn choose_seam(
4453    shape: &Canonical,
4454    from: &[Point],
4455    to: &[Point],
4456    holes: &[Vec<(f64, f64)>],
4457    tol: Tolerances,
4458) -> Option<SeamChoice> {
4459    let tau = core::f64::consts::TAU;
4460    let crosses = |a: (f64, f64), b: (f64, f64)| {
4461        holes.iter().any(|ring| {
4462            [-tau, 0.0, tau].iter().any(|shift| {
4463                (0..ring.len()).any(|i| {
4464                    let (p, q) = (ring[i], ring[(i + 1) % ring.len()]);
4465                    segments_cross(a, b, (p.0 + shift, p.1), (q.0 + shift, q.1))
4466                })
4467            })
4468        })
4469    };
4470    let mut best: Option<(f64, SeamChoice)> = None;
4471    for (i, pa) in from.iter().enumerate() {
4472        let Some((ua, va)) = chart(shape, *pa, tol) else {
4473            continue;
4474        };
4475        for (j, pb) in to.iter().enumerate() {
4476            let Some((ub, vb)) = chart(shape, *pb, tol) else {
4477                continue;
4478            };
4479            let turn = ogeom_math::elementary::wrap_signed_angle(ub - ua);
4480            let (a, b) = ((ua, va), (ua + turn, vb));
4481            if crosses(a, b) {
4482                continue;
4483            }
4484            let score = turn.abs() * 1e3 + pa.distance(*pb);
4485            if best.is_none_or(|held| score < held.0) {
4486                best = Some((score, (i, j, a, b)));
4487            }
4488        }
4489    }
4490    best.map(|(_, choice)| choice)
4491}
4492
4493/// The middle of the widest stretch of angle no hole covers.
4494fn free_angle(holes: &[Vec<(f64, f64)>]) -> Option<f64> {
4495    let tau = core::f64::consts::TAU;
4496    let mut angles: Vec<f64> = holes
4497        .iter()
4498        .flatten()
4499        .map(|(u, _)| u.rem_euclid(tau))
4500        .collect();
4501    if angles.is_empty() {
4502        return None;
4503    }
4504    angles.sort_by(f64::total_cmp);
4505    let mut best = (
4506        angles[0] + tau - angles[angles.len() - 1],
4507        angles[angles.len() - 1],
4508    );
4509    for pair in angles.windows(2) {
4510        if pair[1] - pair[0] > best.0 {
4511            best = (pair[1] - pair[0], pair[0]);
4512        }
4513    }
4514    Some(best.1 + best.0 / 2.0)
4515}
4516
4517/// Whether two chart segments cross, each at a point strictly inside both.
4518fn segments_cross(a: (f64, f64), b: (f64, f64), p: (f64, f64), q: (f64, f64)) -> bool {
4519    let side = |o: (f64, f64), x: (f64, f64), y: (f64, f64)| {
4520        (x.0 - o.0).mul_add(y.1 - o.1, -((x.1 - o.1) * (y.0 - o.0)))
4521    };
4522    let (d1, d2) = (side(p, q, a), side(p, q, b));
4523    let (d3, d4) = (side(a, b, p), side(a, b, q));
4524    d1 * d2 < 0.0 && d3 * d4 < 0.0
4525}
4526
4527/// How many times a ring of half-edges goes round a surface's angle, with
4528/// the sign of its sense; `None` where a vertex has no chart position.
4529fn winding(
4530    shape: &Canonical,
4531    ring: &[Half],
4532    triangles: &[[u32; 3]],
4533    points: &[Point],
4534    tol: Tolerances,
4535) -> Option<i32> {
4536    let mut turned = 0.0;
4537    for &h in ring {
4538        let (a, b) = from_to(triangles, h);
4539        let (ua, _) = chart(shape, points[a as usize], tol)?;
4540        let (ub, _) = chart(shape, points[b as usize], tol)?;
4541        turned += ogeom_math::elementary::wrap_signed_angle(ub - ua);
4542    }
4543    #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
4544    Some((turned / core::f64::consts::TAU).round() as i32)
4545}
4546
4547/// How many times a ring goes round each of a surface's chart directions,
4548/// as [`winding`] counts the first.
4549fn windings(
4550    shape: &Canonical,
4551    ring: &[Half],
4552    triangles: &[[u32; 3]],
4553    points: &[Point],
4554    tol: Tolerances,
4555) -> Option<(i32, i32)> {
4556    let (_, wraps_v) = periodic(shape);
4557    let mut turned = (0.0, 0.0);
4558    for &h in ring {
4559        let (a, b) = from_to(triangles, h);
4560        let (ua, va) = chart(shape, points[a as usize], tol)?;
4561        let (ub, vb) = chart(shape, points[b as usize], tol)?;
4562        turned.0 += ogeom_math::elementary::wrap_signed_angle(ub - ua);
4563        if wraps_v {
4564            turned.1 += ogeom_math::elementary::wrap_signed_angle(vb - va);
4565        }
4566    }
4567    let tau = core::f64::consts::TAU;
4568    #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
4569    Some((
4570        (turned.0 / tau).round() as i32,
4571        (turned.1 / tau).round() as i32,
4572    ))
4573}
4574
4575/// Twice the area a ring of half-edges encloses in a chart, signed.
4576fn ring_area(
4577    ring: &[Half],
4578    triangles: &[[u32; 3]],
4579    chart: impl Fn(Point) -> Option<Point2>,
4580    points: &[Point],
4581) -> f64 {
4582    let mut area = 0.0;
4583    for &h in ring {
4584        let (a, b) = from_to(triangles, h);
4585        if let (Some(a), Some(b)) = (chart(points[a as usize]), chart(points[b as usize])) {
4586            area += a.x * b.y - b.x * a.y;
4587        }
4588    }
4589    area
4590}
4591
4592fn distance_to_line(p: Point, a: Point, b: Point) -> f64 {
4593    let d = b - a;
4594    let m = d.magnitude();
4595    if m == 0.0 {
4596        return p.distance(a);
4597    }
4598    (p - a).cross(d).magnitude() / m
4599}