STEP import: a torus face round most of its tube is formed within one turn of it

A torus face covering most of its tube off the tube's quarter lines (an
exhaust collector's bend: 326° of the tube from −113.8°) needed a fifth
quarter along the tube, which STEP counts from the outer equator: its form
ran past a full turn of the tube and SMLib refused the solid. #8496 turns
frames about the axis only, and the torus label had no tube reference.

The torus label gets a tube start (`tube_start`, radians; 0 and left out
of the record by default, so older records mean what they did): v counts
from there, in every evaluator, form, pcurve, iso curve, tessellation,
placement, SSI and SMLib transfer. A spilling torus face's tube start moves
to where its tube begins, or a turn before where it ends, whichever puts
its tube circles on the grid of a round face they are about the axis of
(a pipe's cylinder meeting the bend); circles round the tube are labelled
from the tube start (brep::form::on_tube), so their forms and pcurves lie
on the face's quarter grid exactly. The saved-format change is one file,
crates/doc/src/format/pending/torus-tube-start.rs.

Engine assembly: COLLECTOR_EXHAUST_VER_1 now comes in as a closed solid
(refused 3 -> 2); Poppy Humanoid unchanged (4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:52:45 -04:00
committed by Irakli Kourtis
parent bd2785717d
commit c070f044ab
21 changed files with 459 additions and 67 deletions
@@ -0,0 +1,5 @@
//! A torus surface may save `tube_start`: the tube angle (radians) its v counts from, so that a
//! face's form starts its tube where the face begins (a face covering most of the tube, off the
//! tube's quarter lines, imported from STEP). An older design has none and counts from the outer
//! equator, as before. The last release refuses a design with this change (the newer format
//! number).
+18 -4
View File
@@ -232,9 +232,18 @@ pub enum Surface {
/// `origin + radius (cos v (cos u x + sin u y) + sin v z)`: u the longitude, v the latitude
/// in [-π/2, π/2] (STEP `SPHERICAL_SURFACE`).
Sphere { frame: Frame, radius: f64 },
/// `origin + (R + r cos v)(cos u x + sin u y) + r sin v z`: u the longitude, v the meridian
/// angle (STEP `TOROIDAL_SURFACE`).
Torus { frame: Frame, major_radius: f64, minor_radius: f64 },
/// `origin + (R + r cos w)(cos u x + sin u y) + r sin w z`, `w = v + tube_start`: u the
/// longitude, v the meridian angle measured from `tube_start` (radians; 0, the outer equator,
/// in STEP `TOROIDAL_SURFACE` and older records). The tube start is the tube's own reference,
/// as the frame's x axis is the longitude's: a face's form starts its tube on a quarter line
/// from there (`form`).
Torus {
frame: Frame,
major_radius: f64,
minor_radius: f64,
#[serde(default, skip_serializing_if = "is_zero")]
tube_start: f64,
},
/// A tensor-product B-spline: `points[i][j]`, i along u (STEP `B_SPLINE_SURFACE_WITH_KNOTS`).
Spline { u_knots: Vec<f64>, v_knots: Vec<f64>, points: Vec<Vec<[f64; 3]>> },
/// A NURBS surface: homogeneous `[x*w, y*w, z*w, w]` coefficients.
@@ -307,6 +316,10 @@ fn frame(f: &Frame, budget: &mut Budget) -> Result<()> {
let (x, y) = (vector(f.x_axis), vector(f.y_axis));
require((x.len() - 1.).abs() <= 1e-8 && (y.len() - 1.).abs() <= 1e-8 && x.dot(y).abs() <= 1e-8, "nonorthonormal frame")
}
/// A field left out of a record where it is zero (its default).
fn is_zero(x: &f64) -> bool {
*x == 0.0
}
fn radius(r: f64) -> Result<()> {
require(r.is_finite() && (1e-12..=1e12).contains(&r), "radius")
}
@@ -465,10 +478,11 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
numbers([*radius_at_origin, *half_angle])?;
require(*radius_at_origin >= 0. && half_angle.abs() >= 1e-12 && half_angle.abs() < std::f64::consts::FRAC_PI_2, "cone dimensions")
}
Surface::Torus { frame: f, major_radius, minor_radius } => {
Surface::Torus { frame: f, major_radius, minor_radius, tube_start } => {
frame(f, budget)?;
radius(*major_radius)?;
radius(*minor_radius)?;
require(tube_start.is_finite() && tube_start.abs() <= std::f64::consts::TAU, "torus tube start")?;
require(major_radius > minor_radius, "ring torus radii")
}
Surface::Spline { u_knots, v_knots, points: p } => grid(u_knots, v_knots, p, budget),
+13 -11
View File
@@ -79,10 +79,12 @@ pub enum SurfaceEval {
axes: Axes,
radius: f64,
},
/// `tube`: the tube angle at v = 0 (`Surface::Torus::tube_start`).
Torus {
axes: Axes,
major: f64,
minor: f64,
tube: f64,
},
}
@@ -103,8 +105,8 @@ impl SurfaceEval {
Ok(SurfaceEval::Cone { axes: Axes::new(frame), radius_at_origin: *radius_at_origin, slope: half_angle.tan() })
}
Surface::Sphere { frame, radius } => Ok(SurfaceEval::Sphere { axes: Axes::new(frame), radius: *radius }),
Surface::Torus { frame, major_radius, minor_radius } => {
Ok(SurfaceEval::Torus { axes: Axes::new(frame), major: *major_radius, minor: *minor_radius })
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Ok(SurfaceEval::Torus { axes: Axes::new(frame), major: *major_radius, minor: *minor_radius, tube: *tube_start })
}
Surface::Spline { u_knots, v_knots, points } => {
let rows: Vec<Vec<[f64; 4]>> = points.iter().map(|r| r.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect()).collect();
@@ -163,9 +165,9 @@ impl SurfaceEval {
let (s, c) = v.sin_cos();
(a.o + (r * c + a.z * s) * *radius, dr * (c * radius), (a.z * c - r * s) * *radius)
}
SurfaceEval::Torus { axes: a, major, minor } => {
SurfaceEval::Torus { axes: a, major, minor, tube } => {
let (r, dr) = a.radial(u);
let (s, c) = v.sin_cos();
let (s, c) = (v + tube).sin_cos();
let ring = major + minor * c;
(a.o + r * ring + a.z * (minor * s), dr * ring, (a.z * c - r * s) * *minor)
}
@@ -195,9 +197,9 @@ impl SurfaceEval {
let (s, c) = v.sin_cos();
(r * (-c * radius), dr * (-s * radius), (r * c + a.z * s) * -*radius)
}
SurfaceEval::Torus { axes: a, major, minor } => {
SurfaceEval::Torus { axes: a, major, minor, tube } => {
let (r, dr) = a.radial(u);
let (s, c) = v.sin_cos();
let (s, c) = (v + tube).sin_cos();
(r * -(major + minor * c), dr * (-s * minor), (r * c + a.z * s) * -*minor)
}
})
@@ -241,8 +243,8 @@ impl SurfaceEval {
let pole = if i == 0 { a.z * s } else { Vec3::ZERO };
Some(lift(a, i, j, (radial(a, i) * c + pole) * *radius))
}),
SurfaceEval::Torus { axes: a, major, minor } => tabulate(|i, j| {
let (c, s) = at_v(j);
SurfaceEval::Torus { axes: a, major, minor, tube } => tabulate(|i, j| {
let (c, s) = trig_derivative(v + tube, j);
let ring = if j == 0 { major + minor * c } else { minor * c };
let tube = if i == 0 { a.z * (minor * s) } else { Vec3::ZERO };
Some(lift(a, i, j, radial(a, i) * ring + tube))
@@ -274,10 +276,10 @@ impl SurfaceEval {
let [a, b, h] = axes.local(p);
[angle(a, b, near.map(|n| n[0])), h.atan2(a.hypot(b))]
}
SurfaceEval::Torus { axes, major, .. } => {
SurfaceEval::Torus { axes, major, tube, .. } => {
let [a, b, h] = axes.local(p);
let v = h.atan2(a.hypot(b) - major);
[angle(a, b, near.map(|n| n[0])), if v < 0.0 { v + TAU } else { v }]
let v = (h.atan2(a.hypot(b) - major) - tube).rem_euclid(TAU);
[angle(a, b, near.map(|n| n[0])), v]
}
};
(q[0].is_finite() && q[1].is_finite() && self.point(q).is_some_and(|s| s.dist(p) <= accept)).then_some(q)
+88 -6
View File
@@ -10,7 +10,8 @@
//! A form spans the quarters its interval needs (at most five for one turn), so an arc across the
//! frame's x axis has a form like any other. A face of five would cover part of its surface twice:
//! its frame is turned about its axis first ([`turned`], [`turn_quarters`]), so that it starts on
//! a quarter line and its form spans four.
//! a quarter line and its form spans four; a torus face's tube likewise starts from its tube start
//! ([`tube_turned`], [`tube_quarters`]).
//!
//! A circle or ellipse is the arcs (an ellipse scaled by its radii, which keeps it exact: its
//! parameter is the eccentric angle). A cylinder or cone is the arcs times a degree-1 line along the
@@ -202,13 +203,35 @@ pub fn turned(s: &Surface, angle: f64) -> Option<Surface> {
Surface::Cone { frame: turn(frame)?, radius_at_origin: *radius_at_origin, half_angle: *half_angle }
}
Surface::Sphere { frame, radius } => Surface::Sphere { frame: turn(frame)?, radius: *radius },
Surface::Torus { frame, major_radius, minor_radius } => {
Surface::Torus { frame: turn(frame)?, major_radius: *major_radius, minor_radius: *minor_radius }
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Surface::Torus { frame: turn(frame)?, major_radius: *major_radius, minor_radius: *minor_radius, tube_start: *tube_start }
}
_ => return None,
})
}
/// How many quarters a torus face over `[u, v]` (its box on the label, as for
/// [`surface_within`]) spans along its tube on the label's quarter grid ([`turn_quarters`] for
/// the tube angle): five when the face does not start on a quarter line of the tube and more
/// than three quarters are left to it. [`tube_turned`] puts its start on one. `None` for another
/// surface.
pub fn tube_quarters(s: &Surface, [_, v]: [[f64; 2]; 2], tolerance: f64) -> Option<usize> {
if !matches!(s, Surface::Torus { .. }) {
return None;
}
Some(face_quarters(v, reaches(s, v, tolerance).1)?.0.1)
}
/// A torus with its tube start moved on by `angle`: the same surface, its tube angle parameter
/// `angle` less at every point. `None` for another surface.
pub fn tube_turned(s: &Surface, angle: f64) -> Option<Surface> {
let Surface::Torus { frame, major_radius, minor_radius, tube_start } = s else { return None };
// Kept within one turn either way, as a record's tube start is.
let start = tube_start + angle;
let start = if start.abs() > TAU { start % TAU } else { start };
Some(Surface::Torus { frame: frame.clone(), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: start })
}
/// [`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
@@ -255,10 +278,12 @@ pub fn surface_within(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Opt
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 } => {
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
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))?;
// The tube's arcs turned on by its start (a rotation of the control points, exact).
let (ts, tc) = tube_start.sin_cos();
let homogeneous = net(frame, &tube, &|[c, s]| (major_radius + minor_radius * (c * tc - s * ts), minor_radius * (s * tc + c * ts)))?;
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, fv]))
}
Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => {
@@ -319,6 +344,46 @@ pub fn on_frame(c: &Curve, [t0, t1]: [f64; 2], frame: &Frame, tol: f64) -> Optio
Some((Curve::Circle { frame: relabel, radius: *radius }, [t0 + shift, t1 + shift]))
}
/// A circle edge round a torus's tube (in a plane through the axis, centred on the ring, of the
/// tube's radius, all within `tol`): the same circle, its angle measured from the tube direction
/// at the torus's tube start (its y axis taken so the circle keeps its own direction), and its
/// interval shifted to match, so its form lies on the torus's quarter grid along the tube. `None`
/// for another circle or surface.
pub fn on_tube(c: &Curve, [t0, t1]: [f64; 2], torus: &Surface, tol: f64) -> Option<(Curve, [f64; 2])> {
let (Curve::Circle { frame: own, radius }, Surface::Torus { frame, major_radius, minor_radius, tube_start }) = (c, torus) else {
return None;
};
let (o, x, _, z) = axes(own)?;
let (fo, _, _, fz) = axes(frame)?;
// The ring's direction at the circle's centre, and the centre on the ring.
let d = o - fo;
let radial = (d - fz * d.dot(fz)).normalized()?;
let on_ring = (o - (fo + radial * *major_radius)).len() <= tol;
if !(on_ring && (radius - minor_radius).abs() <= tol && z.dot(fz).abs() <= 1e-9 && z.dot(radial).abs() <= 1e-9) {
return None;
}
let (s, cs) = tube_start.sin_cos();
let nx = (radial * cs + fz * s).normalized()?;
let ny = z.cross(nx);
let shift = x.dot(ny).atan2(x.dot(nx));
let relabel = Frame { origin: [o.x, o.y, o.z], x_axis: [nx.x, nx.y, nx.z], y_axis: [ny.x, ny.y, ny.z] };
Some((Curve::Circle { frame: relabel, radius: *radius }, [t0 + shift, t1 + shift]))
}
/// Whether a circle round a torus's tube that is about `frame`'s axis (within `tol`) has one
/// quarter grid on both: its angle from `frame` ([`on_frame`]) and from the tube start
/// ([`on_tube`]) differ by whole quarters to rounding, so its form and both faces' lie on one
/// grid. `None` when it is not such a circle.
pub fn tube_on_grid(c: &Curve, interval: [f64; 2], frame: &Frame, torus: &Surface, tol: f64) -> Option<bool> {
let (Curve::Circle { frame: a, .. }, _) = on_frame(c, interval, frame, tol)? else { return None };
let (Curve::Circle { frame: b, .. }, _) = on_tube(c, interval, torus, tol)? else { return None };
let (_, ax, _, _) = axes(&a)?;
let (_, bx, by, _) = axes(&b)?;
let turn = ax.dot(by).atan2(ax.dot(bx));
let off = turn - (turn / FRAC_PI_2).round() * FRAC_PI_2;
Some(off.abs() <= 1e-9)
}
/// The frame of a cylinder, cone, sphere or torus, or of a placed revolution (its axis, angle
/// zero at its profile: [`revolution_frame`]); `None` for another surface.
pub fn frame_of(s: &Surface) -> Option<Frame> {
@@ -383,7 +448,24 @@ pub fn edge<'a>(
frames: impl IntoIterator<Item = &'a Frame>,
tol: f64,
) -> Option<(Curve, [f64; 2], Option<(Curve, [f64; 2])>)> {
let (label, iv) = frames.into_iter().find_map(|f| on_frame(c, interval, f, tol)).unwrap_or_else(|| (c.clone(), interval));
edge_on(c, interval, frames, &[], tol)
}
/// [`edge`], with the torus faces the edge bounds: a circle round one of their tubes that is
/// about none of `frames`' axes is labelled from that torus's tube start ([`on_tube`]), so its
/// form lies on the face's quarter grid along the tube.
pub fn edge_on<'a>(
c: &Curve,
interval: [f64; 2],
frames: impl IntoIterator<Item = &'a Frame>,
tori: &[Surface],
tol: f64,
) -> Option<(Curve, [f64; 2], Option<(Curve, [f64; 2])>)> {
let (label, iv) = frames
.into_iter()
.find_map(|f| on_frame(c, interval, f, tol))
.or_else(|| tori.iter().find_map(|t| on_tube(c, interval, t, tol)))
.unwrap_or_else(|| (c.clone(), interval));
let form = match label {
Curve::Circle { .. } | Curve::Ellipse { .. } => Some(curve(&label, iv)?),
_ => None,
+6 -8
View File
@@ -396,10 +396,7 @@ impl On {
let h = axes.local(c.o)[2];
let v = match &self.surface {
SurfaceEval::Sphere { .. } => h.atan2(*radius),
SurfaceEval::Torus { major, .. } => {
let v = h.atan2(radius - major);
if v < 0.0 { v + std::f64::consts::TAU } else { v }
}
SurfaceEval::Torus { major, tube, .. } => (h.atan2(radius - major) - tube).rem_euclid(std::f64::consts::TAU),
_ => h,
};
Some(segment([u0 + sign * t0, v], [u0 + sign * t1, v]))
@@ -421,9 +418,10 @@ impl On {
/// angle from the outward radial toward the axis, `α + s·t` (`s` ±1 as the circle runs).
/// `None` for any other circle.
fn meridian(&self, axes: &super::eval::Axes, c: &super::eval::Axes, radius: f64, curve: &CurveEval, [t0, t1]: [f64; 2]) -> Option<Curve2> {
let ring = match &self.surface {
SurfaceEval::Sphere { radius: r, .. } => (0.0, *r),
SurfaceEval::Torus { major, minor, .. } => (*major, *minor),
// The ring's radius, the circle's, and the tube angle at v = 0.
let (ring, tube) = match &self.surface {
SurfaceEval::Sphere { radius: r, .. } => ((0.0, *r), 0.0),
SurfaceEval::Torus { major, minor, tube, .. } => ((*major, *minor), *tube),
_ => return None,
};
let size = 1.0 + ring.0 + ring.1;
@@ -442,7 +440,7 @@ impl On {
}
let s = c.z.dot(radial.cross(axes.z)).signum();
let d = m - centre;
let vm = d.dot(axes.z).atan2(d.dot(radial));
let vm = d.dot(axes.z).atan2(d.dot(radial)) - tube;
let alpha = vm - s * tm;
Some(Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![[u0, alpha + s * t0], [u0, alpha + s * t1]] })
}
+9 -4
View File
@@ -95,7 +95,12 @@ fn circles_about_the_axis_are_u_lines() {
(Surface::Cone { frame: about_z(o, false), radius_at_origin: 2., half_angle: half }, 4., 2. + 4. * half.tan(), 4.),
(Surface::Sphere { frame: about_z(o, false), radius: 5. }, 3., 4., 3f64.atan2(4.)),
(Surface::Sphere { frame: about_z(o, false), radius: 5. }, -3., 4., (-3f64).atan2(4.)),
(Surface::Torus { frame: about_z(o, false), major_radius: 10., minor_radius: 2. }, 2f64.sqrt(), 10. - 2f64.sqrt(), 0.75 * PI),
(
Surface::Torus { frame: about_z(o, false), major_radius: 10., minor_radius: 2., tube_start: 0.0 },
2f64.sqrt(),
10. - 2f64.sqrt(),
0.75 * PI,
),
];
for (surface, h, r, v) in cases {
for down in [false, true] {
@@ -326,7 +331,7 @@ fn a_seam_from_pole_to_pole_encloses_the_whole_sphere() {
fn a_torus_bounded_by_its_seams_spans_both_periods() {
let frames = [about_z([0.; 3], false), frame([1., 2., 3.], [0., 0.6, 0.8], [0., -0.8, 0.6])];
for sf in frames {
let torus = Surface::Torus { frame: sf.clone(), major_radius: 10., minor_radius: 2. };
let torus = Surface::Torus { frame: sf.clone(), major_radius: 10., minor_radius: 2., tube_start: 0.0 };
let ax = crate::brep::eval::Axes::new(&sf);
let p = |v: Vec3| [v.x, v.y, v.z];
// The outer equator (v = 0, u from 0) and the tube circle at u = 0 (v from 0, upwards).
@@ -398,7 +403,7 @@ fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
// Each surface and circle on its NURBS form (`form`), as SolidCraft's builders give them.
let on = |c: &Curve, iv: [f64; 2]| crate::brep::form::curve(c, iv).unwrap();
// The torus band: the tube's quarter from its outer equator to its top.
let torus = Surface::Torus { frame: about_z([0.; 3], false), major_radius: big, minor_radius: small };
let torus = Surface::Torus { frame: about_z([0.; 3], false), major_radius: big, minor_radius: small, tube_start: 0.0 };
let (band, _) = crate::brep::form::surface(&torus, [full, [0., FRAC_PI_2]]).unwrap();
let outer = on(&Curve::Circle { frame: about_z([0.; 3], false), radius: big + small }, full);
let top = on(&Curve::Circle { frame: about_z([0., 0., small], false), radius: big }, full);
@@ -606,7 +611,7 @@ fn hostile_records_give_errors() {
};
assert!(pcurves(&cylinder, &one(&touching, [0., 1.]), 1e-6).is_err());
// Extreme but valid: a torus as wide as the schema allows and thin as it allows.
let torus = Surface::Torus { frame: about_z([0.; 3], false), major_radius: 1e12, minor_radius: 1e-12 };
let torus = Surface::Torus { frame: about_z([0.; 3], false), major_radius: 1e12, minor_radius: 1e-12, tube_start: 0.0 };
let far = Curve::Circle { frame: about_z([0.; 3], false), radius: 1e12 };
let _ = pcurves(&torus, &one(&far, [0., 1.]), 1e-6);
}
+2 -2
View File
@@ -96,8 +96,8 @@ impl Rigid {
Surface::Cone { frame: self.frame(frame), radius_at_origin: *radius_at_origin, half_angle: *half_angle }
}
Surface::Sphere { frame, radius } => Surface::Sphere { frame: self.frame(frame), radius: *radius },
Surface::Torus { frame, major_radius, minor_radius } => {
Surface::Torus { frame: self.frame(frame), major_radius: *major_radius, minor_radius: *minor_radius }
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Surface::Torus { frame: self.frame(frame), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: *tube_start }
}
Surface::Spline { u_knots, v_knots, points } => Surface::Spline {
u_knots: u_knots.clone(),
+9 -5
View File
@@ -220,8 +220,12 @@ fn one_face(surface: Surface, domain: [[f64; 2]; 2], edges: Vec<(Curve, [f64; 2]
/// at u = 0, a seam used twice, each on its form.
fn closed_band() -> Brep {
let (big, small) = (5., 1.);
let torus =
Surface::Torus { frame: Frame { origin: [0., 0., 0.], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] }, major_radius: big, minor_radius: small };
let torus = Surface::Torus {
frame: Frame { origin: [0., 0., 0.], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] },
major_radius: big,
minor_radius: small,
tube_start: 0.0,
};
let about = |z: f64| Frame { origin: [0., 0., z], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] };
let quarter = Curve::Circle { frame: Frame { origin: [big, 0., 0.], x_axis: [1., 0., 0.], y_axis: [0., 0., 1.] }, radius: small };
one_face(
@@ -788,7 +792,7 @@ fn analytic_records_are_unbounded_with_independent_frames() {
Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: 0.2 },
Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: -0.2 },
Surface::Sphere { frame: f.clone(), radius: 2. },
Surface::Torus { frame: f.clone(), major_radius: 4., minor_radius: 1. },
Surface::Torus { frame: f.clone(), major_radius: 4., minor_radius: 1., tube_start: 0.0 },
];
// A cone narrowing along z (a negative half angle, as SMLib makes them) lies below its apex.
let apex = 2. / 0.2f64.tan();
@@ -815,7 +819,7 @@ fn analytic_records_are_unbounded_with_independent_frames() {
assert!(frame(&bad, &mut Budget(0)).is_err());
assert!(interval(&[2., 1.], UNBOUNDED).is_err());
assert!(radius(f64::NAN).is_err());
assert!(surface(&Surface::Torus { frame: a_frame(), major_radius: 1., minor_radius: 2. }, 0, &mut Budget(0)).is_err());
assert!(surface(&Surface::Torus { frame: a_frame(), major_radius: 1., minor_radius: 2., tube_start: 0.0 }, 0, &mut Budget(0)).is_err());
}
/// A cylinder face round its axis with a hole beside its seam: the seam at 350 degrees (so its
@@ -883,7 +887,7 @@ fn analytic_partials_are_the_derivatives_of_the_order_below() {
Surface::Cylinder { frame: f.clone(), radius: 3. },
Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: 0.3 },
Surface::Sphere { frame: f.clone(), radius: 4. },
Surface::Torus { frame: f.clone(), major_radius: 5., minor_radius: 1.5 },
Surface::Torus { frame: f.clone(), major_radius: 5., minor_radius: 1.5, tube_start: 0.0 },
];
let (u, v, h) = (0.8, 0.4, 1e-4);
let close = |a: crate::Vec3, b: crate::Vec3, tol: f64| a.dist(b) <= tol * (1.0 + a.len().max(b.len()));
+6 -3
View File
@@ -384,13 +384,16 @@ fn iso_record(surface: &brep::Surface, along_u: bool, fixed: f64, (a, b): (f64,
circle(o, x * fixed.cos() + y * fixed.sin(), z, *radius)
}
}
Surface::Torus { frame: f, major_radius, minor_radius } => {
Surface::Torus { frame: f, major_radius, minor_radius, tube_start } => {
let (o, x, y, z) = frame(f);
if along_u {
circle(o + z * (minor_radius * fixed.sin()), x, y, major_radius + minor_radius * fixed.cos())
let w = fixed + tube_start;
circle(o + z * (minor_radius * w.sin()), x, y, major_radius + minor_radius * w.cos())
} else {
// The tube circle from its start, so its angle is v.
let dir = x * fixed.cos() + y * fixed.sin();
circle(o + dir * *major_radius, dir, z, *minor_radius)
let (s, c) = tube_start.sin_cos();
circle(o + dir * *major_radius, dir * c + z * s, z * c - dir * s, *minor_radius)
}
}
Surface::Spline { u_knots, v_knots, points } => {
+1 -1
View File
@@ -54,7 +54,7 @@ pub(super) fn load_analytic(surface: b::Surface, height: [f64; 2]) -> Result<mt:
let meridian = b::Frame { origin: frame.origin, x_axis: frame.x_axis, y_axis: vector(z) };
revolved(arc(&meridian, [radius; 2], [-FRAC_PI_2, FRAC_PI_2])?.inverse(), p(frame.origin), z, false)
}
b::Surface::Torus { frame, major_radius, minor_radius } => {
b::Surface::Torus { frame, major_radius, minor_radius, .. } => {
let z = axis(&frame);
let profile = b::Frame { origin: point(p(frame.origin) + v(frame.x_axis) * major_radius), x_axis: frame.x_axis, y_axis: vector(z) };
revolved(arc(&profile, [minor_radius; 2], [0., TAU])?.inverse(), p(frame.origin), z, false)
+1 -1
View File
@@ -61,7 +61,7 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
let a = if *half_angle > 0.0 { z } else { -z };
Surf::Cone { v: apex, a, half: half_angle.abs(), convex: out }
}
Surface::Torus { frame, major_radius, minor_radius } => {
Surface::Torus { frame, major_radius, minor_radius, .. } => {
let (o, a) = axes(frame)?;
Surf::Torus { o, a, big: *major_radius, h: 0.0, r: *minor_radius, convex: out }
}
+177
View File
@@ -2583,3 +2583,180 @@ fn a_circle_edge_between_two_vertices_closer_than_the_tolerance_is_the_arc_betwe
assert!(rel(m.volume, pi * r * r * h) < 1e-6, "{}", m.volume);
assert!(rel(m.area, 2.0 * pi * r * r + 2.0 * pi * r * h) < 1e-6, "{}", m.area);
}
/// A ring torus (R 10, r 3) about z, as a closed solid: with `tube` its whole turn about the axis
/// and its tube from `tube.0` for `tube.1` radians, the gap across the tube closed by a cone
/// through the two circles where the tube ends (face #53, its seam a tube arc, the cone #57);
/// without it the whole tube for `sweep` radians from `start` about the axis, closed by two discs
/// (its seam an arc of the outer equator).
fn torus_part(tube: Option<(f64, f64)>, (start, sweep): (f64, f64)) -> String {
let (big, small) = (10.0, 3.0);
let p = |x: f64, y: f64, z: f64| format!("({x:.17},{y:.17},{z:.17})");
let head = format!(
"{HEAD}#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=DIRECTION('',(0.,0.,1.));\n#3=DIRECTION('',(1.,0.,0.));\n#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);\n#50=TOROIDAL_SURFACE('',#4,{big:?},{small:?});\n"
);
let tail = "#80=CLOSED_SHELL('',(#53,#57,#61));\n#81=MANIFOLD_SOLID_BREP('Torus part',#80);\n#82=SHAPE_REPRESENTATION('',(#81),$);\n";
if let Some((v0, dv)) = tube {
// The circles about the axis at the tube's two ends, and the cone through them from the
// narrower one, its axis toward the wider; its sense outward from the tube.
let v1 = v0 + dv;
let ring = |v: f64| (big + small * v.cos(), small * v.sin());
let ((ra, za), (rb, zb)) = (ring(v0), ring(v1));
let ((r_near, z_near), (r_far, z_far)) = if ra < rb { ((ra, za), (rb, zb)) } else { ((rb, zb), (ra, za)) };
let k = (z_far - z_near).signum();
let semi = ((r_far - r_near) / (z_far - z_near).abs()).atan();
// The cone's natural normal at u = 0, (1, 0, −k tan) in (radial, axial), against the way
// out of the tube at the chord's middle.
let (mx, mz) = (0.5 * (ra + rb) - big, 0.5 * (za + zb));
let sense = if mx - k * semi.tan() * mz > 0.0 { "T" } else { "F" };
format!(
"{head}#5=DIRECTION('',(0.,0.,{k:.1}));
#6=CARTESIAN_POINT('',{});
#7=AXIS2_PLACEMENT_3D('',#6,#2,#3);
#8=CARTESIAN_POINT('',{});
#9=AXIS2_PLACEMENT_3D('',#8,#2,#3);
#10=VERTEX_POINT('',#11);
#11=CARTESIAN_POINT('',{});
#12=VERTEX_POINT('',#13);
#13=CARTESIAN_POINT('',{});
#30=CIRCLE('',#7,{ra:.17});
#31=CIRCLE('',#9,{rb:.17});
#40=EDGE_CURVE('',#10,#10,#30,.T.);
#41=EDGE_CURVE('',#12,#12,#31,.T.);
#14=CARTESIAN_POINT('',({big:?},0.,0.));
#15=DIRECTION('',(0.,-1.,0.));
#16=AXIS2_PLACEMENT_3D('',#14,#15,#3);
#32=CIRCLE('',#16,{small:?});
#42=EDGE_CURVE('',#10,#12,#32,.T.);
#200=ORIENTED_EDGE('',*,*,#40,.T.);
#201=ORIENTED_EDGE('',*,*,#41,.F.);
#204=ORIENTED_EDGE('',*,*,#42,.T.);
#205=ORIENTED_EDGE('',*,*,#42,.F.);
#51=EDGE_LOOP('',(#200,#204,#201,#205));
#54=FACE_OUTER_BOUND('',#51,.T.);
#53=ADVANCED_FACE('Tube',(#54),#50,.T.);
#202=ORIENTED_EDGE('',*,*,#40,.F.);
#203=ORIENTED_EDGE('',*,*,#41,.T.);
#56=EDGE_LOOP('',(#202));
#58=EDGE_LOOP('',(#203));
#59=FACE_OUTER_BOUND('',#56,.T.);
#60=FACE_BOUND('',#58,.T.);
#62=CARTESIAN_POINT('',{});
#63=AXIS2_PLACEMENT_3D('',#62,#5,#3);
#64=CONICAL_SURFACE('',#63,{r_near:.17},{semi:.17});
#57=ADVANCED_FACE('Gap',(#59,#60),#64,.{sense}.);
#80=CLOSED_SHELL('',(#53,#57));
#81=MANIFOLD_SOLID_BREP('Torus part',#80);
#82=SHAPE_REPRESENTATION('',(#81),$);
{TAIL}",
p(0.0, 0.0, za),
p(0.0, 0.0, zb),
p(ra, 0.0, za),
p(rb, 0.0, zb),
p(0.0, 0.0, z_near),
)
} else {
// A tube circle in each end's meridian plane, run with the tube angle, and the discs.
let end = |u: f64| {
let (c, s) = (u.cos(), u.sin());
((big * c, big * s), (c, s), (s, -c))
};
let (((x0, y0), (c0, s0), (a0x, a0y)), ((x1, y1), (c1, s1), (a1x, a1y))) = (end(start), end(start + sweep));
format!(
"{head}#6=CARTESIAN_POINT('',{});
#7=DIRECTION('',{});
#8=DIRECTION('',{});
#9=AXIS2_PLACEMENT_3D('',#6,#7,#8);
#14=CARTESIAN_POINT('',{});
#15=DIRECTION('',{});
#16=DIRECTION('',{});
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
#10=VERTEX_POINT('',#11);
#11=CARTESIAN_POINT('',{});
#12=VERTEX_POINT('',#13);
#13=CARTESIAN_POINT('',{});
#30=CIRCLE('',#9,{small:?});
#31=CIRCLE('',#17,{small:?});
#40=EDGE_CURVE('',#10,#10,#30,.T.);
#41=EDGE_CURVE('',#12,#12,#31,.T.);
#32=CIRCLE('',#4,{:.17});
#43=EDGE_CURVE('',#10,#12,#32,.T.);
#200=ORIENTED_EDGE('',*,*,#40,.F.);
#201=ORIENTED_EDGE('',*,*,#41,.T.);
#204=ORIENTED_EDGE('',*,*,#43,.T.);
#205=ORIENTED_EDGE('',*,*,#43,.F.);
#51=EDGE_LOOP('',(#204,#201,#205,#200));
#54=FACE_OUTER_BOUND('',#51,.T.);
#53=ADVANCED_FACE('Tube',(#54),#50,.T.);
#202=ORIENTED_EDGE('',*,*,#40,.T.);
#56=EDGE_LOOP('',(#202));
#59=FACE_OUTER_BOUND('',#56,.T.);
#64=PLANE('',#9);
#57=ADVANCED_FACE('Start',(#59),#64,.T.);
#203=ORIENTED_EDGE('',*,*,#41,.F.);
#58=EDGE_LOOP('',(#203));
#60=FACE_OUTER_BOUND('',#58,.T.);
#65=DIRECTION('',{});
#66=AXIS2_PLACEMENT_3D('',#14,#65,#16);
#67=PLANE('',#66);
#61=ADVANCED_FACE('End',(#60),#67,.T.);
{tail}{TAIL}",
p(x0, y0, 0.0),
p(a0x, a0y, 0.0),
p(c0, s0, 0.0),
p(x1, y1, 0.0),
p(a1x, a1y, 0.0),
p(c1, s1, 0.0),
p(x0 + small * c0, y0 + small * s0, 0.0),
p(x1 + small * c1, y1 + small * s1, 0.0),
big + small,
p(-a1x, -a1y, 0.0),
)
}
}
/// A torus face round most of its tube off the tube's quarter lines (326° of the tube from
/// −113.8°, as an exhaust collector has it) needed a fifth quarter along the tube: its form ran
/// past a full turn of the tube and SMLib refused the solid. Its tube start moves to where the
/// face's tube begins: the same torus, a form of four quarters along the tube, and the solid with
/// its exact volume and area. A face round the whole tube (300° about the axis from 75°, which
/// turns its frame instead) keeps its tube start and its four quarters.
#[test]
fn a_torus_face_round_most_of_its_tube_is_formed_within_one_turn_of_it() {
use std::f64::consts::PI;
let (big, small) = (10.0f64, 3.0f64);
let (v0, dv) = ((-113.8f64).to_radians(), 326f64.to_radians());
for (tube, about) in [(Some((v0, dv)), (0.0, 2.0 * PI)), (None, (75f64.to_radians(), 300f64.to_radians()))] {
let imp = step_import(&torus_part(tube, about)).unwrap();
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
let b = &imp.bodies[0];
assert!(b.closed && b.refused.is_none(), "{tube:?}: {:?} {:?}", b.refused, imp.warnings);
let face = b.face_origin.iter().position(|f| *f == 53).unwrap();
let record = b.body.record();
let Some(solidcraft_geom::brep::Surface::RationalSpline { v_knots, .. }) = &record.faces[face].nurbs else { panic!("the tube's form") };
assert!(v_knots[v_knots.len() - 1] - v_knots[0] <= 4.0, "{tube:?}: the tube's form spans {v_knots:?}");
let m = measure(&b.body).unwrap();
let (volume, area) = match tube {
Some((v0, dv)) => {
// The whole torus less the segment of the tube the cone cuts off, revolved
// (Pappus); the tube's face and the cone's.
let gap = 2.0 * PI - dv;
let segment = 0.5 * small * small * (gap - gap.sin());
let centroid = 4.0 * small * (0.5 * gap).sin().powi(3) / (3.0 * (gap - gap.sin()));
let middle = v0 + dv + 0.5 * gap;
let volume = 2.0 * PI * PI * big * small * small - segment * 2.0 * PI * (big + centroid * middle.cos());
let ring = |v: f64| big + small * v.cos();
let chord = 2.0 * small * (0.5 * gap).sin();
let area = 2.0 * PI * small * (big * dv + small * ((v0 + dv).sin() - v0.sin())) + PI * (ring(v0) + ring(v0 + dv)) * chord;
(volume, area)
}
None => {
assert_eq!(v_knots[v_knots.len() - 1] - v_knots[0], 4.0, "a whole tube is four quarters");
let sweep = about.1;
(PI * small * small * big * sweep, sweep * 2.0 * PI * small * big + 2.0 * PI * small * small)
}
};
assert!(rel(m.volume, volume) < 1e-6, "{tube:?}: volume {} vs {volume}", m.volume);
assert!(rel(m.area, area) < 1e-6, "{tube:?}: area {} vs {area}", m.area);
}
}
+9 -6
View File
@@ -96,9 +96,10 @@ pub(crate) fn eval_surface(surface: &brep::Surface, domain: [[f64; 2]; 2]) -> Re
Ok(EvalCurve { degree: 1, knots: vec![-h1, -h1, -h0, -h0], ctrl: vec![[a.x, a.y, a.z, 1.0], [b.x, b.y, b.z, 1.0]] })
};
// A meridian from the record's least angle to its greatest, reversed (so the normal is the
// record's natural one, outward).
let meridian = |centre: Vec3, x: Vec3, z: Vec3, r: f64, angle: [f64; 2], o: Vec3| -> Result<(EvalSurface, Params)> {
let (curve, spans) = arc(centre, x, z, r, angle);
// record's natural one, outward), its angle measured from `x` toward `y` in the plane of `x`
// and the axis `z`.
let meridian = |centre: Vec3, [x, y]: [Vec3; 2], z: Vec3, r: f64, angle: [f64; 2], o: Vec3| -> Result<(EvalSurface, Params)> {
let (curve, spans) = arc(centre, x, y, r, angle);
Ok((revolved(curve.reversed(), o, z)?, Params::Meridian(spans)))
};
match surface {
@@ -113,11 +114,13 @@ pub(crate) fn eval_surface(surface: &brep::Surface, domain: [[f64; 2]; 2]) -> Re
}
brep::Surface::Sphere { frame, radius } => {
let (o, x, z) = axes(frame);
meridian(o, x, z, *radius, [-FRAC_PI_2, FRAC_PI_2], o)
meridian(o, [x, z], z, *radius, [-FRAC_PI_2, FRAC_PI_2], o)
}
brep::Surface::Torus { frame, major_radius, minor_radius } => {
brep::Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
let (o, x, z) = axes(frame);
meridian(o + x * *major_radius, x, z, *minor_radius, [0.0, TAU], o)
// The tube angle counts from its start.
let (s, c) = tube_start.sin_cos();
meridian(o + x * *major_radius, [x * c + z * s, z * c - x * s], z, *minor_radius, [0.0, TAU], o)
}
_ => match SurfaceEval::new(surface) {
Ok(SurfaceEval::Eval(e, _)) => Ok((e, Params::Same)),
+2
View File
@@ -398,6 +398,8 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
),
major_radius: at(flat.f(FACE_TORUS_MAJOR_RADIUS), i, "torus")?,
minor_radius: at(flat.f(FACE_TORUS_MINOR_RADIUS), i, "torus")?,
// SMLib's torus has no tube start: the face's form carries where its tube begins.
tube_start: 0.0,
},
SURFACE_NURBS => next_nurbs(&mut nurbs_at)?,
other => return Err(unsupported(format!("SMLib surface kind {other}"))),
+9 -2
View File
@@ -195,7 +195,14 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32,
nurbs_block(&mut a, n)?;
true
}
None => surface(&mut a, &f.surface, f.domain, analytic)?,
// SMLib's tube angle counts from the outer equator, the record's from its tube start.
None => match f.surface {
b::Surface::Torus { tube_start, .. } => {
let [u, v] = f.domain;
surface(&mut a, &f.surface, [u, v.map(|t| t + tube_start)], analytic)?
}
_ => surface(&mut a, &f.surface, f.domain, analytic)?,
},
};
if any_form {
a.i(flat::FACE_NURBS_FORM).push(i64::from(f.nurbs.is_some() && !form_is_face));
@@ -358,7 +365,7 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_plane
analytic(a, flat::SURFACE_SPHERE, [flat::FACE_SPHERE_CENTER, flat::FACE_SPHERE_AXIS, flat::FACE_SPHERE_REF_DIRECTION], frame);
a.r(flat::FACE_SPHERE_RADIUS).push(*radius);
}
b::Surface::Torus { frame, major_radius, minor_radius } => {
b::Surface::Torus { frame, major_radius, minor_radius, .. } => {
analytic(a, flat::SURFACE_TORUS, [flat::FACE_TORUS_ORIGIN, flat::FACE_TORUS_AXIS, flat::FACE_TORUS_REF_DIRECTION], frame);
a.r(flat::FACE_TORUS_MAJOR_RADIUS).push(*major_radius);
a.r(flat::FACE_TORUS_MINOR_RADIUS).push(*minor_radius);
+5 -5
View File
@@ -337,7 +337,7 @@ impl Revolution {
if !(major > radius + slack) {
return Err(NOT_ANALYTIC);
}
let torus = b::Surface::Torus { frame: frame(foot), major_radius: major, minor_radius: radius };
let torus = b::Surface::Torus { frame: frame(foot), major_radius: major, minor_radius: radius, tube_start: 0.0 };
Ok((torus, [angle, sorted(first, last)]))
}
}
@@ -358,9 +358,9 @@ fn analytic_uv(s: &b::Surface, p: Vec3, near: [f64; 2]) -> Option<[f64; 2]> {
let q = p - v3(*origin);
return Some([q.dot(v3(*u_axis)), q.dot(v3(*v_axis))]);
}
let (frame, round) = match s {
b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } => (frame, 0.0),
b::Surface::Torus { frame, major_radius, .. } => (frame, *major_radius),
let (frame, round, tube) = match s {
b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } => (frame, 0.0, 0.0),
b::Surface::Torus { frame, major_radius, tube_start, .. } => (frame, *major_radius, *tube_start),
_ => return None,
};
let (x, y) = (v3(frame.x_axis), v3(frame.y_axis));
@@ -371,7 +371,7 @@ fn analytic_uv(s: &b::Surface, p: Vec3, near: [f64; 2]) -> Option<[f64; 2]> {
let r = qx.hypot(qy);
Some(match s {
b::Surface::Sphere { .. } => [u, qz.atan2(r)],
b::Surface::Torus { .. } => [u, unwrap(qz.atan2(r - round), near[1])],
b::Surface::Torus { .. } => [u, unwrap(qz.atan2(r - round) - tube, near[1])],
_ => [u, qz],
})
}
+3 -2
View File
@@ -117,8 +117,9 @@ impl Surface {
Surface::cone(apex, frame.x_axis, frame.y_axis, half_angle.to_degrees(), [(v[0] - at).max(0.0), v[1] - at])
}
S::Sphere { frame, radius } => Surface::sphere(frame.origin, frame.x_axis, frame.y_axis, *radius, deg(u), deg(v)),
S::Torus { frame, major_radius, minor_radius } => {
Surface::torus(frame.origin, frame.x_axis, frame.y_axis, *major_radius, *minor_radius, deg(u), deg(v))
S::Torus { frame, major_radius, minor_radius, tube_start } => {
// SMLib's tube angle counts from the outer equator.
Surface::torus(frame.origin, frame.x_axis, frame.y_axis, *major_radius, *minor_radius, deg(u), deg(v.map(|a| a + tube_start)))
}
S::Spline { u_knots, v_knots, points } => {
let (nu, nv) = (points.len(), points.first().map_or(0, Vec::len));
+8 -2
View File
@@ -962,22 +962,28 @@ impl<'b, 'a> Builder<'b, 'a> {
self.turn_frames()?;
// Each edge's label and NURBS form: a circle about a revolved face's axis labelled on that
// face's frame, so its form lies on the face's quarter grid.
// A circle round a torus face's tube is labelled from its tube start likewise.
let mut frames: Vec<Vec<brep::Frame>> = vec![Vec::new(); self.edges.len()];
let mut tori: Vec<Vec<BSurface>> = vec![Vec::new(); self.edges.len()];
for f in &self.faces {
let Some(frame) = brep::form::frame_of(&f.surface) else { continue };
let torus = matches!(f.surface, BSurface::Torus { .. });
for l in &f.loops {
if let LoopB::Edges(es) = l {
for (e, _) in es {
if let Some(slot) = frames.get_mut(*e as usize) {
slot.push(frame.clone());
}
if torus && let Some(slot) = tori.get_mut(*e as usize) {
slot.push(f.surface.clone());
}
}
}
}
}
for (edge, fr) in self.edges.iter_mut().zip(&frames) {
for ((edge, fr), tr) in self.edges.iter_mut().zip(&frames).zip(&tori) {
let (label, interval, form) =
brep::form::edge(&edge.curve, edge.interval, fr, self.tol).ok_or_else(|| unsupported("an arc of more than one turn"))?;
brep::form::edge_on(&edge.curve, edge.interval, fr, tr, self.tol).ok_or_else(|| unsupported("an arc of more than one turn"))?;
(edge.curve, edge.interval, edge.form) = (label, interval, form);
}
// Each face's pcurves; a face whose loops disagree with its same_sense is turned over.
+79 -1
View File
@@ -17,6 +17,15 @@
//! on a quarter line, or where two of them spill, maybe an end); a face
//! whose frame cannot be turned (a placed revolution, its angle zero at its profile) keeps its
//! group as the file has it.
//!
//! A torus face's tube spills the same way along its tube angle, which STEP counts from the outer
//! equator: its tube start moves to where the face's tube begins, or a turn before where it ends
//! (`brep::form::tube_turned`), the same surface. Its circles round the tube are labelled from
//! the tube start (`brep::form::on_tube`), unless one is about another round face's axis (a
//! pipe's cylinder meeting the tube), which labels it on its own grid: the tube start is then
//! the one of the two that puts the tube's grid on that face's.
use std::f64::consts::TAU;
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::eval::CurveEval;
@@ -53,8 +62,77 @@ fn root(parent: &mut [usize], mut i: usize) -> usize {
impl Builder<'_, '_> {
/// Measure every round face's box on its label, and turn the frames of the faces whose
/// forms would run past one turn, with the faces their circle edges join them to, so that
/// each starts on a quarter line. Returns how many faces were turned.
/// each starts on a quarter line; a torus face whose tube would, its tube start too. Returns
/// how many faces were turned.
pub(super) fn turn_frames(&mut self) -> R<usize> {
let about_axis = self.turn_about_axes()?;
Ok(about_axis + self.turn_tubes())
}
/// The torus faces whose forms would run past one turn of the tube start it where their
/// tube begins, or a turn before where it ends (either fits it in four quarters): the one that
/// puts its tube circles on the quarter grid of the round faces they are about the axes of
/// (a pipe's cylinder meeting the tube), so those circles' pcurves are exact on both; where
/// neither does, where it begins.
fn turn_tubes(&mut self) -> usize {
let tol = self.tol;
let frames: Vec<Option<brep::Frame>> = self.faces.iter().map(|f| brep::form::frame_of(&f.surface)).collect();
// The round faces each edge bounds.
let mut bounding: Vec<Vec<usize>> = vec![Vec::new(); self.edges.len()];
for (fi, f) in self.faces.iter().enumerate() {
for l in &f.loops {
if let LoopB::Edges(es) = l {
for (e, _) in es {
if frames.get(fi).is_some_and(Option::is_some)
&& let Some(b) = bounding.get_mut(*e as usize)
&& !b.contains(&fi)
{
b.push(fi);
}
}
}
}
}
let mut turned = 0;
for fi in 0..self.faces.len() {
let Some(face) = self.faces.get(fi) else { continue };
let Some(e) = face.extent else { continue };
if brep::form::tube_quarters(&face.surface, e, tol).is_none_or(|q| q <= 4) {
continue;
}
// Its tube circles about another round face's axis, with that face's frame.
let mut joined = Vec::new();
for l in &face.loops {
let LoopB::Edges(es) = l else { continue };
for (ei, _) in es {
let Some(edge) = self.edges.get(*ei as usize) else { continue };
for other in bounding.get(*ei as usize).into_iter().flatten().filter(|o| **o != fi) {
if let Some(Some(fr)) = frames.get(*other) {
joined.push((&edge.curve, edge.interval, fr));
}
}
}
}
let on_grid = |start: f64| {
brep::form::tube_turned(&face.surface, start)
.is_some_and(|torus| joined.iter().all(|(c, iv, fr)| brep::form::tube_on_grid(c, *iv, fr, &torus, tol).is_none_or(|on| on)))
};
let (begin, end) = (e[1][0], e[1][1] - TAU);
let start = if !on_grid(begin) && on_grid(end) { end } else { begin };
let Some(surface) = brep::form::tube_turned(&face.surface, start) else { continue };
if let Some(face) = self.faces.get_mut(fi) {
face.surface = surface;
if let Some(e) = face.extent.as_mut() {
e[1] = [e[1][0] - start, e[1][1] - start];
}
turned += 1;
}
}
turned
}
/// Measure the round faces' boxes and turn the groups that spill about their axes.
fn turn_about_axes(&mut self) -> R<usize> {
let n = self.faces.len();
let frames: Vec<Option<brep::Frame>> = self.faces.iter().map(|f| brep::form::frame_of(&f.surface)).collect();
for fi in 0..n {
+4 -3
View File
@@ -546,12 +546,12 @@ fn offset(s: BSurface, d: f64) -> R<BSurface> {
}
BSurface::Cone { frame, radius_at_origin: r, half_angle }
}
BSurface::Torus { frame, major_radius, minor_radius } => {
BSurface::Torus { frame, major_radius, minor_radius, tube_start } => {
let r = minor_radius + d;
if !(r > 1e-9 && r < major_radius) {
return bad("torus that collapses or meets its axis");
}
BSurface::Torus { frame, major_radius, minor_radius: r }
BSurface::Torus { frame, major_radius, minor_radius: r, tube_start }
}
_ => return Err(unsupported("an OFFSET_SURFACE of a spline or swept surface")),
})
@@ -611,6 +611,7 @@ fn canonical(s: &Surface, reversed: &mut bool) -> Option<BSurface> {
frame: solidcraft_geom::brep::Frame { origin: p3(foot), x_axis: p3(x), y_axis: p3(k.cross(x)) },
major_radius: big,
minor_radius: *radius,
tube_start: 0.0,
})
}
_ => None,
@@ -641,7 +642,7 @@ fn map_surface_in(s: &Surface, ext: &Extent, tol: f64, depth: usize, reversed: &
if !(major_radius > minor_radius) {
return torus_revolved(position, *major_radius, *minor_radius, ext);
}
BSurface::Torus { frame: frame(position), major_radius: *major_radius, minor_radius: *minor_radius }
BSurface::Torus { frame: frame(position), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: 0.0 }
}
Surface::DegenerateTorus { position, major_radius, minor_radius, .. } => {
return torus_revolved(position, *major_radius, *minor_radius, ext);
+5 -1
View File
@@ -264,7 +264,7 @@ pub fn surface(s: &BSurface) -> R<(Surface, Map)> {
(Surface::Cone { position, radius: *radius_at_origin, semi_angle: half_angle.abs() }, Map { m: [[k, 0.0], [0.0, k]] })
}
BSurface::Sphere { frame, radius } => (Surface::Sphere { position: placement(frame), radius: *radius }, Map::IDENTITY),
BSurface::Torus { frame, major_radius, minor_radius } => {
BSurface::Torus { frame, major_radius, minor_radius, .. } => {
(Surface::Torus { position: placement(frame), major_radius: *major_radius, minor_radius: *minor_radius }, Map::IDENTITY)
}
BSurface::Spline { u_knots, v_knots, points } => (bspline_surface(u_knots, v_knots, points.clone(), None)?, Map::IDENTITY),
@@ -460,6 +460,10 @@ impl BrepWriter<'_, '_, '_> {
// (STEP's are in the angle); only which way its normal points carries over.
let map = match &face.nurbs {
Some(n) => form_map(&face.surface, n, face.domain, map).ok_or_else(|| format!("face {f}: its NURBS form has no normal"))?,
// STEP's torus has no tube start: its pcurves would be off by it.
None if matches!(face.surface, BSurface::Torus { tube_start, .. } if tube_start != 0.0) => {
return Err(format!("face {f}: a torus with a tube start and no NURBS form"));
}
None => map,
};
let s = (Arc::new(s), map);