Skip to main content

axiolid_brep/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Strict exact B-rep results over neutral analytic curve and surface values.
4//!
5//! [`axiolid_topology::BRep`] remains generic so graph/import clients can link
6//! their own handles. [`ExactBRep`] binds that graph to owned `Curve3`, `Curve2`,
7//! and `Surface` catalogs, then requires every support and trim span explicitly.
8//! It does not evaluate, intersect, or tessellate geometry.
9
10use std::collections::HashMap;
11use std::fmt;
12
13use axiolid_core::Interval;
14use axiolid_curve::{Curve2, Curve3};
15use axiolid_surface::Surface;
16use axiolid_topology::{audit_brep, BRep, BRepHealth, EdgeId, FaceId, LoopId, ShellId};
17
18/// Persistent structural names for faces and edges.
19pub mod name;
20/// Rigid placement of an exact B-rep.
21pub mod transform;
22
23pub use name::{EdgeName, FaceName, Operand, SweptFace};
24pub use transform::{TransformError, RIGID_TOLERANCE};
25
26macro_rules! geometry_id {
27    ($name:ident, $label:literal) => {
28        #[doc = concat!("Typed handle into the exact B-rep ", $label, " catalog.")]
29        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30        pub struct $name(u32);
31
32        impl $name {
33            fn from_index(index: usize) -> Self {
34                Self(u32::try_from(index).expect("exact B-rep catalog exceeds u32 capacity"))
35            }
36
37            /// Zero-based catalog index.
38            pub const fn index(self) -> usize {
39                self.0 as usize
40            }
41        }
42    };
43}
44
45geometry_id!(Curve3Id, "3D curve");
46geometry_id!(Curve2Id, "2D pcurve");
47geometry_id!(SurfaceId, "surface");
48
49/// Topology whose supports are typed references into [`ExactBRep`] catalogs.
50pub type ExactTopology = BRep<Curve3Id, Curve2Id, SurfaceId>;
51
52/// Owned, analytic boundary representation.
53///
54/// Every edge and pcurve use has a finite, non-zero native parameter span. An
55/// interval is deliberately owned here rather than inferred from endpoints:
56/// periodic supports, reversed pcurves, and parameter re-mapping are not
57/// recoverable from coordinates. The result may be an open sheet; an eventual
58/// exact-solid contract will additionally require a selected closed shell.
59#[derive(Debug, Clone, PartialEq)]
60pub struct ExactBRep {
61    topology: ExactTopology,
62    curves3: Vec<Curve3>,
63    curves2: Vec<Curve2>,
64    surfaces: Vec<Surface>,
65    edge_intervals: HashMap<EdgeId, Interval>,
66    pcurve_intervals: HashMap<(LoopId, usize), Interval>,
67    face_names: HashMap<FaceId, FaceName>,
68    edge_names: HashMap<EdgeId, EdgeName>,
69}
70
71impl ExactBRep {
72    /// Persistent structural name for a face, when the producer supplied one.
73    ///
74    /// Absence means the producing operation did not track provenance, not
75    /// that the face is invalid. Callers that require a name should treat
76    /// `None` as a refusal rather than substituting an index.
77    pub fn face_name(&self, face: FaceId) -> Option<&FaceName> {
78        self.face_names.get(&face)
79    }
80
81    /// Persistent structural name for an edge, when the producer supplied one.
82    pub fn edge_name(&self, edge: EdgeId) -> Option<&EdgeName> {
83        self.edge_names.get(&edge)
84    }
85
86    /// Find the face carrying `name`.
87    ///
88    /// This is the lookup that makes names useful across operations: a caller
89    /// holding a name from before an edit resolves it against the new result
90    /// instead of hoping an arena index still points at the same face.
91    pub fn face_by_name(&self, name: &FaceName) -> Option<FaceId> {
92        self.face_names
93            .iter()
94            .find(|(_, candidate)| *candidate == name)
95            .map(|(id, _)| *id)
96    }
97
98    /// Find the edge carrying `name`.
99    pub fn edge_by_name(&self, name: &EdgeName) -> Option<EdgeId> {
100        self.edge_names
101            .iter()
102            .find(|(_, candidate)| *candidate == name)
103            .map(|(id, _)| *id)
104    }
105    /// Name the two cap faces of a swept solid after assembly.
106    ///
107    /// The caps are identified by construction order -- a sweep adds the
108    /// start cap then the end cap before any wall -- rather than by
109    /// geometry, because a cap that shares a plane with a wall would be
110    /// ambiguous geometrically. Passing `None` leaves that cap unnamed.
111    pub fn name_caps(&mut self, start: Option<FaceName>, end: Option<FaceName>) {
112        let start_id = self.topology.face_id_at(0);
113        let end_id = self.topology.face_id_at(1);
114        if let (Some(name), Some(id)) = (start, start_id) {
115            self.face_names.insert(id, name);
116        }
117        if let (Some(name), Some(id)) = (end, end_id) {
118            self.face_names.insert(id, name);
119        }
120    }
121
122    /// Structural topology with typed support handles.
123    pub fn topology(&self) -> &ExactTopology {
124        &self.topology
125    }
126
127    /// Owned exact 3D curve supports.
128    pub fn curves3(&self) -> &[Curve3] {
129        &self.curves3
130    }
131
132    /// Owned exact 2D trim-curve supports.
133    pub fn curves2(&self) -> &[Curve2] {
134        &self.curves2
135    }
136
137    /// Owned exact support surfaces.
138    pub fn surfaces(&self) -> &[Surface] {
139        &self.surfaces
140    }
141
142    /// Native parameter span oriented from an edge's start vertex to its end.
143    pub fn edge_interval(&self, edge: EdgeId) -> Option<Interval> {
144        self.edge_intervals.get(&edge).copied()
145    }
146
147    /// Native parameter span for an edge use's pcurve in loop traversal order.
148    pub fn pcurve_interval(&self, loop_id: LoopId, use_index: usize) -> Option<Interval> {
149        self.pcurve_intervals.get(&(loop_id, use_index)).copied()
150    }
151}
152
153/// Mutable assembly state that can only yield an [`ExactBRep`] after validation.
154#[derive(Debug, Clone, Default, PartialEq)]
155pub struct ExactBRepBuilder {
156    topology: ExactTopology,
157    curves3: Vec<Curve3>,
158    curves2: Vec<Curve2>,
159    surfaces: Vec<Surface>,
160    edge_intervals: HashMap<EdgeId, Interval>,
161    pcurve_intervals: HashMap<(LoopId, usize), Interval>,
162    face_names: HashMap<FaceId, FaceName>,
163    edge_names: HashMap<EdgeId, EdgeName>,
164}
165
166impl ExactBRepBuilder {
167    /// Record the persistent structural name of a face.
168    ///
169    /// Naming is opt-in per producer: an operation that cannot honestly say
170    /// where a face came from simply does not call this, and the face reports
171    /// no name rather than a misleading one.
172    pub fn set_face_name(&mut self, face: FaceId, name: FaceName) {
173        self.face_names.insert(face, name);
174    }
175
176    /// Record the persistent structural name of an edge.
177    pub fn set_edge_name(&mut self, edge: EdgeId, name: EdgeName) {
178        self.edge_names.insert(edge, name);
179    }
180    /// Fallibly reserve owned support and interval catalogs for bounded assembly.
181    pub fn try_reserve(
182        &mut self,
183        curves3: usize,
184        curves2: usize,
185        surfaces: usize,
186        edge_intervals: usize,
187        pcurve_intervals: usize,
188    ) -> Result<(), std::collections::TryReserveError> {
189        self.curves3.try_reserve_exact(curves3)?;
190        self.curves2.try_reserve_exact(curves2)?;
191        self.surfaces.try_reserve_exact(surfaces)?;
192        self.edge_intervals.try_reserve(edge_intervals)?;
193        self.pcurve_intervals.try_reserve(pcurve_intervals)?;
194        Ok(())
195    }
196
197    /// Store an exact 3D curve support.
198    pub fn add_curve3(&mut self, curve: Curve3) -> Curve3Id {
199        let id = Curve3Id::from_index(self.curves3.len());
200        self.curves3.push(curve);
201        id
202    }
203
204    /// Store an exact 2D pcurve support.
205    pub fn add_curve2(&mut self, curve: Curve2) -> Curve2Id {
206        let id = Curve2Id::from_index(self.curves2.len());
207        self.curves2.push(curve);
208        id
209    }
210
211    /// Store an exact surface support.
212    pub fn add_surface(&mut self, surface: Surface) -> SurfaceId {
213        let id = SurfaceId::from_index(self.surfaces.len());
214        self.surfaces.push(surface);
215        id
216    }
217
218    /// Mutable typed topology under construction. [`Self::finish`] validates it.
219    pub fn topology_mut(&mut self) -> &mut ExactTopology {
220        &mut self.topology
221    }
222
223    /// State an edge's finite native curve span.
224    pub fn set_edge_interval(&mut self, edge: EdgeId, interval: Interval) {
225        self.edge_intervals.insert(edge, interval);
226    }
227
228    /// State an edge use's finite native pcurve span in its loop traversal order.
229    pub fn set_pcurve_interval(&mut self, loop_id: LoopId, use_index: usize, interval: Interval) {
230        self.pcurve_intervals.insert((loop_id, use_index), interval);
231    }
232
233    /// Copy every vertex, edge, loop, face and shell of `other` into this
234    /// builder, with its curves, surfaces, intervals and names, and return
235    /// the new handles of `other`'s shells in order.
236    ///
237    /// Solids are NOT copied: the caller says what the shells are -- the
238    /// outer shell of another solid, or a void of one already here. With
239    /// `reverse`, every face is used reversed in its shell, which turns a
240    /// solid's outer shell into the boundary of the same region seen from
241    /// outside it: a void.
242    pub fn append(&mut self, other: &ExactBRep, reverse: bool) -> Vec<ShellId> {
243        use axiolid_topology::{Edge, EdgeUse, Face, Loop, Orientation, Shell, Vertex};
244
245        let curves3: Vec<Curve3Id> = other
246            .curves3
247            .iter()
248            .map(|curve| self.add_curve3(curve.clone()))
249            .collect();
250        let curves2: Vec<Curve2Id> = other
251            .curves2
252            .iter()
253            .map(|curve| self.add_curve2(curve.clone()))
254            .collect();
255        let surfaces: Vec<SurfaceId> = other
256            .surfaces
257            .iter()
258            .map(|surface| self.add_surface(surface.clone()))
259            .collect();
260        let source = &other.topology;
261        let vertices: Vec<_> = source
262            .vertices()
263            .iter()
264            .map(|vertex| {
265                self.topology.add_vertex(Vertex {
266                    position: vertex.position,
267                })
268            })
269            .collect();
270        let mut edges = Vec::with_capacity(source.edges().len());
271        for (index, edge) in source.edges().iter().enumerate() {
272            let id = self.topology.add_edge(Edge {
273                start: vertices[edge.start.index()],
274                end: vertices[edge.end.index()],
275                curve: edge.curve.map(|curve| curves3[curve.index()]),
276            });
277            if let Some(old) = source.edge_id_at(index) {
278                if let Some(interval) = other.edge_intervals.get(&old) {
279                    self.edge_intervals.insert(id, *interval);
280                }
281                if let Some(name) = other.edge_names.get(&old) {
282                    self.edge_names.insert(id, name.clone());
283                }
284            }
285            edges.push(id);
286        }
287        let mut loops = Vec::with_capacity(source.loops().len());
288        for (index, wire) in source.loops().iter().enumerate() {
289            let id = self.topology.add_loop(Loop {
290                edges: wire
291                    .edges
292                    .iter()
293                    .map(|use_| EdgeUse {
294                        edge: edges[use_.edge.index()],
295                        orientation: use_.orientation,
296                        pcurve: use_.pcurve.map(|curve| curves2[curve.index()]),
297                    })
298                    .collect(),
299            });
300            if let Some(old) = source.loop_id_at(index) {
301                for use_index in 0..wire.edges.len() {
302                    if let Some(interval) = other.pcurve_intervals.get(&(old, use_index)) {
303                        self.pcurve_intervals.insert((id, use_index), *interval);
304                    }
305                }
306            }
307            loops.push(id);
308        }
309        let mut faces = Vec::with_capacity(source.faces().len());
310        for (index, face) in source.faces().iter().enumerate() {
311            let id = self.topology.add_face(Face {
312                surface: face.surface.map(|surface| surfaces[surface.index()]),
313                bounds: face
314                    .bounds
315                    .iter()
316                    .map(|bound| axiolid_topology::FaceBound {
317                        loop_id: loops[bound.loop_id.index()],
318                        ..*bound
319                    })
320                    .collect(),
321                orientation: face.orientation,
322            });
323            if let Some(name) = source
324                .face_id_at(index)
325                .and_then(|old| other.face_names.get(&old))
326            {
327                self.face_names.insert(id, name.clone());
328            }
329            faces.push(id);
330        }
331        source
332            .shells()
333            .iter()
334            .map(|shell| {
335                self.topology.add_shell(Shell {
336                    faces: shell
337                        .faces
338                        .iter()
339                        .map(|&(face, sense)| {
340                            let sense = match (sense, reverse) {
341                                (sense, false) => sense,
342                                (Orientation::Forward, true) => Orientation::Reversed,
343                                (Orientation::Reversed, true) => Orientation::Forward,
344                            };
345                            (faces[face.index()], sense)
346                        })
347                        .collect(),
348                    closed: shell.closed,
349                })
350            })
351            .collect()
352    }
353
354    /// Validate and freeze the exact B-rep result.
355    ///
356    /// Refuses a result with no faces, topology that fails
357    /// [`axiolid_topology::audit_brep`], and any edge, edge use or face whose
358    /// support or finite non-zero interval is missing or does not resolve in
359    /// its catalog (see [`ExactBRepError`]). Closure is not required: an open
360    /// sheet is a valid exact result.
361    pub fn finish(self) -> Result<ExactBRep, ExactBRepError> {
362        validate(&self)?;
363        Ok(ExactBRep {
364            topology: self.topology,
365            curves3: self.curves3,
366            curves2: self.curves2,
367            surfaces: self.surfaces,
368            edge_intervals: self.edge_intervals,
369            pcurve_intervals: self.pcurve_intervals,
370            face_names: self.face_names,
371            edge_names: self.edge_names,
372        })
373    }
374}
375
376/// Why exact B-rep assembly was refused.
377#[non_exhaustive]
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub enum ExactBRepError {
380    /// A result without faces is not a boundary representation.
381    Empty,
382    /// Generic topology has unresolved handles or invalid loop/face structure.
383    Topology(BRepHealth),
384    /// An edge did not state its exact three-dimensional support.
385    MissingEdgeCurve { edge_index: usize },
386    /// An edge support handle does not resolve in the 3D curve catalog.
387    UnknownCurve3 { edge_index: usize },
388    /// An edge did not state its native support-curve interval.
389    MissingEdgeInterval { edge_index: usize },
390    /// An edge support interval was non-finite or zero-length.
391    InvalidEdgeInterval { edge_index: usize },
392    /// An edge use did not state its trim curve in the owning face's parameters.
393    MissingPcurve { loop_index: usize, use_index: usize },
394    /// A pcurve support handle does not resolve in the 2D curve catalog.
395    UnknownCurve2 { loop_index: usize, use_index: usize },
396    /// A pcurve did not state its native interval.
397    MissingPcurveInterval { loop_index: usize, use_index: usize },
398    /// A pcurve interval was non-finite or zero-length.
399    InvalidPcurveInterval { loop_index: usize, use_index: usize },
400    /// A face did not state its exact support surface.
401    MissingFaceSurface { face_index: usize },
402    /// A face support handle does not resolve in the surface catalog.
403    UnknownSurface { face_index: usize },
404}
405
406impl fmt::Display for ExactBRepError {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        match self {
409            Self::Empty => f.write_str("exact B-rep requires at least one face"),
410            Self::Topology(_) => f.write_str("exact B-rep topology is structurally invalid"),
411            Self::MissingEdgeCurve { edge_index } => {
412                write!(f, "edge {edge_index} lacks a 3D curve")
413            }
414            Self::UnknownCurve3 { edge_index } => {
415                write!(f, "edge {edge_index} has an unknown 3D curve")
416            }
417            Self::MissingEdgeInterval { edge_index } => {
418                write!(f, "edge {edge_index} lacks a curve interval")
419            }
420            Self::InvalidEdgeInterval { edge_index } => {
421                write!(f, "edge {edge_index} has an invalid curve interval")
422            }
423            Self::MissingPcurve {
424                loop_index,
425                use_index,
426            } => write!(f, "loop {loop_index} use {use_index} lacks a pcurve"),
427            Self::UnknownCurve2 {
428                loop_index,
429                use_index,
430            } => write!(f, "loop {loop_index} use {use_index} has an unknown pcurve"),
431            Self::MissingPcurveInterval {
432                loop_index,
433                use_index,
434            } => write!(
435                f,
436                "loop {loop_index} use {use_index} lacks a pcurve interval"
437            ),
438            Self::InvalidPcurveInterval {
439                loop_index,
440                use_index,
441            } => write!(
442                f,
443                "loop {loop_index} use {use_index} has an invalid pcurve interval"
444            ),
445            Self::MissingFaceSurface { face_index } => {
446                write!(f, "face {face_index} lacks a support surface")
447            }
448            Self::UnknownSurface { face_index } => {
449                write!(f, "face {face_index} has an unknown support surface")
450            }
451        }
452    }
453}
454
455impl std::error::Error for ExactBRepError {}
456
457fn validate(value: &ExactBRepBuilder) -> Result<(), ExactBRepError> {
458    if value.topology.faces().is_empty() {
459        return Err(ExactBRepError::Empty);
460    }
461    let health = audit_brep(&value.topology);
462    if !health.is_tessellable() {
463        return Err(ExactBRepError::Topology(health));
464    }
465    for (edge_index, edge) in value.topology.edges().iter().enumerate() {
466        let Some(curve) = edge.curve else {
467            return Err(ExactBRepError::MissingEdgeCurve { edge_index });
468        };
469        if curve.index() >= value.curves3.len() {
470            return Err(ExactBRepError::UnknownCurve3 { edge_index });
471        }
472        let Some(edge_id) = value.topology.edge_id_at(edge_index) else {
473            return Err(ExactBRepError::MissingEdgeInterval { edge_index });
474        };
475        let Some(interval) = value.edge_intervals.get(&edge_id) else {
476            return Err(ExactBRepError::MissingEdgeInterval { edge_index });
477        };
478        if !valid_interval(*interval) {
479            return Err(ExactBRepError::InvalidEdgeInterval { edge_index });
480        }
481    }
482    for (loop_index, loop_) in value.topology.loops().iter().enumerate() {
483        let Some(loop_id) = value.topology.loop_id_at(loop_index) else {
484            return Err(ExactBRepError::Topology(health));
485        };
486        for (use_index, use_) in loop_.edges.iter().enumerate() {
487            let Some(curve) = use_.pcurve else {
488                return Err(ExactBRepError::MissingPcurve {
489                    loop_index,
490                    use_index,
491                });
492            };
493            if curve.index() >= value.curves2.len() {
494                return Err(ExactBRepError::UnknownCurve2 {
495                    loop_index,
496                    use_index,
497                });
498            }
499            let Some(interval) = value.pcurve_intervals.get(&(loop_id, use_index)) else {
500                return Err(ExactBRepError::MissingPcurveInterval {
501                    loop_index,
502                    use_index,
503                });
504            };
505            if !valid_interval(*interval) {
506                return Err(ExactBRepError::InvalidPcurveInterval {
507                    loop_index,
508                    use_index,
509                });
510            }
511        }
512    }
513    for (face_index, face) in value.topology.faces().iter().enumerate() {
514        let Some(surface) = face.surface else {
515            return Err(ExactBRepError::MissingFaceSurface { face_index });
516        };
517        if surface.index() >= value.surfaces.len() {
518            return Err(ExactBRepError::UnknownSurface { face_index });
519        }
520    }
521    Ok(())
522}
523
524fn valid_interval(interval: Interval) -> bool {
525    interval.start.is_finite() && interval.end.is_finite() && interval.length() > 0.0
526}