1use crate::curve::interior_knot_count;
2use crate::fit::solve_small;
3use crate::{project_point_to_surface, KnotVector, NurbsSurface, Vec3};
4use serde::{Deserialize, Serialize};
5
6const EPSILON: f64 = 1e-12;
7const LINEAR_TOLERANCE: f64 = 1e-7;
8
9const TRANSVERSE_START_CROSS: f64 = 1e-2;
24
25pub(crate) const TRANSVERSE_SEED_CROSS: f64 = TRANSVERSE_START_CROSS;
30
31const MARCH_REACH_EXTENTS: f64 = 4.0;
59
60fn step_budget(maximum_steps: usize, maximum_step: f64, diagonal: f64) -> usize {
63 if !(maximum_step > 0.0) || !diagonal.is_finite() {
64 return maximum_steps;
65 }
66 let needed = (MARCH_REACH_EXTENTS * diagonal / maximum_step).ceil();
67 if !needed.is_finite() || needed <= maximum_steps as f64 {
68 return maximum_steps;
69 }
70 needed as usize
71}
72
73#[derive(Clone, Copy)]
74struct Bounds {
75 minimum: Vec3,
76 maximum: Vec3,
77}
78
79impl Bounds {
80 fn from_surface(surface: &NurbsSurface) -> Result<Self, String> {
81 let mut minimum = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
82 let mut maximum = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
83 for point in surface.control_points.iter().flatten() {
84 let point = point.point()?;
85 minimum.x = minimum.x.min(point.x);
86 minimum.y = minimum.y.min(point.y);
87 minimum.z = minimum.z.min(point.z);
88 maximum.x = maximum.x.max(point.x);
89 maximum.y = maximum.y.max(point.y);
90 maximum.z = maximum.z.max(point.z);
91 }
92 Ok(Self { minimum, maximum })
93 }
94
95 fn diagonal(self) -> f64 {
96 self.maximum.sub(self.minimum).length()
97 }
98
99 fn expanded(self, amount: f64) -> Self {
100 let delta = Vec3::new(amount, amount, amount);
101 Self {
102 minimum: self.minimum.sub(delta),
103 maximum: self.maximum.add(delta),
104 }
105 }
106
107 fn contains(self, point: Vec3) -> bool {
108 point.x >= self.minimum.x
109 && point.x <= self.maximum.x
110 && point.y >= self.minimum.y
111 && point.y <= self.maximum.y
112 && point.z >= self.minimum.z
113 && point.z <= self.maximum.z
114 }
115
116 fn intersects(self, other: Self, tolerance: f64) -> bool {
117 self.expanded(tolerance).minimum.x <= other.maximum.x
118 && self.maximum.x + tolerance >= other.minimum.x
119 && self.minimum.y - tolerance <= other.maximum.y
120 && self.maximum.y + tolerance >= other.minimum.y
121 && self.minimum.z - tolerance <= other.maximum.z
122 && self.maximum.z + tolerance >= other.minimum.z
123 }
124}
125
126#[derive(Clone, Copy)]
127struct SurfaceInfo<'a> {
128 surface: &'a NurbsSurface,
129 u0: f64,
130 u1: f64,
131 v0: f64,
132 v1: f64,
133 closed_u: bool,
134 closed_v: bool,
135}
136
137fn surface_info(surface: &NurbsSurface) -> Result<SurfaceInfo<'_>, String> {
138 let ku = KnotVector::new(surface.knots_u.clone(), surface.degree_u)?;
139 let kv = KnotVector::new(surface.knots_v.clone(), surface.degree_v)?;
140 let [u0, u1] = ku.domain();
141 let [v0, v1] = kv.domain();
142 let mut closed_u = true;
143 let mut closed_v = true;
144 for fraction in [0.17, 0.5, 0.83] {
145 let v = v0 + (v1 - v0) * fraction;
146 if surface
147 .evaluate(u0, v)?
148 .sub(surface.evaluate(u1, v)?)
149 .length()
150 > LINEAR_TOLERANCE * 10.0
151 {
152 closed_u = false;
153 }
154 let u = u0 + (u1 - u0) * fraction;
155 if surface
156 .evaluate(u, v0)?
157 .sub(surface.evaluate(u, v1)?)
158 .length()
159 > LINEAR_TOLERANCE * 10.0
160 {
161 closed_v = false;
162 }
163 }
164 Ok(SurfaceInfo {
165 surface,
166 u0,
167 u1,
168 v0,
169 v1,
170 closed_u,
171 closed_v,
172 })
173}
174
175fn fit(value: f64, minimum: f64, maximum: f64, closed: bool) -> f64 {
176 if closed {
177 (value - minimum).rem_euclid(maximum - minimum) + minimum
178 } else {
179 value.clamp(minimum, maximum)
180 }
181}
182
183
184#[derive(Clone, Copy)]
185struct IntersectionPoint {
186 ua: f64,
187 va: f64,
188 ub: f64,
189 vb: f64,
190 point: Vec3,
191}
192
193fn refine_point(
194 first: SurfaceInfo<'_>,
195 second: SurfaceInfo<'_>,
196 mut ua: f64,
197 mut va: f64,
198 mut ub: f64,
199 mut vb: f64,
200 tolerance: f64,
201) -> Result<Option<IntersectionPoint>, String> {
202 for _ in 0..40 {
203 let da = first.surface.derivatives(ua, va, 1)?;
204 let db = second.surface.derivatives(ub, vb, 1)?;
205 let residual = da[0][0].sub(db[0][0]);
206 if residual.length() <= tolerance * 0.01 + EPSILON {
207 return Ok(Some(IntersectionPoint {
208 ua,
209 va,
210 ub,
211 vb,
212 point: da[0][0].add(db[0][0]).scale(0.5),
213 }));
214 }
215 let columns = [
216 da[1][0],
217 da[0][1],
218 db[1][0].scale(-1.0),
219 db[0][1].scale(-1.0),
220 ];
221 let mut matrix = [[0.0f64; 3]; 3];
222 for column in columns {
223 let values = [column.x, column.y, column.z];
224 for row in 0..3 {
225 for col in 0..3 {
226 matrix[row][col] += values[row] * values[col];
227 }
228 }
229 }
230 let lambda = match solve_small(matrix, [-residual.x, -residual.y, -residual.z], 3) {
231 Ok(value) => value,
232 Err(_) => return Ok(None),
233 };
234 let multiplier = Vec3::new(lambda[0], lambda[1], lambda[2]);
235 let mut changes = columns.map(|column| column.dot(multiplier));
236 let domains = [
237 first.u1 - first.u0,
238 first.v1 - first.v0,
239 second.u1 - second.u0,
240 second.v1 - second.v0,
241 ];
242 for index in 0..4 {
243 changes[index] = changes[index].clamp(-domains[index] / 4.0, domains[index] / 4.0);
244 }
245 ua = fit(ua + changes[0], first.u0, first.u1, first.closed_u);
246 va = fit(va + changes[1], first.v0, first.v1, first.closed_v);
247 ub = fit(ub + changes[2], second.u0, second.u1, second.closed_u);
248 vb = fit(vb + changes[3], second.v0, second.v1, second.closed_v);
249 }
250 let a = first.surface.evaluate(ua, va)?;
251 let b = second.surface.evaluate(ub, vb)?;
252 if a.sub(b).length() <= tolerance {
253 Ok(Some(IntersectionPoint {
254 ua,
255 va,
256 ub,
257 vb,
258 point: a.add(b).scale(0.5),
259 }))
260 } else {
261 Ok(None)
262 }
263}
264
265fn find_start_points(
266 first: SurfaceInfo<'_>,
267 second: SurfaceInfo<'_>,
268 tolerance: f64,
269 density: f64,
270) -> Result<Vec<IntersectionPoint>, String> {
271 let grid_count = |info: SurfaceInfo<'_>, direction_u: bool| {
272 let (knots, degree) = if direction_u {
273 (&info.surface.knots_u, info.surface.degree_u)
274 } else {
275 (&info.surface.knots_v, info.surface.degree_v)
276 };
277 (8.0_f64)
278 .max(((interior_knot_count(knots, degree) + 1) * (degree + 1)) as f64 * density)
279 .ceil()
280 .min(24.0) as usize
281 };
282 let nu = grid_count(first, true);
283 let nv = grid_count(first, false);
284 let first_box = Bounds::from_surface(first.surface)?;
285 let second_box = Bounds::from_surface(second.surface)?;
286 let gate = first_box.diagonal().min(second_box.diagonal()) * 0.15 + tolerance;
287 let expanded_second = second_box.expanded((tolerance * 100.0).max(gate));
288 let mut starts = Vec::new();
289 for i in 0..=nu {
290 for j in 0..=nv {
291 let ua = first.u0 + (first.u1 - first.u0) * i as f64 / nu as f64;
292 let va = first.v0 + (first.v1 - first.v0) * j as f64 / nv as f64;
293 let point = first.surface.evaluate(ua, va)?;
294 if !expanded_second.contains(point) {
295 continue;
296 }
297 let projection = project_point_to_surface(second.surface, point)?;
298 if projection.distance > gate {
299 continue;
300 }
301 if let Some(refined) =
302 refine_point(first, second, ua, va, projection.u, projection.v, tolerance)?
303 {
304 starts.push(refined);
305 }
306 }
307 }
308 Ok(starts)
309}
310
311fn transverse_at(
315 first: SurfaceInfo<'_>,
316 second: SurfaceInfo<'_>,
317 point: IntersectionPoint,
318) -> bool {
319 match (
320 first.surface.normal(point.ua, point.va),
321 second.surface.normal(point.ub, point.vb),
322 ) {
323 (Ok(a), Ok(b)) => a.cross(b).length() >= TRANSVERSE_START_CROSS,
324 _ => false,
325 }
326}
327
328fn tangent_at(
329 first: SurfaceInfo<'_>,
330 second: SurfaceInfo<'_>,
331 point: IntersectionPoint,
332) -> Result<Option<Vec3>, String> {
333 let first_normal = first.surface.normal(point.ua, point.va).ok();
334 let second_normal = second.surface.normal(point.ub, point.vb).ok();
335 match (first_normal, second_normal) {
336 (Some(a), Some(b)) => {
337 let tangent = a.cross(b);
338 if tangent.length() <= 1e-9 {
339 Ok(None)
340 } else {
341 Ok(Some(tangent.normalized()?))
342 }
343 }
344 _ => Ok(None),
345 }
346}
347
348fn polish_boundary(
349 first: SurfaceInfo<'_>,
350 second: SurfaceInfo<'_>,
351 mut point: IntersectionPoint,
352 tolerance: f64,
353) -> Result<Option<IntersectionPoint>, String> {
354 let free = [
355 first.closed_u || (point.ua > first.u0 && point.ua < first.u1),
356 first.closed_v || (point.va > first.v0 && point.va < first.v1),
357 second.closed_u || (point.ub > second.u0 && point.ub < second.u1),
358 second.closed_v || (point.vb > second.v0 && point.vb < second.v1),
359 ];
360 for _ in 0..40 {
361 let da = first.surface.derivatives(point.ua, point.va, 1)?;
362 let db = second.surface.derivatives(point.ub, point.vb, 1)?;
363 let residual = da[0][0].sub(db[0][0]);
364 if residual.length() <= tolerance {
365 point.point = da[0][0].add(db[0][0]).scale(0.5);
366 return Ok(Some(point));
367 }
368 let all_columns = [
369 da[1][0],
370 da[0][1],
371 db[1][0].scale(-1.0),
372 db[0][1].scale(-1.0),
373 ];
374 let indices: Vec<usize> = (0..4).filter(|index| free[*index]).collect();
375 if indices.is_empty() {
376 return Ok(None);
377 }
378 let mut matrix = [[0.0f64; 4]; 4];
379 let mut rhs = [0.0f64; 4];
380 for (row, &r) in indices.iter().enumerate() {
381 rhs[row] = -all_columns[r].dot(residual);
382 for (column, &c) in indices.iter().enumerate() {
383 matrix[row][column] = all_columns[r].dot(all_columns[c]);
384 }
385 }
386 let changes = match solve_small(matrix, rhs, indices.len()) {
387 Ok(value) => value,
388 Err(_) => return Ok(None),
389 };
390 let mut update = [0.0; 4];
391 for (index, ¶meter_index) in indices.iter().enumerate() {
392 update[parameter_index] = changes[index];
393 }
394 point.ua = fit(point.ua + update[0], first.u0, first.u1, first.closed_u);
395 point.va = fit(point.va + update[1], first.v0, first.v1, first.closed_v);
396 point.ub = fit(point.ub + update[2], second.u0, second.u1, second.closed_u);
397 point.vb = fit(point.vb + update[3], second.v0, second.v1, second.closed_v);
398 }
399 Ok(None)
400}
401
402fn correct(
403 first: SurfaceInfo<'_>,
404 second: SurfaceInfo<'_>,
405 from: IntersectionPoint,
406 predicted: Vec3,
407 tangent: Vec3,
408 tolerance: f64,
409) -> Result<Option<(IntersectionPoint, bool)>, String> {
410 let mut point = from;
411 let mut boundary = false;
412 for _ in 0..30 {
413 let da = first.surface.derivatives(point.ua, point.va, 1)?;
414 let db = second.surface.derivatives(point.ub, point.vb, 1)?;
415 let residual = da[0][0].sub(db[0][0]);
416 let plane_residual = da[0][0].sub(predicted).dot(tangent);
417 if residual.length() <= tolerance * 0.01 + EPSILON
418 && plane_residual.abs() <= tolerance * 0.01 + EPSILON
419 {
420 point.point = da[0][0].add(db[0][0]).scale(0.5);
421 return Ok(Some((point, boundary)));
422 }
423 let matrix = [
424 [da[1][0].x, da[0][1].x, -db[1][0].x, -db[0][1].x],
425 [da[1][0].y, da[0][1].y, -db[1][0].y, -db[0][1].y],
426 [da[1][0].z, da[0][1].z, -db[1][0].z, -db[0][1].z],
427 [da[1][0].dot(tangent), da[0][1].dot(tangent), 0.0, 0.0],
428 ];
429 let changes = match solve_small(
430 matrix,
431 [-residual.x, -residual.y, -residual.z, -plane_residual],
432 4,
433 ) {
434 Ok(value) => value,
435 Err(_) => return Ok(None),
436 };
437 let scale = [
438 changes[0].abs() / ((first.u1 - first.u0) / 4.0),
439 changes[1].abs() / ((first.v1 - first.v0) / 4.0),
440 changes[2].abs() / ((second.u1 - second.u0) / 4.0),
441 changes[3].abs() / ((second.v1 - second.v0) / 4.0),
442 1.0,
443 ]
444 .into_iter()
445 .fold(1.0_f64, f64::max);
446 let raw = [
447 point.ua + changes[0] / scale,
448 point.va + changes[1] / scale,
449 point.ub + changes[2] / scale,
450 point.vb + changes[3] / scale,
451 ];
452 let domains = [
453 (first.u0, first.u1, first.closed_u),
454 (first.v0, first.v1, first.closed_v),
455 (second.u0, second.u1, second.closed_u),
456 (second.v0, second.v1, second.closed_v),
457 ];
458 boundary = false;
459 let mut next = [0.0; 4];
460 for index in 0..4 {
461 next[index] = fit(
462 raw[index],
463 domains[index].0,
464 domains[index].1,
465 domains[index].2,
466 );
467 if !domains[index].2
468 && (next[index] == domains[index].0 || next[index] == domains[index].1)
469 && raw[index] != next[index]
470 {
471 boundary = true;
472 }
473 }
474 let old = [point.ua, point.va, point.ub, point.vb];
475 let spans = [
476 first.u1 - first.u0,
477 first.v1 - first.v0,
478 second.u1 - second.u0,
479 second.v1 - second.v0,
480 ];
481 let stalled = (0..4).all(|index| (next[index] - old[index]).abs() <= 1e-14 * spans[index]);
482 point.ua = next[0];
483 point.va = next[1];
484 point.ub = next[2];
485 point.vb = next[3];
486 if stalled {
487 if boundary {
488 if let Some(polished) = polish_boundary(first, second, point, tolerance)? {
489 return Ok(Some((polished, true)));
490 }
491 }
492 return Ok(None);
493 }
494 }
495 Ok(None)
496}
497
498struct Trace {
499 points: Vec<IntersectionPoint>,
500 closed: bool,
501}
502
503fn vector_angle(a: Vec3, b: Vec3) -> f64 {
504 (a.dot(b) / (a.length() * b.length()))
505 .clamp(-1.0, 1.0)
506 .acos()
507}
508
509fn normal_turn_step(
518 first: SurfaceInfo<'_>,
519 second: SurfaceInfo<'_>,
520 point: IntersectionPoint,
521 tangent: Vec3,
522 budget: f64,
523) -> Option<f64> {
524 let mut best: Option<f64> = None;
525 for (surface, u, v) in [
526 (first.surface, point.ua, point.va),
527 (second.surface, point.ub, point.vb),
528 ] {
529 let derivatives = surface.derivatives(u, v, 2).ok()?;
530 let normal = derivatives[1][0].cross(derivatives[0][1]);
531 let normal_length = normal.length();
532 if normal_length <= 1e-12 {
533 continue;
534 }
535 let unit_normal = normal.scale(1.0 / normal_length);
536 for (first_derivative, second_derivative) in [
537 (derivatives[1][0], derivatives[2][0]),
538 (derivatives[0][1], derivatives[0][2]),
539 ] {
540 let metric = first_derivative.dot(first_derivative);
541 if metric <= 1e-16 {
542 continue;
543 }
544 let curvature = second_derivative.dot(unit_normal).abs();
545 if curvature <= 1e-12 {
546 continue; }
548 let advance = budget * metric / curvature;
549 let alignment = first_derivative
550 .scale(1.0 / metric.sqrt())
551 .dot(tangent)
552 .abs();
553 if alignment <= 0.1 {
554 continue; }
556 let along = advance * alignment;
557 if best.map(|value| along < value).unwrap_or(true) {
558 best = Some(along);
559 }
560 }
561 }
562 best
563}
564
565#[derive(Clone, Copy)]
568struct TraceSwitches {
569 first_step_ceiling: bool,
572 debug: bool,
574}
575
576impl TraceSwitches {
577 fn from_env() -> Self {
578 Self {
579 first_step_ceiling: std::env::var("BREP_SSI_FIRST_STEP_BUDGET").as_deref() != Ok("0"),
580 debug: std::env::var_os("BREP_DEBUG_SSI_TRACE").is_some(),
581 }
582 }
583}
584
585#[allow(clippy::too_many_arguments)]
586fn trace(
587 first: SurfaceInfo<'_>,
588 second: SurfaceInfo<'_>,
589 start: IntersectionPoint,
590 direction: f64,
591 tolerance: f64,
592 initial_step: f64,
593 minimum_step: f64,
594 maximum_step: f64,
595 maximum_steps: usize,
596 switches: TraceSwitches,
597) -> Result<Trace, String> {
598 let mut points = vec![start];
599 let mut current = start;
600 let Some(mut tangent) = tangent_at(first, second, start)? else {
601 return Ok(Trace {
602 points,
603 closed: false,
604 });
605 };
606 tangent = tangent.scale(direction);
607 let mut step_size = initial_step.min(maximum_step);
618 if switches.first_step_ceiling {
619 if let Some(budget) = normal_turn_step(first, second, start, tangent, 0.1) {
620 step_size = step_size.min(budget).clamp(minimum_step, maximum_step);
621 }
622 }
623 let mut terminated = false;
624 for _ in 0..maximum_steps {
625 let mut accepted = None;
626 while step_size >= minimum_step {
627 let predicted = current.point.add(tangent.scale(step_size));
628 if let Some((corrected, boundary)) =
629 correct(first, second, current, predicted, tangent, tolerance)?
630 {
631 if corrected.point.sub(current.point).length() <= 2.5 * step_size {
632 accepted = Some((corrected, boundary));
633 break;
634 }
635 }
636 step_size /= 2.0;
637 }
638 let Some((candidate, boundary)) = accepted else {
639 terminated = true;
640 break;
641 };
642 points.push(candidate);
643 if switches.debug {
644 let turn = tangent_at(first, second, candidate)
648 .ok()
649 .flatten()
650 .map(|next| vector_angle(tangent, if next.dot(tangent) < 0.0 { next.scale(-1.0) } else { next }))
651 .unwrap_or(f64::NAN);
652 let budget = normal_turn_step(first, second, current, tangent, 0.1).unwrap_or(f64::NAN);
653 let at = candidate.point;
654 eprintln!(
655 "SSI-TRACE dir {direction} step {step_size:.4e} jump {:.4e} turn {turn:.4} ceiling {budget:.4e} boundary {boundary} p ({:.6},{:.6},{:.6})",
656 at.sub(current.point).length(),
657 at.x,
658 at.y,
659 at.z
660 );
661 }
662 if boundary {
663 terminated = true;
664 break;
665 }
666 let Some(mut next_tangent) = tangent_at(first, second, candidate)? else {
667 terminated = true;
668 break;
669 };
670 if next_tangent.dot(tangent) < 0.0 {
671 next_tangent = next_tangent.scale(-1.0);
672 }
673 let angle = vector_angle(tangent, next_tangent);
674 if angle > 0.35 {
675 points.pop();
676 step_size = minimum_step.max(step_size / 2.0);
677 if step_size <= minimum_step * 1.01 {
678 points.push(candidate);
679 } else {
680 continue;
681 }
682 }
683 let reactive = if angle < 0.03 {
688 step_size * 1.5
689 } else if angle > 0.15 {
690 step_size * 0.6
691 } else {
692 step_size
693 };
694 step_size = match normal_turn_step(first, second, candidate, next_tangent, 0.1) {
695 Some(budget) => reactive.min(budget),
696 None => reactive,
697 }
698 .clamp(minimum_step, maximum_step);
699 if points.len() > 4 && candidate.point.sub(start.point).length() <= step_size * 1.2 {
700 let to_start = start.point.sub(candidate.point);
701 if to_start.length() <= EPSILON || to_start.dot(next_tangent) >= 0.0 {
702 points.push(start);
703 return Ok(Trace {
704 points,
705 closed: true,
706 });
707 }
708 }
709 current = candidate;
710 tangent = next_tangent;
711 }
712 if !terminated {
713 return Err(format!(
717 "surface intersection trace exhausted {maximum_steps} steps \
718 ({} points, step {step_size:.3e}) without reaching a boundary or closure",
719 points.len()
720 ));
721 }
722 Ok(Trace {
723 points,
724 closed: false,
725 })
726}
727
728fn point_segment_distance(point: Vec3, start: Vec3, end: Vec3) -> f64 {
729 let segment = end.sub(start);
730 let length_squared = segment.length_squared();
731 if length_squared <= EPSILON {
732 return point.sub(start).length();
733 }
734 let parameter = point.sub(start).dot(segment) / length_squared;
735 let parameter = parameter.clamp(0.0, 1.0);
736 point.sub(start.add(segment.scale(parameter))).length()
737}
738
739#[derive(Clone, Debug, Serialize)]
740pub struct SurfaceIntersectionCurve {
741 pub points: Vec<Vec3>,
742 pub params_a: Vec<[f64; 2]>,
743 pub params_b: Vec<[f64; 2]>,
744 pub closed: bool,
745}
746
747#[derive(Clone, Debug, Deserialize)]
748pub struct SurfaceIntersectionOptions {
749 #[serde(default = "default_tolerance")]
750 pub tolerance: f64,
751 #[serde(default = "default_density")]
752 pub seed_density: f64,
753 #[serde(default = "default_maximum_steps")]
754 pub maximum_steps: usize,
755 pub maximum_step: Option<f64>,
756 #[serde(default)]
757 pub seed_points: Vec<Vec3>,
758 #[serde(default)]
759 pub seed_only: bool,
760 pub seed_gate: Option<f64>,
761}
762
763fn default_tolerance() -> f64 {
764 LINEAR_TOLERANCE
765}
766fn default_density() -> f64 {
767 1.0
768}
769fn default_maximum_steps() -> usize {
770 4000
771}
772
773impl Default for SurfaceIntersectionOptions {
774 fn default() -> Self {
775 Self {
776 tolerance: default_tolerance(),
777 seed_density: default_density(),
778 maximum_steps: default_maximum_steps(),
779 maximum_step: None,
780 seed_points: Vec::new(),
781 seed_only: false,
782 seed_gate: None,
783 }
784 }
785}
786
787pub(crate) fn refine_to_intersection(
796 first_surface: &NurbsSurface,
797 second_surface: &NurbsSurface,
798 seed: Vec3,
799 tolerance: f64,
800 reach: f64,
801) -> Result<Option<Vec3>, String> {
802 let first = surface_info(first_surface)?;
803 let second = surface_info(second_surface)?;
804 let on_first = project_point_to_surface(first_surface, seed)?;
805 let on_second = project_point_to_surface(second_surface, seed)?;
806 let Some(refined) = refine_point(
807 first,
808 second,
809 on_first.u,
810 on_first.v,
811 on_second.u,
812 on_second.v,
813 tolerance,
814 )?
815 else {
816 return Ok(None);
817 };
818 if refined.point.sub(seed).length() > reach {
819 return Ok(None);
820 }
821 Ok(Some(refined.point))
822}
823
824pub fn intersect_surfaces(
825 first_surface: &NurbsSurface,
826 second_surface: &NurbsSurface,
827 options: &SurfaceIntersectionOptions,
828) -> Result<Vec<SurfaceIntersectionCurve>, String> {
829 let first_box = Bounds::from_surface(first_surface)?;
830 let second_box = Bounds::from_surface(second_surface)?;
831 if !first_box.intersects(second_box, options.tolerance * 10.0) {
832 return Ok(Vec::new());
833 }
834 let first = surface_info(first_surface)?;
835 let second = surface_info(second_surface)?;
836 let diagonal = first_box.diagonal().min(second_box.diagonal());
837 let initial_step = diagonal / 100.0;
838 let minimum_step = (options.tolerance * 100.0).max(diagonal * 1e-6);
839 let maximum_step = (diagonal / 15.0).min(options.maximum_step.unwrap_or(f64::INFINITY));
840 let maximum_steps = step_budget(options.maximum_steps, maximum_step, diagonal);
841 let mut starts = if options.seed_only {
842 Vec::new()
843 } else {
844 find_start_points(first, second, options.tolerance, options.seed_density)?
845 };
846 starts.retain(|start| transverse_at(first, second, *start));
851 let seed_gate = options.seed_gate.unwrap_or(options.tolerance * 100.0);
852 for seed in &options.seed_points {
853 let projection_a = project_point_to_surface(first_surface, *seed)?;
854 if projection_a.distance > seed_gate {
855 continue;
856 }
857 let projection_b = project_point_to_surface(second_surface, *seed)?;
858 if projection_b.distance > seed_gate {
859 continue;
860 }
861 let Some(refined) = refine_point(
862 first,
863 second,
864 projection_a.u,
865 projection_a.v,
866 projection_b.u,
867 projection_b.v,
868 options.tolerance,
869 )?
870 else {
871 continue;
872 };
873 if !transverse_at(first, second, refined) {
874 continue;
875 }
876 starts.push(refined);
877 }
878
879 let switches = TraceSwitches::from_env();
880 let mut curves: Vec<SurfaceIntersectionCurve> = Vec::new();
881 for start in starts {
882 let claimed = curves.iter().any(|curve| {
883 curve.points.windows(2).any(|segment| {
884 point_segment_distance(start.point, segment[0], segment[1]) <= initial_step * 1.5
885 })
886 });
887 if switches.debug {
888 let at = start.point;
889 eprintln!(
890 "SSI-START claimed {claimed} p ({:.6},{:.6},{:.6}) initial {initial_step:.4e} minimum {minimum_step:.4e} maximum {maximum_step:.4e}",
891 at.x, at.y, at.z
892 );
893 }
894 if claimed {
895 continue;
896 }
897 let forward = trace(
898 first,
899 second,
900 start,
901 1.0,
902 options.tolerance,
903 initial_step,
904 minimum_step,
905 maximum_step,
906 maximum_steps,
907 switches,
908 )?;
909 let (all, closed) = if forward.closed {
910 (forward.points, true)
911 } else {
912 let backward = trace(
913 first,
914 second,
915 start,
916 -1.0,
917 options.tolerance,
918 initial_step,
919 minimum_step,
920 maximum_step,
921 maximum_steps,
922 switches,
923 )?;
924 let mut all: Vec<_> = backward.points.into_iter().skip(1).rev().collect();
925 all.extend(forward.points);
926 (all, false)
927 };
928 if all.len() < 2 {
929 continue;
930 }
931 curves.push(SurfaceIntersectionCurve {
932 points: all.iter().map(|point| point.point).collect(),
933 params_a: all.iter().map(|point| [point.ua, point.va]).collect(),
934 params_b: all.iter().map(|point| [point.ub, point.vb]).collect(),
935 closed,
936 });
937 }
938 Ok(curves)
939}
940
941const SUPPLEMENTAL_TRANSVERSE_CROSS: f64 = TRANSVERSE_START_CROSS;
945
946pub fn intersect_surfaces_supplemental(
973 first_surface: &NurbsSurface,
974 second_surface: &NurbsSurface,
975 options: &SurfaceIntersectionOptions,
976) -> Result<Vec<SurfaceIntersectionCurve>, String> {
977 let first_box = Bounds::from_surface(first_surface)?;
978 let second_box = Bounds::from_surface(second_surface)?;
979 if !first_box.intersects(second_box, options.tolerance * 10.0) {
980 return Ok(Vec::new());
981 }
982 let first = surface_info(first_surface)?;
983 let second = surface_info(second_surface)?;
984 let diagonal = first_box.diagonal().min(second_box.diagonal());
985 let initial_step = diagonal / 100.0;
986 let minimum_step = (options.tolerance * 100.0).max(diagonal * 1e-6);
987 let maximum_step = (diagonal / 15.0).min(options.maximum_step.unwrap_or(f64::INFINITY));
988 let maximum_steps = step_budget(options.maximum_steps, maximum_step, diagonal);
989
990 let density = options.seed_density.max(2.0);
995 let mut starts: Vec<IntersectionPoint> = Vec::new();
996 for gridded_second in [false, true] {
997 let (a, b) = if gridded_second {
998 (second, first)
999 } else {
1000 (first, second)
1001 };
1002 for start in find_start_points(a, b, options.tolerance, density)? {
1003 let start = if gridded_second {
1006 IntersectionPoint {
1007 ua: start.ub,
1008 va: start.vb,
1009 ub: start.ua,
1010 vb: start.va,
1011 point: start.point,
1012 }
1013 } else {
1014 start
1015 };
1016 starts.push(start);
1017 }
1018 }
1019 starts.retain(|start| {
1023 match (
1024 first.surface.normal(start.ua, start.va),
1025 second.surface.normal(start.ub, start.vb),
1026 ) {
1027 (Ok(na), Ok(nb)) => na.cross(nb).length() > SUPPLEMENTAL_TRANSVERSE_CROSS,
1028 _ => false,
1029 }
1030 });
1031
1032 let switches = TraceSwitches::from_env();
1033 let mut curves: Vec<SurfaceIntersectionCurve> = Vec::new();
1034 for start in starts {
1035 let claimed = curves.iter().any(|curve| {
1036 curve.points.windows(2).any(|segment| {
1037 point_segment_distance(start.point, segment[0], segment[1]) <= initial_step * 1.5
1038 })
1039 });
1040 if claimed {
1041 continue;
1042 }
1043 let Ok(forward) = trace(
1047 first,
1048 second,
1049 start,
1050 1.0,
1051 options.tolerance,
1052 initial_step,
1053 minimum_step,
1054 maximum_step,
1055 maximum_steps,
1056 switches,
1057 ) else {
1058 continue;
1059 };
1060 let (all, closed) = if forward.closed {
1061 (forward.points, true)
1062 } else {
1063 let Ok(backward) = trace(
1064 first,
1065 second,
1066 start,
1067 -1.0,
1068 options.tolerance,
1069 initial_step,
1070 minimum_step,
1071 maximum_step,
1072 maximum_steps,
1073 switches,
1074 ) else {
1075 continue;
1076 };
1077 let mut all: Vec<_> = backward.points.into_iter().skip(1).rev().collect();
1078 all.extend(forward.points);
1079 (all, false)
1080 };
1081 if all.len() < 2 {
1082 continue;
1083 }
1084 let transverse = all.iter().step_by((all.len() / 5).max(1)).any(|point| {
1088 match (
1089 first.surface.normal(point.ua, point.va),
1090 second.surface.normal(point.ub, point.vb),
1091 ) {
1092 (Ok(na), Ok(nb)) => na.cross(nb).length() > SUPPLEMENTAL_TRANSVERSE_CROSS,
1093 _ => false,
1094 }
1095 });
1096 if !transverse {
1097 continue;
1098 }
1099 curves.push(SurfaceIntersectionCurve {
1100 points: all.iter().map(|point| point.point).collect(),
1101 params_a: all.iter().map(|point| [point.ua, point.va]).collect(),
1102 params_b: all.iter().map(|point| [point.ub, point.vb]).collect(),
1103 closed,
1104 });
1105 }
1106 Ok(curves)
1107}
1108