1use std::collections::{HashMap, HashSet};
20
21use geop_core_math::{
22 geop_error::{GeopError, GeopResult, WithContext},
23 union_find::UnionFind,
24 with_context,
25};
26use geop_core_topology::{
27 Body, CoedgeGeometry, EdgeId, FaceId, ShellId, SolidId,
28 build::{BodySpec, BuiltBody},
29 contains::shell::Classified,
30};
31use geop_ops_model::{BodyNames, NamedModel, Namer, RefId};
32
33use crate::{
34 boolean::{FaceClassification, classify_face},
35 remesh::{RemeshParams, remesh},
36};
37
38pub fn split(
55 model: &mut NamedModel,
56 namer: &Namer,
57 solid: SolidId,
58 sheet: ShellId,
59 params: RemeshParams,
60) -> GeopResult<Vec<SolidId>> {
61 let ctx = with_context!("split(name={}, solid={solid}, sheet={sheet})", namer.root());
62
63 let sheet_faces = model
65 .topology()
66 .body_faces(Body::Sheet(sheet))
67 .with_context(&ctx)?;
68 let cutter = model
69 .copy_faces(&sheet_faces, None, |name| namer.name(&[name]))
70 .and_then(|built| built.body())
71 .with_context(&ctx)?;
72
73 remesh(model, namer, solid, cutter, params).with_context(&ctx)?;
74 let topology = model.topology();
75 let solid_faces = topology.solid_faces(solid).with_context(&ctx)?;
76 let mut cuts = Vec::new();
77 let mut classified = Classified::default();
78 for face in topology.body_faces(cutter).with_context(&ctx)? {
79 let class = classify_face(topology, face, solid, params, &mut classified)
80 .with_context(&ctx)
81 .with_context(with_context!("classifying face {face}"))?;
82 if class == FaceClassification::Inside {
83 cuts.push(face);
84 }
85 }
86 if cuts.is_empty() {
87 return Err(ctx(GeopError::new(
88 "split: the face does not cut through the solid",
89 )));
90 }
91
92 let n = solid_faces.len();
95 let k = cuts.len();
96 let side: HashMap<FaceId, usize> = solid_faces
97 .iter()
98 .chain(&cuts)
99 .enumerate()
100 .map(|(i, &f)| (f, i))
101 .collect();
102 let turned = |i: usize| i + k;
103 let mut pieces = UnionFind::new(n + 2 * k);
104 let mut users: HashMap<EdgeId, Vec<(usize, geop_core_topology::Sense)>> = HashMap::new();
105 for (i, &face) in solid_faces.iter().chain(&cuts).enumerate() {
108 for coedge in topology.iterate_face_coedges(face) {
109 let coedge = topology.get_coedge(coedge)?;
110 if let CoedgeGeometry::Edge(edge) = coedge.geometry {
111 users.entry(edge).or_default().push((i, coedge.sense));
112 }
113 }
114 }
115 let sides_of = |i: usize| if i < n { solid_faces[i] } else { cuts[i - n] };
116 let mut edges: Vec<_> = users.into_iter().collect();
117 edges.sort_by_key(|(e, _)| e.0);
118 for (edge, uses) in edges {
119 let (on_solid, on_cut): (Vec<_>, Vec<_>) = uses.into_iter().partition(|&(i, _)| i < n);
120 match (&on_solid[..], &on_cut[..]) {
121 ([(a, _), (b, _)], []) => pieces.union(*a, *b),
122 ([(a, sense_a), (b, _)], [(cut, sense)]) => {
124 let (with, against) = if sense_a != sense { (*a, *b) } else { (*b, *a) };
125 pieces.union(*cut, with);
126 pieces.union(turned(*cut), against);
127 }
128 ([], [(c, sense_c), (d, sense_d)]) => {
130 if sense_c != sense_d {
131 pieces.union(*c, *d);
132 pieces.union(turned(*c), turned(*d));
133 } else {
134 pieces.union(*c, turned(*d));
135 pieces.union(turned(*c), *d);
136 }
137 }
138 ([], [(cut, _)]) => {
139 let users: Vec<_> = (topology.coedges_of_edge(edge).iter())
140 .filter_map(|&c| topology.get_coedge(c).ok())
141 .map(|c| model.name_of(c.face).unwrap_or("?").to_string())
142 .collect();
143 return Err(ctx(GeopError::new(format!(
144 "split: the face ends inside the solid, at edge {edge} ({:?}) of the cut {:?}, used by the faces {users:?}; it must cut all the way through",
145 model.name_of(edge),
146 model.name_of(sides_of(*cut)),
147 ))));
148 }
149 _ => {
150 return Err(ctx(GeopError::new(format!(
151 "split: {} face(s) of the solid and {} of the cut meet at edge {edge}, which does not divide the solid into pieces",
152 on_solid.len(),
153 on_cut.len()
154 ))));
155 }
156 }
157 }
158
159 let groups = pieces.groups();
160 let piece_of: HashMap<usize, usize> = groups
161 .iter()
162 .enumerate()
163 .flat_map(|(g, members)| members.iter().map(move |&m| (m, g)))
164 .collect();
165 if let Some(&cut) = cuts
166 .iter()
167 .find(|c| piece_of[&side[c]] == piece_of[&turned(side[c])])
168 {
169 return Err(ctx(GeopError::new(format!(
170 "split: the solid stays connected around the cut face {cut}: the face does not divide it into pieces"
171 ))));
172 }
173
174 let sides: Vec<FaceId> = solid_faces
177 .iter()
178 .chain(&cuts)
179 .chain(&cuts)
180 .copied()
181 .collect();
182 let mut described: Vec<(String, BodySpec, Vec<RefId>)> = Vec::new();
183 for members in &groups {
184 let faces: Vec<FaceId> = members.iter().map(|&m| sides[m]).collect();
185 let (mut spec, sources) = topology.body_spec(&faces, true).with_context(&ctx)?;
186 for (face, &m) in spec.faces.iter_mut().zip(members) {
187 if m >= n + k {
188 *face = face.reversed();
189 }
190 }
191 let first = members
192 .iter()
193 .filter(|&&m| m < n)
194 .map(|&m| model.name(solid_faces[m]).map(str::to_string))
195 .collect::<GeopResult<Vec<_>>>()?
196 .into_iter()
197 .min()
198 .ok_or_else(|| ctx(GeopError::new("split: a piece has no face of the solid")))?;
199 let entities = sources
200 .vertices
201 .iter()
202 .map(|&v| RefId::from(v))
203 .chain(sources.edges.iter().map(|&e| e.into()))
204 .chain(sources.faces.iter().map(|&f| f.into()))
205 .collect();
206 described.push((first, spec, entities));
207 }
208 described.sort_by(|a, b| a.0.cmp(&b.0));
209
210 let mut taken: HashSet<RefId> = HashSet::new();
212 let mut named: Vec<(BodySpec, BodyNames)> = Vec::new();
213 for (index, (_, spec, entities)) in described.into_iter().enumerate() {
214 let mut all = Vec::with_capacity(entities.len());
215 for id in &entities {
216 let original = model.name(*id).map(str::to_string)?;
217 all.push(if taken.insert(*id) {
218 original
219 } else {
220 namer.name(&[&original, &index.to_string()])
221 });
222 }
223 let (v, e) = (spec.vertices.len(), spec.edges.len());
224 let names = BodyNames {
225 vertices: all[..v].to_vec(),
226 edges: all[v..v + e].to_vec(),
227 faces: all[v + e..].to_vec(),
228 solid: Some(namer.name(&[&index.to_string()])),
229 };
230 named.push((spec, names));
231 }
232
233 model
234 .assemble_solid(&[solid.into(), cutter], &[], namer.root())
235 .with_context(&ctx)?;
236 named
237 .into_iter()
238 .map(|(spec, names)| {
239 let BuiltBody { solid, .. } = model.build_body(spec, names).with_context(&ctx)?;
240 Ok(solid.expect("built as a solid"))
241 })
242 .collect()
243}
244
245#[cfg(test)]
246mod tests {
247 use super::split;
248 use crate::remesh::RemeshParams;
249 use geop_core_math::{primitives::CoordinateSystem, scalars::Scalar, vector::Vector3};
250 use geop_core_topology::{
251 Body,
252 contains::shell::{Classified, PointClassification, solid_contains},
253 };
254 use geop_lib_sweep::{
255 SweepLoop, extrude, polyline,
256 shapes::{block, v2, v3},
257 };
258 use geop_ops_model::Profile;
259 use geop_ops_model::{NamedModel, Namer, testing::assert_valid};
260
261 fn cube_and_sheet(points: &[[f64; 2]]) -> (NamedModel, geop_core_topology::SolidId, Body) {
264 let mut model = NamedModel::new();
265 let cube = block(&mut model, "c", [-0.5; 3], [0.5; 3]);
266 let chain = polyline(&points.iter().map(|p| v2(p[0], p[1])).collect::<Vec<_>>()).unwrap();
267 let plane = CoordinateSystem::world_at(v3(0.0, 0.0, -1.0));
268 let namer = Namer::new("extrude", "cut").unwrap();
269 let built = extrude(
270 &mut model,
271 &namer,
272 None,
273 &plane,
274 &[Scalar::ZERO, Scalar::from_f64(2.0)],
275 &[SweepLoop::plain(Profile::open(chain))],
276 )
277 .unwrap();
278 (model, cube, built.body().unwrap())
279 }
280
281 fn holding(
283 model: &NamedModel,
284 pieces: &[geop_core_topology::SolidId],
285 p: Vector3,
286 ) -> Vec<usize> {
287 (0..pieces.len())
288 .filter(|&k| {
289 solid_contains(
290 model.topology(),
291 pieces[k],
292 p,
293 2000,
294 Scalar::from_f64(1e-7),
295 7,
296 &mut Classified::default(),
297 )
298 .unwrap()
299 == PointClassification::Inside
300 })
301 .collect()
302 }
303
304 #[test]
307 fn a_flat_sheet_cuts_a_cube_in_two() {
308 let (mut model, cube, sheet) = cube_and_sheet(&[[0.1, -1.0], [0.1, 1.0]]);
309 let Body::Sheet(shell) = sheet else {
310 unreachable!()
311 };
312 let namer = Namer::new("split", "s").unwrap();
313 let pieces = split(&mut model, &namer, cube, shell, RemeshParams::default()).unwrap();
314 assert_valid(&model);
315 assert_eq!(pieces.len(), 2);
316 assert_eq!(model.solid_names(), ["split(s,0)", "split(s,1)"]);
317 let (left, right) = (
318 holding(&model, &pieces, v3(-0.2, 0.0, 0.0)),
319 holding(&model, &pieces, v3(0.3, 0.0, 0.0)),
320 );
321 assert_eq!(left.len(), 1);
322 assert_eq!(right.len(), 1);
323 assert_ne!(left, right);
324 let faces = |k: usize| model.topology().solid_faces(pieces[k]).unwrap().len();
327 assert_eq!(faces(0) + faces(1), 6 + 4 + 2);
328 assert_eq!(model.topology().body_faces(sheet).unwrap().len(), 1);
329 }
330
331 #[test]
335 fn a_bent_sheet_cuts_a_slot_out_of_a_cube() {
336 let (mut model, cube, sheet) =
337 cube_and_sheet(&[[-0.2, 1.0], [-0.2, -0.2], [0.2, -0.2], [0.2, 1.0]]);
338 let Body::Sheet(shell) = sheet else {
339 unreachable!()
340 };
341 let namer = Namer::new("split", "s").unwrap();
342 let pieces = split(&mut model, &namer, cube, shell, RemeshParams::default()).unwrap();
343 assert_valid(&model);
344 assert_eq!(pieces.len(), 2);
345 let slot = holding(&model, &pieces, v3(0.0, 0.2, 0.0));
346 assert_eq!(slot.len(), 1);
347 for p in [v3(-0.4, 0.0, 0.0), v3(0.4, 0.0, 0.0), v3(0.0, -0.4, 0.0)] {
348 let rest = holding(&model, &pieces, p);
349 assert!(rest.len() == 1 && rest != slot, "{p:?}");
350 }
351 }
352
353 #[test]
355 fn a_sheet_beside_the_solid_cuts_nothing() {
356 let (mut model, cube, sheet) = cube_and_sheet(&[[2.0, -1.0], [2.0, 1.0]]);
357 let Body::Sheet(shell) = sheet else {
358 unreachable!()
359 };
360 let namer = Namer::new("split", "s").unwrap();
361 let error = split(&mut model, &namer, cube, shell, RemeshParams::default()).unwrap_err();
362 assert!(error.root_message().contains("does not cut"), "{error:?}");
363 }
364}