mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ceb187308 | |||
| 8263253ad4 | |||
| 77d3eb5cd8 | |||
| 7207472fab | |||
| 2c9d3bfa4b | |||
| a920fa2625 | |||
| 058c05c686 | |||
| 741a2308f4 |
+4
-2
@@ -15,7 +15,9 @@ FLOATS = ["sqrt", "cbrt", "hypot", "sin", "cos", "tan", "sin_cos", "asin", "acos
|
||||
"ln", "ln_1p", "log", "log2", "log10", "mul_add", "recip", "to_radians", "to_degrees"]
|
||||
# Per type: (file, type, path) and the methods of that type kept. A name kept on one type opens
|
||||
# nothing on another. Kept: constructors from given values, field reads and writes, and the
|
||||
# kernel's certified queries on a transform (`is_regular`, `row_lengths`, `of_placement`), which ops calls rather than computes.
|
||||
# kernel's certified queries on a transform (`is_regular`, `row_lengths`, `of_placement`), and the
|
||||
# kernel's maps built from an application's numbers and their composition (`rotation`,
|
||||
# `uniform_scaling`, `reflection`, `concat`), which ops calls rather than computes.
|
||||
TYPES = [
|
||||
("geom/vector3d.rs", "Vec3", "solidcore::Vec3",
|
||||
{"new", "from_array", "to_array", "set_uninitialized", "is_initialized", "is_undef",
|
||||
@@ -25,7 +27,7 @@ TYPES = [
|
||||
"component", "component_mut"}),
|
||||
("math.rs", "Transform", "solidcore::Transform",
|
||||
{"translation", "x_axis", "y_axis", "z_axis", "translation_part", "is_regular", "row_lengths",
|
||||
"of_placement"}),
|
||||
"of_placement", "rotation", "uniform_scaling", "reflection", "concat"}),
|
||||
("geom/transform.rs", "Transform", "solidcore::geom::transform::Transform",
|
||||
{"new", "matrix", "type_code", "type_string", "class_string", "is_kind_of", "read_from_db"}),
|
||||
("geom/matrix.rs", "Matrix", "solidcore::geom::matrix::Matrix",
|
||||
|
||||
-1
@@ -127,7 +127,6 @@ disallowed-methods = [
|
||||
{ path = "solidcore::Vec2::max_dimension", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Vec2::rotate", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Vec2::unitize", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::concat", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::from_axes", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::transform_point", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::transform_vector", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
//! Affine maps from an application's numbers, as the matrices [`crate::Body::transform`] and
|
||||
//! [`crate::curve::transform`] take: rows 0-2 the images of x, y and z, row 3 the translation
|
||||
//! (`p' = p·A + t`), the fourth column `(0, 0, 0, 1)`. A move, a turn about a line, a scale
|
||||
//! about a point, a mirror in a plane, and one map after another. The maps are the kernel's
|
||||
//! ([`solidcore::Transform::translation`], [`solidcore::Transform::rotation`],
|
||||
//! [`solidcore::Transform::uniform_scaling`], [`solidcore::Transform::reflection`],
|
||||
//! [`solidcore::Transform::concat`]); ops checks the numbers and passes them on.
|
||||
|
||||
use solidcore::{Transform, Vec3};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// An affine map in the row-vector convention of [`crate::Body::transform`].
|
||||
pub type Matrix = [[f64; 4]; 4];
|
||||
|
||||
/// Whether every number of `v` is finite.
|
||||
fn finite(v: &[f64]) -> bool {
|
||||
v.iter().all(|x| x.is_finite())
|
||||
}
|
||||
|
||||
/// `v` as the kernel's vector.
|
||||
fn vec3([x, y, z]: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(x, y, z)
|
||||
}
|
||||
|
||||
/// The kernel's map, or [`Error::Argument`] for the numbers it refused.
|
||||
fn taken(map: solidcore::Result<Transform>) -> Result<Matrix> {
|
||||
map.map(|t| t.m).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
/// The move by `by`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a number not finite.
|
||||
pub fn translation(by: [f64; 3]) -> Result<Matrix> {
|
||||
if !finite(&by) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Transform::translation(vec3(by)).m)
|
||||
}
|
||||
|
||||
/// The turn by `angle` radians, right-handed about `axis`, about the line through `point`
|
||||
/// along `axis` (any length but none).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a point, axis or angle not finite, or an axis of no length.
|
||||
pub fn rotation(point: [f64; 3], axis: [f64; 3], angle: f64) -> Result<Matrix> {
|
||||
taken(Transform::rotation(vec3(point), vec3(axis), angle))
|
||||
}
|
||||
|
||||
/// The scale by `factor` in every direction about `centre`, which it keeps.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a centre not finite, or a factor not finite and positive (a mirror
|
||||
/// is [`reflection`]).
|
||||
pub fn scaling(centre: [f64; 3], factor: f64) -> Result<Matrix> {
|
||||
if factor.is_nan() || factor <= 0.0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
taken(Transform::uniform_scaling(vec3(centre), factor))
|
||||
}
|
||||
|
||||
/// The mirror in the plane through `origin` square to `normal` (any length but none).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for an origin or normal not finite, or a normal of no length.
|
||||
pub fn reflection(origin: [f64; 3], normal: [f64; 3]) -> Result<Matrix> {
|
||||
taken(Transform::reflection(vec3(origin), vec3(normal)))
|
||||
}
|
||||
|
||||
/// `first`, then `second` (`p·A1 + t1`, then that `·A2 + t2`): steps stacked into one map.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a map with a number not finite, given or made.
|
||||
pub fn then(first: &Matrix, second: &Matrix) -> Result<Matrix> {
|
||||
if !finite(first.as_flattened()) || !finite(second.as_flattened()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let both = Transform { m: *first }.concat(&Transform { m: *second }).m;
|
||||
if !finite(both.as_flattened()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(both)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::body::REGULAR_RATIO;
|
||||
use solidcore::geom::certified::Verdict;
|
||||
|
||||
/// The mirror in the plane y = 0.
|
||||
const MIRROR_Y: Matrix = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, -1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
const IDENTITY: Matrix = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
/// `p` under `m`, in the row-vector convention.
|
||||
fn apply(m: &Matrix, p: [f64; 3]) -> [f64; 3] {
|
||||
[0, 1, 2].map(|j| p[0] * m[0][j] + p[1] * m[1][j] + p[2] * m[2][j] + m[3][j])
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tol)
|
||||
}
|
||||
|
||||
fn regular(m: &Matrix) -> bool {
|
||||
Transform { m: *m }.is_regular(REGULAR_RATIO) == Verdict::Yes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mirror_in_y_0_is_the_mirror_matrix_and_twice_is_the_identity() {
|
||||
assert_eq!(reflection([0.0; 3], [0.0, 1.0, 0.0]).unwrap(), MIRROR_Y);
|
||||
// any length of an axis normal, any origin on its plane's side: exact
|
||||
for (origin, normal) in [
|
||||
([3.0, 5.0, -2.0], [0.0, 4.0, 0.0]),
|
||||
([1.0, 2.0, 7.5], [0.0, 0.0, -1.0]),
|
||||
([-6.0, 0.0, 1.0], [2.0, 0.0, 0.0]),
|
||||
] {
|
||||
let m = reflection(origin, normal).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert_eq!(then(&m, &m).unwrap(), IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quarter_turn_about_z_takes_x_to_y_and_a_turn_keeps_its_line() {
|
||||
let q = std::f64::consts::FRAC_PI_2;
|
||||
let m = rotation([0.0; 3], [0.0, 0.0, 1.0], q).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, [1.0, 0.0, 0.0]), [0.0, 1.0, 0.0], 1e-15));
|
||||
assert!(near(apply(&m, [0.0, 1.0, 0.0]), [-1.0, 0.0, 0.0], 1e-15));
|
||||
// about the line through (10, 0, 0) along a slanted axis: the line's points stay
|
||||
let m = rotation([10.0, 0.0, 0.0], [1.0, 2.0, 2.0], 0.7).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, [10.0, 0.0, 0.0]), [10.0, 0.0, 0.0], 1e-13));
|
||||
assert!(near(apply(&m, [13.0, 6.0, 6.0]), [13.0, 6.0, 6.0], 1e-12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scale_keeps_its_centre_and_moves_stack() {
|
||||
let c = [1.0, -2.0, 3.0];
|
||||
let m = scaling(c, 2.5).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, c), c, 1e-14));
|
||||
assert!(near(apply(&m, [2.0, -2.0, 3.0]), [3.5, -2.0, 3.0], 1e-14));
|
||||
let (a, b) = ([1.5, -2.0, 3.0], [0.25, 4.0, -1.0]);
|
||||
let sum = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
||||
let stacked = then(&translation(a).unwrap(), &translation(b).unwrap()).unwrap();
|
||||
assert_eq!(stacked, translation(sum).unwrap());
|
||||
assert!(regular(&stacked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_not_finite_or_of_no_size_are_refused() {
|
||||
assert_eq!(translation([f64::NAN, 0.0, 0.0]), Err(Error::Argument));
|
||||
assert_eq!(rotation([0.0; 3], [0.0; 3], 1.0), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
rotation([0.0; 3], [0.0, 0.0, 1.0], f64::INFINITY),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(scaling([0.0; 3], 0.0), Err(Error::Argument));
|
||||
assert_eq!(scaling([0.0; 3], -1.0), Err(Error::Argument));
|
||||
assert_eq!(scaling([0.0; 3], f64::NAN), Err(Error::Argument));
|
||||
assert_eq!(scaling([f64::NAN, 0.0, 0.0], 2.0), Err(Error::Argument));
|
||||
assert_eq!(reflection([0.0; 3], [0.0; 3]), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
reflection([0.0, f64::NAN, 0.0], [0.0, 1.0, 0.0]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
// a stack whose numbers overflow
|
||||
let far = translation([1e308, 0.0, 0.0]).unwrap();
|
||||
assert_eq!(then(&far, &far), Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
+427
-4
@@ -1,15 +1,24 @@
|
||||
//! Free curves: a B-spline from the control points a designer places, one of its control points
|
||||
//! moved, and its curvature comb. The curve is the record's ([`Curve`]); the knots and the
|
||||
//! curvature are the kernel's ([`solidcore::nurbs::knots::clamped_uniform`],
|
||||
//! moved, the curve under an affine map, the curve seen in two views, and its curvature comb.
|
||||
//! The curve is the record's ([`Curve`]); the knots, the map's carrying of the control points,
|
||||
//! the views' meeting and the curvature are the kernel's
|
||||
//! ([`solidcore::nurbs::knots::clamped_uniform`],
|
||||
//! [`solidcore::nurbs::crv_basic::Curve::transform`],
|
||||
//! [`solidcore::intersect::views::curves_from_views`],
|
||||
//! [`solidcore::query::continuity::curve_comb`]). Ops checks the inputs and converts.
|
||||
|
||||
use solidcore::curve::from_parameters::from_parameters;
|
||||
use solidcore::geom::certified::Verdict;
|
||||
use solidcore::intersect::views::{curves_from_views, views_do_not_meet};
|
||||
use solidcore::nurbs::knots::{clamped_uniform, MAX_DEGREE};
|
||||
use solidcore::nurbs::math::{MatrixType, RealMatrix};
|
||||
use solidcore::query::continuity;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_record::brep::Curve;
|
||||
use solidcore::{SolidError, Vec3};
|
||||
use solidcore_record::brep::{Curve, Invalid};
|
||||
|
||||
use crate::body::REGULAR_RATIO;
|
||||
use crate::continuity::{tooth, CombTooth, MAX_COMB_TEETH, MIN_COMB_TEETH};
|
||||
use crate::curve_knots::{net_of, record_of};
|
||||
use crate::session::run;
|
||||
use crate::sweep::{coord_ok, CURVE_MAX_POINTS};
|
||||
use crate::{Error, Result};
|
||||
@@ -85,6 +94,85 @@ fn periodic_twin(knots: &[f64], points: &[[f64; 3]], index: usize) -> Option<usi
|
||||
}
|
||||
}
|
||||
|
||||
/// `curve` under the affine `matrix`, as [`Body::transform`](crate::Body::transform) takes it:
|
||||
/// rows 0-2 the images of x, y and z, row 3 the translation (`p' = p·A + t`), the fourth column
|
||||
/// `(0, 0, 0, 1)`. Every control point is carried by the map
|
||||
/// ([`solidcore::nurbs::crv_basic::Curve::transform`], `N_CrvTransform`); the degree, the knots
|
||||
/// and the weights stay as they were. An affine map
|
||||
/// commutes with the convex combinations a NURBS curve is made of, so the result is the image of
|
||||
/// the curve itself, to the rounding of each carried point (none for a map whose entries are 0
|
||||
/// and ±1 on a spline's points). Reflections, non-uniform scales and shears are maps like any
|
||||
/// other. A spline or a rational spline, clamped or closed (a closed curve's repeated points are
|
||||
/// carried alike, so it stays closed).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`], before the curve is read, for a matrix the kernel does not prove regular
|
||||
/// ([`solidcore::Transform::is_regular`] at [`REGULAR_RATIO`]: an entry not finite, a fourth
|
||||
/// column that is not `(0, 0, 0, 1)`, a smallest scale not proved above the ratio of the
|
||||
/// largest); [`Error::Argument`] for another kind of curve, one the record refuses, or an image
|
||||
/// the record refuses (a coordinate carried past what a double holds); the kernel's status
|
||||
/// otherwise.
|
||||
pub fn transform(curve: &Curve, matrix: &[[f64; 4]; 4]) -> Result<Curve> {
|
||||
let map = solidcore::Transform { m: *matrix };
|
||||
if map.is_regular(REGULAR_RATIO) != Verdict::Yes {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut nurb = net_of(curve)?;
|
||||
// The kernel's matrix acts on a column (`x' = r00 x + r01 y + r02 z + r03`): the record's
|
||||
// row-vector matrix, its entries moved to their transposed places.
|
||||
let [[a00, a01, a02, a03], [a10, a11, a12, a13], [a20, a21, a22, a23], [a30, a31, a32, a33]] =
|
||||
*matrix;
|
||||
let rows = vec![
|
||||
vec![a00, a10, a20, a30],
|
||||
vec![a01, a11, a21, a31],
|
||||
vec![a02, a12, a22, a32],
|
||||
vec![a03, a13, a23, a33],
|
||||
];
|
||||
run("curve::transform", |_| {
|
||||
let columns = RealMatrix::from_rows(3, 3, rows, MatrixType::Full, 3)?;
|
||||
nurb.transform(&columns)?;
|
||||
Ok(record_of(&nurb))
|
||||
})?
|
||||
}
|
||||
|
||||
/// The 3D curves seen as `a` along `a_dir` and as `b` along `b_dir` (a side view and a plan
|
||||
/// view, say): where `a` extruded both ways along `a_dir` meets `b` extruded along `b_dir`, far
|
||||
/// enough that none of it is left out ([`curves_from_views`], whose docs give the bound). Each
|
||||
/// curve is a spline within the kernel's default approximation tolerance of both extrusions.
|
||||
/// The directions need not be unit vectors; `a` and `b` are splines or rational splines, open or
|
||||
/// closed.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a direction not finite or past the coordinate bound, of no length,
|
||||
/// or (within the kernel's angular zero) parallel to the other, or a curve that is not a spline
|
||||
/// the record takes; [`Error::Empty`] when the views do not meet; [`Error::Record`] for a
|
||||
/// meeting curve the record cannot hold; the kernel's status otherwise.
|
||||
pub fn two_view(a: &Curve, a_dir: [f64; 3], b: &Curve, b_dir: [f64; 3]) -> Result<Vec<Curve>> {
|
||||
if !coord_ok(&a_dir) || !coord_ok(&b_dir) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (a, b) = (net_of(a)?, net_of(b)?);
|
||||
let ([ax, ay, az], [bx, by, bz]) = (a_dir, b_dir);
|
||||
run("curve::two_view", |k| {
|
||||
let curves =
|
||||
match curves_from_views(k, &a, Vec3::new(ax, ay, az), &b, Vec3::new(bx, by, bz)) {
|
||||
Ok(curves) => curves,
|
||||
Err(SolidError::InvalidInput { .. }) => return Ok(Err(Error::Argument)),
|
||||
Err(e) if views_do_not_meet(&e) => return Ok(Err(Error::Empty)),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let mut records = Vec::new();
|
||||
for c in &curves {
|
||||
let made = solidcore::curve::from_parameters::record_of(k, c, &c.natural_interval())?;
|
||||
match solidcore_convert::record_curve(made.curve) {
|
||||
Ok(record) => records.push(record),
|
||||
Err(Invalid(why)) => return Ok(Err(Error::Record(why.to_owned()))),
|
||||
}
|
||||
}
|
||||
Ok(Ok(records))
|
||||
})?
|
||||
}
|
||||
|
||||
/// The curvature comb of `curve` over `interval` of its parameter: the kernel's curvature vector
|
||||
/// at `teeth` even steps, both ends included. Stations where the curve has no tangent are left
|
||||
/// out.
|
||||
@@ -257,6 +345,341 @@ mod tests {
|
||||
assert_eq!(points[4], SQUARE[0]);
|
||||
}
|
||||
|
||||
/// The matrix of the reflection about the plane y = 0.
|
||||
const MIRROR_Y: [[f64; 4]; 4] = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, -1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn a_cubic_reflected_about_y_0_has_its_cvs_mirrored_exactly() {
|
||||
let wave = [
|
||||
[0.0, 1.0, 0.5],
|
||||
[2.0, 3.0, -1.0],
|
||||
[4.0, -2.0, 0.25],
|
||||
[6.0, 0.5, 2.0],
|
||||
[8.0, 4.0, 0.0],
|
||||
];
|
||||
let c = cv_curve(&wave, 3).unwrap();
|
||||
let image = transform(&c, &MIRROR_Y).unwrap();
|
||||
let (
|
||||
Curve::Spline { knots, points },
|
||||
Curve::Spline {
|
||||
knots: k1,
|
||||
points: p1,
|
||||
},
|
||||
) = (&c, &image)
|
||||
else {
|
||||
panic!("a spline: {image:?}")
|
||||
};
|
||||
assert_eq!(k1, knots);
|
||||
let mirrored: Vec<[f64; 3]> = points.iter().map(|&[x, y, z]| [x, -y, z]).collect();
|
||||
assert_eq!(p1, &mirrored);
|
||||
// Twice is the curve again.
|
||||
assert_eq!(transform(&image, &MIRROR_Y).unwrap(), c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rational_curve_keeps_its_weights_and_knots() {
|
||||
// A quarter circle of radius 10 about the origin, as a rational quadratic.
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let arc = Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[10.0, 0.0, 0.0, 1.0],
|
||||
[10.0 * w, 10.0 * w, 0.0, w],
|
||||
[0.0, 10.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
let image = transform(&arc, &MIRROR_Y).unwrap();
|
||||
let (
|
||||
Curve::RationalSpline { knots, homogeneous },
|
||||
Curve::RationalSpline {
|
||||
knots: k1,
|
||||
homogeneous: h1,
|
||||
},
|
||||
) = (&arc, &image)
|
||||
else {
|
||||
panic!("a rational spline: {image:?}")
|
||||
};
|
||||
assert_eq!(k1, knots);
|
||||
for (p, q) in homogeneous.iter().zip(h1) {
|
||||
assert_eq!(q[3], p[3], "the weight");
|
||||
let mirrored = [p[0], -p[1], p[2]];
|
||||
assert!(
|
||||
q.iter().zip(mirrored).all(|(a, b)| (a - b).abs() < 1e-12),
|
||||
"{q:?} against {mirrored:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_motion_takes_rows_as_the_axes_images_and_row_3_as_the_move() {
|
||||
// A quarter turn about z (x to y, y to -x), then a move by (1, 2, 3): the convention of
|
||||
// `Body::transform`.
|
||||
let turn = [
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[-1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[1.0, 2.0, 3.0, 1.0],
|
||||
];
|
||||
let image = transform(&cv_curve(&SQUARE, 3).unwrap(), &turn).unwrap();
|
||||
let Curve::Spline { points, .. } = &image else {
|
||||
panic!("a spline: {image:?}")
|
||||
};
|
||||
let moved: Vec<[f64; 3]> = SQUARE
|
||||
.iter()
|
||||
.map(|&[x, y, z]| [1.0 - y, 2.0 + x, 3.0 + z])
|
||||
.collect();
|
||||
assert_eq!(points, &moved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_curve_stays_closed() {
|
||||
let c = crate::curve_fit::closed_cv_curve(&SQUARE, 3).unwrap();
|
||||
let image = transform(&c, &MIRROR_Y).unwrap();
|
||||
let (Curve::Spline { knots, .. }, Curve::Spline { knots: k1, points }) = (&c, &image)
|
||||
else {
|
||||
panic!("a spline: {image:?}")
|
||||
};
|
||||
assert_eq!(k1, knots);
|
||||
// points 0, 1 and 2 are repeated as 4, 5 and 6
|
||||
for (p, copy) in points.iter().zip(&points[4..]) {
|
||||
assert_eq!(p, copy);
|
||||
}
|
||||
assert_eq!(points[1], [10.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_singular_matrix_or_another_curve_is_refused() {
|
||||
let c = cv_curve(&SQUARE, 3).unwrap();
|
||||
let mut flat = MIRROR_Y;
|
||||
flat[1][1] = 0.0;
|
||||
assert_eq!(transform(&c, &flat), Err(Error::Argument));
|
||||
let mut projective = MIRROR_Y;
|
||||
projective[0][3] = 1.0;
|
||||
assert_eq!(transform(&c, &projective), Err(Error::Argument));
|
||||
let mut nan = MIRROR_Y;
|
||||
nan[3][0] = f64::NAN;
|
||||
assert_eq!(transform(&c, &nan), Err(Error::Argument));
|
||||
let line = Curve::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0; 3],
|
||||
};
|
||||
assert_eq!(transform(&line, &MIRROR_Y), Err(Error::Argument));
|
||||
}
|
||||
|
||||
/// The kernel's default approximation tolerance of a traced curve: half its zone tolerance
|
||||
/// of 1e-5.
|
||||
const APPROX_TOL: f64 = 5e-6;
|
||||
|
||||
/// The side view, z = f(x) in the plane y = 0: a cubic Bézier curve whose control points
|
||||
/// are evenly spaced in x, so x = 9 t; its slope |dz/dx| at most 1 (its polygon's).
|
||||
fn side() -> Curve {
|
||||
cv_curve(
|
||||
&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 0.0, 2.0],
|
||||
[6.0, 0.0, -1.0],
|
||||
[9.0, 0.0, 1.0],
|
||||
],
|
||||
3,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The plan view, y = g(x) in the plane z = 0, x = shift - 3 + 15 t; |dy/dx| at most 0.6.
|
||||
fn plan(shift: f64) -> Curve {
|
||||
cv_curve(
|
||||
&[
|
||||
[shift - 3.0, 1.0, 0.0],
|
||||
[shift + 2.0, 4.0, 0.0],
|
||||
[shift + 7.0, 2.0, 0.0],
|
||||
[shift + 12.0, 5.0, 0.0],
|
||||
],
|
||||
3,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The point of the spline `c` at `t`, by the kernel's evaluator.
|
||||
fn at(c: &Curve, t: f64) -> [f64; 3] {
|
||||
let p = net_of(c)
|
||||
.unwrap()
|
||||
.eval(t, solidcore::nurbs::knots::Side::Left)
|
||||
.unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_side_and_a_plan_view_give_the_curve_on_both_extrusions() {
|
||||
let (a, b) = (side(), plan(0.0));
|
||||
let curves = two_view(&a, [0.0, 1.0, 0.0], &b, [0.0, 0.0, 2.0]).unwrap();
|
||||
assert!(!curves.is_empty());
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for c in &curves {
|
||||
let (Curve::Spline { knots, .. } | Curve::RationalSpline { knots, .. }) = c else {
|
||||
panic!("a spline: {c:?}")
|
||||
};
|
||||
let (t0, t1) = (knots[0], knots[knots.len() - 1]);
|
||||
for i in 0..=64 {
|
||||
let x = at(c, t0 + (t1 - t0) * f64::from(i) / 64.0);
|
||||
assert!(
|
||||
(-APPROX_TOL..=9.0 + APPROX_TOL).contains(&x[0]),
|
||||
"past the side view: {x:?}"
|
||||
);
|
||||
(lo, hi) = (lo.min(x[0]), hi.max(x[0]));
|
||||
// On the side view's extrusion along y: a gap measured along z is at most the
|
||||
// distance times sqrt(1 + slope²), below 1 + slope, the slope at most 1.
|
||||
let on_a = at(&a, (x[0] / 9.0).clamp(0.0, 1.0));
|
||||
assert!(
|
||||
(on_a[2] - x[2]).abs() <= 2.0 * APPROX_TOL,
|
||||
"{x:?} against {on_a:?}"
|
||||
);
|
||||
// On the plan view's extrusion along z, the slope at most 0.6.
|
||||
let on_b = at(&b, (x[0] + 3.0) / 15.0);
|
||||
assert!(
|
||||
(on_b[1] - x[1]).abs() <= 1.6 * APPROX_TOL,
|
||||
"{x:?} against {on_b:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// The whole of the side view's run is seen.
|
||||
assert!(
|
||||
lo <= APPROX_TOL && hi >= 9.0 - APPROX_TOL,
|
||||
"from {lo} to {hi}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A side view in y = 0 (seen along y) and a plan view in z = 0 (seen along z) at model
|
||||
/// scale, about 170 mm across: quadratic Bézier curves, both increasing in x.
|
||||
const SIDE_Q: [[f64; 3]; 3] = [
|
||||
[-92.06693847750134, 0.0, -39.76423812596792],
|
||||
[0.0, 0.0, 18.451591211372346],
|
||||
[73.55298836314594, 0.0, 15.024361734430357],
|
||||
];
|
||||
const PLAN_Q: [[f64; 3]; 3] = [
|
||||
[-16.758544054811637, -82.27159357644084, 0.0],
|
||||
[42.459806193714414, -83.42331426400659, 0.0],
|
||||
[76.37870944288886, -34.86770213654586, 0.0],
|
||||
];
|
||||
|
||||
/// The point of the quadratic Bézier curve `q` at `t` and its derivative, in Bernstein's
|
||||
/// form (none of the kernel's evaluators).
|
||||
fn bernstein(q: &[[f64; 3]; 3], t: f64) -> ([f64; 3], [f64; 3]) {
|
||||
let (b0, b1, b2) = ((1.0 - t) * (1.0 - t), 2.0 * t * (1.0 - t), t * t);
|
||||
(
|
||||
[0, 1, 2].map(|i| b0 * q[0][i] + b1 * q[1][i] + b2 * q[2][i]),
|
||||
[0, 1, 2]
|
||||
.map(|i| 2.0 * (1.0 - t) * (q[1][i] - q[0][i]) + 2.0 * t * (q[2][i] - q[1][i])),
|
||||
)
|
||||
}
|
||||
|
||||
/// The parameter of `q` (increasing in x) where its x is `x`, by bisection on [0, 1].
|
||||
fn at_x(q: &[[f64; 3]; 3], x: f64) -> f64 {
|
||||
let (mut lo, mut hi) = (0.0, 1.0);
|
||||
for _ in 0..100 {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if bernstein(q, m).0[0] < x {
|
||||
lo = m;
|
||||
} else {
|
||||
hi = m;
|
||||
}
|
||||
}
|
||||
0.5 * (lo + hi)
|
||||
}
|
||||
|
||||
/// The point at `t` of the record spline with `knots` and homogeneous points `pw`, by
|
||||
/// de Boor's algorithm (none of the kernel's evaluators).
|
||||
fn de_boor(knots: &[f64], pw: &[[f64; 4]], t: f64) -> [f64; 3] {
|
||||
let n = pw.len();
|
||||
let p = knots.len() - n - 1;
|
||||
let k = (p..n).rev().find(|&k| knots[k] <= t).unwrap_or(p);
|
||||
let mut d: Vec<[f64; 4]> = (0..=p).map(|j| pw[j + k - p]).collect();
|
||||
for r in 1..=p {
|
||||
for j in (r..=p).rev() {
|
||||
let (lo, hi) = (knots[j + k - p], knots[j + 1 + k - r]);
|
||||
let alpha = (t - lo) / (hi - lo);
|
||||
d[j] = [0, 1, 2, 3].map(|i| (1.0 - alpha) * d[j - 1][i] + alpha * d[j][i]);
|
||||
}
|
||||
}
|
||||
let h = d[p];
|
||||
[h[0] / h[3], h[1] / h[3], h[2] / h[3]]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_at_model_scale_give_the_curve_within_the_tolerance() {
|
||||
let side = cv_curve(&SIDE_Q, 2).unwrap();
|
||||
let plan = cv_curve(&PLAN_Q, 2).unwrap();
|
||||
let curves = two_view(&side, [0.0, 1.0, 0.0], &plan, [0.0, 0.0, 1.0]).unwrap();
|
||||
assert!(!curves.is_empty());
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for c in &curves {
|
||||
let (knots, pw): (&[f64], Vec<[f64; 4]>) = match c {
|
||||
Curve::Spline { knots, points } => (
|
||||
knots,
|
||||
points.iter().map(|&[x, y, z]| [x, y, z, 1.0]).collect(),
|
||||
),
|
||||
Curve::RationalSpline { knots, homogeneous } => (knots, homogeneous.clone()),
|
||||
_ => panic!("a spline: {c:?}"),
|
||||
};
|
||||
let p = knots.len() - pw.len() - 1;
|
||||
let (t0, t1) = (knots[p], knots[pw.len()]);
|
||||
for i in 0..=512 {
|
||||
let x = de_boor(knots, &pw, t0 + (t1 - t0) * f64::from(i) / 512.0);
|
||||
(lo, hi) = (lo.min(x[0]), hi.max(x[0]));
|
||||
// A gap along the view's other axis is at most the distance times
|
||||
// sqrt(1 + slope²), below 1 + slope; twice the tolerance, as the intersector
|
||||
// checks its spans at interior samples.
|
||||
let (q, d) = bernstein(&SIDE_Q, at_x(&SIDE_Q, x[0]));
|
||||
let allowed = 2.0 * APPROX_TOL * (1.0 + (d[2] / d[0]).abs());
|
||||
assert!(
|
||||
(q[2] - x[2]).abs() <= allowed,
|
||||
"off the side view: {x:?} against {q:?}"
|
||||
);
|
||||
let (q, d) = bernstein(&PLAN_Q, at_x(&PLAN_Q, x[0]));
|
||||
let allowed = 2.0 * APPROX_TOL * (1.0 + (d[1] / d[0]).abs());
|
||||
assert!(
|
||||
(q[1] - x[1]).abs() <= allowed,
|
||||
"off the plan view: {x:?} against {q:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// The overlap of the two runs is seen: from the plan view's start to the side view's end.
|
||||
assert!(
|
||||
(lo - PLAN_Q[0][0]).abs() <= 2.0 * APPROX_TOL
|
||||
&& (hi - SIDE_Q[2][0]).abs() <= 2.0 * APPROX_TOL,
|
||||
"from {lo} to {hi}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_directions_are_refused_and_views_apart_are_empty() {
|
||||
let (a, b) = (side(), plan(0.0));
|
||||
let y = [0.0, 1.0, 0.0];
|
||||
assert_eq!(two_view(&a, y, &b, [0.0, -2.0, 0.0]), Err(Error::Argument));
|
||||
assert_eq!(two_view(&a, y, &b, [0.0; 3]), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
two_view(&a, y, &b, [f64::NAN, 0.0, 1.0]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
let line = Curve::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0; 3],
|
||||
};
|
||||
assert_eq!(
|
||||
two_view(&line, y, &b, [0.0, 0.0, 1.0]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
// The plan view moved to x in [20, 35], past the side view's [0, 9].
|
||||
assert_eq!(
|
||||
two_view(&a, y, &plan(23.0), [0.0, 0.0, 1.0]),
|
||||
Err(Error::Empty)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_arcs_comb_points_at_its_centre_with_the_inverse_radius() {
|
||||
// A quarter circle of radius 10 about (1, 2, 3) in its z plane, as a rational quadratic.
|
||||
|
||||
+18
-6
@@ -131,6 +131,24 @@ fn within(
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for another curve, one the record refuses, or knots not clamped.
|
||||
pub(crate) fn nurb_of(curve: &Curve) -> Result<NurbCurve> {
|
||||
let nurb = net_of(curve)?;
|
||||
let (knots, degree) = (nurb.knots(), nurb.degree());
|
||||
let flat =
|
||||
|ends: Option<&[f64]>| ends.is_some_and(|e| e.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(degree + 1);
|
||||
if !flat(knots.get(..=degree)) || !flat(last.and_then(|s| knots.get(s..))) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(nurb)
|
||||
}
|
||||
|
||||
/// The kernel's curve of a record spline or rational spline that is valid, on any knots the
|
||||
/// record takes (clamped, or a closed curve's periodic ones): its control points and their
|
||||
/// weights, its degree and knots as they are.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for another curve, or one the record refuses.
|
||||
pub(crate) fn net_of(curve: &Curve) -> Result<NurbCurve> {
|
||||
check_curve(curve, &mut Budget::input()).map_err(|_| Error::Argument)?;
|
||||
let (knots, pw): (&[f64], Vec<CPoint>) = match curve {
|
||||
Curve::Spline { knots, points } => (
|
||||
@@ -153,12 +171,6 @@ pub(crate) fn nurb_of(curve: &Curve) -> Result<NurbCurve> {
|
||||
.len()
|
||||
.checked_sub(pw.len() + 1)
|
||||
.ok_or(Error::Argument)?;
|
||||
let flat =
|
||||
|ends: Option<&[f64]>| ends.is_some_and(|e| e.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(degree + 1);
|
||||
if !flat(knots.get(..=degree)) || !flat(last.and_then(|s| knots.get(s..))) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
NurbCurve::new(pw, degree, knots.to_vec()).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -18,6 +18,7 @@
|
||||
//! the kernel's entities;
|
||||
//! - `finish`: the result contract, from what the input was (crate-internal: the families use it);
|
||||
//! - [`provenance`]: the hook an application's construction history rides on.
|
||||
//! - [`affine`]: the maps a body or a curve is moved by, from an application's numbers.
|
||||
//!
|
||||
//! The families: [`measure`] (mass properties, area, box, point class, gap samples, the picks'
|
||||
//! distances and normals), [`contact`] (whether bodies meet, their boundary distance),
|
||||
@@ -29,6 +30,7 @@
|
||||
#![deny(clippy::disallowed_methods)]
|
||||
#![cfg_attr(not(test), deny(clippy::float_arithmetic))]
|
||||
|
||||
pub mod affine;
|
||||
pub mod blend;
|
||||
pub mod body;
|
||||
pub mod boolean;
|
||||
|
||||
+152
-7
@@ -16,10 +16,12 @@ use solidcore::nurbs::b_spline_curve::{
|
||||
BSplineCurve, BSplineCurveForm, CanonicalData, Ends, KnotType,
|
||||
};
|
||||
use solidcore::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
|
||||
use solidcore::nurbs::n_sided::{n_sided as kernel_n_sided, NSidedRefusal, MAX_SIDES, MIN_SIDES};
|
||||
use solidcore::primitive::api_curve_ops::make_curves_compatible;
|
||||
use solidcore::query::api_queries::point_distance;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
use solidcore::topology::brep::p4::stitch_faces;
|
||||
use solidcore::topology::brep::sheet::face_on;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::SolidError;
|
||||
@@ -237,7 +239,8 @@ fn compatible(a: BSplineCurve, b: BSplineCurve) -> solidcore::Result<[BSplineCur
|
||||
/// curve, point for point at the same fraction of its range (a rational curve, an arc, stays the
|
||||
/// same curve but not the same parameter: the kernel gives the four common end weights). Three curves bound the same patch
|
||||
/// with its fourth side collapsed to the point where the third curve meets the first (a
|
||||
/// degenerate Coons patch: one side of its box is that corner). Each curve is taken over its
|
||||
/// degenerate Coons patch: one side of its box is that corner). Five or more bound the
|
||||
/// certified n-sided fill ([`n_sided`]), whose certificate this drops. Each curve is taken over its
|
||||
/// natural range (a line from its start to its end, a spline over its knots' domain).
|
||||
///
|
||||
/// The ends must meet within the model's zone tolerance: the kernel's tolerance within which
|
||||
@@ -248,15 +251,14 @@ fn compatible(a: BSplineCurve, b: BSplineCurve) -> solidcore::Result<[BSplineCur
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than three curves, or a curve refused (a circle, an ellipse, a
|
||||
/// traced intersection curve, numbers the kernel refuses); [`Error::Unsupported`] for five or
|
||||
/// more curves (no n-sided patch yet); [`Error::BoundaryGap`] for curves that do not meet; the
|
||||
/// kernel's status.
|
||||
/// traced intersection curve, numbers the kernel refuses); [`Error::BoundaryGap`] for curves
|
||||
/// that do not meet; for five or more, those of [`n_sided`]; the kernel's status.
|
||||
pub fn boundary(curves: &[Curve]) -> Result<Record> {
|
||||
if curves.len() < 3 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if curves.len() > 4 {
|
||||
return Err(Error::Unsupported("a patch bounded by five or more curves"));
|
||||
return n_sided(curves).map(|(sheet, _)| sheet);
|
||||
}
|
||||
record(run("surface::boundary", |k| {
|
||||
let mut sides = refused!(splines(k, curves)?);
|
||||
@@ -286,6 +288,66 @@ pub fn boundary(curves: &[Curve]) -> Result<Record> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The n-sided patch bounded by `curves` (three or more, meeting end to end round one loop in
|
||||
/// the order given, each turned to run on from the one before, as [`boundary`] takes them): the
|
||||
/// kernel's fill, N four-sided faces round a centre, each bounded by halves of two of the curves
|
||||
/// and two inner curves from their middles to the centre, stitched into one sheet; and the
|
||||
/// certified bound within which every curve is the sheet's boundary (`solidcore::nurbs::n_sided`).
|
||||
/// The faces meet along the inner curves within the model's zone tolerance (G0, certified);
|
||||
/// tangency across them is the fill's aim, within its angle tolerance, not certified.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`boundary`] for the curves and their joins; [`Error::Unsupported`] for a rational curve
|
||||
/// (an arc: the fill takes polynomial curves) or more than 64 curves; [`Error::PastTolerance`]
|
||||
/// for a boundary or an inner join not proved within the zone tolerance; the kernel's status.
|
||||
pub fn n_sided(curves: &[Curve]) -> Result<(Record, f64)> {
|
||||
if curves.len() < MIN_SIDES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if curves.len() > MAX_SIDES {
|
||||
return Err(Error::Unsupported(
|
||||
"an n-sided patch of more than 64 curves",
|
||||
));
|
||||
}
|
||||
let mut bound = None;
|
||||
let made = run("surface::n_sided", |k| {
|
||||
let mut sides = refused!(splines(k, curves)?);
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
refused!(looped(&mut sides, tol)?);
|
||||
match kernel_n_sided(&sides, tol)? {
|
||||
Ok(fill) => {
|
||||
bound = Some(fill.bound);
|
||||
sheet_of(k, fill.patches)
|
||||
}
|
||||
Err(NSidedRefusal::Rational) => Ok(Err(Error::Unsupported(
|
||||
"a rational curve bounding an n-sided patch",
|
||||
))),
|
||||
Err(NSidedRefusal::PastTolerance { bound }) => Ok(Err(Error::PastTolerance { bound })),
|
||||
Err(NSidedRefusal::Joins { gap }) => Ok(Err(Error::PastTolerance { bound: gap })),
|
||||
}
|
||||
});
|
||||
let sheet = record(made)?;
|
||||
Ok((sheet, bound.ok_or(Error::Exception)?))
|
||||
}
|
||||
|
||||
/// A sheet of one face on each of `patches`, over its whole natural box, stitched along the
|
||||
/// sides they share.
|
||||
fn sheet_of(k: &mut Kernel, patches: Vec<BSplineSurface>) -> solidcore::Result<Out<Brep>> {
|
||||
let mut b = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
for s in patches {
|
||||
let s = Surface::from(s);
|
||||
let bx = s.natural_uv_domain_with_nurb(k)?;
|
||||
face_on(k, &mut b, s, &bx)?;
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
let tol = b.tolerance();
|
||||
if let Err(e) = stitch_faces(k, &mut b, tol, None, false) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
finish(k, b, Expect::FACES)
|
||||
}
|
||||
|
||||
/// The ruled surface between `a` and `b`, one face: `S(s, t) = (1 - t) a(s) + t b(s)`, `s` the
|
||||
/// same fraction of each curve's natural range (the two first made compatible, exactly, as in
|
||||
/// [`boundary`]), `t` from 0 on `a` to 1 on `b`. So the surface's sides are the two curves
|
||||
@@ -561,7 +623,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_few_or_too_many_curves_and_circles_are_refused() {
|
||||
fn too_few_curves_and_circles_are_refused_and_five_fill() {
|
||||
assert_eq!(
|
||||
boundary(&[line(P[0], P[1]), line(P[1], P[0])]),
|
||||
Err(Error::Argument)
|
||||
@@ -576,7 +638,8 @@ mod tests {
|
||||
let pentagon: Vec<Curve> = (0..5)
|
||||
.map(|i| line(corners[i], corners[(i + 1) % 5]))
|
||||
.collect();
|
||||
assert!(matches!(boundary(&pentagon), Err(Error::Unsupported(_))));
|
||||
// five curves: the n-sided fill, five faces
|
||||
assert_eq!(boundary(&pentagon).unwrap().faces.len(), 5);
|
||||
let circle = Curve::Circle {
|
||||
frame: Frame {
|
||||
origin: [0.0; 3],
|
||||
@@ -590,4 +653,86 @@ mod tests {
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
/// The corners of a regular pentagon of radius 10 about the z axis, at height `z(x, y)`.
|
||||
fn pentagon(z: impl Fn(f64, f64) -> f64) -> Vec<[f64; 3]> {
|
||||
(0..5)
|
||||
.map(|i| {
|
||||
let a = std::f64::consts::TAU * f64::from(i) / 5.0;
|
||||
let (x, y) = (10.0 * a.cos(), 10.0 * a.sin());
|
||||
[x, y, z(x, y)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_regular_pentagon_of_lines_fills_flat() {
|
||||
let p = pentagon(|_, _| 0.0);
|
||||
let sides: Vec<Curve> = (0..5).map(|i| line(p[i], p[(i + 1) % 5])).collect();
|
||||
let (sheet, bound) = n_sided(&sides).unwrap();
|
||||
assert!(bound <= 1e-9, "{bound}");
|
||||
assert_eq!(sheet.faces.len(), 5);
|
||||
// five faces round a centre, stitched along the five inner curves
|
||||
assert_eq!(sheet.edges.iter().filter(|e| e.uses.len() == 2).count(), 5);
|
||||
// planar data, planar faces: every point of every face in z = 0
|
||||
for face in &sheet.faces {
|
||||
for (s, t) in [(0.0, 0.0), (0.3, 0.6), (1.0, 0.5), (0.5, 1.0)] {
|
||||
assert!(at(face, s, t)[2].abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_splines_on_a_saddle_bound_their_patch() {
|
||||
// each side a cubic whose control points lie on z = x y / 50 at thirds of the chord
|
||||
let z = |x: f64, y: f64| x * y / 50.0;
|
||||
let p = pentagon(z);
|
||||
let sides: Vec<Curve> = (0..5)
|
||||
.map(|i| {
|
||||
let (a, b) = (p[i], p[(i + 1) % 5]);
|
||||
let third = |t: f64| {
|
||||
let (x, y) = (a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1]));
|
||||
[x, y, z(x, y)]
|
||||
};
|
||||
Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: vec![a, third(1.0 / 3.0), third(2.0 / 3.0), b],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (sheet, bound) = n_sided(&sides).unwrap();
|
||||
assert!(bound <= 1e-6, "{bound}");
|
||||
assert_eq!(sheet.faces.len(), 5);
|
||||
// the corners are vertices of the sheet: its boundary runs through them
|
||||
for corner in &p {
|
||||
assert!(
|
||||
sheet.vertices.iter().any(|v| near(v.point, *corner, 1e-6)),
|
||||
"{corner:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_arcs_on_a_sphere_are_refused() {
|
||||
// five arcs of the circle z = 6 on the sphere of radius 10 (radius 8), as rational
|
||||
// quadratics
|
||||
let half = std::f64::consts::PI / 5.0;
|
||||
let w = half.cos();
|
||||
let arcs: Vec<Curve> = (0..5)
|
||||
.map(|i| {
|
||||
let a = 2.0 * half * f64::from(i);
|
||||
let on = |t: f64, r: f64| [r * t.cos(), r * t.sin(), 6.0];
|
||||
let (s, m, e) = (on(a, 8.0), on(a + half, 8.0 / w), on(a + 2.0 * half, 8.0));
|
||||
Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[s[0], s[1], s[2], 1.0],
|
||||
[m[0] * w, m[1] * w, m[2] * w, w],
|
||||
[e[0], e[1], e[2], 1.0],
|
||||
],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(matches!(n_sided(&arcs), Err(Error::Unsupported(_))));
|
||||
}
|
||||
}
|
||||
|
||||
+301
@@ -79,6 +79,147 @@ pub fn move_cvs(sheet: &Brep, moves: &[((usize, usize), [f64; 3])]) -> Result<Br
|
||||
}))
|
||||
}
|
||||
|
||||
/// What [`move_net`] moves: whole rows (u indices), whole columns (v indices) or single control
|
||||
/// points `(i, j)`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Picks {
|
||||
/// Every point of these u indices.
|
||||
Rows(Vec<usize>),
|
||||
/// Every point of these v indices.
|
||||
Columns(Vec<usize>),
|
||||
/// These points, `(u index, v index)`.
|
||||
Cvs(Vec<(usize, usize)>),
|
||||
}
|
||||
|
||||
/// The shape of a soft selection's falloff over the index distance `d` from the picks, with
|
||||
/// `s = d / (radius + 1)`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Shape {
|
||||
/// `1 - s`.
|
||||
Linear,
|
||||
/// `1 - (3 s² - 2 s³)`: flat at the picks and at the radius's edge.
|
||||
Smooth,
|
||||
}
|
||||
|
||||
/// A soft selection: the points within `radius` index steps of the picks move too, by the
|
||||
/// weight [`Shape`] gives their distance.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Falloff {
|
||||
/// The most index steps from a pick a point moves at: 0 moves the picks alone.
|
||||
pub radius: u32,
|
||||
/// How the weight falls from 1 at the picks.
|
||||
pub shape: Shape,
|
||||
}
|
||||
|
||||
/// The weight of a point `d` index steps from the nearest pick: 1 at a pick, falling by
|
||||
/// `falloff`'s shape, 0 past its radius (and for every point off the picks without a falloff).
|
||||
#[must_use]
|
||||
pub fn falloff_weight(falloff: Option<Falloff>, d: usize) -> f64 {
|
||||
let Some(f) = falloff else {
|
||||
return if d == 0 { 1.0 } else { 0.0 };
|
||||
};
|
||||
let Ok(d) = u32::try_from(d) else {
|
||||
return 0.0;
|
||||
};
|
||||
if d > f.radius {
|
||||
return 0.0;
|
||||
}
|
||||
let s = f64::from(d) / (f64::from(f.radius) + 1.0);
|
||||
match f.shape {
|
||||
Shape::Linear => 1.0 - s,
|
||||
Shape::Smooth => 1.0 - s * s * (3.0 - 2.0 * s),
|
||||
}
|
||||
}
|
||||
|
||||
/// The weight table of a `rows` by `cols` net (row by row, as [`Net::points`]): each point's
|
||||
/// [`falloff_weight`] at its index distance from the nearest pick. A row pick's distance is
|
||||
/// along u (`|i - row|`), a column pick's along v (`|j - column|`), a point pick's the larger of
|
||||
/// the two (the square ring of the net around it).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for no picks, a pick past the net, or a net of no points or more than
|
||||
/// [`REBUILD_MAX_SAMPLES`].
|
||||
pub fn net_weights(
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
picks: &Picks,
|
||||
falloff: Option<Falloff>,
|
||||
) -> Result<Vec<f64>> {
|
||||
let total = rows
|
||||
.checked_mul(cols)
|
||||
.filter(|&n| n > 0 && n <= REBUILD_MAX_SAMPLES)
|
||||
.ok_or(Error::Argument)?;
|
||||
let (empty, inside) = match picks {
|
||||
Picks::Rows(p) => (p.is_empty(), p.iter().all(|&i| i < rows)),
|
||||
Picks::Columns(p) => (p.is_empty(), p.iter().all(|&j| j < cols)),
|
||||
Picks::Cvs(p) => (p.is_empty(), p.iter().all(|&(i, j)| i < rows && j < cols)),
|
||||
};
|
||||
if empty || !inside {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let nearest = |i: usize, j: usize| -> usize {
|
||||
let d = match picks {
|
||||
Picks::Rows(p) => p.iter().map(|&r| i.abs_diff(r)).min(),
|
||||
Picks::Columns(p) => p.iter().map(|&c| j.abs_diff(c)).min(),
|
||||
Picks::Cvs(p) => p
|
||||
.iter()
|
||||
.map(|&(a, b)| i.abs_diff(a).max(j.abs_diff(b)))
|
||||
.min(),
|
||||
};
|
||||
d.unwrap_or(usize::MAX)
|
||||
};
|
||||
let mut table = Vec::new();
|
||||
table
|
||||
.try_reserve_exact(total)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
table.push(falloff_weight(falloff, nearest(i, j)));
|
||||
}
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
/// `sheet` with the picked control points moved by `delta`, and with a `falloff` the points
|
||||
/// around them by their weight of it ([`net_weights`]): the kernel moves each point of weight
|
||||
/// `t` by `t delta` (`Surface::displace_cpts`). The knots and degrees are kept; the result is a
|
||||
/// new one-face sheet on the moved surface, its whole box.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`control_net`] and [`net_weights`]; [`Error::Argument`] for a `delta` not finite or
|
||||
/// past the coordinate bound, or a moved point past it; the kernel's status otherwise.
|
||||
pub fn move_net(
|
||||
sheet: &Brep,
|
||||
picks: &Picks,
|
||||
delta: [f64; 3],
|
||||
falloff: Option<Falloff>,
|
||||
) -> Result<Brep> {
|
||||
if !coord_ok(&delta) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let ((u, v), points) = untrimmed(sheet)?;
|
||||
let net = control_net(sheet)?;
|
||||
let weights = net_weights(net.rows, net.cols, picks, falloff)?;
|
||||
let moves: Vec<((usize, usize), f64)> = (0..net.rows)
|
||||
.flat_map(|i| (0..net.cols).map(move |j| (i, j)))
|
||||
.zip(weights)
|
||||
.filter(|&(_, t)| t > 0.0)
|
||||
.collect();
|
||||
let mut surface = kernel_surface(u, v, points)?;
|
||||
let moved = record(run("surface_cvs::move_net", |k| {
|
||||
surface.displace_cpts(&moves, Vec3::new(delta[0], delta[1], delta[2]))?;
|
||||
sheet_of(k, BSplineSurface::from_gw_nurb(&surface))
|
||||
}))?;
|
||||
if !control_net(&moved)?
|
||||
.points
|
||||
.iter()
|
||||
.all(|p| coord_ok(p.as_slice()))
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// `sheet`'s surface refitted to degrees `(deg_u, deg_v)` with `(spans_u, spans_v)` uniform
|
||||
/// spans on `[0, 1]²`, and the kernel's certified bound of how far the refit is from the
|
||||
/// surface at every parameter ([`certified_distance`]), kept when within `tol`. The surface,
|
||||
@@ -374,6 +515,166 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_move_moves_exactly_that_row() {
|
||||
let s = long_sheet();
|
||||
let net = control_net(&s).unwrap();
|
||||
let delta = [0.0, 0.0, 1.0];
|
||||
let last = net.rows - 1;
|
||||
for falloff in [
|
||||
None,
|
||||
Some(Falloff {
|
||||
radius: 0,
|
||||
shape: Shape::Smooth,
|
||||
}),
|
||||
] {
|
||||
let moved = move_net(&s, &Picks::Rows(vec![last]), delta, falloff).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
assert_eq!((after.rows, after.cols), (net.rows, net.cols));
|
||||
for (n, (p, q)) in net.points.iter().zip(&after.points).enumerate() {
|
||||
if n / net.cols == last {
|
||||
assert_eq!(*q, [p[0], p[1], p[2] + 1.0], "point {n}");
|
||||
} else {
|
||||
assert_eq!(p, q, "point {n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// a column the same way
|
||||
let moved = move_net(&s, &Picks::Columns(vec![0]), delta, None).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
for (n, (p, q)) in net.points.iter().zip(&after.points).enumerate() {
|
||||
if n % net.cols == 0 {
|
||||
assert_eq!(*q, [p[0], p[1], p[2] + 1.0], "point {n}");
|
||||
} else {
|
||||
assert_eq!(p, q, "point {n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falloff_weights_are_one_at_the_pick_zero_past_the_radius_and_monotone() {
|
||||
assert_eq!(falloff_weight(None, 0), 1.0);
|
||||
assert_eq!(falloff_weight(None, 1), 0.0);
|
||||
for shape in [Shape::Linear, Shape::Smooth] {
|
||||
let f = Some(Falloff { radius: 3, shape });
|
||||
let w: Vec<f64> = (0..8).map(|d| falloff_weight(f, d)).collect();
|
||||
assert_eq!(w[0], 1.0, "{shape:?}");
|
||||
assert!(
|
||||
w[1..=3].iter().all(|&x| x > 0.0 && x < 1.0),
|
||||
"{shape:?}: {w:?}"
|
||||
);
|
||||
assert!(w[4..].iter().all(|&x| x == 0.0), "{shape:?}: {w:?}");
|
||||
assert!(
|
||||
w.windows(2).all(|p| p[1] < p[0] || p[1] == 0.0),
|
||||
"{shape:?}: {w:?}"
|
||||
);
|
||||
// the table: a point pick's distance is its square ring on the net
|
||||
let table = net_weights(9, 7, &Picks::Cvs(vec![(4, 3)]), f).unwrap();
|
||||
assert_eq!(table.len(), 63);
|
||||
for i in 0..9_usize {
|
||||
for j in 0..7_usize {
|
||||
let d = i.abs_diff(4).max(j.abs_diff(3));
|
||||
assert_eq!(table[i * 7 + j], w[d], "{shape:?} at ({i}, {j})");
|
||||
}
|
||||
}
|
||||
}
|
||||
// the smooth shape is flat at the pick: it falls less over the first step
|
||||
let (linear, smooth) = (
|
||||
falloff_weight(
|
||||
Some(Falloff {
|
||||
radius: 3,
|
||||
shape: Shape::Linear,
|
||||
}),
|
||||
1,
|
||||
),
|
||||
falloff_weight(
|
||||
Some(Falloff {
|
||||
radius: 3,
|
||||
shape: Shape::Smooth,
|
||||
}),
|
||||
1,
|
||||
),
|
||||
);
|
||||
assert!(smooth > linear, "{smooth} {linear}");
|
||||
// rows and columns by their one index
|
||||
let rows = net_weights(
|
||||
5,
|
||||
3,
|
||||
&Picks::Rows(vec![0]),
|
||||
Some(Falloff {
|
||||
radius: 1,
|
||||
shape: Shape::Linear,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rows,
|
||||
vec![1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
);
|
||||
// bad picks
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Rows(vec![]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Rows(vec![5]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Columns(vec![3]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Cvs(vec![(0, 3)]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(0, 3, &Picks::Rows(vec![0]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_soft_move_moves_each_point_by_its_weight() {
|
||||
let s = bicubic();
|
||||
let net = control_net(&s).unwrap();
|
||||
let f = Some(Falloff {
|
||||
radius: 1,
|
||||
shape: Shape::Linear,
|
||||
});
|
||||
let picks = Picks::Cvs(vec![(1, 1)]);
|
||||
let moved = move_net(&s, &picks, [0.0, 0.0, 2.0], f).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
let weights = net_weights(net.rows, net.cols, &picks, f).unwrap();
|
||||
for (n, ((p, q), w)) in net
|
||||
.points
|
||||
.iter()
|
||||
.zip(&after.points)
|
||||
.zip(&weights)
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!((q[0], q[1]), (p[0], p[1]), "point {n}");
|
||||
assert!(
|
||||
(q[2] - (p[2] + 2.0 * w)).abs() <= 1e-15 * (1.0 + p[2].abs()),
|
||||
"point {n}: {p:?} to {q:?}, weight {w}"
|
||||
);
|
||||
}
|
||||
// the pick by the whole delta, its ring by half, the rest not at all
|
||||
assert_eq!(weights[5], 1.0);
|
||||
assert_eq!(weights[0], 0.5);
|
||||
assert_eq!(weights[3 * 4 + 3], 0.0);
|
||||
assert_eq!(after.points[15], net.points[15]);
|
||||
// bad moves
|
||||
assert_eq!(
|
||||
move_net(&s, &picks, [f64::NAN, 0.0, 0.0], f),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
move_net(&s, &Picks::Cvs(vec![(4, 0)]), [0.0; 3], f),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
/// The Coons sheet of four bent cubic Bézier sides: a bicubic Bézier patch.
|
||||
fn bicubic() -> Brep {
|
||||
let side = |points: [[f64; 3]; 4]| Curve::Spline {
|
||||
|
||||
+1
-1
@@ -427,7 +427,7 @@ pub fn record_of(k: &Kernel, c: &Curve, interval: &Extent1d) -> Result<RecordCur
|
||||
|
||||
/// The B-spline a curve is (every kernel line, conic and spline is one, its NURB made on a
|
||||
/// copy where it has none yet); `None` for the curves that are not.
|
||||
fn b_spline_part<'c>(k: &Kernel, c: &'c Curve) -> Result<Option<Cow<'c, BSplineCurve>>> {
|
||||
pub(crate) fn b_spline_part<'c>(k: &Kernel, c: &'c Curve) -> Result<Option<Cow<'c, BSplineCurve>>> {
|
||||
use crate::curve::curve::CurveKind;
|
||||
Ok(Some(match c.kind() {
|
||||
CurveKind::BSpline(b) => Cow::Borrowed(b),
|
||||
|
||||
+1
@@ -16,3 +16,4 @@ pub mod surface_intersector;
|
||||
pub mod surface_silhouette;
|
||||
pub mod surface_tracer;
|
||||
pub mod topology_solver;
|
||||
pub mod views;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! |-------------------------------------------------------|--------------------------------|
|
||||
//! | [`intersect_curves`], [`CurveIntersections`] | `SmApiIntersectCurves` |
|
||||
//! | [`intersect_surfaces`] | `SmApiIntersectSurfaces` |
|
||||
//! | [`intersect_surfaces_within`] | (not upstream) |
|
||||
//! | [`intersect_faces`], [`FaceBodies`], [`Intersections`] | `SmApiIntersectFaces` |
|
||||
//! | [`intersect_breps`], [`BrepOperands`] | `SmApiIntersectBreps` |
|
||||
//! | [`intersect_curve_surface`], [`CurveHits`] | `SmApiIntersectCurveSurface` |
|
||||
@@ -69,6 +70,9 @@ use crate::topology::FaceId;
|
||||
const OP: &str = "intersect::api_intersectors::intersect_brep_with_plane";
|
||||
const OP_CURVES: &str = "intersect::api_intersectors::intersect_curves";
|
||||
const OP_SURFACES: &str = "intersect::api_intersectors::intersect_surfaces";
|
||||
|
||||
/// The phrase of [`intersect_surfaces`]'s failure when the surfaces do not meet at all.
|
||||
pub(crate) const NO_SURFACE_INTERSECTION: &str = "the surfaces do not intersect";
|
||||
const OP_BREPS: &str = "intersect::api_intersectors::intersect_breps";
|
||||
const OP_CURVE_SURFACE: &str = "intersect::api_intersectors::intersect_curve_surface";
|
||||
const OP_CURVE_FACE: &str = "intersect::api_intersectors::intersect_curve_face";
|
||||
@@ -177,6 +181,23 @@ pub fn intersect_surfaces(
|
||||
k: &Kernel,
|
||||
surface1: &Surface,
|
||||
surface2: &Surface,
|
||||
) -> (Vec<Curve>, Result<()>) {
|
||||
intersect_surfaces_within(k, surface1, surface2, None)
|
||||
}
|
||||
|
||||
/// [`intersect_surfaces`] traced to `approx_tol_3d` (`GlobalSurfaceIntersect`'s
|
||||
/// approximation tolerance; `None`: the intersector's default, a thousandth of the size
|
||||
/// of the surfaces' domains, which on large surfaces is far coarser than the kernel's
|
||||
/// tolerances). Not upstream: `SmApiIntersectSurfaces` takes no tolerance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// In the status: those of [`intersect_surfaces`].
|
||||
pub fn intersect_surfaces_within(
|
||||
k: &Kernel,
|
||||
surface1: &Surface,
|
||||
surface2: &Surface,
|
||||
approx_tol_3d: Option<f64>,
|
||||
) -> (Vec<Curve>, Result<()>) {
|
||||
let domains = surface1
|
||||
.natural_uv_domain_with_nurb(k)
|
||||
@@ -191,7 +212,7 @@ pub fn intersect_surfaces(
|
||||
surface2,
|
||||
&domain2,
|
||||
[true, true],
|
||||
None,
|
||||
approx_tol_3d,
|
||||
None,
|
||||
);
|
||||
// check result
|
||||
@@ -199,7 +220,7 @@ pub fn intersect_surfaces(
|
||||
if result.curves_3d.is_empty() {
|
||||
Err(SolidError::intersect_failed(
|
||||
OP_SURFACES,
|
||||
"the surfaces do not intersect",
|
||||
NO_SURFACE_INTERSECTION,
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
//! A curve from two views: the 3D curves seen as one curve along one direction and as another
|
||||
//! along a second, where the two curves' extrusions along their directions meet.
|
||||
//!
|
||||
//! Upstream: none.
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |-------------------------|-----------------|
|
||||
//! | [`curves_from_views`] | (not upstream) |
|
||||
//! | [`views_do_not_meet`] | (not upstream) |
|
||||
//!
|
||||
//! Each curve is extruded along its unit direction both ways from itself, far enough that no
|
||||
//! meeting point is left out, and the two extrusions are intersected
|
||||
//! ([`intersect_surfaces_within`]) at the kernel's default approximation tolerance
|
||||
//! ([`approx_tol3d`] of [`USE_DEFAULT`]: half its zone tolerance). The intersector's own default,
|
||||
//! a thousandth of the surfaces' size, is not taken: these sheets reach far past the curves, so
|
||||
//! at model scale it is a thousand times coarser than the curves need.
|
||||
//!
|
||||
//! How far is a bound, not a search: a point `X` seen on both curves is
|
||||
//! `X = p + s â = q + t b̂` with `p` on the first curve and `q` on the second. Its component
|
||||
//! across `b̂` gives `s (â - (â·b̂) b̂) = (q - p) - ((q - p)·b̂) b̂`, so `|s| sin θ <= |q - p| <= D`,
|
||||
//! with `θ` the angle between the directions and `D` the diagonal of the box around both
|
||||
//! curves' control polygons (a NURBS curve with positive weights lies in the convex hull of its
|
||||
//! control points, so in that box); `|t|` likewise. Each extrusion
|
||||
//! reaches [`REACH_MARGIN`] times `D / sin θ` on either side, so the meeting curves end on the
|
||||
//! curves' own ends, never on an extrusion's far edge.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
#![allow(clippy::neg_cmp_op_on_partial_ord)] // NaN-refusing tests
|
||||
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::from_parameters::b_spline_part;
|
||||
use crate::error::intersect::IntersectFailure;
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::tol::{approx_tol3d, USE_DEFAULT};
|
||||
use crate::geom::vector3d::Vec3;
|
||||
use crate::intersect::api_intersectors::{intersect_surfaces_within, NO_SURFACE_INTERSECTION};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::EFF_ZERO_RAD;
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::crv_basic::Curve as NurbCurve;
|
||||
use crate::nurbs::geom::MinMaxBox;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::sweep::api_surfaces::create_extrude;
|
||||
|
||||
const OP: &str = "intersect::views::curves_from_views";
|
||||
|
||||
/// The phrase of [`curves_from_views`]'s failure when the two views have no point in common
|
||||
/// (read with [`views_do_not_meet`]).
|
||||
pub const NO_MEETING: &str = "the views do not meet";
|
||||
|
||||
/// How many times the bound on a meeting point's distance from its curve each extrusion
|
||||
/// reaches on either side of the curve: past the bound by as much again, so that no meeting
|
||||
/// curve ends on an extrusion's far edge.
|
||||
pub const REACH_MARGIN: f64 = 2.0;
|
||||
|
||||
/// The 3D curves seen as `a` along `a_dir` and as `b` along `b_dir`: where `a` extruded along
|
||||
/// `a_dir` meets `b` extruded along `b_dir`, both ways from each curve (the module docs give
|
||||
/// how far). Each curve is returned as a B-spline, as the intersector traces it to the kernel's
|
||||
/// default approximation tolerance ([`approx_tol3d`] of [`USE_DEFAULT`]) of both extrusions: each
|
||||
/// of its spans checked at interior samples against both (docs/ssi.md, §4.3), not certified.
|
||||
/// The directions need not be
|
||||
/// unit vectors, nor the curves planar: a curve that is not flat across its direction is seen
|
||||
/// as its shadow along it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a direction not finite or shorter than the kernel's zero, directions
|
||||
/// within [`EFF_ZERO_RAD`] of parallel (either way), or curves whose box has no size or one
|
||||
/// not finite; [`crate::SolidError::Failed`] with [`IntersectFailure::Failed`] of
|
||||
/// [`NO_MEETING`] when the extrusions do not meet ([`views_do_not_meet`]); `Unsupported` for a
|
||||
/// meeting curve with no B-spline form; those of the extrusions and of
|
||||
/// [`intersect_surfaces_within`].
|
||||
pub fn curves_from_views(
|
||||
k: &Kernel,
|
||||
a: &NurbCurve,
|
||||
a_dir: Vec3,
|
||||
b: &NurbCurve,
|
||||
b_dir: Vec3,
|
||||
) -> Result<Vec<Curve>> {
|
||||
let finite = |v: Vec3| v.x.is_finite() && v.y.is_finite() && v.z.is_finite();
|
||||
if !finite(a_dir) || !finite(b_dir) {
|
||||
return Err(SolidError::invalid_input(OP, "a direction not finite"));
|
||||
}
|
||||
let (a_unit, b_unit) = (a_dir.unitize()?, b_dir.unitize()?);
|
||||
let sine = a_unit.cross(b_unit).length();
|
||||
if !(sine >= EFF_ZERO_RAD) {
|
||||
return Err(SolidError::invalid_input(OP, "the directions are parallel"));
|
||||
}
|
||||
let span = union(&a.bbox()?, &b.bbox()?).diagonal();
|
||||
if !(span.is_finite() && span > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the curves' box has no size or is not finite",
|
||||
));
|
||||
}
|
||||
let reach = REACH_MARGIN * span / sine;
|
||||
if !reach.is_finite() {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the extrusions' reach is not finite",
|
||||
));
|
||||
}
|
||||
let a_sweep = both_ways(a, a_unit, reach)?;
|
||||
let b_sweep = both_ways(b, b_unit, reach)?;
|
||||
let tol = approx_tol3d(k, USE_DEFAULT);
|
||||
let (curves, status) = intersect_surfaces_within(k, &a_sweep, &b_sweep, Some(tol));
|
||||
match status {
|
||||
Ok(()) => curves.iter().map(|c| spline(k, c)).collect(),
|
||||
Err(e)
|
||||
if curves.is_empty()
|
||||
&& e.intersect_failure()
|
||||
== Some(&IntersectFailure::Failed(NO_SURFACE_INTERSECTION)) =>
|
||||
{
|
||||
Err(SolidError::intersect_failed(OP, NO_MEETING))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `e` is [`curves_from_views`]'s refusal of two views that do not meet.
|
||||
#[must_use]
|
||||
pub fn views_do_not_meet(e: &SolidError) -> bool {
|
||||
e.intersect_failure() == Some(&IntersectFailure::Failed(NO_MEETING))
|
||||
}
|
||||
|
||||
/// The smallest box holding both boxes.
|
||||
fn union(a: &MinMaxBox, b: &MinMaxBox) -> MinMaxBox {
|
||||
MinMaxBox::new(
|
||||
a.xl.min(b.xl),
|
||||
a.xr.max(b.xr),
|
||||
a.yb.min(b.yb),
|
||||
a.yt.max(b.yt),
|
||||
a.zn.min(b.zn),
|
||||
a.zf.max(b.zf),
|
||||
)
|
||||
}
|
||||
|
||||
/// `curve` extruded along the unit vector `unit` from `-reach` to `reach`: a copy moved back by
|
||||
/// `reach`, swept forward by twice it ([`create_extrude`]: linear in u, the curve's parameter
|
||||
/// in v).
|
||||
fn both_ways(curve: &NurbCurve, unit: Vec3, reach: f64) -> Result<Surface> {
|
||||
let mut start = curve.clone();
|
||||
start.translate(unit * -reach);
|
||||
let mut start = BSplineCurve::from_nurb(start, 3, false);
|
||||
Ok(Surface::from(create_extrude(
|
||||
&mut start,
|
||||
unit * (2.0 * reach),
|
||||
)?))
|
||||
}
|
||||
|
||||
/// The meeting curve `c` as a B-spline ([`b_spline_part`]: a traced curve is one already, a
|
||||
/// line made of its two points).
|
||||
fn spline(k: &Kernel, c: &Curve) -> Result<Curve> {
|
||||
let base = b_spline_part(k, c)?.ok_or(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "a meeting curve with no B-spline form",
|
||||
})?;
|
||||
Ok(Curve::from(base.into_owned()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::nurbs::math::{CPoint, NOW};
|
||||
|
||||
/// The cubic Bézier curve on `points`.
|
||||
fn bezier(points: [[f64; 3]; 4]) -> NurbCurve {
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::new(x, y, z, NOW))
|
||||
.collect();
|
||||
NurbCurve::new(pw, 3, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]).unwrap()
|
||||
}
|
||||
|
||||
/// The side view, z = f(x) in the plane y = 0, its control points evenly spaced in x so
|
||||
/// that x = 9 t; its slope |dz/dx| is at most 1 (that of its control polygon).
|
||||
fn side() -> NurbCurve {
|
||||
bezier([
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 0.0, 2.0],
|
||||
[6.0, 0.0, -1.0],
|
||||
[9.0, 0.0, 1.0],
|
||||
])
|
||||
}
|
||||
|
||||
/// The plan view, y = g(x) in the plane z = 0, x = -3 + 15 t (wider than the side view, so
|
||||
/// the curve seen in both ends on the side view's ends); |dy/dx| at most 0.6.
|
||||
fn plan(shift: f64) -> NurbCurve {
|
||||
bezier([
|
||||
[shift - 3.0, 1.0, 0.0],
|
||||
[shift + 2.0, 4.0, 0.0],
|
||||
[shift + 7.0, 2.0, 0.0],
|
||||
[shift + 12.0, 5.0, 0.0],
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_side_and_a_plan_view_meet_in_the_curve_seen_in_both() {
|
||||
let k = Kernel::default();
|
||||
let tol = approx_tol3d(&k, USE_DEFAULT);
|
||||
let (a, b) = (side(), plan(0.0));
|
||||
let curves = curves_from_views(
|
||||
&k,
|
||||
&a,
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
&b,
|
||||
Vec3::new(0.0, 0.0, 3.0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!curves.is_empty());
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for c in &curves {
|
||||
let iv = c.natural_interval();
|
||||
for i in 0..=64 {
|
||||
let s = iv.min() + (iv.max() - iv.min()) * f64::from(i) / 64.0;
|
||||
let x = c.evaluate_point(&k, s).unwrap();
|
||||
assert!(
|
||||
(-tol..=9.0 + tol).contains(&x.x),
|
||||
"past the side view: {x:?}"
|
||||
);
|
||||
(lo, hi) = (lo.min(x.x), hi.max(x.x));
|
||||
// On the side view's extrusion along y: its height there is the side view's.
|
||||
// A gap measured along z is at most the distance times sqrt(1 + slope²),
|
||||
// below 1 + slope, the slope at most 1.
|
||||
let on_a = a.eval((x.x / 9.0).clamp(0.0, 1.0), Side::Left).unwrap();
|
||||
assert!((on_a.z - x.z).abs() <= 2.0 * tol, "{x:?} against {on_a:?}");
|
||||
// On the plan view's extrusion along z, the slope at most 0.6.
|
||||
let on_b = b.eval((x.x + 3.0) / 15.0, Side::Left).unwrap();
|
||||
assert!((on_b.y - x.y).abs() <= 1.6 * tol, "{x:?} against {on_b:?}");
|
||||
}
|
||||
}
|
||||
// The whole of the side view's run is seen.
|
||||
assert!(lo <= tol && hi >= 9.0 - tol, "from {lo} to {hi}");
|
||||
}
|
||||
|
||||
/// A side view in y = 0 (seen along y) and a plan view in z = 0 (seen along z) at model
|
||||
/// scale, about 170 mm across: quadratic Bézier curves, both increasing in x.
|
||||
const SIDE_Q: [[f64; 3]; 3] = [
|
||||
[-92.06693847750134, 0.0, -39.76423812596792],
|
||||
[0.0, 0.0, 18.451591211372346],
|
||||
[73.55298836314594, 0.0, 15.024361734430357],
|
||||
];
|
||||
const PLAN_Q: [[f64; 3]; 3] = [
|
||||
[-16.758544054811637, -82.27159357644084, 0.0],
|
||||
[42.459806193714414, -83.42331426400659, 0.0],
|
||||
[76.37870944288886, -34.86770213654586, 0.0],
|
||||
];
|
||||
|
||||
/// The quadratic Bézier curve on `q`.
|
||||
fn quadratic(q: [[f64; 3]; 3]) -> NurbCurve {
|
||||
let pw = q
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::new(x, y, z, NOW))
|
||||
.collect();
|
||||
NurbCurve::new(pw, 2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap()
|
||||
}
|
||||
|
||||
/// The point of the quadratic Bézier curve `q` at `t`, in Bernstein's form (none of the
|
||||
/// kernel's evaluators).
|
||||
fn bernstein(q: &[[f64; 3]; 3], t: f64) -> [f64; 3] {
|
||||
let (b0, b1, b2) = ((1.0 - t) * (1.0 - t), 2.0 * t * (1.0 - t), t * t);
|
||||
[0, 1, 2].map(|i| b0 * q[0][i] + b1 * q[1][i] + b2 * q[2][i])
|
||||
}
|
||||
|
||||
/// Its derivative at `t`.
|
||||
fn bernstein_d(q: &[[f64; 3]; 3], t: f64) -> [f64; 3] {
|
||||
[0, 1, 2].map(|i| 2.0 * (1.0 - t) * (q[1][i] - q[0][i]) + 2.0 * t * (q[2][i] - q[1][i]))
|
||||
}
|
||||
|
||||
/// The parameter of `q` (increasing in x) where its x is `x`, by bisection on [0, 1].
|
||||
fn at_x(q: &[[f64; 3]; 3], x: f64) -> f64 {
|
||||
let (mut lo, mut hi) = (0.0, 1.0);
|
||||
for _ in 0..100 {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if bernstein(q, m)[0] < x {
|
||||
lo = m;
|
||||
} else {
|
||||
hi = m;
|
||||
}
|
||||
}
|
||||
0.5 * (lo + hi)
|
||||
}
|
||||
|
||||
/// The point at `t` of the B-spline with the full knot vector `knots` and the homogeneous
|
||||
/// control points `pw`, by de Boor's algorithm (none of the kernel's evaluators).
|
||||
fn de_boor(knots: &[f64], pw: &[[f64; 4]], t: f64) -> [f64; 3] {
|
||||
let n = pw.len();
|
||||
let p = knots.len() - n - 1;
|
||||
let k = (p..n).rev().find(|&k| knots[k] <= t).unwrap_or(p);
|
||||
let mut d: Vec<[f64; 4]> = (0..=p).map(|j| pw[j + k - p]).collect();
|
||||
for r in 1..=p {
|
||||
for j in (r..=p).rev() {
|
||||
let (lo, hi) = (knots[j + k - p], knots[j + 1 + k - r]);
|
||||
let alpha = (t - lo) / (hi - lo);
|
||||
d[j] = [0, 1, 2, 3].map(|i| (1.0 - alpha) * d[j - 1][i] + alpha * d[j][i]);
|
||||
}
|
||||
}
|
||||
let h = d[p];
|
||||
[h[0] / h[3], h[1] / h[3], h[2] / h[3]]
|
||||
}
|
||||
|
||||
/// The full knot vector and homogeneous control points of a meeting curve (a B-spline).
|
||||
fn net(c: &Curve) -> (Vec<f64>, Vec<[f64; 4]>) {
|
||||
let crate::curve::curve::CurveKind::BSpline(b) = c.kind() else {
|
||||
panic!("a B-spline: {c:?}")
|
||||
};
|
||||
let can = b.canonical().unwrap();
|
||||
let knots = can
|
||||
.knots
|
||||
.iter()
|
||||
.zip(&can.knot_multiplicities)
|
||||
.flat_map(|(k, m)| std::iter::repeat_n(*k, *m))
|
||||
.collect();
|
||||
let pw = can
|
||||
.control_points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
let w = can.weights.get(i).copied().unwrap_or(1.0);
|
||||
[p.x * w, p.y * w, p.z * w, w]
|
||||
})
|
||||
.collect();
|
||||
(knots, pw)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_at_model_scale_meet_within_the_tolerance() {
|
||||
// The intersector's own default tolerance, a thousandth of these sheets' size, left
|
||||
// this curve 5.75e-3 off its views; it is traced to the kernel's tolerance.
|
||||
let k = Kernel::default();
|
||||
let tol = approx_tol3d(&k, USE_DEFAULT);
|
||||
let (a, b) = (quadratic(SIDE_Q), quadratic(PLAN_Q));
|
||||
let curves = curves_from_views(&k, &a, Vec3::Y, &b, Vec3::Z).unwrap();
|
||||
assert!(!curves.is_empty());
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for c in &curves {
|
||||
let (knots, pw) = net(c);
|
||||
let p = knots.len() - pw.len() - 1;
|
||||
let (t0, t1) = (knots[p], knots[pw.len()]);
|
||||
for i in 0..=512 {
|
||||
let x = de_boor(&knots, &pw, t0 + (t1 - t0) * f64::from(i) / 512.0);
|
||||
(lo, hi) = (lo.min(x[0]), hi.max(x[0]));
|
||||
// A gap along the view's other axis is at most the distance times
|
||||
// sqrt(1 + slope²), below 1 + slope; twice the tolerance, as the intersector
|
||||
// checks its spans at interior samples.
|
||||
let s = at_x(&SIDE_Q, x[0]);
|
||||
let (q, d) = (bernstein(&SIDE_Q, s), bernstein_d(&SIDE_Q, s));
|
||||
let allowed = 2.0 * tol * (1.0 + (d[2] / d[0]).abs());
|
||||
assert!(
|
||||
(q[2] - x[2]).abs() <= allowed,
|
||||
"off the side view: {x:?} against {q:?}"
|
||||
);
|
||||
let s = at_x(&PLAN_Q, x[0]);
|
||||
let (q, d) = (bernstein(&PLAN_Q, s), bernstein_d(&PLAN_Q, s));
|
||||
let allowed = 2.0 * tol * (1.0 + (d[1] / d[0]).abs());
|
||||
assert!(
|
||||
(q[1] - x[1]).abs() <= allowed,
|
||||
"off the plan view: {x:?} against {q:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// The overlap of the two runs is seen: from the plan view's start to the side view's end.
|
||||
assert!(
|
||||
(lo - PLAN_Q[0][0]).abs() <= 2.0 * tol && (hi - SIDE_Q[2][0]).abs() <= 2.0 * tol,
|
||||
"from {lo} to {hi}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_or_zero_directions_are_refused() {
|
||||
let k = Kernel::default();
|
||||
let (a, b) = (side(), plan(0.0));
|
||||
let y = Vec3::new(0.0, 1.0, 0.0);
|
||||
for (da, db) in [
|
||||
(y, Vec3::new(0.0, -2.0, 0.0)),
|
||||
(y, y),
|
||||
(Vec3::ZERO, Vec3::Z),
|
||||
(y, Vec3::new(f64::NAN, 0.0, 1.0)),
|
||||
] {
|
||||
let e = curves_from_views(&k, &a, da, &b, db).unwrap_err();
|
||||
assert!(matches!(e, SolidError::InvalidInput { .. }), "{e:?}");
|
||||
assert!(!views_do_not_meet(&e));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_whose_runs_do_not_overlap_do_not_meet() {
|
||||
let k = Kernel::default();
|
||||
// The plan view moved to x in [20, 35], past the side view's [0, 9].
|
||||
let e = curves_from_views(&k, &side(), Vec3::Y, &plan(23.0), Vec3::Z).unwrap_err();
|
||||
assert!(views_do_not_meet(&e), "{e:?}");
|
||||
}
|
||||
}
|
||||
+187
@@ -13,6 +13,7 @@
|
||||
//! | [`Transform::concat`] | `SmTransform::ConcatTransform` / `smgu_4x4Mult`|
|
||||
//! | [`Transform::transform_point`] | `SmTransform::TransformPoint` |
|
||||
//! | [`Transform::transform_vector`] | `SmTransform::TransformVector` |
|
||||
//! | [`Transform::rotation`], [`Transform::uniform_scaling`], [`Transform::reflection`] | (not upstream) |
|
||||
//! | [`EFF_ZERO`] etc. | `SM_EFF_ZERO` etc. |
|
||||
//! | [`is_infinite_param`] | `SM_IS_INFINITE` |
|
||||
//! | [`max2`], [`min2`], [`max3`], [`min3`], [`max4`], [`min4`] | `smos_Max`, `smos_Min`, `smos_3Max`, `smos_3Min`, `smos_4Max`, `smos_4Min` |
|
||||
@@ -409,10 +410,196 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
/// The operation name of the refusals of the maps built from their numbers.
|
||||
const OP_MAPS: &str = "math::Transform";
|
||||
|
||||
/// Whether every coordinate of `v` is finite.
|
||||
fn finite3(v: Vec3) -> bool {
|
||||
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
|
||||
}
|
||||
|
||||
/// Not upstream: the rigid motions and scalings an application asks for by their numbers
|
||||
/// (a turn about a line, a scale about a point, a mirror in a plane), built here so the
|
||||
/// layers above the kernel only pass the numbers on. Each is in the row-vector convention
|
||||
/// of [`Transform`]: rows 0 to 2 the images of x, y and z, row 3 the translation.
|
||||
impl Transform {
|
||||
/// The linear map whose rows are `x`, `y` and `z`, about the point `fixed`, which it
|
||||
/// keeps: `p' = (p - fixed) A + fixed`, its translation `fixed - fixed A`.
|
||||
fn about(fixed: Point3, x: Vec3, y: Vec3, z: Vec3) -> Self {
|
||||
let image = x * fixed.x + y * fixed.y + z * fixed.z;
|
||||
Self::from_axes(x, y, z, fixed - image)
|
||||
}
|
||||
|
||||
/// The turn by `angle` radians, right-handed about `axis`, about the line through
|
||||
/// `point` along `axis` (any length, made unit here): Rodrigues' rotation formula,
|
||||
/// `R = c I + s [k]x + (1 - c) k kᵀ` with `k` the unit axis, `c` and `s` the cosine and
|
||||
/// sine of the angle, transposed for the row-vector convention.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a point, axis or angle not finite, or an axis whose squared
|
||||
/// length is below [`EFF_ZERO_SQ`].
|
||||
pub fn rotation(point: Point3, axis: Vec3, angle: f64) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(point) && finite3(axis) && angle.is_finite()) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a turn's numbers not finite",
|
||||
));
|
||||
}
|
||||
let k = axis
|
||||
.unitize()
|
||||
.map_err(|_| SolidError::invalid_input(OP_MAPS, "a turn's axis of no length"))?;
|
||||
let (s, c) = angle.sin_cos();
|
||||
let v = 1.0 - c;
|
||||
let (x, y, z) = (k.x, k.y, k.z);
|
||||
Ok(Self::about(
|
||||
point,
|
||||
Vec3::new(c + x * x * v, x * y * v + z * s, x * z * v - y * s),
|
||||
Vec3::new(x * y * v - z * s, c + y * y * v, y * z * v + x * s),
|
||||
Vec3::new(x * z * v + y * s, y * z * v - x * s, c + z * z * v),
|
||||
))
|
||||
}
|
||||
|
||||
/// The scale by `factor` in every direction about `centre`, which it keeps. A negative
|
||||
/// factor turns the scale through the centre as well.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a centre or factor not finite, or a factor of zero (a map with
|
||||
/// no inverse).
|
||||
pub fn uniform_scaling(centre: Point3, factor: f64) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(centre) && factor.is_finite()) || factor == 0.0 {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a scale's numbers not finite, or a factor of zero",
|
||||
));
|
||||
}
|
||||
Ok(Self::about(
|
||||
centre,
|
||||
Vec3::new(factor, 0.0, 0.0),
|
||||
Vec3::new(0.0, factor, 0.0),
|
||||
Vec3::new(0.0, 0.0, factor),
|
||||
))
|
||||
}
|
||||
|
||||
/// The mirror in the plane through `origin` square to `normal` (any length, made unit
|
||||
/// here): `p' = p - 2 ((p - origin)·n) n`, its linear part `I - 2 n nᵀ` (symmetric, so
|
||||
/// the same in either convention) and its translation `2 (origin·n) n`. For a normal
|
||||
/// along an axis every entry is exact, so the mirror twice is the identity exactly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for an origin or normal not finite, or a normal whose squared length
|
||||
/// is below [`EFF_ZERO_SQ`].
|
||||
pub fn reflection(origin: Point3, normal: Vec3) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(origin) && finite3(normal)) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a mirror's numbers not finite",
|
||||
));
|
||||
}
|
||||
let n = normal
|
||||
.unitize()
|
||||
.map_err(|_| SolidError::invalid_input(OP_MAPS, "a mirror's normal of no length"))?;
|
||||
let row = |e: Vec3, ni: f64| e - n * (2.0 * ni);
|
||||
Ok(Self::from_axes(
|
||||
row(Vec3::X, n.x),
|
||||
row(Vec3::Y, n.y),
|
||||
row(Vec3::Z, n.z),
|
||||
n * (2.0 * origin.dot(n)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The largest difference between two matrices' entries.
|
||||
fn apart(a: &Transform, b: &Transform) -> f64 {
|
||||
a.m.iter()
|
||||
.flatten()
|
||||
.zip(b.m.iter().flatten())
|
||||
.map(|(x, y)| (x - y).abs())
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quarter_turn_about_z_takes_x_to_y_and_keeps_its_line() {
|
||||
use crate::geom::certified::Verdict;
|
||||
let q = core::f64::consts::FRAC_PI_2;
|
||||
let turn = Transform::rotation(Point3::ZERO, Vec3::new(0.0, 0.0, 2.0), q).unwrap();
|
||||
let want = Transform::from_axes(Vec3::Y, -Vec3::X, Vec3::Z, Point3::ZERO);
|
||||
assert!(apart(&turn, &want) <= 1e-15, "{turn:?}");
|
||||
// about the line through (10, 0, 0) along z: that point stays, (11, 0, 0) goes to
|
||||
// (10, 1, 0)
|
||||
let p = Point3::new(10.0, 0.0, 0.0);
|
||||
let turn = Transform::rotation(p, Vec3::Z, q).unwrap();
|
||||
assert!(turn.transform_point(p).distance(p) <= 1e-14);
|
||||
let image = turn.transform_point(Point3::new(11.0, 0.0, 0.0));
|
||||
assert!(
|
||||
image.distance(Point3::new(10.0, 1.0, 0.0)) <= 1e-14,
|
||||
"{image:?}"
|
||||
);
|
||||
// a turn about a skew axis keeps lengths and its axis
|
||||
let axis = Vec3::new(1.0, 2.0, 2.0);
|
||||
let turn = Transform::rotation(p, axis, 0.7).unwrap();
|
||||
assert!(
|
||||
turn.transform_point(p + axis * 3.0)
|
||||
.distance(p + axis * 3.0)
|
||||
<= 1e-12
|
||||
);
|
||||
for r in turn.row_lengths() {
|
||||
assert!((r - 1.0).abs() <= 1e-14);
|
||||
}
|
||||
assert_eq!(turn.is_regular(1e-9), Verdict::Yes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scale_keeps_its_centre_and_a_mirror_twice_is_the_identity() {
|
||||
let c = Point3::new(1.0, -2.0, 3.0);
|
||||
let scale = Transform::uniform_scaling(c, 2.5).unwrap();
|
||||
assert!(scale.transform_point(c).distance(c) <= 1e-14);
|
||||
let p = scale.transform_point(c + Vec3::X);
|
||||
assert!(p.distance(c + Vec3::X * 2.5) <= 1e-14, "{p:?}");
|
||||
// the mirror in y = 5, an axis normal of any length: every entry exact
|
||||
let mirror =
|
||||
Transform::reflection(Point3::new(7.0, 5.0, -1.0), Vec3::new(0.0, 3.0, 0.0)).unwrap();
|
||||
assert_eq!(
|
||||
mirror.transform_point(Point3::new(1.0, 2.0, 3.0)),
|
||||
Point3::new(1.0, 8.0, 3.0)
|
||||
);
|
||||
assert_eq!(mirror.concat(&mirror), Transform::IDENTITY);
|
||||
// a slanted mirror keeps its plane's points and reverses its normal
|
||||
let n = Vec3::new(1.0, 1.0, 0.0);
|
||||
let mirror = Transform::reflection(c, n).unwrap();
|
||||
assert!(mirror.transform_point(c).distance(c) <= 1e-14);
|
||||
let back = mirror.transform_point(c + n);
|
||||
assert!(back.distance(c - n) <= 1e-14, "{back:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_from_numbers_refuse_what_is_not_finite_or_has_no_size() {
|
||||
let bad = |r: crate::error::Result<Transform>| matches!(r, Err(crate::error::SolidError::InvalidInput { operation, .. }) if operation == OP_MAPS);
|
||||
assert!(bad(Transform::rotation(Point3::ZERO, Vec3::ZERO, 1.0)));
|
||||
assert!(bad(Transform::rotation(Point3::ZERO, Vec3::Z, f64::NAN)));
|
||||
assert!(bad(Transform::rotation(
|
||||
Point3::new(f64::INFINITY, 0.0, 0.0),
|
||||
Vec3::Z,
|
||||
1.0
|
||||
)));
|
||||
assert!(bad(Transform::uniform_scaling(Point3::ZERO, 0.0)));
|
||||
assert!(bad(Transform::uniform_scaling(Point3::ZERO, f64::NAN)));
|
||||
assert!(bad(Transform::reflection(Point3::ZERO, Vec3::ZERO)));
|
||||
assert!(bad(Transform::reflection(
|
||||
Point3::ZERO,
|
||||
Vec3::new(f64::NAN, 1.0, 0.0)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rigid_motion_is_regular_and_a_flattening_is_not() {
|
||||
use crate::geom::certified::Verdict;
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ pub mod iges;
|
||||
pub(crate) mod index;
|
||||
pub mod knots;
|
||||
pub mod math;
|
||||
pub mod n_sided;
|
||||
pub mod nurbs_crv;
|
||||
pub mod nurbs_srf;
|
||||
pub mod spiral;
|
||||
|
||||
+5
-5
@@ -119,7 +119,7 @@ fn bad(reason: &'static str) -> SolidError {
|
||||
/// equal is the polynomial curve on its points divided by that weight, exactly (a record
|
||||
/// spline's kernel curve carries a weight of 1 on every point). Only unequal weights make it
|
||||
/// rational.
|
||||
fn nurb_of(c: &BSplineCurve) -> Result<Curve> {
|
||||
pub(crate) fn nurb_of(c: &BSplineCurve) -> Result<Curve> {
|
||||
let mut nurb = c
|
||||
.nurb
|
||||
.clone()
|
||||
@@ -182,7 +182,7 @@ fn surface(net: Vec<Vec<Vec3>>, p: usize, q: usize, u: &[f64], v: &[f64]) -> Res
|
||||
}
|
||||
|
||||
/// `c` moved by minus `o`, into the network's own frame.
|
||||
fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
pub(crate) fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
let pw = c
|
||||
.cpts()
|
||||
.iter()
|
||||
@@ -192,7 +192,7 @@ fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
}
|
||||
|
||||
/// `s` moved by minus `o`, into the network's own frame.
|
||||
fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
pub(crate) fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
let net = s
|
||||
.rows()
|
||||
.map(|row| {
|
||||
@@ -206,7 +206,7 @@ fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
}
|
||||
|
||||
/// The largest coordinate of `points`.
|
||||
fn largest<'a>(points: impl Iterator<Item = &'a CPoint>) -> f64 {
|
||||
pub(crate) fn largest<'a>(points: impl Iterator<Item = &'a CPoint>) -> f64 {
|
||||
points
|
||||
.map(|c| c.x.abs().max(c.y.abs()).max(c.z.abs()))
|
||||
.fold(0.0_f64, f64::max)
|
||||
@@ -226,7 +226,7 @@ fn room<T>(count: usize) -> Result<Vec<T>> {
|
||||
}
|
||||
|
||||
/// Whether `x` is at most `tol`; a NaN is not.
|
||||
fn within(x: f64, tol: f64) -> bool {
|
||||
pub(crate) fn within(x: f64, tol: f64) -> bool {
|
||||
matches!(x.partial_cmp(&tol), Some(Ordering::Less | Ordering::Equal))
|
||||
}
|
||||
|
||||
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
//! An n-sided patch bounded by curves meeting end to end round one loop, certified: the kernel's
|
||||
//! fill (`N_FillNSidedHole`, [`BSplineSurface::create_n_sided_patch`]) splits each boundary
|
||||
//! curve at its middle and fills the hole with N four-sided patches round a centre, each bounded
|
||||
//! by halves of two boundary curves and two inner curves from their middles to the centre.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native). The fill is upstream's; the certificate is
|
||||
//! this module's.
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |---------------|----------|
|
||||
//! | [`n_sided`] | none |
|
||||
//!
|
||||
//! What is certified, and what is not:
|
||||
//! - **The boundary**: each half of each boundary curve against the patch side it is (the side
|
||||
//! whose certified distance to it is least, either way round, both on `[0, 1]`):
|
||||
//! [`certified_distance`], plus the rounding of extracting the side and splitting the curve by
|
||||
//! knot insertion (`γ_(12 (d + 1))` of twice the largest coordinate each, a coordinate error
|
||||
//! `e` moving a point by at most `2 e`), counted in the loop's own frame (minus its first
|
||||
//! control point, each moved coordinate rounding once). The largest is returned.
|
||||
//! - **The inner joins (G0)**: every patch side that is no boundary half against the nearest
|
||||
//! side of another patch, certified the same way; the largest must be within the tolerance.
|
||||
//! - **Tangency across the inner curves (G1)** is the fill's aim, within its angle tolerance,
|
||||
//! and is **not** certified here.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). Non-rational curves only (a curve whose weights
|
||||
//! are all one value is its polynomial form). No recursion.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::crv_basic::Curve;
|
||||
use crate::nurbs::crv_deviation::certified_distance;
|
||||
use crate::nurbs::geom::Interval;
|
||||
use crate::nurbs::gordon::{largest, moved, moved_surface, nurb_of, within};
|
||||
use crate::nurbs::math::gamma;
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
|
||||
const OP: &str = "nurbs::n_sided::n_sided";
|
||||
|
||||
/// The fewest curves of an n-sided loop.
|
||||
pub const MIN_SIDES: usize = 3;
|
||||
/// The most curves of an n-sided loop.
|
||||
pub const MAX_SIDES: usize = 64;
|
||||
|
||||
/// The angle (degrees) within which the fill matches the cross-boundary derivatives it builds
|
||||
/// across its inner curves: the fill's own tests' value.
|
||||
const ANGLE_DEG: f64 = 1.0;
|
||||
|
||||
/// The N patches of an n-sided fill and its certificates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NSided {
|
||||
/// The patches, one per boundary curve.
|
||||
pub patches: Vec<BSplineSurface>,
|
||||
/// The largest certified distance between a boundary curve's half and its patch side.
|
||||
pub bound: f64,
|
||||
/// The largest certified distance between the two patch sides of an inner join.
|
||||
pub joins: f64,
|
||||
}
|
||||
|
||||
/// Why a loop has no certified n-sided patch here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum NSidedRefusal {
|
||||
/// A curve is rational (its weights not all one value): the fill takes polynomial curves.
|
||||
Rational,
|
||||
/// A boundary curve is not proved within the tolerance of the patches: the bound.
|
||||
PastTolerance {
|
||||
/// The largest certified distance.
|
||||
bound: f64,
|
||||
},
|
||||
/// Two patches are not proved to meet within the tolerance along an inner curve: the
|
||||
/// largest certified gap.
|
||||
Joins {
|
||||
/// The largest certified gap between two patches' sides.
|
||||
gap: f64,
|
||||
},
|
||||
}
|
||||
|
||||
fn bad(reason: &'static str) -> SolidError {
|
||||
SolidError::invalid_input(OP, reason)
|
||||
}
|
||||
|
||||
/// `c` on `[0, 1]`, as it runs or reversed.
|
||||
fn on_unit(c: &Curve, reversed: bool) -> Result<Curve> {
|
||||
let mut c = c.clone();
|
||||
if reversed {
|
||||
c.reverse()?;
|
||||
}
|
||||
c.reparam_to_interval(Interval::new(0.0, 1.0))?;
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// The four sides of patch `s`, each on `[0, 1]`.
|
||||
fn sides(s: &Surface) -> Result<[Curve; 4]> {
|
||||
let b = s.parameter_bounds();
|
||||
Ok([
|
||||
on_unit(&extract_iso_crv(s, b.vb, Direction::U)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.vt, Direction::U)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.ul, Direction::V)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.ur, Direction::V)?, false)?,
|
||||
])
|
||||
}
|
||||
|
||||
/// The least certified distance from `c` (on `[0, 1]`) to `side` either way round.
|
||||
fn nearest(c: &Curve, side: &Curve) -> Result<f64> {
|
||||
let forward = certified_distance(c, side)?;
|
||||
let backward = certified_distance(c, &on_unit(side, true)?)?;
|
||||
Ok(forward.min(backward))
|
||||
}
|
||||
|
||||
/// The n-sided fill of `curves` (each running on from the one before, the last back to the
|
||||
/// first), its boundary and inner joins certified within `tol` (module docs).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for fewer than [`MIN_SIDES`] or more than [`MAX_SIDES`] curves, a curve
|
||||
/// without its NURB or `tol` not finite and positive; those of the fill (curves that do not
|
||||
/// meet within its model tolerance, ...), the extraction, the split and the certificates.
|
||||
pub fn n_sided(
|
||||
curves: &[BSplineCurve],
|
||||
tol: f64,
|
||||
) -> Result<std::result::Result<NSided, NSidedRefusal>> {
|
||||
if !(MIN_SIDES..=MAX_SIDES).contains(&curves.len()) {
|
||||
return Err(bad("fewer than three or more than 64 curves"));
|
||||
}
|
||||
if !(tol.is_finite() && tol > 0.0) {
|
||||
return Err(bad("a tolerance not finite and positive"));
|
||||
}
|
||||
let nurbs: Vec<Curve> = curves.iter().map(nurb_of).collect::<Result<_>>()?;
|
||||
if nurbs.iter().any(Curve::is_rational) {
|
||||
return Ok(Err(NSidedRefusal::Rational));
|
||||
}
|
||||
|
||||
// the fill, on copies of the polynomial curves (it changes its inputs)
|
||||
let mut bounding: Vec<BSplineCurve> = nurbs
|
||||
.iter()
|
||||
.map(|c| BSplineCurve::from_gw_nurb(3, c))
|
||||
.collect();
|
||||
let mut across: Vec<Option<BSplineCurve>> = bounding.iter().map(|_| None).collect();
|
||||
let patches =
|
||||
BSplineSurface::create_n_sided_patch(&mut bounding, &mut across, ANGLE_DEG, None)?;
|
||||
|
||||
// the loop's own frame
|
||||
let origin = nurbs
|
||||
.first()
|
||||
.and_then(|c| c.cpts().first().copied())
|
||||
.ok_or_else(|| bad("a curve without points"))?;
|
||||
let curves_local: Vec<Curve> = nurbs
|
||||
.iter()
|
||||
.map(|c| moved(c, origin))
|
||||
.collect::<Result<_>>()?;
|
||||
let mut local: Vec<Surface> = Vec::new();
|
||||
for p in &patches {
|
||||
let s = p
|
||||
.nurb
|
||||
.as_ref()
|
||||
.ok_or_else(|| bad("a patch without its NURB"))?;
|
||||
local.push(moved_surface(s, origin)?);
|
||||
}
|
||||
let size = largest(
|
||||
curves_local
|
||||
.iter()
|
||||
.flat_map(Curve::cpts)
|
||||
.chain(local.iter().flat_map(Surface::rows).flatten()),
|
||||
);
|
||||
let degree = curves_local
|
||||
.iter()
|
||||
.map(Curve::degree)
|
||||
.chain(local.iter().map(|s| {
|
||||
let (p, q) = s.degrees();
|
||||
p.max(q)
|
||||
}))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
// splitting a curve and extracting a side (knot insertion each), and the two moves
|
||||
let rounding = 2.0 * (4.0 * gamma(12 * (degree + 1)) * size) + 2.0 * (2.0 * gamma(1) * size);
|
||||
|
||||
let all_sides: Vec<[Curve; 4]> = local.iter().map(sides).collect::<Result<_>>()?;
|
||||
// which patch sides are boundary halves
|
||||
let mut boundary_side = vec![[false; 4]; all_sides.len()];
|
||||
let mut bound = 0.0_f64;
|
||||
for c in &curves_local {
|
||||
let (a, b) = c.param_bounds();
|
||||
let (first, second) = c.split(0.5 * (a + b))?;
|
||||
for half in [first, second] {
|
||||
let half = on_unit(&half, false)?;
|
||||
let mut best: Option<(f64, usize, usize)> = None;
|
||||
for (i, four) in all_sides.iter().enumerate() {
|
||||
for (j, side) in four.iter().enumerate() {
|
||||
let d = nearest(&half, side)?;
|
||||
if best.is_none_or(|(b, _, _)| d < b) {
|
||||
best = Some((d, i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((d, i, j)) = best else {
|
||||
return Err(bad("a fill without patches"));
|
||||
};
|
||||
if let Some(flag) = boundary_side.get_mut(i).and_then(|f| f.get_mut(j)) {
|
||||
*flag = true;
|
||||
}
|
||||
bound = bound.max((d + rounding) * (1.0 + gamma(3)));
|
||||
}
|
||||
}
|
||||
if !within(bound, tol) {
|
||||
return Ok(Err(NSidedRefusal::PastTolerance { bound }));
|
||||
}
|
||||
|
||||
// the inner joins: each side that is no boundary half against the nearest side of another
|
||||
// patch
|
||||
let mut joins = 0.0_f64;
|
||||
for (i, four) in all_sides.iter().enumerate() {
|
||||
for (j, side) in four.iter().enumerate() {
|
||||
if boundary_side
|
||||
.get(i)
|
||||
.and_then(|f| f.get(j))
|
||||
.copied()
|
||||
.unwrap_or(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut best = f64::INFINITY;
|
||||
for (other, theirs) in all_sides.iter().enumerate() {
|
||||
if other == i {
|
||||
continue;
|
||||
}
|
||||
for s in theirs {
|
||||
best = best.min(nearest(side, s)?);
|
||||
}
|
||||
}
|
||||
joins = joins.max((best + rounding) * (1.0 + gamma(3)));
|
||||
}
|
||||
}
|
||||
if !within(joins, tol) {
|
||||
return Ok(Err(NSidedRefusal::Joins { gap: joins }));
|
||||
}
|
||||
Ok(Ok(NSided {
|
||||
patches,
|
||||
bound,
|
||||
joins,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::math::CPoint;
|
||||
|
||||
/// The polynomial curve of degree `p` on `points` with clamped uniform knots.
|
||||
fn spline(points: &[[f64; 3]], p: usize) -> BSplineCurve {
|
||||
let n = points.len();
|
||||
let mut knots = vec![0.0; p + 1];
|
||||
for i in 1..n - p {
|
||||
knots.push(i as f64 / (n - p) as f64);
|
||||
}
|
||||
knots.extend(vec![1.0; p + 1]);
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|q| CPoint::from_point(Vec3::new(q[0], q[1], q[2])))
|
||||
.collect();
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, p, knots).unwrap())
|
||||
}
|
||||
|
||||
/// The corners of a regular pentagon of radius 10 in z = 0.
|
||||
fn pentagon() -> Vec<[f64; 3]> {
|
||||
(0..5)
|
||||
.map(|i| {
|
||||
let a = std::f64::consts::TAU * f64::from(i) / 5.0;
|
||||
[10.0 * a.cos(), 10.0 * a.sin(), 0.0]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pentagon_of_lines_is_filled_flat() {
|
||||
let p = pentagon();
|
||||
let lines: Vec<BSplineCurve> = (0..5).map(|i| spline(&[p[i], p[(i + 1) % 5]], 1)).collect();
|
||||
let f = n_sided(&lines, 1e-6).unwrap().unwrap();
|
||||
assert_eq!(f.patches.len(), 5);
|
||||
assert!(
|
||||
f.bound <= 1e-9 && f.joins <= 1e-9,
|
||||
"{} {}",
|
||||
f.bound,
|
||||
f.joins
|
||||
);
|
||||
// planar data, planar patches: every control point in z = 0
|
||||
for s in &f.patches {
|
||||
for c in s.nurb.as_ref().unwrap().rows().flatten() {
|
||||
assert!(c.z.abs() < 1e-9, "{c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rational_curves_are_refused() {
|
||||
let p = pentagon();
|
||||
let mut curves: Vec<BSplineCurve> =
|
||||
(0..5).map(|i| spline(&[p[i], p[(i + 1) % 5]], 1)).collect();
|
||||
let pw = vec![
|
||||
CPoint::new(p[0][0], p[0][1], 0.0, 1.0),
|
||||
CPoint::new(2.0 * p[1][0], 2.0 * p[1][1], 0.0, 2.0),
|
||||
];
|
||||
curves[0] =
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, 1, vec![0.0, 0.0, 1.0, 1.0]).unwrap());
|
||||
assert_eq!(
|
||||
n_sided(&curves, 1e-6).unwrap().unwrap_err(),
|
||||
NSidedRefusal::Rational
|
||||
);
|
||||
}
|
||||
}
|
||||
+71
-1
@@ -42,6 +42,7 @@
|
||||
//! | [`Surface::average_len`], [`AverageLen`] | `N_SrfGetAverageLen` |
|
||||
//! | [`Direction`] | `NL_UDIR` / `NL_VDIR` / `NL_UVDIR` |
|
||||
//! | [`WEIGHT_MIN`], [`WEIGHT_MAX`], [`WEIGHT_TOL`] | `NL_WMIN`, `NL_WMAX`, `NL_WTOL` |
|
||||
//! | [`Surface::displace_cpts`] | none |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - [`Surface`] owns its control net and knot arrays (upstream points at caller- or
|
||||
@@ -92,7 +93,7 @@ use crate::nurbs::geom::{
|
||||
dist_pt_pt, dist_signed_pt_plane, plane_pt_normal_to_implicit, Rectangle,
|
||||
};
|
||||
use crate::nurbs::knots::{KnotVector, MAX_DEGREE};
|
||||
use crate::nurbs::math::{combine2_pts, combine4_pts, CPoint, MODEL_TOL, NOW};
|
||||
use crate::nurbs::math::{combine2_pts, combine4_pts, CPoint, MODEL_TOL, NOW, NOZ};
|
||||
|
||||
/// Minimum valid weight (`NL_WMIN`).
|
||||
pub const WEIGHT_MIN: f64 = 1.0e-03;
|
||||
@@ -107,6 +108,7 @@ const OP_NEW: &str = "nurbs::srf_basic::Surface::new";
|
||||
const MIXED_RATIONALITY: &str = "control points mix rational and non-rational";
|
||||
const OP_WITH_INDICES: &str = "nurbs::srf_basic::Surface::with_indices";
|
||||
const OP_CPT: &str = "nurbs::srf_basic::Surface::cpt";
|
||||
const OP_DISPLACE: &str = "nurbs::srf_basic::Surface::displace_cpts";
|
||||
const OP_SET_CPTS: &str = "nurbs::srf_basic::Surface::set_cpts_and_knots";
|
||||
const OP_SET_SIZE: &str = "nurbs::srf_basic::Surface::set_size_indices";
|
||||
const OP_COUNTS: &str = "nurbs::srf_basic::Surface::counts_are_valid";
|
||||
@@ -449,6 +451,45 @@ impl Surface {
|
||||
self.rows().map(<[CPoint]>::to_vec).collect()
|
||||
}
|
||||
|
||||
/// Moves control points by fractions of one displacement (not upstream): each
|
||||
/// `((i, j), t)` of `moves` moves the Euclidean point of `Pw[i][j]` by `t * delta`, its
|
||||
/// weight kept (a rational point's weighted coordinates move by `w t delta`). A point
|
||||
/// listed twice moves by the sum of its fractions. All or nothing: on a refusal the net
|
||||
/// is unchanged.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for an index past the net, a fraction or `delta` not finite, a 2D net
|
||||
/// (no `z`), or a moved point not finite.
|
||||
pub fn displace_cpts(&mut self, moves: &[((usize, usize), f64)], delta: Vec3) -> Result<()> {
|
||||
if !(delta.x.is_finite() && delta.y.is_finite() && delta.z.is_finite()) {
|
||||
return Err(invalid(OP_DISPLACE, "a displacement not finite"));
|
||||
}
|
||||
let mut next = self.clone();
|
||||
for &((i, j), t) in moves {
|
||||
if !t.is_finite() {
|
||||
return Err(invalid(OP_DISPLACE, "a fraction not finite"));
|
||||
}
|
||||
let c = next.cpt_mut(i, j)?;
|
||||
if c.z == NOZ {
|
||||
return Err(invalid(OP_DISPLACE, "a 2D control net"));
|
||||
}
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
let moved = CPoint {
|
||||
x: c.x + w * t * delta.x,
|
||||
y: c.y + w * t * delta.y,
|
||||
z: c.z + w * t * delta.z,
|
||||
w: c.w,
|
||||
};
|
||||
if !(moved.x.is_finite() && moved.y.is_finite() && moved.z.is_finite()) {
|
||||
return Err(invalid(OP_DISPLACE, "a moved point not finite"));
|
||||
}
|
||||
*c = moved;
|
||||
}
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The u knots `U[0..=r]` (`N_SrfGetKnots`).
|
||||
#[must_use]
|
||||
pub fn u_knots(&self) -> &[f64] {
|
||||
@@ -1223,6 +1264,35 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn displaced_points_move_by_their_fraction_of_the_delta() {
|
||||
let mut s = bilinear();
|
||||
let d = Vec3::new(0.0, 0.0, 2.0);
|
||||
// a point listed twice moves by the sum of its fractions
|
||||
s.displace_cpts(&[((1, 1), 0.5), ((0, 0), 1.0), ((1, 1), 0.25)], d)
|
||||
.unwrap();
|
||||
assert_eq!(s.cpt(0, 0).unwrap(), CPoint::new(0.0, 0.0, 2.0, NOW));
|
||||
assert_eq!(s.cpt(1, 1).unwrap(), CPoint::new(1.0, 1.0, 1.5, NOW));
|
||||
assert_eq!(s.cpt(0, 1).unwrap(), CPoint::new(0.0, 1.0, 0.0, NOW));
|
||||
assert_eq!(s.cpt(1, 0).unwrap(), CPoint::new(1.0, 0.0, 0.0, NOW));
|
||||
// a rational point keeps its weight; its weighted coordinates move by w t delta
|
||||
let mut r = bilinear();
|
||||
r.non_rat_to_rat();
|
||||
*r.cpt_mut(1, 0).unwrap() = CPoint::new(2.0, 0.0, 0.0, 2.0);
|
||||
r.displace_cpts(&[((1, 0), 1.0)], d).unwrap();
|
||||
assert_eq!(r.cpt(1, 0).unwrap(), CPoint::new(2.0, 0.0, 4.0, 2.0));
|
||||
// all or nothing: a bad move after a good one leaves the net as it was
|
||||
let before = bilinear();
|
||||
let mut t = before.clone();
|
||||
assert!(t.displace_cpts(&[((0, 0), 1.0), ((2, 0), 1.0)], d).is_err());
|
||||
assert!(t.displace_cpts(&[((0, 0), f64::NAN)], d).is_err());
|
||||
assert!(t
|
||||
.displace_cpts(&[((0, 0), 1.0)], Vec3::new(f64::INFINITY, 0.0, 0.0))
|
||||
.is_err());
|
||||
assert!(t.displace_cpts(&[((0, 0), f64::MAX)], d).is_err());
|
||||
assert_eq!(t, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_and_access() {
|
||||
let s = bilinear();
|
||||
|
||||
+37
-8
@@ -160,7 +160,7 @@ pub struct RailedLoft {
|
||||
/// A section curve as plain numbers: degree, full knots, Euclidean control points, weights
|
||||
/// (`None` for a polynomial curve).
|
||||
#[derive(Debug, Clone)]
|
||||
struct Plain {
|
||||
pub(crate) struct Plain {
|
||||
degree: usize,
|
||||
knots: Vec<f64>,
|
||||
points: Vec<Point3>,
|
||||
@@ -361,7 +361,7 @@ fn wire_point(curves: &[(Plain, NurbCurve)], g: f64) -> Result<Point3> {
|
||||
}
|
||||
|
||||
/// A station's curves, and per rail its parameter and point there.
|
||||
type StationAt = (Vec<Plain>, Vec<(f64, Point3)>);
|
||||
pub(crate) type StationAt = (Vec<Plain>, Vec<(f64, Point3)>);
|
||||
|
||||
/// An affine move: `x` goes to `to + m (x - from)`.
|
||||
struct Affine {
|
||||
@@ -710,11 +710,11 @@ impl<'a> Loft<'a> {
|
||||
|
||||
/// A wire of the loft: a section's curves or a station's, at loft parameter `(span, f)`, and
|
||||
/// per rail the rail's parameter and point there.
|
||||
struct Wire {
|
||||
span: usize,
|
||||
f: f64,
|
||||
curves: Vec<Plain>,
|
||||
hits: Vec<(f64, Point3)>,
|
||||
pub(crate) struct Wire {
|
||||
pub(crate) span: usize,
|
||||
pub(crate) f: f64,
|
||||
pub(crate) curves: Vec<Plain>,
|
||||
pub(crate) hits: Vec<(f64, Point3)>,
|
||||
}
|
||||
|
||||
/// The loft through `sections` guided by `rails` (module docs).
|
||||
@@ -759,6 +759,35 @@ pub fn loft_rails(
|
||||
hits,
|
||||
});
|
||||
}
|
||||
skin_and_certify(
|
||||
k,
|
||||
wires,
|
||||
spans,
|
||||
rails,
|
||||
opts,
|
||||
&mut |k: &Kernel, i: usize, f: f64| loft.station(k, i, f),
|
||||
)
|
||||
}
|
||||
|
||||
/// The loft's skin-and-certificate loop (module docs, "Where the stations go, and the
|
||||
/// certificate"): from the wires `wires` (the sections, each with its rails' crossings, `spans`
|
||||
/// spans between them), a station placed by `station(k, span, f)` at each span's middle, the
|
||||
/// wires skinned, each rail bounded against the skin on every span between neighbouring wires,
|
||||
/// and a span whose bound is above the tolerance given a station at its middle, until every
|
||||
/// span is certified; past [`RailLoftOptions::max_wires`] refused, naming the bound reached.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`crate::error::sweep::SweepFailure::RailPastStationCap`] past the wire cap; those of
|
||||
/// `station`, the skin and the bound.
|
||||
pub(crate) fn skin_and_certify(
|
||||
k: &mut Kernel,
|
||||
mut wires: Vec<Wire>,
|
||||
spans: usize,
|
||||
rails: &[&Curve],
|
||||
opts: &RailLoftOptions,
|
||||
station: &mut dyn FnMut(&Kernel, usize, f64) -> Result<StationAt>,
|
||||
) -> Result<RailedLoft> {
|
||||
// one station at each span's middle
|
||||
let mut pending: Vec<(usize, f64)> = (0..spans).map(|i| (i, 0.5)).collect();
|
||||
// the largest bound the last skin proved (none before the first)
|
||||
@@ -776,7 +805,7 @@ pub fn loft_rails(
|
||||
.into_error());
|
||||
}
|
||||
for (i, f) in pending.drain(..) {
|
||||
let (curves, hits) = loft.station(k, i, f)?;
|
||||
let (curves, hits) = station(&*k, i, f)?;
|
||||
wires.push(Wire {
|
||||
span: i,
|
||||
f,
|
||||
|
||||
Reference in New Issue
Block a user