Skip to main content

ogeom_algo/
chart_cross.rs

1//! Where a face's boundary crosses itself in its surface's chart.
2//!
3//! A face is the region of its surface its wires enclose, read in the
4//! surface's chart, the material on the left of every edge as the face
5//! walks it. Where two of its edges' pcurves cross, the boundary walks a
6//! figure eight: the region on one side of the crossing is wound the other
7//! way round from the region on the other, and the face encloses no single
8//! region. The mesher, the classifier and a boolean's arrangement all read
9//! the face in its chart and each make something different of it.
10//!
11//! Two edges meet at the vertex they share, and fitted pcurves wander about
12//! it by the tolerance their edges state: a crossing that lands within that
13//! reach of a vertex of each edge is where the two meet, not a crossing.
14//! Nor is a crossing of two consecutive edges whose loop, from it to the
15//! vertex they meet at, the edges' own tolerances account for. Each
16//! pcurve's image stands within its edge's tolerance of the edge's curve,
17//! so where the images cross the curves stand within the two tolerances
18//! of each other, and toward the vertex, where the curves meet, they draw
19//! together: the images of two curves that do not cross close a loop no
20//! wider than twice the two tolerances. A sharp corner's fitted pcurves
21//! close such a sliver as readily as they miss each other. A touch, where
22//! the two pcurves meet without crossing, is not reported either; nor is
23//! anything about a degenerate edge, which stands for one point however
24//! it runs in the chart.
25//!
26//! Each pcurve is drawn as a polyline, the walk carried across a closed
27//! surface's seam so that consecutive edges meet in one copy of the chart,
28//! and the segments of two edges that cross seed Newton on the pcurves
29//! themselves. On a closed surface each pair is also asked a period apart
30//! either way. Two pcurves that cross within the sag of their polylines
31//! may go unseen; what is reported is a crossing of the pcurves
32//! themselves, found to rounding.
33
34use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
35use ogeom_geom::{Curve2d as _, PlanarCurve, Surface as _};
36use ogeom_math::{Point, Point2, Vector2, solve};
37use ogeom_topo::{EdgeRepr, Model, Orientation, Shape, TShapeId};
38
39/// How many segments a curved pcurve is drawn with. A straight one is
40/// drawn as the one segment it is.
41const SEGMENTS: u32 = 32;
42
43/// A point where a face's boundary crosses itself in its surface's chart.
44#[derive(Debug, Clone, PartialEq)]
45pub struct ChartCrossing {
46    /// The two edges crossing, each as (wire, edge): the index of the wire
47    /// among the face's wires as stored, and of the edge in that wire's
48    /// order. The first comes first in that order.
49    pub edges: [(usize, usize); 2],
50    /// Where on each edge's pcurve, in that pcurve's own parameter.
51    pub parameters: [f64; 2],
52    /// Whole periods of the chart the second edge's pcurve is moved by,
53    /// against the first's, to meet it there, beyond those the walk
54    /// carries it by across the seam. Zero on a surface that does not
55    /// close.
56    pub apart: [i64; 2],
57    /// Where, on the surface, placed as the face is.
58    pub at: Point,
59}
60
61/// One edge as the face walks it.
62struct Walked<'m> {
63    wire: usize,
64    edge: usize,
65    node: TShapeId,
66    pcurve: &'m PlanarCurve,
67    range: (f64, f64),
68    /// Added to the pcurve's points to carry it to where the walk has got
69    /// to across the seam.
70    shift: Vector2,
71    /// The edge's vertices, placed, each with its tolerance.
72    vertices: Vec<(Point, f64)>,
73    tolerance: f64,
74    /// Drawn and asked: not a degenerate edge.
75    drawn: bool,
76    /// Walked from its range's end to its start.
77    reversed: bool,
78    /// The vertices it is walked from and to.
79    ends: Option<(TShapeId, TShapeId)>,
80    /// The polyline: parameters and points, shifted.
81    samples: Vec<(f64, Point2)>,
82}
83
84/// Every place where the boundary of `face` crosses itself in its
85/// surface's chart, as the module documentation describes, in the order of
86/// the edges crossing. Nothing where an edge of the face has no pcurve on
87/// its surface: what cannot be drawn is not read.
88///
89/// # Errors
90///
91/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the face
92/// or its geometry is not in the model.
93pub fn chart_crossings(
94    model: &Model,
95    face: &Shape,
96    tol: Tolerances,
97) -> OgeomResult<Vec<ChartCrossing>> {
98    let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
99        ogeom_bail!(Dangling, "face is not in this model");
100    };
101    let Some(surface) = model.geometry().surface(data.surface) else {
102        ogeom_bail!(Dangling, "face names a surface not in this model");
103    };
104    let ((u0, u1), (v0, v1)) = surface.domain();
105    let periods = [
106        (surface.is_periodic_u() || surface.is_closed_u(tol))
107            .then_some(u1 - u0)
108            .filter(|p| *p > 0.0 && p.is_finite()),
109        (surface.is_periodic_v() || surface.is_closed_v(tol))
110            .then_some(v1 - v0)
111            .filter(|p| *p > 0.0 && p.is_finite()),
112    ];
113    let placement = face.transform(model.datums())?;
114    let stored = face.oriented(Orientation::Forward);
115
116    let mut walked: Vec<Walked<'_>> = Vec::new();
117    for (w, wire) in model.ordered_children_of(&stored)?.into_iter().enumerate() {
118        let mut last: Option<Point2> = None;
119        for (i, edge) in model.ordered_children_of(&wire)?.into_iter().enumerate() {
120            let Some(edge_data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
121                return Ok(Vec::new());
122            };
123            let reversed = edge.orientation() == Orientation::Reversed;
124            let (id, range) = match edge_data.pcurve_for(data.surface, edge.location()) {
125                Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
126                Some(EdgeRepr::Seam {
127                    forward,
128                    reversed: back,
129                    range,
130                    ..
131                }) => (if reversed { *back } else { *forward }, *range),
132                _ => return Ok(Vec::new()),
133            };
134            let Some(pcurve) = model.geometry().pcurve(id) else {
135                ogeom_bail!(Dangling, "an edge names geometry not in this model");
136            };
137            let (first, end) = {
138                let (a, b) = (
139                    pcurve.point_at(range.0, tol)?,
140                    pcurve.point_at(range.1, tol)?,
141                );
142                if reversed { (b, a) } else { (a, b) }
143            };
144            // Whole periods that bring this edge's start to where the last
145            // one ended.
146            let mut shift = Vector2::new(0.0, 0.0);
147            if let Some(last) = last {
148                for (axis, period) in periods.iter().enumerate() {
149                    let Some(period) = period else { continue };
150                    let (to, from) = if axis == 0 {
151                        (last.x, first.x)
152                    } else {
153                        (last.y, first.y)
154                    };
155                    let whole = ((to - from) / period).round() * period;
156                    if axis == 0 {
157                        shift.x = whole;
158                    } else {
159                        shift.y = whole;
160                    }
161                }
162            }
163            last = Some(end + shift);
164            let mut vertices = Vec::with_capacity(2);
165            for v in model.children_of(&edge)? {
166                let Some(vd) = model.node(&v).and_then(|n| n.data().as_vertex()) else {
167                    continue;
168                };
169                vertices.push((
170                    v.transform(model.datums())?.apply(vd.point),
171                    vd.tolerance.get(),
172                ));
173            }
174            let pieces = if matches!(pcurve, PlanarCurve::Line(_)) {
175                1
176            } else {
177                SEGMENTS
178            };
179            let samples = if edge_data.degenerate {
180                Vec::new()
181            } else {
182                (0..=pieces)
183                    .map(|k| {
184                        let t = range.0 + (range.1 - range.0) * f64::from(k) / f64::from(pieces);
185                        pcurve.point_at(t, tol).map(|p| (t, p + shift))
186                    })
187                    .collect::<OgeomResult<_>>()?
188            };
189            let ends =
190                crate::build::edge_vertices(model, &edge)?.map(|(a, b)| (a.node(), b.node()));
191            walked.push(Walked {
192                wire: w,
193                edge: i,
194                node: edge.node(),
195                reversed,
196                ends,
197                pcurve,
198                range,
199                shift,
200                vertices,
201                tolerance: edge_data.tolerance.get(),
202                drawn: !edge_data.degenerate,
203                samples,
204            });
205        }
206    }
207
208    // The period moves each pair is asked at.
209    let mut moves: Vec<[i64; 2]> = vec![[0, 0]];
210    for (axis, period) in periods.iter().enumerate() {
211        if period.is_some() {
212            let mut more = Vec::new();
213            for m in &moves {
214                for k in [-1, 1] {
215                    let mut moved = *m;
216                    moved[axis] = k;
217                    more.push(moved);
218                }
219            }
220            moves.extend(more);
221        }
222    }
223    let offset = |m: [i64; 2]| {
224        #[allow(clippy::cast_precision_loss, reason = "one period either way")]
225        Vector2::new(
226            periods[0].map_or(0.0, |p| p * m[0] as f64),
227            periods[1].map_or(0.0, |p| p * m[1] as f64),
228        )
229    };
230    let boxes: Vec<(Point2, Point2)> = walked.iter().map(|e| bounds(&e.samples)).collect();
231
232    let mut found: Vec<ChartCrossing> = Vec::new();
233    for a in 0..walked.len() {
234        if !walked[a].drawn {
235            continue;
236        }
237        for b in a + 1..walked.len() {
238            if !walked[b].drawn || walked[a].node == walked[b].node {
239                continue;
240            }
241            for &m in &moves {
242                let by = offset(m);
243                let (lo, hi) = (boxes[b].0 + by, boxes[b].1 + by);
244                if lo.x > boxes[a].1.x
245                    || hi.x < boxes[a].0.x
246                    || lo.y > boxes[a].1.y
247                    || hi.y < boxes[a].0.y
248                {
249                    continue;
250                }
251                found.extend(pair_crossings(
252                    &walked[a], &walked[b], by, m, surface, &placement, tol,
253                )?);
254            }
255        }
256    }
257    Ok(found)
258}
259
260/// The box of a polyline's points.
261fn bounds(samples: &[(f64, Point2)]) -> (Point2, Point2) {
262    let mut lo = Point2::new(f64::INFINITY, f64::INFINITY);
263    let mut hi = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
264    for (_, p) in samples {
265        lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
266        hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
267    }
268    (lo, hi)
269}
270
271/// The crossings of two walked edges, the second moved by `by`, that are
272/// no meeting at a vertex and no touch, each once, in order along the
273/// first.
274fn pair_crossings(
275    a: &Walked<'_>,
276    b: &Walked<'_>,
277    by: Vector2,
278    apart: [i64; 2],
279    surface: &ogeom_geom::SurfaceGeometry,
280    placement: &ogeom_math::Transform,
281    tol: Tolerances,
282) -> OgeomResult<Vec<ChartCrossing>> {
283    let mut found: Vec<(Point2, ChartCrossing)> = Vec::new();
284    for i in 1..a.samples.len() {
285        let (pa, qa) = (a.samples[i - 1], a.samples[i]);
286        for j in 1..b.samples.len() {
287            let (pb, qb) = (b.samples[j - 1], b.samples[j]);
288            let Some((sa, sb)) = segments_cross(pa.1, qa.1, pb.1 + by, qb.1 + by) else {
289                continue;
290            };
291            let seed = [pa.0 + (qa.0 - pa.0) * sa, pb.0 + (qb.0 - pb.0) * sb];
292            let Some([ta, tb]) = polish(a, b, by, seed, tol) else {
293                continue;
294            };
295            let uv = a.pcurve.point_at(ta, tol)?;
296            // Two segments either side of a sample seed the same crossing.
297            if found
298                .iter()
299                .any(|(at, _)| at.distance(uv) <= tol.confusion())
300            {
301                continue;
302            }
303            let (Ok((point, du, dv)), Ok(da), Ok(db)) = (
304                surface.point_d1_at(uv.x, uv.y, tol),
305                a.pcurve.d1_at(ta, tol),
306                b.pcurve.d1_at(tb, tol),
307            ) else {
308                continue;
309            };
310            let at = placement.apply(point);
311            // A touch is no crossing: the two must meet at an angle on the
312            // surface.
313            let (ta3, tb3) = (du * da.x + dv * da.y, du * db.x + dv * db.y);
314            let (la, lb) = (ta3.magnitude(), tb3.magnitude());
315            if la <= 0.0
316                || lb <= 0.0
317                || ta3.cross(tb3).magnitude() <= crate::self_cross::CROSSING_SINE * la * lb
318            {
319                continue;
320            }
321            // Where the two meet at a vertex of each, they meet.
322            let widest = a.tolerance.max(b.tolerance).max(tol.approximation());
323            let near = |e: &Walked<'_>| {
324                e.vertices
325                    .iter()
326                    .any(|(v, t)| at.distance(*v) <= crate::check::CHART_GAP_SLACK * widest.max(*t))
327            };
328            if near(a) && near(b) {
329                continue;
330            }
331            if apart == [0, 0] && sliver(a, ta, b, tb, surface, tol)? {
332                continue;
333            }
334            found.push((
335                uv,
336                ChartCrossing {
337                    edges: [(a.wire, a.edge), (b.wire, b.edge)],
338                    parameters: [ta, tb],
339                    apart,
340                    at,
341                },
342            ));
343        }
344    }
345    Ok(found.into_iter().map(|(_, crossing)| crossing).collect())
346}
347
348/// Whether `a` and `b`, crossing at the given parameters of their
349/// pcurves, are consecutive in their wire and run within twice the sum of
350/// their tolerances of each other, on the surface, from the crossing to
351/// the vertex they meet at (see the module documentation).
352fn sliver(
353    a: &Walked<'_>,
354    ta: f64,
355    b: &Walked<'_>,
356    tb: f64,
357    surface: &ogeom_geom::SurfaceGeometry,
358    tol: Tolerances,
359) -> OgeomResult<bool> {
360    const SAMPLES: u32 = 32;
361    let (Some(ea), Some(eb)) = (a.ends, b.ends) else {
362        return Ok(false);
363    };
364    // The edge walked into the vertex they meet at, and the one walked
365    // out of it.
366    let ((into, t_into), (out, t_out)) = if ea.1 == eb.0 {
367        ((a, ta), (b, tb))
368    } else if eb.1 == ea.0 {
369        ((b, tb), (a, ta))
370    } else {
371        return Ok(false);
372    };
373    if into.wire != out.wire {
374        return Ok(false);
375    }
376    let walked_end = |e: &Walked<'_>, last: bool| {
377        if last != e.reversed {
378            e.range.1
379        } else {
380            e.range.0
381        }
382    };
383    let stretch = |e: &Walked<'_>, from: f64, to: f64| -> OgeomResult<Vec<Point>> {
384        (0..=SAMPLES)
385            .map(|k| {
386                let uv = e
387                    .pcurve
388                    .point_at(from + (to - from) * f64::from(k) / f64::from(SAMPLES), tol)?;
389                surface.point_at(uv.x, uv.y, tol)
390            })
391            .collect()
392    };
393    let first = stretch(into, t_into, walked_end(into, true))?;
394    let second = stretch(out, walked_end(out, false), t_out)?;
395    let width = 2.0 * (into.tolerance + out.tolerance);
396    let within = |points: &[Point], line: &[Point]| {
397        points.iter().all(|p| {
398            line.windows(2)
399                .any(|s| distance_to_segment(*p, s[0], s[1]) <= width)
400        })
401    };
402    Ok(within(&first, &second) && within(&second, &first))
403}
404
405/// How far `p` stands from the segment from `a` to `b`.
406fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
407    let along = b - a;
408    let length = along.dot(along);
409    if length <= 0.0 {
410        return p.distance(a);
411    }
412    let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
413    p.distance(a + along * t)
414}
415
416/// Where two chart segments cross, as the fraction along each; ends
417/// included, parallel segments never.
418fn segments_cross(p: Point2, q: Point2, r: Point2, s: Point2) -> Option<(f64, f64)> {
419    let (d, e) = (q - p, s - r);
420    let denominator = d.cross(e);
421    if denominator == 0.0 {
422        return None;
423    }
424    let w = r - p;
425    let (t, u) = (w.cross(e) / denominator, w.cross(d) / denominator);
426    ((0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u)).then_some((t, u))
427}
428
429/// Newton from `seed` onto where the two pcurves, the second moved by
430/// `by`, meet, inside both edges' ranges; `None` where it does not land
431/// there.
432fn polish(
433    a: &Walked<'_>,
434    b: &Walked<'_>,
435    by: Vector2,
436    seed: [f64; 2],
437    tol: Tolerances,
438) -> Option<[f64; 2]> {
439    let clamp = |t: f64, (lo, hi): (f64, f64)| t.max(lo.min(hi)).min(lo.max(hi));
440    let system = |x: &[f64; 2]| {
441        let (ta, tb) = (clamp(x[0], a.range), clamp(x[1], b.range));
442        let pa = a.pcurve.point_at(ta, tol).unwrap_or(Point2::ORIGIN) + a.shift;
443        let pb = b.pcurve.point_at(tb, tol).unwrap_or(Point2::ORIGIN) + b.shift + by;
444        let da = a.pcurve.d1_at(ta, tol).unwrap_or(Vector2::new(0.0, 0.0));
445        let db = b.pcurve.d1_at(tb, tol).unwrap_or(Vector2::new(0.0, 0.0));
446        ([pa.x - pb.x, pa.y - pb.y], [[da.x, -db.x], [da.y, -db.y]])
447    };
448    let criteria = solve::Criteria {
449        residual: tol.confusion() * 0.01,
450        step: tol.parametric(),
451        max_iterations: 40,
452    };
453    let (x, _, _, _) = solve::newton_system_fixed(system, seed, criteria).ok()?;
454    let (ta, tb) = (clamp(x[0], a.range), clamp(x[1], b.range));
455    let pa = a.pcurve.point_at(ta, tol).ok()? + a.shift;
456    let pb = b.pcurve.point_at(tb, tol).ok()? + b.shift + by;
457    (pa.distance(pb) <= tol.confusion()).then_some([ta, tb])
458}