////////////////////////////////////////////////////////////////////// // LibFile: regions.scad // 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. // Includes: // include ////////////////////////////////////////////////////////////////////// // CommonCode: // include // Section: Regions // A region is a list of non-crossing simple polygons. Simple polygons are those without self intersections, // and the polygons of a region can touch at corners, but their segments should not // cross each other. The actual geometry of the region is defined by XORing together // all of the polygons on the list. This may sound obscure, but it simply means that nested // boundaries make rings in the obvious fashion, and non-nested shapes simply union together. // 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 // can clean up an ill-formed region using sanitize_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. function is_region(x) = is_list(x) && is_path(x.x); // Function: check_and_fix_path() // Usage: // check_and_fix_path(path, [valid_dim], [closed], [name]) // Description: // Checks that the input is a path. If it is a region with one component, converts it to a path. // Note that arbitrary paths must have at least two points, but closed paths need at least 3 points. // valid_dim specfies the allowed dimension of the points in the path. // If the path is closed, removes duplicate endpoint if present. // Arguments: // path = path to process // valid_dim = list of allowed dimensions for the points in the path, e.g. [2,3] to require 2 or 3 dimensional input. If left undefined do not perform this check. Default: undef // closed = set to true if the path is closed, which enables a check for endpoint duplication // name = parameter name to use for reporting errors. Default: "path" function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = let( path = is_region(path)? assert(len(path)==1,str("Region ",name," supplied as path does not have exactly one component")) path[0] : assert(is_path(path), str("Input ",name," is not a path")) path ) assert(len(path)>(closed?2:1),closed?str("Closed path ",name," must have at least 3 points") :str("Path ",name," must have at least 2 points")) let(valid=is_undef(valid_dim) || in_list(len(path[0]),force_list(valid_dim))) assert( valid, str( "Input ",name," must has dimension ", len(path[0])," but dimension must be ", is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim ) ) closed && approx(path[0], last(path))? list_head(path) : path; // Function: sanitize_region() // Usage: // r_fixed = sanitize_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 // these defects. // Arguments: // r = region to sanitize // nonzero = set to true to use nonzero rule for polygon membership. Default: false // eps = Epsilon for geometric comparisons. Default: `EPSILON` (1e-9) // Examples: // function sanitize_region(r,nonzero=false,eps=EPSILON) = assert(is_region(r)) exclusive_or( [for(poly=r) each polygon_parts(poly,nonzero,eps)], eps=eps); // Module: region() // Usage: // region(r); // 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) { no_children($children); r = is_path(r) ? [r] : r; points = flatten(r); lengths = [for(path=r) len(path)]; starts = [0,each cumsum(lengths)]; paths = [for(i=idx(r)) count(s=starts[i], n=lengths[i])]; polygon(points=points, paths=paths); } // Function: point_in_region() // Usage: // check = point_in_region(point, region, [eps]); // Description: // Tests if a point is inside, outside, or on the border of a region. // Returns -1 if the point is outside the region. // Returns 0 if the point is on the boundary. // Returns 1 if the point lies inside the region. // Arguments: // point = The point to test. // region = The region to test against. Given as a list of polygon paths. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = _i >= len(region) ? ((_cnt%2==1)? 1 : -1) : let( pip = point_in_polygon(point, region[_i], eps=eps) ) pip==0? 0 : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); // 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 omparisons. Default: `EPSILON` = 1e-9 function is_region_simple(region, eps=EPSILON) = [for(p=region) if (!is_path_simple(p,closed=true,eps)) 1] == []; /* && [for(i=[0:1:len(region)-2]) if (_path_region_intersections(region[i], list_tail(region,i+1), eps=eps) != []) 1 ] ==[]; */ // Function: are_regions_equal() // Usage: // b = are_regions_equal(region1, region2, [eps]) // Description: // Returns true if the components of region1 and region2 are the same polygons (in any order) // within given epsilon tolerance. // Arguments: // region1 = first region // region2 = second region // eps = tolerance for comparison function are_regions_equal(region1, region2) = assert(is_region(region1) && is_region(region2)) len(region1) != len(region2)? false : __are_regions_equal(region1, region2, 0); function __are_regions_equal(region1, region2, i) = i >= len(region1)? true : !is_polygon_in_list(region1[i], region2)? false : __are_regions_equal(region1, region2, i+1); /// Internal Function: _path_region_intersections() /// Usage: /// _path_region_intersections(path, region, [closed], [eps]); /// Description: /// Returns a sorted list of [SEGMENT, U] that describe where a given path intersects the region // in a single point. (Note that intersections of collinear segments, where the intersection is another segment, are // ignored.) /// 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 old_path_region_intersections(path, region, closed=true, eps=EPSILON) = let( pathclosed = closed && !is_closed_path(path), pathlen = len(path), regionsegs = [for(poly=region) each pair(poly, is_closed_path(poly)?false:true)] ) sort( [for(si = [0:1:len(path)-(pathclosed?1:2)]) let( a1 = path[si], a2 = path[(si+1)%pathlen], maxax = max(a1.x,a2.x), minax = min(a1.x,a2.x), maxay = max(a1.y,a2.y), minay = min(a1.y,a2.y) ) for(rseg=regionsegs) let( b1 = rseg[0], b2 = rseg[1], isect = maxax < b1.x && maxax < b2.x || minax > b1.x && minax > b2.x || maxay < b1.y && maxay < b2.y || minay > b1.y && minay > b2.y ? undef : _general_line_intersection([a1,a2],rseg,eps) ) if (isect && isect[1]>=-eps && isect[1]<=1+eps && isect[2]>=-eps && isect[2]<=1+eps) [si,isect[1]] ] ); // find the intersection points of a path and the polygons of a // region; only crossing intersections are caught, no collinear // intersection is returned. function _path_region_intersections(path, region, closed=true, eps=EPSILON, extra=[]) = let( path = closed ? close_path(path,eps=eps) : path ) _sort_vectors( [ each extra, for(si = [0:1:len(path)-2]) let( a1 = path[si], a2 = path[si+1], nrm = norm(a1-a2) ) if( nrm>eps ) let( // ignore zero-length path edges seg_normal = [-(a2-a1).y, (a2-a1).x]/nrm, ref = a1*seg_normal ) // `signs[j]` is the sign of the signed distance from // poly vertex j to the line [a1,a2] where near zero // distances are snapped to zero; poly edges // with equal signs at its vertices cannot intersect // the path edge [a1,a2] or they are collinear and // further tests can be discarded. for(poly=region) let( poly = close_path(poly), signs = [for(v=poly*seg_normal) v-ref> eps ? 1 : v-ref<-eps ? -1 : 0] ) if(max(signs)>=0 && min(signs)<=0 ) // some edge edge intersects line [a1,a2] for(j=[0:1:len(poly)-2]) if( signs[j]!=signs[j+1] ) let( // exclude non-crossing and collinear segments b1 = poly[j], b2 = poly[j+1], isect = _general_line_intersection([a1,a2],[b1,b2],eps=eps) ) if ( isect // && isect[1]> (si==0 && !closed? -eps: 0) && isect[1]>= -eps && isect[1]<= 1+eps // && isect[2]> 0 && isect[2]>= -eps && isect[2]<= 1+eps ) [si,isect[1]] ]); // Function: split_path_at_region_crossings() // Usage: // paths = split_path_at_region_crossings(path, region, [eps]); // Description: // Splits a path into sub-paths wherever the path crosses the perimeter of a region. // Splits may occur mid-segment, so new vertices will be created at the intersection points. // Arguments: // path = The path to split up. // region = The region to check for perimeter crossings of. // closed = If true, treat path as a closed polygon. Default: true // eps = Acceptable variance. Default: `EPSILON` (1e-9) // Example(2D): // path = square(50,center=false); // region = [circle(d=80), circle(d=40)]; // paths = split_path_at_region_crossings(path, region); // color("#aaa") region(region); // rainbow(paths) stroke($item, closed=false, width=2); function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON, extra=[]) = let( path = deduplicate(path, eps=eps), region = [for (path=region) deduplicate(path, eps=eps)], xings = _path_region_intersections(path, region, closed=closed, eps=eps, extra=extra), crossings = deduplicate( concat([[0,0]], xings, [[len(path)-1,1]]), eps=eps ), subpaths = [ for (p = pair(crossings)) deduplicate( _path_select(path, p[0][0], p[0][1], p[1][0], p[1][1], closed=closed), eps=eps ) ] ) [for(s=subpaths) if (len(s)>1) s]; // Function: region_parts() // Usage: // rgns = region_parts(region); // Description: // Divides a region into a list of connected regions. Each connected region has exactly one outside boundary // and zero or more outlines defining internal holes. Note that behavior is undefined on invalid regions whose // components intersect each other. // Example(2D,NoAxes): // R = [for(i=[1:7]) square(i,center=true)]; // region_list = region_parts(R); // rainbow(region_list) region($item); // Example(2D,NoAxes): // R = [back(7,square(3,center=true)), // square([20,10],center=true), // left(5,square(8,center=true)), // for(i=[4:2:8]) // right(5,square(i,center=true))]; // region_list = region_parts(R); // rainbow(region_list) region($item); function old_region_parts(region) = let( paths = sort(idx=0, [ for(i = idx(region)) let( pt = mean([region[i][0],region[i][1]]), cnt = sum([for (j = idx(region)) if (i!=j && point_in_polygon(pt, region[j]) >=0) 1]) ) [cnt, region[i]] ]), outs = [ for (candout = paths) let( lev = candout[0], parent = candout[1] ) if (lev % 2 == 0) [ clockwise_polygon(parent), for (path = paths) if ( path[0] == lev+1 && point_in_polygon( lerp(path[1][0], path[1][1], 0.5) ,parent ) >= 0 ) ccw_polygon(path[1]) ] ] ) outs; function inside(region, ins=[]) = let( i = len(ins) ) i==len(region) ? ins : let( pt=mean([region[i][0],region[i][1]]) ) i==0 ? inside(region, [[0, for(j=[1:1:len(region)-1]) point_in_polygon(pt,region[j])>=0 ? 1 : 0]]) : let( prev = [for(j=[0:i-1]) point_in_polygon(pt,region[j])>=0 ? 1 : 0], check = sum(bselect(ins,prev),repeat(0,len(region))), next = [for(j=[i+1:1:len(region)-1]) check[j]>0 ? 1 : point_in_polygon(pt,region[j])>=0 ? 1 : 0] ) inside(region, [ each ins, [each prev, 0, each next] ]); function region_parts(region) = let( inside = [for(i=idx(region)) let(pt = mean([region[i][0], region[i][1]])) [for(j=idx(region)) i==j ? 0 : point_in_polygon(pt,region[j]) >=0 ? 1 : 0] ], level = inside*repeat(1,len(region)) ) [ for(i=idx(region)) if(level[i]%2==0) let( possible_children = search([level[i]+1],level,0)[0], keep=search([1], select(inside,possible_children), 0, i)[0] ) [ clockwise_polygon(region[i]), for(good=keep) ccw_polygon(region[possible_children[good]]) ] ]; // Section: Region Extrusion and VNFs function _path_path_closest_vertices(path1,path2) = let( dists = [for (i=idx(path1)) let(j=closest_point(path1[i],path2)) [j,norm(path2[j]-path1[i])]], i1 = min_index(subindex(dists,1)), i2 = dists[i1][0] ) [dists[i1][1], i1, i2]; function _join_paths_at_vertices(path1,path2,v1,v2) = let( repeat_start = !approx(path1[v1],path2[v2]), path1 = clockwise_polygon(polygon_shift(path1,v1)), path2 = ccw_polygon(polygon_shift(path2,v2)) ) [ each path1, if (repeat_start) path1[0], each path2, if (repeat_start) path2[0], ]; // Given a region that is connected and has its outer border in region[0], // produces a polygon with the same points that has overlapping connected paths // to join internal holes to the outer border. Output is a single path. function _cleave_connected_region(region) = len(region)==0? [] : len(region)<=1? clockwise_polygon(region[0]) : let( dists = [ for (i=[1:1:len(region)-1]) _path_path_closest_vertices(region[0],region[i]) ], idxi = min_index(subindex(dists,0)), newoline = _join_paths_at_vertices( region[0], region[idxi+1], dists[idxi][1], dists[idxi][2] ) ) len(region)==2? clockwise_polygon(newoline) : let( orgn = [ newoline, for (i=idx(region)) if (i>0 && i!=idxi+1) region[i] ] ) assert(len(orgn)maxind)? true : _segment_good(path,pathseg_unit,pathseg_len, d - 1e-7, shiftsegs[i], alpha) ]; // Determine if a segment is good (approximately) // Input is the path, the path segments normalized to unit length, the length of each path segment // the distance threshold, the segment to test, and the locations on the segment to test (normalized to [0,1]) // The last parameter, index, gives the current alpha index. // // A segment is good if any part of it is farther than distance d from the path. The test is expensive, so // we want to quit as soon as we find a point with distance > d, hence the recursive code structure. // // This test is approximate because it only samples the points listed in alpha. Listing more points // will make the test more accurate, but slower. function _segment_good(path,pathseg_unit,pathseg_len, d, seg,alpha ,index=0) = index == len(alpha) ? false : _point_dist(path,pathseg_unit,pathseg_len, alpha[index]*seg[0]+(1-alpha[index])*seg[1]) > d ? true : _segment_good(path,pathseg_unit,pathseg_len,d,seg,alpha,index+1); // Input is the path, the path segments normalized to unit length, the length of each path segment // and a test point. Computes the (minimum) distance from the path to the point, taking into // account that the minimal distance may be anywhere along a path segment, not just at the ends. function _point_dist(path,pathseg_unit,pathseg_len,pt) = min([ for(i=[0:len(pathseg_unit)-1]) let( v = pt-path[i], projection = v*pathseg_unit[i], segdist = projection < 0? norm(pt-path[i]) : projection > pathseg_len[i]? norm(pt-select(path,i+1)) : norm(v-projection*pathseg_unit[i]) ) segdist ]); function _offset_region(region, r, delta, chamfer, check_valid, quality,closed,return_faces,firstface_index,flip_faces) = let( //dg= echo(region=region), reglist = [for(R=region_parts(region)) is_path(R) ? [R] : R], //fdsa= echo(reglist), ofsregs = [for(R=reglist) [for(i=idx(R)) offset(R[i], r=u_mul(i>0?-1:1,r), delta=u_mul(i>0?-1:1,delta), chamfer=chamfer, check_valid=check_valid, quality=quality,closed=true)]] ) union(ofsregs); function d_offset_region( paths, r, delta, chamfer, closed, check_valid, quality, return_faces, firstface_index, flip_faces, _acc=[], _i=0 ) = _i>=len(paths)? _acc : _offset_region( paths, _i=_i+1, _acc = (paths[_i].x % 2 == 0)? ( union(_acc, [ offset( paths[_i].y, r=r, delta=delta, chamfer=chamfer, closed=closed, check_valid=check_valid, quality=quality, return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces ) ]) ) : ( difference(_acc, [ offset( paths[_i].y, r=u_mul(-1,r), delta=u_mul(-1,delta), chamfer=chamfer, closed=closed, check_valid=check_valid, quality=quality, return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces ) ]) ), r=r, delta=delta, chamfer=chamfer, closed=closed, check_valid=check_valid, quality=quality, return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces ); // Function: offset() // Usage: // offsetpath = offset(path, [r|delta], [chamfer], [closed], [check_valid], [quality]) // path_faces = offset(path, return_faces=true, [r|delta], [chamfer], [closed], [check_valid], [quality], [firstface_index], [flip_faces]) // Description: // Takes an input path and returns a path offset by the specified amount. As with the built-in // offset() module, you can use `r` to specify rounded offset and `delta` to specify offset with // corners. If you used `delta` you can set `chamfer` to true to get chamfers. // Positive offsets shift the path to the left (relative to the direction of the path). // . // When offsets shrink the path, segments cross and become invalid. By default `offset()` checks // for this situation. To test validity the code checks that segments have distance larger than (r // or delta) from the input path. This check takes O(N^2) time and may mistakenly eliminate // segments you wanted included in various situations, so you can disable it if you wish by setting // check_valid=false. Another situation is that the test is not sufficiently thorough and some // segments persist that should be eliminated. In this case, increase `quality` to 2 or 3. (This // increases the number of samples on the segment that are checked.) Run time will increase. In // some situations you may be able to decrease run time by setting quality to 0, which causes only // segment ends to be checked. // . // For construction of polyhedra `offset()` can also return face lists. These list faces between // the original path and the offset path where the vertices are ordered with the original path // first, starting at `firstface_index` and the offset path vertices appearing afterwords. The // direction of the faces can be flipped using `flip_faces`. When you request faces the return // value is a list: [offset_path, face_list]. // Arguments: // path = the path to process. A list of 2d points. // --- // r = offset radius. Distance to offset. Will round over corners. // delta = offset distance. Distance to offset with pointed corners. // chamfer = chamfer corners when you specify `delta`. Default: false // closed = path is a closed curve. Default: False. // check_valid = perform segment validity check. Default: True. // quality = validity check quality parameter, a small integer. Default: 1. // return_faces = return face list. Default: False. // firstface_index = starting index for face list. Default: 0. // flip_faces = flip face direction. Default: false // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, delta=10, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, delta=10, chamfer=true, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, r=10, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, delta=-10, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, delta=-10, chamfer=true, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, r=-10, closed=true, $fn=20)); // Example(2D): This case needs `quality=2` for success // test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]]; // polygon(offset(test,r=-1.9, closed=true, quality=2)); // //polygon(offset(test,r=-1.9, closed=true, quality=1)); // Fails with erroneous 180 deg path error // %down(.1)polygon(test); // Example(2D): This case fails if `check_valid=true` when delta is large enough because segments are too close to the opposite side of the curve. // star = star(5, r=22, ir=13); // stroke(star,width=.2,closed=true); // color("green") // stroke(offset(star, delta=-9, closed=true),width=.2,closed=true); // Works with check_valid=true (the default) // color("red") // stroke(offset(star, delta=-10, closed=true, check_valid=false), // Fails if check_valid=true // width=.2,closed=true); // Example(2D): But if you use rounding with offset then you need `check_valid=true` when `r` is big enough. It works without the validity check as long as the offset shape retains a some of the straight edges at the star tip, but once the shape shrinks smaller than that, it fails. There is no simple way to get a correct result for the case with `r=10`, because as in the previous example, it will fail if you turn on validity checks. // star = star(5, r=22, ir=13); // color("green") // stroke(offset(star, r=-8, closed=true,check_valid=false), width=.1, closed=true); // color("red") // stroke(offset(star, r=-10, closed=true,check_valid=false), width=.1, closed=true); // Example(2D): The extra triangles in this example show that the validity check cannot be skipped // ellipse = scale([20,4], p=circle(r=1,$fn=64)); // stroke(ellipse, closed=true, width=0.3); // stroke(offset(ellipse, r=-3, check_valid=false, closed=true), width=0.3, closed=true); // Example(2D): The triangles are removed by the validity check // ellipse = scale([20,4], p=circle(r=1,$fn=64)); // stroke(ellipse, closed=true, width=0.3); // stroke(offset(ellipse, r=-3, check_valid=true, closed=true), width=0.3, closed=true); // Example(2D): Open path. The path moves from left to right and the positive offset shifts to the left of the initial red path. // sinpath = 2*[for(theta=[-180:5:180]) [theta/4,45*sin(theta)]]; // #stroke(sinpath); // stroke(offset(sinpath, r=17.5)); // Example(2D): Region // rgn = difference(circle(d=100), union(square([20,40], center=true), square([40,20], center=true))); // #linear_extrude(height=1.1) for (p=rgn) stroke(closed=true, width=0.5, p); // region(offset(rgn, r=-5)); function offset( path, r=undef, delta=undef, chamfer=false, closed=false, check_valid=true, quality=1, return_faces=false, firstface_index=0, flip_faces=false ) = is_region(path)? _offset_region(path,r=r,delta=delta,chamfer=chamfer,quality=quality,check_valid=check_valid) /* ( assert(!return_faces, "return_faces not supported for regions.") let( path = [for (p=path) clockwise_polygon(p)], rgn = exclusive_or([for (p = path) [p]]), pathlist = sort(idx=0,[ for (i=[0:1:len(rgn)-1]) [ sum(concat([0],[ for (j=[0:1:len(rgn)-1]) if (i!=j) point_in_polygon(rgn[i][0],rgn[j])>=0? 1 : 0 ])), rgn[i] ] ]) ) _offset_region( pathlist, r=r, delta=delta, chamfer=chamfer, closed=true, check_valid=check_valid, quality=quality, return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces ) )*/ : let(rcount = num_defined([r,delta])) assert(rcount==1,"Must define exactly one of 'delta' and 'r'") let( chamfer = is_def(r) ? false : chamfer, quality = max(0,round(quality)), flip_dir = closed && !is_polygon_clockwise(path)? -1 : 1, d = flip_dir * (is_def(r) ? r : delta), // shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)], shiftsegs = [for(i=[0:len(path)-2]) _shift_segment([path[i],path[i+1]], d), if (closed) _shift_segment([last(path),path[0]],d) else [path[0],path[1]] // dummy segment, not used ], // good segments are ones where no point on the segment is less than distance d from any point on the path good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality) : repeat(true,len(shiftsegs)), goodsegs = bselect(shiftsegs, good), goodpath = bselect(path,good) ) assert(len(goodsegs)>0,"Offset of path is degenerate") let( // Extend the shifted segments to their intersection points sharpcorners = [for(i=[0:len(goodsegs)-1]) _segment_extension(select(goodsegs,i-1), select(goodsegs,i))], // If some segments are parallel then the extended segments are undefined. This case is not handled // Note if !closed the last corner doesn't matter, so exclude it parallelcheck = (len(sharpcorners)==2 && !closed) || all_defined(closed? sharpcorners : select(sharpcorners, 1,-2)) ) assert(parallelcheck, "Path contains sequential parallel segments (either 180 deg turn or 0 deg turn") let( // This is a boolean array that indicates whether a corner is an outside or inside corner // For outside corners, the newcorner is an extension (angle 0), for inside corners, it turns backward // If either side turns back it is an inside corner---must check both. // Outside corners can get rounded (if r is specified and there is space to round them) outsidecorner = len(sharpcorners)==2 ? [false,false] : [for(i=[0:len(goodsegs)-1]) let(prevseg=select(goodsegs,i-1)) (i==0 || i==len(goodsegs)-1) && !closed ? false // In open case first entry is bogus : (goodsegs[i][1]-goodsegs[i][0]) * (goodsegs[i][0]-sharpcorners[i]) > 0 && (prevseg[1]-prevseg[0]) * (sharpcorners[i]-prevseg[1]) > 0 ], steps = is_def(delta) ? [] : [ for(i=[0:len(goodsegs)-1]) r==0 ? 0 // floor is important here to ensure we don't generate extra segments when nearly straight paths expand outward : 1+floor(segs(r)*vector_angle( select(goodsegs,i-1)[1]-goodpath[i], goodsegs[i][0]-goodpath[i]) /360) ], // If rounding is true then newcorners replaces sharpcorners with rounded arcs where needed // Otherwise it's the same as sharpcorners // If rounding is on then newcorners[i] will be the point list that replaces goodpath[i] and newcorners later // gets flattened. If rounding is off then we set it to [sharpcorners] so we can later flatten it and get // plain sharpcorners back. newcorners = is_def(delta) && !chamfer ? [sharpcorners] : [for(i=[0:len(goodsegs)-1]) (!chamfer && steps[i] <=1) // Don't round if steps is smaller than 2 || !outsidecorner[i] // Don't round inside corners || (!closed && (i==0 || i==len(goodsegs)-1)) // Don't round ends of an open path ? [sharpcorners[i]] : chamfer ? _offset_chamfer( goodpath[i], [ select(goodsegs,i-1)[1], sharpcorners[i], goodsegs[i][0] ], d ) : // rounded case arc(cp=goodpath[i], points=[ select(goodsegs,i-1)[1], goodsegs[i][0] ], N=steps[i]) ], pointcount = (is_def(delta) && !chamfer)? repeat(1,len(sharpcorners)) : [for(i=[0:len(goodsegs)-1]) len(newcorners[i])], start = [goodsegs[0][0]], end = [goodsegs[len(goodsegs)-2][1]], edges = closed? flatten(newcorners) : concat(start,slice(flatten(newcorners),1,-2),end), faces = !return_faces? [] : _makefaces( flip_faces, firstface_index, good, pointcount, closed ) ) return_faces? [edges,faces] : edges; /// Internal Function: _tag_subpaths() /// splits the polygon (path) into subpaths by region crossing and then tags each subpath: /// "O" - the subpath is outside the region /// "I" - the subpath is inside the region's interior /// "S" - the subpath is on the region's border and the polygon and region are on the same side of the subpath /// "U" - the subpath is on the region's border and the polygon and region meet at the subpath (from opposite sides) /// The return has the form of a list with entries [TAG, SUBPATH] function _tag_subpaths(region1, region2, eps=EPSILON, SUtags=true) = // We have to compute common vertices between paths in the region because // they can be places where the path must be cut, even though they aren't // found my the split_path function. let( points = flatten(region1), tree = len(points)>0 ? vector_search_tree(points): undef ) [for(path=region1) let( self_int = is_undef(tree)?[]:[for(i=idx(path)) if (len(vector_search(path[i], eps, tree))>1) [i,0]], subpaths = split_path_at_region_crossings(path, region2, eps=eps, extra=self_int) ) for (subpath = subpaths) let( midpt = mean([subpath[0], subpath[1]]), rel = point_in_region(midpt,region2,eps=eps) ) if(rel<0) ["O", subpath] else if (rel>0) ["I", subpath] else if (SUtags) let( sidept = midpt + 0.01*line_normal(subpath[0],subpath[1]), rel1 = point_in_region(sidept,region1,eps=eps)>0, rel2 = point_in_region(sidept,region2,eps=eps)>0 ) rel1==rel2? ["S", subpath] : ["U", subpath] ]; function _tagged_region(region1,region2,keep1,keep2,eps=EPSILON) = let( tagged1 = _tag_subpaths(region1, region2, eps=eps), tagged2 = _tag_subpaths(region2, region1, eps=eps), tagged = [ for (tagpath = tagged1) if (in_list(tagpath[0], keep1)) tagpath[1], for (tagpath = tagged2) if (in_list(tagpath[0], keep2)) tagpath[1] ] ) _assemble_path_fragments(tagged, eps=eps); // Function&Module: union() // Usage: // union() {...} // region = union(regions); // region = union(REGION1,REGION2); // region = union(REGION1,REGION2,REGION3); // Description: // When called as a function and given a list of regions, where each region is a list of closed // 2D paths, returns the boolean union of all given regions. Result is a single region. // When called as the built-in module, makes the boolean union of the given children. // Arguments: // regions = List of regions to union. Each region is a list of closed paths. // Example(2D): // shape1 = move([-8,-8,0], p=circle(d=50)); // shape2 = move([ 8, 8,0], p=circle(d=50)); // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); // color("green") region(union(shape1,shape2)); function union(regions=[],b=undef,c=undef,eps=EPSILON) = b!=undef? union(concat([regions],[b],c==undef?[]:[c]), eps=eps) : len(regions)==0? [] : len(regions)==1? regions[0] : let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) union([ _tagged_region(regions[0],regions[1],["O","S"],["O"], eps=eps), for (i=[2:1:len(regions)-1]) regions[i] ], eps=eps ); // Function&Module: difference() // Usage: // difference() {...} // region = difference(regions); // region = difference(REGION1,REGION2); // region = difference(REGION1,REGION2,REGION3); // Description: // When called as a function, and given a list of regions, where each region is a list of closed // 2D paths, takes the first region and differences away all other regions from it. The resulting // region is returned. // When called as the built-in module, makes the boolean difference of the given children. // Arguments: // regions = List of regions to difference. Each region is a list of closed paths. // Example(2D): // shape1 = move([-8,-8,0], p=circle(d=50)); // shape2 = move([ 8, 8,0], p=circle(d=50)); // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); // color("green") region(difference(shape1,shape2)); function difference(regions=[],b=undef,c=undef,eps=EPSILON) = b!=undef? difference(concat([regions],[b],c==undef?[]:[c]), eps=eps) : len(regions)==0? [] : len(regions)==1? regions[0] : regions[0]==[] ? [] : let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) difference([ _tagged_region(regions[0],regions[1],["O","U"],["I"], eps=eps), for (i=[2:1:len(regions)-1]) regions[i] ], eps=eps ); // Function&Module: intersection() // Usage: // intersection() {...} // region = intersection(regions); // region = intersection(REGION1,REGION2); // region = intersection(REGION1,REGION2,REGION3); // Description: // When called as a function, and given a list of regions, where each region is a list of closed // 2D paths, returns the boolean intersection of all given regions. Result is a single region. // When called as the built-in module, makes the boolean intersection of all the given children. // Arguments: // regions = List of regions to intersection. Each region is a list of closed paths. // Example(2D): // shape1 = move([-8,-8,0], p=circle(d=50)); // shape2 = move([ 8, 8,0], p=circle(d=50)); // for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true); // color("green") region(intersection(shape1,shape2)); function intersection(regions=[],b=undef,c=undef,eps=EPSILON) = // echo(regions=regions) b!=undef? intersection(concat([regions],[b],c==undef?[]:[c]),eps=eps) : len(regions)==0 ? [] : len(regions)==1? regions[0] : regions[0]==[] || regions[1]==[] ? [] : let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)]) intersection([ _tagged_region(regions[0],regions[1],["I","S"],["I"],eps=eps), for (i=[2:1:len(regions)-1]) regions[i] ], eps=eps ); // Function&Module: exclusive_or() // Usage: // exclusive_or() {...} // region = exclusive_or(regions); // region = exclusive_or(REGION1,REGION2); // region = exclusive_or(REGION1,REGION2,REGION3); // Description: // When called as a function and given a list of regions, where each region is a list of closed // 2D paths, returns the boolean exclusive_or of all given regions. Result is a single region. // When called as a module, performs a boolean exclusive-or of up to 10 children. Note that the // xor operator tends to produce shapes that meet at corners, which do not render in CGAL. // Arguments: // regions = List of regions to exclusive_or. Each region is a list of closed paths. // Example(2D): As Function. A linear_sweep of this shape fails to render in CGAL. // shape1 = move([-8,-8,0], p=circle(d=50)); // shape2 = move([ 8, 8,0], p=circle(d=50)); // for (shape = [shape1,shape2]) // color("red") stroke(shape, width=0.5, closed=true); // color("green") region(exclusive_or(shape1,shape2)); // Example(2D): As Module. A linear_extrude() of the resulting geometry fails to render in CGAL. // exclusive_or() { // square(40,center=false); // circle(d=40); // } function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) = b!=undef? exclusive_or([regions, b, if(is_def(c)) c],eps=eps) : len(regions)==0? [] : len(regions)==1? regions[0] : let(regions=[for (r=regions) is_path(r)? [r] : r]) exclusive_or([ _tagged_region(regions[0],regions[1],["I","O"],["I","O"],eps=eps), for (i=[2:1:len(regions)-1]) regions[i] ], eps=eps ); module exclusive_or() { if ($children==1) { children(); } else if ($children==2) { difference() { children(0); children(1); } difference() { children(1); children(0); } } else if ($children==3) { exclusive_or() { exclusive_or() { children(0); children(1); } children(2); } } else if ($children==4) { exclusive_or() { exclusive_or() { children(0); children(1); } exclusive_or() { children(2); children(3); } } } else if ($children==5) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } children(4); } } else if ($children==6) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } children(4); children(5); } } else if ($children==7) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } children(4); children(5); children(6); } } else if ($children==8) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } exclusive_or() { children(4); children(5); children(6); children(7); } } } else if ($children==9) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } exclusive_or() { children(4); children(5); children(6); children(7); } children(8); } } else if ($children==10) { exclusive_or() { exclusive_or() { children(0); children(1); children(2); children(3); } exclusive_or() { children(4); children(5); children(6); children(7); } children(8); children(9); } } else { assert($children<=10, "exclusive_or() can only handle up to 10 children."); } } // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap