Merge pull request #1833 from adrianVmariano/master

Add ring_hook
This commit is contained in:
adrianVmariano 2025-10-23 20:28:53 -04:00 committed by GitHub
commit 7f92da42c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 286 additions and 10 deletions

View file

@ -23,9 +23,8 @@ PrioritizeFiles:
attachments.scad
shapes2d.scad
shapes3d.scad
masks.scad
drawing.scad
masks2d.scad
masks3d.scad
distributors.scad
color.scad
partitions.scad
@ -70,6 +69,7 @@ PrioritizeFiles:
tripod_mounts.scad
walls.scad
wiring.scad
hooks.scad
DefineHeader(BulletList): Side Effects
DefineHeader(Table;Headers=Anchor Name|Position): Named Anchors
DefineHeader(Table;Headers=Anchor Type|What it is): Anchor Types

269
hooks.scad Normal file
View file

@ -0,0 +1,269 @@
include<BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// LibFile: hooks.scad
// Functions and modules for creating hooks and hook like parts.
// At the moment only one part is supported, a ring hook.
// Includes:
// include <BOSL2/hooks.scad>
// FileGroup: Parts
// FileSummary: Hooks and hook-like parts.
//////////////////////////////////////////////////////////////////////
// Module: ring_hook()
// Synopsis: A hook with a circular hole or attached cylinder
// SynTags: Geom
// Topics: Parts
// See Also: prismoid(), rounded_prism(), ycyl()
// Usage:
// ring_hook(base_size, hole_z, or, od=, [ir=], [hole=], [rounding=], [fillet=], [hole_rounding=], [anchor=], [spin=], [orient=])
// Description:
// Form a part that attaches a loop hook with a cylindrical hole a specified distance away from its mount point.
// You specify a rectangle defining the base a hole diameter or radius, and `hole_z`, a distance from the base to the hole.
// You can set the hole diameter to zero to create a solid paddle with no hole.
// .
// In order to calculate a tangent where the base joins the cylinder,
// the lower corners of the base must be outside the cylinder (see Example 3). This scenario occurs when
// the base is narrower than the Y-cylinder and hole_z is less than Y-cylinder radius. Also, hole_z must
// be large enough to accommodate hole rounding and base rounding.
// Arguments:
// base_size = 2-vector specifying x and y sizes of the base
// hole_z = distance in the z direction from the base to the center of the hole
// or = radius of the cylindrical portion of the part (or zero to create no hole)
// ---
// od = diameter of the cylindrical portion of the part
// ir, id = optional radius/diameter of the center hole
// hole = Set to "circle" for a circle hole, "D" for a D-shaped (semicircular) hole or a path to create a custom hole. Default: "circle"
// rounding = rounding of the vertical-ish edges of the prismoid and the exposed edges of the cylinder
// fillet = base rounding, set negative to form a rounded edge instead of fillet
// hole_rounding = rounding of the optional hole
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP`
// Named anchors:
// hole_front = front, center of the cylindrical portion of the part (same as the part FRONT if hole_z=or)
// hole_back = back, center of the cylindrical portion of the part (same as the part BACK if hole_z=or)
// tangent_right = right side anchor at the point where the prismoid merges with Y-cylinder, at y=0
// tangent_left = left side anchor at the point where the prismoid merges with Y-cylinder, at y=0
// Attachable Parts:
// "inside" = The inner hole (not defined if there is no hole)
// Example: Ring connector
// ring_hook([50, 10], 25, 25, ir=20);
// Example: Widen the base, add base fillet, no hole
// ring_hook([70, 10], 25, or=25, ir=0, fillet=3, rounding=1.5);
// Example: Narrow base
// ring_hook([40, 10], 25, or=25, ir=0, fillet=3, rounding=1.5);
// Example(3D,VPR=[90,0,0]): If the base is narrower than the cylinder diameter then its corners have to be outside the cylinder for this shape to be defined because it requires a tangent line to the cylinder. This example shows a valid base corner point in blue. An invalid corner point appears in red: no tangent to the circle exists through the red point.
// hole_z = 20;
// base_size = [40, 10];
// outer_radius = 25;
// ring_hook(base_size, hole_z, outer_radius, ir=0);
// up(hole_z) color("blue", 0.25) ycyl(r=outer_radius, h=base_size.y + 2);
// right(0.5*base_size.x) color("blue") ycyl(r=1, h=base_size.y + 2, $fn=12);
// right(0.3*base_size.x) color("red") ycyl(r=1, h=base_size.y + 2, $fn=12);
// Example(3D,VPR=[60.60,0.00,62.10]): Through hole can be specified using or/od, ir/id, wall variables. All of these are equivalent.
// ydistribute(spacing = 25) {
// ring_hook([50, 10], 40, or=25, ir=20);
// ring_hook([50, 10], 40, 25, wall=5);
// ring_hook([50, 10], 40, wall=5, ir=20);
// ring_hook([50, 10], 40, od=50, id=40);
// ring_hook([50, 10], 40, od=50, wall=5);
// ring_hook([50, 10], 40, wall=5, id=40);
// }
// Example: Semi-circular through hole (a D-hole):
// ring_hook([50, 10], 12, 25, ir=15, hole="D", rounding=3, hole_rounding=3, fillet=2);
// Example: hole_z must be greater than 0 with no hole or with hole="D". Here hole_z is 1, close to the minimum value of zero.
// xdistribute(spacing=60){
// ring_hook([50, 10], 1, 25, ir=0);
// ring_hook([50, 10], 1, 25, ir=15, hole="D");
// }
// Example: hole_z must be greater than ir + hole_rounding + fillet when hole="circle". Here hole_z is only 1 larger than the minimum.
// ring_hook([50, 10], hole_z=27, or=25, ir=20, hole_rounding=3, fillet=3);
// Example: Rounding all edges
// ring_hook([50, 10], 40, 25, ir=15, rounding=5, hole_rounding=5, fillet=5);
// Example: Giving an arbitrary path for the hole, in this case an octagon to make the object printable without support.
// ring_hook([50, 20],30, 25, ir=10, hole=octagon(side=10,realign=true), hole_rounding=3, rounding=4) ;
// Example: The ring_hook includes 4 custom anchors: front & back at the center of the cylinder component and left & right at the tangent points.
// ring_hook([55, 10], 12, 25, ir=0) show_anchors(std=false);
// Example: Use the custom anchor to place a screw hole
// include <BOSL2/screws.scad>
// diff()
// ring_hook([20, 10], 15, 7, ir=0, fillet=3)
// attach("hole_front")
// screw_hole("M5", length=20, head="socket", atype="head", anchor=TOP, orient=UP);
// Example: Use the custom anchor to create a cylindrical extension instead of a hole
// $fs=1;$fa=2;
// ring_hook([30,10], hole_z=17, or=10, ir=0, rounding=1.5)
// attach("hole_front", BOT)
// cyl(d=10, h=14, rounding1=-2, rounding2=2);
// Example(3D,VPR=[83.70,0.00,29.20]): Use the "inner" part to create a bar across the hole:
// diff()
// ring_hook([50, 20],30, 25, ir=10, hole_rounding=3, rounding=4)
// attach_part("inner")
// prism_connector( circle(3, $fn=16),
// parent(), LEFT,
// parent(), RIGHT, fillet=1);
module ring_hook(base_size, hole_z, or, ir, od, id, wall, hole="circle",
rounding=0, fillet=0, hole_rounding=0,
anchor=BOTTOM, spin=0, orient=UP) {
or_tmp = get_radius(r=or, d=od);
ir_tmp = get_radius(r=ir, d=id);
dummy = assert(num_defined([ir_tmp, or_tmp, wall])==2, "Must define exactly two of r/d, ir/id and wall");
ir = is_def(ir_tmp) ? ir_tmp : or_tmp - wall;
or = is_def(or_tmp) ? or_tmp : ir + wall;
dummy2 = assert(ir <= or, "Hole doesn't fit or wall size is negative")
assert(sqrt((0.5*base_size.x)^2 + hole_z^2) > or, "Base corners must be outside the cylinder")
assert(in_list(hole,["circle","D"]) || is_path(hole,2), "hole must be \"circle\", \"D\" or a 2d path")
assert(all_nonnegative([hole_rounding]), "hole_rounding must be greater than or equal to 0");
if (ir > 0 && hole=="circle")
assert(ir + hole_rounding < hole_z-fillet,str("ir + hole_rounding must be less than ",hole_z-fillet));
z_offset = (hole_z - or)/2;
tangents = circle_point_tangents(
r=or,
cp=[0,hole_z],
pt=[0.5*base_size.x, 0]);
// we want the tangent with the larger y value
tangent = tangents[0].y > tangents[1].y
? tangents[0] : tangents[1];
// anchor calcs
angle = atan((tangent.x - 0.5*base_size.x)/tangent.y);
top_x = 0.5*base_size.x + (hole_z + or)*tan(angle);
// when or > 0.5*base_size.x, need to move the anchor
// use x^2 + y^2 = r^2, x = sqrt(r^2 - y^2)
delta_y = z_offset;
mid_x = sqrt(or^2 - delta_y^2);
h = hole_z + or;
w = base_size.y;
size = [base_size.x, w];
size2 = [2*top_x, w];
right_tang_dir = unit([tangent.x, 0, tangent.y-hole_z]);
left_tang_dir = unit([-tangent.x,0, tangent.y-hole_z]);
anchors = [
named_anchor("hole_front", [0, -w/2, z_offset], FRONT, 0),
named_anchor("hole_back", [0, w/2, z_offset], BACK, 180),
named_anchor("tangent_right", [tangent[0], 0, tangent[1] - hole_z + z_offset], right_tang_dir, _compute_spin(right_tang_dir,UP,BACK)),
named_anchor("tangent_left", [-tangent[0], 0, tangent[1] - hole_z + z_offset], left_tang_dir, _compute_spin(left_tang_dir,UP,BACK)),
];
override = [
for (i = [-1, 1], j=[-1:1], k=[0:1])
if (k==0 && j!=0 && or > 0.5*base_size.x)
[[i, j, 0],
[mid_x*unit([i, 0, 0]) + 0.5*base_size.y*unit([0, j, 0])]]
else if (k==0 && or > 0.5*base_size.x)
[[i, 0, 0], [mid_x*unit([i, 0, 0])]]
else if (k==1 && j==0)
[[i, 0, 1], [or*sin(45)*unit([i, 0, 0])
+ (z_offset + or*sin(45))*unit([0, 0, k])]]
else if (k==1)
[[i, j, 1], [or*sin(45)*unit([i, 0, 0])
+ 0.5*base_size.y*unit([0, j, 0])
+ (z_offset + or*sin(45))*unit([0, 0, k])]]
];
profile = is_path(hole) ? hole
: hole=="D" ? arc(angle=180, r=ir, rounding=hole_rounding, wedge=true)
: ir > 0 ? circle(ir)
: undef;
parts = is_undef(profile) ? undef
:[
define_part("inner",
attach_geom(
region=[ymove(z_offset,profile)], l=size.y),
T=xrot(90),
inside=true)
];
attachable( anchor, spin, orient,
size=point3d(size,h),
size2=size2,
anchors=anchors,
override=override,
parts=parts
) {
down(h/2)
difference() {
union() {
startangle = atan2(tangent.y-hole_z, tangent.x);
endangle = posmod(atan2(tangent.y-hole_z, -tangent.x),360);
steps = segs(or,endangle-startangle)+1;
delta = (endangle-startangle)/(steps-1);
profile = rounding == 0 ? [[or,0,-base_size.y/2],[or,0,base_size.y/2]]
: let(
// rounded prism roundings are computed on top face, so cos() correction is needed
// to get them to align properly
bez = _smooth_bez_fill([//[or-rounding*(startangle>0?cos(startangle):1),0,-base_size.y/2],
[or-rounding,0,-base_size.y/2],
[or,0,-base_size.y/2],
[or,0,-base_size.y/2+rounding]],0.92),
pts = bezier_curve(bez)
)
concat(pts, reverse(zflip(pts)));
toplist = [
[for(pt=profile) [0,-or,pt.z]],
if (startangle<0)
move(-[tangent.x-base_size.x/2,tangent.y] ,zrot(startangle, profile)),
for(angle = lerpn(startangle, endangle, steps)) zrot(angle, profile),
if (startangle<0)
move(-[-tangent.x+base_size.x/2,tangent.y] ,zrot(endangle, profile)),
];
intersection(){
up(hole_z)xrot(90)
vnf_vertex_array(transpose(toplist),caps=true,col_wrap=true,reverse=true,triangulate=true);
cuboid([max(base_size.x,2*or),w+1, or+hole_z+1],anchor=BOT);
}
// When base is outside the circle the base needs to be clipped so the roundings don't interfere
// This mask does this clipping
maskpath2 = [zrot(startangle,[or+1,0,0]),
zrot(startangle,[or-rounding, 0, 0]),
zrot(startangle+delta, [or-rounding-.1, 0, 0]),
];
maskpath = up(hole_z,xrot(90, [each maskpath2,
[maskpath2[0].x, maskpath2[0].x*tan(startangle+delta),0]
]));
difference(){
rounded_prism(
rect(base_size),
rect( [ 2*tangent.x, w ] ),
h=tangent.y,
joint_bot=-fillet,
joint_sides=rounding,
k_sides=0.92, k_bot=0.92,
anchor=BOT );
if (startangle>0)
xflip_copy()
vnf_vertex_array([fwd(w/2+1, maskpath), back(w/2+1, maskpath)],
col_wrap=true,caps=true,reverse=true);
}
}
if (ir > 0) {
up(hole_z)
prism_connector(
profile,
parent(), FRONT,
parent(), BACK,
fillet=hole_rounding);
}
}
children();
}
}

View file

@ -280,9 +280,9 @@ function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, clip_angle, fl
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// Example(2D): Mask defined by cut
// mask2d_smooth(cut=3);
// Example(2D): Mask defined by symmetric joint length
// mask2d_smooth(joint=10);
// Example(2D): Asymmetric mask by joint length with different lengths and a larger excess
// Example(2D): Mask defined by symmetric joint length with larger excess (which helps show the ends of the mask)
// mask2d_smooth(joint=10,excess=0.5);
// Example(2D): Asymmetric mask by joint length with different lengths
// mask2d_smooth(joint=[10,7],excess=0.5);
// Example(2D): Acute angle mask by cut
// mask2d_smooth(mask_angle=66,cut=3,excess=0.5);

View file

@ -858,8 +858,10 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
// The erroneous removal of segments is more common when your input
// contains very small segments and in this case can result in an invalid situation where the remaining
// valid segments are parallel and cannot be connected to form an offset curve. If this happens, you
// get an error message to this effect. The only solutions are either to remove the small segments with {{deduplicate()}},
// or if your path permits it, to set check_valid to false.
// get an error message to this effect. The only solutions are either to remove small segments with {{resample_path()}} or
// generate your data with fewer points (e.g. by using a smaller `$fn` or larger `$fa` and `$fs` when constructing your input).
// Be aware that chamfer and rounding increase the length of the path, so iterated offsets can lead to exponential
// growth in the path length.
// .
// Another situation that can arise with validity testing is that the test is not sufficiently thorough and some
// segments persist that should be eliminated. In this case, increase `quality` from its default of 1 to a value of 2 or 3.
@ -869,7 +871,7 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
// .
// When invalid segments are eliminated, the path length decreases, and multiple points on the input path map to the same point
// on the offset path. If you use chamfering or rounding, then
// the chamfers and roundings can increase the length of the output path. Hence points in the output may be
// the chamfers and roundings increase the length of the output path. Hence points in the output may be
// difficult to associate with the input. If you want to maintain alignment between the points you
// can use the `same_length` option. This option requires that you use `delta=` with `chamfer=false` to ensure
// that no points are added. with `same_length`, when points collapse to a single point in the offset, the output includes
@ -1067,7 +1069,11 @@ function offset(
cornercheck = [for(i=idx(goodsegs)) (!closed && (i==0 || i==len(goodsegs)-1))
|| is_def(sharpcorners[i])
|| approx(unit(deltas(select(goodsegs,i-1))[0]) * unit(deltas(goodsegs[i])[0]),-1)],
dummyA = assert(len(sharpcorners)==2 || all(cornercheck),"\nTwo consecutive valid offset segments are parallel but do not meet at their ends, maybe because path contains very short segments that were mistakenly flagged as invalid; unable to compute offset. If you get this error from offset_sweep() try setting ofset=\"delta\"."),
dummyA = assert(len(sharpcorners)==2 || all(cornercheck),
str("\nUnable to compute offset due to segments that are very close to parallel but not exactly parallel. \n",
"This is usually caused by too many points or points that are too close together. \n",
"Use fewer points (lower $fn, larger $fa/$fs) or use resample_path(). \n",
"If you get this error from offset_sweep() using offset=\"delta\" may help")),
reversecheck =
!same_length
|| !(is_def(delta) && !chamfer) // Reversals only a problem in delta mode without chamfers

View file

@ -598,7 +598,7 @@ function _rounding_offsets(edgespec,z_dir=1) =
// Synopsis: Create a smoothed path passing through all the points of a given path, or passing through all the segment midpoint tangents.
// SynTags: Path
// Topics: Rounding, Paths
// See Also: round_corners(), smooth_path(), path_join(), offset_stroke()
// See Also: round_corners(), smooth_path(), path_join(), offset_stroke(), squircle()
// Usage: "edges" method
// smoothed = smooth_path(path, [tangents], [size=|relsize=], [method="edges"], [splinesteps=], [closed=], [uniform=]);
// Usage: "corners" method

View file

@ -1827,6 +1827,7 @@ function rect_tube(
// wedge([40, 80, 30], center=true)
// show_anchors(std=false);
// Example(3D): Rounding the top of the wedge using the "top_edge" anchor
// $fn=32;
// diff()
// wedge([10,15,7])
// attach("top_edge", FWD+LEFT, inside=true)