Skip to main content

ogeom_mesh/
triangulate.rs

1//! Triangulating a face.
2//!
3//! The second half of tessellation. A face is a trimmed region of a surface, so
4//! the triangulation is built in the surface's `(u, v)` parameter space (where
5//! the region is an ordinary polygon with holes) and then lifted back into
6//! space by evaluating the surface at each vertex.
7//!
8//! # Why parameter space
9//!
10//! Triangulating in 3D would mean deciding which side of a curved boundary a
11//! point falls on, in space, which is the point-in-solid problem. In parameter
12//! space the boundary is a closed 2D polygon and the question is a winding
13//! count. The surface does the rest.
14//!
15//! The cost is that parameter space is distorted: equal steps in `(u, v)` cover
16//! very different distances near a sphere's pole than near its equator. So the
17//! interior points are chosen by measuring deflection *in space* and the
18//! triangulation is done in parameter space: measuring where the answer
19//! matters, connecting where it is easy.
20//!
21//! # Watertightness
22//!
23//! A face's boundary points come from discretizing the *edge's* 3D curve and
24//! evaluating the pcurve at those same parameters. Two faces sharing an edge
25//! therefore place their boundary vertices at identical spatial positions, and
26//! the join has no gap. Discretizing each face's pcurve independently would
27//! give each face its own idea of where the edge runs, and the seams would show.
28
29use ogeom_core::clock::Instant;
30use ogeom_core::{Exact, OgeomResult, Predicates, Tolerances, ogeom_bail};
31use ogeom_geom::Curve3d as _;
32use ogeom_geom::{Curve2d, Surface, SurfaceGeometry};
33use ogeom_math::{Direction, Point, Point2, Vector};
34use ogeom_topo::{EdgeRepr, Model, NodeData, Orientation, Shape, ShapeType, Triangulation};
35use spade::handles::FixedVertexHandle;
36use spade::{
37    ConstrainedDelaunayTriangulation, Point2 as SpadePoint, Triangulation as _, mitigate_underflow,
38};
39
40use crate::discretize::{Deflection, discretize};
41
42/// Triangulate one face.
43///
44/// # Errors
45///
46/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if `face` is not a
47/// face or its geometry is missing;
48/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle fails to
49/// resolve; [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if the boundary
50/// cannot be triangulated.
51pub fn triangulate_face(
52    model: &Model,
53    face: &Shape,
54    deflection: Deflection,
55    tol: Tolerances,
56) -> OgeomResult<Triangulation> {
57    // One pass at the caller's chord, and a second only where the first
58    // came back short, the same shape as the whole-shape path, so a caller
59    // meshing face by face pays for one triangulation per face, not two.
60    let nothing = EdgeChords::new();
61    let (mesh, verdict) = triangulate_reporting(model, face, deflection, Some(&nothing), tol)?;
62    if verdict != Verdict::Short {
63        return Ok(mesh);
64    }
65    triangulate_with(model, face, deflection, None, None, tol)
66}
67
68/// As [`triangulate_face`], taking the face from `kept` where it drew this
69/// face at this deflection before, and keeping what it draws there.
70///
71/// # Errors
72///
73/// As [`triangulate_face`].
74pub fn triangulate_face_kept(
75    model: &Model,
76    face: &Shape,
77    deflection: Deflection,
78    kept: &FaceMeshCache,
79    tol: Tolerances,
80) -> OgeomResult<Triangulation> {
81    let nothing = EdgeChords::new();
82    let (mesh, verdict) = reporting_kept(
83        model,
84        face,
85        deflection,
86        Some(&nothing),
87        None,
88        Some(kept),
89        Pass::Agreeing,
90        tol,
91    )?;
92    if verdict != Verdict::Short {
93        return Ok(mesh);
94    }
95    triangulate_with(model, face, deflection, None, Some(kept), tol)
96}
97
98/// Face meshes kept by what each face is, for a caller that meshes a run of
99/// models in which most faces recur unchanged, as a construction retried
100/// after a few of its faces changed does.
101///
102/// A face is known by everything its mesh reads: its content as
103/// [`ogeom_topo::face_content`] writes it out, then the deflection, the
104/// tolerances, and the chord each of its edges was asked to be drawn to.
105/// A face matches a kept one only where every one of these is the same to
106/// the bit, and the kept mesh is the one drawing it again gives. A face
107/// whose content cannot be read is drawn and not kept.
108#[derive(Debug, Default)]
109pub struct FaceMeshCache {
110    held: std::sync::Mutex<ogeom_core::FastMap<Vec<u8>, (Triangulation, Verdict)>>,
111    /// The chords a face that came up short asked its edges to be drawn
112    /// at, under the same key as its mesh: each edge's in the order the
113    /// face's wires walk them, `None` for one it did not name.
114    chords: std::sync::Mutex<ogeom_core::FastMap<Vec<u8>, Vec<Option<f64>>>>,
115}
116
117impl FaceMeshCache {
118    /// An empty cache.
119    #[must_use]
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    fn get(&self, key: &[u8]) -> Option<(Triangulation, Verdict)> {
125        self.held
126            .lock()
127            .unwrap_or_else(std::sync::PoisonError::into_inner)
128            .get(key)
129            .cloned()
130    }
131
132    fn put(&self, key: Vec<u8>, found: (Triangulation, Verdict)) {
133        self.held
134            .lock()
135            .unwrap_or_else(std::sync::PoisonError::into_inner)
136            .insert(key, found);
137    }
138
139    fn chords(&self, key: &[u8]) -> Option<Vec<Option<f64>>> {
140        self.chords
141            .lock()
142            .unwrap_or_else(std::sync::PoisonError::into_inner)
143            .get(key)
144            .cloned()
145    }
146
147    fn keep_chords(&self, key: Vec<u8>, found: Vec<Option<f64>>) {
148        self.chords
149            .lock()
150            .unwrap_or_else(std::sync::PoisonError::into_inner)
151            .insert(key, found);
152    }
153}
154
155/// The key a face's mesh is kept under: see [`FaceMeshCache`]. `finer` is
156/// the shared chord map the face is drawn to, or `None` where it works out
157/// its own from its content alone; `pass` is whether the face is drawn
158/// again if it comes up short.
159fn mesh_key(
160    model: &Model,
161    face: &Shape,
162    deflection: Deflection,
163    finer: Option<&EdgeChords>,
164    pass: Pass,
165    tol: Tolerances,
166) -> Option<Vec<u8>> {
167    let mut key = ogeom_topo::face_content(model, face)?;
168    key.extend_from_slice(format!("{deflection:?}{tol:?}{pass:?}").as_bytes());
169    match finer {
170        None => key.push(b'o'),
171        Some(finer) => {
172            key.push(b's');
173            for wire in model.ordered_children_of(face).ok()? {
174                for edge in model.ordered_children_of(&wire).ok()? {
175                    match finer.get(&edge.node().index()) {
176                        Some(chord) => {
177                            key.push(1);
178                            key.extend_from_slice(&chord.to_bits().to_le_bytes());
179                        }
180                        None => key.push(0),
181                    }
182                }
183            }
184        }
185    }
186    Some(key)
187}
188
189/// [`triangulate_reporting_from`], through `kept` where there is one.
190#[allow(
191    clippy::too_many_arguments,
192    reason = "the pass's settings, each passed through once"
193)]
194fn reporting_kept(
195    model: &Model,
196    face: &Shape,
197    deflection: Deflection,
198    finer: Option<&EdgeChords>,
199    prepared: Option<Trimming>,
200    kept: Option<&FaceMeshCache>,
201    pass: Pass,
202    tol: Tolerances,
203) -> OgeomResult<(Triangulation, Verdict)> {
204    let Some(kept) = kept else {
205        return triangulate_reporting_from(model, face, deflection, finer, prepared, pass, tol);
206    };
207    let key = mesh_key(model, face, deflection, finer, pass, tol);
208    if let Some(key) = &key
209        && let Some(found) = kept.get(key)
210    {
211        return Ok(found);
212    }
213    let found = triangulate_reporting_from(model, face, deflection, finer, prepared, pass, tol)?;
214    if let Some(key) = key {
215        kept.put(key, found.clone());
216    }
217    Ok(found)
218}
219
220/// What one pass over a face found.
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222enum Verdict {
223    /// The boundary enclosed a region and it was drawn.
224    Whole,
225    /// The boundary crossed itself: what came back is fragments, or nothing.
226    /// The edges want drawing finer.
227    Short,
228}
229
230/// Whether a pass over a face is the last it gets.
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232enum Pass {
233    /// Drawn again at finer chords if it comes up short: a boundary drawn
234    /// coarsely enough to cross itself is drawn finer, not taken apart.
235    Agreeing,
236    /// Not drawn again: a ring still crossing itself sheds its tails (see
237    /// [`crate::tails`]) if that leaves it whole.
238    Last,
239}
240
241/// One face, with the finer edge chords the whole shape agreed on.
242///
243/// `None` means this face is being meshed on its own and may work out its
244/// own: alone it has no neighbour to disagree with.
245fn triangulate_with(
246    model: &Model,
247    face: &Shape,
248    deflection: Deflection,
249    finer: Option<&EdgeChords>,
250    kept: Option<&FaceMeshCache>,
251    tol: Tolerances,
252) -> OgeomResult<Triangulation> {
253    let (mesh, verdict) =
254        reporting_kept(model, face, deflection, finer, None, kept, Pass::Last, tol)?;
255    if verdict == Verdict::Short && mesh.triangles.is_empty() {
256        ogeom_bail!(
257            NotDone,
258            "the face's boundary enclosed no triangulable region"
259        );
260    }
261    Ok(mesh)
262}
263
264/// The surface's unit normal at `(u, v)`, or where the surface is
265/// degenerate there (a cone's apex, a sphere's pole, a patch's collapsed
266/// corner), the normal a step inside along the vertex's own column.
267///
268/// A degenerate point has no normal of its own, and `None` for it would
269/// shade the vertex black and drag every normal it is welded with towards
270/// nothing. It has a *limit* normal along any line approaching
271/// it: the apex of a cone seen up one ruling is that ruling's normal, and
272/// a mesh vertex at the apex carries the `u` of the ruling it closes. The
273/// step is a millionth of the domain towards its middle, first along
274/// `v`, then `u`, then both; `None` only where all three are degenerate.
275fn limit_normal(surface: &SurfaceGeometry, u: f64, v: f64, tol: Tolerances) -> Option<Vector> {
276    if let Ok(n) = surface.normal_at(u, v, tol) {
277        return Some(n.vector());
278    }
279    let ((ua, ub), (va, vb)) = surface.domain();
280    let su = if u < f64::midpoint(ua, ub) { 1.0 } else { -1.0 };
281    let sv = if v < f64::midpoint(va, vb) { 1.0 } else { -1.0 };
282    // Widening steps: a patch whose corner collapses with its tangent
283    // (the control rows drawn together and the next row too) is degenerate
284    // to first order for a stretch, and a millionth of the domain is still
285    // inside it; on a sphere a millimetre across the tangents a millionth
286    // in from the pole cross to less than a direction resolves. A
287    // hundredth of the domain is past both, and on a patch that small the
288    // normal there is the corner's for every purpose.
289    for scale in [1e-6, 1e-4, 1e-2] {
290        let du = (ub - ua).abs().max(f64::EPSILON) * scale * su;
291        let dv = (vb - va).abs().max(f64::EPSILON) * scale * sv;
292        let found = [(u, v + dv), (u + du, v), (u + du, v + dv)]
293            .into_iter()
294            .find_map(|(nu, nv)| surface.normal_at(nu, nv, tol).ok().map(|n| n.vector()));
295        if found.is_some() {
296            return found;
297        }
298    }
299    None
300}
301
302/// One face, and whether the rings it was built from crossed themselves.
303///
304/// The flag is how the shape-wide pass learns which faces need their edges
305/// drawn finer without building every face's rings twice: the rings are
306/// already in hand here, and the sweep over them is the only extra cost a
307/// face that does not cross ever pays.
308fn triangulate_reporting(
309    model: &Model,
310    face: &Shape,
311    deflection: Deflection,
312    finer: Option<&EdgeChords>,
313    tol: Tolerances,
314) -> OgeomResult<(Triangulation, Verdict)> {
315    triangulate_reporting_from(model, face, deflection, finer, None, Pass::Agreeing, tol)
316}
317
318/// A face's rings walked ahead of drawing, and the chord its edges want
319/// if it is narrower than a few of the caller's.
320type Walked = (Trimming, Option<f64>);
321
322/// How a drum or a cone is drawn between its rims, by what its rims say
323/// of the face. Its two rims alone where every edge keeps to the face
324/// within a hundredth of the chord and meets its neighbour across a
325/// crease. A row of the face's own through its middle where an edge
326/// strays further, as a fitted intersection in the file does, or the face
327/// runs on smoothly into another curved one: drawn from its rims alone, a
328/// face shows the rims' slop instead of itself, and at a smooth seam no
329/// crease in the mesh says where it ends. Every fourth column of that row
330/// where it runs on smoothly into a flat face only.
331fn rim_holds(
332    model: &Model,
333    face: &Shape,
334    deflection: Deflection,
335    tol: Tolerances,
336) -> OgeomResult<Rows> {
337    use ogeom_geom::{Curve2d as _, Surface as _};
338    let mut rows = Rows::Ends;
339    for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
340        let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
341            continue;
342        };
343        if data.tolerance.get() > deflection.chord * RIM_SLOP {
344            return Ok(Rows::Full);
345        }
346        // The two faces' normals at the edge's middle, each read through
347        // its own pcurve: whether the face runs on smoothly across it, and
348        // into a flat face or a curved one.
349        let mut normals = Vec::new();
350        for repr in &data.representations {
351            if let ogeom_topo::EdgeRepr::PCurve {
352                curve,
353                surface,
354                range,
355                ..
356            } = repr
357                && let (Some(c), Some(s)) = (
358                    model.geometry().pcurve(*curve),
359                    model.geometry().surface(*surface),
360                )
361                && let Ok(uv) = c.point_at(f64::midpoint(range.0, range.1), tol)
362                && let Ok(n) = s.normal_at(uv.x, uv.y, tol)
363            {
364                normals.push((
365                    *surface,
366                    n,
367                    matches!(s.kind(), ogeom_geom::SurfaceKind::Plane),
368                ));
369            }
370        }
371        for (i, (sa, na, pa)) in normals.iter().enumerate() {
372            for (sb, nb, pb) in &normals[i + 1..] {
373                if sa != sb && na.dot(*nb).abs() > SMOOTH_RIM {
374                    if *pa || *pb {
375                        rows = Rows::Sparse;
376                    } else {
377                        return Ok(Rows::Full);
378                    }
379                }
380            }
381        }
382    }
383    Ok(rows)
384}
385
386/// The widest tolerance of a face's edges and vertices: how far apart two
387/// stretches of its boundary may stand and be one.
388fn widest_tolerance(model: &Model, face: &Shape) -> OgeomResult<f64> {
389    let mut widest = 0.0_f64;
390    for kind in [ShapeType::Edge, ShapeType::Vertex] {
391        for shape in ogeom_topo::explore_unique(model, face, kind)? {
392            widest = widest.max(model.tolerance_of(&shape)?.map_or(0.0, |t| t.get()));
393        }
394    }
395    Ok(widest)
396}
397
398/// `planar` with each tail's strip added, wound to meet the region across
399/// the chord at the tail's mouth; `None` where the region does not hold
400/// that chord.
401fn with_strips(mut planar: PlanarMesh, strips: &[crate::tails::Strip]) -> Option<PlanarMesh> {
402    let key = |p: Point2| (p.x.to_bits(), p.y.to_bits());
403    let mut index: ogeom_core::FastMap<(u64, u64), u32> = ogeom_core::FastMap::default();
404    for (i, &(u, v)) in planar.parameters.iter().enumerate() {
405        index.insert(key(Point2::new(u, v)), u32::try_from(i).ok()?);
406    }
407    let mut sides: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
408    for t in &planar.triangles {
409        for k in 0..3 {
410            sides.insert((t[k], t[(k + 1) % 3]));
411        }
412    }
413    for strip in strips {
414        let (from, to) = (
415            *index.get(&key(strip.mouth.0))?,
416            *index.get(&key(strip.mouth.1))?,
417        );
418        // The strip walks its mouth from `to` back to `from`; the region
419        // must walk it the other way.
420        let turned = if sides.contains(&(from, to)) {
421            false
422        } else if sides.contains(&(to, from)) {
423            true
424        } else {
425            return None;
426        };
427        for triangle in &strip.triangles {
428            let mut corners = [0_u32; 3];
429            for (corner, p) in corners.iter_mut().zip(triangle) {
430                *corner = match index.get(&key(*p)) {
431                    Some(&i) => i,
432                    None => {
433                        let i = u32::try_from(planar.parameters.len()).ok()?;
434                        planar.parameters.push((p.x, p.y));
435                        index.insert(key(*p), i);
436                        i
437                    }
438                };
439            }
440            planar.triangles.push(if turned {
441                [corners[0], corners[2], corners[1]]
442            } else {
443                corners
444            });
445        }
446    }
447    Some(planar)
448}
449
450/// How a drum or a cone is drawn between its rims.
451#[derive(Clone, Copy, PartialEq, Eq, Debug)]
452enum Rows {
453    /// A row through its middle, a point on every column.
454    Full,
455    /// That row with a point on every fourth column.
456    Sparse,
457    /// Its two rims alone.
458    Ends,
459}
460
461/// The cosine of the turn under which two faces meet smoothly at an edge:
462/// thirty degrees, the converter's crease.
463const SMOOTH_RIM: f64 = 0.866_025_403_784_438_6;
464
465/// How far an edge may stray from its face, against the chord, and the
466/// face still be drawn from its rims alone: a hundredth.
467const RIM_SLOP: f64 = 1e-2;
468
469/// The deflection a face is drawn to in its own frame: the caller's chord
470/// is a distance where the face stands, and a placement that scales the
471/// face scales every chord drawn before it with it.
472fn in_own_frame(model: &Model, face: &Shape, deflection: Deflection) -> Deflection {
473    let scale = face
474        .transform(model.datums())
475        .map_or(1.0, |t| t.scale_factor().abs());
476    if scale > 0.0 && scale.is_finite() && (scale - 1.0).abs() > f64::EPSILON {
477        Deflection {
478            chord: deflection.chord / scale,
479            ..deflection
480        }
481    } else {
482        deflection
483    }
484}
485
486/// [`triangulate_reporting`] with the rings already walked, where the
487/// caller has them and none of the face's edges were told to draw finer
488/// since; the rings depend on nothing else.
489fn triangulate_reporting_from(
490    model: &Model,
491    face: &Shape,
492    deflection: Deflection,
493    finer: Option<&EdgeChords>,
494    prepared: Option<Trimming>,
495    pass: Pass,
496    tol: Tolerances,
497) -> OgeomResult<(Triangulation, Verdict)> {
498    deflection.validate()?;
499    if model.kind_of(face)? != ShapeType::Face {
500        ogeom_bail!(Construction, "expected a face");
501    }
502    let deflection = in_own_frame(model, face, deflection);
503    let Some(node) = model.node(face) else {
504        ogeom_bail!(Dangling, "face is not in this model");
505    };
506    let NodeData::Face(data) = node.data() else {
507        ogeom_bail!(Construction, "face node holds no face data");
508    };
509    let Some(surface) = model.geometry().surface(data.surface) else {
510        ogeom_bail!(Dangling, "face refers to a surface not in this model");
511    };
512    let placement = face.transform(model.datums())?;
513
514    let own;
515    let finer = match finer {
516        Some(shared) => shared,
517        None => {
518            own = face_chords(
519                model,
520                face,
521                data.surface,
522                surface,
523                deflection,
524                &EdgeChords::new(),
525                tol,
526            )?;
527            &own
528        }
529    };
530    let phase = Instant::now();
531    let Trimming {
532        rings: uv,
533        anchors,
534        met,
535        walked,
536    } = match prepared {
537        Some(trim) => trim,
538        None => trimming_rings(
539            model,
540            face,
541            data.surface,
542            surface,
543            deflection,
544            finer,
545            false,
546            tol,
547        )?,
548    };
549    let rings_ms = phase.elapsed().as_secs_f64() * 1e3;
550    if uv.is_empty() {
551        // Bounded, and bounding next to nothing: a sliver whose sides are
552        // one line in the chart. It is drawn as a fan across each ring its
553        // edges walked, at the points its neighbours share along those
554        // edges, so the whole mesh still closes over it.
555        let reversed = face.orientation() == Orientation::Reversed;
556        return Ok((
557            sliver_fan(&walked, surface, &placement, reversed, tol),
558            Verdict::Whole,
559        ));
560    }
561    let phase = Instant::now();
562    let rows = rim_holds(model, face, deflection, tol)?;
563    let mut planar = triangulate_region(&uv, surface, deflection, rows, tol)?;
564    // A ring that crosses itself where it runs out and back along its own
565    // way bounds nothing over that stretch: the tail comes off and is drawn
566    // as a strip at its edges' points, where the rest triangulates whole.
567    let mut drawn = None;
568    if planar.crossed
569        && pass == Pass::Last
570        && let Some(peeled) = crate::tails::peel(
571            &uv,
572            surface,
573            &placement,
574            widest_tolerance(model, face)?,
575            tol,
576        )?
577    {
578        let again = triangulate_region(&peeled.rings, surface, deflection, rows, tol)?;
579        if !again.crossed
580            && let Some(joined) = with_strips(again, &peeled.strips)
581        {
582            planar = joined;
583            drawn = Some(peeled.rings);
584        }
585    }
586    let drawn = drawn.as_ref().unwrap_or(&uv);
587    let region_ms = phase.elapsed().as_secs_f64() * 1e3;
588    let phase = Instant::now();
589
590    // Boundary-only triangulation checks the exact count before interior
591    // points can hide missing triangles. Its minimum includes shared junctions
592    // between touching lobes, which reduce the simple polygon's count.
593    // Filtering degenerate triangles must still leave that minimum.
594    //
595    // And a face narrower than a few chords is short too, whatever its
596    // count: drawn at the caller's chord its boundary sags by more than
597    // the face is wide, and every triangle across the width stands off the
598    // surface by that sag. Its edges want a chord under the width, which
599    // is [`face_chords`]' first move; it is asked here so the whole-shape
600    // pass tells the neighbours to draw those edges finer too.
601    let boundary: usize = drawn.iter().map(Vec::len).sum();
602    let narrow = narrow_chord(surface, drawn, deflection, tol)
603        .is_some_and(|want| !edges_already_at(model, face, finer, want));
604    let crossed = narrow || planar.crossed || planar.triangles.len() < planar.boundary_triangles;
605    if *MESH_DEBUG_REFINE {
606        eprintln!(
607            "SHORT {} triangles against {boundary} boundary points in {} rings: crossed {crossed} (boundary pass {})",
608            planar.triangles.len(),
609            drawn.len(),
610            planar.crossed
611        );
612    }
613
614    // Boundary vertices take their positions from their edges' own curves
615    // (the shared authority), keyed by their exact parameter-space bits.
616    let mut anchored: ogeom_core::FastMap<(u64, u64), Point> = ogeom_core::FastMap::default();
617    for (ring, ring_anchor) in uv.iter().zip(&anchors) {
618        for (p, a) in ring.iter().zip(ring_anchor) {
619            if let Some(point) = a {
620                anchored.insert((p.x.to_bits(), p.y.to_bits()), *point);
621            }
622        }
623    }
624
625    // Lift into space. The normal follows the face's orientation, not the
626    // surface's: a reversed face presents the other side, and a renderer or a
627    // volume computation that ignored that would have the solid inside out.
628    // A reflecting placement carries the surface's normal to the image's
629    // as it is, but turns the chart over: triangles wound counter-clockwise
630    // in the chart wind the other way in space, so the winding turns once
631    // more and the normal does not.
632    let reversed = face.orientation() == Orientation::Reversed;
633    let flip = reversed != !face.location().preserves_handedness(model.datums())?;
634    let mut mesh = Triangulation::new();
635    mesh.deflection_met = met;
636    let mut hits = 0_usize;
637    for (u, v) in planar.parameters {
638        // Anchors are already in world coordinates (their edges' own
639        // placements applied), where surface lifts still need the face's.
640        let point = match anchored.get(&(u.to_bits(), v.to_bits())) {
641            Some(anchor) => {
642                hits += 1;
643                *anchor
644            }
645            None => placement.apply(surface.point_at(u, v, tol)?),
646        };
647        let normal =
648            limit_normal(surface, u, v, tol).map_or(Vector::ZERO, |n| placement.apply_vector(n));
649        mesh.positions.push(point);
650        mesh.normals.push(if reversed { -normal } else { normal });
651        mesh.parameters.push((u, v));
652    }
653    if *MESH_DEBUG_REFINE && (rings_ms + region_ms) > 50.0 {
654        eprintln!(
655            "PHASE rings {rings_ms:.0}ms region {region_ms:.0}ms lift {:.0}ms  ({} ring points, {} tris)",
656            phase.elapsed().as_secs_f64() * 1e3,
657            uv.iter().map(Vec::len).sum::<usize>(),
658            planar.triangles.len()
659        );
660    }
661    if *MESH_DEBUG {
662        eprintln!(
663            "DBG anchors map={} hits={} verts={}",
664            anchored.len(),
665            hits,
666            mesh.positions.len()
667        );
668    }
669    mesh.triangles = planar
670        .triangles
671        .into_iter()
672        .map(|t| if flip { [t[0], t[2], t[1]] } else { t })
673        .collect();
674    split_flat_triangles(&mut mesh, tol);
675    // The chord is met where the mesh says it is. The grid holds each row
676    // and column to it, and a cell's middle can stand off by both sides'
677    // sags together: each triangle's middle, lifted onto the surface, is
678    // measured against the triangle's plane, where the face stands, and
679    // against the triangle's own middle along the surface's normal there.
680    // The plane alone is blind to a sliver laid along a line the surface
681    // bends under: its plane holds the curve its corners lie on, and its
682    // middle stands inside the bend by most of the bend's sag.
683    if mesh.deflection_met && !matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
684        let chord = deflection.chord * placement.scale_factor().abs();
685        for t in &mesh.triangles {
686            let [a, b, c] = t.map(|i| i as usize);
687            let (pa, pb, pc) = (mesh.positions[a], mesh.positions[b], mesh.positions[c]);
688            let Ok(normal) = (pb - pa).cross(pc - pa).normalized(tol) else {
689                continue;
690            };
691            let (ua, ub, uc) = (mesh.parameters[a], mesh.parameters[b], mesh.parameters[c]);
692            let middle = ((ua.0 + ub.0 + uc.0) / 3.0, (ua.1 + ub.1 + uc.1) / 3.0);
693            let Ok(on) = surface.point_at(middle.0, middle.1, tol) else {
694                continue;
695            };
696            let on = placement.apply(on);
697            let mut off = (on - pa).dot(normal).abs();
698            if let Ok(across) = surface.normal_at(middle.0, middle.1, tol)
699                && let Ok(across) = placement.apply_vector(across.vector()).normalized(tol)
700            {
701                let centre =
702                    Point::from_vector((pa.to_vector() + pb.to_vector() + pc.to_vector()) / 3.0);
703                off = off.max((on - centre).dot(across).abs());
704            }
705            if off > chord + tol.confusion() {
706                mesh.deflection_met = false;
707                break;
708            }
709        }
710    }
711    Ok((
712        mesh,
713        if crossed {
714            Verdict::Short
715        } else {
716            Verdict::Whole
717        },
718    ))
719}
720
721/// Triangulate every face below a shape, welded into one mesh.
722///
723/// # Errors
724///
725/// As [`triangulate_face`].
726pub fn triangulate(
727    model: &Model,
728    shape: &Shape,
729    deflection: Deflection,
730    tol: Tolerances,
731) -> OgeomResult<Triangulation> {
732    Ok(triangulate_with_chords(model, shape, deflection, tol)?.0)
733}
734
735/// As [`triangulate`], with the edge chords the faces agreed on: what
736/// [`edge_chords_for`] answers for the same shape and deflection, without
737/// drawing every face a second time to find it.
738///
739/// # Errors
740///
741/// As [`triangulate_face`].
742pub fn triangulate_with_chords(
743    model: &Model,
744    shape: &Shape,
745    deflection: Deflection,
746    tol: Tolerances,
747) -> OgeomResult<(Triangulation, EdgeChords)> {
748    let (mesh, chords, _) = triangulate_with_face_meshes(model, shape, deflection, None, tol)?;
749    Ok((mesh, chords))
750}
751
752/// As [`triangulate_with_chords`], with every face's own mesh beside the
753/// welded whole: each face below `shape` in exploration order, and what
754/// [`triangulate_face_with`] answers for it with the returned chords. The
755/// whole is welded from those same meshes, so they are drawn once. A face
756/// `kept` has drawn before to the same chords is taken from it.
757///
758/// # Errors
759///
760/// As [`triangulate_face`].
761pub fn triangulate_with_face_meshes(
762    model: &Model,
763    shape: &Shape,
764    deflection: Deflection,
765    kept: Option<&FaceMeshCache>,
766    tol: Tolerances,
767) -> OgeomResult<(Triangulation, EdgeChords, Vec<FaceMesh>)> {
768    // Faces in two phases, as `tessellate` does: each face is meshed from a
769    // model nothing is writing to, in parallel; the pieces are then appended
770    // sequentially in face order. The split is what keeps the answer
771    // bit-identical at any thread count: scheduling decides only who does
772    // which face, never where its triangles land.
773    let (faces, chords) = face_meshes_with_chords(model, shape, deflection, kept, tol)?;
774
775    let mut mesh = Triangulation::new();
776    let mut pieces: Vec<(usize, usize)> = Vec::with_capacity(faces.len());
777    for (_, piece) in &faces {
778        let piece = piece.as_ref().map_err(Clone::clone)?;
779        let t0 = mesh.triangles.len();
780        mesh.append(piece);
781        pieces.push((t0, mesh.triangles.len()));
782    }
783    orient_pieces(&mut mesh, &pieces);
784    let mesh = mesh.welded(tol);
785
786    // A second, border-only pass at the tolerance the model itself recorded:
787    // imported edges carry the file's slop in their widened tolerances, and
788    // two faces lifting the same edge through disagreeing geometry land that
789    // far apart. Interior edges are already manifold and are not touched.
790    let mut reach = 0.0_f64;
791    for kind in [ShapeType::Edge, ShapeType::Vertex] {
792        for shape in ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(kind))? {
793            let recorded = model.node(&shape).map_or(0.0, |n| match n.data() {
794                NodeData::Edge(d) => d.tolerance.get(),
795                NodeData::Vertex(d) => d.tolerance.get(),
796                _ => 0.0,
797            });
798            reach = reach.max(recorded);
799        }
800    }
801    // Floored at a tenth of the chord the caller asked for: a border gap
802    // smaller than that is below the resolution of the mesh they accepted,
803    // whether or not the model recorded the slop that caused it.
804    let reach = reach.max(deflection.chord * 0.1);
805    let mesh = if reach > tol.confusion() {
806        let reach = reach + tol.confusion();
807        mesh.border_welded(reach).border_stitched(reach)
808    } else {
809        mesh
810    };
811    // What is left open narrower than the chord asked for is two faces
812    // sampling a shared corner differently, below the mesh's own
813    // resolution, and is sealed; a wider opening is the shape's.
814    let faces = faces
815        .into_iter()
816        .map(|(face, piece)| {
817            let piece =
818                piece.and_then(|piece| own_answer(model, &face, piece, deflection, &chords, tol));
819            (face, piece)
820        })
821        .collect();
822    Ok((mesh.sealed(deflection.chord), chords, faces))
823}
824
825/// A face and its mesh, or why it would not draw.
826pub type FaceMesh = (Shape, OgeomResult<Triangulation>);
827
828/// Every face below `shape` in exploration order, each with what
829/// [`triangulate_face_with`] answers for it with the chords the faces agree
830/// on (what [`edge_chords_for`] answers), and those chords: the agreement
831/// and the meshes drawn in one pass. A face `kept` has drawn before to the
832/// same chords is taken from it.
833///
834/// # Errors
835///
836/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
837/// deflection is unusable; a face that will not draw carries its own error.
838pub fn face_meshes_for(
839    model: &Model,
840    shape: &Shape,
841    deflection: Deflection,
842    kept: Option<&FaceMeshCache>,
843    tol: Tolerances,
844) -> OgeomResult<(Vec<FaceMesh>, EdgeChords)> {
845    deflection.validate()?;
846    let (faces, chords) = face_meshes_with_chords(model, shape, deflection, kept, tol)?;
847    let faces = faces
848        .into_iter()
849        .map(|(face, piece)| {
850            let piece =
851                piece.and_then(|piece| own_answer(model, &face, piece, deflection, &chords, tol));
852            (face, piece)
853        })
854        .collect();
855    Ok((faces, chords))
856}
857
858/// The faces below `shape` paired with [`face_meshes`]' answers for them.
859fn face_meshes_with_chords(
860    model: &Model,
861    shape: &Shape,
862    deflection: Deflection,
863    kept: Option<&FaceMeshCache>,
864    tol: Tolerances,
865) -> OgeomResult<(Vec<FaceMesh>, EdgeChords)> {
866    let faces: Vec<Shape> =
867        ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
868    let (computed, chords) = face_meshes(model, &faces, deflection, kept, tol)?;
869    Ok((faces.into_iter().zip(computed).collect(), chords))
870}
871
872/// A face's mesh from the shape-wide pass as [`triangulate_face_with`]
873/// answers it: the pass keeps a face that drew nothing, where the face
874/// asked alone is refused, so such a face is asked again.
875fn own_answer(
876    model: &Model,
877    face: &Shape,
878    piece: Triangulation,
879    deflection: Deflection,
880    chords: &EdgeChords,
881    tol: Tolerances,
882) -> OgeomResult<Triangulation> {
883    if piece.triangles.is_empty() {
884        triangulate_face_with(model, face, deflection, chords, tol)
885    } else {
886        Ok(piece)
887    }
888}
889
890/// Every face below a shape drawn to the chords the faces agree on, in face
891/// order, and those chords.
892///
893/// The first pass draws each face at the caller's chord on top of the
894/// narrow faces' asks; a face whose boundary crossed itself folds finer
895/// chords into the agreement, and only the faces touching an edge whose
896/// chord changed are drawn again. A face still short after that is drawn
897/// once more at the same chords as its last pass ([`Pass::Last`]). The
898/// whole-shape mesh welds these, and the stored tessellation keeps them
899/// face by face: the same meshes, drawn once.
900pub(crate) fn face_meshes(
901    read_model: &Model,
902    faces: &[Shape],
903    deflection: Deflection,
904    kept: Option<&FaceMeshCache>,
905    tol: Tolerances,
906) -> OgeomResult<(Vec<OgeomResult<Triangulation>>, EdgeChords)> {
907    let FirstPass {
908        mut finer,
909        mut computed,
910        mut changed,
911        short,
912        mut drawn,
913    } = first_pass(read_model, faces, deflection, kept, tol)?;
914    let mut still = vec![false; faces.len()];
915    for index in short {
916        still[index] = true;
917    }
918    // Every face across a changed edge is drawn again at the new chords,
919    // and asked again whether its rings crossed: a narrow strip drawn whole
920    // with both its rims coarse can cross once one rim is drawn finer by
921    // the face across it, the coarse rim's chords sagging over the fine
922    // one. Its edges are then drawn finer in turn, and so on until no face
923    // comes up short, or the rounds run out; the last round's meshes stand.
924    for _ in 0..SETTLING_ROUNDS {
925        if changed.is_empty() {
926            break;
927        }
928        let again: Vec<usize> = (0..faces.len())
929            .filter(|&i| {
930                ogeom_topo::explore(
931                    read_model,
932                    &faces[i],
933                    ogeom_topo::Filter::OfType(ShapeType::Edge),
934                )
935                .is_ok_and(|es| es.iter().any(|e| changed.contains(&e.node().index())))
936            })
937            .collect();
938        let redone: Vec<OgeomResult<(Triangulation, Verdict)>> =
939            ogeom_core::parallel::map_ordered(&again, |_, &index| {
940                ogeom_core::progress::checkpoint()?;
941                reporting_kept(
942                    read_model,
943                    &faces[index],
944                    deflection,
945                    Some(&finer),
946                    None,
947                    kept,
948                    Pass::Agreeing,
949                    tol,
950                )
951            });
952        let mut short: Vec<usize> = Vec::new();
953        for (index, one) in again.into_iter().zip(redone) {
954            still[index] = matches!(one, Ok((_, Verdict::Short)));
955            computed[index] = match one {
956                Ok((mesh, Verdict::Short)) if mesh.triangles.is_empty() => {
957                    Err(ogeom_core::ogeom_err!(
958                        NotDone,
959                        "the face's boundary enclosed no triangulable region"
960                    ))
961                }
962                Ok((mesh, verdict)) => {
963                    if verdict == Verdict::Short {
964                        short.push(index);
965                    }
966                    Ok(mesh)
967                }
968                Err(e) => Err(e),
969            };
970        }
971        drawn = finer.clone();
972        changed = refine_short(read_model, faces, &short, &mut finer, deflection, kept, tol)?;
973    }
974    // A face still short is drawn once more at the chords it was drawn to,
975    // as its last pass. The chords stay as they are, and so do its
976    // neighbours. Its mesh is taken only where that pass comes out whole.
977    let last: Vec<usize> = (0..faces.len()).filter(|&i| still[i]).collect();
978    let redone: Vec<OgeomResult<(Triangulation, Verdict)>> =
979        ogeom_core::parallel::map_ordered(&last, |_, &index| {
980            ogeom_core::progress::checkpoint()?;
981            reporting_kept(
982                read_model,
983                &faces[index],
984                deflection,
985                Some(&drawn),
986                None,
987                kept,
988                Pass::Last,
989                tol,
990            )
991        });
992    for (index, one) in last.into_iter().zip(redone) {
993        if let Ok((mesh, Verdict::Whole)) = one {
994            computed[index] = Ok(mesh);
995        }
996    }
997    Ok((computed, finer))
998}
999
1000/// How many times the faces across changed edges are drawn again.
1001const SETTLING_ROUNDS: usize = 4;
1002
1003/// The chords each face of `short` (indices into `faces`) wants its edges
1004/// drawn at, merged into `finer` where finer than held: the edges whose
1005/// chord changed.
1006fn refine_short(
1007    read_model: &Model,
1008    faces: &[Shape],
1009    short: &[usize],
1010    finer: &mut EdgeChords,
1011    deflection: Deflection,
1012    kept: Option<&FaceMeshCache>,
1013    tol: Tolerances,
1014) -> OgeomResult<ogeom_core::FastSet<u32>> {
1015    let mut changed: ogeom_core::FastSet<u32> = ogeom_core::FastSet::default();
1016    // Each face asked against the chords held before any of them answers,
1017    // so the answers do not hang on the order the faces are taken in.
1018    // That also lets them be asked side by side; the answers are merged
1019    // in the faces' order, the first error standing as it would one by one.
1020    let held = finer.clone();
1021    let asked: Vec<OgeomResult<Option<EdgeChords>>> =
1022        ogeom_core::parallel::map_ordered(short, |_, &index| {
1023            ogeom_core::progress::checkpoint()?;
1024            let face = &faces[index];
1025            let Some(node) = read_model.node(face) else {
1026                return Ok(None);
1027            };
1028            let NodeData::Face(data) = node.data() else {
1029                return Ok(None);
1030            };
1031            let Some(surface) = read_model.geometry().surface(data.surface) else {
1032                return Ok(None);
1033            };
1034            face_chords_kept(
1035                read_model,
1036                face,
1037                data.surface,
1038                surface,
1039                deflection,
1040                &held,
1041                kept,
1042                tol,
1043            )
1044            .map(Some)
1045        });
1046    for answer in asked {
1047        let Some(chords) = answer? else {
1048            continue;
1049        };
1050        for (edge, chord) in chords {
1051            let held = finer.entry(edge).or_insert(f64::INFINITY);
1052            if chord < *held {
1053                *held = chord;
1054                changed.insert(edge);
1055            }
1056        }
1057    }
1058    Ok(changed)
1059}
1060
1061/// What the first pass over a shape's faces produced: the chords its faces
1062/// agreed to draw their shared edges to, the meshes drawn at the caller's
1063/// chord, and the edges whose chord the crossed faces changed after those
1064/// meshes were drawn; the faces touching one of them are drawn again.
1065struct FirstPass {
1066    finer: EdgeChords,
1067    computed: Vec<OgeomResult<Triangulation>>,
1068    changed: ogeom_core::FastSet<u32>,
1069    /// The faces that came up short.
1070    short: Vec<usize>,
1071    /// The chords the meshes were drawn to, before the short faces'.
1072    drawn: EdgeChords,
1073}
1074
1075/// Every face drawn once at the caller's deflection, each saying whether
1076/// the rings it was drawn from crossed themselves, on top of the chords
1077/// the narrow faces asked for. Nearly none cross, and those faces are
1078/// finished. A face whose boundary crossed itself needs its edges drawn
1079/// finer, and so does every face that shares one of them, or the two
1080/// sides of that edge arrive with a different number of points, which is
1081/// a worse crack than the sliver the refinement was for. Their chords
1082/// are folded into the map and the edges that changed are named.
1083fn first_pass(
1084    read_model: &Model,
1085    faces: &[Shape],
1086    deflection: Deflection,
1087    cache: Option<&FaceMeshCache>,
1088    tol: Tolerances,
1089) -> OgeomResult<FirstPass> {
1090    // `Some(&finer)`, never `None`: a face left to itself refines its own
1091    // edges, which is right when it is meshed alone and wrong here, where
1092    // its neighbours must be told to refine the same ones. Every face is
1093    // drawn at exactly what the caller asked and reports what crossed.
1094    //
1095    // Before that, every face's rings are walked at the caller's chord and
1096    // its width read off them: a face narrower than a few chords wants its
1097    // edges drawn finer, and so do the faces across those edges. Known
1098    // before anything is drawn, those chords go into this pass, and the
1099    // big faces round a thousand small fillets are drawn once rather than
1100    // once and again. The rings are kept for the faces they still
1101    // describe: every face none of whose edges the map names.
1102    let (mut finer, kept) = walk_for_chords(read_model, faces, deflection, tol);
1103    let touched = |face: &Shape| -> bool {
1104        ogeom_topo::explore(
1105            read_model,
1106            face,
1107            ogeom_topo::Filter::OfType(ShapeType::Edge),
1108        )
1109        .is_ok_and(|es| es.iter().any(|e| finer.contains_key(&e.node().index())))
1110    };
1111    // The rings are handed over by the job that draws the face; a shared
1112    // slice cannot give them away, so each sits behind a lock it is taken
1113    // from once.
1114    let jobs: Vec<(&Shape, std::sync::Mutex<Option<Trimming>>)> = faces
1115        .iter()
1116        .zip(kept)
1117        .map(|(face, prep)| {
1118            let trim = prep.filter(|_| !touched(face));
1119            (face, std::sync::Mutex::new(trim))
1120        })
1121        .collect();
1122    let first: Vec<OgeomResult<(Triangulation, Verdict)>> =
1123        ogeom_core::parallel::map_ordered(&jobs, |_, (face, slot)| {
1124            ogeom_core::progress::checkpoint()?;
1125            let trim = slot.lock().ok().and_then(|mut held| held.take());
1126            reporting_kept(
1127                read_model,
1128                face,
1129                deflection,
1130                Some(&finer),
1131                trim,
1132                cache,
1133                Pass::Agreeing,
1134                tol,
1135            )
1136        });
1137    let mut computed: Vec<OgeomResult<Triangulation>> = Vec::with_capacity(faces.len());
1138    let mut crossed: Vec<usize> = Vec::new();
1139    for (index, one) in first.into_iter().enumerate() {
1140        match one {
1141            Ok((mesh, Verdict::Short)) => {
1142                crossed.push(index);
1143                computed.push(Ok(mesh));
1144            }
1145            Ok((mesh, Verdict::Whole)) => computed.push(Ok(mesh)),
1146            Err(e) => computed.push(Err(e)),
1147        }
1148    }
1149    if *MESH_DEBUG_REFINE && !crossed.is_empty() {
1150        eprintln!(
1151            "REFINE {} of {} faces came up short",
1152            crossed.len(),
1153            faces.len()
1154        );
1155    }
1156    // On top of the walk's map, not instead of it: a neighbour drawn again
1157    // must still draw the edges the walk held finer at that chord, or the
1158    // two sides of one of them disagree. Only an edge whose chord
1159    // *changed* sends its faces back.
1160    let drawn = finer.clone();
1161    let changed = refine_short(
1162        read_model, faces, &crossed, &mut finer, deflection, cache, tol,
1163    )?;
1164    Ok(FirstPass {
1165        finer,
1166        computed,
1167        changed,
1168        short: crossed,
1169        drawn,
1170    })
1171}
1172
1173/// Every face's rings walked at the caller's chord, and the chords the
1174/// narrow faces want their edges drawn to, agreed across the shape: the
1175/// finest any face asks of an edge is what every face draws it to.
1176///
1177/// The rings are kept for the faces they still describe (every face none
1178/// of whose edges the map names), so the whole-shape pass draws those once.
1179fn walk_for_chords(
1180    read_model: &Model,
1181    faces: &[Shape],
1182    deflection: Deflection,
1183    tol: Tolerances,
1184) -> (EdgeChords, Vec<Option<Trimming>>) {
1185    let nothing = EdgeChords::new();
1186    let prepared: Vec<OgeomResult<Option<Walked>>> =
1187        ogeom_core::parallel::map_ordered(faces, |_, face| {
1188            ogeom_core::progress::checkpoint()?;
1189            let Some(node) = read_model.node(face) else {
1190                return Ok(None);
1191            };
1192            let NodeData::Face(data) = node.data() else {
1193                return Ok(None);
1194            };
1195            let Some(surface) = read_model.geometry().surface(data.surface) else {
1196                return Ok(None);
1197            };
1198            let deflection = in_own_frame(read_model, face, deflection);
1199            let trim = trimming_rings(
1200                read_model,
1201                face,
1202                data.surface,
1203                surface,
1204                deflection,
1205                &nothing,
1206                false,
1207                tol,
1208            )?;
1209            let narrow = narrow_chord(surface, &trim.rings, deflection, tol);
1210            Ok(Some((trim, narrow)))
1211        });
1212    let mut finer = EdgeChords::new();
1213    let mut kept: Vec<Option<Trimming>> = Vec::with_capacity(faces.len());
1214    for (face, one) in faces.iter().zip(prepared) {
1215        match one {
1216            Ok(Some((prep, narrow))) => {
1217                if let Some(chord) = narrow
1218                    && let Ok(edges) = ogeom_topo::explore(
1219                        read_model,
1220                        face,
1221                        ogeom_topo::Filter::OfType(ShapeType::Edge),
1222                    )
1223                {
1224                    for edge in edges {
1225                        let held = finer.entry(edge.node().index()).or_insert(chord);
1226                        *held = held.min(chord);
1227                    }
1228                }
1229                kept.push(Some(prep));
1230            }
1231            Ok(None) | Err(_) => kept.push(None),
1232        }
1233    }
1234    (finer, kept)
1235}
1236
1237/// The chords the faces below `shape` agree to draw their shared edges to.
1238///
1239/// A face narrower than a few chords draws its edges finer than the
1240/// caller asked, and so does a face whose boundary crosses itself at that
1241/// chord; the face across each of those edges must draw it the same or
1242/// the two meshes disagree along it: a crack in every mesh assembled
1243/// face by face. [`triangulate`] agrees this for itself; a caller meshing
1244/// face by face (a viewer keeping one mesh per face) asks here once per
1245/// shape and hands the answer to [`triangulate_face_with`] for every face.
1246///
1247/// The answer costs a draw of every face at the caller's chord, since a
1248/// boundary is only known to cross itself once it is triangulated; a
1249/// caller meshing face by face pays that draw twice over, and one that
1250/// wants only the welded whole should ask [`triangulate`] instead.
1251///
1252/// # Errors
1253///
1254/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
1255/// deflection is unusable; a face that will not draw asks for nothing
1256/// rather than failing the shape.
1257pub fn edge_chords_for(
1258    model: &Model,
1259    shape: &Shape,
1260    deflection: Deflection,
1261    tol: Tolerances,
1262) -> OgeomResult<EdgeChords> {
1263    deflection.validate()?;
1264    let faces: Vec<Shape> =
1265        ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
1266    Ok(first_pass(model, &faces, deflection, None, tol)?.finer)
1267}
1268
1269/// As [`triangulate_face`], with the edge chords the shape agreed on (the
1270/// answer of [`edge_chords_for`]), so the face's boundary matches its
1271/// neighbours' point for point along every edge the map names.
1272///
1273/// The agreement already holds every refinement the face's own boundary
1274/// asked for, so the face is drawn to it and left there, as the
1275/// whole-shape triangulation leaves it: refining alone past the agreed
1276/// chord, as [`triangulate_face`] would, is what the neighbours cannot
1277/// follow.
1278///
1279/// # Errors
1280///
1281/// As [`triangulate_face`].
1282pub fn triangulate_face_with(
1283    model: &Model,
1284    face: &Shape,
1285    deflection: Deflection,
1286    chords: &EdgeChords,
1287    tol: Tolerances,
1288) -> OgeomResult<Triangulation> {
1289    triangulate_with(model, face, deflection, Some(chords), None, tol)
1290}
1291
1292/// Make the appended face meshes traverse their shared boundaries in
1293/// opposite directions, flipping as few faces as possible.
1294///
1295/// A closed oriented surface walks each interior edge once each way. A file
1296/// whose face orientation flags disagree with each other still *pairs* every
1297/// edge (closed by counting, two-sided nowhere), and every integral over the
1298/// result is reference-dependent garbage. The face flags cannot be judged
1299/// from the model's wires (a synthesised seam's occurrence directions are
1300/// chart bookkeeping, not 3D traversal), but the meshes tell the truth:
1301/// shared boundary vertices are anchored to their edges' own curves, so two
1302/// faces meeting along an edge carry bitwise-identical positions, and the
1303/// direction each walks them is right there in the triangles. Faces are
1304/// flood-filled across those shared runs, each constrained to oppose its
1305/// neighbour, and each connected component keeps the polarity that flips the
1306/// minority; flipping means reversing the piece's windings and negating its
1307/// normals. Conflicts are left standing: this repairs orientation, not
1308/// topology.
1309fn orient_pieces(mesh: &mut Triangulation, pieces: &[(usize, usize)]) {
1310    use ogeom_core::FastMap;
1311    type Key = (u64, u64, u64);
1312    let key = |p: &Point| -> Key { (p.x.to_bits(), p.y.to_bits(), p.z.to_bits()) };
1313
1314    // Directed boundary edges per piece, keyed by position: only each
1315    // piece's *border* edges (used once within the piece) face other pieces.
1316    let mut owners: FastMap<(Key, Key), Vec<(usize, bool)>> = FastMap::default();
1317    for (i, &(t0, t1)) in pieces.iter().enumerate() {
1318        let mut inside: FastMap<(u32, u32), u32> = FastMap::default();
1319        for t in &mesh.triangles[t0..t1] {
1320            for k in 0..3 {
1321                let (a, b) = (t[k], t[(k + 1) % 3]);
1322                *inside.entry((a.min(b), a.max(b))).or_default() += 1;
1323            }
1324        }
1325        for t in &mesh.triangles[t0..t1] {
1326            for k in 0..3 {
1327                let (a, b) = (t[k], t[(k + 1) % 3]);
1328                if inside.get(&(a.min(b), a.max(b))).copied() != Some(1) {
1329                    continue;
1330                }
1331                let (ka, kb) = (
1332                    key(&mesh.positions[a as usize]),
1333                    key(&mesh.positions[b as usize]),
1334                );
1335                // A side whose corners lie on one point (a triangle closing
1336                // at an apex, or a fan ruled onto a seam that shrinks to a
1337                // point) is walked no way at all, and says nothing of
1338                // which way its pieces turn.
1339                if ka == kb {
1340                    continue;
1341                }
1342                // One undirected key, direction recorded: `true` where this
1343                // piece walks the smaller key first.
1344                let (lo, hi, forward) = if ka <= kb {
1345                    (ka, kb, true)
1346                } else {
1347                    (kb, ka, false)
1348                };
1349                owners.entry((lo, hi)).or_default().push((i, forward));
1350            }
1351        }
1352    }
1353
1354    // Manifold constraints: exactly two pieces sharing a run. Same recorded
1355    // direction means exactly one must flip. A pair of faces shares many
1356    // runs and slop can corrupt a few, so each pair's constraint is the
1357    // majority of its runs, and ties abstain rather than guess. Everything
1358    // is aggregated in sorted order, so the answer does not depend on hash
1359    // iteration.
1360    let mut votes: std::collections::BTreeMap<(usize, usize), (u32, u32)> =
1361        std::collections::BTreeMap::new();
1362    for list in owners.values() {
1363        if let [(a, fa), (b, fb)] = list[..]
1364            && a != b
1365        {
1366            let pair = (a.min(b), a.max(b));
1367            let entry = votes.entry(pair).or_insert((0, 0));
1368            if fa == fb {
1369                entry.0 += 1;
1370            } else {
1371                entry.1 += 1;
1372            }
1373        }
1374    }
1375    // Strongest constraints first: a pair vouched for by many runs outranks
1376    // one hanging on a sliver, so when the graph carries a contradiction
1377    // (an odd cycle born of slop), the weakest link is the one disbelieved.
1378    // Union-find with parity keeps the whole resolution order-independent.
1379    // A pair whose runs split evenly still says the faces touch: it joins
1380    // the components with the consistent-shell prior, at zero strength, so
1381    // a fragment cannot drift off and choose its polarity alone.
1382    let mut constraints: Vec<(u32, usize, usize, bool)> = votes
1383        .iter()
1384        .map(|(&(a, b), &(same, opposite))| (same.abs_diff(opposite), a, b, same > opposite))
1385        .collect();
1386    constraints.sort_by(|x, y| y.0.cmp(&x.0).then(x.1.cmp(&y.1)).then(x.2.cmp(&y.2)));
1387
1388    let mut parent: Vec<usize> = (0..pieces.len()).collect();
1389    // Whether a piece is flipped relative to its parent.
1390    let mut parity: Vec<bool> = vec![false; pieces.len()];
1391    fn find(parent: &mut [usize], parity: &mut [bool], i: usize) -> (usize, bool) {
1392        if parent[i] == i {
1393            return (i, false);
1394        }
1395        let (root, above) = find(parent, parity, parent[i]);
1396        parent[i] = root;
1397        parity[i] ^= above;
1398        (root, parity[i])
1399    }
1400    for &(_, a, b, same_direction) in &constraints {
1401        let (ra, pa) = find(&mut parent, &mut parity, a);
1402        let (rb, pb) = find(&mut parent, &mut parity, b);
1403        // One of a same-direction pair flips: their parities must differ.
1404        let need = same_direction;
1405        if ra == rb {
1406            // Agreeing or contradicting, the die is cast; a contradiction
1407            // here lost to stronger evidence.
1408            continue;
1409        }
1410        parent[rb] = ra;
1411        parity[rb] = (pa != pb) != need;
1412    }
1413
1414    let mut flip: Vec<bool> = vec![false; pieces.len()];
1415    let mut components: std::collections::BTreeMap<usize, Vec<usize>> =
1416        std::collections::BTreeMap::new();
1417    for (i, f) in flip.iter_mut().enumerate() {
1418        let (root, p) = find(&mut parent, &mut parity, i);
1419        *f = p;
1420        components.entry(root).or_default().push(i);
1421    }
1422    for members in components.values() {
1423        let flipped = members.iter().filter(|&&i| flip[i]).count();
1424        if flipped * 2 > members.len() {
1425            for &i in members {
1426                flip[i] = !flip[i];
1427            }
1428        }
1429    }
1430
1431    for (i, &(t0, t1)) in pieces.iter().enumerate() {
1432        if !flip[i] {
1433            continue;
1434        }
1435        let mut flipped_vertices: Vec<u32> = Vec::new();
1436        for t in &mut mesh.triangles[t0..t1] {
1437            t.swap(1, 2);
1438            flipped_vertices.extend_from_slice(&t[..]);
1439        }
1440        flipped_vertices.sort_unstable();
1441        flipped_vertices.dedup();
1442        for v in flipped_vertices {
1443            mesh.normals[v as usize] = -mesh.normals[v as usize];
1444        }
1445    }
1446}
1447
1448/// A face's trimming boundary, in its surface's parameter space.
1449///
1450/// The outer wire first, then any holes, each as a closed ring of `(u, v)`
1451/// points with no repeated closing point. A face with no wires covers its
1452/// surface's whole domain, and gets that rectangle as its boundary.
1453///
1454/// Public because parameter-space trimming is not only the triangulator's
1455/// concern: classifying a point against a face, splitting a face in a boolean,
1456/// and hidden-line removal all ask the same question of the same rings, and
1457/// each deriving them separately would be three chances to disagree.
1458///
1459/// # Errors
1460///
1461/// As [`triangulate_face`].
1462pub fn face_boundary(
1463    model: &Model,
1464    face: &Shape,
1465    deflection: Deflection,
1466    tol: Tolerances,
1467) -> OgeomResult<Vec<Vec<Point2>>> {
1468    drawn_boundary(model, face, deflection, false, tol)
1469}
1470
1471/// [`face_boundary`], each ring keeping within the chord of its edges in
1472/// the chart too, read in space through the surface's tangent scales: a
1473/// point on an edge then lies within the chord of its ring however much
1474/// more the edge bends in the chart than in space.
1475///
1476/// The rings to place points against. [`face_boundary`]'s are drawn at the
1477/// points a mesh welds its faces along, and keep to their edges in space
1478/// only: a parabola where a cone meets a plane drafted as the cone is bends
1479/// little in space and much in the cone's chart, and its ring stands off it
1480/// there by more than the chord.
1481///
1482/// # Errors
1483///
1484/// As [`face_boundary`].
1485pub fn face_boundary_in_chart(
1486    model: &Model,
1487    face: &Shape,
1488    deflection: Deflection,
1489    tol: Tolerances,
1490) -> OgeomResult<Vec<Vec<Point2>>> {
1491    drawn_boundary(model, face, deflection, true, tol)
1492}
1493
1494/// [`face_boundary_in_chart`] where every edge's polyline honours the
1495/// deflection, and `None` where one reached [`Deflection::max_segments`]
1496/// first: rings that keep within the chord are what a point can be placed
1497/// against at that chord.
1498///
1499/// # Errors
1500///
1501/// As [`face_boundary`].
1502pub fn face_boundary_in_chart_met(
1503    model: &Model,
1504    face: &Shape,
1505    deflection: Deflection,
1506    tol: Tolerances,
1507) -> OgeomResult<Option<Vec<Vec<Point2>>>> {
1508    let drawn = drawn_trimming(model, face, deflection, true, tol)?;
1509    Ok(drawn.met.then_some(drawn.rings))
1510}
1511
1512/// [`face_boundary`], or with `in_chart` [`face_boundary_in_chart`].
1513fn drawn_boundary(
1514    model: &Model,
1515    face: &Shape,
1516    deflection: Deflection,
1517    in_chart: bool,
1518    tol: Tolerances,
1519) -> OgeomResult<Vec<Vec<Point2>>> {
1520    Ok(drawn_trimming(model, face, deflection, in_chart, tol)?.rings)
1521}
1522
1523fn drawn_trimming(
1524    model: &Model,
1525    face: &Shape,
1526    deflection: Deflection,
1527    in_chart: bool,
1528    tol: Tolerances,
1529) -> OgeomResult<Trimming> {
1530    deflection.validate()?;
1531    if model.kind_of(face)? != ShapeType::Face {
1532        ogeom_bail!(Construction, "expected a face");
1533    }
1534    let Some(node) = model.node(face) else {
1535        ogeom_bail!(Dangling, "face is not in this model");
1536    };
1537    let NodeData::Face(data) = node.data() else {
1538        ogeom_bail!(Construction, "face node holds no face data");
1539    };
1540    let Some(surface) = model.geometry().surface(data.surface) else {
1541        ogeom_bail!(Dangling, "face refers to a surface not in this model");
1542    };
1543
1544    trimming_rings(
1545        model,
1546        face,
1547        data.surface,
1548        surface,
1549        deflection,
1550        &EdgeChords::new(),
1551        in_chart,
1552        tol,
1553    )
1554}
1555
1556/// The chord each edge is to be drawn to, by edge node index, where a face
1557/// wants its edges finer than the caller's chord: what every face sharing
1558/// an edge has to agree on, or their meshes disagree along it.
1559///
1560/// Keyed by the edge's node, so both faces bounding it look the same value
1561/// up and sample it identically. Absent means the caller's own chord.
1562#[allow(
1563    clippy::disallowed_types,
1564    reason = "public type; a std map keeps the API"
1565)]
1566pub type EdgeChords = std::collections::HashMap<u32, f64>;
1567
1568/// How many times a face's boundary may be redrawn finer before its
1569/// crossing is taken to be something the chord cannot fix.
1570///
1571/// Six halvings is a chord sixty-four times tighter than asked. A sliver
1572/// still crossing itself there is degenerate in a way refinement does not
1573/// reach (two boundaries genuinely on top of one another), and drawing it
1574/// a seventh time only spends longer to say so.
1575const REFINEMENTS: usize = 6;
1576
1577/// [`face_chords`], through `kept` where there is one: a face is known by
1578/// its mesh's key at the chords held for its edges, and its answer is kept
1579/// by where each edge comes in the face's walk, so the same face built
1580/// again in another model, its edges named afresh, finds it.
1581#[allow(
1582    clippy::too_many_arguments,
1583    reason = "face_chords' own arguments and the cache"
1584)]
1585fn face_chords_kept(
1586    model: &Model,
1587    face: &Shape,
1588    id: ogeom_topo::SurfaceId,
1589    surface: &SurfaceGeometry,
1590    deflection: Deflection,
1591    held: &EdgeChords,
1592    kept: Option<&FaceMeshCache>,
1593    tol: Tolerances,
1594) -> OgeomResult<EdgeChords> {
1595    let walked = || -> Option<Vec<u32>> {
1596        let mut edges = Vec::new();
1597        for wire in model.ordered_children_of(face).ok()? {
1598            for edge in model.ordered_children_of(&wire).ok()? {
1599                edges.push(edge.node().index());
1600            }
1601        }
1602        Some(edges)
1603    };
1604    let (Some(kept), Some(key), Some(walk)) = (
1605        kept,
1606        mesh_key(model, face, deflection, Some(held), Pass::Agreeing, tol),
1607        walked(),
1608    ) else {
1609        return face_chords(model, face, id, surface, deflection, held, tol);
1610    };
1611    if let Some(found) = kept.chords(&key)
1612        && found.len() == walk.len()
1613    {
1614        return Ok(walk
1615            .iter()
1616            .zip(found)
1617            .filter_map(|(&edge, chord)| chord.map(|c| (edge, c)))
1618            .collect());
1619    }
1620    let found = face_chords(model, face, id, surface, deflection, held, tol)?;
1621    // Kept only where the walk names every edge answered for.
1622    if found.keys().all(|e| walk.contains(e)) {
1623        kept.keep_chords(key, walk.iter().map(|e| found.get(e).copied()).collect());
1624    }
1625    Ok(found)
1626}
1627
1628/// What one face needs its own edges drawn with.
1629///
1630/// A face narrower than the chord error its boundary is drawn with has a
1631/// boundary that crosses *itself*: on an arc sliver a few microns wide,
1632/// drawn at a chord whose sagitta exceeds that width, the inner polyline
1633/// bulges straight through the outer one, and what reaches the
1634/// triangulator is not a region at all but disconnected fragments with
1635/// holes between them, which stop the body meshing closed.
1636///
1637/// The deflection a caller asks for bounds how far the mesh may sit from
1638/// the surface; it is not a licence to hand the triangulator a polygon that
1639/// crosses itself. Refining *lowers* that error, so this never breaks the
1640/// caller's bound.
1641///
1642/// Only this face's own edges are named, and only when they need it.
1643fn face_chords(
1644    model: &Model,
1645    face: &Shape,
1646    id: ogeom_topo::SurfaceId,
1647    surface: &SurfaceGeometry,
1648    deflection: Deflection,
1649    held: &EdgeChords,
1650    tol: Tolerances,
1651) -> OgeomResult<EdgeChords> {
1652    let deflection = in_own_frame(model, face, deflection);
1653    let edges: Vec<u32> =
1654        ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))?
1655            .iter()
1656            .map(|e| e.node().index())
1657            .collect();
1658    // From the chords its edges are already held to: a face whose
1659    // neighbour drew one of its edges finer is asked about its rings as
1660    // they are drawn, one rim fine and the rest as they were, which is
1661    // where a narrow strip's coarse rim sags over its fine one. A chord
1662    // only ever comes down.
1663    let mut finer: EdgeChords = edges
1664        .iter()
1665        .filter_map(|e| held.get(e).map(|c| (*e, *c)))
1666        .collect();
1667    let lower = |finer: &mut EdgeChords, chord: f64| {
1668        for edge in &edges {
1669            let at = finer.entry(*edge).or_insert(chord);
1670            *at = at.min(chord);
1671        }
1672    };
1673    let mut chord = deflection.chord;
1674    // A face narrower than a few chords first: its edges drawn to a
1675    // fraction of its width, so the boundary's sag is small against it.
1676    let first = trimming_rings(model, face, id, surface, deflection, &finer, false, tol)?.rings;
1677    if let Some(want) = narrow_chord(surface, &first, deflection, tol) {
1678        chord = want;
1679        lower(&mut finer, chord);
1680    }
1681    for _ in 0..=REFINEMENTS {
1682        let rings = trimming_rings(model, face, id, surface, deflection, &finer, false, tol)?.rings;
1683        // Only the verdict is read: where the rings crossed, the mesh
1684        // inside them is not drawn.
1685        let planar = region_mesh(
1686            &rings,
1687            surface,
1688            deflection,
1689            rim_holds(model, face, deflection, tol)?,
1690            Upto::Crossing,
1691            tol,
1692        )?;
1693        if !planar.crossed && planar.triangles.len() >= planar.boundary_triangles {
1694            break;
1695        }
1696        chord *= 0.5;
1697        lower(&mut finer, chord);
1698    }
1699    Ok(finer)
1700}
1701
1702/// How many times [`split_flat_triangles`] goes round a face: one per flat
1703/// triangle in the longest row of them along one line.
1704const FLAT_PASSES: usize = 16;
1705
1706/// Take apart every triangle whose corners lie on one line in space.
1707///
1708/// A chart triangle can be sound and its lift have no area: on a cone, the
1709/// apex and two points on one ruling, the apex taken at another `u`. Its
1710/// edges pair with its neighbours', so a weld finds the mesh closed, but a
1711/// reader that drops triangles with no area (as a 3MF reader may) finds a
1712/// hole where it was. The corner on the long side splits the neighbour
1713/// across that side in two instead, and the flat triangle goes: the pair's
1714/// outline, and so every edge the face shares, is what it was.
1715fn split_flat_triangles(mesh: &mut Triangulation, tol: Tolerances) {
1716    let mut across: ogeom_core::FastMap<(u32, u32), usize> = ogeom_core::FastMap::default();
1717    for (i, t) in mesh.triangles.iter().enumerate() {
1718        for k in 0..3 {
1719            across.insert((t[k], t[(k + 1) % 3]), i);
1720        }
1721    }
1722    let at = |mesh: &Triangulation, i: u32| mesh.positions[i as usize];
1723    // Whether `m` lies on the segment from `p` to `q`, strictly between them.
1724    let between = |p: Point, q: Point, m: Point| {
1725        let side = q - p;
1726        let length = side.magnitude();
1727        if length <= tol.confusion() {
1728            return false;
1729        }
1730        let along = (m - p).dot(side) / (length * length);
1731        let off = (m - p).cross(side).magnitude() / length;
1732        off <= tol.confusion()
1733            && along * length > tol.confusion()
1734            && (1.0 - along) * length > tol.confusion()
1735    };
1736    let flat = |mesh: &Triangulation, t: [u32; 3]| {
1737        let [a, b, c] = t.map(|i| at(mesh, i));
1738        (b - a).cross(c - a).magnitude()
1739            <= tol.confusion() * a.distance(b).max(b.distance(c)).max(c.distance(a))
1740    };
1741    let mut gone = vec![false; mesh.triangles.len()];
1742    // A ruling can carry several flat triangles in a row, each the other's
1743    // neighbour across its long side: the one whose neighbour has area goes
1744    // first, and the next pass finds the one inside it a neighbour that
1745    // now has area too.
1746    for _ in 0..FLAT_PASSES {
1747        let mut split = false;
1748        for i in 0..mesh.triangles.len() {
1749            if gone[i] {
1750                continue;
1751            }
1752            let t = mesh.triangles[i];
1753            // The corner `m` on the side from `p` to `q`, wound `p, q, m`.
1754            let Some([p, q, m]) = (0..3)
1755                .map(|k| [t[k], t[(k + 1) % 3], t[(k + 2) % 3]])
1756                .find(|[p, q, m]| between(at(mesh, *p), at(mesh, *q), at(mesh, *m)))
1757            else {
1758                continue;
1759            };
1760            let Some(&n) = across.get(&(q, p)) else {
1761                continue;
1762            };
1763            if n == i || gone[n] {
1764                continue;
1765            }
1766            let other = mesh.triangles[n];
1767            let Some(&d) = other.iter().find(|&&v| v != p && v != q) else {
1768                continue;
1769            };
1770            let halves = [[q, m, d], [m, p, d]];
1771            if halves.iter().any(|h| flat(mesh, *h)) {
1772                continue;
1773            }
1774            gone[i] = true;
1775            mesh.triangles[n] = halves[0];
1776            for k in 0..3 {
1777                across.insert((halves[0][k], halves[0][(k + 1) % 3]), n);
1778            }
1779            across.remove(&(q, p));
1780            across.remove(&(p, q));
1781            let added = mesh.triangles.len();
1782            mesh.triangles.push(halves[1]);
1783            gone.push(false);
1784            for k in 0..3 {
1785                across.insert((halves[1][k], halves[1][(k + 1) % 3]), added);
1786            }
1787            split = true;
1788        }
1789        if !split {
1790            break;
1791        }
1792    }
1793    let mut kept = gone.iter();
1794    mesh.triangles
1795        .retain(|_| !kept.next().copied().unwrap_or(false));
1796}
1797
1798/// How many chords wide a face must be for its edges to be drawn at the
1799/// caller's chord; narrower, they are drawn at the width over this.
1800///
1801/// The boundary of a face drawn at chord `c` sags up to `c` between its
1802/// points, and a triangle from that boundary across the face to the
1803/// other side stands off the surface by the sag. On a face `w` wide that
1804/// is `c / w` of the way to standing on end; held to a quarter, the
1805/// triangles lean at most fourteen degrees.
1806const NARROW: f64 = 4.0;
1807
1808/// The chord a face narrower than [`NARROW`] chords wants its edges drawn
1809/// to, `None` for a face wide enough at the caller's.
1810///
1811/// Width is the rings' chart extent each way scaled by the mean tangent
1812/// length that way, the smaller of the two: right for a strip along a
1813/// chart axis, an over-estimate for one across the chart, which then
1814/// keeps the caller's chord.
1815fn narrow_chord(
1816    surface: &SurfaceGeometry,
1817    rings: &[Vec<Point2>],
1818    deflection: Deflection,
1819    tol: Tolerances,
1820) -> Option<f64> {
1821    let (lo, hi) = chart_extent(rings);
1822    if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
1823        return None;
1824    }
1825    let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
1826    for i in 0..=2 {
1827        for j in 0..=2 {
1828            let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
1829            let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
1830            if let Ok((a, b)) = surface.d1_at(u, v, tol) {
1831                du += a.magnitude();
1832                dv += b.magnitude();
1833                n += 1;
1834            }
1835        }
1836    }
1837    if n == 0 {
1838        return None;
1839    }
1840    #[allow(clippy::cast_precision_loss)]
1841    let width = ((hi.x - lo.x) * du / n as f64).min((hi.y - lo.y) * dv / n as f64);
1842    if !width.is_finite() || width <= tol.confusion() || width >= deflection.chord * NARROW {
1843        return None;
1844    }
1845    Some(width / NARROW)
1846}
1847
1848/// Whether every edge of `face` is already held to `want` or finer.
1849fn edges_already_at(model: &Model, face: &Shape, finer: &EdgeChords, want: f64) -> bool {
1850    ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge)).is_ok_and(
1851        |edges| {
1852            edges
1853                .iter()
1854                .all(|e| finer.get(&e.node().index()).is_some_and(|&c| c <= want))
1855        },
1856    )
1857}
1858
1859/// A face's trimming rings, walked, folded and cleaned.
1860struct Trimming {
1861    /// The outer ring first, then the holes, each closed without a repeated
1862    /// closing point.
1863    rings: Vec<Vec<Point2>>,
1864    /// Each ring point's position in space where its edge's own curve put
1865    /// it, `None` where it was made up.
1866    anchors: Vec<Vec<Option<Point>>>,
1867    /// Whether every edge's polyline honoured the deflection.
1868    met: bool,
1869    /// Each ring as its edges first walked it, chart and space together:
1870    /// what a face whose rings all collapse is drawn from.
1871    walked: Vec<Vec<(Point2, Point)>>,
1872}
1873
1874/// One walked ring: chart points, anchors, deflection honesty, the ambiguous
1875/// whole-period folds taken, and the half-period ties left undecided.
1876type WalkedRing = (
1877    Vec<Point2>,
1878    Vec<Option<Point>>,
1879    bool,
1880    Vec<(usize, f64)>,
1881    Vec<usize>,
1882);
1883
1884/// The whole periods a ring spanning `extent` along a closed direction is
1885/// slid by to stand in `domain`: up where it starts below, down where it
1886/// ends above, and none where it is already inside.
1887///
1888/// A hair past an end is fit noise, not a period: the knots take it, and
1889/// rounding it up to a whole turn would carry the ring a period the wrong
1890/// way. The same hair is taken off before counting, so a ring a whole
1891/// period out by rounding comes back one period, not two.
1892fn turns_into_domain(extent: (f64, f64), domain: (f64, f64)) -> f64 {
1893    let ((least, most), (lo, hi)) = (extent, domain);
1894    let span = hi - lo;
1895    let slack = span * 1e-6;
1896    if least < lo - slack {
1897        ((lo - least - slack) / span).ceil()
1898    } else if most > hi + slack {
1899        -((most - hi - slack) / span).ceil()
1900    } else {
1901        0.0
1902    }
1903}
1904
1905/// A face's rings walked from its wires, each drawn by [`boundary_ring`]
1906/// with `in_chart`.
1907#[allow(
1908    clippy::too_many_arguments,
1909    reason = "the walk's settings, each read once"
1910)]
1911fn trimming_rings(
1912    model: &Model,
1913    face: &Shape,
1914    id: ogeom_topo::SurfaceId,
1915    surface: &SurfaceGeometry,
1916    deflection: Deflection,
1917    finer: &EdgeChords,
1918    in_chart: bool,
1919    tol: Tolerances,
1920) -> OgeomResult<Trimming> {
1921    let mut rings = Vec::new();
1922    let mut ring_anchors = Vec::new();
1923    let mut ring_folds: Vec<Vec<(usize, f64)>> = Vec::new();
1924    let mut ring_ties: Vec<Vec<usize>> = Vec::new();
1925    let mut ring_wires: Vec<Shape> = Vec::new();
1926    let mut met = true;
1927    let wires = model.ordered_children_of(face)?;
1928    let bounded = !wires.is_empty();
1929    let mut walked = Vec::new();
1930    for wire in wires {
1931        let (ring, anchors, ring_met, folds, ties) =
1932            boundary_ring(model, &wire, id, deflection, finer, in_chart, tol)?;
1933        met &= ring_met;
1934        if ring.len() >= 3 {
1935            let mut here = Vec::with_capacity(ring.len());
1936            for (uv, anchor) in ring.iter().zip(&anchors) {
1937                let at = match anchor {
1938                    Some(p) => *p,
1939                    None => surface.point_at(uv.x, uv.y, tol)?,
1940                };
1941                here.push((*uv, at));
1942            }
1943            walked.push(here);
1944            rings.push(ring);
1945            ring_anchors.push(anchors);
1946            ring_folds.push(folds);
1947            ring_ties.push(ties);
1948            ring_wires.push(wire);
1949        }
1950    }
1951
1952    // A band between two *wound* rings: each boundary winds a periodic
1953    // direction once (a bore wall whose ends are a full rim and a
1954    // staircase of arcs, a torus band between two parallels, a ball's
1955    // belt between two latitudes), and neither ring closes on its own. In
1956    // the unrolled chart the face is the strip between the two chains, so
1957    // the pair is merged into one ring by two joining runs standing
1958    // exactly one period apart: their lifted points are the same 3D
1959    // points, and the weld closes them the way it closes a seam. Paired
1960    // before either is closed on its own: a rim closed alone spans the
1961    // whole surface, and two of those cancel where they overlap.
1962    {
1963        use ogeom_geom::Surface as _;
1964        let ((ua, ub), (va, vb)) = surface.domain();
1965        let axes = [
1966            (surface.is_periodic_u(), ub - ua),
1967            (surface.is_periodic_v(), vb - va),
1968        ];
1969        for (axis, (periodic, period)) in axes.into_iter().enumerate() {
1970            if !periodic || period <= 0.0 {
1971                continue;
1972            }
1973            let along = |p: Point2| if axis == 0 { p.x } else { p.y };
1974            let across = |p: Point2| if axis == 0 { p.y } else { p.x };
1975            let make = |a: f64, c: f64| {
1976                if axis == 0 {
1977                    Point2::new(a, c)
1978                } else {
1979                    Point2::new(c, a)
1980                }
1981            };
1982            let d_of = |ring: &[Point2]| -> f64 {
1983                ring.last().map_or(0.0, |l| along(*l) - along(ring[0]))
1984            };
1985            let open_wound: Vec<usize> = rings
1986                .iter()
1987                .enumerate()
1988                .filter(|(_, ring)| {
1989                    ring.len() >= 3
1990                        && (d_of(ring).abs() - period).abs() <= period * 1e-3
1991                        && ring
1992                            .last()
1993                            .is_some_and(|l| ring[0].distance(*l) > period * 0.5)
1994                })
1995                .map(|(i, _)| i)
1996                .collect();
1997            let [i, j] = open_wound[..] else {
1998                continue;
1999            };
2000            if d_of(&rings[i]).signum() == d_of(&rings[j]).signum() {
2001                continue;
2002            }
2003            let sign = d_of(&rings[i]).signum();
2004            // The column the joining runs stand on. Wherever the first rim's
2005            // chain happens to end is as good as anywhere for the rims
2006            // themselves, and no good at all when a third ring lies there:
2007            // a cross hole through a bore wall, whose loop the runs would
2008            // then cut through, two constraints refused and the face never
2009            // drawn whole. So the column is chosen where no other ring is,
2010            // in the widest gap the others leave round the period, and
2011            // both rims are re-cut to begin there: the same chains, the
2012            // same lifted points, begun a turn's fraction round.
2013            let column = {
2014                let mut spans: Vec<(f64, f64)> = Vec::new();
2015                for (k, ring) in rings.iter().enumerate() {
2016                    if k == i || k == j {
2017                        continue;
2018                    }
2019                    let (lo, hi) = ring
2020                        .iter()
2021                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2022                            (lo.min(along(*p)), hi.max(along(*p)))
2023                        });
2024                    if lo.is_finite() && hi - lo < period {
2025                        spans.push((lo.rem_euclid(period), hi - lo));
2026                    }
2027                }
2028                let fallback = rings[i].last().map_or(0.0, |l| along(*l));
2029                if spans.is_empty() {
2030                    fallback
2031                } else {
2032                    // The widest gap between the spans' images on one turn.
2033                    let mut edges: Vec<(f64, f64)> =
2034                        spans.iter().map(|(lo, len)| (*lo, lo + len)).collect();
2035                    edges.sort_by(|a, b| a.0.total_cmp(&b.0));
2036                    let mut best = (f64::NEG_INFINITY, fallback);
2037                    for k in 0..edges.len() {
2038                        let end = edges[k].1;
2039                        let next = if k + 1 < edges.len() {
2040                            edges[k + 1].0
2041                        } else {
2042                            edges[0].0 + period
2043                        };
2044                        if next - end > best.0 {
2045                            best = (next - end, f64::midpoint(end, next));
2046                        }
2047                    }
2048                    best.1
2049                }
2050            };
2051            let recut = |chain: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>| {
2052                // The chain runs one period from its first point, either way
2053                // round; it is cut where it first reaches the column in its
2054                // own direction, and its head carried a period on to the
2055                // tail, closing on the same lifted point.
2056                let n = chain.len();
2057                if n < 3 {
2058                    return;
2059                }
2060                let forward = d_of(chain) > 0.0;
2061                let start = along(chain[0]);
2062                let target = if forward {
2063                    start + (column - start).rem_euclid(period)
2064                } else {
2065                    start - (start - column).rem_euclid(period)
2066                };
2067                let reached = |k: usize| {
2068                    if forward {
2069                        along(chain[k]) >= target
2070                    } else {
2071                        along(chain[k]) <= target
2072                    }
2073                };
2074                let Some(k) = (1..n - 1).find(|&k| reached(k)) else {
2075                    return;
2076                };
2077                if k <= 1 {
2078                    return;
2079                }
2080                let carry = if forward { period } else { -period };
2081                let mut turned: Vec<Point2> = chain[k..].to_vec();
2082                let mut turned_anchors: Vec<Option<Point>> = anchors[k..].to_vec();
2083                for (p, a) in chain[1..=k].iter().zip(&anchors[1..=k]) {
2084                    turned.push(make(along(*p) + carry, across(*p)));
2085                    turned_anchors.push(*a);
2086                }
2087                *chain = turned;
2088                *anchors = turned_anchors;
2089            };
2090            recut(&mut rings[i], &mut ring_anchors[i]);
2091            recut(&mut rings[j], &mut ring_anchors[j]);
2092            let b = rings.remove(j);
2093            let b_anchors = ring_anchors.remove(j);
2094            ring_folds.remove(j);
2095            ring_ties.remove(j);
2096            ring_wires.remove(j);
2097            let a = &mut rings[i];
2098            let a_anchors = &mut ring_anchors[i];
2099            let a_last = a.last().copied().unwrap_or(a[0]);
2100            // Whole periods only: the second chain slides along the unrolled
2101            // chart until its start stands nearest the first chain's end.
2102            let shift = ((along(a_last) - along(b[0])) / period).round() * period;
2103            let b_first = make(along(b[0]) + shift, across(b[0]));
2104            // The run is no edge of the file's, so nothing else draws it to
2105            // the chord: it is drawn here, as the grid's lines are.
2106            let fractions = joining_run(surface, a_last, b_first, deflection, tol);
2107            let inner = &fractions[1..fractions.len() - 1];
2108            for &f in inner {
2109                a.push(make(
2110                    along(a_last) + (along(b_first) - along(a_last)) * f,
2111                    across(a_last) + (across(b_first) - across(a_last)) * f,
2112                ));
2113                a_anchors.push(None);
2114            }
2115            for (p, anchor) in b.iter().zip(&b_anchors) {
2116                a.push(make(along(*p) + shift, across(*p)));
2117                a_anchors.push(*anchor);
2118            }
2119            // The way back: the same run, one period over, walked the other
2120            // way; the two runs lift to identical points.
2121            for &f in inner.iter().rev() {
2122                a.push(make(
2123                    along(a_last) + (along(b_first) - along(a_last)) * f - sign * period,
2124                    across(a_last) + (across(b_first) - across(a_last)) * f,
2125                ));
2126                a_anchors.push(None);
2127            }
2128            // The band now stands on one stretch of the chart, a period wide
2129            // from where the first rim was cut; every other ring (a hole
2130            // through the wall) is slid by whole periods into that stretch,
2131            // the same lifted points, or it stands outside the band it
2132            // belongs in.
2133            let (band_lo, band_hi) = a
2134                .iter()
2135                .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2136                    (lo.min(along(*p)), hi.max(along(*p)))
2137                });
2138            for (k, ring) in rings.iter_mut().enumerate() {
2139                if k == i || ring.is_empty() {
2140                    continue;
2141                }
2142                #[allow(clippy::cast_precision_loss)]
2143                let mean = ring.iter().map(|p| along(*p)).sum::<f64>() / ring.len() as f64;
2144                if mean >= band_lo && mean <= band_hi {
2145                    continue;
2146                }
2147                let turns = ((mean - band_lo) / period).floor();
2148                if turns == 0.0 {
2149                    continue;
2150                }
2151                for p in ring.iter_mut() {
2152                    *p = make(along(*p) - turns * period, across(*p));
2153                }
2154            }
2155        }
2156    }
2157
2158    // A single ring still winding after the pairing had no partner, and if
2159    // it cannot close it was mis-folded: a jump of exactly one period is the
2160    // same 3D point, and the greedy fold that zeroed it may have wound the
2161    // ring instead. Undoing one such ambiguous fold (translating the walk
2162    // from there on by a period) closes the ring; the join it reopens
2163    // still lifts to a single point.
2164    if geometry_winds(surface) {
2165        use ogeom_geom::Surface as _;
2166        let ((ua, ub), _) = surface.domain();
2167        let period = ub - ua;
2168        for ((ring, folds), ties) in rings.iter_mut().zip(&ring_folds).zip(&ring_ties) {
2169            let Some(last) = ring.last().copied() else {
2170                continue;
2171            };
2172            let du = last.x - ring[0].x;
2173            let k = (du / period).round();
2174            if k == 0.0
2175                || (du - k * period).abs() > period * 1e-3
2176                || ring[0].distance(last) <= period * 1e-3
2177            {
2178                continue;
2179            }
2180            // The last ambiguous fold whose applied shift matches the
2181            // winding is the one to undo. Where none does, the walk wound at
2182            // a half-period tie it left in place (a ruling into a cone's
2183            // apex, whose other half the ring needed): the last one is
2184            // where to translate from.
2185            let fold = folds
2186                .iter()
2187                .rev()
2188                .find(|(_, shift)| (shift - k * period).abs() <= period * 1e-6)
2189                .map(|&(fold_start, _)| fold_start)
2190                .filter(|&fold_start| fold_start < ring.len());
2191            if fold.is_none()
2192                && let Some(&tie) = ties.last()
2193                && tie < ring.len()
2194            {
2195                for p in &mut ring[tie..] {
2196                    p.x -= k * period;
2197                }
2198                continue;
2199            }
2200            if let Some(fold_start) = fold {
2201                // Translating from the last undecided tie before the fold
2202                // (where the walk first guessed) keeps the period jump on
2203                // the degenerate row, where it lifts to nothing.
2204                let start = ties
2205                    .iter()
2206                    .rev()
2207                    .find(|&&t| t < fold_start)
2208                    .copied()
2209                    .unwrap_or(fold_start);
2210                for p in &mut ring[start..] {
2211                    p.x -= k * period;
2212                }
2213            }
2214        }
2215    }
2216
2217    // A ring still winding after all that closes on its own: against a
2218    // pole row, or its own translate a period over the other way.
2219    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2220        close_wound_ring(ring, anchors, surface, tol);
2221        snap_chart_junctions(ring, anchors, surface, tol);
2222    }
2223
2224    // Points closer than the triangulator's own resolution make it refuse
2225    // the constraint; a real file's chart can carry them. The consecutive
2226    // near-duplicates collapse, anchors staying aligned.
2227    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2228        collapse_close_points(ring, anchors);
2229    }
2230    // A hole's ring is walked on its own and can come to rest a whole
2231    // period from the ring that bounds the face, its pcurves named in
2232    // another turn of the chart: a cross bore's loop at u = 2pi beside a
2233    // wall drawn from -pi to pi. Left there it lies outside the face's
2234    // region, which is then drawn whole and the hole drawn again as a disc
2235    // of its own. So each ring is slid by whole periods, the same lifted
2236    // points, to stand where the roomiest ring (the face's own bound) does.
2237    {
2238        use ogeom_geom::Surface as _;
2239        let ((ua, ub), (va, vb)) = surface.domain();
2240        let axes = [
2241            (surface.is_periodic_u(), ub - ua),
2242            (surface.is_periodic_v(), vb - va),
2243        ];
2244        let area = |ring: &[Point2]| -> f64 {
2245            let Some(&o) = ring.first() else {
2246                return 0.0;
2247            };
2248            ring.iter()
2249                .zip(ring.iter().cycle().skip(1))
2250                .map(|(a, b)| (*a - o).cross(*b - o))
2251                .sum::<f64>()
2252                .abs()
2253        };
2254        let bound = (0..rings.len()).max_by(|&a, &b| area(&rings[a]).total_cmp(&area(&rings[b])));
2255        for (axis, (periodic, period)) in axes.into_iter().enumerate() {
2256            let Some(bound) = bound else {
2257                break;
2258            };
2259            if !periodic || period <= 0.0 {
2260                continue;
2261            }
2262            let along = |p: &Point2| if axis == 0 { p.x } else { p.y };
2263            let (lo, hi) = rings[bound]
2264                .iter()
2265                .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2266                    (lo.min(along(p)), hi.max(along(p)))
2267                });
2268            if !(lo.is_finite() && hi.is_finite()) {
2269                continue;
2270            }
2271            for (k, ring) in rings.iter_mut().enumerate() {
2272                if k == bound || ring.is_empty() {
2273                    continue;
2274                }
2275                #[allow(clippy::cast_precision_loss)]
2276                let mean = ring.iter().map(along).sum::<f64>() / ring.len() as f64;
2277                if mean >= lo && mean <= hi {
2278                    continue;
2279                }
2280                let turns = ((mean - f64::midpoint(lo, hi)) / period).round();
2281                let moved = mean - turns * period;
2282                if turns == 0.0 || moved < lo || moved > hi {
2283                    continue;
2284                }
2285                for p in ring.iter_mut() {
2286                    if axis == 0 {
2287                        p.x -= turns * period;
2288                    } else {
2289                        p.y -= turns * period;
2290                    }
2291                }
2292            }
2293        }
2294    }
2295    // Folded across a join, a ring on a *closed* surface can come to rest a
2296    // whole period outside the domain. A periodic surface would not mind
2297    // (it wraps), but a surface that merely closes on itself evaluates only
2298    // where its knots are and refuses everywhere else, so a ring every
2299    // point of which lies a turn past the end does not lift at all. The
2300    // fold keeps the ring continuous, which is the part that matters, and
2301    // a rigid slide by whole periods keeps it so: the same points on the
2302    // surface, named inside the chart.
2303    {
2304        use ogeom_geom::Surface as _;
2305        let ((ua, ub), (va, vb)) = surface.domain();
2306        let slides = [
2307            (!surface.is_periodic_u() && surface.is_closed_u(tol), ua, ub),
2308            (!surface.is_periodic_v() && surface.is_closed_v(tol), va, vb),
2309        ];
2310        for (across, (closed, lo, hi)) in [true, false].into_iter().zip(slides) {
2311            if !closed || hi <= lo {
2312                continue;
2313            }
2314            let span = hi - lo;
2315            for ring in &mut rings {
2316                let read = |p: &Point2| if across { p.x } else { p.y };
2317                let (least, most) = ring
2318                    .iter()
2319                    .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), p| {
2320                        (a.min(read(p)), b.max(read(p)))
2321                    });
2322                if !(least.is_finite() && most.is_finite()) || most - least > span * (1.0 + 1e-9) {
2323                    continue;
2324                }
2325                let turns = turns_into_domain((least, most), (lo, hi));
2326                if turns == 0.0 {
2327                    continue;
2328                }
2329                for p in ring.iter_mut() {
2330                    if across {
2331                        p.x += turns * span;
2332                    } else {
2333                        p.y += turns * span;
2334                    }
2335                }
2336            }
2337        }
2338    }
2339    if *MESH_DEBUG {
2340        for (i, (ring, anchors)) in rings.iter().zip(&ring_anchors).enumerate() {
2341            eprintln!("DBG ring {i}: {} points", ring.len());
2342            for (p, a) in ring.iter().zip(anchors) {
2343                eprintln!(
2344                    "DBG   uv({:.5},{:.5}) anchor {}",
2345                    p.x,
2346                    p.y,
2347                    a.map_or("-".to_string(), |q| format!(
2348                        "({:.4},{:.4},{:.4})",
2349                        q.x, q.y, q.z
2350                    ))
2351                );
2352            }
2353        }
2354    }
2355    // A run out along an edge and straight back along it (a ring that
2356    // reads `p, q, p`) is a spike into the region that bounds nothing:
2357    // the file's way of drawing a slit of no width at all on the face's
2358    // own boundary. It triangulates to two hairs and one vertex too many,
2359    // which is one triangle more than a boundary that encloses a region
2360    // has, and it is not a crossing. Off it comes, out to in, until the
2361    // ring reverses nowhere.
2362    // And two consecutive points a millionth of the ring's size apart are
2363    // one point: the end of the last edge and the start of the first, each
2364    // where its own curve put the shared vertex, a file's slop apart. Kept
2365    // both, the second sits on the first's next segment to the last bit,
2366    // and a constraint through a vertex is one the triangulation refuses.
2367    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2368        let reach = chart_reach(ring);
2369        merge_near_duplicates(ring, anchors, reach);
2370        remove_spikes(ring, anchors, reach);
2371    }
2372    let long_enough: Vec<bool> = rings.iter().map(|r| r.len() >= 3).collect();
2373    let mut it = long_enough.iter();
2374    ring_wires.retain(|_| *it.next().unwrap_or(&true));
2375    rings.retain(|r| r.len() >= 3);
2376    ring_anchors.retain(|a| a.len() >= 3);
2377
2378    // An inner ring that walks out and back again along edges through the
2379    // same vertices, thinner than a micron, is a slit, not a hole: a loop
2380    // run out along two arcs and back along two splines fitted to the same
2381    // arcs encloses nothing, and the triangulator can only read it as a
2382    // tangle, drawing holes the face does not have (see [`is_slit`]). The
2383    // outer ring is left alone, whatever it is, since a face that is
2384    // itself a slit is a different question.
2385    if rings.len() > 1 {
2386        // About each ring's first point: the chart's origin may stand far
2387        // off, where the sum is a difference of huge products.
2388        let chart_area = |ring: &[Point2]| -> f64 {
2389            let Some(&anchor) = ring.first() else {
2390                return 0.0;
2391            };
2392            let mut a = 0.0;
2393            for i in 0..ring.len() {
2394                a += (ring[i] - anchor).cross(ring[(i + 1) % ring.len()] - anchor);
2395            }
2396            a.abs()
2397        };
2398        let outer = (0..rings.len())
2399            .max_by(|&i, &j| chart_area(&rings[i]).total_cmp(&chart_area(&rings[j])))
2400            .unwrap_or(0);
2401        let mut keep = Vec::with_capacity(rings.len());
2402        for i in 0..rings.len() {
2403            keep.push(i == outer || !is_slit(model, &ring_wires[i], &ring_anchors[i], tol)?);
2404        }
2405        let mut it = keep.iter();
2406        rings.retain(|_| *it.next().unwrap_or(&true));
2407        let mut it = keep.iter();
2408        ring_anchors.retain(|_| *it.next().unwrap_or(&true));
2409    }
2410
2411    // A face with no wires covers its surface's whole domain, so the domain
2412    // rectangle is the boundary. A face whose wires all collapse (a sliver
2413    // narrower than a millionth of its own length, its two long sides one
2414    // line in the chart) encloses nothing, and keeps no ring at all: taken
2415    // for a face without wires, it would be drawn as the whole plane.
2416    if rings.is_empty() && !bounded {
2417        let ring = domain_ring(surface, deflection, tol);
2418        ring_anchors.push(vec![None; ring.len()]);
2419        rings.push(ring);
2420    }
2421    Ok(Trimming {
2422        rings,
2423        anchors: ring_anchors,
2424        met,
2425        walked,
2426    })
2427}
2428
2429/// Collapse a ring's consecutive near-duplicate points, its closing one
2430/// with them, anchors staying aligned. A ring of fewer than three points is
2431/// left as it is.
2432fn collapse_close_points(ring: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>) {
2433    if ring.len() < 3 {
2434        return;
2435    }
2436    let mut extent = 0.0_f64;
2437    for pair in ring.windows(2) {
2438        extent = extent.max(pair[0].distance(pair[1]));
2439    }
2440    let eps = extent.mul_add(1e-9, 1e-12);
2441    let mut kept_ring = Vec::with_capacity(ring.len());
2442    let mut kept_anchors = Vec::with_capacity(anchors.len());
2443    for (p, a) in ring.iter().zip(anchors.iter()) {
2444        if kept_ring
2445            .last()
2446            .is_some_and(|held: &Point2| held.distance(*p) <= eps)
2447        {
2448            continue;
2449        }
2450        kept_ring.push(*p);
2451        kept_anchors.push(*a);
2452    }
2453    if kept_ring.len() > 2
2454        && let (Some(first), Some(last)) = (kept_ring.first(), kept_ring.last())
2455        && first.distance(*last) <= eps
2456    {
2457        kept_ring.pop();
2458        kept_anchors.pop();
2459    }
2460    *ring = kept_ring;
2461    *anchors = kept_anchors;
2462}
2463
2464/// Whether an inner ring is a slit rather than a hole: its wire walks back
2465/// the way it came, and it is thinner than a micron.
2466///
2467/// A slit's wire runs out along a chain of edges and home along a second
2468/// chain through the same vertices in the opposite order, every edge from
2469/// one vertex to the next answered by another from the next back to it,
2470/// so the two chains bound nothing between them but the rounding of two
2471/// fits to one curve. A hole's wire goes round: a ring of facets, or a
2472/// rim of edges, meets each vertex once, and is a hole however thin. A
2473/// wire of two edges between the same two vertices walks back by its
2474/// vertices whether it is a slit or a round hole cut in two halves; it is
2475/// a slit only where its two sides lie along each other, the ring's width
2476/// under a thousandth of its perimeter. Two halves of a hole bound a round
2477/// region, as wide as a sixth of its perimeter for a circle. Width is
2478/// twice the enclosed area over the perimeter, measured in space through
2479/// the anchors, so a chart's units do not enter into it. A ring with a
2480/// point not anchored is not called a slit.
2481///
2482/// # Errors
2483///
2484/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) where an edge
2485/// of the wire is not in the model.
2486fn is_slit(
2487    model: &Model,
2488    wire: &Shape,
2489    anchors: &[Option<Point>],
2490    tol: Tolerances,
2491) -> OgeomResult<bool> {
2492    let measure = || -> Option<(f64, f64)> {
2493        let pts: Option<Vec<Point>> = anchors.iter().copied().collect();
2494        let pts = pts?;
2495        // About the ring's first point: the sum is the same about any
2496        // point, and about a far one it is a difference of huge products.
2497        let anchor = *pts.first()?;
2498        let mut normal = Vector::ZERO;
2499        let mut perimeter = 0.0;
2500        for i in 0..pts.len() {
2501            let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
2502            normal += (a - anchor).cross(b - anchor);
2503            perimeter += a.distance(b);
2504        }
2505        (perimeter > 0.0).then(|| (normal.magnitude() / perimeter, perimeter))
2506    };
2507    let Some((width, perimeter)) = measure() else {
2508        return Ok(false);
2509    };
2510    if width >= tol.confusion() * 1e4 || !walks_back(model, wire)? {
2511        return Ok(false);
2512    }
2513    Ok(model.ordered_children_of(wire)?.len() > 2 || width < perimeter * 1e-3)
2514}
2515
2516/// Whether a wire's edges pair off, each running from one vertex to
2517/// another answered by a second running from that one back: the walk out
2518/// and the walk home of a ring that goes nowhere. An edge without
2519/// vertices, or one starting where it ends, has no answer.
2520fn walks_back(model: &Model, wire: &Shape) -> OgeomResult<bool> {
2521    let mut ends: Vec<(Shape, Shape)> = Vec::new();
2522    for edge in model.ordered_children_of(wire)? {
2523        let bounds = model.children_of(&edge)?;
2524        let (Some(first), Some(last)) = (bounds.first(), bounds.last()) else {
2525            return Ok(false);
2526        };
2527        if first.is_same(last) {
2528            return Ok(false);
2529        }
2530        ends.push(if edge.orientation() == Orientation::Reversed {
2531            (last.clone(), first.clone())
2532        } else {
2533            (first.clone(), last.clone())
2534        });
2535    }
2536    if ends.len() < 2 || !ends.len().is_multiple_of(2) {
2537        return Ok(false);
2538    }
2539    let mut answered = vec![false; ends.len()];
2540    for i in 0..ends.len() {
2541        if answered[i] {
2542            continue;
2543        }
2544        let (from, to) = &ends[i];
2545        let Some(j) = (i + 1..ends.len())
2546            .find(|&j| !answered[j] && ends[j].0.is_same(to) && ends[j].1.is_same(from))
2547        else {
2548            return Ok(false);
2549        };
2550        answered[i] = true;
2551        answered[j] = true;
2552    }
2553    Ok(true)
2554}
2555
2556/// One wire's trimming ring on a face whose chart neither repeats nor
2557/// closes on itself, as [`face_boundary_in_chart`] draws it among the
2558/// face's other rings; `None` where the wire leaves no ring there.
2559///
2560/// On such a chart each ring is drawn from its own wire alone, so a caller
2561/// asking about a few holes of a face with hundreds draws only those. The
2562/// one ring drawn differently is a slit (a wire walking out and back along
2563/// the same vertices, thinner than a micron), which [`face_boundary`] drops
2564/// unless it is the face's largest ring:
2565/// `largest` says whether this wire's ring is that one.
2566///
2567/// # Errors
2568///
2569/// As [`face_boundary`], and
2570/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
2571/// the face's surface repeats or closes in either chart direction.
2572pub fn open_chart_ring(
2573    model: &Model,
2574    face: &Shape,
2575    wire: &Shape,
2576    largest: bool,
2577    deflection: Deflection,
2578    tol: Tolerances,
2579) -> OgeomResult<Option<Vec<Point2>>> {
2580    use ogeom_geom::Surface as _;
2581    deflection.validate()?;
2582    let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
2583        ogeom_bail!(Construction, "expected a face");
2584    };
2585    let Some(surface) = model.geometry().surface(data.surface) else {
2586        ogeom_bail!(Dangling, "face refers to a surface not in this model");
2587    };
2588    if surface.is_periodic_u()
2589        || surface.is_periodic_v()
2590        || surface.is_closed_u(tol)
2591        || surface.is_closed_v(tol)
2592    {
2593        ogeom_bail!(
2594            Construction,
2595            "a chart that repeats or closes draws its rings together"
2596        );
2597    }
2598    let (mut ring, mut anchors, ..) = boundary_ring(
2599        model,
2600        wire,
2601        data.surface,
2602        deflection,
2603        &EdgeChords::new(),
2604        true,
2605        tol,
2606    )?;
2607    if ring.len() < 3 {
2608        return Ok(None);
2609    }
2610    close_wound_ring(&mut ring, &mut anchors, surface, tol);
2611    collapse_close_points(&mut ring, &mut anchors);
2612    let reach = chart_reach(&ring);
2613    merge_near_duplicates(&mut ring, &mut anchors, reach);
2614    remove_spikes(&mut ring, &mut anchors, reach);
2615    if ring.len() < 3 || anchors.len() < 3 || (!largest && is_slit(model, wire, &anchors, tol)?) {
2616        return Ok(None);
2617    }
2618    Ok(Some(ring))
2619}
2620
2621/// A face with no area as a fan of triangles across each of its walked
2622/// rings, every point where its edges put it. Its normals are the
2623/// surface's carried through the face's placement, turned where the face
2624/// is reversed.
2625fn sliver_fan(
2626    walked: &[Vec<(Point2, Point)>],
2627    surface: &SurfaceGeometry,
2628    placement: &ogeom_math::Transform,
2629    reversed: bool,
2630    tol: Tolerances,
2631) -> Triangulation {
2632    let mut mesh = Triangulation::new();
2633    for ring in walked {
2634        let base = u32::try_from(mesh.positions.len()).unwrap_or(u32::MAX);
2635        for (uv, at) in ring {
2636            mesh.positions.push(*at);
2637            mesh.parameters.push((uv.x, uv.y));
2638            let normal = surface
2639                .normal_at(uv.x, uv.y, tol)
2640                .map_or(ogeom_math::Vector::Z, |n| {
2641                    placement.apply_vector(n.vector())
2642                });
2643            mesh.normals.push(if reversed { -normal } else { normal });
2644        }
2645        let count = u32::try_from(ring.len()).unwrap_or(0);
2646        for k in 1..count.saturating_sub(1) {
2647            mesh.triangles.push([base, base + k, base + k + 1]);
2648        }
2649    }
2650    mesh
2651}
2652
2653/// How near two chart points of a ring stand to be one point: within a
2654/// millionth of the ring's extent along each chart direction, well under
2655/// any feature and well over the slop two edges leave at the vertex they
2656/// share. Each direction is held to its own extent: a chart's two
2657/// directions need not share a unit (a drum's u is an angle and its v a
2658/// length), and a ring a few microns wide round a drum and hundreds of
2659/// millimetres along it keeps its width, where a millionth of its length
2660/// read in radians is wider than the whole ring.
2661#[derive(Debug, Clone, Copy)]
2662struct ChartReach {
2663    u: f64,
2664    v: f64,
2665}
2666
2667impl ChartReach {
2668    /// Whether `p` and `q` are one point.
2669    fn joins(self, p: Point2, q: Point2) -> bool {
2670        (p.x - q.x).abs() <= self.u && (p.y - q.y).abs() <= self.v
2671    }
2672}
2673
2674/// The [`ChartReach`] of `ring`, or `None` for a ring with no extent.
2675fn chart_reach(ring: &[Point2]) -> Option<ChartReach> {
2676    let (mut lo, mut hi) = (
2677        Point2::new(f64::INFINITY, f64::INFINITY),
2678        Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2679    );
2680    for p in ring {
2681        lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
2682        hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
2683    }
2684    let (du, dv) = (hi.x - lo.x, hi.y - lo.y);
2685    (du.is_finite() && dv.is_finite() && du.max(dv) > 0.0).then_some(ChartReach {
2686        u: du * 1e-6,
2687        v: dv * 1e-6,
2688    })
2689}
2690
2691/// Merge consecutive ring points within `reach` of each other,
2692/// cyclically; the earlier point and its anchor stay.
2693fn merge_near_duplicates(
2694    ring: &mut Vec<Point2>,
2695    anchors: &mut Vec<Option<Point>>,
2696    reach: Option<ChartReach>,
2697) {
2698    let Some(reach) = reach else {
2699        return;
2700    };
2701    if ring.len() < 2 || anchors.len() != ring.len() {
2702        return;
2703    }
2704    let mut i = 0;
2705    while i < ring.len() && ring.len() >= 2 {
2706        let next = (i + 1) % ring.len();
2707        if reach.joins(ring[i], ring[next]) {
2708            // The later point goes (the last one when the ring's end
2709            // repeats its start), and the earlier is looked at again, in
2710            // case it sits next to another near-duplicate.
2711            if next == 0 {
2712                ring.remove(i);
2713                anchors.remove(i);
2714                break;
2715            }
2716            ring.remove(next);
2717            anchors.remove(next);
2718        } else {
2719            i += 1;
2720        }
2721    }
2722}
2723
2724/// Strip every `p, q, p` from a ring (a point stepped out to and straight
2725/// back from, the two `p` within `reach` of each other), with its anchors,
2726/// until none is left. Cyclic: the ring's last point is its first's
2727/// neighbour.
2728fn remove_spikes(
2729    ring: &mut Vec<Point2>,
2730    anchors: &mut Vec<Option<Point>>,
2731    reach: Option<ChartReach>,
2732) {
2733    let Some(reach) = reach else {
2734        return;
2735    };
2736    loop {
2737        let n = ring.len();
2738        if n < 3 || anchors.len() != n {
2739            return;
2740        }
2741        let Some(tip) = (0..n).find(|&i| reach.joins(ring[(i + n - 1) % n], ring[(i + 1) % n]))
2742        else {
2743            return;
2744        };
2745        // The tip and one of its two identical neighbours go.
2746        let neighbour = (tip + 1) % n;
2747        let (first, second) = if tip < neighbour {
2748            (neighbour, tip)
2749        } else {
2750            (tip, neighbour)
2751        };
2752        ring.remove(first);
2753        anchors.remove(first);
2754        ring.remove(second);
2755        anchors.remove(second);
2756    }
2757}
2758
2759/// Split a nonincident side at a boundary vertex within chart and surface
2760/// precision. Both occurrences carry the same coordinates and spatial anchor,
2761/// making the junction explicit even under exact orientation predicates.
2762fn snap_chart_junctions(
2763    ring: &mut Vec<Point2>,
2764    anchors: &mut Vec<Option<Point>>,
2765    surface: &SurfaceGeometry,
2766    tol: Tolerances,
2767) {
2768    let original = ring.to_vec();
2769    if original.len() < 3 {
2770        return;
2771    }
2772    // Index both axes: a seam can have thousands of samples at one u, even
2773    // when u is the chart's longer axis. Query the sparser band at each point.
2774    let indexed: Vec<_> = original.iter().map(|p| Point2::new(p.y, p.x)).collect();
2775    let x_bands = RingBands::over(std::slice::from_ref(&indexed));
2776    let y_bands = RingBands::over(std::slice::from_ref(&original));
2777    if x_bands.bands.is_empty() || y_bands.bands.is_empty() {
2778        return;
2779    }
2780    let mut junctions = Vec::new();
2781    for (i, p) in original.iter().copied().enumerate() {
2782        let nearest = junction_candidates(&x_bands, &y_bands, p, tol.parametric())
2783            .iter()
2784            .flatten()
2785            .map(|&k| k as usize)
2786            .filter(|&k| k != i && (k + 1) % original.len() != i)
2787            .filter_map(|k| {
2788                let (a, b) = (original[k], original[(k + 1) % original.len()]);
2789                let (dx, dy) = (b.x - a.x, b.y - a.y);
2790                let l2 = dx * dx + dy * dy;
2791                if l2 == 0.0 {
2792                    return None;
2793                }
2794                let along = ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2;
2795                if !(0.0 < along && along < 1.0) {
2796                    return None;
2797                }
2798                let q = Point2::new(a.x + dx * along, a.y + dy * along);
2799                let distance = p.distance(q);
2800                (distance <= tol.parametric()).then_some((k, along, q, distance))
2801            })
2802            .min_by(|a, b| a.3.total_cmp(&b.3));
2803        if let Some((side, along, q, _)) = nearest
2804            && let (Ok(at), Ok(on)) = (
2805                surface.point_at(p.x, p.y, tol),
2806                surface.point_at(q.x, q.y, tol),
2807            )
2808            && at.distance(on) <= tol.confusion()
2809        {
2810            junctions.push((side, along, i));
2811        }
2812    }
2813    if junctions.is_empty() {
2814        return;
2815    }
2816    // Rebuild in side order so many junctions require one linear copy.
2817    // Each shared vertex bends its constraint by at most the accepted reach.
2818    junctions.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)));
2819    let held = std::mem::take(anchors);
2820    ring.clear();
2821    let mut junctions = junctions.into_iter().peekable();
2822    for (side, (&p, &anchor)) in original.iter().zip(&held).enumerate() {
2823        ring.push(p);
2824        anchors.push(anchor);
2825        while let Some(&(at, _, vertex)) = junctions.peek() {
2826            if at != side {
2827                break;
2828            }
2829            ring.push(original[vertex]);
2830            anchors.push(held[vertex]);
2831            junctions.next();
2832        }
2833    }
2834}
2835
2836/// Any side within `reach` of a point overlaps its band on both axes. Either
2837/// axis is complete; choose the one with fewer entries to keep dense seams
2838/// from turning the junction search into an all-pairs walk.
2839fn junction_candidates<'a>(
2840    x: &'a RingBands,
2841    y: &'a RingBands,
2842    p: Point2,
2843    reach: f64,
2844) -> &'a [Vec<u32>] {
2845    let range = |bands: &RingBands, height: f64| {
2846        let first = RingBands::band_of(height - reach, bands.low, bands.step, bands.bands.len());
2847        let last = RingBands::band_of(height + reach, bands.low, bands.step, bands.bands.len());
2848        first..=last
2849    };
2850    let (xs, ys) = (&x.bands[range(x, p.x)], &y.bands[range(y, p.y)]);
2851    if xs.iter().map(Vec::len).sum::<usize>() <= ys.iter().map(Vec::len).sum::<usize>() {
2852        xs
2853    } else {
2854        ys
2855    }
2856}
2857
2858/// Whether a surface is periodic in `u` alone: the charts on which a wound
2859/// ring cannot close against a translate of itself and needs a partner.
2860fn geometry_winds(surface: &SurfaceGeometry) -> bool {
2861    use ogeom_geom::Surface as _;
2862    surface.is_periodic_u() && !surface.is_periodic_v()
2863}
2864
2865/// Whether a point in parameter space lies inside the region `rings` bound.
2866///
2867/// Even-odd winding: inside the outer ring and outside every hole. The rings
2868/// come from wires whose direction already encodes outer from inner, but
2869/// counting crossings does not depend on that being right, which makes it
2870/// robust to a wire that was built the wrong way round.
2871///
2872/// Says nothing about a point *on* a ring: the crossing count of a boundary
2873/// point is whichever side rounding puts it. A caller that cares has to measure
2874/// its distance to the boundary and decide, which is what classification does.
2875///
2876/// Decided with exact predicates. See [`inside_boundary_with`].
2877#[must_use]
2878pub fn inside_boundary(rings: &[Vec<Point2>], p: Point2) -> bool {
2879    inside_boundary_with::<Exact>(rings, p)
2880}
2881
2882/// As [`inside_boundary`], with the predicate implementation named.
2883///
2884/// This is the seam `docs/DATA_MODEL.md` §9 describes, and it is here rather
2885/// than anywhere else because this is where the *combinatorial* decision is.
2886/// Whether a point is inside a ring is not a measurement that can be a little
2887/// wrong: it decides whether a triangle is kept or dropped, so an error near a
2888/// boundary is a hole in the mesh rather than a slightly misplaced one.
2889///
2890/// The naive form divides to find where an edge crosses the sampling ray, and
2891/// that division cancels catastrophically for a point nearly on the edge.
2892/// `orient2d` answers the same question with no division at all, and
2893/// [`Exact`] answers it correctly however close the point is.
2894#[must_use]
2895pub fn inside_boundary_with<P: Predicates>(rings: &[Vec<Point2>], p: Point2) -> bool {
2896    let mut inside = false;
2897    for ring in rings {
2898        if crosses_odd_times::<P>(ring, p) {
2899            inside = !inside;
2900        }
2901    }
2902    inside
2903}
2904
2905/// The boundary of one wire, in the face's parameter space.
2906///
2907/// Each edge is drawn at its own polyline's points ([`edge_polyline`]) and
2908/// its pcurve evaluated at their parameters, so two faces sharing the edge
2909/// agree on where its points are.
2910/// With `in_chart`, each edge's samples are also refined until its chords
2911/// keep to its pcurve in the chart ([`refine_in_chart`]): a ring to place
2912/// points against rather than to weld a mesh along.
2913fn boundary_ring(
2914    model: &Model,
2915    wire: &Shape,
2916    surface: ogeom_topo::SurfaceId,
2917    deflection: Deflection,
2918    finer: &EdgeChords,
2919    in_chart: bool,
2920    tol: Tolerances,
2921) -> OgeomResult<WalkedRing> {
2922    let mut ring: Vec<Point2> = Vec::new();
2923    let mut anchors: Vec<Option<Point>> = Vec::new();
2924    // Ambiguous folds: where an edge was translated a whole period to
2925    // continue the walk from a start that already coincided modulo the
2926    // period; the fold was one of two defensible choices, recorded so a
2927    // mis-wound ring can be unwound.
2928    let mut folds: Vec<(usize, f64)> = Vec::new();
2929    // Half-period jumps whose side could not be decided when walked.
2930    let mut ties: Vec<usize> = Vec::new();
2931    let mut met = true;
2932    // Whether the ring's first and last points are vertices drawn one step
2933    // past their curves' ends.
2934    let mut stepped_ring = [false; 2];
2935    // Whether each chart direction comes back on itself: periodic, or
2936    // closed without repeating. Asked once here: closure on a spline is a
2937    // walk down a control column, too costly to repeat at every edge of
2938    // every face.
2939    let (wraps_u, wraps_v) = model
2940        .geometry()
2941        .surface(surface)
2942        .map_or((false, false), |g| {
2943            use ogeom_geom::Surface as _;
2944            (
2945                g.is_periodic_u() || g.is_closed_u(tol),
2946                g.is_periodic_v() || g.is_closed_v(tol),
2947            )
2948        });
2949
2950    // Start the walk off a seam if the wire allows it: a seam's side is
2951    // chosen by continuity with the point already walked to, and continuity
2952    // needs something to continue from. The ring is cyclic, so rotating the
2953    // walk changes nothing it reports.
2954    let mut children = model.ordered_children_of(wire)?;
2955    let is_seam = |model: &Model, e: &Shape| -> bool {
2956        model
2957            .node(e)
2958            .and_then(|n| n.data().as_edge())
2959            .and_then(|d| d.pcurve_for(surface, e.location()))
2960            .is_some_and(|r| matches!(r, EdgeRepr::Seam { .. }))
2961    };
2962    if let Some(start) = children.iter().position(|e| !is_seam(model, e)) {
2963        children.rotate_left(start);
2964    }
2965
2966    // The column each seam edge's first traversal effectively walked (after
2967    // folding), and whether its two sides differ in u or in v. A seam bounds
2968    // its face twice, and the two traversals must bracket the ring exactly
2969    // one period apart; the walk checks the second against this record.
2970    let mut seam_walked: ogeom_core::FastMap<ogeom_topo::TShapeId, Point2> =
2971        ogeom_core::FastMap::default();
2972
2973    for edge in children {
2974        let Some(node) = model.node(&edge) else {
2975            ogeom_bail!(Dangling, "edge is not in this model");
2976        };
2977        let NodeData::Edge(data) = node.data() else {
2978            ogeom_bail!(Construction, "edge node holds no edge data");
2979        };
2980        // Whether this edge is a seam, and if so whether its two sides
2981        // differ in u (true) or in v.
2982        let mut seam: Option<bool> = None;
2983        let (pcurve_id, pcurve_range) = match data.pcurve_for(surface, edge.location()) {
2984            Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
2985            // A seam edge runs along a closed surface's join and bounds its
2986            // face twice: up one side of the parameter rectangle and down
2987            // the other. Which side this occurrence takes is decided by the
2988            // ring itself: the side whose oriented start continues the point
2989            // already walked to. Orientation flags cannot answer it (a
2990            // reversed face flips every occurrence while the chart columns
2991            // stay where they were built), but the chart can.
2992            Some(EdgeRepr::Seam {
2993                forward,
2994                reversed,
2995                range,
2996                ..
2997            }) => {
2998                let (f, r) = (*forward, *reversed);
2999                let side_start = |id: ogeom_topo::PCurveId| -> Option<Point2> {
3000                    model.geometry().pcurve(id)?.point_at(range.0, tol).ok()
3001                };
3002                seam = Some(match (side_start(f), side_start(r)) {
3003                    (Some(a), Some(b)) => (a.x - b.x).abs() >= (a.y - b.y).abs(),
3004                    _ => true,
3005                });
3006                let picked = if let Some(last) = ring.last().copied() {
3007                    let start_of = |id: ogeom_topo::PCurveId| -> Option<Point2> {
3008                        let pc = model.geometry().pcurve(id)?;
3009                        let t = if edge.orientation() == Orientation::Reversed {
3010                            range.1
3011                        } else {
3012                            range.0
3013                        };
3014                        pc.point_at(t, tol).ok()
3015                    };
3016                    match (start_of(f), start_of(r)) {
3017                        (Some(a), Some(b)) => {
3018                            if last.distance(a) <= last.distance(b) {
3019                                f
3020                            } else {
3021                                r
3022                            }
3023                        }
3024                        _ => f,
3025                    }
3026                } else if edge.orientation() == Orientation::Reversed {
3027                    r
3028                } else {
3029                    f
3030                };
3031                (picked, *range)
3032            }
3033            _ => ogeom_bail!(
3034                Construction,
3035                "edge has no pcurve on this face, so the face cannot be \
3036                 triangulated in its own parameter space"
3037            ),
3038        };
3039        let Some(pcurve) = model.geometry().pcurve(pcurve_id) else {
3040            ogeom_bail!(Dangling, "pcurve is not in this model");
3041        };
3042
3043        // Sample where the *3D* curve says to, so an adjacent face lands on
3044        // the same points, and *anchor* the boundary vertices to that curve
3045        // too: two faces sharing an edge lift the same parameters through
3046        // different surfaces, and on an imported file those surfaces
3047        // disagree by the file's own slop. The edge is the shared authority,
3048        // so its points are the positions both faces use, and the weld is a
3049        // matter of identity rather than luck.
3050        let mut edge_anchors: Vec<Option<Point>> = Vec::new();
3051        // An edge drawn finer is drawn finer for *every* face that bounds
3052        // it, which is the whole point: the two sides must agree point for
3053        // point or the weld has nothing to join.
3054        let along = match finer.get(&edge.node().index()) {
3055            Some(chord) => Deflection {
3056                chord: *chord,
3057                ..deflection
3058            },
3059            None => deflection,
3060        };
3061        // The ends drawn from the edge's vertices rather than its curve.
3062        let mut snapped_ends: Vec<usize> = Vec::new();
3063        // Whether the edge's start and end are vertices drawn one step past
3064        // its curve's ends, in the curve's direction.
3065        let mut stepped_ends = [false; 2];
3066        let samples = match sample_parameters(model, data, along, tol)? {
3067            Some((parameters, edge_met)) => {
3068                met &= edge_met;
3069                let mut parameters = match (in_chart, model.geometry().surface(surface)) {
3070                    (true, Some(geometry)) => {
3071                        let (refined, chart_met) = refine_in_chart(
3072                            parameters,
3073                            |t| to_pcurve_parameter(t, data, pcurve_range),
3074                            pcurve,
3075                            geometry,
3076                            along,
3077                            tol,
3078                        );
3079                        met &= chart_met;
3080                        refined
3081                    }
3082                    _ => parameters,
3083                };
3084                if let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d()
3085                    && let Some(geometry) = model.geometry().curve(*curve)
3086                    && let Ok(edge_placement) = edge.transform(model.datums())
3087                {
3088                    for t in &parameters {
3089                        edge_anchors.push(
3090                            geometry
3091                                .point_at(*t, tol)
3092                                .ok()
3093                                .map(|p| edge_placement.apply(p)),
3094                        );
3095                    }
3096                    // An edge's curve may end off its vertex by up to the
3097                    // vertex's tolerance, and the next edge's off it on
3098                    // another side: each face round the vertex would then
3099                    // close its own gap between two of the ends, and
3100                    // between them the faces leave a hole. Drawn from the
3101                    // vertex itself, every face round it meets at one
3102                    // point.
3103                    //
3104                    // Moved onto a vertex further off than the chord, an
3105                    // end takes the edge's last segment off its faces by
3106                    // that much along the whole segment: a straight edge
3107                    // is one segment, and a face bounded by such edges
3108                    // alone is drawn wholly beside its surface. The edge
3109                    // then takes one more point on its curve, as far in
3110                    // from the end as the vertex stands off it, and only
3111                    // the step from there to the vertex leaves the curve.
3112                    // The point is the edge's own, so every face along the
3113                    // edge draws it, and every face round the vertex still
3114                    // meets there. In the chart the vertex keeps the
3115                    // curve's end: solved onto the vertex, it can stand
3116                    // inside the face, where the triangulation joins it
3117                    // across the face and takes the face off its surface
3118                    // again.
3119                    let chord = along.chord * edge_placement.scale_factor().abs();
3120                    let corners: Vec<(Point, f64)> = model
3121                        .ordered_children_of(&edge)?
3122                        .iter()
3123                        .filter_map(|v| {
3124                            let data = model.node(v)?.data().as_vertex()?;
3125                            let at = v.transform(model.datums()).ok()?.apply(data.point);
3126                            Some((at, data.tolerance.get()))
3127                        })
3128                        .collect();
3129                    let reach = data.tolerance.get();
3130                    // The end last, so the start's index still holds.
3131                    let mut snapped = [false; 2];
3132                    for i in [edge_anchors.len().saturating_sub(1), 0] {
3133                        let Some(Some(end)) = edge_anchors.get(i).copied() else {
3134                            continue;
3135                        };
3136                        let Some(&(at, within)) = corners
3137                            .iter()
3138                            .min_by(|a, b| a.0.distance(end).total_cmp(&b.0.distance(end)))
3139                        else {
3140                            continue;
3141                        };
3142                        let off = at.distance(end);
3143                        if off > within + reach {
3144                            continue;
3145                        }
3146                        let next = if i == 0 { 1 } else { i - 1 };
3147                        let mut stepped = false;
3148                        if off > chord
3149                            && let (Some(&from), Some(&to)) =
3150                                (parameters.get(i), parameters.get(next))
3151                            && let Some(Some(inner)) = edge_anchors.get(next).copied()
3152                            && inner.distance(end) > 2.0 * off
3153                        {
3154                            let t = from + (to - from) * off / inner.distance(end);
3155                            let step = if i == 0 { 1 } else { i };
3156                            parameters.insert(step, t);
3157                            edge_anchors.insert(
3158                                step,
3159                                geometry
3160                                    .point_at(t, tol)
3161                                    .ok()
3162                                    .map(|p| edge_placement.apply(p)),
3163                            );
3164                            stepped = true;
3165                        }
3166                        let i = if i == 0 { 0 } else { edge_anchors.len() - 1 };
3167                        edge_anchors[i] = Some(at);
3168                        snapped[usize::from(i != 0)] = !stepped;
3169                        stepped_ends[usize::from(i != 0)] = stepped;
3170                    }
3171                    if snapped[0] {
3172                        snapped_ends.push(0);
3173                    }
3174                    if snapped[1] {
3175                        snapped_ends.push(edge_anchors.len() - 1);
3176                    }
3177                }
3178                map_to_pcurve(&parameters, data, pcurve_range)
3179            }
3180            // No 3D curve to defer to: the pcurve's own shape, measured in
3181            // space through the surface, so the chord tolerance means the
3182            // same thing it means everywhere else.
3183            None => {
3184                let Some(geometry) = model.geometry().surface(surface) else {
3185                    ogeom_bail!(Dangling, "face refers to a surface not in this model");
3186                };
3187                // At the chord the faces agreed for this edge, like the
3188                // 3D path above: a face that asked for finer gets it here
3189                // too.
3190                let (_, parameters) = crate::discretize::discretize_on_surface(
3191                    pcurve,
3192                    pcurve_range,
3193                    geometry,
3194                    along,
3195                    tol,
3196                )?;
3197                if in_chart {
3198                    let (refined, chart_met) =
3199                        refine_in_chart(parameters, |t| t, pcurve, geometry, along, tol);
3200                    met &= chart_met;
3201                    refined
3202                } else {
3203                    parameters
3204                }
3205            }
3206        };
3207
3208        let mut points: Vec<Point2> = samples
3209            .iter()
3210            .map(|u| pcurve.point_at(*u, tol))
3211            .collect::<OgeomResult<_>>()?;
3212        // A point drawn from the vertex stands where the vertex does in the
3213        // chart too, or the boundary runs from the curve's end to the
3214        // vertex and back, a fold the triangulation covers twice. Solved
3215        // onto the vertex from the pcurve's own end.
3216        if !snapped_ends.is_empty()
3217            && let Some(geometry) = model.geometry().surface(surface)
3218            && let Ok(placement) = wire.transform(model.datums())
3219            && let Ok(local) = placement.inverse()
3220        {
3221            use ogeom_geom::Surface as _;
3222            for &i in &snapped_ends {
3223                let (Some(Some(target)), Some(start)) = (edge_anchors.get(i), points.get(i)) else {
3224                    continue;
3225                };
3226                let target = local.apply(*target);
3227                let mut uv = *start;
3228                let mut solved = false;
3229                for _ in 0..8 {
3230                    let Ok(jet) = geometry.jet_at(uv.x, uv.y, tol) else {
3231                        break;
3232                    };
3233                    let r = jet.point - target;
3234                    if r.magnitude() <= tol.confusion() {
3235                        solved = true;
3236                        break;
3237                    }
3238                    let (a, b, c) = (jet.du.dot(jet.du), jet.du.dot(jet.dv), jet.dv.dot(jet.dv));
3239                    let det = a.mul_add(c, -(b * b));
3240                    if det.abs() <= f64::MIN_POSITIVE {
3241                        break;
3242                    }
3243                    let (gu, gv) = (jet.du.dot(r), jet.dv.dot(r));
3244                    uv = Point2::new(
3245                        uv.x - (c * gu - b * gv) / det,
3246                        uv.y - (a * gv - b * gu) / det,
3247                    );
3248                }
3249                // Short of the vertex, the point that came nearest it.
3250                if !solved
3251                    && let Ok(p) = geometry.point_at(uv.x, uv.y, tol)
3252                    && let Ok(q) = geometry.point_at(start.x, start.y, tol)
3253                {
3254                    solved = p.distance(target) < q.distance(target);
3255                }
3256                if solved && let Some(slot) = points.get_mut(i) {
3257                    *slot = uv;
3258                }
3259            }
3260        }
3261        if edge.orientation() == Orientation::Reversed {
3262            points.reverse();
3263            edge_anchors.reverse();
3264            stepped_ends.reverse();
3265        }
3266        if edge_anchors.len() != points.len() {
3267            edge_anchors = vec![None; points.len()];
3268        }
3269        // The ends of an edge belong to its *vertices* (the one authority
3270        // every face and every neighbouring edge shares), but only within the
3271        // tolerance the vertex itself records. An imported curve ends within
3272        // the vertex's widened tolerance of it, and lifting the ends through
3273        // the curve alone would leave each corner split as many ways as there
3274        // are curves meeting there. A vertex that sits *beyond* its stated
3275        // tolerance from the curve is not describing the curve's end at all,
3276        // and the curve stays the authority.
3277        if !points.is_empty()
3278            && let Ok(vs) = model.children_of(&edge)
3279            && vs.len() >= 2
3280            && let Ok(edge_placement) = edge.transform(model.datums())
3281        {
3282            let point_of = |v: &Shape| -> Option<(Point, f64)> {
3283                let data = model.node(v)?.data().as_vertex()?;
3284                Some((edge_placement.apply(data.point), data.tolerance.get()))
3285            };
3286            let (from, to) = if edge.orientation() == Orientation::Reversed {
3287                (&vs[vs.len() - 1], &vs[0])
3288            } else {
3289                (&vs[0], &vs[vs.len() - 1])
3290            };
3291            if let Some((p, within)) = point_of(from)
3292                && let Some(a) = edge_anchors.first_mut()
3293                && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
3294            {
3295                *a = Some(p);
3296            }
3297            if let Some((p, within)) = point_of(to)
3298                && let Some(a) = edge_anchors.last_mut()
3299                && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
3300            {
3301                *a = Some(p);
3302            }
3303        }
3304        // Fold onto the branch that continues the ring. Two faces sharing an
3305        // edge share its pcurve, and on a periodic surface the pcurve sits in
3306        // *one* face's window: a cylinder split into two halves has a ruling
3307        // at u = 0 that the other half needs at u = 2pi. The chart cannot
3308        // store both. Continuity with the ring being walked recovers the
3309        // right branch, exactly as the seam sides are chosen.
3310        if let Some(last) = ring.last().copied()
3311            && let Some(first) = points.first().copied()
3312            && let Some(geometry) = model.geometry().surface(surface)
3313        {
3314            use ogeom_geom::Surface as _;
3315            let ((ua, ub), (va, vb)) = geometry.domain();
3316            // Nearest whole period, ties broken toward *not moving*: a jump
3317            // of exactly half a period is what a boundary crossing a
3318            // degenerate row looks like (two rulings into an apex stand
3319            // half a turn apart, and the connecting run along the apex row
3320            // lifts to nothing), and folding it would drag the edge a full
3321            // period from the column its own projection put it on.
3322            let whole_periods = |gap: f64, span: f64| -> f64 {
3323                let r = gap / span;
3324                if (r.fract().abs() - 0.5).abs() <= 1e-9 {
3325                    r.trunc() * span
3326                } else {
3327                    r.round() * span
3328                }
3329            };
3330            let mut shift = Point2::new(0.0, 0.0);
3331            // A repeated seam edge folds like any other, but never onto its
3332            // own first traversal. The two traversals bound the face up one
3333            // side of the chart and down the other, one period apart, and at
3334            // a degenerate row continuity cannot say so: a cone walked to
3335            // its apex reaches a corner that maps to the whole row, both
3336            // sides continue it equally, and folding by nearness closes the
3337            // ring over nothing. The record decides instead: land exactly a
3338            // period from the first walk, on the side the ring occupies.
3339            let prior = seam.and_then(|_| seam_walked.get(&edge.node())).copied();
3340            if wraps_u && (ub - ua) > 0.0 {
3341                let span = ub - ua;
3342                let gap = last.x - first.x;
3343                shift.x = whole_periods(gap, span);
3344                let mut bracketed = false;
3345                if seam == Some(true)
3346                    && let Some(prior) = prior
3347                    && (first.x + shift.x - prior.x).abs() < span * 0.5
3348                {
3349                    let (lo, hi) = ring
3350                        .iter()
3351                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
3352                            (lo.min(p.x), hi.max(p.x))
3353                        });
3354                    // Bracketing is for the face that wraps the period: a
3355                    // band whose rims run the whole way round, its two seam
3356                    // columns a period apart. A *slit* uses one edge twice
3357                    // without wrapping: the ring stays in a fraction of the
3358                    // chart, both traversals stand on one column, and
3359                    // forcing them apart winds the ring, invites a pole row
3360                    // it never touches, and meshes the complement of the
3361                    // face. The ring's own reach
3362                    // says which face this is.
3363                    if hi - lo >= span * 0.5 {
3364                        let side = if f64::midpoint(lo, hi) >= prior.x {
3365                            1.0
3366                        } else {
3367                            -1.0
3368                        };
3369                        shift.x = prior.x + side * span - first.x;
3370                        bracketed = true;
3371                    }
3372                }
3373                if !bracketed {
3374                    if shift.x != 0.0 && (gap - shift.x).abs() <= span * 1e-6 {
3375                        // The start already stood a whole period from the walk
3376                        // (the same 3D point), so this fold is a choice, not a
3377                        // repair; recorded so a mis-wound ring can be unwound.
3378                        folds.push((ring.len(), shift.x));
3379                    } else if ((gap / span).fract().abs() - 0.5).abs() <= 1e-9 {
3380                        // A half-period tie: either side of the degenerate row
3381                        // was defensible, and if the ring comes out wound the
3382                        // unwinding starts here rather than at the later fold.
3383                        ties.push(ring.len());
3384                    }
3385                }
3386            }
3387            if wraps_v && (vb - va) > 0.0 {
3388                let span = vb - va;
3389                shift.y = whole_periods(last.y - first.y, span);
3390                if seam == Some(false)
3391                    && let Some(prior) = prior
3392                    && (first.y + shift.y - prior.y).abs() < span * 0.5
3393                {
3394                    let (lo, hi) = ring
3395                        .iter()
3396                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
3397                            (lo.min(p.y), hi.max(p.y))
3398                        });
3399                    let side = if f64::midpoint(lo, hi) >= prior.y {
3400                        1.0
3401                    } else {
3402                        -1.0
3403                    };
3404                    shift.y = prior.y + side * span - first.y;
3405                }
3406            }
3407            if shift.x != 0.0 || shift.y != 0.0 {
3408                for p in &mut points {
3409                    p.x += shift.x;
3410                    p.y += shift.y;
3411                }
3412            }
3413        }
3414        if seam.is_some()
3415            && let Some(first) = points.first().copied()
3416        {
3417            seam_walked.entry(edge.node()).or_insert(first);
3418        }
3419        // The previous edge already contributed the shared vertex, but only
3420        // where the chart agrees it is shared. Two rulings meeting at an
3421        // apex share the *vertex* while standing apart in the chart, and the
3422        // run between them along the degenerate row is boundary the ring
3423        // needs: dropping its start would cut the corner straight through
3424        // the face's interior.
3425        //
3426        // Two things have to hold for a gap to be such a run, and neither
3427        // alone is enough. It has to be wide: two ends that disagree by the
3428        // file's slop stand a micron over a radius apart, and a thousandth
3429        // of the period is three orders above that. And what lies between
3430        // has to be degenerate: a run along an apex row lifts to one point
3431        // the whole way, so its chart midpoint lands on the shared vertex,
3432        // where a wide gap on a live row lifts to somewhere the width of the
3433        // gap away. Width alone keeps slop on fitted splines. The lift alone
3434        // keeps every gap too small for its midpoint to land anywhere else.
3435        // And no fraction of the period alone is right at all: rulings into
3436        // an apex can stand exactly a quarter turn apart, and a threshold at
3437        // that fraction reads them as not apart and loses the triangle at
3438        // the apex.
3439        let keep_gap = if let (Some(last), Some(first)) = (ring.last(), points.first()) {
3440            model.geometry().surface(surface).is_some_and(|geometry| {
3441                use ogeom_geom::Surface as _;
3442                let ((ua, ub), (va, vb)) = geometry.domain();
3443                let wide = (wraps_u && (last.x - first.x).abs() > (ub - ua) * 1e-3)
3444                    || (wraps_v && (last.y - first.y).abs() > (vb - va) * 1e-3);
3445                if !wide {
3446                    return false;
3447                }
3448                let mid = Point2::new(
3449                    f64::midpoint(last.x, first.x),
3450                    f64::midpoint(last.y, first.y),
3451                );
3452                let reach = tol.confusion() * 1e4;
3453                match (
3454                    geometry.point_at(last.x, last.y, tol),
3455                    geometry.point_at(mid.x, mid.y, tol),
3456                    geometry.point_at(first.x, first.y, tol),
3457                ) {
3458                    (Ok(a), Ok(m), Ok(b)) => a.distance(m) <= reach && m.distance(b) <= reach,
3459                    _ => false,
3460                }
3461            })
3462        } else {
3463            false
3464        };
3465        if !ring.is_empty() && !points.is_empty() && !keep_gap {
3466            points.remove(0);
3467            edge_anchors.remove(0);
3468        }
3469        if ring.is_empty() {
3470            stepped_ring[0] = stepped_ends[0];
3471        }
3472        stepped_ring[1] = stepped_ends[1];
3473        ring.extend(points);
3474        anchors.extend(edge_anchors);
3475    }
3476
3477    // A closed ring repeats its first point at the end; the triangulator wants
3478    // it named once.
3479    if ring.len() > 2
3480        && let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied())
3481    {
3482        // Equal in the chart, or the same vertex in space: the last edge's
3483        // curve ends where the first edge's begins to within the file's
3484        // slop: up to ten microns in a real assembly, recorded on the
3485        // vertex as its widened tolerance. Kept as two points, the ring
3486        // closes with a fold back over its own first segment, a crossing
3487        // a fraction of a micron deep that the triangulation refuses as a
3488        // constraint and the face is then drawn six times finer for.
3489        // The same vertex in space is not enough on its own: a ring that
3490        // winds a periodic chart ends a whole period from where it began
3491        // and lifts to the same point, and that closing is a seam, not
3492        // slop. Close in the chart too (within a thousandth of the ring's
3493        // own extent), or the ring is left to the winding rule below.
3494        let extent = ring.iter().fold(
3495            (
3496                Point2::new(f64::INFINITY, f64::INFINITY),
3497                Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
3498            ),
3499            |(lo, hi), p| {
3500                (
3501                    Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
3502                    Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
3503                )
3504            },
3505        );
3506        let extent = (extent.1.x - extent.0.x).max(extent.1.y - extent.0.y);
3507        let near_in_chart = (first.x - last.x).hypot(first.y - last.y) <= extent * 1e-3;
3508        let same_vertex = near_in_chart
3509            && match (anchors.first(), anchors.last()) {
3510                (Some(Some(a)), Some(Some(b))) => a.distance(*b) <= tol.confusion() * 1e5,
3511                _ => false,
3512            };
3513        // A vertex drawn one step past both the last edge's curve and the
3514        // first's stands at each curve's end in the chart: two points for
3515        // one vertex, closed as the walk closes between any two edges. Ends
3516        // a whole period apart lift to one point and stay to be wound.
3517        let stepped = stepped_ring == [true, true]
3518            && match (anchors.first(), anchors.last()) {
3519                (Some(Some(a)), Some(Some(b))) => a.distance(*b) <= tol.confusion(),
3520                _ => false,
3521            }
3522            && model.geometry().surface(surface).is_some_and(|geometry| {
3523                use ogeom_geom::Surface as _;
3524                match (
3525                    geometry.point_at(first.x, first.y, tol),
3526                    geometry.point_at(last.x, last.y, tol),
3527                ) {
3528                    (Ok(a), Ok(b)) => a.distance(b) > tol.confusion(),
3529                    _ => false,
3530                }
3531            });
3532        if first.is_equal(last, tol) || same_vertex || stepped {
3533            ring.pop();
3534            anchors.pop();
3535        }
3536    }
3537    Ok((ring, anchors, met, folds, ties))
3538}
3539
3540/// Close a ring that winds a periodic direction of its chart and found no
3541/// partner to pair with.
3542///
3543/// A ring that winds one periodic direction of a doubly-periodic surface
3544/// (a diagonal loop on a torus) ends a whole period from where it
3545/// began, and the face is the band between the chain and its own
3546/// translate one period over in the *other* periodic direction, joined
3547/// at the ends by columns that lift to one 3D circle. The translate's
3548/// anchors are the same 3D points, and the joining columns' two copies
3549/// lift identically, so the weld closes them exactly as it closes a seam.
3550/// On a cone or a sphere the ring closes against the row where the
3551/// surface collapses to a point (the apex or the pole), which every `u`
3552/// reaches: that closure costs no area, because the row has none.
3553///
3554/// A ring that has a partner (the other rim of a band) is not closed
3555/// here but paired with it, or each rim would close the whole surface on
3556/// its own and the two would cancel where they overlap.
3557fn close_wound_ring(
3558    ring: &mut Vec<Point2>,
3559    anchors: &mut Vec<Option<Point>>,
3560    geometry: &SurfaceGeometry,
3561    tol: Tolerances,
3562) {
3563    let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied()) else {
3564        return;
3565    };
3566    use ogeom_geom::Surface as _;
3567    let ((ua, ub), (va, vb)) = geometry.domain();
3568    let du = last.x - first.x;
3569    let dv = last.y - first.y;
3570    let winds_u = geometry.is_periodic_u()
3571        && (du.abs() - (ub - ua)).abs() <= (ub - ua) * 1e-3
3572        && dv.abs() <= (vb - va).max(1.0) * 1e-3;
3573    let winds_v = geometry.is_periodic_u()
3574        && geometry.is_periodic_v()
3575        && (dv.abs() - (vb - va)).abs() <= (vb - va) * 1e-3
3576        && du.abs() <= (ub - ua).max(1.0) * 1e-3;
3577    // Where does a u-winding ring close against? On a doubly
3578    // periodic surface, its own translate one v-period over. On a
3579    // cone or sphere, the row where the surface collapses to a point
3580    // (the apex or the pole), which every u reaches: the closure
3581    // costs no area error because the row has none.
3582    let degenerate_row = |v: f64| -> bool {
3583        let (Ok(p), Ok(q), Ok(r)) = (
3584            geometry.point_at(ua, v, tol),
3585            geometry.point_at(f64::midpoint(ua, ub), v, tol),
3586            geometry.point_at(ub, v, tol),
3587        ) else {
3588            return false;
3589        };
3590        p.distance(q) <= tol.confusion() * 10.0 && p.distance(r) <= tol.confusion() * 10.0
3591    };
3592    let target_v = if winds_u && !geometry.is_periodic_v() {
3593        // The nearer degenerate row, if either end has one.
3594        let mid_v = f64::midpoint(first.y, last.y);
3595        if degenerate_row(va) && (mid_v - va).abs() <= (mid_v - vb).abs() {
3596            Some(va)
3597        } else if degenerate_row(vb) {
3598            Some(vb)
3599        } else if degenerate_row(va) {
3600            Some(va)
3601        } else {
3602            None
3603        }
3604    } else {
3605        None
3606    };
3607    let column_steps = 8;
3608    if let Some(v_apex) = target_v {
3609        // Down the seam column to the apex row, across it, and back
3610        // up: the row has no length in space, so the closure adds no
3611        // area and its lifted points weld to the one apex.
3612        let row_steps = ring.len().max(8);
3613        for k in 1..=column_steps {
3614            let f = f64::from(k) / f64::from(column_steps);
3615            ring.push(Point2::new(last.x, last.y + (v_apex - last.y) * f));
3616            anchors.push(None);
3617        }
3618        for k in 1..row_steps {
3619            #[allow(clippy::cast_precision_loss)]
3620            let f = k as f64 / row_steps as f64;
3621            ring.push(Point2::new(last.x + (first.x - last.x) * f, v_apex));
3622            anchors.push(None);
3623        }
3624        for k in 0..column_steps {
3625            let f = f64::from(column_steps - k) / f64::from(column_steps);
3626            ring.push(Point2::new(first.x, first.y + (v_apex - first.y) * f));
3627            anchors.push(None);
3628        }
3629    } else if (winds_u && geometry.is_periodic_v()) || winds_v {
3630        let shift = if winds_u {
3631            Point2::new(0.0, -(vb - va))
3632        } else {
3633            Point2::new(-(ub - ua), 0.0)
3634        };
3635        let chain: Vec<Point2> = ring.clone();
3636        let chain_anchors = anchors.clone();
3637        // Down from the chain's end to its translate's end.
3638        for k in 1..=column_steps {
3639            let f = f64::from(k) / f64::from(column_steps);
3640            ring.push(Point2::new(last.x + shift.x * f, last.y + shift.y * f));
3641            anchors.push(None);
3642        }
3643        // The translate, walked back.
3644        for (p, a) in chain.iter().rev().zip(chain_anchors.iter().rev()).skip(1) {
3645            ring.push(Point2::new(p.x + shift.x, p.y + shift.y));
3646            anchors.push(*a);
3647        }
3648        // Up from the translate's start back to the chain's start,
3649        // stopping one step short of closing.
3650        for k in 1..column_steps {
3651            let f = f64::from(column_steps - k) / f64::from(column_steps);
3652            ring.push(Point2::new(first.x + shift.x * f, first.y + shift.y * f));
3653            anchors.push(None);
3654        }
3655    }
3656}
3657
3658/// Parameters at which to sample an edge, taken from its 3D curve.
3659fn sample_parameters(
3660    model: &Model,
3661    data: &ogeom_topo::EdgeData,
3662    deflection: Deflection,
3663    tol: Tolerances,
3664) -> OgeomResult<Option<(Vec<f64>, bool)>> {
3665    Ok(edge_polyline(model, data, deflection, tol)?
3666        .map(|line| (line.parameters, line.deflection_met)))
3667}
3668
3669/// An edge's polyline in its curve's own frame: its 3D curve discretized
3670/// at `deflection`, refined where a chart chord of it on a face it bounds
3671/// sags past the repair's reach ([`refine_in_every_chart`]). Every face
3672/// bounding the edge draws its boundary through these points, and the
3673/// edge's own polyline is these points; `None` for an edge with no 3D
3674/// curve.
3675pub(crate) fn edge_polyline(
3676    model: &Model,
3677    data: &ogeom_topo::EdgeData,
3678    deflection: Deflection,
3679    tol: Tolerances,
3680) -> OgeomResult<Option<crate::discretize::Polyline>> {
3681    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
3682        return Ok(None);
3683    };
3684    let Some(geometry) = model.geometry().curve(*curve) else {
3685        ogeom_bail!(Dangling, "curve is not in this model");
3686    };
3687    let line = discretize(geometry, *range, deflection, tol)?;
3688    let drawn = line.parameters.len();
3689    let (parameters, met) =
3690        refine_in_every_chart(model, data, line.parameters.clone(), deflection, tol);
3691    if parameters.len() == drawn {
3692        return Ok(Some(crate::discretize::Polyline {
3693            deflection_met: line.deflection_met && met,
3694            ..line
3695        }));
3696    }
3697    let points = parameters
3698        .iter()
3699        .map(|&t| geometry.point_at(t, tol))
3700        .collect::<OgeomResult<Vec<Point>>>()?;
3701    Ok(Some(crate::discretize::Polyline {
3702        points,
3703        parameters,
3704        deflection_met: line.deflection_met && met,
3705    }))
3706}
3707
3708/// Map parameters on the 3D curve onto the pcurve's own range.
3709///
3710/// The two are parameterized over their own intervals; `same_parameter` means
3711/// they agree *proportionally*, which is what this converts.
3712fn map_to_pcurve(
3713    parameters: &[f64],
3714    data: &ogeom_topo::EdgeData,
3715    pcurve_range: (f64, f64),
3716) -> Vec<f64> {
3717    let Some(EdgeRepr::Curve3d { range, .. }) = data.curve3d() else {
3718        return parameters.to_vec();
3719    };
3720    let (ca, cb) = *range;
3721    let (pa, pb) = pcurve_range;
3722    if (cb - ca).abs() <= f64::MIN_POSITIVE {
3723        return parameters.to_vec();
3724    }
3725    parameters
3726        .iter()
3727        .map(|u| pa + (pb - pa) * (u - ca) / (cb - ca))
3728        .collect()
3729}
3730
3731/// [`map_to_pcurve`] for one parameter.
3732fn to_pcurve_parameter(u: f64, data: &ogeom_topo::EdgeData, pcurve_range: (f64, f64)) -> f64 {
3733    map_to_pcurve(&[u], data, pcurve_range)[0]
3734}
3735
3736/// An edge's sample `parameters` refined until its polyline in the chart
3737/// keeps within the chord of its pcurve, read in space through the
3738/// surface's tangent scales, as a classifier's band reads a chart step:
3739/// `(du |S_u|, dv |S_v|)`. Samples placed by the chord in space alone keep
3740/// the edge's chords near the edge in space, but where the edge bends more
3741/// in the chart than in space (a parabola on a cone) the chart's straight
3742/// chords stand off its pcurve by many chords.
3743///
3744/// Each span is read at its quarters and its middle, the greatest of the
3745/// three distances from its chart chord, and split at its middle where that
3746/// exceeds the chord. `to_pcurve` maps a sample parameter onto the pcurve.
3747/// A span whose surface has no tangents where read is left as drawn; the
3748/// flag is false where `max_segments` stopped the refinement short.
3749fn refine_in_chart(
3750    parameters: Vec<f64>,
3751    to_pcurve: impl Fn(f64) -> f64,
3752    pcurve: &ogeom_geom::PlanarCurve,
3753    surface: &SurfaceGeometry,
3754    deflection: Deflection,
3755    tol: Tolerances,
3756) -> (Vec<f64>, bool) {
3757    let at = |t: f64| pcurve.point_at(to_pcurve(t), tol).ok();
3758    let stands_off = |a: f64, b: f64| -> bool {
3759        let (Some(pa), Some(pb)) = (at(a), at(b)) else {
3760            return false;
3761        };
3762        [0.25, 0.5, 0.75].into_iter().any(|share| {
3763            let t = (b - a).mul_add(share, a);
3764            let Some(q) = at(t) else {
3765                return false;
3766            };
3767            let Ok((du, dv)) = surface.d1_at(q.x, q.y, tol) else {
3768                return false;
3769            };
3770            let (su, sv) = (du.magnitude(), dv.magnitude());
3771            let scaled = |p: Point2| Point2::new((p.x - q.x) * su, (p.y - q.y) * sv);
3772            let (a, b) = (scaled(pa), scaled(pb));
3773            let d = b - a;
3774            let squared = d.dot(d);
3775            let w = if squared <= f64::MIN_POSITIVE {
3776                0.0
3777            } else {
3778                (-(a.x * d.x + a.y * d.y) / squared).clamp(0.0, 1.0)
3779            };
3780            let foot = a + d * w;
3781            foot.x.hypot(foot.y) > deflection.chord
3782        })
3783    };
3784    let mut parameters = parameters;
3785    let ceiling = deflection.max_segments.max(parameters.len());
3786    let mut i = 0;
3787    while i + 1 < parameters.len() {
3788        let (a, b) = (parameters[i], parameters[i + 1]);
3789        let mid = f64::midpoint(a, b);
3790        if mid > a && mid < b && stands_off(a, b) {
3791            if parameters.len() > ceiling {
3792                return (parameters, false);
3793            }
3794            parameters.insert(i + 1, mid);
3795        } else {
3796            i += 1;
3797        }
3798    }
3799    (parameters, true)
3800}
3801
3802/// An edge's sample `parameters` refined until, on every curved surface
3803/// it has a pcurve on, no span's chart chord lifts farther off the line
3804/// through its lifted ends than the repair lets a chart edge sag
3805/// ([`REPAIR_SAG`] chords). A face is drawn from its rings' chart chords,
3806/// and the repair splits every triangle along one that sags so, but never
3807/// a span the face shares with its neighbour: where an edge bends in the
3808/// chart and not in space (a curve where a plane cuts a torus near the
3809/// tube's rim), the triangles along such a span draw the repair's points
3810/// towards it, off the face by the sag. Each span is read at its middle and split there, the
3811/// pcurves taken in the order the edge holds them and the round repeated
3812/// until none splits a span: the samples depend on the edge alone, so
3813/// every face bounding it draws the same points. A plane's chart is its
3814/// space and is not asked. The flag is false where `max_segments` stopped
3815/// a refinement short.
3816fn refine_in_every_chart(
3817    model: &Model,
3818    data: &ogeom_topo::EdgeData,
3819    parameters: Vec<f64>,
3820    deflection: Deflection,
3821    tol: Tolerances,
3822) -> (Vec<f64>, bool) {
3823    use ogeom_geom::Surface as _;
3824    let charts: Vec<(&ogeom_geom::PlanarCurve, &SurfaceGeometry, (f64, f64))> = data
3825        .representations
3826        .iter()
3827        .filter_map(|repr| {
3828            let (curve, surface, range) = match repr {
3829                EdgeRepr::PCurve {
3830                    curve,
3831                    surface,
3832                    range,
3833                    ..
3834                } => (*curve, *surface, *range),
3835                EdgeRepr::Seam {
3836                    forward,
3837                    surface,
3838                    range,
3839                    ..
3840                } => (*forward, *surface, *range),
3841                _ => return None,
3842            };
3843            let surface = model.geometry().surface(surface)?;
3844            if matches!(surface, SurfaceGeometry::Plane(_)) {
3845                return None;
3846            }
3847            Some((model.geometry().pcurve(curve)?, surface, range))
3848        })
3849        .collect();
3850    let mut parameters = parameters;
3851    let ceiling = deflection.max_segments.max(parameters.len());
3852    loop {
3853        let before = parameters.len();
3854        for &(pcurve, surface, range) in &charts {
3855            let at = |t: f64| {
3856                pcurve
3857                    .point_at(to_pcurve_parameter(t, data, range), tol)
3858                    .ok()
3859            };
3860            let lift = |p: Point2| surface.point_at(p.x, p.y, tol).ok();
3861            let bows = |a: f64, b: f64| -> bool {
3862                let (Some(pa), Some(pb)) = (at(a), at(b)) else {
3863                    return false;
3864                };
3865                let (Some(start), Some(end)) = (lift(pa), lift(pb)) else {
3866                    return false;
3867                };
3868                lift(pa + (pb - pa) * 0.5)
3869                    .is_some_and(|m| chord_sag(start, end, m, tol) > deflection.chord * REPAIR_SAG)
3870            };
3871            let mut i = 0;
3872            while i + 1 < parameters.len() {
3873                let (a, b) = (parameters[i], parameters[i + 1]);
3874                let mid = f64::midpoint(a, b);
3875                if mid > a && mid < b && bows(a, b) {
3876                    if parameters.len() > ceiling {
3877                        return (parameters, false);
3878                    }
3879                    parameters.insert(i + 1, mid);
3880                } else {
3881                    i += 1;
3882                }
3883            }
3884        }
3885        if parameters.len() == before {
3886            return (parameters, true);
3887        }
3888    }
3889}
3890
3891/// The edge of a surface's domain, as a boundary ring.
3892///
3893/// Refined, not just the four corners. A triangulation only ever connects the
3894/// points it is given, so a boundary named by its corners alone forces long
3895/// triangles reaching right across the domain to find one: on a sphere, a
3896/// sliver from the equator to the pole. The interior refinement cannot fix that;
3897/// the missing points are on the boundary.
3898fn domain_ring(surface: &SurfaceGeometry, deflection: Deflection, tol: Tolerances) -> Vec<Point2> {
3899    let ((ua, ub), (va, vb)) = surface.domain();
3900    if ![ua, ub, va, vb].iter().all(|x| x.is_finite()) {
3901        return Vec::new();
3902    }
3903
3904    let along_u = |v: f64| refine_line(surface, (ua, ub), |u| (u, v), deflection, tol).0;
3905    let along_v = |u: f64| refine_line(surface, (va, vb), |v| (u, v), deflection, tol).0;
3906
3907    // Counter-clockwise around the rectangle. Each side drops its final point,
3908    // which the next side contributes: a repeated vertex would be a
3909    // zero-length boundary edge, and a constraint of zero length is not one.
3910    let (bottom, top) = (along_u(va), along_u(vb));
3911    let (left, right) = (along_v(ua), along_v(ub));
3912    let mut ring = Vec::new();
3913    ring.extend(
3914        bottom[..bottom.len() - 1]
3915            .iter()
3916            .map(|u| Point2::new(*u, va)),
3917    );
3918    ring.extend(right[..right.len() - 1].iter().map(|v| Point2::new(ub, *v)));
3919    ring.extend(top[1..].iter().rev().map(|u| Point2::new(*u, vb)));
3920    ring.extend(left[1..].iter().rev().map(|v| Point2::new(ua, *v)));
3921    ring
3922}
3923
3924/// A triangulation still in parameter space, before it is lifted onto the
3925/// surface.
3926struct PlanarMesh {
3927    /// The `(u, v)` of each vertex.
3928    parameters: Vec<(f64, f64)>,
3929    /// Triangles as indices into `parameters`.
3930    triangles: Vec<[u32; 3]>,
3931    /// Minimum triangle count, accounting for repeated junction vertices.
3932    boundary_triangles: usize,
3933    /// The boundary alone did not triangulate to the count a boundary that
3934    /// encloses a region gives: it crosses itself somewhere.
3935    crossed: bool,
3936}
3937
3938/// Triangulate a region in parameter space, given its boundary rings.
3939///
3940/// The first ring is the outer boundary; the rest are holes.
3941fn triangulate_region(
3942    rings: &[Vec<Point2>],
3943    surface: &SurfaceGeometry,
3944    deflection: Deflection,
3945    rows_kept: Rows,
3946    tol: Tolerances,
3947) -> OgeomResult<PlanarMesh> {
3948    region_mesh(rings, surface, deflection, rows_kept, Upto::Whole, tol)
3949}
3950
3951/// How far [`region_mesh`] goes.
3952#[derive(Clone, Copy, PartialEq, Eq)]
3953enum Upto {
3954    /// The whole mesh.
3955    Whole,
3956    /// Only as far as whether the rings crossed: where they did, the mesh
3957    /// comes back empty, its boundary count and verdict as they are.
3958    Crossing,
3959}
3960
3961/// [`triangulate_region`], stopping where `upto` says.
3962fn region_mesh(
3963    rings: &[Vec<Point2>],
3964    surface: &SurfaceGeometry,
3965    deflection: Deflection,
3966    rows_kept: Rows,
3967    upto: Upto,
3968    tol: Tolerances,
3969) -> OgeomResult<PlanarMesh> {
3970    // A chart mangled enough (trims fitted through millimetres of boundary
3971    // error) can drive the triangulation library past its own asserts, and
3972    // a panic in a dependency is a crash in every consumer. The rings are
3973    // screened for what provably breaks it, and whatever still slips
3974    // through is caught at this boundary and spoken as the refusal it is:
3975    // the kernel's contract is refusal by name, never a crash on bad input.
3976    for ring in rings {
3977        let mut extent = 0.0_f64;
3978        for p in ring {
3979            if !p.x.is_finite() || !p.y.is_finite() {
3980                ogeom_bail!(
3981                    NotDone,
3982                    "a boundary ring carries a non-finite chart coordinate; \
3983                     the face's trim does not describe a region"
3984                );
3985            }
3986            extent = extent.max(p.x.abs()).max(p.y.abs());
3987        }
3988        if extent > 1e12 {
3989            ogeom_bail!(
3990                NotDone,
3991                "a boundary ring reaches {extent:.1e} in the chart; a trim \
3992                 that far out describes no face"
3993            );
3994        }
3995    }
3996    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3997        triangulate_region_inner(rings, surface, deflection, rows_kept, upto, tol)
3998    })) {
3999        Ok(result) => result,
4000        Err(_) => ogeom_bail!(
4001            NotDone,
4002            "the face's boundary broke the triangulation; the chart is too \
4003             degenerate to mesh"
4004        ),
4005    }
4006}
4007
4008/// The chart stretched per axis to the surface's own metric, so that the
4009/// triangulation (Delaunay in the chart) sees distances as space does.
4010///
4011/// A fitted strip a fifth of a millimetre wide and a centimetre long may
4012/// carry `u` over a fiftieth of a unit and `v` over one: in the chart the
4013/// long way is the short way, sixty times over, and Delaunay, which
4014/// connects nearest neighbours *in the chart*, joins points along the
4015/// strip across several columns rather than to the row beside them. The
4016/// triangles it makes are slivers in space that lift folded (a flat
4017/// triangle spanning a bend the surface takes in between, its normal
4018/// pointing where none of its vertices' do), and the face shades as a
4019/// quilt of creases. Scaled by the mean tangent length each way, the
4020/// chart is the surface to first order, and Delaunay in it is Delaunay
4021/// on the surface.
4022#[derive(Debug, Clone, Copy)]
4023struct ChartScale {
4024    su: f64,
4025    sv: f64,
4026}
4027
4028impl ChartScale {
4029    /// The mean tangent length each way over the rings' extent, the longer
4030    /// normalized to one; `(1, 1)` where the surface will not say.
4031    fn of(surface: &SurfaceGeometry, rings: &[Vec<Point2>], tol: Tolerances) -> Self {
4032        let (lo, hi) = chart_extent(rings);
4033        if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
4034            return Self { su: 1.0, sv: 1.0 };
4035        }
4036        let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
4037        for i in 0..=2 {
4038            for j in 0..=2 {
4039                let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
4040                let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
4041                if let Ok((a, b)) = surface.d1_at(u, v, tol) {
4042                    du += a.magnitude();
4043                    dv += b.magnitude();
4044                    n += 1;
4045                }
4046            }
4047        }
4048        if n == 0 || !(du > 0.0 && dv > 0.0) || !du.is_finite() || !dv.is_finite() {
4049            return Self { su: 1.0, sv: 1.0 };
4050        }
4051        let longer = du.max(dv);
4052        Self {
4053            su: du / longer,
4054            sv: dv / longer,
4055        }
4056    }
4057
4058    /// A chart point into the scaled chart.
4059    fn to(self, u: f64, v: f64) -> SpadePoint<f64> {
4060        SpadePoint::new(u * self.su, v * self.sv)
4061    }
4062
4063    /// A scaled-chart point back into the chart.
4064    fn from(self, x: f64, y: f64) -> (f64, f64) {
4065        (x / self.su, y / self.sv)
4066    }
4067}
4068
4069/// The rings' bounding box in the chart.
4070fn chart_extent(rings: &[Vec<Point2>]) -> (Point2, Point2) {
4071    rings.iter().flatten().fold(
4072        (
4073            Point2::new(f64::INFINITY, f64::INFINITY),
4074            Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
4075        ),
4076        |(lo, hi), p| {
4077            (
4078                Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
4079                Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
4080            )
4081        },
4082    )
4083}
4084
4085fn triangulate_region_inner(
4086    rings: &[Vec<Point2>],
4087    surface: &SurfaceGeometry,
4088    deflection: Deflection,
4089    rows_kept: Rows,
4090    upto: Upto,
4091    tol: Tolerances,
4092) -> OgeomResult<PlanarMesh> {
4093    let mut cdt: ConstrainedDelaunayTriangulation<SpadePoint<f64>> =
4094        ConstrainedDelaunayTriangulation::new();
4095    let sub = Instant::now();
4096    let mut refused_total = 0usize;
4097    let mut junctions = 0usize;
4098
4099    // Everything the triangulation sees is in the scaled chart; the rings'
4100    // own chart points are kept by their scaled bits so a boundary vertex
4101    // comes back with exactly the parameters its anchor was keyed by.
4102    let scale = ChartScale::of(surface, rings, tol);
4103    let scaled: Vec<Vec<Point2>> = rings
4104        .iter()
4105        .map(|ring| {
4106            ring.iter()
4107                .map(|p| {
4108                    let q = scale.to(p.x, p.y);
4109                    Point2::new(q.x, q.y)
4110                })
4111                .collect()
4112        })
4113        .collect();
4114    let mut exact: ogeom_core::FastMap<(u64, u64), (f64, f64)> = ogeom_core::FastMap::default();
4115
4116    // The boundary edges are constraints, so the triangulation respects the
4117    // trimming rather than spanning across a hole.
4118    for (ring, chart) in scaled.iter().zip(rings) {
4119        let mut ring_handles = Vec::with_capacity(ring.len());
4120        for (p, uv) in ring.iter().zip(chart) {
4121            // A chart coordinate can come out subnormal-tiny (the sine of
4122            // a fold angle, the residue of an exact cancellation), and the
4123            // triangulation refuses what is, for every purpose, zero. The
4124            // point is known by where the triangulation holds it, so its
4125            // exact parameters, and the anchor they key, are found again.
4126            let held = mitigate_underflow(SpadePoint::new(p.x, p.y));
4127            exact.insert((held.x.to_bits(), held.y.to_bits()), (uv.x, uv.y));
4128            let handle = cdt
4129                .insert(held)
4130                .map_err(|e| ogeom_core::ogeom_err!(NotDone, "boundary insertion failed: {e}"))?;
4131            ring_handles.push(handle);
4132        }
4133        let distinct: ogeom_core::FastSet<_> = ring_handles.iter().copied().collect();
4134        junctions += ring_handles.len() - distinct.len();
4135        let mut refused = 0usize;
4136        let mut same = 0usize;
4137        for i in 0..ring_handles.len() {
4138            let (a, b) = (ring_handles[i], ring_handles[(i + 1) % ring_handles.len()]);
4139            if a == b {
4140                same += 1;
4141            } else if cdt.can_add_constraint(a, b) {
4142                cdt.add_constraint(a, b);
4143            } else {
4144                refused += 1;
4145                if *MESH_DEBUG_REFINE {
4146                    let n = ring.len();
4147                    let partners: Vec<usize> = (0..n)
4148                        .filter(|&j| j != i && j != (i + 1) % n && j != (i + n - 1) % n)
4149                        .filter(|&j| {
4150                            segments_cross(ring[i], ring[(i + 1) % n], ring[j], ring[(j + 1) % n])
4151                        })
4152                        .collect();
4153                    let len = ring[i].distance(ring[(i + 1) % n]);
4154                    eprintln!(
4155                        "REFUSED segment {i} of {n} (chart length {len:.3e}) crosses {partners:?}"
4156                    );
4157                    for j in [(i + n - 1) % n, i, (i + 1) % n, (i + 2) % n, (i + 3) % n] {
4158                        eprintln!("   ring[{j}] = ({:.12}, {:.12})", ring[j].x, ring[j].y);
4159                    }
4160                }
4161            }
4162        }
4163        if *MESH_DEBUG_REFINE && (refused > 0 || same > 0) {
4164            eprintln!(
4165                "CONSTRAINTS ring of {}: {refused} refused, {same} zero-length",
4166                ring.len()
4167            );
4168        }
4169        refused_total += refused;
4170    }
4171
4172    // Before interior points go in, the boundary gives an exact triangle
4173    // count. A simple polygon with holes gives `b + 2w - 4`, where `b` is
4174    // its distinct vertices and `w` its rings. A repeated vertex within a
4175    // ring joins otherwise separate lobes and reduces that count by one.
4176    let bands = RingBands::over(&scaled);
4177    // A refused constraint crosses one already present, regardless of count.
4178    let boundary_triangles = (cdt.num_vertices() + 2 * rings.len()).saturating_sub(4 + junctions);
4179    let crossed = refused_total > 0
4180        || inside_by_parity(&cdt).is_none_or(|inside| inside != boundary_triangles);
4181    if *MESH_DEBUG_REFINE && crossed {
4182        let points: usize = rings.iter().map(Vec::len).sum();
4183        eprintln!(
4184            "PARITY inside {:?} vertices {} points {points} rings {} inner faces {}",
4185            inside_by_parity(&cdt),
4186            cdt.num_vertices(),
4187            rings.len(),
4188            cdt.num_inner_faces()
4189        );
4190    }
4191    if crossed && upto == Upto::Crossing {
4192        return Ok(PlanarMesh {
4193            parameters: Vec::new(),
4194            triangles: Vec::new(),
4195            boundary_triangles,
4196            crossed,
4197        });
4198    }
4199
4200    // Interior points where the surface bends away from the flat triangle. A
4201    // planar face needs none, which is why this is driven by measured
4202    // deflection rather than by a fixed grid.
4203    let boundary_ms = sub.elapsed().as_secs_f64() * 1e3;
4204    let sub = Instant::now();
4205    add_interior_points(&mut cdt, rings, surface, deflection, scale, rows_kept, tol)?;
4206    let interior_ms = sub.elapsed().as_secs_f64() * 1e3;
4207    let interior_points = cdt.num_vertices();
4208    let sub = Instant::now();
4209    let mut rounds_run = 0usize;
4210
4211    // The scale a degenerate chart triangle is measured against: the
4212    // region's own span, the longer way. Its *position* is not its size:
4213    // a face on a cylinder whose axis point sits half a metre away has
4214    // `v` near −500 000 and a span of twenty, and a scale taken from where
4215    // the ring sits rather than how far it reaches would call every honest
4216    // cell a hair.
4217    let (lo, hi) = chart_extent(&scaled);
4218    let extent = (hi.x - lo.x).max(hi.y - lo.y);
4219    let degenerate_area = extent.max(1.0).powi(2) * 1e-12;
4220
4221    // The grid rows guarantee the deflection along their own lines, but a
4222    // hole in the face punches a gap through a row, and where the surface is
4223    // flat in one direction (a cylinder along its axis) there may be no
4224    // other row for the mesher to reach. The band around the hole then fans
4225    // from the rim to the far side of the gap, in triangles that sag through
4226    // the solid by far more than the deflection while every one of their
4227    // vertices sits exactly on the surface: a mesh whose faces all have the
4228    // right area and the wrong volume.
4229    //
4230    // The repair measures the truth: any kept triangle whose midpoints sag
4231    // beyond the chord gets its centre inserted, and the loop runs until the
4232    // mesh is honest or the cap says the surface is being unreasonable.
4233    // The rings do not change while the mesh is refined, so the containment
4234    // test they answer is indexed once and reused by every round below and by
4235    // the output pass.
4236    if !matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
4237        // Whether a chart edge sags past the repair's threshold depends on
4238        // its two ends alone: measured once, however many triangles share
4239        // it and however many rounds it survives.
4240        let mut sags: ogeom_core::FastMap<([u64; 2], [u64; 2]), bool> =
4241            ogeom_core::FastMap::default();
4242        // And a vertex's point on the surface is asked once, however many
4243        // chart edges end at it.
4244        let mut lifted: ogeom_core::FastMap<[u64; 2], Option<Point>> =
4245            ogeom_core::FastMap::default();
4246        for _ in 0..REFINEMENT_ROUNDS {
4247            rounds_run += 1;
4248            let before = cdt.num_vertices();
4249            let mut worst: Vec<SpadePoint<f64>> = Vec::new();
4250            for triangle in cdt.inner_faces() {
4251                let vertices = triangle.vertices();
4252                let centre = triangle.center();
4253                let at = Point2::new(centre.x, centre.y);
4254                if !bands.holds(at) {
4255                    continue;
4256                }
4257                let corners: [(f64, f64); 3] = [
4258                    (vertices[0].position().x, vertices[0].position().y),
4259                    (vertices[1].position().x, vertices[1].position().y),
4260                    (vertices[2].position().x, vertices[2].position().y),
4261                ];
4262                // A chart-degenerate hair is dropped from the mesh, not
4263                // refined: its 3D chord can sag enormously, and feeding its
4264                // centre back in only breeds more hairs along the same line.
4265                let area = ((corners[1].0 - corners[0].0) * (corners[2].1 - corners[0].1)
4266                    - (corners[1].1 - corners[0].1) * (corners[2].0 - corners[0].0))
4267                    .abs()
4268                    / 2.0;
4269                if area < degenerate_area {
4270                    continue;
4271                }
4272                // The grid already bounds sag along rows and columns, and a
4273                // grid triangle's diagonal spanning one cell each way may
4274                // legitimately sag up to the sum (twice the chord). The
4275                // threshold sits clear above that band so the repair fires
4276                // only on the fan triangles it exists for, which sag through
4277                // a hole's gap by tens of chords, and an honest grid
4278                // (including a perfectly symmetric one, whose mesh must stay
4279                // symmetric) is left untouched.
4280                let sagged = (0..3).any(|i| {
4281                    let (a, b) = (corners[i], corners[(i + 1) % 3]);
4282                    let bits = |p: (f64, f64)| [p.0.to_bits(), p.1.to_bits()];
4283                    let key = if bits(a) <= bits(b) {
4284                        (bits(a), bits(b))
4285                    } else {
4286                        (bits(b), bits(a))
4287                    };
4288                    *sags.entry(key).or_insert_with(|| {
4289                        let (from, to) = (scale.from(a.0, a.1), scale.from(b.0, b.1));
4290                        let mut lift = |p: (f64, f64), uv: (f64, f64)| {
4291                            *lifted
4292                                .entry(bits(p))
4293                                .or_insert_with(|| GridSample::point_at(surface, uv, tol))
4294                        };
4295                        let (start, end) = (lift(a, from), lift(b, to));
4296                        let mid = (f64::midpoint(from.0, to.0), f64::midpoint(from.1, to.1));
4297                        let sag = match (start, end, GridSample::point_at(surface, mid, tol)) {
4298                            (Some(start), Some(end), Some(middle)) => {
4299                                chord_sag(start, end, middle, tol)
4300                            }
4301                            // Off the surface's domain; nothing to refine towards.
4302                            _ => 0.0,
4303                        };
4304                        sag > deflection.chord * REPAIR_SAG
4305                    })
4306                });
4307                // A surface curved both ways twists between its rows and
4308                // columns, and a triangle whose edges each keep to the chord
4309                // can still stand off it in its middle. A ruled face is not
4310                // safe either: a triangle joining points on rulings far
4311                // apart (at a cone's apex, where every ruling meets) cuts
4312                // inside its curve. So on every curved face a triangle's
4313                // middle and the middles of its sides, lifted, are held to
4314                // the chord against its plane.
4315                let middle_off = || {
4316                    let lift =
4317                        |p: (f64, f64)| GridSample::point_at(surface, scale.from(p.0, p.1), tol);
4318                    let [a, b, c] = corners.map(lift);
4319                    let (Some(a), Some(b), Some(c)) = (a, b, c) else {
4320                        return false;
4321                    };
4322                    let Ok(n) = (b - a).cross(c - a).normalized(tol) else {
4323                        return false;
4324                    };
4325                    let half = |i: usize, j: usize| {
4326                        (
4327                            f64::midpoint(corners[i].0, corners[j].0),
4328                            f64::midpoint(corners[i].1, corners[j].1),
4329                        )
4330                    };
4331                    [(centre.x, centre.y), half(0, 1), half(1, 2), half(2, 0)]
4332                        .into_iter()
4333                        .filter_map(lift)
4334                        .any(|m| (m - a).dot(n).abs() > deflection.chord)
4335                };
4336                if sagged || middle_off() {
4337                    worst.push(SpadePoint::new(centre.x, centre.y));
4338                }
4339            }
4340            if worst.is_empty() {
4341                break;
4342            }
4343            let mut inserted = 0usize;
4344            for point in worst {
4345                // A centre that lands on a vertex already there is not a new
4346                // point. A sliver whose apex sits on its own base (three
4347                // grid points on a diagonal, the middle one a rounding off
4348                // the line) has its centre at that apex to the last bits,
4349                // and inserting it breeds a hair a few ulps wide, whose
4350                // centre is the same point again: round after round, a
4351                // stack of hairs the degenerate filter then drops, and a
4352                // hole in the face where they were.
4353                if lands_on_the_mesh(&cdt, point, extent * 1e-9, (1.0, 1.0)) {
4354                    continue;
4355                }
4356                cdt.insert(mitigate_underflow(point)).map_err(|e| {
4357                    ogeom_core::ogeom_err!(NotDone, "refinement insertion failed: {e}")
4358                })?;
4359                inserted += 1;
4360            }
4361            if inserted == 0 {
4362                // Everything that sagged was a hair on a vertex; another
4363                // round would find the same hairs.
4364                break;
4365            }
4366            if *MESH_DEBUG_REFINE {
4367                eprintln!(
4368                    "ROUND {rounds_run}: +{} vertices",
4369                    cdt.num_vertices() - before
4370                );
4371            }
4372        }
4373    }
4374
4375    let refine_ms = sub.elapsed().as_secs_f64() * 1e3;
4376    if *MESH_DEBUG_REFINE && boundary_ms + interior_ms + refine_ms > 50.0 {
4377        eprintln!(
4378            "SUB boundary {boundary_ms:.0}ms interior {interior_ms:.0}ms ({interior_points} verts) refine {refine_ms:.0}ms ({rounds_run} rounds, {} verts)",
4379            cdt.num_vertices()
4380        );
4381    }
4382    let mut parameters = Vec::new();
4383    let mut index_of = ogeom_core::FastMap::default();
4384    for (i, vertex) in cdt.vertices().enumerate() {
4385        let p = vertex.position();
4386        index_of.insert(vertex.fix(), i);
4387        parameters.push(
4388            exact
4389                .get(&(p.x.to_bits(), p.y.to_bits()))
4390                .copied()
4391                .unwrap_or_else(|| scale.from(p.x, p.y)),
4392        );
4393    }
4394
4395    let mut triangles = Vec::new();
4396    let (mut dbg_total, mut dbg_outside, mut dbg_degenerate) = (0usize, 0usize, 0usize);
4397    for triangle in cdt.inner_faces() {
4398        dbg_total += 1;
4399        let vertices = triangle.vertices();
4400        let centre = triangle.center();
4401        // A constrained Delaunay covers the convex hull of its input, so
4402        // triangles outside the trimmed region (across a concavity, or inside
4403        // a hole) have to be discarded. Winding tells them apart.
4404        if !bands.holds(Point2::new(centre.x, centre.y)) {
4405            dbg_outside += 1;
4406            continue;
4407        }
4408        // A boundary run whose points differ by last-bit noise (a chart row
4409        // whose corner came off a different pcurve than its interior) lets
4410        // the triangulation weave a hair of a triangle along it: chart area
4411        // measured in ulps, a centroid *on* the boundary that even-odd
4412        // counting places wherever rounding falls, and a lifted sliver that
4413        // spans the run in one spurious stroke. It bounds nothing; drop it.
4414        let area = {
4415            let [a, b, c] = [
4416                vertices[0].position(),
4417                vertices[1].position(),
4418                vertices[2].position(),
4419            ];
4420            ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() / 2.0
4421        };
4422        if area < degenerate_area {
4423            dbg_degenerate += 1;
4424            continue;
4425        }
4426        #[allow(clippy::cast_possible_truncation)]
4427        let indices = [
4428            index_of[&vertices[0].fix()] as u32,
4429            index_of[&vertices[1].fix()] as u32,
4430            index_of[&vertices[2].fix()] as u32,
4431        ];
4432        triangles.push(indices);
4433    }
4434
4435    if *MESH_DEBUG_REFINE {
4436        eprintln!(
4437            "FILTER {dbg_total} triangles: {dbg_outside} outside, {dbg_degenerate} degenerate (area < {degenerate_area:.3e}), {} kept",
4438            triangles.len()
4439        );
4440    }
4441    // No triangles is not refused here: a boundary drawn coarsely enough
4442    // to cross itself can enclose nothing at all (an annulus narrower
4443    // than the sag of its rims' polygons, two arcs a hair apart), and the
4444    // caller's answer to a crossing is to draw the edges finer and ask
4445    // again. Empty counts as short; only a face still empty after that is
4446    // refused, by [`triangulate_with`].
4447    Ok(PlanarMesh {
4448        parameters,
4449        triangles,
4450        boundary_triangles,
4451        crossed,
4452    })
4453}
4454
4455/// Add interior points wherever the surface deviates from flat by more than the
4456/// deflection allows.
4457///
4458/// The two parameter directions are refined *independently*, and that is not an
4459/// optimization; it is the difference between converging and not. A uniform
4460/// grid on a sphere puts as many meridians through the pole as through the
4461/// equator, so the triangles there become arbitrarily thin slivers in space.
4462/// The summed area of such a mesh does not approach the sphere's; it grows
4463/// without bound as the grid tightens. (Schwarz's lantern is the standard
4464/// example: an inscribed polyhedron whose area diverges under refinement.)
4465///
4466/// Refining each direction by its own measured sag fixes it at the source. Near
4467/// a pole the circle of latitude has almost no radius, so a chord right across
4468/// it sags by almost nothing and the direction stops subdividing after one or
4469/// two steps, while the meridian direction, whose curvature does not change,
4470/// keeps refining. The mesh degenerates into a fan, which is the right shape.
4471fn add_interior_points(
4472    cdt: &mut ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
4473    rings: &[Vec<Point2>],
4474    surface: &SurfaceGeometry,
4475    deflection: Deflection,
4476    scale: ChartScale,
4477    rows_kept: Rows,
4478    tol: Tolerances,
4479) -> OgeomResult<()> {
4480    // A plane is flat everywhere; sampling it would add points that buy nothing.
4481    if matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
4482        return Ok(());
4483    }
4484    // Up to half a million grid points each asking the whole boundary: the
4485    // banded index answers the same exact test from one band's edges.
4486    let region = RingBands::over(rings);
4487
4488    let bound = rings
4489        .iter()
4490        .flatten()
4491        .fold(ogeom_math::Aabb::EMPTY, |acc, p| {
4492            acc.with_point(Point::new(p.x, p.y, 0.0))
4493        });
4494    let (Some(low), Some(high)) = (bound.low(), bound.high()) else {
4495        return Ok(());
4496    };
4497    // Within this of a boundary vertex or segment is on it: a hair's width
4498    // at the scaled chart's scale, the same reach the repair pass keeps.
4499    let reach = ((high.x - low.x) * scale.su)
4500        .max((high.y - low.y) * scale.sv)
4501        .max(1.0)
4502        * 1e-9;
4503
4504    // The v resolution has to hold everywhere the region reaches, so its sag is
4505    // the worst over a spread of u probes rather than the sag along one line.
4506    // A surface of revolution is the same at every u and a lofted one is not.
4507    #[allow(clippy::cast_precision_loss)]
4508    let probes: Vec<f64> = (0..=U_PROBES)
4509        .map(|i| low.x + (high.x - low.x) * i as f64 / U_PROBES as f64)
4510        .collect();
4511    let (rows, row_samples) = refine_lines(
4512        surface,
4513        (low.y, high.y),
4514        &probes,
4515        |u, v| (u, v),
4516        deflection,
4517        tol,
4518    );
4519
4520    // Sag alone leaves a cylinder one row: it is straight along its axis, so
4521    // nothing along `v` ever sags. But the triangulation is Delaunay in the
4522    // chart, and a bore four hundred millimetres long with one row in the
4523    // middle hands it two-hundred-millimetre spans from each rim to that
4524    // row. Delaunay bridges those however it likes, and the repair below
4525    // fires only at three chords: a triangle a quarter turn wide on a two
4526    // millimetre bore sags less than that, so it stays, and the bore draws
4527    // as a square between its holes. Cells are held to a bounded aspect
4528    // instead: rows close enough, measured in space through the surface,
4529    // that no triangle between two rows can reach across more than a few
4530    // columns. Rows are added, never removed, and spread evenly, so a grid
4531    // that was symmetric stays symmetric. The same the other way round.
4532    // How far apart two chart points are in space, where the surface says.
4533    let span = |p: (f64, f64), q: (f64, f64)| -> f64 {
4534        use ogeom_geom::Surface as _;
4535        match (
4536            surface.point_at(p.0, p.1, tol),
4537            surface.point_at(q.0, q.1, tol),
4538        ) {
4539            (Ok(a), Ok(b)) => a.distance(b),
4540            _ => 0.0,
4541        }
4542    };
4543    // The columns along one row: the u resolution measured at that row.
4544    let columns_at = |v: f64| refine_line(surface, (low.x, high.x), |u| (u, v), deflection, tol);
4545    // A cone's circumference runs from nothing at its apex to its widest
4546    // at the other end, and Delaunay in a chart scaled by the mean bridges
4547    // the wide end's columns three at a time between rows held to six
4548    // cells: its rows are held to a cell and a half.
4549    let aspect = if matches!(surface.kind(), ogeom_geom::SurfaceKind::Cone) {
4550        CONE_CELL_ASPECT
4551    } else {
4552        CELL_ASPECT
4553    };
4554    // A surface straight along `v` (a drum, a cone, an extrusion) has its
4555    // columns held as constraints below instead: no triangle crosses one,
4556    // so none bridges columns, and rows are wanted only where it sags.
4557    let ruled = matches!(
4558        surface.kind(),
4559        ogeom_geom::SurfaceKind::Cylinder
4560            | ogeom_geom::SurfaceKind::Cone
4561            | ogeom_geom::SurfaceKind::Extrusion
4562    );
4563    // A drum's or cone's rows are its two ends where its rims allow it
4564    // (see `rim_holds`), its rulings drawn between them as constraints
4565    // below: a row inside adds a point per column and asks nothing of the
4566    // chord. Not where one end closes on an apex: its
4567    // columns all meet there, and Delaunay fans across the face from the
4568    // apex's few chart points.
4569    let apex = || {
4570        let um = f64::midpoint(low.x, high.x);
4571        let across = |v: f64| span((low.x, v), (um, v)) + span((um, v), (high.x, v));
4572        let (bottom, top) = (across(low.y), across(high.y));
4573        bottom.min(top) <= bottom.max(top) * 1e-3
4574    };
4575    let round = matches!(
4576        surface.kind(),
4577        ogeom_geom::SurfaceKind::Cylinder | ogeom_geom::SurfaceKind::Cone
4578    );
4579    let rows = if round && rows_kept == Rows::Ends && rows.len() > 2 && !apex() {
4580        vec![rows[0], rows[rows.len() - 1]]
4581    } else if ruled {
4582        rows
4583    } else {
4584        spread_to_aspect(
4585            rows,
4586            aspect,
4587            low.x,
4588            high.x,
4589            |v| columns_at(v).0.len(),
4590            |i| {
4591                row_samples[i]
4592                    .iter()
4593                    .zip(&row_samples[i + 1])
4594                    .map(|(a, b)| a.distance(b))
4595                    .fold(0.0_f64, f64::max)
4596            },
4597            |a, b, v| span((a, v), (b, v)),
4598        )
4599    };
4600
4601    if *MESH_DEBUG_REFINE {
4602        let v = f64::midpoint(low.y, high.y);
4603        let (columns, _) = columns_at(v);
4604        eprintln!(
4605            "GRID u [{:.3},{:.3}] v [{:.3},{:.3}]: {} rows after aspect, {} columns at the middle row, chord {}",
4606            low.x,
4607            high.x,
4608            low.y,
4609            high.y,
4610            rows.len(),
4611            columns.len(),
4612            deflection.chord
4613        );
4614    }
4615    // Each row and each column is held to the chord along its own line,
4616    // and a cell's middle stands off by both sags together: on a surface
4617    // curved both ways, a cell whose sides each sag the chord stands off
4618    // twice it across its diagonal. Where any cell's diagonal sags past
4619    // the chord, every cell gets its centre, which halves the diagonals a
4620    // triangle can take, and the grid's lines between neighbouring points
4621    // are held as constraints, so no triangle crosses a cell's side and
4622    // each cell is drawn alone: four triangles round its centre. A grid
4623    // line inside the chart then holds the triangles as the seam does, and
4624    // a symmetric grid stays symmetric. A surface straight one way never
4625    // asks.
4626    let lifted = |u: f64, v: f64| {
4627        use ogeom_geom::Surface as _;
4628        surface.point_at(u, v, tol).ok()
4629    };
4630    let cells: Vec<(f64, f64, f64, f64)> = rows
4631        .windows(2)
4632        .flat_map(|pair| {
4633            let (v0, v1) = (pair[0], pair[1]);
4634            let (columns, _) = columns_at(f64::midpoint(v0, v1));
4635            columns
4636                .windows(2)
4637                .map(|cell| (cell[0], cell[1], v0, v1))
4638                .collect::<Vec<_>>()
4639        })
4640        .collect();
4641    // A surface straight one way sags only across its rulings, which the
4642    // columns already hold.
4643    let sags = !ruled
4644        && cells.len() >= MIN_CENTRED_CELLS
4645        && cells.iter().any(|&(u0, u1, v0, v1)| {
4646            let (um, vm) = (f64::midpoint(u0, u1), f64::midpoint(v0, v1));
4647            match (
4648                lifted(u0, v0),
4649                lifted(u1, v1),
4650                lifted(u1, v0),
4651                lifted(u0, v1),
4652                lifted(um, vm),
4653            ) {
4654                (Some(a), Some(b), Some(c), Some(d), Some(m)) => {
4655                    chord_sag(a, b, m, tol).max(chord_sag(c, d, m, tol)) > deflection.chord
4656                }
4657                _ => false,
4658            }
4659        });
4660    let round_ruled = matches!(
4661        surface.kind(),
4662        ogeom_geom::SurfaceKind::Cylinder | ogeom_geom::SurfaceKind::Cone
4663    );
4664    let mut placed: Vec<Vec<(usize, f64, FixedVertexHandle)>> = Vec::new();
4665    let mut row_of: Vec<usize> = Vec::new();
4666    for (row, &v) in rows
4667        .iter()
4668        .enumerate()
4669        .take(rows.len().saturating_sub(1))
4670        .skip(1)
4671    {
4672        let mut here: Vec<(usize, f64, FixedVertexHandle)> = Vec::new();
4673        // The row gap either side of this row, the finer of the two: the
4674        // chart scale a keep-out band is measured against along `v`.
4675        let dv = (v - rows[row - 1]).abs().min((rows[row + 1] - v).abs());
4676        // Each row gets its own u resolution, measured at that row.
4677        let (columns, column_samples) = columns_at(v);
4678        // The same the other way round: a surface straight along `u` gets
4679        // two columns from sag, and a row a hundred millimetres wide would
4680        // bridge across the rows as badly as the bore bridged its columns.
4681        let columns = spread_to_aspect(
4682            columns,
4683            CELL_ASPECT,
4684            low.y,
4685            high.y,
4686            |_| rows.len(),
4687            |i| column_samples[i].distance(&column_samples[i + 1]),
4688            |a, b, u| span((u, a), (u, b)),
4689        );
4690        for (column, &u) in columns
4691            .iter()
4692            .enumerate()
4693            .take(columns.len().saturating_sub(1))
4694            .skip(1)
4695        {
4696            if round_ruled && rows_kept == Rows::Sparse && column % 4 != 0 {
4697                continue;
4698            }
4699            // Interior points only: the boundary is already constrained, and a
4700            // point landing just off a constraint would split it.
4701            if !region.holds(Point2::new(u, v)) {
4702                continue;
4703            }
4704            // Inside, and not *on* the boundary: a grid point can fall
4705            // exactly on a ring segment that runs diagonally across the
4706            // chart (the midpoint of two grid corners the ring happens to
4707            // join), and even-odd counting calls it inside. Inserted, it
4708            // splits that constraint on this face alone, and the face
4709            // across the edge is drawn to the unsplit segment.
4710            let point = scale.to(u, v);
4711            if lands_on_the_mesh(cdt, point, reach, (1.0, 1.0)) {
4712                continue;
4713            }
4714            // Nor *near* it, measured in cells. A grid point a sliver's
4715            // width from a boundary chord makes a triangle with that
4716            // chord's two ends that is thin in the chart and, lifted, is
4717            // not thin at all: the chord cuts across the curvature by its
4718            // sag and the point sits on the surface, so the triangle
4719            // stands off the surface as a fin whose normal is tangent to
4720            // it and whose sign is whichever way the sliver leaned. A face
4721            // shades with a crease along every such chord. The point is
4722            // left out and the boundary's own row of triangles reaches
4723            // to the next grid line instead.
4724            let du = (u - columns[column - 1])
4725                .abs()
4726                .min((columns[column + 1] - u).abs());
4727            if du > 0.0
4728                && dv > 0.0
4729                && lands_on_the_mesh(
4730                    cdt,
4731                    point,
4732                    KEEP_OUT,
4733                    (1.0 / (du * scale.su), 1.0 / (dv * scale.sv)),
4734                )
4735            {
4736                continue;
4737            }
4738            let handle = cdt
4739                .insert(mitigate_underflow(point))
4740                .map_err(|e| ogeom_core::ogeom_err!(NotDone, "interior insertion failed: {e}"))?;
4741            if sags {
4742                here.push((column, u, handle));
4743            }
4744        }
4745        if sags {
4746            // Along the row, between neighbouring columns.
4747            for pair in here.windows(2) {
4748                if pair[1].0 == pair[0].0 + 1 && cdt.can_add_constraint(pair[0].2, pair[1].2) {
4749                    cdt.add_constraint(pair[0].2, pair[1].2);
4750                }
4751            }
4752            // Across to the row before, at the same column.
4753            if let (Some(before), Some(&last)) = (placed.last(), row_of.last())
4754                && last + 1 == row
4755            {
4756                for &(_, u, handle) in &here {
4757                    if let Some(&(_, _, above)) =
4758                        before.iter().find(|p| p.1.to_bits() == u.to_bits())
4759                        && cdt.can_add_constraint(above, handle)
4760                    {
4761                        cdt.add_constraint(above, handle);
4762                    }
4763                }
4764            }
4765            // To the boundary where the grid line meets it at a vertex: a
4766            // row's ends on the chart's end columns, the first and last
4767            // rows' points on the end rows.
4768            let edge = |cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>, u: f64, v: f64| {
4769                cdt.locate_vertex(mitigate_underflow(scale.to(u, v)))
4770                    .map(|h| h.fix())
4771            };
4772            let mut ties: Vec<(FixedVertexHandle, FixedVertexHandle)> = Vec::new();
4773            if let (Some(&(first, _, start)), Some(&(last, _, end))) = (here.first(), here.last()) {
4774                if first == 1
4775                    && let Some(b) = edge(cdt, columns[0], v)
4776                {
4777                    ties.push((b, start));
4778                }
4779                if last + 2 == columns.len()
4780                    && let Some(b) = edge(cdt, columns[columns.len() - 1], v)
4781                {
4782                    ties.push((end, b));
4783                }
4784            }
4785            for &(_, u, handle) in &here {
4786                if row == 1
4787                    && let Some(b) = edge(cdt, u, rows[0])
4788                {
4789                    ties.push((b, handle));
4790                }
4791                if row + 2 == rows.len()
4792                    && let Some(b) = edge(cdt, u, rows[rows.len() - 1])
4793                {
4794                    ties.push((handle, b));
4795                }
4796            }
4797            for (a, b) in ties {
4798                if a != b && cdt.can_add_constraint(a, b) {
4799                    cdt.add_constraint(a, b);
4800                }
4801            }
4802            placed.push(here);
4803            row_of.push(row);
4804        }
4805    }
4806    if sags {
4807        for (u0, u1, v0, v1) in cells {
4808            let (um, vm) = (f64::midpoint(u0, u1), f64::midpoint(v0, v1));
4809            if !region.holds(Point2::new(um, vm)) {
4810                continue;
4811            }
4812            // A cell whose side collapses to a point (a pole, an apex) is a
4813            // fan's sliver already, and a centre there lands wherever the
4814            // collapse left the columns.
4815            let collapsed = |v: f64| match (lifted(u0, v), lifted(u1, v)) {
4816                (Some(a), Some(b)) => a.distance(b) <= tol.confusion(),
4817                _ => true,
4818            };
4819            if collapsed(v0) || collapsed(v1) {
4820                continue;
4821            }
4822            let point = scale.to(um, vm);
4823            let (du, dv) = ((u1 - u0) * 0.5, (v1 - v0) * 0.5);
4824            if lands_on_the_mesh(cdt, point, reach, (1.0, 1.0))
4825                || (du > 0.0
4826                    && dv > 0.0
4827                    && lands_on_the_mesh(
4828                        cdt,
4829                        point,
4830                        KEEP_OUT,
4831                        (1.0 / (du * scale.su), 1.0 / (dv * scale.sv)),
4832                    ))
4833            {
4834                continue;
4835            }
4836            cdt.insert(mitigate_underflow(point))
4837                .map_err(|e| ogeom_core::ogeom_err!(NotDone, "interior insertion failed: {e}"))?;
4838        }
4839    }
4840    if ruled {
4841        // Each column, where it runs inside the face, a constraint from a
4842        // keep-out's width inside the boundary to as far inside the other
4843        // end: the triangles between two columns stay between them.
4844        let (bottom, _) = columns_at(low.y);
4845        let (top, _) = columns_at(high.y);
4846        let columns = if bottom.len() >= top.len() {
4847            bottom
4848        } else {
4849            top
4850        };
4851        // The points already in, by chart position across: a column runs
4852        // through the grid's points on it, a rounding off each, and is
4853        // held through each of them rather than past it, which would leave
4854        // the point on the column's far side a hair from its line.
4855        let mut across: Vec<(f64, f64, FixedVertexHandle)> = cdt
4856            .vertices()
4857            .map(|v| (v.position().x, v.position().y, v.fix()))
4858            .collect();
4859        across.sort_by(|a, b| a.0.total_cmp(&b.0));
4860        for (k, &u) in columns
4861            .iter()
4862            .enumerate()
4863            .take(columns.len().saturating_sub(1))
4864            .skip(1)
4865        {
4866            let du = (u - columns[k - 1]).abs().min((columns[k + 1] - u).abs());
4867            let margin = KEEP_OUT * du * scale.su / scale.sv;
4868            // Where the column crosses the boundary, and the boundary's
4869            // vertex there when the column runs through one: a column drawn
4870            // a rounding off a vertex still ends on it.
4871            let snap = du * 1e-6;
4872            let mut crossings: Vec<(f64, Option<Point2>)> = Vec::new();
4873            for ring in rings {
4874                for (i, &p) in ring.iter().enumerate() {
4875                    let q = ring[(i + 1) % ring.len()];
4876                    if (p.x < u) != (q.x < u) {
4877                        let y = p.y + (u - p.x) / (q.x - p.x) * (q.y - p.y);
4878                        let on = [p, q].into_iter().find(|w| (w.x - u).abs() <= snap);
4879                        crossings.push((on.map_or(y, |w| w.y), on));
4880                    }
4881                }
4882            }
4883            crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
4884            for pair in crossings.windows(2) {
4885                let (low_end, high_end) = (pair[0], pair[1]);
4886                if !region.holds(Point2::new(u, f64::midpoint(low_end.0, high_end.0))) {
4887                    continue;
4888                }
4889                // A stretch short against the columns' spacing is one Delaunay
4890                // draws between neighbouring columns anyway.
4891                if (high_end.0 - low_end.0) * scale.sv <= CELL_ASPECT * du * scale.su {
4892                    continue;
4893                }
4894                let pair = [low_end.0, high_end.0];
4895                let mut ends = Vec::with_capacity(2);
4896                // From the boundary's own vertex where the column meets one,
4897                // else from a keep-out's width inside it.
4898                for ((at, on), inward) in [(low_end, margin), (high_end, -margin)] {
4899                    let vertex = on.unwrap_or(Point2::new(u, at));
4900                    if let Some(h) =
4901                        cdt.locate_vertex(mitigate_underflow(scale.to(vertex.x, vertex.y)))
4902                    {
4903                        ends.push(h.fix());
4904                        continue;
4905                    }
4906                    let v = at + inward;
4907                    if (v - f64::midpoint(pair[0], pair[1])).abs()
4908                        >= (pair[1] - pair[0]).abs() * 0.5
4909                    {
4910                        break;
4911                    }
4912                    // A boundary whose pcurves reach past the surface's
4913                    // domain leaves no point there to end on.
4914                    if surface.point_at(u, v, tol).is_err() {
4915                        break;
4916                    }
4917                    let point = mitigate_underflow(scale.to(u, v));
4918                    let handle = match cdt.locate_vertex(point) {
4919                        Some(h) => h.fix(),
4920                        None if !lands_on_the_mesh(cdt, point, reach, (1.0, 1.0)) => {
4921                            cdt.insert(point).map_err(|e| {
4922                                ogeom_core::ogeom_err!(NotDone, "column insertion failed: {e}")
4923                            })?
4924                        }
4925                        None => break,
4926                    };
4927                    ends.push(handle);
4928                }
4929                let [a, b] = ends[..] else {
4930                    continue;
4931                };
4932                if a == b {
4933                    continue;
4934                }
4935                let (ya, yb) = (cdt.vertex(a).position().y, cdt.vertex(b).position().y);
4936                let (x, near) = (u * scale.su, 1e-3 * du * scale.su);
4937                let first = across.partition_point(|p| p.0 < x - near);
4938                let mut through: Vec<(f64, FixedVertexHandle)> = across[first..]
4939                    .iter()
4940                    .take_while(|p| p.0 <= x + near)
4941                    .filter(|p| p.1 > ya.min(yb) && p.1 < ya.max(yb))
4942                    .map(|p| (p.1, p.2))
4943                    .collect();
4944                through.sort_by(|p, q| p.0.total_cmp(&q.0));
4945                if ya > yb {
4946                    through.reverse();
4947                }
4948                let chain: Vec<FixedVertexHandle> = core::iter::once(a)
4949                    .chain(through.into_iter().map(|p| p.1))
4950                    .chain(core::iter::once(b))
4951                    .collect();
4952                for link in chain.windows(2) {
4953                    if link[0] != link[1] && cdt.can_add_constraint(link[0], link[1]) {
4954                        cdt.add_constraint(link[0], link[1]);
4955                    }
4956                }
4957            }
4958        }
4959    }
4960    Ok(())
4961}
4962
4963/// Whether segments `a..b` and `c..d` cross properly: at a point interior
4964/// to both, neither touching the other's end.
4965pub(crate) fn segments_cross(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
4966    let orient =
4967        |p: Point2, q: Point2, r: Point2| (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
4968    let (o1, o2) = (orient(a, b, c), orient(a, b, d));
4969    let (o3, o4) = (orient(c, d, a), orient(c, d, b));
4970    o1 != 0.0
4971        && o2 != 0.0
4972        && o3 != 0.0
4973        && o4 != 0.0
4974        && (o1 > 0.0) != (o2 > 0.0)
4975        && (o3 > 0.0) != (o4 > 0.0)
4976}
4977
4978/// How many of the triangulation's faces lie inside its constraints, told
4979/// by parity rather than by geometry.
4980///
4981/// Walking from a face on the convex hull, which is outside, every
4982/// constraint edge crossed flips inside for outside. Asked of the
4983/// triangulation of a boundary and nothing else, this is exact where the
4984/// even-odd test of a triangle's centre is not: a sliver face triangulates
4985/// to hairs whose centres sit on the boundary to the last bit, and which
4986/// side rounding puts them is a coin toss. `None` when the walk reaches a
4987/// face both ways with different answers: the constraints do not enclose
4988/// consistently, which is a crossing by another name.
4989fn inside_by_parity(cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>) -> Option<usize> {
4990    use ogeom_core::FastMap;
4991    let mut parity: FastMap<spade::handles::FixedFaceHandle<spade::handles::InnerTag>, bool> =
4992        FastMap::with_capacity_and_hasher(cdt.num_inner_faces(), Default::default());
4993    let mut queue = Vec::new();
4994    for hull in cdt.convex_hull() {
4995        // The hull edge's far side is the outer face; its near side is a
4996        // face of the triangulation, outside unless the hull edge itself
4997        // is a boundary.
4998        let Some(face) = hull.rev().face().as_inner() else {
4999            continue;
5000        };
5001        let inside = hull.is_constraint_edge();
5002        match parity.get(&face.fix()) {
5003            Some(&known) if known != inside => return None,
5004            Some(_) => {}
5005            None => {
5006                parity.insert(face.fix(), inside);
5007                queue.push((face.fix(), inside));
5008            }
5009        }
5010    }
5011    while let Some((face, inside)) = queue.pop() {
5012        for edge in cdt.face(face).adjacent_edges() {
5013            let Some(next) = edge.rev().face().as_inner() else {
5014                continue;
5015            };
5016            let next_inside = inside != edge.is_constraint_edge();
5017            match parity.get(&next.fix()) {
5018                Some(&known) if known != next_inside => return None,
5019                Some(_) => {}
5020                None => {
5021                    parity.insert(next.fix(), next_inside);
5022                    queue.push((next.fix(), next_inside));
5023                }
5024            }
5025        }
5026    }
5027    Some(parity.values().filter(|&&inside| inside).count())
5028}
5029
5030/// Whether `point` sits within `reach` of a vertex the triangulation has,
5031/// or of one of its constraint edges.
5032///
5033/// Asked of whatever the point lands on (a vertex, an edge's two ends, a
5034/// face's three corners and whichever of its sides are constraints),
5035/// which is where anything that close must be. A point on a vertex is not
5036/// a new point; a point on a constraint would split it, and a boundary
5037/// split on one face only is a crack against the face across it.
5038fn lands_on_the_mesh(
5039    cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
5040    point: SpadePoint<f64>,
5041    reach: f64,
5042    scale: (f64, f64),
5043) -> bool {
5044    use spade::PositionInTriangulation as At;
5045    // Distances in a chart scaled per axis: `scale` is one over the local
5046    // grid step each way, so `reach` reads in cells, whatever the chart's
5047    // own units: one face's `u` runs over a fiftieth of its `v`.
5048    let scaled = |p: SpadePoint<f64>| ((p.x - point.x) * scale.0, (p.y - point.y) * scale.1);
5049    let near = |v: SpadePoint<f64>| {
5050        let (x, y) = scaled(v);
5051        x.hypot(y) <= reach
5052    };
5053    let along = |a: SpadePoint<f64>, b: SpadePoint<f64>| {
5054        // Distance to the segment `a..b`, the point at the origin.
5055        let (ax, ay) = scaled(a);
5056        let (bx, by) = scaled(b);
5057        let (dx, dy) = (bx - ax, by - ay);
5058        let len2 = dx * dx + dy * dy;
5059        let t = if len2 > 0.0 {
5060            ((-ax * dx - ay * dy) / len2).clamp(0.0, 1.0)
5061        } else {
5062            0.0
5063        };
5064        (ax + t * dx).hypot(ay + t * dy) <= reach
5065    };
5066    match cdt.locate(point) {
5067        At::OnVertex(_) => true,
5068        At::OnEdge(edge) => {
5069            let edge = cdt.directed_edge(edge);
5070            edge.is_constraint_edge() || edge.vertices().iter().any(|v| near(v.position()))
5071        }
5072        At::OnFace(face) => {
5073            let face = cdt.face(face);
5074            face.vertices().iter().any(|v| near(v.position()))
5075                || face.adjacent_edges().iter().any(|e| {
5076                    e.is_constraint_edge() && along(e.from().position(), e.to().position())
5077                })
5078        }
5079        At::OutsideOfConvexHull(_) | At::NoTriangulation => false,
5080    }
5081}
5082
5083/// How close to the boundary, in grid cells, an interior point may sit.
5084///
5085/// Closer than this the triangle between the point and a boundary chord
5086/// is a sliver in the chart and a fin in space; at this and beyond the
5087/// boundary's own row of triangles is at least this tall against the
5088/// chord, and lifts as a facet on the surface rather than off it.
5089const KEEP_OUT: f64 = 0.35;
5090
5091/// How many column widths a grid cell may be tall before rows are added.
5092///
5093/// Delaunay in the chart connects nearest neighbours in the chart; held to
5094/// this aspect, a cell's nearest neighbours across a row gap are the same
5095/// columns, not columns several away, and the triangles between rows stay
5096/// as narrow as the columns are.
5097const CELL_ASPECT: f64 = 6.0;
5098
5099/// [`CELL_ASPECT`] for a cone's rows: see where it is used.
5100const CONE_CELL_ASPECT: f64 = 1.5;
5101
5102/// Grid lines in one direction spread so that no cell is longer, in
5103/// space, than `aspect` times its width the other way: extra
5104/// lines added evenly between the ones sag chose.
5105///
5106/// Written for rows against columns and used both ways round. `lines`
5107/// are the parameters sag chose in this direction; `lo..hi` is the chart
5108/// range the other way; `crossings_at(t)` counts the lines the other way
5109/// at parameter `t` of this one; `length(i)` is the extent in space
5110/// between lines `i` and `i + 1` of this direction; `width(a, b, t)` is
5111/// the extent in space between two parameters of the other direction,
5112/// along this one's line `t`.
5113fn spread_to_aspect(
5114    lines: Vec<f64>,
5115    aspect: f64,
5116    lo: f64,
5117    hi: f64,
5118    crossings_at: impl Fn(f64) -> usize,
5119    length: impl Fn(usize) -> f64,
5120    width: impl Fn(f64, f64, f64) -> f64,
5121) -> Vec<f64> {
5122    if lines.len() < 2 {
5123        return lines;
5124    }
5125    let mid = f64::midpoint(lines[0], lines[lines.len() - 1]);
5126    let crossings = crossings_at(mid);
5127    if crossings < 4 {
5128        // Flat the other way as well: a plane in all but name, and nothing
5129        // to hold an aspect against.
5130        return lines;
5131    }
5132    // One cell's width, not the whole range's: across a closed direction
5133    // the whole range comes back to its own start and measures nothing.
5134    // Read at the middle line, and never narrower than the mean of the
5135    // widths a quarter, half and three quarters of the way along: the
5136    // triangulation is Delaunay in a chart scaled by the mean tangent
5137    // lengths, so the mean is what it bridges against. The middle line
5138    // alone can be a parallel shrinking to a point, such as the inner
5139    // equator of a torus whose hole nearly closes, and would spread every
5140    // line to that parallel's width.
5141    #[allow(clippy::cast_precision_loss)]
5142    let step = (hi - lo) / (crossings - 1) as f64;
5143    let (first, last) = (lines[0], lines[lines.len() - 1]);
5144    let quarter = (last - first) * 0.25;
5145    let widths = [first + quarter, mid, last - quarter].map(|t| width(lo, lo + step, t));
5146    let cell = widths[1].max(widths.iter().sum::<f64>() / 3.0);
5147    if cell.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
5148        return lines;
5149    }
5150    let mut out = Vec::with_capacity(lines.len());
5151    for (i, pair) in lines.windows(2).enumerate() {
5152        let (a, b) = (pair[0], pair[1]);
5153        out.push(a);
5154        let tall = length(i);
5155        let pieces = (tall / (aspect * cell)).ceil();
5156        if pieces.is_finite() && pieces > 1.0 {
5157            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5158            let n = (pieces as usize).min(MAX_DIRECTION_STEPS);
5159            #[allow(clippy::cast_precision_loss)]
5160            for i in 1..n {
5161                out.push(a + (b - a) * i as f64 / n as f64);
5162            }
5163        }
5164    }
5165    out.push(lines[lines.len() - 1]);
5166    out
5167}
5168
5169/// How many places across the domain the v resolution is measured at.
5170const U_PROBES: usize = 8;
5171
5172/// How many rounds of sag-driven refinement a region may take.
5173///
5174/// Each round halves the worst sag roughly; a surface not honest after this
5175/// many is degenerate, and the cap makes that a coarse mesh rather than an
5176/// exhausted allocator.
5177const REFINEMENT_ROUNDS: usize = 12;
5178
5179/// How many chords a chart edge's lift may stand off the line through its
5180/// ends before the repair splits the triangles along it.
5181const REPAIR_SAG: f64 = 3.0;
5182
5183/// The fewest grid cells a face has for its cells to be given centres.
5184///
5185/// A face of fewer (a fillet's sliver, a small corner ball) keeps its grid's
5186/// triangles as drawn, and its middle may stand off by up to twice the
5187/// chord: such a face is drawn mostly by the rims it shares, and its
5188/// neighbours, and a mesh read back into faces, are drawn to agree with the
5189/// grid's own triangles there.
5190const MIN_CENTRED_CELLS: usize = 128;
5191
5192/// The fewest subdivisions one parameter direction may take before its cap
5193/// stops it: the caller's [`Deflection::max_segments`] where that is more.
5194///
5195/// A surface that has not converged by then has a singularity, not a
5196/// resolution problem, and the ceiling is what makes that a coarse mesh
5197/// rather than an exhausted allocator.
5198const MAX_DIRECTION_STEPS: usize = 512;
5199
5200/// How many subdivisions one parameter direction may take at `deflection`.
5201fn direction_steps(deflection: Deflection) -> usize {
5202    deflection.max_segments.max(MAX_DIRECTION_STEPS)
5203}
5204
5205/// Subdivide `[lo, hi]` until no sub-interval errs further than `chord`,
5206/// or there are `cap` values.
5207///
5208/// The same adaptive bisection [`discretize`] uses on a curve, applied to a
5209/// line through parameter space. Each value is sampled once, by `sample`;
5210/// an interval is judged by `error` from its two ends' samples and what
5211/// `middle` gives at its midpoint, and a split keeps that middle, finished
5212/// by `promote`.
5213///
5214/// An interval's verdict depends on its own two ends alone, so the order
5215/// splits are made in changes nothing until the cap: the values come out
5216/// the same however the work is ordered. The splits go in rounds, every
5217/// interval that errs split at once, so a direction the cap stops is
5218/// refined evenly along its whole length; a round the cap cuts short
5219/// splits the intervals erring most.
5220fn refine_direction<S, M>(
5221    (lo, hi): (f64, f64),
5222    chord: f64,
5223    cap: usize,
5224    sample: impl Fn(f64) -> S,
5225    middle: impl Fn(f64) -> M,
5226    error: impl Fn(&S, &S, &M) -> f64,
5227    promote: impl Fn(f64, M) -> S,
5228) -> (Vec<f64>, Vec<S>) {
5229    let centre = f64::midpoint(lo, hi);
5230    let mut values = vec![lo, centre, hi];
5231    let mut samples: Vec<S> = vec![sample(lo), sample(centre), sample(hi)];
5232    // Whether the interval starting at each value is known to be within
5233    // the chord: its ends never move, so it stays known.
5234    let mut settled = vec![false; values.len() - 1];
5235    loop {
5236        // Each interval still open, judged once: (index, error, middle).
5237        let mut erring: Vec<(usize, f64, f64, M)> = Vec::new();
5238        for i in 0..settled.len() {
5239            if settled[i] {
5240                continue;
5241            }
5242            let mid = f64::midpoint(values[i], values[i + 1]);
5243            let at_mid = middle(mid);
5244            let err = error(&samples[i], &samples[i + 1], &at_mid);
5245            // A split that does not divide the interval means the
5246            // parameters have reached the resolution of f64.
5247            if err <= chord || mid <= values[i] || mid >= values[i + 1] {
5248                settled[i] = true;
5249                continue;
5250            }
5251            erring.push((i, err, mid, at_mid));
5252        }
5253        if erring.is_empty() {
5254            break;
5255        }
5256        let room = cap.saturating_sub(values.len());
5257        if room == 0 {
5258            break;
5259        }
5260        if erring.len() > room {
5261            erring.sort_by(|x, y| y.1.total_cmp(&x.1).then(x.0.cmp(&y.0)));
5262            erring.truncate(room);
5263            erring.sort_by_key(|e| e.0);
5264        }
5265        let mut grown_values = Vec::with_capacity(values.len() + erring.len());
5266        let mut grown_samples = Vec::with_capacity(values.len() + erring.len());
5267        let mut grown_settled = Vec::with_capacity(settled.len() + erring.len());
5268        let mut splits = erring.into_iter().peekable();
5269        for (i, (value, sampled)) in values.into_iter().zip(samples).enumerate() {
5270            grown_values.push(value);
5271            grown_samples.push(sampled);
5272            if i == settled.len() {
5273                continue;
5274            }
5275            match splits.next_if(|e| e.0 == i) {
5276                Some((_, _, mid, at_mid)) => {
5277                    grown_settled.extend([false, false]);
5278                    grown_values.push(mid);
5279                    grown_samples.push(promote(mid, at_mid));
5280                }
5281                None => grown_settled.push(settled[i]),
5282            }
5283        }
5284        values = grown_values;
5285        samples = grown_samples;
5286        settled = grown_settled;
5287    }
5288    (values, samples)
5289}
5290
5291/// What a grid line knows of the surface at one of its parameters: the
5292/// point and the normal there, where the surface gives them.
5293#[derive(Clone, Copy)]
5294struct GridSample {
5295    point: Option<Point>,
5296    normal: Option<Direction>,
5297}
5298
5299impl GridSample {
5300    fn at(surface: &SurfaceGeometry, uv: (f64, f64), tol: Tolerances) -> Self {
5301        Self {
5302            point: Self::point_at(surface, uv, tol),
5303            normal: Self::normal_at(surface, uv, tol),
5304        }
5305    }
5306
5307    fn point_at(surface: &SurfaceGeometry, (u, v): (f64, f64), tol: Tolerances) -> Option<Point> {
5308        surface.point_at(u, v, tol).ok()
5309    }
5310
5311    fn normal_at(
5312        surface: &SurfaceGeometry,
5313        (u, v): (f64, f64),
5314        tol: Tolerances,
5315    ) -> Option<Direction> {
5316        surface.normal_at(u, v, tol).ok()
5317    }
5318
5319    /// How far apart two samples are in space; nothing where either has no
5320    /// point.
5321    fn distance(&self, other: &Self) -> f64 {
5322        match (self.point, other.point) {
5323            (Some(a), Some(b)) => a.distance(b),
5324            _ => 0.0,
5325        }
5326    }
5327}
5328
5329/// A grid line through `range` of one chart direction, refined until no
5330/// cell edge along it is further from honest than the chord (see
5331/// [`cell_error`]), with the line's samples at every value. `at(t)` is the
5332/// chart point at `t` along the line.
5333fn refine_line(
5334    surface: &SurfaceGeometry,
5335    range: (f64, f64),
5336    at: impl Fn(f64) -> (f64, f64),
5337    deflection: Deflection,
5338    tol: Tolerances,
5339) -> (Vec<f64>, Vec<GridSample>) {
5340    refine_direction(
5341        range,
5342        deflection.chord,
5343        direction_steps(deflection),
5344        |t| GridSample::at(surface, at(t), tol),
5345        |t| GridSample::point_at(surface, at(t), tol),
5346        |a, b, m| cell_error(a, b, *m, deflection, tol),
5347        |t, point| GridSample {
5348            point,
5349            normal: GridSample::normal_at(surface, at(t), tol),
5350        },
5351    )
5352}
5353
5354/// Fractions along the straight chart run from `from` to `to`, from 0 to
5355/// 1: eight even steps, each halved until no step along the run is
5356/// further from honest than the chord (see [`cell_error`]), or the run
5357/// holds as many steps as one grid direction may.
5358fn joining_run(
5359    surface: &SurfaceGeometry,
5360    from: Point2,
5361    to: Point2,
5362    deflection: Deflection,
5363    tol: Tolerances,
5364) -> Vec<f64> {
5365    const EVEN_STEPS: u32 = 8;
5366    let at = |f: f64| (from.x + (to.x - from.x) * f, from.y + (to.y - from.y) * f);
5367    let mut fractions: Vec<f64> = (0..=EVEN_STEPS)
5368        .map(|k| f64::from(k) / f64::from(EVEN_STEPS))
5369        .collect();
5370    let mut samples: Vec<GridSample> = fractions
5371        .iter()
5372        .map(|&f| GridSample::at(surface, at(f), tol))
5373        .collect();
5374    let cap = direction_steps(deflection);
5375    loop {
5376        let mut grown = Vec::with_capacity(fractions.len() * 2);
5377        let mut grown_samples = Vec::with_capacity(fractions.len() * 2);
5378        let mut split = false;
5379        for i in 0..fractions.len() {
5380            grown.push(fractions[i]);
5381            grown_samples.push(samples[i]);
5382            let Some(&next) = fractions.get(i + 1) else {
5383                continue;
5384            };
5385            let mid = f64::midpoint(fractions[i], next);
5386            if mid <= fractions[i] || mid >= next {
5387                continue;
5388            }
5389            let middle = GridSample::at(surface, at(mid), tol);
5390            if cell_error(&samples[i], &samples[i + 1], middle.point, deflection, tol)
5391                > deflection.chord
5392            {
5393                grown.push(mid);
5394                grown_samples.push(middle);
5395                split = true;
5396            }
5397        }
5398        if !split || grown.len() > cap {
5399            break;
5400        }
5401        fractions = grown;
5402        samples = grown_samples;
5403    }
5404    fractions
5405}
5406
5407/// As [`refine_line`] over several lines at once, sharing their values:
5408/// `lines` are the lines by their parameter in the other direction,
5409/// `at(line, t)` the chart point at `t` along one of them, and an
5410/// interval's error the worst across them.
5411fn refine_lines(
5412    surface: &SurfaceGeometry,
5413    range: (f64, f64),
5414    lines: &[f64],
5415    at: impl Fn(f64, f64) -> (f64, f64),
5416    deflection: Deflection,
5417    tol: Tolerances,
5418) -> (Vec<f64>, Vec<Vec<GridSample>>) {
5419    refine_direction(
5420        range,
5421        deflection.chord,
5422        direction_steps(deflection),
5423        |t| {
5424            lines
5425                .iter()
5426                .map(|&line| GridSample::at(surface, at(line, t), tol))
5427                .collect::<Vec<_>>()
5428        },
5429        |t| {
5430            lines
5431                .iter()
5432                .map(|&line| GridSample::point_at(surface, at(line, t), tol))
5433                .collect::<Vec<_>>()
5434        },
5435        |a, b, m| {
5436            a.iter()
5437                .zip(b)
5438                .zip(m)
5439                .map(|((a, b), m)| cell_error(a, b, *m, deflection, tol))
5440                .fold(0.0_f64, f64::max)
5441        },
5442        |t, points| {
5443            lines
5444                .iter()
5445                .zip(points)
5446                .map(|(&line, point)| GridSample {
5447                    point,
5448                    normal: GridSample::normal_at(surface, at(line, t), tol),
5449                })
5450                .collect()
5451        },
5452    )
5453}
5454
5455/// How far a grid cell's edge is from honest, as a sag: the chord sag
5456/// itself, or the normal's turn across it scaled so that a turn of the
5457/// angular deflection weighs the same as a sag of the chord, whichever
5458/// is worse. From the edge's two ends and the surface's point at its
5459/// middle; the sag is nothing where any of the three is missing.
5460///
5461/// The chord alone is what the boundary's edges are *not* drawn to: a
5462/// curve is discretized to both deflections, so a bore's rims come out
5463/// round at the angular limit while columns held to the chord alone come
5464/// out a polygon of far fewer sides, and the bore changes shape a chord's
5465/// length in from each rim. The interior is held to the same two limits
5466/// the boundary is.
5467fn cell_error(
5468    from: &GridSample,
5469    to: &GridSample,
5470    middle: Option<Point>,
5471    deflection: Deflection,
5472    tol: Tolerances,
5473) -> f64 {
5474    let sag = match (from.point, to.point, middle) {
5475        (Some(a), Some(b), Some(m)) => chord_sag(a, b, m, tol),
5476        // Off the surface's domain; nothing to refine towards.
5477        _ => 0.0,
5478    };
5479    let turn = match (from.normal, to.normal) {
5480        (Some(a), Some(b)) => a.angle(b),
5481        // A pole or an apex has no normal to compare; the sag still governs.
5482        _ => 0.0,
5483    };
5484    sag.max(turn / deflection.angular * deflection.chord)
5485}
5486
5487/// How far `m` stands off the chord from `a` to `b`.
5488///
5489/// Measured in space, which is the only place the number means anything: the
5490/// same step in `u` covers a metre at a sphere's equator and a millimetre near
5491/// its pole.
5492fn chord_sag(a: Point, b: Point, m: Point, tol: Tolerances) -> f64 {
5493    ogeom_math::Axis::through(a, b, tol).map_or_else(|_| a.distance(m), |axis| axis.distance_to(m))
5494}
5495
5496/// Whether a point is inside the region the rings bound.
5497#[cfg(test)]
5498fn inside_region(rings: &[Vec<Point2>], p: Point2) -> bool {
5499    inside_boundary_with::<Exact>(rings, p)
5500}
5501
5502/// Ray-crossing count for one ring, decided by orientation rather than by
5503/// arithmetic.
5504///
5505/// A horizontal ray in `+u`. The half-open `y` comparison counts a vertex lying
5506/// exactly on the ray once rather than twice or not at all; which side of the
5507/// edge the point falls on is then an `orient2d` sign.
5508///
5509/// Deliberately *not* "solve for where the edge crosses the ray, then compare".
5510/// That form divides by the edge's `y` extent, which is near zero for a nearly
5511/// horizontal edge, and subtracts two nearly equal numbers to compare, so for a
5512/// point close to the boundary it can answer either way. Here there is no
5513/// division and the comparison is a determinant's sign, which an exact predicate
5514/// gets right at any separation.
5515fn crosses_odd_times<P: Predicates>(ring: &[Point2], p: Point2) -> bool {
5516    let mut inside = false;
5517    let n = ring.len();
5518    let at = |q: Point2| [q.x, q.y];
5519    for i in 0..n {
5520        let (a, b) = (ring[i], ring[(i + 1) % n]);
5521        if (a.y > p.y) == (b.y > p.y) {
5522            continue;
5523        }
5524        // The edge crosses the ray's line. Whether it crosses the ray *itself*
5525        // (to the right of `p`) is which side of the directed edge `p` is on,
5526        // read the right way round for the edge's direction in `y`.
5527        let side = P::orient2d(at(a), at(b), [p.x, p.y]);
5528        let rightwards = if b.y > a.y {
5529            side == ogeom_core::Sign::Positive
5530        } else {
5531            side == ogeom_core::Sign::Negative
5532        };
5533        if rightwards {
5534            inside = !inside;
5535        }
5536    }
5537    inside
5538}
5539
5540/// Whether to report, per shape, how many faces were drawn again finer.
5541///
5542/// Read once: `env::var` takes a process-wide lock and allocates its answer,
5543/// and this is asked per face.
5544static MESH_DEBUG_REFINE: std::sync::LazyLock<bool> =
5545    std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG_REFINE").is_ok());
5546
5547/// Whether the mesh debug dump is on, read once for the same reason.
5548static MESH_DEBUG: std::sync::LazyLock<bool> =
5549    std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG").is_ok());
5550
5551/// The ring edges that can cross a horizontal ray, bucketed by height.
5552///
5553/// [`crosses_odd_times`] walks every edge of every ring for each point it is
5554/// asked about. That is fine for a handful of queries and ruinous for the
5555/// refinement loop, which asks once per triangle per round while the rings
5556/// themselves never change: a face with 544 boundary points and 41 000
5557/// triangles pays a quarter of a billion edge visits, nearly all on edges
5558/// nowhere near the point.
5559///
5560/// An edge can only straddle a ray at height `y` if `y` lies within the edge's
5561/// own `y` span, so bucketing edges by that span and querying one bucket tests
5562/// a conservative superset of the edges that could contribute. **The answer is
5563/// therefore identical**: the same straddle test and the same exact predicate
5564/// decide each candidate; the index only declines to visit edges that could
5565/// not have counted.
5566///
5567/// Parity is taken over all rings at once, which is what
5568/// [`inside_boundary_with`] computes as an exclusive-or of per-ring parities:
5569/// the two agree because the parity of the total crossing count is the sum of
5570/// the rings' parities.
5571struct RingBands {
5572    /// Every ring's edges, flattened.
5573    edges: Vec<(Point2, Point2)>,
5574    /// Edge indices per band, low `y` first.
5575    bands: Vec<Vec<u32>>,
5576    low: f64,
5577    high: f64,
5578    /// Band height. Zero when every point shares one `y`, which leaves a
5579    /// single band holding everything.
5580    step: f64,
5581}
5582
5583impl RingBands {
5584    /// Index the rings. Cheap enough to build per face and paid back by the
5585    /// first few hundred queries.
5586    fn over(rings: &[Vec<Point2>]) -> Self {
5587        let mut edges = Vec::new();
5588        for ring in rings {
5589            for i in 0..ring.len() {
5590                edges.push((ring[i], ring[(i + 1) % ring.len()]));
5591            }
5592        }
5593        let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
5594        for (a, b) in &edges {
5595            low = low.min(a.y).min(b.y);
5596            high = high.max(a.y).max(b.y);
5597        }
5598        if edges.is_empty() || !low.is_finite() || !high.is_finite() {
5599            return Self {
5600                edges,
5601                bands: Vec::new(),
5602                low: 0.0,
5603                high: 0.0,
5604                step: 0.0,
5605            };
5606        }
5607        // About four edges to a band: enough to keep the per-query walk short
5608        // without spreading a long edge across a table of mostly empty bands.
5609        let count = edges.len().div_ceil(4).clamp(1, 4096);
5610        #[allow(
5611            clippy::cast_precision_loss,
5612            reason = "a band count, far below the integers f64 represents exactly"
5613        )]
5614        let step = (high - low) / count as f64;
5615        let mut bands: Vec<Vec<u32>> = vec![Vec::new(); count];
5616        for (i, (a, b)) in edges.iter().enumerate() {
5617            let (lo, hi) = (a.y.min(b.y), a.y.max(b.y));
5618            let first = Self::band_of(lo, low, step, count);
5619            let last = Self::band_of(hi, low, step, count);
5620            for band in &mut bands[first..=last] {
5621                #[allow(
5622                    clippy::cast_possible_truncation,
5623                    reason = "an edge index, bounded by the ring lengths"
5624                )]
5625                band.push(i as u32);
5626            }
5627        }
5628        Self {
5629            edges,
5630            bands,
5631            low,
5632            high,
5633            step,
5634        }
5635    }
5636
5637    /// Which band a height falls in, clamped to the table.
5638    fn band_of(y: f64, low: f64, step: f64, count: usize) -> usize {
5639        if step <= 0.0 {
5640            return 0;
5641        }
5642        let raw = (y - low) / step;
5643        if raw <= 0.0 {
5644            return 0;
5645        }
5646        #[allow(
5647            clippy::cast_possible_truncation,
5648            clippy::cast_sign_loss,
5649            reason = "clamped to the band count on the next line"
5650        )]
5651        let index = raw as usize;
5652        index.min(count - 1)
5653    }
5654
5655    /// Whether the point lies inside the region the rings bound.
5656    fn holds(&self, p: Point2) -> bool {
5657        if self.bands.is_empty() || p.y < self.low || p.y > self.high {
5658            return false;
5659        }
5660        let band = Self::band_of(p.y, self.low, self.step, self.bands.len());
5661        let at = |q: Point2| [q.x, q.y];
5662        let mut inside = false;
5663        for &i in &self.bands[band] {
5664            let (a, b) = self.edges[i as usize];
5665            if (a.y > p.y) == (b.y > p.y) {
5666                continue;
5667            }
5668            let side = Exact::orient2d(at(a), at(b), [p.x, p.y]);
5669            let rightwards = if b.y > a.y {
5670                side == ogeom_core::Sign::Positive
5671            } else {
5672                side == ogeom_core::Sign::Negative
5673            };
5674            if rightwards {
5675                inside = !inside;
5676            }
5677        }
5678        inside
5679    }
5680}
5681
5682/// The unit normal of a triangle, or `None` if it is degenerate.
5683#[must_use]
5684pub fn triangle_normal(a: Point, b: Point, c: Point, tol: Tolerances) -> Option<Direction> {
5685    Direction::from_cross(b - a, c - a, tol).ok()
5686}
5687
5688/// Discretize an edge into a polyline in space, for display or coarse queries.
5689///
5690/// # Errors
5691///
5692/// As [`discretize`].
5693pub fn polyline_of_edge(
5694    model: &Model,
5695    edge: &Shape,
5696    deflection: Deflection,
5697    tol: Tolerances,
5698) -> OgeomResult<Vec<Point>> {
5699    if model.kind_of(edge)? != ShapeType::Edge {
5700        ogeom_bail!(Construction, "expected an edge");
5701    }
5702    let Some(node) = model.node(edge) else {
5703        ogeom_bail!(Dangling, "edge is not in this model");
5704    };
5705    let NodeData::Edge(data) = node.data() else {
5706        ogeom_bail!(Construction, "edge node holds no edge data");
5707    };
5708    let Some(line) = edge_polyline(model, data, deflection, tol)? else {
5709        return Ok(Vec::new());
5710    };
5711    let placement = edge.transform(model.datums())?;
5712    let mut points: Vec<Point> = line.points.iter().map(|p| placement.apply(*p)).collect();
5713    if edge.orientation() == Orientation::Reversed {
5714        points.reverse();
5715    }
5716    Ok(points)
5717}
5718
5719#[cfg(test)]
5720#[allow(clippy::unwrap_used)]
5721mod tests {
5722    use super::*;
5723
5724    /// Two pieces wound alike about +z share one side, walked once each
5725    /// way, and each closes a triangle at the same two points, a side of
5726    /// which runs from a point to itself. Those sides are walked no way, so
5727    /// they cast no vote, and neither piece is flipped.
5728    #[test]
5729    fn a_side_from_a_point_to_itself_turns_no_piece() {
5730        let (far, farther) = (Point::new(5.0, 5.0, 5.0), Point::new(6.0, 5.0, 5.0));
5731        let mut mesh = Triangulation::new();
5732        mesh.positions = vec![
5733            Point::new(0.0, 0.0, 0.0),
5734            Point::new(1.0, 0.0, 0.0),
5735            Point::new(0.0, 1.0, 0.0),
5736            Point::new(1.0, 1.0, 0.0),
5737            far,
5738            far,
5739            farther,
5740            farther,
5741            far,
5742            far,
5743            farther,
5744            farther,
5745        ];
5746        mesh.normals = vec![Vector::new(0.0, 0.0, 1.0); mesh.positions.len()];
5747        let first = [[0, 1, 2], [0, 4, 5], [0, 6, 7]];
5748        let second = [[1, 3, 2], [3, 8, 9], [3, 10, 11]];
5749        mesh.triangles = first.iter().chain(&second).copied().collect();
5750        let before = mesh.triangles.clone();
5751        orient_pieces(&mut mesh, &[(0, 3), (3, 6)]);
5752        assert_eq!(mesh.triangles, before);
5753    }
5754
5755    /// A ring wrapping a closed chart once, read a whole turn low: the
5756    /// extent a patch's full band reads before it is slid, on a chart of
5757    /// [-3.14159265358979, 3.14159265358979] as a file states it. The slide
5758    /// is one turn up, though the turn's length over the chart's span
5759    /// rounds to a hair over one; a hair past an end is no turn, and a ring
5760    /// inside stays.
5761    #[test]
5762    #[allow(
5763        clippy::excessive_precision,
5764        clippy::approx_constant,
5765        reason = "the values as a file states them, a hair short of pi"
5766    )]
5767    fn a_ring_a_whole_turn_out_slides_back_one_turn() {
5768        let domain = (-3.141_592_653_589_79, 3.141_592_653_589_79);
5769        let span = domain.1 - domain.0;
5770        let low = (-9.424_777_960_769_370_47, -3.141_592_653_589_744_27);
5771        assert!((domain.0 - low.0) / span > 1.0);
5772        assert_eq!(turns_into_domain(low, domain), 1.0);
5773        let high = (low.0 + 2.0 * span, low.1 + 2.0 * span);
5774        assert_eq!(turns_into_domain(high, domain), -1.0);
5775        assert_eq!(turns_into_domain((domain.0 - 1e-9, domain.1), domain), 0.0);
5776        assert_eq!(turns_into_domain((-1.0, 2.0), domain), 0.0);
5777        // Two turns out is two turns back.
5778        let two = (low.0 - span, low.1 - span);
5779        assert_eq!(turns_into_domain(two, domain), 2.0);
5780    }
5781
5782    /// A plane strip 0.03 wide between two concentric arcs of a 15 mm
5783    /// circle, its inner rim already held to a fine chord by the face
5784    /// across it: the outer rim, drawn at the coarse chord, sags across the
5785    /// inner one, so the face asks for its outer rim drawn finer. Asked
5786    /// from nothing, both rims drawn coarse at the same angles, it asks
5787    /// for nothing: the narrow strip is whole until one rim is refined.
5788    #[test]
5789    fn a_strip_with_one_rim_held_fine_asks_for_its_other_rim_finer() {
5790        use ogeom_geom::{CircleCurve, Curve, PlaneSurface};
5791        use ogeom_math::{Circle, Frame, Plane};
5792        let mut model = Model::new();
5793        let (inner, outer, angle) = (15.0, 15.03, core::f64::consts::FRAC_PI_3);
5794        let at = |r: f64, a: f64| Point::new(r * a.cos(), r * a.sin(), 0.0);
5795        let corners = [
5796            at(outer, 0.0),
5797            at(outer, angle),
5798            at(inner, angle),
5799            at(inner, 0.0),
5800        ];
5801        let vertices: Vec<Shape> = corners
5802            .iter()
5803            .map(|p| ogeom_algo::make_vertex(&mut model, *p).shape)
5804            .collect();
5805        let arc =
5806            |r: f64| -> Curve { CircleCurve::new(Circle::new(Frame::WORLD, r, T).unwrap()).into() };
5807        let line = |a: Point, b: Point| -> Curve {
5808            ogeom_geom::LineCurve::segment(a, b, T).unwrap().into()
5809        };
5810        let mut edge = |curve: Curve, range: (f64, f64), from: usize, to: usize| {
5811            ogeom_algo::make_edge_between(
5812                &mut model,
5813                curve,
5814                range,
5815                &vertices[from],
5816                &vertices[to],
5817                T,
5818            )
5819            .unwrap()
5820            .shape
5821        };
5822        let outer_edge = edge(arc(outer), (0.0, angle), 0, 1);
5823        let top = edge(
5824            line(corners[1], corners[2]),
5825            (0.0, corners[1].distance(corners[2])),
5826            1,
5827            2,
5828        );
5829        let inner_edge = edge(arc(inner), (0.0, angle), 3, 2);
5830        let bottom = edge(
5831            line(corners[3], corners[0]),
5832            (0.0, corners[3].distance(corners[0])),
5833            3,
5834            0,
5835        );
5836        let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
5837        let ring = vec![outer_edge.clone(), top, inner_edge.reversed(), bottom];
5838        let face = ogeom_algo::make_face_with_pcurves(&mut model, plane, &[ring], T)
5839            .unwrap()
5840            .shape;
5841        let data = model.node(&face).unwrap().data().as_face().unwrap().clone();
5842        let surface = model.geometry().surface(data.surface).unwrap().clone();
5843        let coarse = Deflection::default();
5844        let alone = face_chords(
5845            &model,
5846            &face,
5847            data.surface,
5848            &surface,
5849            coarse,
5850            &EdgeChords::new(),
5851            T,
5852        )
5853        .unwrap();
5854        assert!(!alone.contains_key(&outer_edge.node().index()), "{alone:?}");
5855        let held: EdgeChords = [(inner_edge.node().index(), coarse.chord / 128.0)].into();
5856        let asked = face_chords(&model, &face, data.surface, &surface, coarse, &held, T).unwrap();
5857        let outer_chord = asked.get(&outer_edge.node().index()).copied();
5858        assert!(outer_chord.is_some_and(|c| c < coarse.chord), "{asked:?}");
5859        assert!(asked[&inner_edge.node().index()] <= coarse.chord / 128.0);
5860    }
5861
5862    #[test]
5863    fn a_wild_chart_refuses_instead_of_crashing() {
5864        let plane: SurfaceGeometry = ogeom_geom::PlaneSurface::over(
5865            ogeom_math::Plane::through(
5866                ogeom_math::Point::new(0.0, 0.0, 0.0),
5867                ogeom_math::Direction::Z,
5868            ),
5869            (-10.0, 10.0),
5870            (-10.0, 10.0),
5871        )
5872        .unwrap()
5873        .into();
5874        let tol = Tolerances::millimetres();
5875        // Coordinates wilder than any face: refused by the screen.
5876        let wild = vec![vec![
5877            Point2::new(0.0, 0.0),
5878            Point2::new(1e15, 0.0),
5879            Point2::new(0.0, 1e15),
5880        ]];
5881        let Err(err) = triangulate_region(&wild, &plane, Deflection::default(), Rows::Ends, tol)
5882        else {
5883            panic!("a wild chart must refuse");
5884        };
5885        assert!(err.to_string().contains("describes no face"), "{err}");
5886        // A non-finite coordinate: refused by name, not fed to the library.
5887        let nan = vec![vec![
5888            Point2::new(0.0, 0.0),
5889            Point2::new(f64::NAN, 1.0),
5890            Point2::new(1.0, 1.0),
5891        ]];
5892        let Err(err) = triangulate_region(&nan, &plane, Deflection::default(), Rows::Ends, tol)
5893        else {
5894            panic!("a NaN chart must refuse");
5895        };
5896        assert!(err.to_string().contains("non-finite"), "{err}");
5897    }
5898
5899    use approx::assert_relative_eq;
5900
5901    #[test]
5902    fn chart_tangency_retains_both_lobes_and_all_boundary_sides() {
5903        let tol = Tolerances::millimetres();
5904        let frame = ogeom_math::Frame::new(
5905            Point::ORIGIN,
5906            ogeom_math::Direction::Z,
5907            ogeom_math::Direction::X,
5908            tol,
5909        )
5910        .unwrap();
5911        let plane: SurfaceGeometry =
5912            ogeom_geom::PlaneSurface::new(ogeom_math::Plane::new(frame)).into();
5913        for angle in [0.0_f64, 0.1, 0.7] {
5914            let (sin, cos) = angle.sin_cos();
5915            let rotate = |p: Point2| Point2::new(cos * p.x - sin * p.y, sin * p.x + cos * p.y);
5916            for offset in [-1e-12, 0.0, 1e-12] {
5917                let mut ring: Vec<_> = [
5918                    Point2::new(0.0, 0.0),
5919                    Point2::new(0.0, 10.0),
5920                    Point2::new(1.0, 10.0),
5921                    Point2::new(offset, 3.0),
5922                    Point2::new(1.0, 0.0),
5923                ]
5924                .into_iter()
5925                .map(rotate)
5926                .collect();
5927                let original = ring.clone();
5928                let mut anchors = vec![None; ring.len()];
5929                snap_chart_junctions(&mut ring, &mut anchors, &plane, tol);
5930                assert_eq!(
5931                    ring[1], original[3],
5932                    "the target side shares the touching vertex"
5933                );
5934                let boundary = ring.clone();
5935                let mesh = triangulate_region(
5936                    &[ring],
5937                    &plane,
5938                    Deflection::with_chord(0.1).unwrap(),
5939                    Rows::Ends,
5940                    tol,
5941                )
5942                .unwrap();
5943                assert!(!mesh.crossed, "angle {angle}, offset {offset}");
5944                assert_eq!(mesh.triangles.len(), 2);
5945                for (&a, &b) in boundary.iter().zip(boundary.iter().cycle().skip(1)) {
5946                    assert!(
5947                        mesh.triangles.iter().any(|t| (0..3).any(|i| {
5948                            let x = mesh.parameters[t[i] as usize];
5949                            let y = mesh.parameters[t[(i + 1) % 3] as usize];
5950                            (x == (a.x, a.y) && y == (b.x, b.y))
5951                                || (x == (b.x, b.y) && y == (a.x, a.y))
5952                        })),
5953                        "missing boundary side {a:?} -- {b:?}"
5954                    );
5955                }
5956                // The two lobes have total area 5. The tolerance covers the
5957                // sub-parametric boundary bend and floating-point rotation.
5958                let area: f64 = mesh
5959                    .triangles
5960                    .iter()
5961                    .map(|t| {
5962                        let [a, b, c] = t.map(|i| {
5963                            let (u, v) = mesh.parameters[i as usize];
5964                            Point2::new(u, v)
5965                        });
5966                        (b - a).cross(c - a).abs() * 0.5
5967                    })
5968                    .sum();
5969                assert!((area - 5.0).abs() < 1e-9, "{area}");
5970                let mut model = Model::new();
5971                let points: Vec<_> = original.iter().map(|p| Point::new(p.x, p.y, 0.0)).collect();
5972                let wire = ogeom_algo::make_polygon(&mut model, &points, true, tol)
5973                    .unwrap()
5974                    .shape;
5975                let face = ogeom_algo::make_face(&mut model, plane.clone(), &[wire], tol)
5976                    .unwrap()
5977                    .shape;
5978                ogeom_algo::build::trimmed_where_bare(&mut model, &face, tol).unwrap();
5979                let (mesh, verdict) = triangulate_reporting(
5980                    &model,
5981                    &face,
5982                    Deflection::with_chord(0.1).unwrap(),
5983                    Some(&EdgeChords::new()),
5984                    tol,
5985                )
5986                .unwrap();
5987                assert!(verdict != Verdict::Short, "angle {angle}, offset {offset}");
5988                let area: f64 = mesh
5989                    .triangles
5990                    .iter()
5991                    .map(|t| {
5992                        let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
5993                        (b - a).cross(c - a).magnitude() * 0.5
5994                    })
5995                    .sum();
5996                assert!((area - 5.0).abs() < 1e-9, "face area {area}");
5997            }
5998        }
5999        let mut separate = vec![
6000            Point2::new(0.0, 0.0),
6001            Point2::new(0.0, 10.0),
6002            Point2::new(1.0, 10.0),
6003            Point2::new(2.0 * tol.parametric(), 5.0),
6004            Point2::new(1.0, 0.0),
6005        ];
6006        let before = separate.clone();
6007        snap_chart_junctions(&mut separate, &mut vec![None; before.len()], &plane, tol);
6008        assert_eq!(separate, before, "a resolvable gap must stay separate");
6009    }
6010    #[test]
6011    fn chart_junction_candidates_stay_bounded_on_dense_seams() {
6012        let samples = 1024;
6013        for transpose in [false, true] {
6014            let mut ring = Vec::new();
6015            for side in 0..4 {
6016                for i in 0..samples {
6017                    let t = i as f64 / samples as f64;
6018                    let p = match side {
6019                        0 => Point2::new(10.0 * t, 0.0),
6020                        1 => Point2::new(10.0, t),
6021                        2 => Point2::new(10.0 * (1.0 - t), 1.0),
6022                        _ => Point2::new(0.0, 1.0 - t),
6023                    };
6024                    ring.push(if transpose { Point2::new(p.y, p.x) } else { p });
6025                }
6026            }
6027            let swapped: Vec<_> = ring.iter().map(|p| Point2::new(p.y, p.x)).collect();
6028            let x = RingBands::over(&[swapped]);
6029            let y = RingBands::over(std::slice::from_ref(&ring));
6030            let mut visited = 0;
6031            for (i, &p) in ring.iter().enumerate() {
6032                let candidates: Vec<_> = junction_candidates(&x, &y, p, 1e-9)
6033                    .iter()
6034                    .flatten()
6035                    .copied()
6036                    .collect();
6037                // Both incident sides must be found, including band boundaries.
6038                let current = u32::try_from(i).unwrap();
6039                let previous = u32::try_from((i + ring.len() - 1) % ring.len()).unwrap();
6040                assert!(candidates.contains(&current));
6041                assert!(candidates.contains(&previous));
6042                visited += candidates.len();
6043            }
6044            // Count work rather than wall time: one axis would visit over a
6045            // million entries on either the horizontal or vertical seam.
6046            assert!(visited < 32 * ring.len(), "visited {visited} sides");
6047        }
6048    }
6049
6050    use ogeom_algo::make_box;
6051    use ogeom_math::Frame;
6052    use ogeom_topo::explore_unique;
6053
6054    const T: Tolerances = Tolerances::millimetres();
6055
6056    /// A cap no profile below reaches but the one written to.
6057    const UNCAPPED: usize = 1 << 16;
6058
6059    /// `refine_direction` by rescan from zero after every split: the
6060    /// reference the rounds are held to.
6061    fn refine_by_rescan<F: Fn(f64, f64) -> f64>(lo: f64, hi: f64, chord: f64, sag: F) -> Vec<f64> {
6062        let mut values = vec![lo, f64::midpoint(lo, hi), hi];
6063        while values.len() < UNCAPPED {
6064            let Some(i) = (0..values.len() - 1).find(|&i| sag(values[i], values[i + 1]) > chord)
6065            else {
6066                break;
6067            };
6068            let mid = f64::midpoint(values[i], values[i + 1]);
6069            if mid <= values[i] || mid >= values[i + 1] {
6070                break;
6071            }
6072            values.insert(i + 1, mid);
6073        }
6074        values
6075    }
6076
6077    #[test]
6078    fn rounds_split_where_rescanning_did() {
6079        // Rounds are only sound because splitting an interval cannot change
6080        // whether another one sags. Held to the rescan's output exactly,
6081        // over sag profiles that bite in different places: flat, steep at one
6082        // end, periodic; and one savage enough to reach the step cap, which
6083        // must leave it refined evenly.
6084        /// A named sag profile to hold both forms to.
6085        type Profile = (&'static str, Box<dyn Fn(f64, f64) -> f64>);
6086        let cases: Vec<Profile> = vec![
6087            ("flat", Box::new(|_a: f64, _b: f64| 0.0)),
6088            ("width", Box::new(|a: f64, b: f64| (b - a).abs())),
6089            (
6090                "steep at the low end",
6091                Box::new(|a: f64, b: f64| (b - a).abs() / a.abs().max(1e-3)),
6092            ),
6093            (
6094                "periodic",
6095                Box::new(|a: f64, b: f64| (b - a).abs() * (a * 12.0).sin().abs()),
6096            ),
6097            (
6098                "beyond the cap",
6099                Box::new(|a: f64, b: f64| (b - a).abs() * 1e6),
6100            ),
6101        ];
6102        for (name, sag) in cases {
6103            for chord in [1.0, 0.1, 0.01, 1e-3] {
6104                let capped = name == "beyond the cap";
6105                let cap = if capped {
6106                    MAX_DIRECTION_STEPS
6107                } else {
6108                    UNCAPPED
6109                };
6110                let (walked, samples) = refine_direction(
6111                    (0.0, 1.0),
6112                    chord,
6113                    cap,
6114                    |t| t,
6115                    |t| t,
6116                    |a, b, _| sag(*a, *b),
6117                    |t, _| t,
6118                );
6119                assert_eq!(walked, samples, "{name}: each value keeps its own sample");
6120                if capped {
6121                    // Stopped by the cap, and refined evenly all along.
6122                    assert!(walked.len() <= cap, "{name}: {} values", walked.len());
6123                    let gaps: Vec<f64> = walked.windows(2).map(|w| w[1] - w[0]).collect();
6124                    let (least, most) = gaps
6125                        .iter()
6126                        .fold((f64::INFINITY, 0.0_f64), |(l, m), g| (l.min(*g), m.max(*g)));
6127                    assert!(most <= least * 2.0, "{name}: gaps from {least} to {most}");
6128                    continue;
6129                }
6130                let rescanned = refine_by_rescan(0.0, 1.0, chord, &sag);
6131                assert_eq!(
6132                    walked, rescanned,
6133                    "{name} at chord {chord}: the rounds split somewhere the rescan did not"
6134                );
6135            }
6136        }
6137    }
6138
6139    #[test]
6140    fn banded_containment_answers_what_the_full_scan_answers() {
6141        // The index may only decline to visit edges that could not have
6142        // counted. Held to the unindexed predicate over a ring with a hole,
6143        // on a grid that straddles both boundaries and the vertices themselves.
6144        let outer: Vec<Point2> = vec![
6145            Point2::new(0.0, 0.0),
6146            Point2::new(4.0, 0.0),
6147            Point2::new(4.0, 3.0),
6148            Point2::new(2.0, 1.5),
6149            Point2::new(0.0, 3.0),
6150        ];
6151        let hole: Vec<Point2> = vec![
6152            Point2::new(1.0, 0.5),
6153            Point2::new(1.0, 1.0),
6154            Point2::new(1.5, 1.0),
6155            Point2::new(1.5, 0.5),
6156        ];
6157        let rings = vec![outer, hole];
6158        let bands = RingBands::over(&rings);
6159        for i in 0..=80 {
6160            for j in 0..=60 {
6161                #[allow(clippy::cast_precision_loss)]
6162                let p = Point2::new(f64::from(i) * 0.05 - 0.1, f64::from(j) * 0.05 - 0.1);
6163                assert_eq!(
6164                    bands.holds(p),
6165                    inside_region(&rings, p),
6166                    "the index disagreed with the full scan at {p:?}"
6167                );
6168            }
6169        }
6170    }
6171
6172    fn fine() -> Deflection {
6173        Deflection {
6174            chord: 1e-3,
6175            angular: 0.05,
6176            ..Deflection::default()
6177        }
6178    }
6179
6180    /// A vertex at a cone's apex or a sphere's pole carries the normal the
6181    /// surface tends to there along its own column, not nothing.
6182    #[test]
6183    fn apex_and_pole_vertices_carry_the_limit_normal() {
6184        let mut model = Model::new();
6185        let cone = ogeom_algo::make_cone(&mut model, Frame::WORLD, 2.0, 0.0, 3.0, T).unwrap();
6186        let sphere = ogeom_algo::make_sphere(&mut model, Frame::WORLD, 1.5, T).unwrap();
6187        for (shape, what) in [(&cone.shape, "cone"), (&sphere.shape, "sphere")] {
6188            for face in explore_unique(&model, shape, ShapeType::Face).unwrap() {
6189                let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
6190                for (i, n) in mesh.normals.iter().enumerate() {
6191                    assert!(
6192                        (n.magnitude() - 1.0).abs() < 1e-9,
6193                        "{what} vertex {i} at {:?} has normal {n:?}",
6194                        mesh.positions[i]
6195                    );
6196                }
6197            }
6198        }
6199        // At the apex the limit normal along a ruling is that ruling's
6200        // normal: on a cone of half-angle atan(2/3) it leans out by that
6201        // much from the axis, the same as every other normal in its column.
6202        let faces = explore_unique(&model, &cone.shape, ShapeType::Face).unwrap();
6203        let lean = (2.0_f64 / 3.0).atan();
6204        for face in &faces {
6205            let mesh = triangulate_face(&model, face, fine(), T).unwrap();
6206            for (i, p) in mesh.positions.iter().enumerate() {
6207                if p.distance(Point::new(0.0, 0.0, 3.0)) < 1e-9 {
6208                    let n = mesh.normals[i];
6209                    let from_axis = n.z.abs().acos();
6210                    assert!(
6211                        ((std::f64::consts::FRAC_PI_2 - from_axis) - lean).abs() < 1e-6,
6212                        "apex normal {n:?} leans {from_axis} from the axis"
6213                    );
6214                }
6215            }
6216        }
6217    }
6218
6219    #[test]
6220    fn a_box_face_triangulates_into_two_triangles() {
6221        // A planar square needs no interior points at all, which is what makes
6222        // deflection-driven refinement worth having: a fixed grid would add
6223        // dozens that buy nothing.
6224        let mut model = Model::new();
6225        let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
6226        let faces = explore_unique(&model, &built.shape, ShapeType::Face).unwrap();
6227
6228        for face in &faces {
6229            let mesh = triangulate_face(&model, face, fine(), T).unwrap();
6230            assert_eq!(mesh.triangle_count(), 2, "a rectangle is two triangles");
6231            assert_eq!(mesh.vertex_count(), 4);
6232            assert!(mesh.deflection_met);
6233        }
6234    }
6235
6236    #[test]
6237    fn a_boxs_mesh_is_closed_and_reports_the_right_volume() {
6238        // The end-to-end check: triangulate, weld, and ask the mesh what it
6239        // encloses. A volume that comes out negative would mean the faces are
6240        // wound inward; one that is wrong in magnitude would mean the
6241        // triangulation is not covering the boundary.
6242        let mut model = Model::new();
6243        let size = (2.0, 3.0, 4.0);
6244        let built = make_box(&mut model, Frame::WORLD, size, T).unwrap();
6245        let mesh = triangulate(&model, &built.shape, fine(), T).unwrap();
6246
6247        assert_eq!(mesh.triangle_count(), 12, "six faces, two triangles each");
6248        assert_eq!(mesh.vertex_count(), 8, "welding merged the shared corners");
6249        assert!(
6250            mesh.is_closed(),
6251            "every triangle edge should be shared by two"
6252        );
6253        assert_relative_eq!(mesh.volume(), size.0 * size.1 * size.2, epsilon = 1e-9);
6254        assert_relative_eq!(
6255            mesh.area(),
6256            2.0 * (size.0 * size.1 + size.1 * size.2 + size.2 * size.0),
6257            epsilon = 1e-9
6258        );
6259    }
6260
6261    /// The segments a full circle of radius `r` takes at chord `c`: the
6262    /// sag of a segment spanning `θ` is `r (1 − cos(θ/2))`, so the circle
6263    /// wants `π / acos(1 − c/r)` of them, and the bisection reaches it from
6264    /// its floor of two by halving every segment alike.
6265    fn circle_segments(r: f64, c: f64) -> usize {
6266        let wanted = std::f64::consts::PI / (1.0 - c / r).acos();
6267        let mut n = Deflection::default().min_segments;
6268        #[allow(clippy::cast_precision_loss, reason = "a segment count")]
6269        while (n as f64) < wanted {
6270            n *= 2;
6271        }
6272        n
6273    }
6274
6275    /// No triangle is longer for want of a length cap: a box is two
6276    /// triangles a face whether it is a 20 mm cube or a bar a thousand
6277    /// millimetres long and a tenth across, and every one of them has area
6278    /// and faces out with its face.
6279    #[test]
6280    fn a_box_is_two_triangles_a_face_however_long_its_faces() {
6281        let deflection = Deflection::curvature_only(0.01, 0.5).unwrap();
6282        for size in [(20.0, 20.0, 20.0), (1000.0, 0.1, 0.1)] {
6283            let mut model = Model::new();
6284            let built = make_box(&mut model, Frame::WORLD, size, T).unwrap();
6285            let mesh = triangulate(&model, &built.shape, deflection, T).unwrap();
6286            assert_eq!(mesh.triangle_count(), 12, "{size:?}");
6287            assert!(mesh.is_closed(), "{size:?}");
6288            // Two triangles on each face of the box, exact to rounding.
6289            assert_relative_eq!(
6290                mesh.volume(),
6291                size.0 * size.1 * size.2,
6292                max_relative = 1e-12
6293            );
6294            for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
6295                assert_faces_out(&triangulate_face(&model, &face, deflection, T).unwrap());
6296            }
6297        }
6298    }
6299
6300    /// Every triangle has area and turns the way the surface's normals
6301    /// at its corners do.
6302    fn assert_faces_out(mesh: &Triangulation) {
6303        for t in &mesh.triangles {
6304            let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
6305            let along = t
6306                .iter()
6307                .map(|&i| mesh.normals[i as usize])
6308                .fold(Vector::ZERO, |sum, n| sum + n);
6309            let normal = (b - a).cross(c - a);
6310            assert!(normal.magnitude() > 0.0, "a triangle with no area");
6311            assert!(normal.dot(along) > 0.0, "a triangle turned over");
6312        }
6313    }
6314
6315    /// A cylinder of radius 10 at a chord of a hundredth: each rim takes
6316    /// the segments the chord asks, the side two triangles a segment and
6317    /// each cap the triangulation of its polygon, at a height of 20 and of
6318    /// 2000 alike.
6319    #[test]
6320    fn a_cylinder_takes_the_segments_its_chord_asks_at_any_height() {
6321        let (r, c) = (10.0, 0.01);
6322        let n = circle_segments(r, c);
6323        // π / acos(0.999) = 70.24: seventy-one segments would do, and the
6324        // halving from two reaches 128.
6325        assert_eq!(n, 128);
6326        let deflection = Deflection::curvature_only(c, 0.5).unwrap();
6327        for height in [20.0, 2000.0] {
6328            let mut model = Model::new();
6329            let built = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, r, height, T).unwrap();
6330            let mesh = triangulate(&model, &built.shape, deflection, T).unwrap();
6331            assert!(mesh.is_closed(), "height {height}");
6332            assert_eq!(
6333                mesh.triangle_count(),
6334                2 * n + 2 * (n - 2),
6335                "height {height}"
6336            );
6337            let mut counts: Vec<usize> = explore_unique(&model, &built.shape, ShapeType::Face)
6338                .unwrap()
6339                .iter()
6340                .map(|face| {
6341                    let drawn = triangulate_face(&model, face, deflection, T).unwrap();
6342                    assert_faces_out(&drawn);
6343                    drawn.triangle_count()
6344                })
6345                .collect();
6346            counts.sort_unstable();
6347            assert_eq!(counts, [n - 2, n - 2, 2 * n], "height {height}");
6348        }
6349    }
6350
6351    /// A tube a twentieth of a millimetre thick: each cap is an annulus far
6352    /// longer round than across, drawn as its two polygons' triangulation,
6353    /// one triangle per boundary point, none of them folded. Its side is
6354    /// ruled between the rims, so the mesh encloses exactly the prism on
6355    /// the two polygons.
6356    #[test]
6357    fn a_thin_annulus_is_its_outline_s_triangulation() {
6358        use ogeom_geom::Curve3d as _;
6359        let (outer, inner, height, c) = (10.05, 10.0, 5.0, 0.01);
6360        let n = circle_segments(inner, c);
6361        assert_eq!(n, circle_segments(outer, c));
6362        let mut model = Model::new();
6363        let ring = |model: &mut Model, frame: Frame, radius: f64| {
6364            let circle = ogeom_math::Circle::new(frame, radius, T).unwrap();
6365            let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
6366            let domain = curve.domain();
6367            ogeom_algo::make_edge(model, curve, domain, T)
6368                .unwrap()
6369                .shape
6370        };
6371        let down =
6372            Frame::from_axes(Point::ORIGIN, Direction::X, -Direction::Y, -Direction::Z, T).unwrap();
6373        let rim = ring(&mut model, Frame::WORLD, outer);
6374        let hole = ring(&mut model, down, inner);
6375        let plane = ogeom_math::Plane::new(Frame::WORLD);
6376        let surface: SurfaceGeometry =
6377            ogeom_geom::PlaneSurface::over(plane, (-20.0, 20.0), (-20.0, 20.0))
6378                .unwrap()
6379                .into();
6380        let cap =
6381            ogeom_algo::make_face_with_pcurves(&mut model, surface, &[vec![rim], vec![hole]], T)
6382                .unwrap()
6383                .shape;
6384        let tube =
6385            ogeom_algo::make_prism(&mut model, &cap, Vector::new(0.0, 0.0, height), T).unwrap();
6386
6387        let deflection = Deflection::curvature_only(c, 0.5).unwrap();
6388        let mesh = triangulate(&model, &tube.shape, deflection, T).unwrap();
6389        assert!(mesh.is_closed());
6390        #[allow(clippy::cast_precision_loss, reason = "a segment count")]
6391        let polygons = n as f64 / 2.0 * (std::f64::consts::TAU / n as f64).sin();
6392        // The prism on the two inscribed polygons, exact to rounding.
6393        assert_relative_eq!(
6394            mesh.volume(),
6395            polygons * (outer * outer - inner * inner) * height,
6396            max_relative = 1e-9
6397        );
6398        for face in explore_unique(&model, &tube.shape, ShapeType::Face).unwrap() {
6399            let drawn = triangulate_face(&model, &face, deflection, T).unwrap();
6400            assert_faces_out(&drawn);
6401            assert_eq!(drawn.triangle_count(), 2 * n);
6402        }
6403    }
6404
6405    #[test]
6406    fn welding_is_what_closes_the_mesh() {
6407        // Without it each face brings its own copy of every boundary vertex, so
6408        // no triangle edge is shared and the surface is a pile of loose squares.
6409        let mut model = Model::new();
6410        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6411
6412        let mut loose = Triangulation::new();
6413        for face in ogeom_topo::explore(
6414            &model,
6415            &built.shape,
6416            ogeom_topo::Filter::OfType(ShapeType::Face),
6417        )
6418        .unwrap()
6419        {
6420            loose.append(&triangulate_face(&model, &face, fine(), T).unwrap());
6421        }
6422        assert_eq!(loose.vertex_count(), 24, "four corners per face, unmerged");
6423        assert!(!loose.is_closed());
6424
6425        let welded = loose.welded(T);
6426        assert_eq!(welded.vertex_count(), 8);
6427        assert!(welded.is_closed());
6428    }
6429
6430    #[test]
6431    fn a_reversed_face_presents_the_other_side() {
6432        // A renderer or a volume computation that ignored orientation would
6433        // have the solid inside out, and nothing about the positions says so.
6434        let mut model = Model::new();
6435        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6436        let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
6437
6438        let forward = triangulate_face(&model, &face, fine(), T).unwrap();
6439        let backward = triangulate_face(&model, &face.reversed(), fine(), T).unwrap();
6440
6441        assert_eq!(forward.triangle_count(), backward.triangle_count());
6442        for (a, b) in forward.normals.iter().zip(&backward.normals) {
6443            assert!(a.is_equal(-*b, T), "normals did not flip: {a:?} vs {b:?}");
6444        }
6445        // And the winding flipped with them, so the two agree.
6446        let winding = |m: &Triangulation, i: usize| {
6447            let [a, b, c] = m.triangles[i].map(|k| m.positions[k as usize]);
6448            (b - a).cross(c - a)
6449        };
6450        assert!(winding(&forward, 0).dot(winding(&backward, 0)) < 0.0);
6451    }
6452
6453    /// Triangles whose winding points against the sum of their vertex
6454    /// normals.
6455    fn against(mesh: &Triangulation) -> usize {
6456        mesh.triangles
6457            .iter()
6458            .filter(|t| {
6459                let [a, b, c] = t.map(|k| mesh.positions[k as usize]);
6460                let facing = (b - a).cross(c - a);
6461                let normal = t
6462                    .iter()
6463                    .fold(Vector::ZERO, |sum, &k| sum + mesh.normals[k as usize]);
6464                facing.dot(normal) < 0.0
6465            })
6466            .collect::<Vec<_>>()
6467            .len()
6468    }
6469
6470    #[test]
6471    fn a_mirrored_face_is_shaded_the_way_its_triangles_face() {
6472        // A reflection turns the chart's natural normal over, so the
6473        // winding has to turn to keep facing the way the image faces; the
6474        // normals are the original's carried through the mirror and do not.
6475        use ogeom_geom::{CircleCurve, Curve};
6476        use ogeom_math::{Circle, Transform};
6477        let mut model = Model::new();
6478        let frame = Frame::new(Point::ORIGIN, Direction::Z, Direction::X, T).unwrap();
6479        let circle = Circle::new(frame, 10.0, T).unwrap();
6480        let arc = ogeom_algo::make_edge(
6481            &mut model,
6482            Curve::Circle(CircleCurve::new(circle)),
6483            (0.0, std::f64::consts::PI),
6484            T,
6485        )
6486        .unwrap()
6487        .shape;
6488        let wall = ogeom_algo::make_prism(&mut model, &arc, Vector::new(0.0, 0.0, 5.0), T)
6489            .unwrap()
6490            .shape;
6491        let face = explore_unique(&model, &wall, ShapeType::Face)
6492            .unwrap()
6493            .remove(0);
6494        let copy = ogeom_algo::copied(&mut model, &face).unwrap().shape;
6495        let mirror = Transform::plane_mirror(Point::ORIGIN, Direction::Y);
6496        let image = ogeom_algo::transformed(&mut model, &copy, mirror)
6497            .unwrap()
6498            .shape;
6499        for f in [&face, &image, &image.reversed()] {
6500            let mesh = triangulate_face(&model, f, Deflection::default(), T).unwrap();
6501            assert!(mesh.triangle_count() > 0);
6502            assert_eq!(against(&mesh), 0, "{} triangles", mesh.triangle_count());
6503        }
6504        // The image's normal at (10, 0, 0) is the original's there, which
6505        // the mirror leaves where it was.
6506        let original = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6507        let mirrored = triangulate_face(&model, &image, Deflection::default(), T).unwrap();
6508        let at = |mesh: &Triangulation| {
6509            let i = mesh
6510                .positions
6511                .iter()
6512                .position(|p| p.distance(Point::new(10.0, 0.0, 0.0)) < 1e-9)
6513                .unwrap();
6514            mesh.normals[i]
6515        };
6516        assert!(at(&mirrored).is_equal(at(&original), T));
6517        // The whole-shape path agrees.
6518        let whole = triangulate(&model, &image, Deflection::default(), T).unwrap();
6519        assert_eq!(against(&whole), 0);
6520    }
6521
6522    #[test]
6523    fn every_triangle_vertex_lies_on_the_surface_it_came_from() {
6524        let mut model = Model::new();
6525        let built = make_box(&mut model, Frame::WORLD, (2.0, 1.0, 3.0), T).unwrap();
6526
6527        for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
6528            let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
6529            let data = model.node(&face).unwrap().data().as_face().unwrap().clone();
6530            let surface = model.geometry().surface(data.surface).unwrap();
6531            for (position, (u, v)) in mesh.positions.iter().zip(&mesh.parameters) {
6532                let exact = surface.point_at(*u, *v, T).unwrap();
6533                assert!(
6534                    position.is_equal(exact, T),
6535                    "{position:?} is not on its surface"
6536                );
6537            }
6538        }
6539    }
6540
6541    #[test]
6542    fn a_finer_deflection_never_gives_fewer_triangles() {
6543        let mut model = Model::new();
6544        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6545        let coarse = triangulate(&model, &built.shape, Deflection::default(), T).unwrap();
6546        let detailed = triangulate(&model, &built.shape, fine(), T).unwrap();
6547        assert!(detailed.triangle_count() >= coarse.triangle_count());
6548        // Both enclose the same volume, since the faces are flat.
6549        assert_relative_eq!(coarse.volume(), 1.0, epsilon = 1e-9);
6550        assert_relative_eq!(detailed.volume(), 1.0, epsilon = 1e-9);
6551    }
6552
6553    #[test]
6554    fn a_sphere_gets_interior_points_and_converges_on_its_true_area() {
6555        // The whole point of measuring deflection in space rather than in
6556        // parameter space. A sphere's parameter rectangle is uniform; the
6557        // surface it maps to is not, and a fixed grid would be dense at the
6558        // poles and coarse at the equator.
6559        use ogeom_algo::make_natural_face;
6560        use ogeom_geom::SphereSurface;
6561        use ogeom_math::Sphere;
6562
6563        let radius = 10.0;
6564        let exact = 4.0 * std::f64::consts::PI * radius * radius;
6565        let mut previous = 0.0;
6566
6567        for chord in [1.0_f64, 0.25, 0.05] {
6568            let mut model = Model::new();
6569            let surface = SphereSurface::new(Sphere::new(Frame::WORLD, radius, T).unwrap());
6570            let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
6571            let deflection = Deflection {
6572                chord,
6573                ..Deflection::default()
6574            };
6575            let mesh = triangulate_face(&model, &face, deflection, T).unwrap();
6576
6577            assert!(
6578                mesh.triangle_count() > 2,
6579                "a curved face needs interior points, got {} triangles",
6580                mesh.triangle_count()
6581            );
6582            // Every triangle chord-cuts the sphere, so the area comes in under
6583            // the truth and climbs as the tolerance tightens.
6584            let area = mesh.area();
6585            assert!(area < exact, "a chord-cut area cannot exceed the surface's");
6586            assert!(
6587                area > previous,
6588                "tightening the chord from the previous step lost area: \
6589                 {area} after {previous}"
6590            );
6591            previous = area;
6592        }
6593        assert!(
6594            previous > exact * 0.99,
6595            "at a chord of 0.05 on a radius of 10 the area should be within a \
6596             percent, got {previous} against {exact}"
6597        );
6598    }
6599
6600    #[test]
6601    fn every_sphere_vertex_sits_at_the_right_radius() {
6602        // Lifting through the surface is what makes the mesh curved at all; a
6603        // vertex left in parameter space, or lifted with the wrong parameters,
6604        // would land nowhere near the sphere.
6605        use ogeom_algo::make_natural_face;
6606        use ogeom_geom::SphereSurface;
6607        use ogeom_math::Sphere;
6608
6609        let mut model = Model::new();
6610        let surface = SphereSurface::new(Sphere::new(Frame::WORLD, 3.0, T).unwrap());
6611        let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
6612        let mesh = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6613
6614        for p in &mesh.positions {
6615            assert_relative_eq!(p.to_vector().magnitude(), 3.0, epsilon = 1e-9);
6616        }
6617    }
6618
6619    #[test]
6620    fn a_curved_domains_boundary_is_refined_not_just_its_corners() {
6621        // The corners alone would leave the triangulation nothing to connect to
6622        // along a side, so it reaches right across the domain for one, and a
6623        // sliver from a sphere's equator to its pole makes the summed area
6624        // diverge under refinement rather than converge.
6625        use ogeom_geom::{PlaneSurface, SphereSurface};
6626        use ogeom_math::{Plane, Sphere};
6627
6628        let sphere: SurfaceGeometry =
6629            SphereSurface::new(Sphere::new(Frame::WORLD, 10.0, T).unwrap()).into();
6630        let coarse = domain_ring(&sphere, Deflection::default(), T);
6631        let fine_ring = domain_ring(&sphere, fine(), T);
6632        assert!(coarse.len() > 4, "a sphere's domain edge is curved");
6633        assert!(
6634            fine_ring.len() > coarse.len(),
6635            "a tighter chord should place more boundary points"
6636        );
6637
6638        // No side may repeat a corner: a zero-length constraint is not one.
6639        for w in fine_ring.windows(2) {
6640            assert!(!w[0].is_equal(w[1], T), "the ring repeats a point");
6641        }
6642
6643        // A plane is flat, so its domain needs only what closes the rectangle.
6644        let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
6645        assert!(domain_ring(&plane, fine(), T).len() >= 4);
6646    }
6647
6648    #[test]
6649    fn winding_detects_points_inside_and_outside_a_ring() {
6650        let square = vec![
6651            Point2::new(0.0, 0.0),
6652            Point2::new(1.0, 0.0),
6653            Point2::new(1.0, 1.0),
6654            Point2::new(0.0, 1.0),
6655        ];
6656        assert!(inside_region(
6657            std::slice::from_ref(&square),
6658            Point2::new(0.5, 0.5)
6659        ));
6660        assert!(!inside_region(
6661            std::slice::from_ref(&square),
6662            Point2::new(1.5, 0.5)
6663        ));
6664        assert!(!inside_region(
6665            std::slice::from_ref(&square),
6666            Point2::new(0.5, -0.5)
6667        ));
6668
6669        // With a hole, the middle is outside again.
6670        let hole = vec![
6671            Point2::new(0.4, 0.4),
6672            Point2::new(0.6, 0.4),
6673            Point2::new(0.6, 0.6),
6674            Point2::new(0.4, 0.6),
6675        ];
6676        let with_hole = vec![square, hole];
6677        assert!(!inside_region(&with_hole, Point2::new(0.5, 0.5)));
6678        assert!(inside_region(&with_hole, Point2::new(0.2, 0.2)));
6679    }
6680
6681    #[test]
6682    fn a_polyline_of_an_edge_runs_in_the_edges_direction() {
6683        let mut model = Model::new();
6684        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6685        let edge = explore_unique(&model, &built.shape, ShapeType::Edge).unwrap()[0].clone();
6686
6687        let forward = polyline_of_edge(&model, &edge, fine(), T).unwrap();
6688        let backward = polyline_of_edge(&model, &edge.reversed(), fine(), T).unwrap();
6689        assert!(forward.len() >= 2);
6690        assert!(forward[0].is_equal(backward[backward.len() - 1], T));
6691        assert!(forward[forward.len() - 1].is_equal(backward[0], T));
6692    }
6693
6694    #[test]
6695    fn triangulating_something_that_is_not_a_face_is_refused() {
6696        let mut model = Model::new();
6697        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6698        assert!(triangulate_face(&model, &built.shape, fine(), T).is_err());
6699
6700        let vertex = explore_unique(&model, &built.shape, ShapeType::Vertex).unwrap()[0].clone();
6701        assert!(triangulate_face(&model, &vertex, fine(), T).is_err());
6702        assert!(polyline_of_edge(&model, &vertex, fine(), T).is_err());
6703    }
6704
6705    #[test]
6706    fn a_triangles_normal_is_none_when_it_is_degenerate() {
6707        let a = Point::ORIGIN;
6708        let b = Point::new(1.0, 0.0, 0.0);
6709        assert!(triangle_normal(a, b, Point::new(0.0, 1.0, 0.0), T).is_some());
6710        assert!(triangle_normal(a, b, Point::new(2.0, 0.0, 0.0), T).is_none());
6711        assert!(triangle_normal(a, a, a, T).is_none());
6712    }
6713
6714    /// A ten millimetre square plate at `z = 0` moved by `at`, with the
6715    /// inner wire `hole` builds into its model given the same move, and its
6716    /// mesh's Euler characteristic and area.
6717    fn plate_with(at: Vector, hole: impl Fn(&mut Model, Vector) -> Shape) -> (i64, f64) {
6718        use ogeom_algo::{make_face, make_polygon};
6719        let mut model = Model::new();
6720        let p = |x: f64, y: f64| Point::new(x, y, 0.0) + at;
6721        let outer = make_polygon(
6722            &mut model,
6723            &[p(0.0, 0.0), p(10.0, 0.0), p(10.0, 10.0), p(0.0, 10.0)],
6724            true,
6725            T,
6726        )
6727        .unwrap()
6728        .shape;
6729        let inner = hole(&mut model, at);
6730        let plane: SurfaceGeometry = ogeom_geom::PlaneSurface::new(ogeom_math::Plane::through(
6731            Point::ORIGIN + at,
6732            Direction::Z,
6733        ))
6734        .into();
6735        let face = make_face(&mut model, plane, &[outer, inner], T)
6736            .unwrap()
6737            .shape;
6738        ogeom_algo::build::trimmed_where_bare(&mut model, &face, T).unwrap();
6739        let mesh = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6740        let mut edges = ogeom_core::FastSet::default();
6741        let mut area = 0.0;
6742        for t in &mesh.triangles {
6743            for i in 0..3 {
6744                let (a, b) = (t[i], t[(i + 1) % 3]);
6745                edges.insert((a.min(b), a.max(b)));
6746            }
6747            let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
6748            area += 0.5 * (b - a).cross(c - a).magnitude();
6749        }
6750        #[allow(clippy::cast_possible_wrap)]
6751        let euler = mesh.positions.len() as i64 - edges.len() as i64 + mesh.triangles.len() as i64;
6752        (euler, area)
6753    }
6754
6755    #[test]
6756    fn a_hole_thinner_than_a_micron_stays_a_hole() {
6757        // A slot two millimetres long and six tenths of a micron across: a
6758        // ring of four distinct vertices, which goes round rather than out
6759        // and back, so it is a hole however thin, and the plate meshes
6760        // with one hole in it.
6761        let (euler, area) = plate_with(Vector::ZERO, |model, _| {
6762            let p = |x: f64, y: f64| Point::new(x, y, 0.0);
6763            ogeom_algo::make_polygon(
6764                model,
6765                &[p(4.0, 5.0), p(4.0, 5.0006), p(6.0, 5.0006), p(6.0, 5.0)],
6766                true,
6767                T,
6768            )
6769            .unwrap()
6770            .shape
6771        });
6772        assert_eq!(euler, 0, "one hole: V - E + F is 1 - 1");
6773        assert_relative_eq!(area, 100.0 - 2.0 * 0.0006, epsilon = 1e-7);
6774    }
6775
6776    #[test]
6777    fn a_ring_walked_out_and_back_thinner_than_a_micron_is_a_slit() {
6778        // Out from one vertex to a second and a third along two lines, and
6779        // home through the same three along two arcs bulging six tenths of
6780        // a micron off them: every edge is answered by one walking it back,
6781        // and the ring is a few tenths of a micron wide on average. It is a
6782        // slit, and the plate meshes whole, up to 2e7 from the origin too.
6783        for offset in [0.0, 1.0e6, 1.0e7, 2.0e7] {
6784            let (euler, area) = slit_plate(Vector::new(offset, -0.7 * offset, 0.3 * offset));
6785            assert_eq!(euler, 1, "no hole at {offset:e}: V - E + F is 1");
6786            assert_relative_eq!(area, 100.0, epsilon = 1e-6);
6787        }
6788    }
6789
6790    #[test]
6791    fn a_small_round_hole_in_two_halves_stays_a_hole_far_from_the_origin() {
6792        // A hole five microns in radius and one four tenths of a micron in
6793        // radius, each circle cut into two halves between the same two
6794        // vertices: the wire walks back by its vertices, and the smaller
6795        // is under a micron wide. Both halves bound a round region whose
6796        // width is a sixth of its perimeter, not two sides lying along
6797        // each other, so the plate meshes with one hole in it up to 2e7
6798        // from the origin.
6799        for radius in [0.005, 0.0004] {
6800            for offset in [0.0, 1.0e6, 1.0e7, 2.0e7] {
6801                let at = Vector::new(offset, -0.7 * offset, 0.3 * offset);
6802                let (euler, _) = plate_with(at, |model, by| {
6803                    use ogeom_algo::{make_edge_between, make_wire};
6804                    let frame = ogeom_math::Frame::new(
6805                        Point::new(5.0, 5.0, 0.0) + by,
6806                        Direction::Z,
6807                        Direction::X,
6808                        T,
6809                    )
6810                    .unwrap();
6811                    let circle = ogeom_math::Circle::new(frame, radius, T).unwrap();
6812                    let ends = [
6813                        frame.origin() + frame.x().vector() * radius,
6814                        frame.origin() - frame.x().vector() * radius,
6815                    ];
6816                    let v: Vec<Shape> = ends
6817                        .iter()
6818                        .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6819                        .collect();
6820                    let half = |model: &mut Model, range: (f64, f64), from: &Shape, to: &Shape| {
6821                        let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
6822                        make_edge_between(model, curve, range, from, to, T)
6823                            .unwrap()
6824                            .shape
6825                    };
6826                    let pi = core::f64::consts::PI;
6827                    let edges = [
6828                        half(model, (0.0, pi), &v[0], &v[1]),
6829                        half(model, (pi, 2.0 * pi), &v[1], &v[0]),
6830                    ];
6831                    make_wire(model, &edges, T).unwrap().shape.reversed()
6832                });
6833                assert_eq!(
6834                    euler, 0,
6835                    "radius {radius}, one hole at {offset:e}: V - E + F is 1 - 1"
6836                );
6837            }
6838        }
6839    }
6840
6841    #[test]
6842    fn two_edges_lying_along_each_other_thinner_than_a_micron_are_a_slit() {
6843        // Out from one vertex to a second along a line two millimetres
6844        // long, home along an arc bulging six tenths of a micron off it:
6845        // two edges between the same two vertices, like a round hole cut
6846        // in two halves, but the ring is a ten-thousandth as wide as it is
6847        // long. It is a slit, and the plate meshes whole.
6848        let (euler, area) = plate_with(Vector::ZERO, |model, _| {
6849            use ogeom_algo::{Spacing, interpolate, make_edge_between, make_wire};
6850            use ogeom_geom::Curve3d as _;
6851            let at = [Point::new(4.0, 5.0, 0.0), Point::new(6.0, 5.0, 0.0)];
6852            let v: Vec<Shape> = at
6853                .iter()
6854                .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6855                .collect();
6856            let line: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(at[0], at[1], T)
6857                .unwrap()
6858                .into();
6859            let out = make_edge_between(model, line, (0.0, 2.0), &v[0], &v[1], T)
6860                .unwrap()
6861                .shape;
6862            let mid = at[0].midpoint(at[1]) + Vector::new(0.0, 0.0006, 0.0);
6863            let arc: ogeom_geom::Curve =
6864                interpolate(&[at[1], mid, at[0]], 2, Spacing::Centripetal, T)
6865                    .unwrap()
6866                    .into();
6867            let range = arc.domain();
6868            let back = make_edge_between(model, arc, range, &v[1], &v[0], T)
6869                .unwrap()
6870                .shape;
6871            make_wire(model, &[out, back], T).unwrap().shape
6872        });
6873        assert_eq!(euler, 1, "no hole: V - E + F is 1");
6874        // The plate's own area, to the rounding of a hundred square
6875        // millimetres summed over its triangles.
6876        assert_relative_eq!(area, 100.0, epsilon = 1e-6);
6877    }
6878
6879    /// The plate of the test above with its slit, moved by `at`.
6880    fn slit_plate(at: Vector) -> (i64, f64) {
6881        plate_with(at, |model, by| {
6882            use ogeom_algo::{Spacing, interpolate, make_edge_between, make_wire};
6883            use ogeom_geom::Curve3d as _;
6884            let p = |x: f64, y: f64| Point::new(x, y, 0.0) + by;
6885            let at = [p(3.0, 5.0), p(5.0, 5.0), p(7.0, 5.0)];
6886            let v: Vec<Shape> = at
6887                .iter()
6888                .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6889                .collect();
6890            let mut edges = Vec::new();
6891            for i in 0..2 {
6892                let line: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(at[i], at[i + 1], T)
6893                    .unwrap()
6894                    .into();
6895                let range = (0.0, at[i].distance(at[i + 1]));
6896                edges.push(
6897                    make_edge_between(model, line, range, &v[i], &v[i + 1], T)
6898                        .unwrap()
6899                        .shape,
6900                );
6901            }
6902            for i in (0..2).rev() {
6903                let mid = at[i].midpoint(at[i + 1]) + Vector::new(0.0, 0.0006, 0.0);
6904                let arc: ogeom_geom::Curve =
6905                    interpolate(&[at[i + 1], mid, at[i]], 2, Spacing::Centripetal, T)
6906                        .unwrap()
6907                        .into();
6908                let range = arc.domain();
6909                edges.push(
6910                    make_edge_between(model, arc, range, &v[i + 1], &v[i], T)
6911                        .unwrap()
6912                        .shape,
6913                );
6914            }
6915            make_wire(model, &edges, T).unwrap().shape
6916        })
6917    }
6918}
6919
6920#[cfg(test)]
6921#[allow(clippy::unwrap_used)]
6922mod predicate_tests {
6923    use super::*;
6924    use ogeom_core::Fast;
6925
6926    /// A triangle with one very long, very nearly diagonal edge.
6927    ///
6928    /// The configuration where a naive crossing test goes wrong. Subtracting
6929    /// the edge's ends from a query point close to the line cancels almost
6930    /// every significant digit, and what is left is rounding rather than
6931    /// geometry.
6932    fn sliver() -> Vec<Vec<Point2>> {
6933        vec![vec![
6934            Point2::new(0.5, 0.5),
6935            Point2::new(1000.0, 1000.0),
6936            Point2::new(1000.0, 0.5),
6937        ]]
6938    }
6939
6940    #[test]
6941    fn the_two_implementations_are_a_real_choice_and_not_a_decoration() {
6942        // The point of the seam. In a band of near-degenerate queries the two
6943        // answer differently, and if they never did, routing through the trait
6944        // would be ceremony rather than a design.
6945        //
6946        // The query points straddle the long edge at a spacing far below what
6947        // the subtraction can resolve, which is exactly the case a mesh hits
6948        // when a face's boundary passes close to a triangulation vertex.
6949        let rings = sliver();
6950        let step = 2.0_f64.powi(-48);
6951        let mut disagreements = 0;
6952        for i in 0..256i32 {
6953            for j in 0..256i32 {
6954                let p = Point2::new(
6955                    250.0 + f64::from(i - 128) * step,
6956                    250.0 + f64::from(j - 128) * step,
6957                );
6958                if inside_boundary_with::<Exact>(&rings, p)
6959                    != inside_boundary_with::<Fast>(&rings, p)
6960                {
6961                    disagreements += 1;
6962                }
6963            }
6964        }
6965        assert!(
6966            disagreements > 0,
6967            "the exact and fast predicates never disagreed, so the seam is not \
6968             carrying anything"
6969        );
6970    }
6971
6972    #[test]
6973    fn the_exact_predicate_is_the_one_that_is_right_near_the_edge() {
6974        // Disagreeing is not enough; the exact one has to be the *correct*
6975        // one, and that needs points whose side is known without asking either
6976        // implementation.
6977        //
6978        // Stepped in units of the last place rather than by a small distance.
6979        // A distance below the spacing of `f64` at 250 rounds away entirely,
6980        // leaving two points that are both exactly *on* the diagonal, where
6981        // either answer is defensible and the test would be asserting nothing.
6982        let rings = sliver();
6983        let base = 250.0_f64;
6984        let mut off = base;
6985        for k in 1..64 {
6986            off = off.next_up();
6987            assert_ne!(off, base, "step {k} did not move the point at all");
6988            // Inside this triangle is below the diagonal `y = x`: larger x.
6989            assert!(
6990                inside_boundary_with::<Exact>(&rings, Point2::new(off, base)),
6991                "a point {k} ulps below the diagonal was reported outside"
6992            );
6993            assert!(
6994                !inside_boundary_with::<Exact>(&rings, Point2::new(base, off)),
6995                "a point {k} ulps above the diagonal was reported inside"
6996            );
6997        }
6998    }
6999
7000    #[test]
7001    fn the_exact_answer_is_the_one_the_geometry_supports() {
7002        // Points placed by construction, so the right answer is known without
7003        // asking either implementation.
7004        let square = vec![vec![
7005            Point2::new(0.0, 0.0),
7006            Point2::new(1.0, 0.0),
7007            Point2::new(1.0, 1.0),
7008            Point2::new(0.0, 1.0),
7009        ]];
7010        assert!(inside_boundary_with::<Exact>(
7011            &square,
7012            Point2::new(0.5, 0.5)
7013        ));
7014        assert!(!inside_boundary_with::<Exact>(
7015            &square,
7016            Point2::new(1.5, 0.5)
7017        ));
7018        assert!(!inside_boundary_with::<Exact>(
7019            &square,
7020            Point2::new(-0.5, 0.5)
7021        ));
7022        assert!(!inside_boundary_with::<Exact>(
7023            &square,
7024            Point2::new(0.5, 1.5)
7025        ));
7026
7027        // A vertex exactly on the sampling ray is counted once, not twice or
7028        // not at all, which is what the half-open comparison is for.
7029        let diamond = vec![vec![
7030            Point2::new(0.0, 0.0),
7031            Point2::new(1.0, 1.0),
7032            Point2::new(2.0, 0.0),
7033            Point2::new(1.0, -1.0),
7034        ]];
7035        assert!(inside_boundary_with::<Exact>(
7036            &diamond,
7037            Point2::new(1.0, 0.0)
7038        ));
7039        assert!(!inside_boundary_with::<Exact>(
7040            &diamond,
7041            Point2::new(3.0, 0.0)
7042        ));
7043        assert!(!inside_boundary_with::<Exact>(
7044            &diamond,
7045            Point2::new(-1.0, 0.0)
7046        ));
7047    }
7048
7049    /// A cylinder's grid has as many rows as its length needs, not as
7050    /// many as its sag asks for.
7051    #[test]
7052    fn rows_are_spread_until_no_cell_is_taller_than_its_aspect() {
7053        // Sixteen columns a millimetre wide over a region a hundred long:
7054        // sag left one interior row, the aspect wants cells six tall.
7055        let sagged = vec![0.0, 50.0, 100.0];
7056        let spread = spread_to_aspect(
7057            sagged.clone(),
7058            CELL_ASPECT,
7059            0.0,
7060            16.0,
7061            |_| 17,
7062            |i| (sagged[i + 1] - sagged[i]).abs(),
7063            |a, b, _| (b - a).abs(),
7064        );
7065        assert_eq!(
7066            spread.len(),
7067            2 * 9 + 1,
7068            "each 50 mm half in nine 6 mm pieces: {spread:?}"
7069        );
7070        assert_eq!(spread[0], 0.0);
7071        assert_eq!(spread[9], 50.0, "the rows sag chose stay where they were");
7072        assert_eq!(spread[18], 100.0);
7073        assert!(spread.windows(2).all(|w| w[1] > w[0]));
7074
7075        // Three columns is flat along `u` as well; nothing to hold to.
7076        let flat = spread_to_aspect(
7077            sagged.clone(),
7078            CELL_ASPECT,
7079            0.0,
7080            16.0,
7081            |_| 3,
7082            |i| (sagged[i + 1] - sagged[i]).abs(),
7083            |a, b, _| (b - a).abs(),
7084        );
7085        assert_eq!(flat, sagged);
7086
7087        // Cells already shorter than the aspect are left alone.
7088        let fine = vec![0.0, 4.0, 8.0];
7089        let same = spread_to_aspect(
7090            fine.clone(),
7091            CELL_ASPECT,
7092            0.0,
7093            16.0,
7094            |_| 17,
7095            |i| (fine[i + 1] - fine[i]).abs(),
7096            |a, b, _| (b - a).abs(),
7097        );
7098        assert_eq!(same, fine);
7099    }
7100
7101    /// A ring's slop-duplicates and spikes come off before it is
7102    /// triangulated.
7103    #[test]
7104    fn a_ring_is_cleaned_of_duplicates_and_spikes() {
7105        let p = |x: f64, y: f64| Point2::new(x, y);
7106        // A square whose closing point repeats its first a hair off, and
7107        // whose right side steps out to a point and straight back.
7108        let mut ring = vec![
7109            p(0.0, 0.0),
7110            p(10.0, 0.0),
7111            p(10.0, 5.0),
7112            p(10.2, 5.0),
7113            p(10.0, 5.0 + 1e-9),
7114            p(10.0, 10.0),
7115            p(0.0, 10.0),
7116            p(1e-9, 1e-9),
7117        ];
7118        let mut anchors = vec![None; ring.len()];
7119        let reach = chart_reach(&ring);
7120        merge_near_duplicates(&mut ring, &mut anchors, reach);
7121        assert_eq!(ring.len(), 7, "the closing duplicate is gone: {ring:?}");
7122        remove_spikes(&mut ring, &mut anchors, reach);
7123        assert_eq!(
7124            ring,
7125            vec![
7126                p(0.0, 0.0),
7127                p(10.0, 0.0),
7128                p(10.0, 5.0),
7129                p(10.0, 10.0),
7130                p(0.0, 10.0)
7131            ],
7132            "the spike is gone and the ring is the square with a point on one side"
7133        );
7134        assert_eq!(anchors.len(), ring.len());
7135    }
7136
7137    /// A ring a ten-thousandth of a radian across a drum's chart and 365
7138    /// along it (a strip of a radius-32 drum a few microns wide) keeps its
7139    /// width: the points across it are a millionth of the ring's extent
7140    /// across apart many times over, though not a millionth of its length.
7141    /// Its closing point repeated a hair off along both directions still
7142    /// goes.
7143    #[test]
7144    fn a_ring_narrow_in_one_chart_direction_keeps_its_width() {
7145        let p = |x: f64, y: f64| Point2::new(x, y);
7146        let w = 1.77e-4;
7147        let strip = vec![
7148            p(0.0, 0.0),
7149            p(w / 2.0, 0.0),
7150            p(w, 0.0),
7151            p(w, 365.0),
7152            p(w / 2.0, 365.0),
7153            p(0.0, 365.0),
7154        ];
7155        let mut ring = strip.clone();
7156        ring.push(p(w * 1e-9, 1e-9));
7157        let mut anchors = vec![None; ring.len()];
7158        let reach = chart_reach(&ring);
7159        merge_near_duplicates(&mut ring, &mut anchors, reach);
7160        remove_spikes(&mut ring, &mut anchors, reach);
7161        assert_eq!(ring, strip, "the strip keeps every corner");
7162        assert_eq!(anchors.len(), ring.len());
7163    }
7164
7165    #[test]
7166    fn a_hole_is_outside_however_either_ring_is_wound() {
7167        // Even-odd does not depend on the wires being wound consistently, which
7168        // is what makes it survive imported geometry.
7169        let outer = vec![
7170            Point2::new(0.0, 0.0),
7171            Point2::new(4.0, 0.0),
7172            Point2::new(4.0, 4.0),
7173            Point2::new(0.0, 4.0),
7174        ];
7175        let hole: Vec<Point2> = vec![
7176            Point2::new(1.0, 1.0),
7177            Point2::new(3.0, 1.0),
7178            Point2::new(3.0, 3.0),
7179            Point2::new(1.0, 3.0),
7180        ];
7181        let backwards: Vec<Point2> = hole.iter().rev().copied().collect();
7182        for inner in [hole, backwards] {
7183            let rings = vec![outer.clone(), inner];
7184            assert!(!inside_boundary_with::<Exact>(
7185                &rings,
7186                Point2::new(2.0, 2.0)
7187            ));
7188            assert!(inside_boundary_with::<Exact>(&rings, Point2::new(0.5, 0.5)));
7189        }
7190    }
7191}