Skip to main content

axiolid_brep_boolean/
section.rs

1//! Section edges: where two exact B-reps' faces cross (ADR 0075, step 2).
2//!
3//! For every pair of faces, one from each operand:
4//!
5//! 1. The support surfaces' exact intersection curves come from
6//!    `exact_surface_intersection` (#119): lines and conics, ruled and
7//!    torus sections (ADR 0076), traced sections (ADR 0077). A ruled or
8//!    torus section piece is read as a traced section over its span, so
9//!    every curve past this point is a line, a conic or an
10//!    `ImplicitSection3`.
11//! 2. Each curve is cut where it crosses a boundary edge of either face.
12//!    Two curves on one surface meet only up to the rounding of their
13//!    constructed doubles, so the crossing is found transversally instead:
14//!    where the curve crosses the ADJACENT face's surface
15//!    (`exact_curve_surface_intersection`), kept when the hit lies on the
16//!    edge within tolerance and inside its span. Across a seam, where the
17//!    adjacent face shares the surface, the cutter is the plane through the
18//!    ruling and the surface normal.
19//! 3. Each piece between consecutive cuts lies wholly inside or wholly
20//!    outside each face, so its midpoint decides it, through the face's
21//!    certified domain (`FaceDomain`). A piece inside both faces is a section
22//!    edge.
23//!
24//! Faces that meet without crossing are handled, not refused:
25//!
26//! - **Tangent supports** touch in a point or along a curve without
27//!   crossing, so they add no section: neither face changes sides there.
28//! - **A section along an existing edge** (the curve lies in the adjacent
29//!   face's surface too) is cut at the edge's ends and marked as lying
30//!   along that face's boundary: it splits the other face, not this one. A
31//!   traced piece along a line or conic edge is that edge's own curve over
32//!   its stretch, so one stretch has one curve on every face (#382: a
33//!   revolved torus wall's cap edge against the plane through its axis).
34//! - **Coincident supports** share a patch of surface. Each face's boundary
35//!   edges are imprinted on the other where they run inside it, so the
36//!   shared patch becomes a region of both faces; classification then sees
37//!   it on the other solid's boundary (see `crate::boolean`). An imprinted
38//!   edge's cuts are merged as a section's, and a cut within tolerance of
39//!   the edge's own end is that end (#276, `imprint_cuts`).
40//!
41//! Work that cannot matter is skipped through sound boxes (`crate::bounds`,
42//! enlarged by the tolerance): face pairs whose boxes are apart are not
43//! sectioned, and a line or conic section is cut only against edges whose
44//! boxes meet the two faces' common box (widened by `2^-20` of its
45//! diagonal, so its planes stay off the faces' extremes), plus where it
46//! leaves that box; pieces outside it lie off one of the faces.
47//!
48//! Operands placed by independent rigid motions agree only up to rounding,
49//! and the exact predicates above see that residue (#228). Within
50//! tolerance, then:
51//!
52//! - a plane parallel or perpendicular to a cylinder's axis cuts rulings
53//!   or a circle, not a degenerate ellipse;
54//! - a curve running along a boundary edge is read as running along it;
55//! - a curve that touches the edge's adjacent surface (a double root,
56//!   which rounding splits in two or loses) is cut where it meets the edge
57//!   itself, and cuts closer together than tolerance are one cut;
58//! - a plane touching a cylinder is read once for the two supports, and a
59//!   curve on either one is cut by the other where it meets their contact
60//!   ruling, in every face pair alike (`crate::contact`, #243).
61//!
62//! Along a smooth edge (a fillet running tangent into its neighbour) a
63//! section is cut where the edge crosses the section's other surface, and
64//! a trace that cannot be decided in a face's box of a periodic carrier is
65//! taken over its whole turn (`crate::tangency`, #249).
66
67use axiolid_brep::ExactBRep;
68use axiolid_core::{Interval, Point2, Point3, Scalar, Tolerance, Vec3};
69use axiolid_curve::Curve3;
70use axiolid_evaluate::surface::{locate, normal};
71use axiolid_evaluate::{curve::locate3, evaluate3};
72use axiolid_measure::FaceDomain;
73use axiolid_nurbs::{
74    exact_curve_curve_intersection3, exact_curve_surface_intersection, exact_surface_intersection,
75    spline_pair_intersection, ExactCurveIntersection, ExactIntersectionRefusal,
76};
77use axiolid_surface::{Plane, Surface};
78use axiolid_topology::FaceId;
79use core::f64::consts::TAU;
80
81use crate::bounds::{edge_box, face_box, Aabb};
82use crate::contact::{Contact, Contacts};
83use crate::predicate;
84use crate::report::{self, Reading, ToleranceDecisionKind};
85use crate::support::{cleaned, same_support, window};
86use crate::BooleanError;
87
88/// Where along a piece its inside/outside sample is taken: off-centre, so
89/// the midpoint of a symmetric section (a meridian's pole) is never it.
90const SAMPLE: Scalar = 0.414_213_562_373_095;
91
92/// How far a section's window reaches past the two faces' common box,
93/// relative to its diagonal: `2^-20`. Widening is sound (the window only
94/// skips work outside it), and keeps the window's planes off the faces'
95/// extremes, where a tangent section would leave it next to the touching
96/// point (#243).
97const WINDOW_PAD: Scalar = 1.0 / 1_048_576.0;
98
99/// A piece of an intersection curve lying on a face of each operand.
100#[derive(Debug, Clone, PartialEq)]
101pub struct SectionEdge {
102    /// The face of the first operand the edge lies on.
103    pub face_a: FaceId,
104    /// The face of the second operand the edge lies on.
105    pub face_b: FaceId,
106    /// The exact curve.
107    pub curve: Curve3,
108    /// The curve's parameter span; for a circle or ellipse it may run past
109    /// `2 pi`.
110    pub span: Interval,
111    /// The curve's point at `span.start`.
112    pub start: Point3,
113    /// The curve's point at `span.end`.
114    pub end: Point3,
115    /// Whether the edge runs along a boundary edge `face_a` already has, so
116    /// it does not split `face_a`.
117    pub along_a: bool,
118    /// Whether the edge runs along a boundary edge `face_b` already has.
119    pub along_b: bool,
120    /// The surface that meets `face_a`'s surface along this edge: `face_b`'s
121    /// for a crossing, or for an edge imprinted from a coincident face the
122    /// surface that bounds it there. Its equation on `face_a` is the edge's
123    /// pcurve.
124    pub other_a: Surface,
125    /// The surface that meets `face_b`'s surface along this edge.
126    pub other_b: Surface,
127}
128
129/// Every section edge between the faces of `a` and the faces of `b`.
130///
131/// # Errors
132///
133/// A face pair whose section is not a line, circle or ellipse, or a piece
134/// too close to a face boundary to classify.
135pub fn section_edges(
136    a: &ExactBRep,
137    b: &ExactBRep,
138    tolerance: Tolerance,
139) -> Result<Vec<SectionEdge>, BooleanError> {
140    let session = report::open(&[a, b], tolerance);
141    let (edges, _) = sections_and_contacts(a, b, tolerance)?;
142    session.finish()?;
143    Ok(edges)
144}
145
146/// [`section_edges`], with the plane/cylinder contacts read within
147/// tolerance on the way (`crate::contact`), which classification must
148/// respect too.
149pub(crate) fn sections_and_contacts(
150    a: &ExactBRep,
151    b: &ExactBRep,
152    tolerance: Tolerance,
153) -> Result<(Vec<SectionEdge>, Contacts), BooleanError> {
154    let side_a = Side::new(a, tolerance)?;
155    let side_b = Side::new(b, tolerance)?;
156    let mut out = Vec::new();
157    // Each plane/cylinder pair of supports is read once, over the largest
158    // window any pair of their faces needs, so every face pair of the two
159    // agrees (#243).
160    let (readings, contacts) = plane_cylinder_readings(&side_a, &side_b, tolerance)?;
161    // Surfaces are shared by many faces: each pair's closed form once.
162    #[allow(clippy::type_complexity)]
163    let mut closed_forms: Vec<(
164        Surface,
165        Surface,
166        Result<axiolid_nurbs::ExactIntersectionCurve, ExactIntersectionRefusal>,
167    )> = Vec::new();
168    for fa in 0..side_a.faces.len() {
169        for fb in 0..side_b.faces.len() {
170            // Faces whose boxes, enlarged by the tolerance, are apart cannot
171            // cross, touch or share a patch (#228).
172            // Where both faces have boxes, the section matters only in their
173            // common part: the window. It is widened a little past the
174            // faces' boxes, whose planes run through the faces' extremes: a
175            // section touching one there (a hole tangent to a face) would
176            // leave the window a hair from the touching point, and the
177            // piece between could not be told inside or outside (#243).
178            let common = match (&side_a.boxes[fa], &side_b.boxes[fb]) {
179                (Some(a), Some(b)) => match a.intersection(b) {
180                    Some(common) => Some(common.widened(WINDOW_PAD * common.diagonal())),
181                    None => continue,
182                },
183                _ => None,
184            };
185            let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
186            if same_support(sa, sb, tolerance) {
187                imprint(
188                    &side_a, fa, &side_b, fb, true, &contacts, tolerance, &mut out,
189                )?;
190                imprint(
191                    &side_b, fb, &side_a, fa, false, &contacts, tolerance, &mut out,
192                )?;
193                continue;
194            }
195            // Lines and conics in closed form; every other section traced in
196            // one face's parameter box (ADR 0077), which holds every part
197            // of it that can matter.
198            let splines = matches!((sa, sb), (Surface::BSpline(_), Surface::BSpline(_)));
199            // A plane parallel or perpendicular to a cylinder's axis to
200            // within rounding, as read for the two supports.
201            let rulings = readings
202                .iter()
203                .find(|(x, y, _)| x == sa && y == sb)
204                .and_then(|(_, _, reading)| reading.as_ref())
205                .map(|reading| reading.curves.clone());
206            if rulings.as_ref().is_some_and(Vec::is_empty) {
207                continue;
208            }
209            // Two B-splines are traced below, in the faces' own boxes.
210            let closed_form = if splines || rulings.is_some() {
211                None
212            } else {
213                let known = closed_forms
214                    .iter()
215                    .find(|(a, b, _)| a == sa && b == sb)
216                    .map(|(_, _, r)| r.clone());
217                let result = known.unwrap_or_else(|| {
218                    let r = exact_surface_intersection(&cleaned(sa), &cleaned(sb));
219                    closed_forms.push((sa.clone(), sb.clone(), r.clone()));
220                    r
221                });
222                match result {
223                    Ok(curve) => {
224                        let conic = curve.branches.iter().zip(&curve.spans).all(|(b, s)| {
225                            matches!(
226                                (b, s),
227                                (Curve3::Line(_), _)
228                                    | (Curve3::Circle(_) | Curve3::Ellipse(_), None)
229                            )
230                        });
231                        conic.then_some(curve)
232                    }
233                    // Apart, or touching without crossing: no section.
234                    Err(
235                        ExactIntersectionRefusal::Disjoint
236                        | ExactIntersectionRefusal::NotRegularCurve,
237                    ) => continue,
238                    Err(_) => None,
239                }
240            };
241            let branches: Vec<(Curve3, Option<Interval>)> = match (rulings, closed_form) {
242                (Some(lines), _) => lines.into_iter().map(|line| (line, None)).collect(),
243                (None, Some(curve)) => curve.branches.into_iter().zip(curve.spans).collect(),
244                (None, None) => {
245                    // Only a B-spline can carry a section with a B-spline
246                    // (it has no equation to read elsewhere); otherwise the
247                    // compact and low-degree surfaces carry best.
248                    let rank = |s: &Surface| match s {
249                        Surface::BSpline(_) => 4,
250                        Surface::Torus(_) => 3,
251                        Surface::Sphere(_) => 2,
252                        Surface::Plane(_) => 0,
253                        _ => 1,
254                    };
255                    // Two B-splines: the section carried on both, traced in
256                    // both faces' boxes (ADR 0077).
257                    if let (Surface::BSpline(ba), Surface::BSpline(bb)) = (sa, sb) {
258                        let (la, ha) = side_a.domains[fa].bounds();
259                        let (lb, hb) = side_b.domains[fb].bounds();
260                        match spline_pair_intersection(
261                            ba,
262                            bb,
263                            Some((window(sa, la, ha), window(sb, lb, hb))),
264                        ) {
265                            Ok(sections) => sections
266                                .into_iter()
267                                .map(|s| {
268                                    let end = s.end();
269                                    (Curve3::PairSection(s), Some(Interval::new(0.0, end)))
270                                })
271                                .collect(),
272                            Err(ExactIntersectionRefusal::Disjoint) => continue,
273                            Err(_) => return Err(BooleanError::UnsupportedSection),
274                        }
275                    } else {
276                        let (carrier, other, side, face) = if rank(sa) >= rank(sb) {
277                            (sa, sb, &side_a, fa)
278                        } else {
279                            (sb, sa, &side_b, fb)
280                        };
281                        let (lo, hi) = side.domains[face].bounds();
282                        match crate::tangency::trace(carrier, other, lo, hi) {
283                            Ok(sections) => sections
284                                .into_iter()
285                                .map(|s| {
286                                    let end = s.curve.end();
287                                    (Curve3::ImplicitSection(s), Some(Interval::new(0.0, end)))
288                                })
289                                .collect(),
290                            // Apart, or touching only at isolated points: no
291                            // section, as for the closed forms.
292                            Err(
293                                ExactIntersectionRefusal::Disjoint
294                                | ExactIntersectionRefusal::NotRegularCurve,
295                            ) => continue,
296                            Err(_) => return Err(BooleanError::UnsupportedSection),
297                        }
298                    }
299                }
300            };
301            for (index, (branch, span)) in branches.iter().enumerate() {
302                // A ray (a cone's ruling from its apex) is a line bounded
303                // at its finite ends.
304                let bounds = match (branch, span) {
305                    (Curve3::Line(_), Some(span)) => Some(*span),
306                    _ => None,
307                };
308                let branch = branch.clone();
309                let branch = &branch;
310                // The window cuts lines and conics in closed form; a traced
311                // section is cut against every edge, as before.
312                let common = common.filter(|_| {
313                    matches!(
314                        branch,
315                        Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
316                    )
317                });
318                let (mut cuts, along_a) =
319                    side_a.cuts(fa, branch, sb, common.as_ref(), &contacts, tolerance)?;
320                let (more, along_b) =
321                    side_b.cuts(fb, branch, sa, common.as_ref(), &contacts, tolerance)?;
322                cuts.extend(more);
323                // Branches of one section meet only where the surfaces touch
324                // (the two ellipses of a Steinmetz pair): such a point splits
325                // both, so the faces' graphs get a vertex there.
326                for (other, _) in branches
327                    .iter()
328                    .skip(index + 1)
329                    .chain(branches.iter().take(index))
330                {
331                    if let Ok(ExactCurveIntersection::Points(hits)) =
332                        exact_curve_curve_intersection3(branch, other)
333                    {
334                        cuts.extend(hits.iter().map(|h| h.parameter.approx()));
335                    }
336                }
337                if let Some(b) = bounds {
338                    let (lo, hi) = (b.start.min(b.end), b.start.max(b.end));
339                    cuts.retain(|&c| c >= lo && c <= hi);
340                    cuts.extend([lo, hi].into_iter().filter(|x| x.is_finite()));
341                }
342                let mut cuts = merge_close(branch, cuts, tolerance)?;
343                // The window's own crossings come after the merge: they are
344                // where the window ends, not features to be merged.
345                if let Some(w) = &common {
346                    let ends = window_ends(&cuts, w.crossings(branch));
347                    cuts.extend(ends.into_iter().filter(|&c| {
348                        bounds.is_none_or(|b| c >= b.start.min(b.end) && c <= b.start.max(b.end))
349                    }));
350                }
351                for (piece_curve, span) in pieces(branch, cuts)? {
352                    // Off-centre, so a symmetric section's pole or seam
353                    // crossing never becomes the sample.
354                    let mid = sample(&piece_curve, span, [sa, sb], &contacts)?;
355                    // Tangent contact adds no section. A traced section is
356                    // never mere touching (the trace drops touching points
357                    // and curves), so where its surfaces are tangent they
358                    // cross: it stays.
359                    let traced = matches!(
360                        piece_curve,
361                        Curve3::ImplicitSection(_) | Curve3::PairSection(_)
362                    );
363                    // Outside the window the piece is off one of the faces.
364                    if common.as_ref().is_some_and(|w| !w.contains(mid)) {
365                        continue;
366                    }
367                    if !traced && touching(sa, sb, mid, tolerance)? {
368                        continue;
369                    }
370                    let at_a = side_a.locate(fa, mid, &along_a, tolerance)?;
371                    if at_a == Place::Outside {
372                        continue;
373                    }
374                    let at_b = side_b.locate(fb, mid, &along_b, tolerance)?;
375                    if at_b == Place::Outside {
376                        continue;
377                    }
378                    // A traced piece along a line or conic edge is that edge's
379                    // own curve over its stretch (#382).
380                    let edges: Vec<&(Curve3, Interval)> = along_a
381                        .iter()
382                        .filter(|_| at_a == Place::Boundary)
383                        .chain(along_b.iter().filter(|_| at_b == Place::Boundary))
384                        .collect();
385                    let (piece_curve, span) =
386                        along_edge_curve(piece_curve, span, mid, &edges, tolerance)?;
387                    out.push(SectionEdge {
388                        face_a: side_a.faces[fa],
389                        face_b: side_b.faces[fb],
390                        start: evaluate3(&piece_curve, span.start)
391                            .map_err(|_| BooleanError::Evaluation)?,
392                        end: evaluate3(&piece_curve, span.end)
393                            .map_err(|_| BooleanError::Evaluation)?,
394                        curve: piece_curve,
395                        span,
396                        along_a: at_a == Place::Boundary,
397                        along_b: at_b == Place::Boundary,
398                        other_a: sb.clone(),
399                        other_b: sa.clone(),
400                    });
401                }
402            }
403        }
404    }
405    Ok((out, contacts))
406}
407
408/// One reading per pair of supports, a plane of one operand and a cylinder
409/// of the other ([`plane_cylinder_within_rounding`]), over the largest
410/// common box of any pair of their faces (none when a pair has no box),
411/// with the contacts it read within tolerance.
412#[allow(clippy::type_complexity)]
413fn plane_cylinder_readings(
414    side_a: &Side<'_>,
415    side_b: &Side<'_>,
416    tolerance: Tolerance,
417) -> Result<(Vec<(Surface, Surface, Option<PlaneCylinder>)>, Contacts), BooleanError> {
418    let mut extents: Vec<(Surface, Surface, Option<Scalar>)> = Vec::new();
419    for fa in 0..side_a.faces.len() {
420        for fb in 0..side_b.faces.len() {
421            let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
422            if !matches!(
423                (sa, sb),
424                (Surface::Plane(_), Surface::Cylinder(_))
425                    | (Surface::Cylinder(_), Surface::Plane(_))
426            ) {
427                continue;
428            }
429            let extent = match (&side_a.boxes[fa], &side_b.boxes[fb]) {
430                (Some(a), Some(b)) => match a.intersection(b) {
431                    Some(common) => Some(common.diagonal()),
432                    None => continue,
433                },
434                _ => None,
435            };
436            match extents.iter_mut().find(|(x, y, _)| x == sa && y == sb) {
437                Some((_, _, known)) => *known = known.zip(extent).map(|(p, q)| p.max(q)),
438                None => extents.push((sa.clone(), sb.clone(), extent)),
439            }
440        }
441    }
442    let mut contacts = Contacts::default();
443    let mut readings = Vec::with_capacity(extents.len());
444    for (sa, sb, extent) in extents {
445        let reading = plane_cylinder_within_rounding(&sa, &sb, extent, tolerance);
446        if let Some(PlaneCylinder {
447            curves,
448            contact: true,
449        }) = &reading
450        {
451            if let [Curve3::Line(ruling)] = curves.as_slice() {
452                let (plane, cylinder) = match sa {
453                    Surface::Plane(_) => (sa.clone(), sb.clone()),
454                    _ => (sb.clone(), sa.clone()),
455                };
456                contacts.push(Contact {
457                    plane,
458                    cylinder,
459                    ruling: *ruling,
460                });
461            }
462        }
463        readings.push((sa, sb, reading));
464    }
465    Ok((readings, contacts))
466}
467
468/// The point of a piece that decides whether it lies in a face: off-centre
469/// at [`SAMPLE`], or, where that point lies where a contact one of the
470/// piece's two `surfaces` is read in is disputed (`crate::contact`), the
471/// first point further along that is not. There the given operands and the
472/// moved ones the reading stands for put the point on different sides of
473/// the plane, and the piece was cut for the moved ones (#243).
474fn sample(
475    curve: &Curve3,
476    span: Interval,
477    surfaces: [&Surface; 2],
478    contacts: &Contacts,
479) -> Result<Point3, BooleanError> {
480    let at = |f: Scalar| {
481        evaluate3(curve, span.start + f * (span.end - span.start))
482            .map_err(|_| BooleanError::Evaluation)
483    };
484    let disputed = |p: Point3| surfaces.iter().any(|s| contacts.disputed(s, p));
485    let first = at(SAMPLE)?;
486    if !disputed(first) {
487        return Ok(first);
488    }
489    for f in [0.75, 0.9, 0.25, 0.1] {
490        let p = at(f)?;
491        if !disputed(p) {
492            return Ok(p);
493        }
494    }
495    Ok(first)
496}
497
498/// The section of a plane and a cylinder whose axis the plane is parallel
499/// or perpendicular to within the linear tolerance over where the section
500/// can matter, or `None` for any other pair.
501///
502/// The exact closed form decides parallel and perpendicular exactly, on the
503/// numbers given. Two operands placed by independent rigid motions -- an
504/// arched opening's jamb plane against its own soffit cylinder, or a wall's
505/// face against it, after a general rotation -- leave the normal and axis
506/// `1e-17` off, and the exact section is then an ellipse `1e16` long, or
507/// tilted by the residue, that nothing can evaluate. Read within rounding,
508/// the section is what the operands describe:
509///
510/// - parallel: two rulings, one where the plane touches the cylinder
511///   (within linear tolerance), or none (an empty list);
512/// - perpendicular: the circle where the axis pierces the plane.
513///
514/// Each curve lies on the cylinder exactly. It lies on a copy of the plane
515/// turned (and, for a touching ruling, moved) by at most the caller's
516/// linear tolerance `eps` over the region it can matter in, so the section
517/// is the exact section of operands perturbed by at most `eps`:
518///
519/// - perpendicular when `r sin(theta) <= eps`, `theta` the angle between
520///   the normal and the axis: the circle leaves the plane by at most
521///   `r sin(theta)`;
522/// - parallel when `|n . a| * extent <= eps`, `extent` the diagonal of the
523///   largest common box of a pair of the two supports' faces (`None`, a
524///   pair with no box: never read parallel; one reading per pair of
525///   supports, so all their face pairs agree, #243): a
526///   ruling leaves the plane by at most that over the box;
527/// - one ruling when the plane is within `eps` of touching the cylinder.
528///
529/// Outside those bounds the exact closed form decides (#228). Exactly
530/// parallel or perpendicular (`crate::predicate`), it decides too, unless
531/// the plane is read as touching (#236).
532fn plane_cylinder_within_rounding(
533    a: &Surface,
534    b: &Surface,
535    extent: Option<Scalar>,
536    tolerance: Tolerance,
537) -> Option<PlaneCylinder> {
538    let (cylinder, plane) = match (a, b) {
539        (Surface::Cylinder(c), Surface::Plane(p)) | (Surface::Plane(p), Surface::Cylinder(c)) => {
540            (c, p)
541        }
542        _ => return None,
543    };
544    let axis = cylinder.frame.z.normalize_or_zero();
545    let normal = plane.frame.z.normalize_or_zero();
546    if axis == Vec3::ZERO || normal == Vec3::ZERO {
547        return None;
548    }
549    let along = axis.dot(normal);
550    // Exactly perpendicular or parallel is the closed form's to decide; only
551    // a reading the exact predicates do not confirm is taken here, and
552    // reported (#236). These readings bound how far the plane moves, not how
553    // far it turns, so only the linear measure gates them.
554    let linear = Tolerance::new(tolerance.linear(), Scalar::MAX).unwrap_or(tolerance);
555    match report::support(
556        ToleranceDecisionKind::PlanePerpendicularToAxis,
557        cylinder.radius * axis.cross(normal).length(),
558        0.0,
559        linear,
560        || predicate::plane_perpendicular(plane, cylinder),
561    ) {
562        Reading::Exact => return None,
563        Reading::Apart => {}
564        Reading::Within => {
565            // Perpendicular to the axis within rounding: the circle where the
566            // axis pierces the plane, not an ellipse tilted by the residue.
567            let centre = cylinder.frame.origin
568                + axis * (normal.dot(plane.frame.origin - cylinder.frame.origin) / along);
569            let x = (cylinder.frame.x - axis * axis.dot(cylinder.frame.x)).normalize_or_zero();
570            if x == Vec3::ZERO || !centre.is_finite() {
571                return None;
572            }
573            return Some(PlaneCylinder {
574                curves: vec![Curve3::Circle(axiolid_curve::Circle3 {
575                    frame: axiolid_core::Frame3 {
576                        origin: centre,
577                        x,
578                        y: axis.cross(x),
579                        z: axis,
580                    },
581                    radius: cylinder.radius,
582                })],
583                contact: false,
584            });
585        }
586    }
587    // No common box: never read parallel.
588    let extent = extent?;
589    let parallel = report::support(
590        ToleranceDecisionKind::PlaneParallelToAxis,
591        along.abs() * extent,
592        0.0,
593        linear,
594        || predicate::plane_parallel(plane, cylinder),
595    );
596    if parallel == Reading::Apart {
597        return None;
598    }
599    // The normal with its rounding along the axis removed.
600    let normal = (normal - axis * along).normalize_or_zero();
601    if normal == Vec3::ZERO {
602        return None;
603    }
604    let distance = normal.dot(cylinder.frame.origin - plane.frame.origin);
605    let foot = cylinder.frame.origin - normal * distance;
606    let across = axis.cross(normal);
607    let r = cylinder.radius;
608    let touching = report::support(
609        ToleranceDecisionKind::PlaneTouchesCylinder,
610        (distance.abs() - r).abs(),
611        0.0,
612        linear,
613        || predicate::plane_touches(plane, cylinder),
614    );
615    // Exactly parallel, the closed form decides, unless the plane is read
616    // as touching within tolerance.
617    if parallel == Reading::Exact && touching != Reading::Within {
618        return None;
619    }
620    let offsets: Vec<Scalar> = if touching.holds() {
621        vec![0.0]
622    } else if distance.abs() > r {
623        Vec::new()
624    } else {
625        let half = (r * r - distance * distance).sqrt();
626        vec![half, -half]
627    };
628    Some(PlaneCylinder {
629        curves: offsets
630            .into_iter()
631            .map(|offset| {
632                Curve3::Line(axiolid_curve::Line3 {
633                    origin: foot + across * offset,
634                    direction: axis,
635                })
636            })
637            .collect(),
638        contact: touching == Reading::Within,
639    })
640}
641
642/// A plane/cylinder section read within rounding or tolerance
643/// ([`plane_cylinder_within_rounding`]).
644#[derive(Debug, Clone)]
645struct PlaneCylinder {
646    /// The rulings, or the circle, of the section; empty when the plane
647    /// misses the cylinder.
648    curves: Vec<Curve3>,
649    /// Whether the plane was read as touching the cylinder within
650    /// tolerance (not exactly): `curves` is then the contact ruling, which
651    /// every other crossing of the two surfaces must agree with
652    /// (`crate::contact`, #243).
653    contact: bool,
654}
655
656/// The points where a surface's angle parameter has no value: a sphere's
657/// poles, a cone's apex.
658fn poles(surface: &Surface) -> Vec<Point3> {
659    match surface {
660        Surface::Sphere(s) => {
661            let z = s.frame.z.normalize() * s.radius;
662            vec![s.frame.origin + z, s.frame.origin - z]
663        }
664        Surface::Cone(c) => {
665            let slope = c.semi_angle.tan();
666            if slope == 0.0 {
667                Vec::new()
668            } else {
669                vec![c.frame.origin - c.frame.z.normalize() * (c.radius / slope)]
670            }
671        }
672        _ => Vec::new(),
673    }
674}
675
676/// Whether two surfaces through `point` share their tangent plane there: a
677/// line where a cylinder rests on a plane, which it touches without
678/// crossing. Analytic surfaces tangent along a whole curve lie on one side
679/// of each other, so such a stretch splits neither face.
680fn touching(
681    a: &Surface,
682    b: &Surface,
683    point: Point3,
684    tolerance: Tolerance,
685) -> Result<bool, BooleanError> {
686    let at = |s: &Surface| -> Result<axiolid_core::Vec3, BooleanError> {
687        let (u, v) =
688            locate(s, point, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
689        Ok(normal(s, u, v)
690            .map_err(|_| BooleanError::Evaluation)?
691            .normalize())
692    };
693    Ok(report::near_angle(
694        ToleranceDecisionKind::Contact,
695        at(a)?.cross(at(b)?).length(),
696        tolerance,
697    ))
698}
699
700/// The boundary edges of `from`'s face `face` that run inside `onto`'s face
701/// `other`, on the same surface, as section edges (`first` when `from` is
702/// the first operand).
703#[allow(clippy::too_many_arguments)]
704fn imprint(
705    from: &Side<'_>,
706    face: usize,
707    onto: &Side<'_>,
708    other: usize,
709    first: bool,
710    contacts: &Contacts,
711    tolerance: Tolerance,
712    out: &mut Vec<SectionEdge>,
713) -> Result<(), BooleanError> {
714    for (edge, curve, span) in from.edges_of(face)? {
715        // The surface that bounds the imprinted edge in its own operand:
716        // together with the shared surface it defines the edge's curve.
717        let bounding = from.cutter(face, edge, &curve, span, tolerance, false)?;
718        let (cuts, along) = onto.cuts(other, &curve, &bounding, None, contacts, tolerance)?;
719        let (cuts, span) = imprint_cuts(&curve, span, cuts, tolerance)?;
720        let shared = onto.surface(other)?;
721        for piece in pieces_within(&curve, span, cuts) {
722            let mid = sample(&curve, piece, [shared, &bounding], contacts)?;
723            let at = onto.locate(other, mid, &along, tolerance)?;
724            if at == Place::Outside {
725                continue;
726            }
727            let (face_a, face_b, along_a, along_b) = if first {
728                (
729                    from.faces[face],
730                    onto.faces[other],
731                    true,
732                    at == Place::Boundary,
733                )
734            } else {
735                (
736                    onto.faces[other],
737                    from.faces[face],
738                    at == Place::Boundary,
739                    true,
740                )
741            };
742            out.push(SectionEdge {
743                face_a,
744                face_b,
745                curve: curve.clone(),
746                span: piece,
747                start: evaluate3(&curve, piece.start).map_err(|_| BooleanError::Evaluation)?,
748                end: evaluate3(&curve, piece.end).map_err(|_| BooleanError::Evaluation)?,
749                along_a,
750                along_b,
751                other_a: bounding.clone(),
752                other_b: bounding.clone(),
753            });
754        }
755    }
756    Ok(())
757}
758
759/// Where a point on a face's support lies relative to the face.
760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761enum Place {
762    Inside,
763    Boundary,
764    Outside,
765}
766
767/// Where `curve` crosses `edge` within tolerance, as a parameter on
768/// `curve`: two lines at their closest approach, or a line and a circle or
769/// ellipse where the line pierces the conic's plane. `None` when they stay
770/// further apart than tolerance, run parallel (a line in the conic's
771/// plane, parallel lines), or are another pair of families.
772///
773/// Both pairs are well conditioned where they cross transversally in
774/// space, which is the case a tangent contact with the edge's adjacent
775/// surface leaves (#228).
776fn near_crossing(
777    curve: &Curve3,
778    edge: &Curve3,
779    tolerance: Tolerance,
780) -> Result<Option<Scalar>, BooleanError> {
781    let conic_plane = |c: &Curve3| match c {
782        Curve3::Circle(c) => Some(c.frame),
783        Curve3::Ellipse(e) => Some(e.frame),
784        _ => None,
785    };
786    let point = match (curve, edge) {
787        (Curve3::Line(a), Curve3::Line(b)) => {
788            let (d1, d2) = (a.direction, b.direction);
789            let r = a.origin - b.origin;
790            let (aa, bb, ab) = (d1.dot(d1), d2.dot(d2), d1.dot(d2));
791            let denominator = aa * bb - ab * ab;
792            if denominator <= 1e-12 * aa * bb {
793                return Ok(None);
794            }
795            let (c, f) = (d1.dot(r), d2.dot(r));
796            let s = (ab * f - c * bb) / denominator;
797            let t = (aa * f - ab * c) / denominator;
798            let (p, q) = (a.origin + d1 * s, b.origin + d2 * t);
799            if !report::near(
800                ToleranceDecisionKind::TangentCrossing,
801                (p - q).length(),
802                tolerance,
803            ) {
804                return Ok(None);
805            }
806            return Ok(Some(s));
807        }
808        (Curve3::Line(line), conic) | (conic, Curve3::Line(line)) => {
809            let Some(frame) = conic_plane(conic) else {
810                return Ok(None);
811            };
812            let n = frame.z.normalize();
813            let along = line.direction.dot(n);
814            if along.abs() <= 1e-9 * line.direction.length() {
815                return Ok(None);
816            }
817            let s = (frame.origin - line.origin).dot(n) / along;
818            line.origin + line.direction * s
819        }
820        _ => return Ok(None),
821    };
822    // The pierce point must lie on the conic, within tolerance.
823    let conic = if matches!(curve, Curve3::Line(_)) {
824        edge
825    } else {
826        curve
827    };
828    let wide = report::floored(tolerance);
829    let Ok(t) = locate3(conic, point, wide) else {
830        return Ok(None);
831    };
832    let on = evaluate3(conic, t).map_err(|_| BooleanError::Evaluation)?;
833    if !report::near(
834        ToleranceDecisionKind::TangentCrossing,
835        (on - point).length(),
836        tolerance,
837    ) {
838        return Ok(None);
839    }
840    match locate3(curve, point, wide) {
841        Ok(t) => Ok(Some(t)),
842        Err(_) => Ok(None),
843    }
844}
845
846/// Cuts on a line, conic or traced section that lie within tolerance of
847/// each other in space, merged into one at their mean parameter.
848///
849/// A curve crossing a face's edge where it touches the edge's other
850/// surface -- a wall's face meeting an arched opening's jamb where the jamb
851/// runs tangent into the arch -- has a double root there, and the exact
852/// intersection returns it as two roots about `sqrt(eps)` apart. Kept
853/// apart, they leave a sliver piece `1e-8` long whose ends no other piece
854/// meets, and the face does not split (#228). A traced section does the same
855/// where it touches a face's edge: a plane at a ring's radius touches the
856/// latitude circle where a revolved circle's quarter faces meet, and the
857/// trace's double root splits into roots `2e-8` apart (#381; the trace's
858/// spurious roots are TODO(#397)). A traced loop is not merged across its
859/// start. Other curves are returned unchanged.
860///
861/// Each merged cut moves by at most the caller's linear tolerance. A chain
862/// of cuts each within tolerance of the next but spanning more than it
863/// would move one of them further, so it is refused
864/// ([`BooleanError::NearCoincidence`]).
865fn merge_close(
866    curve: &Curve3,
867    cuts: Vec<Scalar>,
868    tolerance: Tolerance,
869) -> Result<Vec<Scalar>, BooleanError> {
870    let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
871    if !(periodic || matches!(curve, Curve3::Line(_) | Curve3::ImplicitSection(_))) {
872        return Ok(cuts);
873    }
874    // Cuts within rounding of each other are one cut and need no tolerance;
875    // further apart, merging them is a reading within tolerance (#236).
876    let eps = tolerance.linear().max(report::rounding());
877    let merged = |gap: Scalar| report::near(ToleranceDecisionKind::MergedPoints, gap, tolerance);
878    let at = |t: Scalar| evaluate3(curve, t).map_err(|_| BooleanError::Evaluation);
879    let mut placed: Vec<(Scalar, Point3)> = Vec::with_capacity(cuts.len());
880    for cut in cuts {
881        if !cut.is_finite() {
882            continue;
883        }
884        placed.push((cut, at(cut)?));
885    }
886    placed.sort_by(|a, b| a.0.total_cmp(&b.0));
887    // (parameter sum, count, first parameter, first point, last point) per
888    // cluster.
889    let mut clusters: Vec<(Scalar, Scalar, Scalar, Point3, Point3)> = Vec::new();
890    for (cut, point) in placed {
891        match clusters.last_mut() {
892            Some((sum, count, start, first, last)) if (point - *last).length() <= eps => {
893                if !merged((point - *first).length()) {
894                    return Err(BooleanError::NearCoincidence);
895                }
896                // On a closed conic one point has parameters a whole turn
897                // apart (a seam cut at `0` and at `2 pi`): averaged as they
898                // are, they would name the opposite side (#243).
899                let cut = if periodic {
900                    cut + TAU * ((*start - cut) / TAU).round()
901                } else {
902                    cut
903                };
904                *sum += cut;
905                *count += 1.0;
906                *last = point;
907            }
908            _ => clusters.push((cut, 1.0, cut, point, point)),
909        }
910    }
911    let mut out: Vec<Scalar> = clusters
912        .iter()
913        .map(|(sum, count, _, _, _)| sum / count)
914        .collect();
915    // A closed conic's cuts either side of its parameter origin are one
916    // point.
917    if periodic && clusters.len() >= 2 {
918        let (first, last) = (at(out[0])?, at(out[out.len() - 1])?);
919        if merged((first - last).length()) {
920            out.pop();
921        }
922    }
923    Ok(out)
924}
925
926/// The cuts that split an imprinted edge (`curve` over `span`), as a
927/// section's cuts are settled ([`merge_close`]), and with every cut within
928/// the tolerance of one of the edge's own ends read as that end (#276).
929///
930/// The edge's ends are vertices of its own operand, where the faces that
931/// bound it meet; a cut there is the other operand's boundary crossing the
932/// edge where that operand's face is read as coincident with, or as
933/// running along, the face the edge ends on. Kept apart, the cut would
934/// leave a piece a fraction of the tolerance long that lies on the other
935/// face's boundary, which no point can classify (a door's jamb on a wall's
936/// floor face, with the door a rounding error longer than the wall is
937/// thick). Merging it moves the other operand's boundary onto the end by
938/// at most the tolerance, the reading its coincident faces already stand
939/// for; recorded as [`ToleranceDecisionKind::MergedPoints`] above the
940/// rounding floor, and at [`Tolerance::ZERO`] only a rounding residue
941/// merges.
942///
943/// The wider merge can itself leave faces that do not sew (a door a
944/// micrometre to half a millimetre past a wall's face, under a millimetre
945/// tolerance); the boolean is then cut again with ends merged only within
946/// the rounding floor, as before #276 (#291, `report::imprint_end_tolerance`,
947/// [`crate::boolean_with_report`]).
948///
949/// A line edge that ends short of the other face's boundary, by a cut
950/// within tolerance outside its span, is where the door past the face has
951/// its edge cross the boundary: in the last retry (#409,
952/// `report::extends_ends`) the span reaches that cut, so the piece ends on
953/// the boundary and welds there, not the splitter's fixed slack away.
954fn imprint_cuts(
955    curve: &Curve3,
956    span: Interval,
957    cuts: Vec<Scalar>,
958    tolerance: Tolerance,
959) -> Result<(Vec<Scalar>, Interval), BooleanError> {
960    let cuts = merge_close(curve, cuts, tolerance)?;
961    let at = |t: Scalar| evaluate3(curve, t).map_err(|_| BooleanError::Evaluation);
962    let ends = [at(span.start)?, at(span.end)?];
963    let (mut lo, mut hi) = (span.start.min(span.end), span.start.max(span.end));
964    let mut out = Vec::with_capacity(cuts.len());
965    for cut in cuts {
966        let point = at(cut)?;
967        let gap = ends
968            .iter()
969            .map(|end| (point - *end).length())
970            .fold(Scalar::INFINITY, Scalar::min);
971        let ends = report::imprint_end_tolerance(tolerance);
972        if !report::near(ToleranceDecisionKind::MergedPoints, gap, ends) {
973            out.push(cut);
974        } else if gap > report::rounding() {
975            report::merged_an_end();
976            // A line's end short of the other face's boundary: the edge
977            // reaches the crossing, so its piece ends on the boundary and
978            // not a fraction of the tolerance before it (#409).
979            if matches!(curve, Curve3::Line(_)) && report::extends_ends() {
980                lo = lo.min(cut);
981                hi = hi.max(cut);
982            }
983        }
984    }
985    let span = if span.start <= span.end {
986        Interval::new(lo, hi)
987    } else {
988        Interval::new(hi, lo)
989    };
990    Ok((out, span))
991}
992
993/// The pieces of a bounded edge span between the cuts inside it.
994fn pieces_within(curve: &Curve3, span: Interval, cuts: Vec<Scalar>) -> Vec<Interval> {
995    let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
996    let slack = 1e-12 * (1.0 + lo.abs().max(hi.abs()));
997    let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
998    let mut inside = vec![lo, hi];
999    for cut in cuts {
1000        let candidates: &[Scalar] = if periodic {
1001            &[cut - TAU, cut, cut + TAU, cut + 2.0 * TAU]
1002        } else {
1003            &[cut]
1004        };
1005        for &c in candidates {
1006            if c > lo + slack && c < hi - slack {
1007                inside.push(c);
1008            }
1009        }
1010    }
1011    inside.sort_by(Scalar::total_cmp);
1012    inside.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
1013    inside
1014        .windows(2)
1015        .map(|pair| Interval::new(pair[0], pair[1]))
1016        .collect()
1017}
1018
1019/// Whether two parameters are one cut: [`pieces`]' deduplication slack, relative
1020/// to the parameter. In world-charted parameters it is not small: at
1021/// |t| = 5.6e6 it is 5.6e-6, wider than the 1.2e-6 by which a window's
1022/// crossing can pass a face (#445).
1023fn same_cut(x: Scalar, y: Scalar) -> bool {
1024    (x - y).abs() <= 1e-12 * (1.0 + x.abs().max(y.abs()))
1025}
1026
1027/// The crossings of a window that may end a piece: those that are not
1028/// [`same_cut`] with a feature cut. A window end within the slack of a
1029/// feature cut would otherwise sort first and replace it in [`pieces`]'
1030/// deduplication, moving the section's end off the feature's exact vertex
1031/// (#445). The cut ends the piece.
1032fn window_ends(cuts: &[Scalar], crossings: Vec<Scalar>) -> Vec<Scalar> {
1033    crossings
1034        .into_iter()
1035        .filter(|&c| !cuts.iter().any(|&f| same_cut(c, f)))
1036        .collect()
1037}
1038
1039/// The stretches between consecutive cuts, each with the curve it lies on:
1040/// finite stretches of a line (its unbounded ends leave every bounded
1041/// face), the cyclic arcs of a conic, and for a traced section the
1042/// stretches of its span. A traced loop's stretch across the loop's start
1043/// becomes a curve of its own.
1044fn pieces(curve: &Curve3, mut cuts: Vec<Scalar>) -> Result<Vec<(Curve3, Interval)>, BooleanError> {
1045    cuts.sort_by(Scalar::total_cmp);
1046    cuts.dedup_by(|x, y| same_cut(*x, *y));
1047    let plain = |spans: Vec<Interval>| spans.into_iter().map(|s| (curve.clone(), s)).collect();
1048    Ok(match curve {
1049        Curve3::Line(_) => plain(
1050            cuts.windows(2)
1051                .map(|pair| Interval::new(pair[0], pair[1]))
1052                .collect(),
1053        ),
1054        Curve3::ImplicitSection(section) => {
1055            let n = section.curve.end();
1056            let (pu, pv) = section.carrier.periodic();
1057            let closure = section.curve.closure(pu, pv);
1058            let slack = 1e-9 * (1.0 + n);
1059            // A cut at a loop's own start (a chain closing through a vertex
1060            // where branches cross, #249) is a cut like any other: the
1061            // loop's stretches then run between its start and the cuts.
1062            let at_start = cuts.iter().any(|&c| c <= slack || c >= n - slack);
1063            let mut inner: Vec<Scalar> = cuts
1064                .into_iter()
1065                .filter(|&c| c > slack && c < n - slack)
1066                .collect();
1067            inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
1068            let closure = closure.filter(|_| !at_start);
1069            let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
1070                // One cut on a loop: the whole loop, starting there.
1071                let sub = if (a - b).abs() <= slack {
1072                    closure.and_then(|c| section.curve.rotated(a, c))
1073                } else {
1074                    section.curve.sub(a, b, closure)
1075                };
1076                let sub = sub.ok_or(BooleanError::Evaluation)?;
1077                let end = sub.end();
1078                Ok((
1079                    Curve3::ImplicitSection(axiolid_curve::ImplicitSection3 {
1080                        carrier: section.carrier.clone(),
1081                        curve: sub,
1082                    }),
1083                    Interval::new(0.0, end),
1084                ))
1085            };
1086            if closure.is_some() {
1087                if inner.is_empty() {
1088                    return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
1089                }
1090                let mut out = Vec::new();
1091                for pair in inner.windows(2) {
1092                    out.push(own(pair[0], pair[1])?);
1093                }
1094                // Across the loop's own start.
1095                out.push(own(inner[inner.len() - 1], inner[0])?);
1096                out
1097            } else {
1098                let mut ends = vec![0.0];
1099                ends.extend(inner);
1100                ends.push(n);
1101                let mut out = Vec::new();
1102                for pair in ends.windows(2) {
1103                    out.push(own(pair[0], pair[1])?);
1104                }
1105                out
1106            }
1107        }
1108        Curve3::PairSection(section) => {
1109            let n = section.end();
1110            let slack = 1e-9 * (1.0 + n);
1111            let mut inner: Vec<Scalar> = cuts
1112                .into_iter()
1113                .filter(|&c| c > slack && c < n - slack)
1114                .collect();
1115            inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
1116            let (first, last) = (
1117                section.nodes[0].point,
1118                section.nodes[section.nodes.len() - 1].point,
1119            );
1120            let closed = first == last;
1121            let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
1122                let sub = if a < b {
1123                    section.sub(a, b)
1124                } else {
1125                    // Across a loop's own start.
1126                    section
1127                        .sub(a, n)
1128                        .zip(section.sub(0.0, b))
1129                        .map(|(mut x, y)| {
1130                            x.nodes.extend(y.nodes.into_iter().skip(1));
1131                            x
1132                        })
1133                };
1134                let sub = sub.ok_or(BooleanError::Evaluation)?;
1135                let end = sub.end();
1136                Ok((Curve3::PairSection(sub), Interval::new(0.0, end)))
1137            };
1138            let mut out = Vec::new();
1139            if closed {
1140                if inner.is_empty() {
1141                    return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
1142                }
1143                for pair in inner.windows(2) {
1144                    out.push(own(pair[0], pair[1])?);
1145                }
1146                out.push(own(inner[inner.len() - 1], inner[0])?);
1147            } else {
1148                let mut ends = vec![0.0];
1149                ends.extend(inner);
1150                ends.push(n);
1151                for pair in ends.windows(2) {
1152                    out.push(own(pair[0], pair[1])?);
1153                }
1154            }
1155            out
1156        }
1157        _ => {
1158            if cuts.is_empty() {
1159                return Ok(vec![(curve.clone(), Interval::new(0.0, TAU))]);
1160            }
1161            let mut out: Vec<Interval> = cuts
1162                .windows(2)
1163                .map(|pair| Interval::new(pair[0], pair[1]))
1164                .collect();
1165            out.push(Interval::new(cuts[cuts.len() - 1], cuts[0] + TAU));
1166            plain(out)
1167        }
1168    })
1169}
1170
1171/// One operand, with each face's certified domain built once.
1172struct Side<'a> {
1173    brep: &'a ExactBRep,
1174    faces: Vec<FaceId>,
1175    domains: Vec<FaceDomain<'a>>,
1176    /// A sound box around each face, enlarged by the tolerance.
1177    boxes: Vec<Option<Aabb>>,
1178    /// A sound box around each edge, enlarged by the tolerance.
1179    edge_boxes: Vec<Option<Aabb>>,
1180    /// For each edge, the faces whose loops use it.
1181    edge_faces: Vec<Vec<usize>>,
1182}
1183
1184impl<'a> Side<'a> {
1185    fn new(brep: &'a ExactBRep, tolerance: Tolerance) -> Result<Self, BooleanError> {
1186        let topology = brep.topology();
1187        let mut faces = Vec::with_capacity(topology.faces().len());
1188        let mut domains = Vec::with_capacity(topology.faces().len());
1189        for index in 0..topology.faces().len() {
1190            let face = topology
1191                .face_id_at(index)
1192                .ok_or(BooleanError::DanglingReference)?;
1193            let domain = FaceDomain::new(brep, face, tolerance)
1194                .map_err(BooleanError::Measure)?
1195                .ok_or(BooleanError::UnsupportedTrim)?;
1196            faces.push(face);
1197            domains.push(domain);
1198        }
1199        let mut edge_faces = vec![Vec::new(); topology.edges().len()];
1200        for (index, face) in topology.faces().iter().enumerate() {
1201            for bound in &face.bounds {
1202                let wire = topology
1203                    .loops()
1204                    .get(bound.loop_id.index())
1205                    .ok_or(BooleanError::DanglingReference)?;
1206                for use_ in &wire.edges {
1207                    edge_faces[use_.edge.index()].push(index);
1208                }
1209            }
1210        }
1211        let mut boxes = Vec::with_capacity(domains.len());
1212        for (index, domain) in domains.iter().enumerate() {
1213            let surface = topology.faces()[index]
1214                .surface
1215                .and_then(|id| brep.surfaces().get(id.index()))
1216                .ok_or(BooleanError::DanglingReference)?;
1217            let (lo, hi) = domain.bounds();
1218            boxes.push(face_box(surface, lo, hi, tolerance));
1219        }
1220        let mut edge_boxes = Vec::with_capacity(topology.edges().len());
1221        for (index, edge) in topology.edges().iter().enumerate() {
1222            let curve = edge.curve.and_then(|id| brep.curves3().get(id.index()));
1223            let span = topology
1224                .edge_id_at(index)
1225                .and_then(|id| brep.edge_interval(id));
1226            edge_boxes.push(match (curve, span) {
1227                (Some(curve), Some(span)) => edge_box(curve, span, tolerance),
1228                _ => None,
1229            });
1230        }
1231        Ok(Self {
1232            brep,
1233            faces,
1234            domains,
1235            boxes,
1236            edge_boxes,
1237            edge_faces,
1238        })
1239    }
1240
1241    fn surface(&self, face: usize) -> Result<&'a Surface, BooleanError> {
1242        self.brep.topology().faces()[face]
1243            .surface
1244            .and_then(|id| self.brep.surfaces().get(id.index()))
1245            .ok_or(BooleanError::DanglingReference)
1246    }
1247
1248    /// Where `point`, on the face's support, lies: on one of the edges the
1249    /// curve through it runs along (`along`), or inside or outside the face.
1250    fn locate(
1251        &self,
1252        face: usize,
1253        point: Point3,
1254        along: &[(Curve3, Interval)],
1255        tolerance: Tolerance,
1256    ) -> Result<Place, BooleanError> {
1257        for (curve, span) in along {
1258            if on_edge(curve, *span, point, tolerance)? {
1259                return Ok(Place::Boundary);
1260            }
1261        }
1262        let (u, v) = locate(self.surface(face)?, point, report::floored(tolerance))
1263            .map_err(|_| BooleanError::Evaluation)?;
1264        match self.domains[face]
1265            .contains(Point2::new(u, v))
1266            .map_err(BooleanError::Measure)?
1267        {
1268            Some(true) => Ok(Place::Inside),
1269            Some(false) => Ok(Place::Outside),
1270            None => Err(BooleanError::Undecided),
1271        }
1272    }
1273
1274    /// The face's boundary edges, each once, with their curves and spans.
1275    fn edges_of(&self, face: usize) -> Result<Vec<(usize, Curve3, Interval)>, BooleanError> {
1276        let topology = self.brep.topology();
1277        let mut out = Vec::new();
1278        let mut seen = Vec::new();
1279        for bound in &topology.faces()[face].bounds {
1280            let wire = topology
1281                .loops()
1282                .get(bound.loop_id.index())
1283                .ok_or(BooleanError::DanglingReference)?;
1284            for use_ in &wire.edges {
1285                if seen.contains(&use_.edge) {
1286                    continue;
1287                }
1288                seen.push(use_.edge);
1289                let curve = topology.edges()[use_.edge.index()]
1290                    .curve
1291                    .and_then(|id| self.brep.curves3().get(id.index()))
1292                    .ok_or(BooleanError::DanglingReference)?;
1293                let span = self
1294                    .brep
1295                    .edge_interval(use_.edge)
1296                    .ok_or(BooleanError::DanglingReference)?;
1297                out.push((use_.edge.index(), curve.clone(), span));
1298            }
1299        }
1300        Ok(out)
1301    }
1302
1303    /// Parameters on `curve` where it crosses a boundary edge of the face,
1304    /// and the edges it runs along (lying in the adjacent face's surface
1305    /// too), which cut it at their ends.
1306    #[allow(clippy::type_complexity)]
1307    ///
1308    /// `meets` is the other surface `curve` lies on. Where the curve cannot
1309    /// be intersected with the adjacent face's surface (a B-spline), the
1310    /// edge is intersected with `meets` instead: the curve crosses the edge
1311    /// exactly where the edge crosses `meets`.
1312    ///
1313    /// With a `window` (a box around everything the section can matter in),
1314    /// an edge whose own box misses it is skipped: any crossing with it lies
1315    /// outside the window, where the caller has cut the curve at the
1316    /// window's boundary and drops the pieces (#228).
1317    ///
1318    /// Where the edge's adjacent surface and one of the two the curve lies
1319    /// on are a plane and a cylinder read as touching (`contacts`), the
1320    /// curve is cut where it meets their contact ruling, as for every other
1321    /// face pair of the two (`crate::contact`, #243).
1322    fn cuts(
1323        &self,
1324        face: usize,
1325        curve: &Curve3,
1326        meets: &Surface,
1327        window: Option<&Aabb>,
1328        contacts: &Contacts,
1329        tolerance: Tolerance,
1330    ) -> Result<(Vec<Scalar>, Vec<(Curve3, Interval)>), BooleanError> {
1331        let topology = self.brep.topology();
1332        let mut out = Vec::new();
1333        let mut along = Vec::new();
1334        let mut seen = Vec::new();
1335        // A pole or apex the curve passes through cuts it: its pcurve jumps
1336        // round the angle there.
1337        for pole in poles(self.surface(face)?) {
1338            if let Ok(t) = locate3(curve, pole, report::floored(tolerance)) {
1339                let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1340                if report::near(
1341                    ToleranceDecisionKind::IncidentPoint,
1342                    (on - pole).length(),
1343                    tolerance,
1344                ) {
1345                    out.push(t);
1346                }
1347            }
1348        }
1349        for bound in &topology.faces()[face].bounds {
1350            let wire = topology
1351                .loops()
1352                .get(bound.loop_id.index())
1353                .ok_or(BooleanError::DanglingReference)?;
1354            for use_ in &wire.edges {
1355                if seen.contains(&use_.edge) {
1356                    continue;
1357                }
1358                seen.push(use_.edge);
1359                if let (Some(w), Some(b)) = (window, &self.edge_boxes[use_.edge.index()]) {
1360                    if !w.overlaps(b) {
1361                        continue;
1362                    }
1363                }
1364                let edge = &topology.edges()[use_.edge.index()];
1365                let edge_curve = edge
1366                    .curve
1367                    .and_then(|id| self.brep.curves3().get(id.index()))
1368                    .ok_or(BooleanError::DanglingReference)?;
1369                let span = self
1370                    .brep
1371                    .edge_interval(use_.edge)
1372                    .ok_or(BooleanError::DanglingReference)?;
1373                let mut cutter =
1374                    self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, false)?;
1375                let mut result = exact_curve_surface_intersection(curve, &cutter);
1376                if matches!(result, Ok(ExactCurveIntersection::Contained)) {
1377                    // The adjacent face may lie on the same surface under a
1378                    // different frame (a column's half-walls): cut across
1379                    // the seam instead.
1380                    if let Ok(seam) =
1381                        self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, true)
1382                    {
1383                        cutter = seam;
1384                        result = exact_curve_surface_intersection(curve, &cutter);
1385                    }
1386                }
1387                // Two operands placed independently put one plane at two
1388                // roundings: the curve then misses the adjacent face's
1389                // support by 1e-17 and the exact test calls it apart or
1390                // crossing, though it runs along the edge. Within tolerance
1391                // along the whole edge, it is contained (#228).
1392                if !matches!(result, Ok(ExactCurveIntersection::Contained))
1393                    && runs_along(curve, edge_curve, span, tolerance)?
1394                {
1395                    result = Ok(ExactCurveIntersection::Contained);
1396                }
1397                if !matches!(result, Ok(ExactCurveIntersection::Contained)) {
1398                    let own = self.surface(face)?;
1399                    // Along a smooth edge (a fillet running tangent into
1400                    // the adjacent face) the curve touches the cutter: it
1401                    // is cut where the edge crosses `meets` (#249).
1402                    if let Some(hits) = crate::tangency::smooth_edge_cuts(
1403                        own, &cutter, meets, edge_curve, span, curve, contacts, tolerance,
1404                    )? {
1405                        out.extend(hits);
1406                        continue;
1407                    }
1408                    if let Some(hits) =
1409                        contacts.crossing([own, meets], &cutter, curve, tolerance)?
1410                    {
1411                        for t in hits {
1412                            let point =
1413                                evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1414                            if on_edge(edge_curve, span, point, tolerance)? {
1415                                out.push(t);
1416                            }
1417                        }
1418                        continue;
1419                    }
1420                }
1421                match result {
1422                    Ok(ExactCurveIntersection::Points(hits)) => {
1423                        let before = out.len();
1424                        for hit in hits {
1425                            if on_edge(edge_curve, span, hit.point, tolerance)? {
1426                                out.push(hit.parameter.approx());
1427                            }
1428                        }
1429                        // Where the curve touches the adjacent surface at
1430                        // the edge (a tangent crossing), rounding can leave
1431                        // the double root with no real root at all: the
1432                        // curve still meets the edge itself, transversally,
1433                        // and that crossing is read from the two curves.
1434                        if out.len() == before {
1435                            if let Some(t) = near_crossing(curve, edge_curve, tolerance)? {
1436                                let point =
1437                                    evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1438                                if on_edge(edge_curve, span, point, tolerance)? {
1439                                    out.push(t);
1440                                }
1441                            }
1442                        }
1443                    }
1444                    // The curve lies in the adjacent face's surface as well
1445                    // as this one's: where it meets the edge it runs along
1446                    // it, so it is cut at the edge's ends.
1447                    Ok(ExactCurveIntersection::Contained) => {
1448                        for t in [span.start, span.end] {
1449                            let end =
1450                                evaluate3(edge_curve, t).map_err(|_| BooleanError::Evaluation)?;
1451                            if let Ok(s) = locate3(curve, end, report::floored(tolerance)) {
1452                                let on =
1453                                    evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
1454                                if report::near(
1455                                    ToleranceDecisionKind::IncidentPoint,
1456                                    (on - end).length(),
1457                                    tolerance,
1458                                ) {
1459                                    out.push(s);
1460                                }
1461                            }
1462                        }
1463                        along.push((edge_curve.clone(), span));
1464                    }
1465                    Ok(_) => return Err(BooleanError::UnsupportedSection),
1466                    Err(_) => {
1467                        let hits = match exact_curve_surface_intersection(edge_curve, meets) {
1468                            Ok(ExactCurveIntersection::Points(hits)) => hits,
1469                            _ => return Err(BooleanError::UnsupportedTrim),
1470                        };
1471                        for hit in hits {
1472                            if !on_span(edge_curve, span, hit.point, tolerance)? {
1473                                continue;
1474                            }
1475                            if let Ok(s) = locate3(curve, hit.point, report::floored(tolerance)) {
1476                                let on =
1477                                    evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
1478                                if report::near(
1479                                    ToleranceDecisionKind::IncidentPoint,
1480                                    (on - hit.point).length(),
1481                                    tolerance,
1482                                ) {
1483                                    out.push(s);
1484                                }
1485                            }
1486                        }
1487                    }
1488                }
1489            }
1490        }
1491        Ok((out, along))
1492    }
1493}
1494
1495impl Side<'_> {
1496    /// A surface the section crosses exactly where it crosses the edge: the
1497    /// adjacent face's support, or across a seam (the same support on both
1498    /// sides) the plane through the ruling and the surface normal.
1499    fn cutter(
1500        &self,
1501        face: usize,
1502        edge: usize,
1503        edge_curve: &Curve3,
1504        span: Interval,
1505        tolerance: Tolerance,
1506        across_seam: bool,
1507    ) -> Result<Surface, BooleanError> {
1508        let own = self.surface(face)?;
1509        let adjacent = self.edge_faces[edge]
1510            .iter()
1511            .copied()
1512            .find(|&other| other != face);
1513        if let (Some(other), false) = (adjacent, across_seam) {
1514            let theirs = self.surface(other)?;
1515            if !same_support(own, theirs, tolerance) {
1516                return Ok(theirs.clone());
1517            }
1518        }
1519        // A seam: the same support on both sides (or a free edge). A seam
1520        // circle (a sphere's meridian or latitude, a torus's tube or ring
1521        // circle) is cut by the surface its normals sweep: coaxial with the
1522        // circle, a plane, cylinder or cone, which crosses the face
1523        // transversally along the whole circle.
1524        if let Curve3::Circle(circle) = edge_curve {
1525            return normal_sweep(own, circle, tolerance);
1526        }
1527        let Curve3::Line(line) = edge_curve else {
1528            return Err(BooleanError::UnsupportedTrim);
1529        };
1530        let mid = evaluate3(edge_curve, 0.5 * (span.start + span.end))
1531            .map_err(|_| BooleanError::Evaluation)?;
1532        let (u, v) =
1533            locate(own, mid, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1534        let n = normal(own, u, v).map_err(|_| BooleanError::Evaluation)?;
1535        let across = line.direction.cross(n);
1536        let length = across.length();
1537        if length == 0.0 || !length.is_finite() {
1538            return Err(BooleanError::UnsupportedTrim);
1539        }
1540        let z = across / length;
1541        let x = line.direction.normalize();
1542        Ok(Surface::Plane(Plane {
1543            frame: axiolid_core::Frame3 {
1544                origin: mid,
1545                x,
1546                y: z.cross(x),
1547                z,
1548            },
1549        }))
1550    }
1551}
1552
1553/// The surface swept by `surface`'s normal lines along a circle on it,
1554/// when the surface is one of revolution about the circle's axis (or the
1555/// circle is a meridian, whose normals stay in its plane): a plane, a
1556/// cylinder or a cone coaxial with the circle.
1557fn normal_sweep(
1558    surface: &Surface,
1559    circle: &axiolid_curve::Circle3,
1560    tolerance: Tolerance,
1561) -> Result<Surface, BooleanError> {
1562    let f = circle.frame;
1563    let axis = f.x.cross(f.y).normalize();
1564    let x = f.x.normalize();
1565    let y = axis.cross(x);
1566    let p = f.origin + x * circle.radius;
1567    let (u, v) =
1568        locate(surface, p, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1569    let n = normal(surface, u, v)
1570        .map_err(|_| BooleanError::Evaluation)?
1571        .normalize();
1572    let frame = axiolid_core::Frame3 {
1573        origin: f.origin,
1574        x,
1575        y,
1576        z: axis,
1577    };
1578    let along = n.dot(axis);
1579    let radial = n.dot(x);
1580    let plane = || {
1581        Surface::Plane(Plane {
1582            frame: axiolid_core::Frame3 {
1583                origin: f.origin,
1584                x,
1585                y,
1586                z: axis,
1587            },
1588        })
1589    };
1590    // Normals within the circle's plane: the plane itself.
1591    if along.abs() <= 1e-12 {
1592        return Ok(plane());
1593    }
1594    // Normals along the axis: the cylinder through the circle.
1595    if radial.abs() <= 1e-12 {
1596        return Ok(Surface::Cylinder(axiolid_surface::Cylinder {
1597            frame,
1598            radius: circle.radius,
1599        }));
1600    }
1601    // Otherwise the cone of normal lines: at height `h` along the axis the
1602    // line is `radius + h * radial / along` from it.
1603    Ok(Surface::Cone(axiolid_surface::Cone {
1604        frame,
1605        radius: circle.radius,
1606        semi_angle: (radial / along).atan(),
1607    }))
1608}
1609
1610/// Whether every point of the edge (`edge` over `span`) lies within the
1611/// caller's linear tolerance of `curve`, proved in closed form:
1612///
1613/// - two lines: the distance to a line is convex along a segment, so the
1614///   segment's two ends bound it;
1615/// - two circles: a point of one is within `|c1 - c2| + |r1 - r2| +
1616///   r2 * theta` of the other, `theta` the angle between their planes
1617///   (bounded by `2 |n1 x n2|`).
1618///
1619/// Any other pair is never read as running along (#228).
1620fn runs_along(
1621    curve: &Curve3,
1622    edge: &Curve3,
1623    span: Interval,
1624    tolerance: Tolerance,
1625) -> Result<bool, BooleanError> {
1626    let along =
1627        |residue: Scalar| report::near(ToleranceDecisionKind::SectionAlongEdge, residue, tolerance);
1628    match (curve, edge) {
1629        (Curve3::Line(line), Curve3::Line(_)) => {
1630            let d = line.direction.normalize_or_zero();
1631            if d == Vec3::ZERO {
1632                return Ok(false);
1633            }
1634            let mut residue: Scalar = 0.0;
1635            for t in [span.start, span.end] {
1636                let p = evaluate3(edge, t).map_err(|_| BooleanError::Evaluation)?;
1637                let off = p - line.origin;
1638                residue = residue.max((off - d * off.dot(d)).length());
1639            }
1640            Ok(along(residue))
1641        }
1642        (Curve3::Circle(a), Curve3::Circle(b)) => {
1643            let (na, nb) = (a.frame.z.normalize_or_zero(), b.frame.z.normalize_or_zero());
1644            if na == Vec3::ZERO || nb == Vec3::ZERO {
1645                return Ok(false);
1646            }
1647            let tilt = 2.0 * na.cross(nb).length();
1648            Ok(along(
1649                (a.frame.origin - b.frame.origin).length()
1650                    + (a.radius - b.radius).abs()
1651                    + a.radius * tilt,
1652            ))
1653        }
1654        _ => Ok(false),
1655    }
1656}
1657
1658/// A traced piece (`curve` over `span`, its `sample` inside) that runs along
1659/// one of `edges` that is a line, circle or ellipse, as that edge's own
1660/// curve over the stretch between the piece's ends; any other piece
1661/// unchanged (#382).
1662///
1663/// A plane through a torus's axis meets the torus in the circles its faces
1664/// start from, found as traced sections; the coincident cap on that plane
1665/// imprints the same circles in closed form. A parametric midpoint decides
1666/// whether two copies of a stretch are one (`crate::split`), and a traced
1667/// curve's parameter is not the circle's angle, so the copies would be
1668/// imprinted twice and the faces would not sew. One stretch has one curve.
1669///
1670/// The ends move onto the edge by their residue, adopted only within the
1671/// tolerance (a decision [`ToleranceDecisionKind::SectionAlongEdge`], read
1672/// as for a section along an edge); a piece cut at its edge's ends, as
1673/// every piece along an edge is, does not move further than the incident
1674/// tolerance.
1675fn along_edge_curve(
1676    curve: Curve3,
1677    span: Interval,
1678    sample: Point3,
1679    edges: &[&(Curve3, Interval)],
1680    tolerance: Tolerance,
1681) -> Result<(Curve3, Interval), BooleanError> {
1682    if !matches!(curve, Curve3::ImplicitSection(_) | Curve3::PairSection(_)) {
1683        return Ok((curve, span));
1684    }
1685    let at = |c: &Curve3, t: Scalar| evaluate3(c, t).map_err(|_| BooleanError::Evaluation);
1686    let (start, end) = (at(&curve, span.start)?, at(&curve, span.end)?);
1687    for (edge, edge_span) in edges.iter().copied() {
1688        let periodic = matches!(edge, Curve3::Circle(_) | Curve3::Ellipse(_));
1689        if !(periodic || matches!(edge, Curve3::Line(_)))
1690            || !on_edge(edge, *edge_span, sample, tolerance)?
1691        {
1692            continue;
1693        }
1694        let locate = |p: Point3| {
1695            locate3(edge, p, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)
1696        };
1697        let (ts, te, tm) = (locate(start)?, locate(end)?, locate(sample)?);
1698        let adopted = if periodic {
1699            // Round the stretch from `ts` through `tm` to `te`, whichever
1700            // way it runs: `te` and `tm` in `(ts, ts + 2 pi]`.
1701            let up = |t: Scalar| t - TAU * ((t - ts) / TAU).ceil() + TAU;
1702            let (te_up, tm_up) = (up(te), up(tm));
1703            if tm_up < te_up {
1704                Interval::new(ts, te_up)
1705            } else {
1706                Interval::new(te_up - TAU, ts)
1707            }
1708        } else {
1709            Interval::new(ts.min(te), ts.max(te))
1710        };
1711        let residue = (at(edge, ts)? - start)
1712            .length()
1713            .max((at(edge, te)? - end).length());
1714        if !report::near(ToleranceDecisionKind::SectionAlongEdge, residue, tolerance) {
1715            return Ok((curve, span));
1716        }
1717        return Ok((edge.clone(), adopted));
1718    }
1719    Ok((curve, span))
1720}
1721
1722/// Whether `point` lies on the edge within tolerance and inside its span.
1723fn on_edge(
1724    curve: &Curve3,
1725    span: Interval,
1726    point: Point3,
1727    tolerance: Tolerance,
1728) -> Result<bool, BooleanError> {
1729    let Ok(t) = locate3(curve, point, report::floored(tolerance)) else {
1730        return Ok(false);
1731    };
1732    let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1733    if !on_span(curve, span, point, tolerance)? {
1734        return Ok(false);
1735    }
1736    Ok(report::near(
1737        ToleranceDecisionKind::IncidentPoint,
1738        (on - point).length(),
1739        tolerance,
1740    ))
1741}
1742
1743/// Whether `point`, on `curve`, lies within the edge's span (a closed
1744/// conic's span may start anywhere and run past a full turn).
1745pub(crate) fn on_span(
1746    curve: &Curve3,
1747    span: Interval,
1748    point: Point3,
1749    tolerance: Tolerance,
1750) -> Result<bool, BooleanError> {
1751    let t =
1752        locate3(curve, point, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1753    let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
1754    let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
1755    let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
1756    let candidates: &[Scalar] = if periodic {
1757        &[t - TAU, t, t + TAU, t + 2.0 * TAU]
1758    } else {
1759        &[t]
1760    };
1761    Ok(candidates
1762        .iter()
1763        .any(|c| *c >= lo - slack && *c <= hi + slack))
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768    use super::*;
1769    use axiolid_core::Frame3;
1770    use axiolid_curve::{Circle3, Line3};
1771
1772    const EPS: Scalar = 1e-6;
1773
1774    #[test]
1775    fn a_window_end_never_replaces_a_cut() {
1776        // #445: the corbel's section line at world y = 5.6e6. The window's
1777        // crossings pass the feature cuts by 1.2e-6, inside the dedup slack.
1778        let line = Curve3::Line(Line3 {
1779            origin: Point3::new(600000.2, 0.0, 203.5),
1780            direction: -Vec3::Y,
1781        });
1782        let cuts = vec![-5600000.1, -5599999.9];
1783        let ends = window_ends(&cuts, vec![-5599999.8999988, -5600000.1000012]);
1784        assert!(ends.is_empty());
1785        let spans: Vec<Interval> = pieces(&line, cuts)
1786            .expect("pieces")
1787            .into_iter()
1788            .map(|p| p.1)
1789            .collect();
1790        assert_eq!(spans, vec![Interval::new(-5600000.1, -5599999.9)]);
1791        // Near the origin the slack is 1e-12 and both ends stay.
1792        let kept = window_ends(
1793            &[-0.1, 0.1],
1794            vec![0.10000119073677866, -0.10000119073677866],
1795        );
1796        assert_eq!(kept.len(), 2);
1797    }
1798
1799    fn tol() -> Tolerance {
1800        Tolerance::METRE
1801    }
1802
1803    fn x_axis() -> Curve3 {
1804        Curve3::Line(Line3 {
1805            origin: Point3::ZERO,
1806            direction: Vec3::X,
1807        })
1808    }
1809
1810    #[test]
1811    fn cuts_within_tolerance_merge_and_a_longer_chain_is_refused() {
1812        let line = x_axis();
1813        let merged = merge_close(&line, vec![1.0, 1.0 + 0.5 * EPS, 2.0], tol()).unwrap();
1814        assert_eq!(merged.len(), 2);
1815        assert!((merged[0] - (1.0 + 0.25 * EPS)).abs() < 1e-15);
1816        // Ten tolerances apart: two cuts, decided exactly.
1817        let kept = merge_close(&line, vec![1.0, 1.0 + 10.0 * EPS], tol()).unwrap();
1818        assert_eq!(kept.len(), 2);
1819        // Each within tolerance of the next, 1.2 tolerances end to end.
1820        let chain = merge_close(&line, vec![1.0, 1.0 + 0.6 * EPS, 1.0 + 1.2 * EPS], tol());
1821        assert_eq!(chain, Err(BooleanError::NearCoincidence));
1822    }
1823
1824    #[test]
1825    fn cuts_at_a_circles_seam_from_either_side_merge_at_the_seam() {
1826        // One point named at `0` and twice at `2 pi` (a seam cut, and the
1827        // ends of edges along the seam): one cut there, not their mean
1828        // `4 pi / 3` on the other side of the circle (#243).
1829        let circle = Curve3::Circle(Circle3 {
1830            frame: Frame3 {
1831                origin: Point3::ZERO,
1832                x: Vec3::X,
1833                y: Vec3::Y,
1834                z: Vec3::Z,
1835            },
1836            radius: 1.0,
1837        });
1838        let merged = merge_close(&circle, vec![0.0, TAU, TAU], tol()).unwrap();
1839        assert_eq!(merged.len(), 1);
1840        let at = evaluate3(&circle, merged[0]).unwrap();
1841        assert!((at - Point3::X).length() < 1e-12, "{at:?}");
1842        // Close either side of the seam: their mean is at the seam.
1843        let merged = merge_close(&circle, vec![TAU - 0.2 * EPS, 0.2 * EPS], tol()).unwrap();
1844        assert_eq!(merged.len(), 1);
1845        let at = evaluate3(&circle, merged[0]).unwrap();
1846        assert!((at - Point3::X).length() < 0.25 * EPS, "{at:?}");
1847    }
1848
1849    #[test]
1850    fn an_edge_runs_along_a_curve_only_when_proved_within_tolerance() {
1851        let line = x_axis();
1852        let edge = |dy: Scalar| {
1853            Curve3::Line(Line3 {
1854                origin: Point3::new(0.0, dy, 0.0),
1855                direction: Vec3::new(1.0, dy, 0.0),
1856            })
1857        };
1858        let span = Interval::new(0.0, 1.0);
1859        // From 0.5 eps to 1.0 eps off the axis: within.
1860        assert!(runs_along(&line, &edge(0.5 * EPS), span, tol()).unwrap());
1861        // Its far end ten tolerances off: not.
1862        assert!(!runs_along(&line, &edge(5.0 * EPS), span, tol()).unwrap());
1863        let circle = |r: Scalar, tilt: Scalar| {
1864            Curve3::Circle(Circle3 {
1865                frame: Frame3 {
1866                    origin: Point3::ZERO,
1867                    x: Vec3::X,
1868                    y: Vec3::new(0.0, tilt.cos(), tilt.sin()),
1869                    z: Vec3::new(0.0, -tilt.sin(), tilt.cos()),
1870                },
1871                radius: r,
1872            })
1873        };
1874        let whole = Interval::new(0.0, core::f64::consts::TAU);
1875        assert!(runs_along(
1876            &circle(1.0, 0.0),
1877            &circle(1.0 + 0.5 * EPS, 0.0),
1878            whole,
1879            tol()
1880        )
1881        .unwrap());
1882        assert!(!runs_along(&circle(1.0, 0.0), &circle(1.0, 10.0 * EPS), whole, tol()).unwrap());
1883        // Other families are never read as running along.
1884        assert!(!runs_along(&line, &circle(1.0, 0.0), whole, tol()).unwrap());
1885    }
1886
1887    #[test]
1888    fn a_plane_reads_parallel_to_an_axis_only_within_tolerance_over_its_extent() {
1889        let cylinder = Surface::Cylinder(axiolid_surface::Cylinder {
1890            frame: Frame3 {
1891                origin: Point3::ZERO,
1892                x: Vec3::X,
1893                y: Vec3::Y,
1894                z: Vec3::Z,
1895            },
1896            radius: 1.0,
1897        });
1898        let plane = |tilt: Scalar, offset: Scalar| {
1899            let z = Vec3::new(1.0, 0.0, tilt).normalize();
1900            Surface::Plane(Plane {
1901                frame: Frame3 {
1902                    origin: Point3::new(offset, 0.0, 0.0),
1903                    x: Vec3::Y,
1904                    y: z.cross(Vec3::Y),
1905                    z,
1906                },
1907            })
1908        };
1909        // Tilted 1e-9 over a 10 m extent: 1e-8 off, read parallel; the
1910        // plane through the axis cuts two rulings.
1911        let rulings =
1912            plane_cylinder_within_rounding(&plane(1e-9, 0.0), &cylinder, Some(10.0), tol());
1913        assert_eq!(
1914            rulings.map(|r| (r.curves.len(), r.contact)),
1915            Some((2, false))
1916        );
1917        // Tilted 1e-6 over 10 m: 1e-5 off, left to the exact closed form.
1918        assert!(
1919            plane_cylinder_within_rounding(&plane(1e-6, 0.0), &cylinder, Some(10.0), tol())
1920                .is_none()
1921        );
1922        // No extent: never read parallel.
1923        assert!(
1924            plane_cylinder_within_rounding(&plane(1e-9, 0.0), &cylinder, None, tol()).is_none()
1925        );
1926        // Within tolerance of touching: one ruling.
1927        let touching = plane_cylinder_within_rounding(
1928            &plane(0.0, 1.0 + 0.5 * EPS),
1929            &cylinder,
1930            Some(10.0),
1931            tol(),
1932        );
1933        assert_eq!(
1934            touching.map(|r| (r.curves.len(), r.contact)),
1935            Some((1, true))
1936        );
1937        // Exactly parallel and ten tolerances clear, or exactly parallel
1938        // through the axis: no reading, the exact closed form decides
1939        // (#236).
1940        for offset in [1.0 + 10.0 * EPS, 0.0] {
1941            assert!(plane_cylinder_within_rounding(
1942                &plane(0.0, offset),
1943                &cylinder,
1944                Some(10.0),
1945                tol()
1946            )
1947            .is_none());
1948        }
1949    }
1950
1951    #[test]
1952    fn only_readings_the_exact_predicates_do_not_confirm_are_reported() {
1953        let cylinder = Surface::Cylinder(axiolid_surface::Cylinder {
1954            frame: Frame3 {
1955                origin: Point3::ZERO,
1956                x: Vec3::X,
1957                y: Vec3::Y,
1958                z: Vec3::Z,
1959            },
1960            radius: 1.0,
1961        });
1962        let plane = |z: Vec3, x: Vec3| {
1963            Surface::Plane(Plane {
1964                frame: Frame3 {
1965                    origin: Point3::new(0.0, 0.0, 0.5),
1966                    x,
1967                    y: z.cross(x),
1968                    z,
1969                },
1970            })
1971        };
1972        // Exactly perpendicular: the closed form's circle, nothing read.
1973        let session = report::open(&[], tol());
1974        assert!(plane_cylinder_within_rounding(
1975            &plane(Vec3::Z, Vec3::X),
1976            &cylinder,
1977            Some(10.0),
1978            tol()
1979        )
1980        .is_none());
1981        assert!(session.finish().expect("exact").is_exact());
1982        // Tilted by a rotation's rounding residue: read perpendicular, and
1983        // reported.
1984        let session = report::open(&[], tol());
1985        let tilted = Vec3::new(0.0, (core::f64::consts::FRAC_PI_2).cos(), 1.0);
1986        let circle =
1987            plane_cylinder_within_rounding(&plane(tilted, Vec3::X), &cylinder, Some(10.0), tol());
1988        assert_eq!(
1989            circle.map(|c| (c.curves.len(), c.contact)),
1990            Some((1, false))
1991        );
1992        let report = session.finish().expect("within tolerance");
1993        assert!(report.contains(ToleranceDecisionKind::PlanePerpendicularToAxis));
1994        assert!(report.linear() > 0.0 && report.linear() <= EPS);
1995    }
1996}