///|
/// RevCircle is a cached circular edge at one profile point.
pub(all) struct RevCircle {
edge : @step.Ref
vertex : @step.Ref
point : @step.Ref
}
///|
/// compile_revolved_profile emits a solid of revolution as one closed STEP
/// solid with analytic `CYLINDRICAL_SURFACE`, `CONICAL_SURFACE`,
/// `SPHERICAL_SURFACE`, `TOROIDAL_SURFACE`, and `PLANE` faces.
///
/// Line bands are cylinders, cones, or planar radial rings/discs. Arc bands are
/// spherical (arc centred on the axis) or toroidal (arc centred off-axis).
/// Every band uses its start circle with oriented-edge sense `.T.` and its end
/// circle with `.F.`, so each shared circle is traversed in opposite directions
/// by the two neighbouring faces. Full-period tube bands carry a seam edge
/// (a straight generator for line bands, a trimmed circular arc for arc bands).
pub fn compile_revolved_profile(
ctx : StepContext,
node : SceneNode,
rev : RevolvedProfile,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
let xform = node_transform(node, parent)
match rev.validate() {
Err(_) => return []
Ok(_) => ()
}
// Normalize so the material is on the left when walking the profile.
let raw = rev.segments
let segs = if rev_profile_signed_area(raw) < 0.0 {
rev_reverse_segments(raw)
} else {
raw
}
let axis_dir = xform.apply_dir((0.0, 0.0, 1.0))
let ref_dir = xform.apply_dir((1.0, 0.0, 0.0))
let circles : Map[String, RevCircle] = Map([])
let faces : Array[@step.Ref] = []
for seg in segs {
let band = match seg {
RevLine(a, b) => {
let info : RevBandInfo = {
a,
b,
center: None,
sample: ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0),
normal: rev_line_normal(a, b),
radial: (a.1 - b.1).abs() < 1.0e-9,
axial: (a.0 - b.0).abs() < 1.0e-9,
}
info
}
RevArc(a, b, c) => {
let (sample, normal) = rev_arc_sample_normal(a, b, c)
let info : RevBandInfo = {
a,
b,
center: Some(c),
sample,
normal,
radial: false,
axial: false,
}
info
}
}
let face = rev_compile_band(
ctx,
xform,
band,
axis_dir,
ref_dir,
circles,
reverse~,
)
match face {
Some(f) => faces.push(f)
None => ()
}
}
faces
}
///|
/// RevBandInfo is the resolved geometry of one profile band.
priv struct RevBandInfo {
a : Vec2
b : Vec2
center : Vec2?
sample : Vec2
normal : Vec2
radial : Bool
axial : Bool
}
///|
/// rev_line_normal returns the outward 2D normal of a straight band, assuming
/// the material is on the left of the traversal direction.
fn rev_line_normal(a : Vec2, b : Vec2) -> Vec2 {
let dr = b.0 - a.0
let dz = b.1 - a.1
let len = Double::sqrt(dr * dr + dz * dz)
if len == 0.0 {
(0.0, 1.0)
} else {
(dz / len, -dr / len)
}
}
///|
/// rev_arc_sample_normal returns the midpoint and outward 2D normal of a minor
/// arc band, assuming the material is on the left of the traversal direction.
fn rev_arc_sample_normal(a : Vec2, b : Vec2, c : Vec2) -> (Vec2, Vec2) {
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)
let am = a0 + delta / 2.0
let sign = if delta >= 0.0 { 1.0 } else { -1.0 }
let sample = (c.0 + rho * @math.cos(am), c.1 + rho * @math.sin(am))
let normal = (sign * @math.cos(am), sign * @math.sin(am))
(sample, normal)
}
///|
/// rev_reverse_segments reverses a band chain and each band's endpoints.
fn rev_reverse_segments(segs : Array[RevSegment]) -> Array[RevSegment] {
let out : Array[RevSegment] = []
let n = segs.length()
for i in 0.. RevLine(b, a)
RevArc(a, b, c) => RevArc(b, a, c)
},
)
}
out
}
///|
/// rev_get_circle returns the cached circular edge for a profile point.
fn rev_get_circle(
ctx : StepContext,
xform : Transform,
r : Double,
z : Double,
circles : Map[String, RevCircle],
) -> RevCircle {
let key = @step.fmt_num(r) + "," + @step.fmt_num(z)
match circles.get(key) {
Some(c) => c
None => {
// The CIRCLE axis placement is centred on the revolution axis; the
// vertex sits on the rim at phi = 0.
let center = xform.apply((0.0, 0.0, z))
let center_cp = ctx.cartesian_point(center.0, center.1, center.2)
let rim = xform.apply((r, 0.0, z))
let rim_cp = ctx.cartesian_point(rim.0, rim.1, rim.2)
let up = xform.apply_dir((0.0, 0.0, 1.0))
let right = xform.apply_dir((1.0, 0.0, 0.0))
let a2p = ctx.axis2_placement_3d(
center_cp,
ctx.direction(up.0, up.1, up.2),
ctx.direction(right.0, right.1, right.2),
)
let circle = ctx.add(
@repository.any_entity_circle(
@geometry.Circle::new("", a2p, @step.fmt_num(r)),
),
)
let vp = ctx.vertex_point(rim_cp)
let edge = ctx.edge_curve(vp, vp, circle, true)
let c : RevCircle = { edge, vertex: vp, point: rim_cp, }
circles.set(key, c)
c
}
}
}
///|
/// rev_compile_band emits the single face for one profile band.
fn rev_compile_band(
ctx : StepContext,
xform : Transform,
band : RevBandInfo,
axis_dir : Vec3,
ref_dir : Vec3,
circles : Map[String, RevCircle],
reverse~ : Bool = false,
) -> @step.Ref? {
let ra = band.a.0
let rb = band.b.0
let has_a = ra > 1.0e-9
let has_b = rb > 1.0e-9
let desired_raw = xform.apply_dir((band.normal.0, 0.0, band.normal.1))
let sample_world = xform.apply((band.sample.0, 0.0, band.sample.1))
if band.radial {
return rev_compile_radial(
ctx,
xform,
band,
has_a,
has_b,
desired_raw,
circles,
reverse~,
)
}
if !(has_a || has_b) {
return None
}
// Surface + natural normal.
let (surface_ref, natural) = rev_band_surface(
ctx, xform, band, axis_dir, ref_dir, sample_world,
)
let dot = v_dot(natural, desired_raw)
let same_sense = if reverse { !(dot > 0.0) } else { dot > 0.0 }
// Bounds.
if has_a && has_b {
let ca = rev_get_circle(ctx, xform, ra, band.a.1, circles)
let cb = rev_get_circle(ctx, xform, rb, band.b.1, circles)
let seam = rev_seam_geometry(
ctx,
xform,
band.a,
band.b,
ca.vertex,
cb.vertex,
band.center,
)
let lp = ctx.edge_loop([
ctx.oriented_edge(seam, true),
ctx.oriented_edge(cb.edge, false),
ctx.oriented_edge(seam, false),
ctx.oriented_edge(ca.edge, true),
])
let bound = ctx.face_outer_bound(lp, true)
Some(ctx.advanced_face([bound], surface_ref, same_sense))
} else {
// The band terminates at a pole. Add an explicit seam from the base rim to
// the apex vertex so a full-period surface tessellates without an open
// edge at the singular point.
let (c, base_is_start, base_pt, pole_pt) = if has_a {
(rev_get_circle(ctx, xform, ra, band.a.1, circles), true, band.a, band.b)
} else {
(rev_get_circle(ctx, xform, rb, band.b.1, circles), false, band.b, band.a)
}
let pole = xform.apply((0.0, 0.0, pole_pt.1))
let pole_cp = ctx.cartesian_point(pole.0, pole.1, pole.2)
let pole_vp = ctx.vertex_point(pole_cp)
let seam = rev_seam_geometry(
ctx,
xform,
base_pt,
pole_pt,
c.vertex,
pole_vp,
band.center,
)
let sense = base_is_start
let lp = ctx.edge_loop([
ctx.oriented_edge(seam, true),
ctx.oriented_edge(seam, false),
ctx.oriented_edge(c.edge, sense),
])
let bound = ctx.face_outer_bound(lp, true)
Some(ctx.advanced_face([bound], surface_ref, same_sense))
}
}
///|
/// rev_compile_radial emits a planar disc or annulus face.
fn rev_compile_radial(
ctx : StepContext,
xform : Transform,
band : RevBandInfo,
has_a : Bool,
has_b : Bool,
desired : Vec3,
circles : Map[String, RevCircle],
reverse~ : Bool = false,
) -> @step.Ref? {
let z = band.a.1
let origin = xform.apply((0.0, 0.0, z))
let origin_cp = ctx.cartesian_point(origin.0, origin.1, origin.2)
let refdir = xform.apply_dir((1.0, 0.0, 0.0))
let a2p = ctx.axis2_placement_3d(
origin_cp,
ctx.direction(desired.0, desired.1, desired.2),
ctx.direction(refdir.0, refdir.1, refdir.2),
)
let plane = ctx.add(
@repository.any_entity_plane(@geometry.Plane::new("", a2p)),
)
let same_sense = !reverse
if has_a && has_b {
let ca = rev_get_circle(ctx, xform, band.a.0, band.a.1, circles)
let cb = rev_get_circle(ctx, xform, band.b.0, band.b.1, circles)
// The start edge uses sense true, the end edge sense false; assign the
// larger radius to the outer bound.
let (outer, outer_sense, inner, inner_sense) = if band.a.0 > band.b.0 {
(ca, true, cb, false)
} else {
(cb, false, ca, true)
}
let outer_loop = ctx.edge_loop([ctx.oriented_edge(outer.edge, outer_sense)])
let inner_loop = ctx.edge_loop([ctx.oriented_edge(inner.edge, inner_sense)])
let outer_bound = ctx.face_outer_bound(outer_loop, true)
let inner_bound = ctx.face_bound(inner_loop, true)
Some(ctx.advanced_face([outer_bound, inner_bound], plane, same_sense))
} else if has_a {
let ca = rev_get_circle(ctx, xform, band.a.0, band.a.1, circles)
let lp = ctx.edge_loop([ctx.oriented_edge(ca.edge, true)])
let bound = ctx.face_outer_bound(lp, true)
Some(ctx.advanced_face([bound], plane, same_sense))
} else if has_b {
let cb = rev_get_circle(ctx, xform, band.b.0, band.b.1, circles)
let lp = ctx.edge_loop([ctx.oriented_edge(cb.edge, false)])
let bound = ctx.face_outer_bound(lp, true)
Some(ctx.advanced_face([bound], plane, same_sense))
} else {
None
}
}
///|
/// rev_band_surface emits the analytic surface for a non-radial band and
/// returns it with the world-space natural normal at the band sample.
fn rev_band_surface(
ctx : StepContext,
xform : Transform,
band : RevBandInfo,
axis_dir : Vec3,
ref_dir : Vec3,
sample_world : Vec3,
) -> (@step.Ref, Vec3) {
match band.center {
Some(c) => {
let rho = rev_dist2(band.a, c)
// The surface placement origin is the axis point at the arc centre's
// height; radial offset becomes the major radius (torus) or is zero for
// a sphere centred on the axis.
let center_world = xform.apply((c.0, 0.0, c.1))
let axis_center = xform.apply((0.0, 0.0, c.1))
let origin_cp = ctx.cartesian_point(
axis_center.0,
axis_center.1,
axis_center.2,
)
let a2p = ctx.axis2_placement_3d(
origin_cp,
ctx.direction(axis_dir.0, axis_dir.1, axis_dir.2),
ctx.direction(ref_dir.0, ref_dir.1, ref_dir.2),
)
if c.0.abs() < 1.0e-9 {
let surface = ctx.add(
@repository.any_entity_spherical_surface(
@geometry.SphericalSurface::new("", a2p, @step.fmt_num(rho)),
),
)
let natural = v_norm(v_sub(sample_world, center_world))
(surface, natural)
} else {
let surface = ctx.add(
@repository.any_entity_toroidal_surface(
@geometry.ToroidalSurface::new(
"",
a2p,
@step.fmt_num(c.0.abs()),
@step.fmt_num(rho),
),
),
)
let q = xform.apply((c.0, 0.0, c.1))
let natural = v_norm(v_sub(sample_world, q))
(surface, natural)
}
}
None =>
if band.axial {
// Cylinder: the axis passes through (0, 0, z_start).
let r = band.a.0
let axis_origin = xform.apply((0.0, 0.0, band.a.1))
let origin_cp = ctx.cartesian_point(
axis_origin.0,
axis_origin.1,
axis_origin.2,
)
let a2p = ctx.axis2_placement_3d(
origin_cp,
ctx.direction(axis_dir.0, axis_dir.1, axis_dir.2),
ctx.direction(ref_dir.0, ref_dir.1, ref_dir.2),
)
let surface = ctx.add(
@repository.any_entity_cylindrical_surface(
@geometry.CylindricalSurface::new("", a2p, @step.fmt_num(r)),
),
)
let natural = ref_dir
(surface, natural)
} else {
// Cone: place the reference at the endpoint with the larger radius.
let dr = band.b.0 - band.a.0
let dz = band.b.1 - band.a.1
let m = dr / dz
let alpha = @math.atan(m.abs())
let axis_sign = if m > 0.0 { 1.0 } else { -1.0 }
let (ref_r, ref_z) = if band.a.0 >= band.b.0 {
(band.a.0, band.a.1)
} else {
(band.b.0, band.b.1)
}
// The placement origin lies on the cone axis; `ref_r` is the radius of
// the surface at that axial reference plane.
let axis_point = xform.apply((0.0, 0.0, ref_z))
let origin_cp = ctx.cartesian_point(
axis_point.0,
axis_point.1,
axis_point.2,
)
let cone_axis = v_scale(axis_dir, axis_sign)
let a2p = ctx.axis2_placement_3d(
origin_cp,
ctx.direction(cone_axis.0, cone_axis.1, cone_axis.2),
ctx.direction(ref_dir.0, ref_dir.1, ref_dir.2),
)
// CONICAL_SURFACE's semi-angle is a plane_angle_measure, read in the
// global context's plane-angle unit, which this writer declares as the
// radian.
let surface = ctx.add(
@repository.any_entity_conical_surface(
@geometry.ConicalSurface::new(
"",
a2p,
@step.fmt_num(ref_r),
@step.fmt_num(alpha),
),
),
)
// Natural normal = cos(alpha) * radial - sin(alpha) * axis.
let natural = v_norm(
v_sub(
v_scale(ref_dir, @math.cos(alpha)),
v_scale(cone_axis, @math.sin(alpha)),
),
)
(surface, natural)
}
}
}
///|
/// rev_seam_geometry returns the seam geometry at the profile plane (phi = 0)
/// from point `a` to point `b`, with the given start/end vertices. Line bands
/// use a straight generator; arc bands use a trimmed circular arc.
fn rev_seam_geometry(
ctx : StepContext,
xform : Transform,
a : Vec2,
b : Vec2,
va : @step.Ref,
vb : @step.Ref,
center : Vec2?,
) -> @step.Ref {
match center {
None => {
let pa = xform.apply((a.0, 0.0, a.1))
let pb = xform.apply((b.0, 0.0, b.1))
let start_cp = ctx.cartesian_point(pa.0, pa.1, pa.2)
let dir = v_norm(v_sub(pb, pa))
let dir_ref = ctx.direction(dir.0, dir.1, dir.2)
let vec_ref = ctx.vector(dir_ref, 1.0)
let line = ctx.line(start_cp, vec_ref)
ctx.edge_curve(va, vb, line, true)
}
Some(c) => {
let rho = rev_dist2(a, c)
let center_world = xform.apply((c.0, 0.0, c.1))
let origin_cp = ctx.cartesian_point(
center_world.0,
center_world.1,
center_world.2,
)
// Axis -Y makes the circle parameter increase from +X toward +Z.
let seam_axis = xform.apply_dir((0.0, -1.0, 0.0))
let seam_ref = xform.apply_dir((1.0, 0.0, 0.0))
let a2p = ctx.axis2_placement_3d(
origin_cp,
ctx.direction(seam_axis.0, seam_axis.1, seam_axis.2),
ctx.direction(seam_ref.0, seam_ref.1, seam_ref.2),
)
let circle = ctx.add(
@repository.any_entity_circle(
@geometry.Circle::new("", a2p, @step.fmt_num(rho)),
),
)
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)
// The seam arc is at most a quarter turn, so the two vertices trim the
// circle unambiguously; the edge sense records the traversal direction.
ctx.edge_curve(va, vb, circle, delta >= 0.0)
}
}
}
///|
/// rev_profile_polyline samples a revolved profile into a polyline in (r, z).
pub fn rev_profile_polyline(
rev : RevolvedProfile,
arc_segments~ : Int32 = 12,
) -> Array[Vec2] {
let pts : Array[Vec2] = []
let push_unique = fn(p : Vec2) -> Unit {
match pts.last() {
Some(q) =>
if (q.0 - p.0).abs() > 1.0e-9 || (q.1 - p.1).abs() > 1.0e-9 {
pts.push(p)
}
None => pts.push(p)
}
}
for seg in rev.segments {
match seg {
RevLine(a, b) => {
push_unique(a)
push_unique(b)
}
RevArc(a, b, c) => {
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)
let n = if arc_segments < 2 { 2 } else { arc_segments }
for i in 0..<=n {
let t = Double::from_int(i) / Double::from_int(n)
let ang = a0 + delta * t
push_unique((c.0 + rho * @math.cos(ang), c.1 + rho * @math.sin(ang)))
}
}
}
}
pts
}
///|
/// rev_mesh builds a triangle/quad mesh approximation of a revolved profile for
/// the optional Blender export. STEP output uses the analytic compiler, not
/// this mesh.
pub fn rev_mesh(
rev : RevolvedProfile,
angular_segments~ : Int32 = 48,
arc_segments~ : Int32 = 12,
) -> (Array[Vec3], Array[Array[Int32]]) {
let profile = rev_profile_polyline(rev, arc_segments~)
let m = profile.length()
let k = if angular_segments < 8 { 8 } else { angular_segments }
let verts : Array[Vec3] = []
for j in 0.. Int32 { j * m + i }
for j in 0..