1use axiolid_core::{Interval, Point2, Scalar, Vec2};
14use axiolid_curve::implicit::Cell;
15use axiolid_curve::{
16 Axis, Basis, Carrier, Curve3, Field2, ImplicitCurve2, ImplicitSection3, SeriesField2, Trig2,
17};
18use axiolid_surface::Surface;
19use core::f64::consts::{PI, TAU};
20
21use crate::curve_tower::CurveTower;
22use crate::exact_curve_intersection::{
23 ExactCurveHit, ExactCurveIntersection, ExactCurveParameter, ExactCurveRefusal, Isolated,
24};
25use crate::exact_surface_intersection::ExactIntersectionRefusal;
26use crate::field::{carrier_of, section_field};
27use crate::implicit_trace::Periodic;
28use crate::root_chain::{isolate, Budget, End, RootRefusal, Zone, ROOT_BUDGET};
29
30pub fn trace_section_pcurves(
40 surface: &Surface,
41 other: &Surface,
42 window: (Point2, Point2),
43) -> Result<Vec<ImplicitCurve2>, ExactIntersectionRefusal> {
44 let carrier = carrier_of(surface).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
45 let field = section_field(&carrier, other).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
46 trace_field(&carrier, &field, window, crate::implicit_trace::BUDGET / 4)
49}
50
51fn trace_field(
52 carrier: &Carrier,
53 field: &Field2,
54 window: (Point2, Point2),
55 budget: usize,
56) -> Result<Vec<ImplicitCurve2>, ExactIntersectionRefusal> {
57 let (pu, pv) = carrier.periodic();
58 let (lo, hi) = window;
59 let wraps = |periodic: bool, a: Scalar, b: Scalar| {
60 periodic && ((b - a) - TAU).abs() <= 1e-12 * (1.0 + a.abs())
61 };
62 let periodic = Periodic {
63 u: wraps(pu, lo.x, hi.x),
64 v: wraps(pv, lo.y, hi.y),
65 };
66 crate::implicit_trace::trace_within(field, Cell { lo, hi }, periodic, budget)
67 .map_err(crate::implicit_section::refusal_of)
68}
69
70#[must_use]
76pub fn extract_stretch(
77 curves: &[ImplicitCurve2],
78 periodic: (bool, bool),
79 start: Point2,
80 [first, second]: [Point2; 2],
81 end: Point2,
82 closed: bool,
83) -> Option<ImplicitCurve2> {
84 let shifts = |p: Point2| -> Vec<Point2> {
85 let ku: &[Scalar] = if periodic.0 {
86 &[0.0, -1.0, 1.0, -2.0, 2.0]
87 } else {
88 &[0.0]
89 };
90 let kv: &[Scalar] = if periodic.1 {
91 &[0.0, -1.0, 1.0, -2.0, 2.0]
92 } else {
93 &[0.0]
94 };
95 let mut out = Vec::new();
96 for a in ku {
97 for b in kv {
98 out.push(Point2::new(p.x + a * TAU, p.y + b * TAU));
99 }
100 }
101 out
102 };
103 let find = |curve: &ImplicitCurve2, p: Point2| -> Option<Scalar> {
104 let eps = 1e-7 * (1.0 + p.x.abs().max(p.y.abs()));
105 let mut best: Option<(Scalar, Scalar)> = None;
106 for q in shifts(p) {
107 let Some(t) = curve.parameter_of(q) else {
108 continue;
109 };
110 let Some(on) = curve.point(t) else {
111 continue;
112 };
113 let mut d = on - q;
115 if periodic.0 {
116 d.x -= (d.x / TAU).round() * TAU;
117 }
118 if periodic.1 {
119 d.y -= (d.y / TAU).round() * TAU;
120 }
121 let miss = d.length();
122 if miss <= eps && best.is_none_or(|(m, _)| miss < m) {
123 best = Some((miss, t));
124 }
125 }
126 best.map(|(_, t)| t)
127 };
128 for curve in curves {
129 let (Some(ts), Some(t1), Some(t2), Some(te)) = (
130 find(curve, start),
131 find(curve, first),
132 find(curve, second),
133 find(curve, end),
134 ) else {
135 continue;
136 };
137 let n = curve.end();
138 let closure = curve.closure(periodic.0, periodic.1);
139 let ahead = |t: Scalar| (t - ts).rem_euclid(n);
140 let stretch = if closed {
141 let whole = curve.rotated(ts, closure?)?;
143 if ahead(t1) < ahead(t2) {
144 whole
145 } else {
146 whole.reversed()
147 }
148 } else if let Some(closure) = closure {
149 if ahead(t1) < ahead(t2) && ahead(t2) < ahead(te) {
152 curve.sub(ts, te, Some(closure))?
153 } else {
154 curve.sub(te, ts, Some(closure))?.reversed()
155 }
156 } else if ts <= t1 && t1 <= t2 && t2 <= te {
157 curve.sub(ts, te, None)?
158 } else if te <= t2 && t2 <= t1 && t1 <= ts {
159 curve.sub(te, ts, None)?.reversed()
160 } else {
161 continue;
162 };
163 return Some(stretch);
164 }
165 None
166}
167
168fn defining_field(curve: &Curve3) -> Option<(Carrier, Field2)> {
171 let fourier = |t: &Trig2| vec![t.constant, t.cos, t.sin, t.cos2, t.sin2];
172 match curve {
173 Curve3::ImplicitSection(s) => Some((s.carrier.clone(), s.curve.field.clone())),
174 Curve3::RuledSection(r) => {
175 let (a, b, c) = (
177 fourier(&r.graph.a),
178 fourier(&r.graph.b),
179 fourier(&r.graph.c),
180 );
181 let coefficients = (0..5).map(|i| vec![c[i], b[i], a[i]]).collect();
182 Some((
183 Carrier::Ruled(r.carrier),
184 Field2::Series(SeriesField2 {
185 u: Basis::Fourier,
186 v: Basis::Power,
187 coefficients,
188 }),
189 ))
190 }
191 Curve3::TorusSection(t) => {
192 let (a, b, c) = (
194 fourier(&t.graph.a),
195 fourier(&t.graph.b),
196 fourier(&t.graph.c),
197 );
198 let coefficients = vec![c.iter().map(|x| -x).collect(), a, b];
199 Some((
200 Carrier::Torus(t.torus),
201 Field2::Series(SeriesField2 {
202 u: Basis::Fourier,
203 v: Basis::Fourier,
204 coefficients,
205 }),
206 ))
207 }
208 _ => None,
209 }
210}
211
212#[must_use]
216pub fn implicit_view(curve: &Curve3, span: Interval) -> Option<ImplicitSection3> {
217 if let Curve3::ImplicitSection(s) = curve {
218 let closed = (span.end - span.start).abs() >= s.curve.end() - 1e-12;
219 let sub = if closed {
220 s.curve.clone()
221 } else {
222 let (a, b) = (span.start.min(span.end), span.start.max(span.end));
223 let piece = s.curve.sub(a, b, None)?;
224 if span.end < span.start {
225 piece.reversed()
226 } else {
227 piece
228 }
229 };
230 return Some(ImplicitSection3 {
231 carrier: s.carrier.clone(),
232 curve: sub,
233 });
234 }
235 let (carrier, field) = defining_field(curve)?;
236 let at = |t: Scalar| -> Option<Point2> {
237 let p = axiolid_evaluate::evaluate3(curve, t).ok()?;
238 let (u, v) = carrier.parameters(p);
239 Some(Point2::new(u, v))
240 };
241 let n = 64;
243 let mut samples = Vec::with_capacity(n + 1);
244 for i in 0..=n {
245 let t = span.start + (span.end - span.start) * i as Scalar / n as Scalar;
246 let mut p = at(t)?;
247 if let Some(q) = samples.last().copied() {
248 let q: Point2 = q;
249 let (pu, pv) = carrier.periodic();
250 if pu {
251 p.x += ((q.x - p.x) / TAU).round() * TAU;
252 }
253 if pv {
254 p.y += ((q.y - p.y) / TAU).round() * TAU;
255 }
256 }
257 samples.push(p);
258 }
259 let (mut lo, mut hi) = (samples[0], samples[0]);
260 for p in &samples {
261 lo = lo.min(*p);
262 hi = hi.max(*p);
263 }
264 let pad = (hi - lo) * 0.25 + Vec2::splat(0.05);
265 let (mut lo, mut hi) = (lo - pad, hi + pad);
266 let (pu, pv) = carrier.periodic();
268 if pu && hi.x - lo.x >= TAU {
269 let c = 0.5 * (lo.x + hi.x);
270 (lo.x, hi.x) = (c - PI, c + PI);
271 }
272 if pv && hi.y - lo.y >= TAU {
273 let c = 0.5 * (lo.y + hi.y);
274 (lo.y, hi.y) = (c - PI, c + PI);
275 }
276 let curves = trace_field(&carrier, &field, (lo, hi), crate::implicit_trace::BUDGET).ok()?;
277 let (first, last) = (
279 axiolid_evaluate::evaluate3(curve, span.start).ok()?,
280 axiolid_evaluate::evaluate3(curve, span.end).ok()?,
281 );
282 let closed = (first - last).length() <= 1e-9 * (1.0 + first.length());
283 let stretch = extract_stretch(
284 &curves,
285 (pu, pv),
286 samples[0],
287 [samples[n / 3], samples[2 * n / 3]],
288 samples[n],
289 closed,
290 )?;
291 Some(ImplicitSection3 {
292 carrier,
293 curve: stretch,
294 })
295}
296
297pub fn section_curve_surface_intersection(
317 curve: &Curve3,
318 span: Interval,
319 surface: &Surface,
320) -> Result<ExactCurveIntersection, ExactCurveRefusal> {
321 let view = implicit_view(curve, span).ok_or(ExactCurveRefusal::UnsupportedCurve)?;
322 let other =
323 section_field(&view.carrier, surface).ok_or(ExactCurveRefusal::UnsupportedSurface)?;
324 let own = &view.curve;
325 let scale = 1e-9 * other.magnitude().max(1.0);
328 let samples: Vec<Scalar> = (0..=32)
329 .filter_map(|i| {
330 let p = own.point(own.end() * i as Scalar / 32.0)?;
331 Some(other.value(p))
332 })
333 .collect();
334 if samples.len() == 33 && samples.iter().all(|h| h.abs() <= scale) {
335 return Ok(ExactCurveIntersection::Contained);
336 }
337 let (pu, pv) = view.carrier.periodic();
338 let closed = own.closure(pu, pv).is_some();
339 let roots = roots_along(own, &other, closed).map_err(|_| ExactCurveRefusal::Undecided)?;
340 let mut hits = Vec::new();
341 for (t, multiplicity) in roots {
342 let Some(p) = own.point(t) else { continue };
343 let point = view.carrier.jet(p.x, p.y).point;
344 let parameter = match curve {
346 Curve3::ImplicitSection(_) => {
347 if span.end < span.start {
348 span.start - t
349 } else {
350 span.start + t
351 }
352 }
353 _ => {
354 match axiolid_evaluate::curve::invert3(curve, point, axiolid_core::Tolerance::METRE)
355 {
356 Ok(x) => nearest_turn(x, span),
357 Err(_) => continue,
358 }
359 }
360 };
361 hits.push(ExactCurveHit {
362 parameter: ExactCurveParameter::Certified(Isolated::new(parameter)),
363 multiplicity,
364 point,
365 });
366 }
367 Ok(ExactCurveIntersection::Points(hits))
368}
369
370pub(crate) fn roots_along(
385 own: &ImplicitCurve2,
386 other: &Field2,
387 closed: bool,
388) -> Result<Vec<(Scalar, usize)>, RootRefusal> {
389 roots_along_within(own, other, closed, ROOT_BUDGET)
390}
391
392pub(crate) fn roots_along_within(
394 own: &ImplicitCurve2,
395 other: &Field2,
396 closed: bool,
397 budget: usize,
398) -> Result<Vec<(Scalar, usize)>, RootRefusal> {
399 let tower = CurveTower::new(own, other);
400 let mut budget = Budget::new(budget);
401 let cells = own.cells.len();
402 let mut zones: Vec<Zone> = Vec::new();
403 let mut index = 0;
404 while index < cells {
405 if own.cells[index].bridge.is_some() {
406 for (t, m) in scan_bridge(own, other, index) {
407 zones.push(Zone {
408 lo: t,
409 hi: t,
410 at: t,
411 m,
412 end: None,
413 });
414 }
415 index += 1;
416 continue;
417 }
418 let start = index;
419 while index < cells && own.cells[index].bridge.is_none() {
420 index += 1;
421 }
422 let ends = [
423 if start == 0 { End::Curve } else { End::Inner },
424 if index == cells {
425 End::Curve
426 } else {
427 End::Inner
428 },
429 ];
430 zones.extend(isolate(
431 &tower,
432 start as Scalar,
433 index as Scalar,
434 ends,
435 &mut budget,
436 )?);
437 }
438 zones.sort_by(|a, b| a.at.total_cmp(&b.at));
439 if closed && zones.len() > 1 {
441 let (first, last) = (zones[0], zones[zones.len() - 1]);
442 if first.lo == 0.0 && last.hi == cells as Scalar {
443 zones.pop();
444 zones[0].m = first.m.max(last.m);
445 }
446 }
447 let roots: Vec<(Scalar, usize)> = zones.iter().map(|z| (z.at, z.m)).collect();
448 debug_assert!(roots.windows(2).all(|w| w[0].0 < w[1].0));
449 Ok(roots)
450}
451
452fn scan_bridge(own: &ImplicitCurve2, other: &Field2, index: usize) -> Vec<(Scalar, usize)> {
457 let cell = &own.cells[index];
458 let h = |s: Scalar| -> Option<Scalar> {
459 let free = cell.from + (cell.to - cell.from) * s;
460 let solved = own.solve_cell(cell, free)?;
461 let p = match cell.axis {
462 Axis::U => Point2::new(free, solved),
463 Axis::V => Point2::new(solved, free),
464 };
465 Some(other.value(p))
466 };
467 let mut out = Vec::new();
468 let n = 64;
469 let at = |k: usize| k as Scalar / n as Scalar;
470 let mut last = h(at(0));
471 for k in 1..=n {
472 let now = h(at(k));
473 if let (Some(a), Some(b)) = (last, now) {
474 if a == 0.0 && k == 1 {
475 out.push((at(0), 1));
476 } else if (a < 0.0) != (b < 0.0) && b != 0.0 {
477 out.push((bisect_bridge(&h, at(k - 1), at(k), a), 1));
478 } else if b == 0.0 {
479 out.push((at(k), 1));
480 }
481 }
482 last = now;
483 }
484 let mut roots: Vec<(Scalar, usize)> = out
485 .into_iter()
486 .map(|(s, m)| (index as Scalar + s, m))
487 .collect();
488 roots.dedup_by(|a, b| (a.0 - b.0).abs() <= 1e-9);
489 roots
490}
491
492fn bisect_bridge(
493 h: &dyn Fn(Scalar) -> Option<Scalar>,
494 mut s0: Scalar,
495 mut s1: Scalar,
496 a: Scalar,
497) -> Scalar {
498 let negative = a < 0.0;
499 for _ in 0..100 {
500 let m = 0.5 * (s0 + s1);
501 if m <= s0 || m >= s1 {
502 break;
503 }
504 match h(m) {
505 Some(0.0) => return m,
506 Some(v) if (v < 0.0) == negative => s0 = m,
507 Some(_) => s1 = m,
508 None => break,
509 }
510 }
511 0.5 * (s0 + s1)
512}
513
514fn nearest_turn(x: Scalar, span: Interval) -> Scalar {
516 let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
517 for k in [0.0, 1.0, -1.0, 2.0, -2.0] {
518 let y = x + k * TAU;
519 if y >= lo - 1e-9 && y <= hi + 1e-9 {
520 return y;
521 }
522 }
523 x
524}
525
526pub fn conic_spline_intersection(
537 curve: &Curve3,
538 surface: &Surface,
539) -> Result<ExactCurveIntersection, ExactCurveRefusal> {
540 use axiolid_core::Frame3;
541 use axiolid_surface::{EllipticalCylinder, Plane, Sphere};
542 if !matches!(surface, Surface::BSpline(_)) {
543 return Err(ExactCurveRefusal::UnsupportedSurface);
544 }
545 let plane = |origin: axiolid_core::Point3, z: axiolid_core::Vec3| -> Surface {
546 let z = z.normalize();
547 let helper = if z.x.abs() < 0.9 {
548 axiolid_core::Vec3::X
549 } else {
550 axiolid_core::Vec3::Y
551 };
552 let x = helper.cross(z).normalize();
553 Surface::Plane(Plane {
554 frame: Frame3 {
555 origin,
556 x,
557 y: z.cross(x),
558 z,
559 },
560 })
561 };
562 let (first, second) = match curve {
563 Curve3::Line(l) => {
564 let d = l.direction.normalize();
565 let helper = if d.x.abs() < 0.9 {
566 axiolid_core::Vec3::X
567 } else {
568 axiolid_core::Vec3::Y
569 };
570 let n1 = d.cross(helper).normalize();
571 (plane(l.origin, n1), plane(l.origin, d.cross(n1)))
572 }
573 Curve3::Circle(c) => (
574 plane(c.frame.origin, c.frame.x.cross(c.frame.y)),
575 Surface::Sphere(Sphere {
576 frame: c.frame,
577 radius: c.radius,
578 }),
579 ),
580 Curve3::Ellipse(e) => (
581 plane(e.frame.origin, e.frame.x.cross(e.frame.y)),
582 Surface::EllipticalCylinder(EllipticalCylinder {
583 frame: Frame3 {
584 z: e.frame.x.cross(e.frame.y).normalize(),
585 ..e.frame
586 },
587 semi_axis_x: e.semi_axis_x,
588 semi_axis_y: e.semi_axis_y,
589 }),
590 ),
591 _ => return Err(ExactCurveRefusal::UnsupportedCurve),
592 };
593 let pieces = match crate::implicit_section::implicit_surface_intersection(surface, &first, None)
594 {
595 Ok(pieces) => pieces,
596 Err(ExactIntersectionRefusal::Disjoint) => {
597 return Ok(ExactCurveIntersection::Points(Vec::new()))
598 }
599 Err(_) => return Err(ExactCurveRefusal::UnsupportedSurface),
600 };
601 let mut hits = Vec::new();
602 for piece in pieces {
603 let end = piece.curve.end();
604 let traced = Curve3::ImplicitSection(piece);
605 match section_curve_surface_intersection(&traced, Interval::new(0.0, end), &second)? {
606 ExactCurveIntersection::Points(found) => {
607 for hit in found {
608 let t = match curve {
609 Curve3::Line(l) => {
610 (hit.point - l.origin).dot(l.direction) / l.direction.length_squared()
611 }
612 _ => match axiolid_evaluate::curve::invert3(
613 curve,
614 hit.point,
615 axiolid_core::Tolerance::METRE,
616 ) {
617 Ok(t) => t,
618 Err(_) => continue,
619 },
620 };
621 hits.push(ExactCurveHit {
622 parameter: ExactCurveParameter::Certified(Isolated::new(t)),
623 ..hit
624 });
625 }
626 }
627 _ => return Ok(ExactCurveIntersection::Contained),
630 }
631 }
632 hits.sort_by(|a, b| a.parameter.approx().total_cmp(&b.parameter.approx()));
633 Ok(ExactCurveIntersection::Points(hits))
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use crate::implicit_trace::trace_within;
640 use axiolid_curve::implicit::Basis;
641
642 fn power(terms: &[(usize, usize, Scalar)]) -> Field2 {
643 let mut c = vec![vec![0.0; 10]; 10];
644 for &(i, j, x) in terms {
645 c[i][j] += x;
646 }
647 Field2::Series(SeriesField2 {
648 u: Basis::Power,
649 v: Basis::Power,
650 coefficients: c,
651 })
652 }
653
654 fn traced(field: &Field2, lo: Point2, hi: Point2) -> ImplicitCurve2 {
655 let mut curves = trace_within(
656 field,
657 Cell { lo, hi },
658 Periodic { u: false, v: false },
659 1 << 20,
660 )
661 .expect("a traced curve");
662 assert_eq!(curves.len(), 1);
663 curves.remove(0)
664 }
665
666 fn window() -> (Point2, Point2) {
667 (Point2::new(-1.0, -1.0), Point2::new(1.0, 1.0))
668 }
669
670 fn graph(k: usize) -> (ImplicitCurve2, Field2) {
672 let f = [(0, 1, 1.0), (2, 0, -0.3)];
673 let mut h = f.to_vec();
674 h.push((k, 0, 1.0));
675 let (lo, hi) = window();
676 (traced(&power(&f), lo, hi), power(&h))
677 }
678
679 #[test]
680 fn a_contact_of_any_order_up_to_eight_reports_its_multiplicity() {
681 for k in 2..=8 {
682 let (curve, h) = graph(k);
683 let roots = roots_along(&curve, &h, false).expect("decided");
684 assert_eq!(roots.len(), 1, "u^{k}: {roots:?}");
685 assert_eq!(roots[0].1, k, "u^{k}");
686 let p = curve.point(roots[0].0).unwrap();
687 assert!(p.x.abs() < 1e-6 && p.y.abs() < 1e-6, "u^{k}: {p:?}");
688 }
689 }
690
691 #[test]
692 fn a_contact_above_the_cap_is_undecided() {
693 let (curve, h) = graph(9);
694 assert!(roots_along(&curve, &h, false).is_err());
695 }
696
697 #[test]
698 fn a_contact_at_the_end_of_the_curve_is_reported() {
699 let f = power(&[(0, 1, 1.0), (2, 0, -0.3)]);
700 let h = power(&[(0, 1, 1.0), (2, 0, -0.3), (4, 0, 1.0)]);
701 let curve = traced(&f, Point2::new(-1.0, -1.0), Point2::new(0.0, 1.0));
702 let roots = roots_along(&curve, &h, false).expect("decided");
703 assert_eq!(roots.len(), 1, "{roots:?}");
704 assert_eq!(roots[0].1, 4);
705 assert!(curve.point(roots[0].0).unwrap().x.abs() < 1e-6);
706 }
707
708 #[test]
709 fn the_tangent_at_a_loops_seam_touches_it_once() {
710 let f = power(&[(2, 0, 1.0), (0, 2, 1.0), (0, 0, -0.25)]);
711 let (lo, hi) = window();
712 let curve = traced(&f, lo, hi);
713 let p = curve.point(0.0).unwrap();
714 let h = power(&[(1, 0, p.x), (0, 1, p.y), (0, 0, -(p.x * p.x + p.y * p.y))]);
715 let roots = roots_along(&curve, &h, true).expect("decided");
716 assert_eq!(roots.len(), 1, "{roots:?}");
717 assert_eq!(roots[0].1, 2);
718 }
719
720 #[test]
721 fn the_budget_refuses_by_name() {
722 let f = power(&[(0, 1, 1.0)]);
725 let mut p = vec![0.0, 1.0];
726 for r in [0.2f64, 0.5, 0.8, 0.9] {
727 let mut q = vec![0.0; p.len() + 2];
728 for (i, c) in p.iter().enumerate() {
729 q[i + 2] += c;
730 q[i] -= c * r * r;
731 }
732 p = q;
733 }
734 let terms: Vec<(usize, usize, Scalar)> =
735 p.iter().enumerate().map(|(i, c)| (i, 0, *c)).collect();
736 let h = power(&terms);
737 let (lo, hi) = window();
738 let curve = traced(&f, lo, hi);
739 assert_eq!(roots_along(&curve, &h, false).expect("decided").len(), 9);
740 assert_eq!(
741 roots_along_within(&curve, &h, false, 2),
742 Err(RootRefusal::Budget)
743 );
744 }
745
746 #[test]
751 #[ignore = "calibration, not a check of behaviour"]
752 fn peak_enclosures_of_decided_cases() {
753 for k in 2..=8 {
754 let (curve, h) = graph(k);
755 roots_along(&curve, &h, false).expect("decided");
756 }
757 let peak = crate::root_chain::PEAK.load(std::sync::atomic::Ordering::Relaxed);
758 println!("peak enclosures of the section cases: {peak}");
759 assert!(16 * peak <= ROOT_BUDGET, "{peak}");
760 }
761}