Skip to main content

geop_core_part/
part.rs

1use std::collections::BTreeMap;
2
3use geop_core_math::{
4    geop_error::{GeopError, GeopResult},
5    scalars::Scalar,
6};
7use geop_core_topology::Model;
8
9use crate::datum::Datum;
10use crate::ids::{DatumId, RefId, SketchId};
11use crate::names::NameRegistry;
12use crate::sketch::PlacedSketch;
13
14/// A complete, editable CAD part: its boundary-representation topology, the
15/// sketches and datums used to build it, and a name for every one of those
16/// entities.
17///
18/// The fields are private: the only way to change a part is through its
19/// methods, each of which forwards straight to the identically named
20/// [`Model`] operation, registers every vertex/edge/face/solid it created
21/// under the name the caller supplied, and forgets the name of every one it
22/// deleted. That keeps the invariant [`Part::check_names`] checks — every
23/// entity has exactly one name, and every name one entity — true by
24/// construction rather than by each caller's diligence.
25#[derive(Clone)]
26pub struct Part<S: Scalar> {
27    pub(crate) topology: Model<S>,
28    pub(crate) names: NameRegistry,
29    pub(crate) sketches: BTreeMap<SketchId, PlacedSketch<S>>,
30    pub(crate) datums: BTreeMap<DatumId, Datum<S>>,
31    /// The next sketch or datum id: ids count up in the order they are
32    /// added, so iterating either map goes oldest first.
33    next_id: u64,
34}
35
36impl<S: Scalar> Part<S> {
37    pub fn new() -> Self {
38        Self {
39            topology: Model::new(),
40            names: NameRegistry::new(),
41            sketches: BTreeMap::new(),
42            datums: BTreeMap::new(),
43            next_id: 1,
44        }
45    }
46
47    /// The part's topology, to query. Changing it goes through `Part`'s own
48    /// methods, so that names stay in sync.
49    pub fn topology(&self) -> &Model<S> {
50        &self.topology
51    }
52
53    pub fn names(&self) -> &NameRegistry {
54        &self.names
55    }
56
57    pub fn name_of(&self, id: impl Into<RefId>) -> Option<&str> {
58        self.names.name_of(id)
59    }
60
61    pub fn id_of(&self, name: &str) -> Option<RefId> {
62        self.names.id_of(name)
63    }
64
65    /// Gives `id` the name `new_name` instead — see [`NameRegistry::rename`]
66    /// for the one situation this is for.
67    pub fn rename(&mut self, id: impl Into<RefId>, new_name: impl Into<String>) -> GeopResult<()> {
68        self.names.rename(id, new_name)
69    }
70
71    /// A sketch or datum id no entity has had yet.
72    pub(crate) fn fresh_id(&mut self) -> u64 {
73        let id = self.next_id;
74        self.next_id += 1;
75        id
76    }
77
78    fn exists(&self, id: RefId) -> bool {
79        match id {
80            RefId::Vertex(id) => self.topology.vertices.contains_key(&id),
81            RefId::Edge(id) => self.topology.edges.contains_key(&id),
82            RefId::Face(id) => self.topology.faces.contains_key(&id),
83            RefId::Solid(id) => self.topology.solids.contains_key(&id),
84            RefId::Sketch(id) => self.sketches.contains_key(&id),
85            RefId::Datum(id) => self.datums.contains_key(&id),
86        }
87    }
88
89    /// Forgets the name of every entity that no longer exists — for an
90    /// operation that deletes by reachability rather than one id at a time
91    /// (see [`Model::assemble_solid`]).
92    pub(crate) fn forget_dead_names(&mut self) {
93        let alive: std::collections::HashSet<RefId> = self
94            .names
95            .iter()
96            .map(|(id, _)| id)
97            .filter(|&id| self.exists(id))
98            .collect();
99        self.names.retain(|id| alive.contains(&id));
100    }
101
102    /// Checks the invariant every method keeps: every vertex, edge, face,
103    /// solid, sketch and datum has a name, and every name belongs to one of
104    /// them.
105    pub fn check_names(&self) -> GeopResult<()> {
106        let topology = &self.topology;
107        let entities = topology
108            .vertices
109            .keys()
110            .map(|&id| RefId::from(id))
111            .chain(topology.edges.keys().map(|&id| id.into()))
112            .chain(topology.faces.keys().map(|&id| id.into()))
113            .chain(topology.solids.keys().map(|&id| id.into()))
114            .chain(self.sketches.keys().map(|&id| id.into()))
115            .chain(self.datums.keys().map(|&id| id.into()));
116        let unnamed: Vec<String> = entities
117            .filter(|&id| self.names.name_of(id).is_none())
118            .map(|id| id.to_string())
119            .collect();
120        let dead: Vec<&str> = self
121            .names
122            .iter()
123            .filter(|&(id, _)| !self.exists(id))
124            .map(|(_, name)| name)
125            .collect();
126        if unnamed.is_empty() && dead.is_empty() {
127            Ok(())
128        } else {
129            Err(GeopError::new(format!(
130                "Part::check_names: unnamed entities {unnamed:?}, names of deleted entities {dead:?}"
131            )))
132        }
133    }
134}
135
136impl<S: Scalar> Default for Part<S> {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use geop_core_math::{scalars::scal_in_f64::ScalInF64, vector::Vector3};
145    use geop_core_sketch::Sketch;
146
147    use super::*;
148    use crate::PlacedSketch;
149
150    fn origin() -> Vector3<ScalInF64> {
151        Vector3::from_array([ScalInF64::from_f64(0.0); 3])
152    }
153
154    /// `mvfs` creates a vertex, a face and a solid; each must be given a
155    /// name, and `kvfs` undoing it erases all three names again.
156    #[test]
157    fn mvfs_and_kvfs_keep_names_in_sync() {
158        let mut part = Part::<ScalInF64>::new();
159        let (vertex, face, solid) = part.mvfs(origin(), "v0", "f0", "s0").unwrap();
160
161        assert_eq!(part.name_of(vertex), Some("v0"));
162        assert_eq!(part.name_of(face), Some("f0"));
163        assert_eq!(part.name_of(solid), Some("s0"));
164        assert_eq!(part.id_of("v0"), Some(RefId::Vertex(vertex)));
165        part.check_names().unwrap();
166
167        part.kvfs(solid).unwrap();
168
169        assert_eq!(part.name_of(vertex), None);
170        assert_eq!(part.name_of(face), None);
171        assert_eq!(part.name_of(solid), None);
172        assert!(part.topology().vertices.is_empty());
173        assert!(part.topology().faces.is_empty());
174        assert!(part.topology().solids.is_empty());
175        part.check_names().unwrap();
176    }
177
178    /// Reusing a name that's already taken is rejected.
179    #[test]
180    fn duplicate_name_is_rejected() {
181        let mut part = Part::<ScalInF64>::new();
182        part.mvfs(origin(), "v0", "f0", "s0").unwrap();
183
184        let err = part.mvfs(origin(), "v1", "f1", "s0");
185        assert!(err.is_err());
186    }
187
188    /// A sketch is named like any other entity, and removing it forgets the
189    /// name.
190    #[test]
191    fn sketches_are_named() {
192        let mut part = Part::<ScalInF64>::new();
193        let placed = PlacedSketch {
194            plane: geop_core_math::primitives::CoordinateSystem::try_new(
195                origin(),
196                Vector3::from_array([ScalInF64::ONE, ScalInF64::ZERO, ScalInF64::ZERO]),
197                Vector3::from_array([ScalInF64::ZERO, ScalInF64::ONE, ScalInF64::ZERO]),
198                Vector3::from_array([ScalInF64::ZERO, ScalInF64::ZERO, ScalInF64::ONE]),
199            )
200            .unwrap(),
201            sketch: Sketch::new(),
202        };
203        let id = part.add_sketch(placed, "sketch0").unwrap();
204        assert_eq!(part.name_of(id), Some("sketch0"));
205        assert_eq!(part.sketch_id("sketch0").unwrap(), id);
206        part.check_names().unwrap();
207
208        part.remove_sketch(id).unwrap();
209        assert_eq!(part.name_of(id), None);
210        assert!(part.sketch(id).is_err());
211    }
212
213    /// A provisional name can be settled, but not onto a name in use.
214    #[test]
215    fn rename_settles_a_provisional_name() {
216        let mut part = Part::<ScalInF64>::new();
217        let (vertex, _, _) = part.mvfs(origin(), "v~0", "f0", "s0").unwrap();
218        assert!(part.rename(vertex, "f0").is_err());
219        part.rename(vertex, "v0").unwrap();
220        assert_eq!(part.name_of(vertex), Some("v0"));
221        assert_eq!(part.id_of("v~0"), None);
222    }
223}