diff --git a/crates/kernel/src/step_in/conic_tests.rs b/crates/kernel/src/step_in/conic_tests.rs new file mode 100644 index 00000000..f5942541 --- /dev/null +++ b/crates/kernel/src/step_in/conic_tests.rs @@ -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::::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(¶bola_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"); + } + } +} diff --git a/crates/kernel/src/step_in/geom.rs b/crates/kernel/src/step_in/geom.rs index 1925ddd2..ef2d3a28 100644 --- a/crates/kernel/src/step_in/geom.rs +++ b/crates/kernel/src/step_in/geom.rs @@ -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), Nurbs(mt::NurbsCurve), } @@ -295,6 +306,22 @@ pub(crate) fn curve(cx: &Ctx, id: u64, depth: usize) -> R { } 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 = 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 { + 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 { + 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 { + 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 { + 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::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()), }) diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index 6bf8a91d..0e57150d 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -1268,5 +1268,7 @@ impl StepReading { } } +#[cfg(test)] +mod conic_tests; #[cfg(test)] mod tests; diff --git a/crates/kernel/src/step_out.rs b/crates/kernel/src/step_out.rs index 3cb1c59b..b7c9d378 100644 --- a/crates/kernel/src/step_out.rs +++ b/crates/kernel/src/step_out.rs @@ -532,7 +532,7 @@ fn analytic_revolution(ex: &p21::Exchange, e: &p21::Entity) -> Option [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> { 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))) }); diff --git a/docs/step-import.md b/docs/step-import.md index 6640a350..18fd0acb 100644 --- a/docs/step-import.md +++ b/docs/step-import.md @@ -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. -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