axiolid_brep_boolean/
split.rs

1//! Face splitting: a face cut along its section edges (ADR 0075, step 3).
2//!
3//! Everything happens in the face's own parameters, where its boundary uses
4//! already have pcurves:
5//!
6//! 1. Each section edge on the face gets an exact pcurve, derived from the
7//!    two surfaces: on a plane a line, circle or ellipse maps to its own
8//!    family in the plane's coordinates; on a cylinder a ruling is a vertical
9//!    line, a circle about the axis a horizontal one, and a plane's oblique
10//!    cut a `Sinusoid2` (ADR 0071). Every other section on every analytic
11//!    face -- a sphere, cone or torus, or a curved section on a plane or
12//!    cylinder -- gets the implicit pcurve of ADR 0077: the other surface's
13//!    equation read in the face's parameters, traced once per surface over
14//!    the face's parameter box, and the stretch between the section's ends
15//!    cut out of it.
16//! 2. Boundary uses are split wherever a section edge ends on them.
17//! 3. The pieces form a graph in `(u, v)`: boundary pieces are walked the
18//!    way their loop runs, section pieces both ways. Starting from each
19//!    unused half-edge, the walk turns at every vertex to the first
20//!    outgoing half-edge clockwise from the one it arrived on, which keeps
21//!    one region on its left.
22//! 4. Loops running anticlockwise bound regions; clockwise ones are holes,
23//!    each given to the smallest region around it.
24//!
25//! A seam is two vertices in `(u, v)`, so a periodic face splits like any
26//! other. Two outgoing pieces leaving a vertex in the same direction
27//! (tangent sections) are refused, not ordered by guesswork.
28
29use axiolid_brep::ExactBRep;
30use axiolid_core::{Frame2, Interval, Point2, Point3, Scalar, Tolerance, Vec2};
31use axiolid_curve::{Curve2, Curve3, Ellipse2, Line2, Sinusoid2};
32use axiolid_evaluate::curve::{derivative2, evaluate2, locate2, locate3, second_derivative2};
33use axiolid_evaluate::evaluate3;
34use axiolid_evaluate::surface::locate;
35use axiolid_measure::FaceDomain;
36use axiolid_surface::Surface;
37use axiolid_topology::{EdgeId, FaceId, Orientation};
38use core::f64::consts::{PI, TAU};
39
40use crate::section::SectionEdge;
41use crate::support::{periods, window};
42use crate::BooleanError;
43
44/// Where a piece of a split face's boundary came from.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum PieceSource {
47    /// Part of one of the face's own edges.
48    Boundary(EdgeId),
49    /// Part of the section edge with this index in the list given.
50    Section(usize),
51    /// A stretch of parameters that is one point in space -- a sphere's
52    /// pole, a cone's apex -- closing a loop in the face's parameters. It
53    /// bounds regions while the face is split but is no edge of the result.
54    Collapsed,
55}
56
57/// One stretch of a split face's boundary, in the direction the region's
58/// loop runs.
59#[derive(Debug, Clone, PartialEq)]
60pub struct Piece {
61    /// The 3D curve it lies on.
62    pub curve: Curve3,
63    /// Its span on `curve`, from where the loop enters it to where it
64    /// leaves.
65    pub span: Interval,
66    /// Its pcurve on the face.
67    pub pcurve: Curve2,
68    /// Its span on `pcurve`, in the same direction.
69    pub pspan: Interval,
70    /// Where it came from.
71    pub source: PieceSource,
72}
73
74/// One region of a split face: an outer loop and its holes.
75#[derive(Debug, Clone, PartialEq)]
76pub struct Region {
77    /// Outer loop, anticlockwise in the face's parameters.
78    pub outer: Vec<Piece>,
79    /// Holes, clockwise.
80    pub holes: Vec<Vec<Piece>>,
81    /// Whether the face's own loops wind clockwise in its parameters, so
82    /// the region (always anticlockwise) faces opposite to the face's
83    /// orientation flag.
84    pub against: bool,
85}
86
87/// Split `face` of `brep` along the section edges lying on it.
88///
89/// `sections` are the section edges on this face. `first` says
90/// whether `brep` is the first operand of the sections, which picks the
91/// side whose `along_*` flag applies: a section running along one of the
92/// face's own edges splits nothing but the edge, at its ends. `cuts` are
93/// every point where the operand's edges are cut, by sections on any face:
94/// an edge is cut the same way in both faces that share it.
95///
96/// # Errors
97///
98/// A face or section this stage cannot give exact pcurves to, tangent
99/// pieces at a vertex, or a boundary that does not close in parameters.
100pub fn split_face(
101    brep: &ExactBRep,
102    face: FaceId,
103    sections: &[SectionEdge],
104    first: bool,
105    cuts: &[Point3],
106    tolerance: Tolerance,
107) -> Result<Vec<Region>, BooleanError> {
108    let topology = brep.topology();
109    let record = topology
110        .faces()
111        .get(face.index())
112        .ok_or(BooleanError::DanglingReference)?;
113    let surface = record
114        .surface
115        .and_then(|id| brep.surfaces().get(id.index()))
116        .ok_or(BooleanError::DanglingReference)?;
117    let domain = FaceDomain::new(brep, face, tolerance)
118        .map_err(BooleanError::Measure)?
119        .ok_or(BooleanError::UnsupportedTrim)?;
120    let (lo, hi) = domain.bounds();
121
122    // Section pieces with exact pcurves, their starts placed in the face's
123    // own parameter range.
124    let mut pieces: Vec<(Piece, bool)> = Vec::new();
125    let mut ends: Vec<Point3> = cuts.to_vec();
126    // One stretch of curve can reach a face from several face pairs (a
127    // shared patch's edge is also where the neighbouring faces meet it).
128    let mut seen: Vec<&SectionEdge> = Vec::new();
129    let mut traces = Traces::default();
130    for (index, section) in sections.iter().enumerate() {
131        ends.push(section.start);
132        ends.push(section.end);
133        let (along, other) = if first {
134            (section.along_a, &section.other_a)
135        } else {
136            (section.along_b, &section.other_b)
137        };
138        if along
139            || seen
140                .iter()
141                .any(|other| same_stretch(other, section, tolerance))
142        {
143            continue;
144        }
145        seen.push(section);
146        let piece = section_piece(
147            surface,
148            other,
149            section,
150            index,
151            lo,
152            hi,
153            &mut traces,
154            tolerance,
155        )?;
156        pieces.push((piece, true));
157    }
158
159    // Boundary uses, split where a section ends on them.
160    for bound in &record.bounds {
161        let wire = topology
162            .loops()
163            .get(bound.loop_id.index())
164            .ok_or(BooleanError::DanglingReference)?;
165        let mut uses: Vec<Piece> = Vec::with_capacity(wire.edges.len());
166        for (index, use_) in wire.edges.iter().enumerate() {
167            let edge = &topology.edges()[use_.edge.index()];
168            let curve = edge
169                .curve
170                .and_then(|id| brep.curves3().get(id.index()))
171                .ok_or(BooleanError::DanglingReference)?
172                .clone();
173            let span = brep
174                .edge_interval(use_.edge)
175                .ok_or(BooleanError::DanglingReference)?;
176            let span = match use_.orientation {
177                Orientation::Forward => span,
178                Orientation::Reversed => Interval::new(span.end, span.start),
179            };
180            let pcurve = use_
181                .pcurve
182                .and_then(|id| brep.curves2().get(id.index()))
183                .ok_or(BooleanError::DanglingReference)?
184                .clone();
185            let pspan = brep
186                .pcurve_interval(bound.loop_id, index)
187                .ok_or(BooleanError::DanglingReference)?;
188            uses.push(Piece {
189                curve,
190                span,
191                pcurve,
192                pspan,
193                source: PieceSource::Boundary(use_.edge),
194            });
195        }
196        if bound.orientation == Orientation::Reversed {
197            uses.reverse();
198            for piece in &mut uses {
199                piece.span = Interval::new(piece.span.end, piece.span.start);
200                piece.pspan = Interval::new(piece.pspan.end, piece.pspan.start);
201            }
202        }
203        let closed = close_poles(surface, uses, tolerance)?;
204        for piece in closed {
205            for part in split_use(surface, piece, &ends, tolerance)? {
206                pieces.push((part, false));
207            }
208        }
209    }
210
211    // Sections ending at a pole meet the collapsed piece there: split it at
212    // those points in parameters, so the graph has a vertex for them.
213    let mut pole_ends: Vec<Point2> = Vec::new();
214    for (piece, section) in &pieces {
215        if *section {
216            for t in [piece.pspan.start, piece.pspan.end] {
217                pole_ends.push(evaluate2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?);
218            }
219        }
220    }
221    let mut split_pieces = Vec::with_capacity(pieces.len());
222    for (piece, section) in pieces {
223        if piece.source != PieceSource::Collapsed {
224            split_pieces.push((piece, section));
225            continue;
226        }
227        let (a, b) = (
228            evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?,
229            evaluate2(&piece.pcurve, piece.pspan.end).map_err(|_| BooleanError::Evaluation)?,
230        );
231        let slack = 1e-9 * (1.0 + a.x.abs().max(b.x.abs()));
232        let mut cuts: Vec<Scalar> = pole_ends
233            .iter()
234            .filter(|p| (p.y - a.y).abs() <= slack)
235            .filter(|p| p.x > a.x.min(b.x) + slack && p.x < a.x.max(b.x) - slack)
236            .map(|p| (p.x - a.x) / (b.x - a.x))
237            .collect();
238        cuts.sort_by(Scalar::total_cmp);
239        cuts.dedup_by(|x, y| (*x - *y).abs() <= 1e-12);
240        let mut from = piece.pspan.start;
241        for c in cuts.into_iter().chain(std::iter::once(piece.pspan.end)) {
242            split_pieces.push((
243                Piece {
244                    pspan: Interval::new(from, c),
245                    ..piece.clone()
246                },
247                false,
248            ));
249            from = c;
250        }
251    }
252    let mut pieces = split_pieces;
253
254    // A face whose loops wind clockwise (a region an earlier boolean turned
255    // over) is traced with its boundary reversed, so regions come out
256    // anticlockwise; they then face against the face's flag.
257    let mut swept = 0.0;
258    for (piece, section) in &pieces {
259        if !section {
260            swept += sweep(piece)?;
261        }
262    }
263    let against = swept < 0.0;
264    if against {
265        for (piece, section) in &mut pieces {
266            if !*section {
267                *piece = directed(piece, true);
268            }
269        }
270    }
271    let mut regions = trace(&pieces)?;
272    for region in &mut regions {
273        region.against = against;
274    }
275    Ok(regions)
276}
277
278/// `1/2 int (u dv - v du)` along a piece: summed over a face's loops, its
279/// sign is the loops' winding, even where a seam leaves them open.
280fn sweep(piece: &Piece) -> Result<Scalar, BooleanError> {
281    let n = 64;
282    let mut total = 0.0;
283    let mut previous =
284        evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?;
285    for i in 1..=n {
286        let p =
287            piece.pspan.start + (piece.pspan.end - piece.pspan.start) * i as Scalar / n as Scalar;
288        let q = evaluate2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
289        total += 0.5 * (previous.x * q.y - q.x * previous.y);
290        previous = q;
291    }
292    Ok(total)
293}
294
295/// A loop's uses with a [`PieceSource::Collapsed`] piece wherever one use
296/// ends and the next starts at the same point in space but not in
297/// parameters, at a pole: a sphere's seam reaches its north pole at
298/// `(2 pi, pi/2)` and leaves it at `(0, pi/2)`.
299fn close_poles(
300    surface: &Surface,
301    uses: Vec<Piece>,
302    tolerance: Tolerance,
303) -> Result<Vec<Piece>, BooleanError> {
304    let n = uses.len();
305    let mut out = Vec::with_capacity(n + 2);
306    for i in 0..n {
307        let next = &uses[(i + 1) % n];
308        let a =
309            evaluate2(&uses[i].pcurve, uses[i].pspan.end).map_err(|_| BooleanError::Evaluation)?;
310        let b = evaluate2(&next.pcurve, next.pspan.start).map_err(|_| BooleanError::Evaluation)?;
311        out.push(uses[i].clone());
312        let slack = 1e-7 * (1.0 + a.x.abs().max(a.y.abs()));
313        if (a - b).length() <= slack {
314            continue;
315        }
316        let pa = axiolid_evaluate::surface::evaluate(surface, a.x, a.y)
317            .map_err(|_| BooleanError::Evaluation)?;
318        let pb = axiolid_evaluate::surface::evaluate(surface, b.x, b.y)
319            .map_err(|_| BooleanError::Evaluation)?;
320        let (su, sv) = axiolid_evaluate::surface::partials(surface, a.x, a.y)
321            .map_err(|_| BooleanError::Evaluation)?;
322        let scale = 1.0 + sv.length();
323        let pole =
324            (pa - pb).length() <= tolerance.linear().max(1e-9) && su.length() <= 1e-9 * scale;
325        if !pole {
326            // A gap that is not a pole: the loop winds round a seam with no
327            // seam edge, which this split does not close.
328            return Err(BooleanError::UnclosedSplit);
329        }
330        out.push(Piece {
331            curve: Curve3::Line(axiolid_curve::Line3 {
332                origin: pa,
333                direction: axiolid_core::Vec3::ZERO,
334            }),
335            span: Interval::new(0.0, 1.0),
336            pcurve: Curve2::Line(Line2 {
337                origin: a,
338                direction: b - a,
339            }),
340            pspan: Interval::new(0.0, 1.0),
341            source: PieceSource::Collapsed,
342        });
343    }
344    Ok(out)
345}
346
347/// Whether two section edges are the same stretch of curve, either way.
348fn same_stretch(a: &SectionEdge, b: &SectionEdge, tolerance: Tolerance) -> bool {
349    let eps = tolerance.linear().max(1e-9);
350    let near = |p: Point3, q: Point3| (p - q).length() <= eps;
351    let mid = |e: &SectionEdge| evaluate3(&e.curve, 0.5 * (e.span.start + e.span.end));
352    let ends = (near(a.start, b.start) && near(a.end, b.end))
353        || (near(a.start, b.end) && near(a.end, b.start));
354    ends && matches!((mid(a), mid(b)), (Ok(p), Ok(q)) if near(p, q))
355}
356
357/// Traced sections of the face's surface, one set per other surface, or
358/// the fact that the trace could not be done (so it is not tried again).
359#[derive(Default)]
360struct Traces {
361    done: Vec<(Surface, Option<Vec<axiolid_curve::ImplicitCurve2>>)>,
362}
363
364impl Traces {
365    fn of(
366        &mut self,
367        surface: &Surface,
368        other: &Surface,
369        lo: Point2,
370        hi: Point2,
371    ) -> Result<&[axiolid_curve::ImplicitCurve2], BooleanError> {
372        let index = match self.done.iter().position(|(s, _)| s == other) {
373            Some(index) => index,
374            None => {
375                let curves =
376                    axiolid_nurbs::trace_section_pcurves(surface, other, window(surface, lo, hi))
377                        .ok();
378                self.done.push((other.clone(), curves));
379                self.done.len() - 1
380            }
381        };
382        self.done[index]
383            .1
384            .as_deref()
385            .ok_or(BooleanError::UnsupportedSplit)
386    }
387}
388
389/// A section edge with its exact pcurve on `surface`.
390///
391/// On a seam the start's angle is ambiguous (`0` and `2 pi` name one
392/// point), so each placement in the face's range is tried and the one whose
393/// piece runs inside the range is kept.
394#[allow(clippy::too_many_arguments)]
395fn section_piece(
396    surface: &Surface,
397    other: &Surface,
398    section: &SectionEdge,
399    index: usize,
400    lo: Point2,
401    hi: Point2,
402    traces: &mut Traces,
403    tolerance: Tolerance,
404) -> Result<Piece, BooleanError> {
405    let closed_form = iso_curve(surface, &section.curve, tolerance)
406        || matches!(
407            (surface, &section.curve),
408            (
409                Surface::Plane(_),
410                Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
411            ) | (
412                Surface::Cylinder(_),
413                Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
414            )
415        );
416    if !closed_form {
417        return implicit_piece(surface, other, section, index, lo, hi, traces, tolerance);
418    }
419    // A start at a pole has no angle: read the piece a little way in.
420    let first = match place(surface, section.start, lo, hi, tolerance) {
421        Ok(p) => p,
422        Err(_) => {
423            let t = section.span.start + 0.01 * (section.span.end - section.span.start);
424            let p = evaluate3(&section.curve, t).map_err(|_| BooleanError::Evaluation)?;
425            place(surface, p, lo, hi, tolerance)?
426        }
427    };
428    let slack = 1e-9 * (1.0 + lo.x.abs().max(hi.x.abs()));
429    let mut candidates = vec![first];
430    if periods(surface).0 {
431        for shift in [TAU, -TAU] {
432            let other_turn = Point2::new(first.x + shift, first.y);
433            if other_turn.x >= lo.x - slack && other_turn.x <= hi.x + slack {
434                candidates.push(other_turn);
435            }
436        }
437    }
438    let mut fallback = None;
439    for start_uv in candidates {
440        let piece = section_piece_from(surface, section, index, start_uv, tolerance)?;
441        let mid = evaluate2(&piece.pcurve, 0.5 * (piece.pspan.start + piece.pspan.end))
442            .map_err(|_| BooleanError::Evaluation)?;
443        if mid.x >= lo.x - slack && mid.x <= hi.x + slack {
444            return Ok(piece);
445        }
446        fallback.get_or_insert(piece);
447    }
448    fallback.ok_or(BooleanError::Evaluation)
449}
450
451/// A section edge's pcurve on an analytic face as its space curve read in
452/// the face's parameters (`Curve2::Lifted`), sharing the edge's parameter;
453/// the guide unwraps its angles into the face's range.
454fn lifted_piece(
455    surface: &Surface,
456    section: &SectionEdge,
457    index: usize,
458    lo: Point2,
459    hi: Point2,
460    tolerance: Tolerance,
461) -> Result<Piece, BooleanError> {
462    let carrier = match surface {
463        Surface::Plane(p) => axiolid_curve::Carrier::Plane(p.frame),
464        Surface::Cylinder(c) => axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
465            frame: c.frame,
466            x_radius: c.radius,
467            y_radius: c.radius,
468            slope: 0.0,
469        }),
470        Surface::EllipticalCylinder(c) => {
471            axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
472                frame: c.frame,
473                x_radius: c.semi_axis_x,
474                y_radius: c.semi_axis_y,
475                slope: 0.0,
476            })
477        }
478        Surface::Cone(c) => axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
479            frame: c.frame,
480            x_radius: c.radius,
481            y_radius: c.radius,
482            slope: c.semi_angle.tan(),
483        }),
484        Surface::Sphere(s) => axiolid_curve::Carrier::Sphere {
485            frame: s.frame,
486            radius: s.radius,
487        },
488        Surface::Torus(t) => axiolid_curve::Carrier::Torus(axiolid_curve::TorusCarrier {
489            frame: t.frame,
490            major_radius: t.major_radius,
491            minor_radius: t.minor_radius,
492        }),
493        // A B-spline carries a section of two B-splines in its own
494        // parameters (ADR 0077).
495        Surface::BSpline(b) => axiolid_curve::Carrier::Spline(Box::new(b.clone())),
496        _ => return Err(BooleanError::UnsupportedSplit),
497    };
498    if let Curve3::PairSection(pair) = &section.curve {
499        let first = pair.side(&carrier).ok_or(BooleanError::UnsupportedSplit)?;
500        let (t0, t1) = (section.span.start, section.span.end);
501        let n = 4 * pair.nodes.len();
502        let mut guide = Vec::with_capacity(n + 1);
503        for i in 0..=n {
504            let (a, b, _) = pair
505                .solve(t0 + (t1 - t0) * i as Scalar / n as Scalar)
506                .ok_or(BooleanError::Evaluation)?;
507            guide.push(if first { a } else { b });
508        }
509        return Ok(Piece {
510            curve: section.curve.clone(),
511            span: section.span,
512            pcurve: Curve2::Lifted(axiolid_curve::LiftedCurve2 {
513                curve: Box::new(section.curve.clone()),
514                carrier,
515                start: t0,
516                end: t1,
517                guide,
518            }),
519            pspan: section.span,
520            source: PieceSource::Section(index),
521        });
522    }
523    let (pu, pv) = periods(surface);
524    let n = 128;
525    let (t0, t1) = (section.span.start, section.span.end);
526    let mut guide: Vec<Point2> = Vec::with_capacity(n + 1);
527    for i in 0..=n {
528        let t = t0 + (t1 - t0) * i as Scalar / n as Scalar;
529        let p = evaluate3(&section.curve, t).map_err(|_| BooleanError::Evaluation)?;
530        let (mut u, mut v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
531        if let Some(last) = guide.last() {
532            if pu {
533                u += ((last.x - u) / TAU).round() * TAU;
534            }
535            if pv {
536                v += ((last.y - v) / TAU).round() * TAU;
537            }
538        }
539        guide.push(Point2::new(u, v));
540    }
541    // Whole turns into the face's range, judged at the middle.
542    let middle = guide[n / 2];
543    let into = |x: Scalar, a: Scalar, b: Scalar, periodic: bool| {
544        if !periodic || (x >= a - 1e-9 && x <= b + 1e-9) {
545            0.0
546        } else {
547            ((0.5 * (a + b) - x) / TAU).round() * TAU
548        }
549    };
550    let shift = Vec2::new(
551        into(middle.x, lo.x, hi.x, pu),
552        into(middle.y, lo.y, hi.y, pv),
553    );
554    for p in &mut guide {
555        *p += shift;
556    }
557    Ok(Piece {
558        curve: section.curve.clone(),
559        span: section.span,
560        pcurve: Curve2::Lifted(axiolid_curve::LiftedCurve2 {
561            curve: Box::new(section.curve.clone()),
562            carrier,
563            start: t0,
564            end: t1,
565            guide,
566        }),
567        pspan: section.span,
568        source: PieceSource::Section(index),
569    })
570}
571
572/// A section edge's implicit pcurve (ADR 0077): the stretch of the other
573/// surface's traced equation, in this face's parameters, from the edge's
574/// start through its middle to its end.
575#[allow(clippy::too_many_arguments)]
576fn implicit_piece(
577    surface: &Surface,
578    other: &Surface,
579    section: &SectionEdge,
580    index: usize,
581    lo: Point2,
582    hi: Point2,
583    traces: &mut Traces,
584    tolerance: Tolerance,
585) -> Result<Piece, BooleanError> {
586    let uv = |p: Point3| -> Result<Point2, BooleanError> {
587        let (u, v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
588        Ok(Point2::new(u, v))
589    };
590    let at = |f: Scalar| {
591        evaluate3(
592            &section.curve,
593            section.span.start + f * (section.span.end - section.span.start),
594        )
595        .map_err(|_| BooleanError::Evaluation)
596    };
597    let closed = (section.start - section.end).length() <= tolerance.linear().max(1e-9);
598    let curves = match traces.of(surface, other, lo, hi) {
599        Ok(curves) => curves,
600        // The other surface has no equation to read here (a B-spline), or
601        // its trace here cannot be decided (branches nearly touching, read
602        // far from the parameters' origin): the section's own space curve,
603        // read on this face through its closed-form inverse.
604        Err(BooleanError::UnsupportedSplit) => {
605            return lifted_piece(surface, section, index, lo, hi, tolerance);
606        }
607        Err(e) => return Err(e),
608    };
609    let stretch = axiolid_nurbs::extract_stretch(
610        curves,
611        periods(surface),
612        uv(section.start)?,
613        [uv(at(1.0 / 3.0)?)?, uv(at(2.0 / 3.0)?)?],
614        uv(section.end)?,
615        closed,
616    )
617    .ok_or(BooleanError::UnsupportedSplit)?;
618    // Whole turns into the face's own range, judged at the middle.
619    let (pu, pv) = periods(surface);
620    let middle = stretch
621        .point(0.5 * stretch.end())
622        .ok_or(BooleanError::Evaluation)?;
623    let into = |x: Scalar, a: Scalar, b: Scalar, periodic: bool| {
624        if !periodic || (x >= a - 1e-9 && x <= b + 1e-9) {
625            return 0.0;
626        }
627        let k = ((0.5 * (a + b) - x) / TAU).round();
628        k * TAU
629    };
630    let stretch = stretch.shifted(
631        into(middle.x, lo.x, hi.x, pu),
632        into(middle.y, lo.y, hi.y, pv),
633    );
634    let end = stretch.end();
635    Ok(Piece {
636        curve: section.curve.clone(),
637        span: section.span,
638        pcurve: Curve2::Implicit(stretch),
639        pspan: Interval::new(0.0, end),
640        source: PieceSource::Section(index),
641    })
642}
643
644/// Whether a curve is an iso-curve of the surface -- a sphere's meridian or
645/// latitude, a cone's ruling or circle about its axis, a torus's tube or
646/// ring circle -- whose pcurve is a straight line, affine in the curve's own
647/// parameter.
648fn iso_curve(surface: &Surface, curve: &Curve3, tolerance: Tolerance) -> bool {
649    let eps = tolerance.linear().max(1e-9);
650    let parallel = |a: axiolid_core::Vec3, b: axiolid_core::Vec3| {
651        a.normalize().cross(b.normalize()).length() <= 1e-12
652    };
653    let on_axis =
654        |p: Point3, o: Point3, z: axiolid_core::Vec3| (p - o).cross(z.normalize()).length() <= eps;
655    match (surface, curve) {
656        (Surface::Sphere(sp), Curve3::Circle(c)) => {
657            let n = c.frame.x.cross(c.frame.y);
658            let meridian = (c.frame.origin - sp.frame.origin).length() <= eps
659                && (c.radius - sp.radius).abs() <= eps
660                && n.normalize().dot(sp.frame.z.normalize()).abs() <= 1e-12;
661            let latitude =
662                parallel(n, sp.frame.z) && on_axis(c.frame.origin, sp.frame.origin, sp.frame.z);
663            meridian || latitude
664        }
665        (Surface::Cone(k), Curve3::Line(l)) => {
666            let slope = k.semi_angle.tan();
667            let apex = k.frame.origin - k.frame.z.normalize() * (k.radius / slope);
668            let d = l.direction.normalize();
669            let through = (apex - l.origin).cross(d).length() <= eps;
670            let axis = k.frame.z.normalize();
671            through && (d.dot(axis).abs() - k.semi_angle.cos().abs()).abs() <= 1e-12
672        }
673        (Surface::Cone(k), Curve3::Circle(c)) => {
674            let n = c.frame.x.cross(c.frame.y);
675            parallel(n, k.frame.z) && on_axis(c.frame.origin, k.frame.origin, k.frame.z)
676        }
677        (Surface::Torus(t), Curve3::Circle(c)) => {
678            let n = c.frame.x.cross(c.frame.y);
679            let z = t.frame.z.normalize();
680            let ring = parallel(n, z) && on_axis(c.frame.origin, t.frame.origin, z);
681            let d = c.frame.origin - t.frame.origin;
682            let tube = n.normalize().dot(z).abs() <= 1e-12
683                && d.dot(z).abs() <= eps
684                && (d.length() - t.major_radius).abs() <= eps
685                && (c.radius - t.minor_radius).abs() <= eps;
686            ring || tube
687        }
688        _ => false,
689    }
690}
691
692/// The straight pcurve of an iso-curve piece, affine in the curve's
693/// parameter: read at two interior points (clear of a pole at an end,
694/// where the angle has no value) and extended linearly.
695fn affine_pcurve(
696    surface: &Surface,
697    section: &SectionEdge,
698    start_uv: Point2,
699    tolerance: Tolerance,
700) -> Result<(Curve2, Interval), BooleanError> {
701    let (t0, t1) = (section.span.start, section.span.end);
702    let (ta, tb) = (t0 + 0.25 * (t1 - t0), t0 + 0.75 * (t1 - t0));
703    let (pu, pv) = periods(surface);
704    let uv = |t: Scalar, near: Point2| -> Result<Point2, BooleanError> {
705        let p = evaluate3(&section.curve, t).map_err(|_| BooleanError::Evaluation)?;
706        let (mut u, mut v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
707        if pu {
708            u += ((near.x - u) / TAU).round() * TAU;
709        }
710        if pv {
711            v += ((near.y - v) / TAU).round() * TAU;
712        }
713        Ok(Point2::new(u, v))
714    };
715    let a = uv(ta, start_uv)?;
716    let b = uv(tb, a)?;
717    let direction = (b - a) / (tb - ta);
718    Ok((
719        Curve2::Line(Line2 {
720            origin: a - direction * ta,
721            direction,
722        }),
723        section.span,
724    ))
725}
726
727/// [`section_piece`] with the start's parameters given.
728fn section_piece_from(
729    surface: &Surface,
730    section: &SectionEdge,
731    index: usize,
732    start_uv: Point2,
733    tolerance: Tolerance,
734) -> Result<Piece, BooleanError> {
735    let (pcurve, pspan) = match (surface, &section.curve) {
736        _ if iso_curve(surface, &section.curve, tolerance) => {
737            affine_pcurve(surface, section, start_uv, tolerance)?
738        }
739        (Surface::Plane(p), curve) => {
740            let f = p.frame;
741            let local = |q: Point3| Point2::new((q - f.origin).dot(f.x), (q - f.origin).dot(f.y));
742            let dir = |d: axiolid_core::Vec3| Vec2::new(d.dot(f.x), d.dot(f.y));
743            let pcurve = match curve {
744                Curve3::Line(l) => Curve2::Line(Line2 {
745                    origin: local(l.origin),
746                    direction: dir(l.direction),
747                }),
748                Curve3::Circle(c) => Curve2::Ellipse(Ellipse2 {
749                    frame: Frame2 {
750                        origin: local(c.frame.origin),
751                        x: dir(c.frame.x),
752                        y: dir(c.frame.y),
753                    },
754                    semi_axis_x: c.radius,
755                    semi_axis_y: c.radius,
756                }),
757                Curve3::Ellipse(e) => Curve2::Ellipse(Ellipse2 {
758                    frame: Frame2 {
759                        origin: local(e.frame.origin),
760                        x: dir(e.frame.x),
761                        y: dir(e.frame.y),
762                    },
763                    semi_axis_x: e.semi_axis_x,
764                    semi_axis_y: e.semi_axis_y,
765                }),
766                _ => return Err(BooleanError::UnsupportedSplit),
767            };
768            // On a plane the pcurve shares the curve's parameter.
769            (pcurve, section.span)
770        }
771        (Surface::Cylinder(c), Curve3::Line(l)) => {
772            // A ruling: constant angle, height linear in the parameter.
773            let pcurve = Curve2::Line(Line2 {
774                origin: start_uv - Vec2::new(0.0, l.direction.dot(c.frame.z)) * section.span.start,
775                direction: Vec2::new(0.0, l.direction.dot(c.frame.z)),
776            });
777            (pcurve, section.span)
778        }
779        (Surface::Cylinder(c), Curve3::Circle(circle)) => {
780            // About the axis: constant height, angle turning with the
781            // parameter one way or the other.
782            let turn = circle.frame.x.cross(circle.frame.y).dot(c.frame.z).signum();
783            let pcurve = Curve2::Line(Line2 {
784                origin: start_uv - Vec2::new(turn, 0.0) * section.span.start,
785                direction: Vec2::new(turn, 0.0),
786            });
787            (pcurve, section.span)
788        }
789        (Surface::Cylinder(c), Curve3::Ellipse(ellipse)) => {
790            // A plane's oblique cut: v = mean + a cos u + b sin u, with the
791            // angle itself as parameter. The plane is the ellipse's own.
792            let n = ellipse.frame.x.cross(ellipse.frame.y);
793            let nz = n.dot(c.frame.z);
794            if nz == 0.0 {
795                return Err(BooleanError::UnsupportedSplit);
796            }
797            let wave = Sinusoid2 {
798                mean: n.dot(ellipse.frame.origin - c.frame.origin) / nz,
799                cosine: -c.radius * n.dot(c.frame.x) / nz,
800                sine: -c.radius * n.dot(c.frame.y) / nz,
801            };
802            let pcurve = Curve2::Sinusoid(wave);
803            let span = angle_span(surface, section, start_uv.x, tolerance)?;
804            (pcurve, span)
805        }
806        _ => return Err(BooleanError::UnsupportedSplit),
807    };
808    Ok(Piece {
809        curve: section.curve.clone(),
810        span: section.span,
811        pcurve,
812        pspan,
813        source: PieceSource::Section(index),
814    })
815}
816
817/// The angle span a section covers on a cylinder, unwrapped through its
818/// midpoint from `start`.
819fn angle_span(
820    surface: &Surface,
821    section: &SectionEdge,
822    start: Scalar,
823    tolerance: Tolerance,
824) -> Result<Interval, BooleanError> {
825    let at = |t: Scalar| -> Result<Scalar, BooleanError> {
826        let p = evaluate3(&section.curve, t).map_err(|_| BooleanError::Evaluation)?;
827        Ok(locate(surface, p, tolerance)
828            .map_err(|_| BooleanError::Evaluation)?
829            .0)
830    };
831    let near = |raw: Scalar, reference: Scalar| raw + TAU * ((reference - raw) / TAU).round();
832    let (t0, t1) = (section.span.start, section.span.end);
833    let full = (t1 - t0).abs() >= TAU - 1e-12;
834    let q1 = near(at(t0 + 0.25 * (t1 - t0))?, start);
835    let q2 = near(at(t0 + 0.5 * (t1 - t0))?, q1);
836    let q3 = near(at(t0 + 0.75 * (t1 - t0))?, q2);
837    let end = if full {
838        start + TAU * (q1 - start).signum()
839    } else {
840        near(at(t1)?, q3)
841    };
842    Ok(Interval::new(start, end))
843}
844
845/// A point's parameters on the surface, the angle shifted by whole turns
846/// into the face's own range.
847fn place(
848    surface: &Surface,
849    point: Point3,
850    lo: Point2,
851    hi: Point2,
852    tolerance: Tolerance,
853) -> Result<Point2, BooleanError> {
854    let (mut u, mut v) = locate(surface, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
855    let (pu, pv) = periods(surface);
856    let slack = 1e-9;
857    let wrap = |x: &mut Scalar, a: Scalar, b: Scalar| {
858        while *x < a - slack {
859            *x += TAU;
860        }
861        while *x > b + slack {
862            *x -= TAU;
863        }
864    };
865    if pu {
866        wrap(&mut u, lo.x, hi.x);
867    }
868    if pv {
869        wrap(&mut v, lo.y, hi.y);
870    }
871    Ok(Point2::new(u, v))
872}
873
874/// Split one boundary use wherever a section ends strictly inside it.
875fn split_use(
876    surface: &Surface,
877    piece: Piece,
878    ends: &[Point3],
879    tolerance: Tolerance,
880) -> Result<Vec<Piece>, BooleanError> {
881    let mut cuts: Vec<(Scalar, Scalar)> = Vec::new();
882    let (lo, hi) = (
883        piece.span.start.min(piece.span.end),
884        piece.span.start.max(piece.span.end),
885    );
886    let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
887    for &point in ends {
888        let Ok(t) = locate3(&piece.curve, point, tolerance) else {
889            continue;
890        };
891        let t = if matches!(piece.curve, Curve3::Circle(_) | Curve3::Ellipse(_)) {
892            [t - TAU, t, t + TAU, t + 2.0 * TAU]
893                .into_iter()
894                .find(|c| *c > lo + slack && *c < hi - slack)
895        } else {
896            (t > lo + slack && t < hi - slack).then_some(t)
897        };
898        let Some(t) = t else { continue };
899        let on = evaluate3(&piece.curve, t).map_err(|_| BooleanError::Evaluation)?;
900        if (on - point).length() > tolerance.linear().max(1e-9) {
901            continue;
902        }
903        // The same point on the pcurve, through the surface's parameters.
904        let (u, v) = locate(surface, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
905        let (pu, pv) = periods(surface);
906        let turns: &[Scalar] = &[0.0, TAU, -TAU, 2.0 * TAU, -2.0 * TAU];
907        let mut shifts = Vec::new();
908        for &a in if pu { turns } else { &turns[..1] } {
909            for &b in if pv { turns } else { &turns[..1] } {
910                shifts.push((a, b));
911            }
912        }
913        let mut found = None;
914        for (du, dv) in shifts {
915            if let Ok(p) = locate2(&piece.pcurve, Point2::new(u + du, v + dv), tolerance) {
916                let (plo, phi) = (
917                    piece.pspan.start.min(piece.pspan.end),
918                    piece.pspan.start.max(piece.pspan.end),
919                );
920                let candidates = [p - TAU, p, p + TAU];
921                if let Some(p) = candidates
922                    .into_iter()
923                    .find(|c| *c >= plo - slack && *c <= phi + slack)
924                {
925                    found = Some(p);
926                    break;
927                }
928            }
929        }
930        let p = found.ok_or(BooleanError::Evaluation)?;
931        if !cuts.iter().any(|(c, _)| (c - t).abs() <= slack) {
932            cuts.push((t, p));
933        }
934    }
935    if cuts.is_empty() {
936        return Ok(vec![piece]);
937    }
938    let forward = piece.span.end >= piece.span.start;
939    cuts.sort_by(|a, b| {
940        let order = a.0.total_cmp(&b.0);
941        if forward {
942            order
943        } else {
944            order.reverse()
945        }
946    });
947    let mut out = Vec::with_capacity(cuts.len() + 1);
948    let (mut t0, mut p0) = (piece.span.start, piece.pspan.start);
949    for (t, p) in cuts {
950        out.push(Piece {
951            span: Interval::new(t0, t),
952            pspan: Interval::new(p0, p),
953            ..piece.clone()
954        });
955        (t0, p0) = (t, p);
956    }
957    out.push(Piece {
958        span: Interval::new(t0, piece.span.end),
959        pspan: Interval::new(p0, piece.pspan.end),
960        ..piece
961    });
962    Ok(out)
963}
964
965/// A directed use of a piece in the graph.
966#[derive(Debug, Clone, Copy)]
967struct Half {
968    piece: usize,
969    reversed: bool,
970    from: usize,
971    to: usize,
972    /// Direction leaving `from`, and direction arriving at `to`.
973    leave: Scalar,
974    arrive: Scalar,
975    /// Signed curvature in the running direction at `from` and at `to`,
976    /// which orders pieces leaving a vertex in the same direction.
977    bend_leave: Scalar,
978    bend_arrive: Scalar,
979    /// Points a small way along from `from` and back from `to`, at the
980    /// fractions of [`PROBES`]: where two pieces leave a vertex with the
981    /// same direction and bend (touching to third order or more), their
982    /// chords to these points still part.
983    probe_leave: [Point2; 3],
984    probe_arrive: [Point2; 3],
985}
986
987/// Fractions of a piece's span at which [`Half`] probes it from each end.
988const PROBES: [Scalar; 3] = [1e-4, 1e-3, 1e-2];
989
990fn directed(piece: &Piece, reversed: bool) -> Piece {
991    if reversed {
992        Piece {
993            span: Interval::new(piece.span.end, piece.span.start),
994            pspan: Interval::new(piece.pspan.end, piece.pspan.start),
995            ..piece.clone()
996        }
997    } else {
998        piece.clone()
999    }
1000}
1001
1002fn angle_of(v: Vec2) -> Scalar {
1003    v.y.atan2(v.x)
1004}
1005
1006/// Tangent direction of a piece in parameters at one of its ends, in the
1007/// piece's own running direction.
1008fn tangent(piece: &Piece, at_start: bool) -> Result<Vec2, BooleanError> {
1009    let p = if at_start {
1010        piece.pspan.start
1011    } else {
1012        piece.pspan.end
1013    };
1014    let d = derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1015    let sign = if piece.pspan.end >= piece.pspan.start {
1016        1.0
1017    } else {
1018        -1.0
1019    };
1020    Ok(d * sign)
1021}
1022
1023/// Signed curvature of a piece in parameters at one of its ends, in the
1024/// piece's running direction.
1025fn bend(piece: &Piece, at_start: bool) -> Result<Scalar, BooleanError> {
1026    let p = if at_start {
1027        piece.pspan.start
1028    } else {
1029        piece.pspan.end
1030    };
1031    let d = derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1032    let dd = second_derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1033    let speed = d.length();
1034    if speed == 0.0 {
1035        return Err(BooleanError::Evaluation);
1036    }
1037    // Reversing the parameter keeps d x dd's magnitude but flips its sign.
1038    let sign = if piece.pspan.end >= piece.pspan.start {
1039        1.0
1040    } else {
1041        -1.0
1042    };
1043    Ok(sign * (d.x * dd.y - d.y * dd.x) / (speed * speed * speed))
1044}
1045
1046fn trace(pieces: &[(Piece, bool)]) -> Result<Vec<Region>, BooleanError> {
1047    // Weld piece ends in parameters.
1048    let mut vertices: Vec<Point2> = Vec::new();
1049    let mut vertex = |p: Point2| -> usize {
1050        let slack = 1e-7 * (1.0 + p.x.abs().max(p.y.abs()));
1051        if let Some(i) = vertices.iter().position(|q| (*q - p).length() <= slack) {
1052            return i;
1053        }
1054        vertices.push(p);
1055        vertices.len() - 1
1056    };
1057    let mut halves = Vec::new();
1058    for (index, (piece, both_ways)) in pieces.iter().enumerate() {
1059        let a =
1060            evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?;
1061        let b = evaluate2(&piece.pcurve, piece.pspan.end).map_err(|_| BooleanError::Evaluation)?;
1062        let (va, vb) = (vertex(a), vertex(b));
1063        let leave = angle_of(tangent(piece, true)?);
1064        let arrive = angle_of(tangent(piece, false)?);
1065        let (bend_leave, bend_arrive) = (bend(piece, true)?, bend(piece, false)?);
1066        let (t0, t1) = (piece.pspan.start, piece.pspan.end);
1067        let probe = |f: Scalar| -> Result<Point2, BooleanError> {
1068            evaluate2(&piece.pcurve, t0 + (t1 - t0) * f).map_err(|_| BooleanError::Evaluation)
1069        };
1070        let near_start = [probe(PROBES[0])?, probe(PROBES[1])?, probe(PROBES[2])?];
1071        let near_end = [
1072            probe(1.0 - PROBES[0])?,
1073            probe(1.0 - PROBES[1])?,
1074            probe(1.0 - PROBES[2])?,
1075        ];
1076        halves.push(Half {
1077            piece: index,
1078            reversed: false,
1079            from: va,
1080            to: vb,
1081            leave,
1082            arrive,
1083            bend_leave,
1084            bend_arrive,
1085            probe_leave: near_start,
1086            probe_arrive: near_end,
1087        });
1088        if *both_ways {
1089            halves.push(Half {
1090                piece: index,
1091                reversed: true,
1092                from: vb,
1093                to: va,
1094                leave: arrive + PI,
1095                arrive: leave + PI,
1096                bend_leave: -bend_arrive,
1097                bend_arrive: -bend_leave,
1098                probe_leave: near_end,
1099                probe_arrive: near_start,
1100            });
1101        }
1102    }
1103
1104    // Walk every loop.
1105    let mut used = vec![false; halves.len()];
1106    let mut loops: Vec<Vec<usize>> = Vec::new();
1107    for first in 0..halves.len() {
1108        if used[first] {
1109            continue;
1110        }
1111        let mut walk = Vec::new();
1112        let mut current = first;
1113        loop {
1114            if used[current] {
1115                if current == first {
1116                    break;
1117                }
1118                return Err(BooleanError::UnclosedSplit);
1119            }
1120            used[current] = true;
1121            walk.push(current);
1122            let here = halves[current];
1123            // Back along the arriving piece, then the first outgoing half
1124            // clockwise from there. Pieces leaving in one direction are
1125            // ordered a little way out, by how they bend: a piece bending
1126            // left of another lies clockwise-first from `back`.
1127            let back = here.arrive + PI;
1128            let back_bend = -here.bend_arrive;
1129            let at = vertices[here.to];
1130            // Third order: how far clockwise from the arriving piece's chord
1131            // a candidate's chord turns, a small way out.
1132            let chord_turns = |c: &Half| -> [Scalar; 3] {
1133                [0, 1, 2].map(|k| {
1134                    let (b, o) = (here.probe_arrive[k] - at, c.probe_leave[k] - at);
1135                    (angle_of(b) - angle_of(o)).rem_euclid(TAU)
1136                })
1137            };
1138            let mut keyed: Vec<((Scalar, Scalar), usize)> = Vec::new();
1139            for (index, candidate) in halves.iter().enumerate() {
1140                if candidate.from != here.to {
1141                    continue;
1142                }
1143                let is_twin = candidate.piece == here.piece && candidate.reversed != here.reversed;
1144                let turn = (back - candidate.leave).rem_euclid(TAU);
1145                // Second order: the turn a small step out is about
1146                // `turn + (back_bend - bend) * step`.
1147                let delta = back_bend - candidate.bend_leave;
1148                let key = if is_twin {
1149                    (TAU, Scalar::INFINITY)
1150                } else if !(1e-9..=TAU - 1e-9).contains(&turn) {
1151                    if delta.abs() <= 1e-9 * (1.0 + back_bend.abs()) {
1152                        // Leaving back along the arriving piece, bending
1153                        // alike: its chord a small way out says which side.
1154                        let c = chord_turns(candidate);
1155                        let Some(t) = c.into_iter().find(|t| *t > 1e-12 && *t < TAU - 1e-12) else {
1156                            return Err(BooleanError::TangentSplit);
1157                        };
1158                        keyed.push(((if t < PI { t.min(1e-10) } else { TAU }, t), index));
1159                        continue;
1160                    }
1161                    if delta > 0.0 {
1162                        (0.0, delta)
1163                    } else {
1164                        (TAU, delta)
1165                    }
1166                } else {
1167                    (turn, delta)
1168                };
1169                keyed.push((key, index));
1170            }
1171            let same_turn = |x: Scalar, y: Scalar| (x - y).abs() < 1e-9;
1172            let same_bend =
1173                |x: Scalar, y: Scalar| (x - y).abs() <= 1e-9 * (1.0 + x.abs().min(1e12));
1174            // Tied in direction and bend: the chords a small way out, at the
1175            // smallest step where they part.
1176            let chords = |a: usize, b: usize| -> core::cmp::Ordering {
1177                let (ca, cb) = (chord_turns(&halves[a]), chord_turns(&halves[b]));
1178                for k in 0..3 {
1179                    if (ca[k] - cb[k]).abs() > 1e-12 {
1180                        return ca[k].total_cmp(&cb[k]);
1181                    }
1182                }
1183                core::cmp::Ordering::Equal
1184            };
1185            let order = |x: &((Scalar, Scalar), usize), y: &((Scalar, Scalar), usize)| {
1186                let ((xt, xb), (yt, yb)) = (x.0, y.0);
1187                if !same_turn(xt, yt) {
1188                    xt.total_cmp(&yt)
1189                } else if !same_bend(xb, yb) {
1190                    xb.total_cmp(&yb)
1191                } else {
1192                    chords(x.1, y.1)
1193                }
1194            };
1195            keyed.sort_by(order);
1196            if let [first_entry, second_entry, ..] = keyed.as_slice() {
1197                let ((ft, fb), (st, sb)) = (first_entry.0, second_entry.0);
1198                if same_turn(ft, st)
1199                    && same_bend(fb, sb)
1200                    && ft < TAU
1201                    && chords(first_entry.1, second_entry.1) == core::cmp::Ordering::Equal
1202                {
1203                    return Err(BooleanError::TangentSplit);
1204                }
1205            }
1206            let best = keyed.first().copied();
1207            current = best.ok_or(BooleanError::UnclosedSplit)?.1;
1208        }
1209        loops.push(walk);
1210    }
1211
1212    // Loops as pieces, with signed areas.
1213    let as_pieces = |walk: &[usize]| -> Vec<Piece> {
1214        walk.iter()
1215            .map(|&h| directed(&pieces[halves[h].piece].0, halves[h].reversed))
1216            .collect()
1217    };
1218    let mut outers: Vec<(Vec<Piece>, Scalar, Vec<Point2>)> = Vec::new();
1219    let mut holes: Vec<(Vec<Piece>, Vec<Point2>)> = Vec::new();
1220    for walk in &loops {
1221        let loop_pieces = as_pieces(walk);
1222        let polygon = sample(&loop_pieces)?;
1223        let area = shoelace(&polygon);
1224        if area > 0.0 {
1225            outers.push((loop_pieces, area, polygon));
1226        } else {
1227            holes.push((loop_pieces, polygon));
1228        }
1229    }
1230    let mut regions: Vec<Region> = outers
1231        .iter()
1232        .map(|(pieces, _, _)| Region {
1233            outer: pieces.clone(),
1234            holes: Vec::new(),
1235            against: false,
1236        })
1237        .collect();
1238    for (hole, polygon) in holes {
1239        // A point just left of the hole's boundary, on the owner's side (a
1240        // hole runs clockwise): a point on the boundary itself would also
1241        // read as inside the region the same curve bounds from within.
1242        let piece = &hole[0];
1243        let t = 0.5 * (piece.pspan.start + piece.pspan.end);
1244        let at = evaluate2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?;
1245        let mut d = derivative2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?;
1246        if piece.pspan.end < piece.pspan.start {
1247            d = -d;
1248        }
1249        let size = {
1250            let (mut lo, mut hi) = (polygon[0], polygon[0]);
1251            for p in &polygon {
1252                lo = lo.min(*p);
1253                hi = hi.max(*p);
1254            }
1255            (hi - lo).length()
1256        };
1257        let left = Vec2::new(-d.y, d.x).normalize_or_zero();
1258        let probe = at + left * (1e-6 * size.max(1e-9));
1259        let owner = outers
1260            .iter()
1261            .enumerate()
1262            .filter(|(_, (_, _, outer))| inside_polygon(outer, probe))
1263            .min_by(|a, b| a.1 .1.total_cmp(&b.1 .1))
1264            .map(|(index, _)| index)
1265            .ok_or(BooleanError::UnclosedSplit)?;
1266        regions[owner].holes.push(hole);
1267    }
1268    Ok(regions)
1269}
1270
1271/// Dense points around a loop, for its area sign and containment only.
1272fn sample(pieces: &[Piece]) -> Result<Vec<Point2>, BooleanError> {
1273    let mut out = Vec::new();
1274    for piece in pieces {
1275        for i in 0..32 {
1276            let p = piece.pspan.start + (piece.pspan.end - piece.pspan.start) * i as Scalar / 32.0;
1277            out.push(evaluate2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?);
1278        }
1279    }
1280    Ok(out)
1281}
1282
1283fn shoelace(polygon: &[Point2]) -> Scalar {
1284    let mut area = 0.0;
1285    for i in 0..polygon.len() {
1286        let (p, q) = (polygon[i], polygon[(i + 1) % polygon.len()]);
1287        area += p.x * q.y - q.x * p.y;
1288    }
1289    0.5 * area
1290}
1291
1292pub(crate) fn inside_polygon(polygon: &[Point2], p: Point2) -> bool {
1293    let mut inside = false;
1294    for i in 0..polygon.len() {
1295        let (a, b) = (polygon[i], polygon[(i + 1) % polygon.len()]);
1296        if (a.y > p.y) != (b.y > p.y) {
1297            let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1298            if x > p.x {
1299                inside = !inside;
1300            }
1301        }
1302    }
1303    inside
1304}