1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
18use ogeom_math::{
19 Axis, Cone, ControlGrid, Cylinder, Direction, KnotVector, Plane, Point, Sphere, Torus,
20 Transform, Vector, Weighted, bspline, elementary,
21};
22
23use crate::curve::{BSplineCurve, Curve};
24use crate::traits::{Continuity, Curve3d, Surface, SurfaceKind, Transformable};
25
26pub const SURFACE_EXTENT: f64 = 1.0e9;
32
33const TAU: f64 = core::f64::consts::TAU;
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum SurfaceGeometry {
38 Plane(PlaneSurface),
40 Cylinder(CylinderSurface),
42 Cone(ConeSurface),
44 Sphere(SphereSurface),
46 Torus(TorusSurface),
48 BSpline(BSplineSurface),
50 Revolution(Box<RevolutionSurface>),
52 Extrusion(Box<ExtrusionSurface>),
54 Trimmed(Box<TrimmedSurface>),
56 Offset(Box<OffsetSurface>),
58}
59
60#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct PlaneSurface {
63 plane: Plane,
64 domain: ((f64, f64), (f64, f64)),
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct CylinderSurface {
70 cylinder: Cylinder,
71 height: (f64, f64),
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct ConeSurface {
77 cone: Cone,
78 height: (f64, f64),
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct SphereSurface {
84 sphere: Sphere,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq)]
89pub struct TorusSurface {
90 torus: Torus,
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct BSplineSurface {
96 u_knots: KnotVector,
97 v_knots: KnotVector,
98 grid: ControlGrid<Weighted<Point>>,
99 rational: bool,
100 closed: (bool, bool),
106}
107
108#[derive(Debug, Clone, PartialEq)]
112pub struct RevolutionSurface {
113 curve: Curve,
114 axis: Axis,
115 angle: (f64, f64),
116}
117
118#[derive(Debug, Clone, PartialEq)]
122pub struct ExtrusionSurface {
123 curve: Curve,
124 direction: Direction,
125 extent: (f64, f64),
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct TrimmedSurface {
131 basis: SurfaceGeometry,
132 domain: ((f64, f64), (f64, f64)),
133}
134
135#[derive(Debug, Clone, PartialEq)]
146pub struct OffsetSurface {
147 basis: SurfaceGeometry,
148 distance: f64,
149}
150
151fn check_range(name: &str, lo: f64, hi: f64) -> OgeomResult<()> {
153 if !lo.is_finite() || !hi.is_finite() || hi <= lo {
154 ogeom_bail!(
155 Construction,
156 "{name} range [{lo}, {hi}] is empty or non-finite"
157 );
158 }
159 Ok(())
160}
161
162impl PlaneSurface {
163 #[must_use]
165 pub const fn new(plane: Plane) -> Self {
166 Self {
167 plane,
168 domain: (
169 (-SURFACE_EXTENT, SURFACE_EXTENT),
170 (-SURFACE_EXTENT, SURFACE_EXTENT),
171 ),
172 }
173 }
174
175 pub fn over(plane: Plane, u: (f64, f64), v: (f64, f64)) -> OgeomResult<Self> {
182 check_range("u", u.0, u.1)?;
183 check_range("v", v.0, v.1)?;
184 Ok(Self {
185 plane,
186 domain: (u, v),
187 })
188 }
189
190 #[must_use]
192 pub const fn plane(&self) -> Plane {
193 self.plane
194 }
195}
196
197impl CylinderSurface {
198 pub fn new(cylinder: Cylinder, height: (f64, f64)) -> OgeomResult<Self> {
205 check_range("height", height.0, height.1)?;
206 Ok(Self { cylinder, height })
207 }
208
209 #[must_use]
211 pub const fn cylinder(&self) -> Cylinder {
212 self.cylinder
213 }
214}
215
216impl ConeSurface {
217 pub fn new(cone: Cone, height: (f64, f64)) -> OgeomResult<Self> {
224 check_range("height", height.0, height.1)?;
225 Ok(Self { cone, height })
226 }
227
228 #[must_use]
230 pub const fn cone(&self) -> Cone {
231 self.cone
232 }
233
234 #[must_use]
236 pub fn apex_height(&self) -> f64 {
237 -self.cone.reference_radius() / self.cone.half_angle().tan()
238 }
239}
240
241impl SphereSurface {
242 #[must_use]
244 pub const fn new(sphere: Sphere) -> Self {
245 Self { sphere }
246 }
247
248 #[must_use]
250 pub const fn sphere(&self) -> Sphere {
251 self.sphere
252 }
253}
254
255impl TorusSurface {
256 #[must_use]
258 pub const fn new(torus: Torus) -> Self {
259 Self { torus }
260 }
261
262 #[must_use]
264 pub const fn torus(&self) -> Torus {
265 self.torus
266 }
267}
268
269impl BSplineSurface {
270 pub fn new(
276 u_knots: KnotVector,
277 v_knots: KnotVector,
278 grid: &ControlGrid<Point>,
279 tol: Tolerances,
280 ) -> OgeomResult<Self> {
281 let weighted = ControlGrid::new(
282 grid.points()
283 .iter()
284 .map(|p| Weighted::new(*p, 1.0, tol))
285 .collect::<OgeomResult<Vec<_>>>()?,
286 grid.u_count(),
287 grid.v_count(),
288 )?;
289 Self::rational(u_knots, v_knots, weighted)
290 }
291
292 pub fn rational(
298 u_knots: KnotVector,
299 v_knots: KnotVector,
300 grid: ControlGrid<Weighted<Point>>,
301 ) -> OgeomResult<Self> {
302 if grid.u_count() != u_knots.control_point_count()
303 || grid.v_count() != v_knots.control_point_count()
304 {
305 ogeom_bail!(
306 Dimension,
307 "knot vectors describe a {}x{} grid, got {}x{}",
308 u_knots.control_point_count(),
309 v_knots.control_point_count(),
310 grid.u_count(),
311 grid.v_count()
312 );
313 }
314 let first = grid.points()[0].weight;
315 let rational = grid
316 .points()
317 .iter()
318 .any(|w| (w.weight - first).abs() > 1e-12 * first.abs());
319 let closed = (
320 Self::net_closed_u(&grid, Tolerances::millimetres()),
321 Self::net_closed_v(&grid, Tolerances::millimetres()),
322 );
323 Ok(Self {
324 u_knots,
325 v_knots,
326 grid,
327 rational,
328 closed,
329 })
330 }
331
332 fn net_closed_u(grid: &ControlGrid<Weighted<Point>>, tol: Tolerances) -> bool {
334 let last = grid.u_count() - 1;
335 (0..grid.v_count()).all(|j| match (grid.get(0, j), grid.get(last, j)) {
336 (Some(a), Some(b)) => a.point().is_equal(b.point(), tol),
337 _ => false,
338 })
339 }
340
341 fn net_closed_v(grid: &ControlGrid<Weighted<Point>>, tol: Tolerances) -> bool {
343 let last = grid.v_count() - 1;
344 (0..grid.u_count()).all(|i| match (grid.get(i, 0), grid.get(i, last)) {
345 (Some(a), Some(b)) => a.point().is_equal(b.point(), tol),
346 _ => false,
347 })
348 }
349
350 pub fn extended(
365 &self,
366 along_u: bool,
367 at_end: bool,
368 length: f64,
369 continuity: usize,
370 tol: Tolerances,
371 ) -> OgeomResult<Self> {
372 if !along_u {
373 let turned = Self::rational(
374 self.v_knots.clone(),
375 self.u_knots.clone(),
376 self.grid.transposed(),
377 )?;
378 let longer = turned.extended(true, at_end, length, continuity, tol)?;
379 return Self::rational(
380 longer.v_knots.clone(),
381 longer.u_knots.clone(),
382 longer.grid.transposed(),
383 );
384 }
385 if !(length > 0.0 && length.is_finite()) {
386 ogeom_bail!(
387 Construction,
388 "an extension needs a positive length; got {length}"
389 );
390 }
391 use crate::traits::Surface as _;
392 let ((ua, ub), (va, vb)) = self.domain();
393 let u = if at_end { ub } else { ua };
394 let mut speed = 0.0;
395 const STATIONS: usize = 9;
396 for k in 0..STATIONS {
397 #[allow(clippy::cast_precision_loss)]
398 let v = va + (vb - va) * (k as f64 + 0.5) / STATIONS as f64;
399 speed += self.d1_at(u, v, tol)?.0.magnitude();
400 }
401 #[allow(clippy::cast_precision_loss)]
402 let speed = speed / STATIONS as f64;
403 if speed <= tol.confusion() {
404 ogeom_bail!(
405 Construction,
406 "the surface stands still along that side; there is no \
407 direction to continue in"
408 );
409 }
410 let span = length / speed;
411 let (nu, nv) = (self.grid.u_count(), self.grid.v_count());
412 let mut columns: Vec<Vec<Weighted<Point>>> = Vec::with_capacity(nv);
413 let mut knots = None;
414 for j in 0..nv {
415 let column: Vec<Weighted<Point>> = (0..nu)
416 .map(|i| self.grid.get(i, j).unwrap_or_else(|| self.grid.points()[0]))
417 .collect();
418 let (longer_knots, longer) =
419 ogeom_math::bspline::extend(&self.u_knots, &column, at_end, span, continuity, tol)?;
420 knots.get_or_insert(longer_knots);
421 columns.push(longer);
422 }
423 let Some(u_knots) = knots else {
424 ogeom_bail!(Construction, "a patch with no columns cannot be continued");
425 };
426 let longer_nu = columns[0].len();
427 let mut points = Vec::with_capacity(longer_nu * nv);
428 for i in 0..longer_nu {
429 for column in &columns {
430 points.push(column[i]);
431 }
432 }
433 Self::rational(
434 u_knots,
435 self.v_knots.clone(),
436 ControlGrid::new(points, longer_nu, nv)?,
437 )
438 }
439
440 pub fn iso_u_curve(&self, at: f64, tol: Tolerances) -> OgeomResult<BSplineCurve> {
449 use ogeom_math::Blend as _;
450 let span = self.u_knots.span(at, tol)?;
451 let basis = self.u_knots.basis(span, at);
452 let p = self.u_knots.degree();
453 let (k, l) = (self.grid.u_count(), self.grid.v_count());
454 let mut control: Vec<Weighted<Point>> = Vec::with_capacity(l);
455 for j in 0..l {
456 let mut acc = Weighted::<Point>::zero();
457 for (b, i) in basis.iter().zip(span - p..=span) {
458 if let Some(w) = self.grid.get(i.min(k - 1), j) {
459 acc = acc.add(w.scale(*b));
460 }
461 }
462 control.push(acc);
463 }
464 BSplineCurve::rational(self.v_knots.clone(), control)
465 }
466
467 pub fn iso_v_curve(&self, at: f64, tol: Tolerances) -> OgeomResult<BSplineCurve> {
474 use ogeom_math::Blend as _;
475 let span = self.v_knots.span(at, tol)?;
476 let basis = self.v_knots.basis(span, at);
477 let q = self.v_knots.degree();
478 let (k, l) = (self.grid.u_count(), self.grid.v_count());
479 let mut control: Vec<Weighted<Point>> = Vec::with_capacity(k);
480 for i in 0..k {
481 let mut acc = Weighted::<Point>::zero();
482 for (b, j) in basis.iter().zip(span - q..=span) {
483 if let Some(w) = self.grid.get(i, j.min(l - 1)) {
484 acc = acc.add(w.scale(*b));
485 }
486 }
487 control.push(acc);
488 }
489 BSplineCurve::rational(self.u_knots.clone(), control)
490 }
491
492 pub fn segment(&self, u: (f64, f64), v: (f64, f64), tol: Tolerances) -> OgeomResult<Self> {
500 let (nu, nv) = (self.grid.u_count(), self.grid.v_count());
501 let points = self.grid.points();
502 let v_piece = |row: Vec<Weighted<Point>>| -> OgeomResult<BSplineCurve> {
504 BSplineCurve::rational(self.v_knots.clone(), row)?.segment(v, tol)
505 };
506 let mut rows = Vec::with_capacity(nu);
507 let mut v_knots = self.v_knots.clone();
508 for i in 0..nu {
509 let piece = v_piece(points[i * nv..(i + 1) * nv].to_vec())?;
510 v_knots = piece.knots().clone();
511 rows.push(piece.control_points().to_vec());
512 }
513 let mv = v_knots.control_point_count();
514 let mut columns = Vec::with_capacity(mv);
515 let mut u_knots = self.u_knots.clone();
516 for j in 0..mv {
517 let column: Vec<Weighted<Point>> = rows.iter().map(|r| r[j]).collect();
518 let piece = BSplineCurve::rational(self.u_knots.clone(), column)?.segment(u, tol)?;
519 u_knots = piece.knots().clone();
520 columns.push(piece.control_points().to_vec());
521 }
522 let mu = u_knots.control_point_count();
523 let net: Vec<Weighted<Point>> = (0..mu)
524 .flat_map(|i| columns.iter().map(move |c| c[i]))
525 .collect();
526 Self::rational(u_knots, v_knots, ControlGrid::new(net, mu, mv)?)
527 }
528
529 #[must_use]
531 pub const fn u_knots(&self) -> &KnotVector {
532 &self.u_knots
533 }
534
535 #[must_use]
537 pub const fn v_knots(&self) -> &KnotVector {
538 &self.v_knots
539 }
540
541 #[must_use]
543 pub const fn grid(&self) -> &ControlGrid<Weighted<Point>> {
544 &self.grid
545 }
546
547 #[must_use]
549 pub const fn is_rational(&self) -> bool {
550 self.rational
551 }
552}
553
554impl RevolutionSurface {
555 pub fn new(curve: Curve, axis: Axis, angle: f64) -> OgeomResult<Self> {
562 if !angle.is_finite() || angle <= 0.0 || angle > TAU + 1e-12 {
563 ogeom_bail!(
564 Construction,
565 "revolution angle {angle} must be in (0, 2*pi]"
566 );
567 }
568 Ok(Self {
569 curve,
570 axis,
571 angle: (0.0, angle.min(TAU)),
572 })
573 }
574
575 #[must_use]
577 pub const fn curve(&self) -> &Curve {
578 &self.curve
579 }
580
581 #[must_use]
583 pub const fn axis(&self) -> Axis {
584 self.axis
585 }
586}
587
588impl ExtrusionSurface {
589 pub fn new(curve: Curve, direction: Direction, distance: f64) -> OgeomResult<Self> {
596 if !distance.is_finite() || distance <= 0.0 {
597 ogeom_bail!(
598 Construction,
599 "extrusion distance {distance} must be positive"
600 );
601 }
602 Ok(Self {
603 curve,
604 direction,
605 extent: (0.0, distance),
606 })
607 }
608
609 pub fn over(curve: Curve, direction: Direction, extent: (f64, f64)) -> OgeomResult<Self> {
618 if !extent.0.is_finite() || !extent.1.is_finite() || extent.1 <= extent.0 {
619 ogeom_bail!(
620 Construction,
621 "extrusion window {extent:?} must be finite and increasing"
622 );
623 }
624 Ok(Self {
625 curve,
626 direction,
627 extent,
628 })
629 }
630
631 #[must_use]
633 pub const fn curve(&self) -> &Curve {
634 &self.curve
635 }
636
637 #[must_use]
639 pub const fn direction(&self) -> Direction {
640 self.direction
641 }
642}
643
644impl TrimmedSurface {
645 pub fn new(
652 basis: SurfaceGeometry,
653 u: (f64, f64),
654 v: (f64, f64),
655 tol: Tolerances,
656 ) -> OgeomResult<Self> {
657 check_range("u", u.0, u.1)?;
658 check_range("v", v.0, v.1)?;
659 let ((ua, ub), (va, vb)) = basis.domain();
660 let eps = tol.parametric();
661 if !basis.is_periodic_u() && (u.0 < ua - eps || u.1 > ub + eps) {
662 ogeom_bail!(Domain, "u range [{}, {}] leaves [{ua}, {ub}]", u.0, u.1);
663 }
664 if !basis.is_periodic_v() && (v.0 < va - eps || v.1 > vb + eps) {
665 ogeom_bail!(Domain, "v range [{}, {}] leaves [{va}, {vb}]", v.0, v.1);
666 }
667 Ok(Self {
668 basis,
669 domain: (u, v),
670 })
671 }
672
673 #[must_use]
675 pub const fn basis(&self) -> &SurfaceGeometry {
676 &self.basis
677 }
678}
679
680impl OffsetSurface {
681 pub fn new(basis: SurfaceGeometry, distance: f64) -> OgeomResult<Self> {
688 if !distance.is_finite() || distance == 0.0 {
689 ogeom_bail!(
690 Construction,
691 "an offset of {distance} is not a displacement"
692 );
693 }
694 Ok(Self { basis, distance })
695 }
696
697 #[must_use]
699 pub const fn basis(&self) -> &SurfaceGeometry {
700 &self.basis
701 }
702
703 #[must_use]
705 pub const fn distance(&self) -> f64 {
706 self.distance
707 }
708
709 pub fn analytic(&self, tol: Tolerances) -> OgeomResult<Option<SurfaceGeometry>> {
721 let ((u0, u1), (v0, v1)) = self.basis.domain();
722 let clamp = |lo: f64, hi: f64| {
723 if lo.is_finite() && hi.is_finite() {
724 f64::midpoint(lo, hi)
725 } else {
726 0.0
727 }
728 };
729 let (u, v) = (clamp(u0, u1), clamp(v0, v1));
730 let at = self.basis.point_at(u, v, tol)?;
731 let normal = self.basis.normal_at(u, v, tol)?.vector();
732 let d = self.distance;
733 let outward = |from_axis: Vector| normal.dot(from_axis) > 0.0;
736 Ok(match &self.basis {
737 SurfaceGeometry::Plane(p) => {
738 let plane = p.plane();
739 let frame = plane.frame();
740 let moved =
741 ogeom_math::Frame::new(frame.origin() + normal * d, frame.z(), frame.x(), tol)?;
742 Some(PlaneSurface::over(ogeom_math::Plane::new(moved), (u0, u1), (v0, v1))?.into())
743 }
744 SurfaceGeometry::Cylinder(c) => {
745 let cylinder = c.cylinder();
746 let frame = cylinder.frame();
747 let radial = at - frame.origin();
748 let radial = radial - frame.z().vector() * radial.dot(frame.z().vector());
749 let radius = cylinder.radius() + if outward(radial) { d } else { -d };
750 if radius <= tol.confusion() {
751 return Ok(None);
752 }
753 Some(
754 CylinderSurface::new(ogeom_math::Cylinder::new(frame, radius, tol)?, (v0, v1))?
755 .into(),
756 )
757 }
758 SurfaceGeometry::Cone(c) => {
759 let cone = c.cone();
760 let frame = cone.frame();
761 let radial = at - frame.origin();
762 let radial = radial - frame.z().vector() * radial.dot(frame.z().vector());
763 let grow = if outward(radial) { d } else { -d };
764 let radius = cone.reference_radius() + grow / cone.half_angle().cos();
765 if radius <= tol.confusion() {
766 return Ok(None);
767 }
768 Some(
769 ConeSurface::new(
770 ogeom_math::Cone::new(frame, radius, cone.half_angle(), tol)?,
771 (v0, v1),
772 )?
773 .into(),
774 )
775 }
776 SurfaceGeometry::Sphere(s) => {
777 let sphere = s.sphere();
778 let grow = if outward(at - sphere.frame().origin()) {
779 d
780 } else {
781 -d
782 };
783 let radius = sphere.radius() + grow;
784 if radius <= tol.confusion() {
785 return Ok(None);
786 }
787 Some(
788 SphereSurface::new(ogeom_math::Sphere::new(sphere.frame(), radius, tol)?)
789 .into(),
790 )
791 }
792 SurfaceGeometry::Torus(t) => {
793 let torus = t.torus();
794 let frame = torus.frame();
795 let flat = at - frame.origin();
797 let flat = flat - frame.z().vector() * flat.dot(frame.z().vector());
798 let ring = frame.origin() + flat * (torus.major_radius() / flat.magnitude());
799 let grow = if outward(at - ring) { d } else { -d };
800 let minor = torus.minor_radius() + grow;
801 if minor <= tol.confusion() {
802 return Ok(None);
803 }
804 Some(
805 TorusSurface::new(ogeom_math::Torus::new(
806 frame,
807 torus.major_radius(),
808 minor,
809 tol,
810 )?)
811 .into(),
812 )
813 }
814 _ => None,
815 })
816 }
817}
818
819impl Surface for OffsetSurface {
820 fn domain(&self) -> ((f64, f64), (f64, f64)) {
821 self.basis.domain()
822 }
823
824 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
825 let base = self.basis.point_at(u, v, tol)?;
826 let normal = self.basis.normal_at(u, v, tol)?;
827 Ok(base + normal.vector() * self.distance)
828 }
829
830 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
831 let (su, sv) = self.basis.d1_at(u, v, tol)?;
836 let (suu, suv, svv) = self.basis.d2_at(u, v, tol)?;
837 let c = su.cross(sv);
838 let m = c.magnitude();
839 let scale = su.magnitude().max(sv.magnitude());
840 if m <= tol.angular() * scale * scale {
841 ogeom_bail!(
842 Construction,
843 "the basis is degenerate at ({u}, {v}); the offset has no tangent plane there"
844 );
845 }
846 let n = c / m;
847 let cu = suu.cross(sv) + su.cross(suv);
848 let cv = suv.cross(sv) + su.cross(svv);
849 let nu = (cu - n * n.dot(cu)) / m;
850 let nv = (cv - n * n.dot(cv)) / m;
851 Ok((su + nu * self.distance, sv + nv * self.distance))
852 }
853
854 fn d2_at(&self, _u: f64, _v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
855 ogeom_bail!(
856 Construction,
857 "an offset surface's second derivative needs its basis's third, which the vocabulary does not carry; offset the basis analytically or fit at a stated tolerance instead"
858 )
859 }
860
861 fn kind(&self) -> SurfaceKind {
862 SurfaceKind::Offset
863 }
864
865 fn continuity(&self) -> Continuity {
866 match self.basis.continuity() {
868 Continuity::CInfinity => Continuity::CInfinity,
869 Continuity::C2 | Continuity::G2 => Continuity::C1,
870 Continuity::C1 | Continuity::G1 | Continuity::C0 => Continuity::C0,
871 }
872 }
873
874 fn is_closed_u(&self, tol: Tolerances) -> bool {
875 self.basis.is_closed_u(tol)
876 }
877
878 fn is_closed_v(&self, tol: Tolerances) -> bool {
879 self.basis.is_closed_v(tol)
880 }
881
882 fn is_periodic_u(&self) -> bool {
883 self.basis.is_periodic_u()
884 }
885
886 fn is_periodic_v(&self) -> bool {
887 self.basis.is_periodic_v()
888 }
889}
890
891fn finite_parameters(u: f64, v: f64) -> OgeomResult<()> {
892 if u.is_finite() && v.is_finite() {
893 Ok(())
894 } else {
895 ogeom_bail!(Domain, "parameters ({u}, {v}) are not finite")
896 }
897}
898
899impl Surface for PlaneSurface {
900 fn domain(&self) -> ((f64, f64), (f64, f64)) {
901 self.domain
902 }
903
904 fn point_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<Point> {
908 finite_parameters(u, v)?;
909 Ok(elementary::plane_at(&self.plane, u, v).point)
910 }
911
912 fn d1_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
913 finite_parameters(u, v)?;
914 let f = self.plane.frame();
915 Ok((f.x().vector(), f.y().vector()))
916 }
917
918 fn d2_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
919 finite_parameters(u, v)?;
920 Ok((Vector::ZERO, Vector::ZERO, Vector::ZERO))
921 }
922
923 fn kind(&self) -> SurfaceKind {
924 SurfaceKind::Plane
925 }
926
927 fn continuity(&self) -> Continuity {
928 Continuity::CInfinity
929 }
930
931 fn is_closed_u(&self, _tol: Tolerances) -> bool {
932 false
933 }
934
935 fn is_closed_v(&self, _tol: Tolerances) -> bool {
936 false
937 }
938
939 fn is_periodic_u(&self) -> bool {
940 false
941 }
942
943 fn is_periodic_v(&self) -> bool {
944 false
945 }
946}
947
948impl Surface for CylinderSurface {
949 fn domain(&self) -> ((f64, f64), (f64, f64)) {
950 ((0.0, TAU), self.height)
951 }
952
953 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
954 let (u, v) = self.normalize_parameters(u, v, tol)?;
955 Ok(elementary::cylinder_at(&self.cylinder, u, v).point)
956 }
957
958 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
959 let (u, v) = self.normalize_parameters(u, v, tol)?;
960 let p = elementary::cylinder_at(&self.cylinder, u, v);
961 Ok((p.du, p.dv))
962 }
963
964 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
965 let (u, _) = self.normalize_parameters(u, v, tol)?;
966 let f = self.cylinder.frame();
967 let r = self.cylinder.radius();
968 let (sin, cos) = u.sin_cos();
969 Ok((
972 f.x() * (-r * cos) + f.y() * (-r * sin),
973 Vector::ZERO,
974 Vector::ZERO,
975 ))
976 }
977
978 fn kind(&self) -> SurfaceKind {
979 SurfaceKind::Cylinder
980 }
981
982 fn continuity(&self) -> Continuity {
983 Continuity::CInfinity
984 }
985
986 fn is_closed_u(&self, _tol: Tolerances) -> bool {
987 true
988 }
989
990 fn is_closed_v(&self, _tol: Tolerances) -> bool {
991 false
992 }
993
994 fn is_periodic_u(&self) -> bool {
995 true
996 }
997
998 fn is_periodic_v(&self) -> bool {
999 false
1000 }
1001}
1002
1003impl Surface for ConeSurface {
1004 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1005 ((0.0, TAU), self.height)
1006 }
1007
1008 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1009 let (u, v) = self.normalize_parameters(u, v, tol)?;
1010 Ok(elementary::cone_at(&self.cone, u, v).point)
1011 }
1012
1013 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1014 let (u, v) = self.normalize_parameters(u, v, tol)?;
1015 let p = elementary::cone_at(&self.cone, u, v);
1016 Ok((p.du, p.dv))
1017 }
1018
1019 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1020 let (u, v) = self.normalize_parameters(u, v, tol)?;
1021 let f = self.cone.frame();
1022 let r = self.cone.radius_at(v);
1023 let slope = self.cone.half_angle().tan();
1024 let (sin, cos) = u.sin_cos();
1025 Ok((
1026 f.x() * (-r * cos) + f.y() * (-r * sin),
1028 f.x() * (-slope * sin) + f.y() * (slope * cos),
1031 Vector::ZERO,
1033 ))
1034 }
1035
1036 fn kind(&self) -> SurfaceKind {
1037 SurfaceKind::Cone
1038 }
1039
1040 fn continuity(&self) -> Continuity {
1041 Continuity::CInfinity
1042 }
1043
1044 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1045 true
1046 }
1047
1048 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1049 false
1050 }
1051
1052 fn is_periodic_u(&self) -> bool {
1053 true
1054 }
1055
1056 fn is_periodic_v(&self) -> bool {
1057 false
1058 }
1059}
1060
1061impl Surface for SphereSurface {
1062 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1063 let half = core::f64::consts::FRAC_PI_2;
1064 ((0.0, TAU), (-half, half))
1065 }
1066
1067 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1068 let (u, v) = self.normalize_parameters(u, v, tol)?;
1069 Ok(elementary::sphere_at(&self.sphere, u, v).point)
1070 }
1071
1072 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1073 let (u, v) = self.normalize_parameters(u, v, tol)?;
1074 let p = elementary::sphere_at(&self.sphere, u, v);
1075 Ok((p.du, p.dv))
1076 }
1077
1078 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1079 let (u, v) = self.normalize_parameters(u, v, tol)?;
1080 let f = self.sphere.frame();
1081 let r = self.sphere.radius();
1082 let (sin_u, cos_u) = u.sin_cos();
1083 let (sin_v, cos_v) = v.sin_cos();
1084 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
1085 let ring = r * cos_v;
1086 Ok((
1087 x * (-ring * cos_u) + y * (-ring * sin_u),
1088 x * (r * sin_v * sin_u) + y * (-r * sin_v * cos_u),
1089 x * (-ring * cos_u) + y * (-ring * sin_u) + z * (-r * sin_v),
1090 ))
1091 }
1092
1093 fn kind(&self) -> SurfaceKind {
1094 SurfaceKind::Sphere
1095 }
1096
1097 fn continuity(&self) -> Continuity {
1098 Continuity::CInfinity
1099 }
1100
1101 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1102 true
1103 }
1104
1105 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1106 false
1107 }
1108
1109 fn is_periodic_u(&self) -> bool {
1110 true
1111 }
1112
1113 fn is_periodic_v(&self) -> bool {
1114 false
1118 }
1119}
1120
1121impl Surface for TorusSurface {
1122 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1123 ((0.0, TAU), (0.0, TAU))
1124 }
1125
1126 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1127 let (u, v) = self.normalize_parameters(u, v, tol)?;
1128 Ok(elementary::torus_at(&self.torus, u, v).point)
1129 }
1130
1131 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1132 let (u, v) = self.normalize_parameters(u, v, tol)?;
1133 let p = elementary::torus_at(&self.torus, u, v);
1134 Ok((p.du, p.dv))
1135 }
1136
1137 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1138 let (u, v) = self.normalize_parameters(u, v, tol)?;
1139 let f = self.torus.frame();
1140 let (major, minor) = (self.torus.major_radius(), self.torus.minor_radius());
1141 let (sin_u, cos_u) = u.sin_cos();
1142 let (sin_v, cos_v) = v.sin_cos();
1143 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
1144 let out = x * cos_u + y * sin_u;
1145 let side = x * -sin_u + y * cos_u;
1146 let radius = minor.mul_add(cos_v, major);
1147 Ok((
1148 out * -radius,
1149 side * (-minor * sin_v),
1150 out * (-minor * cos_v) + z * (-minor * sin_v),
1151 ))
1152 }
1153
1154 fn kind(&self) -> SurfaceKind {
1155 SurfaceKind::Torus
1156 }
1157
1158 fn continuity(&self) -> Continuity {
1159 Continuity::CInfinity
1160 }
1161
1162 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1163 true
1164 }
1165
1166 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1167 true
1168 }
1169
1170 fn is_periodic_u(&self) -> bool {
1171 true
1172 }
1173
1174 fn is_periodic_v(&self) -> bool {
1175 true
1176 }
1177}
1178
1179impl Surface for BSplineSurface {
1180 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1181 (self.u_knots.domain(), self.v_knots.domain())
1182 }
1183
1184 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1185 let (u, v) = self.normalize_parameters(u, v, tol)?;
1186 if self.rational {
1187 bspline::evaluate_rational_surface(&self.u_knots, &self.v_knots, &self.grid, u, v, tol)
1188 } else {
1189 Ok(
1190 bspline::evaluate_surface(&self.u_knots, &self.v_knots, &self.grid, u, v, tol)?
1191 .point(),
1192 )
1193 }
1194 }
1195
1196 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1197 let (u, v) = self.normalize_parameters(u, v, tol)?;
1198 let d = bspline::rational_surface_derivatives(
1199 &self.u_knots,
1200 &self.v_knots,
1201 &self.grid,
1202 u,
1203 v,
1204 1,
1205 tol,
1206 )?;
1207 Ok((d[1][0].to_vector(), d[0][1].to_vector()))
1208 }
1209
1210 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1211 let (u, v) = self.normalize_parameters(u, v, tol)?;
1212 let d = bspline::rational_surface_derivatives(
1213 &self.u_knots,
1214 &self.v_knots,
1215 &self.grid,
1216 u,
1217 v,
1218 2,
1219 tol,
1220 )?;
1221 Ok((
1222 d[2][0].to_vector(),
1223 d[1][1].to_vector(),
1224 d[0][2].to_vector(),
1225 ))
1226 }
1227
1228 fn jet_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<crate::SurfaceJet> {
1229 let (u, v) = self.normalize_parameters(u, v, tol)?;
1233 let d = if self.rational {
1234 bspline::rational_surface_derivatives(
1235 &self.u_knots,
1236 &self.v_knots,
1237 &self.grid,
1238 u,
1239 v,
1240 2,
1241 tol,
1242 )?
1243 } else {
1244 bspline::surface_derivatives(&self.u_knots, &self.v_knots, &self.grid, u, v, 2, tol)?
1245 .into_iter()
1246 .map(|row| row.into_iter().map(|w| w.scaled).collect())
1247 .collect()
1248 };
1249 Ok(crate::SurfaceJet {
1250 point: Point::ORIGIN + d[0][0].to_vector(),
1251 du: d[1][0].to_vector(),
1252 dv: d[0][1].to_vector(),
1253 d2u: d[2][0].to_vector(),
1254 duv: d[1][1].to_vector(),
1255 d2v: d[0][2].to_vector(),
1256 })
1257 }
1258
1259 fn kind(&self) -> SurfaceKind {
1260 SurfaceKind::BSpline
1261 }
1262
1263 fn continuity(&self) -> Continuity {
1264 let worst = |k: &KnotVector| {
1266 let (a, b) = k.domain();
1267 k.distinct()
1268 .into_iter()
1269 .filter(|(x, _)| *x > a && *x < b)
1270 .map(|(_, m)| m)
1271 .max()
1272 .map_or(Continuity::CInfinity, |m| {
1273 match k.degree().saturating_sub(m) {
1274 0 => Continuity::C0,
1275 1 => Continuity::C1,
1276 _ => Continuity::C2,
1277 }
1278 })
1279 };
1280 worst(&self.u_knots).min(worst(&self.v_knots))
1281 }
1282
1283 fn is_closed_u(&self, tol: Tolerances) -> bool {
1284 self.closed.0 || Self::net_closed_u(&self.grid, tol)
1287 }
1288
1289 fn is_closed_v(&self, tol: Tolerances) -> bool {
1290 self.closed.1 || Self::net_closed_v(&self.grid, tol)
1291 }
1292
1293 fn is_periodic_u(&self) -> bool {
1294 false
1295 }
1296
1297 fn is_periodic_v(&self) -> bool {
1298 false
1299 }
1300}
1301
1302impl Surface for RevolutionSurface {
1303 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1304 (self.angle, self.curve.domain())
1305 }
1306
1307 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1308 let (u, v) = self.normalize_parameters(u, v, tol)?;
1309 let p = self.curve.point_at(v, tol)?;
1310 Ok(Transform::rotation(self.axis, u).apply(p))
1311 }
1312
1313 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1314 let (u, v) = self.normalize_parameters(u, v, tol)?;
1315 let rotate = Transform::rotation(self.axis, u);
1316 let p = rotate.apply(self.curve.point_at(v, tol)?);
1317 let radius = p - self.axis.project(p);
1320 Ok((
1321 self.axis.direction.cross_with(radius),
1322 rotate.apply_vector(self.curve.d1_at(v, tol)?),
1323 ))
1324 }
1325
1326 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1327 let (u, v) = self.normalize_parameters(u, v, tol)?;
1328 let rotate = Transform::rotation(self.axis, u);
1329 let p = rotate.apply(self.curve.point_at(v, tol)?);
1330 let radius = p - self.axis.project(p);
1331 let d = self.curve.derivatives_at(v, 2, tol)?;
1332 let curve_d1 = rotate.apply_vector(d[1]);
1333 let d2u = -radius;
1335 let duv = self.axis.direction.cross_with(curve_d1);
1338 Ok((d2u, duv, rotate.apply_vector(d[2])))
1339 }
1340
1341 fn kind(&self) -> SurfaceKind {
1342 SurfaceKind::Revolution
1343 }
1344
1345 fn continuity(&self) -> Continuity {
1346 self.curve.continuity()
1348 }
1349
1350 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1351 (self.angle.1 - self.angle.0 - TAU).abs() <= 1e-12
1352 }
1353
1354 fn is_closed_v(&self, tol: Tolerances) -> bool {
1355 self.curve.is_closed(tol)
1356 }
1357
1358 fn is_periodic_u(&self) -> bool {
1359 (self.angle.1 - self.angle.0 - TAU).abs() <= 1e-12
1360 }
1361
1362 fn is_periodic_v(&self) -> bool {
1363 self.curve.is_periodic()
1364 }
1365}
1366
1367impl Surface for ExtrusionSurface {
1368 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1369 (self.curve.domain(), self.extent)
1370 }
1371
1372 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1373 let (u, v) = self.normalize_parameters(u, v, tol)?;
1374 Ok(self.curve.point_at(u, tol)? + self.direction * v)
1375 }
1376
1377 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1378 let (u, _) = self.normalize_parameters(u, v, tol)?;
1379 Ok((self.curve.d1_at(u, tol)?, self.direction.vector()))
1380 }
1381
1382 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1383 let (u, _) = self.normalize_parameters(u, v, tol)?;
1384 Ok((
1386 self.curve.derivatives_at(u, 2, tol)?[2],
1387 Vector::ZERO,
1388 Vector::ZERO,
1389 ))
1390 }
1391
1392 fn kind(&self) -> SurfaceKind {
1393 SurfaceKind::Extrusion
1394 }
1395
1396 fn continuity(&self) -> Continuity {
1397 self.curve.continuity()
1398 }
1399
1400 fn is_closed_u(&self, tol: Tolerances) -> bool {
1401 self.curve.is_closed(tol)
1402 }
1403
1404 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1405 false
1406 }
1407
1408 fn is_periodic_u(&self) -> bool {
1409 self.curve.is_periodic()
1410 }
1411
1412 fn is_periodic_v(&self) -> bool {
1413 false
1414 }
1415}
1416
1417impl Surface for TrimmedSurface {
1418 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1419 self.domain
1420 }
1421
1422 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1423 let (u, v) = self.normalize_parameters(u, v, tol)?;
1424 self.basis.point_at(u, v, tol)
1425 }
1426
1427 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1428 let (u, v) = self.normalize_parameters(u, v, tol)?;
1429 self.basis.d1_at(u, v, tol)
1430 }
1431
1432 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1433 let (u, v) = self.normalize_parameters(u, v, tol)?;
1434 self.basis.d2_at(u, v, tol)
1435 }
1436
1437 fn kind(&self) -> SurfaceKind {
1438 SurfaceKind::Trimmed
1439 }
1440
1441 fn continuity(&self) -> Continuity {
1442 self.basis.continuity()
1443 }
1444
1445 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1446 false
1448 }
1449
1450 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1451 false
1452 }
1453
1454 fn is_periodic_u(&self) -> bool {
1455 false
1456 }
1457
1458 fn is_periodic_v(&self) -> bool {
1459 false
1460 }
1461}
1462
1463macro_rules! dispatch {
1465 ($self:ident, $s:ident => $body:expr) => {
1466 match $self {
1467 Self::Plane($s) => $body,
1468 Self::Cylinder($s) => $body,
1469 Self::Cone($s) => $body,
1470 Self::Sphere($s) => $body,
1471 Self::Torus($s) => $body,
1472 Self::BSpline($s) => $body,
1473 Self::Revolution($s) => $body,
1474 Self::Extrusion($s) => $body,
1475 Self::Trimmed($s) => $body,
1476 Self::Offset($s) => $body,
1477 }
1478 };
1479}
1480
1481impl Surface for SurfaceGeometry {
1482 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1483 dispatch!(self, s => s.domain())
1484 }
1485
1486 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1487 dispatch!(self, s => s.point_at(u, v, tol))
1488 }
1489
1490 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1491 dispatch!(self, s => s.d1_at(u, v, tol))
1492 }
1493
1494 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1495 dispatch!(self, s => s.d2_at(u, v, tol))
1496 }
1497
1498 fn jet_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<crate::SurfaceJet> {
1503 dispatch!(self, s => s.jet_at(u, v, tol))
1504 }
1505
1506 fn kind(&self) -> SurfaceKind {
1507 dispatch!(self, s => s.kind())
1508 }
1509
1510 fn continuity(&self) -> Continuity {
1511 dispatch!(self, s => s.continuity())
1512 }
1513
1514 fn is_closed_u(&self, tol: Tolerances) -> bool {
1515 dispatch!(self, s => s.is_closed_u(tol))
1516 }
1517
1518 fn is_closed_v(&self, tol: Tolerances) -> bool {
1519 dispatch!(self, s => s.is_closed_v(tol))
1520 }
1521
1522 fn is_periodic_u(&self) -> bool {
1523 dispatch!(self, s => s.is_periodic_u())
1524 }
1525
1526 fn is_periodic_v(&self) -> bool {
1527 dispatch!(self, s => s.is_periodic_v())
1528 }
1529}
1530
1531impl Transformable for SurfaceGeometry {
1532 fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
1533 let scale = t.scale_factor().abs();
1534 Ok(match self {
1535 Self::Plane(s) => Self::Plane(PlaneSurface {
1536 plane: s.plane.transformed(t, tol)?,
1537 domain: (
1540 (s.domain.0.0 * scale, s.domain.0.1 * scale),
1541 (s.domain.1.0 * scale, s.domain.1.1 * scale),
1542 ),
1543 }),
1544 Self::Cylinder(s) => Self::Cylinder(CylinderSurface {
1545 cylinder: s.cylinder.transformed(t, tol)?,
1546 height: (s.height.0 * scale, s.height.1 * scale),
1547 }),
1548 Self::Offset(s) => Self::Offset(Box::new(OffsetSurface {
1549 basis: s.basis.transformed(t, tol)?,
1550 distance: s.distance * scale,
1551 })),
1552 Self::Cone(s) => Self::Cone(ConeSurface {
1553 cone: s.cone.transformed(t, tol)?,
1554 height: (s.height.0 * scale, s.height.1 * scale),
1555 }),
1556 Self::Sphere(s) => Self::Sphere(SphereSurface {
1557 sphere: s.sphere.transformed(t, tol)?,
1558 }),
1559 Self::Torus(s) => Self::Torus(TorusSurface {
1560 torus: s.torus.transformed(t, tol)?,
1561 }),
1562 Self::BSpline(s) => {
1563 let grid = ControlGrid::new(
1564 s.grid
1565 .points()
1566 .iter()
1567 .map(|w| Weighted::new(t.apply(w.point()), w.weight, tol))
1568 .collect::<OgeomResult<Vec<_>>>()?,
1569 s.grid.u_count(),
1570 s.grid.v_count(),
1571 )?;
1572 Self::BSpline(BSplineSurface { grid, ..s.clone() })
1573 }
1574 Self::Revolution(s) => Self::Revolution(Box::new(RevolutionSurface {
1575 curve: s.curve.transformed(t, tol)?,
1576 axis: Axis::new(
1577 t.apply(s.axis.location),
1578 t.apply_direction(s.axis.direction, tol)?,
1579 ),
1580 angle: s.angle,
1581 })),
1582 Self::Extrusion(s) => Self::Extrusion(Box::new(ExtrusionSurface {
1583 curve: s.curve.transformed(t, tol)?,
1584 direction: t.apply_direction(s.direction, tol)?,
1585 extent: (s.extent.0 * scale, s.extent.1 * scale),
1586 })),
1587 Self::Trimmed(s) => {
1588 let basis = s.basis.transformed(t, tol)?;
1589 let ((oa, _), (ob, _)) = s.basis.domain();
1592 let ((na, _), (nb, _)) = basis.domain();
1593 let ur = if oa == 0.0 { 1.0 } else { na / oa };
1594 let vr = if ob == 0.0 { 1.0 } else { nb / ob };
1595 Self::Trimmed(Box::new(TrimmedSurface {
1596 basis,
1597 domain: (
1598 (s.domain.0.0 * ur, s.domain.0.1 * ur),
1599 (s.domain.1.0 * vr, s.domain.1.1 * vr),
1600 ),
1601 }))
1602 }
1603 })
1604 }
1605}
1606
1607impl From<PlaneSurface> for SurfaceGeometry {
1608 fn from(s: PlaneSurface) -> Self {
1609 Self::Plane(s)
1610 }
1611}
1612impl From<CylinderSurface> for SurfaceGeometry {
1613 fn from(s: CylinderSurface) -> Self {
1614 Self::Cylinder(s)
1615 }
1616}
1617impl From<ConeSurface> for SurfaceGeometry {
1618 fn from(s: ConeSurface) -> Self {
1619 Self::Cone(s)
1620 }
1621}
1622impl From<SphereSurface> for SurfaceGeometry {
1623 fn from(s: SphereSurface) -> Self {
1624 Self::Sphere(s)
1625 }
1626}
1627impl From<TorusSurface> for SurfaceGeometry {
1628 fn from(s: TorusSurface) -> Self {
1629 Self::Torus(s)
1630 }
1631}
1632impl From<BSplineSurface> for SurfaceGeometry {
1633 fn from(s: BSplineSurface) -> Self {
1634 Self::BSpline(s)
1635 }
1636}
1637impl From<RevolutionSurface> for SurfaceGeometry {
1638 fn from(s: RevolutionSurface) -> Self {
1639 Self::Revolution(Box::new(s))
1640 }
1641}
1642impl From<ExtrusionSurface> for SurfaceGeometry {
1643 fn from(s: ExtrusionSurface) -> Self {
1644 Self::Extrusion(Box::new(s))
1645 }
1646}
1647impl From<TrimmedSurface> for SurfaceGeometry {
1648 fn from(s: TrimmedSurface) -> Self {
1649 Self::Trimmed(Box::new(s))
1650 }
1651}
1652
1653#[cfg(test)]
1654#[allow(clippy::unwrap_used)]
1655mod tests {
1656 #[test]
1666 fn a_closed_surface_wraps_a_parameter_past_its_join() {
1667 use crate::Surface as _;
1668 use ogeom_math::{ControlGrid, KnotVector, Point};
1669 let ring = [
1670 Point::new(1.0, 0.0, 0.0),
1671 Point::new(0.0, 1.0, 0.0),
1672 Point::new(-1.0, 0.0, 0.0),
1673 Point::new(0.0, -1.0, 0.0),
1674 Point::new(1.0, 0.0, 0.0),
1675 ];
1676 let mut control = Vec::new();
1677 for p in &ring {
1678 for z in [0.0, 5.0] {
1679 control.push(Point::new(p.x, p.y, z));
1680 }
1681 }
1682 let tube = BSplineSurface::new(
1683 KnotVector::new(vec![0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0], 1).unwrap(),
1684 KnotVector::clamped_uniform(1, 2).unwrap(),
1685 &ControlGrid::new(control, 5, 2).unwrap(),
1686 T,
1687 )
1688 .unwrap();
1689 assert!(
1690 tube.is_closed_u(T) && !tube.is_periodic_u(),
1691 "closed, not periodic"
1692 );
1693 let inside = tube.point_at(0.3, 0.5, T).unwrap();
1694 let past = tube.point_at(1.3, 0.5, T).unwrap();
1695 let before = tube.point_at(-0.7, 0.5, T).unwrap();
1696 assert!(
1697 inside.is_equal(past, T),
1698 "a period past the end: {inside:?} vs {past:?}"
1699 );
1700 assert!(
1701 inside.is_equal(before, T),
1702 "a period before the start: {inside:?} vs {before:?}"
1703 );
1704 assert!(
1706 tube.point_at(0.3, 1.5, T).is_err(),
1707 "an open direction still refuses"
1708 );
1709 }
1710
1711 use super::*;
1712 use crate::curve::{CircleCurve, LineCurve};
1713 use approx::assert_relative_eq;
1714 use ogeom_math::{Circle, Frame};
1715
1716 const T: Tolerances = Tolerances::millimetres();
1717
1718 #[test]
1719 fn a_jet_agrees_with_the_separate_accessors() {
1720 let close = |a: Vector, b: Vector, what: &str| {
1727 let scale = a.magnitude().max(b.magnitude()).max(1.0);
1728 assert!(
1729 (a - b).magnitude() <= scale * 1e-12,
1730 "{what}: {a:?} against {b:?}"
1731 );
1732 };
1733 let surfaces: Vec<SurfaceGeometry> = vec![
1734 SurfaceGeometry::Plane(PlaneSurface::new(ogeom_math::Plane::XY)),
1735 SurfaceGeometry::Sphere(SphereSurface::new(
1736 ogeom_math::Sphere::new(ogeom_math::Frame::WORLD, 3.0, T).unwrap(),
1737 )),
1738 bspline_patch(),
1739 ];
1740 for surface in &surfaces {
1741 let ((ua, ub), (va, vb)) = surface.domain();
1742 for i in 1..5 {
1743 for j in 1..5 {
1744 let u = ua + (ub - ua) * f64::from(i) / 5.0;
1745 let v = va + (vb - va) * f64::from(j) / 5.0;
1746 let jet = surface.jet_at(u, v, T).unwrap();
1747 let point = surface.point_at(u, v, T).unwrap();
1748 close(jet.point - Point::ORIGIN, point - Point::ORIGIN, "point");
1749 let (du, dv) = surface.d1_at(u, v, T).unwrap();
1750 close(jet.du, du, "du");
1751 close(jet.dv, dv, "dv");
1752 let (d2u, duv, d2v) = surface.d2_at(u, v, T).unwrap();
1753 close(jet.d2u, d2u, "d2u");
1754 close(jet.duv, duv, "duv");
1755 close(jet.d2v, d2v, "d2v");
1756 }
1757 }
1758 }
1759 }
1760
1761 fn bspline_patch() -> SurfaceGeometry {
1763 let knots = KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).unwrap();
1764 let mut points = Vec::new();
1765 for i in 0..4 {
1766 for j in 0..4 {
1767 let z = if i == 1 && j == 2 { 2.0 } else { 0.0 };
1768 points.push(Point::new(f64::from(i), f64::from(j), z));
1769 }
1770 }
1771 let grid = ControlGrid::new(points, 4, 4).unwrap();
1772 SurfaceGeometry::BSpline(BSplineSurface::new(knots.clone(), knots, &grid, T).unwrap())
1773 }
1774
1775 #[test]
1776 fn an_offset_cylinder_is_the_larger_cylinder() {
1777 use ogeom_math::Cylinder;
1778 let basis = SurfaceGeometry::Cylinder(
1779 CylinderSurface::new(Cylinder::new(Frame::WORLD, 2.0, T).unwrap(), (0.0, 5.0)).unwrap(),
1780 );
1781 let offset = OffsetSurface::new(basis, 1.5).unwrap();
1782 for (u, v) in [(0.0, 0.0), (1.0, 2.0), (3.5, 4.5)] {
1785 let p = offset.point_at(u, v, T).unwrap();
1786 let radial = (p.x * p.x + p.y * p.y).sqrt();
1787 assert_relative_eq!(radial, 3.5, epsilon = 1e-12);
1788 }
1789 let h = 1e-6;
1791 let (du, dv) = offset.d1_at(1.0, 2.0, T).unwrap();
1792 let fdu = (offset.point_at(1.0 + h, 2.0, T).unwrap()
1793 - offset.point_at(1.0 - h, 2.0, T).unwrap())
1794 / (2.0 * h);
1795 let fdv = (offset.point_at(1.0, 2.0 + h, T).unwrap()
1796 - offset.point_at(1.0, 2.0 - h, T).unwrap())
1797 / (2.0 * h);
1798 assert_relative_eq!((du - fdu).magnitude(), 0.0, epsilon = 1e-5);
1799 assert_relative_eq!((dv - fdv).magnitude(), 0.0, epsilon = 1e-5);
1800 assert!(offset.d2_at(1.0, 2.0, T).is_err());
1802 }
1803
1804 fn tilted() -> Frame {
1805 Frame::new(
1806 Point::new(1.0, -2.0, 3.0),
1807 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
1808 Direction::X,
1809 T,
1810 )
1811 .unwrap()
1812 }
1813
1814 #[test]
1819 fn a_patch_extended_stays_itself_and_continues() {
1820 use crate::Surface as _;
1821 let cylinder = ogeom_math::Cylinder::new(Frame::WORLD, 5.0, T).unwrap();
1822 let wall =
1823 SurfaceGeometry::Cylinder(crate::CylinderSurface::new(cylinder, (0.0, 10.0)).unwrap())
1824 .to_bspline(T)
1825 .unwrap();
1826 let ((ua, ub), (va, vb)) = wall.domain();
1827 for (along_u, at_end) in [(true, true), (true, false), (false, true), (false, false)] {
1828 let longer = wall.extended(along_u, at_end, 3.0, 2, T).unwrap();
1829 let ((la, lb), (ma, mb)) = longer.domain();
1830 assert!(
1831 (la <= ua && lb >= ub && ma <= va && mb >= vb)
1832 && ((la < ua) || (lb > ub) || (ma < va) || (mb > vb)),
1833 "the domain grows: {:?}",
1834 longer.domain()
1835 );
1836 for i in 0..=6 {
1837 for j in 0..=6 {
1838 let u = la + (lb - la) * f64::from(i) / 6.0;
1839 let v = ma + (mb - ma) * f64::from(j) / 6.0;
1840 let p = longer.point_at(u, v, T).unwrap();
1841 let radial = (p.x * p.x + p.y * p.y).sqrt();
1842 assert!(
1843 (radial - 5.0).abs() < 1e-9,
1844 "off the cylinder at ({u},{v}): {p:?}"
1845 );
1846 if (ua..=ub).contains(&u) && (va..=vb).contains(&v) {
1847 let was = wall.point_at(u, v, T).unwrap();
1848 assert!(was.distance(p) < 1e-9, "the wall itself at ({u},{v})");
1849 }
1850 }
1851 }
1852 if !along_u {
1853 let reach = if at_end { mb } else { ma };
1854 let p = longer.point_at(ua, reach, T).unwrap();
1855 assert!(
1856 (p.z - if at_end { 13.0 } else { -3.0 }).abs() < 1e-9,
1857 "the axial continuation reaches the length asked: {p:?}"
1858 );
1859 }
1860 }
1861 let fitted = patch();
1862 let ((ua, ub), (va, vb)) = fitted.domain();
1863 let longer = fitted.extended(true, true, 1.0, 2, T).unwrap();
1864 for i in 0..=5 {
1865 for j in 0..=5 {
1866 let u = ua + (ub - ua) * f64::from(i) / 5.0;
1867 let v = va + (vb - va) * f64::from(j) / 5.0;
1868 let (was, now) = (
1869 fitted.point_at(u, v, T).unwrap(),
1870 longer.point_at(u, v, T).unwrap(),
1871 );
1872 assert!(was.distance(now) < 1e-9, "the patch itself at ({u},{v})");
1873 }
1874 }
1875 }
1876
1877 #[test]
1880 fn an_iso_curve_traces_the_surface_exactly() {
1881 use crate::Curve3d as _;
1882 let surface = patch();
1883 let ((ua, ub), (va, vb)) = surface.domain();
1884 for frac in [0.1, 0.5, 0.83] {
1885 let u = ua + (ub - ua) * frac;
1886 let column = surface.iso_u_curve(u, T).unwrap();
1887 let v = va + (vb - va) * frac;
1888 let row = surface.iso_v_curve(v, T).unwrap();
1889 for k in 0..=10 {
1890 let f = f64::from(k) / 10.0;
1891 let vv = va + (vb - va) * f;
1892 let on = surface.point_at(u, vv, T).unwrap();
1893 let along = column.point_at(vv, T).unwrap();
1894 assert!(
1895 on.distance(along) < 1e-9,
1896 "column at u {u}: {on:?} vs {along:?}"
1897 );
1898 let uu = ua + (ub - ua) * f;
1899 let on = surface.point_at(uu, v, T).unwrap();
1900 let along = row.point_at(uu, T).unwrap();
1901 assert!(
1902 on.distance(along) < 1e-9,
1903 "row at v {v}: {on:?} vs {along:?}"
1904 );
1905 }
1906 }
1907 }
1908
1909 fn patch() -> BSplineSurface {
1910 let (nu, nv) = (5, 4);
1911 let mut points = Vec::with_capacity(nu * nv);
1912 for i in 0..nu {
1913 for j in 0..nv {
1914 #[allow(clippy::cast_precision_loss)]
1915 let (x, y) = (i as f64, j as f64);
1916 points.push(Point::new(x, y, (x * 0.7).sin() * (y * 0.5).cos()));
1917 }
1918 }
1919 BSplineSurface::new(
1920 KnotVector::clamped_uniform(3, nu).unwrap(),
1921 KnotVector::clamped_uniform(2, nv).unwrap(),
1922 &ControlGrid::new(points, nu, nv).unwrap(),
1923 T,
1924 )
1925 .unwrap()
1926 }
1927
1928 fn every_surface() -> Vec<SurfaceGeometry> {
1929 let circle: Curve = CircleCurve::new(
1930 Circle::new(
1931 Frame::new(Point::new(5.0, 0.0, 0.0), Direction::Y, Direction::X, T).unwrap(),
1932 1.0,
1933 T,
1934 )
1935 .unwrap(),
1936 )
1937 .into();
1938 let line: Curve =
1939 LineCurve::segment(Point::new(2.0, 0.0, 0.0), Point::new(2.0, 0.0, 4.0), T)
1940 .unwrap()
1941 .into();
1942 vec![
1943 PlaneSurface::over(Plane::new(tilted()), (-5.0, 5.0), (-3.0, 3.0))
1944 .unwrap()
1945 .into(),
1946 CylinderSurface::new(Cylinder::new(tilted(), 2.0, T).unwrap(), (-4.0, 4.0))
1947 .unwrap()
1948 .into(),
1949 ConeSurface::new(Cone::new(tilted(), 3.0, 0.6, T).unwrap(), (-1.0, 5.0))
1950 .unwrap()
1951 .into(),
1952 SphereSurface::new(Sphere::new(tilted(), 4.0, T).unwrap()).into(),
1953 TorusSurface::new(Torus::new(tilted(), 5.0, 2.0, T).unwrap()).into(),
1954 patch().into(),
1955 RevolutionSurface::new(circle, Axis::Z, TAU).unwrap().into(),
1956 ExtrusionSurface::new(line, Direction::X, 3.0)
1957 .unwrap()
1958 .into(),
1959 TrimmedSurface::new(patch().into(), (0.2, 0.8), (0.3, 0.7), T)
1960 .unwrap()
1961 .into(),
1962 ]
1963 }
1964
1965 fn interior(s: &SurfaceGeometry, n: usize) -> Vec<(f64, f64)> {
1973 const OFF: f64 = 0.0413;
1974 let ((ua, ub), (va, vb)) = s.domain();
1975 let mut out = Vec::new();
1976 for i in 1..n {
1977 for j in 1..n {
1978 #[allow(clippy::cast_precision_loss)]
1979 let (tu, tv) = (i as f64 / n as f64 + OFF, j as f64 / n as f64 + OFF);
1980 out.push((ua + (ub - ua) * tu, va + (vb - va) * tv));
1981 }
1982 }
1983 out
1984 }
1985
1986 #[test]
1987 fn every_surfaces_first_partials_agree_with_finite_differences() {
1988 let h = 1e-6;
1989 for s in every_surface() {
1990 for (u, v) in interior(&s, 5) {
1991 let (du, dv) = s.d1_at(u, v, T).unwrap();
1992 let nu = (s.point_at(u + h, v, T).unwrap() - s.point_at(u - h, v, T).unwrap())
1993 * (1.0 / (2.0 * h));
1994 let nv = (s.point_at(u, v + h, T).unwrap() - s.point_at(u, v - h, T).unwrap())
1995 * (1.0 / (2.0 * h));
1996 assert!(
1997 (du - nu).magnitude() <= 1e-5 * nu.magnitude().max(1.0),
1998 "{:?} du at ({u}, {v}): {du:?} vs {nu:?}",
1999 s.kind()
2000 );
2001 assert!(
2002 (dv - nv).magnitude() <= 1e-5 * nv.magnitude().max(1.0),
2003 "{:?} dv at ({u}, {v}): {dv:?} vs {nv:?}",
2004 s.kind()
2005 );
2006 }
2007 }
2008 }
2009
2010 #[test]
2011 fn every_surfaces_second_partials_agree_with_finite_differences() {
2012 let h = 1e-5;
2013 for s in every_surface() {
2014 for (u, v) in interior(&s, 4) {
2015 let (d2u, duv, d2v) = s.d2_at(u, v, T).unwrap();
2016
2017 let nuu = (s.point_at(u + h, v, T).unwrap().to_vector()
2018 - s.point_at(u, v, T).unwrap().to_vector() * 2.0
2019 + s.point_at(u - h, v, T).unwrap().to_vector())
2020 * (1.0 / (h * h));
2021 let nvv = (s.point_at(u, v + h, T).unwrap().to_vector()
2022 - s.point_at(u, v, T).unwrap().to_vector() * 2.0
2023 + s.point_at(u, v - h, T).unwrap().to_vector())
2024 * (1.0 / (h * h));
2025 let nuv = (s.point_at(u + h, v + h, T).unwrap()
2026 - s.point_at(u + h, v - h, T).unwrap()
2027 - (s.point_at(u - h, v + h, T).unwrap()
2028 - s.point_at(u - h, v - h, T).unwrap()))
2029 * (1.0 / (4.0 * h * h));
2030
2031 for (analytic, numeric, name) in
2032 [(d2u, nuu, "d2u"), (duv, nuv, "duv"), (d2v, nvv, "d2v")]
2033 {
2034 assert!(
2035 (analytic - numeric).magnitude() <= 1e-3 * numeric.magnitude().max(1.0),
2036 "{:?} {name} at ({u}, {v}): {analytic:?} vs {numeric:?}",
2037 s.kind()
2038 );
2039 }
2040 }
2041 }
2042 }
2043
2044 #[test]
2045 fn out_of_domain_parameters_are_refused_where_the_surface_is_not_periodic() {
2046 for s in every_surface() {
2047 let ((ua, ub), (va, vb)) = s.domain();
2048 let inside = ((ua + ub) / 2.0, (va + vb) / 2.0);
2049 if s.kind() == SurfaceKind::Plane {
2050 let p = s.point_at(ub + 1.0, inside.1, T).unwrap();
2053 let q = s.point_at(ub, inside.1, T).unwrap();
2054 assert!((p.distance(q) - 1.0).abs() < 1e-12);
2055 continue;
2056 }
2057 if s.is_periodic_u() {
2058 assert!(s.point_at(ub + 1.0, inside.1, T).is_ok(), "{:?}", s.kind());
2059 } else {
2060 assert!(s.point_at(ub + 1.0, inside.1, T).is_err(), "{:?}", s.kind());
2061 }
2062 if s.is_periodic_v() {
2063 assert!(s.point_at(inside.0, vb + 1.0, T).is_ok(), "{:?}", s.kind());
2064 } else {
2065 assert!(s.point_at(inside.0, vb + 1.0, T).is_err(), "{:?}", s.kind());
2066 }
2067 }
2068 }
2069
2070 #[test]
2071 fn periodic_parameters_wrap_to_the_same_point() {
2072 for s in every_surface() {
2073 let ((ua, ub), (va, vb)) = s.domain();
2074 let (u, v) = ((ua + ub) * 0.4, (va + vb) * 0.4);
2075 let base = s.point_at(u, v, T).unwrap();
2076 if s.is_periodic_u() {
2077 let wrapped = s.point_at(u + (ub - ua), v, T).unwrap();
2078 assert!(base.is_equal(wrapped, T), "{:?} u wrap", s.kind());
2079 }
2080 if s.is_periodic_v() {
2081 let wrapped = s.point_at(u, v + (vb - va), T).unwrap();
2082 assert!(base.is_equal(wrapped, T), "{:?} v wrap", s.kind());
2083 }
2084 }
2085 }
2086
2087 #[test]
2088 fn analytic_surfaces_contain_their_own_points() {
2089 let cyl = Cylinder::new(tilted(), 2.0, T).unwrap();
2091 let cone = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
2092 let sph = Sphere::new(tilted(), 4.0, T).unwrap();
2093 let tor = Torus::new(tilted(), 5.0, 2.0, T).unwrap();
2094 let plane = Plane::new(tilted());
2095
2096 let cyl_s = CylinderSurface::new(cyl, (-4.0, 4.0)).unwrap();
2097 let cone_s = ConeSurface::new(cone, (-1.0, 5.0)).unwrap();
2098 let sph_s = SphereSurface::new(sph);
2099 let tor_s = TorusSurface::new(tor);
2100 let plane_s = PlaneSurface::over(plane, (-5.0, 5.0), (-3.0, 3.0)).unwrap();
2101
2102 for i in 1..6 {
2103 for j in 1..6 {
2104 let (tu, tv) = (f64::from(i) / 6.0, f64::from(j) / 6.0);
2105 assert!(
2106 plane.contains(
2107 plane_s
2108 .point_at(-5.0 + 10.0 * tu, -3.0 + 6.0 * tv, T)
2109 .unwrap(),
2110 T
2111 )
2112 );
2113 assert!(cyl.contains(cyl_s.point_at(TAU * tu, -4.0 + 8.0 * tv, T).unwrap(), T));
2114 assert!(cone.contains(cone_s.point_at(TAU * tu, -1.0 + 6.0 * tv, T).unwrap(), T));
2115 let half = core::f64::consts::FRAC_PI_2;
2116 assert!(
2117 sph.contains(
2118 sph_s
2119 .point_at(TAU * tu, -half + core::f64::consts::PI * tv, T)
2120 .unwrap(),
2121 T
2122 )
2123 );
2124 assert!(tor.contains(tor_s.point_at(TAU * tu, TAU * tv, T).unwrap(), T));
2125 }
2126 }
2127 }
2128
2129 #[test]
2130 fn normals_of_analytic_surfaces_match_their_own_definitions() {
2131 let sph = Sphere::new(tilted(), 4.0, T).unwrap();
2132 let s = SphereSurface::new(sph);
2133 for i in 1..6 {
2134 for j in 1..6 {
2135 let u = TAU * f64::from(i) / 6.0;
2136 let v = -1.2 + 2.4 * f64::from(j) / 6.0;
2137 let p = s.point_at(u, v, T).unwrap();
2138 assert!(
2139 s.normal_at(u, v, T)
2140 .unwrap()
2141 .is_equal(sph.normal_at(p, T).unwrap(), T)
2142 );
2143 }
2144 }
2145 }
2146
2147 #[test]
2148 fn a_sphere_degenerates_at_its_poles_and_says_so() {
2149 let s = SphereSurface::new(Sphere::new(tilted(), 4.0, T).unwrap());
2150 let half = core::f64::consts::FRAC_PI_2;
2151 for pole in [-half, half] {
2152 assert!(s.is_degenerate_at(1.0, pole, T).unwrap());
2153 assert!(s.normal_at(1.0, pole, T).is_err());
2154 }
2155 assert!(!s.is_degenerate_at(1.0, 0.0, T).unwrap());
2156 assert!(s.normal_at(1.0, 0.0, T).is_ok());
2157 }
2158
2159 #[test]
2160 fn a_cone_degenerates_at_its_apex() {
2161 let cone = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
2162 let apex_height = -3.0 / 0.6_f64.tan();
2163 let s = ConeSurface::new(cone, (apex_height, 5.0)).unwrap();
2164 assert!(s.is_degenerate_at(1.0, apex_height, T).unwrap());
2165 assert!(s.normal_at(1.0, apex_height, T).is_err());
2166 assert!(
2167 s.point_at(1.0, apex_height, T)
2168 .unwrap()
2169 .is_equal(cone.apex(), T)
2170 );
2171 }
2172
2173 #[test]
2174 fn revolving_a_circle_about_an_offset_axis_gives_a_torus() {
2175 let major = 5.0;
2178 let minor = 1.0;
2179 let generator: Curve = CircleCurve::new(
2186 Circle::new(
2187 Frame::new(Point::new(major, 0.0, 0.0), -Direction::Y, Direction::X, T).unwrap(),
2188 minor,
2189 T,
2190 )
2191 .unwrap(),
2192 )
2193 .into();
2194 let revolved = RevolutionSurface::new(generator, Axis::Z, TAU).unwrap();
2195 let torus = TorusSurface::new(Torus::new(Frame::WORLD, major, minor, T).unwrap());
2196
2197 for i in 0..12 {
2198 for j in 0..12 {
2199 let u = TAU * f64::from(i) / 12.0;
2200 let v = TAU * f64::from(j) / 12.0;
2201 let a = revolved.point_at(u, v, T).unwrap();
2202 let b = torus.point_at(u, v, T).unwrap();
2203 assert!(a.is_equal(b, T), "at ({u}, {v}): {a:?} vs {b:?}");
2204 }
2205 }
2206 }
2207
2208 #[test]
2209 fn extruding_a_line_gives_a_plane_and_a_circle_gives_a_cylinder() {
2210 let line: Curve = LineCurve::segment(Point::ORIGIN, Point::new(0.0, 4.0, 0.0), T)
2211 .unwrap()
2212 .into();
2213 let sheet = ExtrusionSurface::new(line, Direction::Z, 3.0).unwrap();
2214 let plane = Plane::new(Frame::WORLD);
2215 for i in 0..=4 {
2216 for j in 0..=4 {
2217 let p = sheet
2218 .point_at(4.0 * f64::from(i) / 4.0, 3.0 * f64::from(j) / 4.0, T)
2219 .unwrap();
2220 assert!(plane.distance_to(p) < 1e-12 || p.x.abs() < 1e-12);
2221 }
2222 }
2223
2224 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2225 let tube = ExtrusionSurface::new(circle, Direction::Z, 5.0).unwrap();
2226 let cylinder = Cylinder::new(Frame::WORLD, 2.0, T).unwrap();
2227 for i in 0..8 {
2228 for j in 0..=4 {
2229 let p = tube
2230 .point_at(TAU * f64::from(i) / 8.0, 5.0 * f64::from(j) / 4.0, T)
2231 .unwrap();
2232 assert!(cylinder.contains(p, T));
2233 }
2234 }
2235 }
2236
2237 #[test]
2238 fn degenerate_constructions_are_refused() {
2239 assert!(PlaneSurface::over(Plane::new(tilted()), (1.0, 1.0), (0.0, 1.0)).is_err());
2240 assert!(
2241 CylinderSurface::new(Cylinder::new(tilted(), 1.0, T).unwrap(), (2.0, 1.0)).is_err()
2242 );
2243 let circle: Curve = CircleCurve::new(Circle::new(tilted(), 1.0, T).unwrap()).into();
2244 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, 0.0).is_err());
2245 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, 7.0).is_err());
2246 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, TAU).is_ok());
2247 assert!(ExtrusionSurface::new(circle.clone(), Direction::Z, 0.0).is_err());
2248 assert!(ExtrusionSurface::new(circle, Direction::Z, f64::NAN).is_err());
2249 }
2250
2251 #[test]
2252 fn trimming_is_bounds_checked_and_agrees_with_its_basis() {
2253 let basis: SurfaceGeometry = patch().into();
2254 assert!(TrimmedSurface::new(basis.clone(), (0.2, 0.8), (0.3, 0.7), T).is_ok());
2255 assert!(TrimmedSurface::new(basis.clone(), (0.8, 0.2), (0.3, 0.7), T).is_err());
2256 assert!(TrimmedSurface::new(basis.clone(), (-0.5, 0.8), (0.3, 0.7), T).is_err());
2257
2258 let trimmed = TrimmedSurface::new(basis.clone(), (0.2, 0.8), (0.3, 0.7), T).unwrap();
2259 assert_eq!(trimmed.domain(), ((0.2, 0.8), (0.3, 0.7)));
2260 for i in 0..=4 {
2261 for j in 0..=4 {
2262 let u = 0.2 + 0.6 * f64::from(i) / 4.0;
2263 let v = 0.3 + 0.4 * f64::from(j) / 4.0;
2264 assert!(
2265 trimmed
2266 .point_at(u, v, T)
2267 .unwrap()
2268 .is_equal(basis.point_at(u, v, T).unwrap(), T)
2269 );
2270 }
2271 }
2272 assert!(trimmed.point_at(0.1, 0.5, T).is_err());
2273 }
2274
2275 #[test]
2276 fn transforms_move_surfaces_and_preserve_their_kind() {
2277 let t =
2278 Transform::rotation(Axis::X, 0.7) * Transform::translation(Vector::new(1.0, 2.0, 3.0));
2279 for s in every_surface() {
2280 let moved = s.transformed(&t, T).unwrap();
2281 assert_eq!(moved.kind(), s.kind());
2282 for (u, v) in interior(&s, 4) {
2283 let expected = t.apply(s.point_at(u, v, T).unwrap());
2284 assert!(
2285 moved.point_at(u, v, T).unwrap().is_equal(expected, T),
2286 "{:?} at ({u}, {v})",
2287 s.kind()
2288 );
2289 }
2290 }
2291 }
2292
2293 #[test]
2294 fn a_scaling_rescales_length_valued_parameters() {
2295 let s: SurfaceGeometry =
2298 CylinderSurface::new(Cylinder::new(Frame::WORLD, 2.0, T).unwrap(), (0.0, 4.0))
2299 .unwrap()
2300 .into();
2301 let scaled = s
2302 .transformed(&Transform::scaling(Point::ORIGIN, 3.0, T).unwrap(), T)
2303 .unwrap();
2304 assert_eq!(scaled.domain(), ((0.0, TAU), (0.0, 12.0)));
2305 assert!(
2306 scaled
2307 .point_at(0.0, 12.0, T)
2308 .unwrap()
2309 .is_equal(Point::new(6.0, 0.0, 12.0), T)
2310 );
2311 }
2312
2313 #[test]
2314 fn a_rational_patch_is_recognized_and_a_uniformly_weighted_one_is_not() {
2315 let g = patch();
2316 assert!(!g.is_rational());
2317
2318 let w = core::f64::consts::FRAC_1_SQRT_2;
2319 let points: Vec<_> = [
2320 (Point::new(1.0, 0.0, 0.0), 1.0),
2321 (Point::new(1.0, 1.0, 0.0), w),
2322 (Point::new(0.0, 1.0, 0.0), 1.0),
2323 (Point::new(1.0, 0.0, 1.0), 1.0),
2324 (Point::new(1.0, 1.0, 1.0), w),
2325 (Point::new(0.0, 1.0, 1.0), 1.0),
2326 ]
2327 .iter()
2328 .map(|(p, w)| Weighted::new(*p, *w, T).unwrap())
2329 .collect();
2330 let arc = BSplineSurface::rational(
2331 KnotVector::clamped_uniform(1, 2).unwrap(),
2332 KnotVector::clamped_uniform(2, 3).unwrap(),
2333 ControlGrid::new(points, 2, 3).unwrap(),
2334 )
2335 .unwrap();
2336 assert!(arc.is_rational());
2337 for i in 0..=4 {
2339 for j in 0..=8 {
2340 let p = arc
2341 .point_at(f64::from(i) / 4.0, f64::from(j) / 8.0, T)
2342 .unwrap();
2343 assert_relative_eq!(p.x.hypot(p.y), 1.0, epsilon = 1e-14);
2344 }
2345 }
2346 }
2347
2348 #[test]
2349 fn closure_and_periodicity_are_reported_correctly() {
2350 let s = every_surface();
2351 let expect = [
2352 (SurfaceKind::Plane, false, false, false, false),
2353 (SurfaceKind::Cylinder, true, false, true, false),
2354 (SurfaceKind::Cone, true, false, true, false),
2355 (SurfaceKind::Sphere, true, false, true, false),
2356 (SurfaceKind::Torus, true, true, true, true),
2357 (SurfaceKind::BSpline, false, false, false, false),
2358 (SurfaceKind::Revolution, true, true, true, true),
2359 (SurfaceKind::Extrusion, false, false, false, false),
2360 (SurfaceKind::Trimmed, false, false, false, false),
2361 ];
2362 for (surface, (kind, cu, cv, pu, pv)) in s.iter().zip(expect) {
2363 assert_eq!(surface.kind(), kind);
2364 assert_eq!(surface.is_closed_u(T), cu, "{kind:?} closed u");
2365 assert_eq!(surface.is_closed_v(T), cv, "{kind:?} closed v");
2366 assert_eq!(surface.is_periodic_u(), pu, "{kind:?} periodic u");
2367 assert_eq!(surface.is_periodic_v(), pv, "{kind:?} periodic v");
2368 }
2369 }
2370
2371 #[test]
2372 fn a_sphere_is_not_periodic_in_latitude() {
2373 let s = SphereSurface::new(Sphere::new(Frame::WORLD, 1.0, T).unwrap());
2376 assert!(!s.is_periodic_v());
2377 assert!(s.point_at(0.0, core::f64::consts::PI, T).is_err());
2378 }
2379}