From 2f895cb8d1a6e9af680dc4474a67652f4c12ea8f Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 22 Sep 2021 14:59:18 -0400 Subject: [PATCH] hiding some infrastructure, added is_path_simple() --- geometry.scad | 7 +- paths.scad | 525 ++++++++++++++++++++++++-------------------------- regions.scad | 105 +++++----- rounding.scad | 8 +- shapes2d.scad | 40 +++- 5 files changed, 353 insertions(+), 332 deletions(-) diff --git a/geometry.scad b/geometry.scad index 251d8e7..514d4e4 100644 --- a/geometry.scad +++ b/geometry.scad @@ -113,7 +113,7 @@ function point_line_distance(pt, line, bounded=false) = // Function: segment_distance() // Usage: -// dist = segment_distance(seg1, seg2); +// dist = segment_distance(seg1, seg2, [eps]); // Topics: Geometry, Segments, Distance // See Also: convex_collision(), convex_distance() // Description: @@ -121,12 +121,13 @@ function point_line_distance(pt, line, bounded=false) = // Arguments: // seg1 = The list of two points representing the first line segment to check the distance of. // seg2 = The list of two points representing the second line segment to check the distance of. +// eps = tolerance for point comparisons // Example: // dist = segment_distance([[-14,3], [-15,9]], [[-10,0], [10,0]]); // Returns: 5 // dist2 = segment_distance([[-5,5], [5,-5]], [[-10,3], [10,-3]]); // Returns: 0 -function segment_distance(seg1, seg2) = +function segment_distance(seg1, seg2,eps=EPSILON) = assert( is_matrix(concat(seg1,seg2),4), "Inputs should be two valid segments." ) - convex_distance(seg1,seg2); + convex_distance(seg1,seg2,eps); // Function: line_normal() diff --git a/paths.scad b/paths.scad index 4cdfcde..961c6d5 100644 --- a/paths.scad +++ b/paths.scad @@ -6,7 +6,7 @@ ////////////////////////////////////////////////////////////////////// -// Section: Functions +// Section: Utility Functions // Function: is_path() @@ -72,7 +72,7 @@ function cleanup_path(path, eps=EPSILON) = is_closed_path(path,eps=eps)? [for (i=[0:1:len(path)-2]) path[i]] : path; -/// internal Function: _path_select() +/// Internal Function: _path_select() /// Usage: /// _path_select(path,s1,u1,s2,u2,[closed]): /// Description: @@ -125,6 +125,8 @@ function path_merge_collinear(path, closed=false, eps=EPSILON) = ) [for (i=indices) path[i]]; +// Section: Path length calculation + // Function: path_length() // Usage: @@ -179,54 +181,30 @@ function path_length_fractions(path, closed=false) = ) partial_len / total_len; -// Function: path_closest_point() -// Usage: -// path_closest_point(path, pt); -// Description: -// Finds the closest path segment, and point on that segment to the given point. -// Returns `[SEGNUM, POINT]` -// Arguments: -// path = The path to find the closest point on. -// pt = the point to find the closest point to. -// Example(2D): -// path = circle(d=100,$fn=6); -// pt = [20,10]; -// closest = path_closest_point(path, pt); -// stroke(path, closed=true); -// color("blue") translate(pt) circle(d=3, $fn=12); -// color("red") translate(closest[1]) circle(d=3, $fn=12); -function path_closest_point(path, pt) = - let( - pts = [for (seg=idx(path)) line_closest_point(select(path,seg,seg+1),pt,SEGMENT)], - dists = [for (p=pts) norm(p-pt)], - min_seg = min_index(dists) - ) [min_seg, pts[min_seg]]; - - -// Function: path_self_intersections() -// Usage: -// isects = path_self_intersections(path, [eps]); -// Description: -// Locates all self intersections of the given path. Returns a list of intersections, where -// each intersection is a list like [POINT, SEGNUM1, PROPORTION1, SEGNUM2, PROPORTION2] where -// POINT is the coordinates of the intersection point, SEGNUMs are the integer indices of the -// intersecting segments along the path, and the PROPORTIONS are the 0.0 to 1.0 proportions -// 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. -// Arguments: -// path = The path to find self intersections of. -// closed = If true, treat path like a closed polygon. Default: true -// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9) -// Example(2D): -// path = [ -// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] -// ]; -// isects = path_self_intersections(path, closed=true); -// // isects == [[[-33.3333, 0], 0, 0.666667, 4, 0.333333], [[33.3333, 0], 1, 0.333333, 3, 0.666667]] -// stroke(path, closed=true, width=1); -// for (isect=isects) translate(isect[0]) color("blue") sphere(d=10); -function path_self_intersections(path, closed=true, eps=EPSILON) = +/// Internal Function: _path_self_intersections() +/// Usage: +/// isects = _path_self_intersections(path, [closed], [eps]); +/// Description: +/// Locates all self intersections of the given path. Returns a list of intersections, where +/// each intersection is a list like [POINT, SEGNUM1, PROPORTION1, SEGNUM2, PROPORTION2] where +/// POINT is the coordinates of the intersection point, SEGNUMs are the integer indices of the +/// intersecting segments along the path, and the PROPORTIONS are the 0.0 to 1.0 proportions +/// 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. +/// Arguments: +/// path = The path to find self intersections of. +/// closed = If true, treat path like a closed polygon. Default: true +/// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9) +/// Example(2D): +/// path = [ +/// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] +/// ]; +/// isects = _path_self_intersections(path, closed=true); +/// // isects == [[[-33.3333, 0], 0, 0.666667, 4, 0.333333], [[33.3333, 0], 1, 0.333333, 3, 0.666667]] +/// stroke(path, closed=true, width=1); +/// for (isect=isects) translate(isect[0]) color("blue") sphere(d=10); +function _path_self_intersections(path, closed=true, eps=EPSILON) = let( path = cleanup_path(path, eps=eps), plen = len(path) @@ -261,7 +239,230 @@ function path_self_intersections(path, closed=true, eps=EPSILON) = -// Section: Geometric Properties of Paths +// Section: Resampling: changing the number of points in a path + + +// Input `data` is a list that sums to an integer. +// Returns rounded version of input data so that every +// 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. +function _sum_preserving_round(data, index=0) = + index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) : + let( + newval = round(data[index]), + error = newval - data[index] + ) _sum_preserving_round( + list_set(data, [index,index+1], [newval, data[index+1]-error]), + index+1 + ); + + +// Function: subdivide_path() +// Usage: +// newpath = subdivide_path(path, [N|refine], method); +// Description: +// Takes a path as input (closed or open) and subdivides the path to produce a more +// finely sampled path. The new points can be distributed proportional to length +// (`method="length"`) or they can be divided up evenly among all the path segments +// (`method="segment"`). If the extra points don't fit evenly on the path then the +// algorithm attempts to distribute them uniformly. The `exact` option requires that +// the final length is exactly as requested. If you set it to `false` then the +// algorithm will favor uniformity and the output path may have a different number of +// points due to rounding error. +// . +// With the `"segment"` method you can also specify a vector of lengths. This vector, +// `N` specfies the desired point count on each segment: with vector input, `subdivide_path` +// attempts to place `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. +// Arguments: +// path = path to subdivide +// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i. +// refine = number of points to add each segment. +// 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. Default: True +// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices evenly along the total path length. If `"segment"`, adds points evenly among the segments. Default: `"length"` +// Example(2D): +// mypath = subdivide_path(square([2,2],center=true), 12); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([8,2],center=true), 12); +// move_copies(mypath)circle(r=.2,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([8,2],center=true), 12, method="segment"); +// move_copies(mypath)circle(r=.2,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([2,2],center=true), 17, closed=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Specifying different numbers of points on each segment +// mypath = subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment"); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Requested point total is 14 but 15 points output due to extra end point +// mypath = subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Since 17 is not divisible by 5, a completely uniform distribution is not possible. +// mypath = subdivide_path(pentagon(side=2), 17); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): With `exact=false` a uniform distribution, but only 15 points +// mypath = subdivide_path(pentagon(side=2), 17, exact=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// 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(FlatSpin,VPD=15,VPT=[0,0,1.5]): Three-dimensional paths also work +// mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); +// move_copies(mypath)sphere(r=.1,$fn=32); +function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = + assert(is_path(path)) + assert(method=="length" || method=="segment") + assert(num_defined([N,refine]),"Must give exactly one of N and refine") + let( + N = !is_undef(N)? N : + !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") + 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") + 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\"") + let( + path_lens = concat( + [ for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i]) ], + closed? [norm(path[len(path)-1]-path[0])] : [] + ), + add_density = (N - len(path)) / sum(path_lens) + ) + path_lens * add_density, + add = exact? _sum_preserving_round(add_guess) : + [for (val=add_guess) round(val)] + ) concat( + [ + for (i=[0:1:count]) each [ + for(j=[0:1:add[i]]) + lerp(path[i],select(path,i+1), j/(add[i]+1)) + ] + ], + closed? [] : [last(path)] + ); + + + +// Function: subdivide_long_segments() +// Topics: Paths, Path Subdivision +// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() +// Usage: +// spath = subdivide_long_segments(path, maxlen, [closed=]); +// Description: +// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. +// Arguments: +// path = The path to subdivide. +// maxlen = The maximum allowed path segment length. +// --- +// closed = If true, treat path like a closed polygon. Default: true +// Example: +// path = pentagon(d=100); +// spath = subdivide_long_segments(path, 10, closed=true); +// stroke(path); +// color("lightgreen") move_copies(path) circle(d=5,$fn=12); +// color("blue") move_copies(spath) circle(d=3,$fn=12); +function subdivide_long_segments(path, maxlen, closed=false) = + assert(is_path(path)) + assert(is_finite(maxlen)) + assert(is_bool(closed)) + [ + for (p=pair(path,closed)) let( + steps = ceil(norm(p[1]-p[0])/maxlen) + ) each lerpn(p[0], p[1], steps, false), + if (!closed) last(path) + ]; + + + +// Function: resample_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 +// 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. +// If you specify `spacing` then the length you give will be 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. +// Arguments: +// path = path to resample +// N = Number of points in output +// spacing = Approximate spacing desired +// closed = set to true if path is closed. Default: false +function resample_path(path, N, spacing, closed=false) = + assert(is_path(path)) + assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") + assert(is_bool(closed)) + let( + length = path_length(path,closed), + // In the open path case decrease N by 1 so that we don't try to get + // path_cut to return the endpoint (which might fail due to rounding) + // Add last point later + N = is_def(N) ? N-(closed?0:1) : round(length/spacing), + distlist = lerpn(0,length,N,false), + cuts = _path_cut_points(path, distlist, closed=closed) + ) + [ each subindex(cuts,0), + if (!closed) last(path) // Then add last point here + ]; + + + + + +// Section: Path Geometry + +// Function: is_path_simple() +// Usage: +// bool = is_path_simple(path, [closed], [eps]); +// Description: +// Returns true if the path is simple, meaning that it has no self-intersections. +// If closed is set to true then treat the path as a polygon. +// Arguments: +// path = path to check +// closed = set to true to treat path as a polygon. Default: false +// eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9) +function is_path_simple(path, closed=false, eps=EPSILON) = + _path_self_intersections(path,closed=closed,eps=eps) == []; + + +// Function: path_closest_point() +// Usage: +// path_closest_point(path, pt); +// Description: +// Finds the closest path segment, and point on that segment to the given point. +// Returns `[SEGNUM, POINT]` +// Arguments: +// path = The path to find the closest point on. +// pt = the point to find the closest point to. +// Example(2D): +// path = circle(d=100,$fn=6); +// pt = [20,10];q +// closest = path_closest_point(path, pt); +// stroke(path, closed=true); +// color("blue") translate(pt) circle(d=3, $fn=12); +// color("red") translate(closest[1]) circle(d=3, $fn=12); +function path_closest_point(path, pt) = + let( + pts = [for (seg=idx(path)) line_closest_point(select(path,seg,seg+1),pt,SEGMENT)], + dists = [for (p=pts) norm(p-pt)], + min_seg = min_index(dists) + ) [min_seg, pts[min_seg]]; + // Function: path_tangents() // Usage: @@ -522,228 +723,6 @@ function _corner_roundover_path(p1, p2, p3, r, d) = -// Function: path_add_jitter() -// Topics: Paths -// See Also: jittered_poly(), subdivide_long_segments() -// Usage: -// jpath = path_add_jitter(path, [dist], [closed=]); -// Description: -// Adds tiny jitter offsets to collinear points in the given path so that they -// are no longer collinear. This is useful for preserving subdivision on long -// straight segments, when making geometry with `polygon()`, for use with -// `linear_exrtrude()` with a `twist()`. -// Arguments: -// path = The path to add jitter to. -// dist = The amount to jitter points by. Default: 1/512 (0.00195) -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example(3D): -// d = 100; h = 75; quadsize = 5; -// path = pentagon(d=d); -// spath = subdivide_long_segments(path, quadsize, closed=true); -// jpath = path_add_jitter(spath, closed=true); -// linear_extrude(height=h, twist=72, slices=h/quadsize) -// polygon(jpath); -function path_add_jitter(path, dist=1/512, closed=true) = - assert(is_path(path)) - assert(is_finite(dist)) - assert(is_bool(closed)) - [ - path[0], - for (i=idx(path,s=1,e=closed?-1:-2)) let( - n = line_normal([path[i-1],path[i]]) - ) path[i] + n * (is_collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), - if (!closed) last(path) - ]; - - - - -// Section: Resampling: changing the number of points in a path - - -// Input `data` is a list that sums to an integer. -// Returns rounded version of input data so that every -// 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. -function _sum_preserving_round(data, index=0) = - index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) : - let( - newval = round(data[index]), - error = newval - data[index] - ) _sum_preserving_round( - list_set(data, [index,index+1], [newval, data[index+1]-error]), - index+1 - ); - - -// Section: Changing sampling of paths - -// Function: subdivide_path() -// Usage: -// newpath = subdivide_path(path, [N|refine], method); -// Description: -// Takes a path as input (closed or open) and subdivides the path to produce a more -// finely sampled path. The new points can be distributed proportional to length -// (`method="length"`) or they can be divided up evenly among all the path segments -// (`method="segment"`). If the extra points don't fit evenly on the path then the -// algorithm attempts to distribute them uniformly. The `exact` option requires that -// the final length is exactly as requested. If you set it to `false` then the -// algorithm will favor uniformity and the output path may have a different number of -// points due to rounding error. -// . -// With the `"segment"` method you can also specify a vector of lengths. This vector, -// `N` specfies the desired point count on each segment: with vector input, `subdivide_path` -// attempts to place `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. -// Arguments: -// path = path to subdivide -// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i. -// refine = number of points to add each segment. -// 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. Default: True -// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices evenly along the total path length. If `"segment"`, adds points evenly among the segments. Default: `"length"` -// Example(2D): -// mypath = subdivide_path(square([2,2],center=true), 12); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([8,2],center=true), 12); -// move_copies(mypath)circle(r=.2,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([8,2],center=true), 12, method="segment"); -// move_copies(mypath)circle(r=.2,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([2,2],center=true), 17, closed=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Specifying different numbers of points on each segment -// mypath = subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment"); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Requested point total is 14 but 15 points output due to extra end point -// mypath = subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Since 17 is not divisible by 5, a completely uniform distribution is not possible. -// mypath = subdivide_path(pentagon(side=2), 17); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): With `exact=false` a uniform distribution, but only 15 points -// mypath = subdivide_path(pentagon(side=2), 17, exact=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// 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(FlatSpin,VPD=15,VPT=[0,0,1.5]): Three-dimensional paths also work -// mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); -// move_copies(mypath)sphere(r=.1,$fn=32); -function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = - assert(is_path(path)) - assert(method=="length" || method=="segment") - assert(num_defined([N,refine]),"Must give exactly one of N and refine") - let( - N = !is_undef(N)? N : - !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") - 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") - 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\"") - let( - path_lens = concat( - [ for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i]) ], - closed? [norm(path[len(path)-1]-path[0])] : [] - ), - add_density = (N - len(path)) / sum(path_lens) - ) - path_lens * add_density, - add = exact? _sum_preserving_round(add_guess) : - [for (val=add_guess) round(val)] - ) concat( - [ - for (i=[0:1:count]) each [ - for(j=[0:1:add[i]]) - lerp(path[i],select(path,i+1), j/(add[i]+1)) - ] - ], - closed? [] : [last(path)] - ); - - - -// Function: subdivide_long_segments() -// Topics: Paths, Path Subdivision -// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() -// Usage: -// spath = subdivide_long_segments(path, maxlen, [closed=]); -// Description: -// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. -// Arguments: -// path = The path to subdivide. -// maxlen = The maximum allowed path segment length. -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example: -// path = pentagon(d=100); -// spath = subdivide_long_segments(path, 10, closed=true); -// stroke(path); -// color("lightgreen") move_copies(path) circle(d=5,$fn=12); -// color("blue") move_copies(spath) circle(d=3,$fn=12); -function subdivide_long_segments(path, maxlen, closed=false) = - assert(is_path(path)) - assert(is_finite(maxlen)) - assert(is_bool(closed)) - [ - for (p=pair(path,closed)) let( - steps = ceil(norm(p[1]-p[0])/maxlen) - ) each lerpn(p[0], p[1], steps, false), - if (!closed) last(path) - ]; - - - -// Function: resample_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 -// 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. -// If you specify `spacing` then the length you give will be 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. -// Arguments: -// path = path to resample -// N = Number of points in output -// spacing = Approximate spacing desired -// closed = set to true if path is closed. Default: false -function resample_path(path, N, spacing, closed=false) = - assert(is_path(path)) - assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") - assert(is_bool(closed)) - let( - length = path_length(path,closed), - // In the open path case decrease N by 1 so that we don't try to get - // path_cut to return the endpoint (which might fail due to rounding) - // Add last point later - N = is_def(N) ? N-(closed?0:1) : round(length/spacing), - distlist = lerpn(0,length,N,false), - cuts = _path_cut_points(path, distlist, closed=closed) - ) - [ each subindex(cuts,0), - if (!closed) last(path) // Then add last point here - ]; - - - // Section: Breaking paths up into subpaths @@ -963,7 +942,7 @@ function split_path_at_self_crossings(path, closed=true, eps=EPSILON) = [[0, 0]], sort([ for ( - a = path_self_intersections(path, closed=closed, eps=eps), + a = _path_self_intersections(path, closed=closed, eps=eps), ss = [ [a[1],a[2]], [a[3],a[4]] ] ) if (ss[0] != undef) ss ]), diff --git a/regions.scad b/regions.scad index 3285ad1..07e1638 100644 --- a/regions.scad +++ b/regions.scad @@ -29,31 +29,17 @@ function is_region(x) = is_list(x) && is_path(x.x); function close_region(region, eps=EPSILON) = [for (path=region) close_path(path, eps=eps)]; -// Module: region() +// Function: cleanup_region() // Usage: -// region(r); +// cleanup_region(region); // Description: -// Creates 2D polygons for the given region. The region given is a list of closed 2D paths. -// Each path will be effectively exclusive-ORed from all other paths in the region, so if a -// path is inside another path, it will be effectively subtracted from it. -// Example(2D): -// region([circle(d=50), square(25,center=true)]); -// Example(2D): -// rgn = concat( -// [for (d=[50:-10:10]) circle(d=d-5)], -// [square([60,10], center=true)] -// ); -// region(rgn); -module region(r) -{ - points = flatten(r); - paths = [ - for (i=[0:1:len(r)-1]) let( - start = default(sum([for (j=[0:1:i-1]) len(r[j])]),0) - ) [for (k=[0:1:len(r[i])-1]) start+k] - ]; - polygon(points=points, paths=paths); -} +// For all paths in the given region, if the last point coincides with the first point, removes the last point. +// Arguments: +// region = The region to clean up. Given as a list of polygon paths. +// eps = Acceptable variance. Default: `EPSILON` (1e-9) +function cleanup_region(region, eps=EPSILON) = + [for (path=region) cleanup_path(path, eps=eps)]; + // Function: check_and_fix_path() @@ -91,16 +77,34 @@ function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = closed && approx(path[0], last(path))? list_head(path) : path; -// Function: cleanup_region() + + +// Module: region() // Usage: -// cleanup_region(region); +// region(r); // Description: -// For all paths in the given region, if the last point coincides with the first point, removes the last point. -// Arguments: -// region = The region to clean up. Given as a list of polygon paths. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) -function cleanup_region(region, eps=EPSILON) = - [for (path=region) cleanup_path(path, eps=eps)]; +// Creates 2D polygons for the given region. The region given is a list of closed 2D paths. +// Each path will be effectively exclusive-ORed from all other paths in the region, so if a +// path is inside another path, it will be effectively subtracted from it. +// Example(2D): +// region([circle(d=50), square(25,center=true)]); +// Example(2D): +// rgn = concat( +// [for (d=[50:-10:10]) circle(d=d-5)], +// [square([60,10], center=true)] +// ); +// region(rgn); +module region(r) +{ + points = flatten(r); + paths = [ + for (i=[0:1:len(r)-1]) let( + start = default(sum([for (j=[0:1:i-1]) len(r[j])]),0) + ) [for (k=[0:1:len(r[i])-1]) start+k] + ]; + polygon(points=points, paths=paths); +} + // Function: point_in_region() @@ -121,6 +125,7 @@ function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = ) pip==0? 0 : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); + // Function: polygons_equal() // Usage: // b = polygons_equal(poly1, poly2, [eps]) @@ -151,23 +156,23 @@ function __polygons_equal(poly1, poly2, eps, st) = max([for(d=poly1-select(poly2,st,st-1)) d*d])= len(polys)? false : polygons_equal(poly, polys[i])? true : - __poly_in_polygons(poly, polys, i+1); + __is_polygon_in_list(poly, polys, i+1); // Function: regions_equal() @@ -187,21 +192,21 @@ function regions_equal(region1, region2) = function __regions_equal(region1, region2, i) = i >= len(region1)? true : - !poly_in_polygons(region1[i], region2)? false : + !is_polygon_in_list(region1[i], region2)? false : __regions_equal(region1, region2, i+1); -// Function: region_path_crossings() -// Usage: -// region_path_crossings(path, region); -// Description: -// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. -// Arguments: -// path = The path to find crossings on. -// region = Region to test for crossings of. -// closed = If true, treat path as a closed polygon. Default: true -// eps = Acceptable variance. Default: `EPSILON` (1e-9) -function region_path_crossings(path, region, closed=true, eps=EPSILON) = sort([ +/// Internal Function: _region_path_crossings() +/// Usage: +/// _region_path_crossings(path, region); +/// Description: +/// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. +/// Arguments: +/// path = The path to find crossings on. +/// region = Region to test for crossings of. +/// closed = If true, treat path as a closed polygon. Default: true +/// eps = Acceptable variance. Default: `EPSILON` (1e-9) +function _region_path_crossings(path, region, closed=true, eps=EPSILON) = sort([ let( segs = pair(closed? close_path(path) : cleanup_path(path)) ) for ( @@ -240,7 +245,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON) let( path = deduplicate(path, eps=eps), region = [for (path=region) deduplicate(path, eps=eps)], - xings = region_path_crossings(path, region, closed=closed, eps=eps), + xings = _region_path_crossings(path, region, closed=closed, eps=eps), crossings = deduplicate( concat([[0,0]], xings, [[len(path)-1,1]]), eps=eps diff --git a/rounding.scad b/rounding.scad index 154b218..da18760 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1931,8 +1931,8 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b top_patch[i][4][4] ] ], - top_intersections = path_self_intersections(faces[0]), - bot_intersections = path_self_intersections(faces[1]), + top_simple = is_path_simple(faces[0],closed=true), + bot_simple = is_path_simple(faces[1],closed=true), // verify vertical edges verify_vert = [for(i=[0:N-1],j=[0:4]) @@ -1949,9 +1949,9 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b ) if (!is_collinear(hline_top) || !is_collinear(hline_bot)) [i,j]] ) - assert(debug || top_intersections==[], + assert(debug || top_simple, "Roundovers interfere with each other on top face: either input is self intersecting or top joint length is too large") - assert(debug || bot_intersections==[], + assert(debug || bot_simple, "Roundovers interfere with each other on bottom face: either input is self intersecting or top joint length is too large") assert(debug || (verify_vert==[] && verify_horiz==[]), "Curvature continuity failed") let( diff --git a/shapes2d.scad b/shapes2d.scad index 1849c83..d99ed29 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -810,9 +810,45 @@ module star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, +/// Internal Function: _path_add_jitter() +/// Topics: Paths +/// See Also: jittered_poly(), subdivide_long_segments() +/// Usage: +/// jpath = _path_add_jitter(path, [dist], [closed=]); +/// Description: +/// Adds tiny jitter offsets to collinear points in the given path so that they +/// are no longer collinear. This is useful for preserving subdivision on long +/// straight segments, when making geometry with `polygon()`, for use with +/// `linear_exrtrude()` with a `twist()`. +/// Arguments: +/// path = The path to add jitter to. +/// dist = The amount to jitter points by. Default: 1/512 (0.00195) +/// --- +/// closed = If true, treat path like a closed polygon. Default: true +/// Example(3D): +/// d = 100; h = 75; quadsize = 5; +/// path = pentagon(d=d); +/// spath = subdivide_long_segments(path, quadsize, closed=true); +/// jpath = _path_add_jitter(spath, closed=true); +/// linear_extrude(height=h, twist=72, slices=h/quadsize) +/// polygon(jpath); +function _path_add_jitter(path, dist=1/512, closed=true) = + assert(is_path(path)) + assert(is_finite(dist)) + assert(is_bool(closed)) + [ + path[0], + for (i=idx(path,s=1,e=closed?-1:-2)) let( + n = line_normal([path[i-1],path[i]]) + ) path[i] + n * (is_collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), + if (!closed) last(path) + ]; + + + // Module: jittered_poly() // Topics: Extrusions -// See Also: path_add_jitter(), subdivide_long_segments() +// See Also: _path_add_jitter(), subdivide_long_segments() // Usage: // jittered_poly(path, [dist]); // Description: @@ -829,7 +865,7 @@ module star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, // linear_extrude(height=h, twist=72, slices=h/quadsize) // jittered_poly(spath); module jittered_poly(path, dist=1/512) { - polygon(path_add_jitter(path, dist, closed=true)); + polygon(_path_add_jitter(path, dist, closed=true)); }