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 gate == RuledGate::Geometry && plane_revolution_section_enabled() {
130 if let Some(curves) = intersect_perpendicular_plane_revolution(first, second, tolerance) {
131 return Some(curves);
132 }
133 if let Some(curves) = intersect_perpendicular_plane_revolution(second, first, tolerance) {
134 return Some(curves);
135 }
136 }
137 if let Some(curves) = intersect_axial_plane_revolution(first, second, tolerance) {
138 return Some(curves);
139 }
140 intersect_axial_plane_revolution(second, first, tolerance)
141}
142
143fn plane_revolution_section_enabled() -> bool {
161 crate::switch!("BREP_PLANE_REVOLUTION_SECTION", true)
162}
163
164fn intersect_perpendicular_plane_revolution(
165 plane_surface: &NurbsSurface,
166 revolution: &NurbsSurface,
167 tolerance: f64,
168) -> Option<Vec<NurbsCurve>> {
169 let plane = plane_data(plane_surface.analytic()?)?;
170 if matches!(revolution.analytic(), Some(AnalyticSurface::Plane { .. })) {
171 return None;
172 }
173 let structure = super::revolution::revolution_structure(revolution)?;
174 let frame = &structure.frame;
175 if plane.normal.dot(frame.axis).abs() < 1.0 - 1e-12 {
176 return None;
177 }
178 let generatrix = &structure.generatrix;
179 let [start, end] = generatrix.domain().ok()?;
180 let height = |t: f64| -> Option<f64> { Some(generatrix.evaluate(t).ok()?.sub(plane.origin).dot(plane.normal)) };
181 let scale = surface_scale(revolution).max(1.0);
182 let band = tolerance.max(1e-9 * scale);
183 let spans = generatrix.knots.windows(2).filter(|pair| pair[1] > pair[0]).count().max(1);
184 let stations = 64 * spans;
185 let mut previous_t = start;
186 let mut previous = height(start)?;
187 if previous.abs() <= band || height(end)?.abs() <= band {
189 return None;
190 }
191 let mut roots = Vec::new();
192 for index in 1..=stations {
193 let t = start + (end - start) * index as f64 / stations as f64;
194 let value = height(t)?;
195 if value.abs() <= band && index < stations {
196 return None;
199 }
200 if (previous < 0.0) != (value < 0.0) {
201 let (mut lo, mut hi, mut lo_value) = (previous_t, t, previous);
202 for _ in 0..200 {
203 let middle = 0.5 * (lo + hi);
204 if middle <= lo || middle >= hi {
205 break;
206 }
207 let middle_value = height(middle)?;
208 if (middle_value < 0.0) == (lo_value < 0.0) {
209 lo = middle;
210 lo_value = middle_value;
211 } else {
212 hi = middle;
213 }
214 }
215 roots.push(0.5 * (lo + hi));
216 }
217 previous_t = t;
218 previous = value;
219 }
220 if roots.is_empty() {
221 return None;
222 }
223 let mut coefficient_signs = Vec::new();
228 for control in &generatrix.control_points {
229 if control.w <= 0.0 {
230 return None;
231 }
232 let value = control.point().ok()?.sub(plane.origin).dot(plane.normal);
233 if value.abs() <= band {
234 return None;
235 }
236 coefficient_signs.push(value > 0.0);
237 }
238 let sign_changes = coefficient_signs.windows(2).filter(|pair| pair[0] != pair[1]).count();
239 if sign_changes != roots.len() {
240 return None;
241 }
242 let radius_floor = (tolerance * 10.0).max(1e-9 * scale);
243 let mut curves = Vec::new();
244 for root in roots {
245 let derivatives = generatrix.derivatives(root, 1).ok()?;
246 let tangent = derivatives.get(1)?.normalized().ok()?;
247 if tangent.dot(plane.normal).abs() <= 1e-9 {
248 return None;
249 }
250 let point = generatrix.evaluate(root).ok()?;
251 let (_, radius, axial) = frame.cylindrical(point);
252 if radius <= radius_floor {
253 return None;
254 }
255 let center = frame.origin.add(frame.axis.scale(axial));
256 curves.push(make_arc(center, frame.x_axis, frame.y_axis, radius, 0.0, structure.sweep).ok()?);
257 }
258 Some(curves)
259}
260
261fn intersect_axial_plane_revolution(
267 plane_surface: &NurbsSurface,
268 revolved: &NurbsSurface,
269 _tolerance: f64,
270) -> Option<Vec<NurbsCurve>> {
271 let plane = match plane_surface.analytic()? {
272 AnalyticSurface::Plane {
273 origin,
274 u_dir,
275 v_dir,
276 ..
277 } => (*origin, u_dir.cross(*v_dir).normalized().ok()?),
278 _ => return None,
279 };
280 let structure = revolution_structure(revolved)?;
281 if !generatrix_is_meridional_half_ray(&structure.generatrix, &structure.frame) {
282 return None;
283 }
284 let scale = surface_scale(plane_surface)
285 .max(surface_scale(revolved))
286 .max(1.0);
287 let (plane_origin, plane_normal) = plane;
288 if structure.frame.axis.dot(plane_normal).abs() > 1e-9 {
289 return None;
290 }
291 if structure
292 .frame
293 .origin
294 .sub(plane_origin)
295 .dot(plane_normal)
296 .abs()
297 > 1e-9 * scale
298 {
299 return None;
300 }
301 let radial = structure.frame.axis.cross(plane_normal).normalized().ok()?;
302 let tau = std::f64::consts::TAU;
303 let mut curves = Vec::new();
304 for direction in [radial, radial.scale(-1.0)] {
305 let mut angle = direction
306 .dot(structure.frame.y_axis)
307 .atan2(direction.dot(structure.frame.x_axis));
308 if angle < -1e-9 {
309 angle += tau;
310 }
311 if angle <= structure.sweep + 1e-9 {
312 curves.push(rotate_curve_about_axis(
313 &structure.generatrix,
314 structure.frame.origin,
315 structure.frame.axis,
316 angle.clamp(0.0, structure.sweep),
317 )?);
318 }
319 }
320 Some(curves)
321}
322
323fn intersect_plane_affine_sphere(
328 first: &NurbsSurface,
329 second: &NurbsSurface,
330 tolerance: f64,
331) -> Option<Vec<NurbsCurve>> {
332 for (plane_surface, other) in [(first, second), (second, first)] {
333 let Some(ellipsoid) = other.affine_sphere() else { continue };
334 let Some(plane) = plane_surface.analytic().and_then(plane_data) else { continue };
335 return ellipsoid.plane_section(plane.origin, plane.normal, tolerance);
336 }
337 None
338}
339
340fn intersect_recognized_pair(
341 first: &NurbsSurface,
342 second: &NurbsSurface,
343 tolerance: f64,
344 gate: RuledGate,
345) -> Option<Vec<NurbsCurve>> {
346 let a = first.analytic()?;
347 let b = second.analytic()?;
348 if !matches!(b, AnalyticSurface::Plane { .. }) {
349 if let Some(plane) = plane_data(a) {
350 return intersect_plane_quadric(&plane, b, tolerance, gate);
351 }
352 }
353 if !matches!(a, AnalyticSurface::Plane { .. }) {
354 if let Some(plane) = plane_data(b) {
355 return intersect_plane_quadric(&plane, a, tolerance, gate);
356 }
357 }
358 if let (Some((center_a, radius_a)), Some((center_b, radius_b))) =
361 (a.sphere_geometry(), b.sphere_geometry())
362 {
363 return intersect_sphere_sphere(center_a, radius_a, center_b, radius_b, tolerance);
364 }
365 None
372}
373
374fn ruled_revolution_data(
381 quadric: &AnalyticSurface,
382 tolerance: f64,
383 gate: RuledGate,
384) -> Option<(RevolutionFrame, f64, f64, f64)> {
385 match quadric {
386 AnalyticSurface::RuledRevolution {
387 frame,
388 rho0,
389 rho1,
390 height,
391 } => Some((frame.clone(), *rho0, *rho1, *height)),
392 AnalyticSurface::Revolution {
393 frame, generatrix, ..
394 } => {
395 let (start, end) = match gate {
396 RuledGate::Count => {
397 let controls = &generatrix.control_points;
398 if generatrix.degree != 1
399 || controls.len() != 2
400 || (controls[0].w - 1.0).abs() > RECOGNITION_TOLERANCE
401 || (controls[1].w - 1.0).abs() > RECOGNITION_TOLERANCE
402 {
403 return None;
404 }
405 (controls[0].point().ok()?, controls[1].point().ok()?)
406 }
407 RuledGate::Geometry => generatrix.straight_segment(tolerance)?,
408 };
409 let (_, rho0, z0) = frame.cylindrical(start);
410 let (_, rho1, z1) = frame.cylindrical(end);
411 let height = z1 - z0;
412 if height.abs() <= 1e-12 * (1.0 + rho0.abs().max(rho1.abs())) {
413 return None;
414 }
415 let origin = frame.origin.add(frame.axis.scale(z0));
416 Some((
417 RevolutionFrame {
418 origin,
419 ..frame.clone()
420 },
421 rho0,
422 rho1,
423 height,
424 ))
425 }
426 _ => None,
427 }
428}
429
430pub(crate) fn ruled_gate_census(surface: &NurbsSurface, tolerance: f64) -> Option<String> {
434 let quadric = surface.analytic()?;
435 let AnalyticSurface::Revolution { generatrix, .. } = quadric else {
436 return None;
437 };
438 let count = ruled_revolution_data(quadric, tolerance, RuledGate::Count).is_some();
439 let geometry = ruled_revolution_data(quadric, tolerance, RuledGate::Geometry).is_some();
440 if count == geometry {
441 return None;
442 }
443 let controls = &generatrix.control_points;
444 let points: Vec<Vec3> = controls
445 .iter()
446 .map(|control| control.point().unwrap_or_default())
447 .collect();
448 let (first, last) = (points[0], points[points.len() - 1]);
449 let direction = last.sub(first);
450 let length_squared = direction.length_squared();
451 let mut collinearity = 0.0_f64;
452 for point in &points {
453 collinearity = collinearity.max(if length_squared <= 0.0 {
454 point.sub(first).length()
455 } else {
456 let fraction = (point.sub(first).dot(direction) / length_squared).clamp(0.0, 1.0);
457 point.sub(first.add(direction.scale(fraction))).length()
458 });
459 }
460 let unit_weights = controls
461 .iter()
462 .all(|control| (control.w - 1.0).abs() <= RECOGNITION_TOLERANCE);
463 Some(format!(
464 "degree={} controls={} collinearity={collinearity:.3e} unit_weights={unit_weights} \
465 count_gate={count} geometry_gate={geometry}",
466 generatrix.degree,
467 controls.len()
468 ))
469}
470
471fn degenerate_apex_frustum(rho0: f64, rho1: f64, height: f64) -> bool {
492 if std::env::var("BREP_CONE_APEX_GUARD").as_deref() == Ok("0") {
493 return false;
494 }
495 let delta = rho1 - rho0;
496 delta * delta <= 64.0 * f64::EPSILON * height.abs() * rho0.abs().max(rho1.abs())
497}
498
499pub fn plane_ruled_section_arc(
532 plane_origin: Vec3,
533 plane_normal: Vec3,
534 frame: &RevolutionFrame,
535 rho0: f64,
536 rho1: f64,
537 height: f64,
538 start_azimuth: f64,
539 sweep: f64,
540 tolerance: f64,
541) -> Result<NurbsCurve, String> {
542 let tau = std::f64::consts::TAU;
543 if !(start_azimuth.is_finite() && sweep.is_finite()) {
544 return Err("plane_ruled_section_arc: non-finite azimuth range".into());
545 }
546 if sweep <= 1e-12 || sweep > tau + 1e-9 {
547 return Err(format!(
548 "plane_ruled_section_arc: the section arc sweeps {sweep:.6e} rad, which is not a \
549 positive arc of at most one turn — refusing"
550 ));
551 }
552 let normal = plane_normal.normalized()?;
553 let radius_scale = rho0.abs().max(rho1.abs()).max(1.0);
554 let axis = frame.axis;
555 let is_cylinder = (rho1 - rho0).abs() <= 1e-9 * radius_scale
556 || degenerate_apex_frustum(rho0, rho1, height);
557 let curve = if is_cylinder {
558 let alignment = normal.dot(axis);
559 if alignment.abs() <= 1e-12 {
560 return Err(
561 "plane_ruled_section_arc: the plane is parallel to the cylinder axis, so the \
562 section is a generatrix pair rather than a conic — refusing"
563 .into(),
564 );
565 }
566 if rho0.abs() <= tolerance {
567 return Err("plane_ruled_section_arc: the cylinder carrier has no radius".into());
568 }
569 let base = make_arc(
570 frame.origin,
571 frame.x_axis,
572 frame.y_axis,
573 rho0.abs(),
574 start_azimuth,
575 start_azimuth + sweep,
576 )?;
577 let origin_dot = plane_origin.dot(normal);
578 map_rational_curve(&base, |p| {
579 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / alignment;
580 crate::Vec4 {
581 x: p.x + axis.x * t,
582 y: p.y + axis.y * t,
583 z: p.z + axis.z * t,
584 w: p.w,
585 }
586 })
587 .ok_or_else(|| {
588 "plane_ruled_section_arc: the cylinder section's weights are not one-signed \
589 — refusing"
590 .to_string()
591 })?
592 } else {
593 let apex = frame.origin.add(axis.scale(height * rho0 / (rho0 - rho1)));
594 let k = plane_origin.sub(apex).dot(normal);
595 if k.abs() <= tolerance {
596 return Err(
597 "plane_ruled_section_arc: the plane passes through the cone apex, so the \
598 section is a line pair rather than a conic — refusing"
599 .into(),
600 );
601 }
602 let (reference_rho, reference_z) = if rho0.abs() > rho1.abs() {
603 (rho0.abs(), 0.0)
604 } else {
605 (rho1.abs(), height)
606 };
607 if reference_rho <= tolerance {
608 return Err("plane_ruled_section_arc: the cone carrier degenerates to its apex".into());
609 }
610 let base = make_arc(
611 frame.origin.add(axis.scale(reference_z)),
612 frame.x_axis,
613 frame.y_axis,
614 reference_rho,
615 start_azimuth,
616 start_azimuth + sweep,
617 )?;
618 map_rational_curve(&base, |p| {
619 let relative = Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
620 let w_new = relative.dot(normal);
621 let scaled = relative.scale(k);
622 crate::Vec4 {
623 x: apex.x * w_new + scaled.x,
624 y: apex.y * w_new + scaled.y,
625 z: apex.z * w_new + scaled.z,
626 w: w_new,
627 }
628 })
629 .ok_or_else(|| {
630 "plane_ruled_section_arc: the section crosses the cone's apex plane (a parabolic \
631 or hyperbolic branch, whose weights change sign) — refusing"
632 .to_string()
633 })?
634 };
635 let [d0, d1] = curve.domain()?;
639 let slope = if height == 0.0 {
640 0.0
641 } else {
642 (rho1 - rho0) / height
643 };
644 for step in 0..=8 {
645 let point = curve.evaluate(d0 + (d1 - d0) * (step as f64 / 8.0))?;
646 let delta = point.sub(frame.origin);
647 let axial = delta.dot(axis);
648 let radial = delta.sub(axis.scale(axial)).length();
649 let off_ruled = (radial - (rho0 + slope * axial)).abs();
650 let off_plane = point.sub(plane_origin).dot(normal).abs();
651 if off_ruled > 10.0 * tolerance || off_plane > 10.0 * tolerance {
652 return Err(format!(
653 "plane_ruled_section_arc: the section does not lie on both carriers (off ruled \
654 {off_ruled:.3e}, off plane {off_plane:.3e}) — refusing"
655 ));
656 }
657 }
658 Ok(curve)
659}
660
661fn intersect_plane_quadric(
662 plane: &PlaneData,
663 quadric: &AnalyticSurface,
664 tolerance: f64,
665 gate: RuledGate,
666) -> Option<Vec<NurbsCurve>> {
667 match quadric {
668 AnalyticSurface::RuledRevolution { .. } | AnalyticSurface::Revolution { .. }
669 if ruled_revolution_data(quadric, tolerance, gate).is_some() =>
670 {
671 let (frame, rho0, rho1, height) = ruled_revolution_data(quadric, tolerance, gate)?;
672 let (frame, rho0, rho1, height) = (&frame, &rho0, &rho1, &height);
673 let alignment = plane.normal.dot(frame.axis);
674 if alignment.abs() >= 1.0 - 1e-12 {
675 let z = plane.origin.sub(frame.origin).dot(frame.axis);
677 let t = z / height;
678 if !(-1e-9..=1.0 + 1e-9).contains(&t) {
679 return Some(Vec::new());
680 }
681 let rho = rho0 + (rho1 - rho0) * t.clamp(0.0, 1.0);
682 if rho <= tolerance {
683 return Some(Vec::new());
684 }
685 return Some(vec![frame_circle(frame, rho, z)?]);
686 }
687 if (rho1 - rho0).abs() <= 1e-12 || degenerate_apex_frustum(*rho0, *rho1, *height) {
688 if alignment.abs() <= 1e-12 {
690 return cylinder_parallel_plane_lines(plane, frame, *rho0, *height, tolerance);
693 }
694 let circle = frame_circle(frame, *rho0, 0.0)?;
697 let denominator = alignment;
698 let origin_dot = plane.origin.dot(plane.normal);
699 let normal = plane.normal;
700 let axis = frame.axis;
701 let ellipse = map_rational_curve(&circle, |p| {
702 let t = (origin_dot * p.w - Vec3::new(p.x, p.y, p.z).dot(normal)) / denominator;
703 crate::Vec4 {
704 x: p.x + axis.x * t,
705 y: p.y + axis.y * t,
706 z: p.z + axis.z * t,
707 w: p.w,
708 }
709 })?;
710 if !section_within_band(&ellipse, frame, *height, tolerance) {
711 return Some(Vec::new());
712 }
713 return Some(vec![ellipse]);
714 }
715 let apex_t = rho0 / (rho0 - rho1);
719 let apex = frame.origin.add(frame.axis.scale(height * apex_t));
720 let reference_rho = if rho0.abs() > rho1.abs() {
721 *rho0
722 } else {
723 *rho1
724 };
725 let reference_z = if rho0.abs() > rho1.abs() {
726 0.0
727 } else {
728 *height
729 };
730 let circle = frame_circle(frame, reference_rho, reference_z)?;
731 let k = plane.origin.sub(apex).dot(plane.normal);
732 if k.abs() <= tolerance {
733 return None;
735 }
736 let normal = plane.normal;
737 let conic = map_rational_curve(&circle, |p| {
738 let relative =
739 Vec3::new(p.x - p.w * apex.x, p.y - p.w * apex.y, p.z - p.w * apex.z);
740 let w_new = relative.dot(normal);
741 let scaled = relative.scale(k);
742 crate::Vec4 {
743 x: apex.x * w_new + scaled.x,
744 y: apex.y * w_new + scaled.y,
745 z: apex.z * w_new + scaled.z,
746 w: w_new,
747 }
748 })?;
749 if !section_within_band(&conic, frame, *height, tolerance) {
750 return Some(Vec::new());
751 }
752 Some(vec![conic])
753 }
754 AnalyticSurface::Sphere { frame, radius } => {
755 plane_sphere_section(plane, frame.origin, *radius, tolerance)
756 }
757 AnalyticSurface::Torus {
758 frame,
759 major_radius,
760 minor_radius,
761 } => plane_torus_section(plane, frame, *major_radius, *minor_radius, tolerance),
762 AnalyticSurface::Plane { .. } => None,
763 AnalyticSurface::RuledRevolution { .. } => None,
764 AnalyticSurface::Revolution { .. } => {
770 if let Some((center, radius)) = quadric.sphere_geometry() {
771 return plane_sphere_section(plane, center, radius, tolerance);
772 }
773 if let Some((frame, major_radius, minor_radius)) = quadric.torus_geometry() {
774 return plane_torus_section(plane, &frame, major_radius, minor_radius, tolerance);
775 }
776 None
777 }
778 }
779}
780
781fn plane_sphere_section(
784 plane: &PlaneData,
785 center: Vec3,
786 radius: f64,
787 tolerance: f64,
788) -> Option<Vec<NurbsCurve>> {
789 let distance = center.sub(plane.origin).dot(plane.normal);
790 if distance.abs() >= radius - tolerance {
791 return Some(Vec::new());
792 }
793 let circle_center = center.sub(plane.normal.scale(distance));
794 let circle_radius = (radius * radius - distance * distance).sqrt();
795 let x_axis = plane.normal.perpendicular().ok()?;
796 let y_axis = plane.normal.cross(x_axis);
797 Some(vec![make_arc(
798 circle_center,
799 x_axis,
800 y_axis,
801 circle_radius,
802 0.0,
803 std::f64::consts::TAU,
804 )
805 .ok()?])
806}
807
808fn plane_torus_section(
813 plane: &PlaneData,
814 frame: &RevolutionFrame,
815 major_radius: f64,
816 minor_radius: f64,
817 tolerance: f64,
818) -> Option<Vec<NurbsCurve>> {
819 let alignment = plane.normal.dot(frame.axis);
820 if alignment.abs() < 1.0 - 1e-12 {
821 return None;
822 }
823 let z = plane.origin.sub(frame.origin).dot(frame.axis);
824 if z.abs() >= minor_radius - tolerance {
825 return Some(Vec::new());
826 }
827 let offset = (minor_radius * minor_radius - z * z).sqrt();
828 let mut circles = Vec::new();
829 for rho in [major_radius - offset, major_radius + offset] {
830 if rho > tolerance {
831 circles.push(frame_circle(frame, rho, z)?);
832 }
833 }
834 Some(circles)
835}
836
837fn section_within_band(
854 curve: &NurbsCurve,
855 frame: &RevolutionFrame,
856 height: f64,
857 tolerance: f64,
858) -> bool {
859 let (band_low, band_high) = if height >= 0.0 {
860 (0.0, height)
861 } else {
862 (height, 0.0)
863 };
864 let margin = tolerance.max(1e-9) + 1e-9;
865 let mut min_z = f64::INFINITY;
866 let mut max_z = f64::NEG_INFINITY;
867 for p in &curve.control_points {
868 let z = Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w)
869 .sub(frame.origin)
870 .dot(frame.axis);
871 min_z = min_z.min(z);
872 max_z = max_z.max(z);
873 }
874 max_z >= band_low - margin && min_z <= band_high + margin
876}
877
878fn cylinder_parallel_plane_lines(
879 plane: &PlaneData,
880 frame: &RevolutionFrame,
881 radius: f64,
882 height: f64,
883 tolerance: f64,
884) -> Option<Vec<NurbsCurve>> {
885 let axis_to_plane = plane.origin.sub(frame.origin).dot(plane.normal);
887 if axis_to_plane.abs() >= radius - tolerance {
888 return Some(Vec::new());
889 }
890 let chord_half = (radius * radius - axis_to_plane * axis_to_plane).sqrt();
892 let chord_direction = frame.axis.cross(plane.normal).normalized().ok()?;
893 let foot = frame.origin.add(plane.normal.scale(axis_to_plane));
894 let mut lines = Vec::new();
895 for sign in [-1.0, 1.0] {
896 let base = foot.add(chord_direction.scale(sign * chord_half));
897 let top = base.add(frame.axis.scale(height));
898 lines.push(crate::make_line(base, top).ok()?);
899 }
900 Some(lines)
901}
902
903fn intersect_sphere_sphere(
904 center_a: Vec3,
905 radius_a: f64,
906 center_b: Vec3,
907 radius_b: f64,
908 tolerance: f64,
909) -> Option<Vec<NurbsCurve>> {
910 let offset = center_b.sub(center_a);
911 let distance = offset.length();
912 if distance <= tolerance {
913 return if (radius_a - radius_b).abs() <= tolerance {
916 None
917 } else {
918 Some(Vec::new())
919 };
920 }
921 if distance >= radius_a + radius_b - tolerance
922 || distance <= (radius_a - radius_b).abs() + tolerance
923 {
924 return Some(Vec::new());
925 }
926 let normal = offset.scale(1.0 / distance);
927 let along =
928 (distance * distance + radius_a * radius_a - radius_b * radius_b) / (2.0 * distance);
929 let circle_radius_squared = radius_a * radius_a - along * along;
930 if circle_radius_squared <= tolerance * tolerance {
931 return Some(Vec::new());
932 }
933 let center = center_a.add(normal.scale(along));
934 let x_axis = normal.perpendicular().ok()?;
935 let y_axis = normal.cross(x_axis);
936 Some(vec![make_arc(
937 center,
938 x_axis,
939 y_axis,
940 circle_radius_squared.sqrt(),
941 0.0,
942 std::f64::consts::TAU,
943 )
944 .ok()?])
945}