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

@ -554,6 +554,7 @@ function dashed_stroke(path, dashpat=[3,3], closed=false) =
module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) { module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) {
no_children($children);
segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed); segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed);
for (seg = segs) for (seg = segs)
stroke(seg, width=width, endcaps=false); stroke(seg, width=width, endcaps=false);
@ -731,6 +732,7 @@ module arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false)
// stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue"); // stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue");
// Example(3D): Flat helix (note points are still 3d) // Example(3D): Flat helix (note points are still 3d)
// stroke(helix(h=0,r1=50,r2=25,l=0, turns=4)); // stroke(helix(h=0,r1=50,r2=25,l=0, turns=4));
module helix(l,h,turns,angle, r, r1, r2, d, d1, d2) {no_module();}
function helix(l,h,turns,angle, r, r1, r2, d, d1, d2)= function helix(l,h,turns,angle, r, r1, r2, d, d1, d2)=
let( let(
r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1), r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1),

View file

@ -1439,7 +1439,7 @@ function _region_centroid(region,eps=EPSILON) =
total[0]/total[1]; total[0]/total[1];
/// Function: _polygon_centroid() /// Internal Function: _polygon_centroid()
/// Usage: /// Usage:
/// cpt = _polygon_centroid(poly); /// cpt = _polygon_centroid(poly);
/// Topics: Geometry, Polygons, Centroid /// Topics: Geometry, Polygons, Centroid

View file

@ -955,11 +955,13 @@ function enumerate(l,idx=undef) =
function pair(list, wrap=false) = function pair(list, wrap=false) =
assert(is_list(list)||is_string(list), "Invalid input." ) assert(is_list(list)||is_string(list), "Invalid input." )
assert(is_bool(wrap)) assert(is_bool(wrap))
let( let( L = len(list)-1)
ll = len(list) L<1 ? [] :
) wrap [
? [for (i=[0:1:ll-1]) [list[i], list[(i+1) % ll]]] for (i=[0:1:L-1]) [list[i], list[i+1]],
: [for (i=[0:1:ll-2]) [list[i], list[i+1]]]; if(wrap) [list[L], list[0]]
];
// Function: triplet() // Function: triplet()
@ -993,6 +995,7 @@ function triplet(list, wrap=false) =
assert(is_list(list)||is_string(list), "Invalid input." ) assert(is_list(list)||is_string(list), "Invalid input." )
assert(is_bool(wrap)) assert(is_bool(wrap))
let(L=len(list)) let(L=len(list))
L<3 ? [] :
[ [
if(wrap) [list[L-1], list[0], list[1]], if(wrap) [list[L-1], list[0], list[1]],
for (i=[0:1:L-3]) [list[i],list[i+1],list[i+2]], for (i=[0:1:L-3]) [list[i],list[i+1],list[i+2]],

View file

@ -530,6 +530,57 @@ module path_extrude2d(path, caps=false, closed=false) {
right_half(planar=true) children(); right_half(planar=true) children();
} }
} }
module new_path_extrude2d(path, caps=false, closed=false) {
extra_ang = 0.1; // Extra angle for overlap of joints
assert(caps==false || closed==false, "Cannot have caps on a closed extrusion");
path = deduplicate(path);
for (i=[0:1:len(path)-(closed?1:2)]){
// for (i=[0:1:1]){
difference(){
extrude_from_to(path[i],select(path,i+1)) xflip()rot(-90)children();
# for(t = [select(path,i-1,i+1)]){ //, select(path,i,i+2)]){
ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
echo(ang=ang);
delt = point3d(t[2] - t[1]);
if (ang!=0)
translate(t[1]) {
frame_map(y=delt, z=UP)
rotate(-sign(ang)*extra_ang/2)
rotate_extrude(angle=ang+sign(ang)*extra_ang)
if (ang<0)
left_half(planar=true) children();
else
right_half(planar=true) children();
}
}
}
}
for (t=triplet(path,wrap=closed)) {
ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
echo(oang=ang);
delt = point3d(t[2] - t[1]);
if (ang!=0)
translate(t[1]) {
frame_map(y=delt, z=UP)
rotate(-sign(ang)*extra_ang/2)
rotate_extrude(angle=ang+sign(ang)*extra_ang)
if (ang<0)
right_half(planar=true) children();
else
left_half(planar=true) children();
}
}
if (caps) {
move_copies([path[0],last(path)])
rotate_extrude()
right_half(planar=true) children();
}
}
// Module: cylindrical_extrude() // Module: cylindrical_extrude()

View file

