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/vectors.scad b/vectors.scad index 1c665884..23877261 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,88 @@ 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(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 ? vnf_vertices(pts) : pts, + dim = len(p[0]), + dum = assert(dim<3 || (dim==3 && is_def(z)), "\n2D data detected with z range specified."), + whichdim = [is_def(x), is_def(y), is_def(z)], // extract only the columns needed + xcol = whichdim.x ? column(p,0) : [0], + ycol = whichdim.y ? column(p,1) : [0], + zcol = whichdim.z ? column(p,2) : [0], + xmin = min(xcol), + ymin = min(ycol), + zmin = min(zcol), + // new scales + xscale = whichdim.x ? (x[1]-x[0]) / (max(xcol)-xmin) : 1, + yscale = whichdim.y ? (y[1]-y[0]) / (max(ycol)-ymin) : 1, + zscale = whichdim.z ? (z[1]-z[0]) / (max(zcol)-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