mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
brep: meridians, revolutions and whole turns exact on their forms
- A meridian of a sphere or a tube circle of a torus (a circle in a plane through the axis) has its exact pcurve on the label: u fixed at the arc's azimuth, v its angle from the outward radial, alpha + s t. - A placed revolution has a NURBS form: its profile's control points turned about the axis on the quarter grid from angle zero, weights multiplied, then its matrix; the record takes forms on revolutions. - `form::extent`: a face's parameter box from its boundary projected on its label (the collapsed parameter at a pole bounds nothing; a boundary that never goes round covers the turn), what its form is built over with no pcurve involved. - A face that goes all the way round gets a form of exactly one turn, closed and so periodic. - `along` projects inside the face's box (a spline extended past its knots, or a profile turned past the face, has other preimages), and takes a closed form within the record's tolerance, as the closed forms on analytic surfaces are. Tests: fits are gone, so curves with no closed form on their face (a tilted circle on a sphere, an ellipse on a cylinder, an intersection record, a curve weaving across a thread flank) are asserted refused; the closed-spline fixtures are built as forms. geom 166/166. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
This commit is contained in:
+14
-6
@@ -89,10 +89,11 @@ pub struct Face {
|
||||
/// What the face is: its surface as the producer names it (a cylinder, a NURBS…).
|
||||
pub surface: Surface,
|
||||
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
|
||||
/// parameters: a cylinder, cone, sphere or torus as SMLib holds it (every SMLib analytic
|
||||
/// surface is a B-spline surface, its trims in that NURBS's parameters). When given, `domain`
|
||||
/// and every pcurve are in this surface's (u, v), and `surface` only says what it is; it is a
|
||||
/// `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere or torus
|
||||
/// parameters: a cylinder, cone, sphere, torus or placed revolution as SMLib holds it (every
|
||||
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters). When
|
||||
/// given, `domain` and every pcurve are in this surface's (u, v), and `surface` only says what
|
||||
/// it is; it is a `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere, torus
|
||||
/// or placed revolution
|
||||
/// (docs/native-brep.md, "Surfaces").
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nurbs: Option<Surface>,
|
||||
@@ -600,8 +601,15 @@ impl Brep {
|
||||
surface(n, 0, &mut budget)?;
|
||||
require(matches!(n, Surface::Spline { .. } | Surface::RationalSpline { .. }), "NURBS form is not a NURBS")?;
|
||||
require(
|
||||
matches!(f.surface, Surface::Cylinder { .. } | Surface::Cone { .. } | Surface::Sphere { .. } | Surface::Torus { .. }),
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere or torus",
|
||||
matches!(
|
||||
f.surface,
|
||||
Surface::Cylinder { .. }
|
||||
| Surface::Cone { .. }
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
),
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, torus or revolution",
|
||||
)?;
|
||||
}
|
||||
let [u, v] = surface_natural(f.parameters());
|
||||
|
||||
@@ -61,6 +61,18 @@ fn quarters([a0, a1]: [f64; 2]) -> Option<(f64, usize)> {
|
||||
(n <= 5).then_some((q0, n))
|
||||
}
|
||||
|
||||
/// A face's quarters along an angle direction and its domain there: a whole turn is exactly
|
||||
/// four quarters from the one it starts in (the form closes on itself, so it is periodic and the
|
||||
/// face's seam uses lie a period apart), anything less the quarters it needs.
|
||||
fn face_quarters(a: [f64; 2]) -> Option<((f64, usize), [f64; 2])> {
|
||||
if a[1] - a[0] >= TAU * (1.0 - 1e-9) {
|
||||
let q0 = quarter_of(a[0]);
|
||||
return Some(((q0, 4), [q0, q0 + 4.0]));
|
||||
}
|
||||
let grid = quarters(a)?;
|
||||
Some((grid, params(a, grid)))
|
||||
}
|
||||
|
||||
/// The form parameters of an angle interval on the quarters `quarters` chose for it (its ends kept
|
||||
/// on them: an end within rounding past a quarter's end stays in that quarter).
|
||||
fn params([a0, a1]: [f64; 2], (q0, n): (f64, usize)) -> [f64; 2] {
|
||||
@@ -128,9 +140,8 @@ fn conic(frame: &Frame, [rx, ry]: [f64; 2], t: [f64; 2]) -> Option<(Curve, [f64;
|
||||
/// parameters: angles, and the height along the axis of a cylinder or cone) and the domain in the
|
||||
/// form's parameters; `None` for another kind, or a domain of more than one turn.
|
||||
pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]; 2])> {
|
||||
let grid = quarters(u)?;
|
||||
let (grid, fu) = face_quarters(u)?;
|
||||
let (u_knots, ring) = arcs(grid.0, grid.1);
|
||||
let fu = params(u, grid);
|
||||
// The ring of arcs times a profile `(control, weight)` mapped to (radial, axial) about the
|
||||
// frame: a rational tensor product, its weights the products.
|
||||
let net = |f: &Frame, profile: &[([f64; 2], f64)], at: &dyn Fn([f64; 2]) -> (f64, f64)| -> Option<Vec<Vec<[f64; 4]>>> {
|
||||
@@ -168,10 +179,45 @@ pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
|
||||
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, params(v, vgrid)]))
|
||||
}
|
||||
Surface::Torus { frame, major_radius, minor_radius } => {
|
||||
let vgrid = quarters(v)?;
|
||||
let (vgrid, fv) = face_quarters(v)?;
|
||||
let (v_knots, tube) = arcs(vgrid.0, vgrid.1);
|
||||
let homogeneous = net(frame, &tube, &|[c, s]| (major_radius + minor_radius * c, minor_radius * s))?;
|
||||
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, params(v, vgrid)]))
|
||||
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, fv]))
|
||||
}
|
||||
Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => {
|
||||
// The profile's control points turned about the axis on the quarter grid from angle
|
||||
// zero (the profile): (a + ib)(c + is) for each point's radial (a, b) in the frame and
|
||||
// each arc control point (c, s), weights multiplied; then the matrix.
|
||||
let (angles, along) = if *forward { (v, u) } else { (u, v) };
|
||||
let (ring_grid, fa) = face_quarters(angles)?;
|
||||
let (a_knots, arcs_r) = arcs(ring_grid.0, ring_grid.1);
|
||||
let (p_knots, h) = profile_nurbs(profile, Some(along))?;
|
||||
let fp = match profile.as_ref() {
|
||||
Curve::Circle { .. } | Curve::Ellipse { .. } => curve(profile, along)?.1,
|
||||
_ => along,
|
||||
};
|
||||
let (o, x, y, k) = revolution_frame(&h, *origin, *axis)?;
|
||||
let net: Vec<Vec<[f64; 4]>> = arcs_r
|
||||
.iter()
|
||||
.map(|([c, s], wu)| {
|
||||
h.iter()
|
||||
.map(|hp| {
|
||||
let d = Vec3::new(hp[0] / hp[3], hp[1] / hp[3], hp[2] / hp[3]) - o;
|
||||
let (a, b, z) = (d.dot(x), d.dot(y), d.dot(k));
|
||||
let q = o + k * z + x * (a * c - b * s) + y * (a * s + b * c);
|
||||
hom(affine(matrix, q), wu * hp[3])
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
if *forward {
|
||||
// u the profile, v the angle: the net turned over.
|
||||
let n = net.first()?.len();
|
||||
let t: Vec<Vec<[f64; 4]>> = (0..n).map(|j| net.iter().filter_map(|r| r.get(j).copied()).collect()).collect();
|
||||
Some((Surface::RationalSpline { u_knots: p_knots, v_knots: a_knots, homogeneous: t }, [fp, fa]))
|
||||
} else {
|
||||
Some((Surface::RationalSpline { u_knots: a_knots, v_knots: p_knots, homogeneous: net }, [fa, fp]))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -196,14 +242,60 @@ pub fn on_frame(c: &Curve, [t0, t1]: [f64; 2], frame: &Frame, tol: f64) -> Optio
|
||||
Some((Curve::Circle { frame: relabel, radius: *radius }, [t0 + shift, t1 + shift]))
|
||||
}
|
||||
|
||||
/// The frame of a cylinder, cone, sphere or torus (`None` for another surface).
|
||||
pub fn frame_of(s: &Surface) -> Option<&Frame> {
|
||||
/// The frame of a cylinder, cone, sphere or torus, or of a placed revolution (its axis, angle
|
||||
/// zero at its profile: [`revolution_frame`]); `None` for another surface.
|
||||
pub fn frame_of(s: &Surface) -> Option<Frame> {
|
||||
match s {
|
||||
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } | Surface::Sphere { frame, .. } | Surface::Torus { frame, .. } => Some(frame),
|
||||
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } | Surface::Sphere { frame, .. } | Surface::Torus { frame, .. } => {
|
||||
Some(frame.clone())
|
||||
}
|
||||
Surface::PlacedRevolution { profile, origin, axis, matrix, .. } => {
|
||||
let (_, h) = profile_nurbs(profile, None)?;
|
||||
let (o, x, y, _) = revolution_frame(&h, *origin, *axis)?;
|
||||
let m = |p: Vec3| affine(matrix, p);
|
||||
let (mo, mx, my) = (m(o), m(o + x) - m(o), m(o + y) - m(o));
|
||||
Some(Frame { origin: [mo.x, mo.y, mo.z], x_axis: [mx.x, mx.y, mx.z], y_axis: [my.x, my.y, my.z] })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `matrix` (column-major, `matrix[column][row]`) applied to `p`.
|
||||
fn affine(matrix: &[[f64; 4]; 4], p: Vec3) -> Vec3 {
|
||||
let row = |r: usize| matrix[0][r] * p.x + matrix[1][r] * p.y + matrix[2][r] * p.z + matrix[3][r];
|
||||
Vec3::new(row(0), row(1), row(2))
|
||||
}
|
||||
|
||||
/// A revolution's profile as a NURBS: its knots and homogeneous control points (a B-spline or
|
||||
/// NURBS as it is, a line or conic as its form over `range`; `None` without a range).
|
||||
fn profile_nurbs(profile: &Curve, range: Option<[f64; 2]>) -> Option<(Vec<f64>, Vec<[f64; 4]>)> {
|
||||
let formed;
|
||||
let c = match profile {
|
||||
Curve::Spline { .. } | Curve::RationalSpline { .. } => profile,
|
||||
other => {
|
||||
formed = curve(other, range?)?.0;
|
||||
&formed
|
||||
}
|
||||
};
|
||||
match c {
|
||||
Curve::Spline { knots, points } => Some((knots.clone(), points.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect())),
|
||||
Curve::RationalSpline { knots, homogeneous } => Some((knots.clone(), homogeneous.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A revolution's frame: the origin and unit axis, x the radial direction of the profile's
|
||||
/// first control point off the axis (angle zero), y = axis × x.
|
||||
fn revolution_frame(h: &[[f64; 4]], origin: [f64; 3], axis: [f64; 3]) -> Option<(Vec3, Vec3, Vec3, Vec3)> {
|
||||
let (o, k) = (v3(origin), v3(axis).normalized()?);
|
||||
let x = h.iter().find_map(|h| {
|
||||
let d = Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]) - o;
|
||||
let r = d - k * d.dot(k);
|
||||
if r.len() > 1e-12 * (1.0 + d.len()) { r.normalized() } else { None }
|
||||
})?;
|
||||
Some((o, x, k.cross(x), k))
|
||||
}
|
||||
|
||||
/// An edge's label and NURBS form: a circle about the axis of one of `frames` (the cylinder,
|
||||
/// cone, sphere and torus faces it bounds) is labelled on that frame ([`on_frame`]), so its form
|
||||
/// lies on that face's quarter grid; a circle or ellipse then has its form ([`curve`]). The label
|
||||
@@ -221,3 +313,52 @@ pub fn edge<'a>(
|
||||
};
|
||||
Some((label, iv, form))
|
||||
}
|
||||
|
||||
/// Points per edge along a loop taken for a face's extent.
|
||||
const EXTENT_POINTS: usize = 32;
|
||||
|
||||
/// The parameter box of a face on `label` over its boundary: each loop's points (`loops`, in
|
||||
/// order along each loop) projected onto the label, a periodic parameter unwrapped from one point
|
||||
/// to the next. What the face's NURBS form ([`surface`]) is built over; no pcurve is involved.
|
||||
pub fn extent(label: &Surface, loops: &[Vec<(&Curve, [f64; 2], bool)>]) -> Option<[[f64; 2]; 2]> {
|
||||
let eval = super::eval::SurfaceEval::new(label).ok()?;
|
||||
let period = eval.domain().period;
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
|
||||
for l in loops {
|
||||
let mut prev: Option<[f64; 2]> = None;
|
||||
for (c, [t0, t1], forward) in l {
|
||||
let curve = super::eval::CurveEval::new(c).ok()?;
|
||||
for k in 0..=EXTENT_POINTS {
|
||||
let f = k as f64 / EXTENT_POINTS as f64;
|
||||
let t = if *forward { t0 * (1.0 - f) + t1 * f } else { t1 * (1.0 - f) + t0 * f };
|
||||
let mut q = eval.project(curve.point(t)?, prev, f64::INFINITY)?;
|
||||
if let Some(p) = prev {
|
||||
for d in 0..2 {
|
||||
if let Some(Some(per)) = period.get(d) {
|
||||
q[d] += ((p[d] - q[d]) / per).round() * per;
|
||||
}
|
||||
}
|
||||
}
|
||||
// At a pole the collapsed parameter is anything: it bounds nothing.
|
||||
let pole = eval.pole(q);
|
||||
for d in 0..2 {
|
||||
if !pole.get(d).copied().unwrap_or(false) {
|
||||
lo[d] = lo[d].min(q[d]);
|
||||
hi[d] = hi[d].max(q[d]);
|
||||
}
|
||||
}
|
||||
if !pole.iter().any(|p| *p) {
|
||||
prev = Some(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// At most one turn round a periodic direction; a boundary that never goes round it (a face
|
||||
// bounded by its seam alone) covers the whole turn from there.
|
||||
for d in 0..2 {
|
||||
if let Some(Some(per)) = period.get(d) {
|
||||
hi[d] = if hi[d] - lo[d] <= 1e-9 * per { lo[d] + per } else { hi[d].min(lo[d] + per) };
|
||||
}
|
||||
}
|
||||
(lo[0] < hi[0] && lo[1] < hi[1]).then_some([[lo[0], hi[0]], [lo[1], hi[1]]])
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ impl On {
|
||||
}
|
||||
|
||||
fn pcurve(&self, record: &Curve, curve: &CurveEval, range: [f64; 2], tol: f64) -> Result<Curve2> {
|
||||
if let Some(pc) = self.exact(record, curve, range)?
|
||||
if let Some(pc) = self.exact(record, curve, range, tol)?
|
||||
&& self.lifts(&pc, curve, range, 0.5 * tol)
|
||||
{
|
||||
return Ok(pc);
|
||||
@@ -221,7 +221,7 @@ impl On {
|
||||
}
|
||||
|
||||
/// The closed-form pcurve where there is one (unchecked).
|
||||
fn exact(&self, record: &Curve, curve: &CurveEval, [t0, t1]: [f64; 2]) -> Result<Option<Curve2>> {
|
||||
fn exact(&self, record: &Curve, curve: &CurveEval, [t0, t1]: [f64; 2], tol: f64) -> Result<Option<Curve2>> {
|
||||
let segment = |a: [f64; 2], b: [f64; 2]| Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![a, b] };
|
||||
let at = |t: f64| curve.point(t).ok_or_else(|| Error::Failed(format!("the curve has no point at {t}")));
|
||||
Ok(match (&self.surface, record) {
|
||||
@@ -283,7 +283,7 @@ impl On {
|
||||
let c = super::eval::Axes::new(frame);
|
||||
let turn = c.z.dot(axes.z);
|
||||
if (turn.abs() - 1.0).abs() > 1e-9 {
|
||||
return Ok(None);
|
||||
return Ok(self.meridian(axes, &c, *radius, curve, [t0, t1]));
|
||||
}
|
||||
let sign = turn.signum();
|
||||
let u0 = c.x.dot(axes.y).atan2(c.x.dot(axes.x));
|
||||
@@ -305,28 +305,69 @@ impl On {
|
||||
let u = b.atan2(a);
|
||||
Some(segment([u, height(t0)?], [u, height(t1)?]))
|
||||
}
|
||||
(SurfaceEval::Eval(..), _) => self.along(curve, [t0, t1]),
|
||||
(SurfaceEval::Eval(..), _) => self.along(curve, [t0, t1], tol),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A meridian of a sphere or a tube circle of a torus (a circle in a plane through the axis,
|
||||
/// centred on the sphere's centre or on the torus's ring): u fixed at the arc's azimuth, v its
|
||||
/// angle from the outward radial toward the axis, `α + s·t` (`s` ±1 as the circle runs).
|
||||
/// `None` for any other circle.
|
||||
fn meridian(&self, axes: &super::eval::Axes, c: &super::eval::Axes, radius: f64, curve: &CurveEval, [t0, t1]: [f64; 2]) -> Option<Curve2> {
|
||||
let ring = match &self.surface {
|
||||
SurfaceEval::Sphere { radius: r, .. } => (0.0, *r),
|
||||
SurfaceEval::Torus { major, minor, .. } => (*major, *minor),
|
||||
_ => return None,
|
||||
};
|
||||
let size = 1.0 + ring.0 + ring.1;
|
||||
if c.z.dot(axes.z).abs() > 1e-9 || (radius - ring.1).abs() > 1e-9 * size {
|
||||
return None;
|
||||
}
|
||||
let tm = 0.5 * (t0 + t1);
|
||||
let m = curve.point(tm)?;
|
||||
let [mx, my, _] = axes.local(m);
|
||||
let u0 = my.atan2(mx);
|
||||
let radial = (axes.x * mx + axes.y * my).normalized()?;
|
||||
// The circle's centre: the sphere's, or the torus's ring at that azimuth.
|
||||
let centre = axes.o + radial * ring.0;
|
||||
if c.o.dist(centre) > 1e-9 * size {
|
||||
return None;
|
||||
}
|
||||
let s = c.z.dot(radial.cross(axes.z)).signum();
|
||||
let d = m - centre;
|
||||
let vm = d.dot(axes.z).atan2(d.dot(radial));
|
||||
let alpha = vm - s * tm;
|
||||
Some(Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![[u0, alpha + s * t0], [u0, alpha + s * t1]] })
|
||||
}
|
||||
|
||||
/// On a spline face (a NURBS form, or another spline), the edge's pcurve where it runs along
|
||||
/// the face's parameters at its own parameter's pace: a straight line in (u, v), between the
|
||||
/// ends' parameters, or with u (or v) the edge's parameter itself (a rim of a revolved form on
|
||||
/// the same quarter grid, whose two ends are one point on the seam). A candidate is taken only
|
||||
/// where it lifts onto the edge to within rounding, so it is the exact pcurve; none is fitted.
|
||||
fn along(&self, curve: &CurveEval, [t0, t1]: [f64; 2]) -> Option<Curve2> {
|
||||
/// where it lifts onto the edge within the record's tolerance, as the closed forms on analytic
|
||||
/// surfaces must: the closed form the geometry has, never a fit.
|
||||
fn along(&self, curve: &CurveEval, [t0, t1]: [f64; 2], tol: f64) -> Option<Curve2> {
|
||||
let (p0, p1, pm) = (curve.point(t0)?, curve.point(t1)?, curve.point(0.5 * (t0 + t1))?);
|
||||
let size = [p0, p1, pm].iter().flat_map(|p| [p.x.abs(), p.y.abs(), p.z.abs()]).fold(1.0, f64::max);
|
||||
let exact = EXACT * size;
|
||||
// Within the record's tolerance, as the closed forms on analytic surfaces are, and never
|
||||
// looser than rounding.
|
||||
let exact = (0.5 * tol).max(EXACT * size);
|
||||
let line = |a: [f64; 2], b: [f64; 2]| Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![a, b] };
|
||||
let mut candidates = Vec::with_capacity(3);
|
||||
if let (Some(a), Some(b)) = (self.project(p0, None, exact), self.project(p1, None, exact)) {
|
||||
let ends = (self.project_in(p0, exact), self.project_in(p1, exact));
|
||||
if let (Some(a), Some(b)) = ends {
|
||||
candidates.push(line(a, b));
|
||||
}
|
||||
// One parameter fixed where the edge's middle is (an end on a seam or at an apex may
|
||||
// project anywhere along it), the other from the ends.
|
||||
if let (Some(a), Some(b), Some(m)) = (ends.0, ends.1, self.project_in(pm, exact)) {
|
||||
candidates.push(line([m[0], a[1]], [m[0], b[1]]));
|
||||
candidates.push(line([a[0], m[1]], [b[0], m[1]]));
|
||||
}
|
||||
// u or v the edge's own parameter, either way along and offset (forms on one frame's
|
||||
// quarter grid, or on the same grid turned over), the other fixed.
|
||||
if let Some([u, v]) = self.project(pm, None, exact) {
|
||||
if let Some([u, v]) = self.project_in(pm, exact) {
|
||||
let tm = 0.5 * (t0 + t1);
|
||||
for sign in [1.0, -1.0] {
|
||||
let (cu, cv) = (u - sign * tm, v - sign * tm);
|
||||
@@ -351,6 +392,17 @@ impl On {
|
||||
})
|
||||
}
|
||||
|
||||
/// The parameters of `p` within `accept` inside the face's parameter box when it has one: a
|
||||
/// spline extended past its knots, or a profile turned past where the face lies, has other
|
||||
/// preimages outside it. The global search first, then from the box's centre.
|
||||
fn project_in(&self, p: Vec3, accept: f64) -> Option<[f64; 2]> {
|
||||
let Some(b) = self.face else { return self.project(p, None, accept) };
|
||||
let pad = |d: usize| 1e-9 * (1.0 + (b[d][1] - b[d][0]).abs());
|
||||
let inside = |q: [f64; 2]| (0..2).all(|d| q[d] >= b[d][0] - pad(d) && q[d] <= b[d][1] + pad(d));
|
||||
let centre = [0.5 * (b[0][0] + b[0][1]), 0.5 * (b[1][0] + b[1][1])];
|
||||
[None, Some(centre)].into_iter().filter_map(|near| self.project(p, near, accept)).find(|q| inside(*q))
|
||||
}
|
||||
|
||||
/// The parameters of `p` within `accept`, from `near` (and unwrapped to it) when given.
|
||||
fn project(&self, p: Vec3, near: Option<[f64; 2]>, accept: f64) -> Option<[f64; 2]> {
|
||||
let mut q = self.surface.project_with(&self.starts, p, near, accept)?;
|
||||
|
||||
@@ -128,10 +128,10 @@ fn rulings_are_v_lines() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Curves with no closed form are fitted cubic B-splines within the tolerance: a tilted circle
|
||||
/// on a sphere, a plane's ellipse on a cylinder, a circle off the axis on a plane.
|
||||
/// Curves with no closed form on their face are refused, never fitted: a tilted circle on a
|
||||
/// sphere, a plane's ellipse on a cylinder, a circle off the axis on a plane.
|
||||
#[test]
|
||||
fn other_curves_are_fitted_within_tolerance() {
|
||||
fn curves_without_a_closed_form_are_refused() {
|
||||
let (s, c) = 0.5f64.sin_cos();
|
||||
let sphere = Surface::Sphere { frame: about_z([0.; 3], false), radius: 6. };
|
||||
let great = Curve::Circle { frame: frame([0.; 3], [1., 0., 0.], [0., c, s]), radius: 6. };
|
||||
@@ -143,22 +143,10 @@ fn other_curves_are_fitted_within_tolerance() {
|
||||
let plane = Surface::Plane { origin: [0.; 3], u_axis: [1., 0., 0.], v_axis: [0., 1., 0.] };
|
||||
let circle = Curve::Circle { frame: about_z([3., 1., 0.], false), radius: 2. };
|
||||
for (surface, curve, interval) in [(&sphere, &great, [0., TAU]), (&cylinder, &ellipse, [0.2, 6.]), (&plane, &circle, [0., 3.])] {
|
||||
for tol in [1e-3, 1e-6] {
|
||||
let pc = &pcurves(surface, &one(curve, interval), tol).unwrap()[0][0];
|
||||
let Curve2::Spline { knots, .. } = pc else { panic!("a B-spline") };
|
||||
assert_eq!(knots.len() - pc_count(pc) - 1, 3, "cubic");
|
||||
let e = lift_error(surface, curve, interval, pc);
|
||||
assert!(e <= tol, "{curve:?} at {tol}: {e}");
|
||||
}
|
||||
let r = pcurves(surface, &one(curve, interval), 1e-6);
|
||||
assert!(matches!(&r, Err(Error::Failed(m)) if m.contains("not supported yet: no exact pcurve")), "{curve:?}: {r:?}");
|
||||
}
|
||||
}
|
||||
fn pc_count(pc: &Curve2) -> usize {
|
||||
match pc {
|
||||
Curve2::Spline { points, .. } => points.len(),
|
||||
Curve2::RationalSpline { homogeneous, .. } => homogeneous.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ends of each use in loop order and the loop's signed area in (u, v).
|
||||
fn walk(l: &[EdgeUse<'_>], pcs: &[Curve2]) -> (Vec<([f64; 2], [f64; 2])>, f64) {
|
||||
let mut ring = Vec::new();
|
||||
@@ -288,27 +276,30 @@ fn a_seam_from_pole_to_pole_encloses_the_whole_sphere() {
|
||||
knots: vec![0., 0., 0., 0.5, 0.5, 1., 1., 1.],
|
||||
homogeneous: vec![h(o - z + side, 1.), h(o - z + r, w), h(o + r, 1.), h(o + z + r, w), h(o + z - side, 1.)],
|
||||
};
|
||||
for (seam, iv) in [(&circle, [-FRAC_PI_2, FRAC_PI_2]), (&nurbs, [0., 1.])] {
|
||||
// The circle on the sphere as it is; the NURBS meridian, on a parameter of its own,
|
||||
// on the sphere's NURBS form (one turn is four quarters there, the latitude two).
|
||||
let (form, _) = crate::brep::form::surface(&sphere, [[0., TAU], [-FRAC_PI_2, FRAC_PI_2]]).unwrap();
|
||||
for (seam, iv, on, period, height) in [(&circle, [-FRAC_PI_2, FRAC_PI_2], &sphere, TAU, PI), (&nurbs, [0., 1.], &form, 4., 2.)] {
|
||||
for order in [[true, false], [false, true]] {
|
||||
let l: Vec<EdgeUse> = order
|
||||
.iter()
|
||||
.map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward, tolerance: 0.0, given: None })
|
||||
.collect();
|
||||
let pcs = &pcurves(&sphere, std::slice::from_ref(&l), tol).unwrap()[0];
|
||||
let pcs = &pcurves(on, std::slice::from_ref(&l), tol).unwrap()[0];
|
||||
let what = format!("{sf:?} {turn} {off} {seam:?} {order:?}");
|
||||
for pc in pcs {
|
||||
assert!(lift_error(&sphere, seam, iv, pc) <= tol, "{what}");
|
||||
assert!(lift_error(on, seam, iv, pc) <= tol, "{what}");
|
||||
}
|
||||
let mid = 0.5 * (iv[0] + iv[1]);
|
||||
let (a, b) = (pcs[0].eval(mid).unwrap(), pcs[1].eval(mid).unwrap());
|
||||
assert!(((a[0] - b[0]).abs() - TAU).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-12, "a period apart: {a:?} {b:?} {what}");
|
||||
assert!(((a[0] - b[0]).abs() - period).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-12, "a period apart: {a:?} {b:?} {what}");
|
||||
let (ends, area) = walk(&l, pcs);
|
||||
for k in 0..2 {
|
||||
assert!((ends[k].1[1] - ends[(k + 1) % 2].0[1]).abs() < 1e-6, "closed in v: {ends:?} {what}");
|
||||
}
|
||||
// The box less the corners `walk` cuts (it joins each use's 16th
|
||||
// sample to the next use's start): about 15/16 of it.
|
||||
assert!((area / (TAU * PI * 15. / 16.) - 1.).abs() < 1e-2, "counter-clockwise around the box: {area} {what}");
|
||||
assert!((area / (period * height * 15. / 16.) - 1.).abs() < 1e-2, "counter-clockwise around the box: {area} {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,28 +337,6 @@ fn a_torus_bounded_by_its_seams_spans_both_periods() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A surface of revolution about +z as a rational spline closed in u, as SMLib writes a fillet
|
||||
/// ring round a hole's rim and STEP a NURBS cylinder: u the nine-point rational quadratic
|
||||
/// circle on [0, 1] (first and last rows the same), v the profile's `[radius, height, weight]`
|
||||
/// points on `v_knots`.
|
||||
pub(crate) fn closed_ring(profile: &[[f64; 3]], v_knots: &[f64]) -> Surface {
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let around = [[1., 0., 1.], [1., 1., w], [0., 1., 1.], [-1., 1., w], [-1., 0., 1.], [-1., -1., w], [0., -1., 1.], [1., -1., w], [1., 0., 1.]];
|
||||
let homogeneous = around
|
||||
.iter()
|
||||
.map(|&[cx, cy, wu]| {
|
||||
profile
|
||||
.iter()
|
||||
.map(|&[r, z, wv]| {
|
||||
let wt = wu * wv;
|
||||
[r * cx * wt, r * cy * wt, z * wt, wt]
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
Surface::RationalSpline { u_knots: vec![0., 0., 0., 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1., 1., 1.], v_knots: v_knots.to_vec(), homogeneous }
|
||||
}
|
||||
|
||||
/// The pcurves of a loop on a spline closed in u (period 1): each lifts within `tol`, the
|
||||
/// seam's two uses (`seam`: their positions) lie one period apart, the loop closes (in v alone
|
||||
/// where it meets a pole) and turns counter-clockwise once around the whole (u, v) box, all in
|
||||
@@ -380,7 +349,9 @@ fn closes_around_the_box(surface: &Surface, l: &[EdgeUse<'_>], seam: [usize; 2],
|
||||
let iv = l[seam[0]].interval;
|
||||
let mid = 0.5 * (iv[0] + iv[1]);
|
||||
let (a, b) = (pcs[seam[0]].eval(mid).unwrap(), pcs[seam[1]].eval(mid).unwrap());
|
||||
assert!(((a[0] - b[0]).abs() - 1.).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9, "{what}: seam uses a period apart: {a:?} {b:?}");
|
||||
let dom = SurfaceEval::new(surface).unwrap().domain();
|
||||
let period = dom.period[0].unwrap();
|
||||
assert!(((a[0] - b[0]).abs() - period).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9, "{what}: seam uses a period apart: {a:?} {b:?}");
|
||||
let (ends, area) = walk(l, pcs);
|
||||
let n = l.len();
|
||||
for k in 0..n {
|
||||
@@ -390,6 +361,8 @@ fn closes_around_the_box(surface: &Surface, l: &[EdgeUse<'_>], seam: [usize; 2],
|
||||
}
|
||||
// At a pole `walk` cuts the corner (it joins each use's 16th sample to the next use's
|
||||
// start, across the pole's line): a sliver of the box less.
|
||||
let [u, v] = dom.range.map(|r| r.unwrap());
|
||||
let area = area / ((u.1 - u.0) * (v.1 - v.0));
|
||||
let least = if pole.is_some() { 0.9 } else { 1. - 1e-6 };
|
||||
assert!(area >= least && area < 1. + 1e-6, "{what}: counter-clockwise around the box: {area}");
|
||||
for pc in pcs {
|
||||
@@ -397,7 +370,7 @@ fn closes_around_the_box(surface: &Surface, l: &[EdgeUse<'_>], seam: [usize; 2],
|
||||
for k in 0..=32 {
|
||||
let t = knots[0] + (knots[knots.len() - 1] - knots[0]) * k as f64 / 32.;
|
||||
let q = pc.eval(t).unwrap();
|
||||
assert!((-1e-9..=1. + 1e-9).contains(&q[0]), "{what}: base period: {q:?}");
|
||||
assert!((u.0 - 1e-9..=u.1 + 1e-9).contains(&q[0]), "{what}: base period: {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,43 +381,45 @@ fn closes_around_the_box(surface: &Surface, l: &[EdgeUse<'_>], seam: [usize; 2],
|
||||
/// into its pole. Each seam's uses come out a period apart, never both on u = 0.
|
||||
#[test]
|
||||
fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let full = [0., TAU];
|
||||
let (big, small) = (5., 1.);
|
||||
// The torus band: the tube's quarter from its outer equator (v = 0) to its top (v = 1).
|
||||
let band = closed_ring(&[[big + small, 0., 1.], [big + small, small, w], [big, small, 1.]], &[0., 0., 0., 1., 1., 1.]);
|
||||
let outer = Curve::Circle { frame: about_z([0.; 3], false), radius: big + small };
|
||||
let top = Curve::Circle { frame: about_z([0., 0., small], false), radius: big };
|
||||
let quarter = Curve::Circle { frame: frame([big, 0., 0.], [1., 0., 0.], [0., 0., 1.]), radius: small };
|
||||
// Each surface and circle on its NURBS form (`form`), as SolidCraft's builders give them.
|
||||
let on = |c: &Curve, iv: [f64; 2]| crate::brep::form::curve(c, iv).unwrap();
|
||||
// The torus band: the tube's quarter from its outer equator to its top.
|
||||
let torus = Surface::Torus { frame: about_z([0.; 3], false), major_radius: big, minor_radius: small };
|
||||
let (band, _) = crate::brep::form::surface(&torus, [full, [0., FRAC_PI_2]]).unwrap();
|
||||
let outer = on(&Curve::Circle { frame: about_z([0.; 3], false), radius: big + small }, full);
|
||||
let top = on(&Curve::Circle { frame: about_z([0., 0., small], false), radius: big }, full);
|
||||
let quarter = on(&Curve::Circle { frame: frame([big, 0., 0.], [1., 0., 0.], [0., 0., 1.]), radius: small }, [0., FRAC_PI_2]);
|
||||
let l = vec![
|
||||
EdgeUse { edge: 0, curve: &outer, interval: full, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 2, curve: &top, interval: full, forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 0, curve: &outer.0, interval: outer.1, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &quarter.0, interval: quarter.1, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 2, curve: &top.0, interval: top.1, forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &quarter.0, interval: quarter.1, forward: false, tolerance: 0.0, given: None },
|
||||
];
|
||||
for tol in [1e-4, 1e-7] {
|
||||
closes_around_the_box(&band, &l, [1, 3], None, tol, &format!("torus band, tol {tol}"));
|
||||
}
|
||||
// The cylinder: radius 3 from z = 0 (v = 0) to z = 4 (v = 1), its seam a line.
|
||||
let cylinder = closed_ring(&[[3., 0., 1.], [3., 4., 1.]], &[0., 0., 1., 1.]);
|
||||
let (low, high) =
|
||||
(Curve::Circle { frame: about_z([0.; 3], false), radius: 3. }, Curve::Circle { frame: about_z([0., 0., 4.], false), radius: 3. });
|
||||
// The cylinder: radius 3 from z = 0 to z = 4, its seam a line.
|
||||
let (cylinder, _) = crate::brep::form::surface(&Surface::Cylinder { frame: about_z([0.; 3], false), radius: 3. }, [full, [0., 4.]]).unwrap();
|
||||
let low = on(&Curve::Circle { frame: about_z([0.; 3], false), radius: 3. }, full);
|
||||
let high = on(&Curve::Circle { frame: about_z([0., 0., 4.], false), radius: 3. }, full);
|
||||
let line = Curve::Line { start: [3., 0., 0.], end: [3., 0., 4.] };
|
||||
let l = vec![
|
||||
EdgeUse { edge: 0, curve: &low, interval: full, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 0, curve: &low.0, interval: low.1, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 2, curve: &high, interval: full, forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 2, curve: &high.0, interval: high.1, forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: false, tolerance: 0.0, given: None },
|
||||
];
|
||||
closes_around_the_box(&cylinder, &l, [1, 3], None, 1e-7, "cylinder");
|
||||
// The dome: radius 6 from its equator (v = 0) to its pole (v = 1), the seam a meridian.
|
||||
let dome = closed_ring(&[[6., 0., 1.], [6., 6., w], [0., 6., 1.]], &[0., 0., 0., 1., 1., 1.]);
|
||||
let equator = Curve::Circle { frame: about_z([0.; 3], false), radius: 6. };
|
||||
let meridian = Curve::Circle { frame: frame([0.; 3], [1., 0., 0.], [0., 0., 1.]), radius: 6. };
|
||||
// The dome: radius 6 from its equator to its pole, the seam a meridian.
|
||||
let (dome, _) = crate::brep::form::surface(&Surface::Sphere { frame: about_z([0.; 3], false), radius: 6. }, [full, [0., FRAC_PI_2]]).unwrap();
|
||||
let equator = on(&Curve::Circle { frame: about_z([0.; 3], false), radius: 6. }, full);
|
||||
let meridian = on(&Curve::Circle { frame: frame([0.; 3], [1., 0., 0.], [0., 0., 1.]), radius: 6. }, [0., FRAC_PI_2]);
|
||||
let l = vec![
|
||||
EdgeUse { edge: 0, curve: &equator, interval: full, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 0, curve: &equator.0, interval: equator.1, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &meridian.0, interval: meridian.1, forward: true, tolerance: 0.0, given: None },
|
||||
EdgeUse { edge: 1, curve: &meridian.0, interval: meridian.1, forward: false, tolerance: 0.0, given: None },
|
||||
];
|
||||
closes_around_the_box(&dome, &l, [1, 2], Some(2), 1e-7, "dome");
|
||||
// A spline whose ends merely come near each other stays open: no period, so the seam's
|
||||
@@ -454,11 +429,11 @@ fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
|
||||
open[8][0][1] += 1e-3;
|
||||
let open = Surface::RationalSpline { u_knots: u_knots.clone(), v_knots: v_knots.clone(), homogeneous: open };
|
||||
assert_eq!(SurfaceEval::new(&open).unwrap().domain().period, [None, None]);
|
||||
assert_eq!(SurfaceEval::new(&cylinder).unwrap().domain().period, [Some(1.), None]);
|
||||
assert_eq!(SurfaceEval::new(&cylinder).unwrap().domain().period, [Some(4.), None]);
|
||||
}
|
||||
|
||||
/// An intersection record evaluates to the point of both surfaces in the leader's normal
|
||||
/// plane, and its pcurves on either surface fit.
|
||||
/// plane; it has no exact pcurve on either.
|
||||
#[test]
|
||||
fn intersection_curves_lie_on_both_surfaces() {
|
||||
// The cylinder x² + y² = 25 cut by the plane z = 20 + x tan 0.4: an ellipse.
|
||||
@@ -482,9 +457,10 @@ fn intersection_curves_lie_on_both_surfaces() {
|
||||
let p = eval.point(12. * i as f64 / 48.).unwrap();
|
||||
assert!((p.x.hypot(p.y) - 5.).abs() < 1e-11 && (p.z - 20. - p.x * k).abs() < 1e-11, "{i}: {p:?}");
|
||||
}
|
||||
// It has no closed-form pcurve on either surface (an intersection record is a construction,
|
||||
// not a NURBS): refused, never fitted.
|
||||
for surface in [&cylinder, &plane] {
|
||||
let pc = &pcurves(surface, &one(&curve, [0., 12.]), 1e-6).unwrap()[0][0];
|
||||
assert!(lift_error(surface, &curve, [0., 12.], pc) <= 1e-6);
|
||||
assert!(matches!(pcurves(surface, &one(&curve, [0., 12.]), 1e-6), Err(Error::Failed(m)) if m.contains("no exact pcurve")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,13 +531,11 @@ fn an_edge_along_the_rim_of_a_band_winding_close_to_itself_stays_on_its_turn() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A curve that is smooth through a knot of full multiplicity (one smooth curve with the knot
|
||||
/// inserted three times, as SMLib's intersection curves come) gets a pcurve with one tangent
|
||||
/// there. Two pieces fitted apart meet at an angle in (u, v) (a tenth of a degree here, over the
|
||||
/// degree SMLib's check allows on a real thread), sharp where u runs hundreds of times as fast as
|
||||
/// v.
|
||||
/// A curve weaving across a thread flank, smooth through a knot of full multiplicity (one smooth
|
||||
/// curve with the knot inserted three times, as SMLib's intersection curves come), has no
|
||||
/// closed-form pcurve on the flank: it is refused, never fitted (SMLib gives its own trims).
|
||||
#[test]
|
||||
fn a_smooth_curve_through_a_full_knot_gets_a_pcurve_with_a_tangent_there() {
|
||||
fn a_curve_weaving_across_a_thread_flank_is_refused() {
|
||||
let (surface, _) = thread_flank();
|
||||
let s = SurfaceEval::new(&surface).unwrap();
|
||||
// Along the flank as it winds, a third of a turn (u = t), weaving a little across it.
|
||||
@@ -576,13 +550,8 @@ fn a_smooth_curve_through_a_full_knot_gets_a_pcurve_with_a_tangent_there() {
|
||||
(knots, ctrl) = crate::bspline::insert_knot(3, &knots, &ctrl, k).unwrap();
|
||||
}
|
||||
let weave = Curve::Spline { knots, points: ctrl.iter().map(|h| h.project().unwrap()).map(|p| [p.x, p.y, p.z]).collect() };
|
||||
let pc = &pcurves(&surface, &one(&weave, [a, b]), 1e-3).unwrap()[0][0];
|
||||
assert!(lift_error(&surface, &weave, [a, b], pc) <= 1e-3);
|
||||
let h = 1e-7;
|
||||
let (l, m, r) = (pc.eval(k - h).unwrap(), pc.eval(k).unwrap(), pc.eval(k + h).unwrap());
|
||||
let (dl, dr) = ([m[0] - l[0], m[1] - l[1]], [r[0] - m[0], r[1] - m[1]]);
|
||||
let cos = (dl[0] * dr[0] + dl[1] * dr[1]) / (dl[0].hypot(dl[1]) * dr[0].hypot(dr[1]));
|
||||
assert!(cos > 1. - 1e-6, "the pcurve turns by {} degrees at the knot", cos.min(1.).acos().to_degrees());
|
||||
// A curve weaving across the flank has no closed-form pcurve there: refused, never fitted.
|
||||
assert!(matches!(pcurves(&surface, &one(&weave, [a, b]), 1e-3), Err(Error::Failed(m)) if m.contains("no exact pcurve")));
|
||||
}
|
||||
|
||||
/// Hostile records give errors, never a panic.
|
||||
@@ -607,11 +576,12 @@ fn hostile_records_give_errors() {
|
||||
// Too many uses, refused before any work.
|
||||
let many = vec![vec![EdgeUse { edge: 0, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0, given: None }; MAX_USES + 1]];
|
||||
assert!(matches!(pcurves(&plane, &many, 1e-6), Err(Error::TooLarge(_))));
|
||||
// A polyline of many points on a curved surface: too many spans for one pcurve.
|
||||
// A polyline of many points across a curved surface: it has no exact pcurve there, and is
|
||||
// refused as such (nothing is fitted).
|
||||
let cylinder = Surface::Cylinder { frame: about_z([0.; 3], false), radius: 1. };
|
||||
let n = 5000;
|
||||
let zigzag = Curve::Polyline { points: (0..n).map(|i| [(i as f64 * 0.01).cos(), (i as f64 * 0.01).sin(), (i % 2) as f64]).collect() };
|
||||
assert!(matches!(pcurves(&cylinder, &one(&zigzag, [0., (n - 1) as f64]), 1e-6), Err(Error::TooLarge(_))));
|
||||
assert!(matches!(pcurves(&cylinder, &one(&zigzag, [0., (n - 1) as f64]), 1e-6), Err(Error::Failed(m)) if m.contains("no exact pcurve")));
|
||||
// A degree the evaluators do not take.
|
||||
let high = Curve::Spline { knots: [vec![0.; 10], vec![1.; 10]].concat(), points: (0..10).map(|i| [i as f64, 0., 0.]).collect() };
|
||||
assert!(matches!(pcurves(&cylinder, &one(&high, [0., 1.]), 1e-6), Err(Error::Failed(_))));
|
||||
|
||||
@@ -38,7 +38,7 @@ fn fit_pcurves(b: &mut Brep) {
|
||||
let e = &b.edges[u.edge as usize];
|
||||
pcurve::EdgeUse {
|
||||
edge: u.edge,
|
||||
curve: &e.curve,
|
||||
curve: e.parameters(),
|
||||
interval: e.interval,
|
||||
forward: u.forward,
|
||||
tolerance: e.tolerance,
|
||||
@@ -49,7 +49,7 @@ fn fit_pcurves(b: &mut Brep) {
|
||||
Loop::Vertex { .. } => vec![],
|
||||
})
|
||||
.collect();
|
||||
let fitted = pcurve::pcurves(&f.surface, &loops, 1e-7).unwrap();
|
||||
let fitted = pcurve::pcurves_within(f.parameters(), &loops, 1e-7, Some(f.domain)).unwrap();
|
||||
for (l, pcs) in f.loops.iter_mut().zip(fitted) {
|
||||
if let Loop::Edges { edges } = l {
|
||||
for (u, pc) in edges.iter_mut().zip(pcs) {
|
||||
@@ -193,18 +193,19 @@ fn one_face(surface: Surface, domain: [[f64; 2]; 2], edges: Vec<(Curve, [f64; 2]
|
||||
fit_pcurves(&mut b);
|
||||
b
|
||||
}
|
||||
/// A ring of a torus as a rational spline closed in u (first and last control rows the same),
|
||||
/// as SMLib writes the fillet round a hole's rim: bounded by its two circles and the tube's
|
||||
/// quarter at u = 0, a seam used twice.
|
||||
/// A ring of a torus on its NURBS form, closed in u (first and last control rows the same), as
|
||||
/// SMLib holds the fillet round a hole's rim: bounded by its two circles and the tube's quarter
|
||||
/// at u = 0, a seam used twice, each on its form.
|
||||
fn closed_band() -> Brep {
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let (big, small) = (5., 1.);
|
||||
let surface = pcurve::tests::closed_ring(&[[big + small, 0., 1.], [big + small, small, w], [big, small, 1.]], &[0., 0., 0., 1., 1., 1.]);
|
||||
let torus =
|
||||
Surface::Torus { frame: Frame { origin: [0., 0., 0.], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] }, major_radius: big, minor_radius: small };
|
||||
let (form, domain) = form::surface(&torus, [[0., TAU], [0., FRAC_PI_2]]).unwrap();
|
||||
let about = |z: f64| Frame { origin: [0., 0., z], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] };
|
||||
let quarter = Curve::Circle { frame: Frame { origin: [big, 0., 0.], x_axis: [1., 0., 0.], y_axis: [0., 0., 1.] }, radius: small };
|
||||
one_face(
|
||||
surface,
|
||||
[[0., 1.], [0., 1.]],
|
||||
let mut b = one_face(
|
||||
torus,
|
||||
domain,
|
||||
vec![
|
||||
(Curve::Circle { frame: about(0.), radius: big + small }, [0., TAU], [0, 0]),
|
||||
(quarter, [0., FRAC_PI_2], [0, 1]),
|
||||
@@ -212,7 +213,15 @@ fn closed_band() -> Brep {
|
||||
],
|
||||
&[[big + small, 0., 0.], [big, 0., small]],
|
||||
&[(0, true), (1, true), (2, false), (1, false)],
|
||||
)
|
||||
);
|
||||
b.faces[0].nurbs = Some(form);
|
||||
for e in &mut b.edges {
|
||||
if let Some((nurbs, interval)) = form::curve(&e.curve, e.interval) {
|
||||
(e.nurbs, e.interval) = (Some(nurbs), interval);
|
||||
}
|
||||
}
|
||||
fit_pcurves(&mut b);
|
||||
b
|
||||
}
|
||||
/// A whole sphere bounded by one meridian from pole to pole, used twice: a closed shell.
|
||||
fn ball() -> Brep {
|
||||
@@ -549,10 +558,11 @@ fn geometric_defects_are_refused_by_record() {
|
||||
(
|
||||
"seam uses of a closed spline on one line",
|
||||
Record::Use { face: 0, r#loop: 0, position: 1 },
|
||||
Box::new(|p: &Problem| *p == Problem::Period { gap: 1., period: 1. }),
|
||||
// On the ring's NURBS form one turn is four quarters.
|
||||
Box::new(|p: &Problem| *p == Problem::Period { gap: 4., period: 4. }),
|
||||
{
|
||||
let mut b = closed_band();
|
||||
shift(pcurve_of(&mut b, 0, 1), [-1., 0.]);
|
||||
shift(pcurve_of(&mut b, 0, 1), [-4., 0.]);
|
||||
b
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user