From 5c239187e993a0a59d7cc29a3d6eff00a000c98b Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Fri, 31 Jul 2020 00:57:52 +0100 Subject: [PATCH 01/57] removal of duplicate definitions --- math.scad | 58 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 56 deletions(-) diff --git a/math.scad b/math.scad index 2262cac..5477512 100644 --- a/math.scad +++ b/math.scad @@ -857,7 +857,7 @@ function determinant(M) = // Description: // Returns true if A is a numeric matrix of height m and width n. If m or n // are omitted or set to undef then true is returned for any positive dimension. -// If `square` is true then the matrix is required to be square. Note if you +// If `square` is true then the matrix is required to be square. // specify m != n and require a square matrix then the result will always be false. // Arguments: // A = matrix to test @@ -1010,16 +1010,6 @@ function all(l, i=0, fail=false) = // count_true([[0,0], [1,0]]); // Returns 1. // count_true([[1,1], [1,1]]); // Returns 4. // count_true([[1,1], [1,1]], nmax=3); // Returns 3. -function count_true(l, nmax=undef, i=0, cnt=0) = - (i>=len(l) || (nmax!=undef && cnt>=nmax))? cnt : - count_true( - l=l, nmax=nmax, i=i+1, cnt=cnt+( - is_list(l[i])? count_true(l[i], nmax=nmax-cnt) : - (l[i]? 1 : 0) - ) - ); - - function count_true(l, nmax) = !is_list(l) ? !(!l) ? 1: 0 : let( c = [for( i = 0, @@ -1212,27 +1202,6 @@ function C_div(z1,z2) = // The polynomial is specified as p=[a_n, a_{n-1},...,a_1,a_0] // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. - -// Note: this should probably be recoded to use division by [1,-z], which is more accurate -// and avoids overflow with large coefficients, but requires poly_div to support complex coefficients. -function polynomial(p, z, _k, _zk, _total) = - is_undef(_k) - ? assert( is_vector(p), "Input polynomial coefficients must be a vector." ) - let(p = _poly_trim(p)) - assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) - polynomial( p, - z, - len(p)-1, - is_num(z)? 1 : [1,0], - is_num(z) ? 0 : [0,0]) - : _k==0 - ? _total + +_zk*p[0] - : polynomial( p, - z, - _k-1, - is_num(z) ? _zk*z : C_times(_zk,z), - _total+_zk*p[_k]); - function polynomial(p,z,k,total) =      is_undef(k)    ?    assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) @@ -1248,36 +1217,13 @@ function polynomial(p,z,k,total) = // Description: // Given a list of polynomials represented as real coefficient lists, with the highest degree coefficient first, // computes the coefficient list of the product polynomial. -function poly_mult(p,q) = - is_undef(q) ? - assert( is_list(p) - && []==[for(pi=p) if( !is_vector(pi) && pi!=[]) 0], - "Invalid arguments to poly_mult") - len(p)==2 ? poly_mult(p[0],p[1]) - : poly_mult(p[0], poly_mult(select(p,1,-1))) - : - _poly_trim( - [ - for(n = [len(p)+len(q)-2:-1:0]) - sum( [for(i=[0:1:len(p)-1]) - let(j = len(p)+len(q)- 2 - n - i) - if (j>=0 && j=0 && j Date: Fri, 31 Jul 2020 00:59:05 +0100 Subject: [PATCH 02/57] Minor edits in in_list --- arrays.scad | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/arrays.scad b/arrays.scad index 365649a..8f8cb45 100644 --- a/arrays.scad +++ b/arrays.scad @@ -108,14 +108,16 @@ function slice(list,start,end) = // Arguments: // val = The simple value to search for. // list = The list to search. -// idx = If given, searches the given subindexes for matches for `val`. +// idx = If given, searches the given subindex for matches for `val`. // Example: // in_list("bar", ["foo", "bar", "baz"]); // Returns true. // in_list("bee", ["foo", "bar", "baz"]); // Returns false. // in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. function in_list(val,list,idx=undef) = + 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[0]==[] ? false + s==[] || s==[[]] ? false : is_undef(idx) ? val==list[s] : val==list[s][idx]; From 3b1d5672194c3a36d78a39331988142e3c4572d2 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Fri, 31 Jul 2020 01:03:32 +0100 Subject: [PATCH 03/57] Revert "Minor edits in in_list" This reverts commit 74cc246c75b97d2203544841ed5eaf717b59782b. --- arrays.scad | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/arrays.scad b/arrays.scad index 8f8cb45..365649a 100644 --- a/arrays.scad +++ b/arrays.scad @@ -108,16 +108,14 @@ function slice(list,start,end) = // Arguments: // val = The simple value to search for. // list = The list to search. -// idx = If given, searches the given subindex for matches for `val`. +// idx = If given, searches the given subindexes for matches for `val`. // Example: // in_list("bar", ["foo", "bar", "baz"]); // Returns true. // in_list("bee", ["foo", "bar", "baz"]); // Returns false. // in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. function in_list(val,list,idx=undef) = - 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 + s==[] || s[0]==[] ? false : is_undef(idx) ? val==list[s] : val==list[s][idx]; From 046266778c1d3afccc3ec840864b5470d65341a0 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 30 Jul 2020 20:46:19 -0400 Subject: [PATCH 04/57] improved back_substitute, cleaned up a few other functions, removed some non-breaking space characters. --- math.scad | 101 ++++++++++++++++++++++-------------------------------- 1 file changed, 41 insertions(+), 60 deletions(-) diff --git a/math.scad b/math.scad index 55e662b..d91ffec 100644 --- a/math.scad +++ b/math.scad @@ -773,26 +773,26 @@ function _qr_factor(A,Q, column, m, n) = // You can supply a compatible matrix b and it will produce the solution for every column of b. Note that if you want to // solve Rx=b1 and Rx=b2 you must set b to transpose([b1,b2]) and then take the transpose of the result. If the matrix // is singular (e.g. has a zero on the diagonal) then it returns []. -function back_substitute(R, b, x=[],transpose = false) = +function back_substitute(R, b, transpose = false) = assert(is_matrix(R, square=true)) let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) - !is_vector(b) ? transpose([for(i=[0:len(b[0])-1]) back_substitute(R,subindex(b,i),transpose=transpose)]) : - transpose? - reverse(back_substitute( - [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], - reverse(b), x, false - )) : - len(x) == n ? x : - let( - ind = n - len(x) - 1 - ) - R[ind][ind] == 0 ? [] : - let( - newvalue = - len(x)==0? b[ind]/R[ind][ind] : - (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] - ) back_substitute(R, b, concat([newvalue],x)); + transpose + ? reverse(_back_substitute([for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], + reverse(b))) + : _back_substitute(R,b); + +function _back_substitute(R, b, x=[]) = + let(n=len(R)) + len(x) == n ? x + : let(ind = n - len(x) - 1) + R[ind][ind] == 0 ? [] + : let( + newvalue = len(x)==0 + ? b[ind]/R[ind][ind] + : (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] + ) + _back_substitute(R, b, concat([newvalue],x)); // Function: det2() @@ -1120,19 +1120,21 @@ function _deriv_nonuniform(data, h, closed) = // closed = boolean to indicate if the data set should be wrapped around from the end to the start. function deriv2(data, h=1, closed=false) = assert( is_consistent(data) , "Input list is not consistent or not numerical.") - assert( len(data)>=3, "Input list has less than 3 elements.") assert( is_finite(h), "The sampling `h` must be a number." ) let( L = len(data) ) - closed? [ + assert( L>=3, "Input list has less than 3 elements.") + closed + ? [ for(i=[0:1:L-1]) (data[(i+1)%L]-2*data[i]+data[(L+i-1)%L])/h/h - ] : + ] + : let( - first = L<3? undef : + first = L==3? data[0] - 2*data[1] + data[2] : L==4? 2*data[0] - 5*data[1] + 4*data[2] - data[3] : (35*data[0] - 104*data[1] + 114*data[2] - 56*data[3] + 11*data[4])/12, - last = L<3? undef : + last = L==3? data[L-1] - 2*data[L-2] + data[L-3] : L==4? -2*data[L-1] + 5*data[L-2] - 4*data[L-3] + data[L-4] : (35*data[L-1] - 104*data[L-2] + 114*data[L-3] - 56*data[L-4] + 11*data[L-5])/12 @@ -1212,34 +1214,13 @@ function C_div(z1,z2) = // The polynomial is specified as p=[a_n, a_{n-1},...,a_1,a_0] // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. - -// Note: this should probably be recoded to use division by [1,-z], which is more accurate -// and avoids overflow with large coefficients, but requires poly_div to support complex coefficients. -function polynomial(p, z, _k, _zk, _total) = - is_undef(_k) - ? assert( is_vector(p), "Input polynomial coefficients must be a vector." ) - let(p = _poly_trim(p)) - assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) - polynomial( p, - z, - len(p)-1, - is_num(z)? 1 : [1,0], - is_num(z) ? 0 : [0,0]) - : _k==0 - ? _total + +_zk*p[0] - : polynomial( p, - z, - _k-1, - is_num(z) ? _zk*z : C_times(_zk,z), - _total+_zk*p[_k]); - function polynomial(p,z,k,total) = -     is_undef(k) -   ?    assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) -        assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) -        polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) -   : k==len(p) ? total -   : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : C_times(total,z)+[p[k],0]); + is_undef(k) + ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) + assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) + polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) + : k==len(p) ? total + : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : C_times(total,z)+[p[k],0]); // Function: poly_mult() // Usage: @@ -1266,18 +1247,18 @@ function poly_mult(p,q) = ]); function poly_mult(p,q) = -    is_undef(q) ? -       len(p)==2 ? poly_mult(p[0],p[1]) -                 : poly_mult(p[0], poly_mult(select(p,1,-1))) -    : -    assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") + is_undef(q) ? + len(p)==2 ? poly_mult(p[0],p[1]) + : poly_mult(p[0], poly_mult(select(p,1,-1))) + : + assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") _poly_trim( [ -                  for(n = [len(p)+len(q)-2:-1:0]) -                      sum( [for(i=[0:1:len(p)-1]) -                           let(j = len(p)+len(q)- 2 - n - i) -                           if (j>=0 && j=0 && j Date: Fri, 31 Jul 2020 15:52:35 +0100 Subject: [PATCH 05/57] Minor edits in in_list and transpose, removal of is_simple list --- arrays.scad | 78 +++++++++--------------------------------- tests/test_arrays.scad | 8 ----- 2 files changed, 16 insertions(+), 70 deletions(-) diff --git a/arrays.scad b/arrays.scad index 365649a..84f0425 100644 --- a/arrays.scad +++ b/arrays.scad @@ -18,20 +18,6 @@ // Section: List Query Operations -// Function: is_simple_list() -// Description: -// Returns true just when all elements of `list` are simple values. -// Usage: -// is_simple_list(list) -// Arguments: -// list = The list to check. -// Example: -// a = is_simple_list([3,4,5,6,7,8,9]); Returns: true -// b = is_simple_list([3,4,5,[6],7,8]); Returns: false -function is_simple_list(list) = - is_list(list) - && []==[for(e=list) if(is_list(e)) 0]; - // Function: select() // Description: @@ -73,9 +59,6 @@ function select(list, start, end=undef) = : concat([for (i = [s:1:l-1]) list[i]], [for (i = [0:1:e]) list[i]]) ; - - - // Function: slice() // Description: // Returns a slice of a list. The first item is index 0. @@ -101,24 +84,23 @@ function slice(list,start,end) = ) [for (i=[s:1:e-1]) if (e>s) list[i]]; - - // Function: in_list() // Description: Returns true if value `val` is in list `list`. When `val==NAN` the answer will be false for any list. // Arguments: // val = The simple value to search for. // list = The list to search. -// idx = If given, searches the given subindexes for matches for `val`. +// idx = If given, searches the given subindex for matches for `val`. // Example: // in_list("bar", ["foo", "bar", "baz"]); // Returns true. // in_list("bee", ["foo", "bar", "baz"]); // Returns false. // in_list("bar", [[2,"foo"], [4,"bar"], [3,"baz"]], idx=1); // Returns true. function in_list(val,list,idx=undef) = + 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[0]==[] ? false + s==[] || s==[[]] ? false : is_undef(idx) ? val==list[s] : val==list[s][idx]; - // Function: min_index() @@ -209,7 +191,6 @@ function repeat(val, n, i=0) = [for (j=[1:1:n[i]]) repeat(val, n, i+1)]; - // Function: list_range() // Usage: // list_range(n, [s], [e]) @@ -249,7 +230,6 @@ function list_range(n=undef, s=0, e=undef, step=undef) = - // Section: List Manipulation // Function: reverse() @@ -315,8 +295,6 @@ function deduplicate(list, closed=false, eps=EPSILON) = : [for (i=[0:1:l-1]) if (i==end || !approx(list[i], list[(i+1)%l], eps)) list[i]]; - - // Function: deduplicate_indexed() // Usage: // new_idxs = deduplicate_indexed(list, indices, [closed], [eps]); @@ -351,8 +329,6 @@ function deduplicate_indexed(list, indices, closed=false, eps=EPSILON) = ]; - - // Function: repeat_entries() // Usage: // newlist = repeat_entries(list, N) @@ -390,8 +366,6 @@ function repeat_entries(list, N, exact = true) = : [for (val=reps_guess) round(val)] ) [for(i=[0:length-1]) each repeat(list[i],reps[i])]; - - // Function: list_set() @@ -431,7 +405,6 @@ function list_set(list=[],indices,values,dflt=0,minlen=0) = dflt , each repeat(dflt, minlen-max(indices)) ]; - // Function: list_insert() @@ -465,8 +438,6 @@ function list_insert(list, indices, values, _i=0) = ]; - - // Function: list_remove() // Usage: // list_remove(list, indices) @@ -489,8 +460,6 @@ function list_remove(list, indices) = if ( []==search(i,indices,1) ) list[i] ]; - - // Function: list_remove_values() // Usage: // list_remove_values(list,values,all=false) = @@ -560,8 +529,6 @@ function list_bset(indexset, valuelist, dflt=0) = ); - - // Section: List Length Manipulation // Function: list_shortest() @@ -574,7 +541,6 @@ function list_shortest(array) = min([for (v = array) len(v)]); - // Function: list_longest() // Description: // Returns the length of the longest sublist in a list of lists. @@ -624,7 +590,6 @@ function list_fit(array, length, fill) = : list_pad(array,length,fill); - // Section: List Shuffling and Sorting // Function: shuffle() @@ -679,6 +644,7 @@ function _sort_vectors2(arr) = ) concat( _sort_vectors2(lesser), equal, _sort_vectors2(greater) ); + // Sort a vector of vectors based on the first three entries of each vector // Lexicographic order, remaining entries of vector ignored function _sort_vectors3(arr) = @@ -706,7 +672,6 @@ function _sort_vectors3(arr) = ) concat( _sort_vectors3(lesser), equal, _sort_vectors3(greater) ); - // Sort a vector of vectors based on the first four entries of each vector // Lexicographic order, remaining entries of vector ignored function _sort_vectors4(arr) = @@ -737,7 +702,6 @@ function _sort_vectors4(arr) = && y[3]>pivot[3] )))))) y ] ) concat( _sort_vectors4(lesser), equal, _sort_vectors4(greater) ); - function _sort_general(arr, idx=undef) = @@ -755,7 +719,8 @@ function _sort_general(arr, idx=undef) = greater = [ for (i = [0:1:len(arr)-1]) if (compare[i] > 0) arr[i] ] ) concat(_sort_general(lesser,idx), equal, _sort_general(greater,idx)); - + + function _sort_general(arr, idx=undef) = (len(arr)<=1) ? arr : let( @@ -774,9 +739,6 @@ function _sort_general(arr, idx=undef) = concat(_sort_general(lesser,idx), equal, _sort_general(greater,idx)); - - - // Function: sort() // Usage: // sort(list, [idx]) @@ -809,7 +771,6 @@ function sort(list, idx=undef) = : _sort_general(list); - // Function: sortidx() // Description: // Given a list, calculates the sort order of the list, and returns @@ -853,6 +814,7 @@ function sortidx(list, idx=undef) = : // general case subindex(_sort_general(aug, idx=list_range(s=1,n=len(aug)-1)), 0); + function sortidx(list, idx=undef) = list==[] ? [] : let( size = array_dim(list), @@ -891,7 +853,6 @@ function unique(arr) = ]; - // Function: unique_count() // Usage: // unique_count(arr); @@ -908,8 +869,6 @@ function unique_count(arr) = [ select(arr,ind), deltas( concat(ind,[len(arr)]) ) ]; - - // Section: List Iteration Helpers // Function: idx() @@ -1104,8 +1063,6 @@ function set_union(a, b, get_indices=false) = ) [idxs, nset]; - - // Function: set_difference() // Usage: // s = set_difference(a, b); @@ -1125,7 +1082,6 @@ function set_difference(a, b) = [ for (i=idx(a)) if(found[i]==[]) a[i] ]; - // Function: set_intersection() // Usage: // s = set_intersection(a, b); @@ -1146,7 +1102,6 @@ function set_intersection(a, b) = - // Section: Array Manipulation // Function: add_scalar() @@ -1165,7 +1120,6 @@ function add_scalar(v,s) = is_finite(s) ? [for (x=v) is_list(x)? add_scalar(x,s) : is_finite(x) ? x+s: x] : v; - // Function: subindex() // Description: // For each array item, return the indexed subitem. @@ -1341,14 +1295,14 @@ function array_dim(v, depth=undef) = // Example: // transpose([3,4,5]); // Returns: [3,4,5] function transpose(arr) = - let( a0 = arr[0] ) - is_list(a0) - ? assert([for(a=arr) if(len(a)!=len(a0)) 1]==[], "The array is not a matrix." ) - [for (i=[0:1:len(a0)-1]) - [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] - : arr; - - + assert( is_list(arr) && len(arr)>0, "The array is not a vector neither a matrix." ) + is_list(arr[0]) + ? let( l0 = len(arr[0]) ) + assert([for(a=arr) if(!is_list(a) || len(a)!=l0) 1 ]==[], "The array is not a vector neither a matrix." ) + [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] + : assert( is_vector(arr), "The array is not a vector neither a matrix." ) + arr; // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index f621cd0..cdaae36 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -3,14 +3,6 @@ include <../std.scad> // Section: List Query Operations -module test_is_simple_list() { - assert(is_simple_list([1,2,3,4])); - assert(is_simple_list([])); - assert(!is_simple_list([1,2,[3,4]])); -} -test_is_simple_list(); - - module test_select() { l = [3,4,5,6,7,8,9]; assert(select(l, 5, 6) == [8,9]); From 855c1da6f1eb89687ab2100103592912112d74de Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Fri, 31 Jul 2020 15:53:06 +0100 Subject: [PATCH 06/57] Minor doc edit --- common.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.scad b/common.scad index 37ba4af..dcb73cb 100644 --- a/common.scad +++ b/common.scad @@ -135,7 +135,7 @@ function is_list_of(list,pattern) = // is_consistent(list) // Description: // Tests whether input is a list of entries which all have the same list structure -// and are filled with finite numerical data. +// and are filled with finite numerical data. It returns `true`for the empty list. // Example: // is_consistent([3,4,5]); // Returns true // is_consistent([[3,4],[4,5],[6,7]]); // Returns true From 45a8b3ec6aaac4ae51ab42a58ce3d9dc5f51e380 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 1 Aug 2020 15:54:58 -0400 Subject: [PATCH 07/57] Moved no_children to common.scad, and changed references to echo_warning in skin.scad and polyhedra.scad. --- common.scad | 15 +++++- errors.scad | 126 ------------------------------------------------- polyhedra.scad | 2 +- skin.scad | 4 +- 4 files changed, 17 insertions(+), 130 deletions(-) delete mode 100644 errors.scad diff --git a/common.scad b/common.scad index f0be6c1..d8941ee 100644 --- a/common.scad +++ b/common.scad @@ -148,7 +148,7 @@ function _list_pattern(list) = // is_consistent([[3,[3,4,[5]]], [5,[2,9,[9]]]]); // Returns true // is_consistent([[3,[3,4,[5]]], [5,[2,9,9]]]); // Returns false function is_consistent(list) = - is_list(list) && is_list_of(list, _list_pattern(list[0])); + /*is_list(list) &&*/ is_list_of(list, _list_pattern(list[0])); //Internal function @@ -346,6 +346,19 @@ function segs(r) = +// Module: no_children() +// Usage: +// no_children($children); +// Description: +// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, +// as indicated by its argument. +// Arguments: +// $children = number of children the module has. +module no_children(count) { + assert(count==0, str("Module ",parent_module(1),"() does not support child modules")); +} + + // Section: Testing Helpers diff --git a/errors.scad b/errors.scad deleted file mode 100644 index 8c174d9..0000000 --- a/errors.scad +++ /dev/null @@ -1,126 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: errors.scad -// Functions and modules to facilitate error reporting. -// To use, include this line at the top of your file: -// ``` -// use -// ``` -////////////////////////////////////////////////////////////////////// - - - -// Section: Warnings and Errors - - -// Module: no_children() -// Usage: -// no_children($children); -// Description: -// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, -// as indicated by its argument. -// Arguments: -// $children = number of children the module has. -module no_children(count) { - assert(count==0, str("Module ",parent_module(1),"() does not support child modules")); -} - - -// Function&Module: echo_error() -// Usage: -// echo_error(msg, [pfx]); -// Description: -// Emulates printing of an error message. The text will be shaded red. -// You can also use this as a function call from a function. -// Arguments: -// msg = The message to print. -// pfx = The prefix to print before `msg`. Default: `ERROR` -module echo_error(msg, pfx="ERROR") { - echo(str("

", pfx, ": ", msg, "

")); -} - -function echo_error(msg, pfx="ERROR") = - echo(str("

", pfx, ": ", msg, "

")); - - -// Function&Module: echo_warning() -// Usage: -// echo_warning(msg, [pfx]); -// Description: -// Emulates printing of a warning message. The text will be shaded yellow. -// You can also use this as a function call from a function. -// Arguments: -// msg = The message to print. -// pfx = The prefix to print before `msg`. Default: `WARNING` -module echo_warning(msg, pfx="WARNING") { - echo(str("

", pfx, ": ", msg, "

")); -} - -function echo_warning(msg, pfx="WARNING") = - echo(str("

", pfx, ": ", msg, "

")); - - -// Function&Module: deprecate() -// Usage: -// deprecate(name, [suggest]); -// Description: -// Show module deprecation warnings. -// You can also use this as a function call from a function. -// Arguments: -// name = The name of the module that is deprecated. -// suggest = If given, the module to recommend using instead. -module deprecate(name, suggest=undef) { - echo_warning(pfx="DEPRECATED", - str( - "`", name, "` is deprecated and should not be used.", - is_undef(suggest)? "" : str( - " You should use `", suggest, "` instead." - ) - ) - ); -} - -function deprecate(name, suggest=undef) = - echo_warning(pfx="DEPRECATED", - str( - "`", name, "` is deprecated and should not be used.", - is_undef(suggest)? "" : str( - " You should use `", suggest, "` instead." - ) - ) - ); - - -// Function&Module: deprecate_argument() -// Usage: -// deprecate(name, arg, [suggest]); -// Description: -// Show argument deprecation warnings. -// You can also use this as a function call from a function. -// Arguments: -// name = The name of the module/function the deprecated argument is used in. -// arg = The name of the deprecated argument. -// suggest = If given, the argument to recommend using instead. -module deprecate_argument(name, arg, suggest=undef) { - echo_warning(pfx="DEPRECATED ARG", str( - "In `", name, "`, ", - "the argument `", arg, "` ", - "is deprecated and should not be used.", - is_undef(suggest)? "" : str( - " You should use `", suggest, "` instead." - ) - )); -} - -function deprecate_argument(name, arg, suggest=undef) = - echo_warning(pfx="DEPRECATED ARG", str( - "In `", name, "`, ", - "the argument `", arg, "` ", - "is deprecated and should not be used.", - is_undef(suggest)? "" : str( - " You should use `", suggest, "` instead." - ) - )); - - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/polyhedra.scad b/polyhedra.scad index b199fe9..e845acd 100644 --- a/polyhedra.scad +++ b/polyhedra.scad @@ -710,7 +710,7 @@ function regular_polyhedron_info( info == "center" ? translation : info == "type" ? entry[class] : info == "name" ? entry[pname] : - echo_warning(str("Unknown info type '",info,"' requested")); + assert(false, str("Unknown info type '",info,"' requested")); diff --git a/skin.scad b/skin.scad index 920f4ed..30d8a17 100644 --- a/skin.scad +++ b/skin.scad @@ -1202,7 +1202,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi let (pathnormal = path_normals(path, tangents, closed)) assert(all_defined(pathnormal),"Natural normal vanishes on your curve, select a different method") let( testnormals = [for(i=[0:len(pathnormal)-1-(closed?1:2)]) pathnormal[i]*select(pathnormal,i+2)], - dummy = min(testnormals) < .5 ? echo_warning("abrupt change in normal direction. Consider a different method") :0 + dummy = min(testnormals) < .5 ? echo("WARNING: ***** Abrupt change in normal direction. Consider a different method *****") :0 ) [for(i=[0:L-(closed?0:1)]) let( rotation = affine_frame_map(x=pathnormal[i%L], z=tangents[i%L]) @@ -1216,7 +1216,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi end = reindex_polygon(start, apply(transform_list[L],path3d(shape))) ) all([for(i=idx(start)) approx(start[i],end[i])]), - dummy = ends_match ? 0 :echo_warning("The points do not match when closing the model") + dummy = ends_match ? 0 : echo("WARNING: ***** The points do not match when closing the model *****") ) transforms ? transform_list : sweep(shape, transform_list, closed=false, caps=fullcaps); From f1cd94e98645cc690938a6f6e897791cc6f3671b Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 1 Aug 2020 15:56:40 -0400 Subject: [PATCH 08/57] remove error.scad include --- std.scad | 1 - 1 file changed, 1 deletion(-) diff --git a/std.scad b/std.scad index 8375fcc..1c2e45e 100644 --- a/std.scad +++ b/std.scad @@ -14,7 +14,6 @@ include include include include -include include include include From 2da259c2cc3ca72bad07c21ede28b2ea42970799 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 2 Aug 2020 00:47:22 +0100 Subject: [PATCH 09/57] Minor is_matrix definition and format --- math.scad | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/math.scad b/math.scad index 4ae8722..5b4284a 100644 --- a/math.scad +++ b/math.scad @@ -240,7 +240,7 @@ function atanh(x) = // quant([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,12,12,12] // quant([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[12,12,12]] function quant(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") is_list(x) ? [for (v=x) quant(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -272,7 +272,7 @@ function quant(x,y) = // quantdn([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,9,9,12] // quantdn([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[9,9,12]] function quantdn(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") is_list(x) ? [for (v=x) quantdn(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -304,7 +304,7 @@ function quantdn(x,y) = // quantup([9,10,10.4,10.5,11,12],3); // Returns: [9,12,12,12,12,12] // quantup([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,12,12],[12,12,12]] function quantup(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") is_list(x) ? [for (v=x) quantup(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -778,22 +778,19 @@ function back_substitute(R, b, x=[],transpose = false) = let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) !is_vector(b) ? transpose([for(i=[0:len(b[0])-1]) back_substitute(R,subindex(b,i),transpose=transpose)]) : - transpose? - reverse(back_substitute( - [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], - reverse(b), x, false - )) : - len(x) == n ? x : - let( - ind = n - len(x) - 1 - ) - R[ind][ind] == 0 ? [] : - let( - newvalue = - len(x)==0? b[ind]/R[ind][ind] : - (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] - ) back_substitute(R, b, concat([newvalue],x)); - + transpose +    ? let( R = [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]] ) + reverse( back_substitute( R, reverse(b), x ) )  + : len(x) == n ? x : + let( + ind = n - len(x) - 1 + ) + R[ind][ind] == 0 ? [] : + let( + newvalue = + len(x)==0? b[ind]/R[ind][ind] : + (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] + ) back_substitute(R, b, concat([newvalue],x)); // Function: det2() // Description: @@ -865,13 +862,16 @@ function determinant(M) = // n = optional width of matrix // square = set to true to require a square matrix. Default: false function is_matrix(A,m,n,square=false) = - is_list(A[0]) -    && ( let(v = A*A[0]) is_num(0*(v*v)) ) // a matrix of finite numbers + is_list(A) + && len(A)>0 + && is_vector(A[0]) +    && is_vector(A*A[0]) // a matrix of finite numbers     && (is_undef(n) || len(A[0])==n )     && (is_undef(m) || len(A)==m )     && ( !square || len(A)==len(A[0])); + // Section: Comparisons and Logic // Function: approx() @@ -1037,7 +1037,7 @@ function count_true(l, nmax) = // data[len(data)-1]. This function uses a symetric derivative approximation // for internal points, f'(t) = (f(t+h)-f(t-h))/2h. For the endpoints (when closed=false) the algorithm // uses a two point method if sufficient points are available: f'(t) = (3*(f(t+h)-f(t)) - (f(t+2*h)-f(t+h)))/2h. -// . +// // If `h` is a vector then it is assumed to be nonuniform, with h[i] giving the sampling distance // between data[i+1] and data[i], and the data values will be linearly resampled at each corner // to produce a uniform spacing for the derivative estimate. At the endpoints a single point method From 84fa648dc505398fd4d2ab95c7d457b4cacc7b62 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 2 Aug 2020 01:08:24 +0100 Subject: [PATCH 10/57] Revert "Minor is_matrix definition and format" This reverts commit 2da259c2cc3ca72bad07c21ede28b2ea42970799. --- math.scad | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/math.scad b/math.scad index 5b4284a..4ae8722 100644 --- a/math.scad +++ b/math.scad @@ -240,7 +240,7 @@ function atanh(x) = // quant([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,12,12,12] // quant([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[12,12,12]] function quant(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") is_list(x) ? [for (v=x) quant(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -272,7 +272,7 @@ function quant(x,y) = // quantdn([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,9,9,12] // quantdn([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,9,9],[9,9,12]] function quantdn(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") is_list(x) ? [for (v=x) quantdn(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -304,7 +304,7 @@ function quantdn(x,y) = // quantup([9,10,10.4,10.5,11,12],3); // Returns: [9,12,12,12,12,12] // quantup([[9,10,10.4],[10.5,11,12]],3); // Returns: [[9,12,12],[12,12,12]] function quantup(x,y) = - assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero integer.") + assert(is_finite(y) && !approx(y,0,eps=1e-24), "The multiple must be a non zero number.") is_list(x) ? [for (v=x) quantup(v,y)] : assert( is_finite(x), "The input to quantize must be a number or a list of numbers.") @@ -778,19 +778,22 @@ function back_substitute(R, b, x=[],transpose = false) = let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) !is_vector(b) ? transpose([for(i=[0:len(b[0])-1]) back_substitute(R,subindex(b,i),transpose=transpose)]) : - transpose -    ? let( R = [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]] ) - reverse( back_substitute( R, reverse(b), x ) )  - : len(x) == n ? x : - let( - ind = n - len(x) - 1 - ) - R[ind][ind] == 0 ? [] : - let( - newvalue = - len(x)==0? b[ind]/R[ind][ind] : - (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] - ) back_substitute(R, b, concat([newvalue],x)); + transpose? + reverse(back_substitute( + [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], + reverse(b), x, false + )) : + len(x) == n ? x : + let( + ind = n - len(x) - 1 + ) + R[ind][ind] == 0 ? [] : + let( + newvalue = + len(x)==0? b[ind]/R[ind][ind] : + (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] + ) back_substitute(R, b, concat([newvalue],x)); + // Function: det2() // Description: @@ -862,16 +865,13 @@ function determinant(M) = // n = optional width of matrix // square = set to true to require a square matrix. Default: false function is_matrix(A,m,n,square=false) = - is_list(A) - && len(A)>0 - && is_vector(A[0]) -    && is_vector(A*A[0]) // a matrix of finite numbers + is_list(A[0]) +    && ( let(v = A*A[0]) is_num(0*(v*v)) ) // a matrix of finite numbers     && (is_undef(n) || len(A[0])==n )     && (is_undef(m) || len(A)==m )     && ( !square || len(A)==len(A[0])); - // Section: Comparisons and Logic // Function: approx() @@ -1037,7 +1037,7 @@ function count_true(l, nmax) = // data[len(data)-1]. This function uses a symetric derivative approximation // for internal points, f'(t) = (f(t+h)-f(t-h))/2h. For the endpoints (when closed=false) the algorithm // uses a two point method if sufficient points are available: f'(t) = (3*(f(t+h)-f(t)) - (f(t+2*h)-f(t+h)))/2h. -// +// . // If `h` is a vector then it is assumed to be nonuniform, with h[i] giving the sampling distance // between data[i+1] and data[i], and the data values will be linearly resampled at each corner // to produce a uniform spacing for the derivative estimate. At the endpoints a single point method From 764420e71de0a2724dab4212e66674e5ffb73c51 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 2 Aug 2020 01:15:07 +0100 Subject: [PATCH 11/57] Minor edits in is_matrix --- math.scad | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/math.scad b/math.scad index 4ae8722..9c8e717 100644 --- a/math.scad +++ b/math.scad @@ -773,26 +773,26 @@ function _qr_factor(A,Q, column, m, n) = // You can supply a compatible matrix b and it will produce the solution for every column of b. Note that if you want to // solve Rx=b1 and Rx=b2 you must set b to transpose([b1,b2]) and then take the transpose of the result. If the matrix // is singular (e.g. has a zero on the diagonal) then it returns []. -function back_substitute(R, b, x=[],transpose = false) = +function back_substitute(R, b, transpose = false) = assert(is_matrix(R, square=true)) let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) - !is_vector(b) ? transpose([for(i=[0:len(b[0])-1]) back_substitute(R,subindex(b,i),transpose=transpose)]) : - transpose? - reverse(back_substitute( - [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], - reverse(b), x, false - )) : - len(x) == n ? x : - let( - ind = n - len(x) - 1 - ) - R[ind][ind] == 0 ? [] : - let( - newvalue = - len(x)==0? b[ind]/R[ind][ind] : - (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] - ) back_substitute(R, b, concat([newvalue],x)); + transpose + ? reverse(_back_substitute([for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], + reverse(b))) + : _back_substitute(R,b); + +function _back_substitute(R, b, x=[]) = + let(n=len(R)) + len(x) == n ? x + : let(ind = n - len(x) - 1) + R[ind][ind] == 0 ? [] + : let( + newvalue = len(x)==0 + ? b[ind]/R[ind][ind] + : (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] + ) + _back_substitute(R, b, concat([newvalue],x)); // Function: det2() @@ -865,8 +865,10 @@ function determinant(M) = // n = optional width of matrix // square = set to true to require a square matrix. Default: false function is_matrix(A,m,n,square=false) = - is_list(A[0]) -    && ( let(v = A*A[0]) is_num(0*(v*v)) ) // a matrix of finite numbers + is_list(A) + && len(A)>0 + && is_vector(A[0]) +    && is_vector(A*A[0]) // a matrix of finite numbers     && (is_undef(n) || len(A[0])==n )     && (is_undef(m) || len(A)==m )     && ( !square || len(A)==len(A[0])); From e2817ae64d94db998b134c656acb905b4665d3d0 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 01:18:33 -0400 Subject: [PATCH 12/57] removed references to errors.scad --- scripts/make_all_docs.sh | 2 +- tests/test_errors.scad | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 tests/test_errors.scad diff --git a/scripts/make_all_docs.sh b/scripts/make_all_docs.sh index ddd9937..fa05079 100755 --- a/scripts/make_all_docs.sh +++ b/scripts/make_all_docs.sh @@ -19,7 +19,7 @@ done if [[ "$FILES" != "" ]]; then PREVIEW_LIBS="$FILES" else - PREVIEW_LIBS="affine arrays attachments beziers bottlecaps common constants coords cubetruss debug distributors edges errors geometry hingesnaps hull involute_gears joiners knurling linear_bearings masks math metric_screws mutators nema_steppers partitions paths phillips_drive polyhedra primitives quaternions queues regions rounding screws shapes shapes2d skin sliders stacks strings structs threading torx_drive transforms triangulation vectors version vnf walls wiring" + PREVIEW_LIBS="affine arrays attachments beziers bottlecaps common constants coords cubetruss debug distributors edges geometry hingesnaps hull involute_gears joiners knurling linear_bearings masks math metric_screws mutators nema_steppers partitions paths phillips_drive polyhedra primitives quaternions queues regions rounding screws shapes shapes2d skin sliders stacks strings structs threading torx_drive transforms triangulation vectors version vnf walls wiring" fi dir="$(basename $PWD)" diff --git a/tests/test_errors.scad b/tests/test_errors.scad deleted file mode 100644 index fffe9eb..0000000 --- a/tests/test_errors.scad +++ /dev/null @@ -1,11 +0,0 @@ -include <../std.scad> - - -// Can't test echo output as yet. Include these for coverage calculations. -module test_echo_error() {} -module test_echo_warning() {} -module test_deprecate() {} -module test_deprecate_argument() {} - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap From 817bdffdef0ef66eaa528cbef750d3a6d19c0cce Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 01:20:11 -0400 Subject: [PATCH 13/57] Removed bogus median function --- math.scad | 11 ----------- regions.scad | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/math.scad b/math.scad index d91ffec..7bc7924 100644 --- a/math.scad +++ b/math.scad @@ -641,17 +641,6 @@ function mean(v) = sum(v)/len(v); -// Function: median() -// Usage: -// x = median(v); -// Description: -// Given a list of numbers or vectors, finds the median value or midpoint. -// If passed a list of vectors, returns the vector of the median of each component. -function median(v) = - is_vector(v) ? (min(v)+max(v))/2 : - is_matrix(v) ? [for(ti=transpose(v)) (min(ti)+max(ti))/2 ] - : assert(false , "Invalid input."); - // Function: convolve() // Usage: // x = convolve(p,q); diff --git a/regions.scad b/regions.scad index 6169337..fa761f0 100644 --- a/regions.scad +++ b/regions.scad @@ -339,7 +339,7 @@ function region_faces(region, transform, reverse=false, vnf=EMPTY_VNF) = // linear_sweep(orgn,height=20,convexity=16) show_anchors(); module linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, style="default", convexity, anchor_isect=false, anchor, spin=0, orient=UP) { region = is_path(region)? [region] : region; - cp = median(flatten(region)); + cp = mean(pointlist_bounds(flatten(region))); anchor = get_anchor(anchor, center, "origin", "origin"); vnf = linear_sweep( region, height=height, From 47d02ae7834174ea6dc5266a5a6d13b66671e9ba Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 01:23:15 -0400 Subject: [PATCH 14/57] removed median tests --- tests/test_math.scad | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_math.scad b/tests/test_math.scad index f1b36b9..e5b1a41 100644 --- a/tests/test_math.scad +++ b/tests/test_math.scad @@ -391,11 +391,13 @@ module test_mean() { } test_mean(); +/* module test_median() { assert_equal(median([2,3,7]), 4.5); assert_equal(median([[1,2,3], [3,4,5], [8,9,10]]), [4.5,5.5,6.5]); } test_median(); +*/ module test_convolve() { From 37ca6efebb6fd144b72a51c39d9f13c212b58e7c Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 10:35:32 -0400 Subject: [PATCH 15/57] Modified subindex to be faster. Note also changed behavior for idx=singleton list. --- arrays.scad | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/arrays.scad b/arrays.scad index 36841fe..1a10b9f 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1172,25 +1172,25 @@ function add_scalar(v,s) = // Function: subindex() +// Usage: +// subindex(M, idx) // Description: -// For each array item, return the indexed subitem. -// Returns a list of the values of each vector at the specfied -// index list or range. If the index list or range has -// only one entry the output list is flattened. +// Extracts the entries listed in idx from each entry in M. For a matrix this means +// selecting a specified set of columsn. If idx is a number the return is a vector, otherwise +// it is a list of lists (the submatrix). // Arguments: -// v = The given list of lists. +// M = The given list of lists. // idx = The index, list of indices, or range of indices to fetch. // Example: -// v = [[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; -// subindex(v,2); // Returns [3, 7, 11, 15] -// subindex(v,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] -// subindex(v,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] -function subindex(v, idx) = - [ for(val=v) - let( value=[for(i=idx) val[i]] ) - len(value)==1 ? value[0] : value - ]; - +// M = [[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; +// subindex(M,2); // Returns [3, 7, 11, 15] +// subindex(M,[2]); // Returns [[3], [7], [11], [15]] +// subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] +// subindex(M,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] +function subindex(v, idx) = + is_num(idx) + ? [for(row=M) row[idx]] + : [for(row=M) [for(i=idx) row[i]]]; // Function: zip() // Usage: From 06b69475cb8d1bef5794e3dcff2d0e2945af3ec9 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 10:38:11 -0400 Subject: [PATCH 16/57] Fix bug. Add test for subindex. --- arrays.scad | 2 +- tests/test_arrays.scad | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/arrays.scad b/arrays.scad index 1a10b9f..0b7b924 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1187,7 +1187,7 @@ function add_scalar(v,s) = // subindex(M,[2]); // Returns [[3], [7], [11], [15]] // subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] // subindex(M,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] -function subindex(v, idx) = +function subindex(M, idx) = is_num(idx) ? [for(row=M) row[idx]] : [for(row=M) [for(i=idx) row[i]]]; diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index f621cd0..4fffb7d 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -358,6 +358,7 @@ test_add_scalar(); module test_subindex() { v = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; assert(subindex(v,2) == [3, 7, 11, 15]); + assert(subindex(v,[2]) == [[3], [7], [11], [15]]); assert(subindex(v,[2,1]) == [[3, 2], [7, 6], [11, 10], [15, 14]]); assert(subindex(v,[1:3]) == [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]]); } From 8f1162e7bf0cb1a30b9ab8758161d2f0c98ede0a Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 2 Aug 2020 10:38:33 -0400 Subject: [PATCH 17/57] doc typo fix --- arrays.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrays.scad b/arrays.scad index 0b7b924..4f1c317 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1182,7 +1182,7 @@ function add_scalar(v,s) = // M = The given list of lists. // idx = The index, list of indices, or range of indices to fetch. // Example: -// M = [[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; +// M = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; // subindex(M,2); // Returns [3, 7, 11, 15] // subindex(M,[2]); // Returns [[3], [7], [11], [15]] // subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] From e56f953c1cd8db7e7e198e6d7d49650f133ab92a Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sun, 2 Aug 2020 23:23:50 -0700 Subject: [PATCH 18/57] Fixed calls to vmul() with heteerogenous vector sizes. --- attachments.scad | 21 +++++++++++---------- distributors.scad | 2 +- math.scad | 2 +- partitions.scad | 8 ++++++-- shapes2d.scad | 2 +- transforms.scad | 8 ++++---- vectors.scad | 5 +---- version.scad | 2 +- 8 files changed, 26 insertions(+), 24 deletions(-) diff --git a/attachments.scad b/attachments.scad index 8775832..6004e29 100644 --- a/attachments.scad +++ b/attachments.scad @@ -285,7 +285,7 @@ function attach_geom_size(geom) = ) [2*maxxr,2*maxyr,l] ) : type == "spheroid"? ( //r let( r=geom[1] ) - is_num(r)? [2,2,2]*r : vmul([2,2,2],r) + is_num(r)? [2,2,2]*r : vmul([2,2,2],point3d(r)) ) : type == "vnf_extent" || type=="vnf_isect"? ( //vnf let( mm = pointlist_bounds(geom[1][0]), @@ -298,7 +298,7 @@ function attach_geom_size(geom) = ) [maxx, size.y] ) : type == "circle"? ( //r let( r=geom[1] ) - is_num(r)? [2,2]*r : vmul([2,2],r) + is_num(r)? [2,2]*r : vmul([2,2],point2d(r)) ) : type == "path_isect" || type == "path_extent"? ( //path let( mm = pointlist_bounds(geom[1]), @@ -430,8 +430,8 @@ function find_anchor(anchor, geom) = ) : type == "cyl"? ( //r1, r2, l, shift let( rr1=geom[1], rr2=geom[2], l=geom[3], shift=point2d(geom[4]), - r1 = is_num(rr1)? [rr1,rr1] : rr1, - r2 = is_num(rr2)? [rr2,rr2] : rr2, + r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1), + r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2), u = (anchor.z+1)/2, axy = unit(point2d(anchor),[0,0]), bot = point3d(vmul(r1,axy), -l/2), @@ -447,9 +447,9 @@ function find_anchor(anchor, geom) = ) : type == "spheroid"? ( //r let( rr = geom[1], - r = is_num(rr)? [rr,rr,rr] : rr, + r = is_num(rr)? [rr,rr,rr] : point3d(rr), anchor = unit(point3d(anchor),CENTER), - pos = point3d(cp) + vmul(r,anchor) + offset, + pos = point3d(cp) + vmul(r,anchor) + point3d(offset), vec = unit(vmul(r,anchor),UP) ) [anchor, pos, vec, oang] ) : type == "vnf_isect"? ( //vnf @@ -519,10 +519,10 @@ function find_anchor(anchor, geom) = ) : type == "circle"? ( //r let( rr = geom[1], - r = is_num(rr)? [rr,rr] : rr, - pos = point2d(cp) + vmul(r,anchor) + offset, + r = is_num(rr)? [rr,rr] : point2d(rr), anchor = unit(point2d(anchor),[0,0]), - vec = unit(vmul([r.y,r.x],anchor),[0,1]) + pos = point2d(cp) + vmul(r,anchor) + point2d(offset), + vec = unit(vmul(r,anchor),[0,1]) ) [anchor, pos, vec, 0] ) : type == "path_isect"? ( //path let( @@ -970,7 +970,8 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) { $attach_anchor = anch; $attach_norot = true; $tags = "mask"; - length = sum(vmul($parent_size, [for (x=vec) x?0:1]))+0.1; + psize = point3d($parent_size); + length = [for (i=[0:2]) if(!vec[i]) psize[i]][0]+0.1; rotang = vec.z<0? [90,0,180+vang(point2d(vec))] : vec.z==0 && sign(vec.x)==sign(vec.y)? 135+vang(point2d(vec)) : diff --git a/distributors.scad b/distributors.scad index 5738033..787693e 100644 --- a/distributors.scad +++ b/distributors.scad @@ -949,7 +949,7 @@ module ovoid_spread(r=undef, d=undef, n=100, cone_ang=90, scale=[1,1,1], perp=tr for ($idx = idx(theta_phis)) { tp = theta_phis[$idx]; xyz = spherical_to_xyz(r, tp[0], tp[1]); - $pos = vmul(xyz,scale); + $pos = vmul(xyz,point3d(scale,1)); $theta = tp[0]; $phi = tp[1]; $rad = r; diff --git a/math.scad b/math.scad index 7bc7924..e9b8579 100644 --- a/math.scad +++ b/math.scad @@ -36,7 +36,7 @@ NAN = acos(2); // The value `nan`, useful for comparisons. function sqr(x) = is_list(x) ? [for(val=x) sqr(val)] : is_finite(x) ? x*x : - assert(is_finite(x) || is_vector(x), "Input is not neither a number nor a list of numbers."); + assert(is_finite(x) || is_vector(x), "Input is not a number nor a list of numbers."); // Function: log2() diff --git a/partitions.scad b/partitions.scad index 534219b..c46134c 100644 --- a/partitions.scad +++ b/partitions.scad @@ -31,9 +31,13 @@ _partition_cutpaths = [ function _partition_cutpath(l, h, cutsize, cutpath, gap) = let( + check = assert(is_finite(l)) + assert(is_finite(h)) + assert(is_finite(gap)) + assert(is_finite(cutsize) || is_vector(cutsize,2)) + assert(is_string(cutpath) || is_path(cutpath,2)), cutsize = is_vector(cutsize)? cutsize : [cutsize*2, cutsize], cutpath = is_path(cutpath)? cutpath : ( - assert(is_string(cutpath), "cutpath must be a 2D path or a string.") let(idx = search([cutpath], _partition_cutpaths)) idx==[[]]? assert(in_list(cutpath,_partition_cutpaths,idx=0)) : _partition_cutpaths[idx.x][1] @@ -79,7 +83,7 @@ function _partition_cutpath(l, h, cutsize, cutpath, gap) = // partition_mask(w=20, cutpath="jigsaw"); module partition_mask(l=100, w=100, h=100, cutsize=10, cutpath=undef, gap=0, inverse=false, spin=0, orient=UP) { - cutsize = is_vector(cutsize)? cutsize : [cutsize*2, cutsize]; + cutsize = is_vector(cutsize)? point2d(cutsize) : [cutsize*2, cutsize]; path = _partition_cutpath(l, h, cutsize, cutpath, gap); fullpath = concat(path, [[l/2,w*(inverse?-1:1)], [-l/2,w*(inverse?-1:1)]]); rot(from=UP,to=orient) { diff --git a/shapes2d.scad b/shapes2d.scad index 4c13d91..4c11efd 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -785,7 +785,7 @@ function rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) = assert(is_num(rounding) || len(rounding)==4) let( size = is_num(size)? [size,size] : point2d(size), - anchor = get_anchor(anchor, center, FRONT+LEFT, FRONT+LEFT), + anchor = point2d(get_anchor(anchor, center, FRONT+LEFT, FRONT+LEFT)), complex = rounding!=0 || chamfer!=0 ) (rounding==0 && chamfer==0)? let( diff --git a/transforms.scad b/transforms.scad index be73fbc..965f5de 100644 --- a/transforms.scad +++ b/transforms.scad @@ -558,12 +558,12 @@ function scale(v=1, p=undef) = len(v)==2? affine2d_scale(v) : affine3d_scale(point3d(v)) ) : ( assert(is_list(p)) - is_num(p.x)? vmul(p,v) : + is_vector(p)? ( len(p)==2? vmul(p,point2d(v)) : vmul(p,point3d(v,1)) ) : is_vnf(p)? let(inv=product([for (x=v) x<0? -1 : 1])) [ - scale(v=v,p=p.x), - inv>=0? p.y : [for (l=p.y) reverse(l)] + scale(v=v, p=p[0]), + inv>=0? p[1] : [for (l=p[1]) reverse(l)] ] : - [for (l=p) is_vector(l)? vmul(l,v) : scale(v=v, p=l)] + [ for (pp=p) scale(v=v, p=pp) ] ); diff --git a/vectors.scad b/vectors.scad index 80909b2..1749981 100644 --- a/vectors.scad +++ b/vectors.scad @@ -64,13 +64,10 @@ function vang(v) = // Example: // vmul([3,4,5], [8,7,6]); // Returns [24, 28, 30] function vmul(v1, v2) = -// this thighter check can be done yet because it would break other codes in the library -// assert( is_vector(v1) && is_vector(v2,len(v1)), "Incompatible vectors") - assert( is_vector(v1) && is_vector(v2), "Invalid vector(s)") + assert( is_vector(v1) && is_vector(v2,len(v1)), "Incompatible vectors") [for (i = [0:1:len(v1)-1]) v1[i]*v2[i]]; - // Function: vdiv() // Description: // Element-wise vector division. Divides each element of vector `v1` by diff --git a/version.scad b/version.scad index 6e03aa9..e6ba60b 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,401]; +BOSL_VERSION = [2,0,402]; // Section: BOSL Library Version Functions From e4bd6238b4399a53767be9a89105b32871486d2a Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 4 Aug 2020 00:08:23 +0100 Subject: [PATCH 19/57] Revert "Minor edits in is_matrix" This reverts commit 764420e71de0a2724dab4212e66674e5ffb73c51. --- math.scad | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/math.scad b/math.scad index 9c8e717..4ae8722 100644 --- a/math.scad +++ b/math.scad @@ -773,26 +773,26 @@ function _qr_factor(A,Q, column, m, n) = // You can supply a compatible matrix b and it will produce the solution for every column of b. Note that if you want to // solve Rx=b1 and Rx=b2 you must set b to transpose([b1,b2]) and then take the transpose of the result. If the matrix // is singular (e.g. has a zero on the diagonal) then it returns []. -function back_substitute(R, b, transpose = false) = +function back_substitute(R, b, x=[],transpose = false) = assert(is_matrix(R, square=true)) let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) - transpose - ? reverse(_back_substitute([for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], - reverse(b))) - : _back_substitute(R,b); - -function _back_substitute(R, b, x=[]) = - let(n=len(R)) - len(x) == n ? x - : let(ind = n - len(x) - 1) - R[ind][ind] == 0 ? [] - : let( - newvalue = len(x)==0 - ? b[ind]/R[ind][ind] - : (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] - ) - _back_substitute(R, b, concat([newvalue],x)); + !is_vector(b) ? transpose([for(i=[0:len(b[0])-1]) back_substitute(R,subindex(b,i),transpose=transpose)]) : + transpose? + reverse(back_substitute( + [for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], + reverse(b), x, false + )) : + len(x) == n ? x : + let( + ind = n - len(x) - 1 + ) + R[ind][ind] == 0 ? [] : + let( + newvalue = + len(x)==0? b[ind]/R[ind][ind] : + (b[ind]-select(R[ind],ind+1,-1) * x)/R[ind][ind] + ) back_substitute(R, b, concat([newvalue],x)); // Function: det2() @@ -865,10 +865,8 @@ function determinant(M) = // n = optional width of matrix // square = set to true to require a square matrix. Default: false function is_matrix(A,m,n,square=false) = - is_list(A) - && len(A)>0 - && is_vector(A[0]) -    && is_vector(A*A[0]) // a matrix of finite numbers + is_list(A[0]) +    && ( let(v = A*A[0]) is_num(0*(v*v)) ) // a matrix of finite numbers     && (is_undef(n) || len(A[0])==n )     && (is_undef(m) || len(A)==m )     && ( !square || len(A)==len(A[0])); From e06519bbfb4026dd51e6518900f688c797149fc9 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 4 Aug 2020 00:38:36 +0100 Subject: [PATCH 20/57] changes to hide recursion args and avoid repetitive arg validations cumsum broken in two to hide recursion args and avoid repetitive arg validations. back_substitute changed to avoid repetitive arg validations in the recursion. minor change in deriv2 and deriv3 to avoid an unecessary call to is_matrix. change in is_matrix for better performance any() and all() broken in two to avoid repetitive arg validation in the recursion and to hide recursion args. change in polymult to call convolve break of poly_div in two to avoid repetitive arg validations in the recursion. --- math.scad | 143 +++++++++++++++++++++++-------------------- tests/test_math.scad | 12 ++-- 2 files changed, 82 insertions(+), 73 deletions(-) diff --git a/math.scad b/math.scad index f112c19..7e16bbe 100644 --- a/math.scad +++ b/math.scad @@ -84,7 +84,7 @@ function hypot(x,y,z=0) = // y = factorial(6); // Returns: 720 // z = factorial(9); // Returns: 362880 function factorial(n,d=0) = - assert(is_int(n) && is_int(d) && n>=0 && d>=0, "Factorial is not defined for negative numbers") + assert(is_int(n) && is_int(d) && n>=0 && d>=0, "Factorial is defined only for non negative integers") assert(d<=n, "d cannot be larger than n") product([1,for (i=[n:-1:d+1]) i]); @@ -164,7 +164,7 @@ function binomial_coefficient(n,k) = function lerp(a,b,u) = assert(same_shape(a,b), "Bad or inconsistent inputs to lerp") is_finite(u)? (1-u)*a + u*b : - assert(is_finite(u) || is_vector(u) || valid_range(u), "Input u to lerp must be a number, vector, or range.") + assert(is_finite(u) || is_vector(u) || valid_range(u), "Input u to lerp must be a number, vector, or valid range.") [for (v = u) (1-v)*a + v*b ]; @@ -387,12 +387,13 @@ function modang(x) = // modrange(90,270,360, step=-45); // Returns: [90,45,0,315,270] // modrange(270,90,360, step=-45); // Returns: [270,225,180,135,90] function modrange(x, y, m, step=1) = - assert( is_finite(x+y+step+m) && !approx(m,0), "Input must be finite numbers. The module value cannot be zero.") + assert( is_finite(x+y+step+m) && !approx(m,0), "Input must be finite numbers and the module value cannot be zero." ) let( a = posmod(x, m), b = posmod(y, m), - c = step>0? (a>b? b+m : b) : (a0? (a>b? b+m : b) + : (a=len(v) ? _total : _sum(v,_total+v[_i], _i+1); // cumsum([2,2,2]); // returns [2,4,6] // cumsum([1,2,3]); // returns [1,3,6] // cumsum([[1,2,3], [3,4,5], [5,6,7]]); // returns [[1,2,3], [4,6,8], [9,12,15]] -function cumsum(v,_i=0,_acc=[]) = +function cumsum(v) = + assert(is_consistent(v), "The input is not consistent." ) + _cumsum(v,_i=0,_acc=[]); + +function _cumsum(v,_i=0,_acc=[]) = _i==len(v) ? _acc : - cumsum( + _cumsum( v, _i+1, concat( _acc, @@ -598,7 +603,7 @@ function deltas(v) = // Description: // Returns the product of all entries in the given list. // If passed a list of vectors of same dimension, returns a vector of products of each part. -// If passed a list of square matrices, returns a the resulting product matrix. +// If passed a list of square matrices, returns the resulting product matrix. // Arguments: // v = The list to get the product of. // Example: @@ -606,7 +611,7 @@ function deltas(v) = // product([[1,2,3], [3,4,5], [5,6,7]]); // returns [15, 48, 105] function product(v) = assert( is_vector(v) || is_matrix(v) || ( is_matrix(v[0],square=true) && is_consistent(v)), - "Invalid input.") + "Invalid input.") _product(v, 1, v[0]); function _product(v, i=0, _tot) = @@ -691,9 +696,12 @@ function linear_solve(A,b) = zeros = [for(i=[0:mindim-1]) if (approx(R[i][i],0)) i] ) zeros != [] ? [] : - m=len(l) || succ)? succ : - any( l, - i+1, - succ = is_list(l[i]) ? any(l[i]) : !(!l[i]) - ); +function any(l) = + assert(is_list(l), "The input is not a list." ) + _any(l, i=0, succ=false); +function _any(l, i=0, succ=false) = + (i>=len(l) || succ)? succ : + _any( l, + i+1, + succ = is_list(l[i]) ? _any(l[i]) : !(!l[i]) + ); // Function: all() @@ -972,12 +982,15 @@ function any(l, i=0, succ=false) = // all([[0,0], [1,0]]); // Returns false. // all([[1,1], [1,1]]); // Returns true. function all(l, i=0, fail=false) = - (i>=len(l) || fail)? !fail : - all( l, - i+1, - fail = is_list(l[i]) ? !all(l[i]) : !l[i] - ) ; + assert( is_list(l), "The input is not a list." ) + _all(l, i=0, fail=false); +function _all(l, i=0, fail=false) = + (i>=len(l) || fail)? !fail : + _all( l, + i+1, + fail = is_list(l[i]) ? !_all(l[i]) : !l[i] + ) ; // Function: count_true() @@ -1195,12 +1208,12 @@ function C_div(z1,z2) = // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. function polynomial(p,z,k,total) = - is_undef(k) - ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) - assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) - polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) - : k==len(p) ? total - : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : C_times(total,z)+[p[k],0]); +    is_undef(k) +    ?   assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) +        assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) +        polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) +    : k==len(p) ? total +    : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : C_times(total,z)+[p[k],0]); // Function: poly_mult() // Usage: @@ -1210,22 +1223,16 @@ function polynomial(p,z,k,total) = // Given a list of polynomials represented as real coefficient lists, with the highest degree coefficient first, // computes the coefficient list of the product polynomial. function poly_mult(p,q) = - is_undef(q) ? - assert( is_list(p) - && []==[for(pi=p) if( !is_vector(pi) && pi!=[]) 0], - "Invalid arguments to poly_mult") - len(p)==2 ? poly_mult(p[0],p[1]) - : poly_mult(p[0], poly_mult(select(p,1,-1))) - : - _poly_trim( - [ - for(n = [len(p)+len(q)-2:-1:0]) - sum( [for(i=[0:1:len(p)-1]) - let(j = len(p)+len(q)- 2 - n - i) - if (j>=0 && j Date: Mon, 3 Aug 2020 19:49:22 -0400 Subject: [PATCH 21/57] Moved submatrix to arrays.scad and relaxed requirement for numerical input. Added examples and tests. --- arrays.scad | 30 ++++++++++++++++++++++++++++++ math.scad | 12 ------------ tests/test_arrays.scad | 21 +++++++++++++++++++++ tests/test_math.scad | 16 ---------------- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/arrays.scad b/arrays.scad index 4f1c317..4cef623 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1192,6 +1192,36 @@ function subindex(M, idx) = ? [for(row=M) row[idx]] : [for(row=M) [for(i=idx) row[i]]]; + +// Function: submatrix() +// Usage: submatrix(M, idx1, idx2) +// Description: +// The input must be a list of lists (a matrix or 2d array). Returns a submatrix by selecting the rows listed in idx1 and columsn listed in idx2. +// Arguments +// M = Given list of lists +// idx1 = rows index list or range +// idx2 = column index list or range +// Example: +// M = [[ 1, 2, 3, 4, 5], +// [ 6, 7, 8, 9,10], +// [11,12,13,14,15], +// [16,17,18,19,20], +// [21,22,23,24,25]]; +// submatrix(M,[1:2],[3:4]); // Returns [[9, 10], [14, 15]] +// submatrix(M,[1], [3,4])); // Returns [[9,10]] +// submatrix(M,1, [3,4])); // Returns [[9,10]] +// submatrix(M,1,3)); // Returns [[9]] +// submatrix(M, [3,4],1); // Returns [[17],[22]]); +// submatrix(M, [1,3],[2,4]); // Returns [[8,10],[18,20]]); +// A = [[true, 17, "test"], +// [[4,2], 91, false], +// [6, [3,4], undef]]; +// submatrix(A,[0,2],[1,2]); // Returns [[17, "test"], [[3, 4], undef]] + +function submatrix(M,idx1,idx2) = + [for(i=idx1) [for(j=idx2) M[i][j] ] ]; + + // Function: zip() // Usage: // zip(v1, v2, v3, [fit], [fill]); diff --git a/math.scad b/math.scad index e9b8579..791ab94 100644 --- a/math.scad +++ b/math.scad @@ -708,18 +708,6 @@ function matrix_inverse(A) = linear_solve(A,ident(len(A))); -// Function: submatrix() -// Usage: submatrix(M, ind1, ind2) -// Description: -// Returns a submatrix with the specified index ranges or index sets. -function submatrix(M,ind1,ind2) = - assert( is_matrix(M), "Input must be a matrix." ) - [for(i=ind1) - [for(j=ind2) - assert( ! is_undef(M[i][j]), "Invalid indexing." ) - M[i][j] ] ]; - - // Function: qr_factor() // Usage: qr = qr_factor(A) // Description: diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index 4fffb7d..51120f5 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -365,6 +365,27 @@ module test_subindex() { test_subindex(); +// Need decision about behavior for out of bounds ranges, empty ranges +module test_submatrix(){ + M = [[1,2,3,4,5], + [6,7,8,9,10], + [11,12,13,14,15], + [16,17,18,19,20], + [21,22,23,24,25]]; + assert_equal(submatrix(M,[1:2], [3:4]), [[9,10],[14,15]]); + assert_equal(submatrix(M,[1], [3,4]), [[9,10]]); + assert_equal(submatrix(M,1, [3,4]), [[9,10]]); + assert_equal(submatrix(M, [3,4],1), [[17],[22]]); + assert_equal(submatrix(M, [1,3],[2,4]), [[8,10],[18,20]]); + assert_equal(submatrix(M, 1,3), [[9]]); + A = [[true, 17, "test"], + [[4,2], 91, false], + [6, [3,4], undef]]; + assert_equal(submatrix(A,[0,2],[1,2]),[[17, "test"], [[3, 4], undef]]); +} +test_submatrix(); + + module test_force_list() { assert_equal(force_list([3,4,5]), [3,4,5]); assert_equal(force_list(5), [5]); diff --git a/tests/test_math.scad b/tests/test_math.scad index e5b1a41..8e9e2c2 100644 --- a/tests/test_math.scad +++ b/tests/test_math.scad @@ -853,22 +853,6 @@ module test_real_roots(){ } test_real_roots(); -// Need decision about behavior for out of bounds ranges, empty ranges -module test_submatrix(){ - M = [[1,2,3,4,5], - [6,7,8,9,10], - [11,12,13,14,15], - [16,17,18,19,20], - [21,22,23,24,25]]; - assert_equal(submatrix(M,[1:2], [3:4]), [[9,10],[14,15]]); - assert_equal(submatrix(M,[1], [3,4]), [[9,10]]); - assert_equal(submatrix(M,1, [3,4]), [[9,10]]); - assert_equal(submatrix(M, [3,4],1), [[17],[22]]); - assert_equal(submatrix(M, [1,3],[2,4]), [[8,10],[18,20]]); -} -test_submatrix(); - - module test_qr_factor() { // Check that R is upper triangular From baf4eb76dc83a8b86ff3add55e310e10516004a3 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 3 Aug 2020 19:52:13 -0400 Subject: [PATCH 22/57] doc tweak --- arrays.scad | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arrays.scad b/arrays.scad index 4cef623..9943c31 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1177,7 +1177,8 @@ function add_scalar(v,s) = // Description: // Extracts the entries listed in idx from each entry in M. For a matrix this means // selecting a specified set of columsn. If idx is a number the return is a vector, otherwise -// it is a list of lists (the submatrix). +// it is a list of lists (the submatrix). Note that unlike subindex, even if you give a number for +// an index the output includes all levels of list nesting. // Arguments: // M = The given list of lists. // idx = The index, list of indices, or range of indices to fetch. From 719f1ff5859d6fd4f265a5d7432bb07476344370 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 3 Aug 2020 19:58:11 -0400 Subject: [PATCH 23/57] doc bugfix --- arrays.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrays.scad b/arrays.scad index 9943c31..2a9f9ee 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1198,7 +1198,7 @@ function subindex(M, idx) = // Usage: submatrix(M, idx1, idx2) // Description: // The input must be a list of lists (a matrix or 2d array). Returns a submatrix by selecting the rows listed in idx1 and columsn listed in idx2. -// Arguments +// Arguments: // M = Given list of lists // idx1 = rows index list or range // idx2 = column index list or range From c6b472318f556ea63042c0b284edee9a27abc5fd Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Wed, 5 Aug 2020 06:16:48 +0100 Subject: [PATCH 24/57] Optimize sorting Optimize _sort_general and change accordingly sort and sortidx. Introduce _valid_idx() to simplify teh validations of arg idx. Add input validation to subindex(). --- arrays.scad | 121 +++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 68 deletions(-) diff --git a/arrays.scad b/arrays.scad index d5e03a5..09fd7e2 100644 --- a/arrays.scad +++ b/arrays.scad @@ -709,40 +709,36 @@ function _sort_vectors4(arr) = ) concat( _sort_vectors4(lesser), equal, _sort_vectors4(greater) ); +// when idx==undef, returns the sorted array +// otherwise, returns the indices of the sorted array function _sort_general(arr, idx=undef) = (len(arr)<=1) ? arr : + is_undef(idx) + ? _sort_scalar(arr) + : let( arrind=[for(k=[0:len(arr)-1], ark=[arr[k]]) [ k, [for (i=idx) ark[i]] ] ] ) + _indexed_sort(arrind); + +// given a list of pairs, return the first element of each pair of the list sorted by the second element of the pair +// the sorting is done using compare_vals() +function _indexed_sort(arrind) = + arrind==[] ? [] : len(arrind)==1? [arrind[0][0]] : + let( pivot = arrind[floor(len(arrind)/2)][1] ) let( - pivot = arr[floor(len(arr)/2)], - pivotval = idx==undef? pivot : [for (i=idx) pivot[i]], - compare = - is_undef(idx) ? [for(entry=arr) compare_vals(entry, pivotval) ] : - [ for (entry = arr) - let( val = [for (i=idx) entry[i] ] ) - compare_vals(val, pivotval) ] , - lesser = [ for (i = [0:1:len(arr)-1]) if (compare[i] < 0) arr[i] ], - equal = [ for (i = [0:1:len(arr)-1]) if (compare[i] ==0) arr[i] ], - greater = [ for (i = [0:1:len(arr)-1]) if (compare[i] > 0) arr[i] ] - ) - concat(_sort_general(lesser,idx), equal, _sort_general(greater,idx)); + lesser = [ for (entry=arrind) if (compare_vals(entry[1], pivot) <0 ) entry ], + equal = [ for (entry=arrind) if (compare_vals(entry[1], pivot)==0 ) entry[0] ], + greater = [ for (entry=arrind) if (compare_vals(entry[1], pivot) >0 ) entry ] + ) + concat(_indexed_sort(lesser), equal, _indexed_sort(greater)); -function _sort_general(arr, idx=undef) = - (len(arr)<=1) ? arr : - let( - pivot = arr[floor(len(arr)/2)], - pivotval = idx==undef? pivot : [for (i=idx) pivot[i]], - compare = [ - for (entry = arr) let( - val = idx==undef? entry : [for (i=idx) entry[i]], - cmp = compare_vals(val, pivotval) - ) cmp - ], - lesser = [ for (i = [0:1:len(arr)-1]) if (compare[i] < 0) arr[i] ], - equal = [ for (i = [0:1:len(arr)-1]) if (compare[i] ==0) arr[i] ], - greater = [ for (i = [0:1:len(arr)-1]) if (compare[i] > 0) arr[i] ] - ) - concat(_sort_general(lesser,idx), equal, _sort_general(greater,idx)); - +// returns true for valid index specifications idx in the interval [imin, imax) +// note that idx can't have any value greater or EQUAL to imax +function _valid_idx(idx,imin,imax) = + is_undef(idx) + || ( is_finite(idx) && idx>=imin && idx< imax ) + || ( is_list(idx) && min(idx)>=imin && max(idx)< imax ) + || ( valid_range(idx) && idx[0]>=imin && idx[2]< imax ); + // Function: sort() // Usage: @@ -761,19 +757,21 @@ function _sort_general(arr, idx=undef) = // sorted = sort(l); // Returns [2,3,8,9,12,16,23,34,37,45,89] function sort(list, idx=undef) = !is_list(list) || len(list)<=1 ? list : - assert( is_undef(idx) || is_finite(idx) || is_vector(idx) || is_range(idx) , "Invalid indices.") - is_def(idx) ? _sort_general(list,idx) : - let(size = array_dim(list)) - len(size)==1 ? _sort_scalars(list) : - len(size)==2 && size[1] <=4 - ? ( - size[1]==0 ? list : - size[1]==1 ? _sort_vectors1(list) : - size[1]==2 ? _sort_vectors2(list) : - size[1]==3 ? _sort_vectors3(list) - /*size[1]==4*/ : _sort_vectors4(list) - ) - : _sort_general(list); + is_def(idx) + ? assert( _valid_idx(idx,0,len(list)) , "Invalid indices.") + let( sarr = _sort_general(list,idx) ) + [for(i=[0:len(sarr)-1]) list[sarr[i]] ] + : let(size = array_dim(list)) + len(size)==1 ? _sort_scalars(list) : + len(size)==2 && size[1] <=4 + ? ( + size[1]==0 ? list : + size[1]==1 ? _sort_vectors1(list) : + size[1]==2 ? _sort_vectors2(list) : + size[1]==3 ? _sort_vectors3(list) + /*size[1]==4*/ : _sort_vectors4(list) + ) + : _sort_general(list); // Function: sortidx() @@ -799,13 +797,13 @@ function sort(list, idx=undef) = // idxs3 = sortidx(lst, idx=[1,3]); // Returns: [3,0,2,1] function sortidx(list, idx=undef) = assert( is_list(list) || is_string(list) , "Invalid input to sort." ) - assert( is_undef(idx) || is_finite(idx) || is_vector(idx) , "Invalid indices.") + assert( _valid_idx(idx,0,len(list)) , "Invalid indices.") list==[] ? [] : let( size = array_dim(list), aug = is_undef(idx) && (len(size) == 1 || (len(size) == 2 && size[1]<=4)) ? zip(list, list_range(len(list))) - : enumerate(list,idx=idx) + : 0 ) is_undef(idx) && len(size) == 1? subindex(_sort_vectors1(aug),1) : is_undef(idx) && len(size) == 2 && size[1] <=4 @@ -817,27 +815,9 @@ function sortidx(list, idx=undef) = /*size[1]==4*/ : subindex(_sort_vectors4(aug),4) ) : // general case - subindex(_sort_general(aug, idx=list_range(s=1,n=len(aug)-1)), 0); + _sort_general(list,idx); -function sortidx(list, idx=undef) = - list==[] ? [] : let( - size = array_dim(list), - aug = is_undef(idx) && (len(size) == 1 || (len(size) == 2 && size[1]<=4))? - zip(list, list_range(len(list))) : - enumerate(list,idx=idx) - ) - is_undef(idx) && len(size) == 1? subindex(_sort_vectors1(aug),1) : - is_undef(idx) && len(size) == 2 && size[1] <=4? ( - size[1]==0? list_range(len(arr)) : - size[1]==1? subindex(_sort_vectors1(aug),1) : - size[1]==2? subindex(_sort_vectors2(aug),2) : - size[1]==3? subindex(_sort_vectors3(aug),3) : - /*size[1]==4*/ subindex(_sort_vectors4(aug),4) - ) : - // general case - subindex(_sort_general(aug, idx=list_range(s=1,n=len(aug)-1)), 0); - // sort() does not accept strings but sortidx does; isn't inconsistent ? @@ -911,10 +891,10 @@ function idx(list, step=1, end=-1,start=0) = // for (p=enumerate(colors)) right(20*p[0]) color(p[1]) circle(d=10); function enumerate(l,idx=undef) = assert(is_list(l)||is_string(list), "Invalid input." ) - assert(is_undef(idx)||is_finite(idx)||is_vector(idx) ||is_range(idx), "Invalid index/indices." ) + assert( _valid_idx(idx,0,len(l)), "Invalid index/indices." ) (idx==undef) ? [for (i=[0:1:len(l)-1]) [i,l[i]]] - : [for (i=[0:1:len(l)-1]) concat([i], [for (j=idx) l[i][j]])]; + : [for (i=[0:1:len(l)-1]) [ i, for (j=idx) l[i][j]] ]; // Function: force_list() @@ -1130,8 +1110,9 @@ function add_scalar(v,s) = // subindex(M, idx) // Description: // Extracts the entries listed in idx from each entry in M. For a matrix this means -// selecting a specified set of columsn. If idx is a number the return is a vector, otherwise -// it is a list of lists (the submatrix). +// selecting a specified set of columns. If idx is a number the return is a vector, +// otherwise it is a list of lists (the submatrix). +// This function will return `undef` at all entry positions indexed by idx not found in the input list M. // Arguments: // M = The given list of lists. // idx = The index, list of indices, or range of indices to fetch. @@ -1141,8 +1122,12 @@ function add_scalar(v,s) = // subindex(M,[2]); // Returns [[3], [7], [11], [15]] // subindex(M,[2,1]); // Returns [[3, 2], [7, 6], [11, 10], [15, 14]] // subindex(M,[1:3]); // Returns [[2, 3, 4], [6, 7, 8], [10, 11, 12], [14, 15, 16]] +// N = [ [1,2], [3], [4,5], [6,7,8] ]; +// subindex(N,[0,1]); // Returns [ [1,2], [3,undef], [4,5], [6,7] ] function subindex(M, idx) = - is_num(idx) + assert( is_list(M), "The input is not a list." ) + assert( !is_undef(idx) && _valid_idx(idx,0,1/0), "Invalid index input." ) + is_finite(idx) ? [for(row=M) row[idx]] : [for(row=M) [for(i=idx) row[i]]]; From 5ebf1c80b956d04b79b6b45372948e5267fa27d1 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 14:32:25 +0100 Subject: [PATCH 25/57] Extend scope of transpose Includes an arg to allow transposing in respecto to the secondary "diagonal" --- arrays.scad | 26 +++++++++++++++++++++++--- tests/test_arrays.scad | 1 + 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/arrays.scad b/arrays.scad index 6124e89..835fd65 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1287,6 +1287,10 @@ function array_dim(v, depth=undef) = // Function: transpose() // Description: Returns the transposition of the given array. +// When reverse=true, the transposition is done in respect to the secondary diagonal, that is: +// . +// reverse(transpose(reverse(arr))) == transpose(arr, reverse=true) +// By default, reverse=false. // Example: // arr = [ // ["a", "b", "c"], @@ -1313,14 +1317,30 @@ function array_dim(v, depth=undef) = // // ["c", "f"], // // ] // Example: +// arr = [ +// ["a", "b", "c"], +// ["d", "e", "f"], +// ["g", "h", "i"] +// ]; +// t = transpose(arr, reverse=true); +// // Returns: +// // [ +// // ["i", "f", "c"], +// // ["h", "e", "b"], +// // ["g", "d", "a"] +// // ] +// Example: // transpose([3,4,5]); // Returns: [3,4,5] -function transpose(arr) = +function transpose(arr, reverse=false) = assert( is_list(arr) && len(arr)>0, "The array is not a vector neither a matrix." ) is_list(arr[0]) ? let( l0 = len(arr[0]) ) assert([for(a=arr) if(!is_list(a) || len(a)!=l0) 1 ]==[], "The array is not a vector neither a matrix." ) - [for (i=[0:1:l0-1]) - [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] + reverse + ? [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[len(arr)-1-j][l0-1-i] ] ] + : [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] : assert( is_vector(arr), "The array is not a vector neither a matrix." ) arr; diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index 64a7fbb..f121266 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -480,6 +480,7 @@ test_array_dim(); module test_transpose() { assert(transpose([[1,2,3],[4,5,6],[7,8,9]]) == [[1,4,7],[2,5,8],[3,6,9]]); assert(transpose([[1,2,3],[4,5,6]]) == [[1,4],[2,5],[3,6]]); + assert(transpose([[1,2,3],[4,5,6]], reverse=true) == [[6,3],[5,2],[4,1]]); assert(transpose([3,4,5]) == [3,4,5]); } test_transpose(); From 1df84f35525ac78c0a65296413bd89b323c50d3d Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 14:33:42 +0100 Subject: [PATCH 26/57] Revert "Extend scope of transpose" This reverts commit 5ebf1c80b956d04b79b6b45372948e5267fa27d1. --- arrays.scad | 26 +++----------------------- tests/test_arrays.scad | 1 - 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/arrays.scad b/arrays.scad index 835fd65..6124e89 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1287,10 +1287,6 @@ function array_dim(v, depth=undef) = // Function: transpose() // Description: Returns the transposition of the given array. -// When reverse=true, the transposition is done in respect to the secondary diagonal, that is: -// . -// reverse(transpose(reverse(arr))) == transpose(arr, reverse=true) -// By default, reverse=false. // Example: // arr = [ // ["a", "b", "c"], @@ -1317,30 +1313,14 @@ function array_dim(v, depth=undef) = // // ["c", "f"], // // ] // Example: -// arr = [ -// ["a", "b", "c"], -// ["d", "e", "f"], -// ["g", "h", "i"] -// ]; -// t = transpose(arr, reverse=true); -// // Returns: -// // [ -// // ["i", "f", "c"], -// // ["h", "e", "b"], -// // ["g", "d", "a"] -// // ] -// Example: // transpose([3,4,5]); // Returns: [3,4,5] -function transpose(arr, reverse=false) = +function transpose(arr) = assert( is_list(arr) && len(arr)>0, "The array is not a vector neither a matrix." ) is_list(arr[0]) ? let( l0 = len(arr[0]) ) assert([for(a=arr) if(!is_list(a) || len(a)!=l0) 1 ]==[], "The array is not a vector neither a matrix." ) - reverse - ? [for (i=[0:1:l0-1]) - [ for (j=[0:1:len(arr)-1]) arr[len(arr)-1-j][l0-1-i] ] ] - : [for (i=[0:1:l0-1]) - [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] + [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] : assert( is_vector(arr), "The array is not a vector neither a matrix." ) arr; diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index f121266..64a7fbb 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -480,7 +480,6 @@ test_array_dim(); module test_transpose() { assert(transpose([[1,2,3],[4,5,6],[7,8,9]]) == [[1,4,7],[2,5,8],[3,6,9]]); assert(transpose([[1,2,3],[4,5,6]]) == [[1,4],[2,5],[3,6]]); - assert(transpose([[1,2,3],[4,5,6]], reverse=true) == [[6,3],[5,2],[4,1]]); assert(transpose([3,4,5]) == [3,4,5]); } test_transpose(); From 50b0f170e7f1fbdf9244b9b12055977ea280831b Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 14:55:25 +0100 Subject: [PATCH 27/57] Extend the scope of transpose It allows a transposition in respect to the secondary "diagonal" --- arrays.scad | 26 +++++++++++++++++++++++--- tests/test_arrays.scad | 2 ++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/arrays.scad b/arrays.scad index 6124e89..835fd65 100644 --- a/arrays.scad +++ b/arrays.scad @@ -1287,6 +1287,10 @@ function array_dim(v, depth=undef) = // Function: transpose() // Description: Returns the transposition of the given array. +// When reverse=true, the transposition is done in respect to the secondary diagonal, that is: +// . +// reverse(transpose(reverse(arr))) == transpose(arr, reverse=true) +// By default, reverse=false. // Example: // arr = [ // ["a", "b", "c"], @@ -1313,14 +1317,30 @@ function array_dim(v, depth=undef) = // // ["c", "f"], // // ] // Example: +// arr = [ +// ["a", "b", "c"], +// ["d", "e", "f"], +// ["g", "h", "i"] +// ]; +// t = transpose(arr, reverse=true); +// // Returns: +// // [ +// // ["i", "f", "c"], +// // ["h", "e", "b"], +// // ["g", "d", "a"] +// // ] +// Example: // transpose([3,4,5]); // Returns: [3,4,5] -function transpose(arr) = +function transpose(arr, reverse=false) = assert( is_list(arr) && len(arr)>0, "The array is not a vector neither a matrix." ) is_list(arr[0]) ? let( l0 = len(arr[0]) ) assert([for(a=arr) if(!is_list(a) || len(a)!=l0) 1 ]==[], "The array is not a vector neither a matrix." ) - [for (i=[0:1:l0-1]) - [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] + reverse + ? [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[len(arr)-1-j][l0-1-i] ] ] + : [for (i=[0:1:l0-1]) + [ for (j=[0:1:len(arr)-1]) arr[j][i] ] ] : assert( is_vector(arr), "The array is not a vector neither a matrix." ) arr; diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index 64a7fbb..2f2ff1c 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -476,10 +476,12 @@ module test_array_dim() { } test_array_dim(); +echo(transpose([[1,2,3],[4,5,6]],reverse=true)); module test_transpose() { assert(transpose([[1,2,3],[4,5,6],[7,8,9]]) == [[1,4,7],[2,5,8],[3,6,9]]); assert(transpose([[1,2,3],[4,5,6]]) == [[1,4],[2,5],[3,6]]); + assert(transpose([[1,2,3],[4,5,6]],reverse=true) == [[6,3], [5,2], [4,1]]); assert(transpose([3,4,5]) == [3,4,5]); } test_transpose(); From 0dc8bf6c8fd311b12418aa35905e88261c4f0c31 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 15:15:49 +0100 Subject: [PATCH 28/57] Review is_path and extend the scope of simplify_path and simplify_pat_indexed - introducing is_matrix in is_path makes it more efficient. - simplify is_path and simplify_path_indexed is now able to deal with path of any dimension. --- paths.scad | 55 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/paths.scad b/paths.scad index 7d5f978..93d82fa 100644 --- a/paths.scad +++ b/paths.scad @@ -43,10 +43,12 @@ include // dim = list of allowed dimensions of the vectors in the path. Default: [2,3] // fast = set to true for fast check that only looks at first entry. Default: false function is_path(list, dim=[2,3], fast=false) = - fast? is_list(list) && is_vector(list[0]) : - is_list(list) && is_list(list[0]) && len(list)>1 && - (is_undef(dim) || in_list(len(list[0]), force_list(dim))) && - is_list_of(list, repeat(0,len(list[0]))); + fast + ? is_list(list) && is_vector(list[0]) + : is_matrix(list) + && len(list)>1 + && len(list[0])>0 + && (is_undef(dim) || in_list(len(list[0]), force_list(dim))); // Function: is_closed_path() @@ -105,32 +107,51 @@ function path_subselect(path, s1, u1, s2, u2, closed=false) = // Function: simplify_path() // Description: -// Takes a path and removes unnecessary collinear points. +// Takes a path and removes unnecessary subsequent collinear points. // Usage: // simplify_path(path, [eps]) // Arguments: -// path = A list of 2D path points. +// path = A list of path points of any dimension. // eps = Largest positional variance allowed. Default: `EPSILON` (1-e9) function simplify_path(path, eps=EPSILON) = - len(path)<=2? path : let( - indices = concat([0], [for (i=[1:1:len(path)-2]) if (!collinear_indexed(path, i-1, i, i+1, eps=eps)) i], [len(path)-1]) - ) [for (i = indices) path[i]]; + assert( is_path(path), "Invalid path." ) + assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) + len(path)<=2 ? path + : let( + indices = [ 0, + for (i=[1:1:len(path)-2]) + if (!collinear(path[i-1],path[i],path[i+1], eps=eps)) i, + len(path)-1 + ] + ) + [for (i = indices) path[i] ]; // Function: simplify_path_indexed() // Description: -// Takes a list of points, and a path as a list of indices into `points`, -// and removes all path points that are unecessarily collinear. +// Takes a list of points, and a list of indices into `points`, +// and removes from the list all indices of subsequent indexed points that are unecessarily collinear. +// Returns the list of the remained indices. // Usage: -// simplify_path_indexed(path, eps) +// simplify_path_indexed(points,indices, eps) // Arguments: // points = A list of points. -// path = A list of indices into `points` that forms a path. +// indices = A list of indices into `points` that forms a path. // eps = Largest angle variance allowed. Default: EPSILON (1-e9) degrees. -function simplify_path_indexed(points, path, eps=EPSILON) = - len(path)<=2? path : let( - indices = concat([0], [for (i=[1:1:len(path)-2]) if (!collinear_indexed(points, path[i-1], path[i], path[i+1], eps=eps)) i], [len(path)-1]) - ) [for (i = indices) path[i]]; +function simplify_path_indexed(points, indices, eps=EPSILON) = + len(indices)<=2? indices + : let( + indices = concat( indices[0], + [for (i=[1:1:len(indices)-2]) + let( + i1 = indices[i-1], + i2 = indices[i], + i3 = indices[i+1] + ) + if (!collinear(points[i1],points[i2],points[i3], eps=eps)) indices[i]], + indices[len(indices)-1] ) + ) + indices; // Function: path_length() From 2e9d87f5841a0921c1e2355b63c2a41da8bc4236 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 15:21:36 +0100 Subject: [PATCH 29/57] explore new scope of transpose in linear_solve and _qr_factor --- math.scad | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/math.scad b/math.scad index e40c1c6..158b1f2 100644 --- a/math.scad +++ b/math.scad @@ -698,9 +698,8 @@ function linear_solve(A,b) = zeros != [] ? [] : mj ? 0 : qr[1][i][j] + qr =_qr_factor(A, Q=ident(m), column=0, m = m, n=n), + Rzero = + let( R = qr[1] ) + [ for(i=[0:m-1]) [ + let( ri = R[i] ) + for(j=[0:n-1]) i>j ? 0 : ri[j] ] - ] + ] ) [qr[0],Rzero]; function _qr_factor(A,Q, column, m, n) = @@ -763,13 +763,12 @@ function back_substitute(R, b, transpose = false) = let(n=len(R)) assert(is_vector(b,n) || is_matrix(b,n),str("R and b are not compatible in back_substitute ",n, len(b))) transpose - ? reverse(_back_substitute([for(i=[0:n-1]) [for(j=[0:n-1]) R[n-1-j][n-1-i]]], - reverse(b))) + ? reverse(_back_substitute(transpose(R, reverse=true), reverse(b))) : _back_substitute(R,b); function _back_substitute(R, b, x=[]) = - let(n=len(R)) - len(x) == n ? x + let(n=len(R)) + len(x) == n ? x : let(ind = n - len(x) - 1) R[ind][ind] == 0 ? [] : let( From b6085e0cbc24b76de7f4f911325341c75dd77fe6 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 11 Aug 2020 15:36:44 +0100 Subject: [PATCH 30/57] Exclude echo() --- tests/test_arrays.scad | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index 2f2ff1c..8df23a8 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -476,7 +476,6 @@ module test_array_dim() { } test_array_dim(); -echo(transpose([[1,2,3],[4,5,6]],reverse=true)); module test_transpose() { assert(transpose([[1,2,3],[4,5,6],[7,8,9]]) == [[1,4,7],[2,5,8],[3,6,9]]); From 3bf22cd236754a7f022941e9ad3ca1d139481034 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 16 Aug 2020 23:33:11 +0100 Subject: [PATCH 31/57] In-depth review Besides param validation and some formating, changes: A. add new functions: 1. _valid_line() 2. _valid_plane() 3. line_from_points() 4. projection_on_plane() 5. points_on_plane() B. rename/redefine/remove functions: 1. points_are_coplanar() >> coplanar() 2. collinear() works with list of points as well as coplanar() 3. find_noncollinear_points >> noncollinear_triple 4. collinear_indexed() removed 5. polygon_is_convex() >> is_convex_polygon() C. recode/optimize the codes of the functions: 1. point_on_segment2d() 2. point_left_of_line2d() 3. distance_from_line() 4. line_closest_point() 5. plane_from_polygon() 6. _general_plane_line_intersection() 7. polygon_line_intersection() 8. find_circle_2tangents() 9. find_circle_3points() 10. polygon_area() 11. is_convex_polygon() 12. reindex_polygon() 13. centroid() 14. polygon_is_clockwise() 15. clockwise_polygon() 16. ccw_polygon() The function name changes were updated in: test_geometry.scad hull.scad rounding.scad vnf.scad Regression tests for the new external functions were included in test_geometry.scad. Unsolved questions: 1. why sorting the indices in plane_from_points and polygon_line_intersection? 2. aren't redundant plane_from_polygon() and plane_from_points()? --- geometry.scad | 1014 +++++++++++++++++++++++++------------- tests/test_geometry.scad | 379 +++++++++----- 2 files changed, 910 insertions(+), 483 deletions(-) diff --git a/geometry.scad b/geometry.scad index 443c026..b3f5ee6 100644 --- a/geometry.scad +++ b/geometry.scad @@ -21,85 +21,86 @@ // edge = Array of two points forming the line segment to test against. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function point_on_segment2d(point, edge, eps=EPSILON) = - approx(point,edge[0],eps=eps) || approx(point,edge[1],eps=eps) || // The point is an endpoint - sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the - && sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints - && approx(point_left_of_segment2d(point, edge),0,eps=eps); // and on the line defined by edge - - -// Function: point_left_of_segment2d() -// Usage: -// point_left_of_segment2d(point, edge); -// Description: -// Return >0 if point is left of the line defined by edge. -// Return =0 if point is on the line. -// Return <0 if point is right of the line. -// Arguments: -// point = The point to check position of. -// edge = Array of two points forming the line segment to test against. -function point_left_of_segment2d(point, edge) = - (edge[1].x-edge[0].x) * (point.y-edge[0].y) - (point.x-edge[0].x) * (edge[1].y-edge[0].y); + assert( is_vector(point,2), "Invalid point." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) + approx(point,edge[0],eps=eps) + || approx(point,edge[1],eps=eps) // The point is an endpoint + || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the + || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints + && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge +function point_on_segment2d(point, edge, eps=EPSILON) = + assert( is_vector(point,2), "Invalid point." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) + let( dp = point-edge[0], + de = edge[1]-edge[0], + ne = norm(de) ) + ( dp*de >= -eps*ne ) + && ( (dp-de)*de <= eps*ne ) // point projects on the segment + && _dist(point-edge[0],unit(de)) point.y && point_left_of_segment2d(point, edge) > 0)? 1 : 0 + (edge[1].y > point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 ) : ( - (edge[1].y <= point.y && point_left_of_segment2d(point, edge) < 0)? -1 : 0 + (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 ); +//Internal +function _valid_line(line,dim,eps=EPSILON) = + is_matrix(line,2,dim) + && ! approx(norm(line[1]-line[0]), 0, eps); + +//Internal +function _valid_plane(p, eps=EPSILON) = is_vector(p,4) && ! approx(norm(p),0,eps); + + +// Function: point_left_of_line2d() +// Usage: +// point_left_of_line2d(point, line); +// Description: +// Return >0 if point is left of the line defined by `line`. +// Return =0 if point is on the line. +// Return <0 if point is right of the line. +// Arguments: +// point = The point to check position of. +// line = Array of two points forming the line segment to test against. +function point_left_of_line2d(point, line) = + assert( is_vector(point,2) && is_vector(line*point, 2), "Improper input." ) + cross(line[0]-point, line[1]-line[0]); + // Function: collinear() // Usage: -// collinear(a, b, c, [eps]); +// collinear(a, [b, c], [eps]); // Description: -// Returns true if three points are co-linear. +// Returns true if the points `a`, `b` and `c` are co-linear or if the list of points `a` is collinear. // Arguments: -// a = First point. -// b = Second point. -// c = Third point. +// a = First point or list of points. +// b = Second point or undef; it should be undef if `c` is undef +// c = Third point or undef. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function collinear(a, b, c, eps=EPSILON) = - approx(a,b,eps=eps)? true : - distance_from_line([a,b], c) < eps; + assert( is_path([a,b,c],dim=undef) + || ( is_undef(b) && is_undef(c) && is_path(a,dim=undef) ), + "Input should be 3 points or a list of points with same dimension.") + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + let( points = is_def(c) ? [a,b,c]: a ) + len(points)<3 ? true + : noncollinear_triple(points,error=false,eps=eps)==[]; + +//*** valid for any dimension -// Function: collinear_indexed() -// Usage: -// collinear_indexed(points, a, b, c, [eps]); -// Description: -// Returns true if three points are co-linear. -// Arguments: -// points = A list of points. -// a = Index in `points` of first point. -// b = Index in `points` of second point. -// c = Index in `points` of third point. -// eps = Acceptable max angle variance. Default: EPSILON (1e-9) degrees. -function collinear_indexed(points, a, b, c, eps=EPSILON) = - let( - p1=points[a], - p2=points[b], - p3=points[c] - ) collinear(p1, p2, p3, eps); - - -// Function: points_are_collinear() -// Usage: -// points_are_collinear(points); -// Description: -// Given a list of points, returns true if all points in the list are collinear. -// Arguments: -// points = The list of points to test. -// eps = How much variance is allowed in testing that each point is on the same line. Default: `EPSILON` (1e-9) -function points_are_collinear(points, eps=EPSILON) = - let( - a = furthest_point(points[0], points), - b = furthest_point(points[a], points), - pa = points[a], - pb = points[b] - ) all([for (pt = points) collinear(pa, pb, pt, eps=eps)]); - + // Function: distance_from_line() // Usage: @@ -112,10 +113,11 @@ function points_are_collinear(points, eps=EPSILON) = // Example: // distance_from_line([[-10,0], [10,0]], [3,8]); // Returns: 8 function distance_from_line(line, pt) = - let(a=line[0], n=unit(line[1]-a), d=a-pt) - norm(d - ((d * n) * n)); - - + assert( _valid_line(line) && is_vector(pt,len(line[0])), + "Invalid line, invalid point or incompatible dimensions." ) + _dist(pt-line[0],unit(line[1]-line[0])); + + // Function: line_normal() // Usage: // line_normal([P1,P2]) @@ -133,9 +135,11 @@ function distance_from_line(line, pt) = // color("green") stroke([p1,p1+10*n], endcap2="arrow2"); // color("blue") move_copies([p1,p2]) circle(d=2, $fn=12); function line_normal(p1,p2) = - is_undef(p2)? - assert(is_path(p1,2)) line_normal(p1[0],p1[1]) : - assert(is_vector(p1,2)&&is_vector(p2,2)) unit([p1.y-p2.y,p2.x-p1.x]); + is_undef(p2) + ? assert( len(p1)==2 && !is_undef(p1[1]) , "Invalid input." ) + line_normal(p1[0],p1[1]) + : assert( _valid_line([p1,p2],dim=2), "Invalid line." ) + unit([p1.y-p2.y,p2.x-p1.x]); // 2D Line intersection from two segments. @@ -166,7 +170,10 @@ function _general_line_intersection(s1,s2,eps=EPSILON) = // l2 = Second 2D line, given as a list of two 2D points on the line. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function line_intersection(l1,l2,eps=EPSILON) = - let(isect = _general_line_intersection(l1,l2,eps=eps)) isect[0]; + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(l1,dim=2,eps=eps) &&_valid_line(l2,dim=2,eps=eps), "Invalid line(s)." ) + let(isect = _general_line_intersection(l1,l2,eps=eps)) + isect[0]; // Function: line_ray_intersection() @@ -180,9 +187,12 @@ function line_intersection(l1,l2,eps=EPSILON) = // ray = The 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function line_ray_intersection(line,ray,eps=EPSILON) = + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(line,dim=2,eps=eps) && _valid_line(ray,dim=2,eps=eps), "Invalid line or ray." ) let( isect = _general_line_intersection(line,ray,eps=eps) - ) isect[2]<0-eps? undef : isect[0]; + ) + (isect[2]<0-eps) ? undef : isect[0]; // Function: line_segment_intersection() @@ -196,6 +206,8 @@ function line_ray_intersection(line,ray,eps=EPSILON) = // segment = The bounded 2D line segment, given as a list of the two 2D endpoints of the segment. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function line_segment_intersection(line,segment,eps=EPSILON) = + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(line, dim=2,eps=eps) &&_valid_line(segment,dim=2,eps=eps), "Invalid line or segment." ) let( isect = _general_line_intersection(line,segment,eps=eps) ) isect[2]<0-eps || isect[2]>1+eps ? undef : isect[0]; @@ -212,9 +224,12 @@ function line_segment_intersection(line,segment,eps=EPSILON) = // r2 = Second 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function ray_intersection(r1,r2,eps=EPSILON) = + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(r1,dim=2,eps=eps) && _valid_line(r2,dim=2,eps=eps), "Invalid ray(s)." ) let( isect = _general_line_intersection(r1,r2,eps=eps) - ) isect[1]<0-eps || isect[2]<0-eps? undef : isect[0]; + ) + isect[1]<0-eps || isect[2]<0-eps ? undef : isect[0]; // Function: ray_segment_intersection() @@ -228,9 +243,16 @@ function ray_intersection(r1,r2,eps=EPSILON) = // segment = The bounded 2D line segment, given as a list of the two 2D endpoints of the segment. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function ray_segment_intersection(ray,segment,eps=EPSILON) = + assert( _valid_line(ray,dim=2,eps=eps) && _valid_line(segment,dim=2,eps=eps), "Invalid ray or segment." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) let( isect = _general_line_intersection(ray,segment,eps=eps) - ) isect[1]<0-eps || isect[2]<0-eps || isect[2]>1+eps ? undef : isect[0]; + ) + isect[1]<0-eps + || isect[2]<0-eps + || isect[2]>1+eps + ? undef + : isect[0]; // Function: segment_intersection() @@ -244,9 +266,17 @@ function ray_segment_intersection(ray,segment,eps=EPSILON) = // s2 = Second 2D segment, given as a list of the two 2D endpoints of the line segment. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function segment_intersection(s1,s2,eps=EPSILON) = + assert( _valid_line(s1,dim=2,eps=eps) && _valid_line(s2,dim=2,eps=eps), "Invalid segment(s)." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) let( isect = _general_line_intersection(s1,s2,eps=eps) - ) isect[1]<0-eps || isect[1]>1+eps || isect[2]<0-eps || isect[2]>1+eps ? undef : isect[0]; + ) + isect[1]<0-eps + || isect[1]>1+eps + || isect[2]<0-eps + || isect[2]>1+eps + ? undef + : isect[0]; // Function: line_closest_point() @@ -311,6 +341,12 @@ function line_closest_point(line,pt) = ) line[0] + projection*segvec; +function line_closest_point(line,pt) = + assert(_valid_line(line), "Invalid line." ) + assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) + let( n = unit( line[0]- line[1]) ) + line[1]+((pt- line[1]) * n) * n; + // Function: ray_closest_point() // Usage: @@ -364,9 +400,8 @@ function line_closest_point(line,pt) = // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); function ray_closest_point(ray,pt) = - assert(is_path(ray)&&len(ray)==2) - assert(same_shape(pt,ray[0])) - assert(!approx(ray[0],ray[1])) + assert( _valid_line(ray), "Invalid ray." ) + assert(is_vector(pt,len(ray[0])), "Invalid point or incompatible dimensions." ) let( seglen = norm(ray[1]-ray[0]), segvec = (ray[1]-ray[0])/seglen, @@ -428,8 +463,8 @@ function ray_closest_point(ray,pt) = // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); function segment_closest_point(seg,pt) = - assert(is_path(seg)&&len(seg)==2) - assert(same_shape(pt,seg[0])) + assert(_valid_line(seg), "Invalid segment." ) + assert(len(pt)==len(seg[0]), "Incompatible dimensions." ) approx(seg[0],seg[1])? seg[0] : let( seglen = norm(seg[1]-seg[0]), @@ -440,6 +475,26 @@ function segment_closest_point(seg,pt) = projection>=seglen ? seg[1] : seg[0] + projection*segvec; + +// Function: line_from_points() +// Usage: +// line_from_points(points, [fast], [eps]); +// Description: +// Given a list of 2 or more colinear points, returns a line containing them. +// If `fast` is false and the points are coincident, then `undef` is returned. +// if `fast` is true, then the collinearity test is skipped and a line passing through 2 distinct arbitrary points is returned. +// Arguments: +// points = The list of points to find the line through. +// fast = If true, don't verify that all points are collinear. Default: false +// eps = How much variance is allowed in testing each point against the line. Default: `EPSILON` (1e-9) +function line_from_points(points, fast=false, eps=EPSILON) = + assert( is_path(points,dim=undef), "Improper point list." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + let( pb = furthest_point(points[0],points) ) + approx(norm(points[pb]-points[0]),0) ? undef : + fast || collinear(points) ? [points[pb], points[0]] : undef; + + // Section: 2D Triangles @@ -486,21 +541,30 @@ function segment_closest_point(seg,pt) = // ang = tri_calc(adj=20,hyp=30)[3]; // ang2 = tri_calc(adj=20,hyp=40)[4]; function tri_calc(ang,ang2,adj,opp,hyp) = - assert(ang==undef || ang2==undef,"You cannot specify both ang and ang2.") - assert(num_defined([ang,ang2,adj,opp,hyp])==2, "You must specify exactly two arguments.") + assert(ang==undef || ang2==undef,"At most one angle is allowed.") + assert(num_defined([ang,ang2,adj,opp,hyp])==2, "Exactly two arguments must be given.") let( - ang = ang!=undef? assert(ang>0&&ang<90) ang : - ang2!=undef? (90-ang2) : - adj==undef? asin(constrain(opp/hyp,-1,1)) : - opp==undef? acos(constrain(adj/hyp,-1,1)) : - atan2(opp,adj), - ang2 = ang2!=undef? assert(ang2>0&&ang2<90) ang2 : (90-ang), - adj = adj!=undef? assert(adj>0) adj : - (opp!=undef? (opp/tan(ang)) : (hyp*cos(ang))), - opp = opp!=undef? assert(opp>0) opp : - (adj!=undef? (adj*tan(ang)) : (hyp*sin(ang))), - hyp = hyp!=undef? assert(hyp>0) assert(adj0&&ang<90, "The input angles should be acute angles." ) ang + : ang2!=undef ? (90-ang2) + : adj==undef ? asin(constrain(opp/hyp,-1,1)) + : opp==undef ? acos(constrain(adj/hyp,-1,1)) + : atan2(opp,adj), + ang2 = ang2!=undef + ? assert(ang2>0&&ang2<90, "The input angles should be acute angles." ) ang2 + : (90-ang), + adj = adj!=undef + ? assert(adj>0, "Triangle side lengths should be positive." ) adj + : (opp!=undef? (opp/tan(ang)) : (hyp*cos(ang))), + opp = opp!=undef + ? assert(opp>0, "Triangle side lengths should be positive." ) opp + : (adj!=undef? (adj*tan(ang)) : (hyp*sin(ang))), + hyp = hyp!=undef + ? assert(hyp>0, "Triangle side lengths should be positive." ) + assert(adj=0) - assert(is_num(opp)&&opp>=0) + assert(is_finite(hyp+opp) && hyp>=0 && opp>=0, + "Triangle side lengths should be a positive numbers." ) sqrt(hyp*hyp-opp*opp); @@ -534,8 +598,8 @@ function hyp_opp_to_adj(hyp,opp) = // Example: // adj = hyp_ang_to_adj(8,60); // Returns: 4 function hyp_ang_to_adj(hyp,ang) = - assert(is_num(hyp)&&hyp>=0) - assert(is_num(ang)&&ang>0&&ang<90) + assert(is_finite(hyp) && hyp>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) hyp*cos(ang); @@ -551,8 +615,8 @@ function hyp_ang_to_adj(hyp,ang) = // Example: // adj = opp_ang_to_adj(8,30); // Returns: 4 function opp_ang_to_adj(opp,ang) = - assert(is_num(opp)&&opp>=0) - assert(is_num(ang)&&ang>0&&ang<90) + assert(is_finite(opp) && opp>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) opp/tan(ang); @@ -567,8 +631,8 @@ function opp_ang_to_adj(opp,ang) = // Example: // opp = hyp_adj_to_opp(5,4); // Returns: 3 function hyp_adj_to_opp(hyp,adj) = - assert(is_num(hyp)&&hyp>=0) - assert(is_num(adj)&&adj>=0) + assert(is_finite(hyp) && hyp>=0 && is_finite(adj) && adj>=0, + "Triangle side lengths should be a positive numbers." ) sqrt(hyp*hyp-adj*adj); @@ -583,8 +647,8 @@ function hyp_adj_to_opp(hyp,adj) = // Example: // opp = hyp_ang_to_opp(8,30); // Returns: 4 function hyp_ang_to_opp(hyp,ang) = - assert(is_num(hyp)&&hyp>=0) - assert(is_num(ang)&&ang>0&&ang<90) + assert(is_finite(hyp)&&hyp>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) hyp*sin(ang); @@ -599,8 +663,8 @@ function hyp_ang_to_opp(hyp,ang) = // Example: // opp = adj_ang_to_opp(8,45); // Returns: 8 function adj_ang_to_opp(adj,ang) = - assert(is_num(adj)&&adj>=0) - assert(is_num(ang)&&ang>0&&ang<90) + assert(is_finite(adj)&&adj>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) adj*tan(ang); @@ -615,8 +679,8 @@ function adj_ang_to_opp(adj,ang) = // Example: // hyp = adj_opp_to_hyp(3,4); // Returns: 5 function adj_opp_to_hyp(adj,opp) = - assert(is_num(adj)&&adj>=0) - assert(is_num(opp)&&opp>=0) + assert(is_finite(opp) && opp>=0 && is_finite(adj) && adj>=0, + "Triangle side lengths should be a positive numbers." ) norm([opp,adj]); @@ -631,8 +695,8 @@ function adj_opp_to_hyp(adj,opp) = // Example: // hyp = adj_ang_to_hyp(4,60); // Returns: 8 function adj_ang_to_hyp(adj,ang) = - assert(is_num(adj)&&adj>=0) - assert(is_num(ang)&&ang>=0&&ang<90) + assert(is_finite(adj) && adj>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) adj/cos(ang); @@ -647,8 +711,8 @@ function adj_ang_to_hyp(adj,ang) = // Example: // hyp = opp_ang_to_hyp(4,30); // Returns: 8 function opp_ang_to_hyp(opp,ang) = - assert(is_num(opp)&&opp>=0) - assert(is_num(ang)&&ang>0&&ang<=90) + assert(is_finite(opp) && opp>=0, "Triangle side length should be a positive number." ) + assert(is_finite(ang) && ang>0 && ang<90, "The angle should be an acute angle." ) opp/sin(ang); @@ -663,8 +727,8 @@ function opp_ang_to_hyp(opp,ang) = // Example: // ang = hyp_adj_to_ang(8,4); // Returns: 60 degrees function hyp_adj_to_ang(hyp,adj) = - assert(is_num(hyp)&&hyp>0) - assert(is_num(adj)&&adj>=0) + assert(is_finite(hyp) && hyp>0 && is_finite(adj) && adj>=0, + "Triangle side lengths should be positive numbers." ) acos(adj/hyp); @@ -679,8 +743,8 @@ function hyp_adj_to_ang(hyp,adj) = // Example: // ang = hyp_opp_to_ang(8,4); // Returns: 30 degrees function hyp_opp_to_ang(hyp,opp) = - assert(is_num(hyp)&&hyp>0) - assert(is_num(opp)&&opp>=0) + assert(is_finite(hyp+opp) && hyp>0 && opp>=0, + "Triangle side lengths should be positive numbers." ) asin(opp/hyp); @@ -695,8 +759,8 @@ function hyp_opp_to_ang(hyp,opp) = // Example: // ang = adj_opp_to_ang(sqrt(3)/2,0.5); // Returns: 30 degrees function adj_opp_to_ang(adj,opp) = - assert(is_num(adj)&&adj>=0) - assert(is_num(opp)&&opp>=0) + assert(is_finite(adj+opp) && adj>0 && opp>=0, + "Triangle side lengths should be positive numbers." ) atan2(opp,adj); @@ -709,8 +773,11 @@ function adj_opp_to_ang(adj,opp) = // Examples: // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 -function triangle_area(a,b,c) = - len(a)==3? 0.5*norm(cross(c-a,c-b)) : ( +function triangle_area(a,b,c) = + assert( is_path([a,b,c]), + "Invalid points or incompatible dimensions." ) + len(a)==3 ? 0.5*norm(cross(c-a,c-b)) + : ( a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y) @@ -720,44 +787,53 @@ function triangle_area(a,b,c) = // Section: Planes + // Function: plane3pt() // Usage: // plane3pt(p1, p2, p3); // Description: -// Generates the cartesian equation of a plane from three non-collinear points on the plane. +// Generates the cartesian equation of a plane from three 3d points. // Returns [A,B,C,D] where Ax + By + Cz = D is the equation of a plane. +// Returns [], if the points are collinear. // Arguments: // p1 = The first point on the plane. // p2 = The second point on the plane. // p3 = The third point on the plane. function plane3pt(p1, p2, p3) = + assert( is_path([p1,p2,p3],dim=3) && len(p1)==3, + "Invalid points or incompatible dimensions." ) let( - p1=point3d(p1), - p2=point3d(p2), - p3=point3d(p3), - normal = unit(cross(p3-p1, p2-p1)) - ) concat(normal, [normal*p1]); + crx = cross(p3-p1, p2-p1), + nrm = norm(crx) + ) + approx(nrm,0) ? [] : + concat(crx/nrm, [crx*p1]/nrm); // Function: plane3pt_indexed() // Usage: // plane3pt_indexed(points, i1, i2, i3); // Description: -// Given a list of points, and the indices of three of those points, +// Given a list of 3d points, and the indices of three of those points, // generates the cartesian equation of a plane that those points all -// lie on. Requires that the three indexed points be non-collinear. -// Returns [A,B,C,D] where Ax+By+Cz=D is the equation of a plane. +// lie on. If the points are not collinear, returns [A,B,C,D] where Ax+By+Cz=D is the equation of a plane. +// If they are collinear, returns []. // Arguments: // points = A list of points. // i1 = The index into `points` of the first point on the plane. // i2 = The index into `points` of the second point on the plane. // i3 = The index into `points` of the third point on the plane. function plane3pt_indexed(points, i1, i2, i3) = + assert( is_vector([i1,i2,i3]) && min(i1,i2,i3)>=0 && is_list(points) && max(i1,i2,i3)=0, "The tolerance should be a positive number." ) let( points = deduplicate(points), - indices = sort(find_noncollinear_points(points)), + indices = noncollinear_triple(points,error=false) + ) + indices==[] ? undef : + let( + indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], - plane = plane3pt(p1,p2,p3), - all_coplanar = fast || all([ - for (pt = points) coplanar(plane,pt,eps=eps) - ]) - ) all_coplanar? plane : undef; + plane = plane3pt(p1,p2,p3) + ) + fast || points_on_plane(points,plane,eps=eps) ? plane : undef; // Function: plane_from_polygon() // Usage: // plane_from_polygon(points, [fast], [eps]); // Description: -// Given a 3D planar polygon, returns the cartesian equation of a plane. +// Given a 3D planar polygon, returns the cartesian equation of its plane. // Returns [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. -// If not all the points in the polygon are coplanar, then `undef` is returned. -// If `fast` is true, then a polygon where not all points are coplanar will -// result in an invalid plane value, as all coplanar checks are skipped. +// If not all the points in the polygon are coplanar, then [] is returned. +// If `fast` is true, the polygon coplanarity check is skipped and the plane may not contain all polygon points. // Arguments: // poly = The planar 3D polygon to find the plane of. -// fast = If true, don't verify that all points in the polygon are coplanar. Default: false +// fast = If true, doesn't verify that all points in the polygon are coplanar. Default: false // eps = How much variance is allowed in testing that each point is on the same plane. Default: `EPSILON` (1e-9) // Example(3D): // xyzpath = rot(45, v=[0,1,0], p=path3d(star(n=5,step=2,d=100), 70)); @@ -824,25 +904,29 @@ function plane_from_points(points, fast=false, eps=EPSILON) = // cp = centroid(xyzpath); // move(cp) rot(from=UP,to=plane_normal(plane)) anchor_arrow(); function plane_from_polygon(poly, fast=false, eps=EPSILON) = + assert( is_path(poly,dim=3), "Invalid polygon." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) let( poly = deduplicate(poly), n = polygon_normal(poly), plane = [n.x, n.y, n.z, n*poly[0]] - ) fast? plane : let( - all_coplanar = [ - for (pt = poly) - if (!coplanar(plane,pt,eps=eps)) 1 - ] == [] - ) all_coplanar? plane : - undef; + ) + fast? plane: coplanar(poly,eps=eps)? plane: []; +//*** +// I don't see why this function uses a criterium different from plane_from_points. +// In practical terms, what is the difference of finding a plane from points and from polygon? +// The docs don't clarify. +// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: // plane_normal(plane); // Description: // Returns the unit length normal vector for the given plane. -function plane_normal(plane) = unit([for (i=[0:2]) plane[i]]); +function plane_normal(plane) = + assert( _valid_plane(plane), "Invalid input plane." ) + unit([plane.x, plane.y, plane.z]); // Function: plane_offset() @@ -851,7 +935,9 @@ function plane_normal(plane) = unit([for (i=[0:2]) plane[i]]); // Description: // Returns D, or the scalar offset of the plane from the origin. This can be a negative value. // The absolute value of this is the distance of the plane from the origin at its closest approach. -function plane_offset(plane) = plane[3]; +function plane_offset(plane) = + assert( _valid_plane(plane), "Invalid input plane." ) + plane[3]/norm([plane.x, plane.y, plane.z]); // Function: plane_transform() @@ -859,8 +945,8 @@ function plane_offset(plane) = plane[3]; // mat = plane_transform(plane); // Description: // Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, returns a 3D affine -// transformation matrix that will rotate and translate from points on that plane -// to points on the XY plane. You can generally then use `path2d()` to drop the +// transformation matrix that will linear transform points on that plane +// into points on the XY plane. You can generally then use `path2d()` to drop the // Z coordinates, so you can work with the points in 2D. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. @@ -875,7 +961,34 @@ function plane_transform(plane) = let( n = plane_normal(plane), cp = n * plane[3] - ) rot(from=n, to=UP) * move(-cp); + ) + rot(from=n, to=UP) * move(-cp); + + +// Function: plane_projection() +// Usage: +// plane_projection(points); +// Description: +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection +// of the points on the plane. +// Arguments: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. +// points = List of points to project +// Example(3D): +// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100)))); +// plane = plane3pt([1,0,0],[0,1,0],[0,0,1]); +// proj = plane_projection(plane,points); +function plane_projection(plane, points) = + assert( _valid_plane(plane), "Invalid plane." ) + assert( is_path(points), "Invalid list of points or dimension." ) + let( + p = len(points[0])==2 + ? [for(pi=points) point3d(pi) ] + : points, + plane = plane/norm([plane.x,plane.y,plane.z]), + n = [plane.x,plane.y,plane.z] + ) + [for(pi=p) pi - (pi*n - plane[3])*n]; // Function: plane_point_nearest_origin() @@ -899,9 +1012,12 @@ function plane_point_nearest_origin(plane) = // will be negative. The normal of the plane is the same as [A,B,C]. // Arguments: // plane = The [A,B,C,D] values for the equation of the plane. -// point = The point to test. +// point = The distance evaluation point. function distance_from_plane(plane, point) = - [plane.x, plane.y, plane.z] * point3d(point) - plane[3]; + assert( _valid_plane(plane), "Invalid input plane." ) + assert( is_vector(point,3), "The point should be a 3D point." ) + let( nrml = [plane.x, plane.y, plane.z] ) + ( nrml* point - plane[3])/norm(nrml); // Function: closest_point_on_plane() @@ -911,13 +1027,16 @@ function distance_from_plane(plane, point) = // Takes a point, and a plane [A,B,C,D] where the equation of that plane is `Ax+By+Cz=D`. // Returns the coordinates of the closest point on that plane to the given `point`. // Arguments: -// plane = The [A,B,C,D] values for the equation of the plane. +// plane = The [A,B,C,D] coefficients for the equation of the plane. // point = The 3D point to find the closest point to. function closest_point_on_plane(plane, point) = + assert( _valid_plane(plane), "Invalid input plane." ) + assert( is_vector(point,3), "Invalid point." ) let( - n = unit(plane_normal(plane)), + n = unit([plane.x, plane.y, plane.z]), d = distance_from_plane(plane, point) - ) point - n*d; + ) + point - n*d; // Returns [POINT, U] if line intersects plane at one point. @@ -931,7 +1050,7 @@ function _general_plane_line_intersection(plane, line, eps=EPSILON) = u = p1 - p0, d = n * u ) abs(d)=0, "The tolerance should be a positive number." ) + assert(_valid_plane(plane,eps=eps) && _valid_line(line,dim=3,eps=eps), "Invalid plane and/or line.") + assert(is_bool(bounded) || (is_list(bounded) && len(bounded)==2), "Invalid bound condition(s).") let( bounded = is_list(bounded)? bounded : [bounded, bounded], res = _general_plane_line_intersection(plane, line, eps=eps) ) - is_undef(res)? undef : - is_undef(res[1])? res[0] : - bounded[0]&&res[1]<0? undef : - bounded[1]&&res[1]>1? undef : + is_undef(res) ? undef : + is_undef(res[1]) ? res[0] : + bounded[0] && res[1]<0 ? undef : + bounded[1] && res[1]>1 ? undef : res[0]; @@ -988,22 +1116,28 @@ function plane_line_intersection(plane, line, bounded=false, eps=EPSILON) = // pt = polygon_line_intersection(poly, line, [bounded], [eps]); // Description: // Takes a possibly bounded line, and a 3D planar polygon, and finds their intersection point. -// If the line is on the plane as the polygon, and intersects, then a list of 3D line -// segments is returned, one for each section of the line that is inside the polygon. -// If the line is not on the plane of the polygon, but intersects, then the 3D intersection -// point is returned. If the line does not intersect the polygon, then `undef` is returned. +// If the line and the polygon are on the same plane then returns a list, possibly empty, of 3D line +// segments, one for each section of the line that is inside the polygon. +// If the line is not on the plane of the polygon, but intersects it, then returns the 3D intersection +// point. If the line does not intersect the polygon, then `undef` is returned. // Arguments: // poly = The 3D planar polygon to find the intersection with. -// line = A list of two 3D points that are on the line. +// line = A list of two distinct 3D points on the line. // bounded = If false, the line is considered unbounded. If true, it is treated as a bounded line segment. If given as `[true, false]` or `[false, true]`, the boundedness of the points are specified individually, allowing the line to be treated as a half-bounded ray. Default: false (unbounded) -// eps = The epsilon error value to determine whether the line is too close to parallel to the plane. Default: `EPSILON` (1e-9) +// eps = The tolerance value in determining whether the line is parallel to the plane. Default: `EPSILON` (1e-9) function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = - assert(is_path(poly)) - assert(is_path(line)&&len(line)==2) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert(is_path(poly,dim=3), "Invalid polygon." ) + assert(is_bool(bounded) || (is_list(bounded) && len(bounded)==2), "Invalid bound condition(s).") + assert(_valid_line(line,dim=3,eps=eps), "Invalid line." ) let( bounded = is_list(bounded)? bounded : [bounded, bounded], poly = deduplicate(poly), - indices = sort(find_noncollinear_points(poly)), + indices = noncollinear_triple(poly) + ) + indices==[] ? undef : + let( + indices = sort(indices), // why sorting? p1 = poly[indices[0]], p2 = poly[indices[1]], p3 = poly[indices[2]], @@ -1011,34 +1145,31 @@ function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = res = _general_plane_line_intersection(plane, line, eps=eps) ) is_undef(res)? undef : - is_undef(res[1])? ( - let( - // Line is on polygon plane. + is_undef(res[1]) + ? ( let(// Line is on polygon plane. linevec = unit(line[1] - line[0]), lp1 = line[0] + (bounded[0]? 0 : -1000000) * linevec, lp2 = line[1] + (bounded[1]? 0 : 1000000) * linevec, poly2d = clockwise_polygon(project_plane(poly, p1, p2, p3)), line2d = project_plane([lp1,lp2], p1, p2, p3), parts = split_path_at_region_crossings(line2d, [poly2d], closed=false), - inside = [ - for (part = parts) - if (point_in_polygon(mean(part), poly2d)>0) part - ] - ) !inside? undef : + inside = [for (part = parts) + if (point_in_polygon(mean(part), poly2d)>0) part + ] + ) + !inside? undef : let( - isegs = [ - for (seg = inside) - lift_plane(seg, p1, p2, p3) - ] - ) isegs - ) : - bounded[0]&&res[1]<0? undef : - bounded[1]&&res[1]>1? undef : - let( - proj = clockwise_polygon(project_plane(poly, p1, p2, p3)), - pt = project_plane(res[0], p1, p2, p3) - ) point_in_polygon(pt, proj) < 0? undef : - res[0]; + isegs = [for (seg = inside) lift_plane(seg, p1, p2, p3) ] + ) + isegs + ) + : bounded[0]&&res[1]<0? [] : + bounded[1]&&res[1]>1? [] : + let( + proj = clockwise_polygon(project_plane(poly, p1, p2, p3)), + pt = project_plane(res[0], p1, p2, p3) + ) + point_in_polygon(pt, proj) < 0 ? undef : res[0]; // Function: plane_intersection() @@ -1047,53 +1178,62 @@ function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = // Description: // Compute the point which is the intersection of the three planes, or the line intersection of two planes. // If you give three planes the intersection is returned as a point. If you give two planes the intersection -// is returned as a list of two points on the line of intersection. If any of the input planes are parallel -// then returns undef. +// is returned as a list of two points on the line of intersection. If any two input planes are parallel +// or coincident then returns undef. function plane_intersection(plane1,plane2,plane3) = - is_def(plane3)? let( - matrix = [for(p=[plane1,plane2,plane3]) select(p,0,2)], - rhs = [for(p=[plane1,plane2,plane3]) p[3]] - ) linear_solve(matrix,rhs) : - let( - normal = cross(plane_normal(plane1), plane_normal(plane2)) - ) approx(norm(normal),0) ? undef : - let( - matrix = [for(p=[plane1,plane2]) select(p,0,2)], - rhs = [for(p=[plane1,plane2]) p[3]], - point = linear_solve(matrix,rhs) - ) is_undef(point)? undef : - [point, point+normal]; + assert( _valid_plane(plane1) && _valid_plane(plane2) && (is_undef(plane3) ||_valid_plane(plane3)), + "The input must be 2 or 3 planes." ) + is_def(plane3) + ? let( + matrix = [for(p=[plane1,plane2,plane3]) select(p,0,2)], + rhs = [for(p=[plane1,plane2,plane3]) p[3]] + ) + linear_solve(matrix,rhs) + : let( normal = cross(plane_normal(plane1), plane_normal(plane2)) ) + approx(norm(normal),0) ? undef : + let( + matrix = [for(p=[plane1,plane2]) select(p,0,2)], + rhs = [for(p=[plane1,plane2]) p[3]], + point = linear_solve(matrix,rhs) + ) + point==[]? undef: [point, point+normal]; // Function: coplanar() // Usage: -// coplanar(plane, point); +// coplanar(points,eps); // Description: -// Given a plane as [A,B,C,D] where the cartesian equation for that plane -// is Ax+By+Cz=D, determines if the given point is on that plane. -// Returns true if the point is on that plane. +// Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: -// plane = The [A,B,C,D] values for the equation of the plane. -// point = The point to test. -// eps = How much variance is allowed in testing that each point is on the same plane. Default: `EPSILON` (1e-9) -function coplanar(plane, point, eps=EPSILON) = - abs(distance_from_plane(plane, point)) <= eps; +// points = The points to test. +// eps = How much variance is allowed in the planarity test. Default: `EPSILON` (1e-9) +function coplanar(points, eps=EPSILON) = + assert( is_path(points,dim=3) , "Input should be a list of 3D points." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a non-negative number." ) + len(points)<=2 ? false + : let( ip = noncollinear_triple(points,error=false,eps=eps) ) + ip == [] ? false : + let( plane = plane3pt(points[ip[0]],points[ip[1]],points[ip[2]]), + normal = point3d(plane) ) + max( points*normal ) - plane[3]< eps*norm(normal); - -// Function: points_are_coplanar() + +// Function: points_on_plane() // Usage: -// points_are_coplanar(points, [eps]); +// points_on_plane(points, plane, eps); // Description: -// Given a list of points, returns true if all points in the list are coplanar. +// Returns true if the given 3D points are on the given plane. // Arguments: -// points = The list of points to test. -// eps = How much variance is allowed in testing that each point is on the same plane. Default: `EPSILON` (1e-9) -function points_are_coplanar(points, eps=EPSILON) = - points_are_collinear(points, eps=eps)? true : - let( - plane = plane_from_points(points, fast=true, eps=eps) - ) all([for (pt = points) coplanar(plane, pt, eps=eps)]); - +// plane = The plane to test the points on. +// points = The list of 3D points to test. +// eps = How much variance is allowed in the planarity testing. Default: `EPSILON` (1e-9) +function points_on_plane(points, plane, eps=EPSILON) = + assert( _valid_plane(plane), "Invalid plane." ) + assert( is_matrix(points,undef,3) && len(points)>0, "Invalid pointlist." ) // using is_matrix it accepts len(points)==1 + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + let( normal = point3d(plane), + pt_nrm = points*normal ) + abs(max( max(pt_nrm) - plane[3], -min(pt_nrm)+plane[3]))< eps*norm(normal); // Function: in_front_of_plane() @@ -1101,12 +1241,12 @@ function points_are_coplanar(points, eps=EPSILON) = // in_front_of_plane(plane, point); // Description: // Given a plane as [A,B,C,D] where the cartesian equation for that plane -// is Ax+By+Cz=D, determines if the given point is on the side of that +// is Ax+By+Cz=D, determines if the given 3D point is on the side of that // plane that the normal points towards. The normal of the plane is the // same as [A,B,C]. // Arguments: -// plane = The [A,B,C,D] values for the equation of the plane. -// point = The point to test. +// plane = The [A,B,C,D] coefficients for the equation of the plane. +// point = The 3D point to test. function in_front_of_plane(plane, point) = distance_from_plane(plane, point) > EPSILON; @@ -1156,37 +1296,45 @@ function in_front_of_plane(plane, point) = function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = let(r = get_radius(r=r, d=d, dflt=undef)) assert(r!=undef, "Must specify either r or d.") - (is_undef(pt2) && is_undef(pt3) && is_list(pt1))? find_circle_2tangents(pt1[0], pt1[1], pt1[2], r=r) : - collinear(pt1, pt2, pt3)? undef : - let( - v1 = unit(pt1 - pt2), - v2 = unit(pt3 - pt2), - vmid = unit(mean([v1, v2])), - n = vector_axis(v1, v2), - a = vector_angle(v1, v2), - hyp = r / sin(a/2), - cp = pt2 + hyp * vmid - ) !tangents? [cp, n] : - let( - x = hyp * cos(a/2), - tp1 = pt2 + x * v1, - tp2 = pt2 + x * v2, - fff=echo(tp1=tp1,cp=cp,pt2=pt2), - dang1 = vector_angle(tp1-cp,pt2-cp), - dang2 = vector_angle(tp2-cp,pt2-cp) - ) [cp, n, tp1, tp2, dang1, dang2]; + assert( ( is_path(pt1) && len(pt1)==3 && is_undef(pt2) && is_undef(pt3)) + || (is_matrix([pt1,pt2,pt3]) && (len(pt1)==2 || len(pt1)==3) ), + "Invalid input points." ) + is_undef(pt2) + ? find_circle_2tangents(pt1[0], pt1[1], pt1[2], r=r, tangents=tangents) + : collinear(pt1, pt2, pt3)? undef : + let( + v1 = unit(pt1 - pt2), + v2 = unit(pt3 - pt2), + vmid = unit(mean([v1, v2])), + n = vector_axis(v1, v2), + a = vector_angle(v1, v2), + hyp = r / sin(a/2), + cp = pt2 + hyp * vmid + ) + !tangents ? [cp, n] : + let( + x = hyp * cos(a/2), + tp1 = pt2 + x * v1, + tp2 = pt2 + x * v2, +// fff=echo(tp1=tp1,cp=cp,pt2=pt2), + dang1 = vector_angle(tp1-cp,pt2-cp), + dang2 = vector_angle(tp2-cp,pt2-cp) + ) + [cp, n, tp1, tp2, dang1, dang2]; // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, pt2, pt3); +// find_circle_3points(pt1, [pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear -// points. The centerpoint will be a 2D or 3D vector, depending on the points input. If all three +// points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). +// The centerpoint will be a 2D or 3D vector, depending on the points input. If all three // points are 2D, then the resulting centerpoint will be 2D, and the normal will be UP ([0,0,1]). // If any of the points are 3D, then the resulting centerpoint will be 3D. If the three points are // collinear, then `[undef,undef,undef]` will be returned. The normal will be a normalized 3D // vector with a non-negative Z axis. +// Instead of 3 arguments, it is acceptable to input the 3 points in a list `pt1`, leaving `pt2`and `pt3` as undef. // Arguments: // pt1 = The first point. // pt2 = The second point. @@ -1198,34 +1346,65 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // translate(circ[0]) color("red") circle(d=3, $fn=12); // move_copies(pts) color("blue") circle(d=3, $fn=12); function find_circle_3points(pt1, pt2, pt3) = - (is_undef(pt2) && is_undef(pt3) && is_list(pt1))? find_circle_3points(pt1[0], pt1[1], pt1[2]) : - collinear(pt1,pt2,pt3)? [undef,undef,undef] : - let( - v1 = pt1-pt2, - v2 = pt3-pt2, - n = vector_axis(v1,v2), - n2 = n.z<0? -n : n - ) len(pt1)+len(pt2)+len(pt3)>6? ( + (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) + ? find_circle_3points(pt1[0], pt1[1], pt1[2]) + : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) + && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, + "Invalid point(s)." ) + collinear(pt1,pt2,pt3)? [undef,undef,undef] : let( - a = project_plane(pt1, pt1, pt2, pt3), - b = project_plane(pt2, pt1, pt2, pt3), - c = project_plane(pt3, pt1, pt2, pt3), - res = find_circle_3points(a, b, c) - ) res[0]==undef? [undef,undef,undef] : let( - cp = lift_plane(res[0], pt1, pt2, pt3), - r = norm(pt2-cp) - ) [cp, r, n2] - ) : let( - mp1 = pt2 + v1/2, - mp2 = pt2 + v2/2, - mpv1 = rot(90, v=n, p=v1), - mpv2 = rot(90, v=n, p=v2), - l1 = [mp1, mp1+mpv1], - l2 = [mp2, mp2+mpv2], - isect = line_intersection(l1,l2) - ) is_undef(isect)? [undef,undef,undef] : let( - r = norm(pt2-isect) - ) [isect, r, n2]; + v1 = pt1-pt2, + v2 = pt3-pt2, + n = vector_axis(v1,v2), + n2 = n.z<0? -n : n + ) len(pt1)+len(pt2)+len(pt3)>6? ( + let( + a = project_plane(pt1, pt1, pt2, pt3), + b = project_plane(pt2, pt1, pt2, pt3), + c = project_plane(pt3, pt1, pt2, pt3), + res = find_circle_3points(a, b, c) + ) res[0]==undef? [undef,undef,undef] : let( + cp = lift_plane(res[0], pt1, pt2, pt3), + r = norm(pt2-cp) + ) [cp, r, n2] + ) : let( + mp1 = pt2 + v1/2, + mp2 = pt2 + v2/2, + mpv1 = rot(90, v=n, p=v1), + mpv2 = rot(90, v=n, p=v2), + l1 = [mp1, mp1+mpv1], + l2 = [mp2, mp2+mpv2], + isect = line_intersection(l1,l2) + ) is_undef(isect)? [undef,undef,undef] : let( + r = norm(pt2-isect) + ) [isect, r, n2]; + +function find_circle_3points(pt1, pt2, pt3) = + (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) + ? find_circle_3points(pt1[0], pt1[1], pt1[2]) + : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) + && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, + "Invalid point(s)." ) + collinear(pt1,pt2,pt3)? [undef,undef,undef] : + let( + v = [ point3d(pt1), point3d(pt2), point3d(pt3) ], // triangle vertices + ed = [for(i=[0:2]) v[(i+1)%3]-v[i] ], // triangle edge vectors + pm = [for(i=[0:2]) v[(i+1)%3]+v[i] ]/2, // edge mean points + es = sortidx( [for(di=ed) norm(di) ] ), + e1 = ed[es[1]], // take the 2 longest edges + e2 = ed[es[2]], + n0 = vector_axis(e1,e2), // normal standardization + n = n0.z<0? -n0 : n0, + sc = plane_intersection( + [ each e1, e1*pm[es[1]] ], // planes orthogonal to 2 edges + [ each e2, e2*pm[es[2]] ], + [ each n, n*v[0] ] ) , // triangle plane + cp = len(pt1)+len(pt2)+len(pt3)>6 ? sc: [sc.x, sc.y], + r = norm(sc-v[0]) + ) + [ cp, r, n ]; + + @@ -1233,14 +1412,14 @@ function find_circle_3points(pt1, pt2, pt3) = // Usage: // tangents = circle_point_tangents(r|d, cp, pt); // Description: -// Given a circle and a point outside that circle, finds the tangent point(s) on the circle for a +// Given a 2d circle and a 2d point outside that circle, finds the 2d tangent point(s) on the circle for a // line passing through the point. Returns list of zero or more sublists of [ANG, TANGPT] // Arguments: // r = Radius of the circle. // d = Diameter of the circle. -// cp = The coordinates of the circle centerpoint. -// pt = The coordinates of the external point. -// Example(2D): +// cp = The coordinates of the 2d circle centerpoint. +// pt = The coordinates of the 2d external point. +// Example: // cp = [-10,-10]; r = 30; pt = [30,10]; // tanpts = subindex(circle_point_tangents(r=r, cp=cp, pt=pt),1); // color("yellow") translate(cp) circle(r=r); @@ -1248,9 +1427,8 @@ function find_circle_3points(pt1, pt2, pt3) = // color("red") move_copies(tanpts) circle(d=3,$fn=12); // color("blue") move_copies([cp,pt]) circle(d=3,$fn=12); function circle_point_tangents(r, d, cp, pt) = - assert(is_num(r) || is_num(d)) - assert(is_vector(cp)) - assert(is_vector(pt)) + assert(is_finite(r) || is_finite(d), "Invalid radius or diameter." ) + assert(is_path([cp, pt],dim=2), "Invalid center point or external point.") let( r = get_radius(r=r, d=d, dflt=1), delta = pt - cp, @@ -1268,7 +1446,7 @@ function circle_point_tangents(r, d, cp, pt) = // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: -// Computes lines tangents to a pair of circles. Returns a list of line endpoints [p1,p2] where +// Computes 2d lines tangents to a pair of circles in 2d. Returns a list of line endpoints [p1,p2] where // p2 is the tangent point on circle 1 and p2 is the tangent point on circle 2. // If four tangents exist then the first one the left hand exterior tangent as regarded looking from // circle 1 toward circle 2. The second value is the right hand exterior tangent. The third entry @@ -1314,6 +1492,7 @@ function circle_point_tangents(r, d, cp, pt) = // move(c2) stroke(circle(r=r2), width=.1, closed=true); // echo(pts); // Returns [] function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = + assert( is_path([c1,c2],dim=2), "Invalid center point(s)." ) let( r1 = get_radius(r1=r1,d1=d1), r2 = get_radius(r1=r2,d1=d2), @@ -1342,25 +1521,32 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = // Section: Pointlists -// Function: find_noncollinear_points() +// Function: noncollinear_triple() // Usage: -// find_noncollinear_points(points); +// noncollinear_triple(points); // Description: // Finds the indices of three good non-collinear points from the points list `points`. -function find_noncollinear_points(points,error=true,eps=EPSILON) = +// If all points are collinear, returns []. +function noncollinear_triple(points,error=true,eps=EPSILON) = + assert( is_path(points), "Invalid input points." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative number." ) let( pa = points[0], - b = furthest_point(pa, points), - n = unit(points[b]-pa), - relpoints = [for(pt=points) pt-pa], - proj = relpoints * n, - distlist = [for(i=[0:len(points)-1]) norm(relpoints[i]-proj[i]*n)] - ) - max(distlist)0 && len(pts[0])>0 , "Invalid pointlist." ) let(ptsT = transpose(pts)) [ [for(row=ptsT) min(row)], [for(row=ptsT) max(row)] ]; + // Function: closest_point() // Usage: // closest_point(pt, points); @@ -1388,6 +1575,8 @@ function pointlist_bounds(pts) = // pt = The point to find the closest point to. // points = The list of points to search. function closest_point(pt, points) = + assert( is_vector(pt), "Invalid point." ) + assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) min_index([for (p=points) norm(p-pt)]); @@ -1400,6 +1589,8 @@ function closest_point(pt, points) = // pt = The point to find the farthest point from. // points = The list of points to search. function furthest_point(pt, points) = + assert( is_vector(pt), "Invalid point." ) + assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) max_index([for (p=points) norm(p-pt)]); @@ -1410,8 +1601,14 @@ function furthest_point(pt, points) = // Usage: // area = polygon_area(poly); // Description: -// Given a 2D or 3D planar polygon, returns the area of that polygon. If the polygon is self-crossing, the results are undefined. +// Given a 2D or 3D planar polygon, returns the area of that polygon. +// If the polygon is self-crossing, the results are undefined. For non-planar points the result is undef. +// When `signed` is true, a signed area is returned; a positive area indicates a counterclockwise polygon. +// Arguments: +// poly = polygon to compute the area of. +// signed = if true, a signed area is returned (default: false) function polygon_area(poly) = + assert(is_path(poly), "Invalid polygon." ) len(poly)<3? 0 : len(poly[0])==2? 0.5*sum([for(i=[0:1:len(poly)-1]) det2(select(poly,i,i+1))]) : let( @@ -1422,19 +1619,33 @@ function polygon_area(poly) = total = sum([for (i=[0:1:len(poly)-1]) cross(poly[i], select(poly,i+1))]), res = abs(total * n) / 2 ) res; + +function polygon_area(poly, signed=false) = + assert(is_path(poly), "Invalid polygon." ) + len(poly)<3 ? 0 : + len(poly[0])==2 + ? sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 + : let( plane = plane_from_points(poly) ) + plane==undef? undef : + let( n = unit(plane_normal(plane)), + total = sum([for(i=[1:1:len(poly)-1]) cross(poly[i]-poly[0],poly[i+1]-poly[0])*n ])/2 + ) + signed ? total : abs(total); -// Function: polygon_is_convex() +// Function: is_convex_polygon() // Usage: -// polygon_is_convex(poly); +// is_convex_polygon(poly); // Description: -// Returns true if the given polygon is convex. Result is undefined if the polygon is self-intersecting. +// Returns true if the given 2D polygon is convex. The result is meaningless if the polygon is not simple (self-intersecting). +// If the points are collinear the result is true. // Example: -// polygon_is_convex(circle(d=50)); // Returns: true +// is_convex_polygon(circle(d=50)); // Returns: true // Example: // spiral = [for (i=[0:36]) let(a=-i*10) (10+i)*[cos(a),sin(a)]]; -// polygon_is_convex(spiral); // Returns: false -function polygon_is_convex(poly) = +// is_convex_polygon(spiral); // Returns: false +function is_convex_polygon(poly) = + assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly), c = [for (i=idx(poly)) cross(poly[(i+1)%l]-poly[i],poly[(i+2)%l]-poly[(i+1)%l])] @@ -1442,6 +1653,19 @@ function polygon_is_convex(poly) = len([for (x=c) if(x>0) 1])==0 || len([for (x=c) if(x<0) 1])==0; +function is_convex_polygon(poly) = + assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) + let( l = len(poly) ) + len([for( i = l-1, + c = cross(poly[(i+1)%l]-poly[i], poly[(i+2)%l]-poly[(i+1)%l]), + s = sign(c); + i>=0 && sign(c)==s; + i = i-1, + c = i<0? 0: cross(poly[(i+1)%l]-poly[i],poly[(i+2)%l]-poly[(i+1)%l]), + s = s==0 ? sign(c) : s + ) i + ])== l; + // Function: polygon_shift() // Usage: @@ -1454,6 +1678,7 @@ function polygon_is_convex(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) = + assert(is_path(poly), "Invalid polygon." ) list_rotate(cleanup_path(poly), i); @@ -1463,6 +1688,8 @@ function polygon_shift(poly, i) = // Description: // Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. function polygon_shift_to_closest_point(path, pt) = + assert(is_vector(pt), "Invalid point." ) + assert(is_path(path,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) let( path = cleanup_path(path), dists = [for (p=path) norm(p-pt)], @@ -1500,8 +1727,9 @@ function polygon_shift_to_closest_point(path, pt) = // 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) = - assert(is_path(reference) && is_path(poly)) - assert(len(reference)==len(poly), "Polygons must be the same length in reindex_polygon") + 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.") let( dim = len(reference[0]), N = len(reference), @@ -1525,13 +1753,34 @@ function reindex_polygon(reference, poly, return_error=false) = return_error? [optimal_poly, min(sums)] : optimal_poly; +function reindex_polygon(reference, poly, return_error=false) = + 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.") + let( + dim = len(reference[0]), + N = len(reference), + fixpoly = dim != 2? poly : + polygon_is_clockwise(reference) + ? clockwise_polygon(poly) + : ccw_polygon(poly), + I = [for(i=[0:N-1]) 1], + val = [ for(k=[0:N-1]) +           [for(i=[0:N-1]) +              (reference[i]*poly[(i+k)%N]) ] ]*I, + optimal_poly = polygon_shift(fixpoly, max_index(val)) + ) + return_error? [optimal_poly, min(poly*(I*poly)-2*val)] : + optimal_poly; + + // Function: align_polygon() // Usage: // newpoly = align_polygon(reference, poly, angles, [cp]); // Description: -// Tries the list or range of angles to find a rotation of the specified polygon that best aligns -// with the reference polygon. For each angle, the polygon is reindexed, which is a costly operation +// Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns +// with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation // so if run time is a problem, use a smaller sampling of angles. Returns the rotated and reindexed // polygon. // Arguments: @@ -1546,9 +1795,11 @@ function reindex_polygon(reference, poly, return_error=false) = // color("red") move_copies(scale(1.4,p=align_polygon(pentagon,hexagon,[0:10:359]))) circle(r=.1); // move_copies(concat(pentagon,hexagon))circle(r=.1); function align_polygon(reference, poly, angles, cp) = - assert(is_path(reference) && is_path(poly)) - assert(len(reference)==len(poly), "Polygons must be the same length to be aligned in align_polygon") - assert(is_num(angles[0]), "The `angle` parameter to align_polygon must be a range or vector") + assert(is_path(reference,dim=2) && is_path(poly,dim=2), + "Invalid polygon(s). " ) + assert(len(reference)==len(poly), "The polygons must have the same length.") + assert( (is_vector(angles) && len(angles)>0) || valid_range(angles), + "The `angle` parameter must be a range or a non void list of numbers.") let( // alignments is a vector of entries of the form: [polygon, error] alignments = [ for(angle=angles) reindex_polygon( @@ -1569,23 +1820,49 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. function centroid(poly) = - len(poly[0])==2? ( - sum([ + assert( is_path(poly), "The input must be a 2D or 3D polygon." ) + len(poly[0])==2 + ? sum([ for(i=[0:len(poly)-1]) let(segment=select(poly,i,i+1)) det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) - ) : ( - let( - n = plane_normal(plane_from_points(poly)), - p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), - p2 = vector_axis(n,p1), - cp = mean(poly), - proj = project_plane(poly,cp,cp+p1,cp+p2), - cxy = centroid(proj) - ) lift_plane(cxy,cp,cp+p1,cp+p2) - ); + ]) / 6 / polygon_area(poly) + : let( plane = plane_from_points(poly, fast=true) ) + assert( !is_undef(plane), "The polygon must be planar." ) + let( + n = plane_normal(plane), + p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), + p2 = vector_axis(n,p1), + cp = mean(poly), + proj = project_plane(poly,cp,cp+p1,cp+p2), + cxy = centroid(proj) + ) + lift_plane(cxy,cp,cp+p1,cp+p2); +function centroid(poly) = + assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) + len(poly[0])==2 + ? sum([ + for(i=[0:len(poly)-1]) + let(segment=select(poly,i,i+1)) + det2(segment)*sum(segment) + ]) / 6 / polygon_area(poly) + : let( plane = plane_from_points(poly, fast=true) ) + assert( !is_undef(plane), "The polygon must be planar." ) + let( + n = plane_normal(plane), + val = sum([for(i=[1:len(poly)-2]) + let( + v0 = poly[0], + v1 = poly[i], + v2 = poly[i+1], + area = cross(v2-v0,v1-v0)*n + ) + [ area, (v0+v1+v2)*area ] + ] ) + ) + val[1]/val[0]/3; + // Function: point_in_polygon() // Usage: @@ -1606,11 +1883,29 @@ function centroid(poly) = // eps = Acceptable variance. Default: `EPSILON` (1e-9) function point_in_polygon(point, path, eps=EPSILON) = // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, + "The point and polygon should be in 2D. The polygon should have more that 2 points." ) + assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. - sum([for(i=[0:1:len(path)-1]) let(seg=select(path,i,i+1)) if(!approx(seg[0],seg[1],eps=eps)) point_on_segment2d(point, seg, eps=eps)?1:0]) > 0? 0 : + let( + on_brd = [for(i=[0:1:len(path)-1]) + let( seg = select(path,i,i+1) ) + if( !approx(seg[0],seg[1],eps=eps) ) + point_on_segment2d(point, seg, eps=eps)? 1:0 ] + ) + sum(on_brd) > 0? 0 : // Otherwise compute winding number and return 1 for interior, -1 for exterior - sum([for(i=[0:1:len(path)-1]) let(seg=select(path,i,i+1)) if(!approx(seg[0],seg[1],eps=eps)) _point_above_below_segment(point, seg)]) != 0? 1 : -1; + let( + windchk = [for(i=[0:1:len(path)-1]) + let(seg=select(path,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1; +//** +// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: @@ -1621,7 +1916,7 @@ function point_in_polygon(point, path, eps=EPSILON) = // Arguments: // path = The list of 2D path points for the perimeter of the polygon. function polygon_is_clockwise(path) = - assert(is_path(path) && len(path[0])==2, "Input must be a 2d path") + assert(is_path(path,dim=2), "Input should be a 2d polygon") let( minx = min(subindex(path,0)), lowind = search(minx, path, 0, 0), @@ -1631,6 +1926,9 @@ function polygon_is_clockwise(path) = extreme = select(lowind,extreme_sub) ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; +function polygon_is_clockwise(path) = + assert(is_path(path,dim=2), "Input should be a 2d path") + polygon_area(path, signed=true)<0; // Function: clockwise_polygon() // Usage: @@ -1638,7 +1936,8 @@ function polygon_is_clockwise(path) = // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. function clockwise_polygon(path) = - polygon_is_clockwise(path)? path : reverse_polygon(path); + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); // Function: ccw_polygon() @@ -1647,7 +1946,8 @@ function clockwise_polygon(path) = // Description: // Given a 2D polygon path, returns the counter-clockwise winding version of that path. function ccw_polygon(path) = - polygon_is_clockwise(path)? reverse_polygon(path) : path; + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; // Function: reverse_polygon() @@ -1656,6 +1956,7 @@ function ccw_polygon(path) = // Description: // Reverses a polygon's winding direction, while still using the same start point. function reverse_polygon(poly) = + assert(is_path(poly), "Input should be a polygon") let(lp=len(poly)) [for (i=idx(poly)) poly[(lp-i)%lp]]; @@ -1666,6 +1967,7 @@ function reverse_polygon(poly) = // Given a 3D planar polygon, returns a unit-length normal vector for the // clockwise orientation of the polygon. function polygon_normal(poly) = + assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( poly = path3d(cleanup_path(poly)), p0 = poly[0], @@ -1772,6 +2074,9 @@ function _split_polygon_at_z(poly, z) = // polys = A list of 3D polygons to split. // xs = A list of scalar X values to split at. function split_polygons_at_each_x(polys, xs, _i=0) = + assert( is_consistent(polys) && is_path(poly[0],dim=3) , + "The input list should contains only 3D polygons." ) + assert( is_finite(xs), "The split value list should contain only numbers." ) _i>=len(xs)? polys : split_polygons_at_each_x( [ @@ -1779,6 +2084,17 @@ function split_polygons_at_each_x(polys, xs, _i=0) = each _split_polygon_at_x(poly, xs[_i]) ], xs, _i=_i+1 ); + +//*** +// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: +// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) +// produces: +// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] +// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] +// and the second polygon is self-intersecting +// besides, it fails in some simple cases as triangles: +// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] +// this last failure may be fatal for vnf_bend // Function: split_polygons_at_each_y() @@ -1790,6 +2106,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = + assert( is_consistent(polys) && is_path(poly[0],dim=3) , + "The input list should contains only 3D polygons." ) + assert( is_finite(ys), "The split value list should contain only numbers." ) _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -1808,6 +2127,9 @@ function split_polygons_at_each_y(polys, ys, _i=0) = // polys = A list of 3D polygons to split. // zs = A list of scalar Z values to split at. function split_polygons_at_each_z(polys, zs, _i=0) = + assert( is_consistent(polys) && is_path(poly[0],dim=3) , + "The input list should contains only 3D polygons." ) + assert( is_finite(zs), "The split value list should contain only numbers." ) _i>=len(zs)? polys : split_polygons_at_each_z( [ diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 8f780f4..da01a97 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -1,6 +1,144 @@ include <../std.scad> +//the commented lines are for tests to be written +//the tests are ordered as they appear in geometry.scad + +test_point_on_segment2d(); +test_point_left_of_line2d(); +test_collinear(); +test_distance_from_line(); +test_line_normal(); +test_line_intersection(); +//test_line_ray_intersection(); +test_line_segment_intersection(); +//test_ray_intersection(); +//test_ray_segment_intersection(); +test_segment_intersection(); +test_line_closest_point(); +//test_ray_closest_point(); +test_segment_closest_point(); +test_line_from_points(); +test_tri_calc(); +//test_hyp_opp_to_adj(); +//test_hyp_ang_to_adj(); +//test_opp_ang_to_adj(); +//test_hyp_adj_to_opp(); +//test_hyp_ang_to_opp(); +//test_adj_ang_to_opp(); +//test_adj_opp_to_hyp(); +//test_adj_ang_to_hyp(); +//test_opp_ang_to_hyp(); +//test_hyp_adj_to_ang(); +//test_hyp_opp_to_ang(); +//test_adj_opp_to_ang(); +test_triangle_area(); +test_plane3pt(); +test_plane3pt_indexed(); +//test_plane_from_normal(); +test_plane_from_points(); +//test_plane_from_polygon(); +test_plane_normal(); +//test_plane_offset(); +//test_plane_transform(); +test_plane_projection(); +//test_plane_point_nearest_origin(); +test_distance_from_plane(); + +test_find_circle_2tangents(); +test_find_circle_3points(); +test_circle_point_tangents(); +test_tri_functions(); +//test_closest_point_on_plane(); +//test__general_plane_line_intersection(); +//test_plane_line_angle(); +//test_plane_line_intersection(); +//test_polygon_line_intersection(); +//test_plane_intersection(); +test_coplanar(); +test_points_on_plane(); +test_in_front_of_plane(); +//test_find_circle_2tangents(); +//test_find_circle_3points(); +//test_circle_point_tangents(); +//test_circle_circle_tangents(); +test_noncollinear_triple(); +test_pointlist_bounds(); +test_closest_point(); +test_furthest_point(); +test_polygon_area(); +test_is_convex_polygon(); +test_polygon_shift(); +test_polygon_shift_to_closest_point(); +test_reindex_polygon(); +test_align_polygon(); +test_centroid(); +test_point_in_polygon(); +test_polygon_is_clockwise(); +test_clockwise_polygon(); +test_ccw_polygon(); +test_reverse_polygon(); +//test_polygon_normal(); +//test_split_polygons_at_each_x(); +//test_split_polygons_at_each_y(); +//test_split_polygons_at_each_z(); + +//tests to migrate to other files +test_is_path(); +test_is_closed_path(); +test_close_path(); +test_cleanup_path(); +test_simplify_path(); +test_simplify_path_indexed(); +test_is_region(); + +// to be used when there are two alternative symmetrical outcomes +// from a function like a plane output. +function standardize(v) = + v==[]? [] : + sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; + +module test_points_on_plane() { + pts = [for(i=[0:40]) rands(-1,1,3) ]; + dir = rands(-10,10,3); + normal0 = unit([1,2,3]); + ang = rands(0,360,1)[0]; + normal = rot(a=ang,p=normal0); + plane = [each normal, normal*dir]; + prj_pts = plane_projection(plane,pts); + assert(points_on_plane(prj_pts,plane)); + assert(!points_on_plane(concat(pts,[normal-dir]),plane)); +} +*test_points_on_plane(); + +module test_plane_projection(){ + ang = rands(0,360,1)[0]; + dir = rands(-10,10,3); + normal0 = unit([1,2,3]); + normal = rot(a=ang,p=normal0); + plane0 = [each normal0, 0]; + plane = [each normal, 0]; + planem = [each normal, normal*dir]; + pts = [for(i=[1:10]) rands(-1,1,3)]; + assert_approx( plane_projection(plane,pts), + plane_projection(plane,plane_projection(plane,pts))); + assert_approx( plane_projection(plane,pts), + rot(a=ang,p=plane_projection(plane0,rot(a=-ang,p=pts)))); + assert_approx( move((-normal*dir)*normal,p=plane_projection(planem,pts)), + plane_projection(plane,pts)); + assert_approx( move((normal*dir)*normal,p=plane_projection(plane,pts)), + plane_projection(planem,pts)); +} +*test_plane_projection(); + +module test_line_from_points() { + assert_approx(line_from_points([[1,0],[0,0],[-1,0]]),[[-1,0],[1,0]]); + assert_approx(line_from_points([[1,1],[0,1],[-1,1]]),[[-1,1],[1,1]]); + assert(line_from_points([[1,1],[0,1],[-1,0]])==undef); + assert(line_from_points([[1,1],[0,1],[-1,0]],fast=true)== [[-1,0],[1,1]]); +} +*test_line_from_points(); + module test_point_on_segment2d() { assert(point_on_segment2d([-15,0], [[-10,0], [10,0]]) == false); assert(point_on_segment2d([-10,0], [[-10,0], [10,0]]) == true); @@ -29,42 +167,28 @@ module test_point_on_segment2d() { assert(point_on_segment2d([ 10, 10], [[-10,-10], [10,10]]) == true); assert(point_on_segment2d([ 15, 15], [[-10,-10], [10,10]]) == false); } -test_point_on_segment2d(); +*test_point_on_segment2d(); -module test_point_left_of_segment() { - assert(point_left_of_segment2d([ -3, 0], [[-10,-10], [10,10]]) > 0); - assert(point_left_of_segment2d([ 0, 0], [[-10,-10], [10,10]]) == 0); - assert(point_left_of_segment2d([ 3, 0], [[-10,-10], [10,10]]) < 0); +module test_point_left_of_line2d() { + assert(point_left_of_line2d([ -3, 0], [[-10,-10], [10,10]]) > 0); + assert(point_left_of_line2d([ 0, 0], [[-10,-10], [10,10]]) == 0); + assert(point_left_of_line2d([ 3, 0], [[-10,-10], [10,10]]) < 0); } -test_point_left_of_segment(); - +*test_point_left_of_line2d(); module test_collinear() { assert(collinear([-10,-10], [-15, -16], [10,10]) == false); + assert(collinear([[-10,-10], [-15, -16], [10,10]]) == false); assert(collinear([-10,-10], [-15, -15], [10,10]) == true); + assert(collinear([[-10,-10], [-15, -15], [10,10]]) == true); assert(collinear([-10,-10], [ -3, 0], [10,10]) == false); assert(collinear([-10,-10], [ 0, 0], [10,10]) == true); assert(collinear([-10,-10], [ 3, 0], [10,10]) == false); assert(collinear([-10,-10], [ 15, 15], [10,10]) == true); assert(collinear([-10,-10], [ 15, 16], [10,10]) == false); } -test_collinear(); - - -module test_collinear_indexed() { - pts = [ - [-20,-20], [-10,-20], [0,-10], [10,0], [20,10], [20,20], [15,30] - ]; - assert(collinear_indexed(pts, 0,1,2) == false); - assert(collinear_indexed(pts, 1,2,3) == true); - assert(collinear_indexed(pts, 2,3,4) == true); - assert(collinear_indexed(pts, 3,4,5) == false); - assert(collinear_indexed(pts, 4,5,6) == false); - assert(collinear_indexed(pts, 4,3,2) == true); - assert(collinear_indexed(pts, 0,5,6) == false); -} -test_collinear_indexed(); +*test_collinear(); module test_distance_from_line() { @@ -73,7 +197,7 @@ module test_distance_from_line() { assert(abs(distance_from_line([[-10,-10,-10], [10,10,10]], [1,-1,0]) - sqrt(2)) < EPSILON); assert(abs(distance_from_line([[-10,-10,-10], [10,10,10]], [8,-8,0]) - 8*sqrt(2)) < EPSILON); } -test_distance_from_line(); +*test_distance_from_line(); module test_line_normal() { @@ -97,7 +221,7 @@ module test_line_normal() { assert(approx(n2, n1)); } } -test_line_normal(); +*test_line_normal(); module test_line_intersection() { @@ -110,7 +234,7 @@ module test_line_intersection() { assert(line_intersection([[-10,-10], [ 10, 10]], [[ 10,-10], [-10, 10]]) == [0,0]); assert(line_intersection([[ -8, 0], [ 12, 4]], [[ 12, 0], [ -8, 4]]) == [2,2]); } -test_line_intersection(); +*test_line_intersection(); module test_segment_intersection() { @@ -126,7 +250,7 @@ module test_segment_intersection() { assert(segment_intersection([[-10,-10], [ 10, 10]], [[ 10,-10], [-10, 10]]) == [0,0]); assert(segment_intersection([[ -8, 0], [ 12, 4]], [[ 12, 0], [ -8, 4]]) == [2,2]); } -test_segment_intersection(); +*test_segment_intersection(); module test_line_segment_intersection() { @@ -141,7 +265,7 @@ module test_line_segment_intersection() { assert(line_segment_intersection([[-10,-10], [ 10, 10]], [[ 10,-10], [ 1, -1]]) == undef); assert(line_segment_intersection([[-10,-10], [ 10, 10]], [[ 10,-10], [ -1, 1]]) == [0,0]); } -test_line_segment_intersection(); +*test_line_segment_intersection(); module test_line_closest_point() { @@ -151,7 +275,7 @@ module test_line_closest_point() { assert(approx(line_closest_point([[-10,-20], [10,20]], [1,2]+[2,-1]), [1,2])); assert(approx(line_closest_point([[-10,-20], [10,20]], [13,31]), [15,30])); } -test_line_closest_point(); +*test_line_closest_point(); module test_segment_closest_point() { @@ -162,10 +286,10 @@ module test_segment_closest_point() { assert(approx(segment_closest_point([[-10,-20], [10,20]], [13,31]), [10,20])); assert(approx(segment_closest_point([[-10,-20], [10,20]], [15,25]), [10,20])); } -test_segment_closest_point(); - +*test_segment_closest_point(); module test_find_circle_2tangents() { +//** missing tests with arg tangent=true assert(approx(find_circle_2tangents([10,10],[0,0],[10,-10],r=10/sqrt(2))[0],[10,0])); assert(approx(find_circle_2tangents([-10,10],[0,0],[-10,-10],r=10/sqrt(2))[0],[-10,0])); assert(approx(find_circle_2tangents([-10,10],[0,0],[10,10],r=10/sqrt(2))[0],[0,10])); @@ -174,9 +298,9 @@ module test_find_circle_2tangents() { assert(approx(find_circle_2tangents([10,0],[0,0],[0,-10],r=10)[0],[10,-10])); assert(approx(find_circle_2tangents([0,-10],[0,0],[-10,0],r=10)[0],[-10,-10])); assert(approx(find_circle_2tangents([-10,0],[0,0],[0,10],r=10)[0],[-10,10])); - assert(approx(find_circle_2tangents(polar_to_xy(10,60),[0,0],[10,0],r=10)[0],polar_to_xy(20,30))); + assert_approx(find_circle_2tangents(polar_to_xy(10,60),[0,0],[10,0],r=10)[0],polar_to_xy(20,30)); } -test_find_circle_2tangents(); +*test_find_circle_2tangents(); module test_find_circle_3points() { @@ -291,7 +415,7 @@ module test_find_circle_3points() { } } } -test_find_circle_3points(); +*test_find_circle_3points(); module test_circle_point_tangents() { @@ -304,7 +428,7 @@ module test_circle_point_tangents() { assert(approx(flatten(got), flatten(expected))); } } -test_circle_point_tangents(); +*test_circle_point_tangents(); module test_tri_calc() { @@ -327,23 +451,9 @@ module test_tri_calc() { assert(approx(tri_calc(hyp=hyp, ang2=ang2), expected)); } } -test_tri_calc(); +*test_tri_calc(); -// Dummy modules to show up in coverage check script. -module test_hyp_opp_to_adj(); -module test_hyp_ang_to_adj(); -module test_opp_ang_to_adj(); -module test_hyp_adj_to_opp(); -module test_hyp_ang_to_opp(); -module test_adj_ang_to_opp(); -module test_adj_opp_to_hyp(); -module test_adj_ang_to_hyp(); -module test_opp_ang_to_hyp(); -module test_hyp_adj_to_ang(); -module test_hyp_opp_to_ang(); -module test_adj_opp_to_ang(); - module test_tri_functions() { sides = rands(1,100,100,seed_value=8181); for (p = pair_wrap(sides)) { @@ -365,7 +475,7 @@ module test_tri_functions() { assert_approx(adj_opp_to_ang(adj,opp), ang); } } -test_tri_functions(); +*test_tri_functions(); module test_triangle_area() { @@ -373,7 +483,7 @@ module test_triangle_area() { assert(abs(triangle_area([0,0], [0,10], [0,15])) < EPSILON); assert(abs(triangle_area([0,0], [10,0], [0,10]) - 50) < EPSILON); } -test_triangle_area(); +*test_triangle_area(); module test_plane3pt() { @@ -384,8 +494,7 @@ module test_plane3pt() { assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); } -test_plane3pt(); - +*test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; @@ -395,11 +504,11 @@ module test_plane3pt_indexed() { assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); - assert(plane3pt_indexed(pts, 0,1,2) == [0,0,-1,0]); - assert(plane3pt_indexed(pts, 3,2,1) == [s13,s13,s13,10*s13]); - assert(plane3pt_indexed(pts, 1,2,3) == [-s13,-s13,-s13,-10*s13]); + assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); + assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); + assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } -test_plane3pt_indexed(); +*test_plane3pt_indexed(); module test_plane_from_points() { @@ -410,7 +519,7 @@ module test_plane_from_points() { assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); } -test_plane_from_points(); +*test_plane_from_points(); module test_plane_normal() { @@ -421,7 +530,7 @@ module test_plane_normal() { assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); } -test_plane_normal(); +*test_plane_normal(); module test_distance_from_plane() { @@ -429,20 +538,16 @@ module test_distance_from_plane() { assert(distance_from_plane(plane1, [0,0,5]) == 5); assert(distance_from_plane(plane1, [5,5,8]) == 8); } -test_distance_from_plane(); +*test_distance_from_plane(); module test_coplanar() { - plane = plane3pt([0,0,0], [0,10,10], [10,0,10]); - assert(coplanar(plane, [5,5,10]) == true); - assert(coplanar(plane, [10/3,10/3,20/3]) == true); - assert(coplanar(plane, [0,0,0]) == true); - assert(coplanar(plane, [1,1,0]) == false); - assert(coplanar(plane, [-1,1,0]) == true); - assert(coplanar(plane, [1,-1,0]) == true); - assert(coplanar(plane, [5,5,5]) == false); + assert(coplanar([ [5,5,1],[0,0,1],[-1,-1,1] ]) == false); + assert(coplanar([ [5,5,1],[0,0,0],[-1,-1,1] ]) == true); + assert(coplanar([ [0,0,0],[1,0,1],[1,1,1], [0,1,2] ]) == false); + assert(coplanar([ [0,0,0],[1,0,1],[1,1,2], [0,1,1] ]) == true); } -test_coplanar(); +*test_coplanar(); module test_in_front_of_plane() { @@ -455,7 +560,7 @@ module test_in_front_of_plane() { assert(in_front_of_plane(plane, [0,0,5]) == true); assert(in_front_of_plane(plane, [0,0,-5]) == false); } -test_in_front_of_plane(); +*test_in_front_of_plane(); module test_is_path() { @@ -470,35 +575,43 @@ module test_is_path() { assert(is_path([[1,2,3],[4,5,6]])); assert(is_path([[1,2,3],[4,5,6],[7,8,9]])); } -test_is_path(); +*test_is_path(); module test_is_closed_path() { assert(!is_closed_path([[1,2,3],[4,5,6],[1,8,9]])); assert(is_closed_path([[1,2,3],[4,5,6],[1,8,9],[1,2,3]])); } -test_is_closed_path(); +*test_is_closed_path(); module test_close_path() { assert(close_path([[1,2,3],[4,5,6],[1,8,9]]) == [[1,2,3],[4,5,6],[1,8,9],[1,2,3]]); assert(close_path([[1,2,3],[4,5,6],[1,8,9],[1,2,3]]) == [[1,2,3],[4,5,6],[1,8,9],[1,2,3]]); } -test_close_path(); +*test_close_path(); module test_cleanup_path() { assert(cleanup_path([[1,2,3],[4,5,6],[1,8,9]]) == [[1,2,3],[4,5,6],[1,8,9]]); assert(cleanup_path([[1,2,3],[4,5,6],[1,8,9],[1,2,3]]) == [[1,2,3],[4,5,6],[1,8,9]]); } -test_cleanup_path(); +*test_cleanup_path(); module test_polygon_area() { assert(approx(polygon_area([[1,1],[-1,1],[-1,-1],[1,-1]]), 4)); assert(approx(polygon_area(circle(r=50,$fn=1000)), -PI*50*50, eps=0.1)); } -test_polygon_area(); +*test_polygon_area(); + + +module test_is_convex_polygon() { + assert(is_convex_polygon([[1,1],[-1,1],[-1,-1],[1,-1]])); + assert(is_convex_polygon(circle(r=50,$fn=1000))); + assert(!is_convex_polygon([[1,1],[0,0],[-1,1],[-1,-1],[1,-1]])); +} +*test_is_convex_polygon(); module test_polygon_shift() { @@ -506,7 +619,7 @@ module test_polygon_shift() { assert(polygon_shift(path,1) == [[-1,1],[-1,-1],[1,-1],[1,1]]); assert(polygon_shift(path,2) == [[-1,-1],[1,-1],[1,1],[-1,1]]); } -test_polygon_shift(); +*test_polygon_shift(); module test_polygon_shift_to_closest_point() { @@ -516,56 +629,45 @@ module test_polygon_shift_to_closest_point() { assert(polygon_shift_to_closest_point(path,[-1.1,-1.1]) == [[-1,-1],[1,-1],[1,1],[-1,1]]); assert(polygon_shift_to_closest_point(path,[1.1,-1.1]) == [[1,-1],[1,1],[-1,1],[-1,-1]]); } -test_polygon_shift_to_closest_point(); +*test_polygon_shift_to_closest_point(); -/* -module test_first_noncollinear(){ - pts = [ - [1,1], [2,2], [3,3], [4,4], [4,5], [5,6] - ]; - assert(first_noncollinear(0,1,pts) == 4); - assert(first_noncollinear(1,0,pts) == 4); - assert(first_noncollinear(0,2,pts) == 4); - assert(first_noncollinear(2,0,pts) == 4); - assert(first_noncollinear(1,2,pts) == 4); - assert(first_noncollinear(2,1,pts) == 4); - assert(first_noncollinear(0,3,pts) == 4); - assert(first_noncollinear(3,0,pts) == 4); - assert(first_noncollinear(1,3,pts) == 4); - assert(first_noncollinear(3,1,pts) == 4); - assert(first_noncollinear(2,3,pts) == 4); - assert(first_noncollinear(3,2,pts) == 4); - assert(first_noncollinear(0,4,pts) == 1); - assert(first_noncollinear(4,0,pts) == 1); - assert(first_noncollinear(1,4,pts) == 0); - assert(first_noncollinear(4,1,pts) == 0); - assert(first_noncollinear(2,4,pts) == 0); - assert(first_noncollinear(4,2,pts) == 0); - assert(first_noncollinear(3,4,pts) == 0); - assert(first_noncollinear(4,3,pts) == 0); - assert(first_noncollinear(0,5,pts) == 1); - assert(first_noncollinear(5,0,pts) == 1); - assert(first_noncollinear(1,5,pts) == 0); - assert(first_noncollinear(5,1,pts) == 0); - assert(first_noncollinear(2,5,pts) == 0); - assert(first_noncollinear(5,2,pts) == 0); - assert(first_noncollinear(3,5,pts) == 0); - assert(first_noncollinear(5,3,pts) == 0); - assert(first_noncollinear(4,5,pts) == 0); - assert(first_noncollinear(5,4,pts) == 0); +module test_reindex_polygon() { + pent = subdivide_path([for(i=[0:4])[sin(72*i),cos(72*i)]],5); + circ = circle($fn=5,r=2.2); + assert_approx(reindex_polygon(circ,pent), [[0.951056516295,0.309016994375],[0.587785252292,-0.809016994375],[-0.587785252292,-0.809016994375],[-0.951056516295,0.309016994375],[0,1]]); + poly = [[-1,1],[-1,-1],[1,-1],[1,1],[0,0]]; + ref = [for(i=[0:4])[sin(72*i),cos(72*i)]]; + assert_approx(reindex_polygon(ref,poly),[[0,0],[1,1],[1,-1],[-1,-1],[-1,1]]); } -test_first_noncollinear(); -*/ +*test_reindex_polygon(); -module test_find_noncollinear_points() { - assert(find_noncollinear_points([[1,1],[2,2],[3,3],[4,4],[4,5],[5,6]]) == [0,5,3]); - assert(find_noncollinear_points([[1,1],[2,2],[8,3],[4,4],[4,5],[5,6]]) == [0,2,5]); +module test_align_polygon() { + pentagon = subdivide_path(pentagon(side=2),10); + hexagon = subdivide_path(hexagon(side=2.7),10); + aligned = [[2.7,0],[2.025,-1.16913429511],[1.35,-2.33826859022], + [-1.35,-2.33826859022],[-2.025,-1.16913429511],[-2.7,0], + [-2.025,1.16913429511],[-1.35,2.33826859022],[1.35,2.33826859022], + [2.025,1.16913429511]]; + assert_approx(align_polygon(pentagon,hexagon,[0:10:359]), aligned); + aligned2 = [[1.37638192047,0],[1.37638192047,-1],[0.425325404176,-1.30901699437], + [-0.525731112119,-1.61803398875],[-1.11351636441,-0.809016994375], + [-1.7013016167,0],[-1.11351636441,0.809016994375], + [-0.525731112119,1.61803398875],[0.425325404176,1.30901699437], + [1.37638192047,1]]; + assert_approx(align_polygon(hexagon,pentagon,[0:10:359]), aligned2); +} +*test_align_polygon(); + + +module test_noncollinear_triple() { + assert(noncollinear_triple([[1,1],[2,2],[3,3],[4,4],[4,5],[5,6]]) == [0,5,3]); + assert(noncollinear_triple([[1,1],[2,2],[8,3],[4,4],[4,5],[5,6]]) == [0,2,5]); u = unit([5,3]); - assert_equal(find_noncollinear_points([for(i = [2,3,4,5,7,12,15]) i * u], error=false),[]); + assert_equal(noncollinear_triple([for(i = [2,3,4,5,7,12,15]) i * u], error=false),[]); } -test_find_noncollinear_points(); +*test_noncollinear_triple(); module test_centroid() { @@ -573,15 +675,18 @@ module test_centroid() { assert_approx(centroid(circle(d=100)), [0,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); + poly = [for(a=[0:90:360]) + move([1,2.5,3.1], rot(p=[cos(a),sin(a),0],from=[0,0,1],to=[1,1,1])) ]; + assert_approx(centroid(poly), [1,2.5,3.1]); } -test_centroid(); +*test_centroid(); module test_simplify_path() { path = [[-20,-20], [-10,-20], [0,-10], [10,0], [20,10], [20,20], [15,30]]; assert(simplify_path(path) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]); } -test_simplify_path(); +*test_simplify_path(); module test_simplify_path_indexed() { @@ -589,7 +694,7 @@ module test_simplify_path_indexed() { path = [4,6,1,0,3,2,5]; assert(simplify_path_indexed(pts, path) == [4,6,3,2,5]); } -test_simplify_path_indexed(); +*test_simplify_path_indexed(); module test_point_in_polygon() { @@ -605,7 +710,7 @@ module test_point_in_polygon() { assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); } -test_point_in_polygon(); +*test_point_in_polygon(); module test_pointlist_bounds() { @@ -626,16 +731,16 @@ module test_pointlist_bounds() { ]; assert(pointlist_bounds(pts2d) == [[-63,-42],[84,42]]); pts5d = [ - [-53,27,12,-53,12], - [-63,97,36,-63,36], - [84,-32,-5,84,-5], - [63,-24,42,63,42], - [23,57,-42,23,-42] + [-53, 27, 12,-53, 12], + [-63, 97, 36,-63, 36], + [ 84,-32, -5, 84, -5], + [ 63,-24, 42, 63, 42], + [ 23, 57,-42, 23,-42] ]; assert(pointlist_bounds(pts5d) == [[-63,-32,-42,-63,-42],[84,97,42,84,42]]); assert(pointlist_bounds([[3,4,5,6]]), [[3,4,5,6],[3,4,5,6]]); } -test_pointlist_bounds(); +*test_pointlist_bounds(); module test_closest_point() { @@ -648,7 +753,7 @@ module test_closest_point() { assert(mindist == dists[pidx]); } } -test_closest_point(); +*test_closest_point(); module test_furthest_point() { @@ -661,7 +766,7 @@ module test_furthest_point() { assert(mindist == dists[pidx]); } } -test_furthest_point(); +*test_furthest_point(); module test_polygon_is_clockwise() { @@ -670,7 +775,7 @@ module test_polygon_is_clockwise() { assert(polygon_is_clockwise(circle(d=100))); assert(polygon_is_clockwise(square(100))); } -test_polygon_is_clockwise(); +*test_polygon_is_clockwise(); module test_clockwise_polygon() { @@ -679,7 +784,7 @@ module test_clockwise_polygon() { assert(clockwise_polygon(path) == path); assert(clockwise_polygon(rpath) == path); } -test_clockwise_polygon(); +*test_clockwise_polygon(); module test_ccw_polygon() { @@ -688,7 +793,7 @@ module test_ccw_polygon() { assert(ccw_polygon(path) == rpath); assert(ccw_polygon(rpath) == rpath); } -test_ccw_polygon(); +*test_ccw_polygon(); module test_reverse_polygon() { @@ -697,7 +802,7 @@ module test_reverse_polygon() { assert(reverse_polygon(path) == rpath); assert(reverse_polygon(rpath) == path); } -test_reverse_polygon(); +*test_reverse_polygon(); module test_is_region() { @@ -709,7 +814,7 @@ module test_is_region() { assert(!is_region(true)); assert(!is_region("foo")); } -test_is_region(); +*test_is_region(); From b4e26c035cdaa24cfe7c4067da07f72410bd427b Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 16 Aug 2020 23:34:31 +0100 Subject: [PATCH 32/57] Changes to noncollinear_triple --- hull.scad | 6 +++--- rounding.scad | 10 +++++----- vnf.scad | 12 +++++++++++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/hull.scad b/hull.scad index da7d2c9..874e2c8 100644 --- a/hull.scad +++ b/hull.scad @@ -92,7 +92,7 @@ function hull2d_path(points) = assert(is_path(points,2),"Invalid input to hull2d_path") len(points) < 2 ? [] : len(points) == 2 ? [0,1] - : let(tri=find_noncollinear_points(points, error=false)) + : 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 ], @@ -170,7 +170,7 @@ function hull3d_faces(points) = assert(is_path(points,3),"Invalid input to hull3d_faces") len(points) < 3 ? list_range(len(points)) : let ( // start with a single non-collinear triangle - tri = find_noncollinear_points(points, error=false) + tri = noncollinear_triple(points, error=false) ) tri==[] ? _hull_collinear(points) : let( @@ -250,7 +250,7 @@ function _find_conflicts(point, planes) = [ function _find_first_noncoplanar(plane, points, i) = - (i >= len(points) || !coplanar(plane, points[i]))? i : + (i >= len(points) || !points_on_plane([points[i]],plane))? i : _find_first_noncoplanar(plane, points, i+1); diff --git a/rounding.scad b/rounding.scad index 254d035..2dc2ea4 100644 --- a/rounding.scad +++ b/rounding.scad @@ -675,7 +675,7 @@ module offset_sweep( r = offset_type=="round"? this_offset : undef, do_chamfer = offset_type == "chamfer" ) - assert(num_defined([r,delta])==1,"Must set `offset` to \"round\" or \"delta") + assert(num_defined([r,delta])==1,str("Must set `offset` to ",round," or ",delta) let( vertices_faces = offset( path, r=r, delta=delta, chamfer = do_chamfer, closed=true, @@ -1532,7 +1532,7 @@ function rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_ // Determine which points are concave by making bottom 2d if necessary bot_proj = len(bottom[0])==2 ? bottom : project_plane(bottom, select(bottom,0,2)), bottom_sign = polygon_is_clockwise(bot_proj) ? 1 : -1, - concave = [for(i=[0:N-1]) bottom_sign*sign(point_left_of_segment2d(select(bot_proj,i+1), select(bot_proj, i-1,i)))>0], + concave = [for(i=[0:N-1]) bottom_sign*sign(point_left_of_line2d(select(bot_proj,i+1), select(bot_proj, i-1,i)))>0], top = is_undef(top) ? path3d(bottom,height/2) : len(top[0])==2 ? path3d(top,height/2) : top, @@ -1547,11 +1547,11 @@ function rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_ assert(jsvecok || jssingleok, str("Argument joint_sides is invalid. All entries must be nonnegative, and it must be a number, 2-vector, or a length ",N," list those.")) assert(is_num(k_sides) || is_vector(k_sides,N), str("Curvature parameter k_sides must be a number or length ",N," vector")) - assert(points_are_coplanar(bottom)) - assert(points_are_coplanar(top)) + assert(coplanar(bottom)) + assert(coplanar(top)) assert(!is_num(k_sides) || (k_sides>=0 && k_sides<=1), "Curvature parameter k_sides must be in interval [0,1]") let( - non_coplanar=[for(i=[0:N-1]) if (!points_are_coplanar(concat(select(top,i,i+1), select(bottom,i,i+1)))) [i,(i+1)%N]], + non_coplanar=[for(i=[0:N-1]) if (!coplanar(concat(select(top,i,i+1), select(bottom,i,i+1)))) [i,(i+1)%N]], k_sides_vec = is_num(k_sides) ? repeat(k_sides, N) : k_sides, kbad = [for(i=[0:N-1]) if (k_sides_vec[i]<0 || k_sides_vec[i]>1) i], joint_sides_vec = jssingleok ? repeat(joint_sides,N) : joint_sides, diff --git a/vnf.scad b/vnf.scad index b72902e..05fd82f 100644 --- a/vnf.scad +++ b/vnf.scad @@ -403,6 +403,16 @@ function _triangulate_planar_convex_polygons(polys) = outtris = concat(tris, newtris, newtris2) ) outtris; +//** +// this function may produce degenerate triangles: +// _triangulate_planar_convex_polygons([ [for(i=[0:1]) [i,i], +// [1,-1], [-1,-1], +// for(i=[-1:0]) [i,i] ] ] ) +// == [[[-1, -1], [ 0, 0], [0, 0]] +// [[-1, -1], [-1, -1], [0, 0]] +// [[ 1, -1], [-1, -1], [0, 0]] +// [[ 0, 0], [ 1, 1], [1, -1]] ] +// // Function: vnf_bend() // Usage: @@ -647,7 +657,7 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) = nonplanars = unique([ for (face = faces) let( faceverts = [for (k=face) varr[k]] - ) if (!points_are_coplanar(faceverts)) [ + ) if (!coplanar(faceverts)) [ "ERROR", "NONPLANAR", "Face vertices are not coplanar", From 143ba0646719baf49b7a9e4719738b8575d472fb Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 16 Aug 2020 23:38:17 +0100 Subject: [PATCH 33/57] Minor edits --- math.scad | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/math.scad b/math.scad index 158b1f2..888d048 100644 --- a/math.scad +++ b/math.scad @@ -675,7 +675,7 @@ function convolve(p,q) = // Usage: linear_solve(A,b) // Description: // Solves the linear system Ax=b. If A is square and non-singular the unique solution is returned. If A is overdetermined -// the least squares solution is returned. If A is underdetermined, the minimal norm solution is returned. +// the least squares solution is returned. If A is underdetermined, the minimal norm solution is returned. // If A is rank deficient or singular then linear_solve returns []. If b is a matrix that is compatible with A // then the problem is solved for the matrix valued right hand side and a matrix is returned. Note that if you // want to solve Ax=b1 and Ax=b2 that you need to form the matrix transpose([b1,b2]) for the right hand side and then @@ -686,7 +686,7 @@ function linear_solve(A,b) = m = len(A), n = len(A[0]) ) - assert(is_vector(b,m) || is_matrix(b,m),"Incompatible matrix and right hand side") + assert(is_vector(b,m) || is_matrix(b,m),"Invalid right hand side or incompatible with the matrix") let ( qr = m Date: Sun, 16 Aug 2020 23:39:11 +0100 Subject: [PATCH 34/57] Minor edits --- common.scad | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/common.scad b/common.scad index eebd141..4545886 100644 --- a/common.scad +++ b/common.scad @@ -289,12 +289,15 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1)? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r1 : - !is_undef(r2)? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r2 : - !is_undef(d1)? d1/2 : - !is_undef(d2)? d2/2 : - !is_undef(r)? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") r : - !is_undef(d)? d/2 : + !is_undef(r1)? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r1), "Invalid radius r1." ) r1 : + !is_undef(r2)? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r2), "Invalid radius r2." ) r2 : + !is_undef(d1)? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 : + !is_undef(d2)? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 : + !is_undef(r)? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + r ://assert(is_finite(r), "Invalid radius r." ) r : // this assert causes an error in shapes + !is_undef(d)? assert(is_finite(d), "Invalid diameter d." ) d/2 : dflt ); From 288f203bb634f0dc9ca00a445ba8522fcbaf6047 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 16 Aug 2020 23:42:59 +0100 Subject: [PATCH 35/57] Revert "Minor edits" This reverts commit bf44fe5b039eedf7f68655f4b5f6bde67ca3bc2a. --- common.scad | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/common.scad b/common.scad index 4545886..eebd141 100644 --- a/common.scad +++ b/common.scad @@ -289,15 +289,12 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1)? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r1), "Invalid radius r1." ) r1 : - !is_undef(r2)? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r2), "Invalid radius r2." ) r2 : - !is_undef(d1)? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 : - !is_undef(d2)? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 : - !is_undef(r)? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - r ://assert(is_finite(r), "Invalid radius r." ) r : // this assert causes an error in shapes - !is_undef(d)? assert(is_finite(d), "Invalid diameter d." ) d/2 : + !is_undef(r1)? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r1 : + !is_undef(r2)? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r2 : + !is_undef(d1)? d1/2 : + !is_undef(d2)? d2/2 : + !is_undef(r)? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") r : + !is_undef(d)? d/2 : dflt ); From 2efd0ca5d01d92d7f7de6e1bac36b82be17e7605 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 16 Aug 2020 23:54:00 +0100 Subject: [PATCH 36/57] Function name change plane_projection >> projection_on_plane --- geometry.scad | 10 +++++----- tests/test_geometry.scad | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/geometry.scad b/geometry.scad index b3f5ee6..eff67bc 100644 --- a/geometry.scad +++ b/geometry.scad @@ -825,7 +825,7 @@ function plane3pt(p1, p2, p3) = // i3 = The index into `points` of the third point on the plane. function plane3pt_indexed(points, i1, i2, i3) = assert( is_vector([i1,i2,i3]) && min(i1,i2,i3)>=0 && is_list(points) && max(i1,i2,i3) Date: Mon, 17 Aug 2020 00:18:33 +0100 Subject: [PATCH 37/57] refine arg validation of get_radius It is assumed that args r and d in get_radius can be a finite number or a vector with dimension 1 or 2. --- common.scad | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/common.scad b/common.scad index eebd141..51d8363 100644 --- a/common.scad +++ b/common.scad @@ -289,13 +289,28 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1)? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r1 : - !is_undef(r2)? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") r2 : - !is_undef(d1)? d1/2 : - !is_undef(d2)? d2/2 : - !is_undef(r)? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") r : - !is_undef(d)? d/2 : - dflt + !is_undef(r1) + ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r1), "Invalid radius r1." ) + r1 + : !is_undef(r2) + ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r2), "Invalid radius r2." ) + r2 + : !is_undef(d1) + ? assert(is_finite(d1), "Invalid diameter d1." ) + d1/2 + : !is_undef(d2) + ? assert(is_finite(d2), "Invalid diameter d2." ) + d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) + ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) + d/2 + : dflt ); // Function: get_height() From c465a5b746746b79ff2dad405ddb92221fd55ad8 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sun, 16 Aug 2020 23:09:59 -0700 Subject: [PATCH 38/57] cuboid() with negative chamfer had mispositioned anti-chamfers. --- shapes.scad | 4 ++-- version.scad | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shapes.scad b/shapes.scad index cb6d4e5..d612b0a 100644 --- a/shapes.scad +++ b/shapes.scad @@ -102,8 +102,8 @@ module cuboid( if (edges == EDGES_ALL && trimcorners) { if (chamfer<0) { cube(size, center=true) { - attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); - attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(TOP,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(BOT,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); } } else { isize = [for (v = size) max(0.001, v-2*chamfer)]; diff --git a/version.scad b/version.scad index e6ba60b..21ab73b 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,402]; +BOSL_VERSION = [2,0,403]; // Section: BOSL Library Version Functions From 25841bda7328edcaf0f82387131bfd84149cf5b2 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sun, 16 Aug 2020 23:10:43 -0700 Subject: [PATCH 39/57] Fixed attachable() anchors for vnf's with extent=false. --- attachments.scad | 35 +++++++++++++---------------------- version.scad | 2 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/attachments.scad b/attachments.scad index 6004e29..2ac4367 100644 --- a/attachments.scad +++ b/attachments.scad @@ -458,10 +458,9 @@ function find_anchor(anchor, geom) = eps = 1/2048, points = vnf[0], faces = vnf[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=points)), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), points), hits = [ - for (i = idx(faces)) let( - face = faces[i], + for (face = faces) let( verts = select(rpts, face) ) if ( max(subindex(verts,0)) >= -eps && @@ -470,35 +469,27 @@ function find_anchor(anchor, geom) = min(subindex(verts,1)) <= eps && min(subindex(verts,2)) <= eps ) let( - pt = polygon_line_intersection( - select(points, face), - [CENTER,anchor], eps=eps - ) - ) if (!is_undef(pt)) [norm(pt), i, pt] + poly = select(points, face), + pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) + ) if (!is_undef(pt)) let( + plane = plane_from_polygon(poly), + n = plane_normal(plane) + ) + [norm(pt-cp), n, pt] ] ) assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.") let( furthest = max_index(subindex(hits,0)), - pos = point3d(cp) + hits[furthest][2], dist = hits[furthest][0], - nfaces = [for (hit = hits) if(approx(hit[0],dist,eps=eps)) hit[1]], - n = unit( - sum([ - for (i = nfaces) let( - faceverts = select(points, faces[i]), - faceplane = plane_from_points(faceverts), - nrm = plane_normal(faceplane) - ) nrm - ]) / len(nfaces), - UP - ) + pos = hits[furthest][2], + n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]])) ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf let( vnf=geom[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=vnf[0])), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]), maxx = max(subindex(rpts,0)), idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i], mm = pointlist_bounds(select(rpts,idxs)), @@ -849,7 +840,7 @@ module attachable( // Module: position() // Usage: -// position(from, [overlap]) ... +// position(from) ... // Description: // Attaches children to a parent object at an anchor point. // Arguments: diff --git a/version.scad b/version.scad index 21ab73b..85cad34 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,403]; +BOSL_VERSION = [2,0,404]; // Section: BOSL Library Version Functions From 2ff93f34cdf49672f34d08d0983309f621507296 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 17 Aug 2020 18:43:58 -0400 Subject: [PATCH 40/57] Added attachment support to all modules, and bug fixed rounded_prism (problem introduced by error checking in det2), and fixed broken example in rounding. --- rounding.scad | 286 ++++++++++++++++++++++++++++---------------------- skin.scad | 63 ++++++++--- 2 files changed, 208 insertions(+), 141 deletions(-) diff --git a/rounding.scad b/rounding.scad index 254d035..b3bfa45 100644 --- a/rounding.scad +++ b/rounding.scad @@ -458,10 +458,10 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals -// Module: offset_sweep() +// Function&Module: offset_sweep() // // Description: -// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. +// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. When invoked as a function returns a VNF; when invoked as a module produces geometry. // You can specify a sequence of offsets values, or you can use several built-in offset profiles that are designed to provide end treatments such as roundovers. // The path is shifted by `offset()` multiple times in sequence // to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade @@ -543,8 +543,12 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // angle = default angle for chamfers. Default: 45 // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover -// convexity = convexity setting for use with polyhedron. Default: 10 -// +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Rounding a star shaped prism with postive radius values // star = star(5, r=22, ir=13); // rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24); @@ -650,118 +654,118 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // up(1) // offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4)); // } -module offset_sweep( - path, height, h, l, - top=[], bottom=[], - offset="round", r=0, steps=16, - quality=1, check_valid=true, - offset_maxstep=1, extra=0, - cut=undef, chamfer_width=undef, chamfer_height=undef, - joint=undef, k=0.75, angle=45, - convexity=10 -) { - // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce - // the inputs for the polyhedron module. - function make_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, vertexcount=0, vertices=[], faces=[] )= - offsetind==len(offsets)? ( - let( - bottom = list_range(n=len(path),s=vertexcount), - oriented_bottom = !flip_faces? bottom : reverse(bottom) - ) [vertices, concat(faces,[oriented_bottom])] - ) : ( - let( - this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], - delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, - r = offset_type=="round"? this_offset : undef, - do_chamfer = offset_type == "chamfer" + + +// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce +// the inputs for the polyhedron module. +function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, + vertexcount=0, vertices=[], faces=[] )= + offsetind==len(offsets)? ( + let( + bottom = list_range(n=len(path),s=vertexcount), + oriented_bottom = !flip_faces? bottom : reverse(bottom) + ) [vertices, concat(faces,[oriented_bottom])] + ) : ( + let( + this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], + delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, + r = offset_type=="round"? this_offset : undef, + do_chamfer = offset_type == "chamfer" + ) + let( + vertices_faces = offset( + path, r=r, delta=delta, chamfer = do_chamfer, closed=true, + check_valid=check_valid, quality=quality, + maxstep=maxstep, return_faces=true, + firstface_index=vertexcount, + flip_faces=flip_faces ) - assert(num_defined([r,delta])==1,"Must set `offset` to \"round\" or \"delta") - let( - vertices_faces = offset( - path, r=r, delta=delta, chamfer = do_chamfer, closed=true, - check_valid=check_valid, quality=quality, - maxstep=maxstep, return_faces=true, - firstface_index=vertexcount, - flip_faces=flip_faces - ) - ) - make_polyhedron( - vertices_faces[0], offsets, offset_type, - flip_faces, quality, check_valid, maxstep, - offsetind+1, vertexcount+len(path), - vertices=concat( - vertices, - zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) - ), - faces=concat(faces, vertices_faces[1]) - ) - ); - - - argspec = [ - ["r",r], - ["extra",extra], - ["type","circle"], - ["check_valid",check_valid], - ["quality",quality], - ["offset_maxstep", offset_maxstep], - ["steps",steps], - ["offset",offset], - ["chamfer_width",chamfer_width], - ["chamfer_height",chamfer_height], - ["angle",angle], - ["cut",cut], - ["joint",joint], - ["k", k], - ["points", []], - ]; - - path = check_and_fix_path(path, [2], closed=true); - clockwise = polygon_is_clockwise(path); - - top = struct_set(argspec, top, grow=false); - bottom = struct_set(argspec, bottom, grow=false); - - // This code does not work. It hits the error in make_polyhedron from offset being wrong - // before this code executes. Had to move the test into make_polyhedron, which is ugly since it's in the loop - //offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) && - // in_list(struct_val(bottom, "offset"),["round","delta"]); - //assert(offsetsok,"Offsets must be one of \"round\" or \"delta\""); - - - offsets_bot = _rounding_offsets(bottom, -1); - offsets_top = _rounding_offsets(top, 1); - - if (offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5)) { - echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested many layers. This can be slow or run out of recursion depth."); - } - // "Extra" height enlarges the result beyond the requested height, so subtract it - bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"); - top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"); - - height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height); - assert(height>=0, "Height must be nonnegative"); - - middle = height-bottom_height-top_height; - assert( - middle>=0, str( - "Specified end treatments (bottom height = ",bottom_height, - " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + _make_offset_polyhedron( + vertices_faces[0], offsets, offset_type, + flip_faces, quality, check_valid, maxstep, + offsetind+1, vertexcount+len(path), + vertices=concat( + vertices, + zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) + ), + faces=concat(faces, vertices_faces[1]) ) ); - initial_vertices_bot = path3d(path); - vertices_faces_bot = make_polyhedron( + +function offset_sweep( + path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45 + ) = + let( + argspec = [ + ["r",r], + ["extra",extra], + ["type","circle"], + ["check_valid",check_valid], + ["quality",quality], + ["offset_maxstep", offset_maxstep], + ["steps",steps], + ["offset",offset], + ["chamfer_width",chamfer_width], + ["chamfer_height",chamfer_height], + ["angle",angle], + ["cut",cut], + ["joint",joint], + ["k", k], + ["points", []], + ], + path = check_and_fix_path(path, [2], closed=true), + clockwise = polygon_is_clockwise(path), + + top = struct_set(argspec, top, grow=false), + bottom = struct_set(argspec, bottom, grow=false), + + // This code does not work. It hits the error in _make_offset_polyhedron from offset being wrong + // before this code executes. Had to move the test into _make_offset_polyhedron, which is ugly since it's in the loop + offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) + && in_list(struct_val(bottom, "offset"),["round","delta"]) + ) + assert(offsetsok,"Offsets must be one of \"round\" or \"delta\"") + let( + offsets_bot = _rounding_offsets(bottom, -1), + offsets_top = _rounding_offsets(top, 1), + dummy = offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5) + ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.") + : 0, + + // "Extra" height enlarges the result beyond the requested height, so subtract it + bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"), + top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"), + + height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height), + middle = height-bottom_height-top_height + ) + assert(height>=0, "Height must be nonnegative") + assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height, + " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + ) + let( + initial_vertices_bot = path3d(path), + + vertices_faces_bot = _make_offset_polyhedron( path, offsets_bot, struct_val(bottom,"offset"), clockwise, struct_val(bottom,"quality"), struct_val(bottom,"check_valid"), struct_val(bottom,"offset_maxstep"), vertices=initial_vertices_bot - ); + ), - top_start_ind = len(vertices_faces_bot[0]); - initial_vertices_top = zip(path, repeat(middle,len(path))); - vertices_faces_top = make_polyhedron( + top_start_ind = len(vertices_faces_bot[0]), + initial_vertices_top = zip(path, repeat(middle,len(path))), + vertices_faces_top = _make_offset_polyhedron( path, move(p=offsets_top,[0,middle]), struct_val(top,"offset"), !clockwise, struct_val(top,"quality"), @@ -769,20 +773,39 @@ module offset_sweep( struct_val(top,"offset_maxstep"), vertexcount=top_start_ind, vertices=initial_vertices_top - ); + ), middle_faces = middle==0 ? [] : [ for(i=[0:len(path)-1]) let( oneface=[i, (i+1)%len(path), top_start_ind+(i+1)%len(path), top_start_ind+i] ) !clockwise ? reverse(oneface) : oneface - ]; - up(bottom_height) { - polyhedron( - concat(vertices_faces_bot[0],vertices_faces_top[0]), - faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces), - convexity=convexity - ); - } -} + ] + ) + [up(bottom_height, concat(vertices_faces_bot[0],vertices_faces_top[0])), // Vertices + concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)]; // Faces + + +module offset_sweep(path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45, + convexity=10,anchor="origin",cp, + spin=0, orient=UP, extent=false) +{ + vnf = offset_sweep(path=path, height=height, h=h, l=l, top=top, bottom=bottom, offset=offset, r=0, steps=steps, + quality=quality, check_valid=true, offset_maxstep=1, extra=0, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle); + + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + + function os_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) = assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`") @@ -924,7 +947,6 @@ function os_profile(points, extra,check_valid, quality, offset_maxstep, offset) // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover // convexity = convexity setting for use with polyhedron. Default: 10 -// // Example: Chamfered elliptical prism. If you stretch a chamfered cylinder the chamfer will be uneven. // convex_offset_extrude(bottom = os_chamfer(height=-2), top=os_chamfer(height=1), height=7) // xscale(4)circle(r=6,$fn=64); @@ -1364,8 +1386,6 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, } } - - function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( N = len(top), @@ -1395,8 +1415,8 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( prev_corner = prev_offset + abs(rtop_in)*in_prev, next_corner = next_offset + abs(rtop_in)*in_next, - prev_degenerate = is_undef(ray_intersection([far_corner, far_corner+prev], [prev_offset, prev_offset+in_prev])), - next_degenerate = is_undef(ray_intersection([far_corner, far_corner+next], [next_offset, next_offset+in_next])) + prev_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+prev]), path2d([prev_offset, prev_offset+in_prev]))), + next_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+next]), path2d([next_offset, next_offset+in_next]))) ) [ prev_degenerate ? far_corner : prev_corner, far_corner, @@ -1452,6 +1472,11 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // splinesteps = number of segments to use for curved patches. Default: 16 // debug = turn on debug mode which displays illegal polyhedra and shows the bezier corner patches for troubleshooting purposes. Default: False // convexity = convexity parameter for polyhedron(), only for module version. Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Uniformly rounded pentagonal prism // rounded_prism(pentagon(3), height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5); // Example: Maximum possible rounding. @@ -1500,15 +1525,21 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)), joint_top=2, joint_bot=1, joint_sides=1); module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides, - k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false) + k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false, + anchor="origin",cp,spin=0, orient=UP, extent=false) { result = rounded_prism(bottom=bottom, top=top, joint_bot=joint_bot, joint_top=joint_top, joint_sides=joint_sides, k_bot=k_bot, k_top=k_top, k_sides=k_sides, k=k, splinesteps=splinesteps, h=h, length=length, height=height, l=l,debug=debug); - if (debug){ - vnf_polyhedron(result[1], convexity=convexity); - trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + vnf = debug ? result[1] : result; + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + if (debug){ + vnf_polyhedron(vnf, convexity=convexity); + trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + } + else vnf_polyhedron(vnf,convexity=convexity); + children(); } - else vnf_polyhedron(result,convexity=convexity); } @@ -1880,7 +1911,7 @@ function _circle_mask(r) = // $fn=128; // difference(){ // tube(or=r, wall=2, h=45); -// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7))),14*15,closed=true))); +// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7)),$fn=32),14*15,closed=true))); // } // } // Example(2D): Cutting a slot in a cylinder is tricky if you want rounded corners at the top. This slot profile has slightly angled top edges to blend into the top edge of the cylinder. @@ -1944,6 +1975,7 @@ function _circle_mask(r) = module bent_cutout_mask(r, thickness, path, convexity=10) { + no_children($children); assert(is_path(path,2),"Input path must be a 2d path") assert(r-thickness>0, "Thickness too large for radius"); assert(thickness>0, "Thickness must be positive"); diff --git a/skin.scad b/skin.scad index 30d8a17..87ae8d4 100644 --- a/skin.scad +++ b/skin.scad @@ -16,7 +16,8 @@ include // Function&Module: skin() // Usage: As module: -// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); +// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], +// [anchor],[cp],[spin],[orient],[extent]); // Usage: As function: // vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Description: @@ -117,6 +118,12 @@ include // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: // skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10); // Example: Rotating the pentagon place the zero index at different locations, giving a twist @@ -315,11 +322,15 @@ include // stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true); // } // } - - -module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10) +module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(skin(profiles, slices, refine, method, sampling, caps, closed, z), convexity=convexity); + vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } } @@ -803,6 +814,12 @@ function associate_vertices(polygons, split, curpoly=0) = // transformations = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: This is the "sweep-drop" example from list-comprehension-demos. // function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1; // function path(t) = [0, 0, 80 + 80 * cos(180 * t)]; @@ -839,9 +856,16 @@ function sweep(shape, transformations, closed=false, caps) = assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); -module sweep(shape, transformations, closed=false, caps, convexity=10) { - vnf_polyhedron(sweep(shape, transformations, closed, caps), convexity=convexity); -} +module sweep(shape, transformations, closed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) +{ + vnf = sweep(shape, transformations, closed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} // Function&Module: path_sweep() @@ -906,8 +930,13 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve) // relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent. Default: false // caps = Can be a boolean or vector of two booleans. Set to false to disable caps at the two ends. Default: true +// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. // convexity = convexity parameter for polyhedron(). Only accepted by the module version. Default: 10 -// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // // Example(2D): We'll use this shape in several examples // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; @@ -1121,13 +1150,19 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); - module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, - symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10) + symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(path_sweep(shape, path, method, normal, closed, twist, twist_by_length, - symmetry, last_normal, tangent, relaxed, caps), convexity=convexity); -} + vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, + symmetry, last_normal, tangent, relaxed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + function path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, transforms=false) = From 9f9c128b6c8a2e5714d21f64df2a6159bff1ded0 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 17 Aug 2020 20:44:42 -0400 Subject: [PATCH 41/57] Added rabbit clips. --- joiners.scad | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/joiners.scad b/joiners.scad index 1ab34e6..1178d4e 100644 --- a/joiners.scad +++ b/joiners.scad @@ -564,6 +564,7 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac } +// Section: Tension Clips // h is total height above 0 of the nub // nub extends below xy plane by distance nub/2 @@ -780,5 +781,244 @@ module snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fi +// Module: rabbit_clip() +// Usage: +// rabbit_clip(type, length, width, snap, thickness, depth, [compression], [clearance], [lock], +// [lock_clearance], [splineteps], [anchor], [orient], [spin]) +// Description: +// Creates a clip with two flexible ears to lock into a mating socket, or create a mask to produce the appropriate +// mating socket. The clip can be made to insert and release easily, or to hold much better, or it can be +// created with locking flanges that will make it very hard or impossible to remove. Unlike the snap pin, this clip +// is rectangular and can be made at any height, so a suitable clip could be very thin. It's also possible to get a +// solid connection with a short pin. +// . +// The type parameters specifies whether to make a clip, a socket mask, or a double clip. The length is the +// total nominal length of the clip. (The actual length will be very close, but not equal to this.) The width +// gives the nominal width of the clip, which is the actual width of the clip at its base. The snap parameter +// gives the depth of the clip sides, which controls how easy the clip is to insert and remove. The clip "ears" are +// made over-wide by the compression value. A nonzero compression helps make the clip secure in its socket. +// The socket's width and length are increased by the clearance value which creates some space and can compensate +// for printing inaccuracy. The socket will be slightly longer than the nominal width. The thickness is the thickness +// curved line that forms the clip. The clip depth is the amount the basic clip shape is extruded. Be sure that you +// make the socket with a larger depth than the clip (try 0.4 mm) to allow ease of insertion of the clip. The clearance +// value does not apply to the depth. The splinesteps parameter increases the sampling of the clip curves. +// . +// By default clips appear with orient=UP and sockets with orient=DOWN. +// . +// The first figure shows the dimensions of the rabbit clip. The second figure shows the clip in red overlayed on +// its socket in yellow. The left clip has a nonzero clearance, so its socket is bigger than the clip all around. +// The right hand locking clip has no clearance, but it has a lock clearance, which provides some space behind +// the lock to allow the clip to fit. (Note that depending on your printer, this can be set to zero.) +// +// Figure(2DMed): +// snap=1.5; +// comp=0.75; +// mid = 8.053; // computed in rabbit_clip +// tip = [-4.58,18.03]; +// translate([9,3]){ +// back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK); +// color("blue"){ +// stroke([[6,0],[6,18]],width=0.1); +// stroke([[6+comp, 12], [6+comp, 18]], width=.1); +// } +// color("red"){ +// stroke([[6-snap,mid], [6,mid]], endcaps="arrow2",width=0.15); +// translate([6+.4,mid-.15])text("snap",size=1,valign="center"); +// translate([6+comp/2,19.5])text("compression", size=1, halign="center"); +// stroke([[6+comp/2,19.3], [6+comp/2,17.7]], endcap2="arrow2", width=.15); +// fwd(1.1)text("width",size=1,halign="center"); +// xflip_copy()stroke([[2,-.7], [6,-.7]], endcap2="arrow2", width=.15); +// move([-6.7,mid])rot(90)text("length", size=1, halign="center"); +// stroke([[-7,10.3], [-7,18]], width=.15, endcap2="arrow2"); +// stroke([[-7,0], [-7,5.8]], width=.15,endcap1="arrow2"); +// stroke([tip, tip-[0,1]], width=.15); +// move([tip.x+2,19.5])text("thickness", halign="center",size=1); +// stroke([[tip.x+2, 19.3], tip+[.1,.1]], width=.15, endcap2="arrow2"); +// } +// } +// +// Figure(2DMed): +// snap=1.5; +// comp=0; +// translate([29,3]){ +// back_half() +// rabbit_clip("socket", width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK,lock=true); +// color("red")back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, +// orient=BACK,lock=true,lock_clearance=1); +// } +// translate([9,3]){ +// back_half() +// rabbit_clip("socket", clearance=.5,width=12, length=18, depth=1, thickness = 1, +// compression=comp, snap=snap, orient=BACK,lock=false); +// color("red")back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, +// orient=BACK,lock=false,lock_clearance=1); +// } +// Arguments: +// type = One of "pin", "socket", "male", "female" or "double" to specify what to make. +// length = nominal clip length +// width = nominal clip width +// snap = depth of hollow on the side of the clip +// thickness = thickness of the clip "line" +// depth = amount to extrude clip (give extra room for the socket, about 0.4mm) +// compression = excess width at the "ears" to lock more tightly. Default: 0.1 +// clearance = extra space in the socket for easier insertion. Default: 0.1 +// lock = set to true to make a locking clip that may be irreversible. Default: false +// lock_clearance = give clearance for the lock. Default: 0 +// splinesteps = number of samples in the curves of the clip. Default: 8 +// anchor = anchor point for clip +// orient = clip orientation. Default: UP for pins, DOWN for sockets +// spin = spin the clip. Default: 0 +// +// Example: Here are several sizes that work printed in PLA on a Prusa MK3, with default clearance of 0.1 and a depth of 5 +// module test_pair(length, width, snap, thickness, compression, lock=false) +// { +// depth = 5; +// extra_depth = 10;// Change this to 0.4 for closed sockets +// cuboid([max(width+5,12),12, depth], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM) +// attach(BACK) +// rabbit_clip(type="pin",length=length, width=width,snap=snap,thickness=thickness,depth=depth, +// compression=compression,lock=lock); +// right(width+13) +// diff("remove") +// cuboid([width+8,max(12,length+2),depth+3], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM) +// attach(BACK) +// rabbit_clip(type="socket",length=length, width=width,snap=snap,thickness=thickness,depth=depth+extra_depth, +// lock=lock,compression=0,$tags="remove"); +// } +// left(37)ydistribute(spacing=28){ +// test_pair(length=6, width=7, snap=0.25, thickness=0.8, compression=0.1); +// test_pair(length=3.5, width=7, snap=0.1, thickness=0.8, compression=0.1); // snap = 0.2 gives a firmer connection +// test_pair(length=3.5, width=5, snap=0.1, thickness=0.8, compression=0.1); // hard to take apart +// } +// right(17)ydistribute(spacing=28){ +// test_pair(length=12, width=10, snap=1, thickness=1.2, compression=0.2); +// test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible +// test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible +// } +// Example: Double clip to connect two sockets +// rabbit_clip("double",length=8, width=7, snap=0.75, thickness=0.8, compression=0.2,depth=5); +// Example: A modified version of the clip that acts like a backpack strap clip, where it locks tightly but you can squeeze to release. +// cuboid([25,15,5],anchor=BOTTOM) +// attach(BACK)rabbit_clip("pin", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5, lock_clearance=3); +// left(32) +// diff("remove") +// cuboid([30,30,11],orient=BACK,anchor=BACK){ +// attach(BACK)rabbit_clip("socket", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5.5, lock_clearance=3,$tags="remove"); +// xflip_copy() +// position(FRONT+LEFT) +// xscale(0.8) +// zcyl(l=20,r=13.5, $tags="remove",$fn=64); +// } +module rabbit_clip(type, length, width, snap, thickness, depth, compression=0.1, clearance=.1, lock=false, lock_clearance=0, + splinesteps=8, anchor, orient, spin=0) +{ + assert(is_num(width) && width>0,"Width must be a positive value"); + assert(is_num(length) && length>0, "Length must be a positive value"); + assert(is_num(thickness) && thickness>0, "Thickness must be a positive value"); + assert(is_num(snap) && snap>=0, "Snap must be a non-negative value"); + assert(is_num(depth) && depth>0, "Depth must be a positive value"); + assert(is_num(compression) && compression >= 0, "Compression must be a nonnegative value"); + assert(is_bool(lock)); + assert(is_num(lock_clearance)); + legal_types = ["pin","socket","male","female","double"]; + assert(in_list(type,legal_types),str("type must be one of ",legal_types)); + + if (type=="double") { + attachable(size=[width+2*compression, depth, 2*length], anchor=default(anchor,BACK), spin=spin, orient=default(orient,BACK)){ + union(){ + rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression, + lock=lock, anchor=BOTTOM, orient=UP); + rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression, + lock=lock, anchor=BOTTOM, orient=DOWN); + cuboid([width-thickness, depth, thickness]); + } + children(); + } + } else { + anchor = default(anchor,BOTTOM); + is_pin = in_list(type,["pin","male"]); + default_overlap = 0.01 * (is_pin?1:-1); // Shift by this much to undo default overlap + extra = 0.02; // Amount of extension below nominal based position for the socket, must exceed default overlap of 0.01 + clearance = is_pin ? 0 : clearance; + compression = is_pin ? compression : 0; + orient = is_def(orient) ? orient + : is_pin ? UP + : DOWN; + earwidth = 2*thickness+snap; + point_length = earwidth/2.15; + // The adjustment is using cos(theta)*earwidth/2 and sin(theta)*point_length, but the computation + // is obscured because theta is atan(length/2/snap) + scaled_len = length - 0.5 * (earwidth * snap + point_length * length) / sqrt(sqr(snap)+sqr(length/2)); + bottom_pt = [0,max(scaled_len*0.15+thickness, 2*thickness)]; + ctr = [width/2,scaled_len] + line_normal([width/2-snap, scaled_len/2], [width/2, scaled_len]) * earwidth/2; + inside_pt = circle_circle_tangents(bottom_pt, 0, ctr, earwidth/2)[0][1]; + sidepath =[ + [width/2,0], + [width/2-snap,scaled_len/2], + [width/2+(is_pin?compression:0), scaled_len], + ctr - point_length * line_normal([width/2,scaled_len], inside_pt), + inside_pt + ]; + fullpath = concat( + sidepath, + [bottom_pt], + reverse(apply(xflip(),sidepath)) + ); + assert(fullpath[4].y < fullpath[3].y, "Pin is too wide for its length"); + + snapmargin = -snap + select(sidepath,-1).x;// - compression; + if (is_pin){ + if (snapmargin<0) echo("WARNING: The snap is too large for the clip to squeeze to fit its socket") + echo(snapmargin=snapmargin); + } + // Force tangent to be vertical at the outer edge of the clip to avoid overshoot + fulltangent = list_set(path_tangents(fullpath, uniform=false),[2,8], [[0,1],[0,-1]]); + + subset = is_pin ? [0:10] : [0,1,2,3, 7,8,9,10]; // Remove internal points from the socket + tangent = select(fulltangent, subset); + path = select(fullpath, subset); + + socket_smooth = .04; + pin_smooth = [.075, .075, .15, .12, .06]; + smoothing = is_pin + ? concat(pin_smooth, reverse(pin_smooth)) + : let(side_smooth=select(pin_smooth, 0, 2)) + concat(side_smooth, [socket_smooth], reverse(side_smooth)); + bez = path_to_bezier(path,relsize=smoothing,tangents=tangent); + rounded = bezier_polyline(bez,splinesteps=splinesteps); + bounds = pointlist_bounds(rounded); + kk = search([bounds[1].y], subindex(rounded,1)); + echo(rounded[kk[0]]); + extrapt = is_pin ? [] : [rounded[0] - [0,extra]]; + finalpath = is_pin ? rounded + : let(withclearance=offset(rounded, r=-clearance)) + concat( [[withclearance[0].x,-extra]], + withclearance, + [[-withclearance[0].x,-extra]]); + attachable(size=[bounds[1].x-bounds[0].x, depth, bounds[1].y-bounds[0].y], anchor=anchor, spin=spin, orient=orient){ + xrot(90) + translate([0,-(bounds[1].y-bounds[0].y)/2+default_overlap,-depth/2]) + linear_extrude(height=depth, convexity=10) { + if (lock) + xflip_copy() + right(clearance) + polygon([sidepath[1]+[-thickness/10,lock_clearance], + sidepath[2], + [sidepath[2].x,sidepath[1].y+lock_clearance]]); + if (is_pin) + offset_stroke(finalpath, width=[thickness,0]); + else + polygon(finalpath); + } + children(); + } + } +} + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap From 7daa16f2382d62e7acd01363a35feb95c6d158c3 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Mon, 17 Aug 2020 18:11:49 -0700 Subject: [PATCH 42/57] Fixed VNF attachment normal vector. --- attachments.scad | 17 +++++++++++++++-- version.scad | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/attachments.scad b/attachments.scad index 2ac4367..c2c969e 100644 --- a/attachments.scad +++ b/attachments.scad @@ -473,7 +473,7 @@ function find_anchor(anchor, geom) = pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) ) if (!is_undef(pt)) let( plane = plane_from_polygon(poly), - n = plane_normal(plane) + n = unit(plane_normal(plane)) ) [norm(pt-cp), n, pt] ] @@ -483,7 +483,20 @@ function find_anchor(anchor, geom) = furthest = max_index(subindex(hits,0)), dist = hits[furthest][0], pos = hits[furthest][2], - n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]])) + hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]], + unorms = len(hitnorms) > 7 + ? unique([for (nn = hitnorms) quant(nn,1e-9)]) + : [ + for (i = idx(hitnorms)) let( + nn = hitnorms[i], + isdup = [ + for (j = [i+1:1:len(hitnorms)-1]) + if (approx(nn, hitnorms[j])) 1 + ] != [] + ) if (!isdup) nn + ], + n = unit(sum(unorms)), + oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90 ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf diff --git a/version.scad b/version.scad index 85cad34..1420f58 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,404]; +BOSL_VERSION = [2,0,405]; // Section: BOSL Library Version Functions From f4a8138b37be3ddafdc14434b7724b344158c3c8 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 18 Aug 2020 11:01:42 +0100 Subject: [PATCH 43/57] Revert "Merge remote-tracking branch 'upstream/revarbat_dev'" This reverts commit ee80b1d08f2e26b2350e2cf9512e4729ad680a73, reversing changes made to 2b12659d009019c340e183f052d148e679797ecc. --- attachments.scad | 48 ++++++++++++++++------------------- rounding.scad | 2 +- shapes.scad | 4 +-- skin.scad | 65 +++++++++++------------------------------------- version.scad | 2 +- 5 files changed, 41 insertions(+), 80 deletions(-) diff --git a/attachments.scad b/attachments.scad index c2c969e..6004e29 100644 --- a/attachments.scad +++ b/attachments.scad @@ -458,9 +458,10 @@ function find_anchor(anchor, geom) = eps = 1/2048, points = vnf[0], faces = vnf[1], - rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), points), + rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=points)), hits = [ - for (face = faces) let( + for (i = idx(faces)) let( + face = faces[i], verts = select(rpts, face) ) if ( max(subindex(verts,0)) >= -eps && @@ -469,40 +470,35 @@ function find_anchor(anchor, geom) = min(subindex(verts,1)) <= eps && min(subindex(verts,2)) <= eps ) let( - poly = select(points, face), - pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) - ) if (!is_undef(pt)) let( - plane = plane_from_polygon(poly), - n = unit(plane_normal(plane)) - ) - [norm(pt-cp), n, pt] + pt = polygon_line_intersection( + select(points, face), + [CENTER,anchor], eps=eps + ) + ) if (!is_undef(pt)) [norm(pt), i, pt] ] ) assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.") let( furthest = max_index(subindex(hits,0)), + pos = point3d(cp) + hits[furthest][2], dist = hits[furthest][0], - pos = hits[furthest][2], - hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]], - unorms = len(hitnorms) > 7 - ? unique([for (nn = hitnorms) quant(nn,1e-9)]) - : [ - for (i = idx(hitnorms)) let( - nn = hitnorms[i], - isdup = [ - for (j = [i+1:1:len(hitnorms)-1]) - if (approx(nn, hitnorms[j])) 1 - ] != [] - ) if (!isdup) nn - ], - n = unit(sum(unorms)), - oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90 + nfaces = [for (hit = hits) if(approx(hit[0],dist,eps=eps)) hit[1]], + n = unit( + sum([ + for (i = nfaces) let( + faceverts = select(points, faces[i]), + faceplane = plane_from_points(faceverts), + nrm = plane_normal(faceplane) + ) nrm + ]) / len(nfaces), + UP + ) ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf let( vnf=geom[1], - rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]), + rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=vnf[0])), maxx = max(subindex(rpts,0)), idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i], mm = pointlist_bounds(select(rpts,idxs)), @@ -853,7 +849,7 @@ module attachable( // Module: position() // Usage: -// position(from) ... +// position(from, [overlap]) ... // Description: // Attaches children to a parent object at an anchor point. // Arguments: diff --git a/rounding.scad b/rounding.scad index 94b8143..2dc2ea4 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1962,4 +1962,4 @@ module bent_cutout_mask(r, thickness, path, convexity=10) } -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap \ No newline at end of file +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/shapes.scad b/shapes.scad index d612b0a..cb6d4e5 100644 --- a/shapes.scad +++ b/shapes.scad @@ -102,8 +102,8 @@ module cuboid( if (edges == EDGES_ALL && trimcorners) { if (chamfer<0) { cube(size, center=true) { - attach(TOP,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); - attach(BOT,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); } } else { isize = [for (v = size) max(0.001, v-2*chamfer)]; diff --git a/skin.scad b/skin.scad index 87ae8d4..30d8a17 100644 --- a/skin.scad +++ b/skin.scad @@ -16,8 +16,7 @@ include // Function&Module: skin() // Usage: As module: -// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], -// [anchor],[cp],[spin],[orient],[extent]); +// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Usage: As function: // vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Description: @@ -118,12 +117,6 @@ include // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d -// convexity = convexity setting for use with polyhedron. (module only) Default: 10 -// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" -// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 -// orient = Vector to rotate top towards after spin (module only) -// extent = use extent method for computing anchors. (module only) Default: false -// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: // skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10); // Example: Rotating the pentagon place the zero index at different locations, giving a twist @@ -322,15 +315,11 @@ include // stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true); // } // } -module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10, - anchor="origin",cp,spin=0, orient=UP, extent=false) + + +module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10) { - vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z); - attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) - { - vnf_polyhedron(vnf,convexity=convexity); - children(); - } + vnf_polyhedron(skin(profiles, slices, refine, method, sampling, caps, closed, z), convexity=convexity); } @@ -814,12 +803,6 @@ function associate_vertices(polygons, split, curpoly=0) = // transformations = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. -// convexity = convexity setting for use with polyhedron. (module only) Default: 10 -// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" -// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 -// orient = Vector to rotate top towards after spin (module only) -// extent = use extent method for computing anchors. (module only) Default: false -// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: This is the "sweep-drop" example from list-comprehension-demos. // function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1; // function path(t) = [0, 0, 80 + 80 * cos(180 * t)]; @@ -856,16 +839,9 @@ function sweep(shape, transformations, closed=false, caps) = assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); -module sweep(shape, transformations, closed=false, caps, convexity=10, - anchor="origin",cp,spin=0, orient=UP, extent=false) -{ - vnf = sweep(shape, transformations, closed, caps); - attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) - { - vnf_polyhedron(vnf,convexity=convexity); - children(); - } -} +module sweep(shape, transformations, closed=false, caps, convexity=10) { + vnf_polyhedron(sweep(shape, transformations, closed, caps), convexity=convexity); +} // Function&Module: path_sweep() @@ -930,13 +906,8 @@ module sweep(shape, transformations, closed=false, caps, convexity=10, // tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve) // relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent. Default: false // caps = Can be a boolean or vector of two booleans. Set to false to disable caps at the two ends. Default: true -// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. // convexity = convexity parameter for polyhedron(). Only accepted by the module version. Default: 10 -// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" -// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 -// orient = Vector to rotate top towards after spin (module only) -// extent = use extent method for computing anchors. (module only) Default: false -// cp = set centerpoint for anchor computation. (module only) Default: object centroid +// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. // // Example(2D): We'll use this shape in several examples // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; @@ -1150,19 +1121,13 @@ module sweep(shape, transformations, closed=false, caps, convexity=10, // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); -module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, - symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, - anchor="origin",cp,spin=0, orient=UP, extent=false) -{ - vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, - symmetry, last_normal, tangent, relaxed, caps); - attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) - { - vnf_polyhedron(vnf,convexity=convexity); - children(); - } -} +module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, + symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10) +{ + vnf_polyhedron(path_sweep(shape, path, method, normal, closed, twist, twist_by_length, + symmetry, last_normal, tangent, relaxed, caps), convexity=convexity); +} function path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, transforms=false) = diff --git a/version.scad b/version.scad index 1420f58..e6ba60b 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,405]; +BOSL_VERSION = [2,0,402]; // Section: BOSL Library Version Functions From 5fab080f8e8d6ddee90a6c584a21cfffaff307f5 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 18 Aug 2020 11:04:56 +0100 Subject: [PATCH 44/57] Revert "Revert "Merge remote-tracking branch 'upstream/revarbat_dev'"" This reverts commit f4a8138b37be3ddafdc14434b7724b344158c3c8. --- attachments.scad | 48 +++++++++++++++++++----------------- rounding.scad | 2 +- shapes.scad | 4 +-- skin.scad | 63 +++++++++++++++++++++++++++++++++++++----------- version.scad | 2 +- 5 files changed, 79 insertions(+), 40 deletions(-) diff --git a/attachments.scad b/attachments.scad index 6004e29..c2c969e 100644 --- a/attachments.scad +++ b/attachments.scad @@ -458,10 +458,9 @@ function find_anchor(anchor, geom) = eps = 1/2048, points = vnf[0], faces = vnf[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=points)), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), points), hits = [ - for (i = idx(faces)) let( - face = faces[i], + for (face = faces) let( verts = select(rpts, face) ) if ( max(subindex(verts,0)) >= -eps && @@ -470,35 +469,40 @@ function find_anchor(anchor, geom) = min(subindex(verts,1)) <= eps && min(subindex(verts,2)) <= eps ) let( - pt = polygon_line_intersection( - select(points, face), - [CENTER,anchor], eps=eps - ) - ) if (!is_undef(pt)) [norm(pt), i, pt] + poly = select(points, face), + pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) + ) if (!is_undef(pt)) let( + plane = plane_from_polygon(poly), + n = unit(plane_normal(plane)) + ) + [norm(pt-cp), n, pt] ] ) assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.") let( furthest = max_index(subindex(hits,0)), - pos = point3d(cp) + hits[furthest][2], dist = hits[furthest][0], - nfaces = [for (hit = hits) if(approx(hit[0],dist,eps=eps)) hit[1]], - n = unit( - sum([ - for (i = nfaces) let( - faceverts = select(points, faces[i]), - faceplane = plane_from_points(faceverts), - nrm = plane_normal(faceplane) - ) nrm - ]) / len(nfaces), - UP - ) + pos = hits[furthest][2], + hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]], + unorms = len(hitnorms) > 7 + ? unique([for (nn = hitnorms) quant(nn,1e-9)]) + : [ + for (i = idx(hitnorms)) let( + nn = hitnorms[i], + isdup = [ + for (j = [i+1:1:len(hitnorms)-1]) + if (approx(nn, hitnorms[j])) 1 + ] != [] + ) if (!isdup) nn + ], + n = unit(sum(unorms)), + oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90 ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf let( vnf=geom[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=vnf[0])), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]), maxx = max(subindex(rpts,0)), idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i], mm = pointlist_bounds(select(rpts,idxs)), @@ -849,7 +853,7 @@ module attachable( // Module: position() // Usage: -// position(from, [overlap]) ... +// position(from) ... // Description: // Attaches children to a parent object at an anchor point. // Arguments: diff --git a/rounding.scad b/rounding.scad index 2dc2ea4..94b8143 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1962,4 +1962,4 @@ module bent_cutout_mask(r, thickness, path, convexity=10) } -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap \ No newline at end of file diff --git a/shapes.scad b/shapes.scad index cb6d4e5..d612b0a 100644 --- a/shapes.scad +++ b/shapes.scad @@ -102,8 +102,8 @@ module cuboid( if (edges == EDGES_ALL && trimcorners) { if (chamfer<0) { cube(size, center=true) { - attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); - attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(TOP,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(BOT,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); } } else { isize = [for (v = size) max(0.001, v-2*chamfer)]; diff --git a/skin.scad b/skin.scad index 30d8a17..87ae8d4 100644 --- a/skin.scad +++ b/skin.scad @@ -16,7 +16,8 @@ include // Function&Module: skin() // Usage: As module: -// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); +// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], +// [anchor],[cp],[spin],[orient],[extent]); // Usage: As function: // vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Description: @@ -117,6 +118,12 @@ include // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: // skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10); // Example: Rotating the pentagon place the zero index at different locations, giving a twist @@ -315,11 +322,15 @@ include // stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true); // } // } - - -module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10) +module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(skin(profiles, slices, refine, method, sampling, caps, closed, z), convexity=convexity); + vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } } @@ -803,6 +814,12 @@ function associate_vertices(polygons, split, curpoly=0) = // transformations = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: This is the "sweep-drop" example from list-comprehension-demos. // function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1; // function path(t) = [0, 0, 80 + 80 * cos(180 * t)]; @@ -839,9 +856,16 @@ function sweep(shape, transformations, closed=false, caps) = assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); -module sweep(shape, transformations, closed=false, caps, convexity=10) { - vnf_polyhedron(sweep(shape, transformations, closed, caps), convexity=convexity); -} +module sweep(shape, transformations, closed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) +{ + vnf = sweep(shape, transformations, closed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} // Function&Module: path_sweep() @@ -906,8 +930,13 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve) // relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent. Default: false // caps = Can be a boolean or vector of two booleans. Set to false to disable caps at the two ends. Default: true +// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. // convexity = convexity parameter for polyhedron(). Only accepted by the module version. Default: 10 -// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // // Example(2D): We'll use this shape in several examples // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; @@ -1121,13 +1150,19 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); - module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, - symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10) + symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(path_sweep(shape, path, method, normal, closed, twist, twist_by_length, - symmetry, last_normal, tangent, relaxed, caps), convexity=convexity); -} + vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, + symmetry, last_normal, tangent, relaxed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + function path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, transforms=false) = diff --git a/version.scad b/version.scad index e6ba60b..1420f58 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,402]; +BOSL_VERSION = [2,0,405]; // Section: BOSL Library Version Functions From 29672105eda4a73218d91ba7c7cfc49776dd1b31 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 18 Aug 2020 23:27:38 +0100 Subject: [PATCH 45/57] Update geometry.scad Removed all double definitions --- geometry.scad | 161 +------------------------------------------------- 1 file changed, 1 insertion(+), 160 deletions(-) diff --git a/geometry.scad b/geometry.scad index eff67bc..7d02252 100644 --- a/geometry.scad +++ b/geometry.scad @@ -20,16 +20,6 @@ // point = The point to test. // edge = Array of two points forming the line segment to test against. // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_on_segment2d(point, edge, eps=EPSILON) = - assert( is_vector(point,2), "Invalid point." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) - approx(point,edge[0],eps=eps) - || approx(point,edge[1],eps=eps) // The point is an endpoint - || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the - || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints - && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge - function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) @@ -98,10 +88,6 @@ function collinear(a, b, c, eps=EPSILON) = : noncollinear_triple(points,error=false,eps=eps)==[]; -//*** valid for any dimension - - - // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -330,17 +316,6 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -function line_closest_point(line,pt) = - assert(is_path(line)&&len(line)==2) - assert(same_shape(pt,line[0])) - assert(!approx(line[0],line[1])) - let( - seglen = norm(line[1]-line[0]), - segvec = (line[1]-line[0])/seglen, - projection = (pt-line[0]) * segvec - ) - line[0] + projection*segvec; - function line_closest_point(line,pt) = assert(_valid_line(line), "Invalid line." ) assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) @@ -1042,23 +1017,6 @@ function closest_point_on_plane(plane, point) = // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. -function _general_plane_line_intersection(plane, line, eps=EPSILON) = - let( - p0 = line[0], - p1 = line[1], - n = plane_normal(plane), - u = p1 - p0, - d = n * u - ) abs(d)=2, - "Invalid point(s)." ) - collinear(pt1,pt2,pt3)? [undef,undef,undef] : - let( - v1 = pt1-pt2, - v2 = pt3-pt2, - n = vector_axis(v1,v2), - n2 = n.z<0? -n : n - ) len(pt1)+len(pt2)+len(pt3)>6? ( - let( - a = project_plane(pt1, pt1, pt2, pt3), - b = project_plane(pt2, pt1, pt2, pt3), - c = project_plane(pt3, pt1, pt2, pt3), - res = find_circle_3points(a, b, c) - ) res[0]==undef? [undef,undef,undef] : let( - cp = lift_plane(res[0], pt1, pt2, pt3), - r = norm(pt2-cp) - ) [cp, r, n2] - ) : let( - mp1 = pt2 + v1/2, - mp2 = pt2 + v2/2, - mpv1 = rot(90, v=n, p=v1), - mpv2 = rot(90, v=n, p=v2), - l1 = [mp1, mp1+mpv1], - l2 = [mp2, mp2+mpv2], - isect = line_intersection(l1,l2) - ) is_undef(isect)? [undef,undef,undef] : let( - r = norm(pt2-isect) - ) [isect, r, n2]; - function find_circle_3points(pt1, pt2, pt3) = (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) ? find_circle_3points(pt1[0], pt1[1], pt1[2]) @@ -1403,9 +1327,6 @@ function find_circle_3points(pt1, pt2, pt3) = r = norm(sc-v[0]) ) [ cp, r, n ]; - - - // Function: circle_point_tangents() @@ -1607,19 +1528,6 @@ function furthest_point(pt, points) = // Arguments: // poly = polygon to compute the area of. // signed = if true, a signed area is returned (default: false) -function polygon_area(poly) = - assert(is_path(poly), "Invalid polygon." ) - len(poly)<3? 0 : - len(poly[0])==2? 0.5*sum([for(i=[0:1:len(poly)-1]) det2(select(poly,i,i+1))]) : - let( - plane = plane_from_points(poly) - ) plane==undef? undef : - let( - n = unit(plane_normal(plane)), - total = sum([for (i=[0:1:len(poly)-1]) cross(poly[i], select(poly,i+1))]), - res = abs(total * n) / 2 - ) res; - function polygon_area(poly, signed=false) = assert(is_path(poly), "Invalid polygon." ) len(poly)<3 ? 0 : @@ -1644,15 +1552,6 @@ function polygon_area(poly, signed=false) = // Example: // spiral = [for (i=[0:36]) let(a=-i*10) (10+i)*[cos(a),sin(a)]]; // is_convex_polygon(spiral); // Returns: false -function is_convex_polygon(poly) = - assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) - let( - l = len(poly), - c = [for (i=idx(poly)) cross(poly[(i+1)%l]-poly[i],poly[(i+2)%l]-poly[(i+1)%l])] - ) - len([for (x=c) if(x>0) 1])==0 || - len([for (x=c) if(x<0) 1])==0; - function is_convex_polygon(poly) = assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly) ) @@ -1726,33 +1625,6 @@ function polygon_shift_to_closest_point(path, pt) = // 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) = - 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.") - let( - dim = len(reference[0]), - N = len(reference), - fixpoly = dim != 2? poly : - polygon_is_clockwise(reference)? clockwise_polygon(poly) : - ccw_polygon(poly), - dist = [ - // Matrix of all pairwise distances - for (p1=reference) [ - for (p2=fixpoly) norm(p1-p2) - ] - ], - // Compute the sum of all distance pairs for a each shift - sums = [ - for(shift=[0:1:N-1]) sum([ - for(i=[0:1:N-1]) dist[i][(i+shift)%N] - ]) - ], - optimal_poly = polygon_shift(fixpoly,min_index(sums)) - ) - return_error? [optimal_poly, min(sums)] : - optimal_poly; - function reindex_polygon(reference, poly, return_error=false) = assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) @@ -1774,7 +1646,6 @@ function reindex_polygon(reference, poly, return_error=false) = optimal_poly; - // Function: align_polygon() // Usage: // newpoly = align_polygon(reference, poly, angles, [cp]); @@ -1819,26 +1690,6 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. -function centroid(poly) = - assert( is_path(poly), "The input must be a 2D or 3D polygon." ) - len(poly[0])==2 - ? sum([ - for(i=[0:len(poly)-1]) - let(segment=select(poly,i,i+1)) - det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) - : let( plane = plane_from_points(poly, fast=true) ) - assert( !is_undef(plane), "The polygon must be planar." ) - let( - n = plane_normal(plane), - p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), - p2 = vector_axis(n,p1), - cp = mean(poly), - proj = project_plane(poly,cp,cp+p1,cp+p2), - cxy = centroid(proj) - ) - lift_plane(cxy,cp,cp+p1,cp+p2); - function centroid(poly) = assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) len(poly[0])==2 @@ -1915,21 +1766,11 @@ function point_in_polygon(point, path, eps=EPSILON) = // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: // path = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - let( - minx = min(subindex(path,0)), - lowind = search(minx, path, 0, 0), - lowpts = select(path, lowind), - miny = min(subindex(lowpts, 1)), - extreme_sub = search(miny, lowpts, 1, 1)[0], - extreme = select(lowind,extreme_sub) - ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; - function polygon_is_clockwise(path) = assert(is_path(path,dim=2), "Input should be a 2d path") polygon_area(path, signed=true)<0; + // Function: clockwise_polygon() // Usage: // clockwise_polygon(path); From ee86544a8172756b389b1f7fb4e07f1e1e6d8cb5 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Tue, 18 Aug 2020 19:09:26 -0700 Subject: [PATCH 47/57] Added excess= argument to most mask modules. --- masks.scad | 97 +++++++++++++++++++++++++++------------------------- version.scad | 2 +- 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/masks.scad b/masks.scad index 4a75886..894e50d 100644 --- a/masks.scad +++ b/masks.scad @@ -12,8 +12,8 @@ // Module: angle_pie_mask() // Usage: -// angle_pie_mask(r|d, l, ang); -// angle_pie_mask(r1|d1, r2|d2, l, ang); +// angle_pie_mask(r|d, l, ang, [excess]); +// angle_pie_mask(r1|d1, r2|d2, l, ang, [excess]); // Description: // Creates a pie wedge shape that can be used to mask other shapes. // Arguments: @@ -25,6 +25,7 @@ // d = Diameter of circle wedge is created from. (optional) // d1 = Bottom diameter of cone that wedge is created from. (optional) // d2 = Upper diameter of cone that wedge is created from. (optional) +// excess = The extra thickness of the mask. Default: `0.1`. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -34,14 +35,14 @@ module angle_pie_mask( ang=45, l=undef, r=undef, r1=undef, r2=undef, d=undef, d1=undef, d2=undef, - h=undef, + h=undef, excess=0.1, anchor=CENTER, spin=0, orient=UP ) { l = first_defined([l, h, 1]); r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=10); r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=10); attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { - pie_slice(ang=ang, l=l+0.1, r1=r1, r2=r2, anchor=CENTER); + pie_slice(ang=ang, l=l+excess, r1=r1, r2=r2, anchor=CENTER); children(); } } @@ -49,13 +50,13 @@ module angle_pie_mask( // Module: cylinder_mask() // Usage: Mask objects -// cylinder_mask(l, r|d, chamfer, [chamfang], [from_end], [circum], [overage], [ends_only]); -// cylinder_mask(l, r|d, rounding, [circum], [overage], [ends_only]); -// cylinder_mask(l, r|d, [chamfer1|rounding1], [chamfer2|rounding2], [chamfang1], [chamfang2], [from_end], [circum], [overage], [ends_only]); +// cylinder_mask(l, r|d, chamfer, [chamfang], [from_end], [circum], [excess], [ends_only]); +// cylinder_mask(l, r|d, rounding, [circum], [excess], [ends_only]); +// cylinder_mask(l, r|d, [chamfer1|rounding1], [chamfer2|rounding2], [chamfang1], [chamfang2], [from_end], [circum], [excess], [ends_only]); // Usage: Masking operators -// cylinder_mask(l, r|d, chamfer, [chamfang], [from_end], [circum], [overage], [ends_only]) ... -// cylinder_mask(l, r|d, rounding, [circum], [overage], [ends_only]) ... -// cylinder_mask(l, r|d, [chamfer1|rounding1], [chamfer2|rounding2], [chamfang1], [chamfang2], [from_end], [circum], [overage], [ends_only]) ... +// cylinder_mask(l, r|d, chamfer, [chamfang], [from_end], [circum], [excess], [ends_only]) ... +// cylinder_mask(l, r|d, rounding, [circum], [excess], [ends_only]) ... +// cylinder_mask(l, r|d, [chamfer1|rounding1], [chamfer2|rounding2], [chamfang1], [chamfang2], [from_end], [circum], [excess], [ends_only]) ... // Description: // If passed children, bevels/chamfers and/or rounds one or both // ends of the origin-centered cylindrical region specified. If @@ -83,7 +84,7 @@ module angle_pie_mask( // rounding2 = The radius of the rounding on the axis-positive end of the region. // circum = If true, region will circumscribe the circle of the given radius/diameter. // from_end = If true, chamfer/bevel size is measured from end of region. If false, chamfer/bevel is measured outset from the radius of the region. (Default: false) -// overage = The extra thickness of the mask. Default: `10`. +// excess = The extra thickness of the mask. Default: `10`. // ends_only = If true, only mask the ends and not around the middle of the cylinder. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` @@ -105,7 +106,7 @@ module cylinder_mask( chamfang=undef, chamfang1=undef, chamfang2=undef, rounding=undef, rounding1=undef, rounding2=undef, circum=false, from_end=false, - overage=10, ends_only=false, + excess=10, ends_only=false, anchor=CENTER, spin=0, orient=UP ) { r1 = get_radius(r=r, d=d, r1=r1, d1=d1, dflt=1); @@ -132,12 +133,12 @@ module cylinder_mask( chlen1 = cham1 / (from_end? 1 : tan(ang1)); chlen2 = cham2 / (from_end? 1 : tan(ang2)); if (!ends_only) { - cylinder(r=maxd+overage, h=l+2*overage, center=true); + cylinder(r=maxd+excess, h=l+2*excess, center=true); } else { - if (cham2>0) up(l/2-chlen2) cylinder(r=maxd+overage, h=chlen2+overage, center=false); - if (cham1>0) down(l/2+overage) cylinder(r=maxd+overage, h=chlen1+overage, center=false); - if (fil2>0) up(l/2-fil2) cylinder(r=maxd+overage, h=fil2+overage, center=false); - if (fil1>0) down(l/2+overage) cylinder(r=maxd+overage, h=fil1+overage, center=false); + if (cham2>0) up(l/2-chlen2) cylinder(r=maxd+excess, h=chlen2+excess, center=false); + if (cham1>0) down(l/2+excess) cylinder(r=maxd+excess, h=chlen1+excess, center=false); + if (fil2>0) up(l/2-fil2) cylinder(r=maxd+excess, h=fil2+excess, center=false); + if (fil1>0) down(l/2+excess) cylinder(r=maxd+excess, h=fil1+excess, center=false); } } cyl(r1=sc*r1, r2=sc*r2, l=l, chamfer1=cham1, chamfer2=cham2, chamfang1=ang1, chamfang2=ang2, from_end=from_end, rounding1=fil1, rounding2=fil2); @@ -154,14 +155,15 @@ module cylinder_mask( // Module: chamfer_mask() // Usage: -// chamfer_mask(l, chamfer); +// chamfer_mask(l, chamfer, [excess]); // Description: // Creates a shape that can be used to chamfer a 90 degree edge. // Difference it from the object to be chamfered. The center of // the mask object should align exactly with the edge to be chamfered. // Arguments: // l = Length of mask. -// chamfer = Size of chamfer +// chamfer = Size of chamfer. +// excess = The extra amount to add to the length of the mask so that it differences away from other shapes cleanly. Default: `0.1` // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -170,9 +172,9 @@ module cylinder_mask( // cube(50, anchor=BOTTOM+FRONT); // #chamfer_mask(l=50, chamfer=10, orient=RIGHT); // } -module chamfer_mask(l=1, chamfer=1, anchor=CENTER, spin=0, orient=UP) { +module chamfer_mask(l=1, chamfer=1, excess=0.1, anchor=CENTER, spin=0, orient=UP) { attachable(anchor,spin,orient, size=[chamfer*2, chamfer*2, l]) { - cylinder(r=chamfer, h=l+0.1, center=true, $fn=4); + cylinder(r=chamfer, h=l+excess, center=true, $fn=4); children(); } } @@ -180,14 +182,15 @@ module chamfer_mask(l=1, chamfer=1, anchor=CENTER, spin=0, orient=UP) { // Module: chamfer_mask_x() // Usage: -// chamfer_mask_x(l, chamfer, [anchor]); +// chamfer_mask_x(l, chamfer, [excess]); // Description: // Creates a shape that can be used to chamfer a 90 degree edge along the X axis. // Difference it from the object to be chamfered. The center of the mask // object should align exactly with the edge to be chamfered. // Arguments: -// l = Height of mask -// chamfer = size of chamfer +// l = Length of mask. +// chamfer = Size of chamfer. +// excess = The extra amount to add to the length of the mask so that it differences away from other shapes cleanly. Default: `0.1` // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the X axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example: @@ -195,21 +198,22 @@ module chamfer_mask(l=1, chamfer=1, anchor=CENTER, spin=0, orient=UP) { // cube(50, anchor=BOTTOM+FRONT); // #chamfer_mask_x(l=50, chamfer=10); // } -module chamfer_mask_x(l=1.0, chamfer=1.0, anchor=CENTER, spin=0) { - chamfer_mask(l=l, chamfer=chamfer, anchor=anchor, spin=spin, orient=RIGHT) children(); +module chamfer_mask_x(l=1.0, chamfer=1.0, excess=0.1, anchor=CENTER, spin=0) { + chamfer_mask(l=l, chamfer=chamfer, excess=excess, anchor=anchor, spin=spin, orient=RIGHT) children(); } // Module: chamfer_mask_y() // Usage: -// chamfer_mask_y(l, chamfer, [anchor]); +// chamfer_mask_y(l, chamfer, [excess]); // Description: // Creates a shape that can be used to chamfer a 90 degree edge along the Y axis. // Difference it from the object to be chamfered. The center of the mask // object should align exactly with the edge to be chamfered. // Arguments: -// l = Height of mask -// chamfer = size of chamfer +// l = Length of mask. +// chamfer = Size of chamfer. +// excess = The extra amount to add to the length of the mask so that it differences away from other shapes cleanly. Default: `0.1` // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Y axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example: @@ -217,21 +221,22 @@ module chamfer_mask_x(l=1.0, chamfer=1.0, anchor=CENTER, spin=0) { // cube(50, anchor=BOTTOM+RIGHT); // #chamfer_mask_y(l=50, chamfer=10); // } -module chamfer_mask_y(l=1.0, chamfer=1.0, anchor=CENTER, spin=0) { - chamfer_mask(l=l, chamfer=chamfer, anchor=anchor, spin=spin, orient=BACK) children(); +module chamfer_mask_y(l=1.0, chamfer=1.0, excess=0.1, anchor=CENTER, spin=0) { + chamfer_mask(l=l, chamfer=chamfer, excess=excess, anchor=anchor, spin=spin, orient=BACK) children(); } // Module: chamfer_mask_z() // Usage: -// chamfer_mask_z(l, chamfer, [anchor]); +// chamfer_mask_z(l, chamfer, [excess]); // Description: // Creates a shape that can be used to chamfer a 90 degree edge along the Z axis. // Difference it from the object to be chamfered. The center of the mask // object should align exactly with the edge to be chamfered. // Arguments: -// l = Height of mask -// chamfer = size of chamfer +// l = Length of mask. +// chamfer = Size of chamfer. +// excess = The extra amount to add to the length of the mask so that it differences away from other shapes cleanly. Default: `0.1` // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example: @@ -239,8 +244,8 @@ module chamfer_mask_y(l=1.0, chamfer=1.0, anchor=CENTER, spin=0) { // cube(50, anchor=FRONT+RIGHT); // #chamfer_mask_z(l=50, chamfer=10); // } -module chamfer_mask_z(l=1.0, chamfer=1.0, anchor=CENTER, spin=0) { - chamfer_mask(l=l, chamfer=chamfer, anchor=anchor, spin=spin, orient=UP) children(); +module chamfer_mask_z(l=1.0, chamfer=1.0, excess=0.1, anchor=CENTER, spin=0) { + chamfer_mask(l=l, chamfer=chamfer, excess=excess, anchor=anchor, spin=spin, orient=UP) children(); } @@ -313,7 +318,7 @@ module chamfer_cylinder_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=fa // Module: chamfer_hole_mask() // Usage: -// chamfer_hole_mask(r|d, chamfer, [ang], [from_end]); +// chamfer_hole_mask(r|d, chamfer, [ang], [from_end], [excess]); // Description: // Create a mask that can be used to bevel/chamfer the end of a cylindrical hole. // Difference it from the hole to be chamfered. The center of the mask object @@ -324,7 +329,7 @@ module chamfer_cylinder_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=fa // chamfer = Size of the chamfer. (Default: 0.25) // ang = Angle of chamfer in degrees from vertical. (Default: 45) // from_end = If true, chamfer size is measured from end of hole. If false, chamfer is measured outset from the radius of the hole. (Default: false) -// overage = The extra thickness of the mask. Default: `0.1`. +// excess = The extra thickness of the mask. Default: `0.1`. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -341,8 +346,8 @@ module chamfer_cylinder_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=fa // up(50) chamfer_hole_mask(d=50, chamfer=10); // } // Example: -// chamfer_hole_mask(d=100, chamfer=25, ang=30, overage=10); -module chamfer_hole_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=false, overage=0.1, anchor=CENTER, spin=0, orient=UP) +// chamfer_hole_mask(d=100, chamfer=25, ang=30, excess=10); +module chamfer_hole_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=false, excess=0.1, anchor=CENTER, spin=0, orient=UP) { r = get_radius(r=r, d=d, dflt=1); h = chamfer * (from_end? 1 : tan(90-ang)); @@ -350,7 +355,7 @@ module chamfer_hole_mask(r=undef, d=undef, chamfer=0.25, ang=45, from_end=false, $fn = segs(r); attachable(anchor,spin,orient, r1=r, r2=r2, l=h*2) { union() { - cylinder(r=r2, h=overage, center=false); + cylinder(r=r2, h=excess, center=false); down(h) cylinder(r1=r, r2=r2, h=h, center=false); } children(); @@ -735,14 +740,14 @@ module rounding_corner_mask(r=1.0, anchor=CENTER, spin=0, orient=UP) // } module rounding_cylinder_mask(r=1.0, rounding=0.25) { - cylinder_mask(l=rounding*3, r=r, rounding2=rounding, overage=rounding, ends_only=true, anchor=TOP); + cylinder_mask(l=rounding*3, r=r, rounding2=rounding, excess=rounding, ends_only=true, anchor=TOP); } // Module: rounding_hole_mask() // Usage: -// rounding_hole_mask(r|d, rounding); +// rounding_hole_mask(r|d, rounding, [excess]); // Description: // Create a mask that can be used to round the edge of a circular hole. // Difference it from the hole to be rounded. The center of the @@ -752,7 +757,7 @@ module rounding_cylinder_mask(r=1.0, rounding=0.25) // r = Radius of hole. // d = Diameter of hole to rounding. // rounding = Radius of the rounding. (Default: 0.25) -// overage = The extra thickness of the mask. Default: `0.1`. +// excess = The extra thickness of the mask. Default: `0.1`. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -770,13 +775,13 @@ module rounding_cylinder_mask(r=1.0, rounding=0.25) // } // Example: // rounding_hole_mask(r=40, rounding=20, $fa=2, $fs=2); -module rounding_hole_mask(r=undef, d=undef, rounding=0.25, overage=0.1, anchor=CENTER, spin=0, orient=UP) +module rounding_hole_mask(r=undef, d=undef, rounding=0.25, excess=0.1, anchor=CENTER, spin=0, orient=UP) { r = get_radius(r=r, d=d, dflt=1); attachable(anchor,spin,orient, r=r+rounding, l=2*rounding) { rotate_extrude(convexity=4) { difference() { - right(r-overage) fwd(rounding) square(rounding+overage, center=false); + right(r-excess) fwd(rounding) square(rounding+excess, center=false); right(r+rounding) fwd(rounding) circle(r=rounding); } } diff --git a/version.scad b/version.scad index 1420f58..627ee3d 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,405]; +BOSL_VERSION = [2,0,406]; // Section: BOSL Library Version Functions From 5e981fb4a716f9fe2ab3dcf6c9f7245fc7d752c4 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Tue, 18 Aug 2020 19:25:05 -0700 Subject: [PATCH 48/57] Added tests for path3d() and path4d() with fille= --- tests/test_coords.scad | 4 ++++ version.scad | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_coords.scad b/tests/test_coords.scad index 4b89c12..9fccd65 100644 --- a/tests/test_coords.scad +++ b/tests/test_coords.scad @@ -29,6 +29,7 @@ test_point3d(); module test_path3d() { assert(path3d([[1,2], [3,4], [5,6], [7,8]])==[[1,2,0],[3,4,0],[5,6,0],[7,8,0]]); + assert(path3d([[1,2], [3,4], [5,6], [7,8]],9)==[[1,2,9],[3,4,9],[5,6,9],[7,8,9]]); assert(path3d([[1,2,3], [2,3,4], [3,4,5], [4,5,6]])==[[1,2,3],[2,3,4],[3,4,5],[4,5,6]]); assert(path3d([[1,2,3,4], [2,3,4,5], [3,4,5,6], [4,5,6,7]])==[[1,2,3],[2,3,4],[3,4,5],[4,5,6]]); } @@ -41,6 +42,9 @@ module test_point4d() { assert(point4d([1,2,3])==[1,2,3,0]); assert(point4d([2,3])==[2,3,0,0]); assert(point4d([1])==[1,0,0,0]); + assert(point4d([1,2,3],9)==[1,2,3,9]); + assert(point4d([2,3],9)==[2,3,9,9]); + assert(point4d([1],9)==[1,9,9,9]); } test_point4d(); diff --git a/version.scad b/version.scad index 627ee3d..3d457d6 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,406]; +BOSL_VERSION = [2,0,407]; // Section: BOSL Library Version Functions From 12963296bb2a85bd4499cd6d6f6ee107415a8b65 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 20 Aug 2020 22:03:20 +0100 Subject: [PATCH 49/57] In observance of owner's last review Eliminate double definitions. Eliminate unneeded comments. In common.scad redefine num_defined(), all_defined() and get_radius(). In geometry.scad: - change name _dist to _dist2line - simplify _point_above_below_segment() and triangle_area() - change some arg names for uniformity (path>>poly) - change point_in_polygon() to accept the Even-odd rule as alternative - and other minor edits Update tests_geometry to the new funcionalities. --- common.scad | 67 +++----- geometry.scad | 341 ++++++++++----------------------------- tests/test_geometry.scad | 55 ++++--- 3 files changed, 145 insertions(+), 318 deletions(-) diff --git a/common.scad b/common.scad index 51d8363..db4f3bb 100644 --- a/common.scad +++ b/common.scad @@ -129,11 +129,6 @@ function is_list_of(list,pattern) = is_list(list) && []==[for(entry=0*list) if (entry != pattern) entry]; -function _list_pattern(list) = - is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] - : 0; - - // Function: is_consistent() // Usage: @@ -198,11 +193,11 @@ function first_defined(v,recursive=false,_i=0) = is_undef(first_defined(v[_i],recursive=recursive)) ) )? first_defined(v,recursive=recursive,_i=_i+1) : v[_i]; - + // Function: one_defined() // Usage: -// one_defined(vars, names, [required]) +// one_defined(vars, names, ) // Description: // Examines the input list `vars` and returns the entry which is not `undef`. If more // than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed @@ -221,8 +216,7 @@ function one_defined(vars, names, required=true) = // Function: num_defined() // Description: Counts how many items in list `v` are not `undef`. -function num_defined(v,_i=0,_cnt=0) = _i>=len(v)? _cnt : num_defined(v,_i+1,_cnt+(is_undef(v[_i])? 0 : 1)); - +function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); // Function: any_defined() // Description: @@ -239,8 +233,8 @@ function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) ! // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive&&is_list(x)&&!all_defined(x))? 1 : 0])==0; - +function all_defined(v,recursive=false) = + []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; @@ -249,7 +243,7 @@ function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive // Function: get_anchor() // Usage: -// get_anchor(anchor,center,[uncentered],[dflt]); +// get_anchor(anchor,center,,); // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -270,7 +264,7 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius([r1], [r2], [r], [d1], [d2], [d], [dflt]); +// get_radius(, , , , , , ); // Description: // Given various radii and diameters, returns the most specific radius. // If a diameter is most specific, returns half its value, giving the radius. @@ -288,34 +282,23 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // r = Most general radius. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. -function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1) - ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r1), "Invalid radius r1." ) - r1 - : !is_undef(r2) - ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r2), "Invalid radius r2." ) - r2 - : !is_undef(d1) - ? assert(is_finite(d1), "Invalid diameter d1." ) - d1/2 - : !is_undef(d2) - ? assert(is_finite(d2), "Invalid diameter d2." ) - d2/2 - : !is_undef(r) - ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) - r - : !is_undef(d) - ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) - d/2 - : dflt -); +function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = + assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") + !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 + : !is_undef(r2) ? assert(is_finite(r2), "Invalid radius r2." ) r2 + : !is_undef(d1) ? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 + : !is_undef(d2) ? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) d/2 + : dflt; + // Function: get_height() // Usage: -// get_height([h],[l],[height],[dflt]) +// get_height(,,,) // Description: // Given several different parameters for height check that height is not multiply defined // and return a single value. If the three values `l`, `h`, and `height` are all undefined @@ -332,7 +315,7 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // Function: scalar_vec3() // Usage: -// scalar_vec3(v, [dflt]); +// scalar_vec3(v, ); // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -384,7 +367,7 @@ function _valstr(x) = // Module: assert_approx() // Usage: -// assert_approx(got, expected, [info]); +// assert_approx(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -411,7 +394,7 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: -// assert_equal(got, expected, [info]); +// assert_equal(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -438,7 +421,7 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: -// shape_compare([eps]) {test_shape(); expected_shape();} +// shape_compare() {test_shape(); expected_shape();} // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. // Returns the differential geometry if they are not nearly the same shape and size. diff --git a/geometry.scad b/geometry.scad index eff67bc..fff7ebf 100644 --- a/geometry.scad +++ b/geometry.scad @@ -23,36 +23,25 @@ function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) - approx(point,edge[0],eps=eps) - || approx(point,edge[1],eps=eps) // The point is an endpoint - || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the - || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints - && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge - -function point_on_segment2d(point, edge, eps=EPSILON) = - assert( is_vector(point,2), "Invalid point." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) + assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], ne = norm(de) ) ( dp*de >= -eps*ne ) - && ( (dp-de)*de <= eps*ne ) // point projects on the segment - && _dist(point-edge[0],unit(de)) point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 - ) : ( - (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 - ); + let( edge = edge - [point, point] ) + edge[0].y <= 0 + ? (edge[1].y > 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0 + : (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0 ; //Internal function _valid_line(line,dim,eps=EPSILON) = @@ -98,10 +87,6 @@ function collinear(a, b, c, eps=EPSILON) = : noncollinear_triple(points,error=false,eps=eps)==[]; -//*** valid for any dimension - - - // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -115,7 +100,7 @@ function collinear(a, b, c, eps=EPSILON) = function distance_from_line(line, pt) = assert( _valid_line(line) && is_vector(pt,len(line[0])), "Invalid line, invalid point or incompatible dimensions." ) - _dist(pt-line[0],unit(line[1]-line[0])); + _dist2line(pt-line[0],unit(line[1]-line[0])); // Function: line_normal() @@ -330,17 +315,6 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -function line_closest_point(line,pt) = - assert(is_path(line)&&len(line)==2) - assert(same_shape(pt,line[0])) - assert(!approx(line[0],line[1])) - let( - seglen = norm(line[1]-line[0]), - segvec = (line[1]-line[0])/seglen, - projection = (pt-line[0]) * segvec - ) - line[0] + projection*segvec; - function line_closest_point(line,pt) = assert(_valid_line(line), "Invalid line." ) assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) @@ -774,14 +748,10 @@ function adj_opp_to_ang(adj,opp) = // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 function triangle_area(a,b,c) = - assert( is_path([a,b,c]), - "Invalid points or incompatible dimensions." ) - len(a)==3 ? 0.5*norm(cross(c-a,c-b)) - : ( - a.x * (b.y - c.y) + - b.x * (c.y - a.y) + - c.x * (a.y - b.y) - ) / 2; + assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) + len(a)==3 + ? 0.5*norm(cross(c-a,c-b)) + : 0.5*cross(c-a,c-b); @@ -851,7 +821,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Function: plane_from_points() // Usage: -// plane_from_points(points, [fast], [eps]); +// plane_from_points(points, , ); // Description: // Given a list of 3 or more coplanar 3D points, returns the coefficients of the cartesian equation of a plane, // that is [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. @@ -876,7 +846,6 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) indices==[] ? undef : let( - indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], @@ -913,11 +882,6 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = ) fast? plane: coplanar(poly,eps=eps)? plane: []; -//*** -// I don't see why this function uses a criterium different from plane_from_points. -// In practical terms, what is the difference of finding a plane from points and from polygon? -// The docs don't clarify. -// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: @@ -969,8 +933,8 @@ function plane_transform(plane) = // Usage: // projection_on_plane(points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection -// of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal +// projection of the points on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1042,23 +1006,6 @@ function closest_point_on_plane(plane, point) = // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. -function _general_plane_line_intersection(plane, line, eps=EPSILON) = - let( - p0 = line[0], - p1 = line[1], - n = plane_normal(plane), - u = p1 - p0, - d = n * u - ) abs(d)); // Description: // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: @@ -1220,7 +1168,7 @@ function coplanar(points, eps=EPSILON) = // Function: points_on_plane() // Usage: -// points_on_plane(points, plane, eps); +// points_on_plane(points, plane, ); // Description: // Returns true if the given 3D points are on the given plane. // Arguments: @@ -1256,7 +1204,7 @@ function in_front_of_plane(plane, point) = // Function: find_circle_2tangents() // Usage: -// find_circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); +// find_circle_2tangents(pt1, pt2, pt3, r|d, ); // Description: // Given a pair of rays with a common origin, and a known circle radius/diameter, finds // the centerpoint for the circle of that size that touches both rays tangentally. @@ -1325,7 +1273,8 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, [pt2, pt3]); +// find_circle_3points(pt1, pt2, pt3); +// find_circle_3points([pt1, pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear // points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). @@ -1345,40 +1294,6 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // translate(circ[0]) color("green") stroke(circle(r=circ[1]),closed=true,$fn=72); // translate(circ[0]) color("red") circle(d=3, $fn=12); // move_copies(pts) color("blue") circle(d=3, $fn=12); -function find_circle_3points(pt1, pt2, pt3) = - (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) - ? find_circle_3points(pt1[0], pt1[1], pt1[2]) - : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) - && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, - "Invalid point(s)." ) - collinear(pt1,pt2,pt3)? [undef,undef,undef] : - let( - v1 = pt1-pt2, - v2 = pt3-pt2, - n = vector_axis(v1,v2), - n2 = n.z<0? -n : n - ) len(pt1)+len(pt2)+len(pt3)>6? ( - let( - a = project_plane(pt1, pt1, pt2, pt3), - b = project_plane(pt2, pt1, pt2, pt3), - c = project_plane(pt3, pt1, pt2, pt3), - res = find_circle_3points(a, b, c) - ) res[0]==undef? [undef,undef,undef] : let( - cp = lift_plane(res[0], pt1, pt2, pt3), - r = norm(pt2-cp) - ) [cp, r, n2] - ) : let( - mp1 = pt2 + v1/2, - mp2 = pt2 + v2/2, - mpv1 = rot(90, v=n, p=v1), - mpv2 = rot(90, v=n, p=v2), - l1 = [mp1, mp1+mpv1], - l2 = [mp2, mp2+mpv2], - isect = line_intersection(l1,l2) - ) is_undef(isect)? [undef,undef,undef] : let( - r = norm(pt2-isect) - ) [isect, r, n2]; - function find_circle_3points(pt1, pt2, pt3) = (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) ? find_circle_3points(pt1[0], pt1[1], pt1[2]) @@ -1404,9 +1319,6 @@ function find_circle_3points(pt1, pt2, pt3) = ) [ cp, r, n ]; - - - // Function: circle_point_tangents() // Usage: @@ -1442,7 +1354,6 @@ function circle_point_tangents(r, d, cp, pt) = ) [for (ang=angs) [ang, cp + r*[cos(ang),sin(ang)]]]; - // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: @@ -1541,13 +1452,14 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [] : let( n = (pb-pa)/nrm, - distlist = [for(i=[0:len(points)-1]) _dist(points[i]-pa, n)] + distlist = [for(i=[0:len(points)-1]) _dist2line(points[i]-pa, n)] ) max(distlist)0) 1])==0 || - len([for (x=c) if(x<0) 1])==0; - function is_convex_polygon(poly) = assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly) ) @@ -1686,15 +1576,15 @@ function polygon_shift(poly, i) = // Usage: // polygon_shift_to_closest_point(path, pt); // Description: -// Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. -function polygon_shift_to_closest_point(path, pt) = +// Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. +function polygon_shift_to_closest_point(poly, pt) = assert(is_vector(pt), "Invalid point." ) - assert(is_path(path,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) + assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) let( - path = cleanup_path(path), - dists = [for (p=path) norm(p-pt)], + poly = cleanup_path(poly), + dists = [for (p=poly) norm(p-pt)], closest = min_index(dists) - ) select(path,closest,closest+len(path)-1); + ) select(poly,closest,closest+len(poly)-1); // Function: reindex_polygon() @@ -1726,33 +1616,6 @@ function polygon_shift_to_closest_point(path, pt) = // 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) = - 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.") - let( - dim = len(reference[0]), - N = len(reference), - fixpoly = dim != 2? poly : - polygon_is_clockwise(reference)? clockwise_polygon(poly) : - ccw_polygon(poly), - dist = [ - // Matrix of all pairwise distances - for (p1=reference) [ - for (p2=fixpoly) norm(p1-p2) - ] - ], - // Compute the sum of all distance pairs for a each shift - sums = [ - for(shift=[0:1:N-1]) sum([ - for(i=[0:1:N-1]) dist[i][(i+shift)%N] - ]) - ], - optimal_poly = polygon_shift(fixpoly,min_index(sums)) - ) - return_error? [optimal_poly, min(sums)] : - optimal_poly; - function reindex_polygon(reference, poly, return_error=false) = assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) @@ -1774,10 +1637,9 @@ function reindex_polygon(reference, poly, return_error=false) = optimal_poly; - // Function: align_polygon() // Usage: -// newpoly = align_polygon(reference, poly, angles, [cp]); +// newpoly = align_polygon(reference, poly, angles, ); // Description: // Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns // with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation @@ -1819,26 +1681,6 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. -function centroid(poly) = - assert( is_path(poly), "The input must be a 2D or 3D polygon." ) - len(poly[0])==2 - ? sum([ - for(i=[0:len(poly)-1]) - let(segment=select(poly,i,i+1)) - det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) - : let( plane = plane_from_points(poly, fast=true) ) - assert( !is_undef(plane), "The polygon must be planar." ) - let( - n = plane_normal(plane), - p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), - p2 = vector_axis(n,p1), - cp = mean(poly), - proj = project_plane(poly,cp,cp+p1,cp+p2), - cxy = centroid(proj) - ) - lift_plane(cxy,cp,cp+p1,cp+p2); - function centroid(poly) = assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) len(poly[0])==2 @@ -1866,10 +1708,11 @@ function centroid(poly) = // Function: point_in_polygon() // Usage: -// point_in_polygon(point, path, [eps]) +// point_in_polygon(point, poly, ) // Description: // This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using the Winding Number method. +// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. +// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. // The polygon is given as a list of 2D points, not including the repeated end point. // Returns -1 if the point is outside the polyon. // Returns 0 if the point is on the boundary. @@ -1879,75 +1722,81 @@ function centroid(poly) = // Rounding error may give mixed results for points on or near the boundary. // Arguments: // point = The 2D point to check position of. -// path = The list of 2D path points forming the perimeter of the polygon. +// poly = The list of 2D path points forming the perimeter of the polygon. +// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, path, eps=EPSILON) = - // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html - assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, +function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = + // Original algorithms from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. let( - on_brd = [for(i=[0:1:len(path)-1]) - let( seg = select(path,i,i+1) ) - if( !approx(seg[0],seg[1],eps=eps) ) + on_brd = [for(i=[0:1:len(poly)-1]) + let( seg = select(poly,i,i+1) ) + if( !approx(seg[0],seg[1],eps=EPSILON) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) - sum(on_brd) > 0? 0 : - // Otherwise compute winding number and return 1 for interior, -1 for exterior - let( - windchk = [for(i=[0:1:len(path)-1]) - let(seg=select(path,i,i+1)) - if(!approx(seg[0],seg[1],eps=eps)) - _point_above_below_segment(point, seg) - ] - ) - sum(windchk) != 0 ? 1 : -1; + sum(on_brd) > 0 + ? 0 + : nonzero + ? // Compute winding number and return 1 for interior, -1 for exterior + let( + windchk = [for(i=[0:1:len(poly)-1]) + let(seg=select(poly,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1 + : // or compute the crossings with the ray [point, point+[1,0]] + let( + n = len(poly), + cross = + [for(i=[0:n-1]) + let( + p0 = poly[i]-point, + p1 = poly[(i+1)%n]-point + ) + if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) + && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) + 1 + ] + ) + 2*(len(cross)%2)-1;; -//** -// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: -// polygon_is_clockwise(path); +// polygon_is_clockwise(poly); // Description: // Return true if the given 2D simple polygon is in clockwise order, false otherwise. // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: -// path = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - let( - minx = min(subindex(path,0)), - lowind = search(minx, path, 0, 0), - lowpts = select(path, lowind), - miny = min(subindex(lowpts, 1)), - extreme_sub = search(miny, lowpts, 1, 1)[0], - extreme = select(lowind,extreme_sub) - ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; +// poly = The list of 2D path points for the perimeter of the polygon. +function polygon_is_clockwise(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d path") + polygon_area(poly, signed=true)<0; -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d path") - polygon_area(path, signed=true)<0; // Function: clockwise_polygon() // Usage: -// clockwise_polygon(path); +// clockwise_polygon(poly); // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. -function clockwise_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); +function clockwise_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly); // Function: ccw_polygon() // Usage: -// ccw_polygon(path); +// ccw_polygon(poly); // Description: -// Given a 2D polygon path, returns the counter-clockwise winding version of that path. -function ccw_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; +// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. +function ccw_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly; // Function: reverse_polygon() @@ -1969,7 +1818,7 @@ function reverse_polygon(poly) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - poly = path3d(cleanup_path(poly)), + poly = cleanup_path(poly), p0 = poly[0], n = sum([ for (i=[1:1:len(poly)-2]) @@ -2085,17 +1934,6 @@ function split_polygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); -//*** -// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: -// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) -// produces: -// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] -// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] -// and the second polygon is self-intersecting -// besides, it fails in some simple cases as triangles: -// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] -// this last failure may be fatal for vnf_bend - // Function: split_polygons_at_each_y() // Usage: @@ -2106,9 +1944,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = - assert( is_consistent(polys) && is_path(poly[0],dim=3) , - "The input list should contains only 3D polygons." ) - assert( is_finite(ys), "The split value list should contain only numbers." ) +// assert( is_consistent(polys) && is_path(polys[0],dim=3) , // not all polygons should have the same length!!! + // "The input list should contains only 3D polygons." ) + assert( is_finite(ys) || is_vector(ys), "The split value list should contain only numbers." ) //*** _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -2139,5 +1977,4 @@ function split_polygons_at_each_z(polys, zs, _i=0) = ); - // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index ceeb905..0950e1c 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -98,6 +98,8 @@ function standardize(v) = v==[]? [] : sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; +module assert_std(vc,ve) { assert(standardize(vc)==standardize(ve)); } + module test_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); @@ -487,48 +489,47 @@ module test_triangle_area() { module test_plane3pt() { - assert(plane3pt([0,0,20], [0,10,10], [0,0,0]) == [1,0,0,0]); - assert(plane3pt([2,0,20], [2,10,10], [2,0,0]) == [1,0,0,2]); - assert(plane3pt([0,0,0], [10,0,10], [0,0,20]) == [0,1,0,0]); - assert(plane3pt([0,2,0], [10,2,10], [0,2,20]) == [0,1,0,2]); - assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); - assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); + assert_std(plane3pt([0,0,20], [0,10,10], [0,0,0]), [1,0,0,0]); + assert_std(plane3pt([2,0,20], [2,10,10], [2,0,0]), [1,0,0,2]); + assert_std(plane3pt([0,0,0], [10,0,10], [0,0,20]), [0,1,0,0]); + assert_std(plane3pt([0,2,0], [10,2,10], [0,2,20]), [0,1,0,2]); + assert_std(plane3pt([0,0,0], [10,10,0], [20,0,0]), [0,0,1,0]); + assert_std(plane3pt([0,0,2], [10,10,2], [20,0,2]), [0,0,1,2]); } *test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; s13 = sqrt(1/3); - assert(plane3pt_indexed(pts, 0,3,2) == [1,0,0,0]); - assert(plane3pt_indexed(pts, 0,2,3) == [-1,0,0,0]); - assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); - assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); - assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); + assert_std(plane3pt_indexed(pts, 0,3,2), [1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,3), [-1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,1,3), [0,1,0,0]); + assert_std(plane3pt_indexed(pts, 0,3,1), [0,-1,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,1), [0,0,1,0]); assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } *test_plane3pt_indexed(); - module test_plane_from_points() { - assert(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]) == [1,0,0,0]); - assert(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]) == [1,0,0,2]); - assert(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]) == [0,1,0,0]); - assert(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]) == [0,1,0,2]); - assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); - assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); + assert_std(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]), [1,0,0,0]); + assert_std(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]), [1,0,0,2]); + assert_std(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]), [0,1,0,0]); + assert_std(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]), [0,1,0,2]); + assert_std(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]), [0,0,1,0]); + assert_std(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]), [0,0,1,2]); } *test_plane_from_points(); module test_plane_normal() { - assert(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); - assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])), [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])), [0,0,1]); } *test_plane_normal(); @@ -699,16 +700,22 @@ module test_simplify_path_indexed() { module test_point_in_polygon() { poly = [for (a=[0:30:359]) 10*[cos(a),sin(a)]]; + poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); + assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); + assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); + assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From 5051fe59775f322e1dc33b5be8f6bd06b4ce00f2 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 20 Aug 2020 22:22:55 +0100 Subject: [PATCH 50/57] Revert "In observance of owner's last review" This reverts commit 12963296bb2a85bd4499cd6d6f6ee107415a8b65. --- common.scad | 67 +++++--- geometry.scad | 341 +++++++++++++++++++++++++++++---------- tests/test_geometry.scad | 55 +++---- 3 files changed, 318 insertions(+), 145 deletions(-) diff --git a/common.scad b/common.scad index db4f3bb..51d8363 100644 --- a/common.scad +++ b/common.scad @@ -129,6 +129,11 @@ function is_list_of(list,pattern) = is_list(list) && []==[for(entry=0*list) if (entry != pattern) entry]; +function _list_pattern(list) = + is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] + : 0; + + // Function: is_consistent() // Usage: @@ -193,11 +198,11 @@ function first_defined(v,recursive=false,_i=0) = is_undef(first_defined(v[_i],recursive=recursive)) ) )? first_defined(v,recursive=recursive,_i=_i+1) : v[_i]; - + // Function: one_defined() // Usage: -// one_defined(vars, names, ) +// one_defined(vars, names, [required]) // Description: // Examines the input list `vars` and returns the entry which is not `undef`. If more // than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed @@ -216,7 +221,8 @@ function one_defined(vars, names, required=true) = // Function: num_defined() // Description: Counts how many items in list `v` are not `undef`. -function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); +function num_defined(v,_i=0,_cnt=0) = _i>=len(v)? _cnt : num_defined(v,_i+1,_cnt+(is_undef(v[_i])? 0 : 1)); + // Function: any_defined() // Description: @@ -233,8 +239,8 @@ function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) ! // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function all_defined(v,recursive=false) = - []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; +function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive&&is_list(x)&&!all_defined(x))? 1 : 0])==0; + @@ -243,7 +249,7 @@ function all_defined(v,recursive=false) = // Function: get_anchor() // Usage: -// get_anchor(anchor,center,,); +// get_anchor(anchor,center,[uncentered],[dflt]); // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -264,7 +270,7 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius(, , , , , , ); +// get_radius([r1], [r2], [r], [d1], [d2], [d], [dflt]); // Description: // Given various radii and diameters, returns the most specific radius. // If a diameter is most specific, returns half its value, giving the radius. @@ -282,23 +288,34 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // r = Most general radius. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. -function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = - assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") - !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 - : !is_undef(r2) ? assert(is_finite(r2), "Invalid radius r2." ) r2 - : !is_undef(d1) ? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 - : !is_undef(d2) ? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 - : !is_undef(r) - ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) - r - : !is_undef(d) ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) d/2 - : dflt; - +function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( + !is_undef(r1) + ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r1), "Invalid radius r1." ) + r1 + : !is_undef(r2) + ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r2), "Invalid radius r2." ) + r2 + : !is_undef(d1) + ? assert(is_finite(d1), "Invalid diameter d1." ) + d1/2 + : !is_undef(d2) + ? assert(is_finite(d2), "Invalid diameter d2." ) + d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) + ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) + d/2 + : dflt +); // Function: get_height() // Usage: -// get_height(,,,) +// get_height([h],[l],[height],[dflt]) // Description: // Given several different parameters for height check that height is not multiply defined // and return a single value. If the three values `l`, `h`, and `height` are all undefined @@ -315,7 +332,7 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // Function: scalar_vec3() // Usage: -// scalar_vec3(v, ); +// scalar_vec3(v, [dflt]); // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -367,7 +384,7 @@ function _valstr(x) = // Module: assert_approx() // Usage: -// assert_approx(got, expected, ); +// assert_approx(got, expected, [info]); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -394,7 +411,7 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: -// assert_equal(got, expected, ); +// assert_equal(got, expected, [info]); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -421,7 +438,7 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: -// shape_compare() {test_shape(); expected_shape();} +// shape_compare([eps]) {test_shape(); expected_shape();} // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. // Returns the differential geometry if they are not nearly the same shape and size. diff --git a/geometry.scad b/geometry.scad index fff7ebf..eff67bc 100644 --- a/geometry.scad +++ b/geometry.scad @@ -23,25 +23,36 @@ function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) + approx(point,edge[0],eps=eps) + || approx(point,edge[1],eps=eps) // The point is an endpoint + || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the + || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints + && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge + +function point_on_segment2d(point, edge, eps=EPSILON) = + assert( is_vector(point,2), "Invalid point." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], ne = norm(de) ) ( dp*de >= -eps*ne ) - && ( (dp-de)*de <= eps*ne ) // point projects on the segment - && _dist2line(point-edge[0],unit(de)) 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0 - : (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0 ; + edge[0].y <= point.y? ( + (edge[1].y > point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 + ) : ( + (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 + ); //Internal function _valid_line(line,dim,eps=EPSILON) = @@ -87,6 +98,10 @@ function collinear(a, b, c, eps=EPSILON) = : noncollinear_triple(points,error=false,eps=eps)==[]; +//*** valid for any dimension + + + // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -100,7 +115,7 @@ function collinear(a, b, c, eps=EPSILON) = function distance_from_line(line, pt) = assert( _valid_line(line) && is_vector(pt,len(line[0])), "Invalid line, invalid point or incompatible dimensions." ) - _dist2line(pt-line[0],unit(line[1]-line[0])); + _dist(pt-line[0],unit(line[1]-line[0])); // Function: line_normal() @@ -315,6 +330,17 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); +function line_closest_point(line,pt) = + assert(is_path(line)&&len(line)==2) + assert(same_shape(pt,line[0])) + assert(!approx(line[0],line[1])) + let( + seglen = norm(line[1]-line[0]), + segvec = (line[1]-line[0])/seglen, + projection = (pt-line[0]) * segvec + ) + line[0] + projection*segvec; + function line_closest_point(line,pt) = assert(_valid_line(line), "Invalid line." ) assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) @@ -748,10 +774,14 @@ function adj_opp_to_ang(adj,opp) = // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 function triangle_area(a,b,c) = - assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) - len(a)==3 - ? 0.5*norm(cross(c-a,c-b)) - : 0.5*cross(c-a,c-b); + assert( is_path([a,b,c]), + "Invalid points or incompatible dimensions." ) + len(a)==3 ? 0.5*norm(cross(c-a,c-b)) + : ( + a.x * (b.y - c.y) + + b.x * (c.y - a.y) + + c.x * (a.y - b.y) + ) / 2; @@ -821,7 +851,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Function: plane_from_points() // Usage: -// plane_from_points(points, , ); +// plane_from_points(points, [fast], [eps]); // Description: // Given a list of 3 or more coplanar 3D points, returns the coefficients of the cartesian equation of a plane, // that is [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. @@ -846,6 +876,7 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) indices==[] ? undef : let( + indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], @@ -882,6 +913,11 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = ) fast? plane: coplanar(poly,eps=eps)? plane: []; +//*** +// I don't see why this function uses a criterium different from plane_from_points. +// In practical terms, what is the difference of finding a plane from points and from polygon? +// The docs don't clarify. +// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: @@ -933,8 +969,8 @@ function plane_transform(plane) = // Usage: // projection_on_plane(points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal -// projection of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection +// of the points on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1006,6 +1042,23 @@ function closest_point_on_plane(plane, point) = // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. +function _general_plane_line_intersection(plane, line, eps=EPSILON) = + let( + p0 = line[0], + p1 = line[1], + n = plane_normal(plane), + u = p1 - p0, + d = n * u + ) abs(d)); +// coplanar(points,eps); // Description: // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: @@ -1168,7 +1220,7 @@ function coplanar(points, eps=EPSILON) = // Function: points_on_plane() // Usage: -// points_on_plane(points, plane, ); +// points_on_plane(points, plane, eps); // Description: // Returns true if the given 3D points are on the given plane. // Arguments: @@ -1204,7 +1256,7 @@ function in_front_of_plane(plane, point) = // Function: find_circle_2tangents() // Usage: -// find_circle_2tangents(pt1, pt2, pt3, r|d, ); +// find_circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); // Description: // Given a pair of rays with a common origin, and a known circle radius/diameter, finds // the centerpoint for the circle of that size that touches both rays tangentally. @@ -1273,8 +1325,7 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, pt2, pt3); -// find_circle_3points([pt1, pt2, pt3]); +// find_circle_3points(pt1, [pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear // points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). @@ -1294,6 +1345,40 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // translate(circ[0]) color("green") stroke(circle(r=circ[1]),closed=true,$fn=72); // translate(circ[0]) color("red") circle(d=3, $fn=12); // move_copies(pts) color("blue") circle(d=3, $fn=12); +function find_circle_3points(pt1, pt2, pt3) = + (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) + ? find_circle_3points(pt1[0], pt1[1], pt1[2]) + : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) + && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, + "Invalid point(s)." ) + collinear(pt1,pt2,pt3)? [undef,undef,undef] : + let( + v1 = pt1-pt2, + v2 = pt3-pt2, + n = vector_axis(v1,v2), + n2 = n.z<0? -n : n + ) len(pt1)+len(pt2)+len(pt3)>6? ( + let( + a = project_plane(pt1, pt1, pt2, pt3), + b = project_plane(pt2, pt1, pt2, pt3), + c = project_plane(pt3, pt1, pt2, pt3), + res = find_circle_3points(a, b, c) + ) res[0]==undef? [undef,undef,undef] : let( + cp = lift_plane(res[0], pt1, pt2, pt3), + r = norm(pt2-cp) + ) [cp, r, n2] + ) : let( + mp1 = pt2 + v1/2, + mp2 = pt2 + v2/2, + mpv1 = rot(90, v=n, p=v1), + mpv2 = rot(90, v=n, p=v2), + l1 = [mp1, mp1+mpv1], + l2 = [mp2, mp2+mpv2], + isect = line_intersection(l1,l2) + ) is_undef(isect)? [undef,undef,undef] : let( + r = norm(pt2-isect) + ) [isect, r, n2]; + function find_circle_3points(pt1, pt2, pt3) = (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) ? find_circle_3points(pt1[0], pt1[1], pt1[2]) @@ -1319,6 +1404,9 @@ function find_circle_3points(pt1, pt2, pt3) = ) [ cp, r, n ]; + + + // Function: circle_point_tangents() // Usage: @@ -1354,6 +1442,7 @@ function circle_point_tangents(r, d, cp, pt) = ) [for (ang=angs) [ang, cp + r*[cos(ang),sin(ang)]]]; + // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: @@ -1452,14 +1541,13 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [] : let( n = (pb-pa)/nrm, - distlist = [for(i=[0:len(points)-1]) _dist2line(points[i]-pa, n)] + distlist = [for(i=[0:len(points)-1]) _dist(points[i]-pa, n)] ) max(distlist)0) 1])==0 || + len([for (x=c) if(x<0) 1])==0; + function is_convex_polygon(poly) = assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly) ) @@ -1576,15 +1686,15 @@ function polygon_shift(poly, i) = // Usage: // polygon_shift_to_closest_point(path, pt); // Description: -// Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. -function polygon_shift_to_closest_point(poly, pt) = +// Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. +function polygon_shift_to_closest_point(path, pt) = assert(is_vector(pt), "Invalid point." ) - assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) + assert(is_path(path,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) let( - poly = cleanup_path(poly), - dists = [for (p=poly) norm(p-pt)], + path = cleanup_path(path), + dists = [for (p=path) norm(p-pt)], closest = min_index(dists) - ) select(poly,closest,closest+len(poly)-1); + ) select(path,closest,closest+len(path)-1); // Function: reindex_polygon() @@ -1616,6 +1726,33 @@ function polygon_shift_to_closest_point(poly, pt) = // 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) = + 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.") + let( + dim = len(reference[0]), + N = len(reference), + fixpoly = dim != 2? poly : + polygon_is_clockwise(reference)? clockwise_polygon(poly) : + ccw_polygon(poly), + dist = [ + // Matrix of all pairwise distances + for (p1=reference) [ + for (p2=fixpoly) norm(p1-p2) + ] + ], + // Compute the sum of all distance pairs for a each shift + sums = [ + for(shift=[0:1:N-1]) sum([ + for(i=[0:1:N-1]) dist[i][(i+shift)%N] + ]) + ], + optimal_poly = polygon_shift(fixpoly,min_index(sums)) + ) + return_error? [optimal_poly, min(sums)] : + optimal_poly; + function reindex_polygon(reference, poly, return_error=false) = assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) @@ -1637,9 +1774,10 @@ function reindex_polygon(reference, poly, return_error=false) = optimal_poly; + // Function: align_polygon() // Usage: -// newpoly = align_polygon(reference, poly, angles, ); +// newpoly = align_polygon(reference, poly, angles, [cp]); // Description: // Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns // with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation @@ -1681,6 +1819,26 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. +function centroid(poly) = + assert( is_path(poly), "The input must be a 2D or 3D polygon." ) + len(poly[0])==2 + ? sum([ + for(i=[0:len(poly)-1]) + let(segment=select(poly,i,i+1)) + det2(segment)*sum(segment) + ]) / 6 / polygon_area(poly) + : let( plane = plane_from_points(poly, fast=true) ) + assert( !is_undef(plane), "The polygon must be planar." ) + let( + n = plane_normal(plane), + p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), + p2 = vector_axis(n,p1), + cp = mean(poly), + proj = project_plane(poly,cp,cp+p1,cp+p2), + cxy = centroid(proj) + ) + lift_plane(cxy,cp,cp+p1,cp+p2); + function centroid(poly) = assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) len(poly[0])==2 @@ -1708,11 +1866,10 @@ function centroid(poly) = // Function: point_in_polygon() // Usage: -// point_in_polygon(point, poly, ) +// point_in_polygon(point, path, [eps]) // Description: // This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. -// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. +// the specified 2D polygon using the Winding Number method. // The polygon is given as a list of 2D points, not including the repeated end point. // Returns -1 if the point is outside the polyon. // Returns 0 if the point is on the boundary. @@ -1722,81 +1879,75 @@ function centroid(poly) = // Rounding error may give mixed results for points on or near the boundary. // Arguments: // point = The 2D point to check position of. -// poly = The list of 2D path points forming the perimeter of the polygon. -// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) +// path = The list of 2D path points forming the perimeter of the polygon. // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = - // Original algorithms from http://geomalgorithms.com/a03-_inclusion.html - assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>2, +function point_in_polygon(point, path, eps=EPSILON) = + // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. let( - on_brd = [for(i=[0:1:len(poly)-1]) - let( seg = select(poly,i,i+1) ) - if( !approx(seg[0],seg[1],eps=EPSILON) ) + on_brd = [for(i=[0:1:len(path)-1]) + let( seg = select(path,i,i+1) ) + if( !approx(seg[0],seg[1],eps=eps) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) - sum(on_brd) > 0 - ? 0 - : nonzero - ? // Compute winding number and return 1 for interior, -1 for exterior - let( - windchk = [for(i=[0:1:len(poly)-1]) - let(seg=select(poly,i,i+1)) - if(!approx(seg[0],seg[1],eps=eps)) - _point_above_below_segment(point, seg) - ] - ) - sum(windchk) != 0 ? 1 : -1 - : // or compute the crossings with the ray [point, point+[1,0]] - let( - n = len(poly), - cross = - [for(i=[0:n-1]) - let( - p0 = poly[i]-point, - p1 = poly[(i+1)%n]-point - ) - if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) - && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) - 1 - ] - ) - 2*(len(cross)%2)-1;; + sum(on_brd) > 0? 0 : + // Otherwise compute winding number and return 1 for interior, -1 for exterior + let( + windchk = [for(i=[0:1:len(path)-1]) + let(seg=select(path,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1; +//** +// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: -// polygon_is_clockwise(poly); +// polygon_is_clockwise(path); // Description: // Return true if the given 2D simple polygon is in clockwise order, false otherwise. // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: -// poly = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d path") - polygon_area(poly, signed=true)<0; +// path = The list of 2D path points for the perimeter of the polygon. +function polygon_is_clockwise(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + let( + minx = min(subindex(path,0)), + lowind = search(minx, path, 0, 0), + lowpts = select(path, lowind), + miny = min(subindex(lowpts, 1)), + extreme_sub = search(miny, lowpts, 1, 1)[0], + extreme = select(lowind,extreme_sub) + ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; +function polygon_is_clockwise(path) = + assert(is_path(path,dim=2), "Input should be a 2d path") + polygon_area(path, signed=true)<0; // Function: clockwise_polygon() // Usage: -// clockwise_polygon(poly); +// clockwise_polygon(path); // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. -function clockwise_polygon(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d polygon") - polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly); +function clockwise_polygon(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); // Function: ccw_polygon() // Usage: -// ccw_polygon(poly); +// ccw_polygon(path); // Description: -// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. -function ccw_polygon(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d polygon") - polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly; +// Given a 2D polygon path, returns the counter-clockwise winding version of that path. +function ccw_polygon(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; // Function: reverse_polygon() @@ -1818,7 +1969,7 @@ function reverse_polygon(poly) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - poly = cleanup_path(poly), + poly = path3d(cleanup_path(poly)), p0 = poly[0], n = sum([ for (i=[1:1:len(poly)-2]) @@ -1934,6 +2085,17 @@ function split_polygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); +//*** +// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: +// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) +// produces: +// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] +// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] +// and the second polygon is self-intersecting +// besides, it fails in some simple cases as triangles: +// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] +// this last failure may be fatal for vnf_bend + // Function: split_polygons_at_each_y() // Usage: @@ -1944,9 +2106,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = -// assert( is_consistent(polys) && is_path(polys[0],dim=3) , // not all polygons should have the same length!!! - // "The input list should contains only 3D polygons." ) - assert( is_finite(ys) || is_vector(ys), "The split value list should contain only numbers." ) //*** + assert( is_consistent(polys) && is_path(poly[0],dim=3) , + "The input list should contains only 3D polygons." ) + assert( is_finite(ys), "The split value list should contain only numbers." ) _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -1977,4 +2139,5 @@ function split_polygons_at_each_z(polys, zs, _i=0) = ); + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 0950e1c..ceeb905 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -98,8 +98,6 @@ function standardize(v) = v==[]? [] : sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; -module assert_std(vc,ve) { assert(standardize(vc)==standardize(ve)); } - module test_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); @@ -489,47 +487,48 @@ module test_triangle_area() { module test_plane3pt() { - assert_std(plane3pt([0,0,20], [0,10,10], [0,0,0]), [1,0,0,0]); - assert_std(plane3pt([2,0,20], [2,10,10], [2,0,0]), [1,0,0,2]); - assert_std(plane3pt([0,0,0], [10,0,10], [0,0,20]), [0,1,0,0]); - assert_std(plane3pt([0,2,0], [10,2,10], [0,2,20]), [0,1,0,2]); - assert_std(plane3pt([0,0,0], [10,10,0], [20,0,0]), [0,0,1,0]); - assert_std(plane3pt([0,0,2], [10,10,2], [20,0,2]), [0,0,1,2]); + assert(plane3pt([0,0,20], [0,10,10], [0,0,0]) == [1,0,0,0]); + assert(plane3pt([2,0,20], [2,10,10], [2,0,0]) == [1,0,0,2]); + assert(plane3pt([0,0,0], [10,0,10], [0,0,20]) == [0,1,0,0]); + assert(plane3pt([0,2,0], [10,2,10], [0,2,20]) == [0,1,0,2]); + assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); + assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); } *test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; s13 = sqrt(1/3); - assert_std(plane3pt_indexed(pts, 0,3,2), [1,0,0,0]); - assert_std(plane3pt_indexed(pts, 0,2,3), [-1,0,0,0]); - assert_std(plane3pt_indexed(pts, 0,1,3), [0,1,0,0]); - assert_std(plane3pt_indexed(pts, 0,3,1), [0,-1,0,0]); - assert_std(plane3pt_indexed(pts, 0,2,1), [0,0,1,0]); + assert(plane3pt_indexed(pts, 0,3,2) == [1,0,0,0]); + assert(plane3pt_indexed(pts, 0,2,3) == [-1,0,0,0]); + assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); + assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); + assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } *test_plane3pt_indexed(); + module test_plane_from_points() { - assert_std(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]), [1,0,0,0]); - assert_std(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]), [1,0,0,2]); - assert_std(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]), [0,1,0,0]); - assert_std(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]), [0,1,0,2]); - assert_std(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]), [0,0,1,0]); - assert_std(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]), [0,0,1,2]); + assert(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]) == [1,0,0,0]); + assert(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]) == [1,0,0,2]); + assert(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]) == [0,1,0,0]); + assert(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]) == [0,1,0,2]); + assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); + assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); } *test_plane_from_points(); module test_plane_normal() { - assert_std(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); - assert_std(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); - assert_std(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])), [0,1,0]); - assert_std(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])), [0,1,0]); - assert_std(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])), [0,0,1]); - assert_std(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])), [0,0,1]); + assert(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])) == [1,0,0]); + assert(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])) == [1,0,0]); + assert(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])) == [0,1,0]); + assert(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])) == [0,1,0]); + assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); + assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); } *test_plane_normal(); @@ -700,22 +699,16 @@ module test_simplify_path_indexed() { module test_point_in_polygon() { poly = [for (a=[0:30:359]) 10*[cos(a),sin(a)]]; - poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); - assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); - assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); - assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From 99e815f077dcd374d3365bb20ed12791dce06e2b Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 20 Aug 2020 22:36:26 +0100 Subject: [PATCH 51/57] In observance of owner's last review Eliminate double definitions. Eliminate unneeded comments. In common.scad redefine num_defined(), all_defined() and get_radius(). In geometry.scad: - change name _dist to _dist2line - simplify _point_above_below_segment() and triangle_area() - change some arg names for uniformity (path>>poly) - change point_in_polygon() to accept the Even-odd rule as alternative - and other minor edits Update tests_geometry to the new funcionalities. --- common.scad | 67 +++----- geometry.scad | 341 ++++++++++----------------------------- tests/test_geometry.scad | 55 ++++--- 3 files changed, 145 insertions(+), 318 deletions(-) diff --git a/common.scad b/common.scad index 51d8363..db4f3bb 100644 --- a/common.scad +++ b/common.scad @@ -129,11 +129,6 @@ function is_list_of(list,pattern) = is_list(list) && []==[for(entry=0*list) if (entry != pattern) entry]; -function _list_pattern(list) = - is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] - : 0; - - // Function: is_consistent() // Usage: @@ -198,11 +193,11 @@ function first_defined(v,recursive=false,_i=0) = is_undef(first_defined(v[_i],recursive=recursive)) ) )? first_defined(v,recursive=recursive,_i=_i+1) : v[_i]; - + // Function: one_defined() // Usage: -// one_defined(vars, names, [required]) +// one_defined(vars, names, ) // Description: // Examines the input list `vars` and returns the entry which is not `undef`. If more // than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed @@ -221,8 +216,7 @@ function one_defined(vars, names, required=true) = // Function: num_defined() // Description: Counts how many items in list `v` are not `undef`. -function num_defined(v,_i=0,_cnt=0) = _i>=len(v)? _cnt : num_defined(v,_i+1,_cnt+(is_undef(v[_i])? 0 : 1)); - +function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); // Function: any_defined() // Description: @@ -239,8 +233,8 @@ function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) ! // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive&&is_list(x)&&!all_defined(x))? 1 : 0])==0; - +function all_defined(v,recursive=false) = + []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; @@ -249,7 +243,7 @@ function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive // Function: get_anchor() // Usage: -// get_anchor(anchor,center,[uncentered],[dflt]); +// get_anchor(anchor,center,,); // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -270,7 +264,7 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius([r1], [r2], [r], [d1], [d2], [d], [dflt]); +// get_radius(, , , , , , ); // Description: // Given various radii and diameters, returns the most specific radius. // If a diameter is most specific, returns half its value, giving the radius. @@ -288,34 +282,23 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // r = Most general radius. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. -function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1) - ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r1), "Invalid radius r1." ) - r1 - : !is_undef(r2) - ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r2), "Invalid radius r2." ) - r2 - : !is_undef(d1) - ? assert(is_finite(d1), "Invalid diameter d1." ) - d1/2 - : !is_undef(d2) - ? assert(is_finite(d2), "Invalid diameter d2." ) - d2/2 - : !is_undef(r) - ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) - r - : !is_undef(d) - ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) - d/2 - : dflt -); +function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = + assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") + !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 + : !is_undef(r2) ? assert(is_finite(r2), "Invalid radius r2." ) r2 + : !is_undef(d1) ? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 + : !is_undef(d2) ? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) d/2 + : dflt; + // Function: get_height() // Usage: -// get_height([h],[l],[height],[dflt]) +// get_height(,,,) // Description: // Given several different parameters for height check that height is not multiply defined // and return a single value. If the three values `l`, `h`, and `height` are all undefined @@ -332,7 +315,7 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // Function: scalar_vec3() // Usage: -// scalar_vec3(v, [dflt]); +// scalar_vec3(v, ); // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -384,7 +367,7 @@ function _valstr(x) = // Module: assert_approx() // Usage: -// assert_approx(got, expected, [info]); +// assert_approx(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -411,7 +394,7 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: -// assert_equal(got, expected, [info]); +// assert_equal(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -438,7 +421,7 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: -// shape_compare([eps]) {test_shape(); expected_shape();} +// shape_compare() {test_shape(); expected_shape();} // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. // Returns the differential geometry if they are not nearly the same shape and size. diff --git a/geometry.scad b/geometry.scad index eff67bc..dc86187 100644 --- a/geometry.scad +++ b/geometry.scad @@ -23,36 +23,25 @@ function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) - approx(point,edge[0],eps=eps) - || approx(point,edge[1],eps=eps) // The point is an endpoint - || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the - || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints - && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge - -function point_on_segment2d(point, edge, eps=EPSILON) = - assert( is_vector(point,2), "Invalid point." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) + assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], ne = norm(de) ) ( dp*de >= -eps*ne ) - && ( (dp-de)*de <= eps*ne ) // point projects on the segment - && _dist(point-edge[0],unit(de)) point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 - ) : ( - (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 - ); + let( edge = edge - [point, point] ) + edge[0].y <= 0 + ? (edge[1].y > 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0 + : (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0 ; //Internal function _valid_line(line,dim,eps=EPSILON) = @@ -98,10 +87,6 @@ function collinear(a, b, c, eps=EPSILON) = : noncollinear_triple(points,error=false,eps=eps)==[]; -//*** valid for any dimension - - - // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -115,7 +100,7 @@ function collinear(a, b, c, eps=EPSILON) = function distance_from_line(line, pt) = assert( _valid_line(line) && is_vector(pt,len(line[0])), "Invalid line, invalid point or incompatible dimensions." ) - _dist(pt-line[0],unit(line[1]-line[0])); + _dist2line(pt-line[0],unit(line[1]-line[0])); // Function: line_normal() @@ -330,17 +315,6 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); -function line_closest_point(line,pt) = - assert(is_path(line)&&len(line)==2) - assert(same_shape(pt,line[0])) - assert(!approx(line[0],line[1])) - let( - seglen = norm(line[1]-line[0]), - segvec = (line[1]-line[0])/seglen, - projection = (pt-line[0]) * segvec - ) - line[0] + projection*segvec; - function line_closest_point(line,pt) = assert(_valid_line(line), "Invalid line." ) assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) @@ -774,14 +748,10 @@ function adj_opp_to_ang(adj,opp) = // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 function triangle_area(a,b,c) = - assert( is_path([a,b,c]), - "Invalid points or incompatible dimensions." ) - len(a)==3 ? 0.5*norm(cross(c-a,c-b)) - : ( - a.x * (b.y - c.y) + - b.x * (c.y - a.y) + - c.x * (a.y - b.y) - ) / 2; + assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) + len(a)==3 + ? 0.5*norm(cross(c-a,c-b)) + : 0.5*cross(c-a,c-b); @@ -851,7 +821,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Function: plane_from_points() // Usage: -// plane_from_points(points, [fast], [eps]); +// plane_from_points(points, , ); // Description: // Given a list of 3 or more coplanar 3D points, returns the coefficients of the cartesian equation of a plane, // that is [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. @@ -876,7 +846,6 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) indices==[] ? undef : let( - indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], @@ -913,11 +882,6 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = ) fast? plane: coplanar(poly,eps=eps)? plane: []; -//*** -// I don't see why this function uses a criterium different from plane_from_points. -// In practical terms, what is the difference of finding a plane from points and from polygon? -// The docs don't clarify. -// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: @@ -969,8 +933,8 @@ function plane_transform(plane) = // Usage: // projection_on_plane(points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection -// of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal +// projection of the points on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1042,23 +1006,6 @@ function closest_point_on_plane(plane, point) = // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. -function _general_plane_line_intersection(plane, line, eps=EPSILON) = - let( - p0 = line[0], - p1 = line[1], - n = plane_normal(plane), - u = p1 - p0, - d = n * u - ) abs(d)); // Description: // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: @@ -1220,7 +1168,7 @@ function coplanar(points, eps=EPSILON) = // Function: points_on_plane() // Usage: -// points_on_plane(points, plane, eps); +// points_on_plane(points, plane, ); // Description: // Returns true if the given 3D points are on the given plane. // Arguments: @@ -1256,7 +1204,7 @@ function in_front_of_plane(plane, point) = // Function: find_circle_2tangents() // Usage: -// find_circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); +// find_circle_2tangents(pt1, pt2, pt3, r|d, ); // Description: // Given a pair of rays with a common origin, and a known circle radius/diameter, finds // the centerpoint for the circle of that size that touches both rays tangentally. @@ -1325,7 +1273,8 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, [pt2, pt3]); +// find_circle_3points(pt1, pt2, pt3); +// find_circle_3points([pt1, pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear // points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). @@ -1345,40 +1294,6 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // translate(circ[0]) color("green") stroke(circle(r=circ[1]),closed=true,$fn=72); // translate(circ[0]) color("red") circle(d=3, $fn=12); // move_copies(pts) color("blue") circle(d=3, $fn=12); -function find_circle_3points(pt1, pt2, pt3) = - (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) - ? find_circle_3points(pt1[0], pt1[1], pt1[2]) - : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) - && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, - "Invalid point(s)." ) - collinear(pt1,pt2,pt3)? [undef,undef,undef] : - let( - v1 = pt1-pt2, - v2 = pt3-pt2, - n = vector_axis(v1,v2), - n2 = n.z<0? -n : n - ) len(pt1)+len(pt2)+len(pt3)>6? ( - let( - a = project_plane(pt1, pt1, pt2, pt3), - b = project_plane(pt2, pt1, pt2, pt3), - c = project_plane(pt3, pt1, pt2, pt3), - res = find_circle_3points(a, b, c) - ) res[0]==undef? [undef,undef,undef] : let( - cp = lift_plane(res[0], pt1, pt2, pt3), - r = norm(pt2-cp) - ) [cp, r, n2] - ) : let( - mp1 = pt2 + v1/2, - mp2 = pt2 + v2/2, - mpv1 = rot(90, v=n, p=v1), - mpv2 = rot(90, v=n, p=v2), - l1 = [mp1, mp1+mpv1], - l2 = [mp2, mp2+mpv2], - isect = line_intersection(l1,l2) - ) is_undef(isect)? [undef,undef,undef] : let( - r = norm(pt2-isect) - ) [isect, r, n2]; - function find_circle_3points(pt1, pt2, pt3) = (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) ? find_circle_3points(pt1[0], pt1[1], pt1[2]) @@ -1404,9 +1319,6 @@ function find_circle_3points(pt1, pt2, pt3) = ) [ cp, r, n ]; - - - // Function: circle_point_tangents() // Usage: @@ -1442,7 +1354,6 @@ function circle_point_tangents(r, d, cp, pt) = ) [for (ang=angs) [ang, cp + r*[cos(ang),sin(ang)]]]; - // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: @@ -1541,13 +1452,14 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [] : let( n = (pb-pa)/nrm, - distlist = [for(i=[0:len(points)-1]) _dist(points[i]-pa, n)] + distlist = [for(i=[0:len(points)-1]) _dist2line(points[i]-pa, n)] ) max(distlist)0) 1])==0 || - len([for (x=c) if(x<0) 1])==0; - function is_convex_polygon(poly) = assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly) ) @@ -1686,15 +1576,15 @@ function polygon_shift(poly, i) = // Usage: // polygon_shift_to_closest_point(path, pt); // Description: -// Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. -function polygon_shift_to_closest_point(path, pt) = +// Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. +function polygon_shift_to_closest_point(poly, pt) = assert(is_vector(pt), "Invalid point." ) - assert(is_path(path,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) + assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) let( - path = cleanup_path(path), - dists = [for (p=path) norm(p-pt)], + poly = cleanup_path(poly), + dists = [for (p=poly) norm(p-pt)], closest = min_index(dists) - ) select(path,closest,closest+len(path)-1); + ) select(poly,closest,closest+len(poly)-1); // Function: reindex_polygon() @@ -1726,33 +1616,6 @@ function polygon_shift_to_closest_point(path, pt) = // 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) = - 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.") - let( - dim = len(reference[0]), - N = len(reference), - fixpoly = dim != 2? poly : - polygon_is_clockwise(reference)? clockwise_polygon(poly) : - ccw_polygon(poly), - dist = [ - // Matrix of all pairwise distances - for (p1=reference) [ - for (p2=fixpoly) norm(p1-p2) - ] - ], - // Compute the sum of all distance pairs for a each shift - sums = [ - for(shift=[0:1:N-1]) sum([ - for(i=[0:1:N-1]) dist[i][(i+shift)%N] - ]) - ], - optimal_poly = polygon_shift(fixpoly,min_index(sums)) - ) - return_error? [optimal_poly, min(sums)] : - optimal_poly; - function reindex_polygon(reference, poly, return_error=false) = assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) @@ -1774,10 +1637,9 @@ function reindex_polygon(reference, poly, return_error=false) = optimal_poly; - // Function: align_polygon() // Usage: -// newpoly = align_polygon(reference, poly, angles, [cp]); +// newpoly = align_polygon(reference, poly, angles, ); // Description: // Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns // with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation @@ -1819,26 +1681,6 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. -function centroid(poly) = - assert( is_path(poly), "The input must be a 2D or 3D polygon." ) - len(poly[0])==2 - ? sum([ - for(i=[0:len(poly)-1]) - let(segment=select(poly,i,i+1)) - det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) - : let( plane = plane_from_points(poly, fast=true) ) - assert( !is_undef(plane), "The polygon must be planar." ) - let( - n = plane_normal(plane), - p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), - p2 = vector_axis(n,p1), - cp = mean(poly), - proj = project_plane(poly,cp,cp+p1,cp+p2), - cxy = centroid(proj) - ) - lift_plane(cxy,cp,cp+p1,cp+p2); - function centroid(poly) = assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) len(poly[0])==2 @@ -1866,10 +1708,11 @@ function centroid(poly) = // Function: point_in_polygon() // Usage: -// point_in_polygon(point, path, [eps]) +// point_in_polygon(point, poly, ) // Description: // This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using the Winding Number method. +// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. +// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. // The polygon is given as a list of 2D points, not including the repeated end point. // Returns -1 if the point is outside the polyon. // Returns 0 if the point is on the boundary. @@ -1879,75 +1722,81 @@ function centroid(poly) = // Rounding error may give mixed results for points on or near the boundary. // Arguments: // point = The 2D point to check position of. -// path = The list of 2D path points forming the perimeter of the polygon. +// poly = The list of 2D path points forming the perimeter of the polygon. +// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, path, eps=EPSILON) = - // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html - assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, +function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = + // Original algorithms from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. let( - on_brd = [for(i=[0:1:len(path)-1]) - let( seg = select(path,i,i+1) ) - if( !approx(seg[0],seg[1],eps=eps) ) + on_brd = [for(i=[0:1:len(poly)-1]) + let( seg = select(poly,i,i+1) ) + if( !approx(seg[0],seg[1],eps=EPSILON) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) - sum(on_brd) > 0? 0 : - // Otherwise compute winding number and return 1 for interior, -1 for exterior - let( - windchk = [for(i=[0:1:len(path)-1]) - let(seg=select(path,i,i+1)) - if(!approx(seg[0],seg[1],eps=eps)) - _point_above_below_segment(point, seg) - ] - ) - sum(windchk) != 0 ? 1 : -1; + sum(on_brd) > 0 + ? 0 + : nonzero + ? // Compute winding number and return 1 for interior, -1 for exterior + let( + windchk = [for(i=[0:1:len(poly)-1]) + let(seg=select(poly,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1 + : // or compute the crossings with the ray [point, point+[1,0]] + let( + n = len(poly), + cross = + [for(i=[0:n-1]) + let( + p0 = poly[i]-point, + p1 = poly[(i+1)%n]-point + ) + if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) + && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) + 1 + ] + ) + 2*(len(cross)%2)-1;; -//** -// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: -// polygon_is_clockwise(path); +// polygon_is_clockwise(poly); // Description: // Return true if the given 2D simple polygon is in clockwise order, false otherwise. // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: -// path = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - let( - minx = min(subindex(path,0)), - lowind = search(minx, path, 0, 0), - lowpts = select(path, lowind), - miny = min(subindex(lowpts, 1)), - extreme_sub = search(miny, lowpts, 1, 1)[0], - extreme = select(lowind,extreme_sub) - ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; +// poly = The list of 2D path points for the perimeter of the polygon. +function polygon_is_clockwise(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d path") + polygon_area(poly, signed=true)<0; -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d path") - polygon_area(path, signed=true)<0; // Function: clockwise_polygon() // Usage: -// clockwise_polygon(path); +// clockwise_polygon(poly); // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. -function clockwise_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); +function clockwise_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly); // Function: ccw_polygon() // Usage: -// ccw_polygon(path); +// ccw_polygon(poly); // Description: -// Given a 2D polygon path, returns the counter-clockwise winding version of that path. -function ccw_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; +// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. +function ccw_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly; // Function: reverse_polygon() @@ -1969,7 +1818,7 @@ function reverse_polygon(poly) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - poly = path3d(cleanup_path(poly)), + poly = cleanup_path(poly), p0 = poly[0], n = sum([ for (i=[1:1:len(poly)-2]) @@ -2085,17 +1934,6 @@ function split_polygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); -//*** -// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: -// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) -// produces: -// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] -// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] -// and the second polygon is self-intersecting -// besides, it fails in some simple cases as triangles: -// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] -// this last failure may be fatal for vnf_bend - // Function: split_polygons_at_each_y() // Usage: @@ -2106,9 +1944,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = - assert( is_consistent(polys) && is_path(poly[0],dim=3) , - "The input list should contains only 3D polygons." ) - assert( is_finite(ys), "The split value list should contain only numbers." ) +// assert( is_consistent(polys) && is_path(polys[0],dim=3) , // not all polygons should have the same length!!! + // "The input list should contains only 3D polygons." ) + assert( is_finite(ys) || is_vector(ys), "The split value list should contain only numbers." ) //*** _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -2139,5 +1977,4 @@ function split_polygons_at_each_z(polys, zs, _i=0) = ); - // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index ceeb905..0950e1c 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -98,6 +98,8 @@ function standardize(v) = v==[]? [] : sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; +module assert_std(vc,ve) { assert(standardize(vc)==standardize(ve)); } + module test_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); @@ -487,48 +489,47 @@ module test_triangle_area() { module test_plane3pt() { - assert(plane3pt([0,0,20], [0,10,10], [0,0,0]) == [1,0,0,0]); - assert(plane3pt([2,0,20], [2,10,10], [2,0,0]) == [1,0,0,2]); - assert(plane3pt([0,0,0], [10,0,10], [0,0,20]) == [0,1,0,0]); - assert(plane3pt([0,2,0], [10,2,10], [0,2,20]) == [0,1,0,2]); - assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); - assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); + assert_std(plane3pt([0,0,20], [0,10,10], [0,0,0]), [1,0,0,0]); + assert_std(plane3pt([2,0,20], [2,10,10], [2,0,0]), [1,0,0,2]); + assert_std(plane3pt([0,0,0], [10,0,10], [0,0,20]), [0,1,0,0]); + assert_std(plane3pt([0,2,0], [10,2,10], [0,2,20]), [0,1,0,2]); + assert_std(plane3pt([0,0,0], [10,10,0], [20,0,0]), [0,0,1,0]); + assert_std(plane3pt([0,0,2], [10,10,2], [20,0,2]), [0,0,1,2]); } *test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; s13 = sqrt(1/3); - assert(plane3pt_indexed(pts, 0,3,2) == [1,0,0,0]); - assert(plane3pt_indexed(pts, 0,2,3) == [-1,0,0,0]); - assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); - assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); - assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); + assert_std(plane3pt_indexed(pts, 0,3,2), [1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,3), [-1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,1,3), [0,1,0,0]); + assert_std(plane3pt_indexed(pts, 0,3,1), [0,-1,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,1), [0,0,1,0]); assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } *test_plane3pt_indexed(); - module test_plane_from_points() { - assert(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]) == [1,0,0,0]); - assert(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]) == [1,0,0,2]); - assert(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]) == [0,1,0,0]); - assert(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]) == [0,1,0,2]); - assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); - assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); + assert_std(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]), [1,0,0,0]); + assert_std(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]), [1,0,0,2]); + assert_std(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]), [0,1,0,0]); + assert_std(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]), [0,1,0,2]); + assert_std(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]), [0,0,1,0]); + assert_std(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]), [0,0,1,2]); } *test_plane_from_points(); module test_plane_normal() { - assert(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); - assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])), [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])), [0,0,1]); } *test_plane_normal(); @@ -699,16 +700,22 @@ module test_simplify_path_indexed() { module test_point_in_polygon() { poly = [for (a=[0:30:359]) 10*[cos(a),sin(a)]]; + poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); + assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); + assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); + assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From 5462616e1efaaa72e26b4fb906e62e11b1ff82fb Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 20 Aug 2020 22:36:50 +0100 Subject: [PATCH 52/57] Revert "In observance of owner's last review" This reverts commit 99e815f077dcd374d3365bb20ed12791dce06e2b. --- common.scad | 67 +++++--- geometry.scad | 341 +++++++++++++++++++++++++++++---------- tests/test_geometry.scad | 55 +++---- 3 files changed, 318 insertions(+), 145 deletions(-) diff --git a/common.scad b/common.scad index db4f3bb..51d8363 100644 --- a/common.scad +++ b/common.scad @@ -129,6 +129,11 @@ function is_list_of(list,pattern) = is_list(list) && []==[for(entry=0*list) if (entry != pattern) entry]; +function _list_pattern(list) = + is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] + : 0; + + // Function: is_consistent() // Usage: @@ -193,11 +198,11 @@ function first_defined(v,recursive=false,_i=0) = is_undef(first_defined(v[_i],recursive=recursive)) ) )? first_defined(v,recursive=recursive,_i=_i+1) : v[_i]; - + // Function: one_defined() // Usage: -// one_defined(vars, names, ) +// one_defined(vars, names, [required]) // Description: // Examines the input list `vars` and returns the entry which is not `undef`. If more // than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed @@ -216,7 +221,8 @@ function one_defined(vars, names, required=true) = // Function: num_defined() // Description: Counts how many items in list `v` are not `undef`. -function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); +function num_defined(v,_i=0,_cnt=0) = _i>=len(v)? _cnt : num_defined(v,_i+1,_cnt+(is_undef(v[_i])? 0 : 1)); + // Function: any_defined() // Description: @@ -233,8 +239,8 @@ function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) ! // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function all_defined(v,recursive=false) = - []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; +function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive&&is_list(x)&&!all_defined(x))? 1 : 0])==0; + @@ -243,7 +249,7 @@ function all_defined(v,recursive=false) = // Function: get_anchor() // Usage: -// get_anchor(anchor,center,,); +// get_anchor(anchor,center,[uncentered],[dflt]); // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -264,7 +270,7 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius(, , , , , , ); +// get_radius([r1], [r2], [r], [d1], [d2], [d], [dflt]); // Description: // Given various radii and diameters, returns the most specific radius. // If a diameter is most specific, returns half its value, giving the radius. @@ -282,23 +288,34 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // r = Most general radius. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. -function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = - assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") - !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 - : !is_undef(r2) ? assert(is_finite(r2), "Invalid radius r2." ) r2 - : !is_undef(d1) ? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 - : !is_undef(d2) ? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 - : !is_undef(r) - ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) - r - : !is_undef(d) ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) d/2 - : dflt; - +function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( + !is_undef(r1) + ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r1), "Invalid radius r1." ) + r1 + : !is_undef(r2) + ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r2), "Invalid radius r2." ) + r2 + : !is_undef(d1) + ? assert(is_finite(d1), "Invalid diameter d1." ) + d1/2 + : !is_undef(d2) + ? assert(is_finite(d2), "Invalid diameter d2." ) + d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) + ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) + d/2 + : dflt +); // Function: get_height() // Usage: -// get_height(,,,) +// get_height([h],[l],[height],[dflt]) // Description: // Given several different parameters for height check that height is not multiply defined // and return a single value. If the three values `l`, `h`, and `height` are all undefined @@ -315,7 +332,7 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // Function: scalar_vec3() // Usage: -// scalar_vec3(v, ); +// scalar_vec3(v, [dflt]); // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -367,7 +384,7 @@ function _valstr(x) = // Module: assert_approx() // Usage: -// assert_approx(got, expected, ); +// assert_approx(got, expected, [info]); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -394,7 +411,7 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: -// assert_equal(got, expected, ); +// assert_equal(got, expected, [info]); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -421,7 +438,7 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: -// shape_compare() {test_shape(); expected_shape();} +// shape_compare([eps]) {test_shape(); expected_shape();} // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. // Returns the differential geometry if they are not nearly the same shape and size. diff --git a/geometry.scad b/geometry.scad index dc86187..eff67bc 100644 --- a/geometry.scad +++ b/geometry.scad @@ -23,25 +23,36 @@ function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) + approx(point,edge[0],eps=eps) + || approx(point,edge[1],eps=eps) // The point is an endpoint + || sign(edge[0].x-point.x)==sign(point.x-edge[1].x) // point is in between the + || ( sign(edge[0].y-point.y)==sign(point.y-edge[1].y) // edge endpoints + && approx(point_left_of_line2d(point, edge),0,eps=eps) ); // and on the line defined by edge + +function point_on_segment2d(point, edge, eps=EPSILON) = + assert( is_vector(point,2), "Invalid point." ) + assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( _valid_line(edge,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], ne = norm(de) ) ( dp*de >= -eps*ne ) - && ( (dp-de)*de <= eps*ne ) // point projects on the segment - && _dist2line(point-edge[0],unit(de)) 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0 - : (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0 ; + edge[0].y <= point.y? ( + (edge[1].y > point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 + ) : ( + (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 + ); //Internal function _valid_line(line,dim,eps=EPSILON) = @@ -87,6 +98,10 @@ function collinear(a, b, c, eps=EPSILON) = : noncollinear_triple(points,error=false,eps=eps)==[]; +//*** valid for any dimension + + + // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -100,7 +115,7 @@ function collinear(a, b, c, eps=EPSILON) = function distance_from_line(line, pt) = assert( _valid_line(line) && is_vector(pt,len(line[0])), "Invalid line, invalid point or incompatible dimensions." ) - _dist2line(pt-line[0],unit(line[1]-line[0])); + _dist(pt-line[0],unit(line[1]-line[0])); // Function: line_normal() @@ -315,6 +330,17 @@ function segment_intersection(s1,s2,eps=EPSILON) = // stroke(line, endcaps="arrow2"); // color("blue") translate(pt) sphere(r=1,$fn=12); // color("red") translate(p2) sphere(r=1,$fn=12); +function line_closest_point(line,pt) = + assert(is_path(line)&&len(line)==2) + assert(same_shape(pt,line[0])) + assert(!approx(line[0],line[1])) + let( + seglen = norm(line[1]-line[0]), + segvec = (line[1]-line[0])/seglen, + projection = (pt-line[0]) * segvec + ) + line[0] + projection*segvec; + function line_closest_point(line,pt) = assert(_valid_line(line), "Invalid line." ) assert( is_vector(pt,len(line[0])), "Invalid point or incompatible dimensions." ) @@ -748,10 +774,14 @@ function adj_opp_to_ang(adj,opp) = // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 function triangle_area(a,b,c) = - assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) - len(a)==3 - ? 0.5*norm(cross(c-a,c-b)) - : 0.5*cross(c-a,c-b); + assert( is_path([a,b,c]), + "Invalid points or incompatible dimensions." ) + len(a)==3 ? 0.5*norm(cross(c-a,c-b)) + : ( + a.x * (b.y - c.y) + + b.x * (c.y - a.y) + + c.x * (a.y - b.y) + ) / 2; @@ -821,7 +851,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Function: plane_from_points() // Usage: -// plane_from_points(points, , ); +// plane_from_points(points, [fast], [eps]); // Description: // Given a list of 3 or more coplanar 3D points, returns the coefficients of the cartesian equation of a plane, // that is [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. @@ -846,6 +876,7 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) indices==[] ? undef : let( + indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], @@ -882,6 +913,11 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = ) fast? plane: coplanar(poly,eps=eps)? plane: []; +//*** +// I don't see why this function uses a criterium different from plane_from_points. +// In practical terms, what is the difference of finding a plane from points and from polygon? +// The docs don't clarify. +// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: @@ -933,8 +969,8 @@ function plane_transform(plane) = // Usage: // projection_on_plane(points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal -// projection of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection +// of the points on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1006,6 +1042,23 @@ function closest_point_on_plane(plane, point) = // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. +function _general_plane_line_intersection(plane, line, eps=EPSILON) = + let( + p0 = line[0], + p1 = line[1], + n = plane_normal(plane), + u = p1 - p0, + d = n * u + ) abs(d)); +// coplanar(points,eps); // Description: // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: @@ -1168,7 +1220,7 @@ function coplanar(points, eps=EPSILON) = // Function: points_on_plane() // Usage: -// points_on_plane(points, plane, ); +// points_on_plane(points, plane, eps); // Description: // Returns true if the given 3D points are on the given plane. // Arguments: @@ -1204,7 +1256,7 @@ function in_front_of_plane(plane, point) = // Function: find_circle_2tangents() // Usage: -// find_circle_2tangents(pt1, pt2, pt3, r|d, ); +// find_circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); // Description: // Given a pair of rays with a common origin, and a known circle radius/diameter, finds // the centerpoint for the circle of that size that touches both rays tangentally. @@ -1273,8 +1325,7 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, pt2, pt3); -// find_circle_3points([pt1, pt2, pt3]); +// find_circle_3points(pt1, [pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear // points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). @@ -1294,6 +1345,40 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // translate(circ[0]) color("green") stroke(circle(r=circ[1]),closed=true,$fn=72); // translate(circ[0]) color("red") circle(d=3, $fn=12); // move_copies(pts) color("blue") circle(d=3, $fn=12); +function find_circle_3points(pt1, pt2, pt3) = + (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) + ? find_circle_3points(pt1[0], pt1[1], pt1[2]) + : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) + && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, + "Invalid point(s)." ) + collinear(pt1,pt2,pt3)? [undef,undef,undef] : + let( + v1 = pt1-pt2, + v2 = pt3-pt2, + n = vector_axis(v1,v2), + n2 = n.z<0? -n : n + ) len(pt1)+len(pt2)+len(pt3)>6? ( + let( + a = project_plane(pt1, pt1, pt2, pt3), + b = project_plane(pt2, pt1, pt2, pt3), + c = project_plane(pt3, pt1, pt2, pt3), + res = find_circle_3points(a, b, c) + ) res[0]==undef? [undef,undef,undef] : let( + cp = lift_plane(res[0], pt1, pt2, pt3), + r = norm(pt2-cp) + ) [cp, r, n2] + ) : let( + mp1 = pt2 + v1/2, + mp2 = pt2 + v2/2, + mpv1 = rot(90, v=n, p=v1), + mpv2 = rot(90, v=n, p=v2), + l1 = [mp1, mp1+mpv1], + l2 = [mp2, mp2+mpv2], + isect = line_intersection(l1,l2) + ) is_undef(isect)? [undef,undef,undef] : let( + r = norm(pt2-isect) + ) [isect, r, n2]; + function find_circle_3points(pt1, pt2, pt3) = (is_undef(pt2) && is_undef(pt3) && is_list(pt1)) ? find_circle_3points(pt1[0], pt1[1], pt1[2]) @@ -1319,6 +1404,9 @@ function find_circle_3points(pt1, pt2, pt3) = ) [ cp, r, n ]; + + + // Function: circle_point_tangents() // Usage: @@ -1354,6 +1442,7 @@ function circle_point_tangents(r, d, cp, pt) = ) [for (ang=angs) [ang, cp + r*[cos(ang),sin(ang)]]]; + // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: @@ -1452,14 +1541,13 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [] : let( n = (pb-pa)/nrm, - distlist = [for(i=[0:len(points)-1]) _dist2line(points[i]-pa, n)] + distlist = [for(i=[0:len(points)-1]) _dist(points[i]-pa, n)] ) max(distlist)0) 1])==0 || + len([for (x=c) if(x<0) 1])==0; + function is_convex_polygon(poly) = assert(is_path(poly,dim=2), "The input should be a 2D polygon." ) let( l = len(poly) ) @@ -1576,15 +1686,15 @@ function polygon_shift(poly, i) = // Usage: // polygon_shift_to_closest_point(path, pt); // Description: -// Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. -function polygon_shift_to_closest_point(poly, pt) = +// Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. +function polygon_shift_to_closest_point(path, pt) = assert(is_vector(pt), "Invalid point." ) - assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) + assert(is_path(path,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) let( - poly = cleanup_path(poly), - dists = [for (p=poly) norm(p-pt)], + path = cleanup_path(path), + dists = [for (p=path) norm(p-pt)], closest = min_index(dists) - ) select(poly,closest,closest+len(poly)-1); + ) select(path,closest,closest+len(path)-1); // Function: reindex_polygon() @@ -1616,6 +1726,33 @@ function polygon_shift_to_closest_point(poly, pt) = // 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) = + 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.") + let( + dim = len(reference[0]), + N = len(reference), + fixpoly = dim != 2? poly : + polygon_is_clockwise(reference)? clockwise_polygon(poly) : + ccw_polygon(poly), + dist = [ + // Matrix of all pairwise distances + for (p1=reference) [ + for (p2=fixpoly) norm(p1-p2) + ] + ], + // Compute the sum of all distance pairs for a each shift + sums = [ + for(shift=[0:1:N-1]) sum([ + for(i=[0:1:N-1]) dist[i][(i+shift)%N] + ]) + ], + optimal_poly = polygon_shift(fixpoly,min_index(sums)) + ) + return_error? [optimal_poly, min(sums)] : + optimal_poly; + function reindex_polygon(reference, poly, return_error=false) = assert(is_path(reference) && is_path(poly,dim=len(reference[0])), "Invalid polygon(s) or incompatible dimensions. " ) @@ -1637,9 +1774,10 @@ function reindex_polygon(reference, poly, return_error=false) = optimal_poly; + // Function: align_polygon() // Usage: -// newpoly = align_polygon(reference, poly, angles, ); +// newpoly = align_polygon(reference, poly, angles, [cp]); // Description: // Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns // with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation @@ -1681,6 +1819,26 @@ function align_polygon(reference, poly, angles, cp) = // Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. // Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. // If the polygon is self-intersecting, the results are undefined. +function centroid(poly) = + assert( is_path(poly), "The input must be a 2D or 3D polygon." ) + len(poly[0])==2 + ? sum([ + for(i=[0:len(poly)-1]) + let(segment=select(poly,i,i+1)) + det2(segment)*sum(segment) + ]) / 6 / polygon_area(poly) + : let( plane = plane_from_points(poly, fast=true) ) + assert( !is_undef(plane), "The polygon must be planar." ) + let( + n = plane_normal(plane), + p1 = vector_angle(n,UP)>15? vector_axis(n,UP) : vector_axis(n,RIGHT), + p2 = vector_axis(n,p1), + cp = mean(poly), + proj = project_plane(poly,cp,cp+p1,cp+p2), + cxy = centroid(proj) + ) + lift_plane(cxy,cp,cp+p1,cp+p2); + function centroid(poly) = assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) len(poly[0])==2 @@ -1708,11 +1866,10 @@ function centroid(poly) = // Function: point_in_polygon() // Usage: -// point_in_polygon(point, poly, ) +// point_in_polygon(point, path, [eps]) // Description: // This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. -// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. +// the specified 2D polygon using the Winding Number method. // The polygon is given as a list of 2D points, not including the repeated end point. // Returns -1 if the point is outside the polyon. // Returns 0 if the point is on the boundary. @@ -1722,81 +1879,75 @@ function centroid(poly) = // Rounding error may give mixed results for points on or near the boundary. // Arguments: // point = The 2D point to check position of. -// poly = The list of 2D path points forming the perimeter of the polygon. -// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) +// path = The list of 2D path points forming the perimeter of the polygon. // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = - // Original algorithms from http://geomalgorithms.com/a03-_inclusion.html - assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>2, +function point_in_polygon(point, path, eps=EPSILON) = + // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. let( - on_brd = [for(i=[0:1:len(poly)-1]) - let( seg = select(poly,i,i+1) ) - if( !approx(seg[0],seg[1],eps=EPSILON) ) + on_brd = [for(i=[0:1:len(path)-1]) + let( seg = select(path,i,i+1) ) + if( !approx(seg[0],seg[1],eps=eps) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) - sum(on_brd) > 0 - ? 0 - : nonzero - ? // Compute winding number and return 1 for interior, -1 for exterior - let( - windchk = [for(i=[0:1:len(poly)-1]) - let(seg=select(poly,i,i+1)) - if(!approx(seg[0],seg[1],eps=eps)) - _point_above_below_segment(point, seg) - ] - ) - sum(windchk) != 0 ? 1 : -1 - : // or compute the crossings with the ray [point, point+[1,0]] - let( - n = len(poly), - cross = - [for(i=[0:n-1]) - let( - p0 = poly[i]-point, - p1 = poly[(i+1)%n]-point - ) - if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) - && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) - 1 - ] - ) - 2*(len(cross)%2)-1;; + sum(on_brd) > 0? 0 : + // Otherwise compute winding number and return 1 for interior, -1 for exterior + let( + windchk = [for(i=[0:1:len(path)-1]) + let(seg=select(path,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1; +//** +// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: -// polygon_is_clockwise(poly); +// polygon_is_clockwise(path); // Description: // Return true if the given 2D simple polygon is in clockwise order, false otherwise. // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: -// poly = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d path") - polygon_area(poly, signed=true)<0; +// path = The list of 2D path points for the perimeter of the polygon. +function polygon_is_clockwise(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + let( + minx = min(subindex(path,0)), + lowind = search(minx, path, 0, 0), + lowpts = select(path, lowind), + miny = min(subindex(lowpts, 1)), + extreme_sub = search(miny, lowpts, 1, 1)[0], + extreme = select(lowind,extreme_sub) + ) det2([select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0; +function polygon_is_clockwise(path) = + assert(is_path(path,dim=2), "Input should be a 2d path") + polygon_area(path, signed=true)<0; // Function: clockwise_polygon() // Usage: -// clockwise_polygon(poly); +// clockwise_polygon(path); // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. -function clockwise_polygon(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d polygon") - polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly); +function clockwise_polygon(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); // Function: ccw_polygon() // Usage: -// ccw_polygon(poly); +// ccw_polygon(path); // Description: -// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. -function ccw_polygon(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d polygon") - polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly; +// Given a 2D polygon path, returns the counter-clockwise winding version of that path. +function ccw_polygon(path) = + assert(is_path(path,dim=2), "Input should be a 2d polygon") + polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; // Function: reverse_polygon() @@ -1818,7 +1969,7 @@ function reverse_polygon(poly) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - poly = cleanup_path(poly), + poly = path3d(cleanup_path(poly)), p0 = poly[0], n = sum([ for (i=[1:1:len(poly)-2]) @@ -1934,6 +2085,17 @@ function split_polygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); +//*** +// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: +// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) +// produces: +// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] +// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] +// and the second polygon is self-intersecting +// besides, it fails in some simple cases as triangles: +// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] +// this last failure may be fatal for vnf_bend + // Function: split_polygons_at_each_y() // Usage: @@ -1944,9 +2106,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = -// assert( is_consistent(polys) && is_path(polys[0],dim=3) , // not all polygons should have the same length!!! - // "The input list should contains only 3D polygons." ) - assert( is_finite(ys) || is_vector(ys), "The split value list should contain only numbers." ) //*** + assert( is_consistent(polys) && is_path(poly[0],dim=3) , + "The input list should contains only 3D polygons." ) + assert( is_finite(ys), "The split value list should contain only numbers." ) _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -1977,4 +2139,5 @@ function split_polygons_at_each_z(polys, zs, _i=0) = ); + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 0950e1c..ceeb905 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -98,8 +98,6 @@ function standardize(v) = v==[]? [] : sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; -module assert_std(vc,ve) { assert(standardize(vc)==standardize(ve)); } - module test_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); @@ -489,47 +487,48 @@ module test_triangle_area() { module test_plane3pt() { - assert_std(plane3pt([0,0,20], [0,10,10], [0,0,0]), [1,0,0,0]); - assert_std(plane3pt([2,0,20], [2,10,10], [2,0,0]), [1,0,0,2]); - assert_std(plane3pt([0,0,0], [10,0,10], [0,0,20]), [0,1,0,0]); - assert_std(plane3pt([0,2,0], [10,2,10], [0,2,20]), [0,1,0,2]); - assert_std(plane3pt([0,0,0], [10,10,0], [20,0,0]), [0,0,1,0]); - assert_std(plane3pt([0,0,2], [10,10,2], [20,0,2]), [0,0,1,2]); + assert(plane3pt([0,0,20], [0,10,10], [0,0,0]) == [1,0,0,0]); + assert(plane3pt([2,0,20], [2,10,10], [2,0,0]) == [1,0,0,2]); + assert(plane3pt([0,0,0], [10,0,10], [0,0,20]) == [0,1,0,0]); + assert(plane3pt([0,2,0], [10,2,10], [0,2,20]) == [0,1,0,2]); + assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); + assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); } *test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; s13 = sqrt(1/3); - assert_std(plane3pt_indexed(pts, 0,3,2), [1,0,0,0]); - assert_std(plane3pt_indexed(pts, 0,2,3), [-1,0,0,0]); - assert_std(plane3pt_indexed(pts, 0,1,3), [0,1,0,0]); - assert_std(plane3pt_indexed(pts, 0,3,1), [0,-1,0,0]); - assert_std(plane3pt_indexed(pts, 0,2,1), [0,0,1,0]); + assert(plane3pt_indexed(pts, 0,3,2) == [1,0,0,0]); + assert(plane3pt_indexed(pts, 0,2,3) == [-1,0,0,0]); + assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); + assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); + assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } *test_plane3pt_indexed(); + module test_plane_from_points() { - assert_std(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]), [1,0,0,0]); - assert_std(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]), [1,0,0,2]); - assert_std(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]), [0,1,0,0]); - assert_std(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]), [0,1,0,2]); - assert_std(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]), [0,0,1,0]); - assert_std(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]), [0,0,1,2]); + assert(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]) == [1,0,0,0]); + assert(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]) == [1,0,0,2]); + assert(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]) == [0,1,0,0]); + assert(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]) == [0,1,0,2]); + assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); + assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); } *test_plane_from_points(); module test_plane_normal() { - assert_std(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); - assert_std(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); - assert_std(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])), [0,1,0]); - assert_std(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])), [0,1,0]); - assert_std(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])), [0,0,1]); - assert_std(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])), [0,0,1]); + assert(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])) == [1,0,0]); + assert(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])) == [1,0,0]); + assert(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])) == [0,1,0]); + assert(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])) == [0,1,0]); + assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); + assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); } *test_plane_normal(); @@ -700,22 +699,16 @@ module test_simplify_path_indexed() { module test_point_in_polygon() { poly = [for (a=[0:30:359]) 10*[cos(a),sin(a)]]; - poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); - assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); - assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); - assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From da5546cbc2720c91e8c864eb80c9f2b671ac5117 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 20 Aug 2020 22:42:24 +0100 Subject: [PATCH 53/57] In observance of owner's last review Eliminate double definitions. Eliminate unneeded comments. In common.scad redefine num_defined(), all_defined() and get_radius(). In geometry.scad: - change name _dist to _dist2line - simplify _point_above_below_segment() and triangle_area() - change some arg names for uniformity (path>>poly) - change point_in_polygon() to accept the Even-odd rule as alternative - and other minor edits Update tests_geometry to the new funcionalities. --- common.scad | 67 ++++++-------- geometry.scad | 184 +++++++++++++++++++-------------------- tests/test_geometry.scad | 55 +++++++----- 3 files changed, 146 insertions(+), 160 deletions(-) diff --git a/common.scad b/common.scad index 51d8363..db4f3bb 100644 --- a/common.scad +++ b/common.scad @@ -129,11 +129,6 @@ function is_list_of(list,pattern) = is_list(list) && []==[for(entry=0*list) if (entry != pattern) entry]; -function _list_pattern(list) = - is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0] - : 0; - - // Function: is_consistent() // Usage: @@ -198,11 +193,11 @@ function first_defined(v,recursive=false,_i=0) = is_undef(first_defined(v[_i],recursive=recursive)) ) )? first_defined(v,recursive=recursive,_i=_i+1) : v[_i]; - + // Function: one_defined() // Usage: -// one_defined(vars, names, [required]) +// one_defined(vars, names, ) // Description: // Examines the input list `vars` and returns the entry which is not `undef`. If more // than one entry is `undef` then issues an assertion specifying "Must define exactly one of" followed @@ -221,8 +216,7 @@ function one_defined(vars, names, required=true) = // Function: num_defined() // Description: Counts how many items in list `v` are not `undef`. -function num_defined(v,_i=0,_cnt=0) = _i>=len(v)? _cnt : num_defined(v,_i+1,_cnt+(is_undef(v[_i])? 0 : 1)); - +function num_defined(v) = len([for(vi=v) if(!is_undef(vi)) 1]); // Function: any_defined() // Description: @@ -239,8 +233,8 @@ function any_defined(v,recursive=false) = first_defined(v,recursive=recursive) ! // Arguments: // v = The list whose items are being checked. // recursive = If true, any sublists are evaluated recursively. -function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive&&is_list(x)&&!all_defined(x))? 1 : 0])==0; - +function all_defined(v,recursive=false) = + []==[for (x=v) if(is_undef(x)||(recursive && is_list(x) && !all_defined(x,recursive))) 0 ]; @@ -249,7 +243,7 @@ function all_defined(v,recursive=false) = max([for (x=v) is_undef(x)||(recursive // Function: get_anchor() // Usage: -// get_anchor(anchor,center,[uncentered],[dflt]); +// get_anchor(anchor,center,,); // Description: // Calculated the correct anchor from `anchor` and `center`. In order: // - If `center` is not `undef` and `center` evaluates as true, then `CENTER` (`[0,0,0]`) is returned. @@ -270,7 +264,7 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // Function: get_radius() // Usage: -// get_radius([r1], [r2], [r], [d1], [d2], [d], [dflt]); +// get_radius(, , , , , , ); // Description: // Given various radii and diameters, returns the most specific radius. // If a diameter is most specific, returns half its value, giving the radius. @@ -288,34 +282,23 @@ function get_anchor(anchor,center,uncentered=BOT,dflt=CENTER) = // r = Most general radius. // d = Most general diameter. // dflt = Value to return if all other values given are `undef`. -function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = ( - !is_undef(r1) - ? assert(is_undef(r2)&&is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r1), "Invalid radius r1." ) - r1 - : !is_undef(r2) - ? assert(is_undef(d1)&&is_undef(d2), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r2), "Invalid radius r2." ) - r2 - : !is_undef(d1) - ? assert(is_finite(d1), "Invalid diameter d1." ) - d1/2 - : !is_undef(d2) - ? assert(is_finite(d2), "Invalid diameter d2." ) - d2/2 - : !is_undef(r) - ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") - assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) - r - : !is_undef(d) - ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) - d/2 - : dflt -); +function get_radius(r1=undef, r2=undef, r=undef, d1=undef, d2=undef, d=undef, dflt=undef) = + assert(num_defined([r1,d1,r2,d2])<2, "Conflicting or redundant radius/diameter arguments given.") + !is_undef(r1) ? assert(is_finite(r1), "Invalid radius r1." ) r1 + : !is_undef(r2) ? assert(is_finite(r2), "Invalid radius r2." ) r2 + : !is_undef(d1) ? assert(is_finite(d1), "Invalid diameter d1." ) d1/2 + : !is_undef(d2) ? assert(is_finite(d2), "Invalid diameter d2." ) d2/2 + : !is_undef(r) + ? assert(is_undef(d), "Conflicting or redundant radius/diameter arguments given.") + assert(is_finite(r) || is_vector(r,1) || is_vector(r,2), "Invalid radius r." ) + r + : !is_undef(d) ? assert(is_finite(d) || is_vector(d,1) || is_vector(d,2), "Invalid diameter d." ) d/2 + : dflt; + // Function: get_height() // Usage: -// get_height([h],[l],[height],[dflt]) +// get_height(,,,) // Description: // Given several different parameters for height check that height is not multiply defined // and return a single value. If the three values `l`, `h`, and `height` are all undefined @@ -332,7 +315,7 @@ function get_height(h=undef,l=undef,height=undef,dflt=undef) = // Function: scalar_vec3() // Usage: -// scalar_vec3(v, [dflt]); +// scalar_vec3(v, ); // Description: // If `v` is a scalar, and `dflt==undef`, returns `[v, v, v]`. // If `v` is a scalar, and `dflt!=undef`, returns `[v, dflt, dflt]`. @@ -384,7 +367,7 @@ function _valstr(x) = // Module: assert_approx() // Usage: -// assert_approx(got, expected, [info]); +// assert_approx(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -411,7 +394,7 @@ module assert_approx(got, expected, info) { // Module: assert_equal() // Usage: -// assert_equal(got, expected, [info]); +// assert_equal(got, expected, ); // Description: // Tests if the value gotten is what was expected. If not, then // the expected and received values are printed to the console and @@ -438,7 +421,7 @@ module assert_equal(got, expected, info) { // Module: shape_compare() // Usage: -// shape_compare([eps]) {test_shape(); expected_shape();} +// shape_compare() {test_shape(); expected_shape();} // Description: // Compares two child shapes, returning empty geometry if they are very nearly the same shape and size. // Returns the differential geometry if they are not nearly the same shape and size. diff --git a/geometry.scad b/geometry.scad index 7d02252..dc86187 100644 --- a/geometry.scad +++ b/geometry.scad @@ -23,26 +23,25 @@ function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert( _valid_line(edge,eps=eps), "Invalid segment." ) + assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], ne = norm(de) ) ( dp*de >= -eps*ne ) - && ( (dp-de)*de <= eps*ne ) // point projects on the segment - && _dist(point-edge[0],unit(de)) point.y && point_left_of_line2d(point, edge) > 0)? 1 : 0 - ) : ( - (edge[1].y <= point.y && point_left_of_line2d(point, edge) < 0)? -1 : 0 - ); + let( edge = edge - [point, point] ) + edge[0].y <= 0 + ? (edge[1].y > 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0 + : (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0 ; //Internal function _valid_line(line,dim,eps=EPSILON) = @@ -101,7 +100,7 @@ function collinear(a, b, c, eps=EPSILON) = function distance_from_line(line, pt) = assert( _valid_line(line) && is_vector(pt,len(line[0])), "Invalid line, invalid point or incompatible dimensions." ) - _dist(pt-line[0],unit(line[1]-line[0])); + _dist2line(pt-line[0],unit(line[1]-line[0])); // Function: line_normal() @@ -749,14 +748,10 @@ function adj_opp_to_ang(adj,opp) = // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 function triangle_area(a,b,c) = - assert( is_path([a,b,c]), - "Invalid points or incompatible dimensions." ) - len(a)==3 ? 0.5*norm(cross(c-a,c-b)) - : ( - a.x * (b.y - c.y) + - b.x * (c.y - a.y) + - c.x * (a.y - b.y) - ) / 2; + assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) + len(a)==3 + ? 0.5*norm(cross(c-a,c-b)) + : 0.5*cross(c-a,c-b); @@ -826,7 +821,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Function: plane_from_points() // Usage: -// plane_from_points(points, [fast], [eps]); +// plane_from_points(points, , ); // Description: // Given a list of 3 or more coplanar 3D points, returns the coefficients of the cartesian equation of a plane, // that is [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. @@ -851,7 +846,6 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) indices==[] ? undef : let( - indices = sort(indices), // why sorting? p1 = points[indices[0]], p2 = points[indices[1]], p3 = points[indices[2]], @@ -888,11 +882,6 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = ) fast? plane: coplanar(poly,eps=eps)? plane: []; -//*** -// I don't see why this function uses a criterium different from plane_from_points. -// In practical terms, what is the difference of finding a plane from points and from polygon? -// The docs don't clarify. -// These functions should be consistent if they are both necessary. The docs might reflect their distinction. // Function: plane_normal() // Usage: @@ -944,8 +933,8 @@ function plane_transform(plane) = // Usage: // projection_on_plane(points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the projection -// of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal +// projection of the points on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1024,6 +1013,7 @@ function _general_plane_line_intersection(plane, line, eps=EPSILON) = ? points_on_plane(line[0],plane,eps)? [line,undef]: undef : [ line[0]+a/b*(line[1]-line[0]), a/b ]; + // Function: plane_line_angle() // Usage: plane_line_angle(plane,line) // Description: @@ -1095,7 +1085,7 @@ function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = ) indices==[] ? undef : let( - indices = sort(indices), // why sorting? + indices = sort(indices), p1 = poly[indices[0]], p2 = poly[indices[1]], p3 = poly[indices[2]], @@ -1159,7 +1149,7 @@ function plane_intersection(plane1,plane2,plane3) = // Function: coplanar() // Usage: -// coplanar(points,eps); +// coplanar(points,); // Description: // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: @@ -1178,7 +1168,7 @@ function coplanar(points, eps=EPSILON) = // Function: points_on_plane() // Usage: -// points_on_plane(points, plane, eps); +// points_on_plane(points, plane, ); // Description: // Returns true if the given 3D points are on the given plane. // Arguments: @@ -1214,7 +1204,7 @@ function in_front_of_plane(plane, point) = // Function: find_circle_2tangents() // Usage: -// find_circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); +// find_circle_2tangents(pt1, pt2, pt3, r|d, ); // Description: // Given a pair of rays with a common origin, and a known circle radius/diameter, finds // the centerpoint for the circle of that size that touches both rays tangentally. @@ -1283,7 +1273,8 @@ function find_circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = // Function: find_circle_3points() // Usage: -// find_circle_3points(pt1, [pt2, pt3]); +// find_circle_3points(pt1, pt2, pt3); +// find_circle_3points([pt1, pt2, pt3]); // Description: // Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear // points where NORMAL is the normal vector of the plane that the circle is on (UP or DOWN if the points are 2D). @@ -1327,7 +1318,7 @@ function find_circle_3points(pt1, pt2, pt3) = r = norm(sc-v[0]) ) [ cp, r, n ]; - + // Function: circle_point_tangents() // Usage: @@ -1363,7 +1354,6 @@ function circle_point_tangents(r, d, cp, pt) = ) [for (ang=angs) [ang, cp + r*[cos(ang),sin(ang)]]]; - // Function: circle_circle_tangents() // Usage: circle_circle_tangents(c1, r1|d1, c2, r2|d2) // Description: @@ -1462,13 +1452,14 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [] : let( n = (pb-pa)/nrm, - distlist = [for(i=[0:len(points)-1]) _dist(points[i]-pa, n)] + distlist = [for(i=[0:len(points)-1]) _dist2line(points[i]-pa, n)] ) max(distlist)); // Description: // Tries the list or range of angles to find a rotation of the specified 2D polygon that best aligns // with the reference 2D polygon. For each angle, the polygon is reindexed, which is a costly operation @@ -1717,10 +1708,11 @@ function centroid(poly) = // Function: point_in_polygon() // Usage: -// point_in_polygon(point, path, [eps]) +// point_in_polygon(point, poly, ) // Description: // This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using the Winding Number method. +// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. +// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. // The polygon is given as a list of 2D points, not including the repeated end point. // Returns -1 if the point is outside the polyon. // Returns 0 if the point is on the boundary. @@ -1730,65 +1722,81 @@ function centroid(poly) = // Rounding error may give mixed results for points on or near the boundary. // Arguments: // point = The 2D point to check position of. -// path = The list of 2D path points forming the perimeter of the polygon. +// poly = The list of 2D path points forming the perimeter of the polygon. +// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) // eps = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, path, eps=EPSILON) = - // Original algorithm from http://geomalgorithms.com/a03-_inclusion.html - assert( is_vector(point,2) && is_path(path,dim=2) && len(path)>2, +function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = + // Original algorithms from http://geomalgorithms.com/a03-_inclusion.html + assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) assert( is_finite(eps) && eps>=0, "Invalid tolerance." ) // Does the point lie on any edges? If so return 0. let( - on_brd = [for(i=[0:1:len(path)-1]) - let( seg = select(path,i,i+1) ) - if( !approx(seg[0],seg[1],eps=eps) ) + on_brd = [for(i=[0:1:len(poly)-1]) + let( seg = select(poly,i,i+1) ) + if( !approx(seg[0],seg[1],eps=EPSILON) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) - sum(on_brd) > 0? 0 : - // Otherwise compute winding number and return 1 for interior, -1 for exterior - let( - windchk = [for(i=[0:1:len(path)-1]) - let(seg=select(path,i,i+1)) - if(!approx(seg[0],seg[1],eps=eps)) - _point_above_below_segment(point, seg) - ] - ) - sum(windchk) != 0 ? 1 : -1; + sum(on_brd) > 0 + ? 0 + : nonzero + ? // Compute winding number and return 1 for interior, -1 for exterior + let( + windchk = [for(i=[0:1:len(poly)-1]) + let(seg=select(poly,i,i+1)) + if(!approx(seg[0],seg[1],eps=eps)) + _point_above_below_segment(point, seg) + ] + ) + sum(windchk) != 0 ? 1 : -1 + : // or compute the crossings with the ray [point, point+[1,0]] + let( + n = len(poly), + cross = + [for(i=[0:n-1]) + let( + p0 = poly[i]-point, + p1 = poly[(i+1)%n]-point + ) + if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) + && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) + 1 + ] + ) + 2*(len(cross)%2)-1;; -//** -// this function should be optimized avoiding the call of other functions // Function: polygon_is_clockwise() // Usage: -// polygon_is_clockwise(path); +// polygon_is_clockwise(poly); // Description: // Return true if the given 2D simple polygon is in clockwise order, false otherwise. // Results for complex (self-intersecting) polygon are indeterminate. // Arguments: -// path = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(path) = - assert(is_path(path,dim=2), "Input should be a 2d path") - polygon_area(path, signed=true)<0; +// poly = The list of 2D path points for the perimeter of the polygon. +function polygon_is_clockwise(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d path") + polygon_area(poly, signed=true)<0; // Function: clockwise_polygon() // Usage: -// clockwise_polygon(path); +// clockwise_polygon(poly); // Description: // Given a 2D polygon path, returns the clockwise winding version of that path. -function clockwise_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? path : reverse_polygon(path); +function clockwise_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly); // Function: ccw_polygon() // Usage: -// ccw_polygon(path); +// ccw_polygon(poly); // Description: -// Given a 2D polygon path, returns the counter-clockwise winding version of that path. -function ccw_polygon(path) = - assert(is_path(path,dim=2), "Input should be a 2d polygon") - polygon_area(path, signed=true)<0 ? reverse_polygon(path) : path; +// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. +function ccw_polygon(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d polygon") + polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly; // Function: reverse_polygon() @@ -1810,7 +1818,7 @@ function reverse_polygon(poly) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - poly = path3d(cleanup_path(poly)), + poly = cleanup_path(poly), p0 = poly[0], n = sum([ for (i=[1:1:len(poly)-2]) @@ -1926,17 +1934,6 @@ function split_polygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); -//*** -// all the functions split_polygons_at_ may generate non simple polygons even from simple polygon inputs: -// split_polygons_at_each_y([[[-1,1,0],[0,0,0],[1,1,0],[1,-1,0],[-1,-1,0]]],[0]) -// produces: -// [ [[0, 0, 0], [1, 0, 0], [1, -1, 0], [-1, -1, 0], [-1, 0, 0]] -// [[-1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0], [-1, 0, 0]] ] -// and the second polygon is self-intersecting -// besides, it fails in some simple cases as triangles: -// split_polygons_at_each_y([ [-1,-1,0],[1,-1,0],[0,1,0]],[0])==[] -// this last failure may be fatal for vnf_bend - // Function: split_polygons_at_each_y() // Usage: @@ -1947,9 +1944,9 @@ function split_polygons_at_each_x(polys, xs, _i=0) = // polys = A list of 3D polygons to split. // ys = A list of scalar Y values to split at. function split_polygons_at_each_y(polys, ys, _i=0) = - assert( is_consistent(polys) && is_path(poly[0],dim=3) , - "The input list should contains only 3D polygons." ) - assert( is_finite(ys), "The split value list should contain only numbers." ) +// assert( is_consistent(polys) && is_path(polys[0],dim=3) , // not all polygons should have the same length!!! + // "The input list should contains only 3D polygons." ) + assert( is_finite(ys) || is_vector(ys), "The split value list should contain only numbers." ) //*** _i>=len(ys)? polys : split_polygons_at_each_y( [ @@ -1980,5 +1977,4 @@ function split_polygons_at_each_z(polys, zs, _i=0) = ); - // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index ceeb905..0950e1c 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -98,6 +98,8 @@ function standardize(v) = v==[]? [] : sign([for(vi=v) if( ! approx(vi,0)) vi,0 ][0])*v; +module assert_std(vc,ve) { assert(standardize(vc)==standardize(ve)); } + module test_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); @@ -487,48 +489,47 @@ module test_triangle_area() { module test_plane3pt() { - assert(plane3pt([0,0,20], [0,10,10], [0,0,0]) == [1,0,0,0]); - assert(plane3pt([2,0,20], [2,10,10], [2,0,0]) == [1,0,0,2]); - assert(plane3pt([0,0,0], [10,0,10], [0,0,20]) == [0,1,0,0]); - assert(plane3pt([0,2,0], [10,2,10], [0,2,20]) == [0,1,0,2]); - assert(plane3pt([0,0,0], [10,10,0], [20,0,0]) == [0,0,1,0]); - assert(plane3pt([0,0,2], [10,10,2], [20,0,2]) == [0,0,1,2]); + assert_std(plane3pt([0,0,20], [0,10,10], [0,0,0]), [1,0,0,0]); + assert_std(plane3pt([2,0,20], [2,10,10], [2,0,0]), [1,0,0,2]); + assert_std(plane3pt([0,0,0], [10,0,10], [0,0,20]), [0,1,0,0]); + assert_std(plane3pt([0,2,0], [10,2,10], [0,2,20]), [0,1,0,2]); + assert_std(plane3pt([0,0,0], [10,10,0], [20,0,0]), [0,0,1,0]); + assert_std(plane3pt([0,0,2], [10,10,2], [20,0,2]), [0,0,1,2]); } *test_plane3pt(); module test_plane3pt_indexed() { pts = [ [0,0,0], [10,0,0], [0,10,0], [0,0,10] ]; s13 = sqrt(1/3); - assert(plane3pt_indexed(pts, 0,3,2) == [1,0,0,0]); - assert(plane3pt_indexed(pts, 0,2,3) == [-1,0,0,0]); - assert(plane3pt_indexed(pts, 0,1,3) == [0,1,0,0]); - assert(plane3pt_indexed(pts, 0,3,1) == [0,-1,0,0]); - assert(plane3pt_indexed(pts, 0,2,1) == [0,0,1,0]); + assert_std(plane3pt_indexed(pts, 0,3,2), [1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,3), [-1,0,0,0]); + assert_std(plane3pt_indexed(pts, 0,1,3), [0,1,0,0]); + assert_std(plane3pt_indexed(pts, 0,3,1), [0,-1,0,0]); + assert_std(plane3pt_indexed(pts, 0,2,1), [0,0,1,0]); assert_approx(plane3pt_indexed(pts, 0,1,2), [0,0,-1,0]); assert_approx(plane3pt_indexed(pts, 3,2,1), [s13,s13,s13,10*s13]); assert_approx(plane3pt_indexed(pts, 1,2,3), [-s13,-s13,-s13,-10*s13]); } *test_plane3pt_indexed(); - module test_plane_from_points() { - assert(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]) == [1,0,0,0]); - assert(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]) == [1,0,0,2]); - assert(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]) == [0,1,0,0]); - assert(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]) == [0,1,0,2]); - assert(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]) == [0,0,1,0]); - assert(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]) == [0,0,1,2]); + assert_std(plane_from_points([[0,0,20], [0,10,10], [0,0,0], [0,5,3]]), [1,0,0,0]); + assert_std(plane_from_points([[2,0,20], [2,10,10], [2,0,0], [2,3,4]]), [1,0,0,2]); + assert_std(plane_from_points([[0,0,0], [10,0,10], [0,0,20], [5,0,7]]), [0,1,0,0]); + assert_std(plane_from_points([[0,2,0], [10,2,10], [0,2,20], [4,2,3]]), [0,1,0,2]); + assert_std(plane_from_points([[0,0,0], [10,10,0], [20,0,0], [8,3,0]]), [0,0,1,0]); + assert_std(plane_from_points([[0,0,2], [10,10,2], [20,0,2], [3,4,2]]), [0,0,1,2]); } *test_plane_from_points(); module test_plane_normal() { - assert(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])) == [1,0,0]); - assert(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])) == [0,1,0]); - assert(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])) == [0,0,1]); - assert(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])) == [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,0,10], [0,0,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,2,0], [10,2,10], [0,2,20])), [0,1,0]); + assert_std(plane_normal(plane3pt([0,0,0], [10,10,0], [20,0,0])), [0,0,1]); + assert_std(plane_normal(plane3pt([0,0,2], [10,10,2], [20,0,2])), [0,0,1]); } *test_plane_normal(); @@ -699,16 +700,22 @@ module test_simplify_path_indexed() { module test_point_in_polygon() { poly = [for (a=[0:30:359]) 10*[cos(a),sin(a)]]; + poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); + assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); + assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); + assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From e5a0a3cad795206d7832354b73274cf7d78ef94f Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Sat, 22 Aug 2020 16:09:28 -0700 Subject: [PATCH 54/57] Fix diff/intersect when some tags are hidden When the first operand is hidden, some strange things happen, so this guards against that. Example: include part_to_show = "A"; // [A,B,C] part_to_hide = "B"; // [A,B,C] module my_part() { tags("A") left(10) cuboid(10); tags("B") diff("neg", "B") right(10) { cuboid(10); cyl(d=10, h=10, $tags="neg"); } tags("C") fwd(10) cuboid(10); } show(part_to_show) hide(part_to_hide) my_part(); Tag "B" here acts very strangely when it is supposed to be hidden, and never hides completely. --- attachments.scad | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/attachments.scad b/attachments.scad index c2c969e..48ed522 100644 --- a/attachments.scad +++ b/attachments.scad @@ -1235,17 +1235,20 @@ module show(tags="") // } module diff(neg, pos=undef, keep=undef) { - difference() { - if (pos != undef) { - show(pos) children(); - } else { - if (keep == undef) { - hide(neg) children(); + // Don't perform the operation if the current tags are hidden + if (attachment_is_shown($tags)) { + difference() { + if (pos != undef) { + show(pos) children(); } else { - hide(str(neg," ",keep)) children(); + if (keep == undef) { + hide(neg) children(); + } else { + hide(str(neg," ",keep)) children(); + } } + show(neg) children(); } - show(neg) children(); } if (keep!=undef) { show(keep) children(); @@ -1280,17 +1283,20 @@ module diff(neg, pos=undef, keep=undef) // } module intersect(a, b=undef, keep=undef) { - intersection() { - if (b != undef) { - show(b) children(); - } else { - if (keep == undef) { - hide(a) children(); + // Don't perform the operation if the current tags are hidden + if (attachment_is_shown($tags)) { + intersection() { + if (b != undef) { + show(b) children(); } else { - hide(str(a," ",keep)) children(); + if (keep == undef) { + hide(a) children(); + } else { + hide(str(a," ",keep)) children(); + } } + show(a) children(); } - show(a) children(); } if (keep!=undef) { show(keep) children(); From fb8d49f8ccddbce0590e5b2f304899ab3d14e178 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 25 Aug 2020 12:28:15 +0100 Subject: [PATCH 55/57] Restore updating calls to geometry --- rounding.scad | 286 ++++++++++++++++++++++++++++---------------------- 1 file changed, 159 insertions(+), 127 deletions(-) diff --git a/rounding.scad b/rounding.scad index 67fab47..ae2aed0 100644 --- a/rounding.scad +++ b/rounding.scad @@ -458,10 +458,10 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals -// Module: offset_sweep() +// Function&Module: offset_sweep() // // Description: -// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. +// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. When invoked as a function returns a VNF; when invoked as a module produces geometry. // You can specify a sequence of offsets values, or you can use several built-in offset profiles that are designed to provide end treatments such as roundovers. // The path is shifted by `offset()` multiple times in sequence // to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade @@ -543,8 +543,12 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // angle = default angle for chamfers. Default: 45 // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover -// convexity = convexity setting for use with polyhedron. Default: 10 -// +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Rounding a star shaped prism with postive radius values // star = star(5, r=22, ir=13); // rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24); @@ -650,118 +654,118 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // up(1) // offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4)); // } -module offset_sweep( - path, height, h, l, - top=[], bottom=[], - offset="round", r=0, steps=16, - quality=1, check_valid=true, - offset_maxstep=1, extra=0, - cut=undef, chamfer_width=undef, chamfer_height=undef, - joint=undef, k=0.75, angle=45, - convexity=10 -) { - // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce - // the inputs for the polyhedron module. - function make_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, vertexcount=0, vertices=[], faces=[] )= - offsetind==len(offsets)? ( - let( - bottom = list_range(n=len(path),s=vertexcount), - oriented_bottom = !flip_faces? bottom : reverse(bottom) - ) [vertices, concat(faces,[oriented_bottom])] - ) : ( - let( - this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], - delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, - r = offset_type=="round"? this_offset : undef, - do_chamfer = offset_type == "chamfer" + + +// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce +// the inputs for the polyhedron module. +function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, + vertexcount=0, vertices=[], faces=[] )= + offsetind==len(offsets)? ( + let( + bottom = list_range(n=len(path),s=vertexcount), + oriented_bottom = !flip_faces? bottom : reverse(bottom) + ) [vertices, concat(faces,[oriented_bottom])] + ) : ( + let( + this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], + delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, + r = offset_type=="round"? this_offset : undef, + do_chamfer = offset_type == "chamfer" + ) + let( + vertices_faces = offset( + path, r=r, delta=delta, chamfer = do_chamfer, closed=true, + check_valid=check_valid, quality=quality, + maxstep=maxstep, return_faces=true, + firstface_index=vertexcount, + flip_faces=flip_faces ) - assert(num_defined([r,delta])==1,str("Must set `offset` to ",round," or ",delta) - let( - vertices_faces = offset( - path, r=r, delta=delta, chamfer = do_chamfer, closed=true, - check_valid=check_valid, quality=quality, - maxstep=maxstep, return_faces=true, - firstface_index=vertexcount, - flip_faces=flip_faces - ) - ) - make_polyhedron( - vertices_faces[0], offsets, offset_type, - flip_faces, quality, check_valid, maxstep, - offsetind+1, vertexcount+len(path), - vertices=concat( - vertices, - zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) - ), - faces=concat(faces, vertices_faces[1]) - ) - ); - - - argspec = [ - ["r",r], - ["extra",extra], - ["type","circle"], - ["check_valid",check_valid], - ["quality",quality], - ["offset_maxstep", offset_maxstep], - ["steps",steps], - ["offset",offset], - ["chamfer_width",chamfer_width], - ["chamfer_height",chamfer_height], - ["angle",angle], - ["cut",cut], - ["joint",joint], - ["k", k], - ["points", []], - ]; - - path = check_and_fix_path(path, [2], closed=true); - clockwise = polygon_is_clockwise(path); - - top = struct_set(argspec, top, grow=false); - bottom = struct_set(argspec, bottom, grow=false); - - // This code does not work. It hits the error in make_polyhedron from offset being wrong - // before this code executes. Had to move the test into make_polyhedron, which is ugly since it's in the loop - //offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) && - // in_list(struct_val(bottom, "offset"),["round","delta"]); - //assert(offsetsok,"Offsets must be one of \"round\" or \"delta\""); - - - offsets_bot = _rounding_offsets(bottom, -1); - offsets_top = _rounding_offsets(top, 1); - - if (offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5)) { - echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested many layers. This can be slow or run out of recursion depth."); - } - // "Extra" height enlarges the result beyond the requested height, so subtract it - bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"); - top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"); - - height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height); - assert(height>=0, "Height must be nonnegative"); - - middle = height-bottom_height-top_height; - assert( - middle>=0, str( - "Specified end treatments (bottom height = ",bottom_height, - " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + _make_offset_polyhedron( + vertices_faces[0], offsets, offset_type, + flip_faces, quality, check_valid, maxstep, + offsetind+1, vertexcount+len(path), + vertices=concat( + vertices, + zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) + ), + faces=concat(faces, vertices_faces[1]) ) ); - initial_vertices_bot = path3d(path); - vertices_faces_bot = make_polyhedron( + +function offset_sweep( + path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45 + ) = + let( + argspec = [ + ["r",r], + ["extra",extra], + ["type","circle"], + ["check_valid",check_valid], + ["quality",quality], + ["offset_maxstep", offset_maxstep], + ["steps",steps], + ["offset",offset], + ["chamfer_width",chamfer_width], + ["chamfer_height",chamfer_height], + ["angle",angle], + ["cut",cut], + ["joint",joint], + ["k", k], + ["points", []], + ], + path = check_and_fix_path(path, [2], closed=true), + clockwise = polygon_is_clockwise(path), + + top = struct_set(argspec, top, grow=false), + bottom = struct_set(argspec, bottom, grow=false), + + // This code does not work. It hits the error in _make_offset_polyhedron from offset being wrong + // before this code executes. Had to move the test into _make_offset_polyhedron, which is ugly since it's in the loop + offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) + && in_list(struct_val(bottom, "offset"),["round","delta"]) + ) + assert(offsetsok,"Offsets must be one of \"round\" or \"delta\"") + let( + offsets_bot = _rounding_offsets(bottom, -1), + offsets_top = _rounding_offsets(top, 1), + dummy = offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5) + ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.") + : 0, + + // "Extra" height enlarges the result beyond the requested height, so subtract it + bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"), + top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"), + + height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height), + middle = height-bottom_height-top_height + ) + assert(height>=0, "Height must be nonnegative") + assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height, + " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + ) + let( + initial_vertices_bot = path3d(path), + + vertices_faces_bot = _make_offset_polyhedron( path, offsets_bot, struct_val(bottom,"offset"), clockwise, struct_val(bottom,"quality"), struct_val(bottom,"check_valid"), struct_val(bottom,"offset_maxstep"), vertices=initial_vertices_bot - ); + ), - top_start_ind = len(vertices_faces_bot[0]); - initial_vertices_top = zip(path, repeat(middle,len(path))); - vertices_faces_top = make_polyhedron( + top_start_ind = len(vertices_faces_bot[0]), + initial_vertices_top = zip(path, repeat(middle,len(path))), + vertices_faces_top = _make_offset_polyhedron( path, move(p=offsets_top,[0,middle]), struct_val(top,"offset"), !clockwise, struct_val(top,"quality"), @@ -769,20 +773,39 @@ module offset_sweep( struct_val(top,"offset_maxstep"), vertexcount=top_start_ind, vertices=initial_vertices_top - ); + ), middle_faces = middle==0 ? [] : [ for(i=[0:len(path)-1]) let( oneface=[i, (i+1)%len(path), top_start_ind+(i+1)%len(path), top_start_ind+i] ) !clockwise ? reverse(oneface) : oneface - ]; - up(bottom_height) { - polyhedron( - concat(vertices_faces_bot[0],vertices_faces_top[0]), - faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces), - convexity=convexity - ); - } -} + ] + ) + [up(bottom_height, concat(vertices_faces_bot[0],vertices_faces_top[0])), // Vertices + concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)]; // Faces + + +module offset_sweep(path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45, + convexity=10,anchor="origin",cp, + spin=0, orient=UP, extent=false) +{ + vnf = offset_sweep(path=path, height=height, h=h, l=l, top=top, bottom=bottom, offset=offset, r=0, steps=steps, + quality=quality, check_valid=true, offset_maxstep=1, extra=0, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle); + + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + + function os_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) = assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`") @@ -924,7 +947,6 @@ function os_profile(points, extra,check_valid, quality, offset_maxstep, offset) // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover // convexity = convexity setting for use with polyhedron. Default: 10 -// // Example: Chamfered elliptical prism. If you stretch a chamfered cylinder the chamfer will be uneven. // convex_offset_extrude(bottom = os_chamfer(height=-2), top=os_chamfer(height=1), height=7) // xscale(4)circle(r=6,$fn=64); @@ -1364,8 +1386,6 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, } } - - function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( N = len(top), @@ -1395,8 +1415,8 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( prev_corner = prev_offset + abs(rtop_in)*in_prev, next_corner = next_offset + abs(rtop_in)*in_next, - prev_degenerate = is_undef(ray_intersection([far_corner, far_corner+prev], [prev_offset, prev_offset+in_prev])), - next_degenerate = is_undef(ray_intersection([far_corner, far_corner+next], [next_offset, next_offset+in_next])) + prev_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+prev]), path2d([prev_offset, prev_offset+in_prev]))), + next_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+next]), path2d([next_offset, next_offset+in_next]))) ) [ prev_degenerate ? far_corner : prev_corner, far_corner, @@ -1452,6 +1472,11 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // splinesteps = number of segments to use for curved patches. Default: 16 // debug = turn on debug mode which displays illegal polyhedra and shows the bezier corner patches for troubleshooting purposes. Default: False // convexity = convexity parameter for polyhedron(), only for module version. Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Uniformly rounded pentagonal prism // rounded_prism(pentagon(3), height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5); // Example: Maximum possible rounding. @@ -1500,15 +1525,21 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)), joint_top=2, joint_bot=1, joint_sides=1); module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides, - k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false) + k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false, + anchor="origin",cp,spin=0, orient=UP, extent=false) { result = rounded_prism(bottom=bottom, top=top, joint_bot=joint_bot, joint_top=joint_top, joint_sides=joint_sides, k_bot=k_bot, k_top=k_top, k_sides=k_sides, k=k, splinesteps=splinesteps, h=h, length=length, height=height, l=l,debug=debug); - if (debug){ - vnf_polyhedron(result[1], convexity=convexity); - trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + vnf = debug ? result[1] : result; + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + if (debug){ + vnf_polyhedron(vnf, convexity=convexity); + trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + } + else vnf_polyhedron(vnf,convexity=convexity); + children(); } - else vnf_polyhedron(result,convexity=convexity); } @@ -1880,7 +1911,7 @@ function _circle_mask(r) = // $fn=128; // difference(){ // tube(or=r, wall=2, h=45); -// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7))),14*15,closed=true))); +// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7)),$fn=32),14*15,closed=true))); // } // } // Example(2D): Cutting a slot in a cylinder is tricky if you want rounded corners at the top. This slot profile has slightly angled top edges to blend into the top edge of the cylinder. @@ -1944,6 +1975,7 @@ function _circle_mask(r) = module bent_cutout_mask(r, thickness, path, convexity=10) { + no_children($children); assert(is_path(path,2),"Input path must be a 2d path") assert(r-thickness>0, "Thickness too large for radius"); assert(thickness>0, "Thickness must be positive"); From 90e02ad7a4bd17d74fb7c6fa9702164fa44fe15b Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Wed, 26 Aug 2020 15:59:35 -0700 Subject: [PATCH 56/57] Fix for #243. Added better docs and asserts to rot() --- transforms.scad | 28 +++++++++++++++------------- version.scad | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/transforms.scad b/transforms.scad index 965f5de..e9cfafd 100644 --- a/transforms.scad +++ b/transforms.scad @@ -306,11 +306,11 @@ function up(z=0,p=undef) = move([0,0,z],p=p); // * Called as a function with a `p` argument containing a list of points, returns the list of rotated points. // * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the rotated patch. // * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the rotated VNF. -// * Called as a function without a `p` argument, and `planar` is true, returns the affine2d rotational matrix. +// * Called as a function without a `p` argument, and `planar` is true, returns the affine2d rotational matrix. Requires that `a` is a finite scalar. // * Called as a function without a `p` argument, and `planar` is false, returns the affine3d rotational matrix. // // Arguments: -// a = Scalar angle or vector of XYZ rotation angles to rotate by, in degrees. +// a = Scalar angle or vector of XYZ rotation angles to rotate by, in degrees. If `planar` is true and `p` is not given, then `a` must be a finite scalar. Default: `0` // v = vector for the axis of rotation. Default: [0,0,1] or UP // cp = centerpoint to rotate around. Default: [0,0,0] // from = Starting vector for vector-based rotations. @@ -343,16 +343,21 @@ module rot(a=0, v=undef, cp=undef, from=undef, to=undef, reverse=false) function rot(a=0, v, cp, from, to, reverse=false, planar=false, p, _m) = assert(is_undef(from)==is_undef(to), "from and to must be specified together.") + assert(is_undef(from) || is_vector(from, zero=false), "'from' must be a non-zero vector.") + assert(is_undef(to) || is_vector(to, zero=false), "'to' must be a non-zero vector.") + assert(is_undef(v) || is_vector(v, zero=false), "'v' must be a non-zero vector.") + assert(is_undef(cp) || is_vector(cp), "'cp' must be a vector.") + assert(is_finite(a) || is_vector(a), "'a' must be a finite scalar or a vector.") + assert(is_bool(reverse)) + assert(is_bool(planar)) is_undef(p)? ( planar? let( + check = assert(is_num(a)), cp = is_undef(cp)? cp : point2d(cp), m1 = is_undef(from)? affine2d_zrot(a) : - assert(is_vector(from)) - assert(!approx(norm(from),0)) - assert(approx(point3d(from).z, 0)) - assert(is_vector(to)) - assert(!approx(norm(to),0)) - assert(approx(point3d(to).z, 0)) + assert(a==0, "'from' and 'to' cannot be used with 'a' when 'planar' is true.") + assert(approx(point3d(from).z, 0), "'from' must be a 2D vector when 'planar' is true.") + assert(approx(point3d(to).z, 0), "'to' must be a 2D vector when 'planar' is true.") affine2d_zrot( vang(point2d(to)) - vang(point2d(from)) @@ -364,13 +369,10 @@ function rot(a=0, v, cp, from, to, reverse=false, planar=false, p, _m) = to = is_undef(to)? undef : point3d(to), cp = is_undef(cp)? undef : point3d(cp), m1 = !is_undef(from)? ( - assert(is_vector(from)) - assert(!approx(norm(from),0)) - assert(is_vector(to)) - assert(!approx(norm(to),0)) + assert(is_num(a)) affine3d_rot_from_to(from,to) * affine3d_zrot(a) ) : - !is_undef(v)? affine3d_rot_by_axis(v,a) : + !is_undef(v)? assert(is_num(a)) affine3d_rot_by_axis(v,a) : is_num(a)? affine3d_zrot(a) : affine3d_zrot(a.z) * affine3d_yrot(a.y) * affine3d_xrot(a.x), m2 = is_undef(cp)? m1 : (move(cp) * m1 * move(-cp)), diff --git a/version.scad b/version.scad index 3d457d6..f2624a8 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,407]; +BOSL_VERSION = [2,0,408]; // Section: BOSL Library Version Functions From b679ea52dc52aa9098eb9d0c5c81e4e5608f76ca Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Wed, 26 Aug 2020 18:02:16 -0700 Subject: [PATCH 57/57] Add is_zero(), is_positive(), is_negative(), is_nonpositive(), is_nonnegative(). --- math.scad | 119 ++++++++++++++++++++++++++++++++++++++++++- tests/test_math.scad | 102 ++++++++++++++++++++++++++++++++++++- version.scad | 2 +- 3 files changed, 220 insertions(+), 3 deletions(-) diff --git a/math.scad b/math.scad index 888d048..90182f1 100644 --- a/math.scad +++ b/math.scad @@ -864,6 +864,123 @@ function is_matrix(A,m,n,square=false) = // Section: Comparisons and Logic +// Function: is_zero() +// Usage: +// is_zero(x); +// Description: +// Returns true if the number passed to it is approximately zero, to within `eps`. +// If passed a list, recursively checks if all items in the list are approximately zero. +// Otherwise, returns false. +// Arguments: +// x = The value to check. +// eps = The maximum allowed variance. Default: `EPSILON` (1e-9) +// Example: +// is_zero(0); // Returns: true. +// is_zero(1e-3); // Returns: false. +// is_zero([0,0,0]); // Returns: true. +// is_zero([0,0,1e-3]); // Returns: false. +function is_zero(x, eps=EPSILON) = + is_list(x)? (x != [] && [for (xx=x) if(!is_zero(xx,eps=eps)) 1] == []) : + is_num(x)? approx(x,eps) : + false; + + +// Function: is_positive() +// Usage: +// is_positive(x); +// Description: +// Returns true if the number passed to it is greater than zero. +// If passed a list, recursively checks if all items in the list are positive. +// Otherwise, returns false. +// Arguments: +// x = The value to check. +// Example: +// is_positive(-2); // Returns: false. +// is_positive(0); // Returns: false. +// is_positive(2); // Returns: true. +// is_positive([0,0,0]); // Returns: false. +// is_positive([0,1,2]); // Returns: false. +// is_positive([3,1,2]); // Returns: true. +// is_positive([3,-1,2]); // Returns: false. +function is_positive(x) = + is_list(x)? (x != [] && [for (xx=x) if(!is_positive(xx)) 1] == []) : + is_num(x)? x>0 : + false; + + +// Function: is_negative() +// Usage: +// is_negative(x); +// Description: +// Returns true if the number passed to it is less than zero. +// If passed a list, recursively checks if all items in the list are negative. +// Otherwise, returns false. +// Arguments: +// x = The value to check. +// Example: +// is_negative(-2); // Returns: true. +// is_negative(0); // Returns: false. +// is_negative(2); // Returns: false. +// is_negative([0,0,0]); // Returns: false. +// is_negative([0,1,2]); // Returns: false. +// is_negative([3,1,2]); // Returns: false. +// is_negative([3,-1,2]); // Returns: false. +// is_negative([-3,-1,-2]); // Returns: true. +function is_negative(x) = + is_list(x)? (x != [] && [for (xx=x) if(!is_negative(xx)) 1] == []) : + is_num(x)? x<0 : + false; + + +// Function: is_nonpositive() +// Usage: +// is_nonpositive(x); +// Description: +// Returns true if the number passed to it is less than or equal to zero. +// If passed a list, recursively checks if all items in the list are nonpositive. +// Otherwise, returns false. +// Arguments: +// x = The value to check. +// Example: +// is_nonpositive(-2); // Returns: true. +// is_nonpositive(0); // Returns: true. +// is_nonpositive(2); // Returns: false. +// is_nonpositive([0,0,0]); // Returns: true. +// is_nonpositive([0,1,2]); // Returns: false. +// is_nonpositive([3,1,2]); // Returns: false. +// is_nonpositive([3,-1,2]); // Returns: false. +// is_nonpositive([-3,-1,-2]); // Returns: true. +function is_nonpositive(x) = + is_list(x)? (x != [] && [for (xx=x) if(!is_nonpositive(xx)) 1] == []) : + is_num(x)? x<=0 : + false; + + +// Function: is_nonnegative() +// Usage: +// is_nonnegative(x); +// Description: +// Returns true if the number passed to it is greater than or equal to zero. +// If passed a list, recursively checks if all items in the list are nonnegative. +// Otherwise, returns false. +// Arguments: +// x = The value to check. +// Example: +// is_nonnegative(-2); // Returns: false. +// is_nonnegative(0); // Returns: true. +// is_nonnegative(2); // Returns: true. +// is_nonnegative([0,0,0]); // Returns: true. +// is_nonnegative([0,1,2]); // Returns: true. +// is_nonnegative([0,-1,-2]); // Returns: false. +// is_nonnegative([3,1,2]); // Returns: true. +// is_nonnegative([3,-1,2]); // Returns: false. +// is_nonnegative([-3,-1,-2]); // Returns: false. +function is_nonnegative(x) = + is_list(x)? (x != [] && [for (xx=x) if(!is_nonnegative(xx)) 1] == []) : + is_num(x)? x>=0 : + false; + + // Function: approx() // Usage: // approx(a,b,[eps]) @@ -1382,4 +1499,4 @@ function real_roots(p,eps=undef,tol=1e-14) = ? [for(z=roots) if (abs(z.y)/(1+norm(z))