diff --git a/comparisons.scad b/comparisons.scad index 7c312de..a0bd832 100644 --- a/comparisons.scad +++ b/comparisons.scad @@ -620,7 +620,9 @@ function _sort_general(arr, idx=undef, indexed=false) = (len(arr)<=1) ? arr : ! indexed && is_undef(idx) ? _lexical_sort(arr) - : let( arrind = _indexed_sort(enumerate(arr,idx)) ) + : let( labeled = is_undef(idx) ? [for(i=idx(arr)) [i,arr[i]]] + : [for(i=idx(arr)) [i, for(j=idx) arr[i][j]]], + arrind = _indexed_sort(labeled)) indexed ? arrind : [for(i=arrind) arr[i]]; @@ -797,7 +799,7 @@ function group_data(groups, values) = assert(is_list(values)) assert(len(groups)==len(values), "The groups and values arguments should be lists of matching length.") - let( sorted = _group_sort_by_index(zip(groups,values),0) ) + let( sorted = _group_sort_by_index([for(i=idx(groups))[groups[i],values[i]]],0) ) // retrieve values and insert [] [ for (i = idx(sorted)) diff --git a/geometry.scad b/geometry.scad index 97ac59d..7ee6219 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1047,7 +1047,7 @@ function _is_point_above_plane(plane, point) = // r = radius of circle // --- // d = diameter of circle -// line = two points defining the unbounded line +// line = two points defining the line // bounded = false for unbounded line, true for a segment, or a vector [false,true] or [true,false] to specify a ray with the first or second end unbounded. Default: false // eps = epsilon used for identifying the case with one solution. Default: 1e-9 function circle_line_intersection(c,r,d,line,bounded=false,eps=EPSILON) = diff --git a/lists.scad b/lists.scad index f90b457..8d829e8 100644 --- a/lists.scad +++ b/lists.scad @@ -822,7 +822,7 @@ function list_remove_values(list,values=[],all=false) = // rng = idx(list, [s=], [e=], [step=]); // for(i=idx(list, [s=], [e=], [step=])) ... // Topics: List Handling, Iteration -// See Also: enumerate(), pair(), triplet(), combinations(), permutations() +// See Also: pair(), triplet(), combinations(), permutations() // Description: // Returns the range of indexes for the given list. // Arguments: @@ -843,39 +843,13 @@ function idx(list, s=0, e=-1, step=1) = ) [_s : step : _e]; -// Function: enumerate() -// Usage: -// arr = enumerate(l, [idx]); -// for (x = enumerate(l, [idx])) ... // x[0] is the index number, x[1] is the item. -// Topics: List Handling, Iteration -// See Also: idx(), pair(), triplet(), combinations(), permutations() -// Description: -// Returns a list, with each item of the given list `l` numbered in a sublist. -// Something like: `[[0,l[0]], [1,l[1]], [2,l[2]], ...]` -// Arguments: -// l = List to enumerate. -// idx = If given, enumerates just the given columns items of `l`. -// Example: -// enumerate(["a","b","c"]); // Returns: [[0,"a"], [1,"b"], [2,"c"]] -// enumerate([[88,"a"],[76,"b"],[21,"c"]], idx=1); // Returns: [[0,"a"], [1,"b"], [2,"c"]] -// enumerate([["cat","a",12],["dog","b",10],["log","c",14]], idx=[1:2]); // Returns: [[0,"a",12], [1,"b",10], [2,"c",14]] -// Example(2D): -// colors = ["red", "green", "blue"]; -// 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( _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]) [ i, for (j=idx) l[i][j]] ]; - // Function: pair() // Usage: // p = pair(list, [wrap]); // for (p = pair(list, [wrap])) ... // On each iteration, p contains a list of two adjacent items. // Topics: List Handling, Iteration -// See Also: idx(), enumerate(), triplet(), combinations(), permutations() +// See Also: idx(), triplet(), combinations(), permutations() // Description: // Takes a list, and returns a list of adjacent pairs from it, optionally wrapping back to the front. // Arguments: @@ -907,7 +881,7 @@ function pair(list, wrap=false) = // list = triplet(list, [wrap]); // for (t = triplet(list, [wrap])) ... // Topics: List Handling, Iteration -// See Also: idx(), enumerate(), pair(), combinations(), permutations() +// See Also: idx(), pair(), combinations(), permutations() // Description: // Takes a list, and returns a list of adjacent triplets from it, optionally wrapping back to the front. // If you set `wrap` to true then the first triplet is the one centered on the first list element, so it includes @@ -945,7 +919,7 @@ function triplet(list, wrap=false) = // Usage: // list = combinations(l, [n]); // Topics: List Handling, Iteration -// See Also: idx(), enumerate(), pair(), triplet(), permutations() +// See Also: idx(), pair(), triplet(), permutations() // Description: // Returns a list of all of the (unordered) combinations of `n` items out of the given list `l`. // For the list `[1,2,3,4]`, with `n=2`, this will return `[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]`. @@ -966,11 +940,12 @@ function combinations(l,n=2,_s=0) = : [for (i=[_s:1:len(l)-n], p=combinations(l,n=n-1,_s=i+1)) concat([l[i]], p)]; + // Function: permutations() // Usage: // list = permutations(l, [n]); // Topics: List Handling, Iteration -// See Also: idx(), enumerate(), pair(), triplet(), combinations() +// See Also: idx(), pair(), triplet(), combinations() // Description: // Returns a list of all of the (ordered) permutation `n` items out of the given list `l`. // For the list `[1,2,3]`, with `n=2`, this will return `[[1,2],[1,3],[2,1],[2,3],[3,1],[3,2]]` @@ -989,8 +964,6 @@ function permutations(l,n=2) = - - // Section: Changing list structure @@ -1050,33 +1023,6 @@ function full_flatten(l) = -// Function: zip() -// Usage: -// pairs = zip(a,b); -// triples = zip(a,b,c); -// quads = zip([LIST1,LIST2,LIST3,LIST4]); -// Topics: List Handling, Iteration -// Description: -// Zips together two or more lists into a single list. For example, if you have two -// lists [3,4,5], and [8,7,6], and zip them together, you get [ [3,8],[4,7],[5,6] ]. -// The list returned will be as long as the shortest list passed to zip(). -// Arguments: -// a = The first list, or a list of lists if b and c are not given. -// b = The second list, if given. -// c = The third list, if given. -// Example: -// a = [9,8,7,6]; b = [1,2,3]; -// for (p=zip(a,b)) echo(p); -// // ECHO: [9,1] -// // ECHO: [8,2] -// // ECHO: [7,3] -function zip(a,b,c) = - b!=undef? zip([a,b,if (c!=undef) c]) : - let(n = min_length(a)) - [for (i=[0:1:n-1]) [for (x=a) x[i]]]; - - - // Section: Set Manipulation // Function: set_union() diff --git a/shapes2d.scad b/shapes2d.scad index ad93b84..6c25153 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -179,7 +179,7 @@ function rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) = // circle(r|d=, ...) { attachables } // Usage: As a Function // path = circle(r|d=, ...); -// See Also: ellipse() +// See Also: ellipse(), circle_2tangents(), circle_3points() // Description: // When called as the builtin module, creates a 2D polygon that approximates a circle of the given size. // When called as a function, returns a 2D list of points (path) for a polygon that approximates a circle of the given size. @@ -224,13 +224,14 @@ module circle(r, d, anchor=CENTER, spin=0) { // Description: // When called as a module, creates a 2D polygon that approximates a circle or ellipse of the given size. // When called as a function, returns a 2D list of points (path) for a polygon that approximates a circle or ellipse of the given size. -// Note that the point list or shape is the same as the one you would get by scaling the output of {{circle()}}, but with this module your -// attachments to the ellipse will +// By default the point list or shape is the same as the one you would get by scaling the output of {{circle()}}, but with this module your +// attachments to the ellipse will retain their dimensions, whereas scaling a circle with attachments will also scale the attachments. +// If you set unifom to true then you will get a polygon with congruent sides whose vertices lie on the ellipse. // Arguments: // r = Radius of the circle or pair of semiaxes of ellipse // --- // d = Diameter of the circle or a pair giving the full X and Y axis lengths. -// realign = If true, rotates the polygon that approximates the circle/ellipse by half of one size. +// realign = If false starts the approximate ellipse with a point on the X+ axis. If true the midpoint of a side is on the X+ axis and the first point of the polygon is below the X+ axis. This can result in a very different polygon when $fn is small. Default: false // circum = If true, the polygon that approximates the circle will be upsized slightly to circumscribe the theoretical circle. If false, it inscribes the theoretical circle. Default: false // 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` @@ -244,6 +245,54 @@ module circle(r, d, anchor=CENTER, spin=0) { // ellipse(d=50, anchor=FRONT, spin=45); // Example(NORENDER): Called as Function // path = ellipse(d=50, anchor=FRONT, spin=45); +// Example(2D,NoAxes): Uniformly sampled hexagon at the top, regular non-uniform one at the bottom +// r=[10,3]; +// ydistribute(7){ +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=6)],width=0.1,color="red"); +// } +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=6,uniform=true)],width=0.1,color="red"); +// } +// } +// Example(2D): The realigned hexagons are even more different +// r=[10,3]; +// ydistribute(7){ +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=6,realign=true)],width=0.1,color="red"); +// } +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=6,realign=true,uniform=true)],width=0.1,color="red"); +// } +// } +// Example(2D): For odd $fn the result may not look very elliptical: +// r=[10,3]; +// ydistribute(7){ +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=5,realign=false)],width=0.1,color="red"); +// } +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue"); +// stroke([ellipse(r=r, $fn=5,realign=false,uniform=true)],width=0.1,color="red"); +// } +// } +// Example(2D): The same ellipse, turned 90 deg, gives a very different result: +// r=[3,10]; +// xdistribute(7){ +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue"); +// stroke([ellipse(r=r, $fn=5,realign=false)],width=0.2,color="red"); +// } +// union(){ +// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue"); +// stroke([ellipse(r=r, $fn=5,realign=false,uniform=true)],width=0.2,color="red"); +// } +// } module ellipse(r, d, realign=false, circum=false, uniform=false, anchor=CENTER, spin=0) { r = force_list(get_radius(r=r, d=d, dflt=1),2); @@ -298,13 +347,41 @@ function _ellipse_refine(a,b,N, _theta=[]) = + +function _ellipse_refine_realign(a,b,N, _theta=[],i=0) = + len(_theta)==0? + _ellipse_refine_realign(a,b,N, count(N-1,180/N,360/N)) + : + let( + pts = [for(t=_theta) [a*cos(t),b*sin(t)], + [a*cos(_theta[0]), -b*sin(_theta[0])]], + lenlist= path_segment_lengths(pts,closed=true), + meanlen = mean(lenlist), + error = lenlist/meanlen + ) + all_equal(error,EPSILON) ? pts + : + let( + dtheta = [each deltas(_theta), + 360-last(_theta)-_theta[0], + 2*_theta[0]], + newdtheta = [for(i=idx(dtheta)) dtheta[i]/error[i]], + normdtheta = newdtheta / sum(newdtheta) * 360, + adjusted = cumsum([last(normdtheta)/2, each list_head(normdtheta, -3)]) + ) + _ellipse_refine_realign(a,b,N,adjusted, i+1); + + + function ellipse(r, d, realign=false, circum=false, uniform=false, anchor=CENTER, spin=0) = let( r = force_list(get_radius(r=r, d=d, dflt=1),2), sides = segs(max(r)) ) uniform ? assert(!circum, "Circum option not allowed when \"uniform\" is true") - reorient(anchor,spin,two_d=true,r=[r.x,r.y],p=_ellipse_refine(r.x,r.y,sides)) + reorient(anchor,spin,two_d=true,r=[r.x,r.y], + p=realign ? reverse(_ellipse_refine_realign(r.x,r.y,sides)) + : reverse_polygon(_ellipse_refine(r.x,r.y,sides))) : let( offset = realign? 180/sides : 0, @@ -336,7 +413,7 @@ function ellipse(r, d, realign=false, circum=false, uniform=false, anchor=CENTER // id = Inside diameter, at center of sides. // side = Length of each side. // rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) -// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// realign = If false, vertex 0 will lie on the X+ axis. If true then the midpoint of the last edge will lie on the X+ axis, and vertex 0 will be below the X axis. Default: false // align_tip = If given as a 2D vector, rotates the whole shape so that the first vertex points in that direction. This occurs before spin. // align_side = If given as a 2D vector, rotates the whole shape so that the normal of side0 points in that direction. This occurs before spin. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` @@ -471,7 +548,7 @@ module regular_ngon(n=6, r, d, or, od, ir, id, side, rounding=0, realign=false, // id = Inside diameter, at center of sides. // side = Length of each side. // rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) -// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// realign = If false, vertex 0 will lie on the X+ axis. If true then the midpoint of the last edge will lie on the X+ axis, and vertex 0 will be below the X axis. Default: false // align_tip = If given as a 2D vector, rotates the whole shape so that the first vertex points in that direction. This occurs before spin. // align_side = If given as a 2D vector, rotates the whole shape so that the normal of side0 points in that direction. This occurs before spin. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` @@ -535,7 +612,7 @@ module pentagon(r, d, or, od, ir, id, side, rounding=0, realign=false, align_tip // id = Inside diameter, at center of sides. // side = Length of each side. // rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) -// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// realign = If false, vertex 0 will lie on the X+ axis. If true then the midpoint of the last edge will lie on the X+ axis, and vertex 0 will be below the X axis. Default: false // align_tip = If given as a 2D vector, rotates the whole shape so that the first vertex points in that direction. This occurs before spin. // align_side = If given as a 2D vector, rotates the whole shape so that the normal of side0 points in that direction. This occurs before spin. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` @@ -598,7 +675,7 @@ module hexagon(r, d, or, od, ir, id, side, rounding=0, realign=false, align_tip, // id = Inside diameter, at center of sides. // side = Length of each side. // rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) -// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// realign = If false, vertex 0 will lie on the X+ axis. If true then the midpoint of the last edge will lie on the X+ axis, and vertex 0 will be below the X axis. Default: false // align_tip = If given as a 2D vector, rotates the whole shape so that the first vertex points in that direction. This occurs before spin. // align_side = If given as a 2D vector, rotates the whole shape so that the normal of side0 points in that direction. This occurs before spin. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` @@ -808,7 +885,7 @@ module trapezoid(h, w1, w2, angle, shift=0, chamfer=0, rounding=0, anchor=CENTER // d/od = The diameter to the tips of the star. // id = The diameter to the inner corners of the star. // step = Calculates the radius of the inner star corners by virtually drawing a straight line `step` tips around the star. 2 <= step < n/2 -// realign = If false, a tip is aligned with the Y+ axis. If true, an inner corner is aligned with the Y+ axis. Default: false +// realign = If false, vertex 0 will lie on the X+ axis. If true then the midpoint of the last edge will lie on the X+ axis, and vertex 0 will be below the X axis. Default: false // align_tip = If given as a 2D vector, rotates the whole shape so that the first star tip points in that direction. This occurs before spin. // align_pit = If given as a 2D vector, rotates the whole shape so that the first inner corner is pointed towards that direction. This occurs before spin. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` diff --git a/tests/test_comparisons.scad b/tests/test_comparisons.scad index 0168037..f96bb93 100644 --- a/tests/test_comparisons.scad +++ b/tests/test_comparisons.scad @@ -8,7 +8,7 @@ module test_sort() { assert(sort([[8,0],[3,1],[9,2],[4,3],[3,4],[1,5],[8,6]],idx=1) == [[8,0],[3,1],[9,2],[4,3],[3,4],[1,5],[8,6]]); assert(sort(["cat", "oat", "sat", "bat", "vat", "rat", "pat", "mat", "fat", "hat", "eat"]) == ["bat", "cat", "eat", "fat", "hat", "mat", "oat", "pat", "rat", "sat", "vat"]); - assert(sort(enumerate([[2,3,4],[1,2,3],[2,4,3]]),idx=1)==[[1,[1,2,3]], [0,[2,3,4]], [2,[2,4,3]]]); + assert(sort([[0,[2,3,4]],[1,[1,2,3]],[2,[2,4,3]]],idx=1)==[[1,[1,2,3]], [0,[2,3,4]], [2,[2,4,3]]]); assert(sort([0,"1",[1,0],2,"a",[1]])== [0,2,"1","a",[1],[1,0]]); assert(sort([["oat",0], ["cat",1], ["bat",3], ["bat",2], ["fat",3]])== [["bat",2],["bat",3],["cat",1],["fat",3],["oat",0]]); } diff --git a/tests/test_lists.scad b/tests/test_lists.scad index 97ca138..badee9e 100644 --- a/tests/test_lists.scad +++ b/tests/test_lists.scad @@ -254,14 +254,6 @@ module test_idx() { test_idx(); -module test_enumerate() { - assert(enumerate(["a","b","c"]) == [[0,"a"], [1,"b"], [2,"c"]]); - assert(enumerate([[88,"a"],[76,"b"],[21,"c"]], idx=1) == [[0,"a"], [1,"b"], [2,"c"]]); - assert(enumerate([["cat","a",12],["dog","b",10],["log","c",14]], idx=[1:2]) == [[0,"a",12], [1,"b",10], [2,"c",14]]); -} -test_enumerate(); - - module test_shuffle() { nums1 = count(100); nums2 = shuffle(nums1,33); @@ -375,16 +367,6 @@ module test_repeat_entries() { test_repeat_entries(); -module test_zip() { - v1 = [1,2,3,4]; - v2 = [5,6,7]; - v3 = [8,9,10,11]; - assert(zip(v1,v3) == [[1,8],[2,9],[3,10],[4,11]]); - assert(zip([v1,v3]) == [[1,8],[2,9],[3,10],[4,11]]); -} -test_zip(); - - module test_list_to_matrix() { v = [1,2,3,4,5,6]; assert(list_to_matrix(v,2) == [[1,2], [3,4], [5,6]]);