renamed is_region_simple to is_valid_region and fixed bugs and added examples

fixed bugs in pair and triplet and added degenerate test cases
This commit is contained in:
Adrian Mariano
2021-11-03 22:30:01 -04:00
parent 530f3b3449
commit fe0586180e
8 changed files with 175 additions and 35 deletions

View File

@@ -24,28 +24,98 @@
// compliant. You can construct regions by making a list of polygons, or by using
// 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().
// can clean up an ill-formed region using make_region().
// Function: is_region()
// Usage:
// is_region(x);
// Description:
// Returns true if the given item looks like a region. A region is defined as a list of zero or more paths.
// Returns true if the given item looks like a region. A region is a list of non-crossing simple paths. This test just checks
// that the argument is a list whose first entry is a path.
function is_region(x) = is_list(x) && is_path(x.x);
// Function: force_region()
// Function: is_valid_region()
// Usage:
// region = force_region(path)
// bool = is_valid_region(region, [eps]);
// Description:
// If the input is a path then return it as a region. Otherwise return it unaltered.
function force_region(path) = is_path(path) ? [path] : path;
// Returns true if the input is a valid region, meaning that it is a list of simple paths whose segments do not cross each other.
// This test can be time consuming with regions that contain many points.
// It differs from `is_region()` which simply checks that the object appears to be a list of paths
// because it searches all the region paths for any self-intersections or intersections with each other.
// Will also return true if given a single simple path. Use {{make_region()}} to convert sets of self-intersecting polygons into
// a region.
// Arguments:
// region = region to check
// eps = tolerance for geometric comparisons. Default: `EPSILON` = 1e-9
// Example(2D,noaxes): Nested squares form a region
// region = [for(i=[3:2:10]) square(i,center=true)];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(6)text(is_valid_region(region) ? "region" : "non-region", size=2,halign="center");
// Example(2D,noaxes): Two non-intersecting squares make a valid region:
// region = [square(10), right(11,square(8))];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Not a region due to a self-intersecting (non-simple) hourglass path
// object = [move([-2,-2],square(14)), [[0,0],[10,0],[0,10],[10,10]]];
// rainbow(object)stroke($item, width=.1,closed=true);
// move([-1.5,13])text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Breaking hourglass in half fixes it. Now it's a region:
// region = [move([-2,-2],square(14)), [[0,0],[10,0],[5,5]], [[5,5],[0,10],[10,10]]];
// rainbow(region)stroke($item, width=.1,closed=true);
// move([1,13])text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): As with the "broken" hourglass, Touching at corners is OK. This is a region.
// region = [square(10), move([10,10], square(8))];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): The squares cross each other, so not a region
// object = [square(10), move([8,8], square(8))];
// rainbow(object)stroke($item, width=.1,closed=true);
// back(17)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): A union is one way to fix the above example and get a region. (Note that union is run here on two simple paths, which are valid regions themselves and hence acceptable inputs to union.
// region = union([square(10), move([8,8], square(8))]);
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): These two squares share part of an edge, hence not a region
// object = [square(10), move([10,2], square(7))];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): These two squares share a full edge, hence not a region
// object = [square(10), right(10, square(10))];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Sharing on edge on the inside, also not a regionn
// object = [square(10), [[0,0], [2,2],[2,8],[0,10]]];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
function is_valid_region(region, eps=EPSILON) =
let(region=force_region(region))
assert(is_region(region), "Input is not a region")
[for(p=region) if (!is_path_simple(p,closed=true,eps=eps)) 1] == []
&&
[for(i=[0:1:len(region)-2])
let( isect = _region_region_intersections([region[i]], list_tail(region,i+1), eps=eps))
each [
// check for intersection points not at the end of a segment
for(pts=flatten(isect[0])) if (pts[2]!=0 && pts[2]!=1) 1,
// check for full segment
for(seg=pair(flatten(isect[0])))
if (seg[0][0]==seg[1][0] // same path
&& seg[0][1]==seg[1][1] // same segment
&& seg[0][2]==0 && seg[1][2]==1) // both ends
1]
] ==[];
// Function: sanitize_region()
// Function: make_region()
// Usage:
// r_fixed = sanitize_region(r, [nonzero], [eps]);
// r_fixed = make_region(r, [nonzero], [eps]);
// Description:
// Takes a malformed input region that contains self-intersecting polygons or polygons
// that cross each other and converts it into a properly defined region without
@@ -56,7 +126,7 @@ function force_region(path) = is_path(path) ? [path] : path;
// eps = Epsilon for geometric comparisons. Default: `EPSILON` (1e-9)
// Examples:
//
function sanitize_region(r,nonzero=false,eps=EPSILON) =
function make_region(r,nonzero=false,eps=EPSILON) =
let(r=force_region(r))
assert(is_region(r), "Input is not a region")
exclusive_or(
@@ -64,6 +134,17 @@ function sanitize_region(r,nonzero=false,eps=EPSILON) =
eps=eps);
// Function: force_region()
// Usage:
// region = force_region(path)
// Description:
// If the input is a path then return it as a region. Otherwise return it unaltered.
function force_region(path) = is_path(path) ? [path] : path;
// Section: Turning a region into geometry
// Module: region()
// Usage:
// region(r);
@@ -93,6 +174,8 @@ module region(r)
// Section: Gometrical calculations with region
// Function: point_in_region()
// Usage:
// check = point_in_region(point, region, [eps]);
@@ -128,23 +211,6 @@ function region_area(region) =
-sum([for(R=parts, poly=R) polygon_area(poly,signed=true)]);
// Function: is_region_simple()
// Usage:
// bool = is_region_simple(region, [eps]);
// Description:
// Returns true if the region is entirely non-self-intersecting, meaning that it is
// formed from a list of simple polygons that do not intersect each other.
// Arguments:
// region = region to check
// eps = tolerance for geometric comparisons. Default: `EPSILON` = 1e-9
function is_region_simple(region, eps=EPSILON) =
let(region=force_region(region))
assert(is_region(region), "Input is not a region")
[for(p=region) if (!is_path_simple(p,closed=true,eps)) 1] == []
&&
[for(i=[0:1:len(region)-2])
if (_region_region_intersections([region[i]], list_tail(region,i+1), eps=eps)[0][0] != []) 1
] ==[];
function _clockwise_region(r) = [for(p=r) clockwise_polygon(p)];
@@ -182,7 +248,7 @@ function __are_regions_equal(region1, region2, i) =
/// Returns a pair of sorted lists such that risect[0] is a list of intersection
/// points for every path in region1, and similarly risect[1] is a list of intersection
/// points for the paths in region2. For each path the intersection list is
/// a sorted list of the form [SEGMENT, U]. You can specify that the paths in either
/// a sorted list of the form [PATHIND, SEGMENT, U]. You can specify that the paths in either
/// region be regarded as open paths if desired. Default is to treat them as
/// regions and hence the paths as closed polygons.
/// .
@@ -252,6 +318,9 @@ function _region_region_intersections(region1, region2, closed1=true,closed2=tru
[for(i=[0:1]) [for(j=counts[i]) _sort_vectors(select(risect[i],pathind[i][j]))]];
// Section: Breaking up regions into subregions
// Function: split_region_at_region_crossings()
// Usage:
// split_region = split_region_at_region_crossings(region1, region2, [closed1], [closed2], [eps])