1//////////////////////////////////////////////////////////////////////
   2// LibFile: joiners.scad
   3//   Modules for joining separately printed parts including screw together, snap-together and dovetails.
   4// Includes:
   5//   include <BOSL2/std.scad>
   6//   include <BOSL2/joiners.scad>
   7// FileGroup: Parts
   8// FileSummary: Joiner shapes for connecting separately printed objects.
   9//////////////////////////////////////////////////////////////////////
  10
  11
  12include <rounding.scad>
  13
  14
  15// Section: Half Joiners
  16
  17
  18// Function&Module: half_joiner_clear()
  19// Synopsis: Creates a mask to clear space for a {{half_joiner()}}.
  20// SynTags: Geom, VNF
  21// Topics: Joiners, Parts
  22// See Also: half_joiner_clear(), half_joiner(), half_joiner2(), joiner_clear(), joiner(), snap_pin(), rabbit_clip(), dovetail()
  23// Usage: As Module
  24//   half_joiner_clear(l, w, [ang=], [clearance=], [overlap=]) [ATTACHMENTS];
  25// Usage: As Function
  26//   vnf = half_joiner_clear(l, w, [ang=], [clearance=], [overlap=]);
  27// Description:
  28//   Creates a mask to clear an area so that a half_joiner can be placed there.
  29// Arguments:
  30//   l = Length of the joiner to clear space for.
  31//   w = Width of the joiner to clear space for.
  32//   ang = Overhang angle of the joiner.
  33//   ---
  34//   clearance = Extra width to clear.
  35//   overlap = Extra depth to clear.
  36//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
  37//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
  38//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
  39// Example:
  40//   half_joiner_clear();
  41function half_joiner_clear(l=20, w=10, ang=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP) =
  42    let(
  43        guide = [w/3-get_slop()*2, ang_adj_to_opp(ang, l/3)*2, l/3],
  44        path = [
  45            [ l/2,-overlap], [ guide.z/2, -guide.y/2-overlap],
  46            [-guide.z/2, -guide.y/2-overlap], [-l/2,-overlap],
  47            [-l/2, overlap], [-guide.z/2,  guide.y/2+overlap],
  48            [ guide.z/2,  guide.y/2+overlap], [ l/2, overlap],
  49        ],
  50        dpath = deduplicate(path, closed=true),
  51        vnf = linear_sweep(dpath, height=w+clearance*2, center=true, spin=90, orient=RIGHT)
  52    ) reorient(anchor,spin,orient, vnf=vnf, p=vnf);
  53
  54module half_joiner_clear(l=20, w=10, ang=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP)
  55{
  56    vnf = half_joiner_clear(l=l, w=w, ang=ang, clearance=clearance, overlap=overlap);
  57    attachable(anchor,spin,orient, vnf=vnf) {
  58        vnf_polyhedron(vnf, convexity=2);
  59        children();
  60    }
  61}
  62
  63
  64// Function&Module: half_joiner()
  65// Synopsis: Creates a half-joiner shape to mate with a {{half_joiner2()}} shape..
  66// SynTags: Geom, VNF
  67// Topics: Joiners, Parts
  68// See Also: half_joiner_clear(), half_joiner(), half_joiner2(), joiner_clear(), joiner(), snap_pin(), rabbit_clip(), dovetail()
  69// Usage: As Module
  70//   half_joiner(l, w, [base=], [ang=], [screwsize=], [$slop=]) [ATTACHMENTS];
  71// Usage: As Function
  72//   vnf = half_joiner(l, w, [base=], [ang=], [screwsize=], [$slop=]);
  73// Description:
  74//   Creates a half_joiner object that can be attached to a matching half_joiner2 object.
  75// Arguments:
  76//   l = Length of the half_joiner.
  77//   w = Width of the half_joiner.
  78//   ---
  79//   base = Length of the backing to the half_joiner.
  80//   ang = Overhang angle of the half_joiner.
  81//   screwsize = If given, diameter of screwhole.
  82//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
  83//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
  84//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
  85//   $slop = Printer specific slop value to make parts fit more closely.
  86// Examples(FlatSpin,VPD=75):
  87//   half_joiner(screwsize=3);
  88//   half_joiner(l=20,w=10,base=10);
  89// Example(3D):
  90//   diff()
  91//   cuboid(40)
  92//       attach([FWD,TOP,RIGHT])
  93//           xcopies(20) half_joiner();
  94function half_joiner(l=20, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP) =
  95    let(
  96        guide = [w/3-get_slop()*2, ang_adj_to_opp(ang, l/3)*2, l/3],
  97        snap_h = 1,
  98        snap = [guide.x+snap_h, 2*snap_h, l*0.6],
  99        slope = guide.z/2/(w/8),
 100        snap_top = slope * (snap.x-guide.x)/2,
 101
 102        verts = [
 103            [-w/2,-base,-l/2], [-w/2,-base,l/2], [w/2,-base,l/2], [w/2,-base,-l/2],
 104
 105            [-w/2, 0,-l/2],
 106            [-w/2,-guide.y/2,-guide.z/2],
 107            [-w/2,-guide.y/2, guide.z/2],
 108            [-w/2, 0,l/2],
 109            [ w/2, 0,l/2],
 110            [ w/2,-guide.y/2, guide.z/2],
 111            [ w/2,-guide.y/2,-guide.z/2],
 112            [ w/2, 0,-l/2],
 113
 114            [-guide.x/2, 0,-l/2],
 115            [-guide.x/2,-guide.y/2,-guide.z/2],
 116            [-guide.x/2-w/8,-guide.y/2, 0],
 117            [-guide.x/2,-guide.y/2, guide.z/2],
 118            [-guide.x/2, 0,l/2],
 119            [ guide.x/2, 0,l/2],
 120            [ guide.x/2,-guide.y/2, guide.z/2],
 121            [ guide.x/2+w/8,-guide.y/2, 0],
 122            [ guide.x/2,-guide.y/2,-guide.z/2],
 123            [ guide.x/2, 0,-l/2],
 124
 125            [-w/6, -snap.y/2, -snap.z/2],
 126            [-w/6, -snap.y/2, -guide.z/2],
 127            [-snap.x/2, 0, min(snap_top-guide.z/2,-default(screwsize,0)*1.1/2)],
 128            [-w/6,  snap.y/2, -guide.z/2],
 129            [-w/6,  snap.y/2, -snap.z/2],
 130            [-snap.x/2, 0, snap_top-snap.z/2],
 131
 132            [-w/6, -snap.y/2, snap.z/2],
 133            [-w/6, -snap.y/2, guide.z/2],
 134            [-snap.x/2, 0, max(guide.z/2-snap_top, default(screwsize,0)*1.1/2)],
 135            [-w/6,  snap.y/2, guide.z/2],
 136            [-w/6,  snap.y/2, snap.z/2],
 137            [-snap.x/2, 0, snap.z/2-snap_top],
 138
 139            [ w/6, -snap.y/2, snap.z/2],
 140            [ w/6, -snap.y/2, guide.z/2],
 141            [ snap.x/2, 0, max(guide.z/2-snap_top, default(screwsize,0)*1.1/2)],
 142            [ w/6,  snap.y/2, guide.z/2],
 143            [ w/6,  snap.y/2, snap.z/2],
 144            [ snap.x/2, 0, snap.z/2-snap_top],
 145
 146            [ w/6, -snap.y/2, -snap.z/2],
 147            [ w/6, -snap.y/2, -guide.z/2],
 148            [ snap.x/2, 0, min(snap_top-guide.z/2,-default(screwsize,0)*1.1/2)],
 149            [ w/6,  snap.y/2, -guide.z/2],
 150            [ w/6,  snap.y/2, -snap.z/2],
 151            [ snap.x/2, 0, snap_top-snap.z/2],
 152
 153            [-w/6, guide.y/2, -guide.z/2],
 154            [-guide.x/2-w/8, guide.y/2, 0],
 155            [-w/6, guide.y/2,  guide.z/2],
 156            [ w/6, guide.y/2,  guide.z/2],
 157            [ guide.x/2+w/8, guide.y/2, 0],
 158            [ w/6, guide.y/2, -guide.z/2],
 159
 160            if (screwsize != undef) each [
 161                for (a = [0:45:359]) [guide.x/2+w/8, 0, 0] + screwsize * 1.1 / 2 * [-abs(sin(a))/slope, cos(a), sin(a)],
 162                for (a = [0:45:359]) [-(guide.x/2+w/8), 0, 0] + screwsize * 1.1 / 2 * [abs(sin(a))/slope, cos(a), sin(a)],
 163            ]
 164        ],
 165        faces = [
 166            [0,1,2], [2,3,0],
 167
 168            [0,4,5], [0,5,6], [0,6,1], [1,6,7],
 169            [3,10,11], [3,9,10], [2,9,3], [2,8,9],
 170
 171            [1,7,16], [1,16,17], [1,17,8], [1,8,2],
 172            [0,3,11], [0,11,21], [0,21,12], [0,12,4],
 173
 174            [10,20,11], [20,21,11],
 175            [12,13,5], [12,5,4],
 176            [9,8,18], [17,18,8],
 177            [6,16,7], [6,15,16],
 178
 179            [19,10,9], [19,9,18], [19,20,10],
 180            [6,14,15], [6,5,14], [5,13,14],
 181
 182            [24,26,25], [26,24,27],
 183            [22,27,24], [22,24,23],
 184            [22,26,27],
 185
 186            [30,32,33], [30,31,32],
 187            [30,33,28], [30,28,29],
 188            [32,28,33],
 189
 190            [40,41,42], [40,42,45],
 191            [45,42,43], [43,44,45],
 192            [40,45,44],
 193
 194            [36,38,37], [36,39,38],
 195            [36,35,34], [36,34,39],
 196            [39,34,38],
 197
 198            [12,26,22], [12,22,13], [22,23,13], [12,46,26], [46,25,26],
 199            [16,28,32], [16,15,28], [15,29,28], [48,16,32], [32,31,48],
 200            [17,38,34], [17,34,18], [18,34,35], [49,38,17], [37,38,49],
 201            [21,40,44], [51,21,44], [43,51,44], [20,40,21], [20,41,40],
 202
 203            [17,16,49], [49,16,48],
 204            [21,51,46], [46,12,21],
 205
 206            [51,50,49], [48,47,46], [46,51,49], [46,49,48],
 207
 208            if (screwsize == undef) each [
 209                [19,36,50], [19,35,36], [19,18,35], [36,37,50], [49,50,37],
 210                [19,50,42], [19,42,41], [41,20,19], [50,43,42], [50,51,43],
 211                [14,24,47], [14,23,24], [14,13,23], [47,24,25], [46,47,25],
 212                [47,30,14], [14,30,29], [14,29,15], [47,31,30], [47,48,31],
 213            ] else each [
 214                [20,19,56], [20,56,57], [20,57,58], [41,58,42], [20,58,41],
 215                [50,51,52], [51,59,52], [51,58,59], [43,42,58], [51,43,58],
 216                [49,50,52], [49,52,53], [49,53,54], [37,54,36], [49,54,37],
 217                [56,19,18], [18,55,56], [18,54,55], [35,36,54], [18,35,54],
 218                [14,64,15], [15,64,63], [15,63,62], [29,62,30], [15,62,29],
 219                [48,31,62], [31,30,62], [48,62,61], [48,61,60], [60,47,48],
 220                [13,23,66], [23,24,66], [13,66,65], [13,65,64], [64,14,13],
 221                [46,47,60], [46,60,67], [46,67,66], [46,66,25], [66,24,25],
 222                for (i=[0:7]) let(b=52) [b+i, b+8+i, b+8+(i+1)%8],
 223                for (i=[0:7]) let(b=52) [b+i, b+8+(i+1)%8, b+(i+1)%8],
 224            ],
 225        ],
 226        pvnf = [verts, faces],
 227        vnf = xrot(90, p=pvnf)
 228    ) reorient(anchor,spin,orient, size=[w,l,base*2], p=vnf);
 229
 230module half_joiner(l=20, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP)
 231{
 232    vnf = half_joiner(l=l, w=w, base=base, ang=ang, screwsize=screwsize);
 233    if (is_list($tags_shown) && in_list("remove",$tags_shown)) {
 234        attachable(anchor,spin,orient, size=[w,l,base*2], $tag="remove") {
 235            half_joiner_clear(l=l, w=w, ang=ang, clearance=1);
 236            union();
 237        }
 238    } else {
 239        attachable(anchor,spin,orient, size=[w,base*2,l], $tag="keep") {
 240            vnf_polyhedron(vnf, convexity=12);
 241            children();
 242        }
 243    }
 244}
 245
 246
 247// Function&Module: half_joiner2()
 248// Synopsis: Creates a half_joiner2 shape to mate with a {{half_joiner()}} shape..
 249// SynTags: Geom, VNF
 250// Topics: Joiners, Parts
 251// See Also: half_joiner_clear(), half_joiner(), half_joiner2(), joiner_clear(), joiner(), snap_pin(), rabbit_clip(), dovetail()
 252// Usage: As Module
 253//   half_joiner2(l, w, [base=], [ang=], [screwsize=])
 254// Usage: As Function
 255//   vnf = half_joiner2(l, w, [base=], [ang=], [screwsize=])
 256// Description:
 257//   Creates a half_joiner2 object that can be attached to half_joiner object.
 258// Arguments:
 259//   l = Length of the half_joiner.
 260//   w = Width of the half_joiner.
 261//   ---
 262//   base = Length of the backing to the half_joiner.
 263//   ang = Overhang angle of the half_joiner.
 264//   screwsize = Diameter of screwhole.
 265//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 266//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 267//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 268// Examples(FlatSpin,VPD=75):
 269//   half_joiner2(screwsize=3);
 270//   half_joiner2(w=10,base=10,l=20);
 271// Example(3D):
 272//   diff()
 273//   cuboid(40)
 274//       attach([FWD,TOP,RIGHT])
 275//           xcopies(20) half_joiner2();
 276function half_joiner2(l=20, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP) =
 277    let(
 278        guide = [w/3, ang_adj_to_opp(ang, l/3)*2, l/3],
 279        snap_h = 1,
 280        snap = [guide.x+snap_h, 2*snap_h, l*0.6],
 281        slope = guide.z/2/(w/8),
 282        snap_top = slope * (snap.x-guide.x)/2,
 283        s1 = min(snap_top-guide.z/2,-default(screwsize,0)*1.1/2),
 284        s2 = max(guide.z/2-snap_top, default(screwsize,0)*1.1/2),
 285
 286        verts = [
 287            [-w/2,-base,-l/2], [-w/2,-base,l/2], [w/2,-base,l/2], [w/2,-base,-l/2],
 288
 289            [-w/2, 0,-l/2],
 290            [-w/2, guide.y/2,-guide.z/2],
 291            [-w/2, guide.y/2, guide.z/2],
 292            [-w/2, 0,l/2],
 293            [ w/2, 0,l/2],
 294            [ w/2, guide.y/2, guide.z/2],
 295            [ w/2, guide.y/2,-guide.z/2],
 296            [ w/2, 0,-l/2],
 297
 298            [-guide.x/2, 0,-l/2],
 299            [-guide.x/2,-guide.y/2,-guide.z/2],
 300            [-guide.x/2-w/8,-guide.y/2, 0],
 301            [-guide.x/2,-guide.y/2, guide.z/2],
 302            [-guide.x/2, 0,l/2],
 303            [ guide.x/2, 0,l/2],
 304            [ guide.x/2,-guide.y/2, guide.z/2],
 305            [ guide.x/2+w/8,-guide.y/2, 0],
 306            [ guide.x/2,-guide.y/2,-guide.z/2],
 307            [ guide.x/2, 0,-l/2],
 308
 309            [-w/6, -snap.y/2, -snap.z/2],
 310            [-w/6, -snap.y/2, -guide.z/2],
 311            [-snap.x/2, 0, s1],
 312            [-w/6,  snap.y/2, -guide.z/2],
 313            [-w/6,  snap.y/2, -snap.z/2],
 314            [-snap.x/2, 0, snap_top-snap.z/2],
 315
 316            [-w/6, -snap.y/2, snap.z/2],
 317            [-w/6, -snap.y/2, guide.z/2],
 318            [-snap.x/2, 0, s2],
 319            [-w/6,  snap.y/2, guide.z/2],
 320            [-w/6,  snap.y/2, snap.z/2],
 321            [-snap.x/2, 0, snap.z/2-snap_top],
 322
 323            [ w/6, -snap.y/2, snap.z/2],
 324            [ w/6, -snap.y/2, guide.z/2],
 325            [ snap.x/2, 0, s2],
 326            [ w/6,  snap.y/2, guide.z/2],
 327            [ w/6,  snap.y/2, snap.z/2],
 328            [ snap.x/2, 0, snap.z/2-snap_top],
 329
 330            [ w/6, -snap.y/2, -snap.z/2],
 331            [ w/6, -snap.y/2, -guide.z/2],
 332            [ snap.x/2, 0, s1],
 333            [ w/6,  snap.y/2, -guide.z/2],
 334            [ w/6,  snap.y/2, -snap.z/2],
 335            [ snap.x/2, 0, snap_top-snap.z/2],
 336
 337            [-w/6, guide.y/2, -guide.z/2],
 338            [-guide.x/2-w/8, guide.y/2, 0],
 339            [-w/6, guide.y/2,  guide.z/2],
 340            [ w/6, guide.y/2,  guide.z/2],
 341            [ guide.x/2+w/8, guide.y/2, 0],
 342            [ w/6, guide.y/2, -guide.z/2],
 343
 344            if (screwsize != undef) each [
 345                for (a = [0:45:359]) [guide.x/2+w/8, 0, 0] + screwsize * 1.1 / 2 * [-abs(sin(a))/slope, cos(a), sin(a)],
 346                for (a = [0:45:359]) [-(guide.x/2+w/8), 0, 0] + screwsize * 1.1 / 2 * [abs(sin(a))/slope, cos(a), sin(a)],
 347                for (a = [0:45:359]) [w/2, 0, 0] + screwsize * 1.1 / 2 * [0, cos(a), sin(a)],
 348                for (a = [0:45:359]) [-w/2, 0, 0] + screwsize * 1.1 / 2 * [0, cos(a), sin(a)],
 349            ]
 350        ],
 351        faces = [
 352            [0,1,2], [2,3,0],
 353
 354            [1,7,16], [1,16,17], [1,17,8], [1,8,2],
 355            [0,3,11], [0,11,21], [0,21,12], [0,12,4],
 356
 357            [10,51,11], [51,21,11],
 358            [12,46,5], [12,5,4],
 359            [9,8,49], [17,49,8],
 360            [6,16,7], [6,48,16],
 361
 362            [50,10,9], [50,9,49], [50,51,10],
 363            [6,47,48], [6,5,47], [5,46,47],
 364
 365            [24,25,26], [26,27,24],
 366            [22,24,27], [22,23,24],
 367            [22,27,26],
 368
 369            [30,33,32], [30,32,31],
 370            [30,28,33], [30,29,28],
 371            [32,33,28],
 372
 373            [40,42,41], [40,45,42],
 374            [45,43,42], [43,45,44],
 375            [40,44,45],
 376
 377            [36,37,38], [36,38,39],
 378            [36,34,35], [36,39,34],
 379            [39,38,34],
 380
 381            [12,22,26], [12,13,22], [22,13,23], [12,26,46], [46,26,25],
 382            [16,32,28], [16,28,15], [15,28,29], [48,32,16], [32,48,31],
 383            [17,34,38], [17,18,34], [18,35,34], [49,17,38], [37,49,38],
 384            [21,44,40], [51,44,21], [43,44,51], [20,21,40], [20,40,41],
 385
 386            [17,16,18], [18,16,15],
 387            [21,20,13], [13,12,21],
 388
 389            [20,19,18], [15,14,13], [13,20,18], [13,18,15],
 390
 391            if (screwsize == undef) each [
 392                [0,4,5], [0,5,6], [0,6,1], [1,6,7],
 393                [3,10,11], [3,9,10], [2,9,3], [2,8,9],
 394
 395                [19,50,36], [19,36,35], [19,35,18], [36,50,37], [49,37,50],
 396                [19,42,50], [19,41,42], [41,19,20], [50,42,43], [50,43,51],
 397                [14,47,24], [14,24,23], [14,23,13], [47,25,24], [46,25,47],
 398                [47,14,30], [14,29,30], [14,15,29], [47,30,31], [47,31,48],
 399            ] else each [
 400                [3,2,72], [2,71,72], [2,70,71], [2,8,70],
 401                [8,9,70], [9,69,70], [9,68,69], [9,10,68],
 402                [10,75,68], [10,74,75], [10,11,74],
 403                [3,72,73], [3,73,74], [3,74,11],
 404
 405                [1,0,80], [0,81,80], [0,82,81], [0,4,82],
 406                [4,5,82], [5,83,82], [5,76,83], [5,6,76],
 407                [6,77,76], [6,78,77], [6,7,78],
 408                [7,1,78], [1,79,78], [1,80,79],
 409
 410                [20,56,19], [20,57,56], [20,41,57], [41,58,57], [41,42,58],
 411                [50,52,51], [51,52,59], [43,59,58], [43,58,42], [51,59,43],
 412                [49,52,50], [49,53,52], [49,37,53], [37,36,54], [54,53,37],
 413                [56,18,19], [18,56,55], [18,55,35], [35,55,54], [36,35,54],
 414                [14,15,64], [15,63,64], [15,29,63], [29,62,63], [29,30,62],
 415                [31,48,61], [31,61,62], [30,31,62], [48,60,61], [60,48,47],
 416                [23,13,65], [65,66,23], [24,23,66], [13,64,65], [64,13,14],
 417                [46,60,47], [46,67,60], [46,25,67], [66,67,25], [25,24,66],
 418
 419                for (i=[0:7]) let(b=52) each [
 420                    [b+i, b+16+(i+1)%8, b+16+i],
 421                    [b+i, b+(i+1)%8, b+16+(i+1)%8],
 422                ],
 423                for (i=[0:7]) let(b=60) each [
 424                    [b+i, b+16+i, b+16+(i+1)%8],
 425                    [b+i, b+16+(i+1)%8, b+(i+1)%8],
 426                ],
 427            ],
 428        ],
 429        verts2 = [
 430            for (i = idx(verts))
 431            !approx(s2, verts[54].z)? verts[i] :
 432            i==54? [ snap.x/2-0.01, verts[i].y, verts[i].z] :
 433            i==58? [ snap.x/2-0.01, verts[i].y, verts[i].z] :
 434            i==62? [-snap.x/2+0.01, verts[i].y, verts[i].z] :
 435            i==66? [-snap.x/2+0.01, verts[i].y, verts[i].z] :
 436            verts[i]
 437        ],
 438        pvnf = [verts2, faces],
 439        vnf = xrot(90, p=pvnf)
 440    ) reorient(anchor,spin,orient, size=[w,l,base*2], p=vnf);
 441
 442module half_joiner2(l=20, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP)
 443{
 444    vnf = half_joiner2(l=l, w=w, base=base, ang=ang, screwsize=screwsize);
 445    if (is_list($tags_shown) && in_list("remove",$tags_shown)) {
 446        attachable(anchor,spin,orient, size=[w,l,base*2], $tag="remove") {
 447            half_joiner_clear(l=l, w=w, ang=ang, clearance=1);
 448            union();
 449        }
 450    } else {
 451        attachable(anchor,spin,orient, size=[w,base*2,l], $tag="keep") {
 452            vnf_polyhedron(vnf, convexity=12);
 453            children();
 454        }
 455    }
 456}
 457
 458
 459
 460// Section: Full Joiners
 461
 462
 463// Module: joiner_clear()
 464// Synopsis: Creates a mask to clear space for a {{joiner()}} shape.
 465// SynTags: Geom
 466// Topics: Joiners, Parts
 467// See Also: half_joiner_clear(), half_joiner(), half_joiner2(), joiner_clear(), joiner(), snap_pin(), rabbit_clip(), dovetail()
 468// Description:
 469//   Creates a mask to clear an area so that a joiner can be placed there.
 470// Usage:
 471//   joiner_clear(l, w, [ang=], [clearance=], [overlap=]) [ATTACHMENTS];
 472// Arguments:
 473//   l = Length of the joiner to clear space for.
 474//   w = Width of the joiner to clear space for.
 475//   ang = Overhang angle of the joiner.
 476//   ---
 477//   clearance = Extra width to clear.
 478//   overlap = Extra depth to clear.
 479//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 480//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 481//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 482// Example:
 483//   joiner_clear();
 484function joiner_clear(l=40, w=10, ang=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP) = no_function("joiner_clear");
 485module joiner_clear(l=40, w=10, ang=30, clearance=0, overlap=0.01, anchor=CENTER, spin=0, orient=UP)
 486{
 487    dmnd_height = l*0.5;
 488    dmnd_width = dmnd_height*tan(ang);
 489    guide_size = w/3;
 490    guide_width = 2*(dmnd_height/2-guide_size)*tan(ang);
 491
 492    attachable(anchor,spin,orient, size=[w, guide_width, l]) {
 493        union() {
 494            back(l/4) half_joiner_clear(l=l/2+0.01, w=w, ang=ang, overlap=overlap, clearance=clearance);
 495            fwd(l/4) half_joiner_clear(l=l/2+0.01, w=w, ang=ang, overlap=overlap, clearance=-0.01);
 496        }
 497        children();
 498    }
 499}
 500
 501
 502
 503// Module: joiner()
 504// Synopsis: Creates a joiner shape that can mate with another rotated joiner shape.
 505// SynTags: Geom
 506// Topics: Joiners, Parts
 507// See Also: half_joiner_clear(), half_joiner(), half_joiner2(), joiner_clear(), joiner(), snap_pin(), rabbit_clip(), dovetail()
 508// Usage:
 509//   joiner(l, w, base, [ang=], [screwsize=], [$slop=]) [ATTACHMENTS];
 510// Description:
 511//   Creates a joiner object that can be attached to another joiner object.
 512// Arguments:
 513//   l = Length of the joiner.
 514//   w = Width of the joiner.
 515//   base = Length of the backing to the joiner.
 516//   ang = Overhang angle of the joiner.
 517//   ---
 518//   screwsize = If given, diameter of screwhole.
 519//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 520//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 521//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 522//   $slop = Printer specific slop value to make parts fit more closely.
 523// Examples(FlatSpin,VPD=125):
 524//   joiner(screwsize=3);
 525//   joiner(l=40, w=10, base=10);
 526// Example(3D):
 527//   diff()
 528//   cuboid(50)
 529//     attach([FWD,TOP,RIGHT])
 530//       zrot_copies(n=2,r=15)
 531//         joiner();
 532function joiner(l=40, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP) = no_function("joiner");
 533module joiner(l=40, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, orient=UP)
 534{
 535    if (is_list($tags_shown) && in_list("remove",$tags_shown)) {
 536        attachable(anchor,spin,orient, size=[w,l,base*2], $tag="remove") {
 537            joiner_clear(w=w, l=l, ang=ang, clearance=1);
 538            union();
 539        }
 540    } else {
 541        attachable(anchor,spin,orient, size=[w,l,base*2], $tag="keep") {
 542            union() {
 543                back(l/4) half_joiner(l=l/2, w=w, base=base, ang=ang, screwsize=screwsize);
 544                fwd(l/4) half_joiner2(l=l/2, w=w, base=base, ang=ang, screwsize=screwsize);
 545            }
 546            children();
 547        }
 548    }
 549}
 550
 551
 552
 553// Section: Dovetails
 554
 555// Module: dovetail()
 556// Synopsis: Creates a possibly tapered dovetail shape.
 557// SynTags: Geom
 558// Topics: Joiners, Parts
 559// See Also: joiner(), snap_pin(), rabbit_clip()
 560//
 561// Usage:
 562//   dovetail(gender, w=|width, h=|height, slide|thickness=, [slope=|angle=], [taper=|back_width=], [chamfer=], [r=|radius=], [round=], [extra=], [$slop=])
 563//
 564// Description:
 565//   Produces a possibly tapered dovetail joint shape to attach to or subtract from two parts you wish to join together.
 566//   The tapered dovetail is particularly advantageous for long joints because the joint assembles without binding until
 567//   it is fully closed, and then wedges tightly.  You can chamfer or round the corners of the dovetail shape for better
 568//   printing and assembly, or choose a fully rounded joint that looks more like a puzzle piece.  The dovetail appears
 569//   parallel to the Y axis and projecting upwards, so in its default orientation it will slide together with a translation
 570//   in the positive Y direction.  The gender determines whether the shape is meant to be added to your model or
 571//   differenced, and it also changes the anchor and orientation.  The default anchor for dovetails is BOTTOM;
 572//   the default orientation depends on the gender, with male dovetails oriented UP and female ones DOWN.  The dovetails by default
 573//   have extra extension of 0.01 for unions and differences.  You should ensure that attachment is done with overlap=0 to ensure that
 574//   the sizing and positioning is correct.  To adjust the fit, use the $slop variable, which increases the depth and width of
 575//   the female part of the joint to allow a clearance gap of $slop on each of the three sides.
 576//
 577// Arguments:
 578//   gender = A string, "male" or "female", to specify the gender of the dovetail.
 579//   w / width = Width (at the wider, top end) of the dovetail before tapering
 580//   h / height = Height of the dovetail (the amount it projects from its base)
 581//   slide / thickness = Distance the dovetail slides when you assemble it (length of sliding dovetails, thickness of regular dovetails)
 582//   ---
 583//   slope = slope of the dovetail.  Standard woodworking slopes are 4, 6, or 8.  Default: 6.
 584//   angle = angle (in degrees) of the dovetail.  Specify only one of slope and angle.
 585//   taper = taper angle (in degrees). Dovetail gets narrower by this angle.  Default: no taper
 586//   back_width = width of right hand end of the dovetail.  This alternate method of specifying the taper may be easier to manage.  Specify only one of `taper` and `back_width`.  Note that `back_width` should be smaller than `width` to taper in the customary direction, with the smaller end at the back.
 587//   chamfer = amount to chamfer the corners of the joint (Default: no chamfer)
 588//   r / radius = amount to round over the corners of the joint (Default: no rounding)
 589//   round = true to round both corners of the dovetail and give it a puzzle piece look.  Default: false.
 590//   $slop = Increase the width of socket by double this amount and depth by this amount to allow adjustment of the fit.
 591//   extra = amount of extra length and base extension added to dovetails for unions and differences.  Default: 0.01
 592// Example: Ordinary straight dovetail, male version (sticking up) and female version (below the xy plane)
 593//   dovetail("male", width=15, height=8, slide=30);
 594//   right(20) dovetail("female", width=15, height=8, slide=30);
 595// Example: Adding a 6 degree taper (Such a big taper is usually not necessary, but easier to see for the example.)
 596//   dovetail("male", w=15, h=8, slide=30, taper=6);
 597//   right(20) dovetail("female", 15, 8, 30, taper=6);  // Same as above
 598// Example: A block that can link to itself
 599//   diff()
 600//     cuboid([50,30,10]){
 601//       attach(BACK) dovetail("male", slide=10, width=15, height=8);
 602//       tag("remove")attach(FRONT) dovetail("female", slide=10, width=15, height=8);
 603//     }
 604// Example: Setting the dovetail angle.  This is too extreme to be useful.
 605//   diff()
 606//     cuboid([50,30,10]){
 607//       attach(BACK) dovetail("male", slide=10, width=15, height=8, angle=30);
 608//       tag("remove")attach(FRONT) dovetail("female", slide=10, width=15, height=8, angle=30);
 609//     }
 610// Example: Adding a chamfer helps printed parts fit together without problems at the corners
 611//   diff("remove")
 612//     cuboid([50,30,10]){
 613//       attach(BACK) dovetail("male", slide=10, width=15, height=8, chamfer=1);
 614//       tag("remove")attach(FRONT) dovetail("female", slide=10, width=15, height=8,chamfer=1);
 615//     }
 616// Example: Rounding the outside corners is another option
 617//   diff("remove")
 618//   cuboid([50,30,10]) {
 619//       attach(BACK)  dovetail("male", slide=10, width=15, height=8, radius=1, $fn=32);
 620//       tag("remove") attach(FRONT) dovetail("female", slide=10, width=15, height=8, radius=1, $fn=32);
 621//   }
 622// Example: Or you can make a fully rounded joint
 623//   $fn=32;
 624//   diff("remove")
 625//   cuboid([50,30,10]){
 626//       attach(BACK) dovetail("male", slide=10, width=15, height=8, radius=1.5, round=true);
 627//       tag("remove")attach(FRONT) dovetail("female", slide=10, width=15, height=8, radius=1.5, round=true);
 628//   }
 629// Example: With a long joint like this, a taper makes the joint easy to assemble.  It will go together easily and wedge tightly if you get the tolerances right.  Specifying the taper with `back_width` may be easier than using a taper angle.
 630//   cuboid([50,30,10])
 631//     attach(TOP) dovetail("male", slide=50, width=18, height=4, back_width=15, spin=90);
 632//   fwd(35)
 633//     diff("remove")
 634//       cuboid([50,30,10])
 635//         tag("remove") attach(TOP) dovetail("female", slide=50, width=18, height=4, back_width=15, spin=90);
 636// Example: A series of dovetails forming a tail board, with the inside of the joint up.  A standard wood joint would have a zero taper.
 637//   cuboid([50,30,10])
 638//     attach(BACK) xcopies(10,5) dovetail("male", slide=10, width=7, taper=4, height=4);
 639// Example: Mating pin board for a half-blind right angle joint, where the joint only shows on the side but not the front.  Note that the anchor method and use of `spin` ensures that the joint works even with a taper.
 640//   diff("remove")
 641//     cuboid([50,30,10])
 642//       tag("remove")position(TOP+BACK) xcopies(10,5) dovetail("female", slide=10, width=7, taper=4, height=4, anchor=BOTTOM+FRONT,spin=180);
 643function dovetail(gender, width, height, slide, h, w, angle, slope, thickness, taper, back_width, chamfer, extra=0.01, r, radius, round=false, anchor=BOTTOM, spin=0, orient) = no_function("dovetail");
 644module dovetail(gender, width, height, slide, h, w, angle, slope, thickness, taper, back_width, chamfer, extra=0.01, r, radius, round=false, anchor=BOTTOM, spin=0, orient)
 645{
 646    radius = get_radius(r1=radius,r2=r);
 647    slide = one_defined([slide,thickness],"slide,thickness");
 648    h = one_defined([h,height],"h,height");
 649    w = one_defined([w,width],"w,width");
 650    orient = is_def(orient) ? orient
 651           : gender == "female" ? DOWN
 652           : UP;
 653    count = num_defined([angle,slope]);
 654    count2 = num_defined([taper,back_width]);
 655    count3 = num_defined([chamfer, radius]);
 656    dummy =
 657        assert(count<=1, "Do not specify both angle and slope")
 658        assert(count2<=1, "Do not specify both taper and back_width")
 659        assert(count3<=1 || (radius==0 && chamfer==0), "Do not specify both chamfer and radius");
 660    slope = is_def(slope) ? slope
 661          : is_def(angle) ? 1/tan(angle)
 662          :  6;
 663    height_slop = gender == "female" ? get_slop() : 0;
 664
 665    // Need taper angle for computing width adjustment, but not used elsewhere
 666    taper_ang = is_def(taper) ? taper
 667              : is_def(back_width) ? atan((back_width-width)/2/slide)
 668              : 0;
 669    // This is the adjustment factor for width to grow in the direction normal to the dovetail face
 670    wfactor = sqrt( 1/slope^2 + 1/cos(taper_ang)^2 );
 671             // adjust width for increased height    adjust for normal to dovetail surface
 672    width_slop = 2*height_slop/slope                + 2* height_slop * wfactor;
 673    width = w + width_slop;
 674    height = h + height_slop;
 675    back_width = u_add(back_width, width_slop);
 676
 677    extra_offset = is_def(taper) ? -extra * tan(taper)
 678                 : is_def(back_width) ? extra * (back_width-width)/slide/2
 679                 : 0;
 680
 681    size = is_def(chamfer) && chamfer>0 ? chamfer
 682         : is_def(radius) && radius>0 ? radius
 683         : 0;
 684    fullsize = round ? [size,size]
 685             : gender == "male" ? [size,0]
 686             : [0,size];
 687
 688    type = is_def(chamfer) && chamfer>0 ? "chamfer" : "circle";
 689
 690    smallend_half = round_corners(
 691        move(
 692            [0,-slide/2-extra,0],
 693            p=[
 694                [0,                                     0, height],
 695                [width/2 - extra_offset,                0, height],
 696                [width/2 - extra_offset - height/slope, 0, 0     ],
 697                [width/2 - extra_offset + height,       0, 0     ]
 698            ]
 699        ),
 700        method=type, cut = fullsize, closed=false
 701    );
 702
 703    smallend_points = concat(select(smallend_half, 1, -2), [down(extra,p=select(smallend_half, -2))]);
 704    offset = is_def(taper) ? -slide * tan(taper)
 705           : is_def(back_width) ? (back_width-width) / 2
 706           : 0;
 707    bigend_points = move([offset+2*extra_offset,slide+2*extra,0], p=smallend_points);
 708
 709    bigenough = all_nonnegative(column(smallend_half,0)) && all_nonnegative(column(bigend_points,0));
 710
 711    assert(bigenough, "Width of dovetail is not large enough for its geometry (angle and taper");
 712
 713    //adjustment = $overlap * (gender == "male" ? -1 : 1);  // Adjustment for default overlap in attach()
 714    adjustment = 0;    // Default overlap is assumed to be zero
 715
 716    // This code computes the true normal from which the exact width factor can be obtained
 717    // as the x component.  Comparing to wfactor above shows that they agree.
 718    //   pts = [smallend_points[0], smallend_points[1], bigend_points[1],bigend_points[0]];
 719    //   n = -polygon_normal(pts);
 720    //   echo(n=n);
 721    //   echo(invwfactor = 1/wfactor, error = n.x-1/wfactor);
 722
 723    attachable(anchor,spin,orient, size=[width+2*offset, slide, height]) {
 724        down(height/2+adjustment) {
 725            //color("red")stroke([pts],width=.1);
 726
 727            skin(
 728                [
 729                    reverse(concat(smallend_points, xflip(p=reverse(smallend_points)))),
 730                    reverse(concat(bigend_points, xflip(p=reverse(bigend_points))))
 731                ],
 732                slices=0, convexity=4
 733            );
 734        }
 735        children();
 736    }
 737}
 738
 739
 740// Section: Tension Clips
 741
 742// h is total height above 0 of the nub
 743// nub extends below xy plane by distance nub/2
 744module _pin_nub(r, nub, h)
 745{
 746    L = h / 4;
 747    rotate_extrude(){
 748      polygon(
 749       [[ 0,-nub/2],
 750        [-r,-nub/2],
 751        [-r-nub, nub/2],
 752        [-r-nub, nub/2+L],
 753        [-r, h],
 754        [0, h]]);
 755     }
 756}
 757
 758
 759module _pin_slot(l, r, t, d, nub, depth, stretch) {
 760  yscale(4)
 761    intersection() {
 762      translate([t, 0, d + t / 4])
 763          _pin_nub(r = r + t, nub = nub, h = l - (d + t / 4));
 764      translate([-t, 0, d + t / 4])
 765          _pin_nub(r = r + t, nub = nub, h = l - (d + t / 4));
 766    }
 767  cube([2 * r, depth, 2 * l], center = true);
 768  up(l)
 769    zscale(stretch)
 770      ycyl(r = r, h = depth);
 771}
 772
 773
 774module _pin_shaft(r, lStraight, nub, nubscale, stretch, d, pointed)
 775{
 776   extra = 0.02;         // This sets the extra extension below the socket bottom
 777                         // so that difference() works without issues
 778   rPoint = r / sqrt(2);
 779   down(extra) cylinder(r = r, h = lStraight + extra);
 780   up(lStraight) {
 781      zscale(stretch) {
 782         hull() {
 783            sphere(r = r);
 784            if (pointed) up(rPoint) cylinder(r1 = rPoint, r2 = 0, h = rPoint/stretch);
 785         }
 786      }
 787   }
 788   up(d) yscale(nubscale) _pin_nub(r = r, nub = nub, h = lStraight - d);
 789}
 790
 791function _pin_size(size) =
 792  is_undef(size) ? [] :
 793  let(sizeok = in_list(size,["tiny", "small","medium", "large", "standard"]))
 794  assert(sizeok,"Pin size must be one of \"tiny\", \"small\", \"medium\" or \"standard\"")
 795  size=="standard" || size=="large" ?
 796     struct_set([], ["length", 10.8,
 797                     "diameter", 7,
 798                     "snap", 0.5,
 799                     "nub_depth", 1.8,
 800                     "thickness", 1.8,
 801                     "preload", 0.2]):
 802  size=="medium" ?
 803     struct_set([], ["length", 8,
 804                     "diameter", 4.6,
 805                     "snap", 0.45,
 806                     "nub_depth", 1.5,
 807                     "thickness", 1.4,
 808                     "preload", 0.2]) :
 809  size=="small" ?
 810     struct_set([], ["length", 6,
 811                     "diameter", 3.2,
 812                     "snap", 0.4,
 813                     "nub_depth", 1.2,
 814                     "thickness", 1.0,
 815                     "preload", 0.16]) :
 816  size=="tiny" ?
 817     struct_set([], ["length", 4,
 818                     "diameter", 2.5,
 819                     "snap", 0.25,
 820                     "nub_depth", 0.9,
 821                     "thickness", 0.8,
 822                     "preload", 0.1]):
 823  undef;
 824
 825
 826// Module: snap_pin()
 827// Synopsis: Creates a snap-pin that can slot into a {{snap_pin_socket()}} to join two parts.
 828// SynTags: Geom
 829// Topics: Joiners, Parts
 830// See Also: snap_pin_socket(), joiner(), dovetail(), snap_pin(), rabbit_clip()
 831// Usage:
 832//    snap_pin(size, [pointed=], [anchor=], [spin=], [orient]=) [ATTACHMENTS];
 833//    snap_pin(r=|radius=|d=|diameter=, l=|length=, nub_depth=, snap=, thickness=, [clearance=], [preload=], [pointed=]) [ATTACHMENTS];
 834// Description:
 835//    Creates a snap pin that can be inserted into an appropriate socket to connect two objects together.  You can choose from some standard
 836//    pin dimensions by giving a size, or you can specify all the pin geometry parameters yourself.  If you use a standard size you can
 837//    override the standard parameters by specifying other ones.  The pins have flat sides so they can
 838//    be printed.  When oriented UP the shaft of the pin runs in the Z direction and the flat sides are the front and back.  The default
 839//    orientation (FRONT) and anchor (FRONT) places the pin in a printable configuration, flat side down on the xy plane.
 840//    The tightness of fit is determined by `preload` and `clearance`.  To make pins tighter increase `preload` and/or decrease `clearance`.
 841//    .
 842//    The "large" or "standard" size pin has a length of 10.8 and diameter of 7.  The "medium" pin has a length of 8 and diameter of 4.6.  The "small" pin
 843//    has a length of 6 and diameter of 3.2.  The "tiny" pin has a length of 4 and a diameter of 2.5.
 844//    .
 845//    This pin is based on https://www.thingiverse.com/thing:213310 by Emmett Lalishe
 846//    and a modified version at https://www.thingiverse.com/thing:3218332 by acwest
 847//    and distributed under the Creative Commons - Attribution - Share Alike License
 848// Arguments:
 849//    size = text string to select from a list of predefined sizes, one of "standard", "medium", "small", or "tiny".
 850//    ---
 851//    pointed = set to true to get a pointed pin, false to get one with a rounded end.  Default: true
 852//    r/radius = radius of the pin
 853//    d/diameter = diameter of the pin
 854//    l/length = length of the pin
 855//    nub_depth = the distance of the nub from the base of the pin
 856//    snap = how much snap the pin provides (the nub projection)
 857//    thickness = thickness of the pin walls
 858//    pointed = if true the pin is pointed, otherwise it has a rounded tip.  Default: true
 859//    clearance = how far to shrink the pin away from the socket walls.  Default: 0.2
 860//    preload = amount to move the nub towards the pin base, which can create tension from the misalignment with the socket.  Default: 0.2
 861// Example: Pin in native orientation
 862//    snap_pin("standard", anchor=CENTER, orient=UP, thickness = 1, $fn=40);
 863// Example: Pins oriented for printing
 864//    xcopies(spacing=10, n=4) snap_pin("standard", $fn=40);
 865function snap_pin(size,r,radius,d,diameter, l,length, nub_depth, snap, thickness, clearance=0.2, preload, pointed=true, anchor=FRONT, spin=0, orient=FRONT, center) =no_function("snap_pin");
 866module snap_pin(size,r,radius,d,diameter, l,length, nub_depth, snap, thickness, clearance=0.2, preload, pointed=true, anchor=FRONT, spin=0, orient=FRONT, center) {
 867  preload_default = 0.2;
 868  sizedat = _pin_size(size);
 869  radius = get_radius(r1=r,r2=radius,d1=d,d2=diameter,dflt=struct_val(sizedat,"diameter")/2);
 870  length = first_defined([l,length,struct_val(sizedat,"length")]);
 871  snap = first_defined([snap, struct_val(sizedat,"snap")]);
 872  thickness = first_defined([thickness, struct_val(sizedat,"thickness")]);
 873  nub_depth = first_defined([nub_depth, struct_val(sizedat,"nub_depth")]);
 874  preload = first_defined([first_defined([preload, struct_val(sizedat, "preload")]),preload_default]);
 875
 876  nubscale = 0.9;      // Mysterious arbitrary parameter
 877
 878  // The basic pin assumes a rounded cap of length sqrt(2)*r, which defines lStraight.
 879  // If the point is enabled the cap length is instead 2*r
 880  // preload shrinks the length, bringing the nubs closer together
 881
 882  rInner = radius - clearance;
 883  stretch = sqrt(2)*radius/rInner;  // extra stretch factor to make cap have proper length even though r is reduced.
 884  lStraight = length - sqrt(2) * radius - clearance;
 885  lPin = lStraight + (pointed ? 2*radius : sqrt(2)*radius);
 886  attachable(anchor=anchor,spin=spin, orient=orient,
 887             size=[nubscale*(2*rInner+2*snap + clearance),radius*sqrt(2)-2*clearance,2*lPin]){
 888  zflip_copy()
 889      difference() {
 890        intersection() {
 891            cube([3 * (radius + snap), radius * sqrt(2) - 2 * clearance, 2 * length + 3 * radius], center = true);
 892            _pin_shaft(rInner, lStraight, snap+clearance/2, nubscale, stretch, nub_depth-preload, pointed);
 893        }
 894        _pin_slot(l = lStraight, r = rInner - thickness, t = thickness, d = nub_depth - preload, nub = snap, depth = 2 * radius + 0.02, stretch = stretch);
 895      }
 896  children();
 897  }
 898}
 899
 900// Module: snap_pin_socket()
 901// Synopsis: Creates a snap-pin socket for a {{snap_pin()}} to slot into.
 902// SynTags: Geom
 903// Topics: Joiners, Parts
 904// See Also: snap_pin(), joiner(), dovetail(), snap_pin(), rabbit_clip()
 905// Usage:
 906//   snap_pin_socket(size, [fixed=], [fins=], [pointed=], [anchor=], [spin=], [orient=]) [ATTACHMENTS];
 907//   snap_pin_socket(r=|radius=|d=|diameter=, l=|length=, nub_depth=, snap=, [fixed=], [pointed=], [fins=]) [ATTACHMENTS];
 908// Description:
 909//   Constructs a socket suitable for a snap_pin with the same parameters.   If `fixed` is true then the socket has flat walls and the
 910//   pin will not rotate in the socket.  If `fixed` is false then the socket is round and the pin will rotate, particularly well
 911//   if you add a lubricant.  If `pointed` is true the socket is pointed to receive a pointed pin, otherwise it has a rounded and and
 912//   will be shorter.  If `fins` is set to true then two fins are included inside the socket to act as supports (which may help when printing tip up,
 913//   especially when `pointed=false`).  The default orientation is DOWN with anchor BOTTOM so that you can difference() the socket away from an object.
 914//   The socket extends 0.02 extra below its bottom anchor point so that differences will work correctly.  (You must have $overlap smaller than 0.02 in
 915//   attach or the socket will be beneath the surface of the parent object.)
 916//   .
 917//   The "large" or "standard" size pin has a length of 10.8 and diameter of 7.  The "medium" pin has a length of 8 and diameter of 4.6.  The "small" pin
 918//   has a length of 6 and diameter of 3.2.  The "tiny" pin has a length of 4 and a diameter of 2.5.
 919// Arguments:
 920//   size = text string to select from a list of predefined sizes, one of "standard", "medium", "small", or "tiny".
 921//   ---
 922//   pointed = set to true to get a pointed pin, false to get one with a rounded end.  Default: true
 923//   r/radius = radius of the pin
 924//   d/diameter = diameter of the pin
 925//   l/length = length of the pin
 926//   nub_depth = the distance of the nub from the base of the pin
 927//   snap = how much snap the pin provides (the nub projection)
 928//   fixed = if true the pin cannot rotate, if false it can.  Default: true
 929//   pointed = if true the socket has a pointed tip.  Default: true
 930//   fins = if true supporting fins are included.  Default: false
 931// Example:  The socket shape itself in native orientation.
 932//   snap_pin_socket("standard", anchor=CENTER, orient=UP, fins=true, $fn=40);
 933// Example:  A spinning socket with fins:
 934//   snap_pin_socket("standard", anchor=CENTER, orient=UP, fins=true, fixed=false, $fn=40);
 935// Example:  A cube with a socket in the middle and one half-way off the front edge so you can see inside:
 936//   $fn=40;
 937//   diff("socket") cuboid([20,20,20])
 938//     tag("socket"){
 939//       attach(TOP) snap_pin_socket("standard");
 940//       position(TOP+FRONT)snap_pin_socket("standard");
 941//     }
 942function snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fixed=true, pointed=true, fins=false, anchor=BOTTOM, spin=0, orient=DOWN) = no_function("snap_pin_socket");
 943module snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fixed=true, pointed=true, fins=false, anchor=BOTTOM, spin=0, orient=DOWN) {
 944  sizedat = _pin_size(size);
 945  radius = get_radius(r1=r,r2=radius,d1=d,d2=diameter,dflt=struct_val(sizedat,"diameter")/2);
 946  length = first_defined([l,length,struct_val(sizedat,"length")]);
 947  snap = first_defined([snap, struct_val(sizedat,"snap")]);
 948  nub_depth = first_defined([nub_depth, struct_val(sizedat,"nub_depth")]);
 949
 950  tip = pointed ? sqrt(2) * radius : radius;
 951  lPin = length + (pointed?(2-sqrt(2))*radius:0);
 952  lStraight = lPin - (pointed?sqrt(2)*radius:radius);
 953  attachable(anchor=anchor,spin=spin,orient=orient,
 954             size=[2*(radius+snap),radius*sqrt(2),lPin])
 955  {
 956  down(lPin/2)
 957    intersection() {
 958      cube([3 * (radius + snap), fixed ? radius * sqrt(2) : 3*(radius+snap), 3 * lPin + 3 * radius], center = true);
 959      union() {
 960        _pin_shaft(radius,lStraight,snap,1,1,nub_depth,pointed);
 961        if (fins)
 962          up(lStraight){
 963            cube([2 * radius, 0.01, 2 * tip], center = true);
 964            cube([0.01, 2 * radius, 2 * tip], center = true);
 965          }
 966      }
 967    }
 968  children();
 969  }
 970}
 971
 972
 973
 974// Module: rabbit_clip()
 975// Synopsis: Creates a rabbit-eared clip that can snap into a slot.
 976// SynTags: Geom
 977// Topics: Joiners, Parts
 978// See Also: snap_pin(), joiner(), dovetail(), snap_pin(), rabbit_clip()
 979// Usage:
 980//   rabbit_clip(type, length, width, snap, thickness, depth, [compression=], [clearance=], [lock=], [lock_clearance=], [splineteps=], [anchor=], [orient=], [spin=]) [ATTACHMENTS];
 981// Description:
 982//   Creates a clip with two flexible ears to lock into a mating socket, or create a mask to produce the appropriate
 983//   mating socket.  The clip can be made to insert and release easily, or to hold much better, or it can be
 984//   created with locking flanges that will make it very hard or impossible to remove.  Unlike the snap pin, this clip
 985//   is rectangular and can be made at any height, so a suitable clip could be very thin.  It's also possible to get a
 986//   solid connection with a short pin.
 987//   .
 988//   The type parameters specifies whether to make a clip, a socket mask, or a double clip.  The length is the
 989//   total nominal length of the clip.  (The actual length will be very close, but not equal to this.)  The width
 990//   gives the nominal width of the clip, which is the actual width of the clip at its base.  The snap parameter
 991//   gives the depth of the clip sides, which controls how easy the clip is to insert and remove.  The clip "ears" are
 992//   made over-wide by the compression value.  A nonzero compression helps make the clip secure in its socket.
 993//   The socket's width and length are increased by the clearance value which creates some space and can compensate
 994//   for printing inaccuracy.  The socket will be slightly longer than the nominal width.  The thickness is the thickness
 995//   curved line that forms the clip.  The clip depth is the amount the basic clip shape is extruded.  Be sure that you
 996//   make the socket with a larger depth than the clip (try 0.4 mm) to allow ease of insertion of the clip.  The clearance
 997//   value does not apply to the depth.  The splinesteps parameter increases the sampling of the clip curves.
 998//   .
 999//   By default clips appear with orient=UP and sockets with orient=DOWN.  The clips and sockets extend 0.02 units below
