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