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.
31//! - **Coincident supports** share a patch of surface. Each face's boundary
32//!   edges are imprinted on the other where they run inside it, so the
33//!   shared patch becomes a region of both faces; classification then sees
34//!   it on the other solid's boundary (see `crate::boolean`).
35
36use axiolid_brep::ExactBRep;
37use axiolid_core::{Interval, Point2, Point3, Scalar, Tolerance};
38use axiolid_curve::Curve3;
39use axiolid_evaluate::surface::{locate, normal};
40use axiolid_evaluate::{curve::locate3, evaluate3};
41use axiolid_measure::FaceDomain;
42use axiolid_nurbs::{
43    exact_curve_curve_intersection3, exact_curve_surface_intersection, exact_surface_intersection,
44    implicit_surface_intersection, spline_pair_intersection, ExactCurveIntersection,
45    ExactIntersectionRefusal,
46};
47use axiolid_surface::{Plane, Surface};
48use axiolid_topology::FaceId;
49use core::f64::consts::TAU;
50
51use crate::support::{cleaned, same_support, window};
52use crate::BooleanError;
53
54/// Where along a piece its inside/outside sample is taken: off-centre, so
55/// the midpoint of a symmetric section (a meridian's pole) is never it.
56const SAMPLE: Scalar = 0.414_213_562_373_095;
57
58/// A piece of an intersection curve lying on a face of each operand.
59#[derive(Debug, Clone, PartialEq)]
60pub struct SectionEdge {
61    /// The face of the first operand the edge lies on.
62    pub face_a: FaceId,
63    /// The face of the second operand the edge lies on.
64    pub face_b: FaceId,
65    /// The exact curve.
66    pub curve: Curve3,
67    /// The curve's parameter span; for a circle or ellipse it may run past
68    /// `2 pi`.
69    pub span: Interval,
70    /// The curve's point at `span.start`.
71    pub start: Point3,
72    /// The curve's point at `span.end`.
73    pub end: Point3,
74    /// Whether the edge runs along a boundary edge `face_a` already has, so
75    /// it does not split `face_a`.
76    pub along_a: bool,
77    /// Whether the edge runs along a boundary edge `face_b` already has.
78    pub along_b: bool,
79    /// The surface that meets `face_a`'s surface along this edge: `face_b`'s
80    /// for a crossing, or for an edge imprinted from a coincident face the
81    /// surface that bounds it there. Its equation on `face_a` is the edge's
82    /// pcurve.
83    pub other_a: Surface,
84    /// The surface that meets `face_b`'s surface along this edge.
85    pub other_b: Surface,
86}
87
88/// Every section edge between the faces of `a` and the faces of `b`.
89///
90/// # Errors
91///
92/// A face pair whose section is not a line, circle or ellipse, or a piece
93/// too close to a face boundary to classify.
94pub fn section_edges(
95    a: &ExactBRep,
96    b: &ExactBRep,
97    tolerance: Tolerance,
98) -> Result<Vec<SectionEdge>, BooleanError> {
99    let side_a = Side::new(a, tolerance)?;
100    let side_b = Side::new(b, tolerance)?;
101    let mut out = Vec::new();
102    // Surfaces are shared by many faces: each pair's closed form once.
103    #[allow(clippy::type_complexity)]
104    let mut closed_forms: Vec<(
105        Surface,
106        Surface,
107        Result<axiolid_nurbs::ExactIntersectionCurve, ExactIntersectionRefusal>,
108    )> = Vec::new();
109    for fa in 0..side_a.faces.len() {
110        for fb in 0..side_b.faces.len() {
111            let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
112            if same_support(sa, sb, tolerance) {
113                imprint(&side_a, fa, &side_b, fb, true, tolerance, &mut out)?;
114                imprint(&side_b, fb, &side_a, fa, false, tolerance, &mut out)?;
115                continue;
116            }
117            // Lines and conics in closed form; every other section traced in
118            // one face's parameter box (ADR 0077), which holds every part
119            // of it that can matter.
120            let splines = matches!((sa, sb), (Surface::BSpline(_), Surface::BSpline(_)));
121            // Two B-splines are traced below, in the faces' own boxes.
122            let closed_form = if splines {
123                None
124            } else {
125                let known = closed_forms
126                    .iter()
127                    .find(|(a, b, _)| a == sa && b == sb)
128                    .map(|(_, _, r)| r.clone());
129                let result = known.unwrap_or_else(|| {
130                    let r = exact_surface_intersection(&cleaned(sa), &cleaned(sb));
131                    closed_forms.push((sa.clone(), sb.clone(), r.clone()));
132                    r
133                });
134                match result {
135                    Ok(curve) => {
136                        let conic = curve.branches.iter().zip(&curve.spans).all(|(b, s)| {
137                            matches!(
138                                (b, s),
139                                (Curve3::Line(_), _)
140                                    | (Curve3::Circle(_) | Curve3::Ellipse(_), None)
141                            )
142                        });
143                        conic.then_some(curve)
144                    }
145                    // Apart, or touching without crossing: no section.
146                    Err(
147                        ExactIntersectionRefusal::Disjoint
148                        | ExactIntersectionRefusal::NotRegularCurve,
149                    ) => continue,
150                    Err(_) => None,
151                }
152            };
153            let branches: Vec<(Curve3, Option<Interval>)> = match closed_form {
154                Some(curve) => curve.branches.into_iter().zip(curve.spans).collect(),
155                None => {
156                    // Only a B-spline can carry a section with a B-spline
157                    // (it has no equation to read elsewhere); otherwise the
158                    // compact and low-degree surfaces carry best.
159                    let rank = |s: &Surface| match s {
160                        Surface::BSpline(_) => 4,
161                        Surface::Torus(_) => 3,
162                        Surface::Sphere(_) => 2,
163                        Surface::Plane(_) => 0,
164                        _ => 1,
165                    };
166                    // Two B-splines: the section carried on both, traced in
167                    // both faces' boxes (ADR 0077).
168                    if let (Surface::BSpline(ba), Surface::BSpline(bb)) = (sa, sb) {
169                        let (la, ha) = side_a.domains[fa].bounds();
170                        let (lb, hb) = side_b.domains[fb].bounds();
171                        match spline_pair_intersection(
172                            ba,
173                            bb,
174                            Some((window(sa, la, ha), window(sb, lb, hb))),
175                        ) {
176                            Ok(sections) => sections
177                                .into_iter()
178                                .map(|s| {
179                                    let end = s.end();
180                                    (Curve3::PairSection(s), Some(Interval::new(0.0, end)))
181                                })
182                                .collect(),
183                            Err(ExactIntersectionRefusal::Disjoint) => continue,
184                            Err(_) => return Err(BooleanError::UnsupportedSection),
185                        }
186                    } else {
187                        let (carrier, other, side, face) = if rank(sa) >= rank(sb) {
188                            (sa, sb, &side_a, fa)
189                        } else {
190                            (sb, sa, &side_b, fb)
191                        };
192                        let (lo, hi) = side.domains[face].bounds();
193                        match implicit_surface_intersection(
194                            carrier,
195                            other,
196                            Some(window(carrier, lo, hi)),
197                        ) {
198                            Ok(sections) => sections
199                                .into_iter()
200                                .map(|s| {
201                                    let end = s.curve.end();
202                                    (Curve3::ImplicitSection(s), Some(Interval::new(0.0, end)))
203                                })
204                                .collect(),
205                            // Apart, or touching only at isolated points: no
206                            // section, as for the closed forms.
207                            Err(
208                                ExactIntersectionRefusal::Disjoint
209                                | ExactIntersectionRefusal::NotRegularCurve,
210                            ) => continue,
211                            Err(_) => return Err(BooleanError::UnsupportedSection),
212                        }
213                    }
214                }
215            };
216            for (index, (branch, span)) in branches.iter().enumerate() {
217                // A ray (a cone's ruling from its apex) is a line bounded
218                // at its finite ends.
219                let bounds = match (branch, span) {
220                    (Curve3::Line(_), Some(span)) => Some(*span),
221                    _ => None,
222                };
223                let branch = branch.clone();
224                let branch = &branch;
225                let (mut cuts, along_a) = side_a.cuts(fa, branch, sb, tolerance)?;
226                let (more, along_b) = side_b.cuts(fb, branch, sa, tolerance)?;
227                cuts.extend(more);
228                // Branches of one section meet only where the surfaces touch
229                // (the two ellipses of a Steinmetz pair): such a point splits
230                // both, so the faces' graphs get a vertex there.
231                for (other, _) in branches
232                    .iter()
233                    .skip(index + 1)
234                    .chain(branches.iter().take(index))
235                {
236                    if let Ok(ExactCurveIntersection::Points(hits)) =
237                        exact_curve_curve_intersection3(branch, other)
238                    {
239                        cuts.extend(hits.iter().map(|h| h.parameter.approx()));
240                    }
241                }
242                if let Some(b) = bounds {
243                    let (lo, hi) = (b.start.min(b.end), b.start.max(b.end));
244                    cuts.retain(|&c| c >= lo && c <= hi);
245                    cuts.extend([lo, hi].into_iter().filter(|x| x.is_finite()));
246                }
247                for (piece_curve, span) in pieces(branch, cuts)? {
248                    // Off-centre, so a symmetric section's pole or seam
249                    // crossing never becomes the sample.
250                    let mid =
251                        evaluate3(&piece_curve, span.start + SAMPLE * (span.end - span.start))
252                            .map_err(|_| BooleanError::Evaluation)?;
253                    // Tangent contact adds no section. A traced section is
254                    // never mere touching (the trace drops touching points
255                    // and curves), so where its surfaces are tangent they
256                    // cross: it stays.
257                    let traced = matches!(
258                        piece_curve,
259                        Curve3::ImplicitSection(_) | Curve3::PairSection(_)
260                    );
261                    if !traced && touching(sa, sb, mid, tolerance)? {
262                        continue;
263                    }
264                    let at_a = side_a.locate(fa, mid, &along_a, tolerance)?;
265                    if at_a == Place::Outside {
266                        continue;
267                    }
268                    let at_b = side_b.locate(fb, mid, &along_b, tolerance)?;
269                    if at_b == Place::Outside {
270                        continue;
271                    }
272                    out.push(SectionEdge {
273                        face_a: side_a.faces[fa],
274                        face_b: side_b.faces[fb],
275                        start: evaluate3(&piece_curve, span.start)
276                            .map_err(|_| BooleanError::Evaluation)?,
277                        end: evaluate3(&piece_curve, span.end)
278                            .map_err(|_| BooleanError::Evaluation)?,
279                        curve: piece_curve,
280                        span,
281                        along_a: at_a == Place::Boundary,
282                        along_b: at_b == Place::Boundary,
283                        other_a: sb.clone(),
284                        other_b: sa.clone(),
285                    });
286                }
287            }
288        }
289    }
290    Ok(out)
291}
292
293/// The points where a surface's angle parameter has no value: a sphere's
294/// poles, a cone's apex.
295fn poles(surface: &Surface) -> Vec<Point3> {
296    match surface {
297        Surface::Sphere(s) => {
298            let z = s.frame.z.normalize() * s.radius;
299            vec![s.frame.origin + z, s.frame.origin - z]
300        }
301        Surface::Cone(c) => {
302            let slope = c.semi_angle.tan();
303            if slope == 0.0 {
304                Vec::new()
305            } else {
306                vec![c.frame.origin - c.frame.z.normalize() * (c.radius / slope)]
307            }
308        }
309        _ => Vec::new(),
310    }
311}
312
313/// Whether two surfaces through `point` share their tangent plane there: a
314/// line where a cylinder rests on a plane, which it touches without
315/// crossing. Analytic surfaces tangent along a whole curve lie on one side
316/// of each other, so such a stretch splits neither face.
317fn touching(
318    a: &Surface,
319    b: &Surface,
320    point: Point3,
321    tolerance: Tolerance,
322) -> Result<bool, BooleanError> {
323    let at = |s: &Surface| -> Result<axiolid_core::Vec3, BooleanError> {
324        let (u, v) = locate(s, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
325        Ok(normal(s, u, v)
326            .map_err(|_| BooleanError::Evaluation)?
327            .normalize())
328    };
329    Ok(at(a)?.cross(at(b)?).length() <= 1e-9)
330}
331
332/// The boundary edges of `from`'s face `face` that run inside `onto`'s face
333/// `other`, on the same surface, as section edges (`first` when `from` is
334/// the first operand).
335#[allow(clippy::too_many_arguments)]
336fn imprint(
337    from: &Side<'_>,
338    face: usize,
339    onto: &Side<'_>,
340    other: usize,
341    first: bool,
342    tolerance: Tolerance,
343    out: &mut Vec<SectionEdge>,
344) -> Result<(), BooleanError> {
345    for (edge, curve, span) in from.edges_of(face)? {
346        // The surface that bounds the imprinted edge in its own operand:
347        // together with the shared surface it defines the edge's curve.
348        let bounding = from.cutter(face, edge, &curve, span, tolerance, false)?;
349        let (cuts, along) = onto.cuts(other, &curve, &bounding, tolerance)?;
350        for piece in pieces_within(&curve, span, cuts) {
351            let mid = evaluate3(&curve, piece.start + SAMPLE * (piece.end - piece.start))
352                .map_err(|_| BooleanError::Evaluation)?;
353            let at = onto.locate(other, mid, &along, tolerance)?;
354            if at == Place::Outside {
355                continue;
356            }
357            let (face_a, face_b, along_a, along_b) = if first {
358                (
359                    from.faces[face],
360                    onto.faces[other],
361                    true,
362                    at == Place::Boundary,
363                )
364            } else {
365                (
366                    onto.faces[other],
367                    from.faces[face],
368                    at == Place::Boundary,
369                    true,
370                )
371            };
372            out.push(SectionEdge {
373                face_a,
374                face_b,
375                curve: curve.clone(),
376                span: piece,
377                start: evaluate3(&curve, piece.start).map_err(|_| BooleanError::Evaluation)?,
378                end: evaluate3(&curve, piece.end).map_err(|_| BooleanError::Evaluation)?,
379                along_a,
380                along_b,
381                other_a: bounding.clone(),
382                other_b: bounding.clone(),
383            });
384        }
385    }
386    Ok(())
387}
388
389/// Where a point on a face's support lies relative to the face.
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391enum Place {
392    Inside,
393    Boundary,
394    Outside,
395}
396
397/// The pieces of a bounded edge span between the cuts inside it.
398fn pieces_within(curve: &Curve3, span: Interval, cuts: Vec<Scalar>) -> Vec<Interval> {
399    let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
400    let slack = 1e-12 * (1.0 + lo.abs().max(hi.abs()));
401    let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
402    let mut inside = vec![lo, hi];
403    for cut in cuts {
404        let candidates: &[Scalar] = if periodic {
405            &[cut - TAU, cut, cut + TAU, cut + 2.0 * TAU]
406        } else {
407            &[cut]
408        };
409        for &c in candidates {
410            if c > lo + slack && c < hi - slack {
411                inside.push(c);
412            }
413        }
414    }
415    inside.sort_by(Scalar::total_cmp);
416    inside.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
417    inside
418        .windows(2)
419        .map(|pair| Interval::new(pair[0], pair[1]))
420        .collect()
421}
422
423/// The stretches between consecutive cuts, each with the curve it lies on:
424/// finite stretches of a line (its unbounded ends leave every bounded
425/// face), the cyclic arcs of a conic, and for a traced section the
426/// stretches of its span. A traced loop's stretch across the loop's start
427/// becomes a curve of its own.
428fn pieces(curve: &Curve3, mut cuts: Vec<Scalar>) -> Result<Vec<(Curve3, Interval)>, BooleanError> {
429    cuts.sort_by(Scalar::total_cmp);
430    cuts.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
431    let plain = |spans: Vec<Interval>| spans.into_iter().map(|s| (curve.clone(), s)).collect();
432    Ok(match curve {
433        Curve3::Line(_) => plain(
434            cuts.windows(2)
435                .map(|pair| Interval::new(pair[0], pair[1]))
436                .collect(),
437        ),
438        Curve3::ImplicitSection(section) => {
439            let n = section.curve.end();
440            let (pu, pv) = section.carrier.periodic();
441            let closure = section.curve.closure(pu, pv);
442            let slack = 1e-9 * (1.0 + n);
443            let mut inner: Vec<Scalar> = cuts
444                .into_iter()
445                .filter(|&c| c > slack && c < n - slack)
446                .collect();
447            inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
448            let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
449                // One cut on a loop: the whole loop, starting there.
450                let sub = if (a - b).abs() <= slack {
451                    closure.and_then(|c| section.curve.rotated(a, c))
452                } else {
453                    section.curve.sub(a, b, closure)
454                };
455                let sub = sub.ok_or(BooleanError::Evaluation)?;
456                let end = sub.end();
457                Ok((
458                    Curve3::ImplicitSection(axiolid_curve::ImplicitSection3 {
459                        carrier: section.carrier.clone(),
460                        curve: sub,
461                    }),
462                    Interval::new(0.0, end),
463                ))
464            };
465            if closure.is_some() {
466                if inner.is_empty() {
467                    return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
468                }
469                let mut out = Vec::new();
470                for pair in inner.windows(2) {
471                    out.push(own(pair[0], pair[1])?);
472                }
473                // Across the loop's own start.
474                out.push(own(inner[inner.len() - 1], inner[0])?);
475                out
476            } else {
477                let mut ends = vec![0.0];
478                ends.extend(inner);
479                ends.push(n);
480                let mut out = Vec::new();
481                for pair in ends.windows(2) {
482                    out.push(own(pair[0], pair[1])?);
483                }
484                out
485            }
486        }
487        Curve3::PairSection(section) => {
488            let n = section.end();
489            let slack = 1e-9 * (1.0 + n);
490            let mut inner: Vec<Scalar> = cuts
491                .into_iter()
492                .filter(|&c| c > slack && c < n - slack)
493                .collect();
494            inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
495            let (first, last) = (
496                section.nodes[0].point,
497                section.nodes[section.nodes.len() - 1].point,
498            );
499            let closed = first == last;
500            let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
501                let sub = if a < b {
502                    section.sub(a, b)
503                } else {
504                    // Across a loop's own start.
505                    section
506                        .sub(a, n)
507                        .zip(section.sub(0.0, b))
508                        .map(|(mut x, y)| {
509                            x.nodes.extend(y.nodes.into_iter().skip(1));
510                            x
511                        })
512                };
513                let sub = sub.ok_or(BooleanError::Evaluation)?;
514                let end = sub.end();
515                Ok((Curve3::PairSection(sub), Interval::new(0.0, end)))
516            };
517            let mut out = Vec::new();
518            if closed {
519                if inner.is_empty() {
520                    return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
521                }
522                for pair in inner.windows(2) {
523                    out.push(own(pair[0], pair[1])?);
524                }
525                out.push(own(inner[inner.len() - 1], inner[0])?);
526            } else {
527                let mut ends = vec![0.0];
528                ends.extend(inner);
529                ends.push(n);
530                for pair in ends.windows(2) {
531                    out.push(own(pair[0], pair[1])?);
532                }
533            }
534            out
535        }
536        _ => {
537            if cuts.is_empty() {
538                return Ok(vec![(curve.clone(), Interval::new(0.0, TAU))]);
539            }
540            let mut out: Vec<Interval> = cuts
541                .windows(2)
542                .map(|pair| Interval::new(pair[0], pair[1]))
543                .collect();
544            out.push(Interval::new(cuts[cuts.len() - 1], cuts[0] + TAU));
545            plain(out)
546        }
547    })
548}
549
550/// One operand, with each face's certified domain built once.
551struct Side<'a> {
552    brep: &'a ExactBRep,
553    faces: Vec<FaceId>,
554    domains: Vec<FaceDomain<'a>>,
555    /// For each edge, the faces whose loops use it.
556    edge_faces: Vec<Vec<usize>>,
557}
558
559impl<'a> Side<'a> {
560    fn new(brep: &'a ExactBRep, tolerance: Tolerance) -> Result<Self, BooleanError> {
561        let topology = brep.topology();
562        let mut faces = Vec::with_capacity(topology.faces().len());
563        let mut domains = Vec::with_capacity(topology.faces().len());
564        for index in 0..topology.faces().len() {
565            let face = topology
566                .face_id_at(index)
567                .ok_or(BooleanError::DanglingReference)?;
568            let domain = FaceDomain::new(brep, face, tolerance)
569                .map_err(BooleanError::Measure)?
570                .ok_or(BooleanError::UnsupportedTrim)?;
571            faces.push(face);
572            domains.push(domain);
573        }
574        let mut edge_faces = vec![Vec::new(); topology.edges().len()];
575        for (index, face) in topology.faces().iter().enumerate() {
576            for bound in &face.bounds {
577                let wire = topology
578                    .loops()
579                    .get(bound.loop_id.index())
580                    .ok_or(BooleanError::DanglingReference)?;
581                for use_ in &wire.edges {
582                    edge_faces[use_.edge.index()].push(index);
583                }
584            }
585        }
586        Ok(Self {
587            brep,
588            faces,
589            domains,
590            edge_faces,
591        })
592    }
593
594    fn surface(&self, face: usize) -> Result<&'a Surface, BooleanError> {
595        self.brep.topology().faces()[face]
596            .surface
597            .and_then(|id| self.brep.surfaces().get(id.index()))
598            .ok_or(BooleanError::DanglingReference)
599    }
600
601    /// Where `point`, on the face's support, lies: on one of the edges the
602    /// curve through it runs along (`along`), or inside or outside the face.
603    fn locate(
604        &self,
605        face: usize,
606        point: Point3,
607        along: &[(Curve3, Interval)],
608        tolerance: Tolerance,
609    ) -> Result<Place, BooleanError> {
610        for (curve, span) in along {
611            if on_edge(curve, *span, point, tolerance)? {
612                return Ok(Place::Boundary);
613            }
614        }
615        let (u, v) =
616            locate(self.surface(face)?, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
617        match self.domains[face]
618            .contains(Point2::new(u, v))
619            .map_err(BooleanError::Measure)?
620        {
621            Some(true) => Ok(Place::Inside),
622            Some(false) => Ok(Place::Outside),
623            None => Err(BooleanError::Undecided),
624        }
625    }
626
627    /// The face's boundary edges, each once, with their curves and spans.
628    fn edges_of(&self, face: usize) -> Result<Vec<(usize, Curve3, Interval)>, BooleanError> {
629        let topology = self.brep.topology();
630        let mut out = Vec::new();
631        let mut seen = Vec::new();
632        for bound in &topology.faces()[face].bounds {
633            let wire = topology
634                .loops()
635                .get(bound.loop_id.index())
636                .ok_or(BooleanError::DanglingReference)?;
637            for use_ in &wire.edges {
638                if seen.contains(&use_.edge) {
639                    continue;
640                }
641                seen.push(use_.edge);
642                let curve = topology.edges()[use_.edge.index()]
643                    .curve
644                    .and_then(|id| self.brep.curves3().get(id.index()))
645                    .ok_or(BooleanError::DanglingReference)?;
646                let span = self
647                    .brep
648                    .edge_interval(use_.edge)
649                    .ok_or(BooleanError::DanglingReference)?;
650                out.push((use_.edge.index(), curve.clone(), span));
651            }
652        }
653        Ok(out)
654    }
655
656    /// Parameters on `curve` where it crosses a boundary edge of the face,
657    /// and the edges it runs along (lying in the adjacent face's surface
658    /// too), which cut it at their ends.
659    #[allow(clippy::type_complexity)]
660    ///
661    /// `meets` is the other surface `curve` lies on. Where the curve cannot
662    /// be intersected with the adjacent face's surface (a B-spline), the
663    /// edge is intersected with `meets` instead: the curve crosses the edge
664    /// exactly where the edge crosses `meets`.
665    fn cuts(
666        &self,
667        face: usize,
668        curve: &Curve3,
669        meets: &Surface,
670        tolerance: Tolerance,
671    ) -> Result<(Vec<Scalar>, Vec<(Curve3, Interval)>), BooleanError> {
672        let topology = self.brep.topology();
673        let mut out = Vec::new();
674        let mut along = Vec::new();
675        let mut seen = Vec::new();
676        // A pole or apex the curve passes through cuts it: its pcurve jumps
677        // round the angle there.
678        for pole in poles(self.surface(face)?) {
679            if let Ok(t) = locate3(curve, pole, tolerance) {
680                let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
681                if (on - pole).length() <= tolerance.linear().max(1e-9) {
682                    out.push(t);
683                }
684            }
685        }
686        for bound in &topology.faces()[face].bounds {
687            let wire = topology
688                .loops()
689                .get(bound.loop_id.index())
690                .ok_or(BooleanError::DanglingReference)?;
691            for use_ in &wire.edges {
692                if seen.contains(&use_.edge) {
693                    continue;
694                }
695                seen.push(use_.edge);
696                let edge = &topology.edges()[use_.edge.index()];
697                let edge_curve = edge
698                    .curve
699                    .and_then(|id| self.brep.curves3().get(id.index()))
700                    .ok_or(BooleanError::DanglingReference)?;
701                let span = self
702                    .brep
703                    .edge_interval(use_.edge)
704                    .ok_or(BooleanError::DanglingReference)?;
705                let mut cutter =
706                    self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, false)?;
707                let mut result = exact_curve_surface_intersection(curve, &cutter);
708                if matches!(result, Ok(ExactCurveIntersection::Contained)) {
709                    // The adjacent face may lie on the same surface under a
710                    // different frame (a column's half-walls): cut across
711                    // the seam instead.
712                    if let Ok(seam) =
713                        self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, true)
714                    {
715                        cutter = seam;
716                        result = exact_curve_surface_intersection(curve, &cutter);
717                    }
718                }
719                match result {
720                    Ok(ExactCurveIntersection::Points(hits)) => {
721                        for hit in hits {
722                            if on_edge(edge_curve, span, hit.point, tolerance)? {
723                                out.push(hit.parameter.approx());
724                            }
725                        }
726                    }
727                    // The curve lies in the adjacent face's surface as well
728                    // as this one's: where it meets the edge it runs along
729                    // it, so it is cut at the edge's ends.
730                    Ok(ExactCurveIntersection::Contained) => {
731                        for t in [span.start, span.end] {
732                            let end =
733                                evaluate3(edge_curve, t).map_err(|_| BooleanError::Evaluation)?;
734                            if let Ok(s) = locate3(curve, end, tolerance) {
735                                let on =
736                                    evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
737                                if (on - end).length() <= tolerance.linear().max(1e-9) {
738                                    out.push(s);
739                                }
740                            }
741                        }
742                        along.push((edge_curve.clone(), span));
743                    }
744                    Ok(_) => return Err(BooleanError::UnsupportedSection),
745                    Err(_) => {
746                        let hits = match exact_curve_surface_intersection(edge_curve, meets) {
747                            Ok(ExactCurveIntersection::Points(hits)) => hits,
748                            _ => return Err(BooleanError::UnsupportedTrim),
749                        };
750                        for hit in hits {
751                            if !on_span(edge_curve, span, hit.point, tolerance)? {
752                                continue;
753                            }
754                            if let Ok(s) = locate3(curve, hit.point, tolerance) {
755                                let on =
756                                    evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
757                                if (on - hit.point).length() <= tolerance.linear().max(1e-9) {
758                                    out.push(s);
759                                }
760                            }
761                        }
762                    }
763                }
764            }
765        }
766        Ok((out, along))
767    }
768}
769
770impl Side<'_> {
771    /// A surface the section crosses exactly where it crosses the edge: the
772    /// adjacent face's support, or across a seam (the same support on both
773    /// sides) the plane through the ruling and the surface normal.
774    fn cutter(
775        &self,
776        face: usize,
777        edge: usize,
778        edge_curve: &Curve3,
779        span: Interval,
780        tolerance: Tolerance,
781        across_seam: bool,
782    ) -> Result<Surface, BooleanError> {
783        let own = self.surface(face)?;
784        let adjacent = self.edge_faces[edge]
785            .iter()
786            .copied()
787            .find(|&other| other != face);
788        if let (Some(other), false) = (adjacent, across_seam) {
789            let theirs = self.surface(other)?;
790            if !same_support(own, theirs, tolerance) {
791                return Ok(theirs.clone());
792            }
793        }
794        // A seam: the same support on both sides (or a free edge). A seam
795        // circle (a sphere's meridian or latitude, a torus's tube or ring
796        // circle) is cut by the surface its normals sweep: coaxial with the
797        // circle, a plane, cylinder or cone, which crosses the face
798        // transversally along the whole circle.
799        if let Curve3::Circle(circle) = edge_curve {
800            return normal_sweep(own, circle, tolerance);
801        }
802        let Curve3::Line(line) = edge_curve else {
803            return Err(BooleanError::UnsupportedTrim);
804        };
805        let mid = evaluate3(edge_curve, 0.5 * (span.start + span.end))
806            .map_err(|_| BooleanError::Evaluation)?;
807        let (u, v) = locate(own, mid, tolerance).map_err(|_| BooleanError::Evaluation)?;
808        let n = normal(own, u, v).map_err(|_| BooleanError::Evaluation)?;
809        let across = line.direction.cross(n);
810        let length = across.length();
811        if length == 0.0 || !length.is_finite() {
812            return Err(BooleanError::UnsupportedTrim);
813        }
814        let z = across / length;
815        let x = line.direction.normalize();
816        Ok(Surface::Plane(Plane {
817            frame: axiolid_core::Frame3 {
818                origin: mid,
819                x,
820                y: z.cross(x),
821                z,
822            },
823        }))
824    }
825}
826
827/// The surface swept by `surface`'s normal lines along a circle on it,
828/// when the surface is one of revolution about the circle's axis (or the
829/// circle is a meridian, whose normals stay in its plane): a plane, a
830/// cylinder or a cone coaxial with the circle.
831fn normal_sweep(
832    surface: &Surface,
833    circle: &axiolid_curve::Circle3,
834    tolerance: Tolerance,
835) -> Result<Surface, BooleanError> {
836    let f = circle.frame;
837    let axis = f.x.cross(f.y).normalize();
838    let x = f.x.normalize();
839    let y = axis.cross(x);
840    let p = f.origin + x * circle.radius;
841    let (u, v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
842    let n = normal(surface, u, v)
843        .map_err(|_| BooleanError::Evaluation)?
844        .normalize();
845    let frame = axiolid_core::Frame3 {
846        origin: f.origin,
847        x,
848        y,
849        z: axis,
850    };
851    let along = n.dot(axis);
852    let radial = n.dot(x);
853    let plane = || {
854        Surface::Plane(Plane {
855            frame: axiolid_core::Frame3 {
856                origin: f.origin,
857                x,
858                y,
859                z: axis,
860            },
861        })
862    };
863    // Normals within the circle's plane: the plane itself.
864    if along.abs() <= 1e-12 {
865        return Ok(plane());
866    }
867    // Normals along the axis: the cylinder through the circle.
868    if radial.abs() <= 1e-12 {
869        return Ok(Surface::Cylinder(axiolid_surface::Cylinder {
870            frame,
871            radius: circle.radius,
872        }));
873    }
874    // Otherwise the cone of normal lines: at height `h` along the axis the
875    // line is `radius + h * radial / along` from it.
876    Ok(Surface::Cone(axiolid_surface::Cone {
877        frame,
878        radius: circle.radius,
879        semi_angle: (radial / along).atan(),
880    }))
881}
882
883/// Whether `point` lies on the edge within tolerance and inside its span.
884fn on_edge(
885    curve: &Curve3,
886    span: Interval,
887    point: Point3,
888    tolerance: Tolerance,
889) -> Result<bool, BooleanError> {
890    let Ok(t) = locate3(curve, point, tolerance) else {
891        return Ok(false);
892    };
893    let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
894    if (on - point).length() > tolerance.linear().max(1e-9) {
895        return Ok(false);
896    }
897    on_span(curve, span, point, tolerance)
898}
899
900/// Whether `point`, on `curve`, lies within the edge's span (a closed
901/// conic's span may start anywhere and run past a full turn).
902fn on_span(
903    curve: &Curve3,
904    span: Interval,
905    point: Point3,
906    tolerance: Tolerance,
907) -> Result<bool, BooleanError> {
908    let t = locate3(curve, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
909    let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
910    let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
911    let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
912    let candidates: &[Scalar] = if periodic {
913        &[t - TAU, t, t + TAU, t + 2.0 * TAU]
914    } else {
915        &[t]
916    };
917    Ok(candidates
918        .iter()
919        .any(|c| *c >= lo - slack && *c <= hi + slack))
920}