BelfrySCAD / BOSL2

The Belfry OpenScad Library, v2.0. An OpenSCAD library of shapes, masks, and manipulators to make working with OpenSCAD easier. BETA
https://github.com/BelfrySCAD/BOSL2/wiki
BSD 2-Clause "Simplified" License
893 stars 109 forks source link

nut catches? #85

Closed adrianVmariano closed 1 year ago

adrianVmariano commented 5 years ago

https://github.com/JohK/nutsnbolts

(Iis this already around somewhere?)

revarbat commented 5 years ago

I've been making nut catches each time, and not even thinking about it. This is sensible to add.

revarbat commented 5 years ago

This may have a dependency on #37 so that nut size specification is consistent.

adrianVmariano commented 5 years ago

Agreed about the dependency. I never did a table of nut thicknesses...but nut widths are certainly there.

rjcarlson49 commented 2 years ago

I've put together a personal library for creating screw holes (masks) that include nut traps of various kinds. It uses the screws module from BOSL2 extensively. The code includes a very limited table of nut dimensions for the screws I use regularly. A key part of the code positions the screw hole in various ways to make it easier to move into position in the assembly.

Here it is:

include <BOSL2/std.scad>
include <BOSL2/screws.scad>

$fn = $preview ? 30 : 90;
_tiny = 1/128;

/*
The default tolerance is a measured amount. Vertical dimensions
in printed holes require this much extra so that a nut will always 
fit. A much smaller tolerance can work if the hole's cross section 
is in the XY plane. However, the .25 value works in the worst case.
*/ 
defaultTolerance = .25; 

/*************************************************************
* Screws and holes are rendered vertically with head at the top
* Slots for nut traps are in the direction of +X

* extendH extend the hole at the top of the head
* extendS extendthe hole at the bottom of the crew
* gap is the space between head and nut

* pos determines alignment with the XY plane, ie Z == 0
*    0 - Top of head end
*    1 - Screw head seat - the head side of the gap
*    2 - center of gap, ie half way between head and nut
*    3 - nut side of the gap 
*    4 - 1 mm beyond screw end
*    5 - Bottom
*
*    "top" - Top of the hole above the head
*    "head" - Screw head seat - the head side of the gap
*    "gap" - center of gap, ie half way between head and nut
*    "nut" - nut side of the gap 
*    "screw" - 1 mm beyond screw end
*    "bot" - Bottom of the screw hole
*
* nut - "none", "hex", "square" or "self", default: "hex"
* trap - "none", "side" or "inline", default: "inline"
***************************************************************/

/*
screw_context 

Creates a structure that includes all the needed parameters to define 
a screw and the holes necessary for it and its nut, if any. The nut 
information is added automatically.

The data structure allows many calls without the overhead of
rebuilding the set of parameters each time.

The screw can be described in one of three ways, in order of precedence
    specify spec (call screw_info to get the spec)
    specify name (as defined by BOSL2, eg "M5,16", metric 5mm diameter, 16 mm long)
    specify d and l, this assumes a name "Md,l"

    For the second and third options, drive and head can also be specified. 
    These will be passed to screw_info to retrieve the spec

name   - BOSL2 string name of the screw, "Md,l" - may replace D and L
d      - diameter of the screw in mm
l      - length of the screw in mm
head   - string head type as defined in BOSL2, defaults to "socket"
drive  - string drive type as defined in BOSL2, defaults to "hex"
spec   - the structure returned by screw_info
pos    - position of screw relative to the XY plane, screw is assumed to be on Z axis
gap    - the distance in mm between head and nut in the assembly
nut    - "hex" (default), "square", "self", "none"
trap   - "none" (default), "inline", "side"
extendH - clearance above the head, ie additional length of the hole for the head
extendS - clearance below the screw, ie additional length of the hole for the screw
trapL  - trap length for a side trap, defaults to 25
pokeL  - poke hole length - a hole opposite the nut trap to allow the nut to be poked out
         default is -1 : no poke hole
         > 0 means include a poke hole
         1 means use the default length of 25
pokeD  - diameter of poke hole, defaults to 2

t      - tolerance added to diameters so screws will fit in holes, default is "defaultTolerance"
*/

function screw_context(
    d = 0, 
    l = 0,
    name = 0,
    head = "socket",
    drive = "hex",
    spec = 0,
    pos = "gap", 
    gap = 4,
    nut = "hex",
    trap = "none", 
    extendH = 1, 
    extendS = 2, 
    pokeL = -1,
    pokeD = 2,
    trapL = 25,
    t = defaultTolerance) =

    let(
        nameFromSpec = (is_list(spec)) ? str("M", _SV1(spec, "diameter"), ",", _SV1(spec, "length")) : undef,
        nameS = (is_list(spec)) ? nameFromSpec : (is_string(name)) ? name : str("M", d, ",", l),
        info = (is_list(spec)) ? spec : screw_info(nameS, head = head, drive = drive),
        xyzzy = assert(nameS != "M0, 0")
    )
    [["pos", pos],                // Screw is returned head up in Z, pos determines the vertical position
     ["gap", gap],                // Gap is the length from the bottom of the head to the top of the nut
     ["nut", nut],                // Gap is the length from the bottom of the head to the top of the nut
     ["trap", trap],              // Gap is the length from the bottom of the head to the top of the nut
     ["extendh", extendH],        // The additional length of the hole above the head
     ["extends", extendS],        // The additional length of the hole below the screw
     ["pokel", pokeL],            // Poke hole length
     ["poked", pokeD],            // Poke hole diameter
     ["trapl", trapL],            // Trap hole length
     ["t", t],                    // Tolerance, defaults to defaultTolerance
     ["screwname", nameS],        // screw name string
     ["spec", info],              // technical description of the screw
     ["nutSpec", _NutInfo(info)] // technical description of the nut
     ];

/*
screw_hole(sc);
    sc - screw context

    Creates a screw hole includes any nut trap (including "poke hole").
    It is vertically positioned as designated in the context.
    If the trap is a side trap, it extends in the direction of the x axis.
    ANy poke hole is opposite the side trap.

screw_from_context(sc);
    sc - screw context

nut_from_context(sc);
    sc - screw context
*/

/*

Nut Info information

diameter - distance face to face
height   - thickness

Hex and square nuts appear to have the same specs
This table only covers metric nuts for screws from 3 to 10 mm
*/

function _NutInfo(sc) = let (d = struct_val(sc, "diameter", 0))
           (d == 3)  ? [["diameter", 5.5],  ["height", 2.4]] : 
           (d == 4)  ? [["diameter", 7.0],  ["height", 3.2]] :
           (d == 5)  ? [["diameter", 8.0],  ["height", 4.0]] :
           (d == 6)  ? [["diameter", 10.0], ["height", 5.0]] :
           (d == 7)  ? [["diameter", 11.0], ["height", 5.5]] :
           (d == 8)  ? [["diameter", 13.0], ["height", 6.5]] :
           (d == 10) ? [["diameter", 17.0], ["height", 8.0]] : [];

/*
Functions for accessing the screw_context.
*/
function _SV1(s, n1, d = 0) = struct_val(s, n1, d);
function _SV2(s, n1, n2, d = 0) = len(struct_val(s, "spec", [])) == 0 
                                ? struct_val(s, n2, d)
                                : struct_val(struct_val(s, n1, ""), n2, d);

function _ScrewD(s)  = _SV2(s, "spec",    "diameter", 0);
function _ScrewL(s)  = _SV2(s, "spec",    "length", 0);
function _ScrewP(s)  = _SV2(s, "spec",    "pitch", 0);
function _HeadHV(s)  = _SV2(s, "spec",    "head_height", 0);
function _HeadD(s)   = _SV2(s, "spec",    "head_size", 0);
function _Head(s)    = _SV2(s, "spec",    "head", 0);
function _Drive(s)   = _SV2(s, "spec",    "drive", 0);
function _HeadA(s)   = _SV2(s, "spec",    "head_angle", 0);
function _NutD(s)    = _SV2(s, "nutSpec", "diameter", 0);
function _NutH(s)    = _SV2(s, "nutSpec", "height", 0);

function _Info(s)    = _SV1(s, "spec",   []);
function _Spec(s)    = _SV1(s, "spec",   []);
function _Pos(s)     = _SV1(s, "pos",    0);
function _Nut(s)     = _SV1(s, "nut",    0);
function _Trap(s)    = _SV1(s, "trap",   0);
function _Gap(s)     = _SV1(s, "gap",    0);
function _ExtendH(s) = _SV1(s, "extendh", 0);
function _ExtendS(s) = _SV1(s, "extends", 0);
function _PokeL(s)   = _SV1(s, "pokel",  0);
function _PokeD(s)   = _SV1(s, "poked",  0);
function _TrapL(s)   = _SV1(s, "trapl",  0);
function _T(s)       = _SV1(s, "t",      0);
function _ScrewName(s) = _SV1(s, "screwname", "");

/*
    The head height value for flat screws in screw_info is 0.
    For our purposes though, the head height is the distance
    from head top surface to beginning of threads.
*/
function _HeadH(sc) = (_Head(sc) == "flat") 
                      ? (_HeadD(sc) - _ScrewD(sc)) / 2 / tan(_HeadA(sc) / 2)
                      : _HeadHV(sc);

function _gap_adjustment(sc) = 
    let (
        pos = _pos_n(_Pos(sc)),
        gap = _Gap(sc),
        headLen = _HeadH(sc) + _ExtendH(sc),
        nutLen = _ScrewL(sc) - gap + _ExtendS(sc),
        xyzzy = echo(headLen)
        )

    (pos <= 0) ? 0 : 
    (pos == 1) ? headLen :
    (pos == 2) ? headLen + gap/2 :
    (pos == 3) ? headLen + gap :
    (pos == 4) ? headLen + _ScrewL(sc):
                 headLen + gap + nutLen;

module screw_hole(sc) {
    headLen = _HeadH(sc) + _ExtendH(sc);
    screwHoleLen = _ScrewL(sc) + _ExtendS(sc);
    nutLen = _ScrewL(sc) - _Gap(sc) + _ExtendS(sc);
    //struct_echo(sc, "screw_hole");

    if (_Nut(sc) == "self") {
        _self_tap(sc);
    } else {
        _screw_hole(sc);
    }

    if (_Trap(sc) == "side") {
        _side_trap(sc);
    } else if (_Trap(sc) == "inline") {
        _inline_trap(sc);
    }
} // screw_hole

module screw_from_context(sc) {
    up(_gap_adjustment(sc) - _ExtendH(sc))
    down(_ScrewL(sc) + _HeadHV(sc)) 
    screw(_ScrewName(sc), head = _Head(sc), drive = _Drive(sc));
} // screw_from_context

module nut_from_context(sc) {
    up(_gap_adjustment(sc))
    down(_ExtendH(sc))
    down(_HeadH(sc)) 
    down(_Gap(sc)) 
    down(_NutH(sc)) 
    if (_Nut(sc) == "square") {
        difference() {
            cube([_NutD(sc), _NutD(sc), _NutH(sc)], anchor = BOT);

            // Remove
            down(.1)
            #screw(_ScrewName(sc), head = _Head(sc), drive = _Drive(sc));
        }
    } else if (_Nut(sc) == "hex") {
        nut(diameter = _NutD(sc), thickness = _NutH(sc), spec = _Info(sc));
    }
} // nut_from_context

