Fix docs for path functions with 1-regions, change is_path_region to is_1region

This commit is contained in:
Adrian Mariano 2021-10-30 11:59:59 -04:00
parent 94033a0bfd
commit 14804421b7
2 changed files with 90 additions and 82 deletions

View file

@ -1,14 +1,16 @@
//////////////////////////////////////////////////////////////////////
// LibFile: paths.scad
// Support for polygons and paths.
// A `path` is a list of points of the same dimensions, usually 2D or 3D, that can
// be connected together to form a sequence of line segments or a polygon.
// The functions in this file work on paths and also 1-regions, which are regions
// that include exactly one path. Capabilities include computing length of paths, computing
// path tangents and normals, resampling of paths, and cutting paths up into smaller paths.
// Includes:
// include <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// Section: Utility Functions
// Function: is_path()
// Usage:
// is_path(list, [dim], [fast])
@ -16,7 +18,8 @@
// 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.
// 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.
// of allowed dimensions, or set it to `undef` to allow any dimension. (Note that this function
// returns `false` on 1-regions.)
// Example:
// bool1 = is_path([[3,4],[5,6]]); // Returns true
// bool2 = is_path([[3,4]]); // Returns false
@ -45,18 +48,17 @@ function is_path(list, dim=[2,3], fast=false) =
&& len(list[0])>0
&& (is_undef(dim) || in_list(len(list[0]), force_list(dim)));
// Function: is_path_region()
// Function: is_1region()
// Usage:
// bool = is_path_region(path, [name])
// bool = is_1region(path, [name])
// Description:
// If `path` is a region with one component then return true. If path is a region with more components
// If `path` is a region with one component (a 1-region) then return true. If path is a region with more components
// then display an error message about the parameter `name` requiring a path or a single component region. If the input
// is not a region then return false. This function helps accept singleton regions in functions that
// operate on a path.
// is not a region then return false. This function helps path functions accept 1-regions.
// Arguments:
// path = input to process
// name = name of parameter to use in error message. Default: "path"
function is_path_region(path, name="path") =
function is_1region(path, name="path") =
!is_region(path)? false
:assert(len(path)==1,str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region"))
true;
@ -65,10 +67,9 @@ function is_path_region(path, name="path") =
// Usage:
// outpath = force_path(path, [name])
// Description:
// If `path` is a region with one component then return that component as a path. If path is a region with more components
// If `path` is a region with one component (a 1-region) then return that component as a path. If path is a region with more components
// then display an error message about the parameter `name` requiring a path or a single component region. If the input
// is not a region then return the input without any checks. This function helps accept singleton regions in functions that
// operate on a path.
// is not a region then return the input without any checks. This function helps path functions accept 1-regions.
// Arguments:
// path = input to process
// name = name of parameter to use in error message. Default: "path"
@ -142,11 +143,11 @@ function _path_select(path, s1, u1, s2, u2, closed=false) =
// Usage:
// path_merge_collinear(path, [eps])
// Arguments:
// path = A list of path points of any dimension.
// path = A path of any dimension or a 1-region
// closed = treat as closed polygon. Default: false
// eps = Largest positional variance allowed. Default: `EPSILON` (1-e9)
function path_merge_collinear(path, closed, eps=EPSILON) =
is_path_region(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))
assert(is_bool(closed))
assert( is_path(path), "Invalid path in path_merge_collinear." )
@ -172,13 +173,13 @@ function path_merge_collinear(path, closed, eps=EPSILON) =
// Description:
// Returns the length of the path.
// Arguments:
// path = The list of points of the path to measure.
// path = Path of any dimension or 1-region.
// closed = true if the path is closed. Default: false
// Example:
// path = [[0,0], [5,35], [60,-25], [80,0]];
// echo(path_length(path));
function path_length(path,closed) =
is_path_region(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")
let(closed=default(closed,false))
assert(is_bool(closed))
@ -192,10 +193,10 @@ function path_length(path,closed) =
// Description:
// Returns list of the length of each segment in a path
// Arguments:
// path = path to measure
// path = path in any dimension or 1-region
// closed = true if the path is closed. Default: false
function path_segment_lengths(path, closed) =
is_path_region(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))
assert(is_path(path),"Invalid path in path_segment_lengths.")
assert(is_bool(closed))
@ -214,10 +215,10 @@ function path_segment_lengths(path, closed) =
// will have one extra point because of the final connecting segment that connects the last
// point of the path to the first point.
// Arguments:
// path = path to operate on
// path = path in any dimension or a 1-region
// closed = set to true if path is closed. Default: false
function path_length_fractions(path, closed) =
is_path_region(path) ? path_length_fractions(path[0], default(closed,true)):
is_1region(path) ? path_length_fractions(path[0], default(closed,true)):
let(closed=default(closed, false))
assert(is_path(path))
assert(is_bool(closed))
@ -337,7 +338,7 @@ function _sum_preserving_round(data, index=0) =
// 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
// path = path in any dimension or a 1-region
// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i.
// refine = number of points to add each segment.
// closed = set to false if the path is open. Default: True
@ -423,7 +424,7 @@ function subdivide_path(path, N, refine, closed=true, exact=true, method="length
// Description:
// Evenly subdivides long `path` segments until they are all shorter than `maxlen`.
// Arguments:
// path = The path to subdivide.
// path = path in any dimension or a 1-region
// maxlen = The maximum allowed path segment length.
// ---
// closed = If true, treat path like a closed polygon. Default: true
@ -459,7 +460,7 @@ function subdivide_long_segments(path, maxlen, closed=true) =
// 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
// path = path in any dimension or a 1-region
// N = Number of points in output
// spacing = Approximate spacing desired
// closed = set to true if path is closed. Default: true
@ -493,11 +494,11 @@ function resample_path(path, N, spacing, closed=true) =
// still be simple.
// If closed is set to true then treat the path as a polygon.
// Arguments:
// path = path to check
// path = 2D path or 1-region
// 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, eps=EPSILON) =
is_path_region(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))
assert(is_path(path, 2),"Must give a 2D path")
assert(is_bool(closed))
@ -551,7 +552,7 @@ function path_closest_point(path, pt, closed=true) =
// assumed to be non-uniform and the derivative is computed with adjustments to produce corrected
// values.
// Arguments:
// path = path to find the tagent vectors for
// path = path of any dimension or a 1-region
// closed = set to true of the path is closed. Default: false
// uniform = set to false to correct for non-uniform sampling. Default: true
// Example(2D): A shape with non-uniform sampling gives distorted derivatives that may be undesirable. Note that derivatives tilt towards the long edges of the rectangle.
@ -569,7 +570,7 @@ function path_closest_point(path, pt, closed=true) =
// for(i=[0:len(tangents)-1])
// stroke([rect[i]-tangents[i], rect[i]+tangents[i]],width=.25, endcap2="arrow2");
function path_tangents(path, closed, uniform=true) =
is_path_region(path) ? path_tangents(path[0], default(closed,true), uniform) :
is_1region(path) ? path_tangents(path[0], default(closed,true), uniform) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_path(path))
@ -592,11 +593,11 @@ function path_tangents(path, closed, uniform=true) =
// For 2d paths the plane is always defined so the normal fails to exist only
// when the derivative is zero (in the case of repeated points).
// Arguments:
// path = path to compute the normals to
// path = 2D or 3D path or a 1-region
// tangents = path tangents optionally supplied
// closed = if true path is treated as a polygon. Default: false
function path_normals(path, tangents, closed) =
is_path_region(path) ? path_normals(path[0], tangents, default(closed,true)) :
is_1region(path) ? path_normals(path[0], tangents, default(closed,true)) :
let(closed=default(closed,false))
assert(is_path(path,[2,3]))
assert(is_bool(closed))
@ -624,8 +625,11 @@ function path_normals(path, tangents, closed) =
// curvs = path_curvature(path, [closed]);
// Description:
// Numerically estimate the curvature of the path (in any dimension).
// Arguments:
// path = path in any dimension or a 1-region
// closed = if true then treat the path as a polygon. Default: false
function path_curvature(path, closed) =
is_path_region(path) ? path_curvature(path[0], default(closed,true)) :
is_1region(path) ? path_curvature(path[0], default(closed,true)) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_path(path))
@ -643,9 +647,12 @@ function path_curvature(path, closed) =
// Function: path_torsion()
// Usage:
// tortions = path_torsion(path, [closed]);
// torsions = path_torsion(path, [closed]);
// Description:
// Numerically estimate the torsion of a 3d path.
// Arguments:
// path = 3D path
// closed = if true then treat path as a polygon. Default: false
function path_torsion(path, closed=false) =
assert(is_path(path,3), "Input path must be a 3d path")
assert(is_bool(closed))
@ -939,19 +946,19 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
// Topics: Paths
// See Also: split_path_at_self_crossings()
// Usage:
// path_list = path_cut(path, cutdist, [closed=]);
// path_list = path_cut(path, cutdist, [closed=]);
// Description:
// Given a list of distances in `cutdist`, cut the path into
// subpaths at those lengths, returning a list of paths.
// If the input path is closed then the final path will include the
// original starting point. The list of cut distances must be
// in ascending order and should not include the endpoints: 0
// or len(path). If you repeat a distance you will get an
// empty list in that position in the output. If you give an
// empty cutdist array you will get the input path as output
// (without the final vertex doubled in the case of a closed path).
// Given a list of distances in `cutdist`, cut the path into
// subpaths at those lengths, returning a list of paths.
// If the input path is closed then the final path will include the
// original starting point. The list of cut distances must be
// in ascending order and should not include the endpoints: 0
// or len(path). If you repeat a distance you will get an
// empty list in that position in the output. If you give an
// empty cutdist array you will get the input path as output
// (without the final vertex doubled in the case of a closed path).
// Arguments:
// path = The original path to split.
// path = path of any dimension or a 1-region
// cutdist = Distance or list of distances where path is cut
// closed = If true, treat the path as a closed polygon. Default: false
// Example(2D,NoAxes):
@ -960,7 +967,7 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
// rainbow(segs) stroke($item, endcaps="butt", width=3);
function path_cut(path,cutdist,closed) =
is_num(cutdist) ? path_cut(path,[cutdist],closed) :
is_path_region(path) ? path_cut(path[0], cutdist, default(closed,true)):
is_1region(path) ? path_cut(path[0], cutdist, default(closed,true)):
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_vector(cutdist))
@ -1017,10 +1024,11 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
// Usage:
// paths = split_path_at_self_crossings(path, [closed], [eps]);
// Description:
// Splits a 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.
// Returns a list of the resulting subpaths.
// Arguments:
// path = The path to split up.
// path = A 2D path or a 1-region.
// closed = If true, treat path as a closed polygon. Default: true
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
// Example(2D,NoAxes):
@ -1081,22 +1089,22 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// Function: polygon_parts()
// Usage:
// splitpaths = polygon_parts(path, [nonzero], [eps]);
// splitpolys = polygon_parts(poly, [nonzero], [eps]);
// Description:
// Given a possibly self-intersecting 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, which
// means it will produce the outer perimeter.
// Given a possibly self-intersecting 2d polygon, constructs a representation of the original polygon as a list of
// non-intersecting simple polygons. If nonzero is set to true then it uses the nonzero method for defining polygon membership.
// For simple cases, such as the pentagram, this will produce the outer perimeter of a self-intersecting polygon.
// Arguments:
// path = The path to split up.
// 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
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
// Example(2D,NoAxes): This cross-crossing polygon breaks up into its 3 components (regardless of the value of nonzero).
// path = [
// poly = [
// [-100,100], [0,-50], [100,100],
// [100,-100], [0,50], [-100,-100]
// ];
// splitpaths = polygon_parts(path);
// rainbow(splitpaths) stroke($item, closed=true, width=3);
// splitpolys = polygon_parts(poly);
// rainbow(splitpolys) stroke($item, closed=true, width=3);
// Example(2D,NoAxes): With nonzero=false you get even-odd mode which matches OpenSCAD, so the pentagram breaks apart into its five points.
// pentagram = turtle(["move",100,"left",144], repeat=4);
// left(100)polygon(pentagram);
@ -1112,39 +1120,39 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// N=12;
// ang=360/N;
// sr=10;
// path = turtle(["angle", 90+ang/2,
// poly = turtle(["angle", 90+ang/2,
// "move", sr, "left",
// "move", 2*sr*sin(ang/2), "left",
// "repeat", 4,
// ["move", 2*sr, "left",
// "move", 2*sr*sin(ang/2), "left"],
// "move", sr]);
// stroke(path, width=.3);
// right(20)rainbow(polygon_parts(path)) polygon($item);
// Example(2D,NoAxes): overlapping path segments disappear
// path = [[0,0], [10,0], [10,10], [0,10],[0,20], [20,10],[10,10], [0,10],[0,0]];
// stroke(path,width=0.3);
// right(22)stroke(polygon_parts(path)[0], width=0.3, closed=true);
// Example(2D,NoAxes): Path segments disappear outside as well
// path = turtle(["repeat", 3, ["move", 17, "left", "move", 10, "left", "move", 7, "left", "move", 10, "left"]]);
// back(2)stroke(path,width=.5);
// fwd(12)rainbow(polygon_parts(path)) stroke($item, closed=true, width=0.5);
// stroke(poly, width=.3);
// right(20)rainbow(polygon_parts(poly)) polygon($item);
// Example(2D,NoAxes): overlapping poly segments disappear
// poly = [[0,0], [10,0], [10,10], [0,10],[0,20], [20,10],[10,10], [0,10],[0,0]];
// stroke(poly,width=0.3);
// right(22)stroke(polygon_parts(poly)[0], width=0.3, closed=true);
// Example(2D,NoAxes): Poly segments disappear outside as well
// poly = turtle(["repeat", 3, ["move", 17, "left", "move", 10, "left", "move", 7, "left", "move", 10, "left"]]);
// back(2)stroke(poly,width=.5);
// fwd(12)rainbow(polygon_parts(poly)) stroke($item, closed=true, width=0.5);
// Example(2D,NoAxes): This shape has six components
// path = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 17, "left"]]);
// polygon(path);
// right(22)rainbow(polygon_parts(path)) polygon($item);
// poly = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 17, "left"]]);
// polygon(poly);
// right(22)rainbow(polygon_parts(poly)) polygon($item);
// Example(2D,NoAxes): When the loops of the shape overlap then nonzero gives a different result than the even-odd method.
// path = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 10, "left"]]);
// polygon(path);
// right(27)rainbow(polygon_parts(path)) polygon($item);
// move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item);
function polygon_parts(path, nonzero=false, eps=EPSILON) =
let(path = force_path(path))
assert(is_path(path,2), "Must give 2D path")
// poly = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 10, "left"]]);
// polygon(poly);
// right(27)rainbow(polygon_parts(poly)) polygon($item);
// move([16,-14])rainbow(polygon_parts(poly,nonzero=true)) polygon($item);
function polygon_parts(poly, nonzero=false, eps=EPSILON) =
let(poly = force_path(poly))
assert(is_path(poly,2), "Must give 2D polygon")
assert(is_bool(nonzero))
let(
path = cleanup_path(path, eps=eps),
tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=true, eps=eps),
poly = cleanup_path(poly, eps=eps),
tagged = _tag_self_crossing_subpaths(poly, nonzero=nonzero, closed=true, eps=eps),
kept = [for (sub = tagged) if(sub[0] == "O") sub[1]],
outregion = _assemble_path_fragments(kept, eps=eps)
) outregion;

View file

@ -3,8 +3,7 @@
// This file provides 2D boolean geometry operations on paths, where you can
// compute the intersection or union of the shape defined by point lists, producing
// a new point list. Of course, boolean operations may produce shapes with multiple
// components. To handle that, we use "regions" which are defined by sets of
// multiple paths.
// components. To handle that, we use "regions" which are defined as lists of paths.
// Includes:
// include <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
@ -23,7 +22,8 @@
// Checking that the polygons on a list are simple and non-crossing can be a time consuming test,
// so it is not done automatically. It is your responsibility to ensure that your regions are
// compliant. You can construct regions by making a list of polygons, or by using
// boolean function operations such as union() or difference(). And if you must you
// boolean function operations such as union() or difference(), which all except paths, as
// well as regions, as their inputs. And if you must you
// can clean up an ill-formed region using sanitize_region().