1use super::*;
2use super::recognition::{generatrix_is_meridional_half_ray, surface_scale};
3use super::revolution::{intersect_coaxial_revolutions, rotate_curve_about_axis};
4
5struct PlaneData {
7 origin: Vec3,
8 normal: Vec3,
9}
10
11fn plane_data(analytic: &AnalyticSurface) -> Option<PlaneData> {
12 match analytic {
13 AnalyticSurface::Plane {
14 origin,
15 u_dir,
16 v_dir,
17 ..
18 } => Some(PlaneData {
19 origin: *origin,
20 normal: u_dir.cross(*v_dir).normalized().ok()?,
21 }),
22 _ => None,
23 }
24}
25
26fn map_rational_curve(
31 curve: &NurbsCurve,
32 map: impl Fn(crate::Vec4) -> crate::Vec4,
33) -> Option<NurbsCurve> {
34 let mut controls = Vec::with_capacity(curve.control_points.len());
35 let mut sign = 0.0f64;
36 for point in &curve.control_points {
37 let image = map(*point);
38 if image.w == 0.0 || !image.w.is_finite() {
39 return None;
40 }
41 if sign == 0.0 {
42 sign = image.w.signum();
43 } else if image.w.signum() != sign {
44 return None;
45 }
46 controls.push(image);
47 }
48 if sign < 0.0 {
49 for point in &mut controls {
50 point.x = -point.x;
51 point.y = -point.y;
52 point.z = -point.z;
53 point.w = -point.w;
54 }
55 }
56 NurbsCurve::new(curve.degree, curve.knots.clone(), controls).ok()
57}
58
59fn frame_circle(frame: &RevolutionFrame, rho: f64, z: f64) -> Option<NurbsCurve> {
62 make_arc(
63 frame.origin.add(frame.axis.scale(z)),
64 frame.x_axis,
65 frame.y_axis,
66 rho,
67 0.0,
68 std::f64::consts::TAU,
69 )
70 .ok()
71}
72
73pub fn intersect_analytic_pair(
77 first: &NurbsSurface,
78 second: &NurbsSurface,
79 tolerance: f64,
80) -> Option<Vec<NurbsCurve>> {
81 intersect_analytic_pair_with(first, second, tolerance, ruled_gate())
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub(crate) enum RuledGate {
88 Count,
90 Geometry,
94}
95
96pub(crate) fn ruled_gate() -> RuledGate {
102 if std::env::var("BREP_RULED_GATE_GEOMETRY").as_deref() == Ok("0") {
103 RuledGate::Count
104 } else {
105 RuledGate::Geometry
106 }
107}
108
109pub(crate) fn intersect_analytic_pair_with(
112 first: &NurbsSurface,
113 second: &NurbsSurface,
114 tolerance: f64,
115 gate: RuledGate,
116) -> Option<Vec<NurbsCurve>> {
117 if let Some(curves) = intersect_recognized_pair(first, second, tolerance, gate) {
118 return Some(curves);
119 }
120 if let Some(curves) = intersect_coaxial_revolutions(first, second, tolerance) {
121 return Some(curves);
122 }
123 if let Some(curves) = intersect_axial_plane_revolution(first, second, tolerance) {
124 return Some(curves);
125 }
126 intersect_axial_plane_revolution(second, first, tolerance)
127}
128
129fn intersect_axial_plane_revolution(
135 plane_surface: &NurbsSurface,
136 revolved: &NurbsSurface,
137 _tolerance: f64,
138) -> Option<Vec<NurbsCurve>> {
139 let plane = match plane_surface.analytic()? {
140 AnalyticSurface::Plane {
141 origin,
142 u_dir,
143 v_dir,
144 ..
145 } => (*origin, u_dir.cross(*v_dir).normalized().ok()?),
146 _ => return None,
147 };
148 let structure = revolution_structure(revolved)?;
149 if !generatrix_is_meridional_half_ray(&structure.generatrix, &structure.frame) {
150 return None;
151 }
152 let scale = surface_scale(plane_surface)
153 .max(surface_scale(revolved))
154 .max(1.0);
155 let (plane_origin, plane_normal) = plane;
156 if structure.frame.axis.dot(plane_normal).abs() > 1e-9 {
157 return None;
158 }
159 if structure
160 .frame
161 .origin
162 .sub(plane_origin)
163 .dot(plane_normal)
164 .abs()
165 > 1e-9 * scale
166 {
167 return None;
168 }
169 let radial = structure.frame.axis.cross(plane_normal).normalized().ok()?;
170 let tau = std::f64::consts::TAU;
171 let mut curves = Vec::new();
172 for direction in [radial, radial.scale(-1.0)] {
173 let mut angle = direction
174 .dot(structure.frame.y_axis)
175 .atan2(direction.dot(structure.frame.x_axis));
176 if angle < -1e-9 {
177 angle += tau;
178 }
179 if angle <= structure.sweep + 1e-9 {
180 curves.push(rotate_curve_about_axis(
181 &structure.generatrix,
182 structure.frame.origin,
183 structure.frame.axis,
184 angle.clamp(0.0, structure.sweep),
185 )?);
186 }
187 }
188 Some(curves)
189}
190
191fn intersect_recognized_pair(
192 first: &NurbsSurface,
193 second: &NurbsSurface,
194 tolerance: f64,
195 gate: RuledGate,
196) -> Option<Vec<NurbsCurve>> {
197 let a = first.analytic()?;
198 let b = second.analytic()?;
199 if !matches!(b, AnalyticSurface::Plane { .. }) {
200 if let Some(plane) = plane_data(a) {
201 return intersect_plane_quadric(&plane, b, tolerance, gate);
202 }
203 }
204 if !matches!(a, AnalyticSurface::Plane { .. }) {
205 if let Some(plane) = plane_data(b) {
206 return intersect_plane_quadric(&plane, a, tolerance, gate);
207 }
208 }
209 if let (Some((center_a, radius_a)), Some((center_b, radius_b))) =
212 (a.sphere_geometry(), b.sphere_geometry())
213 {
214 return intersect_sphere_sphere(center_a, radius_a, center_b, radius_b, tolerance);
215 }
216 None
223}
224
225fn ruled_revolution_data(
232 quadric: &AnalyticSurface,
233 tolerance: f64,
234 gate: RuledGate,
235) -> Option<(RevolutionFrame, f64, f64, f64)> {
236 match quadric {
237 AnalyticSurface::RuledRevolution {
238 frame,
239 rho0,
240 rho1,
241 height,
242 } => Some((frame.clone(), *rho0, *rho1, *height)),
243 AnalyticSurface::Revolution {
244 frame, generatrix, ..
245 } => {
246 let (start, end) = match gate {
247 RuledGate::Count => {
248 let controls = &generatrix.control_points;
249 if generatrix.degree != 1
250 || controls.len() != 2
251 || (controls[0].w - 1.0).abs() > RECOGNITION_TOLERANCE
252 || (controls[1].w - 1.0).abs() > RECOGNITION_TOLERANCE
253 {
254 return None;
255 }
256 (controls[0].point().ok()?, controls[1].point().ok()?)
257 }
258 RuledGate::Geometry => generatrix.straight_segment(tolerance)?,
259 };
260 let (_, rho0, z0) = frame.cylindrical(start);
261 let (_, rho1, z1) = frame.cylindrical(end);
262 let height = z1 - z0;
263 if height.abs() <= 1e-12 * (1.0 + rho0.abs().max(rho1.abs())) {
264 return None;
265 }
266 let origin = frame.origin.add(frame.axis.scale(z0));
267 Some((
268 RevolutionFrame {
269 origin,
270 ..frame.clone()
271 },
272 rho0,
273 rho1,
274 height,
275 ))
276 }
277 _ => None,
278 }
279}
280
281pub(crate) fn ruled_gate_census(surface: &NurbsSurface, tolerance: f64) -> Option<String> {
285 let quadric = surface.analytic()?;
286 let AnalyticSurface::Revolution { generatrix, .. } = quadric else {
287 return None;
288 };
289 let count = ruled_revolution_data(quadric, tolerance, RuledGate::Count).is_some();
290 let geometry = ruled_revolution_data(quadric, tolerance, RuledGate::Geometry).is_some();
291 if count == geometry {
292 return None;
293 }
294 let controls = &generatrix.control_points;
295 let points: Vec<Vec3> = controls
296 .iter()
297 .map(|control| control.point().unwrap_or_default())
298 .collect();
299 let (first, last) = (points[0], points[points.len() - 1]);
300 let direction = last.sub(first);
301 let length_squared = direction.length_squared();
302 let mut collinearity = 0.0_f64;
303 for point in &points {
304 collinearity = collinearity.max(if length_squared <= 0.0 {
305 point.sub(first).length()
306 } else {
307 let fraction = (point.sub(first).dot(direction) / length_squared).clamp(0.0, 1.0);
308 point.sub(first.add(direction.scale(fraction))).length()
309 });
310 }
311 let unit_weights = controls
312 .iter()
313 .all(|control| (control.w - 1.0).abs() <= RECOGNITION_TOLERANCE);
314 Some(format!(
315 "degree={} controls={} collinearity={collinearity:.3e} unit_weights={unit_weights} \
316 count_gate={count} geometry_gate={geometry}",
317 generatrix.degree,
318 controls.len()
319 ))
320}
321
322fn degenerate_apex_frustum(rho0: f64, rho1: f64, height: f64) -> bool {
343 if std::env::var("BREP_CONE_APEX_GUARD").as_deref() == Ok("0") {
344 return false;
345 }
346 let delta = rho1 - rho0;
347 delta * delta <= 64.0 * f64::EPSILON * height.abs() * rho0.abs().max(rho1.abs())
348}
349
350pub fn plane_ruled_section_arc(
383 plane_origin: Vec3,
384 plane_normal: Vec3,
385 frame: &RevolutionFrame,
386 rho0: f64,
387 rho1: f64,
388 height: f64,
389 start_azimuth: f64,
390 sweep: f64,
391 tolerance: f64,
392) -> Result<NurbsCurve, String> {
393 let tau = std::f64::consts::TAU;
394 if !(start_azimuth.is_finite() && sweep.is_finite()) {
395 return Err("plane_ruled_section_arc: non-finite azimuth range".into());
396 }
397 if sweep <= 1e-12 || sweep > tau + 1e-9 {
398 return Err(format!(
399 "plane_ruled_section_arc: the section arc sweeps {sweep:.6e} rad, which is not a \
400 positive arc of at most one turn — refusing"
401 ));
402 }
403 let normal = plane_normal.normalized()?;
404 let radius_scale = rho0.abs().max(rho1.abs()).max(1.0);
405 let axis = frame.axis;
406 let is_cylinder = (rho1 - rho0).abs() <= 1e-9 * radius_scale
407 || degenerate_apex_frustum(rho0, rho1, height);
408 let curve = if is_cylinder {
409 let alignment = normal.dot(axis);
410 if alignment.abs() <= 1e-12 {
411 return Err(
412 "plane_ruled_section_arc: the plane is parallel to the cylinder axis, so the \
413 section is a generatrix pair rather than a conic — refusing"
414 .into(),
415 );
416 }
417 if rho0.abs() <= tolerance {
418 return Err("plane_ruled_section_arc: the cylinder carrier has no radius".into());
419 }
420 let base = make_arc(
421 frame.origin,
422 frame.x_axis,
423 frame.y_axis,
424 rho0.abs(),
425 start_azimuth,
426 start_azimuth + sweep,
427 )?;
428 let origin_dot = plane_origin.dot(normal);
429 map_rational_curve(&base, |p| {
430 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / alignment;
431 crate::Vec4 {
432 x: p.x + axis.x * t,
433 y: p.y + axis.y * t,
434 z: p.z + axis.z * t,
435 w: p.w,
436 }
437 })
438 .ok_or_else(|| {
439 "plane_ruled_section_arc: the cylinder section's weights are not one-signed \
440 — refusing"
441 .to_string()
442 })?
443 } else {
444 let apex = frame.origin.add(axis.scale(height * rho0 / (rho0 - rho1)));
445 let k = plane_origin.sub(apex).dot(normal);
446 if k.abs() <= tolerance {
447 return Err(
448 "plane_ruled_section_arc: the plane passes through the cone apex, so the \
449 section is a line pair rather than a conic — refusing"
450 .into(),
451 );
452 }
453 let (reference_rho, reference_z) = if rho0.abs() > rho1.abs() {
454 (rho0.abs(), 0.0)
455 } else {
456 (rho1.abs(), height)
457 };
458 if reference_rho <= tolerance {
459 return Err("plane_ruled_section_arc: the cone carrier degenerates to its apex".into());
460 }
461 let base = make_arc(
462 frame.origin.add(axis.scale(reference_z)),
463 frame.x_axis,
464 frame.y_axis,
465 reference_rho,
466 start_azimuth,
467 start_azimuth + sweep,
468 )?;
469 map_rational_curve(&base, |p| {
470 let relative = Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
471 let w_new = relative.dot(normal);
472 let scaled = relative.scale(k);
473 crate::Vec4 {
474 x: apex.x * w_new + scaled.x,
475 y: apex.y * w_new + scaled.y,
476 z: apex.z * w_new + scaled.z,
477 w: w_new,
478 }
479 })
480 .ok_or_else(|| {
481 "plane_ruled_section_arc: the section crosses the cone's apex plane (a parabolic \
482 or hyperbolic branch, whose weights change sign) — refusing"
483 .to_string()
484 })?
485 };
486 let [d0, d1] = curve.domain()?;
490 let slope = if height == 0.0 {
491 0.0
492 } else {
493 (rho1 - rho0) / height
494 };
495 for step in 0..=8 {
496 let point = curve.evaluate(d0 + (d1 - d0) * (step as f64 / 8.0))?;
497 let delta = point.sub(frame.origin);
498 let axial = delta.dot(axis);
499 let radial = delta.sub(axis.scale(axial)).length();
500 let off_ruled = (radial - (rho0 + slope * axial)).abs();
501 let off_plane = point.sub(plane_origin).dot(normal).abs();
502 if off_ruled > 10.0 * tolerance || off_plane > 10.0 * tolerance {
503 return Err(format!(
504 "plane_ruled_section_arc: the section does not lie on both carriers (off ruled \
505 {off_ruled:.3e}, off plane {off_plane:.3e}) — refusing"
506 ));
507 }
508 }
509 Ok(curve)
510}
511
512fn intersect_plane_quadric(
513 plane: &PlaneData,
514 quadric: &AnalyticSurface,
515 tolerance: f64,
516 gate: RuledGate,
517) -> Option<Vec<NurbsCurve>> {
518 match quadric {
519 AnalyticSurface::RuledRevolution { .. } | AnalyticSurface::Revolution { .. }
520 if ruled_revolution_data(quadric, tolerance, gate).is_some() =>
521 {
522 let (frame, rho0, rho1, height) = ruled_revolution_data(quadric, tolerance, gate)?;
523 let (frame, rho0, rho1, height) = (&frame, &rho0, &rho1, &height);
524 let alignment = plane.normal.dot(frame.axis);
525 if alignment.abs() >= 1.0 - 1e-12 {
526 let z = plane.origin.sub(frame.origin).dot(frame.axis);
528 let t = z / height;
529 if !(-1e-9..=1.0 + 1e-9).contains(&t) {
530 return Some(Vec::new());
531 }
532 let rho = rho0 + (rho1 - rho0) * t.clamp(0.0, 1.0);
533 if rho <= tolerance {
534 return Some(Vec::new());
535 }
536 return Some(vec![frame_circle(frame, rho, z)?]);
537 }
538 if (rho1 - rho0).abs() <= 1e-12 || degenerate_apex_frustum(*rho0, *rho1, *height) {
539 if alignment.abs() <= 1e-12 {
541 return cylinder_parallel_plane_lines(plane, frame, *rho0, *height, tolerance);
544 }
545 let circle = frame_circle(frame, *rho0, 0.0)?;
548 let denominator = alignment;
549 let origin_dot = plane.origin.dot(plane.normal);
550 let normal = plane.normal;
551 let axis = frame.axis;
552 let ellipse = map_rational_curve(&circle, |p| {
553 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / denominator;
554 crate::Vec4 {
555 x: p.x + axis.x * t,
556 y: p.y + axis.y * t,
557 z: p.z + axis.z * t,
558 w: p.w,
559 }
560 })?;
561 if !section_within_band(&ellipse, frame, *height, tolerance) {
562 return Some(Vec::new());
563 }
564 return Some(vec![ellipse]);
565 }
566 let apex_t = rho0 / (rho0 - rho1);
570 let apex = frame.origin.add(frame.axis.scale(height * apex_t));
571 let reference_rho = if rho0.abs() > rho1.abs() {
572 *rho0
573 } else {
574 *rho1
575 };
576 let reference_z = if rho0.abs() > rho1.abs() {
577 0.0
578 } else {
579 *height
580 };
581 let circle = frame_circle(frame, reference_rho, reference_z)?;
582 let k = plane.origin.sub(apex).dot(plane.normal);
583 if k.abs() <= tolerance {
584 return None;
586 }
587 let normal = plane.normal;
588 let conic = map_rational_curve(&circle, |p| {
589 let relative =
590 Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
591 let w_new = relative.dot(normal);
592 let scaled = relative.scale(k);
593 crate::Vec4 {
594 x: apex.x * w_new + scaled.x,
595 y: apex.y * w_new + scaled.y,
596 z: apex.z * w_new + scaled.z,
597 w: w_new,
598 }
599 })?;
600 if !section_within_band(&conic, frame, *height, tolerance) {
601 return Some(Vec::new());
602 }
603 Some(vec![conic])
604 }
605 AnalyticSurface::Sphere { frame, radius } => {
606 plane_sphere_section(plane, frame.origin, *radius, tolerance)
607 }
608 AnalyticSurface::Torus {
609 frame,
610 major_radius,
611 minor_radius,
612 } => plane_torus_section(plane, frame, *major_radius, *minor_radius, tolerance),
613 AnalyticSurface::Plane { .. } => None,
614 AnalyticSurface::RuledRevolution { .. } => None,
615 AnalyticSurface::Revolution { .. } => {
621 if let Some((center, radius)) = quadric.sphere_geometry() {
622 return plane_sphere_section(plane, center, radius, tolerance);
623 }
624 if let Some((frame, major_radius, minor_radius)) = quadric.torus_geometry() {
625 return plane_torus_section(plane, &frame, major_radius, minor_radius, tolerance);
626 }
627 None
628 }
629 }
630}
631
632fn plane_sphere_section(
635 plane: &PlaneData,
636 center: Vec3,
637 radius: f64,
638 tolerance: f64,
639) -> Option<Vec<NurbsCurve>> {
640 let distance = center.sub(plane.origin).dot(plane.normal);
641 if distance.abs() >= radius - tolerance {
642 return Some(Vec::new());
643 }
644 let circle_center = center.sub(plane.normal.scale(distance));
645 let circle_radius = (radius * radius - distance * distance).sqrt();
646 let x_axis = plane.normal.perpendicular().ok()?;
647 let y_axis = plane.normal.cross(x_axis);
648 Some(vec![make_arc(
649 circle_center,
650 x_axis,
651 y_axis,
652 circle_radius,
653 0.0,
654 std::f64::consts::TAU,
655 )
656 .ok()?])
657}
658
659fn plane_torus_section(
664 plane: &PlaneData,
665 frame: &RevolutionFrame,
666 major_radius: f64,
667 minor_radius: f64,
668 tolerance: f64,
669) -> Option<Vec<NurbsCurve>> {
670 let alignment = plane.normal.dot(frame.axis);
671 if alignment.abs() < 1.0 - 1e-12 {
672 return None;
673 }
674 let z = plane.origin.sub(frame.origin).dot(frame.axis);
675 if z.abs() >= minor_radius - tolerance {
676 return Some(Vec::new());
677 }
678 let offset = (minor_radius * minor_radius - z * z).sqrt();
679 let mut circles = Vec::new();
680 for rho in [major_radius - offset, major_radius + offset] {
681 if rho > tolerance {
682 circles.push(frame_circle(frame, rho, z)?);
683 }
684 }
685 Some(circles)
686}
687
688fn section_within_band(
705 curve: &NurbsCurve,
706 frame: &RevolutionFrame,
707 height: f64,
708 tolerance: f64,
709) -> bool {
710 let (band_low, band_high) = if height >= 0.0 {
711 (0.0, height)
712 } else {
713 (height, 0.0)
714 };
715 let margin = tolerance.max(1e-9) + 1e-9;
716 let mut min_z = f64::INFINITY;
717 let mut max_z = f64::NEG_INFINITY;
718 for p in &curve.control_points {
719 let z = Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w)
720 .sub(frame.origin)
721 .dot(frame.axis);
722 min_z = min_z.min(z);
723 max_z = max_z.max(z);
724 }
725 max_z >= band_low - margin && min_z <= band_high + margin
727}
728
729fn cylinder_parallel_plane_lines(
730 plane: &PlaneData,
731 frame: &RevolutionFrame,
732 radius: f64,
733 height: f64,
734 tolerance: f64,
735) -> Option<Vec<NurbsCurve>> {
736 let axis_to_plane = plane.origin.sub(frame.origin).dot(plane.normal);
738 if axis_to_plane.abs() >= radius - tolerance {
739 return Some(Vec::new());
740 }
741 let chord_half = (radius * radius - axis_to_plane * axis_to_plane).sqrt();
743 let chord_direction = frame.axis.cross(plane.normal).normalized().ok()?;
744 let foot = frame.origin.add(plane.normal.scale(axis_to_plane));
745 let mut lines = Vec::new();
746 for sign in [-1.0, 1.0] {
747 let base = foot.add(chord_direction.scale(sign * chord_half));
748 let top = base.add(frame.axis.scale(height));
749 lines.push(crate::make_line(base, top).ok()?);
750 }
751 Some(lines)
752}
753
754fn intersect_sphere_sphere(
755 center_a: Vec3,
756 radius_a: f64,
757 center_b: Vec3,
758 radius_b: f64,
759 tolerance: f64,
760) -> Option<Vec<NurbsCurve>> {
761 let offset = center_b.sub(center_a);
762 let distance = offset.length();
763 if distance <= tolerance {
764 return if (radius_a - radius_b).abs() <= tolerance {
767 None
768 } else {
769 Some(Vec::new())
770 };
771 }
772 if distance >= radius_a + radius_b - tolerance
773 || distance <= (radius_a - radius_b).abs() + tolerance
774 {
775 return Some(Vec::new());
776 }
777 let normal = offset.scale(1.0 / distance);
778 let along =
779 (distance * distance + radius_a * radius_a - radius_b * radius_b) / (2.0 * distance);
780 let circle_radius_squared = radius_a * radius_a - along * along;
781 if circle_radius_squared <= tolerance * tolerance {
782 return Some(Vec::new());
783 }
784 let center = center_a.add(normal.scale(along));
785 let x_axis = normal.perpendicular().ok()?;
786 let y_axis = normal.cross(x_axis);
787 Some(vec![make_arc(
788 center,
789 x_axis,
790 y_axis,
791 circle_radius_squared.sqrt(),
792 0.0,
793 std::f64::consts::TAU,
794 )
795 .ok()?])
796}