From ec900943df533f0b3d3dca639ebba3acf4139e56 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 19:46:51 -0500
Subject: [PATCH 1/7] strings reorg

---
 linalg.scad              |   4 +-
 regions.scad             |   2 +
 screws.scad              |  20 +--
 shapes3d.scad            |   2 +-
 strings.scad             | 255 ++++++++++++++++++---------------------
 tests/test_mutators.scad |   4 +-
 tests/test_strings.scad  | 214 ++++++++++++++++----------------
 utility.scad             |   4 +-
 8 files changed, 242 insertions(+), 263 deletions(-)

diff --git a/linalg.scad b/linalg.scad
index f42d43b4..a8a47b0f 100644
--- a/linalg.scad
+++ b/linalg.scad
@@ -73,9 +73,9 @@ function is_matrix_symmetric(A,eps=1e-12) =
 function echo_matrix(M,description,sig=4,eps=1e-9) =
   let(
       horiz_line = chr(8213),
-      matstr = matrix_strings(M,sig=sig,eps=eps),
+      matstr = _format_matrix(M,sig=sig,eps=eps),
       separator = str_join(repeat(horiz_line,10)),
-      dummy=echo(str(separator,"  ",is_def(description) ? description : ""))
+      dummy=echo(str(separator,is_def(description) ? str("  ",description) : ""))
             [for(row=matstr) echo(row)]
   )
   echo(separator);
