225 lines
7.5 KiB
OpenSCAD
225 lines
7.5 KiB
OpenSCAD
/*
|
|
PARAMETRIC S-HOOK GENERATOR — reconstructed repository edition
|
|
---------------------------------------------------------------------------
|
|
This file reconstructs the final corrected S-hook design discussed on
|
|
2026-09-23. The original final source was delivered inline in chat and was
|
|
not preserved as a downloadable artifact.
|
|
|
|
The critical geometry correction is preserved:
|
|
- the upper hook joins the neck at the RIGHT side of its ellipse (0 deg)
|
|
- the lower hook joins the neck at the LEFT side of its ellipse (180 deg)
|
|
- the upper arc therefore terminates at 0 deg
|
|
- the lower arc begins at 180 deg
|
|
|
|
This removes the earlier 90-degree neck/arc orientation error.
|
|
|
|
Units: millimetres.
|
|
Intended for recent OpenSCAD Nightly, but uses conservative syntax.
|
|
*/
|
|
|
|
/* [Hook geometry] */
|
|
top_radius = 22; // [5:0.5:80]
|
|
bottom_radius = 18; // [5:0.5:80]
|
|
top_sweep = 290; // [190:1:340]
|
|
bottom_sweep = 290; // [190:1:340]
|
|
neck_length = 22; // [0:0.5:100]
|
|
neck_curve = 0; // [-20:0.5:20]
|
|
overall_scale_x = 1.0; // [0.25:0.05:3]
|
|
overall_scale_y = 1.0; // [0.25:0.05:3]
|
|
|
|
/* [Section] */
|
|
profile = "Round"; // [Round,Oval,Flat]
|
|
rod_diameter = 5.0; // [1.5:0.1:16]
|
|
oval_z_ratio = 0.70; // [0.25:0.05:1.5]
|
|
flat_thickness = 4.0; // [0.8:0.1:12]
|
|
flat_width = 6.0; // [1.5:0.1:20]
|
|
|
|
/* [Tips] */
|
|
tip_style = "Rounded"; // [Plain,Rounded,Bulb]
|
|
tip_bulb_scale = 1.30; // [1:0.05:2]
|
|
tip_extension = 0; // [0:0.5:15]
|
|
|
|
/* [Reinforcement] */
|
|
neck_reinforcement = 0; // [0:0.1:5]
|
|
|
|
/* [Quality] */
|
|
quality = 72; // [16:4:180]
|
|
path_steps = 72; // [20:2:180]
|
|
show_path_markers = false;
|
|
|
|
$fn = quality;
|
|
EPS = 0.01;
|
|
|
|
function lerp(a,b,t) = a + (b-a)*t;
|
|
function bez2(p0,p1,p2,t) =
|
|
let(u=1-t)
|
|
[u*u*p0[0] + 2*u*t*p1[0] + t*t*p2[0],
|
|
u*u*p0[1] + 2*u*t*p1[1] + t*t*p2[1]];
|
|
|
|
function point_on(center, rx, ry, a) =
|
|
[center[0] + rx*cos(a), center[1] + ry*sin(a)];
|
|
|
|
top_center = [-top_radius, neck_length/2];
|
|
bottom_center = [bottom_radius, -neck_length/2];
|
|
|
|
top_connection = [0, neck_length/2];
|
|
bottom_connection = [0, -neck_length/2];
|
|
|
|
// A clockwise sweep from top_sweep to 0° approaches the right tangent from
|
|
// above, then continues down the neck. The earlier reconstruction used
|
|
// 360-top_sweep..360, which put the opening and incoming arc on the wrong side.
|
|
top_start_angle = top_sweep;
|
|
top_points = [
|
|
for (i=[0:path_steps])
|
|
let(a = lerp(top_start_angle, 0, i/path_steps))
|
|
point_on(top_center, top_radius, top_radius, a)
|
|
];
|
|
|
|
// Neck runs from upper right tangent to lower left tangent.
|
|
// A quadratic Bezier allows an optional sideways bow. Larger neck_curve values
|
|
// deliberately turn the join tangent while preserving its downward direction.
|
|
neck_control = [neck_curve, 0];
|
|
neck_points = [
|
|
for (i=[1:path_steps-1])
|
|
bez2(top_connection, neck_control, bottom_connection, i/path_steps)
|
|
];
|
|
|
|
// Lower path deliberately STARTS at 180° (left tangent point).
|
|
bottom_points = [
|
|
for (i=[0:path_steps])
|
|
let(a = lerp(180, 180 + bottom_sweep, i/path_steps))
|
|
point_on(bottom_center, bottom_radius, bottom_radius, a)
|
|
];
|
|
|
|
hook_path = concat(top_points, neck_points, bottom_points);
|
|
|
|
function vsub(a,b) = [a[0]-b[0], a[1]-b[1]];
|
|
function vlen(v) = sqrt(v[0]*v[0] + v[1]*v[1]);
|
|
function vnorm(v) = let(l=max(EPS,vlen(v))) [v[0]/l,v[1]/l];
|
|
function vdot(a,b) = a[0]*b[0] + a[1]*b[1];
|
|
|
|
// Discrete direction checks protect the reconstructed 0°/180° joins. The
|
|
// vectors need to continue in the same general direction; neck_curve is allowed
|
|
// to change the exact tangent as part of its documented shape control.
|
|
top_incoming = vsub(top_points[len(top_points)-1], top_points[len(top_points)-2]);
|
|
neck_outgoing = vsub(neck_points[0], top_connection);
|
|
neck_incoming = vsub(bottom_connection, neck_points[len(neck_points)-1]);
|
|
bottom_outgoing = vsub(bottom_points[1], bottom_points[0]);
|
|
assert(vlen(vsub(top_points[len(top_points)-1], top_connection)) < EPS,
|
|
"Upper arc must end at the neck connection");
|
|
assert(vlen(vsub(bottom_points[0], bottom_connection)) < EPS,
|
|
"Lower arc must start at the neck connection");
|
|
assert(vdot(vnorm(top_incoming), vnorm(neck_outgoing)) > 0,
|
|
"Upper arc and neck must continue in the same general direction");
|
|
assert(vdot(vnorm(neck_incoming), vnorm(bottom_outgoing)) > 0,
|
|
"Neck and lower arc must continue in the same general direction");
|
|
|
|
function tip_extended_point(p0,p1,amount) =
|
|
let(v=vnorm(vsub(p0,p1)))
|
|
[p0[0]+v[0]*amount,p0[1]+v[1]*amount];
|
|
|
|
path_with_tips =
|
|
tip_extension > 0
|
|
? concat(
|
|
[tip_extended_point(hook_path[0],hook_path[1],tip_extension)],
|
|
hook_path,
|
|
[tip_extended_point(
|
|
hook_path[len(hook_path)-1],
|
|
hook_path[len(hook_path)-2],
|
|
tip_extension
|
|
)]
|
|
)
|
|
: hook_path;
|
|
|
|
module flat_node(p, d=flat_width) {
|
|
translate([p[0],p[1],0]) circle(d=d);
|
|
}
|
|
|
|
module flat_path(points) {
|
|
linear_extrude(height=flat_thickness)
|
|
union()
|
|
for (i=[0:len(points)-2])
|
|
hull() {
|
|
flat_node(points[i]);
|
|
flat_node(points[i+1]);
|
|
}
|
|
}
|
|
|
|
module round_node(p, d=rod_diameter, zratio=1) {
|
|
translate([p[0],p[1],rod_diameter*zratio/2])
|
|
scale([1,1,zratio])
|
|
sphere(d=d);
|
|
}
|
|
|
|
module round_path(points, zratio=1) {
|
|
union()
|
|
for (i=[0:len(points)-2])
|
|
hull() {
|
|
round_node(points[i], rod_diameter, zratio);
|
|
round_node(points[i+1], rod_diameter, zratio);
|
|
}
|
|
}
|
|
|
|
module tip_features(points) {
|
|
if (tip_style == "Bulb") {
|
|
if (profile == "Flat") {
|
|
linear_extrude(height=flat_thickness) {
|
|
translate(points[0]) circle(d=flat_width*tip_bulb_scale);
|
|
translate(points[len(points)-1]) circle(d=flat_width*tip_bulb_scale);
|
|
}
|
|
} else {
|
|
zr = profile == "Oval" ? oval_z_ratio : 1;
|
|
round_node(points[0], rod_diameter*tip_bulb_scale, zr);
|
|
round_node(points[len(points)-1], rod_diameter*tip_bulb_scale, zr);
|
|
}
|
|
} else if (tip_style == "Rounded") {
|
|
// Base path already ends in round nodes/caps.
|
|
}
|
|
}
|
|
|
|
module reinforcement() {
|
|
if (neck_reinforcement > 0) {
|
|
if (profile == "Flat") {
|
|
linear_extrude(height=flat_thickness)
|
|
hull() {
|
|
translate(top_connection) circle(d=flat_width + 2*neck_reinforcement);
|
|
translate(bottom_connection) circle(d=flat_width + 2*neck_reinforcement);
|
|
}
|
|
} else {
|
|
zr = profile == "Oval" ? oval_z_ratio : 1;
|
|
for (p=[top_connection,bottom_connection])
|
|
round_node(p,rod_diameter+2*neck_reinforcement,zr);
|
|
}
|
|
}
|
|
}
|
|
|
|
module hook() {
|
|
scale([overall_scale_x,overall_scale_y,1])
|
|
union() {
|
|
if (profile == "Flat")
|
|
flat_path(path_with_tips);
|
|
else if (profile == "Oval")
|
|
round_path(path_with_tips,oval_z_ratio);
|
|
else
|
|
round_path(path_with_tips,1);
|
|
|
|
tip_features(path_with_tips);
|
|
reinforcement();
|
|
}
|
|
}
|
|
|
|
module markers() {
|
|
if (show_path_markers)
|
|
color("red")
|
|
for (p=hook_path)
|
|
translate([p[0],p[1],rod_diameter+2])
|
|
sphere(d=max(0.8,rod_diameter*0.18));
|
|
}
|
|
|
|
echo("S-Hook Generator — reconstructed corrected edition");
|
|
echo("Upper connection", top_connection, "upper arc terminates at 0 degrees");
|
|
echo("Lower connection", bottom_connection, "lower arc begins at 180 degrees");
|
|
echo("Profile", profile, "Top sweep", top_sweep, "Bottom sweep", bottom_sweep);
|
|
|
|
hook();
|
|
markers();
|