///|
/// RevSegment is one band of a profile in the (r, z) half-plane that is revolved
/// about the Z axis.
///
/// `r` is the radius (along +X), `z` is the axial coordinate. `RevLine` is a
/// straight band; `RevArc` is a circular arc band defined by its start point,
/// end point, and center. An arc is always the minor arc (sweep at most 180
/// degrees), which makes the trim unambiguous.
pub(all) enum RevSegment {
RevLine(Vec2, Vec2)
RevArc(Vec2, Vec2, Vec2)
}
///|
/// RevolvedProfile is a solid of revolution.
///
/// The profile is a chain of `RevSegment` values in the (r, z) half-plane with
/// `r >= 0`. The chain must either begin and end on the axis (`r = 0`, yielding
/// poles) or close on itself away from the axis (yielding an annular solid such
/// as a washer or a ring). The result is compiled into one closed STEP solid
/// whose lateral faces are true `CYLINDRICAL_SURFACE`, `CONICAL_SURFACE`,
/// `SPHERICAL_SURFACE`, and `TOROIDAL_SURFACE` bands, with planar caps.
pub struct RevolvedProfile {
segments : Array[RevSegment]
/// Optional per-band face colors, indexed by the segment order given to
/// `new`/`polyline`. The compiler styles those faces individually (a STEP
/// face-level `STYLED_ITEM`) so a single solid can carry more than one
/// color, for example a black shield groove on a grey bearing.
band_colors : Array[(Int32, Rgb)]
}
///|
/// RevProfileError reports why a revolved profile cannot be compiled.
pub(all) enum RevProfileError {
EmptyProfile
DiscontinuousChain(Int32)
NegativeRadius(Int32)
ArcEndpointMismatch(Int32)
DegenerateArc(Int32)
NotClosed
DegenerateArea
}
///|
/// RevProfileError::message returns a human-readable diagnostic.
pub fn RevProfileError::message(self : RevProfileError) -> String {
match self {
EmptyProfile => "revolved profile has no segments"
DiscontinuousChain(i) =>
"revolved profile segment \{i} does not start where the previous one ended"
NegativeRadius(i) => "revolved profile point \{i} has a negative radius"
ArcEndpointMismatch(i) =>
"revolved profile arc \{i} does not have equal start/end radii about its center"
DegenerateArc(i) => "revolved profile arc \{i} has a zero radius"
NotClosed =>
"revolved profile must end on the axis or close on itself to bound a solid"
DegenerateArea => "revolved profile encloses zero area"
}
}
///|
/// RevolvedProfile::new stores a chain of segments without validating it.
pub fn RevolvedProfile::new(segments : Array[RevSegment]) -> RevolvedProfile {
{ segments, band_colors: [], }
}
///|
/// RevolvedProfile::with_band_color returns a copy in which the face emitted
/// for segment `band` carries `color`. Band indices follow the segment order
/// passed to `new`/`polyline`; the compiler remaps them if it reverses the
/// profile winding. The flat STEP writer emits a face-level `STYLED_ITEM` for
/// each colored band, so the solid stays one closed B-rep.
pub fn RevolvedProfile::with_band_color(
self : RevolvedProfile,
band : Int32,
color : Rgb,
) -> RevolvedProfile {
let colors : Array[(Int32, Rgb)] = []
let mut replaced = false
for entry in self.band_colors {
let (index, _existing) = entry
if index == band {
colors.push((band, color))
replaced = true
} else {
colors.push(entry)
}
}
if !replaced {
colors.push((band, color))
}
{ ..self, band_colors: colors, }
}
///|
/// RevolvedProfile::emission_colors returns the per-face colors in the order
/// the compiler emits the faces. It applies the same winding normalization as
/// `compile_revolved_profile`, so callers do not have to know whether the
/// profile was reversed.
pub fn RevolvedProfile::emission_colors(self : RevolvedProfile) -> Array[Rgb?] {
let n = self.segments.length()
let out : Array[Rgb?] = []
for _i in 0..= 0 && band < n {
let target = if reversed { n - 1 - band } else { band }
out[target] = Some(color)
}
}
out
}
///|
/// RevolvedProfile::polyline builds a chain of straight bands through `points`
/// in the (r, z) half-plane. When `closed` is true the last point is joined
/// back to the first, which produces an annular solid.
pub fn RevolvedProfile::polyline(
points : Array[Vec2],
closed~ : Bool = false,
) -> RevolvedProfile {
let n = points.length()
let segments : Array[RevSegment] = []
for i in 0..<(n - 1) {
segments.push(RevLine(points[i], points[i + 1]))
}
if closed && n > 2 {
segments.push(RevLine(points[n - 1], points[0]))
}
{ segments, band_colors: [], }
}
///|
/// RevolvedProfile::cylinder builds a solid right circular cylinder with its
/// base at z = 0 and its axis on +Z.
pub fn RevolvedProfile::cylinder(
diameter : Double,
height : Double,
) -> RevolvedProfile {
let r = diameter / 2.0
RevolvedProfile::new([
RevLine((0.0, 0.0), (r, 0.0)),
RevLine((r, 0.0), (r, height)),
RevLine((r, height), (0.0, height)),
])
}
///|
/// RevolvedProfile::frustum builds a solid frustum between `radius_bottom` at
/// z = 0 and `radius_top` at z = `height`. Either radius may be zero, which
/// makes that end a pole, but not both.
pub fn RevolvedProfile::frustum(
radius_bottom : Double,
radius_top : Double,
height : Double,
) -> RevolvedProfile {
let segs : Array[RevSegment] = []
if radius_bottom > 0.0 {
segs.push(RevLine((0.0, 0.0), (radius_bottom, 0.0)))
segs.push(RevLine((radius_bottom, 0.0), (radius_top, height)))
} else if radius_top > 0.0 {
segs.push(RevLine((0.0, 0.0), (radius_top, height)))
}
if radius_top > 0.0 {
segs.push(RevLine((radius_top, height), (0.0, height)))
}
RevolvedProfile::new(segs)
}
///|
/// RevolvedProfile::cone builds a solid cone with base radius `radius` at z = 0
/// and apex on the axis at z = `height`.
pub fn RevolvedProfile::cone(
radius : Double,
height : Double,
) -> RevolvedProfile {
RevolvedProfile::frustum(radius, 0.0, height)
}
///|
/// RevolvedProfile::sphere builds a solid sphere of the given diameter centred
/// on the origin. It is two hemispherical `SPHERICAL_SURFACE` bands sharing the
/// equator circle.
pub fn RevolvedProfile::sphere(diameter : Double) -> RevolvedProfile {
let r = diameter / 2.0
RevolvedProfile::new([
RevArc((0.0, -r), (r, 0.0), (0.0, 0.0)),
RevArc((r, 0.0), (0.0, r), (0.0, 0.0)),
])
}
///|
/// RevolvedProfile::sphere_cap builds a flat-bottomed dome: a disc of radius
/// `rim_radius` at z = 0 and a spherical cap rising to the axis at z =
/// `height`. It is the natural representation of a button-head screw top.
pub fn RevolvedProfile::sphere_cap(
rim_radius : Double,
height : Double,
) -> RevolvedProfile {
let k = (height * height - rim_radius * rim_radius) / (2.0 * height)
RevolvedProfile::new([
RevLine((0.0, 0.0), (rim_radius, 0.0)),
RevArc((rim_radius, 0.0), (0.0, height), (0.0, k)),
])
}
///|
/// RevolvedProfile::chamfered_cylinder builds a solid right circular cylinder
/// of `diameter` and `height` whose bottom and/or top rims carry a 45-degree
/// analytic chamfer of the given leg length.
///
/// The result is one `CYLINDRICAL_SURFACE`, one `CONICAL_SURFACE` per chamfer,
/// and planar caps. It never degenerates into a prism of planar facets: a
/// circular body's chamfer is a cone, not a ring stack.
pub fn RevolvedProfile::chamfered_cylinder(
diameter : Double,
height : Double,
bottom_chamfer~ : Double = 0.0,
top_chamfer~ : Double = 0.0,
) -> RevolvedProfile {
let r = diameter / 2.0
let cb = if bottom_chamfer > 0.0 && bottom_chamfer < r {
bottom_chamfer
} else {
0.0
}
let ct = if top_chamfer > 0.0 && top_chamfer < r { top_chamfer } else { 0.0 }
if cb <= 0.0 && ct <= 0.0 {
return RevolvedProfile::cylinder(diameter, height)
}
let pts : Array[Vec2] = [(0.0, 0.0)]
if cb > 0.0 {
pts.push((r - cb, 0.0))
pts.push((r, cb))
} else {
pts.push((r, 0.0))
}
if ct > 0.0 {
pts.push((r, height - ct))
pts.push((r - ct, height))
} else {
pts.push((r, height))
}
pts.push((0.0, height))
RevolvedProfile::polyline(pts)
}
///|
/// RevolvedProfile::tube builds an annular solid (washer or plain ring) with
/// the given outside and inside diameters and axial height. Its lateral faces
/// are two analytic `CYLINDRICAL_SURFACE` bands and its ends are annular
/// `PLANE` faces. No polygon facets are emitted.
pub fn RevolvedProfile::tube(
outer_diameter : Double,
inner_diameter : Double,
height : Double,
) -> RevolvedProfile {
let ro = outer_diameter / 2.0
let ri = inner_diameter / 2.0
RevolvedProfile::polyline(
[(ri, 0.0), (ro, 0.0), (ro, height), (ri, height)],
closed=true,
)
}
///|
/// RevolvedProfile::chamfered_tube builds an annular solid whose outer and
/// inner rims carry analytic 45-degree chamfers. Every chamfer is a
/// `CONICAL_SURFACE`, the bores are `CYLINDRICAL_SURFACE`s, and the ends are
/// annular planes. This is the correct representation of a bearing ring or a
/// chamfered washer.
pub fn RevolvedProfile::chamfered_tube(
outer_diameter : Double,
inner_diameter : Double,
height : Double,
outer_bottom_chamfer~ : Double = 0.0,
outer_top_chamfer~ : Double = 0.0,
inner_bottom_chamfer~ : Double = 0.0,
inner_top_chamfer~ : Double = 0.0,
) -> RevolvedProfile {
let ro = outer_diameter / 2.0
let ri = inner_diameter / 2.0
let ob = if outer_bottom_chamfer > 0.0 && outer_bottom_chamfer < ro - ri {
outer_bottom_chamfer
} else {
0.0
}
let ot = if outer_top_chamfer > 0.0 && outer_top_chamfer < ro - ri {
outer_top_chamfer
} else {
0.0
}
let ib = if inner_bottom_chamfer > 0.0 && inner_bottom_chamfer < ro - ri {
inner_bottom_chamfer
} else {
0.0
}
let it = if inner_top_chamfer > 0.0 && inner_top_chamfer < ro - ri {
inner_top_chamfer
} else {
0.0
}
let pts : Array[Vec2] = []
// Outer bottom rim, walking up the outside.
if ob > 0.0 {
pts.push((ro - ob, 0.0))
pts.push((ro, ob))
} else {
pts.push((ro, 0.0))
}
if ot > 0.0 {
pts.push((ro, height - ot))
pts.push((ro - ot, height))
} else {
pts.push((ro, height))
}
// Top annulus inward, walking down the bore.
if it > 0.0 {
pts.push((ri + it, height))
pts.push((ri, height - it))
} else {
pts.push((ri, height))
}
if ib > 0.0 {
pts.push((ri, ib))
pts.push((ri + ib, 0.0))
} else {
pts.push((ri, 0.0))
}
RevolvedProfile::polyline(pts, closed=true)
}
///|
/// RevolvedProfile::cup builds a solid circular cup: an outer cylinder of
/// `outer_diameter` and height `height` with an optional 45-degree top rim
/// chamfer, a blind cylindrical bore of `bore_diameter` whose floor sits at
/// `floor_thickness` above the base, and an optional 45-degree conical lead-in
/// on the bore rim.
///
/// All lateral faces are analytic (`CYLINDRICAL_SURFACE` and
/// `CONICAL_SURFACE`); the base, the annular top, and the bore floor are planes.
/// It is the analytic outer shell of a socket-head fastener; the hexagonal
/// socket flats are added by the caller as planar facets over this shell.
pub fn RevolvedProfile::cup(
outer_diameter : Double,
height : Double,
bore_diameter : Double,
floor_thickness : Double,
top_chamfer~ : Double = 0.0,
bore_lead_in~ : Double = 0.0,
) -> RevolvedProfile {
let ro = outer_diameter / 2.0
let rb = bore_diameter / 2.0
let ct = if top_chamfer > 0.0 && top_chamfer < ro - rb {
top_chamfer
} else {
0.0
}
let lead = if bore_lead_in > 0.0 && bore_lead_in < height - floor_thickness {
bore_lead_in
} else {
0.0
}
let pts : Array[Vec2] = [(0.0, 0.0)]
if ct > 0.0 {
pts.push((ro, 0.0))
pts.push((ro, height - ct))
pts.push((ro - ct, height))
} else {
pts.push((ro, 0.0))
pts.push((ro, height))
}
if lead > 0.0 {
pts.push((rb + lead, height))
pts.push((rb, height - lead))
} else {
pts.push((rb, height))
}
pts.push((rb, floor_thickness))
pts.push((0.0, floor_thickness))
RevolvedProfile::polyline(pts)
}
///|
/// RevolvedProfile::segments returns the segment chain.
pub fn RevolvedProfile::segments(self : RevolvedProfile) -> Array[RevSegment] {
self.segments.copy()
}
///|
/// RevolvedProfile::as_shape wraps the profile as a `Shape` so the fluent
/// `with_name`/`translate`/`rotate_*` builder methods apply.
pub fn RevolvedProfile::as_shape(self : RevolvedProfile) -> Shape {
Shape::RevolvedProfile(self)
}
///|
fn rev_seg_start(seg : RevSegment) -> Vec2 {
match seg {
RevLine(a, _) => a
RevArc(a, _, _) => a
}
}
///|
fn rev_seg_end(seg : RevSegment) -> Vec2 {
match seg {
RevLine(_, b) => b
RevArc(_, b, _) => b
}
}
///|
fn rev_close2(a : Vec2, b : Vec2) -> Bool {
(a.0 - b.0).abs() < 1.0e-9 && (a.1 - b.1).abs() < 1.0e-9
}
///|
/// RevolvedProfile::validate checks the segment chain and returns a typed error
/// when it cannot bound a solid.
pub fn RevolvedProfile::validate(
self : RevolvedProfile,
) -> Result[Unit, RevProfileError] {
let segs = self.segments
let n = segs.length()
if n == 0 {
return Err(EmptyProfile)
}
for i in 0.. {
let r1 = rev_dist2(s, c)
let r2 = rev_dist2(e, c)
if r1 < 1.0e-12 {
return Err(DegenerateArc(i))
}
if (r1 - r2).abs() > 1.0e-6 {
return Err(ArcEndpointMismatch(i))
}
}
RevLine(_, _) => ()
}
if i > 0 {
let prev = rev_seg_end(segs[i - 1])
if !rev_close2(prev, a) {
return Err(DiscontinuousChain(i))
}
}
}
let first = rev_seg_start(segs[0])
let last = rev_seg_end(segs[n - 1])
let open_on_axis = first.0 < 1.0e-9 && last.0 < 1.0e-9
let closed = rev_close2(first, last)
if !(open_on_axis || closed) {
return Err(NotClosed)
}
// The profile must enclose a non-zero signed area.
if rev_profile_signed_area(segs).abs() < 1.0e-12 {
return Err(DegenerateArea)
}
Ok(())
}
///|
fn rev_dist2(a : Vec2, b : Vec2) -> Double {
let dx = a.0 - b.0
let dy = a.1 - b.1
Double::sqrt(dx * dx + dy * dy)
}
///|
/// rev_profile_signed_area returns the signed area enclosed by the profile with
/// the axis closure. A positive value means the material is on the left when
/// walking the profile in (r, z).
fn rev_profile_signed_area(segs : Array[RevSegment]) -> Double {
let mut sum = 0.0
for seg in segs {
match seg {
RevLine(a, b) => sum = sum + a.0 * b.1 - b.0 * a.1
RevArc(a, b, c) => {
// Area contribution of a circular arc band about its chord: the
// shoelace term plus the circular segment area. The segment area is
// signed by the sweep direction.
sum = sum + a.0 * b.1 - b.0 * a.1
let rho = rev_dist2(a, c)
let a0 = @math.atan2(a.1 - c.1, a.0 - c.0)
let a1 = @math.atan2(b.1 - c.1, b.0 - c.0)
let delta = rev_norm_angle(a1 - a0)
sum = sum + rho * rho * (delta - @math.sin(delta))
}
}
}
sum / 2.0
}
///|
/// rev_norm_angle normalizes an angle to the interval (-pi, pi].
fn rev_norm_angle(a : Double) -> Double {
let two_pi = 2.0 * @math.PI
let mut x = a
while x > @math.PI {
x = x - two_pi
}
while x <= -@math.PI {
x = x + two_pi
}
x
}