STEP import: read HYPERBOLA and PARABOLA edges exactly (#1832)

Thanks @NOWN-DEV.
This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-17 04:07:29 -04:00
1 parent f3b5846e6b
commit 2abf4e5a7d
5 files changed
+333 -5

No files matched your search

+197
View File
@@ -0,0 +1,197 @@
//! Hyperbola and parabola edges (ISO 10303-42 HYPERBOLA, PARABOLA): a cone cut by a plane
//! parallel to its axis meets it in a hyperbola, by a plane parallel to a generator in a
//! parabola. Hand-written files, volumes against closed forms.
use std::f64::consts::{FRAC_PI_4, PI};
use mt::{BoundedCurve, InnerSpace, ParametricCurve};
use truck_modeling as mt;
use super::geom::{Frame, hyperbola_arc, parabola_arc};
use super::*;
use crate::{measure, step_export};
const HEAD: &str = "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('t','',(''),(''),'','hand','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\n";
const TAIL: &str = "ENDSEC;\nEND-ISO-10303-21;\n";
const CONTEXT: &str = "#90=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#81),#91);\n#91=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#92))GLOBAL_UNIT_ASSIGNED_CONTEXT((#93,#94))REPRESENTATION_CONTEXT('',''));\n#92=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-6),#93,'','');\n#93=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));\n#94=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));\n";
/// Height of the cone pieces: the 45° cone `x² + y² = z²` up to `z = H`.
const H: f64 = 10.0;
fn rel(a: f64, b: f64) -> f64 {
(a - b).abs() / b.abs().max(1e-9)
}
/// A solid bounded by the 45° cone (apex at the origin, axis Z), its top plane `z = H` and a
/// cutting plane. `curve` defines `#30` (the conic edge from `v1` to `v2`, rising parameter) and
/// `#41`, the cutting plane's placement (normal pointing out of the solid). The top circle runs
/// `#51` from `circle_from` to the other vertex counter-clockwise seen from above; the loops say
/// which way each face uses each edge.
fn cone_piece(curve: &str, v1: [f64; 3], v2: [f64; 3], circle_from_v1: bool, loops: [&str; 3]) -> String {
let mut t = String::from(HEAD);
t += "#1=CARTESIAN_POINT('',(0.,0.,10.));\n#2=DIRECTION('',(0.,0.,1.));\n#3=DIRECTION('',(1.,0.,0.));\n#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);\n";
t += &format!("#5=CONICAL_SURFACE('',#4,10.,{FRAC_PI_4:.15});\n#6=PLANE('',#4);\n");
t += &format!("#10=CARTESIAN_POINT('',({:.15},{:.15},{:.15}));\n#11=VERTEX_POINT('',#10);\n", v1[0], v1[1], v1[2]);
t += &format!("#12=CARTESIAN_POINT('',({:.15},{:.15},{:.15}));\n#13=VERTEX_POINT('',#12);\n", v2[0], v2[1], v2[2]);
t += curve;
t += "#31=EDGE_CURVE('',#11,#13,#30,.T.);\n#42=PLANE('',#41);\n";
t += "#50=CIRCLE('',#4,10.);\n";
t += if circle_from_v1 { "#51=EDGE_CURVE('',#11,#13,#50,.T.);\n" } else { "#51=EDGE_CURVE('',#13,#11,#50,.T.);\n" };
t += "#52=VECTOR('',#3,1.);\n#53=LINE('',#10,#52);\n#54=EDGE_CURVE('',#11,#13,#53,.T.);\n";
// Faces: the cone, the cutting plane, the top.
for (k, (lp, surface)) in loops.iter().zip(["#5", "#42", "#6"]).enumerate() {
let b = 60 + 6 * k;
let edges: Vec<&str> = lp.split(' ').collect();
let mut oes = Vec::new();
for (j, e) in edges.iter().enumerate() {
let (id, fwd) = e.split_at(e.len() - 1);
t += &format!("#{}=ORIENTED_EDGE('',*,*,{id},{});\n", b + j, if fwd == "+" { ".T." } else { ".F." });
oes.push(format!("#{}", b + j));
}
t += &format!("#{}=EDGE_LOOP('',({}));\n#{}=FACE_OUTER_BOUND('',#{},.T.);\n", b + 3, oes.join(","), b + 4, b + 3);
t += &format!("#{}=ADVANCED_FACE('',(#{}),{surface},.T.);\n", b + 5, b + 4);
}
t += "#80=CLOSED_SHELL('',(#65,#71,#77));\n#81=MANIFOLD_SOLID_BREP('Piece',#80);\n";
t += CONTEXT;
t += TAIL;
t
}
/// The cone inside `z ≤ H` cut by the plane `y = D` (parallel to the axis), keeping `y ≥ D`:
/// the cut meets the cone in the hyperbola `z² − x² = D²`, centre `(0, D, 0)`, both semi axes D.
fn hyperbola_piece() -> String {
const D: f64 = 4.0;
let w = (H * H - D * D).sqrt();
// Real axis up the plane (X = +z), normal +y, so the imaginary axis is +x:
// (x, y, z) = (D·sinh t, D, D·cosh t).
let curve = "#20=CARTESIAN_POINT('',(0.,4.,0.));\n#21=DIRECTION('',(0.,1.,0.));\n#22=AXIS2_PLACEMENT_3D('',#20,#21,#2);\n#30=HYPERBOLA('',#22,4.,4.);\n#40=DIRECTION('',(0.,-1.,0.));\n#41=AXIS2_PLACEMENT_3D('',#20,#40,#3);\n";
// Cone: up the hyperbola from +x to −x, back along the arc; the plane (seen from −y):
// the hyperbola from −x to +x, back along the line; the top: the arc, then the line.
cone_piece(curve, [-w, D, H], [w, D, H], false, ["#31- #51-", "#31+ #54-", "#51+ #54+"])
}
/// The cone inside `z ≤ H` cut by the plane `z − y = C` (parallel to the generator in the
/// x = 0, y > 0 half plane), keeping `z − y ≥ C`: the cut meets the cone in a parabola with its
/// vertex at `(0, −C/2, C/2)`, axis along (0, 1, 1) and focal distance `C·√2 / 4`.
fn parabola_piece() -> String {
const C: f64 = 8.0;
let w = (H * H - (H - C) * (H - C)).sqrt();
// Normal (0, 1, −1) out of the solid, axis X = (0, 1, 1), so Y = Z × X = +x:
// (x, y, z) = (4√2·t, −4 + 2t², 4 + 2t²).
let curve = format!(
"#20=CARTESIAN_POINT('',(0.,-4.,4.));\n#21=DIRECTION('',(0.,1.,-1.));\n#23=DIRECTION('',(0.,1.,1.));\n#22=AXIS2_PLACEMENT_3D('',#20,#21,#23);\n#30=PARABOLA('',#22,{:.15});\n#41=AXIS2_PLACEMENT_3D('',#20,#21,#23);\n",
C * 2f64.sqrt() / 4.0
);
// Cone (seen from −y): the parabola from −x to +x, back along the arc; the plane (seen from
// outside, +x on the left): the parabola from +x to −x, back along the line; the top: the
// arc round through −y, then the line back.
cone_piece(&curve, [-w, H - C, H], [w, H - C, H], true, ["#31+ #51-", "#31- #54+", "#51+ #54-"])
}
/// ∫ from D to H of the circular segment `y ≥ D` of the disc of radius z.
fn hyperbola_piece_volume() -> f64 {
let d = 4.0;
let s = (H * H - d * d).sqrt();
H.powi(3) / 3.0 * (d / H).acos() - 2.0 / 3.0 * d * H * s + d.powi(3) / 3.0 * ((H + s) / d).ln()
}
/// ∫ from C/2 to H of the part `y ≤ z − C` of the disc of radius z.
fn parabola_piece_volume() -> f64 {
let c = 8.0;
let q: f64 = 2.0 * c * H - c * c;
H.powi(3) / 3.0 * (PI - (1.0 - c / H).acos())
- (0.4 * q.powf(2.5) + 4.0 / 3.0 * c * c * q.powf(1.5) + 2.0 * c.powi(4) * q.sqrt()) / (24.0 * c * c)
+ (0.4 * q.powf(2.5) - 2.0 / 3.0 * c * c * q.powf(1.5)) / (4.0 * c * c)
}
fn import_piece(text: &str, want: f64) -> StepImport {
assert_eq!(step_orientation_errors(text).unwrap(), Vec::<String>::new());
let imp = step_import(text).unwrap();
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
assert_eq!(imp.bodies.len(), 1);
assert!(imp.bodies[0].closed);
let m = measure(&imp.bodies[0].body).unwrap();
assert_eq!(m.faces, 3);
assert!(rel(m.volume, want) < 1e-4, "volume {} vs {want}", m.volume);
imp
}
/// Export the imported piece and read it back: the conic edges are written as exact rational
/// B-splines and come back as the same solid.
fn round_trip(imp: &StepImport, want: f64) {
let text = step_export(&[&imp.bodies[0].body], "conic").unwrap();
step_validate(&text).unwrap();
let back = step_import(&text).unwrap();
assert!(back.warnings.is_empty(), "{:?}", back.warnings);
assert_eq!(back.bodies.len(), 1);
assert!(back.bodies[0].closed);
let v = measure(&back.bodies[0].body).unwrap().volume;
assert!(rel(v, want) < 1e-4, "round trip volume {v} vs {want}");
}
#[test]
fn hyperbola_bounded_faces_import() {
let want = hyperbola_piece_volume();
assert!(rel(want, 175.447_506_9) < 1e-9, "{want}");
let imp = import_piece(&hyperbola_piece(), want);
round_trip(&imp, want);
}
#[test]
fn parabola_bounded_faces_import() {
let want = parabola_piece_volume();
assert!(rel(want, 447.014_684_29) < 1e-9, "{want}");
let imp = import_piece(&parabola_piece(), want);
round_trip(&imp, want);
}
/// The kernel curves lie exactly on the conics, end to end, whichever way they run.
#[test]
fn conic_arcs_are_exact() {
let f = Frame { o: mt::Point3::new(1.0, 2.0, 3.0), x: mt::Vector3::unit_y(), y: mt::Vector3::unit_z(), z: mt::Vector3::unit_x() };
let local = |p: mt::Point3| ((p - f.o).dot(f.x), (p - f.o).dot(f.y), (p - f.o).dot(f.z));
for (t0, t1) in [(-1.5, 2.0), (3.0, -0.25), (0.0, 7.5)] {
let c = hyperbola_arc(&f, 3.0, 2.0, t0, t1);
let (s0, s1) = c.range_tuple();
for i in 0..=200 {
let (x, y, z) = local(c.subs(s0 + (s1 - s0) * i as f64 / 200.0));
assert!((x * x / 9.0 - y * y / 4.0 - 1.0).abs() < 1e-9 && x > 0.0 && z.abs() < 1e-12, "{x} {y}");
}
let (x, y, _) = local(c.subs(s0));
assert!((x - 3.0 * f64::cosh(t0)).abs() < 1e-9 && (y - 2.0 * f64::sinh(t0)).abs() < 1e-9);
let (x, y, _) = local(c.subs(s1));
assert!((x - 3.0 * f64::cosh(t1)).abs() < 1e-9 && (y - 2.0 * f64::sinh(t1)).abs() < 1e-9);
let c = parabola_arc(&f, 1.5, t0, t1);
let (s0, s1) = c.range_tuple();
for i in 0..=200 {
let (x, y, z) = local(c.subs(s0 + (s1 - s0) * i as f64 / 200.0));
assert!((y * y - 4.0 * 1.5 * x).abs() < 1e-9 * (1.0 + x.abs()) && z.abs() < 1e-12, "{x} {y}");
}
let (x, y, _) = local(c.subs(s1));
assert!((x - 1.5 * t1 * t1).abs() < 1e-9 && (y - 3.0 * t1).abs() < 1e-9);
}
}
/// Hostile conics are errors (the face is reported and left out), never panics.
#[test]
fn hostile_conics_never_panic() {
let good = hyperbola_piece();
for bad in [
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,0.,4.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,4.,-1.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,4.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,1.E300,4.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,1.E-300,1.E-300)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "HYPERBOLA('',#22,5.,4.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "PARABOLA('',#22,0.)"),
good.replace("HYPERBOLA('',#22,4.,4.)", "PARABOLA('',#22,1.)"),
good.replace("#31=EDGE_CURVE('',#11,#13,#30,.T.)", "#31=EDGE_CURVE('',#11,#11,#30,.T.)"),
] {
let r = std::panic::catch_unwind(|| step_import(&bad));
let Ok(r) = r else { panic!("STEP import panicked") };
if let Ok(imp) = r {
assert!(!imp.warnings.is_empty(), "a broken conic imported silently");
}
}
}
+126 -1
View File
@@ -1,6 +1,6 @@
//! STEP geometry (ISO 10303-42 entities) to kernel curves and surfaces.
//!
//! Points, placements, lines, conics, B-splines (plain and rational), elementary surfaces and
//! Points, placements, lines, conics (ellipses, hyperbolas and parabolas), B-splines (plain and rational), elementary surfaces and
//! swept surfaces, with exact offsets of planes, cylinders, cones, spheres and regular tori.
//! Free-form offsets and offsets through collapsed radii remain explicit import warnings.
//! Edge curves are trimmed to their vertices here, so every kernel edge runs
@@ -54,6 +54,17 @@ pub(crate) enum CurveGeo {
a: f64,
b: f64,
},
/// Hyperbola branch `o + a·cosh t·x + b·sinh t·y` in the frame's XY plane.
Hyperbola {
f: Frame,
a: f64,
b: f64,
},
/// Parabola `o + focal·(t²·x + 2t·y)` in the frame's XY plane.
Parabola {
f: Frame,
focal: f64,
},
BSpline(mt::BSplineCurve<mt::Point3>),
Nurbs(mt::NurbsCurve<mt::Vector4>),
}
@@ -295,6 +306,22 @@ pub(crate) fn curve(cx: &Ctx, id: u64, depth: usize) -> R<CurveGeo> {
}
Ok(CurveGeo::Conic { f, a, b })
}
"HYPERBOLA" => {
let f = frame(cx, p.get(1).and_then(Param::as_ref_id).ok_or("HYPERBOLA without placement")?)?;
let (a, b) = (length(cx, p.get(2), "semi axis")?, length(cx, p.get(3), "semi imaginary axis")?);
if a <= 0.0 || b <= 0.0 {
return Err("hyperbola axes must be positive".into());
}
Ok(CurveGeo::Hyperbola { f, a, b })
}
"PARABOLA" => {
let f = frame(cx, p.get(1).and_then(Param::as_ref_id).ok_or("PARABOLA without placement")?)?;
let focal = length(cx, p.get(2), "focal distance")?;
if focal == 0.0 {
return Err("parabola focal distance must not be zero".into());
}
Ok(CurveGeo::Parabola { f, focal })
}
"B_SPLINE_CURVE_WITH_KNOTS" | "BEZIER_CURVE" | "UNIFORM_CURVE" | "QUASI_UNIFORM_CURVE" => bspline_curve(cx, e),
"POLYLINE" => {
let pts: Vec<mt::Point3> = p
@@ -346,6 +373,76 @@ pub(crate) fn conic_arc(f: &Frame, a: f64, b: f64, t0: f64, sweep: f64) -> mt::N
mt::NurbsCurve::new(mt::BSplineCurve::new(mt::KnotVec::from(knots), ctrl))
}
/// Largest hyperbola parameter step one rational quadratic piece spans (keeps its weight
/// `cosh(step / 2)` near 1).
const HYPERBOLA_STEP: f64 = 1.0;
/// Exact rational quadratic NURBS of the hyperbola branch from parameter `t0` to `t1` (either
/// order): pieces whose middle control point is the meeting point of the end tangents, weighted
/// `cosh` of the half step (the hyperbolic counterpart of [`conic_arc`]).
pub(crate) fn hyperbola_arc(f: &Frame, a: f64, b: f64, t0: f64, t1: f64) -> mt::NurbsCurve<mt::Vector4> {
let n = (((t1 - t0).abs() / HYPERBOLA_STEP).ceil() as usize).clamp(1, 64);
let dt = (t1 - t0) / n as f64;
let w = (dt * 0.5).cosh();
let at = |c: f64, s: f64| f.at(a * c, b * s, 0.0);
let mut ctrl = Vec::with_capacity(2 * n + 1);
ctrl.push(at(t0.cosh(), t0.sinh()).to_homogeneous());
let mut knots = vec![0.0, 0.0, 0.0];
for i in 0..n {
let ta = t0 + dt * i as f64;
let tm = ta + dt * 0.5;
let tb = ta + dt;
let m = at(tm.cosh() / w, tm.sinh() / w);
ctrl.push(mt::Vector4::new(m.x * w, m.y * w, m.z * w, w));
ctrl.push(at(tb.cosh(), tb.sinh()).to_homogeneous());
let k = (i + 1) as f64 / n as f64;
knots.push(k);
knots.push(k);
}
knots.push(1.0);
mt::NurbsCurve::new(mt::BSplineCurve::new(mt::KnotVec::from(knots), ctrl))
}
/// The parabola from parameter `t0` to `t1` (either order): one quadratic Bézier, exact.
pub(crate) fn parabola_arc(f: &Frame, focal: f64, t0: f64, t1: f64) -> mt::BSplineCurve<mt::Point3> {
let at = |t: f64| f.at(focal * t * t, 2.0 * focal * t, 0.0);
// The end tangents meet above the parameter midpoint: o + focal·(t0·t1, t0 + t1).
let mid = f.at(focal * t0 * t1, focal * (t0 + t1), 0.0);
mt::BSplineCurve::new(mt::KnotVec::bezier_knot(2), vec![at(t0), mid, at(t1)])
}
/// Parameter of the open conic at `p`, a point on (or within `tol` of) the curve.
fn open_conic_param(g: &CurveGeo, p: mt::Point3, tol: f64) -> R<f64> {
let (t, q) = match g {
CurveGeo::Hyperbola { f, a, b } => {
let t = ((p - f.o).dot(f.y) / b).asinh();
(t, f.at(a * t.cosh(), b * t.sinh(), 0.0))
}
CurveGeo::Parabola { f, focal } => {
let t = (p - f.o).dot(f.y) / (2.0 * focal);
(t, f.at(focal * t * t, 2.0 * focal * t, 0.0))
}
_ => return Err("not an open conic".into()),
};
let d = q.distance(p);
if !t.is_finite() || !(d <= tol) {
return Err(format!(
"edge vertex is {d:.3e} mm off its {} curve",
if matches!(g, CurveGeo::Parabola { .. }) { "parabola" } else { "hyperbola" }
));
}
Ok(t)
}
/// The open conic between parameters `t0` and `t1` as a kernel curve.
fn open_conic_curve(g: &CurveGeo, t0: f64, t1: f64) -> R<mt::Curve> {
match g {
CurveGeo::Hyperbola { f, a, b } => Ok(mt::Curve::NurbsCurve(hyperbola_arc(f, *a, *b, t0, t1))),
CurveGeo::Parabola { f, focal } => Ok(mt::Curve::BSplineCurve(parabola_arc(f, *focal, t0, t1))),
_ => Err("not an open conic".into()),
}
}
fn conic_angle(f: &Frame, a: f64, b: f64, p: mt::Point3) -> f64 {
let d = p - f.o;
(d.dot(f.y) / b).atan2(d.dot(f.x) / a)
@@ -473,6 +570,19 @@ pub(crate) fn edge_pieces(g: &CurveGeo, ps: mt::Point3, pe: mt::Point3, sense: b
})
.collect())
}
CurveGeo::Hyperbola { .. } | CurveGeo::Parabola { .. } => {
if closed {
return Err("closed edge on an open conic".into());
}
// The curve is open, so the edge is the parameters between its vertices: rising
// for the same sense, falling for the opposite one.
let (ts, te) = (open_conic_param(g, ps, tol)?, open_conic_param(g, pe, tol)?);
let scale = 1.0 + ts.abs().max(te.abs());
if (te - ts).abs() <= 1e-9 * scale {
return Err("degenerate conic edge".into());
}
Ok(vec![open_conic_curve(g, ts, te)?])
}
CurveGeo::BSpline(c) => Ok(spline_pieces(c, ps, pe, sense, closed, tol)?.into_iter().map(mt::Curve::BSplineCurve).collect()),
CurveGeo::Nurbs(c) => Ok(spline_pieces(c, ps, pe, sense, closed, tol)?.into_iter().map(mt::Curve::NurbsCurve).collect()),
}
@@ -486,6 +596,21 @@ fn bounded_curve(g: &CurveGeo, extent: &[mt::Point3]) -> R<mt::Curve> {
mt::Curve::Line(mt::Line(*p + *d * lo, *p + *d * hi))
}
CurveGeo::Conic { f, a, b } => mt::Curve::NurbsCurve(conic_arc(f, *a, *b, 0.0, TAU)),
CurveGeo::Hyperbola { f, .. } | CurveGeo::Parabola { f, .. } => {
// Bounded by the face's points (their parameters along the curve), with a margin.
let along = |q: &mt::Point3| match g {
CurveGeo::Hyperbola { b, .. } => ((q - f.o).dot(f.y) / b).asinh(),
CurveGeo::Parabola { focal, .. } => (q - f.o).dot(f.y) / (2.0 * focal),
_ => 0.0,
};
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for t in extent.iter().map(along).filter(|t| t.is_finite()) {
lo = lo.min(t);
hi = hi.max(t);
}
let (lo, hi) = if lo < hi { (lo - (hi - lo) * 0.25 - 1e-3, hi + (hi - lo) * 0.25 + 1e-3) } else { (-1.0, 1.0) };
open_conic_curve(g, lo, hi)?
}
CurveGeo::BSpline(c) => mt::Curve::BSplineCurve(c.clone()),
CurveGeo::Nurbs(c) => mt::Curve::NurbsCurve(c.clone()),
})
+2
View File
@@ -1268,5 +1268,7 @@ impl StepReading {
}
}
#[cfg(test)]
mod conic_tests;
#[cfg(test)]
mod tests;
+7 -3
View File
@@ -532,7 +532,7 @@ fn analytic_revolution(ex: &p21::Exchange, e: &p21::Entity) -> Option<Analytic>
[q.x, q.y, q.z]
})
}
CurveGeo::Conic { .. } => return None,
CurveGeo::Conic { .. } | CurveGeo::Hyperbola { .. } | CurveGeo::Parabola { .. } => return None,
};
// A sample point off the axis, its revolution normal (∂S/∂angle × ∂S/∂profile).
let sample =
@@ -798,7 +798,9 @@ fn edge_points(ex: &p21::Exchange, edge: u64, n: usize) -> Option<Vec<V3>> {
crate::step_in::geom::CurveGeo::Line { .. } => Some(ends.to_vec()),
crate::step_in::geom::CurveGeo::BSpline(c) => Some(sample(&|t| c.subs(t), c.range_tuple())),
crate::step_in::geom::CurveGeo::Nurbs(c) => Some(sample(&|t| c.subs(t), c.range_tuple())),
crate::step_in::geom::CurveGeo::Conic { .. } => None,
crate::step_in::geom::CurveGeo::Conic { .. }
| crate::step_in::geom::CurveGeo::Hyperbola { .. }
| crate::step_in::geom::CurveGeo::Parabola { .. } => None,
}
}
@@ -862,7 +864,9 @@ fn collapse_short_edges(ex: &mut p21::Exchange) {
crate::step_in::geom::CurveGeo::Line { .. } => return Some(true),
crate::step_in::geom::CurveGeo::BSpline(c) => c.subs((c.range_tuple().0 + c.range_tuple().1) / 2.0),
crate::step_in::geom::CurveGeo::Nurbs(c) => c.subs((c.range_tuple().0 + c.range_tuple().1) / 2.0),
crate::step_in::geom::CurveGeo::Conic { .. } => return Some(false),
crate::step_in::geom::CurveGeo::Conic { .. }
| crate::step_in::geom::CurveGeo::Hyperbola { .. }
| crate::step_in::geom::CurveGeo::Parabola { .. } => return Some(false),
};
Some(norm(sub([mid.x, mid.y, mid.z], pa)) <= 1e-6 * (1.0 + norm(pa)))
});