Skip to main content

geop_ops_inspect/
clearance.rs

1//! [`clearance_report`]: which solids of a part come nearer each other than
2//! a clearance — its own, and those of the parts placed in it — and the
3//! least distance between them, where it is attained.
4//!
5//! The least distance between two solids is the least between their faces,
6//! each pair's from the branch and bound of [`closest_points`]: an
7//! enclosure, not the best of samples. Pairs whose boxes are further apart
8//! than the clearance are never looked at (the sweep of
9//! [`candidate_pairs`]), nor pairs of faces whose boxes are further apart
10//! than the least distance found so far; and a pair of the same two shapes
11//! at the same relative pose — every screw beside its nut — is measured
12//! once, as interference checks it once.
13
14use std::collections::HashMap;
15
16use geop_core_editor::editing::{Shape as Drawn, Style, Tone, Visual};
17use geop_core_editor::output::{Answer, AnswerAction, Block, Bounded, Does, Row};
18use geop_core_math::{
19    geop_error::{GeopError, GeopResult},
20    primitives::Pose,
21    scalars::Scalar,
22    vector::Vector3,
23};
24use geop_core_program::{element::PickedElement, reader::Reader};
25use geop_core_topology::distance::{Closest, Feature, PlacedFace, closest_points};
26use geop_ops_model::placed::solids::{PlacedSolid, placed_solids};
27use serde::{Deserialize, Serialize};
28
29use crate::interference::{
30    Box3, LocalBoxes, LocalPoses, Shape, Work, bounding_box, candidate_pairs, corners, hull,
31};
32use crate::mass::drawn;
33
34/// What a clearance check is told: the least distance two solids should
35/// keep, in mm.
36#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
37#[serde(default)]
38pub struct ClearanceOptions {
39    pub clearance: f64,
40}
41
42impl Default for ClearanceOptions {
43    /// A millimetre.
44    fn default() -> Self {
45        Self { clearance: 1.0 }
46    }
47}
48
49/// The clearances offered as buttons, in mm.
50const OFFERED: [f64; 4] = [0.5, 1.0, 2.0, 5.0];
51
52/// Two solids nearer each other than the clearance.
53#[derive(Clone, Debug, Serialize)]
54pub struct Near {
55    pub a: String,
56    pub b: String,
57    /// The least distance between them, in mm: zero for two that touch or
58    /// overlap.
59    pub distance: Bounded,
60    /// Where it is attained: a point on each, where they are placed.
61    pub witness: [Vector3; 2],
62    /// What is drawn of the two, to light.
63    #[serde(skip)]
64    pub drawn: Vec<PickedElement>,
65}
66
67/// A pair that could not be measured, and why.
68#[derive(Clone, Debug, Serialize)]
69pub struct Unmeasured {
70    pub a: String,
71    pub b: String,
72    pub error: String,
73}
74
75/// Every pair of solids a part shows that is nearer than a clearance.
76#[derive(Clone, Debug, Serialize)]
77pub struct ClearanceReport {
78    pub clearance: f64,
79    pub solids: usize,
80    /// The pairs nearer than the clearance, the nearest first.
81    pub near: Vec<Near>,
82    pub unmeasured: Vec<Unmeasured>,
83    /// What finding them took: [`Work::booleans`] counts the pairs of
84    /// solids measured.
85    pub work: Work,
86}
87
88impl ClearanceReport {
89    /// As the inspect panel shows it: the clearances to ask with, then each
90    /// pair nearer, lit while pointed at, how near — and each that could
91    /// not be measured, why.
92    pub fn shown(&self) -> Answer {
93        let actions = (OFFERED.iter())
94            .map(|&clearance| AnswerAction {
95                label: format!("{clearance} mm"),
96                doc: format!("Every pair of solids nearer than {clearance} mm"),
97                chosen: clearance == self.clearance,
98                does: Does::Inspect {
99                    options: serde_json::json!({ "clearance": clearance }),
100                },
101            })
102            .collect();
103        let mut blocks = vec![Block::Actions { actions }];
104        if self.near.is_empty() && self.unmeasured.is_empty() {
105            blocks.push(Block::Text {
106                text: format!(
107                    "No two of {} solids are nearer than {} mm.",
108                    self.solids, self.clearance
109                ),
110                tone: Tone::Hint,
111            });
112        }
113        let mut items: Vec<Row> = (self.near.iter())
114            .map(|n| Row {
115                light: n.drawn.clone(),
116                tone: Tone::Error,
117                ..Row::of(vec![format!(
118                    "{} × {}: {} mm",
119                    n.a,
120                    n.b,
121                    n.distance.show(3)
122                )])
123            })
124            .collect();
125        items.extend(self.unmeasured.iter().map(|u| Row {
126            detail: Some(u.error.clone()),
127            tone: Tone::Hint,
128            ..Row::of(vec![format!("{} × {}: could not be measured", u.a, u.b)])
129        }));
130        if !items.is_empty() {
131            blocks.push(Block::List { items });
132        }
133        Answer {
134            blocks,
135            plane: None,
136        }
137    }
138
139    /// What it draws: a line between the points of each pair nearer than
140    /// the clearance.
141    pub fn visuals(&self) -> Vec<Visual> {
142        (self.near.iter().enumerate())
143            .map(|(k, near)| {
144                let [a, b] = near.witness;
145                Visual::new(
146                    format!("clearance-{k}"),
147                    Drawn::Polyline { points: vec![a, b] },
148                    Style::Selected,
149                )
150            })
151            .collect()
152    }
153}
154
155/// Which solids of the part `reader` reads, its own and those of the parts
156/// placed in it, are nearer each other than `options.clearance`, and how
157/// near (see the module's docs).
158pub fn clearance_report(reader: &Reader, options: ClearanceOptions) -> GeopResult<ClearanceReport> {
159    let clearance = options.clearance;
160    if !clearance.is_finite() || clearance < 0.0 {
161        return Err(GeopError::new(format!(
162            "a clearance of {clearance} mm: give a distance of zero or more"
163        )));
164    }
165    let solids = placed_solids(reader)?;
166    let mut local = LocalBoxes::default();
167    // Half the clearance on every side: two boxes this far apart are as far
168    // apart as the clearance.
169    let half = Scalar::from_f64(clearance)
170        .mul(Scalar::from_f64(0.5))
171        .union(Scalar::ZERO);
172    let pad = Vector3::from_array([half.neg().union(half); 3]);
173    let boxes = (solids.iter())
174        .map(|s| Ok(local.placed(s)?.add(&pad)))
175        .collect::<GeopResult<Vec<_>>>()?;
176    let mut work = Work::default();
177    let pairs = candidate_pairs(&boxes, &mut work.boxes_compared);
178    let mut poses = LocalPoses::default();
179    let mut measured: HashMap<(Shape, Shape), Measured> = HashMap::new();
180    let mut near = Vec::new();
181    let mut unmeasured = Vec::new();
182    let limit = Scalar::from_f64(clearance);
183    for (i, j) in pairs {
184        work.candidates += 1;
185        let (a, b) = (&solids[i], &solids[j]);
186        let relative = poses.relative(reader, a, b)?;
187        let seen = measured.entry((Shape::of(a), Shape::of(b))).or_default();
188        let closest = match seen.iter().find(|(pose, _)| *pose == relative) {
189            Some((_, closest)) => closest.clone(),
190            None => {
191                work.booleans += 1;
192                let closest = least_distance(a, b, relative.as_ref(), limit).map_err(|e| {
193                    e.with_context(format!("measuring {} against {}", a.name, b.name))
194                        .to_string()
195                });
196                seen.push((relative, closest.clone()));
197                closest
198            }
199        };
200        match closest {
201            Ok(Some(closest)) if closest.distance.lower().definitely_less(limit) => {
202                // Measured in the frame of `a`: placed where `a` is.
203                let place = |p: &Vector3| a.pose.as_ref().map_or(*p, |pose| pose.apply(p));
204                near.push(Near {
205                    a: a.name.clone(),
206                    b: b.name.clone(),
207                    distance: Bounded::of(closest.distance),
208                    witness: [place(&closest.a), place(&closest.b)],
209                    drawn: [drawn(a), drawn(b)].concat(),
210                })
211            }
212            Ok(_) => {}
213            Err(error) => unmeasured.push(Unmeasured {
214                a: a.name.clone(),
215                b: b.name.clone(),
216                error,
217            }),
218        }
219    }
220    near.sort_by(|x, y| x.distance.value.total_cmp(&y.distance.value));
221    Ok(ClearanceReport {
222        clearance,
223        solids: solids.len(),
224        near,
225        unmeasured,
226        work,
227    })
228}
229
230/// The least distance between the faces of `a`, where its part has it, and
231/// of `b` placed at `relative` to it — none, if every pair of faces is
232/// further apart than `limit`. Pairs of faces are measured nearest box first, and a pair whose
233/// boxes are definitely further apart than the least distance found, or
234/// than `limit`, is not measured.
235/// What was measured between one pair of shapes, at each relative pose
236/// tried (none: placed as they are).
237type Measured = Vec<(Option<Pose>, Result<Option<Closest>, String>)>;
238
239fn least_distance(
240    a: &PlacedSolid,
241    b: &PlacedSolid,
242    relative: Option<&Pose>,
243    limit: Scalar,
244) -> GeopResult<Option<Closest>> {
245    let faces = |s: &PlacedSolid, pose: Option<&Pose>| -> GeopResult<Vec<_>> {
246        let unplaced = PlacedSolid {
247            pose: None,
248            name: s.name.clone(),
249            instance: s.instance.clone(),
250            reader: s.reader.clone(),
251            model: std::borrow::Cow::Borrowed(&*s.model),
252            solid: s.solid,
253        };
254        (s.model.topology().solid_faces(s.solid)?.into_iter())
255            .map(|face| {
256                let own = bounding_box(&unplaced, &[face])?;
257                let placed = match pose {
258                    None => own,
259                    Some(pose) => hull(&corners(&own).map(|c| pose.apply(&c))),
260                };
261                Ok((face, placed))
262            })
263            .collect()
264    };
265    let (fa, fb) = (faces(a, None)?, faces(b, relative)?);
266    let mut pairs: Vec<(Scalar, usize, usize)> = Vec::new();
267    for (i, (_, box_a)) in fa.iter().enumerate() {
268        for (j, (_, box_b)) in fb.iter().enumerate() {
269            pairs.push((box_gap(box_a, box_b), i, j));
270        }
271    }
272    pairs.sort_by(|x, y| x.0.to_f64().total_cmp(&y.0.to_f64()));
273    let mut best: Option<Closest> = None;
274    for (gap, i, j) in pairs {
275        let bound = best
276            .as_ref()
277            .map_or(limit, |b| b.distance.upper().min(limit));
278        if gap.definitely_greater(bound) {
279            continue;
280        }
281        let closest = closest_points(
282            &Feature::Face(PlacedFace::new(a.model.topology(), fa[i].0, None)?),
283            &Feature::Face(PlacedFace::new(
284                b.model.topology(),
285                fb[j].0,
286                relative.copied(),
287            )?),
288        )?;
289        let nearer = best
290            .as_ref()
291            .is_none_or(|b| (closest.distance.upper()).definitely_less(b.distance.upper()));
292        if nearer {
293            best = Some(closest);
294        }
295    }
296    Ok(best)
297}
298
299/// A lower bound on the distance between two boxes: the length of the gap
300/// between them along each axis, zero where they overlap.
301fn box_gap(a: &Box3, b: &Box3) -> Scalar {
302    let mut sum = Scalar::ZERO;
303    for k in 0..3 {
304        let gap = (b[k].lower().sub(a[k].upper()))
305            .max(a[k].lower().sub(b[k].upper()))
306            .max(Scalar::ZERO)
307            .lower();
308        sum = sum.add(gap.mul(gap));
309    }
310    // The root of a sharp sum of squares: never refused.
311    sum.sqrt().map_or(Scalar::ZERO, Scalar::lower)
312}