1use geop_core_math::{
17 geop_error::{GeopError, GeopResult},
18 scalars::Scalar,
19 vector::Vector3,
20};
21
22use crate::{
23 nurb_curve::{NurbCurve, NurbCurve3D, dehomogenize},
24 nurb_surface::NurbSurface3D,
25};
26
27fn could_be_zero<S: Scalar>(v: &Vector3<S>) -> bool {
29 v.norm_sq().could_be_equal(S::ZERO)
30}
31
32fn could_be_parallel<S: Scalar>(a: &Vector3<S>, b: &Vector3<S>) -> bool {
34 could_be_zero(&a.prod_cross(b))
35}
36
37#[derive(Clone, Debug)]
39pub struct Axis<S: Scalar> {
40 pub point: Vector3<S>,
41 pub direction: Vector3<S>,
42}
43
44impl<S: Scalar> Axis<S> {
45 pub fn try_new(point: Vector3<S>, direction: Vector3<S>) -> GeopResult<Self> {
48 Ok(Self {
49 point,
50 direction: direction.normalize()?,
51 })
52 }
53
54 pub fn project(&self, p: &Vector3<S>) -> Vector3<S> {
57 let along = p.sub(&self.point).prod_dot(&self.direction);
58 self.point.add(&self.direction.prod_scalar(along))
59 }
60
61 pub fn could_contain(&self, p: &Vector3<S>) -> bool {
63 could_be_zero(&p.sub(&self.point).prod_cross(&self.direction))
64 }
65
66 pub fn could_be_parallel(&self, other: &Axis<S>) -> bool {
68 could_be_parallel(&self.direction, &other.direction)
69 }
70
71 pub fn nearest(&self, other: &Axis<S>) -> GeopResult<Vector3<S>> {
75 let n = self.direction.prod_cross(&other.direction);
76 let n2 = n.norm_sq();
77 if n2.could_be_equal(S::ZERO) {
78 return Err(GeopError::new(
79 "the lines are parallel, so no point of them is nearer the other than any other",
80 ));
81 }
82 let r = other.point.sub(&self.point);
83 let s = r.prod_cross(&other.direction).prod_dot(&n).div(n2)?;
84 let t = r.prod_cross(&self.direction).prod_dot(&n).div(n2)?;
85 let a = self.point.add(&self.direction.prod_scalar(s));
86 let b = other.point.add(&other.direction.prod_scalar(t));
87 Ok(Vector3::interpolate(&a, &b, S::ONE.div(S::TWO)?))
88 }
89}
90
91#[derive(Clone, Debug)]
94pub struct Circle<S: Scalar> {
95 pub center: Vector3<S>,
96 pub normal: Vector3<S>,
97 pub radius: S,
98}
99
100impl<S: Scalar> Circle<S> {
101 pub fn axis(&self) -> Axis<S> {
104 Axis {
105 point: self.center,
106 direction: self.normal,
107 }
108 }
109
110 fn could_be_equal(&self, other: &Circle<S>) -> bool {
111 self.center.could_be_equal(&other.center)
112 && self.normal.could_be_equal(&other.normal)
113 && self.radius.could_be_equal(other.radius)
114 }
115
116 fn union(&self, other: &Circle<S>) -> Circle<S> {
119 Circle {
120 center: self.center.union(&other.center),
121 normal: self.normal.union(&other.normal),
122 radius: self.radius.union(other.radius),
123 }
124 }
125}
126
127#[derive(Clone, Debug)]
131pub struct Arc<S: Scalar> {
132 pub circle: Circle<S>,
133 pub start: Vector3<S>,
134 pub end: Vector3<S>,
135}
136
137impl<S: Scalar> Arc<S> {
138 pub fn could_be_closed(&self) -> bool {
140 self.start.could_be_equal(&self.end)
141 }
142
143 pub fn sweep(&self) -> f64 {
149 if self.could_be_closed() {
150 return std::f64::consts::TAU;
151 }
152 let c = &self.circle;
153 let x = self.start.sub(&c.center);
154 let e = self.end.sub(&c.center);
155 let cos = x.prod_dot(&e).to_f64();
156 let sin = c.normal.prod_dot(&x.prod_cross(&e)).to_f64();
157 let angle = sin.atan2(cos);
158 if angle > 0.0 {
159 angle
160 } else {
161 angle + std::f64::consts::TAU
162 }
163 }
164
165 pub fn point_at(&self, fraction: f64) -> GeopResult<Vector3<S>> {
174 let c = &self.circle;
175 let x = self.start.sub(&c.center);
176 let y = c.normal.prod_cross(&x);
177 let angle = S::from_f64(fraction * self.sweep());
178 Ok(c.center
179 .add(&x.prod_scalar(angle.cos()))
180 .add(&y.prod_scalar(angle.sin())))
181 }
182
183 pub fn tangent_at(&self, p: &Vector3<S>) -> GeopResult<Vector3<S>> {
186 self.circle
187 .normal
188 .prod_cross(&p.sub(&self.circle.center))
189 .normalize()
190 }
191}
192
193#[derive(Clone, Debug)]
195pub struct Plane<S: Scalar> {
196 pub point: Vector3<S>,
197 pub normal: Vector3<S>,
198}
199
200impl<S: Scalar> Plane<S> {
201 pub fn try_new(point: Vector3<S>, normal: Vector3<S>) -> GeopResult<Self> {
204 Ok(Self {
205 point,
206 normal: normal.normalize()?,
207 })
208 }
209
210 pub fn signed_distance(&self, p: &Vector3<S>) -> S {
213 p.sub(&self.point).prod_dot(&self.normal)
214 }
215
216 pub fn project(&self, p: &Vector3<S>) -> Vector3<S> {
219 p.sub(&self.normal.prod_scalar(self.signed_distance(p)))
220 }
221
222 pub fn intersect_axis(&self, axis: &Axis<S>) -> GeopResult<Vector3<S>> {
224 let along = axis.direction.prod_dot(&self.normal);
225 if along.could_be_equal(S::ZERO) {
226 return Err(GeopError::new(
227 "the line runs parallel to the plane, so it does not pierce it",
228 ));
229 }
230 let t = self.signed_distance(&axis.point).div(along)?;
231 Ok(axis.point.sub(&axis.direction.prod_scalar(t)))
232 }
233
234 pub fn intersect_plane(&self, other: &Plane<S>) -> GeopResult<Axis<S>> {
237 let direction = self.normal.prod_cross(&other.normal);
238 let n2 = direction.norm_sq();
239 if n2.could_be_equal(S::ZERO) {
240 return Err(GeopError::new(
241 "the planes are parallel, so they do not meet",
242 ));
243 }
244 let (d1, d2) = (
247 self.point.prod_dot(&self.normal),
248 other.point.prod_dot(&other.normal),
249 );
250 let point = other
251 .normal
252 .prod_cross(&direction)
253 .prod_scalar(d1)
254 .add(&direction.prod_cross(&self.normal).prod_scalar(d2))
255 .prod_scalar(S::ONE.div(n2)?);
256 Axis::try_new(point, direction)
257 }
258}
259
260impl<S: Scalar> NurbCurve3D<S> {
263 pub fn as_line(&self) -> GeopResult<Option<Axis<S>>> {
267 let points = dehomogenize::<S, 4, 3>(&self.control_points)?;
268 let (first, last) = (points[0], points[points.len() - 1]);
269 let d = last.sub(&first);
270 if could_be_zero(&d) {
271 return Ok(None);
272 }
273 let axis = Axis::try_new(first, d)?;
274 Ok(points.iter().all(|p| axis.could_contain(p)).then_some(axis))
275 }
276
277 pub fn as_arc(&self) -> GeopResult<Option<Arc<S>>> {
282 if self.degree != 2 {
283 return Ok(None);
284 }
285 let mut circle: Option<Circle<S>> = None;
286 for piece in self.bezier_pieces()? {
287 let Some(c) = bezier_circle(&piece)? else {
288 return Ok(None);
289 };
290 circle = Some(match circle {
291 None => c,
292 Some(prev) if prev.could_be_equal(&c) => prev.union(&c),
293 Some(_) => return Ok(None),
294 });
295 }
296 let Some(circle) = circle else {
297 return Ok(None);
298 };
299 let (t0, t1) = self.domain();
300 Ok(Some(Arc {
301 circle,
302 start: self.evaluate(t0)?,
303 end: self.evaluate(t1)?,
304 }))
305 }
306
307 fn bezier_pieces(&self) -> GeopResult<Vec<Self>> {
310 let end = self.domain().1;
311 let interior = &self.knot_vector[self.degree + 1..self.control_points.len()];
312 let mut rest = self.clone();
313 let mut pieces = Vec::new();
314 for &k in interior {
315 if k.definitely_greater(rest.domain().0) && k.definitely_less(end) {
316 let (left, right) = rest.split(k)?;
317 pieces.push(left);
318 rest = right;
319 }
320 }
321 pieces.push(rest);
322 Ok(pieces)
323 }
324}
325
326fn bezier_circle<S: Scalar>(piece: &NurbCurve3D<S>) -> GeopResult<Option<Circle<S>>> {
336 let [h0, h1, h2] = match piece.control_points.as_slice() {
337 [a, b, c] => [*a, *b, *c],
338 _ => return Ok(None),
339 };
340 let points = dehomogenize::<S, 4, 3>(&[h0, h1, h2])?;
341 let (p0, p1, p2) = (points[0], points[1], points[2]);
342 let (w0, w1, w2) = (h0[3], h1[3], h2[3]);
343 let tangent = p1.sub(&p0).norm_sq();
344 if !tangent.could_be_equal(p1.sub(&p2).norm_sq()) {
345 return Ok(None);
346 }
347 let chord = p2.sub(&p0).norm_sq();
348 let four = S::TWO.add(S::TWO);
349 if !four
350 .mul(w1)
351 .mul(w1)
352 .mul(tangent)
353 .could_be_equal(w0.mul(w2).mul(chord))
354 {
355 return Ok(None);
356 }
357 let m = p0.add(&p2).prod_scalar(S::ONE.div(S::TWO)?);
361 let h = m.sub(&p1).norm_sq();
362 if h.could_be_equal(S::ZERO) {
363 return Ok(None);
364 }
365 let center = p1.add(&m.sub(&p1).prod_scalar(tangent.div(h)?));
366 let radial = p0.sub(¢er);
367 let normal = radial.prod_cross(&p1.sub(&p0)).normalize()?;
370 Ok(Some(Circle {
371 center,
372 normal,
373 radius: radial.norm(),
374 }))
375}
376
377impl<S: Scalar> NurbSurface3D<S> {
380 pub fn as_plane(&self) -> GeopResult<Option<Plane<S>>> {
384 let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
385 let (u, v) = (u0.add(u1).div(S::TWO)?, v0.add(v1).div(S::TWO)?);
386 let plane = Plane {
387 point: self.evaluate(u, v)?,
388 normal: self.normal(u, v)?,
389 };
390 let points = dehomogenize::<S, 4, 3>(&self.control_points)?;
391 Ok(points
392 .iter()
393 .all(|p| plane.signed_distance(p).could_be_equal(S::ZERO))
394 .then_some(plane))
395 }
396
397 pub fn axis_of_revolution(&self) -> GeopResult<Option<Axis<S>>> {
410 for along_u in [true, false] {
411 if let Some(axis) = self.revolution_along(along_u)? {
412 return Ok(Some(axis));
413 }
414 }
415 Ok(None)
416 }
417
418 fn revolution_along(&self, along_u: bool) -> GeopResult<Option<Axis<S>>> {
420 let (rows, len, degree, knots) = if along_u {
421 (self.num_v, self.num_u, self.degree_u, &self.knot_vector_u)
422 } else {
423 (self.num_u, self.num_v, self.degree_v, &self.knot_vector_v)
424 };
425 let row = |j: usize| -> Vec<_> {
426 (0..len)
427 .map(|i| {
428 let index = if along_u {
429 i * self.num_v + j
430 } else {
431 j * self.num_v + i
432 };
433 self.control_points[index]
434 })
435 .collect()
436 };
437 let mut reference: Option<(Arc<S>, Vec<S>)> = None;
440 let mut poles = Vec::new();
441 for j in 0..rows {
442 let cps = row(j);
443 let weights: Vec<S> = cps.iter().map(|p| p[3]).collect();
444 if let Some((_, reference_weights)) = &reference {
445 let proportional = (0..len).all(|i| {
446 weights[i]
447 .mul(reference_weights[0])
448 .could_be_equal(reference_weights[i].mul(weights[0]))
449 });
450 if !proportional {
451 return Ok(None);
452 }
453 }
454 let points = dehomogenize::<S, 4, 3>(&cps)?;
455 if points.iter().all(|p| p.could_be_equal(&points[0])) {
456 poles.push(points[0]);
457 continue;
458 }
459 let Some(arc) = NurbCurve::try_new(degree, cps, knots.clone())?.as_arc()? else {
460 return Ok(None);
461 };
462 match &reference {
463 None => reference = Some((arc, weights)),
464 Some((first, _)) => {
465 let axis = first.circle.axis();
466 let same_angle = arc
467 .start
468 .sub(&arc.circle.center)
469 .normalize()?
470 .could_be_equal(&first.start.sub(&first.circle.center).normalize()?);
471 if !arc.circle.normal.could_be_equal(&axis.direction)
472 || !axis.could_contain(&arc.circle.center)
473 || !same_angle
474 {
475 return Ok(None);
476 }
477 }
478 }
479 }
480 let Some((first, _)) = reference else {
481 return Ok(None);
482 };
483 let axis = first.circle.axis();
484 Ok(poles.iter().all(|p| axis.could_contain(p)).then_some(axis))
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use geop_core_math::{
491 for_all_scalars,
492 scalars::Scalar,
493 vector::{Vector3, Vector4},
494 };
495
496 use super::*;
497 use crate::nurb_surface::NurbSurface;
498
499 fn v<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
500 Vector3::from_array([x, y, z].map(S::from_f64))
501 }
502
503 fn h<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
505 Vector4::from_array([x * w, y * w, z * w, w].map(S::from_f64))
506 }
507
508 fn knots<S: Scalar>(ks: &[f64]) -> Vec<S> {
509 ks.iter().map(|&k| S::from_f64(k)).collect()
510 }
511
512 const R2: f64 = std::f64::consts::FRAC_1_SQRT_2;
513
514 fn unit_circle<S: Scalar>() -> NurbCurve3D<S> {
517 let corners = [
518 (1., 0.),
519 (1., 1.),
520 (0., 1.),
521 (-1., 1.),
522 (-1., 0.),
523 (-1., -1.),
524 (0., -1.),
525 (1., -1.),
526 (1., 0.),
527 ];
528 let cps = corners
529 .iter()
530 .enumerate()
531 .map(|(i, &(x, y))| h(x, y, 0., if i % 2 == 1 { R2 } else { 1. }))
532 .collect();
533 NurbCurve::try_new(
534 2,
535 cps,
536 knots(&[0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.]),
537 )
538 .unwrap()
539 }
540
541 fn check_line<S: Scalar>() {
542 let line = NurbCurve::<S, 4>::try_new(
543 2,
544 vec![h(0., 0., 0., 1.), h(1., 2., 2., 0.5), h(3., 6., 6., 1.)],
545 knots(&[0., 0., 0., 1., 1., 1.]),
546 )
547 .unwrap();
548 let axis = line.as_line().unwrap().expect("collinear control points");
549 assert!(axis.direction.could_be_equal(&v(1. / 3., 2. / 3., 2. / 3.)));
550 assert!(unit_circle::<S>().as_line().unwrap().is_none());
551 }
552 #[test]
553 fn straight_curves_are_lines() {
554 for_all_scalars!(check_line);
555 }
556
557 fn check_circle<S: Scalar>() {
558 let arc = unit_circle::<S>().as_arc().unwrap().expect("a circle");
559 assert!(arc.circle.center.could_be_equal(&v(0., 0., 0.)));
560 assert!(arc.circle.normal.could_be_equal(&v(0., 0., 1.)));
561 assert!(arc.circle.radius.could_be_equal(S::ONE));
562 assert!(arc.could_be_closed());
563 assert!(arc.point_at(0.5).unwrap().could_be_equal(&v(-1., 0., 0.)));
564 let ninety = arc.point_at(0.25).unwrap();
565 assert!(ninety.could_be_equal(&v(0., 1., 0.)));
566 assert!(
567 arc.tangent_at(&ninety)
568 .unwrap()
569 .could_be_equal(&v(-1., 0., 0.))
570 );
571 }
572 #[test]
573 fn a_circle_is_recognized_with_its_turning_sense() {
574 for_all_scalars!(check_circle);
575 }
576
577 fn check_split_arc<S: Scalar>() {
580 let (left, _) = unit_circle::<S>().split(S::from_f64(1.3)).unwrap();
581 let arc = left.as_arc().unwrap().expect("still circular");
582 assert!(arc.circle.radius.could_be_equal(S::ONE));
583 assert!(!arc.could_be_closed());
584 assert!(arc.sweep() > std::f64::consts::FRAC_PI_2);
585 assert!(arc.sweep() < std::f64::consts::PI);
586 let reversed = left.reverse().as_arc().unwrap().expect("still circular");
588 assert!(reversed.circle.normal.could_be_equal(&v(0., 0., -1.)));
589 }
590 #[test]
591 fn a_split_arc_is_still_an_arc() {
592 for_all_scalars!(check_split_arc);
593 }
594
595 fn check_not_circle<S: Scalar>() {
597 let conic = NurbCurve::<S, 4>::try_new(
598 2,
599 vec![h(1., 0., 0., 1.), h(1., 1., 0., 0.5), h(0., 1., 0., 1.)],
600 knots(&[0., 0., 0., 1., 1., 1.]),
601 )
602 .unwrap();
603 assert!(conic.as_arc().unwrap().is_none());
604 let lopsided = NurbCurve::<S, 4>::try_new(
605 2,
606 vec![h(1., 0., 0., 1.), h(1., 2., 0., R2), h(0., 1., 0., 1.)],
607 knots(&[0., 0., 0., 1., 1., 1.]),
608 )
609 .unwrap();
610 assert!(lopsided.as_arc().unwrap().is_none());
611 }
612 #[test]
613 fn other_conics_are_not_arcs() {
614 for_all_scalars!(check_not_circle);
615 }
616
617 fn quarter_cylinder<S: Scalar>() -> NurbSurface3D<S> {
620 let ring = [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)];
621 let cps = ring
622 .iter()
623 .flat_map(|&(x, y, w)| [h(x, y, 0., w), h(x, y, 3., w)])
624 .collect();
625 NurbSurface::try_new(
626 2,
627 1,
628 cps,
629 knots(&[0., 0., 0., 1., 1., 1.]),
630 knots(&[0., 0., 1., 1.]),
631 )
632 .unwrap()
633 }
634
635 fn check_revolution<S: Scalar>() {
636 let cylinder = quarter_cylinder::<S>();
637 let axis = cylinder.axis_of_revolution().unwrap().expect("a cylinder");
638 assert!(axis.could_contain(&v(0., 0., 7.)));
639 assert!(could_be_parallel(&axis.direction, &v(0., 0., 1.)));
640 assert!(cylinder.as_plane().unwrap().is_none());
641
642 let mut twisted = cylinder.clone();
644 twisted.control_points[1] = h(2., 0.1, 3., 1.);
645 assert!(twisted.axis_of_revolution().unwrap().is_none());
646 }
647 #[test]
648 fn a_cylinder_turns_around_its_axis() {
649 for_all_scalars!(check_revolution);
650 }
651
652 fn check_plane<S: Scalar>() {
653 let flat = NurbSurface::<S, 4>::try_new(
654 1,
655 1,
656 vec![
657 h(0., 0., 1., 1.),
658 h(0., 1., 1., 1.),
659 h(1., 0., 1., 1.),
660 h(1., 1., 1., 1.),
661 ],
662 knots(&[0., 0., 1., 1.]),
663 knots(&[0., 0., 1., 1.]),
664 )
665 .unwrap();
666 let plane = flat.as_plane().unwrap().expect("flat");
667 assert!(plane.normal.could_be_equal(&v(0., 0., 1.)));
668 assert!(
669 plane
670 .signed_distance(&v(5., 5., 1.))
671 .could_be_equal(S::ZERO)
672 );
673 let mut bent = flat.clone();
674 bent.control_points[3] = h(1., 1., 1.2, 1.);
675 assert!(bent.as_plane().unwrap().is_none());
676 }
677 #[test]
678 fn flat_surfaces_are_planes() {
679 for_all_scalars!(check_plane);
680 }
681
682 fn check_constructions<S: Scalar>() {
683 let z = Plane::<S>::try_new(v(0., 0., 2.), v(0., 0., 3.)).unwrap();
684 let x = Plane::try_new(v(1., 0., 0.), v(1., 0., 0.)).unwrap();
685 assert!(z.project(&v(4., 5., 6.)).could_be_equal(&v(4., 5., 2.)));
686 let line = z.intersect_plane(&x).unwrap();
687 assert!(line.could_contain(&v(1., 7., 2.)));
688 assert!(could_be_parallel(&line.direction, &v(0., 1., 0.)));
689 let slanted = Axis::try_new(v(0., 0., 0.), v(1., 1., 1.)).unwrap();
690 assert!(
691 z.intersect_axis(&slanted)
692 .unwrap()
693 .could_be_equal(&v(2., 2., 2.))
694 );
695 assert!(
696 slanted
697 .project(&v(3., 0., 0.))
698 .could_be_equal(&v(1., 1., 1.))
699 );
700 let crossing = Axis::try_new(v(2., 0., 2.), v(0., 1., 0.)).unwrap();
701 assert!(
702 slanted
703 .nearest(&crossing)
704 .unwrap()
705 .could_be_equal(&v(2., 2., 2.))
706 );
707 let skew = Axis::try_new(v(2., 0., 3.), v(0., 1., 0.)).unwrap();
710 assert!(
711 slanted
712 .nearest(&skew)
713 .unwrap()
714 .could_be_equal(&v(2.25, 2.5, 2.75))
715 );
716 assert!(slanted.nearest(&slanted).is_err());
717 assert!(z.intersect_plane(&z).is_err());
718 }
719 #[test]
720 fn planes_and_axes_meet_where_they_should() {
721 for_all_scalars!(check_constructions);
722 }
723}