diff --git a/regions.scad b/regions.scad
index 5fbc38ce..16d82b23 100644
--- a/regions.scad
+++ b/regions.scad
@@ -971,6 +971,8 @@ function offset(
         sharpcorners = [for(i=[0:len(goodsegs)-1]) _segment_extension(select(goodsegs,i-1), select(goodsegs,i))],
         // If some segments are parallel then the extended segments are undefined.  This case is not handled
         // Note if !closed the last corner doesn't matter, so exclude it
+fd=        echo(sharpcorners=sharpcorners)echo(alldef=all_defined(sharpcorners))echo(goodsegs=goodsegs),
+        
         parallelcheck =
             (len(sharpcorners)==2 && !closed) ||
             all_defined(closed? sharpcorners : select(sharpcorners, 1,-2))
diff --git a/screws.scad b/screws.scad
index 79a9b205..d1ab6c36 100644
--- a/screws.scad
+++ b/screws.scad
@@ -30,16 +30,16 @@ Torx values:  https://www.stanleyengineeredfastening.com/-/media/web/sef/resourc
 
 function _parse_screw_name(name) =
     let( commasplit = str_split(name,","),
-         length = str_num(commasplit[1]),
+         length = parse_num(commasplit[1]),
          xdash = str_split(commasplit[0], "-x"),
          type = xdash[0],
-         thread = str_float(xdash[1])
+         thread = parse_float(xdash[1])
     )
-    type[0] == "M" || type[0] == "m" ? ["metric", str_float(substr(type,1)), thread, length] :
+    type[0] == "M" || type[0] == "m" ? ["metric", parse_float(substr(type,1)), thread, length] :
     let(
         diam = type[0] == "#" ? type :
-               suffix(type,2)=="''" ? str_float(substr(type,0,len(type)-2)) :
-               let(val=str_num(type))
+               suffix(type,2)=="''" ? parse_float(substr(type,0,len(type)-2)) :
+               let(val=parse_num(type))
                val == floor(val) && val>=0 && val<=12 ? str("#",type) : val
         )
     ["english", diam, thread, u_mul(25.4,length)];
@@ -51,8 +51,8 @@ function _parse_drive(drive=undef, drive_size=undef) =
     is_undef(drive) ? ["none",undef] :
     let(drive = downcase(drive))
     in_list(drive,["hex","phillips", "slot", "torx", "phillips", "none"]) ? [drive, drive_size] :
-    drive[0]=="t" ? ["torx", str_int(substr(drive,1))] :
-    substr(drive,0,2)=="ph" ? ["phillips", str_int(substr(drive,2))] :
+    drive[0]=="t" ? ["torx", parse_int(substr(drive,1))] :
+    substr(drive,0,2)=="ph" ? ["phillips", parse_int(substr(drive,2))] :
     assert(str("Unknown screw drive type ",drive));
 
 
@@ -178,7 +178,7 @@ function screw_info(name, head, thread="coarse", drive, drive_size=undef, oversi
 
 function _screw_info_english(diam, threadcount, head, thread, drive) =
  let(
-   diameter = is_string(diam) ? str_int(substr(diam,1))*0.013 +0.06 :
+   diameter = is_string(diam) ? parse_int(substr(diam,1))*0.013 +0.06 :
               diam,
    pitch =
      is_def(threadcount) ? INCH/threadcount :
@@ -1121,8 +1121,8 @@ function _ISO_thread_tolerance(diameter, pitch, internal=false, tolerance=undef)
   assert(internalok,str("Invalid internal thread tolerance, ",tolerance,".  Must have form <digit><letter>"))
   assert(externalok,str("invalid external thread tolerance, ",tolerance,".  Must have form <digit><letter> or <digit><letter><digit><letter>"))
   let(
-    tol_num_pitch = str_num(tol_str[0]),
-    tol_num_crest = str_num(tol_str[2]),
+    tol_num_pitch = parse_num(tol_str[0]),
+    tol_num_crest = parse_num(tol_str[2]),
     tol_letter = tol_str[1]
   )
   assert(tol_letter==tol_str[3],str("Invalid tolerance, ",tolerance,".  Cannot mix different letters"))
diff --git a/shapes3d.scad b/shapes3d.scad
index e81f9d64..25ef33e3 100644
--- a/shapes3d.scad
+++ b/shapes3d.scad
@@ -2083,7 +2083,7 @@ module atext(text, h=1, size=9, font="Courier", anchor="baseline", spin=0, orien
     anch = !any([for (c=anchor) c=="["])? anchor :
         let(
             parts = str_split(str_split(str_split(anchor,"]")[0],"[")[1],","),
-            vec = [for (p=parts) str_float(str_strip(p," ",start=true))]
+            vec = [for (p=parts) parse_float(str_strip(p," ",start=true))]
         ) vec;
     ha = anchor=="baseline"? "left" :
         anchor==anch && is_string(anchor)? "center" :
diff --git a/strings.scad b/strings.scad
index e9b2b25e..e11e7203 100644
--- a/strings.scad
+++ b/strings.scad
@@ -334,11 +334,11 @@ function upcase(str) =
 
 
 
-// Section: Converting strings to numbers
+// Section: Parsing strings into numbers
 
-// Function: str_int()
+// Function: parse_int()
 // Usage:
-//   str_int(str, [base])
+//   parse_int(str, [base])
 // Description:
 //   Converts a string into an integer with any base up to 16.  Returns NaN if 
 //   conversion fails.  Digits above 9 are represented using letters A-F in either
@@ -347,62 +347,62 @@ function upcase(str) =
 //   str = String to convert.
 //   base = Base for conversion, from 2-16.  Default: 10
 // Example:
-//   str_int("349");        // Returns 349
-//   str_int("-37");        // Returns -37
-//   str_int("+97");        // Returns 97
-//   str_int("43.9");       // Returns nan
-//   str_int("1011010",2);  // Returns 90
-//   str_int("13",2);       // Returns nan
-//   str_int("dead",16);    // Returns 57005
-//   str_int("CEDE", 16);   // Returns 52958
-//   str_int("");           // Returns 0
-function str_int(str,base=10) =
+//   parse_int("349");        // Returns 349
+//   parse_int("-37");        // Returns -37
+//   parse_int("+97");        // Returns 97
+//   parse_int("43.9");       // Returns nan
+//   parse_int("1011010",2);  // Returns 90
+//   parse_int("13",2);       // Returns nan
+//   parse_int("dead",16);    // Returns 57005
+//   parse_int("CEDE", 16);   // Returns 52958
+//   parse_int("");           // Returns 0
+function parse_int(str,base=10) =
     str==undef ? undef :
     len(str)==0 ? 0 : 
     let(str=downcase(str))
-    str[0] == "-" ? -_str_int_recurse(substr(str,1),base,len(str)-2) :
-    str[0] == "+" ?  _str_int_recurse(substr(str,1),base,len(str)-2) :
-    _str_int_recurse(str,base,len(str)-1);
+    str[0] == "-" ? -_parse_int_recurse(substr(str,1),base,len(str)-2) :
+    str[0] == "+" ?  _parse_int_recurse(substr(str,1),base,len(str)-2) :
+    _parse_int_recurse(str,base,len(str)-1);
 
-function _str_int_recurse(str,base,i) =
+function _parse_int_recurse(str,base,i) =
     let(
         digit = search(str[i],"0123456789abcdef"),
         last_digit = digit == [] || digit[0] >= base ? (0/0) : digit[0]
     ) i==0 ? last_digit : 
-    _str_int_recurse(str,base,i-1)*base + last_digit;
+    _parse_int_recurse(str,base,i-1)*base + last_digit;
 
 
-// Function: str_float()
+// Function: parse_float()
 // Usage:
-//   str_float(str)
+//   parse_float(str)
 // Description:
 //   Converts a string to a floating point number.  Returns NaN if the
 //   conversion fails.
 // Arguments:
 //   str = String to convert.
 // Example:
-//   str_float("44");       // Returns 44
-//   str_float("3.4");      // Returns 3.4
-//   str_float("-99.3332"); // Returns -99.3332
-//   str_float("3.483e2");  // Returns 348.3
-//   str_float("-44.9E2");  // Returns -4490
-//   str_float("7.342e-4"); // Returns 0.0007342
-//   str_float("");         // Returns 0
-function str_float(str) =
+//   parse_float("44");       // Returns 44
+//   parse_float("3.4");      // Returns 3.4
+//   parse_float("-99.3332"); // Returns -99.3332
+//   parse_float("3.483e2");  // Returns 348.3
+//   parse_float("-44.9E2");  // Returns -4490
+//   parse_float("7.342e-4"); // Returns 0.0007342
+//   parse_float("");         // Returns 0
+function parse_float(str) =
     str==undef ? undef :
     len(str) == 0 ? 0 :
     in_list(str[1], ["+","-"]) ? (0/0) : // Don't allow --3, or +-3
-    str[0]=="-" ? -str_float(substr(str,1)) :
-    str[0]=="+" ?  str_float(substr(str,1)) :
+    str[0]=="-" ? -parse_float(substr(str,1)) :
+    str[0]=="+" ?  parse_float(substr(str,1)) :
     let(esplit = str_split(str,"eE") )
-    len(esplit)==2 ? str_float(esplit[0]) * pow(10,str_int(esplit[1])) :
+    len(esplit)==2 ? parse_float(esplit[0]) * pow(10,parse_int(esplit[1])) :
     let( dsplit = str_split(str,["."]))
-    str_int(dsplit[0])+str_int(dsplit[1])/pow(10,len(dsplit[1]));
+    parse_int(dsplit[0])+parse_int(dsplit[1])/pow(10,len(dsplit[1]));
 
 
-// Function: str_frac()
+// Function: parse_frac()
 // Usage:
-//   str_frac(str,[mixed],[improper],[signed])
+//   parse_frac(str,[mixed],[improper],[signed])
 // Description:
 //   Converts a string fraction to a floating point number.  A string fraction has the form `[-][# ][#/#]` where each `#` is one or more of the
 //   digits 0-9, and there is an optional sign character at the beginning. 
@@ -418,64 +418,64 @@ function str_float(str) =
 //   improper = set to true to accept improper fractions, false to reject them.  Default: true
 //   signed = set to true to accept a leading sign character, false to reject.  Default: true  
 // Example:
-//   str_frac("3/4");     // Returns 0.75
-//   str_frac("-77/9");   // Returns -8.55556
-//   str_frac("+1/3");    // Returns 0.33333
-//   str_frac("19");      // Returns 19
-//   str_frac("2 3/4");   // Returns 2.75
-//   str_frac("-2 12/4"); // Returns -5
-//   str_frac("");        // Returns 0
-//   str_frac("3/0");     // Returns inf
-//   str_frac("0/0");     // Returns nan
-//   str_frac("-77/9",improper=false);   // Returns nan
-//   str_frac("-2 12/4",improper=false); // Returns nan
-//   str_frac("-2 12/4",signed=false);   // Returns nan
-//   str_frac("-2 12/4",mixed=false);    // Returns nan
-//   str_frac("2 1/4",mixed=false);      // Returns nan
-function str_frac(str,mixed=true,improper=true,signed=true) =
+//   parse_frac("3/4");     // Returns 0.75
+//   parse_frac("-77/9");   // Returns -8.55556
+//   parse_frac("+1/3");    // Returns 0.33333
+//   parse_frac("19");      // Returns 19
+//   parse_frac("2 3/4");   // Returns 2.75
+//   parse_frac("-2 12/4"); // Returns -5
+//   parse_frac("");        // Returns 0
+//   parse_frac("3/0");     // Returns inf
+//   parse_frac("0/0");     // Returns nan
+//   parse_frac("-77/9",improper=false);   // Returns nan
+//   parse_frac("-2 12/4",improper=false); // Returns nan
+//   parse_frac("-2 12/4",signed=false);   // Returns nan
+//   parse_frac("-2 12/4",mixed=false);    // Returns nan
+//   parse_frac("2 1/4",mixed=false);      // Returns nan
+function parse_frac(str,mixed=true,improper=true,signed=true) =
     str == undef ? undef :
     len(str)==0 ? 0 :
-    signed && str[0]=="-" ? -str_frac(substr(str,1),mixed=mixed,improper=improper,signed=false) :
-    signed && str[0]=="+" ?  str_frac(substr(str,1),mixed=mixed,improper=improper,signed=false) :
+    signed && str[0]=="-" ? -parse_frac(substr(str,1),mixed=mixed,improper=improper,signed=false) :
+    signed && str[0]=="+" ?  parse_frac(substr(str,1),mixed=mixed,improper=improper,signed=false) :
     mixed ? (                      
         !in_list(str_find(str," "), [undef,0]) || is_undef(str_find(str,"/"))? (
             let(whole = str_split(str,[" "]))
-            _str_int_recurse(whole[0],10,len(whole[0])-1) + str_frac(whole[1], mixed=false, improper=improper, signed=false)
-        ) : str_frac(str,mixed=false, improper=improper)
+            _parse_int_recurse(whole[0],10,len(whole[0])-1) + parse_frac(whole[1], mixed=false, improper=improper, signed=false)
+        ) : parse_frac(str,mixed=false, improper=improper)
     ) : (
         let(split = str_split(str,"/"))
         len(split)!=2 ? (0/0) :
         let(
-            numerator =  _str_int_recurse(split[0],10,len(split[0])-1),
-            denominator = _str_int_recurse(split[1],10,len(split[1])-1)
+            numerator =  _parse_int_recurse(split[0],10,len(split[0])-1),
+            denominator = _parse_int_recurse(split[1],10,len(split[1])-1)
         ) !improper && numerator>=denominator? (0/0) :
         denominator<0 ? (0/0) : numerator/denominator
     );
 
 
-// Function: str_num()
+// Function: parse_num()
 // Usage:
-//   str_num(str)
+//   parse_num(str)
 // Description:
 //   Converts a string to a number.  The string can be either a fraction (two integers separated by a "/") or a floating point number.
 //   Returns NaN if the conversion fails.
 // Example:
-//   str_num("3/4");    // Returns 0.75
-//   str_num("3.4e-2"); // Returns 0.034
-function str_num(str) =
+//   parse_num("3/4");    // Returns 0.75
+//   parse_num("3.4e-2"); // Returns 0.034
+function parse_num(str) =
     str == undef ? undef :
-    let( val = str_frac(str) )
+    let( val = parse_frac(str) )
     val == val ? val :
-    str_float(str);
+    parse_float(str);
 
 
 
 
-// Section: Formatting data
+// Section: Formatting numbers into strings
 
-// Function: fmt_int()
+// Function: format_int()
 // Usage:
-//   fmt_int(i, [mindigits]);
+//   format_int(i, [mindigits]);
 // Description:
 //   Formats an integer number into a string.  This can handle larger numbers than `str()`.
 // Arguments:
@@ -483,10 +483,10 @@ function str_num(str) =
 //   mindigits = If the number has fewer than this many digits, pad the front with zeros until it does.  Default: 1.
 // Example:
 //   str(123456789012345);  // Returns "1.23457e+14"
-//   fmt_int(123456789012345);  // Returns "123456789012345"
-//   fmt_int(-123456789012345);  // Returns "-123456789012345"
-function fmt_int(i,mindigits=1) =
-    i<0? str("-", fmt_int(-i,mindigits)) :
+//   format_int(123456789012345);  // Returns "123456789012345"
+//   format_int(-123456789012345);  // Returns "-123456789012345"
+function format_int(i,mindigits=1) =
+    i<0? str("-", format_int(-i,mindigits)) :
     let(i=floor(i), e=floor(log(i)))
     i==0? str_join([for (j=[0:1:mindigits-1]) "0"]) :
     str_join(
@@ -497,33 +497,33 @@ function fmt_int(i,mindigits=1) =
     );
 
 
-// Function: fmt_fixed()
+// Function: format_fixed()
 // Usage:
-//   s = fmt_fixed(f, [digits]);
+//   s = format_fixed(f, [digits]);
 // Description:
 //   Given a floating point number, formats it into a string with the given number of digits after the decimal point.
 // Arguments:
 //   f = The floating point number to format.
 //   digits = The number of digits after the decimal to show.  Default: 6
-function fmt_fixed(f,digits=6) =
+function format_fixed(f,digits=6) =
     assert(is_int(digits))
     assert(digits>0)
-    is_list(f)? str("[",str_join(sep=", ", [for (g=f) fmt_fixed(g,digits=digits)]),"]") :
+    is_list(f)? str("[",str_join(sep=", ", [for (g=f) format_fixed(g,digits=digits)]),"]") :
     str(f)=="nan"? "nan" :
     str(f)=="inf"? "inf" :
-    f<0? str("-",fmt_fixed(-f,digits=digits)) :
+    f<0? str("-",format_fixed(-f,digits=digits)) :
     assert(is_num(f))
     let(
         sc = pow(10,digits),
         scaled = floor(f * sc + 0.5),
         whole = floor(scaled/sc),
         part = floor(scaled-(whole*sc))
-    ) str(fmt_int(whole),".",fmt_int(part,digits));
+    ) str(format_int(whole),".",format_int(part,digits));
 
 
-// Function: fmt_float()
+// Function: format_float()
 // Usage:
-//   fmt_float(f,[sig]);
+//   format_float(f,[sig]);
 // Description:
 //   Formats the given floating point number `f` into a string with `sig` significant digits.
 //   Strips trailing `0`s after the decimal point.  Strips trailing decimal point.
@@ -533,22 +533,22 @@ function fmt_fixed(f,digits=6) =
 //   f = The floating point number to format.
 //   sig = The number of significant digits to display.  Default: 12
 // Example:
-//   fmt_float(PI,12);  // Returns: "3.14159265359"
-//   fmt_float([PI,-16.75],12);  // Returns: "[3.14159265359, -16.75]"
-function fmt_float(f,sig=12) =
+//   format_float(PI,12);  // Returns: "3.14159265359"
+//   format_float([PI,-16.75],12);  // Returns: "[3.14159265359, -16.75]"
+function format_float(f,sig=12) =
     assert(is_int(sig))
     assert(sig>0)
-    is_list(f)? str("[",str_join(sep=", ", [for (g=f) fmt_float(g,sig=sig)]),"]") :
+    is_list(f)? str("[",str_join(sep=", ", [for (g=f) format_float(g,sig=sig)]),"]") :
     f==0? "0" :
     str(f)=="nan"? "nan" :
     str(f)=="inf"? "inf" :
-    f<0? str("-",fmt_float(-f,sig=sig)) :
+    f<0? str("-",format_float(-f,sig=sig)) :
     assert(is_num(f))
     let(
         e = floor(log(f)),
         mv = sig - e - 1
-    ) mv == 0? fmt_int(floor(f + 0.5)) :
-    (e<-sig/2||mv<0)? str(fmt_float(f*pow(10,-e),sig=sig),"e",e) :
+    ) mv == 0? format_int(floor(f + 0.5)) :
+    (e<-sig/2||mv<0)? str(format_float(f*pow(10,-e),sig=sig),"e",e) :
     let(
         ff = f + pow(10,-mv)*0.5,
         whole = floor(ff),
@@ -559,26 +559,26 @@ function fmt_float(f,sig=12) =
         str_strip(end=true,
             str_join([
                 ".",
-                fmt_int(part, mindigits=mv)
+                format_int(part, mindigits=mv)
             ]),
             "0."
         )
     ]);
 
 
