1use ogeom_core::{OgeomResult, Tolerances};
14use ogeom_geom::{Surface as _, SurfaceGeometry};
15use ogeom_math::{Aabb, Point, Transform, Vector};
16use ogeom_mesh::Deflection;
17use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
18
19use crate::classify::{Containment, FaceTrim};
20
21pub(crate) const CROSSING_SINE: f64 = 1e-2;
25
26const ASKED_PER_PAIR: usize = 16;
31
32const LEAF: usize = 8;
34
35struct Sheet<'m> {
37 face: &'m Shape,
38 surface: &'m SurfaceGeometry,
39 placement: Transform,
40 reach: f64,
45 trim: std::sync::OnceLock<Option<FaceTrim<'m>>>,
46}
47
48struct Facet {
50 sheet: usize,
51 corners: [Point; 3],
52 chart: [(f64, f64); 3],
53 bound: Aabb,
54}
55
56type Guess = (usize, usize, Point);
58
59#[derive(Debug, Clone)]
61pub struct SelfCrossing {
62 pub face: Shape,
64 pub other: Shape,
66 pub at: Point,
68}
69
70pub fn self_crossings(
79 model: &Model,
80 solid: &Shape,
81 tol: Tolerances,
82) -> OgeomResult<Vec<SelfCrossing>> {
83 crossings(model, solid, &ogeom_mesh::FaceMeshCache::default(), tol)
84}
85
86pub(crate) fn crossings(
97 model: &Model,
98 solid: &Shape,
99 kept: &ogeom_mesh::FaceMeshCache,
100 tol: Tolerances,
101) -> OgeomResult<Vec<SelfCrossing>> {
102 let faces = explore_unique(model, solid, ShapeType::Face)?;
103 let meshed = ogeom_core::parallel::map_ordered(&faces, |_, face| {
104 ogeom_mesh::triangulate_face_kept(model, face, Deflection::default(), kept, tol).ok()
105 });
106 let mut sheets = Vec::with_capacity(faces.len());
107 let mut facets = Vec::new();
108 for (face, mesh) in faces.iter().zip(meshed) {
109 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
110 continue;
111 };
112 let Some(surface) = model.geometry().surface(data.surface) else {
113 continue;
114 };
115 let Some(mesh) = mesh.filter(|m| m.parameters.len() == m.positions.len()) else {
116 continue;
117 };
118 let mut reach = data.tolerance.get().max(tol.confusion());
119 for kind in [ShapeType::Edge, ShapeType::Vertex] {
120 for bound in explore_unique(model, face, kind)? {
121 if let Some(t) = model.node(&bound).and_then(|n| n.data().tolerance()) {
122 reach = reach.max(t.get());
123 }
124 }
125 }
126 let sheet = sheets.len();
127 sheets.push(Sheet {
128 face,
129 surface,
130 placement: face.transform(model.datums())?,
131 reach,
132 trim: std::sync::OnceLock::new(),
133 });
134 for triangle in &mesh.triangles {
135 let corners = triangle.map(|i| mesh.positions[i as usize]);
136 facets.push(Facet {
137 sheet,
138 corners,
139 chart: triangle.map(|i| mesh.parameters[i as usize]),
140 bound: Aabb::of_points(&corners).expanded(tol.confusion()),
141 });
142 }
143 }
144 if facets.len() < 2 {
145 return Ok(Vec::new());
146 }
147
148 let tree = BoxTree::new(&facets);
151 let runs: Vec<(usize, usize)> = (0..facets.len())
152 .step_by(512)
153 .map(|start| (start, (start + 512).min(facets.len())))
154 .collect();
155 let found = ogeom_core::parallel::map_ordered(&runs, |_, &(start, end)| {
156 let mut out: Vec<(usize, usize, usize, usize, Point)> = Vec::new();
157 let mut near = Vec::new();
158 for i in start..end {
159 if i % 64 == 0 {
160 ogeom_core::progress::checkpoint()?;
161 }
162 tree.meeting(&facets, &facets[i].bound, &mut near);
163 for &j in &near {
164 if j <= i || share_a_corner(&facets[i], &facets[j]) {
165 continue;
166 }
167 if let Some(at) = triangles_cross(&facets[i].corners, &facets[j].corners) {
168 let (a, b) = (facets[i].sheet, facets[j].sheet);
169 if a <= b {
170 out.push((a, b, i, j, at));
171 } else {
172 out.push((b, a, j, i, at));
173 }
174 }
175 }
176 }
177 Ok(out)
178 });
179 let mut guesses = Vec::new();
180 for run in found {
181 guesses.extend(run?);
182 }
183 guesses.sort_by_key(|x| (x.0, x.1, x.2, x.3));
184 let mut pairs: Vec<((usize, usize), Vec<Guess>)> = Vec::new();
185 for (a, b, i, j, at) in guesses {
186 match pairs.last_mut() {
187 Some((pair, all)) if *pair == (a, b) => all.push((i, j, at)),
188 _ => pairs.push(((a, b), vec![(i, j, at)])),
189 }
190 }
191 for ((a, b), all) in &mut pairs {
192 if a == b {
193 let surface = sheets[*a].surface;
199 let mut apart: Vec<(f64, Guess)> = all
200 .iter()
201 .map(|&(i, j, at)| (chart_apart(surface, &facets[i], &facets[j]), (i, j, at)))
202 .collect();
203 apart.sort_by(|x, y| y.0.total_cmp(&x.0));
204 *all = apart
205 .into_iter()
206 .take(ASKED_PER_PAIR)
207 .map(|(_, guess)| guess)
208 .collect();
209 } else if all.len() > ASKED_PER_PAIR {
210 *all = (0..ASKED_PER_PAIR)
212 .map(|k| all[k * all.len() / ASKED_PER_PAIR])
213 .collect();
214 }
215 }
216
217 let settled = ogeom_core::parallel::map_ordered(&pairs, |_, ((a, b), asked)| {
218 for &(i, j, guess) in asked {
219 ogeom_core::progress::checkpoint()?;
220 if let Some(at) = settle(model, &sheets, &facets[i], &facets[j], guess, tol)? {
221 return Ok(Some(SelfCrossing {
222 face: sheets[*a].face.clone(),
223 other: sheets[*b].face.clone(),
224 at,
225 }));
226 }
227 }
228 Ok(None)
229 });
230 let mut out = Vec::new();
231 for one in settled {
232 out.extend(one?);
233 }
234 Ok(out)
235}
236
237fn share_a_corner(p: &Facet, q: &Facet) -> bool {
241 p.corners.iter().any(|c| q.corners.contains(c))
242}
243
244fn triangles_cross(p: &[Point; 3], q: &[Point; 3]) -> Option<Point> {
248 let plane = |t: &[Point; 3]| (t[1] - t[0]).cross(t[2] - t[0]);
249 let (np, nq) = (plane(p), plane(q));
250 let side = |n: Vector, origin: Point, t: &[Point; 3]| t.map(|c| n.dot(c - origin));
251 let dp = side(nq, q[0], p);
252 let dq = side(np, p[0], q);
253 let straddles = |d: [f64; 3]| d.iter().any(|&x| x > 0.0) && d.iter().any(|&x| x < 0.0);
254 if !straddles(dp) || !straddles(dq) {
255 return None;
256 }
257 let line = np.cross(nq);
258 if line.dot(line) <= f64::MIN_POSITIVE {
259 return None;
260 }
261 let interval = |t: &[Point; 3], d: [f64; 3]| -> Option<(f64, f64)> {
264 let mut along: Vec<f64> = Vec::with_capacity(3);
265 for k in 0..3 {
266 let (a, b) = (k, (k + 1) % 3);
267 if (d[a] > 0.0) != (d[b] > 0.0) && d[a] != d[b] {
268 let s = d[a] / (d[a] - d[b]);
269 let at = t[a] + (t[b] - t[a]) * s;
270 along.push(line.dot(at - Point::ORIGIN));
271 }
272 }
273 let lo = along.iter().copied().fold(f64::INFINITY, f64::min);
274 let hi = along.iter().copied().fold(f64::NEG_INFINITY, f64::max);
275 (lo <= hi).then_some((lo, hi))
276 };
277 let (pl, ph) = interval(p, dp)?;
278 let (ql, qh) = interval(q, dq)?;
279 let (lo, hi) = (pl.max(ql), ph.min(qh));
280 if lo >= hi {
281 return None;
282 }
283 let middle = 0.5 * (lo + hi);
286 let mut ends: Vec<Point> = Vec::with_capacity(2);
287 for k in 0..3 {
288 let (a, b) = (k, (k + 1) % 3);
289 if (dp[a] > 0.0) != (dp[b] > 0.0) && dp[a] != dp[b] {
290 let s = dp[a] / (dp[a] - dp[b]);
291 ends.push(p[a] + (p[b] - p[a]) * s);
292 }
293 }
294 let [from, to] = [*ends.first()?, *ends.last()?];
295 let (sf, st) = (line.dot(from - Point::ORIGIN), line.dot(to - Point::ORIGIN));
296 if (st - sf).abs() <= f64::MIN_POSITIVE {
297 return Some(from);
298 }
299 Some(from + (to - from) * ((middle - sf) / (st - sf)))
300}
301
302fn chart_apart(surface: &SurfaceGeometry, p: &Facet, q: &Facet) -> f64 {
306 let middle = |f: &Facet| {
307 let [a, b, c] = f.chart;
308 ((a.0 + b.0 + c.0) / 3.0, (a.1 + b.1 + c.1) / 3.0)
309 };
310 let ((u0, u1), (v0, v1)) = surface.domain();
311 let along = |d: f64, span: f64, periodic: bool| {
312 if !(span.is_finite() && span > 0.0) {
313 return d.abs();
314 }
315 let d = if periodic {
316 let wrapped = d.rem_euclid(span);
317 wrapped.min(span - wrapped)
318 } else {
319 d.abs()
320 };
321 d / span
322 };
323 let (pm, qm) = (middle(p), middle(q));
324 along(pm.0 - qm.0, u1 - u0, surface.is_periodic_u()).max(along(
325 pm.1 - qm.1,
326 v1 - v0,
327 surface.is_periodic_v(),
328 ))
329}
330
331fn chart_under(facet: &Facet, at: Point) -> Option<(f64, f64)> {
334 let [a, b, c] = facet.corners;
335 let (e1, e2, w) = (b - a, c - a, at - a);
336 let (d11, d12, d22) = (e1.dot(e1), e1.dot(e2), e2.dot(e2));
337 let (d1w, d2w) = (e1.dot(w), e2.dot(w));
338 let det = d11.mul_add(d22, -d12 * d12);
339 if det.abs() <= f64::MIN_POSITIVE {
340 return None;
341 }
342 let s = d22.mul_add(d1w, -d12 * d2w) / det;
343 let t = d11.mul_add(d2w, -d12 * d1w) / det;
344 let r = 1.0 - s - t;
345 let [pa, pb, pc] = facet.chart;
346 Some((
347 r.mul_add(pa.0, s.mul_add(pb.0, t * pc.0)),
348 r.mul_add(pa.1, s.mul_add(pb.1, t * pc.1)),
349 ))
350}
351
352fn placed_at(
354 sheet: &Sheet<'_>,
355 at: (f64, f64),
356 tol: Tolerances,
357) -> Option<(Point, Vector, Vector, Vector)> {
358 let (p, du, dv) = sheet.surface.point_d1_at(at.0, at.1, tol).ok()?;
359 let (du, dv) = (
360 sheet.placement.apply_vector(du),
361 sheet.placement.apply_vector(dv),
362 );
363 let n = du.cross(dv);
364 let length = n.magnitude();
365 if length <= f64::MIN_POSITIVE || !length.is_finite() {
366 return None;
367 }
368 Some((sheet.placement.apply(p), du, dv, n / length))
369}
370
371fn held(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
374 let ((u0, u1), (v0, v1)) = surface.domain();
375 let u = if surface.is_periodic_u() || u0 > u1 {
376 u
377 } else {
378 u.clamp(u0, u1)
379 };
380 let v = if surface.is_periodic_v() || v0 > v1 {
381 v
382 } else {
383 v.clamp(v0, v1)
384 };
385 (u, v)
386}
387
388fn settle<'m>(
397 model: &'m Model,
398 sheets: &[Sheet<'m>],
399 p: &Facet,
400 q: &Facet,
401 guess: Point,
402 tol: Tolerances,
403) -> OgeomResult<Option<Point>> {
404 let (a, b) = (&sheets[p.sheet], &sheets[q.sheet]);
405 let (Some(start_a), Some(start_b)) = (chart_under(p, guess), chart_under(q, guess)) else {
406 return Ok(None);
407 };
408 let (Some((_, _, _, na)), Some((_, _, _, nb))) =
409 (placed_at(a, start_a, tol), placed_at(b, start_b, tol))
410 else {
411 return Ok(None);
412 };
413 let along = na.cross(nb);
414 if along.magnitude() < CROSSING_SINE {
415 return Ok(None);
416 }
417 let along = along / along.magnitude();
418 let system = |x: &[f64; 4]| {
419 let (ua, va) = held(a.surface, x[0], x[1]);
420 let (ub, vb) = held(b.surface, x[2], x[3]);
421 let (Some((pa, au, av, _)), Some((pb, bu, bv, _))) =
422 (placed_at(a, (ua, va), tol), placed_at(b, (ub, vb), tol))
423 else {
424 return ([f64::INFINITY; 4], [[0.0; 4]; 4]);
425 };
426 let gap = pa - pb;
427 (
428 [gap.x, gap.y, gap.z, (pa - guess).dot(along)],
429 [
430 [au.x, av.x, -bu.x, -bv.x],
431 [au.y, av.y, -bu.y, -bv.y],
432 [au.z, av.z, -bu.z, -bv.z],
433 [au.dot(along), av.dot(along), 0.0, 0.0],
434 ],
435 )
436 };
437 let criteria = ogeom_math::solve::Criteria {
438 residual: tol.confusion() * 0.01,
439 step: tol.parametric(),
440 max_iterations: 40,
441 };
442 let start = [start_a.0, start_a.1, start_b.0, start_b.1];
443 let Ok((x, residual, ..)) = ogeom_math::solve::newton_system_fixed(system, start, criteria)
444 else {
445 return Ok(None);
446 };
447 if residual > tol.confusion() {
448 return Ok(None);
449 }
450 let (on_a, on_b) = (held(a.surface, x[0], x[1]), held(b.surface, x[2], x[3]));
451 let (Some((at, _, _, na)), Some((_, _, _, nb))) =
452 (placed_at(a, on_a, tol), placed_at(b, on_b, tol))
453 else {
454 return Ok(None);
455 };
456 if na.cross(nb).magnitude() < CROSSING_SINE {
457 return Ok(None);
458 }
459 let size = |f: &Facet| {
460 let [x, y, z] = f.corners;
461 x.distance(y).max(y.distance(z)).max(z.distance(x))
462 };
463 if at.distance(guess) > 2.0 * size(p).max(size(q)) {
464 return Ok(None);
465 }
466 for (sheet, chart) in [(a, on_a), (b, on_b)] {
467 let Some(trim) = trim_of(model, sheet, tol) else {
468 return Ok(None);
469 };
470 if trim.holds(model, chart, sheet.reach, tol)? != Containment::In {
471 return Ok(None);
472 }
473 }
474 Ok(Some(at))
475}
476
477fn trim_of<'s, 'm>(
480 model: &'m Model,
481 sheet: &'s Sheet<'m>,
482 tol: Tolerances,
483) -> Option<&'s FaceTrim<'m>> {
484 sheet
485 .trim
486 .get_or_init(|| {
487 let chord = Deflection {
489 chord: tol.confusion() * 1e4,
490 ..Deflection::default()
491 };
492 FaceTrim::of(model, sheet.face, chord, tol).ok()
493 })
494 .as_ref()
495}
496
497struct BoxTree {
500 nodes: Vec<Node>,
501 order: Vec<usize>,
502}
503
504struct Node {
505 bound: Aabb,
506 run: (usize, usize),
507 children: Option<(usize, usize)>,
508}
509
510impl BoxTree {
511 fn new(facets: &[Facet]) -> Self {
512 let mut order: Vec<usize> = (0..facets.len()).collect();
513 let mut tree = Self {
514 nodes: Vec::new(),
515 order: Vec::new(),
516 };
517 let len = order.len();
518 if len > 0 {
519 tree.build(facets, &mut order, 0, len);
520 }
521 tree.order = order;
522 tree
523 }
524
525 fn build(&mut self, facets: &[Facet], order: &mut [usize], start: usize, end: usize) -> usize {
526 let bound = order[start..end]
527 .iter()
528 .fold(Aabb::EMPTY, |acc, &i| acc.union(&facets[i].bound));
529 let at = self.nodes.len();
530 self.nodes.push(Node {
531 bound,
532 run: (start, end),
533 children: None,
534 });
535 if end - start <= LEAF {
536 return at;
537 }
538 let size = bound.size();
539 let axis = if size.x >= size.y && size.x >= size.z {
540 0
541 } else if size.y >= size.z {
542 1
543 } else {
544 2
545 };
546 let centre = |i: usize| -> f64 {
547 let c = facets[i].bound.centre().unwrap_or(Point::ORIGIN);
548 [c.x, c.y, c.z][axis]
549 };
550 let middle = start + (end - start) / 2;
551 order[start..end]
552 .select_nth_unstable_by(middle - start, |&p, &q| centre(p).total_cmp(¢re(q)));
553 let left = self.build(facets, order, start, middle);
554 let right = self.build(facets, order, middle, end);
555 self.nodes[at].children = Some((left, right));
556 at
557 }
558
559 fn meeting(&self, facets: &[Facet], probe: &Aabb, out: &mut Vec<usize>) {
561 out.clear();
562 if self.nodes.is_empty() {
563 return;
564 }
565 let mut stack = vec![0_usize];
566 while let Some(at) = stack.pop() {
567 let node = &self.nodes[at];
568 if !node.bound.intersects(probe) {
569 continue;
570 }
571 match node.children {
572 Some((left, right)) => {
573 stack.push(left);
574 stack.push(right);
575 }
576 None => {
577 for &i in &self.order[node.run.0..node.run.1] {
578 if facets[i].bound.intersects(probe) {
579 out.push(i);
580 }
581 }
582 }
583 }
584 }
585 out.sort_unstable();
586 }
587}