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 inter2d;
36pub(crate) mod inter3d;
37pub(crate) mod loops;
38pub(crate) mod offset;
39pub(crate) mod self_int;
40
41pub use data::{JointType, OffsetOptions};
42pub use error::OffsetError;
43
44use brepkit_topology::Topology;
45use brepkit_topology::face::FaceId;
46use brepkit_topology::solid::SolidId;
47
48use crate::data::OffsetData;
49
50/// Offset all faces of a solid by the given signed distance.
51///
52/// Positive distance offsets outward (enlarges), negative inward (shrinks).
53///
54/// # Errors
55///
56/// Returns [`OffsetError`] if the offset collapses the solid, any
57/// intersection fails, or the result cannot be assembled into a valid solid.
58pub fn offset_solid(
59    topo: &mut Topology,
60    solid: SolidId,
61    distance: f64,
62    options: OffsetOptions,
63) -> Result<SolidId, OffsetError> {
64    thick_solid(topo, solid, distance, &[], options)
65}
66
67/// Offset a solid while excluding specific faces, producing a thick
68/// (hollowed) solid.
69///
70/// Excluded faces are left at their original positions, and side walls
71/// connect them to the offset faces.
72///
73/// # Errors
74///
75/// Returns [`OffsetError`] if the offset collapses the solid, any
76/// intersection fails, or the result cannot be assembled into a valid solid.
77#[allow(clippy::too_many_lines)]
78pub fn thick_solid(
79    topo: &mut Topology,
80    solid: SolidId,
81    distance: f64,
82    exclude: &[FaceId],
83    options: OffsetOptions,
84) -> Result<SolidId, OffsetError> {
85    if !distance.is_finite() || distance.abs() < options.tolerance.linear {
86        return Err(OffsetError::InvalidInput {
87            reason: "offset distance must be non-zero and finite".into(),
88        });
89    }
90
91    let mut data = OffsetData::new(distance, options, exclude.to_vec());
92
93    analyse::analyse_edges(topo, solid, &mut data)?;
94
95    offset::build_offset_faces(topo, solid, &mut data)?;
96
97    inter3d::intersect_faces_3d(topo, solid, &mut data)?;
98
99    inter2d::intersect_pcurves_2d(topo, solid, &mut data)?;
100
101    // Edge splitting (phase 5) is integrated into inter2d for now.
102
103    if data.options.joint == JointType::Arc {
104        arc_joint::build_arc_joints(topo, &mut data)?;
105    }
106
107    loops::build_wire_loops(topo, &mut data)?;
108
109    let result = assemble::assemble_solid(topo, &data)?;
110
111    if data.options.remove_self_intersections {
112        return self_int::remove_self_intersections(topo, result);
113    }
114
115    Ok(result)
116}