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_plane_affine_sphere(first, second, tolerance) {
121 return Some(curves);
122 }
123 if let Some(curves) = intersect_coaxial_revolutions(first, second, tolerance) {
124 return Some(curves);
125 }
126 if let Some(curves) = intersect_axial_plane_revolution(first, second, tolerance) {
127 return Some(curves);
128 }
129 intersect_axial_plane_revolution(second, first, tolerance)
130}
131
132fn intersect_axial_plane_revolution(
138 plane_surface: &NurbsSurface,
139 revolved: &NurbsSurface,
140 _tolerance: f64,
141) -> Option<Vec<NurbsCurve>> {
142 let plane = match plane_surface.analytic()? {
143 AnalyticSurface::Plane {
144 origin,
145 u_dir,
146 v_dir,
147 ..
148 } => (*origin, u_dir.cross(*v_dir).normalized().ok()?),
149 _ => return None,
150 };
151 let structure = revolution_structure(revolved)?;
152 if !generatrix_is_meridional_half_ray(&structure.generatrix, &structure.frame) {
153 return None;
154 }
155 let scale = surface_scale(plane_surface)
156 .max(surface_scale(revolved))
157 .max(1.0);
158 let (plane_origin, plane_normal) = plane;
159 if structure.frame.axis.dot(plane_normal).abs() > 1e-9 {
160 return None;
161 }
162 if structure
163 .frame
164 .origin
165 .sub(plane_origin)
166 .dot(plane_normal)
167 .abs()
168 > 1e-9 * scale
169 {
170 return None;
171 }
172 let radial = structure.frame.axis.cross(plane_normal).normalized().ok()?;
173 let tau = std::f64::consts::TAU;
174 let mut curves = Vec::new();
175 for direction in [radial, radial.scale(-1.0)] {
176 let mut angle = direction
177 .dot(structure.frame.y_axis)
178 .atan2(direction.dot(structure.frame.x_axis));
179 if angle < -1e-9 {
180 angle += tau;
181 }
182 if angle <= structure.sweep + 1e-9 {
183 curves.push(rotate_curve_about_axis(
184 &structure.generatrix,
185 structure.frame.origin,
186 structure.frame.axis,
187 angle.clamp(0.0, structure.sweep),
188 )?);
189 }
190 }
191 Some(curves)
192}
193
194fn intersect_plane_affine_sphere(
199 first: &NurbsSurface,
200 second: &NurbsSurface,
201 tolerance: f64,
202) -> Option<Vec<NurbsCurve>> {
203 for (plane_surface, other) in [(first, second), (second, first)] {
204 let Some(ellipsoid) = other.affine_sphere() else { continue };
205 let Some(plane) = plane_surface.analytic().and_then(plane_data) else { continue };
206 return ellipsoid.plane_section(plane.origin, plane.normal, tolerance);
207 }
208 None
209}
210
211fn intersect_recognized_pair(
212 first: &NurbsSurface,
213 second: &NurbsSurface,
214 tolerance: f64,
215 gate: RuledGate,
216) -> Option<Vec<NurbsCurve>> {
217 let a = first.analytic()?;
218 let b = second.analytic()?;
219 if !matches!(b, AnalyticSurface::Plane { .. }) {
220 if let Some(plane) = plane_data(a) {
221 return intersect_plane_quadric(&plane, b, tolerance, gate);
222 }
223 }
224 if !matches!(a, AnalyticSurface::Plane { .. }) {
225 if let Some(plane) = plane_data(b) {
226 return intersect_plane_quadric(&plane, a, tolerance, gate);
227 }
228 }
229 if let (Some((center_a, radius_a)), Some((center_b, radius_b))) =
232 (a.sphere_geometry(), b.sphere_geometry())
233 {
234 return intersect_sphere_sphere(center_a, radius_a, center_b, radius_b, tolerance);
235 }
236 None
243}
244
245fn ruled_revolution_data(
252 quadric: &AnalyticSurface,
253 tolerance: f64,
254 gate: RuledGate,
255) -> Option<(RevolutionFrame, f64, f64, f64)> {
256 match quadric {
257 AnalyticSurface::RuledRevolution {
258 frame,
259 rho0,
260 rho1,
261 height,
262 } => Some((frame.clone(), *rho0, *rho1, *height)),
263 AnalyticSurface::Revolution {
264 frame, generatrix, ..
265 } => {
266 let (start, end) = match gate {
267 RuledGate::Count => {
268 let controls = &generatrix.control_points;
269 if generatrix.degree != 1
270 || controls.len() != 2
271 || (controls[0].w - 1.0).abs() > RECOGNITION_TOLERANCE
272 || (controls[1].w - 1.0).abs() > RECOGNITION_TOLERANCE
273 {
274 return None;
275 }
276 (controls[0].point().ok()?, controls[1].point().ok()?)
277 }
278 RuledGate::Geometry => generatrix.straight_segment(tolerance)?,
279 };
280 let (_, rho0, z0) = frame.cylindrical(start);
281 let (_, rho1, z1) = frame.cylindrical(end);
282 let height = z1 - z0;
283 if height.abs() <= 1e-12 * (1.0 + rho0.abs().max(rho1.abs())) {
284 return None;
285 }
286 let origin = frame.origin.add(frame.axis.scale(z0));
287 Some((
288 RevolutionFrame {
289 origin,
290 ..frame.clone()
291 },
292 rho0,
293 rho1,
294 height,
295 ))
296 }
297 _ => None,
298 }
299}
300
301pub(crate) fn ruled_gate_census(surface: &NurbsSurface, tolerance: f64) -> Option<String> {
305 let quadric = surface.analytic()?;
306 let AnalyticSurface::Revolution { generatrix, .. } = quadric else {
307 return None;
308 };
309 let count = ruled_revolution_data(quadric, tolerance, RuledGate::Count).is_some();
310 let geometry = ruled_revolution_data(quadric, tolerance, RuledGate::Geometry).is_some();
311 if count == geometry {
312 return None;
313 }
314 let controls = &generatrix.control_points;
315 let points: Vec<Vec3> = controls
316 .iter()
317 .map(|control| control.point().unwrap_or_default())
318 .collect();
319 let (first, last) = (points[0], points[points.len() - 1]);
320 let direction = last.sub(first);
321 let length_squared = direction.length_squared();
322 let mut collinearity = 0.0_f64;
323 for point in &points {
324 collinearity = collinearity.max(if length_squared <= 0.0 {
325 point.sub(first).length()
326 } else {
327 let fraction = (point.sub(first).dot(direction) / length_squared).clamp(0.0, 1.0);
328 point.sub(first.add(direction.scale(fraction))).length()
329 });
330 }
331 let unit_weights = controls
332 .iter()
333 .all(|control| (control.w - 1.0).abs() <= RECOGNITION_TOLERANCE);
334 Some(format!(
335 "degree={} controls={} collinearity={collinearity:.3e} unit_weights={unit_weights} \
336 count_gate={count} geometry_gate={geometry}",
337 generatrix.degree,
338 controls.len()
339 ))
340}
341
342fn degenerate_apex_frustum(rho0: f64, rho1: f64, height: f64) -> bool {
363 if std::env::var("BREP_CONE_APEX_GUARD").as_deref() == Ok("0") {
364 return false;
365 }
366 let delta = rho1 - rho0;
367 delta * delta <= 64.0 * f64::EPSILON * height.abs() * rho0.abs().max(rho1.abs())
368}
369
370pub fn plane_ruled_section_arc(
403 plane_origin: Vec3,
404 plane_normal: Vec3,
405 frame: &RevolutionFrame,
406 rho0: f64,
407 rho1: f64,
408 height: f64,
409 start_azimuth: f64,
410 sweep: f64,
411 tolerance: f64,
412) -> Result<NurbsCurve, String> {
413 let tau = std::f64::consts::TAU;
414 if !(start_azimuth.is_finite() && sweep.is_finite()) {
415 return Err("plane_ruled_section_arc: non-finite azimuth range".into());
416 }
417 if sweep <= 1e-12 || sweep > tau + 1e-9 {
418 return Err(format!(
419 "plane_ruled_section_arc: the section arc sweeps {sweep:.6e} rad, which is not a \
420 positive arc of at most one turn — refusing"
421 ));
422 }
423 let normal = plane_normal.normalized()?;
424 let radius_scale = rho0.abs().max(rho1.abs()).max(1.0);
425 let axis = frame.axis;
426 let is_cylinder = (rho1 - rho0).abs() <= 1e-9 * radius_scale
427 || degenerate_apex_frustum(rho0, rho1, height);
428 let curve = if is_cylinder {
429 let alignment = normal.dot(axis);
430 if alignment.abs() <= 1e-12 {
431 return Err(
432 "plane_ruled_section_arc: the plane is parallel to the cylinder axis, so the \
433 section is a generatrix pair rather than a conic — refusing"
434 .into(),
435 );
436 }
437 if rho0.abs() <= tolerance {
438 return Err("plane_ruled_section_arc: the cylinder carrier has no radius".into());
439 }
440 let base = make_arc(
441 frame.origin,
442 frame.x_axis,
443 frame.y_axis,
444 rho0.abs(),
445 start_azimuth,
446 start_azimuth + sweep,
447 )?;
448 let origin_dot = plane_origin.dot(normal);
449 map_rational_curve(&base, |p| {
450 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / alignment;
451 crate::Vec4 {
452 x: p.x + axis.x * t,
453 y: p.y + axis.y * t,
454 z: p.z + axis.z * t,
455 w: p.w,
456 }
457 })
458 .ok_or_else(|| {
459 "plane_ruled_section_arc: the cylinder section's weights are not one-signed \
460 — refusing"
461 .to_string()
462 })?
463 } else {
464 let apex = frame.origin.add(axis.scale(height * rho0 / (rho0 - rho1)));
465 let k = plane_origin.sub(apex).dot(normal);
466 if k.abs() <= tolerance {
467 return Err(
468 "plane_ruled_section_arc: the plane passes through the cone apex, so the \
469 section is a line pair rather than a conic — refusing"
470 .into(),
471 );
472 }
473 let (reference_rho, reference_z) = if rho0.abs() > rho1.abs() {
474 (rho0.abs(), 0.0)
475 } else {
476 (rho1.abs(), height)
477 };
478 if reference_rho <= tolerance {
479 return Err("plane_ruled_section_arc: the cone carrier degenerates to its apex".into());
480 }
481 let base = make_arc(
482 frame.origin.add(axis.scale(reference_z)),
483 frame.x_axis,
484 frame.y_axis,
485 reference_rho,
486 start_azimuth,
487 start_azimuth + sweep,
488 )?;
489 map_rational_curve(&base, |p| {
490 let relative = Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
491 let w_new = relative.dot(normal);
492 let scaled = relative.scale(k);
493 crate::Vec4 {
494 x: apex.x * w_new + scaled.x,
495 y: apex.y * w_new + scaled.y,
496 z: apex.z * w_new + scaled.z,
497 w: w_new,
498 }
499 })
500 .ok_or_else(|| {
501 "plane_ruled_section_arc: the section crosses the cone's apex plane (a parabolic \
502 or hyperbolic branch, whose weights change sign) — refusing"
503 .to_string()
504 })?
505 };
506 let [d0, d1] = curve.domain()?;
510 let slope = if height == 0.0 {
511 0.0
512 } else {
513 (rho1 - rho0) / height
514 };
515 for step in 0..=8 {
516 let point = curve.evaluate(d0 + (d1 - d0) * (step as f64 / 8.0))?;
517 let delta = point.sub(frame.origin);
518 let axial = delta.dot(axis);
519 let radial = delta.sub(axis.scale(axial)).length();
520 let off_ruled = (radial - (rho0 + slope * axial)).abs();
521 let off_plane = point.sub(plane_origin).dot(normal).abs();
522 if off_ruled > 10.0 * tolerance || off_plane > 10.0 * tolerance {
523 return Err(format!(
524 "plane_ruled_section_arc: the section does not lie on both carriers (off ruled \
525 {off_ruled:.3e}, off plane {off_plane:.3e}) — refusing"
526 ));
527 }
528 }
529 Ok(curve)
530}
531
532fn intersect_plane_quadric(
533 plane: &PlaneData,
534 quadric: &AnalyticSurface,
535 tolerance: f64,
536 gate: RuledGate,
537) -> Option<Vec<NurbsCurve>> {
538 match quadric {
539 AnalyticSurface::RuledRevolution { .. } | AnalyticSurface::Revolution { .. }
540 if ruled_revolution_data(quadric, tolerance, gate).is_some() =>
541 {
542 let (frame, rho0, rho1, height) = ruled_revolution_data(quadric, tolerance, gate)?;
543 let (frame, rho0, rho1, height) = (&frame, &rho0, &rho1, &height);
544 let alignment = plane.normal.dot(frame.axis);
545 if alignment.abs() >= 1.0 - 1e-12 {
546 let z = plane.origin.sub(frame.origin).dot(frame.axis);
548 let t = z / height;
549 if !(-1e-9..=1.0 + 1e-9).contains(&t) {
550 return Some(Vec::new());
551 }
552 let rho = rho0 + (rho1 - rho0) * t.clamp(0.0, 1.0);
553 if rho <= tolerance {
554 return Some(Vec::new());
555 }
556 return Some(vec![frame_circle(frame, rho, z)?]);
557 }
558 if (rho1 - rho0).abs() <= 1e-12 || degenerate_apex_frustum(*rho0, *rho1, *height) {
559 if alignment.abs() <= 1e-12 {
561 return cylinder_parallel_plane_lines(plane, frame, *rho0, *height, tolerance);
564 }
565 let circle = frame_circle(frame, *rho0, 0.0)?;
568 let denominator = alignment;
569 let origin_dot = plane.origin.dot(plane.normal);
570 let normal = plane.normal;
571 let axis = frame.axis;
572 let ellipse = map_rational_curve(&circle, |p| {
573 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / denominator;
574 crate::Vec4 {
575 x: p.x + axis.x * t,
576 y: p.y + axis.y * t,
577 z: p.z + axis.z * t,
578 w: p.w,
579 }
580 })?;
581 if !section_within_band(&ellipse, frame, *height, tolerance) {
582 return Some(Vec::new());
583 }
584 return Some(vec![ellipse]);
585 }
586 let apex_t = rho0 / (rho0 - rho1);
590 let apex = frame.origin.add(frame.axis.scale(height * apex_t));
591 let reference_rho = if rho0.abs() > rho1.abs() {
592 *rho0
593 } else {
594 *rho1
595 };
596 let reference_z = if rho0.abs() > rho1.abs() {
597 0.0
598 } else {
599 *height
600 };
601 let circle = frame_circle(frame, reference_rho, reference_z)?;
602 let k = plane.origin.sub(apex).dot(plane.normal);
603 if k.abs() <= tolerance {
604 return None;
606 }
607 let normal = plane.normal;
608 let conic = map_rational_curve(&circle, |p| {
609 let relative =
610 Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
611 let w_new = relative.dot(normal);
612 let scaled = relative.scale(k);
613 crate::Vec4 {
614 x: apex.x * w_new + scaled.x,
615 y: apex.y * w_new + scaled.y,
616 z: apex.z * w_new + scaled.z,
617 w: w_new,
618 }
619 })?;
620 if !section_within_band(&conic, frame, *height, tolerance) {
621 return Some(Vec::new());
622 }
623 Some(vec![conic])
624 }
625 AnalyticSurface::Sphere { frame, radius } => {
626 plane_sphere_section(plane, frame.origin, *radius, tolerance)
627 }
628 AnalyticSurface::Torus {
629 frame,
630 major_radius,
631 minor_radius,
632 } => plane_torus_section(plane, frame, *major_radius, *minor_radius, tolerance),
633 AnalyticSurface::Plane { .. } => None,
634 AnalyticSurface::RuledRevolution { .. } => None,
635 AnalyticSurface::Revolution { .. } => {
641 if let Some((center, radius)) = quadric.sphere_geometry() {
642 return plane_sphere_section(plane, center, radius, tolerance);
643 }
644 if let Some((frame, major_radius, minor_radius)) = quadric.torus_geometry() {
645 return plane_torus_section(plane, &frame, major_radius, minor_radius, tolerance);
646 }
647 None
648 }
649 }
650}
651
652fn plane_sphere_section(
655 plane: &PlaneData,
656 center: Vec3,
657 radius: f64,
658 tolerance: f64,
659) -> Option<Vec<NurbsCurve>> {
660 let distance = center.sub(plane.origin).dot(plane.normal);
661 if distance.abs() >= radius - tolerance {
662 return Some(Vec::new());
663 }
664 let circle_center = center.sub(plane.normal.scale(distance));
665 let circle_radius = (radius * radius - distance * distance).sqrt();
666 let x_axis = plane.normal.perpendicular().ok()?;
667 let y_axis = plane.normal.cross(x_axis);
668 Some(vec![make_arc(
669 circle_center,
670 x_axis,
671 y_axis,
672 circle_radius,
673 0.0,
674 std::f64::consts::TAU,
675 )
676 .ok()?])
677}
678
679fn plane_torus_section(
684 plane: &PlaneData,
685 frame: &RevolutionFrame,
686 major_radius: f64,
687 minor_radius: f64,
688 tolerance: f64,
689) -> Option<Vec<NurbsCurve>> {
690 let alignment = plane.normal.dot(frame.axis);
691 if alignment.abs() < 1.0 - 1e-12 {
692 return None;
693 }
694 let z = plane.origin.sub(frame.origin).dot(frame.axis);
695 if z.abs() >= minor_radius - tolerance {
696 return Some(Vec::new());
697 }
698 let offset = (minor_radius * minor_radius - z * z).sqrt();
699 let mut circles = Vec::new();
700 for rho in [major_radius - offset, major_radius + offset] {
701 if rho > tolerance {
702 circles.push(frame_circle(frame, rho, z)?);
703 }
704 }
705 Some(circles)
706}
707
708fn section_within_band(
725 curve: &NurbsCurve,
726 frame: &RevolutionFrame,
727 height: f64,
728 tolerance: f64,
729) -> bool {
730 let (band_low, band_high) = if height >= 0.0 {
731 (0.0, height)
732 } else {
733 (height, 0.0)
734 };
735 let margin = tolerance.max(1e-9) + 1e-9;
736 let mut min_z = f64::INFINITY;
737 let mut max_z = f64::NEG_INFINITY;
738 for p in &curve.control_points {
739 let z = Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w)
740 .sub(frame.origin)
741 .dot(frame.axis);
742 min_z = min_z.min(z);
743 max_z = max_z.max(z);
744 }
745 max_z >= band_low - margin && min_z <= band_high + margin
747}
748
749fn cylinder_parallel_plane_lines(
750 plane: &PlaneData,
751 frame: &RevolutionFrame,
752 radius: f64,
753 height: f64,
754 tolerance: f64,
755) -> Option<Vec<NurbsCurve>> {
756 let axis_to_plane = plane.origin.sub(frame.origin).dot(plane.normal);
758 if axis_to_plane.abs() >= radius - tolerance {
759 return Some(Vec::new());
760 }
761 let chord_half = (radius * radius - axis_to_plane * axis_to_plane).sqrt();
763 let chord_direction = frame.axis.cross(plane.normal).normalized().ok()?;
764 let foot = frame.origin.add(plane.normal.scale(axis_to_plane));
765 let mut lines = Vec::new();
766 for sign in [-1.0, 1.0] {
767 let base = foot.add(chord_direction.scale(sign * chord_half));
768 let top = base.add(frame.axis.scale(height));
769 lines.push(crate::make_line(base, top).ok()?);
770 }
771 Some(lines)
772}
773
774fn intersect_sphere_sphere(
775 center_a: Vec3,
776 radius_a: f64,
777 center_b: Vec3,
778 radius_b: f64,
779 tolerance: f64,
780) -> Option<Vec<NurbsCurve>> {
781 let offset = center_b.sub(center_a);
782 let distance = offset.length();
783 if distance <= tolerance {
784 return if (radius_a - radius_b).abs() <= tolerance {
787 None
788 } else {
789 Some(Vec::new())
790 };
791 }
792 if distance >= radius_a + radius_b - tolerance
793 || distance <= (radius_a - radius_b).abs() + tolerance
794 {
795 return Some(Vec::new());
796 }
797 let normal = offset.scale(1.0 / distance);
798 let along =
799 (distance * distance + radius_a * radius_a - radius_b * radius_b) / (2.0 * distance);
800 let circle_radius_squared = radius_a * radius_a - along * along;
801 if circle_radius_squared <= tolerance * tolerance {
802 return Some(Vec::new());
803 }
804 let center = center_a.add(normal.scale(along));
805 let x_axis = normal.perpendicular().ok()?;
806 let y_axis = normal.cross(x_axis);
807 Some(vec![make_arc(
808 center,
809 x_axis,
810 y_axis,
811 circle_radius_squared.sqrt(),
812 0.0,
813 std::f64::consts::TAU,
814 )
815 .ok()?])
816}