diff --git a/.github/workflows/docsgen.yml b/.github/workflows/docsgen.yml
index 9fa44bd..220a1da 100644
--- a/.github/workflows/docsgen.yml
+++ b/.github/workflows/docsgen.yml
@@ -1,12 +1,13 @@
-name: CI
+name: Docs
 on:
-  push:
-    branches:
-      - master
+  pull_request:
+    branches: [master]
+    types: [closed]
 
 jobs:
   GenerateDocs:
-    runs-on: macos-10.15
+    if: github.event.pull_request.merged == true
+    runs-on: ubuntu-latest
     steps:
     - name: Checkout
       uses: actions/checkout@v2
@@ -17,28 +18,32 @@ jobs:
         repository: revarbat/BOSL2.wiki
         path: BOSL2.wiki
 
-    - name: Install gifsicle
-      run: brew install gifsicle
+    - name: Apt Update
+      run: sudo apt update
 
-    - name: Install Pillow
-      run: sudo pip3 install Pillow
+    - name: Install Packages
+      run: sudo apt-get install -y python3-pip python3-dev python3-setuptools python3-pil gifsicle
 
-    - name: Install Docsgen
+    - name: Install openscad-docsgen
       run: sudo pip3 install openscad_docsgen
 
     - name: Install OpenSCAD
-      run: |
-        curl -L -o OpenSCAD.dmg https://files.openscad.org/snapshots/OpenSCAD-2021.05.07.dmg
-        hdiutil attach OpenSCAD.dmg
-        cp -a /Volumes/OpenSCAD/OpenSCAD.app /Applications/
-
-    - name: Generating Docs
       run: |
         cd $GITHUB_WORKSPACE
-        export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE)
+        wget https://files.openscad.org/OpenSCAD-2021.01-x86_64.AppImage
+        sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad
+        sudo chmod +x /usr/local/bin/openscad
         echo "::add-matcher::.github/openscad_docsgen.json"
-        openscad-docsgen -m -i -t -c -I *.scad
-        cd BOSL2.wiki
+
+    - name: Generating Docs Headless
+      run: |
+        export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE)
+        xvfb-run --server-args="-screen 0, 1280x720x24" -a \
+        openscad-docsgen -ticmI *.scad
+
+    - name: Commit Wiki Docs
+      run: |
+        cd $GITHUB_WORKSPACE/BOSL2.wiki
         git config user.name github-actions
         git config user.email github-actions@github.com
         git add --all
diff --git a/.github/workflows/forced_docsgen.yml b/.github/workflows/forced_docsgen.yml
new file mode 100644
index 0000000..6393b90
--- /dev/null
+++ b/.github/workflows/forced_docsgen.yml
@@ -0,0 +1,49 @@
+name: FDocs
+on:
+  workflow_dispatch:
+    branches: [master]
+
+jobs:
+  GenerateDocs:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v2
+
+    - name: Checkout Wiki
+      uses: actions/checkout@v2
+      with:
+        repository: revarbat/BOSL2.wiki
+        path: BOSL2.wiki
+
+    - name: Apt Update
+      run: sudo apt update
+
+    - name: Install Packages
+      run: sudo apt-get install -y python3-pip python3-dev python3-setuptools python3-pil gifsicle
+
+    - name: Install openscad-docsgen
+      run: sudo pip3 install openscad_docsgen
+
+    - name: Install OpenSCAD
+      run: |
+        cd $GITHUB_WORKSPACE
+        wget https://files.openscad.org/OpenSCAD-2021.01-x86_64.AppImage
+        sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad
+        sudo chmod +x /usr/local/bin/openscad
+        echo "::add-matcher::.github/openscad_docsgen.json"
+
+    - name: Generating Docs Headless
+      run: |
+        export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE)
+        xvfb-run --server-args="-screen 0, 1280x720x24" -a \
+        openscad-docsgen -ticmIf *.scad
+
+    - name: Commit Wiki Docs
+      run: |
+        cd $GITHUB_WORKSPACE/BOSL2.wiki
+        git config user.name github-actions
+        git config user.email github-actions@github.com
+        git add --all
+        git commit -m "Wiki docs auto-regen." && git push || true
+
diff --git a/.github/workflows/testwf.yml b/.github/workflows/testwf.yml
new file mode 100644
index 0000000..f99e8f6
--- /dev/null
+++ b/.github/workflows/testwf.yml
@@ -0,0 +1,49 @@
+name: TestWorkflow
+on:
+  workflow_dispatch:
+    branches: [master]
+
+jobs:
+  TestJob:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v2
+
+    - name: Checkout Wiki
+      uses: actions/checkout@v2
+      with:
+        repository: revarbat/BOSL2.wiki
+        path: BOSL2.wiki
+
+    - name: Setup OpenGL
+      uses: openrndr/setup-opengl@v1.1
+
+    - name: Apt Update
+      run: sudo apt update
+
+    - name: Install Packages
+      run: sudo apt-get install -y python3-pip python3-dev python3-setuptools python3-pil gifsicle
+
+    - name: Install openscad-docsgen
+      run: sudo pip3 install openscad_docsgen
+
+    - name: Install OpenSCAD
+      run: |
+        cd $GITHUB_WORKSPACE
+        wget https://files.openscad.org/OpenSCAD-2021.01-x86_64.AppImage
+        sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad
+        sudo chmod +x /usr/local/bin/openscad
+        echo "::add-matcher::.github/openscad_docsgen.json"
+
+    - name: Make SCAD File
+      run: |
+        cd $GITHUB_WORKSPACE
+        echo "cube(50, center=100);" > testwf.scad
+
+    - name: TestScript
+      run: |
+        export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE)
+        xvfb-run --server-args="-screen 0, 1280x720x24" -a \
+        glxinfo
+
diff --git a/.openscad_gendocs_rc b/.openscad_gendocs_rc
index 0d6030c..6591e1b 100644
--- a/.openscad_gendocs_rc
+++ b/.openscad_gendocs_rc
@@ -46,4 +46,5 @@ PrioritizeFiles:
 DefineHeader(BulletList): Side Effects
 DefineHeader(Table:Anchor Name|Position): Extra Anchors
 DefineHeader(Table:Name|Definition): Terminology
+DefineHeader(BulletList): Requirements
 
diff --git a/attachments.scad b/attachments.scad
index 7858789..915b641 100644
--- a/attachments.scad
+++ b/attachments.scad
@@ -328,8 +328,11 @@ function attach_geom_size(geom) =
         [2*maxxr, 2*maxyr,l]
     ) : type == "spheroid"? ( //r
         let( r=geom[1] )
-        is_num(r)? [2,2,2]*r : vmul([2,2,2],point3d(r))
+        is_num(r)? [2,2,2]*r : v_mul([2,2,2],point3d(r))
     ) : type == "vnf_extent" || type=="vnf_isect"? ( //vnf
+        let(
+            vnf = geom[1]
+        ) vnf==EMPTY_VNF? [0,0,0] :
         let(
             mm = pointlist_bounds(geom[1][0]),
             delt = mm[1]-mm[0]
@@ -341,7 +344,7 @@ function attach_geom_size(geom) =
         ) [maxx, size.y]
     ) : type == "circle"? ( //r
         let( r=geom[1] )
-        is_num(r)? [2,2]*r : vmul([2,2],point2d(r))
+        is_num(r)? [2,2]*r : v_mul([2,2],point2d(r))
     ) : type == "path_isect" || type == "path_extent"? ( //path
         let(
             mm = pointlist_bounds(geom[1]),
@@ -425,7 +428,7 @@ function attach_transform(anchor, spin, orient, geom, p) =
             )
         )
     ) is_undef(p)? m :
-    is_vnf(p)? [apply(m, p[0]), p[1]] :
+    is_vnf(p)? [(p==EMPTY_VNF? p : apply(m, p[0])), p[1]] :
     apply(m, p);
 
 
@@ -445,7 +448,8 @@ function attach_transform(anchor, spin, orient, geom, p) =
 function find_anchor(anchor, geom) =
     let(
         cp = select(geom,-3),
-        offset = anchor==CENTER? CENTER : select(geom,-2),
+        offset_raw = select(geom,-2),
+        offset = [for (i=[0:2]) anchor[i]==0? 0 : offset_raw[i]],  // prevents bad centering.
         anchors = last(geom),
         type = geom[0]
     )
@@ -472,8 +476,8 @@ function find_anchor(anchor, geom) =
             h = size.z,
             u = (anch.z+1)/2,
             axy = point2d(anch),
-            bot = point3d(vmul(point2d(size)/2,axy),-h/2),
-            top = point3d(vmul(point2d(size2)/2,axy)+shift,h/2),
+            bot = point3d(v_mul(point2d(size)/2,axy),-h/2),
+            top = point3d(v_mul(point2d(size2)/2,axy)+shift,h/2),
             pos = point3d(cp) + lerp(bot,top,u) + offset,
             sidevec = unit(rot(from=UP, to=top-bot, p=point3d(axy)),UP),
             vvec = anch==CENTER? UP : unit([0,0,anch.z],UP),
@@ -493,8 +497,8 @@ function find_anchor(anchor, geom) =
             anch = rot(from=axis, to=UP, p=anchor),
             u = (anch.z+1)/2,
             axy = unit(point2d(anch),[0,0]),
-            bot = point3d(vmul(r1,axy), -l/2),
-            top = point3d(vmul(r2,axy)+shift, l/2),
+            bot = point3d(v_mul(r1,axy), -l/2),
+            top = point3d(v_mul(r2,axy)+shift, l/2),
             pos = point3d(cp) + lerp(bot,top,u) + offset,
             sidevec = rot(from=UP, to=top-bot, p=point3d(axy)),
             vvec = anch==CENTER? UP : unit([0,0,anch.z],UP),
@@ -510,12 +514,14 @@ function find_anchor(anchor, geom) =
             rr = geom[1],
             r = is_num(rr)? [rr,rr,rr] : point3d(rr),
             anchor = unit(point3d(anchor),CENTER),
-            pos = point3d(cp) + vmul(r,anchor) + point3d(offset),
-            vec = unit(vmul(r,anchor),UP)
+            pos = point3d(cp) + v_mul(r,anchor) + point3d(offset),
+            vec = unit(v_mul(r,anchor),UP)
         ) [anchor, pos, vec, oang]
     ) : type == "vnf_isect"? ( //vnf
         let(
-            vnf=geom[1],
+            vnf=geom[1]
+        ) vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor), 0] :
+        let(
             eps = 1/2048,
             points = vnf[0],
             faces = vnf[1],
@@ -565,7 +571,9 @@ function find_anchor(anchor, geom) =
         [anchor, pos, n, oang]
     ) : type == "vnf_extent"? ( //vnf
         let(
-            vnf=geom[1],
+            vnf=geom[1]
+        ) vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor), 0] :
+        let(
             rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]),
             maxx = max(subindex(rpts,0)),
             idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i],
@@ -589,8 +597,8 @@ function find_anchor(anchor, geom) =
             rr = geom[1],
             r = is_num(rr)? [rr,rr] : point2d(rr),
             anchor = unit(point2d(anchor),[0,0]),
-            pos = point2d(cp) + vmul(r,anchor) + point2d(offset),
-            vec = unit(vmul(r,anchor),[0,1])
+            pos = point2d(cp) + v_mul(r,anchor) + point2d(offset),
+            vec = unit(v_mul(r,anchor),[0,1])
         ) [anchor, pos, vec, 0]
     ) : type == "path_isect"? ( //path
         let(
@@ -986,6 +994,92 @@ module attachable(
 }
 
 
+// Module: atext()
+// Topics: Attachments, Text
+// Usage:
+//   atext(text, <h>, <size>, <font>);
+// Description:
+//   Creates a 3D text block that can be attached to other attachable objects.
+//   NOTE: This cannot have children attached to it.
+// Arguments:
+//   text = The text string to instantiate as an object.
+//   h = The height to which the text should be extruded.  Default: 1
+//   size = The font size used to create the text block.  Default: 10
+//   font = The name of the font used to create the text block.  Default: "Courier"
+//   ---
+//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#anchor).  Default: `"baseline"`
+//   spin = Rotate this many degrees around the Z axis.  See [spin](attachments.scad#spin).  Default: `0`
+//   orient = Vector to rotate top towards.  See [orient](attachments.scad#orient).  Default: `UP`
+// See Also: attachable()
+// Extra Anchors:
+//   "baseline" = Anchors at the baseline of the text, at the start of the string.
+//   str("baseline",VECTOR) = Anchors at the baseline of the text, modified by the X and Z components of the appended vector.
+// Examples:
+//   atext("Foobar", h=3, size=10);
+//   atext("Foobar", h=2, size=12, font="Helvetica");
+//   atext("Foobar", h=2, anchor=CENTER);
+//   atext("Foobar", h=2, anchor=str("baseline",CENTER));
+//   atext("Foobar", h=2, anchor=str("baseline",BOTTOM+RIGHT));
+// Example: Using line_of() distributor
+//   txt = "This is the string.";
+//   line_of(spacing=[10,-5],n=len(txt))
+//       atext(txt[$idx], size=10, anchor=CENTER);
+// Example: Using arc_of() distributor
+//   txt = "This is the string";
+//   arc_of(r=50, n=len(txt), sa=0, ea=180)
+//       atext(select(txt,-1-$idx), size=10, anchor=str("baseline",CENTER), spin=-90);
+module atext(text, h=1, size=9, font="Courier", anchor="baseline", spin=0, orient=UP) {
+    no_children($children);
+    dummy1 =
+        assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
+        assert(is_undef(spin)   || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
+        assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient));
+    anchor = default(anchor, CENTER);
+    spin =   default(spin,   0);
+    orient = default(orient, UP);
+    geom = attach_geom(size=[size,size,h]);
+    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_leading(p," "))]
+        ) vec;
+    ha = anchor=="baseline"? "left" :
+        anchor==anch && is_string(anchor)? "center" :
+        anch.x<0? "left" :
+        anch.x>0? "right" :
+        "center";
+    va = starts_with(anchor,"baseline")? "baseline" :
+        anchor==anch && is_string(anchor)? "center" :
+        anch.y<0? "bottom" :
+        anch.y>0? "top" :
+        "center";
+    base = anchor=="baseline"? CENTER :
+        anchor==anch && is_string(anchor)? CENTER :
+        anch.z<0? BOTTOM :
+        anch.z>0? TOP :
+        CENTER;
+    m = attach_transform(base,spin,orient,geom);
+    multmatrix(m) {
+        $parent_anchor = anchor;
+        $parent_spin   = spin;
+        $parent_orient = orient;
+        $parent_geom   = geom;
+        $parent_size   = attach_geom_size(geom);
+        $attach_to   = undef;
+        do_show = attachment_is_shown($tags);
+        if (do_show) {
+            if (is_undef($color)) {
+                linear_extrude(height=h, center=true)
+                    text(text=text, size=size, halign=ha, valign=va);
+            } else color($color) {
+                $color = undef;
+                linear_extrude(height=h, center=true)
+                    text(text=text, size=size, halign=ha, valign=va);
+            }
+        }
+    }
+}
+
 
 // Section: Attachment Positioning
 
@@ -1057,11 +1151,12 @@ module attach(from, to, overlap, norot=false)
         $attach_to = to;
         $attach_anchor = anch;
         $attach_norot = norot;
+        olap = two_d? [0,-overlap,0] : [0,0,-overlap];
         if (norot || (norm(anch[2]-UP)<1e-9 && anch[3]==0)) {
-            translate(anch[1]) translate([0,0,-overlap]) children();
+            translate(anch[1]) translate(olap) children();
         } else {
             fromvec = two_d? BACK : UP;
-            translate(anch[1]) rot(anch[3],from=fromvec,to=anch[2]) translate([0,0,-overlap]) children();
+            translate(anch[1]) rot(anch[3],from=fromvec,to=anch[2]) translate(olap) children();
         }
     }
 }
