Skip to main content

geop_lib_shell/
delete.rs

1//! [`delete_faces`]: faces taken out of a solid, and what is left either
2//! open, a sheet ([`Heal::Open`]), or healed over by the faces around the
3//! gap grown until they meet ([`Heal::Extend`]).
4//!
5//! Healing is decided by topology, then built by [`rebuilt`]:
6//!
7//! - **A hole closed.** Faces that fill holes of their neighbours — a
8//!   boss standing on a face, a hole through it, a pocket — are bounded
9//!   by whole hole loops of the faces around them. Taking them out and
10//!   those holes away leaves the neighbours as they were otherwise, and
11//!   nothing moves.
12//! - **Faces grown over.** Faces between others — a chamfer, a fillet,
13//!   a chain of them, a cut-off corner — leave a gap bounded by sides on
14//!   their neighbours' loops, which close over it once they meet. Each
15//!   three of the neighbours meet at a corner, each two sharing two
16//!   corners along a new edge: a triangulation of the gap's polygon of
17//!   sides. A gap of three sides leaves one corner; of four, an edge
18//!   between the two faces across it; of more, a tree of edges. Which
19//!   faces meet is not decided by kind but by trying every way, from the
20//!   first the rebuild accepts: the others fail where they would make
21//!   faces meet that never do, as a box's two ends, or edges that turn
22//!   around.
23//!
24//! Anything else is refused, by name.
25
26use geop_core_geometry::{
27    nurb_curve::{NurbCurve2D, NurbCurve3D},
28    search::PROJECT_ITERATIONS,
29};
30use geop_core_math::{
31    geop_error::{GeopError, GeopResult, WithContext},
32    scalars::Scalar,
33    vector::{Vector2, Vector3},
34    with_context,
35};
36use geop_core_topology::{
37    Body, FaceId, Sense,
38    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
39};
40use geop_ops_model::{BodyNames, NamedModel, Namer};
41
42use crate::rebuild::rebuilt;
43
44/// What [`delete_faces`] leaves where the faces were.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum Heal {
47    /// Nothing: the solid is left open, a sheet of the faces that stay.
48    Open,
49    /// The faces around the gap, grown until they meet (see the module
50    /// docs).
51    Extend,
52}
53
54/// Takes the faces `faces` out of their solid of `part` and leaves the gap
55/// as `heal` says — see the module docs — and returns what is left: the
56/// healed solid, named `namer`'s root, or the sheet. Every face, edge and
57/// vertex that stays keeps its name; of two vertices made one, the one the
58/// face's loop reaches first.
59pub fn delete_faces(
60    part: &mut NamedModel,
61    namer: &Namer,
62    faces: &[FaceId],
63    whole_feature: bool,
64    heal: Heal,
65) -> GeopResult<Body> {
66    let ctx = with_context!(
67        "delete_faces({}, faces={faces:?}, heal={heal:?})",
68        namer.root()
69    );
70    let Some(&first) = faces.first() else {
71        return Err(GeopError::new("pick the faces to delete")).with_context(ctx);
72    };
73    let solid = part
74        .solid_of_face(first, "only faces of a solid can be deleted")
75        .with_context(ctx)?;
76    let model = part.topology();
77    let all = model.solid_faces(solid).with_context(ctx)?;
78    if let Some(&f) = faces.iter().find(|f| !all.contains(f)) {
79        return Err(GeopError::new(format!(
80            "face {} is not of the same solid as face {}: delete the faces of one solid at a time",
81            part.name(f)?,
82            part.name(first)?
83        )))
84        .with_context(ctx);
85    }
86    if model.get_solid(solid).with_context(ctx)?.shells.len() != 1 {
87        return Err(GeopError::new(
88            "the solid has a void: deleting its faces is not supported",
89        ))
90        .with_context(ctx);
91    }
92    let (spec, sources) = model.body_spec(&all, true).with_context(ctx)?;
93    let names = BodyNames {
94        solid: Some(namer.root()),
95        ..part.source_names(&sources)?
96    };
97    let mut deleted: Vec<bool> = sources.faces.iter().map(|f| faces.contains(f)).collect();
98    if whole_feature {
99        deleted = feature_of(&spec, &names, &deleted).with_context(ctx)?;
100    }
101    if deleted.iter().all(|&d| d) {
102        return Err(GeopError::new(
103            "every face of the solid is picked: delete the body instead",
104        ))
105        .with_context(ctx);
106    }
107    match heal {
108        Heal::Open => {
109            let mut edited = Edited::new(&spec, &names);
110            edited.delete = deleted;
111            let (sheet, sheet_names, _) = edited.compacted(false)?;
112            part.delete_bodies(&[solid.into()]).with_context(ctx)?;
113            part.build_body(sheet, sheet_names)
114                .with_context(ctx)?
115                .body()
116                .with_context(ctx)
117        }
118        Heal::Extend => {
119            let (healed, healed_names) =
120                healed(&spec, &names, namer, &deleted).with_context(ctx)?;
121            part.replace_with_solid(&[solid.into()], healed, healed_names)
122                .map(Body::Solid)
123                .with_context(ctx)
124        }
125    }
126}
127
128/// The faces of the features the faces marked `picked` are part of: every
129/// face reached from one of them across an edge that lies on no face's hole
130/// — a hole's wall and its bottom, a boss's wall and top — for a feature
131/// that fills holes of the faces around it, and so can be taken out with
132/// them (see the module docs). Refused, naming the face, where what is
133/// reached so is no such feature: a face of the body the features stand
134/// on, whose own holes stop the search.
135fn feature_of(spec: &BodySpec, names: &BodyNames, picked: &[bool]) -> GeopResult<Vec<bool>> {
136    let mut on_hole = vec![false; spec.edges.len()];
137    for face in &spec.faces {
138        for c in face.holes.iter().flatten() {
139            if let CoedgeOn::Edge(e, _) = c.on {
140                on_hole[e] = true;
141            }
142        }
143    }
144    let uses = spec.edge_uses();
145    let mut feature = picked.to_vec();
146    let mut todo: Vec<usize> = (0..picked.len()).filter(|&f| picked[f]).collect();
147    while let Some(f) = todo.pop() {
148        for c in spec.faces[f].loops().into_iter().flatten() {
149            let CoedgeOn::Edge(e, _) = c.on else { continue };
150            if on_hole[e] {
151                continue;
152            }
153            for &(g, ..) in &uses[e] {
154                if !feature[g] {
155                    feature[g] = true;
156                    todo.push(g);
157                }
158            }
159        }
160    }
161    if let Some(f) = (0..picked.len()).find(|&f| picked[f])
162        && feature.iter().all(|&inside| inside)
163    {
164        return Err(GeopError::new(format!(
165            "face {} is part of no feature filling a hole of the faces around it: every face of the solid is reached from it",
166            names.faces[f]
167        )));
168    }
169    // Its boundary has to be holes of the faces around it.
170    for (e, at) in uses.iter().enumerate() {
171        let [(f, ..), (g, ..)] = at.as_slice() else {
172            continue;
173        };
174        if feature[*f] == feature[*g] {
175            continue;
176        }
177        let (inside, outside) = if feature[*f] { (*f, *g) } else { (*g, *f) };
178        let on_outside_hole = (spec.faces[outside].holes.iter().flatten())
179            .any(|c| matches!(c.on, CoedgeOn::Edge(x, _) if x == e));
180        if !on_outside_hole {
181            return Err(GeopError::new(format!(
182                "face {} is part of no feature filling a hole of the faces around it: edge {} between it and face {} lies on no hole of that face",
183                names.faces[inside], names.edges[e], names.faces[outside]
184            )));
185        }
186    }
187    Ok(feature)
188}
189
190/// The solid `spec`, named `names`, with the faces marked `deleted` taken
191/// out and the gap healed over (see the module docs). What it creates is
192/// named by `namer`.
193fn healed(
194    spec: &BodySpec,
195    names: &BodyNames,
196    namer: &Namer,
197    deleted: &[bool],
198) -> GeopResult<(BodySpec, BodyNames)> {
199    // Every loop of a face that stays, and which of its coedges run along
200    // an edge of a face deleted.
201    let uses = spec.edge_uses();
202    let across = |e: usize| uses[e].iter().any(|&(f, ..)| deleted[f]);
203    let mut closed_holes: Vec<(usize, usize)> = Vec::new();
204    let mut crossing = false;
205    for (f, face) in spec.faces.iter().enumerate() {
206        if deleted[f] {
207            continue;
208        }
209        for (l, lp) in face.loops().into_iter().enumerate() {
210            let along = (lp.iter())
211                .filter(|c| matches!(c.on, CoedgeOn::Edge(e, _) if across(e)))
212                .count();
213            if along == 0 {
214                continue;
215            }
216            if l > 0 && along == lp.len() {
217                closed_holes.push((f, l - 1));
218            } else {
219                crossing = true;
220            }
221        }
222    }
223    let listed = || -> Vec<&str> {
224        (0..spec.faces.len())
225            .filter(|&f| deleted[f])
226            .map(|f| names.faces[f].as_str())
227            .collect()
228    };
229    if !crossing {
230        let mut edited = Edited::new(spec, names);
231        edited.delete = deleted.to_vec();
232        edited.closed = closed_holes;
233        return edited.compacted(true).map(|(spec, names, _)| (spec, names));
234    }
235    if !closed_holes.is_empty() {
236        return Err(GeopError::new(format!(
237            "the faces {:?} both fill holes and cross their neighbours' outer loops: healing over them at once is not supported",
238            listed()
239        )));
240    }
241    let sides = gap_sides(spec, names, deleted)?;
242    let n = sides.len();
243    if !(3..=MOST_SIDES).contains(&n) {
244        return Err(GeopError::new(format!(
245            "the gap the faces {:?} leave has {n} sides: healing over a gap of fewer than 3 or more than {MOST_SIDES} sides is not supported",
246            listed()
247        )));
248    }
249    let mut failures = Vec::new();
250    for triangles in triangulations(n) {
251        let tried = Grown::new(spec, names, namer, deleted, &sides, &triangles)
252            .and_then(|grown| grown.compacted())
253            .and_then(|(grown, grown_names, reseat)| {
254                let replaced = vec![false; grown.faces.len()];
255                Ok((
256                    rebuilt(&grown, &grown_names, &replaced, &reseat)?,
257                    grown_names,
258                ))
259            });
260        match tried {
261            Ok(done) => return Ok(done),
262            Err(e) => failures.push(format!("{triangles:?}: {}", e.root_message())),
263        }
264    }
265    let around: Vec<&str> = sides.iter().map(|s| names.faces[s.face].as_str()).collect();
266    Err(GeopError::new(format!(
267        "the faces around the gap, {around:?} in order, meet in none of the {} ways they could close it, each three of them at a corner — {}",
268        failures.len(),
269        failures.join("; ")
270    )))
271    .map_err(|e| e.with_context(format!("healing over the faces {:?}", listed())))
272}
273
274/// The most sides a gap may have to be healed over: every way its faces
275/// could close it is tried, and their number grows as the Catalan numbers
276/// (429 for nine sides).
277const MOST_SIDES: usize = 8;
278
279/// A side of the gap the faces deleted leave: the coedges of one loop of a
280/// face that stays, `face`'s loop `lp`, that run along edges of the faces
281/// deleted, one after the other — at positions `run` of the loop — from
282/// the vertex `start` to `end`.
283struct Side {
284    face: usize,
285    lp: usize,
286    run: Vec<usize>,
287    start: usize,
288    end: usize,
289}
290
291/// The sides of the gap the faces marked `deleted` leave, in order round
292/// it: each starts where the one before ends, at a corner of the gap.
293/// Refused, by name, where the gap has more than one boundary — the faces
294/// deleted have a hole, or enclose a face that stays — or touches itself
295/// at a vertex.
296fn gap_sides(spec: &BodySpec, names: &BodyNames, deleted: &[bool]) -> GeopResult<Vec<Side>> {
297    let uses = spec.edge_uses();
298    let across = |e: usize| uses[e].iter().any(|&(f, ..)| deleted[f]);
299    let mut sides = Vec::new();
300    for (f, face) in spec.faces.iter().enumerate() {
301        if deleted[f] {
302            continue;
303        }
304        for (l, lp) in face.loops().into_iter().enumerate() {
305            let along: Vec<bool> = (lp.iter())
306                .map(|c| matches!(c.on, CoedgeOn::Edge(e, _) if across(e)))
307                .collect();
308            let n = lp.len();
309            for k in 0..n {
310                if !along[k] || along[(k + n - 1) % n] {
311                    continue;
312                }
313                let mut run = Vec::new();
314                let mut j = k;
315                while along[j] && run.len() < n {
316                    run.push(j);
317                    j = (j + 1) % n;
318                }
319                let start = spec.ends(lp[run[0]].on)?.0;
320                let end = spec.ends(lp[run[run.len() - 1]].on)?.1;
321                sides.push(Side {
322                    face: f,
323                    lp: l,
324                    run,
325                    start,
326                    end,
327                });
328            }
329        }
330    }
331    // Chained, each from where the one before ends.
332    let mut ordered: Vec<Side> = Vec::with_capacity(sides.len());
333    let total = sides.len();
334    let Some(first) = sides.pop() else {
335        return Err(GeopError::new(
336            "the faces deleted border no face that stays",
337        ));
338    };
339    ordered.push(first);
340    loop {
341        let end = ordered[ordered.len() - 1].end;
342        let next: Vec<usize> = (0..sides.len())
343            .filter(|&k| sides[k].start == end)
344            .collect();
345        match next.as_slice() {
346            [] if end == ordered[0].start => break,
347            [k] => ordered.push(sides.swap_remove(*k)),
348            _ => {
349                return Err(GeopError::new(format!(
350                    "the gap the faces deleted leave touches itself at vertex {}: healing over it is not supported",
351                    names.vertices[end]
352                )));
353            }
354        }
355    }
356    if ordered.len() != total {
357        return Err(GeopError::new(format!(
358            "the gap the faces deleted leave has more than one boundary — faces {:?} lie along another: healing over it is not supported",
359            sides
360                .iter()
361                .map(|s| names.faces[s.face].as_str())
362                .collect::<Vec<_>>()
363        )));
364    }
365    Ok(ordered)
366}
367
368/// Every triangulation of the polygon of corners `0..n`: the ways the `n`
369/// faces around a gap can close it, each three of them meeting at a
370/// corner, each two not side by side along an edge.
371fn triangulations(n: usize) -> Vec<Vec<[usize; 3]>> {
372    fn of(corners: &[usize]) -> Vec<Vec<[usize; 3]>> {
373        if corners.len() < 3 {
374            return vec![Vec::new()];
375        }
376        let (first, last) = (corners[0], corners[corners.len() - 1]);
377        let mut out = Vec::new();
378        for m in 1..corners.len() - 1 {
379            for left in of(&corners[..=m]) {
380                for right in of(&corners[m..]) {
381                    let mut triangles = vec![[first, corners[m], last]];
382                    triangles.extend(left.iter().copied());
383                    triangles.extend(right.iter().copied());
384                    out.push(triangles);
385                }
386            }
387        }
388        out
389    }
390    of(&(0..n).collect::<Vec<_>>())
391}
392
393/// A solid with the faces deleted taken out and the faces around the gap
394/// grown over it in one way, `triangles`: each three of those faces — the
395/// faces of three sides of the gap — meeting at a corner; each two sharing
396/// two corners meeting along a new edge between them; each two sides side
397/// by side meeting where they met, along the edge at the gap's corner
398/// between them, carried on to where the third face of their corner meets
399/// them. The gap's corners move to the corners of the triangles, a
400/// triangle with none gets a new one, and the faces' sides along the gap
401/// are their new edges to the faces they meet.
402///
403/// The old entities keep their index, the new ones come after them;
404/// [`Grown::compacted`] numbers them anew.
405struct Grown<'a> {
406    spec: &'a BodySpec,
407    names: &'a BodyNames,
408    /// Every vertex — the old, then the new — with the point to start
409    /// looking for it from, its name, and whether it moves.
410    vertices: Vec<(Vector3, String, bool)>,
411    /// Per old vertex: the vertex it becomes.
412    vertex: Vec<usize>,
413    /// The new edges, between the triangles' corners, and their names.
414    edges: Vec<(EdgeSpec, String)>,
415    /// Per face that stays: its loops, the sides along the gap replaced.
416    faces: Vec<(usize, Vec<Vec<CoedgeSpec>>)>,
417}
418
419impl<'a> Grown<'a> {
420    fn new(
421        spec: &'a BodySpec,
422        names: &'a BodyNames,
423        namer: &Namer,
424        deleted: &[bool],
425        sides: &[Side],
426        triangles: &[[usize; 3]],
427    ) -> GeopResult<Self> {
428        let n = sides.len();
429        let face_name = |s: usize| names.faces[sides[s].face].as_str();
430        // Two faces meeting along a new edge have to be two.
431        let adjacent = |a: usize, b: usize| (a + 1) % n == b || (b + 1) % n == a;
432        for t in triangles {
433            for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[0], t[2])] {
434                if !adjacent(a, b) && sides[a].face == sides[b].face {
435                    return Err(GeopError::new(format!(
436                        "face {} would meet itself across the gap",
437                        face_name(a)
438                    )));
439                }
440            }
441        }
442        let around = spec.vertex_faces()?;
443        let mut grown = Grown {
444            spec,
445            names,
446            vertices: (spec.vertices.iter().zip(&names.vertices))
447                .map(|(p, name)| (*p, name.clone(), false))
448                .collect(),
449            vertex: (0..spec.vertices.len()).collect(),
450            edges: Vec::new(),
451            faces: Vec::new(),
452        };
453        // The corner of each triangle: the gap's corners it closes made one,
454        // at their middle, or a new one in the middle of its sides' ends.
455        let corner = |s: usize| sides[s].end;
456        let closes = |t: &[usize; 3], s: usize| t.contains(&s) && t.contains(&((s + 1) % n));
457        let mut corners = Vec::with_capacity(triangles.len());
458        for t in triangles {
459            let closed: Vec<usize> = (0..n).filter(|&s| closes(t, s)).map(corner).collect();
460            let near: Vec<usize> = if closed.is_empty() {
461                (t.iter())
462                    .flat_map(|&s| [sides[s].start, sides[s].end])
463                    .collect()
464            } else {
465                closed.clone()
466            };
467            let mut sum = Vector3::zero();
468            for &v in &near {
469                sum = sum.add(&spec.vertices[v]);
470            }
471            let share = Scalar::from_ratio(1, near.len() as i64)?;
472            let seed = sum.prod_scalar(share).sharpen();
473            let index = match closed.first() {
474                Some(&keep) => {
475                    for &v in &closed {
476                        grown.vertex[v] = keep;
477                    }
478                    keep
479                }
480                None => {
481                    let faces: Vec<&str> = t.iter().map(|&s| face_name(s)).collect();
482                    grown.vertices.push((seed, namer.name(&faces), true));
483                    grown.vertices.len() - 1
484                }
485            };
486            grown.vertices[index].0 = seed;
487            grown.vertices[index].2 = true;
488            corners.push(index);
489        }
490        // Where a triangle's corner is on the face of side `s`: where a
491        // gap's corner it closes was, if one was on that face, or else its
492        // seed's foot on the face's surface, from where the side starts.
493        let uv_on = |s: usize, t: usize| -> GeopResult<Vector2> {
494            let f = sides[s].face;
495            for v in (0..spec.vertices.len()).filter(|&v| grown.vertex[v] == corners[t]) {
496                if let Some(&(_, uv)) = around[v].iter().find(|&&(g, _)| g == f) {
497                    return Ok(uv);
498                }
499            }
500            let start = (around[sides[s].start].iter())
501                .find(|&&(g, _)| g == f)
502                .map(|&(_, uv)| uv)
503                .ok_or_else(|| GeopError::new("a side's start is not on its face"))?;
504            let (u, v) = spec.faces[f].surface.project_in_domain(
505                grown.vertices[corners[t]].0,
506                start[0].sharpen(),
507                start[1].sharpen(),
508                PROJECT_ITERATIONS,
509            )?;
510            Ok(Vector2::from_array([u.sharpen(), v.sharpen()]))
511        };
512        // Per side, the triangles round its face, from the one at the
513        // gap's corner it starts at to the one at the corner it ends at:
514        // by how far round the gap their other sides are, furthest first.
515        let fan = |s: usize| -> Vec<usize> {
516            let offset = |x: usize| (x + n - s) % n;
517            let mut round: Vec<usize> = (0..triangles.len())
518                .filter(|&t| triangles[t].contains(&s))
519                .collect();
520            round
521                .sort_by_key(|&t| std::cmp::Reverse(triangles[t].iter().map(|&x| offset(x)).max()));
522            round
523        };
524        // The new edges: one per pair of triangles sharing two sides.
525        let mut diagonal: Vec<((usize, usize), usize)> = Vec::new();
526        let mut used_names: Vec<usize> = Vec::new();
527        for s in 0..n {
528            let round = fan(s);
529            for pair in round.windows(2) {
530                let (t1, t2) = (pair[0], pair[1]);
531                let x = *(triangles[t1].iter())
532                    .find(|&&x| x != s && triangles[t2].contains(&x))
533                    .expect("two triangles round a side share another");
534                let key = (s.min(x), s.max(x));
535                if diagonal.iter().any(|(k, _)| *k == key) {
536                    continue;
537                }
538                let (a, b) = (grown.vertices[corners[t1]].0, grown.vertices[corners[t2]].0);
539                // Named as the edge of a side of one edge it takes the place
540                // of, where there is one, as a box's edge a chamfer took.
541                let single = |side: usize| match sides[side].run.as_slice() {
542                    [k] => match spec.faces[sides[side].face].loops()[sides[side].lp][*k].on {
543                        CoedgeOn::Edge(e, _) if !used_names.contains(&e) => Some(e),
544                        _ => None,
545                    },
546                    _ => None,
547                };
548                let name = match single(s).or_else(|| single(x)) {
549                    Some(e) => {
550                        used_names.push(e);
551                        names.edges[e].clone()
552                    }
553                    None => namer.name(&[face_name(key.0), face_name(key.1)]),
554                };
555                grown.edges.push((
556                    EdgeSpec {
557                        curve: NurbCurve3D::line(a, b)?,
558                        start: corners[t1],
559                        end: corners[t2],
560                    },
561                    name,
562                ));
563                diagonal.push((key, spec.edges.len() + grown.edges.len() - 1));
564            }
565        }
566        // Each face's loops, each side along the gap replaced by the new
567        // edges round its fan.
568        for (f, face) in spec.faces.iter().enumerate() {
569            if deleted[f] {
570                continue;
571            }
572            let mut loops = Vec::new();
573            for (l, lp) in face.loops().into_iter().enumerate() {
574                let mut kept = Vec::with_capacity(lp.len());
575                for (k, c) in lp.iter().enumerate() {
576                    let Some(s) = (0..n).find(|&s| {
577                        sides[s].face == f && sides[s].lp == l && sides[s].run.contains(&k)
578                    }) else {
579                        kept.push(c.clone());
580                        continue;
581                    };
582                    if sides[s].run[0] != k {
583                        continue;
584                    }
585                    let round = fan(s);
586                    for pair in round.windows(2) {
587                        let (t1, t2) = (pair[0], pair[1]);
588                        let x = *(triangles[t1].iter())
589                            .find(|&&x| x != s && triangles[t2].contains(&x))
590                            .expect("two triangles round a side share another");
591                        let e = (diagonal.iter())
592                            .find(|(key, _)| *key == (s.min(x), s.max(x)))
593                            .map(|&(_, e)| e)
594                            .expect("every pair round a fan has its edge");
595                        let edge = &grown.edges[e - spec.edges.len()].0;
596                        let sense = if edge.start == corners[t1] {
597                            Sense::Forward
598                        } else {
599                            Sense::Reversed
600                        };
601                        let pcurve = NurbCurve2D::line(uv_on(s, t1)?, uv_on(s, t2)?)?;
602                        kept.push(CoedgeSpec {
603                            on: CoedgeOn::Edge(e, sense),
604                            pcurve,
605                        });
606                    }
607                }
608                loops.push(kept);
609            }
610            grown.faces.push((f, loops));
611        }
612        Ok(grown)
613    }
614
615    /// The solid, its entities numbered anew, named, and per vertex whether
616    /// it moves.
617    fn compacted(&self) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
618        let old = self.spec.edges.len();
619        let edge = |e: usize| -> (EdgeSpec, &str) {
620            if e < old {
621                let edge = &self.spec.edges[e];
622                let spec = EdgeSpec {
623                    curve: edge.curve.clone(),
624                    start: self.vertex[edge.start],
625                    end: self.vertex[edge.end],
626                };
627                (spec, self.names.edges[e].as_str())
628            } else {
629                let (spec, name) = &self.edges[e - old];
630                (spec.clone(), name.as_str())
631            }
632        };
633        let faces = (self.faces.iter())
634            .map(|(f, loops)| {
635                let loops = (loops.iter())
636                    .map(|lp| {
637                        (lp.iter())
638                            .map(|c| CoedgeSpec {
639                                on: match c.on {
640                                    CoedgeOn::Vertex(v) => CoedgeOn::Vertex(self.vertex[v]),
641                                    on => on,
642                                },
643                                pcurve: c.pcurve.clone(),
644                            })
645                            .collect()
646                    })
647                    .collect();
648                (*f, loops)
649            })
650            .collect();
651        renumbered(
652            self.spec,
653            self.names,
654            faces,
655            edge,
656            |v| {
657                (
658                    self.vertices[v].0,
659                    self.vertices[v].1.as_str(),
660                    self.vertices[v].2,
661                )
662            },
663            true,
664        )
665    }
666}
667
668/// The solid of the faces `faces` of `spec` — each by its index and its
669/// loops, which run along the edges `edge(e)` gives and the vertices
670/// `vertex(v)` gives, each as its geometry and name, a vertex also whether
671/// it moves — with only the edges and vertices those loops use, numbered
672/// anew in the order the loops reach them.
673fn renumbered<'n>(
674    spec: &BodySpec,
675    names: &BodyNames,
676    faces: Vec<(usize, Vec<Vec<CoedgeSpec>>)>,
677    edge: impl Fn(usize) -> (EdgeSpec, &'n str),
678    vertex: impl Fn(usize) -> (Vector3, &'n str, bool),
679    solid: bool,
680) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
681    let mut edge_index: std::collections::HashMap<usize, usize> = Default::default();
682    let mut vertex_index: std::collections::HashMap<usize, usize> = Default::default();
683    let mut out = BodySpec::new(solid);
684    let mut out_names = BodyNames {
685        solid: names.solid.clone().filter(|_| solid),
686        ..Default::default()
687    };
688    let mut reseat = Vec::new();
689    let mut number_vertex = |v: usize, out: &mut BodySpec, out_names: &mut BodyNames| {
690        *vertex_index.entry(v).or_insert_with(|| {
691            let (point, name, moves) = vertex(v);
692            out.vertices.push(point);
693            out_names.vertices.push(name.to_string());
694            reseat.push(moves);
695            out.vertices.len() - 1
696        })
697    };
698    let mut new_faces = Vec::with_capacity(faces.len());
699    for (f, loops) in faces {
700        let mut renumbered = Vec::with_capacity(loops.len());
701        for lp in loops {
702            let mut coedges = Vec::with_capacity(lp.len());
703            for c in lp {
704                let on = match c.on {
705                    CoedgeOn::Edge(e, sense) => {
706                        let index = match edge_index.get(&e) {
707                            Some(&i) => i,
708                            None => {
709                                let (mut spec_edge, name) = edge(e);
710                                spec_edge.start =
711                                    number_vertex(spec_edge.start, &mut out, &mut out_names);
712                                spec_edge.end =
713                                    number_vertex(spec_edge.end, &mut out, &mut out_names);
714                                out.edges.push(spec_edge);
715                                out_names.edges.push(name.to_string());
716                                edge_index.insert(e, out.edges.len() - 1);
717                                out.edges.len() - 1
718                            }
719                        };
720                        CoedgeOn::Edge(index, sense)
721                    }
722                    CoedgeOn::Vertex(v) => {
723                        CoedgeOn::Vertex(number_vertex(v, &mut out, &mut out_names))
724                    }
725                };
726                coedges.push(CoedgeSpec {
727                    on,
728                    pcurve: c.pcurve,
729                });
730            }
731            renumbered.push(coedges);
732        }
733        if renumbered.iter().any(Vec::is_empty) {
734            return Err(GeopError::new(format!(
735                "a loop of face {} would be left with no edge",
736                names.faces[f]
737            )));
738        }
739        let outer = renumbered.remove(0);
740        new_faces.push(FaceSpec {
741            surface: spec.faces[f].surface.clone(),
742            outer,
743            holes: renumbered,
744        });
745        out_names.faces.push(names.faces[f].clone());
746    }
747    out.faces = new_faces;
748    out.shells = vec![(0..out.faces.len()).collect()];
749    out.check()?;
750    Ok((out, out_names, reseat))
751}
752
753/// A solid described as a [`BodySpec`], and the faces taken out of it and
754/// the holes closed.
755struct Edited<'a> {
756    spec: &'a BodySpec,
757    names: &'a BodyNames,
758    /// Per face: whether it is taken out.
759    delete: Vec<bool>,
760    /// The holes closed: a face, and which of its holes.
761    closed: Vec<(usize, usize)>,
762}
763
764impl<'a> Edited<'a> {
765    fn new(spec: &'a BodySpec, names: &'a BodyNames) -> Self {
766        Self {
767            spec,
768            names,
769            delete: vec![false; spec.faces.len()],
770            closed: Vec::new(),
771        }
772    }
773
774    /// The edited solid — or sheet, unless `solid` — its entities numbered
775    /// anew and named as they were, and per vertex whether it moves.
776    fn compacted(&self, solid: bool) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
777        let spec = self.spec;
778        let faces = (spec.faces.iter().enumerate())
779            .filter(|&(f, _)| !self.delete[f])
780            .map(|(f, face)| {
781                let loops = (face.loops().into_iter().enumerate())
782                    .filter(|&(l, _)| l == 0 || !self.closed.contains(&(f, l - 1)))
783                    .map(|(_, lp)| lp.clone())
784                    .collect();
785                (f, loops)
786            })
787            .collect();
788        renumbered(
789            spec,
790            self.names,
791            faces,
792            |e| (spec.edges[e].clone(), self.names.edges[e].as_str()),
793            |v| (spec.vertices[v], self.names.vertices[v].as_str(), false),
794            solid,
795        )
796    }
797}