mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
STEP import: a face's NURBS form ends where its boundary does, within its tolerance
A face's form covers the quarters of its box, and the box is the extent of its boundary's points, which run past a quarter's end by as much as the edges lie off the surface (nanometres in a file of 1e-11 mm digits). A half tube's form then took the quarter past each end and went round the whole tube, which SMLib does not take for a torus: 16 parts of an alternator came in as meshes. An end within the tolerance of a quarter's end (the angle on the face's widest circle there) is now on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
@@ -50,11 +50,18 @@ pub fn angle(s: f64) -> f64 {
|
||||
q * FRAC_PI_2 + quarter_psi(s - q)
|
||||
}
|
||||
|
||||
/// The quarters an angle interval needs: the first one and how many.
|
||||
fn quarters([a0, a1]: [f64; 2]) -> Option<(f64, usize)> {
|
||||
/// The quarters an angle interval needs: the first one and how many. An end within `reach` (an
|
||||
/// angle) of a quarter's end is on it: a face whose boundary lies within its tolerance of a
|
||||
/// quarter's end needs no part of the quarter past it.
|
||||
fn quarters([a0, a1]: [f64; 2], reach: f64) -> Option<(f64, usize)> {
|
||||
if !(a0.is_finite() && a1.is_finite() && a1 > a0 && a1 - a0 <= TAU * (1.0 + 1e-9)) {
|
||||
return None;
|
||||
}
|
||||
let on_end = |a: f64| {
|
||||
let end = (a / FRAC_PI_2).round() * FRAC_PI_2;
|
||||
if (a - end).abs() <= reach { end } else { a }
|
||||
};
|
||||
let (a0, a1) = (on_end(a0), on_end(a1));
|
||||
let q0 = quarter_of(a0);
|
||||
let q1 = (a1 / FRAC_PI_2 - SLACK).ceil();
|
||||
let n = (q1 - q0).max(1.0) as usize;
|
||||
@@ -63,13 +70,14 @@ fn quarters([a0, a1]: [f64; 2]) -> Option<(f64, usize)> {
|
||||
|
||||
/// A face's quarters along an angle direction and its domain there: a whole turn is exactly
|
||||
/// four quarters from the one it starts in (the form closes on itself, so it is periodic and the
|
||||
/// face's seam uses lie a period apart), anything less the quarters it needs.
|
||||
fn face_quarters(a: [f64; 2]) -> Option<((f64, usize), [f64; 2])> {
|
||||
/// face's seam uses lie a period apart), anything less the quarters it needs ([`quarters`], ends
|
||||
/// within `reach` of a quarter's end on it).
|
||||
fn face_quarters(a: [f64; 2], reach: f64) -> Option<((f64, usize), [f64; 2])> {
|
||||
if a[1] - a[0] >= TAU * (1.0 - 1e-9) {
|
||||
let q0 = quarter_of(a[0]);
|
||||
return Some(((q0, 4), [q0, q0 + 4.0]));
|
||||
}
|
||||
let grid = quarters(a)?;
|
||||
let grid = quarters(a, reach)?;
|
||||
Some((grid, params(a, grid)))
|
||||
}
|
||||
|
||||
@@ -130,7 +138,7 @@ pub fn curve(c: &Curve, [t0, t1]: [f64; 2]) -> Option<(Curve, [f64; 2])> {
|
||||
|
||||
fn conic(frame: &Frame, [rx, ry]: [f64; 2], t: [f64; 2]) -> Option<(Curve, [f64; 2])> {
|
||||
let (o, x, y, _) = axes(frame)?;
|
||||
let grid = quarters(t)?;
|
||||
let grid = quarters(t, 0.0)?;
|
||||
let (knots, pts) = arcs(grid.0, grid.1);
|
||||
let homogeneous = pts.iter().map(|([c, s], w)| hom(o + x * (rx * c) + y * (ry * s), *w)).collect();
|
||||
Some((Curve::RationalSpline { knots, homogeneous }, params(t, grid)))
|
||||
@@ -139,8 +147,30 @@ fn conic(frame: &Frame, [rx, ry]: [f64; 2], t: [f64; 2]) -> Option<(Curve, [f64;
|
||||
/// The NURBS form of a cylinder, cone, sphere or torus face over `domain` (in the label's
|
||||
/// parameters: angles, and the height along the axis of a cylinder or cone) and the domain in the
|
||||
/// form's parameters; `None` for another kind, or a domain of more than one turn.
|
||||
pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]; 2])> {
|
||||
let (grid, fu) = face_quarters(u)?;
|
||||
pub fn surface(s: &Surface, domain: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]; 2])> {
|
||||
surface_within(s, domain, 0.0)
|
||||
}
|
||||
|
||||
/// [`surface`] for a face whose boundary lies within `tolerance` (mm) of its surface: where the
|
||||
/// domain ends within `tolerance` of a quarter's end (the angle that far along the widest circle
|
||||
/// of the face in that direction), the form ends there too. A face's box is the extent of its
|
||||
/// boundary's points ([`extent`]), which may run past a quarter's end by as much as its edges lie
|
||||
/// off the surface; the form then covers no quarter for that alone (a torus face of half a tube
|
||||
/// would otherwise go round the whole tube).
|
||||
pub fn surface_within(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Option<(Surface, [[f64; 2]; 2])> {
|
||||
// The angle `tolerance` makes on a circle of `radius`.
|
||||
let reach = |radius: f64| if tolerance > 0.0 && radius > 0.0 && radius.is_finite() { tolerance / radius } else { 0.0 };
|
||||
let (reach_u, reach_v) = match s {
|
||||
Surface::Cylinder { radius, .. } => (reach(radius.abs()), 0.0),
|
||||
Surface::Cone { radius_at_origin, half_angle, .. } => {
|
||||
let slope = half_angle.tan();
|
||||
(reach(v.iter().map(|h| (radius_at_origin + h * slope).abs()).fold(0.0, f64::max)), 0.0)
|
||||
}
|
||||
Surface::Sphere { radius, .. } => (reach(radius.abs()), reach(radius.abs())),
|
||||
Surface::Torus { major_radius, minor_radius, .. } => (reach(major_radius.abs() + minor_radius.abs()), reach(minor_radius.abs())),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
let (grid, fu) = face_quarters(u, reach_u)?;
|
||||
let (u_knots, ring) = arcs(grid.0, grid.1);
|
||||
// The ring of arcs times a profile `(control, weight)` mapped to (radial, axial) about the
|
||||
// frame: a rational tensor product, its weights the products.
|
||||
@@ -173,13 +203,13 @@ pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
|
||||
Some((Surface::RationalSpline { u_knots, v_knots: vec![v[0], v[0], v[1], v[1]], homogeneous }, [fu, v]))
|
||||
}
|
||||
Surface::Sphere { frame, radius } => {
|
||||
let vgrid = quarters(v)?;
|
||||
let vgrid = quarters(v, reach_v)?;
|
||||
let (v_knots, meridian) = arcs(vgrid.0, vgrid.1);
|
||||
let homogeneous = net(frame, &meridian, &|[c, s]| (radius * c, radius * s))?;
|
||||
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, params(v, vgrid)]))
|
||||
}
|
||||
Surface::Torus { frame, major_radius, minor_radius } => {
|
||||
let (vgrid, fv) = face_quarters(v)?;
|
||||
let (vgrid, fv) = face_quarters(v, reach_v)?;
|
||||
let (v_knots, tube) = arcs(vgrid.0, vgrid.1);
|
||||
let homogeneous = net(frame, &tube, &|[c, s]| (major_radius + minor_radius * c, minor_radius * s))?;
|
||||
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, fv]))
|
||||
@@ -189,7 +219,7 @@ pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
|
||||
// zero (the profile): (a + ib)(c + is) for each point's radial (a, b) in the frame and
|
||||
// each arc control point (c, s), weights multiplied; then the matrix.
|
||||
let (angles, along) = if *forward { (v, u) } else { (u, v) };
|
||||
let (ring_grid, fa) = face_quarters(angles)?;
|
||||
let (ring_grid, fa) = face_quarters(angles, 0.0)?;
|
||||
let (a_knots, arcs_r) = arcs(ring_grid.0, ring_grid.1);
|
||||
let (p_knots, h) = profile_nurbs(profile, Some(along))?;
|
||||
let fp = match profile.as_ref() {
|
||||
|
||||
@@ -394,6 +394,38 @@ fn shelled_three_quarter_tube_round_trips() {
|
||||
round_trip(&b);
|
||||
}
|
||||
|
||||
/// A half tube (half a torus's tube, half way round, closed by a flat annulus and
|
||||
/// two flat ends) whose torus the file places 5 nm off the rest, well within the file's
|
||||
/// accuracy: the torus face's boundary then projects a hair past both ends of its half of the
|
||||
/// tube, and its NURBS form once took the quarter past each end too, going round the whole tube,
|
||||
/// which SMLib does not take for a torus: the part came in as a mesh of its faces.
|
||||
#[test]
|
||||
fn half_tube_a_hair_off_its_torus_imports_as_a_solid() {
|
||||
use solidcraft_geom::Seg2;
|
||||
use std::f64::consts::PI;
|
||||
let (major, minor) = (10.75, 1.25);
|
||||
let d = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::new(major, 0.0), radius: minor, start: 0.0, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(major - minor, 0.0), b: Vec2::new(major + minor, 0.0) },
|
||||
],
|
||||
};
|
||||
let tube = revolve(&Plane::XZ, &[Region2 { outer: d, holes: vec![] }], Vec2::ZERO, Vec2::Y, PI).unwrap().pop().unwrap();
|
||||
let text = step_export(&[&tube], "test").unwrap();
|
||||
// The torus's placement moved to a point of its own, 5 nm along the axis, off the face's side.
|
||||
let torus = text.lines().find_map(|l| l.split_once("=TOROIDAL_SURFACE('',#")).unwrap().1.split(',').next().unwrap().to_string();
|
||||
let place = text.lines().find(|l| l.starts_with(&format!("#{torus}=AXIS2_PLACEMENT_3D('',#"))).unwrap();
|
||||
let (_, axes) = place.split_once("('',#").unwrap().1.split_once(',').unwrap();
|
||||
let mut low = text.replace(place, &format!("#{torus}=AXIS2_PLACEMENT_3D('',#900000,{axes}"));
|
||||
let data_end = low.rfind("ENDSEC;").unwrap();
|
||||
low.insert_str(data_end, "#900000=CARTESIAN_POINT('',(0.0,0.0,-5.E-6));\n");
|
||||
let imp = step_import(&low).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let b = &imp.bodies[0];
|
||||
assert!(b.refused.is_none() && b.closed && !b.body.is_mesh() && !b.body.is_surface(), "{:?} {:?}", b.refused, imp.warnings);
|
||||
assert!(rel(measure(&b.body).unwrap().volume, measure(&tube).unwrap().volume) < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_bodies_and_names() {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();
|
||||
|
||||
@@ -1226,7 +1226,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
boundary.push(b);
|
||||
}
|
||||
let extent = brep::form::extent(&label, &boundary).ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
|
||||
Some(brep::form::surface(&label, extent).ok_or_else(|| unsupported("a face of more than one turn"))?)
|
||||
Some(brep::form::surface_within(&label, extent, tol).ok_or_else(|| unsupported("a face of more than one turn"))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user