use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::curve_math::bspline as curve;
use crate::math::boxtree::{BoxTree, Lazy, box_distance};
use crate::math::newton_min_2d;
use crate::projection::SurfaceProjection;
use crate::surface_math::bspline as math;
use crate::surfaces::ParametricSurface;
use crate::{Point3D, Tolerance, Transform, Vector3D};
pub use crate::curves::BSplineConstructionError;
#[derive(Debug, Clone, PartialEq)]
pub struct BSplineSurface {
u_degree: usize,
v_degree: usize,
u_periodic: bool,
v_periodic: bool,
poles: Vec<Vec<Point3D>>,
weights: Option<Vec<Vec<f64>>>,
u_knots: Vec<f64>,
u_mults: Vec<u32>,
v_knots: Vec<f64>,
v_mults: Vec<u32>,
u_flat: Vec<f64>,
v_flat: Vec<f64>,
flat_poles: Vec<f64>,
n_u: usize,
n_v: usize,
h: usize,
patches: Lazy<PatchTree>,
}
#[derive(Clone)]
struct PatchTree {
u_breaks: Vec<f64>,
v_breaks: Vec<f64>,
u_creases: Vec<bool>,
v_creases: Vec<bool>,
u_spans: (Vec<usize>, Vec<f64>),
v_spans: (Vec<usize>, Vec<f64>),
tree: BoxTree,
}
impl PatchTree {
fn patch(&self, k: usize) -> ([f64; 4], [bool; 4]) {
let n_vspans = self.v_breaks.len() - 1;
let (a, b) = (k / n_vspans, k % n_vspans);
(
[
self.u_breaks[a],
self.u_breaks[a + 1],
self.v_breaks[b],
self.v_breaks[b + 1],
],
[
self.u_creases[a],
self.u_creases[a + 1],
self.v_creases[b],
self.v_creases[b + 1],
],
)
}
fn len(&self) -> usize {
(self.u_breaks.len() - 1) * (self.v_breaks.len() - 1)
}
fn resolution(&self, point: Point3D) -> f64 {
let b = self.tree.bounds();
let diagonal =
((b[3] - b[0]).powi(2) + (b[4] - b[1]).powi(2) + (b[5] - b[2]).powi(2)).sqrt();
1e-12 * (diagonal + box_distance(point, b))
}
}
impl BSplineSurface {
#[allow(clippy::too_many_arguments)] pub fn new(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
Self::build(
u_degree,
v_degree,
poles,
None,
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
)
}
#[allow(clippy::too_many_arguments)] pub fn new_rational(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
weights: Vec<Vec<f64>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
Self::build(
u_degree,
v_degree,
poles,
Some(weights),
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
)
}
#[allow(clippy::too_many_arguments)] fn build(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
weights: Option<Vec<Vec<f64>>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
let n_u = poles.len();
if n_u == 0 || poles[0].is_empty() {
return Err(BSplineConstructionError::PoleCountMismatch);
}
let n_v = poles[0].len();
if poles.iter().any(|row| row.len() != n_v) {
return Err(BSplineConstructionError::PoleCountMismatch);
}
if let Some(ws) = &weights {
if ws.len() != n_u || ws.iter().any(|row| row.len() != n_v) {
return Err(BSplineConstructionError::WeightCountMismatch);
}
if ws.iter().flatten().any(|&w| w <= 0.0) {
return Err(BSplineConstructionError::NonPositiveWeight);
}
}
curve::validate_direction(u_degree, n_u, &u_knots, &u_multiplicities, u_periodic)?;
curve::validate_direction(v_degree, n_v, &v_knots, &v_multiplicities, v_periodic)?;
let u_flat = curve::flat_knots(&u_knots, &u_multiplicities);
let v_flat = curve::flat_knots(&v_knots, &v_multiplicities);
let h = if weights.is_some() { 4 } else { 3 };
let flat_poles = pack_flat_poles(&poles, weights.as_ref(), h);
Ok(BSplineSurface {
u_degree,
v_degree,
u_periodic,
v_periodic,
poles,
weights,
u_knots,
u_mults: u_multiplicities,
v_knots,
v_mults: v_multiplicities,
u_flat,
v_flat,
flat_poles,
n_u,
n_v,
h,
patches: Lazy::default(),
})
}
pub fn u_degree(&self) -> usize {
self.u_degree
}
pub fn v_degree(&self) -> usize {
self.v_degree
}
pub fn is_u_periodic(&self) -> bool {
self.u_periodic
}
pub fn is_v_periodic(&self) -> bool {
self.v_periodic
}
pub fn is_rational(&self) -> bool {
self.weights.is_some()
}
pub fn transformed(&self, transform: Transform) -> BSplineSurface {
let poles: Vec<Vec<Point3D>> = self
.poles
.iter()
.map(|row| row.iter().map(|&p| transform.apply_point(p)).collect())
.collect();
let flat_poles = pack_flat_poles(&poles, self.weights.as_ref(), self.h);
BSplineSurface {
u_degree: self.u_degree,
v_degree: self.v_degree,
u_periodic: self.u_periodic,
v_periodic: self.v_periodic,
poles,
weights: self.weights.clone(),
u_knots: self.u_knots.clone(),
u_mults: self.u_mults.clone(),
v_knots: self.v_knots.clone(),
v_mults: self.v_mults.clone(),
u_flat: self.u_flat.clone(),
v_flat: self.v_flat.clone(),
flat_poles,
n_u: self.n_u,
n_v: self.n_v,
h: self.h,
patches: Lazy::default(),
}
}
pub fn eval_point(&self, u: f64, v: f64) -> Point3D {
math::surface_d0(
u,
v,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
)
}
pub fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point3D> {
uvs.iter().map(|&(u, v)| self.eval_point(u, v)).collect()
}
pub fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector3D {
match (du, dv) {
(0, 0) => panic!(
"eval_derivative: du + dv must be >= 1 (use eval_point for the (0, 0) order)"
),
(1, 0) | (0, 1) => {
let (_s, su, sv) = self.eval_surface_d1(u, v);
if du == 1 { su } else { sv }
}
(2, 0) | (1, 1) | (0, 2) => {
let [_, _, _, suu, suv, svv] = self.eval_surface_d2(u, v);
match du {
2 => suu,
1 => suv,
_ => svv,
}
}
_ => panic!(
"eval_derivative: order du={du}, dv={dv} is not supported (du + dv must be <= 2)"
),
}
}
pub fn contains(&self, point: Point3D, tol: Tolerance) -> bool {
self.project_point(point, tol).distance <= tol.confusion
}
pub fn extrema(&self, point: Point3D, tol: Tolerance) -> Vec<SurfaceProjection> {
use crate::projection::snap_distance;
let global = self.project_point(point, tol);
let patches = self.patch_tree();
let mut search = PatchSearch::new(self, point, patches.resolution(point), true);
for k in 0..patches.len() {
let (rect, creases) = patches.patch(k);
if let Some(root) = search.root(patches, k) {
search.search(root, rect, creases);
}
}
let (u0, u1) = self.u_param_range();
let (v0, v1) = self.v_param_range();
let u_regions = curve::smooth_regions(
&self.u_knots,
&self.u_mults,
self.u_degree,
self.u_periodic,
u0,
u1,
);
let v_regions = curve::smooth_regions(
&self.v_knots,
&self.v_mults,
self.v_degree,
self.v_periodic,
v0,
v1,
);
let (u_period, v_period) = (
self.u_periodic.then_some(u1 - u0),
self.v_periodic.then_some(v1 - v0),
);
let mut candidates = vec![(global.u, global.v)];
for (u, v) in search.minima {
for (ubounds, u) in curve::regions_at(&u_regions, u, u_period) {
for (vbounds, v) in curve::regions_at(&v_regions, v, v_period) {
candidates.push(self.refine_projection(point, u, v, [ubounds, vbounds]));
}
}
}
let mut out: Vec<SurfaceProjection> = Vec::new();
let mut kept: Vec<Point3D> = Vec::new();
for (u, v) in candidates {
let p = self.eval_point(u, v);
if kept.iter().any(|q| q.distance(p) <= tol.confusion) {
continue;
}
kept.push(p);
out.push(SurfaceProjection {
u,
v,
distance: snap_distance(p.distance(point), tol.confusion),
});
}
out[1..].sort_by(|x, y| x.distance.total_cmp(&y.distance));
out
}
pub fn project_point(&self, point: Point3D, tol: Tolerance) -> SurfaceProjection {
use crate::projection::snap_distance;
let patches = self.patch_tree();
let mut search = PatchSearch::new(self, point, patches.resolution(point), false);
let mut roots = Vec::new();
patches.tree.nearest_first(point, |k| {
if let Some(root) = search.root(patches, k) {
let (rect, creases) = patches.patch(k);
roots.push((root, rect, creases));
}
search.best.0
});
roots.sort_by(|a, b| a.0.key.total_cmp(&b.0.key));
for (root, rect, creases) in roots {
search.search(root, rect, creases);
}
let (distance, u, v) = search.best;
SurfaceProjection {
u,
v,
distance: snap_distance(distance, tol.confusion),
}
}
pub fn project_points(&self, points: &[Point3D], tol: Tolerance) -> Vec<SurfaceProjection> {
points.iter().map(|&p| self.project_point(p, tol)).collect()
}
fn patch_tree(&self) -> &PatchTree {
self.patches.get_or_init(|| {
let (u0, u1) = self.u_param_range();
let (v0, v1) = self.v_param_range();
let u_breaks = curve::span_breaks(&self.u_knots, u0, u1);
let v_breaks = curve::span_breaks(&self.v_knots, v0, v1);
let boxes = math::patch_boxes(
&u_breaks,
&v_breaks,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
);
PatchTree {
tree: BoxTree::new(&boxes, v_breaks.len() - 1),
u_spans: curve::span_bezier_matrices(
&u_breaks,
self.u_degree,
self.u_periodic,
&self.u_flat,
self.n_u,
),
v_spans: curve::span_bezier_matrices(
&v_breaks,
self.v_degree,
self.v_periodic,
&self.v_flat,
self.n_v,
),
u_creases: curve::crease_breaks(
&self.u_knots,
&self.u_mults,
self.u_degree,
self.u_periodic,
&u_breaks,
),
v_creases: curve::crease_breaks(
&self.v_knots,
&self.v_mults,
self.v_degree,
self.v_periodic,
&v_breaks,
),
u_breaks,
v_breaks,
}
})
}
fn eval_surface_d1(&self, u: f64, v: f64) -> (Point3D, Vector3D, Vector3D) {
math::surface_d1(
u,
v,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
)
}
fn eval_surface_d2(&self, u: f64, v: f64) -> [Vector3D; 6] {
math::surface_d2(
u,
v,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
)
}
fn refine_projection(
&self,
point: Point3D,
u: f64,
v: f64,
bounds: [Option<(f64, f64)>; 2],
) -> (f64, f64) {
let (u0, u1) = self.u_param_range();
let (v0, v1) = self.v_param_range();
let target = Vector3D::new(point.x, point.y, point.z);
let objective = |u: f64, v: f64| {
let [s, su, sv, suu, suv, svv] =
self.eval_surface_d2(curve::inward(u, bounds[0]), curve::inward(v, bounds[1]));
let r = s - target;
let huv = su.dot(sv) + suv.dot(r);
(
0.5 * r.dot(r),
[su.dot(r), sv.dot(r)],
[
[su.dot(su) + suu.dot(r), huv],
[huv, sv.dot(sv) + svv.dot(r)],
],
)
};
let [u, v] = newton_min_2d(objective, [u, v], bounds, 1e-14, 100);
let wrap = |x: f64, periodic: bool, lo: f64, hi: f64| {
if periodic {
lo + (x - lo).rem_euclid(hi - lo)
} else {
x
}
};
(
wrap(u, self.u_periodic, u0, u1),
wrap(v, self.v_periodic, v0, v1),
)
}
fn u_param_range(&self) -> (f64, f64) {
let sum_mults: u32 = self.u_mults.iter().sum();
curve::param_range(
&self.u_flat,
self.u_degree,
self.u_periodic,
sum_mults as usize,
)
}
fn v_param_range(&self) -> (f64, f64) {
let sum_mults: u32 = self.v_mults.iter().sum();
curve::param_range(
&self.v_flat,
self.v_degree,
self.v_periodic,
sum_mults as usize,
)
}
}
impl ParametricSurface for BSplineSurface {
fn eval_point(&self, u: f64, v: f64) -> Point3D {
BSplineSurface::eval_point(self, u, v)
}
fn project_point(&self, point: Point3D, tol: Tolerance) -> SurfaceProjection {
BSplineSurface::project_point(self, point, tol)
}
fn contains(&self, point: Point3D, tol: Tolerance) -> bool {
BSplineSurface::contains(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector3D {
BSplineSurface::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
self.u_param_range()
}
fn v_bounds(&self) -> (f64, f64) {
self.v_param_range()
}
fn u_period(&self) -> Option<f64> {
if self.u_periodic {
let (first, last) = self.u_param_range();
Some(last - first)
} else {
None
}
}
fn v_period(&self) -> Option<f64> {
if self.v_periodic {
let (first, last) = self.v_param_range();
Some(last - first)
} else {
None
}
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point3D> {
BSplineSurface::eval_points(self, uvs)
}
}
const MAX_SPLITS: usize = 256;
const MAX_FALLBACKS: usize = 8;
const MAX_DEPTH: u32 = 80;
struct PatchSearch<'a> {
surface: &'a BSplineSurface,
point: Point3D,
rows: usize,
cols: usize,
size: usize,
nets: Vec<f64>,
free: Vec<usize>,
pending: BinaryHeap<Subpatch>,
points: Vec<[f64; 3]>,
scratch: Vec<f64>,
best: (f64, f64, f64),
resolution: f64,
all: bool,
minima: Vec<(f64, f64)>,
budget: usize,
}
#[derive(Clone, Copy)]
struct Subpatch {
lower: f64,
key: f64,
slot: usize,
rect: [f64; 4],
depth: u32,
edge: Option<(usize, usize)>,
}
impl PartialEq for Subpatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Subpatch {}
impl PartialOrd for Subpatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Subpatch {
fn cmp(&self, other: &Self) -> Ordering {
other.key.total_cmp(&self.key)
}
}
enum Verdict {
None,
Corner([usize; 2]),
Edge(usize, usize),
Pinned(usize, usize),
Convex,
Unknown(bool),
}
impl<'a> PatchSearch<'a> {
fn new(surface: &'a BSplineSurface, point: Point3D, resolution: f64, all: bool) -> Self {
let (rows, cols) = (surface.u_degree + 1, surface.v_degree + 1);
let size = rows * cols * surface.h;
PatchSearch {
surface,
point,
rows,
cols,
size,
nets: Vec::new(),
free: Vec::new(),
pending: BinaryHeap::new(),
points: Vec::with_capacity(rows * cols),
scratch: vec![0.0; size],
best: (f64::INFINITY, 0.0, 0.0),
resolution,
all,
minima: Vec::new(),
budget: MAX_SPLITS,
}
}
fn root(&mut self, patches: &PatchTree, k: usize) -> Option<Subpatch> {
let s = self.surface;
let slot = self.slot();
let n_vspans = patches.v_breaks.len() - 1;
let (a, b) = (k / n_vspans, k % n_vspans);
let (p, q) = (s.u_degree, s.v_degree);
let (mu, mv) = ((p + 1) * (p + 1), (q + 1) * (q + 1));
math::patch_bezier(
(
&patches.u_spans.1[a * mu..(a + 1) * mu],
patches.u_spans.0[a],
p,
s.n_u,
),
(
&patches.v_spans.1[b * mv..(b + 1) * mv],
patches.v_spans.0[b],
q,
s.n_v,
),
&s.flat_poles,
s.h,
&mut self.scratch,
&mut self.nets[slot..slot + self.size],
);
self.node(slot, patches.patch(k).0, 0, None)
}
fn search(&mut self, root: Subpatch, rect: [f64; 4], creases: [bool; 4]) {
self.pending.push(root);
let (mut splits, mut fallbacks) = (0, 0);
while let Some(node) = self.pending.pop() {
if !self.all && node.lower >= self.best.0 - self.resolution {
self.free.push(node.slot);
continue;
}
let edge = std::array::from_fn(|i| creases[i] && node.rect[i] == rect[i]);
let verdict = self.verdict(&node, edge);
match verdict {
Verdict::None => {}
Verdict::Corner([s, t]) => {
let p = self.points[s * (self.rows - 1) * self.cols + t * (self.cols - 1)];
let distance = Point3D::new(p[0], p[1], p[2]).distance(self.point);
self.found(distance, node.rect[s], node.rect[2 + t]);
}
Verdict::Edge(direction, side) => {
let start = self.closest_control_point(Some((direction, side)));
self.minimise(&node, start);
}
Verdict::Pinned(direction, side) => {
math::collapse_net(
&mut self.nets[node.slot..node.slot + self.size],
self.rows,
self.cols,
self.surface.h,
direction == 0,
side,
);
self.push(node.slot, node.rect, node.depth, Some((direction, side)));
continue;
}
Verdict::Convex => {
let start = self.closest_control_point(None);
self.minimise(&node, start);
}
Verdict::Unknown(along_u) => {
if node.depth < MAX_DEPTH && splits < self.budget {
splits += 1;
self.split(&node, along_u);
} else if fallbacks < MAX_FALLBACKS {
fallbacks += 1;
let start = self.closest_control_point(node.edge);
self.minimise(&node, start);
}
}
}
self.free.push(node.slot);
}
if splits >= self.budget && !self.all {
self.budget = MAX_SPLITS / 8;
}
}
fn slot(&mut self) -> usize {
self.free.pop().unwrap_or_else(|| {
let slot = self.nets.len();
self.nets.resize(slot + self.size, 0.0);
slot
})
}
fn push(&mut self, slot: usize, rect: [f64; 4], depth: u32, edge: Option<(usize, usize)>) {
if let Some(node) = self.node(slot, rect, depth, edge) {
self.pending.push(node);
}
}
fn node(
&mut self,
slot: usize,
rect: [f64; 4],
depth: u32,
edge: Option<(usize, usize)>,
) -> Option<Subpatch> {
let (rows, cols) = (self.rows, self.cols);
if self.all {
return Some(Subpatch {
lower: 0.0,
key: 0.0,
slot,
rect,
depth,
edge,
});
}
math::net_points(
&self.nets[slot..slot + self.size],
self.surface.h,
&mut self.points,
);
for (s, t) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
let p = self.points[s * (rows - 1) * cols + t * (cols - 1)];
let distance = Point3D::new(p[0], p[1], p[2]).distance(self.point);
if distance < self.best.0 {
let [s, t] = match edge {
Some((0, side)) => [side, t],
Some((_, side)) => [s, side],
None => [s, t],
};
self.best = (distance, rect[s], rect[2 + t]);
}
}
let q = [self.point.x, self.point.y, self.point.z];
let lower = math::net_distance_bound(&self.points, rows, cols, q);
if lower >= self.best.0 - self.resolution {
self.free.push(slot);
return None;
}
let key = self
.points
.iter()
.map(|x| (0..3).map(|c| (x[c] - q[c]).powi(2)).sum::<f64>())
.fold(f64::INFINITY, f64::min)
.sqrt();
Some(Subpatch {
lower,
key,
slot,
rect,
depth,
edge,
})
}
fn verdict(&mut self, node: &Subpatch, creases: [bool; 4]) -> Verdict {
let (rows, cols, h) = (self.rows, self.cols, self.surface.h);
let net = &self.nets[node.slot..node.slot + self.size];
math::net_points(net, h, &mut self.points);
let q = [self.point.x, self.point.y, self.point.z];
let pinned = |gradient: [math::Interval; 2]| -> [Option<usize>; 2] {
std::array::from_fn(|direction| match node.edge {
Some((d, side)) if d == direction => Some(side),
_ => {
let g = gradient[direction];
if g.lo > 0.0 {
Some(0)
} else if g.hi < 0.0 {
Some(1)
} else {
None
}
}
})
};
if self.all {
let gradient =
math::distance_bounds(net, &self.points, rows, cols, h, q, false).gradient;
let smooth = |direction: usize, side: usize| !creases[2 * direction + side];
if pinned(gradient)
.iter()
.enumerate()
.any(|(direction, side)| side.is_some_and(|side| smooth(direction, side)))
{
return Verdict::None;
}
}
let bounds = math::distance_bounds(net, &self.points, rows, cols, h, q, true);
let [fss, fst, ftt] = bounds.hessian;
let pinned = pinned(bounds.gradient);
for direction in 0..2 {
let crease = |side: usize| creases[2 * direction + side];
let concave = bounds.hessian[2 * direction].hi < 0.0;
if pinned[direction].is_some_and(|side| !crease(side))
|| (concave && !crease(0) && !crease(1))
{
return Verdict::None;
}
}
if !creases.contains(&true) && nowhere_convex([fss, fst, ftt]) {
return Verdict::None;
}
match pinned {
[Some(s), Some(t)] => Verdict::Corner([s, t]),
[Some(s), None] if ftt.lo > 0.0 => Verdict::Edge(0, s),
[None, Some(t)] if fss.lo > 0.0 => Verdict::Edge(1, t),
[Some(s), None] if node.edge.is_none() => Verdict::Pinned(0, s),
[None, Some(t)] if node.edge.is_none() => Verdict::Pinned(1, t),
[Some(_), None] => Verdict::Unknown(false),
[None, Some(_)] => Verdict::Unknown(true),
[None, None]
if fss.lo > 0.0 && ftt.lo > 0.0 && fss.lo * ftt.lo > fst.magnitude().powi(2) =>
{
Verdict::Convex
}
_ => Verdict::Unknown(bounds.stretch[0] >= bounds.stretch[1]),
}
}
fn closest_control_point(&self, edge: Option<(usize, usize)>) -> (f64, f64) {
let (p, q) = (self.rows - 1, self.cols - 1);
let target = [self.point.x, self.point.y, self.point.z];
let distance = |i: usize, j: usize| {
let x = self.points[i * self.cols + j];
(0..3).map(|c| (x[c] - target[c]).powi(2)).sum::<f64>()
};
let candidates: Vec<(usize, usize)> = match edge {
Some((0, side)) => (0..=q).map(|j| (side * p, j)).collect(),
Some((_, side)) => (0..=p).map(|i| (i, side * q)).collect(),
None => (0..=p).flat_map(|i| (0..=q).map(move |j| (i, j))).collect(),
};
let (i, j) = candidates
.into_iter()
.min_by(|a, b| distance(a.0, a.1).total_cmp(&distance(b.0, b.1)))
.unwrap_or((0, 0));
(i as f64 / p as f64, j as f64 / q as f64)
}
fn minimise(&mut self, node: &Subpatch, (s, t): (f64, f64)) {
let r = node.rect;
let (u, v) = self.surface.refine_projection(
self.point,
r[0] + s * (r[1] - r[0]),
r[2] + t * (r[3] - r[2]),
[Some((r[0], r[1])), Some((r[2], r[3]))],
);
let distance = self.surface.eval_point(u, v).distance(self.point);
self.found(distance, u, v);
}
fn found(&mut self, distance: f64, u: f64, v: f64) {
if distance < self.best.0 {
self.best = (distance, u, v);
}
if self.all {
self.minima.push((u, v));
}
}
fn split(&mut self, node: &Subpatch, along_u: bool) {
self.scratch
.copy_from_slice(&self.nets[node.slot..node.slot + self.size]);
let (left, right) = (self.slot(), self.slot());
let (lo, hi) = if left < right {
let (a, b) = self.nets.split_at_mut(right);
(&mut a[left..left + self.size], &mut b[..self.size])
} else {
let (a, b) = self.nets.split_at_mut(left);
(&mut b[..self.size], &mut a[right..right + self.size])
};
math::halve_net(
&self.scratch,
self.rows,
self.cols,
self.surface.h,
along_u,
lo,
hi,
);
let r = node.rect;
let (first, second) = if along_u {
let mid = 0.5 * (r[0] + r[1]);
([r[0], mid, r[2], r[3]], [mid, r[1], r[2], r[3]])
} else {
let mid = 0.5 * (r[2] + r[3]);
([r[0], r[1], r[2], mid], [r[0], r[1], mid, r[3]])
};
self.push(left, first, node.depth + 1, node.edge);
self.push(right, second, node.depth + 1, node.edge);
}
}
fn nowhere_convex([a, b, c]: [math::Interval; 3]) -> bool {
let largest_ac = (a.lo * c.lo)
.max(a.lo * c.hi)
.max(a.hi * c.lo)
.max(a.hi * c.hi);
let smallest_b = if b.lo > 0.0 {
b.lo
} else if b.hi < 0.0 {
-b.hi
} else {
0.0
};
if largest_ac < smallest_b * smallest_b {
return true;
}
let (am, bm, cm) = (a.mid(), b.mid(), c.mid());
let lower = 0.5 * (am + cm) - (0.25 * (am - cm).powi(2) + bm * bm).sqrt();
let (x, y) = if (am - lower).abs() > (cm - lower).abs() {
(bm, lower - am)
} else {
(lower - cm, bm)
};
let along = if x * y >= 0.0 { b.hi } else { b.lo };
(x != 0.0 || y != 0.0) && x * x * a.hi + 2.0 * x * y * along + y * y * c.hi < 0.0
}
fn pack_flat_poles(poles: &[Vec<Point3D>], weights: Option<&Vec<Vec<f64>>>, h: usize) -> Vec<f64> {
let mut out = Vec::with_capacity(poles.len() * poles[0].len() * h);
for (iu, row) in poles.iter().enumerate() {
for (iv, p) in row.iter().enumerate() {
match weights {
Some(ws) => {
let w = ws[iu][iv];
out.extend_from_slice(&[p.x * w, p.y * w, p.z * w, w]);
}
None => out.extend_from_slice(&[p.x, p.y, p.z]),
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn bilinear() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0), Point3D::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_accessors() {
let s = bilinear();
assert_eq!(s.u_degree(), 1);
assert_eq!(s.v_degree(), 1);
assert!(!s.is_u_periodic());
assert!(!s.is_v_periodic());
assert!(!s.is_rational());
}
#[test]
fn test_bilinear_point_by_hand() {
let s = bilinear();
assert_eq!(s.eval_point(0.5, 0.5), Point3D::new(0.5, 0.5, 0.25));
assert_eq!(s.eval_point(0.0, 0.0), Point3D::new(0.0, 0.0, 0.0));
assert_eq!(s.eval_point(1.0, 1.0), Point3D::new(1.0, 1.0, 1.0));
assert_eq!(s.eval_point(0.3, 0.7), Point3D::new(0.3, 0.7, 0.3 * 0.7));
}
#[test]
fn test_bilinear_derivatives_by_hand() {
let s = bilinear();
assert_eq!(
s.eval_derivative(0.3, 0.7, 1, 0),
Vector3D::new(1.0, 0.0, 0.7)
);
assert_eq!(
s.eval_derivative(0.3, 0.7, 0, 1),
Vector3D::new(0.0, 1.0, 0.3)
);
}
#[test]
fn test_eval_points_matches_mapped_eval_point() {
let s = bilinear();
let uvs = [(0.0, 0.0), (0.2, 0.4), (0.5, 0.5), (1.0, 1.0)];
let expected: Vec<Point3D> = uvs.iter().map(|&(u, v)| s.eval_point(u, v)).collect();
assert_eq!(s.eval_points(&uvs), expected);
}
#[test]
#[should_panic(expected = "du + dv must be >= 1")]
fn test_eval_derivative_zero_order_panics() {
bilinear().eval_derivative(0.5, 0.5, 0, 0);
}
#[test]
fn test_eval_derivative_second_order_bilinear() {
let s = bilinear();
assert_eq!(
s.eval_derivative(0.5, 0.5, 2, 0),
Vector3D::new(0.0, 0.0, 0.0)
);
assert_eq!(
s.eval_derivative(0.5, 0.5, 1, 1),
Vector3D::new(0.0, 0.0, 1.0)
);
assert_eq!(
s.eval_derivative(0.5, 0.5, 0, 2),
Vector3D::new(0.0, 0.0, 0.0)
);
}
#[test]
#[should_panic(expected = "du + dv must be <= 2")]
fn test_eval_derivative_third_order_panics() {
bilinear().eval_derivative(0.5, 0.5, 2, 1);
}
#[test]
fn test_bounds_clamped() {
let s = bilinear();
assert_eq!(ParametricSurface::u_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::v_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::u_period(&s), None);
assert_eq!(ParametricSurface::v_period(&s), None);
}
fn ok_grid() -> Vec<Vec<Point3D>> {
vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0), Point3D::new(1.0, 1.0, 1.0)],
]
}
#[test]
fn test_empty_grid_is_pole_count_mismatch() {
let err = BSplineSurface::new(
1,
1,
vec![],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_ragged_grid_is_pole_count_mismatch() {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0)], ];
let err = BSplineSurface::new(
1,
1,
poles,
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_invalid_u_degree() {
let err = BSplineSurface::new(
0,
1,
ok_grid(),
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::InvalidDegree);
}
#[test]
fn test_u_pole_count_mismatch_from_knots() {
let err = BSplineSurface::new(
1,
1,
ok_grid(),
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_v_knots_not_increasing() {
let err = BSplineSurface::new(
1,
1,
ok_grid(),
vec![0.0, 1.0],
vec![2, 2],
vec![1.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::KnotsNotIncreasing);
}
#[test]
fn test_weight_count_mismatch_ragged() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0], vec![1.0]], vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::WeightCountMismatch);
}
#[test]
fn test_weight_count_mismatch_wrong_rows() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0]], vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::WeightCountMismatch);
}
#[test]
fn test_non_positive_weight() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 0.0], vec![1.0, 1.0]],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::NonPositiveWeight);
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0], vec![-2.0, 1.0]],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::NonPositiveWeight);
}
fn periodic_tube() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(2.0, 0.0, 0.0), Point3D::new(2.2, 0.0, 3.0)],
vec![
Point3D::new(1.0000000000000002, 1.7320508075688772, 0.0),
Point3D::new(1.1000000000000003, 1.905255888325765, 3.0),
],
vec![
Point3D::new(-0.9999999999999996, 1.7320508075688774, 0.0),
Point3D::new(-1.0999999999999996, 1.9052558883257653, 3.0),
],
vec![
Point3D::new(-2.0, 2.4492935982947064e-16, 0.0),
Point3D::new(-2.2, 2.6942229581241775e-16, 3.0),
],
vec![
Point3D::new(-1.0000000000000009, -1.7320508075688767, 0.0),
Point3D::new(-1.100000000000001, -1.9052558883257646, 3.0),
],
vec![
Point3D::new(1.0000000000000002, -1.7320508075688772, 0.0),
Point3D::new(1.1000000000000003, -1.905255888325765, 3.0),
],
];
BSplineSurface::new(
2,
1,
poles,
vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
vec![1, 1, 1, 1, 1, 1, 1],
vec![0.0, 1.0],
vec![2, 2],
true,
false,
)
.unwrap()
}
#[test]
fn test_periodic_bounds_and_period() {
let s = periodic_tube();
assert_eq!(ParametricSurface::u_bounds(&s), (0.0, 6.0));
assert_eq!(ParametricSurface::v_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::u_period(&s), Some(6.0));
assert_eq!(ParametricSurface::v_period(&s), None);
}
#[test]
fn test_periodic_seam_matches_across_period() {
let s = periodic_tube();
for v in [0.0, 0.3, 0.7, 1.0] {
let p0 = s.eval_point(0.0, v);
let p1 = s.eval_point(6.0, v);
assert!((p0.x - p1.x).abs() < 1e-9, "x seam v={v}");
assert!((p0.y - p1.y).abs() < 1e-9, "y seam v={v}");
assert!((p0.z - p1.z).abs() < 1e-9, "z seam v={v}");
let d0 = s.eval_derivative(0.0, v, 1, 0);
let d1 = s.eval_derivative(6.0, v, 1, 0);
assert!((d0.x - d1.x).abs() < 1e-9, "du.x seam v={v}");
assert!((d0.y - d1.y).abs() < 1e-9, "du.y seam v={v}");
assert!((d0.z - d1.z).abs() < 1e-9, "du.z seam v={v}");
}
}
#[test]
fn test_periodic_out_of_window_u_matches_golden() {
let s = periodic_tube();
let p = s.eval_point(-0.8, 0.6);
assert!((p.x - 1.7596000000000003).abs() < 1e-7);
assert!((p.y - -0.5507921568069025).abs() < 1e-7);
assert!((p.z - 1.7999999999999998).abs() < 1e-7);
}
fn bilinear_patch() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 2.0, 0.0)],
vec![Point3D::new(2.0, 0.0, 0.0), Point3D::new(2.0, 2.0, 0.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_bspline_surface_project_point_planar() {
let s = bilinear_patch();
let tol = Tolerance::DEFAULT;
let proj = s.project_point(Point3D::new(1.0, 1.0, 1.0), tol);
assert!((proj.u - 0.5).abs() < 1e-9);
assert!((proj.v - 0.5).abs() < 1e-9);
assert!((proj.distance - 1.0).abs() < 1e-9);
let edge = s.project_point(Point3D::new(3.0, 1.0, 0.0), tol);
assert!((edge.u - 1.0).abs() < 1e-9);
assert!((edge.v - 0.5).abs() < 1e-9);
assert!((edge.distance - 1.0).abs() < 1e-9);
}
#[test]
fn test_bspline_surface_contains_planar() {
let s = bilinear_patch();
let tol = Tolerance::DEFAULT;
assert!(s.contains(Point3D::new(1.0, 1.0, 0.0), tol));
assert!(s.contains(s.eval_point(0.25, 0.75), tol));
assert!(!s.contains(Point3D::new(1.0, 1.0, 1.0), tol));
}
#[test]
fn test_bspline_surface_project_point_tube() {
let s = periodic_tube();
let tol = Tolerance::DEFAULT;
let on = s.eval_point(0.5, 0.5);
assert_eq!(s.project_point(on, tol).distance, 0.0);
assert!(s.contains(on, tol));
let query = Point3D::new(0.0, 0.0, 5.0);
let proj = s.project_point(query, tol);
let (u0, u1) = ParametricSurface::u_bounds(&s);
let (v0, v1) = ParametricSurface::v_bounds(&s);
let mut best = f64::INFINITY;
for j in 0..=60 {
for i in 0..=60 {
let d = s
.eval_point(
u0 + (u1 - u0) * i as f64 / 60.0,
v0 + (v1 - v0) * j as f64 / 60.0,
)
.distance(query);
best = best.min(d);
}
}
assert!(proj.distance <= best + 1e-9);
assert!(best - proj.distance < 1e-2);
}
fn transformed_test_patch() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0), Point3D::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_bspline_surface_transformed_translation_spot_check() {
let surface = transformed_test_patch();
let t = Transform::translation(Vector3D::new(0.0, 0.0, 4.0));
let moved = surface.transformed(t);
assert_eq!(moved.eval_point(0.5, 0.5), Point3D::new(0.5, 0.5, 4.25));
assert_eq!(moved.u_degree(), surface.u_degree());
assert_eq!(moved.v_degree(), surface.v_degree());
}
#[test]
fn test_bspline_surface_transformed_round_trip_and_mirror() {
use crate::{Axis3D, Frame3D};
use std::f64::consts::PI;
let surface = transformed_test_patch();
let axis = Axis3D::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
let t = Transform::rotation(axis, PI / 2.0).then(Transform::scaling(Point3D::ORIGIN, 2.0));
let moved = surface.transformed(t);
let tol = Tolerance::DEFAULT;
for (u, v) in [(0.0, 0.0), (0.5, 0.5), (1.0, 0.25)] {
assert!(moved.contains(t.apply_point(surface.eval_point(u, v)), tol));
}
let inv =
Transform::scaling(Point3D::ORIGIN, 0.5).then(Transform::rotation(axis, -PI / 2.0));
let back = moved.transformed(inv);
for (u, v) in [(0.0, 0.0), (0.5, 0.5), (1.0, 0.25)] {
assert!(back.eval_point(u, v).distance(surface.eval_point(u, v)) < 1e-9);
}
let m = Transform::mirror_plane(Frame3D::WORLD);
let mirrored = surface.transformed(m);
for (u, v) in [(0.0, 0.0), (0.5, 0.5), (1.0, 0.25)] {
assert!(mirrored.contains(m.apply_point(surface.eval_point(u, v)), tol));
}
assert_eq!(mirrored.transformed(m), surface);
}
fn bernstein_point(net: &[f64], rows: usize, cols: usize, h: usize, s: f64, t: f64) -> Point3D {
let lerp = |a: &[f64], n: usize, x: f64| {
let mut a = a.to_vec();
for r in 1..n {
for k in 0..(n - r) * h {
a[k] = (1.0 - x) * a[k] + x * a[k + h];
}
}
a[..h].to_vec()
};
let column: Vec<f64> = (0..rows)
.flat_map(|i| lerp(&net[i * cols * h..(i + 1) * cols * h], cols, t))
.collect();
let p = lerp(&column, rows, s);
let w = if h == 4 { p[3] } else { 1.0 };
Point3D::new(p[0] / w, p[1] / w, p[2] / w)
}
fn two_basins() -> BSplineSurface {
let z = [1.0, 1.0, -1.0, 1.2, -1.04, 1.0, 1.0];
let poles = (0..7)
.map(|i| {
(0..2)
.map(|j| Point3D::new(i as f64 / 6.0, j as f64, z[i]))
.collect()
})
.collect();
BSplineSurface::new(
6,
1,
poles,
vec![0.0, 1.0],
vec![7, 7],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
fn profile_distance(s: &BSplineSurface, point: Point3D) -> f64 {
let d = |u: f64| s.eval_point(u.clamp(0.0, 1.0), 0.5).distance(point);
let n = 20_000;
let best = (0..=n)
.map(|i| i as f64 / n as f64)
.min_by(|a, b| d(*a).total_cmp(&d(*b)))
.unwrap();
let (mut a, mut b) = (best - 1.0 / n as f64, best + 1.0 / n as f64);
let g = 0.5 * (5f64.sqrt() - 1.0);
for _ in 0..80 {
let (x, y) = (b - g * (b - a), a + g * (b - a));
if d(x) < d(y) {
b = y;
} else {
a = x;
}
}
d(0.5 * (a + b))
}
#[test]
fn test_project_point_two_basins_in_one_patch() {
let s = two_basins();
let tol = Tolerance::DEFAULT;
for x in [0.38, 0.42, 0.45, 0.48, 0.5, 0.55] {
let query = Point3D::new(x, 0.5, 0.6);
let proj = s.project_point(query, tol);
let reference = profile_distance(&s, query);
assert!(
(proj.distance - reference).abs() < 1e-12,
"x = {x}: {} vs {reference}",
proj.distance
);
let (p, su, sv) = s.eval_surface_d1(proj.u, proj.v);
let r = p - query;
assert!(r.dot(su).abs() < 1e-9 * su.magnitude() * r.magnitude());
assert!(r.dot(sv).abs() < 1e-9 * sv.magnitude() * r.magnitude());
let extrema = s.extrema(query, tol);
assert_eq!(extrema[0].distance, proj.distance);
assert!(extrema.len() >= 2, "x = {x}: {extrema:?}");
}
}
fn folded_net() -> BSplineSurface {
let row = |p: &[(f64, f64, f64)]| -> Vec<Point3D> {
p.iter().map(|&(x, y, z)| Point3D::new(x, y, z)).collect()
};
#[rustfmt::skip]
let poles = vec![
row(&[(4.52, -4.72, -0.97), (0.94, -2.11, -2.98), (0.99, -4.89, 0.49), (4.03, 2.02, 2.08), (2.35, -4.38, -1.07),
(-2.22, -2.8, -3.92), (-4.6, 2.81, -3.13), (3.21, 2.17, 2.1), (0.74, 0.06, -3.3), (3.03, 3.51, 1.31)]),
row(&[(2.04, -4.83, 3.15), (-4.97, 1.53, 2.88), (0.8, 4.4, 3.79), (-3.33, -3.15, -4.61), (3.81, -4.51, 1.64),
(0.84, -1.12, -4.38), (-1.41, -3.42, -4.87), (-1.57, -3.53, 0.75), (-4.0, 4.32, -1.94), (-2.98, 4.31, -3.09)]),
row(&[(-4.47, 0.45, 0.78), (0.81, -0.98, 2.47), (4.44, 2.43, 2.29), (-1.33, -4.42, 0.08), (1.79, -2.52, -1.4),
(-0.55, -4.36, -4.45), (-4.85, 3.25, -2.06), (0.55, 3.93, -4.22), (2.3, 0.75, 0.34), (2.87, 3.87, -3.4)]),
row(&[(-1.7, 2.4, -2.3), (-0.07, -0.04, 1.97), (-4.41, -0.31, -0.26), (0.5, 1.27, 2.44), (4.69, -2.87, -4.96),
(3.99, -1.46, 4.44), (-4.07, 2.78, -0.86), (3.23, -2.36, -2.98), (1.69, -2.35, 4.7), (-3.49, -0.13, -0.89)]),
row(&[(4.62, -2.18, -3.87), (4.1, -0.82, 0.15), (3.88, -2.59, 4.99), (0.8, 3.99, -0.12), (-3.78, 1.26, -0.58),
(3.27, -1.56, 0.59), (-0.55, 0.41, -2.35), (-2.7, 3.86, 4.63), (-1.19, -3.46, 2.63), (-1.62, -1.04, 1.81)]),
];
BSplineSurface::new(
2,
5,
poles,
vec![0.0, 1.0, 2.0, 3.0],
vec![3, 1, 1, 3],
vec![0.0, 0.56, 1.0],
vec![6, 4, 6],
false,
false,
)
.unwrap()
}
#[test]
fn test_project_point_close_basins_on_a_folded_net() {
let s = folded_net();
let tol = Tolerance::DEFAULT;
let on = s.eval_point(0.05, 0.1);
let proj = s.project_point(on, tol);
assert_eq!(proj.distance, 0.0);
let extrema = s.extrema(on, tol);
assert_eq!(extrema[0].distance, 0.0);
assert!(
extrema[1..]
.iter()
.any(|e| e.u == 0.0 && e.distance > 0.05 && e.distance < 0.1),
"{extrema:?}"
);
let n = 40;
for i in 0..=n {
for j in 0..=n {
let (u, v) = (3.0 * i as f64 / n as f64, j as f64 / n as f64);
let on = s.eval_point(u, v);
let proj = s.project_point(on, tol);
assert!(proj.distance < 1e-12, "({u}, {v}): {proj:?}");
}
}
}
#[test]
fn test_project_point_on_surface_is_zero() {
let tube = periodic_tube();
let (u0, u1) = ParametricSurface::u_bounds(&tube);
let tol = Tolerance::DEFAULT;
for s in [two_basins(), tube, rational_quarter_cylinder(2.0)] {
let (u0, u1) = if s.is_u_periodic() {
(u0, u1)
} else {
ParametricSurface::u_bounds(&s)
};
let (v0, v1) = ParametricSurface::v_bounds(&s);
for i in 0..=7 {
for j in 0..=5 {
let (u, v) = (
u0 + (u1 - u0) * i as f64 / 7.0,
v0 + (v1 - v0) * j as f64 / 5.0,
);
let on = s.eval_point(u, v);
let proj = s.project_point(on, tol);
assert_eq!(proj.distance, 0.0);
assert!(s.eval_point(proj.u, proj.v).distance(on) < 1e-12);
}
}
}
}
fn rational_quarter_cylinder(r: f64) -> BSplineSurface {
let arc = [(r, 0.0), (r, r), (0.0, r)];
let poles = arc
.iter()
.map(|&(x, y)| vec![Point3D::new(x, y, 0.0), Point3D::new(x, y, 1.0)])
.collect();
let w = std::f64::consts::FRAC_1_SQRT_2;
BSplineSurface::new_rational(
2,
1,
poles,
vec![vec![1.0, 1.0], vec![w, w], vec![1.0, 1.0]],
vec![0.0, 1.0],
vec![3, 3],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_project_point_exact_foot_on_rational_cylinder() {
let radius = 2.0;
let s = rational_quarter_cylinder(radius);
let tol = Tolerance::DEFAULT;
for (rho, theta, z) in [(0.5, 0.3, 0.2), (3.0, 1.1, 0.7), (7.5, 0.05, 0.5)] {
let query = Point3D::new(rho * f64::cos(theta), rho * f64::sin(theta), z);
let proj = s.project_point(query, tol);
assert!((proj.distance - (rho - radius).abs()).abs() < 1e-12);
let foot = s.eval_point(proj.u, proj.v);
let expected = Point3D::new(radius * f64::cos(theta), radius * f64::sin(theta), z);
assert!(foot.distance(expected) < 1e-12);
}
}
#[test]
fn test_project_point_exact_foot_on_edge_corner_and_crease() {
let tol = Tolerance::DEFAULT;
let s = bilinear_patch();
let proj = s.project_point(Point3D::new(3.0, 1.0, 1.0), tol);
assert_eq!((proj.u, proj.v), (1.0, 0.5));
assert!((proj.distance - 2f64.sqrt()).abs() < 1e-12);
let proj = s.project_point(Point3D::new(3.0, 3.0, 1.0), tol);
assert_eq!((proj.u, proj.v), (1.0, 1.0));
assert!((proj.distance - 3f64.sqrt()).abs() < 1e-12);
let ridge = BSplineSurface::new(
1,
1,
vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 1.0), Point3D::new(1.0, 1.0, 1.0)],
vec![Point3D::new(2.0, 0.0, 0.0), Point3D::new(2.0, 1.0, 0.0)],
],
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap();
let proj = ridge.project_point(Point3D::new(1.0, 0.25, 3.0), tol);
assert_eq!(proj.u, 1.0);
assert!((proj.v - 0.25).abs() < 1e-12);
assert!((proj.distance - 2.0).abs() < 1e-12);
}
#[test]
fn test_patch_nets_match_the_surface_and_halve() {
for s in [
two_basins(),
periodic_tube(),
rational_quarter_cylinder(1.5),
] {
let patches = s.patch_tree();
let mut search = PatchSearch::new(&s, Point3D::ORIGIN, 0.0, true);
let (rows, cols, h) = (s.u_degree + 1, s.v_degree + 1, s.h);
for k in 0..patches.len() {
let (rect, _) = patches.patch(k);
let root = search.root(patches, k).unwrap();
let net = search.nets[root.slot..root.slot + search.size].to_vec();
let mut halves = vec![vec![0.0; net.len()]; 2];
let [left, right] = &mut halves[..] else {
unreachable!()
};
math::halve_net(&net, rows, cols, h, true, left, right);
for (a, b) in [(0.0, 0.0), (0.3, 0.8), (1.0, 0.5), (0.7, 1.0)] {
let (u, v) = (
rect[0] + a * (rect[1] - rect[0]),
rect[2] + b * (rect[3] - rect[2]),
);
let expected = s.eval_point(u, v);
let got = bernstein_point(&net, rows, cols, h, a, b);
assert!(got.distance(expected) < 1e-12);
let half = if a < 0.5 {
bernstein_point(left, rows, cols, h, 2.0 * a, b)
} else {
bernstein_point(right, rows, cols, h, 2.0 * a - 1.0, b)
};
assert!(half.distance(expected) < 1e-12);
}
}
}
}
}