brepkit_operations/lib.rs
1//! # brepkit-operations
2//!
3//! CAD modeling operations for B-Rep solids, and the entry point for Rust
4//! consumers of brepkit. Layer L3, depending on `brepkit-math`,
5//! `brepkit-topology`, `brepkit-geometry`, `brepkit-algo`, `brepkit-blend`,
6//! `brepkit-heal`, `brepkit-check`, `brepkit-offset`, and `brepkit-sketch`.
7//!
8//! # Getting started
9//!
10//! ```
11//! use brepkit_operations::boolean::{boolean, BooleanOp};
12//! use brepkit_operations::measure::solid_volume;
13//! use brepkit_operations::primitives::{make_box, make_cylinder};
14//! use brepkit_topology::Topology;
15//!
16//! let mut topo = Topology::new();
17//!
18//! // Primitives are anchored at the origin, so this cylinder rounds off the
19//! // block's corner. Use `transform_solid` to place it somewhere else.
20//! let block = make_box(&mut topo, 30.0, 20.0, 10.0)?;
21//! let cutter = make_cylinder(&mut topo, 5.0, 15.0)?;
22//! let notched = boolean(&mut topo, BooleanOp::Cut, block, cutter)?;
23//!
24//! // A quarter-cylinder of radius 5 and height 10 is gone from the corner.
25//! let expected = 30.0 * 20.0 * 10.0 - 0.25 * std::f64::consts::PI * 25.0 * 10.0;
26//! let volume = solid_volume(&topo, notched, 0.01)?;
27//! assert!((volume - expected).abs() / expected < 1e-3);
28//! # Ok::<(), brepkit_operations::OperationsError>(())
29//! ```
30//!
31//! # Conventions
32//!
33//! Modeling operations take the [`Topology`](brepkit_topology::Topology) arena
34//! as `&mut` and return a typed handle into it, so results compose without
35//! copying geometry. Interrogation does not: measurement, classification,
36//! validation, distance, and query borrow the arena as `&` and return a value,
37//! whether a number, a report, or a collection.
38//!
39//! Fallible work returns a [`Result`] rather than panicking. `unwrap`,
40//! `expect`, and `panic!` are denied by lint across the workspace.
41//!
42//! Primitives are anchored at the origin. Place them with
43//! [`transform`] rather than expecting a position argument.
44//!
45//! # Exact geometry, and when it degrades
46//!
47//! Booleans run on an exact path that preserves analytic and NURBS surfaces.
48//! A cylinder cut by a plane stays a cylinder, so face counts stay flat across
49//! chained operations instead of compounding: a nine-step compound boolean
50//! settles around 72 faces where a mesh-based approach would reach several
51//! thousand.
52//!
53//! Some configurations defeat that path and fall back to a mesh-based boolean
54//! built on co-refinement. The usual causes are coincident-face contact,
55//! coaxial analytic surfaces, razor-thin geometry, and very high face counts.
56//! The fallback returns a usable, non-degenerate solid, but the curved faces
57//! come back tessellated and the result is not guaranteed watertight.
58//!
59//! The fallback does not announce itself in the return value, which matters
60//! most for export pipelines: a STEP file written from a fallback result
61//! carries triangles where it should carry a cylinder. Snapshot
62//! [`boolean::mesh_fallback_count`] around the chain and refuse the output
63//! when it grew.
64//!
65//! ```
66//! use brepkit_operations::boolean::{boolean, mesh_fallback_count, BooleanOp};
67//! use brepkit_operations::primitives::{make_box, make_cylinder};
68//! use brepkit_operations::validate::validate_solid;
69//! use brepkit_topology::Topology;
70//!
71//! let mut topo = Topology::new();
72//! let block = make_box(&mut topo, 30.0, 20.0, 10.0)?;
73//! let cutter = make_cylinder(&mut topo, 5.0, 15.0)?;
74//!
75//! let before = mesh_fallback_count();
76//! let notched = boolean(&mut topo, BooleanOp::Cut, block, cutter)?;
77//!
78//! // This cut takes the exact path, so the counter is unmoved and the
79//! // rounded wall is still a real cylinder.
80//! assert_eq!(mesh_fallback_count(), before);
81//!
82//! // Topological checks: wire closure, manifold and boundary edges,
83//! // Euler characteristic, degenerate faces, and duplicate faces.
84//! assert!(validate_solid(&topo, notched)?.is_valid());
85//! # Ok::<(), brepkit_operations::OperationsError>(())
86//! ```
87//!
88//! # Verifying a result
89//!
90//! Three checks, in increasing cost, and they catch different things:
91//!
92//! 1. [`validate::validate_solid`] reports topological defects: an unclosed
93//! wire, a shell with a free edge, a non-manifold edge, a wrong Euler
94//! characteristic, a degenerate face. Cheap, and the right default.
95//! 2. [`measure::solid_volume`] against a closed-form expectation catches
96//! geometric errors that leave the topology intact, which is the failure
97//! mode a boolean is most likely to produce. Pass a tight deflection:
98//! a coarse one under-counts curved faces and will disagree with itself
99//! across values.
100//! 3. [`heal::heal_solid`] repairs what the first two find, merging
101//! coincident vertices, dropping degenerate edges, closing wire gaps, and
102//! fixing face orientation.
103//!
104//! A solid that passes `validate_solid` is well-formed, not necessarily
105//! correct. Volume is what distinguishes the two.
106//!
107//! # Module families
108//!
109//! | Family | Modules | Purpose |
110//! |--------|---------|---------|
111//! | **Core** | [`primitives`], [`extrude`], [`revolve`], [`sweep`], [`loft`], [`pipe`], [`helix`] | Shape creation |
112//! | **Transform** | [`transform`], [`copy`], [`mirror`], [`pattern`] | Spatial operations |
113//! | **Boolean** | [`boolean`], [`mesh_boolean`] | Set operations |
114//! | **Blend** | [`fillet`], [`chamfer`], [`blend_ops`] | Edge smoothing |
115//! | **Offset** | [`offset_face`], [`offset_trim`], [`offset_v2`], [`offset_wire`] | Wall thickness |
116//! | **Surface** | [`fill_face`], [`thicken`], [`shell_op`], [`draft`], [`section`], [`split`] | Surface/solid modification |
117//! | **Repair** | [`heal`], [`defeature`], [`sew`], [`untrim`] | Shape fixing |
118//! | **Analysis** | [`measure`], [`distance`], [`classify`], [`validate`], [`query`], [`feature_recognition`] | Interrogation |
119//! | **Tessellation** | [`tessellate`] | Mesh generation |
120//! | **Infrastructure** | [`assembly`], [`compound_ops`], [`evolution`], [`sketch`] | Utilities |
121//!
122//! # See also
123//!
124//! - [`brepkit_io`](https://docs.rs/brepkit-io): reading and writing STEP and
125//! the mesh formats.
126//! - [`brepkit_topology`](https://docs.rs/brepkit-topology): the arena every
127//! operation here takes, and the surface enums it stores.
128//! - [brepjs.dev](https://brepjs.dev): concepts, task recipes, and the
129//! TypeScript API built on this kernel.
130
131use brepkit_math::vec::{Point3, Vec3};
132
133pub mod extrude;
134pub mod helix;
135pub mod loft;
136pub mod pipe;
137pub mod primitives;
138pub mod projection;
139pub mod revolve;
140pub mod sweep;
141
142pub mod copy;
143pub mod mirror;
144pub mod pattern;
145pub mod transform;
146
147pub mod boolean;
148pub mod mesh_boolean;
149
150pub mod blend_ops;
151pub mod chamfer;
152pub mod fillet;
153
154pub mod offset_face;
155pub mod offset_trim;
156pub mod offset_v2;
157pub mod offset_wire;
158
159pub mod draft;
160pub mod fill_face;
161pub mod section;
162pub mod shell_op;
163pub mod split;
164pub mod thicken;
165
166pub mod defeature;
167pub mod heal;
168pub mod sew;
169pub mod untrim;
170
171pub mod classify;
172pub mod distance;
173pub mod feature_recognition;
174pub mod measure;
175pub mod query;
176pub mod validate;
177
178pub mod tessellate;
179
180pub mod assembly;
181pub(crate) mod cap;
182pub mod compound_ops;
183pub mod evolution;
184pub mod sketch;
185pub(crate) mod winding;
186
187#[cfg(test)]
188pub(crate) mod test_helpers;
189
190/// Compute `n · p` treating a `Point3` as a direction vector.
191///
192/// Equivalent to the dot product `n.x*p.x + n.y*p.y + n.z*p.z`, used
193/// for the plane equation `n · point = d`.
194fn dot_normal_point(n: Vec3, p: Point3) -> f64 {
195 n.dot(Vec3::new(p.x(), p.y(), p.z()))
196}
197
198/// Errors from modeling operations.
199#[derive(Debug, thiserror::Error)]
200pub enum OperationsError {
201 /// The input shape is invalid for this operation.
202 #[error("invalid input: {reason}")]
203 InvalidInput {
204 /// Description of what is wrong.
205 reason: String,
206 },
207
208 /// The operation produced a non-manifold result.
209 #[error("non-manifold result")]
210 NonManifoldResult,
211
212 /// The operation produced an empty result (no geometry).
213 ///
214 /// Boolean operations return this when the algebraic outcome is the
215 /// empty set: `Cut(A, B)` when `A ⊆ B`, or any operation on
216 /// pre-collapsed inputs. Distinguishable from [`InvalidInput`] so
217 /// callers can apply empty-operand identity rules without
218 /// string-matching the error message.
219 ///
220 /// [`InvalidInput`]: Self::InvalidInput
221 #[error("empty result: {reason}")]
222 EmptyResult {
223 /// Description of the empty-result scenario.
224 reason: String,
225 },
226
227 /// A referenced topology entity was not found.
228 #[error(transparent)]
229 Topology(#[from] brepkit_topology::TopologyError),
230
231 /// A math error occurred during the operation.
232 #[error(transparent)]
233 Math(#[from] brepkit_math::MathError),
234
235 /// A GFA algorithm error occurred.
236 #[error("algo: {0}")]
237 Algo(#[from] brepkit_algo::error::AlgoError),
238
239 /// A blend (fillet/chamfer v2) error occurred.
240 #[error("blend: {0}")]
241 Blend(#[from] brepkit_blend::BlendError),
242
243 /// A check (classification/validation/distance) error occurred.
244 #[error("check: {0}")]
245 Check(#[from] brepkit_check::CheckError),
246
247 /// A geometry conversion error occurred.
248 #[error("geometry: {0}")]
249 Geometry(#[from] brepkit_geometry::error::GeomError),
250}