1use axiolid_brep::ExactBRep;
68use axiolid_core::{Interval, Point2, Point3, Scalar, Tolerance, Vec3};
69use axiolid_curve::Curve3;
70use axiolid_evaluate::surface::{locate, normal};
71use axiolid_evaluate::{curve::locate3, evaluate3};
72use axiolid_measure::FaceDomain;
73use axiolid_nurbs::{
74 exact_curve_curve_intersection3, exact_curve_surface_intersection, exact_surface_intersection,
75 spline_pair_intersection, ExactCurveIntersection, ExactIntersectionRefusal,
76};
77use axiolid_surface::{Plane, Surface};
78use axiolid_topology::FaceId;
79use core::f64::consts::TAU;
80
81use crate::bounds::{edge_box, face_box, Aabb};
82use crate::contact::{Contact, Contacts};
83use crate::predicate;
84use crate::report::{self, Reading, ToleranceDecisionKind};
85use crate::support::{cleaned, same_support, window};
86use crate::BooleanError;
87
88const SAMPLE: Scalar = 0.414_213_562_373_095;
91
92const WINDOW_PAD: Scalar = 1.0 / 1_048_576.0;
98
99#[derive(Debug, Clone, PartialEq)]
101pub struct SectionEdge {
102 pub face_a: FaceId,
104 pub face_b: FaceId,
106 pub curve: Curve3,
108 pub span: Interval,
111 pub start: Point3,
113 pub end: Point3,
115 pub along_a: bool,
118 pub along_b: bool,
120 pub other_a: Surface,
125 pub other_b: Surface,
127}
128
129pub fn section_edges(
136 a: &ExactBRep,
137 b: &ExactBRep,
138 tolerance: Tolerance,
139) -> Result<Vec<SectionEdge>, BooleanError> {
140 let session = report::open(&[a, b], tolerance);
141 let (edges, _) = sections_and_contacts(a, b, tolerance)?;
142 session.finish()?;
143 Ok(edges)
144}
145
146pub(crate) fn sections_and_contacts(
150 a: &ExactBRep,
151 b: &ExactBRep,
152 tolerance: Tolerance,
153) -> Result<(Vec<SectionEdge>, Contacts), BooleanError> {
154 let side_a = Side::new(a, tolerance)?;
155 let side_b = Side::new(b, tolerance)?;
156 let mut out = Vec::new();
157 let (readings, contacts) = plane_cylinder_readings(&side_a, &side_b, tolerance)?;
161 #[allow(clippy::type_complexity)]
163 let mut closed_forms: Vec<(
164 Surface,
165 Surface,
166 Result<axiolid_nurbs::ExactIntersectionCurve, ExactIntersectionRefusal>,
167 )> = Vec::new();
168 for fa in 0..side_a.faces.len() {
169 for fb in 0..side_b.faces.len() {
170 let common = match (&side_a.boxes[fa], &side_b.boxes[fb]) {
179 (Some(a), Some(b)) => match a.intersection(b) {
180 Some(common) => Some(common.widened(WINDOW_PAD * common.diagonal())),
181 None => continue,
182 },
183 _ => None,
184 };
185 let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
186 if same_support(sa, sb, tolerance) {
187 imprint(
188 &side_a, fa, &side_b, fb, true, &contacts, tolerance, &mut out,
189 )?;
190 imprint(
191 &side_b, fb, &side_a, fa, false, &contacts, tolerance, &mut out,
192 )?;
193 continue;
194 }
195 let splines = matches!((sa, sb), (Surface::BSpline(_), Surface::BSpline(_)));
199 let rulings = readings
202 .iter()
203 .find(|(x, y, _)| x == sa && y == sb)
204 .and_then(|(_, _, reading)| reading.as_ref())
205 .map(|reading| reading.curves.clone());
206 if rulings.as_ref().is_some_and(Vec::is_empty) {
207 continue;
208 }
209 let closed_form = if splines || rulings.is_some() {
211 None
212 } else {
213 let known = closed_forms
214 .iter()
215 .find(|(a, b, _)| a == sa && b == sb)
216 .map(|(_, _, r)| r.clone());
217 let result = known.unwrap_or_else(|| {
218 let r = exact_surface_intersection(&cleaned(sa), &cleaned(sb));
219 closed_forms.push((sa.clone(), sb.clone(), r.clone()));
220 r
221 });
222 match result {
223 Ok(curve) => {
224 let conic = curve.branches.iter().zip(&curve.spans).all(|(b, s)| {
225 matches!(
226 (b, s),
227 (Curve3::Line(_), _)
228 | (Curve3::Circle(_) | Curve3::Ellipse(_), None)
229 )
230 });
231 conic.then_some(curve)
232 }
233 Err(
235 ExactIntersectionRefusal::Disjoint
236 | ExactIntersectionRefusal::NotRegularCurve,
237 ) => continue,
238 Err(_) => None,
239 }
240 };
241 let branches: Vec<(Curve3, Option<Interval>)> = match (rulings, closed_form) {
242 (Some(lines), _) => lines.into_iter().map(|line| (line, None)).collect(),
243 (None, Some(curve)) => curve.branches.into_iter().zip(curve.spans).collect(),
244 (None, None) => {
245 let rank = |s: &Surface| match s {
249 Surface::BSpline(_) => 4,
250 Surface::Torus(_) => 3,
251 Surface::Sphere(_) => 2,
252 Surface::Plane(_) => 0,
253 _ => 1,
254 };
255 if let (Surface::BSpline(ba), Surface::BSpline(bb)) = (sa, sb) {
258 let (la, ha) = side_a.domains[fa].bounds();
259 let (lb, hb) = side_b.domains[fb].bounds();
260 match spline_pair_intersection(
261 ba,
262 bb,
263 Some((window(sa, la, ha), window(sb, lb, hb))),
264 ) {
265 Ok(sections) => sections
266 .into_iter()
267 .map(|s| {
268 let end = s.end();
269 (Curve3::PairSection(s), Some(Interval::new(0.0, end)))
270 })
271 .collect(),
272 Err(ExactIntersectionRefusal::Disjoint) => continue,
273 Err(_) => return Err(BooleanError::UnsupportedSection),
274 }
275 } else {
276 let (carrier, other, side, face) = if rank(sa) >= rank(sb) {
277 (sa, sb, &side_a, fa)
278 } else {
279 (sb, sa, &side_b, fb)
280 };
281 let (lo, hi) = side.domains[face].bounds();
282 match crate::tangency::trace(carrier, other, lo, hi) {
283 Ok(sections) => sections
284 .into_iter()
285 .map(|s| {
286 let end = s.curve.end();
287 (Curve3::ImplicitSection(s), Some(Interval::new(0.0, end)))
288 })
289 .collect(),
290 Err(
293 ExactIntersectionRefusal::Disjoint
294 | ExactIntersectionRefusal::NotRegularCurve,
295 ) => continue,
296 Err(_) => return Err(BooleanError::UnsupportedSection),
297 }
298 }
299 }
300 };
301 for (index, (branch, span)) in branches.iter().enumerate() {
302 let bounds = match (branch, span) {
305 (Curve3::Line(_), Some(span)) => Some(*span),
306 _ => None,
307 };
308 let branch = branch.clone();
309 let branch = &branch;
310 let common = common.filter(|_| {
313 matches!(
314 branch,
315 Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
316 )
317 });
318 let (mut cuts, along_a) =
319 side_a.cuts(fa, branch, sb, common.as_ref(), &contacts, tolerance)?;
320 let (more, along_b) =
321 side_b.cuts(fb, branch, sa, common.as_ref(), &contacts, tolerance)?;
322 cuts.extend(more);
323 for (other, _) in branches
327 .iter()
328 .skip(index + 1)
329 .chain(branches.iter().take(index))
330 {
331 if let Ok(ExactCurveIntersection::Points(hits)) =
332 exact_curve_curve_intersection3(branch, other)
333 {
334 cuts.extend(hits.iter().map(|h| h.parameter.approx()));
335 }
336 }
337 if let Some(b) = bounds {
338 let (lo, hi) = (b.start.min(b.end), b.start.max(b.end));
339 cuts.retain(|&c| c >= lo && c <= hi);
340 cuts.extend([lo, hi].into_iter().filter(|x| x.is_finite()));
341 }
342 let mut cuts = merge_close(branch, cuts, tolerance)?;
343 if let Some(w) = &common {
346 let ends = window_ends(&cuts, w.crossings(branch));
347 cuts.extend(ends.into_iter().filter(|&c| {
348 bounds.is_none_or(|b| c >= b.start.min(b.end) && c <= b.start.max(b.end))
349 }));
350 }
351 for (piece_curve, span) in pieces(branch, cuts)? {
352 let mid = sample(&piece_curve, span, [sa, sb], &contacts)?;
355 let traced = matches!(
360 piece_curve,
361 Curve3::ImplicitSection(_) | Curve3::PairSection(_)
362 );
363 if common.as_ref().is_some_and(|w| !w.contains(mid)) {
365 continue;
366 }
367 if !traced && touching(sa, sb, mid, tolerance)? {
368 continue;
369 }
370 let at_a = side_a.locate(fa, mid, &along_a, tolerance)?;
371 if at_a == Place::Outside {
372 continue;
373 }
374 let at_b = side_b.locate(fb, mid, &along_b, tolerance)?;
375 if at_b == Place::Outside {
376 continue;
377 }
378 let edges: Vec<&(Curve3, Interval)> = along_a
381 .iter()
382 .filter(|_| at_a == Place::Boundary)
383 .chain(along_b.iter().filter(|_| at_b == Place::Boundary))
384 .collect();
385 let (piece_curve, span) =
386 along_edge_curve(piece_curve, span, mid, &edges, tolerance)?;
387 out.push(SectionEdge {
388 face_a: side_a.faces[fa],
389 face_b: side_b.faces[fb],
390 start: evaluate3(&piece_curve, span.start)
391 .map_err(|_| BooleanError::Evaluation)?,
392 end: evaluate3(&piece_curve, span.end)
393 .map_err(|_| BooleanError::Evaluation)?,
394 curve: piece_curve,
395 span,
396 along_a: at_a == Place::Boundary,
397 along_b: at_b == Place::Boundary,
398 other_a: sb.clone(),
399 other_b: sa.clone(),
400 });
401 }
402 }
403 }
404 }
405 Ok((out, contacts))
406}
407
408#[allow(clippy::type_complexity)]
413fn plane_cylinder_readings(
414 side_a: &Side<'_>,
415 side_b: &Side<'_>,
416 tolerance: Tolerance,
417) -> Result<(Vec<(Surface, Surface, Option<PlaneCylinder>)>, Contacts), BooleanError> {
418 let mut extents: Vec<(Surface, Surface, Option<Scalar>)> = Vec::new();
419 for fa in 0..side_a.faces.len() {
420 for fb in 0..side_b.faces.len() {
421 let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
422 if !matches!(
423 (sa, sb),
424 (Surface::Plane(_), Surface::Cylinder(_))
425 | (Surface::Cylinder(_), Surface::Plane(_))
426 ) {
427 continue;
428 }
429 let extent = match (&side_a.boxes[fa], &side_b.boxes[fb]) {
430 (Some(a), Some(b)) => match a.intersection(b) {
431 Some(common) => Some(common.diagonal()),
432 None => continue,
433 },
434 _ => None,
435 };
436 match extents.iter_mut().find(|(x, y, _)| x == sa && y == sb) {
437 Some((_, _, known)) => *known = known.zip(extent).map(|(p, q)| p.max(q)),
438 None => extents.push((sa.clone(), sb.clone(), extent)),
439 }
440 }
441 }
442 let mut contacts = Contacts::default();
443 let mut readings = Vec::with_capacity(extents.len());
444 for (sa, sb, extent) in extents {
445 let reading = plane_cylinder_within_rounding(&sa, &sb, extent, tolerance);
446 if let Some(PlaneCylinder {
447 curves,
448 contact: true,
449 }) = &reading
450 {
451 if let [Curve3::Line(ruling)] = curves.as_slice() {
452 let (plane, cylinder) = match sa {
453 Surface::Plane(_) => (sa.clone(), sb.clone()),
454 _ => (sb.clone(), sa.clone()),
455 };
456 contacts.push(Contact {
457 plane,
458 cylinder,
459 ruling: *ruling,
460 });
461 }
462 }
463 readings.push((sa, sb, reading));
464 }
465 Ok((readings, contacts))
466}
467
468fn sample(
475 curve: &Curve3,
476 span: Interval,
477 surfaces: [&Surface; 2],
478 contacts: &Contacts,
479) -> Result<Point3, BooleanError> {
480 let at = |f: Scalar| {
481 evaluate3(curve, span.start + f * (span.end - span.start))
482 .map_err(|_| BooleanError::Evaluation)
483 };
484 let disputed = |p: Point3| surfaces.iter().any(|s| contacts.disputed(s, p));
485 let first = at(SAMPLE)?;
486 if !disputed(first) {
487 return Ok(first);
488 }
489 for f in [0.75, 0.9, 0.25, 0.1] {
490 let p = at(f)?;
491 if !disputed(p) {
492 return Ok(p);
493 }
494 }
495 Ok(first)
496}
497
498fn plane_cylinder_within_rounding(
533 a: &Surface,
534 b: &Surface,
535 extent: Option<Scalar>,
536 tolerance: Tolerance,
537) -> Option<PlaneCylinder> {
538 let (cylinder, plane) = match (a, b) {
539 (Surface::Cylinder(c), Surface::Plane(p)) | (Surface::Plane(p), Surface::Cylinder(c)) => {
540 (c, p)
541 }
542 _ => return None,
543 };
544 let axis = cylinder.frame.z.normalize_or_zero();
545 let normal = plane.frame.z.normalize_or_zero();
546 if axis == Vec3::ZERO || normal == Vec3::ZERO {
547 return None;
548 }
549 let along = axis.dot(normal);
550 let linear = Tolerance::new(tolerance.linear(), Scalar::MAX).unwrap_or(tolerance);
555 match report::support(
556 ToleranceDecisionKind::PlanePerpendicularToAxis,
557 cylinder.radius * axis.cross(normal).length(),
558 0.0,
559 linear,
560 || predicate::plane_perpendicular(plane, cylinder),
561 ) {
562 Reading::Exact => return None,
563 Reading::Apart => {}
564 Reading::Within => {
565 let centre = cylinder.frame.origin
568 + axis * (normal.dot(plane.frame.origin - cylinder.frame.origin) / along);
569 let x = (cylinder.frame.x - axis * axis.dot(cylinder.frame.x)).normalize_or_zero();
570 if x == Vec3::ZERO || !centre.is_finite() {
571 return None;
572 }
573 return Some(PlaneCylinder {
574 curves: vec![Curve3::Circle(axiolid_curve::Circle3 {
575 frame: axiolid_core::Frame3 {
576 origin: centre,
577 x,
578 y: axis.cross(x),
579 z: axis,
580 },
581 radius: cylinder.radius,
582 })],
583 contact: false,
584 });
585 }
586 }
587 let extent = extent?;
589 let parallel = report::support(
590 ToleranceDecisionKind::PlaneParallelToAxis,
591 along.abs() * extent,
592 0.0,
593 linear,
594 || predicate::plane_parallel(plane, cylinder),
595 );
596 if parallel == Reading::Apart {
597 return None;
598 }
599 let normal = (normal - axis * along).normalize_or_zero();
601 if normal == Vec3::ZERO {
602 return None;
603 }
604 let distance = normal.dot(cylinder.frame.origin - plane.frame.origin);
605 let foot = cylinder.frame.origin - normal * distance;
606 let across = axis.cross(normal);
607 let r = cylinder.radius;
608 let touching = report::support(
609 ToleranceDecisionKind::PlaneTouchesCylinder,
610 (distance.abs() - r).abs(),
611 0.0,
612 linear,
613 || predicate::plane_touches(plane, cylinder),
614 );
615 if parallel == Reading::Exact && touching != Reading::Within {
618 return None;
619 }
620 let offsets: Vec<Scalar> = if touching.holds() {
621 vec![0.0]
622 } else if distance.abs() > r {
623 Vec::new()
624 } else {
625 let half = (r * r - distance * distance).sqrt();
626 vec![half, -half]
627 };
628 Some(PlaneCylinder {
629 curves: offsets
630 .into_iter()
631 .map(|offset| {
632 Curve3::Line(axiolid_curve::Line3 {
633 origin: foot + across * offset,
634 direction: axis,
635 })
636 })
637 .collect(),
638 contact: touching == Reading::Within,
639 })
640}
641
642#[derive(Debug, Clone)]
645struct PlaneCylinder {
646 curves: Vec<Curve3>,
649 contact: bool,
654}
655
656fn poles(surface: &Surface) -> Vec<Point3> {
659 match surface {
660 Surface::Sphere(s) => {
661 let z = s.frame.z.normalize() * s.radius;
662 vec![s.frame.origin + z, s.frame.origin - z]
663 }
664 Surface::Cone(c) => {
665 let slope = c.semi_angle.tan();
666 if slope == 0.0 {
667 Vec::new()
668 } else {
669 vec![c.frame.origin - c.frame.z.normalize() * (c.radius / slope)]
670 }
671 }
672 _ => Vec::new(),
673 }
674}
675
676fn touching(
681 a: &Surface,
682 b: &Surface,
683 point: Point3,
684 tolerance: Tolerance,
685) -> Result<bool, BooleanError> {
686 let at = |s: &Surface| -> Result<axiolid_core::Vec3, BooleanError> {
687 let (u, v) =
688 locate(s, point, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
689 Ok(normal(s, u, v)
690 .map_err(|_| BooleanError::Evaluation)?
691 .normalize())
692 };
693 Ok(report::near_angle(
694 ToleranceDecisionKind::Contact,
695 at(a)?.cross(at(b)?).length(),
696 tolerance,
697 ))
698}
699
700#[allow(clippy::too_many_arguments)]
704fn imprint(
705 from: &Side<'_>,
706 face: usize,
707 onto: &Side<'_>,
708 other: usize,
709 first: bool,
710 contacts: &Contacts,
711 tolerance: Tolerance,
712 out: &mut Vec<SectionEdge>,
713) -> Result<(), BooleanError> {
714 for (edge, curve, span) in from.edges_of(face)? {
715 let bounding = from.cutter(face, edge, &curve, span, tolerance, false)?;
718 let (cuts, along) = onto.cuts(other, &curve, &bounding, None, contacts, tolerance)?;
719 let (cuts, span) = imprint_cuts(&curve, span, cuts, tolerance)?;
720 let shared = onto.surface(other)?;
721 for piece in pieces_within(&curve, span, cuts) {
722 let mid = sample(&curve, piece, [shared, &bounding], contacts)?;
723 let at = onto.locate(other, mid, &along, tolerance)?;
724 if at == Place::Outside {
725 continue;
726 }
727 let (face_a, face_b, along_a, along_b) = if first {
728 (
729 from.faces[face],
730 onto.faces[other],
731 true,
732 at == Place::Boundary,
733 )
734 } else {
735 (
736 onto.faces[other],
737 from.faces[face],
738 at == Place::Boundary,
739 true,
740 )
741 };
742 out.push(SectionEdge {
743 face_a,
744 face_b,
745 curve: curve.clone(),
746 span: piece,
747 start: evaluate3(&curve, piece.start).map_err(|_| BooleanError::Evaluation)?,
748 end: evaluate3(&curve, piece.end).map_err(|_| BooleanError::Evaluation)?,
749 along_a,
750 along_b,
751 other_a: bounding.clone(),
752 other_b: bounding.clone(),
753 });
754 }
755 }
756 Ok(())
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761enum Place {
762 Inside,
763 Boundary,
764 Outside,
765}
766
767fn near_crossing(
777 curve: &Curve3,
778 edge: &Curve3,
779 tolerance: Tolerance,
780) -> Result<Option<Scalar>, BooleanError> {
781 let conic_plane = |c: &Curve3| match c {
782 Curve3::Circle(c) => Some(c.frame),
783 Curve3::Ellipse(e) => Some(e.frame),
784 _ => None,
785 };
786 let point = match (curve, edge) {
787 (Curve3::Line(a), Curve3::Line(b)) => {
788 let (d1, d2) = (a.direction, b.direction);
789 let r = a.origin - b.origin;
790 let (aa, bb, ab) = (d1.dot(d1), d2.dot(d2), d1.dot(d2));
791 let denominator = aa * bb - ab * ab;
792 if denominator <= 1e-12 * aa * bb {
793 return Ok(None);
794 }
795 let (c, f) = (d1.dot(r), d2.dot(r));
796 let s = (ab * f - c * bb) / denominator;
797 let t = (aa * f - ab * c) / denominator;
798 let (p, q) = (a.origin + d1 * s, b.origin + d2 * t);
799 if !report::near(
800 ToleranceDecisionKind::TangentCrossing,
801 (p - q).length(),
802 tolerance,
803 ) {
804 return Ok(None);
805 }
806 return Ok(Some(s));
807 }
808 (Curve3::Line(line), conic) | (conic, Curve3::Line(line)) => {
809 let Some(frame) = conic_plane(conic) else {
810 return Ok(None);
811 };
812 let n = frame.z.normalize();
813 let along = line.direction.dot(n);
814 if along.abs() <= 1e-9 * line.direction.length() {
815 return Ok(None);
816 }
817 let s = (frame.origin - line.origin).dot(n) / along;
818 line.origin + line.direction * s
819 }
820 _ => return Ok(None),
821 };
822 let conic = if matches!(curve, Curve3::Line(_)) {
824 edge
825 } else {
826 curve
827 };
828 let wide = report::floored(tolerance);
829 let Ok(t) = locate3(conic, point, wide) else {
830 return Ok(None);
831 };
832 let on = evaluate3(conic, t).map_err(|_| BooleanError::Evaluation)?;
833 if !report::near(
834 ToleranceDecisionKind::TangentCrossing,
835 (on - point).length(),
836 tolerance,
837 ) {
838 return Ok(None);
839 }
840 match locate3(curve, point, wide) {
841 Ok(t) => Ok(Some(t)),
842 Err(_) => Ok(None),
843 }
844}
845
846fn merge_close(
866 curve: &Curve3,
867 cuts: Vec<Scalar>,
868 tolerance: Tolerance,
869) -> Result<Vec<Scalar>, BooleanError> {
870 let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
871 if !(periodic || matches!(curve, Curve3::Line(_) | Curve3::ImplicitSection(_))) {
872 return Ok(cuts);
873 }
874 let eps = tolerance.linear().max(report::rounding());
877 let merged = |gap: Scalar| report::near(ToleranceDecisionKind::MergedPoints, gap, tolerance);
878 let at = |t: Scalar| evaluate3(curve, t).map_err(|_| BooleanError::Evaluation);
879 let mut placed: Vec<(Scalar, Point3)> = Vec::with_capacity(cuts.len());
880 for cut in cuts {
881 if !cut.is_finite() {
882 continue;
883 }
884 placed.push((cut, at(cut)?));
885 }
886 placed.sort_by(|a, b| a.0.total_cmp(&b.0));
887 let mut clusters: Vec<(Scalar, Scalar, Scalar, Point3, Point3)> = Vec::new();
890 for (cut, point) in placed {
891 match clusters.last_mut() {
892 Some((sum, count, start, first, last)) if (point - *last).length() <= eps => {
893 if !merged((point - *first).length()) {
894 return Err(BooleanError::NearCoincidence);
895 }
896 let cut = if periodic {
900 cut + TAU * ((*start - cut) / TAU).round()
901 } else {
902 cut
903 };
904 *sum += cut;
905 *count += 1.0;
906 *last = point;
907 }
908 _ => clusters.push((cut, 1.0, cut, point, point)),
909 }
910 }
911 let mut out: Vec<Scalar> = clusters
912 .iter()
913 .map(|(sum, count, _, _, _)| sum / count)
914 .collect();
915 if periodic && clusters.len() >= 2 {
918 let (first, last) = (at(out[0])?, at(out[out.len() - 1])?);
919 if merged((first - last).length()) {
920 out.pop();
921 }
922 }
923 Ok(out)
924}
925
926fn imprint_cuts(
955 curve: &Curve3,
956 span: Interval,
957 cuts: Vec<Scalar>,
958 tolerance: Tolerance,
959) -> Result<(Vec<Scalar>, Interval), BooleanError> {
960 let cuts = merge_close(curve, cuts, tolerance)?;
961 let at = |t: Scalar| evaluate3(curve, t).map_err(|_| BooleanError::Evaluation);
962 let ends = [at(span.start)?, at(span.end)?];
963 let (mut lo, mut hi) = (span.start.min(span.end), span.start.max(span.end));
964 let mut out = Vec::with_capacity(cuts.len());
965 for cut in cuts {
966 let point = at(cut)?;
967 let gap = ends
968 .iter()
969 .map(|end| (point - *end).length())
970 .fold(Scalar::INFINITY, Scalar::min);
971 let ends = report::imprint_end_tolerance(tolerance);
972 if !report::near(ToleranceDecisionKind::MergedPoints, gap, ends) {
973 out.push(cut);
974 } else if gap > report::rounding() {
975 report::merged_an_end();
976 if matches!(curve, Curve3::Line(_)) && report::extends_ends() {
980 lo = lo.min(cut);
981 hi = hi.max(cut);
982 }
983 }
984 }
985 let span = if span.start <= span.end {
986 Interval::new(lo, hi)
987 } else {
988 Interval::new(hi, lo)
989 };
990 Ok((out, span))
991}
992
993fn pieces_within(curve: &Curve3, span: Interval, cuts: Vec<Scalar>) -> Vec<Interval> {
995 let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
996 let slack = 1e-12 * (1.0 + lo.abs().max(hi.abs()));
997 let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
998 let mut inside = vec![lo, hi];
999 for cut in cuts {
1000 let candidates: &[Scalar] = if periodic {
1001 &[cut - TAU, cut, cut + TAU, cut + 2.0 * TAU]
1002 } else {
1003 &[cut]
1004 };
1005 for &c in candidates {
1006 if c > lo + slack && c < hi - slack {
1007 inside.push(c);
1008 }
1009 }
1010 }
1011 inside.sort_by(Scalar::total_cmp);
1012 inside.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
1013 inside
1014 .windows(2)
1015 .map(|pair| Interval::new(pair[0], pair[1]))
1016 .collect()
1017}
1018
1019fn same_cut(x: Scalar, y: Scalar) -> bool {
1024 (x - y).abs() <= 1e-12 * (1.0 + x.abs().max(y.abs()))
1025}
1026
1027fn window_ends(cuts: &[Scalar], crossings: Vec<Scalar>) -> Vec<Scalar> {
1033 crossings
1034 .into_iter()
1035 .filter(|&c| !cuts.iter().any(|&f| same_cut(c, f)))
1036 .collect()
1037}
1038
1039fn pieces(curve: &Curve3, mut cuts: Vec<Scalar>) -> Result<Vec<(Curve3, Interval)>, BooleanError> {
1045 cuts.sort_by(Scalar::total_cmp);
1046 cuts.dedup_by(|x, y| same_cut(*x, *y));
1047 let plain = |spans: Vec<Interval>| spans.into_iter().map(|s| (curve.clone(), s)).collect();
1048 Ok(match curve {
1049 Curve3::Line(_) => plain(
1050 cuts.windows(2)
1051 .map(|pair| Interval::new(pair[0], pair[1]))
1052 .collect(),
1053 ),
1054 Curve3::ImplicitSection(section) => {
1055 let n = section.curve.end();
1056 let (pu, pv) = section.carrier.periodic();
1057 let closure = section.curve.closure(pu, pv);
1058 let slack = 1e-9 * (1.0 + n);
1059 let at_start = cuts.iter().any(|&c| c <= slack || c >= n - slack);
1063 let mut inner: Vec<Scalar> = cuts
1064 .into_iter()
1065 .filter(|&c| c > slack && c < n - slack)
1066 .collect();
1067 inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
1068 let closure = closure.filter(|_| !at_start);
1069 let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
1070 let sub = if (a - b).abs() <= slack {
1072 closure.and_then(|c| section.curve.rotated(a, c))
1073 } else {
1074 section.curve.sub(a, b, closure)
1075 };
1076 let sub = sub.ok_or(BooleanError::Evaluation)?;
1077 let end = sub.end();
1078 Ok((
1079 Curve3::ImplicitSection(axiolid_curve::ImplicitSection3 {
1080 carrier: section.carrier.clone(),
1081 curve: sub,
1082 }),
1083 Interval::new(0.0, end),
1084 ))
1085 };
1086 if closure.is_some() {
1087 if inner.is_empty() {
1088 return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
1089 }
1090 let mut out = Vec::new();
1091 for pair in inner.windows(2) {
1092 out.push(own(pair[0], pair[1])?);
1093 }
1094 out.push(own(inner[inner.len() - 1], inner[0])?);
1096 out
1097 } else {
1098 let mut ends = vec![0.0];
1099 ends.extend(inner);
1100 ends.push(n);
1101 let mut out = Vec::new();
1102 for pair in ends.windows(2) {
1103 out.push(own(pair[0], pair[1])?);
1104 }
1105 out
1106 }
1107 }
1108 Curve3::PairSection(section) => {
1109 let n = section.end();
1110 let slack = 1e-9 * (1.0 + n);
1111 let mut inner: Vec<Scalar> = cuts
1112 .into_iter()
1113 .filter(|&c| c > slack && c < n - slack)
1114 .collect();
1115 inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
1116 let (first, last) = (
1117 section.nodes[0].point,
1118 section.nodes[section.nodes.len() - 1].point,
1119 );
1120 let closed = first == last;
1121 let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
1122 let sub = if a < b {
1123 section.sub(a, b)
1124 } else {
1125 section
1127 .sub(a, n)
1128 .zip(section.sub(0.0, b))
1129 .map(|(mut x, y)| {
1130 x.nodes.extend(y.nodes.into_iter().skip(1));
1131 x
1132 })
1133 };
1134 let sub = sub.ok_or(BooleanError::Evaluation)?;
1135 let end = sub.end();
1136 Ok((Curve3::PairSection(sub), Interval::new(0.0, end)))
1137 };
1138 let mut out = Vec::new();
1139 if closed {
1140 if inner.is_empty() {
1141 return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
1142 }
1143 for pair in inner.windows(2) {
1144 out.push(own(pair[0], pair[1])?);
1145 }
1146 out.push(own(inner[inner.len() - 1], inner[0])?);
1147 } else {
1148 let mut ends = vec![0.0];
1149 ends.extend(inner);
1150 ends.push(n);
1151 for pair in ends.windows(2) {
1152 out.push(own(pair[0], pair[1])?);
1153 }
1154 }
1155 out
1156 }
1157 _ => {
1158 if cuts.is_empty() {
1159 return Ok(vec![(curve.clone(), Interval::new(0.0, TAU))]);
1160 }
1161 let mut out: Vec<Interval> = cuts
1162 .windows(2)
1163 .map(|pair| Interval::new(pair[0], pair[1]))
1164 .collect();
1165 out.push(Interval::new(cuts[cuts.len() - 1], cuts[0] + TAU));
1166 plain(out)
1167 }
1168 })
1169}
1170
1171struct Side<'a> {
1173 brep: &'a ExactBRep,
1174 faces: Vec<FaceId>,
1175 domains: Vec<FaceDomain<'a>>,
1176 boxes: Vec<Option<Aabb>>,
1178 edge_boxes: Vec<Option<Aabb>>,
1180 edge_faces: Vec<Vec<usize>>,
1182}
1183
1184impl<'a> Side<'a> {
1185 fn new(brep: &'a ExactBRep, tolerance: Tolerance) -> Result<Self, BooleanError> {
1186 let topology = brep.topology();
1187 let mut faces = Vec::with_capacity(topology.faces().len());
1188 let mut domains = Vec::with_capacity(topology.faces().len());
1189 for index in 0..topology.faces().len() {
1190 let face = topology
1191 .face_id_at(index)
1192 .ok_or(BooleanError::DanglingReference)?;
1193 let domain = FaceDomain::new(brep, face, tolerance)
1194 .map_err(BooleanError::Measure)?
1195 .ok_or(BooleanError::UnsupportedTrim)?;
1196 faces.push(face);
1197 domains.push(domain);
1198 }
1199 let mut edge_faces = vec![Vec::new(); topology.edges().len()];
1200 for (index, face) in topology.faces().iter().enumerate() {
1201 for bound in &face.bounds {
1202 let wire = topology
1203 .loops()
1204 .get(bound.loop_id.index())
1205 .ok_or(BooleanError::DanglingReference)?;
1206 for use_ in &wire.edges {
1207 edge_faces[use_.edge.index()].push(index);
1208 }
1209 }
1210 }
1211 let mut boxes = Vec::with_capacity(domains.len());
1212 for (index, domain) in domains.iter().enumerate() {
1213 let surface = topology.faces()[index]
1214 .surface
1215 .and_then(|id| brep.surfaces().get(id.index()))
1216 .ok_or(BooleanError::DanglingReference)?;
1217 let (lo, hi) = domain.bounds();
1218 boxes.push(face_box(surface, lo, hi, tolerance));
1219 }
1220 let mut edge_boxes = Vec::with_capacity(topology.edges().len());
1221 for (index, edge) in topology.edges().iter().enumerate() {
1222 let curve = edge.curve.and_then(|id| brep.curves3().get(id.index()));
1223 let span = topology
1224 .edge_id_at(index)
1225 .and_then(|id| brep.edge_interval(id));
1226 edge_boxes.push(match (curve, span) {
1227 (Some(curve), Some(span)) => edge_box(curve, span, tolerance),
1228 _ => None,
1229 });
1230 }
1231 Ok(Self {
1232 brep,
1233 faces,
1234 domains,
1235 boxes,
1236 edge_boxes,
1237 edge_faces,
1238 })
1239 }
1240
1241 fn surface(&self, face: usize) -> Result<&'a Surface, BooleanError> {
1242 self.brep.topology().faces()[face]
1243 .surface
1244 .and_then(|id| self.brep.surfaces().get(id.index()))
1245 .ok_or(BooleanError::DanglingReference)
1246 }
1247
1248 fn locate(
1251 &self,
1252 face: usize,
1253 point: Point3,
1254 along: &[(Curve3, Interval)],
1255 tolerance: Tolerance,
1256 ) -> Result<Place, BooleanError> {
1257 for (curve, span) in along {
1258 if on_edge(curve, *span, point, tolerance)? {
1259 return Ok(Place::Boundary);
1260 }
1261 }
1262 let (u, v) = locate(self.surface(face)?, point, report::floored(tolerance))
1263 .map_err(|_| BooleanError::Evaluation)?;
1264 match self.domains[face]
1265 .contains(Point2::new(u, v))
1266 .map_err(BooleanError::Measure)?
1267 {
1268 Some(true) => Ok(Place::Inside),
1269 Some(false) => Ok(Place::Outside),
1270 None => Err(BooleanError::Undecided),
1271 }
1272 }
1273
1274 fn edges_of(&self, face: usize) -> Result<Vec<(usize, Curve3, Interval)>, BooleanError> {
1276 let topology = self.brep.topology();
1277 let mut out = Vec::new();
1278 let mut seen = Vec::new();
1279 for bound in &topology.faces()[face].bounds {
1280 let wire = topology
1281 .loops()
1282 .get(bound.loop_id.index())
1283 .ok_or(BooleanError::DanglingReference)?;
1284 for use_ in &wire.edges {
1285 if seen.contains(&use_.edge) {
1286 continue;
1287 }
1288 seen.push(use_.edge);
1289 let curve = topology.edges()[use_.edge.index()]
1290 .curve
1291 .and_then(|id| self.brep.curves3().get(id.index()))
1292 .ok_or(BooleanError::DanglingReference)?;
1293 let span = self
1294 .brep
1295 .edge_interval(use_.edge)
1296 .ok_or(BooleanError::DanglingReference)?;
1297 out.push((use_.edge.index(), curve.clone(), span));
1298 }
1299 }
1300 Ok(out)
1301 }
1302
1303 #[allow(clippy::type_complexity)]
1307 fn cuts(
1323 &self,
1324 face: usize,
1325 curve: &Curve3,
1326 meets: &Surface,
1327 window: Option<&Aabb>,
1328 contacts: &Contacts,
1329 tolerance: Tolerance,
1330 ) -> Result<(Vec<Scalar>, Vec<(Curve3, Interval)>), BooleanError> {
1331 let topology = self.brep.topology();
1332 let mut out = Vec::new();
1333 let mut along = Vec::new();
1334 let mut seen = Vec::new();
1335 for pole in poles(self.surface(face)?) {
1338 if let Ok(t) = locate3(curve, pole, report::floored(tolerance)) {
1339 let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1340 if report::near(
1341 ToleranceDecisionKind::IncidentPoint,
1342 (on - pole).length(),
1343 tolerance,
1344 ) {
1345 out.push(t);
1346 }
1347 }
1348 }
1349 for bound in &topology.faces()[face].bounds {
1350 let wire = topology
1351 .loops()
1352 .get(bound.loop_id.index())
1353 .ok_or(BooleanError::DanglingReference)?;
1354 for use_ in &wire.edges {
1355 if seen.contains(&use_.edge) {
1356 continue;
1357 }
1358 seen.push(use_.edge);
1359 if let (Some(w), Some(b)) = (window, &self.edge_boxes[use_.edge.index()]) {
1360 if !w.overlaps(b) {
1361 continue;
1362 }
1363 }
1364 let edge = &topology.edges()[use_.edge.index()];
1365 let edge_curve = edge
1366 .curve
1367 .and_then(|id| self.brep.curves3().get(id.index()))
1368 .ok_or(BooleanError::DanglingReference)?;
1369 let span = self
1370 .brep
1371 .edge_interval(use_.edge)
1372 .ok_or(BooleanError::DanglingReference)?;
1373 let mut cutter =
1374 self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, false)?;
1375 let mut result = exact_curve_surface_intersection(curve, &cutter);
1376 if matches!(result, Ok(ExactCurveIntersection::Contained)) {
1377 if let Ok(seam) =
1381 self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, true)
1382 {
1383 cutter = seam;
1384 result = exact_curve_surface_intersection(curve, &cutter);
1385 }
1386 }
1387 if !matches!(result, Ok(ExactCurveIntersection::Contained))
1393 && runs_along(curve, edge_curve, span, tolerance)?
1394 {
1395 result = Ok(ExactCurveIntersection::Contained);
1396 }
1397 if !matches!(result, Ok(ExactCurveIntersection::Contained)) {
1398 let own = self.surface(face)?;
1399 if let Some(hits) = crate::tangency::smooth_edge_cuts(
1403 own, &cutter, meets, edge_curve, span, curve, contacts, tolerance,
1404 )? {
1405 out.extend(hits);
1406 continue;
1407 }
1408 if let Some(hits) =
1409 contacts.crossing([own, meets], &cutter, curve, tolerance)?
1410 {
1411 for t in hits {
1412 let point =
1413 evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1414 if on_edge(edge_curve, span, point, tolerance)? {
1415 out.push(t);
1416 }
1417 }
1418 continue;
1419 }
1420 }
1421 match result {
1422 Ok(ExactCurveIntersection::Points(hits)) => {
1423 let before = out.len();
1424 for hit in hits {
1425 if on_edge(edge_curve, span, hit.point, tolerance)? {
1426 out.push(hit.parameter.approx());
1427 }
1428 }
1429 if out.len() == before {
1435 if let Some(t) = near_crossing(curve, edge_curve, tolerance)? {
1436 let point =
1437 evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1438 if on_edge(edge_curve, span, point, tolerance)? {
1439 out.push(t);
1440 }
1441 }
1442 }
1443 }
1444 Ok(ExactCurveIntersection::Contained) => {
1448 for t in [span.start, span.end] {
1449 let end =
1450 evaluate3(edge_curve, t).map_err(|_| BooleanError::Evaluation)?;
1451 if let Ok(s) = locate3(curve, end, report::floored(tolerance)) {
1452 let on =
1453 evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
1454 if report::near(
1455 ToleranceDecisionKind::IncidentPoint,
1456 (on - end).length(),
1457 tolerance,
1458 ) {
1459 out.push(s);
1460 }
1461 }
1462 }
1463 along.push((edge_curve.clone(), span));
1464 }
1465 Ok(_) => return Err(BooleanError::UnsupportedSection),
1466 Err(_) => {
1467 let hits = match exact_curve_surface_intersection(edge_curve, meets) {
1468 Ok(ExactCurveIntersection::Points(hits)) => hits,
1469 _ => return Err(BooleanError::UnsupportedTrim),
1470 };
1471 for hit in hits {
1472 if !on_span(edge_curve, span, hit.point, tolerance)? {
1473 continue;
1474 }
1475 if let Ok(s) = locate3(curve, hit.point, report::floored(tolerance)) {
1476 let on =
1477 evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
1478 if report::near(
1479 ToleranceDecisionKind::IncidentPoint,
1480 (on - hit.point).length(),
1481 tolerance,
1482 ) {
1483 out.push(s);
1484 }
1485 }
1486 }
1487 }
1488 }
1489 }
1490 }
1491 Ok((out, along))
1492 }
1493}
1494
1495impl Side<'_> {
1496 fn cutter(
1500 &self,
1501 face: usize,
1502 edge: usize,
1503 edge_curve: &Curve3,
1504 span: Interval,
1505 tolerance: Tolerance,
1506 across_seam: bool,
1507 ) -> Result<Surface, BooleanError> {
1508 let own = self.surface(face)?;
1509 let adjacent = self.edge_faces[edge]
1510 .iter()
1511 .copied()
1512 .find(|&other| other != face);
1513 if let (Some(other), false) = (adjacent, across_seam) {
1514 let theirs = self.surface(other)?;
1515 if !same_support(own, theirs, tolerance) {
1516 return Ok(theirs.clone());
1517 }
1518 }
1519 if let Curve3::Circle(circle) = edge_curve {
1525 return normal_sweep(own, circle, tolerance);
1526 }
1527 let Curve3::Line(line) = edge_curve else {
1528 return Err(BooleanError::UnsupportedTrim);
1529 };
1530 let mid = evaluate3(edge_curve, 0.5 * (span.start + span.end))
1531 .map_err(|_| BooleanError::Evaluation)?;
1532 let (u, v) =
1533 locate(own, mid, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1534 let n = normal(own, u, v).map_err(|_| BooleanError::Evaluation)?;
1535 let across = line.direction.cross(n);
1536 let length = across.length();
1537 if length == 0.0 || !length.is_finite() {
1538 return Err(BooleanError::UnsupportedTrim);
1539 }
1540 let z = across / length;
1541 let x = line.direction.normalize();
1542 Ok(Surface::Plane(Plane {
1543 frame: axiolid_core::Frame3 {
1544 origin: mid,
1545 x,
1546 y: z.cross(x),
1547 z,
1548 },
1549 }))
1550 }
1551}
1552
1553fn normal_sweep(
1558 surface: &Surface,
1559 circle: &axiolid_curve::Circle3,
1560 tolerance: Tolerance,
1561) -> Result<Surface, BooleanError> {
1562 let f = circle.frame;
1563 let axis = f.x.cross(f.y).normalize();
1564 let x = f.x.normalize();
1565 let y = axis.cross(x);
1566 let p = f.origin + x * circle.radius;
1567 let (u, v) =
1568 locate(surface, p, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1569 let n = normal(surface, u, v)
1570 .map_err(|_| BooleanError::Evaluation)?
1571 .normalize();
1572 let frame = axiolid_core::Frame3 {
1573 origin: f.origin,
1574 x,
1575 y,
1576 z: axis,
1577 };
1578 let along = n.dot(axis);
1579 let radial = n.dot(x);
1580 let plane = || {
1581 Surface::Plane(Plane {
1582 frame: axiolid_core::Frame3 {
1583 origin: f.origin,
1584 x,
1585 y,
1586 z: axis,
1587 },
1588 })
1589 };
1590 if along.abs() <= 1e-12 {
1592 return Ok(plane());
1593 }
1594 if radial.abs() <= 1e-12 {
1596 return Ok(Surface::Cylinder(axiolid_surface::Cylinder {
1597 frame,
1598 radius: circle.radius,
1599 }));
1600 }
1601 Ok(Surface::Cone(axiolid_surface::Cone {
1604 frame,
1605 radius: circle.radius,
1606 semi_angle: (radial / along).atan(),
1607 }))
1608}
1609
1610fn runs_along(
1621 curve: &Curve3,
1622 edge: &Curve3,
1623 span: Interval,
1624 tolerance: Tolerance,
1625) -> Result<bool, BooleanError> {
1626 let along =
1627 |residue: Scalar| report::near(ToleranceDecisionKind::SectionAlongEdge, residue, tolerance);
1628 match (curve, edge) {
1629 (Curve3::Line(line), Curve3::Line(_)) => {
1630 let d = line.direction.normalize_or_zero();
1631 if d == Vec3::ZERO {
1632 return Ok(false);
1633 }
1634 let mut residue: Scalar = 0.0;
1635 for t in [span.start, span.end] {
1636 let p = evaluate3(edge, t).map_err(|_| BooleanError::Evaluation)?;
1637 let off = p - line.origin;
1638 residue = residue.max((off - d * off.dot(d)).length());
1639 }
1640 Ok(along(residue))
1641 }
1642 (Curve3::Circle(a), Curve3::Circle(b)) => {
1643 let (na, nb) = (a.frame.z.normalize_or_zero(), b.frame.z.normalize_or_zero());
1644 if na == Vec3::ZERO || nb == Vec3::ZERO {
1645 return Ok(false);
1646 }
1647 let tilt = 2.0 * na.cross(nb).length();
1648 Ok(along(
1649 (a.frame.origin - b.frame.origin).length()
1650 + (a.radius - b.radius).abs()
1651 + a.radius * tilt,
1652 ))
1653 }
1654 _ => Ok(false),
1655 }
1656}
1657
1658fn along_edge_curve(
1676 curve: Curve3,
1677 span: Interval,
1678 sample: Point3,
1679 edges: &[&(Curve3, Interval)],
1680 tolerance: Tolerance,
1681) -> Result<(Curve3, Interval), BooleanError> {
1682 if !matches!(curve, Curve3::ImplicitSection(_) | Curve3::PairSection(_)) {
1683 return Ok((curve, span));
1684 }
1685 let at = |c: &Curve3, t: Scalar| evaluate3(c, t).map_err(|_| BooleanError::Evaluation);
1686 let (start, end) = (at(&curve, span.start)?, at(&curve, span.end)?);
1687 for (edge, edge_span) in edges.iter().copied() {
1688 let periodic = matches!(edge, Curve3::Circle(_) | Curve3::Ellipse(_));
1689 if !(periodic || matches!(edge, Curve3::Line(_)))
1690 || !on_edge(edge, *edge_span, sample, tolerance)?
1691 {
1692 continue;
1693 }
1694 let locate = |p: Point3| {
1695 locate3(edge, p, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)
1696 };
1697 let (ts, te, tm) = (locate(start)?, locate(end)?, locate(sample)?);
1698 let adopted = if periodic {
1699 let up = |t: Scalar| t - TAU * ((t - ts) / TAU).ceil() + TAU;
1702 let (te_up, tm_up) = (up(te), up(tm));
1703 if tm_up < te_up {
1704 Interval::new(ts, te_up)
1705 } else {
1706 Interval::new(te_up - TAU, ts)
1707 }
1708 } else {
1709 Interval::new(ts.min(te), ts.max(te))
1710 };
1711 let residue = (at(edge, ts)? - start)
1712 .length()
1713 .max((at(edge, te)? - end).length());
1714 if !report::near(ToleranceDecisionKind::SectionAlongEdge, residue, tolerance) {
1715 return Ok((curve, span));
1716 }
1717 return Ok((edge.clone(), adopted));
1718 }
1719 Ok((curve, span))
1720}
1721
1722fn on_edge(
1724 curve: &Curve3,
1725 span: Interval,
1726 point: Point3,
1727 tolerance: Tolerance,
1728) -> Result<bool, BooleanError> {
1729 let Ok(t) = locate3(curve, point, report::floored(tolerance)) else {
1730 return Ok(false);
1731 };
1732 let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
1733 if !on_span(curve, span, point, tolerance)? {
1734 return Ok(false);
1735 }
1736 Ok(report::near(
1737 ToleranceDecisionKind::IncidentPoint,
1738 (on - point).length(),
1739 tolerance,
1740 ))
1741}
1742
1743pub(crate) fn on_span(
1746 curve: &Curve3,
1747 span: Interval,
1748 point: Point3,
1749 tolerance: Tolerance,
1750) -> Result<bool, BooleanError> {
1751 let t =
1752 locate3(curve, point, report::floored(tolerance)).map_err(|_| BooleanError::Evaluation)?;
1753 let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
1754 let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
1755 let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
1756 let candidates: &[Scalar] = if periodic {
1757 &[t - TAU, t, t + TAU, t + 2.0 * TAU]
1758 } else {
1759 &[t]
1760 };
1761 Ok(candidates
1762 .iter()
1763 .any(|c| *c >= lo - slack && *c <= hi + slack))
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769 use axiolid_core::Frame3;
1770 use axiolid_curve::{Circle3, Line3};
1771
1772 const EPS: Scalar = 1e-6;
1773
1774 #[test]
1775 fn a_window_end_never_replaces_a_cut() {
1776 let line = Curve3::Line(Line3 {
1779 origin: Point3::new(600000.2, 0.0, 203.5),
1780 direction: -Vec3::Y,
1781 });
1782 let cuts = vec![-5600000.1, -5599999.9];
1783 let ends = window_ends(&cuts, vec![-5599999.8999988, -5600000.1000012]);
1784 assert!(ends.is_empty());
1785 let spans: Vec<Interval> = pieces(&line, cuts)
1786 .expect("pieces")
1787 .into_iter()
1788 .map(|p| p.1)
1789 .collect();
1790 assert_eq!(spans, vec![Interval::new(-5600000.1, -5599999.9)]);
1791 let kept = window_ends(
1793 &[-0.1, 0.1],
1794 vec![0.10000119073677866, -0.10000119073677866],
1795 );
1796 assert_eq!(kept.len(), 2);
1797 }
1798
1799 fn tol() -> Tolerance {
1800 Tolerance::METRE
1801 }
1802
1803 fn x_axis() -> Curve3 {
1804 Curve3::Line(Line3 {
1805 origin: Point3::ZERO,
1806 direction: Vec3::X,
1807 })
1808 }
1809
1810 #[test]
1811 fn cuts_within_tolerance_merge_and_a_longer_chain_is_refused() {
1812 let line = x_axis();
1813 let merged = merge_close(&line, vec![1.0, 1.0 + 0.5 * EPS, 2.0], tol()).unwrap();
1814 assert_eq!(merged.len(), 2);
1815 assert!((merged[0] - (1.0 + 0.25 * EPS)).abs() < 1e-15);
1816 let kept = merge_close(&line, vec![1.0, 1.0 + 10.0 * EPS], tol()).unwrap();
1818 assert_eq!(kept.len(), 2);
1819 let chain = merge_close(&line, vec![1.0, 1.0 + 0.6 * EPS, 1.0 + 1.2 * EPS], tol());
1821 assert_eq!(chain, Err(BooleanError::NearCoincidence));
1822 }
1823
1824 #[test]
1825 fn cuts_at_a_circles_seam_from_either_side_merge_at_the_seam() {
1826 let circle = Curve3::Circle(Circle3 {
1830 frame: Frame3 {
1831 origin: Point3::ZERO,
1832 x: Vec3::X,
1833 y: Vec3::Y,
1834 z: Vec3::Z,
1835 },
1836 radius: 1.0,
1837 });
1838 let merged = merge_close(&circle, vec![0.0, TAU, TAU], tol()).unwrap();
1839 assert_eq!(merged.len(), 1);
1840 let at = evaluate3(&circle, merged[0]).unwrap();
1841 assert!((at - Point3::X).length() < 1e-12, "{at:?}");
1842 let merged = merge_close(&circle, vec![TAU - 0.2 * EPS, 0.2 * EPS], tol()).unwrap();
1844 assert_eq!(merged.len(), 1);
1845 let at = evaluate3(&circle, merged[0]).unwrap();
1846 assert!((at - Point3::X).length() < 0.25 * EPS, "{at:?}");
1847 }
1848
1849 #[test]
1850 fn an_edge_runs_along_a_curve_only_when_proved_within_tolerance() {
1851 let line = x_axis();
1852 let edge = |dy: Scalar| {
1853 Curve3::Line(Line3 {
1854 origin: Point3::new(0.0, dy, 0.0),
1855 direction: Vec3::new(1.0, dy, 0.0),
1856 })
1857 };
1858 let span = Interval::new(0.0, 1.0);
1859 assert!(runs_along(&line, &edge(0.5 * EPS), span, tol()).unwrap());
1861 assert!(!runs_along(&line, &edge(5.0 * EPS), span, tol()).unwrap());
1863 let circle = |r: Scalar, tilt: Scalar| {
1864 Curve3::Circle(Circle3 {
1865 frame: Frame3 {
1866 origin: Point3::ZERO,
1867 x: Vec3::X,
1868 y: Vec3::new(0.0, tilt.cos(), tilt.sin()),
1869 z: Vec3::new(0.0, -tilt.sin(), tilt.cos()),
1870 },
1871 radius: r,
1872 })
1873 };
1874 let whole = Interval::new(0.0, core::f64::consts::TAU);
1875 assert!(runs_along(
1876 &circle(1.0, 0.0),
1877 &circle(1.0 + 0.5 * EPS, 0.0),
1878 whole,
1879 tol()
1880 )
1881 .unwrap());
1882 assert!(!runs_along(&circle(1.0, 0.0), &circle(1.0, 10.0 * EPS), whole, tol()).unwrap());
1883 assert!(!runs_along(&line, &circle(1.0, 0.0), whole, tol()).unwrap());
1885 }
1886
1887 #[test]
1888 fn a_plane_reads_parallel_to_an_axis_only_within_tolerance_over_its_extent() {
1889 let cylinder = Surface::Cylinder(axiolid_surface::Cylinder {
1890 frame: Frame3 {
1891 origin: Point3::ZERO,
1892 x: Vec3::X,
1893 y: Vec3::Y,
1894 z: Vec3::Z,
1895 },
1896 radius: 1.0,
1897 });
1898 let plane = |tilt: Scalar, offset: Scalar| {
1899 let z = Vec3::new(1.0, 0.0, tilt).normalize();
1900 Surface::Plane(Plane {
1901 frame: Frame3 {
1902 origin: Point3::new(offset, 0.0, 0.0),
1903 x: Vec3::Y,
1904 y: z.cross(Vec3::Y),
1905 z,
1906 },
1907 })
1908 };
1909 let rulings =
1912 plane_cylinder_within_rounding(&plane(1e-9, 0.0), &cylinder, Some(10.0), tol());
1913 assert_eq!(
1914 rulings.map(|r| (r.curves.len(), r.contact)),
1915 Some((2, false))
1916 );
1917 assert!(
1919 plane_cylinder_within_rounding(&plane(1e-6, 0.0), &cylinder, Some(10.0), tol())
1920 .is_none()
1921 );
1922 assert!(
1924 plane_cylinder_within_rounding(&plane(1e-9, 0.0), &cylinder, None, tol()).is_none()
1925 );
1926 let touching = plane_cylinder_within_rounding(
1928 &plane(0.0, 1.0 + 0.5 * EPS),
1929 &cylinder,
1930 Some(10.0),
1931 tol(),
1932 );
1933 assert_eq!(
1934 touching.map(|r| (r.curves.len(), r.contact)),
1935 Some((1, true))
1936 );
1937 for offset in [1.0 + 10.0 * EPS, 0.0] {
1941 assert!(plane_cylinder_within_rounding(
1942 &plane(0.0, offset),
1943 &cylinder,
1944 Some(10.0),
1945 tol()
1946 )
1947 .is_none());
1948 }
1949 }
1950
1951 #[test]
1952 fn only_readings_the_exact_predicates_do_not_confirm_are_reported() {
1953 let cylinder = Surface::Cylinder(axiolid_surface::Cylinder {
1954 frame: Frame3 {
1955 origin: Point3::ZERO,
1956 x: Vec3::X,
1957 y: Vec3::Y,
1958 z: Vec3::Z,
1959 },
1960 radius: 1.0,
1961 });
1962 let plane = |z: Vec3, x: Vec3| {
1963 Surface::Plane(Plane {
1964 frame: Frame3 {
1965 origin: Point3::new(0.0, 0.0, 0.5),
1966 x,
1967 y: z.cross(x),
1968 z,
1969 },
1970 })
1971 };
1972 let session = report::open(&[], tol());
1974 assert!(plane_cylinder_within_rounding(
1975 &plane(Vec3::Z, Vec3::X),
1976 &cylinder,
1977 Some(10.0),
1978 tol()
1979 )
1980 .is_none());
1981 assert!(session.finish().expect("exact").is_exact());
1982 let session = report::open(&[], tol());
1985 let tilted = Vec3::new(0.0, (core::f64::consts::FRAC_PI_2).cos(), 1.0);
1986 let circle =
1987 plane_cylinder_within_rounding(&plane(tilted, Vec3::X), &cylinder, Some(10.0), tol());
1988 assert_eq!(
1989 circle.map(|c| (c.curves.len(), c.contact)),
1990 Some((1, false))
1991 );
1992 let report = session.finish().expect("within tolerance");
1993 assert!(report.contains(ToleranceDecisionKind::PlanePerpendicularToAxis));
1994 assert!(report.linear() > 0.0 && report.linear() <= EPS);
1995 }
1996}