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