From c465a5b746746b79ff2dad405ddb92221fd55ad8 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Sun, 16 Aug 2020 23:09:59 -0700
Subject: [PATCH 1/4] cuboid() with negative chamfer had mispositioned
 anti-chamfers.

---
 shapes.scad  | 4 ++--
 version.scad | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/shapes.scad b/shapes.scad
index cb6d4e5..d612b0a 100644
--- a/shapes.scad
+++ b/shapes.scad
@@ -102,8 +102,8 @@ module cuboid(
                 if (edges == EDGES_ALL && trimcorners) {
                     if (chamfer<0) {
                         cube(size, center=true) {
-                            attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP);
-                            attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP);
+                            attach(TOP,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP);
+                            attach(BOT,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP);
                         }
                     } else {
                         isize = [for (v = size) max(0.001, v-2*chamfer)];
diff --git a/version.scad b/version.scad
index e6ba60b..21ab73b 100644
--- a/version.scad
+++ b/version.scad
@@ -8,7 +8,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,402];
+BOSL_VERSION = [2,0,403];
 
 
 // Section: BOSL Library Version Functions

From 25841bda7328edcaf0f82387131bfd84149cf5b2 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Sun, 16 Aug 2020 23:10:43 -0700
Subject: [PATCH 2/4] Fixed attachable() anchors for vnf's with extent=false.

---
 attachments.scad | 35 +++++++++++++----------------------
 version.scad     |  2 +-
 2 files changed, 14 insertions(+), 23 deletions(-)

diff --git a/attachments.scad b/attachments.scad
index 6004e29..2ac4367 100644
--- a/attachments.scad
+++ b/attachments.scad
@@ -458,10 +458,9 @@ function find_anchor(anchor, geom) =
             eps = 1/2048,
             points = vnf[0],
             faces = vnf[1],
-            rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=points)),
+            rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), points),
             hits = [
-                for (i = idx(faces)) let(
-                    face = faces[i],
+                for (face = faces) let(
                     verts = select(rpts, face)
                 ) if (
                     max(subindex(verts,0)) >= -eps &&
@@ -470,35 +469,27 @@ function find_anchor(anchor, geom) =
                     min(subindex(verts,1)) <=  eps &&
                     min(subindex(verts,2)) <=  eps
                 ) let(
-                    pt = polygon_line_intersection(
-                        select(points, face),
-                        [CENTER,anchor], eps=eps
-                    )
-                ) if (!is_undef(pt)) [norm(pt), i, pt]
+                    poly = select(points, face),
+                    pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps)
+                ) if (!is_undef(pt)) let(
+                    plane = plane_from_polygon(poly),
+                    n = plane_normal(plane)
+                )
+                [norm(pt-cp), n, pt]
             ]
         )
         assert(len(hits)>0, "Anchor vector does not intersect with the shape.  Attachment failed.")
         let(
             furthest = max_index(subindex(hits,0)),
-            pos = point3d(cp) + hits[furthest][2],
             dist = hits[furthest][0],
-            nfaces = [for (hit = hits) if(approx(hit[0],dist,eps=eps)) hit[1]],
-            n = unit(
-                sum([
-                    for (i = nfaces) let(
-                        faceverts = select(points, faces[i]),
-                        faceplane = plane_from_points(faceverts),
-                        nrm = plane_normal(faceplane)
-                    ) nrm
-                ]) / len(nfaces),
-                UP
-            )
+            pos = hits[furthest][2],
+            n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]]))
         )
         [anchor, pos, n, oang]
     ) : type == "vnf_extent"? ( //vnf
         let(
             vnf=geom[1],
-            rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=vnf[0])),
+            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],
             mm = pointlist_bounds(select(rpts,idxs)),
@@ -849,7 +840,7 @@ module attachable(
 
 // Module: position()
 // Usage:
-//   position(from, [overlap]) ...
+//   position(from) ...
 // Description:
 //   Attaches children to a parent object at an anchor point.
 // Arguments:
diff --git a/version.scad b/version.scad
index 21ab73b..85cad34 100644
--- a/version.scad
+++ b/version.scad
@@ -8,7 +8,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,403];
+BOSL_VERSION = [2,0,404];
 
 
 // Section: BOSL Library Version Functions

