From 5ce8ba4728c64528b6247ff370d445af7a939d8a Mon Sep 17 00:00:00 2001
From: github-actions <github-actions@github.com>
Date: Tue, 2 Mar 2021 08:52:29 +0000
Subject: [PATCH 1/4] Bump release version.

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

diff --git a/version.scad b/version.scad
index b34f640..b6700b4 100644
--- a/version.scad
+++ b/version.scad
@@ -6,7 +6,7 @@
 //////////////////////////////////////////////////////////////////////
 
 
-BOSL_VERSION = [2,0,579];
+BOSL_VERSION = [2,0,580];
 
 
 // Section: BOSL Library Version Functions

From 863c410404fbff85a51a7014b77977eed0eb247a Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Thu, 4 Mar 2021 18:34:17 -0500
Subject: [PATCH 2/4] doc tweaks for skin(), faster 2d hull()

---
 hull.scad | 85 +++++++++++++++++++++++++------------------------------
 skin.scad | 54 +++++++++++++++++++++--------------
 2 files changed, 71 insertions(+), 68 deletions(-)

diff --git a/hull.scad b/hull.scad
index 0473715..fbfecdb 100644
--- a/hull.scad
+++ b/hull.scad
@@ -74,6 +74,15 @@ module hull_points(points, fast=false) {
 }
 
 
+
+function _backtracking(i,points,h,t,m) =
+    m<t || _is_cw(points[i], points[h[m-1]], points[h[m-2]]) ? m :
+    _backtracking(i,points,h,t,m-1) ;
+
+// clockwise check (2d)
+function _is_cw(a,b,c) = cross(a-c,b-c)<=0;
+
+
 // Function: hull2d_path()
 // Usage:
 //   hull2d_path(points)
@@ -85,34 +94,42 @@ module hull_points(points, fast=false) {
 //   path = hull2d_path(pts);
 //   move_copies(pts) color("red") sphere(1);
 //   polygon(points=pts, paths=[path]);
+
+// Code based on this method:
+// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
+//
 function hull2d_path(points) =
     assert(is_path(points,2),"Invalid input to hull2d_path")
     len(points) < 2 ? []
   : len(points) == 2 ? [0,1]
   : let(tri=noncollinear_triple(points, error=false))
     tri == [] ? _hull_collinear(points)
-  : let(
-        remaining = [ for (i = [0:1:len(points)-1]) if (i != tri[0] && i!=tri[1] && i!=tri[2]) i ],
-        ccw = triangle_area(points[tri[0]], points[tri[1]], points[tri[2]]) > 0,
-        polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]]
-    ) _hull2d_iterative(points, polygon, remaining);
-
-
-
-// Adds the remaining points one by one to the convex hull
-function _hull2d_iterative(points, polygon, remaining, _i=0) =
-    (_i >= len(remaining))? polygon : let (
-        // pick a point
-        i = remaining[_i],
-        // find the segments that are in conflict with the point (point not inside)
-        conflicts = _find_conflicting_segments(points, polygon, points[i])
-        // no conflicts, skip point and move on
-    ) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let(
-        // find the first conflicting segment and the first not conflicting
-        // conflict will be sorted, if not wrapping around, do it the easy way
-        polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i)
-    ) _hull2d_iterative(points, polygon, remaining, _i+1);
-
+  :
+    assert(is_path(points,2))
+    assert(len(points)>=3, "Point list must contain at least 3 points.")
+    let( n  = len(points), 
+         ip = sortidx(points) )
+    // lower hull points
+    let( lh = 
+            [ for( 	i = 2,
+                    k = 2, 
+                    h = [ip[0],ip[1]]; // current list of hull point indices 
+                  i <= n;
+                    k = i<n ? _backtracking(ip[i],points,h,2,k)+1 : k,
+                    h = i<n ? [for(j=[0:1:k-2]) h[j], ip[i]] : [], 
+                    i = i+1
+                 ) if( i==n ) h ][0] )
+    // concat lower hull points with upper hull ones
+    [ for( 	i = n-2,
+            k = len(lh), 
+            t = k+1,
+            h = lh; // current list of hull point indices 
+          i >= -1;
+            k = i>=0 ? _backtracking(ip[i],points,h,t,k)+1 : k,
+            h = [for(j=[0:1:k-2]) h[j], if(i>0) ip[i]],
+            i = i-1
+         ) if( i==-1 ) h ][0] ;
+			 
 
 function _hull_collinear(points) =
     let(
@@ -124,30 +141,6 @@ function _hull_collinear(points) =
     ) [min_i, max_i];
 
 
