OpenSCAD-Generators/s-hook/s-hook.scad

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();