diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index 15c3e32204..6d7b67190f 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -26a4b7b6255a0e4db29fdd81cb4985bb9a9a1a8f +aec68dd91bce6c32b2ff6feec589a6d436873577 diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index 7545cb4887..00727fd4e2 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -6,10 +6,7 @@ //! SMLib's NURB parameters). This module reads them as hostile input and maps them to schema 4: //! - **Geometry:** each analytic surface to its schema kind (SMLib's cone of semi-angle 0 is its //! cylinder), NURBS as splines. Edge curves come as their own kinds, or as NURBS where SMLib -//! holds the edge as one (its primitives' circles do). As a fallback a NURBS line (order 2, -//! two points) and a rational quadratic circle or arc are recognised exactly and written as -//! `line` / `circle` with their interval mapped; other NURBS edges stay splines. The report -//! counts both. +//! holds the edge as one; nothing is recognised. The report counts the NURBS edges. //! - **Sides:** SMLib keeps a face's loops relative to its own natural normal, which may be //! opposite the STEP form's (a surface with SMLib's `insideOut` or `swapUV`). Schema 4 keeps //! them relative to the record's normal, outer loop counter-clockwise: a face whose outer loop @@ -113,8 +110,6 @@ impl Brep { /// What the mapping could not put in its own kind, or turned. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Report { - /// NURBS edge curves recognised as lines or circular arcs and written as those. - pub recognised_edges: usize, /// NURBS edge curves kept as splines (neither a line nor a circular arc). pub nurbs_edges: usize, /// Faces whose loops were reversed (SMLib's natural normal against the record's). @@ -297,9 +292,6 @@ fn blocks(counts: &[i64], total: usize, what: &str) -> Res [f64; 3] { [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] } -fn arr(p: Vec3) -> [f64; 3] { - [p.x, p.y, p.z] -} /// A frame from an origin, z axis and x axis (y = z × x). fn frame(origin: [f64; 3], axis: [f64; 3], x: [f64; 3]) -> b::Frame { b::Frame { origin, x_axis: x, y_axis: cross(axis, x) } @@ -450,80 +442,14 @@ impl Nurbs { } } -/// A NURBS line (order 2, two points, equal weights) as a line, its parameter mapped onto the -/// line's: t ↦ (t − k₁)/(k₂ − k₁). -fn as_line(n: &Nurbs, range: [f64; 2]) -> Option<(b::Curve, [f64; 2])> { - let ([p0, p1], [k0, k1, k2, k3]) = (<[[f64; 3]; 2]>::try_from(n.points.as_slice()).ok()?, <[f64; 4]>::try_from(n.knots.as_slice()).ok()?); - if !(k0 == k1 && k2 == k3 && k1 < k2) || n.weights.first() != n.weights.get(1) || p0 == p1 { - return None; - } - let s = |t: f64| (t - k1) / (k2 - k1); - Some((b::Curve::Line { start: p0, end: p1 }, [s(range[0]), s(range[1])])) -} - -/// A rational quadratic circle or circular arc as a circle: its frame's x axis at the curve's -/// start and its normal the curve's turning direction, so the angle grows along the curve; the -/// interval the angles of the range's ends. Checked: 65 samples lie on the circle within 10⁻⁹ -/// of its radius (at least 1 mm). -fn as_circle(n: &Nurbs, range: [f64; 2]) -> Option<(b::Curve, [f64; 2])> { - if n.knots.len() != n.points.len() + 3 || n.points.len() < 3 { - return None; - } - let eval = b::eval::CurveEval::new(&n.record()).ok()?; - let (a, z) = (*n.knots.get(2)?, *n.knots.get(n.points.len())?); - let at = |t: f64| eval.point(t); - let (p, q, r) = (at(a)?, at(a + (z - a) / 3.0)?, at(a + 2.0 * (z - a) / 3.0)?); - // The circle through three points: centre p + (|w|² (u × w) × u + |u|² w × (u × w)) / (2 |u × w|²). - let (u, w) = (q - p, r - p); - let normal = u.cross(w); - let nn = normal.dot(normal); - if !(nn > 0.0) { - return None; - } - let center = p + (normal.cross(u) * w.dot(w) + w.cross(normal) * u.dot(u)) * (0.5 / nn); - let radius = (p - center).len(); - let k = normal.normalized()?; - let slack = 1e-9 * radius.max(1.0); - for i in 0..=64 { - let s = at(a + (z - a) * f64::from(i) / 64.0)?; - if ((s - center).len() - radius).abs() > slack || (s - center).dot(k).abs() > slack { - return None; - } - } - let x = (p - center).normalized()?; - let y = k.cross(x); - let angle = |t: f64| at(t).map(|s| (s - center).dot(y).atan2((s - center).dot(x))); - // The angle at the range's start in [0, 2π), then unwrapped along the range. - let mut a0 = angle(range[0])?; - if a0 < 0.0 { - a0 += TAU; - } - let (mut prev, mut total) = (a0, a0); - for i in 1..=64 { - let mut t = angle(range[0] + (range[1] - range[0]) * f64::from(i) / 64.0)?; - while t - prev > std::f64::consts::PI { - t -= TAU; - } - while t - prev < -std::f64::consts::PI { - t += TAU; - } - total += t - prev; - prev = t; - } - if !(total > a0) { - return None; - } - Some((b::Curve::Circle { frame: b::Frame { origin: arr(center), x_axis: arr(x), y_axis: arr(y) }, radius }, [a0, total])) -} - /// How a UV trim's parameter (SMLib's edge parameter, as the arrays carry it) becomes the edge /// record's: t ↦ scale · t + offset, scale > 0. None where the record's parameter is not an /// affine image of SMLib's (a circle or ellipse, whose angle is not its NURBS parameter). type TrimMap = Option<[f64; 2]>; /// Each edge's record and interval (in SMLib's direction along it) and its [`TrimMap`], and how -/// many NURBS edges were recognised as lines or circles and how many stayed splines. -fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, Option, [f64; 2], TrimMap)>, usize, usize)> { +/// many edges are NURBS. +fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, Option, [f64; 2], TrimMap)>, usize)> { let kinds = flat.i(EDGE_CURVE_TYPE); let range = flat.f(EDGE_RANGE); if kinds.len() != n_edges || range.len() != 2 * n_edges { @@ -550,7 +476,7 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, Option, [ kn += n + o; Ok(Nurbs { knots, points: cvs.as_chunks::<3>().0.to_vec(), weights }) }; - let (mut recognised, mut kept) = (0usize, 0usize); + let mut kept = 0usize; let mut out = Vec::with_capacity(n_edges); for (e, &kind) in kinds.iter().enumerate() { let r = [at(range, 2 * e, "edge range")?, at(range, 2 * e + 1, "edge range")?]; @@ -590,24 +516,15 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, Option, [ } CURVE_NURBS => { let c = next_nurbs()?; - // A fallback: SMLib exports lines, circles and ellipses as their own kinds. - if let Some((line, s)) = as_line(&c, r) { - recognised += 1; - // as_line's t ↦ (t − k₁)/(k₂ − k₁). - let (k1, k2) = (c.knots.get(1).copied().unwrap_or(0.0), c.knots.get(2).copied().unwrap_or(1.0)); - (line, None, s, Some([1.0 / (k2 - k1), -k1 / (k2 - k1)])) - } else if let Some((circle, s)) = as_circle(&c, r) { - recognised += 1; - (circle, None, s, None) - } else { - kept += 1; - (c.record(), None, r, Some([1.0, 0.0])) - } + // A NURBS edge stays the NURBS SMLib holds, its trims on its own parameter: SMLib + // gives lines, circles and ellipses as their own kinds, and nothing is recognised here. + kept += 1; + (c.record(), None, r, Some([1.0, 0.0])) } other => return Err(unsupported(format!("SMLib curve kind {other}"))), }); } - Ok((out, recognised, kept)) + Ok((out, kept)) } /// Each edgeuse record's UV trim as SMLib holds it (`EDGEUSE_NURBS_*`), along its edgeuse (the @@ -767,34 +684,28 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { return Err(invalid("array too long")); } - // Tolerances: SMLib's, or the size-relative floor where larger. - let size = match flat.f(EXTENT) { - [a, b, c, d, e, f] => ((d - a).powi(2) + (e - b).powi(2) + (f - c).powi(2)).sqrt(), - _ => return Err(invalid("extent")), - }; - if !size.is_finite() { - return Err(invalid("extent")); - } - let floor = (1e-6 * size).max(1e-5); + // Tolerances exactly as SMLib gives them: each edge's and vertex's own, the body's the smallest + // of them (only what needs more carries more). Without per-item tolerances, every tolerance is + // the largest SMLib reports for the body. Nothing of ours is added: the record's tolerances go + // back into SMLib, which makes its approximations to them. let largest = match flat.f(TOLERANCE) { - [t] if t.is_finite() && *t > floor => *t, - [_] => floor, + [t] if t.is_finite() && *t > 0.0 => *t, _ => return Err(invalid("tolerance")), }; - // Each edge's and vertex's own tolerance where SMLib gives them (never below the floor); the - // body's is then the floor, and only what needs more carries more. Without them, every - // tolerance is the largest SMLib reports for the body. let own = |id: i32, n: usize| -> Res>> { match flat.f(id) { [] => Ok(None), - t if t.len() == n && t.iter().all(|x| x.is_finite() && *x > 0.0) => Ok(Some(t.iter().map(|x| x.max(floor)).collect())), + t if t.len() == n && t.iter().all(|x| x.is_finite() && *x > 0.0) => Ok(Some(t.to_vec())), _ => Err(invalid("edge or vertex tolerances")), } }; let n_vertex_points = flat.f(VERTEX_POSITION).len() / 3; let edge_tolerances = own(EDGE_TOLERANCE, flat.i(EDGE_CURVE_TYPE).len())?; let vertex_tolerances = own(VERTEX_TOLERANCE, n_vertex_points)?; - let tolerance = if edge_tolerances.is_some() && vertex_tolerances.is_some() { floor } else { largest }; + let tolerance = match (&edge_tolerances, &vertex_tolerances) { + (Some(e), Some(v)) => e.iter().chain(v).copied().fold(largest, f64::min), + _ => largest, + }; let tolerances = Tolerances { body: tolerance, edges: edge_tolerances, vertices: vertex_tolerances }; // Topology blocks. @@ -829,8 +740,7 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { let loop_edgeuses = blocks(flat.i(LOOP_EDGEUSE_COUNT), n_edgeuses, "loop edgeuses")?; // Edges along their records' parameters. - let (records, recognised, kept) = curves(flat, n_edges)?; - report.recognised_edges = recognised; + let (records, kept) = curves(flat, n_edges)?; report.nurbs_edges = kept; let mut edges = Vec::with_capacity(n_edges); for (e, (curve, nurbs, [a, z], trim_map)) in records.into_iter().enumerate() { diff --git a/crates/smlib/src/export/tests.rs b/crates/smlib/src/export/tests.rs index a8a4c5a9ed..d8158ba33c 100644 --- a/crates/smlib/src/export/tests.rs +++ b/crates/smlib/src/export/tests.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "native")] use super::*; #[cfg(feature = "native")] @@ -30,11 +31,7 @@ mod native_tests { let (native, report) = b.to_native_report().unwrap_or_else(|e| panic!("{name}: {e}")); native.validate().unwrap(); native.check_geometry().unwrap_or_else(|e| panic!("{name}: {e}")); - assert_eq!( - report, - Report { recognised_edges: report.recognised_edges, ..Report::default() }, - "{name}: every edge a line or circle, every face agrees" - ); + assert_eq!(report, Report { nurbs_edges: report.nurbs_edges, ..Report::default() }, "{name}: every face agrees"); let [f, e, n] = b.counts().unwrap(); assert_eq!((native.faces.len(), native.edges.len(), native.vertices.len()), (f, e, n), "{name}"); // One closed shell bounding one material region; no NURBS edges left. @@ -82,7 +79,7 @@ mod native_tests { } #[test] - fn recognised_edges_are_on_smlibs_curves() { + fn edges_are_on_smlibs_curves() { let cyl = Brep::make_cylinder([0.0; 3], 5.0, 10.0).unwrap().to_native().unwrap(); let side = cyl.faces.iter().find(|f| matches!(f.surface, b::Surface::Cylinder { .. })).unwrap(); assert!((side.domain[0][1] - side.domain[0][0] - TAU).abs() < 1e-12); @@ -224,25 +221,3 @@ mod native_tests { } } } - -#[test] -fn nurbs_lines_and_arcs_are_recognised_exactly() { - let line = Nurbs { knots: vec![2.0, 2.0, 4.0, 4.0], points: vec![[0.0; 3], [1.0, 2.0, 3.0]], weights: vec![1.0, 1.0] }; - let (c, r) = as_line(&line, [2.0, 4.0]).unwrap_or_else(|| panic!("line")); - assert_eq!(c, b::Curve::Line { start: [0.0; 3], end: [1.0, 2.0, 3.0] }); - assert_eq!(r, [0.0, 1.0]); - // A quarter circle of radius 2 about (1, 1, 0) as a rational quadratic Bézier. - let w = std::f64::consts::FRAC_1_SQRT_2; - let arc = Nurbs { - knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - points: vec![[3.0, 1.0, 0.0], [3.0, 3.0, 0.0], [1.0, 3.0, 0.0]], - weights: vec![1.0, w, 1.0], - }; - let (c, r) = as_circle(&arc, [0.0, 1.0]).unwrap_or_else(|| panic!("arc")); - let b::Curve::Circle { frame, radius } = c else { panic!("not a circle") }; - assert!((radius - 2.0).abs() < 1e-12 && (r[0]).abs() < 1e-12 && (r[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12, "{r:?}"); - assert!((frame.origin[0] - 1.0).abs() < 1e-12 && (frame.origin[1] - 1.0).abs() < 1e-12); - // A parabola-like quadratic is neither. - let not = Nurbs { knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points: vec![[0.0; 3], [1.0, 2.0, 0.0], [2.0, 0.0, 0.0]], weights: vec![1.0; 3] }; - assert!(as_circle(¬, [0.0, 1.0]).is_none() && as_line(¬, [0.0, 1.0]).is_none()); -}