use crate::curves::{Continuity, Curve};
use crate::surfaces::{BSplineSurface, Cone, Cylinder, Plane, Sphere, SurfaceIsoError, Torus};
use crate::{Point, SurfaceProjection, Tolerance, Vector};
use std::f64::consts::{FRAC_PI_2, TAU};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceCurvatures {
pub min_curvature: f64,
pub max_curvature: f64,
pub min_direction: Vector,
pub max_direction: Vector,
pub mean: f64,
pub gaussian: f64,
}
pub trait ParametricSurface {
fn eval_point(&self, u: f64, v: f64) -> Point;
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector;
fn eval_d1(&self, u: f64, v: f64) -> (Point, [Vector; 2]) {
(
self.eval_point(u, v),
[
self.eval_derivative(u, v, 1, 0),
self.eval_derivative(u, v, 0, 1),
],
)
}
fn eval_d2(&self, u: f64, v: f64) -> (Point, [Vector; 2], [Vector; 3]) {
(
self.eval_point(u, v),
[
self.eval_derivative(u, v, 1, 0),
self.eval_derivative(u, v, 0, 1),
],
[
self.eval_derivative(u, v, 2, 0),
self.eval_derivative(u, v, 1, 1),
self.eval_derivative(u, v, 0, 2),
],
)
}
fn normal(&self, u: f64, v: f64, tol: Tolerance) -> Option<Vector> {
let bounds = [self.u_bounds(), self.v_bounds()];
match normal_recovery(self, u, v, bounds, tol) {
Recovery::Unique(normal) => return Some(normal),
Recovery::Disputed => return None,
Recovery::Absent => {}
}
const STEP_FRACTIONS: [f64; 4] = [1e-7, 1e-6, 1e-5, 1e-4];
const DIAGONALS: [(f64, f64); 4] = [(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)];
let [u_width, v_width] = bounds.map(|range| tol.reference_width(range));
let (u_period, v_period) = (self.u_period(), self.v_period());
for fraction in STEP_FRACTIONS {
for (u_sign, v_sign) in DIAGONALS {
let (Some(stepped_u), Some(stepped_v)) = (
shift_parameter(u, u_sign * fraction * u_width, bounds[0], u_period),
shift_parameter(v, v_sign * fraction * v_width, bounds[1], v_period),
) else {
continue;
};
if let Some(normal) =
normal_from_derivatives(self, stepped_u, stepped_v, bounds, tol)
{
return Some(normal);
}
}
}
None
}
fn u_bounds(&self) -> (f64, f64);
fn v_bounds(&self) -> (f64, f64);
fn u_period(&self) -> Option<f64>;
fn v_period(&self) -> Option<f64>;
fn is_u_closed(&self, tol: Tolerance) -> bool {
let _ = tol;
self.u_period().is_some()
}
fn is_v_closed(&self, tol: Tolerance) -> bool {
let _ = tol;
self.v_period().is_some()
}
fn continuity_u(&self, u: f64, tol: Tolerance) -> Continuity {
let _ = (u, tol);
Continuity::Smooth
}
fn continuity_v(&self, v: f64, tol: Tolerance) -> Continuity {
let _ = (v, tol);
Continuity::Smooth
}
fn min_continuity_u(&self) -> Continuity {
Continuity::Smooth
}
fn min_continuity_v(&self) -> Continuity {
Continuity::Smooth
}
fn curvatures(&self, u: f64, v: f64, tol: Tolerance) -> Option<SurfaceCurvatures> {
let (_, [su, sv], [suu, suv, svv]) = self.eval_d2(u, v);
let (su_length, sv_length) = (su.magnitude(), sv.magnitude());
let cross = su.cross(sv);
let cross_length = cross.magnitude();
let widths = reference_widths(self, tol);
if !partials_span_a_plane(su_length, sv_length, cross_length, widths, tol) {
return None;
}
let normal = cross * (1.0 / cross_length);
let first = su * (1.0 / su_length);
let second = normal.cross(first);
let first_du = 1.0 / su_length;
let second_du = -su.dot(sv) / (su_length * cross_length);
let second_dv = su_length / cross_length;
let (form_uu, form_uv, form_vv) = (suu.dot(normal), suv.dot(normal), svv.dot(normal));
let a = form_uu * first_du * first_du;
let b = first_du * (form_uu * second_du + form_uv * second_dv);
let c = form_uu * second_du * second_du
+ 2.0 * form_uv * second_du * second_dv
+ form_vv * second_dv * second_dv;
let mean = 0.5 * (a + c);
let half_gap = (0.5 * (a - c)).hypot(b);
let (min_curvature, max_curvature) = (mean - half_gap, mean + half_gap);
let offset = 0.5 * (a - c).abs() + half_gap;
let ((min_x, min_y), (max_x, max_y)) = if offset == 0.0 {
((1.0, 0.0), (0.0, 1.0))
} else if a >= c {
((-b, offset), (offset, b))
} else {
((offset, -b), (b, offset))
};
let scale = 1.0 / min_x.hypot(min_y);
Some(SurfaceCurvatures {
min_curvature,
max_curvature,
min_direction: (first * min_x + second * min_y) * scale,
max_direction: (first * max_x + second * max_y) * scale,
mean,
gaussian: a * c - b * b,
})
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
uvs.iter().map(|&(u, v)| self.eval_point(u, v)).collect()
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection;
fn contains(&self, point: Point, tol: Tolerance) -> bool;
fn as_bspline(&self) -> Option<&BSplineSurface> {
None
}
fn as_elementary(&self) -> Option<ElementarySurface> {
None
}
}
enum Recovery {
Unique(Vector),
Absent,
Disputed,
}
fn normal_recovery<S: ParametricSurface + ?Sized>(
surface: &S,
u: f64,
v: f64,
bounds: [(f64, f64); 2],
tol: Tolerance,
) -> Recovery {
let [u_width, v_width] = bounds.map(|range| tol.reference_width(range));
let (point, [su, sv]) = surface.eval_d1(u, v);
if let Some(normal) = direct_normal(su, sv, u_width, v_width, tol) {
return Recovery::Unique(normal);
}
let (_, _, [suu, suv, svv]) = surface.eval_d2(u, v);
let su_vanishes = tol.is_negligible_derivative(su.magnitude(), u_width);
let sv_vanishes = tol.is_negligible_derivative(sv.magnitude(), v_width);
let su_on_line = su_vanishes && lies_on_collapsed_line(point, su, suv, u_width, v, bounds[1]);
let sv_on_line = sv_vanishes && lies_on_collapsed_line(point, sv, suv, v_width, u, bounds[0]);
let measured_signs = [su_vanishes && !su_on_line, sv_vanishes && !sv_on_line];
let Some(candidate) = substituted_normal(
su,
sv,
suu,
suv,
svv,
u,
v,
bounds,
measured_signs,
u_width,
v_width,
tol,
) else {
return Recovery::Absent;
};
for (on_line, direction) in [(su_on_line, Direction::U), (sv_on_line, Direction::V)] {
if on_line && !transverse_agreement(surface, u, v, direction, bounds, candidate, tol) {
return Recovery::Disputed;
}
}
Recovery::Unique(candidate)
}
fn normal_from_derivatives<S: ParametricSurface + ?Sized>(
surface: &S,
u: f64,
v: f64,
bounds: [(f64, f64); 2],
tol: Tolerance,
) -> Option<Vector> {
match normal_recovery(surface, u, v, bounds, tol) {
Recovery::Unique(normal) => Some(normal),
Recovery::Absent | Recovery::Disputed => None,
}
}
fn direct_normal(
su: Vector,
sv: Vector,
u_width: f64,
v_width: f64,
tol: Tolerance,
) -> Option<Vector> {
let (su_length, sv_length) = (su.magnitude(), sv.magnitude());
let cross = su.cross(sv);
let cross_length = cross.magnitude();
partials_span_a_plane(su_length, sv_length, cross_length, [u_width, v_width], tol)
.then(|| cross * (1.0 / cross_length))
}
pub(crate) fn reference_widths<S: ParametricSurface + ?Sized>(
surface: &S,
tol: Tolerance,
) -> [f64; 2] {
[
tol.reference_width(surface.u_bounds()),
tol.reference_width(surface.v_bounds()),
]
}
#[inline]
pub(crate) fn partials_span_a_plane(
su_length: f64,
sv_length: f64,
cross_length: f64,
[u_width, v_width]: [f64; 2],
tol: Tolerance,
) -> bool {
!tol.is_negligible_derivative(su_length, u_width)
&& !tol.is_negligible_derivative(sv_length, v_width)
&& cross_length > tol.angular * su_length * sv_length
}
#[allow(clippy::too_many_arguments)]
fn substituted_normal(
su: Vector,
sv: Vector,
suu: Vector,
suv: Vector,
svv: Vector,
u: f64,
v: f64,
bounds: [(f64, f64); 2],
measured_signs: [bool; 2],
u_width: f64,
v_width: f64,
tol: Tolerance,
) -> Option<Vector> {
let orientation = |x: f64, (first, last): (f64, f64)| {
if x > 0.5 * (first + last) { -1.0 } else { 1.0 }
};
let (u_orientation, v_orientation) = (orientation(u, bounds[0]), orientation(v, bounds[1]));
let sign = |partial: Vector, substitute: Vector, measured: bool, guess: f64| {
let alignment = partial.dot(substitute);
if measured && alignment != 0.0 {
alignment.signum()
} else {
guess
}
};
let [su_measured, sv_measured] = measured_signs;
let [su_by_suv, sv_by_suv, su_by_suu, sv_by_svv] = [
sign(su, suv, su_measured, v_orientation),
sign(sv, suv, sv_measured, u_orientation),
sign(su, suu, su_measured, u_orientation),
sign(sv, svv, sv_measured, v_orientation),
];
let direction = |derivative: Vector, reach: f64| {
let length = derivative.magnitude();
(!tol.is_negligible_derivative(length, reach)).then(|| derivative * (1.0 / length))
};
let su = direction(su, u_width);
let sv = direction(sv, v_width);
let suu = direction(suu, u_width * u_width);
let suv = direction(suv, u_width * v_width);
let svv = direction(svv, v_width * v_width);
let pairs = [
(suv, sv, su_by_suv),
(su, suv, sv_by_suv),
(suu, sv, su_by_suu),
(su, svv, sv_by_svv),
(suu, suv, 1.0),
(suv, svv, 1.0),
(suu, svv, u_orientation * v_orientation),
];
for (first, second, sign) in pairs {
if let (Some(first), Some(second)) = (first, second) {
let cross = first.cross(second);
let cross_length = cross.magnitude();
if cross_length > tol.angular {
return Some(cross * (sign / cross_length));
}
}
}
None
}
#[derive(Clone, Copy)]
enum Direction {
U,
V,
}
fn lies_on_collapsed_line(
point: Point,
partial: Vector,
mixed: Vector,
width: f64,
across: f64,
(across_first, across_last): (f64, f64),
) -> bool {
const ROUNDING: f64 = 64.0 * f64::EPSILON;
let coordinates = point.x.abs().max(point.y.abs()).max(point.z.abs());
let finite_magnitude = |bound: f64| if bound.is_finite() { bound.abs() } else { 0.0 };
let across_magnitude = across
.abs()
.max(finite_magnitude(across_first))
.max(finite_magnitude(across_last));
let rounding = ROUNDING * (coordinates + mixed.magnitude() * width * across_magnitude);
partial.magnitude() * width <= rounding
}
fn transverse_agreement<S: ParametricSurface + ?Sized>(
surface: &S,
u: f64,
v: f64,
direction: Direction,
bounds: [(f64, f64); 2],
candidate: Vector,
tol: Tolerance,
) -> bool {
let agreement = tol.angular.sqrt();
let (first, last) = match direction {
Direction::U => bounds[0],
Direction::V => bounds[1],
};
if !(first.is_finite() && last.is_finite() && last > first) {
return true;
}
let [u_width, v_width] = bounds.map(|range| tol.reference_width(range));
for fraction in [1.0 / 3.0, 2.0 / 3.0] {
let other = first + (last - first) * fraction;
let (other_u, other_v) = match direction {
Direction::U => (other, v),
Direction::V => (u, other),
};
let (_, [su, sv]) = surface.eval_d1(other_u, other_v);
if direct_normal(su, sv, u_width, v_width, tol).is_some() {
return true;
}
let (_, _, [suu, suv, svv]) = surface.eval_d2(other_u, other_v);
if let Some(recovered) = substituted_normal(
su,
sv,
suu,
suv,
svv,
other_u,
other_v,
bounds,
[false, false],
u_width,
v_width,
tol,
) && (recovered - candidate).magnitude() > agreement
{
return false;
}
}
true
}
fn shift_parameter(
parameter: f64,
step: f64,
bounds: (f64, f64),
period: Option<f64>,
) -> Option<f64> {
let shifted = parameter + step;
if let Some(period) = period {
Some(bounds.0 + (shifted - bounds.0).rem_euclid(period))
} else {
(bounds.0 <= shifted && shifted <= bounds.1).then_some(shifted)
}
}
impl ParametricSurface for Plane {
fn eval_point(&self, u: f64, v: f64) -> Point {
Plane::eval_point(self, u, v)
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
Plane::project_point(self, point, tol)
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
Plane::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
Plane::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
(f64::NEG_INFINITY, f64::INFINITY)
}
fn v_bounds(&self) -> (f64, f64) {
(f64::NEG_INFINITY, f64::INFINITY)
}
fn u_period(&self) -> Option<f64> {
None
}
fn v_period(&self) -> Option<f64> {
None
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
Plane::eval_points(self, uvs)
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(ElementarySurface::Plane(*self))
}
}
impl ParametricSurface for Cylinder {
fn eval_point(&self, u: f64, v: f64) -> Point {
Cylinder::eval_point(self, u, v)
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
Cylinder::project_point(self, point, tol)
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
Cylinder::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
Cylinder::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
(0.0, TAU)
}
fn v_bounds(&self) -> (f64, f64) {
(f64::NEG_INFINITY, f64::INFINITY)
}
fn u_period(&self) -> Option<f64> {
Some(TAU)
}
fn v_period(&self) -> Option<f64> {
None
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
Cylinder::eval_points(self, uvs)
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(ElementarySurface::Cylinder(*self))
}
}
impl ParametricSurface for Cone {
fn eval_point(&self, u: f64, v: f64) -> Point {
Cone::eval_point(self, u, v)
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
Cone::project_point(self, point, tol)
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
Cone::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
Cone::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
(0.0, TAU)
}
fn v_bounds(&self) -> (f64, f64) {
(f64::NEG_INFINITY, f64::INFINITY)
}
fn u_period(&self) -> Option<f64> {
Some(TAU)
}
fn v_period(&self) -> Option<f64> {
None
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
Cone::eval_points(self, uvs)
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(ElementarySurface::Cone(*self))
}
}
impl ParametricSurface for Sphere {
fn eval_point(&self, u: f64, v: f64) -> Point {
Sphere::eval_point(self, u, v)
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
Sphere::project_point(self, point, tol)
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
Sphere::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
Sphere::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
(0.0, TAU)
}
fn v_bounds(&self) -> (f64, f64) {
(-FRAC_PI_2, FRAC_PI_2)
}
fn u_period(&self) -> Option<f64> {
Some(TAU)
}
fn v_period(&self) -> Option<f64> {
None
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
Sphere::eval_points(self, uvs)
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(ElementarySurface::Sphere(*self))
}
}
impl ParametricSurface for Torus {
fn eval_point(&self, u: f64, v: f64) -> Point {
Torus::eval_point(self, u, v)
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
Torus::project_point(self, point, tol)
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
Torus::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
Torus::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
(0.0, TAU)
}
fn v_bounds(&self) -> (f64, f64) {
(0.0, TAU)
}
fn u_period(&self) -> Option<f64> {
Some(TAU)
}
fn v_period(&self) -> Option<f64> {
Some(TAU)
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
Torus::eval_points(self, uvs)
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(ElementarySurface::Torus(*self))
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Surface {
Plane(Plane),
Cylinder(Cylinder),
Cone(Cone),
Sphere(Sphere),
Torus(Torus),
BSpline(BSplineSurface),
}
impl Surface {
pub fn u_iso(&self, u: f64) -> Result<Curve, SurfaceIsoError> {
Ok(match self {
Surface::Plane(s) => Curve::Line(s.u_iso(u)?),
Surface::Cylinder(s) => Curve::Line(s.u_iso(u)?),
Surface::Cone(s) => Curve::Line(s.u_iso(u)?),
Surface::Sphere(s) => Curve::Circle(s.u_iso(u)?),
Surface::Torus(s) => Curve::Circle(s.u_iso(u)?),
Surface::BSpline(s) => Curve::BSpline(s.u_iso(u)?),
})
}
pub fn v_iso(&self, v: f64) -> Result<Curve, SurfaceIsoError> {
Ok(match self {
Surface::Plane(s) => Curve::Line(s.v_iso(v)?),
Surface::Cylinder(s) => Curve::Circle(s.v_iso(v)?),
Surface::Cone(s) => Curve::Circle(s.v_iso(v)?),
Surface::Sphere(s) => Curve::Circle(s.v_iso(v)?),
Surface::Torus(s) => Curve::Circle(s.v_iso(v)?),
Surface::BSpline(s) => Curve::BSpline(s.v_iso(v)?),
})
}
pub fn reversed_u(&self) -> Surface {
match self {
Surface::Plane(s) => Surface::Plane(s.reversed_u()),
Surface::Cylinder(s) => Surface::Cylinder(s.reversed_u()),
Surface::Cone(s) => Surface::Cone(s.reversed_u()),
Surface::Sphere(s) => Surface::Sphere(s.reversed_u()),
Surface::Torus(s) => Surface::Torus(s.reversed_u()),
Surface::BSpline(s) => Surface::BSpline(s.reversed_u()),
}
}
pub fn reversed_v(&self) -> Surface {
match self {
Surface::Plane(s) => Surface::Plane(s.reversed_v()),
Surface::Cylinder(s) => Surface::Cylinder(s.reversed_v()),
Surface::Cone(s) => Surface::Cone(s.reversed_v()),
Surface::Sphere(s) => Surface::Sphere(s.reversed_v()),
Surface::Torus(s) => Surface::Torus(s.reversed_v()),
Surface::BSpline(s) => Surface::BSpline(s.reversed_v()),
}
}
pub fn u_reversed_parameter(&self, u: f64) -> f64 {
match self {
Surface::Plane(s) => s.u_reversed_parameter(u),
Surface::Cylinder(s) => s.u_reversed_parameter(u),
Surface::Cone(s) => s.u_reversed_parameter(u),
Surface::Sphere(s) => s.u_reversed_parameter(u),
Surface::Torus(s) => s.u_reversed_parameter(u),
Surface::BSpline(s) => s.u_reversed_parameter(u),
}
}
pub fn v_reversed_parameter(&self, v: f64) -> f64 {
match self {
Surface::Plane(s) => s.v_reversed_parameter(v),
Surface::Cylinder(s) => s.v_reversed_parameter(v),
Surface::Cone(s) => s.v_reversed_parameter(v),
Surface::Sphere(s) => s.v_reversed_parameter(v),
Surface::Torus(s) => s.v_reversed_parameter(v),
Surface::BSpline(s) => s.v_reversed_parameter(v),
}
}
}
impl ParametricSurface for Surface {
fn eval_point(&self, u: f64, v: f64) -> Point {
match self {
Surface::Plane(s) => s.eval_point(u, v),
Surface::Cylinder(s) => s.eval_point(u, v),
Surface::Cone(s) => s.eval_point(u, v),
Surface::Sphere(s) => s.eval_point(u, v),
Surface::Torus(s) => s.eval_point(u, v),
Surface::BSpline(s) => s.eval_point(u, v),
}
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
match self {
Surface::Plane(s) => s.eval_derivative(u, v, du, dv),
Surface::Cylinder(s) => s.eval_derivative(u, v, du, dv),
Surface::Cone(s) => s.eval_derivative(u, v, du, dv),
Surface::Sphere(s) => s.eval_derivative(u, v, du, dv),
Surface::Torus(s) => s.eval_derivative(u, v, du, dv),
Surface::BSpline(s) => s.eval_derivative(u, v, du, dv),
}
}
fn eval_d1(&self, u: f64, v: f64) -> (Point, [Vector; 2]) {
match self {
Surface::Plane(s) => ParametricSurface::eval_d1(s, u, v),
Surface::Cylinder(s) => ParametricSurface::eval_d1(s, u, v),
Surface::Cone(s) => ParametricSurface::eval_d1(s, u, v),
Surface::Sphere(s) => ParametricSurface::eval_d1(s, u, v),
Surface::Torus(s) => ParametricSurface::eval_d1(s, u, v),
Surface::BSpline(s) => s.eval_d1(u, v),
}
}
fn eval_d2(&self, u: f64, v: f64) -> (Point, [Vector; 2], [Vector; 3]) {
match self {
Surface::Plane(s) => ParametricSurface::eval_d2(s, u, v),
Surface::Cylinder(s) => ParametricSurface::eval_d2(s, u, v),
Surface::Cone(s) => ParametricSurface::eval_d2(s, u, v),
Surface::Sphere(s) => ParametricSurface::eval_d2(s, u, v),
Surface::Torus(s) => ParametricSurface::eval_d2(s, u, v),
Surface::BSpline(s) => s.eval_d2(u, v),
}
}
fn u_bounds(&self) -> (f64, f64) {
match self {
Surface::Plane(s) => s.u_bounds(),
Surface::Cylinder(s) => s.u_bounds(),
Surface::Cone(s) => s.u_bounds(),
Surface::Sphere(s) => s.u_bounds(),
Surface::Torus(s) => s.u_bounds(),
Surface::BSpline(s) => s.u_bounds(),
}
}
fn v_bounds(&self) -> (f64, f64) {
match self {
Surface::Plane(s) => s.v_bounds(),
Surface::Cylinder(s) => s.v_bounds(),
Surface::Cone(s) => s.v_bounds(),
Surface::Sphere(s) => s.v_bounds(),
Surface::Torus(s) => s.v_bounds(),
Surface::BSpline(s) => s.v_bounds(),
}
}
fn u_period(&self) -> Option<f64> {
match self {
Surface::Plane(s) => s.u_period(),
Surface::Cylinder(s) => s.u_period(),
Surface::Cone(s) => s.u_period(),
Surface::Sphere(s) => s.u_period(),
Surface::Torus(s) => s.u_period(),
Surface::BSpline(s) => s.u_period(),
}
}
fn v_period(&self) -> Option<f64> {
match self {
Surface::Plane(s) => s.v_period(),
Surface::Cylinder(s) => s.v_period(),
Surface::Cone(s) => s.v_period(),
Surface::Sphere(s) => s.v_period(),
Surface::Torus(s) => s.v_period(),
Surface::BSpline(s) => s.v_period(),
}
}
fn is_u_closed(&self, tol: Tolerance) -> bool {
match self {
Surface::Plane(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Cylinder(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Cone(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Sphere(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Torus(s) => ParametricSurface::is_u_closed(s, tol),
Surface::BSpline(s) => s.is_u_closed(tol),
}
}
fn is_v_closed(&self, tol: Tolerance) -> bool {
match self {
Surface::Plane(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Cylinder(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Cone(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Sphere(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Torus(s) => ParametricSurface::is_v_closed(s, tol),
Surface::BSpline(s) => s.is_v_closed(tol),
}
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
match self {
Surface::Plane(s) => s.project_point(point, tol),
Surface::Cylinder(s) => s.project_point(point, tol),
Surface::Cone(s) => s.project_point(point, tol),
Surface::Sphere(s) => s.project_point(point, tol),
Surface::Torus(s) => s.project_point(point, tol),
Surface::BSpline(s) => s.project_point(point, tol),
}
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
match self {
Surface::Plane(s) => s.contains(point, tol),
Surface::Cylinder(s) => s.contains(point, tol),
Surface::Cone(s) => s.contains(point, tol),
Surface::Sphere(s) => s.contains(point, tol),
Surface::Torus(s) => s.contains(point, tol),
Surface::BSpline(s) => s.contains(point, tol),
}
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
match self {
Surface::Plane(s) => s.eval_points(uvs),
Surface::Cylinder(s) => s.eval_points(uvs),
Surface::Cone(s) => s.eval_points(uvs),
Surface::Sphere(s) => s.eval_points(uvs),
Surface::Torus(s) => s.eval_points(uvs),
Surface::BSpline(s) => s.eval_points(uvs),
}
}
fn continuity_u(&self, u: f64, tol: Tolerance) -> Continuity {
match self {
Surface::Plane(s) => ParametricSurface::continuity_u(s, u, tol),
Surface::Cylinder(s) => ParametricSurface::continuity_u(s, u, tol),
Surface::Cone(s) => ParametricSurface::continuity_u(s, u, tol),
Surface::Sphere(s) => ParametricSurface::continuity_u(s, u, tol),
Surface::Torus(s) => ParametricSurface::continuity_u(s, u, tol),
Surface::BSpline(s) => s.continuity_u(u, tol),
}
}
fn continuity_v(&self, v: f64, tol: Tolerance) -> Continuity {
match self {
Surface::Plane(s) => ParametricSurface::continuity_v(s, v, tol),
Surface::Cylinder(s) => ParametricSurface::continuity_v(s, v, tol),
Surface::Cone(s) => ParametricSurface::continuity_v(s, v, tol),
Surface::Sphere(s) => ParametricSurface::continuity_v(s, v, tol),
Surface::Torus(s) => ParametricSurface::continuity_v(s, v, tol),
Surface::BSpline(s) => s.continuity_v(v, tol),
}
}
fn min_continuity_u(&self) -> Continuity {
match self {
Surface::Plane(s) => ParametricSurface::min_continuity_u(s),
Surface::Cylinder(s) => ParametricSurface::min_continuity_u(s),
Surface::Cone(s) => ParametricSurface::min_continuity_u(s),
Surface::Sphere(s) => ParametricSurface::min_continuity_u(s),
Surface::Torus(s) => ParametricSurface::min_continuity_u(s),
Surface::BSpline(s) => s.min_continuity_u(),
}
}
fn min_continuity_v(&self) -> Continuity {
match self {
Surface::Plane(s) => ParametricSurface::min_continuity_v(s),
Surface::Cylinder(s) => ParametricSurface::min_continuity_v(s),
Surface::Cone(s) => ParametricSurface::min_continuity_v(s),
Surface::Sphere(s) => ParametricSurface::min_continuity_v(s),
Surface::Torus(s) => ParametricSurface::min_continuity_v(s),
Surface::BSpline(s) => s.min_continuity_v(),
}
}
fn curvatures(&self, u: f64, v: f64, tol: Tolerance) -> Option<SurfaceCurvatures> {
match self {
Surface::Plane(s) => ParametricSurface::curvatures(s, u, v, tol),
Surface::Cylinder(s) => ParametricSurface::curvatures(s, u, v, tol),
Surface::Cone(s) => ParametricSurface::curvatures(s, u, v, tol),
Surface::Sphere(s) => ParametricSurface::curvatures(s, u, v, tol),
Surface::Torus(s) => ParametricSurface::curvatures(s, u, v, tol),
Surface::BSpline(s) => ParametricSurface::curvatures(s, u, v, tol),
}
}
fn as_bspline(&self) -> Option<&BSplineSurface> {
match self {
Surface::BSpline(s) => Some(s),
_ => None,
}
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Surface::as_elementary(self)
}
}
impl From<Plane> for Surface {
fn from(surface: Plane) -> Surface {
Surface::Plane(surface)
}
}
impl From<&Plane> for Surface {
fn from(surface: &Plane) -> Surface {
Surface::Plane(*surface)
}
}
impl From<Cylinder> for Surface {
fn from(surface: Cylinder) -> Surface {
Surface::Cylinder(surface)
}
}
impl From<&Cylinder> for Surface {
fn from(surface: &Cylinder) -> Surface {
Surface::Cylinder(*surface)
}
}
impl From<Cone> for Surface {
fn from(surface: Cone) -> Surface {
Surface::Cone(surface)
}
}
impl From<&Cone> for Surface {
fn from(surface: &Cone) -> Surface {
Surface::Cone(*surface)
}
}
impl From<Sphere> for Surface {
fn from(surface: Sphere) -> Surface {
Surface::Sphere(surface)
}
}
impl From<&Sphere> for Surface {
fn from(surface: &Sphere) -> Surface {
Surface::Sphere(*surface)
}
}
impl From<Torus> for Surface {
fn from(surface: Torus) -> Surface {
Surface::Torus(surface)
}
}
impl From<&Torus> for Surface {
fn from(surface: &Torus) -> Surface {
Surface::Torus(*surface)
}
}
impl From<BSplineSurface> for Surface {
fn from(surface: BSplineSurface) -> Surface {
Surface::BSpline(surface)
}
}
impl From<&BSplineSurface> for Surface {
fn from(surface: &BSplineSurface) -> Surface {
Surface::BSpline(surface.clone())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ElementarySurface {
Plane(Plane),
Cylinder(Cylinder),
Cone(Cone),
Sphere(Sphere),
Torus(Torus),
}
impl Surface {
pub fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
ParametricSurface::project_point(self, point, tol)
}
pub fn project_points(&self, points: &[Point], tol: Tolerance) -> Vec<SurfaceProjection> {
points.iter().map(|&p| self.project_point(p, tol)).collect()
}
pub fn parameters_of(&self, point: Point) -> (f64, f64) {
match self {
Surface::Plane(s) => s.parameters_of(point),
Surface::Cylinder(s) => s.parameters_of(point),
Surface::Cone(s) => s.parameters_of(point),
Surface::Sphere(s) => s.parameters_of(point),
Surface::Torus(s) => s.parameters_of(point),
Surface::BSpline(s) => {
let projection = s.project_point(point, Tolerance::DEFAULT);
(projection.u, projection.v)
}
}
}
pub fn as_elementary(&self) -> Option<ElementarySurface> {
match self {
Surface::Plane(s) => Some(ElementarySurface::Plane(*s)),
Surface::Cylinder(s) => Some(ElementarySurface::Cylinder(*s)),
Surface::Cone(s) => Some(ElementarySurface::Cone(*s)),
Surface::Sphere(s) => Some(ElementarySurface::Sphere(*s)),
Surface::Torus(s) => Some(ElementarySurface::Torus(*s)),
Surface::BSpline(_) => None,
}
}
}
impl ElementarySurface {
pub fn parameters_of(&self, point: Point) -> (f64, f64) {
match self {
ElementarySurface::Plane(s) => s.parameters_of(point),
ElementarySurface::Cylinder(s) => s.parameters_of(point),
ElementarySurface::Cone(s) => s.parameters_of(point),
ElementarySurface::Sphere(s) => s.parameters_of(point),
ElementarySurface::Torus(s) => s.parameters_of(point),
}
}
pub fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
ParametricSurface::project_point(self, point, tol)
}
pub fn project_points(&self, points: &[Point], tol: Tolerance) -> Vec<SurfaceProjection> {
points.iter().map(|&p| self.project_point(p, tol)).collect()
}
}
impl ParametricSurface for ElementarySurface {
fn eval_point(&self, u: f64, v: f64) -> Point {
match self {
ElementarySurface::Plane(s) => s.eval_point(u, v),
ElementarySurface::Cylinder(s) => s.eval_point(u, v),
ElementarySurface::Cone(s) => s.eval_point(u, v),
ElementarySurface::Sphere(s) => s.eval_point(u, v),
ElementarySurface::Torus(s) => s.eval_point(u, v),
}
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
match self {
ElementarySurface::Plane(s) => s.eval_derivative(u, v, du, dv),
ElementarySurface::Cylinder(s) => s.eval_derivative(u, v, du, dv),
ElementarySurface::Cone(s) => s.eval_derivative(u, v, du, dv),
ElementarySurface::Sphere(s) => s.eval_derivative(u, v, du, dv),
ElementarySurface::Torus(s) => s.eval_derivative(u, v, du, dv),
}
}
fn u_bounds(&self) -> (f64, f64) {
match self {
ElementarySurface::Plane(s) => s.u_bounds(),
ElementarySurface::Cylinder(s) => s.u_bounds(),
ElementarySurface::Cone(s) => s.u_bounds(),
ElementarySurface::Sphere(s) => s.u_bounds(),
ElementarySurface::Torus(s) => s.u_bounds(),
}
}
fn v_bounds(&self) -> (f64, f64) {
match self {
ElementarySurface::Plane(s) => s.v_bounds(),
ElementarySurface::Cylinder(s) => s.v_bounds(),
ElementarySurface::Cone(s) => s.v_bounds(),
ElementarySurface::Sphere(s) => s.v_bounds(),
ElementarySurface::Torus(s) => s.v_bounds(),
}
}
fn u_period(&self) -> Option<f64> {
match self {
ElementarySurface::Plane(s) => s.u_period(),
ElementarySurface::Cylinder(s) => s.u_period(),
ElementarySurface::Cone(s) => s.u_period(),
ElementarySurface::Sphere(s) => s.u_period(),
ElementarySurface::Torus(s) => s.u_period(),
}
}
fn v_period(&self) -> Option<f64> {
match self {
ElementarySurface::Plane(s) => s.v_period(),
ElementarySurface::Cylinder(s) => s.v_period(),
ElementarySurface::Cone(s) => s.v_period(),
ElementarySurface::Sphere(s) => s.v_period(),
ElementarySurface::Torus(s) => s.v_period(),
}
}
fn project_point(&self, point: Point, tol: Tolerance) -> SurfaceProjection {
match self {
ElementarySurface::Plane(s) => s.project_point(point, tol),
ElementarySurface::Cylinder(s) => s.project_point(point, tol),
ElementarySurface::Cone(s) => s.project_point(point, tol),
ElementarySurface::Sphere(s) => s.project_point(point, tol),
ElementarySurface::Torus(s) => s.project_point(point, tol),
}
}
fn contains(&self, point: Point, tol: Tolerance) -> bool {
match self {
ElementarySurface::Plane(s) => s.contains(point, tol),
ElementarySurface::Cylinder(s) => s.contains(point, tol),
ElementarySurface::Cone(s) => s.contains(point, tol),
ElementarySurface::Sphere(s) => s.contains(point, tol),
ElementarySurface::Torus(s) => s.contains(point, tol),
}
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point> {
match self {
ElementarySurface::Plane(s) => s.eval_points(uvs),
ElementarySurface::Cylinder(s) => s.eval_points(uvs),
ElementarySurface::Cone(s) => s.eval_points(uvs),
ElementarySurface::Sphere(s) => s.eval_points(uvs),
ElementarySurface::Torus(s) => s.eval_points(uvs),
}
}
fn as_elementary(&self) -> Option<ElementarySurface> {
Some(*self)
}
}
impl From<ElementarySurface> for Surface {
fn from(surface: ElementarySurface) -> Surface {
match surface {
ElementarySurface::Plane(s) => Surface::Plane(s),
ElementarySurface::Cylinder(s) => Surface::Cylinder(s),
ElementarySurface::Cone(s) => Surface::Cone(s),
ElementarySurface::Sphere(s) => Surface::Sphere(s),
ElementarySurface::Torus(s) => Surface::Torus(s),
}
}
}
impl From<Plane> for ElementarySurface {
fn from(surface: Plane) -> ElementarySurface {
ElementarySurface::Plane(surface)
}
}
impl From<Cylinder> for ElementarySurface {
fn from(surface: Cylinder) -> ElementarySurface {
ElementarySurface::Cylinder(surface)
}
}
impl From<Cone> for ElementarySurface {
fn from(surface: Cone) -> ElementarySurface {
ElementarySurface::Cone(surface)
}
}
impl From<Sphere> for ElementarySurface {
fn from(surface: Sphere) -> ElementarySurface {
ElementarySurface::Sphere(surface)
}
}
impl From<Torus> for ElementarySurface {
fn from(surface: Torus) -> ElementarySurface {
ElementarySurface::Torus(surface)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Continuity::{Order, Smooth};
use crate::Point;
fn sample<S: ParametricSurface>(s: &S) -> Point {
s.eval_point(0.3, 0.4)
}
fn plane() -> Plane {
Plane::new(Point::ORIGIN, Vector::Z).unwrap()
}
fn cylinder() -> Cylinder {
Cylinder::new(Point::ORIGIN, Vector::Z, 2.0).unwrap()
}
fn cone() -> Cone {
Cone::new(Point::ORIGIN, Vector::Z, 0.4, 2.0).unwrap()
}
fn sphere() -> Sphere {
Sphere::new(Point::ORIGIN, 3.0).unwrap()
}
fn torus() -> Torus {
Torus::new(Point::ORIGIN, Vector::Z, 5.0, 1.5).unwrap()
}
fn bspline() -> BSplineSurface {
let poles = vec![
vec![Point::new(0.0, 0.0, 0.0), Point::new(0.0, 1.0, 0.0)],
vec![Point::new(1.0, 0.0, 0.0), Point::new(1.0, 1.0, 1.0)],
];
BSplineSurface::new(
1,
1,
poles,
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_sample_plane_agrees_with_inherent() {
let p = plane();
assert_eq!(sample(&p), p.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_cylinder_agrees_with_inherent() {
let c = cylinder();
assert_eq!(sample(&c), c.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_cone_agrees_with_inherent() {
let c = cone();
assert_eq!(sample(&c), c.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_sphere_agrees_with_inherent() {
let s = sphere();
assert_eq!(sample(&s), s.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_torus_agrees_with_inherent() {
let t = torus();
assert_eq!(sample(&t), t.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_bspline_agrees_with_inherent() {
let b = bspline();
assert_eq!(sample(&b), b.eval_point(0.3, 0.4));
}
#[test]
fn test_sample_surface_enum_agrees_with_inherent() {
let p = plane();
let wrapped: Surface = p.into();
assert_eq!(sample(&wrapped), p.eval_point(0.3, 0.4));
}
#[test]
fn test_heterogeneous_vec_eval() {
let p = plane();
let c = cylinder();
let cn = cone();
let s = sphere();
let t = torus();
let surfaces: Vec<Surface> = vec![p.into(), c.into(), cn.into(), s.into(), t.into()];
let points: Vec<Point> = surfaces
.iter()
.map(|surf| surf.eval_point(0.2, 0.1))
.collect();
assert_eq!(points[0], p.eval_point(0.2, 0.1));
assert_eq!(points[1], c.eval_point(0.2, 0.1));
assert_eq!(points[2], cn.eval_point(0.2, 0.1));
assert_eq!(points[3], s.eval_point(0.2, 0.1));
assert_eq!(points[4], t.eval_point(0.2, 0.1));
}
#[test]
fn test_from_plane_round_trips() {
let p = plane();
match Surface::from(p) {
Surface::Plane(inner) => assert_eq!(inner, p),
_ => panic!("expected Surface::Plane"),
}
}
#[test]
fn test_from_plane_ref_round_trips() {
let p = plane();
match Surface::from(&p) {
Surface::Plane(inner) => assert_eq!(inner, p),
_ => panic!("expected Surface::Plane"),
}
}
#[test]
fn test_from_cylinder_round_trips() {
let c = cylinder();
match Surface::from(c) {
Surface::Cylinder(inner) => assert_eq!(inner, c),
_ => panic!("expected Surface::Cylinder"),
}
}
#[test]
fn test_from_cylinder_ref_round_trips() {
let c = cylinder();
match Surface::from(&c) {
Surface::Cylinder(inner) => assert_eq!(inner, c),
_ => panic!("expected Surface::Cylinder"),
}
}
#[test]
fn test_from_cone_round_trips() {
let c = cone();
match Surface::from(c) {
Surface::Cone(inner) => assert_eq!(inner, c),
_ => panic!("expected Surface::Cone"),
}
}
#[test]
fn test_from_cone_ref_round_trips() {
let c = cone();
match Surface::from(&c) {
Surface::Cone(inner) => assert_eq!(inner, c),
_ => panic!("expected Surface::Cone"),
}
}
#[test]
fn test_from_sphere_round_trips() {
let s = sphere();
match Surface::from(s) {
Surface::Sphere(inner) => assert_eq!(inner, s),
_ => panic!("expected Surface::Sphere"),
}
}
#[test]
fn test_from_sphere_ref_round_trips() {
let s = sphere();
match Surface::from(&s) {
Surface::Sphere(inner) => assert_eq!(inner, s),
_ => panic!("expected Surface::Sphere"),
}
}
#[test]
fn test_from_torus_round_trips() {
let t = torus();
match Surface::from(t) {
Surface::Torus(inner) => assert_eq!(inner, t),
_ => panic!("expected Surface::Torus"),
}
}
#[test]
fn test_from_torus_ref_round_trips() {
let t = torus();
match Surface::from(&t) {
Surface::Torus(inner) => assert_eq!(inner, t),
_ => panic!("expected Surface::Torus"),
}
}
#[test]
fn test_from_bspline_round_trips() {
let b = bspline();
match Surface::from(b.clone()) {
Surface::BSpline(inner) => assert_eq!(inner, b),
_ => panic!("expected Surface::BSpline"),
}
}
#[test]
fn test_from_bspline_ref_clones() {
let b = bspline();
match Surface::from(&b) {
Surface::BSpline(inner) => assert_eq!(inner, b),
_ => panic!("expected Surface::BSpline"),
}
}
#[test]
#[allow(clippy::needless_borrows_for_generic_args)]
fn test_into_from_reference_supports_parametrize_on_style_call() {
fn accepts(surface: impl Into<Surface>) -> Surface {
surface.into()
}
let c = cylinder();
match accepts(&c) {
Surface::Cylinder(inner) => assert_eq!(inner, c),
_ => panic!("expected Surface::Cylinder"),
}
}
#[test]
fn test_plane_bounds_and_periods() {
let p = plane();
assert_eq!(
ParametricSurface::u_bounds(&p),
(f64::NEG_INFINITY, f64::INFINITY)
);
assert_eq!(
ParametricSurface::v_bounds(&p),
(f64::NEG_INFINITY, f64::INFINITY)
);
assert_eq!(ParametricSurface::u_period(&p), None);
assert_eq!(ParametricSurface::v_period(&p), None);
let wrapped: Surface = p.into();
assert_eq!(wrapped.u_bounds(), (f64::NEG_INFINITY, f64::INFINITY));
assert_eq!(wrapped.v_bounds(), (f64::NEG_INFINITY, f64::INFINITY));
assert_eq!(wrapped.u_period(), None);
assert_eq!(wrapped.v_period(), None);
}
#[test]
fn test_cylinder_bounds_and_periods() {
let c = cylinder();
assert_eq!(ParametricSurface::u_bounds(&c), (0.0, TAU));
assert_eq!(
ParametricSurface::v_bounds(&c),
(f64::NEG_INFINITY, f64::INFINITY)
);
assert_eq!(ParametricSurface::u_period(&c), Some(TAU));
assert_eq!(ParametricSurface::v_period(&c), None);
let wrapped: Surface = c.into();
assert_eq!(wrapped.u_bounds(), (0.0, TAU));
assert_eq!(wrapped.v_bounds(), (f64::NEG_INFINITY, f64::INFINITY));
assert_eq!(wrapped.u_period(), Some(TAU));
assert_eq!(wrapped.v_period(), None);
}
#[test]
fn test_cone_bounds_and_periods() {
let c = cone();
assert_eq!(ParametricSurface::u_bounds(&c), (0.0, TAU));
assert_eq!(
ParametricSurface::v_bounds(&c),
(f64::NEG_INFINITY, f64::INFINITY)
);
assert_eq!(ParametricSurface::u_period(&c), Some(TAU));
assert_eq!(ParametricSurface::v_period(&c), None);
let wrapped: Surface = c.into();
assert_eq!(wrapped.u_bounds(), (0.0, TAU));
assert_eq!(wrapped.v_bounds(), (f64::NEG_INFINITY, f64::INFINITY));
assert_eq!(wrapped.u_period(), Some(TAU));
assert_eq!(wrapped.v_period(), None);
}
#[test]
fn test_sphere_bounds_and_periods() {
let s = sphere();
assert_eq!(ParametricSurface::u_bounds(&s), (0.0, TAU));
assert_eq!(ParametricSurface::v_bounds(&s), (-FRAC_PI_2, FRAC_PI_2));
assert_eq!(ParametricSurface::u_period(&s), Some(TAU));
assert_eq!(ParametricSurface::v_period(&s), None);
let wrapped: Surface = s.into();
assert_eq!(wrapped.u_bounds(), (0.0, TAU));
assert_eq!(wrapped.v_bounds(), (-FRAC_PI_2, FRAC_PI_2));
assert_eq!(wrapped.u_period(), Some(TAU));
assert_eq!(wrapped.v_period(), None);
}
#[test]
fn test_torus_bounds_and_periods() {
let t = torus();
assert_eq!(ParametricSurface::u_bounds(&t), (0.0, TAU));
assert_eq!(ParametricSurface::v_bounds(&t), (0.0, TAU));
assert_eq!(ParametricSurface::u_period(&t), Some(TAU));
assert_eq!(ParametricSurface::v_period(&t), Some(TAU));
let wrapped: Surface = t.into();
assert_eq!(wrapped.u_bounds(), (0.0, TAU));
assert_eq!(wrapped.v_bounds(), (0.0, TAU));
assert_eq!(wrapped.u_period(), Some(TAU));
assert_eq!(wrapped.v_period(), Some(TAU));
}
#[test]
fn test_bspline_bounds_and_periods() {
let b = bspline();
assert_eq!(ParametricSurface::u_bounds(&b), (0.0, 1.0));
assert_eq!(ParametricSurface::v_bounds(&b), (0.0, 1.0));
assert_eq!(ParametricSurface::u_period(&b), None);
assert_eq!(ParametricSurface::v_period(&b), None);
let wrapped: Surface = b.into();
assert_eq!(wrapped.u_bounds(), (0.0, 1.0));
assert_eq!(wrapped.v_bounds(), (0.0, 1.0));
assert_eq!(wrapped.u_period(), None);
assert_eq!(wrapped.v_period(), None);
}
#[test]
fn test_bspline_enum_dispatch_agrees_with_inherent() {
let b = bspline();
let wrapped: Surface = b.clone().into();
assert_eq!(wrapped.eval_point(0.3, 0.4), b.eval_point(0.3, 0.4));
assert_eq!(
wrapped.eval_derivative(0.3, 0.4, 1, 0),
b.eval_derivative(0.3, 0.4, 1, 0)
);
assert_eq!(
wrapped.eval_derivative(0.3, 0.4, 0, 1),
b.eval_derivative(0.3, 0.4, 0, 1)
);
}
#[test]
fn test_default_eval_points_matches_mapped_eval_point_for_each_type() {
let uvs = [(0.0, 0.0), (0.5, 0.2), (1.0, -0.3), (-2.0, 1.1)];
let p = plane();
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| p.eval_point(u, v)).collect();
assert_eq!(ParametricSurface::eval_points(&p, &uvs), expected);
let c = cylinder();
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| c.eval_point(u, v)).collect();
assert_eq!(ParametricSurface::eval_points(&c, &uvs), expected);
let cn = cone();
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| cn.eval_point(u, v)).collect();
assert_eq!(ParametricSurface::eval_points(&cn, &uvs), expected);
let s = sphere();
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| s.eval_point(u, v)).collect();
assert_eq!(ParametricSurface::eval_points(&s, &uvs), expected);
let t = torus();
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| t.eval_point(u, v)).collect();
assert_eq!(ParametricSurface::eval_points(&t, &uvs), expected);
}
#[test]
fn test_surface_enum_eval_points_matches_mapped_eval_point() {
let uvs = [(0.0, 0.0), (0.5, 0.2), (1.0, -0.3), (-2.0, 1.1)];
let surfaces: Vec<Surface> = vec![
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
];
for surface in &surfaces {
let expected: Vec<Point> = uvs.iter().map(|&(u, v)| surface.eval_point(u, v)).collect();
assert_eq!(surface.eval_points(&uvs), expected);
}
}
#[test]
fn test_eval_derivative_delegates_to_inherent() {
let p = plane();
assert_eq!(
ParametricSurface::eval_derivative(&p, 0.3, 0.4, 1, 0),
p.eval_derivative(0.3, 0.4, 1, 0)
);
let s = sphere();
assert_eq!(
ParametricSurface::eval_derivative(&s, 0.3, 0.4, 1, 0),
s.eval_derivative(0.3, 0.4, 1, 0)
);
let wrapped: Surface = torus().into();
assert_eq!(
wrapped.eval_derivative(0.3, 0.4, 0, 1),
torus().eval_derivative(0.3, 0.4, 0, 1)
);
}
#[test]
fn test_normal_plane_is_constant_z() {
let p = plane();
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::normal(&p, 1.0, -2.0, tol),
Some(Vector::Z)
);
assert_eq!(
ParametricSurface::normal(&p, 0.0, 0.0, tol),
Some(p.normal())
);
}
#[test]
fn test_normal_cylinder_points_radially_outward() {
let c = cylinder();
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::normal(&c, 0.0, 3.0, tol),
Some(Vector::X)
);
}
#[test]
fn test_normal_cone_matches_tilted_radial() {
let c = cone();
let tol = Tolerance::DEFAULT;
let n = ParametricSurface::normal(&c, 0.0, 0.0, tol).unwrap();
let expected = Vector::new(0.4f64.cos(), 0.0, -0.4f64.sin());
assert!((n - expected).magnitude() < 1e-12);
assert!((n.magnitude() - 1.0).abs() < 1e-12);
}
#[test]
fn test_normal_sphere_points_outward_and_recovers_at_poles() {
let s = sphere();
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::normal(&s, 0.0, 0.0, tol),
Some(Vector::X)
);
let north = ParametricSurface::normal(&s, 0.0, FRAC_PI_2, tol).unwrap();
assert!((north - Vector::Z).magnitude() < 1e-9);
let south = ParametricSurface::normal(&s, 1.0, -FRAC_PI_2, tol).unwrap();
assert!((south + Vector::Z).magnitude() < 1e-9);
}
#[test]
fn test_normal_torus_points_outward_on_outer_equator() {
let t = torus();
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::normal(&t, 0.0, 0.0, tol),
Some(Vector::X)
);
}
#[test]
fn test_normal_cone_apex_has_no_unique_normal() {
let c = cone();
let tol = Tolerance::DEFAULT;
let v_apex = -2.0 / 0.4f64.sin();
for u in [0.0f64, 1.0, 2.0, 4.0] {
assert_eq!(
ParametricSurface::normal(&c, u, v_apex, tol),
None,
"u = {u}"
);
}
}
#[test]
fn test_normal_pole_recovery_is_independent_of_meridian() {
let s = sphere();
let tol = Tolerance::DEFAULT;
for u in [0.0f64, 1.0, 2.0, 4.0] {
let north = ParametricSurface::normal(&s, u, FRAC_PI_2, tol).unwrap();
assert!((north - Vector::Z).magnitude() < 1e-9, "u = {u}: {north:?}");
let south = ParametricSurface::normal(&s, u, -FRAC_PI_2, tol).unwrap();
assert!((south + Vector::Z).magnitude() < 1e-9, "u = {u}: {south:?}");
}
}
#[test]
fn test_normal_bspline_agrees_with_cross_of_partials() {
let b = bspline();
let tol = Tolerance::DEFAULT;
let (_, [su, sv]) = b.eval_d1(0.3, 0.4);
let expected = su.cross(sv) * (1.0 / su.cross(sv).magnitude());
assert_eq!(ParametricSurface::normal(&b, 0.3, 0.4, tol), Some(expected));
}
#[test]
fn test_normal_sub_confusion_geometry_is_none() {
let tol = Tolerance::DEFAULT;
let tiny = Sphere::new(Point::ORIGIN, 1e-9).unwrap();
assert_eq!(ParametricSurface::normal(&tiny, 0.0, 0.0, tol), None);
}
#[test]
fn test_normal_surface_enum_dispatch_agrees_with_concrete() {
let tol = Tolerance::DEFAULT;
let surfaces: Vec<Surface> = vec![
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
bspline().into(),
];
for surface in &surfaces {
assert_eq!(
surface.normal(0.3, 0.2, tol),
ParametricSurface::normal(surface, 0.3, 0.2, tol)
);
let (_, [su, sv]) = surface.eval_d1(0.3, 0.2);
let n = surface.normal(0.3, 0.2, tol).unwrap();
let expected = su.cross(sv).normalized().unwrap();
assert!((n - expected).magnitude() < 1e-15);
}
}
struct DeadZone {
dead: f64,
scale: f64,
}
impl ParametricSurface for DeadZone {
fn eval_point(&self, _u: f64, _v: f64) -> Point {
Point::ORIGIN
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector {
let reach = self.dead * self.scale;
let dead = u.abs() < reach && v.abs() < reach;
match (du, dv) {
(1, 0) if !dead => Vector::X * (1.0 / self.scale),
(0, 1) if !dead => Vector::Y * (1.0 / self.scale),
_ => Vector::ZERO,
}
}
fn u_bounds(&self) -> (f64, f64) {
(-self.scale, self.scale)
}
fn v_bounds(&self) -> (f64, f64) {
(-self.scale, self.scale)
}
fn u_period(&self) -> Option<f64> {
None
}
fn v_period(&self) -> Option<f64> {
None
}
fn project_point(&self, _point: Point, _tol: Tolerance) -> SurfaceProjection {
SurfaceProjection {
u: 0.0,
v: 0.0,
point: Point::ORIGIN,
distance: 0.0,
}
}
fn contains(&self, _point: Point, _tol: Tolerance) -> bool {
false
}
}
#[test]
fn test_normal_steps_off_a_locally_dead_zone_at_any_parameter_scale() {
let tol = Tolerance::DEFAULT;
for scale in [1.0, 1e8, 1e-8] {
let surface = DeadZone { dead: 5e-8, scale };
for (u, v) in [(0.0, 0.0), (0.5 * scale, -0.5 * scale)] {
let normal = ParametricSurface::normal(&surface, u, v, tol);
let normal = normal.unwrap_or_else(|| panic!("scale {scale}, ({u}, {v}): None"));
assert!((normal - Vector::Z).magnitude() < 1e-15, "scale {scale}");
}
}
}
#[test]
fn test_normal_is_none_where_no_rung_recovers() {
let surface = DeadZone {
dead: f64::INFINITY,
scale: 1.0,
};
let tol = Tolerance::DEFAULT;
assert_eq!(ParametricSurface::normal(&surface, 0.5, 0.5, tol), None);
}
#[test]
fn test_normal_is_none_at_non_finite_parameters() {
let tol = Tolerance::DEFAULT;
let s = sphere();
assert_eq!(ParametricSurface::normal(&s, f64::NAN, 0.0, tol), None);
assert_eq!(ParametricSurface::normal(&s, 0.0, f64::NAN, tol), None);
assert_eq!(ParametricSurface::normal(&s, f64::INFINITY, 0.0, tol), None);
assert_eq!(
ParametricSurface::normal(&bspline(), f64::NAN, 0.5, tol),
None
);
}
fn rescaled(surface: &BSplineSurface, scale: f64) -> BSplineSurface {
let row = surface.v_pole_count();
let poles = surface.poles().chunks(row).map(<[_]>::to_vec).collect();
let u_knots = surface.u_knots().iter().map(|knot| knot * scale).collect();
let v_knots = surface.v_knots().iter().map(|knot| knot * scale).collect();
let (u_multiplicities, v_multiplicities) = (
surface.u_multiplicities().to_vec(),
surface.v_multiplicities().to_vec(),
);
let (u_degree, v_degree) = (surface.u_degree(), surface.v_degree());
let (u_periodic, v_periodic) = (surface.is_u_periodic(), surface.is_v_periodic());
match surface.weights() {
Some(weights) => BSplineSurface::new_rational(
u_degree,
v_degree,
poles,
weights.chunks(row).map(<[_]>::to_vec).collect(),
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
),
None => BSplineSurface::new(
u_degree,
v_degree,
poles,
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
),
}
.unwrap()
}
fn collapsed_row() -> BSplineSurface {
let apex = Point::new(0.0, 0.0, 1.0);
let poles = vec![
vec![apex, apex, apex],
vec![
Point::new(0.5, -0.5, 0.6),
Point::new(0.6, 0.0, 0.7),
Point::new(0.5, 0.5, 0.6),
],
vec![
Point::new(1.0, -1.0, 0.0),
Point::new(1.1, 0.0, 0.1),
Point::new(1.0, 1.0, 0.0),
],
];
let (knots, multiplicities) = (vec![0.0, 1.0], vec![3, 3]);
BSplineSurface::new(
2,
2,
poles,
knots.clone(),
multiplicities.clone(),
knots,
multiplicities,
false,
false,
)
.unwrap()
}
#[test]
fn test_normal_collapsed_row_apex_has_no_unique_normal() {
let surface = collapsed_row();
let tol = Tolerance::DEFAULT;
for v in [0.0, 0.3, 0.5, 1.0] {
assert_eq!(
ParametricSurface::normal(&surface, 0.0, v, tol),
None,
"v = {v}"
);
let limit = ParametricSurface::normal(&surface, 1e-6, v, tol).unwrap();
let beside = ParametricSurface::normal(&surface, 1e-9, v, tol).unwrap();
assert!((beside - limit).magnitude() < 1e-5, "v = {v}: {beside:?}");
}
}
#[test]
fn test_normal_beside_a_cone_apex_is_the_flank_normal() {
let tol = Tolerance::DEFAULT;
for semi_angle in [0.1, 0.4, 1.2] {
let c = Cone::new(Point::ORIGIN, Vector::Z, semi_angle, 2.0).unwrap();
let v_apex = -2.0 / semi_angle.sin();
for offset in [1e-9, -1e-9, 1e-8, -1e-8, 1e-6, -1e-3] {
for u in [0.0, 1.0, 4.0] {
let v = v_apex + offset;
let (_, [su, sv]) = c.eval_d1(u, v);
let exact = su.cross(sv) * (1.0 / su.cross(sv).magnitude());
let normal = ParametricSurface::normal(&c, u, v, tol).unwrap();
assert!(
(normal - exact).magnitude() < 1e-12,
"semi-angle {semi_angle}, offset {offset}, u = {u}: {normal:?}"
);
}
}
let patch = c
.to_bspline_patch((0.0, std::f64::consts::PI), (v_apex, v_apex + 1.0))
.unwrap();
for scale in [1.0, 1e8, 1e-8] {
let scaled = rescaled(&patch, scale);
for u in [0.3, 1.5] {
assert_eq!(
ParametricSurface::normal(&scaled, u * scale, v_apex * scale, tol),
None,
"semi-angle {semi_angle}, scale {scale}, u = {u}"
);
let flank =
ParametricSurface::normal(&scaled, u * scale, (v_apex + 0.5) * scale, tol)
.unwrap();
let beside = (v_apex + 1e-9) * scale;
let normal = ParametricSurface::normal(&scaled, u * scale, beside, tol)
.unwrap_or_else(|| panic!("scale {scale}, u = {u}: None"));
assert!(
(normal - flank).magnitude() < 1e-12,
"semi-angle {semi_angle}, scale {scale}, u = {u}: {normal:?}"
);
}
}
}
}
#[test]
fn test_normal_does_not_change_when_the_knots_are_rescaled() {
let tol = Tolerance::DEFAULT;
let sphere_patch = sphere()
.to_bspline_patch((0.0, 2.0), (-FRAC_PI_2, FRAC_PI_2))
.unwrap();
let parameters = [(0.0, 0.0), (0.0, 0.5), (1e-9, 0.3), (0.5, 0.5), (1.0, 1.0)];
let sphere_parameters = [
(0.5, FRAC_PI_2),
(0.5, -FRAC_PI_2),
(0.0, FRAC_PI_2),
(0.5, 0.3),
];
for (surface, parameters) in [
(collapsed_row(), ¶meters[..]),
(bspline(), ¶meters[..]),
(sphere_patch, &sphere_parameters[..]),
] {
for scale in [1e8, 1e-8] {
let scaled = rescaled(&surface, scale);
for &(u, v) in parameters {
let reference = ParametricSurface::normal(&surface, u, v, tol);
let normal = ParametricSurface::normal(&scaled, u * scale, v * scale, tol);
match (reference, normal) {
(None, None) => {}
(Some(reference), Some(normal)) => assert!(
(normal - reference).magnitude() < 1e-12,
"scale {scale}, ({u}, {v}): {normal:?} vs {reference:?}"
),
_ => panic!("scale {scale}, ({u}, {v}): {normal:?} vs {reference:?}"),
}
}
}
}
}
#[test]
fn test_normal_poles_of_rational_sphere_patch_point_outward() {
let tol = Tolerance::DEFAULT;
let patch = sphere()
.to_bspline_patch((0.0, 2.0), (-FRAC_PI_2, FRAC_PI_2))
.unwrap();
for u in [0.0, 1.0, 2.0] {
let north = ParametricSurface::normal(&patch, u, FRAC_PI_2, tol).unwrap();
let south = ParametricSurface::normal(&patch, u, -FRAC_PI_2, tol).unwrap();
assert!(
(north - Vector::Z).magnitude() < 1e-12,
"u = {u}: {north:?}"
);
assert!(
(south + Vector::Z).magnitude() < 1e-12,
"u = {u}: {south:?}"
);
}
}
#[test]
fn test_normal_next_to_a_pole_is_the_true_normal_within_its_distance() {
let s = sphere();
let tol = Tolerance::DEFAULT;
for polar_angle in [1e-9, 1e-6, 1e-3] {
let v = FRAC_PI_2 - polar_angle;
let normal = ParametricSurface::normal(&s, 0.7, v, tol).unwrap();
let exact = (s.eval_point(0.7, v) - Point::ORIGIN) * (1.0 / 3.0);
assert!(
(normal - exact).magnitude() <= 1.01 * polar_angle,
"polar angle {polar_angle}: {normal:?}"
);
}
}
fn closed_u_patch() -> BSplineSurface {
let row = |x: f64| vec![Point::new(x, 0.0, 0.0), Point::new(x, 1.0, 0.0)];
BSplineSurface::new(
1,
1,
vec![row(0.0), row(1.0), row(0.0)],
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_elementary_closedness_follows_periodicity() {
let tol = Tolerance::DEFAULT;
assert_eq!(
(
ParametricSurface::is_u_closed(&plane(), tol),
ParametricSurface::is_v_closed(&plane(), tol)
),
(false, false)
);
assert_eq!(
(
ParametricSurface::is_u_closed(&cylinder(), tol),
ParametricSurface::is_v_closed(&cylinder(), tol)
),
(true, false)
);
assert_eq!(
(
ParametricSurface::is_u_closed(&cone(), tol),
ParametricSurface::is_v_closed(&cone(), tol)
),
(true, false)
);
assert_eq!(
(
ParametricSurface::is_u_closed(&sphere(), tol),
ParametricSurface::is_v_closed(&sphere(), tol)
),
(true, false)
);
assert_eq!(
(
ParametricSurface::is_u_closed(&torus(), tol),
ParametricSurface::is_v_closed(&torus(), tol)
),
(true, true)
);
}
#[test]
fn test_bspline_closed_clamped_patch_without_period() {
let tol = Tolerance::DEFAULT;
let s = closed_u_patch();
assert_eq!(ParametricSurface::u_period(&s), None);
assert!(ParametricSurface::is_u_closed(&s, tol));
assert!(!ParametricSurface::is_v_closed(&s, tol));
assert!(!ParametricSurface::is_u_closed(&bspline(), tol));
assert!(!ParametricSurface::is_v_closed(&bspline(), tol));
}
#[test]
fn test_bspline_closed_clamped_patch_in_v() {
let column = |y: f64| Point::new(0.0, y, 0.0);
let shifted = |y: f64| Point::new(1.0, y, 0.0);
let s = BSplineSurface::new(
1,
1,
vec![
vec![column(0.0), column(1.0), column(0.0)],
vec![shifted(0.0), shifted(1.0), shifted(0.0)],
],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
false,
false,
)
.unwrap();
let tol = Tolerance::DEFAULT;
assert!(!s.is_u_closed(tol));
assert!(s.is_v_closed(tol));
}
#[test]
fn test_bspline_unclamped_patch_with_wrapped_rows_is_closed() {
let ring: Vec<Point> = (0..6)
.map(|i| {
let angle = i as f64 * std::f64::consts::TAU / 6.0;
Point::new(angle.cos(), angle.sin(), 0.0)
})
.collect();
let rows: Vec<Vec<Point>> = ring
.iter()
.chain(&ring[..2])
.map(|p| vec![*p, Point::new(p.x, p.y, 1.0)])
.collect();
let knot_count = rows.len() + 3;
let s = BSplineSurface::new(
2,
1,
rows,
(0..knot_count).map(|i| i as f64).collect(),
vec![1; knot_count],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap();
let tol = Tolerance::DEFAULT;
assert!(s.is_u_closed(tol));
assert!(!s.is_v_closed(tol));
let (first, last) = s.u_bounds();
assert!(s.eval_point(first, 0.3).distance(s.eval_point(last, 0.3)) <= tol.confusion);
}
#[test]
fn test_bspline_rational_closedness_needs_proportional_weights() {
let row = |x: f64| vec![Point::new(x, 0.0, 0.0), Point::new(x, 1.0, 0.0)];
let patch = |last_row_weights: Vec<f64>| {
BSplineSurface::new_rational(
1,
1,
vec![row(0.0), row(1.0), row(0.0)],
vec![vec![1.0, 2.0], vec![1.0, 1.0], last_row_weights],
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
};
let tol = Tolerance::DEFAULT;
assert!(patch(vec![3.0, 6.0]).is_u_closed(tol));
assert!(!patch(vec![2.0, 1.0]).is_u_closed(tol));
}
#[test]
fn test_surface_enum_closedness_dispatch_agrees_with_concrete() {
let tol = Tolerance::DEFAULT;
let surfaces: Vec<Surface> = vec![
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
bspline().into(),
closed_u_patch().into(),
];
for surface in &surfaces {
let expected = match surface {
Surface::Plane(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Cylinder(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Cone(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Sphere(s) => ParametricSurface::is_u_closed(s, tol),
Surface::Torus(s) => ParametricSurface::is_u_closed(s, tol),
Surface::BSpline(s) => s.is_u_closed(tol),
};
assert_eq!(surface.is_u_closed(tol), expected);
let expected_v = match surface {
Surface::Plane(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Cylinder(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Cone(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Sphere(s) => ParametricSurface::is_v_closed(s, tol),
Surface::Torus(s) => ParametricSurface::is_v_closed(s, tol),
Surface::BSpline(s) => s.is_v_closed(tol),
};
assert_eq!(surface.is_v_closed(tol), expected_v);
}
}
#[test]
fn test_elementary_surface_agrees_with_surface() {
let tol = Tolerance::DEFAULT;
let surfaces: [ElementarySurface; 5] = [
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
];
let query = Point::new(0.7, -1.2, 2.5);
for surface in surfaces {
let wrapped = Surface::from(surface);
assert_eq!(wrapped.as_elementary(), Some(surface));
assert_eq!(surface.eval_point(0.3, 0.4), wrapped.eval_point(0.3, 0.4));
assert_eq!(
surface.eval_derivative(0.3, 0.4, 1, 1),
wrapped.eval_derivative(0.3, 0.4, 1, 1)
);
assert_eq!(surface.u_bounds(), wrapped.u_bounds());
assert_eq!(surface.v_bounds(), wrapped.v_bounds());
assert_eq!(surface.u_period(), wrapped.u_period());
assert_eq!(surface.v_period(), wrapped.v_period());
assert_eq!(
surface.project_point(query, tol),
wrapped.project_point(query, tol)
);
assert_eq!(surface.contains(query, tol), wrapped.contains(query, tol));
}
}
#[test]
fn test_bspline_surface_is_not_elementary() {
assert_eq!(Surface::from(bspline()).as_elementary(), None);
}
fn creased_u_patch() -> BSplineSurface {
let row = |x: f64, z: f64| vec![Point::new(x, 0.0, z), Point::new(x, 1.0, z)];
BSplineSurface::new(
2,
1,
vec![
row(0.0, 0.0),
row(1.0, 0.0),
row(2.0, 1.0),
row(3.0, 1.0),
row(4.0, 0.0),
],
vec![0.0, 0.5, 1.0],
vec![3, 2, 3],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_elementary_surfaces_are_smooth_everywhere() {
let tol = Tolerance::DEFAULT;
for surface in [
Surface::from(plane()),
Surface::from(cylinder()),
Surface::from(cone()),
Surface::from(sphere()),
Surface::from(torus()),
] {
assert_eq!(surface.continuity_u(0.5, tol), Smooth);
assert_eq!(surface.continuity_v(0.5, tol), Smooth);
assert_eq!(surface.min_continuity_u(), Smooth);
assert_eq!(surface.min_continuity_v(), Smooth);
}
}
#[test]
fn test_bspline_continuity_drops_at_repeated_knots() {
let tol = Tolerance::DEFAULT;
let s = creased_u_patch();
assert_eq!(ParametricSurface::continuity_u(&s, 0.5, tol), Order(0));
assert_eq!(s.continuity_u(0.5, tol), Order(0));
assert_eq!(ParametricSurface::continuity_u(&s, 0.25, tol), Smooth);
assert_eq!(ParametricSurface::continuity_v(&s, 0.5, tol), Smooth);
assert_eq!(s.min_continuity_u(), Order(0));
assert_eq!(s.min_continuity_v(), Smooth);
assert_eq!(ParametricSurface::min_continuity_u(&s), Order(0));
}
#[test]
fn test_surface_enum_continuity_dispatch_agrees_with_concrete() {
let tol = Tolerance::DEFAULT;
let surfaces: Vec<Surface> = vec![
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
bspline().into(),
creased_u_patch().into(),
];
for surface in &surfaces {
let expected_u = match surface {
Surface::Plane(s) => ParametricSurface::continuity_u(s, 0.5, tol),
Surface::Cylinder(s) => ParametricSurface::continuity_u(s, 0.5, tol),
Surface::Cone(s) => ParametricSurface::continuity_u(s, 0.5, tol),
Surface::Sphere(s) => ParametricSurface::continuity_u(s, 0.5, tol),
Surface::Torus(s) => ParametricSurface::continuity_u(s, 0.5, tol),
Surface::BSpline(s) => s.continuity_u(0.5, tol),
};
assert_eq!(surface.continuity_u(0.5, tol), expected_u);
assert_eq!(
surface.continuity_v(0.5, tol),
match surface {
Surface::BSpline(s) => s.continuity_v(0.5, tol),
Surface::Plane(s) => ParametricSurface::continuity_v(s, 0.5, tol),
Surface::Cylinder(s) => ParametricSurface::continuity_v(s, 0.5, tol),
Surface::Cone(s) => ParametricSurface::continuity_v(s, 0.5, tol),
Surface::Sphere(s) => ParametricSurface::continuity_v(s, 0.5, tol),
Surface::Torus(s) => ParametricSurface::continuity_v(s, 0.5, tol),
}
);
}
}
fn assert_frame(c: &SurfaceCurvatures, normal: Vector) {
for direction in [c.min_direction, c.max_direction] {
assert!((direction.magnitude() - 1.0).abs() < 1e-12);
assert!(direction.dot(normal).abs() < 1e-12);
}
assert!(c.min_direction.dot(c.max_direction).abs() < 1e-12);
assert!((c.min_curvature + c.max_curvature - 2.0 * c.mean).abs() < 1e-12);
assert!((c.min_curvature * c.max_curvature - c.gaussian).abs() < 1e-12);
assert!(c.min_curvature <= c.max_curvature);
}
#[test]
fn test_curvatures_plane_are_zero() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&plane(), 1.0, 2.0, tol).unwrap();
assert_eq!(
c,
SurfaceCurvatures {
min_curvature: 0.0,
max_curvature: 0.0,
min_direction: Vector::X,
max_direction: Vector::Y,
mean: 0.0,
gaussian: 0.0,
}
);
}
#[test]
fn test_curvatures_cylinder_hoop_and_ruling() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&cylinder(), 0.0, 3.0, tol).unwrap();
assert!((c.min_curvature + 0.5).abs() < 1e-12);
assert_eq!(c.max_curvature, 0.0);
assert_eq!((c.mean, c.gaussian), (-0.25, 0.0));
assert!((c.min_direction.dot(Vector::Y).abs() - 1.0).abs() < 1e-12);
assert!((c.max_direction.dot(Vector::Z).abs() - 1.0).abs() < 1e-12);
assert_frame(&c, Vector::X);
}
#[test]
fn test_curvatures_sphere_umbilic() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&sphere(), 0.0, 0.0, tol).unwrap();
assert!((c.min_curvature + 1.0 / 3.0).abs() < 1e-15);
assert!((c.max_curvature + 1.0 / 3.0).abs() < 1e-15);
assert!((c.mean + 1.0 / 3.0).abs() < 1e-15);
assert!((c.gaussian - 1.0 / 9.0).abs() < 1e-15);
assert_frame(&c, Vector::X);
}
#[test]
fn test_curvatures_torus_outer_equator() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&torus(), 0.0, 0.0, tol).unwrap();
assert!((c.min_curvature + 1.0 / 1.5).abs() < 1e-12);
assert!((c.max_curvature + 1.0 / 6.5).abs() < 1e-12);
assert!((c.gaussian - 1.0 / (6.5 * 1.5)).abs() < 1e-12);
assert!((c.min_direction.dot(Vector::Z).abs() - 1.0).abs() < 1e-12);
assert!((c.max_direction.dot(Vector::Y).abs() - 1.0).abs() < 1e-12);
assert_frame(&c, Vector::X);
}
#[test]
fn test_curvatures_torus_inner_equator_saddle() {
use std::f64::consts::PI;
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&torus(), 0.0, PI, tol).unwrap();
assert!((c.min_curvature + 2.0 / 3.0).abs() < 1e-12);
assert!((c.max_curvature - 2.0 / 7.0).abs() < 1e-12);
assert!((c.mean + 4.0 / 21.0).abs() < 1e-12);
assert!((c.gaussian + 4.0 / 21.0).abs() < 1e-12);
assert!((c.min_direction.dot(Vector::Z).abs() - 1.0).abs() < 1e-12);
assert!((c.max_direction.dot(Vector::Y).abs() - 1.0).abs() < 1e-12);
assert_frame(&c, -Vector::X);
}
#[test]
fn test_curvatures_cone_developable() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&cone(), 0.0, 0.0, tol).unwrap();
assert!((c.min_curvature + 0.4f64.cos() / 2.0).abs() < 1e-9);
assert!(c.max_curvature.abs() < 1e-9);
assert!(c.gaussian.abs() < 1e-9);
assert_frame(&c, Vector::new(0.4f64.cos(), 0.0, -0.4f64.sin()));
}
#[test]
fn test_curvatures_saddle_is_negative_gaussian() {
let tol = Tolerance::DEFAULT;
let c = ParametricSurface::curvatures(&bspline(), 0.5, 0.5, tol).unwrap();
assert!((c.gaussian + 1.0 / 2.25).abs() < 1e-9);
assert!((c.mean + 0.25 / 1.5f64.powf(1.5)).abs() < 1e-9);
let n = Vector::new(-0.5, -0.5, 1.0);
assert_frame(&c, n * (1.0 / n.magnitude()));
}
#[test]
fn test_curvatures_none_at_singular_points() {
use std::f64::consts::FRAC_PI_2;
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::curvatures(&sphere(), 0.0, FRAC_PI_2, tol),
None
);
let v_apex = -2.0 / 0.4f64.sin();
assert_eq!(
ParametricSurface::curvatures(&cone(), 0.0, v_apex, tol),
None
);
}
fn normal_curvature_along<S: ParametricSurface>(
surface: &S,
u: f64,
v: f64,
direction: Vector,
) -> f64 {
let (_, [su, sv], [suu, suv, svv]) = surface.eval_d2(u, v);
let normal = su.cross(sv) * (1.0 / su.cross(sv).magnitude());
let (e, f, g) = (su.dot(su), su.dot(sv), sv.dot(sv));
let (along_u, along_v) = (direction.dot(su), direction.dot(sv));
let determinant = e * g - f * f;
let du = (g * along_u - f * along_v) / determinant;
let dv = (e * along_v - f * along_u) / determinant;
let second = suu * (du * du) + suv * (2.0 * du * dv) + svv * (dv * dv);
let tangent = su * du + sv * dv;
second.dot(normal) / tangent.dot(tangent)
}
#[test]
fn test_curvatures_saddle_directions_carry_principal_curvatures() {
let tol = Tolerance::DEFAULT;
let surface = bspline();
for (u, v) in [(0.5, 0.5), (0.1, 0.8), (0.9, 0.3)] {
let c = ParametricSurface::curvatures(&surface, u, v, tol).unwrap();
let along_min = normal_curvature_along(&surface, u, v, c.min_direction);
let along_max = normal_curvature_along(&surface, u, v, c.max_direction);
assert!((along_min - c.min_curvature).abs() < 1e-14);
assert!((along_max - c.max_curvature).abs() < 1e-14);
let squared = 1.0 + u * u + v * v;
assert!((c.gaussian + 1.0 / (squared * squared)).abs() < 1e-14);
}
}
#[test]
fn test_curvatures_sphere_everywhere_umbilic_with_orthonormal_directions() {
let tol = Tolerance::DEFAULT;
let sphere = Sphere::new(Point::new(1.0, 2.0, 3.0), 3.0).unwrap();
for i in 0..40 {
let (u, v) = (0.157 * f64::from(i), -1.5 + 0.075 * f64::from(i));
let c = ParametricSurface::curvatures(&sphere, u, v, tol).unwrap();
assert!((c.min_curvature + 1.0 / 3.0).abs() < 1e-14, "{u}, {v}");
assert!((c.max_curvature + 1.0 / 3.0).abs() < 1e-14, "{u}, {v}");
let (_, [su, sv]) = sphere.eval_d1(u, v);
assert_frame(&c, su.cross(sv) * (1.0 / su.cross(sv).magnitude()));
}
}
#[test]
fn test_curvatures_rational_cylinder_patch() {
let weight = std::f64::consts::FRAC_1_SQRT_2;
let poles = vec![
vec![Point::new(2.0, 0.0, 0.0), Point::new(2.0, 0.0, 1.0)],
vec![Point::new(2.0, 2.0, 0.0), Point::new(2.0, 2.0, 1.0)],
vec![Point::new(0.0, 2.0, 0.0), Point::new(0.0, 2.0, 1.0)],
];
let weights = vec![vec![1.0, 1.0], vec![weight, weight], vec![1.0, 1.0]];
let surface = BSplineSurface::new_rational(
2,
1,
poles,
weights,
vec![0.0, 1.0],
vec![3, 3],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap();
let tol = Tolerance::DEFAULT;
for u in [0.0, 0.3, 0.5, 1.0] {
let c = ParametricSurface::curvatures(&surface, u, 0.5, tol).unwrap();
assert!((c.min_curvature + 0.5).abs() < 1e-14);
assert!(c.max_curvature.abs() < 1e-14);
assert!((c.max_direction.dot(Vector::Z).abs() - 1.0).abs() < 1e-14);
}
}
#[test]
fn test_curvatures_do_not_depend_on_the_knot_scale() {
let tol = Tolerance::DEFAULT;
let reference = collapsed_row();
for scale in [1e8, 1e-8] {
let surface = rescaled(&reference, scale);
for (u, v) in [(0.3, 0.6), (0.7, 0.2), (1.0, 1.0)] {
let expected = ParametricSurface::curvatures(&reference, u, v, tol).unwrap();
let found = ParametricSurface::curvatures(&surface, u * scale, v * scale, tol)
.unwrap_or_else(|| panic!("scale {scale} at ({u}, {v})"));
let size = expected
.min_curvature
.abs()
.max(expected.max_curvature.abs());
for (got, want) in [
(found.min_curvature, expected.min_curvature),
(found.max_curvature, expected.max_curvature),
(found.mean, expected.mean),
] {
assert!(
(got - want).abs() < 1e-9 * size,
"scale {scale} at ({u}, {v})"
);
}
assert!(
(found.gaussian - expected.gaussian).abs() < 1e-9 * size * size,
"scale {scale} at ({u}, {v})"
);
}
}
}
#[test]
fn test_curvatures_none_for_non_finite_parameters() {
let tol = Tolerance::DEFAULT;
assert_eq!(
ParametricSurface::curvatures(&torus(), f64::NAN, 0.0, tol),
None
);
assert_eq!(
ParametricSurface::curvatures(&cylinder(), f64::INFINITY, 0.0, tol),
None
);
}
#[test]
fn test_surface_enum_curvatures_dispatch_agrees_with_concrete() {
let tol = Tolerance::DEFAULT;
let surfaces: Vec<Surface> = vec![
plane().into(),
cylinder().into(),
cone().into(),
sphere().into(),
torus().into(),
bspline().into(),
];
for surface in &surfaces {
let expected = match surface {
Surface::Plane(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
Surface::Cylinder(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
Surface::Cone(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
Surface::Sphere(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
Surface::Torus(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
Surface::BSpline(s) => ParametricSurface::curvatures(s, 0.3, 0.2, tol),
};
assert_eq!(surface.curvatures(0.3, 0.2, tol), expected);
}
}
}