///|
// Native smooth B-rep for a rectangular profile swept along a helical path.
//
// The four sides of the swept strip are ruled B-spline surfaces between the
// four corner helices (cubic Bezier spans with exact endpoint tangents), and
// the two ends are planar caps. The result is the six-face topology of a
// spring-washer STEP reference rather than a stack of planar facets.
//
// The corner paths are sampled from the sweep path; the curve control points
// use cubic Hermite spans, so the emitted surfaces deviate from the ideal
// helix by only a few 1e-5 mm at 24 spans per turn.
///|
priv struct SmoothCurve {
curve_ref : @step.Ref
cps : Array[@step.Ref]
cps_pos : Array[Vec3]
start_cp : @step.Ref
end_cp : @step.Ref
}
///|
fn smooth_round(x : Double) -> Int32 {
(x + 0.5).to_int()
}
///|
fn smooth_average(points : Array[Vec3]) -> Vec3 {
let mut x = 0.0
let mut y = 0.0
let mut z = 0.0
let n = points.length()
for p in points {
x = x + p.0
y = y + p.1
z = z + p.2
}
let d = n.to_double()
(x / d, y / d, z / d)
}
///|
fn smooth_polygon_normal(points : Array[Vec3]) -> Vec3 {
let mut nx = 0.0
let mut ny = 0.0
let mut nz = 0.0
let n = points.length()
for i in 0.. SmoothCurve {
let num_spans = indices.length() - 1
let points : Array[Vec3] = Array::make(num_spans + 1, (0.0, 0.0, 0.0))
let tangents : Array[Vec3] = Array::make(num_spans + 1, (0.0, 0.0, 0.0))
for k in 0..<=num_spans {
let idx = indices[k]
let p = corner_pts[idx * 4 + profile_index]
points[k] = p
let tangent = if idx == 0 {
v_sub(corner_pts[(idx + 1) * 4 + profile_index], p)
} else if idx == num_path_pts - 1 {
v_sub(p, corner_pts[(idx - 1) * 4 + profile_index])
} else {
v_scale(
v_sub(
corner_pts[(idx + 1) * 4 + profile_index],
corner_pts[(idx - 1) * 4 + profile_index],
),
0.5,
)
}
tangents[k] = tangent
}
let control_pts : Array[Vec3] = []
for k in 0.. (@step.Ref, Bool) {
let du = v_sub(row_b.cps_pos[0], row_a.cps_pos[0])
let dv = v_sub(row_a.cps_pos[1], row_a.cps_pos[0])
let natural_normal = v_cross(du, dv)
// Orient the surface patch so its natural normal points outward; the face
// then uses same_sense = .T. for every patch (the reference form).
let (row0, row1) = if v_dot(natural_normal, expected_out) >= 0.0 {
(row_a, row_b)
} else {
(row_b, row_a)
}
let cp_row = fn(row : Array[@step.Ref]) -> String {
"(" + row.map(fn(r) { "#\{r.id.value}" }).join(",") + ")"
}
let rows = "(" + cp_row(row0.cps) + "," + cp_row(row1.cps) + ")"
let v_mults_text = "(" + v_mults.map(fn(m) { "\{m}" }).join(",") + ")"
let v_knots_text = "(" + v_knots.join(",") + ")"
// Emit the AP214 simple-entity form used by the downloaded references:
// B_SPLINE_SURFACE_WITH_KNOTS('',u_deg,v_deg,((row0),(row1)),...).
let args = "('',1,3," +
rows +
",.UNSPECIFIED.,.F.,.F.,.F.,(2,2)," +
v_mults_text +
",(0.,1.)," +
v_knots_text +
",.PIECEWISE_BEZIER_KNOTS.)"
let raw = @step.RawEntity::new("B_SPLINE_SURFACE_WITH_KNOTS", args)
let surface_ref = ctx.add(@repository.any_entity_raw_entity(raw))
(surface_ref, true)
}
///|
fn smooth_line_edge(
ctx : StepContext,
a_cp : @step.Ref,
a_vp : @step.Ref,
_b_cp : @step.Ref,
b_vp : @step.Ref,
a_pos : Vec3,
b_pos : Vec3,
) -> @step.Ref {
let direction = v_norm(v_sub(b_pos, a_pos))
let dir_ref = ctx.direction(direction.0, direction.1, direction.2)
let vector_ref = ctx.vector(dir_ref, 1.0)
let line_ref = ctx.line(a_cp, vector_ref)
ctx.edge_curve(a_vp, b_vp, line_ref, true)
}
///|
fn smooth_cap_plane(
ctx : StepContext,
outward : Vec3,
location : Vec3,
reference : Vec3,
) -> @step.Ref {
let location_ref = ctx.cartesian_point(location.0, location.1, location.2)
let axis_ref = ctx.direction(outward.0, outward.1, outward.2)
let ref_ref = ctx.direction(reference.0, reference.1, reference.2)
let placement = ctx.axis2_placement_3d(location_ref, axis_ref, ref_ref)
let plane = @geometry.Plane::new("", placement)
ctx.add(@repository.any_entity_plane(plane))
}
///|
fn smooth_face(
ctx : StepContext,
edges : Array[(@step.Ref, Bool)],
surface : @step.Ref,
same_sense : Bool,
) -> @step.Ref {
// Build the loop with the orientation that agrees with the surface normal;
// reversing the caller's cycle keeps every shared edge opposite.
let reversed = edges.rev().map(fn(e) { (e.0, !e.1) })
let oriented = reversed.map(fn(e) { ctx.oriented_edge(e.0, e.1) })
let edge_loop_ref = ctx.edge_loop(oriented)
let bound = ctx.face_outer_bound(edge_loop_ref, true)
ctx.advanced_face([bound], surface, same_sense)
}
///|
fn compile_smooth_helix(
ctx : StepContext,
node : SceneNode,
s : SweptProfile,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
let path = s.path
let profile = s.profile
let num_path_pts = path.points.length()
if num_path_pts < 4 || profile.vertices.length() != 4 {
return []
}
let xform = node_transform(node, parent)
// Profile-local coordinates (the same mapping the faceted sweep uses).
let prof_z = v_norm(profile.normal_dir)
let prof_x = v_norm(profile.right_dir)
let prof_y = v_norm(v_cross(prof_z, prof_x))
let m_global_to_prof = Matrix3x3::{
m00: prof_x.0,
m01: prof_x.1,
m02: prof_x.2,
m10: prof_y.0,
m11: prof_y.1,
m12: prof_y.2,
m20: prof_z.0,
m21: prof_z.1,
m22: prof_z.2,
}
let local_verts = profile.vertices.map(fn(v) {
apply_matrix(m_global_to_prof, v_sub(v, profile.center))
})
// Corner positions at every path point, in world coordinates.
let corner_pts : Array[Vec3] = Array::make(num_path_pts * 4, (0.0, 0.0, 0.0))
for i in 0.. 0.0 { 0 } else { 1 }
role_index[radial * 2 + axial] = j
}
for role in 0..<4 {
if role_index[role] < 0 {
return []
}
}
// Optional reference corner endpoints: blend the sampled corner curves so
// they meet the given high-Z and low-Z cap vertices exactly. The correction
// is expressed in the local (radial, axial, tangential) sweep frame so it
// rotates with the helix instead of distorting the far side of the ring.
match s.corner_override {
Some((start_ref, end_ref)) =>
if start_ref.length() == 4 && end_ref.length() == 4 {
let start_t = xform.apply_dir(v_norm(path.tangents[0]))
let start_n = xform.apply_dir(v_norm(path.normals[0]))
let start_r = v_norm(v_cross(start_t, start_n))
let last = num_path_pts - 1
let end_t = xform.apply_dir(v_norm(path.tangents[last]))
let end_n = xform.apply_dir(v_norm(path.normals[last]))
let end_r = v_norm(v_cross(end_t, end_n))
for role in 0..<4 {
let j = role_index[role]
let start_delta = v_sub(xform.apply(start_ref[role]), corner_pts[j])
let end_delta = v_sub(
xform.apply(end_ref[role]),
corner_pts[last * 4 + j],
)
let s_lx = v_dot(start_delta, start_r)
let s_ly = v_dot(start_delta, start_n)
let s_lz = v_dot(start_delta, start_t)
let e_lx = v_dot(end_delta, end_r)
let e_ly = v_dot(end_delta, end_n)
let e_lz = v_dot(end_delta, end_t)
for i in 0.. ()
}
// Total sweep angle, then one cubic span per 15 degrees (24 per turn).
let mut total_angle = 0.0
for i in 0..<(num_path_pts - 1) {
let a = path.points[i]
let b = path.points[i + 1]
let cross_z = a.0 * b.1 - a.1 * b.0
let dot = a.0 * b.0 + a.1 * b.1
total_angle = total_angle + @math.atan2(cross_z, dot)
}
let turns = Double::abs(total_angle) / (2.0 * @math.PI)
let mut num_spans = smooth_round(turns * 24.0)
if num_spans < 4 {
num_spans = 4
}
if num_spans > num_path_pts - 1 {
num_spans = num_path_pts - 1
}
let indices : Array[Int32] = Array::make(num_spans + 1, 0)
for k in 0..<=num_spans {
indices[k] = smooth_round(
k.to_double() * (num_path_pts - 1).to_double() / num_spans.to_double(),
)
}
let v_mults : Array[Int32] = [4]
for _ in 1.. {
let last = it.cps_pos.length() - 1
let s_cycle = [it.cps_pos[0], ot.cps_pos[0], ob.cps_pos[0], ib.cps_pos[0]]
let e_cycle = [
it.cps_pos[last],
ot.cps_pos[last],
ob.cps_pos[last],
ib.cps_pos[last],
]
let s_center = smooth_average(s_cycle)
let e_center = smooth_average(e_cycle)
let mut s_axis = smooth_polygon_normal(s_cycle)
if v_dot(s_axis, v_sub(e_center, s_center)) < 0.0 {
s_axis = v_scale(s_axis, -1.0)
}
let mut e_axis = smooth_polygon_normal(e_cycle)
if v_dot(e_axis, v_sub(s_center, e_center)) < 0.0 {
e_axis = v_scale(e_axis, -1.0)
}
let s_ref = v_norm(v_sub(s_cycle[1], s_cycle[0]))
let e_ref = v_norm(v_sub(e_cycle[1], e_cycle[0]))
(s_axis, s_center, s_ref, e_axis, e_center, e_ref)
}
None =>
(
start_out,
start_location,
xform.apply_dir(start_radial_local),
end_out,
end_location,
xform.apply_dir(start_radial_local),
)
}
let start_plane = smooth_cap_plane(
ctx, start_axis, start_center, start_ref_dir,
)
let end_plane = smooth_cap_plane(ctx, end_axis, end_center, end_ref_dir)
let flip = fn(same : Bool) -> Bool { if reverse { !same } else { same } }
let faces : Array[@step.Ref] = []
// Top: start radial, outer helix forward, end radial reversed, inner
// helix reversed.
faces.push(
smooth_face(
ctx,
[(s_top_s, true), (he_ot, true), (s_top_e, false), (he_it, false)],
top_surf,
flip(top_same),
),
)
// Bottom.
faces.push(
smooth_face(
ctx,
[(he_ib, true), (s_bot_e, true), (he_ob, false), (s_bot_s, false)],
bottom_surf,
flip(bottom_same),
),
)
// Inner.
faces.push(
smooth_face(
ctx,
[(he_it, true), (s_it_e, true), (he_ib, false), (s_it_s, false)],
inner_surf,
flip(inner_same),
),
)
// Outer.
faces.push(
smooth_face(
ctx,
[(s_ot_s, true), (he_ob, true), (s_ot_e, false), (he_ot, false)],
outer_surf,
flip(outer_same),
),
)
// Start cap (outward opposite the path tangent).
faces.push(
smooth_face(
ctx,
[(s_it_s, true), (s_bot_s, true), (s_ot_s, false), (s_top_s, false)],
start_plane,
flip(true),
),
)
// End cap (outward along the path tangent).
faces.push(
smooth_face(
ctx,
[(s_top_e, true), (s_ot_e, true), (s_bot_e, false), (s_it_e, false)],
end_plane,
flip(true),
),
)
faces
}