1use 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#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
37#[serde(default)]
38pub struct ClearanceOptions {
39 pub clearance: f64,
40}
41
42impl Default for ClearanceOptions {
43 fn default() -> Self {
45 Self { clearance: 1.0 }
46 }
47}
48
49const OFFERED: [f64; 4] = [0.5, 1.0, 2.0, 5.0];
51
52#[derive(Clone, Debug, Serialize)]
54pub struct Near {
55 pub a: String,
56 pub b: String,
57 pub distance: Bounded,
60 pub witness: [Vector3; 2],
62 #[serde(skip)]
64 pub drawn: Vec<PickedElement>,
65}
66
67#[derive(Clone, Debug, Serialize)]
69pub struct Unmeasured {
70 pub a: String,
71 pub b: String,
72 pub error: String,
73}
74
75#[derive(Clone, Debug, Serialize)]
77pub struct ClearanceReport {
78 pub clearance: f64,
79 pub solids: usize,
80 pub near: Vec<Near>,
82 pub unmeasured: Vec<Unmeasured>,
83 pub work: Work,
86}
87
88impl ClearanceReport {
89 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 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
155pub 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 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 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
230type 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
299fn 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 sum.sqrt().map_or(Scalar::ZERO, Scalar::lower)
312}