From 2ff93f34cdf49672f34d08d0983309f621507296 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Mon, 17 Aug 2020 18:43:58 -0400
Subject: [PATCH 3/4] Added attachment support to all modules, and bug fixed
 rounded_prism (problem introduced by error checking in det2), and fixed
 broken example in rounding.

---
 rounding.scad | 286 ++++++++++++++++++++++++++++----------------------
 skin.scad     |  63 ++++++++---
 2 files changed, 208 insertions(+), 141 deletions(-)

diff --git a/rounding.scad b/rounding.scad
index 254d035..b3bfa45 100644
--- a/rounding.scad
+++ b/rounding.scad
@@ -458,10 +458,10 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals
 
 
 
-// Module: offset_sweep()
+// Function&Module: offset_sweep()
 //
 // Description:
-//   Takes a 2d path as input and extrudes it upwards and/or downward.  Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer.
+//   Takes a 2d path as input and extrudes it upwards and/or downward.  Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer.  When invoked as a function returns a VNF; when invoked as a module produces geometry.  
 //   You can specify a sequence of offsets values, or you can use several built-in offset profiles that are designed to provide end treatments such as roundovers.
 //   The path is shifted by `offset()` multiple times in sequence
 //   to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade
@@ -543,8 +543,12 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals
 //   angle = default angle for chamfers.  Default: 45
 //   joint = default joint value for smooth roundover.
 //   k = default curvature parameter value for "smooth" roundover
-//   convexity = convexity setting for use with polyhedron.  Default: 10
-//
+//   convexity = convexity setting for use with polyhedron.  (module only) Default: 10
+//   anchor = Translate so anchor point is at the origin.  (module only) Default: "origin"
+//   spin = Rotate this many degrees around Z axis after anchor.  (module only) Default: 0
+//   orient = Vector to rotate top towards after spin  (module only)
+//   extent = use extent method for computing anchors. (module only)  Default: false
+//   cp = set centerpoint for anchor computation.  (module only) Default: object centroid
 // Example: Rounding a star shaped prism with postive radius values
 //   star = star(5, r=22, ir=13);
 //   rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
@@ -650,118 +654,118 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals
 //       up(1)
 //         offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4));
 //     }
