///|
// Analytic DIN 93 tab washer: one outer cylindrical arc, two concave
// transition cylinders, one bore cylinder, two tab-side planes, a tab-end
// plane, and two planar caps. The tab outline is a line/arc loop; the STEP
// writer emits exactly the reference topology (9 faces, 21 edges, 14
// vertices).
///|
priv struct TWGeom {
r : Double
rb : Double
ww : Double
l : Double
h : Double
rf : Double
c_up : Vec2
c_lo : Vec2
out_up : Vec2
out_lo : Vec2
tab_up : Vec2
tab_lo : Vec2
end_up : Vec2
end_lo : Vec2
}
///|
fn tw_geom(w : TabWasher) -> TWGeom? {
let r = w.outer_diameter_mm / 2.0
let rb = w.bore_diameter_mm / 2.0
let ww = w.tab_width_mm / 2.0
let l = w.tab_length_mm
let h = w.thickness_mm
let rf = w.fillet_radius_mm
if r <= 0.0 ||
rb <= 0.0 ||
rb >= r ||
ww <= 0.0 ||
l <= 0.0 ||
h <= 0.0 ||
rf <= 0.0 ||
r <= ww {
return None
}
let dx = r + rf
let dy = ww + rf
if dx * dx <= dy * dy {
return None
}
let xroot = Double::sqrt(dx * dx - dy * dy)
if xroot >= l {
return None
}
let c_up = (-xroot, dy)
let c_lo = (-xroot, -dy)
let scale = r / dx
let out_up = (c_up.0 * scale, c_up.1 * scale)
let out_lo = (c_lo.0 * scale, c_lo.1 * scale)
Some({
r,
rb,
ww,
l,
h,
rf,
c_up,
c_lo,
out_up,
out_lo,
tab_up: (-xroot, ww),
tab_lo: (-xroot, -ww),
end_up: (-l, ww),
end_lo: (-l, -ww),
})
}
///|
fn tw_world(p : Vec2, z : Double, xform : Transform) -> Vec3 {
xform.apply((p.0, p.1, z))
}
///|
fn tw_cp(ctx : StepContext, p : Vec3) -> @step.Ref {
ctx.cartesian_point(p.0, p.1, p.2)
}
///|
fn tw_arc_edge(
ctx : StepContext,
center : Vec2,
radius : Double,
z : Double,
_start_cp : @step.Ref,
start_vp : @step.Ref,
_end_cp : @step.Ref,
end_vp : @step.Ref,
same_sense : Bool,
xform : Transform,
dir_up : @step.Ref,
dir_right : @step.Ref,
) -> @step.Ref {
let c = tw_world(center, z, xform)
let ccp = tw_cp(ctx, c)
let a2p = ctx.axis2_placement_3d(ccp, dir_up, dir_right)
let circle = ctx.add(
@repository.any_entity_circle(
@geometry.Circle::new("", a2p, @step.fmt_num(radius)),
),
)
ctx.edge_curve(start_vp, end_vp, circle, same_sense)
}
///|
fn tw_line_edge(
ctx : StepContext,
a_cp : @step.Ref,
a_vp : @step.Ref,
_b_cp : @step.Ref,
b_vp : @step.Ref,
a : Vec3,
b : Vec3,
) -> @step.Ref {
let direction = v_norm(v_sub(b, a))
let dir_ref = ctx.direction(direction.0, direction.1, direction.2)
let vector_ref = ctx.vector(dir_ref, 1.0)
ctx.edge_curve(a_vp, b_vp, ctx.line(a_cp, vector_ref), true)
}
///|
fn tw_cylinder(
ctx : StepContext,
center : Vec2,
radius : Double,
xform : Transform,
dir_up : @step.Ref,
dir_right : @step.Ref,
) -> @step.Ref {
let c = tw_world(center, 0.0, xform)
let ccp = tw_cp(ctx, c)
let a2p = ctx.axis2_placement_3d(ccp, dir_up, dir_right)
ctx.add(
@repository.any_entity_cylindrical_surface(
@geometry.CylindricalSurface::new("", a2p, @step.fmt_num(radius)),
),
)
}
///|
fn tw_plane(
ctx : StepContext,
origin : Vec3,
axis : Vec3,
reference : Vec3,
) -> @step.Ref {
let origin_cp = tw_cp(ctx, origin)
let axis_ref = ctx.direction(axis.0, axis.1, axis.2)
let ref_ref = ctx.direction(reference.0, reference.1, reference.2)
let a2p = ctx.axis2_placement_3d(origin_cp, axis_ref, ref_ref)
ctx.add(@repository.any_entity_plane(@geometry.Plane::new("", a2p)))
}
///|
fn tw_newell(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.. @step.Ref {
let oriented = edges.map(fn(e) { ctx.oriented_edge(e.0, e.1) })
let edge_loop = ctx.edge_loop(oriented)
let bound = ctx.face_outer_bound(edge_loop, true)
let loop_normal = tw_newell(loop_points)
let same = v_dot(loop_normal, natural) >= 0.0
ctx.advanced_face([bound], surface, same)
}
///|
/// compile_tab_washer emits one closed solid for a DIN 93 tab washer.
pub fn compile_tab_washer(
ctx : StepContext,
node : SceneNode,
w : TabWasher,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
let g = match tw_geom(w) {
Some(g) => g
None => return []
}
let xform = node_transform(node, parent)
let up = xform.apply_dir((0.0, 0.0, 1.0))
let down = xform.apply_dir((0.0, 0.0, -1.0))
let right = xform.apply_dir((1.0, 0.0, 0.0))
let dir_up = ctx.direction(up.0, up.1, up.2)
let dir_right = ctx.direction(right.0, right.1, right.2)
let p_out_up_b = tw_world(g.out_up, 0.0, xform)
let p_out_up_t = tw_world(g.out_up, g.h, xform)
let p_out_lo_b = tw_world(g.out_lo, 0.0, xform)
let p_out_lo_t = tw_world(g.out_lo, g.h, xform)
let p_fil_up_b = tw_world(g.tab_up, 0.0, xform)
let p_fil_up_t = tw_world(g.tab_up, g.h, xform)
let p_fil_lo_b = tw_world(g.tab_lo, 0.0, xform)
let p_fil_lo_t = tw_world(g.tab_lo, g.h, xform)
let p_tab_up_b = tw_world(g.end_up, 0.0, xform)
let p_tab_up_t = tw_world(g.end_up, g.h, xform)
let p_tab_lo_b = tw_world(g.end_lo, 0.0, xform)
let p_tab_lo_t = tw_world(g.end_lo, g.h, xform)
let p_seam_b = tw_world((g.rb, 0.0), 0.0, xform)
let p_seam_t = tw_world((g.rb, 0.0), g.h, xform)
let cp_out_up_b = tw_cp(ctx, p_out_up_b)
let cp_out_up_t = tw_cp(ctx, p_out_up_t)
let cp_out_lo_b = tw_cp(ctx, p_out_lo_b)
let cp_out_lo_t = tw_cp(ctx, p_out_lo_t)
let cp_fil_up_b = tw_cp(ctx, p_fil_up_b)
let cp_fil_up_t = tw_cp(ctx, p_fil_up_t)
let cp_fil_lo_b = tw_cp(ctx, p_fil_lo_b)
let cp_fil_lo_t = tw_cp(ctx, p_fil_lo_t)
let cp_tab_up_b = tw_cp(ctx, p_tab_up_b)
let cp_tab_up_t = tw_cp(ctx, p_tab_up_t)
let cp_tab_lo_b = tw_cp(ctx, p_tab_lo_b)
let cp_tab_lo_t = tw_cp(ctx, p_tab_lo_t)
let cp_seam_b = tw_cp(ctx, p_seam_b)
let cp_seam_t = tw_cp(ctx, p_seam_t)
let vp_out_up_b = ctx.vertex_point(cp_out_up_b)
let vp_out_up_t = ctx.vertex_point(cp_out_up_t)
let vp_out_lo_b = ctx.vertex_point(cp_out_lo_b)
let vp_out_lo_t = ctx.vertex_point(cp_out_lo_t)
let vp_fil_up_b = ctx.vertex_point(cp_fil_up_b)
let vp_fil_up_t = ctx.vertex_point(cp_fil_up_t)
let vp_fil_lo_b = ctx.vertex_point(cp_fil_lo_b)
let vp_fil_lo_t = ctx.vertex_point(cp_fil_lo_t)
let vp_tab_up_b = ctx.vertex_point(cp_tab_up_b)
let vp_tab_up_t = ctx.vertex_point(cp_tab_up_t)
let vp_tab_lo_b = ctx.vertex_point(cp_tab_lo_b)
let vp_tab_lo_t = ctx.vertex_point(cp_tab_lo_t)
let vp_seam_b = ctx.vertex_point(cp_seam_b)
let vp_seam_t = ctx.vertex_point(cp_seam_t)
// Bottom and top outline arcs and lines.
let out_b = tw_arc_edge(
ctx,
(0.0, 0.0),
g.r,
0.0,
cp_out_lo_b,
vp_out_lo_b,
cp_out_up_b,
vp_out_up_b,
true,
xform,
dir_up,
dir_right,
)
let out_t = tw_arc_edge(
ctx,
(0.0, 0.0),
g.r,
g.h,
cp_out_lo_t,
vp_out_lo_t,
cp_out_up_t,
vp_out_up_t,
true,
xform,
dir_up,
dir_right,
)
let fil_up_b = tw_arc_edge(
ctx,
g.c_up,
g.rf,
0.0,
cp_out_up_b,
vp_out_up_b,
cp_fil_up_b,
vp_fil_up_b,
false,
xform,
dir_up,
dir_right,
)
let fil_up_t = tw_arc_edge(
ctx,
g.c_up,
g.rf,
g.h,
cp_out_up_t,
vp_out_up_t,
cp_fil_up_t,
vp_fil_up_t,
false,
xform,
dir_up,
dir_right,
)
let fil_lo_b = tw_arc_edge(
ctx,
g.c_lo,
g.rf,
0.0,
cp_fil_lo_b,
vp_fil_lo_b,
cp_out_lo_b,
vp_out_lo_b,
false,
xform,
dir_up,
dir_right,
)
let fil_lo_t = tw_arc_edge(
ctx,
g.c_lo,
g.rf,
g.h,
cp_fil_lo_t,
vp_fil_lo_t,
cp_out_lo_t,
vp_out_lo_t,
false,
xform,
dir_up,
dir_right,
)
let tab_up_b = tw_line_edge(
ctx, cp_fil_up_b, vp_fil_up_b, cp_tab_up_b, vp_tab_up_b, p_fil_up_b, p_tab_up_b,
)
let tab_up_t = tw_line_edge(
ctx, cp_fil_up_t, vp_fil_up_t, cp_tab_up_t, vp_tab_up_t, p_fil_up_t, p_tab_up_t,
)
let tab_lo_b = tw_line_edge(
ctx, cp_tab_lo_b, vp_tab_lo_b, cp_fil_lo_b, vp_fil_lo_b, p_tab_lo_b, p_fil_lo_b,
)
let tab_lo_t = tw_line_edge(
ctx, cp_tab_lo_t, vp_tab_lo_t, cp_fil_lo_t, vp_fil_lo_t, p_tab_lo_t, p_fil_lo_t,
)
let end_b = tw_line_edge(
ctx, cp_tab_up_b, vp_tab_up_b, cp_tab_lo_b, vp_tab_lo_b, p_tab_up_b, p_tab_lo_b,
)
let end_t = tw_line_edge(
ctx, cp_tab_up_t, vp_tab_up_t, cp_tab_lo_t, vp_tab_lo_t, p_tab_up_t, p_tab_lo_t,
)
let bore_b = tw_arc_edge(
ctx,
(0.0, 0.0),
g.rb,
0.0,
cp_seam_b,
vp_seam_b,
cp_seam_b,
vp_seam_b,
true,
xform,
dir_up,
dir_right,
)
let bore_t = tw_arc_edge(
ctx,
(0.0, 0.0),
g.rb,
g.h,
cp_seam_t,
vp_seam_t,
cp_seam_t,
vp_seam_t,
true,
xform,
dir_up,
dir_right,
)
let seam = tw_line_edge(
ctx, cp_seam_b, vp_seam_b, cp_seam_t, vp_seam_t, p_seam_b, p_seam_t,
)
// Vertical edges.
let vert = fn(
a_cp : @step.Ref,
a_vp : @step.Ref,
b_cp : @step.Ref,
b_vp : @step.Ref,
a : Vec3,
b : Vec3,
) -> @step.Ref {
tw_line_edge(ctx, a_cp, a_vp, b_cp, b_vp, a, b)
}
let v_out_up = vert(
cp_out_up_b, vp_out_up_b, cp_out_up_t, vp_out_up_t, p_out_up_b, p_out_up_t,
)
let v_out_lo = vert(
cp_out_lo_b, vp_out_lo_b, cp_out_lo_t, vp_out_lo_t, p_out_lo_b, p_out_lo_t,
)
let v_fil_up = vert(
cp_fil_up_b, vp_fil_up_b, cp_fil_up_t, vp_fil_up_t, p_fil_up_b, p_fil_up_t,
)
let v_fil_lo = vert(
cp_fil_lo_b, vp_fil_lo_b, cp_fil_lo_t, vp_fil_lo_t, p_fil_lo_b, p_fil_lo_t,
)
let v_tab_up = vert(
cp_tab_up_b, vp_tab_up_b, cp_tab_up_t, vp_tab_up_t, p_tab_up_b, p_tab_up_t,
)
let v_tab_lo = vert(
cp_tab_lo_b, vp_tab_lo_b, cp_tab_lo_t, vp_tab_lo_t, p_tab_lo_b, p_tab_lo_t,
)
let faces : Array[@step.Ref] = []
let outer_cyl = tw_cylinder(ctx, (0.0, 0.0), g.r, xform, dir_up, dir_right)
let fil_up_cyl = tw_cylinder(ctx, g.c_up, g.rf, xform, dir_up, dir_right)
let fil_lo_cyl = tw_cylinder(ctx, g.c_lo, g.rf, xform, dir_up, dir_right)
let bore_cyl = tw_cylinder(ctx, (0.0, 0.0), g.rb, xform, dir_up, dir_right)
// Cylinder faces. The natural normal is radial from each axis.
let mid_out = v_norm(
xform.apply_dir(
(
@math.cos(
(
@math.atan2(g.out_lo.1, g.out_lo.0) +
@math.atan2(g.out_up.1, g.out_up.0)
) /
2.0,
),
@math.sin(
(
@math.atan2(g.out_lo.1, g.out_lo.0) +
@math.atan2(g.out_up.1, g.out_up.0)
) /
2.0,
),
0.0,
),
),
)
faces.push(
tw_face(
ctx,
[(out_b, true), (v_out_up, true), (out_t, false), (v_out_lo, false)],
outer_cyl,
mid_out,
[p_out_lo_b, p_out_up_b, p_out_up_t, p_out_lo_t],
),
)
let fil_up_natural = v_norm(
xform.apply_dir(
(
@math.cos(@math.atan2(g.out_up.1 - g.c_up.1, g.out_up.0 - g.c_up.0)),
@math.sin(@math.atan2(g.out_up.1 - g.c_up.1, g.out_up.0 - g.c_up.0)),
0.0,
),
),
)
faces.push(
tw_face(
ctx,
[(fil_up_b, true), (v_fil_up, true), (fil_up_t, false), (v_out_up, false)],
fil_up_cyl,
fil_up_natural,
[p_out_up_b, p_fil_up_b, p_fil_up_t, p_out_up_t],
),
)
let fil_lo_natural = v_norm(
xform.apply_dir(
(
@math.cos(@math.atan2(g.out_lo.1 - g.c_lo.1, g.out_lo.0 - g.c_lo.0)),
@math.sin(@math.atan2(g.out_lo.1 - g.c_lo.1, g.out_lo.0 - g.c_lo.0)),
0.0,
),
),
)
faces.push(
tw_face(
ctx,
[(fil_lo_b, true), (v_out_lo, true), (fil_lo_t, false), (v_fil_lo, false)],
fil_lo_cyl,
fil_lo_natural,
[p_fil_lo_b, p_out_lo_b, p_out_lo_t, p_fil_lo_t],
),
)
let bore_loop = ctx.edge_loop([
ctx.oriented_edge(seam, true),
ctx.oriented_edge(bore_t, true),
ctx.oriented_edge(seam, false),
ctx.oriented_edge(bore_b, false),
])
faces.push(
ctx.advanced_face(
[ctx.face_outer_bound(bore_loop, true)],
bore_cyl,
reverse,
),
)
// Planar tab sides and end.
let up_plane = tw_plane(
ctx,
p_fil_up_b,
v_norm(xform.apply_dir((0.0, 1.0, 0.0))),
v_norm(xform.apply_dir((0.0, 0.0, 1.0))),
)
faces.push(
tw_face(
ctx,
[(tab_up_b, true), (v_tab_up, true), (tab_up_t, false), (v_fil_up, false)],
up_plane,
v_norm(xform.apply_dir((0.0, 1.0, 0.0))),
[p_fil_up_b, p_tab_up_b, p_tab_up_t, p_fil_up_t],
),
)
let lo_plane = tw_plane(
ctx,
p_tab_lo_b,
v_norm(xform.apply_dir((0.0, -1.0, 0.0))),
v_norm(xform.apply_dir((0.0, 0.0, 1.0))),
)
faces.push(
tw_face(
ctx,
[(tab_lo_b, true), (v_fil_lo, true), (tab_lo_t, false), (v_tab_lo, false)],
lo_plane,
v_norm(xform.apply_dir((0.0, -1.0, 0.0))),
[p_tab_lo_b, p_fil_lo_b, p_fil_lo_t, p_tab_lo_t],
),
)
let end_plane = tw_plane(
ctx,
p_tab_up_b,
v_norm(xform.apply_dir((-1.0, 0.0, 0.0))),
v_norm(xform.apply_dir((0.0, 0.0, 1.0))),
)
faces.push(
tw_face(
ctx,
[(end_b, true), (v_tab_lo, true), (end_t, false), (v_tab_up, false)],
end_plane,
v_norm(xform.apply_dir((-1.0, 0.0, 0.0))),
[p_tab_up_b, p_tab_lo_b, p_tab_lo_t, p_tab_up_t],
),
)
// Caps: top uses the CCW outline; bottom reverses it.
let cap_top_outer = ctx.edge_loop([
ctx.oriented_edge(out_t, true),
ctx.oriented_edge(fil_up_t, true),
ctx.oriented_edge(tab_up_t, true),
ctx.oriented_edge(end_t, true),
ctx.oriented_edge(tab_lo_t, true),
ctx.oriented_edge(fil_lo_t, true),
])
let cap_bottom_outer = ctx.edge_loop([
ctx.oriented_edge(fil_lo_b, false),
ctx.oriented_edge(tab_lo_b, false),
ctx.oriented_edge(end_b, false),
ctx.oriented_edge(tab_up_b, false),
ctx.oriented_edge(fil_up_b, false),
ctx.oriented_edge(out_b, false),
])
let cap_top_hole = ctx.edge_loop([ctx.oriented_edge(bore_t, false)])
let cap_bottom_hole = ctx.edge_loop([ctx.oriented_edge(bore_b, true)])
let top_origin = tw_world((0.0, 0.0), g.h, xform)
let bottom_origin = tw_world((0.0, 0.0), 0.0, xform)
let cap_reverse = reverse
faces.push(
add_plane_face_from_loops(
ctx,
tw_cp(ctx, bottom_origin),
down,
right,
cap_bottom_outer,
[cap_bottom_hole],
reverse=cap_reverse,
),
)
faces.push(
add_plane_face_from_loops(
ctx,
tw_cp(ctx, top_origin),
up,
right,
cap_top_outer,
[cap_top_hole],
reverse=cap_reverse,
),
)
faces
}
///|
fn tw_outline(w : TabWasher, arc_samples~ : Int32 = 96) -> Array[Vec2] {
let g = match tw_geom(w) {
Some(g) => g
None => return []
}
let n = if arc_samples < 8 { 8 } else { arc_samples }
let pts : Array[Vec2] = []
// Outer major arc, lower-left tangent through +X to upper-left tangent.
let a_lo = @math.atan2(g.out_lo.1, g.out_lo.0)
let a_up = @math.atan2(g.out_up.1, g.out_up.0)
for i in 0..<=n {
let t = i.to_double() / n.to_double()
let a = a_lo + (a_up - a_lo) * t
pts.push((g.r * @math.cos(a), g.r * @math.sin(a)))
}
let fil_up_a0 = @math.atan2(g.out_up.1 - g.c_up.1, g.out_up.0 - g.c_up.0)
let fil_up_a1 = @math.atan2(g.tab_up.1 - g.c_up.1, g.tab_up.0 - g.c_up.0)
let add_fillet = fn(c : Vec2, a0 : Double, a1 : Double) -> Unit {
for i in 1..<=n {
let t = i.to_double() / n.to_double()
let a = a0 + (a1 - a0) * t
pts.push((c.0 + g.rf * @math.cos(a), c.1 + g.rf * @math.sin(a)))
}
}
add_fillet(g.c_up, fil_up_a0, fil_up_a1)
pts.push(g.end_up)
pts.push(g.end_lo)
let fil_lo_a0 = @math.atan2(g.tab_lo.1 - g.c_lo.1, g.tab_lo.0 - g.c_lo.0)
let fil_lo_a1 = @math.atan2(g.out_lo.1 - g.c_lo.1, g.out_lo.0 - g.c_lo.0)
add_fillet(g.c_lo, fil_lo_a0, fil_lo_a1)
// The final point is the lower outer tangent; drop it if it duplicates the
// first outline point.
let nn = pts.length()
if nn > 1 {
let a = pts[nn - 1]
let b = pts[0]
if (a.0 - b.0).abs() < 1.0e-12 && (a.1 - b.1).abs() < 1.0e-12 {
let _ = pts.pop()
}
}
pts
}
///|
fn tw_polygon_moments(points : Array[Vec2]) -> (Double, Double, Double) {
let n = points.length()
let mut a = 0.0
let mut mx = 0.0
let mut my = 0.0
for i in 0.. (Double, Vec2) {
let g = match tw_geom(w) {
Some(g) => g
None => return (0.0, (0.0, 0.0))
}
let points = tw_outline(w)
let (area, mx, my) = tw_polygon_moments(points)
let bore = @math.PI * g.rb * g.rb
let material = area - bore
if material <= 0.0 {
(0.0, (0.0, 0.0))
} else {
(material, (mx / area, my / area))
}
}
///|
fn an_tab_inventory(w : TabWasher) -> SurfaceInventory {
match tw_geom(w) {
None => an_inventory(0, 0, 0, 0, 0, 0, 0, 0)
Some(_) => an_inventory(5, 4, 0, 0, 0, 0, 0, 0)
}
}
///|
fn an_tab_section(w : TabWasher, z : Double) -> Result[Section, AnalysisError] {
if z < -CAD_LENGTH_EPS || z > w.thickness_mm + CAD_LENGTH_EPS {
return Err(EmptySection)
}
let g = match tw_geom(w) {
Some(g) => g
None => return Err(DegenerateProfile)
}
let (material, _c) = tw_area_centroid(w)
if material <= 0.0 {
return Err(EmptySection)
}
let points = tw_outline(w)
let mut perimeter = 2.0 * @math.PI * g.rb
for i in 0.. (Array[Vec3], Array[Array[Int32]]) {
let g = match tw_geom(w) {
Some(g) => g
None => return ([], [])
}
let outline = tw_outline(w, arc_samples~)
let n = outline.length()
if n < 3 {
return ([], [])
}
let verts : Array[Vec3] = []
for p in outline {
verts.push((p.0, p.1, 0.0))
}
for p in outline {
verts.push(
(
g.rb * p.0 / Double::sqrt(p.0 * p.0 + p.1 * p.1),
g.rb * p.1 / Double::sqrt(p.0 * p.0 + p.1 * p.1),
0.0,
),
)
}
for p in outline {
verts.push((p.0, p.1, g.h))
}
for p in outline {
verts.push(
(
g.rb * p.0 / Double::sqrt(p.0 * p.0 + p.1 * p.1),
g.rb * p.1 / Double::sqrt(p.0 * p.0 + p.1 * p.1),
g.h,
),
)
}
let base_ob = 0
let base_ib = n
let base_ot = 2 * n
let base_it = 3 * n
let faces : Array[Array[Int32]] = []
for i in 0..