1//////////////////////////////////////////////////////////////////////
   2// LibFile: bottlecaps.scad
   3//   Bottle caps and necks for PCO18XX standard plastic beverage bottles, and SPI standard bottle necks.  
   4// Includes:
   5//   include <BOSL2/std.scad>
   6//   include <BOSL2/bottlecaps.scad>
   7// FileGroup: Threaded Parts
   8// FileSummary: Standard bottle caps and necks.
   9//////////////////////////////////////////////////////////////////////
  10
  11
  12include <threading.scad>
  13include <structs.scad>
  14include <rounding.scad>
  15
  16// Section: PCO-1810 Bottle Threading
  17
  18
  19// Module: pco1810_neck()
  20// Usage:
  21//   pco1810_neck([wall]) [ATTACHMENTS];
  22// Description:
  23//   Creates an approximation of a standard PCO-1810 threaded beverage bottle neck.
  24// Arguments:
  25//   wall = Wall thickness in mm.
  26//   ---
  27//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
  28//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
  29//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
  30// Extra Anchors:
  31//   "tamper-ring" = Centered at the top of the anti-tamper ring channel.
  32//   "support-ring" = Centered at the bottom of the support ring.
  33// Example:
  34//   pco1810_neck();
  35// Example: Standard Anchors
  36//   pco1810_neck() show_anchors(custom=false);
  37// Example: Custom Named Anchors
  38//   expose_anchors(0.3)
  39//       pco1810_neck()
  40//           show_anchors(std=false);
  41module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP)
  42{
  43    inner_d = 21.74;
  44    neck_d = 26.19;
  45    neck_h = 5.00;
  46    support_d = 33.00;
  47    support_width = 1.45;
  48    support_rad = 0.40;
  49    support_h = 21.00;
  50    support_ang = 16;
  51    tamper_ring_d = 27.97;
  52    tamper_ring_width = 0.50;
  53    tamper_ring_r = 1.60;
  54    tamper_base_d = 25.71;
  55    tamper_base_h = 14.10;
  56    threadbase_d = 24.51;
  57    thread_pitch = 3.18;
  58    flank_angle = 20;
  59    thread_od = 27.43;
  60    lip_d = 25.07;
  61    lip_h = 1.70;
  62    lip_leadin_r = 0.20;
  63    lip_recess_d = 24.94;
  64    lip_recess_h = 1.00;
  65    lip_roundover_r = 0.58;
  66
  67    $fn = segs(support_d/2);
  68    h = support_h+neck_h;
  69    thread_h = (thread_od-threadbase_d)/2;
  70    anchors = [
  71        named_anchor("support-ring", [0,0,neck_h-h/2]),
  72        named_anchor("tamper-ring", [0,0,h/2-tamper_base_h])
  73    ];
  74    attachable(anchor,spin,orient, d1=neck_d, d2=lip_recess_d+2*lip_leadin_r, l=h, anchors=anchors) {
  75        down(h/2) {
  76            rotate_extrude(convexity=10) {
  77                polygon(turtle(
  78                    state=[inner_d/2,0], [
  79                        "untilx", neck_d/2,
  80                        "left", 90,
  81                        "move", neck_h - 1,
  82                        "arcright", 1, 90,
  83                        "untilx", support_d/2-support_rad,
  84                        "arcleft", support_rad, 90,
  85                        "move", support_width,
  86                        "arcleft", support_rad, 90-support_ang,
  87                        "untilx", tamper_base_d/2,
  88                        "right", 90-support_ang,
  89                        "untily", h-tamper_base_h,   // Tamper ring holder base.
  90                        "right", 90,
  91                        "untilx", tamper_ring_d/2,
  92                        "left", 90,
  93                        "move", tamper_ring_width,
  94                        "arcleft", tamper_ring_r, 90,
  95                        "untilx", threadbase_d/2,
  96                        "right", 90,
  97                        "untily", h-lip_h-lip_leadin_r,  // Lip base.
  98                        "arcright", lip_leadin_r, 90,
  99                        "untilx", lip_d/2,
 100                        "left", 90,
 101                        "untily", h-lip_recess_h,
 102                        "left", 90,
 103                        "untilx", lip_recess_d/2,
 104                        "right", 90,
 105                        "untily", h-lip_roundover_r,
 106                        "arcleft", lip_roundover_r, 90,
 107                        "untilx", inner_d/2
 108                    ]
 109                ));
 110            }
 111            up(h-lip_h) {
 112                bottom_half() {
 113                    difference() {
 114                        thread_helix(
 115                            d=threadbase_d-0.1,
 116                            pitch=thread_pitch,
 117                            thread_depth=thread_h+0.1,
 118                            flank_angle=flank_angle,
 119                            turns=810/360,
 120                            taper=-thread_h*2,
 121                            anchor=TOP
 122                        );
 123                        zrot_copies(rots=[90,270]) {
 124                            zrot_copies(rots=[-28,28], r=threadbase_d/2) {
 125                                prismoid([20,1.82], [20,1.82+2*sin(29)*thread_h], h=thread_h+0.1, anchor=BOT, orient=RIGHT);
 126                            }
 127                        }
 128                    }
 129                }
 130            }
 131        }
 132        children();
 133    }
 134}
 135
 136function  pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP) =
 137    no_function("pco1810_neck");
 138
 139
 140// Module: pco1810_cap()
 141// Usage:
 142//   pco1810_cap([wall], [texture]) [ATTACHMENTS];
 143// Description:
 144//   Creates a basic cap for a PCO1810 threaded beverage bottle.
 145// Arguments:
 146//   wall = Wall thickness in mm.
 147//   texture = The surface texture of the cap.  Valid values are "none", "knurled", or "ribbed".  Default: "none"
 148//   ---
 149//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 150//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 151//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 152// Extra Anchors:
 153//   "inside-top" = Centered on the inside top of the cap.
 154// Examples:
 155//   pco1810_cap();
 156//   pco1810_cap(texture="knurled");
 157//   pco1810_cap(texture="ribbed");
 158// Example: Standard Anchors
 159//   pco1810_cap(texture="ribbed") show_anchors(custom=false);
 160// Example: Custom Named Anchors
 161//   expose_anchors(0.3)
 162//       pco1810_cap(texture="ribbed")
 163//           show_anchors(std=false);
 164module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
 165{
 166    cap_id = 28.58;
 167    tamper_ring_h = 14.10;
 168    thread_pitch = 3.18;
 169    flank_angle = 20;
 170    thread_od = cap_id;
 171    thread_depth = 1.6;
 172
 173    $fn = segs(33/2);
 174    w = cap_id + 2*wall;
 175    h = tamper_ring_h + wall;
 176    anchors = [
 177        named_anchor("inside-top", [0,0,-(h/2-wall)])
 178    ];
 179    attachable(anchor,spin,orient, d=w, l=h, anchors=anchors) {
 180        down(h/2) zrot(45) {
 181            difference() {
 182                union() {
 183                    if (texture == "knurled") {
 184                        cyl(d=w, h=h, texture="diamonds", tex_size=[3,3], tex_style="concave", anchor=BOT);
 185                    } else if (texture == "ribbed") {
 186                        cyl(d=w, h=h, texture="ribs", tex_size=[3,3], tex_style="min_edge", anchor=BOT);
 187                    } else {
 188                        cyl(d=w, l=tamper_ring_h+wall, anchor=BOTTOM);
 189                    }
 190                }
 191                up(wall) cyl(d=cap_id, h=tamper_ring_h+wall, anchor=BOTTOM);
 192            }
 193            up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, flank_angle=flank_angle, turns=810/360, taper=-thread_depth, internal=true, anchor=BOTTOM);
 194        }
 195        children();
 196    }
 197}
 198
 199function pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) =
 200    no_function("pco1810_cap");
 201
 202
 203
 204// Section: PCO-1881 Bottle Threading
 205
 206
 207// Module: pco1881_neck()
 208// Usage:
 209//   pco1881_neck([wall]) [ATTACHMENTS];
 210// Description:
 211//   Creates an approximation of a standard PCO-1881 threaded beverage bottle neck.
 212// Arguments:
 213//   wall = Wall thickness in mm.
 214//   ---
 215//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 216//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 217//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 218// Extra Anchors:
 219//   "tamper-ring" = Centered at the top of the anti-tamper ring channel.
 220//   "support-ring" = Centered at the bottom of the support ring.
 221// Example:
 222//   pco1881_neck();
 223// Example:
 224//   pco1881_neck() show_anchors(custom=false);
 225// Example:
 226//   expose_anchors(0.3)
 227//       pco1881_neck()
 228//           show_anchors(std=false);
 229module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP)
 230{
 231    inner_d = 21.74;
 232    neck_d = 26.19;
 233    neck_h = 5.00;
 234    support_d = 33.00;
 235    support_width = 0.58;
 236    support_rad = 0.30;
 237    support_h = 17.00;
 238    support_ang = 15;
 239    tamper_ring_d = 28.00;
 240    tamper_ring_width = 0.30;
 241    tamper_ring_ang = 45;
 242    tamper_base_d = 25.71;
 243    tamper_base_h = 11.20;
 244    tamper_divot_r = 1.08;
 245    threadbase_d = 24.20;
 246    thread_pitch = 2.70;
 247    flank_angle = 15;
 248    thread_od = 27.4;
 249    lip_d = 25.07;
 250    lip_h = 1.70;
 251    lip_leadin_r = 0.30;
 252    lip_recess_d = 24.94;
 253    lip_recess_h = 1.00;
 254    lip_roundover_r = 0.58;
 255
 256    $fn = segs(support_d/2);
 257    h = support_h+neck_h;
 258    thread_h = (thread_od-threadbase_d)/2;
 259    anchors = [
 260        named_anchor("support-ring", [0,0,neck_h-h/2]),
 261        named_anchor("tamper-ring", [0,0,h/2-tamper_base_h])
 262    ];
 263    attachable(anchor,spin,orient, d1=neck_d, d2=lip_recess_d+2*lip_leadin_r, l=h, anchors=anchors) {
 264        down(h/2) {
 265            rotate_extrude(convexity=10) {
 266                polygon(turtle(
 267                    state=[inner_d/2,0], [
 268                        "untilx", neck_d/2,
 269                        "left", 90,
 270                        "move", neck_h - 1,
 271                        "arcright", 1, 90,
 272                        "untilx", support_d/2-support_rad,
 273                        "arcleft", support_rad, 90,
 274                        "move", support_width,
 275                        "arcleft", support_rad, 90-support_ang,
 276                        "untilx", tamper_base_d/2,
 277                        "arcright", tamper_divot_r, 180-support_ang*2,
 278                        "left", 90-support_ang,
 279                        "untily", h-tamper_base_h,   // Tamper ring holder base.
 280                        "right", 90,
 281                        "untilx", tamper_ring_d/2,
 282                        "left", 90,
 283                        "move", tamper_ring_width,
 284                        "left", tamper_ring_ang,
 285                        "untilx", threadbase_d/2,
 286                        "right", tamper_ring_ang,
 287                        "untily", h-lip_h-lip_leadin_r,  // Lip base.
 288                        "arcright", lip_leadin_r, 90,
 289                        "untilx", lip_d/2,
 290                        "left", 90,
 291                        "untily", h-lip_recess_h,
 292                        "left", 90,
 293                        "untilx", lip_recess_d/2,
 294                        "right", 90,
 295                        "untily", h-lip_roundover_r,
 296                        "arcleft", lip_roundover_r, 90,
 297                        "untilx", inner_d/2
 298                    ]
 299                ));
 300            }
 301            up(h-lip_h) {
 302                difference() {
 303                    thread_helix(
 304                        d=threadbase_d-0.1,
 305                        pitch=thread_pitch,
 306                        thread_depth=thread_h+0.1,
 307                        flank_angle=flank_angle,
 308                        turns=650/360,
 309                        taper=-thread_h*2,
 310                        anchor=TOP
 311                    );
 312                    zrot_copies(rots=[90,270]) {
 313                        zrot_copies(rots=[-28,28], r=threadbase_d/2) {
 314                            prismoid([20,1.82], [20,1.82+2*sin(29)*thread_h], h=thread_h+0.1, anchor=BOT, orient=RIGHT);
 315                        }
 316                    }
 317                }
 318            }
 319        }
 320        children();
 321    }
 322}
 323
 324function pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP) =
 325    no_function("pco1881_neck");
 326
 327
 328// Module: pco1881_cap()
 329// Usage:
 330//   pco1881_cap(wall, [texture]) [ATTACHMENTS];
 331// Description:
 332//   Creates a basic cap for a PCO1881 threaded beverage bottle.
 333// Arguments:
 334//   wall = Wall thickness in mm.
 335//   texture = The surface texture of the cap.  Valid values are "none", "knurled", or "ribbed".  Default: "none"
 336//   ---
 337//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 338//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 339//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 340// Extra Anchors:
 341//   "inside-top" = Centered on the inside top of the cap.
 342// Examples:
 343//   pco1881_cap();
 344//   pco1881_cap(texture="knurled");
 345//   pco1881_cap(texture="ribbed");
 346// Example: Standard Anchors
 347//   pco1881_cap(texture="ribbed") show_anchors(custom=false);
 348// Example: Custom Named Anchors
 349//   expose_anchors(0.5)
 350//       pco1881_cap(texture="ribbed")
 351//           show_anchors(std=false);
 352module pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
 353{
 354    $fn = segs(33/2);
 355    w = 28.58 + 2*wall;
 356    h = 11.2 + wall;
 357    anchors = [
 358        named_anchor("inside-top", [0,0,-(h/2-wall)])
 359    ];
 360    attachable(anchor,spin,orient, d=w, l=h, anchors=anchors) {
 361        down(h/2) zrot(45) {
 362            difference() {
 363                union() {
 364                    if (texture == "knurled") {
 365                        cyl(d=w, h=11.2+wall, texture="diamonds", tex_size=[3,3], tex_style="concave", anchor=BOT);
 366                    } else if (texture == "ribbed") {
 367                        cyl(d=w, h=11.2+wall, texture="ribs", tex_size=[3,3], tex_style="min_edge", anchor=BOT);
 368                    } else {
 369                        cyl(d=w, l=11.2+wall, anchor=BOTTOM);
 370                    }
 371                }
 372                up(wall) cyl(d=28.58, h=11.2+wall, anchor=BOTTOM);
 373            }
 374            up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, flank_angle=15, turns=650/360, taper=-1.6, internal=true, anchor=BOTTOM);
 375        }
 376        children();
 377    }
 378}
 379
 380function pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP) =
 381    no_function("pco1881_cap");
 382
 383
 384
 385// Section: Generic Bottle Connectors
 386
 387// Module: generic_bottle_neck()
 388// Usage:
 389//   generic_bottle_neck([wall], ...) [ATTACHMENTS];
 390// Description:
 391//   Creates a bottle neck given specifications.
 392// Arguments:
 393//   wall = distance between ID and any wall that may be below the support
 394//   ---
 395//   neck_d = Outer diameter of neck without threads
 396//   id = Inner diameter of neck
 397//   thread_od = Outer diameter of thread
 398//   height = Height of neck above support
 399//   support_d = Outer diameter of support ring.  Set to 0 for no support.
 400//   pitch = Thread pitch
 401//   round_supp = True to round the lower edge of the support ring
 402//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 403//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 404//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 405// Extra Anchors:
 406//   "support-ring" = Centered at the bottom of the support ring.
 407// Example:
 408//   generic_bottle_neck();
 409module generic_bottle_neck(
 410    wall,
 411    neck_d = 25,
 412    id = 21.4,
 413    thread_od = 27.2,
 414    height = 17,
 415    support_d = 33.0,
 416    pitch = 3.2,
 417    round_supp = false,
 418    anchor = "support-ring",
 419    spin = 0,
 420    orient = UP
 421) {
 422    inner_d = id;
 423    neck_d = neck_d;
 424    supp_d = max(neck_d, support_d);
 425    thread_pitch = pitch;
 426    flank_angle = 15;
 427
 428    diamMagMult = neck_d / 26.19;
 429    heightMagMult = height / 17.00;
 430
 431    sup_r = 0.30 * (heightMagMult > 1 ? heightMagMult : 1);
 432    support_r = floor(((supp_d == neck_d) ? sup_r : min(sup_r, (supp_d - neck_d) / 2)) * 5000) / 10000;
 433    support_rad = (wall == undef || !round_supp) ? support_r :
 434        min(support_r, floor((supp_d - (inner_d + 2 * wall)) * 5000) / 10000);
 435        //Too small of a radius will cause errors with the arc, this limits granularity to .0001mm
 436    support_width = 1 * (heightMagMult > 1 ? heightMagMult : 1) * sign(support_d);
 437    roundover = 0.58 * diamMagMult;
 438    lip_roundover_r = (roundover > (neck_d - inner_d) / 2) ? 0 : roundover;
 439    h = height + support_width;
 440    threadbase_d = neck_d - 0.8 * diamMagMult;
 441
 442    $fn = segs(33 / 2);
 443    thread_h = (thread_od - threadbase_d) / 2;
 444    anchors = [
 445        named_anchor("support-ring", [0, 0, 0 - h / 2])
 446    ];
 447    attachable(anchor, spin, orient, d1 = neck_d, d2 = 0, l = h, anchors = anchors) {
 448        down(h / 2) {
 449            rotate_extrude(convexity = 10) {
 450                polygon(turtle(
 451                    state = [inner_d / 2, 0], (supp_d != neck_d) ? [
 452                        "untilx", supp_d / 2 - ((round_supp) ? support_rad : 0),
 453                        "arcleft", ((round_supp) ? support_rad : 0), 90,
 454                        "untily", support_width - support_rad,
 455                        "arcleft", support_rad, 90,
 456                        "untilx", neck_d / 2,
 457                        "right", 90,
 458                        "untily", h - lip_roundover_r,
 459                        "arcleft", lip_roundover_r, 90,
 460                        "untilx", inner_d / 2
 461                    ] : [
 462                        "untilx", supp_d / 2 - ((round_supp) ? support_rad : 0),
 463                        "arcleft", ((round_supp) ? support_rad : 0), 90,
 464                        "untily", h - lip_roundover_r,
 465                        "arcleft", lip_roundover_r, 90,
 466                        "untilx", inner_d / 2
 467                    ]
 468                ));
 469            }
 470            up(h - pitch / 2 - lip_roundover_r) {
 471                difference() {
 472                    thread_helix(
 473                        d = threadbase_d - 0.1 * diamMagMult,
 474                        pitch = thread_pitch,
 475                        thread_depth = thread_h + 0.1 * diamMagMult,
 476                        flank_angle = flank_angle,
 477                        turns = (height - pitch - lip_roundover_r) * .6167 / pitch,
 478                        taper = -thread_h * 2,
 479                        anchor = TOP
 480                    );
 481                    zrot_copies(rots = [90, 270]) {
 482                        zrot_copies(rots = [-28, 28], r = threadbase_d / 2) {
 483                            prismoid(
 484                                [20 * heightMagMult, 1.82 * diamMagMult],
 485                                [20 * heightMagMult, 1.82 * diamMagMult * .6 + 2 * sin(29) * thread_h],
 486                                h = thread_h + 0.1 * diamMagMult,
 487                                anchor = BOT,
 488                                orient = RIGHT
 489                            );
 490                        }
 491                    }
 492                }
 493            }
 494        }
 495        children();
 496    }
 497}
 498
 499function generic_bottle_neck(
 500    neck_d,
 501    id,
 502    thread_od,
 503    height,
 504    support_d,
 505    pitch,
 506    round_supp,
 507    wall,
 508    anchor, spin, orient
 509) = no_function("generic_bottle_neck");
 510
 511
 512// Module: generic_bottle_cap()
 513// Usage:
 514//   generic_bottle_cap(wall, [texture], ...) [ATTACHMENTS];
 515// Description:
 516//   Creates a basic threaded cap given specifications.
 517// Arguments:
 518//   wall = Wall thickness in mm.
 519//   texture = The surface texture of the cap.  Valid values are "none", "knurled", or "ribbed".  Default: "none"
 520//   ---
 521//   height = Interior height of the cap in mm.
 522//   thread_od = Outer diameter of the threads in mm.
 523//   tolerance = Extra space to add to the outer diameter of threads and neck in mm.  Applied to radius.
 524//   neck_od = Outer diameter of neck in mm.
 525//   flank_angle = Angle of taper on threads.
 526//   pitch = Thread pitch in mm.
 527//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 528//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 529//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 530// Extra Anchors:
 531//   "inside-top" = Centered on the inside top of the cap.
 532// Examples:
 533//   generic_bottle_cap();
 534//   generic_bottle_cap(texture="knurled");
 535//   generic_bottle_cap(texture="ribbed");
 536module generic_bottle_cap(
 537    wall = 2,
 538    texture = "none",
 539    height = 11.2,
 540    thread_od = 28.58,
 541    tolerance = .2,
 542    neck_od = 25.5,
 543    flank_angle = 15,
 544    pitch = 4,
 545    anchor = BOTTOM,
 546    spin = 0,
 547    orient = UP
 548) {
 549    $fn = segs(33 / 2);
 550    threadOuterDTol = thread_od + 2 * tolerance;
 551    w = threadOuterDTol + 2 * wall;
 552    h = height + wall;
 553    neckOuterDTol = neck_od + 2 * tolerance;
 554    threadDepth = (thread_od - neck_od) / 2 + .8;
 555
 556    diamMagMult = (w > 32.58) ? w / 32.58 : 1;
 557    heightMagMult = (height > 11.2) ? height / 11.2 : 1;
 558
 559    anchors = [
 560        named_anchor("inside-top", [0, 0, -(h / 2 - wall)])
 561    ];
 562    attachable(anchor, spin, orient, d = w, l = h, anchors = anchors) {
 563        down(h / 2) {
 564            difference() {
 565                union() {
 566                    // For the knurled and ribbed caps the PCO caps in BOSL2 cut into the wall
 567                    // thickness so the wall+texture are the specified wall thickness.  That
 568                    // seems wrong so this does specified thickness+texture
 569                    if (texture == "knurled") {
 570                        cyl(d=w + 1.5*diamMagMult, l=h, texture="diamonds", tex_size=[3,3], tex_style="concave", anchor=BOT);
 571                    } else if (texture == "ribbed") {
 572                        cyl(d=w + 1.5*diamMagMult, l=h, texture="ribs", tex_size=[3,3], tex_style="min_edge", anchor=BOT);
 573                    } else {
 574                        cyl(d = w, l = h, anchor = BOTTOM);
 575                    }
 576                }
 577                up(wall) cyl(d = threadOuterDTol, h = h, anchor = BOTTOM);
 578            }
 579            difference(){
 580                up(wall + pitch / 2) {
 581                    thread_helix(d = neckOuterDTol, pitch = pitch, thread_depth = threadDepth, flank_angle = flank_angle,
 582                                 turns = ((height - pitch) / pitch), taper = -threadDepth, internal = true, anchor = BOTTOM);
 583                }
 584            }
 585        }
 586        children();
 587    }
 588}
 589
 590function generic_bottle_cap(
 591    wall, texture, height,
 592    thread_od, tolerance,
 593    neck_od, flank_angle, pitch,
 594    anchor, spin, orient
 595) = no_function("generic_bottle_cap");
 596
 597
 598// Module: bottle_adapter_neck_to_cap()
 599// Usage:
 600//   bottle_adapter_neck_to_cap(wall, [texture], ...) [ATTACHMENTS];
 601// Description:
 602//   Creates a threaded neck to cap adapter
 603// Arguments:
 604//   wall = Thickness of wall between neck and cap when d=0.  Leave undefined to have the outside of the tube go from the OD of the neck support ring to the OD of the cap.  Default: undef
 605//   texture = The surface texture of the cap.  Valid values are "none", "knurled", or "ribbed".  Default: "none"
 606//   cap_wall = Wall thickness of the cap in mm.
 607//   cap_h = Interior height of the cap in mm.
 608//   cap_thread_od = Outer diameter of cap threads in mm.
 609//   tolerance = Extra space to add to the outer diameter of threads and neck in mm.  Applied to radius.
 610//   cap_neck_od = Inner diameter of the cap threads.
 611//   cap_neck_id = Inner diameter of the hole through the cap.
 612//   cap_thread_taper = Angle of taper on threads.
 613//   cap_thread_pitch = Thread pitch in mm
 614//   neck_d = Outer diameter of neck w/o threads
 615//   neck_id = Inner diameter of neck
 616//   neck_thread_od = 27.2
 617//   neck_h = Height of neck down to support ring
 618//   neck_thread_pitch = Thread pitch in mm.
 619//   neck_support_od = Outer diameter of neck support ring.  Leave undefined to set equal to OD of cap.  Set to 0 for no ring.  Default: undef
 620//   d = Distance between bottom of neck and top of cap
 621//   taper_lead_in = Length to leave straight before tapering on tube between neck and cap if exists.
 622// Examples:
 623//   bottle_adapter_neck_to_cap();
 624module bottle_adapter_neck_to_cap(
 625    wall,
 626    texture = "none",
 627    cap_wall = 2,
 628    cap_h = 11.2,
 629    cap_thread_od = 28.58,
 630    tolerance = .2,
 631    cap_neck_od = 25.5,
 632    cap_neck_id,
 633    cap_thread_taper = 15,
 634    cap_thread_pitch = 4,
 635    neck_d = 25,
 636    neck_id = 21.4,
 637    neck_thread_od = 27.2,
 638    neck_h = 17,
 639    neck_thread_pitch = 3.2,
 640    neck_support_od,
 641    d = 0,
 642    taper_lead_in = 0
 643) {
 644    neck_support_od = (neck_support_od == undef || (d == 0 && neck_support_od < cap_thread_od + 2 * tolerance)) ? cap_thread_od + 2 * (cap_wall + tolerance) : neck_support_od;
 645    cap_neck_id = (cap_neck_id == undef) ? neck_id : cap_neck_id;
 646    wall = (wall == undef) ? neck_support_od + neck_d + cap_thread_od + neck_id : wall;
 647
 648    $fn = segs(33 / 2);
 649    wallt1 = min(wall, (max(neck_support_od, neck_d) - neck_id) / 2);
 650    wallt2 = min(wall, (cap_thread_od + 2 * (cap_wall + tolerance) - cap_neck_id) / 2);
 651
 652    difference(){
 653        union(){
 654            up(d / 2) {
 655                generic_bottle_neck(neck_d = neck_d,
 656                    id = neck_id,
 657                    thread_od = neck_thread_od,
 658                    height = neck_h,
 659                    support_d = neck_support_od,
 660                    pitch = neck_thread_pitch,
 661                    round_supp = ((wallt1 < (neck_support_od - neck_id) / 2) && (d > 0 || neck_support_od > (cap_thread_od + 2 * (cap_wall + tolerance)))),
 662                    wall = (d > 0) ? wallt1 : min(wallt1, ((cap_thread_od + 2 * (cap_wall + tolerance) - neck_id) / 2))
 663                );
 664            }
 665            if (d != 0) {
 666                rotate_extrude(){
 667                    polygon(points = [
 668                        [0, d / 2],
 669                        [neck_id / 2 + wallt1, d / 2],
 670                        [neck_id / 2 + wallt1, d / 2 - taper_lead_in],
 671                        [cap_neck_id / 2 + wallt2, taper_lead_in - d / 2],
 672                        [cap_neck_id / 2 + wallt2, -d / 2],
 673                        [0, -d / 2]
 674                    ]);
 675                }
 676            }
 677            down(d / 2){
 678                generic_bottle_cap(wall = cap_wall,
 679                    texture = texture,
 680                    height = cap_h,
 681                    thread_od = cap_thread_od,
 682                    tolerance = tolerance,
 683                    neck_od = cap_neck_od,
 684                    flank_angle = cap_thread_taper,
 685                    orient = DOWN,
 686                    pitch = cap_thread_pitch
 687                );
 688            }
 689        }
 690        rotate_extrude() {
 691            polygon(points = [
 692                [0, d / 2 + 0.1],
 693                [neck_id / 2, d / 2],
 694                [neck_id / 2, d / 2 - taper_lead_in],
 695                [cap_neck_id / 2, taper_lead_in - d / 2],
 696                [cap_neck_id / 2, -d / 2 - cap_wall],
 697                [0, -d / 2 - cap_wall - 0.1]
 698            ]);
 699        }
 700    }
 701}
 702
 703function bottle_adapter_neck_to_cap(
 704    wall, texture, cap_wall, cap_h, cap_thread_od,
 705    tolerance, cap_neck_od, cap_neck_id, cap_thread_taper,
 706    cap_thread_pitch, neck_d, neck_id, neck_thread_od,
 707    neck_h, neck_thread_pitch, neck_support_od, d, taper_lead_in
 708) = no_fuction("bottle_adapter_neck_to_cap");
 709
 710
 711// Module: bottle_adapter_cap_to_cap()
 712// Usage:
 713//   bottle_adapter_cap_to_cap(wall, [texture]);
 714// Description:
 715//   Creates a threaded cap to cap adapter.
 716// Arguments:
 717//   wall = Wall thickness in mm.
 718//   texture = The surface texture of the cap.  Valid values are "none", "knurled", or "ribbed".  Default: "none"
 719//   cap_h1 = Interior height of top cap.
 720//   cap_thread_od1 = Outer diameter of threads on top cap.
 721//   tolerance = Extra space to add to the outer diameter of threads and neck in mm.  Applied to radius.
 722//   cap_neck_od1 = Inner diameter of threads on top cap.
 723//   cap_thread_pitch1 = Thread pitch of top cap in mm.
 724//   cap_h2 = Interior height of bottom cap.  Leave undefined to duplicate cap_h1.
 725//   cap_thread_od2 = Outer diameter of threads on bottom cap.  Leave undefined to duplicate capThread1.
 726//   cap_neck_od2 = Inner diameter of threads on top cap.  Leave undefined to duplicate cap_neck_od1.
 727//   cap_thread_pitch2 = Thread pitch of bottom cap in mm.  Leave undefinced to duplicate cap_thread_pitch1.
 728//   d = Distance between caps.
 729//   neck_id1 = Inner diameter of cutout in top cap.
 730//   neck_id2 = Inner diameter of cutout in bottom cap.
 731//   taper_lead_in = Length to leave straight before tapering on tube between caps if exists.
 732// Examples:
 733//   bottle_adapter_cap_to_cap();
 734module bottle_adapter_cap_to_cap(
 735    wall = 2,
 736    texture = "none",
 737    cap_h1 = 11.2,
 738    cap_thread_od1 = 28.58,
 739    tolerance = .2,
 740    cap_neck_od1 = 25.5,
 741    cap_thread_pitch1 = 4,
 742    cap_h2,
 743    cap_thread_od2,
 744    cap_neck_od2,
 745    cap_thread_pitch2,
 746    d = 0,
 747    neck_id1, neck_id2,
 748    taper_lead_in = 0
 749) {
 750    cap_h2 = (cap_h2 == undef) ? cap_h1 : cap_h2;
 751    cap_thread_od2 = (cap_thread_od2 == undef) ? cap_thread_od1 : cap_thread_od2;
 752    cap_neck_od2 = (cap_neck_od2 == undef) ? cap_neck_od1 : cap_neck_od2;
 753    cap_thread_pitch2 = (cap_thread_pitch2 == undef) ? cap_thread_pitch1 : cap_thread_pitch2;
 754    neck_id2 = (neck_id2 == undef && neck_id1 != undef) ? neck_id1 : neck_id2;
 755    taper_lead_in = (d >= taper_lead_in * 2) ? taper_lead_in : d / 2;
 756
 757
 758    $fn = segs(33 / 2);
 759
 760    difference(){
 761        union(){
 762            up(d / 2){
 763                generic_bottle_cap(
 764                    orient = UP,
 765                    wall = wall,
 766                    texture = texture,
 767                    height = cap_h1,
 768                    thread_od = cap_thread_od1,
 769                    tolerance = tolerance,
 770                    neck_od = cap_neck_od1,
 771                    pitch = cap_thread_pitch1
 772                );
 773            }
 774            if (d != 0) {
 775                rotate_extrude() {
 776                    polygon(points = [
 777                        [0, d / 2],
 778                        [cap_thread_od1 / 2 + (wall + tolerance), d / 2],
 779                        [cap_thread_od1 / 2 + (wall + tolerance), d / 2 - taper_lead_in],
 780                        [cap_thread_od2 / 2 + (wall + tolerance), taper_lead_in - d / 2],
 781                        [cap_thread_od2 / 2 + (wall + tolerance), -d / 2],
 782                        [0, -d / 2]
 783                    ]);
 784                }
 785            }
 786            down(d / 2){
 787                generic_bottle_cap(
 788                    orient = DOWN,
 789                    wall = wall,
 790                    texture = texture,
 791                    height = cap_h2,
 792                    thread_od = cap_thread_od2,
 793                    tolerance = tolerance,
 794                    neck_od = cap_neck_od2,
 795                    pitch = cap_thread_pitch2
 796                );
 797            }
 798        }
 799        if (neck_id1 != undef || neck_id2 != undef) {
 800            neck_id1 = (neck_id1 == undef) ? neck_id2 : neck_id1;
 801            neck_id2 = (neck_id2 == undef) ? neck_id1 : neck_id2;
 802
 803            rotate_extrude() {
 804                polygon(points = [
 805                    [0, wall + d / 2 + 0.1],
 806                    [neck_id1 / 2, wall + d / 2],
 807                    [neck_id1 / 2, wall + d / 2 - taper_lead_in],
 808                    [neck_id2 / 2, taper_lead_in - d / 2 - wall],
 809                    [neck_id2 / 2, -d / 2 - wall],
 810                    [0, -d / 2 - wall - 0.1]
 811                ]);
 812            }
 813        }
 814    }
 815}
 816
 817function bottle_adapter_cap_to_cap(
 818    wall, texture, cap_h1, cap_thread_od1, tolerance,
 819    cap_neck_od1, cap_thread_pitch1, cap_h2, cap_thread_od2,
 820    cap_neck_od2, cap_thread_pitch2, d, neck_id1, neck_id2, taper_lead_in
 821) = no_function("bottle_adapter_cap_to_cap");
 822
 823
 824// Module: bottle_adapter_neck_to_neck()
 825// Usage:
 826//   bottle_adapter_neck_to_neck(...);
 827// Description:
 828//   Creates a threaded neck to neck adapter.
 829// Arguments:
 830//   ---
 831//   d = Distance between bottoms of necks
 832//   neck_od1 = Outer diameter of top neck w/o threads
 833//   neck_id1 = Inner diameter of top neck
 834//   thread_od1 = Outer diameter of threads on top neck
 835//   height1 =  Height of top neck above support ring.
 836//   support_od1 = Outer diameter of the support ring on the top neck.  Set to 0 for no ring.
 837//   thread_pitch1 = Thread pitch of top neck.
 838//   neck_od2 = Outer diameter of bottom neck w/o threads.  Leave undefined to duplicate neck_od1
 839//   neck_id2 = Inner diameter of bottom neck.  Leave undefined to duplicate neck_id1
 840//   thread_od2 = Outer diameter of threads on bottom neck.  Leave undefined to duplicate thread_od1
 841//   height2 = Height of bottom neck above support ring.  Leave undefined to duplicate height1
 842//   support_od2 = Outer diameter of the support ring on bottom neck.  Set to 0 for no ring.  Leave undefined to duplicate support_od1 
 843//   pitch2 = Thread pitch of bottom neck.  Leave undefined to duplicate thread_pitch1
 844//   taper_lead_in = Length to leave straight before tapering on tube between necks if exists.
 845//   wall = Thickness of tube wall between necks.  Leave undefined to match outer diameters with the neckODs/supportODs.  
 846// Examples:
 847//   bottle_adapter_neck_to_neck();
 848module bottle_adapter_neck_to_neck(
 849    d = 0,
 850    neck_od1 = 25,
 851    neck_id1 = 21.4,
 852    thread_od1 = 27.2,
 853    height1 = 17,
 854    support_od1 = 33.0,
 855    thread_pitch1 = 3.2,
 856    neck_od2, neck_id2,
 857    thread_od2, height2,
 858    support_od2, pitch2,
 859    taper_lead_in = 0, wall
 860) {
 861    no_children($children);
 862    neck_od2 = (neck_od2 == undef) ? neck_od1 : neck_od2;
 863    neck_id2 = (neck_id2 == undef) ? neck_id1 : neck_id2;
 864    thread_od2 = (thread_od2 == undef) ? thread_od1 : thread_od2;
 865    height2 = (height2 == undef) ? height1 : height2;
 866    support_od2 = (support_od2 == undef) ? support_od1 : support_od2;
 867    pitch2 = (pitch2 == undef) ? thread_pitch1 : pitch2;
 868    wall = (wall == undef) ? support_od1 + support_od2 + neck_id1 + neck_id2 : wall;
 869
 870    supprtOD2 = (d == 0 && support_od2 != 0) ? max(neck_od1, support_od2) : support_od2;
 871    supprtOD1 = (d == 0 && support_od1 != 0) ? max(neck_od2, support_od1) : support_od1;
 872
 873    $fn = segs(33 / 2);
 874    wallt1 = min(wall, (max(supprtOD1, neck_od1) - neck_id1) / 2);
 875    wallt2 = min(wall, (max(supprtOD2, neck_od2) - neck_id2) / 2);
 876
 877    taper_lead_in = (d >= taper_lead_in * 2) ? taper_lead_in : d / 2;
 878
 879    difference(){
 880        union(){
 881            up(d / 2){
 882                generic_bottle_neck(orient = UP,
 883                    neck_d = neck_od1,
 884                    id = neck_id1,
 885                    thread_od = thread_od1,
 886                    height = height1,
 887                    support_d = supprtOD1,
 888                    pitch = thread_pitch1,
 889                    round_supp = ((wallt1 < (supprtOD1 - neck_id1) / 2) || (support_od1 > max(neck_od2, support_od2) && d == 0)),
 890                    wall = (d > 0) ? wallt1 : min(wallt1, ((max(neck_od2, support_od2)) - neck_id1) / 2)
 891                );
 892            }
 893            if (d != 0) {
 894                rotate_extrude() {
 895                    polygon(points = [
 896                        [0, d / 2],
 897                        [neck_id1 / 2 + wallt1, d / 2],
 898                        [neck_id1 / 2 + wallt1, d / 2 - taper_lead_in],
 899                        [neck_id2 / 2 + wallt2, taper_lead_in - d / 2],
 900                        [neck_id2 / 2 + wallt2, -d / 2],
 901                        [0, -d / 2]
 902                    ]);
 903                }
 904            }
 905            down(d / 2){
 906                generic_bottle_neck(orient = DOWN,
 907                    neck_d = neck_od2,
 908                    id = neck_id2,
 909                    thread_od = thread_od2,
 910                    height = height2,
 911                    support_d = supprtOD2,
 912                    pitch = pitch2,
 913                    round_supp = ((wallt2 < (supprtOD2 - neck_id2) / 2) || (support_od2 > max(neck_od1, support_od1) && d == 0)),
 914                    wall = (d > 0) ? wallt2 : min(wallt2, ((max(neck_od1, support_od1)) - neck_id2) / 2)
 915                );
 916            }
 917        }
 918        if (neck_id1 != undef || neck_id2 != undef) {
 919            neck_id1 = (neck_id1 == undef) ? neck_id2 : neck_id1;
 920            neck_id2 = (neck_id2 == undef) ? neck_id1 : neck_id2;
 921
 922            rotate_extrude() {
 923                polygon(points = [
 924                    [0, d / 2],
 925                    [neck_id1 / 2, d / 2],
 926                    [neck_id1 / 2, d / 2 - taper_lead_in],
 927                    [neck_id2 / 2, taper_lead_in - d / 2],
 928                    [neck_id2 / 2, -d / 2],
 929                    [0, -d / 2]
 930                ]);
 931            }
 932        }
 933    }
 934}
 935
 936function bottle_adapter_neck_to_neck(
 937    d, neck_od1, neck_id1, thread_od1, height1,
 938    support_od1, thread_pitch1, neck_od2, neck_id2,
 939    thread_od2, height2, support_od2,
 940    pitch2, taper_lead_in, wall
 941) = no_fuction("bottle_adapter_neck_to_neck");
 942
 943
 944
 945// Section: SPI Bottle Threading
 946
 947
 948// Module: sp_neck()
 949// Usage:
 950//   sp_neck(diam, type, wall|id=, [style=], [bead=]) [ATTACHMENTS];
 951// Description:
 952//   Make a SPI (Society of Plastics Industry) threaded bottle neck.  You must
 953//   supply the nominal outer diameter of the threads and the thread type, one of
 954//   400, 410 and 415.  The 400 type neck has 360 degrees of thread, the 410
 955//   neck has 540 degrees of thread, and the 415 neck has 720 degrees of thread.
 956//   You can also choose between the L style thread, which is symmetric and
 957//   the M style thread, which is an asymmetric buttress thread.  You can
 958//   specify the wall thickness (measured from the base of the threads) or
 959//   the inner diameter, and you can specify an optional bead at the base of the threads.
 960// Arguments:
 961//   diam = nominal outer diameter of threads
 962//   type = thread type, one of 400, 410 and 415
 963//   wall = wall thickness
 964//   ---
 965//   id = inner diameter
 966//   style = Either "L" or "M" to specify the thread style.  Default: "L"
 967//   bead = if true apply a bad to the neck.  Default: false
 968//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 969//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 970//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 971// Examples:
 972//   sp_neck(48,400,2);
 973//   sp_neck(48,400,2,bead=true);
 974//   sp_neck(22,410,2);
 975//   sp_neck(22,410,2,bead=true);
 976//   sp_neck(28,415,id=20,style="M");
 977//   sp_neck(13,415,wall=1,style="M",bead=true);
 978
 979
 980// Thread specs from https://www.isbt.com/threadspecs-downloads.asp
 981
 982//  T = peak to peak diameter (outer diameter)
 983//  I = Inner diameter
 984//  S = space above top thread
 985//  H = total height of neck
 986
 987_sp_specs = [
 988  [400, //diam     T      I      H     S    tpi
 989        [[ 18, [ 17.68,  8.26,  9.42, 0.94, 8]],
 990         [ 20, [ 19.69, 10.26,  9.42, 0.94, 8]],
 991         [ 22, [ 21.69, 12.27,  9.42, 0.94, 8]],
 992         [ 24, [ 23.67, 13.11, 10.16, 1.17, 8]],
 993         [ 28, [ 27.38, 15.60, 10.16, 1.17, 6]],
 994         [ 30, [ 28.37, 16.59, 10.24, 1.17, 6]],
 995         [ 33, [ 31.83, 20.09, 10.24, 1.17, 6]],
 996         [ 35, [ 34.34, 22.23, 10.24, 1.17, 6]],
 997         [ 38, [ 37.19, 25.07, 10.24, 1.17, 6]],
 998         [ 40, [ 39.75, 27.71, 10.24, 1.17, 6]],
 999         [ 43, [ 41.63, 29.59, 10.24, 1.17, 6]],
1000         [ 45, [ 43.82, 31.78, 10.24, 1.17, 6]],
1001         [ 48, [ 47.12, 35.08, 10.24, 1.17, 6]],
1002         [ 51, [ 49.56, 37.57, 10.36, 1.17, 6]],
1003         [ 53, [ 52.07, 40.08, 10.36, 1.17, 6]],
1004         [ 58, [ 56.06, 44.07, 10.36, 1.17, 6]],
1005         [ 60, [ 59.06, 47.07, 10.36, 1.17, 6]],
1006         [ 63, [ 62.08, 50.09, 10.36, 1.17, 6]],
1007         [ 66, [ 65.07, 53.09, 10.36, 1.17, 6]],
1008         [ 70, [ 69.06, 57.07, 10.36, 1.17, 6]],
1009         [ 75, [ 73.56, 61.57, 10.36, 1.17, 6]],
1010         [ 77, [ 76.66, 64.67, 12.37, 1.52, 6]],
1011         [ 83, [ 82.58, 69.93, 12.37, 1.52, 5]],
1012         [ 89, [ 88.75, 74.12, 13.59, 1.52, 5]],
1013         [100, [ 99.57, 84.94, 15.16, 1.52, 5]],
1014         [110, [109.58, 94.92, 15.16, 1.52, 5]],
1015         [120, [119.56,104.93, 17.40, 1.52, 5]],
1016        ]],
1017  [410, //diam     T      I      H     S    tpi  L      W
1018        [[ 18, [ 17.68,  8.26, 13.28, 0.94, 8,  9.17, 2.13]],
1019         [ 20, [ 19.59, 10.26, 14.07, 0.94, 8,  9.17, 2.13]],
1020         [ 22, [ 21.69, 12.27, 14.86, 0.94, 8,  9.55, 2.13]],
1021         [ 24, [ 23.67, 13.11, 16.41, 1.17, 8, 11.10, 2.13]],
1022         [ 28, [ 27.38, 15.60, 17.98, 1.17, 6, 11.76, 2.39]],
1023         ]],
1024  [415, //diam     T      I      H     S    tpi  L      W
1025        [[ 13, [ 12.90,  5.54, 11.48, 0.94,12,  7.77, 1.14]],
1026         [ 15, [ 14.61,  6.55, 14.15, 0.94,12,  8.84, 1.14]],
1027         [ 18, [ 17.68,  8.26, 15.67, 0.94, 8, 10.90, 2.13]],
1028         [ 20, [ 19.69, 10.26, 18.85, 0.94, 8, 11.58, 2.13]],
1029         [ 22, [ 21.69, 12.27, 21.26, 0.94, 8, 13.87, 2.13]],
1030         [ 24, [ 23.67, 13.11, 24.31, 1.17, 8, 14.25, 2.13]],
1031         [ 28, [ 27.38, 15.60, 27.48, 1.17, 6, 16.64, 2.39]],
1032         [ 33, [ 31.83, 20.09, 32.36, 1.17, 6, 19.61, 2.39]],
1033         ]]
1034];
1035
1036_sp_twist = [ [400, 360],
1037              [410, 540],
1038              [415, 720]
1039            ];
1040
1041
1042// profile data: tpi, total width, depth, 
1043_sp_thread_width= [
1044                [5, 3.05],
1045                [6, 2.39],
1046                [8, 2.13],
1047                [12, 1.14],  // But note style M is different
1048               ];
1049
1050
1051function _sp_thread_profile(tpi, a, S, style, flip=false) = 
1052    let(
1053        pitch = 1/tpi*INCH,
1054        cL = a*(1-1/sqrt(3)),
1055        cM = (1-tan(10))*a/2,
1056        // SP specified roundings for the thread profile have special case for tpi=12
1057        roundings = style=="L" && tpi < 12 ? 0.5 
1058                  : style=="M" && tpi < 12 ? [0.25, 0.25, 0.75, 0.75]
1059                  : style=="L" ? [0.38, 0.13, 0.13, 0.38]
1060                  : /* style=="M" */  [0.25, 0.25, 0.2, 0.5],
1061        path1 = style=="L"
1062                  ? round_corners([[-1/2*pitch,-a/2],
1063                                   [-a/2,-a/2],
1064                                   [-cL/2,0],
1065                                   [cL/2,0],
1066                                   [a/2,-a/2],
1067                                   [1/2*pitch,-a/2]], radius=roundings, closed=false,$fn=24)
1068                  : round_corners(
1069                       [[-1/2*pitch,-a/2],
1070                                   [-a/2, -a/2],
1071                                   [-cM, 0],
1072                                   [0,0],
1073                                   [a/2,-a/2],
1074                                   [1/2*pitch,-a/2]], radius=roundings, closed=false, $fn=24),
1075        path2 = flip ? reverse(xflip(path1)) : path1
1076   )
1077   // Shift so that the profile is S mm from the right end to create proper length S top gap
1078   select(right(-a/2+1/2-S,p=path2),1,-2)/pitch;
1079
1080
1081function sp_neck(diam,type,wall,id,style="L",bead=false, anchor, spin, orient) = no_function("sp_neck");
1082module sp_neck(diam,type,wall,id,style="L",bead=false, anchor, spin, orient)
1083{
1084    assert(num_defined([wall,id])==1, "Must define exactly one of wall and id");
1085    
1086    table = struct_val(_sp_specs,type);
1087    dum1=assert(is_def(table),"Unknown SP closure type.  Type must be one of 400, 410, or 415");
1088    entry = struct_val(table, diam);
1089    dum2=assert(is_def(entry), str("Unknown closure nominal diameter.  Allowed diameters for SP",type,": ",struct_keys(table)))
1090         assert(style=="L" || style=="M", "style must be \"L\" or \"M\"");
1091
1092    T = entry[0];
1093    I = entry[1];
1094    H = entry[2];
1095    S = entry[3];
1096    tpi = entry[4];
1097    a = (style=="M" && tpi==12) ? 1.3 : struct_val(_sp_thread_width,tpi);
1098
1099    twist = struct_val(_sp_twist, type);
1100
1101    profile = _sp_thread_profile(tpi,a,S,style);
1102
1103    depth = a/2;
1104    taperlen = 2*a;
1105
1106    beadmax = type==400 ? (T/2-depth)+depth*1.25
1107            : diam <=15 ? (T-.15)/2 : (T-.05)/2;
1108    
1109    W = type==400 ? a*1.5      // arbitrary decision for type 400
1110                  : entry[6];  // specified width for 410 and 415
1111
1112    beadpts = [
1113                [0,-W/2],
1114                each arc(16, points = [[T/2-depth, -W/2],
1115                                       [beadmax, 0],
1116                                       [T/2-depth, W/2]]),
1117                [0,W/2]
1118              ];
1119
1120    isect400 = [for(seg=pair(beadpts)) let(segisect = line_intersection([[T/2,0],[T/2,1]] , seg, LINE, SEGMENT)) if (is_def(segisect)) segisect.y];
1121
1122    extra_bot = type==400 && bead ? -min(column(beadpts,1))+max(isect400) : 0;
1123    bead_shift = type==400 ? H+max(isect400) : entry[5]+W/2;  // entry[5] is L
1124    
1125    attachable(anchor,spin,orient,r=bead ? beadmax : T/2, l=H+extra_bot){
1126        up((H+extra_bot)/2){
1127            difference(){
1128                union(){
1129                    thread_helix(d=T-.01, profile=profile, pitch = INCH/tpi, turns=twist/360, taper=taperlen, anchor=TOP);
1130                    cylinder(d=T-depth*2,l=H,anchor=TOP);
1131                    if (bead)
1132                      down(bead_shift)
1133                         rotate_extrude()
1134                            polygon(beadpts);
1135                }
1136                up(.5)cyl(d=is_def(id) ? id : T-a-2*wall, l=H-extra_bot+1, anchor=TOP);
1137            }
1138        }
1139        children();
1140    }
1141}
1142
1143
1144
1145// Module: sp_cap()
1146// Usage:
1147//   sp_cap(diam, type, wall, [style=], [top_adj=], [bot_adj=], [texture=], [$slop]) [ATTACHMENTS];
1148// Description:
1149//   Make a SPI (Society of Plastics Industry) threaded bottle neck.  You must
1150//   supply the nominal outer diameter of the threads and the thread type, one of
1151//   400, 410 and 415.  The 400 type neck has 360 degrees of thread, the 410
1152//   neck has 540 degrees of thread, and the 415 neck has 720 degrees of thread.
1153//   You can also choose between the L style thread, which is symmetric and
1154//   the M style thread, which is an asymmetric buttress thread.  Note that it
1155//   is OK to mix styles, so you can put an L-style cap onto an M-style neck.  
1156//   .
1157//   These caps often contain a cardboard or foam sealer disk, which can be as much as 1mm thick.
1158//   If you don't include this, your cap may bottom out on the bead on the neck instead of sealing
1159//   against the top.  If you set top_adj to 1 it will make the top space 1mm smaller so that the
1160//   cap will not bottom out.  The 410 and 415 caps have very long unthreaded sections at the bottom.
1161//   The bot_adj parameter specifies an amount to reduce that bottom extension.  Be careful that
1162//   you don't shrink past the threads.
1163//   .
1164//   The inner diameter of the cap is set to allow 10% of the thread depth in clearance.  The diameter
1165//   is further increased by `2 * $slop` so you can increase clearance if necessary. 
1166//   .
1167//   Note: there is a published SPI standard for necks, but absolutely nothing for caps.  This
1168//   cap module was designed based on the neck standard to mate reasonably well, but if you
1169//   find ways that it does the wrong thing, file a report.  
1170// Arguments:
1171//   diam = nominal outer diameter of threads
1172//   type = thread type, one of 400, 410 and 415
1173//   wall = wall thickness
1174//   ---
1175//   style = Either "L" or "M" to specify the thread style.  Default: "L"
1176//   top_adj = Amount to reduce top space in the cap, which means it doesn't screw down as far.  Default: 0
1177//   bot_adj = Amount to reduce extension of cap at the bottom, which also means it doesn't screw down as far.  Default: 0
1178//   texture = texture for outside of cap, one of "knurled", "ribbed" or "none.  Default: "none"
1179//   $slop = Increase inner diameter by `2 * $slop`.  
1180//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
1181//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
1182//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
1183// Examples:
1184//   sp_cap(48,400,2);
1185//   sp_cap(22,410,2);
1186//   sp_cap(28,415,1.5,style="M");
1187module sp_cap(diam,type,wall,style="L",top_adj=0, bot_adj=0, texture="none", anchor, spin, orient)
1188{
1189    table = struct_val(_sp_specs,type);
1190    dum1=assert(is_def(table),"Unknown SP closure type.  Type must be one of 400, 410, or 415");
1191    entry = struct_val(table, diam);
1192    dum2=assert(is_def(entry), str("Unknown closure nominal diameter.  Allowed diameters for SP",type,": ",struct_keys(table)))
1193         assert(style=="L" || style=="M", "style must be \"L\" or \"M\"");
1194
1195    T = entry[0];
1196    I = entry[1];
1197    H = entry[2]-1;
1198    S = entry[3];
1199    tpi = entry[4];
1200    a = (style=="M" && tpi==12) ? 1.3 : struct_val(_sp_thread_width,tpi);
1201
1202    twist = struct_val(_sp_twist, type);
1203
1204    dum3=assert(top_adj<S+0.75*a, str("The top_adj value is too large so the thread won't fit.  It must be smaller than ",S+0.75*a));
1205    oprofile = _sp_thread_profile(tpi,a,S+0.75*a-top_adj,style,flip=true);
1206    bounds=pointlist_bounds(oprofile);
1207    profile = fwd(-bounds[0].y,yflip(oprofile));
1208
1209    depth = a/2;
1210    taperlen = 2*a;
1211    assert(in_list(texture, ["none","knurled","ribbed"]));
1212    space=2*depth/10+2*get_slop();
1213    attachable(anchor,spin,orient,r= (T+space)/2+wall, l=H-bot_adj+wall){
1214        xrot(180)
1215        up((H-bot_adj)/2-wall/2){
1216            difference(){
1217                up(wall){
1218                   if (texture=="knurled")
1219                        cyl(d=T+space+2*wall,l=H+wall-bot_adj,anchor=TOP,texture="trunc_pyramids", tex_size=[3,3], tex_style="convex");
1220                   else if (texture == "ribbed") 
1221                        cyl(d=T+space+2*wall,l=H+wall-bot_adj,anchor=TOP,chamfer2=.8,tex_taper=0,texture="trunc_ribs", tex_size=[3,3], tex_style="min_edge");
1222                   else
1223                        cyl(d=T+space+2*wall,l=H+wall-bot_adj,anchor=TOP,chamfer2=.8);
1224                }
1225                cyl(d=T+space, l=H-bot_adj+1, anchor=TOP);
1226            }
1227            thread_helix(d=T+space-.01, profile=profile, pitch = INCH/tpi, turns=twist/360, taper=taperlen, anchor=TOP, internal=true);
1228        }
1229        children();
1230    }
1231}
1232
1233
1234
1235// Function: sp_diameter()
1236// Usage:
1237//   true_diam = sp_diameter(diam,type)
1238// Description:
1239//   Returns the actual base diameter (root of the threads) for a SPI plastic bottle neck given the nominal diameter and type number (400, 410, 415). 
1240// Arguments:
1241//   diam = nominal diameter
1242//   type = closure type number (400, 410 or 415)
1243function sp_diameter(diam,type) =
1244  let(
1245      table = struct_val(_sp_specs,type)
1246  )
1247  assert(is_def(table),"Unknown SP closure type.  Type must be one of 400, 410, or 415")
1248  let(
1249      entry = struct_val(table, diam)
1250  )
1251  assert(is_def(entry), str("Unknown closure nominal diameter.  Allowed diameters for SP",type,": ",struct_keys(table)))
1252  entry[0];
1253
1254
1255
1256// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap