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