axiolid_brep/
name.rs

1//! Persistent names for exact B-rep faces and edges.
2//!
3//! [`crate::SurfaceId`] and [`axiolid_topology::FaceId`] are arena positions.
4//! They answer "which slot" and are only meaningful inside one assembled
5//! value: rebuild the catalog and slot 7 is a different face. That is fine
6//! for validation, which never outlives assembly, and useless for anything
7//! that must refer to a face *across* an operation -- selecting an edge to
8//! fillet, carrying a material through a boolean, or re-applying a feature
9//! after an upstream edit.
10//!
11//! A [`FaceName`] instead records WHERE THE FACE CAME FROM. It is derived
12//! from the generating inputs, so the same construction names the same face
13//! no matter how the arenas are packed, and an operation that splits a face
14//! can say which face each fragment came from.
15//!
16//! # What a name is not
17//!
18//! A name is not a guarantee that the face still exists, and not a claim
19//! that two equal names are geometrically identical. It is a statement about
20//! provenance: "this face was produced by that part of that input". Callers
21//! that need existence must look the name up and handle absence.
22
23use core::fmt;
24
25/// Which side of a swept profile a face came from.
26///
27/// The cap variants carry no index because a sweep has exactly one of each,
28/// while side walls are per profile edge and need to say which.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub enum SweptFace {
31    /// The face closing the sweep at its start.
32    StartCap,
33    /// The face closing the sweep at its end.
34    EndCap,
35    /// The wall swept from one profile edge, indexed in profile order.
36    Side(u32),
37}
38
39impl fmt::Display for SweptFace {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::StartCap => f.write_str("start-cap"),
43            Self::EndCap => f.write_str("end-cap"),
44            Self::Side(index) => write!(f, "side[{index}]"),
45        }
46    }
47}
48
49/// Which operand of a binary operation a fragment came from.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum Operand {
52    /// The left-hand operand.
53    Subject,
54    /// The right-hand operand.
55    Tool,
56}
57
58impl fmt::Display for Operand {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Subject => f.write_str("subject"),
62            Self::Tool => f.write_str("tool"),
63        }
64    }
65}
66
67/// A persistent, structural name for one exact B-rep face.
68///
69/// Names compose: a boolean fragment of a filleted extrusion wall carries the
70/// whole chain, so a caller can ask both "which operand" and "which original
71/// profile edge" without consulting the operation that produced it.
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub enum FaceName {
74    /// A face of a swept solid, named by its role in the sweep.
75    Swept(SweptFace),
76    /// A blend face introduced by filleting or chamfering a named edge.
77    ///
78    /// The name is the *edge that was blended*, not a fresh anonymous id, so
79    /// re-running the feature after an edit still names the same blend.
80    Blend(Box<EdgeName>),
81    /// A fragment of a face of one operand of a boolean.
82    ///
83    /// Boolean output faces are always pieces of input faces -- the operation
84    /// never invents a surface -- so every fragment can say which input face
85    /// it is part of.
86    Fragment {
87        /// Which operand contributed the face this fragment came from.
88        operand: Operand,
89        /// The name that face carried in its own solid.
90        source: Box<FaceName>,
91    },
92    /// A face whose provenance was not tracked by the producing operation.
93    ///
94    /// Deliberately explicit: an operation that cannot name its output says
95    /// so, instead of fabricating a name that would collide with a real one.
96    Anonymous(u32),
97}
98
99impl FaceName {
100    /// Name a swept cap or wall.
101    pub const fn swept(part: SweptFace) -> Self {
102        Self::Swept(part)
103    }
104
105    /// Name the blend that replaced `edge`.
106    pub fn blend(edge: EdgeName) -> Self {
107        Self::Blend(Box::new(edge))
108    }
109
110    /// Name a fragment of `self` cut out by a boolean.
111    pub fn fragment(self, operand: Operand) -> Self {
112        Self::Fragment {
113            operand,
114            source: Box::new(self),
115        }
116    }
117
118    /// The original face name with all boolean fragment layers stripped.
119    ///
120    /// Answers "what was this before any boolean touched it", which is what a
121    /// material or finish assignment needs.
122    pub fn origin(&self) -> &Self {
123        match self {
124            Self::Fragment { source, .. } => source.origin(),
125            other => other,
126        }
127    }
128
129    /// Whether this name, or anything it came from, is anonymous.
130    ///
131    /// A caller that requires full provenance checks this rather than pattern
132    /// matching the outermost layer, which a fragment would hide.
133    pub fn is_anonymous(&self) -> bool {
134        matches!(self.origin(), Self::Anonymous(_))
135    }
136}
137
138impl fmt::Display for FaceName {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::Swept(part) => write!(f, "{part}"),
142            Self::Blend(edge) => write!(f, "blend({edge})"),
143            Self::Fragment { operand, source } => write!(f, "{operand}/{source}"),
144            Self::Anonymous(index) => write!(f, "anon#{index}"),
145        }
146    }
147}
148
149/// A persistent, structural name for one exact B-rep edge.
150///
151/// An edge is named by the faces that meet there rather than by its own
152/// arena slot, because that is what survives a rebuild: the intersection of
153/// two named faces is the same edge however the arenas are ordered.
154#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
155pub enum EdgeName {
156    /// The edge where two named faces meet.
157    ///
158    /// The pair is stored in a canonical order so that naming the edge from
159    /// either side produces the same name.
160    Between(Box<FaceName>, Box<FaceName>),
161    /// An edge whose provenance was not tracked by the producing operation.
162    Anonymous(u32),
163}
164
165impl EdgeName {
166    /// Name the edge shared by two faces, order-independently.
167    ///
168    /// `between(a, b)` and `between(b, a)` are equal, because "the edge where
169    /// these two faces meet" does not depend on which face is mentioned
170    /// first. Without this an edge would have two names and a fillet applied
171    /// from the other side would miss.
172    pub fn between(first: FaceName, second: FaceName) -> Self {
173        let (low, high) = if first <= second {
174            (first, second)
175        } else {
176            (second, first)
177        };
178        Self::Between(Box::new(low), Box::new(high))
179    }
180
181    /// Whether this name, or either face it names, is anonymous.
182    pub fn is_anonymous(&self) -> bool {
183        match self {
184            Self::Between(first, second) => first.is_anonymous() || second.is_anonymous(),
185            Self::Anonymous(_) => true,
186        }
187    }
188}
189
190impl fmt::Display for EdgeName {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self {
193            Self::Between(first, second) => write!(f, "{first}|{second}"),
194            Self::Anonymous(index) => write!(f, "anon#{index}"),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    /// A fragment remembers the face it was cut from, however deep.
204    ///
205    /// This is what carries a material or finish through a boolean: the
206    /// caller asks the fragment what it originally was.
207    #[test]
208    fn a_fragment_reports_the_face_it_came_from() {
209        let wall = FaceName::swept(SweptFace::Side(2));
210        let once = wall.clone().fragment(Operand::Subject);
211        let twice = once.clone().fragment(Operand::Tool);
212
213        assert_eq!(once.origin(), &wall);
214        assert_eq!(
215            twice.origin(),
216            &wall,
217            "a fragment of a fragment still came from the original wall"
218        );
219    }
220
221    /// Anonymity is inherited, so a wrapper cannot launder it.
222    #[test]
223    fn anonymity_survives_being_wrapped() {
224        let unnamed = FaceName::Anonymous(3);
225        assert!(unnamed.is_anonymous());
226        assert!(
227            unnamed.fragment(Operand::Subject).is_anonymous(),
228            "wrapping an unnamed face must not make it look named"
229        );
230    }
231
232    /// An edge naming an anonymous face is itself not fully named.
233    #[test]
234    fn an_edge_is_anonymous_when_either_side_is() {
235        let known = FaceName::swept(SweptFace::StartCap);
236        let unknown = FaceName::Anonymous(0);
237        assert!(EdgeName::between(known.clone(), unknown).is_anonymous());
238        assert!(!EdgeName::between(known.clone(), known).is_anonymous());
239    }
240
241    /// Distinct provenance must not collide into one name.
242    #[test]
243    fn different_origins_are_different_names() {
244        assert_ne!(
245            FaceName::swept(SweptFace::Side(1)),
246            FaceName::swept(SweptFace::Side(2))
247        );
248        assert_ne!(
249            FaceName::swept(SweptFace::StartCap),
250            FaceName::swept(SweptFace::EndCap)
251        );
252        let wall = FaceName::swept(SweptFace::Side(1));
253        assert_ne!(
254            wall.clone().fragment(Operand::Subject),
255            wall.fragment(Operand::Tool),
256            "the same wall cut from each operand yields distinct fragments"
257        );
258    }
259}