module _screw_hole(sc) {
    headLen = _HeadHV(sc) + _ExtendH(sc);
    screwHoleLen = _ScrewL(sc) + _ExtendS(sc);
    nutLen = _ScrewL(sc) - _Gap(sc) + _ExtendS(sc);
    //struct_echo(sc, "screw_hole");

    up(_gap_adjustment(sc))
    union() {
        // Space for screw head
        up(_tiny)
        zcyl(h = headLen + _tiny*2, d = _HeadD(sc) + _T(sc), anchor = TOP);

        // Space between head and nut
        down(headLen)
        up(_tiny)
        zcyl(h = screwHoleLen + _tiny*2, d = _ScrewD(sc) + _T(sc), anchor = TOP);

        down(headLen)
        if (_Head(sc) == "flat") _FlatHead(sc);
    }
} // _screw_hole

module _FlatHead(sc) {
    head_size = _HeadD(sc) + _T(sc);
    head_height = _HeadH(sc);
    angle = _HeadA(sc) / 2;
    full_height = head_size / 2 / tan(angle);
    height = head_height > 0 ? head_height : full_height;
    d2 = head_size*(1-height/full_height);
    zflip()
    cyl(d1 = head_size, d2 = d2, l = height, anchor = BOTTOM);
} // _FlatHead

module _self_tap(sc) {
    headLen = _HeadH(sc) + _ExtendH(sc);
    holeLen = _Gap(sc) + 1; // 1 mm entry hole
    tapLen  = _ScrewL(sc) + _ExtendS(sc) - holeLen;

    up(_gap_adjustment(sc))
    union() {
        // Space for screw head
        up(.1)
        zcyl(h = headLen + .1, d = _HeadD(sc) + _T(sc), anchor = TOP);

        // Space between head and nut (self tapping surface)
        // self tap hole starts 1mm inside the surface
        down(headLen)
        up(.1)
        zcyl(h = holeLen + .1, d = _ScrewD(sc) + _T(sc), anchor = TOP);

        down(headLen + holeLen)
        up(.1)
        zcyl(h = tapLen + .1, d = _ScrewD(sc) - _ScrewP(sc), anchor = TOP);
    }
} // _self_tap

module _side_trap(sc, slotLen = 15) {
    headLen = _HeadH(sc) + _ExtendH(sc);
    screwHoleLen = _ScrewL(sc) + _ExtendS(sc);
    nutLen = _ScrewL(sc) - _Gap(sc) + _ExtendS(sc);

    up(_gap_adjustment(sc))
    union() {
        // Slot
        down(headLen + _Gap(sc))
        up(_T(sc)/2)
        cube([_TrapL(sc), _NutD(sc) + _T(sc), _NutH(sc) + _T(sc)], anchor = [-1, 0, 1]);

        if (_Nut(sc) == "square") {
            down(headLen + _Gap(sc))
            up(_T(sc)/2)
            cube([_NutD(sc) + _T(sc), _NutD(sc) + _T(sc), _NutH(sc) + _T(sc)], anchor = [0, 0, 1]);
        } else {
            down(headLen + _Gap(sc) + _NutH(sc))
            down(_T(sc)/2)
            _hexCyl(h = _NutH(sc) + _T(sc), id = _NutD(sc) + _T(sc)); 
        }

        // Poke hole
        if (_PokeL(sc) > 0) {
            down(headLen + _Gap(sc))
            down(_NutH(sc)/2)
            xcyl(h = (_PokeL(sc) == 1 ? 25 : _PokeL(sc)), d = _PokeD(sc), anchor = [1, 0, 0]);
        }
    }
} // _side_trap

module _inline_trap(sc) {
    headLen = _HeadH(sc) + _ExtendH(sc);
    screwHoleLen = _ScrewL(sc) + _ExtendS(sc);
    nutLen = _ScrewL(sc) - _Gap(sc) + _ExtendS(sc);

    if (_Nut(sc) == "square") {
        up(_gap_adjustment(sc))
        down(headLen + screwHoleLen)
        cube([_NutD(sc) + _T(sc), _NutD(sc) + _T(sc), nutLen + .1], anchor = [0, 0, -1]);
        //_hexCyl(l = nutLen + .1, id = _NutD(sc) + _T(sc));
    } else {
        up(_gap_adjustment(sc))
        down(headLen + screwHoleLen)
        _hexCyl(l = nutLen + .1, id = _NutD(sc) + _T(sc));
    }
} // _inline_trap

function _pos_n(p) = 
    (!is_string(p)) ? p :
    (p == "top")    ? 0 :
    (p == "head")   ? 1 :
    (p == "gap")    ? 2 :
    (p == "nut")    ? 3 :
    (p == "screw")  ? 4 :
    (p == "bot")    ? 5 : undef;

module _hexCyl(l = -1, h = -1, id = -1, d = -1) {
    H = (h > 0) ? h : l;
    ID = (id > 0) ? id : d;

    linear_extrude(H)
    hexagon(id = ID);
} // _hexCyl

/***************************************************************
*
* Test Code
*
***************************************************************/

_test();

module _test() {
    showPosition = "all";
    trapAngle = 135;
    siName = "M5,16";
    //siHead = "socket";
    siHead = "flat";
    //siHead = "button";
    siDrive = "hex";

    scGap = 4;
    scNut = "hex"; // "hex, "square", "self"
    scTrap = "side"; // "inline", "side", none
    scPoke = 10; // -1, 1
    scTrapL = 25;

    scExtendH = 1;
    scExtendS = 3;

    si = screw_info(name = siName, head = siHead, drive = siDrive);

    sc0 = screw_context(spec = si, pos = 0, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);
    sc1 = screw_context(spec = si, pos = 1, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);
    sc2 = screw_context(spec = si, pos = 2, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);
    sc3 = screw_context(spec = si, pos = 3, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);
    sc4 = screw_context(spec = si, pos = 4, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);
    sc5 = screw_context(spec = si, pos = 5, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);

    echo(_HeadH(sc0));

    xdistribute(20) {
        ydistribute(20) {
            zrot(trapAngle) screw_hole(sc0);
            if (showPosition == "all") zrot(trapAngle) screw_hole(sc1);
            if (showPosition == "all") zrot(trapAngle) screw_hole(sc2);
            if (showPosition == "all") zrot(trapAngle) screw_hole(sc3);
            if (showPosition == "all") zrot(trapAngle) screw_hole(sc4);
            if (showPosition == "all") zrot(trapAngle) screw_hole(sc5);
        }

        ydistribute(20) {
            zrot(trapAngle) union() {screw_from_context(sc0); nut_from_context(sc0);}
            if (showPosition == "all") zrot(trapAngle) union() {screw_from_context(sc1); nut_from_context(sc1);}
            if (showPosition == "all") zrot(trapAngle) union() {screw_from_context(sc2); nut_from_context(sc2);}
            if (showPosition == "all") zrot(trapAngle) union() {screw_from_context(sc3); nut_from_context(sc3);}
            if (showPosition == "all") zrot(trapAngle) union() {screw_from_context(sc4); nut_from_context(sc4);}
            if (showPosition == "all") zrot(trapAngle) union() {screw_from_context(sc5); nut_from_context(sc5);}
        }
    }
}
rjcarlson49 commented 2 years ago

This version is fully attachable I believe. I also makes use of attachments. The anchors defined are described a little ways down in the code.

//   use <../!lib/utils.0.scad>
include <BOSL2/std.scad>
include <BOSL2/screws.scad>

$fn = $preview ? 30 : 90;
_tiny = 1/128;

/*
The default tolerance is a measured amount. Vertical dimensions
in printed holes require this much extra so that a nut will always 
fit. A much smaller tolerance can work if the hole's cross section 
is in the XY plane. However, the .25 value works in the worst case.
*/ 
defaultTolerance = .25; 

/*************************************************************
Screws and holes are rendered vertically with head at the top
Slots for nut traps are in the direction of +X

extendH extend the hole at the top of the head
extendS extendthe hole at the bottom of the crew
gap is the space between head and nut

The screw hole is generated vertically.
If there is a side trap, it extends toward the +X axis.

nut - "none", "hex", "square" or "self", default: "hex"
trap - "none", "side" or "inline", default: "inline"

Several special anchors are defined. All these anchors
are on the Z axis. The designated anchor will be at the XYZ origin.

   TOP     - At the top of the screw hole
   "top"   - At the top of the screw head
   "head"  - At the bottom of the screw head
   "gap"   - Center of gap, ie half way between head and nut
   "nut"   - Nut side of the gap 
   "screw" - At the screw end
   "bot"   - Bottom of the screw
   BOT     - Bottom of the screw hole

   spin can be used to rotate a side trap to the desired direction.
   orient can be used to orient the screw hole as desired, 
   UP (default), DOWN, LEFT, RIGHT, etc.
***************************************************************/

/*************************************************************
screw_context 

Creates a structure that includes all the needed parameters to define 
a screw and the holes necessary for it and its nut, if any. The nut 
information is added automatically.

The data structure allows many calls without the overhead of
rebuilding the set of parameters each time.

The screw can be described in one of three ways, in order of precedence
    specify spec (call screw_info to get the spec)
    specify name (as defined by BOSL2, eg "M5,16", metrix 5mm diameter, 16 mm long)
    specify d and l, this assumes a name "Md,l"

    For the second and third options, drive and head can also be specified. 
    These will be passed to screw_info to retrieve the spec

name   - BOSL2 string name of the screw, "Md,l" - may replace D and L
d      - diameter of the screw in mm
l      - length of the screw in mm
head   - string head type as defined in BOSL2, defaults to "socket"
drive  - string drive type as defined in BOSL2, defaults to "hex"
spec   - the structure returned by screw_info
gap    - the distance in mm between head and nut in the assembly
nut    - "hex" (default), "square", "self", "none"
trap   - "none" (default), "inline", "side"
extendH - clearance above the head, ie additional length of the hole for the head
extendS - clearance below the screw, ie additional length of the hole for the screw
trapL  - trap length for a side trap, defaults to 25
pokeL  - poke hole length - a hole opposite the nut trap to allow the nut to be poked out
         default is -1 : no poke hole
         > 0 means include a poke hole
         1 means use the default length of 25
pokeD  - diameter of poke hole, defaults to 2

t      - tolerance added to diameters so screws will fit in holes, default is "defaultTolerance"
*************************************************************/

function screw_context(
    d = 0, 
    l = 0,
    name = 0,
    head = "socket",
    drive = "hex",
    spec = 0,
    gap = 4,
    nut = "hex",
    trap = "none", 
    extendH = 1, 
    extendS = 1, 
    pokeL = -1,
    pokeD = 2,
    trapL = 25,
    t = defaultTolerance) =

    let(
        nameFromSpec = (is_list(spec)) ? str("M", _SV1(spec, "diameter"), ",", _SV1(spec, "length")) : undef,
        nameS = (is_list(spec)) ? nameFromSpec : (is_string(name)) ? name : str("M", d, ",", l),
        info = (is_list(spec)) ? spec : screw_info(nameS, head = head, drive = drive),
        xyzzy = assert(nameS != "M0, 0")
    )
    [["gap", gap],                // Gap is the length from the bottom of the head to the top of the nut
     ["nut", nut],                // Gap is the length from the bottom of the head to the top of the nut
     ["trap", trap],              // Gap is the length from the bottom of the head to the top of the nut
     ["extendh", extendH],        // The additional length of the hole above the head
     ["extends", extendS],        // The additional length of the hole below the screw
     ["pokel", pokeL],            // Poke hole length
     ["poked", pokeD],            // Poke hole diameter
     ["trapl", trapL],            // Trap hole length
     ["t", t],                    // Tolerance, defaults to defaultTolerance
     ["screwname", nameS],        // screw name string
     ["spec", info],              // technical description of the screw
     ["nutSpec", _NutInfo(info)] // technical description of the nut
     ];

/*
screw_hole(sc);
    sc - screw context

    Creates a screw hole includes any nut trap (including "poke hole").
    It is vertically positioned as designated in the context.
    If the trap is a side trap, it extends in the direction of the x axis.
    ANy poke hole is opposite the side trap.

screw_from_context(sc);
    sc - screw context

nut_from_context(sc);
    sc - screw context
*/

/*

Nut Info information

diameter - distance face to face
height   - thickness

Hex and square nuts appear to have the same specs
This table only covers metric nuts for screws from 3 to 10 mm
*/

function _NutInfo(sc) = let (d = struct_val(sc, "diameter", 0))
           (d == 3)  ? [["diameter", 5.5],  ["height", 2.4]] : 
           (d == 4)  ? [["diameter", 7.0],  ["height", 3.2]] :
           (d == 5)  ? [["diameter", 8.0],  ["height", 4.0]] :
           (d == 6)  ? [["diameter", 10.0], ["height", 5.0]] :
           (d == 7)  ? [["diameter", 11.0], ["height", 5.5]] :
           (d == 8)  ? [["diameter", 13.0], ["height", 6.5]] :
           (d == 10) ? [["diameter", 17.0], ["height", 8.0]] : [];

/*
Functions for accessing the screw_context.
*/
function _SV1(s, n1, d = 0) = struct_val(s, n1, d);
function _SV2(s, n1, n2, d = 0) = len(struct_val(s, "spec", [])) == 0 
                                ? struct_val(s, n2, d)
                                : struct_val(struct_val(s, n1, ""), n2, d);

function _ScrewD(s)    = _SV2(s, "spec",    "diameter", 0);
function _ScrewL(s)    = _SV2(s, "spec",    "length", 0);
function _ScrewP(s)    = _SV2(s, "spec",    "pitch", 0);
function _HeadHV(s)    = _SV2(s, "spec",    "head_height", 0);
function _HeadDV(s)    = _SV2(s, "spec",    "head_size", 0);
function _HeadType(s)  = _SV2(s, "spec",    "head", 0);
function _Drive(s)     = _SV2(s, "spec",    "drive", 0);
function _HeadA(s)     = _SV2(s, "spec",    "head_angle", 0);
function _NutD(s)      = _SV2(s, "nutSpec", "diameter", 0);
function _NutH(s)      = _SV2(s, "nutSpec", "height", 0);

function _Info(s)      = _SV1(s, "spec",   []);
function _Spec(s)      = _SV1(s, "spec",   []);
function _Nut(s)       = _SV1(s, "nut",    0);
function _Trap(s)      = _SV1(s, "trap",   0);
function _Gap(s)       = _SV1(s, "gap",    0);
function _ExtendH(s)   = _SV1(s, "extendh", 0);
function _ExtendS(s)   = _SV1(s, "extends", 0);
function _PokeL(s)     = _SV1(s, "pokel",  0);
function _PokeD(s)     = _SV1(s, "poked",  0);
function _TrapL(s)     = _SV1(s, "trapl",  0);
function _T(s)         = _SV1(s, "t",      0);
function _ScrewName(s) = _SV1(s, "screwname", "");

/*
    The head height value for flat screws in screw_info is 0.
    For our purposes though, the head height is the distance
    from head top surface to beginning of threads.
*/
function _HeadH(sc)   = (_HeadType(sc) == "flat") 
                      ? (_HeadD(sc) - _ScrewD(sc)) / 2 / tan(_HeadA(sc) / 2)
                      : _HeadHV(sc);

function _HeadD(sc)   = (_HeadDV(sc) == undef) 
                      ? _ScrewD(sc)
                      : _HeadDV(sc);

function _WholeScrewL(sc)  = (_HeadType(sc) == "flat") 
                      ? _ScrewL(sc)
                      : _ScrewL(sc) + _HeadH(sc);

function _ThreadL(sc) = (_HeadType(sc) == "flat") 
                      ? _ScrewL(sc) - _HeadH(sc)
                      : _ScrewL(sc);

function _HoleL(sc) = _ExtendH(sc) + _ExtendS(sc) + _WholeScrewL(sc);                      

module screw_hole(sc, anchor = "gap", spin = 0, orient = UP) {
    top_of_head = _HoleL(sc)/2 - _ExtendH(sc);
    bot_of_head = top_of_head  - _HeadH(sc);
    center_of_gap = bot_of_head - _Gap(sc)/2;
    top_of_nut = center_of_gap - _Gap(sc)/2;
    bottom_of_screw = top_of_head - _WholeScrewL(sc);
    head_diameter = _HeadD(sc) + _T(sc);
    struct_echo(sc, "screw_hole");
    echo(_HoleL(sc), top_of_head, bot_of_head, center_of_gap, top_of_nut, bottom_of_screw);

    anchors = [
        named_anchor("top",  [0, 0, top_of_head]),
        named_anchor("head", [0, 0, bot_of_head]),
        named_anchor("gap",  [0, 0, center_of_gap]),
        named_anchor("nut",  [0, 0, top_of_nut]),
        named_anchor("bot",  [0, 0, bottom_of_screw]),
    ];

    attachable(anchor = anchor, spin = spin, orient = orient, d = head_diameter, h = _HoleL(sc), anchors = anchors) {
        up(_HoleL(sc)/2)
        union() {
            if (_Nut(sc) == "self") {
                _self_tap(sc);
            } else {
                _screw_hole(sc);
            }

            if (_Trap(sc) == "side") {
                _side_trap(sc);
            } else if (_Trap(sc) == "inline") {
                _inline_trap(sc);
            }
        }

        children();
    } // attachable()
} // screw_hole

module screw_from_context(sc, anchor = "gap", spin = 0, orient = UP) {
    top_of_head = _HoleL(sc)/2 - _ExtendH(sc);
    bot_of_head = top_of_head  - _HeadH(sc);
    center_of_gap = bot_of_head - _Gap(sc)/2;
    top_of_nut = center_of_gap - _Gap(sc)/2;
    bottom_of_screw = top_of_head - _WholeScrewL(sc);
    head_diameter = _HeadD(sc) + _T(sc);
    struct_echo(sc, "screw_hole");
    echo(_HoleL(sc), top_of_head, bot_of_head, center_of_gap, top_of_nut, bottom_of_screw);

    anchors = [
        named_anchor("top",  [0, 0, top_of_head]),
        named_anchor("head", [0, 0, bot_of_head]),
        named_anchor("gap",  [0, 0, center_of_gap]),
        named_anchor("nut",  [0, 0, top_of_nut]),
        named_anchor("bot",  [0, 0, bottom_of_screw]),
    ];

    attachable(anchor = anchor, spin = spin, orient = orient, d = head_diameter, h = _HoleL(sc), anchors = anchors) {
        up(top_of_head) down(_HeadH(sc))
        screw(spec = _Spec(sc), anchor = TOP);

        children();
    } // attachable()
} // screw_from_context

module nut_from_context(sc, anchor = "gap", spin = 0, orient = UP) {
    top_of_head = _HoleL(sc)/2 - _ExtendH(sc);
    bot_of_head = top_of_head  - _HeadH(sc);
    center_of_gap = bot_of_head - _Gap(sc)/2;
    top_of_nut = center_of_gap - _Gap(sc)/2;
    bottom_of_screw = top_of_head - _WholeScrewL(sc);
    head_diameter = _HeadD(sc) + _T(sc);
    //struct_echo(sc, "nut_from_contex");
    //echo(_HoleL(sc), top_of_head, bot_of_head, center_of_gap, top_of_nut, bottom_of_screw);

    anchors = [
        named_anchor("top",  [0, 0, top_of_head]),
        named_anchor("head", [0, 0, bot_of_head]),
        named_anchor("gap",  [0, 0, center_of_gap]),
        named_anchor("nut",  [0, 0, top_of_nut]),
        named_anchor("bot",  [0, 0, bottom_of_screw]),
    ];

    attachable(anchor = anchor, spin = spin, orient = orient, d = head_diameter, h = _HoleL(sc), anchors = anchors) {
        up(top_of_nut)
        _nut(sc);

        children();
    } // attachable()
} // nut_from_context

module _nut(sc) {
   if (_Nut(sc) == "square") {
        difference() {
            cube([_NutD(sc), _NutD(sc), _NutH(sc)], anchor = TOP);

            // Remove
            screw(spec = _Spec(sc));
        }
    } else if (_Nut(sc) == "hex") {
        nut(diameter = _NutD(sc), thickness = _NutH(sc), spec = _Info(sc), anchor = TOP);
    }
}

module _screw_hole(sc) {
    union() {
        // Head extention
        up(_tiny)
        zcyl(h = _ExtendH(sc) + _tiny, d = _HeadD(sc) + _T(sc), anchor = TOP)

        // Head 
        attach(DOWN, UP) 
        _head(sc)

        // Screw + extention
        attach(DOWN, UP) 
        zcyl(h = _ThreadL(sc) + _ExtendS(sc), d = _ScrewD(sc) + _T(sc));
    }
} // _screw_hole

module _self_tap(sc) {
    //echo("_self_tap");
    union() {
        // Head extention
        up(_tiny)
        zcyl(h = _ExtendH(sc) + _tiny, d = _HeadD(sc) + _T(sc), anchor = TOP)

        // Head 
        attach(DOWN, UP) 
        _head(sc)

        // Screw hole
        attach(DOWN, UP) 
        zcyl(h = _Gap(sc) + 1, d = _ScrewD(sc) + _T(sc))

        // Narrow self tapping section
        attach(DOWN, UP) 
        zcyl(h = _ThreadL(sc) - _Gap(sc), d = _ScrewD(sc) - _ScrewP(sc));
    }
} // _self_tap

module _head(sc, anchor=TOP, orient = UP, spin=0) {
    head_height = _HeadH(sc) + _tiny;
    head_diameter = _HeadD(sc) + _T(sc);

    attachable(anchor, spin, orient, d = head_diameter, h = head_height, l = head_height) {
        if (_HeadType(sc) == "flat") {
            r2 = (_HeadD(sc) + _T(sc)) / 2;
            r1 = (_ScrewD(sc) + _T(sc)) / 2;
            cyl(r1 = r1, r2 = r2, l = head_height, anchor = CENTER);
        } else {
            zcyl(h = _HeadHV(sc) + _tiny, d = head_diameter, anchor = CENTER);
        }

        children();
    }
} // _Head

module _side_trap(sc, slotLen = 15) {
    // returns trap as if screw_hole is anchor = TOP
    top_of_head = - _ExtendH(sc);
    bot_of_head = top_of_head  - _HeadH(sc);
    center_of_gap = bot_of_head - _Gap(sc)/2;
    top_of_nut = center_of_gap - _Gap(sc)/2;
    bottom_of_screw = top_of_head - _WholeScrewL(sc);
    head_diameter = _HeadD(sc) + _T(sc);

