mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 03:51:08 +00:00
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:
@@ -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
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]] })
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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 } => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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}"))),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user