Skip to main content

ogeom_algo/
sew.rs

1//! Joining loose topology: ordering a bag of edges into a wire, and sewing free
2//! faces into a shell.
3//!
4//! Both exist because geometry arrives disconnected. An imported file gives a
5//! pile of faces that *touch* but share nothing; a sketch gives edges in
6//! whatever order they were drawn. Topologically these are unrelated pieces,
7//! and every algorithm that walks a boundary treats them that way: a shell of
8//! faces that merely abut has a free edge everywhere two of them meet, encloses
9//! no volume, and cannot be classified against.
10//!
11//! # Sewing is a topological operation, not a geometric one
12//!
13//! It does not move anything. Two edges within tolerance of each other are
14//! decided to be *one* edge, and every face that used either uses that one,
15//! so the shell closes because the topology says so, not because the geometry
16//! was nudged until it did. A version that moved geometry to close gaps would
17//! be a repair, would need to decide which of two positions is right, and would
18//! quietly invalidate every tolerance in the neighbourhood.
19//!
20//! What it will not do is claim a closure it did not achieve. Faces that do not
21//! meet within tolerance stay in separate shells, and the result says how many
22//! there are.
23
24use ogeom_core::{FastMap, FastSet};
25
26use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
27use ogeom_geom::Curve3d;
28use ogeom_math::Point;
29use ogeom_topo::{
30    EdgeRepr, Filter, Model, NodeData, Orientation, Shape, ShapeType, TShapeId, explore,
31    explore_unique,
32};
33
34use crate::bins::Bins;
35use crate::build::{edge_vertices, make_face_on, make_shell, make_wire};
36use crate::history::{Built, History};
37
38/// Roles sewing assigns.
39pub mod roles {
40    use ogeom_core::Role;
41
42    /// An edge that two faces were found to share.
43    pub const SEWN_EDGE: Role = Role::op_defined(40);
44    /// A face rebuilt on shared edges.
45    pub const SEWN_FACE: Role = Role::op_defined(41);
46}
47
48/// Put a bag of edges into an order that walks them end to end.
49///
50/// Reverses an edge where the chain reaches its far end first, so the result is
51/// a path rather than a set. [`make_wire`] then accepts it: it checks that
52/// consecutive edges meet, and a bag in the order it happened to be built in
53/// almost never does.
54///
55/// Follows the chain from one end. Where an end meets more than two edges the
56/// path is genuinely ambiguous (that is a branching network, not a wire), and
57/// this refuses rather than picking one, because picking one silently discards
58/// the branch nobody asked it to drop.
59///
60/// # Errors
61///
62/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the list is
63/// empty, an edge is unbounded, the edges do not form a single connected path,
64/// or a vertex joins three or more of them.
65pub fn order_edges(model: &Model, edges: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
66    if edges.is_empty() {
67        ogeom_bail!(Construction, "there are no edges to order");
68    }
69    if edges.len() == 1 {
70        return Ok(edges.to_vec());
71    }
72
73    let mut ends = Vec::with_capacity(edges.len());
74    for edge in edges {
75        let Some((start, finish)) = edge_vertices(model, edge)? else {
76            ogeom_bail!(
77                Construction,
78                "an unbounded edge cannot be shown to join anything"
79            );
80        };
81        ends.push((placed(model, &start)?, placed(model, &finish)?));
82    }
83
84    // How many edge-ends meet at each position. Three is a branch, and a branch
85    // has no single walk through it.
86    for i in 0..edges.len() {
87        for at in [ends[i].0, ends[i].1] {
88            let meeting = ends
89                .iter()
90                .filter(|(a, b)| a.is_equal(at, tol) || b.is_equal(at, tol))
91                .count();
92            if meeting > 2 {
93                ogeom_bail!(
94                    Construction,
95                    "{meeting} edges meet at {at:?}; that is a branching \
96                     network rather than a wire, and choosing a path through it \
97                     would silently drop the branches not chosen"
98                );
99            }
100        }
101    }
102
103    // Start from a free end if there is one, so an open chain comes out running
104    // the way it reads. A closed loop has none, and any edge will do.
105    let start = (0..edges.len())
106        .find(|&i| {
107            !ends.iter().enumerate().any(|(j, (a, b))| {
108                j != i && (a.is_equal(ends[i].0, tol) || b.is_equal(ends[i].0, tol))
109            })
110        })
111        .unwrap_or(0);
112
113    let mut used = vec![false; edges.len()];
114    let mut out = Vec::with_capacity(edges.len());
115    used[start] = true;
116    out.push(edges[start].clone());
117    let mut reach = ends[start].1;
118
119    while out.len() < edges.len() {
120        let mut stepped = false;
121        for i in 0..edges.len() {
122            if used[i] {
123                continue;
124            }
125            let (a, b) = ends[i];
126            if a.is_equal(reach, tol) {
127                out.push(edges[i].clone());
128                reach = b;
129            } else if b.is_equal(reach, tol) {
130                // The chain arrived at this edge's far end, so it is walked
131                // backwards. Reversing the occurrence is what keeps the wire a
132                // path; leaving it would make `make_wire` report a gap that is
133                // really a direction.
134                out.push(edges[i].reversed());
135                reach = a;
136            } else {
137                continue;
138            }
139            used[i] = true;
140            stepped = true;
141            break;
142        }
143        if !stepped {
144            ogeom_bail!(
145                Construction,
146                "the edges do not form one connected path: {} of {} could not \
147                 be reached from the first",
148                edges.len() - out.len(),
149                edges.len()
150            );
151        }
152    }
153    Ok(out)
154}
155
156/// Build a wire from edges in any order.
157///
158/// [`order_edges`] then [`make_wire`].
159///
160/// # Errors
161///
162/// As [`order_edges`] and [`make_wire`].
163pub fn make_wire_unordered(
164    model: &mut Model,
165    edges: &[Shape],
166    tol: Tolerances,
167) -> OgeomResult<Built> {
168    let ordered = order_edges(model, edges, tol)?;
169    make_wire(model, &ordered, tol)
170}
171
172/// What sewing produced.
173#[derive(Debug, Clone)]
174pub struct Sewn {
175    /// One shell per connected group of faces.
176    ///
177    /// More than one means the faces did not all meet. That is reported rather
178    /// than papered over: a single shell containing disconnected pieces would
179    /// claim a closure that is not there.
180    pub shells: Vec<Shape>,
181    /// How many pairs of edges were found to be the same edge.
182    pub joined: usize,
183    /// Edges still used by exactly one face after sewing.
184    ///
185    /// Zero means every shell is closed. Anything else is the boundary that
186    /// remains, and a caller that needs a solid needs this to be empty.
187    pub free_edges: Vec<Shape>,
188    /// History, as every operation reports.
189    pub history: History,
190}
191
192impl Sewn {
193    /// The edges two faces of a shell both walk the same way, in shell
194    /// order: none where every face keeps its material on the left of its
195    /// rings.
196    ///
197    /// Sewing turns faces until every edge two of them share is walked once
198    /// each way, so an edge named here belongs to a shell that admits no
199    /// such orientation (a band with a half twist), whose faces sewing left
200    /// as given.
201    ///
202    /// # Errors
203    ///
204    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a shell
205    /// is not in `model`.
206    pub fn edges_walked_one_way(&self, model: &Model) -> OgeomResult<Vec<Shape>> {
207        let mut out = Vec::new();
208        for shell in &self.shells {
209            let mut walks: FastMap<(TShapeId, ogeom_topo::Location), (Shape, usize, usize)> =
210                FastMap::default();
211            let mut order = Vec::new();
212            for face in explore(model, shell, Filter::OfType(ShapeType::Face))? {
213                for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
214                    if model
215                        .node(&edge)
216                        .and_then(|n| n.data().as_edge())
217                        .is_some_and(|d| d.degenerate)
218                    {
219                        continue;
220                    }
221                    let key = (edge.node(), edge.location().clone());
222                    let walk = walks.entry(key.clone()).or_insert_with(|| {
223                        order.push(key);
224                        (edge.clone(), 0, 0)
225                    });
226                    walk.1 += 1;
227                    if edge.orientation() == Orientation::Forward {
228                        walk.2 += 1;
229                    }
230                }
231            }
232            out.extend(order.into_iter().filter_map(|key| {
233                let (edge, count, forward) = walks.remove(&key)?;
234                (count == 2 && forward != 1).then_some(edge)
235            }));
236        }
237        Ok(out)
238    }
239}
240
241/// Sew free faces into shells by finding the edges they share.
242///
243/// Two edges are the same edge when their ends coincide within tolerance
244/// (either way round) *and* a point along them does too. The midpoint test is
245/// what stops two different arcs between the same pair of vertices from being
246/// merged into one, which is a real case: the two halves of a circle share both
247/// ends.
248///
249/// Each face keeps its material on the left of its rings, so two faces
250/// sharing an edge walk it opposite ways. Where two faces of a shell walk
251/// a shared edge the same way, the shell's faces are turned over breadth
252/// first across their shared edges until each is walked once each way,
253/// keeping the orientation most of them already have (open shells too);
254/// a closed shell whose faces were turned is then made to face out of the
255/// volume it bounds. A closed shell whose faces already agree is kept as
256/// given, facing in or out: it may bound a void, which only the solid it
257/// joins can say, and [`make_solid`](crate::make_solid) turns it to face
258/// out as an outer shell or in as a void. A shell that admits no such
259/// orientation (a band with a half twist) keeps its faces as given, and
260/// [`Sewn::edges_walked_one_way`] names the edges that show it.
261///
262/// Nothing is moved. See the module documentation. What the faces share
263/// with other shapes is copied before the sewing edits it
264/// ([`Model::unshare_each`]); the history runs from the faces given.
265///
266/// # Errors
267///
268/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if `faces` is
269/// empty or holds something that is not a face;
270/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle fails to
271/// resolve.
272pub fn sew(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Sewn> {
273    sew_with(model, faces, 0.0, tol)
274}
275
276/// Sew faces whose edges meet across a gap of up to `gap`.
277///
278/// As [`sew`], with two edges taken for one where their ends and middle
279/// lie within `gap` of each other rather than within the confusion
280/// distance: surfaces built apart (an imported sheet, a fitted fill beside
281/// an exact extrusion) meet a few micrometres apart. The edge kept, and the
282/// vertices it ends on, widen their tolerances to reach the edge they
283/// replace, as healing does, so the shell checks clean. An edge that runs
284/// along two or more shorter edges of other faces is first split where
285/// their vertices meet it within `gap`, so each piece has a twin.
286///
287/// The widening is done in place on the given faces' own edges and
288/// vertices; only what they share with other shapes is copied first. The
289/// faces given therefore carry the wider tolerances afterwards, and a later
290/// [`sew`] of the same faces joins what this one joined. To keep the faces
291/// as they were, sew a copy of them.
292///
293/// # Errors
294///
295/// As [`sew`], and [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
296/// if `gap` is not finite and non-negative.
297pub fn sew_within(
298    model: &mut Model,
299    faces: &[Shape],
300    gap: f64,
301    tol: Tolerances,
302) -> OgeomResult<Sewn> {
303    if !(gap.is_finite() && gap >= 0.0) {
304        ogeom_bail!(
305            Construction,
306            "the gap to sew across, {gap}, is not finite and non-negative"
307        );
308    }
309    sew_with(model, faces, gap, tol)
310}
311
312/// Sew faces of which some already share their edges and vertices with
313/// every neighbour and are to be passed through as they stand.
314///
315/// As [`sew`], with each face whose `settled` flag is set taken as already
316/// sewn: it is neither compared nor rebuilt, and comes through as an exact
317/// copy of itself into the shell it closes with the others. The question is
318/// asked only of the other faces, so a large shell with a few faces to sew
319/// costs what those few cost. Where sewing the others would move an edge or
320/// a vertex a settled face holds, the faces are sewn as [`sew`] sews them,
321/// all alike. Unlike [`sew`], nothing the faces share with other shapes is
322/// copied first: the edges and vertices kept are edited where they stand.
323///
324/// # Errors
325///
326/// As [`sew`], and [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
327/// if `settled` does not hold one flag per face.
328pub fn sew_around(
329    model: &mut Model,
330    faces: &[Shape],
331    settled: &[bool],
332    tol: Tolerances,
333) -> OgeomResult<Sewn> {
334    sew_around_within(model, faces, settled, 0.0, tol)
335}
336
337/// Sew faces of which some are settled, as [`sew_around`], across a gap of
338/// up to `gap` as [`sew_within`] sews: the edge kept, and the vertices it
339/// ends on, widen to reach the edge they replace.
340///
341/// # Errors
342///
343/// As [`sew_around`], and [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
344/// if `gap` is not finite and non-negative.
345pub fn sew_around_within(
346    model: &mut Model,
347    faces: &[Shape],
348    settled: &[bool],
349    gap: f64,
350    tol: Tolerances,
351) -> OgeomResult<Sewn> {
352    if !(gap.is_finite() && gap >= 0.0) {
353        ogeom_bail!(
354            Construction,
355            "the gap to sew across, {gap}, is not finite and non-negative"
356        );
357    }
358    if settled.len() != faces.len() {
359        ogeom_bail!(
360            Construction,
361            "{} settled flags for {} faces; one flag per face",
362            settled.len(),
363            faces.len()
364        );
365    }
366    check_faces(model, faces)?;
367    model.begin_operation();
368    if let Some(sewn) = sew_faces(model, faces, settled, gap, tol)? {
369        return Ok(sewn);
370    }
371    let none = vec![false; faces.len()];
372    match sew_faces(model, faces, &none, gap, tol)? {
373        Some(sewn) => Ok(sewn),
374        None => ogeom_bail!(
375            Construction,
376            "sewing with no face settled moved a settled face"
377        ),
378    }
379}
380
381fn check_faces(model: &Model, faces: &[Shape]) -> OgeomResult<()> {
382    if faces.is_empty() {
383        ogeom_bail!(Construction, "there are no faces to sew");
384    }
385    for face in faces {
386        if model.kind_of(face)? != ShapeType::Face {
387            ogeom_bail!(Construction, "sewing joins faces");
388        }
389    }
390    Ok(())
391}
392
393fn sew_with(model: &mut Model, faces: &[Shape], gap: f64, tol: Tolerances) -> OgeomResult<Sewn> {
394    check_faces(model, faces)?;
395    model.begin_operation();
396    // Sewing widens and describes the edges and vertices it keeps where
397    // they stand, so what other shapes hold as well is copied first, a
398    // face held whole included; the history runs from the faces given.
399    let given = faces;
400    let faces = model.unshare_each(given)?.shapes;
401    let none = vec![false; faces.len()];
402    let Some(mut sewn) = sew_faces(model, &faces, &none, gap, tol)? else {
403        ogeom_bail!(
404            Construction,
405            "sewing with no face settled moved a settled face"
406        );
407    };
408    let mut copied = History::new();
409    for (given, face) in given.iter().zip(&faces) {
410        if given.node() != face.node() {
411            copied.copy(given, face.clone());
412        }
413    }
414    if !copied.is_empty() {
415        sewn.history = copied.then(&sewn.history);
416    }
417    Ok(sewn)
418}
419
420/// The sewing itself, the faces flagged `settled` passed through as they
421/// stand; `None` where sewing the others would move an edge or a vertex a
422/// settled face holds.
423fn sew_faces(
424    model: &mut Model,
425    all: &[Shape],
426    settled: &[bool],
427    gap: f64,
428    tol: Tolerances,
429) -> OgeomResult<Option<Sewn>> {
430    let reach = gap.max(tol.confusion());
431    let originals: Vec<Shape> = all
432        .iter()
433        .zip(settled)
434        .filter(|(_, settled)| !**settled)
435        .map(|(face, _)| face.clone())
436        .collect();
437    // What the settled faces hold, which the sewing of the others must
438    // leave where it is.
439    let mut held_edges: FastSet<TShapeId> = FastSet::default();
440    let mut held_vertices: FastSet<TShapeId> = FastSet::default();
441    for (face, _) in all.iter().zip(settled).filter(|(_, settled)| **settled) {
442        for edge in explore_unique(model, face, ShapeType::Edge)? {
443            held_edges.insert(edge.node());
444        }
445        for vertex in explore_unique(model, face, ShapeType::Vertex)? {
446            held_vertices.insert(vertex.node());
447        }
448    }
449
450    // Sewing tells edges apart by their nodes, so one node placed twice (a
451    // prism's far end edges are its profile's, carried along it by a
452    // location) would be read as one edge where it stands first. A face
453    // holding such a node is baked: rebuilt with its placements in its
454    // geometry, every edge a node of its own where it stands.
455    let mut baked = unshared(model, &originals, tol)?;
456    if gap > 0.0 {
457        baked = split_at_vertices(model, &baked, reach, tol)?;
458    }
459    let faces = baked.as_slice();
460
461    // Vertices first, and this is not an optimisation; it is what makes the
462    // rest work. Deciding that two edges are one edge leaves the *neighbouring*
463    // edges ending at the vertices they always had, which sit at the same
464    // places as the survivor's but are different nodes. A wire built from that
465    // mixture is reported to have a gap, because it has one: `is_same_position`
466    // asks whether one node appears at two placements, which is the right
467    // question and not this one.
468    let mut vertices = merge_vertices(model, faces, reach, tol)?;
469    // Two edges decided to be one must end on the same vertices, or the
470    // faces that bounded the dropped one keep its neighbours ending where
471    // the dropped one did, and their wires open. Twins whose ends are still
472    // two vertices (each within its own span of the other edge's end, not
473    // of the other vertex) have those vertices joined, and the edges are
474    // rebuilt on them and matched again.
475    let mut rounds = 0;
476    let (rebuilt_edges, mut merged, mut joined) = loop {
477        let rebuilt_edges = rebuild_edges(model, faces, &vertices)?;
478
479        // Every distinct edge node used by the faces, with the geometry that
480        // decides whether two of them are the same edge.
481        let mut catalogue: Vec<(TShapeId, Fingerprint)> = Vec::new();
482        let mut catalogued: FastSet<TShapeId> = FastSet::default();
483        for face in faces {
484            for edge in explore_unique(model, face, ShapeType::Edge)? {
485                let id = rebuilt_edges
486                    .get(&edge.node())
487                    .copied()
488                    .unwrap_or(edge.node());
489                if !catalogued.insert(id) {
490                    continue;
491                }
492                if let Some(mut print) = fingerprint(model, &Shape::of(id), tol)? {
493                    // Every comparison honours an edge's width, so the gap
494                    // sewn across is carried as one.
495                    print.width = print.width.max(reach);
496                    print.ends = print.ends.map(|w| w.max(reach));
497                    catalogue.push((id, print));
498                }
499            }
500        }
501
502        // Which node each edge is decided to *be*, and whether it runs the other
503        // way from the one it replaced.
504        let mut merged: FastMap<TShapeId, (TShapeId, bool)> = FastMap::default();
505        let mut joined = 0;
506        let starts = StartBins::new(&catalogue, tol);
507        for i in 0..catalogue.len() {
508            if merged.contains_key(&catalogue[i].0) {
509                continue;
510            }
511            for j in starts.twin_candidates(&catalogue, i) {
512                if merged.contains_key(&catalogue[j].0) {
513                    continue;
514                }
515                let Some(flipped) = catalogue[i].1.same_as(&catalogue[j].1, tol)? else {
516                    continue;
517                };
518                // The survivor answers for both descriptions of the edge,
519                // and its vertices must reach the twin's ends: two fingerprints
520                // that matched within their stated widths may still disagree by
521                // more than a fresh vertex's tolerance, and the disagreement is
522                // recorded where the data model records it.
523                let (kept_fp, dropped_fp) = (catalogue[i].1.clone(), catalogue[j].1.clone());
524                let survivor = Shape::of(catalogue[i].0);
525                let ends = if flipped {
526                    [
527                        (kept_fp.start, dropped_fp.end),
528                        (kept_fp.end, dropped_fp.start),
529                    ]
530                } else {
531                    [
532                        (kept_fp.start, dropped_fp.start),
533                        (kept_fp.end, dropped_fp.end),
534                    ]
535                };
536                let bounds = model.children_of(&survivor)?;
537                for vertex in &bounds {
538                    if let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) {
539                        let at = data.point;
540                        let mut need = data.tolerance.get();
541                        for (a, b) in &ends {
542                            if at.distance(*a) <= need.max(tol.confusion() * 1e2) {
543                                need = need.max(a.distance(*b) + tol.confusion());
544                            }
545                        }
546                        if need > data.tolerance.get()
547                            && let Some(node) = model.node_mut(vertex)
548                            && let NodeData::Vertex(v) = node.data_mut()
549                        {
550                            v.tolerance = v.tolerance.widen_to(need);
551                        }
552                    }
553                }
554                // Across a gap, the kept edge answers for where the dropped
555                // one ran: its tolerance reaches the farthest of the dropped
556                // edge's ends and middle from it.
557                if gap > 0.0 {
558                    let mut need = 0.0_f64;
559                    for p in [dropped_fp.start, dropped_fp.middle, dropped_fp.end] {
560                        need = need.max(kept_fp.off(p, tol)?);
561                    }
562                    let need = need + tol.confusion();
563                    // Its vertices with it: none is held tighter than an
564                    // edge it bounds.
565                    model.widen(&survivor, ogeom_core::Tolerance::new(need)?)?;
566                }
567                merged.insert(catalogue[j].0, (catalogue[i].0, flipped));
568                joined += 1;
569            }
570        }
571        let apart = twin_ends_apart(model, &merged)?;
572        if apart.is_empty() || rounds == 3 {
573            break (rebuilt_edges, merged, joined);
574        }
575        rounds += 1;
576        for (gone, keep) in apart {
577            join_vertex(model, &mut vertices, gone, keep, tol)?;
578        }
579    };
580    // A settled face keeps its edges and vertices, so none of them may be
581    // merged away or rebuilt.
582    if vertices.keys().any(|v| held_vertices.contains(v))
583        || rebuilt_edges.keys().any(|e| held_edges.contains(e))
584        || merged.keys().any(|e| held_edges.contains(e))
585    {
586        return Ok(None);
587    }
588
589    // The survivor has to carry the pcurves of the edge it replaced, or the
590    // face that used the replaced one loses its description in parameter space
591    // and stops being triangulable.
592    //
593    // Carrying is not copying. When the merge *flipped* (the two edges run
594    // opposite ways), the dropped edge's pcurve traverses the shared points
595    // backwards relative to the survivor's own curve, and copied unchanged it
596    // makes the survivor's face walk one edge of its boundary the wrong way:
597    // the parameter-space ring zigzags to zero area and the face stops being
598    // triangulable. The pcurve is consumed by *proportional* same-parameter
599    // mapping, so reversing its traversal exactly is swapping the stored
600    // range's ends.
601    //
602    // Nor is it copying when the two edges describe one curve at different
603    // paces: a fitted rim against the exact circle it traces, which the
604    // match admits by asking each middle to lie on the other's stretch. The
605    // dropped edge's pcurve is same-parameter with the *dropped* curve; on
606    // the survivor's parameter it drifts along the edge, and a face walks
607    // its boundary off the vertex it shares. Such a pcurve is refitted at
608    // the survivor's own parameters: the survivor's point at each, found
609    // on the dropped curve, read through the pcurve into the chart.
610    // In a fixed order: this adds pcurves to the geometry tables and
611    // appends to survivors' representations, and a map's order would make
612    // both differ from run to run.
613    let mut carries: Vec<(TShapeId, (TShapeId, bool))> =
614        merged.iter().map(|(d, k)| (*d, *k)).collect();
615    carries.sort_by_key(|(dropped, _)| dropped.index());
616    for (dropped, (kept, flipped)) in carries {
617        let carried: Vec<EdgeRepr> = model
618            .node_by_id(dropped)
619            .and_then(|n| n.data().as_edge())
620            .map(|d| {
621                d.representations
622                    .iter()
623                    .filter(|r| r.is_parametric())
624                    .cloned()
625                    .collect()
626            })
627            .unwrap_or_default();
628        if carried.is_empty() {
629            continue;
630        }
631        let survivor = Shape::of(kept);
632        let Some((paced, missed)) = repaced_carry(
633            model,
634            &Shape::of(dropped),
635            &survivor,
636            flipped,
637            &carried,
638            reach,
639            tol,
640        )?
641        else {
642            // The dropped edge's pcurves, carried to the survivor's pace,
643            // run elsewhere than the survivor does: the two are not one
644            // edge, and each stays with its own faces.
645            merged.remove(&dropped);
646            joined -= 1;
647            continue;
648        };
649        let off = carried_off(model, &survivor, &paced, tol)?;
650        // A face finds an edge's pcurve by its surface, so a carried pcurve
651        // on a surface the survivor already holds one on must run where
652        // that one does, to within what the survivor will state: a face on
653        // the surface reads the first. Where they part further (two edges
654        // that agree only within a vertex widened at one of their ends),
655        // the two are not one edge on that surface.
656        let stated = Some(off.map_or(missed, |o| o.max(missed)));
657        if parts_from_own(model, &survivor, &paced, stated, tol)? {
658            merged.remove(&dropped);
659            joined -= 1;
660            continue;
661        }
662        let Some(node) = model.node_mut(&survivor) else {
663            ogeom_bail!(Dangling, "an edge is not in this model");
664        };
665        let NodeData::Edge(data) = node.data_mut() else {
666            ogeom_bail!(Construction, "edge node holds no edge data");
667        };
668        let off = if missed > data.tolerance.get() {
669            Some(off.map_or(missed, |o| o.max(missed)))
670        } else {
671            off
672        };
673        for repr in paced {
674            data.add(repr);
675        }
676        // Two edges a little apart (a fitted section beside the circle it
677        // hugs) are one edge to their ends' tolerances, and the survivor's
678        // curve then stands off the twin's pcurves by up to the gap between
679        // them. The survivor states that gap, and its vertices hold it,
680        // with the smallest tolerance as margin: a fuzzy sew's confusion
681        // is its fuzz.
682        if let Some(off) = off {
683            model.widen(
684                &survivor,
685                ogeom_core::Tolerance::new(off + ogeom_core::Tolerance::MIN.get())?,
686            )?;
687        }
688    }
689
690    // One map from every original edge to what it becomes: rebuilt onto merged
691    // vertices, then possibly merged with a coincident twin.
692    let mut substitution: FastMap<TShapeId, (TShapeId, bool)> = FastMap::default();
693    for (original, rebuilt) in &rebuilt_edges {
694        let (final_id, flipped) = merged.get(rebuilt).copied().unwrap_or((*rebuilt, false));
695        substitution.insert(*original, (final_id, flipped));
696    }
697    for (dropped, kept) in &merged {
698        substitution.entry(*dropped).or_insert(*kept);
699    }
700    // An edge whose two ends were merged into one vertex, all of it within
701    // the reach of that vertex, has shrunk to the point: no ring walks it.
702    let mut collapsed: FastSet<TShapeId> = FastSet::default();
703    for (original, rebuilt) in &rebuilt_edges {
704        let ends = |id: TShapeId| -> Option<(TShapeId, TShapeId)> {
705            let children = model.node_by_id(id)?.children();
706            Some((children.first()?.node(), children.last()?.node()))
707        };
708        let (Some((a, b)), Some((c, d))) = (ends(*original), ends(*rebuilt)) else {
709            continue;
710        };
711        if a == b || c != d {
712            continue;
713        }
714        let Some(print) = fingerprint(model, &Shape::of(*rebuilt), tol)? else {
715            continue;
716        };
717        let at = placed(model, &Shape::of(c))?;
718        let held = model
719            .node_by_id(c)
720            .and_then(|n| n.data().as_vertex())
721            .map_or(0.0, |v| v.tolerance.get())
722            .max(reach);
723        let points = [
724            print.start,
725            print.middle,
726            print.end,
727            print.quarters[0],
728            print.quarters[1],
729        ];
730        if points.iter().all(|p| p.distance(at) <= held) {
731            let (id, _) = substitution
732                .get(original)
733                .copied()
734                .unwrap_or((*rebuilt, false));
735            collapsed.insert(id);
736        }
737    }
738
739    let mut history = History::new();
740    let mut rebuilt = Vec::with_capacity(all.len());
741    // Each rebuilt face's original, and whether it is that original's copy.
742    let mut sources: Vec<(Shape, bool)> = Vec::with_capacity(all.len());
743    let mut sewing = faces.iter().zip(&originals);
744    for (face, settled) in all.iter().zip(settled) {
745        if *settled {
746            // Its edges and vertices stay as they were: the face is its own
747            // copy.
748            model.set_derived(face, std::slice::from_ref(face), roles::SEWN_FACE)?;
749            rebuilt.push(face.clone());
750            sources.push((face.clone(), true));
751            continue;
752        }
753        let Some((face, original)) = sewing.next() else {
754            ogeom_bail!(Construction, "a face to sew was lost");
755        };
756        let Some((sewn, whole)) = rebuild_face(model, face, &substitution, &collapsed, tol)? else {
757            history.delete(original);
758            continue;
759        };
760        model.set_derived(&sewn, std::slice::from_ref(original), roles::SEWN_FACE)?;
761        // Moved onto the shared edges with every ring kept, the face is the
762        // same face: a copy, on its own surface within its own boundary.
763        rebuilt.push(sewn);
764        sources.push((original.clone(), whole && face == original));
765    }
766
767    for face in &rebuilt {
768        seam_walked_both_ways(model, face, tol)?;
769    }
770    let groups = connected_groups(model, &rebuilt)?;
771    let mut shells = Vec::with_capacity(groups.len());
772    for group in groups {
773        let members: Vec<Shape> = group.iter().map(|&i| rebuilt[i].clone()).collect();
774        let mut shell = make_shell(model, &members)?.shape;
775        if orient_group(model, &mut rebuilt, &group, tol)? {
776            let members: Vec<Shape> = group.iter().map(|&i| rebuilt[i].clone()).collect();
777            shell = make_shell(model, &members)?.shape;
778        }
779        for &i in &group {
780            let (original, copy) = &sources[i];
781            // A face turned over to face out of what it bounds is the same
782            // face presented the other way: changed, not copied.
783            if *copy && rebuilt[i].orientation() == original.orientation() {
784                history.copy(original, rebuilt[i].clone());
785            } else {
786                history.modify(original, rebuilt[i].clone());
787            }
788            history.generate(&rebuilt[i], shell.clone());
789        }
790        shells.push(shell);
791    }
792
793    let free_edges = free_edges(model, &rebuilt)?;
794    Ok(Some(Sewn {
795        shells,
796        joined,
797        free_edges,
798        history,
799    }))
800}
801
802/// How `faces` turn so that every edge two of them walk is walked once
803/// each way, and the runs of faces that edges join.
804#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct Agreement {
806    /// Whether each face, by position, is to be turned.
807    pub turn: Vec<bool>,
808    /// The faces, by position, joined across edges with exactly two of
809    /// them: each run, turned as [`Agreement::turn`] says, faces one way
810    /// throughout.
811    pub runs: Vec<Vec<usize>>,
812}
813
814/// Which of `faces` to turn so that every edge two of them share is walked
815/// once each way, or `None` where no turning does (a band with a half
816/// twist).
817///
818/// Each face keeps its material on the left of its rings, so two faces
819/// sharing an edge agree where they walk it opposite ways, and a face
820/// that walks it the way its neighbour does is turned against it. The
821/// faces are oriented breadth first over their shared edges from a seed,
822/// and each run of them keeps the orientation most of its faces already
823/// have (its first face's on a tie). An edge with one face or more than
824/// two, a degenerate edge and a seam walked both ways by its one face
825/// constrain nothing. Nothing is measured: the answer is the topology's.
826///
827/// # Errors
828///
829/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a face is
830/// not in `model`.
831pub fn agreeing_orientation(model: &Model, faces: &[Shape]) -> OgeomResult<Option<Agreement>> {
832    // Each edge's walks, by position: which faces walk it and whether
833    // forward.
834    let mut walkers: FastMap<(TShapeId, ogeom_topo::Location), Vec<(usize, bool)>> =
835        FastMap::default();
836    for (k, face) in faces.iter().enumerate() {
837        for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
838            if model
839                .node(&edge)
840                .and_then(|n| n.data().as_edge())
841                .is_some_and(|d| d.degenerate)
842            {
843                continue;
844            }
845            walkers
846                .entry((edge.node(), edge.location().clone()))
847                .or_default()
848                .push((k, edge.orientation() == Orientation::Forward));
849        }
850    }
851    // Each face's neighbours across an edge with exactly two faces, and
852    // whether the two must differ in sense to walk it opposite ways (they
853    // must where they now walk it the same way).
854    let mut across: Vec<Vec<(usize, bool)>> = vec![Vec::new(); faces.len()];
855    for walks in walkers.values() {
856        if let [(a, a_forward), (b, b_forward)] = walks[..]
857            && a != b
858        {
859            let differ = a_forward == b_forward;
860            across[a].push((b, differ));
861            across[b].push((a, differ));
862        }
863    }
864    let mut turn: Vec<Option<bool>> = vec![None; faces.len()];
865    let mut runs = Vec::new();
866    for seed in 0..faces.len() {
867        if turn[seed].is_some() {
868            continue;
869        }
870        turn[seed] = Some(false);
871        let mut run = vec![seed];
872        let mut queue = std::collections::VecDeque::from([seed]);
873        while let Some(k) = queue.pop_front() {
874            let here = turn[k].unwrap_or(false);
875            for &(n, differ) in &across[k] {
876                let want = here != differ;
877                match turn[n] {
878                    None => {
879                        turn[n] = Some(want);
880                        run.push(n);
881                        queue.push_back(n);
882                    }
883                    Some(has) if has != want => return Ok(None),
884                    Some(_) => {}
885                }
886            }
887        }
888        // The run keeps the orientation most of its faces have.
889        let against = run.iter().filter(|&&k| turn[k] == Some(true)).count();
890        if 2 * against > run.len() {
891            for &k in &run {
892                turn[k] = turn[k].map(|t| !t);
893            }
894        }
895        runs.push(run);
896    }
897    Ok(Some(Agreement {
898        turn: turn.into_iter().map(|t| t == Some(true)).collect(),
899        runs,
900    }))
901}
902
903/// Turn the faces of one group (indices into `faces`) as
904/// [`agreeing_orientation`] says; whether any was turned. A closed group
905/// whose faces were turned is then made to face out of what it bounds:
906/// where its meshed volume comes out negative, every face is turned.
907/// Where the faces admit no consistent orientation, every face is left
908/// as given, and [`Sewn::edges_walked_one_way`] names the edges that say
909/// so.
910fn orient_group(
911    model: &mut Model,
912    faces: &mut [Shape],
913    group: &[usize],
914    tol: Tolerances,
915) -> OgeomResult<bool> {
916    let uses = walks(model, group.iter().map(|&i| &faces[i]))?;
917    if walked_each_way(&uses) {
918        return Ok(false);
919    }
920    let closed = uses.values().all(|(count, _)| *count >= 2);
921    let members: Vec<Shape> = group.iter().map(|&i| faces[i].clone()).collect();
922    let Some(agreement) = agreeing_orientation(model, &members)? else {
923        return Ok(false);
924    };
925    if !agreement.turn.contains(&true) {
926        return Ok(false);
927    }
928    let mut turned: Vec<Shape> = group
929        .iter()
930        .zip(&agreement.turn)
931        .map(|(&i, t)| {
932            if *t {
933                faces[i].reversed()
934            } else {
935                faces[i].clone()
936            }
937        })
938        .collect();
939    if closed {
940        let candidate = model.add_shell(&turned)?;
941        if let Ok(mesh) =
942            ogeom_mesh::triangulate(model, &candidate, ogeom_mesh::Deflection::default(), tol)
943            && mesh.volume() < 0.0
944        {
945            for face in &mut turned {
946                *face = face.reversed();
947            }
948        }
949    }
950    let mut changed = false;
951    for (&i, face) in group.iter().zip(turned) {
952        changed |= face.orientation() != faces[i].orientation();
953        faces[i] = face;
954    }
955    Ok(changed)
956}
957
958/// The faces, each holding an edge node that some face places elsewhere
959/// too baked into a copy with its placements in its geometry; the others
960/// as they are. Edges are matched on their nodes' own geometry, which is
961/// sound where every edge stands under one placement (an instance's faces
962/// sewn among themselves); where placements differ, a face holding a
963/// placed edge is baked too.
964fn unshared(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
965    let mut placements: FastMap<TShapeId, Vec<ogeom_topo::Location>> = FastMap::default();
966    let mut held: Vec<Vec<TShapeId>> = Vec::with_capacity(faces.len());
967    let mut placed: Vec<bool> = Vec::with_capacity(faces.len());
968    let mut seen: Vec<ogeom_topo::Location> = Vec::new();
969    for face in faces {
970        let mut nodes = Vec::new();
971        let mut moved = false;
972        for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
973            let list = placements.entry(edge.node()).or_default();
974            if !list.contains(edge.location()) {
975                list.push(edge.location().clone());
976            }
977            if !seen.contains(edge.location()) {
978                seen.push(edge.location().clone());
979            }
980            moved |= !edge.location().is_identity();
981            nodes.push(edge.node());
982        }
983        held.push(nodes);
984        placed.push(moved);
985    }
986    let mixed = seen.len() > 1;
987    let placed_twice: Vec<bool> = held
988        .iter()
989        .zip(&placed)
990        .map(|(nodes, moved)| {
991            (mixed && *moved)
992                || nodes
993                    .iter()
994                    .any(|n| placements.get(n).is_some_and(|l| l.len() > 1))
995        })
996        .collect();
997    let to_bake: Vec<Shape> = faces
998        .iter()
999        .zip(&placed_twice)
1000        .filter(|(_, twice)| **twice)
1001        .map(|(face, _)| face.clone())
1002        .collect();
1003    if to_bake.is_empty() {
1004        return Ok(faces.to_vec());
1005    }
1006    // Baked together, in a solid of their own for the rebuild to walk, so
1007    // the edges these faces share stay shared.
1008    let shell = model.add_shell(&to_bake)?;
1009    let solid = model.add_solid(&[shell])?;
1010    let baked = crate::baked_shape(model, &solid, tol)?;
1011    let mut out = Vec::with_capacity(faces.len());
1012    for (face, twice) in faces.iter().zip(&placed_twice) {
1013        let now = if *twice {
1014            baked.history.modified(face).first().cloned()
1015        } else {
1016            None
1017        };
1018        out.push(now.unwrap_or_else(|| face.clone()));
1019    }
1020    Ok(out)
1021}
1022
1023/// The faces with every edge split where a vertex of another edge meets
1024/// its interior within `reach`, the pieces ending on that vertex; faces
1025/// with no such edge as they are. Edges and vertices placed by a location
1026/// are left whole.
1027fn split_at_vertices(
1028    model: &mut Model,
1029    faces: &[Shape],
1030    reach: f64,
1031    tol: Tolerances,
1032) -> OgeomResult<Vec<Shape>> {
1033    let mut vertices: Vec<(TShapeId, Point)> = Vec::new();
1034    let mut seen: FastSet<TShapeId> = FastSet::default();
1035    for face in faces {
1036        for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1037            if vertex.location().is_identity() && seen.insert(vertex.node()) {
1038                vertices.push((vertex.node(), placed(model, &vertex)?));
1039            }
1040        }
1041    }
1042    // Each edge's cuts: the parameter, and the vertex the pieces meet at.
1043    let mut cuts: Vec<(TShapeId, Vec<(f64, TShapeId)>)> = Vec::new();
1044    let mut walked: FastSet<TShapeId> = FastSet::default();
1045    for face in faces {
1046        for edge in explore_unique(model, face, ShapeType::Edge)? {
1047            if !edge.location().is_identity() || !walked.insert(edge.node()) {
1048                continue;
1049            }
1050            let Some(print) = fingerprint(model, &edge, tol)? else {
1051                continue;
1052            };
1053            let own: Vec<TShapeId> = model.children_of(&edge)?.iter().map(Shape::node).collect();
1054            let deflection = ogeom_mesh::Deflection::default();
1055            let drawn = ogeom_mesh::polyline_of_edge(model, &edge, deflection, tol)?;
1056            let (mut lo, mut hi) = (drawn[0], drawn[0]);
1057            for p in &drawn {
1058                lo = Point::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
1059                hi = Point::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
1060            }
1061            let (r0, r1) = (
1062                print.range.0.min(print.range.1),
1063                print.range.0.max(print.range.1),
1064            );
1065            // The curve strays from its polyline's chords by up to the
1066            // chord deflection, past the polyline's box where it bulges
1067            // out at an extreme.
1068            let margin = reach + deflection.chord;
1069            let mut here: Vec<(f64, TShapeId)> = Vec::new();
1070            for &(v, p) in &vertices {
1071                let outside = p.x < lo.x - margin
1072                    || p.y < lo.y - margin
1073                    || p.z < lo.z - margin
1074                    || p.x > hi.x + margin
1075                    || p.y > hi.y + margin
1076                    || p.z > hi.z + margin;
1077                if own.contains(&v)
1078                    || outside
1079                    || p.distance(print.start) <= reach
1080                    || p.distance(print.end) <= reach
1081                {
1082                    continue;
1083                }
1084                let foot = crate::project_on_curve(&print.curve, p, 64, tol)?;
1085                if foot.distance > reach {
1086                    continue;
1087                }
1088                let mut t = foot.parameter;
1089                if print.curve.is_periodic() {
1090                    let (dlo, dhi) = print.curve.domain();
1091                    let period = dhi - dlo;
1092                    if period > 0.0 {
1093                        t = r0 + (t - r0).rem_euclid(period);
1094                    }
1095                }
1096                if t > r0 + tol.parametric() && t < r1 - tol.parametric() {
1097                    here.push((t, v));
1098                }
1099            }
1100            if !here.is_empty() {
1101                here.sort_by(|a, b| a.0.total_cmp(&b.0));
1102                here.dedup_by_key(|c| c.1);
1103                cuts.push((edge.node(), here));
1104            }
1105        }
1106    }
1107    if cuts.is_empty() {
1108        return Ok(faces.to_vec());
1109    }
1110    // Each cut edge's pieces, in its own direction.
1111    let mut pieces: FastMap<TShapeId, Vec<Shape>> = FastMap::default();
1112    for (node, at) in cuts {
1113        let edge = Shape::of(node);
1114        let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()).cloned() else {
1115            continue;
1116        };
1117        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
1118            continue;
1119        };
1120        let Some((first, last)) = edge_vertices(model, &edge)? else {
1121            continue;
1122        };
1123        let Some(geometry) = model.geometry().curve(curve).cloned() else {
1124            continue;
1125        };
1126        // The vertex at the curve's start, and the one at its end.
1127        let start_at = geometry.point_at(range.0, tol)?;
1128        let (from, to) = if placed(model, &first)?.distance(start_at)
1129            <= placed(model, &last)?.distance(start_at)
1130        {
1131            (first, last)
1132        } else {
1133            (last, first)
1134        };
1135        let rising = range.1 >= range.0;
1136        let mut stops: Vec<f64> = at.iter().map(|c| c.0).collect();
1137        let mut through: Vec<Shape> = at.iter().map(|c| Shape::of(c.1)).collect();
1138        if !rising {
1139            stops.reverse();
1140            through.reverse();
1141        }
1142        let mut params = vec![range.0];
1143        params.extend(stops);
1144        params.push(range.1);
1145        let mut ends = vec![from];
1146        ends.extend(through);
1147        ends.push(to);
1148        let span = range.1 - range.0;
1149        let mut out = Vec::with_capacity(params.len() - 1);
1150        for k in 0..params.len() - 1 {
1151            let (a, b) = (params[k], params[k + 1]);
1152            // Each representation's own stretch for this piece, read
1153            // proportionally, as same-parameter pcurves are.
1154            let sub = |r: (f64, f64)| {
1155                let at = |t: f64| r.0 + (t - range.0) / span * (r.1 - r.0);
1156                (at(a), at(b))
1157            };
1158            let mut piece = data.clone();
1159            piece.representations.retain(|r| {
1160                matches!(
1161                    r,
1162                    EdgeRepr::Curve3d { .. } | EdgeRepr::PCurve { .. } | EdgeRepr::Seam { .. }
1163                )
1164            });
1165            for r in &mut piece.representations {
1166                match r {
1167                    EdgeRepr::Curve3d { range: own, .. } => *own = (a, b),
1168                    EdgeRepr::PCurve { range: own, .. } | EdgeRepr::Seam { range: own, .. } => {
1169                        *own = sub(*own);
1170                    }
1171                    _ => {}
1172                }
1173            }
1174            // The vertex the piece ends on reaches the curve where the
1175            // piece does.
1176            for (vertex, t) in [(&ends[k], a), (&ends[k + 1], b)] {
1177                let need =
1178                    placed(model, vertex)?.distance(geometry.point_at(t, tol)?) + tol.confusion();
1179                if let Some(node) = model.node_mut(vertex)
1180                    && let NodeData::Vertex(v) = node.data_mut()
1181                    && need > v.tolerance.get()
1182                {
1183                    v.tolerance = v.tolerance.widen_to(need);
1184                }
1185            }
1186            out.push(model.add_edge(piece, &[ends[k].clone(), ends[k + 1].clone()])?);
1187        }
1188        pieces.insert(node, out);
1189    }
1190    let mut out = Vec::with_capacity(faces.len());
1191    for face in faces {
1192        let touched = explore_unique(model, face, ShapeType::Edge)?
1193            .iter()
1194            .any(|e| pieces.contains_key(&e.node()));
1195        let Some(data) = model.node(face).and_then(|n| n.data().as_face()).cloned() else {
1196            continue;
1197        };
1198        if !touched || !face.location().is_identity() {
1199            out.push(face.clone());
1200            continue;
1201        }
1202        // Read as stored, the face's own sense put back below.
1203        let stored = face.oriented(Orientation::Forward);
1204        let mut wires = Vec::new();
1205        for wire in model.ordered_children_of(&stored)? {
1206            let mut ring = Vec::new();
1207            for edge in model.ordered_children_of(&wire)? {
1208                match pieces.get(&edge.node()) {
1209                    Some(list) if edge.orientation() == Orientation::Reversed => {
1210                        ring.extend(list.iter().rev().map(Shape::reversed));
1211                    }
1212                    Some(list) => ring.extend(list.iter().cloned()),
1213                    None => ring.push(edge),
1214                }
1215            }
1216            wires.push(make_wire(model, &ring, tol)?.shape);
1217        }
1218        let split = make_face_on(model, data.surface, &wires, tol)?.shape;
1219        out.push(if face.orientation() == Orientation::Reversed {
1220            split.reversed()
1221        } else {
1222            split
1223        });
1224    }
1225    Ok(out)
1226}
1227
1228/// Decide which coincident vertices are the same vertex.
1229///
1230/// Returns only the ones that were replaced, mapping each to its survivor.
1231fn merge_vertices(
1232    model: &mut Model,
1233    faces: &[Shape],
1234    reach: f64,
1235    tol: Tolerances,
1236) -> OgeomResult<FastMap<TShapeId, TShapeId>> {
1237    let tolerance_of = |model: &Model, vertex: &Shape| {
1238        model
1239            .node(vertex)
1240            .and_then(|n| n.data().as_vertex())
1241            .map_or(0.0, |d| d.tolerance.get())
1242    };
1243    // The survivors, binned by position on cells sized to the loosest
1244    // vertex's reach; a vertex is compared with the survivors within the widest
1245    // reach any comparison can have, in the order they were kept.
1246    let mut loosest = reach;
1247    for face in faces {
1248        for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1249            loosest = loosest.max(tolerance_of(model, &vertex));
1250        }
1251    }
1252    let mut bins = Bins::new(loosest);
1253    let mut seen: Vec<(TShapeId, Point, f64)> = Vec::new();
1254    let mut index_of: FastMap<TShapeId, usize> = FastMap::default();
1255    let mut widest = reach;
1256    let mut out = FastMap::default();
1257    for face in faces {
1258        for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1259            if index_of.contains_key(&vertex.node()) {
1260                continue;
1261            }
1262            let at = placed(model, &vertex)?;
1263            let own = tolerance_of(model, &vertex);
1264            // Two vertices are one junction within what their *stated*
1265            // tolerances allow, not within a fresh vertex's default: a
1266            // vertex that recorded a welded gap reaches that far, and
1267            // merging by raw confusion would leave its twin standing a
1268            // recorded-but-ignored distance away.
1269            let meets =
1270                |(_, p, w): &&(TShapeId, Point, f64)| p.distance(at) <= reach.max(*w).max(own);
1271            let hit = match bins.near(at, widest.max(own)) {
1272                Some(near) => near.into_iter().map(|i| &seen[i]).find(meets),
1273                None => seen.iter().find(meets),
1274            }
1275            .map(|(kept, p, w)| (*kept, *p, *w));
1276            match hit {
1277                Some((kept, p, w)) => {
1278                    // The survivor answers for the absorbed vertex: its
1279                    // tolerance widens to reach the absorbed position plus
1280                    // whatever that vertex itself was allowed to stray.
1281                    let need = p.distance(at) + own + tol.confusion();
1282                    if need > w
1283                        && let Some(node) = model.node_mut(&Shape::of(kept))
1284                        && let NodeData::Vertex(v) = node.data_mut()
1285                    {
1286                        v.tolerance = v.tolerance.widen_to(need);
1287                    }
1288                    if let Some(entry) = index_of.get(&kept).map(|&i| &mut seen[i]) {
1289                        entry.2 = entry.2.max(need);
1290                        widest = widest.max(entry.2);
1291                    }
1292                    out.insert(vertex.node(), kept);
1293                }
1294                None => {
1295                    bins.insert(at, seen.len());
1296                    index_of.insert(vertex.node(), seen.len());
1297                    seen.push((vertex.node(), at, own));
1298                    widest = widest.max(own);
1299                }
1300            }
1301        }
1302    }
1303    Ok(out)
1304}
1305
1306/// The vertex pairs twin edges end on that are not yet one vertex: the
1307/// dropped edge's end first, the survivor's second.
1308fn twin_ends_apart(
1309    model: &Model,
1310    merged: &FastMap<TShapeId, (TShapeId, bool)>,
1311) -> OgeomResult<Vec<(TShapeId, TShapeId)>> {
1312    let ends = |id: TShapeId| -> Option<(TShapeId, TShapeId)> {
1313        let children = model.node_by_id(id)?.children();
1314        Some((children.first()?.node(), children.last()?.node()))
1315    };
1316    let mut out = Vec::new();
1317    let mut pairs: Vec<(&TShapeId, &(TShapeId, bool))> = merged.iter().collect();
1318    pairs.sort_by_key(|(dropped, _)| dropped.index());
1319    for (dropped, (kept, flipped)) in pairs {
1320        let (Some((d0, d1)), Some((k0, k1))) = (ends(*dropped), ends(*kept)) else {
1321            continue;
1322        };
1323        let matched = if *flipped {
1324            [(d0, k1), (d1, k0)]
1325        } else {
1326            [(d0, k0), (d1, k1)]
1327        };
1328        for (d, k) in matched {
1329            if d != k && !out.contains(&(d, k)) {
1330                out.push((d, k));
1331            }
1332        }
1333    }
1334    Ok(out)
1335}
1336
1337/// Make `gone` one vertex with `keep`: every vertex mapped to either then
1338/// maps to `keep`'s survivor, whose tolerance reaches `gone`'s span.
1339fn join_vertex(
1340    model: &mut Model,
1341    vertices: &mut FastMap<TShapeId, TShapeId>,
1342    gone: TShapeId,
1343    keep: TShapeId,
1344    tol: Tolerances,
1345) -> OgeomResult<()> {
1346    let resolve = |vertices: &FastMap<TShapeId, TShapeId>, mut v: TShapeId| {
1347        while let Some(&next) = vertices.get(&v) {
1348            if next == v {
1349                break;
1350            }
1351            v = next;
1352        }
1353        v
1354    };
1355    let (gone, keep) = (resolve(vertices, gone), resolve(vertices, keep));
1356    if gone == keep {
1357        return Ok(());
1358    }
1359    let (Some(g), Some(k)) = (
1360        model
1361            .node_by_id(gone)
1362            .and_then(|n| n.data().as_vertex())
1363            .map(|d| (d.point, d.tolerance.get())),
1364        model
1365            .node_by_id(keep)
1366            .and_then(|n| n.data().as_vertex())
1367            .map(|d| d.point),
1368    ) else {
1369        return Ok(());
1370    };
1371    let need = g.0.distance(k) + g.1 + tol.confusion();
1372    if let Some(node) = model.node_mut(&Shape::of(keep))
1373        && let NodeData::Vertex(v) = node.data_mut()
1374    {
1375        v.tolerance = v.tolerance.widen_to(need);
1376    }
1377    for target in vertices.values_mut() {
1378        if *target == gone {
1379            *target = keep;
1380        }
1381    }
1382    vertices.insert(gone, keep);
1383    Ok(())
1384}
1385
1386/// Whether a carried plain pcurve runs further from the survivor's own
1387/// plain pcurve on the same surface, at the same placement, than `check`
1388/// lets two fits of one edge part at the tolerance the survivor states
1389/// once it states `off`, measured where the two lie
1390/// on the surface: two columns of a closed surface's seam lie on the
1391/// same points, and become the seam's two sides, where two pcurves that
1392/// part on the surface are no one edge's.
1393fn parts_from_own(
1394    model: &Model,
1395    survivor: &Shape,
1396    carried: &[EdgeRepr],
1397    off: Option<f64>,
1398    tol: Tolerances,
1399) -> OgeomResult<bool> {
1400    let Some(data) = model.node(survivor).and_then(|n| n.data().as_edge()) else {
1401        ogeom_bail!(Dangling, "an edge is not in this model");
1402    };
1403    let stated = off.map_or(data.tolerance.get(), |o| {
1404        data.tolerance
1405            .get()
1406            .max(o + ogeom_core::Tolerance::MIN.get())
1407    });
1408    let reach = crate::check::CHART_GAP_SLACK * stated.max(tol.approximation());
1409    for repr in carried {
1410        let EdgeRepr::PCurve {
1411            curve,
1412            surface,
1413            location,
1414            range,
1415        } = repr
1416        else {
1417            continue;
1418        };
1419        for own in &data.representations {
1420            let EdgeRepr::PCurve {
1421                curve: own_curve,
1422                surface: own_surface,
1423                location: own_location,
1424                range: own_range,
1425            } = own
1426            else {
1427                continue;
1428            };
1429            if own_surface != surface || own_location != location {
1430                continue;
1431            }
1432            let (Some(on), Some(a), Some(b)) = (
1433                model.geometry().surface(*surface),
1434                model.geometry().pcurve(*own_curve),
1435                model.geometry().pcurve(*curve),
1436            ) else {
1437                ogeom_bail!(Dangling, "an edge names geometry not in this model");
1438            };
1439            if lifted_apart(on, (a, *own_range), (b, *range), tol)? > reach {
1440                return Ok(true);
1441            }
1442        }
1443    }
1444    Ok(false)
1445}
1446
1447/// The widest distance in space between two pcurves on one surface, each
1448/// lifted at the same fractions of its range.
1449fn lifted_apart(
1450    surface: &ogeom_geom::SurfaceGeometry,
1451    (a, ra): (&ogeom_geom::PlanarCurve, (f64, f64)),
1452    (b, rb): (&ogeom_geom::PlanarCurve, (f64, f64)),
1453    tol: Tolerances,
1454) -> OgeomResult<f64> {
1455    use ogeom_geom::{Curve2d as _, Surface as _};
1456    const SAMPLES: u32 = 4;
1457    let mut widest = 0.0_f64;
1458    for i in 0..=SAMPLES {
1459        let t = f64::from(i) / f64::from(SAMPLES);
1460        let pa = a.point_at(ra.0 + (ra.1 - ra.0) * t, tol)?;
1461        let pb = b.point_at(rb.0 + (rb.1 - rb.0) * t, tol)?;
1462        let apart = surface
1463            .point_at(pa.x, pa.y, tol)?
1464            .distance(surface.point_at(pb.x, pb.y, tol)?);
1465        widest = widest.max(apart);
1466    }
1467    Ok(widest)
1468}
1469
1470/// The dropped edge's parametric representations as the survivor carries
1471/// them: reversed when the merge flipped, and refitted at the survivor's
1472/// parameters when the two curves pace one stretch differently. Two edges
1473/// on one curve object, or on curves whose middle parameters land within
1474/// the pair's honesty of each other, are carried as they are.
1475///
1476/// A refit is lifted through its surface and measured against the
1477/// survivor's curve at the same parameter, bounded between samples by
1478/// [`widest_refined`]; the widest miss comes back with the carry, for the
1479/// survivor to state where it is wider than its tolerance. `None` where a
1480/// refit misses by more than the readers' trim cap, or the gap sewn across
1481/// where that is wider: the two edges are not one.
1482///
1483/// [`widest_refined`]: crate::pcurve_gap::widest_refined
1484fn repaced_carry(
1485    model: &mut Model,
1486    dropped: &Shape,
1487    survivor: &Shape,
1488    flipped: bool,
1489    carried: &[EdgeRepr],
1490    floor: f64,
1491    tol: Tolerances,
1492) -> OgeomResult<Option<(Vec<EdgeRepr>, f64)>> {
1493    let as_is = || -> Option<(Vec<EdgeRepr>, f64)> {
1494        Some((
1495            carried
1496                .iter()
1497                .cloned()
1498                .map(|r| if flipped { reversed_repr(r) } else { r })
1499                .collect(),
1500            0.0,
1501        ))
1502    };
1503    let (Some(dropped_fp), Some(kept_fp)) = (
1504        fingerprint(&*model, dropped, tol)?,
1505        fingerprint(&*model, survivor, tol)?,
1506    ) else {
1507        return Ok(as_is());
1508    };
1509    let reach = floor.max(dropped_fp.width).max(kept_fp.width);
1510    if dropped_fp.middle.distance(kept_fp.middle) <= reach {
1511        if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1512            eprintln!(
1513                "SEW carry as is: flipped {flipped}, middles {:.2e} apart, kept {:?} {:?} dropped {:?} {:?}",
1514                dropped_fp.middle.distance(kept_fp.middle),
1515                kept_fp.start,
1516                kept_fp.end,
1517                dropped_fp.start,
1518                dropped_fp.end
1519            );
1520        }
1521        return Ok(as_is());
1522    }
1523    const SAMPLES: usize = 24;
1524    let (klo, khi) = (kept_fp.range.0, kept_fp.range.1);
1525    let closes = {
1526        let (da, db) = dropped_fp.curve.domain();
1527        db > da
1528            && dropped_fp
1529                .curve
1530                .point_at(da, tol)?
1531                .distance(dropped_fp.curve.point_at(db, tol)?)
1532                <= tol.confusion()
1533    };
1534    // The survivor's own statement, below which a refit's miss need not be
1535    // known; and the line past which a refit is no description of the edge
1536    // at all: the readers' trim cap, or the gap sewn across where that is
1537    // wider, every pair within it being sewn as one edge.
1538    let stated = model
1539        .node(survivor)
1540        .and_then(|n| n.data().as_edge())
1541        .map_or(0.0, |d| d.tolerance.get());
1542    let cap = floor.max(tol.confusion() * crate::pcurve_fit::TRIM_CAP);
1543    let mut missed = 0.0_f64;
1544    let mut out = Vec::with_capacity(carried.len());
1545    let mut pending: Vec<(
1546        ogeom_topo::SurfaceId,
1547        ogeom_topo::Location,
1548        ogeom_geom::PlanarCurve,
1549        (f64, f64),
1550    )> = Vec::new();
1551    for repr in carried {
1552        let EdgeRepr::PCurve {
1553            curve: pc_id,
1554            surface,
1555            location,
1556            range: prange,
1557        } = repr
1558        else {
1559            out.push(if flipped {
1560                reversed_repr(repr.clone())
1561            } else {
1562                repr.clone()
1563            });
1564            continue;
1565        };
1566        let Some(pcurve) = model.geometry().pcurve(*pc_id) else {
1567            ogeom_bail!(Dangling, "pcurve is not in this model");
1568        };
1569        let periods = model.geometry().surface(*surface).map(|sg| {
1570            use ogeom_geom::Surface as _;
1571            let ((ua, ub), (va, vb)) = sg.domain();
1572            (
1573                if sg.is_periodic_u() { ub - ua } else { 0.0 },
1574                if sg.is_periodic_v() { vb - va } else { 0.0 },
1575            )
1576        });
1577        let (dlo, dhi) = (dropped_fp.range.0, dropped_fp.range.1);
1578        let mut params = Vec::with_capacity(SAMPLES + 1);
1579        let mut image: Vec<ogeom_math::Point2> = Vec::with_capacity(SAMPLES + 1);
1580        for k in 0..=SAMPLES {
1581            #[allow(clippy::cast_precision_loss)]
1582            let t = klo + (khi - klo) * (k as f64) / (SAMPLES as f64);
1583            let p = kept_fp.curve.point_at(t, tol)?;
1584            let foot = crate::project_on_curve(&dropped_fp.curve, p, 64, tol)?;
1585            // The foot's parameter on the dropped curve, then through the
1586            // proportional map onto the pcurve's own window. On a curve that
1587            // closes on itself the foot may come back a turn away from the
1588            // stretch (a rim's last piece, seen from its own points, sits
1589            // at the start of the loop as much as at its end) and is
1590            // carried across the turn before it is clamped.
1591            let (lo, hi) = (dlo.min(dhi), dlo.max(dhi));
1592            let (da, db) = dropped_fp.curve.domain();
1593            let turn = db - da;
1594            let mut s = foot.parameter;
1595            if closes {
1596                if s < lo - tol.parametric() && s + turn <= hi + tol.parametric() {
1597                    s += turn;
1598                } else if s > hi + tol.parametric() && s - turn >= lo - tol.parametric() {
1599                    s -= turn;
1600                }
1601            }
1602            // A foot still off the stretch on a curve that closes is held
1603            // at the end nearer it round the turn, not across the stretch:
1604            // just past the closing point is just before the stretch's
1605            // start.
1606            let s = if (s < lo || s > hi) && closes {
1607                let round = |a: f64, b: f64| {
1608                    let d = (a - b).rem_euclid(turn);
1609                    d.min(turn - d)
1610                };
1611                if round(s, lo) <= round(s, hi) { lo } else { hi }
1612            } else {
1613                s.clamp(lo, hi)
1614            };
1615            let pt = if (dhi - dlo).abs() <= f64::MIN_POSITIVE {
1616                prange.0
1617            } else {
1618                prange.0 + (prange.1 - prange.0) * (s - dlo) / (dhi - dlo)
1619            };
1620            let mut uv = ogeom_geom::Curve2d::point_at(pcurve, pt, tol)?;
1621            if let (Some((pu, pv)), Some(prev)) = (periods, image.last()) {
1622                for (coord, period, before) in [(&mut uv.x, pu, prev.x), (&mut uv.y, pv, prev.y)] {
1623                    if period > 0.0 {
1624                        while *coord - before > period / 2.0 {
1625                            *coord -= period;
1626                        }
1627                        while before - *coord > period / 2.0 {
1628                            *coord += period;
1629                        }
1630                    }
1631                }
1632            }
1633            params.push(t);
1634            image.push(uv);
1635        }
1636        let fitted =
1637            ogeom_geom::fit::fit_points_2d_at(&params, &image, 3, tol.confusion() * 10.0, tol)?;
1638        if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1639            use ogeom_geom::Surface as _;
1640            let mut worst_sample = 0.0_f64;
1641            let mut worst_fit = 0.0_f64;
1642            if let Some(sg) = model.geometry().surface(*surface) {
1643                for (t, uv) in params.iter().zip(&image) {
1644                    let p = kept_fp.curve.point_at(*t, tol)?;
1645                    worst_sample = worst_sample.max(sg.point_at(uv.x, uv.y, tol)?.distance(p));
1646                    let f = ogeom_geom::Curve2d::point_at(&fitted.curve, *t, tol)?;
1647                    worst_fit = worst_fit.max(sg.point_at(f.x, f.y, tol)?.distance(p));
1648                }
1649            }
1650            eprintln!(
1651                "SEW refit: samples off surface by {worst_sample:.2e}, fit off by {worst_fit:.2e} (fit error {:.2e} met {}) domain {:?} over ({klo:.4}, {khi:.4}); dropped range {:?}",
1652                fitted.error,
1653                fitted.met,
1654                ogeom_geom::Curve2d::domain(&fitted.curve),
1655                dropped_fp.range
1656            );
1657        }
1658        if !fitted.met || fitted.error > reach.max(tol.confusion() * 1e3) {
1659            // A refit that misses its budget is no description of the edge;
1660            // the pcurve is carried as it came, its drift along the edge and
1661            // all, rather than replaced by a worse one.
1662            out.push(if flipped {
1663                reversed_repr(repr.clone())
1664            } else {
1665                repr.clone()
1666            });
1667            continue;
1668        }
1669        let planar: ogeom_geom::PlanarCurve = fitted.curve.into();
1670        let Some(chart) = model.geometry().surface(*surface) else {
1671            ogeom_bail!(Dangling, "surface is not in this model");
1672        };
1673        let placement = location.composed(model.datums())?;
1674        let gap = |f: f64| -> OgeomResult<Option<ogeom_math::Vector>> {
1675            let t = klo + (khi - klo) * f;
1676            let uv = ogeom_geom::Curve2d::point_at(&planar, t, tol)?;
1677            let Ok(lifted) = ogeom_geom::Surface::point_at(chart, uv.x, uv.y, tol) else {
1678                return Ok(None);
1679            };
1680            Ok(Some(
1681                placement.apply(lifted) - kept_fp.curve.point_at(t, tol)?,
1682            ))
1683        };
1684        let mut breaks = crate::pcurve_gap::curve_breaks(&kept_fp.curve, (klo, khi));
1685        breaks.extend(crate::pcurve_gap::pcurve_breaks(&planar, (klo, khi)));
1686        let off = crate::pcurve_gap::widest_refined(
1687            &gap,
1688            &breaks,
1689            stated,
1690            tol.confusion(),
1691            tol.parametric() / (khi - klo).abs(),
1692        )?;
1693        if off > cap {
1694            return Ok(None);
1695        }
1696        missed = missed.max(off);
1697        pending.push((*surface, location.clone(), planar, (klo, khi)));
1698    }
1699    for (surface, location, planar, range) in pending {
1700        let curve = model.geometry_mut().add_pcurve(planar);
1701        out.push(EdgeRepr::PCurve {
1702            curve,
1703            surface,
1704            location,
1705            range,
1706        });
1707    }
1708    Ok(Some((out, missed)))
1709}
1710
1711/// Each edge `face` walks once each way, holding two plain pcurves on the
1712/// face's surface at one placement, made the seam it is.
1713///
1714/// Two edges sewn into one can both bound one face, as the two columns of
1715/// a closed surface's seam do: the lines where a plane cuts a drum within
1716/// the weld of tangent, merged into one line the drum's face meets on
1717/// both sides. Each carries its own column of the chart as a plain
1718/// pcurve, and a lookup by surface finds the first for both walks, so one
1719/// walk reads its boundary a period away. The column whose start meets
1720/// the end of the edge walked before it is the one a walk follows, and
1721/// the pair is stated as one seam, the forward walk's column first.
1722fn seam_walked_both_ways(model: &mut Model, face: &Shape, tol: Tolerances) -> OgeomResult<()> {
1723    use ogeom_geom::Curve2d as _;
1724    let Some(surface) = model
1725        .node(face)
1726        .and_then(|n| n.data().as_face())
1727        .map(|d| d.surface)
1728    else {
1729        return Ok(());
1730    };
1731    for wire in model.ordered_children_of(face)? {
1732        let walk = model.ordered_children_of(&wire)?;
1733        let n = walk.len();
1734        for (i, occurrence) in walk.iter().enumerate() {
1735            if occurrence.orientation() != ogeom_topo::Orientation::Forward {
1736                continue;
1737            }
1738            let Some(back) = walk.iter().position(|o| {
1739                o.node() == occurrence.node()
1740                    && o.orientation() == ogeom_topo::Orientation::Reversed
1741            }) else {
1742                continue;
1743            };
1744            let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
1745                continue;
1746            };
1747            let plain: Vec<(ogeom_topo::PCurveId, (f64, f64), ogeom_topo::Location)> = data
1748                .representations
1749                .iter()
1750                .filter_map(|r| match r {
1751                    EdgeRepr::PCurve {
1752                        curve,
1753                        surface: on,
1754                        location,
1755                        range,
1756                    } if *on == surface => Some((*curve, *range, location.clone())),
1757                    _ => None,
1758                })
1759                .collect();
1760            let [(first, range, at), (second, other_range, other_at)] = plain.as_slice() else {
1761                continue;
1762            };
1763            if at != other_at || range != other_range {
1764                continue;
1765            }
1766            // Where the walk stands in the chart just before the forward
1767            // walk of this edge: the end of the edge before it.
1768            let before = &walk[(i + n - 1) % n];
1769            if before.node() == occurrence.node() || back == (i + n - 1) % n {
1770                continue;
1771            }
1772            let Some(arrived) = chart_end(model, before, surface, tol)? else {
1773                continue;
1774            };
1775            let start = |id: ogeom_topo::PCurveId| -> OgeomResult<Option<ogeom_math::Point2>> {
1776                let Some(pc) = model.geometry().pcurve(id) else {
1777                    return Ok(None);
1778                };
1779                Ok(Some(pc.point_at(range.0, tol)?))
1780            };
1781            let (Some(a), Some(b)) = (start(*first)?, start(*second)?) else {
1782                continue;
1783            };
1784            let (forward, reversed) = if a.distance(arrived) <= b.distance(arrived) {
1785                (*first, *second)
1786            } else {
1787                (*second, *first)
1788            };
1789            let (range, at) = (*range, at.clone());
1790            let Some(node) = model.node_mut(occurrence) else {
1791                continue;
1792            };
1793            let ogeom_topo::NodeData::Edge(data) = node.data_mut() else {
1794                continue;
1795            };
1796            data.representations.retain(|r| {
1797                !matches!(r, EdgeRepr::PCurve { curve, surface: on, .. }
1798                    if *on == surface && (*curve == forward || *curve == reversed))
1799            });
1800            data.add(EdgeRepr::Seam {
1801                forward,
1802                reversed,
1803                surface,
1804                location: at,
1805                range,
1806            });
1807        }
1808    }
1809    Ok(())
1810}
1811
1812/// Where an edge occurrence ends in the chart of `surface`, walked as it
1813/// stands: its pcurve there (the column its orientation reads, where it is
1814/// a seam) at the end of its range it arrives at.
1815fn chart_end(
1816    model: &Model,
1817    occurrence: &Shape,
1818    surface: ogeom_topo::SurfaceId,
1819    tol: Tolerances,
1820) -> OgeomResult<Option<ogeom_math::Point2>> {
1821    use ogeom_geom::Curve2d as _;
1822    let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
1823        return Ok(None);
1824    };
1825    let reversed = occurrence.orientation() == ogeom_topo::Orientation::Reversed;
1826    let (id, range) = match data.pcurve_for(surface, occurrence.location()) {
1827        Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
1828        Some(EdgeRepr::Seam {
1829            forward,
1830            reversed: back,
1831            range,
1832            ..
1833        }) => (if reversed { *back } else { *forward }, *range),
1834        _ => return Ok(None),
1835    };
1836    let Some(pc) = model.geometry().pcurve(id) else {
1837        return Ok(None);
1838    };
1839    Ok(Some(
1840        pc.point_at(if reversed { range.0 } else { range.1 }, tol)?,
1841    ))
1842}
1843
1844/// How far the pcurves carried onto `survivor` leave its curve beyond its
1845/// stated tolerance, each lifted through its surface and measured against
1846/// the curve at the same fraction of the way along, or where that is past
1847/// the tolerance, against the nearest point of the curve's stretch, and
1848/// bounded between samples by [`widest_refined`]; `None` where they keep
1849/// within it.
1850///
1851/// [`widest_refined`]: crate::pcurve_gap::widest_refined
1852fn carried_off(
1853    model: &Model,
1854    survivor: &Shape,
1855    carried: &[EdgeRepr],
1856    tol: Tolerances,
1857) -> OgeomResult<Option<f64>> {
1858    let Some(data) = model.node(survivor).and_then(|n| n.data().as_edge()) else {
1859        ogeom_bail!(Dangling, "an edge is not in this model");
1860    };
1861    let Some(EdgeRepr::Curve3d {
1862        curve,
1863        range,
1864        location,
1865    }) = data.curve3d()
1866    else {
1867        return Ok(None);
1868    };
1869    let Some(curve) = model.geometry().curve(*curve) else {
1870        ogeom_bail!(Dangling, "curve is not in this model");
1871    };
1872    // The edge's own statement, not the confusion: a fuzzy sew's
1873    // confusion is its fuzz, and a pcurve standing off within it still
1874    // stands off the curve.
1875    let reach = data.tolerance.get();
1876    let mut widest: Option<f64> = None;
1877    for repr in carried {
1878        let (sides, prange, surface, at) = match repr {
1879            EdgeRepr::PCurve {
1880                curve,
1881                range,
1882                surface,
1883                location,
1884            } => ([Some(*curve), None], *range, *surface, location),
1885            EdgeRepr::Seam {
1886                forward,
1887                reversed,
1888                range,
1889                surface,
1890                location,
1891            } => (
1892                [Some(*forward), Some(*reversed)],
1893                *range,
1894                *surface,
1895                location,
1896            ),
1897            _ => continue,
1898        };
1899        if at != location {
1900            continue;
1901        }
1902        let Some(surface) = model.geometry().surface(surface) else {
1903            ogeom_bail!(Dangling, "surface is not in this model");
1904        };
1905        for id in sides.into_iter().flatten() {
1906            let Some(pcurve) = model.geometry().pcurve(id) else {
1907                ogeom_bail!(Dangling, "pcurve is not in this model");
1908            };
1909            let gap = |f: f64| -> OgeomResult<Option<ogeom_math::Vector>> {
1910                let on_curve = curve.point_at(range.0 + (range.1 - range.0) * f, tol)?;
1911                let uv = ogeom_geom::Curve2d::point_at(
1912                    pcurve,
1913                    prange.0 + (prange.1 - prange.0) * f,
1914                    tol,
1915                )?;
1916                let Ok(lifted) = ogeom_geom::Surface::point_at(surface, uv.x, uv.y, tol) else {
1917                    return Ok(None);
1918                };
1919                let along = lifted - on_curve;
1920                if along.magnitude() <= reach {
1921                    return Ok(Some(along));
1922                }
1923                let (foot, off) =
1924                    crate::pcurve_gap::nearest_point_on_stretch(curve, *range, lifted, tol)?;
1925                Ok(Some(if off < along.magnitude() {
1926                    lifted - curve.point_at(foot, tol)?
1927                } else {
1928                    along
1929                }))
1930            };
1931            let mut breaks = crate::pcurve_gap::curve_breaks(curve, *range);
1932            breaks.extend(crate::pcurve_gap::pcurve_breaks(pcurve, prange));
1933            let wide = (range.1 - range.0).abs().max((prange.1 - prange.0).abs());
1934            // The refinement stops within the confusion distance of the
1935            // widest, so it may read that much short; the edge's own measure
1936            // ([`crate::pcurve_gap::pcurve_gap`], the one every reader of the
1937            // edge holds it to) is taken too, and the wider stands.
1938            let off = crate::pcurve_gap::widest_refined(
1939                &gap,
1940                &breaks,
1941                reach,
1942                tol.confusion(),
1943                tol.parametric() / wide,
1944            )?
1945            .max(crate::pcurve_gap::pcurve_gap(
1946                (curve, *range),
1947                (pcurve, prange),
1948                surface,
1949                reach,
1950                tol,
1951            )?);
1952            if off > reach {
1953                widest = Some(widest.map_or(off, |w| w.max(off)));
1954            }
1955        }
1956    }
1957    Ok(widest)
1958}
1959
1960/// A parametric representation running the other way.
1961///
1962/// The consumers map a 3D-curve parameter onto the stored range
1963/// proportionally, so swapping the range's ends reverses the traversal
1964/// exactly, with no new geometry. A seam also swaps which pcurve is the
1965/// forward one, since "forward" is defined by the traversal that just
1966/// reversed.
1967fn reversed_repr(repr: EdgeRepr) -> EdgeRepr {
1968    match repr {
1969        EdgeRepr::PCurve {
1970            curve,
1971            surface,
1972            location,
1973            range,
1974        } => EdgeRepr::PCurve {
1975            curve,
1976            surface,
1977            location,
1978            range: (range.1, range.0),
1979        },
1980        EdgeRepr::Seam {
1981            forward,
1982            reversed,
1983            surface,
1984            location,
1985            range,
1986        } => EdgeRepr::Seam {
1987            forward: reversed,
1988            reversed: forward,
1989            surface,
1990            location,
1991            range: (range.1, range.0),
1992        },
1993        other => other,
1994    }
1995}
1996
1997/// Rebuild every edge whose bounding vertices were merged away.
1998///
1999/// An edge's bounds live in its node, so an edge cannot be pointed at a
2000/// different vertex; it has to be built again. Its data comes across whole,
2001/// representations included, so the new edge describes itself exactly as the
2002/// old one did and only its ends have changed.
2003fn rebuild_edges(
2004    model: &mut Model,
2005    faces: &[Shape],
2006    vertices: &FastMap<TShapeId, TShapeId>,
2007) -> OgeomResult<FastMap<TShapeId, TShapeId>> {
2008    let mut out = FastMap::default();
2009    if vertices.is_empty() {
2010        return Ok(out);
2011    }
2012    let mut done: FastSet<TShapeId> = FastSet::default();
2013    for face in faces {
2014        for edge in explore_unique(model, face, ShapeType::Edge)? {
2015            if !done.insert(edge.node()) {
2016                continue;
2017            }
2018
2019            let Some(node) = model.node(&edge) else {
2020                ogeom_bail!(Dangling, "edge is not in this model");
2021            };
2022            let bounds: Vec<Shape> = node.children().to_vec();
2023            if !bounds.iter().any(|b| vertices.contains_key(&b.node())) {
2024                continue;
2025            }
2026            let NodeData::Edge(data) = node.data().clone() else {
2027                continue;
2028            };
2029            let moved: Vec<Shape> = bounds
2030                .iter()
2031                .map(|b| match vertices.get(&b.node()) {
2032                    Some(kept) => Shape::new(*kept, b.location().clone(), b.orientation()),
2033                    None => b.clone(),
2034                })
2035                .collect();
2036            let fresh = model.add_edge(*data, &moved)?;
2037            out.insert(edge.node(), fresh.node());
2038        }
2039    }
2040    Ok(out)
2041}
2042
2043/// What decides whether two edges are the same edge.
2044#[derive(Debug, Clone)]
2045struct Fingerprint {
2046    start: Point,
2047    middle: Point,
2048    end: Point,
2049    /// The points a quarter and three quarters of the way along, which tell
2050    /// the sense of a closed edge whose ends are one point.
2051    quarters: [Point; 2],
2052    /// The edge's curve in space and the stretch it covers, for the middle
2053    /// of another edge to be asked whether it lies on this one.
2054    curve: ogeom_geom::Curve,
2055    range: (f64, f64),
2056    /// How far this edge's own stated tolerances let it stray: the widest of
2057    /// the edge's and its vertices'. An edge whose junction was welded across
2058    /// a recorded gap carries that gap here, and the comparison honours it:
2059    /// per-entity tolerances are the data model's, not a nicety of import.
2060    width: f64,
2061    /// How far each end may stray: the edge's own tolerance and that end's
2062    /// vertex's. A vertex widened where it was welded claims its span there
2063    /// and not at the edge's other end.
2064    ends: [f64; 2],
2065}
2066
2067impl Fingerprint {
2068    /// How far `p` sits from this edge's own stretch of its curve.
2069    fn off(&self, p: Point, tol: Tolerances) -> OgeomResult<f64> {
2070        // A line's or a circle's foot is had in closed form; a point on a
2071        // circle's axis, every angle as near, is left to the search.
2072        let closed = match &self.curve {
2073            ogeom_geom::Curve::Line(line) => {
2074                let t = ogeom_math::elementary::line_parameter(line.axis(), p);
2075                let at = ogeom_math::elementary::line_at(line.axis(), t).point;
2076                Some((t, p.distance(at)))
2077            }
2078            ogeom_geom::Curve::Circle(circle) => {
2079                match ogeom_math::elementary::circle_parameter(&circle.circle(), p, tol) {
2080                    Ok(angle) => {
2081                        let t = if circle.is_reversed() {
2082                            (-angle).rem_euclid(core::f64::consts::TAU)
2083                        } else {
2084                            angle
2085                        };
2086                        let at = ogeom_math::elementary::circle_at(&circle.circle(), angle).point;
2087                        Some((t, p.distance(at)))
2088                    }
2089                    Err(_) => None,
2090                }
2091            }
2092            _ => None,
2093        };
2094        let (parameter, distance) = match closed {
2095            Some(foot) => foot,
2096            None => {
2097                let foot = crate::project_on_curve(&self.curve, p, 64, tol)?;
2098                (foot.parameter, foot.distance)
2099            }
2100        };
2101        let (lo, hi) = (
2102            self.range.0.min(self.range.1),
2103            self.range.0.max(self.range.1),
2104        );
2105        let mut t = parameter;
2106        if self.curve.is_periodic() {
2107            let (dlo, dhi) = self.curve.domain();
2108            let period = dhi - dlo;
2109            if period > 0.0 {
2110                t = lo + (t - lo).rem_euclid(period);
2111            }
2112        }
2113        if t >= lo - tol.parametric() && t <= hi + tol.parametric() {
2114            return Ok(distance);
2115        }
2116        Ok(p.distance(self.start).min(p.distance(self.end)))
2117    }
2118
2119    /// Whether two edges coincide, and if so whether the second runs backwards.
2120    fn same_as(&self, other: &Self, tol: Tolerances) -> OgeomResult<Option<bool>> {
2121        let reach = tol.confusion().max(self.width).max(other.width);
2122        let near = |a: Point, b: Point| a.distance(b) <= reach;
2123        // Each pair of ends within what those two ends state: a vertex
2124        // widened where a section was welded into a junction claims that
2125        // span at the junction, and two edges of a sliver that meet there
2126        // stay apart at their other ends by however much they part.
2127        let meet =
2128            |a: Point, wa: f64, b: Point, wb: f64| a.distance(b) <= tol.confusion().max(wa).max(wb);
2129        let [self_start, self_end] = self.ends;
2130        let [other_start, other_end] = other.ends;
2131        // Ends first: they cost a distance each, and most candidates that
2132        // start near this edge end somewhere else. The middle is asked only
2133        // of a pair whose ends already agree.
2134        let along = meet(self.start, self_start, other.start, other_start)
2135            && meet(self.end, self_end, other.end, other_end);
2136        let against = meet(self.start, self_start, other.end, other_end)
2137            && meet(self.end, self_end, other.start, other_start);
2138        if !along && !against {
2139            return Ok(None);
2140        }
2141        // The midpoint is not a nicety. Two arcs between the same pair of
2142        // vertices (the two halves of a circle) agree at both ends and are
2143        // not the same edge, and merging them would fuse a shape to itself.
2144        // Two descriptions of one curve need not agree on where its middle
2145        // *parameter* falls (a fitted rim against the exact circle it
2146        // traces paces itself differently), so each middle is asked to lie
2147        // on the other's stretch instead, which the far half of a circle
2148        // still fails.
2149        if !near(self.middle, other.middle)
2150            && !(other.off(self.middle, tol)? <= reach && self.off(other.middle, tol)? <= reach)
2151        {
2152            if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2153                let foot = crate::project_on_curve(&other.curve, self.middle, 64, tol)?;
2154                eprintln!(
2155                    "SEW near miss: ends agree within {reach:.2e}, middles off {:.2e} / {:.2e} (widths {:.2e}, {:.2e}) at {:?}; foot on other at {:.6} (range {:?}, domain {:?}, periodic {}) distance {:.2e}",
2156                    other.off(self.middle, tol)?,
2157                    self.off(other.middle, tol)?,
2158                    self.width,
2159                    other.width,
2160                    self.middle,
2161                    foot.parameter,
2162                    other.range,
2163                    other.curve.domain(),
2164                    other.curve.is_periodic(),
2165                    foot.distance
2166                );
2167            }
2168            return Ok(None);
2169        }
2170        // A closed edge meets the other's ends both ways round; its sense is
2171        // the one whose quarter points lie the nearer.
2172        if along && against {
2173            let [q1, q3] = self.quarters;
2174            let [p1, p3] = other.quarters;
2175            let forward = q1.distance(p1) + q3.distance(p3);
2176            let backward = q1.distance(p3) + q3.distance(p1);
2177            return Ok(Some(backward < forward));
2178        }
2179        Ok(Some(!along))
2180    }
2181}
2182
2183/// Edge starts binned for the twin search. Two edges are one only when
2184/// each end of one meets an end of the other, so an edge's twin starts
2185/// within reach of one of its ends, and the edges binned near either end
2186/// are every edge that could match. A pair is compared within the wider
2187/// of its two edges' widths, so the few loose edges an import can carry
2188/// are binned apart: the tight edges' cells are sized by the tight edges,
2189/// and one loose edge does not coarsen the search for all of them.
2190struct StartBins {
2191    /// Edges no wider than `cell`.
2192    tight: Bins,
2193    cell: f64,
2194    /// Edges wider than `cell`, in cells sized to the widest.
2195    loose: Bins,
2196    widest: f64,
2197}
2198
2199impl StartBins {
2200    fn new(catalogue: &[(TShapeId, Fingerprint)], tol: Tolerances) -> Self {
2201        let mut widths: Vec<f64> = catalogue
2202            .iter()
2203            .map(|(_, print)| print.width.max(tol.confusion()))
2204            .collect();
2205        widths.sort_by(f64::total_cmp);
2206        let widest = widths.last().copied().unwrap_or(tol.confusion());
2207        // The width nine in ten edges keep within.
2208        let cell = widths
2209            .get(widths.len().saturating_sub(1) * 9 / 10)
2210            .copied()
2211            .unwrap_or(tol.confusion());
2212        let (mut tight, mut loose) = (Bins::new(cell), Bins::new(widest));
2213        for (index, (_, print)) in catalogue.iter().enumerate() {
2214            if print.width <= cell {
2215                tight.insert(print.start, index);
2216            } else {
2217                loose.insert(print.start, index);
2218            }
2219        }
2220        Self {
2221            tight,
2222            cell,
2223            loose,
2224            widest,
2225        }
2226    }
2227
2228    /// The edges after `i` whose start lies within reach of either of its
2229    /// ends: every edge that could be its twin, and perhaps some that are
2230    /// not, in order.
2231    fn twin_candidates(&self, catalogue: &[(TShapeId, Fingerprint)], i: usize) -> Vec<usize> {
2232        let print = &catalogue[i].1;
2233        // A tight edge is compared within this edge's width or its own,
2234        // which is at most `cell`; a loose one within at most the widest.
2235        let near_tight = self.cell.max(print.width);
2236        let found = [
2237            self.tight.near(print.start, near_tight),
2238            self.tight.near(print.end, near_tight),
2239            self.loose.near(print.start, self.widest),
2240            self.loose.near(print.end, self.widest),
2241        ];
2242        // A reach wider than the grid answers for is walked: every later
2243        // edge whose start lies within the widest reach of either end.
2244        if found.iter().any(Option::is_none) {
2245            let reach = self.widest.max(near_tight);
2246            return ((i + 1)..catalogue.len())
2247                .filter(|&j| {
2248                    let start = catalogue[j].1.start;
2249                    start.distance(print.start) <= reach || start.distance(print.end) <= reach
2250                })
2251                .collect();
2252        }
2253        let mut out: Vec<usize> = found
2254            .into_iter()
2255            .flatten()
2256            .flatten()
2257            .filter(|&j| j > i)
2258            .collect();
2259        out.sort_unstable();
2260        out.dedup();
2261        out
2262    }
2263}
2264
2265/// An edge's ends and midpoint, in space.
2266fn fingerprint(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Option<Fingerprint>> {
2267    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
2268        return Ok(None);
2269    };
2270    let Some(EdgeRepr::Curve3d {
2271        curve,
2272        range,
2273        location,
2274    }) = data.curve3d()
2275    else {
2276        // A degenerate edge has no curve and no length; there is nothing about
2277        // it that could match another edge's geometry.
2278        return Ok(None);
2279    };
2280    let Some(geometry) = model.geometry().curve(*curve) else {
2281        ogeom_bail!(Dangling, "curve is not in this model");
2282    };
2283    // The curve sits where its own location puts it, inside wherever the
2284    // edge is placed: a prism's far end edges are its profile's curves
2285    // carried along it by a location, not new curves.
2286    let own = location.composed(model.datums())?;
2287    let outer = edge.transform(model.datums())?;
2288    let own_width = data.tolerance.get();
2289    let bounds = model.children_of(edge)?;
2290    let reach_of = |vertex: Option<&Shape>| {
2291        vertex
2292            .and_then(|v| model.node(v))
2293            .and_then(|n| n.data().as_vertex())
2294            .map_or(own_width, |v| own_width.max(v.tolerance.get()))
2295    };
2296    let ends = [reach_of(bounds.first()), reach_of(bounds.last())];
2297    let mut width = own_width;
2298    for vertex in &bounds {
2299        if let Some(v) = model.node(vertex).and_then(|n| n.data().as_vertex()) {
2300            width = width.max(v.tolerance.get());
2301        }
2302    }
2303    use ogeom_geom::Transformable as _;
2304    let placement = |p: Point| outer.apply(own.apply(p));
2305    Ok(Some(Fingerprint {
2306        start: placement(geometry.point_at(range.0, tol)?),
2307        middle: placement(geometry.point_at(f64::midpoint(range.0, range.1), tol)?),
2308        end: placement(geometry.point_at(range.1, tol)?),
2309        quarters: [
2310            placement(geometry.point_at(range.0 + 0.25 * (range.1 - range.0), tol)?),
2311            placement(geometry.point_at(range.0 + 0.75 * (range.1 - range.0), tol)?),
2312        ],
2313        curve: geometry
2314            .clone()
2315            .transformed(&own, tol)?
2316            .transformed(&outer, tol)?,
2317        range: *range,
2318        width,
2319        ends,
2320    }))
2321}
2322
2323/// A vertex's position in space.
2324fn placed(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
2325    let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
2326        ogeom_bail!(Construction, "expected a vertex");
2327    };
2328    Ok(vertex.transform(model.datums())?.apply(data.point))
2329}
2330
2331/// Rebuild a face with merged edges in place of the ones they replaced.
2332fn rebuild_face(
2333    model: &mut Model,
2334    face: &Shape,
2335    merged: &FastMap<TShapeId, (TShapeId, bool)>,
2336    collapsed: &FastSet<TShapeId>,
2337    tol: Tolerances,
2338) -> OgeomResult<Option<(Shape, bool)>> {
2339    let mut whole = true;
2340    let Some(data) = model.node(face).and_then(|n| n.data().as_face()).cloned() else {
2341        ogeom_bail!(Construction, "expected a face");
2342    };
2343    // Nothing to substitute: the face is already built on the shared edges, and
2344    // rebuilding it would only mint a node identical to the one there.
2345    let mut touched = false;
2346
2347    // The wires are read as the face stores them, its own sense left out:
2348    // the rebuilt face takes that sense back at the end, and reading them
2349    // under it as well would turn every ring of a reversed face around.
2350    let stored = face.oriented(Orientation::Forward);
2351    let mut wires = Vec::new();
2352    for wire in model.ordered_children_of(&stored)? {
2353        let mut ring = Vec::new();
2354        let mut shrunk = false;
2355        for edge in model.ordered_children_of(&wire)? {
2356            match merged.get(&edge.node()) {
2357                Some((kept, flipped)) => {
2358                    touched = true;
2359                    if collapsed.contains(kept) {
2360                        shrunk = true;
2361                        continue;
2362                    }
2363                    let mut replacement =
2364                        Shape::new(*kept, edge.location().clone(), edge.orientation());
2365                    if *flipped {
2366                        replacement = replacement.reversed();
2367                    }
2368                    ring.push(replacement);
2369                }
2370                None => ring.push(edge),
2371            }
2372        }
2373        // A ring that merging collapsed onto one edge, walked out and back,
2374        // bounds no area: the sliver between two coincident strands that the
2375        // sew has just found to be one edge. So does one that, the edges
2376        // shrunk to a point left out, walks each of its edges once each way:
2377        // a sliver whose sides were found to be one edge piece by piece.
2378        // Such a ring is dropped, and a face whose outer ring it was with it.
2379        let one_edge = !ring.is_empty() && ring.iter().all(|e| e.node() == ring[0].node());
2380        let out_and_back = shrunk
2381            && ring.iter().all(|e| {
2382                let walks = |o: Orientation| {
2383                    ring.iter()
2384                        .filter(|f| f.node() == e.node() && f.orientation() == o)
2385                        .count()
2386                };
2387                walks(Orientation::Forward) == 1 && walks(Orientation::Reversed) == 1
2388            });
2389        if (one_edge && ring.len() >= 2) || out_and_back {
2390            if wires.is_empty() {
2391                return Ok(None);
2392            }
2393            whole = false;
2394            continue;
2395        }
2396        let wire = match make_wire(model, &ring, tol) {
2397            Ok(w) => w.shape,
2398            Err(e) => {
2399                if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2400                    eprintln!("SEW WIRE FAIL: {e}");
2401                    for edge in &ring {
2402                        if let Some((a, b)) = edge_vertices(model, edge)? {
2403                            let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
2404                            let fp = fingerprint(model, edge, tol)?;
2405                            eprintln!(
2406                                "   edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{} width {:.2e}",
2407                                edge.node(),
2408                                if edge.orientation() == ogeom_topo::Orientation::Reversed {
2409                                    " rev"
2410                                } else {
2411                                    ""
2412                                },
2413                                pa.x,
2414                                pa.y,
2415                                pa.z,
2416                                a.node().index(),
2417                                pb.x,
2418                                pb.y,
2419                                pb.z,
2420                                b.node().index(),
2421                                fp.map_or(0.0, |f| f.width)
2422                            );
2423                        }
2424                    }
2425                }
2426                return Err(e);
2427            }
2428        };
2429        wires.push(wire);
2430    }
2431    if !touched {
2432        return Ok(Some((face.clone(), whole)));
2433    }
2434    if wires.is_empty() {
2435        return Ok(None);
2436    }
2437    let sewn = match make_face_on(model, data.surface, &wires, tol) {
2438        Ok(built) => built.shape,
2439        Err(e) => {
2440            if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2441                eprintln!("SEW FACE FAIL: {e}");
2442                for (wi, wire) in wires.iter().enumerate() {
2443                    for edge in model.ordered_children_of(wire)? {
2444                        if let Some((a, b)) = edge_vertices(model, &edge)? {
2445                            let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
2446                            eprintln!(
2447                                "   wire {wi} edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{}",
2448                                edge.node(),
2449                                if edge.orientation() == ogeom_topo::Orientation::Reversed {
2450                                    " rev"
2451                                } else {
2452                                    ""
2453                                },
2454                                pa.x,
2455                                pa.y,
2456                                pa.z,
2457                                a.node().index(),
2458                                pb.x,
2459                                pb.y,
2460                                pb.z,
2461                                b.node().index()
2462                            );
2463                        }
2464                    }
2465                }
2466            }
2467            return Err(e);
2468        }
2469    };
2470    Ok(Some((
2471        if face.orientation() == Orientation::Reversed {
2472            sewn.reversed()
2473        } else {
2474            sewn
2475        },
2476        whole,
2477    )))
2478}
2479
2480/// How many times the faces use each edge, and how many of those uses walk
2481/// it forward.
2482fn walks<'a>(
2483    model: &Model,
2484    faces: impl Iterator<Item = &'a Shape>,
2485) -> OgeomResult<FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)>> {
2486    let mut uses: FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)> = FastMap::default();
2487    for face in faces {
2488        for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
2489            if model
2490                .node(&edge)
2491                .and_then(|n| n.data().as_edge())
2492                .is_some_and(|d| d.degenerate)
2493            {
2494                continue;
2495            }
2496            let walk = uses
2497                .entry((edge.node(), edge.location().clone()))
2498                .or_default();
2499            walk.0 += 1;
2500            if edge.orientation() == Orientation::Forward {
2501                walk.1 += 1;
2502            }
2503        }
2504    }
2505    Ok(uses)
2506}
2507
2508/// Whether every edge used by two faces is walked once each way.
2509fn walked_each_way(uses: &FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)>) -> bool {
2510    uses.values()
2511        .all(|(count, forward)| *count != 2 || *forward == 1)
2512}
2513
2514/// Group faces by whether they share an edge, transitively.
2515fn connected_groups(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<usize>>> {
2516    let mut group_of: Vec<usize> = (0..faces.len()).collect();
2517    let mut edges_of = Vec::with_capacity(faces.len());
2518    for face in faces {
2519        edges_of.push(
2520            explore_unique(model, face, ShapeType::Edge)?
2521                .into_iter()
2522                .map(|e| e.node())
2523                .collect::<Vec<_>>(),
2524        );
2525    }
2526
2527    // Union-find: each face joins the first face seen with each of its
2528    // edges, which joins it to every face sharing that edge transitively.
2529    let mut first_user: FastMap<TShapeId, usize> = FastMap::default();
2530    for (i, edges) in edges_of.iter().enumerate() {
2531        for edge in edges {
2532            let j = *first_user.entry(*edge).or_insert(i);
2533            let (a, b) = (find(&mut group_of, j), find(&mut group_of, i));
2534            if a != b {
2535                group_of[b] = a;
2536            }
2537        }
2538    }
2539
2540    let mut groups: FastMap<usize, Vec<usize>> = FastMap::default();
2541    for i in 0..faces.len() {
2542        groups.entry(find(&mut group_of, i)).or_default().push(i);
2543    }
2544    let mut out: Vec<Vec<usize>> = groups.into_values().collect();
2545    // Deterministic: a result whose shells come back in a different order each
2546    // run is one nobody can compare against.
2547    out.sort_by_key(|group| group.first().map(|&i| faces[i].node()));
2548    Ok(out)
2549}
2550
2551/// Follow a union-find chain to its root.
2552fn find(parent: &mut [usize], mut i: usize) -> usize {
2553    // Halving the path on the way up keeps every chain short.
2554    while parent[i] != i {
2555        parent[i] = parent[parent[i]];
2556        i = parent[i];
2557    }
2558    i
2559}
2560
2561/// Edges still used by exactly one face.
2562fn free_edges(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Shape>> {
2563    let mut uses: FastMap<TShapeId, (usize, Shape)> = FastMap::default();
2564    for face in faces {
2565        for wire in model.children_of(face)? {
2566            for edge in model.children_of(&wire)? {
2567                if model
2568                    .node(&edge)
2569                    .and_then(|n| n.data().as_edge())
2570                    .is_some_and(|d| d.degenerate)
2571                {
2572                    continue;
2573                }
2574                let entry = uses.entry(edge.node()).or_insert((0, edge.clone()));
2575                entry.0 += 1;
2576            }
2577        }
2578    }
2579    let mut out: Vec<Shape> = uses
2580        .into_values()
2581        .filter(|(count, _)| count % 2 == 1)
2582        .map(|(_, edge)| edge)
2583        .collect();
2584    out.sort_by_key(Shape::node);
2585    Ok(out)
2586}
2587
2588#[cfg(test)]
2589#[allow(clippy::unwrap_used, clippy::expect_used)]
2590mod tests {
2591    use super::*;
2592    use crate::{check_tessellation, is_shell_closed, make_box, make_polygon};
2593    use ogeom_geom::PlaneSurface;
2594    use ogeom_math::{Frame, Plane, Vector};
2595    use ogeom_topo::Location;
2596
2597    const T: Tolerances = Tolerances::millimetres();
2598
2599    fn fine() -> ogeom_mesh::Deflection {
2600        ogeom_mesh::Deflection {
2601            chord: 0.02,
2602            ..ogeom_mesh::Deflection::default()
2603        }
2604    }
2605
2606    /// A flat face on `corners`, built from its own fresh edges so it shares
2607    /// nothing with anything else.
2608    fn loose_polygon<const N: usize>(model: &mut Model, corners: [Point; N]) -> Shape {
2609        let wire = make_polygon(model, &corners, true, T).unwrap().shape;
2610        let normal =
2611            ogeom_math::Direction::from_cross(corners[1] - corners[0], corners[2] - corners[1], T)
2612                .unwrap();
2613        let frame = ogeom_math::Frame::new(
2614            corners[0],
2615            normal,
2616            ogeom_math::Direction::new(corners[1] - corners[0], T).unwrap(),
2617            T,
2618        )
2619        .unwrap();
2620        let surface = model
2621            .geometry_mut()
2622            .add_surface(PlaneSurface::new(Plane::new(frame)).into());
2623        for edge in model.children_of(&wire).unwrap() {
2624            let (a, b) = crate::edge_vertices(model, &edge).unwrap().unwrap();
2625            let (pa, pb) = (placed(model, &a).unwrap(), placed(model, &b).unwrap());
2626            let flat = |p: Point| {
2627                let l = frame.to_local(p);
2628                ogeom_math::Point2::new(l.x, l.y)
2629            };
2630            crate::attach_pcurve(
2631                model,
2632                &edge,
2633                ogeom_geom::Line2d::segment(flat(pa), flat(pb), T)
2634                    .unwrap()
2635                    .into(),
2636                surface,
2637                Location::identity(),
2638                (0.0, pa.distance(pb)),
2639            )
2640            .unwrap();
2641        }
2642        crate::make_face_on(model, surface, std::slice::from_ref(&wire), T)
2643            .unwrap()
2644            .shape
2645    }
2646
2647    /// Two unit squares side by side, the second a twentieth of a
2648    /// millimetre off: sewn across a tenth they are one shell of one joined
2649    /// pair with nothing broken; across a hundredth they stay apart.
2650    #[test]
2651    fn squares_a_little_apart_sew_across_the_gap_asked_for() {
2652        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2653        for (gap, shells, joined) in [(0.1, 1, 1), (0.01, 2, 0)] {
2654            let mut model = Model::new();
2655            let first = loose_polygon(
2656                &mut model,
2657                [at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
2658            );
2659            let second = loose_polygon(
2660                &mut model,
2661                [at(1.05, 0.0), at(2.05, 0.0), at(2.05, 1.0), at(1.05, 1.0)],
2662            );
2663            let sewn = sew_within(&mut model, &[first, second], gap, T).unwrap();
2664            assert_eq!(sewn.shells.len(), shells, "gap {gap}");
2665            assert_eq!(sewn.joined, joined, "gap {gap}");
2666            if shells == 1 {
2667                let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2668                assert!(d.is_usable(), "{d}");
2669            }
2670        }
2671    }
2672
2673    /// A strip a twentieth wide between two unit squares, its sides each in
2674    /// two pieces and its end standing on a third square's side, all four
2675    /// loose, sewn across a tenth: the strip's end shrinks to a point and
2676    /// its sides are found to be the squares' edges piece by piece, so it
2677    /// bounds nothing and goes, and the third square walks no edge where
2678    /// the strip's end was. Kept, the strip walked each of its sides out
2679    /// and back, and the edge shrunk to a point.
2680    #[test]
2681    fn a_strip_narrower_than_the_gap_between_faces_goes() {
2682        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2683        let mut model = Model::new();
2684        let below = loose_polygon(
2685            &mut model,
2686            [
2687                at(1.0, -1.0),
2688                at(1.0, 0.0),
2689                at(0.5, 0.0),
2690                at(0.0, 0.0),
2691                at(0.0, -1.0),
2692            ],
2693        );
2694        let strip = loose_polygon(
2695            &mut model,
2696            [
2697                at(1.0, 0.0),
2698                at(1.0, 0.05),
2699                at(0.5, 0.05),
2700                at(0.0, 0.05),
2701                at(0.0, 0.0),
2702                at(0.5, 0.0),
2703            ],
2704        );
2705        let above = loose_polygon(
2706            &mut model,
2707            [
2708                at(1.0, 0.05),
2709                at(1.0, 1.05),
2710                at(0.0, 1.05),
2711                at(0.0, 0.05),
2712                at(0.5, 0.05),
2713            ],
2714        );
2715        let beside = loose_polygon(
2716            &mut model,
2717            [
2718                at(2.0, -1.0),
2719                at(2.0, 1.05),
2720                at(1.0, 1.05),
2721                at(1.0, 0.05),
2722                at(1.0, 0.0),
2723                at(1.0, -1.0),
2724            ],
2725        );
2726        let sewn = sew_within(&mut model, &[below, strip, above, beside], 0.1, T).unwrap();
2727        assert_eq!(sewn.shells.len(), 1);
2728        let shell = &sewn.shells[0];
2729        let faces = explore_unique(&model, shell, ShapeType::Face).unwrap();
2730        assert_eq!(faces.len(), 3);
2731        for face in &faces {
2732            for edge in explore_unique(&model, face, ShapeType::Edge).unwrap() {
2733                let (a, b) = crate::edge_vertices(&model, &edge).unwrap().unwrap();
2734                assert!(!a.is_same(&b), "an edge shrunk to a point is walked");
2735            }
2736        }
2737        let d = crate::check(&model, shell, T).unwrap();
2738        assert!(d.is_usable(), "{d}");
2739    }
2740
2741    /// A unit square whose top is an arc bowing a twentieth of a millimetre
2742    /// up, and the square above it on the chord: sewn across a tenth, arc
2743    /// and chord are one edge, which states how far the chord stands off
2744    /// the arc, and the vertices it ends on reach as far.
2745    #[test]
2746    fn an_edge_sewn_across_a_bow_widens_its_vertices_with_it() {
2747        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2748        let mut model = Model::new();
2749        let circle =
2750            ogeom_math::Circle::through(at(1.0, 0.0), at(0.5, 0.05), at(0.0, 0.0), T).unwrap();
2751        let param = |p: Point| ogeom_math::elementary::circle_parameter(&circle, p, T).unwrap();
2752        let (from, middle) = (param(at(1.0, 0.0)), param(at(0.5, 0.05)));
2753        let to = param(at(0.0, 0.0));
2754        let span = |a: f64, b: f64| (b - a).rem_euclid(core::f64::consts::TAU);
2755        let (lo, hi) = if span(from, middle) < span(from, to) {
2756            (from, from + span(from, to))
2757        } else {
2758            (to, to + span(to, from))
2759        };
2760        let corners = [at(0.0, -1.0), at(1.0, -1.0), at(1.0, 0.0), at(0.0, 0.0)];
2761        let vertices: Vec<Shape> = corners
2762            .iter()
2763            .map(|&p| crate::make_vertex(&mut model, p).shape)
2764            .collect();
2765        let mut edges = Vec::new();
2766        for k in [0, 1, 3] {
2767            let (a, b) = (corners[k], corners[(k + 1) % 4]);
2768            let line = ogeom_geom::LineCurve::segment(a, b, T).unwrap();
2769            edges.push(
2770                crate::make_edge_between(
2771                    &mut model,
2772                    line.into(),
2773                    (0.0, a.distance(b)),
2774                    &vertices[k],
2775                    &vertices[(k + 1) % 4],
2776                    T,
2777                )
2778                .unwrap()
2779                .shape,
2780            );
2781        }
2782        let (start, end) = if lo == from { (2, 3) } else { (3, 2) };
2783        let arc = crate::make_edge_between(
2784            &mut model,
2785            ogeom_geom::CircleCurve::new(circle).into(),
2786            (lo, hi),
2787            &vertices[start],
2788            &vertices[end],
2789            T,
2790        )
2791        .unwrap()
2792        .shape;
2793        edges.insert(2, if start == 2 { arc } else { arc.reversed() });
2794        let plane = || PlaneSurface::new(Plane::new(Frame::WORLD)).into();
2795        let below = crate::make_face_with_pcurves(&mut model, plane(), &[edges], T)
2796            .unwrap()
2797            .shape;
2798        let wire = make_polygon(
2799            &mut model,
2800            &[at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
2801            true,
2802            T,
2803        )
2804        .unwrap()
2805        .shape;
2806        let ring = model.ordered_children_of(&wire).unwrap();
2807        let above = crate::make_face_with_pcurves(&mut model, plane(), &[ring], T)
2808            .unwrap()
2809            .shape;
2810        let sewn = sew_within(&mut model, &[below, above], 0.1, T).unwrap();
2811        assert_eq!((sewn.shells.len(), sewn.joined), (1, 1));
2812        let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2813        assert!(d.is_usable(), "{d}");
2814    }
2815
2816    /// A 10 mm edge against two 5 mm edges along it: the long edge is split
2817    /// where the short ones meet, and the three faces sew into one shell.
2818    #[test]
2819    fn a_long_edge_against_two_short_ones_is_split_and_sewn() {
2820        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2821        let mut model = Model::new();
2822        let long = loose_polygon(
2823            &mut model,
2824            [at(0.0, 0.0), at(10.0, 0.0), at(10.0, 10.0), at(0.0, 10.0)],
2825        );
2826        let left = loose_polygon(
2827            &mut model,
2828            [at(0.0, -5.0), at(5.0, -5.0), at(5.0, 0.0), at(0.0, 0.0)],
2829        );
2830        let right = loose_polygon(
2831            &mut model,
2832            [at(5.0, -5.0), at(10.0, -5.0), at(10.0, 0.0), at(5.0, 0.0)],
2833        );
2834        let sewn = sew_within(&mut model, &[long, left, right], 1e-3, T).unwrap();
2835        assert_eq!(sewn.shells.len(), 1);
2836        assert_eq!(sewn.joined, 3);
2837        let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2838        assert!(d.is_usable(), "{d}");
2839    }
2840
2841    /// A circular segment whose arc runs past the top of its circle, beside
2842    /// two annular pieces whose inner arcs meet right at that top. The
2843    /// segment's arc is split there and sewn to both, though its default
2844    /// polyline cuts under the top and leaves the vertex outside its box.
2845    #[test]
2846    fn an_arc_is_split_at_a_vertex_on_its_bulge() {
2847        use ogeom_geom::{CircleCurve, Curve, LineCurve, SurfaceGeometry};
2848        let mut model = Model::new();
2849        let turn = 130_f64.to_radians();
2850        let circle = |r: f64| -> Curve {
2851            CircleCurve::new(ogeom_math::Circle::new(Frame::WORLD, r, T).unwrap()).into()
2852        };
2853        let on = |r: f64, a: f64| Point::new(r * a.cos(), r * a.sin(), 0.0);
2854        // A ring through the corners `(radius, angle)` in turn, each edge
2855        // the arc about the origin where the radius holds and the line
2856        // across where it changes.
2857        let ring = |model: &mut Model, corners: &[(f64, f64)]| -> Vec<Shape> {
2858            let vertices: Vec<Shape> = corners
2859                .iter()
2860                .map(|&(r, a)| crate::make_vertex(model, on(r, a)).shape)
2861                .collect();
2862            let n = corners.len();
2863            (0..n)
2864                .map(|k| {
2865                    let ((r, a), (_, b)) = (corners[k], corners[(k + 1) % n]);
2866                    let (from, to) = (&vertices[k], &vertices[(k + 1) % n]);
2867                    if (corners[(k + 1) % n].0 - r).abs() > 0.0 {
2868                        let curve =
2869                            LineCurve::segment(on(r, a), on(corners[(k + 1) % n].0, b), T).unwrap();
2870                        let range = ogeom_geom::Curve3d::domain(&curve);
2871                        crate::make_edge_between(model, Curve::Line(curve), range, from, to, T)
2872                            .unwrap()
2873                            .shape
2874                    } else if a < b {
2875                        crate::make_edge_between(model, circle(r), (a, b), from, to, T)
2876                            .unwrap()
2877                            .shape
2878                    } else {
2879                        crate::make_edge_between(model, circle(r), (b, a), to, from, T)
2880                            .unwrap()
2881                            .shape
2882                            .reversed()
2883                    }
2884                })
2885                .collect()
2886        };
2887        let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
2888        let quarter = core::f64::consts::FRAC_PI_2;
2889        // The segment's chord is a line between two corners on one circle.
2890        let segment = {
2891            let a = crate::make_vertex(&mut model, on(10.0, 0.0)).shape;
2892            let b = crate::make_vertex(&mut model, on(10.0, turn)).shape;
2893            let chord = LineCurve::segment(on(10.0, turn), on(10.0, 0.0), T).unwrap();
2894            let range = ogeom_geom::Curve3d::domain(&chord);
2895            vec![
2896                crate::make_edge_between(&mut model, circle(10.0), (0.0, turn), &a, &b, T)
2897                    .unwrap()
2898                    .shape,
2899                crate::make_edge_between(&mut model, Curve::Line(chord), range, &b, &a, T)
2900                    .unwrap()
2901                    .shape,
2902            ]
2903        };
2904        let first = ring(
2905            &mut model,
2906            &[(10.0, 0.0), (12.0, 0.0), (12.0, quarter), (10.0, quarter)],
2907        );
2908        let second = ring(
2909            &mut model,
2910            &[(10.0, quarter), (12.0, quarter), (12.0, turn), (10.0, turn)],
2911        );
2912        let faces: Vec<Shape> = [segment, first, second]
2913            .into_iter()
2914            .map(|ring| {
2915                crate::make_face_with_pcurves(&mut model, plane.clone(), &[ring], T)
2916                    .unwrap()
2917                    .shape
2918            })
2919            .collect();
2920        let sewn = sew_within(&mut model, &faces, 1e-6, T).unwrap();
2921        assert_eq!(sewn.shells.len(), 1);
2922        // The segment's arc against both inner arcs, and the two pieces'
2923        // lines between them.
2924        assert_eq!(sewn.joined, 3);
2925    }
2926
2927    /// A prism's walls with a lid built apart at each end: the walls' end
2928    /// edges are the profile's edges, the top ones carried up the prism,
2929    /// and both lids sew on, closing the box.
2930    #[test]
2931    fn a_prism_s_walls_sew_to_lids_at_both_ends() {
2932        let mut model = Model::new();
2933        let at = |x: f64, y: f64, z: f64| Point::new(x, y, z);
2934        let profile = make_polygon(
2935            &mut model,
2936            &[
2937                at(0.0, 0.0, 0.0),
2938                at(10.0, 0.0, 0.0),
2939                at(10.0, 10.0, 0.0),
2940                at(0.0, 10.0, 0.0),
2941            ],
2942            true,
2943            T,
2944        )
2945        .unwrap()
2946        .shape;
2947        let walls = crate::make_prism(&mut model, &profile, Vector::new(0.0, 0.0, 5.0), T)
2948            .unwrap()
2949            .shape;
2950        let mut faces = explore_unique(&model, &walls, ShapeType::Face).unwrap();
2951        faces.push(loose_polygon(
2952            &mut model,
2953            [
2954                at(0.0, 0.0, 0.0),
2955                at(0.0, 10.0, 0.0),
2956                at(10.0, 10.0, 0.0),
2957                at(10.0, 0.0, 0.0),
2958            ],
2959        ));
2960        faces.push(loose_polygon(
2961            &mut model,
2962            [
2963                at(0.0, 0.0, 5.0),
2964                at(10.0, 0.0, 5.0),
2965                at(10.0, 10.0, 5.0),
2966                at(0.0, 10.0, 5.0),
2967            ],
2968        ));
2969        let sewn = sew(&mut model, &faces, T).unwrap();
2970        assert_eq!(sewn.shells.len(), 1);
2971        assert_eq!(sewn.joined, 8);
2972        assert!(sewn.free_edges.is_empty());
2973        assert!(is_shell_closed(&model, &sewn.shells[0]).unwrap());
2974        let solid = crate::make_solid(&mut model, &sewn.shells).unwrap().shape;
2975        let volume = crate::volume_properties(&model, &solid, fine(), T)
2976            .unwrap()
2977            .mass;
2978        assert!((volume.abs() - 500.0).abs() < 1e-6, "{volume}");
2979    }
2980
2981    /// A 10 x 10 x 5 box's six faces: a prism's four walls, turned to face
2982    /// in where `walls_in`, and two lids built apart, the bottom one wound
2983    /// to face up into the box where `bottom_in` and down out of it
2984    /// otherwise; the top one faces up.
2985    fn box_faces(model: &mut Model, walls_in: bool, bottom_in: bool) -> Vec<Shape> {
2986        box_faces_in(model, walls_in, bottom_in, Frame::WORLD)
2987    }
2988
2989    /// [`box_faces`] laid out in `frame`.
2990    fn box_faces_in(
2991        model: &mut Model,
2992        walls_in: bool,
2993        bottom_in: bool,
2994        frame: Frame,
2995    ) -> Vec<Shape> {
2996        let at = |x: f64, y: f64, z: f64| {
2997            frame.origin()
2998                + frame.x().vector() * x
2999                + frame.y().vector() * y
3000                + frame.z().vector() * z
3001        };
3002        let square = |z: f64| {
3003            [
3004                at(0.0, 0.0, z),
3005                at(10.0, 0.0, z),
3006                at(10.0, 10.0, z),
3007                at(0.0, 10.0, z),
3008            ]
3009        };
3010        let profile = make_polygon(model, &square(0.0), true, T).unwrap().shape;
3011        let walls = crate::make_prism(model, &profile, frame.z().vector() * 5.0, T)
3012            .unwrap()
3013            .shape;
3014        let mut faces: Vec<Shape> = explore_unique(model, &walls, ShapeType::Face)
3015            .unwrap()
3016            .into_iter()
3017            .map(|f| if walls_in { f.reversed() } else { f })
3018            .collect();
3019        let mut bottom = square(0.0);
3020        if !bottom_in {
3021            bottom.reverse();
3022        }
3023        faces.push(loose_polygon(model, bottom));
3024        faces.push(loose_polygon(model, square(5.0)));
3025        faces
3026    }
3027
3028    /// The solid a sewn closed shell bounds, checked valid with no face
3029    /// facing into it, and its volume.
3030    fn solid_volume(model: &mut Model, shell: &Shape) -> f64 {
3031        let solid = crate::make_solid(model, std::slice::from_ref(shell))
3032            .unwrap()
3033            .shape;
3034        let d = crate::check(model, &solid, T).unwrap();
3035        assert!(d.is_valid(), "{d}");
3036        assert!(
3037            crate::inside_out_faces(model, &solid, T)
3038                .unwrap()
3039                .is_empty()
3040        );
3041        crate::volume_properties(model, &solid, ogeom_mesh::Deflection::default(), T)
3042            .unwrap()
3043            .mass
3044    }
3045
3046    /// Both lids of a box filled the same way round, so the bottom one
3047    /// faces into the box: sewn, that lid is turned over, every edge is
3048    /// walked once each way and the box measures its volume.
3049    #[test]
3050    fn a_lid_facing_into_the_box_is_turned_over_when_sewn() {
3051        let mut model = Model::new();
3052        let faces = box_faces(&mut model, false, true);
3053        let sewn = sew(&mut model, &faces, T).unwrap();
3054        assert!(sewn.free_edges.is_empty());
3055        assert_eq!(
3056            crate::check::edges_walked_one_way(&model, &sewn.shells[0]),
3057            0
3058        );
3059        let image = sewn.history.modified(&faces[4]);
3060        assert_eq!(image.len(), 1);
3061        assert_ne!(image[0].orientation(), faces[4].orientation());
3062        let volume = solid_volume(&mut model, &sewn.shells[0]);
3063        assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3064    }
3065
3066    /// A box whose walls face in and whose lids face out: the lids are
3067    /// turned to agree with the walls, and the shell, then facing in as a
3068    /// whole, is turned over to face out.
3069    #[test]
3070    fn a_closed_shell_whose_faces_disagree_is_turned_to_face_out() {
3071        let mut model = Model::new();
3072        let faces = box_faces(&mut model, true, false);
3073        let sewn = sew(&mut model, &faces, T).unwrap();
3074        for face in &faces[..4] {
3075            assert_ne!(
3076                sewn.history.modified(face)[0].orientation(),
3077                face.orientation()
3078            );
3079        }
3080        for face in &faces[4..] {
3081            assert!(sewn.history.copy_of(face).is_some());
3082        }
3083        let volume = solid_volume(&mut model, &sewn.shells[0]);
3084        assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3085    }
3086
3087    /// The box whose walls face in and whose lids face out, up to 2e7 from
3088    /// the origin: which way the sewn shell faces is read off the volume it
3089    /// encloses, a sum the box's own size, so the walls are turned there as
3090    /// at the origin and the shell faces out.
3091    #[test]
3092    fn a_closed_shell_far_from_the_origin_whose_faces_disagree_is_turned_to_face_out() {
3093        for o in [0.0, 1e6, 1e7, 2e7] {
3094            let mut model = Model::new();
3095            let n = ogeom_math::Direction::new(Vector::new(1.0, 1.0, 1.0), T).unwrap();
3096            let frame = Frame::new(Point::new(o, -o, o), n, ogeom_math::Direction::X, T).unwrap();
3097            let faces = box_faces_in(&mut model, true, false, frame);
3098            let sewn = sew(&mut model, &faces, T).unwrap();
3099            for face in &faces[..4] {
3100                assert_ne!(
3101                    sewn.history.modified(face)[0].orientation(),
3102                    face.orientation(),
3103                    "o {o:e}: a wall facing in is turned"
3104                );
3105            }
3106            let mesh = ogeom_mesh::triangulate(
3107                &model,
3108                &sewn.shells[0],
3109                ogeom_mesh::Deflection::default(),
3110                T,
3111            )
3112            .unwrap();
3113            let volume = mesh.volume();
3114            assert!((volume - 500.0).abs() < 1e-6, "o {o:e}: {volume}");
3115        }
3116    }
3117
3118    /// A box whose six faces all face in, so every edge is already walked
3119    /// once each way: sewing turns nothing (the shell may bound a void),
3120    /// and the solid made of it faces out.
3121    #[test]
3122    fn a_closed_shell_facing_in_throughout_makes_a_solid_facing_out() {
3123        let mut model = Model::new();
3124        let mut faces = box_faces(&mut model, true, true);
3125        faces[5] = faces[5].reversed();
3126        let sewn = sew(&mut model, &faces, T).unwrap();
3127        assert!(sewn.free_edges.is_empty());
3128        for face in &faces {
3129            let image = sewn
3130                .history
3131                .copy_of(face)
3132                .cloned()
3133                .unwrap_or_else(|| sewn.history.modified(face)[0].clone());
3134            assert_eq!(image.orientation(), face.orientation());
3135        }
3136        let volume = solid_volume(&mut model, &sewn.shells[0]);
3137        assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3138    }
3139
3140    /// Three unit squares in a row, the middle one wound to face down:
3141    /// sewn, the middle one is turned to agree with the two beside it, the
3142    /// others are copies, and every shared edge is walked once each way.
3143    #[test]
3144    fn a_sheet_turns_the_one_face_walking_against_its_neighbours() {
3145        let mut model = Model::new();
3146        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3147        let square = |x: f64| [at(x, 0.0), at(x + 1.0, 0.0), at(x + 1.0, 1.0), at(x, 1.0)];
3148        let mut middle = square(1.0);
3149        middle.reverse();
3150        let faces = vec![
3151            loose_polygon(&mut model, square(0.0)),
3152            loose_polygon(&mut model, middle),
3153            loose_polygon(&mut model, square(2.0)),
3154        ];
3155        let sewn = sew(&mut model, &faces, T).unwrap();
3156        assert_eq!(sewn.shells.len(), 1);
3157        assert_eq!(sewn.joined, 2);
3158        assert!(sewn.edges_walked_one_way(&model).unwrap().is_empty());
3159        let image = sewn.history.modified(&faces[1]);
3160        assert_eq!(image.len(), 1);
3161        assert_ne!(image[0].orientation(), faces[1].orientation());
3162        for face in [&faces[0], &faces[2]] {
3163            assert!(sewn.history.copy_of(face).is_some());
3164        }
3165    }
3166
3167    /// A band of twelve triangles around a circle, given a half twist so
3168    /// that its two sides are one: no orientation walks every shared edge
3169    /// once each way. Sewn, it is one open shell whose faces are all kept
3170    /// as given, and the edges walked the same way are named.
3171    #[test]
3172    fn a_half_twisted_band_is_kept_and_named() {
3173        let mut model = Model::new();
3174        let (radius, half_width, steps) = (5.0, 1.0, 6_u32);
3175        let at = |k: u32, side: f64| {
3176            let u = std::f64::consts::TAU * f64::from(k) / f64::from(steps);
3177            let (radial, up) = ((u / 2.0).cos(), (u / 2.0).sin());
3178            Point::new(
3179                (radius + side * half_width * radial) * u.cos(),
3180                (radius + side * half_width * radial) * u.sin(),
3181                side * half_width * up,
3182            )
3183        };
3184        let mut faces = Vec::new();
3185        for k in 0..steps {
3186            faces.push(loose_polygon(
3187                &mut model,
3188                [at(k, -1.0), at(k + 1, -1.0), at(k, 1.0)],
3189            ));
3190            faces.push(loose_polygon(
3191                &mut model,
3192                [at(k + 1, -1.0), at(k + 1, 1.0), at(k, 1.0)],
3193            ));
3194        }
3195        let sewn = sew(&mut model, &faces, T).unwrap();
3196        assert_eq!(sewn.shells.len(), 1);
3197        // Six rungs and six diagonals shared, the rim of twelve edges free.
3198        assert_eq!(sewn.joined, 12);
3199        assert_eq!(sewn.free_edges.len(), 12);
3200        for face in &faces {
3201            assert!(sewn.history.copy_of(face).is_some());
3202        }
3203        assert!(!sewn.edges_walked_one_way(&model).unwrap().is_empty());
3204    }
3205
3206    #[test]
3207    fn edges_in_any_order_come_back_as_a_path() {
3208        let mut model = Model::new();
3209        let corners = [
3210            Point::new(0.0, 0.0, 0.0),
3211            Point::new(1.0, 0.0, 0.0),
3212            Point::new(1.0, 1.0, 0.0),
3213            Point::new(0.0, 1.0, 0.0),
3214        ];
3215        let wire = make_polygon(&mut model, &corners, true, T).unwrap().shape;
3216        let mut edges = model.children_of(&wire).unwrap();
3217        // Shuffled, and some of them turned round.
3218        edges.swap(0, 2);
3219        edges[1] = edges[1].reversed();
3220        edges[3] = edges[3].reversed();
3221
3222        let ordered = order_edges(&model, &edges, T).unwrap();
3223        assert_eq!(ordered.len(), 4);
3224        // A wire only builds if consecutive edges actually meet, so this is the
3225        // property under test rather than a separate one.
3226        let rebuilt = make_wire(&mut model, &ordered, T).unwrap().shape;
3227        assert!(crate::is_wire_closed(&model, &rebuilt, T).unwrap());
3228    }
3229
3230    #[test]
3231    fn edges_that_do_not_form_one_path_are_refused() {
3232        let mut model = Model::new();
3233        let a = make_polygon(
3234            &mut model,
3235            &[Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3236            false,
3237            T,
3238        )
3239        .unwrap()
3240        .shape;
3241        let b = make_polygon(
3242            &mut model,
3243            &[Point::new(5.0, 0.0, 0.0), Point::new(6.0, 0.0, 0.0)],
3244            false,
3245            T,
3246        )
3247        .unwrap()
3248        .shape;
3249        let mut edges = model.children_of(&a).unwrap();
3250        edges.extend(model.children_of(&b).unwrap());
3251
3252        let err = order_edges(&model, &edges, T).unwrap_err();
3253        assert!(
3254            err.to_string().contains("connected path"),
3255            "unexpected message: {err}"
3256        );
3257        assert!(order_edges(&model, &[], T).is_err());
3258    }
3259
3260    #[test]
3261    fn a_branching_network_is_refused_rather_than_arbitrarily_walked() {
3262        // Three edges from one point. Any path through it drops a branch, and
3263        // dropping one silently is worse than saying there is no answer.
3264        let mut model = Model::new();
3265        let hub = Point::ORIGIN;
3266        let mut edges = Vec::new();
3267        for tip in [
3268            Point::new(1.0, 0.0, 0.0),
3269            Point::new(0.0, 1.0, 0.0),
3270            Point::new(0.0, 0.0, 1.0),
3271        ] {
3272            let w = make_polygon(&mut model, &[hub, tip], false, T)
3273                .unwrap()
3274                .shape;
3275            edges.extend(model.children_of(&w).unwrap());
3276        }
3277        let err = order_edges(&model, &edges, T).unwrap_err();
3278        assert!(
3279            err.to_string().contains("branching"),
3280            "unexpected message: {err}"
3281        );
3282    }
3283
3284    #[test]
3285    fn two_faces_that_touch_are_sewn_into_one_shell() {
3286        let mut model = Model::new();
3287        let left = loose_polygon(
3288            &mut model,
3289            [
3290                Point::new(0.0, 0.0, 0.0),
3291                Point::new(1.0, 0.0, 0.0),
3292                Point::new(1.0, 1.0, 0.0),
3293                Point::new(0.0, 1.0, 0.0),
3294            ],
3295        );
3296        let right = loose_polygon(
3297            &mut model,
3298            [
3299                Point::new(1.0, 0.0, 0.0),
3300                Point::new(2.0, 0.0, 0.0),
3301                Point::new(2.0, 1.0, 0.0),
3302                Point::new(1.0, 1.0, 0.0),
3303            ],
3304        );
3305
3306        // Before: eight edges, nothing shared.
3307        let before = explore_unique(&model, &left, ShapeType::Edge)
3308            .unwrap()
3309            .len()
3310            + explore_unique(&model, &right, ShapeType::Edge)
3311                .unwrap()
3312                .len();
3313        assert_eq!(before, 8);
3314
3315        let sewn = sew(&mut model, &[left.clone(), right.clone()], T).unwrap();
3316        assert_eq!(sewn.shells.len(), 1, "they touch, so they are one shell");
3317        assert_eq!(sewn.joined, 1, "one shared edge");
3318        assert_eq!(
3319            explore_unique(&model, &sewn.shells[0], ShapeType::Edge)
3320                .unwrap()
3321                .len(),
3322            7,
3323            "the shared edge is one edge now, not two"
3324        );
3325        // A sheet, so it still has a boundary: six free edges round the
3326        // outside, and the shared one is not among them.
3327        assert_eq!(sewn.free_edges.len(), 6);
3328        assert!(!is_shell_closed(&model, &sewn.shells[0]).unwrap());
3329        assert!(sewn.history.is_affected(&left));
3330    }
3331
3332    /// A face presented reversed, its rings wound against its surface's
3333    /// normal, beside one presented as built: both face up, so the edge
3334    /// they share is walked once each way. Rebuilt onto the shared edge,
3335    /// the reversed face still walks it against its neighbour.
3336    #[test]
3337    fn a_reversed_face_rebuilt_onto_a_shared_edge_keeps_its_walk() {
3338        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3339        let mut model = Model::new();
3340        let left = loose_polygon(
3341            &mut model,
3342            [at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
3343        );
3344        // Wound clockwise seen from above: its plane faces down, and the
3345        // face is presented the other way round.
3346        let right = loose_polygon(
3347            &mut model,
3348            [at(1.0, 0.0), at(1.0, 1.0), at(2.0, 1.0), at(2.0, 0.0)],
3349        )
3350        .reversed();
3351        let sewn = sew(&mut model, &[left, right], T).unwrap();
3352        assert_eq!(sewn.joined, 1, "one shared edge");
3353        let mut walks: FastMap<TShapeId, (usize, usize)> = FastMap::default();
3354        for face in ogeom_topo::explore(
3355            &model,
3356            &sewn.shells[0],
3357            ogeom_topo::Filter::OfType(ShapeType::Face),
3358        )
3359        .unwrap()
3360        {
3361            for edge in
3362                ogeom_topo::explore(&model, &face, ogeom_topo::Filter::OfType(ShapeType::Edge))
3363                    .unwrap()
3364            {
3365                let entry = walks.entry(edge.node()).or_default();
3366                if edge.orientation() == Orientation::Forward {
3367                    entry.0 += 1;
3368                } else {
3369                    entry.1 += 1;
3370                }
3371            }
3372        }
3373        let shared: Vec<_> = walks.values().filter(|(f, r)| f + r == 2).collect();
3374        assert_eq!(shared, [&(1, 1)], "the shared edge, walked once each way");
3375    }
3376
3377    /// Twin edges whose ends are two vertices apart by more than either
3378    /// vertex's own tolerance, though within the edges': the right square's
3379    /// corner sits a hundredth below the left's, and its shared edge is loose
3380    /// enough to be the left's. Merged, the edges must end on one vertex, or
3381    /// the right square's bottom edge still ends at its own corner and its
3382    /// wire opens.
3383    #[test]
3384    fn twin_edges_join_the_vertices_they_end_on() {
3385        let mut model = Model::new();
3386        let left = loose_polygon(
3387            &mut model,
3388            [
3389                Point::new(0.0, 0.0, 0.0),
3390                Point::new(1.0, 0.0, 0.0),
3391                Point::new(1.0, 1.0, 0.0),
3392                Point::new(0.0, 1.0, 0.0),
3393            ],
3394        );
3395        let right = loose_polygon(
3396            &mut model,
3397            [
3398                Point::new(1.0, -0.01, 0.0),
3399                Point::new(2.0, 0.0, 0.0),
3400                Point::new(2.0, 1.0, 0.0),
3401                Point::new(1.0, 1.0, 0.0),
3402            ],
3403        );
3404        for edge in explore_unique(&model, &right, ShapeType::Edge).unwrap() {
3405            let (a, b) = crate::edge_vertices(&model, &edge).unwrap().unwrap();
3406            let (pa, pb) = (placed(&model, &a).unwrap(), placed(&model, &b).unwrap());
3407            if (pa.x - 1.0).abs() < 1e-9
3408                && (pb.x - 1.0).abs() < 1e-9
3409                && let Some(node) = model.node_mut(&edge)
3410                && let NodeData::Edge(data) = node.data_mut()
3411            {
3412                data.tolerance = data.tolerance.widen_to(0.012);
3413            }
3414        }
3415        let sewn = sew(&mut model, &[left, right], T).unwrap();
3416        assert_eq!(sewn.joined, 1, "the loose edge is the left square's");
3417        assert_eq!(sewn.shells.len(), 1);
3418        assert_eq!(
3419            explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
3420                .unwrap()
3421                .len(),
3422            6,
3423            "the two corners at the bottom of the shared edge are one"
3424        );
3425        assert_eq!(sewn.free_edges.len(), 6);
3426    }
3427
3428    #[test]
3429    fn faces_that_do_not_meet_stay_in_separate_shells() {
3430        // Claiming one shell would claim a closure that is not there.
3431        let mut model = Model::new();
3432        let here = loose_polygon(
3433            &mut model,
3434            [
3435                Point::new(0.0, 0.0, 0.0),
3436                Point::new(1.0, 0.0, 0.0),
3437                Point::new(1.0, 1.0, 0.0),
3438                Point::new(0.0, 1.0, 0.0),
3439            ],
3440        );
3441        let far = loose_polygon(
3442            &mut model,
3443            [
3444                Point::new(50.0, 0.0, 0.0),
3445                Point::new(51.0, 0.0, 0.0),
3446                Point::new(51.0, 1.0, 0.0),
3447                Point::new(50.0, 1.0, 0.0),
3448            ],
3449        );
3450        let sewn = sew(&mut model, &[here, far], T).unwrap();
3451        assert_eq!(sewn.shells.len(), 2);
3452        assert_eq!(sewn.joined, 0);
3453        assert_eq!(sewn.free_edges.len(), 8);
3454    }
3455
3456    #[test]
3457    fn a_boxs_faces_taken_apart_and_sewn_back_close_again() {
3458        // The end-to-end case. The faces already share edges here, so what is
3459        // under test is that sewing does not *break* a shell that was closed,
3460        // and that the mesh still agrees with the topology afterwards, which is
3461        // the check a re-built face is most likely to fail.
3462        let mut model = Model::new();
3463        let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
3464            .unwrap()
3465            .shape;
3466        let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
3467
3468        let sewn = sew(&mut model, &faces, T).unwrap();
3469        assert_eq!(sewn.shells.len(), 1);
3470        assert!(sewn.free_edges.is_empty(), "a box has no free edges");
3471        assert!(is_shell_closed(&model, &sewn.shells[0]).unwrap());
3472        assert!(
3473            check_tessellation(&model, &sewn.shells[0], fine(), T)
3474                .unwrap()
3475                .is_valid()
3476        );
3477    }
3478
3479    #[test]
3480    fn two_arcs_between_the_same_vertices_are_not_the_same_edge() {
3481        // The reason the fingerprint samples the middle. Both halves of a
3482        // circle agree at both ends; merging them would fuse the shape to
3483        // itself and the mistake would look like a successful sew.
3484        let mut model = Model::new();
3485        let circle = ogeom_math::Circle::new(Frame::WORLD, 1.0, T).unwrap();
3486        let upper = crate::make_edge(
3487            &mut model,
3488            ogeom_geom::CircleCurve::new(circle).into(),
3489            (0.0, std::f64::consts::PI),
3490            T,
3491        )
3492        .unwrap()
3493        .shape;
3494        let lower = crate::make_edge(
3495            &mut model,
3496            ogeom_geom::CircleCurve::new(circle).into(),
3497            (std::f64::consts::PI, std::f64::consts::TAU),
3498            T,
3499        )
3500        .unwrap()
3501        .shape;
3502
3503        let a = fingerprint(&model, &upper, T).unwrap().unwrap();
3504        let b = fingerprint(&model, &lower, T).unwrap().unwrap();
3505        assert!(
3506            a.same_as(&b, T).unwrap().is_none(),
3507            "two different arcs were called the same edge"
3508        );
3509        assert!(a.same_as(&a, T).unwrap() == Some(false));
3510    }
3511
3512    #[test]
3513    fn a_wide_vertex_at_one_end_does_not_join_edges_apart_at_the_other() {
3514        // Two sides of a sliver six microns long, a quarter of a micron
3515        // wide at its open end, meeting at a vertex widened to most of a
3516        // micron where a section was welded. The open end's vertices are
3517        // exact, so the sides are two edges and the sliver stays a face.
3518        let mut model = Model::new();
3519        let shared = Point::new(0.0, 0.0, 0.0);
3520        let side = |model: &mut Model, to: Point| {
3521            crate::make_edge(
3522                model,
3523                ogeom_geom::LineCurve::segment(shared, to, T)
3524                    .unwrap()
3525                    .into(),
3526                (0.0, to.distance(shared)),
3527                T,
3528            )
3529            .unwrap()
3530            .shape
3531        };
3532        let a = side(&mut model, Point::new(6e-3, 0.0, 0.0));
3533        let b = side(&mut model, Point::new(6e-3, 2.7e-4, 0.0));
3534        for edge in [&a, &b] {
3535            let start = model.children_of(edge).unwrap()[0].clone();
3536            model
3537                .widen(&start, ogeom_core::Tolerance::new(8.8e-4).unwrap())
3538                .unwrap();
3539        }
3540        let pa = fingerprint(&model, &a, T).unwrap().unwrap();
3541        let pb = fingerprint(&model, &b, T).unwrap().unwrap();
3542        assert!(pa.same_as(&pb, T).unwrap().is_none());
3543    }
3544
3545    #[test]
3546    fn an_edge_found_the_other_way_round_is_reversed_rather_than_dropped() {
3547        let mut model = Model::new();
3548        let up = Point::new(0.0, 0.0, 1.0);
3549        let down = Point::new(0.0, 0.0, 0.0);
3550        let a = crate::make_edge(
3551            &mut model,
3552            ogeom_geom::LineCurve::segment(down, up, T).unwrap().into(),
3553            (0.0, 1.0),
3554            T,
3555        )
3556        .unwrap()
3557        .shape;
3558        let b = crate::make_edge(
3559            &mut model,
3560            ogeom_geom::LineCurve::segment(up, down, T).unwrap().into(),
3561            (0.0, 1.0),
3562            T,
3563        )
3564        .unwrap()
3565        .shape;
3566
3567        let pa = fingerprint(&model, &a, T).unwrap().unwrap();
3568        let pb = fingerprint(&model, &b, T).unwrap().unwrap();
3569        assert_eq!(
3570            pa.same_as(&pb, T).unwrap(),
3571            Some(true),
3572            "the same edge, running the other way"
3573        );
3574    }
3575
3576    #[test]
3577    fn flipped_merges_carry_their_pcurves_the_right_way_round() {
3578        // Six faces of a unit cube, each built loose with its own vertices
3579        // and pre-attached pcurves, wound counter-clockwise around the
3580        // outward normal as a shell is. Sewing merges all twelve edge pairs,
3581        // and every merge is *flipped*: the two faces walk their shared
3582        // edge opposite ways. A carried pcurve copied unchanged then makes
3583        // the losing face walk edges backwards in parameter space; with
3584        // several such edges in one ring the boundary zigzags, and faces
3585        // stop triangulating or triangulate degenerately. Two squares are
3586        // not enough to see it (a single backwards two-point edge self-heals
3587        // in ring assembly), which is why this test is a cube.
3588        //
3589        // The carry must reverse with the merge: consumers map 3D parameters
3590        // onto the pcurve range proportionally, so swapping the stored
3591        // range's ends reverses the traversal exactly.
3592        let mut model = Model::new();
3593        let c = Point::new;
3594        let faces = [
3595            [c(0., 0., 0.), c(0., 1., 0.), c(1., 1., 0.), c(1., 0., 0.)],
3596            [c(0., 0., 1.), c(1., 0., 1.), c(1., 1., 1.), c(0., 1., 1.)],
3597            [c(0., 0., 0.), c(1., 0., 0.), c(1., 0., 1.), c(0., 0., 1.)],
3598            [c(0., 1., 0.), c(0., 1., 1.), c(1., 1., 1.), c(1., 1., 0.)],
3599            [c(0., 0., 0.), c(0., 0., 1.), c(0., 1., 1.), c(0., 1., 0.)],
3600            [c(1., 0., 0.), c(1., 1., 0.), c(1., 1., 1.), c(1., 0., 1.)],
3601        ];
3602        let built: Vec<Shape> = faces
3603            .iter()
3604            .map(|f| loose_polygon(&mut model, *f))
3605            .collect();
3606        let sewn = sew(&mut model, &built, T).unwrap();
3607        assert_eq!(sewn.joined, 12, "every edge pair merged");
3608        assert!(sewn.free_edges.is_empty());
3609        for face in ogeom_topo::explore(
3610            &model,
3611            &sewn.shells[0],
3612            ogeom_topo::Filter::OfType(ShapeType::Face),
3613        )
3614        .unwrap()
3615        {
3616            let mesh = ogeom_mesh::triangulate(&model, &face, fine(), T).unwrap();
3617            assert_eq!(
3618                mesh.triangles.len(),
3619                2,
3620                "a unit square triangulates into two triangles, whichever twin \
3621                 survived and whichever way it runs"
3622            );
3623        }
3624    }
3625
3626    /// One loose edge among tight ones: the tight edges' twin search stays
3627    /// local, and every pair within its wider width is still a candidate.
3628    #[test]
3629    fn one_loose_edge_does_not_widen_every_search() {
3630        let mut model = Model::new();
3631        let mut catalogue = Vec::new();
3632        for k in 0..200 {
3633            let x = f64::from(k);
3634            let wire = make_polygon(
3635                &mut model,
3636                &[Point::new(x, 0.0, 0.0), Point::new(x + 0.5, 0.0, 0.0)],
3637                false,
3638                T,
3639            )
3640            .unwrap()
3641            .shape;
3642            let edge = model.children_of(&wire).unwrap()[0].clone();
3643            let print = fingerprint(&model, &edge, T).unwrap().unwrap();
3644            catalogue.push((edge.node(), print));
3645        }
3646        let loose = 150;
3647        catalogue[loose].1.width = 20.0;
3648        let bins = StartBins::new(&catalogue, T);
3649        assert!(bins.twin_candidates(&catalogue, 0).len() <= 3);
3650        // Edge 140 starts 10 from the loose edge, within its width.
3651        assert!(bins.twin_candidates(&catalogue, 140).contains(&loose));
3652        let from_loose = bins.twin_candidates(&catalogue, loose);
3653        assert!((151..=170).all(|j| from_loose.contains(&j)));
3654    }
3655
3656    #[test]
3657    fn sewing_nothing_and_sewing_the_wrong_kind_are_refused() {
3658        let mut model = Model::new();
3659        assert!(sew(&mut model, &[], T).is_err());
3660        let vertex = model.add_point(Point::ORIGIN);
3661        assert!(sew(&mut model, &[vertex], T).is_err());
3662    }
3663
3664    /// An edge from `(0, 0, 0)` to `(1, 0, 0)` at uniform pace, and its twin
3665    /// on an open quadratic, `x = s²`, used over `[0, 1 − 10⁻⁶]`, whose
3666    /// pcurve on the plane `z = 0` is `(s², 0)`: the twin's pcurve
3667    /// carried to the edge's pace is `(t, 0)`, and the edge's end, whose
3668    /// foot lies past the twin's stretch on a curve that does not close,
3669    /// is held at the stretch's end, not a domain's length back at its
3670    /// start.
3671    #[test]
3672    fn a_pcurve_carried_from_an_open_twin_keeps_its_far_end() {
3673        use ogeom_geom::{BSpline2d, BSplineCurve, Curve2d as _};
3674        use ogeom_math::{KnotVector, Point2};
3675        let mut model = Model::new();
3676        let line = BSplineCurve::new(
3677            KnotVector::clamped_uniform(1, 2).unwrap(),
3678            vec![Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3679            T,
3680        )
3681        .unwrap();
3682        let kept = crate::make_edge(&mut model, line.into(), (0.0, 1.0), T)
3683            .unwrap()
3684            .shape;
3685        let square = BSplineCurve::new(
3686            KnotVector::clamped_uniform(2, 3).unwrap(),
3687            vec![Point::ORIGIN, Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3688            T,
3689        )
3690        .unwrap();
3691        let short = 1.0 - 1e-6;
3692        let dropped = crate::make_edge(&mut model, square.into(), (0.0, short), T)
3693            .unwrap()
3694            .shape;
3695        let frame = Frame::new(
3696            Point::ORIGIN,
3697            ogeom_math::Direction::Z,
3698            ogeom_math::Direction::X,
3699            T,
3700        )
3701        .unwrap();
3702        let surface = model
3703            .geometry_mut()
3704            .add_surface(PlaneSurface::new(Plane::new(frame)).into());
3705        let trace = BSpline2d::new(
3706            KnotVector::clamped_uniform(2, 3).unwrap(),
3707            vec![
3708                Point2::new(0.0, 0.0),
3709                Point2::new(0.0, 0.0),
3710                Point2::new(1.0, 0.0),
3711            ],
3712            T,
3713        )
3714        .unwrap();
3715        let curve = model.geometry_mut().add_pcurve(trace.into());
3716        let carried = [EdgeRepr::PCurve {
3717            curve,
3718            surface,
3719            location: Location::identity(),
3720            range: (0.0, short),
3721        }];
3722        let paced = repaced_carry(
3723            &mut model,
3724            &dropped,
3725            &kept,
3726            false,
3727            &carried,
3728            T.confusion(),
3729            T,
3730        )
3731        .unwrap()
3732        .unwrap()
3733        .0;
3734        let [EdgeRepr::PCurve { curve, range, .. }] = paced.as_slice() else {
3735            panic!("{paced:?}");
3736        };
3737        assert_eq!(*range, (0.0, 1.0));
3738        let pcurve = model.geometry().pcurve(*curve).unwrap();
3739        // The twin's stretch stops `1 − short²` short of the edge's end.
3740        let gap = 1.0 - short * short;
3741        for k in 0..=64 {
3742            let t = f64::from(k) / 64.0;
3743            let uv = pcurve.point_at(t, T).unwrap();
3744            assert!(
3745                uv.distance(Point2::new(t, 0.0)) <= gap + T.confusion(),
3746                "at {t}: {uv:?}"
3747            );
3748        }
3749    }
3750
3751    /// Two strips side by side on the plane `z = 0`: `[0, L] × [−1, 0]`, its
3752    /// edges straight, and `[0, L] × [0, 1]`, whose bottom edge is `bottom`
3753    /// (its curve and range) drawn by `trace` over `[0, 1]` in the plane's
3754    /// own coordinates, the world's `x` and `y`. The first strip's edge
3755    /// along `y = 0`, catalogued first, is the one kept. Returns the strips
3756    /// and the second's surface.
3757    fn strips_meeting_on(
3758        model: &mut Model,
3759        length: f64,
3760        bottom: (ogeom_geom::Curve, (f64, f64)),
3761        trace: ogeom_geom::PlanarCurve,
3762    ) -> ([Shape; 2], ogeom_topo::SurfaceId) {
3763        use ogeom_geom::{Line2d, LineCurve};
3764        use ogeom_math::Point2;
3765        let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3766        let below = loose_polygon(
3767            model,
3768            [
3769                at(0.0, -1.0),
3770                at(length, -1.0),
3771                at(length, 0.0),
3772                at(0.0, 0.0),
3773            ],
3774        );
3775        let surface = model
3776            .geometry_mut()
3777            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3778        let corners = [at(0.0, 0.0), at(length, 0.0), at(length, 1.0), at(0.0, 1.0)];
3779        let vertices: Vec<Shape> = corners
3780            .iter()
3781            .map(|p| crate::make_vertex(model, *p).shape)
3782            .collect();
3783        let mut edges = vec![
3784            crate::make_edge_between(model, bottom.0, bottom.1, &vertices[0], &vertices[1], T)
3785                .unwrap()
3786                .shape,
3787        ];
3788        crate::attach_pcurve(
3789            model,
3790            &edges[0],
3791            trace,
3792            surface,
3793            Location::identity(),
3794            (0.0, 1.0),
3795        )
3796        .unwrap();
3797        for k in 1..4 {
3798            let (a, b) = (corners[k], corners[(k + 1) % 4]);
3799            let edge = crate::make_edge_between(
3800                model,
3801                LineCurve::segment(a, b, T).unwrap().into(),
3802                (0.0, a.distance(b)),
3803                &vertices[k],
3804                &vertices[(k + 1) % 4],
3805                T,
3806            )
3807            .unwrap()
3808            .shape;
3809            crate::attach_pcurve(
3810                model,
3811                &edge,
3812                Line2d::segment(Point2::new(a.x, a.y), Point2::new(b.x, b.y), T)
3813                    .unwrap()
3814                    .into(),
3815                surface,
3816                Location::identity(),
3817                (0.0, a.distance(b)),
3818            )
3819            .unwrap();
3820            edges.push(edge);
3821        }
3822        let wire = crate::make_wire(model, &edges, T).unwrap().shape;
3823        let above = crate::make_face_on(model, surface, std::slice::from_ref(&wire), T)
3824            .unwrap()
3825            .shape;
3826        ([below, above], surface)
3827    }
3828
3829    /// The edge from `(0, 0, 0)` to `(L, 0, 0)` paced as `x = L s²`.
3830    fn squared_pace(length: f64) -> (ogeom_geom::Curve, (f64, f64)) {
3831        use ogeom_geom::BSplineCurve;
3832        use ogeom_math::KnotVector;
3833        let curve = BSplineCurve::new(
3834            KnotVector::clamped_uniform(2, 3).unwrap(),
3835            vec![Point::ORIGIN, Point::ORIGIN, Point::new(length, 0.0, 0.0)],
3836            T,
3837        )
3838        .unwrap();
3839        (curve.into(), (0.0, 1.0))
3840    }
3841
3842    /// The trace `(L s², 0)` drawn as a polyline through 65 of its points
3843    /// over `[0, 1 − 10⁻³]`, then run back to `(0, 0)` over the last
3844    /// thousandth: everywhere on the edge's line, but at `s = 1` at its
3845    /// start, as a pcurve read at a foot carried a domain back from an open
3846    /// curve's end lands.
3847    fn traced_back_to_its_start(length: f64) -> ogeom_geom::PlanarCurve {
3848        use ogeom_geom::BSpline2d;
3849        use ogeom_math::{KnotVector, Point2};
3850        const N: u32 = 64;
3851        let reach = 1.0 - 1e-3;
3852        let mut knots = vec![0.0, 0.0];
3853        let mut control = Vec::new();
3854        for i in 0..=N {
3855            let s = f64::from(i) / f64::from(N);
3856            if i > 0 {
3857                knots.push(s * reach);
3858            }
3859            control.push(Point2::new(length * s * s, 0.0));
3860        }
3861        knots.extend([1.0, 1.0]);
3862        control.push(Point2::new(0.0, 0.0));
3863        BSpline2d::new(KnotVector::new(knots, 1).unwrap(), control, T)
3864            .unwrap()
3865            .into()
3866    }
3867
3868    /// The widest the edge's pcurve on `surface`, lifted, stands off its
3869    /// curve at the same fraction of the way along, over 4096 samples.
3870    fn matched_miss(model: &Model, edge: &Shape, surface: ogeom_topo::SurfaceId) -> f64 {
3871        use ogeom_geom::{Curve2d as _, Surface as _};
3872        let data = model.node(edge).unwrap().data().as_edge().unwrap();
3873        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
3874            panic!("no curve");
3875        };
3876        let curve = model.geometry().curve(*curve).unwrap();
3877        let Some(EdgeRepr::PCurve {
3878            curve: pc,
3879            range: prange,
3880            ..
3881        }) = data.pcurve_for(surface, &Location::identity())
3882        else {
3883            panic!("no pcurve on the surface");
3884        };
3885        let pcurve = model.geometry().pcurve(*pc).unwrap();
3886        let chart = model.geometry().surface(surface).unwrap();
3887        let mut widest = 0.0_f64;
3888        for k in 0..=4096 {
3889            let f = f64::from(k) / 4096.0;
3890            let uv = pcurve
3891                .point_at(prange.0 + (prange.1 - prange.0) * f, T)
3892                .unwrap();
3893            let lifted = chart.point_at(uv.x, uv.y, T).unwrap();
3894            let on = curve
3895                .point_at(range.0 + (range.1 - range.0) * f, T)
3896                .unwrap();
3897            widest = widest.max(lifted.distance(on));
3898        }
3899        widest
3900    }
3901
3902    /// The edge both strips share, after sewing.
3903    fn shared_edge(model: &Model, sewn: &Sewn) -> Shape {
3904        let mut uses: FastMap<TShapeId, (usize, Shape)> = FastMap::default();
3905        for shell in &sewn.shells {
3906            for face in explore_unique(model, shell, ShapeType::Face).unwrap() {
3907                for edge in explore_unique(model, &face, ShapeType::Edge).unwrap() {
3908                    uses.entry(edge.node()).or_insert((0, edge.clone())).0 += 1;
3909                }
3910            }
3911        }
3912        let shared: Vec<Shape> = uses
3913            .into_values()
3914            .filter(|(n, _)| *n == 2)
3915            .map(|(_, e)| e)
3916            .collect();
3917        let [edge] = shared.as_slice() else {
3918            panic!("{} shared edges", shared.len());
3919        };
3920        edge.clone()
3921    }
3922
3923    /// A twin paced `x = 2 s²` whose pcurve lies on the shared line but at
3924    /// its end is back at its start: carried to the kept edge's pace it
3925    /// misses the kept edge's end by its whole length, 2, past the trim
3926    /// cap of a millimetre at unit scale, though every lifted point stands
3927    /// on the line. The two are not one edge, and the strips stay apart.
3928    #[test]
3929    fn a_twin_whose_carried_pcurve_ends_at_its_start_is_kept_apart() {
3930        let mut model = Model::new();
3931        let length = 2.0;
3932        let (strips, _) = strips_meeting_on(
3933            &mut model,
3934            length,
3935            squared_pace(length),
3936            traced_back_to_its_start(length),
3937        );
3938        let sewn = sew(&mut model, &strips, T).unwrap();
3939        assert_eq!(sewn.joined, 0);
3940        assert_eq!(sewn.shells.len(), 2);
3941    }
3942
3943    /// The same twin half a unit long, within the cap: sewn, and the kept
3944    /// edge states how far the carried pcurve misses it, at least half a
3945    /// unit at its end, measured densely.
3946    #[test]
3947    fn a_twin_whose_carried_pcurve_ends_at_its_start_states_its_miss() {
3948        let mut model = Model::new();
3949        let length = 0.5;
3950        let (strips, surface) = strips_meeting_on(
3951            &mut model,
3952            length,
3953            squared_pace(length),
3954            traced_back_to_its_start(length),
3955        );
3956        let sewn = sew(&mut model, &strips, T).unwrap();
3957        assert_eq!(sewn.joined, 1);
3958        let edge = shared_edge(&model, &sewn);
3959        let stated = model
3960            .node(&edge)
3961            .unwrap()
3962            .data()
3963            .as_edge()
3964            .unwrap()
3965            .tolerance
3966            .get();
3967        let miss = matched_miss(&model, &edge, surface);
3968        assert!(miss >= length, "{miss}");
3969        assert!(stated >= miss, "states {stated} against a miss of {miss}");
3970    }
3971
3972    /// The twin paced `x = 2 s²` with its own pcurve `(2 s², 0)`: carried
3973    /// to the kept edge's pace, by length along it, it is `(t, 0)`, and the
3974    /// kept edge states the tolerance its strip's edges held before sewing.
3975    #[test]
3976    fn a_twin_carried_at_its_own_pace_keeps_the_kept_edge_s_tolerance() {
3977        use ogeom_geom::BSpline2d;
3978        use ogeom_math::{KnotVector, Point2};
3979        let mut model = Model::new();
3980        let length = 2.0;
3981        let trace = BSpline2d::new(
3982            KnotVector::clamped_uniform(2, 3).unwrap(),
3983            vec![
3984                Point2::new(0.0, 0.0),
3985                Point2::new(0.0, 0.0),
3986                Point2::new(length, 0.0),
3987            ],
3988            T,
3989        )
3990        .unwrap();
3991        let (strips, surface) =
3992            strips_meeting_on(&mut model, length, squared_pace(length), trace.into());
3993        let before: Vec<f64> = explore_unique(&model, &strips[0], ShapeType::Edge)
3994            .unwrap()
3995            .iter()
3996            .map(|e| {
3997                model
3998                    .node(e)
3999                    .unwrap()
4000                    .data()
4001                    .as_edge()
4002                    .unwrap()
4003                    .tolerance
4004                    .get()
4005            })
4006            .collect();
4007        let sewn = sew(&mut model, &strips, T).unwrap();
4008        assert_eq!(sewn.joined, 1);
4009        let edge = shared_edge(&model, &sewn);
4010        let stated = model
4011            .node(&edge)
4012            .unwrap()
4013            .data()
4014            .as_edge()
4015            .unwrap()
4016            .tolerance
4017            .get();
4018        assert!(
4019            before.iter().all(|b| *b == stated),
4020            "{before:?} against {stated}"
4021        );
4022        assert!(matched_miss(&model, &edge, surface) <= T.confusion());
4023    }
4024
4025    /// A twin on the kept edge's own line and pace, its pcurve rising to a
4026    /// hundredth off the line over a tenth of the width between two of the
4027    /// 33 points a uniform sampling reads, and nowhere near the middle of
4028    /// that span: carried as it is, the kept edge states the rise.
4029    #[test]
4030    fn a_rise_in_a_carried_pcurve_between_samples_is_stated() {
4031        use ogeom_geom::{BSpline2d, LineCurve};
4032        use ogeom_math::{KnotVector, Point2};
4033        let mut model = Model::new();
4034        let length = 1.0;
4035        let (rise, step) = (1e-2, 1.0 / 32.0);
4036        let (from, top, to) = (10.2 * step, 10.25 * step, 10.3 * step);
4037        let trace = BSpline2d::new(
4038            KnotVector::new(vec![0.0, 0.0, from, top, to, 1.0, 1.0], 1).unwrap(),
4039            vec![
4040                Point2::new(0.0, 0.0),
4041                Point2::new(from * length, 0.0),
4042                Point2::new(top * length, rise),
4043                Point2::new(to * length, 0.0),
4044                Point2::new(length, 0.0),
4045            ],
4046            T,
4047        )
4048        .unwrap();
4049        let line = LineCurve::segment(Point::ORIGIN, Point::new(length, 0.0, 0.0), T).unwrap();
4050        let (strips, _) = strips_meeting_on(
4051            &mut model,
4052            length,
4053            (line.into(), (0.0, length)),
4054            trace.into(),
4055        );
4056        let sewn = sew(&mut model, &strips, T).unwrap();
4057        assert_eq!(sewn.joined, 1);
4058        let edge = shared_edge(&model, &sewn);
4059        let stated = model
4060            .node(&edge)
4061            .unwrap()
4062            .data()
4063            .as_edge()
4064            .unwrap()
4065            .tolerance
4066            .get();
4067        assert!(stated >= rise, "states {stated} against a rise of {rise}");
4068        assert!(
4069            stated <= rise * (1.0 + 1e-3),
4070            "states {stated} against a rise of {rise}"
4071        );
4072    }
4073
4074    #[test]
4075    fn sewing_does_not_move_anything() {
4076        // A repair that closes gaps by moving geometry has to decide which of
4077        // two positions is right, and would invalidate every tolerance nearby.
4078        // This decides that two edges *are* one edge and leaves the points
4079        // where they were.
4080        let mut model = Model::new();
4081        let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4082            .unwrap()
4083            .shape;
4084        let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
4085        let before: Vec<Point> = explore_unique(&model, &solid, ShapeType::Vertex)
4086            .unwrap()
4087            .iter()
4088            .map(|v| placed(&model, v).unwrap())
4089            .collect();
4090
4091        let sewn = sew(&mut model, &faces, T).unwrap();
4092        let after: Vec<Point> = explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
4093            .unwrap()
4094            .iter()
4095            .map(|v| placed(&model, v).unwrap())
4096            .collect();
4097        assert_eq!(before.len(), after.len());
4098        for p in &after {
4099            assert!(
4100                before.iter().any(|q| q.is_equal(*p, T)),
4101                "a vertex moved: {p:?}"
4102            );
4103        }
4104        let _ = Vector::ZERO;
4105    }
4106}