-function _find_conflicting_segments(points, polygon, point) = [
-    for (i = [0:1:len(polygon)-1]) let(
-        j = (i+1) % len(polygon),
-        p1 = points[polygon[i]],
-        p2 = points[polygon[j]],
-        area = triangle_area(p1, p2, point)
-    ) if (area < 0) i
-];
-
-
-// remove the conflicting segments from the polygon
-function _remove_conflicts_and_insert_point(polygon, conflicts, point) = 
-    (conflicts[0] == 0)? let(
-        nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ],
-        new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
-        polygon = concat([ for (i = new_indices) polygon[i] ], point)
-    ) polygon : let(
-        before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
-        after_conflicts  = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
-        polygon = concat(before_conflicts, point, after_conflicts)
-    ) polygon;
-
-
-
 // Function: hull3d_faces()
 // Usage:
 //   hull3d_faces(points)
diff --git a/skin.scad b/skin.scad
index 4d5e66d..7fad7b7 100644
--- a/skin.scad
+++ b/skin.scad
@@ -47,31 +47,29 @@
 //   profiles that you specify.  It is generally best if the triangles forming your polyhedron
 //   are approximately equilateral.  The `slices` parameter specifies the number of slices to insert
 //   between each pair of profiles, either a scalar to insert the same number everywhere, or a vector
-//   to insert a different number between each pair.  To resample the profiles you can use set
-//   `refine=N` which will place `N` points on each edge of your profile.  This has the effect of
-//   multiplying the number of points by N, so a profile with 8 points will have 8*N points after
-//   refinement.  Note that when dealing with continuous curves it is always better to adjust the
+//   to insert a different number between each pair.
+//   .
+//   Resampling may occur, depending on the `method` parameter, to make profiles compatible.  
+//   To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which
+//   will multiply the number of points on your profile by `N`.  You can choose between two resampling
+//   schemes using the `sampling` option, which you can set to `"length"` or `"segment"`.
+//   The length resampling method resamples proportional to length.
+//   The segment method divides each segment of a profile into the same number of points.
+//   This means that if you refine a profile with the "segment" method you will get N points
+//   on each edge, but if you refine a profile with the "length" method you will get new points
+//   distributed around the profile based on length, so small segments will get fewer new points than longer ones.  
+//   A uniform division may be impossible, in which case the code computes an approximation, which may result
+//   in arbitrary distribution of extra points.  See `subdivide_path` for more details.
+//   Note that when dealing with continuous curves it is always better to adjust the
 //   sampling in your code to generate the desired sampling rather than using the `refine` argument.
 //   .
-//   Two methods are available for resampling, `"length"` and `"segment"`.  Specify them using
-//   the `sampling` argument.  The length resampling method resamples proportional to length.
-//   The segment method divides each segment of a profile into the same number of points.
-//   A uniform division may be impossible, in which case the code computes an approximation.
-//   See `subdivide_path` for more details.
-//    
 //   You can choose from four methods for specifying alignment for incommensurate profiles.
 //   The available methods are `"distance"`, `"tangent"`, `"direct"` and `"reindex"`.
 //   It is useful to distinguish between continuous curves like a circle and discrete profiles
 //   like a hexagon or star, because the algorithms' suitability depend on this distinction.
 //   .
-//   The "direct" and "reindex" methods work by resampling the profiles if necessary.  As noted above,
-//   for continuous input curves, it is better to generate your curves directly at the desired sample size,
-//   but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
-//   to match the circle.  You can do this in two different ways using the `sampling` parameter.  The default
-//   of `sampling="length"` approximates a uniform length sampling of the profile.  The other option
-//   is `sampling="segment"` which attempts to place the same number of new points on each segment.
-//   If the segments are of varying length, this will produce a different result.  Note that "direct" is
-//   the default method.  If you simply supply a list of compatible profiles it will link them up
+//   The default method for aligning profiles is `method="direct"`.
+//   If you simply supply a list of compatible profiles it will link them up
 //   exactly as you have provided them.  You may find that profiles you want to connect define the
 //   right shapes but the point lists don't start from points that you want aligned in your skinned
 //   polyhedron.  You can correct this yourself using `reindex_polygon`, or you can use the "reindex"
@@ -79,12 +77,25 @@
 //   in the polyhedron---in will produce the least twisted possible result.  This algorithm has quadratic
 //   run time so it can be slow with very large profiles.
 //   .
+//   When the profiles are incommensurate, the "direct" and "reindex" resampling them to match.  As noted above,
+//   for continuous input curves, it is better to generate your curves directly at the desired sample size,
+//   but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
+//   to match the circle.  When you use "direct" or "reindex" the default `sampling` value is 
+//   of `sampling="length"` to approximate a uniform length sampling of the profile.  This will generally
+//   produce the natural result for connecting two continuously sampled profiles or a continuous
+//   profile and a polygonal one.  However depending on your particular case, 
+//   `sampling="segment"` may produce a more pleasing result.  These two approaches differ only when
+//   the segments of your input profiles have unequal length.
+//   .
 //   The "distance" and "tangent" methods work by duplicating vertices to create
 //   triangular faces.  The "distance" method finds the global minimum distance method for connecting two
 //   profiles.  This algorithm generally produces a good result when both profiles are discrete ones with
 //   a small number of vertices.  It is computationally intensive (O(N^3)) and may be
 //   slow on large inputs.  The resulting surfaces generally have curved faces, so be
-//   sure to select a sufficiently large value for `slices` and `refine`.
+//   sure to select a sufficiently large value for `slices` and `refine`.  Note that for
+//   this method, `sampling` must be set to `"segment"`, and hence this is the default setting.
+//   Using sampling by length would ignore the repeated vertices and ruin the alignment.  
+//   .
 //   The `"tangent"` method generally produces good results when
 //   connecting a discrete polygon to a convex, finely sampled curve.  It works by finding
 //   a plane that passed through each edge of the polygon that is tangent to
@@ -92,9 +103,8 @@
 //   all of the tangent points from each other.  It connects all of the points of the curve to the corners of the discrete
 //   polygon using triangular faces.  Using `refine` with this method will have little effect on the model, so
 //   you should do it only for agreement with other profiles, and these models are linear, so extra slices also
-//   have no effect.  For best efficiency set `refine=1` and `slices=0`.  When you use refinement with either
-//   of these methods, it is always the "segment" based resampling described above.  This is necessary because
-//   sampling by length will ignore the repeated vertices and break the alignment.
+//   have no effect.  For best efficiency set `refine=1` and `slices=0`.  As with the "distance" method, refinement
+//   must be done using the "segment" sampling scheme to preserve alignment across duplicated points.  
 //   .
 //   It is possible to specify `method` and `refine` as arrays, but it is important to observe
 //   matching rules when you do this.  If a pair of profiles is connected using "tangent" or "distance"

From f736ef98f76cdf566ce252816f8eeef3268b7758 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Fri, 5 Mar 2021 16:35:41 -0500
Subject: [PATCH 3/4] same_shape bugfix (fails if b==undef) check for collinear
 points in round_corners plus other fixes fix path_cut to work correctly when
 closed==true, and change it to fail with error when cut is too long instead
 of returning undef. Add path_cut_segs.

---
 common.scad            |  2 +-
 paths.scad             | 96 +++++++++++++++++++++++++++++++++---------
 rounding.scad          | 23 +++++++---
 tests/test_common.scad |  6 +++
 4 files changed, 98 insertions(+), 29 deletions(-)

diff --git a/common.scad b/common.scad
index e9e4504..44f1513 100644
--- a/common.scad
+++ b/common.scad
@@ -244,7 +244,7 @@ function _list_pattern(list) =
 // Example:
 //   same_shape([3,[4,5]],[7,[3,4]]);   // Returns true
 //   same_shape([3,4,5], [7,[3,4]]);    // Returns false
-function same_shape(a,b) = _list_pattern(a) == b*0;
+function same_shape(a,b) = is_def(b) && _list_pattern(a) == b*0;
 
 
 // Function: is_bool_list()
diff --git a/paths.scad b/paths.scad
index c870b62..d3b18da 100644
--- a/paths.scad
+++ b/paths.scad
@@ -1225,11 +1225,17 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals
 //   Cuts a path at a list of distances from the first point in the path.  Returns a list of the cut
 //   points and indices of the next point in the path after that point.  So for example, a return
 //   value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after
-//   this point is path[5].  If the path is too short then path_cut returns undef.  If you set
+//   this point is path[5].  If the path is too short then path_cut fails with an error.  If you set
 //   `direction` to true then `path_cut` will also return the tangent vector to the path and a normal
 //   vector to the path.  It tries to find a normal vector that is coplanar to the path near the cut
 //   point.  If this fails it will return a normal vector parallel to the xy plane.  The output with
 //   direction vectors will be `[point, next_index, tangent, normal]`.
+//   .
+//   If you give the very last point of the path as a cut point then the returned index will be
+//   one larger than the last index (so it will not be a valid index).  If you use the closed
+//   option then the returned index will be equal to the path length for cuts along the closing
+//   path segment, and if you give a point equal to the path length you will get an
+//   index of len(path)+1 for the index.  
 //
 // Arguments:
 //   path = path to cut
@@ -1246,8 +1252,10 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals
 function path_cut(path, dists, closed=false, direction=false) =
     let(long_enough = len(path) >= (closed ? 3 : 2))
     assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points")
-    !is_list(dists)? path_cut(path, [dists],closed, direction)[0]
-    : let(cuts = _path_cut(path,dists,closed))
+    is_num(dists) ? path_cut(path, [dists],closed, direction)[0] :
+    assert(is_vector(dists))
+    assert(list_increasing(dists), "Cut distances must be an increasing list")
+    let(cuts = _path_cut(path,dists,closed))
     !direction
        ? cuts
        : let(
@@ -1260,20 +1268,23 @@ function path_cut(path, dists, closed=false, direction=false) =
 function _path_cut(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) =
     dind == len(dists) ? result :
     let(
-        lastpt = len(result)>0? select(result,-1)[0] : [],
-        dpartial = len(result)==0? 0 : norm(lastpt-path[pind]),
-        nextpoint = dpartial > dists[dind]-dtotal?
-            [lerp(lastpt,path[pind], (dists[dind]-dtotal)/dpartial),pind] :
-            _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind)
-    ) is_undef(nextpoint)?
-        concat(result, repeat(undef,len(dists)-dind)) :
-        _path_cut(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint]));
+        lastpt = len(result)==0? [] : select(result,-1)[0],       // location of last cut point
+        dpartial = len(result)==0? 0 : norm(lastpt-select(path,pind)),  // remaining length in segment
+        nextpoint = dists[dind] <= dpartial+dtotal  // Do we have enough length left on the current segment?
+           ? [lerp(lastpt,select(path,pind),(dists[dind]-dtotal)/dpartial),pind] 
+           : _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind)
+    ) 
+    _path_cut(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint]));
+
 
 // Search for a single cut point in the path
 function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) =
