Skip to main content

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 &regions {
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}