Skip to main content

ogeom_bool/
section_face.rs

1//! Sectioning faces by a plane, with no solid behind them.
2//!
3//! The boolean's section needs two solids, because it runs the whole
4//! arrangement and asks each piece which side of the other it lies on. A
5//! face on its own (a sheet read from a file, one face picked from a part)
6//! has no inside to ask about, and what a section of it means is simpler:
7//! the curves where its surface crosses the plane, kept where they run on
8//! the face.
9
10use ogeom_algo::{
11    Built, Containment, History, classify_on_face, make_edge_between, make_vertex, shape_bounds,
12};
13use ogeom_core::{OgeomResult, Tolerances};
14use ogeom_geom::{Curve, Curve3d as _, PlaneSurface, SurfaceGeometry, Transformable as _};
15use ogeom_intersect::{CurveCurveOptions, IntersectOptions, SurfaceIntersection};
16use ogeom_math::{Plane, Point};
17use ogeom_mesh::Deflection;
18use ogeom_topo::{EdgeRepr, Filter, Model, Shape, ShapeType, explore, explore_unique};
19
20/// The edges where the faces of `shape` cross `plane`, each trimmed to the
21/// face it lies on.
22///
23/// `shape` is a face, a shell, a solid or a compound of them. A curve is
24/// exact where the face's surface and the plane have a closed-form section
25/// (a line, a circle, an ellipse) and fitted otherwise. A face lying in the
26/// plane gives its own boundary. A plane that misses every face gives an
27/// empty compound.
28///
29/// # Errors
30///
31/// As the surface and curve intersectors, and
32/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
33/// `shape` has no faces.
34pub fn section_face(
35    model: &mut Model,
36    shape: &Shape,
37    plane: &Plane,
38    tol: Tolerances,
39) -> OgeomResult<Built> {
40    let faces = explore(model, shape, Filter::OfType(ShapeType::Face))?;
41    if faces.is_empty() {
42        ogeom_core::ogeom_bail!(Construction, "a section by a plane needs faces to cut");
43    }
44    // The plane, windowed to reach past the shape on every side, so its
45    // sections with the faces are bounded curves.
46    let bounds = shape_bounds(model, shape, tol)?;
47    let (Some(low), Some(high)) = (bounds.low(), bounds.high()) else {
48        return empty(model, shape);
49    };
50    let reach = low.distance(high).max(tol.confusion() * 1e3);
51    let centre = plane.frame().to_local(low.midpoint(high));
52    let cutter: SurfaceGeometry = PlaneSurface::over(
53        *plane,
54        (centre.x - reach, centre.x + reach),
55        (centre.y - reach, centre.y + reach),
56    )?
57    .into();
58
59    let mut edges: Vec<Shape> = Vec::new();
60    let mut vertices: Vec<(Point, Shape)> = Vec::new();
61    let mut seen_boundary: Vec<ogeom_topo::TShapeId> = Vec::new();
62    for face in &faces {
63        // A face whose box stands wholly on one side of the plane cannot
64        // meet it: its corners' signed distances say so without asking the
65        // intersector.
66        let corners = shape_bounds(model, face, tol)?.corners();
67        let side = |c: &Point| plane.signed_distance_to(*c);
68        if !corners.is_empty()
69            && (corners.iter().all(|c| side(c) > tol.confusion())
70                || corners.iter().all(|c| side(c) < -tol.confusion()))
71        {
72            continue;
73        }
74        let Some(surface_id) = model
75            .node(face)
76            .and_then(|n| n.data().as_face())
77            .map(|d| d.surface)
78        else {
79            continue;
80        };
81        let Some(surface) = model.geometry().surface(surface_id) else {
82            continue;
83        };
84        let placed = surface.transformed(&face.transform(model.datums())?, tol)?;
85        let found = ogeom_intersect::intersect_surfaces(
86            &placed,
87            &cutter,
88            IntersectOptions {
89                tolerance: tol.confusion() * 0.5,
90                ..IntersectOptions::default()
91            },
92            tol,
93        )?;
94        let curves = match found {
95            SurfaceIntersection::Along(curves) => curves,
96            // A face in the plane: its boundary is where the two meet.
97            SurfaceIntersection::Same => {
98                for edge in explore_unique(model, face, ShapeType::Edge)? {
99                    if !seen_boundary.contains(&edge.node()) {
100                        seen_boundary.push(edge.node());
101                        edges.push(edge);
102                    }
103                }
104                continue;
105            }
106            SurfaceIntersection::Apart | SurfaceIntersection::Touching(_) => continue,
107        };
108        let boundary = boundary_curves(model, face, tol)?;
109        for section in curves {
110            for (lo, hi) in on_face(model, face, &section.curve, &boundary, tol)? {
111                let from = section.curve.point_at(lo, tol)?;
112                let to = section.curve.point_at(hi, tol)?;
113                let v0 = vertex(model, &mut vertices, from, tol)?;
114                let v1 = vertex(model, &mut vertices, to, tol)?;
115                edges.push(
116                    make_edge_between(model, section.curve.clone(), (lo, hi), &v0, &v1, tol)?.shape,
117                );
118            }
119        }
120    }
121    let result = model.add_compound(&edges)?;
122    let mut history = History::new();
123    history.modify(shape, result.clone());
124    Ok(Built::new(result, history))
125}
126
127fn empty(model: &mut Model, shape: &Shape) -> OgeomResult<Built> {
128    let result = model.add_compound(&[])?;
129    let mut history = History::new();
130    history.modify(shape, result.clone());
131    Ok(Built::new(result, history))
132}
133
134/// One vertex per place: neighbouring faces' sections meet at the edge they
135/// share, and there their ends are one vertex.
136pub(crate) fn vertex(
137    model: &mut Model,
138    vertices: &mut Vec<(Point, Shape)>,
139    at: Point,
140    tol: Tolerances,
141) -> OgeomResult<Shape> {
142    if let Some((p, v)) = vertices
143        .iter()
144        .find(|(p, _)| p.distance(at) <= tol.confusion() * 10.0)
145    {
146        // The shared vertex stands for this end too: it must reach it, or
147        // the edge ending here is refused as missing its vertex.
148        let gap = p.distance(at) * 1.01;
149        if gap > tol.confusion()
150            && let Ok(reach) = ogeom_core::Tolerance::new(gap)
151        {
152            model.widen(v, reach)?;
153        }
154        return Ok(v.clone());
155    }
156    let v = make_vertex(model, at).shape;
157    vertices.push((at, v.clone()));
158    Ok(v)
159}
160
161/// The face's boundary edges as curves over their own ranges, placed.
162fn boundary_curves(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<Vec<Curve>> {
163    let mut out = Vec::new();
164    for edge in explore_unique(model, face, ShapeType::Edge)? {
165        let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
166            continue;
167        };
168        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
169            continue;
170        };
171        let Some(geometry) = model.geometry().curve(*curve) else {
172            continue;
173        };
174        let placed = geometry
175            .clone()
176            .transformed(&edge.transform(model.datums())?, tol)?;
177        if let Ok(trimmed) = ogeom_geom::TrimmedCurve::new(placed, range.0, range.1, tol) {
178            out.push(trimmed.into());
179        }
180    }
181    Ok(out)
182}
183
184/// The stretches of `curve` that run on the face: split wherever it
185/// crosses or leaves the face's boundary, each stretch kept if its middle
186/// lies on the face.
187fn on_face(
188    model: &Model,
189    face: &Shape,
190    curve: &Curve,
191    boundary: &[Curve],
192    tol: Tolerances,
193) -> OgeomResult<Vec<(f64, f64)>> {
194    let (lo, hi) = curve.domain();
195    let mut cuts = vec![lo, hi];
196    let options = CurveCurveOptions {
197        gap: tol.confusion() * 10.0,
198        ..CurveCurveOptions::default()
199    };
200    for edge in boundary {
201        let found = ogeom_intersect::intersect_curves(curve, edge, options, tol)?;
202        cuts.extend(found.crossings.iter().map(|c| c.on_a));
203        for overlap in &found.overlaps {
204            cuts.push(overlap.on_a.0);
205            cuts.push(overlap.on_a.1);
206        }
207    }
208    cuts.retain(|t| *t >= lo && *t <= hi);
209    cuts.sort_by(f64::total_cmp);
210    cuts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
211    let mut kept: Vec<(f64, f64)> = Vec::new();
212    for pair in cuts.windows(2) {
213        let (a, b) = (pair[0], pair[1]);
214        if b - a <= tol.parametric() {
215            continue;
216        }
217        let middle = curve.point_at(f64::midpoint(a, b), tol)?;
218        if classify_on_face(model, face, middle, Deflection::default(), tol)? == Containment::In {
219            // Stretches that meet end to end on the face are one edge.
220            match kept.last_mut() {
221                Some((_, end)) if (*end - a).abs() <= tol.parametric() => *end = b,
222                _ => kept.push((a, b)),
223            }
224        }
225    }
226    Ok(kept)
227}