Skip to main content

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