    union() {
        // Slot
        up(top_of_nut)
        up(_T(sc)/2) {
            cube([_TrapL(sc), _NutD(sc) + _T(sc), _NutH(sc) + _T(sc)], anchor = [-1, 0, 1]);

            if (_Nut(sc) == "square") {
                cube([_NutD(sc) + _T(sc), _NutD(sc) + _T(sc), _NutH(sc) + _T(sc)], anchor = [0, 0, 1]);
            } else {
                down(_NutH(sc) + _T(sc))
                _hexCyl(h = _NutH(sc) + _T(sc), id = _NutD(sc) + _T(sc)); 
            }

            // Poke hole
            if (_PokeL(sc) > 0) {
                down(_NutH(sc)/2)
                xcyl(h = (_PokeL(sc) == 1 ? 25 : _PokeL(sc)), d = _PokeD(sc), anchor = [1, 0, 0]);
            }
        } // up() up()
    }
} // _side_trap

module _inline_trap(sc) {
    // returns trap as if screw_hole is anchor = TOP
    top_of_head = - _ExtendH(sc);
    bot_of_head = top_of_head  - _HeadH(sc);
    center_of_gap = bot_of_head - _Gap(sc)/2;
    top_of_nut = center_of_gap - _Gap(sc)/2;
    trap_len = _HoleL(sc) + top_of_nut + _T(sc); // top_of_nut is negative

    up(top_of_nut + _T(sc)/2)
    if (_Nut(sc) == "square") {
        cube([_NutD(sc) + _T(sc), _NutD(sc) + _T(sc),  trap_len], anchor = [0, 0, 1]);
    } else {
        down(trap_len)
        _hexCyl(l = trap_len, id = _NutD(sc) + _T(sc));
    }
} // _inline_trap

module _hexCyl(l = -1, h = -1, id = -1, d = -1) {
    H = (h > 0) ? h : l;
    ID = (id > 0) ? id : d;

    linear_extrude(H)
    hexagon(id = ID);
} // _hexCyl

/***************************************************************
*
* Test Code
*
***************************************************************/

_test();

module _test() {
    showAnchor = "all";

    trap_angle = 180;
    siName = "M5,16";
    siHead = "socket";
    //siHead = "flat";
    //siHead = "button";
    siDrive = "hex";

    scGap = 4;
    scNut = "hex"; // "hex, "square", "self"
    scTrap = "side"; // "inline", "side", none
    scPoke = 10; // -1, 1
    scTrapL = 25;

    scExtendH = 1;
    scExtendS = 3;

    si = screw_info(name = siName, head = siHead, drive = siDrive);

    sc = screw_context(spec = si, gap = scGap, extendH = scExtendH, extendS = scExtendS, 
          nut = scNut, trap = scTrap, pokeL = scPoke);

    function show(p) = (p == showAnchor || showAnchor == "all") ? true : false;

    echo(show(TOP), show("top"));

    xdistribute(20) {
        ydistribute(15) {
            if (show(TOP))    screw_hole(sc, anchor = TOP,    spin = trap_angle);
            if (show("top"))  screw_hole(sc, anchor = "top",  spin = trap_angle);
            if (show("head")) screw_hole(sc, anchor = "head", spin = trap_angle);
            if (show("gap"))  screw_hole(sc, anchor = "gap",  spin = trap_angle);
            if (show("nut"))  screw_hole(sc, anchor = "nut",  spin = trap_angle);
            if (show("bot"))  screw_hole(sc, anchor = "bot",  spin = trap_angle);
            if (show(BOT))    screw_hole(sc, anchor = BOT,    spin = trap_angle);
        }

        ydistribute(15) {
            if (show(TOP))    union() {
                screw_from_context(sc, anchor = TOP, spin = trap_angle);
                nut_from_context(  sc, anchor = TOP, spin = trap_angle);
            }
            if (show("top"))    union() {
                screw_from_context(sc, anchor = "top", spin = trap_angle);
                nut_from_context(  sc, anchor = "top", spin = trap_angle);
            }
            if (show("head"))    union() {
                screw_from_context(sc, anchor = "head", spin = trap_angle);
                nut_from_context(  sc, anchor = "head", spin = trap_angle);
            }
            if (show("gap"))    union() {
                screw_from_context(sc, anchor = "gap", spin = trap_angle);
                nut_from_context(  sc, anchor = "gap", spin = trap_angle);
            }
            if (show("nut"))    union() {
                screw_from_context(sc, anchor = "nut", spin = trap_angle);
                nut_from_context(  sc, anchor = "nut", spin = trap_angle);
            }
            if (show("bot"))    union() {
                screw_from_context(sc, anchor = "bot", spin = trap_angle);
                nut_from_context(  sc, anchor = "bot", spin = trap_angle);
            }
            if (show(BOT))    union() {
                screw_from_context(sc, anchor = BOT, spin = trap_angle);
                nut_from_context(  sc, anchor = BOT, spin = trap_angle);
            }
        }
    }
}
adrianVmariano commented 2 years ago

What is the "nut" anchor? It says the nut side of the gap, but I'm not sure what that means. Does it mean right above the nut? Also, when do you use the "gap" anchor?

You have "inline" and "side" for the nut trap. What does that mean?

rjcarlson49 commented 2 years ago

The "nut" anchor is the top edge of the nut. The gap is the space between the head and the nut, but the "gap" anchor is the center of that space. The "gap" anchor usually corresponds to the place where the mating surface will be. An exception to that is a self tapping screw. For that case the mating surface is at the "nut" anchor.

A "side" trap is a slot where the nut slides in at 90 degrees from the screw. "An inline" trap is an extension of the screw hole shaped like the nut, either hex or square.

It's pretty easy to edit the test code and show any kind of screw head, nut, trap etc. It shows the resulting screw holes in each anchor position. Next to each "hole" is the corresponding screw and nut using the same anchor.

adrianVmariano commented 2 years ago

An inline trap doesn't look like it traps anything. What is the intended use for this?

rjcarlson49 commented 2 years ago

The inline trap keeps the nut from rotating. It's hex for a hex nut and square for a square nut. Once you get the screw started it will continue through and become as tight as you want.

rjcarlson49 commented 2 years ago

I considered adding anchor, spin and orient to the screw_context for use when these are always or usually the same in a given application. The anchor, spin and orient arguments in screw_hole(...), etc. would override the ones in the screw_context. It seemed to me that leaving them out of the context was more consistent with the rest of BOSL2. If you like it, that is an option. It's also an option to fold this functionality into existing calls by adding a context argument. Screw_hole, or screw_mask if you prefer, is probably the only one that seems to need to be added.

adrianVmariano commented 2 years ago

The inline trap should have been more obvious to me. It makes sense now.

Right now my thinking on this matter is that you are duplicating too much functionality that already exists for it to make sense for us to directly use your code. You have basically created a second screw context system even though one already exists. And you have also duplicated the head-drawing code, which is problematic, since it creates two parallel lines of code to maintain. If I'm reading your code correctly, the flat head code will not work right on 82 deg or 100 deg angled flat heads, for example. And if one specified an undercut screw it looks like things would also go awry. And with my latest code to fix sharp flathead corners, I think there will also be more problems, because you need to use the sharp diameter for a hole instead of the true diameter.

But this functionality is important, so I do want to figure out a good way to incorporate it.

rjcarlson49 commented 2 years ago

I relied on the BOSL2 stuff as much as I thought I could. The only code I rewrote was the flat head because I couldn't see any other way. I used the angle from screw_info so I don't see why it wouldn't be correct for an 82 or 100 degree screw. As for the other heads, AFAIK they are all round and I use the head diameter from screw_info so I don't know why that would mess up. One reason I had to write the flat head code is that my custom anchors know about the "bottom of the head". For a flat screw I treat this as the bottom of the cone (usually also the top of the thread). This seemed most intuitive to me.

If you point me in a direction where I could use more BOSL2, I can adapt the code more closely.

I'm not sure what you mean about there already being a screw context system. I assume you are talking about screw_info. Screw_info is entirely a description of the screw itself. The data call comes from BOSL2 once the screw is chosen. Screw_context is, except for the incorporated screw_info, entirely a description of what's around the screw. Screw_info is incorporated only to make it more efficient. Because of the use of structs, I think the code is pretty resilient to changes in screw_info as long as clear incompatibilities are not introduced. Because OpenSCAD effectively only has constants, a user can't really add values to a struct. Yeah I know you can make a copy, but that's inelegant at best. So if you wanted to add my context information to screw_info, you'd have to a lot of arguments to screw_info or a lot of arguments to other modules.

There's a natural trade off. You can often add more arguments to a single module or you can split the module into 2 or three and each one will have fewer arguments. I love BOSL2 but IMHO it leans too far in the former direction. Case in point, I never even noticed the oversize argument for screw. I had no idea that functionality existed. This what at a minimum I think you need to a screw_hole (or _mask) module.

Typically I find I never have a single screw hole I need, it's always several. That's why screw_context. Specifying a bunch of arguments on a call and having several identical calls with all the arguments is error prone because any change requires changing all the calls. Putting all the arguments in "variables" allows you to make a change in one place, but that's kind of messy too.

adrianVmariano commented 2 years ago

I've been working on fixing various bugs and deficiencies in screw() for the past couple months. Revar and I had a discussion a couple weeks ago about hole making and I split off the hole making functionality into screw_hole() but I think the version currently posted has some bugs and deficiencies. I think I'm almost done, but a key thing that will be of interest to you which is still missing is flexible anchoring. I got kind of stuck on the naming and anchoring for the threaded part of the screw (which might not be threaded), the shank, the shank + threaded part (shaft), and how to handle all the cases in a clear fashion. Hopefully I'll finish this in the next couple days.

You're right, the head size computation in your code looks reasonable for flat heads as things stand. (I was evidently looking at the wrong thing.) But in the new code they look like this:

image

Screw on the left, and matching sized clearance hole mask on the right. That's with the ASME "normal" hole tolerance and $slop=0. If you use the actual head diameter (that's on the left) to make a clearance hole you'll get a clearance hole that leaves the flat part of the screw sticking up out of the hole. This whole business with flatheads was pretty annoying to get nailed down.

Fundamentally, I can't see a justification for generating your screw (or screw mask), including the head, by any other means than a call to screw() itself, because then you're duplicating functionality. That was the original reason I tried to use screw() to also make masks---to avoid making the code more complex. For uniformity of the interface the anchors should be identical between screw() and screw_hole(), so duplicating code creates an issue there as well, especially if we end up tweaking the anchors. My new code has screw_hole() as basically a pass-through that invokes screw, and I hid the arguments to screw() that are only suited to holes, like counterbore and internal.

If you have a struct and you rewrite it to add to or change entries you're making a new data structure. If you create a new data structure that has the struct as a sub-part you're also creating a new data structure. Maybe OpenSCAD is clever and uses a reference to the existing struct in your new one? But the amount of memory is insignificant. I don't think it matters which one you do from a performance perspective. The struct_set() function will change or extend structures, so the code to do it is simple enough. In fact, I even imagined that a user might want to create a screw and then tweak it.

To me it seems like what you're doing with screw_context should be done with a user-defined module. That is, if you need some specific screw arrangement a bunch of times you make a module to produce it and then you can create it as needed.

