Skip to main content

geop_lib_booleans/
trim.rs

1//! Trimming a sheet "up to next": of a sheet swept from a profile, keeping
2//! only what its start reaches before it meets a solid's boundary.
3//!
4//! The solid is imprinted onto the sheet exactly as a boolean imprints two
5//! bodies ([`remesh`]) — but a copy of it, so the solid itself is left as it
6//! is: a sheet stopping at a solid does nothing to the solid. After that,
7//! every face of the sheet lies inside the solid, outside it, or on its
8//! boundary, and where the solid's boundary crosses the sheet there is an
9//! edge between faces on either side of it. The piece kept is the connected
10//! set of faces on the start's side, joined across edges the solid does not
11//! cross.
12
13use std::collections::{HashMap, HashSet};
14
15use geop_core_math::{
16    geop_error::{GeopError, GeopResult, WithContext},
17    union_find::UnionFind,
18    with_context,
19};
20use geop_core_topology::{Body, EdgeId, FaceId, ShellId, SolidId, contains::shell::Classified};
21use geop_ops_model::{NamedModel, Namer};
22
23use crate::{
24    boolean::{classify_face, copy_of},
25    remesh::{RemeshParams, remesh},
26};
27
28/// Trims the sheet `sheet` up to the next face of any of `targets`: keeps
29/// the piece of it that its edges named `start` lie on — up to where it
30/// meets their boundary — and drops the rest. `targets` are not changed.
31///
32/// What the trimming creates is named by `namer` as a boolean names what it
33/// creates (see `naming`). Fails if the piece also reaches an edge
34/// named in `end`: nothing stopped the sheet.
35pub fn trim_up_to_next(
36    model: &mut NamedModel,
37    namer: &Namer,
38    sheet: ShellId,
39    targets: &[SolidId],
40    (start, end): (&HashSet<String>, &HashSet<String>),
41    params: RemeshParams,
42) -> GeopResult<ShellId> {
43    let ctx = with_context!(
44        "trim_up_to_next(name={}, sheet={sheet}, targets={targets:?})",
45        namer.root()
46    );
47    // A copy of the target to imprint, so the target stays as it is.
48    let copy = copy_of(model, namer, targets).with_context(&ctx)?;
49
50    let origins = remesh(model, namer, Body::Sheet(sheet), copy, params).with_context(&ctx)?;
51    let topology = model.topology();
52    let faces = topology.body_faces(Body::Sheet(sheet)).with_context(&ctx)?;
53    let copy_faces: HashSet<FaceId> = topology
54        .solid_faces(copy)
55        .with_context(&ctx)?
56        .into_iter()
57        .collect();
58    let mut classes = Vec::with_capacity(faces.len());
59    let mut classified = Classified::default();
60    for &face in &faces {
61        classes.push(
62            classify_face(topology, face, copy, params, &mut classified)
63                .with_context(&ctx)
64                .with_context(with_context!("classifying face {face}"))?,
65        );
66    }
67
68    // Faces on one side of the target, sharing an edge the target does not
69    // cross, are one piece.
70    let mut users: HashMap<EdgeId, Vec<usize>> = HashMap::new();
71    let mut crossed: HashSet<EdgeId> = HashSet::new();
72    for (k, &face) in faces.iter().enumerate() {
73        for edge in topology.face_edges(face) {
74            users.entry(edge).or_default().push(k);
75        }
76    }
77    for &face in &copy_faces {
78        crossed.extend(topology.face_edges(face));
79    }
80    let mut pieces = UnionFind::new(faces.len());
81    for (edge, users) in &users {
82        if crossed.contains(edge) {
83            continue;
84        }
85        for pair in users.windows(2) {
86            if classes[pair[0]] == classes[pair[1]] {
87                pieces.union(pair[0], pair[1]);
88            }
89        }
90    }
91    let on = |names: &HashSet<String>, edge: &EdgeId| {
92        origins
93            .edges
94            .get(edge)
95            .is_some_and(|origin| names.contains(origin))
96    };
97    let mut reached = HashSet::new();
98    for (edge, users) in &users {
99        if on(start, edge) {
100            reached.extend(users.iter().map(|&k| pieces.find(k)));
101        }
102    }
103    if reached.is_empty() {
104        return Err(ctx(GeopError::new("trim: the sheet's start is gone")));
105    }
106    if users.iter().any(|(edge, users)| {
107        on(end, edge) && users.iter().any(|&k| reached.contains(&pieces.find(k)))
108    }) {
109        return Err(ctx(GeopError::new(
110            "up to next: nothing stops the face — it reaches its end without meeting the target all along",
111        )));
112    }
113    let keep: Vec<FaceId> = (0..faces.len())
114        .filter(|&k| reached.contains(&pieces.find(k)))
115        .map(|k| faces[k])
116        .collect();
117    let kept = model
118        .assemble_sheet(&[Body::Sheet(sheet), copy.into()], &keep)
119        .with_context(&ctx)?;
120    Ok(kept.expect("the start's piece is kept"))
121}