BOSL2/regions.scad
Adrian Mariano bb77faa0c9 assorted doc fixes
eliminate trace_path
_path_self_intersections fix
2021-10-06 21:16:39 -04:00

1156 lines
49 KiB
OpenSCAD

//////////////////////////////////////////////////////////////////////
// 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 <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// CommonCode:
// include <BOSL2/rounding.scad>
// 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);
// 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.
function sanitize_region(r) = exclusive_or([for(poly=r) each polygon_parts(poly)]);
// 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)
{
points = flatten(r);
paths = [
for (i=[0:1:len(r)-1]) let(
start = default(sum([for (j=[0:1:i-1]) len(r[j])]),0)
) [for (k=[0:1:len(r[i])-1]) start+k]
];
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 _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]]
]
);
// 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) =
let(
path = deduplicate(path, eps=eps),
region = [for (path=region) deduplicate(path, eps=eps)],
xings = _path_region_intersections(path, region, closed=closed, eps=eps),
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 = split_nested_region(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 = split_nested_region(R);
// rainbow(region_list) region($item);
function region_parts(region) =
let(
paths = sort(idx=0, [
for(i = idx(region)) let(
cnt = sum([
for (j = idx(region)) if (i!=j)
let(pt = lerp(region[i][0],region[i][1],0.5))
point_in_polygon(pt, region[j]) >=0 ? 1 : 0
])
) [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;
// 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)<len(region))
_cleave_connected_region(orgn);
// Function: region_faces()
// Usage:
// vnf = region_faces(region, [transform], [reverse], [vnf]);
// Description:
// Given a region, applies the given transformation matrix to it and makes a VNF of
// faces for that region, reversed if necessary.
// Arguments:
// region = The region to make faces for.
// transform = If given, a transformation matrix to apply to the faces generated from the region. Default: No transformation applied.
// reverse = If true, reverse the normals of the faces generated from the region. An untransformed region will have face normals pointing `UP`. Default: false
// vnf = If given, the faces are added to this VNF. Default: `EMPTY_VNF`
function region_faces(region, transform, reverse=false, vnf=EMPTY_VNF) =
let (
regions = region_parts(region),
vnfs = [
if (vnf != EMPTY_VNF) vnf,
for (rgn = regions) let(
cleaved = path3d(_cleave_connected_region(rgn)),
face = is_undef(transform)? cleaved : apply(transform,cleaved),
faceidxs = reverse? [for (i=[len(face)-1:-1:0]) i] : [for (i=[0:1:len(face)-1]) i]
) [face, [faceidxs]]
],
outvnf = vnf_merge(vnfs)
) outvnf;
// Function&Module: linear_sweep()
// Usage:
// linear_sweep(region, height, [center], [slices], [twist], [scale], [style], [convexity]);
// Description:
// If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or path.
// If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
// of the given 2D region or path. The benefit of using this, over using `linear_extrude region(rgn)` is
// that you can use `anchor`, `spin`, `orient` and attachments with it. Also, you can make more refined
// twisted extrusions by using `maxseg` to subsample flat faces.
// Arguments:
// region = The 2D [Region](regions.scad) or path that is to be extruded.
// height = The height to extrude the region. Default: 1
// center = If true, the created polyhedron will be vertically centered. If false, it will be extruded upwards from the origin. Default: `false`
// slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist. Default: `twist/5`
// maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length. This can refine twisting flat faces a lot. Default: `undef` (no subsampling)
// twist = The number of degrees to rotate the shape clockwise around the Z axis, as it rises from bottom to top. Default: 0
// scale = The amount to scale the shape, from bottom to top. Default: 1
// style = The style to use when triangulating the surface of the object. Valid values are `"default"`, `"alt"`, or `"quincunx"`.
// convexity = Max number of surfaces any single ray could pass through. Module use only.
// anchor_isect = If true, anchoring it performed by finding where the anchor vector intersects the swept shape. Default: false
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP`
// Example: Extruding a Compound Region.
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(orgn,height=20,convexity=16);
// Example: With Twist, Scale, Slices and Maxseg.
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(orgn,height=50,maxseg=2,slices=40,twist=180,scale=0.5,convexity=16);
// Example: Anchors on an Extruded Region
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(orgn,height=20,convexity=16) show_anchors();
module linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, style="default", convexity, anchor_isect=false, anchor, spin=0, orient=UP) {
region = is_path(region)? [region] : region;
cp = mean(pointlist_bounds(flatten(region)));
anchor = get_anchor(anchor, center, "origin", "origin");
vnf = linear_sweep(
region, height=height,
twist=twist, scale=scale,
slices=slices, maxseg=maxseg,
style=style
);
attachable(anchor,spin,orient, cp=cp, vnf=vnf, extent=!anchor_isect) {
vnf_polyhedron(vnf, convexity=convexity);
children();
}
}
function linear_sweep(region, height=1, center, twist=0, scale=1, slices,
maxseg, style="default", anchor_isect=false, anchor, spin=0, orient=UP) =
let(
anchor = get_anchor(anchor,center,BOT,BOT),
region = is_path(region)? [region] : region,
cp = mean(pointlist_bounds(flatten(region))),
regions = region_parts(region),
slices = default(slices, floor(twist/5+1)),
step = twist/slices,
hstep = height/slices,
trgns = [
for (rgn=regions) [
for (path=rgn) let(
p = cleanup_path(path),
path = is_undef(maxseg)? p : [
for (seg=pair(p,true)) each
let(steps=ceil(norm(seg.y-seg.x)/maxseg))
lerpn(seg.x, seg.y, steps, false)
]
)
rot(twist, p=scale([scale,scale],p=path))
]
],
vnf = vnf_merge([
for (rgn = regions)
for (pathnum = idx(rgn)) let(
p = cleanup_path(rgn[pathnum]),
path = is_undef(maxseg)? p : [
for (seg=pair(p,true)) each
let(steps=ceil(norm(seg.y-seg.x)/maxseg))
lerpn(seg.x, seg.y, steps, false)
],
verts = [
for (i=[0:1:slices]) let(
sc = lerp(1, scale, i/slices),
ang = i * step,
h = i * hstep - height/2
) scale([sc,sc,1], p=rot(ang, p=path3d(path,h)))
]
) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
for (rgn = regions) region_faces(rgn, move([0,0,-height/2]), reverse=true),
for (rgn = trgns) region_faces(rgn, move([0,0, height/2]), reverse=false)
])
) reorient(anchor,spin,orient, cp=cp, vnf=vnf, extent=!anchor_isect, p=vnf);
// Section: Offsets and Boolean 2D Geometry
function _offset_chamfer(center, points, delta) =
let(
dist = sign(delta)*norm(center-line_intersection(select(points,[0,2]), [center, points[1]])),
endline = _shift_segment(select(points,[0,2]), delta-dist)
) [
line_intersection(endline, select(points,[0,1])),
line_intersection(endline, select(points,[1,2]))
];
function _shift_segment(segment, d) =
assert(!approx(segment[0],segment[1]),"Path has repeated points")
move(d*line_normal(segment),segment);
// Extend to segments to their intersection point. First check if the segments already have a point in common,
// which can happen if two colinear segments are input to the path variant of `offset()`
function _segment_extension(s1,s2) =
norm(s1[1]-s2[0])<1e-6 ? s1[1] : line_intersection(s1,s2,LINE,LINE);
function _makefaces(direction, startind, good, pointcount, closed) =
let(
lenlist = list_bset(good, pointcount),
numfirst = len(lenlist),
numsecond = sum(lenlist),
prelim_faces = _makefaces_recurse(startind, startind+len(lenlist), numfirst, numsecond, lenlist, closed)
)
direction? [for(entry=prelim_faces) reverse(entry)] : prelim_faces;
function _makefaces_recurse(startind1, startind2, numfirst, numsecond, lenlist, closed, firstind=0, secondind=0, faces=[]) =
// We are done if *both* firstind and secondind reach their max value, which is the last point if !closed or one past
// the last point if closed (wrapping around). If you don't check both you can leave a triangular gap in the output.
((firstind == numfirst - (closed?0:1)) && (secondind == numsecond - (closed?0:1)))? faces :
_makefaces_recurse(
startind1, startind2, numfirst, numsecond, lenlist, closed, firstind+1, secondind+lenlist[firstind],
lenlist[firstind]==0? (
// point in original path has been deleted in offset path, so it has no match. We therefore
// make a triangular face using the current point from the offset (second) path
// (The current point in the second path can be equal to numsecond if firstind is the last point)
concat(faces,[[secondind%numsecond+startind2, firstind+startind1, (firstind+1)%numfirst+startind1]])
// in this case a point or points exist in the offset path corresponding to the original path
) : (
concat(faces,
// First generate triangular faces for all of the extra points (if there are any---loop may be empty)
[for(i=[0:1:lenlist[firstind]-2]) [firstind+startind1, secondind+i+1+startind2, secondind+i+startind2]],
// Finish (unconditionally) with a quadrilateral face
[
[
firstind+startind1,
(firstind+1)%numfirst+startind1,
(secondind+lenlist[firstind])%numsecond+startind2,
(secondind+lenlist[firstind]-1)%numsecond+startind2
]
]
)
)
);
// Determine which of the shifted segments are good
function _good_segments(path, d, shiftsegs, closed, quality) =
let(
maxind = len(path)-(closed ? 1 : 2),
pathseg = [for(i=[0:maxind]) select(path,i+1)-path[i]],
pathseg_len = [for(seg=pathseg) norm(seg)],
pathseg_unit = [for(i=[0:maxind]) pathseg[i]/pathseg_len[i]],
// Order matters because as soon as a valid point is found, the test stops
// This order works better for circular paths because they succeed in the center
alpha = concat([for(i=[1:1:quality]) i/(quality+1)],[0,1])
) [
for (i=[0:len(shiftsegs)-1])
(i>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(
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)? (
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(path, region, eps=EPSILON) =
let(
subpaths = split_path_at_region_crossings(path, region, eps=eps),
tagged = [
for (subpath = subpaths)
let(
midpt = mean([subpath[0], subpath[1]]),
rel = point_in_region(midpt,region,eps=eps)
)
rel<0? ["O", subpath]
: rel>0? ["I", subpath]
: let(
vec = unit(subpath[1]-subpath[0]),
perp = rot(90, planar=true, p=vec),
sidept = midpt + perp*0.01,
rel1 = point_in_polygon(sidept,path,eps=eps)>0,
rel2 = point_in_region(sidept,region,eps=eps)>0
)
rel1==rel2? ["S", subpath] : ["U", subpath]
]
) tagged;
function _tag_region_subpaths(region1, region2, eps=EPSILON) =
[for (path=region1) each _tag_subpaths(path, region2, eps=eps)];
function _tagged_region(region1,region2,keep1,keep2,eps=EPSILON) =
let(
tagged1 = _tag_region_subpaths(region1, region2, eps=eps),
tagged2 = _tag_region_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)<=1? regions[0] :
union(
let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)])
concat(
[_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] :
difference(
let(regions=[for (r=regions) quant(is_path(r)? [r] : r, 1/65536)])
concat(
[_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) =
b!=undef? intersection(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)])
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)<=1? regions[0] :
exclusive_or(
let(regions=[for (r=regions) is_path(r)? [r] : r])
concat(
[union([
difference([regions[0],regions[1]], eps=eps),
difference([regions[1],regions[0]], eps=eps)
], 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