-// Function: matrix_strings()
-// Usage:
-//   matrix_strings(M, [sig], [eps])
-// Description:
-//   Convert a numerical matrix into a matrix of strings where every column
-//   is the same width so it will display in neat columns when printed.
-//   Values below eps will display as zero.  The matrix can include nans, infs
-//   or undefs and the rows can be different lengths.  
-// Arguments:
-//   M = numerical matrix to convert
-//   sig = significant digits to display.  Default: 4
-//   eps = values smaller than this are shown as zero.  Default: 1e-9
-function matrix_strings(M, sig=4, eps=1e-9) = 
+/// Function: _format_matrix()
+/// Usage:
+///   _format_matrix(M, [sig], [eps])
+/// Description:
+///   Convert a numerical matrix into a matrix of strings where every column
+///   is the same width so it will display in neat columns when printed.
+///   Values below eps will display as zero.  The matrix can include nans, infs
+///   or undefs and the rows can be different lengths.  
+/// Arguments:
+///   M = numerical matrix to convert
+///   sig = significant digits to display.  Default: 4
+///   eps = values smaller than this are shown as zero.  Default: 1e-9
+function _format_matrix(M, sig=4, eps=1e-9) = 
    let(
        columngap = 1,
        figure_dash = chr(8210),
@@ -590,7 +590,7 @@ function matrix_strings(M, sig=4, eps=1e-9) =
                  let(
                      text = is_undef(entry) ? "und"
                           : abs(entry) < eps ? "0"             // Replace hyphens with figure dashes
-                          : str_replace_char(fmt_float(entry, sig),"-",figure_dash),
+                          : str_replace_char(format_float(entry, sig),"-",figure_dash),
                      have_dot = is_def(str_find(text, "."))
                  )
                  // If the text lacks a dot we add a space the same width as a dot to
@@ -616,9 +616,9 @@ function matrix_strings(M, sig=4, eps=1e-9) =
 
 
 
-// Function: str_format()
+// Function: format()
 // Usage:
-//   s = str_format(fmt, vals);
+//   s = format(fmt, vals);
 // Description:
 //   Given a format string and a list of values, inserts the values into the placeholders in the format string and returns it.
 //   Formatting placeholders have the following syntax:
@@ -645,13 +645,13 @@ function matrix_strings(M, sig=4, eps=1e-9) =
 //   fmt = The formatting string, with placeholders to format the values into.
 //   vals = The list of values to format.
 // Example(NORENDER):
-//   str_format("The value of {} is {:.14f}.", ["pi", PI]);  // Returns: "The value of pi is 3.14159265358979."
-//   str_format("The value {1:f} is known as {0}.", ["pi", PI]);  // Returns: "The value 3.141593 is known as pi."
-//   str_format("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]);  // Returns: "We use a very small value 1e-9 as EPSILON."
-//   str_format("{:-5s}{:i}{:b}", ["foo", 12e3, 5]);  // Returns: "foo  12000true"
-//   str_format("{:-10s}{:.3f}", ["plecostamus",27.43982]);  // Returns: "plecostamus27.440"
-//   str_format("{:-10.9s}{:.3f}", ["plecostamus",27.43982]);  // Returns: "plecostam 27.440"
-function str_format(fmt, vals) =
+//   format("The value of {} is {:.14f}.", ["pi", PI]);  // Returns: "The value of pi is 3.14159265358979."
+//   format("The value {1:f} is known as {0}.", ["pi", PI]);  // Returns: "The value 3.141593 is known as pi."
+//   format("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]);  // Returns: "We use a very small value 1e-9 as EPSILON."
+//   format("{:-5s}{:i}{:b}", ["foo", 12e3, 5]);  // Returns: "foo  12000true"
+//   format("{:-10s}{:.3f}", ["plecostamus",27.43982]);  // Returns: "plecostamus27.440"
+//   format("{:-10.9s}{:.3f}", ["plecostamus",27.43982]);  // Returns: "plecostam 27.440"
+function format(fmt, vals) =
     let(
         parts = str_split(fmt,"{")
     ) str_join([
@@ -666,7 +666,7 @@ function str_format(fmt, vals) =
             assert(i<99)
             is_undef(fmta)? "" : let(
                 fmtb = str_split(fmta,":"),
-                num = is_digit(fmtb[0])? str_int(fmtb[0]) : (i-1),
+                num = is_digit(fmtb[0])? parse_int(fmtb[0]) : (i-1),
                 left = fmtb[1][0] == "-",
                 fmtb1 = default(fmtb[1],""),
                 fmtc = left? substr(fmtb1,1) : fmtb1,
@@ -676,21 +676,21 @@ function str_format(fmt, vals) =
                 typ = hastyp? lch : "s",
                 fmtd = hastyp? substr(fmtc,0,len(fmtc)-1) : fmtc,
                 fmte = str_split((zero? substr(fmtd,1) : fmtd), "."),
-                wid = str_int(fmte[0]),
-                prec = str_int(fmte[1]),
+                wid = parse_int(fmte[0]),
+                prec = parse_int(fmte[1]),
                 val = assert(num>=0&&num<len(vals)) vals[num],
                 unpad = typ=="s"? (
                         let( sval = str(val) )
                         is_undef(prec)? sval :
                         substr(sval, 0, min(len(sval)-1, prec))
                     ) :
-                    (typ=="d" || typ=="i")? fmt_int(val) :
+                    (typ=="d" || typ=="i")? format_int(val) :
                     typ=="b"? (val? "true" : "false") :
                     typ=="B"? (val? "TRUE" : "FALSE") :
-                    typ=="f"? downcase(fmt_fixed(val,default(prec,6))) :
-                    typ=="F"? upcase(fmt_fixed(val,default(prec,6))) :
-                    typ=="g"? downcase(fmt_float(val,default(prec,6))) :
-                    typ=="G"? upcase(fmt_float(val,default(prec,6))) :
+                    typ=="f"? downcase(format_fixed(val,default(prec,6))) :
+                    typ=="F"? upcase(format_fixed(val,default(prec,6))) :
+                    typ=="g"? downcase(format_float(val,default(prec,6))) :
+                    typ=="G"? upcase(format_float(val,default(prec,6))) :
                     assert(false,str("Unknown format type: ",typ)),
                 padlen = max(0,wid-len(unpad)),
                 padfill = str_join([for (i=[0:1:padlen-1]) zero? "0" : " "]),
@@ -701,29 +701,6 @@ function str_format(fmt, vals) =
     ]);
     
 
-// Function&Module: echofmt()
-// Usage:
-//   echofmt(fmt,vals);
-// Description:
-//   Formats the given `vals` with the given format string `fmt` using [`str_format()`](#str_format), and echos the resultant string.
-// Arguments:
-//   fmt = The formatting string, with placeholders to format the values into.
-//   vals = The list of values to format.
-// Example(NORENDER):
-//   echofmt("The value of {} is {:.14f}.", ["pi", PI]);  // ECHO: "The value of pi is 3.14159265358979."
-//   echofmt("The value {1:f} is known as {0}.", ["pi", PI]);  // ECHO: "The value 3.141593 is known as pi."
-//   echofmt("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]);  // ECHO: "We use a ver small value 1e-9 as EPSILON."
-//   echofmt("{:-5s}{:i}{:b}", ["foo", 12e3, 5]);  // ECHO: "foo  12000true"
-//   echofmt("{:-10s}{:.3f}", ["plecostamus",27.43982]);  // ECHO: "plecostamus27.440"
-//   echofmt("{:-10.9s}{:.3f}", ["plecostamus",27.43982]);  // ECHO: "plecostam 27.440"
-function echofmt(fmt, vals) = echo(str_format(fmt,vals));
-module echofmt(fmt, vals) {
-   no_children($children);
-   echo(str_format(fmt,vals));
-}
-
-
-
 
 // Section: Checking character class
 
diff --git a/tests/test_mutators.scad b/tests/test_mutators.scad
index 73614344..b706486f 100644
--- a/tests/test_mutators.scad
+++ b/tests/test_mutators.scad
@@ -16,7 +16,7 @@ module test_HSL() {
                     h<=300? [x,0,c] :
                             [c,0,x]
                 );
-                assert_approx(HSL(h,s,l), rgb, str_format("h={}, s={}, l={}", [h,s,l]));
+                assert_approx(HSL(h,s,l), rgb, format("h={}, s={}, l={}", [h,s,l]));
             }
         }
     }
@@ -39,7 +39,7 @@ module test_HSV() {
                     h<=300? [x,0,c] :
                             [c,0,x]
                 );
-                assert_approx(HSV(h,s,v), rgb, str_format("h={}, s={}, v={}", [h,s,v]));
+                assert_approx(HSV(h,s,v), rgb, format("h={}, s={}, v={}", [h,s,v]));
             }
         }
     }
