brepkit_topology/lib.rs
1//! # brepkit-topology
2//!
3//! Arena-allocated boundary representation (B-Rep) data structures.
4//! Layer L1, depending only on `brepkit-math`.
5//!
6//! # Topology and geometry are separate
7//!
8//! A B-Rep solid is defined by its boundary: the surfaces, edges, and vertices
9//! that form its skin. brepkit keeps *how things connect* apart from *where
10//! they are in space*.
11//!
12//! - **Topology**: [`Vertex`](vertex::Vertex) to [`Edge`](edge::Edge) to
13//! [`Wire`](wire::Wire) to [`Face`](face::Face) to [`Shell`](shell::Shell)
14//! to [`Solid`](solid::Solid).
15//! - **Geometry**: points, curves, and surfaces, all owned by `brepkit-math`.
16//!
17//! A [`Face`](face::Face) knows which wires bound it (topology) and which
18//! [`FaceSurface`](face::FaceSurface) defines its shape (geometry). Keeping
19//! the two apart is what lets a boolean reason about connectivity without
20//! re-deriving it from coordinates every time.
21//!
22//! # Arena allocation
23//!
24//! Every entity lives in a central [`Arena`] and is referenced by a typed
25//! [`Id<T>`](arena::Id) handle rather than a pointer or an `Rc`. This keeps
26//! traversal cache-friendly, drops reference-counting overhead, gives O(1)
27//! lookup, and makes ownership unambiguous: the arena owns everything, and a
28//! handle is just an index.
29//!
30//! The consequence to know about is aliasing. You cannot hold a shared borrow
31//! of the arena while taking a mutable one, so read what you need into locals
32//! first, then allocate:
33//!
34//! ```
35//! use brepkit_math::vec::Point3;
36//! use brepkit_topology::Topology;
37//! use brepkit_topology::vertex::Vertex;
38//!
39//! let mut topo = Topology::new();
40//! let original = topo.add_vertex(Vertex::new(Point3::new(1.0, 2.0, 3.0), 1e-7));
41//!
42//! // Snapshot the read, then allocate. Doing both in one expression would
43//! // borrow the arena immutably and mutably at the same time.
44//! let position = topo.vertex(original)?.point();
45//! let copy = topo.add_vertex(Vertex::new(position, 1e-7));
46//!
47//! assert_eq!(topo.vertex(copy)?.point().x(), 1.0);
48//! # Ok::<(), brepkit_topology::TopologyError>(())
49//! ```
50//!
51//! # Surfaces and curves are enums
52//!
53//! [`FaceSurface`](face::FaceSurface) is one of `Plane`, `Cylinder`, `Cone`,
54//! `Sphere`, `Torus`, or `Nurbs`. [`EdgeCurve`](edge::EdgeCurve) is one of
55//! `Line`, `Circle`, `Ellipse`, or `NurbsCurve`. Analytic types are
56//! deliberately special-cased rather than collapsed into NURBS:
57//!
58//! - Operations preserve them. A cylinder cut by a plane stays a cylinder, so
59//! face counts stay flat across chained booleans instead of growing at every
60//! step.
61//! - Intersections take exact closed-form paths where a pair allows one,
62//! falling back to NURBS marching only when no analytic solution exists.
63//! - STEP export writes them as native surface entities, so a round-trip is
64//! lossless rather than an approximation.
65//!
66//! Both enums are exhaustive, with no `_ =>` wildcards in production code.
67//! That is a deliberate trade: adding a variant is a breaking change for every
68//! downstream matcher, but the compiler finds every site that needs updating.
69//! Prefer the delegate methods (`evaluate`, `normal`, `type_tag`, and the
70//! rest, defined in `brepkit_math::traits`) over matching variants directly,
71//! since code that goes through a delegate keeps compiling when a variant is
72//! added.
73//!
74//! # A solid is not just its outer shell
75//!
76//! This is the trap that costs the most time. A [`Solid`](solid::Solid) has an
77//! outer shell *and* zero or more inner shells, which are the cavity walls
78//! left by a hollowing operation or a boolean cut that opened a void. Code
79//! that reaches through `outer_shell()` and iterates its faces compiles, runs,
80//! and gives the right answer on every model without a cavity. On a hollow
81//! part it silently skips the interior and reports a volume or a face count
82//! that is quietly wrong. (A bounding box survives, because a cavity sits
83//! inside the outer shell and cannot extend it.)
84//!
85//! Use [`explorer::solid_faces`], which flattens outer and inner shells into
86//! one list:
87//!
88//! ```
89//! use brepkit_topology::Topology;
90//! use brepkit_topology::explorer::solid_faces;
91//!
92//! let mut topo = Topology::new();
93//! let solid = topo.add_empty_solid();
94//!
95//! // Covers cavity faces too. Walking `outer_shell()` by hand does not.
96//! let faces = solid_faces(&topo, solid)?;
97//! assert!(faces.is_empty());
98//! # Ok::<(), brepkit_topology::TopologyError>(())
99//! ```
100//!
101//! The exception is work that is genuinely per-shell: orientation fixes,
102//! sewing, and any guard whose job is to reason about one shell at a time.
103//! Those should keep iterating shell by shell. The rule is about scope. If the
104//! question is about the solid (how many faces, what does it weigh, what type
105//! is this feature), flatten. If the question is about a shell, do not.
106//!
107//! [`explorer::solid_edges`] and [`explorer::solid_vertices`] follow the same
108//! convention.
109//!
110//! # See also
111//!
112//! - [`brepkit_math`](https://docs.rs/brepkit-math): the geometry these
113//! structures carry, and the tolerance model they compare with.
114//! - [`brepkit_operations`](https://docs.rs/brepkit-operations): the modeling
115//! operations that build and consume this topology.
116//! - [brepjs.dev](https://brepjs.dev/concepts/topology): the same hierarchy
117//! from the TypeScript side.
118
119pub mod adjacency;
120pub mod arena;
121pub mod builder;
122pub mod compound;
123pub mod compsolid;
124pub mod edge;
125pub mod explorer;
126pub mod face;
127pub mod orientation;
128
129pub mod pcurve;
130pub mod shell;
131pub mod solid;
132#[cfg(feature = "test-utils")]
133pub mod test_utils;
134pub mod topology;
135pub mod validation;
136pub mod vertex;
137pub mod wire;
138
139pub use arena::Arena;
140pub use compound::CompoundId;
141pub use compsolid::CompSolidId;
142pub use edge::EdgeId;
143pub use face::FaceId;
144pub use shell::ShellId;
145pub use solid::SolidId;
146pub use topology::Topology;
147pub use vertex::VertexId;
148pub use wire::{OrientedEdge, WireId};
149
150/// Errors from topology operations.
151#[derive(Debug, thiserror::Error)]
152pub enum TopologyError {
153 /// A referenced vertex ID does not exist in the arena.
154 #[error("vertex {0:?} not found")]
155 VertexNotFound(vertex::VertexId),
156
157 /// A referenced edge ID does not exist in the arena.
158 #[error("edge {0:?} not found")]
159 EdgeNotFound(edge::EdgeId),
160
161 /// A referenced wire ID does not exist in the arena.
162 #[error("wire {0:?} not found")]
163 WireNotFound(wire::WireId),
164
165 /// A referenced face ID does not exist in the arena.
166 #[error("face {0:?} not found")]
167 FaceNotFound(face::FaceId),
168
169 /// A referenced shell ID does not exist in the arena.
170 #[error("shell {0:?} not found")]
171 ShellNotFound(shell::ShellId),
172
173 /// A referenced solid ID does not exist in the arena.
174 #[error("solid {0:?} not found")]
175 SolidNotFound(solid::SolidId),
176
177 /// A referenced compound ID does not exist in the arena.
178 #[error("compound {0:?} not found")]
179 CompoundNotFound(compound::CompoundId),
180
181 /// A referenced comp-solid ID does not exist in the arena.
182 #[error("compsolid {0:?} not found")]
183 CompSolidNotFound(compsolid::CompSolidId),
184
185 /// A wire does not form a closed loop.
186 #[error("wire is not closed")]
187 WireNotClosed,
188
189 /// The topology is not manifold.
190 #[error("non-manifold topology: {reason}")]
191 NonManifold {
192 /// Description of the manifold violation.
193 reason: String,
194 },
195
196 /// An empty collection was provided where at least one element is required.
197 #[error("empty {entity} — at least one element is required")]
198 Empty {
199 /// The kind of entity that was empty.
200 entity: &'static str,
201 },
202
203 /// A wire's edge geometry does not lie within tolerance of any single
204 /// plane, so a planar face cannot be constructed from it.
205 #[error("wire is not planar")]
206 NotPlanar,
207}