1use axiolid_brep::ExactBRep;
37use axiolid_core::{Interval, Point2, Point3, Scalar, Tolerance};
38use axiolid_curve::Curve3;
39use axiolid_evaluate::surface::{locate, normal};
40use axiolid_evaluate::{curve::locate3, evaluate3};
41use axiolid_measure::FaceDomain;
42use axiolid_nurbs::{
43 exact_curve_curve_intersection3, exact_curve_surface_intersection, exact_surface_intersection,
44 implicit_surface_intersection, spline_pair_intersection, ExactCurveIntersection,
45 ExactIntersectionRefusal,
46};
47use axiolid_surface::{Plane, Surface};
48use axiolid_topology::FaceId;
49use core::f64::consts::TAU;
50
51use crate::support::{cleaned, same_support, window};
52use crate::BooleanError;
53
54const SAMPLE: Scalar = 0.414_213_562_373_095;
57
58#[derive(Debug, Clone, PartialEq)]
60pub struct SectionEdge {
61 pub face_a: FaceId,
63 pub face_b: FaceId,
65 pub curve: Curve3,
67 pub span: Interval,
70 pub start: Point3,
72 pub end: Point3,
74 pub along_a: bool,
77 pub along_b: bool,
79 pub other_a: Surface,
84 pub other_b: Surface,
86}
87
88pub fn section_edges(
95 a: &ExactBRep,
96 b: &ExactBRep,
97 tolerance: Tolerance,
98) -> Result<Vec<SectionEdge>, BooleanError> {
99 let side_a = Side::new(a, tolerance)?;
100 let side_b = Side::new(b, tolerance)?;
101 let mut out = Vec::new();
102 #[allow(clippy::type_complexity)]
104 let mut closed_forms: Vec<(
105 Surface,
106 Surface,
107 Result<axiolid_nurbs::ExactIntersectionCurve, ExactIntersectionRefusal>,
108 )> = Vec::new();
109 for fa in 0..side_a.faces.len() {
110 for fb in 0..side_b.faces.len() {
111 let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
112 if same_support(sa, sb, tolerance) {
113 imprint(&side_a, fa, &side_b, fb, true, tolerance, &mut out)?;
114 imprint(&side_b, fb, &side_a, fa, false, tolerance, &mut out)?;
115 continue;
116 }
117 let splines = matches!((sa, sb), (Surface::BSpline(_), Surface::BSpline(_)));
121 let closed_form = if splines {
123 None
124 } else {
125 let known = closed_forms
126 .iter()
127 .find(|(a, b, _)| a == sa && b == sb)
128 .map(|(_, _, r)| r.clone());
129 let result = known.unwrap_or_else(|| {
130 let r = exact_surface_intersection(&cleaned(sa), &cleaned(sb));
131 closed_forms.push((sa.clone(), sb.clone(), r.clone()));
132 r
133 });
134 match result {
135 Ok(curve) => {
136 let conic = curve.branches.iter().zip(&curve.spans).all(|(b, s)| {
137 matches!(
138 (b, s),
139 (Curve3::Line(_), _)
140 | (Curve3::Circle(_) | Curve3::Ellipse(_), None)
141 )
142 });
143 conic.then_some(curve)
144 }
145 Err(
147 ExactIntersectionRefusal::Disjoint
148 | ExactIntersectionRefusal::NotRegularCurve,
149 ) => continue,
150 Err(_) => None,
151 }
152 };
153 let branches: Vec<(Curve3, Option<Interval>)> = match closed_form {
154 Some(curve) => curve.branches.into_iter().zip(curve.spans).collect(),
155 None => {
156 let rank = |s: &Surface| match s {
160 Surface::BSpline(_) => 4,
161 Surface::Torus(_) => 3,
162 Surface::Sphere(_) => 2,
163 Surface::Plane(_) => 0,
164 _ => 1,
165 };
166 if let (Surface::BSpline(ba), Surface::BSpline(bb)) = (sa, sb) {
169 let (la, ha) = side_a.domains[fa].bounds();
170 let (lb, hb) = side_b.domains[fb].bounds();
171 match spline_pair_intersection(
172 ba,
173 bb,
174 Some((window(sa, la, ha), window(sb, lb, hb))),
175 ) {
176 Ok(sections) => sections
177 .into_iter()
178 .map(|s| {
179 let end = s.end();
180 (Curve3::PairSection(s), Some(Interval::new(0.0, end)))
181 })
182 .collect(),
183 Err(ExactIntersectionRefusal::Disjoint) => continue,
184 Err(_) => return Err(BooleanError::UnsupportedSection),
185 }
186 } else {
187 let (carrier, other, side, face) = if rank(sa) >= rank(sb) {
188 (sa, sb, &side_a, fa)
189 } else {
190 (sb, sa, &side_b, fb)
191 };
192 let (lo, hi) = side.domains[face].bounds();
193 match implicit_surface_intersection(
194 carrier,
195 other,
196 Some(window(carrier, lo, hi)),
197 ) {
198 Ok(sections) => sections
199 .into_iter()
200 .map(|s| {
201 let end = s.curve.end();
202 (Curve3::ImplicitSection(s), Some(Interval::new(0.0, end)))
203 })
204 .collect(),
205 Err(
208 ExactIntersectionRefusal::Disjoint
209 | ExactIntersectionRefusal::NotRegularCurve,
210 ) => continue,
211 Err(_) => return Err(BooleanError::UnsupportedSection),
212 }
213 }
214 }
215 };
216 for (index, (branch, span)) in branches.iter().enumerate() {
217 let bounds = match (branch, span) {
220 (Curve3::Line(_), Some(span)) => Some(*span),
221 _ => None,
222 };
223 let branch = branch.clone();
224 let branch = &branch;
225 let (mut cuts, along_a) = side_a.cuts(fa, branch, sb, tolerance)?;
226 let (more, along_b) = side_b.cuts(fb, branch, sa, tolerance)?;
227 cuts.extend(more);
228 for (other, _) in branches
232 .iter()
233 .skip(index + 1)
234 .chain(branches.iter().take(index))
235 {
236 if let Ok(ExactCurveIntersection::Points(hits)) =
237 exact_curve_curve_intersection3(branch, other)
238 {
239 cuts.extend(hits.iter().map(|h| h.parameter.approx()));
240 }
241 }
242 if let Some(b) = bounds {
243 let (lo, hi) = (b.start.min(b.end), b.start.max(b.end));
244 cuts.retain(|&c| c >= lo && c <= hi);
245 cuts.extend([lo, hi].into_iter().filter(|x| x.is_finite()));
246 }
247 for (piece_curve, span) in pieces(branch, cuts)? {
248 let mid =
251 evaluate3(&piece_curve, span.start + SAMPLE * (span.end - span.start))
252 .map_err(|_| BooleanError::Evaluation)?;
253 let traced = matches!(
258 piece_curve,
259 Curve3::ImplicitSection(_) | Curve3::PairSection(_)
260 );
261 if !traced && touching(sa, sb, mid, tolerance)? {
262 continue;
263 }
264 let at_a = side_a.locate(fa, mid, &along_a, tolerance)?;
265 if at_a == Place::Outside {
266 continue;
267 }
268 let at_b = side_b.locate(fb, mid, &along_b, tolerance)?;
269 if at_b == Place::Outside {
270 continue;
271 }
272 out.push(SectionEdge {
273 face_a: side_a.faces[fa],
274 face_b: side_b.faces[fb],
275 start: evaluate3(&piece_curve, span.start)
276 .map_err(|_| BooleanError::Evaluation)?,
277 end: evaluate3(&piece_curve, span.end)
278 .map_err(|_| BooleanError::Evaluation)?,
279 curve: piece_curve,
280 span,
281 along_a: at_a == Place::Boundary,
282 along_b: at_b == Place::Boundary,
283 other_a: sb.clone(),
284 other_b: sa.clone(),
285 });
286 }
287 }
288 }
289 }
290 Ok(out)
291}
292
293fn poles(surface: &Surface) -> Vec<Point3> {
296 match surface {
297 Surface::Sphere(s) => {
298 let z = s.frame.z.normalize() * s.radius;
299 vec![s.frame.origin + z, s.frame.origin - z]
300 }
301 Surface::Cone(c) => {
302 let slope = c.semi_angle.tan();
303 if slope == 0.0 {
304 Vec::new()
305 } else {
306 vec![c.frame.origin - c.frame.z.normalize() * (c.radius / slope)]
307 }
308 }
309 _ => Vec::new(),
310 }
311}
312
313fn touching(
318 a: &Surface,
319 b: &Surface,
320 point: Point3,
321 tolerance: Tolerance,
322) -> Result<bool, BooleanError> {
323 let at = |s: &Surface| -> Result<axiolid_core::Vec3, BooleanError> {
324 let (u, v) = locate(s, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
325 Ok(normal(s, u, v)
326 .map_err(|_| BooleanError::Evaluation)?
327 .normalize())
328 };
329 Ok(at(a)?.cross(at(b)?).length() <= 1e-9)
330}
331
332#[allow(clippy::too_many_arguments)]
336fn imprint(
337 from: &Side<'_>,
338 face: usize,
339 onto: &Side<'_>,
340 other: usize,
341 first: bool,
342 tolerance: Tolerance,
343 out: &mut Vec<SectionEdge>,
344) -> Result<(), BooleanError> {
345 for (edge, curve, span) in from.edges_of(face)? {
346 let bounding = from.cutter(face, edge, &curve, span, tolerance, false)?;
349 let (cuts, along) = onto.cuts(other, &curve, &bounding, tolerance)?;
350 for piece in pieces_within(&curve, span, cuts) {
351 let mid = evaluate3(&curve, piece.start + SAMPLE * (piece.end - piece.start))
352 .map_err(|_| BooleanError::Evaluation)?;
353 let at = onto.locate(other, mid, &along, tolerance)?;
354 if at == Place::Outside {
355 continue;
356 }
357 let (face_a, face_b, along_a, along_b) = if first {
358 (
359 from.faces[face],
360 onto.faces[other],
361 true,
362 at == Place::Boundary,
363 )
364 } else {
365 (
366 onto.faces[other],
367 from.faces[face],
368 at == Place::Boundary,
369 true,
370 )
371 };
372 out.push(SectionEdge {
373 face_a,
374 face_b,
375 curve: curve.clone(),
376 span: piece,
377 start: evaluate3(&curve, piece.start).map_err(|_| BooleanError::Evaluation)?,
378 end: evaluate3(&curve, piece.end).map_err(|_| BooleanError::Evaluation)?,
379 along_a,
380 along_b,
381 other_a: bounding.clone(),
382 other_b: bounding.clone(),
383 });
384 }
385 }
386 Ok(())
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391enum Place {
392 Inside,
393 Boundary,
394 Outside,
395}
396
397fn pieces_within(curve: &Curve3, span: Interval, cuts: Vec<Scalar>) -> Vec<Interval> {
399 let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
400 let slack = 1e-12 * (1.0 + lo.abs().max(hi.abs()));
401 let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
402 let mut inside = vec![lo, hi];
403 for cut in cuts {
404 let candidates: &[Scalar] = if periodic {
405 &[cut - TAU, cut, cut + TAU, cut + 2.0 * TAU]
406 } else {
407 &[cut]
408 };
409 for &c in candidates {
410 if c > lo + slack && c < hi - slack {
411 inside.push(c);
412 }
413 }
414 }
415 inside.sort_by(Scalar::total_cmp);
416 inside.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
417 inside
418 .windows(2)
419 .map(|pair| Interval::new(pair[0], pair[1]))
420 .collect()
421}
422
423fn pieces(curve: &Curve3, mut cuts: Vec<Scalar>) -> Result<Vec<(Curve3, Interval)>, BooleanError> {
429 cuts.sort_by(Scalar::total_cmp);
430 cuts.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
431 let plain = |spans: Vec<Interval>| spans.into_iter().map(|s| (curve.clone(), s)).collect();
432 Ok(match curve {
433 Curve3::Line(_) => plain(
434 cuts.windows(2)
435 .map(|pair| Interval::new(pair[0], pair[1]))
436 .collect(),
437 ),
438 Curve3::ImplicitSection(section) => {
439 let n = section.curve.end();
440 let (pu, pv) = section.carrier.periodic();
441 let closure = section.curve.closure(pu, pv);
442 let slack = 1e-9 * (1.0 + n);
443 let mut inner: Vec<Scalar> = cuts
444 .into_iter()
445 .filter(|&c| c > slack && c < n - slack)
446 .collect();
447 inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
448 let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
449 let sub = if (a - b).abs() <= slack {
451 closure.and_then(|c| section.curve.rotated(a, c))
452 } else {
453 section.curve.sub(a, b, closure)
454 };
455 let sub = sub.ok_or(BooleanError::Evaluation)?;
456 let end = sub.end();
457 Ok((
458 Curve3::ImplicitSection(axiolid_curve::ImplicitSection3 {
459 carrier: section.carrier.clone(),
460 curve: sub,
461 }),
462 Interval::new(0.0, end),
463 ))
464 };
465 if closure.is_some() {
466 if inner.is_empty() {
467 return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
468 }
469 let mut out = Vec::new();
470 for pair in inner.windows(2) {
471 out.push(own(pair[0], pair[1])?);
472 }
473 out.push(own(inner[inner.len() - 1], inner[0])?);
475 out
476 } else {
477 let mut ends = vec![0.0];
478 ends.extend(inner);
479 ends.push(n);
480 let mut out = Vec::new();
481 for pair in ends.windows(2) {
482 out.push(own(pair[0], pair[1])?);
483 }
484 out
485 }
486 }
487 Curve3::PairSection(section) => {
488 let n = section.end();
489 let slack = 1e-9 * (1.0 + n);
490 let mut inner: Vec<Scalar> = cuts
491 .into_iter()
492 .filter(|&c| c > slack && c < n - slack)
493 .collect();
494 inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
495 let (first, last) = (
496 section.nodes[0].point,
497 section.nodes[section.nodes.len() - 1].point,
498 );
499 let closed = first == last;
500 let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
501 let sub = if a < b {
502 section.sub(a, b)
503 } else {
504 section
506 .sub(a, n)
507 .zip(section.sub(0.0, b))
508 .map(|(mut x, y)| {
509 x.nodes.extend(y.nodes.into_iter().skip(1));
510 x
511 })
512 };
513 let sub = sub.ok_or(BooleanError::Evaluation)?;
514 let end = sub.end();
515 Ok((Curve3::PairSection(sub), Interval::new(0.0, end)))
516 };
517 let mut out = Vec::new();
518 if closed {
519 if inner.is_empty() {
520 return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
521 }
522 for pair in inner.windows(2) {
523 out.push(own(pair[0], pair[1])?);
524 }
525 out.push(own(inner[inner.len() - 1], inner[0])?);
526 } else {
527 let mut ends = vec![0.0];
528 ends.extend(inner);
529 ends.push(n);
530 for pair in ends.windows(2) {
531 out.push(own(pair[0], pair[1])?);
532 }
533 }
534 out
535 }
536 _ => {
537 if cuts.is_empty() {
538 return Ok(vec![(curve.clone(), Interval::new(0.0, TAU))]);
539 }
540 let mut out: Vec<Interval> = cuts
541 .windows(2)
542 .map(|pair| Interval::new(pair[0], pair[1]))
543 .collect();
544 out.push(Interval::new(cuts[cuts.len() - 1], cuts[0] + TAU));
545 plain(out)
546 }
547 })
548}
549
550struct Side<'a> {
552 brep: &'a ExactBRep,
553 faces: Vec<FaceId>,
554 domains: Vec<FaceDomain<'a>>,
555 edge_faces: Vec<Vec<usize>>,
557}
558
559impl<'a> Side<'a> {
560 fn new(brep: &'a ExactBRep, tolerance: Tolerance) -> Result<Self, BooleanError> {
561 let topology = brep.topology();
562 let mut faces = Vec::with_capacity(topology.faces().len());
563 let mut domains = Vec::with_capacity(topology.faces().len());
564 for index in 0..topology.faces().len() {
565 let face = topology
566 .face_id_at(index)
567 .ok_or(BooleanError::DanglingReference)?;
568 let domain = FaceDomain::new(brep, face, tolerance)
569 .map_err(BooleanError::Measure)?
570 .ok_or(BooleanError::UnsupportedTrim)?;
571 faces.push(face);
572 domains.push(domain);
573 }
574 let mut edge_faces = vec![Vec::new(); topology.edges().len()];
575 for (index, face) in topology.faces().iter().enumerate() {
576 for bound in &face.bounds {
577 let wire = topology
578 .loops()
579 .get(bound.loop_id.index())
580 .ok_or(BooleanError::DanglingReference)?;
581 for use_ in &wire.edges {
582 edge_faces[use_.edge.index()].push(index);
583 }
584 }
585 }
586 Ok(Self {
587 brep,
588 faces,
589 domains,
590 edge_faces,
591 })
592 }
593
594 fn surface(&self, face: usize) -> Result<&'a Surface, BooleanError> {
595 self.brep.topology().faces()[face]
596 .surface
597 .and_then(|id| self.brep.surfaces().get(id.index()))
598 .ok_or(BooleanError::DanglingReference)
599 }
600
601 fn locate(
604 &self,
605 face: usize,
606 point: Point3,
607 along: &[(Curve3, Interval)],
608 tolerance: Tolerance,
609 ) -> Result<Place, BooleanError> {
610 for (curve, span) in along {
611 if on_edge(curve, *span, point, tolerance)? {
612 return Ok(Place::Boundary);
613 }
614 }
615 let (u, v) =
616 locate(self.surface(face)?, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
617 match self.domains[face]
618 .contains(Point2::new(u, v))
619 .map_err(BooleanError::Measure)?
620 {
621 Some(true) => Ok(Place::Inside),
622 Some(false) => Ok(Place::Outside),
623 None => Err(BooleanError::Undecided),
624 }
625 }
626
627 fn edges_of(&self, face: usize) -> Result<Vec<(usize, Curve3, Interval)>, BooleanError> {
629 let topology = self.brep.topology();
630 let mut out = Vec::new();
631 let mut seen = Vec::new();
632 for bound in &topology.faces()[face].bounds {
633 let wire = topology
634 .loops()
635 .get(bound.loop_id.index())
636 .ok_or(BooleanError::DanglingReference)?;
637 for use_ in &wire.edges {
638 if seen.contains(&use_.edge) {
639 continue;
640 }
641 seen.push(use_.edge);
642 let curve = topology.edges()[use_.edge.index()]
643 .curve
644 .and_then(|id| self.brep.curves3().get(id.index()))
645 .ok_or(BooleanError::DanglingReference)?;
646 let span = self
647 .brep
648 .edge_interval(use_.edge)
649 .ok_or(BooleanError::DanglingReference)?;
650 out.push((use_.edge.index(), curve.clone(), span));
651 }
652 }
653 Ok(out)
654 }
655
656 #[allow(clippy::type_complexity)]
660 fn cuts(
666 &self,
667 face: usize,
668 curve: &Curve3,
669 meets: &Surface,
670 tolerance: Tolerance,
671 ) -> Result<(Vec<Scalar>, Vec<(Curve3, Interval)>), BooleanError> {
672 let topology = self.brep.topology();
673 let mut out = Vec::new();
674 let mut along = Vec::new();
675 let mut seen = Vec::new();
676 for pole in poles(self.surface(face)?) {
679 if let Ok(t) = locate3(curve, pole, tolerance) {
680 let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
681 if (on - pole).length() <= tolerance.linear().max(1e-9) {
682 out.push(t);
683 }
684 }
685 }
686 for bound in &topology.faces()[face].bounds {
687 let wire = topology
688 .loops()
689 .get(bound.loop_id.index())
690 .ok_or(BooleanError::DanglingReference)?;
691 for use_ in &wire.edges {
692 if seen.contains(&use_.edge) {
693 continue;
694 }
695 seen.push(use_.edge);
696 let edge = &topology.edges()[use_.edge.index()];
697 let edge_curve = edge
698 .curve
699 .and_then(|id| self.brep.curves3().get(id.index()))
700 .ok_or(BooleanError::DanglingReference)?;
701 let span = self
702 .brep
703 .edge_interval(use_.edge)
704 .ok_or(BooleanError::DanglingReference)?;
705 let mut cutter =
706 self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, false)?;
707 let mut result = exact_curve_surface_intersection(curve, &cutter);
708 if matches!(result, Ok(ExactCurveIntersection::Contained)) {
709 if let Ok(seam) =
713 self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, true)
714 {
715 cutter = seam;
716 result = exact_curve_surface_intersection(curve, &cutter);
717 }
718 }
719 match result {
720 Ok(ExactCurveIntersection::Points(hits)) => {
721 for hit in hits {
722 if on_edge(edge_curve, span, hit.point, tolerance)? {
723 out.push(hit.parameter.approx());
724 }
725 }
726 }
727 Ok(ExactCurveIntersection::Contained) => {
731 for t in [span.start, span.end] {
732 let end =
733 evaluate3(edge_curve, t).map_err(|_| BooleanError::Evaluation)?;
734 if let Ok(s) = locate3(curve, end, tolerance) {
735 let on =
736 evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
737 if (on - end).length() <= tolerance.linear().max(1e-9) {
738 out.push(s);
739 }
740 }
741 }
742 along.push((edge_curve.clone(), span));
743 }
744 Ok(_) => return Err(BooleanError::UnsupportedSection),
745 Err(_) => {
746 let hits = match exact_curve_surface_intersection(edge_curve, meets) {
747 Ok(ExactCurveIntersection::Points(hits)) => hits,
748 _ => return Err(BooleanError::UnsupportedTrim),
749 };
750 for hit in hits {
751 if !on_span(edge_curve, span, hit.point, tolerance)? {
752 continue;
753 }
754 if let Ok(s) = locate3(curve, hit.point, tolerance) {
755 let on =
756 evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
757 if (on - hit.point).length() <= tolerance.linear().max(1e-9) {
758 out.push(s);
759 }
760 }
761 }
762 }
763 }
764 }
765 }
766 Ok((out, along))
767 }
768}
769
770impl Side<'_> {
771 fn cutter(
775 &self,
776 face: usize,
777 edge: usize,
778 edge_curve: &Curve3,
779 span: Interval,
780 tolerance: Tolerance,
781 across_seam: bool,
782 ) -> Result<Surface, BooleanError> {
783 let own = self.surface(face)?;
784 let adjacent = self.edge_faces[edge]
785 .iter()
786 .copied()
787 .find(|&other| other != face);
788 if let (Some(other), false) = (adjacent, across_seam) {
789 let theirs = self.surface(other)?;
790 if !same_support(own, theirs, tolerance) {
791 return Ok(theirs.clone());
792 }
793 }
794 if let Curve3::Circle(circle) = edge_curve {
800 return normal_sweep(own, circle, tolerance);
801 }
802 let Curve3::Line(line) = edge_curve else {
803 return Err(BooleanError::UnsupportedTrim);
804 };
805 let mid = evaluate3(edge_curve, 0.5 * (span.start + span.end))
806 .map_err(|_| BooleanError::Evaluation)?;
807 let (u, v) = locate(own, mid, tolerance).map_err(|_| BooleanError::Evaluation)?;
808 let n = normal(own, u, v).map_err(|_| BooleanError::Evaluation)?;
809 let across = line.direction.cross(n);
810 let length = across.length();
811 if length == 0.0 || !length.is_finite() {
812 return Err(BooleanError::UnsupportedTrim);
813 }
814 let z = across / length;
815 let x = line.direction.normalize();
816 Ok(Surface::Plane(Plane {
817 frame: axiolid_core::Frame3 {
818 origin: mid,
819 x,
820 y: z.cross(x),
821 z,
822 },
823 }))
824 }
825}
826
827fn normal_sweep(
832 surface: &Surface,
833 circle: &axiolid_curve::Circle3,
834 tolerance: Tolerance,
835) -> Result<Surface, BooleanError> {
836 let f = circle.frame;
837 let axis = f.x.cross(f.y).normalize();
838 let x = f.x.normalize();
839 let y = axis.cross(x);
840 let p = f.origin + x * circle.radius;
841 let (u, v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
842 let n = normal(surface, u, v)
843 .map_err(|_| BooleanError::Evaluation)?
844 .normalize();
845 let frame = axiolid_core::Frame3 {
846 origin: f.origin,
847 x,
848 y,
849 z: axis,
850 };
851 let along = n.dot(axis);
852 let radial = n.dot(x);
853 let plane = || {
854 Surface::Plane(Plane {
855 frame: axiolid_core::Frame3 {
856 origin: f.origin,
857 x,
858 y,
859 z: axis,
860 },
861 })
862 };
863 if along.abs() <= 1e-12 {
865 return Ok(plane());
866 }
867 if radial.abs() <= 1e-12 {
869 return Ok(Surface::Cylinder(axiolid_surface::Cylinder {
870 frame,
871 radius: circle.radius,
872 }));
873 }
874 Ok(Surface::Cone(axiolid_surface::Cone {
877 frame,
878 radius: circle.radius,
879 semi_angle: (radial / along).atan(),
880 }))
881}
882
883fn on_edge(
885 curve: &Curve3,
886 span: Interval,
887 point: Point3,
888 tolerance: Tolerance,
889) -> Result<bool, BooleanError> {
890 let Ok(t) = locate3(curve, point, tolerance) else {
891 return Ok(false);
892 };
893 let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
894 if (on - point).length() > tolerance.linear().max(1e-9) {
895 return Ok(false);
896 }
897 on_span(curve, span, point, tolerance)
898}
899
900fn on_span(
903 curve: &Curve3,
904 span: Interval,
905 point: Point3,
906 tolerance: Tolerance,
907) -> Result<bool, BooleanError> {
908 let t = locate3(curve, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
909 let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
910 let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
911 let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
912 let candidates: &[Scalar] = if periodic {
913 &[t - TAU, t, t + TAU, t + 2.0 * TAU]
914 } else {
915 &[t]
916 };
917 Ok(candidates
918 .iter()
919 .any(|c| *c >= lo - slack && *c <= hi + slack))
920}