I think that designing the library interface is hard, and there's no guarantee that we get it right the first time. I'm curious if there are other specific cases that leap to mind where you feel BOSL2 has too much functionality in one module, and it should instead have been split. Both Revar and I think that nut traps should be a separate module from the screw_hole module, so in this instance, you're leaning the other way, and in fact actually combining two things in one module.

rjcarlson49 commented 1 year ago

I'm in the process of simplifying the code I last posted. There is really not very much code that might need maintenance. Most of it is in setting up and accessing the screw_context. And I understand how structs work.

However, I think we are converging.

I certainly entertained the idea of making the traps a separate call and I don't think much is lost by separating it.

The real key is my custom anchors. All of them are actually found relative to the screw and nut position, rather than the entire screw hole. The only place where the anchors need to "know" about the head and screw extensions of the hole are when you use TOP or BOTTOM when placing a screw hole. In my experience, I've never found myself using those enchors except in test code.

The custom anchors "gap" and "nut" require knowledge of the nut position, AKA, the size of the gap. So in order to use those anchors with screw (and thus screw_hole in your pass thru), would you add a gap parameter? Something else I haven't thought of?

Other than gap, the entries in the context are all either not strictly needed (like spec (screw_info) and nutSpec because they can be handled by passing screw_info, OR they are only needed as arguments to nut_trap call.

So to implement my set of custom anchors and nut traps and have all the screws, nuts, holes and traps use the same set:

1) No screw_context is necessary. 2) Add a gap argument to screw() and nut(). 3) Add screw_hole() that takes screw_info and gap arguments. Include self tapping in screw hole. 4) Add nut_trap() that would require screw_info and gap arguments, plus trap type, trapL, extendH, extendS, pokeL and pokeD and nut type (hex or square).

I think this will do everything my code does and within your constraints. The biggest change is the introduction of gap and the 2 anchors that depend on gap. Lest you be tempted to leave these out, they are the ones I use 90% of the time. The gap determines where the mating surface is and that is almost always the reference point I need when I move the screw hole or screw into place.

One more point. I often render a screw and nut in place in my rendering just to check for correctness, so I need the same anchors in screw, nut, screw_hole and nut_trap.

Well one more. I include a "poke hole" option. I quickly found I liked having a way to get a nut out of a trap.

rjcarlson49 commented 1 year ago

I notice also that your nut info is not nearly as robust as the screw tables in the code. I think this should be fixed and square nuts accommodated.

rjcarlson49 commented 1 year ago

Screw_hole also needs the extendH and extendS arguments.

adrianVmariano commented 1 year ago

Getting the data and then entering it and organizing it is a major challenge in all of this. Part of the big problem I was trying to fix is that torx drivers didn't have valid depth info anywhere, and there was generic torx depth in screw_drive.scad that was bogus (much too deep). I would say we don't have nut info at all...I did recently download a nut spec standard, asme b18.2.2. But I need to nail down the screws before thinking about nuts. For the record, the long term plan is to eliminate metric_screws() completely and have screws.scad replace it. I think the only nut dimensions are in metric_screws.scad right now. To do nuts seriously presumably means building a nut_info() function analogous to screw_info with lookup tables or formulas for the different nuts. I don't know how many different sizes/types of nuts there are. "heavy" "jam" hex and square. Is that all? One nice thing about the standard I found is that it has formulas in appendix A1 so that means a lot less data entry and tables to worry about. I found some formulas for flat head dimensions as well when I was trying to straighten that out. ASME seems nicer about revealing underlying formulas than ISO.

I have non-flat-head anchoring working in my screws code, but am at the moment confused about how anchors should be defined for flat heads. Once I come to a conclusion about that, I think it shouldn't take long to implement and I can push my current code. (Though...hmmm....maybe I should also write docs first.)

I wonder if nut_trap should be drawn by the same code that draws nuts. That would be a way to ensure/enforce uniformity.

The poke hole seems like a great idea. I've on occasion tried to extract hardware from nut traps with some difficulty.

So "gap" is the point right above the nut, yes? So if you put the nut somewhere...then the "gap" is automatically right above wherever you just put the nut? I mean, I was thinking of placing the screw and then making the nut trap relative to the screw, but there's no reason it couldn't be done in reverse: place the nut trap so the "gap" point is where you need it and then place the screw relative to the nut trap.

Could you post an example or two of your code being used to create a nut trap in an actual context that shows the typical use case and makes it clear why the gap is important? (Something simple, not a huge model with 20 screws.) I mean, if I understand this all (no guarantees) then you should be able to create the gap you need with something like:

screw_hole(....) position("head_bot") down(gap) nut_trap(....,anchor=TOP);

You mention screw_hole taking a gap argument. What would it do with that argument?

