Skip to main content

geop_core_topology/edit/
assemble_solid.rs

1use std::collections::{HashMap, HashSet};
2
3use imbl::OrdMap;
4
5use crate::{
6    Body, CoedgeGeometry, CoedgeId, EdgeId, FaceId, Model, Shell, ShellId, Solid, SolidId,
7    VertexId, boundary::BoundaryType,
8};
9use geop_core_math::{
10    geop_error::{GeopError, GeopResult},
11    union_find::UnionFind,
12};
13
14impl Model {
15    /// Replace the bodies `consumed` by one new solid bounded by `keep`,
16    /// which must be faces of those bodies: a shell for each connected set
17    /// of them, faces sharing an edge being connected. A boolean that cuts
18    /// a bar in two, or leaves a void inside a block, so keeps two shells,
19    /// each a closed surface of its own (see [`Solid`]). Every other face
20    /// the bodies owned is deleted, and so is everything no face reaches any
21    /// more.
22    ///
23    /// What a boolean does last, once it has decided which faces survive.
24    /// `Ok(None)` (and no solid) when `keep` is empty: the result is empty,
25    /// which is an answer rather than a failure — and also how a body is
26    /// deleted outright.
27    ///
28    /// Deletion is by reachability rather than by tracking what was split or
29    /// re-homed on the way here, so no dangling id can be left behind — and
30    /// it only ever deletes from what `consumed` owned: faces of any other
31    /// body in the model are none of this operation's business.
32    pub fn assemble_solid(
33        &mut self,
34        consumed: &[Body],
35        keep: &[FaceId],
36    ) -> GeopResult<Option<SolidId>> {
37        let shells = self.assemble_shells(consumed, &connected(self, keep))?;
38        if shells.is_empty() {
39            return Ok(None);
40        }
41        let solid_id = self.insert_solid(Solid {
42            shells: shells.clone(),
43        });
44        for shell_id in shells {
45            self.get_shell_mut(shell_id)?.solid = Some(solid_id);
46        }
47        Ok(Some(solid_id))
48    }
49
50    /// Like [`Model::assemble_solid`], but `keep` becomes a sheet — faces
51    /// standing on their own, in one shell whether connected or not —
52    /// rather than a solid: what is left of a sheet once it is trimmed.
53    pub fn assemble_sheet(
54        &mut self,
55        consumed: &[Body],
56        keep: &[FaceId],
57    ) -> GeopResult<Option<ShellId>> {
58        let groups = if keep.is_empty() {
59            Vec::new()
60        } else {
61            vec![keep.to_vec()]
62        };
63        Ok(self.assemble_shells(consumed, &groups)?.first().copied())
64    }
65
66    /// [`Model::assemble_solid`]'s work, short of making the shells of
67    /// `groups` (one shell each) a solid's.
68    fn assemble_shells(
69        &mut self,
70        consumed: &[Body],
71        groups: &[Vec<FaceId>],
72    ) -> GeopResult<Vec<ShellId>> {
73        let mut consumed_faces: HashSet<FaceId> = HashSet::new();
74        for &body in consumed {
75            consumed_faces.extend(self.body_faces(body)?);
76        }
77        if let Some(face) = groups
78            .iter()
79            .flatten()
80            .find(|f| !consumed_faces.contains(f))
81        {
82            return Err(GeopError::new(format!(
83                "Model::assemble_solid: face {face} is to be kept, but belongs to none of the consumed bodies {consumed:?}"
84            )));
85        }
86        for &body in consumed {
87            match body {
88                Body::Solid(solid) => {
89                    for shell in self.get_solid(solid)?.shells.clone() {
90                        self.shells.remove(&shell);
91                    }
92                    self.solids.remove(&solid);
93                }
94                Body::Sheet(shell) => {
95                    self.shells.remove(&shell);
96                }
97            }
98        }
99
100        let kept: HashSet<FaceId> = groups.iter().flatten().copied().collect();
101        retain(&mut self.faces, |id| {
102            !consumed_faces.contains(id) || kept.contains(id)
103        });
104
105        let mut shells = Vec::with_capacity(groups.len());
106        for faces in groups {
107            let shell_id = self.insert_shell(Shell {
108                faces: faces.clone(),
109                solid: None,
110            });
111            for &face_id in faces {
112                self.get_face_mut(face_id)?.shell = shell_id;
113            }
114            shells.push(shell_id);
115        }
116        self.prune_unreachable();
117        Ok(shells)
118    }
119
120    /// Move every shell of `from` into `into`, deleting `from`: one solid of
121    /// both bodies. Only valid for bodies that don't touch — nothing is
122    /// intersected, so two overlapping ones would make a solid whose shells
123    /// cross; combining those is a boolean union's job.
124    pub fn merge_solids(&mut self, into: SolidId, from: SolidId) -> GeopResult<()> {
125        if into == from {
126            return Err(GeopError::new(format!(
127                "Model::merge_solids: refusing to merge solid {into} into itself"
128            )));
129        }
130        self.get_solid(into)?;
131        let shells = self.get_solid(from)?.shells.clone();
132        for &shell in &shells {
133            self.get_shell_mut(shell)?.solid = Some(into);
134        }
135        self.solids.remove(&from);
136        self.get_solid_mut(into)?.shells.extend(shells);
137        Ok(())
138    }
139
140    /// Drop everything no longer reachable from a face or a wire: the
141    /// coedges no face owns, then edges and vertices nothing refers to any
142    /// more.
143    pub(crate) fn prune_unreachable(&mut self) {
144        let live_faces: Vec<FaceId> = self.faces.keys().copied().collect();
145        let live_coedges: HashSet<CoedgeId> = live_faces
146            .iter()
147            .flat_map(|&f| self.iterate_face_coedges(f).collect::<Vec<_>>())
148            .collect();
149        retain(&mut self.coedges, |id| live_coedges.contains(id));
150
151        let live_edges: HashSet<EdgeId> = self
152            .coedges
153            .values()
154            .filter_map(|c| c.edge().ok())
155            .chain(self.wires.values().flat_map(|w| w.edges.iter().copied()))
156            .collect();
157        retain(&mut self.edges, |id| live_edges.contains(id));
158
159        let mut live_vertices: HashSet<VertexId> = self
160            .edges
161            .values()
162            .flat_map(|e| [e.start_vertex, e.end_vertex])
163            .collect();
164        for coedge in self.coedges.values() {
165            if let CoedgeGeometry::Vertex(v) = coedge.geometry {
166                live_vertices.insert(v);
167            }
168        }
169        for face in self.faces.values() {
170            for boundary in face.boundaries() {
171                if let BoundaryType::Vertex(v) = boundary {
172                    live_vertices.insert(v);
173                }
174            }
175        }
176        for wire in self.wires.values() {
177            live_vertices.extend(wire.vertices.iter().copied());
178        }
179        retain(&mut self.vertices, |id| live_vertices.contains(id));
180    }
181}
182
183/// `faces` in sets connected by the edges they share, each in the order
184/// given, the sets ordered by their first face.
185fn connected(model: &Model, faces: &[FaceId]) -> Vec<Vec<FaceId>> {
186    let mut sets = UnionFind::new(faces.len());
187    let mut first_user: HashMap<EdgeId, usize> = HashMap::new();
188    for (i, &face) in faces.iter().enumerate() {
189        for coedge in model.iterate_face_coedges(face) {
190            let Some(Ok(edge)) = model.coedges.get(&coedge).map(|c| c.edge()) else {
191                continue;
192            };
193            match first_user.get(&edge) {
194                Some(&j) => sets.union(i, j),
195                None => {
196                    first_user.insert(edge, i);
197                }
198            }
199        }
200    }
201    let mut groups = sets.groups();
202    for group in &mut groups {
203        group.sort();
204    }
205    groups.sort_by_key(|g| g[0]);
206    groups
207        .into_iter()
208        .map(|g| g.into_iter().map(|i| faces[i]).collect())
209        .collect()
210}
211
212/// Removes the entries of `map` whose ids `keep` rejects, touching nothing
213/// else, so that a copy sharing the map shares what is kept.
214fn retain<K: Ord + Clone, V: Clone>(map: &mut OrdMap<K, V>, keep: impl Fn(&K) -> bool) {
215    let dropped: Vec<K> = map.keys().filter(|id| !keep(id)).cloned().collect();
216    for id in dropped {
217        map.remove(&id);
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use crate::{Model, test_fixtures::test_cube_solid};
224
225    /// A copy of a model shares its arenas, and pruning what is all reachable
226    /// touches none of them: a version made by editing a model costs what the
227    /// edit touches, not the model's size.
228    #[test]
229    fn pruning_a_copy_keeps_it_shared() {
230        let mut model = Model::new();
231        test_cube_solid(&mut model);
232        let copy = model.clone();
233        model.prune_unreachable();
234        assert!(model.vertices.ptr_eq(&copy.vertices));
235        assert!(model.edges.ptr_eq(&copy.edges));
236        assert!(model.coedges.ptr_eq(&copy.coedges));
237        assert!(model.faces.ptr_eq(&copy.faces));
238    }
239}