1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
30use ogeom_math::{
31 Axis, Circle, Ellipse, Frame, Hyperbola, KnotVector, Parabola, Point, Transform, Vector,
32 Weighted, bspline, elementary,
33};
34
35use crate::traits::{Continuity, Curve3d, CurveKind, Reversible, Transformable};
36
37pub const LINE_EXTENT: f64 = 1.0e9;
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum Curve {
47 Line(LineCurve),
49 Circle(CircleCurve),
51 Ellipse(EllipseCurve),
53 Hyperbola(HyperbolaCurve),
55 Parabola(ParabolaCurve),
57 BSpline(BSplineCurve),
59 Helix(HelixCurve),
61 Trimmed(Box<TrimmedCurve>),
63 Offset(Box<OffsetCurve>),
66 OnSurface(Box<CurveOnSurface>),
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct LineCurve {
73 axis: Axis,
74 domain: (f64, f64),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct CircleCurve {
80 circle: Circle,
81 reversed: bool,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct EllipseCurve {
87 ellipse: Ellipse,
88 reversed: bool,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct HyperbolaCurve {
94 hyperbola: Hyperbola,
95 domain: (f64, f64),
96 reversed: bool,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
101pub struct ParabolaCurve {
102 parabola: Parabola,
103 domain: (f64, f64),
104 reversed: bool,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
116pub struct HelixCurve {
117 frame: Frame,
118 radius: f64,
119 pitch: f64,
120 taper: f64,
124 domain: (f64, f64),
125 reversed: bool,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct BSplineCurve {
131 knots: KnotVector,
132 control: Vec<Weighted<Point>>,
133 rational: bool,
134 periodic: bool,
135}
136
137#[derive(Debug, Clone, PartialEq)]
139pub struct TrimmedCurve {
140 basis: Curve,
141 domain: (f64, f64),
142 reversed: bool,
143}
144
145impl LineCurve {
146 #[must_use]
148 pub const fn new(axis: Axis) -> Self {
149 Self {
150 axis,
151 domain: (-LINE_EXTENT, LINE_EXTENT),
152 }
153 }
154
155 pub fn segment(from: Point, to: Point, tol: Tolerances) -> OgeomResult<Self> {
165 let axis = Axis::through(from, to, tol)?;
166 Ok(Self {
167 axis,
168 domain: (0.0, from.distance(to)),
169 })
170 }
171
172 pub fn over(axis: Axis, start: f64, end: f64) -> OgeomResult<Self> {
179 if !start.is_finite() || !end.is_finite() || end <= start {
180 ogeom_bail!(Construction, "line range [{start}, {end}] is empty");
181 }
182 Ok(Self {
183 axis,
184 domain: (start, end),
185 })
186 }
187
188 #[must_use]
190 pub const fn axis(&self) -> Axis {
191 self.axis
192 }
193}
194
195impl CircleCurve {
196 #[must_use]
198 pub const fn new(circle: Circle) -> Self {
199 Self {
200 circle,
201 reversed: false,
202 }
203 }
204
205 #[must_use]
207 pub const fn circle(&self) -> Circle {
208 self.circle
209 }
210
211 #[must_use]
217 pub const fn is_reversed(&self) -> bool {
218 self.reversed
219 }
220}
221
222impl EllipseCurve {
223 #[must_use]
225 pub const fn new(ellipse: Ellipse) -> Self {
226 Self {
227 ellipse,
228 reversed: false,
229 }
230 }
231
232 #[must_use]
234 pub const fn ellipse(&self) -> Ellipse {
235 self.ellipse
236 }
237
238 #[must_use]
244 pub const fn is_reversed(&self) -> bool {
245 self.reversed
246 }
247}
248
249impl HyperbolaCurve {
250 pub fn new(hyperbola: Hyperbola, extent: f64) -> OgeomResult<Self> {
257 if !extent.is_finite() || extent <= 0.0 {
258 ogeom_bail!(
259 Construction,
260 "hyperbola extent {extent} must be finite and positive"
261 );
262 }
263 Ok(Self {
264 hyperbola,
265 domain: (-extent, extent),
266 reversed: false,
267 })
268 }
269
270 pub fn over(hyperbola: Hyperbola, start: f64, end: f64) -> OgeomResult<Self> {
278 if !start.is_finite() || !end.is_finite() || start >= end {
279 ogeom_bail!(
280 Construction,
281 "hyperbola domain [{start}, {end}] must be finite and increasing"
282 );
283 }
284 Ok(Self {
285 hyperbola,
286 domain: (start, end),
287 reversed: false,
288 })
289 }
290
291 #[must_use]
293 pub const fn hyperbola(&self) -> Hyperbola {
294 self.hyperbola
295 }
296
297 #[must_use]
303 pub const fn is_reversed(&self) -> bool {
304 self.reversed
305 }
306}
307
308impl ParabolaCurve {
309 pub fn new(parabola: Parabola, extent: f64) -> OgeomResult<Self> {
316 if !extent.is_finite() || extent <= 0.0 {
317 ogeom_bail!(
318 Construction,
319 "parabola extent {extent} must be finite and positive"
320 );
321 }
322 Ok(Self {
323 parabola,
324 domain: (-extent, extent),
325 reversed: false,
326 })
327 }
328
329 pub fn over(parabola: Parabola, start: f64, end: f64) -> OgeomResult<Self> {
336 if !start.is_finite() || !end.is_finite() || start >= end {
337 ogeom_bail!(
338 Construction,
339 "parabola domain [{start}, {end}] must be finite and increasing"
340 );
341 }
342 Ok(Self {
343 parabola,
344 domain: (start, end),
345 reversed: false,
346 })
347 }
348
349 #[must_use]
351 pub const fn parabola(&self) -> Parabola {
352 self.parabola
353 }
354
355 #[must_use]
361 pub const fn is_reversed(&self) -> bool {
362 self.reversed
363 }
364}
365
366impl HelixCurve {
367 pub fn new(frame: Frame, radius: f64, pitch: f64, turns: f64) -> OgeomResult<Self> {
376 if !turns.is_finite() || turns <= 0.0 {
377 ogeom_bail!(Construction, "a helix over {turns} turns is not a curve");
378 }
379 Self::over(frame, radius, pitch, 0.0, core::f64::consts::TAU * turns)
380 }
381
382 pub fn over(frame: Frame, radius: f64, pitch: f64, start: f64, end: f64) -> OgeomResult<Self> {
389 if !radius.is_finite() || radius <= 0.0 {
390 ogeom_bail!(
391 Construction,
392 "helix radius {radius} must be finite and positive"
393 );
394 }
395 if !pitch.is_finite() || pitch == 0.0 {
396 ogeom_bail!(
397 Construction,
398 "helix pitch {pitch} must be finite and non-zero; a zero pitch is a circle"
399 );
400 }
401 if !start.is_finite() || !end.is_finite() || start >= end {
402 ogeom_bail!(
403 Construction,
404 "helix domain [{start}, {end}] must be finite and increasing"
405 );
406 }
407 Ok(Self {
408 frame,
409 radius,
410 pitch,
411 taper: 0.0,
412 domain: (start, end),
413 reversed: false,
414 })
415 }
416
417 pub fn conical(
428 frame: Frame,
429 radius: f64,
430 pitch: f64,
431 taper: f64,
432 start: f64,
433 end: f64,
434 ) -> OgeomResult<Self> {
435 let mut helix = Self::over(frame, radius, pitch, start, end)?;
436 if !taper.is_finite() {
437 ogeom_bail!(Construction, "helix taper {taper} must be finite");
438 }
439 let slope = taper / core::f64::consts::TAU;
440 let (ra, rb) = (slope.mul_add(start, radius), slope.mul_add(end, radius));
441 if ra <= 0.0 || rb <= 0.0 {
442 ogeom_bail!(
443 Construction,
444 "the helix radius runs non-positive on [{start}, {end}]; \
445 past the apex there is no cone to wind"
446 );
447 }
448 helix.taper = taper;
449 Ok(helix)
450 }
451
452 #[must_use]
454 pub const fn frame(&self) -> &Frame {
455 &self.frame
456 }
457
458 #[must_use]
460 pub const fn radius(&self) -> f64 {
461 self.radius
462 }
463
464 #[must_use]
466 pub const fn pitch(&self) -> f64 {
467 self.pitch
468 }
469
470 #[must_use]
472 pub const fn taper(&self) -> f64 {
473 self.taper
474 }
475
476 #[must_use]
478 pub const fn is_reversed(&self) -> bool {
479 self.reversed
480 }
481
482 #[must_use]
485 pub fn arc_length(&self, from: f64, to: f64) -> f64 {
486 (to - from).abs() * self.radius.hypot(self.pitch / core::f64::consts::TAU)
487 }
488
489 fn at(&self, t: f64) -> (Point, Vector, Vector, Vector) {
491 let (sin, cos) = t.sin_cos();
492 let x = self.frame.x().vector();
493 let y = self.frame.y().vector();
494 let z = self.frame.z().vector();
495 let rise = self.pitch / core::f64::consts::TAU;
496 let slope = self.taper / core::f64::consts::TAU;
497 let r = slope.mul_add(t, self.radius);
498 let point = self.frame.origin() + x * (r * cos) + y * (r * sin) + z * (rise * t);
499 let d1 = x * slope.mul_add(cos, -(r * sin)) + y * slope.mul_add(sin, r * cos) + z * rise;
500 let d2 = x * (2.0 * slope).mul_add(-sin, -(r * cos))
501 + y * (2.0 * slope).mul_add(cos, -(r * sin));
502 let d3 =
503 x * (3.0 * slope).mul_add(-cos, r * sin) + y * (3.0 * slope).mul_add(-sin, -(r * cos));
504 (point, d1, d2, d3)
505 }
506}
507
508impl Curve3d for HelixCurve {
509 fn domain(&self) -> (f64, f64) {
510 self.domain
511 }
512
513 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
514 let u = self.normalize_parameter(u, tol)?;
515 let t = if self.reversed {
516 mirror(u, self.domain.0, self.domain.1)
517 } else {
518 u
519 };
520 Ok(self.at(t).0)
521 }
522
523 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
524 Ok(self.derivatives_at(u, 1, tol)?[1])
525 }
526
527 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
528 let u = self.normalize_parameter(u, tol)?;
529 let t = if self.reversed {
530 mirror(u, self.domain.0, self.domain.1)
531 } else {
532 u
533 };
534 let (point, d1, d2, d3) = self.at(t);
535 let sign = if self.reversed { -1.0 } else { 1.0 };
537 let mut out = vec![point.to_vector(), d1 * sign, d2, d3 * sign];
538 out.resize(n.max(3) + 1, Vector::ZERO);
539 out.truncate(n + 1);
540 Ok(out)
541 }
542
543 fn kind(&self) -> CurveKind {
544 CurveKind::Helix
545 }
546
547 fn continuity(&self) -> Continuity {
548 Continuity::CInfinity
549 }
550
551 fn is_closed(&self, _tol: Tolerances) -> bool {
552 false
553 }
554
555 fn is_periodic(&self) -> bool {
556 false
557 }
558}
559
560#[derive(Debug, Clone, PartialEq)]
570pub struct OffsetCurve {
571 basis: Curve,
572 distance: f64,
573 reference: ogeom_math::Direction,
574}
575
576impl OffsetCurve {
577 pub fn new(basis: Curve, distance: f64, reference: ogeom_math::Direction) -> OgeomResult<Self> {
584 if !distance.is_finite() || distance == 0.0 {
585 ogeom_bail!(
586 Construction,
587 "an offset of {distance} is not a displacement"
588 );
589 }
590 Ok(Self {
591 basis,
592 distance,
593 reference,
594 })
595 }
596
597 #[must_use]
599 pub const fn basis(&self) -> &Curve {
600 &self.basis
601 }
602
603 #[must_use]
605 pub const fn distance(&self) -> f64 {
606 self.distance
607 }
608
609 #[must_use]
611 pub const fn reference(&self) -> ogeom_math::Direction {
612 self.reference
613 }
614
615 fn direction_and_slope(&self, t: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
617 let d = self.basis.derivatives_at(t, 2, tol)?;
618 let v = self.reference.vector();
619 let w = d[1].cross(v);
620 let m = w.magnitude();
621 if m <= tol.confusion() * d[1].magnitude().max(1.0) {
622 ogeom_bail!(
623 Construction,
624 "the tangent at {t} runs along the reference; the offset direction is undefined"
625 );
626 }
627 let n = w / m;
628 let wp = d[2].cross(v);
629 let np = (wp - n * n.dot(wp)) / m;
630 Ok((n, np))
631 }
632}
633
634impl Curve3d for OffsetCurve {
635 fn domain(&self) -> (f64, f64) {
636 self.basis.domain()
637 }
638
639 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
640 let base = self.basis.point_at(u, tol)?;
641 let (n, _) = self.direction_and_slope(u, tol)?;
642 Ok(base + n * self.distance)
643 }
644
645 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
646 let d1 = self.basis.d1_at(u, tol)?;
647 let (_, np) = self.direction_and_slope(u, tol)?;
648 Ok(d1 + np * self.distance)
649 }
650
651 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
652 if n >= 2 {
653 ogeom_bail!(
654 Construction,
655 "an offset curve's second derivative needs its basis's third, which the \
656 vocabulary does not carry"
657 );
658 }
659 let mut out = vec![self.point_at(u, tol)?.to_vector()];
660 if n >= 1 {
661 out.push(self.d1_at(u, tol)?);
662 }
663 Ok(out)
664 }
665
666 fn kind(&self) -> CurveKind {
667 CurveKind::Offset
668 }
669
670 fn continuity(&self) -> Continuity {
671 match self.basis.continuity() {
672 Continuity::CInfinity => Continuity::CInfinity,
673 Continuity::C2 | Continuity::G2 => Continuity::C1,
674 Continuity::C1 | Continuity::G1 | Continuity::C0 => Continuity::C0,
675 }
676 }
677
678 fn is_closed(&self, tol: Tolerances) -> bool {
679 self.basis.is_closed(tol)
680 }
681
682 fn is_periodic(&self) -> bool {
683 self.basis.is_periodic()
684 }
685}
686
687#[derive(Debug, Clone, PartialEq)]
694pub struct CurveOnSurface {
695 pcurve: crate::curve2d::PlanarCurve,
696 surface: crate::surface::SurfaceGeometry,
697}
698
699impl CurveOnSurface {
700 #[must_use]
702 pub const fn new(
703 pcurve: crate::curve2d::PlanarCurve,
704 surface: crate::surface::SurfaceGeometry,
705 ) -> Self {
706 Self { pcurve, surface }
707 }
708
709 #[must_use]
711 pub const fn pcurve(&self) -> &crate::curve2d::PlanarCurve {
712 &self.pcurve
713 }
714
715 #[must_use]
717 pub const fn surface(&self) -> &crate::surface::SurfaceGeometry {
718 &self.surface
719 }
720}
721
722impl Curve3d for CurveOnSurface {
723 fn domain(&self) -> (f64, f64) {
724 use crate::traits::Curve2d as _;
725 self.pcurve.domain()
726 }
727
728 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
729 use crate::traits::{Curve2d as _, Surface as _};
730 let p = self.pcurve.point_at(u, tol)?;
731 self.surface.point_at(p.x, p.y, tol)
732 }
733
734 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
735 Ok(self.derivatives_at(u, 1, tol)?[1])
736 }
737
738 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
739 use crate::traits::{Curve2d as _, Surface as _};
740 if n >= 3 {
741 ogeom_bail!(
742 Construction,
743 "a surface curve's third derivative needs the surface's, which the \
744 vocabulary does not carry"
745 );
746 }
747 let d = self.pcurve.derivatives_at(u, n.max(2), tol)?;
748 let at = d[0];
749 let point = self.surface.point_at(at.x, at.y, tol)?;
750 let mut out = vec![point.to_vector()];
751 if n >= 1 {
752 let (su, sv) = self.surface.d1_at(at.x, at.y, tol)?;
753 out.push(su * d[1].x + sv * d[1].y);
754 if n >= 2 {
755 let (suu, suv, svv) = self.surface.d2_at(at.x, at.y, tol)?;
756 let second = suu * (d[1].x * d[1].x)
759 + suv * (2.0 * d[1].x * d[1].y)
760 + svv * (d[1].y * d[1].y)
761 + su * d[2].x
762 + sv * d[2].y;
763 out.push(second);
764 }
765 }
766 Ok(out)
767 }
768
769 fn kind(&self) -> CurveKind {
770 CurveKind::OnSurface
771 }
772
773 fn continuity(&self) -> Continuity {
774 use crate::traits::Surface as _;
775 self.pcurve_continuity().min(self.surface.continuity())
777 }
778
779 fn is_closed(&self, tol: Tolerances) -> bool {
780 use crate::traits::Curve2d as _;
781 self.pcurve.is_closed(tol)
782 }
783
784 fn is_periodic(&self) -> bool {
785 use crate::traits::Curve2d as _;
786 self.pcurve.is_periodic()
787 }
788}
789
790impl CurveOnSurface {
791 fn pcurve_continuity(&self) -> Continuity {
792 Continuity::C2
796 }
797}
798
799impl BSplineCurve {
800 pub fn new(knots: KnotVector, control: Vec<Point>, tol: Tolerances) -> OgeomResult<Self> {
807 let weighted = control
808 .into_iter()
809 .map(|p| Weighted::new(p, 1.0, tol))
810 .collect::<OgeomResult<Vec<_>>>()?;
811 Self::rational(knots, weighted)
812 }
813
814 pub fn rational(knots: KnotVector, control: Vec<Weighted<Point>>) -> OgeomResult<Self> {
821 if control.len() != knots.control_point_count() {
822 ogeom_bail!(
823 Dimension,
824 "knot vector describes {} control points, got {}",
825 knots.control_point_count(),
826 control.len()
827 );
828 }
829 let first = control[0].weight;
832 let rational = control
833 .iter()
834 .any(|w| (w.weight - first).abs() > 1e-12 * first.abs());
835 Ok(Self {
836 knots,
837 control,
838 rational,
839 periodic: false,
840 })
841 }
842
843 pub fn periodic(control: &[Point], degree: usize, tol: Tolerances) -> OgeomResult<Self> {
855 if degree == 0 {
856 ogeom_bail!(Construction, "a curve needs a degree of at least one");
857 }
858 if control.len() <= degree {
859 ogeom_bail!(
860 Construction,
861 "a periodic ring of {} points cannot carry degree {degree}",
862 control.len()
863 );
864 }
865 let n = control.len();
866 let mut wrapped: Vec<Point> = Vec::with_capacity(n + degree);
867 wrapped.extend_from_slice(control);
868 wrapped.extend_from_slice(&control[..degree]);
869 #[allow(clippy::cast_precision_loss)]
870 let knots: Vec<f64> = (0..wrapped.len() + degree + 1).map(|i| i as f64).collect();
871 let mut built = Self::new(KnotVector::new(knots, degree)?, wrapped, tol)?;
872 built.periodic = true;
873 Ok(built)
874 }
875
876 pub fn periodic_from_parts(
885 knots: KnotVector,
886 control: Vec<Weighted<Point>>,
887 tol: Tolerances,
888 ) -> OgeomResult<Self> {
889 let degree = knots.degree();
890 if control.len() <= degree {
891 ogeom_bail!(
892 Construction,
893 "a periodic curve of {} controls cannot carry degree {degree}",
894 control.len()
895 );
896 }
897 let n = control.len() - degree;
898 for i in 0..degree {
899 let (a, b) = (control[i], control[n + i]);
900 if !a.point().is_equal(b.point(), tol) || (a.weight - b.weight).abs() > 1e-12 {
901 ogeom_bail!(
902 Construction,
903 "a periodic curve's trailing controls must repeat its \
904 leading ones; control {} does not",
905 n + i
906 );
907 }
908 }
909 let mut built = Self::rational(knots, control)?;
910 built.periodic = true;
911 Ok(built)
912 }
913
914 #[must_use]
916 pub const fn knots(&self) -> &KnotVector {
917 &self.knots
918 }
919
920 #[must_use]
922 pub fn control_points(&self) -> &[Weighted<Point>] {
923 &self.control
924 }
925
926 #[must_use]
928 pub const fn is_rational(&self) -> bool {
929 self.rational
930 }
931
932 #[must_use]
934 pub const fn degree(&self) -> usize {
935 self.knots.degree()
936 }
937
938 pub fn with_knot_inserted(&self, u: f64, count: usize, tol: Tolerances) -> OgeomResult<Self> {
944 let (knots, control) = bspline::insert_knot(&self.knots, &self.control, u, count, tol)?;
945 Ok(Self {
946 knots,
947 control,
948 ..self.clone()
949 })
950 }
951
952 pub fn elevated(&self, tol: Tolerances) -> OgeomResult<Self> {
958 let (knots, control) = bspline::elevate_degree(&self.knots, &self.control, tol)?;
959 Ok(Self {
960 knots,
961 control,
962 ..self.clone()
963 })
964 }
965
966 pub fn reseamed_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Self> {
978 if self.periodic {
979 ogeom_bail!(Construction, "a periodic curve has no seam to move");
980 }
981 let (start, end) = self.domain();
982 let (head, tail) = (self.point_at(start, tol)?, self.point_at(end, tol)?);
983 if head.distance(tail) > tol.confusion() * 1e3 {
987 ogeom_bail!(
988 Construction,
989 "the curve does not close: its ends are {:.3e} apart",
990 head.distance(tail)
991 );
992 }
993 let (before, after) = self.split_at(u, tol)?;
994 let (knots, control) = bspline::join(
995 &(after.knots, after.control),
996 &(before.knots, before.control),
997 )?;
998 Ok(Self {
999 knots,
1000 control,
1001 ..self.clone()
1002 })
1003 }
1004
1005 pub fn extended(
1021 &self,
1022 at_end: bool,
1023 length: f64,
1024 continuity: usize,
1025 tol: Tolerances,
1026 ) -> OgeomResult<Self> {
1027 if !(length > 0.0 && length.is_finite()) {
1028 ogeom_bail!(
1029 Construction,
1030 "an extension needs a positive length; got {length}"
1031 );
1032 }
1033 let speed = self.end_speed(at_end, tol)?;
1034 let build = |span: f64| -> OgeomResult<Self> {
1035 let (knots, control) =
1036 bspline::extend(&self.knots, &self.control, at_end, span, continuity, tol)?;
1037 Ok(Self {
1038 knots,
1039 control,
1040 ..self.clone()
1041 })
1042 };
1043 let run = |curve: &Self, span: f64| -> OgeomResult<f64> {
1047 let (lo, hi) = curve.domain();
1048 let (a, b) = if at_end {
1049 (hi - span, hi)
1050 } else {
1051 (lo, lo + span)
1052 };
1053 curve.length_over((a, b), tol)
1054 };
1055 let mut s0 = length / speed;
1056 let mut c0 = build(s0)?;
1057 let mut l0 = run(&c0, s0)? - length;
1058 let mut s1 = s0 * 1.1;
1059 for _ in 0..40 {
1060 if l0.abs() <= tol.confusion() {
1061 break;
1062 }
1063 let c1 = build(s1)?;
1064 let l1 = run(&c1, s1)? - length;
1065 let next = if (l1 - l0).abs() > f64::EPSILON {
1066 s1 - l1 * (s1 - s0) / (l1 - l0)
1067 } else {
1068 s1
1069 };
1070 (s0, c0, l0) = (s1, c1, l1);
1071 s1 = if next > 0.0 { next } else { s0 * 0.5 };
1072 }
1073 Ok(c0)
1074 }
1075
1076 pub fn extended_to(
1087 &self,
1088 at_end: bool,
1089 target: Point,
1090 continuity: usize,
1091 tol: Tolerances,
1092 ) -> OgeomResult<Self> {
1093 let speed = self.end_speed(at_end, tol)?;
1094 let (lo, hi) = self.domain();
1095 let at = if at_end { hi } else { lo };
1096 let from = self.point_at(at, tol)?;
1097 let gap = from.distance(target);
1098 if gap <= tol.confusion() {
1099 ogeom_bail!(Construction, "the curve already ends at the point");
1100 }
1101 let end = if at_end {
1102 self.control[self.control.len() - 1]
1103 } else {
1104 self.control[0]
1105 };
1106 let weighted = Weighted::new(target, end.weight, tol)?;
1107 let (knots, control) = bspline::extend_to(
1108 &self.knots,
1109 &self.control,
1110 at_end,
1111 weighted,
1112 gap / speed,
1113 continuity,
1114 tol,
1115 )?;
1116 let rational = self.rational;
1117 Ok(Self {
1118 knots,
1119 control,
1120 rational,
1121 periodic: false,
1122 })
1123 }
1124
1125 fn end_speed(&self, at_end: bool, tol: Tolerances) -> OgeomResult<f64> {
1127 if self.periodic {
1128 ogeom_bail!(Construction, "a periodic curve has no end to continue from");
1129 }
1130 let (lo, hi) = self.domain();
1131 let at = if at_end { hi } else { lo };
1132 let speed = self.d1_at(at, tol)?.magnitude();
1133 if speed <= tol.confusion() {
1134 ogeom_bail!(
1135 Construction,
1136 "the curve stands still at its end; there is no direction to continue in"
1137 );
1138 }
1139 Ok(speed)
1140 }
1141
1142 fn length_over(&self, range: (f64, f64), tol: Tolerances) -> OgeomResult<f64> {
1145 const NODES: [(f64, f64); 5] = [
1146 (0.0, 0.568_888_888_888_888_9),
1147 (-0.538_469_310_105_683_1, 0.478_628_670_499_366_5),
1148 (0.538_469_310_105_683_1, 0.478_628_670_499_366_5),
1149 (-0.906_179_845_938_664, 0.236_926_885_056_189_1),
1150 (0.906_179_845_938_664, 0.236_926_885_056_189_1),
1151 ];
1152 let mut breaks: Vec<f64> = vec![range.0];
1153 breaks.extend(
1154 self.knots
1155 .distinct()
1156 .into_iter()
1157 .map(|(k, _)| k)
1158 .filter(|k| *k > range.0 && *k < range.1),
1159 );
1160 breaks.push(range.1);
1161 let mut total = 0.0;
1162 for w in breaks.windows(2) {
1163 for part in 0..8 {
1165 let a = w[0] + (w[1] - w[0]) * f64::from(part) / 8.0;
1166 let b = w[0] + (w[1] - w[0]) * f64::from(part + 1) / 8.0;
1167 let (mid, half) = (0.5 * (a + b), 0.5 * (b - a));
1168 for (x, weight) in NODES {
1169 total += weight * half * self.d1_at(mid + half * x, tol)?.magnitude();
1170 }
1171 }
1172 }
1173 Ok(total)
1174 }
1175
1176 pub fn segment(&self, range: (f64, f64), tol: Tolerances) -> OgeomResult<Self> {
1184 let (a, b) = self.knots.domain();
1185 let eps = tol.parametric();
1186 if range.1 <= range.0 + eps || range.0 < a - eps || range.1 > b + eps {
1187 ogeom_bail!(
1188 Domain,
1189 "[{}, {}] is no piece of [{a}, {b}]",
1190 range.0,
1191 range.1
1192 );
1193 }
1194 let mut piece = Self {
1195 periodic: false,
1196 ..self.clone()
1197 };
1198 if range.0 > a + eps {
1199 piece = piece.split_at(range.0, tol)?.1;
1200 }
1201 if range.1 < b - eps {
1202 piece = piece.split_at(range.1, tol)?.0;
1203 }
1204 Ok(piece)
1205 }
1206
1207 pub fn split_at(&self, u: f64, tol: Tolerances) -> OgeomResult<(Self, Self)> {
1213 let ((lk, lc), (rk, rc)) = bspline::split(&self.knots, &self.control, u, tol)?;
1214 Ok((
1215 Self {
1216 knots: lk,
1217 control: lc,
1218 ..self.clone()
1219 },
1220 Self {
1221 knots: rk,
1222 control: rc,
1223 ..self.clone()
1224 },
1225 ))
1226 }
1227}
1228
1229impl TrimmedCurve {
1230 pub fn new(basis: Curve, start: f64, end: f64, tol: Tolerances) -> OgeomResult<Self> {
1237 let (a, b) = basis.domain();
1238 if !start.is_finite() || !end.is_finite() || end <= start + tol.parametric() {
1239 ogeom_bail!(Domain, "trim range [{start}, {end}] is empty");
1240 }
1241 if !basis.is_periodic() && (start < a - tol.parametric() || end > b + tol.parametric()) {
1242 ogeom_bail!(
1243 Domain,
1244 "trim range [{start}, {end}] leaves the basis domain [{a}, {b}]"
1245 );
1246 }
1247 Ok(Self {
1248 basis,
1249 domain: (start, end),
1250 reversed: false,
1251 })
1252 }
1253
1254 #[must_use]
1256 pub const fn basis(&self) -> &Curve {
1257 &self.basis
1258 }
1259
1260 #[must_use]
1266 pub const fn is_reversed(&self) -> bool {
1267 self.reversed
1268 }
1269
1270 fn basis_parameter(&self, u: f64, tol: Tolerances) -> OgeomResult<f64> {
1272 let u = self.normalize_parameter(u, tol)?;
1273 Ok(if self.reversed {
1274 mirror(u, self.domain.0, self.domain.1)
1275 } else {
1276 u
1277 })
1278 }
1279}
1280
1281fn mirror(u: f64, a: f64, b: f64) -> f64 {
1287 a + b - u
1288}
1289
1290impl Curve3d for LineCurve {
1291 fn domain(&self) -> (f64, f64) {
1292 self.domain
1293 }
1294
1295 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1296 let u = self.normalize_parameter(u, tol)?;
1297 Ok(self.axis.point_at(u))
1298 }
1299
1300 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1301 self.normalize_parameter(u, tol)?;
1302 Ok(self.axis.direction.vector())
1303 }
1304
1305 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1306 let p = self.point_at(u, tol)?;
1307 let mut out = vec![p.to_vector(), self.axis.direction.vector()];
1308 out.resize(n + 1, Vector::ZERO);
1309 out.truncate(n + 1);
1310 Ok(out)
1311 }
1312
1313 fn kind(&self) -> CurveKind {
1314 CurveKind::Line
1315 }
1316
1317 fn continuity(&self) -> Continuity {
1318 Continuity::CInfinity
1319 }
1320
1321 fn is_closed(&self, _tol: Tolerances) -> bool {
1322 false
1323 }
1324
1325 fn is_periodic(&self) -> bool {
1326 false
1327 }
1328}
1329
1330impl Curve3d for CircleCurve {
1331 fn domain(&self) -> (f64, f64) {
1332 (0.0, core::f64::consts::TAU)
1333 }
1334
1335 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1336 let u = self.normalize_parameter(u, tol)?;
1337 let angle = if self.reversed { -u } else { u };
1338 Ok(elementary::circle_at(&self.circle, angle).point)
1339 }
1340
1341 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1342 Ok(self.derivatives_at(u, 1, tol)?[1])
1343 }
1344
1345 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1346 let u = self.normalize_parameter(u, tol)?;
1347 let angle = if self.reversed { -u } else { u };
1348 let c = elementary::circle_at(&self.circle, angle);
1349 let sign = if self.reversed { -1.0 } else { 1.0 };
1352 let mut out = vec![c.point.to_vector(), c.d1 * sign, c.d2];
1353 out.resize(n.max(2) + 1, Vector::ZERO);
1354 out.truncate(n + 1);
1355 Ok(out)
1356 }
1357
1358 fn kind(&self) -> CurveKind {
1359 CurveKind::Circle
1360 }
1361
1362 fn continuity(&self) -> Continuity {
1363 Continuity::CInfinity
1364 }
1365
1366 fn is_closed(&self, _tol: Tolerances) -> bool {
1367 true
1368 }
1369
1370 fn is_periodic(&self) -> bool {
1371 true
1372 }
1373}
1374
1375impl Curve3d for EllipseCurve {
1376 fn domain(&self) -> (f64, f64) {
1377 (0.0, core::f64::consts::TAU)
1378 }
1379
1380 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1381 let u = self.normalize_parameter(u, tol)?;
1382 let angle = if self.reversed { -u } else { u };
1383 Ok(elementary::ellipse_at(&self.ellipse, angle).point)
1384 }
1385
1386 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1387 Ok(self.derivatives_at(u, 1, tol)?[1])
1388 }
1389
1390 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1391 let u = self.normalize_parameter(u, tol)?;
1392 let angle = if self.reversed { -u } else { u };
1393 let c = elementary::ellipse_at(&self.ellipse, angle);
1394 let sign = if self.reversed { -1.0 } else { 1.0 };
1395 let mut out = vec![c.point.to_vector(), c.d1 * sign, c.d2];
1396 out.resize(n.max(2) + 1, Vector::ZERO);
1397 out.truncate(n + 1);
1398 Ok(out)
1399 }
1400
1401 fn kind(&self) -> CurveKind {
1402 CurveKind::Ellipse
1403 }
1404
1405 fn continuity(&self) -> Continuity {
1406 Continuity::CInfinity
1407 }
1408
1409 fn is_closed(&self, _tol: Tolerances) -> bool {
1410 true
1411 }
1412
1413 fn is_periodic(&self) -> bool {
1414 true
1415 }
1416}
1417
1418impl Curve3d for HyperbolaCurve {
1419 fn domain(&self) -> (f64, f64) {
1420 self.domain
1421 }
1422
1423 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1424 let u = self.normalize_parameter(u, tol)?;
1425 let t = if self.reversed {
1426 mirror(u, self.domain.0, self.domain.1)
1427 } else {
1428 u
1429 };
1430 Ok(elementary::hyperbola_at(&self.hyperbola, t).point)
1431 }
1432
1433 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1434 Ok(self.derivatives_at(u, 1, tol)?[1])
1435 }
1436
1437 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1438 let u = self.normalize_parameter(u, tol)?;
1439 let t = if self.reversed {
1440 mirror(u, self.domain.0, self.domain.1)
1441 } else {
1442 u
1443 };
1444 let c = elementary::hyperbola_at(&self.hyperbola, t);
1445 let sign = if self.reversed { -1.0 } else { 1.0 };
1446 let mut out = vec![c.point.to_vector(), c.d1 * sign, c.d2];
1447 out.resize(n.max(2) + 1, Vector::ZERO);
1448 out.truncate(n + 1);
1449 Ok(out)
1450 }
1451
1452 fn kind(&self) -> CurveKind {
1453 CurveKind::Hyperbola
1454 }
1455
1456 fn continuity(&self) -> Continuity {
1457 Continuity::CInfinity
1458 }
1459
1460 fn is_closed(&self, _tol: Tolerances) -> bool {
1461 false
1462 }
1463
1464 fn is_periodic(&self) -> bool {
1465 false
1466 }
1467}
1468
1469impl Curve3d for ParabolaCurve {
1470 fn domain(&self) -> (f64, f64) {
1471 self.domain
1472 }
1473
1474 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1475 let u = self.normalize_parameter(u, tol)?;
1476 let t = if self.reversed {
1477 mirror(u, self.domain.0, self.domain.1)
1478 } else {
1479 u
1480 };
1481 Ok(elementary::parabola_at(&self.parabola, t).point)
1482 }
1483
1484 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1485 Ok(self.derivatives_at(u, 1, tol)?[1])
1486 }
1487
1488 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1489 let u = self.normalize_parameter(u, tol)?;
1490 let t = if self.reversed {
1491 mirror(u, self.domain.0, self.domain.1)
1492 } else {
1493 u
1494 };
1495 let c = elementary::parabola_at(&self.parabola, t);
1496 let sign = if self.reversed { -1.0 } else { 1.0 };
1497 let mut out = vec![c.point.to_vector(), c.d1 * sign, c.d2];
1498 out.resize(n.max(2) + 1, Vector::ZERO);
1499 out.truncate(n + 1);
1500 Ok(out)
1501 }
1502
1503 fn kind(&self) -> CurveKind {
1504 CurveKind::Parabola
1505 }
1506
1507 fn continuity(&self) -> Continuity {
1508 Continuity::CInfinity
1509 }
1510
1511 fn is_closed(&self, _tol: Tolerances) -> bool {
1512 false
1513 }
1514
1515 fn is_periodic(&self) -> bool {
1516 false
1517 }
1518}
1519
1520impl Curve3d for BSplineCurve {
1521 fn domain(&self) -> (f64, f64) {
1522 self.knots.domain()
1523 }
1524
1525 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1526 let u = self.normalize_parameter(u, tol)?;
1527 if self.rational {
1528 bspline::evaluate_rational(&self.knots, &self.control, u, tol)
1529 } else {
1530 Ok(bspline::evaluate(&self.knots, &self.control, u, tol)?.point())
1533 }
1534 }
1535
1536 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1537 Ok(self.derivatives_at(u, 1, tol)?[1])
1538 }
1539
1540 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1541 let u = self.normalize_parameter(u, tol)?;
1542 let points = bspline::rational_derivatives(&self.knots, &self.control, u, n, tol)?;
1543 Ok(points.into_iter().map(Point::to_vector).collect())
1544 }
1545
1546 fn kind(&self) -> CurveKind {
1547 CurveKind::BSpline
1548 }
1549
1550 fn continuity(&self) -> Continuity {
1562 let degree = self.knots.degree();
1563 let (a, b) = self.knots.domain();
1564 let worst = self
1565 .knots
1566 .distinct()
1567 .into_iter()
1568 .filter(|(v, _)| *v > a && *v < b)
1569 .map(|(_, m)| m)
1570 .max();
1571 match worst {
1572 None => Continuity::CInfinity,
1573 Some(m) => match degree.saturating_sub(m) {
1574 0 => Continuity::C0,
1575 1 => Continuity::C1,
1576 _ => Continuity::C2,
1577 },
1578 }
1579 }
1580
1581 fn is_closed(&self, tol: Tolerances) -> bool {
1582 let (first, last) = (self.control[0], self.control[self.control.len() - 1]);
1583 first.point().is_equal(last.point(), tol)
1584 }
1585
1586 fn is_periodic(&self) -> bool {
1587 self.periodic
1588 }
1589}
1590
1591impl Curve3d for TrimmedCurve {
1592 fn domain(&self) -> (f64, f64) {
1593 self.domain
1594 }
1595
1596 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1597 self.basis.point_at(self.basis_parameter(u, tol)?, tol)
1598 }
1599
1600 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1601 let d = self.basis.d1_at(self.basis_parameter(u, tol)?, tol)?;
1602 Ok(if self.reversed { -d } else { d })
1603 }
1604
1605 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1606 let t = self.basis_parameter(u, tol)?;
1607 let mut out = self.basis.derivatives_at(t, n, tol)?;
1608 if self.reversed {
1609 for (order, d) in out.iter_mut().enumerate() {
1612 if order % 2 == 1 {
1613 *d = -*d;
1614 }
1615 }
1616 }
1617 Ok(out)
1618 }
1619
1620 fn kind(&self) -> CurveKind {
1621 CurveKind::Trimmed
1622 }
1623
1624 fn continuity(&self) -> Continuity {
1625 self.basis.continuity()
1626 }
1627
1628 fn is_closed(&self, tol: Tolerances) -> bool {
1629 match (self.start(tol), self.end(tol)) {
1630 (Ok(a), Ok(b)) => a.is_equal(b, tol),
1631 _ => false,
1632 }
1633 }
1634
1635 fn is_periodic(&self) -> bool {
1636 false
1637 }
1638}
1639
1640macro_rules! dispatch {
1642 ($self:ident, $c:ident => $body:expr) => {
1643 match $self {
1644 Self::Line($c) => $body,
1645 Self::Circle($c) => $body,
1646 Self::Ellipse($c) => $body,
1647 Self::Hyperbola($c) => $body,
1648 Self::Parabola($c) => $body,
1649 Self::BSpline($c) => $body,
1650 Self::Helix($c) => $body,
1651 Self::Trimmed($c) => $body,
1652 Self::Offset($c) => $body,
1653 Self::OnSurface($c) => $body,
1654 }
1655 };
1656}
1657
1658impl Curve3d for Curve {
1659 fn domain(&self) -> (f64, f64) {
1660 dispatch!(self, c => c.domain())
1661 }
1662
1663 fn point_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Point> {
1664 dispatch!(self, c => c.point_at(u, tol))
1665 }
1666
1667 fn d1_at(&self, u: f64, tol: Tolerances) -> OgeomResult<Vector> {
1668 dispatch!(self, c => c.d1_at(u, tol))
1669 }
1670
1671 fn derivatives_at(&self, u: f64, n: usize, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
1672 dispatch!(self, c => c.derivatives_at(u, n, tol))
1673 }
1674
1675 fn kind(&self) -> CurveKind {
1676 dispatch!(self, c => c.kind())
1677 }
1678
1679 fn continuity(&self) -> Continuity {
1680 dispatch!(self, c => c.continuity())
1681 }
1682
1683 fn is_closed(&self, tol: Tolerances) -> bool {
1684 dispatch!(self, c => c.is_closed(tol))
1685 }
1686
1687 fn is_periodic(&self) -> bool {
1688 dispatch!(self, c => c.is_periodic())
1689 }
1690}
1691
1692impl Transformable for Curve {
1693 fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
1694 Ok(match self {
1695 Self::Line(c) => Self::Line(LineCurve {
1696 axis: Axis::new(
1697 t.apply(c.axis.location),
1698 t.apply_direction(c.axis.direction, tol)?,
1699 ),
1700 domain: (
1703 c.domain.0 * t.scale_factor().abs(),
1704 c.domain.1 * t.scale_factor().abs(),
1705 ),
1706 }),
1707 Self::Circle(c) => Self::Circle(CircleCurve {
1712 circle: c.circle.transformed(t, tol)?,
1713 ..*c
1714 }),
1715 Self::Ellipse(c) => Self::Ellipse(EllipseCurve {
1716 ellipse: c.ellipse.transformed(t, tol)?,
1717 ..*c
1718 }),
1719 Self::Hyperbola(c) => Self::Hyperbola(HyperbolaCurve {
1720 hyperbola: c.hyperbola.transformed(t, tol)?,
1721 ..*c
1722 }),
1723 Self::Parabola(c) => Self::Parabola(ParabolaCurve {
1724 parabola: c.parabola.transformed(t, tol)?,
1725 ..*c
1726 }),
1727 Self::BSpline(c) => {
1728 let control = c
1729 .control
1730 .iter()
1731 .map(|w| Weighted::new(t.apply(w.point()), w.weight, tol))
1732 .collect::<OgeomResult<Vec<_>>>()?;
1733 Self::BSpline(BSplineCurve {
1734 control,
1735 ..c.clone()
1736 })
1737 }
1738 Self::Helix(c) => Self::Helix(HelixCurve {
1739 frame: t.apply_frame(&c.frame, tol)?,
1740 radius: c.radius * t.scale_factor().abs(),
1741 pitch: c.pitch * t.scale_factor().abs(),
1742 taper: c.taper * t.scale_factor().abs(),
1743 ..*c
1744 }),
1745 Self::Offset(c) => Self::Offset(Box::new(OffsetCurve {
1746 basis: c.basis.transformed(t, tol)?,
1747 distance: c.distance * t.scale_factor().abs(),
1748 reference: t.apply_direction(c.reference, tol)?,
1749 })),
1750 Self::OnSurface(c) => Self::OnSurface(Box::new(CurveOnSurface {
1751 pcurve: c.pcurve.clone(),
1752 surface: c.surface.transformed(t, tol)?,
1753 })),
1754 Self::Trimmed(c) => Self::Trimmed(Box::new(TrimmedCurve {
1755 basis: c.basis.transformed(t, tol)?,
1756 domain: if matches!(c.basis, Self::Line(_)) {
1759 let s = t.scale_factor().abs();
1760 (c.domain.0 * s, c.domain.1 * s)
1761 } else {
1762 c.domain
1763 },
1764 reversed: c.reversed,
1765 })),
1766 })
1767 }
1768}
1769
1770impl Reversible for Curve {
1771 fn reversed(&self) -> Self {
1772 match self {
1773 Self::Line(c) => Self::Line(LineCurve {
1774 axis: Axis::new(
1775 c.axis.point_at(c.domain.0 + c.domain.1),
1776 c.axis.direction.reversed(),
1777 ),
1778 domain: c.domain,
1779 }),
1780 Self::Circle(c) => Self::Circle(CircleCurve {
1781 reversed: !c.reversed,
1782 ..*c
1783 }),
1784 Self::Ellipse(c) => Self::Ellipse(EllipseCurve {
1785 reversed: !c.reversed,
1786 ..*c
1787 }),
1788 Self::Hyperbola(c) => Self::Hyperbola(HyperbolaCurve {
1789 reversed: !c.reversed,
1790 ..*c
1791 }),
1792 Self::Parabola(c) => Self::Parabola(ParabolaCurve {
1793 reversed: !c.reversed,
1794 ..*c
1795 }),
1796 Self::BSpline(c) => {
1797 let (knots, control) = bspline::reverse(&c.knots, &c.control);
1798 Self::BSpline(BSplineCurve {
1799 knots,
1800 control,
1801 ..c.clone()
1802 })
1803 }
1804 Self::Helix(c) => Self::Helix(HelixCurve {
1805 reversed: !c.reversed,
1806 ..*c
1807 }),
1808 Self::Offset(c) => Self::Offset(Box::new(OffsetCurve {
1811 basis: c.basis.reversed(),
1812 distance: -c.distance,
1813 reference: c.reference,
1814 })),
1815 Self::OnSurface(c) => Self::OnSurface(Box::new(CurveOnSurface {
1816 pcurve: c.pcurve.reversed(),
1817 surface: c.surface.clone(),
1818 })),
1819 Self::Trimmed(c) => Self::Trimmed(Box::new(TrimmedCurve {
1823 reversed: !c.reversed,
1824 ..(**c).clone()
1825 })),
1826 }
1827 }
1828}
1829
1830impl From<LineCurve> for Curve {
1831 fn from(c: LineCurve) -> Self {
1832 Self::Line(c)
1833 }
1834}
1835impl From<CircleCurve> for Curve {
1836 fn from(c: CircleCurve) -> Self {
1837 Self::Circle(c)
1838 }
1839}
1840impl From<EllipseCurve> for Curve {
1841 fn from(c: EllipseCurve) -> Self {
1842 Self::Ellipse(c)
1843 }
1844}
1845impl From<HelixCurve> for Curve {
1846 fn from(c: HelixCurve) -> Self {
1847 Self::Helix(c)
1848 }
1849}
1850
1851impl From<HyperbolaCurve> for Curve {
1852 fn from(c: HyperbolaCurve) -> Self {
1853 Self::Hyperbola(c)
1854 }
1855}
1856impl From<ParabolaCurve> for Curve {
1857 fn from(c: ParabolaCurve) -> Self {
1858 Self::Parabola(c)
1859 }
1860}
1861impl From<BSplineCurve> for Curve {
1862 fn from(c: BSplineCurve) -> Self {
1863 Self::BSpline(c)
1864 }
1865}
1866impl From<TrimmedCurve> for Curve {
1867 fn from(c: TrimmedCurve) -> Self {
1868 Self::Trimmed(Box::new(c))
1869 }
1870}
1871
1872#[cfg(test)]
1873#[allow(clippy::unwrap_used)]
1874mod reseam_tests {
1875 use super::*;
1876 use ogeom_core::Tolerances;
1877 use ogeom_math::{KnotVector, Point};
1878
1879 #[test]
1880 fn a_reseamed_closed_curve_is_the_same_curve_from_a_new_start() {
1881 let tol = Tolerances::millimetres();
1882 let ring = [
1884 Point::new(1.0, 0.0, 0.0),
1885 Point::new(1.0, 1.0, 0.5),
1886 Point::new(-1.0, 1.0, 0.0),
1887 Point::new(-1.0, -1.0, -0.5),
1888 Point::new(1.0, -1.0, 0.0),
1889 Point::new(1.0, 0.0, 0.0),
1890 ];
1891 let knots = KnotVector::clamped_uniform(3, ring.len()).unwrap();
1892 let curve = BSplineCurve::new(knots, ring.to_vec(), tol).unwrap();
1893 let (start, end) = curve.domain();
1894 let seam = 0.35;
1895 let moved = curve.reseamed_at(seam, tol).unwrap();
1896 let (new_start, new_end) = moved.domain();
1897 assert!((new_start - seam).abs() < 1e-12, "begins at the new seam");
1898 assert!(
1899 ((new_end - new_start) - (end - start)).abs() < 1e-12,
1900 "keeps its length"
1901 );
1902 for i in 0..=40 {
1903 let s = (end - start) * f64::from(i) / 40.0;
1904 let old_u = if seam + s <= end {
1905 seam + s
1906 } else {
1907 seam + s - (end - start)
1908 };
1909 let a = curve.point_at(old_u, tol).unwrap();
1910 let b = moved.point_at(new_start + s, tol).unwrap();
1911 assert!(a.is_equal(b, tol), "at {s} along: {a:?} against {b:?}");
1912 }
1913 }
1914}
1915
1916#[cfg(test)]
1917#[allow(clippy::unwrap_used)]
1918mod tests {
1919 use super::*;
1920 use approx::assert_relative_eq;
1921 use ogeom_math::{Direction, Frame};
1922
1923 const T: Tolerances = Tolerances::millimetres();
1924
1925 fn tilted() -> Frame {
1926 Frame::new(
1927 Point::new(1.0, -2.0, 3.0),
1928 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
1929 Direction::X,
1930 T,
1931 )
1932 .unwrap()
1933 }
1934
1935 fn every_curve() -> Vec<Curve> {
1936 let spline = {
1937 let control = vec![
1938 Point::new(0.0, 0.0, 0.0),
1939 Point::new(1.0, 2.0, 0.0),
1940 Point::new(3.0, 1.0, 1.0),
1941 Point::new(5.0, 0.0, 2.0),
1942 Point::new(6.0, -1.0, 0.0),
1943 ];
1944 let knots = KnotVector::clamped_uniform(3, control.len()).unwrap();
1945 BSplineCurve::new(knots, control, T).unwrap()
1946 };
1947 vec![
1948 LineCurve::segment(Point::ORIGIN, Point::new(3.0, 4.0, 0.0), T)
1949 .unwrap()
1950 .into(),
1951 CircleCurve::new(Circle::new(tilted(), 2.0, T).unwrap()).into(),
1952 EllipseCurve::new(Ellipse::new(tilted(), 5.0, 3.0, T).unwrap()).into(),
1953 HyperbolaCurve::new(Hyperbola::new(tilted(), 3.0, 4.0, T).unwrap(), 1.5)
1954 .unwrap()
1955 .into(),
1956 ParabolaCurve::new(Parabola::new(tilted(), 2.0, T).unwrap(), 4.0)
1957 .unwrap()
1958 .into(),
1959 spline.clone().into(),
1960 HelixCurve::new(tilted(), 2.5, 1.25, 2.0).unwrap().into(),
1961 TrimmedCurve::new(spline.into(), 0.2, 0.8, T)
1962 .unwrap()
1963 .into(),
1964 ]
1965 }
1966
1967 #[test]
1968 fn a_helix_rises_one_pitch_per_turn_and_knows_its_length() {
1969 let helix = HelixCurve::new(Frame::WORLD, 3.0, 2.0, 2.0).unwrap();
1970 let tau = core::f64::consts::TAU;
1971 let start = helix.point_at(0.0, T).unwrap();
1972 let after_one_turn = helix.point_at(tau, T).unwrap();
1973 assert_relative_eq!(start.x, 3.0);
1974 assert_relative_eq!(after_one_turn.x, 3.0, epsilon = 1e-12);
1975 assert_relative_eq!(after_one_turn.y, 0.0, epsilon = 1e-12);
1976 assert_relative_eq!(after_one_turn.z - start.z, 2.0, epsilon = 1e-12);
1977
1978 let exact = helix.arc_length(0.0, 2.0 * tau);
1981 assert_relative_eq!(exact, 2.0 * tau * 3.0f64.hypot(2.0 / tau), epsilon = 1e-12);
1982 let mut chords = 0.0;
1983 let n = 20_000;
1984 for i in 0..n {
1985 let a = 2.0 * tau * f64::from(i) / f64::from(n);
1986 let b = 2.0 * tau * f64::from(i + 1) / f64::from(n);
1987 chords += helix
1988 .point_at(a, T)
1989 .unwrap()
1990 .distance(helix.point_at(b, T).unwrap());
1991 }
1992 assert!((exact - chords) / exact < 1e-6, "{exact} vs {chords}");
1993
1994 let left = HelixCurve::new(Frame::WORLD, 3.0, -2.0, 2.0).unwrap();
1998 let q = left.point_at(tau / 4.0, T).unwrap();
1999 assert_relative_eq!(q.y, 3.0, epsilon = 1e-12);
2000 assert!(q.z < 0.0);
2001 }
2002
2003 #[test]
2004 fn a_reversed_helix_swaps_its_ends_and_flips_its_tangent() {
2005 let helix: Curve = HelixCurve::new(tilted(), 2.0, 1.0, 1.5).unwrap().into();
2006 let (lo, hi) = helix.domain();
2007 let back = helix.reversed();
2008 assert_relative_eq!(
2009 helix
2010 .point_at(lo, T)
2011 .unwrap()
2012 .distance(back.point_at(hi, T).unwrap()),
2013 0.0,
2014 epsilon = 1e-12
2015 );
2016 let d_fwd = helix.d1_at(f64::midpoint(lo, hi), T).unwrap();
2017 let d_back = back.d1_at(f64::midpoint(lo, hi), T).unwrap();
2018 assert_relative_eq!((d_fwd + d_back).magnitude(), 0.0, epsilon = 1e-12);
2019 }
2020
2021 #[test]
2022 fn an_offset_circle_is_the_larger_circle() {
2023 let circle: Curve = CircleCurve::new(Circle::new(tilted(), 2.0, T).unwrap()).into();
2027 let bigger = Circle::new(tilted(), 3.0, T).unwrap();
2028 let offset = OffsetCurve::new(circle, 1.0, tilted().z()).unwrap();
2029 for i in 0..8 {
2030 let t = core::f64::consts::TAU * f64::from(i) / 8.0;
2031 let p = offset.point_at(t, T).unwrap();
2032 assert_relative_eq!(p.distance(bigger.centre()), 3.0, epsilon = 1e-12);
2033 }
2034 let h = 1e-6;
2036 let d = offset.d1_at(1.0, T).unwrap();
2037 let fd = (offset.point_at(1.0 + h, T).unwrap() - offset.point_at(1.0 - h, T).unwrap())
2038 / (2.0 * h);
2039 assert_relative_eq!((d - fd).magnitude(), 0.0, epsilon = 1e-5);
2040 assert!(offset.derivatives_at(1.0, 2, T).is_err());
2042 }
2043
2044 #[test]
2045 fn a_sloped_line_on_a_cylinder_chart_is_a_helix() {
2046 use crate::curve2d::{Line2d, PlanarCurve};
2047 use crate::surface::{CylinderSurface, SurfaceGeometry};
2048 use ogeom_math::{Cylinder, Point2};
2049
2050 let radius = 3.0;
2054 let pitch = 2.0;
2055 let tau = core::f64::consts::TAU;
2056 let cylinder = SurfaceGeometry::Cylinder(
2057 CylinderSurface::new(Cylinder::new(Frame::WORLD, radius, T).unwrap(), (-1.0, 5.0))
2058 .unwrap(),
2059 );
2060 let rise = pitch / tau;
2061 let slope = (1.0 + rise * rise).sqrt();
2062 let line = Line2d::segment(Point2::new(0.0, 0.0), Point2::new(tau, pitch), T).unwrap();
2063 let on_surface = CurveOnSurface::new(PlanarCurve::Line(line), cylinder);
2064 let helix = HelixCurve::new(Frame::WORLD, radius, pitch, 1.0).unwrap();
2065 for i in 0..=8 {
2066 let angle = tau * f64::from(i) / 8.0;
2067 let lifted = on_surface.point_at(angle * slope, T).unwrap();
2069 let wound = helix.point_at(angle, T).unwrap();
2070 assert_relative_eq!(lifted.distance(wound), 0.0, epsilon = 1e-9);
2071 }
2072 let h = 1e-6;
2075 let d2 = on_surface.derivatives_at(2.0, 2, T).unwrap()[2];
2076 let fd = (on_surface.d1_at(2.0 + h, T).unwrap() - on_surface.d1_at(2.0 - h, T).unwrap())
2077 / (2.0 * h);
2078 assert_relative_eq!((d2 - fd).magnitude(), 0.0, epsilon = 1e-5);
2079 }
2080
2081 #[test]
2082 fn a_helix_has_no_exact_spline_and_says_so() {
2083 let helix: Curve = HelixCurve::new(Frame::WORLD, 1.0, 1.0, 1.0).unwrap().into();
2084 assert!(helix.to_bspline(T).is_err());
2085 }
2086
2087 fn interior(c: &Curve, n: usize) -> Vec<f64> {
2089 let (a, b) = c.domain();
2090 (1..n)
2091 .map(|i| {
2092 #[allow(clippy::cast_precision_loss)]
2093 let t = i as f64 / n as f64;
2094 a + (b - a) * t
2095 })
2096 .collect()
2097 }
2098
2099 #[test]
2100 fn every_curves_derivative_agrees_with_finite_differences() {
2101 let h = 1e-6;
2102 for c in every_curve() {
2103 for u in interior(&c, 8) {
2104 let d1 = c.d1_at(u, T).unwrap();
2105 let numeric = (c.point_at(u + h, T).unwrap() - c.point_at(u - h, T).unwrap())
2106 * (1.0 / (2.0 * h));
2107 let scale = numeric.magnitude().max(1.0);
2108 assert!(
2109 (d1 - numeric).magnitude() <= 1e-5 * scale,
2110 "{:?} at {u}: {d1:?} vs {numeric:?}",
2111 c.kind()
2112 );
2113 }
2114 }
2115 }
2116
2117 #[test]
2118 fn derivatives_at_zero_returns_the_point_itself() {
2119 for c in every_curve() {
2120 for u in interior(&c, 4) {
2121 let d = c.derivatives_at(u, 0, T).unwrap();
2122 assert_eq!(d.len(), 1);
2123 assert!(Point::from_vector(d[0]).is_equal(c.point_at(u, T).unwrap(), T));
2124 }
2125 }
2126 }
2127
2128 #[test]
2129 fn out_of_domain_parameters_are_refused_for_non_periodic_curves() {
2130 for c in every_curve() {
2131 let (a, b) = c.domain();
2132 if c.is_periodic() {
2133 assert!(c.point_at(b + 1.0, T).is_ok());
2135 assert!(c.point_at(a - 1.0, T).is_ok());
2136 } else {
2137 assert!(c.point_at(b + 1.0, T).is_err(), "{:?}", c.kind());
2138 assert!(c.point_at(a - 1.0, T).is_err(), "{:?}", c.kind());
2139 }
2140 }
2141 }
2142
2143 #[test]
2144 fn a_periodic_curve_wraps_to_the_same_point() {
2145 let c: Curve = CircleCurve::new(Circle::new(tilted(), 2.0, T).unwrap()).into();
2146 assert!(c.is_periodic() && c.is_closed(T));
2147 let base = c.point_at(0.7, T).unwrap();
2148 for k in [-2.0_f64, -1.0, 1.0, 3.0] {
2149 let wrapped = c
2150 .point_at(k.mul_add(core::f64::consts::TAU, 0.7), T)
2151 .unwrap();
2152 assert!(base.is_equal(wrapped, T), "wrap by {k} moved the point");
2153 }
2154 }
2155
2156 #[test]
2157 fn reversal_traverses_the_same_points_backwards() {
2158 for c in every_curve() {
2159 let r = c.reversed();
2160 let (a, b) = c.domain();
2161 assert_eq!(r.domain(), (a, b), "{:?} changed its domain", c.kind());
2162 for i in 0..=8 {
2163 let t = f64::from(i) / 8.0;
2164 let forward = c.point_at(a + (b - a) * t, T).unwrap();
2165 let backward = r.point_at(a + (b - a) * (1.0 - t), T).unwrap();
2166 assert!(
2167 forward.is_equal(backward, T),
2168 "{:?} at t = {t}: {forward:?} vs {backward:?}",
2169 c.kind()
2170 );
2171 }
2172 }
2173 }
2174
2175 #[test]
2176 fn reversing_twice_is_the_identity() {
2177 for c in every_curve() {
2178 let twice = c.reversed().reversed();
2179 for u in interior(&c, 8) {
2180 assert!(
2181 c.point_at(u, T)
2182 .unwrap()
2183 .is_equal(twice.point_at(u, T).unwrap(), T),
2184 "{:?}",
2185 c.kind()
2186 );
2187 }
2188 }
2189 }
2190
2191 #[test]
2192 fn a_reversed_curves_tangent_points_the_other_way() {
2193 for c in every_curve() {
2194 let r = c.reversed();
2195 let (a, b) = c.domain();
2196 let u = a + (b - a) * 0.4;
2197 let forward = c.tangent_at(u, T).unwrap();
2198 let backward = r.tangent_at(mirror(u, a, b), T).unwrap();
2199 assert!(forward.is_opposite(backward, T), "{:?}", c.kind());
2200 }
2201 }
2202
2203 #[test]
2204 fn transforms_move_curves_and_preserve_their_shape() {
2205 let t =
2206 Transform::rotation(Axis::X, 0.7) * Transform::translation(Vector::new(1.0, 2.0, 3.0));
2207 for c in every_curve() {
2208 let moved = c.transformed(&t, T).unwrap();
2209 assert_eq!(moved.kind(), c.kind());
2210 for u in interior(&c, 8) {
2211 let expected = t.apply(c.point_at(u, T).unwrap());
2212 assert!(
2213 moved.point_at(u, T).unwrap().is_equal(expected, T),
2214 "{:?} at {u}",
2215 c.kind()
2216 );
2217 }
2218 }
2219 }
2220
2221 #[test]
2222 fn a_scaling_rescales_a_lines_arc_length_domain() {
2223 let line = LineCurve::segment(Point::ORIGIN, Point::new(3.0, 4.0, 0.0), T).unwrap();
2226 let c: Curve = line.into();
2227 assert_eq!(c.domain(), (0.0, 5.0));
2228 let scaled = c
2229 .transformed(&Transform::scaling(Point::ORIGIN, 2.0, T).unwrap(), T)
2230 .unwrap();
2231 assert_eq!(scaled.domain(), (0.0, 10.0));
2232 assert!(
2233 scaled
2234 .end(T)
2235 .unwrap()
2236 .is_equal(Point::new(6.0, 8.0, 0.0), T)
2237 );
2238 }
2239
2240 #[test]
2241 fn mirroring_a_circle_moves_every_point_by_the_mirror() {
2242 let c: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2246 let m = Transform::plane_mirror(Point::ORIGIN, Direction::X);
2247 let mirrored = c.transformed(&m, T).unwrap();
2248 for u in interior(&c, 8) {
2249 let expected = m.apply(c.point_at(u, T).unwrap());
2250 assert!(
2251 mirrored.point_at(u, T).unwrap().is_equal(expected, T),
2252 "at {u}"
2253 );
2254 }
2255 }
2256
2257 #[test]
2258 fn a_line_segments_parameter_is_arc_length() {
2259 let line = LineCurve::segment(Point::ORIGIN, Point::new(3.0, 4.0, 0.0), T).unwrap();
2260 assert_eq!(line.domain(), (0.0, 5.0));
2261 assert!(line.point_at(0.0, T).unwrap().is_equal(Point::ORIGIN, T));
2262 assert!(
2263 line.point_at(5.0, T)
2264 .unwrap()
2265 .is_equal(Point::new(3.0, 4.0, 0.0), T)
2266 );
2267 assert!(
2268 line.point_at(2.5, T)
2269 .unwrap()
2270 .is_equal(Point::new(1.5, 2.0, 0.0), T)
2271 );
2272 assert_relative_eq!(
2273 line.d1_at(1.0, T).unwrap().magnitude(),
2274 1.0,
2275 epsilon = 1e-15
2276 );
2277 }
2278
2279 #[test]
2280 fn degenerate_constructions_are_refused() {
2281 assert!(LineCurve::segment(Point::ORIGIN, Point::ORIGIN, T).is_err());
2282 assert!(LineCurve::over(Axis::X, 1.0, 1.0).is_err());
2283 assert!(LineCurve::over(Axis::X, 0.0, f64::NAN).is_err());
2284 assert!(HyperbolaCurve::new(Hyperbola::new(tilted(), 1.0, 1.0, T).unwrap(), 0.0).is_err());
2285 assert!(ParabolaCurve::new(Parabola::new(tilted(), 1.0, T).unwrap(), -1.0).is_err());
2286 }
2287
2288 #[test]
2289 fn trimming_is_bounds_checked() {
2290 let base: Curve = LineCurve::over(Axis::X, 0.0, 10.0).unwrap().into();
2291 assert!(TrimmedCurve::new(base.clone(), 2.0, 8.0, T).is_ok());
2292 assert!(
2293 TrimmedCurve::new(base.clone(), 8.0, 2.0, T).is_err(),
2294 "empty"
2295 );
2296 assert!(
2297 TrimmedCurve::new(base.clone(), 5.0, 5.0, T).is_err(),
2298 "empty"
2299 );
2300 assert!(
2301 TrimmedCurve::new(base, -1.0, 5.0, T).is_err(),
2302 "outside the basis"
2303 );
2304 }
2305
2306 #[test]
2307 fn a_trimmed_curve_agrees_with_its_basis() {
2308 let base: Curve = CircleCurve::new(Circle::new(tilted(), 2.0, T).unwrap()).into();
2309 let trimmed = TrimmedCurve::new(base.clone(), 0.5, 2.0, T).unwrap();
2310 assert_eq!(trimmed.domain(), (0.5, 2.0));
2311 for i in 0..=8 {
2312 let u = 0.5 + 1.5 * (f64::from(i) / 8.0);
2313 assert!(
2314 trimmed
2315 .point_at(u, T)
2316 .unwrap()
2317 .is_equal(base.point_at(u, T).unwrap(), T)
2318 );
2319 }
2320 assert!(trimmed.point_at(0.4, T).is_err());
2321 assert!(trimmed.point_at(2.1, T).is_err());
2322 }
2323
2324 #[test]
2325 fn a_rational_curve_is_recognized_and_a_uniformly_weighted_one_is_not() {
2326 let knots = KnotVector::clamped_uniform(2, 3).unwrap();
2327 let points = [
2328 Point::new(1.0, 0.0, 0.0),
2329 Point::new(1.0, 1.0, 0.0),
2330 Point::new(0.0, 1.0, 0.0),
2331 ];
2332
2333 let uniform: Vec<_> = points
2334 .iter()
2335 .map(|p| Weighted::new(*p, 3.0, T).unwrap())
2336 .collect();
2337 assert!(
2338 !BSplineCurve::rational(knots.clone(), uniform)
2339 .unwrap()
2340 .is_rational(),
2341 "equal weights are polynomial whatever their value"
2342 );
2343
2344 let w = core::f64::consts::FRAC_1_SQRT_2;
2345 let arc: Vec<_> = points
2346 .iter()
2347 .zip([1.0, w, 1.0])
2348 .map(|(p, w)| Weighted::new(*p, w, T).unwrap())
2349 .collect();
2350 let c = BSplineCurve::rational(knots, arc).unwrap();
2351 assert!(c.is_rational());
2352 for i in 0..=20 {
2354 let u = f64::from(i) / 20.0;
2355 assert_relative_eq!(
2356 c.point_at(u, T).unwrap().to_vector().magnitude(),
2357 1.0,
2358 epsilon = 1e-14
2359 );
2360 }
2361 }
2362
2363 #[test]
2364 fn spline_continuity_follows_interior_knot_multiplicity() {
2365 let control = vec![
2366 Point::ORIGIN,
2367 Point::new(1.0, 1.0, 0.0),
2368 Point::new(2.0, 0.0, 0.0),
2369 Point::new(3.0, 1.0, 0.0),
2370 Point::new(4.0, 0.0, 0.0),
2371 ];
2372 let smooth = BSplineCurve::new(
2375 KnotVector::clamped_uniform(3, control.len()).unwrap(),
2376 control.clone(),
2377 T,
2378 )
2379 .unwrap();
2380 assert_eq!(smooth.continuity(), Continuity::C2);
2381
2382 let bezier = BSplineCurve::new(
2385 KnotVector::clamped_uniform(4, control.len()).unwrap(),
2386 control.clone(),
2387 T,
2388 )
2389 .unwrap();
2390 assert_eq!(bezier.continuity(), Continuity::CInfinity);
2391
2392 let kinked = BSplineCurve::new(
2394 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
2395 {
2396 let mut c = control.clone();
2397 c.push(Point::new(5.0, 1.0, 0.0));
2398 c
2399 },
2400 T,
2401 )
2402 .unwrap();
2403 assert_eq!(kinked.continuity(), Continuity::C1);
2404
2405 let corner = BSplineCurve::new(
2407 KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap(),
2408 control,
2409 T,
2410 )
2411 .unwrap();
2412 assert_eq!(corner.continuity(), Continuity::C0);
2413 }
2414
2415 #[test]
2416 fn spline_refinement_does_not_move_the_curve() {
2417 let control = vec![
2418 Point::ORIGIN,
2419 Point::new(1.0, 2.0, 0.0),
2420 Point::new(3.0, 1.0, 1.0),
2421 Point::new(5.0, 0.0, 2.0),
2422 ];
2423 let c = BSplineCurve::new(
2424 KnotVector::clamped_uniform(3, control.len()).unwrap(),
2425 control,
2426 T,
2427 )
2428 .unwrap();
2429 let refined = c.with_knot_inserted(0.5, 1, T).unwrap();
2430 let elevated = c.elevated(T).unwrap();
2431 assert_eq!(elevated.degree(), 4);
2432 for i in 0..=20 {
2433 let u = f64::from(i) / 20.0;
2434 let base = c.point_at(u, T).unwrap();
2435 assert!(refined.point_at(u, T).unwrap().is_equal(base, T));
2436 assert!(elevated.point_at(u, T).unwrap().is_equal(base, T));
2437 }
2438 }
2439
2440 #[test]
2441 fn curvature_of_a_circle_is_the_reciprocal_of_its_radius() {
2442 for r in [0.5_f64, 2.0, 50.0] {
2443 let c: Curve = CircleCurve::new(Circle::new(tilted(), r, T).unwrap()).into();
2444 assert_relative_eq!(
2445 c.curvature_at(1.1, T).unwrap(),
2446 1.0 / r,
2447 max_relative = 1e-12
2448 );
2449 }
2450 let line: Curve = LineCurve::segment(Point::ORIGIN, Point::new(1.0, 1.0, 1.0), T)
2452 .unwrap()
2453 .into();
2454 assert_relative_eq!(line.curvature_at(0.5, T).unwrap(), 0.0);
2455 }
2456
2457 #[test]
2458 fn kinds_are_reported_for_dispatch() {
2459 let kinds: Vec<_> = every_curve().iter().map(Curve3d::kind).collect();
2460 assert_eq!(
2461 kinds,
2462 vec![
2463 CurveKind::Line,
2464 CurveKind::Circle,
2465 CurveKind::Ellipse,
2466 CurveKind::Hyperbola,
2467 CurveKind::Parabola,
2468 CurveKind::BSpline,
2469 CurveKind::Helix,
2470 CurveKind::Trimmed,
2471 ]
2472 );
2473 }
2474}
2475
2476#[cfg(test)]
2477#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
2478mod conical_tests {
2479 use super::*;
2480 use ogeom_math::Vector;
2481
2482 #[test]
2486 fn a_curve_extended_to_a_point_ends_there_smoothly() {
2487 use crate::Curve3d as _;
2488 let cubic = BSplineCurve::rational(
2489 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
2490 [
2491 Point::new(0.0, 0.0, 0.0),
2492 Point::new(1.0, 2.0, 0.0),
2493 Point::new(3.0, 2.5, 1.0),
2494 Point::new(4.0, 0.5, 1.0),
2495 Point::new(6.0, 1.0, 0.0),
2496 ]
2497 .iter()
2498 .map(|p| Weighted::new(*p, 1.0, T).unwrap())
2499 .collect(),
2500 )
2501 .unwrap();
2502 let circle = Circle::new(Frame::WORLD, 5.0, T).unwrap();
2503 let arc = Curve::Trimmed(Box::new(
2504 TrimmedCurve::new(
2505 Curve::Circle(CircleCurve::new(circle)),
2506 0.0,
2507 core::f64::consts::FRAC_PI_2,
2508 T,
2509 )
2510 .unwrap(),
2511 ))
2512 .to_bspline(T)
2513 .unwrap();
2514 for curve in [cubic, arc] {
2515 let (lo, hi) = curve.domain();
2516 for at_end in [true, false] {
2517 let target = if at_end {
2518 Point::new(8.0, 3.0, -1.0)
2519 } else {
2520 Point::new(-2.0, -1.0, 0.5)
2521 };
2522 let longer = curve.extended_to(at_end, target, 2, T).unwrap();
2523 let (elo, ehi) = longer.domain();
2524 let end = longer.point_at(if at_end { ehi } else { elo }, T).unwrap();
2525 assert!(end.distance(target) < 1e-9, "ends at the point: {end:?}");
2526 for i in 0..=8 {
2527 let u = lo + (hi - lo) * f64::from(i) / 8.0;
2528 let (was, now) = (
2529 curve.point_at(u, T).unwrap(),
2530 longer.point_at(u, T).unwrap(),
2531 );
2532 assert!(was.distance(now) < 1e-9, "the run itself at {u}");
2533 }
2534 let join = if at_end { hi } else { lo };
2535 let step = if at_end { 1e-6 } else { -1e-6 };
2536 let inside = curve.derivatives_at(join - step, 2, T).unwrap();
2537 let outside = longer.derivatives_at(join + step, 2, T).unwrap();
2538 for order in 1..=2 {
2539 let gap = (inside[order] - outside[order]).magnitude();
2540 let scale = inside[order].magnitude().max(1.0);
2541 assert!(gap < scale * 1e-3, "order {order} across the join: {gap}");
2542 }
2543 }
2544 }
2545 }
2546
2547 #[test]
2552 fn a_rational_arc_extended_stays_on_its_circle() {
2553 use crate::Curve3d as _;
2554 let circle = Circle::new(Frame::WORLD, 5.0, T).unwrap();
2555 let arc = TrimmedCurve::new(
2556 Curve::Circle(CircleCurve::new(circle)),
2557 0.0,
2558 core::f64::consts::FRAC_PI_2,
2559 T,
2560 )
2561 .unwrap();
2562 let spline = Curve::Trimmed(Box::new(arc)).to_bspline(T).unwrap();
2563 let (lo, hi) = spline.domain();
2564 for at_end in [true, false] {
2565 let longer = spline.extended(at_end, 4.0, 2, T).unwrap();
2566 let (elo, ehi) = longer.domain();
2567 for i in 0..=8 {
2568 let u = lo + (hi - lo) * f64::from(i) / 8.0;
2569 let (was, now) = (
2570 spline.point_at(u, T).unwrap(),
2571 longer.point_at(u, T).unwrap(),
2572 );
2573 assert!(was.distance(now) < 1e-9, "the arc itself at {u}");
2574 }
2575 let (from, to) = if at_end { (hi, ehi) } else { (elo, lo) };
2576 let mut swept = 0.0;
2577 let mut last = longer.point_at(from, T).unwrap();
2578 for i in 1..=64 {
2579 let u = from + (to - from) * f64::from(i) / 64.0;
2580 let p = longer.point_at(u, T).unwrap();
2581 assert!(
2582 (p.distance(Point::ORIGIN) - 5.0).abs() < 1e-9,
2583 "off the circle at {u}: {p:?}"
2584 );
2585 swept += last.distance(p);
2586 last = p;
2587 }
2588 assert!((swept - 4.0).abs() < 1e-3, "the length asked: {swept}");
2589 }
2590 let ring = BSplineCurve::periodic(
2591 &[
2592 Point::new(0.0, 0.0, 0.0),
2593 Point::new(1.0, 0.0, 0.0),
2594 Point::new(1.0, 1.0, 0.0),
2595 Point::new(0.0, 1.0, 0.0),
2596 ],
2597 3,
2598 T,
2599 )
2600 .unwrap();
2601 assert!(ring.extended(true, 1.0, 2, T).is_err(), "a ring has no end");
2602 }
2603
2604 const T: Tolerances = Tolerances::millimetres();
2605
2606 #[test]
2607 fn a_conical_helix_winds_its_cone_and_speaks_its_derivatives() {
2608 let tau = core::f64::consts::TAU;
2609 let helix = HelixCurve::conical(Frame::WORLD, 5.0, 3.0, 2.0, 0.0, 2.0 * tau).unwrap();
2611 for i in 0..=8 {
2612 let t = 2.0 * tau * f64::from(i) / 8.0;
2613 let p = helix.point_at(t, T).unwrap();
2614 let r = 2.0f64.mul_add(t / tau, 5.0);
2615 assert!((p.to_vector().dot(Vector::Z) - 3.0 * t / tau).abs() < 1e-9);
2616 assert!((p.x.hypot(p.y) - r).abs() < 1e-9, "radius at {t}");
2617 }
2618 let t = 1.234;
2620 let h = 1e-6;
2621 let d1 = helix.d1_at(t, T).unwrap();
2622 let fwd = helix.point_at(t + h, T).unwrap();
2623 let bwd = helix.point_at(t - h, T).unwrap();
2624 let fd = (fwd - bwd) / (2.0 * h);
2625 assert!((d1 - fd).magnitude() < 1e-6, "d1 {d1:?} against {fd:?}");
2626 }
2627
2628 #[test]
2629 fn a_helix_past_its_apex_is_refused_by_name() {
2630 let tau = core::f64::consts::TAU;
2631 let err = HelixCurve::conical(Frame::WORLD, 1.0, 3.0, -2.0, 0.0, 2.0 * tau).unwrap_err();
2632 assert!(err.to_string().contains("apex"), "{err}");
2633 }
2634
2635 #[test]
2636 fn a_periodic_bspline_wraps_smoothly_and_says_so() {
2637 let ring: Vec<Point> = (0..8)
2638 .map(|i| {
2639 let a = core::f64::consts::TAU * f64::from(i) / 8.0;
2640 Point::new(a.cos() * 4.0, a.sin() * 4.0, 0.0)
2641 })
2642 .collect();
2643 let curve = BSplineCurve::periodic(&ring, 3, T).unwrap();
2644 assert!(Curve3d::is_periodic(&curve));
2645 let (lo, hi) = Curve3d::domain(&curve);
2646 let p_lo = curve.point_at(lo, T).unwrap();
2648 let p_hi = curve.point_at(hi, T).unwrap();
2649 assert!(p_lo.distance(p_hi) < 1e-9);
2650 let d_lo = curve.d1_at(lo, T).unwrap();
2651 let d_hi = curve.d1_at(hi, T).unwrap();
2652 assert!((d_lo - d_hi).magnitude() < 1e-9, "C1 across the seam");
2653 let inside = curve.point_at(lo + 0.4, T).unwrap();
2655 let wrapped = curve.point_at(hi + 0.4, T).unwrap();
2656 assert!(inside.distance(wrapped) < 1e-9);
2657 }
2658}