diff --git a/math.scad b/math.scad index 7cc20145..e125ba38 100644 --- a/math.scad +++ b/math.scad @@ -56,7 +56,7 @@ NAN = acos(2); // Creates a list of `n` numbers, starting at `s`, incrementing by `step` each time. // You can also pass a list for n and then the length of the input list is used. // Arguments: -// n = The length of the list of numbers to create, or a list to match the length of +// n = The length of the list of numbers to create, or a list to match the length of. // s = The starting value of the list of numbers. // step = The amount to increment successive numbers in the list. // reverse = Reverse the list. Default: false. @@ -80,11 +80,12 @@ function count(n,s=0,step=1,reverse=false) = let(n=is_list(n) ? len(n) : n) // l = lerp(a, b, LIST); // Description: // Interpolate between two values or vectors. -// If `u` is given as a number, returns the single interpolated value. -// If `u` is 0.0, then the value of `a` is returned. -// If `u` is 1.0, then the value of `b` is returned. -// If `u` is a range, or list of numbers, returns a list of interpolated values. -// It is valid to use a `u` value outside the range 0 to 1. The result will be an extrapolation +// * If `u` is given as a number, returns the single interpolated value. +// * If `u` is 0.0, then the value of `a` is returned. +// * If `u` is 1.0, then the value of `b` is returned. +// * If `u` is a range, or list of numbers, returns a list of interpolated values. +// . +// It is valid to use a `u` value outside the range 0 to 1 to extrapolate // along the slope formed by `a` and `b`. // Arguments: // a = First value or vector. @@ -105,9 +106,9 @@ function count(n,s=0,step=1,reverse=false) = let(n=is_list(n) ? len(n) : n) // // Points colored in ROYGBIV order. // rainbow(pts) translate($item) circle(d=3,$fn=8); function lerp(a,b,u) = - assert(same_shape(a,b), "Bad or inconsistent inputs to lerp") + assert(same_shape(a,b), "\nBad 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 valid range.") + assert(is_finite(u) || is_vector(u) || valid_range(u), "\nInput u to lerp must be a number, vector, or valid range.") [for (v = u) (1-v)*a + v*b ]; @@ -133,7 +134,7 @@ function lerp(a,b,u) = // l = lerpn(0,1,6); // Returns: [0, 0.2, 0.4, 0.6, 0.8, 1] // l = lerpn(0,1,5,false); // Returns: [0, 0.2, 0.4, 0.6, 0.8] function lerpn(a,b,n,endpoint=true) = - assert(same_shape(a,b), "Bad or inconsistent inputs to lerpn") + assert(same_shape(a,b), "\nBad or inconsistent inputs to lerpn.") assert(is_int(n)) assert(is_bool(endpoint)) let( d = n - (endpoint? 1 : 0) ) @@ -148,7 +149,7 @@ function lerpn(a,b,n,endpoint=true) = // Description: // Compute bilinear interpolation between four values using two // coordinates that are meant to lie in [0,1]. (If they are outside -// this range, the function will extrapolate values.) The `pts` +// this range, the function extrapolates values.) The `pts` // argument is a list of the four values at the for corners, `[A,B,C,D]`. // These values are arranged on the corners as shown below. The `x` and // `y` parameters give the fraction of the distance from the left and bottom @@ -196,7 +197,7 @@ function bilerp(points,x,y) = // sqr([2,3,4]); // Returns: 29 // sqr([[1,2],[3,4]]); // Returns [[7,10],[15,22]] function sqr(x) = - assert(is_finite(x) || is_vector(x) || is_matrix(x), "Input is not a number nor a list of numbers.") + assert(is_finite(x) || is_vector(x) || is_matrix(x), "\nInput is not a number nor a list of numbers.") x*x; @@ -213,7 +214,7 @@ function sqr(x) = // log2(16); // Returns: 4 // log2(256); // Returns: 8 function log2(x) = - assert( is_finite(x), "Input is not a number.") + assert( is_finite(x), "\nInput is not a number.") ln(x)/ln(2); // this may return NAN or INF; should it check x>0 ? @@ -234,7 +235,7 @@ function log2(x) = // l = hypot(3,4); // Returns: 5 // l = hypot(3,4,5); // Returns: ~7.0710678119 function hypot(x,y,z=0) = - assert( is_vector([x,y,z]), "Improper number(s).") + assert( is_vector([x,y,z]), "\nImproper number(s).") norm([x,y,z]); @@ -248,14 +249,14 @@ function hypot(x,y,z=0) = // Returns the factorial of the given integer value, or n!/d! if d is given. // Arguments: // n = The integer number to get the factorial of. (n!) -// d = If given, the returned value will be (n! / d!) +// d = If given, the returned value is (n! / d!) // Example: // x = factorial(4); // Returns: 24 // 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 defined only for non negative integers") - assert(d<=n, "d cannot be larger than n") + assert(is_int(n) && is_int(d) && n>=0 && d>=0, "\nFactorial is defined only for non negative integers.") + assert(d<=n, "\nd cannot be larger than n.") product([1,for (i=[n:-1:d+1]) i]); @@ -274,7 +275,7 @@ function factorial(n,d=0) = // y = binomial(4); // Returns: [1,4,6,4,1] // z = binomial(6); // Returns: [1,6,15,20,15,6,1] function binomial(n) = - assert( is_int(n) && n>0, "Input is not an integer greater than 0.") + assert( is_int(n) && n>0, "\nInput must be an integer greater than 0.") [for( c = 1, i = 0; i<=n; c = c*(n-i)/(i+1), i = i+1 @@ -296,7 +297,7 @@ function binomial(n) = // x = binomial_coefficient(3,2); // Returns: 3 // y = binomial_coefficient(10,6); // Returns: 210 function binomial_coefficient(n,k) = - assert( is_int(n) && is_int(k), "Some input is not a number.") + assert( is_int(n) && is_int(k), "\nSome input is not a number.") k < 0 || k > n ? 0 : k ==0 || k ==n ? 1 : let( k = min(k, n-k), @@ -316,14 +317,14 @@ function binomial_coefficient(n,k) = // Description: // Computes the Greatest Common Divisor/Factor of `a` and `b`. function gcd(a,b) = - assert(is_int(a) && is_int(b),"Arguments to gcd must be integers") + assert(is_int(a) && is_int(b),"\nArguments to gcd must be integers.") b==0 ? abs(a) : gcd(b,a % b); // Computes lcm for two integers function _lcm(a,b) = - assert(is_int(a) && is_int(b), "Invalid non-integer parameters to lcm") - assert(a!=0 && b!=0, "Arguments to lcm should not be zero") + assert(is_int(a) && is_int(b), "\nInvalid non-integer parameters to lcm.") + assert(a!=0 && b!=0, "\nArguments to lcm must be non-zero.") abs(a*b) / gcd(a,b); @@ -348,7 +349,7 @@ function lcm(a,b=[]) = !is_list(a) && !is_list(b) ? _lcm(a,b) : let( arglist = concat(force_list(a),force_list(b)) ) - assert(len(arglist)>0, "Invalid call to lcm with empty list(s)") + assert(len(arglist)>0, "\nInvalid call to lcm with empty list(s).") _lcmlist(arglist); // Function rational_approx() @@ -392,7 +393,7 @@ function _cfrac_to_pq(cfrac,p=0,q=1,ind) = // a = sinh(x); // Description: Takes a value `x`, and returns the hyperbolic sine of it. function sinh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") (exp(x)-exp(-x))/2; // Function: cosh() @@ -403,7 +404,7 @@ function sinh(x) = // a = cosh(x); // Description: Takes a value `x`, and returns the hyperbolic cosine of it. function cosh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") (exp(x)+exp(-x))/2; @@ -416,7 +417,7 @@ function cosh(x) = // Description: Takes a value `x`, and returns the hyperbolic tangent of it. function tanh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") let (e = exp(2*x) + 1) e == INF ? 1 : (e-2)/e; @@ -428,7 +429,7 @@ function tanh(x) = // a = asinh(x); // Description: Takes a value `x`, and returns the inverse hyperbolic sine of it. function asinh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") ln(x+sqrt(x*x+1)); @@ -440,7 +441,7 @@ function asinh(x) = // a = acosh(x); // Description: Takes a value `x`, and returns the inverse hyperbolic cosine of it. function acosh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") ln(x+sqrt(x*x-1)); @@ -452,7 +453,7 @@ function acosh(x) = // a = atanh(x); // Description: Takes a value `x`, and returns the inverse hyperbolic tangent of it. function atanh(x) = - assert(is_finite(x), "The input must be a finite number.") + assert(is_finite(x), "\nThe input must be a finite number.") ln((1+x)/(1-x))/2; @@ -467,7 +468,7 @@ function atanh(x) = // Description: // Quantize a value `x` to an integer multiple of `y`, rounding to the nearest multiple. // The value of `y` does NOT have to be an integer. If `x` is a list, then every item -// in that list will be recursively quantized. +// in that list is recursively quantized. // Arguments: // x = The value or list to quantize. // y = Positive quantum to quantize to @@ -491,7 +492,7 @@ function atanh(x) = // q = quant([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,12,12,12] // r = 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) && y>0, "The quantum `y` must be a positive value.") + assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.") is_num(x) ? round(x/y)*y : _roundall(x/y)*y; @@ -508,7 +509,7 @@ function _roundall(data) = // Description: // Quantize a value `x` to an integer multiple of `y`, rounding down to the previous multiple. // The value of `y` does NOT have to be an integer. If `x` is a list, then every item in that -// list will be recursively quantized down. +// list is recursively quantized down. // Arguments: // x = The value or list to quantize. // y = Postive quantum to quantize to. @@ -532,7 +533,7 @@ function _roundall(data) = // q = quantdn([9,10,10.4,10.5,11,12],3); // Returns: [9,9,9,9,9,12] // r = 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) && y>0, "The quantum `y` must be a positive value.") + assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.") is_num(x) ? floor(x/y)*y : _floorall(x/y)*y; @@ -549,7 +550,7 @@ function _floorall(data) = // Description: // Quantize a value `x` to an integer multiple of `y`, rounding up to the next multiple. // The value of `y` does NOT have to be an integer. If `x` is a list, then every item in -// that list will be recursively quantized up. +// that list is recursively quantized up. // Arguments: // x = The value or list to quantize. // y = Positive quantum to quantize to. @@ -573,7 +574,7 @@ function _floorall(data) = // q = quantup([9,10,10.4,10.5,11,12],3); // Returns: [9,12,12,12,12,12] // r = 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) && y>0, "The quantum `y` must be a positive value.") + assert( is_finite(y) && y>0, "\nThe quantum `y` must be a positive value.") is_num(x) ? ceil(x/y)*y : _ceilall(x/y)*y; @@ -602,7 +603,7 @@ function _ceilall(data) = // d = constrain(9.1, 0, 9); // Returns: 9 // e = constrain(-0.1, 0, 9); // Returns: 0 function constrain(v, minval, maxval) = - assert( is_finite(v+minval+maxval), "Input must be finite number(s).") + assert( is_finite(v+minval+maxval), "\nInput must be finite number(s).") min(maxval, max(minval, v)); @@ -613,7 +614,7 @@ function constrain(v, minval, maxval) = // Usage: // mod = posmod(x, m) // Description: -// Returns the positive modulo `m` of `x`. Value returned will be satisfy `0 <= mod < m`. +// Returns the positive modulo `m` of `x`. The value returned satisfies `0 <= mod < m`. // Arguments: // x = The value to constrain. // m = Modulo value. @@ -626,7 +627,7 @@ function constrain(v, minval, maxval) = // f = posmod(700,360); // Returns: 340 // g = posmod(3,2.5); // Returns: 0.5 function posmod(x,m) = - assert( is_finite(x) && is_finite(m) && !approx(m,0) , "Input must be finite numbers. The divisor cannot be zero.") + assert( is_finite(x) && is_finite(m) && !approx(m,0) , "\nInput must be finite numbers. The divisor cannot be zero.") (x%m+m)%m; @@ -646,7 +647,7 @@ function posmod(x,m) = // a5 = modang(270); // Returns: -90 // a6 = modang(700); // Returns: -20 function modang(x) = - assert( is_finite(x), "Input must be a finite number.") + assert( is_finite(x), "\nInput must be a finite number.") let(xx = posmod(x,360)) xx<180? xx : xx-360; @@ -660,12 +661,12 @@ function modang(x) = // Takes two angles (degrees) in any range and finds the angle halfway between // the given angles, where halfway is interpreted using the shorter direction. // In the case where the angles are exactly 180 degrees apart, -// it will return `angle1+90`. The returned angle is always in the interval [0,360). +// it returns `angle1+90`. The returned angle is always in the interval [0,360). // Arguments: // angle1 = first angle // angle2 = second angle function mean_angle(angle1,angle2) = - assert(is_vector([angle1,angle2]), "Inputs must be finite numbers.") + assert(is_vector([angle1,angle2]), "\nInputs must be finite numbers.") let( ang1 = posmod(angle1,360), ang2 = posmod(angle2,360) @@ -675,6 +676,62 @@ function mean_angle(angle1,angle2) = : posmod((ang1+ang2-360)/2,360); +// Function: fit_to_range() +// Synopsis: Scale the values in an array to span a range. +// Topics: Math, Bounds, Scaling +// See Also: fit_to_box() +// Usage: +// a = fit_to_range(M, minval, maxval); +// Description: +// Given a vector or list of vectors, scale the values so that they span the full range from `minval` to +// `maxval`. If `minval>maxval`, then the output is a rescaled mirror image of the input. +// Arguments: +// M = vector or list of vectors to scale. A list of vectors needn't be a rectangular matrix; the vectors can have different lengths. +// minval = Minimum value of the rescaled data range. +// maxval = Maximum value of the rescaled data range. +// Example: +// a = [0.0066, 0.194, 0.598, 0.194, 0.0066]; +// v = fit_to_range(a,5,10); +// // Returns: [5, 6.584, 10, 6.584, 5] +// +// b = [ [20,20,0], [40,80,20], [60,40,20] ]; +// m = fit_to_range(b,-10,10); +// // Returns: [[-5,-5,-10], [0,10,-5], [5,0,-5]] +// +// c = [2,3,4,5,6]; +// inv = fit_to_range(c, 20, 8); // inverted range! +// // Returns: [20, 17, 14, 11, 8] +// Example(3D): A texture tile that spans the range [-1,1] is rescaled to span [0,1], resulting in the edges of the texture (which were at z=0) to be raised due to raising the minimu from -1 to 0. +// tex = [ +// [0,0,0, 0, 0, 0,0,0,0], +// [0,1,1, 1, 1, 1,1,1,0], +// [0,1,0, 0, 0, 0,0,1,0], +// [0,1,0,-1,-1,-1,0,1,0], +// [0,1,0,-1, 0,-1,0,1,0], +// [0,1,0,-1,-1,-1,0,1,0], +// [0,1,0, 0, 0, 0,0,1,0], +// [0,1,1, 1, 1, 1,1,1,0], +// [0,0,0, 0, 0, 0,0,0,0] +// ]; +// left(5) textured_tile(tex, +// [9,9,2],tex_reps=1, anchor=BOTTOM); +// right(5) textured_tile(fit_to_range(tex,0,1), +// [9,9,2],tex_reps=1, anchor=BOTTOM); + +function fit_to_range(M, minval, maxval) = + let( + is_vec = is_vector(M), + dum = assert(is_vec || (is_list(M) && is_vector(M[0])), "\nParameter M must be a vector or list of vectors."), + rowlen = len(is_vec ? M : M[0]), + v = is_vec ? M : flatten(M), + a = min(v), + b = max(v) + ) a==b ? M + : is_vec ? add_scalar(add_scalar(M,-a) * ((maxval-minval)/(b-a)), minval) + : [ for(row=M) + add_scalar(add_scalar(row, -a) * ((maxval-minval)/(b-a)), + minval) + ]; + // Section: Operations on Lists (Sums, Mean, Products) @@ -688,7 +745,7 @@ function mean_angle(angle1,angle2) = // Returns the sum of all entries in the given consistent list. // If passed an array of vectors, returns the sum the vectors. // If passed an array of matrices, returns the sum of the matrices. -// If passed an empty list, the value of `dflt` will be returned. +// If passed an empty list, the value of `dflt` is returned. // Arguments: // v = The list to get the sum of. // dflt = The default value to return if `v` is an empty list. Default: 0 @@ -697,7 +754,7 @@ function mean_angle(angle1,angle2) = // sum([[1,2,3], [3,4,5], [5,6,7]]); // returns [9, 12, 15] function sum(v, dflt=0) = v==[]? dflt : - assert(is_consistent(v), "Input to sum is non-numeric or inconsistent") + assert(is_consistent(v), "\nInput to sum is non-numeric or inconsistent.") is_finite(v[0]) || is_vector(v[0]) ? [for(i=v) 1]*v : _sum(v,v[0]*0); @@ -721,7 +778,7 @@ function _sum(v,_total,_i=0) = _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1); // mean([2,3,4]); // returns 3. // mean([[1,2,3], [3,4,5], [5,6,7]]); // returns [3, 4, 5] function mean(v) = - assert(is_list(v) && len(v)>0, "Invalid list.") + assert(is_list(v) && len(v)>0, "\nInvalid list.") sum(v)/len(v); @@ -735,7 +792,7 @@ function mean(v) = // Description: // Returns the median of the given vector. function median(v) = - assert(is_vector(v), "Input to median must be a vector") + assert(is_vector(v), "\nInput to median must be a vector.") len(v)%2 ? max( list_smallest(v, ceil(len(v)/2)) ) : let( lowest = list_smallest(v, len(v)/2 + 1), max = max(lowest), @@ -762,7 +819,7 @@ function median(v) = // deltas([2,5,9,17]); // returns [3,4,8]. // deltas([[1,2,3], [3,6,8], [4,8,11]]); // returns [[2,4,5], [1,2,3]] function deltas(v, wrap=false) = - assert( is_consistent(v) && len(v)>1 , "Inconsistent list or with length<=1.") + assert( is_consistent(v) && len(v)>1 , "\nInconsistent list or with length<=1.") [for (p=pair(v,wrap)) p[1]-p[0]] ; @@ -784,7 +841,7 @@ function deltas(v, wrap=false) = // cumsum([[1,2,3], [3,4,5], [5,6,7]]); // returns [[1,2,3], [4,6,8], [9,12,15]] function cumsum(v) = v==[] ? [] : - assert(is_consistent(v), "The input is not consistent." ) + assert(is_consistent(v), "\nThe input is not consistent." ) [for (a = v[0], i = 1 ; @@ -822,7 +879,7 @@ function product(list,right=true) = if (i==len(list)) a][0] : assert(is_vector(list) || (is_matrix(list[0],square=true) && is_consistent(list)), - "Input must be a vector, a list of vectors, or a list of matrices.") + "\nInput must be a vector, a list of vectors, or a list of matrices.") [for (a = list[0], i = 1 ; @@ -842,8 +899,8 @@ function product(list,right=true) = // Description: // Returns a list where each item is the cumulative product of all items up to and including the corresponding entry in the input list. // If passed an array of vectors, returns a list of elementwise vector products. If passed a list of square matrices by default returns matrix -// products multiplying on the left, so a list `[A,B,C]` will produce the output `[A,BA,CBA]`. If you set `right=true` then it returns -// the product of multiplying on the right, so a list `[A,B,C]` will produce the output `[A,AB,ABC]` in that case. +// products multiplying on the left, so a list `[A,B,C]` produces the output `[A,BA,CBA]`. If you set `right=true` then it returns +// the product of multiplying on the right, so a list `[A,B,C]` produces the output `[A,AB,ABC]` in that case. // Arguments: // list = The list to get the cumulative product of. // right = if true multiply matrices on the right @@ -865,7 +922,7 @@ function cumprod(list,right=false) = a] : assert(is_vector(list) || (is_matrix(list[0],square=true) && is_consistent(list)), - "Input must be a listector, a list of listectors, or a list of matrices.") + "\nInput must be a listector, a list of listectors, or a list of matrices.") [for (a = list[0], i = 1 ; @@ -903,7 +960,7 @@ function convolve(p,q) = p==[] || q==[] ? [] : assert( (is_vector(p) || is_matrix(p)) && ( is_vector(q) || (is_matrix(q) && ( !is_vector(p[0]) || (len(p[0])==len(q[0])) ) ) ) , - "The inputs should be vectors or paths all of the same dimension.") + "\nThe inputs should be vectors or paths all of the same dimension.") let( n = len(p), m = len(q)) [for(i=[0:n+m-2], k1 = max(0,i-n+1), k2 = min(i,m-1) ) @@ -959,8 +1016,8 @@ function sum_of_sines(a, sines) = // ints = rand_int(0,100,3); // int = rand_int(-10,10,1)[0]; function rand_int(minval, maxval, n, seed=undef) = - assert( is_finite(minval+maxval+n) && (is_undef(seed) || is_finite(seed) ), "Input must be finite numbers.") - assert(maxval >= minval, "Max value cannot be smaller than minval") + assert( is_finite(minval+maxval+n) && (is_undef(seed) || is_finite(seed) ), "\nInput must be finite numbers.") + assert(maxval >= minval, "\nMax value cannot be smaller than minval.") let (rvect = is_def(seed) ? rands(minval,maxval+1,n,seed) : rands(minval,maxval+1,n)) [for(entry = rvect) floor(entry)]; @@ -981,9 +1038,9 @@ function rand_int(minval, maxval, n, seed=undef) = // scale = the scale of the point coordinates. Default: 1 // seed = an optional seed for the random generation. function random_points(n, dim, scale=1, seed) = - assert( is_int(n) && n>=0, "The number of points should be a non-negative integer.") - assert( is_int(dim) && dim>=1, "The point dimensions should be an integer greater than 1.") - assert( is_finite(scale) || is_vector(scale,dim), "The scale should be a number or a vector with length equal to d.") + assert( is_int(n) && n>=0, "\nThe number of points should be a non-negative integer.") + assert( is_int(dim) && dim>=1, "\nThe point dimensions should be an integer greater than 1.") + assert( is_finite(scale) || is_vector(scale,dim), "\nThe scale should be a number or a vector with length equal to d.") let( rnds = is_undef(seed) ? rands(-1,1,n*dim) @@ -1011,18 +1068,18 @@ function gaussian_rands(n=1, mean=0, cov=1, seed=undef) = let( dim = is_num(mean) ? 1 : len(mean) ) - assert((dim==1 && is_num(cov)) || is_matrix(cov,dim,dim),"mean and covariance matrix not compatible") + assert((dim==1 && is_num(cov)) || is_matrix(cov,dim,dim),"\nmean and covariance matrix not compatible.") assert(is_undef(seed) || is_finite(seed)) let( nums = is_undef(seed)? rands(0,1,dim*n*2) : rands(0,1,dim*n*2,seed), rdata = [for (i = count(dim*n,0,2)) sqrt(-2*ln(nums[i]))*cos(360*nums[i+1])] ) dim==1 ? add_scalar(sqrt(cov)*rdata,mean) : - assert(is_matrix_symmetric(cov),"Supplied covariance matrix is not symmetric") + assert(is_matrix_symmetric(cov),"\nSupplied covariance matrix is not symmetric.") let( L = cholesky(cov) ) - assert(is_def(L), "Supplied covariance matrix is not positive definite") + assert(is_def(L), "\nSupplied covariance matrix is not positive definite.") move(mean,list_to_matrix(rdata,dim)*transpose(L)); @@ -1117,7 +1174,7 @@ function random_polygon(n=3,size=1, seed) = // 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 +// between data[i+1] and data[i], and the data values are linearly resampled at each corner // to produce a uniform spacing for the derivative estimate. At the endpoints a single point method // is used: f'(t) = (f(t+h)-f(t))/h. // Arguments: @@ -1287,7 +1344,7 @@ function complex(list) = // c = c_mul(z1,z2) // Description: // Multiplies two complex numbers, vectors or matrices, where complex numbers -// or entries are represented as vectors: [REAL, IMAGINARY]. Note that all +// or entries are represented as vectors: [REAL, IMAGINARY]. All // entries in both arguments must be complex. // Arguments: // z1 = First complex number, vector or matrix @@ -1411,7 +1468,7 @@ function c_norm(z) = norm_fro(z); // roots = quadratic_roots(a, b, c, [real]) // Description: // Computes roots of the quadratic equation a*x^2+b*x+c==0, where the -// coefficients are real numbers. If real is true then returns only the +// coefficients are real numbers. If real is true, then returns only the // real roots. Otherwise returns a pair of complex values. This method // may be more reliable than the general root finder at distinguishing // real roots from complex roots. @@ -1491,7 +1548,7 @@ function poly_mult(p,q) = // Computes division of the numerator polynomial by the denominator polynomial and returns // a list of two polynomials, [quotient, remainder]. If the division has no remainder then // the zero polynomial [0] is returned for the remainder. Similarly if the quotient is zero -// the returned quotient will be [0]. +// the returned quotient is [0]. function poly_div(n,d) = assert( is_vector(n) && is_vector(d) , "Invalid polynomials." ) let( d = _poly_trim(d), @@ -1664,7 +1721,7 @@ function real_roots(p,eps=undef,tol=1e-14) = // argument. You must have a version of OpenSCAD that supports function literals // (2021.01 or newer). The tolerance (tol) specifies the accuracy of the solution: // abs(f(x)) < tol * yrange, where yrange is the range of observed function values. -// This function can only find roots that cross the x axis: it cannot find the +// This function can find only those roots that *cross* the x axis: it cannot find the // the root of x^2. // Arguments: // f = function literal for a scalar-valued single variable function diff --git a/shapes3d.scad b/shapes3d.scad index 1c51a0a2..ab8b29cf 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -1175,7 +1175,7 @@ function regular_prism(n, // Synopsis: Creates a cube or trapezoidal prism with a textured top face for attaching to objects. // SynTags: Geom, VNF // Topics: Shapes (3D), Attachable, VNF Generators, Textures -// See Also: cuboid(), prismoid(), texture(), cyl(), rotate_sweep(), linear_sweep() +// See Also: cuboid(), prismoid(), texture(), cyl(), rotate_sweep(), linear_sweep(), plot3d() // Usage: // textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [diff=], [tex_extra=], [tex_skip=], ...) [ATTACHMENTS]; // vnf = textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [tex_extra=], [tex_skip=], ...); @@ -1190,6 +1190,10 @@ function regular_prism(n, // then the texture actually sinks into its base, so the default is set to the 0.1 more than the inset depth. To ensure a valid geometry, with a positive // `inset` or a texture that has negative values you must select a thickness strictly **larger** than the depth the texture extends below zero. // . +// Textures are meant to be between 0 and 1 so that `tex_depth` and `tex_inset` behave as expected. If you have a custom textures that +// has a different range you can still use it directly, but you may find it more convenient to rescale a height map texture using {{fit_to_range()}] +// or a VNF texture using {{fit_to_box()}}. +// . // You can also specify a trapzoidal prism using parameters equivalent to those accepted by {{trapezoid()}}, with one change: // `ysize` specifies the width of the prism in the Y direction, and `h`, `height` or `thickness` are used to specify the height // in the Z direction. When you texture a trapezoid the texture will be scaled to the `w1` length if you specify it by size using `tex_size`. The @@ -1198,7 +1202,7 @@ function regular_prism(n, // Two anchor types are available. The default atype is "tex" which assumes you want to place the texture on another object using // {{attach()}}. It provides anchors that ignore the base object and place the BOTTOM anchor at the bottom of the texture. The TOP anchor // will be at the top face of the texture. Note that if your texture doesn't span the range from [0,1] these anchors won't be correctly located. -// For an inset texture, the "tex" anchors are all at the top of the texture. This anchor type works with `anchor(face,BOT)` where `face` is some +// For an inset texture, the "tex" anchors are all at the top of the texture. This anchor type works with `attach(face,BOT)` where `face` is some // face on a parent object that needs a texture. If you want to use the textured object directly the "std" anchors are probably more useful. // These anchors are the usual anchors for the base object, ignoring the applied texture. If you want the anchors to be on top of the texture, // set `tex_inset=true`. @@ -1240,33 +1244,37 @@ function regular_prism(n, // tex_extra = number of extra lines of a hightfield texture to add at the end. Can be a scalar or 2-vector to give x and y values. Default: 0 if `tex_reps=[1,1]`, 1 otherwise // tex_skip = number of lines of a heightfield texture to skip when starting. Can be a scalar or two vector to give x and y values. Default: 0 // style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "min_edge" -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `BOTTOM` if `astyle` is "tex", `CENTER` otherwise // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` // Example(3D,NoScales,VPT=[-0.257402,0.467403,-0.648606],VPR=[46.6,0,16.6],VPD=29.2405): Basic textured tile // textured_tile("trunc_diamonds", 10, tex_reps=[5,5]); // Example(3D,NoAxes,VPT=[-0.0852782,0.259593,0.139667],VPR=[58.5,0,345.1],VPD=36.0994): Attaching a tile to a cube // cuboid([12,12,4]) attach(TOP,BOT) -// textured_tile("trunc_pyramids", 10, tex_reps=[5,5], style="convex"); +// textured_tile("trunc_pyramids", 10, tex_reps=[5,5], +// style="convex"); // Example(3D,NoScales,VPT = [-0.0788193, 0.10015, -0.0938629], VPR = [57.8, 0, 34.1], VPD = 29.2405): This inset texture doesn't look obviously different, but you can see that the object is below the XY plane. -// textured_tile("trunc_pyramids_vnf", 10, tex_reps=[5,5], tex_inset=true); +// textured_tile("trunc_pyramids_vnf", 10, tex_reps=[5,5], +// tex_inset=true); // Example(3D,NoAxes,VPT=[0.242444,0.170054,-0.0714754],VPR=[67.6,0,33.4],VPD=36.0994): Here we use the `diff` option combined with {{diff()}} to attach the inset texture to the front of a parent cuboid. // diff() // cuboid([12,5,10]) attach(FRONT, BOT) // textured_tile("trunc_pyramids_vnf", [10,8], // tex_reps=[5,5], tex_inset=true, diff=true); // Example(3D,NoAxes,VPT=[5.86588,-0.107082,-0.311155],VPR=[17.2,0,9.6],VPD=32.4895): Tile shaped like a rhombic prism -// textured_tile("ribs", w1=10, w2=10, shift=4, ysize=7, tex_reps=[5,1]); +// textured_tile("ribs", w1=10, w2=10, shift=4, ysize=7, +// tex_reps=[5,1]); // Example(3D,NoAxes,VPT=[-0.487417,-0.398897,-0.143258],VPR=[10.2,0,12.4],VPD=26.3165): A tile shaped like a trapezoidal prism. Note that trapezoidal tiles will always distort the texture, resulting in curves // textured_tile("diamonds", w1=10, w2=7, ysize=7, tex_reps=5); // Example(3D,NoAxes,VPT=[-0.0889877,-0.31974,0.554444],VPR=[22.1,0,22.2],VPD=32.4895): An inset trapezoidal tile placed into a cube // diff()cuboid([10,10,2]) // attach(TOP,BOT) -// textured_tile("trunc_diamonds", tex_reps=[5,5], tex_inset=true, -// w1=8, w2=4, ysize=8, diff=true); +// textured_tile("trunc_diamonds", tex_reps=[5,5], +// tex_inset=true, diff=true, +// w1=8, w2=4, ysize=8); // Example(3D,NoAxes,VPT=[-0.0889877,-0.31974,0.554444],VPR=[58.5,0,21.5],VPD=32.4895): This example shows what happens if you set `tex_extra` to zero for the "pyramids" texture. Note that the texture doesn't finish. The default of `tex_extra=1` produces the correct result. // textured_tile("pyramids", 10, tex_reps=[5,5], tex_extra=0); -// Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): This texture has an asymmetry even with the default `tex_extra=1`. +// Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): This texture has an asymmetry with the default `tex_extra=1`. // textured_tile("trunc_ribs", 10, tex_reps=[5,1]); // Example(3D,NoAxes,VPT=[-0.212176,-0.651766,0.124004],VPR=[58.5,0,21.5],VPD=29.2405): It could be fixed by setting `tex_extra=2`, which would place an extra flat strip on the right. But another option is to use the `tex_skip` parameter to trim the flat part from the left. Note that we are also skipping in the y direction, but it doesn't make a difference for this texture, except that you need to have enough texture tiles to accommodate the skip, so we increased the Y reps value to 2. You can also set `tex_skip` to a vector. // textured_tile("trunc_ribs", 10, tex_reps=[5,2], tex_skip=1); @@ -1303,10 +1311,10 @@ module textured_tile( tex_skip=0, style="min_edge", atype="tex", - anchor=CENTER, spin=0, orient=UP + anchor, spin=0, orient=UP ) { - + anchor = default(anchor, atype=="tex" ? BOTTOM : CENTER); vnf_data = textured_tile(size=size, ysize=ysize, height=height, w1=w1, w2=w2, ang=ang, h=h, shift=shift, texture=texture, tex_size=tex_size, tex_reps=tex_reps,tex_extra=tex_extra, @@ -1354,15 +1362,17 @@ function textured_tile( atype="tex", tex_extra, tex_skip=0, - anchor=CENTER, spin=0, orient=UP, + anchor, spin=0, orient=UP, _return_anchor=false -) = +) = + assert(in_list(atype,["tex","std"]), "atype must be \"tex\" or \"std\"") assert(is_undef(tex_reps) || is_int(tex_reps) || (all_integer(tex_reps) && len(tex_reps)==2), "tex_reps must be an integer or list of two integers") assert(is_undef(tex_size) || is_vector(tex_size,2) || is_finite(tex_size)) assert(num_defined([tex_size, tex_reps])==1, "Must give exactly one of tex_size and tex_reps") assert(is_undef(size) || is_num(size) || is_vector(size,2) || is_vector(size,3), "size must be a 2-vector or 3-vector") assert(is_undef(size) || num_defined([ysize,h, height, thickness, w1,w2,ang])==0, "Cannot combine size with any other dimensional specifications") let( + anchor = default(anchor, atype=="tex" ? BOTTOM : CENTER), inset = is_num(tex_inset)? tex_inset : tex_inset? 1 : 0, default_thick = inset>0 ? 0.1+abs(tex_depth)*inset : 0.1, extra_ht = max(0,abs(tex_depth)*(1-inset)), @@ -1388,8 +1398,8 @@ function textured_tile( tex_reps = is_def(tex_reps) ? force_list(tex_reps,2) : let(tex_size=force_list(tex_size,2)) [round(size.x/tex_size.x), round(size.y/tex_size.y)], - extra = is_undef(extra)? tex_reps == [1,1] ? [0,0] : [1,1] - : force_list(tex_extra,2), + extra = is_undef(tex_extra)? tex_reps == [1,1] ? [0,0] : [1,1] + : force_list(tex_extra,2), skip = force_list(tex_skip,2), scale = [size.x/tex_reps.x, size.y/tex_reps.y], setz=function (v,z) [v.x,v.y,z], @@ -1405,7 +1415,7 @@ function textured_tile( scaled_tex = tex_depth < 0 ? [for(row=texture) [for(p=row) -(1-p-inset)*tex_depth]] : [for(row=texture) [for(p=row) (p-inset)*tex_depth]], check = [for(row=scaled_tex, p=row) if (p<=-height) p], - dummy=assert(check==[], str("texture extends too far below zero (",min([each check,0]),") to fit in cube with height ",height)), + dummy=assert(check==[], str("texture extends too far below zero (",min([each check,0]),") to fit entirely within height ",height)), pts=[for(y=idx(ypts)) [ [xpts[0],ypts[y],-height/2], for(x=idx(xpts)) @@ -1418,9 +1428,12 @@ function textured_tile( : let( zadj_vnf = [ - [for(p=texture[0]) [p.x, p.y, tex_depth<0 ? height/2-(1-p.z-inset)*tex_depth : height/2+(p.z-inset)*tex_depth]], + [for(p=texture[0]) [p.x, p.y, height/2 + _tex_height(tex_depth,inset,p.z)]], texture[1] ], + minz = min(column(zadj_vnf[0],2)), + dummy=assert(minz>-height/2, str("texture extends too far below zero (",minz-height/2,") to fit entirely within height ",height)), + scaled_vnf = scale(scale, zadj_vnf), tiled_vnf = [for(i=[0:1:tex_reps.x-1], j=[0:1:tex_reps.y-1]) move([scale.x*i,scale.y*j], scaled_vnf)], @@ -1802,12 +1815,12 @@ function rect_tube( // wedge([20, 40, 15], center=true); // Example: *Non*-Centered // wedge([20, 40, 15]); -// Example: Standard Anchors +// Example(3D,Med,VPR=[59.50,0.00,36.90],VPD=257.38,VPT=[5.60,-1.98,3.65]): Standard Anchors // wedge([40, 80, 30], center=true) // show_anchors(custom=false); // color([0.5,0.5,0.5,0.1]) // cube([40, 80, 30], center=true); -// Example: Named Anchors +// Example(3D,Med,VPR=[55.00,0.00,25.00],VPD=151.98,VPT=[2.30,-11.81,-5.66]): Named Anchors // wedge([40, 80, 30], center=true) // show_anchors(std=false); @@ -4286,7 +4299,7 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor // Synopsis: Generates a surface by evaluating a function on a 2D grid // SynTags: Geom, VNF // Topics: Function Plotting -// See Also: plot_revolution() +// See Also: plot_revolution(), textured_tile() // Usage: As Module // plot3d(f, x, y, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS]; // Usage: As Function diff --git a/skin.scad b/skin.scad index 85719800..b784ad59 100644 --- a/skin.scad +++ b/skin.scad @@ -949,7 +949,12 @@ function linear_sweep( // Anchor Types: // "hull" = Anchors to the virtual convex hull of the shape. // "intersect" = Anchors to the surface of the shape. -// Example: +// Example(3D,NoAxes,VPR=[60.20,0.00,41.80],VPD=151.98,VPT=[0.85,-2.95,3.10]): Sweeping a shape that looks like a plus sign +// rgn = right(30, +// union([for (a = [0, 90]) +// zrot(a, rect([15,5]))])); +// rotate_sweep(rgn); +// Example(3D,NoAxes,VPR=[50.40,0.00,28.50],VPD=208.48,VPT=[0.23,-1.89,5.20]): Sweeping a region with multiple components // rgn = [ // for (a = [0, 120, 240]) let( // cp = polar_to_xy(15, a) + [30,0] @@ -959,27 +964,35 @@ function linear_sweep( // ] // ]; // rotate_sweep(rgn, angle=240); -// Example: -// rgn = right(30, p=union([for (a = [0, 90]) rot(a, p=rect([15,5]))])); -// rotate_sweep(rgn); // Example(3D,NoAxes,VPR=[55.00,0.00,25.00],VPD=292.71,VPT=[1.59,1.80,-1.35]): Torus with bricks texture // path = right(50, p=circle(d=40)); // rotate_sweep(path, texture="bricks_vnf",tex_size=10, // tex_depth=0.5, style="concave"); +// Example(3D,NoAxes,VPR=[76.30,0.00,44.60],VPD=257.38,VPT=[2.58,-5.21,0.37]): Applying a texture to a region. Both the inside and outside receive texture. +// rgn = [ +// right(40, p=circle(d=50)), +// right(40, p=circle(d=40,$fn=6)), +// ]; +// rotate_sweep( +// rgn, texture="diamonds", +// tex_size=[10,10], tex_depth=1, +// angle=240, style="concave"); // Example(NoAxes): The simplest way to create a cylinder with just a single line segment and `caps=true`. With this cylinder, the top and bottom have no texture. // rotate_sweep([[20,-10],[20,10]], texture="dots", // tex_reps=[6,2],caps=true); // Example(NoAxes): If we manually connect the top and bottom then they also receive texture. -// rotate_sweep([[0,-10],[20,-10],[20,10],[0,10]], texture="dots", -// tex_reps=[6,6],tex_depth=1.5); +// rotate_sweep([[0,-10],[20,-10],[20,10],[0,10]], +// tex_reps=[6,6],tex_depth=1.5, +// texture="dots"); // Example(NoAxes,VPR=[95.60,0.00,69.80],VPD=74.40,VPT=[5.81,5.74,1.97]): You can connect just the top or bottom alone instead of both to get texture on one and a flat cap on the other. Here you can see that the sloped top has texture but the bottom does not. Also note that the texture doesn't fit neatly on the side and top like it did in the previous two examples, but makes a somewhat ugly transition across the corner. You have to size your object carefully so that the tops and sides each fit an integer number of texture tiles to avoid this type of transition. -// rotate_sweep([[15,-10],[15,10],[0,15]], texture="dots", tex_reps=[6,6], +// rotate_sweep([[15,-10],[15,10],[0,15]], +// texture="dots", tex_reps=[6,6], // angle=90,caps=true,tex_depth=1.5); // Example(NoAxes,VPR=[55.00,0.00,25.00],VPD=126.00,VPT=[1.37,0.06,-0.75]): Ribbed sphere. // path = arc(r=20, $fn=64, angle=[-90, 90]); // rotate_sweep(path, 360, texture = texture("wave_ribs",n=15), // tex_size=[8,1.5]); -// Example: For this model we use `closed=false` to create the flat, untextured caps. +// Example(3D,NoAxes,VPR=[60.20,0.00,56.50],VPD=231.64,VPT=[4.18,-2.66,1.31]): This model uses `caps=true` to create the untextured caps with a user supplied texture. They are flat because the texture is zero at its edges. // tex = [ // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], @@ -999,7 +1012,7 @@ function linear_sweep( // path, caps=true, // texture=tex, tex_size=[20,20], // tex_depth=1, style="concave"); -// Example: +// Example(3D,NoAxes,VPR=[60.20,0.00,56.50],VPD=187.63,VPT=[2.07,-4.53,2.58]): An example with a more complicated path. Here the caps are not flat because the diamonds texture is not zero at the edges. // bezpath = [ // [15, 30], [10,15], // [10, 0], [20, 10], [30,12], @@ -1011,28 +1024,7 @@ function linear_sweep( // path, caps=true, // texture="diamonds", tex_size=[10,10], // tex_depth=1, style="concave"); -// Example: -// path = [ -// [20, 30], [20, 20], -// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]), -// [20,-20], [20,-30], -// ]; -// vnf = rotate_sweep( -// path, caps=true, -// texture="trunc_pyramids", -// tex_size=[5,5], tex_depth=1, -// style="convex"); -// vnf_polyhedron(vnf, convexity=10); -// Example: -// rgn = [ -// right(40, p=circle(d=50)), -// right(40, p=circle(d=40,$fn=6)), -// ]; -// rotate_sweep( -// rgn, texture="diamonds", -// tex_size=[10,10], tex_depth=1, -// angle=240, style="concave"); -// Example(3D,NoAxes): Tapering the ends of the texturing to zero produces flat caps. +// Example(3D,NoAxes,VPR=[70.00,0.00,58.60],VPD=208.48,VPT=[1.92,-3.81,2.21]): The normal direction at the ends is perpendicular to the Z axis, so even though the texture is not zero, the caps are flat, unlike the previous example. // path = [ // [20, 30], [20, 20], // each arc(r=20, corner=[[20,20],[10,0],[20,-20]]), @@ -1042,16 +1034,15 @@ function linear_sweep( // path, caps=true, // texture="diamonds", // tex_size=[5,5], tex_depth=1, -// tex_taper=undef, -// style="flip2", +// style="concave", // convexity=10); -// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): The top cap is definite not flat. +// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): The top cap is definitely not flat. // rotate_sweep( // arc(r=20,angle=[-45,45],n=45), // caps=true, texture="diamonds", // tex_size=[5,5], tex_depth=2, // convexity=10); -// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): Setting `tex_taper=0` abruptly tapers right the the caps so that the cap is flat: +// Example(3D,NoAxes,VPR=[59.20,0.00,226.90],VPD=113.40,VPT=[-4.53,3.03,3.84]): Setting `tex_taper=0` abruptly tapers right at the caps so that the cap is flat: // rotate_sweep( // arc(r=20,angle=[-45,45],n=45), // caps=true, texture="diamonds", diff --git a/vectors.scad b/vectors.scad index 1c665884..b19608e7 100644 --- a/vectors.scad +++ b/vectors.scad @@ -380,31 +380,6 @@ function vector_perp(v,w) = // Section: Vector Searching -// Function: pointlist_bounds() -// Synopsis: Returns the min and max bounding coordinates for the given list of points. -// Topics: Geometry, Bounding Boxes, Bounds -// See Also: closest_point(), furthest_point(), vnf_bounds() -// Usage: -// pt_pair = pointlist_bounds(pts); -// Description: -// Finds the bounds containing all the points in `pts` which can be a list of points in any dimension. -// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with -// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]` -// Arguments: -// pts = List of points. -function pointlist_bounds(pts) = - assert(is_path(pts,dim=undef,fast=true) , "\nInvalid pointlist." ) - let( - select = ident(len(pts[0])), - spread = [ - for(i=[0:len(pts[0])-1]) - let( spreadi = pts*select[i] ) - [ min(spreadi), max(spreadi) ] - ] - ) transpose(spread); - - - // Function: closest_point() // Synopsis: Finds the closest point in a list of points. // Topics: Geometry, Points, Distance @@ -417,7 +392,7 @@ 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), "\nInvalid point." ) + assert(is_vector(pt), "\nInvalid point." ) assert(is_path(points,dim=len(pt)), "\nInvalid pointlist or incompatible dimensions." ) min_index([for (p=points) norm(p-pt)]); @@ -687,4 +662,97 @@ function _insert_many(list, k, newlist,i=0) = +// Section: Bounds + + +// Function: pointlist_bounds() +// Synopsis: Returns the min and max bounding coordinates for the given list of points. +// Topics: Geometry, Bounding Boxes, Bounds, Scaling +// See Also: closest_point(), furthest_point(), vnf_bounds() +// Usage: +// pt_pair = pointlist_bounds(pts); +// Description: +// Finds the bounds containing all the points in `pts`, which can be a list of points in any dimension. +// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with +// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]` +// Arguments: +// pts = List of points. +function pointlist_bounds(pts) = + assert(is_path(pts,dim=undef,fast=true) , "\nInvalid pointlist." ) + let( + select = ident(len(pts[0])), + spread = [ + for(i=[0:len(pts[0])-1]) + let( spreadi = pts*select[i] ) + [ min(spreadi), max(spreadi) ] + ] + ) transpose(spread); + + +// Function: fit_to_box() +// Synopsis: Scale the x, y, and/or z coordinantes of a list of points to span a range. +// Topics: Geometry, Bounding Boxes, Bounds, VNF Manipulation +// See Also: fit_to_range() +// Usage: +// new_pts = fit_to_box(pts, [x=], [y=], [z=]); +// new_vnf = fit_to_box(vnf, [x=], [y=], [z=]); +// Description: +// Given a list of 2D or 3D points, or a VNF structure, rescale and position one or more of the coordinates +// to fit within specified ranges. At least one range (`x`, `y`, or `z`) must be specified. A normal use case +// for this function is to rescale a VNF texture to fit within `0 <= z <= 1`. +// . +// While a range is typically `[min_value,max_value]`, the minimum and maximum values can be reversed, +// resulting in new coordinates being a rescaled mirror image of the original coordinates. +// Arguments: +// pts = List of points, or a VNF structure. +// x = `[min,max]` of rescaled x coordinates. Default: undef +// y = `[min,max]` of rescaled y coordinates. Default: undef +// z = `[min,max]` of rescaled z coordinates. Default: undef +// Example(2D): A 2D bezier path (red) rescaled (blue) to fit in a square box centered on the origin. +// bez = [ +// [10,60], [-5,30], +// [20,60], [50,50], [100,30], +// [50,30], [70,20] +// ]; +// path = bezpath_curve(bez); +// newpath = fit_to_box(path, x=[0,40], y=[0,40]); +// stroke(path, width=2, color="red"); +// stroke(square(40), width=1, closed=true); +// stroke(newpath, width=2, color="blue"); +// Example(3D): A prismoid (left) is rescaled to fit new x and z bounds. The z bounds minimum and maximum values are reversed, resulting in the new object on the right having inverted z coordinates. +// vnf = prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]); +// vnf_boxed = fit_to_box(vnf, x=[30,55], z=[5,-15]); +// vnf_polyhedron(vnf); +// vnf_polyhedron(vnf_boxed); +function fit_to_box(pts, x, y, z) = + assert(is_path(pts) || is_vnf(pts), "\npts must be a valid 2D or 3D path, or a VNF structure.") + assert(any_defined([x,y,z]), "\nAt least one [min,max] range x, y, or z must be defined.") + assert(is_undef(x) || is_vector(x,2), "\nx must be a 2-vector [min,max].") + assert(is_undef(y) || is_vector(y,2), "\nx must be a 2-vector [min,max].") + assert(is_undef(z) || is_vector(z,2), "\nx must be a 2-vector [min,max].") + let( + isvnf = is_vnf(pts), + p = isvnf ? pts[0] : pts, + bounds = isvnf ? vnf_bounds(pts) : pointlist_bounds(pts), + dim = len(bounds[0]), + err = assert(is_undef(z) || (dim>2 && is_def(z)), "\n2D data detected with z range specified."), + whichdim = [is_def(x), is_def(y), is_def(z)], + xmin = bounds[0][0], + ymin = bounds[0][1], + zmin = dim>2 ? bounds[0][2] : 0, + // new scales + xscale = whichdim.x ? (x[1]-x[0]) / (bounds[1][0]-xmin) : 1, + yscale = whichdim.y ? (y[1]-y[0]) / (bounds[1][1]-ymin) : 1, + zscale = whichdim.z ? (z[1]-z[0]) / (bounds[1][2]-zmin) : 1, + // new offsets + xo = whichdim.x ? x[0] : 0, + yo = whichdim.y ? y[0] : 0, + zo = whichdim.z ? z[0] : 0, + // shift original min to 0, rescale to new scale, shift back to new min + newpts = move(dim>2 ? [xo,yo,zo] : [xo,yo], + scale(dim>2 ? [xscale,yscale,zscale] : [xscale,yscale], + move(dim>2 ? -[xmin,ymin,zmin] : -[xmin,ymin], pts))) + ) isvnf ? [newpts[0], pts[1]] : newpts; + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap