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}