Skip to main content

geop_core_geometry/intersection/
coincidence.rs

1//! Coincidence handling around the clipping searches (`curve_curve`,
2//! `curve_surface`), which themselves assume no coincident arcs.
3//!
4//! A *primary* curve (parameter `t`) overlaps another object — a curve, or a
5//! surface — along stretches of `t`. For single-piece rational geometry,
6//! such a stretch can only begin or end where one object's boundary is: an
7//! end of the primary curve lying on the other object, or a point of the
8//! primary curve on the other object's boundary (the other curve's ends; the
9//! surface's four boundary curves). Collecting those *candidates* and
10//! testing the midpoint between each consecutive pair therefore finds every
11//! overlap, without a search that could only ever see an overlap as "too
12//! many solutions":
13//!
14//! - candidates sorted by `t`; ones that could be equal merge (a shared
15//!   vertex seen from both sides is one candidate);
16//! - between consecutive candidates, the primary curve either lies on the
17//!   other object throughout or nowhere in the interior — so one probe at the
18//!   midpoint decides it.
19//!
20//! The midpoint probe is the one place this is a test rather than a proof:
21//! a curve touching the other object exactly at two candidates *and* the
22//! midpoint between them, while leaving it in between, is read as an overlap.
23
24use geop_core_math::{
25    disjoint_set::{DisjointSet, Mergeable},
26    geop_error::GeopResult,
27    scalars::Scalar,
28};
29
30use super::Intersections;
31use crate::nurb_curve::NurbCurve;
32
33/// A point of the primary curve at parameter `t` that lies on the other
34/// object at `partner` (its parameter(s) there).
35#[derive(Clone, Copy, Debug)]
36pub struct Hit<P> {
37    pub t: Scalar,
38    pub partner: P,
39}
40
41/// A stretch `[start.t, end.t]` of the primary curve lying on the other
42/// object.
43#[derive(Clone, Copy, Debug)]
44pub struct Overlap<P> {
45    pub start: Hit<P>,
46    pub end: Hit<P>,
47}
48
49/// Every overlap bounded by `candidates` (see the module doc). `probe(t)`
50/// locates the primary curve's point at a sharp `t` on the other object.
51/// Adjacent overlaps sharing a candidate are joined into one.
52pub(crate) fn find_overlaps<P: Mergeable>(
53    mut candidates: Vec<Hit<P>>,
54    mut probe: impl FnMut(Scalar) -> GeopResult<Option<P>>,
55) -> GeopResult<Vec<Overlap<P>>> {
56    // Sorting is only an order to visit them in: any order of candidates
57    // that could be equal is fine, since those merge.
58    candidates.sort_by(|a, b| a.t.to_f64().total_cmp(&b.t.to_f64()));
59    let mut merged: Vec<Hit<P>> = Vec::with_capacity(candidates.len());
60    for c in candidates {
61        match merged.last_mut() {
62            Some(last) if last.t.could_be_equal(c.t) => {
63                last.t = last.t.union(c.t);
64                last.partner = last.partner.union(&c.partner);
65            }
66            _ => merged.push(c),
67        }
68    }
69
70    let mut overlaps: Vec<Overlap<P>> = Vec::new();
71    let mut joins_previous = false;
72    for pair in merged.windows(2) {
73        let (a, b) = (pair[0], pair[1]);
74        // A free choice strictly between the two candidates.
75        let mid = a.t.upper().add(b.t.lower()).div(Scalar::TWO)?.sharpen();
76        if probe(mid)?.is_some() {
77            match overlaps.last_mut() {
78                Some(last) if joins_previous => last.end = b,
79                _ => overlaps.push(Overlap { start: a, end: b }),
80            }
81            joins_previous = true;
82        } else {
83            joins_previous = false;
84        }
85    }
86    Ok(overlaps)
87}
88
89/// The isolated crossings of the primary `curve` away from `overlaps`:
90/// `search` run on the whole curve if there are none, else on each stretch
91/// between them — cut at the overlaps' sharp *outer* bounds, and only where
92/// definitely longer than a point.
93pub(crate) fn crossings_between<const D: usize, P, R>(
94    curve: &NurbCurve<D>,
95    overlaps: &[Overlap<P>],
96    mut search: impl FnMut(&NurbCurve<D>) -> GeopResult<Vec<R>>,
97) -> GeopResult<Vec<R>> {
98    if overlaps.is_empty() {
99        return search(curve);
100    }
101    let (start, end) = curve.domain();
102    let mut bounds = vec![start];
103    for o in overlaps {
104        bounds.extend([o.start.t.lower(), o.end.t.upper()]);
105    }
106    bounds.push(end);
107    let mut crossings = Vec::new();
108    for gap in bounds.chunks(2) {
109        if gap[0].definitely_less(gap[1]) {
110            let piece = curve.sub_curve(gap[0], gap[1])?;
111            crossings.extend(search(&piece).map_err(|e| {
112                e.with_context(format!(
113                    "on the stretch {:?} of the curve",
114                    (gap[0], gap[1])
115                ))
116            })?);
117        }
118    }
119    Ok(crossings)
120}
121
122/// Points spread evenly over the overlaps, by length: as many as
123/// [`assemble`] has room for under `max_solutions` after the overlaps' ends
124/// and the `crossings` it puts first. `probe` as in [`find_overlaps`]; a
125/// sample it cannot place is skipped.
126///
127/// As many as there is room for, not `max_solutions` cut short: spread for
128/// more than are kept, the ones kept were all at the start of the first
129/// overlap. Three points of a blend's contact 7.6e-7 long, both ends and one
130/// a quarter along, were the three a boolean judged it by, and each was
131/// within a pcurve's width of the edge it continues, so it was never found
132/// to enter the face it lies in. The middle is the point furthest from both.
133pub(crate) fn samples<P>(
134    overlaps: &[Overlap<P>],
135    crossings: usize,
136    max_solutions: usize,
137    mut probe: impl FnMut(Scalar) -> GeopResult<Option<P>>,
138) -> GeopResult<Vec<Hit<P>>> {
139    let count = max_solutions.saturating_sub(2 * overlaps.len() + crossings);
140    let spans: Vec<(Scalar, Scalar)> = overlaps
141        .iter()
142        .map(|o| (o.start.t.upper(), o.end.t.lower()))
143        .collect();
144    let lengths: Vec<f64> = spans
145        .iter()
146        .map(|(a, b)| (b.to_f64() - a.to_f64()).max(0.0))
147        .collect();
148    let total: f64 = lengths.iter().sum();
149    let mut out = Vec::new();
150    for ((lo, hi), len) in spans.into_iter().zip(lengths) {
151        let n = if total > 0.0 {
152            ((count as f64) * len / total).ceil() as usize
153        } else {
154            count
155        };
156        for j in 1..=n {
157            // Free choices inside the overlap.
158            let frac = Scalar::from_ratio(j as i64, (n + 1) as i64)?;
159            let t = Scalar::fraction(lo, hi, frac);
160            if let Some(partner) = probe(t)? {
161                out.push(Hit { t, partner });
162            }
163        }
164    }
165    Ok(out)
166}
167
168/// The wrapper's result, with the old `Intersections` contract:
169/// `Coincident` iff an overlap was found, carrying the overlaps' end points
170/// first (where the objects part — the points that matter for splitting),
171/// then the isolated crossings elsewhere, then the spread `samples`, merged
172/// (`DisjointSet`) and capped at `max_solutions`. Without overlaps it is
173/// `Found` with the first `max_solutions` crossings.
174pub(crate) fn assemble<P: Mergeable>(
175    overlaps: &[Overlap<P>],
176    crossings: Vec<(Scalar, P)>,
177    samples: Vec<Hit<P>>,
178    max_solutions: usize,
179) -> Intersections<(Scalar, P)> {
180    let mut set: DisjointSet<(Scalar, P)> = DisjointSet::new();
181    let ends = overlaps.iter().flat_map(|o| [o.start, o.end]);
182    let points = ends
183        .map(|h| (h.t, h.partner))
184        .chain(crossings)
185        .chain(samples.into_iter().map(|h| (h.t, h.partner)));
186    for p in points {
187        if set.len() >= max_solutions {
188            break;
189        }
190        set.insert(p);
191    }
192    let mut v = set.into_vec();
193    v.truncate(max_solutions);
194    if overlaps.is_empty() {
195        Intersections::Found(v)
196    } else {
197        Intersections::Coincident(v)
198    }
199}