1use crate::MathError;
8use crate::aabb::Aabb3;
9use crate::frame::Frame3;
10use crate::nurbs::surface::NurbsSurface;
11use crate::vec::{Point3, Vec3};
12
13#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct CylindricalSurface {
20 origin: Point3,
21 axis: Vec3,
22 radius: f64,
23 x_axis: Vec3,
24 y_axis: Vec3,
25}
26
27impl CylindricalSurface {
28 pub fn new(origin: Point3, axis: Vec3, radius: f64) -> Result<Self, MathError> {
33 if radius <= 0.0 {
34 return Err(MathError::ParameterOutOfRange {
35 value: radius,
36 min: f64::EPSILON,
37 max: f64::MAX,
38 });
39 }
40 let f = Frame3::from_normal(origin, axis)?;
41 Ok(Self {
42 origin,
43 axis: f.z,
44 radius,
45 x_axis: f.x,
46 y_axis: f.y,
47 })
48 }
49
50 #[must_use]
52 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
53 let (sin_u, cos_u) = u.sin_cos();
54 self.origin
55 + self.x_axis * (self.radius * cos_u)
56 + self.y_axis * (self.radius * sin_u)
57 + self.axis * v
58 }
59
60 #[must_use]
62 pub fn normal(&self, u: f64, _v: f64) -> Vec3 {
63 let (sin_u, cos_u) = u.sin_cos();
64 self.x_axis * cos_u + self.y_axis * sin_u
65 }
66
67 #[must_use]
69 pub const fn origin(&self) -> Point3 {
70 self.origin
71 }
72
73 #[must_use]
75 pub const fn axis(&self) -> Vec3 {
76 self.axis
77 }
78
79 #[must_use]
81 pub const fn radius(&self) -> f64 {
82 self.radius
83 }
84
85 #[must_use]
87 pub const fn x_axis(&self) -> Vec3 {
88 self.x_axis
89 }
90
91 #[must_use]
93 pub const fn y_axis(&self) -> Vec3 {
94 self.y_axis
95 }
96
97 pub fn with_ref_dir(
106 origin: Point3,
107 axis: Vec3,
108 radius: f64,
109 ref_dir: Vec3,
110 ) -> Result<Self, MathError> {
111 if radius <= 0.0 {
112 return Err(MathError::ParameterOutOfRange {
113 value: radius,
114 min: f64::EPSILON,
115 max: f64::MAX,
116 });
117 }
118 let f = Frame3::from_normal_and_ref(origin, axis, ref_dir)?;
119 Ok(Self {
120 origin,
121 axis: f.z,
122 radius,
123 x_axis: f.x,
124 y_axis: f.y,
125 })
126 }
127
128 #[must_use]
130 pub fn translated(&self, offset: Vec3) -> Self {
131 Self {
132 origin: self.origin + offset,
133 ..self.clone()
134 }
135 }
136
137 #[must_use]
141 pub fn turned(&self, angle: f64) -> Self {
142 let (sin_a, cos_a) = angle.sin_cos();
143 Self {
144 x_axis: self.x_axis * cos_a + self.y_axis * sin_a,
145 y_axis: self.y_axis * cos_a - self.x_axis * sin_a,
146 ..self.clone()
147 }
148 }
149
150 #[must_use]
154 pub fn project_point(&self, point: Point3) -> (f64, f64) {
155 let to_pt = Vec3::new(
156 point.x() - self.origin.x(),
157 point.y() - self.origin.y(),
158 point.z() - self.origin.z(),
159 );
160 let v = self.axis.dot(to_pt);
161 let radial = to_pt - self.axis * v;
162 let x = self.x_axis.dot(radial);
163 let y = self.y_axis.dot(radial);
164 let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
165 (u, v)
166 }
167
168 pub fn to_nurbs(&self, v_min: f64, v_max: f64) -> Result<NurbsSurface, MathError> {
177 let w1 = std::f64::consts::FRAC_1_SQRT_2;
179 let circle_weights = [1.0, w1, 1.0, w1, 1.0, w1, 1.0, w1, 1.0];
180 let dirs: [(f64, f64); 9] = [
182 (1.0, 0.0),
183 (1.0, 1.0),
184 (0.0, 1.0),
185 (-1.0, 1.0),
186 (-1.0, 0.0),
187 (-1.0, -1.0),
188 (0.0, -1.0),
189 (1.0, -1.0),
190 (1.0, 0.0),
191 ];
192
193 let mut cps = Vec::with_capacity(9);
194 let mut ws = Vec::with_capacity(9);
195 for (i, &(dx, dy)) in dirs.iter().enumerate() {
196 let radial = self.x_axis * (self.radius * dx) + self.y_axis * (self.radius * dy);
197 let p_bot = self.origin + radial + self.axis * v_min;
198 let p_top = self.origin + radial + self.axis * v_max;
199 cps.push(vec![p_bot, p_top]);
200 ws.push(vec![circle_weights[i], circle_weights[i]]);
201 }
202
203 let knots_u = vec![
204 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
205 ];
206 let knots_v = vec![0.0, 0.0, 1.0, 1.0];
207 NurbsSurface::new(2, 1, knots_u, knots_v, cps, ws)
208 }
209}
210
211#[derive(Debug, Clone)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub struct ConicalSurface {
218 apex: Point3,
219 axis: Vec3,
220 half_angle: f64,
221 x_axis: Vec3,
222 y_axis: Vec3,
223}
224
225impl ConicalSurface {
226 pub fn new(apex: Point3, axis: Vec3, half_angle: f64) -> Result<Self, MathError> {
236 if half_angle <= 0.0 || half_angle >= std::f64::consts::FRAC_PI_2 {
237 return Err(MathError::ParameterOutOfRange {
238 value: half_angle,
239 min: f64::EPSILON,
240 max: std::f64::consts::FRAC_PI_2,
241 });
242 }
243 let f = Frame3::from_normal(apex, axis)?;
244 Ok(Self {
245 apex,
246 axis: f.z,
247 half_angle,
248 x_axis: f.x,
249 y_axis: f.y,
250 })
251 }
252
253 #[must_use]
255 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
256 let (sin_u, cos_u) = u.sin_cos();
257 let (sin_a, cos_a) = self.half_angle.sin_cos();
258 let radial = self.x_axis * cos_u + self.y_axis * sin_u;
259 self.apex + (radial * cos_a + self.axis * sin_a) * v
260 }
261
262 #[must_use]
264 pub fn normal(&self, u: f64, _v: f64) -> Vec3 {
265 let (sin_u, cos_u) = u.sin_cos();
266 let (sin_a, cos_a) = self.half_angle.sin_cos();
267 let radial = self.x_axis * cos_u + self.y_axis * sin_u;
268 radial * sin_a + self.axis * (-cos_a)
270 }
271
272 #[must_use]
274 pub const fn apex(&self) -> Point3 {
275 self.apex
276 }
277
278 #[must_use]
280 pub const fn axis(&self) -> Vec3 {
281 self.axis
282 }
283
284 #[must_use]
286 pub const fn half_angle(&self) -> f64 {
287 self.half_angle
288 }
289
290 #[must_use]
292 pub const fn x_axis(&self) -> Vec3 {
293 self.x_axis
294 }
295
296 #[must_use]
298 pub const fn y_axis(&self) -> Vec3 {
299 self.y_axis
300 }
301
302 pub fn with_ref_dir(
311 apex: Point3,
312 axis: Vec3,
313 half_angle: f64,
314 ref_dir: Vec3,
315 ) -> Result<Self, MathError> {
316 if half_angle <= 0.0 || half_angle >= std::f64::consts::FRAC_PI_2 {
317 return Err(MathError::ParameterOutOfRange {
318 value: half_angle,
319 min: f64::EPSILON,
320 max: std::f64::consts::FRAC_PI_2,
321 });
322 }
323 let f = Frame3::from_normal_and_ref(apex, axis, ref_dir)?;
324 Ok(Self {
325 apex,
326 axis: f.z,
327 half_angle,
328 x_axis: f.x,
329 y_axis: f.y,
330 })
331 }
332
333 #[must_use]
335 pub fn translated(&self, offset: Vec3) -> Self {
336 Self {
337 apex: self.apex + offset,
338 ..self.clone()
339 }
340 }
341
342 #[must_use]
346 pub fn turned(&self, angle: f64) -> Self {
347 let (sin_a, cos_a) = angle.sin_cos();
348 Self {
349 x_axis: self.x_axis * cos_a + self.y_axis * sin_a,
350 y_axis: self.y_axis * cos_a - self.x_axis * sin_a,
351 ..self.clone()
352 }
353 }
354
355 #[must_use]
357 pub fn radius_at(&self, v: f64) -> f64 {
358 v * self.half_angle.cos()
359 }
360
361 #[must_use]
366 pub fn project_point(&self, point: Point3) -> (f64, f64) {
367 let to_pt = Vec3::new(
368 point.x() - self.apex.x(),
369 point.y() - self.apex.y(),
370 point.z() - self.apex.z(),
371 );
372
373 let h = self.axis.dot(to_pt);
374 let radial = to_pt - self.axis * h;
375 let x = self.x_axis.dot(radial);
376 let y = self.y_axis.dot(radial);
377
378 let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
379
380 let sin_a = self.half_angle.sin();
381 let v = if sin_a.abs() > 1e-15 {
382 h / sin_a
383 } else {
384 let cos_a = self.half_angle.cos();
385 if cos_a.abs() > 1e-15 {
386 radial.length() / cos_a
387 } else {
388 0.0
389 }
390 };
391
392 (u, v)
393 }
394
395 pub fn to_nurbs(&self, v_min: f64, v_max: f64) -> Result<NurbsSurface, MathError> {
401 analytic_to_nurbs_sampled(
402 |u, v| self.evaluate(u, v),
403 (0.0, std::f64::consts::TAU),
404 (v_min, v_max),
405 )
406 }
407
408 pub fn to_rational_nurbs(&self, v_min: f64, v_max: f64) -> Result<NurbsSurface, MathError> {
419 let w1 = std::f64::consts::FRAC_1_SQRT_2;
420 let circle_weights = [1.0, w1, 1.0, w1, 1.0, w1, 1.0, w1, 1.0];
421 let dirs: [(f64, f64); 9] = [
422 (1.0, 0.0),
423 (1.0, 1.0),
424 (0.0, 1.0),
425 (-1.0, 1.0),
426 (-1.0, 0.0),
427 (-1.0, -1.0),
428 (0.0, -1.0),
429 (1.0, -1.0),
430 (1.0, 0.0),
431 ];
432 let (sin_a, cos_a) = self.half_angle.sin_cos();
433 let ring = |v: f64, dx: f64, dy: f64| {
434 let rho = v * cos_a;
435 self.apex
436 + self.axis * (v * sin_a)
437 + self.x_axis * (rho * dx)
438 + self.y_axis * (rho * dy)
439 };
440 let mut cps = Vec::with_capacity(9);
441 let mut ws = Vec::with_capacity(9);
442 for (i, &(dx, dy)) in dirs.iter().enumerate() {
443 cps.push(vec![ring(v_min, dx, dy), ring(v_max, dx, dy)]);
444 ws.push(vec![circle_weights[i], circle_weights[i]]);
445 }
446 let knots_u = vec![
447 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
448 ];
449 let knots_v = vec![0.0, 0.0, 1.0, 1.0];
450 NurbsSurface::new(2, 1, knots_u, knots_v, cps, ws)
451 }
452}
453
454#[derive(Debug, Clone)]
459#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
460pub struct SphericalSurface {
461 center: Point3,
462 radius: f64,
463 x_axis: Vec3,
464 y_axis: Vec3,
465 z_axis: Vec3,
466}
467
468impl SphericalSurface {
469 pub fn new(center: Point3, radius: f64) -> Result<Self, MathError> {
474 if radius <= 0.0 {
475 return Err(MathError::ParameterOutOfRange {
476 value: radius,
477 min: f64::EPSILON,
478 max: f64::MAX,
479 });
480 }
481 Ok(Self {
482 center,
483 radius,
484 x_axis: Vec3::new(1.0, 0.0, 0.0),
485 y_axis: Vec3::new(0.0, 1.0, 0.0),
486 z_axis: Vec3::new(0.0, 0.0, 1.0),
487 })
488 }
489
490 pub fn with_axis(center: Point3, radius: f64, z_axis: Vec3) -> Result<Self, MathError> {
495 if radius <= 0.0 {
496 return Err(MathError::ParameterOutOfRange {
497 value: radius,
498 min: f64::EPSILON,
499 max: f64::MAX,
500 });
501 }
502 let f = Frame3::from_normal(center, z_axis)?;
503 Ok(Self {
504 center,
505 radius,
506 x_axis: f.x,
507 y_axis: f.y,
508 z_axis: f.z,
509 })
510 }
511
512 pub fn with_axis_and_ref_dir(
522 center: Point3,
523 radius: f64,
524 z_axis: Vec3,
525 ref_dir: Vec3,
526 ) -> Result<Self, MathError> {
527 if radius <= 0.0 {
528 return Err(MathError::ParameterOutOfRange {
529 value: radius,
530 min: f64::EPSILON,
531 max: f64::MAX,
532 });
533 }
534 let f = Frame3::from_normal_and_ref(center, z_axis, ref_dir)?;
535 Ok(Self {
536 center,
537 radius,
538 x_axis: f.x,
539 y_axis: f.y,
540 z_axis: f.z,
541 })
542 }
543
544 #[must_use]
546 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
547 let (sin_u, cos_u) = u.sin_cos();
548 let (sin_v, cos_v) = v.sin_cos();
549 self.center
550 + self.x_axis * (self.radius * cos_v * cos_u)
551 + self.y_axis * (self.radius * cos_v * sin_u)
552 + self.z_axis * (self.radius * sin_v)
553 }
554
555 #[must_use]
557 pub fn normal(&self, u: f64, v: f64) -> Vec3 {
558 let (sin_u, cos_u) = u.sin_cos();
559 let (sin_v, cos_v) = v.sin_cos();
560 self.x_axis * (cos_v * cos_u) + self.y_axis * (cos_v * sin_u) + self.z_axis * sin_v
561 }
562
563 #[must_use]
565 pub const fn center(&self) -> Point3 {
566 self.center
567 }
568
569 #[must_use]
571 pub const fn radius(&self) -> f64 {
572 self.radius
573 }
574
575 #[must_use]
577 pub const fn x_axis(&self) -> Vec3 {
578 self.x_axis
579 }
580
581 #[must_use]
583 pub const fn y_axis(&self) -> Vec3 {
584 self.y_axis
585 }
586
587 #[must_use]
589 pub const fn z_axis(&self) -> Vec3 {
590 self.z_axis
591 }
592
593 #[must_use]
595 pub fn translated(&self, offset: Vec3) -> Self {
596 Self {
597 center: self.center + offset,
598 ..self.clone()
599 }
600 }
601
602 #[must_use]
606 pub fn turned(&self, angle: f64) -> Self {
607 let (sin_a, cos_a) = angle.sin_cos();
608 Self {
609 x_axis: self.x_axis * cos_a + self.y_axis * sin_a,
610 y_axis: self.y_axis * cos_a - self.x_axis * sin_a,
611 ..self.clone()
612 }
613 }
614
615 #[must_use]
621 pub fn aabb(&self) -> Aabb3 {
622 let r = self.radius;
623 Aabb3 {
624 min: Point3::new(
625 self.center.x() - r,
626 self.center.y() - r,
627 self.center.z() - r,
628 ),
629 max: Point3::new(
630 self.center.x() + r,
631 self.center.y() + r,
632 self.center.z() + r,
633 ),
634 }
635 }
636
637 #[must_use]
644 pub fn aabb_region(&self, pole: Vec3) -> Aabb3 {
645 let Ok(n) = pole.normalize() else {
646 return self.aabb();
647 };
648 let c = self.center;
649 let r = self.radius;
650 let span = |en: f64| -> (f64, f64) {
654 let s = (1.0 - en * en).max(0.0).sqrt();
655 let hi = if en >= 0.0 { 1.0 } else { s };
656 let lo = if en <= 0.0 { -1.0 } else { -s };
657 (lo, hi)
658 };
659 let (lx, hx) = span(n.x());
660 let (ly, hy) = span(n.y());
661 let (lz, hz) = span(n.z());
662 Aabb3 {
663 min: Point3::new(c.x() + r * lx, c.y() + r * ly, c.z() + r * lz),
664 max: Point3::new(c.x() + r * hx, c.y() + r * hy, c.z() + r * hz),
665 }
666 }
667
668 #[must_use]
672 pub fn project_point(&self, point: Point3) -> (f64, f64) {
673 let to_pt = Vec3::new(
674 point.x() - self.center.x(),
675 point.y() - self.center.y(),
676 point.z() - self.center.z(),
677 );
678 let r = to_pt.length();
679 if r < 1e-15 {
680 return (0.0, 0.0);
681 }
682 let x = self.x_axis.dot(to_pt);
683 let y = self.y_axis.dot(to_pt);
684 let z = self.z_axis.dot(to_pt);
685 let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
686 let v = (z / r).clamp(-1.0, 1.0).asin();
687 (u, v)
688 }
689
690 pub fn to_nurbs(&self) -> Result<NurbsSurface, MathError> {
696 analytic_to_nurbs_sampled(
697 |u, v| self.evaluate(u, v),
698 (0.0, std::f64::consts::TAU),
699 (-std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2),
700 )
701 }
702}
703
704#[derive(Debug, Clone)]
710#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
711pub struct ToroidalSurface {
712 center: Point3,
713 major_radius: f64,
714 minor_radius: f64,
715 x_axis: Vec3,
716 y_axis: Vec3,
717 z_axis: Vec3,
718}
719
720impl ToroidalSurface {
721 pub fn new(center: Point3, major_radius: f64, minor_radius: f64) -> Result<Self, MathError> {
726 if major_radius <= 0.0 {
727 return Err(MathError::ParameterOutOfRange {
728 value: major_radius,
729 min: f64::EPSILON,
730 max: f64::MAX,
731 });
732 }
733 if minor_radius <= 0.0 {
734 return Err(MathError::ParameterOutOfRange {
735 value: minor_radius,
736 min: f64::EPSILON,
737 max: f64::MAX,
738 });
739 }
740 Ok(Self {
741 center,
742 major_radius,
743 minor_radius,
744 x_axis: Vec3::new(1.0, 0.0, 0.0),
745 y_axis: Vec3::new(0.0, 1.0, 0.0),
746 z_axis: Vec3::new(0.0, 0.0, 1.0),
747 })
748 }
749
750 pub fn with_axis(
758 center: Point3,
759 major_radius: f64,
760 minor_radius: f64,
761 z_axis: Vec3,
762 ) -> Result<Self, MathError> {
763 if major_radius <= 0.0 {
764 return Err(MathError::ParameterOutOfRange {
765 value: major_radius,
766 min: f64::EPSILON,
767 max: f64::MAX,
768 });
769 }
770 if minor_radius <= 0.0 {
771 return Err(MathError::ParameterOutOfRange {
772 value: minor_radius,
773 min: f64::EPSILON,
774 max: f64::MAX,
775 });
776 }
777 let f = Frame3::from_normal(center, z_axis)?;
778 Ok(Self {
779 center,
780 major_radius,
781 minor_radius,
782 x_axis: f.x,
783 y_axis: f.y,
784 z_axis: f.z,
785 })
786 }
787
788 pub fn with_axis_and_ref_dir(
804 center: Point3,
805 major_radius: f64,
806 minor_radius: f64,
807 z_axis: Vec3,
808 ref_dir: Vec3,
809 ) -> Result<Self, MathError> {
810 if major_radius <= 0.0 {
811 return Err(MathError::ParameterOutOfRange {
812 value: major_radius,
813 min: f64::EPSILON,
814 max: f64::MAX,
815 });
816 }
817 if minor_radius <= 0.0 {
818 return Err(MathError::ParameterOutOfRange {
819 value: minor_radius,
820 min: f64::EPSILON,
821 max: f64::MAX,
822 });
823 }
824 let f = Frame3::from_normal_and_ref(center, z_axis, ref_dir)?;
825 Ok(Self {
826 center,
827 major_radius,
828 minor_radius,
829 x_axis: f.x,
830 y_axis: f.y,
831 z_axis: f.z,
832 })
833 }
834
835 #[must_use]
837 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
838 let (sin_u, cos_u) = u.sin_cos();
839 let (sin_v, cos_v) = v.sin_cos();
840 let tube_radius = self.minor_radius.mul_add(cos_v, self.major_radius);
841 self.center
842 + self.x_axis * (tube_radius * cos_u)
843 + self.y_axis * (tube_radius * sin_u)
844 + self.z_axis * (self.minor_radius * sin_v)
845 }
846
847 #[must_use]
849 pub fn normal(&self, u: f64, v: f64) -> Vec3 {
850 let (sin_u, cos_u) = u.sin_cos();
851 let (sin_v, cos_v) = v.sin_cos();
852 let radial = self.x_axis * cos_u + self.y_axis * sin_u;
853 radial * cos_v + self.z_axis * sin_v
854 }
855
856 #[must_use]
858 pub const fn center(&self) -> Point3 {
859 self.center
860 }
861
862 #[must_use]
864 pub fn translated(&self, offset: Vec3) -> Self {
865 Self {
866 center: self.center + offset,
867 ..self.clone()
868 }
869 }
870
871 #[must_use]
875 pub fn turned(&self, angle: f64) -> Self {
876 let (sin_a, cos_a) = angle.sin_cos();
877 Self {
878 x_axis: self.x_axis * cos_a + self.y_axis * sin_a,
879 y_axis: self.y_axis * cos_a - self.x_axis * sin_a,
880 ..self.clone()
881 }
882 }
883
884 #[must_use]
891 pub fn aabb(&self) -> Aabb3 {
892 let rr = self.major_radius + self.minor_radius;
893 let r = self.minor_radius;
894 let hx = rr * self.x_axis.x().hypot(self.y_axis.x()) + r * self.z_axis.x().abs();
895 let hy = rr * self.x_axis.y().hypot(self.y_axis.y()) + r * self.z_axis.y().abs();
896 let hz = rr * self.x_axis.z().hypot(self.y_axis.z()) + r * self.z_axis.z().abs();
897 Aabb3 {
898 min: Point3::new(
899 self.center.x() - hx,
900 self.center.y() - hy,
901 self.center.z() - hz,
902 ),
903 max: Point3::new(
904 self.center.x() + hx,
905 self.center.y() + hy,
906 self.center.z() + hz,
907 ),
908 }
909 }
910
911 #[must_use]
913 pub const fn major_radius(&self) -> f64 {
914 self.major_radius
915 }
916
917 #[must_use]
919 pub const fn minor_radius(&self) -> f64 {
920 self.minor_radius
921 }
922
923 #[must_use]
925 pub const fn x_axis(&self) -> Vec3 {
926 self.x_axis
927 }
928
929 #[must_use]
931 pub const fn y_axis(&self) -> Vec3 {
932 self.y_axis
933 }
934
935 #[must_use]
937 pub const fn z_axis(&self) -> Vec3 {
938 self.z_axis
939 }
940
941 #[must_use]
946 pub fn project_point(&self, point: Point3) -> (f64, f64) {
947 let to_pt = Vec3::new(
948 point.x() - self.center.x(),
949 point.y() - self.center.y(),
950 point.z() - self.center.z(),
951 );
952
953 let x_comp = self.x_axis.dot(to_pt);
954 let y_comp = self.y_axis.dot(to_pt);
955 let u = y_comp.atan2(x_comp).rem_euclid(std::f64::consts::TAU);
956
957 let (sin_u, cos_u) = u.sin_cos();
958 let tube_center = self.center
959 + self.x_axis * (self.major_radius * cos_u)
960 + self.y_axis * (self.major_radius * sin_u);
961
962 let to_tube = Vec3::new(
963 point.x() - tube_center.x(),
964 point.y() - tube_center.y(),
965 point.z() - tube_center.z(),
966 );
967
968 let radial_dir = self.x_axis * cos_u + self.y_axis * sin_u;
969 let r_comp = radial_dir.dot(to_tube);
970 let z_comp = self.z_axis.dot(to_tube);
971
972 let v = z_comp.atan2(r_comp).rem_euclid(std::f64::consts::TAU);
973 (u, v)
974 }
975
976 pub fn to_nurbs(&self) -> Result<NurbsSurface, MathError> {
982 analytic_to_nurbs_sampled(
983 |u, v| self.evaluate(u, v),
984 (0.0, std::f64::consts::TAU),
985 (0.0, std::f64::consts::TAU),
986 )
987 }
988
989 pub fn to_rational_nurbs(&self) -> Result<NurbsSurface, MathError> {
999 use std::f64::consts::{FRAC_1_SQRT_2, TAU};
1000 let dirs: [(f64, f64); 9] = [
1001 (1.0, 0.0),
1002 (1.0, 1.0),
1003 (0.0, 1.0),
1004 (-1.0, 1.0),
1005 (-1.0, 0.0),
1006 (-1.0, -1.0),
1007 (0.0, -1.0),
1008 (1.0, -1.0),
1009 (1.0, 0.0),
1010 ];
1011 let weight = |i: usize| if i % 2 == 1 { FRAC_1_SQRT_2 } else { 1.0 };
1012 let mut cps = Vec::with_capacity(9);
1013 let mut weights = Vec::with_capacity(9);
1014 for (i, &(ux, uy)) in dirs.iter().enumerate() {
1015 let mut row = Vec::with_capacity(9);
1016 let mut w_row = Vec::with_capacity(9);
1017 for (j, &(dx, dy)) in dirs.iter().enumerate() {
1018 let radial = dx.mul_add(self.minor_radius, self.major_radius);
1019 row.push(
1020 self.center
1021 + self.x_axis * (ux * radial)
1022 + self.y_axis * (uy * radial)
1023 + self.z_axis * (dy * self.minor_radius),
1024 );
1025 w_row.push(weight(i) * weight(j));
1026 }
1027 cps.push(row);
1028 weights.push(w_row);
1029 }
1030 let knots = vec![
1031 0.0,
1032 0.0,
1033 0.0,
1034 TAU * 0.25,
1035 TAU * 0.25,
1036 TAU * 0.5,
1037 TAU * 0.5,
1038 TAU * 0.75,
1039 TAU * 0.75,
1040 TAU,
1041 TAU,
1042 TAU,
1043 ];
1044 NurbsSurface::new(2, 2, knots.clone(), knots, cps, weights)
1045 }
1046}
1047
1048#[derive(Debug, Clone)]
1054pub struct RevolutionSurface {
1055 origin: Point3,
1056 axis: Vec3,
1057 x_axis: Vec3,
1058 y_axis: Vec3,
1059 generatrix_radii: Vec<f64>,
1061 generatrix_heights: Vec<f64>,
1062}
1063
1064impl RevolutionSurface {
1065 pub fn new(
1072 origin: Point3,
1073 axis: Vec3,
1074 radii: Vec<f64>,
1075 heights: Vec<f64>,
1076 ) -> Result<Self, MathError> {
1077 if radii.is_empty() || heights.is_empty() {
1078 return Err(MathError::EmptyInput);
1079 }
1080 if radii.len() != heights.len() {
1081 return Err(MathError::InvalidWeights {
1082 expected: radii.len(),
1083 got: heights.len(),
1084 });
1085 }
1086 let f = Frame3::from_normal(origin, axis)?;
1087 Ok(Self {
1088 origin,
1089 axis: f.z,
1090 x_axis: f.x,
1091 y_axis: f.y,
1092 generatrix_radii: radii,
1093 generatrix_heights: heights,
1094 })
1095 }
1096
1097 #[must_use]
1100 #[allow(
1101 clippy::cast_precision_loss,
1102 clippy::cast_possible_truncation,
1103 clippy::cast_sign_loss
1104 )]
1105 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
1106 let num_pts = self.generatrix_radii.len();
1107 let param = v.clamp(0.0, 1.0) * (num_pts - 1) as f64;
1108 let idx = (param as usize).min(num_pts - 2);
1109 let frac = param - idx as f64;
1110
1111 let r = frac.mul_add(
1112 self.generatrix_radii[idx + 1] - self.generatrix_radii[idx],
1113 self.generatrix_radii[idx],
1114 );
1115 let height = frac.mul_add(
1116 self.generatrix_heights[idx + 1] - self.generatrix_heights[idx],
1117 self.generatrix_heights[idx],
1118 );
1119
1120 let (sin_u, cos_u) = u.sin_cos();
1121 self.origin + self.x_axis * (r * cos_u) + self.y_axis * (r * sin_u) + self.axis * height
1122 }
1123
1124 #[must_use]
1126 pub const fn origin(&self) -> Point3 {
1127 self.origin
1128 }
1129
1130 #[must_use]
1132 pub const fn axis(&self) -> Vec3 {
1133 self.axis
1134 }
1135}
1136
1137fn analytic_to_nurbs_sampled(
1148 surface_fn: impl Fn(f64, f64) -> Point3,
1149 u_range: (f64, f64),
1150 v_range: (f64, f64),
1151) -> Result<NurbsSurface, MathError> {
1152 let nu = 33;
1156 let nv = 9;
1157
1158 let mut cps = Vec::with_capacity(nu);
1159 let mut weights = Vec::with_capacity(nu);
1160
1161 #[allow(clippy::cast_precision_loss)]
1162 for iu in 0..nu {
1163 let u = u_range.0 + (u_range.1 - u_range.0) * (iu as f64 / (nu - 1) as f64);
1164 let mut row = Vec::with_capacity(nv);
1165 let mut w_row = Vec::with_capacity(nv);
1166 for iv in 0..nv {
1167 let v = v_range.0 + (v_range.1 - v_range.0) * (iv as f64 / (nv - 1) as f64);
1168 row.push(surface_fn(u, v));
1169 w_row.push(1.0);
1170 }
1171 cps.push(row);
1172 weights.push(w_row);
1173 }
1174
1175 let knots_u = uniform_clamped_knots(nu, 1);
1178 let knots_v = uniform_clamped_knots(nv, 1);
1179
1180 NurbsSurface::new(1, 1, knots_u, knots_v, cps, weights)
1181}
1182
1183#[allow(clippy::cast_precision_loss)]
1188fn uniform_clamped_knots(n: usize, degree: usize) -> Vec<f64> {
1189 let mut k = vec![0.0; degree + 1];
1190 for i in 1..n - degree {
1191 k.push(i as f64 / (n - degree) as f64);
1192 }
1193 k.extend(vec![1.0; degree + 1]);
1194 k
1195}
1196
1197#[cfg(test)]
1198#[allow(clippy::unwrap_used, clippy::expect_used)]
1199mod tests;