Skip to main content

brepkit_offset/
lib.rs

1//! # brepkit-offset
2//!
3//! Solid offset engine for brepkit.
4//!
5//! This is layer L2, depending on `brepkit-math`, `brepkit-topology`,
6//! and `brepkit-geometry`.
7//!
8//! # Pipeline
9//!
10//! The offset algorithm follows a 9-phase pipeline:
11//!
12//! 1. **Analyse** — classify edges as convex/concave/tangent, derive vertex
13//!    classes.
14//! 2. **Offset** — construct the offset surface for each face (translate
15//!    planes, adjust cylinder radii, etc.).
16//! 3. **Intersect 3D** — intersect adjacent offset faces in 3D to find new
17//!    edge curves.
18//! 4. **Intersect 2D** — intersect offset PCurves in parameter space to find
19//!    edge split points.
20//! 5. **Split edges** — split original edges at intersection parameters.
21//! 6. **Arc joints** — optionally insert rolling-ball arc fillets at convex
22//!    edges.
23//! 7. **Build loops** — assemble trimmed edges into closed wire loops for each
24//!    offset face.
25//! 8. **Assemble** — build the final shell and solid from offset faces and
26//!    wire loops.
27//! 9. **Self-intersection removal** — detect and excise global
28//!    self-intersections if enabled.
29
30pub(crate) mod analyse;
31pub(crate) mod arc_joint;
32pub(crate) mod assemble;
33pub(crate) mod data;
34pub mod error;
35pub(crate) mod image;
36pub(crate) mod inter2d;
37pub(crate) mod inter3d;
38pub(crate) mod loops;
39pub(crate) mod offset;
40pub(crate) mod self_int;
41
42pub use data::{JointType, OffsetOptions};
43pub use error::OffsetError;
44
45use brepkit_topology::Topology;
46use brepkit_topology::face::FaceId;
47use brepkit_topology::solid::SolidId;
48
49use crate::data::OffsetData;
50
51/// Offset all faces of a solid by the given signed distance.
52///
53/// Positive distance offsets outward (enlarges), negative inward (shrinks).
54///
55/// # Errors
56///
57/// Returns [`OffsetError`] if the offset collapses the solid, any
58/// intersection fails, or the result cannot be assembled into a valid solid.
59pub fn offset_solid(
60    topo: &mut Topology,
61    solid: SolidId,
62    distance: f64,
63    options: OffsetOptions,
64) -> Result<SolidId, OffsetError> {
65    if !distance.is_finite() || distance.abs() < options.tolerance.linear {
66        return Err(OffsetError::InvalidInput {
67            reason: "offset distance must be non-zero and finite".into(),
68        });
69    }
70    if options.joint == JointType::Intersection
71        && let Some(result) = image::offset_solid(topo, solid, distance, options.tolerance.linear)?
72    {
73        return Ok(result);
74    }
75    offset_by_intersection(topo, solid, distance, &[], options)
76}
77
78/// Offset a solid while excluding specific faces, producing a thick
79/// (hollowed) solid.
80///
81/// Excluded faces are left at their original positions, and side walls
82/// connect them to the offset faces. With no excluded face the wall closes
83/// round a cavity, which only the exact path ([`thick_solid_by_image`])
84/// builds.
85///
86/// # Errors
87///
88/// Returns [`OffsetError`] if the offset collapses the solid, any
89/// intersection fails, the result cannot be assembled into a valid solid, or
90/// no face is excluded and the exact path does not apply.
91pub fn thick_solid(
92    topo: &mut Topology,
93    solid: SolidId,
94    distance: f64,
95    exclude: &[FaceId],
96    options: OffsetOptions,
97) -> Result<SolidId, OffsetError> {
98    if !distance.is_finite() || distance.abs() < options.tolerance.linear {
99        return Err(OffsetError::InvalidInput {
100            reason: "offset distance must be non-zero and finite".into(),
101        });
102    }
103    if options.joint == JointType::Intersection
104        && let Some(result) =
105            image::thick_solid(topo, solid, distance, exclude, options.tolerance.linear)?
106    {
107        return Ok(result);
108    }
109    if exclude.is_empty() {
110        return Err(OffsetError::InvalidInput {
111            reason: "a wall with no opening is built only where it keeps the solid's topology"
112                .into(),
113        });
114    }
115    offset_by_intersection(topo, solid, distance, exclude, options)
116}
117
118/// The thick solid of [`thick_solid`] built exact, as images of the input's
119/// faces with sharp joints, or `None` when that does not apply. With no
120/// excluded face the wall closes round a cavity.
121///
122/// It does not apply when the wall would not keep the input's topology (a
123/// face collapses, an edge turns round, loops meet), or to NURBS faces,
124/// edges other than lines and circles, open faces that are curved, holed,
125/// or touch each other, and solids with cavities.
126///
127/// # Errors
128///
129/// Returns [`OffsetError`] if `distance` is zero or not finite, or a
130/// topology lookup fails.
131pub fn thick_solid_by_image(
132    topo: &mut Topology,
133    solid: SolidId,
134    distance: f64,
135    exclude: &[FaceId],
136    tolerance: f64,
137) -> Result<Option<SolidId>, OffsetError> {
138    if !distance.is_finite() || distance.abs() < tolerance {
139        return Err(OffsetError::InvalidInput {
140            reason: "offset distance must be non-zero and finite".into(),
141        });
142    }
143    image::thick_solid(topo, solid, distance, exclude, tolerance)
144}
145
146/// The phased pipeline: offset faces intersected pairwise and rebuilt into
147/// loops.
148fn offset_by_intersection(
149    topo: &mut Topology,
150    solid: SolidId,
151    distance: f64,
152    exclude: &[FaceId],
153    options: OffsetOptions,
154) -> Result<SolidId, OffsetError> {
155    let mut data = OffsetData::new(distance, options, exclude.to_vec());
156
157    analyse::analyse_edges(topo, solid, &mut data)?;
158
159    offset::build_offset_faces(topo, solid, &mut data)?;
160
161    inter3d::intersect_faces_3d(topo, solid, &mut data)?;
162
163    inter2d::intersect_pcurves_2d(topo, solid, &mut data)?;
164
165    // Edge splitting (phase 5) is integrated into inter2d for now.
166
167    if data.options.joint == JointType::Arc {
168        arc_joint::build_arc_joints(topo, &mut data)?;
169    }
170
171    loops::build_wire_loops(topo, &mut data)?;
172
173    let result = assemble::assemble_solid(topo, &data)?;
174
175    if data.options.remove_self_intersections {
176        return self_int::remove_self_intersections(topo, result);
177    }
178
179    Ok(result)
180}