Skip to main content

geop_ops_step/import/
body.rs

1//! One body of a STEP file — a solid or a sheet — as a [`BodySpec`] the
2//! kernel builds.
3//!
4//! A STEP B-rep and a geop one differ in three ways, and reading one is
5//! mostly bridging them:
6//!
7//! - **Periodic surfaces.** STEP parametrizes a cylinder, cone, sphere or
8//!   torus all the way round, so a face that wraps around its axis either
9//!   runs along a *seam* — one edge its loop uses twice — or is bounded by
10//!   loops that each go once round. A geop face lies on a NURBS patch that
11//!   does not wrap: its loops close in the patch's `(u, v)`. So a wrapping
12//!   face loses its seam and is cut along meridians into sectors, each on a
13//!   patch of its own (see [`Builder::cut_wrapping_face`]); one going round
14//!   a torus' tube — a pipe bend's — is cut along parallels instead
15//!   ([`Builder::cut_tube_wrapping_face`]), and a whole torus into bands
16//!   round its axis first ([`Builder::cut_whole_torus`]). A face turning
17//!   about its axis more than once without going round it — a thread's
18//!   flank — is cut along meridians too ([`Builder::cut_helical_face`]).
19//! - **Closed edges.** A STEP edge may start and end at one vertex — a whole
20//!   circle. A geop edge may not, so one is split in two.
21//! - **Tolerance.** A STEP file's vertices, curves and surfaces agree only
22//!   to within the file's accuracy. A geop vertex is an enclosure, so each
23//!   is the union of every place the file says it is: its point, the ends
24//!   of its edges' curves, and its foot points on its faces' surfaces (see
25//!   `AGENTS.md` on combining enclosures of one value with `union`). An
26//!   edge is widened until it reaches the midpoints of its pcurve's points
27//!   on each of its faces. What lies further apart than the kernel's
28//!   accuracy is rebuilt from the surfaces, which a B-rep is defined by:
29//!   a vertex where its faces' surfaces meet, an edge along where they
30//!   meet — and where the surfaces themselves contradict each other, the
31//!   smallest faces there from their edges (see [`heal`]). The file's
32//!   uncertainty bounds it: what is rebuilt lies on its surfaces to within
33//!   it, and what cannot be is refused, naming the entities and saying how
34//!   far. What was rebuilt, and how far it moved, is reported
35//!   ([`Healing`]).
36//!
37//! Every surface is built as an exact NURBS patch covering its face — or,
38//! for a B-spline, taken as it is — oriented so its normal points out of
39//! the material, and every pcurve is fitted by projecting the edge onto it
40//! (`NurbSurface::fit_pcurve`), as the kernel fits all its pcurves.
41
42use std::collections::HashMap;
43
44use serde::{Deserialize, Serialize};
45
46use geop_core_geometry::{
47    nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, loop_signed_area},
48    nurb_surface::NurbSurface3D,
49};
50use geop_core_math::{
51    geop_error::{GeopError, GeopResult, WithContext},
52    plain::{P3, add, cross, distance, dot, norm, normalize, scale, sub, unwrap_angle},
53    scalars::Scalar,
54    solvers::{
55        bisect::{Bracket, bisect},
56        iterate::{Repeat, Step, iterate},
57        linear::solve_symmetric2,
58        refine::{Refine, Refined, refine},
59        repeat::{self, repeat},
60        walk::{Walked, walk},
61    },
62    vector::{Vector2, Vector3},
63};
64use geop_core_topology::{
65    Sense,
66    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
67    contains::face::{PointClassification, pcurves_contain},
68};
69
70use super::{
71    geometry::{Frame, Profile, Revolved, Scope, Shape, SurfaceDef, SurfaceKind, parameter_of},
72    midpoints::{CurveMidpoints, SurfaceMidpoints},
73    reader::{Reader, unsupported},
74    structure::Item,
75};
76
77/// A body read from a file, ready to build: its description, and the name
78/// of every entity in it — the arguments a step's names are made of, as
79/// `["f3", "q1"]` for the second sector of the file's fourth face. It
80/// serializes exactly, as its body does: what a file read as is kept
81/// (`geop_core_program::cache`).
82#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83pub struct ImportedBody {
84    /// What the file calls it.
85    pub label: String,
86    pub spec: BodySpec,
87    pub vertex_names: Vec<Vec<String>>,
88    pub edge_names: Vec<Vec<String>>,
89    pub face_names: Vec<Vec<String>>,
90    /// What was rebuilt where the file disagrees with itself further than
91    /// the kernel can carry (see `heal`).
92    pub healed: Healing,
93}
94
95/// What [`heal`] rebuilt of a body where its file disagrees with itself
96/// further than the kernel can carry, and how far it moved.
97#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
98pub struct Healing {
99    /// Each vertex moved to where its faces meet, and how far.
100    pub vertices: Vec<(String, f64)>,
101    /// Each edge rebuilt where its faces meet, and how far it moved at most.
102    pub edges: Vec<(String, f64)>,
103    /// Each face rebuilt from its edges.
104    pub faces: Vec<String>,
105    /// The file's uncertainty, which everything rebuilt meets to.
106    pub uncertainty: f64,
107}
108
109impl Healing {
110    pub fn is_empty(&self) -> bool {
111        self.vertices.is_empty() && self.edges.is_empty() && self.faces.is_empty()
112    }
113
114    /// How far anything moved, at most.
115    pub fn furthest(&self) -> f64 {
116        self.vertices
117            .iter()
118            .chain(&self.edges)
119            .map(|m| m.1)
120            .fold(0.0, f64::max)
121    }
122
123    /// In one sentence, for a user: how far, how much, which faces.
124    pub fn summary(&self) -> String {
125        let mut out = format!(
126            "its faces disagree with each other by up to {:.1e} mm (the file states {:e} mm): {} vertices and {} edges were moved onto them",
127            self.furthest(),
128            self.uncertainty,
129            self.vertices.len(),
130            self.edges.len()
131        );
132        if !self.faces.is_empty() {
133            out += &format!(
134                ", and {} faces rebuilt from their edges: {}",
135                self.faces.len(),
136                self.faces.join(", ")
137            );
138        }
139        out
140    }
141
142    /// Entity by entity, how far each moved.
143    pub fn details(&self) -> Vec<String> {
144        let line = |what: &str, moved: &[(String, f64)]| {
145            let names: Vec<String> = moved
146                .iter()
147                .map(|(name, by)| format!("{name} by {by:.1e}"))
148                .collect();
149            format!("{what}: {}", names.join(", "))
150        };
151        let mut out = Vec::new();
152        if !self.vertices.is_empty() {
153            out.push(line("vertices put where their faces meet", &self.vertices));
154        }
155        if !self.edges.is_empty() {
156            out.push(line("edges rebuilt where their faces meet", &self.edges));
157        }
158        if !self.faces.is_empty() {
159            out.push(format!(
160                "faces rebuilt from their edges, their surfaces contradicting their neighbours': {}",
161                self.faces.join(", ")
162            ));
163        }
164        out
165    }
166}
167
168/// Search budget for the containment searches `fit_pcurve` falls back on.
169/// Every pcurve here is pinned at both ends, so they are not run; the
170/// values are the kernel's usual ones.
171const MAX_NODES: usize = 5000;
172const MIN_SUBDIVISION_SIZE: f64 = 1e-4;
173/// How many faces normalizing a body may look at, each kept or cut into
174/// pieces that are looked at in turn: an effort budget only. A body whose
175/// faces keep being cut beyond it is refused, never taken as normalized.
176const NORMALIZE_STEPS: usize = 1_000_000;
177
178/// Samples per edge when walking a loop around an axis: enough that two
179/// consecutive ones are less than half a turn apart even on an edge going
180/// all the way round.
181const LOOP_SAMPLES: usize = 16;
182
183/// Candidate axes for a sphere whose file's axis does not serve (see
184/// [`Builder::sphere_axis`]).
185const SPHERE_AXES: usize = 256;
186
187struct Vertex {
188    /// Every place the file says the vertex is, united (see the module
189    /// docs): grows as its edges and faces are built.
190    point: Vector3,
191    /// Where the vertex itself says it is: what its foot points on its
192    /// faces are projected from, so that one face's foot point does not
193    /// widen what the next one is projected from.
194    origin: Vector3,
195    name: Vec<String>,
196    /// The file's `VERTEX_POINT`, for one read from it.
197    id: Option<u64>,
198}
199
200impl Vertex {
201    /// Its name, and the file's id of it where it has one: for messages.
202    fn label(&self) -> String {
203        label(&self.name, self.id)
204    }
205}
206
207/// Points along the loop `lp` of `edges`, `LOOP_SAMPLES` per coedge as it
208/// runs, each coedge's end left out as the next one's start. Where to
209/// sample is a free choice, and the points are compared in `f64`.
210fn loop_points(edges: &[Edge], lp: &[Use]) -> Vec<P3> {
211    let mut points = Vec::new();
212    for &(e, forward) in lp {
213        let curve = CurveMidpoints::of(&edges[e].curve);
214        let (lo, hi) = curve.domain();
215        for i in 0..LOOP_SAMPLES {
216            let f = i as f64 / LOOP_SAMPLES as f64;
217            let f = if forward { f } else { 1.0 - f };
218            points.push(curve.point(lo + (hi - lo) * f));
219        }
220    }
221    points
222}
223
224struct Edge {
225    curve: NurbCurve3D,
226    start: usize,
227    end: usize,
228    name: Vec<String>,
229    /// False once split into pieces or found to be a seam.
230    alive: bool,
231    /// The file's `EDGE_CURVE`, for one read from it or a piece of one.
232    id: Option<u64>,
233    /// Where the file's curve is a circle or a line, which: for one read
234    /// from it or a piece of one, until it is rebuilt.
235    shape: Option<Shape>,
236}
237
238impl Edge {
239    /// Its name, and the file's id of it where it has one: for messages.
240    fn label(&self) -> String {
241        label(&self.name, self.id)
242    }
243
244    /// The vertex a coedge running along it `forward` — or against it —
245    /// starts at, and the one it ends at.
246    fn ends(&self, forward: bool) -> (usize, usize) {
247        if forward {
248            (self.start, self.end)
249        } else {
250            (self.end, self.start)
251        }
252    }
253}
254
255/// `name`, and the file's id `#id` where there is one: `e3,p1 (#120)`.
256fn label(name: &[String], id: Option<u64>) -> String {
257    match id {
258        Some(id) => format!("{} (#{id})", name.join(",")),
259        None => name.join(","),
260    }
261}
262
263/// A coedge of a loop: an edge, and whether the loop runs along it.
264type Use = (usize, bool);
265
266struct Face {
267    surface: SurfaceDef,
268    /// Whether the face's outward normal is the surface's normal as the
269    /// file defines it.
270    same_sense: bool,
271    /// Loops, each running counter-clockwise about the outward normal
272    /// around the face — the outer one — or clockwise around a hole.
273    loops: Vec<Vec<Use>>,
274    /// Which loop is the outer one, where that is known by construction.
275    outer: Option<usize>,
276    /// The patch, where cutting the face decided it: the angles and
277    /// profile parameters of a sector.
278    patch: Option<[f64; 4]>,
279    shell: usize,
280    name: Vec<String>,
281    /// For messages: the file's face.
282    id: u64,
283}
284
285impl Face {
286    /// Whether the NURBS built for the surface points out of the material
287    /// as it is, or has to be turned around.
288    fn outward_is_natural(&self) -> bool {
289        self.same_sense != self.surface.flipped
290    }
291}
292
293struct Builder<'r> {
294    reader: Reader<'r>,
295    scope: Scope,
296    vertices: Vec<Vertex>,
297    vertex_index: HashMap<u64, usize>,
298    edges: Vec<Edge>,
299    edge_index: HashMap<u64, usize>,
300    faces: Vec<Face>,
301    surfaces: HashMap<u64, SurfaceDef>,
302    shells: usize,
303}
304
305fn s3(p: P3) -> Vector3 {
306    Vector3::from_array(p.map(Scalar::from_f64))
307}
308
309fn name(prefix: &str, k: usize) -> Vec<String> {
310    vec![format!("{prefix}{k}")]
311}
312
313/// The body `item` of the file, as a description to build.
314pub fn read_body(reader: Reader<'_>, item: &Item) -> GeopResult<ImportedBody> {
315    let mut builder = Builder {
316        reader,
317        scope: item.scope,
318        vertices: Vec::new(),
319        vertex_index: HashMap::new(),
320        edges: Vec::new(),
321        edge_index: HashMap::new(),
322        faces: Vec::new(),
323        surfaces: HashMap::new(),
324        shells: 0,
325    };
326    let instance = reader.instance(item.id)?;
327    let solid = !instance.is("SHELL_BASED_SURFACE_MODEL");
328    if instance.is("MANIFOLD_SOLID_BREP") {
329        let args = reader.args(item.id, "MANIFOLD_SOLID_BREP")?;
330        builder.shell(args.reference(1)?, false)?;
331    } else if instance.is("BREP_WITH_VOIDS") {
332        let args = reader.args(item.id, "BREP_WITH_VOIDS")?;
333        builder.shell(args.reference(1)?, false)?;
334        for void in args.references(2)? {
335            let v = reader.args(void, "ORIENTED_CLOSED_SHELL")?;
336            builder.shell(v.reference(2)?, !v.logical(3)?)?;
337        }
338    } else {
339        let args = reader.args(item.id, "SHELL_BASED_SURFACE_MODEL")?;
340        for shell in args.references(1)? {
341            let s = reader.instance(shell)?;
342            if s.is("ORIENTED_OPEN_SHELL") || s.is("ORIENTED_CLOSED_SHELL") {
343                let (_, o) =
344                    reader.args_of_any(shell, &["ORIENTED_OPEN_SHELL", "ORIENTED_CLOSED_SHELL"])?;
345                builder.shell(o.reference(2)?, !o.logical(3)?)?;
346            } else {
347                builder.shell(shell, false)?;
348            }
349        }
350    }
351    builder.normalize()?;
352    builder.spec(solid, item.label.clone())
353}
354
355impl Builder<'_> {
356    fn shell(&mut self, id: u64, reversed: bool) -> GeopResult<()> {
357        let (_, args) = self
358            .reader
359            .args_of_any(id, &["CLOSED_SHELL", "OPEN_SHELL"])?;
360        let shell = self.shells;
361        self.shells += 1;
362        for face in args.references(1)? {
363            self.face(face, shell, reversed)
364                .map_err(|e| e.with_context(format!("reading the face #{face}")))?;
365        }
366        Ok(())
367    }
368
369    fn vertex(&mut self, id: u64) -> GeopResult<usize> {
370        if let Some(&v) = self.vertex_index.get(&id) {
371            return Ok(v);
372        }
373        let args = self.reader.args(id, "VERTEX_POINT")?;
374        let point = self.reader.point(&self.scope, args.reference(1)?)?;
375        let v = self.vertices.len();
376        self.vertices.push(Vertex {
377            point: s3(point),
378            origin: s3(point),
379            name: name("v", self.vertex_index.len()),
380            id: Some(id),
381        });
382        self.vertex_index.insert(id, v);
383        Ok(v)
384    }
385
386    fn edge(&mut self, id: u64) -> GeopResult<usize> {
387        if let Some(&e) = self.edge_index.get(&id) {
388            return Ok(e);
389        }
390        let ctx = |e: GeopError| e.with_context(format!("reading the edge #{id}"));
391        let args = self.reader.args(id, "EDGE_CURVE").map_err(ctx)?;
392        let start = self.vertex(args.reference(1)?).map_err(ctx)?;
393        let end = self.vertex(args.reference(2)?).map_err(ctx)?;
394        let (curve, reversed) = self
395            .reader
396            .curve(&self.scope, args.reference(3)?)
397            .map_err(ctx)?;
398        let shape = curve.shape();
399        let along = args.logical(4).map_err(ctx)? != reversed;
400        let (a, b) = (
401            self.vertices[start].point.to_f64_array(),
402            self.vertices[end].point.to_f64_array(),
403        );
404        let closed = start == end;
405        let curve = if along {
406            curve.edge(a, b, closed, self.scope.uncertainty)
407        } else {
408            curve
409                .edge(b, a, closed, self.scope.uncertainty)
410                .map(|c| c.reverse())
411        }
412        .map_err(ctx)?;
413        let e = self.edges.len();
414        self.edges.push(Edge {
415            curve,
416            start,
417            end,
418            name: name("e", self.edge_index.len()),
419            alive: true,
420            id: Some(id),
421            shape,
422        });
423        self.edge_index.insert(id, e);
424        Ok(e)
425    }
426
427    /// The edge an `ORIENTED_EDGE` runs along, and whether it runs along
428    /// it forwards.
429    fn oriented_edge(&mut self, id: u64) -> GeopResult<Use> {
430        let instance = self.reader.instance(id)?;
431        if instance.is("EDGE_CURVE") {
432            return Ok((self.edge(id)?, true));
433        }
434        let args = self.reader.args(id, "ORIENTED_EDGE")?;
435        let (edge, forward) = self.oriented_edge(args.reference(3)?)?;
436        Ok((edge, forward == args.logical(4)?))
437    }
438
439    fn face(&mut self, id: u64, shell: usize, reversed: bool) -> GeopResult<()> {
440        let (_, args) = self
441            .reader
442            .args_of_any(id, &["ADVANCED_FACE", "FACE_SURFACE"])?;
443        let surface_id = args.reference(2)?;
444        let surface = match self.surfaces.get(&surface_id) {
445            Some(s) => s.clone(),
446            None => {
447                let s = self.reader.surface(&self.scope, surface_id)?;
448                self.surfaces.insert(surface_id, s.clone());
449                s
450            }
451        };
452        let mut loops = Vec::new();
453        for bound in args.references(1)? {
454            let (_, b) = self
455                .reader
456                .args_of_any(bound, &["FACE_OUTER_BOUND", "FACE_BOUND"])?;
457            let lp = b.reference(1)?;
458            let forward = b.logical(2)? != reversed;
459            let lp_instance = self.reader.instance(lp)?;
460            if lp_instance.is("VERTEX_LOOP") {
461                // A loop of one vertex marks a pole or an apex, which the
462                // face's patch has anyway.
463                continue;
464            }
465            if !lp_instance.is("EDGE_LOOP") {
466                return Err(unsupported(
467                    lp,
468                    lp_instance,
469                    "a face bound that is not a loop of edges",
470                ));
471            }
472            let mut coedges = Vec::new();
473            for oriented in self.reader.args(lp, "EDGE_LOOP")?.references(1)? {
474                coedges.push(self.oriented_edge(oriented)?);
475            }
476            if !forward {
477                coedges = coedges.into_iter().rev().map(|(e, f)| (e, !f)).collect();
478            }
479            if coedges.is_empty() {
480                continue;
481            }
482            loops.push(coedges);
483        }
484        let surface = match surface.revolved() {
485            Some(revolved) if matches!(revolved.profile, Profile::Sphere { .. }) => SurfaceDef {
486                kind: SurfaceKind::Revolved(self.sphere_axis(revolved, &loops)?),
487                ..surface
488            },
489            _ => surface,
490        };
491        // A face on a circle crossing its axis lies on one of its two
492        // sheets: the one past the axis is a surface of its own. Decided at
493        // the vertex that tells them apart best — not one at a pole.
494        let surface = match surface.revolved() {
495            Some(revolved) => {
496                let mut best: Option<f64> = None;
497                for &u in loops.iter().flatten() {
498                    let p = self.vertices[self.ends(u).0].point.to_f64_array();
499                    if let Some(m) = revolved.past_axis(p)
500                        && best.is_none_or(|b| m.abs() > b.abs())
501                    {
502                        best = Some(m);
503                    }
504                }
505                match best {
506                    Some(m) if m > 0.0 => SurfaceDef {
507                        kind: SurfaceKind::Revolved(revolved.mirrored()),
508                        flipped: !surface.flipped,
509                    },
510                    _ => surface,
511                }
512            }
513            None => surface,
514        };
515        let same_sense = args.logical(3)? != reversed;
516        let index = self.faces.len();
517        self.faces.push(Face {
518            surface,
519            same_sense,
520            loops,
521            outer: None,
522            patch: None,
523            shell,
524            name: name("f", index),
525            id,
526        });
527        Ok(())
528    }
529
530    /// The axis a face on the sphere `sphere` is built about. Which axis a
531    /// sphere has is a free choice: the file's, unless one of its poles
532    /// lies on an edge of the face's `loops` away from the edge's ends. A
533    /// loop running through a pole has no angle about the axis there, and
534    /// its angle turns by half a turn across it, which neither the
535    /// windings nor the cuts along meridians can follow. Then the axis is
536    /// chosen whose poles lie furthest from the loops — inside the face,
537    /// where it is cut into sectors round it, or outside it.
538    fn sphere_axis(&self, sphere: &Revolved, loops: &[Vec<Use>]) -> GeopResult<Revolved> {
539        let frame = &sphere.frame;
540        let poles = [frame.point([0.0, 0.0, 1.0]), frame.point([0.0, 0.0, -1.0])];
541        let Profile::Sphere { radius } = sphere.profile else {
542            unreachable!("a sphere's profile");
543        };
544        let poles = poles.map(|p| add(frame.origin, scale(sub(p, frame.origin), radius)));
545        let mut through = false;
546        for &(e, _) in loops.iter().flatten() {
547            let edge = &self.edges[e];
548            let ends = [edge.start, edge.end].map(|v| self.vertices[v].origin.to_f64_array());
549            for pole in poles {
550                if ends
551                    .iter()
552                    .any(|&end| distance(end, pole) <= sphere.on_axis)
553                {
554                    continue;
555                }
556                let t = parameter_of(&edge.curve, pole)?;
557                if distance(edge.curve.evaluate(t)?.to_f64_array(), pole) <= sphere.on_axis {
558                    through = true;
559                }
560            }
561        }
562        if !through {
563            return Ok(sphere.clone());
564        }
565        let mut points = Vec::new();
566        for &u in loops.iter().flatten() {
567            points.extend(
568                self.samples(u, LOOP_SAMPLES)?
569                    .into_iter()
570                    .filter_map(|(_, p)| normalize(sub(p, frame.origin))),
571            );
572        }
573        // Candidates spread evenly over a half sphere (both ends of an
574        // axis are poles): each scored by how near its poles come to a
575        // point of the loops, as the cosine of the angle between.
576        let nearest = |axis: P3| {
577            points
578                .iter()
579                .map(|p| dot(*p, axis).abs())
580                .fold(0.0, f64::max)
581        };
582        let golden = std::f64::consts::PI * (3.0 - 5f64.sqrt());
583        let axis = (0..SPHERE_AXES)
584            .map(|k| {
585                let z = (k as f64 + 0.5) / SPHERE_AXES as f64;
586                let r = (1.0 - z * z).sqrt();
587                let (s, c) = (golden * k as f64).sin_cos();
588                [r * c, r * s, z]
589            })
590            .min_by(|a, b| nearest(*a).total_cmp(&nearest(*b)))
591            .expect("candidates");
592        Ok(Revolved {
593            frame: Frame::new(frame.origin, axis, frame.x)?,
594            ..sphere.clone()
595        })
596    }
597
598    /// The vertex a coedge starts at, and the one it ends at.
599    fn ends(&self, (e, forward): Use) -> (usize, usize) {
600        self.edges[e].ends(forward)
601    }
602
603    /// Points along a coedge, as it runs, with the edge parameter of each:
604    /// `n + 1` of them, ends included.
605    fn samples(&self, (e, forward): Use, n: usize) -> GeopResult<Vec<(f64, P3)>> {
606        // Where to sample is a free choice, and the points are compared in
607        // `f64`.
608        let midpoints = CurveMidpoints::of(&self.edges[e].curve);
609        let (lo, hi) = midpoints.domain();
610        let mut out = Vec::with_capacity(n + 1);
611        for i in 0..=n {
612            let f = i as f64 / n as f64;
613            let f = if forward { f } else { 1.0 - f };
614            let t = lo + (hi - lo) * f;
615            out.push((t, midpoints.point(t)));
616        }
617        Ok(out)
618    }
619
620    /// Points along a loop, each coedge's last one left out as the next
621    /// one's first.
622    fn loop_points(&self, lp: &[Use]) -> GeopResult<Vec<P3>> {
623        Ok(loop_points(&self.edges, lp))
624    }
625
626    /// Splits `edge` at the parameters `ts`, strictly inside its domain
627    /// and increasing, into pieces, with a new vertex at each — in every
628    /// loop of every face. Returns the new vertices.
629    fn split_edge(&mut self, e: usize, ts: &[Scalar]) -> GeopResult<Vec<usize>> {
630        let edge = &self.edges[e];
631        let (start, end, base, id, shape) =
632            (edge.start, edge.end, edge.name.clone(), edge.id, edge.shape);
633        let curve = edge.curve.clone();
634        let (lo, hi) = curve.domain();
635        let mut bounds = vec![lo];
636        bounds.extend_from_slice(ts);
637        bounds.push(hi);
638        let mut new_vertices = Vec::new();
639        for (k, &t) in ts.iter().enumerate() {
640            let v = self.vertices.len();
641            let mut name = base.clone();
642            name.push(format!("c{k}"));
643            self.vertices.push(Vertex {
644                point: curve.evaluate(t)?,
645                origin: curve.evaluate(t)?,
646                name,
647                id: None,
648            });
649            new_vertices.push(v);
650        }
651        let mut chain = vec![start];
652        chain.extend(&new_vertices);
653        chain.push(end);
654        let mut pieces = Vec::new();
655        for k in 0..bounds.len() - 1 {
656            let piece = curve.sub_curve(bounds[k], bounds[k + 1])?;
657            let mut name = base.clone();
658            name.push(format!("p{k}"));
659            pieces.push(self.edges.len());
660            self.edges.push(Edge {
661                curve: piece,
662                start: chain[k],
663                end: chain[k + 1],
664                name,
665                alive: true,
666                id,
667                shape,
668            });
669        }
670        self.edges[e].alive = false;
671        for face in &mut self.faces {
672            for lp in &mut face.loops {
673                let mut out = Vec::with_capacity(lp.len() + pieces.len());
674                for &(edge, forward) in lp.iter() {
675                    if edge != e {
676                        out.push((edge, forward));
677                    } else if forward {
678                        out.extend(pieces.iter().map(|&p| (p, true)));
679                    } else {
680                        out.extend(pieces.iter().rev().map(|&p| (p, false)));
681                    }
682                }
683                *lp = out;
684            }
685        }
686        Ok(new_vertices)
687    }
688
689    /// Brings the faces read into the shape geop needs: wrapping faces cut
690    /// into sectors, closed edges split.
691    fn normalize(&mut self) -> GeopResult<()> {
692        self.join_coincident_vertices();
693        // Each step keeps the face `f` and moves on, or cuts it, taking it
694        // out and adding its pieces at the end, to be looked at in turn.
695        let mut f = 0;
696        repeat(NORMALIZE_STEPS, || -> GeopResult<_> {
697            if f >= self.faces.len() {
698                return Ok(repeat::Step::Done(()));
699            }
700            let face = &self.faces[f];
701            let id = face.id;
702            let revolved = face.surface.revolved().cloned();
703            let had_seam = self
704                .drop_doubled_edges(f)
705                .map_err(|e| e.with_context(format!("taking the seams out of the face #{id}")))?;
706            let wraps = match &revolved {
707                Some(revolved) => {
708                    had_seam
709                        || self.faces[f].loops.is_empty()
710                        || self.windings(f, revolved)?.iter().any(|&w| w != 0)
711                }
712                None => false,
713            };
714            if let (true, Some(revolved)) = (wraps, &revolved) {
715                let round_axis = self.windings(f, revolved)?.iter().any(|&w| w != 0);
716                if revolved.v_is_angle() && !round_axis {
717                    if self.tube_windings(f, revolved)?.iter().any(|&w| w != 0) {
718                        self.cut_tube_wrapping_face(f, revolved).map_err(|e| {
719                            e.with_context(format!(
720                                "cutting the face #{id}, which goes round its torus' tube, into pieces along parallels"
721                            ))
722                        })?;
723                    } else {
724                        self.cut_whole_torus(f, revolved).map_err(|e| {
725                            e.with_context(format!(
726                                "cutting the face #{id}, a whole torus, into bands round its axis"
727                            ))
728                        })?;
729                    }
730                } else {
731                    self.cut_wrapping_face(f).map_err(|e| {
732                        e.with_context(format!(
733                            "cutting the face #{id}, which wraps around its axis, into sectors"
734                        ))
735                    })?;
736                }
737                // Its pieces were pushed at the end; it is gone.
738                return Ok(repeat::Step::Again);
739            }
740            if let Some(revolved) = &revolved
741                && self.turns_more_than_once(f, revolved)?
742            {
743                self.cut_helical_face(f, revolved).map_err(|e| {
744                    e.with_context(format!(
745                        "cutting the face #{id}, which turns about its axis more than once without going round it, along meridians"
746                    ))
747                })?;
748                return Ok(repeat::Step::Again);
749            }
750            f += 1;
751            Ok(repeat::Step::Again)
752        })?
753        .done("cutting the faces into the shape geop needs")?;
754        // Closed edges left: split each in two.
755        for e in 0..self.edges.len() {
756            let edge = &self.edges[e];
757            if edge.alive && edge.start == edge.end {
758                let mid = edge.curve.mid_parameter()?;
759                self.split_edge(e, &[mid])?;
760            }
761        }
762        Ok(())
763    }
764
765    /// Makes one of the vertices the file gives twice within its
766    /// uncertainty: where a coedge of a loop ends at one and the next
767    /// starts at the other, and at the two ends of an edge of no length the
768    /// file can tell — every point of its curve's hull within the
769    /// uncertainty of where it starts — which then leaves its loops. The
770    /// vertex kept is where either was said to be, united. Nothing is
771    /// squeezed: it is put where its faces meet, and the edges from it
772    /// rebuilt there, by the healing every vertex goes through (see
773    /// `heal`).
774    fn join_coincident_vertices(&mut self) {
775        let tolerance = self.scope.uncertainty;
776        let near = |vertices: &[Vertex], a: usize, b: usize| {
777            distance(
778                vertices[a].origin.to_f64_array(),
779                vertices[b].origin.to_f64_array(),
780            ) <= tolerance
781        };
782        for f in 0..self.faces.len() {
783            for l in 0..self.faces[f].loops.len() {
784                let n = self.faces[f].loops[l].len();
785                for k in 0..n {
786                    let lp = &self.faces[f].loops[l];
787                    let (a, b) = (lp[k], lp[(k + 1) % n]);
788                    let end = self.edges[a.0].ends(a.1).1;
789                    let start = self.edges[b.0].ends(b.1).0;
790                    if end != start && near(&self.vertices, end, start) {
791                        self.merge_vertex(end, start);
792                    }
793                }
794            }
795        }
796        for e in 0..self.edges.len() {
797            let edge = &self.edges[e];
798            if !edge.alive || edge.start == edge.end || !near(&self.vertices, edge.start, edge.end)
799            {
800                continue;
801            }
802            let at = self.vertices[edge.start].origin.to_f64_array();
803            let short = (edge.curve.control_points.iter()).all(|c| {
804                c.dehomogenized()
805                    .is_ok_and(|p| distance(p.to_f64_array(), at) <= tolerance)
806            });
807            if !short {
808                continue;
809            }
810            let (start, end) = (edge.start, edge.end);
811            self.edges[e].alive = false;
812            self.merge_vertex(start, end);
813            for face in &mut self.faces {
814                for lp in &mut face.loops {
815                    lp.retain(|&(x, _)| x != e);
816                }
817            }
818        }
819    }
820
821    /// The vertex `gone` made one with `keep`: every edge from it from
822    /// `keep`, which is where either was said to be.
823    fn merge_vertex(&mut self, keep: usize, gone: usize) {
824        let point = self.vertices[gone].point;
825        self.vertices[keep].point = self.vertices[keep].point.union(&point);
826        for edge in &mut self.edges {
827            if edge.start == gone {
828                edge.start = keep;
829            }
830            if edge.end == gone {
831                edge.end = keep;
832            }
833        }
834    }
835
836    /// Takes out of face `f` every edge it runs along twice, once each
837    /// way, which no other face uses: a seam, where a periodic surface's
838    /// parametrization wraps, or a bridge joining a hole to the loop around
839    /// it. Either runs within the face, so it bounds nothing; taking it out
840    /// splits its loop in two — the part between its two uses and the
841    /// rest. Says whether there was any.
842    fn drop_doubled_edges(&mut self, f: usize) -> GeopResult<bool> {
843        let mut count: HashMap<usize, usize> = HashMap::new();
844        for lp in &self.faces[f].loops {
845            for &(e, _) in lp {
846                *count.entry(e).or_default() += 1;
847            }
848        }
849        // One another face uses too is no seam or bridge but an edge where
850        // the solid touches itself: a round pocket tangent to a wall slits
851        // the wall along the line it touches, and the pocket's faces meet
852        // there too. It bounds the face, and stays.
853        let used_elsewhere = |e: usize| {
854            (self.faces.iter().enumerate())
855                .any(|(g, face)| g != f && face.loops.iter().flatten().any(|&(x, _)| x == e))
856        };
857        let mut doubled: Vec<usize> = count
858            .into_iter()
859            .filter(|&(e, n)| n == 2 && !used_elsewhere(e))
860            .map(|(e, _)| e)
861            .collect();
862        doubled.sort();
863        for &e in &doubled {
864            let loops = std::mem::take(&mut self.faces[f].loops);
865            let mut out = Vec::new();
866            for lp in loops {
867                let uses: Vec<usize> = (0..lp.len()).filter(|&k| lp[k].0 == e).collect();
868                match uses.as_slice() {
869                    [] => out.push(lp),
870                    &[i, j] => {
871                        if lp[i].1 == lp[j].1 {
872                            return Err(GeopError::new(format!(
873                                "it runs along the edge {} twice the same way",
874                                self.edges[e].label()
875                            )));
876                        }
877                        let inner: Vec<Use> = lp[i + 1..j].to_vec();
878                        let outer: Vec<Use> = lp[j + 1..].iter().chain(&lp[..i]).copied().collect();
879                        out.extend([inner, outer].into_iter().filter(|l| !l.is_empty()));
880                    }
881                    // A loop of the closed edge alone: what is left of a
882                    // whole torus' loop round both seams once one is out,
883                    // twice, once each way. Neither bounds anything.
884                    [_] if lp.len() == 1 => {}
885                    _ => {
886                        return Err(GeopError::new(format!(
887                            "the edge {} is used twice, by different loops of it",
888                            self.edges[e].label()
889                        )));
890                    }
891                }
892            }
893            self.faces[f].loops = out;
894            self.edges[e].alive = false;
895        }
896        for lp in &self.faces[f].loops {
897            let (start, _) = self.ends(lp[0]);
898            let (_, end) = self.ends(*lp.last().expect("not empty"));
899            if start != end {
900                return Err(GeopError::new(
901                    "taking them out leaves a loop that does not close",
902                ));
903            }
904        }
905        Ok(!doubled.is_empty())
906    }
907
908    /// How often each loop of face `f` goes round the tube of `revolved`, a
909    /// torus, the way its profile parameter runs.
910    fn tube_windings(&self, f: usize, revolved: &Revolved) -> GeopResult<Vec<i64>> {
911        self.faces[f]
912            .loops
913            .iter()
914            .map(|lp| {
915                Ok(winding(&unwrap_tube(
916                    revolved,
917                    &self.loop_points(lp)?,
918                    true,
919                )))
920            })
921            .collect()
922    }
923
924    /// How often each loop of face `f` goes round the axis of `revolved`,
925    /// counter-clockwise about it.
926    fn windings(&self, f: usize, revolved: &Revolved) -> GeopResult<Vec<i64>> {
927        self.faces[f]
928            .loops
929            .iter()
930            .map(|lp| {
931                let ccw = self.faces[f].outward_is_natural();
932                Ok(winding(&unwrap(
933                    revolved,
934                    &self.loop_points(lp)?,
935                    true,
936                    ccw,
937                )))
938            })
939            .collect()
940    }
941
942    /// Cuts the face `f`, which wraps around its axis, into sectors along
943    /// meridians, each on a patch that does not wrap.
944    ///
945    /// Without its seams, the face is a band around the axis between a
946    /// bottom and a top, each a loop going once round — a *ring* — or a
947    /// pole, with any holes in between. Where the cuts go is a free
948    /// choice, made where nothing is: clear of every vertex and hole, as
949    /// far from them as can be.
950    fn cut_wrapping_face(&mut self, f: usize) -> GeopResult<()> {
951        let revolved = self.faces[f]
952            .surface
953            .revolved()
954            .expect("only a surface of revolution wraps")
955            .clone();
956        let outward_natural = self.faces[f].outward_is_natural();
957        let mut rings: Vec<usize> = Vec::new();
958        let mut bottom = None;
959        let mut top = None;
960        let mut holes = Vec::new();
961        // Each loop's winding and mean profile parameter, for messages.
962        let mut summary = Vec::new();
963        for (k, lp) in self.faces[f].loops.iter().enumerate() {
964            let points = self.loop_points(lp)?;
965            let angles = unwrap(&revolved, &points, true, outward_natural);
966            let mean_v =
967                points.iter().map(|&p| revolved.chart(p).1).sum::<f64>() / points.len() as f64;
968            summary.push((winding(&angles), mean_v, lp.len()));
969            match winding(&angles) {
970                0 => holes.push(k),
971                w @ (1 | -1) => {
972                    // The face lies to the left of its loops, about its
973                    // outward normal: above a ring running counter-clockwise
974                    // about the natural one.
975                    let above = (w > 0) == outward_natural;
976                    let slot = if above { &mut bottom } else { &mut top };
977                    if slot.replace(k).is_some() {
978                        return Err(GeopError::new(format!(
979                            "it has more than one loop round its axis with the face {} it",
980                            if above { "above" } else { "below" }
981                        )));
982                    }
983                    rings.push(k);
984                }
985                w => {
986                    return Err(GeopError::new(format!(
987                        "a loop of it goes round its axis {w} times"
988                    )));
989                }
990            }
991        }
992        // A torus' profile parameter is an angle too: read it from where
993        // the face is not, so it runs on across the face. The face lies
994        // above its bottom ring, up to its top one: it is not between the
995        // top ring and the bottom one, going on round the tube.
996        let v_start = if revolved.v_is_angle() {
997            let (Some(b), Some(t)) = (bottom, top) else {
998                return Err(GeopError::new(format!(
999                    "it goes round its torus' axis without a loop round the axis on either side of it (its loops' windings, mean profile parameters and lengths: {summary:?})"
1000                )));
1001            };
1002            let mean = |k: usize| -> GeopResult<f64> {
1003                let v = unwrap_tube(
1004                    &revolved,
1005                    &self.loop_points(&self.faces[f].loops[k])?,
1006                    false,
1007                );
1008                Ok(v.iter().sum::<f64>() / v.len().max(1) as f64)
1009            };
1010            let (b, t) = (mean(b)?, mean(t)?);
1011            t + (b - t).rem_euclid(std::f64::consts::TAU) / 2.0
1012        } else {
1013            f64::NEG_INFINITY
1014        };
1015        let v_of = |p: P3| {
1016            let v = revolved.chart(p).1;
1017            if v_start.is_finite() {
1018                v_start + (v - v_start).rem_euclid(std::f64::consts::TAU)
1019            } else {
1020                v
1021            }
1022        };
1023        let ring_v = |builder: &Self, k: usize| -> GeopResult<f64> {
1024            let points = builder.loop_points(&builder.faces[f].loops[k])?;
1025            Ok(points.iter().map(|&p| v_of(p)).sum::<f64>() / points.len() as f64)
1026        };
1027        let bottom_v = bottom.map(|k| ring_v(self, k)).transpose()?;
1028        let top_v = top.map(|k| ring_v(self, k)).transpose()?;
1029        let poles = revolved.poles();
1030        // A side with no ring is closed off by the pole on that side.
1031        let bottom_pole = match bottom {
1032            Some(_) => None,
1033            None => Some(
1034                poles
1035                    .iter()
1036                    .copied()
1037                    .filter(|&p| top_v.is_none_or(|t| p < t))
1038                    // The pole nearest below the top, where there is one.
1039                    .reduce(if top_v.is_some() { f64::max } else { f64::min })
1040                    .ok_or_else(|| GeopError::new(format!("it has nothing closing it off below: no loop round its axis and no pole (its loops' windings, mean profile parameters and lengths: {summary:?}, the face's outward normal natural: {outward_natural})")))?,
1041            ),
1042        };
1043        let top_pole = match top {
1044            Some(_) => None,
1045            None => Some(
1046                poles
1047                    .iter()
1048                    .copied()
1049                    // The pole nearest above the bottom.
1050                    .filter(|&p| bottom_v.or(bottom_pole).is_none_or(|b| p > b))
1051                    .reduce(f64::min)
1052                    .ok_or_else(|| GeopError::new(format!("it has nothing closing it off above: no loop round its axis and no pole (its loops' windings, mean profile parameters and lengths: {summary:?}, the face's outward normal natural: {outward_natural})")))?,
1053            ),
1054        };
1055        if let (Some(b), Some(t)) = (bottom_v.or(bottom_pole), top_v.or(top_pole))
1056            && b >= t
1057        {
1058            return Err(GeopError::new(format!(
1059                "its bottom {b} is not below its top {t} (its loops' windings, mean profile parameters and lengths: {summary:?})"
1060            )));
1061        }
1062
1063        // Where not to cut: at any vertex, or through a hole.
1064        let mut blocked: Vec<(f64, f64)> = Vec::new();
1065        for (k, lp) in self.faces[f].loops.iter().enumerate() {
1066            let points = self.loop_points(lp)?;
1067            if holes.contains(&k) {
1068                let angles = unwrap(&revolved, &points, false, outward_natural);
1069                let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1070                let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1071                blocked.push((lo, hi));
1072            }
1073            for &u in lp {
1074                let (start, _) = self.ends(u);
1075                if let (Some(a), _) = revolved.chart(self.vertices[start].point.to_f64_array()) {
1076                    blocked.push((a, a));
1077                }
1078            }
1079        }
1080        let cuts = choose_cuts(&blocked);
1081
1082        let ring_vertices = self.split_rings(f, &rings, &cuts, &|p| revolved.chart(p).0)?;
1083
1084        // A meridian along each cut, from bottom to top.
1085        let point_at = |builder: &Self,
1086                        side: Option<usize>,
1087                        pole: Option<f64>,
1088                        c: usize|
1089         -> (P3, f64, Option<usize>) {
1090            match side {
1091                Some(ring) => {
1092                    let v = ring_vertices[&ring][c];
1093                    let p = builder.vertices[v].point.to_f64_array();
1094                    (p, v_of(p), Some(v))
1095                }
1096                None => {
1097                    let pole = pole.expect("a side without a ring has a pole");
1098                    (revolved.profile_point(pole), pole, None)
1099                }
1100            }
1101        };
1102        let pole_vertex = |builder: &mut Self, pole: Option<f64>, which: &str| -> Option<usize> {
1103            pole.map(|v| {
1104                let p = revolved.profile_point(v);
1105                // A vertex of the file there already, or a new one.
1106                let existing = builder
1107                    .vertices
1108                    .iter()
1109                    .position(|x| distance(x.point.to_f64_array(), p) <= builder.scope.uncertainty);
1110                existing.unwrap_or_else(|| {
1111                    builder.vertices.push(Vertex {
1112                        point: s3(p),
1113                        origin: s3(p),
1114                        name: vec![builder.faces[f].name[0].clone(), which.to_string()],
1115                        id: None,
1116                    });
1117                    builder.vertices.len() - 1
1118                })
1119            })
1120        };
1121        let bottom_pole_vertex = pole_vertex(self, bottom_pole, "south");
1122        let top_pole_vertex = pole_vertex(self, top_pole, "north");
1123        let mut meridians = Vec::new();
1124        for (c, &cut) in cuts.iter().enumerate() {
1125            let (pb, vb, b) = point_at(self, bottom, bottom_pole, c);
1126            let (pt, vt, t) = point_at(self, top, top_pole, c);
1127            let start = b.or(bottom_pole_vertex).expect("a bottom");
1128            let end = t.or(top_pole_vertex).expect("a top");
1129            let curve = self.meridian(
1130                &revolved,
1131                cut,
1132                (pb, vb, bottom_pole.is_some()),
1133                (pt, vt, top_pole.is_some()),
1134            )?;
1135            let mut name = self.faces[f].name.clone();
1136            name.push(format!("m{c}"));
1137            meridians.push(self.edges.len());
1138            self.edges.push(Edge {
1139                curve,
1140                start,
1141                end,
1142                name,
1143                alive: true,
1144                id: None,
1145                shape: Some(revolved.iso()),
1146            });
1147        }
1148
1149        // The sectors between consecutive cuts.
1150        let n = cuts.len();
1151        let face_loops = self.faces[f].loops.clone();
1152        let ring_run = |builder: &Self,
1153                        ring: usize,
1154                        from: usize,
1155                        to: usize,
1156                        up: bool|
1157         -> GeopResult<Vec<Use>> {
1158            // The ring's coedges from the vertex at cut `from` to the one at
1159            // cut `to`, running counter-clockwise about the natural normal
1160            // (`up`) or clockwise.
1161            let lp = &face_loops[ring];
1162            let angles = unwrap(&revolved, &builder.loop_points(lp)?, true, outward_natural);
1163            let along = (winding(&angles) > 0) == up;
1164            builder.run_between(
1165                lp,
1166                ring_vertices[&ring][from],
1167                ring_vertices[&ring][to],
1168                along,
1169            )
1170        };
1171        let face = &self.faces[f];
1172        let (surface, same_sense, shell, base_name) = (
1173            face.surface.clone(),
1174            face.same_sense,
1175            face.shell,
1176            face.name.clone(),
1177        );
1178        // Each hole goes to the sector it lies in.
1179        let mut hole_sector = Vec::new();
1180        for &h in &holes {
1181            let points = self.loop_points(&face_loops[h])?;
1182            let a = unwrap(&revolved, &points, false, outward_natural);
1183            let mid = a.iter().sum::<f64>() / a.len() as f64;
1184            let sector = (0..n)
1185                .find(|&s| {
1186                    let lo = cuts[s];
1187                    let hi = if s + 1 < n {
1188                        cuts[s + 1]
1189                    } else {
1190                        cuts[0] + std::f64::consts::TAU
1191                    };
1192                    lo + (mid - lo).rem_euclid(std::f64::consts::TAU) < hi
1193                })
1194                .expect("the sectors cover every angle");
1195            hole_sector.push((h, sector));
1196        }
1197        let v_range_of = |builder: &Self, loops: &[usize]| -> GeopResult<(f64, f64)> {
1198            let mut lo = f64::INFINITY;
1199            let mut hi = f64::NEG_INFINITY;
1200            for &k in loops {
1201                for p in builder.loop_points(&face_loops[k])? {
1202                    let v = v_of(p);
1203                    lo = lo.min(v);
1204                    hi = hi.max(v);
1205                }
1206            }
1207            Ok((lo, hi))
1208        };
1209        for s in 0..n {
1210            let next = (s + 1) % n;
1211            let lo = cuts[s];
1212            let hi = if s + 1 < n {
1213                cuts[s + 1]
1214            } else {
1215                cuts[0] + std::f64::consts::TAU
1216            };
1217            let mut lp: Vec<Use> = Vec::new();
1218            if let Some(ring) = bottom {
1219                lp.extend(ring_run(self, ring, s, next, true)?);
1220            }
1221            lp.push((meridians[next], true));
1222            if let Some(ring) = top {
1223                lp.extend(ring_run(self, ring, next, s, false)?);
1224            }
1225            lp.push((meridians[s], false));
1226            if !outward_natural {
1227                lp = lp.into_iter().rev().map(|(e, fw)| (e, !fw)).collect();
1228            }
1229            let mut loops = vec![lp];
1230            let mut members: Vec<usize> = bottom.into_iter().chain(top).collect();
1231            for &(h, sector) in &hole_sector {
1232                if sector == s {
1233                    loops.push(face_loops[h].clone());
1234                    members.push(h);
1235                }
1236            }
1237            let (v_lo, v_hi) = v_range_of(self, &members)?;
1238            let v0 = bottom_pole.unwrap_or(v_lo);
1239            let v1 = top_pole.unwrap_or(v_hi);
1240            // Rings a whole turn of the tube apart are one circle: the face
1241            // goes round the tube as well as the axis — a whole torus.
1242            // (Samples a hundredth of a turn short of one still are.)
1243            if revolved.v_is_angle() && v1 - v0 >= 0.99 * std::f64::consts::TAU {
1244                return Err(GeopError::new(
1245                    "it goes all the way round its torus' tube as well as its axis, as a whole torus does: not supported",
1246                ));
1247            }
1248            let (v0, v1) = widen_v(&revolved, v0, v1, bottom_pole.is_some(), top_pole.is_some());
1249            let mut name = base_name.clone();
1250            name.push(format!("q{s}"));
1251            self.faces.push(Face {
1252                surface: surface.clone(),
1253                same_sense,
1254                loops,
1255                outer: Some(0),
1256                patch: Some([lo, hi, v0, v1]),
1257                shell,
1258                name,
1259                id: self.faces[f].id,
1260            });
1261        }
1262        self.faces.remove(f);
1263        Ok(())
1264    }
1265
1266    /// Cuts the face `f`, which goes all the way round the tube of its
1267    /// torus `revolved` but not round its axis — a pipe bend's — into
1268    /// pieces along parallels, each on a patch that does not wrap.
1269    ///
1270    /// Without its seam, the face is a sleeve round the tube between two
1271    /// rings, each going once round the tube, with any holes in between.
1272    /// Where the parallels go is a free choice, made as the meridians of
1273    /// [`Builder::cut_wrapping_face`] are: clear of every vertex and hole.
1274    fn cut_tube_wrapping_face(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1275        use std::f64::consts::TAU;
1276        let outward_natural = self.faces[f].outward_is_natural();
1277        let face_loops = self.faces[f].loops.clone();
1278        let tube = |p: P3| revolved.chart(p).1;
1279        let mut left = None;
1280        let mut right = None;
1281        let mut holes = Vec::new();
1282        let mut windings = Vec::new();
1283        for (k, lp) in face_loops.iter().enumerate() {
1284            let w = winding(&unwrap_tube(revolved, &self.loop_points(lp)?, true));
1285            windings.push(w);
1286            match w {
1287                0 => holes.push(k),
1288                1 | -1 => {
1289                    // The face lies to the left of its loops, about its
1290                    // outward normal: at larger angles about the axis than
1291                    // a ring running backwards round the tube about the
1292                    // natural one.
1293                    let is_left = (w < 0) == outward_natural;
1294                    let slot = if is_left { &mut left } else { &mut right };
1295                    if slot.replace(k).is_some() {
1296                        return Err(GeopError::new(format!(
1297                            "it has more than one loop round its tube with the face at {} angles about the axis (the loops' windings round the tube: {windings:?})",
1298                            if is_left { "larger" } else { "smaller" }
1299                        )));
1300                    }
1301                }
1302                w => {
1303                    return Err(GeopError::new(format!(
1304                        "a loop of it goes round its tube {w} times"
1305                    )));
1306                }
1307            }
1308        }
1309        let (Some(left), Some(right)) = (left, right) else {
1310            return Err(GeopError::new(format!(
1311                "it is not bounded by two loops round its tube, one at either end (the loops' windings round the tube: {windings:?}, the face's outward normal natural: {outward_natural})"
1312            )));
1313        };
1314        // The face runs about the axis from its left ring to its right one,
1315        // not all the way round: angles are read from the middle of the
1316        // gap between the right ring and the left one.
1317        let mean_angle = |builder: &Self, k: usize| -> GeopResult<f64> {
1318            let angles = unwrap(
1319                revolved,
1320                &builder.loop_points(&face_loops[k])?,
1321                false,
1322                outward_natural,
1323            );
1324            Ok(angles.iter().sum::<f64>() / angles.len().max(1) as f64)
1325        };
1326        let (a_left, a_right) = (mean_angle(self, left)?, mean_angle(self, right)?);
1327        let u_start = a_right + (a_left - a_right).rem_euclid(TAU) / 2.0;
1328        let u_of = |p: P3| -> GeopResult<f64> {
1329            let a = revolved
1330                .chart(p)
1331                .0
1332                .ok_or_else(|| GeopError::new(format!("a point of it, {p:?}, is on its axis")))?;
1333            Ok(u_start + (a - u_start).rem_euclid(TAU))
1334        };
1335
1336        // Where not to cut: at any vertex, or through a hole.
1337        let mut blocked: Vec<(f64, f64)> = Vec::new();
1338        for (k, lp) in face_loops.iter().enumerate() {
1339            if holes.contains(&k) {
1340                let angles = unwrap_tube(revolved, &self.loop_points(lp)?, false);
1341                let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1342                let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1343                blocked.push((lo, hi));
1344            }
1345            for &u in lp {
1346                let a = tube(self.vertices[self.ends(u).0].point.to_f64_array());
1347                blocked.push((a, a));
1348            }
1349        }
1350        let cuts = choose_cuts(&blocked);
1351        let ring_vertices = self.split_rings(f, &[left, right], &cuts, &|p| Some(tube(p)))?;
1352        // The rings, split: their coedges now run through the cut vertices.
1353        let face_loops = self.faces[f].loops.clone();
1354
1355        // A parallel along each cut, from the left ring to the right one.
1356        let mut parallels = Vec::new();
1357        for (c, &cut) in cuts.iter().enumerate() {
1358            let (a, b) = (ring_vertices[&left][c], ring_vertices[&right][c]);
1359            let from = u_of(self.vertices[a].point.to_f64_array())?;
1360            let to = u_of(self.vertices[b].point.to_f64_array())?;
1361            if from >= to {
1362                return Err(GeopError::new(format!(
1363                    "its rings round the tube cross at the angle {cut} round it: the left one at the angle {from} about the axis, the right one at {to}"
1364                )));
1365            }
1366            let curve = revolved.parallel(cut, from, to)?;
1367            let mut name = self.faces[f].name.clone();
1368            name.push(format!("m{c}"));
1369            parallels.push(self.edges.len());
1370            self.edges.push(Edge {
1371                curve,
1372                start: a,
1373                end: b,
1374                name,
1375                alive: true,
1376                id: None,
1377                shape: Some(revolved.iso()),
1378            });
1379        }
1380
1381        // The pieces between consecutive parallels, each running
1382        // counter-clockwise in angle about the axis and round the tube:
1383        // along the lower parallel, up the right ring, back along the
1384        // upper parallel, down the left ring.
1385        let n = cuts.len();
1386        let upper = |s: usize| {
1387            if s + 1 < n {
1388                cuts[s + 1]
1389            } else {
1390                cuts[0] + TAU
1391            }
1392        };
1393        let mut hole_piece = Vec::new();
1394        for &h in &holes {
1395            let a = unwrap_tube(revolved, &self.loop_points(&face_loops[h])?, false);
1396            let mid = a.iter().sum::<f64>() / a.len() as f64;
1397            let piece = (0..n)
1398                .find(|&s| cuts[s] + (mid - cuts[s]).rem_euclid(TAU) < upper(s))
1399                .expect("the pieces cover every angle");
1400            hole_piece.push((h, piece));
1401        }
1402        let (w_left, w_right) = (windings[left], windings[right]);
1403        let face = &self.faces[f];
1404        let (surface, same_sense, shell, base_name, id) = (
1405            face.surface.clone(),
1406            face.same_sense,
1407            face.shell,
1408            face.name.clone(),
1409            face.id,
1410        );
1411        for s in 0..n {
1412            let next = (s + 1) % n;
1413            let mut lp = vec![(parallels[s], true)];
1414            lp.extend(self.run_between(
1415                &face_loops[right],
1416                ring_vertices[&right][s],
1417                ring_vertices[&right][next],
1418                w_right > 0,
1419            )?);
1420            lp.push((parallels[next], false));
1421            lp.extend(self.run_between(
1422                &face_loops[left],
1423                ring_vertices[&left][next],
1424                ring_vertices[&left][s],
1425                w_left < 0,
1426            )?);
1427            let mut loops = vec![lp];
1428            for &(h, piece) in &hole_piece {
1429                if piece == s {
1430                    loops.push(face_loops[h].clone());
1431                }
1432            }
1433            // The angles about the axis the piece spans, a little widened.
1434            let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1435            for lp in &loops {
1436                for p in self.loop_points(lp)? {
1437                    let u = u_of(p)?;
1438                    lo = lo.min(u);
1439                    hi = hi.max(u);
1440                }
1441            }
1442            let span = hi - lo;
1443            let margin = (0.02 * span + 1e-3).min((TAU - span) / 4.0);
1444            if !outward_natural {
1445                loops[0] = loops[0].iter().rev().map(|&(e, fw)| (e, !fw)).collect();
1446            }
1447            let mut name = base_name.clone();
1448            name.push(format!("q{s}"));
1449            self.faces.push(Face {
1450                surface: surface.clone(),
1451                same_sense,
1452                loops,
1453                outer: Some(0),
1454                patch: Some([lo - margin, hi + margin, cuts[s], upper(s)]),
1455                shell,
1456                name,
1457                id,
1458            });
1459        }
1460        self.faces.remove(f);
1461        Ok(())
1462    }
1463
1464    /// Cuts the face `f`, a whole torus — going round both its tube and
1465    /// its axis, with no loops but holes — along parallels into bands, each
1466    /// of which goes round the axis between two of them and is then cut
1467    /// into sectors as any face going round its axis is.
1468    fn cut_whole_torus(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1469        use std::f64::consts::TAU;
1470        let outward_natural = self.faces[f].outward_is_natural();
1471        let holes = self.faces[f].loops.clone();
1472        // Where not to cut: at any vertex, or through a hole — round the
1473        // tube for the parallels, round the axis for where they start.
1474        let mut round_tube: Vec<(f64, f64)> = Vec::new();
1475        let mut round_axis: Vec<(f64, f64)> = Vec::new();
1476        for lp in &holes {
1477            let points = self.loop_points(lp)?;
1478            let range = |angles: Vec<f64>| {
1479                let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1480                let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1481                (lo, hi)
1482            };
1483            round_tube.push(range(unwrap_tube(revolved, &points, false)));
1484            round_axis.push(range(unwrap(revolved, &points, false, outward_natural)));
1485        }
1486        let cuts = choose_cuts(&round_tube);
1487        let start = choose_cuts(&round_axis)[0];
1488        let base_name = self.faces[f].name.clone();
1489        let mut parallels = Vec::new();
1490        for (c, &cut) in cuts.iter().enumerate() {
1491            let curve = revolved.parallel(cut, start, start)?;
1492            let point = curve.evaluate(curve.domain().0)?;
1493            let mut name = base_name.clone();
1494            name.push(format!("m{c}"));
1495            let v = self.vertices.len();
1496            self.vertices.push(Vertex {
1497                point,
1498                origin: point,
1499                name: [name.clone(), vec!["v".to_string()]].concat(),
1500                id: None,
1501            });
1502            parallels.push(self.edges.len());
1503            self.edges.push(Edge {
1504                curve,
1505                start: v,
1506                end: v,
1507                name,
1508                alive: true,
1509                id: None,
1510                shape: Some(revolved.iso()),
1511            });
1512        }
1513        let n = cuts.len();
1514        let upper = |s: usize| {
1515            if s + 1 < n {
1516                cuts[s + 1]
1517            } else {
1518                cuts[0] + TAU
1519            }
1520        };
1521        let face = &self.faces[f];
1522        let (surface, same_sense, shell, id) =
1523            (face.surface.clone(), face.same_sense, face.shell, face.id);
1524        let mut bands: Vec<Vec<Vec<Use>>> = (0..n)
1525            .map(|s| {
1526                // Above the lower parallel, running counter-clockwise about
1527                // the axis, below the upper one, running back — about the
1528                // natural normal.
1529                let next = (s + 1) % n;
1530                vec![
1531                    vec![(parallels[s], outward_natural)],
1532                    vec![(parallels[next], !outward_natural)],
1533                ]
1534            })
1535            .collect();
1536        for lp in holes {
1537            let a = unwrap_tube(revolved, &self.loop_points(&lp)?, false);
1538            let mid = a.iter().sum::<f64>() / a.len() as f64;
1539            let band = (0..n)
1540                .find(|&s| cuts[s] + (mid - cuts[s]).rem_euclid(TAU) < upper(s))
1541                .expect("the bands cover every angle");
1542            bands[band].push(lp);
1543        }
1544        for (s, loops) in bands.into_iter().enumerate() {
1545            let mut name = base_name.clone();
1546            name.push(format!("b{s}"));
1547            self.faces.push(Face {
1548                surface: surface.clone(),
1549                same_sense,
1550                loops,
1551                outer: None,
1552                patch: None,
1553                shell,
1554                name,
1555                id,
1556            });
1557        }
1558        self.faces.remove(f);
1559        Ok(())
1560    }
1561
1562    /// Whether a loop of face `f`, which does not wrap round the axis of
1563    /// `revolved`, still turns about it through a whole turn or more from
1564    /// end to end — as a thread's flank does, a strip winding round a
1565    /// cylinder.
1566    fn turns_more_than_once(&self, f: usize, revolved: &Revolved) -> GeopResult<bool> {
1567        let natural = self.faces[f].outward_is_natural();
1568        for lp in &self.faces[f].loops {
1569            let angles = unwrap(revolved, &self.loop_points(lp)?, false, natural);
1570            let lo = angles.iter().copied().fold(f64::INFINITY, f64::min);
1571            let hi = angles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1572            if hi - lo >= std::f64::consts::TAU {
1573                return Ok(true);
1574            }
1575        }
1576        Ok(false)
1577    }
1578
1579    /// Cuts the face `f`, a strip turning about the axis of `revolved` more
1580    /// than once without going round it — a thread's flank — along
1581    /// meridians half a turn apart into pieces each turning less than once.
1582    ///
1583    /// Unwrapped, the angle about the axis runs on along the strip, and the
1584    /// face is a region of the plane of angles and profile parameters. Each
1585    /// cut, a line of one angle, crosses its loop an even number of times;
1586    /// in order along the line, the crossings pair up into the stretches of
1587    /// meridian inside the face. Every piece is then traced along the loop,
1588    /// turning along a meridian at each crossing it reaches — the face
1589    /// stays on the left throughout.
1590    fn cut_helical_face(&mut self, f: usize, revolved: &Revolved) -> GeopResult<()> {
1591        use std::f64::consts::PI;
1592        if self.faces[f].loops.len() != 1 {
1593            return Err(GeopError::new(format!(
1594                "it has {} loops: only a strip bounded by one loop is cut",
1595                self.faces[f].loops.len()
1596            )));
1597        }
1598        if revolved.v_is_angle() {
1599            return Err(GeopError::new(
1600                "it lies on a torus: only strips on cylinders, cones and other surfaces of revolution with a profile from end to end are cut",
1601            ));
1602        }
1603        let lp = self.faces[f].loops[0].clone();
1604        // The loop's angles, unwrapped from coedge to coedge, sampled.
1605        let mut samples: Vec<(usize, f64, f64, f64)> = Vec::new(); // (coedge, t, angle, v)
1606        let mut prev: Option<f64> = None;
1607        for (k, &u) in lp.iter().enumerate() {
1608            for (t, p) in self.samples(u, LOOP_SAMPLES)? {
1609                let (Some(a), v) = revolved.chart(p) else {
1610                    return Err(GeopError::new(format!(
1611                        "its loop runs through its axis at {p:?}"
1612                    )));
1613                };
1614                let a = prev.map_or(a, |r| unwrap_angle(a, r));
1615                prev = Some(a);
1616                samples.push((k, t, a, v));
1617            }
1618        }
1619        let lo = samples.iter().map(|s| s.2).fold(f64::INFINITY, f64::min);
1620        let hi = samples
1621            .iter()
1622            .map(|s| s.2)
1623            .fold(f64::NEG_INFINITY, f64::max);
1624        // Cuts half a turn apart, as far from every vertex as can be.
1625        let corners: Vec<f64> = samples
1626            .iter()
1627            .enumerate()
1628            .filter(|&(i, s)| i == 0 || samples[i - 1].0 != s.0)
1629            .map(|(_, s)| s.2)
1630            .collect();
1631        let clearance = |offset: f64| {
1632            corners
1633                .iter()
1634                .map(|&a| {
1635                    let r = (a - offset).rem_euclid(PI);
1636                    r.min(PI - r)
1637                })
1638                .fold(f64::INFINITY, f64::min)
1639        };
1640        let offset = (0..720)
1641            .map(|i| lo + PI * i as f64 / 720.0)
1642            .max_by(|a, b| clearance(*a).total_cmp(&clearance(*b)))
1643            .expect("candidates");
1644        let cuts: Vec<f64> = (1..)
1645            .map(|k| offset + PI * k as f64)
1646            .take_while(|&a| a < hi)
1647            .collect();
1648
1649        // Where the loop crosses each cut: the edge, its parameter, and
1650        // the profile parameter there.
1651        let mut crossings: Vec<(usize, f64, usize, f64)> = Vec::new(); // (edge, t, cut, v)
1652        for w in samples.windows(2) {
1653            let ((k0, t0, a0, _), (k1, t1, a1, _)) = (w[0], w[1]);
1654            if k0 != k1 {
1655                continue;
1656            }
1657            let u = lp[k0];
1658            for (c, &cut) in cuts.iter().enumerate() {
1659                // Once for each crossing, a sample on the cut included.
1660                if (a0 < cut) == (a1 < cut) {
1661                    continue;
1662                }
1663                // Bisection, as far as `f64` goes: where exactly is a free
1664                // choice the meridian is built through.
1665                let angle_at = |t: f64| -> GeopResult<f64> {
1666                    let p = super::geometry::point_at(&self.edges[u.0].curve, t)?;
1667                    let a = revolved
1668                        .chart(p)
1669                        .0
1670                        .ok_or_else(|| GeopError::new("its loop runs through its axis"))?;
1671                    Ok(unwrap_angle(a, cut))
1672                };
1673                let below = angle_at(t0)? < cut;
1674                let Bracket { lo, hi, .. } = bisect(t0, t1, 100, true, |mid| -> GeopResult<_> {
1675                    Ok(Some((angle_at(mid)? < cut) == below))
1676                })?;
1677                let t = 0.5 * (lo + hi);
1678                let v = revolved
1679                    .chart(super::geometry::point_at(&self.edges[u.0].curve, t)?)
1680                    .1;
1681                crossings.push((u.0, t, c, v));
1682            }
1683        }
1684        // Split every edge where it is crossed.
1685        let mut by_edge: HashMap<usize, Vec<(f64, usize, f64)>> = HashMap::new();
1686        for &(e, t, c, v) in &crossings {
1687            by_edge.entry(e).or_default().push((t, c, v));
1688        }
1689        let mut edges: Vec<usize> = by_edge.keys().copied().collect();
1690        edges.sort();
1691        let mut on_cut: Vec<Vec<(f64, usize)>> = vec![Vec::new(); cuts.len()]; // (v, vertex)
1692        for e in edges {
1693            let mut list = by_edge[&e].clone();
1694            list.sort_by(|a, b| a.0.total_cmp(&b.0));
1695            let ts: Vec<Scalar> = list.iter().map(|x| Scalar::from_f64(x.0)).collect();
1696            let (d0, d1) = self.edges[e].curve.domain();
1697            if ts
1698                .iter()
1699                .any(|t| !(t.definitely_greater(d0) && t.definitely_less(d1)))
1700            {
1701                return Err(GeopError::new(format!(
1702                    "a cut crosses its loop at a vertex, at the end of the edge {}",
1703                    self.edges[e].label()
1704                )));
1705            }
1706            let created = self.split_edge(e, &ts)?;
1707            for ((_, c, v), vertex) in list.into_iter().zip(created) {
1708                on_cut[c].push((v, vertex));
1709            }
1710        }
1711        // Along each cut, the crossings in order pair up into the
1712        // stretches of meridian inside the face.
1713        let mut partner: HashMap<usize, (usize, usize)> = HashMap::new(); // vertex -> (other, meridian)
1714        let mut count = 0;
1715        for (c, list) in on_cut.iter_mut().enumerate() {
1716            list.sort_by(|a, b| a.0.total_cmp(&b.0));
1717            if list.len() % 2 != 0 {
1718                return Err(GeopError::new(format!(
1719                    "the cut at the angle {} crosses its loop an odd number of times, {}",
1720                    cuts[c],
1721                    list.len()
1722                )));
1723            }
1724            for pair in list.chunks(2) {
1725                let ((vb, b), (vt, t)) = (pair[0], pair[1]);
1726                let pb = self.vertices[b].point.to_f64_array();
1727                let pt = self.vertices[t].point.to_f64_array();
1728                let curve = self.meridian(revolved, cuts[c], (pb, vb, false), (pt, vt, false))?;
1729                let mut name = self.faces[f].name.clone();
1730                name.push(format!("m{count}"));
1731                count += 1;
1732                let m = self.edges.len();
1733                self.edges.push(Edge {
1734                    curve,
1735                    start: b,
1736                    end: t,
1737                    name,
1738                    alive: true,
1739                    id: None,
1740                    shape: Some(revolved.iso()),
1741                });
1742                partner.insert(b, (t, m));
1743                partner.insert(t, (b, m));
1744            }
1745        }
1746
1747        // The pieces, traced along the split loop.
1748        let lp = self.faces[f].loops[0].clone();
1749        let starting: HashMap<usize, usize> = lp
1750            .iter()
1751            .enumerate()
1752            .map(|(i, &u)| (self.ends(u).0, i))
1753            .collect();
1754        let mut used = vec![false; lp.len()];
1755        let mut pieces: Vec<Vec<Use>> = Vec::new();
1756        for first in 0..lp.len() {
1757            if used[first] {
1758                continue;
1759            }
1760            // On along the loop, or across the meridian where one leaves.
1761            // One running into a piece already traced runs round it into
1762            // itself.
1763            let around = walk(first, |&i| {
1764                let end = self.ends(lp[i]).1;
1765                Ok::<_, GeopError>(Some(match partner.get(&end) {
1766                    Some(&(other, _)) => starting[&other],
1767                    None => (i + 1) % lp.len(),
1768                }))
1769            })?;
1770            if around.end != Walked::Closed {
1771                return Err(GeopError::new(
1772                    "tracing a piece along its loop and the meridians did not close",
1773                ));
1774            }
1775            let mut piece = Vec::new();
1776            for &i in &around.nodes {
1777                used[i] = true;
1778                piece.push(lp[i]);
1779                let end = self.ends(lp[i]).1;
1780                if let Some(&(_, m)) = partner.get(&end) {
1781                    piece.push((m, self.edges[m].start == end));
1782                }
1783            }
1784            pieces.push(piece);
1785        }
1786        let face = &self.faces[f];
1787        let (surface, same_sense, shell, base_name, id) = (
1788            face.surface.clone(),
1789            face.same_sense,
1790            face.shell,
1791            face.name.clone(),
1792            face.id,
1793        );
1794        for (q, piece) in pieces.into_iter().enumerate() {
1795            let mut name = base_name.clone();
1796            name.push(format!("q{q}"));
1797            self.faces.push(Face {
1798                surface: surface.clone(),
1799                same_sense,
1800                loops: vec![piece],
1801                outer: Some(0),
1802                patch: None,
1803                shell,
1804                name,
1805                id,
1806            });
1807        }
1808        self.faces.remove(f);
1809        Ok(())
1810    }
1811
1812    /// Splits each of the rings `rings` of face `f` where it crosses each
1813    /// of the angles `cuts`, as `angle_of` measures a point's: the vertex
1814    /// each ring has at each cut, by ring.
1815    fn split_rings(
1816        &mut self,
1817        f: usize,
1818        rings: &[usize],
1819        cuts: &[f64],
1820        angle_of: &dyn Fn(P3) -> Option<f64>,
1821    ) -> GeopResult<HashMap<usize, Vec<usize>>> {
1822        let mut ring_vertices: HashMap<usize, Vec<usize>> = HashMap::new();
1823        let mut splits: HashMap<usize, Vec<(f64, usize, usize)>> = HashMap::new(); // edge -> (t, ring loop, cut)
1824        for &k in rings {
1825            let lp = self.faces[f].loops[k].clone();
1826            for (c, &cut) in cuts.iter().enumerate() {
1827                let (e, t) = self.crossing(&lp, cut, angle_of)?;
1828                splits.entry(e).or_default().push((t, k, c));
1829            }
1830        }
1831        let mut edges: Vec<usize> = splits.keys().copied().collect();
1832        edges.sort();
1833        for e in edges {
1834            let mut list = splits[&e].clone();
1835            list.sort_by(|a, b| a.0.total_cmp(&b.0));
1836            let ts: Vec<Scalar> = list.iter().map(|(t, _, _)| Scalar::from_f64(*t)).collect();
1837            let (lo, hi) = self.edges[e].curve.domain();
1838            if ts
1839                .iter()
1840                .any(|t| !(t.definitely_greater(lo) && t.definitely_less(hi)))
1841            {
1842                return Err(GeopError::new(format!(
1843                    "a cut crosses a loop round it at a vertex: the edge {} at its parameters {ts:?}, its domain [{lo:?}, {hi:?}]",
1844                    self.edges[e].label()
1845                )));
1846            }
1847            let created = self.split_edge(e, &ts)?;
1848            for ((_, ring, cut), v) in list.into_iter().zip(created) {
1849                let slots = ring_vertices
1850                    .entry(ring)
1851                    .or_insert_with(|| vec![usize::MAX; cuts.len()]);
1852                slots[cut] = v;
1853            }
1854        }
1855        Ok(ring_vertices)
1856    }
1857
1858    /// The coedges of the loop `lp` from the vertex `a` to the vertex `b`,
1859    /// running the loop's way (`along`) or against it.
1860    fn run_between(&self, lp: &[Use], a: usize, b: usize, along: bool) -> GeopResult<Vec<Use>> {
1861        let seq: Vec<Use> = if along {
1862            lp.to_vec()
1863        } else {
1864            lp.iter().rev().map(|&(e, fw)| (e, !fw)).collect()
1865        };
1866        let start = seq
1867            .iter()
1868            .position(|&u| self.ends(u).0 == a)
1869            .ok_or_else(|| GeopError::new("a cut vertex is not on its ring"))?;
1870        let mut run = Vec::new();
1871        for j in 0..seq.len() {
1872            let u = seq[(start + j) % seq.len()];
1873            run.push(u);
1874            if self.ends(u).1 == b {
1875                return Ok(run);
1876            }
1877        }
1878        Err(GeopError::new("a ring does not reach the next cut"))
1879    }
1880
1881    /// Where along the ring `lp` it crosses the angle `cut`: the edge,
1882    /// and its parameter there. A free choice of where exactly, refined
1883    /// as far as `f64` bisection goes: the cut's meridian is built through
1884    /// what the edge has there.
1885    fn crossing(
1886        &self,
1887        lp: &[Use],
1888        cut: f64,
1889        angle_of: &dyn Fn(P3) -> Option<f64>,
1890    ) -> GeopResult<(usize, f64)> {
1891        use std::f64::consts::TAU;
1892        let mut prev: Option<f64> = None;
1893        let mut covered = (f64::INFINITY, f64::NEG_INFINITY);
1894        for &u in lp {
1895            let samples = self.samples(u, LOOP_SAMPLES)?;
1896            let mut last: Option<(f64, f64)> = None; // (parameter, unwrapped angle)
1897            for &(t, p) in &samples {
1898                let Some(a) = angle_of(p) else {
1899                    continue;
1900                };
1901                let a = match prev {
1902                    Some(r) => unwrap_angle(a, r),
1903                    None => a,
1904                };
1905                if let Some((t_prev, a_prev)) = last {
1906                    let (lo, hi) = (a_prev.min(a), a_prev.max(a));
1907                    let x = cut + TAU * ((lo - cut) / TAU).ceil();
1908                    if x <= hi && lo < hi {
1909                        let theta = |t: f64| -> GeopResult<f64> {
1910                            let p = super::geometry::point_at(&self.edges[u.0].curve, t)?;
1911                            let angle = angle_of(p).ok_or_else(|| {
1912                                GeopError::new("a loop round its axis runs through it")
1913                            })?;
1914                            Ok(unwrap_angle(angle, x) - x)
1915                        };
1916                        let below = theta(t_prev)? <= 0.0;
1917                        let Bracket { lo, hi, .. } =
1918                            bisect(t_prev, t, 100, true, |mid| -> GeopResult<_> {
1919                                Ok(Some((theta(mid)? <= 0.0) == below))
1920                            })?;
1921                        return Ok((u.0, 0.5 * (lo + hi)));
1922                    }
1923                }
1924                last = Some((t, a));
1925                prev = Some(a);
1926                covered = (covered.0.min(a), covered.1.max(a));
1927            }
1928        }
1929        Err(GeopError::new(format!(
1930            "a loop round its axis never reaches the angle {cut}: it covers the angles {covered:?}"
1931        )))
1932    }
1933
1934    /// The meridian at angle `angle` from `bottom` to `top`, each a point,
1935    /// its profile parameter, and whether it is a pole.
1936    fn meridian(
1937        &self,
1938        revolved: &Revolved,
1939        angle: f64,
1940        (pb, vb, _): (P3, f64, bool),
1941        (pt, vt, _): (P3, f64, bool),
1942    ) -> GeopResult<NurbCurve3D> {
1943        let turn = revolved.turn(angle)?;
1944        let profile = match &revolved.profile {
1945            Profile::Curve(curve) => {
1946                let curve = curve.to_nurbs()?;
1947                let back = turn.inverse();
1948                let at = |p: P3| -> GeopResult<Scalar> {
1949                    let p = back.apply(&s3(p)).to_f64_array();
1950                    super::geometry::parameter_of(&curve, p)
1951                };
1952                let (tb, tt) = (at(pb)?, at(pt)?);
1953                let (lo, hi) = curve.domain();
1954                let (a, b, reversed) = if tb.definitely_less(tt) {
1955                    (tb, tt, false)
1956                } else {
1957                    (tt, tb, true)
1958                };
1959                let piece = curve.sub_curve(
1960                    if a.could_be_equal(lo) { lo } else { a },
1961                    if b.could_be_equal(hi) { hi } else { b },
1962                )?;
1963                if reversed { piece.reverse() } else { piece }
1964            }
1965            _ => revolved.profile_curve(vb, vt)?,
1966        };
1967        Ok(profile.transform(&turn.motion()))
1968    }
1969
1970    /// The body built, `label` what the file calls it.
1971    fn spec(self, solid: bool, label: String) -> GeopResult<ImportedBody> {
1972        let Builder {
1973            vertices,
1974            edges,
1975            faces,
1976            shells,
1977            scope,
1978            ..
1979        } = self;
1980        let mut vertices = vertices;
1981        let mut edges = edges;
1982        // Which edges and vertices are left, renumbered.
1983        let mut edge_map = vec![usize::MAX; edges.len()];
1984        let mut used_vertex = vec![false; vertices.len()];
1985        for face in &faces {
1986            for &(e, _) in face.loops.iter().flatten() {
1987                edge_map[e] = 0;
1988                used_vertex[edges[e].start] = true;
1989                used_vertex[edges[e].end] = true;
1990            }
1991        }
1992        let mut vertex_map = vec![usize::MAX; vertices.len()];
1993        let mut next = 0;
1994        for (v, used) in used_vertex.iter().enumerate() {
1995            if *used {
1996                vertex_map[v] = next;
1997                next += 1;
1998            }
1999        }
2000
2001        // Each face's patch, pointing out of the material.
2002        let mut face_specs = Vec::with_capacity(faces.len());
2003        // Each face's patch extent, for messages.
2004        let mut extents = Vec::with_capacity(faces.len());
2005        let mut face_names = Vec::with_capacity(faces.len());
2006        let mut face_shells = vec![Vec::new(); shells];
2007        let face_ctx = |face: &Face| {
2008            let what = format!("building the face #{} ({})", face.id, face.name.join(","));
2009            move |e: GeopError| e.with_context(what.clone())
2010        };
2011        // Every face's patch first: what its vertices and edges are healed
2012        // onto where they disagree with it.
2013        let mut built = Vec::with_capacity(faces.len());
2014        for face in &faces {
2015            let ctx = face_ctx(face);
2016            let patch = patch_of(face, &edges, &scope).map_err(&ctx)?;
2017            let surface = if face.outward_is_natural() {
2018                patch.surface.clone()
2019            } else {
2020                patch.surface.reverse_u()
2021            };
2022            let grid = Grid::new(&surface);
2023            built.push((patch, surface, grid));
2024        }
2025        let (rebuild, healed) = heal(&faces, &built, &mut edges, &mut vertices, &scope)?;
2026        for (f, face) in faces.iter().enumerate() {
2027            if rebuild[f] {
2028                let surface = rebuilt_surface(face, &edges).map_err(|e| {
2029                    e.with_context(format!(
2030                        "rebuilding the face #{} ({}) from its edges, where its surface contradicts its neighbours'",
2031                        face.id,
2032                        face.name.join(",")
2033                    ))
2034                })?;
2035                let grid = Grid::new(&surface);
2036                let patch = Patch {
2037                    surface: surface.clone(),
2038                    extent: None,
2039                    kind: PatchKind::Other,
2040                };
2041                built[f] = (patch, surface, grid);
2042            }
2043        }
2044        for (face, (patch, surface, grid)) in faces.iter().zip(built) {
2045            let ctx = face_ctx(face);
2046            let natural = face.outward_is_natural();
2047            let mut loops = Vec::new();
2048            for lp in &face.loops {
2049                loops.push(
2050                    fit_loop(lp, &surface, &grid, &patch, &edges, &mut vertices, &scope)
2051                        .map_err(&ctx)?,
2052                );
2053            }
2054            // The outer loop runs counter-clockwise in the patch, a hole
2055            // clockwise. A face of several outer loops — as some systems
2056            // write a face their own kernel splits later — is a face for
2057            // each, its holes in the one around them.
2058            let pieces = match face.outer {
2059                Some(k) => {
2060                    let outer = loops.remove(k);
2061                    vec![(outer, loops)]
2062                }
2063                None => split_by_outer_loops(&surface, loops).map_err(|e| {
2064                    ctx(e.with_context(format!(
2065                        "its outward normal the surface's natural one: {natural}; built over the angles and profile parameters {:?}",
2066                        patch.extent
2067                    )))
2068                })?,
2069            };
2070            let several = pieces.len() > 1;
2071            for (k, (outer_loop, holes)) in pieces.into_iter().enumerate() {
2072                face_shells[face.shell].push(face_specs.len());
2073                face_specs.push((surface.clone(), outer_loop, holes));
2074                extents.push(patch.extent);
2075                let mut name = face.name.clone();
2076                if several {
2077                    name.push(format!("o{k}"));
2078                }
2079                face_names.push(name);
2080            }
2081        }
2082
2083        // Every place the file says each edge is, united: its curve, and
2084        // where its pcurves put it on its faces' surfaces.
2085        // Widened by what is measured, an edge's enclosure meets what it is
2086        // measured against only to within rounding: measured again, it is
2087        // widened by what is left, until nothing is.
2088        let read: Vec<NurbCurve3D> = edges.iter().map(|e| e.curve.clone()).collect();
2089        // How far each edge has been widened so far, either way.
2090        let mut total = vec![0.0f64; edges.len()];
2091        let mut history: Vec<Vec<f64>> = vec![Vec::new(); edges.len()];
2092        // Which edges to measure: all at first, then those just widened —
2093        // an edge left as it was measures as it did.
2094        let mut measure = vec![true; edges.len()];
2095        // At most `WIDENINGS` rounds; what is left after them stays.
2096        refine(0, WIDENINGS, |round| -> GeopResult<_> {
2097            let mut gaps = vec![[0.0f64; 3]; edges.len()];
2098            for (((surface, outer, holes), face_name), extent) in
2099                face_specs.iter().zip(&face_names).zip(&extents)
2100            {
2101                for (on, pcurve) in outer.iter().chain(holes.iter().flatten()) {
2102                    if let CoedgeOnLocal::Edge(e, _) = *on
2103                        && measure[e]
2104                    {
2105                        let (gap, worst) =
2106                            edge_gap(&edges[e].curve, surface, pcurve).map_err(|err| {
2107                                err.with_context(format!(
2108                            "measuring how far the edge {} lies from where its pcurve puts it",
2109                            edges[e].label()
2110                        ))
2111                            })?;
2112                        // Widened by it either way, the edge must still be
2113                        // one curve to the kernel.
2114                        let most = total[e] + gap.iter().copied().fold(0.0, f64::max);
2115                        if 2.0 * most > ACCURACY {
2116                            // Whether the file's curve is off the surface,
2117                            // or the pcurve fitted to it is off the curve.
2118                            let off = off_surface(&read[e], surface, pcurve).map_or_else(
2119                                |err| format!("not measured: {err}"),
2120                                |d| format!("{d:e} mm"),
2121                            );
2122                            let furthest = worst.map_or_else(String::new, |w| {
2123                                format!(
2124                                    ", furthest {:e} mm at {} of the pcurve, which puts it at {:?}, the edge's nearest point {:?}",
2125                                    w.apart, w.at, w.point, w.near
2126                                )
2127                            });
2128                            return Err(GeopError::new(format!(
2129                                "the edge {} lies up to {most:e} mm from where its pcurve puts it on its face {}, its own points up to {off} from the surface — the curve widened by that either way would be wider than the {ACCURACY:e} mm the kernel can carry as one curve (measured {gap:?} per coordinate after widening it by {:?} in {round} rounds{furthest}; the edge from {:?} to {:?}, the pcurve from {:?} to {:?} on the patch over the angles and profile parameters {extent:?})",
2130                                edges[e].label(),
2131                                face_name.join(","),
2132                                history[e],
2133                                read[e].evaluate(read[e].domain().0)?.to_f64_array(),
2134                                read[e].evaluate(read[e].domain().1)?.to_f64_array(),
2135                                pcurve.evaluate(pcurve.domain().0)?,
2136                                pcurve.evaluate(pcurve.domain().1)?,
2137                            )));
2138                        }
2139                        for c in 0..3 {
2140                            gaps[e][c] = gaps[e][c].max(gap[c]);
2141                        }
2142                    }
2143                }
2144            }
2145            if gaps.iter().all(|g| *g == [0.0; 3]) {
2146                return Ok(Refine::Done(()));
2147            }
2148            for ((edge, gap), total) in edges.iter_mut().zip(&gaps).zip(&mut total) {
2149                // After the first round what is left is rounding, and a
2150                // widening by exactly it falls short by a rounding again:
2151                // the gap is measured from the curve's bound rounded
2152                // outwards, past the curve's true bound towards the point.
2153                // An edge `4.4e-16` off a plane was widened four times and
2154                // ended `6.7e-80` short of it. Twice the rounding reaches.
2155                // The factor is a workaround, kept knowingly: the fix is to
2156                // measure the gap from the curve's bound rounded towards the
2157                // point, which reaches with the gap itself.
2158                let gap = if round == 0 {
2159                    *gap
2160                } else {
2161                    gap.map(|g| 2.0 * g)
2162                };
2163                edge.curve = widened(&edge.curve, gap)?;
2164                *total += gap.iter().copied().fold(0.0, f64::max);
2165            }
2166            for (h, gap) in history.iter_mut().zip(&gaps) {
2167                h.push(gap.iter().copied().fold(0.0, f64::max));
2168            }
2169            for (m, gap) in measure.iter_mut().zip(&gaps) {
2170                *m = *gap != [0.0; 3];
2171            }
2172            Ok(Refine::Finer(round + 1))
2173        })?;
2174
2175        let mut edge_specs = Vec::new();
2176        let mut edge_names = Vec::new();
2177        for (e, edge) in edges.iter().enumerate() {
2178            if edge_map[e] == usize::MAX {
2179                continue;
2180            }
2181            edge_map[e] = edge_specs.len();
2182            edge_specs.push(EdgeSpec {
2183                curve: edge.curve.clone(),
2184                start: vertex_map[edge.start],
2185                end: vertex_map[edge.end],
2186            });
2187            edge_names.push(edge.name.clone());
2188        }
2189        let mut vertex_points = Vec::new();
2190        let mut vertex_names = Vec::new();
2191        for (v, vertex) in vertices.into_iter().enumerate() {
2192            if used_vertex[v] {
2193                vertex_points.push(vertex.point);
2194                vertex_names.push(vertex.name);
2195            }
2196        }
2197        let remap = |lp: Vec<(CoedgeOnLocal, NurbCurve2D)>| -> Vec<CoedgeSpec> {
2198            lp.into_iter()
2199                .map(|(on, pcurve)| CoedgeSpec {
2200                    on: match on {
2201                        CoedgeOnLocal::Edge(e, forward) => {
2202                            CoedgeOn::Edge(edge_map[e], Sense::from_forward(forward))
2203                        }
2204                        CoedgeOnLocal::Vertex(v) => CoedgeOn::Vertex(vertex_map[v]),
2205                    },
2206                    pcurve,
2207                })
2208                .collect()
2209        };
2210        let faces = face_specs
2211            .into_iter()
2212            .map(|(surface, outer, holes)| FaceSpec {
2213                surface,
2214                outer: remap(outer),
2215                holes: holes.into_iter().map(remap).collect(),
2216            })
2217            .collect();
2218        let spec = BodySpec {
2219            vertices: vertex_points,
2220            edges: edge_specs,
2221            faces,
2222            shells: face_shells.into_iter().filter(|s| !s.is_empty()).collect(),
2223            solid,
2224        };
2225        Ok(ImportedBody {
2226            label,
2227            spec,
2228            vertex_names,
2229            edge_names,
2230            face_names,
2231            healed,
2232        })
2233    }
2234}
2235
2236#[derive(Clone, Copy, Debug)]
2237enum CoedgeOnLocal {
2238    Edge(usize, bool),
2239    Vertex(usize),
2240}
2241
2242/// The surface a face lies on, built to cover it.
2243struct Patch {
2244    surface: NurbSurface3D,
2245    /// For a surface of revolution, the angles and profile parameters it
2246    /// was built over: for messages.
2247    extent: Option<[f64; 4]>,
2248    kind: PatchKind,
2249}
2250
2251/// What a [`Patch`] is the surface of, as far as a pcurve on it can be
2252/// written down rather than fitted.
2253#[derive(Clone, Debug)]
2254enum PatchKind {
2255    /// A plane's parallelogram, on which every pcurve is its edge in the
2256    /// plane's coordinates (see [`plane_pcurve`]).
2257    Plane,
2258    /// A surface of revolution's, on which a parallel or a meridian is a
2259    /// straight pcurve (see [`Revolved::iso_line`]).
2260    Revolved(Revolved),
2261    Other,
2262}
2263
2264/// A row of a patch's control points collapsed to one point: a pole of its
2265/// parametrization. The row is where `u` (`fixes_u`) or `v` is `at`.
2266#[derive(Clone, Copy, Debug)]
2267struct Pole {
2268    fixes_u: bool,
2269    at: Scalar,
2270    point: P3,
2271}
2272
2273/// The poles of `surface`: each boundary row whose control points all lie
2274/// within `uncertainty` of each other — the file's own statement of which
2275/// points are one.
2276fn poles_of(surface: &NurbSurface3D, uncertainty: f64) -> GeopResult<Vec<Pole>> {
2277    let point = |i: usize, j: usize| -> GeopResult<P3> {
2278        Ok(surface.control_points[i * surface.num_v + j]
2279            .dehomogenized()?
2280            .to_f64_array())
2281    };
2282    let (u_lo, u_hi) = surface.domain_u();
2283    let (v_lo, v_hi) = surface.domain_v();
2284    let (nu, nv) = (surface.num_u, surface.num_v);
2285    let mut poles = Vec::new();
2286    // Each boundary row: whether it is one of `u`, where, and its control
2287    // points by index.
2288    type Row = (bool, Scalar, Vec<(usize, usize)>);
2289    let rows: [Row; 4] = [
2290        (true, u_lo, (0..nv).map(|j| (0, j)).collect()),
2291        (true, u_hi, (0..nv).map(|j| (nu - 1, j)).collect()),
2292        (false, v_lo, (0..nu).map(|i| (i, 0)).collect()),
2293        (false, v_hi, (0..nu).map(|i| (i, nv - 1)).collect()),
2294    ];
2295    for (fixes_u, at, row) in rows {
2296        let points: Vec<P3> = row
2297            .iter()
2298            .map(|&(i, j)| point(i, j))
2299            .collect::<GeopResult<_>>()?;
2300        if points
2301            .iter()
2302            .all(|&p| distance(p, points[0]) <= uncertainty)
2303        {
2304            poles.push(Pole {
2305                fixes_u,
2306                at,
2307                point: points[0],
2308            });
2309        }
2310    }
2311    Ok(poles)
2312}
2313
2314/// The unwrapped angles about `revolved`'s axis of `points`, a loop's in
2315/// order: each within half a turn of the one before. With `closing`, the
2316/// loop is followed back to where it started, so the last angle less the
2317/// first is how far it turns.
2318///
2319/// A point on the axis has no angle. Where a loop runs through a pole, it
2320/// turns there by whatever angle lies between where it arrives and where
2321/// it leaves — the long way round as readily as the short — and which way
2322/// is the face's: a loop running counter-clockwise about the surface's
2323/// natural normal (`ccw`) runs along a pole at the top of the profile
2324/// backwards in angle, along one at the bottom forwards, as it runs along
2325/// the top and bottom of its region.
2326fn unwrap(revolved: &Revolved, points: &[P3], closing: bool, ccw: bool) -> Vec<f64> {
2327    use std::f64::consts::TAU;
2328    // Start at a point with an angle, so that a pole is always passed
2329    // between two.
2330    let start = points
2331        .iter()
2332        .position(|&p| revolved.chart(p).0.is_some())
2333        .unwrap_or(0);
2334    let n = points.len();
2335    let mean_v = points.iter().map(|&p| revolved.chart(p).1).sum::<f64>() / n.max(1) as f64;
2336    let mut out: Vec<f64> = Vec::with_capacity(n + 1);
2337    let mut pole: Option<f64> = None;
2338    for k in 0..n + usize::from(closing) {
2339        let p = points[(start + k) % n];
2340        let (angle, v) = revolved.chart(p);
2341        let Some(a) = angle else {
2342            pole = Some(v);
2343            continue;
2344        };
2345        let a = match (out.last(), pole.take()) {
2346            (None, _) => a,
2347            (Some(&prev), None) => unwrap_angle(a, prev),
2348            (Some(&prev), Some(v)) => {
2349                let turn = (a - prev).rem_euclid(TAU);
2350                let forwards = ccw != (v > mean_v);
2351                prev + if forwards || turn == 0.0 {
2352                    turn
2353                } else {
2354                    turn - TAU
2355                }
2356            }
2357        };
2358        out.push(a);
2359    }
2360    out
2361}
2362
2363/// The unwrapped angles round the tube of `revolved`, a torus, of
2364/// `points`, a loop's in order: each within half a turn of the one before.
2365/// With `closing`, the loop is followed back to where it started.
2366fn unwrap_tube(revolved: &Revolved, points: &[P3], closing: bool) -> Vec<f64> {
2367    let n = points.len();
2368    if n == 0 {
2369        return Vec::new();
2370    }
2371    let mut out: Vec<f64> = Vec::with_capacity(n + 1);
2372    for k in 0..n + usize::from(closing) {
2373        let a = revolved.chart(points[k % n]).1;
2374        out.push(match out.last() {
2375            None => a,
2376            Some(&prev) => unwrap_angle(a, prev),
2377        });
2378    }
2379    out
2380}
2381
2382/// How often unwrapped closing angles go round.
2383fn winding(angles: &[f64]) -> i64 {
2384    match (angles.first(), angles.last()) {
2385        (Some(a), Some(b)) => ((b - a) / std::f64::consts::TAU).round() as i64,
2386        _ => 0,
2387    }
2388}
2389
2390/// Angles to cut a band around an axis at, given the angles (single ones,
2391/// or ranges, unwrapped) where it must not be cut: two or more, evenly
2392/// spaced, placed as far from everything blocked as can be — the fewest
2393/// cuts that keep a clear berth.
2394fn choose_cuts(blocked: &[(f64, f64)]) -> Vec<f64> {
2395    use std::f64::consts::TAU;
2396    let clearance = |a: f64| -> f64 {
2397        blocked
2398            .iter()
2399            .map(|&(lo, hi)| {
2400                if hi - lo >= TAU {
2401                    return 0.0;
2402                }
2403                // The distance from `a` to the arc from `lo` to `hi`.
2404                let rel = (a - lo).rem_euclid(TAU);
2405                if rel <= hi - lo {
2406                    0.0
2407                } else {
2408                    (rel - (hi - lo)).min(TAU - rel)
2409                }
2410            })
2411            .fold(f64::INFINITY, f64::min)
2412    };
2413    let mut best: Option<(f64, Vec<f64>)> = None;
2414    for n in 2..=8usize {
2415        let step = TAU / n as f64;
2416        for i in 0..720 {
2417            let start = step * i as f64 / 720.0;
2418            let cuts: Vec<f64> = (0..n).map(|k| start + step * k as f64).collect();
2419            let c = cuts
2420                .iter()
2421                .map(|&a| clearance(a))
2422                .fold(f64::INFINITY, f64::min);
2423            // More cuts only where they clear things by much more.
2424            if best.as_ref().is_none_or(|(b, _)| c > 2.0 * b) {
2425                best = Some((c, cuts));
2426            }
2427        }
2428    }
2429    best.expect("at least one choice").1
2430}
2431
2432/// `[v0, v1]` widened a little either way where it is free to be — not at
2433/// a pole, nor past one, nor for a torus' tube further than once round.
2434fn widen_v(revolved: &Revolved, v0: f64, v1: f64, pole0: bool, pole1: bool) -> (f64, f64) {
2435    if matches!(revolved.profile, Profile::Curve(_)) {
2436        return (v0, v1);
2437    }
2438    let span = (v1 - v0).abs();
2439    let mut margin = 0.02 * span + 1e-3 * span.max(1e-9);
2440    if revolved.v_is_angle() {
2441        margin = margin.min((std::f64::consts::TAU - span) / 2.0 * 0.5);
2442    }
2443    let poles = revolved.poles();
2444    let lowest = poles
2445        .iter()
2446        .copied()
2447        .filter(|&p| p <= v0)
2448        .fold(f64::NEG_INFINITY, f64::max);
2449    let highest = poles
2450        .iter()
2451        .copied()
2452        .filter(|&p| p >= v1)
2453        .fold(f64::INFINITY, f64::min);
2454    let a = if pole0 { v0 } else { (v0 - margin).max(lowest) };
2455    let b = if pole1 {
2456        v1
2457    } else {
2458        (v1 + margin).min(highest)
2459    };
2460    (a, b)
2461}
2462
2463/// The patch a face lies on: the surface, built to cover the face.
2464fn patch_of(face: &Face, edges: &[Edge], scope: &Scope) -> GeopResult<Patch> {
2465    let mut points = Vec::new();
2466    let mut loop_points = Vec::new();
2467    for lp in &face.loops {
2468        let mut this = Vec::new();
2469        for &(e, forward) in lp {
2470            let curve = &edges[e].curve;
2471            let (lo, hi) = curve.domain();
2472            let (lo, hi) = (lo.to_f64(), hi.to_f64());
2473            for i in 0..LOOP_SAMPLES {
2474                let f = i as f64 / LOOP_SAMPLES as f64;
2475                let f = if forward { f } else { 1.0 - f };
2476                this.push(super::geometry::point_at(curve, lo + (hi - lo) * f)?);
2477            }
2478        }
2479        points.extend(this.iter().copied());
2480        loop_points.push(this);
2481    }
2482    match &face.surface.kind {
2483        SurfaceKind::Plane(frame) => {
2484            let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
2485            for &p in &points {
2486                let l = frame.local(p);
2487                for c in 0..2 {
2488                    lo[c] = lo[c].min(l[c]);
2489                    hi[c] = hi[c].max(l[c]);
2490                }
2491            }
2492            let margin = 0.05 * ((hi[0] - lo[0]).max(hi[1] - lo[1])).max(scope.uncertainty);
2493            let corner = |a: f64, b: f64| s3(frame.point([a, b, 0.0]));
2494            let (a0, a1, b0, b1) = (
2495                lo[0] - margin,
2496                hi[0] + margin,
2497                lo[1] - margin,
2498                hi[1] + margin,
2499            );
2500            let surface = NurbSurface3D::bilinear(
2501                corner(a0, b0),
2502                corner(a1, b0),
2503                corner(a1, b1),
2504                corner(a0, b1),
2505            )?;
2506            Ok(Patch {
2507                surface,
2508                extent: None,
2509                kind: PatchKind::Plane,
2510            })
2511        }
2512        SurfaceKind::Nurbs(nurbs) => Ok(Patch {
2513            surface: nurbs.to_nurbs()?,
2514            extent: None,
2515            kind: PatchKind::Other,
2516        }),
2517        SurfaceKind::Extrusion { curve, vector } => {
2518            let length2 = dot(*vector, *vector);
2519            let dir =
2520                normalize(*vector).ok_or_else(|| GeopError::new("an extrusion along nothing"))?;
2521            let along = |p: P3| dot(p, dir);
2522            let (p_lo, p_hi) = points
2523                .iter()
2524                .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &p| {
2525                    (a.min(along(p)), b.max(along(p)))
2526                });
2527            let (c_lo, c_hi) = curve
2528                .points
2529                .iter()
2530                .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &p| {
2531                    (a.min(along(p)), b.max(along(p)))
2532                });
2533            let length = length2.sqrt();
2534            let (v0, v1) = ((p_lo - c_hi) / length, (p_hi - c_lo) / length);
2535            let margin = 0.05 * (v1 - v0).abs().max(scope.uncertainty / length);
2536            let (v0, v1) = (v0 - margin, v1 + margin);
2537            let curve = curve.to_nurbs()?;
2538            let start = curve.translate(s3(scale(*vector, v0)));
2539            Ok(Patch {
2540                surface: start.sweep(s3(scale(*vector, v1 - v0))),
2541                extent: None,
2542                kind: PatchKind::Other,
2543            })
2544        }
2545        SurfaceKind::Revolved(revolved) => {
2546            let [from, to, v0, v1] = match face.patch {
2547                Some(patch) => patch,
2548                None => revolved_extent(revolved, face, &loop_points, scope)?,
2549            };
2550            Ok(Patch {
2551                surface: revolved.patch(from, to, v0, v1)?,
2552                extent: Some([from, to, v0, v1]),
2553                kind: PatchKind::Revolved(revolved.clone()),
2554            })
2555        }
2556    }
2557}
2558
2559/// The angles and profile parameters a face on a surface of revolution
2560/// covers, which does not wrap around its axis, a little widened where
2561/// they are free to be.
2562fn revolved_extent(
2563    revolved: &Revolved,
2564    face: &Face,
2565    loops: &[Vec<P3>],
2566    scope: &Scope,
2567) -> GeopResult<[f64; 4]> {
2568    // The outer loop spans the face's angles; each hole lies within them.
2569    let mut best: Option<(f64, f64, f64)> = None;
2570    for points in loops {
2571        let a = unwrap(revolved, points, false, face.outward_is_natural());
2572        if a.is_empty() {
2573            continue;
2574        }
2575        let lo = a.iter().copied().fold(f64::INFINITY, f64::min);
2576        let hi = a.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2577        if best.is_none_or(|(b_lo, b_hi, _)| hi - lo > b_hi - b_lo) {
2578            best = Some((lo, hi, 0.0));
2579        }
2580    }
2581    let (lo, hi, _) =
2582        best.ok_or_else(|| GeopError::new("a face whose every point lies on its axis"))?;
2583    let span = hi - lo;
2584    if span >= std::f64::consts::TAU {
2585        return Err(GeopError::new(format!(
2586            "#{}: a face spanning a full turn or more around its axis without a loop going round it — as a thread's flank does — is not supported",
2587            face.id
2588        )));
2589    }
2590    let margin = (0.02 * span + 1e-3).min((std::f64::consts::TAU - span) / 4.0);
2591    // Profile parameters: the analytic ones, or, for a pole, exactly it.
2592    let mut v_lo = f64::INFINITY;
2593    let mut v_hi = f64::NEG_INFINITY;
2594    let mut torus_angles = Vec::new();
2595    for points in loops {
2596        for &p in points {
2597            let v = revolved.chart(p).1;
2598            torus_angles.push(v);
2599            v_lo = v_lo.min(v);
2600            v_hi = v_hi.max(v);
2601        }
2602    }
2603    if revolved.v_is_angle() {
2604        // A torus' tube angle wraps at half a turn; the face is where the
2605        // samples are, so measure from the largest gap between them.
2606        torus_angles.sort_by(f64::total_cmp);
2607        let n = torus_angles.len();
2608        let mut gap = (
2609            torus_angles[0] + std::f64::consts::TAU - torus_angles[n - 1],
2610            0usize,
2611        );
2612        for i in 1..n {
2613            let g = torus_angles[i] - torus_angles[i - 1];
2614            if g > gap.0 {
2615                gap = (g, i);
2616            }
2617        }
2618        v_lo = torus_angles[gap.1];
2619        v_hi = if gap.1 == 0 {
2620            torus_angles[n - 1]
2621        } else {
2622            torus_angles[gap.1 - 1] + std::f64::consts::TAU
2623        };
2624    }
2625    let near_pole = |v: f64| {
2626        revolved.poles().into_iter().find(|&pole| {
2627            distance(revolved.profile_point(pole), revolved.profile_point(v))
2628                <= scope.uncertainty.max(1e-9)
2629        })
2630    };
2631    let (pole0, pole1) = match &revolved.profile {
2632        Profile::Curve(_) => (None, None),
2633        _ => (near_pole(v_lo), near_pole(v_hi)),
2634    };
2635    let (v0, v1) = widen_v(
2636        revolved,
2637        pole0.unwrap_or(v_lo),
2638        pole1.unwrap_or(v_hi),
2639        pole0.is_some(),
2640        pole1.is_some(),
2641    );
2642    Ok([lo - margin, hi + margin, v0, v1])
2643}
2644
2645/// A grid of a patch's points, to seed projections onto it from, and its
2646/// midpoints to search for foot points on in `f64`.
2647struct Grid {
2648    surface: SurfaceMidpoints,
2649    points: Vec<(f64, f64, P3)>,
2650}
2651
2652const GRID: usize = 12;
2653
2654/// How many of the nearest grid points a projection tries Newton from.
2655const SEEDS: usize = 4;
2656
2657impl Grid {
2658    fn new(surface: &NurbSurface3D) -> Self {
2659        let surface = SurfaceMidpoints::of(surface);
2660        let (u0, u1) = surface.domain_u();
2661        let (v0, v1) = surface.domain_v();
2662        let mut points = Vec::with_capacity((GRID + 1) * (GRID + 1));
2663        for i in 0..=GRID {
2664            for j in 0..=GRID {
2665                let u = u0 + (u1 - u0) * i as f64 / GRID as f64;
2666                let v = v0 + (v1 - v0) * j as f64 / GRID as f64;
2667                points.push((u, v, surface.point(u, v)));
2668            }
2669        }
2670        Self { surface, points }
2671    }
2672
2673    /// The foot point `(u, v)` of `p` on the surface's midpoints, a free
2674    /// choice of where to start from or a measurement in `f64`: Newton
2675    /// runs from the few nearest grid points, and from `seed` if there is
2676    /// one, and the foot point nearest `p` wins — on a curved patch the
2677    /// nearest grid point can lie in the basin of another foot point, and a
2678    /// seed at a pole may lead down the wrong meridian.
2679    fn foot(&self, p: P3, seed: Option<(f64, f64)>) -> (f64, f64) {
2680        let mut near: Vec<&(f64, f64, P3)> = self.points.iter().collect();
2681        let nearest = SEEDS.min(near.len());
2682        near.select_nth_unstable_by(nearest - 1, |a, b| {
2683            distance(a.2, p).total_cmp(&distance(b.2, p))
2684        });
2685        let seeds = near[..nearest].iter().map(|&&(u, v, _)| (u, v));
2686        let mut best = (f64::INFINITY, (near[0].0, near[0].1));
2687        for (u, v) in seed.into_iter().chain(seeds) {
2688            let (u, v) = self.surface.project(p, u, v);
2689            let d = distance(self.surface.point(u, v), p);
2690            if d < best.0 {
2691                best = (d, (u, v));
2692            }
2693        }
2694        best.1
2695    }
2696
2697    /// The foot point of `p` on `surface`, the patch's own: Newton on the
2698    /// enclosures, from the foot point on its midpoints.
2699    fn project(&self, surface: &NurbSurface3D, p: &Vector3) -> GeopResult<(Scalar, Scalar)> {
2700        let (u, v) = self.foot(p.to_f64_array(), None);
2701        surface.project(*p, Scalar::from_f64(u), Scalar::from_f64(v), 30)
2702    }
2703}
2704
2705/// Fits the pcurves of the loop `lp` on `surface`, pinning both ends of
2706/// each to its vertex's foot point and joining coedges meeting at a pole
2707/// with a coedge sitting at it. Widens every vertex to enclose the ends of
2708/// its edges and its foot points.
2709fn fit_loop(
2710    lp: &[Use],
2711    surface: &NurbSurface3D,
2712    grid: &Grid,
2713    patch: &Patch,
2714    edges: &[Edge],
2715    vertices: &mut [Vertex],
2716    scope: &Scope,
2717) -> GeopResult<Vec<(CoedgeOnLocal, NurbCurve2D)>> {
2718    let n = lp.len();
2719    let ends = |(e, forward): Use| edges[e].ends(forward);
2720    let poles = poles_of(surface, scope.uncertainty)?;
2721    // The pole a vertex sits at, if any.
2722    let pole_of = |v: usize, vertices: &[Vertex]| -> Option<Pole> {
2723        let p = vertices[v].origin.to_f64_array();
2724        poles
2725            .iter()
2726            .find(|pole| distance(pole.point, p) <= scope.uncertainty)
2727            .copied()
2728    };
2729    // Where each corner — the end of coedge `k`, the start of `k + 1` —
2730    // is on the patch: its vertex's foot point, unless at a pole.
2731    let mut corners: Vec<Option<Vector2>> = Vec::with_capacity(n);
2732    for &u in lp {
2733        let (_, end) = ends(u);
2734        corners.push(match pole_of(end, vertices) {
2735            Some(_) => None,
2736            None => {
2737                let (pu, pv) = grid.project(surface, &vertices[end].origin)?;
2738                Some(Vector2::from_array([pu, pv]))
2739            }
2740        });
2741    }
2742    let mut out = Vec::new();
2743    let mut pins = Vec::with_capacity(n);
2744    for k in 0..n {
2745        let u = lp[k];
2746        let (start, end) = ends(u);
2747        let edge = &edges[u.0];
2748        let curve = if u.1 {
2749            edge.curve.clone()
2750        } else {
2751            edge.curve.reverse()
2752        };
2753        let pin = |corner: Option<Vector2>, vertex: usize, at_start: bool| -> GeopResult<Vector2> {
2754            match corner {
2755                Some(uv) => Ok(uv),
2756                None => {
2757                    // At a pole the free parameter names the same point
2758                    // whatever it is: take the one the curve arrives along,
2759                    // from a point of it near the pole.
2760                    let (lo, hi) = curve.domain();
2761                    let f = Scalar::from_ratio(1, 64)?;
2762                    let t = if at_start {
2763                        lo.add(hi.sub(lo).mul(f))
2764                    } else {
2765                        hi.sub(hi.sub(lo).mul(f))
2766                    };
2767                    let near = curve.evaluate(t.sharpen())?;
2768                    let (pu, pv) = grid.project(surface, &near)?;
2769                    let pole = pole_of(vertex, vertices)
2770                        .expect("a corner without a foot point is at a pole");
2771                    Ok(if pole.fixes_u {
2772                        Vector2::from_array([pole.at, pv])
2773                    } else {
2774                        Vector2::from_array([pu, pole.at])
2775                    })
2776                }
2777            }
2778        };
2779        let pin_start = pin(corners[(k + n - 1) % n], start, true)?;
2780        let pin_end = pin(corners[k], end, false)?;
2781        let ctx = |e: GeopError| {
2782            e.with_context(format!("fitting the pcurve of the edge {}", edge.label()))
2783        };
2784        // Not from a pole: there the pin's free parameter is taken from a
2785        // point near it (see `pin`), not where the line runs.
2786        let at_pole = corners[(k + n - 1) % n].is_none() || corners[k].is_none();
2787        let straight = match (&patch.kind, &edge.shape) {
2788            (PatchKind::Revolved(revolved), Some(shape)) => {
2789                !at_pole && revolved.iso_line(shape, scope.uncertainty)
2790            }
2791            _ => false,
2792        };
2793        let pcurve = if let PatchKind::Plane = patch.kind {
2794            plane_pcurve(surface, &curve, pin_start, pin_end).with_context(&ctx)?
2795        } else if straight {
2796            // A parallel or a meridian: straight in the patch, from one
2797            // end's foot point to the other's.
2798            NurbCurve2D::line(pin_start, pin_end).with_context(&ctx)?
2799        } else {
2800            surface
2801                .fit_pcurve(
2802                    &curve,
2803                    Some(pin_start),
2804                    Some(pin_end),
2805                    MAX_NODES,
2806                    Scalar::from_f64(MIN_SUBDIVISION_SIZE),
2807                )
2808                .with_context(&ctx)?
2809        };
2810        out.push((CoedgeOnLocal::Edge(u.0, u.1), pcurve));
2811        pins.push((pin_start, pin_end));
2812    }
2813    // Every place each vertex is said to be, united.
2814    for k in 0..n {
2815        let u = lp[k];
2816        let (start, end) = ends(u);
2817        let edge = &edges[u.0];
2818        let (lo, hi) = edge.curve.domain();
2819        let (curve_start, curve_end) = if u.1 {
2820            (edge.curve.evaluate(lo)?, edge.curve.evaluate(hi)?)
2821        } else {
2822            (edge.curve.evaluate(hi)?, edge.curve.evaluate(lo)?)
2823        };
2824        // The pins, not the pcurve's ends: those are widened by the fit's
2825        // drift, which says where the pcurve may be, not the vertex.
2826        let (a, b) = pins[k];
2827        let on_start = surface.evaluate(a[0], a[1])?;
2828        let on_end = surface.evaluate(b[0], b[1])?;
2829        for (v, extra) in [(start, [curve_start, on_start]), (end, [curve_end, on_end])] {
2830            for (p, source) in extra.into_iter().zip([
2831                "the end of the edge",
2832                "its foot point on this face, at the end of the edge",
2833            ]) {
2834                let off = distance(p.to_f64_array(), vertices[v].origin.to_f64_array());
2835                if off > ACCURACY {
2836                    return Err(GeopError::new(format!(
2837                        "the vertex {} is at {:?}, but {source} {} is {off:e} mm from it — more than the {ACCURACY:e} mm the kernel can carry as one point (the face built over the angles and profile parameters {:?})",
2838                        vertices[v].label(),
2839                        vertices[v].origin.to_f64_array(),
2840                        edge.label(),
2841                        patch.extent,
2842                    )));
2843                }
2844                vertices[v].point = vertices[v].point.union(&p);
2845            }
2846        }
2847    }
2848    // Coedges meeting at a pole, where their pcurves end at different `u`,
2849    // are joined along the pole's row.
2850    let mut joined = Vec::with_capacity(out.len());
2851    for k in 0..n {
2852        joined.push(out[k].clone());
2853        let next = (k + 1) % n;
2854        let (_, end) = ends(lp[k]);
2855        if corners[k].is_none() {
2856            let (_, t1) = out[k].1.domain();
2857            let (t0, _) = out[next].1.domain();
2858            let from = out[k].1.evaluate(t1)?;
2859            let to = out[next].1.evaluate(t0)?;
2860            if !from.could_be_equal(&to) {
2861                let pcurve = NurbCurve2D::line(from, to)?;
2862                joined.push((CoedgeOnLocal::Vertex(end), pcurve));
2863            }
2864        }
2865    }
2866    Ok(joined)
2867}
2868
2869/// The nearest point of `surface` to `p`, and the surface's unit normal
2870/// there — none at an apex, where it has no single one. Measured on its
2871/// midpoints, where the derivatives there span a plane; at a pole, where
2872/// they do not, the surface's own limit normal.
2873fn foot(surface: &NurbSurface3D, grid: &Grid, p: P3) -> GeopResult<(P3, Option<P3>)> {
2874    let (u, v) = grid.foot(p, None);
2875    let (at, su, sv) = grid.surface.partials(u, v);
2876    let normal = match normalize(cross(su, sv)) {
2877        Some(n) => Some(n),
2878        None => surface
2879            .normal(Scalar::from_f64(u), Scalar::from_f64(v))
2880            .ok()
2881            .map(|n| n.to_f64_array()),
2882    };
2883    Ok((at, normal))
2884}
2885
2886/// How far `p` lies from `surface`.
2887fn off(surface: &NurbSurface3D, grid: &Grid, p: P3) -> GeopResult<f64> {
2888    Ok(distance(foot(surface, grid, p)?.0, p))
2889}
2890
2891/// Samples along an edge telling whether it strays from its faces.
2892const STRAY_SAMPLES: usize = 16;
2893
2894/// How far `curve` strays from the surface `grid` is of, sampled: each
2895/// sample's foot point found from the grid and, a walk along the curve as
2896/// `fit_pcurve` takes, from the one before.
2897fn strays(curve: &CurveMidpoints, grid: &Grid) -> f64 {
2898    let (lo, hi) = curve.domain();
2899    let mut seed: Option<(f64, f64)> = None;
2900    let mut most = 0.0f64;
2901    for k in 0..=STRAY_SAMPLES {
2902        let p = curve.point(lo + (hi - lo) * k as f64 / STRAY_SAMPLES as f64);
2903        let (u, v) = grid.foot(p, seed);
2904        seed = Some((u, v));
2905        most = most.max(distance(grid.surface.point(u, v), p));
2906    }
2907    most
2908}
2909
2910/// Newton steps taken towards where surfaces meet.
2911const MEET_ITERATIONS: usize = 40;
2912
2913/// How much a step towards where surfaces meet is held back along a
2914/// direction they barely constrain (Levenberg–Marquardt damping, against
2915/// unit normals): where surfaces meet at an angle under a thousandth of a
2916/// radian, the point stays near where it was along their tangent instead of
2917/// running off along it. A free choice of which point, among those the
2918/// surfaces leave free; whether it lies on them is checked after.
2919const MEET_DAMPING: f64 = 1e-6;
2920
2921/// The point near `start` lying on every one of `surfaces`, where they
2922/// meet: Newton's step on their tangent planes, from `start`, each moving
2923/// the point as little as satisfies them. Whether it got there is for the
2924/// caller to check ([`off`]): surfaces that do not meet leave it where they
2925/// come nearest to each other.
2926fn meet(start: P3, surfaces: &[(&NurbSurface3D, &Grid)]) -> GeopResult<P3> {
2927    // In plain numbers, so a step that changes nothing is a fixed point.
2928    Ok(iterate(
2929        start,
2930        start,
2931        MEET_ITERATIONS,
2932        Repeat::FixedPoint,
2933        |&next| (next, next),
2934        |&x| -> GeopResult<Step<P3>> {
2935            // (Σ n nᵀ + μ I) dx = -Σ n (n · (x - f)).
2936            let mut a: [[f64; 3]; 3] = std::array::from_fn(|i| {
2937                std::array::from_fn(|j| if i == j { MEET_DAMPING } else { 0.0 })
2938            });
2939            let mut b = [0.0f64; 3];
2940            for (surface, grid) in surfaces {
2941                let (f, normal) = foot(surface, grid, x)?;
2942                // Off the surface, the way to its nearest point is its normal
2943                // there, whatever the surface's own: what an apex has instead.
2944                let Some(n) = normal.or_else(|| normalize(sub(x, f))) else {
2945                    continue;
2946                };
2947                let r = dot(n, sub(x, f));
2948                for i in 0..3 {
2949                    for j in 0..3 {
2950                        a[i][j] += n[i] * n[j];
2951                    }
2952                    b[i] -= n[i] * r;
2953                }
2954            }
2955            let det = |m: [[f64; 3]; 3]| dot(m[0], cross(m[1], m[2]));
2956            let d = det(a);
2957            // Cramer's rule: `a` is symmetric positive definite.
2958            let dx: P3 = std::array::from_fn(|c| {
2959                let mut m = a;
2960                for (row, value) in m.iter_mut().zip(b) {
2961                    row[c] = value;
2962                }
2963                det(m) / d
2964            });
2965            Ok(Step::Next(add(x, dx)))
2966        },
2967    )?
2968    .seed)
2969}
2970
2971/// Samples along an edge rebuilt where its faces' surfaces meet, at first
2972/// and at most.
2973const REBUILT_SAMPLES: usize = 16;
2974const MOST_REBUILT_SAMPLES: usize = 256;
2975
2976/// Where the file's vertices and edges lie further from the surfaces of
2977/// their faces than the kernel can carry as one point or one curve (see
2978/// [`ACCURACY`]), they are rebuilt from those surfaces, which a B-rep is
2979/// defined by: a vertex where the surfaces of its faces meet nearest to the
2980/// file's point, an edge through where the surfaces of its faces meet along
2981/// it, between its vertices. Nearer, the file's places are kept and united
2982/// (see `fit_loop` and the edges' widening in `Builder::spec`), as the
2983/// enclosure of one point or curve.
2984///
2985/// The file's uncertainty bounds what is rebuilt: the surfaces have to meet
2986/// to within it — they are the file's statement of where the solid is, to
2987/// its accuracy. Where they contradict each other — those of a vertex's
2988/// faces do not meet near it, or those of an edge's along it — the
2989/// smallest of the faces there are left out, one by one, until the rest
2990/// meet: their surfaces are rebuilt from their edges instead (see
2991/// [`rebuilt_surface`]), and returned as such. A face that small is one the
2992/// file approximated more coarsely than its neighbours: a narrow wall
2993/// running tangent into a hole, say. An edge between such faces only is
2994/// moved with its vertices. Refused, by name, where a vertex or an edge
2995/// would move further than half the length of an edge there: that is no
2996/// longer the same vertex or edge.
2997fn heal(
2998    faces: &[Face],
2999    built: &[(Patch, NurbSurface3D, Grid)],
3000    edges: &mut [Edge],
3001    vertices: &mut [Vertex],
3002    scope: &Scope,
3003) -> GeopResult<(Vec<bool>, Healing)> {
3004    let mut faces_of_edge: Vec<Vec<usize>> = vec![Vec::new(); edges.len()];
3005    let mut faces_of_vertex: Vec<Vec<usize>> = vec![Vec::new(); vertices.len()];
3006    let mut edges_of_vertex: Vec<Vec<usize>> = vec![Vec::new(); vertices.len()];
3007    for (f, face) in faces.iter().enumerate() {
3008        for &(e, _) in face.loops.iter().flatten() {
3009            let add = |list: &mut Vec<usize>, item: usize| {
3010                if !list.contains(&item) {
3011                    list.push(item);
3012                }
3013            };
3014            add(&mut faces_of_edge[e], f);
3015            for v in [edges[e].start, edges[e].end] {
3016                add(&mut faces_of_vertex[v], f);
3017                add(&mut edges_of_vertex[v], e);
3018            }
3019        }
3020    }
3021    let surfaces = |fs: &[usize]| -> Vec<(&NurbSurface3D, &Grid)> {
3022        fs.iter().map(|&f| (&built[f].1, &built[f].2)).collect()
3023    };
3024    let names = |fs: &[usize]| -> String {
3025        fs.iter()
3026            .map(|&f| format!("{} (#{})", faces[f].name.join(","), faces[f].id))
3027            .collect::<Vec<_>>()
3028            .join(", ")
3029    };
3030    // How far `p` lies from each of `fs`, at most.
3031    let furthest = |p: P3, fs: &[usize]| -> GeopResult<f64> {
3032        let mut most = 0.0f64;
3033        for (surface, grid) in surfaces(fs) {
3034            most = most.max(off(surface, grid, p)?);
3035        }
3036        Ok(most)
3037    };
3038    let chord = |e: &Edge, vertices: &[Vertex]| {
3039        distance(
3040            vertices[e.start].origin.to_f64_array(),
3041            vertices[e.end].origin.to_f64_array(),
3042        )
3043    };
3044    // How large each face is, to tell which to leave out first: the area
3045    // its loops' samples enclose, as vectors summed — the face's own area
3046    // where it is flat, about it where it is not.
3047    let mut size = Vec::with_capacity(faces.len());
3048    for face in faces {
3049        let mut area = [0.0f64; 3];
3050        for lp in &face.loops {
3051            let points = loop_points(edges, lp);
3052            for (k, &p) in points.iter().enumerate() {
3053                area = add(area, scale(cross(p, points[(k + 1) % points.len()]), 0.5));
3054            }
3055        }
3056        size.push(norm(area));
3057    }
3058
3059    // Of the faces `fs`, those whose surfaces are kept: all but those to
3060    // be rebuilt, the smallest first.
3061    let kept = |fs: &[usize], rebuild: &[bool]| -> Vec<usize> {
3062        let mut kept: Vec<usize> = fs.iter().copied().filter(|&f| !rebuild[f]).collect();
3063        kept.sort_by(|&a, &b| size[a].total_cmp(&size[b]));
3064        kept
3065    };
3066    // Where the surfaces of the faces `fs` meet within `reach` of `p` —
3067    // where they contradict each other, the least area of faces that can be
3068    // rebuilt (see `rebuilt_surface`) left out, to be rebuilt, so that the
3069    // rest do. `None` where all of them are rebuilt; an error naming them
3070    // where no choice serves.
3071    let settle = |p: P3,
3072                  fs: &[usize],
3073                  reach: f64,
3074                  rebuild: &mut [bool]|
3075     -> GeopResult<Option<P3>> {
3076        let kept = kept(fs, rebuild);
3077        let free: Vec<usize> = kept
3078            .iter()
3079            .copied()
3080            .filter(|&f| rebuildable(&faces[f]))
3081            .take(MOST_REBUILT_AT_ONCE)
3082            .collect();
3083        let mut choices: Vec<(f64, Vec<usize>)> = (0..1usize << free.len())
3084            .map(|mask| {
3085                let out: Vec<usize> = (0..free.len())
3086                    .filter(|k| mask & (1 << k) != 0)
3087                    .map(|k| free[k])
3088                    .collect();
3089                (out.iter().map(|&f| size[f]).sum(), out)
3090            })
3091            .collect();
3092        choices.sort_by(|a, b| a.0.total_cmp(&b.0));
3093        let mut nearest = None;
3094        for (_, out) in choices {
3095            let rest: Vec<usize> = kept.iter().copied().filter(|f| !out.contains(f)).collect();
3096            let healed = if rest.is_empty() {
3097                None
3098            } else {
3099                let healed = meet(p, &surfaces(&rest))?;
3100                let left = furthest(healed, &rest)?;
3101                nearest.get_or_insert((left, healed));
3102                if left > scope.uncertainty || distance(healed, p) > reach {
3103                    continue;
3104                }
3105                Some(healed)
3106            };
3107            for f in out {
3108                rebuild[f] = true;
3109            }
3110            return Ok(healed);
3111        }
3112        let (left, healed) = nearest.unwrap_or((0.0, p));
3113        Err(GeopError::new(format!(
3114            "the surfaces of its faces {} {} — and no choice of the faces of one loop of four edges or more among them, rebuilt from their edges instead, leaves the rest meeting there",
3115            names(&kept),
3116            if left > scope.uncertainty {
3117                format!(
3118                    "do not meet near {p:?} to within the file's uncertainty of {:e} mm: they come no nearer each other than {left:e} mm, at {healed:?}",
3119                    scope.uncertainty
3120                )
3121            } else {
3122                format!(
3123                    "meet only {:e} mm from {p:?}, at {healed:?}, more than the {reach:e} mm it may move: half the shortest edge there",
3124                    distance(healed, p)
3125                )
3126            },
3127        )))
3128    };
3129
3130    let mut rebuild = vec![false; faces.len()];
3131    let mut moved = vec![false; vertices.len()];
3132    // What was moved, and how far, for the report.
3133    let mut moved_vertices: Vec<(String, f64)> = Vec::new();
3134    let mut moved_edges: Vec<(String, f64)> = Vec::new();
3135    // How far each vertex and edge lies from the surfaces of its faces kept,
3136    // as the first pass measured it: the second measures again only where
3137    // a face of it is to be rebuilt since.
3138    let mut vertex_apart: Vec<Option<(Vec<usize>, f64)>> = vec![None; vertices.len()];
3139    let mut edge_apart: Vec<Option<(Vec<usize>, f64)>> = vec![None; edges.len()];
3140    // Twice: first deciding which faces are rebuilt, everywhere, then
3141    // moving what is to move onto the surfaces kept — so that a vertex is
3142    // not put where a surface meets the others that a later vertex finds
3143    // contradicting its neighbours.
3144    for apply in [false, true] {
3145        moved.fill(false);
3146        for v in 0..vertices.len() {
3147            if faces_of_vertex[v].is_empty() {
3148                continue;
3149            }
3150            let at = vertices[v].origin.to_f64_array();
3151            let what = format!("healing the vertex {} at {at:?}", vertices[v].label());
3152            let vctx = |e: GeopError| e.with_context(what.clone());
3153            let fs = kept(&faces_of_vertex[v], &rebuild);
3154            let apart = match &vertex_apart[v] {
3155                Some((measured, apart)) if *measured == fs => *apart,
3156                _ => furthest(at, &fs).map_err(vctx)?,
3157            };
3158            vertex_apart[v] = Some((fs.clone(), apart));
3159            if 2.0 * apart <= ACCURACY {
3160                continue;
3161            }
3162            let shortest = edges_of_vertex[v]
3163                .iter()
3164                .map(|&e| chord(&edges[e], vertices))
3165                .fold(f64::INFINITY, f64::min);
3166            let Some(healed) = settle(at, &fs, shortest / 2.0, &mut rebuild).map_err(vctx)? else {
3167                // Every face of it rebuilt: through it, where it is.
3168                continue;
3169            };
3170            moved[v] = true;
3171            if !apply {
3172                continue;
3173            }
3174            vertices[v].origin = s3(healed);
3175            vertices[v].point = s3(healed);
3176            moved_vertices.push((vertices[v].label(), distance(healed, at)));
3177        }
3178
3179        for e in 0..edges.len() {
3180            if faces_of_edge[e].is_empty() || !edges[e].alive {
3181                continue;
3182            }
3183            let what = format!("healing the edge {}", edges[e].label());
3184            let ectx = |e: GeopError| e.with_context(what.clone());
3185            let old = edges[e].curve.clone();
3186            let (lo, hi) = old.domain();
3187            let (lo, hi) = (lo.to_f64(), hi.to_f64());
3188            let at = |f: f64| super::geometry::point_at(&old, lo + (hi - lo) * f);
3189            let (start, end) = (edges[e].start, edges[e].end);
3190            let fs = kept(&faces_of_edge[e], &rebuild);
3191            let apart = match &edge_apart[e] {
3192                Some((measured, apart)) if *measured == fs => *apart,
3193                _ => {
3194                    let midpoints = CurveMidpoints::of(&old);
3195                    let mut apart = 0.0f64;
3196                    for (_, grid) in surfaces(&fs) {
3197                        apart = apart.max(strays(&midpoints, grid));
3198                    }
3199                    apart
3200                }
3201            };
3202            edge_apart[e] = Some((fs.clone(), apart));
3203            if !moved[start] && !moved[end] && 2.0 * apart <= ACCURACY {
3204                continue;
3205            }
3206            // Which faces' surfaces the edge is rebuilt on: decided at its
3207            // middle.
3208            let length = chord(&edges[e], vertices);
3209            let middle =
3210                settle(at(0.5).map_err(ectx)?, &fs, length / 2.0, &mut rebuild).map_err(ectx)?;
3211            if !apply {
3212                continue;
3213            }
3214            let fs = kept(&fs, &rebuild);
3215            // How far its ends move with its vertices.
3216            let ends = [at(0.0).map_err(ectx)?, at(1.0).map_err(ectx)?];
3217            let shifts = [
3218                sub(vertices[start].origin.to_f64_array(), ends[0]),
3219                sub(vertices[end].origin.to_f64_array(), ends[1]),
3220            ];
3221            // Where the old curve's point `p`, `f` of the way along it, goes:
3222            // where the kept surfaces meet, or — none kept — along with its
3223            // vertices.
3224            let furthest_shift = std::cell::Cell::new(0.0f64);
3225            let onto = |p: P3, f: f64| -> GeopResult<P3> {
3226                if middle.is_none() {
3227                    let moved = add(p, add(scale(shifts[0], 1.0 - f), scale(shifts[1], f)));
3228                    furthest_shift.set(furthest_shift.get().max(distance(moved, p)));
3229                    return Ok(moved);
3230                }
3231                let healed = meet(p, &surfaces(&fs))?;
3232                let left = furthest(healed, &fs)?;
3233                let shift = distance(healed, p);
3234                furthest_shift.set(furthest_shift.get().max(shift));
3235                if left > scope.uncertainty || 2.0 * shift > length {
3236                    return Err(GeopError::new(format!(
3237                        "the edge {} lies {apart:e} mm off the surfaces of its faces {}{}, and they {} — so it cannot be rebuilt where they meet",
3238                        edges[e].label(),
3239                        names(&fs),
3240                        if moved[start] || moved[end] {
3241                            ", and a vertex of it was moved to where they meet"
3242                        } else {
3243                            ""
3244                        },
3245                        if left > scope.uncertainty {
3246                            format!(
3247                                "do not meet along it to within the file's uncertainty of {:e} mm: near its point {p:?} they come no nearer each other than {left:e} mm",
3248                                scope.uncertainty
3249                            )
3250                        } else {
3251                            format!(
3252                                "meet only {shift:e} mm from its point {p:?}, more than half its length of {length:e} mm"
3253                            )
3254                        },
3255                    )));
3256                }
3257                Ok(healed)
3258            };
3259            // Sampled more densely while the cubic through the samples strays
3260            // between them from where the surfaces meet by more than the
3261            // file's uncertainty.
3262            // The cap ends it: `samples` doubles each step, so it passes
3263            // any cap within as many steps as it has bits.
3264            let rebuilt = refine(
3265                REBUILT_SAMPLES,
3266                usize::BITS as usize,
3267                |samples| -> GeopResult<_> {
3268                    let mut points = vec![vertices[start].origin];
3269                    for k in 1..samples {
3270                        let f = k as f64 / samples as f64;
3271                        points.push(s3(onto(at(f).map_err(ectx)?, f).map_err(ectx)?));
3272                    }
3273                    points.push(vertices[end].origin);
3274                    let rebuilt = NurbCurve3D::interpolate(&points, 3).map_err(|err| {
3275                        err.with_context(format!(
3276                            "rebuilding the edge {} where its faces {} meet",
3277                            edges[e].label(),
3278                            names(&fs)
3279                        ))
3280                    })?;
3281                    if samples >= MOST_REBUILT_SAMPLES || middle.is_none() {
3282                        return Ok(Refine::Done(rebuilt));
3283                    }
3284                    let (r0, r1) = rebuilt.domain();
3285                    let (r0, r1) = (r0.to_f64(), r1.to_f64());
3286                    let mut strays = 0.0f64;
3287                    for k in 0..samples {
3288                        let t = r0 + (r1 - r0) * (k as f64 + 0.5) / samples as f64;
3289                        let p = super::geometry::point_at(&rebuilt, t).map_err(ectx)?;
3290                        strays = strays.max(furthest(p, &fs).map_err(ectx)?);
3291                    }
3292                    if strays <= scope.uncertainty {
3293                        return Ok(Refine::Done(rebuilt));
3294                    }
3295                    Ok(Refine::Finer(samples * 2))
3296                },
3297            )?;
3298            let Refined::Done(rebuilt) = rebuilt else {
3299                return Err(GeopError::new(format!(
3300                    "rebuilding the edge {}: its samples doubled past every cap",
3301                    edges[e].label()
3302                )));
3303            };
3304            edges[e].curve = rebuilt;
3305            edges[e].shape = None;
3306            moved_edges.push((edges[e].label(), furthest_shift.get()));
3307        }
3308    }
3309    let healing = Healing {
3310        vertices: moved_vertices,
3311        edges: moved_edges,
3312        faces: (0..faces.len())
3313            .filter(|&f| rebuild[f])
3314            .map(|f| names(&[f]))
3315            .collect(),
3316        uncertainty: scope.uncertainty,
3317    };
3318    Ok((rebuild, healing))
3319}
3320
3321/// A loop of a face: each coedge's edge and pcurve, in order.
3322type Loop<T> = Vec<(T, NurbCurve2D)>;
3323
3324/// A face's loops: its outer one, and its holes.
3325type FaceLoops<T> = (Loop<T>, Vec<Loop<T>>);
3326
3327/// Seed of the ray casting that tells which outer loop a hole lies in: any
3328/// gives the same answer (see `pcurves_contain`).
3329const SEED: u64 = 0x57E9_0000_0000_0001;
3330
3331/// The loops `loops` of a face on `surface`, each a list of its coedges'
3332/// edges and pcurves, as faces: each loop running counter-clockwise in the
3333/// patch an outer one, each running clockwise a hole of the smallest outer
3334/// loop around it — asked of a point of its first pcurve by ray casting.
3335/// Refused where a loop runs neither way, or a hole lies in none.
3336fn split_by_outer_loops<T: Clone>(
3337    surface: &NurbSurface3D,
3338    loops: Vec<Loop<T>>,
3339) -> GeopResult<Vec<FaceLoops<T>>> {
3340    let areas: Vec<Scalar> = (loops.iter())
3341        .map(|lp| loop_signed_area(lp.iter().map(|(_, pcurve)| pcurve)))
3342        .collect::<GeopResult<_>>()?;
3343    if let Some(k) = areas
3344        .iter()
3345        .position(|a| !a.definitely_greater(Scalar::ZERO) && !a.definitely_less(Scalar::ZERO))
3346    {
3347        return Err(GeopError::new(format!(
3348            "its loop {k} runs neither way round about its normal (signed areas {areas:?})"
3349        )));
3350    }
3351    let mut outers = Vec::new();
3352    let mut holes = Vec::new();
3353    for (lp, area) in loops.into_iter().zip(&areas) {
3354        if area.definitely_greater(Scalar::ZERO) {
3355            outers.push((lp, Vec::new(), *area));
3356        } else {
3357            holes.push(lp);
3358        }
3359    }
3360    if outers.is_empty() {
3361        return Err(GeopError::new(format!(
3362            "none of its loops runs counter-clockwise about its normal: it has no outer loop (signed areas {areas:?})"
3363        )));
3364    }
3365    for hole in holes {
3366        let first = &hole[0].1;
3367        let point = first.evaluate(first.mid_parameter()?)?;
3368        let mut owner: Option<usize> = None;
3369        for (i, (outer, _, area)) in outers.iter().enumerate() {
3370            let pcurves: Vec<&NurbCurve2D> = outer.iter().map(|(_, pcurve)| pcurve).collect();
3371            let inside = pcurves_contain(
3372                surface,
3373                &pcurves,
3374                point,
3375                MAX_NODES,
3376                Scalar::from_f64(MIN_SUBDIVISION_SIZE),
3377                SEED,
3378            )? == PointClassification::Inside;
3379            if inside && owner.is_none_or(|o| area.definitely_less(outers[o].2)) {
3380                owner = Some(i);
3381            }
3382        }
3383        let owner = owner.ok_or_else(|| {
3384            GeopError::new(format!(
3385                "a hole of it lies in none of its outer loops (signed areas {areas:?})"
3386            ))
3387        })?;
3388        outers[owner].1.push(hole);
3389    }
3390    Ok(outers
3391        .into_iter()
3392        .map(|(outer, holes, _)| (outer, holes))
3393        .collect())
3394}
3395
3396/// The most faces at one vertex or edge among which [`heal`] chooses which to
3397/// rebuild: every choice is tried.
3398const MOST_REBUILT_AT_ONCE: usize = 6;
3399
3400/// Whether [`rebuilt_surface`] can rebuild the surface of `face`.
3401fn rebuildable(face: &Face) -> bool {
3402    matches!(face.loops.as_slice(), [lp] if lp.len() >= 4)
3403}
3404
3405/// The surface of `face` rebuilt from its edges, where its own contradicts
3406/// its neighbours' (see [`heal`]): the Coons patch of its loop, cut into
3407/// four sides at the four corners where it turns most sharply — a narrow
3408/// wall's two long edges and its two short ones. Which corners is a free
3409/// choice; the patch passes through every edge whichever it is. Its normal
3410/// is the one the loop winds about, which runs counter-clockwise about the
3411/// face's outward normal. Only for a face of one loop of four edges or
3412/// more.
3413fn rebuilt_surface(face: &Face, edges: &[Edge]) -> GeopResult<NurbSurface3D> {
3414    let [lp] = face.loops.as_slice() else {
3415        return Err(GeopError::new(format!(
3416            "it has {} loops: only a face of one loop is rebuilt from its edges",
3417            face.loops.len()
3418        )));
3419    };
3420    let n = lp.len();
3421    if n < 4 {
3422        return Err(GeopError::new(format!(
3423            "its loop has {n} edges: only one of four or more is rebuilt from its edges, cut into four sides"
3424        )));
3425    }
3426    let curves: Vec<NurbCurve3D> = lp
3427        .iter()
3428        .map(|&(e, forward)| {
3429            if forward {
3430                edges[e].curve.clone()
3431            } else {
3432                edges[e].curve.reverse()
3433            }
3434        })
3435        .collect();
3436    let direction = |c: &NurbCurve3D, at_end: bool| -> GeopResult<P3> {
3437        let (lo, hi) = c.domain();
3438        let t = if at_end { hi } else { lo };
3439        normalize(c.tangent(t)?.to_f64_array())
3440            .ok_or_else(|| GeopError::new("an edge of it has no direction at an end"))
3441    };
3442    // How sharply the loop turns at the end of each coedge.
3443    let mut turns = Vec::with_capacity(n);
3444    for k in 0..n {
3445        let arriving = direction(&curves[k], true)?;
3446        let leaving = direction(&curves[(k + 1) % n], false)?;
3447        turns.push((1.0 - dot(arriving, leaving), k));
3448    }
3449    turns.sort_by(|a, b| b.0.total_cmp(&a.0));
3450    let mut corners: Vec<usize> = turns[..4].iter().map(|&(_, k)| k).collect();
3451    corners.sort();
3452    let mut sides = Vec::with_capacity(4);
3453    for j in 0..4 {
3454        let (from, to) = (corners[j] + 1, corners[(j + 1) % 4]);
3455        let count = (to + n - from) % n + 1;
3456        let chain: Vec<NurbCurve3D> = (0..count).map(|i| curves[(from + i) % n].clone()).collect();
3457        sides.push(NurbCurve3D::join(&chain)?);
3458    }
3459    NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]])
3460}
3461
3462/// The pcurve of `curve` on `surface`, a plane's parallelogram patch
3463/// `P00 + u e1 + v e2` on `[0, 1]²`: the curve in the plane's coordinates,
3464/// exact — its homogeneous control points mapped by the affine map's
3465/// inverse, its weights and knots its own — and pinned at `start` and `end`,
3466/// its ends' foot points, as every pcurve of a loop is (see `fit_loop`).
3467/// Fitted instead, a cubic through samples of a large circle drifts from it
3468/// by more than the kernel's accuracy.
3469fn plane_pcurve(
3470    surface: &NurbSurface3D,
3471    curve: &NurbCurve3D,
3472    start: Vector2,
3473    end: Vector2,
3474) -> GeopResult<NurbCurve2D> {
3475    let corner = |i: usize, j: usize| {
3476        let cp = surface.control_points[i * surface.num_v + j];
3477        Vector3::from_array([cp[0], cp[1], cp[2]])
3478    };
3479    let origin = corner(0, 0);
3480    let (e1, e2) = (corner(1, 0).sub(&origin), corner(0, 1).sub(&origin));
3481    let gram = (e1.prod_dot(&e1), e1.prod_dot(&e2), e2.prod_dot(&e2));
3482    let n = curve.control_points.len();
3483    let mut control_points = Vec::with_capacity(n);
3484    for (k, cp) in curve.control_points.iter().enumerate() {
3485        let w = cp[3];
3486        let pin = match k {
3487            0 => Some(start),
3488            _ if k + 1 == n => Some(end),
3489            _ => None,
3490        };
3491        let (u, v) = match pin {
3492            Some(p) => (p[0].mul(w), p[1].mul(w)),
3493            None => {
3494                let d = Vector3::from_array([cp[0], cp[1], cp[2]]).sub(&origin.prod_scalar(w));
3495                solve_symmetric2(gram, (d.prod_dot(&e1), d.prod_dot(&e2)))?
3496            }
3497        };
3498        control_points.push(Vector3::from_array([u, v, w]));
3499    }
3500    NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
3501}
3502
3503/// The widest a vertex or a control point may be in the kernel (its
3504/// validation's bound): places the file says are one point lying further
3505/// apart than this cannot be one geop vertex.
3506const ACCURACY: f64 = 1e-4;
3507
3508/// How often an edge is measured against its faces and widened: the first
3509/// time by the file's disagreement, after by rounding.
3510const WIDENINGS: usize = 4;
3511
3512/// Samples along an edge's pcurve, at fractions of its domain: those the
3513/// kernel's validation samples at among them.
3514const GAP_SAMPLES: usize = 64;
3515
3516/// Golden-section steps finding the nearest point of an edge to a pcurve's
3517/// sample: from two table steps down to a rounding of the parameter.
3518const NEAREST_ITERATIONS: usize = 80;
3519
3520/// Of those, the last ones, taken on the curve's enclosure rather than its
3521/// midpoints in `f64`.
3522const NEAREST_ON_ENCLOSURE: usize = 12;
3523
3524/// A golden-section search for the least of a unimodal merit on `[a, b]`,
3525/// in plain numbers. Its bracket and the two points inside it carry over
3526/// from one call of [`GoldenSection::narrow`] to the next, so the search
3527/// can go on with a finer merit for its last steps.
3528#[derive(Clone, Copy, Debug)]
3529struct GoldenSection {
3530    a: f64,
3531    b: f64,
3532    x: f64,
3533    y: f64,
3534}
3535
3536impl GoldenSection {
3537    /// The ratio the bracket shrinks by each step, `(√5 - 1) / 2`.
3538    fn ratio() -> f64 {
3539        (5f64.sqrt() - 1.0) / 2.0
3540    }
3541
3542    fn new(a: f64, b: f64) -> Self {
3543        let ratio = Self::ratio();
3544        GoldenSection {
3545            a,
3546            b,
3547            x: b - ratio * (b - a),
3548            y: a + ratio * (b - a),
3549        }
3550    }
3551
3552    /// `steps` steps on `merit`, which is asked at both points inside the
3553    /// bracket first: nothing, for no steps.
3554    fn narrow(
3555        &mut self,
3556        steps: usize,
3557        mut merit: impl FnMut(f64) -> GeopResult<f64>,
3558    ) -> GeopResult<()> {
3559        if steps == 0 {
3560            return Ok(());
3561        }
3562        let ratio = Self::ratio();
3563        let GoldenSection {
3564            mut a,
3565            mut b,
3566            mut x,
3567            mut y,
3568        } = *self;
3569        let (mut fx, mut fy) = (merit(x)?, merit(y)?);
3570        for _ in 0..steps {
3571            if fx <= fy {
3572                b = y;
3573                (y, fy) = (x, fx);
3574                x = b - ratio * (b - a);
3575                fx = merit(x)?;
3576            } else {
3577                a = x;
3578                (x, fx) = (y, fy);
3579                y = a + ratio * (b - a);
3580                fy = merit(y)?;
3581            }
3582        }
3583        *self = GoldenSection { a, b, x, y };
3584        Ok(())
3585    }
3586}
3587
3588/// How far, per coordinate, the points a pcurve puts on its surface lie
3589/// from the edge's curve `curve` — sampled, each against the curve's
3590/// nearest point: how far the point's midpoint, the pcurve's own best
3591/// guess, lies outside the curve's enclosure there.
3592fn edge_gap(
3593    curve: &NurbCurve3D,
3594    surface: &NurbSurface3D,
3595    pcurve: &NurbCurve2D,
3596) -> GeopResult<([f64; 3], Option<Worst>)> {
3597    let (t0, t1) = pcurve.domain();
3598    let (c0, c1) = curve.domain();
3599    let mut gap = [0.0f64; 3];
3600    let mut worst: Option<Worst> = None;
3601    // Each sample's nearest curve point: the nearest of a table of the
3602    // curve's points, polished by Newton.
3603    let (c0f, c1f) = (c0.to_f64(), c1.to_f64());
3604    let midpoints = CurveMidpoints::of(curve);
3605    let rows = 8 * curve.control_points.len() + 32;
3606    let table: Vec<(f64, P3)> = (0..=rows)
3607        .map(|k| {
3608            let t = c0f + (c1f - c0f) * k as f64 / rows as f64;
3609            (t, midpoints.point(t))
3610        })
3611        .collect();
3612    let step = (c1f - c0f) / rows as f64;
3613    for i in 0..=GAP_SAMPLES {
3614        let f = Scalar::from_ratio(i as i64, GAP_SAMPLES as i64)?;
3615        let t = match i {
3616            0 => t0,
3617            GAP_SAMPLES => t1,
3618            _ => Scalar::fraction(t0, t1, f),
3619        };
3620        let uv = pcurve.evaluate(t)?;
3621        // A pcurve too uncertain to place on the surface at all is for the
3622        // model's validation to judge; it says nothing about the edge.
3623        let Ok(point) = surface.evaluate(uv[0], uv[1]) else {
3624            continue;
3625        };
3626        let target = point.to_f64_array();
3627        let &(seed, _) = table
3628            .iter()
3629            .min_by(|a, b| distance(a.1, target).total_cmp(&distance(b.1, target)))
3630            .expect("a table of points");
3631        // Which parameter is a free choice — a nearer point only measures
3632        // a smaller gap — so it is found in `f64`, on the curve's
3633        // midpoints, by golden section between the table's neighbours of
3634        // the nearest. (Newton's refinement gives its window back for a
3635        // point at an end, reached a rounding past it, and the window's
3636        // middle is half a step off; on a widened curve its last step
3637        // inherits the width, and is off along the curve by about that.)
3638        // The midpoints in `f64` narrow the window, which is cheap; the
3639        // enclosure's own midpoints, which the gap is measured against,
3640        // decide its last bits, where the two differ by roundings.
3641        let mut search = GoldenSection::new((seed - step).max(c0f), (seed + step).min(c1f));
3642        search.narrow(NEAREST_ITERATIONS - NEAREST_ON_ENCLOSURE, |t| {
3643            Ok(distance(midpoints.point(t), target))
3644        })?;
3645        // The last steps on the enclosure.
3646        search.narrow(NEAREST_ON_ENCLOSURE, |t| {
3647            Ok(distance(super::geometry::point_at(curve, t)?, target))
3648        })?;
3649        let GoldenSection { a, b, .. } = search;
3650        // The ends are the domain's own. The pcurve's own ends are pinned
3651        // where the edge ends, at its vertices: they are measured against
3652        // the curve's nearer end, never a point next to it — which can be
3653        // as near to a rounding, its enclosure holding the point where the
3654        // end's does not, while the validation's clipping, tight at an
3655        // end, holds to the end's. Which is nearest is told on the curve's
3656        // enclosure, which the gap is measured against.
3657        let candidates = [
3658            (c0f, c0),
3659            (c1f, c1),
3660            ((a + b) / 2.0, Scalar::from_f64((a + b) / 2.0)),
3661        ];
3662        let exact_gap_at = |t: f64| -> GeopResult<f64> {
3663            Ok(distance(super::geometry::point_at(curve, t)?, target))
3664        };
3665        let pinned = i == 0 || i == GAP_SAMPLES;
3666        let choices = if pinned {
3667            &candidates[..2]
3668        } else {
3669            &candidates[..]
3670        };
3671        // The first of the nearest: an end where one is as near.
3672        let mut nearest: Option<(Scalar, f64)> = None;
3673        for &(t, exact) in choices {
3674            let d = exact_gap_at(t)?;
3675            if nearest.is_none_or(|(_, best)| d < best) {
3676                nearest = Some((exact, d));
3677            }
3678        }
3679        let (s, _) = nearest.expect("candidates");
3680        let near = curve.evaluate(s)?;
3681        // How far the point lies outside the curve's enclosure: what the
3682        // curve must widen by to reach it.
3683        for c in 0..3 {
3684            // Up to the point's midpoint, not merely its enclosure: an edge
3685            // that only touches its pcurve's points, by a rounding, is
3686            // not found to pass through them by the validation's clipping.
3687            let mid = point[c].midpoint();
3688            let apart = (mid.sub(near[c].upper()).upper().to_f64())
3689                .max(near[c].lower().sub(mid).upper().to_f64())
3690                .max(0.0);
3691            gap[c] = gap[c].max(apart);
3692            if worst.is_none_or(|w: Worst| apart > w.apart) {
3693                worst = Some(Worst {
3694                    apart,
3695                    at: i as f64 / GAP_SAMPLES as f64,
3696                    point: point.to_f64_array(),
3697                    near: near.to_f64_array(),
3698                });
3699            }
3700        }
3701    }
3702    Ok((gap, worst))
3703}
3704
3705/// Where along a pcurve [`edge_gap`] found it furthest from its edge: the
3706/// fraction of its domain, the point it puts on the surface there, and the
3707/// edge's nearest point.
3708#[derive(Clone, Copy, Debug)]
3709struct Worst {
3710    apart: f64,
3711    at: f64,
3712    point: P3,
3713    near: P3,
3714}
3715
3716/// How far the points of `curve` lie from `surface`, sampled: each
3717/// projected onto it, from where `pcurve`, the curve's on the surface,
3718/// is at the same fraction of its domain.
3719fn off_surface(
3720    curve: &NurbCurve3D,
3721    surface: &NurbSurface3D,
3722    pcurve: &NurbCurve2D,
3723) -> GeopResult<f64> {
3724    let (c0, c1) = curve.domain();
3725    let (t0, t1) = pcurve.domain();
3726    let mut most = 0.0f64;
3727    for i in 0..=GAP_SAMPLES {
3728        let f = i as f64 / GAP_SAMPLES as f64;
3729        let p = curve.evaluate(Scalar::from_f64(
3730            c0.to_f64() + (c1.to_f64() - c0.to_f64()) * f,
3731        ))?;
3732        let seed = pcurve.evaluate(Scalar::from_f64(
3733            t0.to_f64() + (t1.to_f64() - t0.to_f64()) * f,
3734        ))?;
3735        let (u, v) = surface.project(p, seed[0].sharpen(), seed[1].sharpen(), 30)?;
3736        let foot = surface.evaluate(u, v)?;
3737        most = most.max(distance(foot.to_f64_array(), p.to_f64_array()));
3738    }
3739    Ok(most)
3740}
3741
3742/// `curve` with every control point widened by `gap` in each coordinate:
3743/// a curve whose every point may be up to `gap` off where it was.
3744fn widened(curve: &NurbCurve3D, gap: [f64; 3]) -> GeopResult<NurbCurve3D> {
3745    if gap == [0.0; 3] {
3746        return Ok(curve.clone());
3747    }
3748    let control_points = curve
3749        .control_points
3750        .iter()
3751        .map(|cp| {
3752            let w = cp[3];
3753            let mut out = *cp;
3754            for c in 0..3 {
3755                let spread = Scalar::from_f64(-gap[c]).union(Scalar::from_f64(gap[c]));
3756                out[c] = cp[c].add(w.mul(spread));
3757            }
3758            out
3759        })
3760        .collect();
3761    NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
3762}