1000//   their base so that unions and differences will work without trouble, but be sure that the attach overlap is smaller
1001//   than 0.02.
1002//   .
1003//   The first figure shows the dimensions of the rabbit clip.  The second figure shows the clip in red overlayed on
1004//   its socket in yellow.  The left clip has a nonzero clearance, so its socket is bigger than the clip all around.
1005//   The right hand locking clip has no clearance, but it has a lock clearance, which provides some space behind
1006//   the lock to allow the clip to fit.  (Note that depending on your printer, this can be set to zero.)
1007// Figure(2DMed,NoAxes):
1008//   snap=1.5;
1009//   comp=0.75;
1010//   mid = 8.053;  // computed in rabbit_clip
1011//   tip = [-4.58,18.03];
1012//   translate([9,3]){
1013//   back_half()
1014//      rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK);
1015//   color("blue"){
1016//      stroke([[6,0],[6,18]],width=0.1);
1017//      stroke([[6+comp, 12], [6+comp, 18]], width=.1);
1018//   }
1019//   color("red"){
1020//      stroke([[6-snap,mid], [6,mid]], endcaps="arrow2",width=0.15);
1021//      translate([6+.4,mid-.15])text("snap",size=1,valign="center");
1022//      translate([6+comp/2,19.5])text("compression", size=1, halign="center");
1023//      stroke([[6+comp/2,19.3], [6+comp/2,17.7]], endcap2="arrow2", width=.15);
1024//      fwd(1.1)text("width",size=1,halign="center");
1025//      xflip_copy()stroke([[2,-.7], [6,-.7]], endcap2="arrow2", width=.15);
1026//      move([-6.7,mid])rot(90)text("length", size=1, halign="center");
1027//      stroke([[-7,10.3], [-7,18]], width=.15, endcap2="arrow2");
1028//      stroke([[-7,0], [-7,5.8]], width=.15,endcap1="arrow2");
1029//      stroke([tip, tip-[0,1]], width=.15);
1030//      move([tip.x+2,19.5])text("thickness", halign="center",size=1);
1031//      stroke([[tip.x+2, 19.3], tip+[.1,.1]], width=.15, endcap2="arrow2");
1032//   }
1033//   }
1034//
1035// Figure(2DMed,NoAxes):
1036//   snap=1.5;
1037//   comp=0;
1038//   translate([29,3]){
1039//   back_half()
1040//      rabbit_clip("socket", width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK,lock=true);
1041//   color("red")back_half()
1042//      rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap,
1043//               orient=BACK,lock=true,lock_clearance=1);
1044//   }
1045//   translate([9,3]){
1046//   back_half()
1047//      rabbit_clip("socket", clearance=.5,width=12, length=18, depth=1, thickness = 1,
1048//                  compression=comp, snap=snap, orient=BACK,lock=false);
1049//   color("red")back_half()
1050//      rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap,
1051//               orient=BACK,lock=false,lock_clearance=1);
1052//   }
1053// Arguments:
1054//   type = One of "pin",  "socket", "male", "female" or "double" to specify what to make.
1055//   length = nominal clip length
1056//   width = nominal clip width
1057//   snap = depth of hollow on the side of the clip
1058//   thickness = thickness of the clip "line"
1059//   depth = amount to extrude clip (give extra room for the socket, about 0.4mm)
1060//   ---
1061//   compression = excess width at the "ears" to lock more tightly.  Default: 0.1
1062//   clearance = extra space in the socket for easier insertion.  Default: 0.1
1063//   lock = set to true to make a locking clip that may be irreversible.  Default: false
1064//   lock_clearance = give clearance for the lock.  Default: 0
1065//   splinesteps = number of samples in the curves of the clip.  Default: 8
1066//   anchor = anchor point for clip
1067//   orient = clip orientation.  Default: UP for pins, DOWN for sockets
1068//   spin = spin the clip.  Default: 0
1069//
1070// Example:  Here are several sizes that work printed in PLA on a Prusa MK3, with default clearance of 0.1 and a depth of 5
1071//   module test_pair(length, width, snap, thickness, compression, lock=false)
1072//   {
1073//     depth = 5;
1074//     extra_depth = 10;// Change this to 0.4 for closed sockets
1075//     cuboid([max(width+5,12),12, depth], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM)
1076//         attach(BACK)
1077//           rabbit_clip(type="pin",length=length, width=width,snap=snap,thickness=thickness,depth=depth,
1078//                       compression=compression,lock=lock);
1079//     right(width+13)
1080//     diff("remove")
1081//         cuboid([width+8,max(12,length+2),depth+3], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM)
1082//           tag("remove")
1083//             attach(BACK)
1084//               rabbit_clip(type="socket",length=length, width=width,snap=snap,thickness=thickness,
1085//                           depth=depth+extra_depth, lock=lock,compression=0);
1086//   }
1087//   left(37)ydistribute(spacing=28){
1088//     test_pair(length=6, width=7, snap=0.25, thickness=0.8, compression=0.1);
1089//     test_pair(length=3.5, width=7, snap=0.1, thickness=0.8, compression=0.1);  // snap = 0.2 gives a firmer connection
1090//     test_pair(length=3.5, width=5, snap=0.1, thickness=0.8, compression=0.1);  // hard to take apart
1091//   }
1092//   right(17)ydistribute(spacing=28){
1093//     test_pair(length=12, width=10, snap=1, thickness=1.2, compression=0.2);
1094//     test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible
1095//     test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible
1096//   }
1097// Example: Double clip to connect two sockets
1098//   rabbit_clip("double",length=8, width=7, snap=0.75, thickness=0.8, compression=0.2,depth=5);
1099// Example:  A modified version of the clip that acts like a backpack strap clip, where it locks tightly but you can squeeze to release.
1100//   cuboid([25,15,5],anchor=BOTTOM)
1101//       attach(BACK)rabbit_clip("pin", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5, lock_clearance=3);
1102//   left(32)
1103//   diff("remove")
1104//   cuboid([30,30,11],orient=BACK,anchor=BACK){
1105//       tag("remove")attach(BACK)rabbit_clip("socket", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5.5, lock_clearance=3);
1106//       xflip_copy()
1107//         position(FRONT+LEFT)
1108//         xscale(0.8)
1109//         tag("remove")zcyl(l=20,r=13.5, $fn=64);
1110//   }
1111
1112function rabbit_clip(type, length, width,  snap, thickness, depth, compression=0.1,  clearance=.1, lock=false, lock_clearance=0,
1113                   splinesteps=8, anchor, orient, spin=0) = no_function("rabbit_clip");
1114
1115module rabbit_clip(type, length, width,  snap, thickness, depth, compression=0.1,  clearance=.1, lock=false, lock_clearance=0,
1116                   splinesteps=8, anchor, orient, spin=0)
1117{
1118  legal_types = ["pin","socket","male","female","double"];
1119  check =
1120    assert(is_num(width) && width>0,"Width must be a positive value")
1121    assert(is_num(length) && length>0, "Length must be a positive value")
1122    assert(is_num(thickness) && thickness>0, "Thickness must be a positive value")
1123    assert(is_num(snap) && snap>=0, "Snap must be a non-negative value")
1124    assert(is_num(depth) && depth>0, "Depth must be a positive value")
1125    assert(is_num(compression) && compression >= 0, "Compression must be a nonnegative value")
1126    assert(is_bool(lock))
1127    assert(is_num(lock_clearance))
1128    assert(in_list(type,legal_types),str("type must be one of ",legal_types));
1129  if (type=="double") {
1130    attachable(size=[width+2*compression, depth, 2*length], anchor=default(anchor,BACK), spin=spin, orient=default(orient,BACK)){
1131      union(){
1132        rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression,
1133                    lock=lock, anchor=BOTTOM, orient=UP);
1134        rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression,
1135                    lock=lock, anchor=BOTTOM, orient=DOWN);
1136        cuboid([width-thickness, depth, thickness]);
1137      }
1138      children();
1139    }
1140  } else {
1141    anchor = default(anchor,BOTTOM);
1142    is_pin = in_list(type,["pin","male"]);
1143    //default_overlap = 0.01 * (is_pin?1:-1);    // Shift by this much to undo default overlap
1144    default_overlap = 0;
1145    extra = 0.02;  // Amount of extension below nominal based position for the socket, must exceed default overlap of 0.01
1146    clearance = is_pin ? 0 : clearance;
1147    compression = is_pin ? compression : 0;
1148    orient =  is_def(orient) ? orient
1149            : is_pin ? UP
1150            : DOWN;
1151    earwidth = 2*thickness+snap;
1152    point_length = earwidth/2.15;
1153    // The adjustment is using cos(theta)*earwidth/2 and sin(theta)*point_length, but the computation
1154    // is obscured because theta is atan(length/2/snap)
1155    scaled_len = length - 0.5 * (earwidth * snap + point_length * length) / sqrt(sqr(snap)+sqr(length/2));
1156    bottom_pt = [0,max(scaled_len*0.15+thickness, 2*thickness)];
1157    ctr = [width/2,scaled_len] + line_normal([width/2-snap, scaled_len/2], [width/2, scaled_len]) * earwidth/2;
1158    inside_pt = circle_circle_tangents(0, bottom_pt, earwidth/2, ctr)[0][1];
1159    sidepath =[
1160               [width/2,0],
1161               [width/2-snap,scaled_len/2],
1162               [width/2+(is_pin?compression:0), scaled_len],
1163               ctr - point_length * line_normal([width/2,scaled_len], inside_pt),
1164               inside_pt
1165              ];
1166    fullpath = concat(
1167                      sidepath,
1168                      [bottom_pt],
1169                      reverse(apply(xflip(),sidepath))
1170                      );
1171    dummy2 = assert(fullpath[4].y < fullpath[3].y, "Pin is too wide for its length");
1172
1173    snapmargin = -snap + last(sidepath).x;// - compression;
1174    if (is_pin){
1175      if (snapmargin<0) echo("WARNING: The snap is too large for the clip to squeeze to fit its socket")
1176      echo(snapmargin=snapmargin);
1177    }
1178    // Force tangent to be vertical at the outer edge of the clip to avoid overshoot
1179    fulltangent = list_set(path_tangents(fullpath, uniform=false),[2,8], [[0,1],[0,-1]]);
1180
1181    subset = is_pin ? [0:10] : [0,1,2,3, 7,8,9,10];  // Remove internal points from the socket
1182    tangent = select(fulltangent, subset);
1183    path = select(fullpath, subset);
1184
1185    socket_smooth = .04;
1186    pin_smooth = [.075, .075, .15, .12, .06];
1187    smoothing = is_pin
1188                  ? concat(pin_smooth, reverse(pin_smooth))
1189                  : let(side_smooth=select(pin_smooth, 0, 2))
1190                    concat(side_smooth, [socket_smooth], reverse(side_smooth));
1191    bez = path_to_bezpath(path,relsize=smoothing,tangents=tangent);
1192    rounded = bezpath_curve(bez,splinesteps=splinesteps);
1193    bounds = pointlist_bounds(rounded);
1194    extrapt = is_pin ? [] : [rounded[0] - [0,extra]];
1195    finalpath = is_pin ? rounded
1196                       : let(withclearance=offset(rounded, r=-clearance))
1197                         concat( [[withclearance[0].x,-extra]],
1198                                 withclearance,
1199                                 [[-withclearance[0].x,-extra]]);
1200    attachable(size=[bounds[1].x-bounds[0].x, depth, bounds[1].y-bounds[0].y], anchor=anchor, spin=spin, orient=orient){
1201      xrot(90)
1202        translate([0,-(bounds[1].y-bounds[0].y)/2+default_overlap,-depth/2])
1203        linear_extrude(height=depth, convexity=10) {
1204            if (lock)
1205              xflip_copy()
1206              right(clearance)
1207              polygon([sidepath[1]+[-thickness/10,lock_clearance],
1208                       sidepath[2]-[thickness*.75,0],
1209                       sidepath[2],
1210                       [sidepath[2].x,sidepath[1].y+lock_clearance]]);
1211            if (is_pin)
1212              offset_stroke(finalpath, width=[thickness,0]);
1213            else
1214              polygon(finalpath);
1215        }
1216      children();
1217    }
1218  }
1219}
1220
1221
1222
1223// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap