///|
/// SweptProfile::new generates a new Shape by sweeping a 2D profile along a 3D path.
/// By default, it generates end caps, making it a topologically-closed solid.
///
/// When `straight_holes` is true the profile's holes keep the orientation of the
/// first path point instead of following the path frame, which gives straight
/// bores through twisted sweeps (e.g. a hex socket in a herringbone gear).
///
/// When `straight_outline` is true (the default) the outer boundary keeps the
/// orientation of the first path point, which gives a straight outer wall while
/// the holes twist with the path (e.g. a smooth cylindrical shell around a
/// herringbone ring gear). Set it to false when the outer boundary itself must
/// follow the path, such as a thread or a coil wire.
pub fn SweptProfile::new(
path~ : SweepPath,
profile~ : SweepProfile,
make_start_cap~ : Bool = true,
make_end_cap~ : Bool = true,
straight_holes~ : Bool = false,
straight_outline~ : Bool = true,
smooth_helix~ : Bool = false,
corner_override~ : (Array[Vec3], Array[Vec3])? = None,
) -> Shape {
Shape::SweptProfile({
path,
profile,
make_start_cap,
make_end_cap,
straight_holes,
straight_outline,
smooth_helix,
corner_override,
})
}
///|
fn compile_swept_profile(
ctx : StepContext,
node : SceneNode,
s : SweptProfile,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
let xform = node_transform(node, parent)
let path = s.path
let profile = s.profile
if path.points.length() < 2 || profile.vertices.length() < 3 {
return []
}
if s.smooth_helix {
return compile_smooth_helix(ctx, node, s, reverse~, parent~)
}
let num_path_pts = path.points.length()
// Profile loops: outer boundary first, then any holes.
let loops : Array[Array[Vec3]] = [
profile.vertices,
..profile.holes.filter(fn(h) { h.length() >= 3 }),
]
let num_loops = loops.length()
// 1. Calculate profile-local to global (centered at origin)
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))
// global to profile-local (centered at origin)
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,
}
// Vertices in profile-local coordinate system (relative to profile.center).
// All loops are flattened into one array; `loop_offsets` records where each
// loop starts (within a single profile slice).
let local_loops = loops.map(fn(lp) {
lp.map(fn(v) { apply_matrix(m_global_to_prof, v_sub(v, profile.center)) })
})
let loop_offsets : Array[Int32] = []
let local_verts : Array[Vec3] = []
let is_hole : Array[Bool] = []
let mut loop_index = 0
for lp in local_loops {
loop_offsets.push(local_verts.length())
for v in lp {
local_verts.push(v)
is_hole.push(loop_index > 0)
}
loop_index = loop_index + 1
}
let num_prof_verts = local_verts.length()
// Holes and/or the outer boundary can optionally stay parallel to the sweep
// axis instead of rotating with the path frame. Each then uses the frame at
// the first path point.
let t0 = v_norm(path.tangents[0])
let n0 = v_norm(path.normals[0])
let r0 = v_norm(v_cross(t0, n0))
// 2. Transform local_verts to each path point.
let pts_raw : Array[Vec3] = []
for i in 0.. Unit {
let key = if a < b { "\{a}:\{b}" } else { "\{b}:\{a}" }
if seen.get(key).unwrap_or(false) {
return
}
seen.set(key, true)
let start = if a < b { a } else { b }
let end = if a < b { b } else { a }
let dir_raw = v_norm(v_sub(pts[end], pts[start]))
let dir_ref = ctx.direction(dir_raw.0, dir_raw.1, dir_raw.2)
let vec_ref = ctx.vector(dir_ref, 1.0)
let line_ref = ctx.line(cp_refs[start], vec_ref)
let ec_ref = ctx.edge_curve(vp_refs[start], vp_refs[end], line_ref, true)
edges.push(EdgeInfo::{ start, end, edge: ec_ref, })
}
// Profile edges (at each path point)
for i in 0.. Array[Int32] {
let lo = loop_offsets[li]
let n = local_loops[li].length()
let cycle = Array::make(n, 0)
for j in 0..