axiolid_measure/
exact_domain.rs

1//! A face's domain in its surface parameters: the pcurves of its loops,
2//! joined across seams and poles.
3//!
4//! Shared by the mass-property integral ([`crate::exact_face`]) and the
5//! certified distance ([`crate::exact_distance`]). See `exact_face` for why
6//! a periodic surface's loops need not close in the parameter plane, and how
7//! a net winding reaches a pole.
8
9use axiolid_brep::ExactBRep;
10use axiolid_core::{Point2, Point3, Scalar, Vec2};
11use axiolid_curve::Curve2;
12use axiolid_evaluate::surface::evaluate;
13use axiolid_evaluate::{derivative2, evaluate2};
14use axiolid_surface::Surface;
15use axiolid_topology::{Face, Orientation};
16use core::f64::consts::{FRAC_PI_2, TAU};
17
18use crate::exact::ExactMeasureError;
19
20/// How a face's surface is parameterised: its periods and its poles.
21pub(crate) struct Chart {
22    pub(crate) u_period: Option<Scalar>,
23    pub(crate) v_period: Option<Scalar>,
24    /// `v` values where the surface collapses to one point for every `u`.
25    pub(crate) poles: Vec<Scalar>,
26}
27
28pub(crate) fn chart(surface: &Surface) -> Result<Chart, ExactMeasureError> {
29    let chart = match surface {
30        Surface::Plane(_) | Surface::BSpline(_) => Chart {
31            u_period: None,
32            v_period: None,
33            poles: Vec::new(),
34        },
35        Surface::Cylinder(_) | Surface::EllipticalCylinder(_) => Chart {
36            u_period: Some(TAU),
37            v_period: None,
38            poles: Vec::new(),
39        },
40        Surface::Cone(cone) => {
41            let slope = cone.semi_angle.tan();
42            let poles = if slope.is_finite() && slope != 0.0 {
43                vec![-cone.radius / slope]
44            } else {
45                Vec::new()
46            };
47            Chart {
48                u_period: Some(TAU),
49                v_period: None,
50                poles,
51            }
52        }
53        Surface::Sphere(_) => Chart {
54            u_period: Some(TAU),
55            v_period: None,
56            poles: vec![-FRAC_PI_2, FRAC_PI_2],
57        },
58        Surface::Torus(_) => Chart {
59            u_period: Some(TAU),
60            v_period: Some(TAU),
61            poles: Vec::new(),
62        },
63        _ => {
64            return Err(ExactMeasureError::NonPlanarFace(crate::exact::family(
65                surface,
66            )))
67        }
68    };
69    Ok(chart)
70}
71
72/// One stretch of a loop in the parameter plane.
73pub(crate) enum Piece<'a> {
74    /// A pcurve over `[start, end]`, shifted by whole periods.
75    Curve {
76        curve: &'a Curve2,
77        start: Scalar,
78        end: Scalar,
79        offset: Vec2,
80    },
81    /// A straight closing stretch: across a pole, or a gap within tolerance.
82    Segment { from: Point2, to: Point2 },
83}
84
85impl Piece<'_> {
86    pub(crate) fn span(&self) -> (Scalar, Scalar) {
87        match self {
88            Self::Curve { start, end, .. } => (*start, *end),
89            Self::Segment { .. } => (0.0, 1.0),
90        }
91    }
92
93    /// The span split where the pcurve's derivative may jump: an implicit
94    /// pcurve's cell boundaries (ADR 0077), where the parameter's speed
95    /// changes. Integrating piecewise between them keeps each panel smooth.
96    pub(crate) fn smooth_spans(&self) -> Vec<(Scalar, Scalar)> {
97        let (a, b) = self.span();
98        let mut cuts = vec![a];
99        let kinked = match self {
100            Self::Curve {
101                curve: Curve2::Implicit(_),
102                ..
103            } => true,
104            Self::Curve {
105                curve: Curve2::Lifted(l),
106                ..
107            } => matches!(
108                l.curve.as_ref(),
109                axiolid_curve::Curve3::ImplicitSection(_) | axiolid_curve::Curve3::PairSection(_)
110            ),
111            _ => false,
112        };
113        if kinked {
114            let (lo, hi) = (a.min(b), a.max(b));
115            let mut k = lo.floor() + 1.0;
116            let mut inner = Vec::new();
117            while k < hi {
118                inner.push(k);
119                k += 1.0;
120            }
121            if b < a {
122                inner.reverse();
123            }
124            cuts.extend(inner);
125        }
126        cuts.push(b);
127        cuts.windows(2).map(|w| (w[0], w[1])).collect()
128    }
129
130    pub(crate) fn at(&self, t: Scalar) -> Result<(Point2, Vec2), ExactMeasureError> {
131        match self {
132            Self::Curve { curve, offset, .. } => {
133                let point = evaluate2(curve, t).map_err(|_| crate::exact::EVALUATION)?;
134                let tangent = derivative2(curve, t).map_err(|_| crate::exact::EVALUATION)?;
135                Ok((point + *offset, tangent))
136            }
137            Self::Segment { from, to } => Ok((*from + (*to - *from) * t, *to - *from)),
138        }
139    }
140
141    pub(crate) fn end_point(&self) -> Result<Point2, ExactMeasureError> {
142        Ok(self.at(self.span().1)?.0)
143    }
144}
145
146/// A face's boundary, assembled in the parameter plane.
147pub(crate) struct Boundary<'a> {
148    pub(crate) pieces: Vec<Piece<'a>>,
149    /// Net whole periods each loop winds, summed over the face.
150    pub(crate) winding: [i64; 2],
151    /// Whether any single loop winds in `u` / in `v`.
152    pub(crate) wraps: [bool; 2],
153    /// A point on the boundary, for choosing the reference line.
154    pub(crate) anchor: Point2,
155}
156
157/// The pole the face's domain reaches, used as the reference line so the
158/// pole itself -- the boundary no loop states -- contributes zero.
159pub(crate) fn pole_on_domain_side(
160    chart: &Chart,
161    boundary: &Boundary<'_>,
162) -> Result<Scalar, ExactMeasureError> {
163    // A loop running +u keeps its domain on its left, towards +v.
164    let upward = match boundary.winding[0] {
165        1 => true,
166        -1 => false,
167        _ => {
168            return Err(ExactMeasureError::NonPlanarFace(
169                "face boundary winds around the surface more than once",
170            ))
171        }
172    };
173    chart
174        .poles
175        .iter()
176        .copied()
177        .filter(|pole| (*pole > boundary.anchor.y) == upward)
178        .min_by(|a, b| {
179            (a - boundary.anchor.y)
180                .abs()
181                .total_cmp(&(b - boundary.anchor.y).abs())
182        })
183        .ok_or(ExactMeasureError::NonPlanarFace(
184            "face boundary winds around a surface with no pole on the domain side",
185        ))
186}
187
188/// Assemble every bound's pcurves into one boundary, joining each use to the
189/// next through whole periods, across a pole, or over a gap within tolerance.
190pub(crate) fn assemble<'a>(
191    brep: &'a ExactBRep,
192    face: &Face<axiolid_brep::SurfaceId>,
193    surface: &Surface,
194    chart: &Chart,
195    linear: Scalar,
196) -> Result<Boundary<'a>, ExactMeasureError> {
197    let topology = brep.topology();
198    let mut pieces = Vec::new();
199    let mut winding = [0_i64; 2];
200    let mut wraps = [false; 2];
201    let mut anchor = None;
202
203    for bound in &face.bounds {
204        let wire = topology
205            .loops()
206            .get(bound.loop_id.index())
207            .ok_or(ExactMeasureError::DanglingReference)?;
208        let reversed = bound.orientation == Orientation::Reversed;
209
210        // Each use's pcurve interval already runs in the use's traversal
211        // (ADR 0024); a reversed bound walks the loop backwards.
212        let mut uses = Vec::with_capacity(wire.edges.len());
213        for (index, use_) in wire.edges.iter().enumerate() {
214            let pcurve = use_.pcurve.ok_or(ExactMeasureError::DanglingReference)?;
215            let curve = brep
216                .curves2()
217                .get(pcurve.index())
218                .ok_or(ExactMeasureError::DanglingReference)?;
219            let interval = brep
220                .pcurve_interval(bound.loop_id, index)
221                .ok_or(ExactMeasureError::DanglingReference)?;
222            let (start, end) = if reversed {
223                (interval.end, interval.start)
224            } else {
225                (interval.start, interval.end)
226            };
227            uses.push((curve, start, end));
228        }
229        if reversed {
230            uses.reverse();
231        }
232        let Some(&(first_curve, first_start, _)) = uses.first() else {
233            continue;
234        };
235
236        let loop_start =
237            evaluate2(first_curve, first_start).map_err(|_| crate::exact::EVALUATION)?;
238        anchor.get_or_insert(loop_start);
239        let mut offset = Vec2::ZERO;
240        let mut cursor = loop_start;
241        for (curve, start, end) in uses {
242            let raw = evaluate2(curve, start).map_err(|_| crate::exact::EVALUATION)?;
243            let (shift, bridge) = join(surface, chart, cursor, raw + offset, linear)?;
244            offset += shift;
245            if let Some(segment) = bridge {
246                pieces.push(segment);
247            }
248            let piece = Piece::Curve {
249                curve,
250                start,
251                end,
252                offset,
253            };
254            cursor = piece.end_point()?;
255            pieces.push(piece);
256        }
257
258        // Close the loop back onto its own start.
259        let (shift, bridge) = join(surface, chart, cursor, loop_start, linear)?;
260        if let Some(segment) = bridge {
261            pieces.push(segment);
262        }
263        // `shift` moved the start onto the end: the loop ended that many
264        // periods on from where it began.
265        let turns = [
266            periods(shift.x, chart.u_period),
267            periods(shift.y, chart.v_period),
268        ];
269        for axis in 0..2 {
270            winding[axis] += turns[axis];
271            wraps[axis] |= turns[axis] != 0;
272        }
273    }
274
275    let anchor = anchor.ok_or(ExactMeasureError::Degenerate)?;
276    Ok(Boundary {
277        pieces,
278        winding,
279        wraps,
280        anchor,
281    })
282}
283
284/// Whole periods in `shift`, which is already a multiple of `period`.
285fn periods(shift: Scalar, period: Option<Scalar>) -> i64 {
286    period.map_or(0, |period| (shift / period).round() as i64)
287}
288
289/// Join a loop that has reached `from` to the next use starting at `to`.
290///
291/// Returns the whole-period shift to apply to `to` and everything after it,
292/// and the closing segment, if one is needed. The two must be the same point
293/// on the surface; the parameter gap between them may be whole periods (a
294/// seam), a stretch along a pole, or a residue within tolerance.
295fn join<'a>(
296    surface: &Surface,
297    chart: &Chart,
298    from: Point2,
299    to: Point2,
300    linear: Scalar,
301) -> Result<(Vec2, Option<Piece<'a>>), ExactMeasureError> {
302    let there = point(surface, from)?;
303    let here = point(surface, to)?;
304    if (there - here).length() > linear {
305        return Err(ExactMeasureError::NonPlanarFace(
306            "consecutive pcurves of a face loop do not meet on the surface",
307        ));
308    }
309
310    // Along a pole `u` is free: keep the stretch as it is, so its `H du`
311    // is integrated, rather than folding it into periods.
312    let on_pole = chart.poles.iter().any(|pole| {
313        (from.y - pole).abs() <= parameter_slack(*pole)
314            && (to.y - pole).abs() <= parameter_slack(*pole)
315    });
316    let shift = if on_pole {
317        Vec2::ZERO
318    } else {
319        let wrap = |gap: Scalar, period: Option<Scalar>| {
320            period.map_or(0.0, |period| (gap / period).round() * period)
321        };
322        Vec2::new(
323            wrap(from.x - to.x, chart.u_period),
324            wrap(from.y - to.y, chart.v_period),
325        )
326    };
327    let landed = to + shift;
328    let bridge = (landed != from).then_some(Piece::Segment { from, to: landed });
329    Ok((shift, bridge))
330}
331
332fn parameter_slack(value: Scalar) -> Scalar {
333    1e-9 * value.abs().max(1.0)
334}
335
336pub(crate) fn point(surface: &Surface, at: Point2) -> Result<Point3, ExactMeasureError> {
337    evaluate(surface, at.x, at.y).map_err(|_| crate::exact::EVALUATION)
338}
339
340/// One stretch of a boundary piece over which both parameters are monotone.
341///
342/// Its box is the box of its two ends, exactly, and a line `u = c` crosses
343/// it at most once -- which is what lets a crossing be decided from the
344/// ends alone.
345struct MonoArc {
346    piece: usize,
347    t0: Scalar,
348    t1: Scalar,
349    a: Point2,
350    b: Point2,
351}
352
353/// A face's parameter domain, able to say which points lie in it.
354///
355/// Decisions are certified or refused: a point too close to the boundary
356/// for the crossing count to be sure is `None`, never a guess.
357pub(crate) struct Domain<'a> {
358    boundary: Boundary<'a>,
359    arcs: Vec<MonoArc>,
360    /// Loops wind round the tube (`v`): coordinates are swapped so the ray
361    /// is always cast in the second one.
362    transpose: bool,
363    /// Period of the first (possibly swapped) coordinate.
364    period: Option<Scalar>,
365    /// Period of the second (possibly swapped) coordinate, when the surface
366    /// has one but the loops do not wind round it (a torus face that goes
367    /// round the axis but not round the tube): a point is moved by whole
368    /// periods into the box before it is classified.
369    across: Option<Scalar>,
370    /// The pole the domain reaches and the net winding that reaches it.
371    pole: Option<(Scalar, i64)>,
372    /// Box of the domain, in unswapped `(u, v)`.
373    pub(crate) min: Point2,
374    pub(crate) max: Point2,
375}
376
377/// Parameters in `(lo, hi)` where one coordinate of a pcurve turns.
378fn turning_points(curve: &Curve2, lo: Scalar, hi: Scalar) -> Option<Vec<Scalar>> {
379    let mut out = Vec::new();
380    let mut add_trig = |a: Scalar, b: Scalar| {
381        // `a cos t + b sin t` turns where its derivative vanishes.
382        if a == 0.0 && b == 0.0 {
383            return;
384        }
385        let base = b.atan2(a);
386        let first = ((lo - base) / core::f64::consts::PI).floor() as i64;
387        let last = ((hi - base) / core::f64::consts::PI).ceil() as i64;
388        for k in first..=last {
389            let t = base + k as Scalar * core::f64::consts::PI;
390            if t > lo && t < hi {
391                out.push(t);
392            }
393        }
394    };
395    match curve {
396        Curve2::Line(_) => {}
397        Curve2::Circle(c) => {
398            add_trig(c.radius * c.frame.x.x, c.radius * c.frame.y.x);
399            add_trig(c.radius * c.frame.x.y, c.radius * c.frame.y.y);
400        }
401        Curve2::Ellipse(e) => {
402            add_trig(e.semi_axis_x * e.frame.x.x, e.semi_axis_y * e.frame.y.x);
403            add_trig(e.semi_axis_x * e.frame.x.y, e.semi_axis_y * e.frame.y.y);
404        }
405        Curve2::Sinusoid(w) => add_trig(w.cosine, w.sine),
406        Curve2::Polyline(_) => {
407            let mut k = lo.floor() + 1.0;
408            while k < hi {
409                out.push(k);
410                k += 1.0;
411            }
412        }
413        // Cell boundaries and certified turns of the solved parameter
414        // (ADR 0077).
415        Curve2::Implicit(c) => out.extend(c.turning_points(lo, hi)),
416        // A B-spline trim: its knots, where the derivative may jump, and
417        // its turns by a dense scan between them.
418        Curve2::BSpline(b) => {
419            out.extend(b.knots.iter().copied().filter(|k| *k > lo && *k < hi));
420            out.extend(scanned_turns(curve, lo, hi)?);
421        }
422        // A space curve read on an analytic surface: its turns are found
423        // by a dense scan of the derivative's signs and bisection, and the
424        // space curve's own cell boundaries are kept as breaks.
425        Curve2::Lifted(l) => {
426            if let axiolid_curve::Curve3::ImplicitSection(_)
427            | axiolid_curve::Curve3::PairSection(_) = l.curve.as_ref()
428            {
429                let mut k = lo.floor() + 1.0;
430                while k < hi {
431                    out.push(k);
432                    k += 1.0;
433                }
434            }
435            out.extend(scanned_turns(curve, lo, hi)?);
436        }
437        _ => return None,
438    }
439    out.sort_by(Scalar::total_cmp);
440    Some(out)
441}
442
443/// Where either coordinate of a pcurve's derivative changes sign in
444/// `(lo, hi)`: a scan of 256 samples, each change bisected to the last
445/// bits.
446fn scanned_turns(curve: &Curve2, lo: Scalar, hi: Scalar) -> Option<Vec<Scalar>> {
447    let n = 256;
448    let d = |t: Scalar| derivative2(curve, t).ok();
449    let mut out = Vec::new();
450    let mut previous = (lo, d(lo + (hi - lo) * 1e-9)?);
451    for i in 1..=n {
452        let t = if i == n {
453            hi - (hi - lo) * 1e-9
454        } else {
455            lo + (hi - lo) * i as Scalar / n as Scalar
456        };
457        let now = d(t)?;
458        for axis in 0..2 {
459            let (a, b) = (previous.1[axis], now[axis]);
460            if (a < 0.0) != (b < 0.0) && a != 0.0 && b != 0.0 {
461                let (mut x0, mut x1) = (previous.0, t);
462                for _ in 0..80 {
463                    let m = 0.5 * (x0 + x1);
464                    let dm = d(m)?[axis];
465                    if (dm < 0.0) == (a < 0.0) {
466                        x0 = m;
467                    } else {
468                        x1 = m;
469                    }
470                }
471                out.push(0.5 * (x0 + x1));
472            }
473        }
474        previous = (t, now);
475    }
476    Some(out)
477}
478
479fn slack(value: Scalar) -> Scalar {
480    1e-9 * (1.0 + value.abs())
481}
482
483impl<'a> Domain<'a> {
484    /// The face's domain, or `None` when a pcurve family has no monotone
485    /// split here (a B-spline or intrinsic trim): such a face is still
486    /// bounded, just never classified.
487    pub(crate) fn new(
488        brep: &'a ExactBRep,
489        face: &Face<axiolid_brep::SurfaceId>,
490        surface: &Surface,
491        linear: Scalar,
492    ) -> Result<Option<Self>, ExactMeasureError> {
493        let chart = chart(surface)?;
494        let boundary = assemble(brep, face, surface, &chart, linear)?;
495        let transpose = boundary.wraps[1];
496        if transpose && (boundary.wraps[0] || boundary.winding[1] != 0) {
497            return Err(ExactMeasureError::NonPlanarFace(
498                "face boundary winds around the surface in both directions",
499            ));
500        }
501        let pole = if !transpose && boundary.winding[0] != 0 {
502            Some((pole_on_domain_side(&chart, &boundary)?, boundary.winding[0]))
503        } else {
504            None
505        };
506        let swap = |p: Point2| if transpose { Point2::new(p.y, p.x) } else { p };
507
508        let mut arcs = Vec::new();
509        for (index, piece) in boundary.pieces.iter().enumerate() {
510            let (start, end) = piece.span();
511            let (lo, hi) = (start.min(end), start.max(end));
512            let mut cuts = match piece {
513                Piece::Curve { curve, .. } => match turning_points(curve, lo, hi) {
514                    Some(cuts) => cuts,
515                    None => return Ok(None),
516                },
517                Piece::Segment { .. } => Vec::new(),
518            };
519            if start > end {
520                cuts.reverse();
521            }
522            let mut ts = vec![start];
523            ts.extend(cuts);
524            ts.push(end);
525            for pair in ts.windows(2) {
526                let a = swap(piece.at(pair[0])?.0);
527                let b = swap(piece.at(pair[1])?.0);
528                arcs.push(MonoArc {
529                    piece: index,
530                    t0: pair[0],
531                    t1: pair[1],
532                    a,
533                    b,
534                });
535            }
536        }
537        if arcs.is_empty() {
538            return Ok(None);
539        }
540        // Where two pieces meet, their ends are the same vertex evaluated
541        // twice and may differ in the last bits. One value per vertex keeps
542        // the ray test's left/right decision at that vertex the same for
543        // both arcs.
544        let mut vertices: Vec<Point2> = Vec::new();
545        let mut snap = |p: Point2| -> Point2 {
546            let near =
547                |q: &Point2| (q.x - p.x).abs() <= slack(p.x) && (q.y - p.y).abs() <= slack(p.y);
548            if let Some(q) = vertices.iter().find(|q| near(q)) {
549                return *q;
550            }
551            vertices.push(p);
552            p
553        };
554        for arc in &mut arcs {
555            arc.a = snap(arc.a);
556            arc.b = snap(arc.b);
557        }
558
559        let mut min = Point2::splat(Scalar::INFINITY);
560        let mut max = Point2::splat(Scalar::NEG_INFINITY);
561        for arc in &arcs {
562            min = min.min(arc.a.min(arc.b));
563            max = max.max(arc.a.max(arc.b));
564        }
565        if let Some((pole, _)) = pole {
566            min.y = min.y.min(pole);
567            max.y = max.y.max(pole);
568        }
569        let period = if transpose {
570            chart.v_period
571        } else {
572            chart.u_period
573        };
574        let across = if transpose {
575            chart.u_period
576        } else {
577            chart.v_period
578        };
579        let (min, max) = if transpose {
580            (Point2::new(min.y, min.x), Point2::new(max.y, max.x))
581        } else {
582            (min, max)
583        };
584        Ok(Some(Self {
585            boundary,
586            arcs,
587            transpose,
588            period,
589            across,
590            pole,
591            min,
592            max,
593        }))
594    }
595
596    fn swap(&self, p: Point2) -> Point2 {
597        if self.transpose {
598            Point2::new(p.y, p.x)
599        } else {
600            p
601        }
602    }
603
604    /// Whole-period shifts `s` of the first coordinate for which the span
605    /// `[lo, hi]`, moved to `[lo - s, hi - s]`, can meet the boundary's own
606    /// range `[min, max]`: `s` in `[lo - max, hi - min]`.
607    fn shifts(&self, lo: Scalar, hi: Scalar) -> Vec<Scalar> {
608        let Some(period) = self.period else {
609            return vec![0.0];
610        };
611        let (min, max) = self.arcs.iter().fold(
612            (Scalar::INFINITY, Scalar::NEG_INFINITY),
613            |(min, max), arc| (min.min(arc.a.x.min(arc.b.x)), max.max(arc.a.x.max(arc.b.x))),
614        );
615        let first = ((lo - max) / period).floor() as i64;
616        let last = ((hi - min) / period).ceil() as i64;
617        (first..=last).map(|k| k as Scalar * period).collect()
618    }
619
620    /// Whether the boundary may pass through the rectangle.
621    ///
622    /// `false` is certain. A monotone arc's box is exact at its ends but
623    /// covers a whole triangle beside a diagonal, so an arc whose box meets
624    /// the rectangle is bisected until its halves clear the rectangle or
625    /// one lies inside it.
626    pub(crate) fn touches(&self, lo: Point2, hi: Point2) -> Result<bool, ExactMeasureError> {
627        let (lo, hi) = {
628            let (a, b) = (self.swap(lo), self.swap(hi));
629            (a.min(b), a.max(b))
630        };
631        for shift in self.shifts(lo.x, hi.x) {
632            let (lo, hi) = (
633                Point2::new(lo.x - shift, lo.y),
634                Point2::new(hi.x - shift, hi.y),
635            );
636            for arc in &self.arcs {
637                if self.arc_touches(arc, lo, hi, 0)? {
638                    return Ok(true);
639                }
640            }
641        }
642        Ok(false)
643    }
644
645    fn arc_touches(
646        &self,
647        arc: &MonoArc,
648        lo: Point2,
649        hi: Point2,
650        depth: u32,
651    ) -> Result<bool, ExactMeasureError> {
652        let (a_lo, a_hi) = (arc.a.min(arc.b), arc.a.max(arc.b));
653        let (sx, sy) = (
654            slack(a_hi.x.abs().max(a_lo.x.abs())),
655            slack(a_hi.y.abs().max(a_lo.y.abs())),
656        );
657        if a_lo.x - sx > hi.x || a_hi.x + sx < lo.x || a_lo.y - sy > hi.y || a_hi.y + sy < lo.y {
658            return Ok(false);
659        }
660        // A straight stretch lying along one of the rectangle's own sides
661        // (a rim or seam of a wall, cut exactly where the patch was cut)
662        // does not enter it: the rectangle is still wholly in or out.
663        let along = |a: Scalar, b: Scalar, edge: Scalar, s: Scalar| {
664            (a - b).abs() <= s && (a - edge).abs() <= s && (b - edge).abs() <= s
665        };
666        if along(arc.a.x, arc.b.x, lo.x, sx)
667            || along(arc.a.x, arc.b.x, hi.x, sx)
668            || along(arc.a.y, arc.b.y, lo.y, sy)
669            || along(arc.a.y, arc.b.y, hi.y, sy)
670        {
671            return Ok(false);
672        }
673        let inside = |p: Point2| p.x >= lo.x && p.x <= hi.x && p.y >= lo.y && p.y <= hi.y;
674        if inside(arc.a) || inside(arc.b) || depth >= 40 {
675            return Ok(true);
676        }
677        let tm = 0.5 * (arc.t0 + arc.t1);
678        let m = self.swap(self.boundary.pieces[arc.piece].at(tm)?.0);
679        let first = MonoArc {
680            piece: arc.piece,
681            t0: arc.t0,
682            t1: tm,
683            a: arc.a,
684            b: m,
685        };
686        let second = MonoArc {
687            piece: arc.piece,
688            t0: tm,
689            t1: arc.t1,
690            a: m,
691            b: arc.b,
692        };
693        Ok(self.arc_touches(&first, lo, hi, depth + 1)?
694            || self.arc_touches(&second, lo, hi, depth + 1)?)
695    }
696
697    /// Whether `p` lies in the domain: `Some` when certain, `None` when it
698    /// is too close to the boundary to say.
699    pub(crate) fn contains(&self, p: Point2) -> Result<Option<bool>, ExactMeasureError> {
700        // The same point a whole period away in the coordinate the loops do
701        // not wind round, moved into the box when that lands it there.
702        let p = match self.across {
703            Some(period) => {
704                let q = self.swap(p);
705                let (lo, hi) = {
706                    let (a, b) = (self.swap(self.min), self.swap(self.max));
707                    (a.y.min(b.y), a.y.max(b.y))
708                };
709                let k = ((0.5 * (lo + hi) - q.y) / period).round();
710                let moved = q.y + k * period;
711                if moved >= lo - slack(lo) && moved <= hi + slack(hi) {
712                    self.swap(Point2::new(q.x, moved))
713                } else {
714                    p
715                }
716            }
717            None => p,
718        };
719        // Outside the domain's box in a coordinate that does not wrap is
720        // outside, certainly -- and it is where a ray cast along the box's
721        // own edge could not decide.
722        let beyond = |value: Scalar, lo: Scalar, hi: Scalar| {
723            value < lo - slack(lo) || value > hi + slack(hi)
724        };
725        let (u_wraps, v_wraps) = if self.transpose {
726            (false, self.period.is_some())
727        } else {
728            (self.period.is_some(), false)
729        };
730        if (!u_wraps && beyond(p.x, self.min.x, self.max.x))
731            || (!v_wraps && beyond(p.y, self.min.y, self.max.y))
732        {
733            return Ok(Some(false));
734        }
735        let q = self.swap(p);
736        let mut total: i64 = 0;
737        for shift in self.shifts(q.x, q.x) {
738            for arc in &self.arcs {
739                match self.crossing(arc, q.x - shift, q.y, 0)? {
740                    Some(weight) => total += weight,
741                    None => return Ok(None),
742                }
743            }
744        }
745        if let Some((pole, winding)) = self.pole {
746            if (pole - q.y).abs() <= slack(pole) {
747                return Ok(None);
748            }
749            if pole > q.y {
750                total += winding;
751            }
752        }
753        // Swapping the coordinates mirrors the plane: an anticlockwise loop
754        // counts clockwise.
755        if self.transpose {
756            total = -total;
757        }
758        // A face whose loops wind clockwise in its parameters (a reversed
759        // face, its holes then anticlockwise) counts -1 inside.
760        Ok(match total {
761            0 => Some(false),
762            1 | -1 => Some(true),
763            _ => None,
764        })
765    }
766
767    /// Signed crossing of the ray `x = c, y > y0` with one monotone arc:
768    /// `+1` where the boundary runs towards `-x`, `-1` towards `+x`.
769    fn crossing(
770        &self,
771        arc: &MonoArc,
772        c: Scalar,
773        y0: Scalar,
774        depth: u32,
775    ) -> Result<Option<i64>, ExactMeasureError> {
776        let (a, b) = (arc.a, arc.b);
777        // Simulation of simplicity: the ray runs at `x = c + epsilon`, so an
778        // end exactly at `c` lies left of it. A ray through a vertex then
779        // crosses once where the boundary passes through and not at all
780        // where it only touches, and an arc along the ray is never crossed.
781        let left = |x: Scalar| x <= c;
782        if left(a.x) == left(b.x) {
783            return Ok(Some(0));
784        }
785        let weight = if b.x < a.x { 1 } else { -1 };
786        let dy = slack(y0);
787        if a.y.min(b.y) > y0 + dy {
788            return Ok(Some(weight));
789        }
790        if a.y.max(b.y) < y0 - dy {
791            return Ok(Some(0));
792        }
793        if depth >= 48 {
794            return Ok(None);
795        }
796        // Monotone: the half that spans `c` holds the crossing.
797        let tm = 0.5 * (arc.t0 + arc.t1);
798        let m = self.swap(self.boundary.pieces[arc.piece].at(tm)?.0);
799        let half = if left(a.x) != left(m.x) {
800            MonoArc {
801                piece: arc.piece,
802                t0: arc.t0,
803                t1: tm,
804                a,
805                b: m,
806            }
807        } else {
808            MonoArc {
809                piece: arc.piece,
810                t0: tm,
811                t1: arc.t1,
812                a: m,
813                b,
814            }
815        };
816        self.crossing(&half, c, y0, depth + 1)
817    }
818}
819
820/// Which parameters of one exact face lie inside it, decided with a
821/// certificate or not at all.
822///
823/// The public face of the crate-private `Domain`: the pcurves of the face's loops, joined
824/// across seams and poles, split into pieces monotone in both parameters
825/// so that a ray crossing is decided from the pieces' ends. `contains`
826/// answers `Some(true)` or `Some(false)` only when certain, and `None` for
827/// a point too close to the boundary to say.
828pub struct FaceDomain<'a> {
829    domain: Domain<'a>,
830}
831
832impl<'a> FaceDomain<'a> {
833    /// The domain of `face`, or `None` when a pcurve family has no monotone
834    /// split here (a B-spline or intrinsic trim).
835    ///
836    /// # Errors
837    ///
838    /// A missing surface, a dangling handle, a boundary that encloses no
839    /// domain in the surface's parameters, or an evaluation failure.
840    pub fn new(
841        brep: &'a ExactBRep,
842        face: axiolid_topology::FaceId,
843        tolerance: axiolid_core::Tolerance,
844    ) -> Result<Option<Self>, ExactMeasureError> {
845        let topology = brep.topology();
846        let record = topology
847            .faces()
848            .get(face.index())
849            .ok_or(ExactMeasureError::DanglingReference)?;
850        let surface = record
851            .surface
852            .and_then(|id| brep.surfaces().get(id.index()))
853            .ok_or(ExactMeasureError::MissingSurface)?;
854        let linear = tolerance.linear().max(1e-12);
855        Ok(Domain::new(brep, record, surface, linear)?.map(|domain| Self { domain }))
856    }
857
858    /// Whether the parameters `at` lie in the face: `None` when too close to
859    /// its boundary to decide.
860    ///
861    /// # Errors
862    ///
863    /// A pcurve that cannot be evaluated where the decision needs it.
864    pub fn contains(&self, at: Point2) -> Result<Option<bool>, ExactMeasureError> {
865        self.domain.contains(at)
866    }
867
868    /// A box in `(u, v)` holding the whole domain.
869    #[must_use]
870    pub fn bounds(&self) -> (Point2, Point2) {
871        (self.domain.min, self.domain.max)
872    }
873}