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