@@ -1137,10 +1232,10 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) {
         psize = point3d($parent_size);
         length = [for (i=[0:2]) if(!vec[i]) psize[i]][0]+0.1;
         rotang =
-            vec.z<0? [90,0,180+vang(point2d(vec))] :
-            vec.z==0 && sign(vec.x)==sign(vec.y)? 135+vang(point2d(vec)) :
-            vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+vang(point2d(vec))] :
-            [-90,0,180+vang(point2d(vec))];
+            vec.z<0? [90,0,180+v_theta(vec)] :
+            vec.z==0 && sign(vec.x)==sign(vec.y)? 135+v_theta(vec) :
+            vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+v_theta(vec)] :
+            [-90,0,180+v_theta(vec)];
         translate(anch[1]) {
             rot(rotang) {
                 linear_extrude(height=length, center=true, convexity=convexity) {
@@ -1191,8 +1286,8 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
         $attach_norot = true;
         $tags = "mask";
         rotang = vec.z<0?
-            [  0,0,180+vang(point2d(vec))-45] :
-            [180,0,-90+vang(point2d(vec))-45];
+            [  0,0,180+v_theta(vec)-45] :
+            [180,0,-90+v_theta(vec)-45];
         translate(anch[1]) {
             rot(rotang) {
                 render(convexity=convexity)
@@ -1223,8 +1318,18 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
 // Topics: Attachments
 // See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask()
 // Description:
-//   Takes a 3D mask shape, and attaches it to the given edges, with the appropriate orientation to be `diff()`ed away.
-//   For a more step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
+//   Takes a 3D mask shape, and attaches it to the given edges, with the appropriate orientation to be
+//   `diff()`ed away.  The mask shape should be vertically oriented (Z-aligned) with the back-right
+//   quadrant (X+Y+) shaped to be diffed away from the edge of parent attachable shape.  For a more
+//   step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
+// Figure: A Typical Edge Rounding Mask
+//   module roundit(l,r) difference() {
+//       translate([-1,-1,-l/2])
+//           cube([r+1,r+1,l]);
+//       translate([r,r])
+//           cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
+//   }
+//   roundit(l=30,r=10);
 // Arguments:
 //   edges = Edges to mask.  See the docs for [`edges()`](edges.scad#edges) to see acceptable values.  Default: All edges.
 //   except = Edges to explicitly NOT mask.  See the docs for [`edges()`](edges.scad#edges) to see acceptable values.  Default: No edges.
@@ -1252,10 +1357,10 @@ module edge_mask(edges=EDGES_ALL, except=[]) {
         $attach_norot = true;
         $tags = "mask";
         rotang =
-            vec.z<0? [90,0,180+vang(point2d(vec))] :
-            vec.z==0 && sign(vec.x)==sign(vec.y)? 135+vang(point2d(vec)) :
-            vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+vang(point2d(vec))] :
-            [-90,0,180+vang(point2d(vec))];
+            vec.z<0? [90,0,180+v_theta(vec)] :
+            vec.z==0 && sign(vec.x)==sign(vec.y)? 135+v_theta(vec) :
+            vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+v_theta(vec)] :
+            [-90,0,180+v_theta(vec)];
         translate(anch[1]) rot(rotang) children();
     }
 }
@@ -1296,8 +1401,8 @@ module corner_mask(corners=CORNERS_ALL, except=[]) {
         $attach_norot = true;
         $tags = "mask";
         rotang = vec.z<0?
-            [  0,0,180+vang(point2d(vec))-45] :
-            [180,0,-90+vang(point2d(vec))-45];
+            [  0,0,180+v_theta(vec)-45] :
+            [180,0,-90+v_theta(vec)-45];
         translate(anch[1]) rot(rotang) children();
     }
 }
diff --git a/beziers.scad b/beziers.scad
index feacb48..51808f8 100644
--- a/beziers.scad
+++ b/beziers.scad
@@ -1461,7 +1461,7 @@ function bezier_patch_flat(size=[100,100], N=4, spin=0, orient=UP, trans=[0,0,0]
         patch = [
             for (x=[0:1:N]) [
                 for (y=[0:1:N])
-                vmul(point3d(size), [x/N-0.5, 0.5-y/N, 0])
+                v_mul(point3d(size), [x/N-0.5, 0.5-y/N, 0])
             ]
         ],
         m = move(trans) * rot(a=spin, from=UP, to=orient)
diff --git a/bottlecaps.scad b/bottlecaps.scad
index a65da94..49aa340 100644
--- a/bottlecaps.scad
+++ b/bottlecaps.scad
@@ -603,9 +603,9 @@ function generic_bottle_cap(
 ) = no_function("generic_bottle_cap");
 
 
-// Module: thread_adapter_NC()
+// Module: bottle_adapter_neck_to_cap()
 // Usage:
-//   thread_adapter_NC(wall, <texture>);
+//   bottle_adapter_neck_to_cap(wall, <texture>);
 // Description:
 //   Creates a threaded neck to cap adapter
 // Arguments:
@@ -628,8 +628,8 @@ function generic_bottle_cap(
 //   d = Distance between bottom of neck and top of cap
 //   taper_lead_in = Length to leave straight before tapering on tube between neck and cap if exists.
 // Examples:
-//   thread_adapter_NC();
-module thread_adapter_NC(
+//   bottle_adapter_neck_to_cap();
+module bottle_adapter_neck_to_cap(
     wall,
     texture = "none",
     cap_wall = 2,
@@ -708,17 +708,17 @@ module thread_adapter_NC(
     }
 }
 
-function thread_adapter_NC(
+function bottle_adapter_neck_to_cap(
     wall, texture, cap_wall, cap_h, cap_thread_od,
     tolerance, cap_neck_od, cap_neck_id, cap_thread_taper,
     cap_thread_pitch, neck_d, neck_id, neck_thread_od,
     neck_h, neck_thread_pitch, neck_support_od, d, taper_lead_in
-) = no_fuction("thread_adapter_NC");
+) = no_fuction("bottle_adapter_neck_to_cap");
 
 
-// Module: thread_adapter_CC()
+// Module: bottle_adapter_cap_to_cap()
 // Usage:
-//   thread_adapter_CC(wall, <texture>);
+//   bottle_adapter_cap_to_cap(wall, <texture>);
 // Description:
 //   Creates a threaded cap to cap adapter.
 // Arguments:
@@ -738,8 +738,8 @@ function thread_adapter_NC(
 //   neck_id2 = Inner diameter of cutout in bottom cap.
 //   taper_lead_in = Length to leave straight before tapering on tube between caps if exists.
 // Examples:
-//   thread_adapter_CC();
-module thread_adapter_CC(
+//   bottle_adapter_cap_to_cap();
+module bottle_adapter_cap_to_cap(
     wall = 2,
     texture = "none",
     cap_h1 = 11.2,
@@ -822,16 +822,16 @@ module thread_adapter_CC(
     }
 }
 
-function thread_adapter_CC(
+function bottle_adapter_cap_to_cap(
     wall, texture, cap_h1, cap_thread_od1, tolerance,
     cap_neck_od1, cap_thread_pitch1, cap_h2, cap_thread_od2,
     cap_neck_od2, cap_thread_pitch2, d, neck_id1, neck_id2, taper_lead_in
-) = no_function("thread_adapter_CC");
+) = no_function("bottle_adapter_cap_to_cap");
 
 
-// Module: thread_adapter_NN()
+// Module: bottle_adapter_neck_to_neck()
 // Usage:
-//   thread_adapter_NN();
+//   bottle_adapter_neck_to_neck();
 // Description:
 //   Creates a threaded neck to neck adapter.
 // Arguments:
@@ -851,8 +851,8 @@ function thread_adapter_CC(
 //   taper_lead_in = Length to leave straight before tapering on tube between necks if exists.
 //   wall = Thickness of tube wall between necks.  Leave undefined to match outer diameters with the neckODs/supportODs.  
 // Examples:
-//   thread_adapter_NN();
-module thread_adapter_NN(
+//   bottle_adapter_neck_to_neck();
+module bottle_adapter_neck_to_neck(
     d = 0,
     neck_od1 = 25,
     neck_id1 = 21.4,
@@ -939,12 +939,12 @@ module thread_adapter_NN(
     }
 }
 
-function thread_adapter_NN(
+function bottle_adapter_neck_to_neck(
     d, neck_od1, neck_id1, thread_od1, height1,
     support_od1, thread_pitch1, neck_od2, neck_id2,
     thread_od2, height2, support_od2,
     pitch2, taper_lead_in, wall
-) = no_fuction("thread_adapter_NN");
+) = no_fuction("bottle_adapter_neck_to_neck");
 
 
 
diff --git a/cubetruss.scad b/cubetruss.scad
index 880a301..670a5c0 100644
--- a/cubetruss.scad
+++ b/cubetruss.scad
@@ -101,6 +101,66 @@ module cubetruss_segment(size, strut, bracing, anchor=CENTER, spin=0, orient=UP)
 }
 
 
+// Module: cubetruss_support()
+// Usage:
+//   cubetruss_support(<size>, <strut>);
+// Description:
+//   Creates a single cubetruss support.
+// Arguments:
+//   size = The length of each side of the cubetruss cubes.  Default: `$cubetruss_size` (usually 30)
+//   strut = The width of the struts on the cubetruss cubes.  Default: `$cubetruss_strut_size` (usually 3)
+//   extents = If given as an integer, specifies the number of vertical segments for the support.  If given as a list of 3 integers, specifies the number of segments in the X, Y, and Z directions.  Default: 1.
+//   ---
+//   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.  See [spin](attachments.scad#spin).  Default: `0`
+//   orient = Vector to rotate top towards.  See [orient](attachments.scad#orient).  Default: `UP`
+// Topics: Attachable, Trusses
+// Example(VPT=[0,0,0],VPD=150):
+//   cubetruss_support();
+// Example(VPT=[0,0,0],VPD=200):
+//   cubetruss_support(extents=2);
+// Example(VPT=[0,0,0],VPD=250):
+//   cubetruss_support(extents=3);
+// Example(VPT=[0,0,0],VPD=350):
+//   cubetruss_support(extents=[2,2,3]);
+// Example(VPT=[0,0,0],VPD=150):
+//   cubetruss_support(strut=4);
+// Example(VPT=[0,0,0],VPD=260):
+//   cubetruss_support(extents=2) show_anchors();
+module cubetruss_support(size, strut, extents=1, anchor=CENTER, spin=0, orient=UP) {
+    extents = is_num(extents)? [1,1,extents] : extents;
+    size = is_undef(size)? $cubetruss_size : size;
+    strut = is_undef(strut)? $cubetruss_strut_size : strut;
+    assert(is_int(extents.x) && extents.x > 0);
+    assert(is_int(extents.y) && extents.y > 0);
+    assert(is_int(extents.z) && extents.z > 0);
+    w = (size-strut) * extents.x + strut;
+    l = (size-strut) * extents.y + strut;
+    h = (size-strut) * extents.z + strut;
+    attachable(anchor,spin,orient, size=[w,l,h], size2=[l,0], shift=[0,l/2], axis=DOWN) {
+        xcopies(size-strut, n=extents.x) {
+            difference() {
+                half_of(BACK/extents.y + UP/extents.z, s=size*(max(extents)+1))
+                    cube([size,l,h], center=true);
+                half_of(BACK/extents.y + UP/extents.z, cp=strut, s=size*(max(extents)+1)) {
+                    ycopies(size-strut, n=extents.y) {
+                        zcopies(size-strut, n=extents.z) {
+                            cyl(h=size+1, d=size-2*strut, circum=true, realign=true, orient=RIGHT, $fn=8);
+                            cyl(h=size+1, d=size-2*strut, circum=true, realign=true, $fn=8);
+                            cube(size-2*strut, center=true);
+                        }
+                    }
+                }
+                zcopies(size-strut, n=extents.z) {
+                    cyl(h=extents.y*size+1, d=size-2*strut, circum=true, realign=true, orient=BACK, $fn=8);
+                }
+            }
+        }
+        children();
+    }
+}
+
+
 // Module: cubetruss_clip()
 // Usage:
 //   cubetruss_clip(extents, <size>, <strut>, <clipthick>);
@@ -370,7 +430,7 @@ module cubetruss_uclip(dual=true, size, strut, clipthick, anchor=CENTER, spin=0,
 //   spin = Rotate this many degrees around the Z axis.  See [spin](attachments.scad#spin).  Default: `0`
 //   orient = Vector to rotate top towards.  See [orient](attachments.scad#orient).  Default: `UP`
 // Topics: Attachable, Trusses
-// Examples(FlatSpin,VPD=444):
+// Examples:
 //   cubetruss(extents=3);
 //   cubetruss(extents=3, clips=FRONT);
 //   cubetruss(extents=3, clips=[FRONT,BACK]);
@@ -405,7 +465,7 @@ module cubetruss(extents=6, clips=[], bracing, size, strut, clipthick, anchor=CE
             }
             if (clipthick > 0) {
                 for (vec = clips) {
-                    exts = vabs(rot(from=FWD, to=vec, p=extents));
+                    exts = v_abs(rot(from=FWD, to=vec, p=extents));
                     rot(from=FWD,to=vec) {
                         for (zrow = [0:1:exts.z-1]) {
                             up((zrow-(exts.z-1)/2)*(size-strut)) {
@@ -440,7 +500,7 @@ module cubetruss(extents=6, clips=[], bracing, size, strut, clipthick, anchor=CE
 //   spin = Rotate this many degrees around the Z axis.  See [spin](attachments.scad#spin).  Default: `0`
 //   orient = Vector to rotate top towards.  See [orient](attachments.scad#orient).  Default: `UP`
 // Topics: Attachable, Trusses
-// Examples(FlatSpin):
+// Examples:
 //   cubetruss_corner(extents=2);
 //   cubetruss_corner(extents=2, h=2);
 //   cubetruss_corner(extents=[3,3,0,0,2]);
@@ -452,12 +512,12 @@ module cubetruss_corner(h=1, extents=[1,1,0,0,1], bracing, size, strut, clipthic
     bracing = is_undef(bracing)? $cubetruss_bracing : bracing;
     clipthick = is_undef(clipthick)? $cubetruss_clip_thickness : clipthick;
     exts = is_vector(extents)? list_fit(extents,5,fill=0) : [extents, extents, 0, 0, extents];
-    s = [cubetruss_dist(1+exts[0]+exts[2],1), cubetruss_dist(1+exts[1]+exts[3],1), cubetruss_dist(h+exts[4],1)];
-    offset = [cubetruss_dist(exts[0]-exts[2],0), cubetruss_dist(exts[1]-exts[3],0), cubetruss_dist(exts[4],0)]/2;
+    s = [cubetruss_dist(exts[0]+1+exts[2],1), cubetruss_dist(exts[1]+1+exts[3],1), cubetruss_dist(h+exts[4],1)];
+    offset = [cubetruss_dist(exts[0]-exts[2],0), cubetruss_dist(exts[1]-exts[3],0), cubetruss_dist(h+exts[4]-1,0)]/2;
     attachable(anchor,spin,orient, size=s, offset=offset) {
         union() {
             for (zcol = [0:h-1]) {
-                up((size-strut+0.01)*zcol) {
+                up((size-strut)*zcol) {
                     cubetruss_segment(size=size, strut=strut, bracing=bracing);
                 }
             }
diff --git a/debug.scad b/debug.scad
index 4bb8479..920eec9 100644
--- a/debug.scad
+++ b/debug.scad
@@ -387,13 +387,24 @@ module show_anchors(s=10, std=true, custom=true) {
                 color("black")
                 noop($tags="anchor-arrow") {
                     xrot(two_d? 0 : 90) {
-                        up(s/10) {
-                            linear_extrude(height=0.01, convexity=12, center=true) {
-                                text(text=anchor[0], size=s/4, halign="center", valign="center");
+                        back(s/3) {
+                            yrot_copies(n=2)
+                            up(s/30) {
+                                linear_extrude(height=0.01, convexity=12, center=true) {
+                                    text(text=anchor[0], size=s/4, halign="center", valign="center");
+                                }
                             }
                         }
                     }
                 }
+                color([1, 1, 1, 0.4])
+                noop($tags="anchor-arrow") {
+                    xrot(two_d? 0 : 90) {
+                        back(s/3) {
+                            zcopies(s/21) cube([s/4.5*len(anchor[0]), s/3, 0.01], center=true);
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/distributors.scad b/distributors.scad
index 588acb8..6010b12 100644
--- a/distributors.scad
+++ b/distributors.scad
@@ -520,20 +520,20 @@ module grid2d(spacing, n, size, stagger=false, inside=undef)
         ) :
         is_vector(spacing)? assert(len(spacing)==2) spacing :
         size!=undef? (
-            is_num(n)? vdiv(size,(n-1)*[1,1]) :
-            is_vector(n)? assert(len(n)==2) vdiv(size,n-[1,1]) :
-            vdiv(size,(stagger==false? [1,1] : [2,2]))
+            is_num(n)? v_div(size,(n-1)*[1,1]) :
+            is_vector(n)? assert(len(n)==2) v_div(size,n-[1,1]) :
+            v_div(size,(stagger==false? [1,1] : [2,2]))
         ) :
         undef;
     n = is_num(n)? [n,n] :
         is_vector(n)? assert(len(n)==2) n :
-        size!=undef && spacing!=undef? vfloor(vdiv(size,spacing))+[1,1] :
+        size!=undef && spacing!=undef? v_floor(v_div(size,spacing))+[1,1] :
         [2,2];
-    offset = vmul(spacing, n-[1,1])/2;
+    offset = v_mul(spacing, n-[1,1])/2;
     if (stagger == false) {
         for (row = [0:1:n.y-1]) {
             for (col = [0:1:n.x-1]) {
-                pos = vmul([col,row],spacing) - offset;
+                pos = v_mul([col,row],spacing) - offset;
                 if (
                     is_undef(inside) ||
                     (is_path(inside) && point_in_polygon(pos, inside)>=0) ||
@@ -556,7 +556,7 @@ module grid2d(spacing, n, size, stagger=false, inside=undef)
             if (rowcols > 0) {
                 for (col = [0:1:rowcols-1]) {
                     rowdx = (row%2 != staggermod)? spacing.x : 0;
-                    pos = vmul([2*col,row],spacing) + [rowdx,0] - offset;
+                    pos = v_mul([2*col,row],spacing) + [rowdx,0] - offset;
                     if (
                         is_undef(inside) ||
                         (is_path(inside) && point_in_polygon(pos, inside)>=0) ||
@@ -616,7 +616,7 @@ module grid3d(xa=[0], ya=[0], za=[0], n=undef, spacing=undef)
             for (yi = [0:1:n.y-1]) {
                 for (zi = [0:1:n.z-1]) {
                     $idx = [xi,yi,zi];
-                    $pos = vmul(spacing, $idx - (n-[1,1,1])/2);
+                    $pos = v_mul(spacing, $idx - (n-[1,1,1])/2);
                     translate($pos) children();
                 }
             }
@@ -989,7 +989,7 @@ module arc_of(
 //
 // Example:
 //   ovoid_spread(n=500, d=100, cone_ang=180)
-//       color(unit(point3d(vabs($pos))))
+//       color(unit(point3d(v_abs($pos))))
 //           cylinder(d=8, h=10, center=false);
 module ovoid_spread(r=undef, d=undef, n=100, cone_ang=90, scale=[1,1,1], perp=true)
 {
@@ -1004,7 +1004,7 @@ module ovoid_spread(r=undef, d=undef, n=100, cone_ang=90, scale=[1,1,1], perp=tr
     for ($idx = idx(theta_phis)) {
         tp = theta_phis[$idx];
         xyz = spherical_to_xyz(r, tp[0], tp[1]);
-        $pos = vmul(xyz,point3d(scale,1));
+        $pos = v_mul(xyz,point3d(scale,1));
         $theta = tp[0];
         $phi = tp[1];
         $rad = r;
diff --git a/edges.scad b/edges.scad
index 43d58c5..0ddc880 100644
--- a/edges.scad
+++ b/edges.scad
@@ -139,7 +139,7 @@ function _edge_set(v) =
                     str(v, " must be a vector, edge array, or one of ", valid_values)
                 ) v
             ) :
-            let(nonz = sum(vabs(v)))
+            let(nonz = sum(v_abs(v)))
             nonz==2? (v==v2) :  // Edge: return matching edge.
             let(
                 matches = count_true([
diff --git a/gears.scad b/gears.scad
index aea28db..8a35a42 100644
--- a/gears.scad
+++ b/gears.scad
@@ -800,7 +800,6 @@ module spur_gear(
                     backlash = backlash,
                     interior = interior
                 );
-                circle(d=shaft_diam+4);
             }
             if (shaft_diam > 0) {
                 cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2)));
@@ -929,7 +928,7 @@ function bevel_gear(
         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),
+        radcpang = v_theta(radcp),
         sang = radcpang - (180-angC1),
         eang = radcpang - (180-angC2),
         profile = gear_tooth_profile(
@@ -945,7 +944,7 @@ function bevel_gear(
         verts1 = [
             for (v = lerpn(0,1,slices+1)) let(
                 p = radcp + polar_to_xy(cutter_radius, lerp(sang,eang,v)),
-                ang = vang(p)-90,
+                ang = v_theta(p)-90,
                 dist = norm(p)
             ) [
                 let(
diff --git a/geometry.scad b/geometry.scad
index 36e30dc..e2569b9 100644
--- a/geometry.scad
+++ b/geometry.scad
@@ -80,7 +80,7 @@ function collinear(a, b, c, eps=EPSILON) =
 
 // Function: point_line_distance()
 // Usage:
-//   point_line_distance(line, pt);
+//   point_line_distance(pt, line);
 // Description:
 //   Finds the perpendicular distance of a point `pt` from the line `line`.
 // Arguments:
diff --git a/math.scad b/math.scad
index 73659ca..96bd475 100644
--- a/math.scad
+++ b/math.scad
@@ -237,7 +237,7 @@ function u_sub(a,b) = is_undef(a) || is_undef(b)? undef : a - b;
 //   b = Second value.
 function u_mul(a,b) =
     is_undef(a) || is_undef(b)? undef :
-    is_vector(a) && is_vector(b)? vmul(a,b) :
+    is_vector(a) && is_vector(b)? v_mul(a,b) :
     a * b;
 
 
@@ -252,7 +252,7 @@ function u_mul(a,b) =
 //   b = Second value.
 function u_div(a,b) =
     is_undef(a) || is_undef(b)? undef :
-    is_vector(a) && is_vector(b)? vdiv(a,b) :
+    is_vector(a) && is_vector(b)? v_div(a,b) :
     a / b;
 
 
@@ -674,7 +674,7 @@ function _product(v, i=0, _tot) =
     i>=len(v) ? _tot :
     _product( v, 
               i+1, 
-              ( is_vector(v[i])? vmul(_tot,v[i]) : _tot*v[i] ) );
+              ( is_vector(v[i])? v_mul(_tot,v[i]) : _tot*v[i] ) );
                
 
 
@@ -711,7 +711,7 @@ function _cumprod_vec(v,_i=0,_acc=[]) =
         v, _i+1,
         concat(
             _acc,
-            [_i==0 ? v[_i] : vmul(_acc[len(_acc)-1],v[_i])]
+            [_i==0 ? v[_i] : v_mul(_acc[len(_acc)-1],v[_i])]
         )
     );
 
@@ -1261,8 +1261,10 @@ function compare_lists(a, b) =
 
 // Function: any()
 // Usage:
-//   b = any(l);
-//   b = any(l,func);
+//   bool = any(l);
+//   bool = any(l,func);   // Requires OpenSCAD 2021.01 or later.
+// Requirements:
+//   Requires OpenSCAD 2021.01 or later to use the `func=` argument.
 // Description:
 //   Returns true if any item in list `l` evaluates as true.
 // Arguments:
@@ -1292,8 +1294,10 @@ function _any_bool(l, i=0, out=false) =
 
 // Function: all()
 // Usage:
-//   b = all(l);
-//   b = all(l,func);
+//   bool = all(l);
+//   bool = all(l,func);   // Requires OpenSCAD 2021.01 or later.
+// Requirements:
+//   Requires OpenSCAD 2021.01 or later to use the `func=` argument.
 // Description:
 //   Returns true if all items in list `l` evaluate as true.  If `func` is given a function liteal
 //   of signature (x), returning bool, then that function literal is evaluated for each list item.
@@ -1325,8 +1329,10 @@ function _all_bool(l, i=0, out=true) =
 
 // Function: count_true()
 // Usage:
-//   n = count_true(l,<nmax=>)
-//   n = count_true(l,func,<nmax=>)
+//   n = count_true(l,<nmax=>);
+//   n = count_true(l,func,<nmax=>);  // Requires OpenSCAD 2021.01 or later.
+// Requirements:
+//   Requires OpenSCAD 2021.01 or later to use the `func=` argument.
 // Description:
 //   Returns the number of items in `l` that evaluate as true.
 //   If `l` is a lists of lists, this is applied recursively to each
@@ -1631,8 +1637,6 @@ function c_ident(n) = [for (i = [0:1:n-1]) [for (j = [0:1:n-1]) (i==j)?[1,0]:[0,
 //   Compute the norm of a complex number or vector. 
 function c_norm(z) = norm_fro(z);
 
-
-
 // Section: Polynomials
 
 // Function: quadratic_roots()
@@ -1840,12 +1844,19 @@ function _poly_roots(p, pderiv, s, z, tol, i=0) =
 //   parts are zero.  You can specify eps, in which case the test is
 //   z.y/(1+norm(z)) < eps.  Because
 //   of poor convergence and higher error for repeated roots, such roots may
-//   be missed by the algorithm because their imaginary part is large.  
+//   be missed by the algorithm because their imaginary part is large.
 // Arguments:
 //   p = polynomial to solve as coefficient list, highest power term first
 //   eps = used to determine whether imaginary parts of roots are zero
 //   tol = tolerance for the complex polynomial root finder
 
+//   The algorithm is based on Brent's method and is a combination of
+//   bisection and inverse quadratic approximation, where bisection occurs
+//   at every step, with refinement using inverse quadratic approximation
+//   only when that approximation gives a good result.  The detail
+//   of how to decide when to use the quadratic came from an article
+//   by Crenshaw on "The World's Best Root Finder".
+//   https://www.embedded.com/worlds-best-root-finder/
 function real_roots(p,eps=undef,tol=1e-14) =
     assert( is_vector(p), "Invalid polynomial." )
     let( p = _poly_trim(p,eps=0) )
@@ -1859,4 +1870,76 @@ function real_roots(p,eps=undef,tol=1e-14) =
     ? [for(z=roots) if (abs(z.y)/(1+norm(z))<eps) z.x]
     : [for(i=idx(roots)) if (abs(roots[i].y)<=err[i]) roots[i].x];
 
+
+// Section: Operations on Functions
+
+// Function: root_find()
+// Usage:
+//    x = root_find(f, x0, x1, [tol])
+// Description:
+//    Find a root of the continuous function f where the sign of f(x0) is different
+//    from the sign of f(x1).  The function f is a function literal accepting one
+//    argument.  You must have a version of OpenSCAD that supports function literals
+//    (2021.01 or newer).  The tolerance (tol) specifies the accuracy of the solution:
+//    abs(f(x)) < tol * yrange, where yrange is the range of observed function values.
+//    This function can only find roots that cross the x axis:  it cannot find the
+//    the root of x^2.
+// Arguments:
+//    f = function literal for a single variable function
+//    x0 = endpoint of interval to search for root
+//    x1 = second endpoint of interval to search for root
+//    tol = tolerance for solution.  Default: 1e-15
+function root_find(f,x0,x1,tol=1e-15) =
+   let(
+        y0 = f(x0),
+        y1 = f(x1),
+        yrange = y0<y1 ? [y0,y1] : [y1,y0]
+   )
+   // Check endpoints
+   y0==0 || _rfcheck(x0, y0,yrange,tol) ? x0 :
+   y1==0 || _rfcheck(x1, y1,yrange,tol) ? x1 :
+   assert(y0*y1<0, "Sign of function must be different at the interval endpoints")
+   _rootfind(f,[x0,x1],[y0,y1],yrange,tol);
+
+function _rfcheck(x,y,range,tol) =
+   assert(is_finite(y), str("Function not finite at ",x))
+   abs(y) < tol*(range[1]-range[0]);
+
+// xpts and ypts are arrays whose first two entries contain the
+// interval bracketing the root.  Extra entries are ignored.
+// yrange is the total observed range of y values (used for the
+// tolerance test).  
+function _rootfind(f, xpts, ypts, yrange, tol, i=0) =
+    assert(i<100, "root_find did not converge to a solution")
+    let(
+         xmid = (xpts[0]+xpts[1])/2,
+         ymid = f(xmid),
+         yrange = [min(ymid, yrange[0]), max(ymid, yrange[1])]
+    )
+    _rfcheck(xmid, ymid, yrange, tol) ? xmid :
+    let(
+         // Force root to be between x0 and midpoint
+         y = ymid * ypts[0] < 0 ? [ypts[0], ymid, ypts[1]]
+                                : [ypts[1], ymid, ypts[0]],
+         x = ymid * ypts[0] < 0 ? [xpts[0], xmid, xpts[1]]
+                                : [xpts[1], xmid, xpts[0]],
+         v = y[2]*(y[2]-y[0]) - 2*y[1]*(y[1]-y[0])
+    )
+    v <= 0 ? _rootfind(f,x,y,yrange,tol,i+1)  // Root is between first two points, extra 3rd point doesn't hurt
+    :
+    let(  // Do quadratic approximation
+        B = (x[1]-x[0]) / (y[1]-y[0]),
+        C = y*[-1,2,-1] / (y[2]-y[1]) / (y[2]-y[0]),
+        newx = x[0] - B * y[0] *(1-C*y[1]),
+        newy = f(newx),
+        new_yrange = [min(yrange[0],newy), max(yrange[1], newy)],
+        // select interval that contains the root by checking sign
+        yinterval = newy*y[0] < 0 ? [y[0],newy] : [newy,y[1]],
+        xinterval = newy*y[0] < 0 ? [x[0],newx] : [newx,x[1]]
+     )
+     _rfcheck(newx, newy, new_yrange, tol)
+        ? newx
+        : _rootfind(f, xinterval, yinterval, new_yrange, tol, i+1);
+
+
 // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
diff --git a/mutators.scad b/mutators.scad
index 1f954a8..14e4b7a 100644
--- a/mutators.scad
+++ b/mutators.scad
@@ -515,7 +515,7 @@ module path_extrude2d(path, caps=true) {
         }
     }
     for (t=triplet(path)) {
-        ang = vang(t[2]-t[1]) - vang(t[1]-t[0]);
+        ang = v_theta(t[2]-t[1]) - v_theta(t[1]-t[0]);
         delt = t[2] - t[1];
         translate(t[1]) {
             minkowski() {
diff --git a/partitions.scad b/partitions.scad
index 2e516a0..747279f 100644
--- a/partitions.scad
+++ b/partitions.scad
@@ -44,7 +44,7 @@ function _partition_cutpath(l, h, cutsize, cutpath, gap) =
         cplen = (cutsize.x+gap) * reps,
         path = deduplicate(concat(
             [[-l/2, cutpath[0].y*cutsize.y]],
-            [for (i=[0:1:reps-1], pt=cutpath) vmul(pt,cutsize)+[i*(cutsize.x+gap)+gap/2-cplen/2,0]],
+            [for (i=[0:1:reps-1], pt=cutpath) v_mul(pt,cutsize)+[i*(cutsize.x+gap)+gap/2-cplen/2,0]],
             [[ l/2, cutpath[len(cutpath)-1].y*cutsize.y]]
         ))
     ) path;
@@ -164,7 +164,7 @@ module partition(size=100, spread=10, cutsize=10, cutpath=undef, gap=0, spin=0)
 {
     size = is_vector(size)? size : [size,size,size];
     cutsize = is_vector(cutsize)? cutsize : [cutsize*2, cutsize];
-    rsize = vabs(rot(spin,p=size));
+    rsize = v_abs(rot(spin,p=size));
     vec = rot(spin,p=BACK)*spread/2;
     move(vec) {
         intersection() {
diff --git a/paths.scad b/paths.scad
index 1dc1c2f..88d9ec7 100644
--- a/paths.scad
+++ b/paths.scad
@@ -661,7 +661,7 @@ function path_self_intersections(path, closed=true, eps=EPSILON) =
         path = cleanup_path(path, eps=eps),
         plen = len(path)
     ) [
-        for (i = [0:1:plen-(closed?2:3)], j=[i+1:1:plen-(closed?1:2)]) let(
+        for (i = [0:1:plen-(closed?2:3)], j=[i+2:1:plen-(closed?1:2)]) let(
             a1 = path[i],
             a2 = path[(i+1)%plen],
             b1 = path[j],
@@ -675,15 +675,17 @@ function path_self_intersections(path, closed=true, eps=EPSILON) =
                     c = a1-a2,
                     d = b1-b2,
                     denom = (c.x*d.y)-(c.y*d.x)
-                ) abs(denom)<eps? undef : let(
+                ) abs(denom)<eps? undef :
+                let(
                     e = a1-b1,
                     t = ((e.x*d.y)-(e.y*d.x)) / denom,
                     u = ((e.x*c.y)-(e.y*c.x)) / denom
                 ) [a1+t*(a2-a1), t, u]
         ) if (
+            (!closed || i!=0 || j!=plen-1) &&
             isect != undef &&
-            isect[1]>eps && isect[1]<=1+eps &&
-            isect[2]>eps && isect[2]<=1+eps
+            isect[1]>=-eps && isect[1]<=1+eps &&
+            isect[2]>=-eps && isect[2]<=1+eps
         ) [isect[0], i, isect[1], j, isect[2]]
     ];
 
@@ -1109,11 +1111,11 @@ module spiral_sweep(poly, h, r, twist=360, higbee, center, r1, r2, d, d1, d2, hi
 //   path = [ [0, 0, 0], [33, 33, 33], [66, 33, 40], [100, 0, 0], [150,0,0] ];
 //   path_extrude(path) circle(r=10, $fn=6);
 module path_extrude(path, convexity=10, clipsize=100) {
-    function polyquats(path, q=Q_Ident(), v=[0,0,1], i=0) = let(
+    function polyquats(path, q=q_ident(), v=[0,0,1], i=0) = let(
             v2 = path[i+1] - path[i],
             ang = vector_angle(v,v2),
             axis = ang>0.001? unit(cross(v,v2)) : [0,0,1],
-            newq = Q_Mul(Quat(axis, ang), q),
+            newq = q_mul(quat(axis, ang), q),
             dist = norm(v2)
         ) i < (len(path)-2)?
             concat([[dist, newq, ang]], polyquats(path, newq, v2, i+1)) :
@@ -1129,7 +1131,7 @@ module path_extrude(path, convexity=10, clipsize=100) {
         q = pquats[i][1];
         difference() {
             translate(pt1) {
-                Qrot(q) {
+                q_rot(q) {
                     down(clipsize/2/2) {
                         if ((dist+clipsize/2) > 0) {
                             linear_extrude(height=dist+clipsize/2, convexity=convexity) {
@@ -1140,12 +1142,12 @@ module path_extrude(path, convexity=10, clipsize=100) {
                 }
             }
             translate(pt1) {
-                hq = (i > 0)? Q_Slerp(q, pquats[i-1][1], 0.5) : q;
-                Qrot(hq) down(clipsize/2+epsilon) cube(clipsize, center=true);
+                hq = (i > 0)? q_slerp(q, pquats[i-1][1], 0.5) : q;
+                q_rot(hq) down(clipsize/2+epsilon) cube(clipsize, center=true);
             }
             translate(pt2) {
-                hq = (i < ptcount-2)? Q_Slerp(q, pquats[i+1][1], 0.5) : q;
-                Qrot(hq) up(clipsize/2+epsilon) cube(clipsize, center=true);
+                hq = (i < ptcount-2)? q_slerp(q, pquats[i+1][1], 0.5) : q;
+                q_rot(hq) up(clipsize/2+epsilon) cube(clipsize, center=true);
             }
         }
     }
diff --git a/polyhedra.scad b/polyhedra.scad
index 0914a60..be5f62a 100644
--- a/polyhedra.scad
+++ b/polyhedra.scad
@@ -377,7 +377,7 @@ function _point_ref(points, sign="both") =
     unique([
         for(i=[-1,1],j=[-1,1],k=[-1,1])
             if (sign=="both" || sign=="even" && i*j*k>0 || sign=="odd" && i*j*k<0)
-                each [for(point=points) vmul(point,[i,j,k])]
+                each [for(point=points) v_mul(point,[i,j,k])]
     ]);
 //
 _tribonacci=(1+4*cosh(acosh(2+3/8)/3))/3;
diff --git a/primitives.scad b/primitives.scad
index de96579..c6dc1f5 100644
--- a/primitives.scad
+++ b/primitives.scad
@@ -139,7 +139,7 @@ function cube(size=1, center, anchor, spin=0, orient=UP) =
             [-1,-1, 1],[1,-1, 1],[1,1, 1],[-1,1, 1],
         ]/2,
         verts = is_num(size)? unscaled * size :
-            is_vector(size,3)? [for (p=unscaled) vmul(p,size)] :
+            is_vector(size,3)? [for (p=unscaled) v_mul(p,size)] :
             assert(is_num(size) || is_vector(size,3)),
         faces = [
             [0,1,2], [0,2,3],  //BOTTOM
diff --git a/quaternions.scad b/quaternions.scad
index 1dc7192..7598655 100644
--- a/quaternions.scad
+++ b/quaternions.scad
@@ -16,190 +16,190 @@
 
 
 // Internal
-function _Quat(a,s,w) = [a[0]*s, a[1]*s, a[2]*s, w];
+function _quat(a,s,w) = [a[0]*s, a[1]*s, a[2]*s, w];
 
-function _Qvec(q) = [q.x,q.y,q.z];
+function _qvec(q) = [q.x,q.y,q.z];
 
-function _Qreal(q) = q[3];
+function _qreal(q) = q[3];
 
-function _Qset(v,r) = concat( v, r );
+function _qset(v,r) = concat( v, r );
 
 // normalizes without checking
-function _Qnorm(q) = q/norm(q);
+function _qnorm(q) = q/norm(q);
 
 
-// Function: Q_is_quat()
+// Function: is_quaternion()
 // Usage:
-//   if(Q_is_quat(q)) a=0;
+//   if(is_quaternion(q)) a=0;
 // Description: Return true if q is a valid non-zero quaternion.
 // Arguments:
 //   q = object to check.
-function Q_is_quat(q) = is_vector(q,4) && ! approx(norm(q),0) ;
+function is_quaternion(q) = is_vector(q,4) && ! approx(norm(q),0) ;
 
 
-// Function: Quat()
+// Function: quat()
 // Usage:
-//   Quat(ax, ang);
+//   quat(ax, ang);
 // Description: Create a normalized Quaternion from axis and angle of rotation.
 // Arguments:
 //   ax = Vector of axis of rotation.
 //   ang = Number of degrees to rotate around the axis counter-clockwise, when facing the origin.
-function Quat(ax=[0,0,1], ang=0) = 
+function quat(ax=[0,0,1], ang=0) = 
     assert( is_vector(ax,3) && is_finite(ang), "Invalid input")
     let( n = norm(ax) )
     approx(n,0) 
-    ? _Quat([0,0,0], sin(ang/2), cos(ang/2))
-    : _Quat(ax/n, sin(ang/2), cos(ang/2));
+    ? _quat([0,0,0], sin(ang/2), cos(ang/2))
+    : _quat(ax/n, sin(ang/2), cos(ang/2));
 
 
-// Function: QuatX()
+// Function: quat_x()
 // Usage:
-//   QuatX(a);
+//   quat_x(a);
 // Description: Create a normalized Quaternion for rotating around the X axis [1,0,0].
 // Arguments:
 //   a = Number of degrees to rotate around the axis counter-clockwise, when facing the origin.
-function QuatX(a=0) = 
+function quat_x(a=0) = 
     assert( is_finite(a), "Invalid angle" )
-    Quat([1,0,0],a);
+    quat([1,0,0],a);
 
 
-// Function: QuatY()
+// Function: quat_y()
 // Usage:
-//   QuatY(a);
+//   quat_y(a);
 // Description: Create a normalized Quaternion for rotating around the Y axis [0,1,0].
 // Arguments:
 //   a = Number of degrees to rotate around the axis counter-clockwise, when facing the origin.
-function QuatY(a=0) =  
+function quat_y(a=0) =  
     assert( is_finite(a), "Invalid angle" )
-    Quat([0,1,0],a);
+    quat([0,1,0],a);
 
 
-// Function: QuatZ()
+// Function: quat_z()
 // Usage:
-//   QuatZ(a);
+//   quat_z(a);
 // Description: Create a normalized Quaternion for rotating around the Z axis [0,0,1].
 // Arguments:
 //   a = Number of degrees to rotate around the axis counter-clockwise, when facing the origin.
-function QuatZ(a=0) =  
+function quat_z(a=0) =  
     assert( is_finite(a), "Invalid angle" )
-    Quat([0,0,1],a);
+    quat([0,0,1],a);
 
 
-// Function: QuatXYZ()
+// Function: quat_xyz()
 // Usage:
-//   QuatXYZ([X,Y,Z])
+//   quat_xyz([X,Y,Z])
 // Description:
 //   Creates a normalized quaternion from standard [X,Y,Z] rotation angles in degrees.
 // Arguments:
 //   a = The triplet of rotation angles, [X,Y,Z]
-function QuatXYZ(a=[0,0,0]) =
+function quat_xyz(a=[0,0,0]) =
     assert( is_vector(a,3), "Invalid angles")
     let(
-      qx = QuatX(a[0]),
-      qy = QuatY(a[1]),
-      qz = QuatZ(a[2])
+      qx = quat_x(a[0]),
+      qy = quat_y(a[1]),
+      qz = quat_z(a[2])
     )
-    Q_Mul(qz, Q_Mul(qy, qx));
+    q_mul(qz, q_mul(qy, qx));
 
 
-// Function: Q_From_to()
+// Function: q_from_to()
 // Usage:
-//    q = Q_From_to(v1, v2);
+//    q = q_from_to(v1, v2);
 // Description: 
 //   Returns the normalized quaternion that rotates the non zero 3D vector v1 
 //   to the non zero 3D vector v2.
-function Q_From_to(v1, v2) =
+function q_from_to(v1, v2) =
     assert( is_vector(v1,3) && is_vector(v2,3) 
             && ! approx(norm(v1),0) && ! approx(norm(v2),0)
             , "Invalid vector(s)")
     let( ax = cross(v1,v2),
          n  = norm(ax) )
     approx(n, 0)
-    ? v1*v2>0 ? Q_Ident() : Quat([ v1.y, -v1.x, 0], 180)  
-    : Quat(ax, atan2( n , v1*v2 ));
+    ? v1*v2>0 ? q_ident() : quat([ v1.y, -v1.x, 0], 180)  
+    : quat(ax, atan2( n , v1*v2 ));
 
 
-// Function: Q_Ident()
+// Function: q_ident()
 // Description: Returns the "Identity" zero-rotation Quaternion.
-function Q_Ident() = [0, 0, 0, 1];
+function q_ident() = [0, 0, 0, 1];
 
 
-// Function: Q_Add_S()
+// Function: q_add_s()
 // Usage:
-//   Q_Add_S(q, s)
+//   q_add_s(q, s)
 // Description: 
 //   Adds a scalar value `s` to the W part of a quaternion `q`.
 //   The returned quaternion is usually not normalized.
-function Q_Add_S(q, s) =  
+function q_add_s(q, s) =  
     assert( is_finite(s), "Invalid scalar" )
     q+[0,0,0,s];
 
 
-// Function: Q_Sub_S()
+// Function: q_sub_s()
 // Usage:
-//   Q_Sub_S(q, s)
+//   q_sub_s(q, s)
 // Description: 
 //   Subtracts a scalar value `s` from the W part of a quaternion `q`.
 //   The returned quaternion is usually not normalized.
-function Q_Sub_S(q, s) =  
+function q_sub_s(q, s) =  
     assert( is_finite(s), "Invalid scalar" )
     q-[0,0,0,s];
 
 
-// Function: Q_Mul_S()
+// Function: q_mul_s()
 // Usage:
-//   Q_Mul_S(q, s)
+//   q_mul_s(q, s)
 // Description: 
 //   Multiplies each part of a quaternion `q` by a scalar value `s`.
 //   The returned quaternion is usually not normalized.
-function Q_Mul_S(q, s) =  
+function q_mul_s(q, s) =  
     assert( is_finite(s), "Invalid scalar" )
     q*s;
 
 
-// Function: Q_Div_S()
+// Function: q_div_s()
 // Usage:
-//   Q_Div_S(q, s)
+//   q_div_s(q, s)
 // Description: 
 //   Divides each part of a quaternion `q` by a scalar value `s`.
 //   The returned quaternion is usually not normalized.
-function Q_Div_S(q, s) =   
+function q_div_s(q, s) =   
     assert( is_finite(s) && ! approx(s,0) , "Invalid scalar" )
     q/s;
 
 
-// Function: Q_Add()
+// Function: q_add()
 // Usage:
-//   Q_Add(a, b)
+//   q_add(a, b)
 // Description: 
 //   Adds each part of two quaternions together.
 //   The returned quaternion is usually not normalized.
-function Q_Add(a, b) = 
-    assert( Q_is_quat(a) && Q_is_quat(a), "Invalid quaternion(s)") 
+function q_add(a, b) = 
+    assert( is_quaternion(a) && is_quaternion(a), "Invalid quaternion(s)") 
     assert( ! approx(norm(a+b),0), "Quaternions cannot be opposed" )
     a+b;
 
 
-// Function: Q_Sub()
+// Function: q_sub()
 // Usage:
-//   Q_Sub(a, b)
+//   q_sub(a, b)
 // Description: 
 //   Subtracts each part of quaternion `b` from quaternion `a`.
 //   The returned quaternion is usually not normalized.
-function Q_Sub(a, b) = 
-    assert( Q_is_quat(a) && Q_is_quat(a), "Invalid quaternion(s)") 
+function q_sub(a, b) = 
+    assert( is_quaternion(a) && is_quaternion(a), "Invalid quaternion(s)") 
     assert( ! approx(a,b), "Quaternions cannot be equal" )
     a-b;
 
 
-// Function: Q_Mul()
+// Function: q_mul()
 // Usage:
-//   Q_Mul(a, b)
+//   q_mul(a, b)
 // Description: 
 //   Multiplies quaternion `a` by quaternion `b`.
 //   The returned quaternion is normalized if both `a` and `b` are normalized
-function Q_Mul(a, b) = 
-    assert( Q_is_quat(a) && Q_is_quat(b), "Invalid quaternion(s)")
+function q_mul(a, b) = 
+    assert( is_quaternion(a) && is_quaternion(b), "Invalid quaternion(s)")
     [
       a[3]*b.x  + a.x*b[3] + a.y*b.z  - a.z*b.y,
       a[3]*b.y  - a.x*b.z  + a.y*b[3] + a.z*b.x,
@@ -208,94 +208,94 @@ function Q_Mul(a, b) =
     ];
 
 
-// Function: Q_Cumulative()
+// Function: q_cumulative()
 // Usage:
-//   Q_Cumulative(v);
+//   q_cumulative(v);
 // Description:
 //   Given a list of Quaternions, cumulatively multiplies them, returning a list
 //   of each cumulative Quaternion product.  It starts with the first quaternion
 //   given in the list, and applies successive quaternion rotations in list order.
 //   The quaternion in the returned list are normalized if each quaternion in v
 //   is normalized.
-function Q_Cumulative(v, _i=0, _acc=[]) = 
+function q_cumulative(v, _i=0, _acc=[]) = 
     _i==len(v) ? _acc :
-    Q_Cumulative(
+    q_cumulative(
         v, _i+1,
         concat(
             _acc,
-            [_i==0 ? v[_i] : Q_Mul(v[_i], last(_acc))]
+            [_i==0 ? v[_i] : q_mul(v[_i], last(_acc))]
         )
     );
 
 
-// Function: Q_Dot()
+// Function: q_dot()
 // Usage:
-//   Q_Dot(a, b)
+//   q_dot(a, b)
 // Description: Calculates the dot product between quaternions `a` and `b`.
-function Q_Dot(a, b) = 
-    assert( Q_is_quat(a) && Q_is_quat(b), "Invalid quaternion(s)" )
+function q_dot(a, b) = 
+    assert( is_quaternion(a) && is_quaternion(b), "Invalid quaternion(s)" )
     a*b;
 
-// Function: Q_Neg()
+// Function: q_neg()
 // Usage:
-//   Q_Neg(q)
+//   q_neg(q)
 // Description: Returns the negative of quaternion `q`.
-function Q_Neg(q) = 
-    assert( Q_is_quat(q), "Invalid quaternion" )
+function q_neg(q) = 
+    assert( is_quaternion(q), "Invalid quaternion" )
     -q;
 
 
-// Function: Q_Conj()
+// Function: q_conj()
 // Usage:
-//   Q_Conj(q)
+//   q_conj(q)
 // Description: Returns the conjugate of quaternion `q`.
-function Q_Conj(q) = 
-    assert( Q_is_quat(q), "Invalid quaternion" )
+function q_conj(q) = 
+    assert( is_quaternion(q), "Invalid quaternion" )
     [-q.x, -q.y, -q.z, q[3]];
 
 
-// Function: Q_Inverse()
+// Function: q_inverse()
 // Usage:
-//   qc = Q_Inverse(q)
+//   qc = q_inverse(q)
 // Description: Returns the multiplication inverse of quaternion `q`  that is normalized only if `q` is normalized.
-function Q_Inverse(q) = 
-    assert( Q_is_quat(q), "Invalid quaternion" )
-    let(q = _Qnorm(q) )
+function q_inverse(q) = 
+    assert( is_quaternion(q), "Invalid quaternion" )
+    let(q = _qnorm(q) )
     [-q.x, -q.y, -q.z, q[3]];
 
 
-// Function: Q_Norm()
+// Function: q_norm()
 // Usage:
-//   Q_Norm(q)
+//   q_norm(q)
 // Description: 
 //   Returns the `norm()` "length" of quaternion `q`.
 //   Normalized quaternions have unitary norm. 
-function Q_Norm(q) = 
-    assert( Q_is_quat(q), "Invalid quaternion" )
+function q_norm(q) = 
+    assert( is_quaternion(q), "Invalid quaternion" )
     norm(q);
 
 
-// Function: Q_Normalize()
+// Function: q_normalize()
 // Usage:
-//   Q_Normalize(q)
+//   q_normalize(q)
 // Description: Normalizes quaternion `q`, so that norm([W,X,Y,Z]) == 1.
-function Q_Normalize(q) = 
-    assert( Q_is_quat(q) , "Invalid quaternion" )
+function q_normalize(q) = 
+    assert( is_quaternion(q) , "Invalid quaternion" )
     q/norm(q);
 
 
-// Function: Q_Dist()
+// Function: q_dist()
 // Usage:
-//   Q_Dist(q1, q2)
+//   q_dist(q1, q2)
 // Description: Returns the "distance" between two quaternions.
-function Q_Dist(q1, q2) =   
-    assert( Q_is_quat(q1) && Q_is_quat(q2), "Invalid quaternion(s)" )
+function q_dist(q1, q2) =   
+    assert( is_quaternion(q1) && is_quaternion(q2), "Invalid quaternion(s)" )
     norm(q2-q1);
 
 
-// Function: Q_Slerp()
+// Function: q_slerp()
 // Usage:
-//   Q_Slerp(q1, q2, u);
+//   q_slerp(q1, q2, u);
 // Description:
 //   Returns a quaternion that is a spherical interpolation between two quaternions.
 // Arguments:
@@ -303,45 +303,45 @@ function Q_Dist(q1, q2) =
 //   q2 = The second quaternion. (u=1)
 //   u = The proportional value, from 0 to 1, of what part of the interpolation to return.
 // Example(3D): Giving `u` as a Scalar
-//   a = QuatY(-135);
-//   b = QuatXYZ([0,-30,30]);
+//   a = quat_y(-135);
+//   b = quat_xyz([0,-30,30]);
 //   for (u=[0:0.1:1])
-//       Qrot(Q_Slerp(a, b, u))
+//       q_rot(q_slerp(a, b, u))
 //           right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): Giving `u` as a Range
-//   a = QuatZ(-135);
-//   b = QuatXYZ([90,0,-45]);
-//   for (q = Q_Slerp(a, b, [0:0.1:1]))
-//       Qrot(q) right(80) cube([10,10,1]);
+//   a = quat_z(-135);
+//   b = quat_xyz([90,0,-45]);
+//   for (q = q_slerp(a, b, [0:0.1:1]))
+//       q_rot(q) right(80) cube([10,10,1]);
 //   #sphere(r=80);
-function Q_Slerp(q1, q2, u, _dot) =
+function q_slerp(q1, q2, u, _dot) =
     is_undef(_dot) 
     ?   assert(is_finite(u) || is_range(u) || is_vector(u), "Invalid interpolation coefficient(s)")
-        assert(Q_is_quat(q1) && Q_is_quat(q2), "Invalid quaternion(s)" )
+        assert(is_quaternion(q1) && is_quaternion(q2), "Invalid quaternion(s)" )
         let(
           _dot = q1*q2,
           q1   = q1/norm(q1),
           q2   = _dot<0 ? -q2/norm(q2) : q2/norm(q2),
           dot  = abs(_dot)
         )
-        ! is_finite(u) ? [for (uu=u) Q_Slerp(q1, q2, uu, dot)] :
-        Q_Slerp(q1, q2, u, dot)  
+        ! is_finite(u) ? [for (uu=u) q_slerp(q1, q2, uu, dot)] :
+        q_slerp(q1, q2, u, dot)  
     :   _dot>0.9995 
-        ?   _Qnorm(q1 + u*(q2-q1))
+        ?   _qnorm(q1 + u*(q2-q1))
         :   let( theta = u*acos(_dot),
-                 q3    = _Qnorm(q2 - _dot*q1)
+                 q3    = _qnorm(q2 - _dot*q1)
                ) 
-            _Qnorm(q1*cos(theta) + q3*sin(theta));
+            _qnorm(q1*cos(theta) + q3*sin(theta));
 
 
-// Function: Q_Matrix3()
+// Function: q_matrix3()
 // Usage:
-//   Q_Matrix3(q);
+//   q_matrix3(q);
 // Description:
 //   Returns the 3x3 rotation matrix for the given normalized quaternion q.
-function Q_Matrix3(q) =   
-    let( q = Q_Normalize(q) )
+function q_matrix3(q) =   
+    let( q = q_normalize(q) )
     [
       [1-2*q[1]*q[1]-2*q[2]*q[2],   2*q[0]*q[1]-2*q[2]*q[3],   2*q[0]*q[2]+2*q[1]*q[3]],
       [  2*q[0]*q[1]+2*q[2]*q[3], 1-2*q[0]*q[0]-2*q[2]*q[2],   2*q[1]*q[2]-2*q[0]*q[3]],
@@ -349,13 +349,13 @@ function Q_Matrix3(q) =
     ];
 
 
-// Function: Q_Matrix4()
+// Function: q_matrix4()
 // Usage:
-//   Q_Matrix4(q);
+//   q_matrix4(q);
 // Description:
 //   Returns the 4x4 rotation matrix for the given normalized quaternion q.
-function Q_Matrix4(q) =    
-    let( q = Q_Normalize(q) )
+function q_matrix4(q) =    
+    let( q = q_normalize(q) )
     [
       [1-2*q[1]*q[1]-2*q[2]*q[2],   2*q[0]*q[1]-2*q[2]*q[3],   2*q[0]*q[2]+2*q[1]*q[3], 0],
       [  2*q[0]*q[1]+2*q[2]*q[3], 1-2*q[0]*q[0]-2*q[2]*q[2],   2*q[1]*q[2]-2*q[0]*q[3], 0],
@@ -364,115 +364,115 @@ function Q_Matrix4(q) =
     ];
 
 
-// Function: Q_Axis()
+// Function: q_axis()
 // Usage:
-//   Q_Axis(q)
+//   q_axis(q)
 // Description:
 //   Returns the axis of rotation of a normalized quaternion `q`.
 //   The input doesn't need to be normalized.
-function Q_Axis(q) =   
-    assert( Q_is_quat(q) , "Invalid quaternion" )
-    let( d = norm(_Qvec(q)) )
-    approx(d,0)? [0,0,1] : _Qvec(q)/d;
+function q_axis(q) =   
+    assert( is_quaternion(q) , "Invalid quaternion" )
+    let( d = norm(_qvec(q)) )
+    approx(d,0)? [0,0,1] : _qvec(q)/d;
 
-// Function: Q_Angle()
+// Function: q_angle()
 // Usage:
-//   a = Q_Angle(q)
-//   a12 = Q_Angle(q1,q2);
+//   a = q_angle(q)
+//   a12 = q_angle(q1,q2);
 // Description:
 //   If only q1 is given, returns the angle of rotation (in degrees) of that quaternion.
 //   If both q1 and q2 are given, returns the angle (in degrees) between them.
 //   The input quaternions don't need to be normalized.
-function Q_Angle(q1,q2) =
-    assert(Q_is_quat(q1) && (is_undef(q2) || Q_is_quat(q2)), "Invalid quaternion(s)" )
-    let( n1 = is_undef(q2)? norm(_Qvec(q1)): norm(q1) )
+function q_angle(q1,q2) =
+    assert(is_quaternion(q1) && (is_undef(q2) || is_quaternion(q2)), "Invalid quaternion(s)" )
+    let( n1 = is_undef(q2)? norm(_qvec(q1)): norm(q1) )
     is_undef(q2) 
-    ?   2 * atan2(n1,_Qreal(q1))
+    ?   2 * atan2(n1,_qreal(q1))
     :   let( q1 = q1/norm(q1),
              q2 = q2/norm(q2) )
         4 * atan2(norm(q1 - q2), norm(q1 + q2));
 
-// Function&Module: Qrot()
+// Function&Module: q_rot()
 // Usage: As Module
-//   Qrot(q) ...
+//   q_rot(q) ...
 // Usage: As Function
-//   pts = Qrot(q,p);
+//   pts = q_rot(q,p);
 // Description:
 //   When called as a module, rotates all children by the rotation stored in quaternion `q`.
 //   When called as a function with a `p` argument, rotates the point or list of points in `p` by the rotation stored in quaternion `q`.
 //   When called as a function without a `p` argument, returns the affine3d rotation matrix for the rotation stored in quaternion `q`.
 // Example(FlatSpin,VPD=225,VPT=[71,-26,16]):
 //   module shape() translate([80,0,0]) cube([10,10,1]);
-//   q = QuatXYZ([90,-15,-45]);
-//   Qrot(q) shape();
+//   q = quat_xyz([90,-15,-45]);
+//   q_rot(q) shape();
 //   #shape();
 // Example(NORENDER):
-//   q = QuatXYZ([45,35,10]);
-//   mat4x4 = Qrot(q);
+//   q = quat_xyz([45,35,10]);
+//   mat4x4 = q_rot(q);
 // Example(NORENDER):
-//   q = QuatXYZ([45,35,10]);
-//   pt = Qrot(q, p=[4,5,6]);
+//   q = quat_xyz([45,35,10]);
+//   pt = q_rot(q, p=[4,5,6]);
 // Example(NORENDER):
-//   q = QuatXYZ([45,35,10]);
-//   pts = Qrot(q, p=[[2,3,4], [4,5,6], [9,2,3]]);
-module Qrot(q) {
-    multmatrix(Q_Matrix4(q)) {
+//   q = quat_xyz([45,35,10]);
+//   pts = q_rot(q, p=[[2,3,4], [4,5,6], [9,2,3]]);
+module q_rot(q) {
+    multmatrix(q_matrix4(q)) {
         children();
     }
 }
 
-function Qrot(q,p) =
-      is_undef(p)? Q_Matrix4(q) :
-      is_vector(p)? Qrot(q,[p])[0] :
-      apply(Q_Matrix4(q), p);
+function q_rot(q,p) =
+      is_undef(p)? q_matrix4(q) :
+      is_vector(p)? q_rot(q,[p])[0] :
+      apply(q_matrix4(q), p);
 
 
-// Module: Qrot_copies()
+// Module: q_rot_copies()
 // Usage:
-//   Qrot_copies(quats) ...
+//   q_rot_copies(quats) ...
 // Description:
 //   For each quaternion given in the list `quats`, rotates to that orientation and creates a copy
-//   of all children.  This is equivalent to `for (q=quats) Qrot(q) ...`.
+//   of all children.  This is equivalent to `for (q=quats) q_rot(q) ...`.
 // Arguments:
 //   quats = A list containing all quaternions to rotate to and create copies of all children for.
 // Example:
-//   a = QuatZ(-135);
-//   b = QuatXYZ([0,-30,30]);
-//   Qrot_copies(Q_Slerp(a, b, [0:0.1:1]))
+//   a = quat_z(-135);
+//   b = quat_xyz([0,-30,30]);
+//   q_rot_copies(q_slerp(a, b, [0:0.1:1]))
 //       right(80) cube([10,10,1]);
 //   #sphere(r=80);
-module Qrot_copies(quats) for (q=quats) Qrot(q) children();
+module q_rot_copies(quats) for (q=quats) q_rot(q) children();
 
 
-// Function: Q_Rotation()
+// Function: q_rotation()
 // Usage:
-//   Q_Rotation(R)
+//   q_rotation(R)
 // Description:
 //   Returns a normalized quaternion corresponding to the rotation matrix R.
 //   R may be a 3x3 rotation matrix or a homogeneous 4x4 rotation matrix.
 //   The last row and last column of R are ignored for 4x4 matrices.
 //   It doesn't check whether R is in fact a rotation matrix.
 //   If R is not a rotation, the returned quaternion is an unpredictable quaternion .
-function Q_Rotation(R) =
+function q_rotation(R) =
     assert( is_matrix(R,3,3) || is_matrix(R,4,4) , 
                       "Matrix is neither 3x3 nor 4x4")
     let( tr = R[0][0]+R[1][1]+R[2][2] ) // R trace
     tr>0 
     ?   let( r = 1+tr  )
-        _Qnorm( _Qset([ R[1][2]-R[2][1], R[2][0]-R[0][2], R[0][1]-R[1][0] ], -r ) )
+        _qnorm( _qset([ R[1][2]-R[2][1], R[2][0]-R[0][2], R[0][1]-R[1][0] ], -r ) )
     :   let( i = max_index([ R[0][0], R[1][1], R[2][2] ]),
              r = 1 + 2*R[i][i] -R[0][0] -R[1][1] -R[2][2] )
-        i==0 ? _Qnorm( _Qset( [ 4*r, (R[1][0]+R[0][1]), (R[0][2]+R[2][0]) ], (R[2][1]-R[1][2])) ):
-        i==1 ? _Qnorm( _Qset( [ (R[1][0]+R[0][1]), 4*r, (R[2][1]+R[1][2]) ], (R[0][2]-R[2][0])) ):
-            _Qnorm( _Qset( [ (R[2][0]+R[0][2]), (R[1][2]+R[2][1]), 4*r ], (R[1][0]-R[0][1])) ) ;
+        i==0 ? _qnorm( _qset( [ 4*r, (R[1][0]+R[0][1]), (R[0][2]+R[2][0]) ], (R[2][1]-R[1][2])) ):
+        i==1 ? _qnorm( _qset( [ (R[1][0]+R[0][1]), 4*r, (R[2][1]+R[1][2]) ], (R[0][2]-R[2][0])) ):
+            _qnorm( _qset( [ (R[2][0]+R[0][2]), (R[1][2]+R[2][1]), 4*r ], (R[1][0]-R[0][1])) ) ;
 
 
-// Function&Module: Q_Rotation_path()
+// Function&Module: q_rotation_path()
 // Usage: As a function
-//   path = Q_Rotation_path(q1, n, q2);
-//   path = Q_Rotation_path(q1, n);
+//   path = q_rotation_path(q1, n, q2);
+//   path = q_rotation_path(q1, n);
 // Usage: As a module
-//   Q_Rotation_path(q1, n, q2) ...
+//   q_rotation_path(q1, n, q2) ...
 // Description:
 //   If q2 is undef and it is called as a function, the path, with length n+1 (n>=1), will be the 
 //   cumulative multiplications of the matrix rotation of q1 by itself.
@@ -488,50 +488,50 @@ function Q_Rotation(R) =
 //   q2 = The quaternion of the last rotation.
 //   n  = An integer defining the path length ( path length = n+1). 
 // Example(3D): as a function
-//   a = QuatY(-135);
-//   b = QuatXYZ([0,-30,30]);
-//   for (M=Q_Rotation_path(a, 10, b))
+//   a = quat_y(-135);
+//   b = quat_xyz([0,-30,30]);
+//   for (M=q_rotation_path(a, 10, b))
 //       multmatrix(M)
 //           right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): as a module
-//   a = QuatY(-135);
-//   b = QuatXYZ([0,-30,30]);
-//   Q_Rotation_path(a, 10, b)
+//   a = quat_y(-135);
+//   b = quat_xyz([0,-30,30]);
+//   q_rotation_path(a, 10, b)
 //      right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): as a function
-//   a = QuatY(5);
-//   for (M=Q_Rotation_path(a, 10))
+//   a = quat_y(5);
+//   for (M=q_rotation_path(a, 10))
 //       multmatrix(M)
 //           right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): as a module
-//   a = QuatY(5);
-//   Q_Rotation_path(a, 10)
+//   a = quat_y(5);
+//   q_rotation_path(a, 10)
 //      right(80) cube([10,10,1]);
 //   #sphere(r=80);
-function Q_Rotation_path(q1, n=1, q2) =
-    assert( Q_is_quat(q1) && (is_undef(q2) || Q_is_quat(q2) ), "Invalid quaternion(s)" )
+function q_rotation_path(q1, n=1, q2) =
+    assert( is_quaternion(q1) && (is_undef(q2) || is_quaternion(q2) ), "Invalid quaternion(s)" )
     assert( is_finite(n) && n>=1 && n==floor(n), "Invalid integer" )
     assert( is_undef(q2) || ! approx(norm(q1+q2),0), "Quaternions cannot be opposed" )
     is_undef(q2) 
-    ?   [for( i=0, dR=Q_Matrix4(q1), R=dR; i<=n; i=i+1, R=dR*R ) R] 
-    :   let( q2 = Q_Normalize( q1*q2<0 ? -q2: q2 ),
-             dq = Q_pow( Q_Mul( q2, Q_Inverse(q1) ), 1/n ),
-             dR = Q_Matrix4(dq) )
-        [for( i=0, R=Q_Matrix4(q1); i<=n; i=i+1, R=dR*R ) R];
+    ?   [for( i=0, dR=q_matrix4(q1), R=dR; i<=n; i=i+1, R=dR*R ) R] 
+    :   let( q2 = q_normalize( q1*q2<0 ? -q2: q2 ),
+             dq = q_pow( q_mul( q2, q_inverse(q1) ), 1/n ),
+             dR = q_matrix4(dq) )
+        [for( i=0, R=q_matrix4(q1); i<=n; i=i+1, R=dR*R ) R];
 
-module Q_Rotation_path(q1, n=1, q2) {
-    for(Mi=Q_Rotation_path(q1, n, q2))
+module q_rotation_path(q1, n=1, q2) {
+    for(Mi=q_rotation_path(q1, n, q2))
         multmatrix(Mi)
             children();
 }
 
 
-// Function: Q_Nlerp()
+// Function: q_nlerp()
 // Usage:
-//   q = Q_Nlerp(q1, q2, u);
+//   q = q_nlerp(q1, q2, u);
 // Description:
 //   Returns a quaternion that is a normalized linear interpolation between two quaternions
 //   when u is a number.
@@ -543,33 +543,33 @@ module Q_Rotation_path(q1, n=1, q2) {
 //   q2 = The second quaternion. (u=1)
 //   u  = A value (or a list of values), between 0 and 1, of the proportion(s) of each quaternion in the interpolation. 
 // Example(3D): Giving `u` as a Scalar
-//   a = QuatY(-135);
-//   b = QuatXYZ([0,-30,30]);
+//   a = quat_y(-135);
+//   b = quat_xyz([0,-30,30]);
 //   for (u=[0:0.1:1])
-//       Qrot(Q_Nlerp(a, b, u))
+//       q_rot(q_nlerp(a, b, u))
 //           right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): Giving `u` as a Range
-//   a = QuatZ(-135);
-//   b = QuatXYZ([90,0,-45]);
-//   for (q = Q_Nlerp(a, b, [0:0.1:1]))
-//       Qrot(q) right(80) cube([10,10,1]);
+//   a = quat_z(-135);
+//   b = quat_xyz([90,0,-45]);
+//   for (q = q_nlerp(a, b, [0:0.1:1]))
+//       q_rot(q) right(80) cube([10,10,1]);
 //   #sphere(r=80);
-function Q_Nlerp(q1,q2,u) =
+function q_nlerp(q1,q2,u) =
     assert(is_finite(u) || is_range(u) || is_vector(u) ,
            "Invalid interpolation coefficient(s)" )
-    assert(Q_is_quat(q1) && Q_is_quat(q2), "Invalid quaternion(s)" )
+    assert(is_quaternion(q1) && is_quaternion(q2), "Invalid quaternion(s)" )
     assert( ! approx(norm(q1+q2),0), "Quaternions cannot be opposed" )
-    let( q1  = Q_Normalize(q1),
-         q2  = Q_Normalize(q2) )
+    let( q1  = q_normalize(q1),
+         q2  = q_normalize(q2) )
     is_num(u) 
-    ? _Qnorm((1-u)*q1 + u*q2 )
-    : [for (ui=u) _Qnorm((1-ui)*q1 + ui*q2 ) ];
+    ? _qnorm((1-u)*q1 + u*q2 )
+    : [for (ui=u) _qnorm((1-ui)*q1 + ui*q2 ) ];
 
 
-// Function: Q_Squad()
+// Function: q_squad()
 // Usage:
-//   qn = Q_Squad(q1,q2,q3,q4,u);
+//   qn = q_squad(q1,q2,q3,q4,u);
 // Description:
 //   Returns a quaternion that is a cubic spherical interpolation of the quaternions  
 //   q1 and q4 taking the other two quaternions, q2 and q3, as parameter of a cubic 
@@ -586,71 +586,72 @@ function Q_Nlerp(q1,q2,u) =
 //   q4 = The end quaternion. (u=1)
 //   u  = A value (or a list of values), of the proportion(s) of each quaternion in the cubic interpolation. 
 // Example(3D): Giving `u` as a Scalar
-//   a = QuatY(-135);
-//   b = QuatXYZ([-50,-50,120]);
-//   c = QuatXYZ([-50,-40,30]);
-//   d = QuatY(-45);
+//   a = quat_y(-135);
+//   b = quat_xyz([-50,-50,120]);
+//   c = quat_xyz([-50,-40,30]);
+//   d = quat_y(-45);
 //   color("red"){
-//     Qrot(b) right(80) cube([10,10,1]);
-//     Qrot(c) right(80) cube([10,10,1]);
+//     q_rot(b) right(80) cube([10,10,1]);
+//     q_rot(c) right(80) cube([10,10,1]);
 //   }
 //   for (u=[0:0.05:1])
-//       Qrot(Q_Squad(a, b, c, d, u))
+//       q_rot(q_squad(a, b, c, d, u))
 //           right(80) cube([10,10,1]);
 //   #sphere(r=80);
 // Example(3D): Giving `u` as a Range
-//   a = QuatY(-135);
-//   b = QuatXYZ([-50,-50,120]);
-//   c = QuatXYZ([-50,-40,30]);
-//   d = QuatY(-45);
-//   for (q = Q_Squad(a, b, c, d, [0:0.05:1]))
-//       Qrot(q) right(80) cube([10,10,1]);
+//   a = quat_y(-135);
+//   b = quat_xyz([-50,-50,120]);
+//   c = quat_xyz([-50,-40,30]);
+//   d = quat_y(-45);
+//   for (q = q_squad(a, b, c, d, [0:0.05:1]))
+//       q_rot(q) right(80) cube([10,10,1]);
 //   #sphere(r=80);
-function Q_Squad(q1,q2,q3,q4,u) =
+function q_squad(q1,q2,q3,q4,u) =
     assert(is_finite(u) || is_range(u) || is_vector(u) ,
            "Invalid interpolation coefficient(s)" )
     is_num(u) 
-    ? Q_Slerp( Q_Slerp(q1,q4,u), Q_Slerp(q2,q3,u), 2*u*(1-u))
-    : [for(ui=u) Q_Slerp( Q_Slerp(q1,q4,ui), Q_Slerp(q2,q3,ui), 2*ui*(1-ui) ) ];
+    ? q_slerp( q_slerp(q1,q4,u), q_slerp(q2,q3,u), 2*u*(1-u))
+    : [for(ui=u) q_slerp( q_slerp(q1,q4,ui), q_slerp(q2,q3,ui), 2*ui*(1-ui) ) ];
 
 
-// Function: Q_exp()
+// Function: q_exp()
 // Usage:
-//   q2 = Q_exp(q);
+//   q2 = q_exp(q);
 // Description:
 //   Returns the quaternion that is the exponential of the quaternion q in base e
 //   The returned quaternion is usually not normalized.
-function Q_exp(q) =
+function q_exp(q) =
     assert( is_vector(q,4), "Input is not a valid quaternion")
-    let( nv = norm(_Qvec(q)) ) // q may be equal to zero here!
-    exp(_Qreal(q))*Quat(_Qvec(q),2*nv);
+    let( nv = norm(_qvec(q)) ) // q may be equal to zero here!
+    exp(_qreal(q))*quat(_qvec(q),2*nv);
 
 
-// Function: Q_ln()
+// Function: q_ln()
 // Usage:
-//   q2 = Q_ln(q);
+//   q2 = q_ln(q);
 // Description:
 //   Returns the quaternion that is the natural logarithm of the quaternion q.
 //   The returned quaternion is usually not normalized and may be zero.
-function Q_ln(q) =
-    assert(Q_is_quat(q), "Input is not a valid quaternion")
-    let( nq = norm(q),
-         nv = norm(_Qvec(q)) )
-    approx(nv,0) ? _Qset([0,0,0] , ln(nq) ) :
-    _Qset(_Qvec(q)*atan2(nv,_Qreal(q))/nv, ln(nq));
+function q_ln(q) =
+    assert(is_quaternion(q), "Input is not a valid quaternion")
+    let(
+        nq = norm(q),
+        nv = norm(_qvec(q))
+    )
+    approx(nv,0) ? _qset([0,0,0] , ln(nq) ) :
+    _qset(_qvec(q)*atan2(nv,_qreal(q))/nv, ln(nq));
 
 
-// Function: Q_pow()
+// Function: q_pow()
 // Usage:
-//   q2 = Q_pow(q, r);
+//   q2 = q_pow(q, r);
 // Description:
 //   Returns the quaternion that is the power of the quaternion q to the real exponent r.
 //   The returned quaternion is normalized if `q` is normalized.
-function Q_pow(q,r=1) =
-    assert( Q_is_quat(q) && is_finite(r),
-             "Invalid inputs")
-    let( theta = 2*atan2(norm(_Qvec(q)),_Qreal(q)) )
-    Quat(_Qvec(q), r*theta); //  Q_exp(r*Q_ln(q)); 
+function q_pow(q,r=1) =
+    assert( is_quaternion(q) && is_finite(r), "Invalid inputs")
+    let( theta = 2*atan2(norm(_qvec(q)),_qreal(q)) )
+    quat(_qvec(q), r*theta); //  q_exp(r*q_ln(q)); 
 
 
 
diff --git a/scripts/increment_version.sh b/scripts/increment_version.sh
index 1109536..298ef3c 100755
--- a/scripts/increment_version.sh
+++ b/scripts/increment_version.sh
@@ -1,15 +1,17 @@
-#!/bin/sh
+#!/bin/bash
 
 VERFILE="version.scad"
 
-vernums=$(grep ^BOSL_VERSION "$VERFILE" | sed 's/^.*[[]\([0-9,]*\)[]].*$/\1/')
-major=$(echo "$vernums" | awk -F, '{print $1}')
-minor=$(echo "$vernums" | awk -F, '{print $2}')
-revision=$(echo "$vernums" | awk -F, '{print $3}')
+if [[ "$(cat "$VERFILE")" =~  BOSL_VERSION.*=.*\[([0-9]+),\ *([0-9]+),\ *([0-9]+)\]\; ]]; then
+  major=${BASH_REMATCH[1]} minor=${BASH_REMATCH[2]} revision=${BASH_REMATCH[3]}
+  new_revision=$(( revision+1 ))
 
-newrev=$(($revision+1))
-echo "Current Version: $major.$minor.$revision"
-echo "New Version: $major.$minor.$newrev"
-
-sed -i '' 's/^BOSL_VERSION = .*$/BOSL_VERSION = ['"$major,$minor,$newrev];/g" $VERFILE
+  echo "Current Version: $major.$minor.$revision"
+  echo "New Version: $major.$minor.$new_revision"
 
+  sed -i.bak -e 's/^BOSL_VERSION = .*$/BOSL_VERSION = ['"$major,$minor,$new_revision];/g" "$VERFILE"
+  rm "$VERFILE".bak
+else
+  echo "Could not extract version number from $VERFILE" >&2
+  exit 1
+fi
diff --git a/scripts/linecount.sh b/scripts/linecount.sh
index 234d8cd..16ca273 100755
--- a/scripts/linecount.sh
+++ b/scripts/linecount.sh
@@ -1,17 +1,12 @@
 #!/bin/bash
 
-lib_comment_lines=$(grep '^// ' *.scad | wc -l)
-lib_code_lines=$(grep '^ *[^ /]' *.scad | wc -l)
-script_code_lines=$(grep '^ *[^ /]' scripts/*.sh scripts/*.py | wc -l)
-example_code_lines=$(grep '^ *[^ /]' examples/*.scad | wc -l)
-test_code_lines=$(grep '^ *[^ /]' tests/*.scad | wc -l)
-tutorial_lines=$(grep '^ *[^ /]' tutorials/*.md | wc -l)
+lib_comment_lines=$(cat -- *.scad | grep -c '^// ')
+tutorial_lines=$(cat tutorials/*.md | grep -c '^ *[^ /]')
 
-y=$(printf "%06d" 13)
-
-printf "Documentation Lines : %6d\n" $(($lib_comment_lines+$tutorial_lines))
-printf "Example Code Lines  : %6d\n" $example_code_lines
-printf "Library Code Lines  : %6d\n" $lib_code_lines
-printf "Support Script Lines: %6d\n" $script_code_lines
-printf "Test Code Lines     : %6d\n" $test_code_lines
+printf '%-20s: %6d\n' \
+  'Documentation Lines'  "$(( lib_comment_lines + tutorial_lines ))" \
+  'Example Code Lines'   "$(cat examples/*.scad | grep -c '^ *[^ /]')" \
+  'Library Code Lines'   "$(cat -- *.scad | grep -c '^ *[^ /]')" \
+  'Support Script Lines' "$(cat scripts/*.sh scripts/*.py | grep -c '^ *[^ /]')" \
+  'Test Code Lines'      "$(cat tests/*.scad | grep -c '^ *[^ /]')"
 
diff --git a/scripts/make_tutorials.sh b/scripts/make_tutorials.sh
index c092db5..b728607 100755
--- a/scripts/make_tutorials.sh
+++ b/scripts/make_tutorials.sh
@@ -1,40 +1,38 @@
 #!/bin/bash
 
-FORCED=""
-FILES=""
-DISPMD=""
+DISPMD=0
+GEN_ARGS=()
+FILES=()
 for opt in "$@" ; do
-  case $opt in
-    -f ) FORCED=$opt ;;
-    -d ) DISPMD=$opt ;;
-    -* ) echo "Unknown option $opt"; exit -1 ;;
-    * ) FILES="$FILES $opt" ;;
+  case "$opt" in
+    -f ) GEN_ARGS+=(-f) ;;
+    -d ) DISPMD=1 ;;
+    -* ) echo "Unknown option $opt" >&2; exit 1 ;;
+    * ) FILES+=("$opt") ;;
   esac
 done
 
-if [[ "$FILES" != "" ]]; then
-    PREVIEW_LIBS="$FILES"
-else
-    PREVIEW_LIBS="Shapes2d Shapes3d Transforms Distributors Mutators Attachments Paths FractalTree"
+if (( ${#FILES[@]} == 0 )); then
+    FILES=(Shapes2d Shapes3d Transforms Distributors Mutators Attachments Paths FractalTree)
 fi
 
-dir="$(basename $PWD)"
-if [ "$dir" = "BOSL2" ]; then
-    cd BOSL2.wiki
-elif [ "$dir" != "BOSL2.wiki" ]; then
-    echo "Must run this script from the BOSL2 or BOSL2/BOSL2.wiki directories."
+# Try to cd to the BOSL2.wiki directory if run from the BOSL2 root
+if [[ "$(basename "$PWD")" != "BOSL2.wiki" ]]; then
+  if ! cd BOSL2.wiki; then
+    echo "BOSL2.wiki directory not found, try running from the BOSL2 or BOSL2/BOSL2.wiki directory" >&2
     exit 1
+  fi
 fi
 
 rm -f tmp_*.scad
-for base in $PREVIEW_LIBS; do
-    base="$(basename $base .md)"
+for base in "${FILES[@]}"; do
+    base="$(basename "$base" .md)"
     mkdir -p images/tutorials
-    rm -f images/tutorials/${base}_*.png images/tutorials/${base}_*.gif
-    echo "$base.md"
-    ../scripts/tutorial_gen.py ../tutorials/$base.md -o Tutorial-$base.md $FORCED -I images/tutorials/ || exit 1
-    if [ "$DISPMD" != "" ]; then
-        open -a Typora Tutorial-$base.md
+    rm -f "images/tutorials/${base}"_*.png "images/tutorials/${base}"_*.gif
+    echo "${base}.md"
+    ../scripts/tutorial_gen.py "../tutorials/${base}.md" -o "Tutorial-${base}.md" "${GEN_ARGS[@]}" -I images/tutorials/ || exit 1
+    if (( DISPMD )); then
+        open -a Typora "Tutorial-${base}.md"
     fi
 done
 
diff --git a/scripts/purge_wiki_history.sh b/scripts/purge_wiki_history.sh
index 5392677..7cef346 100755
--- a/scripts/purge_wiki_history.sh
+++ b/scripts/purge_wiki_history.sh
@@ -1,16 +1,16 @@
 #!/bin/bash
 
 if [[ ! -d BOSL2.wiki/.git ]] ; then
-	echo "Must be run from the BOSL2 directory, with the BOSL2.wiki repo inside."
-	exit -1
+	echo "Must be run from above the BOSL2.wiki repo." >&2
+	exit 1
 fi
 
+set -e # short-circuit if any command fails
 cd BOSL2.wiki
 rm -rf .git
 git init
 git add .
 git commit -m "Purged wiki history."
+git config pull.rebase false
 git remote add origin git@github.com:revarbat/BOSL2.wiki.git
 git push -u --force origin master
-cd ..
-
diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh
index 5fc0099..bcfc494 100755
--- a/scripts/run_tests.sh
+++ b/scripts/run_tests.sh
@@ -1,35 +1,31 @@
 #!/bin/bash
 
-if [ "$(uname -s)" != "Darwin" ]; then
-    OPENSCAD=openscad
-else
+OPENSCAD=openscad
+if [ "$(uname -s)" == "Darwin" ]; then
     OPENSCAD=/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD
 fi
 
-if [ "$*" != "" ] ; then
-    INFILES="$*"
-else
-    INFILES="tests/test_*.scad"
+INFILES=("$@")
+if (( ${#INFILES[@]} == 0 )); then
+    INFILES=(tests/test_*.scad)
 fi
 
 OUTCODE=0
-for testscript in $INFILES ; do
-    repname="$(basename $testscript | sed 's/^test_//')"
-    testfile="tests/test_$repname"
-    if [ -f "$testfile" ] ; then
-        ${OPENSCAD} -o out.echo --hardwarnings --check-parameters true --check-parameter-ranges true $testfile 2>&1
+for testfile in "${INFILES[@]}"; do
+    if [[ -f "$testfile" ]] ; then
+        repname="$(basename "$testfile" | sed 's/^test_//')"
+        "${OPENSCAD}" -o out.echo --hardwarnings --check-parameters true --check-parameter-ranges true "$testfile" 2>&1
         retcode=$?
-        res=$(cat out.echo)
-        if [ $retcode -eq 0 ] && [ "$res" = "" ] ; then
+        output=$(cat out.echo)
+        if (( retcode == 0 )) && [[ "$output" = "" ]]; then
             echo "$repname: PASS"
         else
             echo "$repname: FAIL!"
-            cat out.echo
-            echo
-            OUTCODE=-1
+            echo "$output"
+            OUTCODE=1
         fi
         rm -f out.echo
     fi
 done
-exit $OUTCODE
+exit "$OUTCODE"
 
diff --git a/shapes.scad b/shapes.scad
index f485b5a..388bcec 100644
--- a/shapes.scad
+++ b/shapes.scad
@@ -57,17 +57,41 @@
 // Example: Rounded Edges, Untrimmed Corners
 //   cuboid([30,40,50], rounding=10, trimcorners=false);
 // Example: Chamferring Selected Edges
-//   cuboid([30,40,50], chamfer=5, edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT], $fn=24);
+//   cuboid(
+//       [30,40,50], chamfer=5,
+//       edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT],
+//       $fn=24
+//   );
 // Example: Rounding Selected Edges
-//   cuboid([30,40,50], rounding=5, edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT], $fn=24);
+//   cuboid(
+//       [30,40,50], rounding=5,
+//       edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT],
+//       $fn=24
+//   );
 // Example: Negative Chamferring
-//   cuboid([30,40,50], chamfer=-5, edges=[TOP,BOT], except_edges=RIGHT, $fn=24);
+//   cuboid(
+//       [30,40,50], chamfer=-5,
+//       edges=[TOP,BOT], except_edges=RIGHT,
+//       $fn=24
+//   );
 // Example: Negative Chamferring, Untrimmed Corners
-//   cuboid([30,40,50], chamfer=-5, edges=[TOP,BOT], except_edges=RIGHT, trimcorners=false, $fn=24);
+//   cuboid(
+//       [30,40,50], chamfer=-5,
+//       edges=[TOP,BOT], except_edges=RIGHT,
+//       trimcorners=false, $fn=24
+//   );
 // Example: Negative Rounding
-//   cuboid([30,40,50], rounding=-5, edges=[TOP,BOT], except_edges=RIGHT, $fn=24);
+//   cuboid(
+//       [30,40,50], rounding=-5,
+//       edges=[TOP,BOT], except_edges=RIGHT,
+//       $fn=24
+//   );
 // Example: Negative Rounding, Untrimmed Corners
-//   cuboid([30,40,50], rounding=-5, edges=[TOP,BOT], except_edges=RIGHT, trimcorners=false, $fn=24);
+//   cuboid(
+//       [30,40,50], rounding=-5,
+//       edges=[TOP,BOT], except_edges=RIGHT,
+//       trimcorners=false, $fn=24
+//   );
 // Example: Standard Connectors
 //   cuboid(40) show_anchors();
 module cuboid(
@@ -87,9 +111,9 @@ module cuboid(
         cnt = sum(e);
         r = first_defined([chamfer, rounding, 0]);
         c = [min(r,size.x/2), min(r,size.y/2), min(r,size.z/2)];
-        c2 = vmul(corner,c/2);
+        c2 = v_mul(corner,c/2);
         $fn = is_finite(chamfer)? 4 : segs(r);
-        translate(vmul(corner, size/2-c)) {
+        translate(v_mul(corner, size/2-c)) {
             if (cnt == 0 || approx(r,0)) {
                 translate(c2) cube(c, center=true);
             } else if (cnt == 1) {
@@ -130,6 +154,7 @@ module cuboid(
     size = scalar_vec3(size);
     edges = edges(edges, except=except_edges);
     assert(is_vector(size,3));
+    assert(all_positive(size));
     assert(is_undef(chamfer) || is_finite(chamfer));
     assert(is_undef(rounding) || is_finite(rounding));
     assert(is_undef(p1) || is_vector(p1));
@@ -138,7 +163,7 @@ module cuboid(
     if (!is_undef(p1)) {
         if (!is_undef(p2)) {
             translate(pointlist_bounds([p1,p2])[0]) {
-                cuboid(size=vabs(p2-p1), chamfer=chamfer, rounding=rounding, edges=edges, trimcorners=trimcorners, anchor=ALLNEG) children();
+                cuboid(size=v_abs(p2-p1), chamfer=chamfer, rounding=rounding, edges=edges, trimcorners=trimcorners, anchor=ALLNEG) children();
             }
         } else {
             translate(p1) {
@@ -184,7 +209,7 @@ module cuboid(
                             for (i = [0:3], axis=[0:1]) {
                                 if (edges[axis][i]>0) {
                                     vec = EDGE_OFFSETS[axis][i];
-                                    translate(vmul(vec/2, size+[ach,ach,-ach])) {
+                                    translate(v_mul(vec/2, size+[ach,ach,-ach])) {
                                         rotate(majrots[axis]) {
                                             cube([ach, ach, size[axis]], center=true);
                                         }
@@ -197,7 +222,7 @@ module cuboid(
                                 for (za=[-1,1], ya=[-1,1], xa=[-1,1]) {
                                     ce = corner_edges(edges, [xa,ya,za]);
                                     if (ce.x + ce.y > 1) {
-                                        translate(vmul([xa,ya,za]/2, size+[ach-0.01,ach-0.01,-ach])) {
+                                        translate(v_mul([xa,ya,za]/2, size+[ach-0.01,ach-0.01,-ach])) {
                                             cube([ach+0.01,ach+0.01,ach], center=true);
                                         }
                                     }
@@ -209,7 +234,7 @@ module cuboid(
                         for (i = [0:3], axis=[0:1]) {
                             if (edges[axis][i]>0) {
                                 vec = EDGE_OFFSETS[axis][i];
-                                translate(vmul(vec/2, size+[2*ach,2*ach,-2*ach])) {
+                                translate(v_mul(vec/2, size+[2*ach,2*ach,-2*ach])) {
                                     rotate(majrots[axis]) {
                                         zrot(45) cube([ach*sqrt(2), ach*sqrt(2), size[axis]+2.1*ach], center=true);
                                     }
@@ -271,7 +296,7 @@ module cuboid(
                             for (i = [0:3], axis=[0:1]) {
                                 if (edges[axis][i]>0) {
                                     vec = EDGE_OFFSETS[axis][i];
-                                    translate(vmul(vec/2, size+[ard,ard,-ard])) {
+                                    translate(v_mul(vec/2, size+[ard,ard,-ard])) {
                                         rotate(majrots[axis]) {
                                             cube([ard, ard, size[axis]], center=true);
                                         }
@@ -284,7 +309,7 @@ module cuboid(
                                 for (za=[-1,1], ya=[-1,1], xa=[-1,1]) {
                                     ce = corner_edges(edges, [xa,ya,za]);
                                     if (ce.x + ce.y > 1) {
-                                        translate(vmul([xa,ya,za]/2, size+[ard-0.01,ard-0.01,-ard])) {
+                                        translate(v_mul([xa,ya,za]/2, size+[ard-0.01,ard-0.01,-ard])) {
                                             cube([ard+0.01,ard+0.01,ard], center=true);
                                         }
                                     }
@@ -296,7 +321,7 @@ module cuboid(
                         for (i = [0:3], axis=[0:1]) {
                             if (edges[axis][i]>0) {
                                 vec = EDGE_OFFSETS[axis][i];
-                                translate(vmul(vec/2, size+[2*ard,2*ard,-2*ard])) {
+                                translate(v_mul(vec/2, size+[2*ard,2*ard,-2*ard])) {
                                     rotate(majrots[axis]) {
                                         cyl(l=size[axis]+2.1*ard, r=ard);
                                     }
@@ -363,10 +388,6 @@ function cuboid(
 //   Creates a rectangular prismoid shape with optional roundovers and chamfering.
 //   You can only round or chamfer the vertical(ish) edges.  For those edges, you can
 //   specify rounding and/or chamferring per-edge, and for top and bottom separately.
-//   Note: if using chamfers or rounding, you **must** also include the hull.scad file:
-//   ```
-//   include <BOSL2/hull.scad>
-//   ```
 //
 // Arguments:
 //   size1 = [width, length] of the bottom end of the prism.
@@ -374,12 +395,12 @@ function cuboid(
 //   h|l = Height of the prism.
 //   shift = [X,Y] amount to shift the center of the top end with respect to the center of the bottom end.
 //   ---
-//   rounding = The roundover radius for the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding)
-//   rounding1 = The roundover radius for the bottom of the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
-//   rounding2 = The roundover radius for the top of the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
-//   chamfer = The chamfer size for the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].  Default: 0 (no chamfer)
-//   chamfer1 = The chamfer size for the bottom of the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
-//   chamfer2 = The chamfer size for the top of the vertical-ish edges of the prismoid.  Requires including hull.scad.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
+//   rounding = The roundover radius for the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding)
+//   rounding1 = The roundover radius for the bottom of the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
+//   rounding2 = The roundover radius for the top of the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
+//   chamfer = The chamfer size for the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].  Default: 0 (no chamfer)
+//   chamfer1 = The chamfer size for the bottom of the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
+//   chamfer2 = The chamfer size for the top of the vertical-ish edges of the prismoid.  If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-].
 //   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`
@@ -401,25 +422,22 @@ function cuboid(
 // Example(FlatSpin,VPD=160,VPT=[0,0,10]): Shifting/Skewing
 //   prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]);
 // Example: Rounding
-//   include <BOSL2/hull.scad>
 //   prismoid(100, 80, rounding=10, h=30);
 // Example: Outer Chamfer Only
-//   include <BOSL2/hull.scad>
 //   prismoid(100, 80, chamfer=5, h=30);
 // Example: Gradiant Rounding
-//   include <BOSL2/hull.scad>
 //   prismoid(100, 80, rounding1=10, rounding2=0, h=30);
 // Example: Per Corner Rounding
-//   include <BOSL2/hull.scad>
 //   prismoid(100, 80, rounding=[0,5,10,15], h=30);
 // Example: Per Corner Chamfer
-//   include <BOSL2/hull.scad>
 //   prismoid(100, 80, chamfer=[0,5,10,15], h=30);
 // Example: Mixing Chamfer and Rounding
-//   include <BOSL2/hull.scad>
-//   prismoid(100, 80, chamfer=[0,5,0,10], rounding=[5,0,10,0], h=30);
+//   prismoid(
+//       100, 80, h=30,
+//       chamfer=[0,5,0,10],
+//       rounding=[5,0,10,0]
+//   );
 // Example: Really Mixing It Up
-//   include <BOSL2/hull.scad>
 //   prismoid(
 //       size1=[100,80], size2=[80,60], h=20,
 //       chamfer1=[0,5,0,10], chamfer2=[5,0,10,0],
@@ -448,6 +466,10 @@ module prismoid(
     eps = pow(2,-14);
     size1 = is_num(size1)? [size1,size1] : size1;
     size2 = is_num(size2)? [size2,size2] : size2;
+    assert(all_nonnegative(size1));
+    assert(all_nonnegative(size2));
+    assert(size1.x + size2.x > 0);
+    assert(size1.y + size2.y > 0);
     s1 = [max(size1.x, eps), max(size1.y, eps)];
     s2 = [max(size2.x, eps), max(size2.y, eps)];
     rounding1 = default(rounding1, rounding);
@@ -499,8 +521,8 @@ function prismoid(
             let(
                 corners = [[1,1],[1,-1],[-1,-1],[-1,1]] * 0.5,
                 points = [
-                    for (p=corners) point3d(vmul(s2,p), +h/2) + shiftby,
-                    for (p=corners) point3d(vmul(s1,p), -h/2)
+                    for (p=corners) point3d(v_mul(s2,p), +h/2) + shiftby,
+                    for (p=corners) point3d(v_mul(s1,p), -h/2)
                 ],
                 faces=[
                     [0,1,2], [0,2,3], [0,4,5], [0,5,1],
@@ -551,10 +573,6 @@ function prismoid(
 //   You can only round or chamfer the vertical(ish) edges.  For those edges, you can
 //   specify rounding and/or chamferring per-edge, and for top and bottom, inside and
 //   outside  separately.
-//   Note: if using chamfers or rounding, you **must** also include the hull.scad file:
-//   ```
-//   include <BOSL2/hull.scad>
-//   ```
 // Arguments:
 //   h|l = The height or length of the rectangular tube.  Default: 1
 //   size = The outer [X,Y] size of the rectangular tube.
@@ -588,33 +606,42 @@ function prismoid(
 //   rect_tube(isize=[60,80], wall=5, h=30);
 //   rect_tube(size=[100,60], isize=[90,50], h=30);
 //   rect_tube(size1=[100,60], size2=[70,40], wall=5, h=30);
-//   rect_tube(size1=[100,60], size2=[70,40], isize1=[40,20], isize2=[65,35], h=15);
+// Example:
+//   rect_tube(
+//       size1=[100,60], size2=[70,40],
+//       isize1=[40,20], isize2=[65,35], h=15
+//   );
 // Example: Outer Rounding Only
-//   include <BOSL2/hull.scad>
 //   rect_tube(size=100, wall=5, rounding=10, irounding=0, h=30);
 // Example: Outer Chamfer Only
-//   include <BOSL2/hull.scad>
 //   rect_tube(size=100, wall=5, chamfer=5, ichamfer=0, h=30);
 // Example: Outer Rounding, Inner Chamfer
-//   include <BOSL2/hull.scad>
 //   rect_tube(size=100, wall=5, rounding=10, ichamfer=8, h=30);
 // Example: Inner Rounding, Outer Chamfer
-//   include <BOSL2/hull.scad>
 //   rect_tube(size=100, wall=5, chamfer=10, irounding=8, h=30);
 // Example: Gradiant Rounding
-//   include <BOSL2/hull.scad>
-//   rect_tube(size1=100, size2=80, wall=5, rounding1=10, rounding2=0, irounding1=8, irounding2=0, h=30);
+//   rect_tube(
+//       size1=100, size2=80, wall=5, h=30,
+//       rounding1=10, rounding2=0,
+//       irounding1=8, irounding2=0
+//   );
 // Example: Per Corner Rounding
-//   include <BOSL2/hull.scad>
-//   rect_tube(size=100, wall=10, rounding=[0,5,10,15], irounding=0, h=30);
+//   rect_tube(
+//       size=100, wall=10, h=30,
+//       rounding=[0,5,10,15], irounding=0
+//   );
 // Example: Per Corner Chamfer
-//   include <BOSL2/hull.scad>
-//   rect_tube(size=100, wall=10, chamfer=[0,5,10,15], ichamfer=0, h=30);
+//   rect_tube(
+//       size=100, wall=10, h=30,
+//       chamfer=[0,5,10,15], ichamfer=0
+//   );
 // Example: Mixing Chamfer and Rounding
-//   include <BOSL2/hull.scad>
-//   rect_tube(size=100, wall=10, chamfer=[0,5,0,10], ichamfer=0, rounding=[5,0,10,0], irounding=0, h=30);
+//   rect_tube(
+//       size=100, wall=10, h=30,
+//       chamfer=[0,5,0,10], ichamfer=0,
+//       rounding=[5,0,10,0], irounding=0
+//   );
 // Example: Really Mixing It Up
-//   include <BOSL2/hull.scad>
 //   rect_tube(
 //       size1=[100,80], size2=[80,60],
 //       isize1=[50,30], isize2=[70,50], h=20,
@@ -838,7 +865,11 @@ function right_triangle(size=[1,1,1], center, anchor, spin=0, orient=UP) =
 //   }
 //
 // Example: Putting it all together
-//   cyl(l=40, d1=25, d2=15, chamfer1=10, chamfang1=30, from_end=true, rounding2=5);
+//   cyl(
+//       l=40, d1=25, d2=15,
+//       chamfer1=10, chamfang1=30,
+//       from_end=true, rounding2=5
+//   );
 //
 // Example: External Chamfers
 //   cyl(l=50, r=30, chamfer=-5, chamfang=30, $fa=1, $fs=1);
@@ -1504,6 +1535,9 @@ function spheroid(r, style="aligned", d, circum=false, anchor=CENTER, spin=0, or
 // Usage: Typical
 //   teardrop(h|l, r, <ang>, <cap_h>, ...);
 //   teardrop(h|l, d=, <ang=>, <cap_h=>, ...);
+// Usage: Psuedo-Conical
+//   teardrop(h|l, r1=, r2=, <ang=>, <cap_h1=>, <cap_h2=>, ...);
+//   teardrop(h|l, d1=, d2=, <ang=>, <cap_h1=>, <cap_h2=>, ...);
 // Usage: Attaching Children
 //   teardrop(h|l, r, ...) <attachments>;
 //
@@ -1513,33 +1547,71 @@ function spheroid(r, style="aligned", d, circum=false, anchor=CENTER, spin=0, or
 //   ang = Angle of hat walls from the Z axis.  Default: 45 degrees
 //   cap_h = If given, height above center where the shape will be truncated. Default: `undef` (no truncation)
 //   ---
-//   d = Diameter of circular portion of bottom. (Use instead of r)
+//   r1 = Radius of circular portion of the front end of the teardrop shape.
+//   r2 = Radius of circular portion of the back end of the teardrop shape.
+//   d = Diameter of circular portion of the teardrop shape.
+//   d1 = Diameter of circular portion of the front end of the teardrop shape.
+//   d2 = Diameter of circular portion of the back end of the teardrop shape.
+//   cap_h1 = If given, height above center where the shape will be truncated, on the front side. Default: `undef` (no truncation)
+//   cap_h2 = If given, height above center where the shape will be truncated, on the back side. Default: `undef` (no truncation)
 //   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:
+//   cap = The center of the top of the cap, oriented with the cap face normal.
+//   cap_fwd = The front edge of the cap.
+//   cap_back = The back edge of the cap.
+//
 // Example: Typical Shape
 //   teardrop(r=30, h=10, ang=30);
 // Example: Crop Cap
 //   teardrop(r=30, h=10, ang=30, cap_h=40);
 // Example: Close Crop
 //   teardrop(r=30, h=10, ang=30, cap_h=20);
-// Example: Standard Connectors
-//   teardrop(r=30, h=10, ang=30) show_anchors();
-module teardrop(h, r, ang=45, cap_h, d, l, anchor=CENTER, spin=0, orient=UP)
+// Example: Psuedo-Conical
+//   teardrop(r1=20, r2=30, h=40, cap_h1=25, cap_h2=35);
+// Example: Standard Conical Connectors
+//   teardrop(d1=20, d2=30, h=20, cap_h1=11, cap_h2=16)
+//       show_anchors(custom=false);
+// Example(Spin,VPD=275): Named Conical Connectors
+//   teardrop(d1=20, d2=30, h=20, cap_h1=11, cap_h2=16)
+//       show_anchors(std=false);
+module teardrop(h, r, ang=45, cap_h, r1, r2, d, d1, d2, cap_h1, cap_h2, l, anchor=CENTER, spin=0, orient=UP)
 {
-    r = get_radius(r=r, d=d, dflt=1);
+    r1 = get_radius(r=r, r1=r1, d=d, d1=d1, dflt=1);
+    r2 = get_radius(r=r, r1=r2, d=d, d1=d2, dflt=1);
     l = first_defined([l, h, 1]);
-    tip_y = adj_ang_to_hyp(r, 90-ang);
-    cap_h = min(default(cap_h,tip_y), tip_y);
+    tip_y1 = adj_ang_to_hyp(r1, 90-ang);
+    tip_y2 = adj_ang_to_hyp(r2, 90-ang);
+    cap_h1 = min(first_defined([cap_h1, cap_h, tip_y1]), tip_y1);
+    cap_h2 = min(first_defined([cap_h2, cap_h, tip_y2]), tip_y2);
+    capvec = unit([0, cap_h1-cap_h2, l]);
     anchors = [
-        ["cap", [0,0,cap_h], UP, 0]
+        anchorpt("cap",      [0,0,(cap_h1+cap_h2)/2], capvec),
+        anchorpt("cap_fwd",  [0,-l/2,cap_h1],         unit((capvec+FWD)/2)),
+        anchorpt("cap_back", [0,+l/2,cap_h2],         unit((capvec+BACK)/2), 180),
     ];
-    attachable(anchor,spin,orient, r=r, l=l, axis=BACK, anchors=anchors) {
+    attachable(anchor,spin,orient, r1=r1, r2=r2, l=l, axis=BACK, anchors=anchors) {
         rot(from=UP,to=FWD) {
             if (l > 0) {
-                linear_extrude(height=l, center=true, slices=2) {
-                    teardrop2d(r=r, ang=ang, cap_h=cap_h);
+                if (r1 == r2) {
+                    linear_extrude(height=l, center=true, slices=2) {
+                        teardrop2d(r=r1, ang=ang, cap_h=cap_h);
+                    }
+                } else {
+                    hull() {
+                        up(l/2-0.001) {
+                            linear_extrude(height=0.001, center=false) {
+                                teardrop2d(r=r1, ang=ang, cap_h=cap_h1);
+                            }
+                        }
+                        down(l/2) {
+                            linear_extrude(height=0.001, center=false) {
+                                teardrop2d(r=r2, ang=ang, cap_h=cap_h2);
+                            }
+                        }
+                    }
                 }
             }
         }
@@ -1702,9 +1774,15 @@ module pie_slice(
 //
 // Example:
 //   union() {
-//       translate([0,2,-4]) cube([20, 4, 24], anchor=BOTTOM);
-//       translate([0,-10,-4]) cube([20, 20, 4], anchor=BOTTOM);
-//       color("green") interior_fillet(l=20, r=10, spin=180, orient=RIGHT);
+//       translate([0,2,-4])
+//           cube([20, 4, 24], anchor=BOTTOM);
+//       translate([0,-10,-4])
+//           cube([20, 20, 4], anchor=BOTTOM);
+//       color("green")
+//           interior_fillet(
+//               l=20, r=10,
+//               spin=180, orient=RIGHT
+//           );
 //   }
 //
 // Example:
@@ -1762,21 +1840,30 @@ module interior_fillet(l=1.0, r, ang=90, overlap=0.01, d, anchor=FRONT+LEFT, spi
 //   orient = Vector to rotate top towards.  See [orient](attachments.scad#orient).  Default: `UP`
 // Example:
 //   heightfield(size=[100,100], bottom=-20, data=[
-//       for (y=[-180:4:180]) [for(x=[-180:4:180]) 10*cos(3*norm([x,y]))]
+//       for (y=[-180:4:180]) [
+//           for(x=[-180:4:180])
+//           10*cos(3*norm([x,y]))
+//       ]
 //   ]);
 // Example:
 //   intersection() {
 //       heightfield(size=[100,100], data=[
-//           for (y=[-180:5:180]) [for(x=[-180:5:180]) 10+5*cos(3*x)*sin(3*y)]
+//           for (y=[-180:5:180]) [
+//               for(x=[-180:5:180])
+//               10+5*cos(3*x)*sin(3*y)
+//           ]
 //       ]);
 //       cylinder(h=50,d=100);
 //   }
-// Example(NORENDER): Heightfield by Function
+// Example: Heightfield by Function
 //   fn = function (x,y) 10*sin(x*360)*cos(y*360);
 //   heightfield(size=[100,100], data=fn);
-// Example(NORENDER): Heightfield by Function, with Specific Ranges
+// Example: Heightfield by Function, with Specific Ranges
 //   fn = function (x,y) 2*cos(5*norm([x,y]));
-//   heightfield(size=[100,100], bottom=-20, data=fn, xrange=[-180:2:180], yrange=[-180:2:180]);
+//   heightfield(
+//       size=[100,100], bottom=-20, data=fn,
+//       xrange=[-180:2:180], yrange=[-180:2:180]
+//   );
 module heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04:1], yrange=[-1:0.04:1], style="default", convexity=10, anchor=CENTER, spin=0, orient=UP)
 {
     size = is_num(size)? [size,size] : point2d(size);
diff --git a/shapes2d.scad b/shapes2d.scad
index f24fa0c..b54a64f 100644
--- a/shapes2d.scad
+++ b/shapes2d.scad
@@ -307,15 +307,15 @@ module stroke(
                 multmatrix(mat) polygon(endcap_shape2);
             }
         } else {
-            quatsums = Q_Cumulative([
+            quatsums = q_cumulative([
                 for (i = idx(path2,e=-2)) let(
                     vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP),
                     vec2 = unit(path2[i+1]-path2[i], UP),
                     axis = vector_axis(vec1,vec2),
                     ang = vector_angle(vec1,vec2)
-                ) Quat(axis,ang)
+                ) quat(axis,ang)
             ]);
-            rotmats = [for (q=quatsums) Q_Matrix4(q)];
+            rotmats = [for (q=quatsums) q_matrix4(q)];
             sides = [
                 for (i = idx(path2,e=-2))
                 quantup(segs(max(widths[i],widths[i+1])/2),4)
@@ -959,7 +959,7 @@ function rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) =
             [-size.x/2,  size.y/2],
             [ size.x/2,  size.y/2] 
         ]
-    ) rot(spin, p=move(-vmul(anchor,size/2), p=path)) :
+    ) rot(spin, p=move(-v_mul(anchor,size/2), p=path)) :
     let(
         chamfer = is_list(chamfer)? chamfer : [for (i=[0:3]) chamfer],
         rounding = is_list(rounding)? rounding : [for (i=[0:3]) rounding],
@@ -978,7 +978,7 @@ function rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) =
                 quad = quadorder[i],
                 inset = insets[quad],
                 cverts = quant(segs(inset),4)/4,
-                cp = vmul(size/2-[inset,inset], quadpos[quad]),
+                cp = v_mul(size/2-[inset,inset], quadpos[quad]),
                 step = 90/cverts,
                 angs =
                     chamfer[quad] > 0?  [0,-90]-90*[i,i] :
diff --git a/skin.scad b/skin.scad
index 4609556..1614fdb 100644
--- a/skin.scad
+++ b/skin.scad
@@ -812,24 +812,30 @@ function _skin_tangent_match(poly1, poly2) =
         newbig = polygon_shift(big, shift),
         repeat_counts = [for(i=[0:len(small)-1]) posmod(cutpts[i]-select(cutpts,i-1),len(big))],
         newsmall = repeat_entries(small,repeat_counts)
-      )
-      assert(len(newsmall)==len(newbig), "Tangent alignment failed, probably because of insufficient points or a concave curve")
-      swap ? [newbig, newsmall] : [newsmall, newbig];
+    )
+    assert(len(newsmall)==len(newbig), "Tangent alignment failed, probably because of insufficient points or a concave curve")
+    swap ? [newbig, newsmall] : [newsmall, newbig];
 
 
 function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) =
-  let(
-   angles = 
-     [for(i=[0:len(curve)-(closed?1:2)])
-       let( 
-         plane = plane3pt( edge[0], edge[1], curve[i]),
-         tangent = [curve[i], select(curve,i+1)]
-       )
-     plane_line_angle(plane,tangent)],
-   zero_cross = [for(i=[0:len(curve)-(closed?1:2)]) if (sign(angles[i]) != sign(select(angles,i+1))) i],
-   d = [for(i=zero_cross) distance_from_line(edge, curve[i]+curve_offset)]
-  )
-  zero_cross[min_index(d)];
+    let(
+        angles = [
+            for (i = [0:len(curve)-(closed?1:2)])
+            let( 
+                plane = plane3pt( edge[0], edge[1], curve[i]),
+                tangent = [curve[i], select(curve,i+1)]
+            ) plane_line_angle(plane,tangent)
+        ],
+        zero_cross = [
+            for (i = [0:len(curve)-(closed?1:2)])
+            if (sign(angles[i]) != sign(select(angles,i+1)))
+            i
+        ],
+        d = [
+            for (i = zero_cross)
+            point_line_distance(curve[i]+curve_offset, edge)
+        ]
+    ) zero_cross[min_index(d)];
 
 
 // Function: associate_vertices()
diff --git a/std.scad b/std.scad
index 72b5dc8..f99e984 100644
--- a/std.scad
+++ b/std.scad
@@ -27,6 +27,7 @@ include <quaternions.scad>
 include <affine.scad>
 include <coords.scad>
 include <geometry.scad>
+include <hull.scad>
 include <regions.scad>
 include <strings.scad>
 include <skin.scad>
diff --git a/tests/test_math.scad b/tests/test_math.scad
index 025dde7..adcfb6d 100644
--- a/tests/test_math.scad
+++ b/tests/test_math.scad
@@ -1264,4 +1264,36 @@ module test_poly_add(){
 }
 test_poly_add();
 
+
+module test_root_find(){
+  flist = [
+      function(x) x*x*x-2*x-5,
+      function(x) 1-1/x/x,
+      function(x) pow(x-3,3),
+      function(x) pow(x-2,5),
+      function(x) (let(xi=0.61489) -3062*(1-xi)*exp(-x)/(xi+(1-xi)*exp(-x)) -1013 + 1628/x),
+      function(x) exp(x)-2-.01/x/x + .000002/x/x/x,
+  ];
+  fint=[
+        [0,4],
+        [1e-4, 4],
+        [0,6],
+        [0,4],
+        [1e-4,5],
+        [-1,4]
+  ];
+  answers = [2.094551481542328,
+             1,
+             3,
+             2,
+             1.037536033287040,
+             0.7032048403631350
+  ];
+  
+  roots = [for(i=idx(flist)) root_find(flist[i], fint[i][0], fint[i][1])];
+  assert_approx(roots, answers, 1e-10);
+}
+test_root_find();
+
+
 // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
diff --git a/tests/test_quaternions.scad b/tests/test_quaternions.scad
index b7c2f6f..fe95ebc 100644
--- a/tests/test_quaternions.scad
+++ b/tests/test_quaternions.scad
@@ -1,403 +1,384 @@
 include <../std.scad>
-include <../strings.scad>
 
 
-function rec_cmp(a,b,eps=1e-9) =
-    typeof(a)!=typeof(b)? false :
-    is_num(a)? approx(a,b,eps=eps) :
-    is_list(a)? len(a)==len(b) && all([for (i=idx(a)) rec_cmp(a[i],b[i],eps=eps)]) :
-    a == b;
 
-function Qstandard(q) = sign([for(qi=q) if( ! approx(qi,0)) qi,0 ][0])*q;
+function _q_standard(q) = sign([for(qi=q) if( ! approx(qi,0)) qi,0 ][0])*q;
 
-module verify_f(actual,expected) {
-    if (!rec_cmp(actual,expected)) {
-        echo(str("Expected: ",fmt_float(expected,10)));
-        echo(str("        : ",expected));
-        echo(str("Actual  : ",fmt_float(actual,10)));
-        echo(str("        : ",actual));
-        echo(str("Delta   : ",fmt_float(expected-actual,10)));
-        echo(str("        : ",expected-actual));
-        assert(approx(expected,actual));
-    }
+
+module test_is_quaternion() {
+    assert_approx(is_quaternion([0]),false);
+    assert_approx(is_quaternion([0,0,0,0]),false);
+    assert_approx(is_quaternion([1,0,2,0]),true);
+    assert_approx(is_quaternion([1,0,2,0,0]),false);
 }
+test_is_quaternion();
 
 
-module test_Q_is_quat() {
-    verify_f(Q_is_quat([0]),false);
-    verify_f(Q_is_quat([0,0,0,0]),false);
-    verify_f(Q_is_quat([1,0,2,0]),true);
-    verify_f(Q_is_quat([1,0,2,0,0]),false);
+module test_quat() {
+    assert_approx(quat(UP,0),[0,0,0,1]);
+    assert_approx(quat(FWD,0),[0,0,0,1]);
+    assert_approx(quat(LEFT,0),[0,0,0,1]);
+    assert_approx(quat(UP,45),[0,0,0.3826834324,0.9238795325]);
+    assert_approx(quat(LEFT,45),[-0.3826834324, 0, 0, 0.9238795325]);
+    assert_approx(quat(BACK,45),[0,0.3826834323,0,0.9238795325]);
+    assert_approx(quat(FWD+RIGHT,30),[0.1830127019, -0.1830127019, 0, 0.9659258263]);
 }
-test_Q_is_quat();
+test_quat();
 
 
-module test_Quat() {
-    verify_f(Quat(UP,0),[0,0,0,1]);
-    verify_f(Quat(FWD,0),[0,0,0,1]);
-    verify_f(Quat(LEFT,0),[0,0,0,1]);
-    verify_f(Quat(UP,45),[0,0,0.3826834324,0.9238795325]);
-    verify_f(Quat(LEFT,45),[-0.3826834324, 0, 0, 0.9238795325]);
-    verify_f(Quat(BACK,45),[0,0.3826834323,0,0.9238795325]);
-    verify_f(Quat(FWD+RIGHT,30),[0.1830127019, -0.1830127019, 0, 0.9659258263]);
+module test_quat_x() {
+    assert_approx(quat_x(0),[0,0,0,1]);
+    assert_approx(quat_x(35),[0.3007057995,0,0,0.9537169507]);
+    assert_approx(quat_x(45),[0.3826834324,0,0,0.9238795325]);
 }
-test_Quat();
+test_quat_x();
 
 
-module test_QuatX() {
-    verify_f(QuatX(0),[0,0,0,1]);
-    verify_f(QuatX(35),[0.3007057995,0,0,0.9537169507]);
-    verify_f(QuatX(45),[0.3826834324,0,0,0.9238795325]);
+module test_quat_y() {
+    assert_approx(quat_y(0),[0,0,0,1]);
+    assert_approx(quat_y(35),[0,0.3007057995,0,0.9537169507]);
+    assert_approx(quat_y(45),[0,0.3826834323,0,0.9238795325]);
 }
-test_QuatX();
+test_quat_y();
 
 
-module test_QuatY() {
-    verify_f(QuatY(0),[0,0,0,1]);
-    verify_f(QuatY(35),[0,0.3007057995,0,0.9537169507]);
-    verify_f(QuatY(45),[0,0.3826834323,0,0.9238795325]);
+module test_quat_z() {
+    assert_approx(quat_z(0),[0,0,0,1]);
+    assert_approx(quat_z(36),[0,0,0.3090169944,0.9510565163]);
+    assert_approx(quat_z(45),[0,0,0.3826834324,0.9238795325]);
 }
-test_QuatY();
+test_quat_z();
 
 
-module test_QuatZ() {
-    verify_f(QuatZ(0),[0,0,0,1]);
-    verify_f(QuatZ(36),[0,0,0.3090169944,0.9510565163]);
-    verify_f(QuatZ(45),[0,0,0.3826834324,0.9238795325]);
+module test_quat_xyz() {
+    assert_approx(quat_xyz([0,0,0]), [0,0,0,1]);
+    assert_approx(quat_xyz([30,0,0]), [0.2588190451, 0, 0, 0.9659258263]);
+    assert_approx(quat_xyz([90,0,0]), [0.7071067812, 0, 0, 0.7071067812]);
+    assert_approx(quat_xyz([-270,0,0]), [-0.7071067812, 0, 0, -0.7071067812]);
+    assert_approx(quat_xyz([180,0,0]), [1,0,0,0]);
+    assert_approx(quat_xyz([270,0,0]), [0.7071067812, 0, 0, -0.7071067812]);
+    assert_approx(quat_xyz([-90,0,0]), [-0.7071067812, 0, 0, 0.7071067812]);
+    assert_approx(quat_xyz([360,0,0]), [0,0,0,-1]);
+
+    assert_approx(quat_xyz([0,0,0]), [0,0,0,1]);
+    assert_approx(quat_xyz([0,30,0]), [0, 0.2588190451, 0, 0.9659258263]);
+    assert_approx(quat_xyz([0,90,0]), [0, 0.7071067812, 0, 0.7071067812]);
+    assert_approx(quat_xyz([0,-270,0]), [0, -0.7071067812, 0, -0.7071067812]);
+    assert_approx(quat_xyz([0,180,0]), [0,1,0,0]);
+    assert_approx(quat_xyz([0,270,0]), [0, 0.7071067812, 0, -0.7071067812]);
+    assert_approx(quat_xyz([0,-90,0]), [0, -0.7071067812, 0, 0.7071067812]);
+    assert_approx(quat_xyz([0,360,0]), [0,0,0,-1]);
+
+    assert_approx(quat_xyz([0,0,0]), [0,0,0,1]);
+    assert_approx(quat_xyz([0,0,30]), [0, 0, 0.2588190451, 0.9659258263]);
+    assert_approx(quat_xyz([0,0,90]), [0, 0, 0.7071067812, 0.7071067812]);
+    assert_approx(quat_xyz([0,0,-270]), [0, 0, -0.7071067812, -0.7071067812]);
+    assert_approx(quat_xyz([0,0,180]), [0,0,1,0]);
+    assert_approx(quat_xyz([0,0,270]), [0, 0, 0.7071067812, -0.7071067812]);
+    assert_approx(quat_xyz([0,0,-90]), [0, 0, -0.7071067812, 0.7071067812]);
+    assert_approx(quat_xyz([0,0,360]), [0,0,0,-1]);
+
+    assert_approx(quat_xyz([30,30,30]), [0.1767766953, 0.3061862178, 0.1767766953, 0.9185586535]);
+    assert_approx(quat_xyz([12,34,56]), [-0.04824789229, 0.3036636044, 0.4195145429, 0.8540890495]);
 }
-test_QuatZ();
+test_quat_xyz();
 
 
-module test_QuatXYZ() {
-    verify_f(QuatXYZ([0,0,0]), [0,0,0,1]);
-    verify_f(QuatXYZ([30,0,0]), [0.2588190451, 0, 0, 0.9659258263]);
-    verify_f(QuatXYZ([90,0,0]), [0.7071067812, 0, 0, 0.7071067812]);
-    verify_f(QuatXYZ([-270,0,0]), [-0.7071067812, 0, 0, -0.7071067812]);
-    verify_f(QuatXYZ([180,0,0]), [1,0,0,0]);
-    verify_f(QuatXYZ([270,0,0]), [0.7071067812, 0, 0, -0.7071067812]);
-    verify_f(QuatXYZ([-90,0,0]), [-0.7071067812, 0, 0, 0.7071067812]);
-    verify_f(QuatXYZ([360,0,0]), [0,0,0,-1]);
-
-    verify_f(QuatXYZ([0,0,0]), [0,0,0,1]);
-    verify_f(QuatXYZ([0,30,0]), [0, 0.2588190451, 0, 0.9659258263]);
-    verify_f(QuatXYZ([0,90,0]), [0, 0.7071067812, 0, 0.7071067812]);
-    verify_f(QuatXYZ([0,-270,0]), [0, -0.7071067812, 0, -0.7071067812]);
-    verify_f(QuatXYZ([0,180,0]), [0,1,0,0]);
-    verify_f(QuatXYZ([0,270,0]), [0, 0.7071067812, 0, -0.7071067812]);
-    verify_f(QuatXYZ([0,-90,0]), [0, -0.7071067812, 0, 0.7071067812]);
-    verify_f(QuatXYZ([0,360,0]), [0,0,0,-1]);
-
-    verify_f(QuatXYZ([0,0,0]), [0,0,0,1]);
-    verify_f(QuatXYZ([0,0,30]), [0, 0, 0.2588190451, 0.9659258263]);
-    verify_f(QuatXYZ([0,0,90]), [0, 0, 0.7071067812, 0.7071067812]);
-    verify_f(QuatXYZ([0,0,-270]), [0, 0, -0.7071067812, -0.7071067812]);
-    verify_f(QuatXYZ([0,0,180]), [0,0,1,0]);
-    verify_f(QuatXYZ([0,0,270]), [0, 0, 0.7071067812, -0.7071067812]);
-    verify_f(QuatXYZ([0,0,-90]), [0, 0, -0.7071067812, 0.7071067812]);
-    verify_f(QuatXYZ([0,0,360]), [0,0,0,-1]);
-
-    verify_f(QuatXYZ([30,30,30]), [0.1767766953, 0.3061862178, 0.1767766953, 0.9185586535]);
-    verify_f(QuatXYZ([12,34,56]), [-0.04824789229, 0.3036636044, 0.4195145429, 0.8540890495]);
+module test_q_from_to() {
+    assert_approx(q_mul(q_from_to([1,2,3], [4,5,2]),q_from_to([4,5,2], [1,2,3])), q_ident());
+    assert_approx(q_matrix4(q_from_to([1,2,3], [4,5,2])), rot(from=[1,2,3],to=[4,5,2]));
+    assert_approx(q_rot(q_from_to([1,2,3], -[1,2,3]),[1,2,3]), -[1,2,3]);
+    assert_approx(unit(q_rot(q_from_to([1,2,3],  [4,5,2]),[1,2,3])), unit([4,5,2]));
 }
-test_QuatXYZ();
+test_q_from_to();
 
 
-module test_Q_From_to() {
-    verify_f(Q_Mul(Q_From_to([1,2,3], [4,5,2]),Q_From_to([4,5,2], [1,2,3])), Q_Ident());
-    verify_f(Q_Matrix4(Q_From_to([1,2,3], [4,5,2])), rot(from=[1,2,3],to=[4,5,2]));
-    verify_f(Qrot(Q_From_to([1,2,3], -[1,2,3]),[1,2,3]), -[1,2,3]);
-    verify_f(unit(Qrot(Q_From_to([1,2,3],  [4,5,2]),[1,2,3])), unit([4,5,2]));
+module test_q_ident() {
+    assert_approx(q_ident(), [0,0,0,1]);
 }
-test_Q_From_to();
+test_q_ident();
 
 
-module test_Q_Ident() {
-    verify_f(Q_Ident(), [0,0,0,1]);
+module test_q_add_s() {
+    assert_approx(q_add_s([0,0,0,1],3),[0,0,0,4]);
+    assert_approx(q_add_s([0,0,1,0],3),[0,0,1,3]);
+    assert_approx(q_add_s([0,1,0,0],3),[0,1,0,3]);
+    assert_approx(q_add_s([1,0,0,0],3),[1,0,0,3]);
+    assert_approx(q_add_s(quat(LEFT+FWD,23),1),[-0.1409744184, -0.1409744184, 0, 1.979924705]);
 }
-test_Q_Ident();
+test_q_add_s();
 
 
-module test_Q_Add_S() {
-    verify_f(Q_Add_S([0,0,0,1],3),[0,0,0,4]);
-    verify_f(Q_Add_S([0,0,1,0],3),[0,0,1,3]);
-    verify_f(Q_Add_S([0,1,0,0],3),[0,1,0,3]);
-    verify_f(Q_Add_S([1,0,0,0],3),[1,0,0,3]);
-    verify_f(Q_Add_S(Quat(LEFT+FWD,23),1),[-0.1409744184, -0.1409744184, 0, 1.979924705]);
+module test_q_sub_s() {
+    assert_approx(q_sub_s([0,0,0,1],3),[0,0,0,-2]);
+    assert_approx(q_sub_s([0,0,1,0],3),[0,0,1,-3]);
+    assert_approx(q_sub_s([0,1,0,0],3),[0,1,0,-3]);
+    assert_approx(q_sub_s([1,0,0,0],3),[1,0,0,-3]);
+    assert_approx(q_sub_s(quat(LEFT+FWD,23),1),[-0.1409744184, -0.1409744184, 0, -0.02007529538]);
 }
-test_Q_Add_S();
+test_q_sub_s();
 
 
-module test_Q_Sub_S() {
-    verify_f(Q_Sub_S([0,0,0,1],3),[0,0,0,-2]);
-    verify_f(Q_Sub_S([0,0,1,0],3),[0,0,1,-3]);
-    verify_f(Q_Sub_S([0,1,0,0],3),[0,1,0,-3]);
-    verify_f(Q_Sub_S([1,0,0,0],3),[1,0,0,-3]);
-    verify_f(Q_Sub_S(Quat(LEFT+FWD,23),1),[-0.1409744184, -0.1409744184, 0, -0.02007529538]);
+module test_q_mul_s() {
+    assert_approx(q_mul_s([0,0,0,1],3),[0,0,0,3]);
+    assert_approx(q_mul_s([0,0,1,0],3),[0,0,3,0]);
+    assert_approx(q_mul_s([0,1,0,0],3),[0,3,0,0]);
+    assert_approx(q_mul_s([1,0,0,0],3),[3,0,0,0]);
+    assert_approx(q_mul_s([1,0,0,1],3),[3,0,0,3]);
+    assert_approx(q_mul_s(quat(LEFT+FWD,23),4),[-0.5638976735, -0.5638976735, 0, 3.919698818]);
 }
-test_Q_Sub_S();
+test_q_mul_s();
 
 
-module test_Q_Mul_S() {
-    verify_f(Q_Mul_S([0,0,0,1],3),[0,0,0,3]);
-    verify_f(Q_Mul_S([0,0,1,0],3),[0,0,3,0]);
-    verify_f(Q_Mul_S([0,1,0,0],3),[0,3,0,0]);
-    verify_f(Q_Mul_S([1,0,0,0],3),[3,0,0,0]);
-    verify_f(Q_Mul_S([1,0,0,1],3),[3,0,0,3]);
-    verify_f(Q_Mul_S(Quat(LEFT+FWD,23),4),[-0.5638976735, -0.5638976735, 0, 3.919698818]);
+
+module test_q_div_s() {
+    assert_approx(q_div_s([0,0,0,1],3),[0,0,0,1/3]);
+    assert_approx(q_div_s([0,0,1,0],3),[0,0,1/3,0]);
+    assert_approx(q_div_s([0,1,0,0],3),[0,1/3,0,0]);
+    assert_approx(q_div_s([1,0,0,0],3),[1/3,0,0,0]);
+    assert_approx(q_div_s([1,0,0,1],3),[1/3,0,0,1/3]);
+    assert_approx(q_div_s(quat(LEFT+FWD,23),4),[-0.03524360459, -0.03524360459, 0, 0.2449811762]);
 }
-test_Q_Mul_S();
+test_q_div_s();
 
 
-
-module test_Q_Div_S() {
-    verify_f(Q_Div_S([0,0,0,1],3),[0,0,0,1/3]);
-    verify_f(Q_Div_S([0,0,1,0],3),[0,0,1/3,0]);
-    verify_f(Q_Div_S([0,1,0,0],3),[0,1/3,0,0]);
-    verify_f(Q_Div_S([1,0,0,0],3),[1/3,0,0,0]);
-    verify_f(Q_Div_S([1,0,0,1],3),[1/3,0,0,1/3]);
-    verify_f(Q_Div_S(Quat(LEFT+FWD,23),4),[-0.03524360459, -0.03524360459, 0, 0.2449811762]);
+module test_q_add() {
+    assert_approx(q_add([2,3,4,5],[-1,-1,-1,-1]),[1,2,3,4]);
+    assert_approx(q_add([2,3,4,5],[-3,-3,-3,-3]),[-1,0,1,2]);
+    assert_approx(q_add([2,3,4,5],[0,0,0,0]),[2,3,4,5]);
+    assert_approx(q_add([2,3,4,5],[1,1,1,1]),[3,4,5,6]);
+    assert_approx(q_add([2,3,4,5],[1,0,0,0]),[3,3,4,5]);
+    assert_approx(q_add([2,3,4,5],[0,1,0,0]),[2,4,4,5]);
+    assert_approx(q_add([2,3,4,5],[0,0,1,0]),[2,3,5,5]);
+    assert_approx(q_add([2,3,4,5],[0,0,0,1]),[2,3,4,6]);
+    assert_approx(q_add([2,3,4,5],[2,1,2,1]),[4,4,6,6]);
+    assert_approx(q_add([2,3,4,5],[1,2,1,2]),[3,5,5,7]);
 }
-test_Q_Div_S();
+test_q_add();
 
 
-module test_Q_Add() {
-    verify_f(Q_Add([2,3,4,5],[-1,-1,-1,-1]),[1,2,3,4]);
-    verify_f(Q_Add([2,3,4,5],[-3,-3,-3,-3]),[-1,0,1,2]);
-    verify_f(Q_Add([2,3,4,5],[0,0,0,0]),[2,3,4,5]);
-    verify_f(Q_Add([2,3,4,5],[1,1,1,1]),[3,4,5,6]);
-    verify_f(Q_Add([2,3,4,5],[1,0,0,0]),[3,3,4,5]);
-    verify_f(Q_Add([2,3,4,5],[0,1,0,0]),[2,4,4,5]);
-    verify_f(Q_Add([2,3,4,5],[0,0,1,0]),[2,3,5,5]);
-    verify_f(Q_Add([2,3,4,5],[0,0,0,1]),[2,3,4,6]);
-    verify_f(Q_Add([2,3,4,5],[2,1,2,1]),[4,4,6,6]);
-    verify_f(Q_Add([2,3,4,5],[1,2,1,2]),[3,5,5,7]);
+module test_q_sub() {
+    assert_approx(q_sub([2,3,4,5],[-1,-1,-1,-1]),[3,4,5,6]);
+    assert_approx(q_sub([2,3,4,5],[-3,-3,-3,-3]),[5,6,7,8]);
+    assert_approx(q_sub([2,3,4,5],[0,0,0,0]),[2,3,4,5]);
+    assert_approx(q_sub([2,3,4,5],[1,1,1,1]),[1,2,3,4]);
+    assert_approx(q_sub([2,3,4,5],[1,0,0,0]),[1,3,4,5]);
+    assert_approx(q_sub([2,3,4,5],[0,1,0,0]),[2,2,4,5]);
+    assert_approx(q_sub([2,3,4,5],[0,0,1,0]),[2,3,3,5]);
+    assert_approx(q_sub([2,3,4,5],[0,0,0,1]),[2,3,4,4]);
+    assert_approx(q_sub([2,3,4,5],[2,1,2,1]),[0,2,2,4]);
+    assert_approx(q_sub([2,3,4,5],[1,2,1,2]),[1,1,3,3]);
 }
-test_Q_Add();
+test_q_sub();
 
 
-module test_Q_Sub() {
-    verify_f(Q_Sub([2,3,4,5],[-1,-1,-1,-1]),[3,4,5,6]);
-    verify_f(Q_Sub([2,3,4,5],[-3,-3,-3,-3]),[5,6,7,8]);
-    verify_f(Q_Sub([2,3,4,5],[0,0,0,0]),[2,3,4,5]);
-    verify_f(Q_Sub([2,3,4,5],[1,1,1,1]),[1,2,3,4]);
-    verify_f(Q_Sub([2,3,4,5],[1,0,0,0]),[1,3,4,5]);
-    verify_f(Q_Sub([2,3,4,5],[0,1,0,0]),[2,2,4,5]);
-    verify_f(Q_Sub([2,3,4,5],[0,0,1,0]),[2,3,3,5]);
-    verify_f(Q_Sub([2,3,4,5],[0,0,0,1]),[2,3,4,4]);
-    verify_f(Q_Sub([2,3,4,5],[2,1,2,1]),[0,2,2,4]);
-    verify_f(Q_Sub([2,3,4,5],[1,2,1,2]),[1,1,3,3]);
+module test_q_mul() {
+    assert_approx(q_mul(quat_z(30),quat_x(57)),[0.4608999698, 0.1234977747, 0.2274546059, 0.8488721457]);
+    assert_approx(q_mul(quat_y(30),quat_z(23)),[0.05160021841, 0.2536231763, 0.1925746368, 0.94653458]);
 }
-test_Q_Sub();
+test_q_mul();
 
 
-module test_Q_Mul() {
-    verify_f(Q_Mul(QuatZ(30),QuatX(57)),[0.4608999698, 0.1234977747, 0.2274546059, 0.8488721457]);
-    verify_f(Q_Mul(QuatY(30),QuatZ(23)),[0.05160021841, 0.2536231763, 0.1925746368, 0.94653458]);
+module test_q_cumulative() {
+    assert_approx(q_cumulative([quat_z(30),quat_x(57),quat_y(18)]),[[0, 0, 0.2588190451, 0.9659258263], [0.4608999698, -0.1234977747, 0.2274546059, 0.8488721457], [0.4908072659, 0.01081554785, 0.1525536221, 0.8577404293]]);
 }
-test_Q_Mul();
+test_q_cumulative();
 
 
-module test_Q_Cumulative() {
-    verify_f(Q_Cumulative([QuatZ(30),QuatX(57),QuatY(18)]),[[0, 0, 0.2588190451, 0.9659258263], [0.4608999698, -0.1234977747, 0.2274546059, 0.8488721457], [0.4908072659, 0.01081554785, 0.1525536221, 0.8577404293]]);
+module test_q_dot() {
+    assert_approx(q_dot(quat_z(30),quat_x(57)),0.8488721457);
+    assert_approx(q_dot(quat_y(30),quat_z(23)),0.94653458);
 }
-test_Q_Cumulative();
+test_q_dot();
 
 
-module test_Q_Dot() {
-    verify_f(Q_Dot(QuatZ(30),QuatX(57)),0.8488721457);
-    verify_f(Q_Dot(QuatY(30),QuatZ(23)),0.94653458);
+module test_q_neg() {
+    assert_approx(q_neg([1,0,0,1]),[-1,0,0,-1]);
+    assert_approx(q_neg([0,1,1,0]),[0,-1,-1,0]);
+    assert_approx(q_neg(quat_xyz([23,45,67])),[0.0533818345,-0.4143703268,-0.4360652669,-0.7970537592]);
 }
-test_Q_Dot();
+test_q_neg();
 
 
-module test_Q_Neg() {
-    verify_f(Q_Neg([1,0,0,1]),[-1,0,0,-1]);
-    verify_f(Q_Neg([0,1,1,0]),[0,-1,-1,0]);
-    verify_f(Q_Neg(QuatXYZ([23,45,67])),[0.0533818345,-0.4143703268,-0.4360652669,-0.7970537592]);
+module test_q_conj() {
+    assert_approx(q_conj([1,0,0,1]),[-1,0,0,1]);
+    assert_approx(q_conj([0,1,1,0]),[0,-1,-1,0]);
+    assert_approx(q_conj(quat_xyz([23,45,67])),[0.0533818345, -0.4143703268, -0.4360652669, 0.7970537592]);
 }
-test_Q_Neg();
+test_q_conj();
 
 
-module test_Q_Conj() {
-    verify_f(Q_Conj([1,0,0,1]),[-1,0,0,1]);
-    verify_f(Q_Conj([0,1,1,0]),[0,-1,-1,0]);
-    verify_f(Q_Conj(QuatXYZ([23,45,67])),[0.0533818345, -0.4143703268, -0.4360652669, 0.7970537592]);
+module test_q_inverse() {
+
+    assert_approx(q_inverse([1,0,0,1]),[-1,0,0,1]/sqrt(2));
+    assert_approx(q_inverse([0,1,1,0]),[0,-1,-1,0]/sqrt(2));
+    assert_approx(q_inverse(quat_xyz([23,45,67])),q_conj(quat_xyz([23,45,67])));
+    assert_approx(q_mul(q_inverse(quat_xyz([23,45,67])),quat_xyz([23,45,67])),q_ident());
 }
-test_Q_Conj();
+test_q_inverse();
 
 
-module test_Q_Inverse() {
-
-    verify_f(Q_Inverse([1,0,0,1]),[-1,0,0,1]/sqrt(2));
-    verify_f(Q_Inverse([0,1,1,0]),[0,-1,-1,0]/sqrt(2));
-    verify_f(Q_Inverse(QuatXYZ([23,45,67])),Q_Conj(QuatXYZ([23,45,67])));
-    verify_f(Q_Mul(Q_Inverse(QuatXYZ([23,45,67])),QuatXYZ([23,45,67])),Q_Ident());
+module test_q_Norm() {
+    assert_approx(q_norm([1,0,0,1]),1.414213562);
+    assert_approx(q_norm([0,1,1,0]),1.414213562);
+    assert_approx(q_norm(quat_xyz([23,45,67])),1);
 }
-test_Q_Inverse();
+test_q_Norm();
 
 
-module test_Q_Norm() {
-    verify_f(Q_Norm([1,0,0,1]),1.414213562);
-    verify_f(Q_Norm([0,1,1,0]),1.414213562);
-    verify_f(Q_Norm(QuatXYZ([23,45,67])),1);
+module test_q_normalize() {
+    assert_approx(q_normalize([1,0,0,1]),[0.7071067812, 0, 0, 0.7071067812]);
+    assert_approx(q_normalize([0,1,1,0]),[0, 0.7071067812, 0.7071067812, 0]);
+    assert_approx(q_normalize(quat_xyz([23,45,67])),[-0.0533818345, 0.4143703268, 0.4360652669, 0.7970537592]);
 }
-test_Q_Norm();
+test_q_normalize();
 
 
-module test_Q_Normalize() {
-    verify_f(Q_Normalize([1,0,0,1]),[0.7071067812, 0, 0, 0.7071067812]);
-    verify_f(Q_Normalize([0,1,1,0]),[0, 0.7071067812, 0.7071067812, 0]);
-    verify_f(Q_Normalize(QuatXYZ([23,45,67])),[-0.0533818345, 0.4143703268, 0.4360652669, 0.7970537592]);
+module test_q_dist() {
+    assert_approx(q_dist(quat_xyz([23,45,67]),quat_xyz([23,45,67])),0);
+    assert_approx(q_dist(quat_xyz([23,45,67]),quat_xyz([12,34,56])),0.1257349854);
 }
-test_Q_Normalize();
+test_q_dist();
 
 
-module test_Q_Dist() {
-    verify_f(Q_Dist(QuatXYZ([23,45,67]),QuatXYZ([23,45,67])),0);
-    verify_f(Q_Dist(QuatXYZ([23,45,67]),QuatXYZ([12,34,56])),0.1257349854);
+module test_q_slerp() {
+    assert_approx(q_slerp(quat_x(45),quat_y(30),0.0),quat_x(45));
+    assert_approx(q_slerp(quat_x(45),quat_y(30),0.5),[0.1967063121, 0.1330377423, 0, 0.9713946602]);
+    assert_approx(q_slerp(quat_x(45),quat_y(30),1.0),quat_y(30));
 }
-test_Q_Dist();
+test_q_slerp();
 
 
-module test_Q_Slerp() {
-    verify_f(Q_Slerp(QuatX(45),QuatY(30),0.0),QuatX(45));
-    verify_f(Q_Slerp(QuatX(45),QuatY(30),0.5),[0.1967063121, 0.1330377423, 0, 0.9713946602]);
-    verify_f(Q_Slerp(QuatX(45),QuatY(30),1.0),QuatY(30));
+module test_q_matrix3() {
+    assert_approx(q_matrix3(quat_z(37)),rot(37,planar=true));
+    assert_approx(q_matrix3(quat_z(-49)),rot(-49,planar=true));
 }
-test_Q_Slerp();
+test_q_matrix3();
 
 
-module test_Q_Matrix3() {
-    verify_f(Q_Matrix3(QuatZ(37)),rot(37,planar=true));
-    verify_f(Q_Matrix3(QuatZ(-49)),rot(-49,planar=true));
+module test_q_matrix4() {
+    assert_approx(q_matrix4(quat_z(37)),rot(37));
+    assert_approx(q_matrix4(quat_z(-49)),rot(-49));
+    assert_approx(q_matrix4(quat_x(37)),rot([37,0,0]));
+    assert_approx(q_matrix4(quat_y(37)),rot([0,37,0]));
+    assert_approx(q_matrix4(quat_xyz([12,34,56])),rot([12,34,56]));
 }
-test_Q_Matrix3();
+test_q_matrix4();
 
 
-module test_Q_Matrix4() {
-    verify_f(Q_Matrix4(QuatZ(37)),rot(37));
-    verify_f(Q_Matrix4(QuatZ(-49)),rot(-49));
-    verify_f(Q_Matrix4(QuatX(37)),rot([37,0,0]));
-    verify_f(Q_Matrix4(QuatY(37)),rot([0,37,0]));
-    verify_f(Q_Matrix4(QuatXYZ([12,34,56])),rot([12,34,56]));
+module test_q_axis() {
+    assert_approx(q_axis(quat_x(37)),RIGHT);
+    assert_approx(q_axis(quat_x(-37)),LEFT);
+    assert_approx(q_axis(quat_y(37)),BACK);
+    assert_approx(q_axis(quat_y(-37)),FWD);
+    assert_approx(q_axis(quat_z(37)),UP);
+    assert_approx(q_axis(quat_z(-37)),DOWN);
 }
-test_Q_Matrix4();
+test_q_axis();
 
 
-module test_Q_Axis() {
-    verify_f(Q_Axis(QuatX(37)),RIGHT);
-    verify_f(Q_Axis(QuatX(-37)),LEFT);
-    verify_f(Q_Axis(QuatY(37)),BACK);
-    verify_f(Q_Axis(QuatY(-37)),FWD);
-    verify_f(Q_Axis(QuatZ(37)),UP);
-    verify_f(Q_Axis(QuatZ(-37)),DOWN);
+module test_q_angle() {
+    assert_approx(q_angle(quat_x(0)),0);
+    assert_approx(q_angle(quat_y(0)),0);
+    assert_approx(q_angle(quat_z(0)),0);
+    assert_approx(q_angle(quat_x(37)),37);
+    assert_approx(q_angle(quat_x(-37)),37);
+    assert_approx(q_angle(quat_y(37)),37);
+    assert_approx(q_angle(quat_y(-37)),37);
+    assert_approx(q_angle(quat_z(37)),37);
+    assert_approx(q_angle(quat_z(-37)),37);
+
+    assert_approx(q_angle(quat_z(-37),quat_z(-37)), 0);
+    assert_approx(q_angle(quat_z( 37.123),quat_z(-37.123)), 74.246);
+    assert_approx(q_angle(quat_x( 37),quat_y(-37)), 51.86293283);
 }
-test_Q_Axis();
+test_q_angle();
 
 
-module test_Q_Angle() {
-    verify_f(Q_Angle(QuatX(0)),0);
-    verify_f(Q_Angle(QuatY(0)),0);
-    verify_f(Q_Angle(QuatZ(0)),0);
-    verify_f(Q_Angle(QuatX(37)),37);
-    verify_f(Q_Angle(QuatX(-37)),37);
-    verify_f(Q_Angle(QuatY(37)),37);
-    verify_f(Q_Angle(QuatY(-37)),37);
-    verify_f(Q_Angle(QuatZ(37)),37);
-    verify_f(Q_Angle(QuatZ(-37)),37);
-
-    verify_f(Q_Angle(QuatZ(-37),QuatZ(-37)), 0);
-    verify_f(Q_Angle(QuatZ( 37.123),QuatZ(-37.123)), 74.246);
-    verify_f(Q_Angle(QuatX( 37),QuatY(-37)), 51.86293283);
+module test_q_rot() {
+    assert_approx(q_rot(quat_xyz([12,34,56])),rot([12,34,56]));
+    assert_approx(q_rot(quat_xyz([12,34,56]),p=[2,3,4]),rot([12,34,56],p=[2,3,4]));
+    assert_approx(q_rot(quat_xyz([12,34,56]),p=[[2,3,4],[4,9,6]]),rot([12,34,56],p=[[2,3,4],[4,9,6]]));
 }
-test_Q_Angle();
+test_q_rot();
 
 
-module test_Qrot() {
-    verify_f(Qrot(QuatXYZ([12,34,56])),rot([12,34,56]));
-    verify_f(Qrot(QuatXYZ([12,34,56]),p=[2,3,4]),rot([12,34,56],p=[2,3,4]));
-    verify_f(Qrot(QuatXYZ([12,34,56]),p=[[2,3,4],[4,9,6]]),rot([12,34,56],p=[[2,3,4],[4,9,6]]));
+module test_q_rotation() {
+    assert_approx(_q_standard(q_rotation(q_matrix3(quat([12,34,56],33)))),_q_standard(quat([12,34,56],33)));
+    assert_approx(q_matrix3(q_rotation(q_matrix3(quat_xyz([12,34,56])))),
+             q_matrix3(quat_xyz([12,34,56])));
 }
-test_Qrot();
+test_q_rotation();
 
 
-module test_Q_Rotation() {
-    verify_f(Qstandard(Q_Rotation(Q_Matrix3(Quat([12,34,56],33)))),Qstandard(Quat([12,34,56],33)));
-    verify_f(Q_Matrix3(Q_Rotation(Q_Matrix3(QuatXYZ([12,34,56])))),
-             Q_Matrix3(QuatXYZ([12,34,56])));
-}
-test_Q_Rotation();
+module test_q_rotation_path() {
+    assert_approx(q_rotation_path(quat_x(135), 5, quat_y(13.5))[0] , q_matrix4(quat_x(135)));
+    assert_approx(q_rotation_path(quat_x(135), 11, quat_y(13.5))[11] , yrot(13.5));
+    assert_approx(q_rotation_path(quat_x(135), 16, quat_y(13.5))[8] , q_rotation_path(quat_x(135), 8, quat_y(13.5))[4]);
+    assert_approx(q_rotation_path(quat_x(135), 16, quat_y(13.5))[7] , 
+             q_rotation_path(quat_y(13.5),16, quat_x(135))[9]);
 
-
-module test_Q_Rotation_path() {
-    
-    verify_f(Q_Rotation_path(QuatX(135), 5, QuatY(13.5))[0] , Q_Matrix4(QuatX(135)));
-    verify_f(Q_Rotation_path(QuatX(135), 11, QuatY(13.5))[11] , yrot(13.5));
-    verify_f(Q_Rotation_path(QuatX(135), 16, QuatY(13.5))[8] , Q_Rotation_path(QuatX(135), 8, QuatY(13.5))[4]);
-    verify_f(Q_Rotation_path(QuatX(135), 16, QuatY(13.5))[7] , 
-             Q_Rotation_path(QuatY(13.5),16, QuatX(135))[9]);
-
-    verify_f(Q_Rotation_path(QuatX(11), 5)[0] , xrot(11));
-    verify_f(Q_Rotation_path(QuatX(11), 5)[4] , xrot(55));
+    assert_approx(q_rotation_path(quat_x(11), 5)[0] , xrot(11));
+    assert_approx(q_rotation_path(quat_x(11), 5)[4] , xrot(55));
 
 }
-test_Q_Rotation_path();
+test_q_rotation_path();
 
 
-module test_Q_Nlerp() {
-    verify_f(Q_Nlerp(QuatX(45),QuatY(30),0.0),QuatX(45));
-    verify_f(Q_Nlerp(QuatX(45),QuatY(30),0.5),[0.1967063121, 0.1330377423, 0, 0.9713946602]);
-    verify_f(Q_Rotation_path(QuatX(135), 16, QuatY(13.5))[8] , Q_Matrix4(Q_Nlerp(QuatX(135), QuatY(13.5),0.5)));
-    verify_f(Q_Nlerp(QuatX(45),QuatY(30),1.0),QuatY(30));
+module test_q_nlerp() {
+    assert_approx(q_nlerp(quat_x(45),quat_y(30),0.0),quat_x(45));
+    assert_approx(q_nlerp(quat_x(45),quat_y(30),0.5),[0.1967063121, 0.1330377423, 0, 0.9713946602]);
+    assert_approx(q_rotation_path(quat_x(135), 16, quat_y(13.5))[8] , q_matrix4(q_nlerp(quat_x(135), quat_y(13.5),0.5)));
+    assert_approx(q_nlerp(quat_x(45),quat_y(30),1.0),quat_y(30));
 }
-test_Q_Nlerp();
+test_q_nlerp();
 
 
-module test_Q_Squad() {
-    verify_f(Q_Squad(QuatX(45),QuatZ(30),QuatX(90),QuatY(30),0.0),QuatX(45));
-    verify_f(Q_Squad(QuatX(45),QuatZ(30),QuatX(90),QuatY(30),1.0),QuatY(30));
-    verify_f(Q_Squad(QuatX(0),QuatX(30),QuatX(90),QuatX(120),0.5),
-              Q_Slerp(QuatX(0),QuatX(120),0.5));
-    verify_f(Q_Squad(QuatY(0),QuatY(0),QuatX(120),QuatX(120),0.3),
-              Q_Slerp(QuatY(0),QuatX(120),0.3));
+module test_q_squad() {
+    assert_approx(q_squad(quat_x(45),quat_z(30),quat_x(90),quat_y(30),0.0),quat_x(45));
+    assert_approx(q_squad(quat_x(45),quat_z(30),quat_x(90),quat_y(30),1.0),quat_y(30));
+    assert_approx(q_squad(quat_x(0),quat_x(30),quat_x(90),quat_x(120),0.5),
+              q_slerp(quat_x(0),quat_x(120),0.5));
+    assert_approx(q_squad(quat_y(0),quat_y(0),quat_x(120),quat_x(120),0.3),
+              q_slerp(quat_y(0),quat_x(120),0.3));
 }
-test_Q_Squad();
+test_q_squad();
 
 
-module test_Q_exp() {
-   verify_f(Q_exp(Q_Ident()), exp(1)*Q_Ident()); 
-   verify_f(Q_exp([0,0,0,33.7]), exp(33.7)*Q_Ident());
-   verify_f(Q_exp(Q_ln(Q_Ident())), Q_Ident());
-   verify_f(Q_exp(Q_ln([1,2,3,0])), [1,2,3,0]);
-   verify_f(Q_exp(Q_ln(QuatXYZ([31,27,34]))), QuatXYZ([31,27,34]));
-   let(q=QuatXYZ([12,23,34])) 
-     verify_f(Q_exp(q+Q_Inverse(q)),Q_Mul(Q_exp(q),Q_exp(Q_Inverse(q))));
+module test_q_exp() {
+   assert_approx(q_exp(q_ident()), exp(1)*q_ident()); 
+   assert_approx(q_exp([0,0,0,33.7]), exp(33.7)*q_ident());
+   assert_approx(q_exp(q_ln(q_ident())), q_ident());
+   assert_approx(q_exp(q_ln([1,2,3,0])), [1,2,3,0]);
+   assert_approx(q_exp(q_ln(quat_xyz([31,27,34]))), quat_xyz([31,27,34]));
+   let(q=quat_xyz([12,23,34])) 
+     assert_approx(q_exp(q+q_inverse(q)),q_mul(q_exp(q),q_exp(q_inverse(q))));
 
 }
-test_Q_exp();
+test_q_exp();
 
 
-module test_Q_ln() {
-   verify_f(Q_ln([1,2,3,0]),  [24.0535117721, 48.1070235442, 72.1605353164, 1.31952866481]); 
-   verify_f(Q_ln(Q_Ident()), [0,0,0,0]); 
-   verify_f(Q_ln(5.5*Q_Ident()), [0,0,0,ln(5.5)]); 
-   verify_f(Q_ln(Q_exp(QuatXYZ([13,37,43]))), QuatXYZ([13,37,43]));
-   verify_f(Q_ln(QuatXYZ([12,23,34]))+Q_ln(Q_Inverse(QuatXYZ([12,23,34]))), [0,0,0,0]);
+module test_q_ln() {
+   assert_approx(q_ln([1,2,3,0]),  [24.0535117721, 48.1070235442, 72.1605353164, 1.31952866481]); 
+   assert_approx(q_ln(q_ident()), [0,0,0,0]); 
+   assert_approx(q_ln(5.5*q_ident()), [0,0,0,ln(5.5)]); 
+   assert_approx(q_ln(q_exp(quat_xyz([13,37,43]))), quat_xyz([13,37,43]));
+   assert_approx(q_ln(quat_xyz([12,23,34]))+q_ln(q_inverse(quat_xyz([12,23,34]))), [0,0,0,0]);
 } 
-test_Q_ln();
+test_q_ln();
 
 
-module test_Q_pow() {
-    q = Quat([1,2,3],77);
-    verify_f(Q_pow(q,1), q);
-    verify_f(Q_pow(q,0), Q_Ident());
-    verify_f(Q_pow(q,-1), Q_Inverse(q));
-    verify_f(Q_pow(q,2), Q_Mul(q,q));
-    verify_f(Q_pow(q,3), Q_Mul(q,Q_pow(q,2)));
-    verify_f(Q_Mul(Q_pow(q,0.456),Q_pow(q,0.544)), q);
-    verify_f(Q_Mul(Q_pow(q,0.335),Q_Mul(Q_pow(q,.552),Q_pow(q,.113))), q);
+module test_q_pow() {
+    q = quat([1,2,3],77);
+    assert_approx(q_pow(q,1), q);
+    assert_approx(q_pow(q,0), q_ident());
+    assert_approx(q_pow(q,-1), q_inverse(q));
+    assert_approx(q_pow(q,2), q_mul(q,q));
+    assert_approx(q_pow(q,3), q_mul(q,q_pow(q,2)));
+    assert_approx(q_mul(q_pow(q,0.456),q_pow(q,0.544)), q);
+    assert_approx(q_mul(q_pow(q,0.335),q_mul(q_pow(q,.552),q_pow(q,.113))), q);
 }
-test_Q_pow();
+test_q_pow();
 
 
 
diff --git a/tests/test_shapes.scad b/tests/test_shapes.scad
index d1e7570..ca904d7 100644
--- a/tests/test_shapes.scad
+++ b/tests/test_shapes.scad
@@ -9,17 +9,17 @@ module test_prismoid() {
     assert_approx(prismoid([100,80],[40,50],h=50,anchor=BOT), [[[20,25,50],[20,-25,50],[-20,-25,50],[-20,25,50],[50,40,0],[50,-40,0],[-50,-40,0],[-50,40,0]],[[0,1,2],[0,2,3],[0,4,5],[0,5,1],[1,5,6],[1,6,2],[2,6,7],[2,7,3],[3,7,4],[3,4,0],[4,7,6],[4,6,5]]]);
     assert_approx(prismoid([100,80],[40,50],h=50,anchor=TOP+RIGHT), [[[0,25,0],[0,-25,0],[-40,-25,0],[-40,25,0],[30,40,-50],[30,-40,-50],[-70,-40,-50],[-70,40,-50]],[[0,1,2],[0,2,3],[0,4,5],[0,5,1],[1,5,6],[1,6,2],[2,6,7],[2,7,3],[3,7,4],[3,4,0],[4,7,6],[4,6,5]]]);
     assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5]), [[[30,30,50],[30,-20,50],[-10,-20,50],[-10,30,50],[50,40,0],[50,-40,0],[-50,-40,0],[-50,40,0]],[[0,1,2],[0,2,3],[0,4,5],[0,5,1],[1,5,6],[1,6,2],[2,6,7],[2,7,3],[3,7,4],[3,4,0],[4,7,6],[4,6,5]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer=5), [[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-15,50],[25,-20,50],[-5,-20,50],[-10,-15,50],[-10,25,50],[-5,30,50],[25,30,50],[30,25,50]],[[4,1,0],[1,4,2],[2,4,3],[4,0,5],[14,5,6],[5,0,6],[14,6,7],[6,0,7],[0,1,8],[7,0,8],[1,2,9],[8,1,9],[14,8,9],[2,3,10],[9,2,10],[14,9,10],[14,10,11],[3,4,11],[10,3,11],[4,5,12],[14,11,12],[11,4,12],[5,14,13],[14,12,13],[12,5,13],[14,7,15],[7,8,15],[8,14,15]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=5),[[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-20,50],[-10,-20,50],[-10,30,50],[30,30,50]],[[4,1,0],[1,4,2],[2,4,3],[4,0,5],[11,5,6],[5,0,6],[0,11,7],[11,6,7],[6,0,7],[11,0,8],[0,1,8],[1,2,8],[3,4,9],[2,3,9],[8,2,9],[11,8,9],[4,5,10],[5,11,10],[11,9,10],[9,4,10]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer2=5), [[[50,-40,0],[-50,-40,0],[-50,40,0],[50,40,0],[30,-15,50],[25,-20,50],[-5,-20,50],[-10,-15,50],[-10,25,50],[-5,30,50],[25,30,50],[30,25,50]],[[2,1,0],[10,2,3],[2,0,3],[3,0,4],[10,4,5],[0,1,5],[4,0,5],[10,5,6],[5,1,6],[1,2,7],[6,1,7],[10,6,7],[10,7,8],[7,2,8],[2,10,9],[10,8,9],[8,2,9],[10,3,11],[3,4,11],[4,10,11]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=5,chamfer2=10), [[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-10,50],[20,-20,50],[0,-20,50],[-10,-10,50],[-10,20,50],[0,30,50],[20,30,50],[30,20,50]],[[4,1,0],[1,4,2],[2,4,3],[4,0,5],[14,5,6],[5,0,6],[14,6,7],[6,0,7],[0,1,8],[7,0,8],[1,2,9],[8,1,9],[14,8,9],[2,3,10],[9,2,10],[14,9,10],[14,10,11],[3,4,11],[10,3,11],[4,5,12],[14,11,12],[11,4,12],[5,14,13],[14,12,13],[12,5,13],[14,7,15],[7,8,15],[8,14,15]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding=5), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-15,50],[29.8296291314,-16.2940952255,50],[29.3301270189,-17.5,50],[28.5355339059,-18.5355339059,50],[27.5,-19.3301270189,50],[26.2940952255,-19.8296291314,50],[25,-20,50],[-5,-20,50],[-6.29409522551,-19.8296291314,50],[-7.5,-19.3301270189,50],[-8.53553390593,-18.5355339059,50],[-9.33012701892,-17.5,50],[-9.82962913145,-16.2940952255,50],[-10,-15,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[25,30,50],[26.2940952255,29.8296291314,50],[27.5,29.3301270189,50],[28.5355339059,28.5355339059,50],[29.3301270189,27.5,50],[29.8296291314,26.2940952255,50],[30,25,50]],[[16,1,0],[1,16,2],[2,16,3],[3,16,4],[4,16,5],[5,16,6],[6,16,7],[7,16,8],[8,16,9],[9,16,10],[10,16,11],[11,16,12],[12,16,13],[13,16,14],[14,16,15],[16,0,17],[17,0,18],[18,0,19],[19,0,20],[20,0,21],[21,0,22],[51,22,23],[22,0,23],[51,23,24],[23,0,24],[24,0,25],[25,0,26],[26,0,27],[0,1,28],[27,0,28],[1,2,29],[28,1,29],[51,28,29],[2,3,30],[29,2,30],[51,29,30],[3,4,31],[30,3,31],[51,30,31],[4,5,32],[31,4,32],[51,31,32],[5,6,33],[32,5,33],[51,32,33],[51,33,34],[6,7,34],[33,6,34],[51,34,35],[7,8,35],[34,7,35],[51,35,36],[8,9,36],[35,8,36],[51,36,37],[9,10,37],[36,9,37],[51,37,38],[10,11,38],[37,10,38],[51,38,39],[11,12,39],[38,11,39],[51,39,40],[12,13,40],[39,12,40],[51,40,41],[13,14,41],[40,13,41],[51,41,42],[14,15,42],[41,14,42],[51,42,43],[15,16,43],[42,15,43],[51,43,44],[16,17,44],[43,16,44],[17,18,45],[44,17,45],[51,44,45],[18,19,46],[45,18,46],[51,45,46],[19,20,47],[46,19,47],[51,46,47],[20,21,48],[47,20,48],[51,47,48],[21,22,49],[51,48,49],[48,21,49],[22,51,50],[51,49,50],[49,22,50],[51,24,52],[24,25,52],[28,51,52],[25,26,53],[52,25,53],[28,52,53],[26,27,54],[53,26,54],[28,53,54],[27,28,55],[28,54,55],[54,27,55]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-20,50],[-10,-20,50],[-10,30,50],[30,30,50]],[[16,1,0],[1,16,2],[2,16,3],[3,16,4],[4,16,5],[5,16,6],[6,16,7],[7,16,8],[8,16,9],[9,16,10],[10,16,11],[11,16,12],[12,16,13],[13,16,14],[14,16,15],[16,0,17],[17,0,18],[18,0,19],[19,0,20],[31,20,21],[20,0,21],[31,21,22],[21,0,22],[31,22,23],[22,0,23],[31,23,24],[23,0,24],[31,24,25],[24,0,25],[31,25,26],[25,0,26],[0,31,27],[31,26,27],[26,0,27],[31,0,28],[0,1,28],[1,2,28],[2,3,28],[3,4,28],[4,5,28],[5,6,28],[6,7,28],[13,14,29],[7,8,29],[28,7,29],[8,9,29],[9,10,29],[10,11,29],[11,12,29],[12,13,29],[31,28,29],[17,18,30],[18,19,30],[19,20,30],[20,31,30],[15,16,30],[14,15,30],[29,14,30],[16,17,30],[31,29,30]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding2=5), [[[50,-40,0],[-50,-40,0],[-50,40,0],[50,40,0],[30,-15,50],[29.8296291314,-16.2940952255,50],[29.3301270189,-17.5,50],[28.5355339059,-18.5355339059,50],[27.5,-19.3301270189,50],[26.2940952255,-19.8296291314,50],[25,-20,50],[-5,-20,50],[-6.29409522551,-19.8296291314,50],[-7.5,-19.3301270189,50],[-8.53553390593,-18.5355339059,50],[-9.33012701892,-17.5,50],[-9.82962913145,-16.2940952255,50],[-10,-15,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[25,30,50],[26.2940952255,29.8296291314,50],[27.5,29.3301270189,50],[28.5355339059,28.5355339059,50],[29.3301270189,27.5,50],[29.8296291314,26.2940952255,50],[30,25,50]],[[2,1,0],[2,0,3],[3,0,4],[28,4,5],[4,0,5],[28,5,6],[5,0,6],[28,6,7],[6,0,7],[28,7,8],[7,0,8],[28,8,9],[8,0,9],[28,9,10],[0,1,10],[9,0,10],[10,1,11],[28,10,11],[11,1,12],[28,11,12],[12,1,13],[28,12,13],[13,1,14],[28,13,14],[14,1,15],[28,14,15],[15,1,16],[28,15,16],[1,2,17],[16,1,17],[28,16,17],[28,17,18],[17,2,18],[28,18,19],[18,2,19],[28,19,20],[19,2,20],[28,20,21],[20,2,21],[28,21,22],[21,2,22],[22,2,23],[28,22,23],[2,3,24],[23,2,24],[28,23,24],[28,24,25],[24,3,25],[28,25,26],[25,3,26],[3,28,27],[28,26,27],[26,3,27],[28,3,29],[4,28,29],[4,29,30],[29,3,30],[3,4,31],[4,30,31],[30,3,31]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5,rounding2=10), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-10,50],[29.6592582629,-12.588190451,50],[28.6602540378,-15,50],[27.0710678119,-17.0710678119,50],[25,-18.6602540378,50],[22.588190451,-19.6592582629,50],[20,-20,50],[0,-20,50],[-2.58819045103,-19.6592582629,50],[-5,-18.6602540378,50],[-7.07106781187,-17.0710678119,50],[-8.66025403784,-15,50],[-9.65925826289,-12.588190451,50],[-10,-10,50],[-10,20,50],[-9.65925826289,22.588190451,50],[-8.66025403784,25,50],[-7.07106781187,27.0710678119,50],[-5,28.6602540378,50],[-2.58819045103,29.6592582629,50],[0,30,50],[20,30,50],[22.588190451,29.6592582629,50],[25,28.6602540378,50],[27.0710678119,27.0710678119,50],[28.6602540378,25,50],[29.6592582629,22.588190451,50],[30,20,50]],[[16,1,0],[1,16,2],[2,16,3],[3,16,4],[4,16,5],[5,16,6],[6,16,7],[7,16,8],[8,16,9],[9,16,10],[10,16,11],[11,16,12],[12,16,13],[13,16,14],[14,16,15],[16,0,17],[17,0,18],[18,0,19],[19,0,20],[20,0,21],[21,0,22],[51,22,23],[22,0,23],[51,23,24],[23,0,24],[24,0,25],[25,0,26],[26,0,27],[0,1,28],[27,0,28],[1,2,29],[28,1,29],[51,28,29],[2,3,30],[29,2,30],[51,29,30],[3,4,31],[30,3,31],[51,30,31],[4,5,32],[31,4,32],[51,31,32],[5,6,33],[32,5,33],[51,32,33],[51,33,34],[6,7,34],[33,6,34],[51,34,35],[7,8,35],[34,7,35],[51,35,36],[8,9,36],[35,8,36],[51,36,37],[9,10,37],[36,9,37],[51,37,38],[10,11,38],[37,10,38],[51,38,39],[11,12,39],[38,11,39],[51,39,40],[12,13,40],[39,12,40],[51,40,41],[13,14,41],[40,13,41],[51,41,42],[14,15,42],[41,14,42],[51,42,43],[15,16,43],[42,15,43],[51,43,44],[16,17,44],[43,16,44],[51,44,45],[17,18,45],[44,17,45],[51,45,46],[18,19,46],[45,18,46],[19,20,47],[46,19,47],[51,46,47],[20,21,48],[47,20,48],[51,47,48],[21,22,49],[51,48,49],[48,21,49],[22,51,50],[51,49,50],[49,22,50],[51,24,52],[24,25,52],[28,51,52],[25,26,53],[52,25,53],[28,52,53],[26,27,54],[53,26,54],[28,53,54],[27,28,55],[28,54,55],[54,27,55]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5,chamfer2=10), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-10,50],[20,-20,50],[0,-20,50],[-10,-10,50],[-10,20,50],[0,30,50],[20,30,50],[30,20,50]],[[0,16,9],[28,0,1],[0,9,1],[28,1,2],[1,9,2],[28,2,3],[2,9,3],[3,9,4],[4,9,5],[5,9,6],[6,9,7],[7,9,8],[9,16,10],[10,16,11],[11,16,12],[12,16,13],[13,16,14],[14,16,15],[16,0,17],[17,0,18],[18,0,19],[19,0,20],[20,0,21],[21,0,22],[22,0,23],[23,0,24],[24,0,25],[25,0,26],[0,28,27],[26,0,27],[28,3,29],[3,4,29],[4,5,29],[5,6,29],[6,7,29],[8,9,30],[7,8,30],[29,7,30],[9,10,30],[28,29,30],[28,30,31],[10,11,31],[30,10,31],[11,12,31],[12,13,31],[13,14,31],[28,31,32],[15,16,32],[14,15,32],[31,14,32],[16,17,32],[20,21,33],[28,32,33],[19,20,33],[17,18,33],[32,17,33],[18,19,33],[23,24,34],[28,33,34],[21,22,34],[33,21,34],[22,23,34],[26,27,35],[27,28,35],[25,26,35],[28,34,35],[24,25,35],[34,24,35]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer=[0,5,10,15]), [[[50,-25,0],[35,-40,0],[-40,-40,0],[-50,-30,0],[-50,35,0],[-45,40,0],[50,40,0],[30,-5,50],[15,-20,50],[0,-20,50],[-10,-10,50],[-10,25,50],[-5,30,50],[30,30,50]],[[4,1,0],[1,4,2],[2,4,3],[4,0,5],[0,13,6],[13,5,6],[5,0,6],[13,0,7],[0,1,7],[1,2,8],[7,1,8],[13,7,8],[13,8,9],[2,3,9],[8,2,9],[13,9,10],[3,4,10],[9,3,10],[4,5,11],[13,10,11],[10,4,11],[5,13,12],[13,11,12],[11,5,12]]]);
-    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=[15,10,5,0], rounding2=[0,5,10,15]),  [[[50,-40,0],[-45,-40,0],[-50,-35,0],[-50,30,0],[-40,40,0],[35,40,0],[50,25,0],[30,-5,50],[29.4888873943,-8.88228567654,50],[27.9903810568,-12.5,50],[25.6066017178,-15.6066017178,50],[22.5,-17.9903810568,50],[18.8822856765,-19.4888873943,50],[15,-20,50],[0,-20,50],[-2.58819045103,-19.6592582629,50],[-5,-18.6602540378,50],[-7.07106781187,-17.0710678119,50],[-8.66025403784,-15,50],[-9.65925826289,-12.588190451,50],[-10,-10,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[30,30,50]],[[3,1,0],[1,3,2],[3,0,4],[28,4,5],[4,0,5],[0,28,6],[28,5,6],[5,0,6],[28,0,7],[7,0,8],[28,7,8],[28,8,9],[8,0,9],[28,9,10],[9,0,10],[28,10,11],[10,0,11],[28,11,12],[11,0,12],[28,12,13],[0,1,13],[12,0,13],[28,13,14],[13,1,14],[28,14,15],[14,1,15],[28,15,16],[15,1,16],[28,16,17],[1,2,17],[16,1,17],[28,17,18],[17,2,18],[28,18,19],[18,2,19],[28,19,20],[2,3,20],[19,2,20],[28,20,21],[20,3,21],[21,3,22],[28,21,22],[22,3,23],[28,22,23],[3,4,24],[23,3,24],[28,23,24],[28,24,25],[24,4,25],[28,25,26],[25,4,26],[4,28,27],[28,26,27],[26,4,27]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer=5), [[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-15,50],[25,-20,50],[-5,-20,50],[-10,-15,50],[-10,25,50],[-5,30,50],[25,30,50],[30,25,50]],[[14,7,15],[7,8,15],[8,14,15],[5,14,13],[14,12,13],[12,5,13],[14,11,12],[11,4,12],[4,5,12],[14,10,11],[3,4,11],[10,3,11],[2,3,10],[9,2,10],[14,9,10],[14,8,9],[1,2,9],[8,1,9],[7,0,8],[0,1,8],[14,6,7],[6,0,7],[14,5,6],[5,0,6],[4,0,5],[2,4,3],[1,4,2],[4,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=5), [[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-20,50],[-10,-20,50],[-10,30,50],[30,30,50]],[[11,9,10],[9,4,10],[4,5,10],[5,11,10],[2,3,9],[8,2,9],[11,8,9],[3,4,9],[1,2,8],[11,0,8],[0,1,8],[0,11,7],[11,6,7],[6,0,7],[11,5,6],[5,0,6],[4,0,5],[2,4,3],[1,4,2],[4,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer2=5), [[[50,-40,0],[-50,-40,0],[-50,40,0],[50,40,0],[30,-15,50],[25,-20,50],[-5,-20,50],[-10,-15,50],[-10,25,50],[-5,30,50],[25,30,50],[30,25,50]],[[10,3,11],[3,4,11],[4,10,11],[2,10,9],[10,8,9],[8,2,9],[10,7,8],[7,2,8],[1,2,7],[6,1,7],[10,6,7],[10,5,6],[5,1,6],[10,4,5],[0,1,5],[4,0,5],[3,0,4],[10,2,3],[2,0,3],[2,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=5,chamfer2=10), [[[50,-35,0],[45,-40,0],[-45,-40,0],[-50,-35,0],[-50,35,0],[-45,40,0],[45,40,0],[50,35,0],[30,-10,50],[20,-20,50],[0,-20,50],[-10,-10,50],[-10,20,50],[0,30,50],[20,30,50],[30,20,50]],[[14,7,15],[7,8,15],[8,14,15],[5,14,13],[14,12,13],[12,5,13],[14,11,12],[11,4,12],[4,5,12],[14,10,11],[3,4,11],[10,3,11],[2,3,10],[9,2,10],[14,9,10],[14,8,9],[1,2,9],[8,1,9],[7,0,8],[0,1,8],[14,6,7],[6,0,7],[14,5,6],[5,0,6],[4,0,5],[2,4,3],[1,4,2],[4,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding=5), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-15,50],[29.8296291314,-16.2940952255,50],[29.3301270189,-17.5,50],[28.5355339059,-18.5355339059,50],[27.5,-19.3301270189,50],[26.2940952255,-19.8296291314,50],[25,-20,50],[-5,-20,50],[-6.29409522551,-19.8296291314,50],[-7.5,-19.3301270189,50],[-8.53553390593,-18.5355339059,50],[-9.33012701892,-17.5,50],[-9.82962913145,-16.2940952255,50],[-10,-15,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[25,30,50],[26.2940952255,29.8296291314,50],[27.5,29.3301270189,50],[28.5355339059,28.5355339059,50],[29.3301270189,27.5,50],[29.8296291314,26.2940952255,50],[30,25,50]],[[27,28,55],[28,54,55],[54,27,55],[28,53,54],[26,27,54],[53,26,54],[28,52,53],[25,26,53],[52,25,53],[28,51,52],[51,24,52],[24,25,52],[22,51,50],[51,49,50],[49,22,50],[51,48,49],[48,21,49],[21,22,49],[20,21,48],[47,20,48],[51,47,48],[19,20,47],[46,19,47],[51,46,47],[51,45,46],[18,19,46],[45,18,46],[51,44,45],[17,18,45],[44,17,45],[16,17,44],[43,16,44],[51,43,44],[15,16,43],[42,15,43],[51,42,43],[14,15,42],[41,14,42],[51,41,42],[13,14,41],[40,13,41],[51,40,41],[12,13,40],[39,12,40],[51,39,40],[11,12,39],[38,11,39],[51,38,39],[10,11,38],[37,10,38],[51,37,38],[9,10,37],[36,9,37],[51,36,37],[8,9,36],[35,8,36],[51,35,36],[7,8,35],[34,7,35],[51,34,35],[6,7,34],[33,6,34],[51,33,34],[51,32,33],[5,6,33],[32,5,33],[51,31,32],[4,5,32],[31,4,32],[51,30,31],[3,4,31],[30,3,31],[51,29,30],[2,3,30],[29,2,30],[51,28,29],[1,2,29],[28,1,29],[27,0,28],[0,1,28],[26,0,27],[25,0,26],[24,0,25],[51,23,24],[23,0,24],[51,22,23],[22,0,23],[21,0,22],[20,0,21],[19,0,20],[18,0,19],[17,0,18],[16,0,17],[14,16,15],[13,16,14],[12,16,13],[11,16,12],[10,16,11],[9,16,10],[8,16,9],[7,16,8],[6,16,7],[5,16,6],[4,16,5],[3,16,4],[2,16,3],[1,16,2],[16,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-20,50],[-10,-20,50],[-10,30,50],[30,30,50]],[[16,17,30],[31,29,30],[15,16,30],[14,15,30],[29,14,30],[19,20,30],[20,31,30],[18,19,30],[17,18,30],[12,13,29],[31,28,29],[11,12,29],[10,11,29],[9,10,29],[8,9,29],[7,8,29],[28,7,29],[13,14,29],[6,7,28],[5,6,28],[4,5,28],[3,4,28],[2,3,28],[1,2,28],[31,0,28],[0,1,28],[0,31,27],[31,26,27],[26,0,27],[31,25,26],[25,0,26],[31,24,25],[24,0,25],[31,23,24],[23,0,24],[31,22,23],[22,0,23],[31,21,22],[21,0,22],[31,20,21],[20,0,21],[19,0,20],[18,0,19],[17,0,18],[16,0,17],[14,16,15],[13,16,14],[12,16,13],[11,16,12],[10,16,11],[9,16,10],[8,16,9],[7,16,8],[6,16,7],[5,16,6],[4,16,5],[3,16,4],[2,16,3],[1,16,2],[16,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding2=5), [[[50,-40,0],[-50,-40,0],[-50,40,0],[50,40,0],[30,-15,50],[29.8296291314,-16.2940952255,50],[29.3301270189,-17.5,50],[28.5355339059,-18.5355339059,50],[27.5,-19.3301270189,50],[26.2940952255,-19.8296291314,50],[25,-20,50],[-5,-20,50],[-6.29409522551,-19.8296291314,50],[-7.5,-19.3301270189,50],[-8.53553390593,-18.5355339059,50],[-9.33012701892,-17.5,50],[-9.82962913145,-16.2940952255,50],[-10,-15,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[25,30,50],[26.2940952255,29.8296291314,50],[27.5,29.3301270189,50],[28.5355339059,28.5355339059,50],[29.3301270189,27.5,50],[29.8296291314,26.2940952255,50],[30,25,50]],[[3,4,31],[4,30,31],[30,3,31],[4,29,30],[29,3,30],[28,3,29],[4,28,29],[3,28,27],[28,26,27],[26,3,27],[28,25,26],[25,3,26],[28,24,25],[24,3,25],[2,3,24],[23,2,24],[28,23,24],[22,2,23],[28,22,23],[28,21,22],[21,2,22],[28,20,21],[20,2,21],[28,19,20],[19,2,20],[28,18,19],[18,2,19],[28,17,18],[17,2,18],[1,2,17],[16,1,17],[28,16,17],[15,1,16],[28,15,16],[14,1,15],[28,14,15],[13,1,14],[28,13,14],[12,1,13],[28,12,13],[11,1,12],[28,11,12],[10,1,11],[28,10,11],[0,1,10],[9,0,10],[28,9,10],[8,0,9],[28,8,9],[28,7,8],[7,0,8],[28,6,7],[6,0,7],[28,5,6],[5,0,6],[28,4,5],[4,0,5],[3,0,4],[2,0,3],[2,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5,rounding2=10), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-10,50],[29.6592582629,-12.588190451,50],[28.6602540378,-15,50],[27.0710678119,-17.0710678119,50],[25,-18.6602540378,50],[22.588190451,-19.6592582629,50],[20,-20,50],[0,-20,50],[-2.58819045103,-19.6592582629,50],[-5,-18.6602540378,50],[-7.07106781187,-17.0710678119,50],[-8.66025403784,-15,50],[-9.65925826289,-12.588190451,50],[-10,-10,50],[-10,20,50],[-9.65925826289,22.588190451,50],[-8.66025403784,25,50],[-7.07106781187,27.0710678119,50],[-5,28.6602540378,50],[-2.58819045103,29.6592582629,50],[0,30,50],[20,30,50],[22.588190451,29.6592582629,50],[25,28.6602540378,50],[27.0710678119,27.0710678119,50],[28.6602540378,25,50],[29.6592582629,22.588190451,50],[30,20,50]],[[27,28,55],[28,54,55],[54,27,55],[28,53,54],[26,27,54],[53,26,54],[28,52,53],[25,26,53],[52,25,53],[28,51,52],[51,24,52],[24,25,52],[22,51,50],[51,49,50],[49,22,50],[51,48,49],[48,21,49],[21,22,49],[20,21,48],[47,20,48],[51,47,48],[19,20,47],[46,19,47],[51,46,47],[18,19,46],[45,18,46],[51,45,46],[17,18,45],[44,17,45],[51,44,45],[16,17,44],[43,16,44],[51,43,44],[15,16,43],[42,15,43],[51,42,43],[14,15,42],[41,14,42],[51,41,42],[13,14,41],[40,13,41],[51,40,41],[12,13,40],[39,12,40],[51,39,40],[11,12,39],[38,11,39],[51,38,39],[10,11,38],[37,10,38],[51,37,38],[9,10,37],[36,9,37],[51,36,37],[8,9,36],[35,8,36],[51,35,36],[7,8,35],[34,7,35],[51,34,35],[6,7,34],[33,6,34],[51,33,34],[51,32,33],[5,6,33],[32,5,33],[51,31,32],[4,5,32],[31,4,32],[51,30,31],[3,4,31],[30,3,31],[51,29,30],[2,3,30],[29,2,30],[51,28,29],[1,2,29],[28,1,29],[27,0,28],[0,1,28],[26,0,27],[25,0,26],[24,0,25],[51,23,24],[23,0,24],[51,22,23],[22,0,23],[21,0,22],[20,0,21],[19,0,20],[18,0,19],[17,0,18],[16,0,17],[14,16,15],[13,16,14],[12,16,13],[11,16,12],[10,16,11],[9,16,10],[8,16,9],[7,16,8],[6,16,7],[5,16,6],[4,16,5],[3,16,4],[2,16,3],[1,16,2],[16,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],rounding1=5,chamfer2=10), [[[50,-35,0],[49.8296291314,-36.2940952255,0],[49.3301270189,-37.5,0],[48.5355339059,-38.5355339059,0],[47.5,-39.3301270189,0],[46.2940952255,-39.8296291314,0],[45,-40,0],[-45,-40,0],[-46.2940952255,-39.8296291314,0],[-47.5,-39.3301270189,0],[-48.5355339059,-38.5355339059,0],[-49.3301270189,-37.5,0],[-49.8296291314,-36.2940952255,0],[-50,-35,0],[-50,35,0],[-49.8296291314,36.2940952255,0],[-49.3301270189,37.5,0],[-48.5355339059,38.5355339059,0],[-47.5,39.3301270189,0],[-46.2940952255,39.8296291314,0],[-45,40,0],[45,40,0],[46.2940952255,39.8296291314,0],[47.5,39.3301270189,0],[48.5355339059,38.5355339059,0],[49.3301270189,37.5,0],[49.8296291314,36.2940952255,0],[50,35,0],[30,-10,50],[20,-20,50],[0,-20,50],[-10,-10,50],[-10,20,50],[0,30,50],[20,30,50],[30,20,50]],[[24,25,35],[34,24,35],[25,26,35],[28,34,35],[26,27,35],[27,28,35],[22,23,34],[21,22,34],[33,21,34],[28,33,34],[23,24,34],[18,19,33],[17,18,33],[32,17,33],[19,20,33],[28,32,33],[20,21,33],[16,17,32],[15,16,32],[14,15,32],[31,14,32],[28,31,32],[13,14,31],[12,13,31],[11,12,31],[10,11,31],[30,10,31],[28,30,31],[28,29,30],[9,10,30],[8,9,30],[7,8,30],[29,7,30],[6,7,29],[5,6,29],[4,5,29],[28,3,29],[3,4,29],[0,28,27],[26,0,27],[25,0,26],[24,0,25],[23,0,24],[22,0,23],[21,0,22],[20,0,21],[19,0,20],[18,0,19],[17,0,18],[16,0,17],[14,16,15],[13,16,14],[12,16,13],[11,16,12],[10,16,11],[9,16,10],[7,9,8],[6,9,7],[5,9,6],[4,9,5],[3,9,4],[28,2,3],[2,9,3],[28,1,2],[1,9,2],[28,0,1],[0,9,1],[0,16,9]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer=[0,5,10,15]), [[[50,-25,0],[35,-40,0],[-40,-40,0],[-50,-30,0],[-50,35,0],[-45,40,0],[50,40,0],[30,-5,50],[15,-20,50],[0,-20,50],[-10,-10,50],[-10,25,50],[-5,30,50],[30,30,50]],[[5,13,12],[13,11,12],[11,5,12],[13,10,11],[10,4,11],[4,5,11],[13,9,10],[3,4,10],[9,3,10],[2,3,9],[8,2,9],[13,8,9],[13,7,8],[1,2,8],[7,1,8],[13,0,7],[0,1,7],[0,13,6],[13,5,6],[5,0,6],[4,0,5],[2,4,3],[1,4,2],[4,1,0]]]);
+    assert_approx(prismoid([100,80],[40,50],h=50,shift=[10,5],chamfer1=[15,10,5,0], rounding2=[0,5,10,15]),  [[[50,-40,0],[-45,-40,0],[-50,-35,0],[-50,30,0],[-40,40,0],[35,40,0],[50,25,0],[30,-5,50],[29.4888873943,-8.88228567654,50],[27.9903810568,-12.5,50],[25.6066017178,-15.6066017178,50],[22.5,-17.9903810568,50],[18.8822856765,-19.4888873943,50],[15,-20,50],[0,-20,50],[-2.58819045103,-19.6592582629,50],[-5,-18.6602540378,50],[-7.07106781187,-17.0710678119,50],[-8.66025403784,-15,50],[-9.65925826289,-12.588190451,50],[-10,-10,50],[-10,25,50],[-9.82962913145,26.2940952255,50],[-9.33012701892,27.5,50],[-8.53553390593,28.5355339059,50],[-7.5,29.3301270189,50],[-6.29409522551,29.8296291314,50],[-5,30,50],[30,30,50]],[[4,28,27],[28,26,27],[26,4,27],[28,25,26],[25,4,26],[28,24,25],[24,4,25],[3,4,24],[23,3,24],[28,23,24],[22,3,23],[28,22,23],[21,3,22],[28,21,22],[28,20,21],[20,3,21],[28,19,20],[2,3,20],[19,2,20],[28,18,19],[18,2,19],[28,17,18],[17,2,18],[1,2,17],[16,1,17],[28,16,17],[15,1,16],[28,15,16],[14,1,15],[28,14,15],[13,1,14],[28,13,14],[0,1,13],[12,0,13],[28,12,13],[11,0,12],[28,11,12],[10,0,11],[28,10,11],[9,0,10],[28,9,10],[8,0,9],[28,8,9],[28,7,8],[7,0,8],[28,0,7],[0,28,6],[28,5,6],[5,0,6],[28,4,5],[4,0,5],[3,0,4],[1,3,2],[3,1,0]]]);
 }
 test_prismoid();
 
diff --git a/tests/test_transforms.scad b/tests/test_transforms.scad
index b19d50b..20ab59c 100644
--- a/tests/test_transforms.scad
+++ b/tests/test_transforms.scad
@@ -111,7 +111,7 @@ module test_scale() {
     for (val=vals) {
         assert_equal(scale(point2d(val)), [[val.x,0,0],[0,val.y,0],[0,0,1]]);
         assert_equal(scale(val), [[val.x,0,0,0],[0,val.y,0,0],[0,0,val.z,0],[0,0,0,1]]);
-        assert_equal(scale(val, p=[1,2,3]), vmul([1,2,3], val));
+        assert_equal(scale(val, p=[1,2,3]), v_mul([1,2,3], val));
         scale(val) nil();
     }
     assert_equal(scale(3), [[3,0,0,0],[0,3,0,0],[0,0,3,0],[0,0,0,1]]);
@@ -122,7 +122,7 @@ module test_scale() {
     assert_equal(scale([2,3], p=square(1)), square([2,3]));
     assert_equal(scale([2,2], cp=[0.5,0.5], p=square(1)), move([-0.5,-0.5], p=square([2,2])));
     assert_equal(scale([2,3,4], p=cb), cube([2,3,4]));
-    assert_equal(scale([-2,-3,-4], p=cb), [[for (p=cb[0]) vmul(p,[-2,-3,-4])], [for (f=cb[1]) reverse(f)]]);
+    assert_equal(scale([-2,-3,-4], p=cb), [[for (p=cb[0]) v_mul(p,[-2,-3,-4])], [for (f=cb[1]) reverse(f)]]);
     // Verify that module at least doesn't crash.
     scale(-5) scale(5) nil();
 }
@@ -289,7 +289,7 @@ module test_rot() {
         for (vec2 = vecs2d) {
             assert_equal(
                 rot(from=vec1, to=vec2, p=pts2d, planar=true),
-                apply(affine2d_zrot(vang(vec2)-vang(vec1)), pts2d),
+                apply(affine2d_zrot(v_theta(vec2)-v_theta(vec1)), pts2d),
                 info=str(
                     "from = ", vec1, ", ",
                     "to = ", vec2, ", ",
diff --git a/tests/test_vectors.scad b/tests/test_vectors.scad
index b23ccb0..f430524 100644
--- a/tests/test_vectors.scad
+++ b/tests/test_vectors.scad
@@ -32,66 +32,67 @@ module test_is_vector() {
 test_is_vector();
 
 
-module test_vfloor() {
-    assert_equal(vfloor([2.0, 3.14, 18.9, 7]), [2,3,18,7]);
-    assert_equal(vfloor([-2.0, -3.14, -18.9, -7]), [-2,-4,-19,-7]);
+module test_v_floor() {
+    assert_equal(v_floor([2.0, 3.14, 18.9, 7]), [2,3,18,7]);
+    assert_equal(v_floor([-2.0, -3.14, -18.9, -7]), [-2,-4,-19,-7]);
 }
-test_vfloor();
+test_v_floor();
 
 
-module test_vceil() {
-    assert_equal(vceil([2.0, 3.14, 18.9, 7]), [2,4,19,7]);
-    assert_equal(vceil([-2.0, -3.14, -18.9, -7]), [-2,-3,-18,-7]);
+module test_v_ceil() {
+    assert_equal(v_ceil([2.0, 3.14, 18.9, 7]), [2,4,19,7]);
+    assert_equal(v_ceil([-2.0, -3.14, -18.9, -7]), [-2,-3,-18,-7]);
 }
-test_vceil();
+test_v_ceil();
 
 
-module test_vmul() {
-    assert_equal(vmul([3,4,5], [8,7,6]), [24,28,30]);
-    assert_equal(vmul([1,2,3], [4,5,6]), [4,10,18]);
-    assert_equal(vmul([[1,2,3],[4,5,6],[7,8,9]], [[4,5,6],[3,2,1],[5,9,3]]), [32,28,134]);
+module test_v_mul() {
+    assert_equal(v_mul([3,4,5], [8,7,6]), [24,28,30]);
+    assert_equal(v_mul([1,2,3], [4,5,6]), [4,10,18]);
+    assert_equal(v_mul([[1,2,3],[4,5,6],[7,8,9]], [[4,5,6],[3,2,1],[5,9,3]]), [32,28,134]);
 }
-test_vmul();
+test_v_mul();
 
 
-module test_vdiv() {
-    assert(vdiv([24,28,30], [8,7,6]) == [3, 4, 5]);
+module test_v_div() {
+    assert(v_div([24,28,30], [8,7,6]) == [3, 4, 5]);
 }
-test_vdiv();
+test_v_div();
 
 
-module test_vabs() {
-    assert(vabs([2,4,8]) == [2,4,8]);
-    assert(vabs([-2,-4,-8]) == [2,4,8]);
-    assert(vabs([-2,4,8]) == [2,4,8]);
-    assert(vabs([2,-4,8]) == [2,4,8]);
-    assert(vabs([2,4,-8]) == [2,4,8]);
+module test_v_abs() {
+    assert(v_abs([2,4,8]) == [2,4,8]);
+    assert(v_abs([-2,-4,-8]) == [2,4,8]);
+    assert(v_abs([-2,4,8]) == [2,4,8]);
+    assert(v_abs([2,-4,8]) == [2,4,8]);
+    assert(v_abs([2,4,-8]) == [2,4,8]);
 }
-test_vabs();
+test_v_abs();
 
 include <../strings.scad>
-module test_vang() {
-    assert(vang([1,0])==0);
-    assert(vang([0,1])==90);
-    assert(vang([-1,0])==180);
-    assert(vang([0,-1])==-90);
-    assert(vang([1,1])==45);
-    assert(vang([-1,1])==135);
-    assert(vang([1,-1])==-45);
-    assert(vang([-1,-1])==-135);
-    assert(vang([0,0,1])==[0,90]);
-    assert(vang([0,1,1])==[90,45]);
-    assert(vang([0,1,-1])==[90,-45]);
-    assert(vang([1,0,0])==[0,0]);
-    assert(vang([0,1,0])==[90,0]);
-    assert(vang([0,-1,0])==[-90,0]);
-    assert(vang([-1,0,0])==[180,0]);
-    assert(vang([1,0,1])==[0,45]);
-    assert(vang([0,1,1])==[90,45]);
-    assert(vang([0,-1,1])==[-90,45]);
-    assert(approx(vang([1,1,1]),[45, 35.2643896828]));
+module test_v_theta() {
+    assert_approx(v_theta([0,0]), 0);
+    assert_approx(v_theta([1,0]), 0);
+    assert_approx(v_theta([0,1]), 90);
+    assert_approx(v_theta([-1,0]), 180);
+    assert_approx(v_theta([0,-1]), -90);
+    assert_approx(v_theta([1,1]), 45);
+    assert_approx(v_theta([-1,1]), 135);
+    assert_approx(v_theta([1,-1]), -45);
+    assert_approx(v_theta([-1,-1]), -135);
+    assert_approx(v_theta([0,0,1]), 0);
+    assert_approx(v_theta([0,1,1]), 90);
+    assert_approx(v_theta([0,1,-1]), 90);
+    assert_approx(v_theta([1,0,0]), 0);
+    assert_approx(v_theta([0,1,0]), 90);
+    assert_approx(v_theta([0,-1,0]), -90);
+    assert_approx(v_theta([-1,0,0]), 180);
+    assert_approx(v_theta([1,0,1]), 0);
+    assert_approx(v_theta([0,1,1]), 90);
+    assert_approx(v_theta([0,-1,1]), -90);
+    assert_approx(v_theta([1,1,1]), 45);
 }
-test_vang();
+test_v_theta();
 
 
 module test_unit() {
diff --git a/threading.scad b/threading.scad
index 50e1336..3a0ca7c 100644
--- a/threading.scad
+++ b/threading.scad
@@ -183,7 +183,7 @@ module trapezoidal_threaded_rod(
     higthr1 = ceil(higang1 / 360);
     higthr2 = ceil(higang2 / 360);
     pdepth = -min(subindex(profile,1));
-    dummy1 = assert(_r1>2*pdepth) assert(_r2>2*pdepth);
+    dummy1 = assert(_r1>pdepth) assert(_r2>pdepth);
     skew_mat = affine3d_skew(sxz=(_r2-_r1)/l);
     side_mat = affine3d_xrot(90) *
         affine3d_mirror([-1,1,0]) *
@@ -208,7 +208,7 @@ module trapezoidal_threaded_rod(
             prof = apply(side_mat, [
                 for (thread = [-threads/2:1:threads/2-1]) let(
                     tang = (thread/starts) * 360 + ang,
-                    hsc =
+                    hsc = internal? 1 :
                         abs(tang) > twist/2? 0 :
                         (higang1==0 && higang2==0)? 1 :
                         lookup(tang, hig_table),
@@ -225,7 +225,7 @@ module trapezoidal_threaded_rod(
     ];
     thread_vnfs = vnf_merge([
         for (i=[0:1:starts-1])
-            zrot(i*360/starts, p=vnf_vertex_array(thread_verts, reverse=left_handed)),
+            zrot(i*360/starts, p=vnf_vertex_array(thread_verts, reverse=left_handed, style="min_edge")),
         for (i=[0:1:starts-1]) let(
             rmat = zrot(i*360/starts),
             pts = deduplicate(list_head(thread_verts[0], len(prof3d)+1)),
@@ -447,11 +447,15 @@ module threaded_nut(
 
 // Module: npt_threaded_rod()
 // Description:
-//   Constructs a standard NPT pipe threading.
+//   Constructs a standard NPT pipe end threading. If `internal=true`, creates a mask for making
+//   internal pipe threads.  Tapers smaller upwards if `internal=false`.  Tapers smaller downwards
+//   if `internal=true`.  If `hollow=true` and `internal=false`, then the pipe threads will be
+//   hollowed out into a pipe with the apropriate internal diameter.
 // Arguments:
-//   d = Outer diameter of threaded rod.
-//   left_handed = if true, create left-handed threads.  Default = false
-//   bevel = if true, bevel the thread ends.  Default: false
+//   size = NPT standard pipe size in inches.  1/16", 1/8", 1/4", 3/8", 1/2", 3/4", 1", 1+1/4", 1+1/2", or 2".  Default: 1/2"
+//   left_handed = If true, create left-handed threads.  Default = false
+//   bevel = If true, bevel the thread ends.  Default: false
+//   hollow = If true, create a pipe with the correct internal diameter.
 //   internal = If true, make this a mask for making internal threads.
 //   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`
@@ -467,9 +471,16 @@ module npt_threaded_rod(
     size=1/2,
     left_handed=false,
     bevel=false,
+    hollow=false,
     internal=false,
     anchor, spin, orient
 ) {
+    assert(is_finite(size));
+    assert(is_bool(left_handed));
+    assert(is_bool(bevel));
+    assert(is_bool(hollow));
+    assert(is_bool(internal));
+    assert(!(internal&&hollow), "Cannot created a hollow internal threads mask.");
     info_table = [
         // Size    OD      len    TPI
         [ 1/16,  [ 0.3896, 0.308, 27  ]],
@@ -488,8 +499,10 @@ module npt_threaded_rod(
     l = 25.4 * info[0];
     d = 25.4 * info[1];
     pitch = 25.4 / info[2];
-    r1 = get_radius(d=d, dflt=0.84 * 25.4 / 2);
-    r2 = r1 - l/32;
+    rr = get_radius(d=d, dflt=0.84 * 25.4 / 2);
+    rr2 = rr - l/32;
+    r1 = internal? rr2 : rr;
+    r2 = internal? rr : rr2;
     depth = pitch * cos(30) * 5/8;
     profile = internal? [
         [-6/16, -depth/pitch],
@@ -506,20 +519,28 @@ module npt_threaded_rod(
         [ 6/16, -depth/pitch],
         [ 7/16, -depth/pitch*1.07]
     ];
-    trapezoidal_threaded_rod(
-        d1=2*r1, d2=2*r2, l=l,
-        pitch=pitch,
-        thread_depth=depth,
-        thread_angle=30,
-        profile=profile,
-        left_handed=left_handed,
-        bevel=bevel,
-        internal=internal,
-        higbee=r1*PI/2,
-        anchor=anchor,
-        spin=spin,
-        orient=orient
-    ) children();
+    attachable(anchor,spin,orient, l=l, r1=r1, r2=r2) {
+        difference() {
+            trapezoidal_threaded_rod(
+                d1=2*r1, d2=2*r2, l=l,
+                pitch=pitch,
+                thread_depth=depth,
+                thread_angle=30,
+                profile=profile,
+                left_handed=left_handed,
+                bevel=bevel,
+                internal=internal,
+                higbee=r1*PI/2,
+                anchor=anchor,
+                spin=spin,
+                orient=orient
+            );
+            if (hollow) {
+                cylinder(l=l+1, d=size*INCH, center=true);
+            } else nil();
+        }
+        children();
+    }
 }
 
 
diff --git a/transforms.scad b/transforms.scad
index e5372de..9d4a9a9 100644
--- a/transforms.scad
+++ b/transforms.scad
@@ -414,8 +414,8 @@ function rot(a=0, v, cp, from, to, reverse=false, planar=false, p, _m) =
                 assert(approx(point3d(from).z, 0), "'from' must be a 2D vector when 'planar' is true.")
                 assert(approx(point3d(to).z, 0), "'to' must be a 2D vector when 'planar' is true.")
                 affine2d_zrot(
-                    vang(point2d(to)) -
-                    vang(point2d(from))
+                    v_theta(to) -
+                    v_theta(from)
                 ),
             m2 = is_undef(cp)? m1 : (move(cp) * m1 * move(-cp)),
             m3 = reverse? matrix_inverse(m2) : m2
@@ -946,8 +946,8 @@ function yscale(y=1, p, cp=0, planar=false) =
     assert(is_bool(planar))
     let( cp = is_num(cp)? [0,cp,0] : cp )
     (planar || (!is_undef(p) && len(p)==2))
-      ? scale([1,y],p=p)
-      : scale([1,y,1],p=p);
+      ? scale([1,y], cp=cp, p=p)
+      : scale([1,y,1], cp=cp, p=p);
 
 
 // Function&Module: zscale()
diff --git a/turtle3d.scad b/turtle3d.scad
index 7f1e4c5..200c6e2 100644
--- a/turtle3d.scad
+++ b/turtle3d.scad
@@ -90,7 +90,7 @@ function _rotpart(T) = [for(i=[0:3]) [for(j=[0:3]) j<3 || i==3 ? T[i][j] : 0]];
 //   "xjump"    |  | x                  | Move the turtle's x position to the specified value
 //   "yjump     |  | y                  | Move the turtle's y position to the specified value
 //   "zjump     |  | y                  | Move the turtle's y position to the specified value
-//   "left"        | [angle]            | Turn turtle left by specified angle or default angle
+//   "left"     |  | [angle]            | Turn turtle left by specified angle or default angle
 //   "right"    |  | [angle]            | Turn turtle to the right by specified angle or default angle
 //   "up"       |  | [angle]            | Turn turtle up by specified angle or default angle
 //   "down"     |  | [angle]            | Turn turtle down by specified angle or default angle
@@ -693,7 +693,7 @@ function _turtle3d_list_command(command,arcsteps,movescale, lastT,lastPre,index)
    assert(is_vector(grow,2), str("Parameter to \"grow\" must be a scalar or 2d vector at index ",index))
    assert(is_vector(shrink,2), str("Parameter to \"shrink\" must be a scalar or 2d vector at index ",index))
    let(
-       scaling = point3d(vdiv(grow,shrink),1),
+       scaling = point3d(v_div(grow,shrink),1),
        usersteps = struct_val(keys,"steps"),
        roll = struct_val(keys,"roll"),
        ////////////////////////////////////////////////////////////////////////////////////////
diff --git a/tutorials/Attachments.md b/tutorials/Attachments.md
index b17b70a..dc94bbd 100644
--- a/tutorials/Attachments.md
+++ b/tutorials/Attachments.md
@@ -91,6 +91,23 @@ overrides the `anchor=` argument.  A `center=true` argument is the same as `anch
 A `center=false` argument can mean `anchor=[-1,-1,-1]` for a cube, or `anchor=BOTTOM` for a
 cylinder.
 
+Many 2D shapes provided by BOSL2 are also anchorable.  Due to technical limitations of OpenSCAD,
+however, `square()` and `circle()` are *not*.  BOSL2 provides `rect()` and `oval()` as attachable
+and anchorable equivalents.  You can only anchor on the XY plane, of course, but you can use the
+same `FRONT`, `BACK`, `LEFT`, `RIGHT`, and `CENTER` anchor constants.
+
+```openscad-2D
+rect([40,30], anchor=BACK+LEFT);
+```
+
+```openscad-2D
+oval(d=50, anchor=FRONT);
+```
+
+```openscad-2D
+hexagon(d=50, anchor=BACK);
+```
+
 
 ## Spin
 Attachable shapes also can be spun in place as you create them.  You can do this by passing in
@@ -107,6 +124,17 @@ vector, like [Xang,Yang,Zang]:
 cube([20,20,40], center=true, spin=[10,20,30]);
 ```
 
+You can also apply spin to 2D shapes from BOSL2.  Again, you should use `rect()` and `oval()`
+instead of `square()` and `circle()`:
+
+```openscad-2D
+rect([40,30], spin=30);
+```
+
+```openscad-2D
+oval(d=[40,30], spin=30);
+```
+
 
 ## Orientation
 Another way to specify a rotation for an attachable shape, is to pass a 3D vector via the
@@ -117,6 +145,9 @@ For example, you can make a cone that is tilted up and to the right like this:
 cylinder(h=100, r1=50, r2=20, orient=UP+RIGHT);
 ```
 
+You can *not* use `orient=` with 2D shapes.
+
+
 ## Mixing Anchoring, Spin, and Orientation
 When giving `anchor=`, `spin=`, and `orient=`, they are applied anchoring first, spin second,
 then orient last.  For example, here's a cube:
@@ -150,8 +181,14 @@ However, since spin is applied *after* anchoring, it can actually have a signifi
 cylinder(d=50, l=40, anchor=FWD, spin=-30);
 ```
 
+For 2D shapes, you can mix `anchor=` with `spin=`, but not with `orient=`.
 
-## Attaching Children
+```openscad-2D
+rect([40,30], anchor=BACK+LEFT, spin=30);
+```
+
+
+## Attaching 3D Children
 The reason attachables are called that, is because they can be attached to each other.
 You can do that by making one attachable shape be a child of another attachable shape.
 By default, the child of an attachable is attached to the center of the parent shape.
@@ -178,13 +215,21 @@ cube(50,center=true)
     attach(TOP,TOP) cylinder(d1=50,d2=20,l=20);
 ```
 
-By default, `attach()` causes the child to overlap the parent by 0.01, to let CGAL correctly
-join the parts.  If you need the child to have no overlap, or a different overlap, you can use
-the `overlap=` argument:
+By default, `attach()` places the child exactly flush with the surface of the parent.  Sometimes
+it's useful to have the child overlap the parent by insetting a bit.  You can do this with the
+`overlap=` argument to `attach()`.  A positive value will inset the child into the parent, and
+a negative value will outset out from the parent:
 
 ```openscad
 cube(50,center=true)
-    attach(TOP,TOP,overlap=0) cylinder(d1=50,d2=20,l=20);
+    attach(TOP,overlap=10)
+        cylinder(d=20,l=20);
+```
+
+```openscad
+cube(50,center=true)
+    attach(TOP,overlap=-20)
+        cylinder(d=20,l=20);
 ```
 
 If you want to position the child at the parent's anchorpoint, without re-orienting, you can
@@ -217,6 +262,23 @@ cube(50, center=true)
     position([TOP,RIGHT,FRONT]) cylinder(d1=50,d2=20,l=20);
 ```
 
+## Attaching 2D Children
+You can use attachments in 2D as well, but only in the XY plane.  Also, the built-in `square()`
+and `circle()` 2D modules do not support attachments.  Instead, you should use the `rect()` and
+`oval()` modules:
+
+```openscad-2D
+rect(50,center=true)
+    attach(RIGHT,FRONT)
+        trapezoid(w1=30,w2=0,h=30);
+```
+
+```openscad-2D
+oval(d=50)
+    attach(BACK,FRONT,overlap=5)
+        trapezoid(w1=30,w2=0,h=30);
+```
+
 ## Anchor Arrows
 One way that is useful to show the position and orientation of an anchorpoint is by attaching
 an anchor arrow to that anchor.
@@ -259,6 +321,7 @@ cylinder(h=100, d=100, center=true)
     show_anchors(s=30);
 ```
 
+
 ## Tagged Operations
 BOSL2 introduces the concept of tags.  Tags are names that can be given to attachables, so that
 you can refer to them when performing `diff()`, `intersect()`, and `hulling()` operations.
@@ -372,12 +435,171 @@ cube(50, center=true, $tags="hull") {
 ```
 
 
-## Masking Children
-TBW
+## 3D Masking Attachments
+To make it easier to mask away shapes from various edges of an attachable parent shape, there
+are a few specialized alternatives to the `attach()` and `position()` modules.
+
+### `edge_mask()`
+If you have a 3D mask shape that you want to difference away from various edges, you can use
+the `edge_mask()` module.  This module will take a vertically oriented shape, and will rotate
+and move it such that the BACK, RIGHT (X+,Y+) side of the shape will be aligned with the given
+edges.  The shape will be tagged as a "mask" so that you can use `diff("mask")`.  For example,
+here's a shape for rounding an edge:
+
+```openscad
+module round_edge(l,r) difference() {
+    translate([-1,-1,-l/2])
+        cube([r+1,r+1,l]);
+    translate([r,r])
+        cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
+}
+round_edge(l=30, r=19);
+```
+
+You can use that mask to round various edges of a cube:
+
+```openscad
+module round_edge(l,r) difference() {
+    translate([-1,-1,-l/2])
+        cube([r+1,r+1,l]);
+    translate([r,r])
+        cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
+}
+diff("mask")
+cube([50,60,70],center=true)
+    edge_mask([TOP,"Z"],except=[BACK,TOP+LEFT])
+        round_edge(l=71,r=10);
+```
+
+### `corner_mask()`
+If you have a 3D mask shape that you want to difference away from various corners, you can use
+the `corner_mask()` module.  This module will take a shape and rotate and move it such that the
+BACK RIGHT TOP (X+,Y+,Z+) side of the shape will be aligned with the given corner.  The shape
+will be tagged as a "mask" so that you can use `diff("mask")`.  For example, here's a shape for
+rounding a corner:
+
+```openscad
+module round_corner(r) difference() {
+    translate(-[1,1,1])
+    	cube(r+1);
+    translate([r,r,r])
+    	sphere(r=r, style="aligned", $fn=quantup(segs(r),4));
+}
+round_corner(r=10);
+```
+
+You can use that mask to round various corners of a cube:
+
+```openscad
+module round_corner(r) difference() {
+    translate(-[1,1,1])
+    	cube(r+1);
+    translate([r,r,r])
+    	sphere(r=r, style="aligned", $fn=quantup(segs(r),4));
+}
+diff("mask")
+cube([50,60,70],center=true)
+    corner_mask([TOP,FRONT],LEFT+FRONT+TOP)
+        round_corner(r=10);
+```
+
+### Mix and Match Masks
+You can use `edge_mask()` and `corner_mask()` together as well:
+
+```openscad
+module round_corner(r) difference() {
+    translate(-[1,1,1])
+    	cube(r+1);
+    translate([r,r,r])
+    	sphere(r=r, style="aligned", $fn=quantup(segs(r),4));
+}
+module round_edge(l,r) difference() {
+    translate([-1,-1,-l/2])
+        cube([r+1,r+1,l]);
+    translate([r,r])
+        cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
+}
+diff("mask")
+cube([50,60,70],center=true) {
+    edge_mask("ALL") round_edge(l=71,r=10);
+    corner_mask("ALL") round_corner(r=10);
+}
+```
+
+## 2D Profile Mask Attachments
+While 3D mask shapes give you a great deal of control, you need to make sure they are correctly
+sized, and you need to provide separate mask shapes for corners and edges.  Often, a single 2D
+profile could be used to describe the edge mask shape (via `linear_extrude()`), and the corner
+mask shape (via `rotate_extrude()`).  This is where `edge_profile()`, `corner_profile()`, and
+`face_profile()` come in.
+
+### `edge_profile()`
+Using the `edge_profile()` module, you can provide a 2D profile shape and it will be linearly
+extruded to a mask of the apropriate length for each given edge.  The resultant mask will be
+tagged with "mask" so that you can difference it away with `diff("mask")`.  The 2D profile is
+assumed to be oriented with the BACK, RIGHT (X+,Y+) quadrant as the "cutter edge" that gets
+re-oriented towards the edges of the parent shape.  A typical mask profile for chamfering an
+edge may look like:
+
+```openscad-2D
+mask2d_roundover(10);
+```
+
+Using that mask profile, you can mask the edges of a cube like:
+
+```openscad
+diff("mask")
+cube([50,60,70],center=true)
+   edge_profile("ALL")
+       mask2d_roundover(10);
+```
+
+### `corner_profile()`
+You can use the same profile to make a rounded corner mask as well:
+
+```openscad
+diff("mask")
+cube([50,60,70],center=true)
+   corner_profile("ALL", r=10)
+       mask2d_roundover(10);
+```
+
+### `face_profile()`
+As a simple shortcut to apply a profile mask to all edges and corners of a face, you can use the
+`face_profile()` module:
+
+```openscad
+diff("mask")
+cube([50,60,70],center=true)
+   face_profile(TOP, r=10)
+       mask2d_roundover(10);
+```
 
 
 ## Coloring Attachables
-TBW
+Usually, when coloring a shape with the `color()` module, the parent color overrides the colors of
+all children.  This is often not what you want:
+
+```openscad
+$fn = 24;
+color("red") spheroid(d=3) {
+    attach(CENTER,BOT) color("white") cyl(h=10, d=1) {
+        attach(TOP,BOT) color("green") cyl(h=5, d1=3, d2=0);
+    }
+}
+```
+
+If you use the `recolor()` module, however, the child's color overrides the color of the parent.
+This is probably easier to understand by example:
+
+```openscad
+$fn = 24;
+recolor("red") spheroid(d=3) {
+    attach(CENTER,BOT) recolor("white") cyl(h=10, d=1) {
+        attach(TOP,BOT) recolor("green") cyl(h=5, d1=3, d2=0);
+    }
+}
+```
 
 
 ## Making Attachables
@@ -683,6 +905,74 @@ stellate_cube() show_anchors(50);
 
 
 ## Making Named Anchors
-TBW
+While vector anchors are often useful, sometimes there are logically extra attachment points that
+aren't on the perimeter of the shape.  This is what named string anchors are for.  For example,
+the `teardrop()` shape uses a cylindrical geometry for it's vector anchors, but it also provides
+a named anchor "cap" that is at the tip of the hat of the teardrop shape.
+
+Named anchors are passed as an array of `anchorpt()`s to the `anchors=` argument of `attachable()`.
+The `anchorpt()` call takes a name string, a positional point, an orientation vector, and a spin.
+The name is the name of the anchor.  The positional point is where the anchorpoint is at.  The
+orientation vector is the direction that a child attached at that anchorpoint should be oriented.
+The spin is the number of degrees that an attached child should be rotated counter-clockwise around
+the orientation vector.  Spin is optional, and defaults to 0.
+
+To make a simple attachable shape similar to a `teardrop()` that provides a "cap" anchor, you may
+define it like this:
+
+```openscad
+module raindrop(r, thick, anchor=CENTER, spin=0, orient=UP) {
+    anchors = [
+        anchorpt("cap", [0,r/sin(45),0], BACK, 0)
+    ];
+    attachable(anchor,spin,orient, r=r, l=thick, anchors=anchors) {
+        linear_extrude(height=thick, center=true) {
+            circle(r=r);
+            back(r*sin(45)) zrot(45) square(r, center=true);
+        }
+        children();
+    }
+}
+raindrop(r=25, thick=20, anchor="cap");
+```
+
+If you want multiple named anchors, just add them to the list of anchors:
+
+```openscad-FlatSpin,VPD=150
+module raindrop(r, thick, anchor=CENTER, spin=0, orient=UP) {
+    anchors = [
+        anchorpt("captop", [0,r/sin(45), thick/2], BACK+UP,   0),
+        anchorpt("cap",    [0,r/sin(45), 0      ], BACK,      0),
+        anchorpt("capbot", [0,r/sin(45),-thick/2], BACK+DOWN, 0)
+    ];
+    attachable(anchor,spin,orient, r=r, l=thick, anchors=anchors) {
+        linear_extrude(height=thick, center=true) {
+            circle(r=r);
+            back(r*sin(45)) zrot(45) square(r, center=true);
+        }
+        children();
+    }
+}
+raindrop(r=15, thick=10) show_anchors();
+```
+
+Sometimes the named anchor you want to add may be at a point that is reached through a complicated
+set of translations and rotations.  One quick way to calculate that point is to reproduce those
+transformations in a transformation matrix chain.  This is simplified by how you can use the
+function forms of almost all the transformation modules to get the transformation matrices, and
+chain them together with matrix multiplication.  For example, if you have:
+
+```
+scale([1.1, 1.2, 1.3]) xrot(15) zrot(25) right(20) sphere(d=1);
+```
+
+and you want to calculate the centerpoint of the sphere, you can do it like:
+
+```
+sphere_pt = apply(
+    scale([1.1, 1.2, 1.3]) * xrot(15) * zrot(25) * right(20),
+    [0,0,0]
+);
+```
 
 
diff --git a/vectors.scad b/vectors.scad
index a98594a..fcdacdf 100644
--- a/vectors.scad
+++ b/vectors.scad
@@ -11,7 +11,7 @@
 
 // Function: is_vector()
 // Usage:
-//   is_vector(v, [length]);
+//   is_vector(v, <length>, ...);
 // Description:
 //   Returns true if v is a list of finite numbers.
 // Arguments:
@@ -42,20 +42,17 @@ function is_vector(v, length, zero, all_nonzero=false, eps=EPSILON) =
     && (!all_nonzero || all_nonzero(v)) ;
 
 
-// Function: vang()
+// Function: v_theta()
 // Usage:
-//   theta = vang([X,Y]);
-//   theta_phi = vang([X,Y,Z]);
+//   theta = v_theta([X,Y]);
 // Description:
-//   Given a 2D vector, returns the angle in degrees counter-clockwise from X+ on the XY plane.
-//   Given a 3D vector, returns [THETA,PHI] where THETA is the number of degrees counter-clockwise from X+ on the XY plane, and PHI is the number of degrees up from the X+ axis along the XZ plane.
-function vang(v) =
+//   Given a vector, returns the angle in degrees counter-clockwise from X+ on the XY plane.
+function v_theta(v) =
     assert( is_vector(v,2) || is_vector(v,3) , "Invalid vector")
-    len(v)==2? atan2(v.y,v.x) :
-    let(res=xyz_to_spherical(v)) [res[1], 90-res[2]];
+    atan2(v.y,v.x);
 
 
-// Function: vmul()
+// Function: v_mul()
 // Description:
 //   Element-wise multiplication.  Multiplies each element of `v1` by the corresponding element of `v2`.
 //   Both `v1` and `v2` must be the same length.  Returns a vector of the products.
@@ -63,13 +60,13 @@ function vang(v) =
 //   v1 = The first vector.
 //   v2 = The second vector.
 // Example:
-//   vmul([3,4,5], [8,7,6]);  // Returns [24, 28, 30]
-function vmul(v1, v2) = 
+//   v_mul([3,4,5], [8,7,6]);  // Returns [24, 28, 30]
+function v_mul(v1, v2) = 
     assert( is_list(v1) && is_list(v2) && len(v1)==len(v2), "Incompatible input")
     [for (i = [0:1:len(v1)-1]) v1[i]*v2[i]];
     
 
-// Function: vdiv()
+// Function: v_div()
 // Description:
 //   Element-wise vector division.  Divides each element of vector `v1` by
 //   the corresponding element of vector `v2`.  Returns a vector of the quotients.
@@ -77,35 +74,35 @@ function vmul(v1, v2) =
 //   v1 = The first vector.
 //   v2 = The second vector.
 // Example:
-//   vdiv([24,28,30], [8,7,6]);  // Returns [3, 4, 5]
-function vdiv(v1, v2) = 
+//   v_div([24,28,30], [8,7,6]);  // Returns [3, 4, 5]
+function v_div(v1, v2) = 
     assert( is_vector(v1) && is_vector(v2,len(v1)), "Incompatible vectors")
     [for (i = [0:1:len(v1)-1]) v1[i]/v2[i]];
 
 
-// Function: vabs()
+// Function: v_abs()
 // Description: Returns a vector of the absolute value of each element of vector `v`.
 // Arguments:
 //   v = The vector to get the absolute values of.
 // Example:
-//   vabs([-1,3,-9]);  // Returns: [1,3,9]
-function vabs(v) =
+//   v_abs([-1,3,-9]);  // Returns: [1,3,9]
+function v_abs(v) =
     assert( is_vector(v), "Invalid vector" ) 
     [for (x=v) abs(x)];
 
 
-// Function: vfloor()
+// Function: v_floor()
 // Description:
 //   Returns the given vector after performing a `floor()` on all items.
-function vfloor(v) =
+function v_floor(v) =
     assert( is_vector(v), "Invalid vector" ) 
     [for (x=v) floor(x)];
 
 
-// Function: vceil()
+// Function: v_ceil()
 // Description:
 //   Returns the given vector after performing a `ceil()` on all items.
-function vceil(v) =
+function v_ceil(v) =
     assert( is_vector(v), "Invalid vector" ) 
     [for (x=v) ceil(x)];
 
@@ -213,7 +210,7 @@ function vector_axis(v1,v2=undef,v3=undef) =
               w1 = point3d(v1/norm(v1)),
               w2 = point3d(v2/norm(v2)),
               w3 = (norm(w1-w2) > eps && norm(w1+w2) > eps) ? w2 
-                   : (norm(vabs(w2)-UP) > eps)? UP 
+                   : (norm(v_abs(w2)-UP) > eps)? UP 
                    : RIGHT
             ) unit(cross(w1,w3));
 
@@ -291,6 +288,18 @@ function _vp_tree(ptlist, ind, leafsize) =
 //   tree = vantage point tree from vp_tree
 //   p = point to search for
 //   r = search radius
+// Example: A set of four queries to find points within 1 unit of the query.  The circles show the search region and all have radius 1.  
+//   $fn=32;
+//   k = 2000;
+//   points = array_group(rands(0,10,k*2,seed=13333),2);
+//   vp = vp_tree(points);
+//   queries = [for(i=[3,7],j=[3,7]) [i,j]];
+//   search_ind = [for(q=queries) vp_search(points, vp, q, 1)];
+//   move_copies(points) circle(r=.08);
+//   for(i=idx(queries)){
+//     color("blue")stroke(move(queries[i],circle(r=1)), closed=true, width=.08);
+//     color("red")move_copies(select(points, search_ind[i])) circle(r=.08);
+//   }
 function _vp_search(points, tree, p, r) =
     is_list(tree[0]) ? [for(i=tree[0]) if (norm(points[i]-p)<=r) i]
     :
@@ -324,6 +333,20 @@ function vp_search(points, tree, p, r) =
 //    tree = vantage point tree from vp_tree
 //    p = point to search for
 //    k = number of neighbors to return
+// Example:  Four queries to find the 15 nearest points.  The circles show the radius defined by the most distant query result.  Note they are different for each query.  
+//    $fn=32;
+//    k = 2000;
+//    points = array_group(rands(0,10,k*2,seed=13333),2);
+//    vp = vp_tree(points);
+//    queries = [for(i=[3,7],j=[3,7]) [i,j]];
+//    search_ind = [for(q=queries) vp_nearest(points, vp, q, 15)];
+//    move_copies(points) circle(r=.08);
+//    for(i=idx(queries)){
+//      color("red")move_copies(select(points, search_ind[i])) circle(r=.08);
+//      color("blue")stroke(move(queries[i],
+//                               circle(r=norm(points[last(search_ind[i])]-queries[i]))),
+//                          closed=true, width=.08);  
+//    }
 function _insert_sorted(list, k, new) =
       len(list)==k && new[1]>= last(list)[1] ? list
     : [
diff --git a/version.scad b/version.scad
index 5c37f18..0de9998 100644
--- a/version.scad
+++ b/version.scad
@@ -6,7 +6,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,628];
+BOSL_VERSION = [2,0,652];
 
 
 // Section: BOSL Library Version Functions