Added reduce_path(), minor wordsmithing of docs

This commit is contained in:
Alex Matulich
2025-06-29 18:06:47 -07:00
parent 821f80f1e9
commit f4180d5662

View File

@@ -32,7 +32,7 @@
// Returns true if `list` is a {{path}}. A path is a list of two or more numeric vectors (AKA {{points}}). // 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 may only 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 // 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.) // returns `false` on 1-regions.)
// Example: // Example:
// bool1 = is_path([[3,4],[5,6]]); // Returns true // bool1 = is_path([[3,4],[5,6]]); // Returns true
@@ -77,7 +77,7 @@ function is_path(list, dim=[2,3], fast=false) =
// name = name of parameter to use in error message. Default: "path" // name = name of parameter to use in error message. Default: "path"
function is_1region(path, name="path") = function is_1region(path, name="path") =
!is_region(path)? false !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; true;
@@ -98,7 +98,7 @@ function is_1region(path, name="path") =
// name = name of parameter to use in error message. Default: "path" // name = name of parameter to use in error message. Default: "path"
function force_path(path, name="path") = function force_path(path, name="path") =
is_region(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[0]
: path; : path;
@@ -139,7 +139,7 @@ function _path_select(path, s1, u1, s2, u2, closed=false) =
// SynTags: Path // SynTags: Path
// Topics: Paths, Regions // Topics: Paths, Regions
// Description: // 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. // endpoints may be removed.
// Usage: // Usage:
// path_merge_collinear(path, [eps]) // 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) : is_1region(path) ? path_merge_collinear(path[0], default(closed,true), eps) :
let(closed=default(closed,false)) let(closed=default(closed,false))
assert(is_bool(closed)) assert(is_bool(closed))
assert( is_path(path), "Invalid path in path_merge_collinear." ) assert( is_path(path), "\nInvalid path in path_merge_collinear.")
assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "\nInvalid tolerance.")
len(path)<=2 ? path : len(path)<=2 ? path :
let(path = deduplicate(path, closed=closed)) let(path = deduplicate(path, closed=closed))
[ [
@@ -182,7 +182,7 @@ function path_merge_collinear(path, closed, eps=EPSILON) =
// echo(path_length(path)); // echo(path_length(path));
function path_length(path,closed) = function path_length(path,closed) =
is_1region(path) ? path_length(path[0], default(closed,true)) : 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)) let(closed=default(closed,false))
assert(is_bool(closed)) assert(is_bool(closed))
len(path)<2? 0 : len(path)<2? 0 :
@@ -203,7 +203,7 @@ function path_length(path,closed) =
function path_segment_lengths(path, closed) = function path_segment_lengths(path, closed) =
is_1region(path) ? path_segment_lengths(path[0], default(closed,true)) : is_1region(path) ? path_segment_lengths(path[0], default(closed,true)) :
let(closed=default(closed,false)) 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)) assert(is_bool(closed))
[ [
for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), 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]); // fracs = path_length_fractions(path, [closed]);
// Description: // Description:
// Returns the distance fraction of each point in the {{path}} along the path, so the first // 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 // will have one extra point because of the final connecting segment that connects the last
// point of the path to the first point. // point of the path to the first point.
// Arguments: // 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 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. /// 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. /// where non-parallel segments intersect.
/// Arguments: /// Arguments:
/// path = The path to find self intersections of. /// path = The path to find self intersections of.
@@ -301,6 +301,7 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
[isect[0], i, isect[1], j, isect[2]] [isect[0], i, isect[1], j, isect[2]]
]; ];
// Section: Resampling - changing the number of points in a path // Section: Resampling - changing the number of points in a path
@@ -309,7 +310,7 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
// entry is rounded to an integer and the sum is the same as // 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 // that of the input. Works by rounding an entry in the list
// and passing the rounding error forward to the next entry. // 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) = function _sum_preserving_round(data, index=0) =
index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) : index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) :
let( let(
@@ -346,9 +347,9 @@ function _sum_preserving_round(data, index=0) =
// exactly as requested. For example, if you subdivide a four point square and request `n=13` then one edge will have // 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. // an extra point compared to the others.
// If you set `exact=false` then the // If you set `exact=false` then the
// algorithm will favor uniformity and the output path may have a different number of // algorithm favors 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 // points than you requested, but the sampling is still 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. // 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 // 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 // means that the number of points on a segment is based on its length, but the points are still
@@ -357,8 +358,8 @@ function _sum_preserving_round(data, index=0) =
// specifies the desired point count on each segment: with vector valued `n` the `subdivide_path` // 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 // 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 // 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 // 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 will be sum(n)+1. // 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. // 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, // Each segment is subdivided into the largest number of segments meeting your requirement. As above,
@@ -400,10 +401,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 // 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); // mypath = subdivide_path(pentagon(side=2), 18, exact=false);
// move_copies(mypath)circle(r=.1,$fn=32); // 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); // mypath = subdivide_path(pentagon(side=2), refine=3);
// move_copies(mypath)circle(r=.1,$fn=32); // 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); // mypath = subdivide_path(square([8,2],center=true), refine=3);
// move_copies(mypath)circle(r=.2,$fn=32); // move_copies(mypath)circle(r=.2,$fn=32);
// Example(2D): In this example with maxlen, every side gets a different number of new points // Example(2D): In this example with maxlen, every side gets a different number of new points
@@ -416,11 +417,11 @@ function _sum_preserving_round(data, index=0) =
function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) = function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
let(path = force_path(path)) let(path = force_path(path))
assert(is_path(path)) assert(is_path(path))
assert(num_defined([n,refine,maxlen]),"Must give exactly one of n, refine, and maxlen") assert(num_defined([n,refine,maxlen]), "\nMust give exactly one of n, refine, and maxlen.")
refine==1 || n==len(path) ? path : refine==1 || n==len(path) ? path :
is_def(maxlen) ? is_def(maxlen) ?
assert(is_undef(method), "Cannot give method with maxlen") assert(is_undef(method), "\nCannot give method with maxlen.")
assert(is_undef(exact), "Cannot give exact with maxlen") assert(is_undef(exact), "\nCannot give exact with maxlen.")
[ [
for (p=pair(path,closed)) for (p=pair(path,closed))
let(steps = ceil(norm(p[1]-p[0])/maxlen)) let(steps = ceil(norm(p[1]-p[0])/maxlen))
@@ -438,18 +439,18 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
!is_undef(refine)? len(path) * refine : !is_undef(refine)? len(path) * refine :
undef 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( let(
count = len(path) - (closed?0:1), count = len(path) - (closed?0:1),
add_guess = method=="segment"? add_guess = method=="segment"?
( (
is_list(n) 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) add_scalar(n,-1)
: repeat((n-len(path)) / count, count) : repeat((n-len(path)) / count, count)
) )
: // method=="length" : // 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( let(
path_lens = path_segment_lengths(path,closed), path_lens = path_segment_lengths(path,closed),
add_density = (n - len(path)) / sum(path_lens) add_density = (n - len(path)) / sum(path_lens)
@@ -475,13 +476,14 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
// Usage: // Usage:
// newpath = resample_path(path, n|spacing=, [closed=]); // newpath = resample_path(path, n|spacing=, [closed=]);
// Description: // 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 // {{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 // 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`. // 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. // 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 // .
// 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. // the sampling of the input. If you want very accurate output, use a lot of points for the input.
// Arguments: // Arguments:
// path = path in any dimension or a 1-region // path = path in any dimension or a 1-region
@@ -519,7 +521,7 @@ function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) =
function resample_path(path, n, spacing, keep_corners, closed=true) = function resample_path(path, n, spacing, keep_corners, closed=true) =
let(path = force_path(path)) let(path = force_path(path))
assert(is_path(path)) assert(is_path(path))
assert(num_defined([n,spacing])==1,"Must define exactly one of n and spacing") assert(num_defined([n,spacing])==1,"\nMust define exactly one of n and spacing.")
assert(n==undef || (is_integer(n) && n>0)) assert(n==undef || (is_integer(n) && n>0))
assert(spacing==undef || (is_finite(spacing) && spacing>0)) assert(spacing==undef || (is_finite(spacing) && spacing>0))
assert(is_bool(closed)) assert(is_bool(closed))
@@ -538,7 +540,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]] ], subpaths = [ for (p = pair(corners)) [for(i = [p.x:1:p.y]) path[i%pcnt]] ],
n = is_undef(n)? undef : closed? n+1 : n 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( let(
lens = [for (subpath = subpaths) path_length(subpath)], lens = [for (subpath = subpaths) path_length(subpath)],
part_ns = is_undef(n) part_ns = is_undef(n)
@@ -563,6 +565,108 @@ function resample_path(path, n, spacing, keep_corners, closed=true) =
) out; ) out;
// Function: reduce_path()
// Synopsis: Removes points from an irregular path, preserving dominant features.
// SynTags: Path
// Topics: Paths
// See Also:
// Usage:
// newpath = reduce_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. 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.
// Arguments:
// path = Path in any dimension
// 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. 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 = reduce_path(calif, 120, closed=true);
// left(4000) polygon(calif);
// right(4000) color("lightgreen") polygon(newpoly);
function reduce_path(path, maxerr, closed=false) =
assert(is_path(path), "\nInvalid path.")
assert(is_num(maxerr) && maxerr>0, "\nParameter 'maxerr' must be a positive number.")
let(
n = len(path),
unclosed = _err_resample(path, maxerr, n)
) 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 // Section: Path Geometry
// Function: is_path_simple() // Function: is_path_simple()
@@ -583,7 +687,7 @@ function resample_path(path, n, spacing, keep_corners, closed=true) =
function is_path_simple(path, closed, eps=EPSILON) = function is_path_simple(path, closed, eps=EPSILON) =
is_1region(path) ? is_path_simple(path[0], default(closed,true), eps) : is_1region(path) ? is_path_simple(path[0], default(closed,true), eps) :
let(closed=default(closed,false)) 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)) assert(is_bool(closed))
let( let(
path = deduplicate(path,closed=closed,eps=eps) path = deduplicate(path,closed=closed,eps=eps)
@@ -623,8 +727,8 @@ function is_path_simple(path, closed, eps=EPSILON) =
// color("red") translate(closest[1]) circle(d=3, $fn=12); // color("red") translate(closest[1]) circle(d=3, $fn=12);
function path_closest_point(path, pt, closed=true) = function path_closest_point(path, pt, closed=true) =
let(path = force_path(path)) let(path = force_path(path))
assert(is_path(path), "Input must be a path") assert(is_path(path), "\nInput must be a path.")
assert(is_vector(pt, len(path[0])), "Input pt must be a compatible vector") assert(is_vector(pt, len(path[0])), "\nInput pt must be a compatible vector.")
assert(is_bool(closed)) assert(is_bool(closed))
let( let(
pts = [for (seg=pair(path,closed)) line_closest_point(seg,pt,SEGMENT)], pts = [for (seg=pair(path,closed)) line_closest_point(seg,pt,SEGMENT)],
@@ -641,7 +745,7 @@ function path_closest_point(path, pt, closed=true) =
// tangs = path_tangents(path, [closed], [uniform]); // tangs = path_tangents(path, [closed], [uniform]);
// Description: // Description:
// Compute the tangent vector to the input {{path}}. The derivative approximation is described in deriv(). // 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 // 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 // assumed to be non-uniform and the derivative is computed with adjustments to produce corrected
// values. // values.
@@ -649,7 +753,7 @@ function path_closest_point(path, pt, closed=true) =
// path = path of any dimension or a 1-region // path = path of any dimension or a 1-region
// closed = set to true of the path is closed. Default: false // closed = set to true of the path is closed. Default: false
// uniform = set to false to correct for non-uniform sampling. Default: true // 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]); // rect = square([10,3]);
// tangents = path_tangents(rect,closed=true); // tangents = path_tangents(rect,closed=true);
// stroke(rect,closed=true, width=0.25); // stroke(rect,closed=true, width=0.25);
@@ -683,7 +787,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 // 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 // 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 // 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 // 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 // a 3d path has no center of curvature, and hence the
// normal is not uniquely defined. In this case the function issues an error. // normal is not uniquely defined. In this case the function issues an error.
@@ -702,7 +806,7 @@ function path_normals(path, tangents, closed) =
tangents = default(tangents, path_tangents(path,closed)), tangents = default(tangents, path_tangents(path,closed)),
dim=len(path[0]) 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)) for(i=idx(path))
let( let(
@@ -712,7 +816,7 @@ function path_normals(path, tangents, closed) =
) )
dim == 2 ? [tangents[i].y,-tangents[i].x] dim == 2 ? [tangents[i].y,-tangents[i].x]
: let( v=cross(cross(pts[1]-pts[0], pts[2]-pts[0]),tangents[i])) : 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) unit(v)
]; ];
@@ -757,7 +861,7 @@ function path_curvature(path, closed) =
// path = 3D path // path = 3D path
// closed = if true then treat path as a polygon. Default: false // closed = if true then treat path as a polygon. Default: false
function path_torsion(path, closed=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)) assert(is_bool(closed))
let( let(
d1 = deriv(path,closed=closed), d1 = deriv(path,closed=closed),
@@ -810,12 +914,12 @@ function surface_normals(surf, col_wrap=false, row_wrap=false) =
// Description: // Description:
// Given a list of distances in `cutdist`, cut the {{path}} into // Given a list of distances in `cutdist`, cut the {{path}} into
// subpaths at those lengths, returning a list of paths. // 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 // original starting {{point}}. The list of cut distances must be
// in ascending order and should not include the endpoints: 0 // 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 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). // (without the final vertex doubled in the case of a closed path).
// Arguments: // Arguments:
// path = path of any dimension or a 1-region // path = path of any dimension or a 1-region
@@ -831,8 +935,8 @@ function path_cut(path,cutdist,closed) =
let(closed=default(closed,false)) let(closed=default(closed,false))
assert(is_bool(closed)) assert(is_bool(closed))
assert(is_vector(cutdist)) assert(is_vector(cutdist))
assert(last(cutdist)<path_length(path,closed=closed)-EPSILON,"Cut distances must be smaller than the path length") assert(last(cutdist)<path_length(path,closed=closed)-EPSILON,"\nCut distances must be smaller than the path length.")
assert(cutdist[0]>EPSILON, "Cut distances must be strictly positive") assert(cutdist[0]>EPSILON, "\nCut distances must be strictly positive.")
let( let(
cutlist = path_cut_points(path,cutdist,closed=closed) cutlist = path_cut_points(path,cutdist,closed=closed)
) )
@@ -874,15 +978,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 // 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 // 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 // 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 // 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 // point. If this fails, it returns a normal vector parallel to the xy plane. The output with
// direction vectors will be `[point, next_index, tangent, normal]`. // 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 // If you give the very last point of the path as a cut point, then the returned index is
// one larger than the last index (so it will not be a valid index). If you use the closed // one larger than the last index (so it would 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 // 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 will get an // path segment, and if you give a point equal to the path length you get an
// index of len(path)+1 for the index. // index of len(path)+1 for the index.
// //
// Arguments: // Arguments:
@@ -900,10 +1004,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] // 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) = function path_cut_points(path, cutdist, closed=false, direction=false) =
let(long_enough = len(path) >= (closed ? 3 : 2)) 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] : is_num(cutdist) ? path_cut_points(path, [cutdist],closed, direction)[0] :
assert(is_vector(cutdist)) 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)) let(cuts = path_cut_points_recurse(path,cutdist,closed))
!direction !direction
? cuts ? cuts
@@ -931,7 +1035,7 @@ 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 very 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 // 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) ? 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] [select(path,ind),ind+1]
:let(d = norm(path[ind]-select(path,ind+1))) d > dist ? :let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
[lerp(path[ind],select(path,ind+1),dist/d), ind+1] : [lerp(path[ind],select(path,ind+1),dist/d), ind+1] :
@@ -1009,7 +1113,7 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
// paths = split_path_at_self_crossings(path, [closed], [eps]); // paths = split_path_at_self_crossings(path, [closed], [eps]);
// Description: // Description:
// Splits a 2D {{path}} into sub-paths wherever the original path crosses itself. // 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. // Returns a list of the resulting subpaths.
// Arguments: // Arguments:
// path = A 2D path or a 1-region. // path = A 2D path or a 1-region.
@@ -1021,7 +1125,7 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
// rainbow(paths) stroke($item, closed=false, width=3); // rainbow(paths) stroke($item, closed=false, width=3);
function split_path_at_self_crossings(path, closed=true, eps=EPSILON) = function split_path_at_self_crossings(path, closed=true, eps=EPSILON) =
let(path = force_path(path)) 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)) assert(is_bool(closed))
let( let(
path = list_unwrap(path, eps=eps), path = list_unwrap(path, eps=eps),
@@ -1081,7 +1185,7 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// Description: // Description:
// Given a possibly self-intersecting 2D {{polygon}}, constructs a representation of the original polygon as a list of // 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. // 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: // Arguments:
// poly = a 2D polygon or 1-region // 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 // nonzero = If true use the nonzero method for checking if a point is in a polygon. Otherwise use the even-odd method. Default: false
@@ -1136,7 +1240,7 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// move([16,-14])rainbow(polygon_parts(poly,nonzero=true)) polygon($item); // move([16,-14])rainbow(polygon_parts(poly,nonzero=true)) polygon($item);
function polygon_parts(poly, nonzero=false, eps=EPSILON) = function polygon_parts(poly, nonzero=false, eps=EPSILON) =
let(poly = force_path(poly)) 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)) assert(is_bool(nonzero))
let( let(
poly = list_unwrap(poly, eps=eps), poly = list_unwrap(poly, eps=eps),
@@ -1242,7 +1346,7 @@ function _assemble_a_path_from_fragments(fragments, rightmost=true, startfrag=0,
/// _assemble_path_fragments(subpaths); /// _assemble_path_fragments(subpaths);
/// Description: /// Description:
/// Given a list of paths, assembles them together into complete closed polygon paths if it can. /// 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: /// Arguments:
/// fragments = List of paths to be assembled into complete polygons. /// 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) /// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
@@ -1282,10 +1386,10 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) =
/// ///
/// Takes a list of paths that are in the correct direction and assembles /// 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. /// 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 /// If `closed=false` then any paths that are closed have duplicate
/// endpoints, and open paths will not have duplicate endpoints. /// endpoints, and open paths do not have duplicate endpoints.
/// If closed=true then all paths are assumed closed and none of the returned /// If `closed=true` then all paths are assumed closed and none of the returned
/// paths will have duplicate endpoints. /// paths have duplicate endpoints.
/// ///
/// It is assumed that the paths do not intersect each other. /// It is assumed that the paths do not intersect each other.
/// Paths can be in any dimension /// Paths can be in any dimension