diff --git a/README.md b/README.md
index b8fbfc7..292142d 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,10 @@ The printable holes allow your slicer to bridge the gap inside the countersunk m
## Recommendations
For best results, use a version of OpenSCAD with the fast-csg feature. As of writing, this feature is only implemented in the [development snapshots](https://openscad.org/downloads.html). To enable the feature, go to Edit > Preferences > Features > fast-csg. On my computer, this sped up rendering from 10 minutes down to a couple of seconds, even for comically large bins.
+## External libraries
+
+- `threads-scad` (https://github.com/rcolyer/threads-scad) is used for creating threaded holes, and is included in this project under `external/threads-scad/threads.scad`.
+
## Enjoy!
[]()
diff --git a/external/threads-scad/threads.scad b/external/threads-scad/threads.scad
new file mode 100644
index 0000000..344c51a
--- /dev/null
+++ b/external/threads-scad/threads.scad
@@ -0,0 +1,631 @@
+// Created 2016-2017 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+//
+// https://www.thingiverse.com/thing:1686322
+//
+// v2.1
+
+
+screw_resolution = 0.2; // in mm
+
+
+// Provides standard metric thread pitches.
+function ThreadPitch(diameter) =
+ (diameter <= 64) ?
+ lookup(diameter, [
+ [2, 0.4],
+ [2.5, 0.45],
+ [3, 0.5],
+ [4, 0.7],
+ [5, 0.8],
+ [6, 1.0],
+ [7, 1.0],
+ [8, 1.25],
+ [10, 1.5],
+ [12, 1.75],
+ [14, 2.0],
+ [16, 2.0],
+ [18, 2.5],
+ [20, 2.5],
+ [22, 2.5],
+ [24, 3.0],
+ [27, 3.0],
+ [30, 3.5],
+ [33, 3.5],
+ [36, 4.0],
+ [39, 4.0],
+ [42, 4.5],
+ [48, 5.0],
+ [52, 5.0],
+ [56, 5.5],
+ [60, 5.5],
+ [64, 6.0]
+ ]) :
+ diameter * 6.0 / 64;
+
+
+// Provides standard metric hex head widths across the flats.
+function HexAcrossFlats(diameter) =
+ (diameter <= 64) ?
+ lookup(diameter, [
+ [2, 4],
+ [2.5, 5],
+ [3, 5.5],
+ [3.5, 6],
+ [4, 7],
+ [5, 8],
+ [6, 10],
+ [7, 11],
+ [8, 13],
+ [10, 16],
+ [12, 18],
+ [14, 21],
+ [16, 24],
+ [18, 27],
+ [20, 30],
+ [22, 34],
+ [24, 36],
+ [27, 41],
+ [30, 46],
+ [33, 50],
+ [36, 55],
+ [39, 60],
+ [42, 65],
+ [48, 75],
+ [52, 80],
+ [56, 85],
+ [60, 90],
+ [64, 95]
+ ]) :
+ diameter * 95 / 64;
+
+// Provides standard metric hex head widths across the corners.
+function HexAcrossCorners(diameter) =
+ HexAcrossFlats(diameter) / cos(30);
+
+
+// Provides standard metric hex (Allen) drive widths across the flats.
+function HexDriveAcrossFlats(diameter) =
+ (diameter <= 64) ?
+ lookup(diameter, [
+ [2, 1.5],
+ [2.5, 2],
+ [3, 2.5],
+ [3.5, 3],
+ [4, 3],
+ [5, 4],
+ [6, 5],
+ [7, 5],
+ [8, 6],
+ [10, 8],
+ [12, 10],
+ [14, 12],
+ [16, 14],
+ [18, 15],
+ [20, 17],
+ [22, 18],
+ [24, 19],
+ [27, 20],
+ [30, 22],
+ [33, 24],
+ [36, 27],
+ [39, 30],
+ [42, 32],
+ [48, 36],
+ [52, 36],
+ [56, 41],
+ [60, 42],
+ [64, 46]
+ ]) :
+ diameter * 46 / 64;
+
+// Provides standard metric hex (Allen) drive widths across the corners.
+function HexDriveAcrossCorners(diameter) =
+ HexDriveAcrossFlats(diameter) / cos(30);
+
+// Provides metric countersunk hex (Allen) drive widths across the flats.
+function CountersunkDriveAcrossFlats(diameter) =
+ (diameter <= 14) ?
+ HexDriveAcrossFlats(HexDriveAcrossFlats(diameter)) :
+ round(0.6*diameter);
+
+// Provides metric countersunk hex (Allen) drive widths across the corners.
+function CountersunkDriveAcrossCorners(diameter) =
+ CountersunkDriveAcrossFlats(diameter) / cos(30);
+
+// Provides standard metric nut thickness.
+function NutThickness(diameter) =
+ (diameter <= 64) ?
+ lookup(diameter, [
+ [2, 1.6],
+ [2.5, 2],
+ [3, 2.4],
+ [3.5, 2.8],
+ [4, 3.2],
+ [5, 4.7],
+ [6, 5.2],
+ [7, 6.0],
+ [8, 6.8],
+ [10, 8.4],
+ [12, 10.8],
+ [14, 12.8],
+ [16, 14.8],
+ [18, 15.8],
+ [20, 18.0],
+ [22, 21.1],
+ [24, 21.5],
+ [27, 23.8],
+ [30, 25.6],
+ [33, 28.7],
+ [36, 31.0],
+ [42, 34],
+ [48, 38],
+ [56, 45],
+ [64, 51]
+ ]) :
+ diameter * 51 / 64;
+
+
+// This generates a closed polyhedron from an array of arrays of points,
+// with each inner array tracing out one loop outlining the polyhedron.
+// pointarrays should contain an array of N arrays each of size P outlining a
+// closed manifold. The points must obey the right-hand rule. For example,
+// looking down, the P points in the inner arrays are counter-clockwise in a
+// loop, while the N point arrays increase in height. Points in each inner
+// array do not need to be equal height, but they usually should not meet or
+// cross the line segments from the adjacent points in the other arrays.
+// (N>=2, P>=3)
+// Core triangles:
+// [j][i], [j+1][i], [j+1][(i+1)%P]
+// [j][i], [j+1][(i+1)%P], [j][(i+1)%P]
+// Then triangles are formed in a loop with the middle point of the first
+// and last array.
+module ClosePoints(pointarrays) {
+ function recurse_avg(arr, n=0, p=[0,0,0]) = (n>=len(arr)) ? p :
+ recurse_avg(arr, n+1, p+(arr[n]-p)/(n+1));
+
+ N = len(pointarrays);
+ P = len(pointarrays[0]);
+ NP = N*P;
+ lastarr = pointarrays[N-1];
+ midbot = recurse_avg(pointarrays[0]);
+ midtop = recurse_avg(pointarrays[N-1]);
+
+ faces_bot = [
+ for (i=[0:P-1])
+ [0,i+1,1+(i+1)%len(pointarrays[0])]
+ ];
+
+ loop_offset = 1;
+ bot_len = loop_offset + P;
+
+ faces_loop = [
+ for (j=[0:N-2], i=[0:P-1], t=[0:1])
+ [loop_offset, loop_offset, loop_offset] + (t==0 ?
+ [j*P+i, (j+1)*P+i, (j+1)*P+(i+1)%P] :
+ [j*P+i, (j+1)*P+(i+1)%P, j*P+(i+1)%P])
+ ];
+
+ top_offset = loop_offset + NP - P;
+ midtop_offset = top_offset + P;
+
+ faces_top = [
+ for (i=[0:P-1])
+ [midtop_offset,top_offset+(i+1)%P,top_offset+i]
+ ];
+
+ points = [
+ for (i=[-1:NP])
+ (i<0) ? midbot :
+ ((i==NP) ? midtop :
+ pointarrays[floor(i/P)][i%P])
+ ];
+ faces = concat(faces_bot, faces_loop, faces_top);
+
+ polyhedron(points=points, faces=faces);
+}
+
+
+
+// This creates a vertical rod at the origin with external threads. It uses
+// metric standards by default.
+module ScrewThread(outer_diam, height, pitch=0, tooth_angle=30, tolerance=0.4, tip_height=0, tooth_height=0, tip_min_fract=0) {
+
+ pitch = (pitch==0) ? ThreadPitch(outer_diam) : pitch;
+ tooth_height = (tooth_height==0) ? pitch : tooth_height;
+ tip_min_fract = (tip_min_fract<0) ? 0 :
+ ((tip_min_fract>0.9999) ? 0.9999 : tip_min_fract);
+
+ outer_diam_cor = outer_diam + 0.25*tolerance; // Plastic shrinkage correction
+ inner_diam = outer_diam - tooth_height/tan(tooth_angle);
+ or = (outer_diam_cor < screw_resolution) ?
+ screw_resolution/2 : outer_diam_cor / 2;
+ ir = (inner_diam < screw_resolution) ? screw_resolution/2 : inner_diam / 2;
+ height = (height < screw_resolution) ? screw_resolution : height;
+
+ steps_per_loop_try = ceil(2*3.14159265359*or / screw_resolution);
+ steps_per_loop = (steps_per_loop_try < 4) ? 4 : steps_per_loop_try;
+ hs_ext = 3;
+ hsteps = ceil(3 * height / pitch) + 2*hs_ext;
+
+ extent = or - ir;
+
+ tip_start = height-tip_height;
+ tip_height_sc = tip_height / (1-tip_min_fract);
+
+ tip_height_ir = (tip_height_sc > tooth_height/2) ?
+ tip_height_sc - tooth_height/2 : tip_height_sc;
+
+ tip_height_w = (tip_height_sc > tooth_height) ? tooth_height : tip_height_sc;
+ tip_wstart = height + tip_height_sc - tip_height - tip_height_w;
+
+
+ function tooth_width(a, h, pitch, tooth_height, extent) =
+ let(
+ ang_full = h*360.0/pitch-a,
+ ang_pn = atan2(sin(ang_full), cos(ang_full)),
+ ang = ang_pn < 0 ? ang_pn+360 : ang_pn,
+ frac = ang/360,
+ tfrac_half = tooth_height / (2*pitch),
+ tfrac_cut = 2*tfrac_half
+ )
+ (frac > tfrac_cut) ? 0 : (
+ (frac <= tfrac_half) ?
+ ((frac / tfrac_half) * extent) :
+ ((1 - (frac - tfrac_half)/tfrac_half) * extent)
+ );
+
+
+ pointarrays = [
+ for (hs=[0:hsteps])
+ [
+ for (s=[0:steps_per_loop-1])
+ let(
+ ang_full = s*360.0/steps_per_loop,
+ ang_pn = atan2(sin(ang_full), cos(ang_full)),
+ ang = ang_pn < 0 ? ang_pn+360 : ang_pn,
+
+ h_fudge = pitch*0.001,
+
+ h_mod =
+ (hs%3 == 2) ?
+ ((s == steps_per_loop-1) ? tooth_height - h_fudge : (
+ (s == steps_per_loop-2) ? tooth_height/2 : 0)) : (
+ (hs%3 == 0) ?
+ ((s == steps_per_loop-1) ? pitch-tooth_height/2 : (
+ (s == steps_per_loop-2) ? pitch-tooth_height + h_fudge : 0)) :
+ ((s == steps_per_loop-1) ? pitch-tooth_height/2 + h_fudge : (
+ (s == steps_per_loop-2) ? tooth_height/2 : 0))
+ ),
+
+ h_level =
+ (hs%3 == 2) ? tooth_height - h_fudge : (
+ (hs%3 == 0) ? 0 : tooth_height/2),
+
+ h_ub = floor((hs-hs_ext)/3) * pitch
+ + h_level + ang*pitch/360.0 - h_mod,
+ h_max = height - (hsteps-hs) * h_fudge,
+ h_min = hs * h_fudge,
+ h = (h_ub < h_min) ? h_min : ((h_ub > h_max) ? h_max : h_ub),
+
+ ht = h - tip_start,
+ hf_ir = ht/tip_height_ir,
+ ht_w = h - tip_wstart,
+ hf_w_t = ht_w/tip_height_w,
+ hf_w = (hf_w_t < 0) ? 0 : ((hf_w_t > 1) ? 1 : hf_w_t),
+
+ ext_tip = (h <= tip_wstart) ? extent : (1-hf_w) * extent,
+ wnormal = tooth_width(ang, h, pitch, tooth_height, ext_tip),
+ w = (h <= tip_wstart) ? wnormal :
+ (1-hf_w) * wnormal +
+ hf_w * (0.1*screw_resolution + (wnormal * wnormal * wnormal /
+ (ext_tip*ext_tip+0.1*screw_resolution))),
+ r = (ht <= 0) ? ir + w :
+ ( (ht < tip_height_ir ? ((2/(1+(hf_ir*hf_ir))-1) * ir) : 0) + w)
+ )
+ [r*cos(ang), r*sin(ang), h]
+ ]
+ ];
+
+
+ ClosePoints(pointarrays);
+}
+
+
+// This creates a vertical rod at the origin with external auger-style
+// threads.
+module AugerThread(outer_diam, inner_diam, height, pitch, tooth_angle=30, tolerance=0.4, tip_height=0, tip_min_fract=0) {
+ tooth_height = tan(tooth_angle)*(outer_diam-inner_diam);
+ ScrewThread(outer_diam, height, pitch, tooth_angle, tolerance, tip_height,
+ tooth_height, tip_min_fract);
+}
+
+
+// This creates a threaded hole in its children using metric standards by
+// default.
+module ScrewHole(outer_diam, height, position=[0,0,0], rotation=[0,0,0], pitch=0, tooth_angle=30, tolerance=0.4, tooth_height=0) {
+ extra_height = 0.001 * height;
+
+ difference() {
+ children();
+ translate(position)
+ rotate(rotation)
+ translate([0, 0, -extra_height/2])
+ ScrewThread(1.01*outer_diam + 1.25*tolerance, height + extra_height,
+ pitch, tooth_angle, tolerance, tooth_height=tooth_height);
+ }
+}
+
+
+// This creates an auger-style threaded hole in its children.
+module AugerHole(outer_diam, inner_diam, height, pitch, position=[0,0,0], rotation=[0,0,0], tooth_angle=30, tolerance=0.4) {
+ tooth_height = tan(tooth_angle)*(outer_diam-inner_diam);
+ ScrewHole(outer_diam, height, position, rotation, pitch, tooth_angle,
+ tolerance, tooth_height=tooth_height) children();
+}
+
+
+// This inserts a ClearanceHole in its children.
+// The rotation vector is applied first, then the position translation,
+// starting from a position upward from the z-axis at z=0.
+module ClearanceHole(diameter, height, position=[0,0,0], rotation=[0,0,0], tolerance=0.4) {
+ extra_height = 0.001 * height;
+
+ difference() {
+ children();
+ translate(position)
+ rotate(rotation)
+ translate([0, 0, -extra_height/2])
+ cylinder(h=height + extra_height, r=(diameter/2+tolerance));
+ }
+}
+
+
+// This inserts a ClearanceHole with a recessed bolt hole in its children.
+// The rotation vector is applied first, then the position translation,
+// starting from a position upward from the z-axis at z=0. The default
+// recessed parameters fit a standard metric bolt.
+module RecessedClearanceHole(diameter, height, position=[0,0,0], rotation=[0,0,0], recessed_diam=-1, recessed_height=-1, tolerance=0.4) {
+ recessed_diam = (recessed_diam < 0) ?
+ HexAcrossCorners(diameter) : recessed_diam;
+ recessed_height = (recessed_height < 0) ? diameter : recessed_height;
+ extra_height = 0.001 * height;
+
+ difference() {
+ children();
+ translate(position)
+ rotate(rotation)
+ translate([0, 0, -extra_height/2])
+ cylinder(h=height + extra_height, r=(diameter/2+tolerance));
+ translate(position)
+ rotate(rotation)
+ translate([0, 0, -extra_height/2])
+ cylinder(h=recessed_height + extra_height/2,
+ r=(recessed_diam/2+tolerance));
+ }
+}
+
+
+// This inserts a countersunk ClearanceHole in its children.
+// The rotation vector is applied first, then the position translation,
+// starting from a position upward from the z-axis at z=0.
+// The countersunk side is on the bottom by default.
+module CountersunkClearanceHole(diameter, height, position=[0,0,0], rotation=[0,0,0], sinkdiam=0, sinkangle=45, tolerance=0.4) {
+ extra_height = 0.001 * height;
+ sinkdiam = (sinkdiam==0) ? 2*diameter : sinkdiam;
+ sinkheight = ((sinkdiam-diameter)/2)/tan(sinkangle);
+
+ difference() {
+ children();
+ translate(position)
+ rotate(rotation)
+ translate([0, 0, -extra_height/2])
+ union() {
+ cylinder(h=height + extra_height, r=(diameter/2+tolerance));
+ cylinder(h=sinkheight + extra_height, r1=(sinkdiam/2+tolerance), r2=(diameter/2+tolerance), $fn=24*diameter);
+ }
+ }
+}
+
+
+// This inserts a Phillips tip shaped hole into its children.
+// The rotation vector is applied first, then the position translation,
+// starting from a position upward from the z-axis at z=0.
+module PhillipsTip(width=7, thickness=0, straightdepth=0, position=[0,0,0], rotation=[0,0,0]) {
+ thickness = (thickness <= 0) ? width*2.5/7 : thickness;
+ straightdepth = (straightdepth <= 0) ? width*3.5/7 : straightdepth;
+ angledepth = (width-thickness)/2;
+ height = straightdepth + angledepth;
+ extra_height = 0.001 * height;
+
+ difference() {
+ children();
+ translate(position)
+ rotate(rotation)
+ union() {
+ hull() {
+ translate([-width/2, -thickness/2, -extra_height/2])
+ cube([width, thickness, straightdepth+extra_height]);
+ translate([-thickness/2, -thickness/2, height-extra_height])
+ cube([thickness, thickness, extra_height]);
+ }
+ hull() {
+ translate([-thickness/2, -width/2, -extra_height/2])
+ cube([thickness, width, straightdepth+extra_height]);
+ translate([-thickness/2, -thickness/2, height-extra_height])
+ cube([thickness, thickness, extra_height]);
+ }
+ }
+ }
+}
+
+
+
+// Create a standard sized metric bolt with hex head and hex key.
+module MetricBolt(diameter, length, tolerance=0.4) {
+ drive_tolerance = pow(3*tolerance/HexDriveAcrossCorners(diameter),2)
+ + 0.75*tolerance;
+
+ difference() {
+ cylinder(h=diameter, r=(HexAcrossCorners(diameter)/2-0.5*tolerance), $fn=6);
+ cylinder(h=diameter,
+ r=(HexDriveAcrossCorners(diameter)+drive_tolerance)/2, $fn=6,
+ center=true);
+ }
+ translate([0,0,diameter-0.01])
+ ScrewThread(diameter, length+0.01, tolerance=tolerance,
+ tip_height=ThreadPitch(diameter), tip_min_fract=0.75);
+}
+
+
+// Create a standard sized metric countersunk (flat) bolt with hex key drive.
+// In compliance with convention, the length for this includes the head.
+module MetricCountersunkBolt(diameter, length, tolerance=0.4) {
+ drive_tolerance = pow(3*tolerance/CountersunkDriveAcrossCorners(diameter),2)
+ + 0.75*tolerance;
+
+ difference() {
+ cylinder(h=diameter/2, r1=diameter, r2=diameter/2, $fn=24*diameter);
+ cylinder(h=0.8*diameter,
+ r=(CountersunkDriveAcrossCorners(diameter)+drive_tolerance)/2, $fn=6,
+ center=true);
+ }
+ translate([0,0,diameter/2-0.01])
+ ScrewThread(diameter, length-diameter/2+0.01, tolerance=tolerance,
+ tip_height=ThreadPitch(diameter), tip_min_fract=0.75);
+}
+
+
+// Create a standard sized metric countersunk (flat) bolt with hex key drive.
+// In compliance with convention, the length for this includes the head.
+module MetricWoodScrew(diameter, length, tolerance=0.4) {
+ drive_tolerance = pow(3*tolerance/CountersunkDriveAcrossCorners(diameter),2)
+ + 0.75*tolerance;
+
+ PhillipsTip(diameter-2)
+ union() {
+ cylinder(h=diameter/2, r1=diameter, r2=diameter/2, $fn=24*diameter);
+
+ translate([0,0,diameter/2-0.01])
+ ScrewThread(diameter, length-diameter/2+0.01, tolerance=tolerance,
+ tip_height=diameter);
+ }
+}
+
+
+// Create a standard sized metric hex nut.
+module MetricNut(diameter, thickness=0, tolerance=0.4) {
+ thickness = (thickness==0) ? NutThickness(diameter) : thickness;
+ ScrewHole(diameter, thickness, tolerance=tolerance)
+ cylinder(h=thickness, r=HexAcrossCorners(diameter)/2-0.5*tolerance, $fn=6);
+}
+
+
+// Create a convenient washer size for a metric nominal thread diameter.
+module MetricWasher(diameter) {
+ difference() {
+ cylinder(h=diameter/5, r=1.15*diameter, $fn=24*diameter);
+ cylinder(h=2*diameter, r=0.575*diameter, $fn=12*diameter, center=true);
+ }
+}
+
+
+// Solid rod on the bottom, external threads on the top.
+module RodStart(diameter, height, thread_len=0, thread_diam=0, thread_pitch=0) {
+ // A reasonable default.
+ thread_diam = (thread_diam==0) ? 0.75*diameter : thread_diam;
+ thread_len = (thread_len==0) ? 0.5*diameter : thread_len;
+ thread_pitch = (thread_pitch==0) ? ThreadPitch(thread_diam) : thread_pitch;
+
+ cylinder(r=diameter/2, h=height, $fn=24*diameter);
+
+ translate([0, 0, height])
+ ScrewThread(thread_diam, thread_len, thread_pitch,
+ tip_height=thread_pitch, tip_min_fract=0.75);
+}
+
+
+// Solid rod on the bottom, internal threads on the top.
+// Flips around x-axis after printing to pair with RodStart.
+module RodEnd(diameter, height, thread_len=0, thread_diam=0, thread_pitch=0) {
+ // A reasonable default.
+ thread_diam = (thread_diam==0) ? 0.75*diameter : thread_diam;
+ thread_len = (thread_len==0) ? 0.5*diameter : thread_len;
+ thread_pitch = (thread_pitch==0) ? ThreadPitch(thread_diam) : thread_pitch;
+
+ ScrewHole(thread_diam, thread_len, [0, 0, height], [180,0,0], thread_pitch)
+ cylinder(r=diameter/2, h=height, $fn=24*diameter);
+}
+
+
+// Internal threads on the bottom, external threads on the top.
+module RodExtender(diameter, height, thread_len=0, thread_diam=0, thread_pitch=0) {
+ // A reasonable default.
+ thread_diam = (thread_diam==0) ? 0.75*diameter : thread_diam;
+ thread_len = (thread_len==0) ? 0.5*diameter : thread_len;
+ thread_pitch = (thread_pitch==0) ? ThreadPitch(thread_diam) : thread_pitch;
+
+ max_bridge = height - thread_len;
+ // Use 60 degree slope if it will fit.
+ bridge_height = ((thread_diam/4) < max_bridge) ? thread_diam/4 : max_bridge;
+
+ difference() {
+ union() {
+ ScrewHole(thread_diam, thread_len, pitch=thread_pitch)
+ cylinder(r=diameter/2, h=height, $fn=24*diameter);
+
+ translate([0,0,height])
+ ScrewThread(thread_diam, thread_len, pitch=thread_pitch,
+ tip_height=thread_pitch, tip_min_fract=0.75);
+ }
+ // Carve out a small conical area as a bridge.
+ translate([0,0,thread_len])
+ cylinder(h=bridge_height, r1=thread_diam/2, r2=0.1);
+ }
+}
+
+
+// Produces a matching set of metric bolts, nuts, and washers.
+module MetricBoltSet(diameter, length, quantity=1) {
+ for (i=[0:quantity-1]) {
+ translate([0, i*4*diameter, 0]) MetricBolt(diameter, length);
+ translate([4*diameter, i*4*diameter, 0]) MetricNut(diameter);
+ translate([8*diameter, i*4*diameter, 0]) MetricWasher(diameter);
+ }
+}
+
+
+module Demo() {
+ translate([0,-0,0]) MetricBoltSet(3, 8);
+ translate([0,-20,0]) MetricBoltSet(4, 8);
+ translate([0,-40,0]) MetricBoltSet(5, 8);
+ translate([0,-60,0]) MetricBoltSet(6, 8);
+ translate([0,-80,0]) MetricBoltSet(8, 8);
+
+ translate([0,25,0]) MetricCountersunkBolt(5, 10);
+ translate([23,18,5])
+ scale([1,1,-1])
+ CountersunkClearanceHole(5, 8, [7,7,0], [0,0,0])
+ cube([14, 14, 5]);
+
+ translate([70, -10, 0])
+ RodStart(20, 30);
+ translate([70, 20, 0])
+ RodEnd(20, 30);
+
+ translate([70, -45, 0])
+ MetricWoodScrew(8, 20);
+
+ translate([12, 50, 0])
+ union() {
+ translate([0, 0, 5.99])
+ AugerThread(15, 3.5, 22, 7, tooth_angle=15, tip_height=7);
+ translate([-4, -9, 0]) cube([8, 18, 6]);
+ }
+}
+
+
+Demo();
+
+//MetricBoltSet(6, 8, 10);
diff --git a/gridfinity-rebuilt-bins.scad b/gridfinity-rebuilt-bins.scad
index 3b36dba..6a3aa9b 100644
--- a/gridfinity-rebuilt-bins.scad
+++ b/gridfinity-rebuilt-bins.scad
@@ -95,6 +95,8 @@ crush_ribs = true;
chamfer_holes = true;
// Magnet/Screw holes will be printed so supports are not needed.
printable_hole_top = true;
+// Enable "gridfinity-refined" thumbscrew hole in the center of each base: https://www.printables.com/model/413761-gridfinity-refined
+enable_thumbscrew = false;
hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top);
@@ -112,7 +114,7 @@ gridfinityInit(gridx, gridy, height(gridz, gridz_define, style_lip, enable_zsnap
cutCylinders(n_divx=cdivx, n_divy=cdivy, cylinder_diameter=cd, cylinder_height=ch, coutout_depth=c_depth, orientation=c_orientation, chamfer=c_chamfer);
}
}
-gridfinityBase(gridx, gridy, l_grid, div_base_x, div_base_y, hole_options, only_corners=only_corners);
+gridfinityBase(gridx, gridy, l_grid, div_base_x, div_base_y, hole_options, only_corners=only_corners, thumbscrew=enable_thumbscrew);
}
diff --git a/gridfinity-rebuilt-utility.scad b/gridfinity-rebuilt-utility.scad
index 9130e30..ae826be 100644
--- a/gridfinity-rebuilt-utility.scad
+++ b/gridfinity-rebuilt-utility.scad
@@ -7,6 +7,7 @@
include
use
use
+use
// ===== User Modules ===== //
@@ -202,8 +203,9 @@ module cut_move(x, y, w, h) {
/**
*@summary Create the base of a gridfinity bin, or use it for a custom object.
* @param length X,Y size of a single Gridfinity base.
+ * @param thumbscrew Enable "gridfinity-refined" thumbscrew hole in the center of each base unit. This is a ISO Metric Profile, 15.0mm size, M15x1.5 designation.
*/
-module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options(), off=0, final_cut=true, only_corners=false) {
+module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options(), off=0, final_cut=true, only_corners=false, thumbscrew=false) {
assert(
is_num(gx) &&
is_num(gy) &&
@@ -211,7 +213,8 @@ module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options()
is_num(dx) &&
is_num(dy) &&
is_bool(final_cut) &&
- is_bool(only_corners)
+ is_bool(only_corners) &&
+ is_bool(thumbscrew)
);
dbnxt = [for (i=[1:5]) if (abs(gx*i)%1 < 0.001 || abs(gx*i)%1 > 0.999) i];
@@ -242,7 +245,7 @@ module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options()
if(only_corners) {
difference(){
pattern_linear(grid_size.x, grid_size.y, base_center_distance_mm.x, base_center_distance_mm.y)
- block_base(bundle_hole_options(), 0, individual_base_size_mm);
+ block_base(bundle_hole_options(), 0, individual_base_size_mm, thumbscrew=thumbscrew);
copy_mirror([0, 1, 0]) {
copy_mirror([1, 0, 0]) {
@@ -258,7 +261,7 @@ module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options()
}
else {
pattern_linear(grid_size.x, grid_size.y, base_center_distance_mm.x, base_center_distance_mm.y)
- block_base(hole_options, off, individual_base_size_mm);
+ block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew);
}
}
@@ -267,9 +270,14 @@ module gridfinityBase(gx, gy, length, dx, dy, hole_options=bundle_hole_options()
* @param hole_options @see block_base_hole.hole_options
* @param off
* @param size [x, y] size of a single base. Only set if deviating from the standard!
+ * @param thumbscrew Enable "gridfinity-refined" thumbscrew hole in the center of each base unit. This is a ISO Metric Profile, 15.0mm size, M15x1.5 designation.
*/
-module block_base(hole_options, off=0, size=[BASE_SIZE, BASE_SIZE]) {
- assert(is_list(size) && len(size) == 2);
+module block_base(hole_options, off=0, size=[BASE_SIZE, BASE_SIZE], thumbscrew=false) {
+ assert(
+ is_list(size) &&
+ len(size) == 2 &&
+ is_bool(thumbscrew)
+ );
// How far, in the +x direction,
// the profile needs to be from it's [0, 0] point
@@ -284,6 +292,13 @@ module block_base(hole_options, off=0, size=[BASE_SIZE, BASE_SIZE]) {
str("Minimum size of a single base must be greater than ", outer_diameter)
);
+ thumbscrew_outerdiam = 15;
+ thumbscrew_height = 5;
+ thumbscrew_tolerance = 0.4;
+ thumbscrew_tooth_angle = 30;
+ thumbscrew_pitch = 1.5;
+
+
render(convexity = 2)
difference() {
union() {
@@ -302,6 +317,16 @@ module block_base(hole_options, off=0, size=[BASE_SIZE, BASE_SIZE]) {
);
}
+ if (thumbscrew) {
+ ScrewThread(
+ 1.01 * thumbscrew_outerdiam + 1.25 * thumbscrew_tolerance,
+ thumbscrew_height,
+ thumbscrew_pitch,
+ thumbscrew_tooth_angle,
+ thumbscrew_tolerance,
+ tooth_height=0
+ );
+ }
// 4 holes
// Need this fancy code to support refined holes and non-square bases.
for(a=[0:90:270]){