Skip to main content

ogeom_algo/
self_cross.rs

1//! Where a solid's boundary passes through itself.
2//!
3//! A boundary that crosses itself bounds no single region: a point between
4//! the two sheets is inside by one and outside by the other, and which a
5//! probe meets decides what every question about the solid answers. The
6//! faces are meshed, the triangles that cross each other are found by their
7//! boxes, and each crossing is only a guess until it is settled on the
8//! surfaces themselves: a point on both, where they meet at an angle,
9//! inside both faces' trims and clear of their boundaries. A mesh can cross
10//! where the surfaces do not (two walls closer than the chord), and two
11//! faces always meet along the edges they share; neither is reported.
12
13use ogeom_core::{OgeomResult, Tolerances};
14use ogeom_geom::{Surface as _, SurfaceGeometry};
15use ogeom_math::{Aabb, Point, Transform, Vector};
16use ogeom_mesh::Deflection;
17use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
18
19use crate::classify::{Containment, FaceTrim};
20
21/// The smallest sine of the angle two sheets cross at to count: below it
22/// they touch, which is where faces meet along an edge they share or bend
23/// smoothly into one another.
24pub(crate) const CROSSING_SINE: f64 = 1e-2;
25
26/// How many crossings of one pair of faces' meshes are asked of the
27/// surfaces before the pair is let go: a pair whose meshes cross
28/// everywhere without the surfaces doing so meets its walls closer than
29/// the chord, and asking every triangle would cost without telling more.
30const ASKED_PER_PAIR: usize = 16;
31
32/// Below this many triangles a node of the box tree holds them as they are.
33const LEAF: usize = 8;
34
35/// One face of the solid the crossing test reads.
36struct Sheet<'m> {
37    face: &'m Shape,
38    surface: &'m SurfaceGeometry,
39    placement: Transform,
40    /// How far from its trim a point counts as on it: the widest of the
41    /// face's tolerance, its edges' and its vertices'. Edges meet at a
42    /// vertex only within the vertex's tolerance, so near a loose vertex
43    /// the drawn trim can stand that far from where the face ends.
44    reach: f64,
45    trim: std::sync::OnceLock<Option<FaceTrim<'m>>>,
46}
47
48/// One triangle of a face's mesh.
49struct Facet {
50    sheet: usize,
51    corners: [Point; 3],
52    chart: [(f64, f64); 3],
53    bound: Aabb,
54}
55
56/// Two crossing triangles, by index, and a point where their meshes cross.
57type Guess = (usize, usize, Point);
58
59/// A point where a solid's boundary passes through itself.
60#[derive(Debug, Clone)]
61pub struct SelfCrossing {
62    /// The face crossing.
63    pub face: Shape,
64    /// The face it crosses: the same one where a face folds through itself.
65    pub other: Shape,
66    /// Where, on both surfaces.
67    pub at: Point,
68}
69
70/// The pairs of faces of `solid` that pass through each other, a face
71/// folding through itself among them, one point for each: what
72/// [`check`](crate::check()) reports as a boundary crossing itself.
73///
74/// # Errors
75///
76/// [`OgeomError::Cancelled`](ogeom_core::OgeomError::Cancelled) and
77/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) only.
78pub fn self_crossings(
79    model: &Model,
80    solid: &Shape,
81    tol: Tolerances,
82) -> OgeomResult<Vec<SelfCrossing>> {
83    crossings(model, solid, &ogeom_mesh::FaceMeshCache::default(), tol)
84}
85
86/// The pairs of faces of `solid` (a face with itself among them) that
87/// cross each other, one point for each, in the order of the faces.
88///
89/// A face that will not mesh is not asked: what the test cannot draw it
90/// cannot settle, and names nothing on a guess.
91///
92/// # Errors
93///
94/// [`OgeomError::Cancelled`](ogeom_core::OgeomError::Cancelled) and
95/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) only.
96pub(crate) fn crossings(
97    model: &Model,
98    solid: &Shape,
99    kept: &ogeom_mesh::FaceMeshCache,
100    tol: Tolerances,
101) -> OgeomResult<Vec<SelfCrossing>> {
102    let faces = explore_unique(model, solid, ShapeType::Face)?;
103    let meshed = ogeom_core::parallel::map_ordered(&faces, |_, face| {
104        ogeom_mesh::triangulate_face_kept(model, face, Deflection::default(), kept, tol).ok()
105    });
106    let mut sheets = Vec::with_capacity(faces.len());
107    let mut facets = Vec::new();
108    for (face, mesh) in faces.iter().zip(meshed) {
109        let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
110            continue;
111        };
112        let Some(surface) = model.geometry().surface(data.surface) else {
113            continue;
114        };
115        let Some(mesh) = mesh.filter(|m| m.parameters.len() == m.positions.len()) else {
116            continue;
117        };
118        let mut reach = data.tolerance.get().max(tol.confusion());
119        for kind in [ShapeType::Edge, ShapeType::Vertex] {
120            for bound in explore_unique(model, face, kind)? {
121                if let Some(t) = model.node(&bound).and_then(|n| n.data().tolerance()) {
122                    reach = reach.max(t.get());
123                }
124            }
125        }
126        let sheet = sheets.len();
127        sheets.push(Sheet {
128            face,
129            surface,
130            placement: face.transform(model.datums())?,
131            reach,
132            trim: std::sync::OnceLock::new(),
133        });
134        for triangle in &mesh.triangles {
135            let corners = triangle.map(|i| mesh.positions[i as usize]);
136            facets.push(Facet {
137                sheet,
138                corners,
139                chart: triangle.map(|i| mesh.parameters[i as usize]),
140                bound: Aabb::of_points(&corners).expanded(tol.confusion()),
141            });
142        }
143    }
144    if facets.len() < 2 {
145        return Ok(Vec::new());
146    }
147
148    // The crossing triangles, a few per pair of faces, found in runs of
149    // triangles so the search shares out.
150    let tree = BoxTree::new(&facets);
151    let runs: Vec<(usize, usize)> = (0..facets.len())
152        .step_by(512)
153        .map(|start| (start, (start + 512).min(facets.len())))
154        .collect();
155    let found = ogeom_core::parallel::map_ordered(&runs, |_, &(start, end)| {
156        let mut out: Vec<(usize, usize, usize, usize, Point)> = Vec::new();
157        let mut near = Vec::new();
158        for i in start..end {
159            if i % 64 == 0 {
160                ogeom_core::progress::checkpoint()?;
161            }
162            tree.meeting(&facets, &facets[i].bound, &mut near);
163            for &j in &near {
164                if j <= i || share_a_corner(&facets[i], &facets[j]) {
165                    continue;
166                }
167                if let Some(at) = triangles_cross(&facets[i].corners, &facets[j].corners) {
168                    let (a, b) = (facets[i].sheet, facets[j].sheet);
169                    if a <= b {
170                        out.push((a, b, i, j, at));
171                    } else {
172                        out.push((b, a, j, i, at));
173                    }
174                }
175            }
176        }
177        Ok(out)
178    });
179    let mut guesses = Vec::new();
180    for run in found {
181        guesses.extend(run?);
182    }
183    guesses.sort_by_key(|x| (x.0, x.1, x.2, x.3));
184    let mut pairs: Vec<((usize, usize), Vec<Guess>)> = Vec::new();
185    for (a, b, i, j, at) in guesses {
186        match pairs.last_mut() {
187            Some((pair, all)) if *pair == (a, b) => all.push((i, j, at)),
188            _ => pairs.push(((a, b), vec![(i, j, at)])),
189        }
190    }
191    for ((a, b), all) in &mut pairs {
192        if a == b {
193            // A face's mesh crosses itself where the face folds back on
194            // itself sharply as well as where it passes through itself,
195            // and there the two triangles stand side by side in the chart
196            // and the surfaces meet only where they are one point. The
197            // triangles farthest apart in the chart are asked.
198            let surface = sheets[*a].surface;
199            let mut apart: Vec<(f64, Guess)> = all
200                .iter()
201                .map(|&(i, j, at)| (chart_apart(surface, &facets[i], &facets[j]), (i, j, at)))
202                .collect();
203            apart.sort_by(|x, y| y.0.total_cmp(&x.0));
204            *all = apart
205                .into_iter()
206                .take(ASKED_PER_PAIR)
207                .map(|(_, guess)| guess)
208                .collect();
209        } else if all.len() > ASKED_PER_PAIR {
210            // Spread along the crossing, rather than crowded at one end.
211            *all = (0..ASKED_PER_PAIR)
212                .map(|k| all[k * all.len() / ASKED_PER_PAIR])
213                .collect();
214        }
215    }
216
217    let settled = ogeom_core::parallel::map_ordered(&pairs, |_, ((a, b), asked)| {
218        for &(i, j, guess) in asked {
219            ogeom_core::progress::checkpoint()?;
220            if let Some(at) = settle(model, &sheets, &facets[i], &facets[j], guess, tol)? {
221                return Ok(Some(SelfCrossing {
222                    face: sheets[*a].face.clone(),
223                    other: sheets[*b].face.clone(),
224                    at,
225                }));
226            }
227        }
228        Ok(None)
229    });
230    let mut out = Vec::new();
231    for one in settled {
232        out.extend(one?);
233    }
234    Ok(out)
235}
236
237/// Whether two triangles have a corner at the same place: neighbours in one
238/// face's mesh, or across an edge two faces share, whose boundary points
239/// both draw from the edge's own curve.
240fn share_a_corner(p: &Facet, q: &Facet) -> bool {
241    p.corners.iter().any(|c| q.corners.contains(c))
242}
243
244/// A point on the segment along which two triangles cross, where each
245/// passes through the other's plane; `None` where they only touch, lie in
246/// one plane, or miss.
247fn triangles_cross(p: &[Point; 3], q: &[Point; 3]) -> Option<Point> {
248    let plane = |t: &[Point; 3]| (t[1] - t[0]).cross(t[2] - t[0]);
249    let (np, nq) = (plane(p), plane(q));
250    let side = |n: Vector, origin: Point, t: &[Point; 3]| t.map(|c| n.dot(c - origin));
251    let dp = side(nq, q[0], p);
252    let dq = side(np, p[0], q);
253    let straddles = |d: [f64; 3]| d.iter().any(|&x| x > 0.0) && d.iter().any(|&x| x < 0.0);
254    if !straddles(dp) || !straddles(dq) {
255        return None;
256    }
257    let line = np.cross(nq);
258    if line.dot(line) <= f64::MIN_POSITIVE {
259        return None;
260    }
261    // Where each triangle's edges pass through the other's plane, as an
262    // interval along the line the two planes share.
263    let interval = |t: &[Point; 3], d: [f64; 3]| -> Option<(f64, f64)> {
264        let mut along: Vec<f64> = Vec::with_capacity(3);
265        for k in 0..3 {
266            let (a, b) = (k, (k + 1) % 3);
267            if (d[a] > 0.0) != (d[b] > 0.0) && d[a] != d[b] {
268                let s = d[a] / (d[a] - d[b]);
269                let at = t[a] + (t[b] - t[a]) * s;
270                along.push(line.dot(at - Point::ORIGIN));
271            }
272        }
273        let lo = along.iter().copied().fold(f64::INFINITY, f64::min);
274        let hi = along.iter().copied().fold(f64::NEG_INFINITY, f64::max);
275        (lo <= hi).then_some((lo, hi))
276    };
277    let (pl, ph) = interval(p, dp)?;
278    let (ql, qh) = interval(q, dq)?;
279    let (lo, hi) = (pl.max(ql), ph.min(qh));
280    if lo >= hi {
281        return None;
282    }
283    // The middle of the shared stretch, on the line: a point of the first
284    // triangle's crossing segment.
285    let middle = 0.5 * (lo + hi);
286    let mut ends: Vec<Point> = Vec::with_capacity(2);
287    for k in 0..3 {
288        let (a, b) = (k, (k + 1) % 3);
289        if (dp[a] > 0.0) != (dp[b] > 0.0) && dp[a] != dp[b] {
290            let s = dp[a] / (dp[a] - dp[b]);
291            ends.push(p[a] + (p[b] - p[a]) * s);
292        }
293    }
294    let [from, to] = [*ends.first()?, *ends.last()?];
295    let (sf, st) = (line.dot(from - Point::ORIGIN), line.dot(to - Point::ORIGIN));
296    if (st - sf).abs() <= f64::MIN_POSITIVE {
297        return Some(from);
298    }
299    Some(from + (to - from) * ((middle - sf) / (st - sf)))
300}
301
302/// How far apart two triangles of one face stand in its chart, as a
303/// fraction of the chart's extent along the direction they are farthest
304/// apart in, the short way round a direction that repeats.
305fn chart_apart(surface: &SurfaceGeometry, p: &Facet, q: &Facet) -> f64 {
306    let middle = |f: &Facet| {
307        let [a, b, c] = f.chart;
308        ((a.0 + b.0 + c.0) / 3.0, (a.1 + b.1 + c.1) / 3.0)
309    };
310    let ((u0, u1), (v0, v1)) = surface.domain();
311    let along = |d: f64, span: f64, periodic: bool| {
312        if !(span.is_finite() && span > 0.0) {
313            return d.abs();
314        }
315        let d = if periodic {
316            let wrapped = d.rem_euclid(span);
317            wrapped.min(span - wrapped)
318        } else {
319            d.abs()
320        };
321        d / span
322    };
323    let (pm, qm) = (middle(p), middle(q));
324    along(pm.0 - qm.0, u1 - u0, surface.is_periodic_u()).max(along(
325        pm.1 - qm.1,
326        v1 - v0,
327        surface.is_periodic_v(),
328    ))
329}
330
331/// The chart point of `facet` under `at`, by the barycentric weights of
332/// its foot in the triangle's plane.
333fn chart_under(facet: &Facet, at: Point) -> Option<(f64, f64)> {
334    let [a, b, c] = facet.corners;
335    let (e1, e2, w) = (b - a, c - a, at - a);
336    let (d11, d12, d22) = (e1.dot(e1), e1.dot(e2), e2.dot(e2));
337    let (d1w, d2w) = (e1.dot(w), e2.dot(w));
338    let det = d11.mul_add(d22, -d12 * d12);
339    if det.abs() <= f64::MIN_POSITIVE {
340        return None;
341    }
342    let s = d22.mul_add(d1w, -d12 * d2w) / det;
343    let t = d11.mul_add(d2w, -d12 * d1w) / det;
344    let r = 1.0 - s - t;
345    let [pa, pb, pc] = facet.chart;
346    Some((
347        r.mul_add(pa.0, s.mul_add(pb.0, t * pc.0)),
348        r.mul_add(pa.1, s.mul_add(pb.1, t * pc.1)),
349    ))
350}
351
352/// A sheet's point, its two chart tangents and its unit normal, placed.
353fn placed_at(
354    sheet: &Sheet<'_>,
355    at: (f64, f64),
356    tol: Tolerances,
357) -> Option<(Point, Vector, Vector, Vector)> {
358    let (p, du, dv) = sheet.surface.point_d1_at(at.0, at.1, tol).ok()?;
359    let (du, dv) = (
360        sheet.placement.apply_vector(du),
361        sheet.placement.apply_vector(dv),
362    );
363    let n = du.cross(dv);
364    let length = n.magnitude();
365    if length <= f64::MIN_POSITIVE || !length.is_finite() {
366        return None;
367    }
368    Some((sheet.placement.apply(p), du, dv, n / length))
369}
370
371/// Hold a chart point inside the surface's domain along its directions
372/// that do not repeat.
373fn held(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
374    let ((u0, u1), (v0, v1)) = surface.domain();
375    let u = if surface.is_periodic_u() || u0 > u1 {
376        u
377    } else {
378        u.clamp(u0, u1)
379    };
380    let v = if surface.is_periodic_v() || v0 > v1 {
381        v
382    } else {
383        v.clamp(v0, v1)
384    };
385    (u, v)
386}
387
388/// The point where two crossing triangles' surfaces really cross, near
389/// `guess`; `None` where they do not cross there.
390///
391/// Newton's method from the triangles' chart points onto a point of both
392/// surfaces, held from sliding along the curve they meet in. The point
393/// counts where the surfaces cross at an angle there, it lies near the
394/// triangles, and it is inside both faces, clear of their boundaries by
395/// their tolerances.
396fn settle<'m>(
397    model: &'m Model,
398    sheets: &[Sheet<'m>],
399    p: &Facet,
400    q: &Facet,
401    guess: Point,
402    tol: Tolerances,
403) -> OgeomResult<Option<Point>> {
404    let (a, b) = (&sheets[p.sheet], &sheets[q.sheet]);
405    let (Some(start_a), Some(start_b)) = (chart_under(p, guess), chart_under(q, guess)) else {
406        return Ok(None);
407    };
408    let (Some((_, _, _, na)), Some((_, _, _, nb))) =
409        (placed_at(a, start_a, tol), placed_at(b, start_b, tol))
410    else {
411        return Ok(None);
412    };
413    let along = na.cross(nb);
414    if along.magnitude() < CROSSING_SINE {
415        return Ok(None);
416    }
417    let along = along / along.magnitude();
418    let system = |x: &[f64; 4]| {
419        let (ua, va) = held(a.surface, x[0], x[1]);
420        let (ub, vb) = held(b.surface, x[2], x[3]);
421        let (Some((pa, au, av, _)), Some((pb, bu, bv, _))) =
422            (placed_at(a, (ua, va), tol), placed_at(b, (ub, vb), tol))
423        else {
424            return ([f64::INFINITY; 4], [[0.0; 4]; 4]);
425        };
426        let gap = pa - pb;
427        (
428            [gap.x, gap.y, gap.z, (pa - guess).dot(along)],
429            [
430                [au.x, av.x, -bu.x, -bv.x],
431                [au.y, av.y, -bu.y, -bv.y],
432                [au.z, av.z, -bu.z, -bv.z],
433                [au.dot(along), av.dot(along), 0.0, 0.0],
434            ],
435        )
436    };
437    let criteria = ogeom_math::solve::Criteria {
438        residual: tol.confusion() * 0.01,
439        step: tol.parametric(),
440        max_iterations: 40,
441    };
442    let start = [start_a.0, start_a.1, start_b.0, start_b.1];
443    let Ok((x, residual, ..)) = ogeom_math::solve::newton_system_fixed(system, start, criteria)
444    else {
445        return Ok(None);
446    };
447    if residual > tol.confusion() {
448        return Ok(None);
449    }
450    let (on_a, on_b) = (held(a.surface, x[0], x[1]), held(b.surface, x[2], x[3]));
451    let (Some((at, _, _, na)), Some((_, _, _, nb))) =
452        (placed_at(a, on_a, tol), placed_at(b, on_b, tol))
453    else {
454        return Ok(None);
455    };
456    if na.cross(nb).magnitude() < CROSSING_SINE {
457        return Ok(None);
458    }
459    let size = |f: &Facet| {
460        let [x, y, z] = f.corners;
461        x.distance(y).max(y.distance(z)).max(z.distance(x))
462    };
463    if at.distance(guess) > 2.0 * size(p).max(size(q)) {
464        return Ok(None);
465    }
466    for (sheet, chart) in [(a, on_a), (b, on_b)] {
467        let Some(trim) = trim_of(model, sheet, tol) else {
468            return Ok(None);
469        };
470        if trim.holds(model, chart, sheet.reach, tol)? != Containment::In {
471            return Ok(None);
472        }
473    }
474    Ok(Some(at))
475}
476
477/// A sheet's trim, drawn the first time a crossing asks about it; `None`
478/// for a face whose rings will not draw.
479fn trim_of<'s, 'm>(
480    model: &'m Model,
481    sheet: &'s Sheet<'m>,
482    tol: Tolerances,
483) -> Option<&'s FaceTrim<'m>> {
484    sheet
485        .trim
486        .get_or_init(|| {
487            // Drawn at the chord the inside-out probe holds faces to.
488            let chord = Deflection {
489                chord: tol.confusion() * 1e4,
490                ..Deflection::default()
491            };
492            FaceTrim::of(model, sheet.face, chord, tol).ok()
493        })
494        .as_ref()
495}
496
497/// The facets' boxes held in a tree, so the boxes meeting one are found by
498/// descending where it reaches.
499struct BoxTree {
500    nodes: Vec<Node>,
501    order: Vec<usize>,
502}
503
504struct Node {
505    bound: Aabb,
506    run: (usize, usize),
507    children: Option<(usize, usize)>,
508}
509
510impl BoxTree {
511    fn new(facets: &[Facet]) -> Self {
512        let mut order: Vec<usize> = (0..facets.len()).collect();
513        let mut tree = Self {
514            nodes: Vec::new(),
515            order: Vec::new(),
516        };
517        let len = order.len();
518        if len > 0 {
519            tree.build(facets, &mut order, 0, len);
520        }
521        tree.order = order;
522        tree
523    }
524
525    fn build(&mut self, facets: &[Facet], order: &mut [usize], start: usize, end: usize) -> usize {
526        let bound = order[start..end]
527            .iter()
528            .fold(Aabb::EMPTY, |acc, &i| acc.union(&facets[i].bound));
529        let at = self.nodes.len();
530        self.nodes.push(Node {
531            bound,
532            run: (start, end),
533            children: None,
534        });
535        if end - start <= LEAF {
536            return at;
537        }
538        let size = bound.size();
539        let axis = if size.x >= size.y && size.x >= size.z {
540            0
541        } else if size.y >= size.z {
542            1
543        } else {
544            2
545        };
546        let centre = |i: usize| -> f64 {
547            let c = facets[i].bound.centre().unwrap_or(Point::ORIGIN);
548            [c.x, c.y, c.z][axis]
549        };
550        let middle = start + (end - start) / 2;
551        order[start..end]
552            .select_nth_unstable_by(middle - start, |&p, &q| centre(p).total_cmp(&centre(q)));
553        let left = self.build(facets, order, start, middle);
554        let right = self.build(facets, order, middle, end);
555        self.nodes[at].children = Some((left, right));
556        at
557    }
558
559    /// The facets whose boxes meet `probe`, ascending, into `out`.
560    fn meeting(&self, facets: &[Facet], probe: &Aabb, out: &mut Vec<usize>) {
561        out.clear();
562        if self.nodes.is_empty() {
563            return;
564        }
565        let mut stack = vec![0_usize];
566        while let Some(at) = stack.pop() {
567            let node = &self.nodes[at];
568            if !node.bound.intersects(probe) {
569                continue;
570            }
571            match node.children {
572                Some((left, right)) => {
573                    stack.push(left);
574                    stack.push(right);
575                }
576                None => {
577                    for &i in &self.order[node.run.0..node.run.1] {
578                        if facets[i].bound.intersects(probe) {
579                            out.push(i);
580                        }
581                    }
582                }
583            }
584        }
585        out.sort_unstable();
586    }
587}