-module offset_sweep(
-        path, height, h, l,
-        top=[], bottom=[],
-        offset="round", r=0, steps=16,
-        quality=1, check_valid=true,
-        offset_maxstep=1, extra=0,
-        cut=undef, chamfer_width=undef, chamfer_height=undef,
-        joint=undef, k=0.75, angle=45,
-        convexity=10
-) {
-        // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce
-        // the inputs for the polyhedron module.
-        function make_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, vertexcount=0, vertices=[], faces=[] )=
-                offsetind==len(offsets)? (
-                        let(
-                                bottom = list_range(n=len(path),s=vertexcount),
-                                oriented_bottom = !flip_faces? bottom : reverse(bottom)
-                        ) [vertices, concat(faces,[oriented_bottom])]
-                ) : (
-                        let(
-                                this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0],
-                                delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef,
-                                r = offset_type=="round"? this_offset : undef,
-                                do_chamfer = offset_type == "chamfer"
+
+
+// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce
+// the inputs for the polyhedron module.
+function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0,
+                                 vertexcount=0, vertices=[], faces=[] )=
+        offsetind==len(offsets)? (
+                let(
+                        bottom = list_range(n=len(path),s=vertexcount),
+                        oriented_bottom = !flip_faces? bottom : reverse(bottom)
+                ) [vertices, concat(faces,[oriented_bottom])]
+        ) : (
+                let(
+                        this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0],
+                        delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef,
+                        r = offset_type=="round"? this_offset : undef,
+                        do_chamfer = offset_type == "chamfer"
+                )
+                let(
+                        vertices_faces = offset(
+                                path, r=r, delta=delta, chamfer = do_chamfer, closed=true,
+                                check_valid=check_valid, quality=quality,
+                                maxstep=maxstep, return_faces=true,
+                                firstface_index=vertexcount,
+                                flip_faces=flip_faces
                         )
-                        assert(num_defined([r,delta])==1,"Must set `offset` to \"round\" or \"delta")
-                        let(
-                                vertices_faces = offset(
-                                        path, r=r, delta=delta, chamfer = do_chamfer, closed=true,
-                                        check_valid=check_valid, quality=quality,
-                                        maxstep=maxstep, return_faces=true,
-                                        firstface_index=vertexcount,
-                                        flip_faces=flip_faces
-                                )
-                        )
-                        make_polyhedron(
-                                vertices_faces[0], offsets, offset_type,
-                                flip_faces, quality, check_valid, maxstep,
-                                offsetind+1, vertexcount+len(path),
-                                vertices=concat(
-                                        vertices,
-                                        zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0])))
-                                ),
-                                faces=concat(faces, vertices_faces[1])
-                        )
-                );
-
-
-        argspec = [
-                ["r",r],
-                ["extra",extra],
-                ["type","circle"],
-                ["check_valid",check_valid],
-                ["quality",quality],
-                ["offset_maxstep", offset_maxstep],
-                ["steps",steps],
-                ["offset",offset],
-                ["chamfer_width",chamfer_width],
-                ["chamfer_height",chamfer_height],
-                ["angle",angle],
-                ["cut",cut],
-                ["joint",joint],
-                ["k", k],
-                ["points", []],
-        ];
-
-        path = check_and_fix_path(path, [2], closed=true);
-        clockwise = polygon_is_clockwise(path);
-
-        top = struct_set(argspec, top, grow=false);
-        bottom = struct_set(argspec, bottom, grow=false);
-
-        //  This code does not work.  It hits the error in make_polyhedron from offset being wrong
-        //  before this code executes.  Had to move the test into make_polyhedron, which is ugly since it's in the loop
-        //offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) &&
-        //      in_list(struct_val(bottom, "offset"),["round","delta"]);
-        //assert(offsetsok,"Offsets must be one of \"round\" or \"delta\"");
-
-
-        offsets_bot = _rounding_offsets(bottom, -1);
-        offsets_top = _rounding_offsets(top, 1);
-
-        if (offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5)) {
-          echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested many layers.  This can be slow or run out of recursion depth.");
-        }
-        // "Extra" height enlarges the result beyond the requested height, so subtract it
-        bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra");
-        top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra");
-
-        height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height);
-        assert(height>=0, "Height must be nonnegative");
-
-        middle = height-bottom_height-top_height;
-        assert(
-                middle>=0, str(
-                        "Specified end treatments (bottom height = ",bottom_height,
-                        " top_height = ",top_height,") are too large for extrusion height (",height,")"
+                )
+                _make_offset_polyhedron(
+                        vertices_faces[0], offsets, offset_type,
+                        flip_faces, quality, check_valid, maxstep,
+                        offsetind+1, vertexcount+len(path),
+                        vertices=concat(
+                                vertices,
+                                zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0])))
+                        ),
+                        faces=concat(faces, vertices_faces[1])
                 )
         );
-        initial_vertices_bot = path3d(path);
 
