mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-08-16 13:04:16 +02:00
Merge pull request #1739 from amatulic/general_dev
Added simplify_path(), slerp(), slerpn(), improved random_polygon(), minor wordsmithing of docs
This commit is contained in:
166
math.scad
166
math.scad
@@ -121,13 +121,13 @@ function lerp(a,b,u) =
|
||||
// x = lerpn(a, b, n, [endpoint]);
|
||||
// Description:
|
||||
// Returns exactly `n` values, linearly interpolated between `a` and `b`.
|
||||
// If `endpoint` is true, then the last value will exactly equal `b`.
|
||||
// If `endpoint` is false, then the last value will be `a+(b-a)*(1-1/n)`.
|
||||
// If `endpoint` is true, then the last value equals `b`.
|
||||
// If `endpoint` is false, then the last value is `a+(b-a)*(1-1/n)`.
|
||||
// Arguments:
|
||||
// a = First value or vector.
|
||||
// b = Second value or vector.
|
||||
// n = The number of values to return.
|
||||
// endpoint = If true, the last value will be exactly `b`. If false, the last value will be one step less.
|
||||
// endpoint = If true, the last value equals `b`. If false, the last value is one step less. Default: true
|
||||
// Example:
|
||||
// l = lerpn(-4,4,9); // Returns: [-4,-3,-2,-1,0,1,2,3,4]
|
||||
// l = lerpn(-4,4,8,false); // Returns: [-4,-3,-2,-1,0,1,2,3]
|
||||
@@ -140,6 +140,7 @@ function lerpn(a,b,n,endpoint=true) =
|
||||
let( d = n - (endpoint? 1 : 0) )
|
||||
[for (i=[0:1:n-1]) let(u=i/d) (1-u)*a + u*b];
|
||||
|
||||
|
||||
// Function: bilerp()
|
||||
// Synopsis: Bi-linear interpolation between four values
|
||||
// Topics: Interpolation, Math
|
||||
@@ -173,11 +174,78 @@ function lerpn(a,b,n,endpoint=true) =
|
||||
// x = First proportional distance
|
||||
// y = Second proportional distance
|
||||
|
||||
|
||||
function bilerp(points,x,y) =
|
||||
[1,y,x,x*y]*[[1, 0, 0, 0],[-1, 0, 1, 0],[-1,1,0,0],[1,-1,-1,1]]*points;
|
||||
|
||||
|
||||
// Function: slerp()
|
||||
// Synopsis: Spherical lerp(), great-circle interpolation on a unit sphere.
|
||||
// Topics: Interpolation, Math
|
||||
// See Also: slerpn(), lerp()
|
||||
// Usage:
|
||||
// interp_vector = slerp(v1, v2, u);
|
||||
// Description:
|
||||
// Given two points on a sphere represented by 3-vectors from the sphere's center, return a vector that is interpolated along a great-circle arc between the original two points. The input vectors need not be unit size, but the output vectors correspond to a unit sphere, so you should scale the result back to your original radius.
|
||||
// * If `u` is given as a number, returns the single interpolated value.
|
||||
// * If `u` is 0.0, then the unit-size value of `v1` is returned.
|
||||
// * If `u` is 1.0, then the unit-size value of `v2` is returned.
|
||||
// * If `u` is a range, or list of numbers, returns a list of interpolated values.
|
||||
// Arguments:
|
||||
// v1 = First 3D vector, needn't be unit size.
|
||||
// v2 = Second 3D vector, needn't be unit size.
|
||||
// u = The proportion from `v1` to `v2` to calculate. Standard range is 0.0 to 1.0, inclusive. If given as a list or range of values, returns a list of results.
|
||||
function slerp(v1, v2, u) =
|
||||
assert(is_vector(v1,3) && is_vector(v2,3), "\nv1 and v2 must be 3-vectors.")
|
||||
let(
|
||||
a = unit(v1),
|
||||
b = unit(v2),
|
||||
theta = acos(max(-1, min(1, a*b))),
|
||||
err = assert(abs(theta-180)>EPSILON, "\nNo solution when vectors v1 and v2 are 180° apart."),
|
||||
sin_theta = sin(theta)
|
||||
) sin_theta < EPSILON ? unit(a+b) // fallback
|
||||
: is_finite(u) ? (sin_theta < EPSILON ? unit(a+b)
|
||||
: (a * sin((1 - u) * theta) + b * sin(u * theta)) / sin_theta)
|
||||
: [for(t=u) sin_theta < EPSILON ? unit(a+b)
|
||||
: (a * sin((1 - t) * theta) + b * sin(t * theta)) / sin_theta];
|
||||
|
||||
|
||||
// Function: slerpn()
|
||||
// Synopsis: Spherical lerpn(), returns exactly `n` vectors interpolated on a great circle.
|
||||
// Topics: Interpolation, Math
|
||||
// See Also: slerp(), lerpn()
|
||||
// Usage:
|
||||
// vec_list = slerpn(v1, v2, n);
|
||||
// vec_list = slerpn(v1, v2, n, [endpoint]);
|
||||
// Description:
|
||||
// Returns exactly `n` values, interpolated along a great-circle arc on a unit sphere between 3D vectors `v1` and `v2`. The input vectors need not be unit size, although the result is always a list of unit vectors, which should be scaled by your original sphere radius.
|
||||
// Arguments:
|
||||
// v1 = First 3D vector, needn't be unit size.
|
||||
// v2 = Second 3D vector, needn't be unit size.
|
||||
// n = The number of values to return.
|
||||
// endpoint = If true, the last value is `v2`. If false, the last value is one step less. Default: true
|
||||
// Example(3D,VPD=220,VPT=[0,0,0]): Seven points interpolated along a great-circle arc.
|
||||
// radius = 40;
|
||||
// interps = slerpn([-1,-1,0],[1,0.5,1], 7);
|
||||
// stroke(interps*radius, dots=true, width=2);
|
||||
// %sphere(radius);
|
||||
function slerpn(v1, v2, n, endpoint=true) =
|
||||
assert(is_vector(v1,3) && is_vector(v2,3), "\nv1 and v2 must be 3-vectors.")
|
||||
assert(is_int(n))
|
||||
assert(is_bool(endpoint))
|
||||
let(
|
||||
a = unit(v1),
|
||||
b = unit(v2),
|
||||
theta = acos(max(-1, min(1, a*b))),
|
||||
err = assert(abs(theta-180)>EPSILON, "\nNo solution when vectors v1 and v2 are 180° apart."),
|
||||
sin_theta = sin(theta),
|
||||
d = n - (endpoint ? 1 : 0)
|
||||
) [
|
||||
for(i=[0:n-1]) let(u=i/d)
|
||||
sin_theta < EPSILON ? unit(a+b) // fallback
|
||||
: (a * sin((1 - u) * theta) + b * sin(u * theta)) / sin_theta
|
||||
];
|
||||
|
||||
|
||||
|
||||
// Section: Miscellaneous Functions
|
||||
|
||||
@@ -931,7 +999,7 @@ function cumprod(list,right=false) =
|
||||
a]
|
||||
:
|
||||
assert(is_vector(list) || (is_matrix(list[0],square=true) && is_consistent(list)),
|
||||
"\nInput must be a listector, a list of listectors, 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
|
||||
;
|
||||
@@ -1102,10 +1170,10 @@ function gaussian_rands(n=1, mean=0, cov=1, seed=undef) =
|
||||
// Returns random numbers with an exponential distribution with parameter lambda, and hence mean 1/lambda.
|
||||
// Arguments:
|
||||
// n = number of points to return. Default: 1
|
||||
// lambda = distribution parameter. The mean will be 1/lambda. Default: 1
|
||||
// lambda = distribution parameter. The mean is 1/lambda. Default: 1
|
||||
function exponential_rands(n=1, lambda=1, seed) =
|
||||
assert( is_int(n) && n>=1, "The number of points should be an integer greater than zero.")
|
||||
assert( is_num(lambda) && lambda>0, "The lambda parameter must be a positive number.")
|
||||
assert( is_int(n) && n>=1, "\nThe number of points should be an integer greater than zero.")
|
||||
assert( is_num(lambda) && lambda>0, "\nThe lambda parameter must be a positive number.")
|
||||
let(
|
||||
unif = is_def(seed) ? rands(0,1,n,seed=seed) : rands(0,1,n)
|
||||
)
|
||||
@@ -1127,8 +1195,8 @@ function exponential_rands(n=1, lambda=1, seed) =
|
||||
|
||||
// See https://mathworld.wolfram.com/SpherePointPicking.html
|
||||
function spherical_random_points(n=1, radius=1, seed) =
|
||||
assert( is_int(n) && n>=1, "The number of points should be an integer greater than zero.")
|
||||
assert( is_num(radius) && radius>0, "The radius should be a non-negative number.")
|
||||
assert( is_int(n) && n>=1, "\nThe number of points should be an integer greater than zero.")
|
||||
assert( is_num(radius) && radius>0, "\nThe radius should be a non-negative number.")
|
||||
let( theta = is_undef(seed)
|
||||
? rands(0,360,n)
|
||||
: rands(0,360,n, seed),
|
||||
@@ -1147,20 +1215,24 @@ function spherical_random_points(n=1, radius=1, seed) =
|
||||
// Usage:
|
||||
// points = random_polygon([n], [size], [seed]);
|
||||
// Description:
|
||||
// Generate the `n` vertices of a random counter-clockwise simple 2d polygon
|
||||
// inside a circle centered at the origin with radius `size`.
|
||||
// Generate the `n` vertices of a random counter-clockwise simple 2d polygon
|
||||
// inside a circle centered at the origin with radius `size`.
|
||||
// Arguments:
|
||||
// n = number of vertices of the polygon. Default: 3
|
||||
// size = the radius of a circle centered at the origin containing the polygon. Default: 1
|
||||
// seed = an optional seed for the random generation.
|
||||
// n = number of vertices of the polygon. Default: 3
|
||||
// size = the [min,max] radius of a circle centered at the origin containing the polygon. A single number specifies the max radius. Default: [0.01,1]
|
||||
// seed = an optional seed for the random generation.
|
||||
// Example(2D): A 17-sided polygon with vertices between radii 10 and 20.
|
||||
// polygon(random_polygon(17, [10,20], 888));
|
||||
function random_polygon(n=3,size=1, seed) =
|
||||
assert( is_int(n) && n>2, "Improper number of polygon vertices.")
|
||||
assert( is_num(size) && size>0, "Improper size.")
|
||||
let(
|
||||
seed = is_undef(seed) ? rands(0,1,1)[0] : seed,
|
||||
assert( is_int(n) && n>2, "\nImproper number of polygon vertices.")
|
||||
assert(all_positive(size) && (is_vector(size,2) || is_num(size)), "\nImproper size.")
|
||||
let(
|
||||
rmin = is_num(size) ? 0.01 : size[0],
|
||||
rmax = is_num(size) ? size : size[1],
|
||||
seed = is_undef(seed) ? rands(0,1000,1)[0] : seed,
|
||||
cumm = cumsum(rands(0.1,10,n+1,seed)),
|
||||
angs = 360*cumm/cumm[n-1],
|
||||
rads = rands(.01,size,n,seed+cumm[0])
|
||||
rads = rands(rmin,rmax,n,seed+cumm[0])
|
||||
)
|
||||
[for(i=count(n)) rads[i]*[cos(angs[i]), sin(angs[i])] ];
|
||||
|
||||
@@ -1191,11 +1263,11 @@ function random_polygon(n=3,size=1, seed) =
|
||||
// h = the parametric sampling of the data.
|
||||
// closed = boolean to indicate if the data set should be wrapped around from the end to the start.
|
||||
function deriv(data, h=1, closed=false) =
|
||||
assert( is_consistent(data) , "Input list is not consistent or not numerical.")
|
||||
assert( len(data)>=2, "Input `data` should have at least 2 elements.")
|
||||
assert( is_finite(h) || is_vector(h), "The sampling `h` must be a number or a list of numbers." )
|
||||
assert( is_consistent(data) , "\nInput list is not consistent or not numerical.")
|
||||
assert( len(data)>=2, "\nInput `data` should have at least 2 elements.")
|
||||
assert( is_finite(h) || is_vector(h), "\nThe sampling `h` must be a number or a list of numbers.")
|
||||
assert( is_num(h) || len(h) == len(data)-(closed?0:1),
|
||||
str("Vector valued `h` must have length ",len(data)-(closed?0:1)))
|
||||
str("\nVector valued `h` must have length ",len(data)-(closed?0:1)))
|
||||
is_vector(h) ? _deriv_nonuniform(data, h, closed=closed) :
|
||||
let( L = len(data) )
|
||||
closed
|
||||
@@ -1257,10 +1329,10 @@ function _deriv_nonuniform(data, h, closed) =
|
||||
// h = the constant parametric sampling of the data.
|
||||
// closed = boolean to indicate if the data set should be wrapped around from the end to the start.
|
||||
function deriv2(data, h=1, closed=false) =
|
||||
assert( is_consistent(data) , "Input list is not consistent or not numerical.")
|
||||
assert( is_finite(h), "The sampling `h` must be a number." )
|
||||
assert( is_consistent(data) , "\nInput list is not consistent or not numerical.")
|
||||
assert( is_finite(h), "\nThe sampling `h` must be a number." )
|
||||
let( L = len(data) )
|
||||
assert( L>=3, "Input list has less than 3 elements.")
|
||||
assert( L>=3, "\nInput list has less than 3 elements.")
|
||||
closed
|
||||
? [
|
||||
for(i=[0:1:L-1])
|
||||
@@ -1303,9 +1375,9 @@ function deriv2(data, h=1, closed=false) =
|
||||
// h = the constant parametric sampling of the data.
|
||||
// closed = boolean to indicate if the data set should be wrapped around from the end to the start.
|
||||
function deriv3(data, h=1, closed=false) =
|
||||
assert( is_consistent(data) , "Input list is not consistent or not numerical.")
|
||||
assert( len(data)>=5, "Input list has less than 5 elements.")
|
||||
assert( is_finite(h), "The sampling `h` must be a number." )
|
||||
assert( is_consistent(data) , "\nInput list is not consistent or not numerical.")
|
||||
assert( len(data)>=5, "\nInput list has less than 5 elements.")
|
||||
assert( is_finite(h), "\nThe sampling `h` must be a number." )
|
||||
let(
|
||||
L = len(data),
|
||||
h3 = h*h*h
|
||||
@@ -1397,8 +1469,8 @@ function _c_mul(z1,z2) =
|
||||
// z1 = First complex number, given as a 2D vector [REAL, IMAGINARY]
|
||||
// z2 = Second complex number, given as a 2D vector [REAL, IMAGINARY]
|
||||
function c_div(z1,z2) =
|
||||
assert( is_vector(z1,2) && is_vector(z2), "Complex numbers should be represented by 2D vectors." )
|
||||
assert( !approx(z2,0), "The divisor `z2` cannot be zero." )
|
||||
assert( is_vector(z1,2) && is_vector(z2), "\nComplex numbers should be represented by 2D vectors.")
|
||||
assert( !approx(z2,0), "\nThe divisor `z2` cannot be zero.")
|
||||
let(den = z2.x*z2.x + z2.y*z2.y)
|
||||
[(z1.x*z2.x + z1.y*z2.y)/den, (z1.y*z2.x - z1.x*z2.y)/den];
|
||||
|
||||
@@ -1487,7 +1559,7 @@ function quadratic_roots(a,b,c,real=false) =
|
||||
:
|
||||
is_undef(b) && is_undef(c) && is_vector(a,3) ? quadratic_roots(a[0],a[1],a[2]) :
|
||||
assert(is_num(a) && is_num(b) && is_num(c))
|
||||
assert(a!=0 || b!=0 || c!=0, "Quadratic must have a nonzero coefficient")
|
||||
assert(a!=0 || b!=0 || c!=0, "\nQuadratic must have a nonzero coefficient.")
|
||||
a==0 && b==0 ? [] : // No solutions
|
||||
a==0 ? [[-c/b,0]] :
|
||||
let(
|
||||
@@ -1518,8 +1590,8 @@ function quadratic_roots(a,b,c,real=false) =
|
||||
// The result is a number if `z` is a number and a complex number otherwise.
|
||||
function polynomial(p,z,k,total) =
|
||||
is_undef(k)
|
||||
? assert( is_vector(p) , "Input polynomial coefficients must be a vector." )
|
||||
assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." )
|
||||
? assert( is_vector(p) , "\nInput polynomial coefficients must be a vector.")
|
||||
assert( is_finite(z) || is_vector(z,2), "\nThe value of `z` must be a real or a complex number.")
|
||||
polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0])
|
||||
: k==len(p) ? total
|
||||
: polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]);
|
||||
@@ -1541,7 +1613,7 @@ function poly_mult(p,q) =
|
||||
? poly_mult(p[0],p[1])
|
||||
: poly_mult(p[0], poly_mult(list_tail(p)))
|
||||
:
|
||||
assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult")
|
||||
assert( is_vector(p) && is_vector(q),"\nInvalid arguments to poly_mult.")
|
||||
p*p==0 || q*q==0
|
||||
? [0]
|
||||
: _poly_trim(convolve(p,q));
|
||||
@@ -1559,10 +1631,10 @@ function poly_mult(p,q) =
|
||||
// the zero polynomial [0] is returned for the remainder. Similarly if the quotient is zero
|
||||
// the returned quotient is [0].
|
||||
function poly_div(n,d) =
|
||||
assert( is_vector(n) && is_vector(d) , "Invalid polynomials." )
|
||||
assert( is_vector(n) && is_vector(d) , "\nInvalid polynomials.")
|
||||
let( d = _poly_trim(d),
|
||||
n = _poly_trim(n) )
|
||||
assert( d!=[0] , "Denominator cannot be a zero polynomial." )
|
||||
assert( d!=[0] , "\nDenominator cannot be a zero polynomial.")
|
||||
n==[0]
|
||||
? [[0],[0]]
|
||||
: _poly_div(n,d,q=[]);
|
||||
@@ -1597,7 +1669,7 @@ function _poly_trim(p,eps=0) =
|
||||
// Description:
|
||||
// Computes the sum of two polynomials.
|
||||
function poly_add(p,q) =
|
||||
assert( is_vector(p) && is_vector(q), "Invalid input polynomial(s)." )
|
||||
assert( is_vector(p) && is_vector(q), "\nInvalid input polynomial(s).")
|
||||
let( plen = len(p),
|
||||
qlen = len(q),
|
||||
long = plen>qlen ? p : q,
|
||||
@@ -1628,9 +1700,9 @@ function poly_add(p,q) =
|
||||
// Dario Bini. "Numerical computation of polynomial zeros by means of Aberth's Method", Numerical Algorithms, Feb 1996.
|
||||
// https://www.researchgate.net/publication/225654837_Numerical_computation_of_polynomial_zeros_by_means_of_Aberth's_method
|
||||
function poly_roots(p,tol=1e-14,error_bound=false) =
|
||||
assert( is_vector(p), "Invalid polynomial." )
|
||||
assert( is_vector(p), "\nInvalid polynomial.")
|
||||
let( p = _poly_trim(p,eps=0) )
|
||||
assert( p!=[0], "Input polynomial cannot be zero." )
|
||||
assert( p!=[0], "\nInput polynomial cannot be zero.")
|
||||
p[len(p)-1] == 0 ? // Strip trailing zero coefficients
|
||||
let( solutions = poly_roots(list_head(p),tol=tol, error_bound=error_bound))
|
||||
(error_bound ? [ [[0,0], each solutions[0]], [0, each solutions[1]]]
|
||||
@@ -1665,7 +1737,7 @@ function poly_roots(p,tol=1e-14,error_bound=false) =
|
||||
// tol = root tolerance
|
||||
// i=iteration counter
|
||||
function _poly_roots(p, pderiv, s, z, tol, i=0) =
|
||||
assert(i<45, str("Polyroot exceeded iteration limit. Current solution:", z))
|
||||
assert(i<45, str("\nPolyroot exceeded iteration limit. Current solution:", z))
|
||||
let(
|
||||
n = len(z),
|
||||
svals = [for(zk=z) tol*polynomial(s,norm(zk))],
|
||||
@@ -1703,9 +1775,9 @@ function _poly_roots(p, pderiv, s, z, tol, i=0) =
|
||||
// tol = tolerance for the complex polynomial root finder
|
||||
|
||||
function real_roots(p,eps=undef,tol=1e-14) =
|
||||
assert( is_vector(p), "Invalid polynomial." )
|
||||
assert( is_vector(p), "\nInvalid polynomial.")
|
||||
let( p = _poly_trim(p,eps=0) )
|
||||
assert( p!=[0], "Input polynomial cannot be zero." )
|
||||
assert( p!=[0], "\nInput polynomial cannot be zero.")
|
||||
let(
|
||||
roots_err = poly_roots(p,error_bound=true),
|
||||
roots = roots_err[0],
|
||||
@@ -1762,11 +1834,11 @@ function root_find(f,x0,x1,tol=1e-15) =
|
||||
// Check endpoints
|
||||
y0==0 || _rfcheck(x0, y0,yrange,tol) ? x0 :
|
||||
y1==0 || _rfcheck(x1, y1,yrange,tol) ? x1 :
|
||||
assert(y0*y1<0, "Sign of function must be different at the interval endpoints")
|
||||
assert(y0*y1<0, "\nSign of function must be different at the interval endpoints.")
|
||||
_rootfind(f,[x0,x1],[y0,y1],yrange,tol);
|
||||
|
||||
function _rfcheck(x,y,range,tol) =
|
||||
assert(is_finite(y), str("Function not finite at ",x))
|
||||
assert(is_finite(y), str("\nFunction not finite at ",x))
|
||||
abs(y) < tol*(range[1]-range[0]);
|
||||
|
||||
// xpts and ypts are arrays whose first two entries contain the
|
||||
@@ -1774,7 +1846,7 @@ function _rfcheck(x,y,range,tol) =
|
||||
// yrange is the total observed range of y values (used for the
|
||||
// tolerance test).
|
||||
function _rootfind(f, xpts, ypts, yrange, tol, i=0) =
|
||||
assert(i<100, "root_find did not converge to a solution")
|
||||
assert(i<100, "\nroot_find did not converge to a solution.")
|
||||
let(
|
||||
xmid = (xpts[0]+xpts[1])/2,
|
||||
ymid = f(xmid),
|
||||
|
256
paths.scad
256
paths.scad
@@ -30,9 +30,9 @@
|
||||
// is_path(list, [dim], [fast])
|
||||
// Description:
|
||||
// Returns true if `list` is a {{path}}. A path is a list of two or more numeric vectors (AKA {{points}}).
|
||||
// All vectors must of the same size, and may only contain numbers that are not inf or nan.
|
||||
// All vectors must of the same size, and must contain numbers that are not inf or nan.
|
||||
// By default the vectors in a path must be 2D or 3D. Set the `dim` parameter to specify a list
|
||||
// of allowed dimensions, or set it to `undef` to allow any dimension. (Note that this function
|
||||
// of allowed dimensions, or set it to `undef` to allow any dimension. (This function
|
||||
// returns `false` on 1-regions.)
|
||||
// Example:
|
||||
// bool1 = is_path([[3,4],[5,6]]); // Returns true
|
||||
@@ -53,7 +53,7 @@
|
||||
// Arguments:
|
||||
// list = list to check
|
||||
// dim = list of allowed dimensions of the vectors in the path. Default: [2,3]
|
||||
// fast = set to true for fast check that only looks at first entry. Default: false
|
||||
// fast = set to true for fast check that looks at only the first entry. Default: false
|
||||
function is_path(list, dim=[2,3], fast=false) =
|
||||
fast
|
||||
? is_list(list) && is_vector(list[0])
|
||||
@@ -77,7 +77,7 @@ function is_path(list, dim=[2,3], fast=false) =
|
||||
// name = name of parameter to use in error message. Default: "path"
|
||||
function is_1region(path, name="path") =
|
||||
!is_region(path)? false
|
||||
:assert(len(path)==1,str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region"))
|
||||
:assert(len(path)==1,str("\nParameter \"",name,"\" must be a path or singleton region, but is a multicomponent region."))
|
||||
true;
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ function is_1region(path, name="path") =
|
||||
// name = name of parameter to use in error message. Default: "path"
|
||||
function force_path(path, name="path") =
|
||||
is_region(path) ?
|
||||
assert(len(path)==1, str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region"))
|
||||
assert(len(path)==1, str("\nParameter \"",name,"\" must be a path or singleton region, but is a multicomponent region."))
|
||||
path[0]
|
||||
: path;
|
||||
|
||||
@@ -139,7 +139,7 @@ function _path_select(path, s1, u1, s2, u2, closed=false) =
|
||||
// SynTags: Path
|
||||
// Topics: Paths, Regions
|
||||
// Description:
|
||||
// Takes a {{path}} and removes unnecessary sequential collinear {{points}}. Note that when `closed=true` either of the path
|
||||
// Takes a {{path}} and removes unnecessary sequential collinear {{points}}. When `closed=true` either of the path
|
||||
// endpoints may be removed.
|
||||
// Usage:
|
||||
// path_merge_collinear(path, [eps])
|
||||
@@ -151,8 +151,8 @@ function path_merge_collinear(path, closed, eps=EPSILON) =
|
||||
is_1region(path) ? path_merge_collinear(path[0], default(closed,true), eps) :
|
||||
let(closed=default(closed,false))
|
||||
assert(is_bool(closed))
|
||||
assert( is_path(path), "Invalid path in path_merge_collinear." )
|
||||
assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." )
|
||||
assert( is_path(path), "\nInvalid path in path_merge_collinear.")
|
||||
assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "\nInvalid tolerance.")
|
||||
len(path)<=2 ? path :
|
||||
let(path = deduplicate(path, closed=closed))
|
||||
[
|
||||
@@ -182,7 +182,7 @@ function path_merge_collinear(path, closed, eps=EPSILON) =
|
||||
// echo(path_length(path));
|
||||
function path_length(path,closed) =
|
||||
is_1region(path) ? path_length(path[0], default(closed,true)) :
|
||||
assert(is_path(path), "Invalid path in path_length")
|
||||
assert(is_path(path), "\nInvalid path in path_length.")
|
||||
let(closed=default(closed,false))
|
||||
assert(is_bool(closed))
|
||||
len(path)<2? 0 :
|
||||
@@ -203,7 +203,7 @@ function path_length(path,closed) =
|
||||
function path_segment_lengths(path, closed) =
|
||||
is_1region(path) ? path_segment_lengths(path[0], default(closed,true)) :
|
||||
let(closed=default(closed,false))
|
||||
assert(is_path(path),"Invalid path in path_segment_lengths.")
|
||||
assert(is_path(path),"\nInvalid path in path_segment_lengths.")
|
||||
assert(is_bool(closed))
|
||||
[
|
||||
for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]),
|
||||
@@ -219,7 +219,7 @@ function path_segment_lengths(path, closed) =
|
||||
// fracs = path_length_fractions(path, [closed]);
|
||||
// Description:
|
||||
// Returns the distance fraction of each point in the {{path}} along the path, so the first
|
||||
// point is zero and the final point is 1. If the path is closed the length of the output
|
||||
// point is zero and the final point is 1. If the path is closed, the length of the output
|
||||
// will have one extra point because of the final connecting segment that connects the last
|
||||
// point of the path to the first point.
|
||||
// Arguments:
|
||||
@@ -253,7 +253,7 @@ function path_length_fractions(path, closed) =
|
||||
/// of how far along those segments they intersect at. A proportion of 0.0 indicates the start
|
||||
/// of the segment, and a proportion of 1.0 indicates the end of the segment.
|
||||
/// .
|
||||
/// Note that this function does not return self-intersecting segments, only the points
|
||||
/// This function does not return self-intersecting segments, only the points
|
||||
/// where non-parallel segments intersect.
|
||||
/// Arguments:
|
||||
/// path = The path to find self intersections of.
|
||||
@@ -301,6 +301,8 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
|
||||
[isect[0], i, isect[1], j, isect[2]]
|
||||
];
|
||||
|
||||
|
||||
|
||||
// Section: Resampling - changing the number of points in a path
|
||||
|
||||
|
||||
@@ -309,7 +311,7 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
|
||||
// entry is rounded to an integer and the sum is the same as
|
||||
// that of the input. Works by rounding an entry in the list
|
||||
// and passing the rounding error forward to the next entry.
|
||||
// This will generally distribute the error in a uniform manner.
|
||||
// This generally distributes the error in a uniform manner.
|
||||
function _sum_preserving_round(data, index=0) =
|
||||
index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) :
|
||||
let(
|
||||
@@ -343,12 +345,12 @@ function _sum_preserving_round(data, index=0) =
|
||||
// (`method="segment"`). If the extra points don't fit evenly on the path then the
|
||||
// algorithm attempts to distribute them as uniformly as possible, but the result may be uneven.
|
||||
// The `exact` option, which is true by default, requires that the final point count is
|
||||
// exactly as requested. For example, if you subdivide a four point square and request `n=13` then one edge will have
|
||||
// an extra point compared to the others.
|
||||
// exactly as requested. For example, if you subdivide a four point square and request `n=13` then one
|
||||
// edge will have an extra point compared to the others.
|
||||
// If you set `exact=false` then the
|
||||
// algorithm will favor uniformity and the output path may have a different number of
|
||||
// points than you requested, but the sampling will be uniform. In our example of the
|
||||
// square with `n=13`, you will get only 12 points output, with the same number of points on each edge.
|
||||
// algorithm favors uniformity and the output path may have a different number of
|
||||
// points than you requested, but the sampling is still uniform. In our example of the
|
||||
// square with `n=13`, you get only 12 points output, with the same number of points on each edge.
|
||||
// .
|
||||
// The points are always distributed uniformly on each segment. The `method="length"` option does
|
||||
// means that the number of points on a segment is based on its length, but the points are still
|
||||
@@ -357,8 +359,8 @@ function _sum_preserving_round(data, index=0) =
|
||||
// specifies the desired point count on each segment: with vector valued `n` the `subdivide_path`
|
||||
// function places `n[i]-1` points on segment `i`. The reason for the -1 is to avoid
|
||||
// double counting the endpoints, which are shared by pairs of segments, so that for
|
||||
// a closed polygon the total number of points will be sum(n). Note that with an open
|
||||
// path there is an extra point at the end, so the number of points will be sum(n)+1.
|
||||
// a closed polygon the total number of points is sum(n). With an open
|
||||
// path there is an extra point at the end, so the number of points is sum(n)+1.
|
||||
// .
|
||||
// If you use the `maxlen` option then you specify the maximum length segment allowed in the output.
|
||||
// Each segment is subdivided into the largest number of segments meeting your requirement. As above,
|
||||
@@ -368,8 +370,8 @@ function _sum_preserving_round(data, index=0) =
|
||||
// path = path in any dimension or a 1-region
|
||||
// n = scalar total number of points desired or with `method="segment"` can be a vector requesting `n[i]-1` new points added to segment i.
|
||||
// ---
|
||||
// refine = increase total number of points by this factor (Specify only one of n, refine and maxlen)
|
||||
// maxlen = maximum length segment in the output (Specify only one of n, refine and maxlen)
|
||||
// refine = increase total number of points by this factor (specify only one of n, refine and maxlen)
|
||||
// maxlen = maximum length segment in the output (specify only one of n, refine and maxlen)
|
||||
// closed = set to false if the path is open. Default: True
|
||||
// exact = if true return exactly the requested number of points, possibly sacrificing uniformity. If false, return uniform point sample that may not match the number of points requested. (Not allowed with maxlen.) Default: true
|
||||
// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices in proportion to segment length, so short segments get fewer points. If `"segment"`, add points evenly among the segments, so all segments get the same number of points. (Not allowed with maxlen.) Default: `"length"`
|
||||
@@ -400,10 +402,10 @@ function _sum_preserving_round(data, index=0) =
|
||||
// Example(2D): With `exact=false` you can also get extra points, here 20 instead of requested 18
|
||||
// mypath = subdivide_path(pentagon(side=2), 18, exact=false);
|
||||
// move_copies(mypath)circle(r=.1,$fn=32);
|
||||
// Example(2D): Using refine in this example multiplies the point count by 3 by adding 2 points to each edge
|
||||
// Example(2D): Using refine in this example multiplies the point count by 3 by adding 2 points to each edge.
|
||||
// mypath = subdivide_path(pentagon(side=2), refine=3);
|
||||
// move_copies(mypath)circle(r=.1,$fn=32);
|
||||
// Example(2D): But note that refine doesn't distribute evenly by segment unless you change the method. with the default method set to `"length"`, the points are distributed with more on the long segments in this example using refine.
|
||||
// Example(2D): However, refine doesn't distribute evenly by segment unless you change the method. With the default method set to `"length"`, the points are distributed with more on the long segments in this example using refine.
|
||||
// mypath = subdivide_path(square([8,2],center=true), refine=3);
|
||||
// move_copies(mypath)circle(r=.2,$fn=32);
|
||||
// Example(2D): In this example with maxlen, every side gets a different number of new points
|
||||
@@ -415,12 +417,12 @@ function _sum_preserving_round(data, index=0) =
|
||||
// move_copies(mypath)sphere(r=.1,$fn=32);
|
||||
function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
|
||||
let(path = force_path(path))
|
||||
assert(is_path(path))
|
||||
assert(num_defined([n,refine,maxlen]),"Must give exactly one of n, refine, and maxlen")
|
||||
assert(is_path(path), "\nInvalid path or 1-region.")
|
||||
assert(num_defined([n,refine,maxlen]), "\nMust give exactly one of n, refine, and maxlen.")
|
||||
refine==1 || n==len(path) ? path :
|
||||
is_def(maxlen) ?
|
||||
assert(is_undef(method), "Cannot give method with maxlen")
|
||||
assert(is_undef(exact), "Cannot give exact with maxlen")
|
||||
assert(is_undef(method), "\nCannot give method with maxlen.")
|
||||
assert(is_undef(exact), "\nCannot give exact with maxlen.")
|
||||
[
|
||||
for (p=pair(path,closed))
|
||||
let(steps = ceil(norm(p[1]-p[0])/maxlen))
|
||||
@@ -438,18 +440,18 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
|
||||
!is_undef(refine)? len(path) * refine :
|
||||
undef
|
||||
)
|
||||
assert((is_num(n) && n>0) || is_vector(n),"Parameter n to subdivide_path must be postive number or vector")
|
||||
assert((is_num(n) && n>0) || is_vector(n), "\nParameter n to subdivide_path must be postive number or vector.")
|
||||
let(
|
||||
count = len(path) - (closed?0:1),
|
||||
add_guess = method=="segment"?
|
||||
(
|
||||
is_list(n)
|
||||
? assert(len(n)==count,"Vector parameter n to subdivide_path has the wrong length")
|
||||
? assert(len(n)==count, "\nVector parameter n to subdivide_path has the wrong length.")
|
||||
add_scalar(n,-1)
|
||||
: repeat((n-len(path)) / count, count)
|
||||
)
|
||||
: // method=="length"
|
||||
assert(is_num(n),"Parameter n to subdivide path must be a number when method=\"length\"")
|
||||
assert(is_num(n), "\nParameter n to subdivide path must be a number when method=\"length\".")
|
||||
let(
|
||||
path_lens = path_segment_lengths(path,closed),
|
||||
add_density = (n - len(path)) / sum(path_lens)
|
||||
@@ -471,18 +473,19 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
|
||||
// Synopsis: Returns an equidistant set of points along a path.
|
||||
// SynTags: Path
|
||||
// Topics: Paths
|
||||
// See Also: subdivide_path()
|
||||
// See Also: simplify_path(), subdivide_path()
|
||||
// Usage:
|
||||
// newpath = resample_path(path, n|spacing=, [closed=]);
|
||||
// Description:
|
||||
// Compute a uniform resampling of the input {{path}}. If you specify `n` then the output path will have n
|
||||
// Compute a uniform resampling of the input {{path}}. If you specify `n` then the output path has `n`
|
||||
// {{points}} spaced uniformly (by linear interpolation along the input path segments). The only points of the
|
||||
// input path that are guaranteed to appear in the output path are the starting and ending points, and any
|
||||
// points that have an angular deflection of at least the number of degrees given in `keep_corners`.
|
||||
// If you specify `spacing` then the length you give will be rounded to the nearest spacing that gives
|
||||
// If you specify `spacing` then the length you give is rounded to the nearest spacing that gives
|
||||
// a uniform sampling of the path and the resulting uniformly sampled path is returned.
|
||||
// Note that because this function operates on a discrete input path the quality of the output depends on
|
||||
// the sampling of the input. If you want very accurate output, use a lot of points for the input.
|
||||
// .
|
||||
// Because this function operates on a discrete input path the quality of the output depends on
|
||||
// the sampling of the input. If you want accurate output, use many points for the input.
|
||||
// Arguments:
|
||||
// path = path in any dimension or a 1-region
|
||||
// n = Number of points in output
|
||||
@@ -518,8 +521,8 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
|
||||
|
||||
function resample_path(path, n, spacing, keep_corners, closed=true) =
|
||||
let(path = force_path(path))
|
||||
assert(is_path(path))
|
||||
assert(num_defined([n,spacing])==1,"Must define exactly one of n and spacing")
|
||||
assert(is_path(path), "\nInvalid path or 1-region.")
|
||||
assert(num_defined([n,spacing])==1,"\nMust define exactly one of n and spacing.")
|
||||
assert(n==undef || (is_integer(n) && n>0))
|
||||
assert(spacing==undef || (is_finite(spacing) && spacing>0))
|
||||
assert(is_bool(closed))
|
||||
@@ -538,7 +541,7 @@ function resample_path(path, n, spacing, keep_corners, closed=true) =
|
||||
subpaths = [ for (p = pair(corners)) [for(i = [p.x:1:p.y]) path[i%pcnt]] ],
|
||||
n = is_undef(n)? undef : closed? n+1 : n
|
||||
)
|
||||
assert(n==undef || n >= len(corners), "There are nore than `n=` corners whose angle is greater than `keep_corners=`.")
|
||||
assert(n==undef || n >= len(corners), "\nThere are nore than `n=` corners whose angle is greater than `keep_corners=`.")
|
||||
let(
|
||||
lens = [for (subpath = subpaths) path_length(subpath)],
|
||||
part_ns = is_undef(n)
|
||||
@@ -563,6 +566,111 @@ function resample_path(path, n, spacing, keep_corners, closed=true) =
|
||||
) out;
|
||||
|
||||
|
||||
// Function: simplify_path()
|
||||
// Synopsis: Removes points from an irregular path, preserving dominant features.
|
||||
// SynTags: Path
|
||||
// Topics: Paths
|
||||
// See Also: resample_path()
|
||||
// Usage:
|
||||
// newpath = simplify_path(path, maxerr, [closed=]);
|
||||
// Description:
|
||||
// This is intended for irregular paths such as coastlines, or paths having fractal self-similarity.
|
||||
// The original path is simplified by removing points that fall within a specified margin of error,
|
||||
// leaving behind those points that contribute to dominant features of the path. This operation has the
|
||||
// effect of making the point spacing somewhat more uniform. For coastlines, up to 80% reduction in path
|
||||
// length is possible with small degradation of the original shape. The input path may be 2D or 3D.
|
||||
// .
|
||||
// The `maxerr` parameter determines which points of the original path are kept. A point is kept if it
|
||||
// deviates beyond `maxerr` distance from a straight line between the last kept point and a point further
|
||||
// along the path. When a new deviation is found, that deviating point is kept and the process repeats from
|
||||
// that new kept point. The best value of `maxerr` depends on the geometry of the path and the amount of
|
||||
// reduction you want. A smaller value of `maxerr` returns more detail in the output, and a larger value
|
||||
// causes details to be lost. For paths such as coastlines, a `maxerr` value less than 1% of the maximum
|
||||
// bounding box dimension is a good starting value.
|
||||
// .
|
||||
// For unclosed paths (where `closed=false`) the endpoints of the path are preserved. When `closed=true`,
|
||||
// the path is treated as continuous and only dominant features that happen to be near the endpoints are
|
||||
// included in the output.
|
||||
// Arguments:
|
||||
// path = Path in any dimension or 1-region
|
||||
// maxerr = Maximum deviation from line connecting last kept point to a further point; points beyond this deviation are kept.
|
||||
// ---
|
||||
// closed = Set to true if path is closed. If false, endpoints are retained in the output. Default: false
|
||||
// Example(2D,Med,VPD=34900,VPT=[5702,6507,0]): A map of California, originally a 262-point polygon (yellow, on left), reduced to 39 points (green, on right).
|
||||
// calif = [
|
||||
// [225,12681], [199,12544], [180,12490], [221,12435], [300,12342], [310,12315], [320,12263], [350,12154],
|
||||
// [374,11968], [350,11820], [328,11707], [291,11586], [259,11553], [275,11499], [304,11420], [312,11321],
|
||||
// [273,11189], [233,11066], [200,10995], [160,10942], [104,10820], [0,10568], [25,10510], [50,10420],
|
||||
// [65,10312], [271,10108], [368,10004], [438,9909], [517,9809], [569,9741], [600,9666], [615,9600],
|
||||
// [630,9567], [649,9526], [679,9385], [670,9245], [650,9187], [635,9113], [644,8985], [673,8938], [694,8846],
|
||||
// [740,8745], [770,8678], [780,8635], [771,8528], [745,8449], [738,8403], [807,8364], [872,8298], [894,8264],
|
||||
// [1090,8076], [1270,7877], [1366,7798], [1440,7679], [1495,7596], [1543,7541], [1560,7487], [1575,7447],
|
||||
// [1576,7350], [1536,7234], [1521,7168], [1587,7184], [1761,7129], [1838,7050], [1893,7050], [1995,6995],
|
||||
// [2081,6940], [2109,7006], [2100,7045], [2100,7090], [2109,7155], [2115,7210], [2100,7269], [2124,7334],
|
||||
// [2179,7365], [2209,7391], [2242,7362], [2308,7311], [2280,7215], [2220,7164], [2210,7150], [2200,7095],
|
||||
// [2200,7040], [2234,7040], [2274,6932], [2415,6775], [2459,6691], [2483,6578], [2558,6497], [2610,6449],
|
||||
// [2598,6430], [2490,6475], [2444,6500], [2410,6515], [2406,6530], [2375,6570], [2305,6610], [2224,6638],
|
||||
// [2225,6806], [2211,6867], [2159,6913], [2109,6912], [2075,6810], [2074,6583], [2068,6521], [2104,6503],
|
||||
// [2140,6454], [2153,6417], [2184,6336], [2187,6243], [2173,6158], [2213,6065], [2250,6005], [2283,5970],
|
||||
// [2343,5928], [2370,5875], [2428,5822], [2485,5779], [2606,5782], [2728,5785], [2772,5725], [2850,5561],
|
||||
// [2839,5472], [2820,5391], [2797,5322], [2734,5321], [2676,5330], [2640,5289], [2656,5236], [2661,5205],
|
||||
// [2671,5144], [2712,5083], [2720,4973], [2738,4882], [2806,4819], [2891,4780], [2966,4737], [3004,4662],
|
||||
// [3043,4604], [3080,4542], [3128,4491], [3170,4453], [3294,4262], [3370,4150], [3384,4090], [3402,4057],
|
||||
// [3442,4029], [3602,3909], [3753,3777], [3855,3600], [3830,3521], [3900,3425], [3957,3394], [4000,3390],
|
||||
// [4045,3393], [4109,3315], [4121,3235], [4089,3125], [4074,3085], [4081,3019], [4098,2923], [4116,2848],
|
||||
// [4160,2774], [4135,2734], [4116,2697], [4100,2645], [4123,2585], [4208,2558], [4272,2478], [4367,2441],
|
||||
// [4453,2461], [4533,2485], [4635,2488], [4795,2472], [4875,2450], [4896,2425], [4933,2402], [4988,2404],
|
||||
// [5036,2409], [5124,2401], [5388,2338], [5479,2252], [5552,2199], [5610,2121], [5658,2051], [5802,1986],
|
||||
// [5858,1955], [5994,1930], [6110,1905], [6122,1880], [6174,1895], [6309,1910], [6447,1875], [6510,1797],
|
||||
// [6532,1730], [6525,1594], [6531,1521], [6615,1490], [6697,1525], [6729,1555], [6812,1543], [6901,1506],
|
||||
// [7084,1351], [7193,1305], [7250,1205], [7255,1155], [7305,1152], [7420,1087], [7522,995], [7751,608],
|
||||
// [7771,455], [7780,278], [7856,218], [7914,192], [7928,143], [7860,170], [7910,80], [7931,39], [7944,-2],
|
||||
// [8009,4], [8490,46], [10165,189], [10542,220], [10575,240], [10654,239], [10701,218], [10735,242],
|
||||
// [10776,308], [10811,372], [10834,527], [10781,598], [10685,625], [10595,763], [10588,913], [10553,965],
|
||||
// [10572,995], [10585,1062], [10617,1120], [10672,1153], [10755,1340], [10781,1436], [10811,1637],
|
||||
// [10791,1768], [10771,1807], [10824,1852], [10927,2015], [10995,2073], [11184,2212], [11204,2270],
|
||||
// [11174,2312], [11045,2430], [10931,2585], [10881,2678], [10806,2818], [10739,2936], [10670,3102],
|
||||
// [10670,3166], [4823,8540], [4804,12775], [4798,12800], [2515,12800], [232,12800]
|
||||
// ];
|
||||
// newpoly = simplify_path(calif, 120, closed=true);
|
||||
// left(4000) polygon(calif);
|
||||
// right(4000) color("lightgreen") polygon(newpoly);
|
||||
|
||||
function simplify_path(path, maxerr, closed=false) =
|
||||
let(path = force_path(path))
|
||||
assert(is_path(path), "\nInvalid path or 1-region.")
|
||||
assert(is_num(maxerr) && maxerr>0, "\nParameter 'maxerr' must be a positive number.")
|
||||
let(
|
||||
n = len(path),
|
||||
unclosed = _err_resample(path, maxerr, n) // get simplified path including original endpoints
|
||||
) closed ? let( // search for new corners between the corners found on either side of the end points
|
||||
nu = len(unclosed),
|
||||
cornerpath = [
|
||||
for(i=[unclosed[nu-2]:n-1]) path[i],
|
||||
for(i=[0:unclosed[1]]) path[i]
|
||||
],
|
||||
corner_resample = _err_resample(cornerpath, maxerr, len(cornerpath)),
|
||||
nc = len(corner_resample)
|
||||
) [
|
||||
for(i=[1:nu-2]) path[unclosed[i]], // exclude endpoints
|
||||
if(nc>2) for(i=[1:nc-2]) cornerpath[corner_resample[i]] // insert new corners if any
|
||||
]
|
||||
: [ for(i=unclosed) path[i] ];
|
||||
|
||||
/// return a resampled path based on error deviation, retaining path endpoints (i.e. assume path is not closed)
|
||||
function _err_resample(path, maxerr, n, i1=0, i2=2, resultidx=[0], iter=0) =
|
||||
n <= 2 ? path :
|
||||
i2 >= n || i2-i1<2 ? concat(resultidx, [n-1]) : let(
|
||||
dists = [ for(i=[i1+1:i2-1]) let(j=i%n) point_line_distance(path[j], [path[i1], path[i2%n]]) ],
|
||||
imaxdist = max_index(dists),
|
||||
newfound = dists[imaxdist] >= maxerr,
|
||||
newidx1 = newfound ? i1+imaxdist+1 : i1,
|
||||
newidx2 = newfound ? min(newidx1+2, n) : min(i2+1,n)
|
||||
)
|
||||
_err_resample(path, maxerr, n, newidx1, newidx2, newfound ? concat(resultidx, [newidx1]) : resultidx, iter+1);
|
||||
|
||||
|
||||
|
||||
// Section: Path Geometry
|
||||
|
||||
// Function: is_path_simple()
|
||||
@@ -583,7 +691,7 @@ function resample_path(path, n, spacing, keep_corners, closed=true) =
|
||||
function is_path_simple(path, closed, eps=EPSILON) =
|
||||
is_1region(path) ? is_path_simple(path[0], default(closed,true), eps) :
|
||||
let(closed=default(closed,false))
|
||||
assert(is_path(path, 2),"Must give a 2D path")
|
||||
assert(is_path(path, 2),"\nMust give a 2D path.")
|
||||
assert(is_bool(closed))
|
||||
let(
|
||||
path = deduplicate(path,closed=closed,eps=eps)
|
||||
@@ -623,8 +731,8 @@ function is_path_simple(path, closed, eps=EPSILON) =
|
||||
// color("red") translate(closest[1]) circle(d=3, $fn=12);
|
||||
function path_closest_point(path, pt, closed=true) =
|
||||
let(path = force_path(path))
|
||||
assert(is_path(path), "Input must be a path")
|
||||
assert(is_vector(pt, len(path[0])), "Input pt must be a compatible vector")
|
||||
assert(is_path(path), "\nInput must be a path.")
|
||||
assert(is_vector(pt, len(path[0])), "\nInput pt must be a compatible vector.")
|
||||
assert(is_bool(closed))
|
||||
let(
|
||||
pts = [for (seg=pair(path,closed)) line_closest_point(seg,pt,SEGMENT)],
|
||||
@@ -641,7 +749,7 @@ function path_closest_point(path, pt, closed=true) =
|
||||
// tangs = path_tangents(path, [closed], [uniform]);
|
||||
// Description:
|
||||
// Compute the tangent vector to the input {{path}}. The derivative approximation is described in deriv().
|
||||
// The returns vectors will be normalized to length 1. If any derivatives are zero then
|
||||
// The returned vectors are normalized to length 1. If any derivatives are zero then
|
||||
// the function fails with an error. If you set `uniform` to false then the sampling is
|
||||
// assumed to be non-uniform and the derivative is computed with adjustments to produce corrected
|
||||
// values.
|
||||
@@ -649,7 +757,7 @@ function path_closest_point(path, pt, closed=true) =
|
||||
// path = path of any dimension or a 1-region
|
||||
// closed = set to true of the path is closed. Default: false
|
||||
// uniform = set to false to correct for non-uniform sampling. Default: true
|
||||
// Example(2D): A shape with non-uniform sampling gives distorted derivatives that may be undesirable. Note that derivatives tilt towards the long edges of the rectangle.
|
||||
// Example(2D): A shape with non-uniform sampling gives distorted derivatives that may be undesirable. Derivatives tilt toward the long edges of the rectangle.
|
||||
// rect = square([10,3]);
|
||||
// tangents = path_tangents(rect,closed=true);
|
||||
// stroke(rect,closed=true, width=0.25);
|
||||
@@ -683,7 +791,7 @@ function path_tangents(path, closed, uniform=true) =
|
||||
// path tangent and lies in the plane of the curve. For 3d paths we define the plane of the curve
|
||||
// at path {{point}} i to be the plane defined by point i and its two neighbors. At the endpoints of open paths
|
||||
// we use the three end points. For 3d paths the computed normal is the one lying in this plane that points
|
||||
// towards the center of curvature at that path point. For 2D paths, which lie in the xy plane, the normal
|
||||
// toward the center of curvature at that path point. For 2D paths, which lie in the xy plane, the normal
|
||||
// is the path pointing to the right of the direction the path is traveling. If points are collinear then
|
||||
// a 3d path has no center of curvature, and hence the
|
||||
// normal is not uniquely defined. In this case the function issues an error.
|
||||
@@ -702,7 +810,7 @@ function path_normals(path, tangents, closed) =
|
||||
tangents = default(tangents, path_tangents(path,closed)),
|
||||
dim=len(path[0])
|
||||
)
|
||||
assert(is_path(tangents) && len(tangents[0])==dim,"Dimensions of path and tangents must match")
|
||||
assert(is_path(tangents) && len(tangents[0])==dim,"\nDimensions of path and tangents must match.")
|
||||
[
|
||||
for(i=idx(path))
|
||||
let(
|
||||
@@ -712,7 +820,7 @@ function path_normals(path, tangents, closed) =
|
||||
)
|
||||
dim == 2 ? [tangents[i].y,-tangents[i].x]
|
||||
: let( v=cross(cross(pts[1]-pts[0], pts[2]-pts[0]),tangents[i]))
|
||||
assert(norm(v)>EPSILON, "3D path contains collinear points")
|
||||
assert(norm(v)>EPSILON, "\n3D path contains collinear points.")
|
||||
unit(v)
|
||||
];
|
||||
|
||||
@@ -757,7 +865,7 @@ function path_curvature(path, closed) =
|
||||
// path = 3D path
|
||||
// closed = if true then treat path as a polygon. Default: false
|
||||
function path_torsion(path, closed=false) =
|
||||
assert(is_path(path,3), "Input path must be a 3d path")
|
||||
assert(is_path(path,3), "\nInput path must be a 3d path.")
|
||||
assert(is_bool(closed))
|
||||
let(
|
||||
d1 = deriv(path,closed=closed),
|
||||
@@ -810,12 +918,12 @@ function surface_normals(surf, col_wrap=false, row_wrap=false) =
|
||||
// Description:
|
||||
// Given a list of distances in `cutdist`, cut the {{path}} into
|
||||
// subpaths at those lengths, returning a list of paths.
|
||||
// If the input path is closed then the final path will include the
|
||||
// If the input path is closed then the final path includes the
|
||||
// original starting {{point}}. The list of cut distances must be
|
||||
// in ascending order and should not include the endpoints: 0
|
||||
// or `len(path)`. If you repeat a distance you will get an
|
||||
// or `len(path)`. If you repeat a distance, you get an
|
||||
// empty list in that position in the output. If you give an
|
||||
// empty cutdist array you will get the input path as output
|
||||
// empty cutdist array, you get the input path as output
|
||||
// (without the final vertex doubled in the case of a closed path).
|
||||
// Arguments:
|
||||
// path = path of any dimension or a 1-region
|
||||
@@ -831,8 +939,8 @@ function path_cut(path,cutdist,closed) =
|
||||
let(closed=default(closed,false))
|
||||
assert(is_bool(closed))
|
||||
assert(is_vector(cutdist))
|
||||
assert(last(cutdist)<path_length(path,closed=closed)-EPSILON,"Cut distances must be smaller than the path length")
|
||||
assert(cutdist[0]>EPSILON, "Cut distances must be strictly positive")
|
||||
assert(last(cutdist)<path_length(path,closed=closed)-EPSILON,"\nCut distances must be smaller than the path length.")
|
||||
assert(cutdist[0]>EPSILON, "\nCut distances must be strictly positive.")
|
||||
let(
|
||||
cutlist = path_cut_points(path,cutdist,closed=closed)
|
||||
)
|
||||
@@ -874,15 +982,15 @@ function _path_cut_getpaths(path, cutlist, closed) =
|
||||
// points and indices of the next point in the path after that point. So for example, a return
|
||||
// value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after
|
||||
// this point is path[5]. If the path is too short then path_cut_points returns undef. If you set
|
||||
// `direction` to true then `path_cut_points` will also return the tangent vector to the path and a normal
|
||||
// `direction` to true then `path_cut_points` also returns the tangent vector to the path and a normal
|
||||
// vector to the path. It tries to find a normal vector that is coplanar to the path near the cut
|
||||
// point. If this fails it will return a normal vector parallel to the xy plane. The output with
|
||||
// direction vectors will be `[point, next_index, tangent, normal]`.
|
||||
// point. If this fails, it returns a normal vector parallel to the xy plane. The output with
|
||||
// direction vectors are in the form `[point, next_index, tangent, normal]`.
|
||||
// .
|
||||
// If you give the very last point of the path as a cut point then the returned index will be
|
||||
// one larger than the last index (so it will not be a valid index). If you use the closed
|
||||
// option then the returned index will be equal to the path length for cuts along the closing
|
||||
// path segment, and if you give a point equal to the path length you will get an
|
||||
// If you give the last point of the path as a cut point, then the returned index is
|
||||
// one larger than the last index (so it would not be a valid index). If you use the closed
|
||||
// option then the returned index is equal to the path length for cuts along the closing
|
||||
// path segment, and if you give a point equal to the path length you get an
|
||||
// index of len(path)+1 for the index.
|
||||
//
|
||||
// Arguments:
|
||||
@@ -900,10 +1008,10 @@ function _path_cut_getpaths(path, cutlist, closed) =
|
||||
// path_cut_points(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef]
|
||||
function path_cut_points(path, cutdist, closed=false, direction=false) =
|
||||
let(long_enough = len(path) >= (closed ? 3 : 2))
|
||||
assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points")
|
||||
assert(long_enough,len(path)<2 ? "\nTwo points needed to define a path." : "\nClosed path must include three points.")
|
||||
is_num(cutdist) ? path_cut_points(path, [cutdist],closed, direction)[0] :
|
||||
assert(is_vector(cutdist))
|
||||
assert(is_increasing(cutdist), "Cut distances must be an increasing list")
|
||||
assert(is_increasing(cutdist), "\nCut distances must be an increasing list.")
|
||||
let(cuts = path_cut_points_recurse(path,cutdist,closed))
|
||||
!direction
|
||||
? cuts
|
||||
@@ -928,10 +1036,10 @@ function path_cut_points_recurse(path, dists, closed=false, pind=0, dtotal=0, di
|
||||
|
||||
// Search for a single cut point in the path
|
||||
function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) =
|
||||
// If we get to the very end of the path (ind is last point or wraparound for closed case) then
|
||||
// If we get to the end of the path (ind is last point or wraparound for closed case) then
|
||||
// check if we are within epsilon of the final path point. If not we're out of path, so we fail
|
||||
ind==len(path)-(closed?0:1) ?
|
||||
assert(dist<eps,"Path is too short for specified cut distance")
|
||||
assert(dist<eps,"\nPath is too short for specified cut distance.")
|
||||
[select(path,ind),ind+1]
|
||||
:let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
|
||||
[lerp(path[ind],select(path,ind+1),dist/d), ind+1] :
|
||||
@@ -1009,7 +1117,7 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
|
||||
// paths = split_path_at_self_crossings(path, [closed], [eps]);
|
||||
// Description:
|
||||
// Splits a 2D {{path}} into sub-paths wherever the original path crosses itself.
|
||||
// Splits may occur mid-segment, so new vertices will be created at the intersection points.
|
||||
// Splits may occur mid-segment, so new vertices are created at the intersection points.
|
||||
// Returns a list of the resulting subpaths.
|
||||
// Arguments:
|
||||
// path = A 2D path or a 1-region.
|
||||
@@ -1021,7 +1129,7 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
|
||||
// rainbow(paths) stroke($item, closed=false, width=3);
|
||||
function split_path_at_self_crossings(path, closed=true, eps=EPSILON) =
|
||||
let(path = force_path(path))
|
||||
assert(is_path(path,2), "Must give a 2D path")
|
||||
assert(is_path(path,2), "\nMust give a 2D path.")
|
||||
assert(is_bool(closed))
|
||||
let(
|
||||
path = list_unwrap(path, eps=eps),
|
||||
@@ -1081,7 +1189,7 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
|
||||
// Description:
|
||||
// Given a possibly self-intersecting 2D {{polygon}}, constructs a representation of the original polygon as a list of
|
||||
// non-intersecting simple polygons. If nonzero is set to true then it uses the nonzero method for defining polygon membership.
|
||||
// For simple cases, such as the pentagram, this will produce the outer perimeter of a self-intersecting polygon.
|
||||
// For simple cases, such as the pentagram, this produces the outer perimeter of a self-intersecting polygon.
|
||||
// Arguments:
|
||||
// poly = a 2D polygon or 1-region
|
||||
// nonzero = If true use the nonzero method for checking if a point is in a polygon. Otherwise use the even-odd method. Default: false
|
||||
@@ -1093,7 +1201,7 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
|
||||
// ];
|
||||
// splitpolys = polygon_parts(poly);
|
||||
// rainbow(splitpolys) stroke($item, closed=true, width=3);
|
||||
// Example(2D,NoAxes): With nonzero=false you get even-odd mode which matches OpenSCAD, so the pentagram breaks apart into its five points.
|
||||
// Example(2D,NoAxes): With nonzero=false you get even-odd mode that matches OpenSCAD, so the pentagram breaks apart into its five points.
|
||||
// pentagram = turtle(["move",100,"left",144], repeat=4);
|
||||
// left(100)polygon(pentagram);
|
||||
// rainbow(polygon_parts(pentagram,nonzero=false))
|
||||
@@ -1136,7 +1244,7 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
|
||||
// move([16,-14])rainbow(polygon_parts(poly,nonzero=true)) polygon($item);
|
||||
function polygon_parts(poly, nonzero=false, eps=EPSILON) =
|
||||
let(poly = force_path(poly))
|
||||
assert(is_path(poly,2), "Must give 2D polygon")
|
||||
assert(is_path(poly,2), "\nMust give 2D polygon.")
|
||||
assert(is_bool(nonzero))
|
||||
let(
|
||||
poly = list_unwrap(poly, eps=eps),
|
||||
@@ -1242,7 +1350,7 @@ function _assemble_a_path_from_fragments(fragments, rightmost=true, startfrag=0,
|
||||
/// _assemble_path_fragments(subpaths);
|
||||
/// Description:
|
||||
/// Given a list of paths, assembles them together into complete closed polygon paths if it can.
|
||||
/// Polygons with area < eps will be discarded and not returned.
|
||||
/// Polygons with area < eps are discarded and not returned.
|
||||
/// Arguments:
|
||||
/// fragments = List of paths to be assembled into complete polygons.
|
||||
/// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
|
||||
@@ -1282,10 +1390,10 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) =
|
||||
///
|
||||
/// Takes a list of paths that are in the correct direction and assembles
|
||||
/// them into a list of paths. Returns a list of assembled paths.
|
||||
/// If closed is false then any paths that are closed will have duplicate
|
||||
/// endpoints, and open paths will not have duplicate endpoints.
|
||||
/// If closed=true then all paths are assumed closed and none of the returned
|
||||
/// paths will have duplicate endpoints.
|
||||
/// If `closed=false` then any paths that are closed have duplicate
|
||||
/// endpoints, and open paths do not have duplicate endpoints.
|
||||
/// If `closed=true` then all paths are assumed closed and none of the returned
|
||||
/// paths have duplicate endpoints.
|
||||
///
|
||||
/// It is assumed that the paths do not intersect each other.
|
||||
/// Paths can be in any dimension
|
||||
|
Reference in New Issue
Block a user