From 775e0c4c48697afd1673e42be7ce398972c3f202 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Tue, 13 Oct 2020 22:32:20 -0700
Subject: [PATCH] Fixed bevel_gear() so that complementary gears will mesh.

---
 geometry.scad       |  73 +++++++++-
 involute_gears.scad | 339 +++++++++++++++++++++-----------------------
 version.scad        |   2 +-
 3 files changed, 232 insertions(+), 182 deletions(-)

diff --git a/geometry.scad b/geometry.scad
index 6a69e11..b18456f 100644
--- a/geometry.scad
+++ b/geometry.scad
@@ -472,6 +472,73 @@ function line_from_points(points, fast=false, eps=EPSILON) =
 
 // Section: 2D Triangles
 
+
+// Function: law_of_cosines()
+// Usage:
+//   C = law_of_cosines(a, b, c);
+//   c = law_of_cosines(a, b, C);
+// Description:
+//   Applies the Law of Cosines for an arbitrary triangle.
+//   Given three side lengths, returns the angle in degrees for the corner opposite of the third side.
+//   Given two side lengths, and the angle between them, returns the length of the third side.
+// Figure(2D):
+//   stroke([[-50,0], [10,60], [50,0]], closed=true);
+//   color("black") {
+//       translate([ 33,35]) text(text="a", size=8, halign="center", valign="center");
+//       translate([  0,-6]) text(text="b", size=8, halign="center", valign="center");
+//       translate([-22,35]) text(text="c", size=8, halign="center", valign="center");
+//   }
+//   color("blue") {
+//       translate([-37, 6]) text(text="A", size=8, halign="center", valign="center");
+//       translate([  9,51]) text(text="B", size=8, halign="center", valign="center");
+//       translate([ 38, 6]) text(text="C", size=8, halign="center", valign="center");
+//   }
+// Arguments:
+//   a = The length of the first side.
+//   b = The length of the second side.
+//   c = The length of the third side.
+//   C = The angle in degrees of the corner opposite of the third side.
+function law_of_cosines(a, b, c, C) =
+    // Triangle Law of Cosines:
+    //   c^2 = a^2 + b^2 - 2*a*b*cos(C)
+    assert(num_defined([c,C]) == 1, "Must give exactly one of c= or C=.")
+    is_undef(c) ? sqrt(a*a + b*b - 2*a*b*cos(C)) :
+    acos(constrain((a*a + b*b - c*c) / (2*a*b), -1, 1));
+
+
+// Function: law_of_sines()
+// Usage:
+//   B = law_of_sines(a, A, b);
+//   b = law_of_sines(a, A, B);
+// Description:
+//   Applies the Law of Sines for an arbitrary triangle.
+//   Given two triangle side lengths and the angle between them, returns the angle of the corner opposite of the second side.
+//   Given a side length, the opposing angle, and a second angle, returns the length of the side opposite of the second angle.
+// Figure(2D):
+//   stroke([[-50,0], [10,60], [50,0]], closed=true);
+//   color("black") {
+//       translate([ 33,35]) text(text="a", size=8, halign="center", valign="center");
+//       translate([  0,-6]) text(text="b", size=8, halign="center", valign="center");
+//       translate([-22,35]) text(text="c", size=8, halign="center", valign="center");
+//   }
+//   color("blue") {
+//       translate([-37, 6]) text(text="A", size=8, halign="center", valign="center");
+//       translate([  9,51]) text(text="B", size=8, halign="center", valign="center");
+//       translate([ 38, 6]) text(text="C", size=8, halign="center", valign="center");
+//   }
+// Arguments:
+//   a = The length of the first side.
+//   A = The angle in degrees of the corner opposite of the first side.
+//   b = The length of the second side.
+//   B = The angle in degrees of the corner opposite of the second side.
+function law_of_sines(a, A, b, B) =
+    // Triangle Law of Sines:
+    //   a/sin(A) = b/sin(B) = c/sin(C)
+    assert(num_defined([b,B]) == 1, "Must give exactly one of b= or B=.")
+    let( r = a/sin(A) )
+    is_undef(b) ? r*sin(B) : asin(constrain(b/r, -1, 1));
+
+
 // Function: tri_calc()
 // Usage:
 //   tri_calc(ang,ang2,adj,opp,hyp);