-    ind>=len(path)? undef :
-    ind==len(path)-1 && !closed? (dist<eps? [path[ind],ind+1] : undef) :
-    let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
+    // If we get to the very end of the path (ind is last point or wraparound for closed case) then
+    // check if we are within epsilon of the final path point.  If not we're out of path, so we fail
+    ind==len(path)-(closed?0:1) ?
+       assert(dist<eps,"Path is too short for specified cut distance")
+       [select(path,ind),ind+1]
+    :let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
         [lerp(path[ind],select(path,ind+1),dist/d), ind+1] :
         _path_cut_single(path, dist-d,closed, ind+1, eps);
 
@@ -1307,18 +1318,61 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
             zeros = path[0]*0,
             nextind = cuts[ind][1],
             nextpath = unit(select(path, nextind+1)-select(path, nextind),zeros),
-            thispath = unit(select(path, nextind) - path[nextind-1],zeros),
-            lastpath = unit(path[nextind-1] - select(path, nextind-2),zeros),
+            thispath = unit(select(path, nextind) - select(path,nextind-1),zeros),
+            lastpath = unit(select(path,nextind-1) - select(path, nextind-2),zeros),
             nextdir =
                 nextind==len(path) && !closed? lastpath :
-                (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps)?
-                    unit(nextpath+thispath) :
-                    (nextind>1 || closed) && approx(cuts[ind][0],path[nextind-1],eps)?
-                        unit(thispath+lastpath) :
-                        thispath
+                (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps)
+                   ? unit(nextpath+thispath)
+              : (nextind>1 || closed) && approx(cuts[ind][0],select(path,nextind-1),eps)
+                   ? unit(thispath+lastpath)
+              :  thispath
         ) nextdir
     ];
 
+
+// Function: path_cut_segs()
+// Usage:
+//    path_list = path_cut_segs(path, cutdist, <closed>);
+// Description:
+//    Given a list of distances in `cutdist`, cut the path into
+//    subpaths at those lengths, returning a list of paths.
+//    If the input path is closed then the final path will include the
+//    original starting point.  The list of cut distances must be
+//    in ascending order.  If you repeat a distance you will get an
+//    empty list in that position in the output.
+// Arguments:
+//    path = path to cut
+//    cutdist = distance or list of distances where path is cut
+//    closed = set to true for a closed path.  Default: false
+function path_cut_segs(path,cutdist,closed) =
+  is_num(cutdist) ? path_cut_segs(path,[cutdist],closed) :
+  assert(is_vector(cutdist))
+  assert(select(cutdist,-1)<path_length(path,closed=closed),"Cut distances must be smaller than the path length")
+  assert(cutdist[0]>0, "Cut distances must be strictly positive")
+  let(
+      cutlist = path_cut(path,cutdist,closed=closed),
+      cuts = len(cutlist)
+  )
+  [
+      [ each slice(path,0,cutlist[0][1]),
+        if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0]
+      ],
+      for(i=[0:1:cuts-2])
+          cutlist[i][0]==cutlist[i+1][0] ? []
+          :
+          [ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0],
+            each slice(path,cutlist[i][1], cutlist[i+1][1]),
+            if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0],
+          ],
+      [
+        if (!approx(cutlist[cuts-1][0], select(path,cutlist[cuts-1][1]))) cutlist[cuts-1][0],
+        each select(path,cutlist[cuts-1][1],closed ? 0 : -1)
+      ]
+  ];
+
+
+
 // Input `data` is a list that sums to an integer. 
 // Returns rounded version of input data so that every 
 // entry is rounded to an integer and the sum is the same as
diff --git a/rounding.scad b/rounding.scad
index abfe0ba..a6c1d14 100644
--- a/rounding.scad
+++ b/rounding.scad
@@ -60,7 +60,8 @@ include <structs.scad>
 //   or you can specify a list that has length len(path)-2, omitting the two dummy values.
 //   .
 //   If your input path includes collinear points you must use a cut or radius value of zero for those "corners".  You can
-//   choose a nonzero joint parameter, which will cause extra points to be inserted.  
+//   choose a nonzero joint parameter when the collinear points form a 180 degree angle.  This will cause extra points to be inserted. 
+//   If the collinear points form a spike (0 degree angle) then round_corners will fail. 
 //   .
 //   Examples:
 //   * `method="circle", radius=2`:
@@ -75,7 +76,8 @@ include <structs.scad>
 //   ignored.  Note that $fn is interpreted as the number of points on the roundover curve, which is
 //   not equivalent to its meaning for rounding circles because roundovers are usually small fractions
 //   of a circular arc.  When doing continuous curvature rounding be sure to use lots of segments or the effect
-//   will be hidden by the discretization.
+//   will be hidden by the discretization.  Note that if you use $fn then $fn with "smooth" then $fn points are added at each corner, even
+//   if the "corner" is flat, with collinear points, so this guarantees a specific output length.  
 //
 // Figure(2D,Med):
 //   h = 18;
@@ -260,10 +262,16 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true
         dk = [
               for(i=[0:1:len(path)-1])
                   let(
-                      angle = vector_angle(select(path,i-1,i+1))/2
+                      pathbit = select(path,i-1,i+1),
+                      angle = approx(pathbit[0],pathbit[1]) || approx(pathbit[1],pathbit[2]) ? undef
+                            : vector_angle(select(path,i-1,i+1))/2,
+                            f=echo(angle=angle)
                   )
                   (!closed && (i==0 || i==len(path)-1))  ? [0] :          // Force zeros at ends for non-closed
                   parm[i]==0 ? [0]    : // If no rounding requested then don't try to compute parameters
+                  assert(is_def(angle), str("Repeated point in path at index ",i," with nonzero rounding"))
+                  assert(!approx(angle,0), closed && i==0 ? "Closing the path causes it to turn back on itself at the end" :
+                                                            str("Path turns back on itself at index ",i," with nonzero rounding"))
                   (method=="chamfer" && measure=="joint")? [parm[i]] :
                   (method=="chamfer" && measure=="cut")  ? [parm[i]/cos(angle)] :
                   (method=="smooth" && measure=="joint") ? [parm[i],k[i]] :
@@ -277,10 +285,11 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true
         lengths = [for(i=[0:1:len(path)]) norm(select(path,i)-select(path,i-1))],
         scalefactors = [
             for(i=[0:1:len(path)-1])
-                min(
+                if (closed || (i!=0 && i!=len(path)-1))
+                 min(
                     lengths[i]/(select(dk,i-1)[0]+dk[i][0]),
                     lengths[i+1]/(dk[i][0]+select(dk,i+1)[0])
-                )
+                 )
         ],
         dummy = verbose ? echo("Roundover scale factors:",scalefactors) : 0
     )
@@ -639,12 +648,12 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false)
       d_next = is_vector(joint[i]) ? joint[i][1] : joint[i]
   )
   assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1))
+  assert(d_first<path_length(revresult),str("Path ",i," is too short for specified cut distance ",d_first))
+  assert(d_next<path_length(nextpath), str("Path ",i+1," is too short for specified cut distance ",d_next))
   let(
       firstcut = path_cut(revresult, d_first, direction=true),
       nextcut = path_cut(nextpath, d_next, direction=true)
   )
