Skip to main content

brepkit_operations/boolean/
types.rs

1#![allow(dead_code)]
2//! Shared type definitions, constants, and the selection truth table for the
3//! boolean pipeline.
4
5use brepkit_math::surfaces::CylindricalSurface;
6use brepkit_math::tolerance::Tolerance;
7use brepkit_math::vec::{Point3, Vec3};
8use brepkit_topology::edge::EdgeCurve;
9use brepkit_topology::face::{FaceId, FaceSurface};
10
11/// Number of samples used when discretizing closed curves (circles, ellipses)
12/// in the analytic boolean path. All code paths must use this constant so that
13/// band fragments, cap face polygons, and holed-face inner wires share the
14/// same vertices and edges at their boundaries.
15pub(super) const CLOSED_CURVE_SAMPLES: usize = 32;
16
17/// Minimum fragment count for parallel classification via rayon.
18/// Below this threshold, sequential iteration is faster due to rayon's
19/// thread-pool synchronization overhead (~5-20us).
20#[cfg(not(target_arch = "wasm32"))]
21pub(super) const PARALLEL_THRESHOLD: usize = 64;
22
23/// Default tessellation deflection for non-planar faces in boolean operations.
24///
25/// A larger value produces fewer triangles (faster but coarser approximation).
26/// Since the boolean result decomposes curved faces into individual planar
27/// triangles, keeping this coarse avoids face-count explosion in sequential
28/// boolean operations.
29pub(super) const DEFAULT_BOOLEAN_DEFLECTION: f64 = 0.1;
30
31/// Number of angular segments used to approximate cylinder faces in the
32/// classification face data. 16 segments = 16 quads per cylinder band,
33/// sufficient for correct ray-crossing parity.
34pub(super) const CLASSIFIER_CYL_SEGMENTS: usize = 16;
35
36/// Threshold: use CDT batch splitting for faces with this many or more chords.
37///
38/// Below this threshold, the iterative approach is fast enough and avoids the
39/// CDT setup overhead. Above it, the iterative O(N*F) approach becomes a
40/// bottleneck while CDT stays O(N log N).
41pub(super) const CDT_CHORD_THRESHOLD: usize = 5;
42
43/// Snap distance multiplier for CDT vertex matching.
44///
45/// Chord endpoints are computed by line-edge intersection, which accumulates
46/// floating-point error on the order of ~10x `tol.linear`. Use 100x as the
47/// snap threshold to reliably capture all on-chord/on-boundary vertices
48/// without pulling in nearby-but-off-chord polygon vertices.
49pub(super) const CDT_SNAP_FACTOR: f64 = 100.0;
50
51/// Minimum face count for a valid solid.
52///
53/// A cylinder (2 caps + 1 barrel = 3 faces) is the minimal closed solid
54/// produced by boolean operations between boxes and curved primitives.
55pub(super) const MIN_SOLID_FACES: usize = 3;
56
57/// The type of boolean operation.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum BooleanOp {
60    /// Union of two solids.
61    Fuse,
62    /// Subtraction: first minus second.
63    Cut,
64    /// Intersection: common volume.
65    Intersect,
66}
67
68/// A face specification for mixed-surface solid assembly.
69///
70/// Used by `assemble_solid_mixed` to build solids with faces of any
71/// surface type -- not just planar.
72#[derive(Clone)]
73pub enum FaceSpec {
74    /// A planar face defined by vertex positions and plane equation.
75    Planar {
76        /// Vertex positions (at least 3).
77        vertices: Vec<Point3>,
78        /// Outward-facing normal.
79        normal: Vec3,
80        /// Plane equation signed distance (n * p = d).
81        d: f64,
82        /// Inner wire vertex loops (holes in the face).
83        inner_wires: Vec<Vec<Point3>>,
84    },
85    /// A face with a pre-built surface and vertex positions for the boundary wire.
86    Surface {
87        /// Vertex positions for the outer wire (at least 3).
88        vertices: Vec<Point3>,
89        /// The surface geometry.
90        surface: FaceSurface,
91        /// Whether the face's surface normal should be reversed.
92        reversed: bool,
93        /// Inner wire vertex loops (holes in the face).
94        inner_wires: Vec<Vec<Point3>>,
95    },
96    /// A cylindrical face with circle edges on angular boundaries.
97    ///
98    /// Unlike `Surface`, this variant creates `EdgeCurve::Circle` for edges
99    /// that span an angular range on the cylinder (constant-v boundaries),
100    /// preserving curve geometry for correct tessellation and volume computation.
101    CylindricalFace {
102        /// Vertex positions for the outer wire (at least 3).
103        vertices: Vec<Point3>,
104        /// The cylindrical surface geometry.
105        cylinder: CylindricalSurface,
106        /// Whether the face's surface normal should be reversed.
107        reversed: bool,
108        /// Inner wire vertex loops (holes in the face).
109        inner_wires: Vec<Vec<Point3>>,
110    },
111}
112
113impl FaceSpec {
114    /// Returns a reference to this face's inner wires.
115    #[must_use]
116    pub fn inner_wires(&self) -> &[Vec<Point3>] {
117        match self {
118            Self::Planar { inner_wires, .. }
119            | Self::Surface { inner_wires, .. }
120            | Self::CylindricalFace { inner_wires, .. } => inner_wires,
121        }
122    }
123
124    /// Returns a mutable reference to this face's inner wires.
125    pub fn inner_wires_mut(&mut self) -> &mut Vec<Vec<Point3>> {
126        match self {
127            Self::Planar { inner_wires, .. }
128            | Self::Surface { inner_wires, .. }
129            | Self::CylindricalFace { inner_wires, .. } => inner_wires,
130        }
131    }
132}
133
134/// Options for boolean operations.
135#[derive(Debug, Clone, Copy)]
136pub struct BooleanOptions {
137    /// Tessellation deflection for non-planar faces.
138    ///
139    /// Lower values produce more triangles (more accurate but slower).
140    /// Default: 0.1.
141    pub deflection: f64,
142    /// Tolerance for geometric comparisons.
143    ///
144    /// Controls vertex merging, point classification, and predicate
145    /// thresholds throughout the boolean pipeline. Default: `Tolerance::new()`.
146    pub tolerance: Tolerance,
147    /// Merge co-surface face fragments after assembly.
148    ///
149    /// When `true`, the pipeline calls `unify_faces` to merge adjacent faces
150    /// that share the same underlying surface (same-domain
151    /// analysis). This dramatically reduces face count -- e.g. sequential
152    /// booleans on curved surfaces drop from 2871 to ~106 faces.
153    ///
154    /// Non-convex merged faces are handled correctly by the
155    /// `polygon_clip_intervals` fallback in the analytic chord splitter,
156    /// so this is safe for intermediate results fed into further booleans.
157    ///
158    /// Default: `true`.
159    pub unify_faces: bool,
160    /// Run full shape healing on the boolean result via [`crate::heal::heal_solid`].
161    ///
162    /// Use for final results only -- healing can corrupt intermediates fed into
163    /// further booleans (non-convex merged faces confuse chord splitting).
164    ///
165    /// Default: `false`.
166    pub heal_after_boolean: bool,
167}
168
169impl Default for BooleanOptions {
170    fn default() -> Self {
171        Self {
172            deflection: DEFAULT_BOOLEAN_DEFLECTION,
173            tolerance: Tolerance::new(),
174            unify_faces: true,
175            heal_after_boolean: false,
176        }
177    }
178}
179
180/// Which operand a face fragment originated from.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum Source {
183    A,
184    B,
185}
186
187pub(super) use brepkit_algo::FaceClass;
188
189/// Result of classifying an intersection curve against a face boundary.
190pub(super) enum CurveClassification {
191    /// The curve crosses the face boundary -- contains entry/exit points.
192    Crossings(Vec<Point3>),
193    /// The entire curve lies inside the face (no boundary crossings).
194    FullyContained,
195    /// The entire curve lies outside the face.
196    FullyOutside,
197}
198
199/// Internal context carrying tolerance-derived thresholds through the boolean
200/// pipeline. Computed once from `BooleanOptions` at the start of a boolean
201/// operation to avoid repeated derivation and hardcoded epsilon values.
202#[derive(Debug, Clone, Copy)]
203pub(super) struct BooleanContext {
204    /// Base tolerance (used when wiring ctx through the full pipeline).
205    #[allow(dead_code)]
206    pub(super) tol: Tolerance,
207    /// Vertex merge distance: vertices closer than this are considered identical.
208    pub(super) vertex_merge: f64,
209    /// Point classification tolerance: distance threshold for on-surface tests.
210    pub(super) classify_tol: f64,
211    /// Degenerate polygon threshold: skip polygons with area below this.
212    pub(super) degenerate_area: f64,
213}
214
215impl BooleanContext {
216    pub(super) fn from_options(opts: &BooleanOptions) -> Self {
217        let tol = opts.tolerance;
218        Self {
219            tol,
220            // 1000x linear tolerance for vertex merging -- aggressive enough to
221            // catch coincident vertices while preserving distinct features.
222            vertex_merge: tol.linear * 1000.0,
223            // Classification tolerance for point-in-solid tests.
224            classify_tol: tol.linear * 100.0,
225            // Degenerate area threshold (area < this -> skip polygon).
226            degenerate_area: tol.linear * tol.linear,
227        }
228    }
229}
230
231/// An intersection segment between two faces.
232#[derive(Debug)]
233pub(super) struct IntersectionSegment {
234    pub(super) face_a: FaceId,
235    pub(super) face_b: FaceId,
236    pub(super) p0: Point3,
237    pub(super) p1: Point3,
238}
239
240/// A fragment of a face after splitting along intersection chords.
241#[derive(Debug)]
242pub(super) struct FaceFragment {
243    pub(super) vertices: Vec<Point3>,
244    pub(super) normal: Vec3,
245    pub(super) d: f64,
246    pub(super) source: Source,
247}
248
249/// Parameters for a single face in a face-pair intersection test.
250pub(super) struct FacePairSide<'a> {
251    pub(super) fid: FaceId,
252    pub(super) verts: &'a [Point3],
253    pub(super) normal: Vec3,
254    pub(super) d: f64,
255}
256
257/// Snapshot of face data for analytic boolean processing.
258pub(super) struct FaceSnapshot {
259    pub(super) id: FaceId,
260    pub(super) surface: FaceSurface,
261    pub(super) vertices: Vec<Point3>,
262    pub(super) normal: Vec3,
263    pub(super) d: f64,
264    /// Whether the original face was reversed (needed to preserve orientation
265    /// when carrying unsplit faces through sequential booleans).
266    pub(super) reversed: bool,
267}
268
269/// Analytic face fragment preserving the original surface type.
270pub(super) struct AnalyticFragment {
271    /// Polygon boundary in 3D (for classification and planar assembly fallback).
272    pub(super) vertices: Vec<Point3>,
273    /// The original surface type of the face.
274    pub(super) surface: FaceSurface,
275    /// Normal of the face (for planar) or of the polygon approximation.
276    pub(super) normal: Vec3,
277    /// Plane d coefficient (for planar faces).
278    pub(super) d: f64,
279    /// Which operand this fragment came from.
280    pub(super) source: Source,
281    /// Edge curve types for the boundary segments.
282    /// `None` = straight line, `Some(curve)` = exact curve (circle, ellipse).
283    pub(super) edge_curves: Vec<Option<EdgeCurve>>,
284    /// Whether the source face was reversed (preserved for non-planar faces).
285    pub(super) source_reversed: bool,
286    /// The original input `FaceId` this fragment was created from.
287    /// Used by `BooleanState` to track provenance (images/origins).
288    pub(super) source_face_id: Option<FaceId>,
289}
290
291/// Extracted face data: `(FaceId, vertices, normal, d)`.
292pub(super) type FaceData = Vec<(FaceId, Vec<Point3>, Vec3, f64)>;
293
294/// Determine whether a fragment should be kept and whether to flip its normal.
295///
296/// Returns `Some(false)` to keep as-is, `Some(true)` to keep and flip, or
297/// `None` to discard.
298#[allow(clippy::match_same_arms)] // arms are semantically distinct (truth table rows)
299pub(super) const fn select_fragment(
300    source: Source,
301    class: FaceClass,
302    op: BooleanOp,
303) -> Option<bool> {
304    match (source, class, op) {
305        // From A, Outside B
306        (Source::A, FaceClass::Outside, BooleanOp::Fuse | BooleanOp::Cut) => Some(false),
307        (Source::A, FaceClass::Outside, BooleanOp::Intersect) => None,
308        // From A, Inside B
309        (Source::A, FaceClass::Inside, BooleanOp::Fuse | BooleanOp::Cut) => None,
310        (Source::A, FaceClass::Inside, BooleanOp::Intersect) => Some(false),
311        // From B, Outside A
312        (Source::B, FaceClass::Outside, BooleanOp::Fuse) => Some(false),
313        (Source::B, FaceClass::Outside, BooleanOp::Cut | BooleanOp::Intersect) => None,
314        // From B, Inside A
315        (Source::B, FaceClass::Inside, BooleanOp::Fuse) => None,
316        (Source::B, FaceClass::Inside, BooleanOp::Cut) => Some(true), // flip
317        (Source::B, FaceClass::Inside, BooleanOp::Intersect) => Some(false),
318        // Coplanar same -- keep only from A to avoid duplicates.
319        (Source::A, FaceClass::CoplanarSame, BooleanOp::Fuse | BooleanOp::Intersect) => Some(false),
320        (_, FaceClass::CoplanarSame, _) => None,
321        // Coplanar opposite -- for Cut, A's face facing opposite B should be kept
322        // (it forms the "skin" at the cut boundary). In all other cases, discard.
323        (Source::A, FaceClass::CoplanarOpposite, BooleanOp::Cut) => Some(false),
324        (_, FaceClass::CoplanarOpposite, _) => None,
325        // On boundary -- treat like CoplanarSame: keep from A only.
326        (Source::A, FaceClass::On, BooleanOp::Fuse | BooleanOp::Cut | BooleanOp::Intersect) => {
327            Some(false)
328        }
329        (_, FaceClass::On, _) => None,
330        // Unknown is only used by the algo crate's builder; never emitted by
331        // the operations pipeline classifier.
332        (_, FaceClass::Unknown, _) => {
333            debug_assert!(
334                false,
335                "FaceClass::Unknown must never reach fragment selection"
336            );
337            None
338        }
339    }
340}