Skip to main content

axiolid_model/
graph.rs

1//! Immutable geometry DAG and append-only builder.
2
3use core::fmt;
4
5use crate::{id::GraphId, BuiltInNode, GeometryNode, NodeId};
6
7/// Invalid graph construction.
8#[non_exhaustive]
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum GraphError {
11    /// A node handle belongs to a different graph builder.
12    ForeignReference { reference: NodeId },
13    /// A node referenced itself or a later/not-yet-inserted node.
14    NonPriorReference { node: NodeId, reference: NodeId },
15    /// A reference resolves locally but points to the wrong node family.
16    InvalidReferenceType {
17        /// Existing node whose family is invalid for this edge.
18        reference: NodeId,
19        /// Human-readable family accepted by the edge.
20        expected: &'static str,
21        /// Human-readable family of the referenced node.
22        actual: &'static str,
23    },
24    /// A requested root does not exist.
25    UnknownRoot { root: NodeId, node_count: usize },
26    /// A master representation names a side the relation does not have.
27    ContradictoryMaster {
28        /// What the contradiction is, in the caller's terms.
29        detail: &'static str,
30    },
31    /// A station or a list of stations is malformed (#241): a non-finite
32    /// or negative distance, a non-finite offset, too few stations,
33    /// distances that do not increase, or inconsistent tags.
34    InvalidStation {
35        /// What is wrong, in the caller's terms.
36        detail: &'static str,
37    },
38    /// A ruled surface's sides are malformed (#402): a non-finite or empty
39    /// interval, a non-finite apex, two apexes, or the same curve run twice.
40    InvalidRuledSurface {
41        /// What is wrong, in the caller's terms.
42        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/// Immutable acyclic geometry graph with one or more roots.
77#[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    /// Number of nodes.
96    pub fn len(&self) -> usize {
97        self.nodes.len()
98    }
99
100    /// Whether the graph has no nodes.
101    pub fn is_empty(&self) -> bool {
102        self.nodes.is_empty()
103    }
104
105    /// Read a node by typed handle. A handle owned by another graph returns `None`.
106    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    /// Output roots in caller-specified order.
114    pub fn roots(&self) -> &[NodeId] {
115        &self.roots
116    }
117
118    /// The node payloads in insertion order, for the wire writer.
119    #[cfg(feature = "serde")]
120    pub(crate) fn nodes(&self) -> &[GeometryNode] {
121        &self.nodes
122    }
123
124    /// All nodes in stable topological insertion order.
125    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/// Append-only builder that makes cycles and dangling references unrepresentable.
134#[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    /// Create an empty builder.
148    pub const fn new() -> Self {
149        Self {
150            owner: None,
151            nodes: Vec::new(),
152        }
153    }
154
155    /// An empty builder owning `owner`'s brand: the graph reader brands
156    /// every decoded reference with it before pushing.
157    #[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    /// Insert a node. Every reference must be to an earlier node.
166    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    /// Insert one canonical representation without spelling its enum variant.
191    ///
192    /// The accepted set is deliberately sealed; adapters must translate custom
193    /// values into a built-in neutral representation before graph construction.
194    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    /// Freeze the graph after validating roots.
202    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}