1use geop_core_math::{
17 geop_error::{GeopError, GeopResult},
18 primitives::Motion,
19 scalars::Scalar,
20 vector::Vector3,
21};
22
23use crate::{
24 nurb_curve::{NurbCurve, NurbCurve3D, dehomogenize},
25 nurb_surface::NurbSurface3D,
26};
27
28fn could_be_zero(v: &Vector3) -> bool {
30 v.norm_sq().could_be_equal(Scalar::ZERO)
31}
32
33#[derive(Clone, Debug, PartialEq)]
35pub struct Axis {
36 pub point: Vector3,
37 pub direction: Vector3,
38}
39
40impl Axis {
41 pub fn try_new(point: Vector3, direction: Vector3) -> GeopResult<Self> {
44 Ok(Self {
45 point,
46 direction: direction.normalize()?,
47 })
48 }
49
50 pub fn project(&self, p: &Vector3) -> Vector3 {
53 let along = p.sub(&self.point).prod_dot(&self.direction);
54 self.point.add(&self.direction.prod_scalar(along))
55 }
56
57 pub fn could_contain(&self, p: &Vector3) -> bool {
59 could_be_zero(&p.sub(&self.point).prod_cross(&self.direction))
60 }
61
62 pub fn could_be_parallel(&self, other: &Axis) -> bool {
64 self.direction.could_be_parallel(&other.direction)
65 }
66
67 pub fn nearest(&self, other: &Axis) -> GeopResult<Vector3> {
71 let n = self.direction.prod_cross(&other.direction);
72 let n2 = n.norm_sq();
73 if n2.could_be_equal(Scalar::ZERO) {
74 return Err(GeopError::new(
75 "the lines are parallel, so no point of them is nearer the other than any other",
76 ));
77 }
78 let r = other.point.sub(&self.point);
79 let s = r.prod_cross(&other.direction).prod_dot(&n).div(n2)?;
80 let t = r.prod_cross(&self.direction).prod_dot(&n).div(n2)?;
81 let a = self.point.add(&self.direction.prod_scalar(s));
82 let b = other.point.add(&other.direction.prod_scalar(t));
83 Ok(Vector3::interpolate(&a, &b, Scalar::ONE.div(Scalar::TWO)?))
84 }
85
86 pub fn transform(&self, motion: &Motion) -> Self {
88 Self {
89 point: motion.apply(&self.point),
90 direction: motion.rotate(&self.direction),
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
98pub struct Circle {
99 pub center: Vector3,
100 pub normal: Vector3,
101 pub radius: Scalar,
102}
103
104impl Circle {
105 pub fn axis(&self) -> Axis {
108 Axis {
109 point: self.center,
110 direction: self.normal,
111 }
112 }
113
114 pub fn transform(&self, motion: &Motion) -> Self {
116 Self {
117 center: motion.apply(&self.center),
118 normal: motion.rotate(&self.normal),
119 radius: self.radius,
120 }
121 }
122
123 pub fn could_contain(&self, p: &Vector3) -> bool {
126 let radial = p.sub(&self.center);
127 radial.prod_dot(&self.normal).could_be_equal(Scalar::ZERO)
128 && radial.norm().could_be_equal(self.radius)
129 }
130
131 fn could_be_equal(&self, other: &Circle) -> bool {
132 self.center.could_be_equal(&other.center)
133 && self.normal.could_be_equal(&other.normal)
134 && self.radius.could_be_equal(other.radius)
135 }
136
137 fn union(&self, other: &Circle) -> Circle {
140 Circle {
141 center: self.center.union(&other.center),
142 normal: self.normal.union(&other.normal),
143 radius: self.radius.union(other.radius),
144 }
145 }
146}
147
148#[derive(Clone, Debug)]
152pub struct Arc {
153 pub circle: Circle,
154 pub start: Vector3,
155 pub end: Vector3,
156}
157
158impl Arc {
159 pub fn transform(&self, motion: &Motion) -> Self {
162 let (start, end) = (motion.apply(&self.start), motion.apply(&self.end));
163 let (start, end) = match motion.mirrors() {
164 true => (end, start),
165 false => (start, end),
166 };
167 Self {
168 circle: self.circle.transform(motion),
169 start,
170 end,
171 }
172 }
173
174 pub fn could_be_closed(&self) -> bool {
176 self.start.could_be_equal(&self.end)
177 }
178
179 pub fn angle(&self) -> GeopResult<Scalar> {
184 if self.could_be_closed() {
185 return Ok(Scalar::TWO.mul(Scalar::PI));
186 }
187 let c = &self.circle;
188 let x = self.start.sub(&c.center);
189 let e = self.end.sub(&c.center);
190 let angle = c.normal.prod_dot(&x.prod_cross(&e)).atan2(x.prod_dot(&e));
191 if angle.definitely_greater(Scalar::ZERO) {
192 Ok(angle)
193 } else if angle.definitely_less(Scalar::ZERO) {
194 Ok(angle.add(Scalar::TWO.mul(Scalar::PI)))
195 } else {
196 Err(GeopError::new(format!(
197 "Arc::angle: the arc could turn by nothing or all round, from {:?} to {:?} about {:?}",
198 self.start, self.end, c.center
199 )))
200 }
201 }
202
203 pub fn length(&self) -> GeopResult<Scalar> {
205 Ok(self.circle.radius.mul(self.angle()?))
206 }
207
208 pub fn sweep(&self) -> f64 {
212 if self.could_be_closed() {
213 return std::f64::consts::TAU;
214 }
215 let c = &self.circle;
216 let x = self.start.sub(&c.center);
217 let e = self.end.sub(&c.center);
218 let cos = x.prod_dot(&e).to_f64();
219 let sin = c.normal.prod_dot(&x.prod_cross(&e)).to_f64();
220 let angle = sin.atan2(cos);
221 if angle > 0.0 {
222 angle
223 } else {
224 angle + std::f64::consts::TAU
225 }
226 }
227
228 pub fn point_at(&self, fraction: f64) -> GeopResult<Vector3> {
237 let c = &self.circle;
238 let x = self.start.sub(&c.center);
239 let y = c.normal.prod_cross(&x);
240 let angle = Scalar::from_f64(fraction * self.sweep());
241 Ok(c.center
242 .add(&x.prod_scalar(angle.cos()))
243 .add(&y.prod_scalar(angle.sin())))
244 }
245
246 pub fn tangent_at(&self, p: &Vector3) -> GeopResult<Vector3> {
249 self.circle
250 .normal
251 .prod_cross(&p.sub(&self.circle.center))
252 .normalize()
253 }
254
255 pub fn to_curve(&self) -> GeopResult<NurbCurve3D> {
266 let quarter = std::f64::consts::FRAC_PI_2;
267 let pieces = ((self.sweep() / quarter).ceil() as usize).max(1);
268 let mut points = vec![self.start];
269 for k in 1..pieces {
270 points.push(self.point_at(k as f64 / pieces as f64)?);
271 }
272 points.push(self.end);
273 let origin = points[0].sharpen();
280 let c = self.circle.center.sub(&origin);
281 let r2 = self.circle.radius.mul(self.circle.radius);
282 let mut control_points = vec![points[0].homogeneous(Scalar::ONE)];
283 let mut knots = vec![Scalar::ZERO; 3];
284 for k in 0..pieces {
285 let (a, b) = (
286 points[k].sub(&origin).sub(&c),
287 points[k + 1].sub(&origin).sub(&c),
288 );
289 let one_plus_cos = Scalar::ONE.add(a.prod_dot(&b).div(r2)?);
290 let middle = origin.add(&c.add(&a.add(&b).prod_scalar(Scalar::ONE.div(one_plus_cos)?)));
291 let weight = one_plus_cos.div(Scalar::TWO)?.sqrt()?;
292 control_points.push(middle.homogeneous(weight));
293 control_points.push(points[k + 1].homogeneous(Scalar::ONE));
294 if k + 1 < pieces {
295 let knot = Scalar::from_ratio(k as i64 + 1, pieces as i64)?;
296 knots.extend([knot, knot]);
297 }
298 }
299 knots.extend([Scalar::ONE; 3]);
300 NurbCurve::try_new(2, control_points, knots)
301 }
302}
303
304#[derive(Clone, Debug)]
306pub struct Plane {
307 pub point: Vector3,
308 pub normal: Vector3,
309}
310
311impl Plane {
312 pub fn try_new(point: Vector3, normal: Vector3) -> GeopResult<Self> {
315 Ok(Self {
316 point,
317 normal: normal.normalize()?,
318 })
319 }
320
321 pub fn signed_distance(&self, p: &Vector3) -> Scalar {
324 p.sub(&self.point).prod_dot(&self.normal)
325 }
326
327 pub fn project(&self, p: &Vector3) -> Vector3 {
330 p.sub(&self.normal.prod_scalar(self.signed_distance(p)))
331 }
332
333 pub fn intersect_axis(&self, axis: &Axis) -> GeopResult<Vector3> {
335 let along = axis.direction.prod_dot(&self.normal);
336 if along.could_be_equal(Scalar::ZERO) {
337 return Err(GeopError::new(
338 "the line runs parallel to the plane, so it does not pierce it",
339 ));
340 }
341 let t = self.signed_distance(&axis.point).div(along)?;
342 Ok(axis.point.sub(&axis.direction.prod_scalar(t)))
343 }
344
345 pub fn intersect_plane(&self, other: &Plane) -> GeopResult<Axis> {
348 let direction = self.normal.prod_cross(&other.normal);
349 let n2 = direction.norm_sq();
350 if n2.could_be_equal(Scalar::ZERO) {
351 return Err(GeopError::new(
352 "the planes are parallel, so they do not meet",
353 ));
354 }
355 let (d1, d2) = (
358 self.point.prod_dot(&self.normal),
359 other.point.prod_dot(&other.normal),
360 );
361 let point = other
362 .normal
363 .prod_cross(&direction)
364 .prod_scalar(d1)
365 .add(&direction.prod_cross(&self.normal).prod_scalar(d2))
366 .prod_scalar(Scalar::ONE.div(n2)?);
367 Axis::try_new(point, direction)
368 }
369}
370
371impl NurbCurve3D {
374 pub fn as_line(&self) -> GeopResult<Option<Axis>> {
378 let points = dehomogenize::<4, 3>(&self.control_points);
379 let (first, last) = (points[0], points[points.len() - 1]);
380 let d = last.sub(&first);
381 if could_be_zero(&d) {
382 return Ok(None);
383 }
384 let axis = Axis::try_new(first, d)?;
385 Ok(points.iter().all(|p| axis.could_contain(p)).then_some(axis))
386 }
387
388 pub fn as_arc(&self) -> GeopResult<Option<Arc>> {
393 if self.degree != 2 {
394 return Ok(None);
395 }
396 let mut circle: Option<Circle> = None;
397 for piece in self.bezier_pieces()? {
398 let Some(c) = bezier_circle(&piece)? else {
399 return Ok(None);
400 };
401 circle = Some(match circle {
402 None => c,
403 Some(prev) if prev.could_be_equal(&c) => prev.union(&c),
404 Some(_) => return Ok(None),
405 });
406 }
407 let Some(circle) = circle else {
408 return Ok(None);
409 };
410 let (t0, t1) = self.domain();
411 Ok(Some(Arc {
412 circle,
413 start: self.evaluate(t0)?,
414 end: self.evaluate(t1)?,
415 }))
416 }
417}
418
419fn bezier_circle(piece: &NurbCurve3D) -> GeopResult<Option<Circle>> {
429 let [h0, h1, h2] = match piece.control_points.as_slice() {
430 [a, b, c] => [*a, *b, *c],
431 _ => return Ok(None),
432 };
433 let points = dehomogenize::<4, 3>(&[h0, h1, h2]);
434 let origin = points[0].sharpen();
439 let (p0, p1, p2) = (
440 points[0].sub(&origin),
441 points[1].sub(&origin),
442 points[2].sub(&origin),
443 );
444 let (w0, w1, w2) = (h0[3], h1[3], h2[3]);
445 let tangent = p1.sub(&p0).norm_sq();
446 if !tangent.could_be_equal(p1.sub(&p2).norm_sq()) {
447 return Ok(None);
448 }
449 let chord = p2.sub(&p0).norm_sq();
450 let four = Scalar::TWO.add(Scalar::TWO);
451 if !four
452 .mul(w1)
453 .mul(w1)
454 .mul(tangent)
455 .could_be_equal(w0.mul(w2).mul(chord))
456 {
457 return Ok(None);
458 }
459 let m = p0.add(&p2).prod_scalar(Scalar::ONE.div(Scalar::TWO)?);
463 let h = m.sub(&p1).norm_sq();
464 if h.could_be_equal(Scalar::ZERO) {
465 return Ok(None);
466 }
467 let center = p1.add(&m.sub(&p1).prod_scalar(tangent.div(h)?));
468 let radial = p0.sub(¢er);
469 let normal = radial.prod_cross(&p1.sub(&p0)).normalize()?;
472 Ok(Some(Circle {
473 center: origin.add(¢er),
474 normal,
475 radius: radial.norm(),
476 }))
477}
478
479impl NurbSurface3D {
482 pub fn as_plane(&self) -> GeopResult<Option<Plane>> {
486 let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
487 let (u, v) = (u0.add(u1).div(Scalar::TWO)?, v0.add(v1).div(Scalar::TWO)?);
488 let plane = Plane {
489 point: self.evaluate(u, v)?,
490 normal: self.normal(u, v)?,
491 };
492 let points = dehomogenize::<4, 3>(&self.control_points);
493 Ok(points
494 .iter()
495 .all(|p| plane.signed_distance(p).could_be_equal(Scalar::ZERO))
496 .then_some(plane))
497 }
498
499 pub fn axis_of_revolution(&self) -> GeopResult<Option<Axis>> {
512 for along_u in [true, false] {
513 if let Some(axis) = self.revolution_along(along_u)? {
514 return Ok(Some(axis));
515 }
516 }
517 Ok(None)
518 }
519
520 pub fn as_round_wall(&self) -> GeopResult<Option<RoundWall>> {
529 for along_u in [true, false] {
530 let Some(revolution) = self.revolution_rows(along_u)? else {
531 continue;
532 };
533 if !revolution.poles.is_empty() {
534 continue;
535 }
536 let axis = revolution.axis;
537 let along = |c: &Vector3| c.sub(&axis.point).prod_dot(&axis.direction);
538 let rows: Vec<(Scalar, Scalar)> = (revolution.rows.iter())
539 .map(|(c, r)| (along(c), *r))
540 .collect();
541 let (&(z0, r0), &(z1, r1)) = match (rows.first(), rows.last()) {
542 (Some(first), Some(last)) => (first, last),
543 _ => continue,
544 };
545 let straight = rows.iter().all(|(_, r)| r.could_be_equal(r0));
546 let taper = if straight {
547 Scalar::ZERO
548 } else if z1.could_be_equal(z0) {
549 continue;
550 } else {
551 r1.sub(r0).div(z1.sub(z0))?
552 };
553 let radius = r0.sub(taper.mul(z0));
554 let on_line = |&(z, r): &(Scalar, Scalar)| r.could_be_equal(radius.add(taper.mul(z)));
555 if !rows.iter().all(on_line) {
556 continue;
557 }
558 let radius = if straight {
559 rows.iter().fold(r0, |a, &(_, r)| a.union(r))
561 } else {
562 radius
563 };
564 return Ok(Some(RoundWall {
565 axis,
566 radius,
567 taper,
568 }));
569 }
570 Ok(None)
571 }
572
573 pub(crate) fn revolution_along(&self, along_u: bool) -> GeopResult<Option<Axis>> {
575 Ok(self.revolution_rows(along_u)?.map(|r| r.axis))
576 }
577
578 fn revolution_rows(&self, along_u: bool) -> GeopResult<Option<Revolution>> {
581 let (rows, len, degree, knots) = if along_u {
582 (self.num_v, self.num_u, self.degree_u, &self.knot_vector_u)
583 } else {
584 (self.num_u, self.num_v, self.degree_v, &self.knot_vector_v)
585 };
586 let row = |j: usize| -> Vec<_> {
587 (0..len)
588 .map(|i| {
589 let index = if along_u {
590 i * self.num_v + j
591 } else {
592 j * self.num_v + i
593 };
594 self.control_points[index]
595 })
596 .collect()
597 };
598 let mut reference: Option<(Arc, Vec<Scalar>)> = None;
601 let mut poles = Vec::new();
602 let mut arcs = Vec::new();
603 for j in 0..rows {
604 let cps = row(j);
605 let weights: Vec<Scalar> = cps.iter().map(|p| p[3]).collect();
606 if let Some((_, reference_weights)) = &reference {
607 let proportional = (0..len).all(|i| {
608 weights[i]
609 .mul(reference_weights[0])
610 .could_be_equal(reference_weights[i].mul(weights[0]))
611 });
612 if !proportional {
613 return Ok(None);
614 }
615 }
616 let points = dehomogenize::<4, 3>(&cps);
617 if points.iter().all(|p| p.could_be_equal(&points[0])) {
618 poles.push(points[0]);
619 continue;
620 }
621 let Some(arc) = NurbCurve::try_new(degree, cps, knots.clone())?.as_arc()? else {
622 return Ok(None);
623 };
624 arcs.push((arc.circle.center, arc.circle.radius));
625 match &reference {
626 None => reference = Some((arc, weights)),
627 Some((first, _)) => {
628 let axis = first.circle.axis();
629 let same_angle = arc
630 .start
631 .sub(&arc.circle.center)
632 .normalize()?
633 .could_be_equal(&first.start.sub(&first.circle.center).normalize()?);
634 if !arc.circle.normal.could_be_equal(&axis.direction)
635 || !axis.could_contain(&arc.circle.center)
636 || !same_angle
637 {
638 return Ok(None);
639 }
640 }
641 }
642 }
643 let Some((first, _)) = reference else {
644 return Ok(None);
645 };
646 let axis = first.circle.axis();
647 Ok(poles
648 .iter()
649 .all(|p| axis.could_contain(p))
650 .then_some(Revolution {
651 axis,
652 rows: arcs,
653 poles,
654 }))
655 }
656}
657
658struct Revolution {
662 axis: Axis,
663 rows: Vec<(Vector3, Scalar)>,
664 poles: Vec<Vector3>,
665}
666
667#[derive(Clone, Debug)]
671pub struct RoundWall {
672 pub axis: Axis,
673 pub radius: Scalar,
674 pub taper: Scalar,
676}
677
678impl RoundWall {
679 pub fn along(&self, p: &Vector3) -> Scalar {
681 p.sub(&self.axis.point).prod_dot(&self.axis.direction)
682 }
683
684 pub fn radius_at(&self, p: &Vector3) -> Scalar {
686 self.radius.add(self.taper.mul(self.along(p)))
687 }
688
689 pub fn could_be_equal(&self, other: &RoundWall) -> bool {
693 let same_way = self.axis.direction.prod_dot(&other.axis.direction);
694 self.axis.could_be_parallel(&other.axis)
695 && self.axis.could_contain(&other.axis.point)
696 && self
697 .radius_at(&other.axis.point)
698 .could_be_equal(other.radius)
699 && self.taper.mul(same_way).could_be_equal(other.taper)
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use crate::test_fixtures::knots;
706 use geop_core_math::{
707 scalars::Scalar,
708 vector::{Vector3, Vector4},
709 };
710
711 use super::*;
712 use crate::nurb_surface::NurbSurface;
713
714 fn v(x: f64, y: f64, z: f64) -> Vector3 {
715 Vector3::from_array([x, y, z].map(Scalar::from_f64))
716 }
717
718 fn h(x: f64, y: f64, z: f64, w: f64) -> Vector4 {
720 Vector4::from_array([x * w, y * w, z * w, w].map(Scalar::from_f64))
721 }
722
723 const R2: f64 = std::f64::consts::FRAC_1_SQRT_2;
724
725 fn unit_circle() -> NurbCurve3D {
728 let corners = [
729 (1., 0.),
730 (1., 1.),
731 (0., 1.),
732 (-1., 1.),
733 (-1., 0.),
734 (-1., -1.),
735 (0., -1.),
736 (1., -1.),
737 (1., 0.),
738 ];
739 let cps = corners
740 .iter()
741 .enumerate()
742 .map(|(i, &(x, y))| h(x, y, 0., if i % 2 == 1 { R2 } else { 1. }))
743 .collect();
744 NurbCurve::try_new(
745 2,
746 cps,
747 knots(&[0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.]),
748 )
749 .unwrap()
750 }
751
752 #[test]
753 fn straight_curves_are_lines() {
754 let line = NurbCurve::<4>::try_new(
755 2,
756 vec![h(0., 0., 0., 1.), h(1., 2., 2., 0.5), h(3., 6., 6., 1.)],
757 knots(&[0., 0., 0., 1., 1., 1.]),
758 )
759 .unwrap();
760 let axis = line.as_line().unwrap().expect("collinear control points");
761 assert!(axis.direction.could_be_equal(&v(1. / 3., 2. / 3., 2. / 3.)));
762 assert!(unit_circle().as_line().unwrap().is_none());
763 }
764
765 #[test]
766 fn a_circle_is_recognized_with_its_turning_sense() {
767 let arc = unit_circle().as_arc().unwrap().expect("a circle");
768 assert!(arc.circle.center.could_be_equal(&v(0., 0., 0.)));
769 assert!(arc.circle.normal.could_be_equal(&v(0., 0., 1.)));
770 assert!(arc.circle.radius.could_be_equal(Scalar::ONE));
771 assert!(arc.could_be_closed());
772 assert!(arc.point_at(0.5).unwrap().could_be_equal(&v(-1., 0., 0.)));
773 let ninety = arc.point_at(0.25).unwrap();
774 assert!(ninety.could_be_equal(&v(0., 1., 0.)));
775 assert!(
776 arc.tangent_at(&ninety)
777 .unwrap()
778 .could_be_equal(&v(-1., 0., 0.))
779 );
780 }
781
782 #[test]
785 fn a_split_arc_is_still_an_arc() {
786 let (left, _) = unit_circle().split(Scalar::from_f64(1.3)).unwrap();
787 let arc = left.as_arc().unwrap().expect("still circular");
788 assert!(arc.circle.radius.could_be_equal(Scalar::ONE));
789 assert!(!arc.could_be_closed());
790 assert!(arc.sweep() > std::f64::consts::FRAC_PI_2);
791 assert!(arc.sweep() < std::f64::consts::PI);
792 let angle = arc.angle().unwrap();
794 assert!(
795 angle.could_be_equal(Scalar::from_f64(arc.sweep())),
796 "{angle:?}"
797 );
798 let length = arc.length().unwrap();
799 assert!(length.could_be_equal(angle), "{length:?}");
800 assert!(length.width().to_f64() < 1e-5, "{length:?}");
801 let reversed = left.reverse().as_arc().unwrap().expect("still circular");
803 assert!(reversed.circle.normal.could_be_equal(&v(0., 0., -1.)));
804 }
805
806 #[test]
808 fn other_conics_are_not_arcs() {
809 let conic = NurbCurve::<4>::try_new(
810 2,
811 vec![h(1., 0., 0., 1.), h(1., 1., 0., 0.5), h(0., 1., 0., 1.)],
812 knots(&[0., 0., 0., 1., 1., 1.]),
813 )
814 .unwrap();
815 assert!(conic.as_arc().unwrap().is_none());
816 let lopsided = NurbCurve::<4>::try_new(
817 2,
818 vec![h(1., 0., 0., 1.), h(1., 2., 0., R2), h(0., 1., 0., 1.)],
819 knots(&[0., 0., 0., 1., 1., 1.]),
820 )
821 .unwrap();
822 assert!(lopsided.as_arc().unwrap().is_none());
823 }
824
825 #[test]
826 fn arcs_become_curves() {
827 let circle = Circle {
829 center: v(1., 1., 2.),
830 normal: v(0., 0., -1.),
831 radius: Scalar::from_f64(2.),
832 };
833 let at = |angle: f64| v(1. + 2. * angle.cos(), 1. - 2. * angle.sin(), 2.);
834 for (from, to) in [(0.3, 1.2), (0.3, 3.0), (-2.0, 3.5), (0.5, 0.5)] {
835 let arc = Arc {
836 circle: circle.clone(),
837 start: at(from),
838 end: at(to),
839 };
840 let curve = arc.to_curve().unwrap();
841 let traced = curve.as_arc().unwrap().expect("an arc");
842 assert!(traced.circle.could_be_equal(&circle), "{traced:?}");
843 assert!(traced.start.could_be_equal(&arc.start));
844 assert!(traced.end.could_be_equal(&arc.end));
845 let sweep = if from == to {
848 std::f64::consts::TAU
849 } else {
850 to - from
851 };
852 let mid = curve.evaluate(Scalar::from_f64(0.5)).unwrap();
853 let expected = at(from + sweep / 2.);
854 let off = (0..3)
855 .map(|k| mid[k].sub(expected[k]).abs().to_f64())
856 .fold(0., f64::max);
857 assert!(off < 1e-7, "{mid:?} vs {expected:?}");
858 }
859 }
860
861 fn quarter_cylinder() -> NurbSurface3D {
864 let ring = [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)];
865 let cps = ring
866 .iter()
867 .flat_map(|&(x, y, w)| [h(x, y, 0., w), h(x, y, 3., w)])
868 .collect();
869 NurbSurface::try_new(
870 2,
871 1,
872 cps,
873 knots(&[0., 0., 0., 1., 1., 1.]),
874 knots(&[0., 0., 1., 1.]),
875 )
876 .unwrap()
877 }
878
879 #[test]
880 fn a_cylinder_turns_around_its_axis() {
881 let cylinder = quarter_cylinder();
882 let axis = cylinder.axis_of_revolution().unwrap().expect("a cylinder");
883 assert!(axis.could_contain(&v(0., 0., 7.)));
884 assert!(axis.direction.could_be_parallel(&v(0., 0., 1.)));
885 assert!(cylinder.as_plane().unwrap().is_none());
886
887 let mut twisted = cylinder.clone();
889 twisted.control_points[1] = h(2., 0.1, 3., 1.);
890 assert!(twisted.axis_of_revolution().unwrap().is_none());
891
892 let found = cylinder.as_round_wall().unwrap().expect("a cylinder");
893 assert!(found.radius.could_be_equal(Scalar::from_f64(2.0)));
894 assert!(found.taper.could_be_equal(Scalar::ZERO));
895 assert!(found.axis.could_contain(&v(0., 0., -1.)));
896 let mut cone = cylinder.clone();
899 for (k, &(x, y, w)) in [(1., 0., 1.), (1., 1., R2), (0., 1., 1.)]
900 .iter()
901 .enumerate()
902 {
903 cone.control_points[2 * k + 1] = h(x, y, 3., w);
904 }
905 assert!(cone.axis_of_revolution().unwrap().is_some());
906 let found = cone.as_round_wall().unwrap().expect("a cone");
907 let up = found.axis.direction[2].to_f64().signum();
908 assert!(
909 found
910 .taper
911 .mul(Scalar::from_f64(up))
912 .could_be_equal(Scalar::from_f64(-1. / 3.))
913 );
914 assert!(
915 found
916 .radius_at(&v(0., 0., 1.5))
917 .could_be_equal(Scalar::from_f64(1.5))
918 );
919 let bulged = NurbSurface::try_new(
921 2,
922 2,
923 [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)]
924 .iter()
925 .flat_map(|&(x, y, w)| {
926 [h(x, y, 0., w), h(1.5 * x, 1.5 * y, 1.5, w), h(x, y, 3., w)]
927 })
928 .collect(),
929 knots(&[0., 0., 0., 1., 1., 1.]),
930 knots(&[0., 0., 0., 1., 1., 1.]),
931 )
932 .unwrap();
933 assert!(bulged.axis_of_revolution().unwrap().is_some());
934 assert!(bulged.as_round_wall().unwrap().is_none());
935 }
936
937 #[test]
938 fn flat_surfaces_are_planes() {
939 let flat = NurbSurface::<4>::try_new(
940 1,
941 1,
942 vec![
943 h(0., 0., 1., 1.),
944 h(0., 1., 1., 1.),
945 h(1., 0., 1., 1.),
946 h(1., 1., 1., 1.),
947 ],
948 knots(&[0., 0., 1., 1.]),
949 knots(&[0., 0., 1., 1.]),
950 )
951 .unwrap();
952 let plane = flat.as_plane().unwrap().expect("flat");
953 assert!(plane.normal.could_be_equal(&v(0., 0., 1.)));
954 assert!(
955 plane
956 .signed_distance(&v(5., 5., 1.))
957 .could_be_equal(Scalar::ZERO)
958 );
959 let mut bent = flat.clone();
960 bent.control_points[3] = h(1., 1., 1.2, 1.);
961 assert!(bent.as_plane().unwrap().is_none());
962 }
963
964 #[test]
965 fn planes_and_axes_meet_where_they_should() {
966 let z = Plane::try_new(v(0., 0., 2.), v(0., 0., 3.)).unwrap();
967 let x = Plane::try_new(v(1., 0., 0.), v(1., 0., 0.)).unwrap();
968 assert!(z.project(&v(4., 5., 6.)).could_be_equal(&v(4., 5., 2.)));
969 let line = z.intersect_plane(&x).unwrap();
970 assert!(line.could_contain(&v(1., 7., 2.)));
971 assert!(line.direction.could_be_parallel(&v(0., 1., 0.)));
972 let slanted = Axis::try_new(v(0., 0., 0.), v(1., 1., 1.)).unwrap();
973 assert!(
974 z.intersect_axis(&slanted)
975 .unwrap()
976 .could_be_equal(&v(2., 2., 2.))
977 );
978 assert!(
979 slanted
980 .project(&v(3., 0., 0.))
981 .could_be_equal(&v(1., 1., 1.))
982 );
983 let crossing = Axis::try_new(v(2., 0., 2.), v(0., 1., 0.)).unwrap();
984 assert!(
985 slanted
986 .nearest(&crossing)
987 .unwrap()
988 .could_be_equal(&v(2., 2., 2.))
989 );
990 let skew = Axis::try_new(v(2., 0., 3.), v(0., 1., 0.)).unwrap();
993 assert!(
994 slanted
995 .nearest(&skew)
996 .unwrap()
997 .could_be_equal(&v(2.25, 2.5, 2.75))
998 );
999 assert!(slanted.nearest(&slanted).is_err());
1000 assert!(z.intersect_plane(&z).is_err());
1001 }
1002}