@ -24,28 +24,98 @@
// compliant. You can construct regions by making a list of polygons, or by using // 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 // boolean function operations such as union() or difference(), which all except paths, as
// well as regions, as their inputs. And if you must you // 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() // Function: is_region()
// Usage: // Usage:
// is_region(x); // is_region(x);
// Description: // 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 is_region(x) = is_list(x) && is_path(x.x);
// Function: force_region() // Function: is_valid_region()
// Usage: // Usage:
// region = force_region(path) // bool = is_valid_region(region, [eps]);
// Description: // Description:
// If the input is a path then return it as a region. Otherwise return it unaltered. // 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.
function force_region(path) = is_path(path) ? [path] : path; // 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: // Usage:
// r_fixed = sanitize_region(r, [nonzero], [eps]); // r_fixed = make_region(r, [nonzero], [eps]);
// Description: // Description:
// Takes a malformed input region that contains self-intersecting polygons or polygons // 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 // 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) // eps = Epsilon for geometric comparisons. Default: `EPSILON` (1e-9)
// Examples: // Examples:
// //
function sanitize_region(r,nonzero=false,eps=EPSILON) = function make_region(r,nonzero=false,eps=EPSILON) =
let(r=force_region(r)) let(r=force_region(r))
assert(is_region(r), "Input is not a region") assert(is_region(r), "Input is not a region")
exclusive_or( exclusive_or(
@ -64,6 +134,17 @@ function sanitize_region(r,nonzero=false,eps=EPSILON) =
eps=eps); 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() // Module: region()
// Usage: // Usage:
// region(r); // region(r);
@ -93,6 +174,8 @@ module region(r)
// Section: Gometrical calculations with region
// Function: point_in_region() // Function: point_in_region()
// Usage: // Usage:
// check = point_in_region(point, region, [eps]); // 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)]); -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)]; 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 /// 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 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 /// 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 /// region be regarded as open paths if desired. Default is to treat them as
/// regions and hence the paths as closed polygons. /// 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]))]]; [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() // Function: split_region_at_region_crossings()
// Usage: // Usage:
// split_region = split_region_at_region_crossings(region1, region2, [closed1], [closed2], [eps]) // split_region = split_region_at_region_crossings(region1, region2, [closed1], [closed2], [eps])

View file

@ -351,6 +351,12 @@ module test_pair() {
assert(pair("ABCD",true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]); assert(pair("ABCD",true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]);
assert(pair([3,4,5,6],wrap=true) == [[3,4], [4,5], [5,6], [6,3]]); assert(pair([3,4,5,6],wrap=true) == [[3,4], [4,5], [5,6], [6,3]]);
assert(pair("ABCD",wrap=true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]); assert(pair("ABCD",wrap=true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]);
assert_equal(pair([],wrap=true),[]);
assert_equal(pair([],wrap=false),[]);
assert_equal(pair([1],wrap=true),[]);
assert_equal(pair([1],wrap=false),[]);
assert_equal(pair([1,2],wrap=false),[[1,2]]);
assert_equal(pair([1,2],wrap=true),[[1,2],[2,1]]);
} }
test_pair(); test_pair();
@ -361,6 +367,14 @@ module test_triplet() {
assert(triplet([3,4,5,6],true) == [[6,3,4],[3,4,5], [4,5,6], [5,6,3]]); assert(triplet([3,4,5,6],true) == [[6,3,4],[3,4,5], [4,5,6], [5,6,3]]);
assert(triplet("ABCD",true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]); assert(triplet("ABCD",true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]);
assert(triplet("ABCD",wrap=true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]); assert(triplet("ABCD",wrap=true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]);
assert_equal(triplet([],wrap=true),[]);
assert_equal(triplet([],wrap=false),[]);
assert_equal(triplet([1],wrap=true),[]);
assert_equal(triplet([1],wrap=false),[]);
assert_equal(triplet([1,2],wrap=true),[]);
assert_equal(triplet([1,2],wrap=false),[]);
assert_equal(triplet([1,2,3],wrap=true),[[3,1,2],[1,2,3],[2,3,1]]);
assert_equal(triplet([1,2,3],wrap=false),[[1,2,3]]);
} }
test_triplet(); test_triplet();

View file

@ -427,6 +427,7 @@ function _turtle3d_state_valid(state) =
&& is_num(state[3]) && is_num(state[3])
&& is_num(state[4]); && is_num(state[4]);
module turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) {no_module();}
function turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) = function turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) =
assert(is_bool(transforms)) assert(is_bool(transforms))
let( let(

View file

@ -620,7 +620,7 @@ function _split_2dpolygons_at_each_x(polys, xs, _i=0) =
], xs, _i=_i+1 ], xs, _i=_i+1
); );
/// Function: _slice_3dpolygons() /// Internal Function: _slice_3dpolygons()
/// Usage: /// Usage:
/// splitpolys = _slice_3dpolygons(polys, dir, cuts); /// splitpolys = _slice_3dpolygons(polys, dir, cuts);
/// Topics: Geometry, Polygons, Intersections /// Topics: Geometry, Polygons, Intersections
@ -760,7 +760,7 @@ function vnf_area(vnf) =
sum([for(face=vnf[1]) polygon_area(select(verts,face))]); sum([for(face=vnf[1]) polygon_area(select(verts,face))]);
/// Function: _vnf_centroid() /// Internal Function: _vnf_centroid()
/// Usage: /// Usage:
/// vol = _vnf_centroid(vnf); /// vol = _vnf_centroid(vnf);
/// Description: /// Description: