From 5ce8ba4728c64528b6247ff370d445af7a939d8a Mon Sep 17 00:00:00 2001 From: github-actions 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 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 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= -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 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 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 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, ); +// 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)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 // 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 // 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 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],