With your self-tapping screws you are making the holes flat walled but sized to match d_minor instead of d_major? This is actually a significant issue because if I need access to threading information in screw_hole() I need to change its interface somehow. (Basically you need to be able to specify threads and then say you don't want threads. I guess maybe a tapping=true argument could work.)

adrianVmariano commented 1 year ago

I don't understand the need for extendH and extendS. Why not just make the screw longer?

rjcarlson49 commented 1 year ago

You can't make the head longer. And wouldn't you want the code to reflect the actual length of the screw as opposed to the length of the whole you need? Also the screws come in standard lengths, like M3,12 or M3,16.

I've done designs where the screw is as much as 2-3 cm below the surface, requiring an ExtendH and S of 30. If you have a side trap a standard length for ExtendS of 2mm could be used. But when an inline trap is used you have to know who long it must be.

You could make the extensions really long and assume that could cover all cases, but what if you have an unusual case where the extensions would run into some other part as you difference it out? If you went with this option, you would make head holes, side traps and inline traps very long and standard screw holes 2 beyond the end of the screw. But how long is long enough?

adrianVmariano commented 1 year ago

So it sounds like extendH corresponds to my counterbore parameter.

I'm not sure I understand why the code shouldn't reflect the length of the hole being created. If mean, you could always write length=12+4 or whatever to highlight the relationship to the actual screw. The one benefit I can see to this extension idea is that it makes it easier to create the screw that goes in the hole. This is basically a situation where we face the question: is it better to complicate the interface (add a parameter) or simplify the interface (user knows how to use "+" operation if necessary). I wonder what @revarbat thinks about this.

rjcarlson49 commented 1 year ago

Getting the data and then entering it and organizing it is a major challenge in all of this. Part of the big problem I was trying to fix is that torx drivers didn't have valid depth info anywhere, and there was generic torx depth in screw_drive.scad that was bogus (much too deep). I would say we don't have nut info at all...I did recently download a nut spec standard, asme b18.2.2. But I need to nail down the screws before thinking about nuts. For the record, the long term plan is to eliminate metric_screws() completely and have screws.scad replace it. I think the only nut dimensions are in metric_screws.scad right now. To do nuts seriously presumably means building a nut_info() function analogous to screw_info with lookup tables or formulas for the different nuts. I don't know how many different sizes/types of nuts there are. "heavy" "jam" hex and square. Is that all? One nice thing about the standard I found is that it has formulas in appendix A1 so that means a lot less data entry and tables to worry about. I found some formulas for flat head dimensions as well when I was trying to straighten that out. ASME seems nicer about revealing underlying formulas than ISO.

No doubt that's a difficult issue. I suspect that even your screw data is incomplete if looked at by an actual ME. I think nut_info should take screw_info as an argument. That specifies a LOT. Then maybe only one more argument to add the nut type.

I'm pretty sure I am getting all my information about metric screws (the only ones I use) from screw_info. I did not realize there is a metric_screws().

I have non-flat-head anchoring working in my screws code, but am at the moment confused about how anchors should be defined for flat heads. Once I come to a conclusion about that, I think it shouldn't take long to implement and I can push my current code. (Though...hmmm....maybe I should also write docs first.)

I really think a flat head should be regarded as having a "top", the flat part and a bottom, the spot where the taper intersects the shaft. That intuitively matches the definition of heads. Also, otherwise the "top" and "head" anchors would be the same and you would have no anchor and the bottom of the taper.

I wonder if nut_trap should be drawn by the same code that draws nuts. That would be a way to ensure/enforce uniformity.

I really don't like that idea. The nut trap is really easy to make. You only need the distance between faces, the nut thickness and the type, hex or square. I really don't like the idea of drawing a screw hole with the screw code. I haven't looked yet but I imagine the "oversize" argument to screw REALLY complicates the code. Wouldn't you rather get rid of that? What does it even mean to oversize threads? I think these are prime examples where two different calls makes the code simpler and more understandable.

The poke hole seems like a great idea. I've on occasion tried to extract hardware from nut traps with some difficulty.

So "gap" is the point right above the nut, yes? So if you put the nut somewhere...then the "gap" is automatically right above wherever you just put the nut? I mean, I was thinking of placing the screw and then making the nut trap relative to the screw, but there's no reason it couldn't be done in reverse: place the nut trap so the "gap" point is where you need it and then place the screw relative to the nut trap.

The "gap" anchor is the midpoint between the bottom of the head and the top of the nut. The "nut" anchor is the top of the nut.

Could you post an example or two of your code being used to create a nut trap in an actual context that shows the typical use case and makes it clear why the gap is important? (Something simple, not a huge model with 20 screws.) I mean, if I understand this all (no guarantees) then you should be able to create the gap you need with something like:

screw_hole(....) position("head_bot") down(gap) nut_trap(....,anchor=TOP);

I'll do that.

You mention screw_hole taking a gap argument. What would it do with that argument?

It is used in calculating the custom anchors. Assuming the same anchors are supported by screw and screw_hole uses screw, it would pass gap to screw as well. I have code for the "anchors" argument that fits in a simple function that can take just screw_info as an argument. I'll recode it to use struct_val calls and then you can just drop it into your code. Hopefully tomorrow.

With your self-tapping screws you are making the holes flat walled but sized to match d_minor instead of d_major? This is actually a significant issue because if I need access to threading information in screw_hole() I need to change its interface somehow. (Basically you need to be able to specify threads and then say you don't want threads. I guess maybe a tapping=true argument could work.)

You pass the screw_info structure to screw_hole. The thread info is in there.

Given a day or two I am pretty sure I can code up my stuff in a form pretty close to something you can just drop into yours that will conform to what I described in my long comment yesterday. I hope you'll wait to see that.

rjcarlson49 commented 1 year ago

This is a simple ring that clamps onto a bar on my golf push cart. It has a mounting for a magnet. The clamp is formed by a cube with a screw hole with inline trap. A slice of the cube is removed from within the gap so that the screw pulls the two parts together. Crude but it works. It's on my cart now.

You can see the code that shows the screw in place and the cross section reference that lets me look at it that way.

include <BCLib/common.3.scad>

barD = 31 + 1; // +1 for pad
magnetD = 18;
magnetH = 3;

bandH = 10;
bandT = 3; 

screwName = "M3,12";

screwContext = screw_context(name = screwName, gap = 6, entendH = 6, extendS = 6, trap = "inline");

//crossSection("y") {
    left(bandH/2 + 1)
    clamp();

    right(barD/2)
    band();

    fwd(magnetD/2 + 1 - bandH/2)
    right(barD)
    setting();
//}

/*
left(bandH/2 + 1) {
    screw_from_context(screwContext);
    nut_from_context(screwContext);
}
*/

module clamp() {
    difference() {
        cube([bandH, bandH, 14], anchor = CENTER);

        // Remove
        screw_hole(screwContext, anchor = "gap");

        cube([10, 10, 2], anchor = CENTER);
    }
}

module band() {
    difference() {
        ycyl(h = bandH, d = barD + bandT*2);

        // Remove
        ycyl(h = bandH + t2, d = barD);

        left(barD/2)
        cube([10, 10, 2], anchor = CENTER);
    }
}

module setting() {
    right(bandT - tiny)
    difference() {
        xcyl(d = magnetD + 2 + t2, h = 3, anchor = LEFT);

        // Remove
        xcyl(d = magnetD+ t2, h = 4, anchor = LEFT);
    }

    xcyl(d = magnetD + 2 + t2, h = bandT, anchor = LEFT);
}
adrianVmariano commented 1 year ago

I am making a PR for my current code, but I don't think anchoring works right yet. (It turns out the existing posted version has a serious bug, so need to get something out.) But you might want to take a look at screw_hole().

My expectation is that very few users will use the screw_info structure. I anticipate that a user will write something like screw("M6",...) to make a screw andscrew_hole("M6",...)` to make a hole As a user, why do I want some extra data structure to hold the information "M6"? Also, I think the common use case is going to be to use only one of screw() or screw_hole(), not both, so there's no reason to believe a screw specification structure already exists. I mainly expose that structure for users to enable them to do more complex stuff, like making nonstandard screws, tweak head geometry, or adjusting driver settings. So all the modules should work without needing a screw_info spec as input.

I can't think of any reason to oversize threads in a perfect world, but for 3d printing, oversizing the threaded hole may be necessary for parts to mate, depending on the accuracy of your machine, and I wouldn't be surprised if a screw needed to be undersized to mate with metal hardware, so a negative oversize. The way this works is that the diameter just gets oversize added to it. I don't think it's a massive complication. The threads keep the same profile. I don't recall at this point---I think with my printer I was able to get parts to mate by just using the tolerance settings. But for pipe threads I know I needed to artificially adjust the size of the threads (with the $slop parameter) to mate with an existing pipe threaded fitting. Since screw() doesn't respect $slop (because it applies to holes only) undersizing may be needed under some circumstances.

The nut_info() function should definitely not take a screw spec as a required argument. This would lead to the mess of having to do nut_info(screw_info(.....)) to create a nut. You should be able to write something like nut("M6") and get a nut, and someone who wants to mess around with a nut_info structure should not need to explicitly call screw_info to do it. It does seem that the way things are currently set up, nut_info will need to call screw_info with a headless screw to get threading info. Perhaps threading sizing info should be split off into a subfunction?

So for screw_hole and screw the anchoring has to be the same, and the anchoring alone is complicated enough that repeating it seems like a bad idea. I didn't try to go through screw() and rip out everything that's not needed for screw holes to see if the code got significantly simpler, but why bother? Keep in mind that users may want to make threadless screws to show in models without time consuming calculation, so we need the ability to make threadless screws anyway. It's not like it's special for holes. I do not understand what the point is of splitting the functionality and repeating code. I see no way in which it doesn't result in increased complications for maintenance.

The way I have it, screw_hole() is basically a pass-through to screw().

Note that there are two different issues here, one having to do with the interface seen by the user and the other one about code internals. You may be right that there is no benefit from combining nut trap functionality and nut functionality into a single module.

adrianVmariano commented 1 year ago

Oh, and I have literally hundreds of pages of standards documents for screws and related stuff. There's no way that the contents of BOSL2 is even close to "complete". Wanna go add truss heads? Oval heads? Revar wants shoulder screws, which is still a complication. And note that if you want to make a screw hole for a shoulder screw you need more information now, and more complication if you're repeating it in another module.

adrianVmariano commented 1 year ago

Regarding standards, I took a look at asme b18.2.2 and it lists specs for 14 different kinds of nut.

And mysteriously Table 1-1 says "dimensions of square and hex machine screw nuts" and shows a picture of a square nut with the label "no chamfer allowed". And then two pages later, in Table 2 we see "Dimensions of Square Nuts" and a 25 deg chamfer is required.

I haven't tried to track down the ISO standard for nuts to see what the metric scene looks like.

rjcarlson49 commented 1 year ago

Ok, here it is. Screw_context is gone. There are basically 3 calls, screw_hole, screw_in_hole and nut_in_hole. All use identical anchors and produce identical positioning for all three outputs when the same inputs are used.

If you look at line 217, there is a commented out call to screw() that can replace the call above it. Note I do not use the "internal" parameter because that is not in the copy of BOSL2 I current have.

I did add the slight counterbore on the my flat head code so it should be flush or close when extendH is 0.

I have kept all the "access" functions rather than use struct_val because I like to hide details of data structure as much as possible.

//   use <../!lib/utils.0.scad>
include <BOSL2/std.scad>
include <BOSL2/screws.scad>

$fn = $preview ? 30 : 90;
_tiny = 1/128;

/*
The default tolerance is a measured amount. Vertical dimensions
in printed holes require this much extra so that a nut will always 
fit. A much smaller tolerance can work if the hole's cross section 
is in the XY plane. However, the .25 value works in the worst case.
*/ 
defaultTolerance = .25; 

/*************************************************************
Screws and holes are rendered vertically with head at the top
Slots for nut traps are in the direction of +X

extendH extend the hole at the top of the head
extendS extendthe hole at the bottom of the crew
gap is the space between head and nut

The screw hole is generated vertically.
If there is a side trap, it extends toward the +X axis.
If there is a poke hole with the side trap, it extends on -X

nut - "none", "hex", "square" or "self", default: "hex"
trap - "none", "side" or "inline", default: "inline"

Several special anchors are defined. All these anchors
are on the Z axis. The designated anchor will be at the XYZ origin.

   TOP     - At the top of the screw hole
   "top"   - At the top of the screw head
   "head"  - At the bottom of the screw head
   "gap"   - Center of gap, ie half way between head and nut
   "nut"   - Nut side of the gap 
   "screw" - At the screw end
   "bot"   - Bottom of the screw
   BOT     - Bottom of the screw hole

   spin can be used to rotate a side trap to the desired direction.
   orient can be used to orient the screw hole as desired, 
   UP (default), DOWN, LEFT, RIGHT, etc.
***************************************************************/

/*
screw_hole(sc, anchor = "gap", spin = 0, orient = UP)
    sc - screw context

    Creates a screw hole includes any nut trap (including "poke hole").
    It is vertically positioned as designated in the context.
    If the trap is a side trap, it extends in the direction of the x axis.
    ANy poke hole is opposite the side trap.

screw_from_context(sc, anchor = "gap", spin = 0, orient = UP)
    sc - screw context

nut_from_context(sc, anchor = "gap", spin = 0, orient = UP)
    sc - screw context
*/

/*

Nut Info information

diameter - distance face to face
height   - thickness

Hex and square nuts appear to have the same specs
This table only covers metric nuts for screws from 3 to 10 mm
*/

function _NutInfo(si) = let (d = struct_val(si, "diameter", 0))
           (d == 3)  ? [["diameter", 5.5],  ["height", 2.4]] : 
           (d == 4)  ? [["diameter", 7.0],  ["height", 3.2]] :
           (d == 5)  ? [["diameter", 8.0],  ["height", 4.0]] :
           (d == 6)  ? [["diameter", 10.0], ["height", 5.0]] :
           (d == 7)  ? [["diameter", 11.0], ["height", 5.5]] :
           (d == 8)  ? [["diameter", 13.0], ["height", 6.5]] :
           (d == 10) ? [["diameter", 17.0], ["height", 8.0]] : [];

/*
Functions for accessing the screw_context.
*/
function _SV1(s, n1, d = 0) = struct_val(s, n1, d);
function _SV2(s, n1, n2, d = 0) = len(struct_val(s, "spec", [])) == 0 
                                ? struct_val(s, n2, d)
                                : struct_val(struct_val(s, n1, ""), n2, d);

function _ScrewD(si)    = struct_val(si, "diameter", 0);
function _ScrewL(si)    = struct_val(si, "length", 0);
function _ScrewP(si)    = struct_val(si, "pitch", 0);
function _HeadHV(si)    = struct_val(si, "head_height", 0);
function _HeadDV(si)    = struct_val(si, "head_size", 0);
function _HeadType(si)  = struct_val(si, "head", 0);
function _Drive(si)     = struct_val(si, "drive", 0);
function _HeadA(si)     = struct_val(si, "head_angle", 0);

function _NutD(si)      = struct_val(_NutInfo(si), "diameter", 0);
function _NutH(si)      = struct_val(_NutInfo(si), "height", 0);

/*
    The head height value for flat screws in screw_info is 0.
    For our purposes though, the head height is the distance
    from head top surface to beginning of threads.
*/
function _HeadH(si)   = (_HeadType(si) == "flat") 
                      ? (_HeadD(si) - _ScrewD(si)) / 2 / tan(_HeadA(si) / 2)
                      : _HeadHV(si);

function _HeadD(si)   = (_HeadDV(si) == undef) 
                      ? _ScrewD(si)
                      : _HeadDV(si);

function _WholeScrewL(si)  = (_HeadType(si) == "flat") 
                      ? _ScrewL(si)
                      : _ScrewL(si) + _HeadH(si);

function _ThreadL(si) = (_HeadType(si) == "flat") 
                      ? _ScrewL(si) - _HeadH(si)
                      : _ScrewL(si);

function _HoleL(si, extendH, extendS) = extendH + extendS + _WholeScrewL(si);

// The following are all positions relative to the middle of the entire screw
// IE TOP, top of the screwr hole is 0.
function _TopOfHead(si)      = _WholeScrewL(si)/2;
function _BotOfHead(si)      = _TopOfHead(si)      - _HeadH(si);
function _CenterGap(si, gap) = _BotOfHead(si)      - gap/2;
function _TopOfNut(si, gap)  = _CenterGap(si, gap) - gap/2;
function _BotOfScrew(si)     = _TopOfHead(si)      - _WholeScrewL(si);

// Adding the adjustment gives the position of the anchors after the 
// screw_hole plus extensions is centered.
// The adjustment needed is (extendH + extendS) / 2.
function _anchors(si, gap, adjust = 0) = 
    [
        named_anchor("top",  [0, 0, adjust + _TopOfHead(si)]),
        named_anchor("head", [0, 0, adjust + _BotOfHead(si)]),
        named_anchor("gap",  [0, 0, adjust + _CenterGap(si, gap)]),
        named_anchor("nut",  [0, 0, adjust + _TopOfNut(si, gap)]),
        named_anchor("bot",  [0, 0, adjust + _BotOfScrew(si)]),
    ];

/*************************************************************
screw_hole

si      - the structure returned by screw_info
gap     - the distance in mm between head and nut in the assembly
nut     - "hex" (default), "square", "self", "none"
trap    - "none" (default), "inline", "side"
extendH - clearance above the head, ie additional length of the hole for the head
extendS - clearance below the screw, ie additional length of the hole for the screw
trapL   - trap length for a side trap, defaults to 25
pokeL   - poke hole length - a hole opposite the nut trap to allow the nut to be poked out
          default is -1 : no poke hole
          > 0 means include a poke hole
          1 means use the default length of 25
pokeD   - diameter of poke hole, defaults to 2
t       - tolerance added to diameters so screws will fit in holes, default is "defaultTolerance"

anchor  - anchor, The usual set plus the custom anchors described above
spin    - as usual, default to 0
orient  - as usual, default to UP
*************************************************************/

module screw_hole(si, 
                  gap = 0, 
                  nut = "hex",
                  trap = "none", 
                  extendH = 1, 
                  extendS = 1, 
                  trapL = 25,
                  pokeL = 25,
                  pokeD = 2,
                  t = defaultTolerance, 

                  anchor = "gap", 
                  spin = 0, 
                  orient = UP) 
{
    // for a self tapping screw extendS must be >= 1
    eS = (nut == "self" && extendS < 1) ? 1 : extendS;
    hole_length = _WholeScrewL(si) + extendH + eS;
    adj = (eS - extendH) / 2;

    struct_echo(_anchors(si, gap = gap, adjust = adj), "_anchors");
    echo("TOP", _HoleL(si, extendH, eS)/2);
    echo("...", _WholeScrewL(si), extendH, eS, adj);

    attachable(anchor = anchor, 
               spin = spin, 
               orient = orient, 
               d = _HeadD(si) + t, 
               h = hole_length, 
               anchors = _anchors(si, gap = gap, adjust = adj)) 
    {
        union() {
            if (nut == "self") {
                up(hole_length/2 - extendH) {
                    if (extendH > 0) {
                        zcyl(d = _HeadD(si) + t, h = extendH, anchor = BOTTOM);
                    }

                    _self_tap(si, gap = gap, extendH = extendH, extendS = eS, t = t);
                }
            } else {
                up(hole_length/2 - extendH) {
                    if (extendH > 0) {
                        #zcyl(d = _HeadD(si) + t, h = extendH, anchor = BOTTOM);
                    }

                    _screw_hole(si, t);
                    //screw(spec = si, shank = _ScrewL(si), oversize = t, anchor = TOP);

                    down(_TopOfHead(si) - _TopOfNut(si, gap))
                    if (trap == "side") {
                        _side_trap(si, nut = nut, trapL = trapL, pokeL = pokeL, pokeD = pokeD, t = t);
                    } else if (trap == "inline") {
                        inlineTrapL = extendS + _TopOfNut(si, gap = gap) - _BotOfScrew(si);
                        _inline_trap(si, nut = nut, trapL = inlineTrapL, t = t);
                    }

                    down(hole_length - extendH)
                    if (extendS > 0) {
                        zcyl(d = _ScrewD(si) + t, h = extendS + _tiny, anchor = BOTTOM);
                    }
                } // up
            }
        }

        children();
    } // attachable()
} // screw_hole

module _screw_hole(si, t) {
    union() {
        // Head 
        _head(si, anchor = TOP)

        // Screw + extention
        attach(DOWN, UP) 
        zcyl(h = _ThreadL(si) + _tiny, d = _ScrewD(si) + t);
    }
} // _screw_hole

module _self_tap(si, gap, extendH, extendS, t) {
    // Return centered
    union() {
        // Head extention
        up(_tiny)
        zcyl(h = extendH + _tiny, d = _HeadD(si) + t, anchor = TOP)

        // Head 
        attach(DOWN, UP) 
        _head(si, t = t)

        // Screw hole
        attach(DOWN, UP) 
        zcyl(h = gap + 1, d = _ScrewD(si) + t) // gap + 1 extends the full size hole 1 mm into the tapped surface

        // Narrow self tapping section
        attach(DOWN, UP) 
        zcyl(h = _ThreadL(si) - gap + extendS, d = _ScrewD(si) - _ScrewP(si));
        // extendS is always >= 1, along with the (gap + 1) above, the tapped hole is always >= 2 mm longer than the screw
    }
} // _self_tap

module _head(si, t = defaultTolerance, anchor = TOP, orient = UP, spin = 0) {
    head_height = _HeadH(si) + _tiny;
    head_diameter = _HeadD(si) + t;

    attachable(anchor = anchor, spin = spin, orient = orient, d = head_diameter, h = head_height) {
        if (_HeadType(si) == "flat") {
            r2 = (_HeadD(si) + t) / 2;
            r1 = (_ScrewD(si) + t) / 2;
            down(t - _tiny)
            cyl(r1 = r1, r2 = r2, h = head_height, anchor = CENTER)
            attach(TOP, BOTTOM)
            cyl(r = r2, h = t);
        } else {
            zcyl(h = _HeadHV(si) + _tiny, d = head_diameter, anchor = CENTER);
        }

        children();
    }
} // _Head

module _side_trap(si, nut, trapL, pokeL, pokeD, t) {
    // returns trap with top of trap at the origin (nominally) and the trap extending along the X axis
    // The top of the trap is actually t above the XY plane
    // Slot
    up(t/2) {
        cube([trapL, _NutD(si) + t, _NutH(si) + t], anchor = [-1, 0, 1]);

        if (nut == "square") {
            cube([_NutD(si) + t, _NutD(si) + t, _NutH(si) + t], anchor = [0, 0, 1]);
        } else {
            down(_NutH(si) + t)
            _hexCyl(h = _NutH(si) + t, id = _NutD(si) + t); 
        }

        // Poke hole - extends along the -X axis
        if (pokeL > 0) {
            down(_NutH(si)/2)
            xcyl(h = (pokeL == 1 ? 25 : pokeL), d = pokeD, anchor = [1, 0, 0]);
        }
    }
} // _side_trap

module _inline_trap(si, nut, trapL, t) {
    // returns trap with top of trap at the origin
    echo(nut, trapL, t, si);

    up(_tiny)
    if (nut == "square") {
        cube([_NutD(si) + t, _NutD(si) + t,  trap_len], anchor = [0, 0, 1]);
    } else {
        down(trapL)
        _hexCyl(l = trapL, id = _NutD(si) + t);
    }
} // _inline_trap

module _hexCyl(l = -1, h = -1, id = -1, d = -1) {
    H = (h > 0) ? h : l;
    ID = (id > 0) ? id : d;

    linear_extrude(H)
    hexagon(id = ID);
} // _hexCyl

/*************************************************************
screw_in_hole
Renders a screw positioned as it would be in a screw hole using the same custom anchors

si      - the structure returned by screw_info
gap     - the distance in mm between head and nut in the assembly
trap    - "none" (default), "inline", "side"
extendH - clearance above the head, ie additional length of the hole for the head
extendS - clearance below the screw, ie additional length of the hole for the screw
t       - tolerance added to diameters so screws will fit in holes, default is "defaultTolerance"

anchor  - anchor, The usual set plus the custom anchors described above
spin    - as usual, default to 0
orient  - as usual, default to UP
*************************************************************/

module screw_in_hole(
    si, 
    gap = 5, 
    extendH, 
    extendS, 
    t = defaultTolerance, 

    anchor = "gap", 
    spin = 0, 
    orient = UP) 
{
    hole_length = _WholeScrewL(si) + extendH + extendS;
    adj = (extendS - extendH) / 2;

    attachable(anchor = anchor, 
               spin = spin, 
               orient = orient, 
               d = _HeadD(si) + t, 
               h = hole_length, 
               anchors = _anchors(si, gap = gap, adjust = adj)) 
    {
        up(hole_length/2 - extendH - _WholeScrewL(si))
        screw(spec = si, anchor = BOT);

        children();
    } // attachable()
} // screw_from_context

/*************************************************************
nut_in_hole

si      - the structure returned by screw_info
gap     - the distance in mm between head and nut in the assembly
nut     - "hex" (default), "square", "self", "none"
extendH - clearance above the head, ie additional length of the hole for the head
extendS - clearance below the screw, ie additional length of the hole for the screw
t       - tolerance added to diameters so screws will fit in holes, default is "defaultTolerance"

anchor  - anchor, The usual set plus the custom anchors described above
spin    - as usual, default to 0
orient  - as usual, default to UP
*************************************************************/

module nut_in_hole(
    si, 
    gap = 5, 
    nut = "hex", 
    extendH, 
    extendS, 
    t = defaultTolerance, 

    anchor = "gap", 
    spin = 0, 
    orient = UP) 
{
    hole_length = _WholeScrewL(si) + extendH + extendS;
    adj = (extendS - extendH) / 2;

    attachable(anchor = anchor, 
               spin = spin, 
               orient = orient, 
               d = _HeadD(si) + t, 
               h = hole_length, 
               anchors = _anchors(si, gap = gap, adjust = adj)) 
    {
        up(hole_length/2 - extendH - _TopOfHead(si) + _TopOfNut(si, gap))
        _nut(si, nut);

        children();
    } // attachable()
} // nut_in_hole

module _nut(si, nut) {
   if (nut == "square") {
        difference() {
            cube([_NutD(si), _NutD(si), _NutH(si)], anchor = TOP);

            // Remove
            screw(spec = si);
        }
    } else if (nut == "hex") {
        nut(diameter = _NutD(si), thickness = _NutH(si), spec = si, anchor = TOP);
    }
}

/***************************************************************
*
* Test Code
*
***************************************************************/

_test(); 

module _test() {
    showAnchor = "all";

    trap_angle = 180;
    name = "M5,16";
    head = "socket";
    //siHead = "flat";
    //siHead = "button";
    drive = "hex";

    gap = 8;
    nut = "hex"; // "hex, "square", "self"
    trap = "side"; // "inline", "side", none
    poke = 10; // -1, 1
    trapL = 25;

    extendH = 6;
    extendS = 3;

    si = screw_info(name = name, head = head, drive = drive);

    function show(p) = (p == showAnchor || showAnchor == "all") ? true : false;

    echo(show(TOP), show("top"));

    xdistribute(20) {
        ydistribute(15) {
            if (show(TOP))    
                screw_hole(si, 
                           anchor = TOP,    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);

            if (show("top"))  
                screw_hole(si, 
                           anchor = "top",    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
            if (show("head")) 
                screw_hole(si, 
                           anchor = "head",    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
            if (show("gap"))  
                screw_hole(si, 
                           anchor = "gap",    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
            if (show("nut"))  
                screw_hole(si, 
                           anchor = "nut",    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
            if (show("bot"))  
                screw_hole(si, 
                           anchor = "bot",    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
            if (show(BOT))    
                screw_hole(si, 
                           anchor = BOT,    
                           spin = trap_angle, 
                           gap = gap, 
                           trap= trap,
                           extendH = extendH,
                           extendS = extendS,
                           trapL = trapL, 
                           pokeL = poke);
        }

//*
        ydistribute(15) {
            if (show(TOP))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = TOP);
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = TOP);
            }
            if (show("top"))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "top");
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "top");
            }
            if (show("head"))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "head");
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "head");
            }
            if (show("gap"))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "gap");
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "gap");
            }
            if (show("nut"))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "nut");
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "nut");
            }
            if (show("bot"))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "bot");
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = "bot");
            }
            if (show(BOT))    union() {
                screw_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = BOT);
                nut_in_hole(si, gap = gap, extendH = extendH, extendS = extendS, anchor = BOT);
            }
        }
        //*/
    }
}
rjcarlson49 commented 1 year ago

I have a question about creating attachable objects. How is the problem of coincident faces handled. I made my objects larger than the outside anchor points by _tiny, which I have set to 1/128. How does the rest of BOSL2 handle it?

adrianVmariano commented 1 year ago

There is an overlap argument and also $overlap global that makes attached objects sink in by that amount. I think Revar had it default to something nonzero, but then that lead to some kind of confusing situation.

My thinking on this matter is that if you are making a mask, then by all means, make it slightly bigger than its attached volume so that it subtracts in a well-behaved manner. But if you are making an object, think hard before doing this kind of thing, because it means you'll be making an object explicitly different than what you said you were making. This could result in a 3d model where objects aren't all on the same z base, which might conceivably create problems for printing. (I once had a series of print failures that were a result of sphere() not actually producing the specified radius, resulting in a small offset of part of the model.) We used to have $slop set with a nonzero default and that also created a problem where a user wondered why an object was the wrong size.

But consider the implications of making things oversized: you request a 1x1x1 cube and get a 1.001x1.001x1.001 cube. It's a little puzzling. And if you make the cube the right size and then put the anchors inside you can end up with a cube that's not where you expected it.

I think that the majority of the objects in the BOSL2 library were designed before the attachments system was invented, so there may be room for improvement. At this moment I still haven't quite nailed down how attachments will work for screws and screw holes, and shoulder screws are presenting various questions. (For example on attachments, I want it to be natural to make holes that are spaced away from a wall by a specified amount, or whose heads are spaced away from a wall by a specified amount.) Did you make "top" of a length L screw the point distance L from the screw's tip? That is, the point above the head on a flathead and underneath the head on other screws?

Note that you can still move objects around even after using attach or position. So something like attach() down(tiny) thing() is a way to get an overlap, though admitedly it seems somewhat clumsy.

Also I talked briefly with Revar and he does not like the idea of two length arguments (e.g. length and extendS).

rjcarlson49 commented 1 year ago

First, I found some problems in the self tap code posted above. I'm fixing them.

I see the problem with making objects the "wrong" size behind the scenes. As you said, no problem for screw_hole I think.

There aren't 2 length arguments. When you call screw_info, you specify the length of the screw. When you call screw_hole then you need to specify how far above and below the hole must go. I don't see a way around that at the moment.

Currently I assume that the head height in the screw tables is the max height of a given head type. I found that for flat screws, the head height was always 0. My code treats it differently for calculating where the "head" anchor goes because by my definition it is the bottom of the head. The virtual cylinder that the screw_hole anchors are using uses head_diameter, so if you want to place a hole n mm from a wall, that should be easy. By default, the screw_hole is aligned on the Z axis so placing the center of a hole n mm from a wall is also easy.

My "top" anchor is the top of the head, wherever that is. The "head" anchor is the bottom of the head. So "head" is L above the tip of an MDxL screw. I don't have an anchor for the end of a shank, but that would be easy to add. The TOP anchor of a screw_hole is the top of the hole including the head extension.

rjcarlson49 commented 1 year ago

I've realized that extendS really only has meaning when an inline trap is wanted. ExtendS could be replaced by trapL for the inline trap case and all other screw holes could be made 1 or 2 mm longer than L. 1 is probably enough in all cases though, right?

adrianVmariano commented 1 year ago

Right now you can specify a screw with screw("M6", length=25) or you can do screw("M6,25"). If we add an "extend" argument so you have length=25, extend=2 then there will be two arguments that specify length. The head height in the tables is the height of the head above the screw, or the length the head adds to the screw length. Maybe that's a design error and it should be the actual head height. The original code never even computed the head height for flat heads. Is that value in the tables the maximum height? Maybe, but I wouldn't guarantee it. It's possible it might be the middle height height allowed by the tolerance range or typical head type or the head type of a screw sold by McMaster.

You want to be able to place the screw head next to something but also the screw shaft. I'm not sure if that means the default anchor should be a cylinder that contains the screw (or screw hole mask), or if the default should be a cylinder whose diameter is the screw diameter. And I guess it seems like there should be an anchor that on a screw of length L is distance L from the tip, but I don't know what to call that anchor. This would be on top of a flat head but underneath other heads and seems like the natural point of reference for a screw.

adrianVmariano commented 1 year ago

I think the hole should be the length the user requested. If the user wants it longer, the user can request it longer. This is not a case where excess length will vanish, unless the user is making a through hole.

If the inline trap is a separate module (which is how Revar and I think) then it will have its own length parameter, just like the sideways trap needs a length argument to specify how far it extends to the side.

rjcarlson49 commented 1 year ago

Extend (or trapL) is not relevant to screw_info, only screw_hole.

The cylinder for the anchors should be the diameter of the head. It's really hard for a user to obtain the head diameter so having anchors there helps the user. if the user really wants to reference the shaft, the shaft diameter and its center are both known.

adrianVmariano commented 1 year ago

No, actually, shaft diameter is not known. Only the nominal shaft diameter is known.

adrianVmariano commented 1 year ago

There will need to be multiple anchor types. I had actually wondered if we could make the anchor type system cleaner, but it seemed like maybe it's difficult on the implementation side. I was thinking something like anchor=["head",BOT], anchor=["threads", TOP], or whatever, as compared to the more clumsy "atype" system currently used for this.

adrianVmariano commented 1 year ago

I remembered why I didn't put the flathead height into the screw structure: the flat head height isn't known until you determine the screw diameter, and the screw structure only contains the nominal diameter, not the actual diameter, so it's impossible to compute it.

rjcarlson49 commented 1 year ago

No, actually, shaft diameter is not known. Only the nominal shaft diameter is known.

Technically, all the measurements are nominal. I don't see how that matters. The standards are mostly for maximum measurements just so you can ensure fit.

rjcarlson49 commented 1 year ago

I remembered why I didn't put the flathead height into the screw structure: the flat head height isn't known until you determine the screw diameter, and the screw structure only contains the nominal diameter, not the actual diameter, so it's impossible to compute it.

I don't understand why this matters for screw holes. The nominal screw diameter is a known maximum and the hole has to be based on that.

rjcarlson49 commented 1 year ago

There will need to be multiple anchor types. I had actually wondered if we could make the anchor type system cleaner, but it seemed like maybe it's difficult on the implementation side. I was thinking something like anchor=["head",BOT], anchor=["threads", TOP], or whatever, as compared to the more clumsy "atype" system currently used for this.

This seems needlessly complicated since I found a rational and so far very useful set of anchors based on the current system with a single anchor argument.

I don't understand what you mean by "atype" system.

rjcarlson49 commented 1 year ago

If the inline trap is a separate module (which is how Revar and I think) then it will have its own length parameter, just like the sideways trap needs a length argument to specify how far it extends to the side.

I get the impression that your desire for this is all based on wanting to enhance the existing screw code and use it for drawing screw holes. With that starting point, I don't see how you get a total system that allows you to position screw, nut, screw_hole and trap all simply and easily. The screw interface is already very complicated and hard to understand. Case in point is that I had no idea there were features intended to let you use it for screw holes.

My API for screw_holes is ONE module that creates screw_hole and trap and positions it with a wide variety of anchor points. Two other modules can position a screw or nut within that hole, all using identical arguments and anchors. I don't see how you can achieve that with what you are contemplating.

adrianVmariano commented 1 year ago

"Technically all measurements are nominal"

Huh? That doesn't make any sense. In fact, no measurement is nominal because if you have measured something then that makes it actual (with error bars if you like). A nominal dimension is a a name, hopefully with some connection to the real size of the part, like "2x4" lumber which is actually a smaller size. In the case of screws, if you request an M6 screw the model you get from screw() will not be 6mm in diameter, but will be adjusted according to the ISO tolerance formulas. The actual dimension of the screw is the size that OpenSCAD actually creates the part at. That is clearly a different thing than the nominal dimension. Note that the nominal screw diameter is also not the same as the diameter for a hole. ISO/ASME specify tolerances for this and the hole must be even larger. If you base the flat head height on nominal rather than actual diameter then the flat head will have the wrong size, or the anchor point that is supposed to be under the head won't actually be at the intersection of the head and shaft, or you could potentially end up with a small gap between the shaft and head---depending on how the implementation works. Basically something will go wrong somewhere, I think.

Avoiding this complication was actually why I originally modeled flat heads as full cones and just unioned them with the screw, but this produced bad threads at the top and for very short screws, the cone would be too long. So it wasn't really a satisfactory solution. But the head height is a derived value so if I'm going to compute the head height, I want it to be correct, not just sort of correct.

Regarding an anchor located at the underside of a flat head (top of the screw shaft): is there any use for this? Neither Revar nor I could think of one.

There needs to be a way to anchor on the side of the shaft. There needs to be a way to anchor on the side of the head. These are two inconsistent requirements. The solution we have for this is to allow you to select a different attachment geometry using atype, which is short for attachment type. So you might do screw(....,atype="shaft", anchor=RIGHT) to anchor on the right of the shaft, but screw(...,atype="head", anchor=RIGHT) to anchor on the right side of the head. There are various other places around BOSL2 where we needed multiple conflicting attachment geometries, which is why we introduced atype. But I do think it feels like a kludge, and it's inflexible because you have to do all your anchoring relative to a single anchor type. The idea above is cleaner because it combines the anchor type with the anchor location, so at least syntactically it would allow for multiple different attachment geometries to be used at once.

As far as I can tell, the complications for the screw model are there because modeling screws is complicated. Modeling screw holes basically comes for free once the screw model works, so why wouldn't I take advantage? (The one single addition needed is the table of screw hole tolerances from ASME B18.2.8 and ISO 273, and I guess forcing flatheads to have sharp heads.) I don't understand what simplifications I could make if I decided to duplicate the code and have two entirely separate modules for screws and holes. It's a negligible simplification of the screw() code and it means the hole making code is more complicated and creates a maintenance headache due to duplicated code.

I do not like the idea of the trap as part of the hole code, as I've mentioned before. When I mentioned this idea to Revar he immediately said that the trap should be a separate attachable module. I have not explored this yet to see if there is some compelling reason to bind together two separate objects like you want to do.

adrianVmariano commented 1 year ago

Note, just to be clear about screw code: all of the threading code in the whole library is intended to make threaded rods or screw masks. The idea that I got this idea to enhance screw() to make holes is not right. It's more like I realized that the capability that was supposed to be there was missing. I originally wrote the screw code a couple years ago and got burnt out on it and didn't really finish, so implementation gaps and deficiencies are no big shock.

rjcarlson49 commented 1 year ago

I do not like the idea of the trap as part of the hole code, as I've mentioned before. When I mentioned this idea to Revar he immediately said that the trap should be a separate attachable module. I have not explored this yet to see if there is some compelling reason to bind together two separate objects like you want to do.

I could easily separate the trap code from the hole code. However, the trap always attaches its TOP to the hole's "nut" anchor (in my set of anchors.) Why make the user do this if it is always the same?

adrianVmariano commented 1 year ago

It seems like I might not want the trap there. What if I have 20mm long screws and want the trap 8mm up from the bottom to work with my model?

adrianVmariano commented 1 year ago

Is it possible to use two nut traps to connect three parts together?

adrianVmariano commented 1 year ago

I suppose there's probably a better solution.

rjcarlson49 commented 1 year ago

The your gap argument would be set to 12, 20 - 8. Then "nut" gives you that spot.

rjcarlson49 commented 1 year ago

Is it possible to use two nut traps to connect three parts together?

Can't see how.

rjcarlson49 commented 1 year ago

The gap argument IS the distance between bottom of head and top of nut. By definition that's where the nut trap goes.

adrianVmariano commented 1 year ago

But this means you're making a different mechanism for positioning an object relative to another object instead of using the existing anchoring mechanism. Also in my example I want to position the nut trap relative to the bottom of the screw, not the top.