-  assert(is_def(firstcut),str("Path ",i," is too short for specified cut distance ",d_first))
-  assert(is_def(nextcut),str("Path ",i+1," is too short for specified cut distance ",d_next))
   assert(!loop || nextcut[1] < len(revresult)-1-firstcut[1], "Path is too short to close the loop")
   let(
      first_dir=firstcut[2],
diff --git a/tests/test_common.scad b/tests/test_common.scad
index 2397437..3568440 100644
--- a/tests/test_common.scad
+++ b/tests/test_common.scad
@@ -245,6 +245,12 @@ test_is_consistent();
 module test_same_shape() {
     assert(same_shape([3,[4,5]],[7,[3,4]]));
     assert(!same_shape([3,4,5], [7,[3,4]]));
+    assert(!same_shape([3,4,5],undef));
+    assert(!same_shape([5,3],3));
+    assert(!same_shape(undef,[3,4]));
+    assert(same_shape(4,5));
+    assert(!same_shape(5,undef));
+           
 }
 test_same_shape();
 

From 6a1b141d5c62dd692620f12efb9791f11d88a3c9 Mon Sep 17 00:00:00 2001
From: Adrian Mariano <avm4@cornell.edu>
Date: Fri, 5 Mar 2021 20:39:15 -0500
Subject: [PATCH 4/4] hull 2d tweak

---
 hull.scad            | 18 +++++++++---------
 tests/test_hull.scad |  8 ++++----
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/hull.scad b/hull.scad
index fbfecdb..5658bec 100644
--- a/hull.scad
+++ b/hull.scad
@@ -80,7 +80,7 @@ function _backtracking(i,points,h,t,m) =
     _backtracking(i,points,h,t,m-1) ;
 
 // clockwise check (2d)
-function _is_cw(a,b,c) = cross(a-c,b-c)<=0;
+function _is_cw(a,b,c) = cross(a-c,b-c)<-EPSILON*norm(a-c)*norm(b-c);
 
 
 // Function: hull2d_path()
@@ -100,13 +100,7 @@ function _is_cw(a,b,c) = cross(a-c,b-c)<=0;
 //
 function hull2d_path(points) =
     assert(is_path(points,2),"Invalid input to hull2d_path")
-    len(points) < 2 ? []
-  : len(points) == 2 ? [0,1]
-  : let(tri=noncollinear_triple(points, error=false))
-    tri == [] ? _hull_collinear(points)
-  :
-    assert(is_path(points,2))
-    assert(len(points)>=3, "Point list must contain at least 3 points.")
+    len(points) < 2 ? [] :
     let( n  = len(points), 
          ip = sortidx(points) )
     // lower hull points
@@ -134,13 +128,19 @@ function hull2d_path(points) =
 function _hull_collinear(points) =
     let(
         a = points[0],
-        n = points[1] - a,
+        i = max_index([for(pt=points) norm(pt-a)]),
+        n = points[i] - a
+    )
+    norm(n)==0 ? [0]
+    :
+    let(
         points1d = [ for(p = points) (p-a)*n ],
         min_i = min_index(points1d),
         max_i = max_index(points1d)
     ) [min_i, max_i];
 
 
+
 // Function: hull3d_faces()
 // Usage:
 //   hull3d_faces(points)
diff --git a/tests/test_hull.scad b/tests/test_hull.scad
index 3aba71a..a6a7926 100644
--- a/tests/test_hull.scad
+++ b/tests/test_hull.scad
@@ -6,9 +6,9 @@ module test_hull() {
     assert_equal(hull([[3,4,1],[5,5,3]]), [0,1]);
 
     test_collinear_2d = let(u = unit([5,3]))    [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
-    assert_equal(hull(test_collinear_2d), [7,1]);
+    assert_equal(sort(hull(test_collinear_2d)), [1,7]);
     test_collinear_3d = let(u = unit([5,3,2]))    [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
-    assert_equal(hull(test_collinear_3d), [7,1]);
+    assert_equal(sort(hull(test_collinear_3d)), [1,7]);
 
     /*    // produces some extra points along edges
     test_square_2d = [for(x=[1:5], y=[2:6]) [x,y]];
@@ -105,9 +105,9 @@ module test_hull2d_path() {
     assert_equal(hull([[3,4,1],[5,5,3]]), [0,1]);
 
     test_collinear_2d = let(u = unit([5,3]))    [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
-    assert_equal(hull(test_collinear_2d), [7,1]);
+    assert_equal(sort(hull(test_collinear_2d)), [1,7]);
     test_collinear_3d = let(u = unit([5,3,2]))    [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
-    assert_equal(hull(test_collinear_3d), [7,1]);
+    assert_equal(sort(hull(test_collinear_3d)), [1,7]);
 
     rand10_2d = [[1.55356, -1.98965], [4.23157, -0.947788], [-4.06193, -1.55463],
                  [1.23889, -3.73133], [-1.02637, -4.0155], [4.26806, -4.61909],