axiolid_brep_boolean/lib.rs
1#![forbid(unsafe_code)]
2
3//! General exact booleans between exact B-reps with analytic faces.
4//!
5//! The general-fuse pipeline of ADR 0075, built in stages:
6//!
7//! - [`section_edges`]: where two operands' faces cross, as exact curves
8//! trimmed exactly to both faces;
9//! - [`split_face`]: one face cut along its section edges into regions, in
10//! the face's own parameters, with exact pcurves;
11//! - [`boolean`]: the regions classified against the other solid, selected
12//! by operator and sewn into the result.
13//!
14//! Operands may touch as well as cross: faces sharing a patch of one
15//! surface, sections running along existing edges, tangent contact, and
16//! solids meeting along an edge or at a point are all handled, not
17//! refused. A traced section along an operand's line or conic edge takes
18//! that edge's curve, so a revolution's cap coplanar with a face sews
19//! (#382).
20//!
21//! Nothing is meshed or fitted. A configuration a step cannot build is
22//! refused by name ([`BooleanError`]).
23//!
24//! # What a result guarantees
25//!
26//! Every surface of the result is an operand's surface, unchanged. Every
27//! new edge lies on an exact section curve (a closed form or a certified
28//! trace); vertices are those curves evaluated in `f64`.
29//!
30//! **Exact decisions.** Which section two supports have (closed forms,
31//! ruled, torus and traced sections), where a section crosses the surface
32//! next to a face's edge (`exact_curve_surface_intersection`), whether a
33//! point lies in a face (certified membership; too close to decide is
34//! refused, not guessed) and whether it lies in a solid (ray parity over
35//! exact ray/surface intersections).
36//!
37//! **Decisions within tolerance.** Operands built or placed separately meet
38//! faces that agree only up to rounding (#228). These decisions use the
39//! caller's [`Tolerance`] and nothing else: `eps = tolerance.linear()` for
40//! distances, `alpha = tolerance.angular()` for directions.
41//!
42//! - two supports are one surface when their normals or axes agree within
43//! `alpha` and their offsets within `eps`;
44//! - two supports touch rather than cross where their normals agree within
45//! `alpha`;
46//! - a point lies on an edge, or on a pole, within `eps`;
47//! - a section runs along an edge when every point of the edge is proved
48//! within `eps` of it (two lines, or two circles, in closed form);
49//! - a section that touches the surface next to an edge, whose double root
50//! rounding split or lost, is cut where it meets the edge itself, within
51//! `eps`;
52//! - cuts on a section (traced sections too, #381) within `eps` of each
53//! other are one cut, a boundary split within `eps` of the boundary
54//! piece's end is that end, a cut on an edge imprinted from a coincident
55//! face within `eps` of that edge's own end is that end (#276), and two
56//! evaluations of a vertex within `eps` are one vertex;
57//! - a section is an iso-parameter curve of a face within `eps`;
58//! - a plane is parallel or perpendicular to a cylinder's axis, or touches
59//! the cylinder, when that moves the plane by at most `eps` over where
60//! the section can matter: the largest common box of any pair of their
61//! faces, one reading per pair of supports.
62//!
63//! **One contact, every face pair (#243).** A plane read as touching a
64//! cylinder (a round hole tangent to a face, placed separately, or a
65//! fraction of the tolerance into or short of it) stands for the plane
66//! moved onto the cylinder, touching it along one ruling. Every face pair
67//! that crosses either surface near it agrees: a curve on the cylinder
68//! (a cap's circle, a face's circle across the axis) and a line in the
69//! plane (a cap's chord, the plane's own edge) are cut where they meet
70//! that contact ruling, not at their exact roots `2 sqrt(2 r d)` apart; and
71//! no point where the given and the moved plane disagree on a side decides
72//! a piece or a region. A curve whose meeting with the ruling cannot be
73//! placed is refused ([`BooleanError::UnsupportedContact`]) at that
74//! tolerance (see the finer retry below). An exactly
75//! tangent pair is decided by the exact predicates instead.
76//!
77//! **Oblique round walls (#287).** An exact extrusion along a leaning
78//! direction sweeps each arc of its profile into a tilted
79//! [`EllipticalCylinder`](axiolid_surface::EllipticalCylinder) whose axis
80//! is the direction. Such a face is evaluated, inverted (the angle of the
81//! section coordinates over the semi-axes) and classified like any other.
82//! A plane meets it in an ellipse, or a circle where it is exactly one, and
83//! a plane along its axis in rulings; a cylinder or another such wall on an
84//! exactly parallel axis meets it in rulings: all closed forms, cased by
85//! exact predicates, with closed-form pcurves. Other pairs take the ruled
86//! and traced sections as before. The within-tolerance plane/cylinder
87//! readings above are for right cylinders only: whether a plane is
88//! parallel to such a wall's axis or touches it is always decided exactly.
89//!
90//! **A hole touching a fillet (#249).** A round hole touching a flange
91//! with root fillets touches each fillet too, and the perpendicular
92//! cylinders meet in two loops crossing at the fillet/flange edge. The
93//! trace is taken over the fillet's whole turn when its face's box cuts
94//! through the crossing, a section is cut at a smooth edge where the edge
95//! crosses the other surface, and under a contact reading the crossing is
96//! placed on the contact; where the section misses the contact point by
97//! more than the tolerance the reading is refused
98//! ([`BooleanError::UnsupportedContact`]) and the boolean is cut again at
99//! the finer tolerance below, where a hole a fraction of the tolerance off
100//! the flange is no contact but a gap, decided exactly. See `tangency`.
101//!
102//! **Exact first (#236).** A reading about the operands' own surfaces --
103//! two supports are one, a plane is parallel or perpendicular to a
104//! cylinder's axis or touches it -- is first put to an exact predicate on
105//! the operands' `f64` numbers (dyadic arithmetic, `axiolid-exact`). Faces
106//! that are exactly coplanar, parallel or perpendicular, as operands placed
107//! by matrices with entries `0` and `+-1` meet, are decided exactly and use
108//! no tolerance. A reading the exact predicate rejects is not exact even
109//! when its `f64` measure is exactly zero (one general rotation turns a
110//! hole's axis and a wall's normal into the same `f64` vector, though the
111//! numbers given are not perpendicular), so a tolerance with a zero part
112//! never takes it (#251): at [`Tolerance::ZERO`] the exact answer stands
113//! and the general closed form decides. A reading about constructed points
114//! (a cut, a vertex, a point on an edge) compares `f64` evaluations of exact curves, so a
115//! residue up to `2^-40` of the operands' extent (about four thousand units
116//! in the last place) is the rounding of one exact point, read without the
117//! tolerance; only a larger residue is a decision within tolerance. The
118//! widening that covers such unrecorded residues is
119//! [`BooleanReport::rounding_floor`] (#244): [`ROUNDING_FACTOR`] times
120//! [`BooleanReport::extent`], which defines the extent exactly, plus the
121//! move of an angular residue of that factor (radians) over the same
122//! extent. It is not the threshold for a decision.
123//!
124//! **Guarantee.** [`boolean_with_report`] returns a [`BooleanReport`] of the
125//! within-tolerance decisions that fired, by kind, with the furthest each
126//! moved or turned the operands. When it is empty
127//! ([`BooleanReport::is_exact`]), the result is the exact boolean of the
128//! operands as given: every decision was exact, and new vertices are exact
129//! points evaluated in `f64`. Constructed points closer than
130//! [`ROUNDING_FACTOR`] times [`BooleanReport::extent`] count as one point
131//! and are not reported, and [`BooleanReport::rounding_floor`] covers them, so a consumer widening distances on any result, exact or not,
132//! widens them by that floor too. At [`Tolerance::ZERO`] nothing can be
133//! read within tolerance, so every result is exact (and still has its
134//! floor); operands that only miss each other by rounding are then refused,
135//! not guessed. The report's session enforces it (#251): a decision
136//! recorded at [`Tolerance::ZERO`], or beyond the caller's tolerance, is a
137//! debug assertion, and otherwise refused
138//! ([`BooleanError::ToleranceExceeded`]) rather than returned with a
139//! report that misdescribes it. When the report is not
140//! empty, the result is the exact boolean of operands whose faces were
141//! moved by at most `eps` (and, for a coincidence or contact of
142//! directions, turned by at most `alpha`), with every surface and curve
143//! exact for those; the report bounds how far. A reading that no single
144//! such perturbation explains is refused instead: cuts chained within `eps`
145//! of each other over more than `eps` are [`BooleanError::NearCoincidence`].
146//! Features further apart than the tolerance always go through the exact
147//! predicates: a gap of ten tolerances stays a gap.
148//!
149//! **Finer where the readings fail (#332).** Two features of the operands
150//! closer than `eps` but further apart than rounding are each read within
151//! the tolerance, and the readings need not agree: the sides of two
152//! abutting openings in a curved wall, one reaching a few micrometres (or
153//! just under a millimetre) past the other, their planes turned by more
154//! than `alpha`, leave cuts merged at a point on neither face's boundary,
155//! and the faces do not sew. A boolean refused at the caller's tolerance
156//! is cut again at [`FINER_FACTOR`] (`2^-10`) times it, where such
157//! features are apart and decided by the exact predicates, and the
158//! readings still cover the rounding of separately placed operands. The
159//! result is then the exact boolean of operands moved by at most the finer
160//! tolerance, and [`BooleanReport::finer`] says so; a boolean refused at
161//! both returns the refusal at the caller's tolerance. A boolean that
162//! succeeds at the caller's tolerance is never cut again.
163//!
164//! **A line edge short of the face it ends on (#409).** A door stopping
165//! `3e-7` short of a wall's face has its end face read as the wall's, but
166//! the edge of its floor along that face ends that short of the wall
167//! floor's edge, further than the fixed slack with which a split face
168//! welds piece ends, so the wall's floor does not split round the door's
169//! footprint. A boolean still refused at the finer tolerance is cut a last
170//! time at the caller's, with an imprinted line edge extended to the
171//! crossing with the other face's boundary where that lies within `eps` of
172//! the edge's end (`imprint_cuts`), mirroring the door past the face,
173//! whose edge the crossing falls inside. The move is a merge of points
174//! above the rounding floor, so the report is not exact; a boolean the
175//! finer tolerance cuts is never cut so.
176//!
177//! **A point near a support it is not read on (#332, #327).** A region is
178//! classified at a point inside it by ray parity, and a point within `eps`
179//! of any face's support is first not trusted: a reading may have moved
180//! that face onto the point's own. Where no point of a region is decided
181//! so, its points are classified again reading the ray's start exactly: a
182//! point is on a support only within the rounding floor, but for the faces
183//! read as its own support. A box whose face is tangent to a curved wall's
184//! inner cylinder (its face's points micrometres to millimetres from the
185//! cylinder, off the contact ruling) is then decided at the caller's
186//! tolerance, for the operands as given.
187//!
188//! **Operands far from the origin (#418, #419).** A coordinate a million
189//! metres out has a nanometre ulp, and the certified decisions work at the
190//! scale of the operands: the same union that sews at the origin was
191//! refused ("the kept faces do not sew", "a point too close to a face
192//! boundary to classify") at `(6e5, 5.6e6, 200)`. Operands whose common
193//! vertex box lies at least four diagonals from the origin are moved by
194//! `-c` first, `c` its centre rounded to the ulp of the largest
195//! coordinate, and the result is moved back by `+c`. The move is taken only
196//! where [`ExactBRep::translated_exactly`] proves that every vertex, frame
197//! origin, curve origin and control point moves with no rounding: the
198//! recentred operands are then the same solids, shifted, and a vertex of an
199//! operand that survives returns to its own coordinates. A new vertex is
200//! rounded once, to the ulp of its far coordinate. The report refers to the
201//! operands as given, whose extent scales the rounding floor. An axis whose
202//! centre lies within four diagonals of zero is not moved. Operands that
203//! do not move exactly (a curve or control point whose sum with `c`
204//! rounds, an elevated or banked curve), and operands whose recentred cut
205//! is refused, are cut where they are, as before: planar faces charted in
206//! world coordinates (#227) keep parameters as far out as they were, and
207//! the classifier cannot decide them there. Operands nearer the origin on
208//! every axis are cut bit for bit as without it.
209//!
210//! **Checked before it is returned (#445).** Every vertex of a result lies
211//! on the curve of each edge it bounds, and on each edge use's pcurve lifted
212//! through its face's surface at the use's ends, within the caller's linear
213//! tolerance and, for an exact report, within the rounding floor too (at
214//! [`Tolerance::ZERO`] the floor alone). A result over that bound is refused
215//! ([`BooleanError::VertexOffCurve`]) rather than returned with a report that
216//! misdescribes it; the check runs inside the cut, so the retries above
217//! apply to its refusal. Far from the origin the floor can exceed the
218//! caller's tolerance (a tolerance within a few ulps of the coordinates is
219//! refused this way); the check caps what reaches a vertex, it does not
220//! tighten the floor.
221//!
222//! Bookkeeping in a face's parameters (ordering pieces leaving a vertex,
223//! naming a point's parameters on the surface it was evaluated from) uses
224//! fixed relative slacks. Relative to a parameter they are far below any
225//! tolerance only near the origin: in world-charted parameters such as a
226//! line's `-y = 5.6e6` they span 5.6e-6, more than a tolerance. So a
227//! window's end never replaces a feature cut within the slack of it (#445),
228//! and the check above refuses what a slack leaves. Two piece ends weld into
229//! one vertex of a split face only where they also lie within the
230//! tolerance on the surface (#332), so a slack spanning more of a long arc
231//! than a fine tolerance never joins what a split kept apart. It never
232//! decides geometry: an order it cannot settle is refused
233//! ([`BooleanError::TangentSplit`], [`BooleanError::UnclosedSplit`]).
234
235mod assemble;
236mod bounds;
237mod classify;
238mod consistency;
239mod contact;
240mod predicate;
241mod recentre;
242mod report;
243mod seams;
244mod section;
245mod split;
246mod support;
247mod tangency;
248
249pub use report::{
250 BooleanReport, ToleranceDecision, ToleranceDecisionKind, FINER_FACTOR, ROUNDING_FACTOR,
251};
252pub use section::{section_edges, SectionEdge};
253pub use split::{split_face, Piece, PieceSource, Region};
254
255use axiolid_brep::ExactBRep;
256use axiolid_core::{BooleanOperator, Tolerance};
257use axiolid_evaluate::surface::{locate, normal};
258use axiolid_surface::Surface;
259use axiolid_topology::Orientation;
260
261/// The exact boolean of two exact B-rep solids.
262///
263/// ADR 0075 stages 1 and 2: faces on planes, cylinders, elliptical
264/// cylinders, cones, spheres and tori, meeting in any section
265/// `exact_surface_intersection` builds (lines, conics, ruled, torus and
266/// traced sections).
267///
268/// # Errors
269///
270/// Anything a step refuses (see [`BooleanError`]), or a result with nothing
271/// left.
272pub fn boolean(
273 a: &ExactBRep,
274 b: &ExactBRep,
275 operator: BooleanOperator,
276 tolerance: Tolerance,
277) -> Result<ExactBRep, BooleanError> {
278 boolean_with_report(a, b, operator, tolerance).map(|(result, _)| result)
279}
280
281/// [`boolean`], with the within-tolerance decisions it took (#236).
282///
283/// An exact report ([`BooleanReport::is_exact`]) means no decision used the
284/// tolerance: the result is the exact boolean of `a` and `b` as given (see
285/// "What a result guarantees"). Otherwise the report names each kind of
286/// reading that fired and the furthest it moved or turned the operands,
287/// within `tolerance`. At [`Tolerance::ZERO`] nothing can be read within
288/// tolerance, so a result is always exact: operands placed with exact axis
289/// matrices (entries `0` and `+-1`) meet in exactly coincident, parallel and
290/// perpendicular faces, which the exact predicates decide.
291///
292/// A boolean refused at `tolerance` is cut again at [`FINER_FACTOR`] times
293/// it (#332): features closer than `tolerance` but further apart than the
294/// finer one are then decided exactly. The report says so
295/// ([`BooleanReport::finer`]), and its readings lie within the finer
296/// tolerance. Where that is refused too, the refusal at `tolerance` is
297/// returned.
298///
299/// # Errors
300///
301/// As [`boolean`].
302pub fn boolean_with_report(
303 a: &ExactBRep,
304 b: &ExactBRep,
305 operator: BooleanOperator,
306 tolerance: Tolerance,
307) -> Result<(ExactBRep, BooleanReport), BooleanError> {
308 // The session reads the operands as given: its readings and rounding
309 // floor refer to them, recentred or not.
310 let session = report::open(&[a, b], tolerance);
311 // Only an owned session can be started over for the fallback below.
312 let near = session.owns().then(|| recentre::operands(a, b)).flatten();
313 if let Some((near_a, near_b, centre)) = near {
314 let moved = solve(&session, &near_a, &near_b, operator, tolerance)
315 .and_then(|near| recentre::restore(&near, centre))
316 .and_then(|result| consistency::check(&result, tolerance).map(|()| result))
317 .ok()
318 .filter(|_| !session.exceeded());
319 if let Some(result) = moved {
320 return Ok((result, session.finish()?));
321 }
322 // Never worse than the operands as given: planar faces charted in
323 // world coordinates (#227) keep parameters as far out as before the
324 // move, so a refusal here may be the move's. Cut them where they are,
325 // in a session as it was opened.
326 session.restart(tolerance);
327 }
328 solve(&session, a, b, operator, tolerance).and_then(|result| Ok((result, session.finish()?)))
329}
330
331/// The attempts of [`boolean_with_report`], each in the open session.
332fn solve(
333 session: &report::Guard,
334 a: &ExactBRep,
335 b: &ExactBRep,
336 operator: BooleanOperator,
337 tolerance: Tolerance,
338) -> Result<ExactBRep, BooleanError> {
339 let refused = match attempt(session, a, b, operator, tolerance) {
340 Ok(result) => return Ok(result),
341 Err(refused) => refused,
342 };
343 // Finer where the tolerance's readings fail (#332): see
344 // [`FINER_FACTOR`]. Nothing is retried where nothing is left, at a
345 // tolerance with a zero part, or at or below the rounding floor.
346 let finer = Tolerance::new(tolerance.linear() * FINER_FACTOR, tolerance.angular());
347 if let Ok(finer) = finer {
348 if refused != BooleanError::EmptyResult
349 && finer.linear() > report::rounding()
350 && finer.angular() > 0.0
351 && session.retry_at(finer)
352 {
353 if let Ok(result) = attempt(session, a, b, operator, finer) {
354 return Ok(result);
355 }
356 }
357 }
358 // Last, a line edge a fraction of the tolerance short of the face it
359 // ends on is extended to it (#409): at the caller's tolerance, so the
360 // exact finer cut above is preferred where it succeeds.
361 if refused != BooleanError::EmptyResult
362 && tolerance.linear() > report::rounding()
363 && session.retry_extending(tolerance)
364 {
365 // Not `attempt`: its fallback to ends merged only within the
366 // rounding floor is the cut the first attempt already made.
367 return run(a, b, operator, tolerance).map_err(|_| refused);
368 }
369 Err(refused)
370}
371
372/// [`run`] in the open session, cut again with imprinted edges' ends
373/// merged only within the rounding floor where the wider merge left it
374/// refused (#291: never worse for the end merge).
375fn attempt(
376 session: &report::Guard,
377 a: &ExactBRep,
378 b: &ExactBRep,
379 operator: BooleanOperator,
380 tolerance: Tolerance,
381) -> Result<ExactBRep, BooleanError> {
382 match run(a, b, operator, tolerance) {
383 Ok(result) => Ok(result),
384 Err(_) if session.merged_an_end() => {
385 session.retry_with_rounding_ends();
386 run(a, b, operator, tolerance)
387 }
388 Err(refused) => Err(refused),
389 }
390}
391
392fn run(
393 a: &ExactBRep,
394 b: &ExactBRep,
395 operator: BooleanOperator,
396 tolerance: Tolerance,
397) -> Result<ExactBRep, BooleanError> {
398 // Faces that wind round their surface without a seam get one first.
399 let seamed_a = seams::with_seams(a, tolerance)?;
400 let seamed_b = seams::with_seams(b, tolerance)?;
401 let a = seamed_a.as_ref().unwrap_or(a);
402 let b = seamed_b.as_ref().unwrap_or(b);
403 let (edges, contacts) = section::sections_and_contacts(a, b, tolerance)?;
404 let solid_a = classify::Solid::new(a, tolerance)?;
405 let solid_b = classify::Solid::new(b, tolerance)?;
406 let mut kept = Vec::new();
407 let cuts: Vec<axiolid_core::Point3> = edges.iter().flat_map(|e| [e.start, e.end]).collect();
408 for (operand, other, other_solid, first) in [(a, b, &solid_b, true), (b, a, &solid_a, false)] {
409 let topology = operand.topology();
410 for index in 0..topology.faces().len() {
411 let face = topology
412 .face_id_at(index)
413 .ok_or(BooleanError::DanglingReference)?;
414 let record = &topology.faces()[index];
415 let surface: Surface = record
416 .surface
417 .and_then(|id| operand.surfaces().get(id.index()))
418 .ok_or(BooleanError::DanglingReference)?
419 .clone();
420 let mine: Vec<SectionEdge> = edges
421 .iter()
422 .filter(|e| {
423 if first {
424 e.face_a == face
425 } else {
426 e.face_b == face
427 }
428 })
429 .cloned()
430 .collect();
431 // The other operand's faces on this face's surface: a region
432 // may lie on one of them rather than inside or outside.
433 let coincident: Vec<usize> = (0..other.topology().faces().len())
434 .filter(|&f| {
435 other.topology().faces()[f]
436 .surface
437 .and_then(|s| other.surfaces().get(s.index()))
438 .is_some_and(|s| support::same_support(&surface, s, tolerance))
439 })
440 .collect();
441 for region in split_face(operand, face, &mine, first, &cuts, tolerance)? {
442 let sign = match record.orientation {
443 Orientation::Forward if !region.against => 1.0,
444 Orientation::Reversed if region.against => 1.0,
445 _ => -1.0,
446 };
447 let (keep, flip) = decide(
448 ®ion,
449 &surface,
450 sign,
451 other_solid,
452 &coincident,
453 &contacts,
454 operator,
455 first,
456 tolerance,
457 )?;
458 if keep {
459 kept.push(assemble::Kept {
460 surface: surface.clone(),
461 orientation: record.orientation,
462 region,
463 flip,
464 });
465 }
466 }
467 }
468 }
469 let result = assemble::assemble(&kept, tolerance)?;
470 consistency::check(&result, tolerance)?;
471 Ok(result)
472}
473
474/// Whether a region is kept, and whether it bounds the result from its
475/// other side: classified at the first interior point that decides.
476///
477/// A point within the tolerance of a surface its own is read as touching
478/// (`contacts`) does not decide: there the given operands and the moved
479/// ones the reading stands for can put it on different sides (#243).
480///
481/// The points are first classified with their ray's start read within
482/// tolerance; only where none decides so are they classified again with
483/// it read exactly ([`classify::Start::Exact`], #332). A region lying all
484/// over within the tolerance of a support it is not read as one with (a
485/// side face of one opening a few micrometres inside its neighbour, their
486/// planes turned by more than the angular tolerance) is decided by the
487/// exact crossing of that support, for the operands as given.
488#[allow(clippy::too_many_arguments)]
489fn decide(
490 region: &Region,
491 surface: &Surface,
492 sign: f64,
493 other_solid: &classify::Solid<'_>,
494 coincident: &[usize],
495 contacts: &contact::Contacts,
496 operator: BooleanOperator,
497 first: bool,
498 tolerance: Tolerance,
499) -> Result<(bool, bool), BooleanError> {
500 let points = classify::interior_points(region, surface)?;
501 for start in [
502 classify::Start::Tolerance,
503 classify::Start::Exact(coincident),
504 ] {
505 for &point in &points {
506 if contacts.disputed(surface, point) {
507 continue;
508 }
509 match classify_point(
510 point,
511 surface,
512 sign,
513 other_solid,
514 coincident,
515 start,
516 operator,
517 first,
518 tolerance,
519 ) {
520 Err(BooleanError::Undecided) => {}
521 other => return other,
522 }
523 }
524 }
525 Err(BooleanError::Undecided)
526}
527
528#[allow(clippy::too_many_arguments)]
529fn classify_point(
530 point: axiolid_core::Point3,
531 surface: &Surface,
532 sign: f64,
533 other_solid: &classify::Solid<'_>,
534 coincident: &[usize],
535 start: classify::Start<'_>,
536 operator: BooleanOperator,
537 first: bool,
538 tolerance: Tolerance,
539) -> Result<(bool, bool), BooleanError> {
540 if let Some(theirs) = other_solid.on_face(point, coincident, tolerance)? {
541 // On the other solid's boundary: kept once, from the first
542 // operand, where the operator leaves a boundary.
543 let (u, v) = locate(surface, point, report::floored(tolerance))
544 .map_err(|_| BooleanError::Evaluation)?;
545 let ours = normal(surface, u, v).map_err(|_| BooleanError::Evaluation)? * sign;
546 let same = ours.dot(theirs) > 0.0;
547 let keep = first
548 && match operator {
549 BooleanOperator::Union | BooleanOperator::Intersection => same,
550 BooleanOperator::Difference => !same,
551 _ => return Err(BooleanError::UnsupportedSection),
552 };
553 return Ok((keep, false));
554 }
555 let inside = other_solid.contains(point, start, tolerance)?;
556 Ok(match (operator, first) {
557 (BooleanOperator::Union, _) => (!inside, false),
558 (BooleanOperator::Intersection, _) => (inside, false),
559 (BooleanOperator::Difference, true) => (!inside, false),
560 (BooleanOperator::Difference, false) => (inside, true),
561 _ => return Err(BooleanError::UnsupportedSection),
562 })
563}
564
565use axiolid_measure::ExactMeasureError;
566use core::fmt;
567
568/// Why a boolean step could not be carried out exactly.
569#[non_exhaustive]
570#[derive(Debug, Clone, PartialEq, Eq)]
571pub enum BooleanError {
572 /// Two faces' supports intersect in a curve this stage does not take
573 /// (anything but a line, circle or ellipse).
574 UnsupportedSection,
575 /// A face is trimmed by an edge or pcurve family this stage cannot
576 /// intersect or classify against.
577 UnsupportedTrim,
578 /// A point was too close to a face boundary to classify.
579 Undecided,
580 /// A curve or surface could not be evaluated or inverted.
581 Evaluation,
582 /// A handle referenced missing geometry.
583 DanglingReference,
584 /// A face domain could not be built.
585 Measure(ExactMeasureError),
586 /// A face whose surface, or a section on it, has no exact pcurve in
587 /// this stage.
588 UnsupportedSplit,
589 /// The pieces of a split face do not close into loops.
590 UnclosedSplit,
591 /// Two pieces leave a vertex of a split face in the same direction and
592 /// bend alike there, so no order between them can be read off.
593 TangentSplit,
594 /// The operation leaves nothing: an intersection of solids that only
595 /// touch, or a difference that removes everything. An exact B-rep
596 /// cannot be empty, so this is the answer, not a refusal.
597 EmptyResult,
598 /// A cavity of the result lies inside none of its solids.
599 AmbiguousCavity,
600 /// The kept faces did not sew into a valid exact B-rep.
601 Assembly,
602 /// Features lie within tolerance of each other in a chain longer than
603 /// the tolerance, so no single perturbation within tolerance makes them
604 /// one: reading them as one would move a feature further than the
605 /// caller allowed (#228).
606 NearCoincidence,
607 /// A plane read as touching a cylinder within tolerance is crossed by a
608 /// curve whose meeting with their contact ruling this stage cannot
609 /// place (a curve of the plane that is no line, a curve of the
610 /// cylinder that is no ruling or conic), so the reading could not be
611 /// made the same for every face pair (#243).
612 UnsupportedContact,
613 /// A decision this stage reads within tolerance was asked for beyond
614 /// the caller's tolerance: any such reading at [`Tolerance::ZERO`],
615 /// where the result must be exact (#251). The within-tolerance
616 /// readings never ask for one; this refusal stands in for a result
617 /// whose report would misdescribe it.
618 ToleranceExceeded,
619 /// A vertex of the result lies off the curve of an edge it bounds, or
620 /// off an edge use's pcurve lifted at its end, further than the report
621 /// allows: the caller's linear tolerance, and for an exact report the
622 /// rounding floor too (#445). Refused rather than returned with a
623 /// report that misdescribes it.
624 VertexOffCurve,
625}
626
627impl fmt::Display for BooleanError {
628 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629 match self {
630 Self::UnsupportedSection => {
631 f.write_str("a face pair meets in a curve this stage does not build")
632 }
633 Self::UnsupportedTrim => {
634 f.write_str("a face is trimmed by a curve family this stage cannot intersect")
635 }
636 Self::Undecided => f.write_str("a point lies too close to a face boundary to classify"),
637 Self::Evaluation => f.write_str("a curve or surface could not be evaluated"),
638 Self::DanglingReference => f.write_str("a handle references missing geometry"),
639 Self::Measure(error) => write!(f, "a face domain could not be built: {error}"),
640 Self::UnsupportedSplit => {
641 f.write_str("a face or section has no exact pcurve in this stage")
642 }
643 Self::UnclosedSplit => f.write_str("the pieces of a split face do not close"),
644 Self::TangentSplit => {
645 f.write_str("two pieces leave a vertex of a split face in one direction")
646 }
647 Self::EmptyResult => f.write_str("the operation leaves nothing"),
648 Self::AmbiguousCavity => {
649 f.write_str("a cavity of the result lies inside none of its solids")
650 }
651 Self::Assembly => f.write_str("the kept faces did not sew into a valid exact B-rep"),
652 Self::NearCoincidence => f.write_str(
653 "features lie within tolerance of each other in a chain longer than the tolerance",
654 ),
655 Self::UnsupportedContact => f.write_str(
656 "a curve crosses a plane read as touching a cylinder where the contact cannot be placed",
657 ),
658 Self::ToleranceExceeded => {
659 f.write_str("a decision within tolerance was asked for beyond the caller's tolerance")
660 }
661 Self::VertexOffCurve => f.write_str(
662 "a vertex of the result lies off its edge's curve beyond what its report allows",
663 ),
664 }
665 }
666}
667
668impl std::error::Error for BooleanError {}