use super::completeness::{Region, box_union, meet, of_unsettled, span};
use super::curve_surface_hull::Range;
use super::pieces::{
CurvePieces, Pool, apart_along, bounding_box, box_size, cross, curves_meet_at_most_once, dot,
euclidean, extent, norm, sub, tangent_sign,
};
use super::results::sort_by_known_parameter;
use super::{Completeness, CurveCurveExtremum, CurveCurveHit};
use crate::curve_math::bspline::LARGE;
use crate::curves::BSplineCurve;
use crate::math::boxtree::{Aabb, BoxTree, boxes_apart};
use crate::math::{newton_min_1d, newton_min_2d, solve_2x2};
use crate::surface_math::bspline::{Interval, derivative_bounds};
use crate::{Point, Tolerance};
use std::ops::Deref;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CurveCurveHits {
hits: Vec<CurveCurveHit>,
completeness: Completeness<CurveCurveRegion>,
}
impl CurveCurveHits {
pub(super) fn new(
hits: Vec<CurveCurveHit>,
completeness: Completeness<CurveCurveRegion>,
) -> Self {
CurveCurveHits { hits, completeness }
}
pub(super) fn sampled(hits: Vec<CurveCurveHit>) -> Self {
Self::new(hits, Completeness::Sampled)
}
pub(super) fn add_sampled(&mut self, sampled: Vec<CurveCurveHit>, tol: Tolerance) {
let merge_gap = tol.confusion.sqrt();
for hit in sampled {
if self
.hits
.iter()
.all(|h| h.point.distance(hit.point) > merge_gap)
{
self.hits.push(hit);
}
}
sort_by_known_parameter(&mut self.hits, |hit| hit.first_parameter);
}
pub fn into_hits(self) -> Vec<CurveCurveHit> {
self.hits
}
pub fn completeness(&self) -> &Completeness<CurveCurveRegion> {
&self.completeness
}
pub fn is_complete(&self) -> bool {
self.completeness.is_complete()
}
}
impl Deref for CurveCurveHits {
type Target = [CurveCurveHit];
fn deref(&self) -> &[CurveCurveHit] {
&self.hits
}
}
impl IntoIterator for CurveCurveHits {
type Item = CurveCurveHit;
type IntoIter = std::vec::IntoIter<CurveCurveHit>;
fn into_iter(self) -> std::vec::IntoIter<CurveCurveHit> {
self.hits.into_iter()
}
}
impl<'a> IntoIterator for &'a CurveCurveHits {
type Item = &'a CurveCurveHit;
type IntoIter = std::slice::Iter<'a, CurveCurveHit>;
fn into_iter(self) -> std::slice::Iter<'a, CurveCurveHit> {
self.hits.iter()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CurveCurveExtrema {
minima: Vec<CurveCurveExtremum>,
completeness: Completeness<CurveCurveRegion>,
}
impl CurveCurveExtrema {
pub(super) fn new(
mut minima: Vec<CurveCurveExtremum>,
completeness: Completeness<CurveCurveRegion>,
) -> Self {
minima.sort_by(|x, y| x.distance.total_cmp(&y.distance));
CurveCurveExtrema {
minima,
completeness,
}
}
pub(super) fn sampled(minima: Vec<CurveCurveExtremum>) -> Self {
Self::new(minima, Completeness::Sampled)
}
pub fn minima(&self) -> &[CurveCurveExtremum] {
&self.minima
}
pub fn into_minima(self) -> Vec<CurveCurveExtremum> {
self.minima
}
pub fn completeness(&self) -> &Completeness<CurveCurveRegion> {
&self.completeness
}
pub fn is_complete(&self) -> bool {
self.completeness.is_complete()
}
}
impl Deref for CurveCurveExtrema {
type Target = [CurveCurveExtremum];
fn deref(&self) -> &[CurveCurveExtremum] {
&self.minima
}
}
impl IntoIterator for CurveCurveExtrema {
type Item = CurveCurveExtremum;
type IntoIter = std::vec::IntoIter<CurveCurveExtremum>;
fn into_iter(self) -> std::vec::IntoIter<CurveCurveExtremum> {
self.minima.into_iter()
}
}
impl<'a> IntoIterator for &'a CurveCurveExtrema {
type Item = &'a CurveCurveExtremum;
type IntoIter = std::slice::Iter<'a, CurveCurveExtremum>;
fn into_iter(self) -> std::slice::Iter<'a, CurveCurveExtremum> {
self.minima.iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurveCurveRegion {
pub first_range: (f64, f64),
pub second_range: (f64, f64),
pub min_corner: Point,
pub max_corner: Point,
}
impl Region for CurveCurveRegion {
fn touches(&self, other: &CurveCurveRegion) -> bool {
meet(self.first_range, other.first_range) && meet(self.second_range, other.second_range)
}
fn union(&self, other: &CurveCurveRegion) -> CurveCurveRegion {
let (min_corner, max_corner) = box_union(
(self.min_corner, self.max_corner),
(other.min_corner, other.max_corner),
);
CurveCurveRegion {
first_range: span(self.first_range, other.first_range),
second_range: span(self.second_range, other.second_range),
min_corner,
max_corner,
}
}
fn start(&self) -> f64 {
self.first_range.0
}
}
const SPLITS_PER_PAIR: usize = 256;
const MAX_DEPTH: u32 = 64;
const NEWTON_STEPS: usize = 32;
pub(super) fn bspline_curve_curve(
a: &BSplineCurve,
ra: Range,
b: &BSplineCurve,
rb: Range,
tol: Tolerance,
) -> (Vec<CurveCurveExtremum>, Completeness<CurveCurveRegion>) {
search_meetings(a, ra, b, rb, tol, SPLITS_PER_PAIR)
}
fn search_meetings(
a: &BSplineCurve,
ra: Range,
b: &BSplineCurve,
rb: Range,
tol: Tolerance,
splits_per_pair: usize,
) -> (Vec<CurveCurveExtremum>, Completeness<CurveCurveRegion>) {
let (pieces_a, pieces_b) = (
CurvePieces::new(a, (ra.lo, ra.hi)),
CurvePieces::new(b, (rb.lo, rb.hi)),
);
let mut pairs = Pairs::new(
[pieces_a.width, pieces_b.width],
pieces_a.len() + pieces_b.len(),
splits_per_pair,
);
let mut points = Vec::new();
let boxes_b: Vec<Aabb> = (0..pieces_b.len())
.map(|j| {
euclidean(pieces_b.piece(j), &mut points);
bounding_box(&points)
})
.collect();
let tree = BoxTree::new(&boxes_b, 1);
for i in 0..pieces_a.len() {
euclidean(pieces_a.piece(i), &mut points);
tree.overlapping(&bounding_box(&points), tol.confusion, |j| {
pairs.push(
[pieces_a.piece(i), pieces_b.piece(j)],
[pieces_a.ranges[i], pieces_b.ranges[j]],
);
});
}
let gap = tol.confusion;
let mut found = Vec::new();
let unsettled = pairs.search(|pairs, node| {
let [pa, pb] = &pairs.points;
if boxes_apart(&bounding_box(pa), &bounding_box(pb), gap) || separated(pa, pb, gap) {
return Visit::Settled;
}
if curves_meet_at_most_once(pa, pb)
&& let Some((hit, settles)) = meeting(a, ra, b, rb, pa, node.ranges, tol)
{
found.push(hit);
if settles {
return Visit::Settled;
}
}
match along_one_line(pa, pb, gap) {
Some(axis) if apart_along(axis, pa, pb, gap) => Visit::Settled,
Some(_) => Visit::Unseparated,
None => Visit::Split,
}
});
found.sort_by(|p, q| p.distance.total_cmp(&q.distance));
(found, unsettled.completeness())
}
fn meeting(
a: &BSplineCurve,
ra: Range,
b: &BSplineCurve,
rb: Range,
pa: &[[f64; 3]],
ranges: [(f64, f64); 2],
tol: Tolerance,
) -> Option<(CurveCurveExtremum, bool)> {
let [(s0, s1), (t0, t1)] = ranges;
let x = closest_pair(a, b, [0.5 * (s0 + s1), 0.5 * (t0 + t1)])?;
let (s, t) = (ra.admit(x[0])?, rb.admit(x[1])?);
let point = a.eval_point(s);
let distance = point.distance(b.eval_point(t));
if distance > tol.confusion {
return None;
}
let within = |x: f64, (lo, hi): (f64, f64), range: Range| {
lo - range.slack() <= x && x <= hi + range.slack()
};
let inside = within(x[0], (s0, s1), ra) && within(x[1], (t0, t1), rb);
let merge_gap = tol.confusion.sqrt();
let settles = inside
|| pa
.iter()
.all(|p| norm(sub(*p, [point.x, point.y, point.z])) <= merge_gap);
let hit = CurveCurveExtremum {
first_parameter: s,
second_parameter: t,
distance,
};
Some((hit, settles))
}
fn separated(a: &[[f64; 3]], b: &[[f64; 3]], gap: f64) -> bool {
let chord = |p: &[[f64; 3]]| sub(p[p.len() - 1], p[0]);
let (ca, cb) = (chord(a), chord(b));
let mut normal = cross(ca, cb);
if normal == [0.0; 3] {
normal = cross(ca, sub(b[0], a[0]));
}
[normal, cross(ca, normal), cross(cb, normal)]
.into_iter()
.any(|axis| apart_along(axis, a, b, gap))
}
fn along_one_line(a: &[[f64; 3]], b: &[[f64; 3]], gap: f64) -> Option<[f64; 3]> {
let chord = |p: &[[f64; 3]]| sub(p[p.len() - 1], p[0]);
let (ca, cb) = (chord(a), chord(b));
let (origin, axis) = if dot(ca, ca) >= dot(cb, cb) {
(a[0], ca)
} else {
(b[0], cb)
};
let length = norm(axis);
if !(length > 0.0 && length.is_finite()) {
return None;
}
let gap = gap * length;
a.iter()
.chain(b)
.all(|p| norm(cross(sub(*p, origin), axis)) <= gap)
.then_some(axis)
}
fn closest_pair(a: &BSplineCurve, b: &BSplineCurve, mut x: [f64; 2]) -> Option<[f64; 2]> {
let eval = |x: [f64; 2]| {
let (p, dp) = a.eval_d1(x[0]);
let (q, dq) = b.eval_d1(x[1]);
(p - q, dp, dq)
};
let (mut f, mut da, mut db) = eval(x);
for _ in 0..NEWTON_STEPS {
if f.square_magnitude() == 0.0 {
return Some(x);
}
let cross = -da.dot(db);
let normal = [[da.dot(da), cross], [cross, db.dot(db)]];
let step = solve_2x2(normal, [-da.dot(f), db.dot(f)], 1e-14)?;
let mut scale = 1.0;
loop {
let trial = [x[0] + scale * step[0], x[1] + scale * step[1]];
let (ft, dat, dbt) = eval(trial);
if ft.square_magnitude() < f.square_magnitude() {
(x, f, da, db) = (trial, ft, dat, dbt);
break;
}
scale *= 0.5;
if scale < 1e-3 {
return Some(x);
}
}
let moved = (scale * step[0]).abs() + (scale * step[1]).abs();
if moved <= 1e-14 * (2.0 + x[0].abs() + x[1].abs()) {
return Some(x);
}
}
None
}
pub(super) fn bspline_minima(
a: &BSplineCurve,
ra: Range,
b: &BSplineCurve,
rb: Range,
tol: Tolerance,
) -> (Vec<[f64; 2]>, Completeness<CurveCurveRegion>) {
search_minima(a, ra, b, rb, tol, SPLITS_PER_PAIR)
}
fn search_minima(
a: &BSplineCurve,
ra: Range,
b: &BSplineCurve,
rb: Range,
tol: Tolerance,
splits_per_pair: usize,
) -> (Vec<[f64; 2]>, Completeness<CurveCurveRegion>) {
let pieces = [
CurvePieces::new(a, (ra.lo, ra.hi)),
CurvePieces::new(b, (rb.lo, rb.hi)),
];
let ends = |r: Range| if r.periodic { vec![] } else { vec![r.lo, r.hi] };
let (ends_a, ends_b) = (ends(ra), ends(rb));
let mut seeds: Vec<[f64; 2]> = ends_a
.iter()
.flat_map(|&s| ends_b.iter().map(move |&t| [s, t]))
.collect();
let mut pairs = Pairs::new(
[pieces[0].width, pieces[1].width],
pieces[0].len() * pieces[1].len(),
splits_per_pair,
);
for i in 0..pieces[0].len() {
for j in 0..pieces[1].len() {
pairs.push(
[pieces[0].piece(i), pieces[1].piece(j)],
[pieces[0].ranges[i], pieces[1].ranges[j]],
);
}
}
let mut unsettled = pairs.search(|pairs, node| {
let [pa, pb] = &pairs.points;
if tangent_sign(pa, pb).is_some() || tangent_sign(pb, pa).is_some() {
return Visit::Settled;
}
let (va, da) = curve_bounds(pairs.net(0, node), pa);
let (vb, db) = curve_bounds(pairs.net(1, node), pb);
let [mut h11, mut h12, mut h22] = [Interval::ZERO; 3];
for c in 0..3 {
let r = va[c].sub(vb[c]);
h11 = h11.add(da[0][c].square()).add(r.mul(da[1][c]));
h22 = h22.add(db[0][c].square()).sub(r.mul(db[1][c]));
h12 = h12.add(da[0][c].mul(db[0][c]));
}
let off = h12.square();
if h11.hi < 0.0 || h22.hi < 0.0 || h11.mul(h22).hi < off.lo {
return Visit::Settled;
}
if !(h11.lo > 0.0 && h22.lo > 0.0 && h11.lo * h22.lo > off.hi) {
return match along_one_line(pa, pb, tol.confusion) {
Some(_) => Visit::Unseparated,
None => Visit::Split,
};
}
let (net_a, net_b) = (pairs.net(0, node), pairs.net(1, node));
let local = |u: f64, v: f64| {
let [pa, a1, a2] = bezier_d2(net_a, u);
let [pb, b1, b2] = bezier_d2(net_b, v);
let r = sub(pa, pb);
let cross = -dot(a1, b1);
(
(
0.5 * dot(r, r),
[dot(r, a1), -dot(r, b1)],
[
[dot(a1, a1) + dot(r, a2), cross],
[cross, dot(b1, b1) - dot(r, b2)],
],
),
[norm(a1), norm(b1)],
)
};
let unit = Some((0.0, 1.0));
let x = newton_min_2d(|u, v| local(u, v).0, [0.5; 2], [unit; 2], 1e-14, 100);
let ((_, g, _), speed) = local(x[0], x[1]);
let stationary =
(0..2).all(|i| (0.0 < x[i] && x[i] < 1.0) || g[i].abs() <= tol.confusion * speed[i]);
if stationary {
let [(s0, s1), (t0, t1)] = node.ranges;
seeds.push([s0 + x[0] * (s1 - s0), t0 + x[1] * (t1 - t0)]);
}
Visit::Settled
});
let end = |curve: &BSplineCurve, u: f64, r: Range| {
let (p, d) = curve.eval_d1(u);
let hold = if u == r.lo { d } else { -d };
([p.x, p.y, p.z], [hold.x, hold.y, hold.z])
};
for &s in &ends_a {
let (point, hold) = end(a, s, ra);
let (ts, left) = minima_from_point(point, hold, &pieces[1], tol, splits_per_pair);
seeds.extend(ts.into_iter().map(|t| [s, t]));
unsettled.absorb(left, |r| CurveCurveRegion {
first_range: (s, s),
..r
});
}
for &t in &ends_b {
let (point, hold) = end(b, t, rb);
let (ss, left) = minima_from_point(point, hold, &pieces[0], tol, splits_per_pair);
seeds.extend(ss.into_iter().map(|s| [s, t]));
unsettled.absorb(left, |r| CurveCurveRegion {
first_range: r.second_range,
second_range: (t, t),
..r
});
}
seeds.retain(|x| {
let ((pa, a1), (pb, b1)) = (a.eval_d1(x[0]), b.eval_d1(x[1]));
let r = pa - pb;
let (g, speed) = ([r.dot(a1), -r.dot(b1)], [a1.magnitude(), b1.magnitude()]);
[ra, rb].into_iter().enumerate().all(|(i, r)| {
let held =
!r.periodic && ((x[i] <= r.lo && g[i] > 0.0) || (x[i] >= r.hi && g[i] < 0.0));
held || g[i].abs() <= tol.confusion * speed[i]
})
});
(seeds, unsettled.completeness())
}
fn minima_from_point(
point: [f64; 3],
hold: [f64; 3],
pieces: &CurvePieces,
tol: Tolerance,
splits_per_pair: usize,
) -> (Vec<f64>, Unsettled) {
let reach = dot(point, hold) + tol.confusion * norm(hold);
let mut pairs = Pairs::new([1, pieces.width], pieces.len(), splits_per_pair);
let fixed = [point[0], point[1], point[2], 1.0];
for j in 0..pieces.len() {
pairs.push([&fixed, pieces.piece(j)], [(0.0, 0.0), pieces.ranges[j]]);
}
let mut found = Vec::new();
let unsettled = pairs.search(|pairs, node| {
let [fixed, pc] = &pairs.points;
if extent(pc, hold).0 > reach || tangent_sign(pc, fixed).is_some() {
return Visit::Settled;
}
let (value, d) = curve_bounds(pairs.net(1, node), pc);
let mut second = Interval::ZERO;
for c in 0..3 {
let at = Interval {
lo: point[c],
hi: point[c],
};
second = second
.add(d[0][c].square())
.add(value[c].sub(at).mul(d[1][c]));
}
if second.hi < 0.0 {
return Visit::Settled;
}
if second.lo <= 0.0 {
return Visit::Split;
}
let net = pairs.net(1, node);
let local = |u: f64| {
let [c, d1, d2] = bezier_d2(net, u);
let r = sub(c, point);
(
(0.5 * dot(r, r), dot(r, d1), dot(d1, d1) + dot(r, d2)),
norm(d1),
)
};
let u = newton_min_1d(|u| local(u).0, 0.5, Some((0.0, 1.0)), 1e-14, 100);
let ((_, g, _), speed) = local(u);
if (0.0 < u && u < 1.0) || g.abs() <= tol.confusion * speed {
let (lo, hi) = node.ranges[1];
found.push(lo + u * (hi - lo));
}
Visit::Settled
});
(found, unsettled)
}
fn bezier_d2(net: &[f64], u: f64) -> [[f64; 3]; 3] {
let n = net.len() / 4;
let p = (n - 1) as f64;
let mut q = [[0.0; 4]; LARGE];
for (k, x) in net.as_chunks::<4>().0.iter().enumerate() {
q[k] = *x;
}
let lerp = |a: [f64; 4], b: [f64; 4]| std::array::from_fn(|c| a[c] + u * (b[c] - a[c]));
let kept = n.min(3);
for r in 0..n - kept {
for i in 0..n - 1 - r {
q[i] = lerp(q[i], q[i + 1]);
}
}
let (h, h1, h2): ([f64; 4], [f64; 4], [f64; 4]) = if kept == 3 {
let (r0, r1) = (lerp(q[0], q[1]), lerp(q[1], q[2]));
(
lerp(r0, r1),
std::array::from_fn(|c| p * (r1[c] - r0[c])),
std::array::from_fn(|c| p * (p - 1.0) * (q[2][c] - 2.0 * q[1][c] + q[0][c])),
)
} else {
(
lerp(q[0], q[1]),
std::array::from_fn(|c| q[1][c] - q[0][c]),
[0.0; 4],
)
};
let w = h[3];
let point: [f64; 3] = std::array::from_fn(|c| h[c] / w);
let first: [f64; 3] = std::array::from_fn(|c| (h1[c] - h1[3] * point[c]) / w);
let second = std::array::from_fn(|c| (h2[c] - 2.0 * h1[3] * first[c] - h2[3] * point[c]) / w);
[point, first, second]
}
fn curve_bounds(net: &[f64], points: &[[f64; 3]]) -> ([Interval; 3], [[Interval; 3]; 2]) {
if net.as_chunks::<4>().0.iter().any(|x| x[3] != 1.0) {
let (value, d) = derivative_bounds(net, points, points.len(), 1, 4, true);
return (value, [d[0], d[2]]);
}
let n = points.len();
let p = (n - 1) as f64;
let empty = Interval {
lo: f64::INFINITY,
hi: f64::NEG_INFINITY,
};
let mut value = [empty; 3];
let mut first = [empty; 3];
let mut second = [empty; 3];
let widen = |i: &mut Interval, x: f64| {
i.lo = i.lo.min(x);
i.hi = i.hi.max(x);
};
for k in 0..n {
for c in 0..3 {
widen(&mut value[c], points[k][c]);
if k + 1 < n {
widen(&mut first[c], p * (points[k + 1][c] - points[k][c]));
}
if k + 2 < n {
let bend = points[k + 2][c] - 2.0 * points[k + 1][c] + points[k][c];
widen(&mut second[c], p * (p - 1.0) * bend);
}
}
}
let settle = |i: Interval| if i.lo > i.hi { Interval::ZERO } else { i };
(value, [first.map(settle), second.map(settle)])
}
struct Node {
slots: [usize; 2],
ranges: [(f64, f64); 2],
depth: u32,
}
enum Visit {
Settled,
Split,
Unseparated,
}
struct Unsettled {
regions: Vec<CurveCurveRegion>,
exhausted: bool,
}
impl Unsettled {
fn absorb(&mut self, other: Unsettled, placed: impl Fn(CurveCurveRegion) -> CurveCurveRegion) {
self.regions.extend(other.regions.into_iter().map(placed));
self.exhausted |= other.exhausted;
}
fn completeness(self) -> Completeness<CurveCurveRegion> {
of_unsettled(self.regions, self.exhausted)
}
}
struct Pairs {
shapes: [(usize, usize, usize); 2],
pools: [Pool; 2],
pending: Vec<Node>,
points: [Vec<[f64; 3]>; 2],
unseparated: Vec<CurveCurveRegion>,
splits_per_pair: usize,
}
impl Pairs {
fn new(widths: [usize; 2], count: usize, splits_per_pair: usize) -> Self {
let room = 4 * count + 16;
Pairs {
shapes: widths.map(|w| (w, 1, 4)),
pools: widths.map(|w| {
let mut pool = Pool::new(4 * w);
pool.reserve(room);
pool
}),
pending: Vec::with_capacity(room),
points: widths.map(Vec::with_capacity),
unseparated: Vec::new(),
splits_per_pair,
}
}
fn push(&mut self, nets: [&[f64]; 2], ranges: [(f64, f64); 2]) {
let slots = [0, 1].map(|k| {
let slot = self.pools[k].take();
self.pools[k].get_mut(slot).copy_from_slice(nets[k]);
slot
});
self.pending.push(Node {
slots,
ranges,
depth: 0,
});
}
fn net(&self, k: usize, node: &Node) -> &[f64] {
self.pools[k].get(node.slots[k])
}
fn search(mut self, mut visit: impl FnMut(&Self, &Node) -> Visit) -> Unsettled {
let mut budget = self.splits_per_pair * self.pending.len();
let mut exhausted = false;
while let Some(node) = self.pending.pop() {
self.load(&node);
match visit(&self, &node) {
Visit::Settled => {}
Visit::Split if node.depth < MAX_DEPTH => {
if budget == 0 {
self.pending.push(node);
exhausted = true;
break;
}
budget -= 1;
let sizes = self.points.each_ref().map(|p| box_size(&bounding_box(p)));
self.split(node, usize::from(sizes[1] >= sizes[0]));
continue;
}
Visit::Split | Visit::Unseparated => {
let region = self.region(&node);
self.unseparated.push(region);
}
}
for k in 0..2 {
self.pools[k].release(node.slots[k]);
}
}
let mut regions = std::mem::take(&mut self.unseparated);
for node in std::mem::take(&mut self.pending) {
self.load(&node);
regions.push(self.region(&node));
}
Unsettled { regions, exhausted }
}
fn load(&mut self, node: &Node) {
for k in 0..2 {
euclidean(self.pools[k].get(node.slots[k]), &mut self.points[k]);
}
}
fn region(&self, node: &Node) -> CurveCurveRegion {
let corners = |p: &[[f64; 3]]| {
let b = bounding_box(p);
(Point::new(b[0], b[1], b[2]), Point::new(b[3], b[4], b[5]))
};
let (min_corner, max_corner) =
box_union(corners(&self.points[0]), corners(&self.points[1]));
CurveCurveRegion {
first_range: node.ranges[0],
second_range: node.ranges[1],
min_corner,
max_corner,
}
}
fn split(&mut self, node: Node, k: usize) {
let other = 1 - k;
let upper = self.pools[k].halve(node.slots[k], self.shapes[k], true);
let copy = self.pools[other].take();
self.pools[other].copy(node.slots[other], copy);
let (lo, hi) = node.ranges[k];
let mid = 0.5 * (lo + hi);
let depth = node.depth + 1;
let mut slots = node.slots;
let mut ranges = node.ranges;
slots[k] = upper;
slots[other] = copy;
ranges[k] = (mid, hi);
self.pending.push(Node {
slots,
ranges,
depth,
});
ranges[k] = (lo, mid);
self.pending.push(Node {
ranges,
depth,
..node
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Point;
use crate::curves::{Curve, ParametricCurve, TrimmedCurve};
use crate::intersect::curve_curve::{
ParamDomain, curve_curve_minima, hits_at_minima, refine_minima, sampled_curve_curve,
};
use crate::intersect::{IncompleteReason, curve_curve_extrema, search_curve_curve};
const TOL: Tolerance = Tolerance::DEFAULT;
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> f64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
fn wave(&mut self) -> f64 {
2.0 * self.next() - 1.0
}
}
fn cubic(
rng: &mut Lcg,
from: impl Fn(f64) -> [f64; 3],
amplitude: [f64; 3],
rational: bool,
) -> BSplineCurve {
let poles = (0..6)
.map(|k| {
let [x, y, z] = from(k as f64 / 5.0);
Point::new(
x + amplitude[0] * rng.wave(),
y + amplitude[1] * rng.wave(),
z + amplitude[2] * rng.wave(),
)
})
.collect();
let (knots, mults) = (vec![0.0, 1.0, 2.0, 3.0], vec![4, 1, 1, 4]);
if rational {
let weights = (0..6).map(|_| 0.4 + 1.6 * rng.next()).collect();
BSplineCurve::new_rational(3, poles, weights, knots, mults, false).unwrap()
} else {
BSplineCurve::new(3, poles, knots, mults, false).unwrap()
}
}
fn crossing(rng: &mut Lcg, rational: bool, wave: f64) -> (BSplineCurve, BSplineCurve) {
let tilt = |[x, y, _]: [f64; 3]| [x, y, 0.5 * x];
let (y0, y1, x0, x1) = (
5.0 * rng.next(),
5.0 * rng.next(),
5.0 * rng.next(),
5.0 * rng.next(),
);
let across = cubic(
rng,
|s| [-0.5 + 6.0 * s, y0 + (y1 - y0) * s, 0.0],
[0.0, wave, 0.0],
rational,
);
let up = cubic(
rng,
|s| [x0 + (x1 - x0) * s, -0.5 + 6.0 * s, 0.0],
[wave, 0.0, 0.0],
rational,
);
let tilted = |c: &BSplineCurve| {
let poles = c.poles().iter().map(|p| {
let [x, y, z] = tilt([p.x, p.y, p.z]);
Point::new(x, y, z)
});
let (knots, mults) = (c.knots().to_vec(), c.multiplicities().to_vec());
match c.weights() {
Some(w) => {
BSplineCurve::new_rational(3, poles.collect(), w.to_vec(), knots, mults, false)
}
None => BSplineCurve::new(3, poles.collect(), knots, mults, false),
}
.unwrap()
};
(tilted(&across), tilted(&up))
}
fn skew(rng: &mut Lcg, rational: bool) -> (BSplineCurve, BSplineCurve) {
let (y0, y1, x0, x1) = (
5.0 * rng.next(),
5.0 * rng.next(),
5.0 * rng.next(),
5.0 * rng.next(),
);
let below = cubic(
rng,
|s| [6.0 * s, y0 + (y1 - y0) * s, 0.0],
[0.5, 1.0, 0.5],
rational,
);
let above = cubic(
rng,
|s| [x0 + (x1 - x0) * s, 6.0 * s, 2.0],
[1.0, 0.5, 0.5],
rational,
);
(below, above)
}
fn search<A: ParametricCurve, B: ParametricCurve>(a: &A, b: &B) -> Vec<CurveCurveHit> {
let (ra, rb) = (ParamDomain::of(a).range(), ParamDomain::of(b).range());
let (sa, sb) = (a.as_bspline().unwrap(), b.as_bspline().unwrap());
let (meetings, completeness) = bspline_curve_curve(sa, ra, sb, rb, TOL);
assert!(completeness.is_complete(), "{completeness:?}");
hits_at_minima(a, b, meetings, TOL)
}
fn minima<A: ParametricCurve, B: ParametricCurve>(a: &A, b: &B) -> Vec<CurveCurveExtremum> {
let (da, db) = (ParamDomain::of(a), ParamDomain::of(b));
let (sa, sb) = (a.as_bspline().unwrap(), b.as_bspline().unwrap());
let (seeds, completeness) = bspline_minima(sa, da.range(), sb, db.range(), TOL);
assert!(completeness.is_complete(), "{completeness:?}");
let mut out = refine_minima(a, da, b, db, seeds, TOL);
out.sort_by(|x, y| x.distance.total_cmp(&y.distance));
out
}
#[track_caller]
fn assert_hits_match<A: ParametricCurve, B: ParametricCurve>(
a: &A,
b: &B,
) -> Vec<CurveCurveHit> {
let found = search(a, b);
let ((a0, a1), (b0, b1)) = (a.bounds(), b.bounds());
for hit in &found {
assert!((a0..=a1).contains(&hit.first_parameter.unwrap()), "{hit:?}");
assert!(
(b0..=b1).contains(&hit.second_parameter.unwrap()),
"{hit:?}"
);
assert!(
hit.point
.distance(a.eval_point(hit.first_parameter.unwrap()))
< 1e-12
);
assert!(
hit.point
.distance(b.eval_point(hit.second_parameter.unwrap()))
<= TOL.confusion
);
}
for pair in found.windows(2) {
assert!(pair[0].first_parameter < pair[1].first_parameter);
}
let sampled = sampled_curve_curve(a, b, TOL);
for want in &sampled {
assert!(
found.iter().any(|hit| hit.point.distance(want.point) < 1e-9
&& (hit.first_parameter.unwrap() - want.first_parameter.unwrap()).abs() < 1e-9
&& (hit.second_parameter.unwrap() - want.second_parameter.unwrap()).abs()
< 1e-9
&& hit.multiplicity == want.multiplicity),
"{want:?} missing from {found:#?}"
);
}
let hits = search_curve_curve(a, b, TOL);
assert!(hits.is_complete(), "{:?}", hits.completeness());
assert_eq!(hits.into_hits(), found);
found
}
#[track_caller]
fn assert_minima_match<A: ParametricCurve, B: ParametricCurve>(
a: &A,
b: &B,
) -> Vec<CurveCurveExtremum> {
let found = minima(a, b);
let grid = curve_curve_minima(a, b, TOL, false);
for want in &grid {
assert!(
found
.iter()
.any(|e| (e.first_parameter - want.first_parameter).abs() < 1e-7
&& (e.second_parameter - want.second_parameter).abs() < 1e-7
&& (e.distance - want.distance).abs() < 1e-12),
"{want:?} missing from {found:#?}"
);
}
let (da, db) = (ParamDomain::of(a), ParamDomain::of(b));
let ((a0, a1), (b0, b1)) = (a.bounds(), b.bounds());
for e in &found {
for (ds, dt) in [(1.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, -1.0)] {
for h in [-1e-4, 1e-4] {
let (s, t) = (e.first_parameter + h * ds, e.second_parameter + h * dt);
let s = if da.is_periodic() { s } else { s.clamp(a0, a1) };
let t = if db.is_periodic() { t } else { t.clamp(b0, b1) };
let d = a.eval_point(s).distance(b.eval_point(t));
assert!(
d >= e.distance - 1e-12,
"{e:?} is no minimum: {d} at ({s}, {t})"
);
}
}
}
let extrema = curve_curve_extrema(a, b, TOL);
assert!(extrema.is_complete(), "{:?}", extrema.completeness());
assert_eq!(extrema.into_minima(), found);
found
}
#[test]
fn test_crossing_polynomial_curves_match_sampled() {
let mut rng = Lcg(42);
let mut count = 0;
for _ in 0..32 {
let (a, b) = crossing(&mut rng, false, 1.0);
count += assert_hits_match(&a, &b).len();
assert_minima_match(&a, &b);
}
assert!(count >= 32, "{count}");
}
#[test]
fn test_crossing_rational_curves_match_sampled() {
let mut rng = Lcg(7);
let mut count = 0;
for _ in 0..24 {
let (a, b) = crossing(&mut rng, true, 1.0);
count += assert_hits_match(&a, &b).len();
assert_minima_match(&a, &b);
}
assert!(count >= 24, "{count}");
}
#[test]
fn test_skew_curves_minima_match_grid() {
let mut rng = Lcg(3);
for k in 0..48 {
let (a, b) = skew(&mut rng, k % 3 == 2);
assert!(assert_hits_match(&a, &b).is_empty());
assert!(!assert_minima_match(&a, &b).is_empty());
}
}
#[test]
fn test_curves_crossing_many_times_match_sampled() {
let mut rng = Lcg(11);
let mut most = 0;
for k in 0..24 {
let (a, b) = crossing(&mut rng, k % 2 == 1, 3.0);
most = most.max(assert_hits_match(&a, &b).len());
assert_minima_match(&a, &b);
}
assert!(most >= 3, "{most}");
}
fn ring(centre: [f64; 3], radius: f64, lift: f64) -> BSplineCurve {
let poles = (0..8)
.map(|k| {
let angle = k as f64 * std::f64::consts::TAU / 8.0;
let z = if k % 2 == 0 { lift } else { -lift };
Point::new(
centre[0] + radius * angle.cos(),
centre[1] + radius * angle.sin(),
centre[2] + z,
)
})
.collect();
let knots = (0..=8).map(f64::from).collect();
BSplineCurve::new(3, poles, knots, vec![1; 9], true).unwrap()
}
#[test]
fn test_periodic_curves_match_sampled() {
let loop_a = ring([2.5, 2.5, 0.0], 2.0, 0.0);
let loop_b = ring([3.5, 2.0, 0.0], 1.5, 0.0);
let hits = assert_hits_match(&loop_a, &loop_b);
assert_eq!(hits.len(), 2);
assert!(
hits.iter()
.all(|h| (0.0..8.0).contains(&h.first_parameter.unwrap()))
);
assert!(
hits.iter()
.all(|h| (0.0..8.0).contains(&h.second_parameter.unwrap()))
);
assert_minima_match(&loop_a, &loop_b);
let mut rng = Lcg(5);
for _ in 0..8 {
let (across, _) = crossing(&mut rng, false, 1.0);
let flat = |c: &BSplineCurve| {
let poles = c
.poles()
.iter()
.map(|p| Point::new(p.x, p.y, 0.0))
.collect();
BSplineCurve::new(
3,
poles,
c.knots().to_vec(),
c.multiplicities().to_vec(),
false,
)
.unwrap()
};
let across = flat(&across);
assert_hits_match(&loop_a, &across);
assert_hits_match(&across, &loop_a);
assert_minima_match(&across, &loop_a);
}
let wobbly = ring([2.5, 2.5, 0.0], 2.0, 0.5);
let (_, above) = skew(&mut rng, false);
assert!(assert_hits_match(&wobbly, &above).is_empty());
assert!(!assert_minima_match(&wobbly, &above).is_empty());
assert_minima_match(&above, &wobbly);
}
#[test]
fn test_trimmed_curves_search_only_their_range() {
let ring = ring([2.5, 2.5, 0.0], 2.0, 0.0);
let wavy = BSplineCurve::new(
3,
(0..8)
.map(|k| {
Point::new(
-0.5 + 0.85 * k as f64,
if k % 2 == 0 { -4.0 } else { 9.0 },
0.0,
)
})
.collect(),
(0..=5).map(f64::from).collect(),
vec![4, 1, 1, 1, 1, 4],
false,
)
.unwrap();
let all = assert_hits_match(&ring, &wavy);
assert!(all.len() >= 6, "{all:#?}");
let wrapped = |t: f64| if t < 6.5 { t + 8.0 } else { t };
let expected: Vec<f64> = all
.iter()
.map(|h| wrapped(h.first_parameter.unwrap()))
.filter(|&t| t <= 9.5)
.collect();
assert!(expected.iter().any(|&t| t < 8.0) && expected.iter().any(|&t| t > 8.0));
let borrowed = TrimmedCurve::new(&ring, 6.5, 9.5).unwrap();
let owned = TrimmedCurve::new(ring.clone(), 6.5, 9.5).unwrap();
for hits in [
assert_hits_match(&borrowed, &wavy),
assert_hits_match(&owned, &wavy),
] {
assert_eq!(hits.len(), expected.len());
for hit in &hits {
assert!(
expected
.iter()
.any(|&t| (t - hit.first_parameter.unwrap()).abs() < 1e-9)
);
}
}
let (first, last) = (
all[1]
.second_parameter
.unwrap()
.min(all[2].second_parameter.unwrap()),
4.2,
);
let middle = TrimmedCurve::new(&wavy, first, last).unwrap();
let hits = assert_hits_match(&ring, &middle);
let inside = all
.iter()
.filter(|h| (first - 1e-9..=last + 1e-9).contains(&h.second_parameter.unwrap()))
.count();
assert_eq!(hits.len(), inside);
assert_minima_match(&borrowed, &wavy);
assert_minima_match(&owned, &middle);
let mut rng = Lcg(9);
let (below, above) = skew(&mut rng, true);
let piece = TrimmedCurve::new(&below, 0.4, 2.3).unwrap();
assert_minima_match(&piece, &TrimmedCurve::new(above, 1.2, 2.9).unwrap());
}
fn quadratic(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> BSplineCurve {
let poles = [p0, p1, p2].map(|[x, y, z]| Point::new(x, y, z)).to_vec();
BSplineCurve::new(2, poles, vec![0.0, 1.0], vec![3, 3], false).unwrap()
}
fn segment(from: [f64; 3], to: [f64; 3]) -> BSplineCurve {
let poles = [from, to].map(|[x, y, z]| Point::new(x, y, z)).to_vec();
BSplineCurve::new(1, poles, vec![0.0, 1.0], vec![2, 2], false).unwrap()
}
fn assert_search_hits(a: &BSplineCurve, b: &BSplineCurve) -> Vec<CurveCurveHit> {
let hits = search(a, b);
for hit in &hits {
assert!((a.bounds().0..=a.bounds().1).contains(&hit.first_parameter.unwrap()));
assert!((b.bounds().0..=b.bounds().1).contains(&hit.second_parameter.unwrap()));
assert!(
hit.point
.distance(b.eval_point(hit.second_parameter.unwrap()))
<= TOL.confusion
);
}
assert_eq!(search_curve_curve(a, b, TOL).into_hits(), hits);
hits
}
#[test]
fn test_close_hits_near_a_tangency_are_separated() {
let line = segment([0.0, 0.0, 0.0], [4.0, 0.0, 0.0]);
for depth in [1e-3, 1e-6] {
let dip = quadratic(
[0.0, 1.0 - depth, 0.0],
[2.0, -1.0 - depth, 0.0],
[4.0, 1.0 - depth, 0.0],
);
let hits = assert_search_hits(&dip, &line);
assert_eq!(hits.len(), 2, "{hits:#?}");
let half = (4.0 * depth).sqrt();
assert!((hits[0].point.x - (2.0 - half)).abs() < 1e-9, "{hits:#?}");
assert!((hits[1].point.x - (2.0 + half)).abs() < 1e-9, "{hits:#?}");
assert!(hits.iter().all(|h| h.multiplicity == 1));
}
}
#[track_caller]
fn incomplete(
completeness: &Completeness<CurveCurveRegion>,
) -> (IncompleteReason, &[CurveCurveRegion]) {
match completeness {
Completeness::Incomplete { reason, regions } => (*reason, regions),
other => panic!("expected an incomplete search, got {other:?}"),
}
}
fn holds<A: ParametricCurve, B: ParametricCurve>(
region: &CurveCurveRegion,
(a, s): (&A, f64),
(b, t): (&B, f64),
) -> bool {
let within = |x: f64, (lo, hi): (f64, f64)| lo - 1e-9 <= x && x <= hi + 1e-9;
let in_box = |p: Point| {
let (lo, hi) = (region.min_corner, region.max_corner);
within(p.x, (lo.x, hi.x)) && within(p.y, (lo.y, hi.y)) && within(p.z, (lo.z, hi.z))
};
within(s, region.first_range)
&& within(t, region.second_range)
&& in_box(a.eval_point(s))
&& in_box(b.eval_point(t))
}
#[track_caller]
fn assert_sampled_hits_kept<A: ParametricCurve, B: ParametricCurve>(
hits: &CurveCurveHits,
a: &A,
b: &B,
) {
let ((a0, a1), (b0, b1)) = (a.bounds(), b.bounds());
for hit in hits {
assert!((a0..=a1).contains(&hit.first_parameter.unwrap()), "{hit:?}");
assert!(
(b0..=b1).contains(&hit.second_parameter.unwrap()),
"{hit:?}"
);
assert!(
hit.point
.distance(b.eval_point(hit.second_parameter.unwrap()))
<= TOL.confusion
);
}
for pair in hits.windows(2) {
assert!(pair[0].first_parameter <= pair[1].first_parameter);
}
for want in sampled_curve_curve(a, b, TOL) {
let near = |h: &CurveCurveHit| h.point.distance(want.point) <= TOL.confusion.sqrt();
assert!(hits.iter().any(near), "{want:?} missing from {hits:?}");
}
}
#[test]
fn test_tangent_contact_is_unseparated_around_it() {
let line = segment([0.0, 0.0, 0.0], [4.0, 0.0, 0.0]);
let touching = quadratic([0.0, 1.0, 0.0], [2.0, -1.0, 0.0], [4.0, 1.0, 0.0]);
let contact = Point::new(2.0, 0.0, 0.0);
assert_eq!(touching.eval_point(0.5), contact);
for (a, b, at) in [
(&touching, &line, (0.5, 0.5)),
(&line, &touching, (0.5, 0.5)),
] {
let hits = search_curve_curve(a, b, TOL);
let (reason, regions) = incomplete(hits.completeness());
assert_eq!(reason, IncompleteReason::Unseparated);
assert_eq!(regions.len(), 1, "{regions:#?}");
assert!(holds(®ions[0], (a, at.0), (b, at.1)), "{regions:#?}");
let (s0, s1) = regions[0].first_range;
let (t0, t1) = regions[0].second_range;
assert!(s1 - s0 < 1e-2 && t1 - t0 < 1e-2, "{regions:#?}");
assert_eq!(hits.len(), 1, "{hits:#?}");
assert!(hits[0].point.distance(contact) < TOL.confusion.sqrt());
assert_sampled_hits_kept(&hits, a, b);
let extrema = curve_curve_extrema(a, b, TOL);
let (_, regions) = incomplete(extrema.completeness());
assert!(regions.iter().any(|r| holds(r, (a, at.0), (b, at.1))));
assert!(extrema[0].distance < TOL.confusion, "{extrema:#?}");
}
}
#[test]
fn test_straight_overlap_is_unseparated_along_it() {
let along = |x: f64| [x, 0.5 * x, -0.25 * x];
let poles = (0..5)
.map(|k| {
let [x, y, z] = along(k as f64);
Point::new(x, y, z)
})
.collect();
let knots = (0..=4).map(f64::from).collect();
let polyline = BSplineCurve::new(1, poles, knots, vec![2, 1, 1, 1, 2], false).unwrap();
let part = segment(along(1.5), along(3.5));
for swapped in [false, true] {
let hits = if swapped {
search_curve_curve(&part, &polyline, TOL)
} else {
search_curve_curve(&polyline, &part, TOL)
};
let (reason, regions) = incomplete(hits.completeness());
assert_eq!(reason, IncompleteReason::Unseparated);
assert_eq!(regions.len(), 1, "{regions:#?}");
for k in 0..=16 {
let x = 1.5 + 2.0 * k as f64 / 16.0;
let (s, t) = (x, (x - 1.5) / 2.0);
let held = if swapped {
holds(®ions[0], (&part, t), (&polyline, s))
} else {
holds(®ions[0], (&polyline, s), (&part, t))
};
assert!(held, "{x}: {regions:#?}");
}
assert!(!hits.is_empty());
if swapped {
assert_sampled_hits_kept(&hits, &part, &polyline);
} else {
assert_sampled_hits_kept(&hits, &polyline, &part);
}
}
}
#[test]
fn test_curved_overlap_is_incomplete_along_it() {
let mut rng = Lcg(1);
let (curve, _) = crossing(&mut rng, false, 1.0);
let part = TrimmedCurve::new(&curve, 0.5, 2.0).unwrap();
let hits = search_curve_curve(&curve, &part, TOL);
let (_, regions) = incomplete(hits.completeness());
for k in 0..=16 {
let s = 0.5 + 1.5 * k as f64 / 16.0;
assert!(
regions.iter().any(|r| holds(r, (&curve, s), (&part, s))),
"{s}: {regions:#?}"
);
}
assert!(!hits.is_empty());
assert_sampled_hits_kept(&hits, &curve, &part);
let extrema = curve_curve_extrema(&curve, &part, TOL);
let (_, regions) = incomplete(extrema.completeness());
for k in 0..=16 {
let s = 0.5 + 1.5 * k as f64 / 16.0;
assert!(regions.iter().any(|r| holds(r, (&curve, s), (&part, s))));
}
assert!(extrema[0].distance < TOL.confusion, "{extrema:#?}");
}
#[track_caller]
fn exhausted(completeness: &Completeness<CurveCurveRegion>) -> &[CurveCurveRegion] {
if completeness.is_complete() {
return &[];
}
let (reason, regions) = incomplete(completeness);
assert_eq!(reason, IncompleteReason::BudgetExhausted);
regions
}
#[test]
fn test_exhausted_budget_leaves_the_pending_pairs() {
let mut rng = Lcg(17);
let mut count = 0;
for k in 0..8 {
let (a, b) = crossing(&mut rng, k % 2 == 1, 3.0);
let (da, db) = (ParamDomain::of(&a), ParamDomain::of(&b));
let (ra, rb) = (da.range(), db.range());
let all = search(&a, &b);
let (meetings, completeness) = search_meetings(&a, ra, &b, rb, TOL, 0);
let found = hits_at_minima(&a, &b, meetings, TOL);
let regions = exhausted(&completeness);
count += usize::from(!regions.is_empty());
for hit in &all {
let held = regions.iter().any(|r| {
holds(
r,
(&a, hit.first_parameter.unwrap()),
(&b, hit.second_parameter.unwrap()),
)
});
let near = |h: &CurveCurveHit| h.point.distance(hit.point) < 1e-9;
assert!(held || found.iter().any(near), "{hit:?}: {regions:#?}");
}
for pair in regions.windows(2) {
assert!(!pair[0].touches(&pair[1]));
}
let all = minima(&a, &b);
let (seeds, completeness) = search_minima(&a, ra, &b, rb, TOL, 0);
let found = refine_minima(&a, da, &b, db, seeds, TOL);
let regions = exhausted(&completeness);
count += usize::from(!regions.is_empty());
for e in &all {
let (s, t) = (e.first_parameter, e.second_parameter);
let held = regions.iter().any(|r| holds(r, (&a, s), (&b, t)));
let near = |f: &CurveCurveExtremum| {
(f.first_parameter - s).abs() < 1e-7 && (f.second_parameter - t).abs() < 1e-7
};
assert!(held || found.iter().any(near), "{e:?}: {regions:#?}");
}
}
assert!(count >= 12, "{count}");
}
#[test]
fn test_crossing_and_skew_bsplines_are_complete() {
let mut rng = Lcg(23);
for k in 0..8 {
let (a, b) = crossing(&mut rng, k % 2 == 1, 1.0);
let hits = search_curve_curve(&a, &b, TOL);
assert!(hits.is_complete(), "{:?}", hits.completeness());
assert!(!hits.is_empty());
let (below, above) = skew(&mut rng, k % 2 == 0);
let extrema = curve_curve_extrema(&below, &above, TOL);
assert_eq!(extrema.completeness(), &Completeness::Complete);
assert!(!extrema.is_empty());
assert!(search_curve_curve(&below, &above, TOL).is_complete());
}
}
#[test]
fn test_elementary_pairs_are_sampled_and_mixed_pairs_are_not() {
let circle = crate::Circle::new(Point::ORIGIN, crate::Vector::Z, 2.0).unwrap();
let line = crate::Line::new(Point::ORIGIN, crate::Vector::X).unwrap();
let hits = search_curve_curve(&line, &circle, TOL);
assert_eq!(hits.completeness(), &Completeness::Sampled);
assert_eq!(hits.len(), 2);
assert_eq!(
curve_curve_extrema(&line, &circle, TOL).completeness(),
&Completeness::Sampled
);
let chord = segment([-3.0, 0.5, 0.0], [3.0, 0.5, 0.0]);
let hits = search_curve_curve(&chord, &circle, TOL);
assert_eq!(hits.completeness(), &Completeness::Complete);
assert_eq!(hits.len(), 2);
let extrema = curve_curve_extrema(&circle, &chord, TOL);
assert_eq!(extrema.completeness(), &Completeness::Sampled);
assert!(!extrema.is_empty());
let line = crate::Line::new(Point::new(0.0, 0.5, 1.0), crate::Vector::X).unwrap();
let extrema = curve_curve_extrema(&line, &chord, TOL);
assert_eq!(extrema.completeness(), &Completeness::Sampled);
let bend = quadratic([-1.0, 0.0, 0.0], [0.0, 2.0, 1.0], [1.0, 0.0, 0.0]);
let extrema = curve_curve_extrema(&line, &bend, TOL);
assert_eq!(extrema.completeness(), &Completeness::Sampled);
let piece = crate::curves::TrimmedCurve::new(line, -2.0, 2.0).unwrap();
let extrema = curve_curve_extrema(&piece, &bend, TOL);
assert_eq!(extrema.completeness(), &Completeness::Complete);
}
#[test]
fn test_hits_at_curve_ends_and_knots() {
let bend = quadratic([0.0, 0.0, 0.0], [1.0, 2.0, 0.5], [2.0, 0.0, 1.0]);
let end = bend.eval_point(1.0);
let middle = bend.eval_point(0.3);
let cases = [
(bend.eval_point(1.0), [3.0, 1.0, 0.0], end),
(Point::new(-1.0, 1.0, 0.5), [0.0, 0.0, 0.0], Point::ORIGIN),
(
Point::new(middle.x, middle.y - 1.0, middle.z),
[middle.x, middle.y + 1.0, middle.z],
middle,
),
]
.map(|(from, to, at): (Point, [f64; 3], Point)| {
(segment([from.x, from.y, from.z], to), at)
});
for (line, at) in &cases {
for hits in [
assert_search_hits(&bend, line),
assert_search_hits(line, &bend),
] {
assert_eq!(hits.len(), 1, "{hits:#?}");
assert!(hits[0].point.distance(*at) < 1e-12, "{hits:#?}");
}
}
let knotted = |p: [[f64; 3]; 3]| {
let poles = p.map(|[x, y, z]| Point::new(x, y, z)).to_vec();
BSplineCurve::new(1, poles, vec![0.0, 1.0, 2.0], vec![2, 1, 2], false).unwrap()
};
let a = knotted([[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 1.5, 0.0]]);
let b = knotted([[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.5, 3.0, 0.0]]);
let hits = assert_search_hits(&a, &b);
assert_eq!(hits.len(), 1, "{hits:#?}");
assert_eq!(
(hits[0].first_parameter, hits[0].second_parameter),
(Some(1.0), Some(1.0))
);
}
#[test]
fn test_enums_and_borrows_take_the_search() {
let mut rng = Lcg(21);
let (a, b) = crossing(&mut rng, true, 2.0);
let (ea, eb) = (Curve::BSpline(a.clone()), Curve::BSpline(b.clone()));
let hits = assert_hits_match(&ea, &eb);
assert_eq!(hits, search_curve_curve(&a, &b, TOL).into_hits());
assert_eq!(hits, search_curve_curve(&&a, &b, TOL).into_hits());
assert_eq!(
curve_curve_extrema(&ea, &eb, TOL).into_minima(),
assert_minima_match(&a, &b)
);
}
#[test]
fn test_concentric_arcs_are_incomplete_along_the_valley() {
let w = std::f64::consts::FRAC_1_SQRT_2;
let arc = |r: f64| {
let poles = vec![
Point::new(r, 0.0, 0.0),
Point::new(r, r, 0.0),
Point::new(0.0, r, 0.0),
];
BSplineCurve::new_rational(
2,
poles,
vec![1.0, w, 1.0],
vec![0.0, 1.0],
vec![3, 3],
false,
)
.unwrap()
};
let (a, b) = (arc(2.0), arc(3.0));
let ext = curve_curve_extrema(&a, &b, TOL);
let (_, regions) = incomplete(ext.completeness());
for k in 0..=16 {
let s = k as f64 / 16.0;
assert!(
regions.iter().any(|r| holds(r, (&a, s), (&b, s))),
"{s}: {regions:#?}"
);
}
assert_eq!(
ext.minima(),
curve_curve_minima(&a, &b, TOL, false).minima()
);
assert_eq!(ext.len(), 1, "{ext:#?}");
assert!((ext[0].distance - 1.0).abs() < 1e-12);
assert!(search_curve_curve(&a, &b, TOL).is_empty());
assert!(search_curve_curve(&a, &b, TOL).is_complete());
}
#[test]
fn test_minima_on_ends_and_corners_match_grid() {
let bow = quadratic([0.0, 0.0, 0.0], [1.0, -1.0, 0.3], [2.0, 0.0, 0.0]);
let rail = segment([-0.5, 1.0, 0.0], [2.5, 1.2, 0.0]);
let ext = assert_minima_match(&bow, &rail);
assert!(
ext.iter()
.any(|e| e.first_parameter == 0.0 || e.first_parameter == 1.0),
"{ext:#?}"
);
let stub = segment([1.0, 2.0, 0.5], [1.2, 4.0, 0.5]);
let ext = assert_minima_match(&bow, &stub);
assert!(ext.iter().any(|e| e.second_parameter == 0.0), "{ext:#?}");
let loop_a = ring([0.0, 0.0, 0.0], 2.0, 0.3);
let ext = assert_minima_match(&stub, &loop_a);
assert!(ext.iter().any(|e| e.first_parameter == 0.0), "{ext:#?}");
assert!(ext.iter().all(|e| (0.0..8.0).contains(&e.second_parameter)));
}
}