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 let scale = self.du.magnitude().max(self.dv.magnitude());
96 Direction::new(self.du.cross(self.dv) / (scale * scale), tol)
97 }
98
99 #[must_use]
110 pub fn is_degenerate(&self, tol: Tolerances) -> bool {
111 let scale = self.du.magnitude().max(self.dv.magnitude());
112 self.du.cross(self.dv).magnitude() <= tol.angular() * scale * scale
113 }
114}
115
116#[must_use]
118pub fn wrap_angle(angle: f64) -> f64 {
119 let wrapped = angle.rem_euclid(core::f64::consts::TAU);
120 if wrapped >= core::f64::consts::TAU {
123 0.0
124 } else {
125 wrapped
126 }
127}
128
129#[must_use]
131pub fn wrap_signed_angle(angle: f64) -> f64 {
132 let wrapped = wrap_angle(angle);
133 if wrapped > core::f64::consts::PI {
134 wrapped - core::f64::consts::TAU
135 } else {
136 wrapped
137 }
138}
139
140#[must_use]
142pub fn line_at(axis: Axis, t: f64) -> CurvePoint {
143 CurvePoint {
144 point: axis.point_at(t),
145 d1: axis.direction.vector(),
146 d2: Vector::ZERO,
147 }
148}
149
150#[must_use]
152pub fn line_parameter(axis: Axis, p: Point) -> f64 {
153 axis.parameter_of(p)
154}
155
156#[must_use]
158pub fn circle_at(circle: &Circle, angle: f64) -> CurvePoint {
159 let f = circle.frame();
160 let r = circle.radius();
161 let (sin, cos) = angle.sin_cos();
162 let (x, y) = (f.x().vector(), f.y().vector());
163 CurvePoint {
164 point: circle.centre() + x * (r * cos) + y * (r * sin),
165 d1: x * (-r * sin) + y * (r * cos),
166 d2: x * (-r * cos) + y * (-r * sin),
167 }
168}
169
170pub fn circle_parameter(circle: &Circle, p: Point, tol: Tolerances) -> OgeomResult<f64> {
177 let local = circle.frame().to_local(p);
178 if local.x.hypot(local.y) <= tol.confusion() {
179 ogeom_bail!(
180 Construction,
181 "point is on the circle's axis; no nearest angle"
182 );
183 }
184 Ok(wrap_angle(local.y.atan2(local.x)))
185}
186
187#[must_use]
193pub fn ellipse_at(ellipse: &Ellipse, angle: f64) -> CurvePoint {
194 let f = ellipse.frame();
195 let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
196 let (sin, cos) = angle.sin_cos();
197 let (x, y) = (f.x().vector(), f.y().vector());
198 CurvePoint {
199 point: ellipse.centre() + x * (a * cos) + y * (b * sin),
200 d1: x * (-a * sin) + y * (b * cos),
201 d2: x * (-a * cos) + y * (-b * sin),
202 }
203}
204
205pub fn ellipse_parameter(ellipse: &Ellipse, p: Point, tol: Tolerances) -> OgeomResult<f64> {
212 let local = ellipse.frame().to_local(p);
213 if local.x.hypot(local.y) <= tol.confusion() {
214 ogeom_bail!(Construction, "point projects to the ellipse's centre");
215 }
216 Ok(wrap_angle(
219 (local.y / ellipse.minor_radius()).atan2(local.x / ellipse.major_radius()),
220 ))
221}
222
223#[must_use]
225pub fn hyperbola_at(hyperbola: &Hyperbola, t: f64) -> CurvePoint {
226 let f = hyperbola.frame();
227 let (a, b) = (hyperbola.major_radius(), hyperbola.minor_radius());
228 let (cosh, sinh) = (t.cosh(), t.sinh());
229 let (x, y) = (f.x().vector(), f.y().vector());
230 CurvePoint {
231 point: hyperbola.centre() + x * (a * cosh) + y * (b * sinh),
232 d1: x * (a * sinh) + y * (b * cosh),
233 d2: x * (a * cosh) + y * (b * sinh),
237 }
238}
239
240pub fn hyperbola_parameter(hyperbola: &Hyperbola, p: Point, tol: Tolerances) -> OgeomResult<f64> {
247 let local = hyperbola.frame().to_local(p);
248 let a = local.x / hyperbola.major_radius();
249 if a < 1.0 - tol.parametric() {
250 ogeom_bail!(
251 Domain,
252 "point is not on the described branch of the hyperbola"
253 );
254 }
255 Ok((local.y / hyperbola.minor_radius()).asinh())
258}
259
260#[must_use]
262pub fn parabola_at(parabola: &Parabola, t: f64) -> CurvePoint {
263 let f = parabola.frame();
264 let focal = parabola.focal();
265 let (x, y) = (f.x().vector(), f.y().vector());
266 let scale = 1.0 / (4.0 * focal);
267 CurvePoint {
268 point: parabola.apex() + x * (t * t * scale) + y * t,
269 d1: x * (2.0 * t * scale) + y,
270 d2: x * (2.0 * scale),
271 }
272}
273
274#[must_use]
276pub fn parabola_parameter(parabola: &Parabola, p: Point) -> f64 {
277 parabola.frame().to_local(p).y
278}
279
280#[must_use]
282pub fn plane_at(plane: &Plane, u: f64, v: f64) -> SurfacePoint {
283 let f = plane.frame();
284 SurfacePoint {
285 point: f.origin() + f.x() * u + f.y() * v,
286 du: f.x().vector(),
287 dv: f.y().vector(),
288 }
289}
290
291#[must_use]
293pub fn plane_parameters(plane: &Plane, p: Point) -> (f64, f64) {
294 let local = plane.frame().to_local(p);
295 (local.x, local.y)
296}
297
298#[must_use]
300pub fn cylinder_at(cylinder: &Cylinder, angle: f64, height: f64) -> SurfacePoint {
301 let f = cylinder.frame();
302 let r = cylinder.radius();
303 let (sin, cos) = angle.sin_cos();
304 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
305 SurfacePoint {
306 point: f.origin() + x * (r * cos) + y * (r * sin) + z * height,
307 du: x * (-r * sin) + y * (r * cos),
308 dv: z,
309 }
310}
311
312pub fn cylinder_parameters(
319 cylinder: &Cylinder,
320 p: Point,
321 tol: Tolerances,
322) -> OgeomResult<(f64, f64)> {
323 let local = cylinder.frame().to_local(p);
324 if local.x.hypot(local.y) <= tol.confusion() {
325 ogeom_bail!(
326 Construction,
327 "point is on the cylinder's axis; no nearest angle"
328 );
329 }
330 Ok((wrap_angle(local.y.atan2(local.x)), local.z))
331}
332
333#[must_use]
339pub fn cone_at(cone: &Cone, angle: f64, height: f64) -> SurfacePoint {
340 let f = cone.frame();
341 let r = cone.radius_at(height);
342 let slope = cone.half_angle().tan();
343 let (sin, cos) = angle.sin_cos();
344 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
345 SurfacePoint {
346 point: f.origin() + x * (r * cos) + y * (r * sin) + z * height,
347 du: x * (-r * sin) + y * (r * cos),
348 dv: x * (slope * cos) + y * (slope * sin) + z,
349 }
350}
351
352pub fn cone_parameters(cone: &Cone, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
364 let local = cone.frame().to_local(p);
365 let ring = local.x.hypot(local.y);
366 if ring <= tol.confusion() {
367 ogeom_bail!(
368 Construction,
369 "point is on the cone's axis; no nearest angle"
370 );
371 }
372 let angle = local.y.atan2(local.x);
373 let (start, slope) = (cone.reference_radius(), cone.half_angle().tan());
374 let apex = -start / slope;
375 let squared = slope.mul_add(slope, 1.0);
376 let near = (local.z + slope * (ring - start)) / squared;
380 let far = (local.z - slope * (ring + start)) / squared;
381 let on_own_side = |v: f64, sign: f64| {
382 if sign * slope.mul_add(v, start) >= 0.0 {
383 v
384 } else {
385 apex
386 }
387 };
388 let (near, far) = (on_own_side(near, 1.0), on_own_side(far, -1.0));
389 let gap = |v: f64, sign: f64| (sign * slope.mul_add(v, start) - ring).hypot(v - local.z);
390 Ok(if gap(far, -1.0) < gap(near, 1.0) {
391 (wrap_angle(angle + core::f64::consts::PI), far)
392 } else {
393 (wrap_angle(angle), near)
394 })
395}
396
397#[must_use]
401pub fn sphere_at(sphere: &Sphere, longitude: f64, latitude: f64) -> SurfacePoint {
402 let f = sphere.frame();
403 let r = sphere.radius();
404 let (sin_lon, cos_lon) = longitude.sin_cos();
405 let (sin_lat, cos_lat) = latitude.sin_cos();
406 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
407 let ring = r * cos_lat;
408 SurfacePoint {
409 point: sphere.centre() + x * (ring * cos_lon) + y * (ring * sin_lon) + z * (r * sin_lat),
410 du: x * (-ring * sin_lon) + y * (ring * cos_lon),
411 dv: x * (-r * sin_lat * cos_lon) + y * (-r * sin_lat * sin_lon) + z * (r * cos_lat),
412 }
413}
414
415pub fn sphere_parameters(sphere: &Sphere, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
424 let local = sphere.frame().to_local(p);
425 let ring = local.x.hypot(local.y);
426 if ring.hypot(local.z) <= tol.confusion() {
427 ogeom_bail!(Construction, "point is the sphere's centre");
428 }
429 let longitude = if ring <= tol.confusion() {
430 0.0
431 } else {
432 wrap_angle(local.y.atan2(local.x))
433 };
434 Ok((longitude, local.z.atan2(ring)))
438}
439
440#[must_use]
442pub fn torus_at(torus: &Torus, u: f64, v: f64) -> SurfacePoint {
443 let f = torus.frame();
444 let (major, minor) = (torus.major_radius(), torus.minor_radius());
445 let (sin_u, cos_u) = u.sin_cos();
446 let (sin_v, cos_v) = v.sin_cos();
447 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
448 let out = x * cos_u + y * sin_u;
449 let radius = minor.mul_add(cos_v, major);
450 SurfacePoint {
451 point: f.origin() + out * radius + z * (minor * sin_v),
452 du: (x * -sin_u + y * cos_u) * radius,
453 dv: out * (-minor * sin_v) + z * (minor * cos_v),
454 }
455}
456
457pub fn torus_parameters(torus: &Torus, p: Point, tol: Tolerances) -> OgeomResult<(f64, f64)> {
464 let local = torus.frame().to_local(p);
465 let ring = local.x.hypot(local.y);
466 if ring <= tol.confusion() {
467 ogeom_bail!(Construction, "point is on the torus's axis");
468 }
469 let angle = local.y.atan2(local.x);
470 let (major, minor) = (torus.major_radius(), torus.minor_radius());
475 let radial = ring - major;
476 let beyond = -ring - major;
477 let near_gap = (radial.hypot(local.z) - minor).abs();
478 let far_gap = (beyond.hypot(local.z) - minor).abs();
479 let (u, radial) = if far_gap < near_gap {
480 (angle + core::f64::consts::PI, beyond)
481 } else {
482 (angle, radial)
483 };
484 if radial.hypot(local.z) <= tol.confusion() {
485 ogeom_bail!(Construction, "point is on the tube's centre circle");
486 }
487 Ok((wrap_angle(u), wrap_angle(local.z.atan2(radial))))
488}
489
490#[cfg(test)]
491#[allow(clippy::unwrap_used)]
492mod tests {
493 use super::*;
494 use crate::Frame;
495 use approx::assert_relative_eq;
496
497 const T: Tolerances = Tolerances::millimetres();
498 const PI: f64 = core::f64::consts::PI;
499
500 #[test]
501 fn a_small_healthy_chart_has_a_normal() {
502 let at = SurfacePoint {
503 point: Point::ORIGIN,
504 du: Vector::new(1e-4, 0.0, 0.0),
505 dv: Vector::new(0.0, 1e-4, 0.0),
506 };
507 assert!(!at.is_degenerate(T));
508 assert_relative_eq!(at.normal(T).unwrap().z(), 1.0);
509 }
510
511 fn tilted() -> Frame {
512 Frame::new(
513 Point::new(1.0, -2.0, 3.0),
514 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
515 Direction::from_coords(1.0, 0.0, 0.0, T).unwrap(),
516 T,
517 )
518 .unwrap()
519 }
520
521 fn check_curve_derivatives(f: impl Fn(f64) -> CurvePoint, t: f64) {
523 let h = 1e-6;
524 let (a, b, c) = (f(t - h), f(t), f(t + h));
525 let d1 = (c.point - a.point) * (1.0 / (2.0 * h));
526 let d2 = (c.d1 - a.d1) * (1.0 / (2.0 * h));
527 let scale = d1.magnitude().max(1.0);
528 assert!((b.d1 - d1).magnitude() <= 1e-5 * scale, "d1 wrong at {t}");
529 assert!(
530 (b.d2 - d2).magnitude() <= 1e-5 * d2.magnitude().max(1.0),
531 "d2 wrong at {t}"
532 );
533 }
534
535 fn check_surface_derivatives(f: impl Fn(f64, f64) -> SurfacePoint, u: f64, v: f64) {
537 let h = 1e-6;
538 let du = (f(u + h, v).point - f(u - h, v).point) * (1.0 / (2.0 * h));
539 let dv = (f(u, v + h).point - f(u, v - h).point) * (1.0 / (2.0 * h));
540 let p = f(u, v);
541 assert!(
542 (p.du - du).magnitude() <= 1e-5 * du.magnitude().max(1.0),
543 "du wrong"
544 );
545 assert!(
546 (p.dv - dv).magnitude() <= 1e-5 * dv.magnitude().max(1.0),
547 "dv wrong"
548 );
549 }
550
551 #[test]
552 fn angle_wrapping_stays_inside_its_range() {
553 for a in [-10.0_f64, -PI, -1e-18, 0.0, 1.0, PI, 7.0, 100.0] {
554 let w = wrap_angle(a);
555 assert!(
556 (0.0..core::f64::consts::TAU).contains(&w),
557 "{a} wrapped to {w}"
558 );
559 let s = wrap_signed_angle(a);
560 assert!(
561 s > -PI - 1e-15 && s <= PI + 1e-15,
562 "{a} signed-wrapped to {s}"
563 );
564 }
565 assert_eq!(wrap_angle(-1e-300), 0.0);
568 }
569
570 #[test]
571 fn line_evaluation_and_inversion() {
572 let axis = Axis::new(Point::new(1.0, 2.0, 3.0), Direction::Z);
573 let c = line_at(axis, 5.0);
574 assert!(c.point.is_equal(Point::new(1.0, 2.0, 8.0), T));
575 assert!(c.d1.is_equal(Vector::Z, T));
576 assert_relative_eq!(c.curvature(), 0.0);
577 assert_relative_eq!(line_parameter(axis, c.point), 5.0, epsilon = 1e-12);
578 }
579
580 #[test]
581 fn circle_evaluation_derivatives_and_inversion() {
582 let c = Circle::new(tilted(), 3.0, T).unwrap();
583 for i in 0..12 {
584 let angle = f64::from(i) * PI / 6.0;
585 let p = circle_at(&c, angle);
586 assert!(c.contains(p.point, T));
587 assert_relative_eq!(
588 circle_parameter(&c, p.point, T).unwrap(),
589 wrap_angle(angle),
590 epsilon = 1e-12
591 );
592 check_curve_derivatives(|t| circle_at(&c, t), angle);
593 assert_relative_eq!(p.curvature(), 1.0 / 3.0, epsilon = 1e-12);
595 }
596 assert!(circle_parameter(&c, c.centre(), T).is_err());
597 }
598
599 #[test]
600 fn circle_starts_on_its_frames_x_axis() {
601 let f = tilted();
604 let c = Circle::new(f, 2.0, T).unwrap();
605 assert!(
606 circle_at(&c, 0.0)
607 .point
608 .is_equal(c.centre() + f.x() * 2.0, T)
609 );
610 assert!(
611 circle_at(&c, PI / 2.0)
612 .point
613 .is_equal(c.centre() + f.y() * 2.0, T)
614 );
615 }
616
617 #[test]
618 fn ellipse_evaluation_derivatives_and_inversion() {
619 let e = Ellipse::new(tilted(), 5.0, 3.0, T).unwrap();
620 for i in 0..12 {
621 let angle = f64::from(i) * PI / 6.0;
622 let p = ellipse_at(&e, angle);
623 assert_relative_eq!(
624 ellipse_parameter(&e, p.point, T).unwrap(),
625 wrap_angle(angle),
626 epsilon = 1e-12
627 );
628 check_curve_derivatives(|t| ellipse_at(&e, t), angle);
629 }
630 let p = ellipse_at(&e, PI / 4.0);
633 let local = e.frame().to_local(p.point);
634 assert_relative_eq!(
635 local.x,
636 5.0 * core::f64::consts::FRAC_1_SQRT_2,
637 epsilon = 1e-12
638 );
639 assert_relative_eq!(
640 local.y,
641 3.0 * core::f64::consts::FRAC_1_SQRT_2,
642 epsilon = 1e-12
643 );
644 }
645
646 #[test]
647 fn ellipse_curvature_is_extreme_at_the_ends_of_its_axes() {
648 let e = Ellipse::new(Frame::WORLD, 5.0, 3.0, T).unwrap();
649 assert_relative_eq!(ellipse_at(&e, 0.0).curvature(), 5.0 / 9.0, epsilon = 1e-12);
652 assert_relative_eq!(
653 ellipse_at(&e, PI / 2.0).curvature(),
654 3.0 / 25.0,
655 epsilon = 1e-12
656 );
657 }
658
659 #[test]
660 fn hyperbola_evaluation_and_inversion() {
661 let h = Hyperbola::new(tilted(), 3.0, 4.0, T).unwrap();
662 for t in [-2.0_f64, -0.5, 0.0, 0.5, 2.0] {
663 let p = hyperbola_at(&h, t);
664 assert_relative_eq!(
665 hyperbola_parameter(&h, p.point, T).unwrap(),
666 t,
667 epsilon = 1e-11
668 );
669 check_curve_derivatives(|s| hyperbola_at(&h, s), t);
670 }
671 assert!(hyperbola_at(&h, 0.0).point.is_equal(h.vertex(), T));
672 let far = h.centre() - h.frame().x() * 5.0;
674 assert!(hyperbola_parameter(&h, far, T).is_err());
675 }
676
677 #[test]
678 fn hyperbola_inversion_is_accurate_near_the_vertex() {
679 let h = Hyperbola::new(Frame::WORLD, 3.0, 4.0, T).unwrap();
683 for t in [1e-8_f64, 1e-5, 1e-3] {
684 let p = hyperbola_at(&h, t);
685 assert_relative_eq!(
686 hyperbola_parameter(&h, p.point, T).unwrap(),
687 t,
688 max_relative = 1e-9
689 );
690 }
691 }
692
693 #[test]
694 fn parabola_evaluation_and_inversion() {
695 let p = Parabola::new(tilted(), 2.0, T).unwrap();
696 for t in [-4.0_f64, -1.0, 0.0, 1.0, 4.0] {
697 let c = parabola_at(&p, t);
698 assert_relative_eq!(parabola_parameter(&p, c.point), t, epsilon = 1e-11);
699 check_curve_derivatives(|s| parabola_at(&p, s), t);
700 }
701 assert!(parabola_at(&p, 0.0).point.is_equal(p.apex(), T));
702 let focus = p.focus();
704 for t in [-3.0_f64, 1.0, 5.0] {
705 let point = parabola_at(&p, t).point;
706 let local = p.frame().to_local(point);
707 let to_directrix = local.x + p.focal();
708 assert_relative_eq!(point.distance(focus), to_directrix, epsilon = 1e-11);
709 }
710 }
711
712 #[test]
713 fn plane_evaluation_and_inversion() {
714 let plane = Plane::new(tilted());
715 for (u, v) in [(0.0, 0.0), (3.0, -2.0), (-100.0, 50.0)] {
716 let p = plane_at(&plane, u, v);
717 assert!(plane.contains(p.point, T));
718 let (bu, bv) = plane_parameters(&plane, p.point);
719 assert_relative_eq!(bu, u, epsilon = 1e-11);
720 assert_relative_eq!(bv, v, epsilon = 1e-11);
721 assert!(p.normal(T).unwrap().is_equal(plane.normal(), T));
722 }
723 check_surface_derivatives(|u, v| plane_at(&plane, u, v), 1.0, 2.0);
724 }
725
726 #[test]
727 fn cylinder_evaluation_inversion_and_normal() {
728 let c = Cylinder::new(tilted(), 2.0, T).unwrap();
729 for i in 0..8 {
730 let angle = f64::from(i) * PI / 4.0;
731 for h in [-5.0_f64, 0.0, 7.0] {
732 let p = cylinder_at(&c, angle, h);
733 assert!(c.contains(p.point, T));
734 let (ba, bh) = cylinder_parameters(&c, p.point, T).unwrap();
735 assert_relative_eq!(ba, wrap_angle(angle), epsilon = 1e-11);
736 assert_relative_eq!(bh, h, epsilon = 1e-11);
737 assert!(p.normal(T).unwrap().dot(c.frame().z()).abs() < 1e-12);
739 }
740 }
741 check_surface_derivatives(|u, v| cylinder_at(&c, u, v), 0.7, 3.0);
742 assert!(cylinder_parameters(&c, c.frame().origin(), T).is_err());
743 }
744
745 #[test]
746 fn a_cone_inverts_past_its_apex_and_off_its_surface() {
747 let c = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
748 let apex = -3.0 / 0.6_f64.tan();
749 for i in 0..8 {
751 let angle = f64::from(i) * PI / 4.0 + 0.1;
752 for h in [apex - 1.0, apex - 4.0] {
753 let p = cone_at(&c, angle, h).point;
754 let (u, v) = cone_parameters(&c, p, T).unwrap();
755 assert!(cone_at(&c, u, v).point.distance(p) < 1e-10, "{angle} {h}");
756 }
757 }
758 let off = cone_at(&c, 0.7, 2.0).point + Vector::new(0.3, -0.2, 0.5);
761 let (u, v) = cone_parameters(&c, off, T).unwrap();
762 let foot = cone_at(&c, u, v).point.distance(off);
763 for i in 0..64 {
764 for j in 0..64 {
765 let (a, h) = (f64::from(i) * PI / 32.0, apex + f64::from(j) * 0.2 - 6.0);
766 assert!(cone_at(&c, a, h).point.distance(off) >= foot - 1e-9);
767 }
768 }
769 }
770
771 #[test]
772 fn a_spindle_torus_inverts_on_its_folded_half() {
773 let t = Torus::new(tilted(), 2.0, 3.0, T).unwrap();
774 for i in 0..8 {
775 for j in 0..12 {
776 let (u, v) = (
777 f64::from(i) * PI / 4.0 + 0.1,
778 f64::from(j) * PI / 6.0 + 0.05,
779 );
780 let p = torus_at(&t, u, v).point;
781 let (bu, bv) = torus_parameters(&t, p, T).unwrap();
782 assert!(torus_at(&t, bu, bv).point.distance(p) < 1e-10, "{u} {v}");
783 }
784 }
785 }
786
787 #[test]
788 fn cone_evaluation_inversion_and_apex_degeneracy() {
789 let c = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
790 for i in 0..8 {
791 let angle = f64::from(i) * PI / 4.0;
792 for h in [-1.0_f64, 0.0, 4.0] {
793 let p = cone_at(&c, angle, h);
794 assert!(
795 c.contains(p.point, T),
796 "distance {}",
797 c.distance_to(p.point)
798 );
799 let (ba, bh) = cone_parameters(&c, p.point, T).unwrap();
800 assert_relative_eq!(ba, wrap_angle(angle), epsilon = 1e-11);
801 assert_relative_eq!(bh, h, epsilon = 1e-11);
802 }
803 }
804 check_surface_derivatives(|u, v| cone_at(&c, u, v), 0.7, 2.0);
805
806 let apex_height = -3.0 / 0.6_f64.tan();
809 let at_apex = cone_at(&c, 1.0, apex_height);
810 assert!(at_apex.point.is_equal(c.apex(), T));
811 assert!(at_apex.is_degenerate(T));
812 assert!(at_apex.normal(T).is_err());
813 }
814
815 #[test]
816 fn sphere_evaluation_inversion_and_poles() {
817 let s = Sphere::new(tilted(), 4.0, T).unwrap();
818 for i in 0..8 {
819 let lon = f64::from(i) * PI / 4.0;
820 for lat in [-1.2_f64, -0.4, 0.0, 0.9] {
821 let p = sphere_at(&s, lon, lat);
822 assert!(s.contains(p.point, T));
823 let (blon, blat) = sphere_parameters(&s, p.point, T).unwrap();
824 assert_relative_eq!(blon, wrap_angle(lon), epsilon = 1e-11);
825 assert_relative_eq!(blat, lat, epsilon = 1e-11);
826 assert!(
828 p.normal(T)
829 .unwrap()
830 .is_equal(s.normal_at(p.point, T).unwrap(), T)
831 );
832 }
833 }
834 check_surface_derivatives(|u, v| sphere_at(&s, u, v), 1.1, 0.3);
835
836 let north = sphere_at(&s, 2.0, PI / 2.0);
839 assert!(north.point.is_equal(s.centre() + s.frame().z() * 4.0, T));
840 assert!(north.is_degenerate(T));
841 let (lon, lat) = sphere_parameters(&s, north.point, T).unwrap();
842 assert_relative_eq!(lon, 0.0);
843 assert_relative_eq!(lat, PI / 2.0, epsilon = 1e-8);
844 assert!(sphere_parameters(&s, s.centre(), T).is_err());
845 }
846
847 #[test]
848 fn torus_evaluation_inversion_and_normal() {
849 let t = Torus::new(tilted(), 5.0, 2.0, T).unwrap();
850 for i in 0..6 {
851 let u = f64::from(i) * PI / 3.0;
852 for j in 0..6 {
853 let v = f64::from(j) * PI / 3.0;
854 let p = torus_at(&t, u, v);
855 assert!(
856 t.contains(p.point, T),
857 "distance {}",
858 t.distance_to(p.point)
859 );
860 let (bu, bv) = torus_parameters(&t, p.point, T).unwrap();
861 assert_relative_eq!(bu, wrap_angle(u), epsilon = 1e-10);
862 assert_relative_eq!(bv, wrap_angle(v), epsilon = 1e-10);
863 assert!(p.normal(T).is_ok());
864 }
865 }
866 check_surface_derivatives(|u, v| torus_at(&t, u, v), 0.7, 2.0);
867 assert!(torus_parameters(&t, t.centre(), T).is_err());
868 }
869
870 #[test]
871 fn curvature_of_a_circle_is_the_reciprocal_of_its_radius() {
872 for r in [0.1_f64, 1.0, 100.0] {
873 let c = Circle::new(Frame::WORLD, r, T).unwrap();
874 assert_relative_eq!(
875 circle_at(&c, 1.3).curvature(),
876 1.0 / r,
877 max_relative = 1e-12
878 );
879 }
880 }
881
882 #[test]
883 fn tangent_of_a_circle_is_perpendicular_to_its_radius() {
884 let c = Circle::new(tilted(), 3.0, T).unwrap();
885 for i in 0..8 {
886 let angle = f64::from(i) * PI / 4.0;
887 let p = circle_at(&c, angle);
888 let radius = p.point - c.centre();
889 assert!(p.tangent(T).unwrap().dot_vector(radius).abs() < 1e-12);
890 }
891 }
892}