-        vertices_faces_bot = make_polyhedron(
+
+function offset_sweep(
+                       path, height, h, l,
+                       top=[], bottom=[],
+                       offset="round", r=0, steps=16,
+                       quality=1, check_valid=true,
+                       offset_maxstep=1, extra=0,
+                       cut=undef, chamfer_width=undef, chamfer_height=undef,
+                       joint=undef, k=0.75, angle=45
+                      ) =
+    let(
+        argspec = [
+                   ["r",r],
+                   ["extra",extra],
+                   ["type","circle"],
+                   ["check_valid",check_valid],
+                   ["quality",quality],
+                   ["offset_maxstep", offset_maxstep],
+                   ["steps",steps],
+                   ["offset",offset],
+                   ["chamfer_width",chamfer_width],
+                   ["chamfer_height",chamfer_height],
+                   ["angle",angle],
+                   ["cut",cut],
+                   ["joint",joint],
+                   ["k", k],
+                   ["points", []],
+        ],
+        path = check_and_fix_path(path, [2], closed=true),
+        clockwise = polygon_is_clockwise(path),
+        
+        top = struct_set(argspec, top, grow=false),
+        bottom = struct_set(argspec, bottom, grow=false),
+
+        //  This code does not work.  It hits the error in _make_offset_polyhedron from offset being wrong
+        //  before this code executes.  Had to move the test into _make_offset_polyhedron, which is ugly since it's in the loop
+        offsetsok = in_list(struct_val(top, "offset"),["round","delta"])
+                    && in_list(struct_val(bottom, "offset"),["round","delta"])
+    )
+    assert(offsetsok,"Offsets must be one of \"round\" or \"delta\"")
+    let( 
+        offsets_bot = _rounding_offsets(bottom, -1),
+        offsets_top = _rounding_offsets(top, 1),
+        dummy = offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5)
+                ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers.  This can be slow or run out of recursion depth.")
+                : 0,
+
+        // "Extra" height enlarges the result beyond the requested height, so subtract it
+        bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"),
+        top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"),
+
+        height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height),
+        middle = height-bottom_height-top_height
+    )
+    assert(height>=0, "Height must be nonnegative") 
+    assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height,
+                          " top_height = ",top_height,") are too large for extrusion height (",height,")"
+                         )
+    )
+    let(
+        initial_vertices_bot = path3d(path),
+
+        vertices_faces_bot = _make_offset_polyhedron(
                 path, offsets_bot, struct_val(bottom,"offset"), clockwise,
                 struct_val(bottom,"quality"),
                 struct_val(bottom,"check_valid"),
                 struct_val(bottom,"offset_maxstep"),
                 vertices=initial_vertices_bot
-        );
+        ),
 
