From 323a42177d914387faba0a9992b2efa2560fe347 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Tue, 2 Feb 2021 02:14:59 -0800
Subject: [PATCH 1/2] Fix manifold issues with trapezoidal_threaded_rod().

---
 threading.scad |  74 ++++++++++++++-------------
 version.scad   |   2 +-
 vnf.scad       | 134 +++++++++++++++++++++++--------------------------
 3 files changed, 101 insertions(+), 109 deletions(-)

diff --git a/threading.scad b/threading.scad
index bd77ad9..47c6db4 100644
--- a/threading.scad
+++ b/threading.scad
@@ -103,7 +103,7 @@ module thread_helix(
 //   internal = If true, make this a mask for making internal threads.
 //   d1 = Bottom outside diameter of threads.
 //   d2 = Top outside diameter of threads.
-//   higbee = Length to taper thread ends over.  Default: 0
+//   higbee = Length to taper thread ends over.  Default: 0 (No higbee thread tapering)
 //   higbee1 = Length to taper bottom thread end over.
 //   higbee2 = Length to taper top thread end over.
 //   center = If given, overrides `anchor`.  A true value sets `anchor=CENTER`, false sets `anchor=UP`.
@@ -148,90 +148,92 @@ module trapezoidal_threaded_rod(
     profile,
     internal=false,
     d1, d2,
-    higbee=0, higbee1, higbee2,
+    higbee, higbee1, higbee2,
     center, anchor, spin, orient
 ) {
     _r1 = get_radius(d1=d1, d=d, dflt=10);
     _r2 = get_radius(d1=d2, d=d, dflt=10);
     sides = quantup(segs(max(_r1,_r2)), starts);
     rsc = internal? (1/cos(180/sides) + $slop*3) : 1;
-    threads = ceil(l/pitch/starts)+(starts<4?4-starts:1);
+    threads = ceil(l/pitch/starts) + 2;
+    ll = threads * pitch * starts;
     depth = min((thread_depth==undef? pitch/2 : thread_depth), pitch/2/tan(thread_angle));
     pa_delta = min(pitch/4-0.01,depth*tan(thread_angle)/2)/pitch;
     dir = left_handed? -1 : 1;
     twist = 360 * l / pitch / starts;
-    higbee1 = first_defined([higbee1, higbee, 0]);
-    higbee2 = first_defined([higbee2, higbee, 0]);
-    higang1 = 360 * higbee1 / (2 * _r1 * PI);
-    higang2 = 360 * higbee2 / (2 * _r2 * PI);
-    higsteps1 = ceil(higang1/360*sides);
-    higsteps2 = ceil(higang2/360*sides);
+    _higbee1 = first_defined([higbee1, higbee, 0]);
+    _higbee2 = first_defined([higbee2, higbee, 0]);
+    higang1 = 360 * _higbee1 / (2 * PI * _r1);
+    higang2 = 360 * _higbee2 / (2 * PI * _r2);
     assert(higang1 < twist/2);
     assert(higang2 < twist/2);
 
+    higstart = twist/2 + 360/starts/4;
     higbee_table = [
-        [-twist,           0.01],
-        [-twist/2,         0.01],
-        [-twist/2+higang1, 1],
-        [ twist/2-higang2, 1],
-        [ twist/2,         0.01],
-        [ twist,           0.01]
+        [-higstart*2,       0.01],
+        [-higstart-0.001,   0.01],
+        [-higstart+higang1, 1   ],
+        [+higstart-higang2, 1   ],
+        [+higstart+0.001,   0.01],
+        [+higstart*2,       0.01]
     ];
+    echo(higbee_table);
 
     r1 = -depth/pitch;
     z1 = 1/4-pa_delta;
     z2 = 1/4+pa_delta;
-    profile = profile!=undef? profile : [
-        [-z2, r1],
-        [-z1,  0],
-        [ z1,  0],
-        [ z2, r1],
-    ];
+    profile = pitch * (
+        profile!=undef? profile : [
+            [-z2, r1],
+            [-z1,  0],
+            [ z1,  0],
+            [ z2, r1],
+        ]
+    );
     pdepth = -min(subindex(profile,1));
     eprofile = [
-        [-0.5, 0],
         each move([0,pdepth], p=profile),
-        [ 0.5, 0],
-    ] * pitch;
+        move([pitch,pdepth], p=profile[0]),
+    ];
     angstep = 360 / sides;
-    angsteps = ceil(twist / (360 / sides)) + sides;
+    angsteps = ceil(sides * (twist / 360 + 2));
     zang = atan2(_r2-_r1,l);
     thread_verts = [
-        [for (x = eprofile) [0,0,-l/2]],
-        for (a = [0:1:angsteps]) let (
-            u = (a-angsteps/2) / (angsteps-sides),
-            ang = u * twist,
+        [for (i = idx(eprofile)) [0,0,-ll/2]],
+        for (thread = [0:1:threads-1], side=[0:1:sides-1]) let(
+            ang = ((thread - threads/2) + (side / sides)) * 360,
+            u = ang / twist,
             r = lerp(_r1, _r2, u) * rsc,
             hsc = higbee1==0 && higbee2==0? 1 : lookup(ang, higbee_table),
             mat = affine3d_zrot(ang*dir) *
-                affine3d_translate([r-pdepth*pitch, 0, l*u-0*pitch]) *
+                affine3d_translate([r-pdepth*pitch, 0, l*u]) *
                 affine3d_xrot(90) *
                 affine3d_skew_xz(xa=zang) * 
                 affine3d_mirror([-1,1]) *
                 affine3d_scale([1,hsc,1]),
             pts = apply(mat, path3d(eprofile))
         ) pts,
-        [for (x = eprofile) [0,0, l/2]],
+        [for (x = eprofile) [0,0,+ll/2]],
     ];
     thread_vnf = vnf_vertex_array(thread_verts, reverse=left_handed);
     eplen = len(eprofile);
     vlen = len(thread_vnf[0]);
     thread_vnf2 = [
-        concat(thread_vnf[0], [[0,0,-l/2], [0,0,l/2]]),
+        concat(thread_vnf[0], [[0,0,-ll/2], [0,0,+ll/2]]),
         concat(thread_vnf[1], [
             for (i = [0:1:sides/starts]) each
             left_handed? [
                 [eplen*(i+1), eplen*i, vlen],
-                [vlen-eplen*(i+1)-1, vlen-eplen*i-1, vlen+1]
+                [vlen-eplen*(i+1)-1, vlen-eplen*(i+0)-1, vlen+1]
             ] : [
-                [eplen*i,        eplen*(i+1),      vlen],
-                [vlen-eplen*i-1, vlen-eplen*(i+1)-1, vlen+1]
+                [eplen*i, eplen*(i+1), vlen],
+                [vlen-eplen*(i+0)-1, vlen-eplen*(i+1)-1, vlen+1]
             ]
         ])
     ];
     thread_vnfs = vnf_merge([
         for (start = [0:1:starts-1]) zrot(start*360/starts, p=thread_vnf2)
-    ]);
+    ], cleanup=true);
     anchor = get_anchor(anchor, center, BOT, CENTER);
     attachable(anchor,spin,orient, r1=_r1, r2=_r2, l=l) {
         difference() {
diff --git a/version.scad b/version.scad
index 429cd19..47a6dfe 100644
--- a/version.scad
+++ b/version.scad
@@ -6,7 +6,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,555];
+BOSL_VERSION = [2,0,556];
 
 
 // Section: BOSL Library Version Functions
diff --git a/vnf.scad b/vnf.scad
index 9e5ada8..98a1767 100644
--- a/vnf.scad
+++ b/vnf.scad
@@ -145,18 +145,31 @@ function vnf_add_faces(vnf=EMPTY_VNF, faces) =
 
 // Function: vnf_merge()
 // Usage:
-//   vnf = vnf_merge([VNF, VNF, VNF, ...]);
+//   vnf = vnf_merge([VNF, VNF, VNF, ...], <cleanup>);
 // Description:
 //   Given a list of VNF structures, merges them all into a single VNF structure.
-function vnf_merge(vnfs=[],_i=0,_acc=EMPTY_VNF) =
-    (assert(is_vnf_list(vnfs)) _i>=len(vnfs))? _acc :
-    vnf_merge(
-        vnfs, _i=_i+1,
-        _acc = let(base=len(_acc[0])) [
-            concat(_acc[0], vnfs[_i][0]),
-            concat(_acc[1], [for (f=vnfs[_i][1]) [for (i=f) i+base]]),
+function vnf_merge(vnfs, cleanup=false) =
+    let (
+        offs = cumsum([
+            0, for (vnf = vnfs) len(vnf[0])
+        ])
+    ) [
+        [for (vnf=vnfs) each vnf[0]],
+        [
+            for (i = idx(vnfs)) let(
+                vnf = vnfs[i],
+                verts = vnf[0],
+                faces = vnf[1]
+            )
+            for (face = faces) let(
+                dface = !cleanup ? face :
+                    deduplicate_indexed(verts, face, closed=true)
+            )
+            if (len(dface) >= 3)
+            [ for (j = dface) offs[i] + j ]
         ]
-    );
+    ];
+
 
 // Function: vnf_compact()
 // Usage:
@@ -281,7 +294,7 @@ function vnf_vertex_array(
         rowcnt = rows - (row_wrap?0:1)
     )
     rows<=1 || cols<=1 ? vnf : 
-    vnf_merge([
+    vnf_merge(cleanup=true, [
         vnf, [
             concat(
                 pts,
@@ -681,13 +694,8 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
         uniq_edges = edgecnts[0],
         big_faces = !show_warns? [] : [
             for (face = faces)
-            if (len(face) > 3) [
-                "WARNING",
-                "BIG_FACE",
-                "Face has more than 3 vertices, and may confuse CGAL",
-                [for (i=face) varr[i]],
-                "yellow"
-            ]
+            if (len(face) > 3)
+            _vnf_validate_err("BIG_FACE", [for (i=face) varr[i]])
         ],
         null_faces = !show_warns? [] : [
             for (face = faces) let(
@@ -696,13 +704,9 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
             if (len(face)>=3) let(
                 faceverts = [for (k=face) varr[k]],
                 area = polygon_area(faceverts)
-            ) if (is_num(area) && abs(area) < EPSILON) [
-                "WARNING",
-                "NULL_FACE",
-                str("Face has zero area: ",fmt_float(abs(area),15)),
-                faceverts,
-                "brown"
-            ]
+            )
+            if (is_num(area) && abs(area) < EPSILON)
+            _vnf_validate_err("NULL_FACE", faceverts)
         ],
         nonplanars = unique([
             for (face = faces) let(
@@ -710,23 +714,13 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
                 area = polygon_area(faceverts)
             )
             if (is_num(area) && abs(area) > EPSILON)
-            if (!coplanar(faceverts)) [
-                "ERROR",
-                "NONPLANAR",
-                "Face vertices are not coplanar",
-                faceverts,
-                "cyan"
-            ]
+            if (!coplanar(faceverts))
+            _vnf_validate_err("NONPLANAR", faceverts)
         ]),
         overpop_edges = unique([
             for (i=idx(uniq_edges))
-            if (edgecnts[1][i]>2) [
-                "ERROR",
-                "OVRPOP_EDGE",
-                "Too many faces attached at Edge",
-                [for (i=uniq_edges[i]) varr[i]],
-                "#f70"
-            ]
+            if (edgecnts[1][i]>2)
+            _vnf_validate_err("OVRPOP_EDGE", [for (i=uniq_edges[i]) varr[i]])
         ]),
         reversals = unique([
             for(i = idx(faces), j = idx(faces)) if(i != j)
@@ -736,13 +730,7 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
             for(edge2 = pair(faces[j],true))
             if(edge1 == edge2)  // Valid adjacent faces will never have the same vertex ordering.
             if(_edge_not_reported(edge1, varr, overpop_edges))
-            [
-                "ERROR",
-                "REVERSAL",
-                "Faces Reverse Across Edge",
-                [for (i=edge1) varr[i]],
-                "violet"
-            ]
+            _vnf_validate_err("REVERSAL", [for (i=edge1) varr[i]])
         ]),
         t_juncts = unique([
             for (v=idx(varr), edge=uniq_edges)
@@ -754,13 +742,8 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
             if (a != b && b != c && a != c) let(
                 pt = segment_closest_point([a,c],b)
             )
-            if (pt == b) [
-                "ERROR",
-                "T_JUNCTION",
-                "Vertex is mid-edge on another Face",
-                [b],
-                "red"
-            ]
+            if (pt == b)
+            _vnf_validate_err("T_JUNCTION", [b])
         ]),
         isect_faces = !check_isects? [] : unique([
             for (i = [0:1:len(faces)-2])
@@ -791,26 +774,15 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
             )
             if (!is_undef(isects2))
             for (seg=isects2)
-            if (seg[0] != seg[1]) [
-                "ERROR",
-                "FACE_ISECT",
-                "Faces intersect",
-                seg,
-                "blue"
-            ]
+            if (seg[0] != seg[1])
+            _vnf_validate_err("FACE_ISECT", seg)
         ]),
         hole_edges = unique([
             for (i=idx(uniq_edges))
             if (edgecnts[1][i]<2)
             if (_pts_not_reported(uniq_edges[i], varr, t_juncts))
             if (_pts_not_reported(uniq_edges[i], varr, isect_faces))
-            [
-                "ERROR",
-                "HOLE_EDGE",
-                "Edge bounds Hole",
-                [for (i=uniq_edges[i]) varr[i]],
-                "magenta"
-            ]
+            _vnf_validate_err("HOLE_EDGE", [for (i=uniq_edges[i]) varr[i]])
         ])
     ) concat(
         big_faces,
@@ -824,6 +796,24 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) =
     );
 
 
+_vnf_validate_errs = [
+    ["BIG_FACE",    "WARNING", "cyan",    "Face has more than 3 vertices, and may confuse CGAL"],
+    ["NULL_FACE",   "WARNING", "blue",    "Face has zero area."],
+    ["NONPLANAR",   "ERROR",   "yellow",  "Face vertices are not coplanar"],
+    ["OVRPOP_EDGE", "ERROR",   "orange",  "Too many faces attached at Edge"],
+    ["REVERSAL",    "ERROR",   "violet",  "Faces Reverse Across Edge"],
+    ["T_JUNCTION",  "ERROR",   "magenta", "Vertex is mid-edge on another Face"],
+    ["FACE_ISECT",  "ERROR",   "brown",   "Faces intersect"],
+    ["HOLE_EDGE",   "ERROR",   "red",     "Edge bounds Hole"]
+];
+
+
+function _vnf_validate_err(name, extra) =
+    let(
+        info = [for (x = _vnf_validate_errs) if (x[0] == name) x][0]
+    ) concat(info, [extra]);
+
+
 function _pts_not_reported(pts, varr, reports) =
     [
         for (i = pts, report = reports, pt = report[3])
@@ -847,12 +837,12 @@ module vnf_validate(vnf, size=1, show_warns=true, check_isects=false) {
         check_isects=check_isects
     );
     for (fault = faults) {
-        typ = fault[0];
-        err = fault[1];
-        msg = fault[2];
-        pts = fault[3];
-        clr = fault[4];
-        echo(str(typ, " ", err, ": ", msg, " at ", pts));
+        err = fault[0];
+        typ = fault[1];
+        clr = fault[2];
+        msg = fault[3];
+        pts = fault[4];
+        echo(str(typ, " ", err, " (", clr ,"): ", msg, " at ", pts));
         color(clr) {
             if (len(pts)==2) {
                 stroke(pts, width=size);

From 624f6fa2587e80d14300ed6a9fe7807ed594be41 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Tue, 2 Feb 2021 02:33:32 -0800
Subject: [PATCH 2/2] Fix python Pillow install.

---
 .github/workflows/main.yml | 11 +----------
 version.scad               |  2 +-
 2 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 58aabc3..d3bd39f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -48,17 +48,8 @@ jobs:
         cd $GITHUB_WORKSPACE
         git clone https://github.com/revarbat/BOSL2.wiki.git
 
-    - name: Install Python pip
-      run: sudo apt-get install python3-pip
-
     - name: Install Python dev
-      run: sudo apt-get install python3-dev python3-setuptools
-
-    - name: Install Pillow Dependencies
-      run: sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
-
-    - name: Install Pillow
-      run: sudo pip3 install Pillow
+      run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil
 
     - name: Install OpenSCAD
       run: |
diff --git a/version.scad b/version.scad
index 47a6dfe..e1d0bb0 100644
--- a/version.scad
+++ b/version.scad
@@ -6,7 +6,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,556];
+BOSL_VERSION = [2,0,557];
 
 
 // Section: BOSL Library Version Functions