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