diff --git a/tests/test_strings.scad b/tests/test_strings.scad
index df722638..c0e69ed7 100644
--- a/tests/test_strings.scad
+++ b/tests/test_strings.scad
@@ -46,50 +46,50 @@ module test_ends_with() {
 test_ends_with();
 
 
-module test_fmt_int() {
-    assert(fmt_int(0,6) == "000000");
-    assert(fmt_int(3,6) == "000003");
-    assert(fmt_int(98765,6) == "098765");
-    assert(fmt_int(-3,6) == "-000003");
-    assert(fmt_int(-98765,6) == "-098765");
+module test_format_int() {
+    assert(format_int(0,6) == "000000");
+    assert(format_int(3,6) == "000003");
+    assert(format_int(98765,6) == "098765");
+    assert(format_int(-3,6) == "-000003");
+    assert(format_int(-98765,6) == "-098765");
 }
-test_fmt_int();
+test_format_int();
 
 
-module test_fmt_fixed() {
-    assert(fmt_fixed(-PI*100,8) == "-314.15926536");
-    assert(fmt_fixed(-PI,8) == "-3.14159265");
-    assert(fmt_fixed(-3,8) == "-3.00000000");
-    assert(fmt_fixed(3,8) == "3.00000000");
-    assert(fmt_fixed(PI*100,8) == "314.15926536");
-    assert(fmt_fixed(PI,8) == "3.14159265");
-    assert(fmt_fixed(0,8) == "0.00000000");
-    assert(fmt_fixed(-PI*100,3) == "-314.159");
-    assert(fmt_fixed(-PI,3) == "-3.142");
-    assert(fmt_fixed(-3,3) == "-3.000");
-    assert(fmt_fixed(3,3) == "3.000");
-    assert(fmt_fixed(PI*100,3) == "314.159");
-    assert(fmt_fixed(PI,3) == "3.142");
+module test_format_fixed() {
+    assert(format_fixed(-PI*100,8) == "-314.15926536");
+    assert(format_fixed(-PI,8) == "-3.14159265");
+    assert(format_fixed(-3,8) == "-3.00000000");
+    assert(format_fixed(3,8) == "3.00000000");
+    assert(format_fixed(PI*100,8) == "314.15926536");
+    assert(format_fixed(PI,8) == "3.14159265");
+    assert(format_fixed(0,8) == "0.00000000");
+    assert(format_fixed(-PI*100,3) == "-314.159");
+    assert(format_fixed(-PI,3) == "-3.142");
+    assert(format_fixed(-3,3) == "-3.000");
+    assert(format_fixed(3,3) == "3.000");
+    assert(format_fixed(PI*100,3) == "314.159");
+    assert(format_fixed(PI,3) == "3.142");
 }
-test_fmt_fixed();
+test_format_fixed();
 
 
-module test_fmt_float() {
-    assert(fmt_float(-PI*100,8) == "-314.15927");
-    assert(fmt_float(-PI,8) == "-3.1415927");
-    assert(fmt_float(-3,8) == "-3");
-    assert(fmt_float(3,8) == "3");
-    assert(fmt_float(PI*100,8) == "314.15927");
-    assert(fmt_float(PI,8) == "3.1415927");
-    assert(fmt_float(0,8) == "0");
-    assert(fmt_float(-PI*100,3) == "-314");
-    assert(fmt_float(-PI,3) == "-3.14");
-    assert(fmt_float(-3,3) == "-3");
-    assert(fmt_float(3,3) == "3");
-    assert(fmt_float(PI*100,3) == "314");
-    assert(fmt_float(PI,3) == "3.14");
+module test_format_float() {
+    assert(format_float(-PI*100,8) == "-314.15927");
+    assert(format_float(-PI,8) == "-3.1415927");
+    assert(format_float(-3,8) == "-3");
+    assert(format_float(3,8) == "3");
+    assert(format_float(PI*100,8) == "314.15927");
+    assert(format_float(PI,8) == "3.1415927");
+    assert(format_float(0,8) == "0");
+    assert(format_float(-PI*100,3) == "-314");
+    assert(format_float(-PI,3) == "-3.14");
+    assert(format_float(-3,3) == "-3");
+    assert(format_float(3,3) == "3");
+    assert(format_float(PI*100,3) == "314");
+    assert(format_float(PI,3) == "3.14");
 }
-test_fmt_float();
+test_format_float();
 
 
 module test_is_digit() {
@@ -173,78 +173,78 @@ module test_is_upper() {
 test_is_upper();
 
 
-module test_str_float() {
-    assert(str_float("3.1416") == 3.1416);
-    assert(str_float("-3.1416") == -3.1416);
-    assert(str_float("3.000") == 3.0);
-    assert(str_float("-3.000") == -3.0);
-    assert(str_float("3") == 3.0);
-    assert(str_float("0") == 0.0);
+module test_parse_float() {
+    assert(parse_float("3.1416") == 3.1416);
+    assert(parse_float("-3.1416") == -3.1416);
+    assert(parse_float("3.000") == 3.0);
+    assert(parse_float("-3.000") == -3.0);
+    assert(parse_float("3") == 3.0);
+    assert(parse_float("0") == 0.0);
 }
-test_str_float();
+test_parse_float();
 
 
-module test_str_frac() {
-    assert(str_frac("") == 0);
-    assert(str_frac("1/2") == 1/2);
-    assert(str_frac("+1/2") == 1/2);
-    assert(str_frac("-1/2") == -1/2);
-    assert(str_frac("7/8") == 7/8);
-    assert(str_frac("+7/8") == 7/8);
-    assert(str_frac("-7/8") == -7/8);
-    assert(str_frac("1 1/2") == 1 + 1/2);
-    assert(str_frac("+1 1/2") == 1 + 1/2);
-    assert(str_frac("-1 1/2") == -(1 + 1/2));
-    assert(str_frac("768 3/4") == 768 + 3/4);
-    assert(str_frac("+768 3/4") == 768 + 3/4);
-    assert(str_frac("-768 3/4") == -(768 + 3/4));
-    assert(str_frac("19") == 19);
-    assert(str_frac("+19") == 19);
-    assert(str_frac("-19") == -19);
-    assert(str_frac("3/0") == INF);
-    assert(str_frac("-3/0") == -INF);
-    assert(is_nan(str_frac("0/0")));
+module test_parse_frac() {
+    assert(parse_frac("") == 0);
+    assert(parse_frac("1/2") == 1/2);
+    assert(parse_frac("+1/2") == 1/2);
+    assert(parse_frac("-1/2") == -1/2);
+    assert(parse_frac("7/8") == 7/8);
+    assert(parse_frac("+7/8") == 7/8);
+    assert(parse_frac("-7/8") == -7/8);
+    assert(parse_frac("1 1/2") == 1 + 1/2);
+    assert(parse_frac("+1 1/2") == 1 + 1/2);
+    assert(parse_frac("-1 1/2") == -(1 + 1/2));
+    assert(parse_frac("768 3/4") == 768 + 3/4);
+    assert(parse_frac("+768 3/4") == 768 + 3/4);
+    assert(parse_frac("-768 3/4") == -(768 + 3/4));
+    assert(parse_frac("19") == 19);
+    assert(parse_frac("+19") == 19);
+    assert(parse_frac("-19") == -19);
+    assert(parse_frac("3/0") == INF);
+    assert(parse_frac("-3/0") == -INF);
+    assert(is_nan(parse_frac("0/0")));
 }
-test_str_frac();
+test_parse_frac();
 
 
-module test_str_num() {
-    assert(str_num("") == 0);
-    assert(str_num("1/2") == 1/2);
-    assert(str_num("+1/2") == 1/2);
-    assert(str_num("-1/2") == -1/2);
-    assert(str_num("7/8") == 7/8);
-    assert(str_num("+7/8") == 7/8);
-    assert(str_num("-7/8") == -7/8);
-    assert(str_num("1 1/2") == 1 + 1/2);
-    assert(str_num("+1 1/2") == 1 + 1/2);
-    assert(str_num("-1 1/2") == -(1 + 1/2));
-    assert(str_num("768 3/4") == 768 + 3/4);
-    assert(str_num("+768 3/4") == 768 + 3/4);
-    assert(str_num("-768 3/4") == -(768 + 3/4));
-    assert(str_num("19") == 19);
-    assert(str_num("+19") == 19);
-    assert(str_num("-19") == -19);
-    assert(str_num("3/0") == INF);
-    assert(str_num("-3/0") == -INF);
-    assert(str_num("3.14159") == 3.14159);
-    assert(str_num("-3.14159") == -3.14159);
-    assert(is_nan(str_num("0/0")));
+module test_parse_num() {
+    assert(parse_num("") == 0);
+    assert(parse_num("1/2") == 1/2);
+    assert(parse_num("+1/2") == 1/2);
+    assert(parse_num("-1/2") == -1/2);
+    assert(parse_num("7/8") == 7/8);
+    assert(parse_num("+7/8") == 7/8);
+    assert(parse_num("-7/8") == -7/8);
+    assert(parse_num("1 1/2") == 1 + 1/2);
+    assert(parse_num("+1 1/2") == 1 + 1/2);
+    assert(parse_num("-1 1/2") == -(1 + 1/2));
+    assert(parse_num("768 3/4") == 768 + 3/4);
+    assert(parse_num("+768 3/4") == 768 + 3/4);
+    assert(parse_num("-768 3/4") == -(768 + 3/4));
+    assert(parse_num("19") == 19);
+    assert(parse_num("+19") == 19);
+    assert(parse_num("-19") == -19);
+    assert(parse_num("3/0") == INF);
+    assert(parse_num("-3/0") == -INF);
+    assert(parse_num("3.14159") == 3.14159);
+    assert(parse_num("-3.14159") == -3.14159);
+    assert(is_nan(parse_num("0/0")));
 }
-test_str_num();
+test_parse_num();
 
 
-module test_str_int() {
-    assert(str_int("0") == 0);
-    assert(str_int("3") == 3);
-    assert(str_int("7655") == 7655);
-    assert(str_int("+3") == 3);
-    assert(str_int("+7655") == 7655);
-    assert(str_int("-3") == -3);
-    assert(str_int("-7655") == -7655);
-    assert(str_int("ffff",16) == 65535);
+module test_parse_int() {
+    assert(parse_int("0") == 0);
+    assert(parse_int("3") == 3);
+    assert(parse_int("7655") == 7655);
+    assert(parse_int("+3") == 3);
+    assert(parse_int("+7655") == 7655);
+    assert(parse_int("-3") == -3);
+    assert(parse_int("-7655") == -7655);
+    assert(parse_int("ffff",16) == 65535);
 }
-test_str_int();
+test_parse_int();
 
 
 module test_str_join() {
@@ -342,15 +342,15 @@ module test_str_find() {
 test_str_find();
 
 
-module test_str_format() {
-    assert(str_format("The value of {} is {:.14f}.", ["pi", PI]) == "The value of pi is 3.14159265358979.");
-    assert(str_format("The value {1:f} is known as {0}.", ["pi", PI]) == "The value 3.141593 is known as pi.");
-    assert(str_format("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]) == "We use a very small value 1e-9 as EPSILON.");
-    assert(str_format("{:-5s}{:i}{:b}", ["foo", 12e3, 5]) == "foo  12000true");
-    assert(str_format("{:-10s}{:.3f}", ["plecostamus",27.43982]) == "plecostamus27.440");
-    assert(str_format("{:-10.9s}{:.3f}", ["plecostamus",27.43982]) == "plecostam 27.440");
+module test_format() {
+    assert(format("The value of {} is {:.14f}.", ["pi", PI]) == "The value of pi is 3.14159265358979.");
+    assert(format("The value {1:f} is known as {0}.", ["pi", PI]) == "The value 3.141593 is known as pi.");
+    assert(format("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]) == "We use a very small value 1e-9 as EPSILON.");
+    assert(format("{:-5s}{:i}{:b}", ["foo", 12e3, 5]) == "foo  12000true");
+    assert(format("{:-10s}{:.3f}", ["plecostamus",27.43982]) == "plecostamus27.440");
+    assert(format("{:-10.9s}{:.3f}", ["plecostamus",27.43982]) == "plecostam 27.440");
 }
-test_str_format();
+test_format();
 
 
 /*
diff --git a/utility.scad b/utility.scad
index a9c66be2..d9a54b31 100644
--- a/utility.scad
+++ b/utility.scad
@@ -584,8 +584,8 @@ module no_module() {
 function _valstr(x) =
     is_string(x)? str("\"",str_replace_char(x, "\"", "\\\""),"\"") :
     is_list(x)? str("[",str_join([for (xx=x) _valstr(xx)],","),"]") :
-    is_num(x) && x==floor(x)? fmt_int(x) :
-    is_finite(x)? fmt_float(x,12) : x;
+    is_num(x) && x==floor(x)? format_int(x) :
+    is_finite(x)? format_float(x,12) : x;
 
 
 // Module: assert_approx()

From 000ddf87c32e1ea7b4be174b96467dc2b35dbca4 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 19:55:54 -0500
Subject: [PATCH 2/7] hide is_polygon_on_list remove debug echo

---
 geometry.scad | 28 ++++++++++++++--------------
 regions.scad  |  4 +---
 2 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/geometry.scad b/geometry.scad
index 29032839..e9762a6b 100644
--- a/geometry.scad
+++ b/geometry.scad
@@ -2171,23 +2171,23 @@ function _are_polygons_equal(poly1, poly2, eps, st) =
     max([for(d=poly1-select(poly2,st,st-1)) d*d])<eps*eps;
 
 
-// Function: is_polygon_in_list()
-// Topics: Polygons, Comparators
-// See Also: are_polygons_equal(), are_regions_equal()
-// Usage:
-//   bool = is_polygon_in_list(poly, polys);
-// Description:
-//   Returns true if one of the polygons in `polys` is equivalent to the polygon `poly`.
-// Arguments:
-//   poly = The polygon to search for.
-//   polys = The list of polygons to look for the polygon in.
-function is_polygon_in_list(poly, polys) =
-    __is_polygon_in_list(poly, polys, 0);
+/// Function: _is_polygon_in_list()
+/// Topics: Polygons, Comparators
+/// See Also: are_polygons_equal(), are_regions_equal()
+/// Usage:
+///   bool = _is_polygon_in_list(poly, polys);
+/// Description:
+///   Returns true if one of the polygons in `polys` is equivalent to the polygon `poly`.
+/// Arguments:
+///   poly = The polygon to search for.
+///   polys = The list of polygons to look for the polygon in.
+function _is_polygon_in_list(poly, polys) =
+    ___is_polygon_in_list(poly, polys, 0);
 
-function __is_polygon_in_list(poly, polys, i) =
+function ___is_polygon_in_list(poly, polys, i) =
     i >= len(polys)? false :
     are_polygons_equal(poly, polys[i])? true :
-    __is_polygon_in_list(poly, polys, i+1);
+    ___is_polygon_in_list(poly, polys, i+1);
 
 
 // Section: Convex Hull
diff --git a/regions.scad b/regions.scad
index 16d82b23..4f686f9b 100644
--- a/regions.scad
+++ b/regions.scad
@@ -398,7 +398,7 @@ function are_regions_equal(region1, region2, either_winding=false) =
 
 function __are_regions_equal(region1, region2, i) =
     i >= len(region1)? true :
-    !is_polygon_in_list(region1[i], region2)? false :
+    !_is_polygon_in_list(region1[i], region2)? false :
     __are_regions_equal(region1, region2, i+1);
 
 
@@ -971,8 +971,6 @@ function offset(
         sharpcorners = [for(i=[0:len(goodsegs)-1]) _segment_extension(select(goodsegs,i-1), select(goodsegs,i))],
         // If some segments are parallel then the extended segments are undefined.  This case is not handled
         // Note if !closed the last corner doesn't matter, so exclude it
-fd=        echo(sharpcorners=sharpcorners)echo(alldef=all_defined(sharpcorners))echo(goodsegs=goodsegs),
-        
         parallelcheck =
             (len(sharpcorners)==2 && !closed) ||
             all_defined(closed? sharpcorners : select(sharpcorners, 1,-2))

From 9a71c5072bd5686b15fce558495a11efc21e2922 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 20:22:11 -0500
Subject: [PATCH 3/7] fix format call

---
 constants.scad | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/constants.scad b/constants.scad
index df79e34a..1e2a1d3d 100644
--- a/constants.scad
+++ b/constants.scad
@@ -52,7 +52,7 @@ _UNDEF="LRG+HX7dy89RyHvDlAKvb9Y04OTuaikpx205CTh8BSI";
 //           cuboid([holesize.x + 2*s, holesize.y + 2*s, h+0.2]);
 //           fwd(w/2-1) xrot(90) linear_extrude(1.1) {
 //             text(
-//               text=fmt_fixed(s,2),
+//               text=format_fixed(s,2),
 //               size=0.4*holesize.x,
 //               halign="center",
 //               valign="center"

From dd9b1976780b79eb6eda7ace1a03fe82bc294518 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 21:00:39 -0500
Subject: [PATCH 4/7] move linear_sweep and spiral_sweep to skin.scad

---
 bottlecaps.scad |  14 +--
 mutators.scad   |  93 --------------------
 regions.scad    | 120 --------------------------
 skin.scad       | 225 ++++++++++++++++++++++++++++++++++++++++++++++++
 threading.scad  |  16 ++--
 5 files changed, 240 insertions(+), 228 deletions(-)

diff --git a/bottlecaps.scad b/bottlecaps.scad
index 00e715a3..a91b9ea3 100644
--- a/bottlecaps.scad
+++ b/bottlecaps.scad
@@ -117,7 +117,7 @@ module pco1810_neck(wall=2, anchor="support-ring", spin=0, orient=UP)
                             pitch=thread_pitch,
                             thread_depth=thread_h+0.1,
                             flank_angle=flank_angle,
-                            twist=810,
+                            turns=810/360,
                             higbee=thread_h*2,
                             anchor=TOP
                         );
@@ -195,7 +195,7 @@ module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
                 }
                 up(wall) cyl(d=cap_id, h=tamper_ring_h+wall, anchor=BOTTOM);
             }
-            up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, flank_angle=flank_angle, twist=810, higbee=thread_depth, internal=true, anchor=BOTTOM);
+            up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, flank_angle=flank_angle, twist=810/360, higbee=thread_depth, internal=true, anchor=BOTTOM);
         }
         children();
     }
@@ -310,7 +310,7 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP)
                         pitch=thread_pitch,
                         thread_depth=thread_h+0.1,
                         flank_angle=flank_angle,
-                        twist=650,
+                        twist=650/360,
                         higbee=thread_h*2,
                         anchor=TOP
                     );
@@ -379,7 +379,7 @@ module pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
                 }
                 up(wall) cyl(d=28.58, h=11.2+wall, anchor=BOTTOM);
             }
-            up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, flank_angle=15, twist=650, higbee=1.6, internal=true, anchor=BOTTOM);
+            up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, flank_angle=15, twist=650/360, higbee=1.6, internal=true, anchor=BOTTOM);
         }
         children();
     }
@@ -482,7 +482,7 @@ module generic_bottle_neck(
                         pitch = thread_pitch,
                         thread_depth = thread_h + 0.1 * diamMagMult,
                         flank_angle = flank_angle,
-                        twist = 360 * (height - pitch - lip_roundover_r) * .6167 / pitch,
+                        turns = (height - pitch - lip_roundover_r) * .6167 / pitch,
                         higbee = thread_h * 2,
                         anchor = TOP
                     );
@@ -590,7 +590,7 @@ module generic_bottle_cap(
             }
             difference(){
                 up(wall + pitch / 2) {
-                    thread_helix(d = neckOuterDTol, pitch = pitch, thread_depth = threadDepth, flank_angle = flank_angle, twist = 360 * ((height - pitch) / pitch), higbee = threadDepth, internal = true, anchor = BOTTOM);
+                    thread_helix(d = neckOuterDTol, pitch = pitch, thread_depth = threadDepth, flank_angle = flank_angle, turns = ((height - pitch) / pitch), higbee = threadDepth, internal = true, anchor = BOTTOM);
                 }
             }
         }
@@ -1130,7 +1130,7 @@ module sp_neck(diam,type,wall,id,style="L",bead=false, anchor, spin, orient)
         up((H+extra_bot)/2){
             difference(){
                 union(){
-                    thread_helix(d=T-.01, profile=profile, pitch = INCH/tpi, twist=twist+2*higang, higbee=higlen, anchor=TOP);
+                    thread_helix(d=T-.01, profile=profile, pitch = INCH/tpi, turns=(twist+2*higang)/360, higbee=higlen, anchor=TOP);
                     cylinder(d=T-depth*2,l=H,anchor=TOP);
                     if (bead)
                       down(bead_shift)
diff --git a/mutators.scad b/mutators.scad
index 5e755176..c0fc6823 100644
--- a/mutators.scad
+++ b/mutators.scad
@@ -348,99 +348,6 @@ module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) {
 
 
 
-// Module: spiral_sweep()
-// Description:
-//   Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path
-//   of a given radius, height and twist.  The origin in the profile traces out the helix of the specified radius.
-//   If twist is positive the path will be right-handed;  if twist is negative the path will be left-handed.
-//   .
-//   Higbee specifies tapering applied to the ends of the extrusion and is given as the linear distance
-//   over which to taper.  
-// Arguments:
-//   poly = Array of points of a polygon path, to be extruded.
-//   h = height of the spiral to extrude along.
-//   r = Radius of the spiral to extrude along. Default: 50
-//   twist = number of degrees of rotation to spiral up along height.
-//   ---
-//   d = Diameter of the spiral to extrude along.
-//   higbee = Length to taper thread ends over.
-//   higbee1 = Taper length at start
-//   higbee2 = Taper length at end
-//   internal = direction to taper the threads with higbee.  If true threads taper outward; if false they taper inward.   Default: false
-//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
-//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
-//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
-//   center = If given, overrides `anchor`.  A true value sets `anchor=CENTER`, false sets `anchor=BOTTOM`.
-// Example:
-//   poly = [[-10,0], [-3,-5], [3,-5], [10,0], [0,-30]];
-//   spiral_sweep(poly, h=200, r=50, twist=1080, $fn=36);
-module spiral_sweep(poly, h, r, twist=360, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal=false, anchor, spin=0, orient=UP) {
-    higsample = 10;         // Oversample factor for higbee tapering
-    dummy1=assert(is_num(twist) && twist != 0);
-    bounds = pointlist_bounds(poly);
-    yctr = (bounds[0].y+bounds[1].y)/2;
-    xmin = bounds[0].x;
-    xmax = bounds[1].x;
-    poly = path3d(clockwise_polygon(poly));
-    anchor = get_anchor(anchor,center,BOT,BOT);
-    r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50);
-    r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50);
-    sides = segs(max(r1,r2));
-    dir = sign(twist);
-    ang_step = 360/sides*dir;
-    anglist = [for(ang = [0:ang_step:twist-EPSILON]) ang,
-               twist];
-    higbee1 = first_defined([higbee1, higbee, 0]);
-    higbee2 = first_defined([higbee2, higbee, 0]);
-    higang1 = 360 * higbee1 / (2 * r1 * PI);
-    higang2 = 360 * higbee2 / (2 * r2 * PI);
-    dummy2=assert(higbee1>=0 && higbee2>=0)
-           assert(higang1 < dir*twist/2,"Higbee1 is more than half the threads")
-           assert(higang2 < dir*twist/2,"Higbee2 is more than half the threads");
-    function polygon_r(N,theta) =
-        let( alpha = 360/N )
-        cos(alpha/2)/(cos(posmod(theta,alpha)-alpha/2));
-    higofs = pow(0.05,2);   // Smallest hig scale is the square root of this value
-    function taperfunc(x) = sqrt((1-higofs)*x+higofs);
-    interp_ang = [
-                  for(i=idx(anglist,e=-2))
-                      each lerpn(anglist[i],anglist[i+1],
-                                 (higang1>0 && higang1>dir*anglist[i+1]
-                                  || (higang2>0 && higang2>dir*(twist-anglist[i]))) ? ceil((anglist[i+1]-anglist[i])/ang_step*higsample)
-                                                                                    : 1,
-                                 endpoint=false),
-                  last(anglist)
-                 ];
-    skewmat = affine3d_skew_xz(xa=atan2(r2-r1,h));
-    points = [
-        for (a = interp_ang) let (
-            hsc = dir*a<higang1 ? taperfunc(dir*a/higang1)
-                : dir*(twist-a)<higang2 ? taperfunc(dir*(twist-a)/higang2)
-                : 1,
-            u = a/twist,
-            r = lerp(r1,r2,u),
-            mat = affine3d_zrot(a)
-                * affine3d_translate([polygon_r(sides,a)*r, 0, h * (u-0.5)])
-                * affine3d_xrot(90)
-                * skewmat
-                * scale([hsc,lerp(hsc,1,0.25),1], cp=[internal ? xmax : xmin, yctr, 0]),
-            pts = apply(mat, poly)
-        ) pts
-    ];
-
-    vnf = vnf_vertex_array(
-        points, col_wrap=true, caps=true, reverse=dir>0?true:false, 
-        style=higbee1>0 || higbee2>0 ? "quincunx" : "alt"
-    );
-
-    attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
-        vnf_polyhedron(vnf, convexity=ceil(2*dir*twist/360));
-        children();
-    }
-}
-
-
-
 // Module: path_extrude()
 // Description:
 //   Extrudes 2D children along a 3D path.  This may be slow.
diff --git a/regions.scad b/regions.scad
index 4f686f9b..e9cd822d 100644
--- a/regions.scad
+++ b/regions.scad
@@ -591,126 +591,6 @@ function region_parts(region) =
     ];
 
 
-// Section: Region Extrusion and VNFs
-
-
-
-// Function&Module: linear_sweep()
-// Usage:
-//   linear_sweep(region, height, [center], [slices], [twist], [scale], [style], [convexity]) {attachments};
-// Description:
-//   If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or polygon.
-//   If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
-//   of the given 2D region or polygon.  The benefit of using this, over using `linear_extrude region(rgn)` is
-//   that it supports `anchor`, `spin`, `orient` and attachments.  You can also make more refined
-//   twisted extrusions by using `maxseg` to subsample flat faces.
-//   Note that the center option centers vertically using the named anchor "zcenter" whereas
-//   `anchor=CENTER` centers the entire shape relative to
-//   the shape's centroid, or other centerpoint you specify.  The centerpoint can be "centroid", "mean", "box" or
-//   a custom point location.  
-// Arguments:
-//   region = The 2D [Region](regions.scad) or polygon that is to be extruded.
-//   height = The height to extrude the region.  Default: 1
-//   center = If true, the created polyhedron will be vertically centered.  If false, it will be extruded upwards from the XY plane.  Default: `false`
-//   slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist.  Default: `twist/5`
-//   maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length.  This can refine twisting flat faces a lot.  Default: `undef` (no subsampling)
-//   twist = The number of degrees to rotate the shape clockwise around the Z axis, as it rises from bottom to top.  Default: 0
-//   scale = The amount to scale the shape, from bottom to top.  Default: 1
-//   style = The style to use when triangulating the surface of the object.  Valid values are `"default"`, `"alt"`, or `"quincunx"`.
-//   convexity = Max number of surfaces any single ray could pass through.  Module use only.
-//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `"origin"`
-//   atype = Set to "hull" or "intersect" to select anchor type.  Default: "hull"
-//   cp = Centerpoint for determining intersection anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
-//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
-//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
-// Example: Extruding a Compound Region.
-//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
-//   rgn2 = [square(30,center=false)];
-//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
-//   mrgn = union(rgn1,rgn2);
-//   orgn = difference(mrgn,rgn3);
-//   linear_sweep(orgn,height=20,convexity=16);
-// Example: With Twist, Scale, Slices and Maxseg.
-//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
-//   rgn2 = [square(30,center=false)];
-//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
-//   mrgn = union(rgn1,rgn2);
-//   orgn = difference(mrgn,rgn3);
-//   linear_sweep(orgn,height=50,maxseg=2,slices=40,twist=180,scale=0.5,convexity=16);
-// Example: Anchors on an Extruded Region
-//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
-//   rgn2 = [square(30,center=false)];
-//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
-//   mrgn = union(rgn1,rgn2);
-//   orgn = difference(mrgn,rgn3);
-//   linear_sweep(orgn,height=20,convexity=16) show_anchors();
-module linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, style="default", convexity,
-                    spin=0, orient=UP, cp="centroid", anchor="origin", atype="hull") {
-    region = force_region(region);
-    dummy=assert(is_region(region),"Input is not a region");
-    anchor = center ? "zcenter" : anchor;
-    anchors = [named_anchor("zcenter", [0,0,height/2], UP)];
-    vnf = linear_sweep(
-        region, height=height,
-        twist=twist, scale=scale,
-        slices=slices, maxseg=maxseg,
-        style=style
-    );
-    attachable(anchor,spin,orient, cp=cp, region=region, h=height, extent=atype=="hull", anchors=anchors) {
-        vnf_polyhedron(vnf, convexity=convexity);
-        children();
-    }
-}
-
-
-function linear_sweep(region, height=1, center, twist=0, scale=1, slices,
-                      maxseg, style="default", cp="centroid", atype="hull", anchor, spin=0, orient=UP) =
-    let(
-        region = force_region(region)
-    )
-    assert(is_region(region), "Input is not a region")
-    let(
-        anchor = center ? "zcenter" : anchor,
-        anchors = [named_anchor("zcenter", [0,0,height/2], UP)],
-        regions = region_parts(region),
-        slices = default(slices, floor(twist/5+1)),
-        step = twist/slices,
-        hstep = height/slices,
-        trgns = [
-            for (rgn=regions) [
-                for (path=rgn) let(
-                    p = cleanup_path(path),
-                    path = is_undef(maxseg)? p : [
-                        for (seg=pair(p,true)) each
-                        let(steps=ceil(norm(seg.y-seg.x)/maxseg))
-                        lerpn(seg.x, seg.y, steps, false)
-                    ]
-                )
-                rot(twist, p=scale([scale,scale],p=path))
-            ]
-        ],
-        vnf = vnf_join([
-            for (rgn = regions)
-            for (pathnum = idx(rgn)) let(
-                p = cleanup_path(rgn[pathnum]),
-                path = is_undef(maxseg)? p : [
-                    for (seg=pair(p,true)) each
-                    let(steps=ceil(norm(seg.y-seg.x)/maxseg))
-                    lerpn(seg.x, seg.y, steps, false)
-                ],
-                verts = [
-                    for (i=[0:1:slices]) let(
-                        sc = lerp(1, scale, i/slices),
-                        ang = i * step,
-                        h = i * hstep //- height/2
-                    ) scale([sc,sc,1], p=rot(ang, p=path3d(path,h)))
-                ]
-            ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
-            for (rgn = regions) vnf_from_region(rgn, ident(4), reverse=true),
-            for (rgn = trgns) vnf_from_region(rgn, up(height), reverse=false)
-        ])
-    ) reorient(anchor,spin,orient, cp=cp, vnf=vnf, extent=atype=="hull", p=vnf, anchors=anchors);
-
 
 
 // Section: Offset and 2D Boolean Set Operations
diff --git a/skin.scad b/skin.scad
index 03cc4719..1f198860 100644
--- a/skin.scad
+++ b/skin.scad
@@ -501,6 +501,231 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close
   reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
 
 
+
+// Function&Module: linear_sweep()
+// Usage:
+//   linear_sweep(region, height, [center], [slices], [twist], [scale], [style], [convexity]) {attachments};
+// Description:
+//   If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or polygon.
+//   If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
+//   of the given 2D region or polygon.  The benefit of using this, over using `linear_extrude region(rgn)` is
+//   that it supports `anchor`, `spin`, `orient` and attachments.  You can also make more refined
+//   twisted extrusions by using `maxseg` to subsample flat faces.
+//   Note that the center option centers vertically using the named anchor "zcenter" whereas
+//   `anchor=CENTER` centers the entire shape relative to
+//   the shape's centroid, or other centerpoint you specify.  The centerpoint can be "centroid", "mean", "box" or
+//   a custom point location.  
+// Arguments:
+//   region = The 2D [Region](regions.scad) or polygon that is to be extruded.
+//   height = The height to extrude the region.  Default: 1
+//   center = If true, the created polyhedron will be vertically centered.  If false, it will be extruded upwards from the XY plane.  Default: `false`
+//   slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist.  Default: `twist/5`
+//   maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length.  This can refine twisting flat faces a lot.  Default: `undef` (no subsampling)
+//   twist = The number of degrees to rotate the shape clockwise around the Z axis, as it rises from bottom to top.  Default: 0
+//   scale = The amount to scale the shape, from bottom to top.  Default: 1
+//   style = The style to use when triangulating the surface of the object.  Valid values are `"default"`, `"alt"`, or `"quincunx"`.
+//   convexity = Max number of surfaces any single ray could pass through.  Module use only.
+//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `"origin"`
+//   atype = Set to "hull" or "intersect" to select anchor type.  Default: "hull"
+//   cp = Centerpoint for determining intersection anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
+//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
+//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
+// Example: Extruding a Compound Region.
+//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
+//   rgn2 = [square(30,center=false)];
+//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
+//   mrgn = union(rgn1,rgn2);
+//   orgn = difference(mrgn,rgn3);
+//   linear_sweep(orgn,height=20,convexity=16);
+// Example: With Twist, Scale, Slices and Maxseg.
+//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
+//   rgn2 = [square(30,center=false)];
+//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
+//   mrgn = union(rgn1,rgn2);
+//   orgn = difference(mrgn,rgn3);
+//   linear_sweep(orgn,height=50,maxseg=2,slices=40,twist=180,scale=0.5,convexity=16);
+// Example: Anchors on an Extruded Region
+//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
+//   rgn2 = [square(30,center=false)];
+//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
+//   mrgn = union(rgn1,rgn2);
+//   orgn = difference(mrgn,rgn3);
+//   linear_sweep(orgn,height=20,convexity=16) show_anchors();
+module linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, style="default", convexity,
+                    spin=0, orient=UP, cp="centroid", anchor="origin", atype="hull") {
+    region = force_region(region);
+    dummy=assert(is_region(region),"Input is not a region");
+    anchor = center ? "zcenter" : anchor;
+    anchors = [named_anchor("zcenter", [0,0,height/2], UP)];
+    vnf = linear_sweep(
+        region, height=height,
+        twist=twist, scale=scale,
+        slices=slices, maxseg=maxseg,
+        style=style
+    );
+    attachable(anchor,spin,orient, cp=cp, region=region, h=height, extent=atype=="hull", anchors=anchors) {
+        vnf_polyhedron(vnf, convexity=convexity);
+        children();
+    }
+}
+
+
+function linear_sweep(region, height=1, center, twist=0, scale=1, slices,
+                      maxseg, style="default", cp="centroid", atype="hull", anchor, spin=0, orient=UP) =
+    let(
+        region = force_region(region)
+    )
+    assert(is_region(region), "Input is not a region")
+    let(
+        anchor = center ? "zcenter" : anchor,
+        anchors = [named_anchor("zcenter", [0,0,height/2], UP)],
+        regions = region_parts(region),
+        slices = default(slices, floor(twist/5+1)),
+        step = twist/slices,
+        hstep = height/slices,
+        trgns = [
+            for (rgn=regions) [
+                for (path=rgn) let(
+                    p = cleanup_path(path),
+                    path = is_undef(maxseg)? p : [
+                        for (seg=pair(p,true)) each
+                        let(steps=ceil(norm(seg.y-seg.x)/maxseg))
+                        lerpn(seg.x, seg.y, steps, false)
+                    ]
+                )
+                rot(twist, p=scale([scale,scale],p=path))
+            ]
+        ],
+        vnf = vnf_join([
+            for (rgn = regions)
+            for (pathnum = idx(rgn)) let(
+                p = cleanup_path(rgn[pathnum]),
+                path = is_undef(maxseg)? p : [
+                    for (seg=pair(p,true)) each
+                    let(steps=ceil(norm(seg.y-seg.x)/maxseg))
+                    lerpn(seg.x, seg.y, steps, false)
+                ],
+                verts = [
+                    for (i=[0:1:slices]) let(
+                        sc = lerp(1, scale, i/slices),
+                        ang = i * step,
+                        h = i * hstep //- height/2
+                    ) scale([sc,sc,1], p=rot(ang, p=path3d(path,h)))
+                ]
+            ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
+            for (rgn = regions) vnf_from_region(rgn, ident(4), reverse=true),
+            for (rgn = trgns) vnf_from_region(rgn, up(height), reverse=false)
+        ])
+    ) reorient(anchor,spin,orient, cp=cp, vnf=vnf, extent=atype=="hull", p=vnf, anchors=anchors);
+
+
+
+// Function&Module: spiral_sweep()
+// Usage:
+//   spiral_sweep(poly, h, r, turns, [higbee], [center], [r1], [r2], [d], [d1], [d2], [higbee1], [higbee2], [internal], [anchor], [spin], [orient]);
+//   vnf = spiral_sweep(poly, h, r, turns, ...);
+// Description:
+//   Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path
+//   of a given radius, height and degrees of rotation.  The origin in the profile traces out the helix of the specified radius.
+//   If turns is positive the path will be right-handed;  if turns is negative the path will be left-handed.
+//   .
+//   Higbee specifies tapering applied to the ends of the extrusion and is given as the linear distance
+//   over which to taper.  
+// Arguments:
+//   poly = Array of points of a polygon path, to be extruded.
+//   h = height of the spiral to extrude along.
+//   r = Radius of the spiral to extrude along. Default: 50
+//   turns = number of revolutions to spiral up along the height.
+//   ---
+//   d = Diameter of the spiral to extrude along.
+//   higbee = Length to taper thread ends over.
+//   higbee1 = Taper length at start
+//   higbee2 = Taper length at end
+//   internal = direction to taper the threads with higbee.  If true threads taper outward; if false they taper inward.   Default: false
+//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
+//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
+//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
+//   center = If given, overrides `anchor`.  A true value sets `anchor=CENTER`, false sets `anchor=BOTTOM`.
+// Example:
+//   poly = [[-10,0], [-3,-5], [3,-5], [10,0], [0,-30]];
+//   spiral_sweep(poly, h=200, r=50, turns=3, $fn=36);
+function _taperfunc(x) =
+     let(higofs = pow(0.05,2))   // Smallest hig scale is the square root of this value
+     sqrt((1-higofs)*x+higofs);
+function _ss_polygon_r(N,theta) =
+        let( alpha = 360/N )
+        cos(alpha/2)/(cos(posmod(theta,alpha)-alpha/2));
+function spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal=false, anchor=CENTER, spin=0, orient=UP) =
+    assert(is_num(turns) && turns != 0)
+    let(
+        twist = 360*turns, 
+        higsample = 10,         // Oversample factor for higbee tapering
+        bounds = pointlist_bounds(poly),
+        yctr = (bounds[0].y+bounds[1].y)/2,
+        xmin = bounds[0].x,
+        xmax = bounds[1].x,
+        poly = path3d(clockwise_polygon(poly)),
+        anchor = get_anchor(anchor,center,BOT,BOT),
+        r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50),
+        r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50),
+        sides = segs(max(r1,r2)),
+        dir = sign(twist),
+        ang_step = 360/sides*dir,
+        anglist = [for(ang = [0:ang_step:twist-EPSILON]) ang,
+                   twist],
+        higbee1 = first_defined([higbee1, higbee, 0]),
+        higbee2 = first_defined([higbee2, higbee, 0]),
+        higang1 = 360 * higbee1 / (2 * r1 * PI),
+        higang2 = 360 * higbee2 / (2 * r2 * PI)
+    )
+    assert(higbee1>=0 && higbee2>=0)
+    assert(higang1 < dir*twist/2,"Higbee1 is more than half the threads")
+    assert(higang2 < dir*twist/2,"Higbee2 is more than half the threads")
+    let(
+        interp_ang = [
+                      for(i=idx(anglist,e=-2))
+                          each lerpn(anglist[i],anglist[i+1],
+                                     (higang1>0 && higang1>dir*anglist[i+1]
+                                      || (higang2>0 && higang2>dir*(twist-anglist[i]))) ? ceil((anglist[i+1]-anglist[i])/ang_step*higsample)
+                                                                                        : 1,
+                                     endpoint=false),
+                      last(anglist)
+                     ],
+        skewmat = affine3d_skew_xz(xa=atan2(r2-r1,h)),
+        points = [
+            for (a = interp_ang) let (
+                hsc = dir*a<higang1 ? _taperfunc(dir*a/higang1)
+                    : dir*(twist-a)<higang2 ? _taperfunc(dir*(twist-a)/higang2)
+                    : 1,
+                u = a/twist,
+                r = lerp(r1,r2,u),
+                mat = affine3d_zrot(a)
+                    * affine3d_translate([_ss_polygon_r(sides,a)*r, 0, h * (u-0.5)])
+                    * affine3d_xrot(90)
+                    * skewmat
+                    * scale([hsc,lerp(hsc,1,0.25),1], cp=[internal ? xmax : xmin, yctr, 0]),
+                pts = apply(mat, poly)
+            ) pts
+        ],
+        vnf = vnf_vertex_array(
+            points, col_wrap=true, caps=true, reverse=dir>0?true:false, 
+            style=higbee1>0 || higbee2>0 ? "quincunx" : "alt"
+        )
+    )
+    reorient(anchor,spin,orient, vnf=vnf, r1=r1, r2=r2, l=h, p=vnf);
+
+
+
+module spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal=false, anchor=CENTER, spin=0, orient=UP) {
+    vnf = spiral_sweep(poly, h, r, turns, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal);
+    attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
+        vnf_polyhedron(vnf, convexity=ceil(abs(2*turns)));
+        children();
+    }
+}
+
+
+
 // Function&Module: path_sweep()
 // Usage: As module
 //   path_sweep(shape, path, [method], [normal=], [closed=], [twist=], [twist_by_length=], [symmetry=], [last_normal=], [tangent=], [relaxed=], [caps=], [style=], [convexity=], [anchor=], [cp=], [spin=], [orient=], [atype=]) {attachments};
diff --git a/threading.scad b/threading.scad
index fd03d9fe..e55d0198 100644
--- a/threading.scad
+++ b/threading.scad
@@ -1154,7 +1154,7 @@ module generic_threaded_nut(
 //   pitch = Distance between threads.  Default: 2mm/thread
 //   thread_depth = Depth of threads from top to bottom.
 //   flank_angle = Angle of thread faces to plane perpendicular to screw.  Default: 15 degrees.
-//   twist = Number of degrees to rotate thread around.  Default: 720 degrees.
+//   turns = Number of revolutions to rotate thread around.  Default: 2.
 //   ---
 //   profile = If an asymmetrical thread profile is needed, it can be specified here.
 //   starts = The number of thread starts.  Default: 1
@@ -1203,18 +1203,18 @@ module generic_threaded_nut(
 //       right(14)back(14)text("angle",size=4,halign="center");
 //      }
 // Examples:
-//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, twist=900, $fn=72);
-//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, twist=900, higbee=1, $fn=72);
-//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, twist=720, higbee=2, internal=true, $fn=72);
-//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, twist=360, left_handed=true, higbee=1, $fn=36);
+//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, turns=2.5, $fn=72);
+//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, turns=2.5, higbee=1, $fn=72);
+//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, turns=2, higbee=2, internal=true, $fn=72);
+//   thread_helix(d=10, pitch=2, thread_depth=0.75, flank_angle=15, turns=1, left_handed=true, higbee=1, $fn=36);
 module thread_helix(
-    d, pitch, thread_depth, flank_angle, twist=720,
+    d, pitch, thread_depth, flank_angle, turns=2,
     profile, starts=1, left_handed=false, internal=false,
     d1, d2, higbee, higbee1, higbee2,
     anchor, spin, orient
 ) {
     dummy1=assert(is_undef(profile) || !any_defined([thread_depth, flank_angle]),"Cannot give thread_depth or flank_angle with a profile");
-    h = pitch*starts*twist/360;
+    h = pitch*starts*turns;
     r1 = get_radius(d1=d1, d=d, dflt=10);
     r2 = get_radius(d1=d2, d=d, dflt=10);
     profile = is_def(profile) ? profile :
@@ -1241,7 +1241,7 @@ module thread_helix(
     dir = left_handed? -1 : 1;
     attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
         zrot_copies(n=starts) {
-            spiral_sweep(pline, h=h, r1=r1, r2=r2, twist=twist*dir, higbee=higbee, higbee1=higbee1, higbee2=higbee2, internal=internal, anchor=CENTER);
+            spiral_sweep(pline, h=h, r1=r1, r2=r2, turns=turns*dir, higbee=higbee, higbee1=higbee1, higbee2=higbee2, internal=internal, anchor=CENTER);
         }
         children();
     }

From 7e388293e87a186c7733ffe9db4d0e04066b7caf Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 21:23:09 -0500
Subject: [PATCH 5/7] add centering option to move()

---
 tests/test_transforms.scad |  4 ++++
 transforms.scad            | 18 ++++++++++++++++--
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/tests/test_transforms.scad b/tests/test_transforms.scad
index 23145940..2721d564 100644
--- a/tests/test_transforms.scad
+++ b/tests/test_transforms.scad
@@ -23,6 +23,10 @@ module test_move() {
     // Verify that module at least doesn't crash.
     move(x=-5) move(y=-5) move(z=-5) move([-5,-5,-5]) union(){};
     move(x=5) move(y=5) move(z=5) move([5,5,5]) union(){};
+    sq = square(10);
+    assert_equal(move("centroid", sq), move(-centroid(sq),sq));
+    assert_equal(move("mean", vals), move(-mean(vals), vals));
+    assert_equal(move("box", vals), move(-mean(pointlist_bounds(vals)),vals));
 }
 test_move();
 
diff --git a/transforms.scad b/transforms.scad
index d4eb4cc2..53cfc1f5 100644
--- a/transforms.scad
+++ b/transforms.scad
@@ -77,9 +77,10 @@ _NO_ARG = [true,[123232345],false];
 // Usage: As Module
 //   move([x=], [y=], [z=]) ...
 //   move(v) ...
-// Usage: Translate Points
+// Usage: As a function to translate points, VNF, or Bezier patch
 //   pts = move(v, p);
 //   pts = move([x=], [y=], [z=], p=);
+//   pts = move(STRING, p);
 // Usage: Get Translation Matrix
 //   mat = move(v);
 //   mat = move([x=], [y=], [z=]);
@@ -95,11 +96,12 @@ _NO_ARG = [true,[123232345],false];
 //   * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the translated patch.
 //   * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the translated VNF.
 //   * Called as a function with the `p` argument, returns the translated point or list of points.
+//   * Called as a function with the `p` argument set to a VNF or a polygon and `v` set to "centroid", "mean" or "box", translates the argument to the centroid, mean, or bounding box center respectively.
 //   * Called as a function without a `p` argument, with a 2D offset vector `v`, returns an affine2d translation matrix.
 //   * Called as a function without a `p` argument, with a 3D offset vector `v`, returns an affine3d translation matrix.
 //
 // Arguments:
-//   v = An [X,Y,Z] vector to translate by.
+//   v = An [X,Y,Z] vector to translate by.  For function form with `p` is a point list or VNF, can be "centroid", "mean" or "box".  
 //   p = Either a point, or a list of points to be translated when used as a function.
 //   ---
 //   x = X axis translation.
@@ -139,12 +141,24 @@ _NO_ARG = [true,[123232345],false];
 //   mat2d = move([2,3]);    // Returns: [[1,0,2],[0,1,3],[0,0,1]]
 //   mat3d = move([2,3,4]);  // Returns: [[1,0,0,2],[0,1,0,3],[0,0,1,4],[0,0,0,1]]
 module move(v=[0,0,0], p, x=0, y=0, z=0) {
+    assert(!is_string(v),"Module form of `move()` does not accept string `v` arguments");
     assert(is_undef(p), "Module form `move()` does not accept p= argument.");
     assert(is_vector(v) && (len(v)==3 || len(v)==2), "Invalid value for `v`")
     translate(point3d(v)+[x,y,z]) children();
 }
 
 function move(v=[0,0,0], p=_NO_ARG, x=0, y=0, z=0) =
+    is_string(v) ? (
+        assert(is_vnf(p) || is_path(p),"String movements only work with point lists and VNFs")
+        let(
+             center = v=="centroid" ? centroid(p)
+                    : v=="mean" ? mean(p)
+                    : v=="box" ? mean(pointlist_bounds(p))
+                    : assert(false,str("Unknown string movement ",v))
+        )
+        move(-center,p=p, x=x,y=y,z=z)
+      )
+    :
     assert(is_vector(v) && (len(v)==3 || len(v)==2), "Invalid value for `v`")
     let(
         m = affine3d_translate(point3d(v)+[x,y,z])

From 05c5e4efe5d4d885713a479ce5ffef287c000d3a Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 21:50:32 -0500
Subject: [PATCH 6/7] bugfix

---
 skin.scad | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/skin.scad b/skin.scad
index 1f198860..b7dfd9cc 100644
--- a/skin.scad
+++ b/skin.scad
@@ -718,6 +718,8 @@ function spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, hi
 
 module spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal=false, anchor=CENTER, spin=0, orient=UP) {
     vnf = spiral_sweep(poly, h, r, turns, higbee, center, r1, r2, d, d1, d2, higbee1, higbee2, internal);
+    r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50);
+    r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50);
     attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
         vnf_polyhedron(vnf, convexity=ceil(abs(2*turns)));
         children();

From 837c74c10a52b527c3b805b5adffefb4fed23bc0 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 10 Jan 2022 22:28:35 -0500
Subject: [PATCH 7/7] bugfixes

---
 bottlecaps.scad | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/bottlecaps.scad b/bottlecaps.scad
index a91b9ea3..68ed8112 100644
--- a/bottlecaps.scad
+++ b/bottlecaps.scad
@@ -195,7 +195,7 @@ module pco1810_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
                 }
                 up(wall) cyl(d=cap_id, h=tamper_ring_h+wall, anchor=BOTTOM);
             }
-            up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, flank_angle=flank_angle, twist=810/360, higbee=thread_depth, internal=true, anchor=BOTTOM);
+            up(wall+2) thread_helix(d=thread_od-thread_depth*2, pitch=thread_pitch, thread_depth=thread_depth, flank_angle=flank_angle, turns=810/360, higbee=thread_depth, internal=true, anchor=BOTTOM);
         }
         children();
     }
@@ -310,7 +310,7 @@ module pco1881_neck(wall=2, anchor="support-ring", spin=0, orient=UP)
                         pitch=thread_pitch,
                         thread_depth=thread_h+0.1,
                         flank_angle=flank_angle,
-                        twist=650/360,
+                        turns=650/360,
                         higbee=thread_h*2,
                         anchor=TOP
                     );
@@ -379,7 +379,7 @@ module pco1881_cap(wall=2, texture="none", anchor=BOTTOM, spin=0, orient=UP)
                 }
                 up(wall) cyl(d=28.58, h=11.2+wall, anchor=BOTTOM);
             }
-            up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, flank_angle=15, twist=650/360, higbee=1.6, internal=true, anchor=BOTTOM);
+            up(wall+2) thread_helix(d=25.5, pitch=2.7, thread_depth=1.6, flank_angle=15, turns=650/360, higbee=1.6, internal=true, anchor=BOTTOM);
         }
         children();
     }