Skip to main content

ogeom_heal/
seam.rs

1//! Seams and poles for periodic faces that arrive without them.
2//!
3//! A STEP face on a cylinder may be a band between two rings that each go
4//! round once, with no seam edge: one ring a whole circle, the other a
5//! notched run of arcs and rulings. A face on a sphere may be a cap bounded
6//! by one circle, an octant whose two meridians meet at the pole with no
7//! edge along the pole's row, or a hemisphere bounded by one great circle
8//! through both poles. Each is a valid solid's boundary and meshes, but in
9//! the surface's chart the boundary does not close (or, for the great
10//! circle, has no exact image), and anything that works in the chart (a
11//! boolean's arrangement) cannot use it.
12//!
13//! The band gets a seam at a column where each ring passes once: a vertex
14//! of the notched ring, with the whole circle re-anchored there when it has
15//! no vertex of its own on that column. The cap gets a pole vertex, a
16//! degenerate pole edge and a seam down to it. The octant gets a degenerate
17//! edge along the pole's row between the two meridians. The great circle
18//! is split at the poles into half meridians, each a straight line in the
19//! chart, with a degenerate edge along the pole's row between them.
20
21use ogeom_core::FastMap;
22
23use ogeom_algo::{
24    Built, History, is_shell_closed, make_band_of_rings, make_edge_between, make_face_on,
25    make_shell, make_solid, make_vertex, make_wire,
26};
27use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
28use ogeom_geom::{Curve, Curve2d as _, Curve3d as _, Surface as _, SurfaceGeometry};
29use ogeom_math::{Axis2, Direction2, Point, Point2, Vector2};
30use ogeom_topo::{
31    EdgeData, EdgeRepr, Filter, Location, Model, NodeData, Shape, ShapeType, TShapeId, explore,
32    explore_unique,
33};
34
35use crate::reanchor::{attach_face_pcurves, circle_parameter};
36
37/// Give every seamless periodic face of a solid the seam or pole edge its
38/// chart needs to close.
39///
40/// Four shapes of face are repaired, each only on a cylinder, cone or
41/// sphere and only where every edge has (or, split, takes) a closed-form
42/// pcurve: a band between two rings that each go once round, a sphere's cap
43/// bounded by one ring, a sphere's face reaching a pole with no edge along
44/// the pole's row, and a sphere's face whose boundary runs along a great
45/// circle through a pole (split there, its neighbours taking the pieces). A
46/// face that resists is left as it was. Returns the solid and how many faces
47/// were repaired; a solid with none comes back as itself with an empty
48/// history.
49///
50/// # Errors
51///
52/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
53/// shape is not a solid, or the rebuilt shell does not close.
54pub fn seam_periodic_faces(
55    model: &mut Model,
56    shape: &Shape,
57    tol: Tolerances,
58) -> OgeomResult<(Built, usize)> {
59    if model.kind_of(shape)? != ShapeType::Solid {
60        ogeom_bail!(Construction, "seaming heals solids");
61    }
62    let mut substitution: FastMap<TShapeId, Vec<Shape>> = FastMap::default();
63    let mut face_map: FastMap<TShapeId, Shape> = FastMap::default();
64    // Edges a rebuilt face already holds, which no later repair may replace.
65    let mut settled: ogeom_core::FastSet<TShapeId> = ogeom_core::FastSet::default();
66    for face in explore_unique(model, shape, ShapeType::Face)? {
67        let Some((surface_id, surface)) = seamless_periodic(model, &face)? else {
68            continue;
69        };
70        // The rings as the face stores them: each rebuilt face is the face
71        // as stored, and the shells below turn it as the old one was.
72        let rings: Vec<Vec<Shape>> = model
73            .ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))?
74            .iter()
75            .map(|w| model.ordered_children_of(w))
76            .collect::<OgeomResult<_>>()?;
77        // A face whose edges another repair here already replaced takes
78        // them as replaced where it can: a cap on a re-anchored circle
79        // seams wherever the circle now starts. Anything else is left
80        // alone, since its rings would have to agree with that repair's
81        // column.
82        let resolved: Vec<Vec<Shape>> = rings
83            .iter()
84            .map(|ring| {
85                ring.iter()
86                    .flat_map(|e| match substitution.get(&e.node()) {
87                        Some(new_edges) if e.orientation() == ogeom_topo::Orientation::Reversed => {
88                            new_edges.iter().rev().map(Shape::reversed).collect()
89                        }
90                        Some(new_edges) => new_edges.clone(),
91                        None => vec![e.clone()],
92                    })
93                    .collect()
94            })
95            .collect();
96        if resolved != rings {
97            let rebuilt = match &resolved[..] {
98                [ring] if lone_circle(model, ring).is_some() => {
99                    cap(model, &face, &surface, ring, tol)
100                }
101                [ring] => meridian_poles(
102                    model,
103                    &face,
104                    (surface_id, &surface),
105                    ring,
106                    &settled,
107                    true,
108                    tol,
109                )
110                .map(|done| done.and_then(|(rebuilt, more)| more.is_empty().then_some(rebuilt))),
111                _ => Ok(None),
112            };
113            if let Ok(Some(rebuilt)) = rebuilt {
114                settled.extend(
115                    explore_unique(model, &rebuilt, ShapeType::Edge)?
116                        .iter()
117                        .map(Shape::node),
118                );
119                face_map.insert(face.node(), rebuilt);
120            }
121            continue;
122        }
123        if let [ring] = &rings[..]
124            && let Ok(Some((rebuilt, added))) = meridian_poles(
125                model,
126                &face,
127                (surface_id, &surface),
128                ring,
129                &settled,
130                false,
131                tol,
132            )
133        {
134            settled.extend(
135                explore_unique(model, &face, ShapeType::Edge)?
136                    .iter()
137                    .map(Shape::node),
138            );
139            substitution.extend(added);
140            face_map.insert(face.node(), rebuilt);
141            continue;
142        }
143        let walks: Vec<Option<Walk>> = rings
144            .iter()
145            .map(|ring| walk(model, surface_id, &surface, ring, tol))
146            .collect::<OgeomResult<_>>()?;
147        let repaired = match (rings.len(), &walks[..]) {
148            (2, [Some(a), Some(b)]) if a.wraps && b.wraps => band(
149                model,
150                &surface,
151                [&rings[0], &rings[1]],
152                [a, b],
153                (&mut substitution, &settled),
154                tol,
155            ),
156            (1, [Some(a)]) if a.wraps => match (0..rings[0].len()).find(|&i| a.passes_once_at(i)) {
157                Some(i) => {
158                    let mut ring = rings[0].clone();
159                    ring.rotate_left(i);
160                    cap(model, &face, &surface, &ring, tol)
161                }
162                None => Ok(None),
163            },
164            (1, [Some(a)]) => pole_gap(model, surface_id, &surface, &rings[0], a, tol),
165            _ => Ok(None),
166        };
167        // A face the repair cannot rebuild is kept unchanged.
168        if let Ok(Some(rebuilt)) = repaired {
169            settled.extend(
170                explore_unique(model, &face, ShapeType::Edge)?
171                    .iter()
172                    .map(Shape::node),
173            );
174            face_map.insert(face.node(), rebuilt);
175        }
176    }
177    if face_map.is_empty() {
178        return Ok((Built::new(shape.clone(), History::new()), 0));
179    }
180    let repaired = face_map.len();
181
182    // Neighbours of a re-anchored ring use its new edge.
183    let mut history = History::new();
184    for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
185        if face_map.contains_key(&face.node()) {
186            continue;
187        }
188        let uses_any = explore_unique(model, &face, ShapeType::Edge)?
189            .iter()
190            .any(|e| substitution.contains_key(&e.node()));
191        if uses_any {
192            let rebuilt = rebuild_with(model, &face, &substitution, tol)?;
193            face_map.insert(face.node(), rebuilt);
194        }
195    }
196    for face in explore_unique(model, shape, ShapeType::Face)? {
197        if let Some(rebuilt) = face_map.get(&face.node()) {
198            let oriented = if face.orientation() == ogeom_topo::Orientation::Reversed {
199                rebuilt.reversed()
200            } else {
201                rebuilt.clone()
202            };
203            history.modify(&face, oriented);
204        }
205    }
206    let mut shells = Vec::new();
207    for shell in explore_unique(model, shape, ShapeType::Shell)? {
208        let faces: Vec<Shape> = explore(model, &shell, Filter::OfType(ShapeType::Face))?
209            .into_iter()
210            .map(|f| {
211                face_map.get(&f.node()).map_or(f.clone(), |n| {
212                    if f.orientation() == ogeom_topo::Orientation::Reversed {
213                        n.reversed()
214                    } else {
215                        n.clone()
216                    }
217                })
218            })
219            .collect();
220        let rebuilt = make_shell(model, &faces)?.shape;
221        if !is_shell_closed(model, &rebuilt)? {
222            ogeom_bail!(
223                Construction,
224                "seaming left the shell open; the shape resists this repair"
225            );
226        }
227        history.modify(&shell, rebuilt.clone());
228        shells.push(rebuilt);
229    }
230    let solid = make_solid(model, &shells)?.shape;
231    history.modify(shape, solid.clone());
232    Ok((Built::new(solid, history), repaired))
233}
234
235/// The face's surface, where it closes in `u` only, is a cylinder, cone or
236/// sphere, and the face has no seam on it.
237fn seamless_periodic(
238    model: &Model,
239    face: &Shape,
240) -> OgeomResult<Option<(ogeom_topo::SurfaceId, SurfaceGeometry)>> {
241    let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
242        return Ok(None);
243    };
244    let Some(surface) = model.geometry().surface(data.surface) else {
245        return Ok(None);
246    };
247    if !surface.is_periodic_u()
248        || surface.is_periodic_v()
249        || !matches!(
250            surface,
251            SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_) | SurfaceGeometry::Sphere(_)
252        )
253    {
254        return Ok(None);
255    }
256    for wire in model.ordered_children_of(face)? {
257        let edges = model.ordered_children_of(&wire)?;
258        for edge in &edges {
259            if edges.iter().filter(|e| e.node() == edge.node()).count() > 1 {
260                return Ok(None);
261            }
262            let seam = model
263                .node(edge)
264                .and_then(|n| n.data().as_edge())
265                .and_then(|d| d.pcurve_for(data.surface, edge.location()))
266                .is_some_and(|r| matches!(r, EdgeRepr::Seam { .. }));
267            if seam {
268                return Ok(None);
269            }
270        }
271    }
272    Ok(Some((data.surface, surface.clone())))
273}
274
275/// A ring walked through its stored pcurves, each piece moved by whole
276/// periods to continue the one before.
277struct Walk {
278    /// Each occurrence's samples, first to last.
279    samples: Vec<Vec<Point2>>,
280    /// Whether the walk goes once round in `u`.
281    wraps: bool,
282    /// How far round it goes: plus or minus the period, or less.
283    turn: f64,
284    span: f64,
285    /// The surface the pcurves were read on.
286    surface: ogeom_topo::SurfaceId,
287    /// How far in `u` each occurrence's stored pcurve was moved.
288    shifts: Vec<f64>,
289}
290
291impl Walk {
292    /// Occurrence `i`'s start, in the walk's unwrapped chart.
293    fn start(&self, i: usize) -> Point2 {
294        self.samples[i][0]
295    }
296
297    /// Whether the column through occurrence `i`'s start meets the ring
298    /// there only: every other sample stands strictly between the column
299    /// and its copy a turn along.
300    fn passes_once_at(&self, i: usize) -> bool {
301        let from = self.start(i).x;
302        let n = self.samples.len();
303        let margin = 1e-6 * self.span;
304        for k in 0..n {
305            let occurrence = (i + k) % n;
306            let shift = if i + k >= n { self.turn } else { 0.0 };
307            for (j, p) in self.samples[occurrence].iter().enumerate() {
308                if k == 0 && j == 0 {
309                    continue;
310                }
311                if k == n - 1 && j == self.samples[occurrence].len() - 1 {
312                    continue;
313                }
314                let along = (p.x + shift - from) / self.turn;
315                if along * self.span.abs() <= margin || (1.0 - along) * self.span <= margin {
316                    return false;
317                }
318            }
319        }
320        true
321    }
322}
323
324fn walk(
325    model: &Model,
326    surface_id: ogeom_topo::SurfaceId,
327    surface: &SurfaceGeometry,
328    ring: &[Shape],
329    tol: Tolerances,
330) -> OgeomResult<Option<Walk>> {
331    const SAMPLES: u32 = 16;
332    let ((ua, ub), _) = surface.domain();
333    let span = ub - ua;
334    let mut samples: Vec<Vec<Point2>> = Vec::with_capacity(ring.len());
335    let mut shifts = Vec::with_capacity(ring.len());
336    let mut at: Option<Point2> = None;
337    for occurrence in ring {
338        let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
339            return Ok(None);
340        };
341        let Some(EdgeRepr::PCurve { curve, range, .. }) =
342            data.pcurve_for(surface_id, occurrence.location())
343        else {
344            return Ok(None);
345        };
346        let Some(pcurve) = model.geometry().pcurve(*curve) else {
347            return Ok(None);
348        };
349        let reversed = occurrence.orientation() == ogeom_topo::Orientation::Reversed;
350        let mut piece = Vec::with_capacity(SAMPLES as usize + 1);
351        for k in 0..=SAMPLES {
352            let f = f64::from(k) / f64::from(SAMPLES);
353            let t = if reversed {
354                range.1 - (range.1 - range.0) * f
355            } else {
356                range.0 + (range.1 - range.0) * f
357            };
358            piece.push(pcurve.point_at(t, tol)?);
359        }
360        let shift = at.map_or(0.0, |at| ((at.x - piece[0].x) / span).round() * span);
361        for p in &mut piece {
362            p.x += shift;
363        }
364        shifts.push(shift);
365        at = piece.last().copied();
366        samples.push(piece);
367    }
368    let (Some(first), Some(last)) = (samples.first(), at) else {
369        return Ok(None);
370    };
371    let turn = last.x - first[0].x;
372    let wraps = (turn.abs() - span).abs() <= 1e-6 * span;
373    Ok(Some(Walk {
374        samples,
375        wraps,
376        turn,
377        span,
378        surface: surface_id,
379        shifts,
380    }))
381}
382
383/// A ring's single closed circle edge, if that is all it is.
384fn lone_circle(model: &Model, ring: &[Shape]) -> Option<(Curve, (f64, f64))> {
385    let [edge] = ring else {
386        return None;
387    };
388    let data = model.node(edge).and_then(|n| n.data().as_edge())?;
389    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
390        return None;
391    };
392    let curve = model.geometry().curve(*curve)?;
393    matches!(curve, Curve::Circle(_)).then(|| (curve.clone(), *range))
394}
395
396/// A band between two wrapping rings, seamed at a column clear of every
397/// vertex where each ring passes once: a lone circle is re-anchored there,
398/// any other ring has the edge crossing it split.
399fn band(
400    model: &mut Model,
401    surface: &SurfaceGeometry,
402    rings: [&[Shape]; 2],
403    walks: [&Walk; 2],
404    (substitution, settled): (
405        &mut FastMap<TShapeId, Vec<Shape>>,
406        &ogeom_core::FastSet<TShapeId>,
407    ),
408    tol: Tolerances,
409) -> OgeomResult<Option<Shape>> {
410    let span = walks[0].span;
411    // An edge a rebuilt face holds cannot be replaced.
412    if rings
413        .iter()
414        .flat_map(|r| r.iter())
415        .any(|e| settled.contains(&e.node()))
416    {
417        return Ok(None);
418    }
419    // Candidate columns midway between neighbouring vertex columns, the
420    // widest gaps first.
421    let mut columns: Vec<f64> = walks
422        .iter()
423        .flat_map(|w| (0..w.samples.len()).map(|i| w.start(i).x.rem_euclid(span)))
424        .collect();
425    columns.sort_by(f64::total_cmp);
426    columns.dedup_by(|a, b| (*a - *b).abs() <= 1e-9 * span);
427    let mut gaps: Vec<(f64, f64)> = (0..columns.len())
428        .map(|k| {
429            let (a, b) = (
430                columns[k],
431                columns.get(k + 1).copied().unwrap_or(columns[0] + span),
432            );
433            (b - a, f64::midpoint(a, b))
434        })
435        .collect();
436    gaps.sort_by(|x, y| y.0.total_cmp(&x.0));
437    for (_, column) in gaps {
438        let plans = [
439            plan(model, rings[0], walks[0], column, tol)?,
440            plan(model, rings[1], walks[1], column, tol)?,
441        ];
442        let [Some(first), Some(second)] = plans else {
443            continue;
444        };
445        let mut built = Vec::with_capacity(2);
446        let mut added = Vec::new();
447        for (ring, planned) in rings.iter().zip([first, second]) {
448            let (ring, replaced) =
449                apply(model, surface, walks[0].surface, ring, planned, column, tol)?;
450            built.push(ring);
451            added.extend(replaced);
452        }
453        match make_band_of_rings(model, surface, &built[0], &built[1], tol) {
454            Ok(face) => {
455                substitution.extend(added);
456                return Ok(Some(face));
457            }
458            Err(_) => continue,
459        }
460    }
461    Ok(None)
462}
463
464/// How one ring comes to have a vertex on a column.
465enum Plan {
466    /// A lone circle, anchored anew there.
467    Reanchor,
468    /// Occurrence `k` crosses there, at its stored pcurve's parameter `t`.
469    Split { k: usize, t: f64 },
470}
471
472/// How a ring gets a vertex on `column`, where it passes the column once
473/// and not at a vertex.
474fn plan(
475    model: &Model,
476    ring: &[Shape],
477    walk: &Walk,
478    column: f64,
479    tol: Tolerances,
480) -> OgeomResult<Option<Plan>> {
481    if lone_circle(model, ring).is_some() {
482        return Ok(Some(Plan::Reanchor));
483    }
484    let span = walk.span;
485    let level = |p: Point2| ((p.x - column) / span).floor();
486    let margin = 1e-6;
487    let mut crossing = None;
488    let mut crossings = 0.0;
489    for (k, piece) in walk.samples.iter().enumerate() {
490        for pair in piece.windows(2) {
491            let near = |p: Point2| {
492                let g = (p.x - column) / span;
493                (g - g.round()).abs() <= margin
494            };
495            if near(pair[0]) || near(pair[1]) {
496                return Ok(None);
497            }
498            let steps = (level(pair[1]) - level(pair[0])).abs();
499            if steps > 0.0 {
500                crossings += steps;
501                crossing = Some((k, pair[0], pair[1]));
502            }
503        }
504    }
505    let Some((k, _, _)) = crossing else {
506        return Ok(None);
507    };
508    if crossings != 1.0 {
509        return Ok(None);
510    }
511    // Where along the occurrence's stored pcurve the column falls.
512    let Some((pcurve, range, shift)) = stored_piece(model, walk, ring, k)? else {
513        return Ok(None);
514    };
515    let target = {
516        let at = pcurve.point_at(range.0, tol)?.x + shift;
517        column + ((at - column) / span).round() * span
518    };
519    let u = |t: f64| -> OgeomResult<f64> { Ok(pcurve.point_at(t, tol)?.x + shift) };
520    let (mut lo, mut hi) = range;
521    let (mut f_lo, f_hi) = (u(lo)? - target, u(hi)? - target);
522    if f_lo.signum() == f_hi.signum() {
523        return Ok(None);
524    }
525    for _ in 0..80 {
526        let mid = f64::midpoint(lo, hi);
527        let f_mid = u(mid)? - target;
528        if f_mid.signum() == f_lo.signum() {
529            lo = mid;
530            f_lo = f_mid;
531        } else {
532            hi = mid;
533        }
534    }
535    Ok(Some(Plan::Split {
536        k,
537        t: f64::midpoint(lo, hi),
538    }))
539}
540
541/// A stored pcurve, its range, and the shift a walk moved it by.
542type StoredPiece = (ogeom_geom::PlanarCurve, (f64, f64), f64);
543
544/// Occurrence `k`'s stored pcurve, its range, and the shift the walk moved
545/// it by.
546fn stored_piece(
547    model: &Model,
548    walk: &Walk,
549    ring: &[Shape],
550    k: usize,
551) -> OgeomResult<Option<StoredPiece>> {
552    let occurrence = &ring[k];
553    let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
554        return Ok(None);
555    };
556    let surface = walk.surface;
557    let Some(EdgeRepr::PCurve { curve, range, .. }) =
558        data.pcurve_for(surface, occurrence.location())
559    else {
560        return Ok(None);
561    };
562    let Some(pcurve) = model.geometry().pcurve(*curve) else {
563        return Ok(None);
564    };
565    Ok(Some((pcurve.clone(), *range, walk.shifts[k])))
566}
567
568/// Give a ring its vertex on `column` as planned: the ring's occurrences
569/// with the new edges in place, starting at that vertex, and what each
570/// replaced edge became, in its stored direction.
571#[allow(clippy::type_complexity)]
572fn apply(
573    model: &mut Model,
574    surface: &SurfaceGeometry,
575    surface_id: ogeom_topo::SurfaceId,
576    ring: &[Shape],
577    planned: Plan,
578    column: f64,
579    tol: Tolerances,
580) -> OgeomResult<(Vec<Shape>, Vec<(TShapeId, Vec<Shape>)>)> {
581    let edge_curve = |model: &Model, edge: &Shape| -> OgeomResult<(Curve, (f64, f64))> {
582        let Some(EdgeRepr::Curve3d { curve, range, .. }) = model
583            .node(edge)
584            .and_then(|n| n.data().as_edge())
585            .and_then(|d| d.curve3d())
586        else {
587            ogeom_bail!(Construction, "a ring edge has no curve");
588        };
589        let Some(geometry) = model.geometry().curve(*curve) else {
590            ogeom_bail!(Dangling, "curve is not in this model");
591        };
592        Ok((geometry.clone(), *range))
593    };
594    let reversed = |e: &Shape| e.orientation() == ogeom_topo::Orientation::Reversed;
595    match planned {
596        Plan::Reanchor => {
597            let old = &ring[0];
598            let (curve, range) = edge_curve(model, old)?;
599            let row = {
600                let Some((a, _)) = ogeom_algo::edge_vertices(model, old)? else {
601                    ogeom_bail!(Construction, "a ring has no vertex");
602                };
603                let Some(data) = model.node(&a).and_then(|n| n.data().as_vertex()) else {
604                    ogeom_bail!(Dangling, "vertex is not in this model");
605                };
606                ogeom_algo::measure::project_on_surface(surface, data.point, 32, tol)?
607                    .parameters
608                    .1
609            };
610            let target = surface.point_at(column, row, tol)?;
611            let Some(at) = circle_parameter(&curve, target) else {
612                ogeom_bail!(Construction, "an anchor point fell off its circle");
613            };
614            let period = range.1 - range.0;
615            let vertex = make_vertex(model, target).shape;
616            let edge =
617                make_edge_between(model, curve, (at, at + period), &vertex, &vertex, tol)?.shape;
618            let placed = if reversed(old) {
619                edge.reversed()
620            } else {
621                edge.clone()
622            };
623            Ok((vec![placed], vec![(old.node(), vec![edge])]))
624        }
625        Plan::Split { k, t } => {
626            let old = &ring[k];
627            let (curve, range) = edge_curve(model, old)?;
628            // The stored pcurve and the edge share their parameter
629            // proportionally across their ranges.
630            let Some(EdgeRepr::PCurve { range: prange, .. }) = model
631                .node(old)
632                .and_then(|n| n.data().as_edge())
633                .and_then(|d| d.pcurve_for(surface_id, old.location()))
634                .cloned()
635            else {
636                ogeom_bail!(Construction, "a ring edge has no pcurve");
637            };
638            let at = range.0 + (t - prange.0) / (prange.1 - prange.0) * (range.1 - range.0);
639            let bounds = model.children_of(old)?;
640            let (Some(first), Some(last)) = (bounds.first(), bounds.last()) else {
641                ogeom_bail!(Construction, "a ring edge has no vertices");
642            };
643            let point = curve.point_at(at, tol)?;
644            let vertex = make_vertex(model, point).shape;
645            let head =
646                make_edge_between(model, curve.clone(), (range.0, at), first, &vertex, tol)?.shape;
647            let tail = make_edge_between(model, curve, (at, range.1), &vertex, last, tol)?.shape;
648            // In the ring's direction: the occurrence's two halves, the
649            // second starting on the column.
650            let halves = if reversed(old) {
651                [tail.reversed(), head.reversed()]
652            } else {
653                [head.clone(), tail.clone()]
654            };
655            let mut out: Vec<Shape> = Vec::with_capacity(ring.len() + 1);
656            out.push(halves[1].clone());
657            out.extend(ring[k + 1..].iter().cloned());
658            out.extend(ring[..k].iter().cloned());
659            out.push(halves[0].clone());
660            Ok((out, vec![(old.node(), vec![head, tail])]))
661        }
662    }
663}
664
665/// A sphere's cap bounded by one wrapping ring, given from a vertex the
666/// seam may start at: a pole vertex inside it, a degenerate edge there, and
667/// a seam down to the ring.
668fn cap(
669    model: &mut Model,
670    face: &Shape,
671    surface: &SurfaceGeometry,
672    ring: &[Shape],
673    tol: Tolerances,
674) -> OgeomResult<Option<Shape>> {
675    let SurfaceGeometry::Sphere(_) = surface else {
676        return Ok(None);
677    };
678    let half = core::f64::consts::FRAC_PI_2;
679    // The pole the face covers: its mesh reaches that one. The chart does
680    // not close, so asking the face itself of a point is unreliable; its
681    // triangulation works from the edges and is not.
682    let mesh = ogeom_mesh::triangulate(model, face, ogeom_mesh::Deflection::default(), tol)?;
683    let reach = |pole: Point| {
684        mesh.positions
685            .iter()
686            .map(|p| p.distance(pole))
687            .fold(f64::INFINITY, f64::min)
688    };
689    let poles = [
690        surface.point_at(0.0, half, tol)?,
691        surface.point_at(0.0, -half, tol)?,
692    ];
693    let pole = if mesh.positions.is_empty() {
694        None
695    } else if reach(poles[0]) < reach(poles[1]) {
696        Some(poles[0])
697    } else {
698        Some(poles[1])
699    };
700    let Some(pole) = pole else {
701        return Ok(None);
702    };
703    let apex = make_vertex(model, pole).shape;
704    let mut data = EdgeData::new();
705    data.degenerate = true;
706    let degenerate = model.add_edge(data, &[apex.clone(), apex])?;
707    Ok(Some(make_band_of_rings(
708        model,
709        surface,
710        ring,
711        &[degenerate],
712        tol,
713    )?))
714}
715
716/// A sphere's face whose boundary reaches a pole from one column and leaves
717/// it on another: a degenerate edge along the pole's row between them.
718fn pole_gap(
719    model: &mut Model,
720    surface_id: ogeom_topo::SurfaceId,
721    surface: &SurfaceGeometry,
722    ring: &[Shape],
723    walk: &Walk,
724    tol: Tolerances,
725) -> OgeomResult<Option<Shape>> {
726    let SurfaceGeometry::Sphere(_) = surface else {
727        return Ok(None);
728    };
729    let half = core::f64::consts::FRAC_PI_2;
730    let n = ring.len();
731    let mut edges = Vec::with_capacity(n + 1);
732    let mut inserted = false;
733    for (k, occurrence) in ring.iter().enumerate() {
734        edges.push(occurrence.clone());
735        let end = *walk.samples[k].last().unwrap_or(&walk.samples[k][0]);
736        let next = walk.samples[(k + 1) % n][0];
737        // The walk moved each piece to continue the last, so a piece
738        // leaving the pole on another column shows as a jump along the row.
739        // The ring does not go round, so it closes where it started.
740        let gap = next.x - end.x;
741        if (end.y.abs() - half).abs() > 1e-9 || gap.abs() <= 1e-9 * walk.span {
742            continue;
743        }
744        let Some((_, vertex)) = ogeom_algo::edge_vertices(model, occurrence)? else {
745            return Ok(None);
746        };
747        let mut data = EdgeData::new();
748        data.degenerate = true;
749        let degenerate = model.add_edge(data, &[vertex.clone(), vertex])?;
750        // The pole's pcurve joins the stored pieces where they stand.
751        let stored_end = stored_end(model, surface_id, occurrence, tol)?;
752        let Some(stored_end) = stored_end else {
753            return Ok(None);
754        };
755        let pcurve: ogeom_geom::PlanarCurve = ogeom_geom::Line2d::over(
756            Axis2::new(
757                stored_end,
758                Direction2::new(Vector2::new(gap.signum(), 0.0), tol)?,
759            ),
760            0.0,
761            gap.abs(),
762        )?
763        .into();
764        ogeom_algo::attach_pcurve(
765            model,
766            &degenerate,
767            pcurve,
768            surface_id,
769            Location::identity(),
770            (0.0, gap.abs()),
771        )?;
772        edges.push(degenerate);
773        inserted = true;
774    }
775    if !inserted {
776        return Ok(None);
777    }
778    // Which way the closed loop goes round the chart, read on the pcurves
779    // as stored, where the pole's edges join them: clockwise, the face
780    // faces against the surface.
781    let mut area = 0.0;
782    let stored: Vec<Point2> = walk
783        .samples
784        .iter()
785        .zip(&walk.shifts)
786        .flat_map(|(piece, shift)| piece.iter().map(move |p| Point2::new(p.x - shift, p.y)))
787        .collect();
788    for (k, p) in stored.iter().enumerate() {
789        let q = stored[(k + 1) % stored.len()];
790        area += p.x * q.y - q.x * p.y;
791    }
792    let wire = make_wire(model, &edges, tol)?.shape;
793    let face = make_face_on(model, surface_id, &[wire], tol)?.shape;
794    Ok(Some(if area < 0.0 { face.reversed() } else { face }))
795}
796
797/// What [`meridian_poles`] rebuilt: the face, and each split edge's pieces in
798/// its stored direction.
799type Halved = (Shape, Vec<(TShapeId, Vec<Shape>)>);
800
801/// A sphere's face bounded by one ring where an edge on a great circle
802/// through the poles passes a pole inside its range: such a circle has no
803/// single chart image (its longitude jumps by half a turn at the pole), so
804/// any stored pcurve for it is a fit that strays off the meridian. Each such
805/// edge is split at the poles it passes into exact half meridians, and the
806/// pole's row between two pieces gets a degenerate edge.
807///
808/// With `resolved`, `ring` holds pieces another face's repair split, which
809/// take exact pcurves here; without it, the repair splits an edge or does
810/// nothing. Each piece's column is the copy, a whole turn
811/// apart, nearest the longitude of the face's middle (read on its mesh,
812/// which works from the edges' 3D curves). The rebuilt face must mesh to the
813/// old face's area, or the repair is refused.
814fn meridian_poles(
815    model: &mut Model,
816    face: &Shape,
817    (surface_id, surface): (ogeom_topo::SurfaceId, &SurfaceGeometry),
818    ring: &[Shape],
819    settled: &ogeom_core::FastSet<TShapeId>,
820    resolved: bool,
821    tol: Tolerances,
822) -> OgeomResult<Option<Halved>> {
823    let SurfaceGeometry::Sphere(sphere) = surface else {
824        return Ok(None);
825    };
826    if !face.location().is_identity() {
827        return Ok(None);
828    }
829    let sphere = sphere.sphere();
830    let half = core::f64::consts::FRAC_PI_2;
831    let turn = core::f64::consts::TAU;
832    let poles = [
833        surface.point_at(0.0, half, tol)?,
834        surface.point_at(0.0, -half, tol)?,
835    ];
836    let has_pcurve = |model: &Model, e: &Shape| {
837        model
838            .node(e)
839            .and_then(|n| n.data().as_edge())
840            .and_then(|d| d.pcurve_for(surface_id, e.location()))
841            .is_some()
842    };
843    let ring_vertices: Vec<Shape> = ring
844        .iter()
845        .filter_map(|e| ogeom_algo::edge_vertices(model, e).ok().flatten())
846        .flat_map(|(a, b)| [a, b])
847        .collect();
848    let vertex_at = |model: &Model, p: Point| {
849        ring_vertices.iter().find(|v| {
850            model
851                .node(v)
852                .and_then(|n| n.data().as_vertex())
853                .is_some_and(|d| d.point.is_within(p, d.tolerance.get().max(tol.confusion())))
854        })
855    };
856
857    // The ring with each pole-crossing meridian edge split at its poles.
858    let mut pieces: Vec<Shape> = Vec::with_capacity(ring.len() + 2);
859    let mut added: Vec<(TShapeId, Vec<Shape>)> = Vec::new();
860    let mut fresh: Vec<Shape> = Vec::new();
861    for occurrence in ring {
862        let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
863            return Ok(None);
864        };
865        if data.degenerate {
866            pieces.push(occurrence.clone());
867            continue;
868        }
869        // Pieces are built on the curve as stored, so only an edge placed
870        // where its curve lies is split.
871        let Some(EdgeRepr::Curve3d {
872            curve,
873            range,
874            location,
875        }) = data.curve3d()
876        else {
877            return Ok(None);
878        };
879        if !location.is_identity() || !occurrence.location().is_identity() {
880            return Ok(None);
881        }
882        let (curve_id, range) = (*curve, *range);
883        let Some(Curve::Circle(circle)) = model.geometry().curve(curve_id).cloned() else {
884            if !has_pcurve(model, occurrence) {
885                return Ok(None);
886            }
887            pieces.push(occurrence.clone());
888            continue;
889        };
890        let c = circle.circle();
891        let great = c.centre().distance(sphere.centre()) <= tol.confusion()
892            && (c.radius() - sphere.radius()).abs() <= tol.confusion();
893        // The circle's parameters strictly inside the range where it passes
894        // a pole.
895        let mut cuts: Vec<(f64, Point)> = Vec::new();
896        if great {
897            let margin = 1e-9 * (range.1 - range.0).abs().max(1.0);
898            for pole in poles {
899                let Some(base) = circle_parameter(&Curve::Circle(circle), pole) else {
900                    continue;
901                };
902                for t0 in [base, -base] {
903                    let first = t0 + ((range.0 - t0) / turn).ceil() * turn;
904                    let mut t = first;
905                    while t < range.1 - margin {
906                        if t > range.0 + margin
907                            && circle.point_at(t, tol)?.is_within(pole, tol.confusion())
908                            && !cuts.iter().any(|(s, _)| (s - t).abs() <= margin)
909                        {
910                            cuts.push((t, pole));
911                        }
912                        t += turn;
913                    }
914                }
915            }
916        }
917        if cuts.is_empty() {
918            if !has_pcurve(model, occurrence) {
919                fresh.push(occurrence.clone());
920            }
921            pieces.push(occurrence.clone());
922            continue;
923        }
924        if settled.contains(&occurrence.node()) {
925            return Ok(None);
926        }
927        cuts.sort_by(|a, b| a.0.total_cmp(&b.0));
928        let bounds = model.children_of(occurrence)?;
929        let (Some(first), Some(last)) = (bounds.first().cloned(), bounds.last().cloned()) else {
930            return Ok(None);
931        };
932        let mut split = Vec::with_capacity(cuts.len() + 1);
933        let (mut from, mut at) = (first, range.0);
934        for (t, pole) in cuts {
935            let vertex = match vertex_at(model, pole) {
936                Some(v) => v.clone(),
937                None => make_vertex(model, pole).shape,
938            };
939            let edge =
940                make_edge_between(model, Curve::Circle(circle), (at, t), &from, &vertex, tol)?
941                    .shape;
942            split.push(edge);
943            (from, at) = (vertex, t);
944        }
945        let edge = make_edge_between(
946            model,
947            Curve::Circle(circle),
948            (at, range.1),
949            &from,
950            &last,
951            tol,
952        )?
953        .shape;
954        split.push(edge);
955        fresh.extend(split.iter().cloned());
956        if occurrence.orientation() == ogeom_topo::Orientation::Reversed {
957            pieces.extend(split.iter().rev().map(Shape::reversed));
958        } else {
959            pieces.extend(split.iter().cloned());
960        }
961        added.push((occurrence.node(), split));
962    }
963    if fresh.is_empty() || (!resolved && added.is_empty()) {
964        return Ok(None);
965    }
966
967    // The longitude of the face's middle, from its mesh.
968    let old_mesh = ogeom_mesh::triangulate(model, face, ogeom_mesh::Deflection::default(), tol)?;
969    let mut middle = ogeom_math::Vector::new(0.0, 0.0, 0.0);
970    for p in &old_mesh.positions {
971        if let Ok(d) = (*p - sphere.centre()).normalized(tol) {
972            middle += d;
973        }
974    }
975    let local = sphere.frame().to_local(sphere.centre() + middle);
976    if local.x.hypot(local.y) <= 1e-6 * middle.magnitude().max(1.0) {
977        return Ok(None);
978    }
979    let centre_u = local.y.atan2(local.x);
980    for edge in &fresh {
981        let Some(EdgeRepr::Curve3d { curve, range, .. }) = model
982            .node(edge)
983            .and_then(|n| n.data().as_edge())
984            .and_then(|d| d.curve3d())
985            .cloned()
986        else {
987            return Ok(None);
988        };
989        let Some(geometry) = model.geometry().curve(curve).cloned() else {
990            return Ok(None);
991        };
992        let Some(pcurve) = ogeom_intersect::exact_pcurve_over(&geometry, range, surface, tol)
993        else {
994            return Ok(None);
995        };
996        // A meridian's column moved by whole turns to the copy nearest the
997        // face's middle; a parallel spans the whole row and stays.
998        let pcurve = match &pcurve {
999            ogeom_geom::PlanarCurve::Line(l) if l.axis().direction.vector().x.abs() <= 1e-12 => {
1000                let at = l.axis().location;
1001                let shift = ((centre_u - at.x) / turn).round() * turn;
1002                ogeom_geom::Line2d::over(
1003                    Axis2::new(Point2::new(at.x + shift, at.y), l.axis().direction),
1004                    l.domain().0,
1005                    l.domain().1,
1006                )?
1007                .into()
1008            }
1009            _ => pcurve,
1010        };
1011        ogeom_algo::attach_pcurve(model, edge, pcurve, surface_id, Location::identity(), range)?;
1012    }
1013
1014    // Each piece's stored ends; where two pieces meet on a pole's row at
1015    // different columns, a degenerate edge along the row joins them.
1016    let mut ends: Vec<(Point2, Point2)> = Vec::with_capacity(pieces.len());
1017    let mut samples: Vec<Point2> = Vec::new();
1018    for piece in &pieces {
1019        let Some(EdgeRepr::PCurve { curve, range, .. }) = model
1020            .node(piece)
1021            .and_then(|n| n.data().as_edge())
1022            .and_then(|d| d.pcurve_for(surface_id, piece.location()))
1023            .cloned()
1024        else {
1025            return Ok(None);
1026        };
1027        let Some(pcurve) = model.geometry().pcurve(curve).cloned() else {
1028            return Ok(None);
1029        };
1030        let reversed = piece.orientation() == ogeom_topo::Orientation::Reversed;
1031        const SAMPLES: u32 = 16;
1032        let mut run = Vec::with_capacity(SAMPLES as usize + 1);
1033        for k in 0..=SAMPLES {
1034            let f = f64::from(k) / f64::from(SAMPLES);
1035            let t = if reversed {
1036                range.1 - (range.1 - range.0) * f
1037            } else {
1038                range.0 + (range.1 - range.0) * f
1039            };
1040            run.push(pcurve.point_at(t, tol)?);
1041        }
1042        ends.push((run[0], run[run.len() - 1]));
1043        samples.extend(run);
1044    }
1045    let n = pieces.len();
1046    let joint = tol.parametric().max(1e-9);
1047    let mut edges = Vec::with_capacity(n + 2);
1048    for k in 0..n {
1049        edges.push(pieces[k].clone());
1050        let (end, next) = (ends[k].1, ends[(k + 1) % n].0);
1051        if end.distance(next) <= joint {
1052            continue;
1053        }
1054        if (end.y - next.y).abs() > joint || (end.y.abs() - half).abs() > joint {
1055            return Ok(None);
1056        }
1057        let Some((_, vertex)) = ogeom_algo::edge_vertices(model, &pieces[k])? else {
1058            return Ok(None);
1059        };
1060        let gap = next.x - end.x;
1061        let mut data = EdgeData::new();
1062        data.degenerate = true;
1063        let degenerate = model.add_edge(data, &[vertex.clone(), vertex])?;
1064        let pcurve: ogeom_geom::PlanarCurve = ogeom_geom::Line2d::over(
1065            Axis2::new(end, Direction2::new(Vector2::new(gap.signum(), 0.0), tol)?),
1066            0.0,
1067            gap.abs(),
1068        )?
1069        .into();
1070        ogeom_algo::attach_pcurve(
1071            model,
1072            &degenerate,
1073            pcurve,
1074            surface_id,
1075            Location::identity(),
1076            (0.0, gap.abs()),
1077        )?;
1078        edges.push(degenerate);
1079    }
1080    // Clockwise in the chart, the face faces against the surface, as for a
1081    // pole gap.
1082    let mut area = 0.0;
1083    for (k, p) in samples.iter().enumerate() {
1084        let q = samples[(k + 1) % samples.len()];
1085        area += p.x * q.y - q.x * p.y;
1086    }
1087    let wire = make_wire(model, &edges, tol)?.shape;
1088    let rebuilt = make_face_on(model, surface_id, &[wire], tol)?.shape;
1089    let rebuilt = if area < 0.0 {
1090        rebuilt.reversed()
1091    } else {
1092        rebuilt
1093    };
1094    let new_mesh =
1095        ogeom_mesh::triangulate(model, &rebuilt, ogeom_mesh::Deflection::default(), tol)?;
1096    let (before, after) = (old_mesh.area(), new_mesh.area());
1097    if (before - after).abs() > 1e-2 * before.max(after) {
1098        return Ok(None);
1099    }
1100    Ok(Some((rebuilt, added)))
1101}
1102
1103/// Where an occurrence's stored pcurve ends, as stored.
1104fn stored_end(
1105    model: &Model,
1106    surface_id: ogeom_topo::SurfaceId,
1107    occurrence: &Shape,
1108    tol: Tolerances,
1109) -> OgeomResult<Option<Point2>> {
1110    let Some(EdgeRepr::PCurve { curve, range, .. }) = model
1111        .node(occurrence)
1112        .and_then(|n| n.data().as_edge())
1113        .and_then(|d| d.pcurve_for(surface_id, occurrence.location()))
1114    else {
1115        return Ok(None);
1116    };
1117    let Some(pcurve) = model.geometry().pcurve(*curve) else {
1118        return Ok(None);
1119    };
1120    let t = if occurrence.orientation() == ogeom_topo::Orientation::Reversed {
1121        range.0
1122    } else {
1123        range.1
1124    };
1125    Ok(Some(pcurve.point_at(t, tol)?))
1126}
1127
1128/// Rebuild a face around replaced edges, each by the edges it became (in
1129/// its stored direction), pcurves on the face's surface found in closed
1130/// form for the new ones.
1131fn rebuild_with(
1132    model: &mut Model,
1133    face: &Shape,
1134    substitution: &FastMap<TShapeId, Vec<Shape>>,
1135    tol: Tolerances,
1136) -> OgeomResult<Shape> {
1137    let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
1138        ogeom_bail!(Dangling, "face is not in this model");
1139    };
1140    let surface_id = data.surface;
1141    let Some(surface) = model.geometry().surface(surface_id).cloned() else {
1142        ogeom_bail!(Dangling, "face refers to a surface not in this model");
1143    };
1144    let mut wires = Vec::new();
1145    let mut replaced = Vec::new();
1146    // The wires as the face stores them: the caller turns the rebuilt face
1147    // as the old one was turned.
1148    for wire in model.ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))? {
1149        let mut edges = Vec::new();
1150        for edge in model.ordered_children_of(&wire)? {
1151            match substitution.get(&edge.node()) {
1152                Some(new_edges) => {
1153                    replaced.extend(new_edges.iter().cloned());
1154                    if edge.orientation() == ogeom_topo::Orientation::Reversed {
1155                        edges.extend(new_edges.iter().rev().map(Shape::reversed));
1156                    } else {
1157                        edges.extend(new_edges.iter().cloned());
1158                    }
1159                }
1160                None => edges.push(edge),
1161            }
1162        }
1163        wires.push(make_wire(model, &edges, tol)?.shape);
1164    }
1165    attach_face_pcurves(model, &surface, surface_id, &replaced, tol)?;
1166    Ok(make_face_on(model, surface_id, &wires, tol)?.shape)
1167}