Skip to main content

ogeom_bool/
self_union.rs

1//! A solid whose boundary passes through itself, resolved into the union
2//! of the material it encloses.
3//!
4//! A boundary that crosses itself encloses some regions twice: the winding
5//! number, how many more times a ray from a point leaves through the
6//! boundary than enters, is two there, one in the material enclosed once
7//! and zero outside. The material is where it is one or more, and its
8//! boundary is made of the patches of faces with none on their outer side
9//! and some on their inner side.
10//!
11//! So every face is split by every other face it meets (the general fuse
12//! of the solid with itself, each face against all but itself and the
13//! faces it shares an edge or a corner with), and each piece is read on
14//! its two sides, a step off it along its normal that meets no other face
15//! on the way. A piece with winding zero beyond it and one or more behind
16//! it bounds the union and is kept; every other piece lies inside the
17//! union or outside it, and goes. Where faces lie on one surface over a
18//! patch, facing the same way, the patch is kept once, from the first of
19//! them. The kept pieces are sewn and closed as a boolean's are.
20
21use ogeom_algo::{Built, History, SolidBoundary};
22use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
23use ogeom_geom::Surface as _;
24use ogeom_math::{Point, Point2, Vector};
25use ogeom_mesh::Deflection;
26use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
27
28use super::{
29    FacePiece, GFace, GeneralFused, Operands, assemble_result, baked_if_scaled, general_fuse_as,
30    outward_normal,
31};
32
33/// The union of the material `solid` encloses, where its boundary passes
34/// through itself: the regions it encloses twice made one, and its faces
35/// cut where they cross and trimmed back to the union's boundary.
36///
37/// A solid whose boundary does not pass through itself (what
38/// [`self_crossings`](ogeom_algo::self_crossings) finds) comes back as it
39/// is, with an empty history. A region the boundary winds round negatively,
40/// enclosed inside out, is not material and is left out.
41///
42/// # Errors
43///
44/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
45/// `solid` is not a solid;
46/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) where a face
47/// passes through itself or through a face it shares an edge or a corner
48/// with (only faces apart are split against each other), where a piece's
49/// two sides cannot be read, or where the kept pieces do not close or the
50/// union still crosses itself; and as the boolean's operations.
51pub fn self_union(model: &mut Model, solid: &Shape, tol: Tolerances) -> OgeomResult<Built> {
52    if model.kind_of(solid)? != ShapeType::Solid {
53        ogeom_bail!(Construction, "a self-union resolves a solid");
54    }
55    let crossings = ogeom_algo::self_crossings(model, solid, tol)?;
56    if crossings.is_empty() {
57        return Ok(Built::new(solid.clone(), History::new()));
58    }
59    // Only faces apart are split against each other: a face meets those it
60    // shares an edge or a corner with there already.
61    let corners = |face: &Shape| -> OgeomResult<ogeom_core::FastSet<super::EdgeKey>> {
62        Ok(explore_unique(model, face, ShapeType::Vertex)?
63            .iter()
64            .map(super::EdgeKey::of)
65            .collect())
66    };
67    for crossing in &crossings {
68        if crossing.face.is_same(&crossing.other)
69            || !corners(&crossing.face)?.is_disjoint(&corners(&crossing.other)?)
70        {
71            ogeom_bail!(
72                NotDone,
73                "a face passes through itself or through a face it shares an edge or a \
74                 corner with, which the self-union does not split"
75            );
76        }
77    }
78    let solid = &baked_if_scaled(model, solid, tol)?;
79    let fused = general_fuse_as(model, solid, solid, Operands::Itself, None, tol)?;
80    let boundary = SolidBoundary::of(model, solid, tol.confusion() * 1e4, tol)?;
81    let mut keep: Vec<Option<bool>> = vec![None; fused.pieces.len()];
82    for (index, piece) in fused.pieces.iter().enumerate() {
83        if piece.from_a && !piece.outlines.is_empty() {
84            let Some(bounds) = bounds_the_union(model, &fused, piece, &boundary, tol)? else {
85                ogeom_bail!(
86                    NotDone,
87                    "a piece of a face reads its two sides nowhere a step off it meets no \
88                     other face"
89                );
90            };
91            keep[index] = Some(bounds);
92        }
93    }
94    for group in quiet_groups(&fused) {
95        let mut read = None;
96        for &index in &group {
97            let Some(this) = bounds_the_union(model, &fused, &fused.pieces[index], &boundary, tol)?
98            else {
99                continue;
100            };
101            if read.is_some_and(|read| read != this) {
102                ogeom_bail!(
103                    NotDone,
104                    "a run of faces nothing crosses bounds the union in part, which only a \
105                     face meeting its edges makes"
106                );
107            }
108            read = Some(this);
109        }
110        let Some(bounds) = read else {
111            ogeom_bail!(
112                NotDone,
113                "a run of faces nothing crosses reads its two sides nowhere a step off it \
114                 meets no other face"
115            );
116        };
117        for index in group {
118            keep[index] = Some(bounds);
119        }
120    }
121    let kept: Vec<(usize, bool)> = keep
122        .iter()
123        .enumerate()
124        .filter(|(_, keep)| **keep == Some(true))
125        .map(|(index, _)| (index, false))
126        .collect();
127    let built = assemble_result(model, &fused, &kept, [false, false], solid, solid, tol)?;
128    for made in explore_unique(model, &built.shape, ShapeType::Solid)? {
129        if !ogeom_algo::self_crossings(model, &made, tol)?.is_empty() {
130            ogeom_bail!(NotDone, "the union still passes through itself");
131        }
132    }
133    Ok(built)
134}
135
136/// Whether `piece` bounds the union: winding zero just beyond it, one or
137/// more just behind it, and, where faces lie on its surface there facing
138/// the same way, it is the first of them. `None` where the piece has no
139/// interior point drawn, no normal there, or no step off it reads.
140fn bounds_the_union(
141    model: &Model,
142    fused: &GeneralFused,
143    piece: &FacePiece,
144    boundary: &SolidBoundary,
145    tol: Tolerances,
146) -> OgeomResult<Option<bool>> {
147    let face = &fused.a.faces[piece.face];
148    let Some((at, uv)) = interior_point(model, face, piece, tol)? else {
149        return Ok(None);
150    };
151    let normal = outward_normal(face, uv, tol)?;
152    let length = normal.magnitude();
153    if length <= tol.confusion() {
154        return Ok(None);
155    }
156    let normal = normal / length;
157    // The faces lying where the piece does, itself among them, and how
158    // many of them face the way it does net of those facing against it:
159    // what stepping through them all changes the winding by.
160    let on = boundary.faces_at(model, at, tol)?;
161    let mut net = 0_i64;
162    let mut first = true;
163    for other in &on {
164        let Some(gathered) = fused.a.faces.iter().position(|g| g.face.is_same(other)) else {
165            ogeom_bail!(NotDone, "a face of the solid was not gathered");
166        };
167        let theirs = &fused.a.faces[gathered];
168        let foot = ogeom_algo::project_on_surface(&theirs.surface, at, 16, tol)?;
169        let (u, v) = foot.parameters;
170        let along = outward_normal(theirs, Point2::new(u, v), tol)?.dot(normal);
171        if along > 0.0 {
172            net += 1;
173            if gathered < piece.face {
174                first = false;
175            }
176        } else {
177            net -= 1;
178        }
179    }
180    let Some((beyond, behind)) = sides(model, face, at, normal, &on, boundary, tol)? else {
181        return Ok(None);
182    };
183    if behind - beyond != net {
184        ogeom_bail!(
185            NotDone,
186            "a piece of a face reads windings {beyond} and {behind} on its two sides, \
187             which the {net} faces lying there do not step between"
188        );
189    }
190    if beyond >= 1 && behind <= 0 {
191        ogeom_bail!(
192            NotDone,
193            "a piece of a face bounds the union facing into it, which no face lying there \
194             is turned to"
195        );
196    }
197    Ok(Some(beyond == 0 && behind >= 1 && first))
198}
199
200/// The winding numbers just beyond `at` along `normal` and just behind it,
201/// read a step off each way along a segment that meets no face past those
202/// it starts on, `on`: from a twentieth of the face's size down by tenths to twenty
203/// times the band a point counts as on it within. `None` where no step
204/// reads.
205fn sides(
206    model: &Model,
207    face: &GFace,
208    at: Point,
209    normal: Vector,
210    on: &[Shape],
211    boundary: &SolidBoundary,
212    tol: Tolerances,
213) -> OgeomResult<Option<(i64, i64)>> {
214    let shortest = face.tolerance.max(tol.confusion() * 1e3) * 20.0;
215    let mut step = (face.filter.diagonal() * 0.05).max(shortest);
216    loop {
217        ogeom_core::progress::checkpoint()?;
218        let (out_at, in_at) = (at + normal * step, at - normal * step);
219        if boundary.clear_from(model, at, out_at, on, tol)?
220            && boundary.clear_from(model, at, in_at, on, tol)?
221            && let (Some(beyond), Some(behind)) = (
222                boundary.winding(model, out_at, tol)?,
223                boundary.winding(model, in_at, tol)?,
224            )
225        {
226            return Ok(Some((beyond, behind)));
227        }
228        if step <= shortest {
229            return Ok(None);
230        }
231        step = (step * 0.1).max(shortest);
232    }
233}
234
235/// A point inside `piece`, in space and in its face's chart: the probe the
236/// arrangement chose for a piece it split off, and for a face nothing split
237/// the chart middle of a triangle of its mesh, the largest first, that lies
238/// inside the face itself, clear of its trim. `None` where none does.
239fn interior_point(
240    model: &Model,
241    face: &GFace,
242    piece: &FacePiece,
243    tol: Tolerances,
244) -> OgeomResult<Option<(Point, Point2)>> {
245    if !piece.outlines.is_empty() {
246        let foot = ogeom_algo::project_on_surface(&face.surface, piece.probe, 16, tol)?;
247        let (u, v) = foot.parameters;
248        return Ok(Some((piece.probe, Point2::new(u, v))));
249    }
250    let mesh = ogeom_mesh::triangulate_face(model, &face.face, Deflection::default(), tol)?;
251    if mesh.parameters.len() != mesh.positions.len() {
252        return Ok(None);
253    }
254    let area = |t: &[u32; 3]| {
255        let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
256        (b - a).cross(c - a).magnitude()
257    };
258    let mut triangles: Vec<&[u32; 3]> = mesh.triangles.iter().collect();
259    triangles.sort_by(|x, y| area(y).total_cmp(&area(x)));
260    for triangle in triangles {
261        let corners = triangle.map(|i| mesh.parameters[i as usize]);
262        let uv = Point2::new(
263            (corners[0].0 + corners[1].0 + corners[2].0) / 3.0,
264            (corners[0].1 + corners[1].1 + corners[2].1) / 3.0,
265        );
266        let at = face.surface.point_at(uv.x, uv.y, tol)?;
267        if ogeom_algo::classify_on_face(model, &face.face, at, Deflection::default(), tol)?
268            == ogeom_algo::Containment::In
269        {
270            return Ok(Some((at, uv)));
271        }
272    }
273    Ok(None)
274}
275
276/// The pieces that are whole faces nothing crosses, in runs joined across
277/// the edges only two faces of the solid meet at, each run in face order.
278///
279/// Nothing crosses such a face or comes near its edges, so beside one of
280/// its edges the faces either side of it bound the same two regions:
281/// a whole run reads alike. Each face of it is read where it can be, and
282/// the readings must agree.
283fn quiet_groups(fused: &GeneralFused) -> Vec<Vec<usize>> {
284    let quiet: Vec<usize> = fused
285        .pieces
286        .iter()
287        .enumerate()
288        .filter(|(_, piece)| piece.from_a && piece.outlines.is_empty())
289        .map(|(index, _)| index)
290        .collect();
291    let mut parent: Vec<usize> = (0..quiet.len()).collect();
292    let find = |parent: &mut Vec<usize>, mut i: usize| {
293        while parent[i] != i {
294            parent[i] = parent[parent[i]];
295            i = parent[i];
296        }
297        i
298    };
299    // An edge more faces than two meet at joins none of them: a face beside
300    // it may lie between the others.
301    let mut uses: ogeom_core::FastMap<super::EdgeKey, usize> = ogeom_core::FastMap::default();
302    for face in &fused.a.faces {
303        for edge in &face.edges {
304            *uses.entry(edge.node).or_default() += 1;
305        }
306    }
307    let mut by_edge: ogeom_core::FastMap<super::EdgeKey, usize> = ogeom_core::FastMap::default();
308    for (slot, &index) in quiet.iter().enumerate() {
309        for edge in &fused.a.faces[fused.pieces[index].face].edges {
310            let key = edge.node;
311            if uses.get(&key) != Some(&2) {
312                continue;
313            }
314            if let Some(&other) = by_edge.get(&key) {
315                let (a, b) = (find(&mut parent, slot), find(&mut parent, other));
316                parent[a.max(b)] = a.min(b);
317            } else {
318                by_edge.insert(key, slot);
319            }
320        }
321    }
322    let mut groups: Vec<Vec<usize>> = Vec::new();
323    let mut root_group: ogeom_core::FastMap<usize, usize> = ogeom_core::FastMap::default();
324    for (slot, &index) in quiet.iter().enumerate() {
325        let root = find(&mut parent, slot);
326        let group = *root_group.entry(root).or_insert_with(|| {
327            groups.push(Vec::new());
328            groups.len() - 1
329        });
330        groups[group].push(index);
331    }
332    groups
333}