axiolid_measure/exact_bodies.rs
1//! Certified boundary distance and Hausdorff distance between bodies made
2//! of several exact solids (#229).
3//!
4//! A body here is a slice of [`ExactBRep`] items, one per solid: how an IFC
5//! body with several items arrives. A slice, not one B-rep holding several
6//! solids, because the items are produced and placed one by one, a
7//! witness has to say which item it lies on, and the layout check below
8//! works item against item. (One `ExactBRep` with several solids is
9//! measured by the single-B-rep queries as the union of all its faces,
10//! with none of the checks made here.)
11//!
12//! # Distance
13//!
14//! [`body_boundary_distance`] is the least distance between a point on the
15//! boundary of an item of `a` and a point on the boundary of an item of
16//! `b`: the minimum over item pairs, found by ONE branch and bound over the
17//! elements of every item (as [`crate::boundary_distance`] for one pair),
18//! so a pair of items far apart gets a large lower bound at the root and is
19//! never refined. The witnesses name the items they lie on.
20//!
21//! Items may touch or overlap. The minimum over pairs is then still the
22//! distance between the boundaries of the two unions, except where part of
23//! one body lies wholly inside the other: a boundary point of an item that
24//! is not on the union's boundary is interior to the union, and from a
25//! point outside the union the segment to it crosses the union's boundary
26//! at a point no farther away. As for one pair, containment is a separate
27//! classification.
28//!
29//! # In plan
30//!
31//! [`body_plan_boundary_distance`], [`body_plan_boundary_clearance`] and
32//! [`body_plan_overlap`] measure between the bodies' shadows on the XY
33//! plane by the same shared search (#237, `plan.rs`). Items of one body may
34//! overlap freely there, so nothing about their layout is checked.
35//!
36//! # Hausdorff
37//!
38//! [`body_boundary_hausdorff_distance`] and
39//! [`one_sided_body_boundary_hausdorff`] measure the boundary of the UNION
40//! of each body's items. Every body is first checked, item pair by item
41//! pair, for a layout under which that boundary can be formed exactly:
42//!
43//! - **Apart**: shown by a plane with a certified positive gap between
44//! them -- a direction along which the exact projection ranges of every
45//! face patch and edge span of the two are disjoint, tried along the axes
46//! and every planar face's normal -- or, failing that, by a positive
47//! certified lower bound on the distance between their boundaries
48//! together with a boundary point of each outside an enclosing box of the
49//! other. The second test needs each item to be one solid (one outer
50//! shell, voids allowed): two such solids whose boundaries do not meet
51//! are disjoint unless the outer shell of one lies inside the other, and
52//! then its box lies in the other's box. However small the gap -- an
53//! exporter's 0.3 mm between parts -- both facing faces are on the
54//! union's boundary and are measured as such: a gap is never glued.
55//! - **Touching without a shared patch**: a plane separates them to within
56//! rounding (`1e-9` relative to their size), and one of the two has no
57//! face that can hold a patch of that plane -- no planar face lying in it
58//! and no B-spline face reaching it (no other family holds an open patch
59//! of a plane). Interiors are then disjoint, and a boundary point of one
60//! that is not on the union's boundary would have a ball round it covered
61//! by the two, cut by the plane into halves, one in each: a disc of the
62//! plane on both boundaries, which one of them cannot hold. So contact
63//! along edges or at points leaves the union boundary the union of the
64//! boundaries. Planes are tried along the axes, every planar face's
65//! normal, and the sum of any two touching ones (two blocks meeting at an
66//! edge are separated by the diagonal plane, where neither has a face).
67//! - **Exact face contact**: they touch on a plane where both have faces,
68//! every such face planar and lying, in the B-rep's own numbers, on one
69//! plane normal to a coordinate axis (its plane, vertices, lines and
70//! circles all at the very same coordinate along it; a placement turning
71//! about that axis keeps this), and bounded by lines and circles. Each
72//! face in contact is then cut down to its free region, what is left of
73//! it once the faces of other items lying against it are cut away, by an
74//! exact arrangement of their boundaries on the plane (`contact`).
75//!
76//! Why cutting is right: interiors are disjoint, so a point `p` of an
77//! item's face `F` is off the union's boundary only if a ball round it is
78//! covered by items. By the argument above, the half of the ball across
79//! `F`'s plane is covered by other items whose faces then hold a disc of
80//! that plane round `p` -- faces lying against `F`. So the union's boundary
81//! is the closure of the free regions of all faces: those in no contact as
82//! they are, and those in contact cut. Its edges are the edges of those
83//! regions; an edge between two faces both cut away (the foot of a wall
84//! shared by two blocks on a footing) is not on it and is dropped.
85//!
86//! Anything else is refused by name, never glued or snapped:
87//! [`BodyMeasureError::ContactPlaneNotAxisNormal`] when two items touch, to
88//! within rounding, on a plane no coordinate axis is normal to (a tilted
89//! assembly, walls turned in plan: whether they meet, clear or overlap is
90//! below rounding there); [`BodyMeasureError::ItemsNearlyShareFace`] with
91//! the gap when they interpenetrate by no more than the caller's tolerance,
92//! or their faces are on axis planes at coordinates that differ below
93//! rounding; [`BodyMeasureError::ItemsShareFace`]
94//! when such faces cannot be cut (a B-spline face, an elliptical edge);
95//! and [`BodyMeasureError::ItemsOverlap`] when they were not shown apart or
96//! touching: boundaries that cross or come closer than the search resolves
97//! with no plane between them, one item possibly inside another, or curved
98//! items touching (no planar face to separate them by).
99//!
100//! Gluing near contact and widening the result by the gap is not sound:
101//! lifted 0.3 mm off its footing, a column's base disc and the disc of the
102//! footing's top under it are both on the union's boundary, and their
103//! centres are a whole radius from the boundary of the body glued shut. So
104//! a gap is measured as a gap, and an interpenetration is refused.
105//!
106//! The one-sided distance is then measured by the search of
107//! [`crate::one_sided_boundary_hausdorff`] on the union's boundary of each
108//! body. Faces are matched across every pair of items, so a body moved by
109//! a translation closes as fast as one solid does: every face patch is
110//! held at `|t|` by its own translate, and the support point of each item
111//! against `t` seeds the lower bound. A free region is bounded through the
112//! face it was cut from, so an exact cut that falls out differently in the
113//! last bit for a translated copy (a sliver along a wall turned in plan)
114//! still closes at once (`exact_hausdorff/cut.rs`).
115//!
116//! # Placed bodies
117//!
118//! Every query takes each side as a [`PlacedBody`]: the items in the
119//! body's own frame and one rigid placement. An IFC body's extruded items
120//! stand on axis-normal planes in the body's frame however the body is
121//! placed in the world, so contact is found and cut there, and the
122//! placement then moves the cut boundary (and the items it was cut from)
123//! before anything is measured: witnesses are in the world. A placement
124//! cannot change what is measured, a rigid motion carrying the union's
125//! boundary onto the placed union's. Items turned against each other in
126//! the body's own frame still touch on planes no axis is normal to and are
127//! refused as [`BodyMeasureError::ContactPlaneNotAxisNormal`]; a slice of
128//! items converts to a placed body with the identity placement.
129
130use std::borrow::Cow;
131use std::collections::HashMap;
132
133use core::fmt;
134
135use axiolid_brep::{ExactBRep, ExactBRepBuilder, TransformError};
136use axiolid_core::{Point3, Scalar, Tolerance, Transform3, Vec3};
137use axiolid_evaluate::evaluate3;
138use axiolid_surface::Surface;
139
140use crate::exact::ExactMeasureError;
141use crate::exact_distance::{
142 closed, search, search_within, surface_of, Clearance, DistanceBounds, Metric, Shape, Side,
143 OVERLAP_STEPS,
144};
145use crate::exact_hausdorff::{witnessed, Cut, MAX_SPLITS};
146use crate::mesh_hausdorff::HausdorffBounds;
147
148mod contact;
149mod plan;
150
151use contact::{Assembler, AxisPlane, Uncut};
152pub use plan::{
153 body_plan_boundary_clearance, body_plan_boundary_distance, body_plan_overlap, BodyPlanOverlap,
154};
155
156/// Which argument of a body query a refusal is about.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum BodySide {
159 /// The first slice: `a`, or `from` of a one-sided query.
160 First,
161 /// The second slice: `b`, or `to` of a one-sided query.
162 Second,
163}
164
165/// A body: its items in the body's own frame, and one rigid placement
166/// that puts them in the world (#229).
167///
168/// Every `body_*` query takes one per side, and a slice of items (or a
169/// vector or array of them) converts into one with the identity placement.
170/// Face contact is found and cut in the body's own frame, where an IFC
171/// body's extruded items usually stand on axis-normal planes, and the
172/// placement then moves the cut boundary, so a body turned in any
173/// direction still has its contact cut. Witnesses are in the world.
174///
175/// Items turned against each other within the body's own frame still touch
176/// on planes no axis is normal to, and are refused by
177/// [`BodyMeasureError::ContactPlaneNotAxisNormal`].
178#[derive(Debug, Clone, Copy, PartialEq)]
179pub struct PlacedBody<'a> {
180 /// The items, in the body's own frame.
181 pub items: &'a [ExactBRep],
182 /// The rigid placement of the body's frame in the world.
183 pub placement: Transform3,
184}
185
186impl<'a> PlacedBody<'a> {
187 /// `items` placed by `placement`.
188 #[must_use]
189 pub fn new(items: &'a [ExactBRep], placement: Transform3) -> Self {
190 Self { items, placement }
191 }
192}
193
194impl<'a> From<&'a [ExactBRep]> for PlacedBody<'a> {
195 fn from(items: &'a [ExactBRep]) -> Self {
196 Self::new(items, Transform3::IDENTITY)
197 }
198}
199
200impl<'a> From<&'a Vec<ExactBRep>> for PlacedBody<'a> {
201 fn from(items: &'a Vec<ExactBRep>) -> Self {
202 Self::new(items, Transform3::IDENTITY)
203 }
204}
205
206impl<'a, const N: usize> From<&'a [ExactBRep; N]> for PlacedBody<'a> {
207 fn from(items: &'a [ExactBRep; N]) -> Self {
208 Self::new(items, Transform3::IDENTITY)
209 }
210}
211
212/// Why two bodies of exact solids could not be measured.
213#[non_exhaustive]
214#[derive(Debug, Clone, PartialEq)]
215pub enum BodyMeasureError {
216 /// The body has no items.
217 EmptyBody {
218 /// Which body.
219 body: BodySide,
220 },
221 /// An item could not be measured, as by the single-solid queries.
222 Measure(ExactMeasureError),
223 /// Two items of one body were not shown apart or touching (see the
224 /// module docs): their boundaries may cross, one may lie inside the
225 /// other, or they touch where no planar face separates them. The
226 /// boundary of their union is not formed here.
227 ItemsOverlap {
228 /// Which body.
229 body: BodySide,
230 /// The first item, by index in the slice.
231 first: usize,
232 /// The second item, by index in the slice.
233 second: usize,
234 },
235 /// Two items of one body touch where both have a face in the plane
236 /// that separates them, and the shared patch cannot be cut exactly: a
237 /// B-spline face reaches the plane, or a face in contact is bounded by
238 /// an edge that is neither a line nor a circle.
239 ItemsShareFace {
240 /// Which body.
241 body: BodySide,
242 /// The first item, by index in the slice.
243 first: usize,
244 /// The second item, by index in the slice.
245 second: usize,
246 },
247 /// Two items of one body are within the caller's tolerance of face
248 /// contact without being in exact contact: they interpenetrate by up to
249 /// `-gap`, or their faces are on planes normal to one axis but not at
250 /// the same coordinate in their own numbers (a gap or an overlap below
251 /// rounding). Gluing them would measure a boundary the items do not
252 /// have, so the pair is refused rather than snapped (a certified
253 /// positive gap is measured exactly instead: the two faces are on the
254 /// union's boundary).
255 ItemsNearlyShareFace {
256 /// Which body.
257 body: BodySide,
258 /// The first item, by index in the slice.
259 first: usize,
260 /// The second item, by index in the slice.
261 second: usize,
262 /// The certified least separation along the plane between them:
263 /// negative where they may interpenetrate, by at most its size.
264 gap: Scalar,
265 },
266 /// Two items of one body touch, to within rounding, where both have
267 /// faces on a plane that no coordinate axis is normal to: walls of
268 /// items turned against each other, or an assembly placed before it was
269 /// measured. Contact is cut only on axis-normal planes, where it can be
270 /// shown exact; pass the items in the body's own frame with the
271 /// placement in a [`PlacedBody`] instead, so the contact is cut before
272 /// the placement turns it.
273 ContactPlaneNotAxisNormal {
274 /// Which body.
275 body: BodySide,
276 /// The first item, by index in the slice.
277 first: usize,
278 /// The second item, by index in the slice.
279 second: usize,
280 },
281 /// The placement of a [`PlacedBody`] is not rigid, or its items cannot
282 /// be moved by it.
283 Placement {
284 /// Which body.
285 body: BodySide,
286 /// Why the items could not be placed.
287 error: TransformError,
288 },
289}
290
291impl fmt::Display for BodyMeasureError {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self {
294 Self::EmptyBody { body } => write!(f, "the {body:?} body has no items"),
295 Self::Measure(error) => write!(f, "an item could not be measured: {error}"),
296 Self::ItemsOverlap {
297 body,
298 first,
299 second,
300 } => write!(
301 f,
302 "items {first} and {second} of the {body:?} body were not shown apart \
303 or touching; the boundary of their union is not formed"
304 ),
305 Self::ItemsShareFace {
306 body,
307 first,
308 second,
309 } => write!(
310 f,
311 "items {first} and {second} of the {body:?} body may share a patch of \
312 face that cannot be cut exactly"
313 ),
314 Self::ItemsNearlyShareFace {
315 body,
316 first,
317 second,
318 gap,
319 } => write!(
320 f,
321 "items {first} and {second} of the {body:?} body are within the \
322 tolerance of face contact (gap {gap}) but not in exact contact"
323 ),
324 Self::ContactPlaneNotAxisNormal {
325 body,
326 first,
327 second,
328 } => write!(
329 f,
330 "items {first} and {second} of the {body:?} body touch on a plane no \
331 coordinate axis is normal to; pass them unplaced in a PlacedBody"
332 ),
333 Self::Placement { body, error } => {
334 write!(f, "the {body:?} body could not be placed: {error}")
335 }
336 }
337 }
338}
339
340impl std::error::Error for BodyMeasureError {
341 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
342 match self {
343 Self::Measure(error) => Some(error),
344 Self::Placement { error, .. } => Some(error),
345 _ => None,
346 }
347 }
348}
349
350impl From<ExactMeasureError> for BodyMeasureError {
351 fn from(error: ExactMeasureError) -> Self {
352 Self::Measure(error)
353 }
354}
355
356/// A certified distance between two bodies and the items its witnesses lie
357/// on.
358#[derive(Debug, Clone, PartialEq)]
359pub struct BodyDistance {
360 /// The interval and the witnesses, as for one pair.
361 pub bounds: DistanceBounds,
362 /// The item of the first body holding `bounds.point_a`.
363 pub item_a: usize,
364 /// The item of the second body holding `bounds.point_b`.
365 pub item_b: usize,
366}
367
368/// A certified one-sided Hausdorff distance between two bodies and the
369/// items its witnesses lie on.
370#[derive(Debug, Clone, Copy, PartialEq)]
371pub struct BodyHausdorffBounds {
372 /// The interval and the witnesses, as for one pair.
373 pub bounds: HausdorffBounds,
374 /// The item of the measured body holding `bounds.point_from`.
375 pub item_from: usize,
376 /// The item of the other body holding `bounds.point_to`.
377 pub item_to: usize,
378}
379
380/// The two-sided Hausdorff distance between the boundaries of two bodies
381/// and both one-sided ones.
382#[derive(Debug, Clone, Copy, PartialEq)]
383pub struct BodyHausdorff {
384 /// `max(h(A, B), h(B, A))`, witnessed by the side with the larger lower
385 /// bound.
386 pub distance: BodyHausdorffBounds,
387 /// `h(A, B)`, from the first body's items to the second's.
388 pub forward: BodyHausdorffBounds,
389 /// `h(B, A)`, from the second body's items to the first's.
390 pub backward: BodyHausdorffBounds,
391}
392
393/// Distance between the boundaries of two bodies of exact solids, to within
394/// `accuracy` (#229): the least over item pairs, by one shared search.
395///
396/// Items may touch or overlap (see the module docs). A negative or NaN
397/// accuracy is read as zero.
398///
399/// # Errors
400///
401/// [`BodyMeasureError::EmptyBody`] for an empty slice, and
402/// [`BodyMeasureError::Measure`] with what [`crate::boundary_distance`]
403/// refuses.
404pub fn body_boundary_distance<'a, 'b>(
405 a: impl Into<PlacedBody<'a>>,
406 b: impl Into<PlacedBody<'b>>,
407 accuracy: Scalar,
408 tolerance: Tolerance,
409) -> Result<BodyDistance, BodyMeasureError> {
410 let accuracy = accuracy.max(0.0);
411 distance(
412 a.into(),
413 b.into(),
414 tolerance,
415 Metric::Space,
416 &mut |lower, upper, floor| closed(lower, upper, accuracy, floor),
417 )
418}
419
420/// [`body_boundary_distance`] refined only until it clears `limit`.
421///
422/// # Errors
423///
424/// As [`body_boundary_distance`].
425pub fn body_boundary_clearance<'a, 'b>(
426 a: impl Into<PlacedBody<'a>>,
427 b: impl Into<PlacedBody<'b>>,
428 limit: Scalar,
429 tolerance: Tolerance,
430) -> Result<(BodyDistance, Clearance), BodyMeasureError> {
431 let found = distance(
432 a.into(),
433 b.into(),
434 tolerance,
435 Metric::Space,
436 &mut |lower, upper, floor| {
437 upper < limit || lower > limit || closed(lower, upper, 0.0, floor)
438 },
439 )?;
440 let clearance = found.bounds.against(limit);
441 Ok((found, clearance))
442}
443
444/// The least distance by `metric` over item pairs, by one search over the
445/// placed items merged into one B-rep per body.
446fn distance(
447 a: PlacedBody<'_>,
448 b: PlacedBody<'_>,
449 tolerance: Tolerance,
450 metric: Metric,
451 done: &mut dyn FnMut(Scalar, Scalar, Scalar) -> bool,
452) -> Result<BodyDistance, BodyMeasureError> {
453 let (a, b) = Items::pair(a, b)?;
454 let found = search(&a.brep, &b.brep, tolerance, metric, done)?;
455 Ok(BodyDistance {
456 item_a: a.item(found.on.0),
457 item_b: b.item(found.on.1),
458 bounds: found.bounds,
459 })
460}
461
462/// One-sided Hausdorff distance from the boundary of the union of `from`'s
463/// items to that of `to`'s, to within `accuracy` (#229).
464///
465/// # Errors
466///
467/// [`BodyMeasureError::EmptyBody`] for an empty slice;
468/// [`BodyMeasureError::ItemsOverlap`] or
469/// [`BodyMeasureError::ItemsShareFace`] for two items of one body not shown
470/// apart or touching without a shared patch (see the module docs); and
471/// [`BodyMeasureError::Measure`] with what
472/// [`crate::one_sided_boundary_hausdorff`] refuses.
473pub fn one_sided_body_boundary_hausdorff<'a, 'b>(
474 from: impl Into<PlacedBody<'a>>,
475 to: impl Into<PlacedBody<'b>>,
476 accuracy: Scalar,
477 tolerance: Tolerance,
478) -> Result<BodyHausdorffBounds, BodyMeasureError> {
479 one_sided_body_boundary_hausdorff_with_budget(from, to, accuracy, tolerance, MAX_SPLITS)
480}
481
482/// [`one_sided_body_boundary_hausdorff`] with the caller's cap on the
483/// splits, as [`crate::one_sided_boundary_hausdorff_with_budget`].
484///
485/// # Errors
486///
487/// As [`one_sided_body_boundary_hausdorff`].
488pub fn one_sided_body_boundary_hausdorff_with_budget<'a, 'b>(
489 from: impl Into<PlacedBody<'a>>,
490 to: impl Into<PlacedBody<'b>>,
491 accuracy: Scalar,
492 tolerance: Tolerance,
493 max_splits: usize,
494) -> Result<BodyHausdorffBounds, BodyMeasureError> {
495 let source = Items::boundary(from.into(), BodySide::First, tolerance)?;
496 let target = Items::boundary(to.into(), BodySide::Second, tolerance)?;
497 one_sided(&source, &target, accuracy, tolerance, max_splits)
498}
499
500/// Two-sided Hausdorff distance between the boundaries of the unions of two
501/// bodies' items, to within `accuracy` (#229).
502///
503/// # Errors
504///
505/// As [`one_sided_body_boundary_hausdorff`].
506pub fn body_boundary_hausdorff_distance<'a, 'b>(
507 a: impl Into<PlacedBody<'a>>,
508 b: impl Into<PlacedBody<'b>>,
509 accuracy: Scalar,
510 tolerance: Tolerance,
511) -> Result<BodyHausdorff, BodyMeasureError> {
512 let first = Items::boundary(a.into(), BodySide::First, tolerance)?;
513 let second = Items::boundary(b.into(), BodySide::Second, tolerance)?;
514 let forward = one_sided(&first, &second, accuracy, tolerance, MAX_SPLITS)?;
515 let backward = one_sided(&second, &first, accuracy, tolerance, MAX_SPLITS)?;
516 let mut distance = if backward.bounds.lower > forward.bounds.lower {
517 backward
518 } else {
519 forward
520 };
521 distance.bounds.upper = forward.bounds.upper.max(backward.bounds.upper);
522 Ok(BodyHausdorff {
523 distance,
524 forward,
525 backward,
526 })
527}
528
529fn one_sided(
530 from: &Items<'_>,
531 to: &Items<'_>,
532 accuracy: Scalar,
533 tolerance: Tolerance,
534 max_splits: usize,
535) -> Result<BodyHausdorffBounds, BodyMeasureError> {
536 let cuts = match (&from.cut, &to.cut) {
537 (None, None) => None,
538 _ => Some((from.cut(), to.cut())),
539 };
540 let found = witnessed(
541 &from.brep,
542 &to.brep,
543 accuracy,
544 tolerance,
545 max_splits,
546 &from.edge_ranges(),
547 cuts.as_ref().map(|(a, b)| (a, b)),
548 )?;
549 Ok(BodyHausdorffBounds {
550 bounds: found.bounds,
551 item_from: from.item(found.from),
552 item_to: to.item(found.to),
553 })
554}
555
556/// A body's items as one B-rep holding all their faces and edges, with the
557/// first face and edge of each item to name it by.
558struct Items<'a> {
559 brep: Cow<'a, ExactBRep>,
560 faces: Vec<usize>,
561 edges: Vec<usize>,
562 /// Where `brep` is the items' boundary cut down to free regions: the
563 /// items uncut, and how each cut face and each face in contact relate.
564 cut: Option<CutFrom<'a>>,
565}
566
567/// The items uncut, the face of them each cut face of the boundary came
568/// from, and the faces lying against each face in contact, all by index
569/// into the uncut items.
570struct CutFrom<'a> {
571 items: Cow<'a, ExactBRep>,
572 origin: HashMap<usize, usize>,
573 partners: HashMap<usize, Vec<usize>>,
574}
575
576impl<'a> Items<'a> {
577 fn new(items: &'a [ExactBRep], body: BodySide) -> Result<Self, BodyMeasureError> {
578 match items {
579 [] => Err(BodyMeasureError::EmptyBody { body }),
580 [one] => Ok(Self {
581 brep: Cow::Borrowed(one),
582 faces: vec![0],
583 edges: vec![0],
584 cut: None,
585 }),
586 _ => {
587 let mut builder = ExactBRepBuilder::default();
588 let (mut faces, mut edges) = (Vec::new(), Vec::new());
589 for item in items {
590 faces.push(builder.topology_mut().faces().len());
591 edges.push(builder.topology_mut().edges().len());
592 builder.append(item, false);
593 }
594 // Every item is a valid B-rep, and so is their disjoint
595 // union; a refusal here is a catalog the items lost.
596 let brep = builder
597 .finish()
598 .map_err(|_| ExactMeasureError::DanglingReference)?;
599 Ok(Self {
600 brep: Cow::Owned(brep),
601 faces,
602 edges,
603 cut: None,
604 })
605 }
606 }
607 }
608
609 /// Both bodies' items, uncut, merged and placed.
610 fn pair(a: PlacedBody<'a>, b: PlacedBody<'a>) -> Result<(Self, Self), BodyMeasureError> {
611 let a = Self::new(a.items, BodySide::First)?.placed(a.placement, BodySide::First)?;
612 let b = Self::new(b.items, BodySide::Second)?.placed(b.placement, BodySide::Second)?;
613 Ok((a, b))
614 }
615
616 /// How the measured boundary was cut from the items: nothing cut when
617 /// no faces were in contact.
618 fn cut(&self) -> Cut<'_> {
619 match &self.cut {
620 Some(cut) => Cut {
621 items: &cut.items,
622 origin: cut.origin.clone(),
623 partners: cut.partners.clone(),
624 },
625 None => Cut {
626 items: &self.brep,
627 origin: HashMap::new(),
628 partners: HashMap::new(),
629 },
630 }
631 }
632
633 /// The boundary of the union of a body's items: their faces, with those
634 /// in exact contact cut down to their free regions, once every pair of
635 /// items is shown apart, touching or in exact contact.
636 fn boundary(
637 placed: PlacedBody<'a>,
638 body: BodySide,
639 tolerance: Tolerance,
640 ) -> Result<Self, BodyMeasureError> {
641 Self::cut_boundary(placed.items, body, tolerance)?.placed(placed.placement, body)
642 }
643
644 /// Move the measured boundary (and the items it was cut from) by a
645 /// rigid placement; face and edge indices are kept.
646 fn placed(self, placement: Transform3, body: BodySide) -> Result<Self, BodyMeasureError> {
647 if placement == Transform3::IDENTITY {
648 return Ok(self);
649 }
650 let place = |brep: &ExactBRep| {
651 brep.transformed(&placement)
652 .map_err(|error| BodyMeasureError::Placement { body, error })
653 };
654 let cut = match self.cut {
655 Some(cut) => Some(CutFrom {
656 items: Cow::Owned(place(&cut.items)?),
657 origin: cut.origin,
658 partners: cut.partners,
659 }),
660 None => None,
661 };
662 Ok(Self {
663 brep: Cow::Owned(place(&self.brep)?),
664 faces: self.faces,
665 edges: self.edges,
666 cut,
667 })
668 }
669
670 /// The boundary of the union of the items, in their own frame.
671 fn cut_boundary(
672 items: &'a [ExactBRep],
673 body: BodySide,
674 tolerance: Tolerance,
675 ) -> Result<Self, BodyMeasureError> {
676 if items.is_empty() {
677 return Err(BodyMeasureError::EmptyBody { body });
678 }
679 let partners = check_layout(items, body, tolerance)?;
680 if partners.is_empty() {
681 return Self::new(items, body);
682 }
683 let uncut = Self::new(items, body)?;
684 let mut origin = HashMap::new();
685 let mut assembler = Assembler::default();
686 let (mut faces, mut edges) = (Vec::new(), Vec::new());
687 for (index, item) in items.iter().enumerate() {
688 let (first_face, first_edge) = assembler.counts();
689 faces.push(first_face);
690 edges.push(first_edge);
691 assembler.next_item();
692 for face in 0..item.topology().faces().len() {
693 let cut = match partners.get(&(index, face)) {
694 Some((plane, against)) => {
695 let against: Vec<(&ExactBRep, usize)> =
696 against.iter().map(|&(j, g)| (&items[j], g)).collect();
697 contact::free_regions(item, face, &against, *plane)
698 .map_err(|()| BodyMeasureError::ItemsShareFace {
699 body,
700 first: index.min(partners[&(index, face)].1[0].0),
701 second: index.max(partners[&(index, face)].1[0].0),
702 })?
703 .map(|regions| (regions, *plane))
704 }
705 None => None,
706 };
707 match cut {
708 Some((regions, plane)) => {
709 for region in ®ions {
710 origin.insert(assembler.counts().0, uncut.faces[index] + face);
711 assembler.add_region(region, plane);
712 }
713 }
714 None => assembler
715 .copy_face(item, face)
716 .ok_or(ExactMeasureError::DanglingReference)?,
717 }
718 }
719 }
720 let brep = assembler
721 .finish()
722 .ok_or(ExactMeasureError::DanglingReference)?;
723 let partners = partners
724 .iter()
725 .map(|(&(item, face), (_, against))| {
726 (
727 uncut.faces[item] + face,
728 against
729 .iter()
730 .map(|&(other, g)| uncut.faces[other] + g)
731 .collect(),
732 )
733 })
734 .collect();
735 Ok(Self {
736 brep: Cow::Owned(brep),
737 faces,
738 edges,
739 cut: Some(CutFrom {
740 items: uncut.brep,
741 origin,
742 partners,
743 }),
744 })
745 }
746
747 /// Each item's edges, by index in the merged B-rep.
748 fn edge_ranges(&self) -> Vec<core::ops::Range<usize>> {
749 let total = self.brep.topology().edges().len();
750 (0..self.edges.len())
751 .map(|k| self.edges[k]..self.edges.get(k + 1).copied().unwrap_or(total))
752 .collect()
753 }
754
755 /// The item an element of the merged B-rep belongs to.
756 fn item(&self, shape: Shape) -> usize {
757 let (starts, index) = match shape {
758 Shape::Face { face, .. } => (&self.faces, face),
759 Shape::Edge { edge, .. } => (&self.edges, edge),
760 };
761 starts
762 .partition_point(|&start| start <= index)
763 .saturating_sub(1)
764 }
765}
766
767/// How two items of one body lie (see the module docs).
768#[derive(Debug, Clone, PartialEq)]
769enum Layout {
770 /// Shown disjoint.
771 Apart,
772 /// Shown touching, with no patch of face shared.
773 Touching,
774 /// In exact face contact on `plane`: these faces of each lie on it.
775 Contact {
776 plane: AxisPlane,
777 faces_a: Vec<usize>,
778 faces_b: Vec<usize>,
779 },
780 /// Touching where both may hold a patch of the separating plane, on a
781 /// face that is not planar.
782 MayShareFace,
783 /// Within `gap` of face contact (negative: interpenetrating), on axis
784 /// planes that differ, or across a plane more than rounding deep.
785 NearlyShareFace { gap: Scalar },
786 /// Touching to within rounding where both have faces on a plane that
787 /// no coordinate axis is normal to.
788 NotAxisNormal,
789 /// Neither shown.
790 Undecided,
791}
792
793/// The faces of other items lying against each face in exact contact, by
794/// item and face, with the plane they share.
795type Partners = HashMap<(usize, usize), (AxisPlane, Vec<(usize, usize)>)>;
796
797/// Refuse a body two of whose items are not shown apart, touching without
798/// a shared patch, or in exact face contact; and list the faces in contact.
799fn check_layout(
800 items: &[ExactBRep],
801 body: BodySide,
802 tolerance: Tolerance,
803) -> Result<Partners, BodyMeasureError> {
804 let mut partners = Partners::new();
805 if items.len() < 2 {
806 return Ok(partners);
807 }
808 let linear = tolerance.linear().max(1e-12);
809 let sides = items
810 .iter()
811 .map(|item| Side::new(item, linear, Metric::Space))
812 .collect::<Result<Vec<_>, _>>()?;
813 for first in 0..sides.len() {
814 for second in first + 1..sides.len() {
815 match layout(&sides[first], &sides[second], tolerance)? {
816 Layout::Apart | Layout::Touching => {}
817 Layout::Contact {
818 plane,
819 faces_a,
820 faces_b,
821 } => {
822 for &fa in &faces_a {
823 for &fb in &faces_b {
824 for (from, to) in
825 [((first, fa), (second, fb)), ((second, fb), (first, fa))]
826 {
827 partners
828 .entry(from)
829 .or_insert_with(|| (plane, Vec::new()))
830 .1
831 .push(to);
832 }
833 }
834 }
835 }
836 Layout::NearlyShareFace { gap } => {
837 return Err(BodyMeasureError::ItemsNearlyShareFace {
838 body,
839 first,
840 second,
841 gap,
842 })
843 }
844 Layout::NotAxisNormal => {
845 return Err(BodyMeasureError::ContactPlaneNotAxisNormal {
846 body,
847 first,
848 second,
849 })
850 }
851 Layout::MayShareFace => {
852 return Err(BodyMeasureError::ItemsShareFace {
853 body,
854 first,
855 second,
856 })
857 }
858 Layout::Undecided => {
859 return Err(BodyMeasureError::ItemsOverlap {
860 body,
861 first,
862 second,
863 })
864 }
865 }
866 }
867 }
868 Ok(partners)
869}
870
871/// The range of `d . x` over every element of a side: a sound enclosure of
872/// the item's extent along `d`, which is its boundary's.
873fn extent(side: &Side<'_>, d: Vec3) -> Result<(Scalar, Scalar), ExactMeasureError> {
874 let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
875 for element in &side.elements {
876 let (lo, hi) = side.project(element, d)?;
877 range = (range.0.min(lo), range.1.max(hi));
878 }
879 if range.0.is_finite() && range.1.is_finite() {
880 Ok(range)
881 } else {
882 Err(crate::exact::EVALUATION)
883 }
884}
885
886/// The unit normals of a side's planar faces.
887fn plane_normals(side: &Side<'_>) -> Result<Vec<Vec3>, ExactMeasureError> {
888 let mut normals = Vec::new();
889 for face in side.brep.topology().faces() {
890 if let Surface::Plane(plane) = surface_of(side.brep, face.surface)? {
891 let n = plane.frame.x.cross(plane.frame.y);
892 let length = n.length();
893 if length.is_finite() && length > 0.0 {
894 normals.push(n / length);
895 }
896 }
897 }
898 Ok(normals)
899}
900
901/// A plane `d . x = level` with `a` on its low side and `b` on its high
902/// side, to within `slack`; `gap` is the certified least separation of
903/// the two along `d`, negative where their ranges overlap.
904#[derive(Debug, Clone, Copy)]
905struct Plane {
906 d: Vec3,
907 level: Scalar,
908 slack: Scalar,
909 gap: Scalar,
910}
911
912/// How the items' ranges along the unit `d` lie: certainly apart (a
913/// positive gap), within rounding of a plane between them, overlapping by
914/// no more than `tolerance`, or overlapping more.
915fn separate(
916 a: &Side<'_>,
917 b: &Side<'_>,
918 d: Vec3,
919 tolerance: Scalar,
920) -> Result<Separation, ExactMeasureError> {
921 let (a_lo, a_hi) = extent(a, d)?;
922 let (b_lo, b_hi) = extent(b, d)?;
923 let scale = a_lo.abs().max(a_hi.abs()).max(b_lo.abs()).max(b_hi.abs());
924 let slack = 1e-9 * (1.0 + scale);
925 let mut found = Separation::Overlapping;
926 for (gap, d, level) in [
927 (b_lo - a_hi, d, 0.5 * (a_hi + b_lo)),
928 (a_lo - b_hi, -d, -0.5 * (b_hi + a_lo)),
929 ] {
930 if gap > 0.0 {
931 return Ok(Separation::Apart);
932 }
933 let plane = Plane {
934 d,
935 level,
936 slack,
937 gap,
938 };
939 if gap >= -slack {
940 found = Separation::Touching(plane);
941 } else if gap >= -tolerance && !matches!(found, Separation::Touching(_)) {
942 found = Separation::Near(Plane {
943 slack: slack - gap,
944 ..plane
945 });
946 }
947 }
948 Ok(found)
949}
950
951#[derive(Debug, Clone, Copy)]
952enum Separation {
953 Apart,
954 Touching(Plane),
955 Near(Plane),
956 Overlapping,
957}
958
959/// The faces of the side that may hold an open patch of the plane: the
960/// planar faces lying in it, and whether a B-spline face reaches it (no
961/// other family holds an open patch of a plane).
962fn patch_faces(side: &Side<'_>, plane: &Plane) -> Result<(Vec<usize>, bool), ExactMeasureError> {
963 let band = (
964 plane.level - 2.0 * plane.slack,
965 plane.level + 2.0 * plane.slack,
966 );
967 let mut planar = Vec::new();
968 let mut spline = false;
969 for element in &side.elements {
970 let Shape::Face { face, .. } = element.shape else {
971 continue;
972 };
973 let surface = surface_of(side.brep, side.brep.topology().faces()[face].surface)?;
974 let (lo, hi) = side.project(element, plane.d)?;
975 match surface {
976 Surface::Plane(_) if lo >= band.0 && hi <= band.1 => {
977 if !planar.contains(&face) {
978 planar.push(face);
979 }
980 }
981 Surface::BSpline(_) if hi >= band.0 && lo <= band.1 => spline = true,
982 _ => {}
983 }
984 }
985 Ok((planar, spline))
986}
987
988/// Whether some face of the side may hold an open patch of the plane.
989fn may_hold_patch(side: &Side<'_>, plane: &Plane) -> Result<bool, ExactMeasureError> {
990 let (planar, spline) = patch_faces(side, plane)?;
991 Ok(spline || !planar.is_empty())
992}
993
994/// The layout of two items touching on `plane` where both may hold a patch
995/// of it: exact face contact when every such face is planar and lies, in
996/// its own numbers, on one axis plane; else near contact, or a face that
997/// cannot be cut.
998fn contact(a: &Side<'_>, b: &Side<'_>, plane: &Plane) -> Result<Layout, ExactMeasureError> {
999 let (faces_a, spline_a) = patch_faces(a, plane)?;
1000 let (faces_b, spline_b) = patch_faces(b, plane)?;
1001 if spline_a || spline_b {
1002 return Ok(Layout::MayShareFace);
1003 }
1004 let mut common: Option<AxisPlane> = None;
1005 let mut off_level = false;
1006 let mut not_axis_normal = false;
1007 for (side, faces) in [(a, &faces_a), (b, &faces_b)] {
1008 for &face in faces {
1009 match (contact::axis_plane(side.brep, face), common) {
1010 (Err(Uncut::Edge), _) => return Ok(Layout::MayShareFace),
1011 (Err(Uncut::NotAxisNormal), _) => not_axis_normal = true,
1012 (Ok(found), None) => common = Some(found),
1013 (Ok(found), Some(seen)) if found == seen => {}
1014 _ => off_level = true,
1015 }
1016 }
1017 }
1018 // Touching to within rounding on a plane no axis is normal to: the
1019 // contact cannot be shown exact there, whatever the gap.
1020 if not_axis_normal {
1021 return Ok(Layout::NotAxisNormal);
1022 }
1023 // On axis planes, but not at one coordinate: a gap or an overlap below
1024 // rounding, or within the tolerance.
1025 if off_level {
1026 return Ok(Layout::NearlyShareFace { gap: plane.gap });
1027 }
1028 Ok(match common {
1029 Some(on) => Layout::Contact {
1030 plane: on,
1031 faces_a,
1032 faces_b,
1033 },
1034 None => Layout::NearlyShareFace { gap: plane.gap },
1035 })
1036}
1037
1038fn layout(a: &Side<'_>, b: &Side<'_>, tolerance: Tolerance) -> Result<Layout, ExactMeasureError> {
1039 let mut candidates = vec![Vec3::X, Vec3::Y, Vec3::Z];
1040 for n in plane_normals(a)?.into_iter().chain(plane_normals(b)?) {
1041 // `n` and `-n` are one candidate: both senses are tried.
1042 if candidates
1043 .iter()
1044 .all(|seen| seen.dot(n).abs() < 1.0 - 1e-12)
1045 {
1046 candidates.push(n);
1047 }
1048 }
1049 let linear = tolerance.linear();
1050 let mut touching: Vec<Plane> = Vec::new();
1051 let mut near: Vec<Plane> = Vec::new();
1052 for d in candidates {
1053 match separate(a, b, d, linear)? {
1054 Separation::Apart => return Ok(Layout::Apart),
1055 Separation::Touching(plane) => touching.push(plane),
1056 Separation::Near(plane) => near.push(plane),
1057 Separation::Overlapping => {}
1058 }
1059 }
1060 // Two touching planes with `a` below both: so it is below their sum.
1061 let base = touching.len();
1062 for i in 0..base {
1063 for j in i + 1..base {
1064 let sum = touching[i].d + touching[j].d;
1065 let length = sum.length();
1066 if length > 1e-6 {
1067 match separate(a, b, sum / length, linear)? {
1068 Separation::Apart => return Ok(Layout::Apart),
1069 Separation::Touching(plane) => touching.push(plane),
1070 Separation::Near(_) | Separation::Overlapping => {}
1071 }
1072 }
1073 }
1074 }
1075 for plane in &touching {
1076 if !may_hold_patch(a, plane)? || !may_hold_patch(b, plane)? {
1077 return Ok(Layout::Touching);
1078 }
1079 }
1080 // Touching where both hold a patch of the plane: face contact, cut
1081 // exactly or refused by name.
1082 if let Some(plane) = touching.first() {
1083 return contact(a, b, plane);
1084 }
1085 if boundaries_apart(a, b, tolerance)? && !nested(a, b)? {
1086 return Ok(Layout::Apart);
1087 }
1088 // Interpenetrating by no more than the tolerance across a plane where
1089 // both have a face: near contact, refused by name.
1090 for plane in &near {
1091 if may_hold_patch(a, plane)? && may_hold_patch(b, plane)? {
1092 return Ok(Layout::NearlyShareFace { gap: plane.gap });
1093 }
1094 }
1095 Ok(Layout::Undecided)
1096}
1097
1098/// Whether the boundaries are certainly apart: a positive lower bound on
1099/// the distance between them.
1100fn boundaries_apart(
1101 a: &Side<'_>,
1102 b: &Side<'_>,
1103 tolerance: Tolerance,
1104) -> Result<bool, ExactMeasureError> {
1105 let found = search_within(
1106 a.brep,
1107 b.brep,
1108 tolerance,
1109 Metric::Space,
1110 OVERLAP_STEPS,
1111 &mut |lower, _, _| lower > 0.0,
1112 )?;
1113 Ok(found.bounds.lower > 0.0)
1114}
1115
1116/// Whether one item may lie inside the other, for two items whose
1117/// boundaries are apart: unless each is one solid with a boundary point
1118/// outside an enclosing box of the other (see the module docs).
1119fn nested(a: &Side<'_>, b: &Side<'_>) -> Result<bool, ExactMeasureError> {
1120 let one_solid = |brep: &ExactBRep| {
1121 let topology = brep.topology();
1122 topology.solids().len() == 1
1123 || (topology.solids().is_empty() && topology.shells().len() == 1)
1124 };
1125 if !one_solid(a.brep) || !one_solid(b.brep) {
1126 return Ok(true);
1127 }
1128 Ok(!sticks_out(a, b)? || !sticks_out(b, a)?)
1129}
1130
1131/// Whether a point certainly on `a`'s boundary lies outside an enclosing
1132/// box of `b`.
1133fn sticks_out(a: &Side<'_>, b: &Side<'_>) -> Result<bool, ExactMeasureError> {
1134 let mut boxes = [(0.0, 0.0); 3];
1135 for (axis, d) in [Vec3::X, Vec3::Y, Vec3::Z].into_iter().enumerate() {
1136 boxes[axis] = extent(b, d)?;
1137 }
1138 let outside = |p: Point3| {
1139 (0..3).any(|axis| {
1140 let (lo, hi) = boxes[axis];
1141 let margin = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
1142 p[axis] < lo - margin || p[axis] > hi + margin
1143 })
1144 };
1145 if a.elements
1146 .iter()
1147 .filter_map(|element| element.witness)
1148 .any(outside)
1149 {
1150 return Ok(true);
1151 }
1152 // Edge ends: a corner sticking out is often the only point that does.
1153 let topology = a.brep.topology();
1154 for (index, edge) in topology.edges().iter().enumerate() {
1155 let Some(curve) = edge.curve.and_then(|id| a.brep.curves3().get(id.index())) else {
1156 continue;
1157 };
1158 let Some(span) = topology
1159 .edge_id_at(index)
1160 .and_then(|id| a.brep.edge_interval(id))
1161 else {
1162 continue;
1163 };
1164 for t in [span.start, span.end] {
1165 if evaluate3(curve, t).is_ok_and(outside) {
1166 return Ok(true);
1167 }
1168 }
1169 }
1170 Ok(false)
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 //! Placing a cut boundary moves the items it was cut from with it: the
1176 //! cut faces' bound (`exact_hausdorff/cut.rs`) matches those items
1177 //! between bodies, and left unplaced they would match bodies placed
1178 //! apart as though they coincided. A rigid motion makes some uncut face
1179 //! tie every cut face's farthest point, so no closed form sees it.
1180
1181 use super::{contact, BodySide, Cow, CutFrom, HashMap, Items};
1182 use axiolid_core::{Point2, Transform3, Vec3};
1183 use axiolid_overlay::ArcRing;
1184
1185 #[test]
1186 fn placing_a_cut_boundary_places_its_uncut_items() {
1187 let plane = contact::AxisPlane {
1188 axis: 2,
1189 level: 0.5,
1190 };
1191 let mut assembler = contact::Assembler::default();
1192 assembler.add_region(
1193 &[ArcRing::from_points(&[
1194 Point2::new(0.0, 0.0),
1195 Point2::new(1.0, 0.0),
1196 Point2::new(1.0, 1.0),
1197 Point2::new(0.0, 1.0),
1198 ])],
1199 plane,
1200 );
1201 let sheet = assembler.finish().expect("a sheet");
1202 let items = Items {
1203 brep: Cow::Owned(sheet.clone()),
1204 faces: vec![0],
1205 edges: vec![0],
1206 cut: Some(CutFrom {
1207 items: Cow::Owned(sheet),
1208 origin: HashMap::new(),
1209 partners: HashMap::new(),
1210 }),
1211 };
1212 let shift = Vec3::new(2.0, -1.0, 3.0);
1213 let placed = items
1214 .placed(Transform3::from_translation(shift), BodySide::First)
1215 .expect("rigid");
1216 let first = |brep: &axiolid_brep::ExactBRep| brep.topology().vertices()[0].position;
1217 let cut = placed.cut.as_ref().expect("still cut");
1218 assert_eq!(first(&placed.brep), first(&cut.items));
1219 assert!((first(&cut.items).z - 3.5).abs() < 1e-12);
1220 }
1221}