1use crate::curve::interior_knot_count;
43use crate::fit::solve_small;
44use crate::surface_surface_intersection::{SurfaceIntersectionCurve, SurfaceIntersectionOptions};
45use crate::{project_point_to_surface, NurbsSurface, Vec3};
46
47const EPSILON: f64 = crate::tolerance::FP_FLOOR;
48
49pub const EXTENDED_RANK_FLOOR: f64 = 1e-6;
55
56const REFINE_ITERATIONS: usize = 60;
58const CORRECT_ITERATIONS: usize = 30;
60const RESIDUAL_PROGRESS: f64 = 1e-3;
63const REFINE_PATIENCE: usize = 4;
64const LEVENBERG_DAMPING: f64 = 1e-10;
69const JACOBIAN_STEP: f64 = 1e-7;
71const TURN_POP: f64 = 0.35;
75const TURN_GROW: f64 = 0.03;
76const TURN_SHRINK: f64 = 0.15;
77const NORMAL_TURN_BUDGET: f64 = 0.1;
78const MARCH_REACH_EXTENTS: f64 = 4.0;
80
81const GRAZING_CLAMP: f64 = 1e-7;
105const CANDIDATE_GATE_FRACTION: f64 = 1e-2;
108const CANDIDATE_ATTEMPTS: usize = 4;
110
111#[derive(Clone, Copy)]
112struct Domain {
113 u0: f64,
114 u1: f64,
115 v0: f64,
116 v1: f64,
117 closed_u: bool,
118 closed_v: bool,
119}
120
121impl Domain {
122 fn of(surface: &NurbsSurface) -> Result<Self, String> {
123 let [u0, u1] = surface.domain_u()?;
124 let [v0, v1] = surface.domain_v()?;
125 let mut closed_u = true;
126 let mut closed_v = true;
127 for fraction in [0.17, 0.5, 0.83] {
128 let v = v0 + (v1 - v0) * fraction;
129 if surface.evaluate(u0, v)?.sub(surface.evaluate(u1, v)?).length() > 1e-6 {
130 closed_u = false;
131 }
132 let u = u0 + (u1 - u0) * fraction;
133 if surface.evaluate(u, v0)?.sub(surface.evaluate(u, v1)?).length() > 1e-6 {
134 closed_v = false;
135 }
136 }
137 Ok(Self { u0, u1, v0, v1, closed_u, closed_v })
138 }
139
140 fn spans(self) -> [f64; 2] {
141 [self.u1 - self.u0, self.v1 - self.v0]
142 }
143
144 fn fit(self, value: f64, direction_u: bool) -> (f64, bool) {
147 let (minimum, maximum, closed) = if direction_u {
148 (self.u0, self.u1, self.closed_u)
149 } else {
150 (self.v0, self.v1, self.closed_v)
151 };
152 if closed {
153 ((value - minimum).rem_euclid(maximum - minimum) + minimum, false)
154 } else {
155 let fitted = value.clamp(minimum, maximum);
156 (fitted, fitted != value)
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug)]
163struct Station {
164 ua: f64,
165 va: f64,
166 ub: f64,
167 vb: f64,
168 point: Vec3,
169}
170
171impl Station {
172 fn parameter(&self, index: usize) -> f64 {
173 match index {
174 0 => self.ua,
175 1 => self.va,
176 2 => self.ub,
177 _ => self.vb,
178 }
179 }
180
181 fn with_parameter(mut self, index: usize, value: f64) -> Self {
182 match index {
183 0 => self.ua = value,
184 1 => self.va = value,
185 2 => self.ub = value,
186 _ => self.vb = value,
187 }
188 self
189 }
190}
191
192#[derive(Clone, Copy)]
196struct Frame {
197 normal_a: Vec3,
198 normal_b: Vec3,
199 e1: Vec3,
200 e2: Vec3,
201}
202
203fn frame_at(first: &NurbsSurface, second: &NurbsSurface, station: Station) -> Option<Frame> {
204 let normal_a = first.normal(station.ua, station.va).ok()?;
205 let normal_b = second.normal(station.ub, station.vb).ok()?;
206 let e1 = normal_a.perpendicular().ok()?;
207 let e2 = normal_a.cross(e1).normalized().ok()?;
208 Some(Frame { normal_a, normal_b, e1, e2 })
209}
210
211fn contact_residual(
215 first: &NurbsSurface,
216 second: &NurbsSurface,
217 station: Station,
218 e1: Vec3,
219 e2: Vec3,
220) -> Option<[f64; 5]> {
221 let point_a = first.evaluate(station.ua, station.va).ok()?;
222 let point_b = second.evaluate(station.ub, station.vb).ok()?;
223 let normal_a = first.normal(station.ua, station.va).ok()?;
224 let normal_b = second.normal(station.ub, station.vb).ok()?;
225 let cross = normal_a.cross(normal_b);
226 let gap = point_a.sub(point_b);
227 Some([gap.x, gap.y, gap.z, cross.dot(e1), cross.dot(e2)])
228}
229
230fn gauss_newton_step<const R: usize>(
233 residual: [f64; R],
234 jacobian: [[f64; 4]; R],
235 free: [bool; 4],
236) -> Option<[f64; 4]> {
237 let mut matrix = [[0.0f64; 4]; 4];
238 let mut rhs = [0.0f64; 4];
239 for row in 0..4 {
240 if !free[row] {
241 matrix[row][row] = 1.0;
243 continue;
244 }
245 for column in 0..4 {
246 if !free[column] {
247 continue;
248 }
249 matrix[row][column] = (0..R).map(|k| jacobian[k][row] * jacobian[k][column]).sum();
250 }
251 rhs[row] = -(0..R).map(|k| jacobian[k][row] * residual[k]).sum::<f64>();
252 }
253 let diagonal = (0..4)
254 .filter(|&index| free[index])
255 .fold(0.0f64, |largest, index| largest.max(matrix[index][index]));
256 if diagonal <= 0.0 {
257 return None;
258 }
259 for index in 0..4 {
260 if free[index] {
261 matrix[index][index] += LEVENBERG_DAMPING * diagonal;
262 }
263 }
264 solve_small(matrix, rhs, 4).ok()
265}
266
267fn jacobian_of<const R: usize>(
271 station: Station,
272 spans: [f64; 4],
273 free: [bool; 4],
274 fit: impl Fn(Station) -> Station,
275 residual_at: impl Fn(Station) -> Option<[f64; R]>,
276) -> Option<[[f64; 4]; R]> {
277 let mut jacobian = [[0.0f64; 4]; R];
278 for index in 0..4 {
279 if !free[index] {
280 continue;
281 }
282 let step = spans[index] * JACOBIAN_STEP;
283 if step <= 0.0 {
284 return None;
285 }
286 let plus = residual_at(fit(station.with_parameter(index, station.parameter(index) + step)))?;
287 let minus = residual_at(fit(station.with_parameter(index, station.parameter(index) - step)))?;
288 for row in 0..R {
289 jacobian[row][index] = (plus[row] - minus[row]) / (2.0 * step);
290 }
291 }
292 Some(jacobian)
293}
294
295fn refine_contact(
302 first: &NurbsSurface,
303 second: &NurbsSurface,
304 domain_a: Domain,
305 domain_b: Domain,
306 seed: Station,
307 tolerance: f64,
308) -> Result<Option<(Station, f64)>, String> {
309 let spans = [domain_a.spans()[0], domain_a.spans()[1], domain_b.spans()[0], domain_b.spans()[1]];
310 let fit = |station: Station| Station {
311 ua: domain_a.fit(station.ua, true).0,
312 va: domain_a.fit(station.va, false).0,
313 ub: domain_b.fit(station.ub, true).0,
314 vb: domain_b.fit(station.vb, false).0,
315 point: station.point,
316 };
317 let free = [true; 4];
318 let mut current = fit(seed);
319 let mut best = current;
320 let mut best_residual = f64::INFINITY;
321 let mut best_tangency = f64::INFINITY;
322 let mut without_progress = 0usize;
323 for _ in 0..REFINE_ITERATIONS {
324 let Some(frame) = frame_at(first, second, current) else {
325 return Ok(None);
326 };
327 let residual_at = |station: Station| contact_residual(first, second, station, frame.e1, frame.e2);
328 let Some(residual) = residual_at(current) else {
329 return Ok(None);
330 };
331 let gap = (residual[0] * residual[0] + residual[1] * residual[1] + residual[2] * residual[2]).sqrt();
332 let tangency = (residual[3] * residual[3] + residual[4] * residual[4]).sqrt();
333 let worst = gap.max(tangency);
334 if worst < best_residual * (1.0 - RESIDUAL_PROGRESS) {
335 without_progress = 0;
336 } else {
337 without_progress += 1;
338 }
339 if worst < best_residual {
340 best_residual = worst;
341 best_tangency = tangency;
342 best = current;
343 }
344 if worst == 0.0 || without_progress >= REFINE_PATIENCE {
345 break;
346 }
347 let Some(jacobian) = jacobian_of::<5>(current, spans, free, fit, residual_at) else {
348 return Ok(None);
349 };
350 let Some(changes) = gauss_newton_step(residual, jacobian, free) else {
351 return Ok(None);
352 };
353 let mut scale = 1.0f64;
357 for index in 0..4 {
358 let limit = spans[index] / 4.0;
359 if changes[index].abs() > limit {
360 scale = scale.min(limit / changes[index].abs());
361 }
362 }
363 let next = fit(Station {
364 ua: current.ua + changes[0] * scale,
365 va: current.va + changes[1] * scale,
366 ub: current.ub + changes[2] * scale,
367 vb: current.vb + changes[3] * scale,
368 point: current.point,
369 });
370 let moved = (0..4)
371 .map(|index| (next.parameter(index) - current.parameter(index)).abs() / spans[index])
372 .fold(0.0f64, f64::max);
373 current = next;
374 if moved <= 1e-12 {
375 break;
376 }
377 }
378 if best_residual > tolerance {
379 return Ok(None);
380 }
381 let point_a = first.evaluate(best.ua, best.va)?;
382 let point_b = second.evaluate(best.ub, best.vb)?;
383 best.point = point_a.add(point_b).scale(0.5);
384 Ok(Some((best, best_tangency)))
385}
386
387fn second_fundamental_form(
390 surface: &NurbsSurface,
391 u: f64,
392 v: f64,
393 e1: Vec3,
394 e2: Vec3,
395 normal: Vec3,
396) -> Option<[[f64; 2]; 2]> {
397 let derivatives = surface.derivatives(u, v, 2).ok()?;
398 let s_u = derivatives[1][0];
399 let s_v = derivatives[0][1];
400 let s_uu = derivatives[2][0];
401 let s_uv = derivatives[1][1];
402 let s_vv = derivatives[0][2];
403 let a11 = s_u.dot(s_u);
404 let a12 = s_u.dot(s_v);
405 let a22 = s_v.dot(s_v);
406 let determinant = a11 * a22 - a12 * a12;
407 if determinant.abs() <= f64::MIN_POSITIVE {
408 return None;
409 }
410 let coefficients = |target: Vec3| -> (f64, f64) {
411 let b1 = s_u.dot(target);
412 let b2 = s_v.dot(target);
413 ((b1 * a22 - b2 * a12) / determinant, (a11 * b2 - a12 * b1) / determinant)
414 };
415 let along_1 = coefficients(e1);
416 let along_2 = coefficients(e2);
417 let l = s_uu.dot(normal);
418 let m = s_uv.dot(normal);
419 let n = s_vv.dot(normal);
420 let form = |x: (f64, f64), y: (f64, f64)| l * x.0 * y.0 + m * (x.0 * y.1 + x.1 * y.0) + n * x.1 * y.1;
421 Some([
422 [form(along_1, along_1), form(along_1, along_2)],
423 [form(along_2, along_1), form(along_2, along_2)],
424 ])
425}
426
427fn eigenvalues(matrix: [[f64; 2]; 2]) -> (f64, f64) {
428 crate::eigen2::eigenvalues(matrix)
429}
430
431fn spectral_norm(matrix: [[f64; 2]; 2]) -> f64 {
432 crate::eigen2::spectral_norm(matrix)
433}
434
435fn eigenvector(matrix: [[f64; 2]; 2], lambda: f64) -> [f64; 2] {
437 crate::eigen2::eigenvector(matrix, lambda)
438}
439
440#[derive(Clone, Copy, Debug)]
443struct HeightDifference {
444 rank_ratio: f64,
445 curvature_scale: f64,
446 saddle: bool,
447 null_direction: Vec3,
450 normal_curvature_along: f64,
453}
454
455fn height_difference(
456 first: &NurbsSurface,
457 second: &NurbsSurface,
458 station: Station,
459 frame: Frame,
460) -> Option<HeightDifference> {
461 let orientation = if frame.normal_a.dot(frame.normal_b) < 0.0 { -1.0 } else { 1.0 };
464 let form_a = second_fundamental_form(first, station.ua, station.va, frame.e1, frame.e2, frame.normal_a)?;
465 let form_b = second_fundamental_form(
466 second,
467 station.ub,
468 station.vb,
469 frame.e1,
470 frame.e2,
471 frame.normal_b.scale(orientation),
472 )?;
473 let mut difference = [[0.0f64; 2]; 2];
474 for row in 0..2 {
475 for column in 0..2 {
476 difference[row][column] = form_a[row][column] - form_b[row][column];
477 }
478 }
479 let curvature_scale = spectral_norm(form_a).max(spectral_norm(form_b));
480 if curvature_scale <= 0.0 {
481 return None;
483 }
484 let (lambda_first, lambda_second) = eigenvalues(difference);
485 let (smallest, largest) = if lambda_first.abs() <= lambda_second.abs() {
486 (lambda_first, lambda_second)
487 } else {
488 (lambda_second, lambda_first)
489 };
490 let _ = largest;
491 let [x, y] = eigenvector(difference, smallest);
492 let null_direction = frame.e1.scale(x).add(frame.e2.scale(y)).normalized().ok()?;
493 let along = |form: [[f64; 2]; 2]| form[0][0] * x * x + 2.0 * form[0][1] * x * y + form[1][1] * y * y;
494 Some(HeightDifference {
495 rank_ratio: smallest.abs() / curvature_scale,
496 curvature_scale,
497 saddle: lambda_first * lambda_second < 0.0,
498 null_direction,
499 normal_curvature_along: along(form_a).abs().max(along(form_b).abs()),
500 })
501}
502
503#[derive(Clone, Copy, Debug)]
506pub struct TangentContact {
507 pub point: Vec3,
509 pub params_a: [f64; 2],
511 pub params_b: [f64; 2],
512 pub rank_ratio: f64,
515 pub curvature_scale: f64,
517 pub tangency_residual: f64,
519 pub saddle: bool,
522 pub direction: Option<Vec3>,
525}
526
527impl TangentContact {
528 pub fn is_extended(&self) -> bool {
530 self.direction.is_some()
531 }
532}
533
534fn classify_station(
535 first: &NurbsSurface,
536 second: &NurbsSurface,
537 station: Station,
538 tangency_residual: f64,
539) -> Option<TangentContact> {
540 let frame = frame_at(first, second, station)?;
541 let difference = height_difference(first, second, station, frame)?;
542 let extended = difference.rank_ratio < EXTENDED_RANK_FLOOR;
543 Some(TangentContact {
544 point: station.point,
545 params_a: [station.ua, station.va],
546 params_b: [station.ub, station.vb],
547 rank_ratio: difference.rank_ratio,
548 curvature_scale: difference.curvature_scale,
549 tangency_residual,
550 saddle: difference.saddle,
551 direction: extended.then_some(difference.null_direction),
552 })
553}
554
555pub fn classify_tangent_contact_at(
560 first: &NurbsSurface,
561 second: &NurbsSurface,
562 witness: Vec3,
563 tolerance: f64,
564) -> Result<Option<TangentContact>, String> {
565 let domain_a = Domain::of(first)?;
566 let domain_b = Domain::of(second)?;
567 let on_a = project_point_to_surface(first, witness)?;
568 let on_b = project_point_to_surface(second, witness)?;
569 let seed = Station { ua: on_a.u, va: on_a.v, ub: on_b.u, vb: on_b.v, point: witness };
570 let Some((station, tangency)) = refine_contact(first, second, domain_a, domain_b, seed, tolerance)? else {
571 return Ok(None);
572 };
573 Ok(classify_station(first, second, station, tangency))
574}
575
576fn extent(surface: &NurbsSurface) -> Result<f64, String> {
577 let mut minimum = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
578 let mut maximum = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
579 for point in surface.control_points.iter().flatten() {
580 let point = point.point()?;
581 minimum = Vec3::new(minimum.x.min(point.x), minimum.y.min(point.y), minimum.z.min(point.z));
582 maximum = Vec3::new(maximum.x.max(point.x), maximum.y.max(point.y), maximum.z.max(point.z));
583 }
584 Ok(maximum.sub(minimum).length())
585}
586
587fn grid_count(surface: &NurbsSurface, direction_u: bool) -> usize {
588 let (knots, degree) = if direction_u {
589 (&surface.knots_u, surface.degree_u)
590 } else {
591 (&surface.knots_v, surface.degree_v)
592 };
593 (8.0_f64)
594 .max(((interior_knot_count(knots, degree) + 1) * (degree + 1)) as f64)
595 .ceil()
596 .min(24.0) as usize
597}
598
599pub fn find_tangent_contact(
608 first: &NurbsSurface,
609 second: &NurbsSurface,
610 tolerance: f64,
611) -> Result<Option<TangentContact>, String> {
612 let diagonal = extent(first)?.min(extent(second)?);
613 let gate = CANDIDATE_GATE_FRACTION * diagonal + tolerance * 100.0;
614 for swapped in [false, true] {
615 let (grid_surface, other) = if swapped { (second, first) } else { (first, second) };
616 let domain_grid = Domain::of(grid_surface)?;
617 let nu = grid_count(grid_surface, true);
618 let nv = grid_count(grid_surface, false);
619 let mut candidates: Vec<(f64, Station)> = Vec::new();
620 for i in 0..=nu {
621 for j in 0..=nv {
622 let u = domain_grid.u0 + (domain_grid.u1 - domain_grid.u0) * i as f64 / nu as f64;
623 let v = domain_grid.v0 + (domain_grid.v1 - domain_grid.v0) * j as f64 / nv as f64;
624 let point = grid_surface.evaluate(u, v)?;
625 let projection = project_point_to_surface(other, point)?;
626 if projection.distance > gate {
627 continue;
628 }
629 let (Ok(normal_grid), Ok(normal_other)) =
630 (grid_surface.normal(u, v), other.normal(projection.u, projection.v))
631 else {
632 continue;
633 };
634 let score = projection.distance / diagonal.max(EPSILON) + normal_grid.cross(normal_other).length();
635 let station = if swapped {
636 Station { ua: projection.u, va: projection.v, ub: u, vb: v, point }
637 } else {
638 Station { ua: u, va: v, ub: projection.u, vb: projection.v, point }
639 };
640 candidates.push((score, station));
641 }
642 }
643 candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
644 let domain_a = Domain::of(first)?;
645 let domain_b = Domain::of(second)?;
646 for (_, seed) in candidates.into_iter().take(CANDIDATE_ATTEMPTS) {
647 if let Some((station, tangency)) = refine_contact(first, second, domain_a, domain_b, seed, tolerance)? {
648 if let Some(contact) = classify_station(first, second, station, tangency) {
649 return Ok(Some(contact));
650 }
651 }
652 }
653 }
654 Ok(None)
655}
656
657#[allow(clippy::too_many_arguments)]
663fn correct(
664 first: &NurbsSurface,
665 second: &NurbsSurface,
666 domain_a: Domain,
667 domain_b: Domain,
668 from: Station,
669 predicted: Vec3,
670 direction: Vec3,
671 tolerance: f64,
672 on_edge: [bool; 4],
673) -> Result<Option<(Station, bool)>, String> {
674 let spans = [domain_a.spans()[0], domain_a.spans()[1], domain_b.spans()[0], domain_b.spans()[1]];
675 let fit = |station: Station| Station {
676 ua: domain_a.fit(station.ua, true).0,
677 va: domain_a.fit(station.va, false).0,
678 ub: domain_b.fit(station.ub, true).0,
679 vb: domain_b.fit(station.vb, false).0,
680 point: station.point,
681 };
682 let bar = tolerance * 0.01 + EPSILON;
683 let mut current = from;
684 let mut free = [true; 4];
685 let mut boundary = false;
686 for _ in 0..CORRECT_ITERATIONS {
687 let Some(frame) = frame_at(first, second, current) else {
688 return Ok(None);
689 };
690 let residual_at = |station: Station| -> Option<[f64; 6]> {
691 let contact = contact_residual(first, second, station, frame.e1, frame.e2)?;
692 let point_a = first.evaluate(station.ua, station.va).ok()?;
693 let plane = if boundary { 0.0 } else { point_a.sub(predicted).dot(direction) };
694 Some([contact[0], contact[1], contact[2], contact[3], contact[4], plane])
695 };
696 let Some(residual) = residual_at(current) else {
697 return Ok(None);
698 };
699 let gap = (residual[0] * residual[0] + residual[1] * residual[1] + residual[2] * residual[2]).sqrt();
700 let tangency = (residual[3] * residual[3] + residual[4] * residual[4]).sqrt();
701 if gap <= bar && tangency <= bar && residual[5].abs() <= bar {
702 let point_a = first.evaluate(current.ua, current.va)?;
703 let point_b = second.evaluate(current.ub, current.vb)?;
704 current.point = point_a.add(point_b).scale(0.5);
705 return Ok(Some((current, boundary)));
706 }
707 let Some(jacobian) = jacobian_of::<6>(current, spans, free, fit, residual_at) else {
708 return Ok(None);
709 };
710 let Some(changes) = gauss_newton_step(residual, jacobian, free) else {
711 return Ok(None);
712 };
713 let mut scale = 1.0f64;
714 for index in 0..4 {
715 let limit = spans[index] / 4.0;
716 if changes[index].abs() > limit {
717 scale = scale.min(limit / changes[index].abs());
718 }
719 }
720 let mut next = current;
721 let mut moved = 0.0f64;
722 for index in 0..4 {
723 if !free[index] {
724 continue;
725 }
726 let raw = current.parameter(index) + changes[index] * scale;
727 let (fitted, clamped) = match index {
728 0 => domain_a.fit(raw, true),
729 1 => domain_a.fit(raw, false),
730 2 => domain_b.fit(raw, true),
731 _ => domain_b.fit(raw, false),
732 };
733 if clamped {
734 free[index] = false;
739 if !on_edge[index] || (raw - fitted).abs() > GRAZING_CLAMP * spans[index] {
740 boundary = true;
741 }
742 }
743 moved = moved.max((fitted - current.parameter(index)).abs() / spans[index]);
744 next = next.with_parameter(index, fitted);
745 }
746 current = next;
747 if !free.iter().any(|&f| f) {
748 return Ok(None);
749 }
750 if moved <= 1e-14 {
751 return Ok(None);
752 }
753 }
754 Ok(None)
755}
756
757struct Trace {
758 stations: Vec<Station>,
759 tangents: Vec<Vec3>,
760 closed: bool,
761}
762
763fn vector_angle(a: Vec3, b: Vec3) -> f64 {
764 (a.dot(b) / (a.length() * b.length())).clamp(-1.0, 1.0).acos()
765}
766
767fn normal_turn_ceiling(difference: &HeightDifference) -> Option<f64> {
771 (difference.normal_curvature_along > 1e-12).then(|| NORMAL_TURN_BUDGET / difference.normal_curvature_along)
772}
773
774#[allow(clippy::too_many_arguments)]
775fn trace(
776 first: &NurbsSurface,
777 second: &NurbsSurface,
778 domain_a: Domain,
779 domain_b: Domain,
780 start: Station,
781 start_direction: Vec3,
782 tolerance: f64,
783 initial_step: f64,
784 minimum_step: f64,
785 maximum_step: f64,
786 maximum_steps: usize,
787) -> Result<Trace, String> {
788 let mut stations = vec![start];
789 let mut tangents = vec![start_direction];
790 let mut current = start;
791 let mut direction = start_direction;
792 let mut step_size = initial_step.min(maximum_step);
793 let on_edge = {
798 let spans = [domain_a.spans()[0], domain_a.spans()[1], domain_b.spans()[0], domain_b.spans()[1]];
799 let ends = [
800 (domain_a.u0, domain_a.u1, domain_a.closed_u, domain_a.closed_v),
801 (domain_a.v0, domain_a.v1, domain_a.closed_v, domain_a.closed_u),
802 (domain_b.u0, domain_b.u1, domain_b.closed_u, domain_b.closed_v),
803 (domain_b.v0, domain_b.v1, domain_b.closed_v, domain_b.closed_u),
804 ];
805 let mut flags = [false; 4];
806 for index in 0..4 {
807 let (low, high, closed, edge_is_ring) = ends[index];
808 let value = start.parameter(index);
809 flags[index] = !closed
810 && !edge_is_ring
811 && ((value - low).abs() <= GRAZING_CLAMP * spans[index] || (value - high).abs() <= GRAZING_CLAMP * spans[index]);
812 }
813 flags
814 };
815 if let Some(frame) = frame_at(first, second, start) {
816 if let Some(difference) = height_difference(first, second, start, frame) {
817 if let Some(ceiling) = normal_turn_ceiling(&difference) {
818 step_size = step_size.min(ceiling).clamp(minimum_step, maximum_step);
819 }
820 }
821 }
822 let debug = std::env::var_os("BREP_DEBUG_SSI_TRACE").is_some();
823 let mut terminated = false;
824 for _ in 0..maximum_steps {
825 let mut accepted = None;
826 while step_size >= minimum_step {
827 let predicted = current.point.add(direction.scale(step_size));
828 if let Some((corrected, boundary)) =
829 correct(first, second, domain_a, domain_b, current, predicted, direction, tolerance, on_edge)?
830 {
831 if corrected.point.sub(current.point).length() <= 2.5 * step_size {
832 accepted = Some((corrected, boundary));
833 break;
834 }
835 }
836 step_size /= 2.0;
837 }
838 let Some((candidate, boundary)) = accepted else {
839 terminated = true;
840 break;
841 };
842 let next = frame_at(first, second, candidate)
843 .and_then(|frame| height_difference(first, second, candidate, frame));
844 let next_direction = match next {
845 Some(difference) if difference.rank_ratio < EXTENDED_RANK_FLOOR => {
846 let null = difference.null_direction;
847 Some((if null.dot(direction) < 0.0 { null.scale(-1.0) } else { null }, difference))
848 }
849 _ => None,
850 };
851 if debug {
852 let at = candidate.point;
853 eprintln!(
854 "TANGENT-TRACE step {step_size:.4e} jump {:.4e} boundary {boundary} extended {} p ({:.6},{:.6},{:.6})",
855 at.sub(current.point).length(),
856 next_direction.is_some(),
857 at.x,
858 at.y,
859 at.z
860 );
861 }
862 let Some((next_direction, difference)) = next_direction else {
863 stations.push(candidate);
864 tangents.push(direction);
865 terminated = true;
866 break;
867 };
868 if boundary {
869 stations.push(candidate);
870 tangents.push(next_direction);
871 terminated = true;
872 break;
873 }
874 let angle = vector_angle(direction, next_direction);
875 if angle > TURN_POP && step_size > minimum_step * 1.01 {
876 step_size = minimum_step.max(step_size / 2.0);
877 continue;
878 }
879 stations.push(candidate);
880 tangents.push(next_direction);
881 let reactive = if angle < TURN_GROW {
882 step_size * 1.5
883 } else if angle > TURN_SHRINK {
884 step_size * 0.6
885 } else {
886 step_size
887 };
888 step_size = match normal_turn_ceiling(&difference) {
889 Some(ceiling) => reactive.min(ceiling),
890 None => reactive,
891 }
892 .clamp(minimum_step, maximum_step);
893 if stations.len() > 4 && candidate.point.sub(start.point).length() <= step_size * 1.2 {
894 let to_start = start.point.sub(candidate.point);
895 if to_start.length() <= EPSILON || to_start.dot(next_direction) >= 0.0 {
896 stations.push(start);
897 tangents.push(tangents[0]);
898 return Ok(Trace { stations, tangents, closed: true });
899 }
900 }
901 current = candidate;
902 direction = next_direction;
903 }
904 if !terminated {
905 return Err(format!(
906 "tangent-curve trace exhausted {maximum_steps} steps ({} stations, step {step_size:.3e}) \
907 without reaching a boundary or closure",
908 stations.len()
909 ));
910 }
911 Ok(Trace { stations, tangents, closed: false })
912}
913
914pub fn trace_tangent_curve(
922 first: &NurbsSurface,
923 second: &NurbsSurface,
924 contact: &TangentContact,
925 options: &SurfaceIntersectionOptions,
926) -> Result<Option<SurfaceIntersectionCurve>, String> {
927 let Some(direction) = contact.direction else {
928 return Ok(None);
929 };
930 let domain_a = Domain::of(first)?;
931 let domain_b = Domain::of(second)?;
932 let diagonal = extent(first)?.min(extent(second)?);
933 let initial_step = diagonal / 100.0;
934 let minimum_step = (options.tolerance * 100.0).max(diagonal * 1e-6);
935 let maximum_step = (diagonal / 15.0).min(options.maximum_step.unwrap_or(f64::INFINITY));
936 let needed = (MARCH_REACH_EXTENTS * diagonal / maximum_step).ceil();
937 let maximum_steps = if needed.is_finite() && needed > options.maximum_steps as f64 {
938 needed as usize
939 } else {
940 options.maximum_steps
941 };
942 let start = Station {
943 ua: contact.params_a[0],
944 va: contact.params_a[1],
945 ub: contact.params_b[0],
946 vb: contact.params_b[1],
947 point: contact.point,
948 };
949 let forward = trace(
950 first, second, domain_a, domain_b, start, direction, options.tolerance,
951 initial_step, minimum_step, maximum_step, maximum_steps,
952 )?;
953 let (stations, tangents, closed) = if forward.closed {
954 (forward.stations, forward.tangents, true)
955 } else {
956 let backward = trace(
957 first, second, domain_a, domain_b, start, direction.scale(-1.0), options.tolerance,
958 initial_step, minimum_step, maximum_step, maximum_steps,
959 )?;
960 let mut stations: Vec<Station> = backward.stations.into_iter().skip(1).rev().collect();
961 let mut tangents: Vec<Vec3> = backward
962 .tangents
963 .into_iter()
964 .skip(1)
965 .rev()
966 .map(|tangent| tangent.scale(-1.0))
967 .collect();
968 stations.extend(forward.stations);
969 tangents.extend(forward.tangents);
970 (stations, tangents, false)
971 };
972 let mut kept_stations: Vec<Station> = Vec::with_capacity(stations.len());
976 let mut kept_tangents: Vec<Vec3> = Vec::with_capacity(tangents.len());
977 for (station, tangent) in stations.into_iter().zip(tangents) {
978 if let Some(last) = kept_stations.last() {
979 if station.point.sub(last.point).length() <= options.tolerance {
980 continue;
981 }
982 }
983 kept_stations.push(station);
984 kept_tangents.push(tangent);
985 }
986 let (stations, tangents) = (kept_stations, kept_tangents);
987 if stations.len() < 2 {
988 return Ok(None);
989 }
990 Ok(Some(SurfaceIntersectionCurve {
991 points: stations.iter().map(|station| station.point).collect(),
992 params_a: stations.iter().map(|station| [station.ua, station.va]).collect(),
993 params_b: stations.iter().map(|station| [station.ub, station.vb]).collect(),
994 closed,
995 tangents: Some(tangents),
996 span_deviation: None,
997 tangent: true,
998 }))
999}
1000