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 pub fn to_rational_nurbs(&self) -> Result<NurbsSurface, MathError> {
713 use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, TAU};
714 let round: [(f64, f64); 9] = [
715 (1.0, 0.0),
716 (1.0, 1.0),
717 (0.0, 1.0),
718 (-1.0, 1.0),
719 (-1.0, 0.0),
720 (-1.0, -1.0),
721 (0.0, -1.0),
722 (1.0, -1.0),
723 (1.0, 0.0),
724 ];
725 let meridian: [(f64, f64); 5] =
726 [(0.0, -1.0), (1.0, -1.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
727 let weight = |i: usize| if i % 2 == 1 { FRAC_1_SQRT_2 } else { 1.0 };
728 let r = self.radius;
729 let mut cps = Vec::with_capacity(9);
730 let mut weights = Vec::with_capacity(9);
731 for (i, &(ux, uy)) in round.iter().enumerate() {
732 let mut row = Vec::with_capacity(5);
733 let mut w_row = Vec::with_capacity(5);
734 for (j, &(rho, z)) in meridian.iter().enumerate() {
735 row.push(
736 self.center
737 + self.x_axis * (ux * rho * r)
738 + self.y_axis * (uy * rho * r)
739 + self.z_axis * (z * r),
740 );
741 w_row.push(weight(i) * weight(j));
742 }
743 cps.push(row);
744 weights.push(w_row);
745 }
746 let knots_u = vec![
747 0.0,
748 0.0,
749 0.0,
750 TAU * 0.25,
751 TAU * 0.25,
752 TAU * 0.5,
753 TAU * 0.5,
754 TAU * 0.75,
755 TAU * 0.75,
756 TAU,
757 TAU,
758 TAU,
759 ];
760 let knots_v = vec![
761 -FRAC_PI_2, -FRAC_PI_2, -FRAC_PI_2, 0.0, 0.0, FRAC_PI_2, FRAC_PI_2, FRAC_PI_2,
762 ];
763 NurbsSurface::new(2, 2, knots_u, knots_v, cps, weights)
764 }
765}
766
767#[derive(Debug, Clone)]
773#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
774pub struct ToroidalSurface {
775 center: Point3,
776 major_radius: f64,
777 minor_radius: f64,
778 x_axis: Vec3,
779 y_axis: Vec3,
780 z_axis: Vec3,
781}
782
783impl ToroidalSurface {
784 pub fn new(center: Point3, major_radius: f64, minor_radius: f64) -> Result<Self, MathError> {
789 if major_radius <= 0.0 {
790 return Err(MathError::ParameterOutOfRange {
791 value: major_radius,
792 min: f64::EPSILON,
793 max: f64::MAX,
794 });
795 }
796 if minor_radius <= 0.0 {
797 return Err(MathError::ParameterOutOfRange {
798 value: minor_radius,
799 min: f64::EPSILON,
800 max: f64::MAX,
801 });
802 }
803 Ok(Self {
804 center,
805 major_radius,
806 minor_radius,
807 x_axis: Vec3::new(1.0, 0.0, 0.0),
808 y_axis: Vec3::new(0.0, 1.0, 0.0),
809 z_axis: Vec3::new(0.0, 0.0, 1.0),
810 })
811 }
812
813 pub fn with_axis(
821 center: Point3,
822 major_radius: f64,
823 minor_radius: f64,
824 z_axis: Vec3,
825 ) -> Result<Self, MathError> {
826 if major_radius <= 0.0 {
827 return Err(MathError::ParameterOutOfRange {
828 value: major_radius,
829 min: f64::EPSILON,
830 max: f64::MAX,
831 });
832 }
833 if minor_radius <= 0.0 {
834 return Err(MathError::ParameterOutOfRange {
835 value: minor_radius,
836 min: f64::EPSILON,
837 max: f64::MAX,
838 });
839 }
840 let f = Frame3::from_normal(center, z_axis)?;
841 Ok(Self {
842 center,
843 major_radius,
844 minor_radius,
845 x_axis: f.x,
846 y_axis: f.y,
847 z_axis: f.z,
848 })
849 }
850
851 pub fn with_axis_and_ref_dir(
867 center: Point3,
868 major_radius: f64,
869 minor_radius: f64,
870 z_axis: Vec3,
871 ref_dir: Vec3,
872 ) -> Result<Self, MathError> {
873 if major_radius <= 0.0 {
874 return Err(MathError::ParameterOutOfRange {
875 value: major_radius,
876 min: f64::EPSILON,
877 max: f64::MAX,
878 });
879 }
880 if minor_radius <= 0.0 {
881 return Err(MathError::ParameterOutOfRange {
882 value: minor_radius,
883 min: f64::EPSILON,
884 max: f64::MAX,
885 });
886 }
887 let f = Frame3::from_normal_and_ref(center, z_axis, ref_dir)?;
888 Ok(Self {
889 center,
890 major_radius,
891 minor_radius,
892 x_axis: f.x,
893 y_axis: f.y,
894 z_axis: f.z,
895 })
896 }
897
898 #[must_use]
900 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
901 let (sin_u, cos_u) = u.sin_cos();
902 let (sin_v, cos_v) = v.sin_cos();
903 let tube_radius = self.minor_radius.mul_add(cos_v, self.major_radius);
904 self.center
905 + self.x_axis * (tube_radius * cos_u)
906 + self.y_axis * (tube_radius * sin_u)
907 + self.z_axis * (self.minor_radius * sin_v)
908 }
909
910 #[must_use]
912 pub fn normal(&self, u: f64, v: f64) -> Vec3 {
913 let (sin_u, cos_u) = u.sin_cos();
914 let (sin_v, cos_v) = v.sin_cos();
915 let radial = self.x_axis * cos_u + self.y_axis * sin_u;
916 radial * cos_v + self.z_axis * sin_v
917 }
918
919 #[must_use]
921 pub const fn center(&self) -> Point3 {
922 self.center
923 }
924
925 #[must_use]
927 pub fn translated(&self, offset: Vec3) -> Self {
928 Self {
929 center: self.center + offset,
930 ..self.clone()
931 }
932 }
933
934 #[must_use]
938 pub fn turned(&self, angle: f64) -> Self {
939 let (sin_a, cos_a) = angle.sin_cos();
940 Self {
941 x_axis: self.x_axis * cos_a + self.y_axis * sin_a,
942 y_axis: self.y_axis * cos_a - self.x_axis * sin_a,
943 ..self.clone()
944 }
945 }
946
947 #[must_use]
954 pub fn aabb(&self) -> Aabb3 {
955 let rr = self.major_radius + self.minor_radius;
956 let r = self.minor_radius;
957 let hx = rr * self.x_axis.x().hypot(self.y_axis.x()) + r * self.z_axis.x().abs();
958 let hy = rr * self.x_axis.y().hypot(self.y_axis.y()) + r * self.z_axis.y().abs();
959 let hz = rr * self.x_axis.z().hypot(self.y_axis.z()) + r * self.z_axis.z().abs();
960 Aabb3 {
961 min: Point3::new(
962 self.center.x() - hx,
963 self.center.y() - hy,
964 self.center.z() - hz,
965 ),
966 max: Point3::new(
967 self.center.x() + hx,
968 self.center.y() + hy,
969 self.center.z() + hz,
970 ),
971 }
972 }
973
974 #[must_use]
976 pub const fn major_radius(&self) -> f64 {
977 self.major_radius
978 }
979
980 #[must_use]
982 pub const fn minor_radius(&self) -> f64 {
983 self.minor_radius
984 }
985
986 #[must_use]
988 pub const fn x_axis(&self) -> Vec3 {
989 self.x_axis
990 }
991
992 #[must_use]
994 pub const fn y_axis(&self) -> Vec3 {
995 self.y_axis
996 }
997
998 #[must_use]
1000 pub const fn z_axis(&self) -> Vec3 {
1001 self.z_axis
1002 }
1003
1004 #[must_use]
1009 pub fn project_point(&self, point: Point3) -> (f64, f64) {
1010 let to_pt = Vec3::new(
1011 point.x() - self.center.x(),
1012 point.y() - self.center.y(),
1013 point.z() - self.center.z(),
1014 );
1015
1016 let x_comp = self.x_axis.dot(to_pt);
1017 let y_comp = self.y_axis.dot(to_pt);
1018 let u = y_comp.atan2(x_comp).rem_euclid(std::f64::consts::TAU);
1019
1020 let (sin_u, cos_u) = u.sin_cos();
1021 let tube_center = self.center
1022 + self.x_axis * (self.major_radius * cos_u)
1023 + self.y_axis * (self.major_radius * sin_u);
1024
1025 let to_tube = Vec3::new(
1026 point.x() - tube_center.x(),
1027 point.y() - tube_center.y(),
1028 point.z() - tube_center.z(),
1029 );
1030
1031 let radial_dir = self.x_axis * cos_u + self.y_axis * sin_u;
1032 let r_comp = radial_dir.dot(to_tube);
1033 let z_comp = self.z_axis.dot(to_tube);
1034
1035 let v = z_comp.atan2(r_comp).rem_euclid(std::f64::consts::TAU);
1036 (u, v)
1037 }
1038
1039 pub fn to_nurbs(&self) -> Result<NurbsSurface, MathError> {
1045 analytic_to_nurbs_sampled(
1046 |u, v| self.evaluate(u, v),
1047 (0.0, std::f64::consts::TAU),
1048 (0.0, std::f64::consts::TAU),
1049 )
1050 }
1051
1052 pub fn to_rational_nurbs(&self) -> Result<NurbsSurface, MathError> {
1062 use std::f64::consts::{FRAC_1_SQRT_2, TAU};
1063 let dirs: [(f64, f64); 9] = [
1064 (1.0, 0.0),
1065 (1.0, 1.0),
1066 (0.0, 1.0),
1067 (-1.0, 1.0),
1068 (-1.0, 0.0),
1069 (-1.0, -1.0),
1070 (0.0, -1.0),
1071 (1.0, -1.0),
1072 (1.0, 0.0),
1073 ];
1074 let weight = |i: usize| if i % 2 == 1 { FRAC_1_SQRT_2 } else { 1.0 };
1075 let mut cps = Vec::with_capacity(9);
1076 let mut weights = Vec::with_capacity(9);
1077 for (i, &(ux, uy)) in dirs.iter().enumerate() {
1078 let mut row = Vec::with_capacity(9);
1079 let mut w_row = Vec::with_capacity(9);
1080 for (j, &(dx, dy)) in dirs.iter().enumerate() {
1081 let radial = dx.mul_add(self.minor_radius, self.major_radius);
1082 row.push(
1083 self.center
1084 + self.x_axis * (ux * radial)
1085 + self.y_axis * (uy * radial)
1086 + self.z_axis * (dy * self.minor_radius),
1087 );
1088 w_row.push(weight(i) * weight(j));
1089 }
1090 cps.push(row);
1091 weights.push(w_row);
1092 }
1093 let knots = vec![
1094 0.0,
1095 0.0,
1096 0.0,
1097 TAU * 0.25,
1098 TAU * 0.25,
1099 TAU * 0.5,
1100 TAU * 0.5,
1101 TAU * 0.75,
1102 TAU * 0.75,
1103 TAU,
1104 TAU,
1105 TAU,
1106 ];
1107 NurbsSurface::new(2, 2, knots.clone(), knots, cps, weights)
1108 }
1109}
1110
1111#[derive(Debug, Clone)]
1117pub struct RevolutionSurface {
1118 origin: Point3,
1119 axis: Vec3,
1120 x_axis: Vec3,
1121 y_axis: Vec3,
1122 generatrix_radii: Vec<f64>,
1124 generatrix_heights: Vec<f64>,
1125}
1126
1127impl RevolutionSurface {
1128 pub fn new(
1135 origin: Point3,
1136 axis: Vec3,
1137 radii: Vec<f64>,
1138 heights: Vec<f64>,
1139 ) -> Result<Self, MathError> {
1140 if radii.is_empty() || heights.is_empty() {
1141 return Err(MathError::EmptyInput);
1142 }
1143 if radii.len() != heights.len() {
1144 return Err(MathError::InvalidWeights {
1145 expected: radii.len(),
1146 got: heights.len(),
1147 });
1148 }
1149 let f = Frame3::from_normal(origin, axis)?;
1150 Ok(Self {
1151 origin,
1152 axis: f.z,
1153 x_axis: f.x,
1154 y_axis: f.y,
1155 generatrix_radii: radii,
1156 generatrix_heights: heights,
1157 })
1158 }
1159
1160 #[must_use]
1163 #[allow(
1164 clippy::cast_precision_loss,
1165 clippy::cast_possible_truncation,
1166 clippy::cast_sign_loss
1167 )]
1168 pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
1169 let num_pts = self.generatrix_radii.len();
1170 let param = v.clamp(0.0, 1.0) * (num_pts - 1) as f64;
1171 let idx = (param as usize).min(num_pts - 2);
1172 let frac = param - idx as f64;
1173
1174 let r = frac.mul_add(
1175 self.generatrix_radii[idx + 1] - self.generatrix_radii[idx],
1176 self.generatrix_radii[idx],
1177 );
1178 let height = frac.mul_add(
1179 self.generatrix_heights[idx + 1] - self.generatrix_heights[idx],
1180 self.generatrix_heights[idx],
1181 );
1182
1183 let (sin_u, cos_u) = u.sin_cos();
1184 self.origin + self.x_axis * (r * cos_u) + self.y_axis * (r * sin_u) + self.axis * height
1185 }
1186
1187 #[must_use]
1189 pub const fn origin(&self) -> Point3 {
1190 self.origin
1191 }
1192
1193 #[must_use]
1195 pub const fn axis(&self) -> Vec3 {
1196 self.axis
1197 }
1198}
1199
1200fn analytic_to_nurbs_sampled(
1211 surface_fn: impl Fn(f64, f64) -> Point3,
1212 u_range: (f64, f64),
1213 v_range: (f64, f64),
1214) -> Result<NurbsSurface, MathError> {
1215 let nu = 33;
1219 let nv = 9;
1220
1221 let mut cps = Vec::with_capacity(nu);
1222 let mut weights = Vec::with_capacity(nu);
1223
1224 #[allow(clippy::cast_precision_loss)]
1225 for iu in 0..nu {
1226 let u = u_range.0 + (u_range.1 - u_range.0) * (iu as f64 / (nu - 1) as f64);
1227 let mut row = Vec::with_capacity(nv);
1228 let mut w_row = Vec::with_capacity(nv);
1229 for iv in 0..nv {
1230 let v = v_range.0 + (v_range.1 - v_range.0) * (iv as f64 / (nv - 1) as f64);
1231 row.push(surface_fn(u, v));
1232 w_row.push(1.0);
1233 }
1234 cps.push(row);
1235 weights.push(w_row);
1236 }
1237
1238 let knots_u = uniform_clamped_knots(nu, 1);
1241 let knots_v = uniform_clamped_knots(nv, 1);
1242
1243 NurbsSurface::new(1, 1, knots_u, knots_v, cps, weights)
1244}
1245
1246#[allow(clippy::cast_precision_loss)]
1251fn uniform_clamped_knots(n: usize, degree: usize) -> Vec<f64> {
1252 let mut k = vec![0.0; degree + 1];
1253 for i in 1..n - degree {
1254 k.push(i as f64 / (n - degree) as f64);
1255 }
1256 k.extend(vec![1.0; degree + 1]);
1257 k
1258}
1259
1260#[cfg(test)]
1261#[allow(clippy::unwrap_used, clippy::expect_used)]
1262mod tests;