From fe0586180eaee4685626b9c94d33172b68f71e38 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 3 Nov 2021 22:30:01 -0400 Subject: [PATCH] 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 --- drawing.scad | 2 + geometry.scad | 2 +- lists.scad | 13 +++-- mutators.scad | 51 ++++++++++++++++++ regions.scad | 123 ++++++++++++++++++++++++++++++++---------- tests/test_lists.scad | 14 +++++ turtle3d.scad | 1 + vnf.scad | 4 +- 8 files changed, 175 insertions(+), 35 deletions(-) diff --git a/drawing.scad b/drawing.scad index 8a46059..2a8726f 100644 --- a/drawing.scad +++ b/drawing.scad @@ -554,6 +554,7 @@ function dashed_stroke(path, dashpat=[3,3], 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); for (seg = segs) 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"); // Example(3D): Flat helix (note points are still 3d) // 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)= let( r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1), diff --git a/geometry.scad b/geometry.scad index 498be1c..4b134db 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1439,7 +1439,7 @@ function _region_centroid(region,eps=EPSILON) = total[0]/total[1]; -/// Function: _polygon_centroid() +/// Internal Function: _polygon_centroid() /// Usage: /// cpt = _polygon_centroid(poly); /// Topics: Geometry, Polygons, Centroid diff --git a/lists.scad b/lists.scad index 8947b7f..9da1972 100644 --- a/lists.scad +++ b/lists.scad @@ -955,11 +955,13 @@ function enumerate(l,idx=undef) = function pair(list, wrap=false) = assert(is_list(list)||is_string(list), "Invalid input." ) assert(is_bool(wrap)) - let( - ll = len(list) - ) wrap - ? [for (i=[0:1:ll-1]) [list[i], list[(i+1) % ll]]] - : [for (i=[0:1:ll-2]) [list[i], list[i+1]]]; + let( L = len(list)-1) + L<1 ? [] : + [ + for (i=[0:1:L-1]) [list[i], list[i+1]], + if(wrap) [list[L], list[0]] + ]; + // Function: triplet() @@ -993,6 +995,7 @@ function triplet(list, wrap=false) = assert(is_list(list)||is_string(list), "Invalid input." ) assert(is_bool(wrap)) let(L=len(list)) + L<3 ? [] : [ if(wrap) [list[L-1], list[0], list[1]], for (i=[0:1:L-3]) [list[i],list[i+1],list[i+2]], diff --git a/mutators.scad b/mutators.scad index 88836a2..9e50e9e 100644 --- a/mutators.scad +++ b/mutators.scad @@ -530,6 +530,57 @@ module path_extrude2d(path, caps=false, closed=false) { 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() diff --git a/regions.scad b/regions.scad index 5380d47..36549b3 100644 --- a/regions.scad +++ b/regions.scad @@ -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]) diff --git a/tests/test_lists.scad b/tests/test_lists.scad index 3672802..1c0aa62 100644 --- a/tests/test_lists.scad +++ b/tests/test_lists.scad @@ -351,6 +351,12 @@ module test_pair() { 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("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(); @@ -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("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_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(); diff --git a/turtle3d.scad b/turtle3d.scad index 0b4c943..2d42c06 100644 --- a/turtle3d.scad +++ b/turtle3d.scad @@ -427,6 +427,7 @@ function _turtle3d_state_valid(state) = && is_num(state[3]) && 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) = assert(is_bool(transforms)) let( diff --git a/vnf.scad b/vnf.scad index dafad7f..61d329d 100644 --- a/vnf.scad +++ b/vnf.scad @@ -620,7 +620,7 @@ function _split_2dpolygons_at_each_x(polys, xs, _i=0) = ], xs, _i=_i+1 ); -/// Function: _slice_3dpolygons() +/// Internal Function: _slice_3dpolygons() /// Usage: /// splitpolys = _slice_3dpolygons(polys, dir, cuts); /// Topics: Geometry, Polygons, Intersections @@ -760,7 +760,7 @@ function vnf_area(vnf) = sum([for(face=vnf[1]) polygon_area(select(verts,face))]); -/// Function: _vnf_centroid() +/// Internal Function: _vnf_centroid() /// Usage: /// vol = _vnf_centroid(vnf); /// Description: