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}