@@ -750,8 +817,8 @@ function adj_opp_to_ang(adj,opp) =
 function triangle_area(a,b,c) = 
     assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." )    
     len(a)==3 
-    ? 0.5*norm(cross(c-a,c-b)) 
-    : 0.5*cross(c-a,c-b);
+      ? 0.5*norm(cross(c-a,c-b)) 
+      : 0.5*cross(c-a,c-b);
 
 
 
@@ -816,7 +883,7 @@ function plane3pt_indexed(points, i1, i2, i3) =
 function plane_from_normal(normal, pt=[0,0,0]) =
   assert( is_matrix([normal,pt],2,3) && !approx(norm(normal),0), 
           "Inputs `normal` and `pt` should 3d vectors/points and `normal` cannot be zero." )
-  concat(normal, normal*pt)/norm(normal);
+  concat(normal, normal*pt) / norm(normal);
 
 
 // Function: plane_from_points()
diff --git a/involute_gears.scad b/involute_gears.scad
index a424eac..ababec9 100644
--- a/involute_gears.scad
+++ b/involute_gears.scad
@@ -130,12 +130,12 @@ function base_radius(pitch=5, teeth=11, PA=28) =
 // Usage:
 //   x = bevel_pitch_angle(teeth, mate_teeth, [drive_angle]);
 // Description:
-//   Returns the correct pitch angle (bevelang) for a bevel gear with a given number of tooth, that is
+//   Returns the correct pitch angle for a bevel gear with a given number of tooth, that is
 //   matched to another bevel gear with a (possibly different) number of teeth.
 // Arguments:
 //   teeth = Number of teeth that this gear has.
 //   mate_teeth = Number of teeth that the matching gear has.
-//   drive_angle = Angle between the drive shafts of each gear.  Usually 90º.
+//   drive_angle = Angle between the drive shafts of each gear.  Default: 90º.
 function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) =
     atan(sin(drive_angle)/((mate_teeth/teeth)+cos(drive_angle)));
 
@@ -160,15 +160,18 @@ function _gear_q7(f,r,b,r2,t,s) = _gear_q6(b,s,t,(1-f)*max(b,r)+f*r2);        //
 // Arguments:
 //   pitch = The circular pitch, or distance between teeth around the pitch circle, in mm.
 //   teeth = Total number of teeth along the rack
-//   PA = Controls how straight or bulged the tooth sides are. In degrees.
+//   PA = Pressure Angle.  Controls how straight or bulged the tooth sides are. In degrees.
 //   clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear (in millimeters)
 //   backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle
 //   interior = If true, create a mask for difference()ing from something else.
-//   valleys = If true, add the valley bottoms on either side of the tooth.
+//   valleys = If true, add the valley bottoms on either side of the tooth.  Default: true
+//   center = If true, centers the pitch circle of the tooth profile at the origin.  Default: false.
 // Example(2D):
 //   gear_tooth_profile(pitch=5, teeth=20, PA=20);
 // Example(2D):
