mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-01-16 13:50:23 +01:00
hiding some infrastructure, added is_path_simple()
This commit is contained in:
parent
43f37908df
commit
2f895cb8d1
@ -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()
|
||||
|
525
paths.scad
525
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
|
||||
]),
|
||||
|
105
regions.scad
105
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])<eps*eps;
|
||||
|
||||
|
||||
// Function: poly_in_polygons()
|
||||
// Function: is_polygon_in_list()
|
||||
// Topics: Polygons, Comparators
|
||||
// See Also: polygons_equal(), regions_equal()
|
||||
// Usage:
|
||||
// bool = poly_in_polygons(poly, polys);
|
||||
// bool = is_polygon_in_list(poly, polys);
|
||||
// Description:
|
||||
// Returns true if one of the polygons in `polys` is equivalent to the polygon `poly`.
|
||||
// Arguments:
|
||||
// poly = The polygon to search for.
|
||||
// polys = The list of polygons to look for the polygon in.
|
||||
function poly_in_polygons(poly, polys) =
|
||||
__poly_in_polygons(poly, polys, 0);
|
||||
function is_polygon_in_list(poly, polys) =
|
||||
__is_polygon_in_list(poly, polys, 0);
|
||||
|
||||
function __poly_in_polygons(poly, polys, i) =
|
||||
function __is_polygon_in_list(poly, polys, i) =
|
||||
i >= 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
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user