Skip to main content

geop_ops_sheetmetal/
layout.rs

1//! The sheet laid out flat — [`Layout`] — and cut.
2//!
3//! Every face of a sheet, flat or bent, is a region of one plane in the
4//! flat pattern's coordinates: the first flat's sheet coordinates, every
5//! other flat moved by its shift (see [`Sheet::flat_shifts`]), every bend
6//! the strip between its two lines, as wide as its developed length. The
7//! regions share the edges and vertices between them. A cut is subtracted
8//! there ([`Layout::subtract`]), where a bend is no different from a flat
9//! and every curve still a line or an arc, and then each face is folded
10//! back into its own place (see [`Sheet::folded`]). So a hole across a bend
11//! is cut where it lies unrolled, and the flat pattern of a cut body is
12//! exact: unfolding moves its curves, never approximates them.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use geop_core_geometry::{
17    intersection::curve_curve_overlaps_and_crossings, nurb_curve::NurbCurve2D, search::MAX_NODES,
18};
19use geop_core_math::{
20    geop_error::{GeopError, GeopResult, WithContext},
21    scalars::Scalar,
22    solvers::{
23        probe::{Probe, attempts},
24        walk::{Walked, walk},
25    },
26    vector::{Vector2, Vector3},
27    with_context,
28};
29use geop_core_topology::Sense;
30use geop_ops_model::Namer;
31
32use crate::sheet::Sheet;
33
34/// Where a crossing search between curves that are no lines or arcs hands
35/// over (see `AGENTS.md`): a value of its own, finer than the default
36/// [`geop_core_geometry::search::MIN_SUBDIVISION_SIZE`], with the default
37/// node budget [`MAX_NODES`].
38fn min_subdivision_size() -> Scalar {
39    Scalar::from_f64(1e-9)
40}
41
42/// Which face of the sheet an entity is folded with: a flat's, or a
43/// bend's.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum Home {
46    Flat(usize),
47    Bend(usize),
48}
49
50/// A vertex of the layout.
51#[derive(Clone, Debug)]
52pub struct LayoutVertex {
53    /// Where it is, in the flat pattern's coordinates.
54    pub at: Vector2,
55    /// Its vertices are `N(a)`, `N(b)` — named after an edge starting
56    /// there; `None` until a cut's vertex is.
57    pub name: Option<Namer>,
58    /// A vertex of the sheet as built: its flat, and where it is in that
59    /// flat's own sheet coordinates.
60    pub own: Option<(usize, Vector2)>,
61}
62
63/// An edge of the layout, from vertex `start` to vertex `end`.
64#[derive(Clone, Debug)]
65pub struct LayoutEdge {
66    /// In the flat pattern's coordinates.
67    pub curve: NurbCurve2D,
68    pub start: usize,
69    pub end: usize,
70    /// Its edges `N(a)`, `N(b)`, its wall `N` (see [`crate::thicken`]).
71    pub name: Namer,
72    pub home: Home,
73    /// The curve of the sheet as built, in its flat's own coordinates,
74    /// while no cut has split it.
75    pub own: Option<NurbCurve2D>,
76    /// The key of the sheet's edge it is, or is a piece of — a flat's edge,
77    /// or the side `s0`/`s1` of a bend; `None` for an edge a cut made.
78    pub origin: Option<String>,
79}
80
81/// A face of the layout: its loops, the outer one counter-clockwise and
82/// holes clockwise, each a list of edges and the sense it runs them in.
83#[derive(Clone, Debug)]
84pub struct LayoutFace {
85    pub home: Home,
86    pub outer: Vec<(usize, Sense)>,
87    pub holes: Vec<Vec<(usize, Sense)>>,
88    /// Its faces are `N(a)` and `N(b)`.
89    pub name: Namer,
90}
91
92impl LayoutFace {
93    pub fn loops(&self) -> impl Iterator<Item = &Vec<(usize, Sense)>> {
94        std::iter::once(&self.outer).chain(&self.holes)
95    }
96}
97
98/// A sheet laid out flat (see the module docs). Edges and vertices a cut
99/// took away stay in the lists, used by no face.
100#[derive(Clone, Debug, Default)]
101pub struct Layout {
102    pub vertices: Vec<LayoutVertex>,
103    pub edges: Vec<LayoutEdge>,
104    pub faces: Vec<LayoutFace>,
105    /// The keys of the sheet's edges a cut split or took away.
106    pub changed: BTreeSet<String>,
107    /// Per bend, the vertices at its corners as built — where its parent
108    /// edge starts and ends, where its child edge ends and starts — which
109    /// stay in the list when a cut takes them away.
110    pub corners: Vec<[usize; 4]>,
111}
112
113/// A curve of a cut's loop, in the flat pattern's coordinates, and its
114/// name.
115#[derive(Clone, Debug)]
116pub struct CutCurve {
117    pub curve: NurbCurve2D,
118    pub name: Namer,
119}
120
121impl Sheet {
122    /// The sheet laid out flat, its cuts subtracted (see the module docs).
123    pub fn layout(&self) -> GeopResult<Layout> {
124        let shifts = self.flat_shifts()?;
125        let mut layout = Layout::default();
126        let mut edge_of: BTreeMap<String, usize> = BTreeMap::new();
127        for (f, flat) in self.flats.iter().enumerate() {
128            let ctx = with_context!("flat {}", flat.name.root());
129            let mut loops = Vec::new();
130            for lp in std::iter::once(&flat.outer).chain(&flat.holes) {
131                let first = layout.vertices.len();
132                let n = lp.len();
133                let mut coedges = Vec::new();
134                for (k, e) in lp.iter().enumerate() {
135                    let curve = e.curve.translate(shifts[f]);
136                    layout.vertices.push(LayoutVertex {
137                        at: curve.start()?,
138                        name: Some(e.name.scoped("v")),
139                        own: Some((f, e.curve.start()?)),
140                    });
141                    layout.edges.push(LayoutEdge {
142                        curve,
143                        start: first + k,
144                        end: first + (k + 1) % n,
145                        name: e.name.clone(),
146                        home: Home::Flat(f),
147                        own: Some(e.curve.clone()),
148                        origin: Some(e.key()),
149                    });
150                    let index = layout.edges.len() - 1;
151                    if edge_of.insert(e.key(), index).is_some() {
152                        return Err(GeopError::new(format!("two edges are named {}", e.key())))
153                            .with_context(ctx);
154                    }
155                    coedges.push((index, Sense::Forward));
156                }
157                loops.push(coedges);
158            }
159            let outer = loops.remove(0);
160            layout.faces.push(LayoutFace {
161                home: Home::Flat(f),
162                outer,
163                holes: loops,
164                name: flat.name.clone(),
165            });
166        }
167        for (b, bend) in self.bends.iter().enumerate() {
168            let edge = |key: &str| {
169                edge_of.get(key).copied().ok_or_else(|| {
170                    GeopError::new(format!("bend {} meets no edge {key}", bend.name.root()))
171                })
172            };
173            let (pe, ce) = (edge(&bend.parent_edge)?, edge(&bend.child_edge)?);
174            let (pa, pb) = (layout.edges[pe].start, layout.edges[pe].end);
175            let (cb, ca) = (layout.edges[ce].start, layout.edges[ce].end);
176            layout.corners.push([pa, pb, ca, cb]);
177            for (side, start, end) in [("s0", pa, ca), ("s1", pb, cb)] {
178                let name = bend.name.scoped(side);
179                layout.edges.push(LayoutEdge {
180                    curve: NurbCurve2D::line(layout.vertices[start].at, layout.vertices[end].at)?,
181                    start,
182                    end,
183                    origin: Some(name.root()),
184                    name,
185                    home: Home::Bend(b),
186                    own: None,
187                });
188            }
189            let n = layout.edges.len();
190            layout.faces.push(LayoutFace {
191                home: Home::Bend(b),
192                outer: vec![
193                    (n - 2, Sense::Forward),
194                    (ce, Sense::Reversed),
195                    (n - 1, Sense::Reversed),
196                    (pe, Sense::Reversed),
197                ],
198                holes: Vec::new(),
199                name: bend.name.clone(),
200            });
201        }
202        for cut in &self.cuts {
203            let ctx = with_context!("cut {}", cut.name.root());
204            let loops = cut
205                .loops
206                .iter()
207                .map(|lp| {
208                    lp.iter()
209                        .map(|e| {
210                            Ok(CutCurve {
211                                curve: e.curve.translate(shifts[cut.flat]),
212                                name: e.name.clone(),
213                            })
214                        })
215                        .collect::<GeopResult<Vec<_>>>()
216                })
217                .collect::<GeopResult<Vec<_>>>()
218                .with_context(ctx)?;
219            layout.subtract(&loops).with_context(ctx)?;
220        }
221        Ok(layout)
222    }
223}
224
225impl Layout {
226    /// The edges some face uses, in order.
227    pub fn used_edges(&self) -> Vec<usize> {
228        let used: BTreeSet<usize> = self
229            .faces
230            .iter()
231            .flat_map(|f| f.loops().flatten().map(|&(e, _)| e))
232            .collect();
233        used.into_iter().collect()
234    }
235
236    /// Takes the area inside `cut` — closed loops, counter-clockwise, apart
237    /// from each other — out of the faces: their edges inside it go, the
238    /// cut's curves inside a face become its edges, every curve split where
239    /// they cross, or where a joint of the cut lies on one. Refuses, naming
240    /// the curves, a cut that crosses an edge at its end, touches one,
241    /// runs along one, or crosses one that is no line or arc; and one that
242    /// splits a face in two, takes one away entirely, or misses the sheet.
243    pub fn subtract(&mut self, cut: &[Vec<CutCurve>]) -> GeopResult<()> {
244        let used = self.used_edges();
245        let segs: BTreeMap<usize, Seg> = used
246            .iter()
247            .map(|&e| Ok((e, Seg::of(&self.edges[e].curve)?)))
248            .collect::<GeopResult<_>>()?;
249        let cut_segs: Vec<Vec<Seg>> = cut
250            .iter()
251            .map(|lp| lp.iter().map(|c| Seg::of(&c.curve)).collect())
252            .collect::<GeopResult<_>>()?;
253        // What the faces were before the cut, to place its curves in.
254        let regions: Vec<Vec<Vec<Piece<'_>>>> = self
255            .faces
256            .iter()
257            .map(|face| {
258                face.loops()
259                    .map(|lp| {
260                        lp.iter()
261                            .map(|&(e, _)| Piece {
262                                seg: &segs[&e],
263                                curve: &self.edges[e].curve,
264                            })
265                            .collect()
266                    })
267                    .collect()
268            })
269            .collect();
270        let cut_region: Vec<Vec<Piece<'_>>> = cut
271            .iter()
272            .zip(&cut_segs)
273            .map(|(lp, segs)| {
274                lp.iter()
275                    .zip(segs)
276                    .map(|(c, seg)| Piece {
277                        seg,
278                        curve: &c.curve,
279                    })
280                    .collect()
281            })
282            .collect();
283
284        // The joints of the cut's loops — joint `i` where its curve `i`
285        // starts — and those of them on an edge, where the loop crosses
286        // it: a vertex each, which splits the edge there.
287        let mut vertices = self.vertices.clone();
288        let joints: Vec<Vec<usize>> = cut
289            .iter()
290            .map(|lp| {
291                lp.iter()
292                    .map(|c| {
293                        vertices.push(LayoutVertex {
294                            at: c.curve.start()?,
295                            name: None,
296                            own: None,
297                        });
298                        Ok(vertices.len() - 1)
299                    })
300                    .collect()
301            })
302            .collect::<GeopResult<_>>()?;
303        // Where the cut's curves cross the edges between their ends: a
304        // vertex each, at its place along both.
305        let mut on_edge: BTreeMap<usize, Vec<(Scalar, usize)>> = BTreeMap::new();
306        let mut on_cut: Vec<Vec<Vec<(Scalar, usize)>>> =
307            cut.iter().map(|lp| vec![Vec::new(); lp.len()]).collect();
308        for &e in &used {
309            let edge = &self.edges[e];
310            for (l, lp) in cut.iter().enumerate() {
311                for (i, c) in lp.iter().enumerate() {
312                    if !edge.curve.could_overlap(&c.curve) {
313                        continue;
314                    }
315                    let found = crossings(&segs[&e], &cut_segs[l][i], &edge.curve, &c.curve)
316                        .map_err(|why| {
317                            GeopError::new(format!(
318                                "the cut's curve {} {why} the edge {}",
319                                c.name.root(),
320                                edge.name.root()
321                            ))
322                        })?;
323                    for (along_edge, along_cut, at) in found {
324                        vertices.push(LayoutVertex {
325                            at,
326                            name: None,
327                            own: None,
328                        });
329                        on_edge
330                            .entry(e)
331                            .or_default()
332                            .push((along_edge, vertices.len() - 1));
333                        on_cut[l][i].push((along_cut, vertices.len() - 1));
334                    }
335                }
336            }
337        }
338        // The joints on an edge.
339        for &e in &used {
340            let edge = &self.edges[e];
341            for (l, lp) in cut.iter().enumerate() {
342                for (i, c) in lp.iter().enumerate() {
343                    let into = &lp[(i + lp.len() - 1) % lp.len()];
344                    let joint = joints[l][i];
345                    let along = joint_on(&segs[&e], &vertices[joint].at, &into.curve, &c.curve)
346                        .map_err(|why| {
347                            GeopError::new(format!(
348                                "the cut's curves {} and {} {why} the edge {}, where they meet",
349                                into.name.root(),
350                                c.name.root(),
351                                edge.name.root()
352                            ))
353                        })?;
354                    if let Some(along) = along {
355                        on_edge.entry(e).or_default().push((along, joint));
356                    }
357                }
358            }
359        }
360
361        // The edges split where the cut crosses them.
362        let mut edges = self.edges.clone();
363        let mut pieces_of: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
364        let mut changed = self.changed.clone();
365        for (&e, crossed) in &mut on_edge {
366            let edge = &self.edges[e];
367            let points = ordered(crossed, &edge.name)?;
368            let mut stops = vec![edge.start];
369            stops.extend(points);
370            stops.push(edge.end);
371            let ats: Vec<Vector2> = stops.iter().map(|&v| vertices[v].at).collect();
372            let curves = segs[&e].split(&ats)?;
373            let mut pieces = Vec::new();
374            for (k, curve) in curves.into_iter().enumerate() {
375                edges.push(LayoutEdge {
376                    curve,
377                    start: stops[k],
378                    end: stops[k + 1],
379                    name: edge.name.scoped(&k.to_string()),
380                    home: edge.home,
381                    own: None,
382                    origin: edge.origin.clone(),
383                });
384                pieces.push(edges.len() - 1);
385            }
386            changed.extend(edge.origin.clone());
387            pieces_of.insert(e, pieces);
388        }
389
390        // The cut's curves split where they cross the edges, each piece
391        // run backwards — the cut's area on its right, the face's left —
392        // and placed in the face it lies in, if any.
393        let mut added: Vec<Vec<usize>> = vec![Vec::new(); self.faces.len()];
394        for (l, lp) in cut.iter().enumerate() {
395            let joints = &joints[l];
396            for (i, c) in lp.iter().enumerate() {
397                let points = ordered(&mut on_cut[l][i], &c.name)?;
398                let mut stops = vec![joints[i]];
399                stops.extend(points);
400                stops.push(joints[(i + 1) % lp.len()]);
401                let ats: Vec<Vector2> = stops.iter().map(|&v| vertices[v].at).collect();
402                let curves = cut_segs[l][i].split(&ats)?;
403                let split = curves.len() > 1;
404                for (k, curve) in curves.into_iter().enumerate() {
405                    let seg = Seg::of(&curve)?;
406                    let middle = seg.middle(&curve)?;
407                    let mut home = None;
408                    for (f, region) in regions.iter().enumerate() {
409                        if contains(region, &middle).map_err(|why| {
410                            GeopError::new(format!(
411                                "the cut's curve {} {why} face {}",
412                                c.name.root(),
413                                self.faces[f].name.root()
414                            ))
415                        })? {
416                            home = Some(f);
417                            break;
418                        }
419                    }
420                    let Some(f) = home else {
421                        continue;
422                    };
423                    edges.push(LayoutEdge {
424                        curve: curve.reverse(),
425                        start: stops[k + 1],
426                        end: stops[k],
427                        name: if split {
428                            c.name.scoped(&k.to_string())
429                        } else {
430                            c.name.clone()
431                        },
432                        home: self.faces[f].home,
433                        own: None,
434                        origin: None,
435                    });
436                    added[f].push(edges.len() - 1);
437                }
438            }
439        }
440
441        // Each face's loops: what is left of its edges, and the cut's
442        // pieces in it, joined end to start.
443        let mut kept: BTreeMap<usize, bool> = BTreeMap::new();
444        let mut faces = Vec::new();
445        let mut any = false;
446        for (f, face) in self.faces.iter().enumerate() {
447            let mut pool: Vec<(usize, Sense)> = Vec::new();
448            let mut touched = !added[f].is_empty();
449            for &(e, sense) in face.loops().flatten() {
450                let pieces = pieces_of.get(&e).cloned().unwrap_or_else(|| vec![e]);
451                touched |= pieces.len() > 1;
452                let ordered: Vec<usize> = match sense {
453                    Sense::Forward => pieces,
454                    Sense::Reversed => pieces.into_iter().rev().collect(),
455                };
456                for p in ordered {
457                    let keep = match kept.get(&p) {
458                        Some(&k) => k,
459                        None => {
460                            let seg = Seg::of(&edges[p].curve)?;
461                            let middle = seg.middle(&edges[p].curve)?;
462                            let inside = contains(&cut_region, &middle).map_err(|why| {
463                                GeopError::new(format!(
464                                    "the edge {} {why} the cut",
465                                    edges[p].name.root()
466                                ))
467                            })?;
468                            kept.insert(p, !inside);
469                            !inside
470                        }
471                    };
472                    if keep {
473                        pool.push((p, sense));
474                    } else {
475                        touched = true;
476                        changed.extend(edges[p].origin.clone());
477                    }
478                }
479            }
480            if !touched {
481                faces.push(face.clone());
482                continue;
483            }
484            any = true;
485            pool.extend(added[f].iter().map(|&e| (e, Sense::Forward)));
486            faces.extend(relink(face, &pool, &edges, &vertices)?);
487        }
488        if !any {
489            return Err(GeopError::new(
490                "the cut lies off the sheet: it takes nothing away",
491            ));
492        }
493
494        // A cut's vertex is named after an edge starting there.
495        for face in &faces {
496            for &(e, sense) in face.loops().flatten() {
497                let v = match sense {
498                    Sense::Forward => edges[e].start,
499                    Sense::Reversed => edges[e].end,
500                };
501                if vertices[v].name.is_none() {
502                    vertices[v].name = Some(edges[e].name.scoped("v"));
503                }
504            }
505        }
506        self.vertices = vertices;
507        self.edges = edges;
508        self.faces = faces;
509        self.changed = changed;
510        Ok(())
511    }
512}
513
514/// The face `face` bounded anew by the coedges `pool`: joined end to start
515/// into loops, those turning counter-clockwise outer loops — of a bend,
516/// one each for the pieces a cut right across leaves of it; of a flat,
517/// only one. Refuses a flat the cut splits, and a face it takes away.
518fn relink(
519    face: &LayoutFace,
520    pool: &[(usize, Sense)],
521    edges: &[LayoutEdge],
522    vertices: &[LayoutVertex],
523) -> GeopResult<Vec<LayoutFace>> {
524    let ends = |&(e, sense): &(usize, Sense)| match sense {
525        Sense::Forward => (edges[e].start, edges[e].end),
526        Sense::Reversed => (edges[e].end, edges[e].start),
527    };
528    let mut leaving: BTreeMap<usize, usize> = BTreeMap::new();
529    for (k, coedge) in pool.iter().enumerate() {
530        let (from, _) = ends(coedge);
531        if leaving.insert(from, k).is_some() {
532            return Err(GeopError::new(format!(
533                "the cut leaves face {} touching itself at a point",
534                face.name.root()
535            )));
536        }
537    }
538    let mut used = vec![false; pool.len()];
539    let mut outer: Vec<Vec<(usize, Sense)>> = Vec::new();
540    let mut holes = Vec::new();
541    for first in 0..pool.len() {
542        if used[first] {
543            continue;
544        }
545        // On from each piece to the one leaving where it ends. One running
546        // into a loop already followed runs round that loop into itself.
547        let around = walk(first, |&k| {
548            let (_, to) = ends(&pool[k]);
549            let next = leaving.get(&to).ok_or_else(|| {
550                GeopError::new(format!(
551                    "the cut leaves face {} open at {:?}",
552                    face.name.root(),
553                    vertices[to].at
554                ))
555            })?;
556            Ok::<_, GeopError>(Some(*next))
557        })?;
558        if around.end != Walked::Closed {
559            return Err(GeopError::new(format!(
560                "the cut leaves face {}'s boundary tangled",
561                face.name.root()
562            )));
563        }
564        for &k in &around.nodes {
565            used[k] = true;
566        }
567        let lp: Vec<(usize, Sense)> = around.nodes.iter().map(|&k| pool[k]).collect();
568        let area = loop_area(&lp, edges)?;
569        if area.definitely_greater(Scalar::ZERO) {
570            outer.push(lp);
571        } else if area.definitely_less(Scalar::ZERO) {
572            holes.push(lp);
573        } else {
574            return Err(GeopError::new(format!(
575                "the cut leaves a sliver of face {}: a loop enclosing {area:?}",
576                face.name.root()
577            )));
578        }
579    }
580    match (outer.len(), face.home) {
581        (0, _) => Err(GeopError::new(format!(
582            "the cut takes face {} away entirely: cut less, or cut before the flange it belongs to",
583            face.name.root()
584        ))),
585        (1, _) => Ok(vec![LayoutFace {
586            home: face.home,
587            outer: outer.remove(0),
588            holes,
589            name: face.name.clone(),
590        }]),
591        // A bend cut right across is two bends, side by side, `N,0`,
592        // `N,1`, ...: each takes the holes inside it.
593        (n, Home::Bend(_)) => {
594            let mut pieces: Vec<LayoutFace> = outer
595                .into_iter()
596                .enumerate()
597                .map(|(k, lp)| LayoutFace {
598                    home: face.home,
599                    outer: lp,
600                    holes: Vec::new(),
601                    name: face.name.scoped(&k.to_string()),
602                })
603                .collect();
604            for hole in holes {
605                let (e, sense) = hole[0];
606                let probe = vertices[match sense {
607                    Sense::Forward => edges[e].start,
608                    Sense::Reversed => edges[e].end,
609                }]
610                .at;
611                let mut home = None;
612                for (k, piece) in pieces.iter().enumerate() {
613                    let segs = piece
614                        .outer
615                        .iter()
616                        .map(|&(e, _)| Seg::of(&edges[e].curve))
617                        .collect::<GeopResult<Vec<_>>>()?;
618                    let boundary = vec![
619                        piece
620                            .outer
621                            .iter()
622                            .zip(&segs)
623                            .map(|(&(e, _), seg)| Piece {
624                                seg,
625                                curve: &edges[e].curve,
626                            })
627                            .collect::<Vec<_>>(),
628                    ];
629                    if contains(&boundary, &probe).map_err(|why| {
630                        GeopError::new(format!(
631                            "a hole of the cut {why} a piece of face {}",
632                            face.name.root()
633                        ))
634                    })? {
635                        home = Some(k);
636                        break;
637                    }
638                }
639                let k = home.ok_or_else(|| {
640                    GeopError::new(format!(
641                        "a hole the cut leaves in face {} lies in none of its {n} pieces",
642                        face.name.root()
643                    ))
644                })?;
645                pieces[k].holes.push(hole);
646            }
647            Ok(pieces)
648        }
649        (n, Home::Flat(_)) => Err(GeopError::new(format!(
650            "the cut splits face {} into {n} pieces: a flat stays in one piece — cut a slot short of its far side",
651            face.name.root()
652        ))),
653    }
654}
655
656/// The vertices `crossed` along a curve, in order of their places along
657/// it — which must be apart.
658fn ordered(crossed: &mut [(Scalar, usize)], name: &Namer) -> GeopResult<Vec<usize>> {
659    crossed.sort_by(|a, b| a.0.to_f64().total_cmp(&b.0.to_f64()));
660    for w in crossed.windows(2) {
661        if !w[1].0.definitely_greater(w[0].0) {
662            return Err(GeopError::new(format!(
663                "the cut crosses {} twice too close together to tell apart",
664                name.root()
665            )));
666        }
667    }
668    Ok(crossed.iter().map(|&(_, v)| v).collect())
669}
670
671/// The area a loop encloses: positive counter-clockwise.
672fn loop_area(lp: &[(usize, Sense)], edges: &[LayoutEdge]) -> GeopResult<Scalar> {
673    let mut area = Scalar::ZERO;
674    for &(e, sense) in lp {
675        let curve = &edges[e].curve;
676        let swept = Seg::of(curve)?.swept_area(curve)?;
677        area = area.add(match sense {
678            Sense::Forward => swept,
679            Sense::Reversed => swept.neg(),
680        });
681    }
682    Ok(area)
683}
684
685/// A curve of the layout, as far as cutting is concerned: a line, an arc
686/// of less than half a turn, or something else, which a cut may not cross.
687#[derive(Clone, Debug)]
688pub enum Seg {
689    Line {
690        a: Vector2,
691        b: Vector2,
692    },
693    /// From `a` to `b` about `center`, counter-clockwise if `ccw`.
694    Arc {
695        center: Vector2,
696        radius: Scalar,
697        a: Vector2,
698        b: Vector2,
699        ccw: bool,
700    },
701    Other,
702}
703
704/// Where a point lies along a line or an arc it is known to lie on the
705/// line or circle of.
706enum Along {
707    /// Strictly between its ends, this far along it.
708    Inside(Scalar),
709    Outside,
710    /// Possibly at an end.
711    Undecided,
712}
713
714impl Seg {
715    /// What `curve` is.
716    pub fn of(curve: &NurbCurve2D) -> GeopResult<Self> {
717        let (a, b) = (curve.start()?, curve.end()?);
718        if curve.degree == 1 && curve.control_points.len() == 2 {
719            return Ok(Seg::Line { a, b });
720        }
721        let flat = curve.embed(&Vector3::zero(), &Vector3::axis(0), &Vector3::axis(1))?;
722        let Some(arc) = flat.as_arc()? else {
723            return Ok(Seg::Other);
724        };
725        let ccw = arc.circle.normal[2].definitely_greater(Scalar::ZERO);
726        let center = Vector2::from_array([arc.circle.center[0], arc.circle.center[1]]);
727        let sigma = if ccw { Scalar::ONE } else { Scalar::ONE.neg() };
728        // Less than half a turn: the center on the arc's left.
729        let turn = a.sub(&center).prod_cross(&b.sub(&center)).mul(sigma);
730        if !turn.definitely_greater(Scalar::ZERO) {
731            return Ok(Seg::Other);
732        }
733        Ok(Seg::Arc {
734            center,
735            radius: arc.circle.radius,
736            a,
737            b,
738            ccw,
739        })
740    }
741
742    /// Where `p`, on its line or circle, lies along it.
743    fn along(&self, p: &Vector2) -> Along {
744        let (a, b, outside) = match self {
745            Seg::Line { a, b } => (a, b, Scalar::ZERO),
746            Seg::Arc { a, b, ccw, .. } => {
747                // The arc lies right of its chord, seen turning
748                // counter-clockwise.
749                let side = b.sub(a).prod_cross(&p.sub(a));
750                (a, b, if *ccw { side } else { side.neg() })
751            }
752            Seg::Other => return Along::Undecided,
753        };
754        if outside.definitely_greater(Scalar::ZERO) {
755            return Along::Outside;
756        }
757        let chord = b.sub(a);
758        let key = p.sub(a).prod_dot(&chord);
759        let length = chord.norm_sq();
760        if key.definitely_less(Scalar::ZERO) || key.definitely_greater(length) {
761            Along::Outside
762        } else if key.definitely_greater(Scalar::ZERO)
763            && key.definitely_less(length)
764            && (matches!(self, Seg::Line { .. }) || outside.definitely_less(Scalar::ZERO))
765        {
766            Along::Inside(key)
767        } else {
768            Along::Undecided
769        }
770    }
771
772    /// Its ends, if it is a line or an arc.
773    fn ends(&self) -> Vec<Vector2> {
774        match self {
775            Seg::Line { a, b } | Seg::Arc { a, b, .. } => vec![*a, *b],
776            Seg::Other => Vec::new(),
777        }
778    }
779
780    /// A point of it, away from its ends: halfway, along a line or an arc.
781    pub fn middle(&self, curve: &NurbCurve2D) -> GeopResult<Vector2> {
782        match self {
783            Seg::Line { a, b } => Ok(a.add(b).prod_scalar(Scalar::ONE.div(Scalar::TWO)?)),
784            Seg::Arc {
785                center,
786                radius,
787                a,
788                b,
789                ..
790            } => {
791                let bisector = a.sub(center).add(&b.sub(center)).normalize()?;
792                Ok(center.add(&bisector.prod_scalar(*radius)))
793            }
794            Seg::Other => curve.evaluate(curve.mid_parameter()?),
795        }
796    }
797
798    /// `∮ (x dy - y dx) / 2` along it: what it adds to a loop's area.
799    fn swept_area(&self, curve: &NurbCurve2D) -> GeopResult<Scalar> {
800        let half = Scalar::ONE.div(Scalar::TWO)?;
801        Ok(match self {
802            Seg::Line { a, b } => a.prod_cross(b).mul(half),
803            Seg::Arc {
804                center,
805                radius,
806                a,
807                b,
808                ccw,
809            } => {
810                let (u, v) = (a.sub(center), b.sub(center));
811                let angle = u.prod_cross(&v).abs().atan2(u.prod_dot(&v));
812                // The segment between the chord and the arc, outside the
813                // chord turning counter-clockwise.
814                let segment = radius.mul(*radius).mul(angle.sub(angle.sin())).mul(half);
815                let chord = a.prod_cross(b).mul(half);
816                if *ccw {
817                    chord.add(segment)
818                } else {
819                    chord.sub(segment)
820                }
821            }
822            // Integrated along the curve itself, from the same origin as
823            // the lines and arcs.
824            Seg::Other => curve.area_term(&Vector2::zero())?,
825        })
826    }
827
828    /// The curve split at `stops` — its start, the points along it in
829    /// order, its end — into pieces of the same line or circle.
830    fn split(&self, stops: &[Vector2]) -> GeopResult<Vec<NurbCurve2D>> {
831        stops
832            .windows(2)
833            .map(|w| match self {
834                Seg::Line { .. } => NurbCurve2D::line(w[0], w[1]),
835                Seg::Arc { center, radius, .. } => arc_between(center, *radius, &w[0], &w[1]),
836                Seg::Other => Err(GeopError::new(
837                    "a curve that is no line or arc cannot be split",
838                )),
839            })
840            .collect()
841    }
842}
843
844/// The arc about `center` of `radius` from `p` to `q`, less than half a
845/// turn: a rational quadratic through the point where its end tangents
846/// meet, `center + (u + v) r² / (r² + u·v)`, weighted `cos` of half its
847/// angle.
848fn arc_between(
849    center: &Vector2,
850    radius: Scalar,
851    p: &Vector2,
852    q: &Vector2,
853) -> GeopResult<NurbCurve2D> {
854    let (u, v) = (p.sub(center), q.sub(center));
855    let r2 = radius.mul(radius);
856    let dot = u.prod_dot(&v);
857    let apex = center.add(&u.add(&v).prod_scalar(r2.div(r2.add(dot))?));
858    let weight = r2.add(dot).div(Scalar::TWO.mul(r2))?.sqrt()?;
859    NurbCurve2D::arc(*p, apex, *q, weight)
860}
861
862/// Where the line or arc `e` and the line or arc `c` cross: each crossing's
863/// place along `e`, along `c`, and its point. A curve that is neither may
864/// cross nothing. Why not, if they touch, overlap or cross at an end.
865fn crossings(
866    e: &Seg,
867    c: &Seg,
868    e_curve: &NurbCurve2D,
869    c_curve: &NurbCurve2D,
870) -> Result<Vec<(Scalar, Scalar, Vector2)>, String> {
871    if matches!(e, Seg::Other) || matches!(c, Seg::Other) {
872        let (overlaps, crossings) = curve_curve_overlaps_and_crossings(
873            e_curve,
874            c_curve,
875            Scalar::ZERO,
876            MAX_NODES,
877            min_subdivision_size(),
878        )
879        .map_err(|err| format!("could not be checked against ({err})"))?;
880        if overlaps.is_empty() && crossings.is_empty() {
881            return Ok(Vec::new());
882        }
883        return Err("meets, and a cut can only cross lines and arcs, not other curves, as".into());
884    }
885    let candidates: Vec<(Vector2, bool)> = match (e, c) {
886        (Seg::Line { a, b }, Seg::Line { a: p, b: q }) => {
887            let (d1, d2) = (b.sub(a), q.sub(p));
888            let den = d1.prod_cross(&d2);
889            if den.could_be_equal(Scalar::ZERO) {
890                // Parallel: apart, or along one line.
891                if !p.sub(a).prod_cross(&d1).could_be_equal(Scalar::ZERO) {
892                    return Ok(Vec::new());
893                }
894                let reach = |x: &Vector2| x.sub(a).prod_dot(&d1);
895                let (lo, hi) = (reach(p).min(reach(q)), reach(p).max(reach(q)));
896                if hi.definitely_less(Scalar::ZERO) || lo.definitely_greater(d1.norm_sq()) {
897                    return Ok(Vec::new());
898                }
899                return Err("runs along".into());
900            }
901            let s = p
902                .sub(a)
903                .prod_cross(&d2)
904                .div(den)
905                .map_err(|e| e.to_string())?;
906            vec![(a.add(&d1.prod_scalar(s)), false)]
907        }
908        (Seg::Line { a, b }, Seg::Arc { center, radius, .. })
909        | (Seg::Arc { center, radius, .. }, Seg::Line { a, b }) => {
910            line_circle(a, b, center, *radius).map_err(|e| e.to_string())?
911        }
912        (
913            Seg::Arc {
914                center: c1,
915                radius: r1,
916                ..
917            },
918            Seg::Arc {
919                center: c2,
920                radius: r2,
921                ..
922            },
923        ) => circle_circle(c1, *r1, c2, *r2, e, c)?,
924        _ => unreachable!("other curves are handled above"),
925    };
926    let mut found = Vec::new();
927    for (p, tangent) in candidates {
928        match (e.along(&p), c.along(&p)) {
929            (Along::Outside, _) | (_, Along::Outside) => {}
930            (Along::Inside(ke), Along::Inside(kc)) if !tangent => found.push((ke, kc, p)),
931            (Along::Inside(_), Along::Inside(_)) => return Err("touches".into()),
932            // At a joint of the cut's loop, which `joint_on` decides.
933            (Along::Inside(_), Along::Undecided)
934                if c.ends().iter().any(|q| q.could_be_equal(&p)) => {}
935            _ => return Err("crosses, at an end of one of them,".into()),
936        }
937    }
938    Ok(found)
939}
940
941/// Where the joint `at` of a cut's loop, between its curves `into` and
942/// `out`, lies along the line or arc `e`: `None` off it, else its place
943/// along `e`, the loop crossing `e` there. Why not, if it lies at an end of
944/// `e`, or the loop could only touch `e` there. A curve that is neither
945/// line nor arc is checked against the cut's curves whole (see
946/// [`crossings`]).
947fn joint_on(
948    e: &Seg,
949    at: &Vector2,
950    into: &NurbCurve2D,
951    out: &NurbCurve2D,
952) -> Result<Option<Scalar>, String> {
953    // The direction of `e` there.
954    let direction = match e {
955        Seg::Line { a, b } => {
956            let d = b.sub(a);
957            if !d.prod_cross(&at.sub(a)).could_be_equal(Scalar::ZERO) {
958                return Ok(None);
959            }
960            d
961        }
962        Seg::Arc { center, radius, .. } => {
963            let r = at.sub(center);
964            if !r.norm_sq().could_be_equal(radius.mul(*radius)) {
965                return Ok(None);
966            }
967            r.perp()
968        }
969        Seg::Other => return Ok(None),
970    };
971    let along = match e.along(at) {
972        Along::Outside => return Ok(None),
973        Along::Inside(along) => along,
974        Along::Undecided => return Err("meet, at an end of".into()),
975    };
976    // The loop crosses `e` if it arrives from one side and leaves to the
977    // other: both its tangents there on one side of `e`'s direction.
978    let tangent = |curve: &NurbCurve2D, t: Scalar| curve.tangent(t).map_err(|e| e.to_string());
979    let side_in = direction.prod_cross(&tangent(into, into.domain().1)?);
980    let side_out = direction.prod_cross(&tangent(out, out.domain().0)?);
981    let zero = Scalar::ZERO;
982    if (side_in.definitely_greater(zero) && side_out.definitely_greater(zero))
983        || (side_in.definitely_less(zero) && side_out.definitely_less(zero))
984    {
985        Ok(Some(along))
986    } else {
987        Err("touch".into())
988    }
989}
990
991/// Where the line through `a` and `b` meets the circle: each point, and
992/// whether it could be where the line only touches it.
993fn line_circle(
994    a: &Vector2,
995    b: &Vector2,
996    center: &Vector2,
997    radius: Scalar,
998) -> GeopResult<Vec<(Vector2, bool)>> {
999    let d = b.sub(a);
1000    let w = a.sub(center);
1001    let qa = d.norm_sq();
1002    let qb = d.prod_dot(&w);
1003    let qc = w.norm_sq().sub(radius.mul(radius));
1004    // `qa s² + 2 qb s + qc = 0`.
1005    let disc = qb.mul(qb).sub(qa.mul(qc));
1006    if disc.definitely_less(Scalar::ZERO) {
1007        return Ok(Vec::new());
1008    }
1009    if disc.could_be_equal(Scalar::ZERO) {
1010        let s = qb.neg().div(qa)?;
1011        return Ok(vec![(a.add(&d.prod_scalar(s)), true)]);
1012    }
1013    let root = disc.sqrt()?;
1014    [qb.neg().sub(root), qb.neg().add(root)]
1015        .into_iter()
1016        .map(|num| Ok((a.add(&d.prod_scalar(num.div(qa)?)), false)))
1017        .collect()
1018}
1019
1020/// Where two circles meet: each point, and whether it could be where they
1021/// only touch. Why not, if they could be one circle along both arcs.
1022fn circle_circle(
1023    c1: &Vector2,
1024    r1: Scalar,
1025    c2: &Vector2,
1026    r2: Scalar,
1027    e: &Seg,
1028    c: &Seg,
1029) -> Result<Vec<(Vector2, bool)>, String> {
1030    let d = c2.sub(c1);
1031    let d2 = d.norm_sq();
1032    if d2.could_be_equal(Scalar::ZERO) {
1033        if !r1.could_be_equal(r2) {
1034            return Ok(Vec::new());
1035        }
1036        // One circle: apart only if neither arc reaches into the other.
1037        let apart = c
1038            .ends()
1039            .iter()
1040            .all(|p| matches!(e.along(p), Along::Outside))
1041            && e.ends()
1042                .iter()
1043                .all(|p| matches!(c.along(p), Along::Outside));
1044        return if apart {
1045            Ok(Vec::new())
1046        } else {
1047            Err("runs along".into())
1048        };
1049    }
1050    let err = |e: GeopError| e.to_string();
1051    // From `c1` along `d` by `k`, across it by `h`, both in units of `|d|`.
1052    let k = r1
1053        .mul(r1)
1054        .sub(r2.mul(r2))
1055        .add(d2)
1056        .div(Scalar::TWO.mul(d2))
1057        .map_err(err)?;
1058    let h2 = r1.mul(r1).div(d2).map_err(err)?.sub(k.mul(k));
1059    if h2.definitely_less(Scalar::ZERO) {
1060        return Ok(Vec::new());
1061    }
1062    let base = c1.add(&d.prod_scalar(k));
1063    let across = d.perp();
1064    if h2.could_be_equal(Scalar::ZERO) {
1065        return Ok(vec![(base, true)]);
1066    }
1067    let h = h2.sqrt().map_err(err)?;
1068    Ok(vec![
1069        (base.add(&across.prod_scalar(h)), false),
1070        (base.sub(&across.prod_scalar(h)), false),
1071    ])
1072}
1073
1074/// A curve of a region's loop, with what it is.
1075struct Piece<'a> {
1076    seg: &'a Seg,
1077    curve: &'a NurbCurve2D,
1078}
1079
1080/// Directions to cast a ray in, until one gives a clear answer: spread by
1081/// the golden angle, none along an axis — which is where the edges of a
1082/// sheet most often run.
1083fn ray_direction(k: usize) -> Vector2 {
1084    let angle = 0.7 + 2.399_963_229_728_653 * k as f64;
1085    Vector2::from_array([Scalar::from_f64(angle.cos()), Scalar::from_f64(angle.sin())])
1086}
1087
1088/// How many directions a ray cast tries before giving up.
1089const RAY_ATTEMPTS: usize = 16;
1090
1091/// Whether `p` lies inside the region bounded by `loops`, by the parity
1092/// of a ray's crossings. Why not, if every ray met the boundary at a
1093/// point it could not tell inside from outside — `p` on it.
1094fn contains(loops: &[Vec<Piece<'_>>], p: &Vector2) -> Result<bool, String> {
1095    let reach = loops
1096        .iter()
1097        .flatten()
1098        .map(|piece| piece.curve.aabb())
1099        .map(|b| {
1100            [0, 1]
1101                .map(|k| b[k].sub(p[k]).abs().upper().to_f64())
1102                .iter()
1103                .sum::<f64>()
1104        })
1105        .fold(1.0, f64::max);
1106    // Directions tried in turn until one crosses every piece cleanly.
1107    let found = attempts(RAY_ATTEMPTS, |&k| -> Result<_, String> {
1108        let d = ray_direction(k);
1109        let mut count = 0usize;
1110        for piece in loops.iter().flatten() {
1111            match ray_crossings(piece, p, &d, reach) {
1112                Some(n) => count += n,
1113                None => return Ok(Probe::Next),
1114            }
1115        }
1116        Ok(Probe::Found(count % 2 == 1))
1117    })?;
1118    found.ok_or_else(|| "cannot be told inside or outside, lying on the boundary of,".into())
1119}
1120
1121/// How many times the ray from `p` along `d` crosses the curve; `None` if
1122/// it could be through an end of it, along it or touching it.
1123fn ray_crossings(piece: &Piece<'_>, p: &Vector2, d: &Vector2, reach: f64) -> Option<usize> {
1124    let ahead = |q: &Vector2| -> Option<bool> {
1125        let t = q.sub(p).prod_dot(d);
1126        if t.definitely_greater(Scalar::ZERO) {
1127            Some(true)
1128        } else if t.definitely_less(Scalar::ZERO) {
1129            Some(false)
1130        } else {
1131            None
1132        }
1133    };
1134    let far = p.add(&d.prod_scalar(Scalar::from_f64(2.0 * reach)));
1135    let points: Vec<(Vector2, bool)> = match piece.seg {
1136        Seg::Line { a, b } => {
1137            let e = b.sub(a);
1138            let den = d.prod_cross(&e);
1139            if den.could_be_equal(Scalar::ZERO) {
1140                return if a.sub(p).prod_cross(d).could_be_equal(Scalar::ZERO) {
1141                    None
1142                } else {
1143                    Some(0)
1144                };
1145            }
1146            let s = p.sub(a).prod_cross(d).div(den).ok()?.neg();
1147            vec![(a.add(&e.prod_scalar(s)), false)]
1148        }
1149        Seg::Arc { center, radius, .. } => line_circle(p, &far, center, *radius).ok()?,
1150        Seg::Other => {
1151            let ray = NurbCurve2D::line(*p, far).ok()?;
1152            let (overlaps, crossings) = curve_curve_overlaps_and_crossings(
1153                &ray,
1154                piece.curve,
1155                Scalar::ZERO,
1156                MAX_NODES,
1157                min_subdivision_size(),
1158            )
1159            .ok()?;
1160            if !overlaps.is_empty() {
1161                return None;
1162            }
1163            let (t0, t1) = piece.curve.domain();
1164            let mut n = 0;
1165            for (_, t) in crossings {
1166                if t.could_be_equal(t0) || t.could_be_equal(t1) {
1167                    return None;
1168                }
1169                n += 1;
1170            }
1171            return Some(n);
1172        }
1173    };
1174    let mut n = 0;
1175    for (q, tangent) in points {
1176        match (piece.seg.along(&q), ahead(&q)) {
1177            (Along::Outside, _) | (_, Some(false)) => {}
1178            (Along::Inside(_), Some(true)) if !tangent => n += 1,
1179            _ => return None,
1180        }
1181    }
1182    Some(n)
1183}