Skip to main content

geop_ops/part/
mod.rs

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