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::{Exact, OgeomResult, Predicates, Tolerances, ogeom_bail};
30use ogeom_geom::Curve3d as _;
31use ogeom_geom::{Curve2d, Surface, SurfaceGeometry};
32use ogeom_math::{Direction, Point, Point2, Vector};
33use ogeom_topo::{EdgeRepr, Model, NodeData, Orientation, Shape, ShapeType, Triangulation};
34use spade::{
35    ConstrainedDelaunayTriangulation, Point2 as SpadePoint, Triangulation as _, mitigate_underflow,
36};
37
38use crate::discretize::{Deflection, discretize};
39
40/// Triangulate one face.
41///
42/// # Errors
43///
44/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if `face` is not a
45/// face or its geometry is missing;
46/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle fails to
47/// resolve; [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if the boundary
48/// cannot be triangulated.
49pub fn triangulate_face(
50    model: &Model,
51    face: &Shape,
52    deflection: Deflection,
53    tol: Tolerances,
54) -> OgeomResult<Triangulation> {
55    // One pass at the caller's chord, and a second only where the first
56    // came back short, the same shape as the whole-shape path, so a caller
57    // meshing face by face pays for one triangulation per face, not two.
58    let nothing = EdgeChords::new();
59    let (mesh, verdict) = triangulate_reporting(model, face, deflection, Some(&nothing), tol)?;
60    if verdict != Verdict::Short {
61        return Ok(mesh);
62    }
63    triangulate_with(model, face, deflection, None, tol)
64}
65
66/// What one pass over a face found.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68enum Verdict {
69    /// The boundary enclosed a region and it was drawn.
70    Whole,
71    /// The boundary crossed itself: what came back is fragments, or nothing.
72    /// The edges want drawing finer.
73    Short,
74}
75
76/// One face, with the finer edge chords the whole shape agreed on.
77///
78/// `None` means this face is being meshed on its own and may work out its
79/// own: alone it has no neighbour to disagree with.
80fn triangulate_with(
81    model: &Model,
82    face: &Shape,
83    deflection: Deflection,
84    finer: Option<&EdgeChords>,
85    tol: Tolerances,
86) -> OgeomResult<Triangulation> {
87    let (mesh, verdict) = triangulate_reporting(model, face, deflection, finer, tol)?;
88    if verdict == Verdict::Short && mesh.triangles.is_empty() {
89        ogeom_bail!(
90            NotDone,
91            "the face's boundary enclosed no triangulable region"
92        );
93    }
94    Ok(mesh)
95}
96
97/// The surface's unit normal at `(u, v)`, or where the surface is
98/// degenerate there (a cone's apex, a sphere's pole, a patch's collapsed
99/// corner), the normal a step inside along the vertex's own column.
100///
101/// A degenerate point has no normal of its own, and `None` for it left
102/// the vertex shading black and dragged every normal it was welded with
103/// towards nothing. It has a *limit* normal along any line approaching
104/// it: the apex of a cone seen up one ruling is that ruling's normal, and
105/// a mesh vertex at the apex carries the `u` of the ruling it closes. The
106/// step is a millionth of the domain towards its middle, first along
107/// `v`, then `u`, then both; `None` only where all three are degenerate.
108fn limit_normal(surface: &SurfaceGeometry, u: f64, v: f64, tol: Tolerances) -> Option<Vector> {
109    if let Ok(n) = surface.normal_at(u, v, tol) {
110        return Some(n.vector());
111    }
112    let ((ua, ub), (va, vb)) = surface.domain();
113    let su = if u < f64::midpoint(ua, ub) { 1.0 } else { -1.0 };
114    let sv = if v < f64::midpoint(va, vb) { 1.0 } else { -1.0 };
115    // Widening steps: a patch whose corner collapses with its tangent
116    // (the control rows drawn together and the next row too) is degenerate
117    // to first order for a stretch, and a millionth of the domain is still
118    // inside it; on a sphere a millimetre across the tangents a millionth
119    // in from the pole cross to less than a direction resolves. A
120    // hundredth of the domain is past both, and on a patch that small the
121    // normal there is the corner's for every purpose.
122    for scale in [1e-6, 1e-4, 1e-2] {
123        let du = (ub - ua).abs().max(f64::EPSILON) * scale * su;
124        let dv = (vb - va).abs().max(f64::EPSILON) * scale * sv;
125        let found = [(u, v + dv), (u + du, v), (u + du, v + dv)]
126            .into_iter()
127            .find_map(|(nu, nv)| surface.normal_at(nu, nv, tol).ok().map(|n| n.vector()));
128        if found.is_some() {
129            return found;
130        }
131    }
132    None
133}
134
135/// One face, and whether the rings it was built from crossed themselves.
136///
137/// The flag is how the shape-wide pass learns which faces need their edges
138/// drawn finer without building every face's rings twice: the rings are
139/// already in hand here, and the sweep over them is the only extra cost a
140/// face that does not cross ever pays.
141fn triangulate_reporting(
142    model: &Model,
143    face: &Shape,
144    deflection: Deflection,
145    finer: Option<&EdgeChords>,
146    tol: Tolerances,
147) -> OgeomResult<(Triangulation, Verdict)> {
148    triangulate_reporting_from(model, face, deflection, finer, None, tol)
149}
150
151/// A face's rings walked ahead of drawing, and the chord its edges want
152/// if it is narrower than a few of the caller's.
153type Walked = (Trimming, Option<f64>);
154
155/// [`triangulate_reporting`] with the rings already walked, where the
156/// caller has them and none of the face's edges were told to draw finer
157/// since; the rings depend on nothing else.
158fn triangulate_reporting_from(
159    model: &Model,
160    face: &Shape,
161    deflection: Deflection,
162    finer: Option<&EdgeChords>,
163    prepared: Option<Trimming>,
164    tol: Tolerances,
165) -> OgeomResult<(Triangulation, Verdict)> {
166    deflection.validate()?;
167    if model.kind_of(face)? != ShapeType::Face {
168        ogeom_bail!(Construction, "expected a face");
169    }
170    let Some(node) = model.node(face) else {
171        ogeom_bail!(Dangling, "face is not in this model");
172    };
173    let NodeData::Face(data) = node.data() else {
174        ogeom_bail!(Construction, "face node holds no face data");
175    };
176    let Some(surface) = model.geometry().surface(data.surface) else {
177        ogeom_bail!(Dangling, "face refers to a surface not in this model");
178    };
179    let placement = face.transform(model.datums())?;
180
181    let own;
182    let finer = match finer {
183        Some(shared) => shared,
184        None => {
185            own = face_chords(model, face, data.surface, surface, deflection, tol)?;
186            &own
187        }
188    };
189    let phase = std::time::Instant::now();
190    let Trimming {
191        rings: uv,
192        anchors,
193        met,
194        walked,
195    } = match prepared {
196        Some(trim) => trim,
197        None => trimming_rings(model, face, data.surface, surface, deflection, finer, tol)?,
198    };
199    let rings_ms = phase.elapsed().as_secs_f64() * 1e3;
200    if uv.is_empty() {
201        // Bounded, and bounding next to nothing: a sliver whose sides are
202        // one line in the chart. It is drawn as a fan across each ring its
203        // edges walked, at the points its neighbours share along those
204        // edges, so the whole mesh still closes over it.
205        return Ok((sliver_fan(&walked, surface, tol), Verdict::Whole));
206    }
207    let phase = std::time::Instant::now();
208    let planar = triangulate_region(&uv, surface, deflection, tol)?;
209    let region_ms = phase.elapsed().as_secs_f64() * 1e3;
210    let phase = std::time::Instant::now();
211
212    // Whether the triangulator was handed a region at all, asked of what it
213    // returned rather than of what it was given. A well-formed triangulation
214    // over `b` boundary points in `w` rings has at least `b + 2w - 4`
215    // triangles: exactly that where no interior point is added, more where
216    // refinement adds them. Fewer is not a coarse answer, it is a different
217    // shape: the boundary crossed itself and what came back is disconnected
218    // fragments with holes between them.
219    //
220    // Counting is why it is asked this way round. Sweeping the rings for a
221    // crossing is the direct question and costs a sort and an active list
222    // per face; measured over a hundred thousand faces it was the whole of
223    // an eighteen per cent regression, to catch four bodies. The count is
224    // already in hand and exact for the failure that matters.
225    //
226    // That count is exact only before interior points go in; over a face
227    // that takes hundreds of them, a crossing that costs a handful of
228    // triangles is lost in the total. So the region also counts its own
229    // triangles the moment the boundary is in and nothing else, where the
230    // number is exactly `b + 2w - 4` for a boundary that encloses a region
231    // and anything else for one that crosses.
232    //
233    // And a face narrower than a few chords is short too, whatever its
234    // count: drawn at the caller's chord its boundary sags by more than
235    // the face is wide, and every triangle across the width stands off the
236    // surface by that sag. Its edges want a chord under the width, which
237    // is [`face_chords`]' first move; it is asked here so the whole-shape
238    // pass tells the neighbours to draw those edges finer too.
239    let boundary: usize = uv.iter().map(Vec::len).sum();
240    let narrow = narrow_chord(surface, &uv, deflection, tol)
241        .is_some_and(|want| !edges_already_at(model, face, finer, want));
242    let crossed = narrow || planar.crossed || planar.triangles.len() + 4 < boundary + 2 * uv.len();
243    if *MESH_DEBUG_REFINE {
244        eprintln!(
245            "SHORT {} triangles against {boundary} boundary points in {} rings: crossed {crossed} (boundary pass {})",
246            planar.triangles.len(),
247            uv.len(),
248            planar.crossed
249        );
250    }
251
252    // Boundary vertices take their positions from their edges' own curves
253    // (the shared authority), keyed by their exact parameter-space bits.
254    let mut anchored: std::collections::HashMap<(u64, u64), Point> =
255        std::collections::HashMap::new();
256    for (ring, ring_anchor) in uv.iter().zip(&anchors) {
257        for (p, a) in ring.iter().zip(ring_anchor) {
258            if let Some(point) = a {
259                anchored.insert((p.x.to_bits(), p.y.to_bits()), *point);
260            }
261        }
262    }
263
264    // Lift into space. The normal follows the face's orientation, not the
265    // surface's: a reversed face presents the other side, and a renderer or a
266    // volume computation that ignored that would have the solid inside out.
267    // A reflecting placement flips it once more: the mirrored chart's
268    // natural normal points the other way through the same flag.
269    let flip = (face.orientation() == Orientation::Reversed)
270        != !face.location().preserves_handedness(model.datums())?;
271    let mut mesh = Triangulation::new();
272    mesh.deflection_met = met;
273    let mut hits = 0_usize;
274    for (u, v) in planar.parameters {
275        // Anchors are already in world coordinates (their edges' own
276        // placements applied), where surface lifts still need the face's.
277        let point = match anchored.get(&(u.to_bits(), v.to_bits())) {
278            Some(anchor) => {
279                hits += 1;
280                *anchor
281            }
282            None => placement.apply(surface.point_at(u, v, tol)?),
283        };
284        let normal =
285            limit_normal(surface, u, v, tol).map_or(Vector::ZERO, |n| placement.apply_vector(n));
286        mesh.positions.push(point);
287        mesh.normals.push(if flip { -normal } else { normal });
288        mesh.parameters.push((u, v));
289    }
290    if *MESH_DEBUG_REFINE && (rings_ms + region_ms) > 50.0 {
291        eprintln!(
292            "PHASE rings {rings_ms:.0}ms region {region_ms:.0}ms lift {:.0}ms  ({} ring points, {} tris)",
293            phase.elapsed().as_secs_f64() * 1e3,
294            uv.iter().map(Vec::len).sum::<usize>(),
295            planar.triangles.len()
296        );
297    }
298    if *MESH_DEBUG {
299        eprintln!(
300            "DBG anchors map={} hits={} verts={}",
301            anchored.len(),
302            hits,
303            mesh.positions.len()
304        );
305    }
306    mesh.triangles = planar
307        .triangles
308        .into_iter()
309        .map(|t| if flip { [t[0], t[2], t[1]] } else { t })
310        .collect();
311    Ok((
312        mesh,
313        if crossed {
314            Verdict::Short
315        } else {
316            Verdict::Whole
317        },
318    ))
319}
320
321/// Triangulate every face below a shape, welded into one mesh.
322///
323/// # Errors
324///
325/// As [`triangulate_face`].
326pub fn triangulate(
327    model: &Model,
328    shape: &Shape,
329    deflection: Deflection,
330    tol: Tolerances,
331) -> OgeomResult<Triangulation> {
332    // Faces in two phases, as `tessellate` does: each face is meshed from a
333    // model nothing is writing to, in parallel; the pieces are then appended
334    // sequentially in face order. The split is what keeps the answer
335    // bit-identical at any thread count: scheduling decides only who does
336    // which face, never where its triangles land.
337    let faces: Vec<Shape> =
338        ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
339    let read_model: &Model = model;
340
341    let (computed, _) = face_meshes(read_model, &faces, deflection, tol)?;
342
343    let mut mesh = Triangulation::new();
344    let mut pieces: Vec<(usize, usize)> = Vec::with_capacity(faces.len());
345    for piece in computed {
346        let piece = piece?;
347        let t0 = mesh.triangles.len();
348        mesh.append(&piece);
349        pieces.push((t0, mesh.triangles.len()));
350    }
351    orient_pieces(&mut mesh, &pieces);
352    let mesh = mesh.welded(tol);
353
354    // A second, border-only pass at the tolerance the model itself recorded:
355    // imported edges carry the file's slop in their widened tolerances, and
356    // two faces lifting the same edge through disagreeing geometry land that
357    // far apart. Interior edges are already manifold and are not touched.
358    let mut reach = 0.0_f64;
359    for kind in [ShapeType::Edge, ShapeType::Vertex] {
360        for shape in ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(kind))? {
361            let recorded = model.node(&shape).map_or(0.0, |n| match n.data() {
362                NodeData::Edge(d) => d.tolerance.get(),
363                NodeData::Vertex(d) => d.tolerance.get(),
364                _ => 0.0,
365            });
366            reach = reach.max(recorded);
367        }
368    }
369    // Floored at a tenth of the chord the caller asked for: a border gap
370    // smaller than that is below the resolution of the mesh they accepted,
371    // whether or not the model recorded the slop that caused it.
372    let reach = reach.max(deflection.chord * 0.1);
373    let mesh = if reach > tol.confusion() {
374        let reach = reach + tol.confusion();
375        mesh.border_welded(reach).border_stitched(reach)
376    } else {
377        mesh
378    };
379    // What is left open narrower than the chord asked for is two faces
380    // sampling a shared corner differently, below the mesh's own
381    // resolution, and is sealed; a wider opening is the shape's.
382    Ok(mesh.sealed(deflection.chord))
383}
384
385/// Every face below a shape drawn to the chords the faces agree on, in face
386/// order, and those chords.
387///
388/// The first pass draws each face at the caller's chord on top of the
389/// narrow faces' asks; a face whose boundary crossed itself folds finer
390/// chords into the agreement, and only the faces touching an edge whose
391/// chord changed are drawn again. The whole-shape mesh welds these, and the
392/// stored tessellation keeps them face by face: the same meshes, drawn
393/// once.
394pub(crate) fn face_meshes(
395    read_model: &Model,
396    faces: &[Shape],
397    deflection: Deflection,
398    tol: Tolerances,
399) -> OgeomResult<(Vec<OgeomResult<Triangulation>>, EdgeChords)> {
400    let FirstPass {
401        finer,
402        mut computed,
403        changed,
404    } = first_pass(read_model, faces, deflection, tol)?;
405    if !changed.is_empty() {
406        let again: Vec<usize> = (0..faces.len())
407            .filter(|&i| {
408                ogeom_topo::explore(
409                    read_model,
410                    &faces[i],
411                    ogeom_topo::Filter::OfType(ShapeType::Edge),
412                )
413                .is_ok_and(|es| es.iter().any(|e| changed.contains(&e.node().index())))
414            })
415            .collect();
416        let redone: Vec<OgeomResult<Triangulation>> =
417            ogeom_core::parallel::map_ordered(&again, |_, &index| {
418                ogeom_core::progress::checkpoint()?;
419                triangulate_with(read_model, &faces[index], deflection, Some(&finer), tol)
420            });
421        for (index, one) in again.into_iter().zip(redone) {
422            computed[index] = one;
423        }
424    }
425    Ok((computed, finer))
426}
427
428/// What the first pass over a shape's faces produced: the chords its faces
429/// agreed to draw their shared edges to, the meshes drawn at the caller's
430/// chord, and the edges whose chord the crossed faces changed after those
431/// meshes were drawn; the faces touching one of them are drawn again.
432struct FirstPass {
433    finer: EdgeChords,
434    computed: Vec<OgeomResult<Triangulation>>,
435    changed: std::collections::HashSet<u32>,
436}
437
438/// Every face drawn once at the caller's deflection, each saying whether
439/// the rings it was drawn from crossed themselves, on top of the chords
440/// the narrow faces asked for. Nearly none cross, and those faces are
441/// finished. A face whose boundary crossed itself needs its edges drawn
442/// finer, and so does every face that shares one of them, or the two
443/// sides of that edge arrive with a different number of points, which is
444/// a worse crack than the sliver the refinement was for. Their chords
445/// are folded into the map and the edges that changed are named.
446fn first_pass(
447    read_model: &Model,
448    faces: &[Shape],
449    deflection: Deflection,
450    tol: Tolerances,
451) -> OgeomResult<FirstPass> {
452    // `Some(&finer)`, never `None`: a face left to itself refines its own
453    // edges, which is right when it is meshed alone and wrong here, where
454    // its neighbours must be told to refine the same ones. Every face is
455    // drawn at exactly what the caller asked and reports what crossed.
456    //
457    // Before that, every face's rings are walked at the caller's chord and
458    // its width read off them: a face narrower than a few chords wants its
459    // edges drawn finer, and so do the faces across those edges. Known
460    // before anything is drawn, those chords go into this pass, and the
461    // big faces round a thousand small fillets are drawn once rather than
462    // once and again. The rings are kept for the faces they still
463    // describe: every face none of whose edges the map names.
464    let (mut finer, kept) = walk_for_chords(read_model, faces, deflection, tol);
465    let touched = |face: &Shape| -> bool {
466        ogeom_topo::explore(
467            read_model,
468            face,
469            ogeom_topo::Filter::OfType(ShapeType::Edge),
470        )
471        .is_ok_and(|es| es.iter().any(|e| finer.contains_key(&e.node().index())))
472    };
473    // The rings are handed over by the job that draws the face; a shared
474    // slice cannot give them away, so each sits behind a lock it is taken
475    // from once.
476    let jobs: Vec<(&Shape, std::sync::Mutex<Option<Trimming>>)> = faces
477        .iter()
478        .zip(kept)
479        .map(|(face, prep)| {
480            let trim = prep.filter(|_| !touched(face));
481            (face, std::sync::Mutex::new(trim))
482        })
483        .collect();
484    let first: Vec<OgeomResult<(Triangulation, Verdict)>> =
485        ogeom_core::parallel::map_ordered(&jobs, |_, (face, slot)| {
486            ogeom_core::progress::checkpoint()?;
487            let trim = slot.lock().ok().and_then(|mut held| held.take());
488            triangulate_reporting_from(read_model, face, deflection, Some(&finer), trim, tol)
489        });
490    let mut computed: Vec<OgeomResult<Triangulation>> = Vec::with_capacity(faces.len());
491    let mut crossed: Vec<usize> = Vec::new();
492    for (index, one) in first.into_iter().enumerate() {
493        match one {
494            Ok((mesh, Verdict::Short)) => {
495                crossed.push(index);
496                computed.push(Ok(mesh));
497            }
498            Ok((mesh, Verdict::Whole)) => computed.push(Ok(mesh)),
499            Err(e) => computed.push(Err(e)),
500        }
501    }
502    if *MESH_DEBUG_REFINE && !crossed.is_empty() {
503        eprintln!(
504            "REFINE {} of {} faces came up short",
505            crossed.len(),
506            faces.len()
507        );
508    }
509    // On top of the walk's map, not instead of it: a neighbour drawn again
510    // must still draw the edges the walk held finer at that chord, or the
511    // two sides of one of them disagree. Only an edge whose chord
512    // *changed* sends its faces back.
513    let mut changed: std::collections::HashSet<u32> = std::collections::HashSet::new();
514    for &index in &crossed {
515        let face = &faces[index];
516        let Some(node) = read_model.node(face) else {
517            continue;
518        };
519        let NodeData::Face(data) = node.data() else {
520            continue;
521        };
522        let Some(surface) = read_model.geometry().surface(data.surface) else {
523            continue;
524        };
525        for (edge, chord) in face_chords(read_model, face, data.surface, surface, deflection, tol)?
526        {
527            let held = finer.entry(edge).or_insert(f64::INFINITY);
528            if chord < *held {
529                *held = chord;
530                changed.insert(edge);
531            }
532        }
533    }
534    Ok(FirstPass {
535        finer,
536        computed,
537        changed,
538    })
539}
540
541/// Every face's rings walked at the caller's chord, and the chords the
542/// narrow faces want their edges drawn to, agreed across the shape: the
543/// finest any face asks of an edge is what every face draws it to.
544///
545/// The rings are kept for the faces they still describe (every face none
546/// of whose edges the map names), so the whole-shape pass draws those once.
547fn walk_for_chords(
548    read_model: &Model,
549    faces: &[Shape],
550    deflection: Deflection,
551    tol: Tolerances,
552) -> (EdgeChords, Vec<Option<Trimming>>) {
553    let nothing = EdgeChords::new();
554    let prepared: Vec<OgeomResult<Option<Walked>>> =
555        ogeom_core::parallel::map_ordered(faces, |_, face| {
556            ogeom_core::progress::checkpoint()?;
557            let Some(node) = read_model.node(face) else {
558                return Ok(None);
559            };
560            let NodeData::Face(data) = node.data() else {
561                return Ok(None);
562            };
563            let Some(surface) = read_model.geometry().surface(data.surface) else {
564                return Ok(None);
565            };
566            let trim = trimming_rings(
567                read_model,
568                face,
569                data.surface,
570                surface,
571                deflection,
572                &nothing,
573                tol,
574            )?;
575            let narrow = narrow_chord(surface, &trim.rings, deflection, tol);
576            Ok(Some((trim, narrow)))
577        });
578    let mut finer = EdgeChords::new();
579    let mut kept: Vec<Option<Trimming>> = Vec::with_capacity(faces.len());
580    for (face, one) in faces.iter().zip(prepared) {
581        match one {
582            Ok(Some((prep, narrow))) => {
583                if let Some(chord) = narrow
584                    && let Ok(edges) = ogeom_topo::explore(
585                        read_model,
586                        face,
587                        ogeom_topo::Filter::OfType(ShapeType::Edge),
588                    )
589                {
590                    for edge in edges {
591                        let held = finer.entry(edge.node().index()).or_insert(chord);
592                        *held = held.min(chord);
593                    }
594                }
595                kept.push(Some(prep));
596            }
597            Ok(None) | Err(_) => kept.push(None),
598        }
599    }
600    (finer, kept)
601}
602
603/// The chords the faces below `shape` agree to draw their shared edges to.
604///
605/// A face narrower than a few chords draws its edges finer than the
606/// caller asked, and so does a face whose boundary crosses itself at that
607/// chord; the face across each of those edges must draw it the same or
608/// the two meshes disagree along it: a crack in every mesh assembled
609/// face by face. [`triangulate`] agrees this for itself; a caller meshing
610/// face by face (a viewer keeping one mesh per face) asks here once per
611/// shape and hands the answer to [`triangulate_face_with`] for every face.
612///
613/// The answer costs a draw of every face at the caller's chord, since a
614/// boundary is only known to cross itself once it is triangulated; a
615/// caller meshing face by face pays that draw twice over, and one that
616/// wants only the welded whole should ask [`triangulate`] instead.
617///
618/// # Errors
619///
620/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
621/// deflection is unusable; a face that will not draw asks for nothing
622/// rather than failing the shape.
623pub fn edge_chords_for(
624    model: &Model,
625    shape: &Shape,
626    deflection: Deflection,
627    tol: Tolerances,
628) -> OgeomResult<EdgeChords> {
629    deflection.validate()?;
630    let faces: Vec<Shape> =
631        ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
632    Ok(first_pass(model, &faces, deflection, tol)?.finer)
633}
634
635/// As [`triangulate_face`], with the edge chords the shape agreed on (the
636/// answer of [`edge_chords_for`]), so the face's boundary matches its
637/// neighbours' point for point along every edge the map names.
638///
639/// The agreement already holds every refinement the face's own boundary
640/// asked for, so the face is drawn to it and left there, as the
641/// whole-shape triangulation leaves it: refining alone past the agreed
642/// chord, as [`triangulate_face`] would, is what the neighbours cannot
643/// follow.
644///
645/// # Errors
646///
647/// As [`triangulate_face`].
648pub fn triangulate_face_with(
649    model: &Model,
650    face: &Shape,
651    deflection: Deflection,
652    chords: &EdgeChords,
653    tol: Tolerances,
654) -> OgeomResult<Triangulation> {
655    triangulate_with(model, face, deflection, Some(chords), tol)
656}
657
658/// Make the appended face meshes traverse their shared boundaries in
659/// opposite directions, flipping as few faces as possible.
660///
661/// A closed oriented surface walks each interior edge once each way. A file
662/// whose face orientation flags disagree with each other still *pairs* every
663/// edge (closed by counting, two-sided nowhere), and every integral over the
664/// result is reference-dependent garbage. The face flags cannot be judged
665/// from the model's wires (a synthesised seam's occurrence directions are
666/// chart bookkeeping, not 3D traversal), but the meshes tell the truth:
667/// shared boundary vertices are anchored to their edges' own curves, so two
668/// faces meeting along an edge carry bitwise-identical positions, and the
669/// direction each walks them is right there in the triangles. Faces are
670/// flood-filled across those shared runs, each constrained to oppose its
671/// neighbour, and each connected component keeps the polarity that flips the
672/// minority; flipping means reversing the piece's windings and negating its
673/// normals. Conflicts are left standing: this repairs orientation, not
674/// topology.
675fn orient_pieces(mesh: &mut Triangulation, pieces: &[(usize, usize)]) {
676    use std::collections::HashMap;
677    type Key = (u64, u64, u64);
678    let key = |p: &Point| -> Key { (p.x.to_bits(), p.y.to_bits(), p.z.to_bits()) };
679
680    // Directed boundary edges per piece, keyed by position: only each
681    // piece's *border* edges (used once within the piece) face other pieces.
682    let mut owners: HashMap<(Key, Key), Vec<(usize, bool)>> = HashMap::new();
683    for (i, &(t0, t1)) in pieces.iter().enumerate() {
684        let mut inside: HashMap<(u32, u32), u32> = HashMap::new();
685        for t in &mesh.triangles[t0..t1] {
686            for k in 0..3 {
687                let (a, b) = (t[k], t[(k + 1) % 3]);
688                *inside.entry((a.min(b), a.max(b))).or_default() += 1;
689            }
690        }
691        for t in &mesh.triangles[t0..t1] {
692            for k in 0..3 {
693                let (a, b) = (t[k], t[(k + 1) % 3]);
694                if inside.get(&(a.min(b), a.max(b))).copied() != Some(1) {
695                    continue;
696                }
697                let (ka, kb) = (
698                    key(&mesh.positions[a as usize]),
699                    key(&mesh.positions[b as usize]),
700                );
701                // One undirected key, direction recorded: `true` where this
702                // piece walks the smaller key first.
703                let (lo, hi, forward) = if ka <= kb {
704                    (ka, kb, true)
705                } else {
706                    (kb, ka, false)
707                };
708                owners.entry((lo, hi)).or_default().push((i, forward));
709            }
710        }
711    }
712
713    // Manifold constraints: exactly two pieces sharing a run. Same recorded
714    // direction means exactly one must flip. A pair of faces shares many
715    // runs and slop can corrupt a few, so each pair's constraint is the
716    // majority of its runs, and ties abstain rather than guess. Everything
717    // is aggregated in sorted order, so the answer does not depend on hash
718    // iteration.
719    let mut votes: std::collections::BTreeMap<(usize, usize), (u32, u32)> =
720        std::collections::BTreeMap::new();
721    for list in owners.values() {
722        if let [(a, fa), (b, fb)] = list[..]
723            && a != b
724        {
725            let pair = (a.min(b), a.max(b));
726            let entry = votes.entry(pair).or_insert((0, 0));
727            if fa == fb {
728                entry.0 += 1;
729            } else {
730                entry.1 += 1;
731            }
732        }
733    }
734    // Strongest constraints first: a pair vouched for by many runs outranks
735    // one hanging on a sliver, so when the graph carries a contradiction
736    // (an odd cycle born of slop), the weakest link is the one disbelieved.
737    // Union-find with parity keeps the whole resolution order-independent.
738    // A pair whose runs split evenly still says the faces touch: it joins
739    // the components with the consistent-shell prior, at zero strength, so
740    // a fragment cannot drift off and choose its polarity alone.
741    let mut constraints: Vec<(u32, usize, usize, bool)> = votes
742        .iter()
743        .map(|(&(a, b), &(same, opposite))| (same.abs_diff(opposite), a, b, same > opposite))
744        .collect();
745    constraints.sort_by(|x, y| y.0.cmp(&x.0).then(x.1.cmp(&y.1)).then(x.2.cmp(&y.2)));
746
747    let mut parent: Vec<usize> = (0..pieces.len()).collect();
748    // Whether a piece is flipped relative to its parent.
749    let mut parity: Vec<bool> = vec![false; pieces.len()];
750    fn find(parent: &mut [usize], parity: &mut [bool], i: usize) -> (usize, bool) {
751        if parent[i] == i {
752            return (i, false);
753        }
754        let (root, above) = find(parent, parity, parent[i]);
755        parent[i] = root;
756        parity[i] ^= above;
757        (root, parity[i])
758    }
759    for &(_, a, b, same_direction) in &constraints {
760        let (ra, pa) = find(&mut parent, &mut parity, a);
761        let (rb, pb) = find(&mut parent, &mut parity, b);
762        // One of a same-direction pair flips: their parities must differ.
763        let need = same_direction;
764        if ra == rb {
765            // Agreeing or contradicting, the die is cast; a contradiction
766            // here lost to stronger evidence.
767            continue;
768        }
769        parent[rb] = ra;
770        parity[rb] = (pa != pb) != need;
771    }
772
773    let mut flip: Vec<bool> = vec![false; pieces.len()];
774    let mut components: std::collections::BTreeMap<usize, Vec<usize>> =
775        std::collections::BTreeMap::new();
776    for (i, f) in flip.iter_mut().enumerate() {
777        let (root, p) = find(&mut parent, &mut parity, i);
778        *f = p;
779        components.entry(root).or_default().push(i);
780    }
781    for members in components.values() {
782        let flipped = members.iter().filter(|&&i| flip[i]).count();
783        if flipped * 2 > members.len() {
784            for &i in members {
785                flip[i] = !flip[i];
786            }
787        }
788    }
789
790    for (i, &(t0, t1)) in pieces.iter().enumerate() {
791        if !flip[i] {
792            continue;
793        }
794        let mut flipped_vertices: Vec<u32> = Vec::new();
795        for t in &mut mesh.triangles[t0..t1] {
796            t.swap(1, 2);
797            flipped_vertices.extend_from_slice(&t[..]);
798        }
799        flipped_vertices.sort_unstable();
800        flipped_vertices.dedup();
801        for v in flipped_vertices {
802            mesh.normals[v as usize] = -mesh.normals[v as usize];
803        }
804    }
805}
806
807/// A face's trimming boundary, in its surface's parameter space.
808///
809/// The outer wire first, then any holes, each as a closed ring of `(u, v)`
810/// points with no repeated closing point. A face with no wires covers its
811/// surface's whole domain, and gets that rectangle as its boundary.
812///
813/// Public because parameter-space trimming is not only the triangulator's
814/// concern: classifying a point against a face, splitting a face in a boolean,
815/// and hidden-line removal all ask the same question of the same rings, and
816/// each deriving them separately would be three chances to disagree.
817///
818/// # Errors
819///
820/// As [`triangulate_face`].
821pub fn face_boundary(
822    model: &Model,
823    face: &Shape,
824    deflection: Deflection,
825    tol: Tolerances,
826) -> OgeomResult<Vec<Vec<Point2>>> {
827    deflection.validate()?;
828    if model.kind_of(face)? != ShapeType::Face {
829        ogeom_bail!(Construction, "expected a face");
830    }
831    let Some(node) = model.node(face) else {
832        ogeom_bail!(Dangling, "face is not in this model");
833    };
834    let NodeData::Face(data) = node.data() else {
835        ogeom_bail!(Construction, "face node holds no face data");
836    };
837    let Some(surface) = model.geometry().surface(data.surface) else {
838        ogeom_bail!(Dangling, "face refers to a surface not in this model");
839    };
840
841    Ok(trimming_rings(
842        model,
843        face,
844        data.surface,
845        surface,
846        deflection,
847        &EdgeChords::new(),
848        tol,
849    )?
850    .rings)
851}
852
853/// The rings bounding a face in parameter space, and whether every boundary
854/// edge met its deflection.
855/// Boundary rings with, per ring vertex, the 3D anchor its edge's own curve
856/// provides; `None` where an edge has no 3D curve to defer to.
857/// The chord each edge must be drawn with, where the caller's is too coarse.
858///
859/// Keyed by the edge's node, so both faces bounding it look the same value
860/// up and sample it identically. Absent means the caller's own chord.
861/// The chord each edge is to be drawn to, by edge node index, where a face
862/// wants its edges finer than the caller's chord: what every face sharing
863/// an edge has to agree on, or their meshes disagree along it.
864pub type EdgeChords = std::collections::HashMap<u32, f64>;
865
866/// How many times a face's boundary may be redrawn finer before its
867/// crossing is taken to be something the chord cannot fix.
868///
869/// Six halvings is a chord sixty-four times tighter than asked. A sliver
870/// still crossing itself there is degenerate in a way refinement does not
871/// reach (two boundaries genuinely on top of one another), and drawing it
872/// a seventh time only spends longer to say so.
873const REFINEMENTS: usize = 6;
874
875/// What one face needs its own edges drawn with.
876///
877/// A face narrower than the chord error its boundary is drawn with has a
878/// boundary that crosses *itself*. One body of a real assembly carries a
879/// quarter-arc sliver forty-five millimetres long and eighteen microns
880/// wide: at a tenth of a millimetre the sagitta of each bounding arc is
881/// twenty-nine microns, so the inner polyline bulges straight through the
882/// outer one, and what reaches the triangulator is not a region at all. It
883/// answered with sixteen triangles in fifteen disconnected pieces, and the
884/// holes between them were what stopped the body meshing closed.
885///
886/// The deflection a caller asks for bounds how far the mesh may sit from
887/// the surface; it is not a licence to hand the triangulator a polygon that
888/// crosses itself. Refining *lowers* that error, so this never breaks the
889/// caller's bound.
890///
891/// Only this face's own edges are named, and only when they need it.
892fn face_chords(
893    model: &Model,
894    face: &Shape,
895    id: ogeom_topo::SurfaceId,
896    surface: &SurfaceGeometry,
897    deflection: Deflection,
898    tol: Tolerances,
899) -> OgeomResult<EdgeChords> {
900    let mut finer = EdgeChords::new();
901    let mut chord = deflection.chord;
902    // A face narrower than a few chords first: its edges drawn to a
903    // fraction of its width, so the boundary's sag is small against it.
904    let first = trimming_rings(model, face, id, surface, deflection, &finer, tol)?.rings;
905    if let Some(want) = narrow_chord(surface, &first, deflection, tol) {
906        chord = want;
907        for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
908            finer.insert(edge.node().index(), chord);
909        }
910    }
911    for _ in 0..=REFINEMENTS {
912        let rings = trimming_rings(model, face, id, surface, deflection, &finer, tol)?.rings;
913        let planar = triangulate_region(&rings, surface, deflection, tol)?;
914        let boundary: usize = rings.iter().map(Vec::len).sum();
915        if !planar.crossed && planar.triangles.len() + 4 >= boundary + 2 * rings.len() {
916            break;
917        }
918        chord *= 0.5;
919        for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
920            finer.insert(edge.node().index(), chord);
921        }
922    }
923    Ok(finer)
924}
925
926/// How many chords wide a face must be for its edges to be drawn at the
927/// caller's chord; narrower, they are drawn at the width over this.
928///
929/// The boundary of a face drawn at chord `c` sags up to `c` between its
930/// points, and a triangle from that boundary across the face to the
931/// other side stands off the surface by the sag. On a face `w` wide that
932/// is `c / w` of the way to standing on end; held to a quarter, the
933/// triangles lean at most fourteen degrees.
934const NARROW: f64 = 4.0;
935
936/// The chord a face narrower than [`NARROW`] chords wants its edges drawn
937/// to, `None` for a face wide enough at the caller's.
938///
939/// Width is the rings' chart extent each way scaled by the mean tangent
940/// length that way, the smaller of the two: right for a strip along a
941/// chart axis, an over-estimate for one across the chart, which then
942/// keeps the caller's chord.
943fn narrow_chord(
944    surface: &SurfaceGeometry,
945    rings: &[Vec<Point2>],
946    deflection: Deflection,
947    tol: Tolerances,
948) -> Option<f64> {
949    let (lo, hi) = chart_extent(rings);
950    if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
951        return None;
952    }
953    let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
954    for i in 0..=2 {
955        for j in 0..=2 {
956            let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
957            let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
958            if let Ok((a, b)) = surface.d1_at(u, v, tol) {
959                du += a.magnitude();
960                dv += b.magnitude();
961                n += 1;
962            }
963        }
964    }
965    if n == 0 {
966        return None;
967    }
968    #[allow(clippy::cast_precision_loss)]
969    let width = ((hi.x - lo.x) * du / n as f64).min((hi.y - lo.y) * dv / n as f64);
970    if !width.is_finite() || width <= tol.confusion() || width >= deflection.chord * NARROW {
971        return None;
972    }
973    Some(width / NARROW)
974}
975
976/// Whether every edge of `face` is already held to `want` or finer.
977fn edges_already_at(model: &Model, face: &Shape, finer: &EdgeChords, want: f64) -> bool {
978    ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge)).is_ok_and(
979        |edges| {
980            edges
981                .iter()
982                .all(|e| finer.get(&e.node().index()).is_some_and(|&c| c <= want))
983        },
984    )
985}
986
987/// A face's trimming rings, walked, folded and cleaned.
988struct Trimming {
989    /// The outer ring first, then the holes, each closed without a repeated
990    /// closing point.
991    rings: Vec<Vec<Point2>>,
992    /// Each ring point's position in space where its edge's own curve put
993    /// it, `None` where it was made up.
994    anchors: Vec<Vec<Option<Point>>>,
995    /// Whether every edge's polyline honoured the deflection.
996    met: bool,
997    /// Each ring as its edges first walked it, chart and space together:
998    /// what a face whose rings all collapse is drawn from.
999    walked: Vec<Vec<(Point2, Point)>>,
1000}
1001
1002/// One walked ring: chart points, anchors, deflection honesty, the ambiguous
1003/// whole-period folds taken, and the half-period ties left undecided.
1004type WalkedRing = (
1005    Vec<Point2>,
1006    Vec<Option<Point>>,
1007    bool,
1008    Vec<(usize, f64)>,
1009    Vec<usize>,
1010);
1011
1012fn trimming_rings(
1013    model: &Model,
1014    face: &Shape,
1015    id: ogeom_topo::SurfaceId,
1016    surface: &SurfaceGeometry,
1017    deflection: Deflection,
1018    finer: &EdgeChords,
1019    tol: Tolerances,
1020) -> OgeomResult<Trimming> {
1021    let mut rings = Vec::new();
1022    let mut ring_anchors = Vec::new();
1023    let mut ring_folds: Vec<Vec<(usize, f64)>> = Vec::new();
1024    let mut ring_ties: Vec<Vec<usize>> = Vec::new();
1025    let mut met = true;
1026    let wires = model.ordered_children_of(face)?;
1027    let bounded = !wires.is_empty();
1028    let mut walked = Vec::new();
1029    for wire in wires {
1030        let (ring, anchors, ring_met, folds, ties) =
1031            boundary_ring(model, &wire, id, deflection, finer, tol)?;
1032        met &= ring_met;
1033        if ring.len() >= 3 {
1034            let mut here = Vec::with_capacity(ring.len());
1035            for (uv, anchor) in ring.iter().zip(&anchors) {
1036                let at = match anchor {
1037                    Some(p) => *p,
1038                    None => surface.point_at(uv.x, uv.y, tol)?,
1039                };
1040                here.push((*uv, at));
1041            }
1042            walked.push(here);
1043            rings.push(ring);
1044            ring_anchors.push(anchors);
1045            ring_folds.push(folds);
1046            ring_ties.push(ties);
1047        }
1048    }
1049
1050    // A band between two *wound* rings: each boundary winds a periodic
1051    // direction once (a bore wall whose ends are a full rim and a
1052    // staircase of arcs, a torus band between two parallels, a ball's
1053    // belt between two latitudes), and neither ring closes on its own. In
1054    // the unrolled chart the face is the strip between the two chains, so
1055    // the pair is merged into one ring by two joining runs standing
1056    // exactly one period apart: their lifted points are the same 3D
1057    // points, and the weld closes them the way it closes a seam. Paired
1058    // before either is closed on its own: a rim closed alone spans the
1059    // whole surface, and two of those cancel where they overlap.
1060    {
1061        use ogeom_geom::Surface as _;
1062        let ((ua, ub), (va, vb)) = surface.domain();
1063        let axes = [
1064            (surface.is_periodic_u(), ub - ua),
1065            (surface.is_periodic_v(), vb - va),
1066        ];
1067        for (axis, (periodic, period)) in axes.into_iter().enumerate() {
1068            if !periodic || period <= 0.0 {
1069                continue;
1070            }
1071            let along = |p: Point2| if axis == 0 { p.x } else { p.y };
1072            let across = |p: Point2| if axis == 0 { p.y } else { p.x };
1073            let make = |a: f64, c: f64| {
1074                if axis == 0 {
1075                    Point2::new(a, c)
1076                } else {
1077                    Point2::new(c, a)
1078                }
1079            };
1080            let d_of = |ring: &[Point2]| -> f64 {
1081                ring.last().map_or(0.0, |l| along(*l) - along(ring[0]))
1082            };
1083            let open_wound: Vec<usize> = rings
1084                .iter()
1085                .enumerate()
1086                .filter(|(_, ring)| {
1087                    ring.len() >= 3
1088                        && (d_of(ring).abs() - period).abs() <= period * 1e-3
1089                        && ring
1090                            .last()
1091                            .is_some_and(|l| ring[0].distance(*l) > period * 0.5)
1092                })
1093                .map(|(i, _)| i)
1094                .collect();
1095            let [i, j] = open_wound[..] else {
1096                continue;
1097            };
1098            if d_of(&rings[i]).signum() == d_of(&rings[j]).signum() {
1099                continue;
1100            }
1101            let sign = d_of(&rings[i]).signum();
1102            // The column the joining runs stand on. Wherever the first rim's
1103            // chain happens to end is as good as anywhere for the rims
1104            // themselves, and no good at all when a third ring lies there:
1105            // a cross hole through a bore wall, whose loop the runs would
1106            // then cut through, two constraints refused and the face never
1107            // drawn whole. So the column is chosen where no other ring is,
1108            // in the widest gap the others leave round the period, and
1109            // both rims are re-cut to begin there: the same chains, the
1110            // same lifted points, begun a turn's fraction round.
1111            let column = {
1112                let mut spans: Vec<(f64, f64)> = Vec::new();
1113                for (k, ring) in rings.iter().enumerate() {
1114                    if k == i || k == j {
1115                        continue;
1116                    }
1117                    let (lo, hi) = ring
1118                        .iter()
1119                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
1120                            (lo.min(along(*p)), hi.max(along(*p)))
1121                        });
1122                    if lo.is_finite() && hi - lo < period {
1123                        spans.push((lo.rem_euclid(period), hi - lo));
1124                    }
1125                }
1126                let fallback = rings[i].last().map_or(0.0, |l| along(*l));
1127                if spans.is_empty() {
1128                    fallback
1129                } else {
1130                    // The widest gap between the spans' images on one turn.
1131                    let mut edges: Vec<(f64, f64)> =
1132                        spans.iter().map(|(lo, len)| (*lo, lo + len)).collect();
1133                    edges.sort_by(|a, b| a.0.total_cmp(&b.0));
1134                    let mut best = (f64::NEG_INFINITY, fallback);
1135                    for k in 0..edges.len() {
1136                        let end = edges[k].1;
1137                        let next = if k + 1 < edges.len() {
1138                            edges[k + 1].0
1139                        } else {
1140                            edges[0].0 + period
1141                        };
1142                        if next - end > best.0 {
1143                            best = (next - end, f64::midpoint(end, next));
1144                        }
1145                    }
1146                    best.1
1147                }
1148            };
1149            let recut = |chain: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>| {
1150                // The chain runs one period from its first point, either way
1151                // round; it is cut where it first reaches the column in its
1152                // own direction, and its head carried a period on to the
1153                // tail, closing on the same lifted point.
1154                let n = chain.len();
1155                if n < 3 {
1156                    return;
1157                }
1158                let forward = d_of(chain) > 0.0;
1159                let start = along(chain[0]);
1160                let target = if forward {
1161                    start + (column - start).rem_euclid(period)
1162                } else {
1163                    start - (start - column).rem_euclid(period)
1164                };
1165                let reached = |k: usize| {
1166                    if forward {
1167                        along(chain[k]) >= target
1168                    } else {
1169                        along(chain[k]) <= target
1170                    }
1171                };
1172                let Some(k) = (1..n - 1).find(|&k| reached(k)) else {
1173                    return;
1174                };
1175                if k <= 1 {
1176                    return;
1177                }
1178                let carry = if forward { period } else { -period };
1179                let mut turned: Vec<Point2> = chain[k..].to_vec();
1180                let mut turned_anchors: Vec<Option<Point>> = anchors[k..].to_vec();
1181                for (p, a) in chain[1..=k].iter().zip(&anchors[1..=k]) {
1182                    turned.push(make(along(*p) + carry, across(*p)));
1183                    turned_anchors.push(*a);
1184                }
1185                *chain = turned;
1186                *anchors = turned_anchors;
1187            };
1188            recut(&mut rings[i], &mut ring_anchors[i]);
1189            recut(&mut rings[j], &mut ring_anchors[j]);
1190            let b = rings.remove(j);
1191            let b_anchors = ring_anchors.remove(j);
1192            ring_folds.remove(j);
1193            ring_ties.remove(j);
1194            let a = &mut rings[i];
1195            let a_anchors = &mut ring_anchors[i];
1196            let a_last = a.last().copied().unwrap_or(a[0]);
1197            // Whole periods only: the second chain slides along the unrolled
1198            // chart until its start stands nearest the first chain's end.
1199            let shift = ((along(a_last) - along(b[0])) / period).round() * period;
1200            let b_first = make(along(b[0]) + shift, across(b[0]));
1201            let steps = 8;
1202            for k in 1..steps {
1203                let f = f64::from(k) / f64::from(steps);
1204                a.push(make(
1205                    along(a_last) + (along(b_first) - along(a_last)) * f,
1206                    across(a_last) + (across(b_first) - across(a_last)) * f,
1207                ));
1208                a_anchors.push(None);
1209            }
1210            for (p, anchor) in b.iter().zip(&b_anchors) {
1211                a.push(make(along(*p) + shift, across(*p)));
1212                a_anchors.push(*anchor);
1213            }
1214            // The way back: the same run, one period over, walked the other
1215            // way; the two runs lift to identical points.
1216            for k in (1..steps).rev() {
1217                let f = f64::from(k) / f64::from(steps);
1218                a.push(make(
1219                    along(a_last) + (along(b_first) - along(a_last)) * f - sign * period,
1220                    across(a_last) + (across(b_first) - across(a_last)) * f,
1221                ));
1222                a_anchors.push(None);
1223            }
1224            // The band now stands on one stretch of the chart, a period wide
1225            // from where the first rim was cut; every other ring (a hole
1226            // through the wall) is slid by whole periods into that stretch,
1227            // the same lifted points, or it stands outside the band it
1228            // belongs in.
1229            let (band_lo, band_hi) = a
1230                .iter()
1231                .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
1232                    (lo.min(along(*p)), hi.max(along(*p)))
1233                });
1234            for (k, ring) in rings.iter_mut().enumerate() {
1235                if k == i || ring.is_empty() {
1236                    continue;
1237                }
1238                #[allow(clippy::cast_precision_loss)]
1239                let mean = ring.iter().map(|p| along(*p)).sum::<f64>() / ring.len() as f64;
1240                if mean >= band_lo && mean <= band_hi {
1241                    continue;
1242                }
1243                let turns = ((mean - band_lo) / period).floor();
1244                if turns == 0.0 {
1245                    continue;
1246                }
1247                for p in ring.iter_mut() {
1248                    *p = make(along(*p) - turns * period, across(*p));
1249                }
1250            }
1251        }
1252    }
1253
1254    // A single ring still winding after the pairing had no partner, and if
1255    // it cannot close it was mis-folded: a jump of exactly one period is the
1256    // same 3D point, and the greedy fold that zeroed it may have wound the
1257    // ring instead. Undoing one such ambiguous fold (translating the walk
1258    // from there on by a period) closes the ring; the join it reopens
1259    // still lifts to a single point.
1260    if geometry_winds(surface) {
1261        use ogeom_geom::Surface as _;
1262        let ((ua, ub), _) = surface.domain();
1263        let period = ub - ua;
1264        for ((ring, folds), ties) in rings.iter_mut().zip(&ring_folds).zip(&ring_ties) {
1265            let Some(last) = ring.last().copied() else {
1266                continue;
1267            };
1268            let du = last.x - ring[0].x;
1269            let k = (du / period).round();
1270            if k == 0.0
1271                || (du - k * period).abs() > period * 1e-3
1272                || ring[0].distance(last) <= period * 1e-3
1273            {
1274                continue;
1275            }
1276            // The last ambiguous fold whose applied shift matches the
1277            // winding is the one to undo.
1278            if let Some(&(fold_start, _)) = folds
1279                .iter()
1280                .rev()
1281                .find(|(_, shift)| (shift - k * period).abs() <= period * 1e-6)
1282                && fold_start < ring.len()
1283            {
1284                // Translating from the last undecided tie before the fold
1285                // (where the walk first guessed) keeps the period jump on
1286                // the degenerate row, where it lifts to nothing.
1287                let start = ties
1288                    .iter()
1289                    .rev()
1290                    .find(|&&t| t < fold_start)
1291                    .copied()
1292                    .unwrap_or(fold_start);
1293                for p in &mut ring[start..] {
1294                    p.x -= k * period;
1295                }
1296            }
1297        }
1298    }
1299
1300    // A ring still winding after all that closes on its own: against a
1301    // pole row, or its own translate a period over the other way.
1302    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
1303        close_wound_ring(ring, anchors, surface, tol);
1304    }
1305
1306    // Points closer than the triangulator's own resolution make it refuse
1307    // the constraint; a real file's chart can carry them. The consecutive
1308    // near-duplicates collapse, anchors staying aligned.
1309    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
1310        if ring.len() < 3 {
1311            continue;
1312        }
1313        let mut extent = 0.0_f64;
1314        for pair in ring.windows(2) {
1315            extent = extent.max(pair[0].distance(pair[1]));
1316        }
1317        let eps = extent.mul_add(1e-9, 1e-12);
1318        let mut kept_ring = Vec::with_capacity(ring.len());
1319        let mut kept_anchors = Vec::with_capacity(anchors.len());
1320        for (p, a) in ring.iter().zip(anchors.iter()) {
1321            if kept_ring
1322                .last()
1323                .is_some_and(|held: &Point2| held.distance(*p) <= eps)
1324            {
1325                continue;
1326            }
1327            kept_ring.push(*p);
1328            kept_anchors.push(*a);
1329        }
1330        if kept_ring.len() > 2
1331            && let (Some(first), Some(last)) = (kept_ring.first(), kept_ring.last())
1332            && first.distance(*last) <= eps
1333        {
1334            kept_ring.pop();
1335            kept_anchors.pop();
1336        }
1337        *ring = kept_ring;
1338        *anchors = kept_anchors;
1339    }
1340    // Folded across a join, a ring on a *closed* surface can come to rest a
1341    // whole period outside the domain. A periodic surface would not mind
1342    // (it wraps), but a surface that merely closes on itself evaluates only
1343    // where its knots are, and refuses everywhere else; three bodies of one
1344    // assembly stopped meshing that way, every point of one ring a turn
1345    // past the end. The fold kept the ring continuous, which is the part
1346    // that matters, and a rigid slide by whole periods keeps it so: the
1347    // same points on the surface, named inside the chart.
1348    {
1349        use ogeom_geom::Surface as _;
1350        let ((ua, ub), (va, vb)) = surface.domain();
1351        let slides = [
1352            (!surface.is_periodic_u() && surface.is_closed_u(tol), ua, ub),
1353            (!surface.is_periodic_v() && surface.is_closed_v(tol), va, vb),
1354        ];
1355        for (across, (closed, lo, hi)) in [true, false].into_iter().zip(slides) {
1356            if !closed || hi <= lo {
1357                continue;
1358            }
1359            let span = hi - lo;
1360            for ring in &mut rings {
1361                let read = |p: &Point2| if across { p.x } else { p.y };
1362                let (least, most) = ring
1363                    .iter()
1364                    .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), p| {
1365                        (a.min(read(p)), b.max(read(p)))
1366                    });
1367                if !(least.is_finite() && most.is_finite()) || most - least > span * (1.0 + 1e-9) {
1368                    continue;
1369                }
1370                // A hair past the end is fit noise, not a period: the knots
1371                // take it, and rounding it up to a whole turn would carry the
1372                // ring a period the wrong way, which is exactly what it did
1373                // to the face this was written for, before the slack.
1374                let slack = span * 1e-6;
1375                let turns = if least < lo - slack {
1376                    ((lo - least) / span).ceil()
1377                } else if most > hi + slack {
1378                    -((most - hi) / span).ceil()
1379                } else {
1380                    0.0
1381                };
1382                if turns == 0.0 {
1383                    continue;
1384                }
1385                for p in ring.iter_mut() {
1386                    if across {
1387                        p.x += turns * span;
1388                    } else {
1389                        p.y += turns * span;
1390                    }
1391                }
1392            }
1393        }
1394    }
1395    if *MESH_DEBUG {
1396        for (i, (ring, anchors)) in rings.iter().zip(&ring_anchors).enumerate() {
1397            eprintln!("DBG ring {i}: {} points", ring.len());
1398            for (p, a) in ring.iter().zip(anchors) {
1399                eprintln!(
1400                    "DBG   uv({:.5},{:.5}) anchor {}",
1401                    p.x,
1402                    p.y,
1403                    a.map_or("-".to_string(), |q| format!(
1404                        "({:.4},{:.4},{:.4})",
1405                        q.x, q.y, q.z
1406                    ))
1407                );
1408            }
1409        }
1410    }
1411    // A run out along an edge and straight back along it (a ring that
1412    // reads `p, q, p`) is a spike into the region that bounds nothing:
1413    // the file's way of drawing a slit of no width at all on the face's
1414    // own boundary. It triangulates to two hairs and one vertex too many,
1415    // which is one triangle more than a boundary that encloses a region
1416    // has, and it is not a crossing. Off it comes, out to in, until the
1417    // ring reverses nowhere.
1418    // And two consecutive points a millionth of the ring's size apart are
1419    // one point: the end of the last edge and the start of the first, each
1420    // where its own curve put the shared vertex, a file's slop apart. Kept
1421    // both, the second sits on the first's next segment to the last bit,
1422    // and a constraint through a vertex is one the triangulation refuses.
1423    for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
1424        let reach = chart_reach(ring);
1425        merge_near_duplicates(ring, anchors, reach);
1426        remove_spikes(ring, anchors, reach);
1427    }
1428    rings.retain(|r| r.len() >= 3);
1429    ring_anchors.retain(|a| a.len() >= 3);
1430
1431    // An inner ring thinner than a micron is a slit, not a hole. A real file
1432    // draws one by running out along two arcs and back along two splines
1433    // fitted to the same arcs: a loop three millimetres long and a fifth of
1434    // a micron wide, enclosing nothing, which the triangulator can only
1435    // read as a tangle: one face carrying five of them drew with twelve
1436    // holes it does not have. Measured in space through the ring's own
1437    // anchors, so a chart's units do not enter into it; a ring not anchored
1438    // end to end is left alone, and so is the outer ring, whatever its
1439    // width, since a face that is itself a slit is a different question.
1440    // The thinnest real feature in the assembly that showed this is twenty
1441    // microns across, twenty times the cutoff.
1442    if rings.len() > 1 {
1443        let chart_area = |ring: &[Point2]| -> f64 {
1444            let mut a = 0.0;
1445            for i in 0..ring.len() {
1446                let (p, q) = (ring[i], ring[(i + 1) % ring.len()]);
1447                a += p.x * q.y - q.x * p.y;
1448            }
1449            a.abs()
1450        };
1451        let outer = (0..rings.len())
1452            .max_by(|&i, &j| chart_area(&rings[i]).total_cmp(&chart_area(&rings[j])))
1453            .unwrap_or(0);
1454        let width = |anchors: &[Option<Point>]| -> Option<f64> {
1455            let pts: Option<Vec<Point>> = anchors.iter().copied().collect();
1456            let pts = pts?;
1457            let mut normal = Vector::ZERO;
1458            let mut perimeter = 0.0;
1459            for i in 0..pts.len() {
1460                let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
1461                normal += a.to_vector().cross(b.to_vector());
1462                perimeter += a.distance(b);
1463            }
1464            (perimeter > 0.0).then(|| normal.magnitude() * 0.5 / perimeter)
1465        };
1466        let keep: Vec<bool> = (0..rings.len())
1467            .map(|i| {
1468                i == outer || width(&ring_anchors[i]).is_none_or(|w| w >= tol.confusion() * 1e4)
1469            })
1470            .collect();
1471        let mut it = keep.iter();
1472        rings.retain(|_| *it.next().unwrap_or(&true));
1473        let mut it = keep.iter();
1474        ring_anchors.retain(|_| *it.next().unwrap_or(&true));
1475    }
1476
1477    // A face with no wires covers its surface's whole domain, so the domain
1478    // rectangle is the boundary. A face whose wires all collapse (a sliver
1479    // narrower than a millionth of its own length, its two long sides one
1480    // line in the chart) encloses nothing, and keeps no ring at all: taken
1481    // for a face without wires, it would be drawn as the whole plane.
1482    if rings.is_empty() && !bounded {
1483        let ring = domain_ring(surface, deflection, tol);
1484        ring_anchors.push(vec![None; ring.len()]);
1485        rings.push(ring);
1486    }
1487    Ok(Trimming {
1488        rings,
1489        anchors: ring_anchors,
1490        met,
1491        walked,
1492    })
1493}
1494
1495/// A face with no area as a fan of triangles across each of its walked
1496/// rings, every point where its edges put it.
1497fn sliver_fan(
1498    walked: &[Vec<(Point2, Point)>],
1499    surface: &SurfaceGeometry,
1500    tol: Tolerances,
1501) -> Triangulation {
1502    let mut mesh = Triangulation::new();
1503    for ring in walked {
1504        let base = u32::try_from(mesh.positions.len()).unwrap_or(u32::MAX);
1505        for (uv, at) in ring {
1506            mesh.positions.push(*at);
1507            mesh.parameters.push((uv.x, uv.y));
1508            mesh.normals.push(
1509                surface
1510                    .normal_at(uv.x, uv.y, tol)
1511                    .map_or(ogeom_math::Vector::Z, |n| n.vector()),
1512            );
1513        }
1514        let count = u32::try_from(ring.len()).unwrap_or(0);
1515        for k in 1..count.saturating_sub(1) {
1516            mesh.triangles.push([base, base + k, base + k + 1]);
1517        }
1518    }
1519    mesh
1520}
1521
1522/// Within this of each other, two chart points of a ring are one point: a
1523/// millionth of the ring's extent, well under any feature and well over
1524/// the slop two edges leave at the vertex they share.
1525fn chart_reach(ring: &[Point2]) -> f64 {
1526    let (mut lo, mut hi) = (
1527        Point2::new(f64::INFINITY, f64::INFINITY),
1528        Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
1529    );
1530    for p in ring {
1531        lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
1532        hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
1533    }
1534    let extent = (hi.x - lo.x).max(hi.y - lo.y);
1535    if extent.is_finite() && extent > 0.0 {
1536        extent * 1e-6
1537    } else {
1538        0.0
1539    }
1540}
1541
1542/// Merge consecutive ring points within `reach` of each other,
1543/// cyclically; the earlier point and its anchor stay.
1544fn merge_near_duplicates(ring: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>, reach: f64) {
1545    if ring.len() < 2 || anchors.len() != ring.len() || reach <= 0.0 {
1546        return;
1547    }
1548    let mut i = 0;
1549    while i < ring.len() && ring.len() >= 2 {
1550        let next = (i + 1) % ring.len();
1551        let (p, q) = (ring[i], ring[next]);
1552        if (p.x - q.x).hypot(p.y - q.y) <= reach {
1553            // The later point goes (the last one when the ring's end
1554            // repeats its start), and the earlier is looked at again, in
1555            // case it sits next to another near-duplicate.
1556            if next == 0 {
1557                ring.remove(i);
1558                anchors.remove(i);
1559                break;
1560            }
1561            ring.remove(next);
1562            anchors.remove(next);
1563        } else {
1564            i += 1;
1565        }
1566    }
1567}
1568
1569/// Strip every `p, q, p` from a ring (a point stepped out to and straight
1570/// back from, the two `p` within `reach` of each other), with its anchors,
1571/// until none is left. Cyclic: the ring's last point is its first's
1572/// neighbour.
1573fn remove_spikes(ring: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>, reach: f64) {
1574    loop {
1575        let n = ring.len();
1576        if n < 3 || anchors.len() != n {
1577            return;
1578        }
1579        let Some(tip) = (0..n).find(|&i| {
1580            let (before, after) = (ring[(i + n - 1) % n], ring[(i + 1) % n]);
1581            (before.x - after.x).hypot(before.y - after.y) <= reach
1582        }) else {
1583            return;
1584        };
1585        // The tip and one of its two identical neighbours go.
1586        let neighbour = (tip + 1) % n;
1587        let (first, second) = if tip < neighbour {
1588            (neighbour, tip)
1589        } else {
1590            (tip, neighbour)
1591        };
1592        ring.remove(first);
1593        anchors.remove(first);
1594        ring.remove(second);
1595        anchors.remove(second);
1596    }
1597}
1598
1599/// Whether a surface is periodic in `u` alone: the charts on which a wound
1600/// ring cannot close against a translate of itself and needs a partner.
1601fn geometry_winds(surface: &SurfaceGeometry) -> bool {
1602    use ogeom_geom::Surface as _;
1603    surface.is_periodic_u() && !surface.is_periodic_v()
1604}
1605
1606/// Whether a point in parameter space lies inside the region `rings` bound.
1607///
1608/// Even-odd winding: inside the outer ring and outside every hole. The rings
1609/// come from wires whose direction already encodes outer from inner, but
1610/// counting crossings does not depend on that being right, which makes it
1611/// robust to a wire that was built the wrong way round.
1612///
1613/// Says nothing about a point *on* a ring: the crossing count of a boundary
1614/// point is whichever side rounding puts it. A caller that cares has to measure
1615/// its distance to the boundary and decide, which is what classification does.
1616///
1617/// Decided with exact predicates. See [`inside_boundary_with`].
1618#[must_use]
1619pub fn inside_boundary(rings: &[Vec<Point2>], p: Point2) -> bool {
1620    inside_boundary_with::<Exact>(rings, p)
1621}
1622
1623/// As [`inside_boundary`], with the predicate implementation named.
1624///
1625/// This is the seam `docs/DATA_MODEL.md` §9 describes, and it is here rather
1626/// than anywhere else because this is where the *combinatorial* decision is.
1627/// Whether a point is inside a ring is not a measurement that can be a little
1628/// wrong: it decides whether a triangle is kept or dropped, so an error near a
1629/// boundary is a hole in the mesh rather than a slightly misplaced one.
1630///
1631/// The naive form divides to find where an edge crosses the sampling ray, and
1632/// that division cancels catastrophically for a point nearly on the edge.
1633/// `orient2d` answers the same question with no division at all, and
1634/// [`Exact`] answers it correctly however close the point is.
1635#[must_use]
1636pub fn inside_boundary_with<P: Predicates>(rings: &[Vec<Point2>], p: Point2) -> bool {
1637    let mut inside = false;
1638    for ring in rings {
1639        if crosses_odd_times::<P>(ring, p) {
1640            inside = !inside;
1641        }
1642    }
1643    inside
1644}
1645
1646/// The boundary of one wire, in the face's parameter space.
1647///
1648/// Each edge is discretized in *space* and its pcurve evaluated at the resulting
1649/// parameters, so two faces sharing the edge agree on where its points are.
1650fn boundary_ring(
1651    model: &Model,
1652    wire: &Shape,
1653    surface: ogeom_topo::SurfaceId,
1654    deflection: Deflection,
1655    finer: &EdgeChords,
1656    tol: Tolerances,
1657) -> OgeomResult<WalkedRing> {
1658    let mut ring: Vec<Point2> = Vec::new();
1659    let mut anchors: Vec<Option<Point>> = Vec::new();
1660    // Ambiguous folds: where an edge was translated a whole period to
1661    // continue the walk from a start that already coincided modulo the
1662    // period; the fold was one of two defensible choices, recorded so a
1663    // mis-wound ring can be unwound.
1664    let mut folds: Vec<(usize, f64)> = Vec::new();
1665    // Half-period jumps whose side could not be decided when walked.
1666    let mut ties: Vec<usize> = Vec::new();
1667    let mut met = true;
1668    // Whether each chart direction comes back on itself: periodic, or
1669    // closed without repeating. Asked once here: closure on a spline is a
1670    // walk down a control column, and asking it at every edge of every face
1671    // of a hundred-thousand-face assembly was a tenth of the meshing time.
1672    let (wraps_u, wraps_v) = model
1673        .geometry()
1674        .surface(surface)
1675        .map_or((false, false), |g| {
1676            use ogeom_geom::Surface as _;
1677            (
1678                g.is_periodic_u() || g.is_closed_u(tol),
1679                g.is_periodic_v() || g.is_closed_v(tol),
1680            )
1681        });
1682
1683    // Start the walk off a seam if the wire allows it: a seam's side is
1684    // chosen by continuity with the point already walked to, and continuity
1685    // needs something to continue from. The ring is cyclic, so rotating the
1686    // walk changes nothing it reports.
1687    let mut children = model.ordered_children_of(wire)?;
1688    let is_seam = |model: &Model, e: &Shape| -> bool {
1689        model
1690            .node(e)
1691            .and_then(|n| n.data().as_edge())
1692            .and_then(|d| d.pcurve_for(surface, e.location()))
1693            .is_some_and(|r| matches!(r, EdgeRepr::Seam { .. }))
1694    };
1695    if let Some(start) = children.iter().position(|e| !is_seam(model, e)) {
1696        children.rotate_left(start);
1697    }
1698
1699    // The column each seam edge's first traversal effectively walked (after
1700    // folding), and whether its two sides differ in u or in v. A seam bounds
1701    // its face twice, and the two traversals must bracket the ring exactly
1702    // one period apart; the walk checks the second against this record.
1703    let mut seam_walked: std::collections::HashMap<ogeom_topo::TShapeId, Point2> =
1704        std::collections::HashMap::new();
1705
1706    for edge in children {
1707        let Some(node) = model.node(&edge) else {
1708            ogeom_bail!(Dangling, "edge is not in this model");
1709        };
1710        let NodeData::Edge(data) = node.data() else {
1711            ogeom_bail!(Construction, "edge node holds no edge data");
1712        };
1713        // Whether this edge is a seam, and if so whether its two sides
1714        // differ in u (true) or in v.
1715        let mut seam: Option<bool> = None;
1716        let (pcurve_id, pcurve_range) = match data.pcurve_for(surface, edge.location()) {
1717            Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
1718            // A seam edge runs along a closed surface's join and bounds its
1719            // face twice: up one side of the parameter rectangle and down
1720            // the other. Which side this occurrence takes is decided by the
1721            // ring itself: the side whose oriented start continues the point
1722            // already walked to. Orientation flags cannot answer it (a
1723            // reversed face flips every occurrence while the chart columns
1724            // stay where they were built), but the chart can.
1725            Some(EdgeRepr::Seam {
1726                forward,
1727                reversed,
1728                range,
1729                ..
1730            }) => {
1731                let (f, r) = (*forward, *reversed);
1732                let side_start = |id: ogeom_topo::PCurveId| -> Option<Point2> {
1733                    model.geometry().pcurve(id)?.point_at(range.0, tol).ok()
1734                };
1735                seam = Some(match (side_start(f), side_start(r)) {
1736                    (Some(a), Some(b)) => (a.x - b.x).abs() >= (a.y - b.y).abs(),
1737                    _ => true,
1738                });
1739                let picked = if let Some(last) = ring.last().copied() {
1740                    let start_of = |id: ogeom_topo::PCurveId| -> Option<Point2> {
1741                        let pc = model.geometry().pcurve(id)?;
1742                        let t = if edge.orientation() == Orientation::Reversed {
1743                            range.1
1744                        } else {
1745                            range.0
1746                        };
1747                        pc.point_at(t, tol).ok()
1748                    };
1749                    match (start_of(f), start_of(r)) {
1750                        (Some(a), Some(b)) => {
1751                            if last.distance(a) <= last.distance(b) {
1752                                f
1753                            } else {
1754                                r
1755                            }
1756                        }
1757                        _ => f,
1758                    }
1759                } else if edge.orientation() == Orientation::Reversed {
1760                    r
1761                } else {
1762                    f
1763                };
1764                (picked, *range)
1765            }
1766            _ => ogeom_bail!(
1767                Construction,
1768                "edge has no pcurve on this face, so the face cannot be \
1769                 triangulated in its own parameter space"
1770            ),
1771        };
1772        let Some(pcurve) = model.geometry().pcurve(pcurve_id) else {
1773            ogeom_bail!(Dangling, "pcurve is not in this model");
1774        };
1775
1776        // Sample where the *3D* curve says to, so an adjacent face lands on
1777        // the same points, and *anchor* the boundary vertices to that curve
1778        // too: two faces sharing an edge lift the same parameters through
1779        // different surfaces, and on an imported file those surfaces
1780        // disagree by the file's own slop. The edge is the shared authority,
1781        // so its points are the positions both faces use, and the weld is a
1782        // matter of identity rather than luck.
1783        let mut edge_anchors: Vec<Option<Point>> = Vec::new();
1784        // An edge drawn finer is drawn finer for *every* face that bounds
1785        // it, which is the whole point: the two sides must agree point for
1786        // point or the weld has nothing to join.
1787        let along = match finer.get(&edge.node().index()) {
1788            Some(chord) => Deflection {
1789                chord: *chord,
1790                ..deflection
1791            },
1792            None => deflection,
1793        };
1794        let samples = match sample_parameters(model, data, along, tol)? {
1795            Some((parameters, edge_met)) => {
1796                met &= edge_met;
1797                if let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d()
1798                    && let Some(geometry) = model.geometry().curve(*curve)
1799                    && let Ok(edge_placement) = edge.transform(model.datums())
1800                {
1801                    for t in &parameters {
1802                        edge_anchors.push(
1803                            geometry
1804                                .point_at(*t, tol)
1805                                .ok()
1806                                .map(|p| edge_placement.apply(p)),
1807                        );
1808                    }
1809                }
1810                map_to_pcurve(&parameters, data, pcurve_range)
1811            }
1812            // No 3D curve to defer to: the pcurve's own shape, measured in
1813            // space through the surface, so the chord tolerance means the
1814            // same thing it means everywhere else.
1815            None => {
1816                let Some(geometry) = model.geometry().surface(surface) else {
1817                    ogeom_bail!(Dangling, "face refers to a surface not in this model");
1818                };
1819                let (_, parameters) = crate::discretize::discretize_on_surface(
1820                    pcurve,
1821                    pcurve_range,
1822                    geometry,
1823                    deflection,
1824                    tol,
1825                )?;
1826                parameters
1827            }
1828        };
1829
1830        let mut points: Vec<Point2> = samples
1831            .iter()
1832            .map(|u| pcurve.point_at(*u, tol))
1833            .collect::<OgeomResult<_>>()?;
1834        if edge.orientation() == Orientation::Reversed {
1835            points.reverse();
1836            edge_anchors.reverse();
1837        }
1838        if edge_anchors.len() != points.len() {
1839            edge_anchors = vec![None; points.len()];
1840        }
1841        // The ends of an edge belong to its *vertices* (the one authority
1842        // every face and every neighbouring edge shares), but only within the
1843        // tolerance the vertex itself records. An imported curve ends within
1844        // the vertex's widened tolerance of it, and lifting the ends through
1845        // the curve alone would leave each corner split as many ways as there
1846        // are curves meeting there; a vertex that sits *beyond* its stated
1847        // tolerance from the curve is not describing the curve's end at all,
1848        // and the curve stays the authority.
1849        if !points.is_empty()
1850            && let Ok(vs) = model.children_of(&edge)
1851            && vs.len() >= 2
1852            && let Ok(edge_placement) = edge.transform(model.datums())
1853        {
1854            let point_of = |v: &Shape| -> Option<(Point, f64)> {
1855                let data = model.node(v)?.data().as_vertex()?;
1856                Some((edge_placement.apply(data.point), data.tolerance.get()))
1857            };
1858            let (from, to) = if edge.orientation() == Orientation::Reversed {
1859                (&vs[vs.len() - 1], &vs[0])
1860            } else {
1861                (&vs[0], &vs[vs.len() - 1])
1862            };
1863            if let Some((p, within)) = point_of(from)
1864                && let Some(a) = edge_anchors.first_mut()
1865                && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
1866            {
1867                *a = Some(p);
1868            }
1869            if let Some((p, within)) = point_of(to)
1870                && let Some(a) = edge_anchors.last_mut()
1871                && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
1872            {
1873                *a = Some(p);
1874            }
1875        }
1876        // Fold onto the branch that continues the ring. Two faces sharing an
1877        // edge share its pcurve, and on a periodic surface the pcurve sits in
1878        // *one* face's window: a cylinder split into two halves has a ruling
1879        // at u = 0 that the other half needs at u = 2pi. The chart cannot
1880        // store both; continuity with the ring being walked recovers the
1881        // right branch, exactly as the seam sides are chosen.
1882        if let Some(last) = ring.last().copied()
1883            && let Some(first) = points.first().copied()
1884            && let Some(geometry) = model.geometry().surface(surface)
1885        {
1886            use ogeom_geom::Surface as _;
1887            let ((ua, ub), (va, vb)) = geometry.domain();
1888            // Nearest whole period, ties broken toward *not moving*: a jump
1889            // of exactly half a period is what a boundary crossing a
1890            // degenerate row looks like (two rulings into an apex stand
1891            // half a turn apart, and the connecting run along the apex row
1892            // lifts to nothing), and folding it would drag the edge a full
1893            // period from the column its own projection put it on.
1894            let whole_periods = |gap: f64, span: f64| -> f64 {
1895                let r = gap / span;
1896                if (r.fract().abs() - 0.5).abs() <= 1e-9 {
1897                    r.trunc() * span
1898                } else {
1899                    r.round() * span
1900                }
1901            };
1902            let mut shift = Point2::new(0.0, 0.0);
1903            // A repeated seam edge folds like any other, but never onto its
1904            // own first traversal. The two traversals bound the face up one
1905            // side of the chart and down the other, one period apart, and at
1906            // a degenerate row continuity cannot say so: a cone walked to
1907            // its apex reaches a corner that maps to the whole row, both
1908            // sides continue it equally, and folding by nearness closes the
1909            // ring over nothing. The record decides instead: land exactly a
1910            // period from the first walk, on the side the ring occupies.
1911            let prior = seam.and_then(|_| seam_walked.get(&edge.node())).copied();
1912            if wraps_u && (ub - ua) > 0.0 {
1913                let span = ub - ua;
1914                let gap = last.x - first.x;
1915                shift.x = whole_periods(gap, span);
1916                let mut bracketed = false;
1917                if seam == Some(true)
1918                    && let Some(prior) = prior
1919                    && (first.x + shift.x - prior.x).abs() < span * 0.5
1920                {
1921                    let (lo, hi) = ring
1922                        .iter()
1923                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
1924                            (lo.min(p.x), hi.max(p.x))
1925                        });
1926                    // Bracketing is for the face that wraps the period: a
1927                    // band whose rims run the whole way round, its two seam
1928                    // columns a period apart. A *slit* uses one edge twice
1929                    // without wrapping: the ring stays in a fraction of the
1930                    // chart, both traversals stand on one column, and
1931                    // forcing them apart winds the ring, invites a pole row
1932                    // it never touches, and meshes the complement of the
1933                    // face. The ring's own reach
1934                    // says which face this is.
1935                    if hi - lo >= span * 0.5 {
1936                        let side = if f64::midpoint(lo, hi) >= prior.x {
1937                            1.0
1938                        } else {
1939                            -1.0
1940                        };
1941                        shift.x = prior.x + side * span - first.x;
1942                        bracketed = true;
1943                    }
1944                }
1945                if !bracketed {
1946                    if shift.x != 0.0 && (gap - shift.x).abs() <= span * 1e-6 {
1947                        // The start already stood a whole period from the walk
1948                        // (the same 3D point), so this fold is a choice, not a
1949                        // repair; recorded so a mis-wound ring can be unwound.
1950                        folds.push((ring.len(), shift.x));
1951                    } else if ((gap / span).fract().abs() - 0.5).abs() <= 1e-9 {
1952                        // A half-period tie: either side of the degenerate row
1953                        // was defensible, and if the ring comes out wound the
1954                        // unwinding starts here rather than at the later fold.
1955                        ties.push(ring.len());
1956                    }
1957                }
1958            }
1959            if wraps_v && (vb - va) > 0.0 {
1960                let span = vb - va;
1961                shift.y = whole_periods(last.y - first.y, span);
1962                if seam == Some(false)
1963                    && let Some(prior) = prior
1964                    && (first.y + shift.y - prior.y).abs() < span * 0.5
1965                {
1966                    let (lo, hi) = ring
1967                        .iter()
1968                        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
1969                            (lo.min(p.y), hi.max(p.y))
1970                        });
1971                    let side = if f64::midpoint(lo, hi) >= prior.y {
1972                        1.0
1973                    } else {
1974                        -1.0
1975                    };
1976                    shift.y = prior.y + side * span - first.y;
1977                }
1978            }
1979            if shift.x != 0.0 || shift.y != 0.0 {
1980                for p in &mut points {
1981                    p.x += shift.x;
1982                    p.y += shift.y;
1983                }
1984            }
1985        }
1986        if seam.is_some()
1987            && let Some(first) = points.first().copied()
1988        {
1989            seam_walked.entry(edge.node()).or_insert(first);
1990        }
1991        // The previous edge already contributed the shared vertex, but only
1992        // where the chart agrees it is shared. Two rulings meeting at an
1993        // apex share the *vertex* while standing apart in the chart, and the
1994        // run between them along the degenerate row is boundary the ring
1995        // needs: dropping its start would cut the corner straight through
1996        // the face's interior.
1997        //
1998        // Two things have to hold for a gap to be such a run, and neither
1999        // alone is enough. It has to be wide: two ends that disagree by the
2000        // file's slop stand a micron over a radius apart, and a thousandth
2001        // of the period is three orders above that. And what lies between
2002        // has to be degenerate: a run along an apex row lifts to one point
2003        // the whole way, so its chart midpoint lands on the shared vertex,
2004        // where a wide gap on a live row lifts to somewhere the width of the
2005        // gap away. Width alone kept slop on fitted splines; the lift alone
2006        // kept every gap too small for its midpoint to land anywhere else.
2007        // And no fraction of the period alone is right at all: one real
2008        // part's rulings stand exactly a quarter turn apart, which a
2009        // quarter-period test read as not apart, and the face lost the
2010        // triangle at its apex.
2011        let keep_gap = if let (Some(last), Some(first)) = (ring.last(), points.first()) {
2012            model.geometry().surface(surface).is_some_and(|geometry| {
2013                use ogeom_geom::Surface as _;
2014                let ((ua, ub), (va, vb)) = geometry.domain();
2015                let wide = (wraps_u && (last.x - first.x).abs() > (ub - ua) * 1e-3)
2016                    || (wraps_v && (last.y - first.y).abs() > (vb - va) * 1e-3);
2017                if !wide {
2018                    return false;
2019                }
2020                let mid = Point2::new(
2021                    f64::midpoint(last.x, first.x),
2022                    f64::midpoint(last.y, first.y),
2023                );
2024                let reach = tol.confusion() * 1e4;
2025                match (
2026                    geometry.point_at(last.x, last.y, tol),
2027                    geometry.point_at(mid.x, mid.y, tol),
2028                    geometry.point_at(first.x, first.y, tol),
2029                ) {
2030                    (Ok(a), Ok(m), Ok(b)) => a.distance(m) <= reach && m.distance(b) <= reach,
2031                    _ => false,
2032                }
2033            })
2034        } else {
2035            false
2036        };
2037        if !ring.is_empty() && !points.is_empty() && !keep_gap {
2038            points.remove(0);
2039            edge_anchors.remove(0);
2040        }
2041        ring.extend(points);
2042        anchors.extend(edge_anchors);
2043    }
2044
2045    // A closed ring repeats its first point at the end; the triangulator wants
2046    // it named once.
2047    if ring.len() > 2
2048        && let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied())
2049    {
2050        // Equal in the chart, or the same vertex in space: the last edge's
2051        // curve ends where the first edge's begins to within the file's
2052        // slop: up to ten microns in a real assembly, recorded on the
2053        // vertex as its widened tolerance. Kept as two points, the ring
2054        // closes with a fold back over its own first segment, a crossing
2055        // a fraction of a micron deep that the triangulation refuses as a
2056        // constraint and the face is then drawn six times finer for.
2057        // The same vertex in space is not enough on its own: a ring that
2058        // winds a periodic chart ends a whole period from where it began
2059        // and lifts to the same point, and that closing is a seam, not
2060        // slop. Close in the chart too (within a thousandth of the ring's
2061        // own extent), or the ring is left to the winding rule below.
2062        let extent = ring.iter().fold(
2063            (
2064                Point2::new(f64::INFINITY, f64::INFINITY),
2065                Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2066            ),
2067            |(lo, hi), p| {
2068                (
2069                    Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
2070                    Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
2071                )
2072            },
2073        );
2074        let extent = (extent.1.x - extent.0.x).max(extent.1.y - extent.0.y);
2075        let near_in_chart = (first.x - last.x).hypot(first.y - last.y) <= extent * 1e-3;
2076        let same_vertex = near_in_chart
2077            && match (anchors.first(), anchors.last()) {
2078                (Some(Some(a)), Some(Some(b))) => a.distance(*b) <= tol.confusion() * 1e5,
2079                _ => false,
2080            };
2081        if first.is_equal(last, tol) || same_vertex {
2082            ring.pop();
2083            anchors.pop();
2084        }
2085    }
2086    Ok((ring, anchors, met, folds, ties))
2087}
2088
2089/// Close a ring that winds a periodic direction of its chart and found no
2090/// partner to pair with.
2091///
2092/// A ring that winds one periodic direction of a doubly-periodic surface
2093/// (a diagonal loop on a torus) ends a whole period from where it
2094/// began, and the face is the band between the chain and its own
2095/// translate one period over in the *other* periodic direction, joined
2096/// at the ends by columns that lift to one 3D circle. The translate's
2097/// anchors are the same 3D points, and the joining columns' two copies
2098/// lift identically, so the weld closes them exactly as it closes a seam.
2099/// On a cone or a sphere the ring closes against the row where the
2100/// surface collapses to a point (the apex or the pole), which every `u`
2101/// reaches: that closure costs no area, because the row has none.
2102///
2103/// A ring that has a partner (the other rim of a band) is not closed
2104/// here but paired with it, or each rim would close the whole surface on
2105/// its own and the two would cancel where they overlap.
2106fn close_wound_ring(
2107    ring: &mut Vec<Point2>,
2108    anchors: &mut Vec<Option<Point>>,
2109    geometry: &SurfaceGeometry,
2110    tol: Tolerances,
2111) {
2112    let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied()) else {
2113        return;
2114    };
2115    // A ring that winds one periodic direction of a doubly-periodic
2116    // surface: a diagonal loop on a torus. The folded walk ends a
2117    // whole period from where it began, and the face is the band
2118    // between the chain and its own translate one period over in the
2119    // *other* periodic direction, joined at the ends by columns that
2120    // lift to one 3D circle. The translate's anchors are the same 3D
2121    // points, and the joining columns' two copies lift identically,
2122    // so the weld closes them exactly as it closes a seam.
2123    use ogeom_geom::Surface as _;
2124    let ((ua, ub), (va, vb)) = geometry.domain();
2125    let du = last.x - first.x;
2126    let dv = last.y - first.y;
2127    let winds_u = geometry.is_periodic_u()
2128        && (du.abs() - (ub - ua)).abs() <= (ub - ua) * 1e-3
2129        && dv.abs() <= (vb - va).max(1.0) * 1e-3;
2130    let winds_v = geometry.is_periodic_u()
2131        && geometry.is_periodic_v()
2132        && (dv.abs() - (vb - va)).abs() <= (vb - va) * 1e-3
2133        && du.abs() <= (ub - ua).max(1.0) * 1e-3;
2134    // Where does a u-winding ring close against? On a doubly
2135    // periodic surface, its own translate one v-period over. On a
2136    // cone or sphere, the row where the surface collapses to a point
2137    // (the apex or the pole), which every u reaches: the closure
2138    // costs no area error because the row has none.
2139    let degenerate_row = |v: f64| -> bool {
2140        let (Ok(p), Ok(q), Ok(r)) = (
2141            geometry.point_at(ua, v, tol),
2142            geometry.point_at(f64::midpoint(ua, ub), v, tol),
2143            geometry.point_at(ub, v, tol),
2144        ) else {
2145            return false;
2146        };
2147        p.distance(q) <= tol.confusion() * 10.0 && p.distance(r) <= tol.confusion() * 10.0
2148    };
2149    let target_v = if winds_u && !geometry.is_periodic_v() {
2150        // The nearer degenerate row, if either end has one.
2151        let mid_v = f64::midpoint(first.y, last.y);
2152        if degenerate_row(va) && (mid_v - va).abs() <= (mid_v - vb).abs() {
2153            Some(va)
2154        } else if degenerate_row(vb) {
2155            Some(vb)
2156        } else if degenerate_row(va) {
2157            Some(va)
2158        } else {
2159            None
2160        }
2161    } else {
2162        None
2163    };
2164    let column_steps = 8;
2165    if let Some(v_apex) = target_v {
2166        // Down the seam column to the apex row, across it, and back
2167        // up: the row has no length in space, so the closure adds no
2168        // area and its lifted points weld to the one apex.
2169        let row_steps = ring.len().max(8);
2170        for k in 1..=column_steps {
2171            let f = f64::from(k) / f64::from(column_steps);
2172            ring.push(Point2::new(last.x, last.y + (v_apex - last.y) * f));
2173            anchors.push(None);
2174        }
2175        for k in 1..row_steps {
2176            #[allow(clippy::cast_precision_loss)]
2177            let f = k as f64 / row_steps as f64;
2178            ring.push(Point2::new(last.x + (first.x - last.x) * f, v_apex));
2179            anchors.push(None);
2180        }
2181        for k in 0..column_steps {
2182            let f = f64::from(column_steps - k) / f64::from(column_steps);
2183            ring.push(Point2::new(first.x, first.y + (v_apex - first.y) * f));
2184            anchors.push(None);
2185        }
2186    } else if (winds_u && geometry.is_periodic_v()) || winds_v {
2187        let shift = if winds_u {
2188            Point2::new(0.0, -(vb - va))
2189        } else {
2190            Point2::new(-(ub - ua), 0.0)
2191        };
2192        let chain: Vec<Point2> = ring.clone();
2193        let chain_anchors = anchors.clone();
2194        // Down from the chain's end to its translate's end.
2195        for k in 1..=column_steps {
2196            let f = f64::from(k) / f64::from(column_steps);
2197            ring.push(Point2::new(last.x + shift.x * f, last.y + shift.y * f));
2198            anchors.push(None);
2199        }
2200        // The translate, walked back.
2201        for (p, a) in chain.iter().rev().zip(chain_anchors.iter().rev()).skip(1) {
2202            ring.push(Point2::new(p.x + shift.x, p.y + shift.y));
2203            anchors.push(*a);
2204        }
2205        // Up from the translate's start back to the chain's start,
2206        // stopping one step short of closing.
2207        for k in 1..column_steps {
2208            let f = f64::from(column_steps - k) / f64::from(column_steps);
2209            ring.push(Point2::new(first.x + shift.x * f, first.y + shift.y * f));
2210            anchors.push(None);
2211        }
2212    }
2213}
2214
2215/// Parameters at which to sample an edge, taken from its 3D curve.
2216fn sample_parameters(
2217    model: &Model,
2218    data: &ogeom_topo::EdgeData,
2219    deflection: Deflection,
2220    tol: Tolerances,
2221) -> OgeomResult<Option<(Vec<f64>, bool)>> {
2222    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
2223        return Ok(None);
2224    };
2225    let Some(geometry) = model.geometry().curve(*curve) else {
2226        ogeom_bail!(Dangling, "curve is not in this model");
2227    };
2228    let line = discretize(geometry, *range, deflection, tol)?;
2229    Ok(Some((line.parameters, line.deflection_met)))
2230}
2231
2232/// Map parameters on the 3D curve onto the pcurve's own range.
2233///
2234/// The two are parameterized over their own intervals; `same_parameter` means
2235/// they agree *proportionally*, which is what this converts.
2236fn map_to_pcurve(
2237    parameters: &[f64],
2238    data: &ogeom_topo::EdgeData,
2239    pcurve_range: (f64, f64),
2240) -> Vec<f64> {
2241    let Some(EdgeRepr::Curve3d { range, .. }) = data.curve3d() else {
2242        return parameters.to_vec();
2243    };
2244    let (ca, cb) = *range;
2245    let (pa, pb) = pcurve_range;
2246    if (cb - ca).abs() <= f64::MIN_POSITIVE {
2247        return parameters.to_vec();
2248    }
2249    parameters
2250        .iter()
2251        .map(|u| pa + (pb - pa) * (u - ca) / (cb - ca))
2252        .collect()
2253}
2254
2255/// The edge of a surface's domain, as a boundary ring.
2256///
2257/// Refined, not just the four corners. A triangulation only ever connects the
2258/// points it is given, so a boundary named by its corners alone forces long
2259/// triangles reaching right across the domain to find one: on a sphere, a
2260/// sliver from the equator to the pole. The interior refinement cannot fix that;
2261/// the missing points are on the boundary.
2262fn domain_ring(surface: &SurfaceGeometry, deflection: Deflection, tol: Tolerances) -> Vec<Point2> {
2263    let ((ua, ub), (va, vb)) = surface.domain();
2264    if ![ua, ub, va, vb].iter().all(|x| x.is_finite()) {
2265        return Vec::new();
2266    }
2267
2268    let along_u = |v: f64| {
2269        refine_direction(ua, ub, deflection.chord, |a, b| {
2270            cell_error(surface, (a, v), (b, v), deflection, tol)
2271        })
2272    };
2273    let along_v = |u: f64| {
2274        refine_direction(va, vb, deflection.chord, |a, b| {
2275            cell_error(surface, (u, a), (u, b), deflection, tol)
2276        })
2277    };
2278
2279    // Counter-clockwise around the rectangle. Each side drops its final point,
2280    // which the next side contributes: a repeated vertex would be a
2281    // zero-length boundary edge, and a constraint of zero length is not one.
2282    let (bottom, top) = (along_u(va), along_u(vb));
2283    let (left, right) = (along_v(ua), along_v(ub));
2284    let mut ring = Vec::new();
2285    ring.extend(
2286        bottom[..bottom.len() - 1]
2287            .iter()
2288            .map(|u| Point2::new(*u, va)),
2289    );
2290    ring.extend(right[..right.len() - 1].iter().map(|v| Point2::new(ub, *v)));
2291    ring.extend(top[1..].iter().rev().map(|u| Point2::new(*u, vb)));
2292    ring.extend(left[1..].iter().rev().map(|v| Point2::new(ua, *v)));
2293    ring
2294}
2295
2296/// A triangulation still in parameter space, before it is lifted onto the
2297/// surface.
2298struct PlanarMesh {
2299    /// The `(u, v)` of each vertex.
2300    parameters: Vec<(f64, f64)>,
2301    /// Triangles as indices into `parameters`.
2302    triangles: Vec<[u32; 3]>,
2303    /// The boundary alone did not triangulate to the count a boundary that
2304    /// encloses a region gives: it crosses itself somewhere.
2305    crossed: bool,
2306}
2307
2308/// Triangulate a region in parameter space, given its boundary rings.
2309///
2310/// The first ring is the outer boundary; the rest are holes.
2311fn triangulate_region(
2312    rings: &[Vec<Point2>],
2313    surface: &SurfaceGeometry,
2314    deflection: Deflection,
2315    tol: Tolerances,
2316) -> OgeomResult<PlanarMesh> {
2317    // A chart mangled enough (trims fitted through millimetres of boundary
2318    // error) can drive the triangulation library past its own asserts, and
2319    // a panic in a dependency is a crash in every consumer. The rings are
2320    // screened for what provably breaks it, and whatever still slips
2321    // through is caught at this boundary and spoken as the refusal it is:
2322    // the kernel's contract is refusal by name, never a crash on bad input.
2323    for ring in rings {
2324        let mut extent = 0.0_f64;
2325        for p in ring {
2326            if !p.x.is_finite() || !p.y.is_finite() {
2327                ogeom_bail!(
2328                    NotDone,
2329                    "a boundary ring carries a non-finite chart coordinate; \
2330                     the face's trim does not describe a region"
2331                );
2332            }
2333            extent = extent.max(p.x.abs()).max(p.y.abs());
2334        }
2335        if extent > 1e12 {
2336            ogeom_bail!(
2337                NotDone,
2338                "a boundary ring reaches {extent:.1e} in the chart; a trim \
2339                 that far out describes no face"
2340            );
2341        }
2342    }
2343    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2344        triangulate_region_inner(rings, surface, deflection, tol)
2345    })) {
2346        Ok(result) => result,
2347        Err(_) => ogeom_bail!(
2348            NotDone,
2349            "the face's boundary broke the triangulation; the chart is too \
2350             degenerate to mesh"
2351        ),
2352    }
2353}
2354
2355/// The chart stretched per axis to the surface's own metric, so that the
2356/// triangulation (Delaunay in the chart) sees distances as space does.
2357///
2358/// A fitted strip a fifth of a millimetre wide and a centimetre long may
2359/// carry `u` over a fiftieth of a unit and `v` over one: in the chart the
2360/// long way is the short way, sixty times over, and Delaunay, which
2361/// connects nearest neighbours *in the chart*, joins points along the
2362/// strip across several columns rather than to the row beside them. The
2363/// triangles it makes are slivers in space that lift folded (a flat
2364/// triangle spanning a bend the surface takes in between, its normal
2365/// pointing where none of its vertices' do), and the face shades as a
2366/// quilt of creases. Scaled by the mean tangent length each way, the
2367/// chart is the surface to first order, and Delaunay in it is Delaunay
2368/// on the surface.
2369#[derive(Debug, Clone, Copy)]
2370struct ChartScale {
2371    su: f64,
2372    sv: f64,
2373}
2374
2375impl ChartScale {
2376    /// The mean tangent length each way over the rings' extent, the longer
2377    /// normalized to one; `(1, 1)` where the surface will not say.
2378    fn of(surface: &SurfaceGeometry, rings: &[Vec<Point2>], tol: Tolerances) -> Self {
2379        let (lo, hi) = chart_extent(rings);
2380        if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
2381            return Self { su: 1.0, sv: 1.0 };
2382        }
2383        let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
2384        for i in 0..=2 {
2385            for j in 0..=2 {
2386                let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
2387                let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
2388                if let Ok((a, b)) = surface.d1_at(u, v, tol) {
2389                    du += a.magnitude();
2390                    dv += b.magnitude();
2391                    n += 1;
2392                }
2393            }
2394        }
2395        if n == 0 || !(du > 0.0 && dv > 0.0) || !du.is_finite() || !dv.is_finite() {
2396            return Self { su: 1.0, sv: 1.0 };
2397        }
2398        let longer = du.max(dv);
2399        Self {
2400            su: du / longer,
2401            sv: dv / longer,
2402        }
2403    }
2404
2405    /// A chart point into the scaled chart.
2406    fn to(self, u: f64, v: f64) -> SpadePoint<f64> {
2407        SpadePoint::new(u * self.su, v * self.sv)
2408    }
2409
2410    /// A scaled-chart point back into the chart.
2411    fn from(self, x: f64, y: f64) -> (f64, f64) {
2412        (x / self.su, y / self.sv)
2413    }
2414}
2415
2416/// The rings' bounding box in the chart.
2417fn chart_extent(rings: &[Vec<Point2>]) -> (Point2, Point2) {
2418    rings.iter().flatten().fold(
2419        (
2420            Point2::new(f64::INFINITY, f64::INFINITY),
2421            Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2422        ),
2423        |(lo, hi), p| {
2424            (
2425                Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
2426                Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
2427            )
2428        },
2429    )
2430}
2431
2432fn triangulate_region_inner(
2433    rings: &[Vec<Point2>],
2434    surface: &SurfaceGeometry,
2435    deflection: Deflection,
2436    tol: Tolerances,
2437) -> OgeomResult<PlanarMesh> {
2438    let mut cdt: ConstrainedDelaunayTriangulation<SpadePoint<f64>> =
2439        ConstrainedDelaunayTriangulation::new();
2440    let sub = std::time::Instant::now();
2441    let mut refused_total = 0usize;
2442
2443    // Everything the triangulation sees is in the scaled chart; the rings'
2444    // own chart points are kept by their scaled bits so a boundary vertex
2445    // comes back with exactly the parameters its anchor was keyed by.
2446    let scale = ChartScale::of(surface, rings, tol);
2447    let scaled: Vec<Vec<Point2>> = rings
2448        .iter()
2449        .map(|ring| {
2450            ring.iter()
2451                .map(|p| {
2452                    let q = scale.to(p.x, p.y);
2453                    Point2::new(q.x, q.y)
2454                })
2455                .collect()
2456        })
2457        .collect();
2458    let mut exact: std::collections::HashMap<(u64, u64), (f64, f64)> =
2459        std::collections::HashMap::new();
2460
2461    // The boundary edges are constraints, so the triangulation respects the
2462    // trimming rather than spanning across a hole.
2463    for (ring, chart) in scaled.iter().zip(rings) {
2464        let mut ring_handles = Vec::with_capacity(ring.len());
2465        for (p, uv) in ring.iter().zip(chart) {
2466            exact.insert((p.x.to_bits(), p.y.to_bits()), (uv.x, uv.y));
2467            // A chart coordinate can come out subnormal-tiny (the sine of
2468            // a fold angle, the residue of an exact cancellation), and the
2469            // triangulation refuses what is, for every purpose, zero.
2470            let handle = cdt
2471                .insert(mitigate_underflow(SpadePoint::new(p.x, p.y)))
2472                .map_err(|e| ogeom_core::ogeom_err!(NotDone, "boundary insertion failed: {e}"))?;
2473            ring_handles.push(handle);
2474        }
2475        let mut refused = 0usize;
2476        let mut same = 0usize;
2477        for i in 0..ring_handles.len() {
2478            let (a, b) = (ring_handles[i], ring_handles[(i + 1) % ring_handles.len()]);
2479            if a == b {
2480                same += 1;
2481            } else if cdt.can_add_constraint(a, b) {
2482                cdt.add_constraint(a, b);
2483            } else {
2484                refused += 1;
2485                if *MESH_DEBUG_REFINE {
2486                    let n = ring.len();
2487                    let partners: Vec<usize> = (0..n)
2488                        .filter(|&j| j != i && j != (i + 1) % n && j != (i + n - 1) % n)
2489                        .filter(|&j| {
2490                            segments_cross(ring[i], ring[(i + 1) % n], ring[j], ring[(j + 1) % n])
2491                        })
2492                        .collect();
2493                    let len = ring[i].distance(ring[(i + 1) % n]);
2494                    eprintln!(
2495                        "REFUSED segment {i} of {n} (chart length {len:.3e}) crosses {partners:?}"
2496                    );
2497                    for j in [(i + n - 1) % n, i, (i + 1) % n, (i + 2) % n, (i + 3) % n] {
2498                        eprintln!("   ring[{j}] = ({:.12}, {:.12})", ring[j].x, ring[j].y);
2499                    }
2500                }
2501            }
2502        }
2503        if *MESH_DEBUG_REFINE && (refused > 0 || same > 0) {
2504            eprintln!(
2505                "CONSTRAINTS ring of {}: {refused} refused, {same} zero-length",
2506                ring.len()
2507            );
2508        }
2509        refused_total += refused;
2510    }
2511
2512    // With the boundary in and nothing else, a boundary that encloses a
2513    // region triangulates to exactly `b + 2w - 4` triangles inside it, `b`
2514    // its distinct vertices and `w` its rings: the count any triangulation
2515    // of a polygon with holes has. One that crosses itself gives another
2516    // number: a segment refused as a constraint, a lobe wound the wrong
2517    // way. Asked here, before interior points bury the difference.
2518    let bands = RingBands::over(&scaled);
2519    // A segment the triangulation refused as a constraint crossed one
2520    // already there; that alone is the answer.
2521    let crossed = refused_total > 0
2522        || inside_by_parity(&cdt)
2523            .is_none_or(|inside| inside + 4 != cdt.num_vertices() + 2 * rings.len());
2524    if *MESH_DEBUG_REFINE && crossed {
2525        let points: usize = rings.iter().map(Vec::len).sum();
2526        eprintln!(
2527            "PARITY inside {:?} vertices {} points {points} rings {} inner faces {}",
2528            inside_by_parity(&cdt),
2529            cdt.num_vertices(),
2530            rings.len(),
2531            cdt.num_inner_faces()
2532        );
2533    }
2534
2535    // Interior points where the surface bends away from the flat triangle. A
2536    // planar face needs none, which is why this is driven by measured
2537    // deflection rather than by a fixed grid.
2538    let boundary_ms = sub.elapsed().as_secs_f64() * 1e3;
2539    let sub = std::time::Instant::now();
2540    add_interior_points(&mut cdt, rings, surface, deflection, scale, tol)?;
2541    let interior_ms = sub.elapsed().as_secs_f64() * 1e3;
2542    let interior_points = cdt.num_vertices();
2543    let sub = std::time::Instant::now();
2544    let mut rounds_run = 0usize;
2545
2546    // The scale a degenerate chart triangle is measured against: the
2547    // region's own span, the longer way. Its *position* is not its size:
2548    // a face on a cylinder whose axis point sits half a metre away has
2549    // `v` near −500 000 and a span of twenty, and a scale taken from where
2550    // the ring sits rather than how far it reaches would call every honest
2551    // cell a hair.
2552    let (lo, hi) = chart_extent(&scaled);
2553    let extent = (hi.x - lo.x).max(hi.y - lo.y);
2554    let degenerate_area = extent.max(1.0).powi(2) * 1e-12;
2555
2556    // The grid rows guarantee the deflection along their own lines, but a
2557    // hole in the face punches a gap through a row, and where the surface is
2558    // flat in one direction (a cylinder along its axis) there may be no
2559    // other row for the mesher to reach. The band around the hole then fans
2560    // from the rim to the far side of the gap, in triangles that sag through
2561    // the solid by far more than the deflection while every one of their
2562    // vertices sits exactly on the surface. The boolean caught this as a
2563    // fused solid whose faces all had the right area and the wrong volume.
2564    //
2565    // The repair measures the truth: any kept triangle whose midpoints sag
2566    // beyond the chord gets its centre inserted, and the loop runs until the
2567    // mesh is honest or the cap says the surface is being unreasonable.
2568    // The rings do not change while the mesh is refined, so the containment
2569    // test they answer is indexed once and reused by every round below and by
2570    // the output pass.
2571    if !matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
2572        for _ in 0..REFINEMENT_ROUNDS {
2573            rounds_run += 1;
2574            let before = cdt.num_vertices();
2575            let mut worst: Vec<SpadePoint<f64>> = Vec::new();
2576            for triangle in cdt.inner_faces() {
2577                let vertices = triangle.vertices();
2578                let centre = triangle.center();
2579                let at = Point2::new(centre.x, centre.y);
2580                if !bands.holds(at) {
2581                    continue;
2582                }
2583                let corners: [(f64, f64); 3] = [
2584                    (vertices[0].position().x, vertices[0].position().y),
2585                    (vertices[1].position().x, vertices[1].position().y),
2586                    (vertices[2].position().x, vertices[2].position().y),
2587                ];
2588                // A chart-degenerate hair is dropped from the mesh, not
2589                // refined: its 3D chord can sag enormously, and feeding its
2590                // centre back in only breeds more hairs along the same line.
2591                let area = ((corners[1].0 - corners[0].0) * (corners[2].1 - corners[0].1)
2592                    - (corners[1].1 - corners[0].1) * (corners[2].0 - corners[0].0))
2593                    .abs()
2594                    / 2.0;
2595                if area < degenerate_area {
2596                    continue;
2597                }
2598                // The grid already bounds sag along rows and columns, and a
2599                // grid triangle's diagonal spanning one cell each way may
2600                // legitimately sag up to the sum (twice the chord), which
2601                // was the guarantee before this loop existed. The threshold
2602                // sits clear above that band so the repair fires only on the
2603                // fan triangles it exists for, which sag through a hole's
2604                // gap by tens of chords, and an honest grid (including a
2605                // perfectly symmetric one, whose mesh must stay symmetric)
2606                // is left untouched.
2607                let sagged = (0..3).any(|i| {
2608                    let (a, b) = (corners[i], corners[(i + 1) % 3]);
2609                    sag_between(surface, scale.from(a.0, a.1), scale.from(b.0, b.1), tol)
2610                        > deflection.chord * 3.0
2611                });
2612                if sagged {
2613                    worst.push(SpadePoint::new(centre.x, centre.y));
2614                }
2615            }
2616            if worst.is_empty() {
2617                break;
2618            }
2619            let mut inserted = 0usize;
2620            for point in worst {
2621                // A centre that lands on a vertex already there is not a new
2622                // point. A sliver whose apex sits on its own base (three
2623                // grid points on a diagonal, the middle one a rounding off
2624                // the line) has its centre at that apex to the last bits,
2625                // and inserting it breeds a hair a few ulps wide, whose
2626                // centre is the same point again: round after round, a
2627                // stack of hairs the degenerate filter then drops, and a
2628                // hole in the face where they were.
2629                if lands_on_the_mesh(&cdt, point, extent * 1e-9, (1.0, 1.0)) {
2630                    continue;
2631                }
2632                cdt.insert(mitigate_underflow(point)).map_err(|e| {
2633                    ogeom_core::ogeom_err!(NotDone, "refinement insertion failed: {e}")
2634                })?;
2635                inserted += 1;
2636            }
2637            if inserted == 0 {
2638                // Everything that sagged was a hair on a vertex; another
2639                // round would find the same hairs.
2640                break;
2641            }
2642            if *MESH_DEBUG_REFINE {
2643                eprintln!(
2644                    "ROUND {rounds_run}: +{} vertices",
2645                    cdt.num_vertices() - before
2646                );
2647            }
2648        }
2649    }
2650
2651    let refine_ms = sub.elapsed().as_secs_f64() * 1e3;
2652    if *MESH_DEBUG_REFINE && boundary_ms + interior_ms + refine_ms > 50.0 {
2653        eprintln!(
2654            "SUB boundary {boundary_ms:.0}ms interior {interior_ms:.0}ms ({interior_points} verts) refine {refine_ms:.0}ms ({rounds_run} rounds, {} verts)",
2655            cdt.num_vertices()
2656        );
2657    }
2658    let mut parameters = Vec::new();
2659    let mut index_of = std::collections::HashMap::new();
2660    for (i, vertex) in cdt.vertices().enumerate() {
2661        let p = vertex.position();
2662        index_of.insert(vertex.fix(), i);
2663        parameters.push(
2664            exact
2665                .get(&(p.x.to_bits(), p.y.to_bits()))
2666                .copied()
2667                .unwrap_or_else(|| scale.from(p.x, p.y)),
2668        );
2669    }
2670
2671    let mut triangles = Vec::new();
2672    let (mut dbg_total, mut dbg_outside, mut dbg_degenerate) = (0usize, 0usize, 0usize);
2673    for triangle in cdt.inner_faces() {
2674        dbg_total += 1;
2675        let vertices = triangle.vertices();
2676        let centre = triangle.center();
2677        // A constrained Delaunay covers the convex hull of its input, so
2678        // triangles outside the trimmed region (across a concavity, or inside
2679        // a hole) have to be discarded. Winding tells them apart.
2680        if !bands.holds(Point2::new(centre.x, centre.y)) {
2681            dbg_outside += 1;
2682            continue;
2683        }
2684        // A boundary run whose points differ by last-bit noise (a chart row
2685        // whose corner came off a different pcurve than its interior) lets
2686        // the triangulation weave a hair of a triangle along it: chart area
2687        // measured in ulps, a centroid *on* the boundary that even-odd
2688        // counting places wherever rounding falls, and a lifted sliver that
2689        // spans the run in one spurious stroke. It bounds nothing; drop it.
2690        let area = {
2691            let [a, b, c] = [
2692                vertices[0].position(),
2693                vertices[1].position(),
2694                vertices[2].position(),
2695            ];
2696            ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() / 2.0
2697        };
2698        if area < degenerate_area {
2699            dbg_degenerate += 1;
2700            continue;
2701        }
2702        #[allow(clippy::cast_possible_truncation)]
2703        let indices = [
2704            index_of[&vertices[0].fix()] as u32,
2705            index_of[&vertices[1].fix()] as u32,
2706            index_of[&vertices[2].fix()] as u32,
2707        ];
2708        triangles.push(indices);
2709    }
2710
2711    if *MESH_DEBUG_REFINE {
2712        eprintln!(
2713            "FILTER {dbg_total} triangles: {dbg_outside} outside, {dbg_degenerate} degenerate (area < {degenerate_area:.3e}), {} kept",
2714            triangles.len()
2715        );
2716    }
2717    // No triangles is not refused here: a boundary drawn coarsely enough
2718    // to cross itself can enclose nothing at all (an annulus narrower
2719    // than the sag of its rims' polygons, two arcs a hair apart), and the
2720    // caller's answer to a crossing is to draw the edges finer and ask
2721    // again. Empty counts as short; only a face still empty after that is
2722    // refused, by [`triangulate_with`].
2723    Ok(PlanarMesh {
2724        parameters,
2725        triangles,
2726        crossed,
2727    })
2728}
2729
2730/// Add interior points wherever the surface deviates from flat by more than the
2731/// deflection allows.
2732///
2733/// The two parameter directions are refined *independently*, and that is not an
2734/// optimization; it is the difference between converging and not. A uniform
2735/// grid on a sphere puts as many meridians through the pole as through the
2736/// equator, so the triangles there become arbitrarily thin slivers in space.
2737/// The summed area of such a mesh does not approach the sphere's; it grows
2738/// without bound as the grid tightens. (Schwarz's lantern is the standard
2739/// example: an inscribed polyhedron whose area diverges under refinement.)
2740///
2741/// Refining each direction by its own measured sag fixes it at the source. Near
2742/// a pole the circle of latitude has almost no radius, so a chord right across
2743/// it sags by almost nothing and the direction stops subdividing after one or
2744/// two steps, while the meridian direction, whose curvature does not change,
2745/// keeps refining. The mesh degenerates into a fan, which is the right shape.
2746fn add_interior_points(
2747    cdt: &mut ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
2748    rings: &[Vec<Point2>],
2749    surface: &SurfaceGeometry,
2750    deflection: Deflection,
2751    scale: ChartScale,
2752    tol: Tolerances,
2753) -> OgeomResult<()> {
2754    // A plane is flat everywhere; sampling it would add points that buy nothing.
2755    if matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
2756        return Ok(());
2757    }
2758
2759    let bound = rings
2760        .iter()
2761        .flatten()
2762        .fold(ogeom_math::Aabb::EMPTY, |acc, p| {
2763            acc.with_point(Point::new(p.x, p.y, 0.0))
2764        });
2765    let (Some(low), Some(high)) = (bound.low(), bound.high()) else {
2766        return Ok(());
2767    };
2768    // Within this of a boundary vertex or segment is on it: a hair's width
2769    // at the scaled chart's scale, the same reach the repair pass keeps.
2770    let reach = ((high.x - low.x) * scale.su)
2771        .max((high.y - low.y) * scale.sv)
2772        .max(1.0)
2773        * 1e-9;
2774
2775    // The v resolution has to hold everywhere the region reaches, so its sag is
2776    // the worst over a spread of u probes rather than the sag along one line.
2777    // A surface of revolution is the same at every u and a lofted one is not.
2778    #[allow(clippy::cast_precision_loss)]
2779    let probes: Vec<f64> = (0..=U_PROBES)
2780        .map(|i| low.x + (high.x - low.x) * i as f64 / U_PROBES as f64)
2781        .collect();
2782    let rows = refine_direction(low.y, high.y, deflection.chord, |a, b| {
2783        probes
2784            .iter()
2785            .map(|u| cell_error(surface, (*u, a), (*u, b), deflection, tol))
2786            .fold(0.0_f64, f64::max)
2787    });
2788
2789    // Sag alone leaves a cylinder one row: it is straight along its axis, so
2790    // nothing along `v` ever sags. But the triangulation is Delaunay in the
2791    // chart, and a bore four hundred millimetres long with one row in the
2792    // middle hands it two-hundred-millimetre spans from each rim to that
2793    // row. Delaunay bridges those however it likes, and the repair below
2794    // fires only at three chords; a triangle a quarter turn wide on a two
2795    // millimetre bore sags less than that, so it stayed, and the bore drew
2796    // as a square between its holes. Cells are held to a bounded aspect
2797    // instead: rows close enough, measured in space through the surface,
2798    // that no triangle between two rows can reach across more than a few
2799    // columns. Rows are added, never removed, and spread evenly, so a grid
2800    // that was symmetric stays symmetric. The same the other way round.
2801    // How far apart two chart points are in space, where the surface says.
2802    let span = |p: (f64, f64), q: (f64, f64)| -> f64 {
2803        use ogeom_geom::Surface as _;
2804        match (
2805            surface.point_at(p.0, p.1, tol),
2806            surface.point_at(q.0, q.1, tol),
2807        ) {
2808            (Ok(a), Ok(b)) => a.distance(b),
2809            _ => 0.0,
2810        }
2811    };
2812    let rows = spread_to_aspect(
2813        rows,
2814        low.x,
2815        high.x,
2816        |v| {
2817            let columns = refine_direction(low.x, high.x, deflection.chord, |a, b| {
2818                cell_error(surface, (a, v), (b, v), deflection, tol)
2819            });
2820            columns.len()
2821        },
2822        |a, b| {
2823            probes
2824                .iter()
2825                .map(|&u| span((u, a), (u, b)))
2826                .fold(0.0_f64, f64::max)
2827        },
2828        |a, b, v| span((a, v), (b, v)),
2829    );
2830
2831    if *MESH_DEBUG_REFINE {
2832        let v = f64::midpoint(low.y, high.y);
2833        let columns = refine_direction(low.x, high.x, deflection.chord, |a, b| {
2834            cell_error(surface, (a, v), (b, v), deflection, tol)
2835        });
2836        eprintln!(
2837            "GRID u [{:.3},{:.3}] v [{:.3},{:.3}]: {} rows after aspect, {} columns at the middle row, chord {}",
2838            low.x,
2839            high.x,
2840            low.y,
2841            high.y,
2842            rows.len(),
2843            columns.len(),
2844            deflection.chord
2845        );
2846    }
2847    for (row, &v) in rows
2848        .iter()
2849        .enumerate()
2850        .take(rows.len().saturating_sub(1))
2851        .skip(1)
2852    {
2853        // The row gap either side of this row, the finer of the two: the
2854        // chart scale a keep-out band is measured against along `v`.
2855        let dv = (v - rows[row - 1]).abs().min((rows[row + 1] - v).abs());
2856        // Each row gets its own u resolution, measured at that row.
2857        let columns = refine_direction(low.x, high.x, deflection.chord, |a, b| {
2858            cell_error(surface, (a, v), (b, v), deflection, tol)
2859        });
2860        // The same the other way round: a surface straight along `u` gets
2861        // two columns from sag, and a row a hundred millimetres wide would
2862        // bridge across the rows as badly as the bore bridged its columns.
2863        let columns = spread_to_aspect(
2864            columns,
2865            low.y,
2866            high.y,
2867            |_| rows.len(),
2868            |a, b| span((a, v), (b, v)),
2869            |a, b, u| span((u, a), (u, b)),
2870        );
2871        for (column, &u) in columns
2872            .iter()
2873            .enumerate()
2874            .take(columns.len().saturating_sub(1))
2875            .skip(1)
2876        {
2877            // Interior points only: the boundary is already constrained, and a
2878            // point landing just off a constraint would split it.
2879            if !inside_region(rings, Point2::new(u, v)) {
2880                continue;
2881            }
2882            // Inside, and not *on* the boundary: a grid point can fall
2883            // exactly on a ring segment that runs diagonally across the
2884            // chart (the midpoint of two grid corners the ring happens to
2885            // join), and even-odd counting calls it inside. Inserted, it
2886            // splits that constraint on this face alone, and the face
2887            // across the edge is drawn to the unsplit segment.
2888            let point = scale.to(u, v);
2889            if lands_on_the_mesh(cdt, point, reach, (1.0, 1.0)) {
2890                continue;
2891            }
2892            // Nor *near* it, measured in cells. A grid point a sliver's
2893            // width from a boundary chord makes a triangle with that
2894            // chord's two ends that is thin in the chart and, lifted, is
2895            // not thin at all: the chord cuts across the curvature by its
2896            // sag and the point sits on the surface, so the triangle
2897            // stands off the surface as a fin whose normal is tangent to
2898            // it and whose sign is whichever way the sliver leaned. A face
2899            // shades with a crease along every such chord. The point is
2900            // left out and the boundary's own row of triangles reaches
2901            // to the next grid line instead.
2902            let du = (u - columns[column - 1])
2903                .abs()
2904                .min((columns[column + 1] - u).abs());
2905            if du > 0.0
2906                && dv > 0.0
2907                && lands_on_the_mesh(
2908                    cdt,
2909                    point,
2910                    KEEP_OUT,
2911                    (1.0 / (du * scale.su), 1.0 / (dv * scale.sv)),
2912                )
2913            {
2914                continue;
2915            }
2916            cdt.insert(mitigate_underflow(point))
2917                .map_err(|e| ogeom_core::ogeom_err!(NotDone, "interior insertion failed: {e}"))?;
2918        }
2919    }
2920    Ok(())
2921}
2922
2923/// Whether segments `a..b` and `c..d` cross properly: at a point interior
2924/// to both, neither touching the other's end.
2925fn segments_cross(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
2926    let orient =
2927        |p: Point2, q: Point2, r: Point2| (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
2928    let (o1, o2) = (orient(a, b, c), orient(a, b, d));
2929    let (o3, o4) = (orient(c, d, a), orient(c, d, b));
2930    o1 != 0.0
2931        && o2 != 0.0
2932        && o3 != 0.0
2933        && o4 != 0.0
2934        && (o1 > 0.0) != (o2 > 0.0)
2935        && (o3 > 0.0) != (o4 > 0.0)
2936}
2937
2938/// How many of the triangulation's faces lie inside its constraints, told
2939/// by parity rather than by geometry.
2940///
2941/// Walking from a face on the convex hull, which is outside, every
2942/// constraint edge crossed flips inside for outside. Asked of the
2943/// triangulation of a boundary and nothing else, this is exact where the
2944/// even-odd test of a triangle's centre is not: a sliver face triangulates
2945/// to hairs whose centres sit on the boundary to the last bit, and which
2946/// side rounding puts them is a coin toss. `None` when the walk reaches a
2947/// face both ways with different answers: the constraints do not enclose
2948/// consistently, which is a crossing by another name.
2949fn inside_by_parity(cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>) -> Option<usize> {
2950    use std::collections::HashMap;
2951    let mut parity: HashMap<spade::handles::FixedFaceHandle<spade::handles::InnerTag>, bool> =
2952        HashMap::with_capacity(cdt.num_inner_faces());
2953    let mut queue = Vec::new();
2954    for hull in cdt.convex_hull() {
2955        // The hull edge's far side is the outer face; its near side is a
2956        // face of the triangulation, outside unless the hull edge itself
2957        // is a boundary.
2958        let Some(face) = hull.rev().face().as_inner() else {
2959            continue;
2960        };
2961        let inside = hull.is_constraint_edge();
2962        match parity.get(&face.fix()) {
2963            Some(&known) if known != inside => return None,
2964            Some(_) => {}
2965            None => {
2966                parity.insert(face.fix(), inside);
2967                queue.push((face.fix(), inside));
2968            }
2969        }
2970    }
2971    while let Some((face, inside)) = queue.pop() {
2972        for edge in cdt.face(face).adjacent_edges() {
2973            let Some(next) = edge.rev().face().as_inner() else {
2974                continue;
2975            };
2976            let next_inside = inside != edge.is_constraint_edge();
2977            match parity.get(&next.fix()) {
2978                Some(&known) if known != next_inside => return None,
2979                Some(_) => {}
2980                None => {
2981                    parity.insert(next.fix(), next_inside);
2982                    queue.push((next.fix(), next_inside));
2983                }
2984            }
2985        }
2986    }
2987    Some(parity.values().filter(|&&inside| inside).count())
2988}
2989
2990/// Whether `point` sits within `reach` of a vertex the triangulation has,
2991/// or of one of its constraint edges.
2992///
2993/// Asked of whatever the point lands on (a vertex, an edge's two ends, a
2994/// face's three corners and whichever of its sides are constraints),
2995/// which is where anything that close must be. A point on a vertex is not
2996/// a new point; a point on a constraint would split it, and a boundary
2997/// split on one face only is a crack against the face across it.
2998fn lands_on_the_mesh(
2999    cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
3000    point: SpadePoint<f64>,
3001    reach: f64,
3002    scale: (f64, f64),
3003) -> bool {
3004    use spade::PositionInTriangulation as At;
3005    // Distances in a chart scaled per axis: `scale` is one over the local
3006    // grid step each way, so `reach` reads in cells, whatever the chart's
3007    // own units: one face's `u` runs over a fiftieth of its `v`.
3008    let scaled = |p: SpadePoint<f64>| ((p.x - point.x) * scale.0, (p.y - point.y) * scale.1);
3009    let near = |v: SpadePoint<f64>| {
3010        let (x, y) = scaled(v);
3011        x.hypot(y) <= reach
3012    };
3013    let along = |a: SpadePoint<f64>, b: SpadePoint<f64>| {
3014        // Distance to the segment `a..b`, the point at the origin.
3015        let (ax, ay) = scaled(a);
3016        let (bx, by) = scaled(b);
3017        let (dx, dy) = (bx - ax, by - ay);
3018        let len2 = dx * dx + dy * dy;
3019        let t = if len2 > 0.0 {
3020            ((-ax * dx - ay * dy) / len2).clamp(0.0, 1.0)
3021        } else {
3022            0.0
3023        };
3024        (ax + t * dx).hypot(ay + t * dy) <= reach
3025    };
3026    match cdt.locate(point) {
3027        At::OnVertex(_) => true,
3028        At::OnEdge(edge) => {
3029            let edge = cdt.directed_edge(edge);
3030            edge.is_constraint_edge() || edge.vertices().iter().any(|v| near(v.position()))
3031        }
3032        At::OnFace(face) => {
3033            let face = cdt.face(face);
3034            face.vertices().iter().any(|v| near(v.position()))
3035                || face.adjacent_edges().iter().any(|e| {
3036                    e.is_constraint_edge() && along(e.from().position(), e.to().position())
3037                })
3038        }
3039        At::OutsideOfConvexHull(_) | At::NoTriangulation => false,
3040    }
3041}
3042
3043/// How close to the boundary, in grid cells, an interior point may sit.
3044///
3045/// Closer than this the triangle between the point and a boundary chord
3046/// is a sliver in the chart and a fin in space; at this and beyond the
3047/// boundary's own row of triangles is at least this tall against the
3048/// chord, and lifts as a facet on the surface rather than off it.
3049const KEEP_OUT: f64 = 0.35;
3050
3051/// How many column widths a grid cell may be tall before rows are added.
3052///
3053/// Delaunay in the chart connects nearest neighbours in the chart; held to
3054/// this aspect, a cell's nearest neighbours across a row gap are the same
3055/// columns, not columns several away, and the triangles between rows stay
3056/// as narrow as the columns are.
3057const CELL_ASPECT: f64 = 6.0;
3058
3059/// Grid lines in one direction spread so that no cell is longer, in
3060/// space, than [`CELL_ASPECT`] times its width the other way: extra
3061/// lines added evenly between the ones sag chose.
3062///
3063/// Written for rows against columns and used both ways round. `lines`
3064/// are the parameters sag chose in this direction; `lo..hi` is the chart
3065/// range the other way; `crossings_at(t)` counts the lines the other way
3066/// at parameter `t` of this one; `length(a, b)` is the extent in space
3067/// between two lines of this direction; `width(a, b, t)` is the extent in
3068/// space between two parameters of the other direction, along this one's
3069/// line `t`.
3070fn spread_to_aspect(
3071    lines: Vec<f64>,
3072    lo: f64,
3073    hi: f64,
3074    crossings_at: impl Fn(f64) -> usize,
3075    length: impl Fn(f64, f64) -> f64,
3076    width: impl Fn(f64, f64, f64) -> f64,
3077) -> Vec<f64> {
3078    if lines.len() < 2 {
3079        return lines;
3080    }
3081    let mid = f64::midpoint(lines[0], lines[lines.len() - 1]);
3082    let crossings = crossings_at(mid);
3083    if crossings < 4 {
3084        // Flat the other way as well: a plane in all but name, and nothing
3085        // to hold an aspect against.
3086        return lines;
3087    }
3088    // One cell's width, not the whole range's: across a closed direction
3089    // the whole range comes back to its own start and measures nothing.
3090    #[allow(clippy::cast_precision_loss)]
3091    let step = (hi - lo) / (crossings - 1) as f64;
3092    let cell = width(lo, lo + step, mid);
3093    if cell.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
3094        return lines;
3095    }
3096    let mut out = Vec::with_capacity(lines.len());
3097    for pair in lines.windows(2) {
3098        let (a, b) = (pair[0], pair[1]);
3099        out.push(a);
3100        let tall = length(a, b);
3101        let pieces = (tall / (CELL_ASPECT * cell)).ceil();
3102        if pieces.is_finite() && pieces > 1.0 {
3103            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3104            let n = (pieces as usize).min(MAX_DIRECTION_STEPS);
3105            #[allow(clippy::cast_precision_loss)]
3106            for i in 1..n {
3107                out.push(a + (b - a) * i as f64 / n as f64);
3108            }
3109        }
3110    }
3111    out.push(lines[lines.len() - 1]);
3112    out
3113}
3114
3115/// How many places across the domain the v resolution is measured at.
3116const U_PROBES: usize = 8;
3117
3118/// How many rounds of sag-driven refinement a region may take.
3119///
3120/// Each round halves the worst sag roughly; a surface not honest after this
3121/// many is degenerate, and the cap makes that a coarse mesh rather than an
3122/// exhausted allocator.
3123const REFINEMENT_ROUNDS: usize = 12;
3124
3125/// The most subdivisions one parameter direction may take.
3126///
3127/// A surface that has not converged by 512 has a singularity, not a resolution
3128/// problem, and the ceiling is what makes that a coarse mesh rather than an
3129/// exhausted allocator.
3130const MAX_DIRECTION_STEPS: usize = 512;
3131
3132/// Subdivide `[lo, hi]` until no sub-interval sags further than `chord`.
3133///
3134/// The same adaptive bisection [`discretize`] uses on a curve, applied to a
3135/// line through parameter space. Returns the parameters in increasing order,
3136/// endpoints included.
3137fn refine_direction<F: Fn(f64, f64) -> f64>(lo: f64, hi: f64, chord: f64, sag: F) -> Vec<f64> {
3138    let mut values = vec![lo, f64::midpoint(lo, hi), hi];
3139    // A cursor rather than a rescan. Splitting an interval cannot change
3140    // whether an *earlier* one sags (the earlier one's endpoints do not move),
3141    // so restarting the search at zero re-measures intervals already known
3142    // to be good, and re-measuring is what costs: each measurement here is
3143    // several `sag_between` calls and each of those is three surface
3144    // evaluations. Reaching n points that way costs on the order of n²
3145    // measurements; walking forward costs n, and splits in the same
3146    // left-to-right order, so the values come out identical, including where
3147    // the step cap truncates them.
3148    let mut i = 0;
3149    while i + 1 < values.len() && values.len() < MAX_DIRECTION_STEPS {
3150        if sag(values[i], values[i + 1]) <= chord {
3151            i += 1;
3152            continue;
3153        }
3154        let mid = f64::midpoint(values[i], values[i + 1]);
3155        // A split that does not divide the interval means the parameters have
3156        // reached the resolution of f64, and refining further would loop.
3157        if mid <= values[i] || mid >= values[i + 1] {
3158            break;
3159        }
3160        values.insert(i + 1, mid);
3161    }
3162    values
3163}
3164
3165/// How far the surface departs from the chord joining two parameter points.
3166///
3167/// Measured in space, which is the only place the number means anything: the
3168/// same step in `u` covers a metre at a sphere's equator and a millimetre near
3169/// its pole.
3170/// How far a grid cell's edge is from honest, as a sag: the chord sag
3171/// itself, or the normal's turn across it scaled so that a turn of the
3172/// angular deflection weighs the same as a sag of the chord, whichever
3173/// is worse.
3174///
3175/// The chord alone is what the boundary's edges are *not* drawn to: a
3176/// curve is discretized to both deflections, so a bore's rims come out
3177/// round at the angular limit while columns held to the chord alone come
3178/// out a polygon of far fewer sides, and the bore changes shape a chord's
3179/// length in from each rim. The interior is held to the same two limits
3180/// the boundary is.
3181fn cell_error(
3182    surface: &SurfaceGeometry,
3183    from: (f64, f64),
3184    to: (f64, f64),
3185    deflection: Deflection,
3186    tol: Tolerances,
3187) -> f64 {
3188    let sag = sag_between(surface, from, to, tol);
3189    let turn = match (
3190        surface.normal_at(from.0, from.1, tol),
3191        surface.normal_at(to.0, to.1, tol),
3192    ) {
3193        (Ok(a), Ok(b)) => a.angle(b),
3194        // A pole or an apex has no normal to compare; the sag still governs.
3195        _ => 0.0,
3196    };
3197    sag.max(turn / deflection.angular * deflection.chord)
3198}
3199
3200fn sag_between(
3201    surface: &SurfaceGeometry,
3202    from: (f64, f64),
3203    to: (f64, f64),
3204    tol: Tolerances,
3205) -> f64 {
3206    let mid = (f64::midpoint(from.0, to.0), f64::midpoint(from.1, to.1));
3207    let (Ok(a), Ok(b), Ok(m)) = (
3208        surface.point_at(from.0, from.1, tol),
3209        surface.point_at(to.0, to.1, tol),
3210        surface.point_at(mid.0, mid.1, tol),
3211    ) else {
3212        // Off the surface's domain; nothing to refine towards.
3213        return 0.0;
3214    };
3215    ogeom_math::Axis::through(a, b, tol).map_or_else(|_| a.distance(m), |axis| axis.distance_to(m))
3216}
3217
3218/// Whether a point is inside the region the rings bound.
3219fn inside_region(rings: &[Vec<Point2>], p: Point2) -> bool {
3220    inside_boundary_with::<Exact>(rings, p)
3221}
3222
3223/// Ray-crossing count for one ring, decided by orientation rather than by
3224/// arithmetic.
3225///
3226/// A horizontal ray in `+u`. The half-open `y` comparison counts a vertex lying
3227/// exactly on the ray once rather than twice or not at all; which side of the
3228/// edge the point falls on is then an `orient2d` sign.
3229///
3230/// Deliberately *not* "solve for where the edge crosses the ray, then compare".
3231/// That form divides by the edge's `y` extent, which is near zero for a nearly
3232/// horizontal edge, and subtracts two nearly equal numbers to compare, so for a
3233/// point close to the boundary it can answer either way. Here there is no
3234/// division and the comparison is a determinant's sign, which an exact predicate
3235/// gets right at any separation.
3236fn crosses_odd_times<P: Predicates>(ring: &[Point2], p: Point2) -> bool {
3237    let mut inside = false;
3238    let n = ring.len();
3239    let at = |q: Point2| [q.x, q.y];
3240    for i in 0..n {
3241        let (a, b) = (ring[i], ring[(i + 1) % n]);
3242        if (a.y > p.y) == (b.y > p.y) {
3243            continue;
3244        }
3245        // The edge crosses the ray's line. Whether it crosses the ray *itself*
3246        // (to the right of `p`) is which side of the directed edge `p` is on,
3247        // read the right way round for the edge's direction in `y`.
3248        let side = P::orient2d(at(a), at(b), [p.x, p.y]);
3249        let rightwards = if b.y > a.y {
3250            side == ogeom_core::Sign::Positive
3251        } else {
3252            side == ogeom_core::Sign::Negative
3253        };
3254        if rightwards {
3255            inside = !inside;
3256        }
3257    }
3258    inside
3259}
3260
3261/// Whether the mesh debug dump is on, read once.
3262///
3263/// `env::var` takes a process-wide lock and allocates its answer, and this was
3264/// asked once per face; on an imported assembly, once per face of every part.
3265/// Whether to report, per shape, how many faces were drawn again finer.
3266static MESH_DEBUG_REFINE: std::sync::LazyLock<bool> =
3267    std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG_REFINE").is_ok());
3268
3269static MESH_DEBUG: std::sync::LazyLock<bool> =
3270    std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG").is_ok());
3271
3272/// The ring edges that can cross a horizontal ray, bucketed by height.
3273///
3274/// [`crosses_odd_times`] walks every edge of every ring for each point it is
3275/// asked about. That is fine for a handful of queries and ruinous for the
3276/// refinement loop, which asks once per triangle per round while the rings
3277/// themselves never change: a face with 544 boundary points and 41 000
3278/// triangles pays a quarter of a billion edge visits, nearly all on edges
3279/// nowhere near the point.
3280///
3281/// An edge can only straddle a ray at height `y` if `y` lies within the edge's
3282/// own `y` span, so bucketing edges by that span and querying one bucket tests
3283/// a conservative superset of the edges that could contribute. **The answer is
3284/// therefore identical**: the same straddle test and the same exact predicate
3285/// decide each candidate; the index only declines to visit edges that could
3286/// not have counted.
3287///
3288/// Parity is taken over all rings at once, which is what
3289/// [`inside_boundary_with`] computes as an exclusive-or of per-ring parities:
3290/// the two agree because the parity of the total crossing count is the sum of
3291/// the rings' parities.
3292struct RingBands {
3293    /// Every ring's edges, flattened.
3294    edges: Vec<(Point2, Point2)>,
3295    /// Edge indices per band, low `y` first.
3296    bands: Vec<Vec<u32>>,
3297    low: f64,
3298    high: f64,
3299    /// Band height. Zero when every point shares one `y`, which leaves a
3300    /// single band holding everything.
3301    step: f64,
3302}
3303
3304impl RingBands {
3305    /// Index the rings. Cheap enough to build per face and paid back by the
3306    /// first few hundred queries.
3307    fn over(rings: &[Vec<Point2>]) -> Self {
3308        let mut edges = Vec::new();
3309        for ring in rings {
3310            for i in 0..ring.len() {
3311                edges.push((ring[i], ring[(i + 1) % ring.len()]));
3312            }
3313        }
3314        let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
3315        for (a, b) in &edges {
3316            low = low.min(a.y).min(b.y);
3317            high = high.max(a.y).max(b.y);
3318        }
3319        if edges.is_empty() || !low.is_finite() || !high.is_finite() {
3320            return Self {
3321                edges,
3322                bands: Vec::new(),
3323                low: 0.0,
3324                high: 0.0,
3325                step: 0.0,
3326            };
3327        }
3328        // About four edges to a band: enough to keep the per-query walk short
3329        // without spreading a long edge across a table of mostly empty bands.
3330        let count = edges.len().div_ceil(4).clamp(1, 4096);
3331        #[allow(
3332            clippy::cast_precision_loss,
3333            reason = "a band count, far below the integers f64 represents exactly"
3334        )]
3335        let step = (high - low) / count as f64;
3336        let mut bands: Vec<Vec<u32>> = vec![Vec::new(); count];
3337        for (i, (a, b)) in edges.iter().enumerate() {
3338            let (lo, hi) = (a.y.min(b.y), a.y.max(b.y));
3339            let first = Self::band_of(lo, low, step, count);
3340            let last = Self::band_of(hi, low, step, count);
3341            for band in &mut bands[first..=last] {
3342                #[allow(
3343                    clippy::cast_possible_truncation,
3344                    reason = "an edge index, bounded by the ring lengths"
3345                )]
3346                band.push(i as u32);
3347            }
3348        }
3349        Self {
3350            edges,
3351            bands,
3352            low,
3353            high,
3354            step,
3355        }
3356    }
3357
3358    /// Which band a height falls in, clamped to the table.
3359    fn band_of(y: f64, low: f64, step: f64, count: usize) -> usize {
3360        if step <= 0.0 {
3361            return 0;
3362        }
3363        let raw = (y - low) / step;
3364        if raw <= 0.0 {
3365            return 0;
3366        }
3367        #[allow(
3368            clippy::cast_possible_truncation,
3369            clippy::cast_sign_loss,
3370            reason = "clamped to the band count on the next line"
3371        )]
3372        let index = raw as usize;
3373        index.min(count - 1)
3374    }
3375
3376    /// Whether the point lies inside the region the rings bound.
3377    fn holds(&self, p: Point2) -> bool {
3378        if self.bands.is_empty() || p.y < self.low || p.y > self.high {
3379            return false;
3380        }
3381        let band = Self::band_of(p.y, self.low, self.step, self.bands.len());
3382        let at = |q: Point2| [q.x, q.y];
3383        let mut inside = false;
3384        for &i in &self.bands[band] {
3385            let (a, b) = self.edges[i as usize];
3386            if (a.y > p.y) == (b.y > p.y) {
3387                continue;
3388            }
3389            let side = Exact::orient2d(at(a), at(b), [p.x, p.y]);
3390            let rightwards = if b.y > a.y {
3391                side == ogeom_core::Sign::Positive
3392            } else {
3393                side == ogeom_core::Sign::Negative
3394            };
3395            if rightwards {
3396                inside = !inside;
3397            }
3398        }
3399        inside
3400    }
3401}
3402
3403/// The unit normal of a triangle, or `None` if it is degenerate.
3404#[must_use]
3405pub fn triangle_normal(a: Point, b: Point, c: Point, tol: Tolerances) -> Option<Direction> {
3406    Direction::from_cross(b - a, c - a, tol).ok()
3407}
3408
3409/// Discretize an edge into a polyline in space, for display or coarse queries.
3410///
3411/// # Errors
3412///
3413/// As [`discretize`].
3414pub fn polyline_of_edge(
3415    model: &Model,
3416    edge: &Shape,
3417    deflection: Deflection,
3418    tol: Tolerances,
3419) -> OgeomResult<Vec<Point>> {
3420    if model.kind_of(edge)? != ShapeType::Edge {
3421        ogeom_bail!(Construction, "expected an edge");
3422    }
3423    let Some(node) = model.node(edge) else {
3424        ogeom_bail!(Dangling, "edge is not in this model");
3425    };
3426    let NodeData::Edge(data) = node.data() else {
3427        ogeom_bail!(Construction, "edge node holds no edge data");
3428    };
3429    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
3430        return Ok(Vec::new());
3431    };
3432    let Some(geometry) = model.geometry().curve(*curve) else {
3433        ogeom_bail!(Dangling, "curve is not in this model");
3434    };
3435    let placement = edge.transform(model.datums())?;
3436    let line = discretize(geometry, *range, deflection, tol)?;
3437    let mut points: Vec<Point> = line.points.iter().map(|p| placement.apply(*p)).collect();
3438    if edge.orientation() == Orientation::Reversed {
3439        points.reverse();
3440    }
3441    Ok(points)
3442}
3443
3444#[cfg(test)]
3445#[allow(clippy::unwrap_used)]
3446mod tests {
3447    use super::*;
3448
3449    #[test]
3450    fn a_wild_chart_refuses_instead_of_crashing() {
3451        let plane: SurfaceGeometry = ogeom_geom::PlaneSurface::over(
3452            ogeom_math::Plane::through(
3453                ogeom_math::Point::new(0.0, 0.0, 0.0),
3454                ogeom_math::Direction::Z,
3455            ),
3456            (-10.0, 10.0),
3457            (-10.0, 10.0),
3458        )
3459        .unwrap()
3460        .into();
3461        let tol = Tolerances::millimetres();
3462        // Coordinates wilder than any face: refused by the screen.
3463        let wild = vec![vec![
3464            Point2::new(0.0, 0.0),
3465            Point2::new(1e15, 0.0),
3466            Point2::new(0.0, 1e15),
3467        ]];
3468        let Err(err) = triangulate_region(&wild, &plane, Deflection::default(), tol) else {
3469            panic!("a wild chart must refuse");
3470        };
3471        assert!(err.to_string().contains("describes no face"), "{err}");
3472        // A non-finite coordinate: refused by name, not fed to the library.
3473        let nan = vec![vec![
3474            Point2::new(0.0, 0.0),
3475            Point2::new(f64::NAN, 1.0),
3476            Point2::new(1.0, 1.0),
3477        ]];
3478        let Err(err) = triangulate_region(&nan, &plane, Deflection::default(), tol) else {
3479            panic!("a NaN chart must refuse");
3480        };
3481        assert!(err.to_string().contains("non-finite"), "{err}");
3482    }
3483
3484    use approx::assert_relative_eq;
3485    use ogeom_algo::make_box;
3486    use ogeom_math::Frame;
3487    use ogeom_topo::explore_unique;
3488
3489    const T: Tolerances = Tolerances::millimetres();
3490
3491    /// `refine_direction` as it was written: rescan from zero after every
3492    /// split. Kept here as the reference the cursor form is held to.
3493    fn refine_by_rescan<F: Fn(f64, f64) -> f64>(lo: f64, hi: f64, chord: f64, sag: F) -> Vec<f64> {
3494        let mut values = vec![lo, f64::midpoint(lo, hi), hi];
3495        while values.len() < MAX_DIRECTION_STEPS {
3496            let Some(i) = (0..values.len() - 1).find(|&i| sag(values[i], values[i + 1]) > chord)
3497            else {
3498                break;
3499            };
3500            let mid = f64::midpoint(values[i], values[i + 1]);
3501            if mid <= values[i] || mid >= values[i + 1] {
3502                break;
3503            }
3504            values.insert(i + 1, mid);
3505        }
3506        values
3507    }
3508
3509    #[test]
3510    fn walking_forward_splits_where_rescanning_did() {
3511        // The cursor is only sound because splitting an interval cannot change
3512        // whether an earlier one sags. Held to the old form's output exactly,
3513        // over sag profiles that bite in different places: flat, steep at one
3514        // end, periodic, and one savage enough to reach the step cap.
3515        /// A named sag profile to hold both forms to.
3516        type Profile = (&'static str, Box<dyn Fn(f64, f64) -> f64>);
3517        let cases: Vec<Profile> = vec![
3518            ("flat", Box::new(|_a: f64, _b: f64| 0.0)),
3519            ("width", Box::new(|a: f64, b: f64| (b - a).abs())),
3520            (
3521                "steep at the low end",
3522                Box::new(|a: f64, b: f64| (b - a).abs() / a.abs().max(1e-3)),
3523            ),
3524            (
3525                "periodic",
3526                Box::new(|a: f64, b: f64| (b - a).abs() * (a * 12.0).sin().abs()),
3527            ),
3528            (
3529                "beyond the cap",
3530                Box::new(|a: f64, b: f64| (b - a).abs() * 1e6),
3531            ),
3532        ];
3533        for (name, sag) in cases {
3534            for chord in [1.0, 0.1, 0.01, 1e-3] {
3535                let walked = refine_direction(0.0, 1.0, chord, &sag);
3536                let rescanned = refine_by_rescan(0.0, 1.0, chord, &sag);
3537                assert_eq!(
3538                    walked, rescanned,
3539                    "{name} at chord {chord}: the cursor split somewhere the rescan did not"
3540                );
3541            }
3542        }
3543    }
3544
3545    #[test]
3546    fn banded_containment_answers_what_the_full_scan_answers() {
3547        // The index may only decline to visit edges that could not have
3548        // counted. Held to the unindexed predicate over a ring with a hole,
3549        // on a grid that straddles both boundaries and the vertices themselves.
3550        let outer: Vec<Point2> = vec![
3551            Point2::new(0.0, 0.0),
3552            Point2::new(4.0, 0.0),
3553            Point2::new(4.0, 3.0),
3554            Point2::new(2.0, 1.5),
3555            Point2::new(0.0, 3.0),
3556        ];
3557        let hole: Vec<Point2> = vec![
3558            Point2::new(1.0, 0.5),
3559            Point2::new(1.0, 1.0),
3560            Point2::new(1.5, 1.0),
3561            Point2::new(1.5, 0.5),
3562        ];
3563        let rings = vec![outer, hole];
3564        let bands = RingBands::over(&rings);
3565        for i in 0..=80 {
3566            for j in 0..=60 {
3567                #[allow(clippy::cast_precision_loss)]
3568                let p = Point2::new(f64::from(i) * 0.05 - 0.1, f64::from(j) * 0.05 - 0.1);
3569                assert_eq!(
3570                    bands.holds(p),
3571                    inside_region(&rings, p),
3572                    "the index disagreed with the full scan at {p:?}"
3573                );
3574            }
3575        }
3576    }
3577
3578    fn fine() -> Deflection {
3579        Deflection {
3580            chord: 1e-3,
3581            angular: 0.05,
3582            ..Deflection::default()
3583        }
3584    }
3585
3586    /// A vertex at a cone's apex or a sphere's pole carries the normal the
3587    /// surface tends to there along its own column, not nothing.
3588    #[test]
3589    fn apex_and_pole_vertices_carry_the_limit_normal() {
3590        let mut model = Model::new();
3591        let cone = ogeom_algo::make_cone(&mut model, Frame::WORLD, 2.0, 0.0, 3.0, T).unwrap();
3592        let sphere = ogeom_algo::make_sphere(&mut model, Frame::WORLD, 1.5, T).unwrap();
3593        for (shape, what) in [(&cone.shape, "cone"), (&sphere.shape, "sphere")] {
3594            for face in explore_unique(&model, shape, ShapeType::Face).unwrap() {
3595                let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
3596                for (i, n) in mesh.normals.iter().enumerate() {
3597                    assert!(
3598                        (n.magnitude() - 1.0).abs() < 1e-9,
3599                        "{what} vertex {i} at {:?} has normal {n:?}",
3600                        mesh.positions[i]
3601                    );
3602                }
3603            }
3604        }
3605        // At the apex the limit normal along a ruling is that ruling's
3606        // normal: on a cone of half-angle atan(2/3) it leans out by that
3607        // much from the axis, the same as every other normal in its column.
3608        let faces = explore_unique(&model, &cone.shape, ShapeType::Face).unwrap();
3609        let lean = (2.0_f64 / 3.0).atan();
3610        for face in &faces {
3611            let mesh = triangulate_face(&model, face, fine(), T).unwrap();
3612            for (i, p) in mesh.positions.iter().enumerate() {
3613                if p.distance(Point::new(0.0, 0.0, 3.0)) < 1e-9 {
3614                    let n = mesh.normals[i];
3615                    let from_axis = n.z.abs().acos();
3616                    assert!(
3617                        ((std::f64::consts::FRAC_PI_2 - from_axis) - lean).abs() < 1e-6,
3618                        "apex normal {n:?} leans {from_axis} from the axis"
3619                    );
3620                }
3621            }
3622        }
3623    }
3624
3625    #[test]
3626    fn a_box_face_triangulates_into_two_triangles() {
3627        // A planar square needs no interior points at all, which is what makes
3628        // deflection-driven refinement worth having: a fixed grid would add
3629        // dozens that buy nothing.
3630        let mut model = Model::new();
3631        let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
3632        let faces = explore_unique(&model, &built.shape, ShapeType::Face).unwrap();
3633
3634        for face in &faces {
3635            let mesh = triangulate_face(&model, face, fine(), T).unwrap();
3636            assert_eq!(mesh.triangle_count(), 2, "a rectangle is two triangles");
3637            assert_eq!(mesh.vertex_count(), 4);
3638            assert!(mesh.deflection_met);
3639        }
3640    }
3641
3642    #[test]
3643    fn a_boxs_mesh_is_closed_and_reports_the_right_volume() {
3644        // The end-to-end check: triangulate, weld, and ask the mesh what it
3645        // encloses. A volume that comes out negative would mean the faces are
3646        // wound inward; one that is wrong in magnitude would mean the
3647        // triangulation is not covering the boundary.
3648        let mut model = Model::new();
3649        let size = (2.0, 3.0, 4.0);
3650        let built = make_box(&mut model, Frame::WORLD, size, T).unwrap();
3651        let mesh = triangulate(&model, &built.shape, fine(), T).unwrap();
3652
3653        assert_eq!(mesh.triangle_count(), 12, "six faces, two triangles each");
3654        assert_eq!(mesh.vertex_count(), 8, "welding merged the shared corners");
3655        assert!(
3656            mesh.is_closed(),
3657            "every triangle edge should be shared by two"
3658        );
3659        assert_relative_eq!(mesh.volume(), size.0 * size.1 * size.2, epsilon = 1e-9);
3660        assert_relative_eq!(
3661            mesh.area(),
3662            2.0 * (size.0 * size.1 + size.1 * size.2 + size.2 * size.0),
3663            epsilon = 1e-9
3664        );
3665    }
3666
3667    #[test]
3668    fn welding_is_what_closes_the_mesh() {
3669        // Without it each face brings its own copy of every boundary vertex, so
3670        // no triangle edge is shared and the surface is a pile of loose squares.
3671        let mut model = Model::new();
3672        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3673
3674        let mut loose = Triangulation::new();
3675        for face in ogeom_topo::explore(
3676            &model,
3677            &built.shape,
3678            ogeom_topo::Filter::OfType(ShapeType::Face),
3679        )
3680        .unwrap()
3681        {
3682            loose.append(&triangulate_face(&model, &face, fine(), T).unwrap());
3683        }
3684        assert_eq!(loose.vertex_count(), 24, "four corners per face, unmerged");
3685        assert!(!loose.is_closed());
3686
3687        let welded = loose.welded(T);
3688        assert_eq!(welded.vertex_count(), 8);
3689        assert!(welded.is_closed());
3690    }
3691
3692    #[test]
3693    fn a_reversed_face_presents_the_other_side() {
3694        // A renderer or a volume computation that ignored orientation would
3695        // have the solid inside out, and nothing about the positions says so.
3696        let mut model = Model::new();
3697        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3698        let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
3699
3700        let forward = triangulate_face(&model, &face, fine(), T).unwrap();
3701        let backward = triangulate_face(&model, &face.reversed(), fine(), T).unwrap();
3702
3703        assert_eq!(forward.triangle_count(), backward.triangle_count());
3704        for (a, b) in forward.normals.iter().zip(&backward.normals) {
3705            assert!(a.is_equal(-*b, T), "normals did not flip: {a:?} vs {b:?}");
3706        }
3707        // And the winding flipped with them, so the two agree.
3708        let winding = |m: &Triangulation, i: usize| {
3709            let [a, b, c] = m.triangles[i].map(|k| m.positions[k as usize]);
3710            (b - a).cross(c - a)
3711        };
3712        assert!(winding(&forward, 0).dot(winding(&backward, 0)) < 0.0);
3713    }
3714
3715    #[test]
3716    fn every_triangle_vertex_lies_on_the_surface_it_came_from() {
3717        let mut model = Model::new();
3718        let built = make_box(&mut model, Frame::WORLD, (2.0, 1.0, 3.0), T).unwrap();
3719
3720        for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
3721            let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
3722            let data = model.node(&face).unwrap().data().as_face().unwrap().clone();
3723            let surface = model.geometry().surface(data.surface).unwrap();
3724            for (position, (u, v)) in mesh.positions.iter().zip(&mesh.parameters) {
3725                let exact = surface.point_at(*u, *v, T).unwrap();
3726                assert!(
3727                    position.is_equal(exact, T),
3728                    "{position:?} is not on its surface"
3729                );
3730            }
3731        }
3732    }
3733
3734    #[test]
3735    fn a_finer_deflection_never_gives_fewer_triangles() {
3736        let mut model = Model::new();
3737        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3738        let coarse = triangulate(&model, &built.shape, Deflection::default(), T).unwrap();
3739        let detailed = triangulate(&model, &built.shape, fine(), T).unwrap();
3740        assert!(detailed.triangle_count() >= coarse.triangle_count());
3741        // Both enclose the same volume, since the faces are flat.
3742        assert_relative_eq!(coarse.volume(), 1.0, epsilon = 1e-9);
3743        assert_relative_eq!(detailed.volume(), 1.0, epsilon = 1e-9);
3744    }
3745
3746    #[test]
3747    fn a_sphere_gets_interior_points_and_converges_on_its_true_area() {
3748        // The whole point of measuring deflection in space rather than in
3749        // parameter space. A sphere's parameter rectangle is uniform; the
3750        // surface it maps to is not, and a fixed grid would be dense at the
3751        // poles and coarse at the equator.
3752        use ogeom_algo::make_natural_face;
3753        use ogeom_geom::SphereSurface;
3754        use ogeom_math::Sphere;
3755
3756        let radius = 10.0;
3757        let exact = 4.0 * std::f64::consts::PI * radius * radius;
3758        let mut previous = 0.0;
3759
3760        for chord in [1.0_f64, 0.25, 0.05] {
3761            let mut model = Model::new();
3762            let surface = SphereSurface::new(Sphere::new(Frame::WORLD, radius, T).unwrap());
3763            let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
3764            let deflection = Deflection {
3765                chord,
3766                ..Deflection::default()
3767            };
3768            let mesh = triangulate_face(&model, &face, deflection, T).unwrap();
3769
3770            assert!(
3771                mesh.triangle_count() > 2,
3772                "a curved face needs interior points, got {} triangles",
3773                mesh.triangle_count()
3774            );
3775            // Every triangle chord-cuts the sphere, so the area comes in under
3776            // the truth and climbs as the tolerance tightens.
3777            let area = mesh.area();
3778            assert!(area < exact, "a chord-cut area cannot exceed the surface's");
3779            assert!(
3780                area > previous,
3781                "tightening the chord from the previous step lost area: \
3782                 {area} after {previous}"
3783            );
3784            previous = area;
3785        }
3786        assert!(
3787            previous > exact * 0.99,
3788            "at a chord of 0.05 on a radius of 10 the area should be within a \
3789             percent, got {previous} against {exact}"
3790        );
3791    }
3792
3793    #[test]
3794    fn every_sphere_vertex_sits_at_the_right_radius() {
3795        // Lifting through the surface is what makes the mesh curved at all; a
3796        // vertex left in parameter space, or lifted with the wrong parameters,
3797        // would land nowhere near the sphere.
3798        use ogeom_algo::make_natural_face;
3799        use ogeom_geom::SphereSurface;
3800        use ogeom_math::Sphere;
3801
3802        let mut model = Model::new();
3803        let surface = SphereSurface::new(Sphere::new(Frame::WORLD, 3.0, T).unwrap());
3804        let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
3805        let mesh = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
3806
3807        for p in &mesh.positions {
3808            assert_relative_eq!(p.to_vector().magnitude(), 3.0, epsilon = 1e-9);
3809        }
3810    }
3811
3812    #[test]
3813    fn a_curved_domains_boundary_is_refined_not_just_its_corners() {
3814        // The corners alone would leave the triangulation nothing to connect to
3815        // along a side, so it reaches right across the domain for one, and a
3816        // sliver from a sphere's equator to its pole makes the summed area
3817        // diverge under refinement rather than converge.
3818        use ogeom_geom::{PlaneSurface, SphereSurface};
3819        use ogeom_math::{Plane, Sphere};
3820
3821        let sphere: SurfaceGeometry =
3822            SphereSurface::new(Sphere::new(Frame::WORLD, 10.0, T).unwrap()).into();
3823        let coarse = domain_ring(&sphere, Deflection::default(), T);
3824        let fine_ring = domain_ring(&sphere, fine(), T);
3825        assert!(coarse.len() > 4, "a sphere's domain edge is curved");
3826        assert!(
3827            fine_ring.len() > coarse.len(),
3828            "a tighter chord should place more boundary points"
3829        );
3830
3831        // No side may repeat a corner: a zero-length constraint is not one.
3832        for w in fine_ring.windows(2) {
3833            assert!(!w[0].is_equal(w[1], T), "the ring repeats a point");
3834        }
3835
3836        // A plane is flat, so its domain needs only what closes the rectangle.
3837        let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
3838        assert!(domain_ring(&plane, fine(), T).len() >= 4);
3839    }
3840
3841    #[test]
3842    fn winding_detects_points_inside_and_outside_a_ring() {
3843        let square = vec![
3844            Point2::new(0.0, 0.0),
3845            Point2::new(1.0, 0.0),
3846            Point2::new(1.0, 1.0),
3847            Point2::new(0.0, 1.0),
3848        ];
3849        assert!(inside_region(
3850            std::slice::from_ref(&square),
3851            Point2::new(0.5, 0.5)
3852        ));
3853        assert!(!inside_region(
3854            std::slice::from_ref(&square),
3855            Point2::new(1.5, 0.5)
3856        ));
3857        assert!(!inside_region(
3858            std::slice::from_ref(&square),
3859            Point2::new(0.5, -0.5)
3860        ));
3861
3862        // With a hole, the middle is outside again.
3863        let hole = vec![
3864            Point2::new(0.4, 0.4),
3865            Point2::new(0.6, 0.4),
3866            Point2::new(0.6, 0.6),
3867            Point2::new(0.4, 0.6),
3868        ];
3869        let with_hole = vec![square, hole];
3870        assert!(!inside_region(&with_hole, Point2::new(0.5, 0.5)));
3871        assert!(inside_region(&with_hole, Point2::new(0.2, 0.2)));
3872    }
3873
3874    #[test]
3875    fn a_polyline_of_an_edge_runs_in_the_edges_direction() {
3876        let mut model = Model::new();
3877        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3878        let edge = explore_unique(&model, &built.shape, ShapeType::Edge).unwrap()[0].clone();
3879
3880        let forward = polyline_of_edge(&model, &edge, fine(), T).unwrap();
3881        let backward = polyline_of_edge(&model, &edge.reversed(), fine(), T).unwrap();
3882        assert!(forward.len() >= 2);
3883        assert!(forward[0].is_equal(backward[backward.len() - 1], T));
3884        assert!(forward[forward.len() - 1].is_equal(backward[0], T));
3885    }
3886
3887    #[test]
3888    fn triangulating_something_that_is_not_a_face_is_refused() {
3889        let mut model = Model::new();
3890        let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3891        assert!(triangulate_face(&model, &built.shape, fine(), T).is_err());
3892
3893        let vertex = explore_unique(&model, &built.shape, ShapeType::Vertex).unwrap()[0].clone();
3894        assert!(triangulate_face(&model, &vertex, fine(), T).is_err());
3895        assert!(polyline_of_edge(&model, &vertex, fine(), T).is_err());
3896    }
3897
3898    #[test]
3899    fn a_triangles_normal_is_none_when_it_is_degenerate() {
3900        let a = Point::ORIGIN;
3901        let b = Point::new(1.0, 0.0, 0.0);
3902        assert!(triangle_normal(a, b, Point::new(0.0, 1.0, 0.0), T).is_some());
3903        assert!(triangle_normal(a, b, Point::new(2.0, 0.0, 0.0), T).is_none());
3904        assert!(triangle_normal(a, a, a, T).is_none());
3905    }
3906}
3907
3908#[cfg(test)]
3909#[allow(clippy::unwrap_used)]
3910mod predicate_tests {
3911    use super::*;
3912    use ogeom_core::Fast;
3913
3914    /// A triangle with one very long, very nearly diagonal edge.
3915    ///
3916    /// The configuration where a naive crossing test goes wrong. Subtracting
3917    /// the edge's ends from a query point close to the line cancels almost
3918    /// every significant digit, and what is left is rounding rather than
3919    /// geometry.
3920    fn sliver() -> Vec<Vec<Point2>> {
3921        vec![vec![
3922            Point2::new(0.5, 0.5),
3923            Point2::new(1000.0, 1000.0),
3924            Point2::new(1000.0, 0.5),
3925        ]]
3926    }
3927
3928    #[test]
3929    fn the_two_implementations_are_a_real_choice_and_not_a_decoration() {
3930        // The point of the seam. In a band of near-degenerate queries the two
3931        // answer differently, and if they never did, routing through the trait
3932        // would be ceremony rather than a design.
3933        //
3934        // The query points straddle the long edge at a spacing far below what
3935        // the subtraction can resolve, which is exactly the case a mesh hits
3936        // when a face's boundary passes close to a triangulation vertex.
3937        let rings = sliver();
3938        let step = 2.0_f64.powi(-48);
3939        let mut disagreements = 0;
3940        for i in 0..256i32 {
3941            for j in 0..256i32 {
3942                let p = Point2::new(
3943                    250.0 + f64::from(i - 128) * step,
3944                    250.0 + f64::from(j - 128) * step,
3945                );
3946                if inside_boundary_with::<Exact>(&rings, p)
3947                    != inside_boundary_with::<Fast>(&rings, p)
3948                {
3949                    disagreements += 1;
3950                }
3951            }
3952        }
3953        assert!(
3954            disagreements > 0,
3955            "the exact and fast predicates never disagreed, so the seam is not \
3956             carrying anything"
3957        );
3958    }
3959
3960    #[test]
3961    fn the_exact_predicate_is_the_one_that_is_right_near_the_edge() {
3962        // Disagreeing is not enough; the exact one has to be the *correct*
3963        // one, and that needs points whose side is known without asking either
3964        // implementation.
3965        //
3966        // Stepped in units of the last place rather than by a small distance.
3967        // A distance below the spacing of `f64` at 250 rounds away entirely,
3968        // leaving two points that are both exactly *on* the diagonal, where
3969        // either answer is defensible and the test would be asserting nothing.
3970        let rings = sliver();
3971        let base = 250.0_f64;
3972        let mut off = base;
3973        for k in 1..64 {
3974            off = off.next_up();
3975            assert_ne!(off, base, "step {k} did not move the point at all");
3976            // Inside this triangle is below the diagonal `y = x`: larger x.
3977            assert!(
3978                inside_boundary_with::<Exact>(&rings, Point2::new(off, base)),
3979                "a point {k} ulps below the diagonal was reported outside"
3980            );
3981            assert!(
3982                !inside_boundary_with::<Exact>(&rings, Point2::new(base, off)),
3983                "a point {k} ulps above the diagonal was reported inside"
3984            );
3985        }
3986    }
3987
3988    #[test]
3989    fn the_exact_answer_is_the_one_the_geometry_supports() {
3990        // Points placed by construction, so the right answer is known without
3991        // asking either implementation.
3992        let square = vec![vec![
3993            Point2::new(0.0, 0.0),
3994            Point2::new(1.0, 0.0),
3995            Point2::new(1.0, 1.0),
3996            Point2::new(0.0, 1.0),
3997        ]];
3998        assert!(inside_boundary_with::<Exact>(
3999            &square,
4000            Point2::new(0.5, 0.5)
4001        ));
4002        assert!(!inside_boundary_with::<Exact>(
4003            &square,
4004            Point2::new(1.5, 0.5)
4005        ));
4006        assert!(!inside_boundary_with::<Exact>(
4007            &square,
4008            Point2::new(-0.5, 0.5)
4009        ));
4010        assert!(!inside_boundary_with::<Exact>(
4011            &square,
4012            Point2::new(0.5, 1.5)
4013        ));
4014
4015        // A vertex exactly on the sampling ray is counted once, not twice or
4016        // not at all, which is what the half-open comparison is for.
4017        let diamond = vec![vec![
4018            Point2::new(0.0, 0.0),
4019            Point2::new(1.0, 1.0),
4020            Point2::new(2.0, 0.0),
4021            Point2::new(1.0, -1.0),
4022        ]];
4023        assert!(inside_boundary_with::<Exact>(
4024            &diamond,
4025            Point2::new(1.0, 0.0)
4026        ));
4027        assert!(!inside_boundary_with::<Exact>(
4028            &diamond,
4029            Point2::new(3.0, 0.0)
4030        ));
4031        assert!(!inside_boundary_with::<Exact>(
4032            &diamond,
4033            Point2::new(-1.0, 0.0)
4034        ));
4035    }
4036
4037    /// A cylinder's grid has as many rows as its length needs, not as
4038    /// many as its sag asks for.
4039    #[test]
4040    fn rows_are_spread_until_no_cell_is_taller_than_its_aspect() {
4041        // Sixteen columns a millimetre wide over a region a hundred long:
4042        // sag left one interior row, the aspect wants cells six tall.
4043        let sagged = vec![0.0, 50.0, 100.0];
4044        let spread = spread_to_aspect(
4045            sagged.clone(),
4046            0.0,
4047            16.0,
4048            |_| 17,
4049            |a, b| (b - a).abs(),
4050            |a, b, _| (b - a).abs(),
4051        );
4052        assert_eq!(
4053            spread.len(),
4054            2 * 9 + 1,
4055            "each 50 mm half in nine 6 mm pieces: {spread:?}"
4056        );
4057        assert_eq!(spread[0], 0.0);
4058        assert_eq!(spread[9], 50.0, "the rows sag chose stay where they were");
4059        assert_eq!(spread[18], 100.0);
4060        assert!(spread.windows(2).all(|w| w[1] > w[0]));
4061
4062        // Three columns is flat along `u` as well; nothing to hold to.
4063        let flat = spread_to_aspect(
4064            sagged.clone(),
4065            0.0,
4066            16.0,
4067            |_| 3,
4068            |a, b| (b - a).abs(),
4069            |a, b, _| (b - a).abs(),
4070        );
4071        assert_eq!(flat, sagged);
4072
4073        // Cells already shorter than the aspect are left alone.
4074        let fine = vec![0.0, 4.0, 8.0];
4075        let same = spread_to_aspect(
4076            fine.clone(),
4077            0.0,
4078            16.0,
4079            |_| 17,
4080            |a, b| (b - a).abs(),
4081            |a, b, _| (b - a).abs(),
4082        );
4083        assert_eq!(same, fine);
4084    }
4085
4086    /// A ring's slop-duplicates and spikes come off before it is
4087    /// triangulated.
4088    #[test]
4089    fn a_ring_is_cleaned_of_duplicates_and_spikes() {
4090        let p = |x: f64, y: f64| Point2::new(x, y);
4091        // A square whose closing point repeats its first a hair off, and
4092        // whose right side steps out to a point and straight back.
4093        let mut ring = vec![
4094            p(0.0, 0.0),
4095            p(10.0, 0.0),
4096            p(10.0, 5.0),
4097            p(10.2, 5.0),
4098            p(10.0, 5.0 + 1e-9),
4099            p(10.0, 10.0),
4100            p(0.0, 10.0),
4101            p(1e-9, 1e-9),
4102        ];
4103        let mut anchors = vec![None; ring.len()];
4104        let reach = chart_reach(&ring);
4105        merge_near_duplicates(&mut ring, &mut anchors, reach);
4106        assert_eq!(ring.len(), 7, "the closing duplicate is gone: {ring:?}");
4107        remove_spikes(&mut ring, &mut anchors, reach);
4108        assert_eq!(
4109            ring,
4110            vec![
4111                p(0.0, 0.0),
4112                p(10.0, 0.0),
4113                p(10.0, 5.0),
4114                p(10.0, 10.0),
4115                p(0.0, 10.0)
4116            ],
4117            "the spike is gone and the ring is the square with a point on one side"
4118        );
4119        assert_eq!(anchors.len(), ring.len());
4120    }
4121
4122    #[test]
4123    fn a_hole_is_outside_however_either_ring_is_wound() {
4124        // Even-odd does not depend on the wires being wound consistently, which
4125        // is what makes it survive imported geometry.
4126        let outer = vec![
4127            Point2::new(0.0, 0.0),
4128            Point2::new(4.0, 0.0),
4129            Point2::new(4.0, 4.0),
4130            Point2::new(0.0, 4.0),
4131        ];
4132        let hole: Vec<Point2> = vec![
4133            Point2::new(1.0, 1.0),
4134            Point2::new(3.0, 1.0),
4135            Point2::new(3.0, 3.0),
4136            Point2::new(1.0, 3.0),
4137        ];
4138        let backwards: Vec<Point2> = hole.iter().rev().copied().collect();
4139        for inner in [hole, backwards] {
4140            let rings = vec![outer.clone(), inner];
4141            assert!(!inside_boundary_with::<Exact>(
4142                &rings,
4143                Point2::new(2.0, 2.0)
4144            ));
4145            assert!(inside_boundary_with::<Exact>(&rings, Point2::new(0.5, 0.5)));
4146        }
4147    }
4148}