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, CircleCurve, 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 pub fn over(curve: Curve, axis: Axis, range: (f64, f64)) -> OgeomResult<Self> {
585 let (a0, a1) = range;
586 let sweep = a1 - a0;
587 if !a0.is_finite() || !a1.is_finite() || sweep <= 0.0 || sweep > TAU + 1e-12 {
588 ogeom_bail!(
589 Construction,
590 "revolution range [{a0}, {a1}] must sweep (0, 2*pi]"
591 );
592 }
593 Ok(Self {
594 curve,
595 axis,
596 angle: (a0, a0 + sweep.min(TAU)),
597 })
598 }
599
600 #[must_use]
602 pub const fn curve(&self) -> &Curve {
603 &self.curve
604 }
605
606 #[must_use]
608 pub const fn axis(&self) -> Axis {
609 self.axis
610 }
611}
612
613impl ExtrusionSurface {
614 pub fn new(curve: Curve, direction: Direction, distance: f64) -> OgeomResult<Self> {
621 if !distance.is_finite() || distance <= 0.0 {
622 ogeom_bail!(
623 Construction,
624 "extrusion distance {distance} must be positive"
625 );
626 }
627 Ok(Self {
628 curve,
629 direction,
630 extent: (0.0, distance),
631 })
632 }
633
634 pub fn over(curve: Curve, direction: Direction, extent: (f64, f64)) -> OgeomResult<Self> {
643 if !extent.0.is_finite() || !extent.1.is_finite() || extent.1 <= extent.0 {
644 ogeom_bail!(
645 Construction,
646 "extrusion window {extent:?} must be finite and increasing"
647 );
648 }
649 Ok(Self {
650 curve,
651 direction,
652 extent,
653 })
654 }
655
656 #[must_use]
658 pub const fn curve(&self) -> &Curve {
659 &self.curve
660 }
661
662 #[must_use]
664 pub const fn direction(&self) -> Direction {
665 self.direction
666 }
667}
668
669impl TrimmedSurface {
670 pub fn new(
677 basis: SurfaceGeometry,
678 u: (f64, f64),
679 v: (f64, f64),
680 tol: Tolerances,
681 ) -> OgeomResult<Self> {
682 check_range("u", u.0, u.1)?;
683 check_range("v", v.0, v.1)?;
684 let ((ua, ub), (va, vb)) = basis.domain();
685 let eps = tol.parametric();
686 if !basis.is_periodic_u() && (u.0 < ua - eps || u.1 > ub + eps) {
687 ogeom_bail!(Domain, "u range [{}, {}] leaves [{ua}, {ub}]", u.0, u.1);
688 }
689 if !basis.is_periodic_v() && (v.0 < va - eps || v.1 > vb + eps) {
690 ogeom_bail!(Domain, "v range [{}, {}] leaves [{va}, {vb}]", v.0, v.1);
691 }
692 Ok(Self {
693 basis,
694 domain: (u, v),
695 })
696 }
697
698 #[must_use]
700 pub const fn basis(&self) -> &SurfaceGeometry {
701 &self.basis
702 }
703}
704
705impl OffsetSurface {
706 pub fn new(basis: SurfaceGeometry, distance: f64) -> OgeomResult<Self> {
713 if !distance.is_finite() || distance == 0.0 {
714 ogeom_bail!(
715 Construction,
716 "an offset of {distance} is not a displacement"
717 );
718 }
719 Ok(Self { basis, distance })
720 }
721
722 #[must_use]
724 pub const fn basis(&self) -> &SurfaceGeometry {
725 &self.basis
726 }
727
728 #[must_use]
730 pub const fn distance(&self) -> f64 {
731 self.distance
732 }
733
734 pub fn analytic(&self, tol: Tolerances) -> OgeomResult<Option<SurfaceGeometry>> {
748 let ((u0, u1), (v0, v1)) = self.basis.domain();
749 let clamp = |lo: f64, hi: f64| {
750 if lo.is_finite() && hi.is_finite() {
751 f64::midpoint(lo, hi)
752 } else {
753 0.0
754 }
755 };
756 let (u, v) = (clamp(u0, u1), clamp(v0, v1));
757 let at = self.basis.point_at(u, v, tol)?;
758 let normal = self.basis.normal_at(u, v, tol)?.vector();
759 let d = self.distance;
760 let outward = |from_axis: Vector| normal.dot(from_axis) > 0.0;
763 Ok(match &self.basis {
764 SurfaceGeometry::Plane(p) => {
765 let plane = p.plane();
766 let frame = plane.frame();
767 let moved = ogeom_math::Frame::from_axes(
770 frame.origin() + normal * d,
771 frame.x(),
772 frame.y(),
773 frame.z(),
774 tol,
775 )?;
776 Some(PlaneSurface::over(ogeom_math::Plane::new(moved), (u0, u1), (v0, v1))?.into())
777 }
778 SurfaceGeometry::Cylinder(c) => {
779 let cylinder = c.cylinder();
780 let frame = cylinder.frame();
781 let radial = at - frame.origin();
782 let radial = radial - frame.z().vector() * radial.dot(frame.z().vector());
783 let radius = cylinder.radius() + if outward(radial) { d } else { -d };
784 if radius <= tol.confusion() {
785 return Ok(None);
786 }
787 Some(
788 CylinderSurface::new(ogeom_math::Cylinder::new(frame, radius, tol)?, (v0, v1))?
789 .into(),
790 )
791 }
792 SurfaceGeometry::Cone(c) => {
793 let cone = c.cone();
794 let frame = cone.frame();
795 let radial = at - frame.origin();
796 let radial = radial - frame.z().vector() * radial.dot(frame.z().vector());
797 let grow = if outward(radial) { d } else { -d };
798 let rise = normal.dot(frame.z().vector()) * d;
804 let radius = cone.reference_radius() + grow * cone.half_angle().cos();
805 if radius <= tol.confusion() {
806 return Ok(None);
807 }
808 let raised = ogeom_math::Frame::from_axes(
809 frame.origin() + frame.z().vector() * rise,
810 frame.x(),
811 frame.y(),
812 frame.z(),
813 tol,
814 )?;
815 Some(
816 ConeSurface::new(
817 ogeom_math::Cone::new(raised, radius, cone.half_angle(), tol)?,
818 (v0, v1),
819 )?
820 .into(),
821 )
822 }
823 SurfaceGeometry::Sphere(s) => {
824 let sphere = s.sphere();
825 let grow = if outward(at - sphere.frame().origin()) {
826 d
827 } else {
828 -d
829 };
830 let radius = sphere.radius() + grow;
831 if radius <= tol.confusion() {
832 return Ok(None);
833 }
834 Some(
835 SphereSurface::new(ogeom_math::Sphere::new(sphere.frame(), radius, tol)?)
836 .into(),
837 )
838 }
839 SurfaceGeometry::Torus(t) => {
840 let torus = t.torus();
841 let frame = torus.frame();
842 let flat = at - frame.origin();
844 let flat = flat - frame.z().vector() * flat.dot(frame.z().vector());
845 let ring = frame.origin() + flat * (torus.major_radius() / flat.magnitude());
846 let grow = if outward(at - ring) { d } else { -d };
847 let minor = torus.minor_radius() + grow;
848 if minor <= tol.confusion() {
849 return Ok(None);
850 }
851 Some(
852 TorusSurface::new(ogeom_math::Torus::new(
853 frame,
854 torus.major_radius(),
855 minor,
856 tol,
857 )?)
858 .into(),
859 )
860 }
861 SurfaceGeometry::Revolution(r) => self.revolved_circle_offset(r, tol)?,
862 _ => None,
863 })
864 }
865
866 fn revolved_circle_offset(
874 &self,
875 r: &RevolutionSurface,
876 tol: Tolerances,
877 ) -> OgeomResult<Option<SurfaceGeometry>> {
878 let Curve::Circle(profile) = r.curve() else {
879 return Ok(None);
880 };
881 let circle = profile.forward(tol)?.circle();
883 let frame = circle.frame();
884 let axis = r.axis();
885 let normal = frame.z().vector();
886 if normal.dot(axis.direction.vector()).abs() > tol.angular()
889 || (axis.location - frame.origin()).dot(normal).abs() > tol.confusion()
890 {
891 return Ok(None);
892 }
893 let ((u0, u1), (v0, v1)) = r.domain();
896 let u = f64::midpoint(u0, u1);
897 let mut grow = None;
898 for k in 1..8 {
899 let v = v0 + (v1 - v0) * f64::from(k) / 8.0;
900 let Ok(n) = r.normal_at(u, v, tol) else {
901 continue;
902 };
903 let at = r.point_at(u, v, tol)?;
904 let centre = Transform::rotation(axis, u).apply(frame.origin());
905 let along = n.vector().dot(at - centre);
906 if along.abs() > 0.5 * circle.radius() {
907 grow = Some(if along > 0.0 { 1.0 } else { -1.0 });
908 break;
909 }
910 }
911 let Some(grow) = grow else {
912 return Ok(None);
913 };
914 let radius = circle.radius() + grow * self.distance;
915 if radius <= tol.confusion() {
916 return Ok(None);
917 }
918 let curve = CircleCurve::new(ogeom_math::Circle::new(frame, radius, tol)?);
919 let angle = r.domain().0;
920 Ok(Some(
921 RevolutionSurface::over(curve.into(), axis, angle)?.into(),
922 ))
923 }
924}
925
926impl Surface for OffsetSurface {
927 fn domain(&self) -> ((f64, f64), (f64, f64)) {
928 self.basis.domain()
929 }
930
931 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
932 let base = self.basis.point_at(u, v, tol)?;
933 let normal = self.basis.normal_at(u, v, tol)?;
934 Ok(base + normal.vector() * self.distance)
935 }
936
937 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
938 let (su, sv) = self.basis.d1_at(u, v, tol)?;
943 let (suu, suv, svv) = self.basis.d2_at(u, v, tol)?;
944 let c = su.cross(sv);
945 let m = c.magnitude();
946 let scale = su.magnitude().max(sv.magnitude());
947 if m <= tol.angular() * scale * scale {
948 ogeom_bail!(
949 Construction,
950 "the basis is degenerate at ({u}, {v}); the offset has no tangent plane there"
951 );
952 }
953 let n = c / m;
954 let cu = suu.cross(sv) + su.cross(suv);
955 let cv = suv.cross(sv) + su.cross(svv);
956 let nu = (cu - n * n.dot(cu)) / m;
957 let nv = (cv - n * n.dot(cv)) / m;
958 Ok((su + nu * self.distance, sv + nv * self.distance))
959 }
960
961 fn d2_at(&self, _u: f64, _v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
962 ogeom_bail!(
963 Construction,
964 "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"
965 )
966 }
967
968 fn kind(&self) -> SurfaceKind {
969 SurfaceKind::Offset
970 }
971
972 fn continuity(&self) -> Continuity {
973 match self.basis.continuity() {
975 Continuity::CInfinity => Continuity::CInfinity,
976 Continuity::C2 | Continuity::G2 => Continuity::C1,
977 Continuity::C1 | Continuity::G1 | Continuity::C0 => Continuity::C0,
978 }
979 }
980
981 fn is_closed_u(&self, tol: Tolerances) -> bool {
982 self.basis.is_closed_u(tol)
983 }
984
985 fn is_closed_v(&self, tol: Tolerances) -> bool {
986 self.basis.is_closed_v(tol)
987 }
988
989 fn is_periodic_u(&self) -> bool {
990 self.basis.is_periodic_u()
991 }
992
993 fn is_periodic_v(&self) -> bool {
994 self.basis.is_periodic_v()
995 }
996}
997
998fn finite_parameters(u: f64, v: f64) -> OgeomResult<()> {
999 if u.is_finite() && v.is_finite() {
1000 Ok(())
1001 } else {
1002 ogeom_bail!(Domain, "parameters ({u}, {v}) are not finite")
1003 }
1004}
1005
1006impl Surface for PlaneSurface {
1007 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1008 self.domain
1009 }
1010
1011 fn point_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<Point> {
1015 finite_parameters(u, v)?;
1016 Ok(elementary::plane_at(&self.plane, u, v).point)
1017 }
1018
1019 fn d1_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1020 finite_parameters(u, v)?;
1021 let f = self.plane.frame();
1022 Ok((f.x().vector(), f.y().vector()))
1023 }
1024
1025 fn d2_at(&self, u: f64, v: f64, _tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1026 finite_parameters(u, v)?;
1027 Ok((Vector::ZERO, Vector::ZERO, Vector::ZERO))
1028 }
1029
1030 fn kind(&self) -> SurfaceKind {
1031 SurfaceKind::Plane
1032 }
1033
1034 fn continuity(&self) -> Continuity {
1035 Continuity::CInfinity
1036 }
1037
1038 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1039 false
1040 }
1041
1042 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1043 false
1044 }
1045
1046 fn is_periodic_u(&self) -> bool {
1047 false
1048 }
1049
1050 fn is_periodic_v(&self) -> bool {
1051 false
1052 }
1053}
1054
1055impl Surface for CylinderSurface {
1056 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1057 ((0.0, TAU), self.height)
1058 }
1059
1060 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1061 let (u, v) = self.normalize_parameters(u, v, tol)?;
1062 Ok(elementary::cylinder_at(&self.cylinder, u, v).point)
1063 }
1064
1065 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1066 let (u, v) = self.normalize_parameters(u, v, tol)?;
1067 let p = elementary::cylinder_at(&self.cylinder, u, v);
1068 Ok((p.du, p.dv))
1069 }
1070
1071 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1072 let (u, _) = self.normalize_parameters(u, v, tol)?;
1073 let f = self.cylinder.frame();
1074 let r = self.cylinder.radius();
1075 let (sin, cos) = u.sin_cos();
1076 Ok((
1079 f.x() * (-r * cos) + f.y() * (-r * sin),
1080 Vector::ZERO,
1081 Vector::ZERO,
1082 ))
1083 }
1084
1085 fn kind(&self) -> SurfaceKind {
1086 SurfaceKind::Cylinder
1087 }
1088
1089 fn continuity(&self) -> Continuity {
1090 Continuity::CInfinity
1091 }
1092
1093 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1094 true
1095 }
1096
1097 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1098 false
1099 }
1100
1101 fn is_periodic_u(&self) -> bool {
1102 true
1103 }
1104
1105 fn is_periodic_v(&self) -> bool {
1106 false
1107 }
1108}
1109
1110impl Surface for ConeSurface {
1111 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1112 ((0.0, TAU), self.height)
1113 }
1114
1115 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1116 let (u, v) = self.normalize_parameters(u, v, tol)?;
1117 Ok(elementary::cone_at(&self.cone, u, v).point)
1118 }
1119
1120 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1121 let (u, v) = self.normalize_parameters(u, v, tol)?;
1122 let p = elementary::cone_at(&self.cone, u, v);
1123 Ok((p.du, p.dv))
1124 }
1125
1126 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1127 let (u, v) = self.normalize_parameters(u, v, tol)?;
1128 let f = self.cone.frame();
1129 let r = self.cone.radius_at(v);
1130 let slope = self.cone.half_angle().tan();
1131 let (sin, cos) = u.sin_cos();
1132 Ok((
1133 f.x() * (-r * cos) + f.y() * (-r * sin),
1135 f.x() * (-slope * sin) + f.y() * (slope * cos),
1138 Vector::ZERO,
1140 ))
1141 }
1142
1143 fn kind(&self) -> SurfaceKind {
1144 SurfaceKind::Cone
1145 }
1146
1147 fn continuity(&self) -> Continuity {
1148 Continuity::CInfinity
1149 }
1150
1151 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1152 true
1153 }
1154
1155 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1156 false
1157 }
1158
1159 fn is_periodic_u(&self) -> bool {
1160 true
1161 }
1162
1163 fn is_periodic_v(&self) -> bool {
1164 false
1165 }
1166}
1167
1168impl Surface for SphereSurface {
1169 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1170 let half = core::f64::consts::FRAC_PI_2;
1171 ((0.0, TAU), (-half, half))
1172 }
1173
1174 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1175 let (u, v) = self.normalize_parameters(u, v, tol)?;
1176 Ok(elementary::sphere_at(&self.sphere, u, v).point)
1177 }
1178
1179 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1180 let (u, v) = self.normalize_parameters(u, v, tol)?;
1181 let p = elementary::sphere_at(&self.sphere, u, v);
1182 Ok((p.du, p.dv))
1183 }
1184
1185 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1186 let (u, v) = self.normalize_parameters(u, v, tol)?;
1187 let f = self.sphere.frame();
1188 let r = self.sphere.radius();
1189 let (sin_u, cos_u) = u.sin_cos();
1190 let (sin_v, cos_v) = v.sin_cos();
1191 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
1192 let ring = r * cos_v;
1193 Ok((
1194 x * (-ring * cos_u) + y * (-ring * sin_u),
1195 x * (r * sin_v * sin_u) + y * (-r * sin_v * cos_u),
1196 x * (-ring * cos_u) + y * (-ring * sin_u) + z * (-r * sin_v),
1197 ))
1198 }
1199
1200 fn kind(&self) -> SurfaceKind {
1201 SurfaceKind::Sphere
1202 }
1203
1204 fn continuity(&self) -> Continuity {
1205 Continuity::CInfinity
1206 }
1207
1208 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1209 true
1210 }
1211
1212 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1213 false
1214 }
1215
1216 fn is_periodic_u(&self) -> bool {
1217 true
1218 }
1219
1220 fn is_periodic_v(&self) -> bool {
1221 false
1225 }
1226}
1227
1228impl Surface for TorusSurface {
1229 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1230 ((0.0, TAU), (0.0, TAU))
1231 }
1232
1233 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1234 let (u, v) = self.normalize_parameters(u, v, tol)?;
1235 Ok(elementary::torus_at(&self.torus, u, v).point)
1236 }
1237
1238 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1239 let (u, v) = self.normalize_parameters(u, v, tol)?;
1240 let p = elementary::torus_at(&self.torus, u, v);
1241 Ok((p.du, p.dv))
1242 }
1243
1244 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1245 let (u, v) = self.normalize_parameters(u, v, tol)?;
1246 let f = self.torus.frame();
1247 let (major, minor) = (self.torus.major_radius(), self.torus.minor_radius());
1248 let (sin_u, cos_u) = u.sin_cos();
1249 let (sin_v, cos_v) = v.sin_cos();
1250 let (x, y, z) = (f.x().vector(), f.y().vector(), f.z().vector());
1251 let out = x * cos_u + y * sin_u;
1252 let side = x * -sin_u + y * cos_u;
1253 let radius = minor.mul_add(cos_v, major);
1254 Ok((
1255 out * -radius,
1256 side * (-minor * sin_v),
1257 out * (-minor * cos_v) + z * (-minor * sin_v),
1258 ))
1259 }
1260
1261 fn kind(&self) -> SurfaceKind {
1262 SurfaceKind::Torus
1263 }
1264
1265 fn continuity(&self) -> Continuity {
1266 Continuity::CInfinity
1267 }
1268
1269 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1270 true
1271 }
1272
1273 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1274 true
1275 }
1276
1277 fn is_periodic_u(&self) -> bool {
1278 true
1279 }
1280
1281 fn is_periodic_v(&self) -> bool {
1282 true
1283 }
1284}
1285
1286impl Surface for BSplineSurface {
1287 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1288 (self.u_knots.domain(), self.v_knots.domain())
1289 }
1290
1291 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1292 let (u, v) = self.normalize_parameters(u, v, tol)?;
1293 if self.rational {
1294 bspline::evaluate_rational_surface(&self.u_knots, &self.v_knots, &self.grid, u, v, tol)
1295 } else {
1296 Ok(
1297 bspline::evaluate_surface(&self.u_knots, &self.v_knots, &self.grid, u, v, tol)?
1298 .point(),
1299 )
1300 }
1301 }
1302
1303 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1304 let (u, v) = self.normalize_parameters(u, v, tol)?;
1305 if !self.rational {
1309 let d = bspline::surface_derivatives(
1310 &self.u_knots,
1311 &self.v_knots,
1312 &self.grid,
1313 u,
1314 v,
1315 1,
1316 tol,
1317 )?;
1318 return Ok((d[1][0].scaled.to_vector(), d[0][1].scaled.to_vector()));
1319 }
1320 let d = bspline::rational_surface_derivatives(
1321 &self.u_knots,
1322 &self.v_knots,
1323 &self.grid,
1324 u,
1325 v,
1326 1,
1327 tol,
1328 )?;
1329 Ok((d[1][0].to_vector(), d[0][1].to_vector()))
1330 }
1331
1332 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1333 let (u, v) = self.normalize_parameters(u, v, tol)?;
1334 if !self.rational {
1335 let d = bspline::surface_derivatives(
1336 &self.u_knots,
1337 &self.v_knots,
1338 &self.grid,
1339 u,
1340 v,
1341 2,
1342 tol,
1343 )?;
1344 return Ok((
1345 d[2][0].scaled.to_vector(),
1346 d[1][1].scaled.to_vector(),
1347 d[0][2].scaled.to_vector(),
1348 ));
1349 }
1350 let d = bspline::rational_surface_derivatives(
1351 &self.u_knots,
1352 &self.v_knots,
1353 &self.grid,
1354 u,
1355 v,
1356 2,
1357 tol,
1358 )?;
1359 Ok((
1360 d[2][0].to_vector(),
1361 d[1][1].to_vector(),
1362 d[0][2].to_vector(),
1363 ))
1364 }
1365
1366 fn point_d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Point, Vector, Vector)> {
1367 let (u, v) = self.normalize_parameters(u, v, tol)?;
1368 if self.rational {
1369 let d = bspline::rational_surface_derivatives(
1370 &self.u_knots,
1371 &self.v_knots,
1372 &self.grid,
1373 u,
1374 v,
1375 1,
1376 tol,
1377 )?;
1378 return Ok((
1379 Point::ORIGIN + d[0][0].to_vector(),
1380 d[1][0].to_vector(),
1381 d[0][1].to_vector(),
1382 ));
1383 }
1384 let d =
1385 bspline::surface_derivatives(&self.u_knots, &self.v_knots, &self.grid, u, v, 1, tol)?;
1386 Ok((
1387 Point::ORIGIN + d[0][0].scaled.to_vector(),
1388 d[1][0].scaled.to_vector(),
1389 d[0][1].scaled.to_vector(),
1390 ))
1391 }
1392
1393 fn jet_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<crate::SurfaceJet> {
1394 let (u, v) = self.normalize_parameters(u, v, tol)?;
1398 let d = if self.rational {
1399 bspline::rational_surface_derivatives(
1400 &self.u_knots,
1401 &self.v_knots,
1402 &self.grid,
1403 u,
1404 v,
1405 2,
1406 tol,
1407 )?
1408 } else {
1409 bspline::surface_derivatives(&self.u_knots, &self.v_knots, &self.grid, u, v, 2, tol)?
1410 .into_iter()
1411 .map(|row| row.into_iter().map(|w| w.scaled).collect())
1412 .collect()
1413 };
1414 Ok(crate::SurfaceJet {
1415 point: Point::ORIGIN + d[0][0].to_vector(),
1416 du: d[1][0].to_vector(),
1417 dv: d[0][1].to_vector(),
1418 d2u: d[2][0].to_vector(),
1419 duv: d[1][1].to_vector(),
1420 d2v: d[0][2].to_vector(),
1421 })
1422 }
1423
1424 fn kind(&self) -> SurfaceKind {
1425 SurfaceKind::BSpline
1426 }
1427
1428 fn continuity(&self) -> Continuity {
1429 let worst = |k: &KnotVector| {
1431 let (a, b) = k.domain();
1432 k.distinct()
1433 .into_iter()
1434 .filter(|(x, _)| *x > a && *x < b)
1435 .map(|(_, m)| m)
1436 .max()
1437 .map_or(Continuity::CInfinity, |m| {
1438 match k.degree().saturating_sub(m) {
1439 0 => Continuity::C0,
1440 1 => Continuity::C1,
1441 _ => Continuity::C2,
1442 }
1443 })
1444 };
1445 worst(&self.u_knots).min(worst(&self.v_knots))
1446 }
1447
1448 fn is_closed_u(&self, tol: Tolerances) -> bool {
1449 self.closed.0 || Self::net_closed_u(&self.grid, tol)
1452 }
1453
1454 fn is_closed_v(&self, tol: Tolerances) -> bool {
1455 self.closed.1 || Self::net_closed_v(&self.grid, tol)
1456 }
1457
1458 fn is_periodic_u(&self) -> bool {
1459 false
1460 }
1461
1462 fn is_periodic_v(&self) -> bool {
1463 false
1464 }
1465}
1466
1467impl Surface for RevolutionSurface {
1468 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1469 (self.angle, self.curve.domain())
1470 }
1471
1472 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1473 let (u, v) = self.normalize_parameters(u, v, tol)?;
1474 let p = self.curve.point_at(v, tol)?;
1475 Ok(Transform::rotation(self.axis, u).apply(p))
1476 }
1477
1478 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1479 let (u, v) = self.normalize_parameters(u, v, tol)?;
1480 let rotate = Transform::rotation(self.axis, u);
1481 let p = rotate.apply(self.curve.point_at(v, tol)?);
1482 let radius = p - self.axis.project(p);
1485 Ok((
1486 self.axis.direction.cross_with(radius),
1487 rotate.apply_vector(self.curve.d1_at(v, tol)?),
1488 ))
1489 }
1490
1491 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1492 let (u, v) = self.normalize_parameters(u, v, tol)?;
1493 let rotate = Transform::rotation(self.axis, u);
1494 let p = rotate.apply(self.curve.point_at(v, tol)?);
1495 let radius = p - self.axis.project(p);
1496 let d = self.curve.derivatives_at(v, 2, tol)?;
1497 let curve_d1 = rotate.apply_vector(d[1]);
1498 let d2u = -radius;
1500 let duv = self.axis.direction.cross_with(curve_d1);
1503 Ok((d2u, duv, rotate.apply_vector(d[2])))
1504 }
1505
1506 fn kind(&self) -> SurfaceKind {
1507 SurfaceKind::Revolution
1508 }
1509
1510 fn continuity(&self) -> Continuity {
1511 self.curve.continuity()
1513 }
1514
1515 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1516 (self.angle.1 - self.angle.0 - TAU).abs() <= 1e-12
1517 }
1518
1519 fn is_closed_v(&self, tol: Tolerances) -> bool {
1520 self.curve.is_closed(tol)
1521 }
1522
1523 fn is_periodic_u(&self) -> bool {
1524 (self.angle.1 - self.angle.0 - TAU).abs() <= 1e-12
1525 }
1526
1527 fn is_periodic_v(&self) -> bool {
1528 self.curve.is_periodic()
1529 }
1530}
1531
1532impl Surface for ExtrusionSurface {
1533 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1534 (self.curve.domain(), self.extent)
1535 }
1536
1537 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1538 let (u, v) = self.normalize_parameters(u, v, tol)?;
1539 Ok(self.curve.point_at(u, tol)? + self.direction * v)
1540 }
1541
1542 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1543 let (u, _) = self.normalize_parameters(u, v, tol)?;
1544 Ok((self.curve.d1_at(u, tol)?, self.direction.vector()))
1545 }
1546
1547 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1548 let (u, _) = self.normalize_parameters(u, v, tol)?;
1549 Ok((
1551 self.curve.derivatives_at(u, 2, tol)?[2],
1552 Vector::ZERO,
1553 Vector::ZERO,
1554 ))
1555 }
1556
1557 fn kind(&self) -> SurfaceKind {
1558 SurfaceKind::Extrusion
1559 }
1560
1561 fn continuity(&self) -> Continuity {
1562 self.curve.continuity()
1563 }
1564
1565 fn is_closed_u(&self, tol: Tolerances) -> bool {
1566 self.curve.is_closed(tol)
1567 }
1568
1569 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1570 false
1571 }
1572
1573 fn is_periodic_u(&self) -> bool {
1574 self.curve.is_periodic()
1575 }
1576
1577 fn is_periodic_v(&self) -> bool {
1578 false
1579 }
1580}
1581
1582impl Surface for TrimmedSurface {
1583 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1584 self.domain
1585 }
1586
1587 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1588 let (u, v) = self.normalize_parameters(u, v, tol)?;
1589 self.basis.point_at(u, v, tol)
1590 }
1591
1592 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1593 let (u, v) = self.normalize_parameters(u, v, tol)?;
1594 self.basis.d1_at(u, v, tol)
1595 }
1596
1597 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1598 let (u, v) = self.normalize_parameters(u, v, tol)?;
1599 self.basis.d2_at(u, v, tol)
1600 }
1601
1602 fn kind(&self) -> SurfaceKind {
1603 SurfaceKind::Trimmed
1604 }
1605
1606 fn continuity(&self) -> Continuity {
1607 self.basis.continuity()
1608 }
1609
1610 fn is_closed_u(&self, _tol: Tolerances) -> bool {
1611 false
1613 }
1614
1615 fn is_closed_v(&self, _tol: Tolerances) -> bool {
1616 false
1617 }
1618
1619 fn is_periodic_u(&self) -> bool {
1620 false
1621 }
1622
1623 fn is_periodic_v(&self) -> bool {
1624 false
1625 }
1626}
1627
1628macro_rules! dispatch {
1630 ($self:ident, $s:ident => $body:expr) => {
1631 match $self {
1632 Self::Plane($s) => $body,
1633 Self::Cylinder($s) => $body,
1634 Self::Cone($s) => $body,
1635 Self::Sphere($s) => $body,
1636 Self::Torus($s) => $body,
1637 Self::BSpline($s) => $body,
1638 Self::Revolution($s) => $body,
1639 Self::Extrusion($s) => $body,
1640 Self::Trimmed($s) => $body,
1641 Self::Offset($s) => $body,
1642 }
1643 };
1644}
1645
1646impl Surface for SurfaceGeometry {
1647 fn domain(&self) -> ((f64, f64), (f64, f64)) {
1648 dispatch!(self, s => s.domain())
1649 }
1650
1651 fn point_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<Point> {
1652 dispatch!(self, s => s.point_at(u, v, tol))
1653 }
1654
1655 fn d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector)> {
1656 dispatch!(self, s => s.d1_at(u, v, tol))
1657 }
1658
1659 fn d2_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Vector, Vector, Vector)> {
1660 dispatch!(self, s => s.d2_at(u, v, tol))
1661 }
1662
1663 fn point_d1_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<(Point, Vector, Vector)> {
1668 dispatch!(self, s => s.point_d1_at(u, v, tol))
1669 }
1670
1671 fn jet_at(&self, u: f64, v: f64, tol: Tolerances) -> OgeomResult<crate::SurfaceJet> {
1672 dispatch!(self, s => s.jet_at(u, v, tol))
1673 }
1674
1675 fn kind(&self) -> SurfaceKind {
1676 dispatch!(self, s => s.kind())
1677 }
1678
1679 fn continuity(&self) -> Continuity {
1680 dispatch!(self, s => s.continuity())
1681 }
1682
1683 fn is_closed_u(&self, tol: Tolerances) -> bool {
1684 dispatch!(self, s => s.is_closed_u(tol))
1685 }
1686
1687 fn is_closed_v(&self, tol: Tolerances) -> bool {
1688 dispatch!(self, s => s.is_closed_v(tol))
1689 }
1690
1691 fn is_periodic_u(&self) -> bool {
1692 dispatch!(self, s => s.is_periodic_u())
1693 }
1694
1695 fn is_periodic_v(&self) -> bool {
1696 dispatch!(self, s => s.is_periodic_v())
1697 }
1698}
1699
1700impl Transformable for SurfaceGeometry {
1701 fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
1702 if t.kind() == ogeom_math::TransformKind::Identity {
1703 return Ok(self.clone());
1704 }
1705 let scale = t.scale_factor().abs();
1706 Ok(match self {
1707 Self::Plane(s) => Self::Plane(PlaneSurface {
1708 plane: s.plane.transformed(t, tol)?,
1709 domain: (
1712 (s.domain.0.0 * scale, s.domain.0.1 * scale),
1713 (s.domain.1.0 * scale, s.domain.1.1 * scale),
1714 ),
1715 }),
1716 Self::Cylinder(s) => Self::Cylinder(CylinderSurface {
1717 cylinder: s.cylinder.transformed(t, tol)?,
1718 height: (s.height.0 * scale, s.height.1 * scale),
1719 }),
1720 Self::Offset(s) => Self::Offset(Box::new(OffsetSurface {
1723 basis: s.basis.transformed(t, tol)?,
1724 distance: s.distance * scale * if t.preserves_handedness() { 1.0 } else { -1.0 },
1725 })),
1726 Self::Cone(s) => Self::Cone(ConeSurface {
1727 cone: s.cone.transformed(t, tol)?,
1728 height: (s.height.0 * scale, s.height.1 * scale),
1729 }),
1730 Self::Sphere(s) => Self::Sphere(SphereSurface {
1731 sphere: s.sphere.transformed(t, tol)?,
1732 }),
1733 Self::Torus(s) => Self::Torus(TorusSurface {
1734 torus: s.torus.transformed(t, tol)?,
1735 }),
1736 Self::BSpline(s) => {
1737 let grid = ControlGrid::new(
1738 s.grid
1739 .points()
1740 .iter()
1741 .map(|w| Weighted::new(t.apply(w.point()), w.weight, tol))
1742 .collect::<OgeomResult<Vec<_>>>()?,
1743 s.grid.u_count(),
1744 s.grid.v_count(),
1745 )?;
1746 Self::BSpline(BSplineSurface {
1747 u_knots: s.u_knots.clone(),
1748 v_knots: s.v_knots.clone(),
1749 grid,
1750 rational: s.rational,
1751 closed: s.closed,
1752 })
1753 }
1754 Self::Revolution(s) => {
1755 let direction = t.apply_direction(s.axis.direction, tol)?;
1760 let direction = if t.preserves_handedness() {
1761 direction
1762 } else {
1763 direction.reversed()
1764 };
1765 Self::Revolution(Box::new(RevolutionSurface {
1766 curve: s.curve.transformed(t, tol)?,
1767 axis: Axis::new(t.apply(s.axis.location), direction),
1768 angle: s.angle,
1769 }))
1770 }
1771 Self::Extrusion(s) => Self::Extrusion(Box::new(ExtrusionSurface {
1772 curve: s.curve.transformed(t, tol)?,
1773 direction: t.apply_direction(s.direction, tol)?,
1774 extent: (s.extent.0 * scale, s.extent.1 * scale),
1775 })),
1776 Self::Trimmed(s) => {
1777 let basis = s.basis.transformed(t, tol)?;
1778 let (old_u, old_v) = s.basis.domain();
1783 let (new_u, new_v) = basis.domain();
1784 let carry = |x: f64, from: (f64, f64), to: (f64, f64)| carried(x, from, to);
1785 Self::Trimmed(Box::new(TrimmedSurface {
1786 basis,
1787 domain: (
1788 (
1789 carry(s.domain.0.0, old_u, new_u),
1790 carry(s.domain.0.1, old_u, new_u),
1791 ),
1792 (
1793 carry(s.domain.1.0, old_v, new_v),
1794 carry(s.domain.1.1, old_v, new_v),
1795 ),
1796 ),
1797 }))
1798 }
1799 })
1800 }
1801}
1802
1803impl From<PlaneSurface> for SurfaceGeometry {
1804 fn from(s: PlaneSurface) -> Self {
1805 Self::Plane(s)
1806 }
1807}
1808impl From<CylinderSurface> for SurfaceGeometry {
1809 fn from(s: CylinderSurface) -> Self {
1810 Self::Cylinder(s)
1811 }
1812}
1813impl From<ConeSurface> for SurfaceGeometry {
1814 fn from(s: ConeSurface) -> Self {
1815 Self::Cone(s)
1816 }
1817}
1818impl From<SphereSurface> for SurfaceGeometry {
1819 fn from(s: SphereSurface) -> Self {
1820 Self::Sphere(s)
1821 }
1822}
1823impl From<TorusSurface> for SurfaceGeometry {
1824 fn from(s: TorusSurface) -> Self {
1825 Self::Torus(s)
1826 }
1827}
1828impl From<BSplineSurface> for SurfaceGeometry {
1829 fn from(s: BSplineSurface) -> Self {
1830 Self::BSpline(s)
1831 }
1832}
1833impl From<RevolutionSurface> for SurfaceGeometry {
1834 fn from(s: RevolutionSurface) -> Self {
1835 Self::Revolution(Box::new(s))
1836 }
1837}
1838impl From<ExtrusionSurface> for SurfaceGeometry {
1839 fn from(s: ExtrusionSurface) -> Self {
1840 Self::Extrusion(Box::new(s))
1841 }
1842}
1843impl From<TrimmedSurface> for SurfaceGeometry {
1844 fn from(s: TrimmedSurface) -> Self {
1845 Self::Trimmed(Box::new(s))
1846 }
1847}
1848
1849#[must_use]
1854pub fn carried(x: f64, from: (f64, f64), to: (f64, f64)) -> f64 {
1855 let span = from.1 - from.0;
1856 if span == 0.0 || !span.is_finite() || !(to.1 - to.0).is_finite() {
1857 return x;
1858 }
1859 if from == to {
1860 return x;
1861 }
1862 let ratio = (to.1 - to.0) / span;
1863 let about_zero = |a: f64, b: f64| (a * ratio - b).abs() <= f64::EPSILON * 4.0 * b.abs();
1867 if about_zero(from.0, to.0) && about_zero(from.1, to.1) {
1868 return x * ratio;
1869 }
1870 to.0 + (x - from.0) * ratio
1871}
1872
1873#[cfg(test)]
1874#[allow(clippy::unwrap_used)]
1875mod tests {
1876 #[test]
1881 fn an_offset_s_closed_form_is_the_offset_point_for_point() {
1882 use crate::Surface as _;
1883 use ogeom_math::{Cone, Cylinder, Direction, Plane, Point, Sphere};
1884 let tilted = Frame::new(
1885 Point::new(1.0, -2.0, 3.0),
1886 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
1887 Direction::from_coords(3.0, 0.0, -1.0, T).unwrap(),
1888 T,
1889 )
1890 .unwrap();
1891 let bases: Vec<SurfaceGeometry> = vec![
1892 PlaneSurface::new(Plane::new(tilted.mirrored())).into(),
1893 CylinderSurface::new(Cylinder::new(tilted, 5.0, T).unwrap(), (0.0, 10.0))
1894 .unwrap()
1895 .into(),
1896 ConeSurface::new(Cone::new(tilted, 5.0, 0.4, T).unwrap(), (0.0, 10.0))
1897 .unwrap()
1898 .into(),
1899 SphereSurface::new(Sphere::new(tilted, 5.0, T).unwrap()).into(),
1900 ];
1901 for basis in bases {
1902 for d in [1.5, -1.5] {
1903 let offset = OffsetSurface::new(basis.clone(), d).unwrap();
1904 let closed = offset.analytic(T).unwrap().unwrap();
1905 for (u, v) in [(0.3, 1.0), (2.0, 4.0), (4.5, 8.0)] {
1906 let v = if matches!(basis, SurfaceGeometry::Sphere(_)) {
1907 v / 10.0
1908 } else {
1909 v
1910 };
1911 let want = offset.point_at(u, v, T).unwrap();
1912 let got = closed.point_at(u, v, T).unwrap();
1913 assert!(
1914 got.distance(want) < 1e-9,
1915 "{basis:?} by {d} at ({u}, {v}): {got:?} against {want:?}"
1916 );
1917 let (a, b) = (
1918 offset.normal_at(u, v, T).unwrap(),
1919 closed.normal_at(u, v, T).unwrap(),
1920 );
1921 assert!(
1922 a.vector().dot(b.vector()) > 0.999_999,
1923 "{basis:?} by {d}: normals {a:?} against {b:?}"
1924 );
1925 }
1926 }
1927 }
1928 }
1929
1930 #[test]
1933 fn a_mirrored_offset_surface_is_the_image_of_the_original() {
1934 use crate::Surface as _;
1935 use ogeom_math::{Direction, Point, Sphere, Transform};
1936 let sphere: SurfaceGeometry =
1937 SphereSurface::new(Sphere::new(Frame::WORLD, 20.0, T).unwrap()).into();
1938 let offset = SurfaceGeometry::Offset(Box::new(OffsetSurface::new(sphere, 3.0).unwrap()));
1939 for t in [
1940 Transform::plane_mirror(Point::ORIGIN, Direction::X),
1941 Transform::point_mirror(Point::new(1.0, 2.0, 3.0)),
1942 Transform::scaling(Point::ORIGIN, -2.0, T).unwrap(),
1943 ] {
1944 let moved = offset.transformed(&t, T).unwrap();
1945 for (u, v) in [(0.0, 0.0), (0.5, 0.3), (2.0, -1.0), (4.0, 1.2)] {
1946 let want = t.apply(offset.point_at(u, v, T).unwrap());
1947 let got = moved.point_at(u, v, T).unwrap();
1948 assert!(
1949 got.distance(want) < 1e-9,
1950 "{t:?} at ({u}, {v}): {got:?} against {want:?}"
1951 );
1952 }
1953 }
1954 }
1955
1956 #[test]
1959 fn a_mirrored_revolution_is_the_image_of_the_original() {
1960 use crate::Surface as _;
1961 use crate::curve::LineCurve;
1962 use ogeom_math::{Direction, Point, Transform};
1963 let profile: Curve =
1964 LineCurve::segment(Point::new(10.0, 0.0, 0.0), Point::new(20.0, 0.0, 5.0), T)
1965 .unwrap()
1966 .into();
1967 let original: SurfaceGeometry = RevolutionSurface::new(profile, Axis::Z, 1.5)
1968 .unwrap()
1969 .into();
1970 for t in [
1971 Transform::plane_mirror(Point::ORIGIN, Direction::X),
1972 Transform::point_mirror(Point::new(1.0, 2.0, 3.0)),
1973 Transform::rotation(ogeom_math::Axis::new(Point::ORIGIN, Direction::Y), 0.7),
1974 ] {
1975 let moved = original.transformed(&t, T).unwrap();
1976 for (u, v) in [(0.0, 0.0), (0.5, 3.0), (1.2, 7.5), (1.5, 11.0)] {
1977 let want = t.apply(original.point_at(u, v, T).unwrap());
1978 let got = moved.point_at(u, v, T).unwrap();
1979 assert!(
1980 got.distance(want) < 1e-9,
1981 "{t:?} at ({u}, {v}): {got:?} against {want:?}"
1982 );
1983 }
1984 }
1985 }
1986
1987 #[test]
1997 fn a_closed_surface_wraps_a_parameter_past_its_join() {
1998 use crate::Surface as _;
1999 use ogeom_math::{ControlGrid, KnotVector, Point};
2000 let ring = [
2001 Point::new(1.0, 0.0, 0.0),
2002 Point::new(0.0, 1.0, 0.0),
2003 Point::new(-1.0, 0.0, 0.0),
2004 Point::new(0.0, -1.0, 0.0),
2005 Point::new(1.0, 0.0, 0.0),
2006 ];
2007 let mut control = Vec::new();
2008 for p in &ring {
2009 for z in [0.0, 5.0] {
2010 control.push(Point::new(p.x, p.y, z));
2011 }
2012 }
2013 let tube = BSplineSurface::new(
2014 KnotVector::new(vec![0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0], 1).unwrap(),
2015 KnotVector::clamped_uniform(1, 2).unwrap(),
2016 &ControlGrid::new(control, 5, 2).unwrap(),
2017 T,
2018 )
2019 .unwrap();
2020 assert!(
2021 tube.is_closed_u(T) && !tube.is_periodic_u(),
2022 "closed, not periodic"
2023 );
2024 let inside = tube.point_at(0.3, 0.5, T).unwrap();
2025 let past = tube.point_at(1.3, 0.5, T).unwrap();
2026 let before = tube.point_at(-0.7, 0.5, T).unwrap();
2027 assert!(
2028 inside.is_equal(past, T),
2029 "a period past the end: {inside:?} vs {past:?}"
2030 );
2031 assert!(
2032 inside.is_equal(before, T),
2033 "a period before the start: {inside:?} vs {before:?}"
2034 );
2035 assert!(
2037 tube.point_at(0.3, 1.5, T).is_err(),
2038 "an open direction still refuses"
2039 );
2040 }
2041
2042 use super::*;
2043 use crate::curve::{CircleCurve, LineCurve};
2044 use approx::assert_relative_eq;
2045 use ogeom_math::{Circle, Frame};
2046
2047 const T: Tolerances = Tolerances::millimetres();
2048
2049 #[test]
2050 fn a_jet_agrees_with_the_separate_accessors() {
2051 let close = |a: Vector, b: Vector, what: &str| {
2058 let scale = a.magnitude().max(b.magnitude()).max(1.0);
2059 assert!(
2060 (a - b).magnitude() <= scale * 1e-12,
2061 "{what}: {a:?} against {b:?}"
2062 );
2063 };
2064 let surfaces: Vec<SurfaceGeometry> = vec![
2065 SurfaceGeometry::Plane(PlaneSurface::new(ogeom_math::Plane::XY)),
2066 SurfaceGeometry::Sphere(SphereSurface::new(
2067 ogeom_math::Sphere::new(ogeom_math::Frame::WORLD, 3.0, T).unwrap(),
2068 )),
2069 bspline_patch(),
2070 ];
2071 for surface in &surfaces {
2072 let ((ua, ub), (va, vb)) = surface.domain();
2073 for i in 1..5 {
2074 for j in 1..5 {
2075 let u = ua + (ub - ua) * f64::from(i) / 5.0;
2076 let v = va + (vb - va) * f64::from(j) / 5.0;
2077 let jet = surface.jet_at(u, v, T).unwrap();
2078 let point = surface.point_at(u, v, T).unwrap();
2079 close(jet.point - Point::ORIGIN, point - Point::ORIGIN, "point");
2080 let (du, dv) = surface.d1_at(u, v, T).unwrap();
2081 close(jet.du, du, "du");
2082 close(jet.dv, dv, "dv");
2083 let (d2u, duv, d2v) = surface.d2_at(u, v, T).unwrap();
2084 close(jet.d2u, d2u, "d2u");
2085 close(jet.duv, duv, "duv");
2086 close(jet.d2v, d2v, "d2v");
2087 }
2088 }
2089 }
2090 }
2091
2092 fn bspline_patch() -> SurfaceGeometry {
2094 let knots = KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).unwrap();
2095 let mut points = Vec::new();
2096 for i in 0..4 {
2097 for j in 0..4 {
2098 let z = if i == 1 && j == 2 { 2.0 } else { 0.0 };
2099 points.push(Point::new(f64::from(i), f64::from(j), z));
2100 }
2101 }
2102 let grid = ControlGrid::new(points, 4, 4).unwrap();
2103 SurfaceGeometry::BSpline(BSplineSurface::new(knots.clone(), knots, &grid, T).unwrap())
2104 }
2105
2106 #[test]
2107 fn an_offset_cylinder_is_the_larger_cylinder() {
2108 use ogeom_math::Cylinder;
2109 let basis = SurfaceGeometry::Cylinder(
2110 CylinderSurface::new(Cylinder::new(Frame::WORLD, 2.0, T).unwrap(), (0.0, 5.0)).unwrap(),
2111 );
2112 let offset = OffsetSurface::new(basis, 1.5).unwrap();
2113 for (u, v) in [(0.0, 0.0), (1.0, 2.0), (3.5, 4.5)] {
2116 let p = offset.point_at(u, v, T).unwrap();
2117 let radial = (p.x * p.x + p.y * p.y).sqrt();
2118 assert_relative_eq!(radial, 3.5, epsilon = 1e-12);
2119 }
2120 let h = 1e-6;
2122 let (du, dv) = offset.d1_at(1.0, 2.0, T).unwrap();
2123 let fdu = (offset.point_at(1.0 + h, 2.0, T).unwrap()
2124 - offset.point_at(1.0 - h, 2.0, T).unwrap())
2125 / (2.0 * h);
2126 let fdv = (offset.point_at(1.0, 2.0 + h, T).unwrap()
2127 - offset.point_at(1.0, 2.0 - h, T).unwrap())
2128 / (2.0 * h);
2129 assert_relative_eq!((du - fdu).magnitude(), 0.0, epsilon = 1e-5);
2130 assert_relative_eq!((dv - fdv).magnitude(), 0.0, epsilon = 1e-5);
2131 assert!(offset.d2_at(1.0, 2.0, T).is_err());
2133 }
2134
2135 fn tilted() -> Frame {
2136 Frame::new(
2137 Point::new(1.0, -2.0, 3.0),
2138 Direction::from_coords(1.0, 2.0, 3.0, T).unwrap(),
2139 Direction::X,
2140 T,
2141 )
2142 .unwrap()
2143 }
2144
2145 #[test]
2150 fn a_patch_extended_stays_itself_and_continues() {
2151 use crate::Surface as _;
2152 let cylinder = ogeom_math::Cylinder::new(Frame::WORLD, 5.0, T).unwrap();
2153 let wall =
2154 SurfaceGeometry::Cylinder(crate::CylinderSurface::new(cylinder, (0.0, 10.0)).unwrap())
2155 .to_bspline(T)
2156 .unwrap();
2157 let ((ua, ub), (va, vb)) = wall.domain();
2158 for (along_u, at_end) in [(true, true), (true, false), (false, true), (false, false)] {
2159 let longer = wall.extended(along_u, at_end, 3.0, 2, T).unwrap();
2160 let ((la, lb), (ma, mb)) = longer.domain();
2161 assert!(
2162 (la <= ua && lb >= ub && ma <= va && mb >= vb)
2163 && ((la < ua) || (lb > ub) || (ma < va) || (mb > vb)),
2164 "the domain grows: {:?}",
2165 longer.domain()
2166 );
2167 for i in 0..=6 {
2168 for j in 0..=6 {
2169 let u = la + (lb - la) * f64::from(i) / 6.0;
2170 let v = ma + (mb - ma) * f64::from(j) / 6.0;
2171 let p = longer.point_at(u, v, T).unwrap();
2172 let radial = (p.x * p.x + p.y * p.y).sqrt();
2173 assert!(
2174 (radial - 5.0).abs() < 1e-9,
2175 "off the cylinder at ({u},{v}): {p:?}"
2176 );
2177 if (ua..=ub).contains(&u) && (va..=vb).contains(&v) {
2178 let was = wall.point_at(u, v, T).unwrap();
2179 assert!(was.distance(p) < 1e-9, "the wall itself at ({u},{v})");
2180 }
2181 }
2182 }
2183 if !along_u {
2184 let reach = if at_end { mb } else { ma };
2185 let p = longer.point_at(ua, reach, T).unwrap();
2186 assert!(
2187 (p.z - if at_end { 13.0 } else { -3.0 }).abs() < 1e-9,
2188 "the axial continuation reaches the length asked: {p:?}"
2189 );
2190 }
2191 }
2192 let fitted = patch();
2193 let ((ua, ub), (va, vb)) = fitted.domain();
2194 let longer = fitted.extended(true, true, 1.0, 2, T).unwrap();
2195 for i in 0..=5 {
2196 for j in 0..=5 {
2197 let u = ua + (ub - ua) * f64::from(i) / 5.0;
2198 let v = va + (vb - va) * f64::from(j) / 5.0;
2199 let (was, now) = (
2200 fitted.point_at(u, v, T).unwrap(),
2201 longer.point_at(u, v, T).unwrap(),
2202 );
2203 assert!(was.distance(now) < 1e-9, "the patch itself at ({u},{v})");
2204 }
2205 }
2206 }
2207
2208 #[test]
2211 fn an_iso_curve_traces_the_surface_exactly() {
2212 use crate::Curve3d as _;
2213 let surface = patch();
2214 let ((ua, ub), (va, vb)) = surface.domain();
2215 for frac in [0.1, 0.5, 0.83] {
2216 let u = ua + (ub - ua) * frac;
2217 let column = surface.iso_u_curve(u, T).unwrap();
2218 let v = va + (vb - va) * frac;
2219 let row = surface.iso_v_curve(v, T).unwrap();
2220 for k in 0..=10 {
2221 let f = f64::from(k) / 10.0;
2222 let vv = va + (vb - va) * f;
2223 let on = surface.point_at(u, vv, T).unwrap();
2224 let along = column.point_at(vv, T).unwrap();
2225 assert!(
2226 on.distance(along) < 1e-9,
2227 "column at u {u}: {on:?} vs {along:?}"
2228 );
2229 let uu = ua + (ub - ua) * f;
2230 let on = surface.point_at(uu, v, T).unwrap();
2231 let along = row.point_at(uu, T).unwrap();
2232 assert!(
2233 on.distance(along) < 1e-9,
2234 "row at v {v}: {on:?} vs {along:?}"
2235 );
2236 }
2237 }
2238 }
2239
2240 fn patch() -> BSplineSurface {
2241 let (nu, nv) = (5, 4);
2242 let mut points = Vec::with_capacity(nu * nv);
2243 for i in 0..nu {
2244 for j in 0..nv {
2245 #[allow(clippy::cast_precision_loss)]
2246 let (x, y) = (i as f64, j as f64);
2247 points.push(Point::new(x, y, (x * 0.7).sin() * (y * 0.5).cos()));
2248 }
2249 }
2250 BSplineSurface::new(
2251 KnotVector::clamped_uniform(3, nu).unwrap(),
2252 KnotVector::clamped_uniform(2, nv).unwrap(),
2253 &ControlGrid::new(points, nu, nv).unwrap(),
2254 T,
2255 )
2256 .unwrap()
2257 }
2258
2259 fn every_surface() -> Vec<SurfaceGeometry> {
2260 let circle: Curve = CircleCurve::new(
2261 Circle::new(
2262 Frame::new(Point::new(5.0, 0.0, 0.0), Direction::Y, Direction::X, T).unwrap(),
2263 1.0,
2264 T,
2265 )
2266 .unwrap(),
2267 )
2268 .into();
2269 let line: Curve =
2270 LineCurve::segment(Point::new(2.0, 0.0, 0.0), Point::new(2.0, 0.0, 4.0), T)
2271 .unwrap()
2272 .into();
2273 vec![
2274 PlaneSurface::over(Plane::new(tilted()), (-5.0, 5.0), (-3.0, 3.0))
2275 .unwrap()
2276 .into(),
2277 CylinderSurface::new(Cylinder::new(tilted(), 2.0, T).unwrap(), (-4.0, 4.0))
2278 .unwrap()
2279 .into(),
2280 ConeSurface::new(Cone::new(tilted(), 3.0, 0.6, T).unwrap(), (-1.0, 5.0))
2281 .unwrap()
2282 .into(),
2283 SphereSurface::new(Sphere::new(tilted(), 4.0, T).unwrap()).into(),
2284 TorusSurface::new(Torus::new(tilted(), 5.0, 2.0, T).unwrap()).into(),
2285 patch().into(),
2286 RevolutionSurface::new(circle, Axis::Z, TAU).unwrap().into(),
2287 ExtrusionSurface::new(line, Direction::X, 3.0)
2288 .unwrap()
2289 .into(),
2290 TrimmedSurface::new(patch().into(), (0.2, 0.8), (0.3, 0.7), T)
2291 .unwrap()
2292 .into(),
2293 ]
2294 }
2295
2296 fn interior(s: &SurfaceGeometry, n: usize) -> Vec<(f64, f64)> {
2304 const OFF: f64 = 0.0413;
2305 let ((ua, ub), (va, vb)) = s.domain();
2306 let mut out = Vec::new();
2307 for i in 1..n {
2308 for j in 1..n {
2309 #[allow(clippy::cast_precision_loss)]
2310 let (tu, tv) = (i as f64 / n as f64 + OFF, j as f64 / n as f64 + OFF);
2311 out.push((ua + (ub - ua) * tu, va + (vb - va) * tv));
2312 }
2313 }
2314 out
2315 }
2316
2317 #[test]
2318 fn every_surfaces_first_partials_agree_with_finite_differences() {
2319 let h = 1e-6;
2320 for s in every_surface() {
2321 for (u, v) in interior(&s, 5) {
2322 let (du, dv) = s.d1_at(u, v, T).unwrap();
2323 let nu = (s.point_at(u + h, v, T).unwrap() - s.point_at(u - h, v, T).unwrap())
2324 * (1.0 / (2.0 * h));
2325 let nv = (s.point_at(u, v + h, T).unwrap() - s.point_at(u, v - h, T).unwrap())
2326 * (1.0 / (2.0 * h));
2327 assert!(
2328 (du - nu).magnitude() <= 1e-5 * nu.magnitude().max(1.0),
2329 "{:?} du at ({u}, {v}): {du:?} vs {nu:?}",
2330 s.kind()
2331 );
2332 assert!(
2333 (dv - nv).magnitude() <= 1e-5 * nv.magnitude().max(1.0),
2334 "{:?} dv at ({u}, {v}): {dv:?} vs {nv:?}",
2335 s.kind()
2336 );
2337 }
2338 }
2339 }
2340
2341 #[test]
2342 fn every_surfaces_second_partials_agree_with_finite_differences() {
2343 let h = 1e-5;
2344 for s in every_surface() {
2345 for (u, v) in interior(&s, 4) {
2346 let (d2u, duv, d2v) = s.d2_at(u, v, T).unwrap();
2347
2348 let nuu = (s.point_at(u + h, v, T).unwrap().to_vector()
2349 - s.point_at(u, v, T).unwrap().to_vector() * 2.0
2350 + s.point_at(u - h, v, T).unwrap().to_vector())
2351 * (1.0 / (h * h));
2352 let nvv = (s.point_at(u, v + h, T).unwrap().to_vector()
2353 - s.point_at(u, v, T).unwrap().to_vector() * 2.0
2354 + s.point_at(u, v - h, T).unwrap().to_vector())
2355 * (1.0 / (h * h));
2356 let nuv = (s.point_at(u + h, v + h, T).unwrap()
2357 - s.point_at(u + h, v - h, T).unwrap()
2358 - (s.point_at(u - h, v + h, T).unwrap()
2359 - s.point_at(u - h, v - h, T).unwrap()))
2360 * (1.0 / (4.0 * h * h));
2361
2362 for (analytic, numeric, name) in
2363 [(d2u, nuu, "d2u"), (duv, nuv, "duv"), (d2v, nvv, "d2v")]
2364 {
2365 assert!(
2366 (analytic - numeric).magnitude() <= 1e-3 * numeric.magnitude().max(1.0),
2367 "{:?} {name} at ({u}, {v}): {analytic:?} vs {numeric:?}",
2368 s.kind()
2369 );
2370 }
2371 }
2372 }
2373 }
2374
2375 #[test]
2376 fn out_of_domain_parameters_are_refused_where_the_surface_is_not_periodic() {
2377 for s in every_surface() {
2378 let ((ua, ub), (va, vb)) = s.domain();
2379 let inside = ((ua + ub) / 2.0, (va + vb) / 2.0);
2380 if s.kind() == SurfaceKind::Plane {
2381 let p = s.point_at(ub + 1.0, inside.1, T).unwrap();
2384 let q = s.point_at(ub, inside.1, T).unwrap();
2385 assert!((p.distance(q) - 1.0).abs() < 1e-12);
2386 continue;
2387 }
2388 if s.is_periodic_u() {
2389 assert!(s.point_at(ub + 1.0, inside.1, T).is_ok(), "{:?}", s.kind());
2390 } else {
2391 assert!(s.point_at(ub + 1.0, inside.1, T).is_err(), "{:?}", s.kind());
2392 }
2393 if s.is_periodic_v() {
2394 assert!(s.point_at(inside.0, vb + 1.0, T).is_ok(), "{:?}", s.kind());
2395 } else {
2396 assert!(s.point_at(inside.0, vb + 1.0, T).is_err(), "{:?}", s.kind());
2397 }
2398 }
2399 }
2400
2401 #[test]
2402 fn periodic_parameters_wrap_to_the_same_point() {
2403 for s in every_surface() {
2404 let ((ua, ub), (va, vb)) = s.domain();
2405 let (u, v) = ((ua + ub) * 0.4, (va + vb) * 0.4);
2406 let base = s.point_at(u, v, T).unwrap();
2407 if s.is_periodic_u() {
2408 let wrapped = s.point_at(u + (ub - ua), v, T).unwrap();
2409 assert!(base.is_equal(wrapped, T), "{:?} u wrap", s.kind());
2410 }
2411 if s.is_periodic_v() {
2412 let wrapped = s.point_at(u, v + (vb - va), T).unwrap();
2413 assert!(base.is_equal(wrapped, T), "{:?} v wrap", s.kind());
2414 }
2415 }
2416 }
2417
2418 #[test]
2419 fn analytic_surfaces_contain_their_own_points() {
2420 let cyl = Cylinder::new(tilted(), 2.0, T).unwrap();
2422 let cone = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
2423 let sph = Sphere::new(tilted(), 4.0, T).unwrap();
2424 let tor = Torus::new(tilted(), 5.0, 2.0, T).unwrap();
2425 let plane = Plane::new(tilted());
2426
2427 let cyl_s = CylinderSurface::new(cyl, (-4.0, 4.0)).unwrap();
2428 let cone_s = ConeSurface::new(cone, (-1.0, 5.0)).unwrap();
2429 let sph_s = SphereSurface::new(sph);
2430 let tor_s = TorusSurface::new(tor);
2431 let plane_s = PlaneSurface::over(plane, (-5.0, 5.0), (-3.0, 3.0)).unwrap();
2432
2433 for i in 1..6 {
2434 for j in 1..6 {
2435 let (tu, tv) = (f64::from(i) / 6.0, f64::from(j) / 6.0);
2436 assert!(
2437 plane.contains(
2438 plane_s
2439 .point_at(-5.0 + 10.0 * tu, -3.0 + 6.0 * tv, T)
2440 .unwrap(),
2441 T
2442 )
2443 );
2444 assert!(cyl.contains(cyl_s.point_at(TAU * tu, -4.0 + 8.0 * tv, T).unwrap(), T));
2445 assert!(cone.contains(cone_s.point_at(TAU * tu, -1.0 + 6.0 * tv, T).unwrap(), T));
2446 let half = core::f64::consts::FRAC_PI_2;
2447 assert!(
2448 sph.contains(
2449 sph_s
2450 .point_at(TAU * tu, -half + core::f64::consts::PI * tv, T)
2451 .unwrap(),
2452 T
2453 )
2454 );
2455 assert!(tor.contains(tor_s.point_at(TAU * tu, TAU * tv, T).unwrap(), T));
2456 }
2457 }
2458 }
2459
2460 #[test]
2461 fn normals_of_analytic_surfaces_match_their_own_definitions() {
2462 let sph = Sphere::new(tilted(), 4.0, T).unwrap();
2463 let s = SphereSurface::new(sph);
2464 for i in 1..6 {
2465 for j in 1..6 {
2466 let u = TAU * f64::from(i) / 6.0;
2467 let v = -1.2 + 2.4 * f64::from(j) / 6.0;
2468 let p = s.point_at(u, v, T).unwrap();
2469 assert!(
2470 s.normal_at(u, v, T)
2471 .unwrap()
2472 .is_equal(sph.normal_at(p, T).unwrap(), T)
2473 );
2474 }
2475 }
2476 }
2477
2478 #[test]
2479 fn a_sphere_degenerates_at_its_poles_and_says_so() {
2480 let s = SphereSurface::new(Sphere::new(tilted(), 4.0, T).unwrap());
2481 let half = core::f64::consts::FRAC_PI_2;
2482 for pole in [-half, half] {
2483 assert!(s.is_degenerate_at(1.0, pole, T).unwrap());
2484 assert!(s.normal_at(1.0, pole, T).is_err());
2485 }
2486 assert!(!s.is_degenerate_at(1.0, 0.0, T).unwrap());
2487 assert!(s.normal_at(1.0, 0.0, T).is_ok());
2488 }
2489
2490 #[test]
2491 fn a_cone_degenerates_at_its_apex() {
2492 let cone = Cone::new(tilted(), 3.0, 0.6, T).unwrap();
2493 let apex_height = -3.0 / 0.6_f64.tan();
2494 let s = ConeSurface::new(cone, (apex_height, 5.0)).unwrap();
2495 assert!(s.is_degenerate_at(1.0, apex_height, T).unwrap());
2496 assert!(s.normal_at(1.0, apex_height, T).is_err());
2497 assert!(
2498 s.point_at(1.0, apex_height, T)
2499 .unwrap()
2500 .is_equal(cone.apex(), T)
2501 );
2502 }
2503
2504 #[test]
2505 fn revolving_a_circle_about_an_offset_axis_gives_a_torus() {
2506 let major = 5.0;
2509 let minor = 1.0;
2510 let generator: Curve = CircleCurve::new(
2517 Circle::new(
2518 Frame::new(Point::new(major, 0.0, 0.0), -Direction::Y, Direction::X, T).unwrap(),
2519 minor,
2520 T,
2521 )
2522 .unwrap(),
2523 )
2524 .into();
2525 let revolved = RevolutionSurface::new(generator, Axis::Z, TAU).unwrap();
2526 let torus = TorusSurface::new(Torus::new(Frame::WORLD, major, minor, T).unwrap());
2527
2528 for i in 0..12 {
2529 for j in 0..12 {
2530 let u = TAU * f64::from(i) / 12.0;
2531 let v = TAU * f64::from(j) / 12.0;
2532 let a = revolved.point_at(u, v, T).unwrap();
2533 let b = torus.point_at(u, v, T).unwrap();
2534 assert!(a.is_equal(b, T), "at ({u}, {v}): {a:?} vs {b:?}");
2535 }
2536 }
2537 }
2538
2539 #[test]
2540 fn extruding_a_line_gives_a_plane_and_a_circle_gives_a_cylinder() {
2541 let line: Curve = LineCurve::segment(Point::ORIGIN, Point::new(0.0, 4.0, 0.0), T)
2542 .unwrap()
2543 .into();
2544 let sheet = ExtrusionSurface::new(line, Direction::Z, 3.0).unwrap();
2545 let plane = Plane::new(Frame::WORLD);
2546 for i in 0..=4 {
2547 for j in 0..=4 {
2548 let p = sheet
2549 .point_at(4.0 * f64::from(i) / 4.0, 3.0 * f64::from(j) / 4.0, T)
2550 .unwrap();
2551 assert!(plane.distance_to(p) < 1e-12 || p.x.abs() < 1e-12);
2552 }
2553 }
2554
2555 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2556 let tube = ExtrusionSurface::new(circle, Direction::Z, 5.0).unwrap();
2557 let cylinder = Cylinder::new(Frame::WORLD, 2.0, T).unwrap();
2558 for i in 0..8 {
2559 for j in 0..=4 {
2560 let p = tube
2561 .point_at(TAU * f64::from(i) / 8.0, 5.0 * f64::from(j) / 4.0, T)
2562 .unwrap();
2563 assert!(cylinder.contains(p, T));
2564 }
2565 }
2566 }
2567
2568 #[test]
2569 fn degenerate_constructions_are_refused() {
2570 assert!(PlaneSurface::over(Plane::new(tilted()), (1.0, 1.0), (0.0, 1.0)).is_err());
2571 assert!(
2572 CylinderSurface::new(Cylinder::new(tilted(), 1.0, T).unwrap(), (2.0, 1.0)).is_err()
2573 );
2574 let circle: Curve = CircleCurve::new(Circle::new(tilted(), 1.0, T).unwrap()).into();
2575 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, 0.0).is_err());
2576 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, 7.0).is_err());
2577 assert!(RevolutionSurface::new(circle.clone(), Axis::Z, TAU).is_ok());
2578 assert!(ExtrusionSurface::new(circle.clone(), Direction::Z, 0.0).is_err());
2579 assert!(ExtrusionSurface::new(circle, Direction::Z, f64::NAN).is_err());
2580 }
2581
2582 #[test]
2583 fn trimming_is_bounds_checked_and_agrees_with_its_basis() {
2584 let basis: SurfaceGeometry = patch().into();
2585 assert!(TrimmedSurface::new(basis.clone(), (0.2, 0.8), (0.3, 0.7), T).is_ok());
2586 assert!(TrimmedSurface::new(basis.clone(), (0.8, 0.2), (0.3, 0.7), T).is_err());
2587 assert!(TrimmedSurface::new(basis.clone(), (-0.5, 0.8), (0.3, 0.7), T).is_err());
2588
2589 let trimmed = TrimmedSurface::new(basis.clone(), (0.2, 0.8), (0.3, 0.7), T).unwrap();
2590 assert_eq!(trimmed.domain(), ((0.2, 0.8), (0.3, 0.7)));
2591 for i in 0..=4 {
2592 for j in 0..=4 {
2593 let u = 0.2 + 0.6 * f64::from(i) / 4.0;
2594 let v = 0.3 + 0.4 * f64::from(j) / 4.0;
2595 assert!(
2596 trimmed
2597 .point_at(u, v, T)
2598 .unwrap()
2599 .is_equal(basis.point_at(u, v, T).unwrap(), T)
2600 );
2601 }
2602 }
2603 assert!(trimmed.point_at(0.1, 0.5, T).is_err());
2604 }
2605
2606 #[test]
2607 fn transforms_move_surfaces_and_preserve_their_kind() {
2608 let t =
2609 Transform::rotation(Axis::X, 0.7) * Transform::translation(Vector::new(1.0, 2.0, 3.0));
2610 for s in every_surface() {
2611 let moved = s.transformed(&t, T).unwrap();
2612 assert_eq!(moved.kind(), s.kind());
2613 for (u, v) in interior(&s, 4) {
2614 let expected = t.apply(s.point_at(u, v, T).unwrap());
2615 assert!(
2616 moved.point_at(u, v, T).unwrap().is_equal(expected, T),
2617 "{:?} at ({u}, {v})",
2618 s.kind()
2619 );
2620 }
2621 }
2622 }
2623
2624 #[test]
2625 fn a_scaling_rescales_length_valued_parameters() {
2626 let s: SurfaceGeometry =
2629 CylinderSurface::new(Cylinder::new(Frame::WORLD, 2.0, T).unwrap(), (0.0, 4.0))
2630 .unwrap()
2631 .into();
2632 let scaled = s
2633 .transformed(&Transform::scaling(Point::ORIGIN, 3.0, T).unwrap(), T)
2634 .unwrap();
2635 assert_eq!(scaled.domain(), ((0.0, TAU), (0.0, 12.0)));
2636 assert!(
2637 scaled
2638 .point_at(0.0, 12.0, T)
2639 .unwrap()
2640 .is_equal(Point::new(6.0, 0.0, 12.0), T)
2641 );
2642 }
2643
2644 #[test]
2645 fn a_rational_patch_is_recognized_and_a_uniformly_weighted_one_is_not() {
2646 let g = patch();
2647 assert!(!g.is_rational());
2648
2649 let w = core::f64::consts::FRAC_1_SQRT_2;
2650 let points: Vec<_> = [
2651 (Point::new(1.0, 0.0, 0.0), 1.0),
2652 (Point::new(1.0, 1.0, 0.0), w),
2653 (Point::new(0.0, 1.0, 0.0), 1.0),
2654 (Point::new(1.0, 0.0, 1.0), 1.0),
2655 (Point::new(1.0, 1.0, 1.0), w),
2656 (Point::new(0.0, 1.0, 1.0), 1.0),
2657 ]
2658 .iter()
2659 .map(|(p, w)| Weighted::new(*p, *w, T).unwrap())
2660 .collect();
2661 let arc = BSplineSurface::rational(
2662 KnotVector::clamped_uniform(1, 2).unwrap(),
2663 KnotVector::clamped_uniform(2, 3).unwrap(),
2664 ControlGrid::new(points, 2, 3).unwrap(),
2665 )
2666 .unwrap();
2667 assert!(arc.is_rational());
2668 for i in 0..=4 {
2670 for j in 0..=8 {
2671 let p = arc
2672 .point_at(f64::from(i) / 4.0, f64::from(j) / 8.0, T)
2673 .unwrap();
2674 assert_relative_eq!(p.x.hypot(p.y), 1.0, epsilon = 1e-14);
2675 }
2676 }
2677 }
2678
2679 #[test]
2680 fn closure_and_periodicity_are_reported_correctly() {
2681 let s = every_surface();
2682 let expect = [
2683 (SurfaceKind::Plane, false, false, false, false),
2684 (SurfaceKind::Cylinder, true, false, true, false),
2685 (SurfaceKind::Cone, true, false, true, false),
2686 (SurfaceKind::Sphere, true, false, true, false),
2687 (SurfaceKind::Torus, true, true, true, true),
2688 (SurfaceKind::BSpline, false, false, false, false),
2689 (SurfaceKind::Revolution, true, true, true, true),
2690 (SurfaceKind::Extrusion, false, false, false, false),
2691 (SurfaceKind::Trimmed, false, false, false, false),
2692 ];
2693 for (surface, (kind, cu, cv, pu, pv)) in s.iter().zip(expect) {
2694 assert_eq!(surface.kind(), kind);
2695 assert_eq!(surface.is_closed_u(T), cu, "{kind:?} closed u");
2696 assert_eq!(surface.is_closed_v(T), cv, "{kind:?} closed v");
2697 assert_eq!(surface.is_periodic_u(), pu, "{kind:?} periodic u");
2698 assert_eq!(surface.is_periodic_v(), pv, "{kind:?} periodic v");
2699 }
2700 }
2701
2702 #[test]
2703 fn a_sphere_is_not_periodic_in_latitude() {
2704 let s = SphereSurface::new(Sphere::new(Frame::WORLD, 1.0, T).unwrap());
2707 assert!(!s.is_periodic_v());
2708 assert!(s.point_at(0.0, core::f64::consts::PI, T).is_err());
2709 }
2710}