Skip to main content

geop_core_topology/
merge.rs

1//! Topology that is no longer needed, taken out of a body described as a
2//! [`BodySpec`] ([`BodySpec::merged`]): faces that lie on one surface made
3//! one, and vertices between two edges of the same two faces dropped, the
4//! edges made one.
5//!
6//! A union leaves both solids' faces where they lay: two blocks standing
7//! flush leave two faces in one plane, a seam edge between them that is no
8//! edge of the shape. A face that lies on another's surface is decided on
9//! the representation, not by recognising a plane or a cylinder: the
10//! other's patch carried on reaches it, and the two are one polynomial
11//! there
12//! ([`geop_core_geometry::nurb_surface::NurbSurface3D::carrying`]). Two faces of one surface parted
13//! because their patches cannot be one — a cylinder's quarters, which
14//! would wrap round — stay apart: their loops would not close in one
15//! patch's `(u, v)`.
16
17use geop_core_geometry::nurb_curve::loop_signed_area;
18use geop_core_math::{
19    geop_error::{GeopError, GeopResult},
20    scalars::Scalar,
21};
22
23use crate::{
24    Sense,
25    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
26};
27
28thread_local! {
29    static SURFACES_COMPARED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
30}
31
32/// How many pairs of faces this thread has asked whether they lie on one
33/// surface, ever: what merging costs grows with this, so a test can bound
34/// the work of a cut by the faces it touches rather than time it.
35pub fn surfaces_compared() -> usize {
36    SURFACES_COMPARED.get()
37}
38
39/// Where the entities of a [`BodySpec`] went in [`BodySpec::merged`]'s
40/// result, index for index.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Merged {
43    /// Per face: the face it is part of now.
44    pub faces: Vec<usize>,
45    /// Per edge: the edge it is part of now; none for an edge between two
46    /// faces made one.
47    pub edges: Vec<Option<usize>>,
48    /// Per vertex: the vertex it is now; none for one dropped.
49    pub vertices: Vec<Option<usize>>,
50}
51
52impl BodySpec {
53    /// The same body without topology it does not need (see the module
54    /// docs): every two faces across an edge that lie on one surface made
55    /// one, on the surface of the one first in order — the older, for a
56    /// body described in the order its faces were made — and then every
57    /// vertex that only two edges of the same two faces meet at dropped,
58    /// the two edges made one, the first in order. Also says where each
59    /// entity went.
60    ///
61    /// Two faces are asked whether they lie on one surface only if
62    /// `edge_may_go` lets every edge between them go, and two edges are
63    /// joined only if `vertex_may_go` lets the vertex between them go. A
64    /// boolean lets go what it left behind itself, so that an edge a step
65    /// drew on purpose stays; an edit lets go all.
66    pub fn merged(
67        &self,
68        edge_may_go: impl Fn(usize) -> bool,
69        vertex_may_go: impl Fn(usize) -> bool,
70    ) -> GeopResult<(BodySpec, Merged)> {
71        let mut work = Work::new(self);
72        while work.merge_one_pair(&edge_may_go)? {}
73        while work.join_one_pair(&vertex_may_go)? {}
74        work.compacted()
75    }
76}
77
78/// A body being simplified: the original's entities, by their index there,
79/// each gone or changed.
80struct Work<'a> {
81    spec: &'a BodySpec,
82    /// Per face: what it is now, none if made part of another.
83    faces: Vec<Option<FaceSpec>>,
84    /// Per face: the face it is part of.
85    face_into: Vec<usize>,
86    /// Per edge: what it is now, none if gone.
87    edges: Vec<Option<EdgeSpec>>,
88    /// Per edge: the edge it is part of, none if it went between two faces
89    /// made one.
90    edge_into: Vec<Option<usize>>,
91    /// Pairs of faces found not to make one face, as they are: asked again
92    /// only once one of them changes.
93    apart: std::collections::HashSet<(usize, usize)>,
94}
95
96impl<'a> Work<'a> {
97    fn new(spec: &'a BodySpec) -> Self {
98        Self {
99            spec,
100            faces: spec.faces.iter().cloned().map(Some).collect(),
101            face_into: (0..spec.faces.len()).collect(),
102            edges: spec.edges.iter().cloned().map(Some).collect(),
103            edge_into: (0..spec.edges.len()).map(Some).collect(),
104            apart: Default::default(),
105        }
106    }
107
108    /// Per edge, the faces using it, once per use.
109    fn uses(&self) -> Vec<Vec<usize>> {
110        let mut uses = vec![Vec::new(); self.edges.len()];
111        for (f, face) in self.faces.iter().enumerate() {
112            let Some(face) = face else { continue };
113            for c in face.loops().into_iter().flatten() {
114                if let CoedgeOn::Edge(e, _) = c.on {
115                    uses[e].push(f);
116                }
117            }
118        }
119        uses
120    }
121
122    /// The vertex a coedge running along `on` starts at, and the one it
123    /// ends at, in the body as it is now.
124    fn ends(&self, on: CoedgeOn) -> (usize, usize) {
125        match on {
126            CoedgeOn::Edge(e, sense) => {
127                let edge = self.edges[e]
128                    .as_ref()
129                    .expect("a coedge runs along a live edge");
130                match sense {
131                    Sense::Forward => (edge.start, edge.end),
132                    Sense::Reversed => (edge.end, edge.start),
133                }
134            }
135            CoedgeOn::Vertex(v) => (v, v),
136        }
137    }
138
139    /// Makes one pair of faces across an edge, on one surface, one face.
140    /// Whether it found one.
141    fn merge_one_pair(&mut self, edge_may_go: &impl Fn(usize) -> bool) -> GeopResult<bool> {
142        let uses = self.uses();
143        let pair = |f: usize, g: usize| (f.min(g), f.max(g));
144        // Faces with an edge between them that has to stay.
145        let held: std::collections::HashSet<(usize, usize)> = (uses.iter().enumerate())
146            .filter_map(|(e, faces)| match faces.as_slice() {
147                [f, g] if !edge_may_go(e) => Some(pair(*f, *g)),
148                _ => None,
149            })
150            .collect();
151        for faces in &uses {
152            let [f, g] = faces.as_slice() else { continue };
153            if f == g {
154                continue;
155            }
156            let (keep, gone) = pair(*f, *g);
157            if held.contains(&(keep, gone)) || self.apart.contains(&(keep, gone)) {
158                continue;
159            }
160            let Some(face) = self.merged_face(keep, gone, &uses)? else {
161                self.apart.insert((keep, gone));
162                continue;
163            };
164            {
165                self.apart.retain(|&(a, b)| a != keep && b != keep);
166                self.faces[keep] = Some(face);
167                self.faces[gone] = None;
168                for into in self.face_into.iter_mut() {
169                    if *into == gone {
170                        *into = keep;
171                    }
172                }
173                for (e, faces) in uses.iter().enumerate() {
174                    if faces.len() == 2 && faces.iter().all(|&h| h == keep || h == gone) {
175                        self.edges[e] = None;
176                        self.edge_into[e] = None;
177                    }
178                }
179                return Ok(true);
180            }
181        }
182        Ok(false)
183    }
184
185    /// The faces `keep` and `gone` as one, if they lie on one surface and
186    /// their loops close up as the loops of one face on it.
187    fn merged_face(
188        &self,
189        keep: usize,
190        gone: usize,
191        uses: &[Vec<usize>],
192    ) -> GeopResult<Option<FaceSpec>> {
193        SURFACES_COMPARED.set(SURFACES_COMPARED.get() + 1);
194        let (a, b) = (
195            self.faces[keep].as_ref().expect("live"),
196            self.faces[gone].as_ref().expect("live"),
197        );
198        // On the surface of the one that carries the other, its pcurves
199        // as they are and the other's mapped onto it.
200        let (surface, coedges) = if let Some((surface, map)) = a.surface.carrying(&b.surface)? {
201            (surface, self.coedges(a, b, |c| map.apply_to(c))?)
202        } else if let Some((surface, map)) = b.surface.carrying(&a.surface)? {
203            (surface, self.coedges(b, a, |c| map.apply_to(c))?)
204        } else {
205            return Ok(None);
206        };
207        // Coedges along the edges between the two go.
208        let inside = |c: &CoedgeSpec| match c.on {
209            CoedgeOn::Edge(e, _) => {
210                uses[e].len() == 2 && uses[e].iter().all(|&h| h == keep || h == gone)
211            }
212            CoedgeOn::Vertex(_) => false,
213        };
214        let coedges: Vec<CoedgeSpec> = coedges.into_iter().filter(|c| !inside(c)).collect();
215        let Some(loops) = self.loops(coedges)? else {
216            return Ok(None);
217        };
218        let mut outer = None;
219        let mut holes = Vec::new();
220        for lp in loops {
221            let area = loop_signed_area(lp.iter().map(|c| &c.pcurve))?;
222            if area.definitely_greater(Scalar::ZERO) && outer.is_none() {
223                outer = Some(lp);
224            } else if area.definitely_less(Scalar::ZERO) {
225                holes.push(lp);
226            } else {
227                return Ok(None);
228            }
229        }
230        Ok(outer.map(|outer| FaceSpec {
231            surface,
232            outer,
233            holes,
234        }))
235    }
236
237    /// Every coedge of `first` as it is, and of `second` with its pcurve
238    /// mapped by `map`.
239    fn coedges(
240        &self,
241        first: &FaceSpec,
242        second: &FaceSpec,
243        map: impl Fn(&crate::Curve2) -> GeopResult<crate::Curve2>,
244    ) -> GeopResult<Vec<CoedgeSpec>> {
245        let mut out: Vec<CoedgeSpec> = first.loops().into_iter().flatten().cloned().collect();
246        for c in second.loops().into_iter().flatten() {
247            out.push(CoedgeSpec {
248                on: c.on,
249                pcurve: map(&c.pcurve)?,
250            });
251        }
252        Ok(out)
253    }
254
255    /// `coedges` followed end to end into loops, if they close up as one
256    /// face's: one coedge leaving each vertex, each meeting the next where
257    /// it starts in `(u, v)` too.
258    fn loops(&self, coedges: Vec<CoedgeSpec>) -> GeopResult<Option<Vec<Vec<CoedgeSpec>>>> {
259        // A pole's row runs from its vertex to that vertex: the loop goes
260        // along it on arriving there, and only then on along the coedge
261        // leaving it.
262        let mut leaving: std::collections::HashMap<usize, usize> = Default::default();
263        let mut rows: std::collections::HashMap<usize, usize> = Default::default();
264        for (i, c) in coedges.iter().enumerate() {
265            let at = if matches!(c.on, CoedgeOn::Vertex(_)) {
266                &mut rows
267            } else {
268                &mut leaving
269            };
270            if at.insert(self.ends(c.on).0, i).is_some() {
271                return Ok(None);
272            }
273        }
274        let mut used = vec![false; coedges.len()];
275        let mut loops = Vec::new();
276        for first in 0..coedges.len() {
277            if used[first] {
278                continue;
279            }
280            let mut lp = Vec::new();
281            let mut at = first;
282            loop {
283                if used[at] {
284                    if at != first {
285                        return Ok(None);
286                    }
287                    break;
288                }
289                used[at] = true;
290                lp.push(at);
291                let arrives = self.ends(coedges[at].on).1;
292                let along_row = matches!(coedges[at].on, CoedgeOn::Vertex(_));
293                let next = match (rows.get(&arrives), leaving.get(&arrives)) {
294                    (Some(&row), _) if !along_row => row,
295                    (_, Some(&next)) => next,
296                    (Some(&row), None) => row,
297                    (None, None) => return Ok(None),
298                };
299                let (end, start) = (coedges[at].pcurve.ends()?.1, coedges[next].pcurve.ends()?.0);
300                if !end.could_be_equal(&start) {
301                    return Ok(None);
302                }
303                at = next;
304            }
305            loops.push(lp.into_iter().map(|i| coedges[i].clone()).collect());
306        }
307        Ok(Some(loops))
308    }
309
310    /// Makes one pair of edges that only meet each other at a vertex, and
311    /// run between the same two faces, one edge. Whether it found one.
312    fn join_one_pair(&mut self, vertex_may_go: &impl Fn(usize) -> bool) -> GeopResult<bool> {
313        let uses = self.uses();
314        let n = self.spec.vertices.len();
315        let mut at_vertex: Vec<Vec<usize>> = vec![Vec::new(); n];
316        for (e, edge) in self.edges.iter().enumerate() {
317            let Some(edge) = edge else { continue };
318            at_vertex[edge.start].push(e);
319            at_vertex[edge.end].push(e);
320        }
321        let mut pole = vec![false; n];
322        for face in self.faces.iter().flatten() {
323            for c in face.loops().into_iter().flatten() {
324                if let CoedgeOn::Vertex(v) = c.on {
325                    pole[v] = true;
326                }
327            }
328        }
329        for v in 0..n {
330            let [e1, e2] = at_vertex[v].as_slice() else {
331                continue;
332            };
333            let (e1, e2) = ((*e1).min(*e2), (*e1).max(*e2));
334            if e1 == e2 || pole[v] || !vertex_may_go(v) {
335                continue;
336            }
337            let (one, two) = (
338                self.edges[e1].as_ref().unwrap(),
339                self.edges[e2].as_ref().unwrap(),
340            );
341            let far = |edge: &EdgeSpec| {
342                if edge.start == v {
343                    edge.end
344                } else {
345                    edge.start
346                }
347            };
348            if far(one) == far(two) || one.start == one.end || two.start == two.end {
349                continue;
350            }
351            let mut faces = (uses[e1].clone(), uses[e2].clone());
352            faces.0.sort();
353            faces.1.sort();
354            if faces.0 != faces.1 || faces.0.len() != 2 || faces.0[0] == faces.0[1] {
355                continue;
356            }
357            // Two edges meeting at a corner are no one edge: joined, the
358            // curve would turn there on a knot, its tangent lost.
359            let tangent_at_v = |edge: &EdgeSpec| {
360                let (t0, t1) = edge.curve.domain();
361                edge.curve.tangent(if edge.start == v { t0 } else { t1 })
362            };
363            if !tangent_at_v(one)?.could_be_parallel(&tangent_at_v(two)?) {
364                continue;
365            }
366            if self.join(v, e1, e2)? {
367                return Ok(true);
368            }
369        }
370        Ok(false)
371    }
372
373    /// The edges `e1` and `e2`, meeting at `v` only, made one, `e1`; every
374    /// loop's two coedges there one, their pcurves joined. Whether the
375    /// curves could be joined.
376    fn join(&mut self, v: usize, e1: usize, e2: usize) -> GeopResult<bool> {
377        let (one, two) = (
378            self.edges[e1].clone().unwrap(),
379            self.edges[e2].clone().unwrap(),
380        );
381        // Along `e1`, through `v`, on along `e2`, or the other way round.
382        let toward_v = |edge: &EdgeSpec| {
383            if edge.end == v {
384                edge.curve.clone()
385            } else {
386                edge.curve.reverse()
387            }
388        };
389        let (curve, start, end) = if one.end == v {
390            let from_v = toward_v(&two).reverse();
391            (
392                one.curve.joined(&from_v),
393                one.start,
394                if two.start == v { two.end } else { two.start },
395            )
396        } else {
397            let into_v = toward_v(&two);
398            (
399                into_v.joined(&one.curve),
400                if two.end == v { two.start } else { two.end },
401                one.end,
402            )
403        };
404        let Ok(curve) = curve else {
405            return Ok(false);
406        };
407        // Each loop through `v`: its two coedges there, in order, as one.
408        let mut faces = self.faces.clone();
409        for face in faces.iter_mut().flatten() {
410            let mut loops: Vec<Vec<CoedgeSpec>> = face.loops().into_iter().cloned().collect();
411            for lp in &mut loops {
412                let n = lp.len();
413                let on = |c: &CoedgeSpec, e: usize| matches!(c.on, CoedgeOn::Edge(x, _) if x == e);
414                let Some(k) = (0..n).find(|&k| {
415                    let next = &lp[(k + 1) % n];
416                    (on(&lp[k], e1) && on(next, e2)) || (on(&lp[k], e2) && on(next, e1))
417                }) else {
418                    continue;
419                };
420                let (first, second) = (lp[k].clone(), lp[(k + 1) % n].clone());
421                let Ok(pcurve) = first.pcurve.joined(&second.pcurve) else {
422                    return Ok(false);
423                };
424                // Running from the vertex `first` starts at to the one
425                // `second` ends at: the joined edge forward if that is its
426                // start.
427                let from = self.ends(first.on).0;
428                let sense = if from == start {
429                    Sense::Forward
430                } else if from == end {
431                    Sense::Reversed
432                } else {
433                    return Err(GeopError::new(format!(
434                        "BodySpec::merged: a loop runs edges {e1} and {e2} from vertex {from}, which neither ends"
435                    )));
436                };
437                let joined = CoedgeSpec {
438                    on: CoedgeOn::Edge(e1, sense),
439                    pcurve,
440                };
441                if k + 1 < n {
442                    lp.splice(k..k + 2, [joined]);
443                } else {
444                    // The pair wraps round the loop's start.
445                    lp.remove(k);
446                    lp[0] = joined;
447                }
448            }
449            let outer = loops.remove(0);
450            *face = FaceSpec {
451                surface: face.surface.clone(),
452                outer,
453                holes: loops,
454            };
455        }
456        self.faces = faces;
457        self.edges[e1] = Some(EdgeSpec { curve, start, end });
458        self.edges[e2] = None;
459        for into in self.edge_into.iter_mut() {
460            if *into == Some(e2) {
461                *into = Some(e1);
462            }
463        }
464        Ok(true)
465    }
466
467    /// The body as it is now, its entities numbered anew, and where each of
468    /// the original's went.
469    fn compacted(self) -> GeopResult<(BodySpec, Merged)> {
470        let spec = self.spec;
471        let mut face_index = vec![None; spec.faces.len()];
472        let mut faces = Vec::new();
473        for (f, face) in self.faces.iter().enumerate() {
474            if let Some(face) = face {
475                face_index[f] = Some(faces.len());
476                faces.push(face.clone());
477            }
478        }
479        let mut vertex_index = vec![None; spec.vertices.len()];
480        let mut vertices = Vec::new();
481        let mut vertex = |v: usize, vertices: &mut Vec<_>| {
482            *vertex_index[v].get_or_insert_with(|| {
483                vertices.push(spec.vertices[v]);
484                vertices.len() - 1
485            })
486        };
487        let mut edge_index = vec![None; spec.edges.len()];
488        let mut edges = Vec::new();
489        for (e, edge) in self.edges.iter().enumerate() {
490            if let Some(edge) = edge {
491                edge_index[e] = Some(edges.len());
492                edges.push(EdgeSpec {
493                    curve: edge.curve.clone(),
494                    start: vertex(edge.start, &mut vertices),
495                    end: vertex(edge.end, &mut vertices),
496                });
497            }
498        }
499        for face in &mut faces {
500            let mut renumber = |lp: &mut Vec<CoedgeSpec>, vertices: &mut Vec<_>| {
501                for c in lp {
502                    c.on = match c.on {
503                        CoedgeOn::Edge(e, sense) => {
504                            CoedgeOn::Edge(edge_index[e].expect("a live edge"), sense)
505                        }
506                        CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertex(v, vertices)),
507                    };
508                }
509            };
510            renumber(&mut face.outer, &mut vertices);
511            for hole in &mut face.holes {
512                renumber(hole, &mut vertices);
513            }
514        }
515        let shells = (spec.shells.iter())
516            .map(|members| {
517                let mut kept: Vec<usize> = members.iter().filter_map(|&f| face_index[f]).collect();
518                kept.dedup();
519                kept
520            })
521            .collect();
522        let out = BodySpec {
523            vertices,
524            edges,
525            faces,
526            shells,
527            solid: spec.solid,
528        };
529        out.check()?;
530        let merged = Merged {
531            faces: (self.face_into.iter())
532                .map(|&f| face_index[f].expect("a face is part of a live one"))
533                .collect(),
534            edges: (self.edge_into.iter())
535                .map(|e| e.and_then(|e| edge_index[e]))
536                .collect(),
537            vertices: vertex_index,
538        };
539        Ok((out, merged))
540    }
541}
542
543#[cfg(test)]
544mod tests {
545
546    use crate::{
547        Model,
548        test_fixtures::test_cube_solid,
549        validation::{ValidationParameters, validate},
550    };
551
552    /// A cube is as simple as it gets: merged, it is the same.
553    #[test]
554    fn a_cube_stays_a_cube() {
555        let mut model = Model::new();
556        let solid = test_cube_solid(&mut model);
557        let faces = model.solid_faces(solid).unwrap();
558        let (spec, _) = model.body_spec(&faces, true).unwrap();
559        let (merged, map) = spec.merged(|_| true, |_| true).unwrap();
560        assert_eq!(merged.faces.len(), 6);
561        assert_eq!(merged.edges.len(), 12);
562        assert_eq!(merged.vertices.len(), 8);
563        assert_eq!(map.faces, (0..6).collect::<Vec<_>>());
564        let mut model = Model::new();
565        model.build_body(merged).unwrap();
566        validate(&ValidationParameters::default(), &model).unwrap();
567    }
568}