1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
24
25use crate::{
26 Axis, Circle, Cone, Cylinder, Direction, Ellipse, Hyperbola, Parabola, Plane, Point, Sphere,
27 Torus, Vector,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct CurvePoint {
33 pub point: Point,
35 pub d1: Vector,
37 pub d2: Vector,
39}
40
41impl CurvePoint {
42 pub fn tangent(&self, tol: Tolerances) -> OgeomResult<Direction> {
49 Direction::new(self.d1, tol)
50 }
51
52 #[must_use]
57 pub fn curvature(&self) -> f64 {
58 let speed = self.d1.magnitude();
59 if speed == 0.0 {
60 return 0.0;
61 }
62 self.d1.cross(self.d2).magnitude() / (speed * speed * speed)
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct SurfacePoint {
69 pub point: Point,
71 pub du: Vector,
73 pub dv: Vector,
75}
76
77impl SurfacePoint {
78 pub fn normal(&self, tol: Tolerances) -> OgeomResult<Direction> {
86 if self.is_degenerate(tol) {
87 ogeom_bail!(
88 Construction,
89 "surface point is degenerate; the tangents determine no normal"
90 );
91 }
92 Direction::new(self.du.cross(self.dv), tol)
93 }
94
95 #[must_use]
106 pub fn is_degenerate(&self, tol: Tolerances) -> bool {
107 let scale = self.du.magnitude().max(self.dv.magnitude());
108 self.du.cross(self.dv).magnitude() <= tol.angular() * scale * scale
109 }
110}
111
112#[must_use]
114pub fn wrap_angle(angle: f64) -> f64 {
115 let wrapped = angle.rem_euclid(core::f64::consts::TAU);
116 if wrapped >= core::f64::consts::TAU {
119 0.0
120 } else {
121 wrapped
122 }
123}
124
125#[must_use]
127pub fn wrap_signed_angle(angle: f64) -> f64 {
128 let wrapped = wrap_angle(angle);
129 if wrapped > core::f64::consts::PI {
130 wrapped - core::f64::consts::TAU
131 } else {
132 wrapped
133 }
134}
135
136#[must_use]
138pub fn line_at(axis: Axis, t: f64) -> CurvePoint {
139 CurvePoint {
140 point: axis.point_at(t),
141 d1: axis.direction.vector(),
142 d2: Vector::ZERO,
143 }
144}
145
146#[must_use]
148pub fn line_parameter(axis: Axis, p: Point) -> f64 {
149 axis.parameter_of(p)
150}
151
152#[must_use]
154pub fn circle_at(circle: &Circle, angle: f64) -> CurvePoint {
155 let f = circle.frame();
156 let r = circle.radius();
157 let (sin, cos) = angle.sin_cos();
158 let (x, y) = (f.x().vector(), f.y().vector());
159 CurvePoint {
160 point: circle.centre() + x * (r * cos) + y * (r * sin),
161 d1: x * (-r * sin) + y * (r * cos),
162 d2: x * (-r * cos) + y * (-r * sin),
163 }
164}
165
166pub fn circle_parameter(circle: &Circle, p: Point, tol: Tolerances) -> OgeomResult<f64> {
173 let local = circle.frame().to_local(p);
174 if local.x.hypot(local.y) <= tol.confusion() {
175 ogeom_bail!(
176 Construction,
177 "point is on the circle's axis; no nearest angle"
178 );
179 }
180 Ok(wrap_angle(local.y.atan2(local.x)))
181}
182
183#[must_use]
189pub fn ellipse_at(ellipse: &Ellipse, angle: f64) -> CurvePoint {
190 let f = ellipse.frame();
191 let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
192 let (sin, cos) = angle.sin_cos();
193 let (x, y) = (f.x().vector(), f.y().vector());
194 CurvePoint {
195 point: ellipse.centre() + x * (a * cos) + y * (b * sin),
196 d1: x * (-a * sin) + y * (b * cos),
197 d2: x * (-a * cos) + y * (-b * sin),
198 }
199}
200
201pub fn ellipse_parameter(ellipse: &Ellipse, p: Point, tol: Tolerances) -> OgeomResult<f64> {
208 let local = ellipse.frame().to_local(p);
209 if local.x.hypot(local.y) <= tol.confusion() {
210 ogeom_bail!(Construction, "point projects to the ellipse's centre");
211 }
212 Ok(wrap_angle(
215 (local.y / ellipse.minor_radius()).atan2(local.x / ellipse.major_radius()),
216 ))
217}
218
219#[must_use]
221pub fn hyperbola_at(hyperbola: &Hyperbola, t: f64) -> CurvePoint {
222 let f = hyperbola.frame();
223 let (a, b) = (hyperbola.major_radius(), hyperbola.minor_radius());
224 let (cosh, sinh) = (t.cosh(), t.sinh());
225 let (x, y) = (f.x().vector(), f.y().vector());
226 CurvePoint {
227 point: hyperbola.centre() + x * (a * cosh) + y * (b * sinh),
228 d1: x * (a * sinh) + y * (b * cosh),
229 d2: x * (a * cosh) + y * (b * sinh),
233 }
234}
235
236pub fn hyperbola_parameter(hyperbola: &Hyperbola, p: Point, tol: Tolerances) -> OgeomResult<f64> {
243 let local = hyperbola.frame().to_local(p);
244 let a = local.x / hyperbola.major_radius();
245 if a < 1.0 - tol.parametric() {
246 ogeom_bail!(
247 Domain,
248 "point is not on the described branch of the hyperbola"
249 );
250 }
251 Ok((local.y / hyperbola.minor_radius()).asinh())
254}
255
256#[must_use]
258pub fn parabola_at(parabola: &Parabola, t: f64) -> CurvePoint {
259 let f = parabola.frame();
260 let focal = parabola.focal();
261 let (x, y) = (f.x().vector(), f.y().vector());
262 let scale = 1.0 / (4.0 * focal);
263 CurvePoint {
264 point: parabola.apex() + x * (t * t * scale) + y * t,
265 d1: x * (2.0 * t * scale) + y,
266 d2: x * (2.0 * scale),
267 }
268}
269
270#[must_use]
272pub fn parabola_parameter(parabola: &Parabola, p: Point) -> f64 {
273 parabola.frame().to_local(p).y
274}
275
276#[must_use]
278pub fn plane_at(plane: &Plane, u: f64, v: f64) -> SurfacePoint {
279 let f = plane.frame();
280 SurfacePoint {
281 point: f.origin() + f.x() * u + f.y() * v,
282 du: f.x().vector(),
283 dv: f.y().vector(),
284 }
285}
286
287#[must_use]
289pub fn plane_parameters(plane: &Plane, p: Point) -> (f64, f64) {
290 let local = plane.frame().to_local(p);
291 (local.x, local.y)
292}
293
294#[must_use]
296pub fn cylinder_at(cylinder: &Cylinder, angle: f64, height: f64) -> SurfacePoint {
297 let f = cylinder.frame();
298 let r = cylinder.radius();
299 let (sin, cos) = angle.sin_cos();
300 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
301 SurfacePoint {
302 point: f.origin() + x * (r * cos) + y * (r * sin) + z * height,
303 du: x * (-r * sin) + y * (r * cos),
304 dv: z,
305 }
306}
307
308pub fn cylinder_parameters(
315 cylinder: &Cylinder,
316 p: Point,
317 tol: Tolerances,
318) -> OgeomResult<(f64, f64)> {
319 let local = cylinder.frame().to_local(p);
320 if local.x.hypot(local.y) <= tol.confusion() {
321 ogeom_bail!(
322 Construction,
323 "point is on the cylinder's axis; no nearest angle"
324 );
325 }
326 Ok((wrap_angle(local.y.atan2(local.x)), local.z))
327}
328
329#[must_use]
335pub fn cone_at(cone: &Cone, angle: f64, height: f64) -> SurfacePoint {
336 let f = cone.frame();
337 let r = cone.radius_at(height);
338 let slope = cone.half_angle().tan();
339 let (sin, cos) = angle.sin_cos();
340 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
341 SurfacePoint {
342 point: f.origin() + x * (r * cos) + y * (r * sin) + z * height,
343 du: x * (-r * sin) + y * (r * cos),
344 dv: x * (slope * cos) + y * (slope * sin) + z,
345 }
346}
347
348pub fn cone_parameters(cone: &Cone, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
355 let local = cone.frame().to_local(p);
356 if local.x.hypot(local.y) <= tol.confusion() {
357 ogeom_bail!(
358 Construction,
359 "point is on the cone's axis; no nearest angle"
360 );
361 }
362 Ok((wrap_angle(local.y.atan2(local.x)), local.z))
363}
364
365#[must_use]
369pub fn sphere_at(sphere: &Sphere, longitude: f64, latitude: f64) -> SurfacePoint {
370 let f = sphere.frame();
371 let r = sphere.radius();
372 let (sin_lon, cos_lon) = longitude.sin_cos();
373 let (sin_lat, cos_lat) = latitude.sin_cos();
374 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
375 let ring = r * cos_lat;
376 SurfacePoint {
377 point: sphere.centre() + x * (ring * cos_lon) + y * (ring * sin_lon) + z * (r * sin_lat),
378 du: x * (-ring * sin_lon) + y * (ring * cos_lon),
379 dv: x * (-r * sin_lat * cos_lon) + y * (-r * sin_lat * sin_lon) + z * (r * cos_lat),
380 }
381}
382
383pub fn sphere_parameters(sphere: &Sphere, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
392 let local = sphere.frame().to_local(p);
393 let ring = local.x.hypot(local.y);
394 if ring.hypot(local.z) <= tol.confusion() {
395 ogeom_bail!(Construction, "point is the sphere's centre");
396 }
397 let longitude = if ring <= tol.confusion() {
398 0.0
399 } else {
400 wrap_angle(local.y.atan2(local.x))
401 };
402 Ok((longitude, local.z.atan2(ring)))
406}
407
408#[must_use]
410pub fn torus_at(torus: &Torus, u: f64, v: f64) -> SurfacePoint {
411 let f = torus.frame();
412 let (major, minor) = (torus.major_radius(), torus.minor_radius());
413 let (sin_u, cos_u) = u.sin_cos();
414 let (sin_v, cos_v) = v.sin_cos();
415 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
416 let out = x * cos_u + y * sin_u;
417 let radius = minor.mul_add(cos_v, major);
418 SurfacePoint {
419 point: f.origin() + out * radius + z * (minor * sin_v),
420 du: (x * -sin_u + y * cos_u) * radius,
421 dv: out * (-minor * sin_v) + z * (minor * cos_v),
422 }
423}
424
425pub fn torus_parameters(torus: &Torus, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
432 let local = torus.frame().to_local(p);
433 let ring = local.x.hypot(local.y);
434 if ring <= tol.confusion() {
435 ogeom_bail!(Construction, "point is on the torus's axis");
436 }
437 let u = wrap_angle(local.y.atan2(local.x));
438 let radial = ring - torus.major_radius();
439 if radial.hypot(local.z) <= tol.confusion() {
440 ogeom_bail!(Construction, "point is on the tube's centre circle");
441 }
442 Ok((u, wrap_angle(local.z.atan2(radial))))
443}
444
445#[cfg(test)]
446#[allow(clippy::unwrap_used)]
447mod tests {
448 use super::*;
449 use crate::Frame;
450 use approx::assert_relative_eq;
451
452 const T: Tolerances = Tolerances::millimetres();
453 const PI: f64 = core::f64::consts::PI;
454
455 fn tilted() -> Frame {
456 Frame::new(
457 Point::new(1.0, -2.0, 3.0),
458 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
459 Direction::from_coords(1.0, 0.0, 0.0, T).unwrap(),
460 T,
461 )
462 .unwrap()
463 }
464
465 fn check_curve_derivatives(f: impl Fn(f64) -> CurvePoint, t: f64) {
467 let h = 1e-6;
468 let (a, b, c) = (f(t - h), f(t), f(t + h));
469 let d1 = (c.point - a.point) * (1.0 / (2.0 * h));
470 let d2 = (c.d1 - a.d1) * (1.0 / (2.0 * h));
471 let scale = d1.magnitude().max(1.0);
472 assert!((b.d1 - d1).magnitude() <= 1e-5 * scale, "d1 wrong at {t}");
473 assert!(
474 (b.d2 - d2).magnitude() <= 1e-5 * d2.magnitude().max(1.0),
475 "d2 wrong at {t}"
476 );
477 }
478
479 fn check_surface_derivatives(f: impl Fn(f64, f64) -> SurfacePoint, u: f64, v: f64) {
481 let h = 1e-6;
482 let du = (f(u + h, v).point - f(u - h, v).point) * (1.0 / (2.0 * h));
483 let dv = (f(u, v + h).point - f(u, v - h).point) * (1.0 / (2.0 * h));
484 let p = f(u, v);
485 assert!(
486 (p.du - du).magnitude() <= 1e-5 * du.magnitude().max(1.0),
487 "du wrong"
488 );
489 assert!(
490 (p.dv - dv).magnitude() <= 1e-5 * dv.magnitude().max(1.0),
491 "dv wrong"
492 );
493 }
494
495 #[test]
496 fn angle_wrapping_stays_inside_its_range() {
497 for a in [-10.0_f64, -PI, -1e-18, 0.0, 1.0, PI, 7.0, 100.0] {
498 let w = wrap_angle(a);
499 assert!(
500 (0.0..core::f64::consts::TAU).contains(&w),
501 "{a} wrapped to {w}"
502 );
503 let s = wrap_signed_angle(a);
504 assert!(
505 s > -PI - 1e-15 && s <= PI + 1e-15,
506 "{a} signed-wrapped to {s}"
507 );
508 }
509 assert_eq!(wrap_angle(-1e-300), 0.0);
512 }
513
514 #[test]
515 fn line_evaluation_and_inversion() {
516 let axis = Axis::new(Point::new(1.0, 2.0, 3.0), Direction::Z);
517 let c = line_at(axis, 5.0);
518 assert!(c.point.is_equal(Point::new(1.0, 2.0, 8.0), T));
519 assert!(c.d1.is_equal(Vector::Z, T));
520 assert_relative_eq!(c.curvature(), 0.0);
521 assert_relative_eq!(line_parameter(axis, c.point), 5.0, epsilon = 1e-12);
522 }
523
524 #[test]
525 fn circle_evaluation_derivatives_and_inversion() {
526 let c = Circle::new(tilted(), 3.0, T).unwrap();
527 for i in 0..12 {
528 let angle = f64::from(i) * PI / 6.0;
529 let p = circle_at(&c, angle);
530 assert!(c.contains(p.point, T));
531 assert_relative_eq!(
532 circle_parameter(&c, p.point, T).unwrap(),
533 wrap_angle(angle),
534 epsilon = 1e-12
535 );
536 check_curve_derivatives(|t| circle_at(&c, t), angle);
537 assert_relative_eq!(p.curvature(), 1.0 / 3.0, epsilon = 1e-12);
539 }
540 assert!(circle_parameter(&c, c.centre(), T).is_err());
541 }
542
543 #[test]
544 fn circle_starts_on_its_frames_x_axis() {
545 let f = tilted();
548 let c = Circle::new(f, 2.0, T).unwrap();
549 assert!(
550 circle_at(&c, 0.0)
551 .point
552 .is_equal(c.centre() + f.x() * 2.0, T)
553 );
554 assert!(
555 circle_at(&c, PI / 2.0)
556 .point
557 .is_equal(c.centre() + f.y() * 2.0, T)
558 );
559 }
560
561 #[test]
562 fn ellipse_evaluation_derivatives_and_inversion() {
563 let e = Ellipse::new(tilted(), 5.0, 3.0, T).unwrap();
564 for i in 0..12 {
565 let angle = f64::from(i) * PI / 6.0;
566 let p = ellipse_at(&e, angle);
567 assert_relative_eq!(
568 ellipse_parameter(&e, p.point, T).unwrap(),
569 wrap_angle(angle),
570 epsilon = 1e-12
571 );
572 check_curve_derivatives(|t| ellipse_at(&e, t), angle);
573 }
574 let p = ellipse_at(&e, PI / 4.0);
577 let local = e.frame().to_local(p.point);
578 assert_relative_eq!(
579 local.x,
580 5.0 * core::f64::consts::FRAC_1_SQRT_2,
581 epsilon = 1e-12
582 );
583 assert_relative_eq!(
584 local.y,
585 3.0 * core::f64::consts::FRAC_1_SQRT_2,
586 epsilon = 1e-12
587 );
588 }
589
590 #[test]
591 fn ellipse_curvature_is_extreme_at_the_ends_of_its_axes() {
592 let e = Ellipse::new(Frame::WORLD, 5.0, 3.0, T).unwrap();
593 assert_relative_eq!(ellipse_at(&e, 0.0).curvature(), 5.0 / 9.0, epsilon = 1e-12);
596 assert_relative_eq!(
597 ellipse_at(&e, PI / 2.0).curvature(),
598 3.0 / 25.0,
599 epsilon = 1e-12
600 );
601 }
602
603 #[test]
604 fn hyperbola_evaluation_and_inversion() {
605 let h = Hyperbola::new(tilted(), 3.0, 4.0, T).unwrap();
606 for t in [-2.0_f64, -0.5, 0.0, 0.5, 2.0] {
607 let p = hyperbola_at(&h, t);
608 assert_relative_eq!(
609 hyperbola_parameter(&h, p.point, T).unwrap(),
610 t,
611 epsilon = 1e-11
612 );
613 check_curve_derivatives(|s| hyperbola_at(&h, s), t);
614 }
615 assert!(hyperbola_at(&h, 0.0).point.is_equal(h.vertex(), T));
616 let far = h.centre() - h.frame().x() * 5.0;
618 assert!(hyperbola_parameter(&h, far, T).is_err());
619 }
620
621 #[test]
622 fn hyperbola_inversion_is_accurate_near_the_vertex() {
623 let h = Hyperbola::new(Frame::WORLD, 3.0, 4.0, T).unwrap();
627 for t in [1e-8_f64, 1e-5, 1e-3] {
628 let p = hyperbola_at(&h, t);
629 assert_relative_eq!(
630 hyperbola_parameter(&h, p.point, T).unwrap(),
631 t,
632 max_relative = 1e-9
633 );
634 }
635 }
636
637 #[test]
638 fn parabola_evaluation_and_inversion() {
639 let p = Parabola::new(tilted(), 2.0, T).unwrap();
640 for t in [-4.0_f64, -1.0, 0.0, 1.0, 4.0] {
641 let c = parabola_at(&p, t);
642 assert_relative_eq!(parabola_parameter(&p, c.point), t, epsilon = 1e-11);
643 check_curve_derivatives(|s| parabola_at(&p, s), t);
644 }
645 assert!(parabola_at(&p, 0.0).point.is_equal(p.apex(), T));
646 let focus = p.focus();
648 for t in [-3.0_f64, 1.0, 5.0] {
649 let point = parabola_at(&p, t).point;
650 let local = p.frame().to_local(point);
651 let to_directrix = local.x + p.focal();
652 assert_relative_eq!(point.distance(focus), to_directrix, epsilon = 1e-11);
653 }
654 }
655
656 #[test]
657 fn plane_evaluation_and_inversion() {
658 let plane = Plane::new(tilted());
659 for (u, v) in [(0.0, 0.0), (3.0, -2.0), (-100.0, 50.0)] {
660 let p = plane_at(&plane, u, v);
661 assert!(plane.contains(p.point, T));
662 let (bu, bv) = plane_parameters(&plane, p.point);
663 assert_relative_eq!(bu, u, epsilon = 1e-11);
664 assert_relative_eq!(bv, v, epsilon = 1e-11);
665 assert!(p.normal(T).unwrap().is_equal(plane.normal(), T));
666 }
667 check_surface_derivatives(|u, v| plane_at(&plane, u, v), 1.0, 2.0);
668 }
669
670 #[test]
671 fn cylinder_evaluation_inversion_and_normal() {
672 let c = Cylinder::new(tilted(), 2.0, T).unwrap();
673 for i in 0..8 {
674 let angle = f64::from(i) * PI / 4.0;
675 for h in [-5.0_f64, 0.0, 7.0] {
676 let p = cylinder_at(&c, angle, h);
677 assert!(c.contains(p.point, T));
678 let (ba, bh) = cylinder_parameters(&c, p.point, T).unwrap();
679 assert_relative_eq!(ba, wrap_angle(angle), epsilon = 1e-11);
680 assert_relative_eq!(bh, h, epsilon = 1e-11);
681 assert!(p.normal(T).unwrap().dot(c.frame().z()).abs() < 1e-12);
683 }
684 }
685 check_surface_derivatives(|u, v| cylinder_at(&c, u, v), 0.7, 3.0);
686 assert!(cylinder_parameters(&c, c.frame().origin(), T).is_err());
687 }
688
689 #[test]
690 fn cone_evaluation_inversion_and_apex_degeneracy() {
691 let c = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
692 for i in 0..8 {
693 let angle = f64::from(i) * PI / 4.0;
694 for h in [-1.0_f64, 0.0, 4.0] {
695 let p = cone_at(&c, angle, h);
696 assert!(
697 c.contains(p.point, T),
698 "distance {}",
699 c.distance_to(p.point)
700 );
701 let (ba, bh) = cone_parameters(&c, p.point, T).unwrap();
702 assert_relative_eq!(ba, wrap_angle(angle), epsilon = 1e-11);
703 assert_relative_eq!(bh, h, epsilon = 1e-11);
704 }
705 }
706 check_surface_derivatives(|u, v| cone_at(&c, u, v), 0.7, 2.0);
707
708 let apex_height = -3.0 / 0.6_f64.tan();
711 let at_apex = cone_at(&c, 1.0, apex_height);
712 assert!(at_apex.point.is_equal(c.apex(), T));
713 assert!(at_apex.is_degenerate(T));
714 assert!(at_apex.normal(T).is_err());
715 }
716
717 #[test]
718 fn sphere_evaluation_inversion_and_poles() {
719 let s = Sphere::new(tilted(), 4.0, T).unwrap();
720 for i in 0..8 {
721 let lon = f64::from(i) * PI / 4.0;
722 for lat in [-1.2_f64, -0.4, 0.0, 0.9] {
723 let p = sphere_at(&s, lon, lat);
724 assert!(s.contains(p.point, T));
725 let (blon, blat) = sphere_parameters(&s, p.point, T).unwrap();
726 assert_relative_eq!(blon, wrap_angle(lon), epsilon = 1e-11);
727 assert_relative_eq!(blat, lat, epsilon = 1e-11);
728 assert!(
730 p.normal(T)
731 .unwrap()
732 .is_equal(s.normal_at(p.point, T).unwrap(), T)
733 );
734 }
735 }
736 check_surface_derivatives(|u, v| sphere_at(&s, u, v), 1.1, 0.3);
737
738 let north = sphere_at(&s, 2.0, PI / 2.0);
741 assert!(north.point.is_equal(s.centre() + s.frame().z() * 4.0, T));
742 assert!(north.is_degenerate(T));
743 let (lon, lat) = sphere_parameters(&s, north.point, T).unwrap();
744 assert_relative_eq!(lon, 0.0);
745 assert_relative_eq!(lat, PI / 2.0, epsilon = 1e-8);
746 assert!(sphere_parameters(&s, s.centre(), T).is_err());
747 }
748
749 #[test]
750 fn torus_evaluation_inversion_and_normal() {
751 let t = Torus::new(tilted(), 5.0, 2.0, T).unwrap();
752 for i in 0..6 {
753 let u = f64::from(i) * PI / 3.0;
754 for j in 0..6 {
755 let v = f64::from(j) * PI / 3.0;
756 let p = torus_at(&t, u, v);
757 assert!(
758 t.contains(p.point, T),
759 "distance {}",
760 t.distance_to(p.point)
761 );
762 let (bu, bv) = torus_parameters(&t, p.point, T).unwrap();
763 assert_relative_eq!(bu, wrap_angle(u), epsilon = 1e-10);
764 assert_relative_eq!(bv, wrap_angle(v), epsilon = 1e-10);
765 assert!(p.normal(T).is_ok());
766 }
767 }
768 check_surface_derivatives(|u, v| torus_at(&t, u, v), 0.7, 2.0);
769 assert!(torus_parameters(&t, t.centre(), T).is_err());
770 }
771
772 #[test]
773 fn curvature_of_a_circle_is_the_reciprocal_of_its_radius() {
774 for r in [0.1_f64, 1.0, 100.0] {
775 let c = Circle::new(Frame::WORLD, r, T).unwrap();
776 assert_relative_eq!(
777 circle_at(&c, 1.3).curvature(),
778 1.0 / r,
779 max_relative = 1e-12
780 );
781 }
782 }
783
784 #[test]
785 fn tangent_of_a_circle_is_perpendicular_to_its_radius() {
786 let c = Circle::new(tilted(), 3.0, T).unwrap();
787 for i in 0..8 {
788 let angle = f64::from(i) * PI / 4.0;
789 let p = circle_at(&c, angle);
790 let radius = p.point - c.centre();
791 assert!(p.tangent(T).unwrap().dot_vector(radius).abs() < 1e-12);
792 }
793 }
794}