STEP import: read HYPERBOLA and PARABOLA edges exactly (#1832)
Thanks @NOWN-DEV.
This commit is contained in:
1 parent
f3b5846e6b
commit
2abf4e5a7d
5 files changed
+333
-5
No files matched your search
@@ -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(¶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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
})
|
||||
|
||||
@@ -1268,5 +1268,7 @@ impl StepReading {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod conic_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -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)))
|
||||
});
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user