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. //! 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. //! 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. //! 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 //! Edge curves are trimmed to their vertices here, so every kernel edge runs
@@ -54,6 +54,17 @@ pub(crate) enum CurveGeo {
a: f64, a: f64,
b: 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>), BSpline(mt::BSplineCurve<mt::Point3>),
Nurbs(mt::NurbsCurve<mt::Vector4>), 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 }) 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), "B_SPLINE_CURVE_WITH_KNOTS" | "BEZIER_CURVE" | "UNIFORM_CURVE" | "QUASI_UNIFORM_CURVE" => bspline_curve(cx, e),
"POLYLINE" => { "POLYLINE" => {
let pts: Vec<mt::Point3> = p 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)) 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 { fn conic_angle(f: &Frame, a: f64, b: f64, p: mt::Point3) -> f64 {
let d = p - f.o; let d = p - f.o;
(d.dot(f.y) / b).atan2(d.dot(f.x) / a) (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()) .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::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()), 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)) 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::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::BSpline(c) => mt::Curve::BSplineCurve(c.clone()),
CurveGeo::Nurbs(c) => mt::Curve::NurbsCurve(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)] #[cfg(test)]
mod tests; 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] [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). // A sample point off the axis, its revolution normal (∂S/∂angle × ∂S/∂profile).
let sample = 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::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::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::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::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::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::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))) Some(norm(sub([mid.x, mid.y, mid.z], pa)) <= 1e-6 * (1.0 + norm(pa)))
}); });
+1 -1
View File
@@ -34,7 +34,7 @@ The contents of an imported assembly are **rigid by default**: rigid joints (a r
Designs saved before imports made components keep their Import where it is (no component is made for it) and rebuild as they did. Designs saved before imports made components keep their Import where it is (no component is made for it) and rebuild as they did.
Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and linear extrusion, trimmed surfaces; lines, circles, ellipses, B-spline curves, polylines, trimmed/surface/seam curves; length and angle units (SI prefixes, conversion-based units such as inches); product names (body names come from the solid's name, else the product's; `names` below chooses); surface colours; assemblies (next-assembly-usage occurrences placed by item-defined transformations, and mapped items) — the product tree is kept in the feature and becomes the import's components (above). Files are untrusted: sizes, entity counts and nesting are capped and every kernel call is guarded. What cannot be read becomes a warning on the feature (a face left out gives an open body); not read yet: offset surfaces, pcurve-only edges. Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and linear extrusion, trimmed surfaces; lines, circles, ellipses, hyperbolas and parabolas (as exact rational curves, which export writes back as exact rational B-splines), B-spline curves, polylines, trimmed/surface/seam curves; length and angle units (SI prefixes, conversion-based units such as inches); product names (body names come from the solid's name, else the product's; `names` below chooses); surface colours; assemblies (next-assembly-usage occurrences placed by item-defined transformations, and mapped items) — the product tree is kept in the feature and becomes the import's components (above). Files are untrusted: sizes, entity counts and nesting are capped and every kernel call is guarded. What cannot be read becomes a warning on the feature (a face left out gives an open body); not read yet: offset surfaces, pcurve-only edges.
## Files from other CAD systems ## Files from other CAD systems