Skip to main content

ogeom_algo/
out_and_back.rs

1//! Where a face's boundary runs out along a path and straight back along
2//! it.
3//!
4//! Two consecutive edges of a wire, the first from one vertex to another
5//! and the second from that one back along the first, bound nothing
6//! between them: the region the wire encloses is the same with that stretch
7//! of boundary as without. The tip is a vertex of the boundary that bounds
8//! no corner, every boolean through the face meets a strand there, and a
9//! mesher has to strip it before it can triangulate.
10//!
11//! - A **tail**: the second edge comes back to the vertex the first left
12//!   from. Both edges run out and back whole. [`is_tail`] also takes one
13//!   edge walked out and straight back (a slit), which the booleans cut and
14//!   make with exact volumes; [`out_and_back`] does not report a slit,
15//!   only two distinct edges over one path.
16//! - A **notch**: one edge runs farther than the other. The shorter one's
17//!   far end stands on the longer within the tolerance either edge or that
18//!   vertex states, and the stretch of the longer from there to the tip
19//!   runs along the shorter. Only that stretch and the shorter edge bound
20//!   nothing.
21//!
22//! A pair is one only where all of these hold, so that nothing a neighbour
23//! holds or the chart tells apart is taken for one:
24//!
25//! - neither edge is degenerate, and the pair's ends are distinct vertices;
26//! - no other face of the shape walks either edge, nor this face again,
27//!   except as the pair itself (one edge walked out and back, a tail).
28//!   Uses are counted per placed occurrence: two placements of one face
29//!   share every node, but each walks its own edges;
30//! - neither edge is a seam on the face, and in the face's chart the tip is
31//!   one point, and a tail's walk back ends where its walk out began, with
32//!   no whole period between them along a direction the surface closes in:
33//!   two meridians a period apart on a sphere are one curve in space and
34//!   bound the whole sphere between them in the chart;
35//! - the two curves stand no farther apart than the wider of the two
36//!   edges' tolerances anywhere along the stretch that runs out and back.
37
38use ogeom_core::{FastMap, OgeomResult, Tolerances};
39use ogeom_geom::{Curve, Curve2d as _, Surface as _, SurfaceGeometry};
40use ogeom_math::{Point2, Transform};
41use ogeom_topo::{
42    EdgeRepr, Model, Orientation, SameKey, Shape, ShapeType, SurfaceId, TShapeId, explore_unique,
43};
44
45use crate::build::edge_vertices;
46use crate::pcurve_gap::{nearest_point_on_stretch, stretches_apart};
47
48/// Two consecutive edges of a face's wire running out and back along one
49/// path. See the module documentation.
50#[derive(Debug, Clone, PartialEq)]
51pub struct OutAndBack {
52    /// The face, as the shape holds it.
53    pub face: Shape,
54    /// The wire's index among the face's wires as stored.
55    pub wire: usize,
56    /// The edge run out along and the edge run back along, by index in the
57    /// wire's order: the second follows the first, the last wrapping to
58    /// the first.
59    pub edges: [usize; 2],
60    /// Where one of them runs farther than the other: `None` for a tail.
61    pub notch: Option<Notch>,
62}
63
64/// Where a notch's shorter edge turns back from the longer.
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct Notch {
67    /// Which of the pair runs farther: 0 the edge out, 1 the edge back.
68    pub longer: usize,
69    /// The parameter of the longer edge's curve nearest the shorter one's
70    /// far end: from there to the tip, the longer runs along the shorter.
71    pub parameter: f64,
72}
73
74/// How many times each edge node is walked by the faces of `shape`, every
75/// placed occurrence of it together.
76///
77/// Whether an edge is free or shared is a question about one occurrence:
78/// two placements of one face share every node and are two faces, each
79/// walking its own edges. [`placed_edge_uses`] answers it.
80///
81/// # Errors
82///
83/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle
84/// fails to resolve.
85pub fn edge_uses(model: &Model, shape: &Shape) -> OgeomResult<FastMap<TShapeId, usize>> {
86    let mut uses: FastMap<TShapeId, usize> = FastMap::default();
87    for (edge, n) in placed_edge_uses(model, shape)? {
88        *uses.entry(edge.0.node()).or_default() += n;
89    }
90    Ok(uses)
91}
92
93/// How many times each edge of `shape`, as placed (node and placement,
94/// [`SameKey`]), is walked by the faces of `shape`.
95///
96/// # Errors
97///
98/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle
99/// fails to resolve.
100pub fn placed_edge_uses(model: &Model, shape: &Shape) -> OgeomResult<FastMap<SameKey, usize>> {
101    let mut uses: FastMap<SameKey, usize> = FastMap::default();
102    for face in explore_unique(model, shape, ShapeType::Face)? {
103        for wire in model.children_of(&face)? {
104            for edge in model.children_of(&wire)? {
105                *uses.entry(SameKey(edge)).or_default() += 1;
106            }
107        }
108    }
109    Ok(uses)
110}
111
112/// Every pair of consecutive edges of a face of `shape` that runs out and
113/// back along one path, tails of two distinct edges and notches alike, face
114/// by face in the shape's order and wire by wire; a slit, one edge walked
115/// out and back, is not one. See the module documentation.
116///
117/// # Errors
118///
119/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle
120/// fails to resolve; an evaluation error where a curve cannot be evaluated
121/// over its range.
122pub fn out_and_back(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<OutAndBack>> {
123    let uses = placed_edge_uses(model, shape)?;
124    let mut found = Vec::new();
125    for face in explore_unique(model, shape, ShapeType::Face)? {
126        let Some(surface_id) = model
127            .node(&face)
128            .and_then(|n| n.data().as_face())
129            .map(|d| d.surface)
130        else {
131            continue;
132        };
133        let Some(surface) = model.geometry().surface(surface_id) else {
134            continue;
135        };
136        let stored = face.oriented(Orientation::Forward);
137        for (w, wire) in model.ordered_children_of(&stored)?.into_iter().enumerate() {
138            let walked = model.ordered_children_of(&wire.oriented(Orientation::Forward))?;
139            if walked.len() < 2 {
140                continue;
141            }
142            for i in 0..walked.len() {
143                let j = (i + 1) % walked.len();
144                if walked.len() == 2 && j == 0 {
145                    // Two edges: the pair round the wire's start is the
146                    // same pair walked the other way.
147                    continue;
148                }
149                let (out, back) = (&walked[i], &walked[j]);
150                if out.node() == back.node() {
151                    continue;
152                }
153                let notch = if is_tail(model, (surface_id, surface), out, back, &uses, tol)? {
154                    None
155                } else if let Some(notch) =
156                    notch_of(model, (surface_id, surface), out, back, &uses, tol)?
157                {
158                    Some(notch)
159                } else {
160                    continue;
161                };
162                found.push(OutAndBack {
163                    face: face.clone(),
164                    wire: w,
165                    edges: [i, j],
166                    notch,
167                });
168            }
169        }
170    }
171    Ok(found)
172}
173
174/// Whether `out`, then `back`, walked in that order by one wire of the
175/// face on `surface`, are a tail. `uses` counts how many times the faces of
176/// the shape walk each edge as placed ([`placed_edge_uses`]). See the
177/// module documentation.
178///
179/// # Errors
180///
181/// As [`out_and_back`].
182pub fn is_tail(
183    model: &Model,
184    (surface_id, surface): (SurfaceId, &SurfaceGeometry),
185    out: &Shape,
186    back: &Shape,
187    uses: &FastMap<SameKey, usize>,
188    tol: Tolerances,
189) -> OgeomResult<bool> {
190    let Some((out_data, back_data)) = alone(model, out, back, uses) else {
191        return Ok(false);
192    };
193    let (Some((a, b)), Some((c, d))) = (edge_vertices(model, out)?, edge_vertices(model, back)?)
194    else {
195        return Ok(false);
196    };
197    if a.node() == b.node() || c.node() != b.node() || d.node() != a.node() {
198        return Ok(false);
199    }
200    let (Some(out_chart), Some(back_chart)) = (
201        chart_ends(model, surface_id, out, tol)?,
202        chart_ends(model, surface_id, back, tol)?,
203    ) else {
204        return Ok(false);
205    };
206    if !within_a_period(surface, out_chart.0, back_chart.1, tol)
207        || !within_a_period(surface, out_chart.1, back_chart.0, tol)
208    {
209        return Ok(false);
210    }
211    if out.node() == back.node() {
212        return Ok(true);
213    }
214    let reach = out_data.tolerance.get().max(back_data.tolerance.get());
215    Ok(crate::edges_apart(model, out, back, tol)?.is_some_and(|apart| apart <= reach))
216}
217
218/// The notch `out`, then `back`, walked in that order by one wire of the
219/// face on `surface`, make, where they make one. See [`is_tail`] and the
220/// module documentation.
221///
222/// # Errors
223///
224/// As [`out_and_back`].
225pub fn notch_of(
226    model: &Model,
227    (surface_id, surface): (SurfaceId, &SurfaceGeometry),
228    out: &Shape,
229    back: &Shape,
230    uses: &FastMap<SameKey, usize>,
231    tol: Tolerances,
232) -> OgeomResult<Option<Notch>> {
233    let Some((out_data, back_data)) = alone(model, out, back, uses) else {
234        return Ok(None);
235    };
236    if out.node() == back.node() {
237        return Ok(None);
238    }
239    let (Some((a, b)), Some((c, d))) = (edge_vertices(model, out)?, edge_vertices(model, back)?)
240    else {
241        return Ok(None);
242    };
243    if a.node() == b.node() || c.node() != b.node() || d.node() == c.node() || d.node() == a.node()
244    {
245        return Ok(None);
246    }
247    let (Some(out_chart), Some(back_chart)) = (
248        chart_ends(model, surface_id, out, tol)?,
249        chart_ends(model, surface_id, back, tol)?,
250    ) else {
251        return Ok(None);
252    };
253    if !within_a_period(surface, out_chart.1, back_chart.0, tol) {
254        return Ok(None);
255    }
256    let (Some(out_curve), Some(back_curve)) =
257        (placed_curve(model, out)?, placed_curve(model, back)?)
258    else {
259        return Ok(None);
260    };
261    let reach = out_data.tolerance.get().max(back_data.tolerance.get());
262    // Each way round: the longer edge, the shorter, the shorter's far end,
263    // and which end of the longer's range is the tip.
264    let tip_at_end = |edge: &Shape, walked_into: bool| {
265        (edge.orientation() == Orientation::Reversed) != walked_into
266    };
267    for (longer, (long_curve, short_curve, far, tip_high)) in [
268        (out_curve, back_curve, &d, tip_at_end(out, true)),
269        (back_curve, out_curve, &a, tip_at_end(back, false)),
270    ]
271    .into_iter()
272    .enumerate()
273    {
274        let Some(vertex) = model.node(far).and_then(|n| n.data().as_vertex()) else {
275            continue;
276        };
277        let placed = far.transform(model.datums())?.apply(vertex.point);
278        let (curve, range, placement) = long_curve;
279        let into = placement.inverse()?;
280        let (t, off) = nearest_point_on_stretch(curve, range, into.apply(placed), tol)?;
281        let off = off * placement.scale_factor().abs();
282        if off > reach.max(vertex.tolerance.get()) {
283            continue;
284        }
285        let (lo, hi) = (range.0.min(range.1), range.0.max(range.1));
286        if !(t > lo && t < hi) {
287            continue;
288        }
289        let stretch = if tip_high { (t, range.1) } else { (range.0, t) };
290        if stretches_apart((curve, stretch, placement), short_curve, tol)? <= reach {
291            return Ok(Some(Notch {
292                longer,
293                parameter: t,
294            }));
295        }
296    }
297    Ok(None)
298}
299
300/// The data of `out` and `back` where neither is degenerate and no face
301/// walks either but the one walking them as a pair: each once, or one
302/// edge out and back.
303fn alone<'m>(
304    model: &'m Model,
305    out: &Shape,
306    back: &Shape,
307    uses: &FastMap<SameKey, usize>,
308) -> Option<(&'m ogeom_topo::EdgeData, &'m ogeom_topo::EdgeData)> {
309    let data = |edge: &Shape| model.node(edge).and_then(|n| n.data().as_edge());
310    let (out_data, back_data) = (data(out)?, data(back)?);
311    if out_data.degenerate || back_data.degenerate {
312        return None;
313    }
314    let used = |edge: &Shape| uses.get(&SameKey(edge.clone())).copied().unwrap_or(0);
315    let alone = if out.node() == back.node() {
316        used(out) == 2 && out.orientation() != back.orientation()
317    } else {
318        used(out) == 1 && used(back) == 1
319    };
320    alone.then_some((out_data, back_data))
321}
322
323/// A curve, the range of it an edge runs over, and where the edge's
324/// occurrence places it.
325type PlacedCurve<'m> = (&'m Curve, (f64, f64), Transform);
326
327/// `edge`'s curve, its range, and where its occurrence places it.
328fn placed_curve<'m>(model: &'m Model, edge: &Shape) -> OgeomResult<Option<PlacedCurve<'m>>> {
329    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
330        return Ok(None);
331    };
332    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
333        return Ok(None);
334    };
335    let Some(curve) = model.geometry().curve(*curve) else {
336        return Ok(None);
337    };
338    Ok(Some((curve, *range, edge.transform(model.datums())?)))
339}
340
341/// Where `edge`'s one plain pcurve on `surface` begins and ends as its
342/// occurrence walks it. `None` for a seam, or an edge with no pcurve there.
343fn chart_ends(
344    model: &Model,
345    surface: SurfaceId,
346    edge: &Shape,
347    tol: Tolerances,
348) -> OgeomResult<Option<(Point2, Point2)>> {
349    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
350        return Ok(None);
351    };
352    let Some(EdgeRepr::PCurve {
353        curve,
354        range,
355        location,
356        ..
357    }) = data.pcurve_for(surface, edge.location())
358    else {
359        return Ok(None);
360    };
361    // Two plain pcurves on one surface at one placement are a seam stored
362    // as two columns, walked once each way.
363    let plain = data
364        .representations
365        .iter()
366        .filter(|r| {
367            matches!(r, EdgeRepr::PCurve { .. })
368                && r.surface() == Some(surface)
369                && r.location() == Some(location)
370        })
371        .count();
372    if plain > 1 {
373        return Ok(None);
374    }
375    let Some(pcurve) = model.geometry().pcurve(*curve) else {
376        return Ok(None);
377    };
378    let (start, end) = (
379        pcurve.point_at(range.0, tol)?,
380        pcurve.point_at(range.1, tol)?,
381    );
382    Ok(Some(if edge.orientation() == Orientation::Reversed {
383        (end, start)
384    } else {
385        (start, end)
386    }))
387}
388
389/// Whether `p` and `q` stand less than half a period apart along every
390/// direction `surface` closes in: the same copy of the chart.
391fn within_a_period(surface: &SurfaceGeometry, p: Point2, q: Point2, tol: Tolerances) -> bool {
392    let ((u0, u1), (v0, v1)) = surface.domain();
393    let closes = [
394        surface.is_periodic_u() || surface.is_closed_u(tol),
395        surface.is_periodic_v() || surface.is_closed_v(tol),
396    ];
397    [
398        (closes[0], u1 - u0, (p.x - q.x).abs()),
399        (closes[1], v1 - v0, (p.y - q.y).abs()),
400    ]
401    .into_iter()
402    .all(|(closes, period, apart)| !closes || !period.is_finite() || 2.0 * apart < period)
403}