-        top_start_ind = len(vertices_faces_bot[0]);
-        initial_vertices_top = zip(path, repeat(middle,len(path)));
-        vertices_faces_top = make_polyhedron(
+        top_start_ind = len(vertices_faces_bot[0]),
+        initial_vertices_top = zip(path, repeat(middle,len(path))),
+        vertices_faces_top = _make_offset_polyhedron(
                 path, move(p=offsets_top,[0,middle]),
                 struct_val(top,"offset"), !clockwise,
                 struct_val(top,"quality"),
@@ -769,20 +773,39 @@ module offset_sweep(
                 struct_val(top,"offset_maxstep"),
                 vertexcount=top_start_ind,
                 vertices=initial_vertices_top
-        );
+        ),
         middle_faces = middle==0 ? [] : [
                 for(i=[0:len(path)-1]) let(
                         oneface=[i, (i+1)%len(path), top_start_ind+(i+1)%len(path), top_start_ind+i]
                 ) !clockwise ? reverse(oneface) : oneface
-        ];
-        up(bottom_height) {
-                polyhedron(
-                        concat(vertices_faces_bot[0],vertices_faces_top[0]),
-                        faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces),
-                        convexity=convexity
-                );
-        }
-}
+        ]
+    )
+    [up(bottom_height, concat(vertices_faces_bot[0],vertices_faces_top[0])),  // Vertices
+     concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)];     // Faces
+
+
+module offset_sweep(path, height, h, l,
+                    top=[], bottom=[],
+                    offset="round", r=0, steps=16,
+                    quality=1, check_valid=true,
+                    offset_maxstep=1, extra=0,
+                    cut=undef, chamfer_width=undef, chamfer_height=undef,
+                    joint=undef, k=0.75, angle=45,
+                    convexity=10,anchor="origin",cp,
+                    spin=0, orient=UP, extent=false)
+{
+    vnf = offset_sweep(path=path, height=height, h=h, l=l, top=top, bottom=bottom, offset=offset, r=0, steps=steps,
+                       quality=quality, check_valid=true, offset_maxstep=1, extra=0, cut=cut, chamfer_width=chamfer_width,
+                       chamfer_height=chamfer_height, joint=joint, k=k, angle=angle);
+  
+    attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf))
+    {
+        vnf_polyhedron(vnf,convexity=convexity);
+        children();
+    }   
+}   
+
+
 
 function os_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) =
         assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`")
@@ -924,7 +947,6 @@ function os_profile(points, extra,check_valid, quality, offset_maxstep, offset)
 //   joint = default joint value for smooth roundover.
 //   k = default curvature parameter value for "smooth" roundover
 //   convexity = convexity setting for use with polyhedron.  Default: 10
-//
 // Example: Chamfered elliptical prism.  If you stretch a chamfered cylinder the chamfer will be uneven.
 //   convex_offset_extrude(bottom = os_chamfer(height=-2), top=os_chamfer(height=1), height=7)
 //   xscale(4)circle(r=6,$fn=64);
@@ -1364,8 +1386,6 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true,
         }
 }
 
-
-
 function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) =
    let(
      N = len(top),
@@ -1395,8 +1415,8 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) =
                     let(
                        prev_corner = prev_offset + abs(rtop_in)*in_prev,
                        next_corner = next_offset + abs(rtop_in)*in_next,
-                       prev_degenerate = is_undef(ray_intersection([far_corner, far_corner+prev], [prev_offset, prev_offset+in_prev])),
-                       next_degenerate = is_undef(ray_intersection([far_corner, far_corner+next], [next_offset, next_offset+in_next]))
+                       prev_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+prev]), path2d([prev_offset, prev_offset+in_prev]))),
+                       next_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+next]), path2d([next_offset, next_offset+in_next])))
                     )
                     [ prev_degenerate ? far_corner : prev_corner,
                       far_corner,
@@ -1452,6 +1472,11 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) =
 //   splinesteps = number of segments to use for curved patches.  Default: 16
 //   debug = turn on debug mode which displays illegal polyhedra and shows the bezier corner patches for troubleshooting purposes.  Default: False
 //   convexity = convexity parameter for polyhedron(), only for module version.  Default: 10
+//   anchor = Translate so anchor point is at the origin.  (module only) Default: "origin"
+//   spin = Rotate this many degrees around Z axis after anchor.  (module only) Default: 0
+//   orient = Vector to rotate top towards after spin  (module only)
+//   extent = use extent method for computing anchors. (module only)  Default: false
+//   cp = set centerpoint for anchor computation.  (module only) Default: object centroid
 // Example: Uniformly rounded pentagonal prism
 //   rounded_prism(pentagon(3), height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
 // Example: Maximum possible rounding.
@@ -1500,15 +1525,21 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) =
 //   rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)), joint_top=2, joint_bot=1, joint_sides=1);
 
 module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides,
-                     k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false)
+                     k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false,
+                     anchor="origin",cp,spin=0, orient=UP, extent=false)
 {
   result = rounded_prism(bottom=bottom, top=top, joint_bot=joint_bot, joint_top=joint_top, joint_sides=joint_sides,
                          k_bot=k_bot, k_top=k_top, k_sides=k_sides, k=k, splinesteps=splinesteps, h=h, length=length, height=height, l=l,debug=debug);
-  if (debug){
-      vnf_polyhedron(result[1], convexity=convexity);
-      trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false);
+  vnf = debug ? result[1] : result;
+  attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf))
+  {
+    if (debug){
+        vnf_polyhedron(vnf, convexity=convexity);
+        trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false);
+    }
+    else vnf_polyhedron(vnf,convexity=convexity);
+    children();
   }
-  else vnf_polyhedron(result,convexity=convexity);
 }
 
 
@@ -1880,7 +1911,7 @@ function _circle_mask(r) =
 //     $fn=128;
 //     difference(){
 //       tube(or=r, wall=2, h=45);
-//       bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7))),14*15,closed=true)));
+//       bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7)),$fn=32),14*15,closed=true)));
 //     }
 //   }
 // Example(2D): Cutting a slot in a cylinder is tricky if you want rounded corners at the top.  This slot profile has slightly angled top edges to blend into the top edge of the cylinder.
@@ -1944,6 +1975,7 @@ function _circle_mask(r) =
 
 module bent_cutout_mask(r, thickness, path, convexity=10)
 {
+  no_children($children);
   assert(is_path(path,2),"Input path must be a 2d path")
   assert(r-thickness>0, "Thickness too large for radius");
   assert(thickness>0, "Thickness must be positive");
diff --git a/skin.scad b/skin.scad
index 30d8a17..87ae8d4 100644
--- a/skin.scad
+++ b/skin.scad
@@ -16,7 +16,8 @@ include <vnf.scad>
 
 // Function&Module: skin()
 // Usage: As module:
-//   skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]);
+//   skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity],
+//        [anchor],[cp],[spin],[orient],[extent]);
 // Usage: As function:
 //   vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]);
 // Description:
@@ -117,6 +118,12 @@ include <vnf.scad>
 //   caps = true to create endcap faces when closed is false.  Can be a length 2 boolean array.  Default is true if closed is false.
 //   method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex".  Default: "direct".
 //   z = array of height values for each profile if the profiles are 2d
+//   convexity = convexity setting for use with polyhedron.  (module only) Default: 10
+//   anchor = Translate so anchor point is at the origin.  (module only) Default: "origin"
+//   spin = Rotate this many degrees around Z axis after anchor.  (module only) Default: 0
+//   orient = Vector to rotate top towards after spin  (module only)
+//   extent = use extent method for computing anchors. (module only)  Default: false
+//   cp = set centerpoint for anchor computation.  (module only) Default: object centroid
 // Example:
 //   skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10);
 // Example: Rotating the pentagon place the zero index at different locations, giving a twist
@@ -315,11 +322,15 @@ include <vnf.scad>
 //              stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true);
 //          }
 //      }
-
-
-module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10)
+module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10,
+            anchor="origin",cp,spin=0, orient=UP, extent=false)
 {
-      vnf_polyhedron(skin(profiles, slices, refine, method, sampling, caps, closed, z), convexity=convexity);
+    vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z);
+    attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf))
+    {      
+        vnf_polyhedron(vnf,convexity=convexity);
+        children();
+    }
 }        
 
 
@@ -803,6 +814,12 @@ function associate_vertices(polygons, split, curpoly=0) =
 //   transformations = list of 4x4 matrices to apply
 //   closed = set to true to form a closed (torus) model.  Default: false
 //   caps = true to create endcap faces when closed is false.  Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array.  Default is true if closed is false.
+//   convexity = convexity setting for use with polyhedron.  (module only) Default: 10
+//   anchor = Translate so anchor point is at the origin.  (module only) Default: "origin"
+//   spin = Rotate this many degrees around Z axis after anchor.  (module only) Default: 0
+//   orient = Vector to rotate top towards after spin  (module only)
+//   extent = use extent method for computing anchors. (module only)  Default: false
+//   cp = set centerpoint for anchor computation.  (module only) Default: object centroid
 // Example: This is the "sweep-drop" example from list-comprehension-demos.
 //   function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1;
 //   function path(t) = [0, 0, 80 + 80 * cos(180 * t)];
@@ -839,9 +856,16 @@ function sweep(shape, transformations, closed=false, caps) =
   assert(!closed || !caps, "Cannot make closed shape with caps")
   _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps);
 
-module sweep(shape, transformations, closed=false, caps, convexity=10) {
-  vnf_polyhedron(sweep(shape, transformations, closed, caps), convexity=convexity);
-}  
+module sweep(shape, transformations, closed=false, caps, convexity=10,
+             anchor="origin",cp,spin=0, orient=UP, extent=false)
+{
+    vnf = sweep(shape, transformations, closed, caps);
+    attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf))
+    {      
+        vnf_polyhedron(vnf,convexity=convexity);
+        children();
+    }
+}        
 
 
 // Function&Module: path_sweep()
@@ -906,8 +930,13 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) {
 //   tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve)
 //   relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent.  Default: false
 //   caps = Can be a boolean or vector of two booleans.  Set to false to disable caps at the two ends.  Default: true
+//   transforms = set to true to return transforms instead of a VNF.  These transforms can be manipulated and passed to sweep().  Default: false.
 //   convexity = convexity parameter for polyhedron().  Only accepted by the module version.  Default: 10
-//   transforms = set to true to return transforms instead of a VNF.  These transforms can be manipulated and passed to sweep().  Default: false. 
+//   anchor = Translate so anchor point is at the origin.  (module only) Default: "origin"
+//   spin = Rotate this many degrees around Z axis after anchor.  (module only) Default: 0
+//   orient = Vector to rotate top towards after spin  (module only)
+//   extent = use extent method for computing anchors. (module only)  Default: false
+//   cp = set centerpoint for anchor computation.  (module only) Default: object centroid
 //
 // Example(2D): We'll use this shape in several examples
 //   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
@@ -1121,13 +1150,19 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) {
 //   outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))];
 //   inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))];
 //   sweep(shape, concat(outside,inside),closed=true);
-
 module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true,
-                    symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10)
+                    symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10,
+                    anchor="origin",cp,spin=0, orient=UP, extent=false)
 {
-  vnf_polyhedron(path_sweep(shape, path, method, normal, closed, twist, twist_by_length,
-                    symmetry, last_normal, tangent, relaxed, caps), convexity=convexity);
-}
+    vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length,
+                    symmetry, last_normal, tangent, relaxed, caps);
+    attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf))
+    {      
+        vnf_polyhedron(vnf,convexity=convexity);
+        children();
+    }
+}        
+
 
 function path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true,
                     symmetry=1, last_normal, tangent, relaxed=false, caps, transforms=false) = 

From 7daa16f2382d62e7acd01363a35feb95c6d158c3 Mon Sep 17 00:00:00 2001
From: Garth Minette <gminette@gmail.com>
Date: Mon, 17 Aug 2020 18:11:49 -0700
Subject: [PATCH 4/4] Fixed VNF attachment normal vector.

---
 attachments.scad | 17 +++++++++++++++--
 version.scad     |  2 +-
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/attachments.scad b/attachments.scad
index 2ac4367..c2c969e 100644
--- a/attachments.scad
+++ b/attachments.scad
@@ -473,7 +473,7 @@ function find_anchor(anchor, geom) =
                     pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps)
                 ) if (!is_undef(pt)) let(
                     plane = plane_from_polygon(poly),
-                    n = plane_normal(plane)
+                    n = unit(plane_normal(plane))
                 )
                 [norm(pt-cp), n, pt]
             ]
@@ -483,7 +483,20 @@ function find_anchor(anchor, geom) =
             furthest = max_index(subindex(hits,0)),
             dist = hits[furthest][0],
             pos = hits[furthest][2],
-            n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]]))
+            hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]],
+            unorms = len(hitnorms) > 7
+              ? unique([for (nn = hitnorms) quant(nn,1e-9)])
+              : [
+                    for (i = idx(hitnorms)) let(
+                        nn = hitnorms[i],
+                        isdup = [
+                            for (j = [i+1:1:len(hitnorms)-1])
+                            if (approx(nn, hitnorms[j])) 1
+                        ] != []
+                    ) if (!isdup) nn
+                ],
+            n = unit(sum(unorms)),
+            oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90
         )
         [anchor, pos, n, oang]
     ) : type == "vnf_extent"? ( //vnf
diff --git a/version.scad b/version.scad
index 85cad34..1420f58 100644
--- a/version.scad
+++ b/version.scad
@@ -8,7 +8,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,404];
+BOSL_VERSION = [2,0,405];
 
 
 // Section: BOSL Library Version Functions