1#![allow(
16 clippy::many_single_char_names,
17 clippy::similar_names,
18 clippy::suboptimal_flops,
19 clippy::cast_precision_loss
20)]
21
22use crate::MathError;
23use crate::aabb::Aabb3;
24use crate::bvh::Bvh;
25use crate::nurbs::curve::NurbsCurve;
26use crate::nurbs::surface::NurbsSurface;
27use crate::vec::{Point3, Vec3};
28
29#[derive(Debug, Clone)]
34pub struct SelfIntersectionCurve {
35 pub curve: NurbsCurve,
37 pub params_a: Vec<(f64, f64)>,
39 pub params_b: Vec<(f64, f64)>,
41}
42
43struct SampleTriangle {
45 aabb: Aabb3,
47 indices: [(usize, usize); 3],
49 uv_mid: (f64, f64),
51}
52
53pub fn detect_self_intersection(
68 surface: &NurbsSurface,
69 grid_res: usize,
70 tolerance: f64,
71) -> Result<Vec<SelfIntersectionCurve>, MathError> {
72 let n = grid_res.max(5);
73 let (u_min, u_max) = surface.domain_u();
74 let (v_min, v_max) = surface.domain_v();
75
76 let mut grid_pts: Vec<Vec<Point3>> = Vec::with_capacity(n + 1);
78 let mut grid_uv: Vec<Vec<(f64, f64)>> = Vec::with_capacity(n + 1);
79
80 for i in 0..=n {
81 let u = u_min + (u_max - u_min) * (i as f64 / n as f64);
82 let mut row_pts = Vec::with_capacity(n + 1);
83 let mut row_uv = Vec::with_capacity(n + 1);
84 for j in 0..=n {
85 let v = v_min + (v_max - v_min) * (j as f64 / n as f64);
86 row_pts.push(surface.evaluate(u, v));
87 row_uv.push((u, v));
88 }
89 grid_pts.push(row_pts);
90 grid_uv.push(row_uv);
91 }
92
93 let mut triangles: Vec<SampleTriangle> = Vec::with_capacity(2 * n * n);
95
96 for i in 0..n {
97 for j in 0..n {
98 let t1_pts = [grid_pts[i][j], grid_pts[i + 1][j], grid_pts[i + 1][j + 1]];
100 let t1_aabb = Aabb3::from_points(t1_pts.iter().copied());
101 let t1_uv_mid = (
102 (grid_uv[i][j].0 + grid_uv[i + 1][j].0 + grid_uv[i + 1][j + 1].0) / 3.0,
103 (grid_uv[i][j].1 + grid_uv[i + 1][j].1 + grid_uv[i + 1][j + 1].1) / 3.0,
104 );
105 triangles.push(SampleTriangle {
106 aabb: t1_aabb,
107 indices: [(i, j), (i + 1, j), (i + 1, j + 1)],
108 uv_mid: t1_uv_mid,
109 });
110
111 let t2_pts = [grid_pts[i][j], grid_pts[i + 1][j + 1], grid_pts[i][j + 1]];
113 let t2_aabb = Aabb3::from_points(t2_pts.iter().copied());
114 let t2_uv_mid = (
115 (grid_uv[i][j].0 + grid_uv[i + 1][j + 1].0 + grid_uv[i][j + 1].0) / 3.0,
116 (grid_uv[i][j].1 + grid_uv[i + 1][j + 1].1 + grid_uv[i][j + 1].1) / 3.0,
117 );
118 triangles.push(SampleTriangle {
119 aabb: t2_aabb,
120 indices: [(i, j), (i + 1, j + 1), (i, j + 1)],
121 uv_mid: t2_uv_mid,
122 });
123 }
124 }
125
126 let bvh_entries: Vec<(usize, Aabb3)> = triangles
128 .iter()
129 .enumerate()
130 .map(|(i, t)| (i, t.aabb))
131 .collect();
132 let bvh = Bvh::build(&bvh_entries);
133
134 let adjacency_threshold = 2; let mut candidate_pairs: Vec<((f64, f64), (f64, f64))> = Vec::new();
137
138 for (i, tri_a) in triangles.iter().enumerate() {
139 let overlaps = bvh.query_overlap(&tri_a.aabb);
140 for &j in &overlaps {
141 if j <= i {
142 continue; }
144 let tri_b = &triangles[j];
145
146 if are_adjacent(&tri_a.indices, &tri_b.indices, adjacency_threshold) {
148 continue;
149 }
150
151 candidate_pairs.push((tri_a.uv_mid, tri_b.uv_mid));
152 }
153 }
154
155 if candidate_pairs.is_empty() {
156 return Ok(Vec::new());
157 }
158
159 #[allow(clippy::type_complexity)]
161 let mut self_int_points: Vec<((f64, f64), (f64, f64), Point3)> = Vec::new();
162
163 for &((u1, v1), (u2, v2)) in &candidate_pairs {
164 if let Some((pt, pa, pb)) =
165 refine_self_intersection_point(surface, u1, v1, u2, v2, tolerance)
166 {
167 let param_dist = ((pa.0 - pb.0).powi(2) + (pa.1 - pb.1).powi(2)).sqrt();
169 if param_dist > tolerance * 10.0 {
170 let is_dup = self_int_points.iter().any(|(existing, _, _)| {
172 let dist = ((existing.0 - pa.0).powi(2) + (existing.1 - pa.1).powi(2)).sqrt();
173 dist < tolerance * 100.0
174 });
175 if !is_dup {
176 self_int_points.push((pa, pb, pt));
177 }
178 }
179 }
180 }
181
182 if self_int_points.is_empty() {
183 return Ok(Vec::new());
184 }
185
186 let points_3d: Vec<Point3> = self_int_points.iter().map(|(_, _, p)| *p).collect();
188 let params_a: Vec<(f64, f64)> = self_int_points.iter().map(|(a, _, _)| *a).collect();
189 let params_b: Vec<(f64, f64)> = self_int_points.iter().map(|(_, b, _)| *b).collect();
190
191 if points_3d.len() < 2 {
193 let degree = 1;
195 let curve = crate::nurbs::interpolate(&[points_3d[0], points_3d[0]], degree)?;
196 return Ok(vec![SelfIntersectionCurve {
197 curve,
198 params_a,
199 params_b,
200 }]);
201 }
202
203 let degree = 3.min(points_3d.len() - 1);
204 let curve = if points_3d.len() > 50 {
205 let num_cps = (points_3d.len() / 3).max(degree + 1).min(points_3d.len());
206 crate::nurbs::fitting::approximate_lspia(&points_3d, degree, num_cps, 1e-6, 100)?
207 } else {
208 crate::nurbs::interpolate(&points_3d, degree)?
209 };
210
211 Ok(vec![SelfIntersectionCurve {
212 curve,
213 params_a,
214 params_b,
215 }])
216}
217
218fn are_adjacent(a: &[(usize, usize); 3], b: &[(usize, usize); 3], threshold: usize) -> bool {
220 for &(ai, aj) in a {
221 for &(bi, bj) in b {
222 let di = ai.abs_diff(bi);
223 let dj = aj.abs_diff(bj);
224 if di <= threshold && dj <= threshold {
225 return true;
226 }
227 }
228 }
229 false
230}
231
232#[allow(clippy::type_complexity)]
238fn refine_self_intersection_point(
239 surface: &NurbsSurface,
240 u1_guess: f64,
241 v1_guess: f64,
242 u2_guess: f64,
243 v2_guess: f64,
244 tolerance: f64,
245) -> Option<(Point3, (f64, f64), (f64, f64))> {
246 let (u_min, u_max) = surface.domain_u();
247 let (v_min, v_max) = surface.domain_v();
248 let eps = (u_max - u_min + v_max - v_min) * 0.01;
249
250 let mut u1 = u1_guess;
251 let mut v1 = v1_guess;
252 let mut u2 = u2_guess;
253 let mut v2 = v2_guess;
254
255 for _ in 0..50 {
256 let p1 = surface.evaluate(u1, v1);
257 let p2 = surface.evaluate(u2, v2);
258 let residual = p1 - p2;
259
260 if residual.length() < tolerance {
261 let param_dist = ((u1 - u2).powi(2) + (v1 - v2).powi(2)).sqrt();
263 if param_dist > eps {
264 return Some((p1, (u1, v1), (u2, v2)));
265 }
266 return None; }
268
269 let (du2, dv2) = surface_newton_step_self(surface, u2, v2, p1);
271 u2 = (u2 + du2).clamp(u_min, u_max);
272 v2 = (v2 + dv2).clamp(v_min, v_max);
273
274 let p2_new = surface.evaluate(u2, v2);
276 let (du1, dv1) = surface_newton_step_self(surface, u1, v1, p2_new);
277 u1 = (u1 + du1).clamp(u_min, u_max);
278 v1 = (v1 + dv1).clamp(v_min, v_max);
279
280 let param_dist = ((u1 - u2).powi(2) + (v1 - v2).powi(2)).sqrt();
282 if param_dist < eps * 0.5 {
283 return None; }
285 }
286
287 let p1 = surface.evaluate(u1, v1);
289 let p2 = surface.evaluate(u2, v2);
290 if (p1 - p2).length() < tolerance * 100.0 {
291 let param_dist = ((u1 - u2).powi(2) + (v1 - v2).powi(2)).sqrt();
292 if param_dist > eps {
293 return Some((p1, (u1, v1), (u2, v2)));
294 }
295 }
296
297 None
298}
299
300fn surface_newton_step_self(surface: &NurbsSurface, u: f64, v: f64, target: Point3) -> (f64, f64) {
302 let pt = surface.evaluate(u, v);
303 let r = target - pt;
304 let r_vec = Vec3::new(r.x(), r.y(), r.z());
305
306 let derivs = surface.derivatives(u, v, 1);
307 let su = derivs[1][0];
308 let sv = derivs[0][1];
309
310 let a11 = su.dot(su);
311 let a12 = su.dot(sv);
312 let a22 = sv.dot(sv);
313 let b1 = su.dot(r_vec);
314 let b2 = sv.dot(r_vec);
315
316 let det = a11.mul_add(a22, -(a12 * a12));
317 if det.abs() < 1e-20 {
318 return (0.0, 0.0);
319 }
320
321 let du = b1.mul_add(a22, -(b2 * a12)) / det;
322 let dv = a11.mul_add(b2, -(a12 * b1)) / det;
323
324 (du, dv)
325}
326
327#[cfg(test)]
328mod tests {
329 #![allow(clippy::unwrap_used, clippy::expect_used)]
330
331 use super::*;
332 use crate::nurbs::surface::NurbsSurface;
333 use crate::vec::Point3;
334
335 fn flat_surface() -> NurbsSurface {
337 NurbsSurface::new(
338 1,
339 1,
340 vec![0.0, 0.0, 1.0, 1.0],
341 vec![0.0, 0.0, 1.0, 1.0],
342 vec![
343 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
344 vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
345 ],
346 vec![vec![1.0, 1.0], vec![1.0, 1.0]],
347 )
348 .unwrap()
349 }
350
351 fn folded_surface() -> NurbsSurface {
354 NurbsSurface::new(
355 2,
356 2,
357 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
358 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
359 vec![
360 vec![
361 Point3::new(0.0, 0.0, 0.0),
362 Point3::new(0.5, 0.0, 0.0),
363 Point3::new(1.0, 0.0, 0.0),
364 ],
365 vec![
366 Point3::new(0.0, 0.5, 1.0),
368 Point3::new(0.5, 0.5, -1.0),
369 Point3::new(1.0, 0.5, 1.0),
370 ],
371 vec![
372 Point3::new(0.0, 1.0, 0.0),
373 Point3::new(0.5, 1.0, 0.0),
374 Point3::new(1.0, 1.0, 0.0),
375 ],
376 ],
377 vec![vec![1.0; 3]; 3],
378 )
379 .unwrap()
380 }
381
382 #[test]
383 fn flat_surface_clean() {
384 let surf = flat_surface();
385 let result = detect_self_intersection(&surf, 10, 1e-6).unwrap();
386 assert!(
387 result.is_empty(),
388 "flat surface should have no self-intersection"
389 );
390 }
391
392 #[test]
393 fn folded_surface_detected() {
394 let surf = folded_surface();
395 let result = detect_self_intersection(&surf, 15, 1e-4).unwrap();
397
398 for si in &result {
402 assert!(
403 si.params_a.len() == si.params_b.len(),
404 "param lists should have same length"
405 );
406 for (pa, pb) in si.params_a.iter().zip(si.params_b.iter()) {
407 let dist = ((pa.0 - pb.0).powi(2) + (pa.1 - pb.1).powi(2)).sqrt();
408 assert!(
409 dist > 1e-6,
410 "self-intersection params should be distinct: {pa:?} vs {pb:?}"
411 );
412 }
413 }
414 }
415}