1use geop_core_editor::editing::Tone;
6use geop_core_editor::output::{Answer, Block, Bounded, Row};
7use geop_core_math::{
8 geop_error::{GeopError, GeopResult, WithContext},
9 primitives::Pose,
10 vector::{Vector, Vector3},
11};
12use geop_core_program::{
13 element::PickedElement,
14 placed::{INSTANCE_SEPARATOR, instance_path},
15 reader::Reader,
16 watch,
17};
18use geop_core_topology::{
19 Body, SolidId,
20 distance::{Feature, PlacedFace, could_touch},
21};
22use geop_ops_model::placed::{
23 locate,
24 solids::{PlacedSolid, placed_solids},
25};
26use geop_ops_model::{BodyNames, NamedModel, Namer};
27use std::collections::HashMap;
28
29use crate::{Scope, mass::drawn, scope::Stopped};
30use geop_lib_booleans::{RemeshParams, boolean};
31use geop_ops_model::BooleanOp;
32use serde::Serialize;
33
34pub type Box3 = Vector3;
36
37pub(crate) fn hull(points: &[Vector3]) -> Box3 {
39 points
40 .iter()
41 .copied()
42 .reduce(|a, b| a.union(&b))
43 .unwrap_or_else(Vector3::everything)
44}
45
46pub(crate) fn apart(a: &Box3, b: &Box3) -> bool {
48 (0..3).any(|k| {
49 a[k].upper().definitely_less(b[k].lower()) || b[k].upper().definitely_less(a[k].lower())
50 })
51}
52
53fn place_control_point(solid: &PlacedSolid, cp: &Vector<4>) -> GeopResult<Box3> {
56 let p = cp.dehomogenized()?;
57 Ok(match &solid.pose {
58 Some(pose) => pose.apply(&p),
59 None => p,
60 })
61}
62
63pub fn bounding_box(solid: &PlacedSolid, faces: &[geop_core_topology::FaceId]) -> GeopResult<Box3> {
66 let model = solid.model.topology();
67 let mut points = Vec::new();
68 for &face in faces {
69 for cp in &model.get_face(face)?.surface.control_points {
70 points.push(place_control_point(solid, cp)?);
71 }
72 }
73 Ok(hull(&points))
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "snake_case")]
79pub enum Contact {
80 Overlap,
82 Touch,
84}
85
86#[derive(Clone, Debug, Serialize)]
88pub struct Interference {
89 pub a: String,
90 pub b: String,
91 pub contact: Contact,
92 pub volume: Option<Bounded>,
94 #[serde(skip)]
96 pub drawn: Vec<PickedElement>,
97}
98
99#[derive(Clone, Debug, Serialize)]
101pub struct Unchecked {
102 pub a: String,
103 pub b: String,
104 pub error: String,
105}
106
107#[derive(Clone, Debug, Serialize)]
110pub struct InterferenceReport {
111 pub solids: usize,
113 pub found: Vec<Interference>,
114 pub unchecked: Vec<Unchecked>,
115 pub work: Work,
117 pub stopped: Option<Stopped>,
120}
121
122impl InterferenceReport {
123 pub fn shown(&self) -> Answer {
126 let mut blocks = Vec::new();
127 if let Some(Stopped { checked, of }) = self.stopped {
128 blocks.push(Block::Text {
129 text: format!("Stopped after {checked} of {of} pairs: the rest may meet."),
130 tone: Tone::Error,
131 });
132 } else if self.found.is_empty() && self.unchecked.is_empty() {
133 let plural = if self.solids == 1 { "" } else { "s" };
134 blocks.push(Block::Text {
135 text: format!("No interference among {} solid{plural}.", self.solids),
136 tone: Tone::Hint,
137 });
138 }
139 let mut items: Vec<Row> = (self.found.iter())
140 .map(|f| {
141 let (how, tone) = match f.contact {
142 Contact::Overlap => {
143 let volume = f.volume.map_or("?".into(), |v| v.show(3));
144 (format!("overlap {volume} mm³"), Tone::Error)
145 }
146 Contact::Touch => ("touch".into(), Tone::Normal),
147 };
148 Row {
149 light: f.drawn.clone(),
150 tone,
151 ..Row::of(vec![format!("{} × {}: {how}", f.a, f.b)])
152 }
153 })
154 .collect();
155 items.extend(self.unchecked.iter().map(|u| Row {
156 detail: Some(u.error.clone()),
157 tone: Tone::Hint,
158 ..Row::of(vec![format!("{} × {}: could not be checked", u.a, u.b)])
159 }));
160 if !items.is_empty() {
161 blocks.push(Block::List { items });
162 }
163 Answer {
164 blocks,
165 plane: None,
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
173pub struct Work {
174 pub boxes_compared: usize,
176 pub candidates: usize,
179 pub booleans: usize,
182}
183
184pub fn interference_report(reader: &Reader, scope: &Scope) -> GeopResult<InterferenceReport> {
202 let solids = placed_solids(reader)?;
203 scope.check(&solids)?;
204 let mut local = LocalBoxes::default();
205 let boxes = (solids.iter())
206 .map(|s| local.placed(s))
207 .collect::<GeopResult<Vec<_>>>()?;
208 let mut work = Work::default();
209 let mut pairs = candidate_pairs(&boxes, &mut work.boxes_compared);
210 pairs.retain(|&(i, j)| scope.holds_pair(&solids[i].instance, &solids[j].instance));
211 let of = pairs.len();
212 let mut stopped = None;
213 let mut checked = Checked::default();
214 let mut poses = LocalPoses::default();
215 let mut found = Vec::new();
216 let mut unchecked = Vec::new();
217 for (at, (i, j)) in pairs.into_iter().enumerate() {
218 if !watch::go_on("checking pairs of solids", at, of) {
219 stopped = Some(Stopped { checked: at, of });
220 break;
221 }
222 work.candidates += 1;
223 let (a, b) = (&solids[i], &solids[j]);
224 let relative = poses.relative(reader, a, b)?;
225 match checked.check(a, b, relative, &mut work.booleans) {
226 Ok(Some((contact, volume))) => found.push(Interference {
227 a: a.name.clone(),
228 b: b.name.clone(),
229 contact,
230 volume,
231 drawn: [drawn(a), drawn(b)].concat(),
232 }),
233 Ok(None) => {}
234 Err(e) => unchecked.push(Unchecked {
235 a: a.name.clone(),
236 b: b.name.clone(),
237 error: e.to_string(),
238 }),
239 }
240 }
241 Ok(InterferenceReport {
242 solids: solids.len(),
243 found,
244 unchecked,
245 work,
246 stopped,
247 })
248}
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
254pub(crate) struct Shape(*const NamedModel, SolidId);
255
256impl Shape {
257 pub(crate) fn of(solid: &PlacedSolid) -> Self {
258 Shape(std::ptr::from_ref(&*solid.model), solid.solid)
259 }
260}
261
262#[derive(Default)]
265pub(crate) struct LocalBoxes(HashMap<Shape, Box3>);
266
267impl LocalBoxes {
268 pub(crate) fn placed(&mut self, solid: &PlacedSolid) -> GeopResult<Box3> {
271 let shape = Shape::of(solid);
272 let own = match self.0.get(&shape) {
273 Some(own) => *own,
274 None => {
275 let unplaced = PlacedSolid {
276 pose: None,
277 name: solid.name.clone(),
278 instance: solid.instance.clone(),
279 reader: solid.reader.clone(),
280 model: std::borrow::Cow::Borrowed(&*solid.model),
281 solid: solid.solid,
282 };
283 let faces = solid.model.topology().solid_faces(solid.solid)?;
284 let own = bounding_box(&unplaced, &faces)?;
285 self.0.insert(shape, own);
286 own
287 }
288 };
289 Ok(match &solid.pose {
290 None => own,
291 Some(pose) => hull(&corners(&own).map(|c| pose.apply(&c))),
292 })
293 }
294}
295
296pub(crate) fn corners(b: &Box3) -> [Vector3; 8] {
298 std::array::from_fn(|k| {
299 Vector3::from_array(std::array::from_fn(|axis| match (k >> axis) & 1 {
300 0 => b[axis].lower(),
301 _ => b[axis].upper(),
302 }))
303 })
304}
305
306pub fn candidate_pairs(boxes: &[Box3], compared: &mut usize) -> Vec<(usize, usize)> {
311 let start = |k: usize| boxes[k][0].lower();
312 let mut order: Vec<usize> = (0..boxes.len()).collect();
313 order.sort_by(|&a, &b| {
315 if start(a).definitely_less(start(b)) {
316 std::cmp::Ordering::Less
317 } else if start(b).definitely_less(start(a)) {
318 std::cmp::Ordering::Greater
319 } else {
320 a.cmp(&b)
321 }
322 });
323 let mut active: Vec<usize> = Vec::new();
324 let mut pairs = Vec::new();
325 for &k in &order {
326 active.retain(|&j| !boxes[j][0].upper().definitely_less(start(k)));
329 for &j in &active {
330 *compared += 1;
331 if !apart(&boxes[j], &boxes[k]) {
332 pairs.push((j.min(k), j.max(k)));
333 }
334 }
335 active.push(k);
336 }
337 pairs.sort_unstable();
338 pairs
339}
340
341#[derive(Default)]
343pub(crate) struct LocalPoses(HashMap<String, Pose>);
344
345impl LocalPoses {
346 pub(crate) fn relative(
353 &mut self,
354 reader: &Reader,
355 a: &PlacedSolid,
356 b: &PlacedSolid,
357 ) -> GeopResult<Option<Pose>> {
358 let (path_a, path_b) = (levels(&a.instance), levels(&b.instance));
359 let common = (path_a.iter().zip(&path_b))
360 .take_while(|(x, y)| x == y)
361 .count();
362 let below_a = self.below(reader, &path_a[common..])?;
363 let below_b = self.below(reader, &path_b[common..])?;
364 Ok(match (below_a, below_b) {
365 (None, b) => b,
366 (Some(a), None) => Some(a.inverse()),
367 (Some(a), Some(b)) => Some(a.inverse().compose(&b)),
368 })
369 }
370
371 fn below(&mut self, reader: &Reader, paths: &[String]) -> GeopResult<Option<Pose>> {
374 let mut pose: Option<Pose> = None;
375 for path in paths {
376 let local = match self.0.get(path) {
377 Some(local) => *local,
378 None => {
379 let local = locate(reader, path)?.local;
380 self.0.insert(path.clone(), local);
381 local
382 }
383 };
384 pose = Some(match pose {
385 None => local,
386 Some(outer) => outer.compose(&local),
387 });
388 }
389 Ok(pose)
390 }
391}
392
393fn levels(instance: &str) -> Vec<String> {
396 let mut paths: Vec<String> = Vec::new();
397 for name in instance.split(INSTANCE_SEPARATOR).filter(|n| !n.is_empty()) {
398 let outer = paths.last().map_or("", String::as_str);
399 paths.push(instance_path(outer, name));
400 }
401 paths
402}
403
404type Meeting = Option<(Contact, Option<Bounded>)>;
407
408#[derive(Default)]
415struct Checked(HashMap<(Shape, Shape), Checks>);
416
417type Checks = Vec<(Option<Pose>, Result<Meeting, String>)>;
420
421impl Checked {
422 fn check(
425 &mut self,
426 a: &PlacedSolid,
427 b: &PlacedSolid,
428 relative: Option<Pose>,
429 booleans: &mut usize,
430 ) -> GeopResult<Meeting> {
431 let seen = self.0.entry((Shape::of(a), Shape::of(b))).or_default();
432 if let Some((_, meeting)) = seen.iter().find(|(pose, _)| *pose == relative) {
433 return meeting.clone().map_err(GeopError::new);
434 }
435 *booleans += 1;
436 let first = PlacedSolid {
437 pose: None,
438 name: a.name.clone(),
439 instance: a.instance.clone(),
440 reader: a.reader.clone(),
441 model: std::borrow::Cow::Borrowed(&*a.model),
442 solid: a.solid,
443 };
444 let second = PlacedSolid {
445 pose: relative,
446 name: b.name.clone(),
447 instance: b.instance.clone(),
448 reader: b.reader.clone(),
449 model: std::borrow::Cow::Borrowed(&*b.model),
450 solid: b.solid,
451 };
452 let meeting = check_pair(&first, &second).map_err(|e| e.to_string());
453 seen.push((relative, meeting.clone()));
454 meeting.map_err(GeopError::new)
455 }
456}
457
458fn check_pair(a: &PlacedSolid, b: &PlacedSolid) -> GeopResult<Meeting> {
460 let ctx = |e: GeopError| e.with_context(format!("checking {} against {}", a.name, b.name));
461 let mut scratch = NamedModel::new();
462 let copy_a = copy_solid(&mut scratch, a, "a").with_context(&ctx)?;
463 let copy_b = copy_solid(&mut scratch, b, "b").with_context(&ctx)?;
464 let namer = Namer::new("interference", "overlap").with_context(&ctx)?;
465 let overlap = boolean(
466 &mut scratch,
467 &namer,
468 copy_a,
469 copy_b,
470 BooleanOp::Intersection,
471 RemeshParams::default(),
472 )
473 .with_context(&ctx)?;
474 if let Some(solid) = overlap {
475 let volume = scratch
476 .topology()
477 .mass_properties(solid)
478 .with_context(&ctx)?
479 .volume;
480 return Ok(Some((Contact::Overlap, Some(Bounded::of(volume)))));
481 }
482 Ok(touch(a, b)
483 .with_context(&ctx)?
484 .then_some((Contact::Touch, None)))
485}
486
487fn touch(a: &PlacedSolid, b: &PlacedSolid) -> GeopResult<bool> {
490 let faces = |s: &PlacedSolid| -> GeopResult<Vec<_>> {
491 (s.model.topology().solid_faces(s.solid)?.into_iter())
492 .map(|face| Ok((face, bounding_box(s, &[face])?)))
493 .collect()
494 };
495 let (fa, fb) = (faces(a)?, faces(b)?);
496 for (at, (face_a, box_a)) in fa.iter().enumerate() {
497 watch::checkpoint("looking for where two solids touch", at, fa.len())?;
498 for (face_b, box_b) in &fb {
499 if apart(box_a, box_b) {
500 continue;
501 }
502 let touching = could_touch(
503 &Feature::Face(PlacedFace::new(a.model.topology(), *face_a, a.pose)?),
504 &Feature::Face(PlacedFace::new(b.model.topology(), *face_b, b.pose)?),
505 )?;
506 if touching {
507 return Ok(true);
508 }
509 }
510 }
511 Ok(false)
512}
513
514fn copy_solid(scratch: &mut NamedModel, solid: &PlacedSolid, prefix: &str) -> GeopResult<SolidId> {
518 let shells = &solid.model.topology().get_solid(solid.solid)?.shells;
519 if shells.len() != 1 {
520 return Err(GeopError::new(format!(
521 "the solid {} has {} shells — a void inside it — and interference is checked for \
522 solids of one shell only",
523 solid.name,
524 shells.len()
525 )));
526 }
527 let (spec, names) = solid.model.body_record(Body::Solid(solid.solid))?;
528 let spec = match &solid.pose {
529 Some(pose) => spec.placed(pose),
530 None => spec,
531 };
532 let names = BodyNames {
533 solid: Some(prefix.to_string()),
534 ..names.renamed(|name| format!("{prefix}.{name}"))
535 };
536 let built = scratch.build_body(spec, names)?;
537 built
538 .solid
539 .ok_or_else(|| GeopError::new(format!("the copy of {} is no solid", solid.name)))
540}