-//   gear_tooth_profile(pitch=5, teeth=20, PA=20, valleys=true);
+//   gear_tooth_profile(pitch=5, teeth=20, PA=20, valleys=false);
+// Example(2D): As a function
+//   stroke(gear_tooth_profile(pitch=5, teeth=20, PA=20, valleys=false));
 function gear_tooth_profile(
     pitch     = 3,
     teeth     = 11,
@@ -176,7 +179,8 @@ function gear_tooth_profile(
     clearance = undef,
     backlash  = 0.0,
     interior  = false,
-    valleys   = true
+    valleys   = true,
+    center    = false
 ) = let(
     p = pitch_radius(pitch, teeth),
     c = outer_radius(pitch, teeth, clearance, interior),
@@ -186,23 +190,22 @@ function gear_tooth_profile(
     k  = -_gear_iang(b, p) - t/2/p/PI*180,  //angle to where involute meets base circle on each side of tooth
     kk = r<b? k : -180/teeth,
     isteps = 5,
-    pts = concat(
-        valleys? [
-            _gear_polar(r-1, -180.1/teeth),
-            _gear_polar(r, -180.1/teeth),
-        ] : [
-        ],
-        [_gear_polar(r, kk)],
-        [for (i=[0: 1:isteps]) _gear_q7(i/isteps,r,b,c,k, 1)],
-        [for (i=[isteps:-1:0]) _gear_q7(i/isteps,r,b,c,k,-1)],
-        [_gear_polar(r, -kk)],
-        valleys? [
-            _gear_polar(r, 180.1/teeth),
+    pts = [
+        if (valleys) each [
             _gear_polar(r-1, 180.1/teeth),
-        ] : [
+            _gear_polar(r, 180.1/teeth),
+        ],
+        _gear_polar(r, -kk),
+        for (i=[0: 1:isteps]) _gear_q7(i/isteps,r,b,c,k,-1),
+        for (i=[isteps:-1:0]) _gear_q7(i/isteps,r,b,c,k, 1),
+        _gear_polar(r, kk),
+        if (valleys) each [
+            _gear_polar(r, -180.1/teeth),
+            _gear_polar(r-1, -180.1/teeth),
         ]
-    )
-) reverse(pts);
+    ],
+    pts2 = center? fwd(p, p=pts) : pts
+) pts2;
 
 
 module gear_tooth_profile(
@@ -212,7 +215,8 @@ module gear_tooth_profile(
     backlash  = 0.0,
     clearance = undef,
     interior  = false,
-    valleys   = true
+    valleys   = true,
+    center    = false
 ) {
     r = root_radius(pitch, teeth, clearance, interior);
     translate([0,-r,0])
@@ -224,7 +228,8 @@ module gear_tooth_profile(
             backlash  = backlash,
             clearance = clearance,
             interior  = interior,
-            valleys   = valleys
+            valleys   = valleys,
+            center    = center
         )
     );
 }
@@ -359,7 +364,7 @@ module gear2d(
 //   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#orient).  Default: `UP`
 // Example: Spur Gear
 //   gear(pitch=5, teeth=20, thickness=8, shaft_diam=5);
-// Example: Beveled Gear
+// Example: Helical Gear
 //   gear(pitch=5, teeth=20, thickness=10, shaft_diam=5, helical=-30, slices=12, $fa=1, $fs=1);
 // Example: Assembly of Gears
 //   n1 = 11; //red gear number of teeth
@@ -426,38 +431,28 @@ module gear(
 
 // Module: bevel_gear()
 // Usage:
-//   bevel_gear(pitch, teeth, face_width, bevelang, <shaft_diam>, <hide>, <PA>, <clearance>, <backlash>, <spiral_rad>, <spiral_ang>, <slices>, <interior>);
+//   bevel_gear(pitch, teeth, face_width, pitch_angle, <shaft_diam>, <hide>, <PA>, <clearance>, <backlash>, <cutter_radius>, <spiral_angle>, <slices>, <interior>);
 // Description:
-//   Creates a (potentially spiral) bevel gear.
-//   The module `bevel_gear()` gives an bevel gear, with reasonable
-//   defaults for all the parameters.  Normally, you should just choose
-//   the first 4 parameters, and let the rest be default values.  The
-//   module `bevel_gear()` gives a gear in the XY plane, centered on the origin,
-//   with one tooth centered on the positive Y axis.  The various functions
-//   below it take the same parameters, and return various measurements
-//   for the gear.  The most important is `pitch_radius()`, which tells
-//   how far apart to space gears that are meshing, and `outer_radius()`,
-//   which gives the size of the region filled by the gear.  A gear has
-//   a "pitch circle", which is an invisible circle that cuts through
-//   the middle of each tooth (though not the exact center). In order
-//   for two gears to mesh, their pitch circles should just touch.  So
-//   the distance between their centers should be `pitch_radius()` for
-//   one, plus `pitch_radius()` for the other, which gives the radii of
-//   their pitch circles.
-//   In order for two gears to mesh, they must have the same `pitch`
-//   and `PA` parameters.  `pitch` gives the number
-//   of millimeters of arc around the pitch circle covered by one tooth
-//   and one space between teeth.  The `PA` controls how flat or
-//   bulged the sides of the teeth are.  Common values include 14.5
-//   degrees and 20 degrees, and occasionally 25.  Though I've seen 28
-//   recommended for plastic gears. Larger numbers bulge out more, giving
-//   stronger teeth, so 28 degrees is the default here.
-//   The ratio of `teeth` for two meshing gears gives how many
-//   times one will make a full revolution when the the other makes one
-//   full revolution.  If the two numbers are coprime (i.e.  are not
-//   both divisible by the same number greater than 1), then every tooth
-//   on one gear will meet every tooth on the other, for more even wear.
-//   So coprime numbers of teeth are good.
+//   Creates a (potentially spiral) bevel gear.  The module `bevel_gear()` gives a bevel gear, with
+//   reasonable defaults for all the parameters.  Normally, you should just choose the first 4
+//   parameters, and let the rest be default values.  The module `bevel_gear()` gives a gear in the XY
+//   plane, centered on the origin, with one tooth centered on the positive Y axis.  The various
+//   functions below it take the same parameters, and return various measurements for the gear.  The
+//   most important is `pitch_radius()`, which tells how far apart to space gears that are meshing,
+//   and `outer_radius()`, which gives the size of the region filled by the gear.  A gear has a "pitch
+//   circle", which is an invisible circle that cuts through the middle of each tooth (though not the
+//   exact center). In order for two gears to mesh, their pitch circles should just touch.  So the
+//   distance between their centers should be `pitch_radius()` for one, plus `pitch_radius()` for the
+//   other, which gives the radii of their pitch circles.  In order for two gears to mesh, they must
+//   have the same `pitch` and `PA` parameters.  `pitch` gives the number of millimeters of arc around
+//   the pitch circle covered by one tooth and one space between teeth.  The `PA` controls how flat or
+//   bulged the sides of the teeth are.  Common values include 14.5 degrees and 20 degrees, and
+//   occasionally 25.  Though I've seen 28 recommended for plastic gears. Larger numbers bulge out
+//   more, giving stronger teeth, so 28 degrees is the default here.  The ratio of `teeth` for two
+//   meshing gears gives how many times one will make a full revolution when the the other makes one
+//   full revolution.  If the two numbers are coprime (i.e.  are not both divisible by the same number
+//   greater than 1), then every tooth on one gear will meet every tooth on the other, for more even
+//   wear.  So coprime numbers of teeth are good.
 // Arguments:
 //   pitch = The circular pitch, or distance between teeth around the pitch circle, in mm.
 //   teeth = Total number of teeth around the entire perimeter
@@ -467,139 +462,127 @@ module gear(
 //   PA = Controls how straight or bulged the tooth sides are. In degrees.
 //   clearance = Clearance gap at the bottom of the inter-tooth valleys.
 //   backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle
-//   bevelang = Angle of beveled gear face.
-//   spiral_rad = Radius of spiral arc for teeth.  If 0, then gear will not be spiral.  Default: 0
-//   spiral_ang = The base angle for spiral teeth.  Default: 0
-//   slices = Number of vertical layers to divide gear into.  Useful for refining gears with `spiral`.
-//   scale = Scale of top of gear compared to bottom.  Useful for making crown gears.
+//   pitch_angle = Angle of beveled gear face.
+//   cutter_radius = Radius of spiral arc for teeth.  If 0, then gear will not be spiral.  Default: 0
+//   spiral_angle = The base angle for spiral teeth.  Default: 0
+//   slices = Number of vertical layers to divide gear into.  Useful for refining gears with `spiral`.  Default: 1
 //   interior = If true, create a mask for difference()ing from something else.
 //   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#anchor).  Default: `CENTER`
 //   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#spin).  Default: `0`
 //   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#orient).  Default: `UP`
+// Extra Anchors:
+//   "pitchbase" = At the natural height of the pitch radius of the beveled gear.
+//   "flattop" = At the top of the flat top of the bevel gear.
 // Example: Beveled Gear
-//   bevel_gear(pitch=5, teeth=36, face_width=10, shaft_diam=5, spiral_rad=-20, spiral_ang=35, bevelang=45, slices=12, $fa=1, $fs=1);
+//   bevel_gear(pitch=5, teeth=36, face_width=10, shaft_diam=5, pitch_angle=45, spiral_angle=0);
+// Example: Spiral Beveled Gear and Pinion
+//   t1 = 14;
+//   t2 = 32;
+//   a1 = atan(t1/t2);
+//   a2 = atan(t2/t1);
+//   down(pitch_radius(5, t1))
+//       zrot(180/t2) bevel_gear(pitch=5, teeth=t2, face_width=10, shaft_diam=6, pitch_angle=a2, left_handed=true, slices=8, orient=UP);
+//   back(pitch_radius(5, t2))
+//       bevel_gear(pitch=5, teeth=t1, face_width=10, shaft_diam=6, pitch_angle=a1, slices=8, orient=FWD);
 module bevel_gear(
-    pitch      = 3,
-    teeth      = 11,
-    face_width = 6,
-    bevelang   = 45,
-    shaft_diam = 3,
-    hide       = 0,
-    PA         = 20,
-    clearance  = undef,
-    backlash   = 0.0,
-    spiral_rad = 0,
-    spiral_ang = 0,
-    slices     = 2,
-    interior   = false,
-    anchor     = CENTER,
-    spin       = 0,
-    orient     = UP
+    pitch       = 3,
+    teeth       = 11,
+    face_width  = 6,
+    pitch_angle = 45,
+    shaft_diam  = 3,
+    hide        = 0,
+    PA          = 20,
+    clearance   = undef,
+    backlash    = 0.0,
+    cutter_radius  = 30,
+    spiral_angle = 35,
+    left_handed = false,
+    slices      = 1,
+    interior    = false,
+    anchor      = "pitchbase",
+    spin        = 0,
+    orient      = UP
 ) {
-    thickness = face_width * cos(bevelang);
-    slices = spiral_rad==0? 1 : slices;
-    spiral_rad = spiral_rad==0? 10000 : spiral_rad;
-    p1 = pitch_radius(pitch, teeth);
-    r1 = root_radius(pitch, teeth, clearance, interior);
-    c1 = outer_radius(pitch, teeth, clearance, interior);
-    dx = thickness * tan(bevelang);
-    dy = (p1-r1) * sin(bevelang);
-    scl = (p1-dx)/p1;
-    p2 = pitch_radius(pitch*scl, teeth);
-    r2 = root_radius(pitch*scl, teeth, clearance, interior);
-    c2 = outer_radius(pitch*scl, teeth, clearance, interior);
-    slice_u = 1/slices;
-    Rm = (p1+p2)/2;
-    H = spiral_rad * cos(spiral_ang);
-    V = Rm - abs(spiral_rad) * sin(spiral_ang);
-    spiral_cp = [H,V,0];
-    S = norm(spiral_cp);
-    theta_r = acos((S*S+spiral_rad*spiral_rad-p1*p1)/(2*S*spiral_rad)) - acos((S*S+spiral_rad*spiral_rad-p2*p2)/(2*S*spiral_rad));
-    theta_ro = acos((S*S+spiral_rad*spiral_rad-p1*p1)/(2*S*spiral_rad)) - acos((S*S+spiral_rad*spiral_rad-Rm*Rm)/(2*S*spiral_rad));
-    theta_ri = theta_r - theta_ro;
-    extent_u = 2*(p2-r2)*tan(bevelang) / thickness;
-    slice_us = concat(
-        [for (u = [0:slice_u:1+extent_u]) u]
+    slices = cutter_radius==0? 1 : slices;
+    pr = pitch_radius(pitch, teeth);
+    rr = root_radius(pitch, teeth, clearance, interior);
+    pitchoff = (pr-rr) * cos(pitch_angle);
+    ocone_rad = opp_ang_to_hyp(pr, pitch_angle);
+    icone_rad = ocone_rad - face_width;
+    cutter_radius = cutter_radius==0? 1000 : cutter_radius;
+    midpr = (icone_rad + ocone_rad) / 2;
+    radcp = [0, midpr] + polar_to_xy(cutter_radius, 180+spiral_angle);
+    angC1 = law_of_cosines(a=cutter_radius, b=norm(radcp), c=ocone_rad);
+    angC2 = law_of_cosines(a=cutter_radius, b=norm(radcp), c=icone_rad);
+    radcpang = vang(radcp);
+    sang = radcpang - (180-angC1);
+    eang = radcpang - (180-angC2);
+    slice_us = [for (i=[0:1:slices]) i/slices];
+    apts = [for (u=slice_us) radcp + polar_to_xy(cutter_radius, lerp(sang,eang,u))];
+    polars = [for (p=apts) [vang(p)-90, norm(p)]];
+    profile = gear_tooth_profile(
+        pitch     = pitch,
+        teeth     = teeth,
+        PA        = PA,
+        clearance = clearance,
+        backlash  = backlash,
+        interior  = interior,
+        valleys   = false,
+        center    = true
     );
-    lsus = len(slice_us);
-    vertices = concat(
-        [
-            for (u=slice_us, tooth=[0:1:teeth-1]) let(
-                p = lerp(p1,p2,u),
-                r = lerp(r1,r2,u),
-                theta = lerp(-theta_ro, theta_ri, u),
-                profile = gear_tooth_profile(
-                    pitch     = pitch*(p/p1),
-                    teeth     = teeth,
-                    PA        = PA,
-                    clearance = clearance,
-                    backlash  = backlash,
-                    interior  = interior,
-                    valleys   = false
-                ),
-                pp = rot(theta, cp=spiral_cp, p=[0,Rm,0]),
-                ang = atan2(pp.y,pp.x)-90,
-                pts = apply_list(
-                    path3d(profile), [
-                        move([0,-p,0]),
-                        rot([0,ang,0]),
-                        rot([bevelang,0,0]),
-                        move(pp),
-                        rot(tooth*360/teeth),
-                        move([0,0,thickness*u])
-                    ]
-                )
-            ) each pts
-        ], [
-            [0,0,-dy], [0,0,thickness]
+    verts1 = [
+        for (polar=polars) [
+            let(
+                u = polar.y / ocone_rad,
+                m = up((1-u) * pr / tan(pitch_angle)) *
+                    up(2*pitchoff) *
+                    zrot(polar.x/sin(pitch_angle)) *
+                    back(u * pr) *
+                    xrot(pitch_angle) *
+                    scale(u)
+            )
+            for (tooth=[0:1:teeth-1])
+            each apply(xflip() * zrot(360*tooth/teeth) * m, path3d(profile))
         ]
-    );
-    lcnt = (len(vertices)-2)/lsus/teeth;
-    function _gv(layer,tooth,i) = ((layer*teeth)+(tooth%teeth))*lcnt+(i%lcnt);
-    function _lv(layer,i) = layer*teeth*lcnt+(i%(teeth*lcnt));
-    faces = concat(
-        [
-            for (sl=[0:1:lsus-2], i=[0:1:lcnt*teeth-1]) each [
-                [_lv(sl,i), _lv(sl+1,i), _lv(sl,i+1)],
-                [_lv(sl+1,i), _lv(sl+1,i+1), _lv(sl,i+1)]
-            ]
-        ], [
-            for (tooth=[0:1:teeth-1], i=[0:1:lcnt/2-1]) each [
-                [_gv(0,tooth,i), _gv(0,tooth,i+1), _gv(0,tooth,lcnt-1-(i+1))],
-                [_gv(0,tooth,i), _gv(0,tooth,lcnt-1-(i+1)), _gv(0,tooth,lcnt-1-i)],
-                [_gv(lsus-1,tooth,i), _gv(lsus-1,tooth,lcnt-1-(i+1)), _gv(lsus-1,tooth,i+1)],
-                [_gv(lsus-1,tooth,i), _gv(lsus-1,tooth,lcnt-1-i), _gv(lsus-1,tooth,lcnt-1-(i+1))],
-            ]
-        ], [
-            for (tooth=[0:1:teeth-1]) each [
-                [len(vertices)-2, _gv(0,tooth,0), _gv(0,tooth,lcnt-1)],
-                [len(vertices)-2, _gv(0,tooth,lcnt-1), _gv(0,tooth+1,0)],
-                [len(vertices)-1, _gv(lsus-1,tooth,lcnt-1), _gv(lsus-1,tooth,0)],
-                [len(vertices)-1, _gv(lsus-1,tooth+1,0), _gv(lsus-1,tooth,lcnt-1)],
-            ]
+    ];
+    thickness = abs(verts1[0][0].z - select(verts1,-1)[0].z);
+    vertices = [for (x=verts1) down(thickness/2, p=reverse(x))];
+    sides_vnf = vnf_vertex_array(vertices, caps=false, col_wrap=true, reverse=true);
+    top_verts = select(vertices,-1);
+    bot_verts = select(vertices,0);
+    gear_pts = len(top_verts);
+    face_pts = gear_pts / teeth;
+    top_faces =[
+        for (i=[0:1:teeth-1], j=[0:1:(face_pts/2)-1]) each [
+            [i*face_pts+j, (i+1)*face_pts-j-1, (i+1)*face_pts-j-2],
+            [i*face_pts+j, (i+1)*face_pts-j-2, i*face_pts+j+1]
+        ],
+        for (i=[0:1:teeth-1]) each [
+            [gear_pts, (i+1)*face_pts-1, i*face_pts],
+            [gear_pts, ((i+1)%teeth)*face_pts, (i+1)*face_pts-1]
         ]
-    );
-    attachable(anchor,spin,orient, r1=p1, r2=p2, l=thickness) {
-        union() {
-            difference() {
-                down(thickness/2) {
-                    polyhedron(points=vertices, faces=faces, convexity=floor(teeth/2));
-                }
-                if (shaft_diam > 0) {
-                    cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2)));
-                }
-                if (bevelang != 0) {
-                    h = (c1-r1)/tan(45);
-                    down(thickness/2+dy) {
-                        difference() {
-                            cube([2*c1/cos(45),2*c1/cos(45),2*h], center=true);
-                            cylinder(h=h, r1=r1-0.5, r2=c1-0.5, center=false, $fn=teeth*4);
-                        }
-                    }
-                    up(thickness/2-0.01) {
-                        cylinder(h=(c2-r2)/tan(45)*5, r1=r2-0.5, r2=lerp(r2-0.5,c2-0.5,5), center=false, $fn=teeth*4);
-                    }
-                }
+    ];
+    vnf1 = vnf_merge([
+        [
+            [each top_verts, [0,0,top_verts[0].z]],
+            top_faces
+        ],
+        [
+            [each bot_verts, [0,0,bot_verts[0].z]],
+            [for (x=top_faces) reverse(x)]
+        ],
+        sides_vnf
+    ]);
+    vnf = left_handed? vnf1 : xflip(p=vnf1);
+    anchors = [
+        anchorpt("pitchbase", [0,0,pitchoff-thickness/2]),
+        anchorpt("flattop", [0,0,thickness/2])
+    ];
+    attachable(anchor,spin,orient, vnf=vnf, extent=true, anchors=anchors) {
+        difference() {
+            vnf_polyhedron(vnf, convexity=teeth);
+            if (shaft_diam > 0) {
+                cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2)));
             }
         }
         children();
diff --git a/version.scad b/version.scad
index 23b052c..a3c5e94 100644
--- a/version.scad
+++ b/version.scad
@@ -8,7 +8,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,447];
+BOSL_VERSION = [2,0,448];
 
 
 // Section: BOSL Library Version Functions