From 024011ea5e44447eb0a8bd1d1d9580f7f34590c0 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 27 Oct 2021 20:21:12 -0400 Subject: [PATCH 1/4] list_remove fix rename array_dim to list_shape --- comparisons.scad | 4 +- lists.scad | 148 +++++++++++++++++++++++------------------- skin.scad | 2 +- tests/test_lists.scad | 29 +++++---- 4 files changed, 103 insertions(+), 80 deletions(-) diff --git a/comparisons.scad b/comparisons.scad index 9718ec5..e78d189 100644 --- a/comparisons.scad +++ b/comparisons.scad @@ -650,7 +650,7 @@ function sort(list, idx=undef) = is_string(list)? str_join(sort([for (x = list) x],idx)) : !is_list(list) || len(list)<=1 ? list : is_homogeneous(list,1) - ? let(size = array_dim(list[0])) + ? let(size = list_shape(list[0])) size==0 ? _sort_scalars(list) : len(size)!=1 ? _sort_general(list,idx) : is_undef(idx) ? _sort_vectors(list) @@ -692,7 +692,7 @@ function sortidx(list, idx=undef) = !is_list(list) || len(list)<=1 ? list : is_homogeneous(list,1) ? let( - size = array_dim(list[0]), + size = list_shape(list[0]), aug = ! (size==0 || len(size)==1) ? 0 // for general sorting : [for(i=[0:len(list)-1]) concat(i,list[i])], // for scalar or vector sorting lidx = size==0? [1] : // scalar sorting diff --git a/lists.scad b/lists.scad index fb2d804..f8bd372 100644 --- a/lists.scad +++ b/lists.scad @@ -84,6 +84,59 @@ function max_length(array) = + +// Internal. Not exposed. +function _list_shape_recurse(v) = + !is_list(v[0]) + ? len( [for(entry=v) if(!is_list(entry)) 0] ) == 0 ? [] : [undef] + : let( + firstlen = is_list(v[0]) ? len(v[0]): undef, + first = len( [for(entry = v) if(! is_list(entry) || (len(entry) != firstlen)) 0 ] ) == 0 ? firstlen : undef, + leveldown = flatten(v) + ) + is_list(leveldown[0]) + ? concat([first],_list_shape_recurse(leveldown)) + : [first]; + +function _list_shape_recurse(v) = + let( alen = [for(vi=v) is_list(vi) ? len(vi): -1] ) + v==[] || max(alen)==-1 ? [] : + let( add = max(alen)!=min(alen) ? undef : alen[0] ) + concat( add, _list_shape_recurse(flatten(v))); + + +// Function: list_shape() +// Usage: +// dims = list_shape(v, [depth]); +// Topics: Matrices, Array Handling +// Description: +// Returns the size of a multi-dimensional array, a list of the lengths at each depth. +// If the returned value has `dims[i] = j` then it means the ith index ranges of j items. +// The return `dims[0]` is equal to the length of v. Then `dims[1]` is equal to the +// length of the lists in v, and in general, `dims[i]` is equal to the length of the items +// nested to depth i in the list v. If the length of items at that depth is inconsistent, then +// `undef` is returned. If no items exist at that depth then `0` is returned. Note that +// for simple vectors or matrices it is faster to compute `len(v)` and `len(v[0])`. +// Arguments: +// v = list to get shape of +// depth = depth to compute the size of. If not given, returns a list of sizes at all depths. +// Example: +// a = list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]); // Returns [2,2,3] +// b = list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0); // Returns 2 +// c = list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2); // Returns 3 +// d = list_shape([[[1,2,3],[4,5,6]],[[7,8,9]]]); // Returns [2,undef,3] +function list_shape(v, depth=undef) = + assert( is_undef(depth) || ( is_finite(depth) && depth>=0 ), "Invalid depth.") + ! is_list(v) ? 0 : + (depth == undef) + ? concat([len(v)], _list_shape_recurse(v)) + : (depth == 0) + ? len(v) + : let( dimlist = _list_shape_recurse(v)) + (depth > len(dimlist))? 0 : dimlist[depth-1] ; + + + // Function: in_list() // Usage: // bool = in_list(val, list, [idx]); @@ -641,31 +694,44 @@ function list_insert(list, indices, values) = // Function: list_remove() // Usage: -// list = list_remove(list, indices); +// list = list_remove(list, ind); // Topics: List Handling // See Also: list_set(), list_insert(), list_remove_values() // Description: -// Remove all items from `list` whose indexes are in `indices`. +// If `ind` is a number remove `list[ind]` from the list. If `ind` is a list of indices +// remove from the list the item all items whose indices appear in `ind`. If you give +// indices that are not in the list they are ignored. // Arguments: // list = The list to remove items from. -// indices = The list of indexes of items to remove. +// ind = index or list of indices of items to remove. // Example: -// a = list_insert([3,6,9,12],1); // Returns: [3,9,12] -// b = list_insert([3,6,9,12],[1,3]); // Returns: [3,9] -function list_remove(list, indices) = - assert(is_list(list)) - is_finite(indices) ? - [ - for (i=[0:1:min(indices, len(list)-1)-1]) list[i], - for (i=[min(indices, len(list)-1)+1:1:len(list)-1]) list[i] - ] - : indices==[] ? list - : assert( is_vector(indices), "Invalid list `indices`." ) +// a = list_remove([3,6,9,12],1); // Returns: [3,9,12] +// b = list_remove([3,6,9,12],[1,3]); // Returns: [3,9] +// c = list_remove([3,6],3); // Returns: [3,6] +function list_remove(list, ind) = + assert(is_list(list), "Invalid list in list_remove") + is_finite(ind) ? + ( + (ind<0 || ind>=len(list)) ? list + : + [ + for (i=[0:1:ind-1]) list[i], + for (i=[ind+1:1:len(list)-1]) list[i] + ] + ) + : ind==[] ? list + : assert( is_vector(ind), "Invalid index list in list_remove") + let(sres = search(count(list),ind,1)) [ for(i=[0:len(list)-1]) - if ( []==search(i,indices,1) ) - list[i] - ]; + if (sres[i] == []) + list[i] + ]; + +// This method is faster for long lists with few values to remove +// let( rem = list_set([], indices, repeat(1,len(indices)), minlen=len(list))) +// [for(i=idx(list)) if (rem[i]==0) list[i]]; + // Function: list_remove_values() @@ -931,54 +997,6 @@ function permutations(l,n=2) = // Section: Changing list structure -// Internal. Not exposed. -function _array_dim_recurse(v) = - !is_list(v[0]) - ? len( [for(entry=v) if(!is_list(entry)) 0] ) == 0 ? [] : [undef] - : let( - firstlen = is_list(v[0]) ? len(v[0]): undef, - first = len( [for(entry = v) if(! is_list(entry) || (len(entry) != firstlen)) 0 ] ) == 0 ? firstlen : undef, - leveldown = flatten(v) - ) - is_list(leveldown[0]) - ? concat([first],_array_dim_recurse(leveldown)) - : [first]; - -function _array_dim_recurse(v) = - let( alen = [for(vi=v) is_list(vi) ? len(vi): -1] ) - v==[] || max(alen)==-1 ? [] : - let( add = max(alen)!=min(alen) ? undef : alen[0] ) - concat( add, _array_dim_recurse(flatten(v))); - - -// Function: array_dim() -// Usage: -// dims = array_dim(v, [depth]); -// Topics: Matrices, Array Handling -// Description: -// Returns the size of a multi-dimensional array. Returns a list of dimension lengths. The length -// of `v` is the dimension `0`. The length of the items in `v` is dimension `1`. The length of the -// items in the items in `v` is dimension `2`, etc. For each dimension, if the length of items at -// that depth is inconsistent, `undef` will be returned. If no items of that dimension depth exist, -// `0` is returned. Otherwise, the consistent length of items in that dimensional depth is -// returned. -// Arguments: -// v = Array to get dimensions of. -// depth = Dimension to get size of. If not given, returns a list of dimension lengths. -// Example: -// a = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]); // Returns [2,2,3] -// b = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0); // Returns 2 -// c = array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2); // Returns 3 -// d = array_dim([[[1,2,3],[4,5,6]],[[7,8,9]]]); // Returns [2,undef,3] -function array_dim(v, depth=undef) = - assert( is_undef(depth) || ( is_finite(depth) && depth>=0 ), "Invalid depth.") - ! is_list(v) ? 0 : - (depth == undef) - ? concat([len(v)], _array_dim_recurse(v)) - : (depth == 0) - ? len(v) - : let( dimlist = _array_dim_recurse(v)) - (depth > len(dimlist))? 0 : dimlist[depth-1] ; // Function: list_to_matrix() diff --git a/skin.scad b/skin.scad index 37f01cc..804d0db 100644 --- a/skin.scad +++ b/skin.scad @@ -436,7 +436,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") let( - profile_dim=array_dim(profiles,2), + profile_dim=list_shape(profiles,2), profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)), profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3 ) diff --git a/tests/test_lists.scad b/tests/test_lists.scad index bbfc541..be29703 100644 --- a/tests/test_lists.scad +++ b/tests/test_lists.scad @@ -156,9 +156,14 @@ test_list_set(); module test_list_remove() { assert(list_remove([3,6,9,12],1) == [3,9,12]); + assert(list_remove([3,6,9,12],[1]) == [3,9,12]); assert(list_remove([3,6,9,12],[1,3]) == [3,9]); assert(list_remove([3,6,9],[]) == [3,6,9]); assert(list_remove([],[]) == []); + assert(list_remove([1,2,3], -1)==[1,2,3]); + assert(list_remove([1,2,3], 3)==[1,2,3]); + assert(list_remove([1,2,3], [-1,3])==[1,2,3]); + assert(list_remove([1,2,3], [-1,1,3])==[1,3]); } test_list_remove(); @@ -402,19 +407,19 @@ module test_full_flatten() { test_full_flatten(); -module test_array_dim() { - assert(array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]) == [2,2,3]); - assert(array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0) == 2); - assert(array_dim([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2) == 3); - assert(array_dim([[[1,2,3],[4,5,6]],[[7,8,9]]]) == [2,undef,3]); - assert(array_dim([1,2,3,4,5,6,7,8,9]) == [9]); - assert(array_dim([[1],[2],[3],[4],[5],[6],[7],[8],[9]]) == [9,1]); - assert(array_dim([]) == [0]); - assert(array_dim([[]]) == [1,0]); - assert(array_dim([[],[]]) == [2,0]); - assert(array_dim([[],[1]]) == [2,undef]); +module test_list_shape() { + assert(list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]) == [2,2,3]); + assert(list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 0) == 2); + assert(list_shape([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], 2) == 3); + assert(list_shape([[[1,2,3],[4,5,6]],[[7,8,9]]]) == [2,undef,3]); + assert(list_shape([1,2,3,4,5,6,7,8,9]) == [9]); + assert(list_shape([[1],[2],[3],[4],[5],[6],[7],[8],[9]]) == [9,1]); + assert(list_shape([]) == [0]); + assert(list_shape([[]]) == [1,0]); + assert(list_shape([[],[]]) == [2,0]); + assert(list_shape([[],[1]]) == [2,undef]); } -test_array_dim(); +test_list_shape(); // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap From ea0ec797901e69ee97ab7279ec4bffb40b026127 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 28 Oct 2021 20:23:16 -0400 Subject: [PATCH 2/4] list_remove_values, in_list bugfixes remove quantization from region booleans (bugfix) --- lists.scad | 79 +++++++++++++++++++------ regions.scad | 131 +++++++++++++++++++++++------------------- tests/test_lists.scad | 17 ++++++ 3 files changed, 151 insertions(+), 76 deletions(-) diff --git a/lists.scad b/lists.scad index f8bd372..85fcf14 100644 --- a/lists.scad +++ b/lists.scad @@ -151,13 +151,25 @@ function list_shape(v, depth=undef) = // a = in_list("bar", ["foo", "bar", "baz"]); // Returns true. // b = in_list("bee", ["foo", "bar", "baz"]); // Returns false. // c = in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. + +// Note: a huge complication occurs because OpenSCAD's search() finds +// index i as a hits if the val equals list[i] but also if val equals list[i][0]. +// This means every hit needs to be checked to see if it's actually a hit, +// and if the first hit is a mismatch we have to keep searching. +// We assume that the normal case doesn't have mixed data, and try first +// with just one hit, but if this finds a mismatch then we try again +// with all hits, which could be slow for long lists. function in_list(val,list,idx) = - assert( is_list(list) && (is_undef(idx) || is_finite(idx)), - "Invalid input." ) - let( s = search([val], list, num_returns_per_match=1, index_col_num=idx)[0] ) - s==[] || s==[[]] ? false - : is_undef(idx) ? val==list[s] - : val==list[s][idx]; + assert(is_list(list),"Input is not a list") + assert(is_undef(idx) || is_finite(idx), "Invalid idx value.") + let( firsthit = search([val], list, num_returns_per_match=1, index_col_num=idx)[0] ) + firsthit==[] ? false + : is_undef(idx) && val==list[firsthit] ? true + : is_def(idx) && val==list[firsthit][idx] ? true + // first hit was found but didn't match, so try again with all hits + : let ( allhits = search([val], list, 0, idx)[0]) + is_undef(idx) ? [for(hit=allhits) if (list[hit]==val) 1] != [] + : [for(hit=allhits) if (list[hit][idx]==val) 1] != []; // Function: add_scalar() @@ -723,7 +735,7 @@ function list_remove(list, ind) = : assert( is_vector(ind), "Invalid index list in list_remove") let(sres = search(count(list),ind,1)) [ - for(i=[0:len(list)-1]) + for(i=idx(list)) if (sres[i] == []) list[i] ]; @@ -741,13 +753,22 @@ function list_remove(list, ind) = // Topics: List Handling // See Also: list_set(), list_insert(), list_remove() // Description: -// Removes the first, or all instances of the given `values` from the `list`. -// Returns the modified list. +// Removes the first, or all instances of the given value or list of values from the list. +// If you specify `all=false` and list a value twice then the first two instances will be removed. +// Note that if you want to remove a list value such as `[3,4]` then you must give it as +// a singleton list, or it will be interpreted as a list of two scalars to remove. // Arguments: // list = The list to modify. -// values = The values to remove from the list. +// values = The value or list of values to remove from the list. // all = If true, remove all instances of the value `value` from the list `list`. If false, remove only the first. Default: false // Example: +// test = [3,4,[5,6],7,5,[5,6],4,[6,5],7,[4,4]]; +// a=list_remove_values(test,4); // Returns: [3, [5, 6], 7, 5, [5, 6], 4, [6, 5], 7, [4, 4]] +// b=list_remove_values(test,[4,4]); // Returns: [3, [5, 6], 7, 5, [5, 6], [6, 5], 7, [4, 4]] +// c=list_remove_values(test,[4,7]); // Returns: [3, [5, 6], 5, [5, 6], 4, [6, 5], 7, [4, 4]] +// d=list_remove_values(test,[5,6]); // Returns: [3, 4, [5, 6], 7, [5, 6], 4, [6, 5], 7, [4, 4]] +// e=list_remove_values(test,[[5,6]]); // Returns: [3,4,7,5,[5,6],4,[6,5],7,[4,4]] +// f=list_remove_values(test,[[5,6]],all=true); // Returns: [3,4,7,5,4,[6,5],7,[4,4]] // animals = ["bat", "cat", "rat", "dog", "bat", "rat"]; // animals2 = list_remove_values(animals, "rat"); // Returns: ["bat","cat","dog","bat","rat"] // nonflying = list_remove_values(animals, "bat", all=true); // Returns: ["cat","rat","dog","rat"] @@ -755,13 +776,39 @@ function list_remove(list, ind) = // domestic = list_remove_values(animals, ["bat","rat"], all=true); // Returns: ["cat","dog"] // animals4 = list_remove_values(animals, ["tucan","rat"], all=true); // Returns: ["bat","cat","dog","bat"] function list_remove_values(list,values=[],all=false) = - assert(is_list(list)) !is_list(values)? list_remove_values(list, values=[values], all=all) : - let( - idxs = all? flatten(search(values,list,0)) : search(values,list,1), - uidxs = unique(idxs) - ) list_remove(list,uidxs); - + assert(is_list(list), "Invalid list") + len(values)==0 ? list : + len(values)==1 ? + ( + !all ? + ( + let(firsthit = search(values,list,1)[0]) + firsthit==[] ? list + : list[firsthit]==values[0] ? list_remove(list,firsthit) + : let(allhits = search(values,list,0)[0], + allind = [for(i=allhits) if (list[i]==values[0]) i] + ) + allind==[] ? list : list_remove(list,min(allind)) + ) + : + ( + let(allhits = search(values,list,0)[0], + allind = [for(i=allhits) if (list[i]==values[0]) i] + ) + allind==[] ? list : list_remove(list,allind) + ) + ) + :!all ? list_remove_values(list_remove_values(list, values[0],all=all), list_tail(values),all=all) + : + [ + for(i=idx(list)) + let(hit=search([list[i]],values,0)[0]) + if (hit==[]) list[i] + else + let(check = [for(j=hit) if (values[j]==list[i]) 1]) + if (check==[]) list[i] + ]; // Section: List Length Manipulation diff --git a/regions.scad b/regions.scad index d9f96c2..721b0e5 100644 --- a/regions.scad +++ b/regions.scad @@ -840,40 +840,51 @@ function offset( /// "S" - the subpath is on the 2nd region's border and the two regions interiors are on the same side of the subpath /// "U" - the subpath is on the 2nd region's border and the two regions meet at the subpath from opposite sides /// You specify which type of subpaths to keep with a string of the desired types such as "OS". -function _filter_region_parts(region1, region2, keep1, keep2, eps=EPSILON) = +function _filter_region_parts(region1, region2, keep, eps=EPSILON) = // We have to compute common vertices between paths in the region because // they can be places where the path must be cut, even though they aren't // found my the split_path function. let( - keep = [keep1,keep2], subpaths = split_region_at_region_crossings(region1,region2,eps=eps), - regions=[region1,region2] + regions=[force_region(region1), + force_region(region2)] ) _assemble_path_fragments( - [for(i=[0:1]) - let( - keepS = search("S",keep[i])!=[], - keepU = search("U",keep[i])!=[], - keepoutside = search("O",keep[i]) !=[], - keepinside = search("I",keep[i]) !=[], - all_subpaths = flatten(subpaths[i]) - ) - for (subpath = all_subpaths) - let( - midpt = mean([subpath[0], subpath[1]]), - rel = point_in_region(midpt,regions[1-i],eps=eps), - keepthis = rel<0 ? keepoutside - : rel>0 ? keepinside - : !(keepS || keepU) ? false - : let( - sidept = midpt + 0.01*line_normal(subpath[0],subpath[1]), - rel1 = point_in_region(sidept,region1,eps=eps)>0, - rel2 = point_in_region(sidept,region2,eps=eps)>0 - ) - rel1==rel2 ? keepS : keepU - ) - if (keepthis) subpath - ]); + [for(i=[0:1]) + let( + keepS = search("S",keep[i])!=[], + keepU = search("U",keep[i])!=[], + keepoutside = search("O",keep[i]) !=[], + keepinside = search("I",keep[i]) !=[], + all_subpaths = flatten(subpaths[i]) + ) + for (subpath = all_subpaths) + let( + midpt = mean([subpath[0], subpath[1]]), + rel = point_in_region(midpt,regions[1-i],eps=eps), + keepthis = rel<0 ? keepoutside + : rel>0 ? keepinside + : !(keepS || keepU) ? false + : let( + sidept = midpt + 0.01*line_normal(subpath[0],subpath[1]), + rel1 = point_in_region(sidept,regions[0],eps=eps)>0, + rel2 = point_in_region(sidept,regions[1],eps=eps)>0 + ) + rel1==rel2 ? keepS : keepU + ) + if (keepthis) subpath + ] + ); + + +function _list_three(a,b,c) = + is_undef(b) ? a : + [ + a, + if (is_def(b)) b, + if (is_def(c)) c + ]; + // Function&Module: union() @@ -894,12 +905,12 @@ function _filter_region_parts(region1, region2, keep1, keep2, eps=EPSILON) = // color("green") region(union(shape1,shape2)); // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); function union(regions=[],b=undef,c=undef,eps=EPSILON) = - b!=undef? union(concat([regions],[b],c==undef?[]:[c]), eps=eps) : + let(regions=_list_three(regions,b,c)) len(regions)==0? [] : len(regions)==1? regions[0] : - let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) + let(regions=[for (r=regions) is_path(r)? [r] : r]) union([ - _filter_region_parts(regions[0],regions[1],"OS", "O", eps=eps), + _filter_region_parts(regions[0],regions[1],["OS", "O"], eps=eps), for (i=[2:1:len(regions)-1]) regions[i] ], eps=eps @@ -925,17 +936,17 @@ function union(regions=[],b=undef,c=undef,eps=EPSILON) = // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); // color("green") region(difference(shape1,shape2)); function difference(regions=[],b=undef,c=undef,eps=EPSILON) = - b!=undef? difference(concat([regions],[b],c==undef?[]:[c]), eps=eps) : - len(regions)==0? [] : - len(regions)==1? regions[0] : - regions[0]==[] ? [] : - let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) - difference([ - _filter_region_parts(regions[0],regions[1],"OU", "I", eps=eps), - for (i=[2:1:len(regions)-1]) regions[i] - ], - eps=eps - ); + let(regions = _list_three(regions,b,c)) + len(regions)==0? [] + : len(regions)==1? regions[0] + : regions[0]==[] ? [] + : let(regions=[for (r=regions) is_path(r)? [r] : r]) + difference([ + _filter_region_parts(regions[0],regions[1],["OU", "I"], eps=eps), + for (i=[2:1:len(regions)-1]) regions[i] + ], + eps=eps + ); // Function&Module: intersection() @@ -956,17 +967,16 @@ function difference(regions=[],b=undef,c=undef,eps=EPSILON) = // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); // color("green") region(intersection(shape1,shape2)); function intersection(regions=[],b=undef,c=undef,eps=EPSILON) = - b!=undef? intersection(concat([regions],[b],c==undef?[]:[c]),eps=eps) - : len(regions)==0 ? [] + let(regions = _list_three(regions,b,c)) + len(regions)==0 ? [] : len(regions)==1? regions[0] : regions[0]==[] || regions[1]==[] ? [] - : let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) - intersection([ - _filter_region_parts(regions[0],regions[1],"IS","I",eps=eps), - for (i=[2:1:len(regions)-1]) regions[i] - ], - eps=eps - ); + : intersection([ + _filter_region_parts(regions[0],regions[1],["IS","I"],eps=eps), + for (i=[2:1:len(regions)-1]) regions[i] + ], + eps=eps + ); @@ -995,16 +1005,17 @@ function intersection(regions=[],b=undef,c=undef,eps=EPSILON) = // circle(d=40); // } function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) = - b!=undef? exclusive_or([regions, b, if(is_def(c)) c],eps=eps) : - len(regions)==0? [] : - len(regions)==1? regions[0] : - let(regions=[for (r=regions) is_path(r)? [r] : r]) - exclusive_or([ - _filter_region_parts(regions[0],regions[1],"IO","IO",eps=eps), - for (i=[2:1:len(regions)-1]) regions[i] - ], - eps=eps - ); + let(regions = _list_three(regions,b,c)) + len(regions)==0? [] + : len(regions)==1? regions[0] + : regions[0]==[] ? exclusive_or(list_tail(regions)) + : regions[1]==[] ? exclusive_or(list_remove(regions,1)) + : exclusive_or([ + _filter_region_parts(regions[0],regions[1],["IO","IO"],eps=eps), + for (i=[2:1:len(regions)-1]) regions[i] + ], + eps=eps + ); module exclusive_or() { diff --git a/tests/test_lists.scad b/tests/test_lists.scad index be29703..b2618cc 100644 --- a/tests/test_lists.scad +++ b/tests/test_lists.scad @@ -174,6 +174,23 @@ module test_list_remove_values() { assert(list_remove_values(animals, ["bat","rat"]) == ["cat","dog","bat","rat"]); assert(list_remove_values(animals, ["bat","rat"], all=true) == ["cat","dog"]); assert(list_remove_values(animals, ["tucan","rat"], all=true) == ["bat","cat","dog","bat"]); + + test = [3,4,[5,6],7,5,[5,6],4,[6,5],7,[4,4]]; + assert_equal(list_remove_values(test,4), [3, [5, 6], 7, 5, [5, 6], 4, [6, 5], 7, [4, 4]]); + assert_equal(list_remove_values(test,[4,4]), [3, [5, 6], 7, 5, [5, 6], [6, 5], 7, [4, 4]]); + assert_equal(list_remove_values(test,[4,7]), [3, [5, 6], 5, [5, 6], 4, [6, 5], 7, [4, 4]]); + assert_equal(list_remove_values(test,[5,6]), [3, 4, [5, 6], 7, [5, 6], 4, [6, 5], 7, [4, 4]]); + assert_equal(list_remove_values(test,[[5,6]]), [3,4,7,5,[5,6],4,[6,5],7,[4,4]]); + assert_equal(list_remove_values(test,[[5,6]],all=true), [3,4,7,5,4,[6,5],7,[4,4]]); + assert_equal(list_remove_values(test,4,all=true), [3, [5, 6], 7, 5, [5, 6], [6, 5],7, [4, 4]]); + assert_equal(list_remove_values(test,[4,7],all=true), [3, [5, 6], 5, [5, 6], [6, 5], [4, 4]]); + assert_equal(list_remove_values(test,[]),test); + assert_equal(list_remove_values(test,[],all=true),test); + assert_equal(list_remove_values(test,99), test); + assert_equal(list_remove_values(test,99,all=true), test); + assert_equal(list_remove_values(test,[99,100],all=true), test); + assert_equal(list_remove_values(test,[99,100]), test); + } test_list_remove_values(); From 9cf991bb2941d6ce44ff93fb45f4459209ead351 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 28 Oct 2021 21:07:41 -0400 Subject: [PATCH 3/4] doc tweak --- lists.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lists.scad b/lists.scad index 85fcf14..07356e9 100644 --- a/lists.scad +++ b/lists.scad @@ -152,7 +152,7 @@ function list_shape(v, depth=undef) = // b = in_list("bee", ["foo", "bar", "baz"]); // Returns false. // c = in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. -// Note: a huge complication occurs because OpenSCAD's search() finds +// Note that a huge complication occurs because OpenSCAD's search() finds // index i as a hits if the val equals list[i] but also if val equals list[i][0]. // This means every hit needs to be checked to see if it's actually a hit, // and if the first hit is a mismatch we have to keep searching. From 052200433b7ccfe8d0b876dce1fab5770441413a Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 29 Oct 2021 19:29:51 -0400 Subject: [PATCH 4/4] Make most path functions accept singleton regions Replace check_and_fix_path with force_path --- geometry.scad | 14 +++-- paths.scad | 115 +++++++++++++++++++++++++++++++++++------- regions.scad | 50 +++++------------- rounding.scad | 34 ++++++++----- skin.scad | 10 ++-- tests/test_paths.scad | 1 + 6 files changed, 150 insertions(+), 74 deletions(-) diff --git a/geometry.scad b/geometry.scad index d7c6cf2..37326e1 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1860,6 +1860,7 @@ function ccw_polygon(poly) = // Arguments: // poly = The list of the path points for the perimeter of the polygon. function reverse_polygon(poly) = + let(poly=force_path(poly,"poly")) assert(is_path(poly), "Input should be a polygon") [ poly[0], for(i=[len(poly)-1:-1:1]) poly[i] ]; @@ -1878,6 +1879,7 @@ function reverse_polygon(poly) = // Example: // polygon_shift([[3,4], [8,2], [0,2], [-4,0]], 2); // Returns [[0,2], [-4,0], [3,4], [8,2]] function polygon_shift(poly, i) = + let(poly=force_path(poly,"poly")) assert(is_path(poly), "Invalid polygon." ) list_rotate(cleanup_path(poly), i); @@ -1895,7 +1897,7 @@ function polygon_shift(poly, i) = // makes the total sum over all pairs as small as possible. Returns the reindexed polygon. Note // that the geometry of the polygon is not changed by this operation, just the labeling of its // vertices. If the input polygon is 2d and is oriented opposite the reference then its point order is -// flipped. +// reversed. // Arguments: // reference = reference polygon path // poly = input polygon to reindex @@ -1913,7 +1915,9 @@ function polygon_shift(poly, i) = // move_copies(concat(circ,pent)) circle(r=.1,$fn=32); // color("red") move_copies([pent[0],circ[0]]) circle(r=.1,$fn=32); // color("blue") translate(reindexed[0])circle(r=.1,$fn=32); - function reindex_polygon(reference, poly, return_error=false) = +function reindex_polygon(reference, poly, return_error=false) = + let(reference=force_path(reference,"reference"), + poly=force_path(poly,"poly")) assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) assert(len(reference)==len(poly), "The polygons must have the same length.") @@ -1971,6 +1975,8 @@ function polygon_shift(poly, i) = // stroke(ellipse, width=.5, closed=true); // color("blue")stroke(aligned,width=.5,closed=true); function align_polygon(reference, poly, angles, cp, trans, return_ind=false) = + let(reference=force_path(reference,"reference"), + poly=force_path(poly,"poly")) assert(is_undef(trans) || (is_undef(angles) && is_undef(cp)), "Cannot give both angles/cp and trans as input") let( trans = is_def(trans) ? trans : @@ -1978,8 +1984,8 @@ function align_polygon(reference, poly, angles, cp, trans, return_ind=false) = "The `angle` parameter must be a range or a non void list of numbers.") [for(angle=angles) zrot(angle,cp=cp)] ) - assert(is_path(reference,dim=2) && is_path(poly,dim=2), - "Invalid polygon(s). " ) + assert(is_path(reference,dim=2), "reference must be a 2D polygon") + assert(is_path(poly,dim=2), "poly must be a 2D polygon") assert(len(reference)==len(poly), "The polygons must have the same length.") let( // alignments is a vector of entries of the form: [polygon, error] alignments = [ diff --git a/paths.scad b/paths.scad index 31d85dd..fe7fd86 100644 --- a/paths.scad +++ b/paths.scad @@ -45,6 +45,39 @@ function is_path(list, dim=[2,3], fast=false) = && len(list[0])>0 && (is_undef(dim) || in_list(len(list[0]), force_list(dim))); +// Function: is_path_region() +// Usage: +// bool = is_path_region(path, [name]) +// Description: +// If `path` is a region with one component then return true. If path is a region with more components +// then display an error message about the parameter `name` requiring a path or a single component region. If the input +// is not a region then return false. This function helps accept singleton regions in functions that +// operate on a path. +// Arguments: +// path = input to process +// name = name of parameter to use in error message. Default: "path" +function is_path_region(path, name="path") = + !is_region(path)? false + :assert(len(path)==1,str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region")) + true; + +// Function: force_path() +// Usage: +// outpath = force_path(path, [name]) +// Description: +// If `path` is a region with one component then return that component as a path. If path is a region with more components +// then display an error message about the parameter `name` requiring a path or a single component region. If the input +// is not a region then return the input without any checks. This function helps accept singleton regions in functions that +// operate on a path. +// Arguments: +// path = input to process +// name = name of parameter to use in error message. Default: "path" +function force_path(path, name="path") = + is_region(path) ? + assert(len(path)==1, str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region")) + path[0] + : path; + // Function: is_closed_path() // Usage: @@ -102,6 +135,7 @@ function _path_select(path, s1, u1, s2, u2, closed=false) = ) pathout; + // Function: path_merge_collinear() // Description: // Takes a path and removes unnecessary sequential collinear points. @@ -111,8 +145,11 @@ function _path_select(path, s1, u1, s2, u2, closed=false) = // path = A list of path points of any dimension. // closed = treat as closed polygon. Default: false // eps = Largest positional variance allowed. Default: `EPSILON` (1-e9) -function path_merge_collinear(path, closed=false, eps=EPSILON) = - assert( is_path(path), "Invalid path." ) +function path_merge_collinear(path, closed, eps=EPSILON) = + is_path_region(path) ? path_merge_collinear(path[0], default(closed,true), eps) : + let(closed=default(closed,false)) + assert(is_bool(closed)) + assert( is_path(path), "Invalid path in path_merge_collinear." ) assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) len(path)<=2 ? path : let( @@ -140,7 +177,11 @@ function path_merge_collinear(path, closed=false, eps=EPSILON) = // Example: // path = [[0,0], [5,35], [60,-25], [80,0]]; // echo(path_length(path)); -function path_length(path,closed=false) = +function path_length(path,closed) = + is_path_region(path) ? path_length(path[0], default(closed,true)) : + assert(is_path(path), "Invalid path in path_length") + let(closed=default(closed,false)) + assert(is_bool(closed)) len(path)<2? 0 : sum([for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i])])+(closed?norm(path[len(path)-1]-path[0]):0); @@ -153,7 +194,11 @@ function path_length(path,closed=false) = // Arguments: // path = path to measure // closed = true if the path is closed. Default: false -function path_segment_lengths(path, closed=false) = +function path_segment_lengths(path, closed) = + is_path_region(path) ? path_segment_lengths(path[0], default(closed,true)) : + let(closed=default(closed,false)) + assert(is_path(path),"Invalid path in path_segment_lengths.") + assert(is_bool(closed)) [ for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), if (closed) norm(path[0]-last(path)) @@ -171,7 +216,9 @@ function path_segment_lengths(path, closed=false) = // Arguments: // path = path to operate on // closed = set to true if path is closed. Default: false -function path_length_fractions(path, closed=false) = +function path_length_fractions(path, closed) = + is_path_region(path) ? path_length_fractions(path[0], default(closed,true)): + let(closed=default(closed, false)) assert(is_path(path)) assert(is_bool(closed)) let( @@ -327,6 +374,7 @@ function _sum_preserving_round(data, index=0) = // mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); // move_copies(mypath)sphere(r=.1,$fn=32); function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = + let(path = force_path(path)) assert(is_path(path)) assert(method=="length" || method=="segment") assert(num_defined([N,refine]),"Must give exactly one of N and refine") @@ -385,7 +433,8 @@ function subdivide_path(path, N, refine, closed=true, exact=true, method="length // stroke(path,width=2,closed=true); // color("red") move_copies(path) circle(d=9,$fn=12); // color("blue") move_copies(spath) circle(d=5,$fn=12); -function subdivide_long_segments(path, maxlen, closed=false) = +function subdivide_long_segments(path, maxlen, closed=true) = + let(path=force_path(path)) assert(is_path(path)) assert(is_finite(maxlen)) assert(is_bool(closed)) @@ -413,8 +462,9 @@ function subdivide_long_segments(path, maxlen, closed=false) = // path = path to resample // N = Number of points in output // spacing = Approximate spacing desired -// closed = set to true if path is closed. Default: false -function resample_path(path, N, spacing, closed=false) = +// closed = set to true if path is closed. Default: true +function resample_path(path, N, spacing, closed=true) = + let(path = force_path(path)) assert(is_path(path)) assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") assert(is_bool(closed)) @@ -432,9 +482,6 @@ function resample_path(path, N, spacing, closed=false) = ]; - - - // Section: Path Geometry // Function: is_path_simple() @@ -449,8 +496,11 @@ function resample_path(path, N, spacing, closed=false) = // path = path to check // closed = set to true to treat path as a polygon. Default: false // eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9) -function is_path_simple(path, closed=false, eps=EPSILON) = +function is_path_simple(path, closed, eps=EPSILON) = + is_path_region(path) ? is_path_simple(path[0], default(closed,true), eps) : + let(closed=default(closed,false)) assert(is_path(path, 2),"Must give a 2D path") + assert(is_bool(closed)) [for(i=[0:1:len(path)-(closed?2:3)]) let(v1=path[i+1]-path[i], v2=select(path,i+2)-path[i+1], @@ -471,6 +521,7 @@ function is_path_simple(path, closed=false, eps=EPSILON) = // Arguments: // path = The path to find the closest point on. // pt = the point to find the closest point to. +// closed = // Example(2D): // path = circle(d=100,$fn=6); // pt = [20,10]; @@ -478,9 +529,13 @@ function is_path_simple(path, closed=false, eps=EPSILON) = // stroke(path, closed=true); // color("blue") translate(pt) circle(d=3, $fn=12); // color("red") translate(closest[1]) circle(d=3, $fn=12); -function path_closest_point(path, pt) = +function path_closest_point(path, pt, closed=true) = + let(path = force_path(path)) + assert(is_path(path,[2,3]), "Must give 2D or 3D path.") + assert(is_vector(pt, len(path[0])), "Input pt must be a compatible vector") + assert(is_bool(closed)) let( - pts = [for (seg=idx(path)) line_closest_point(select(path,seg,seg+1),pt,SEGMENT)], + pts = [for (seg=[0:1:len(path)-(closed?1:2)]) line_closest_point(select(path,seg,seg+1),pt,SEGMENT)], dists = [for (p=pts) norm(p-pt)], min_seg = min_index(dists) ) [min_seg, pts[min_seg]]; @@ -513,7 +568,10 @@ function path_closest_point(path, pt) = // color("purple") // for(i=[0:len(tangents)-1]) // stroke([rect[i]-tangents[i], rect[i]+tangents[i]],width=.25, endcap2="arrow2"); -function path_tangents(path, closed=false, uniform=true) = +function path_tangents(path, closed, uniform=true) = + is_path_region(path) ? path_tangents(path[0], default(closed,true), uniform) : + let(closed=default(closed,false)) + assert(is_bool(closed)) assert(is_path(path)) !uniform ? [for(t=deriv(path,closed=closed, h=path_segment_lengths(path,closed))) unit(t)] : [for(t=deriv(path,closed=closed)) unit(t)]; @@ -533,7 +591,13 @@ function path_tangents(path, closed=false, uniform=true) = // normal is not uniquely defined. In this case the function issues an error. // For 2d paths the plane is always defined so the normal fails to exist only // when the derivative is zero (in the case of repeated points). -function path_normals(path, tangents, closed=false) = +// Arguments: +// path = path to compute the normals to +// tangents = path tangents optionally supplied +// closed = if true path is treated as a polygon. Default: false +function path_normals(path, tangents, closed) = + is_path_region(path) ? path_normals(path[0], tangents, default(closed,true)) : + let(closed=default(closed,false)) assert(is_path(path,[2,3])) assert(is_bool(closed)) let( @@ -560,7 +624,11 @@ function path_normals(path, tangents, closed=false) = // curvs = path_curvature(path, [closed]); // Description: // Numerically estimate the curvature of the path (in any dimension). -function path_curvature(path, closed=false) = +function path_curvature(path, closed) = + is_path_region(path) ? path_curvature(path[0], default(closed,true)) : + let(closed=default(closed,false)) + assert(is_bool(closed)) + assert(is_path(path)) let( d1 = deriv(path, closed=closed), d2 = deriv2(path, closed=closed) @@ -579,6 +647,8 @@ function path_curvature(path, closed=false) = // Description: // Numerically estimate the torsion of a 3d path. function path_torsion(path, closed=false) = + assert(is_path(path,3), "Input path must be a 3d path") + assert(is_bool(closed)) let( d1 = deriv(path,closed=closed), d2 = deriv2(path,closed=closed), @@ -883,13 +953,16 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = // Arguments: // path = The original path to split. // cutdist = Distance or list of distances where path is cut -// closed = If true, treat the path as a closed polygon. +// closed = If true, treat the path as a closed polygon. Default: false // Example(2D,NoAxes): // path = circle(d=100); // segs = path_cut(path, [50, 200], closed=true); // rainbow(segs) stroke($item, endcaps="butt", width=3); function path_cut(path,cutdist,closed) = is_num(cutdist) ? path_cut(path,[cutdist],closed) : + is_path_region(path) ? path_cut(path[0], cutdist, default(closed,true)): + let(closed=default(closed,false)) + assert(is_bool(closed)) assert(is_vector(cutdist)) assert(last(cutdist)0, "Cut distances must be strictly positive") @@ -955,6 +1028,9 @@ function _cut_to_seg_u_form(pathcut, path, closed) = // paths = split_path_at_self_crossings(path); // rainbow(paths) stroke($item, closed=false, width=3); function split_path_at_self_crossings(path, closed=true, eps=EPSILON) = + let(path = force_path(path)) + assert(is_path(path,2), "Must give a 2D path") + assert(is_bool(closed)) let( path = cleanup_path(path, eps=eps), isects = deduplicate( @@ -1063,6 +1139,9 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) = // right(27)rainbow(polygon_parts(path)) polygon($item); // move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item); function polygon_parts(path, nonzero=false, eps=EPSILON) = + let(path = force_path(path)) + assert(is_path(path,2), "Must give 2D path") + assert(is_bool(nonzero)) let( path = cleanup_path(path, eps=eps), tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=true, eps=eps), diff --git a/regions.scad b/regions.scad index 721b0e5..662d860 100644 --- a/regions.scad +++ b/regions.scad @@ -43,42 +43,6 @@ function is_region(x) = is_list(x) && is_path(x.x); function force_region(path) = is_path(path) ? [path] : path; -// Function: check_and_fix_path() -// Usage: -// check_and_fix_path(path, [valid_dim], [closed], [name]) -// Description: -// Checks that the input is a path. If it is a region with one component, converts it to a path. -// Note that arbitrary paths must have at least two points, but closed paths need at least 3 points. -// valid_dim specfies the allowed dimension of the points in the path. -// If the path is closed, removes duplicate endpoint if present. -// Arguments: -// path = path to process -// valid_dim = list of allowed dimensions for the points in the path, e.g. [2,3] to require 2 or 3 dimensional input. If left undefined do not perform this check. Default: undef -// closed = set to true if the path is closed, which enables a check for endpoint duplication -// name = parameter name to use for reporting errors. Default: "path" -function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = - let( - path = - is_region(path)? - assert(len(path)==1,str("Region ",name," supplied as path does not have exactly one component")) - path[0] - : - assert(is_path(path), str("Input ",name," is not a path")) - path - ) - assert(len(path)>(closed?2:1),closed?str("Closed path ",name," must have at least 3 points") - :str("Path ",name," must have at least 2 points")) - let(valid=is_undef(valid_dim) || in_list(len(path[0]),force_list(valid_dim))) - assert( - valid, str( - "Input ",name," must has dimension ", len(path[0])," but dimension must be ", - is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim - ) - ) - closed && approx(path[0], last(path))? list_head(path) : path; - - - // Function: sanitize_region() // Usage: // r_fixed = sanitize_region(r, [nonzero], [eps]); @@ -150,6 +114,20 @@ function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); +// Function: region_area() +// Usage: +// area=region_area(region); +// Description: +// Computes the area of the specified valid region. (If the region is invalid and has self intersections +// the result is meaningless.) +function region_area(region) = + assert(is_region(region), "Input must be a region") + let( + parts = region_parts(region) + ) + -sum([for(R=parts, poly=R) polygon_area(poly,signed=true)]); + + // Function: is_region_simple() // Usage: // bool = is_region_simple(region, [eps]); diff --git a/rounding.scad b/rounding.scad index 45e77e2..ae2edb3 100644 --- a/rounding.scad +++ b/rounding.scad @@ -227,9 +227,7 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true let( default_k = 0.5, size=one_defined([radius, cut, joint], "radius,cut,joint"), - path = is_region(path)? - assert(len(path)==1, "Region supplied as path does not have exactly one component") - path[0] : path, + path = force_path(path), size_ok = is_num(size) || len(size)==len(path) || (!closed && len(size)==len(path)-2), k_ok = is_undef(k) || (method=="smooth" && (is_num(k) || len(k)==len(path) || (!closed && len(k)==len(path)-2))), measure = is_def(radius) ? "radius" : @@ -611,6 +609,7 @@ module path_join(paths,joint=0,k=0.5,relocate=true,closed=false) { no_module();} function path_join(paths,joint=0,k=0.5,relocate=true,closed=false)= assert(is_list(paths),"Input paths must be a list of paths") let( + paths = [for(i=idx(paths)) force_path(paths[i],str("paths[",i,"]"))], badpath = [for(j=idx(paths)) if (!is_path(paths[j])) j] ) assert(badpath==[], str("Entries in paths are not valid paths: ",badpath)) @@ -963,7 +962,10 @@ function offset_sweep( ["k", k], ["points", []], ], - path = check_and_fix_path(path, [2], closed=true), + path = force_path(path) + ) + assert(is_path(path,2), "Input path must be a 2D path") + let( clockwise = is_polygon_clockwise(path), dummy1 = _struct_valid(top,"offset_sweep","top"), dummy2 = _struct_valid(bottom,"offset_sweep","bottom"), @@ -1456,6 +1458,7 @@ function _remove_undefined_vals(list) = // right(12) // offset_stroke(path, width=1, closed=true); function offset_stroke(path, width=1, rounded=true, start="flat", end="flat", check_valid=true, quality=1, chamfer=false, closed=false) = + let(path = force_path(path)) assert(is_path(path,2),"path is not a 2d path") let(closedok = !closed || (is_undef(start) && is_undef(end))) assert(closedok, "Parameters `start` and `end` not allowed with closed path") @@ -1832,7 +1835,11 @@ module rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot, k_top, k_sides, k=0.5, splinesteps=16, h, length, l, height, debug=false) = - assert(is_path(bottom) && len(bottom)>=3) + let( + bottom = force_path(bottom,"bottom"), + top = force_path(top,"top") + ) + assert(is_path(bottom,[2,3]) && len(bottom)>=3, "bottom must be a 2D or 3D path") assert(is_num(k) && k>=0 && k<=1, "Curvature parameter k must be in interval [0,1]") let( N=len(bottom), @@ -2155,21 +2162,22 @@ module bent_cutout_mask(r, thickness, path, radius, convexity=10) { no_children($children); r = get_radius(r1=r, r2=radius); - dummy=assert(is_def(r) && r>0,"Radius of the cylinder to bend around must be positive"); - assert(is_path(path,2),"Input path must be a 2d path"); + dummy1=assert(is_def(r) && r>0,"Radius of the cylinder to bend around must be positive"); + path2 = force_path(path); + dummy2=assert(is_path(path2,2),"Input path must be a 2D path"); assert(r-thickness>0, "Thickness too large for radius"); assert(thickness>0, "Thickness must be positive"); - path = clockwise_polygon(path); + fixpath = clockwise_polygon(path2); curvepoints = arc(d=thickness, angle = [-180,0]); - profiles = [for(pt=curvepoints) _cyl_hole(r+pt.x,apply(xscale((r+pt.x)/r), offset(path,delta=thickness/2+pt.y,check_valid=false,closed=true)))]; - pathx = column(path,0); + profiles = [for(pt=curvepoints) _cyl_hole(r+pt.x,apply(xscale((r+pt.x)/r), offset(fixpath,delta=thickness/2+pt.y,check_valid=false,closed=true)))]; + pathx = column(fixpath,0); minangle = (min(pathx)-thickness/2)*360/(2*PI*r); maxangle = (max(pathx)+thickness/2)*360/(2*PI*r); mindist = (r+thickness/2)/cos((maxangle-minangle)/2); assert(maxangle-minangle<180,"Cutout angle span is too large. Must be smaller than 180."); - zmean = mean(column(path,1)); - innerzero = repeat([0,0,zmean], len(path)); - outerpt = repeat( [1.5*mindist*cos((maxangle+minangle)/2),1.5*mindist*sin((maxangle+minangle)/2),zmean], len(path)); + zmean = mean(column(fixpath,1)); + innerzero = repeat([0,0,zmean], len(fixpath)); + outerpt = repeat( [1.5*mindist*cos((maxangle+minangle)/2),1.5*mindist*sin((maxangle+minangle)/2),zmean], len(fixpath)); vnf_polyhedron(vnf_vertex_array([innerzero, each profiles, outerpt],col_wrap=true),convexity=convexity); } diff --git a/skin.scad b/skin.scad index 804d0db..888b009 100644 --- a/skin.scad +++ b/skin.scad @@ -829,8 +829,8 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") -// let(shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape")) - assert(is_path(path), "input path is not a path") + let(path = force_path(path)) + assert(is_path(path,[2,3]), "input path is not a 2D or 3D path") assert(!closed || !approx(path[0],last(path)), "Closed path includes start point at the end") let( path = path3d(path), @@ -973,8 +973,11 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1, style="min_edg : closed ? false : true, capsOK = is_bool(caps) || is_bool_list(caps,2), fullcaps = is_bool(caps) ? [caps,caps] : caps, - shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape") + shape = force_path(shape,"shape"), + path = force_path(path) ) + assert(is_path(shape,2), "shape must be a 2D path") + assert(is_path(path,2), "path must be a 2D path") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") let( @@ -1222,6 +1225,7 @@ function _smooth(data,len,closed=false,angle=false) = ) result; + // Function: rot_resample() // Usage: // rlist = rot_resample(rotlist, N, [method], [twist], [scale], [smoothlen], [long], [turns], [closed]) diff --git a/tests/test_paths.scad b/tests/test_paths.scad index 63a635a..317b4be 100644 --- a/tests/test_paths.scad +++ b/tests/test_paths.scad @@ -39,6 +39,7 @@ test_cleanup_path(); module test_path_merge_collinear() { path = [[-20,-20], [-10,-20], [0,-10], [10,0], [20,10], [20,20], [15,30]]; assert(path_merge_collinear(path) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]); + assert(path_merge_collinear([path]) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]); } test_path_merge_collinear();