mirror of
https://github.com/BelfrySCAD/BOSL2.git
synced 2024-12-28 15:59:45 +00:00
Split regions and boolean geometry out of geometry.scad into regions.scad. Added various ray intersection functions. Added plane intersection functions.
This commit is contained in:
parent
0ab64b746d
commit
4f0e6392e2
4 changed files with 1375 additions and 1214 deletions
1748
geometry.scad
1748
geometry.scad
File diff suppressed because it is too large
Load diff
838
regions.scad
Normal file
838
regions.scad
Normal file
|
@ -0,0 +1,838 @@
|
|||
//////////////////////////////////////////////////////////////////////
|
||||
// LibFile: regions.scad
|
||||
// Regions and 2D boolean geometry
|
||||
// To use, add the following lines to the beginning of your file:
|
||||
// ```
|
||||
// use <BOSL2/std.scad>
|
||||
// ```
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// CommonCode:
|
||||
// include <BOSL2/rounding.scad>
|
||||
|
||||
|
||||
// Section: Regions
|
||||
|
||||
|
||||
// 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: close_region()
|
||||
// Usage:
|
||||
// close_region(region);
|
||||
// Description:
|
||||
// Closes all paths within a given region.
|
||||
function close_region(region, eps=EPSILON) = [for (path=region) close_path(path, 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)
|
||||
{
|
||||
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: check_and_fix_path()
|
||||
// Usage:
|
||||
// check_and_fix_path(path, [valid_dim], [closed])
|
||||
// Description:
|
||||
// Checks that the input is a path. If it is a region with one component, converts it to a path.
|
||||
// valid_dim specfies the allowed dimension of the points in the path.
|
||||
// If the path is closed, removed 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
|
||||
function check_and_fix_path(path, valid_dim=undef, closed=false) =
|
||||
let(
|
||||
path = is_region(path)? (
|
||||
assert(len(path)==1,"Region supplied as path does not have exactly one component")
|
||||
path[0]
|
||||
) : (
|
||||
assert(is_path(path), "Input is not a path")
|
||||
path
|
||||
),
|
||||
dim = array_dim(path)
|
||||
)
|
||||
assert(dim[0]>1,"Path must have at least 2 points")
|
||||
assert(len(dim)==2,"Invalid path: path is either a list of scalars or a list of matrices")
|
||||
assert(is_def(dim[1]), "Invalid path: entries in the path have variable length")
|
||||
let(valid=is_undef(valid_dim) || in_list(dim[1],valid_dim))
|
||||
assert(
|
||||
valid, str(
|
||||
"The points on the path have length ",
|
||||
dim[1], " but length must be ",
|
||||
len(valid_dim)==1? valid_dim[0] : str("one of ",valid_dim)
|
||||
)
|
||||
)
|
||||
closed && approx(path[0],select(path,-1))? slice(path,0,-2) : path;
|
||||
|
||||
|
||||
// Function: cleanup_region()
|
||||
// Usage:
|
||||
// cleanup_region(region);
|
||||
// Description:
|
||||
// For all paths in the given region, if the last point coincides with the first point, removes the last point.
|
||||
// Arguments:
|
||||
// region = The region to clean up. Given as a list of polygon paths.
|
||||
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
|
||||
function cleanup_region(region, eps=EPSILON) =
|
||||
[for (path=region) cleanup_path(path, eps=eps)];
|
||||
|
||||
|
||||
// Function: point_in_region()
|
||||
// Usage:
|
||||
// point_in_region(point, region);
|
||||
// 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: region_path_crossings()
|
||||
// Usage:
|
||||
// region_path_crossings(path, region);
|
||||
// Description:
|
||||
// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path.
|
||||
// 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 region_path_crossings(path, region, closed=true, eps=EPSILON) = sort([
|
||||
let(
|
||||
segs = pair(closed? close_path(path) : cleanup_path(path))
|
||||
) for (
|
||||
si = idx(segs),
|
||||
p = close_region(region),
|
||||
s2 = pair(p)
|
||||
) let (
|
||||
isect = _general_line_intersection(segs[si], s2, eps=eps)
|
||||
) if (
|
||||
!is_undef(isect) &&
|
||||
isect[1] >= 0-eps && isect[1] < 1+eps &&
|
||||
isect[2] >= 0-eps && isect[2] < 1+eps
|
||||
)
|
||||
[si, isect[1]]
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// 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) =
|
||||
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);
|
||||
|
||||
|
||||
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,
|
||||
maxstep, 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,
|
||||
maxstep=maxstep, 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=-r, delta=-delta, chamfer=chamfer, closed=closed,
|
||||
maxstep=maxstep, 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,
|
||||
maxstep=maxstep, check_valid=check_valid, quality=quality,
|
||||
return_faces=return_faces, firstface_index=firstface_index, flip_faces=flip_faces
|
||||
);
|
||||
|
||||
|
||||
// Function: offset()
|
||||
//
|
||||
// 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. 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));
|
||||
// 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,
|
||||
maxstep=0.1, 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) polygon_is_clockwise(p)? p : reverse(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,
|
||||
maxstep=maxstep, 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 && !polygon_is_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)],
|
||||
// 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) : replist(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(select(sharpcorners,closed?0:1,-1))
|
||||
)
|
||||
assert(parallelcheck, "Path turns back on itself (180 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 = [
|
||||
for(i=[0:len(goodsegs)-1]) let(
|
||||
prevseg=select(goodsegs,i-1)
|
||||
) (
|
||||
(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])
|
||||
ceil(
|
||||
abs(r)*vector_angle(
|
||||
select(goodsegs,i-1)[1]-goodpath[i],
|
||||
goodsegs[i][0]-goodpath[i]
|
||||
)*PI/180/maxstep
|
||||
)
|
||||
],
|
||||
// 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] <=2) //Chamfer all points but only round if steps is 3 or more
|
||||
|| !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
|
||||
) :
|
||||
arc(
|
||||
cp=goodpath[i],
|
||||
points=[
|
||||
select(goodsegs,i-1)[1],
|
||||
goodsegs[i][0]
|
||||
],
|
||||
N=steps[i]
|
||||
)
|
||||
)
|
||||
],
|
||||
pointcount = (is_def(delta) && !chamfer)?
|
||||
replist(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;
|
||||
|
||||
|
||||
// Function: split_path_at_region_crossings()
|
||||
// Usage:
|
||||
// polylines = split_path_at_region_crossings(path, region, [eps]);
|
||||
// Description:
|
||||
// Splits a path into polyline sections 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)];
|
||||
// polylines = split_path_at_region_crossings(path, region);
|
||||
// color("#aaa") region(region);
|
||||
// rainbow(polylines) 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 = region_path_crossings(path, region, closed=closed, eps=eps),
|
||||
crossings = deduplicate(
|
||||
concat([[0,0]], xings, [[len(path)-1,1]]),
|
||||
eps=eps
|
||||
),
|
||||
subpaths = [
|
||||
for (p = pair(crossings))
|
||||
deduplicate(eps=eps,
|
||||
path_subselect(path, p[0][0], p[0][1], p[1][0], p[1][1], closed=closed)
|
||||
)
|
||||
]
|
||||
)
|
||||
subpaths;
|
||||
|
||||
|
||||
function _tag_subpaths(path, region, eps=EPSILON) =
|
||||
let(
|
||||
subpaths = split_path_at_region_crossings(path, region, eps=eps),
|
||||
tagged = [
|
||||
for (sub = subpaths) let(
|
||||
subpath = deduplicate(sub)
|
||||
) if (len(sub)>1) let(
|
||||
midpt = lerp(subpath[0], subpath[1], 0.5),
|
||||
rel = point_in_region(midpt,region,eps=eps)
|
||||
) rel<0? ["O", subpath] : rel>0? ["I", subpath] : let(
|
||||
vec = normalize(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(
|
||||
region1 = close_region(region1, eps=eps),
|
||||
region2 = close_region(region2, eps=eps),
|
||||
tagged1 = _tag_region_subpaths(region1, region2, eps=eps),
|
||||
tagged2 = _tag_region_subpaths(region2, region1, eps=eps),
|
||||
tagged = concat(
|
||||
[for (tagpath = tagged1) if (in_list(tagpath[0], keep1)) tagpath[1]],
|
||||
[for (tagpath = tagged2) if (in_list(tagpath[0], keep2)) tagpath[1]]
|
||||
),
|
||||
outregion = assemble_path_fragments(tagged, eps=eps)
|
||||
) outregion;
|
||||
|
||||
|
||||
|
||||
// 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) is_path(r)? [r] : r])
|
||||
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)<=1? regions[0] :
|
||||
difference(
|
||||
let(regions=[for (r=regions) is_path(r)? [r] : r])
|
||||
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)<=1? regions[0] :
|
||||
intersection(
|
||||
let(regions=[for (r=regions) is_path(r)? [r] : r])
|
||||
concat(
|
||||
[_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.
|
||||
// Arguments:
|
||||
// regions = List of regions to exclusive_or. Each region is a list of closed paths.
|
||||
// Example(2D): As Function
|
||||
// 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
|
||||
// exclusive_or() {
|
||||
// square(40,center=false);
|
||||
// circle(d=40);
|
||||
// }
|
||||
function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) =
|
||||
b!=undef? exclusive_or(concat([regions],[b],c==undef?[]:[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: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
|
1
std.scad
1
std.scad
|
@ -23,6 +23,7 @@ include <quaternions.scad>
|
|||
include <affine.scad>
|
||||
include <coords.scad>
|
||||
include <geometry.scad>
|
||||
include <regions.scad>
|
||||
|
||||
include <transforms.scad>
|
||||
include <attachments.scad>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
BOSL_VERSION = [2,0,48];
|
||||
BOSL_VERSION = [2,0,49];
|
||||
|
||||
|
||||
// Section: BOSL Library Version Functions
|
||||
|
|
Loading…
Reference in a new issue