1use core::fmt;
4
5use crate::{id::GraphId, BuiltInNode, GeometryNode, NodeId};
6
7#[non_exhaustive]
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum GraphError {
11 ForeignReference { reference: NodeId },
13 NonPriorReference { node: NodeId, reference: NodeId },
15 InvalidReferenceType {
17 reference: NodeId,
19 expected: &'static str,
21 actual: &'static str,
23 },
24 UnknownRoot { root: NodeId, node_count: usize },
26 ContradictoryMaster {
28 detail: &'static str,
30 },
31 InvalidStation {
35 detail: &'static str,
37 },
38}
39
40impl fmt::Display for GraphError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::ForeignReference { reference } => {
44 write!(f, "{reference} belongs to another geometry graph")
45 }
46 Self::NonPriorReference { node, reference } => {
47 write!(f, "{node} references non-prior {reference}")
48 }
49 Self::InvalidReferenceType {
50 reference,
51 expected,
52 actual,
53 } => write!(f, "{reference} has node type {actual}; expected {expected}"),
54 Self::ContradictoryMaster { detail } => {
55 write!(f, "contradictory surface-curve master: {detail}")
56 }
57 Self::InvalidStation { detail } => write!(f, "invalid station: {detail}"),
58 Self::UnknownRoot { root, node_count } => {
59 write!(f, "root {root} exceeds graph size {node_count}")
60 }
61 }
62 }
63}
64
65impl std::error::Error for GraphError {}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct GeometryGraph {
70 owner: GraphId,
71 nodes: Vec<GeometryNode>,
72 roots: Vec<NodeId>,
73}
74
75impl Default for GeometryGraph {
76 fn default() -> Self {
77 Self {
78 owner: GraphId::fresh(),
79 nodes: Vec::new(),
80 roots: Vec::new(),
81 }
82 }
83}
84
85impl GeometryGraph {
86 pub fn len(&self) -> usize {
88 self.nodes.len()
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.nodes.is_empty()
94 }
95
96 pub fn get(&self, id: NodeId) -> Option<&GeometryNode> {
98 if !id.belongs_to(self.owner) {
99 return None;
100 }
101 self.nodes.get(id.index())
102 }
103
104 pub fn roots(&self) -> &[NodeId] {
106 &self.roots
107 }
108
109 pub fn iter(&self) -> impl ExactSizeIterator<Item = (NodeId, &GeometryNode)> {
111 self.nodes
112 .iter()
113 .enumerate()
114 .map(|(index, node)| (NodeId::from_index(self.owner, index), node))
115 }
116}
117
118#[derive(Debug)]
120pub struct GeometryGraphBuilder {
121 owner: Option<GraphId>,
122 nodes: Vec<GeometryNode>,
123}
124
125impl Default for GeometryGraphBuilder {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl GeometryGraphBuilder {
132 pub const fn new() -> Self {
134 Self {
135 owner: None,
136 nodes: Vec::new(),
137 }
138 }
139
140 pub fn push(&mut self, node: GeometryNode) -> Result<NodeId, GraphError> {
142 let owner = *self.owner.get_or_insert_with(GraphId::fresh);
143 let id = NodeId::from_index(owner, self.nodes.len());
144 let references = node.references();
145 if let Some(&reference) = references
146 .iter()
147 .find(|reference| !reference.belongs_to(owner))
148 {
149 return Err(GraphError::ForeignReference { reference });
150 }
151 if let Some(&reference) = references
152 .iter()
153 .find(|reference| reference.index() >= id.index())
154 {
155 return Err(GraphError::NonPriorReference {
156 node: id,
157 reference,
158 });
159 }
160 crate::validation::validate_reference_types(&node, &self.nodes)?;
161 self.nodes.push(node);
162 Ok(id)
163 }
164
165 pub fn push_value<T>(&mut self, value: T) -> Result<NodeId, GraphError>
170 where
171 T: BuiltInNode,
172 {
173 self.push(value.into())
174 }
175
176 pub fn finish(self, roots: Vec<NodeId>) -> Result<GeometryGraph, GraphError> {
178 let owner = self.owner.unwrap_or_else(GraphId::fresh);
179 if let Some(&reference) = roots.iter().find(|root| !root.belongs_to(owner)) {
180 return Err(GraphError::ForeignReference { reference });
181 }
182 if let Some(&root) = roots.iter().find(|root| root.index() >= self.nodes.len()) {
183 return Err(GraphError::UnknownRoot {
184 root,
185 node_count: self.nodes.len(),
186 });
187 }
188 Ok(GeometryGraph {
189 owner,
190 nodes: self.nodes,
191 roots,
192 })
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use axiolid_core::Vec3;
199
200 use super::*;
201 use crate::Instance;
202
203 const EMPTY_BUILDER: GeometryGraphBuilder = GeometryGraphBuilder::new();
204
205 #[test]
206 fn const_constructor_remains_source_compatible() {
207 assert!(EMPTY_BUILDER.finish(Vec::new()).unwrap().is_empty());
208 }
209
210 #[test]
211 fn insertion_order_is_topological_order() {
212 let mut builder = GeometryGraphBuilder::new();
213 let source = builder.push(GeometryNode::Point3(Vec3::ZERO)).unwrap();
214 let instance = builder
215 .push(GeometryNode::Instance(Instance {
216 source,
217 transform: axiolid_core::Transform3::IDENTITY,
218 }))
219 .unwrap();
220 let graph = builder.finish(vec![instance]).unwrap();
221 assert_eq!(graph.len(), 2);
222 assert_eq!(graph.roots(), &[instance]);
223 }
224
225 #[test]
226 fn sealed_built_in_values_have_an_ergonomic_builder_path() {
227 let mut builder = GeometryGraphBuilder::new();
228 let sphere = builder
229 .push_value(axiolid_primitive::Primitive::Sphere { radius: 1.0 })
230 .unwrap();
231 let graph = builder.finish(vec![sphere]).unwrap();
232 assert!(matches!(
233 graph.get(sphere),
234 Some(GeometryNode::Primitive(
235 axiolid_primitive::Primitive::Sphere { radius: 1.0 }
236 ))
237 ));
238 }
239
240 #[test]
241 fn handles_from_another_builder_cannot_alias_local_nodes() {
242 let mut foreign_builder = GeometryGraphBuilder::new();
243 let foreign = foreign_builder
244 .push(GeometryNode::Point3(Vec3::ZERO))
245 .unwrap();
246
247 let mut builder = GeometryGraphBuilder::new();
248 let local = builder.push(GeometryNode::Point3(Vec3::ZERO)).unwrap();
249 let error = builder
250 .push(GeometryNode::Instance(Instance {
251 source: foreign,
252 transform: axiolid_core::Transform3::IDENTITY,
253 }))
254 .unwrap_err();
255 assert!(matches!(
256 error,
257 GraphError::ForeignReference { reference } if reference == foreign
258 ));
259 let error = builder.finish(vec![foreign]).unwrap_err();
260 assert!(matches!(
261 error,
262 GraphError::ForeignReference { reference } if reference == foreign
263 ));
264
265 let graph = foreign_builder.finish(vec![foreign]).unwrap();
266 assert!(graph.get(local).is_none());
267 }
268
269 #[test]
270 fn semantic_reference_types_are_validated_before_insertion() {
271 let mut builder = GeometryGraphBuilder::new();
272 let point = builder.push(GeometryNode::Point3(Vec3::ZERO)).unwrap();
273 let error = builder
274 .push(GeometryNode::SolidOperation(
275 crate::SolidOperation::Extrusion {
276 profile: point,
277 direction: Vec3::Z,
278 depth: 1.0,
279 },
280 ))
281 .unwrap_err();
282 assert!(matches!(
283 error,
284 GraphError::InvalidReferenceType {
285 reference,
286 expected: "profile",
287 actual: "point3",
288 } if reference == point
289 ));
290 }
291
292 #[test]
293 fn instance_nodes_preserve_their_source_reference_family() {
294 let mut builder = GeometryGraphBuilder::new();
295 let point = builder.push(GeometryNode::Point3(Vec3::ZERO)).unwrap();
296 let instance = builder
297 .push(GeometryNode::Instance(Instance {
298 source: point,
299 transform: axiolid_core::Transform3::IDENTITY,
300 }))
301 .unwrap();
302
303 let error = builder
304 .push(GeometryNode::SolidOperation(
305 crate::SolidOperation::Boolean {
306 left: instance,
307 right: instance,
308 operator: axiolid_core::BooleanOperator::Union,
309 },
310 ))
311 .unwrap_err();
312 let GraphError::InvalidReferenceType {
313 reference,
314 expected,
315 actual,
316 } = error
317 else {
318 panic!("unexpected graph error: {error:?}");
319 };
320 assert_eq!(reference, instance);
321 assert_eq!(expected, "solid");
322 assert_eq!(actual, "instance");
323
324 let solid = builder
325 .push(GeometryNode::Primitive(
326 axiolid_primitive::Primitive::Sphere { radius: 1.0 },
327 ))
328 .unwrap();
329 let solid_instance = builder
330 .push(GeometryNode::Instance(Instance {
331 source: solid,
332 transform: axiolid_core::Transform3::IDENTITY,
333 }))
334 .unwrap();
335 assert!(builder
336 .push(GeometryNode::SolidOperation(
337 crate::SolidOperation::Boolean {
338 left: solid_instance,
339 right: solid_instance,
340 operator: axiolid_core::BooleanOperator::Union,
341 },
342 ))
343 .is_ok());
344 }
345
346 #[test]
347 fn surface_curve_requires_a_three_dimensional_basis() {
348 let mut builder = GeometryGraphBuilder::new();
349 let curve_2d = builder
350 .push(GeometryNode::Curve2(axiolid_curve::Curve2::Line(
351 axiolid_curve::Line2 {
352 origin: axiolid_core::Vec2::ZERO,
353 direction: axiolid_core::Vec2::X,
354 },
355 )))
356 .unwrap();
357 let plane = builder
358 .push(GeometryNode::Surface(axiolid_surface::Surface::Plane(
359 axiolid_surface::Plane {
360 frame: axiolid_core::Frame3 {
361 origin: Vec3::ZERO,
362 x: Vec3::X,
363 y: Vec3::Y,
364 z: Vec3::Z,
365 },
366 },
367 )))
368 .unwrap();
369 let error = builder
370 .push(GeometryNode::CurveRelation(
371 crate::CurveRelation::SurfaceCurve {
372 curve_3d: curve_2d,
373 sides: crate::SurfaceSides::one(plane, curve_2d),
374 master: crate::MasterRepresentation::Curve3d,
375 },
376 ))
377 .unwrap_err();
378 assert!(matches!(
379 error,
380 GraphError::InvalidReferenceType {
381 reference,
382 expected: "curve3",
383 actual: "curve2",
384 } if reference == curve_2d
385 ));
386
387 let curve_3d = builder
388 .push(GeometryNode::Curve3(axiolid_curve::Curve3::Line(
389 axiolid_curve::Line3 {
390 origin: Vec3::ZERO,
391 direction: Vec3::X,
392 },
393 )))
394 .unwrap();
395 let trimmed_3d = builder
396 .push(GeometryNode::CurveRelation(crate::CurveRelation::Trimmed {
397 basis: curve_3d,
398 start: Vec::new(),
399 end: Vec::new(),
400 sense_agreement: true,
401 preference: crate::TrimmingPreference::Unspecified,
402 }))
403 .unwrap();
404 assert!(builder
405 .push(GeometryNode::CurveRelation(
406 crate::CurveRelation::SurfaceCurve {
407 curve_3d: trimmed_3d,
408 sides: crate::SurfaceSides::one(plane, curve_2d),
409 master: crate::MasterRepresentation::Curve3d,
410 },
411 ))
412 .is_ok());
413 }
414
415 #[test]
416 fn parameter_curve_requires_a_two_dimensional_reference() {
417 let mut builder = GeometryGraphBuilder::new();
418 let surface = builder
419 .push(GeometryNode::Surface(axiolid_surface::Surface::Plane(
420 axiolid_surface::Plane {
421 frame: axiolid_core::Frame3 {
422 origin: Vec3::ZERO,
423 x: Vec3::X,
424 y: Vec3::Y,
425 z: Vec3::Z,
426 },
427 },
428 )))
429 .unwrap();
430 let curve_3d = builder
431 .push(GeometryNode::Curve3(axiolid_curve::Curve3::Line(
432 axiolid_curve::Line3 {
433 origin: Vec3::ZERO,
434 direction: Vec3::X,
435 },
436 )))
437 .unwrap();
438 let error = builder
439 .push(GeometryNode::CurveRelation(
440 crate::CurveRelation::ParameterCurve {
441 basis_surface: surface,
442 reference_curve: curve_3d,
443 },
444 ))
445 .unwrap_err();
446 assert!(matches!(
447 error,
448 GraphError::InvalidReferenceType {
449 reference,
450 expected: "curve2",
451 actual: "curve3",
452 } if reference == curve_3d
453 ));
454 }
455}