2019-12-02 23:35:03 +00:00
//////////////////////////////////////////////////////////////////////
// LibFile: regions.scad
2021-09-28 20:36:32 +00:00
// 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.
2021-01-05 09:20:01 +00:00
// Includes:
// include <BOSL2/std.scad>
2019-12-02 23:35:03 +00:00
//////////////////////////////////////////////////////////////////////
// CommonCode:
// include <BOSL2/rounding.scad>
// Section: Regions
2021-09-28 20:36:32 +00:00
// 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().
2019-12-02 23:35:03 +00:00
// 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:
2021-03-12 23:40:24 +00:00
// check_and_fix_path(path, [valid_dim], [closed], [name])
2019-12-02 23:35:03 +00:00
// Description:
// Checks that the input is a path. If it is a region with one component, converts it to a path.
2021-03-12 23:40:24 +00:00
// Note that arbitrary paths must have at least two points, but closed paths need at least 3 points.
2019-12-02 23:35:03 +00:00
// valid_dim specfies the allowed dimension of the points in the path.
2021-03-12 23:40:24 +00:00
// If the path is closed, removes duplicate endpoint if present.
2019-12-02 23:35:03 +00:00
// 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
2021-03-12 23:40:24 +00:00
// name = parameter name to use for reporting errors. Default: "path"
function check_and_fix_path ( path , valid_dim = undef , closed = false , name = "path" ) =
2020-05-30 02:04:34 +00:00
let (
2021-03-12 23:40:24 +00:00
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
2020-05-30 02:04:34 +00:00
)
2021-03-12 23:40:24 +00:00
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 ) ) )
2020-05-30 02:04:34 +00:00
assert (
valid , str (
2021-03-12 23:40:24 +00:00
"Input " , name , " must has dimension " , len ( path [ 0 ] ) , " but dimension must be " ,
is_list ( valid_dim ) ? str ( "one of " , valid_dim ) : valid_dim
2020-05-30 02:04:34 +00:00
)
)
2021-03-25 07:23:36 +00:00
closed && approx ( path [ 0 ] , last ( path ) ) ? list_head ( path ) : path ;
2019-12-02 23:35:03 +00:00
2021-09-22 18:59:18 +00:00
2021-09-28 01:36:24 +00:00
// Function: sanitize_region()
// Usage:
2021-10-10 01:44:26 +00:00
// r_fixed = sanitize_region(r, [nonzero], [eps]);
2021-09-28 01:36:24 +00:00
// 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.
2021-10-10 01:44:26 +00:00
// 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 ) ;
2021-09-28 01:36:24 +00:00
2021-09-22 18:59:18 +00:00
// Module: region()
2019-12-02 23:35:03 +00:00
// Usage:
2021-09-22 18:59:18 +00:00
// region(r);
2019-12-02 23:35:03 +00:00
// Description:
2021-09-22 18:59:18 +00:00
// 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 )
{
2021-10-10 01:44:26 +00:00
no_children ( $children ) ;
r = is_path ( r ) ? [ r ] : r ;
2021-09-22 18:59:18 +00:00
points = flatten ( r ) ;
2021-10-10 01:44:26 +00:00
lengths = [ for ( path = r ) len ( path ) ] ;
starts = [ 0 , each cumsum ( lengths ) ] ;
paths = [ for ( i = idx ( r ) ) count ( s = starts [ i ] , n = lengths [ i ] ) ] ;
2021-09-22 18:59:18 +00:00
polygon ( points = points , paths = paths ) ;
}
2019-12-02 23:35:03 +00:00
// Function: point_in_region()
// Usage:
2021-10-07 01:16:39 +00:00
// check = point_in_region(point, region, [eps]);
2019-12-02 23:35:03 +00:00
// 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 ) =
2021-09-28 20:36:32 +00:00
_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 ) ) ;
2019-12-02 23:35:03 +00:00
2021-09-22 18:59:18 +00:00
2021-09-27 22:33:44 +00:00
// 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 ) =
2021-10-10 01:44:26 +00:00
[ for ( p = region ) if ( ! is_path_simple ( p , closed = true , eps ) ) 1 ] = = [ ] ;
/* &&
2021-09-28 20:36:32 +00:00
[ for ( i = [ 0 : 1 : len ( region ) - 2 ] )
if ( _path_region_intersections ( region [ i ] , list_tail ( region , i + 1 ) , eps = eps ) ! = [ ] ) 1
] = = [ ] ;
2021-10-10 01:44:26 +00:00
* /
2021-09-27 22:33:44 +00:00
2021-09-28 23:08:47 +00:00
// Function: are_regions_equal()
2021-03-14 08:11:15 +00:00
// Usage:
2021-09-28 23:08:47 +00:00
// b = are_regions_equal(region1, region2, [eps])
2021-03-14 08:11:15 +00:00
// Description:
2021-09-21 23:19:02 +00:00
// Returns true if the components of region1 and region2 are the same polygons (in any order)
2021-03-14 08:11:15 +00:00
// within given epsilon tolerance.
// Arguments:
2021-09-21 23:19:02 +00:00
// region1 = first region
// region2 = second region
2021-03-14 08:11:15 +00:00
// eps = tolerance for comparison
2021-09-28 23:08:47 +00:00
function are_regions_equal ( region1 , region2 ) =
2021-03-14 08:11:15 +00:00
assert ( is_region ( region1 ) && is_region ( region2 ) )
len ( region1 ) ! = len ( region2 ) ? false :
2021-09-28 23:08:47 +00:00
__are_regions_equal ( region1 , region2 , 0 ) ;
2021-03-14 08:11:15 +00:00
2021-09-28 23:08:47 +00:00
function __are_regions_equal ( region1 , region2 , i ) =
2021-03-14 08:11:15 +00:00
i >= len ( region1 ) ? true :
2021-09-22 18:59:18 +00:00
! is_polygon_in_list ( region1 [ i ] , region2 ) ? false :
2021-09-28 23:08:47 +00:00
__are_regions_equal ( region1 , region2 , i + 1 ) ;
2021-03-14 08:11:15 +00:00
2021-09-27 22:33:44 +00:00
/// Internal Function: _path_region_intersections()
2021-09-22 18:59:18 +00:00
/// Usage:
2021-09-28 20:36:32 +00:00
/// _path_region_intersections(path, region, [closed], [eps]);
2021-09-22 18:59:18 +00:00
/// Description:
2021-09-27 22:33:44 +00:00
/// 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.)
2021-09-22 18:59:18 +00:00
/// 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)
2021-10-08 01:31:58 +00:00
function old_path_region_intersections ( path , region , closed = true , eps = EPSILON ) =
2020-05-30 02:04:34 +00:00
let (
2021-09-28 20:36:32 +00:00
pathclosed = closed && ! is_closed_path ( path ) ,
pathlen = len ( path ) ,
regionsegs = [ for ( poly = region ) each pair ( poly , is_closed_path ( poly ) ? false : true ) ]
2020-05-30 02:04:34 +00:00
)
2021-09-27 22:33:44 +00:00
sort (
2021-09-28 20:36:32 +00:00
[ for ( si = [ 0 : 1 : len ( path ) - ( pathclosed ? 1 : 2 ) ] )
2021-09-27 22:33:44 +00:00
let (
2021-09-28 20:36:32 +00:00
a1 = path [ si ] ,
a2 = path [ ( si + 1 ) % pathlen ] ,
2021-09-27 22:33:44 +00:00
maxax = max ( a1 . x , a2 . x ) ,
minax = min ( a1 . x , a2 . x ) ,
maxay = max ( a1 . y , a2 . y ) ,
minay = min ( a1 . y , a2 . y )
)
2021-09-28 20:36:32 +00:00
for ( rseg = regionsegs )
2021-09-27 22:33:44 +00:00
let (
2021-09-28 20:36:32 +00:00
b1 = rseg [ 0 ] ,
b2 = rseg [ 1 ] ,
2021-09-27 22:33:44 +00:00
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
2021-09-28 20:36:32 +00:00
: _general_line_intersection ( [ a1 , a2 ] , rseg , eps )
2021-09-27 22:33:44 +00:00
)
if ( isect && isect [ 1 ] >= - eps && isect [ 1 ] < = 1 + eps
&& isect [ 2 ] >= - eps && isect [ 2 ] < = 1 + eps )
[ si , isect [ 1 ] ]
]
) ;
2019-12-02 23:35:03 +00:00
2021-10-08 01:31:58 +00:00
// find the intersection points of a path and the polygons of a
// region; only crossing intersections are caught, no collinear
// intersection is returned.
2021-10-10 01:44:26 +00:00
function _path_region_intersections ( path , region , closed = true , eps = EPSILON , extra = [ ] ) =
2021-10-08 01:31:58 +00:00
let ( path = closed ? close_path ( path , eps = eps ) : path )
_sort_vectors (
2021-10-10 01:44:26 +00:00
[ each extra ,
for ( si = [ 0 : 1 : len ( path ) - 2 ] ) let (
2021-10-08 01:31:58 +00:00
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
2021-10-08 03:20:46 +00:00
// && isect[1]> (si==0 && !closed? -eps: 0)
&& isect [ 1 ] >= - eps
2021-10-08 01:31:58 +00:00
&& isect [ 1 ] < = 1 + eps
2021-10-08 03:20:46 +00:00
// && isect[2]> 0
&& isect [ 2 ] >= - eps
2021-10-08 01:31:58 +00:00
&& isect [ 2 ] < = 1 + eps )
[ si , isect [ 1 ] ]
] ) ;
2021-09-28 20:36:32 +00:00
2020-02-01 05:35:04 +00:00
// Function: split_path_at_region_crossings()
// Usage:
2020-11-17 01:50:08 +00:00
// paths = split_path_at_region_crossings(path, region, [eps]);
2020-02-01 05:35:04 +00:00
// Description:
2020-11-17 01:50:08 +00:00
// Splits a path into sub-paths wherever the path crosses the perimeter of a region.
2020-02-01 05:35:04 +00:00
// 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)];
2020-11-17 01:50:08 +00:00
// paths = split_path_at_region_crossings(path, region);
2020-02-01 05:35:04 +00:00
// color("#aaa") region(region);
2020-11-17 01:50:08 +00:00
// rainbow(paths) stroke($item, closed=false, width=2);
2021-10-10 01:44:26 +00:00
function split_path_at_region_crossings ( path , region , closed = true , eps = EPSILON , extra = [ ] ) =
2020-05-30 02:04:34 +00:00
let (
path = deduplicate ( path , eps = eps ) ,
region = [ for ( path = region ) deduplicate ( path , eps = eps ) ] ,
2021-10-10 01:44:26 +00:00
xings = _path_region_intersections ( path , region , closed = closed , eps = eps , extra = extra ) ,
2020-05-30 02:04:34 +00:00
crossings = deduplicate (
concat ( [ [ 0 , 0 ] ] , xings , [ [ len ( path ) - 1 , 1 ] ] ) ,
eps = eps
) ,
subpaths = [
for ( p = pair ( crossings ) )
2020-09-09 08:37:31 +00:00
deduplicate (
2021-09-18 23:11:08 +00:00
_path_select ( path , p [ 0 ] [ 0 ] , p [ 0 ] [ 1 ] , p [ 1 ] [ 0 ] , p [ 1 ] [ 1 ] , closed = closed ) ,
2020-09-09 08:37:31 +00:00
eps = eps
2020-05-30 02:04:34 +00:00
)
]
)
2021-09-27 22:33:44 +00:00
[ for ( s = subpaths ) if ( len ( s ) > 1 ) s ] ;
2020-02-01 05:35:04 +00:00
2021-10-07 01:16:39 +00:00
// Function: region_parts()
2020-02-29 05:39:58 +00:00
// Usage:
2021-10-07 01:16:39 +00:00
// rgns = region_parts(region);
2020-02-29 05:39:58 +00:00
// Description:
2021-10-07 01:16:39 +00:00
// 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)];
2021-10-07 01:53:46 +00:00
// region_list = region_parts(R);
2021-10-07 01:16:39 +00:00
// 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))];
2021-10-07 01:53:46 +00:00
// region_list = region_parts(R);
2021-10-07 01:16:39 +00:00
// rainbow(region_list) region($item);
2021-10-10 01:44:26 +00:00
function old_region_parts ( region ) =
2020-05-30 02:04:34 +00:00
let (
2021-10-10 01:44:26 +00:00
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 ] ]
2020-05-30 02:04:34 +00:00
] ) ,
2021-10-10 01:44:26 +00:00
2020-05-30 02:04:34 +00:00
outs = [
2021-10-10 01:44:26 +00:00
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 ] )
]
2020-05-30 02:04:34 +00:00
]
2021-10-10 01:44:26 +00:00
)
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 ]
] ) ;
2020-02-29 05:39:58 +00:00
2021-10-10 01:44:26 +00:00
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 ] ] )
]
] ;
2020-02-29 05:39:58 +00:00
2019-12-02 23:35:03 +00:00
2020-03-12 05:26:43 +00:00
// Section: Region Extrusion and VNFs
function _path_path_closest_vertices ( path1 , path2 ) =
2020-05-30 02:04:34 +00:00
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 ] ;
2020-03-12 05:26:43 +00:00
2021-10-03 13:46:39 +00:00
function _join_paths_at_vertices ( path1 , path2 , v1 , v2 ) =
2021-10-01 03:11:01 +00:00
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
2021-10-03 13:46:39 +00:00
// to join internal holes to the outer border. Output is a single path.
2021-10-01 03:11:01 +00:00
function _cleave_connected_region ( region ) =
2020-05-30 02:04:34 +00:00
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 ) ) ,
2021-10-03 13:46:39 +00:00
newoline = _join_paths_at_vertices (
2020-05-30 02:04:34 +00:00
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 ) )
2021-10-03 13:46:39 +00:00
_cleave_connected_region ( orgn ) ;
2020-03-12 05:26:43 +00:00
// 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 ) =
2020-05-30 02:04:34 +00:00
let (
2021-10-07 01:16:39 +00:00
regions = region_parts ( region ) ,
2020-05-30 02:04:34 +00:00
vnfs = [
if ( vnf ! = EMPTY_VNF ) vnf ,
for ( rgn = regions ) let (
2021-10-01 03:11:01 +00:00
cleaved = path3d ( _cleave_connected_region ( rgn ) ) ,
2020-11-17 22:25:08 +00:00
face = is_undef ( transform ) ? cleaved : apply ( transform , cleaved ) ,
2020-05-30 02:04:34 +00:00
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 ;
2020-03-12 05:26:43 +00:00
// Function&Module: linear_sweep()
// Usage:
2020-10-20 05:51:35 +00:00
// linear_sweep(region, height, [center], [slices], [twist], [scale], [style], [convexity]);
2020-03-12 05:26:43 +00:00
// 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:
2020-10-20 05:51:35 +00:00
// region = The 2D [Region](regions.scad) or path that is to be extruded.
// height = The height to extrude the region. Default: 1
2020-03-12 05:26:43 +00:00
// 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
2020-10-20 05:51:35 +00:00
// scale = The amount to scale the shape, from bottom to top. Default: 1
2020-03-12 05:26:43 +00:00
// style = The style to use when triangulating the surface of the object. Valid values are `"default"`, `"alt"`, or `"quincunx"`.
2020-10-20 05:51:35 +00:00
// convexity = Max number of surfaces any single ray could pass through. Module use only.
2020-03-12 05:26:43 +00:00
// anchor_isect = If true, anchoring it performed by finding where the anchor vector intersects the swept shape. Default: false
2020-10-20 05:51:35 +00:00
// 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`
2020-03-12 05:26:43 +00:00
// 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();
2020-07-07 00:07:20 +00:00
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 ;
2020-08-02 05:20:11 +00:00
cp = mean ( pointlist_bounds ( flatten ( region ) ) ) ;
2020-07-07 00:07:20 +00:00
anchor = get_anchor ( anchor , center , "origin" , "origin" ) ;
2020-05-30 02:04:34 +00:00
vnf = linear_sweep (
region , height = height ,
twist = twist , scale = scale ,
slices = slices , maxseg = maxseg ,
style = style
) ;
2020-07-07 00:07:20 +00:00
attachable ( anchor , spin , orient , cp = cp , vnf = vnf , extent = ! anchor_isect ) {
2020-05-30 02:04:34 +00:00
vnf_polyhedron ( vnf , convexity = convexity ) ;
children ( ) ;
}
2020-03-12 05:26:43 +00:00
}
2021-10-07 01:16:39 +00:00
function linear_sweep ( region , height = 1 , center , twist = 0 , scale = 1 , slices ,
maxseg , style = "default" , anchor_isect = false , anchor , spin = 0 , orient = UP ) =
2020-05-30 02:04:34 +00:00
let (
2020-10-20 05:51:35 +00:00
anchor = get_anchor ( anchor , center , BOT , BOT ) ,
2020-05-30 02:04:34 +00:00
region = is_path ( region ) ? [ region ] : region ,
2020-10-20 05:51:35 +00:00
cp = mean ( pointlist_bounds ( flatten ( region ) ) ) ,
2021-10-07 01:16:39 +00:00
regions = region_parts ( region ) ,
2020-05-30 02:04:34 +00:00
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 : [
2021-01-25 07:26:39 +00:00
for ( seg = pair ( p , true ) ) each
2020-05-30 02:04:34 +00:00
let ( steps = ceil ( norm ( seg . y - seg . x ) / maxseg ) )
2021-04-08 03:57:45 +00:00
lerpn ( seg . x , seg . y , steps , false )
2020-05-30 02:04:34 +00:00
]
)
rot ( twist , p = scale ( [ scale , scale ] , p = path ) )
]
2020-10-20 05:51:35 +00:00
] ,
vnf = vnf_merge ( [
for ( rgn = regions )
for ( pathnum = idx ( rgn ) ) let (
p = cleanup_path ( rgn [ pathnum ] ) ,
path = is_undef ( maxseg ) ? p : [
2021-01-25 07:26:39 +00:00
for ( seg = pair ( p , true ) ) each
2020-10-20 05:51:35 +00:00
let ( steps = ceil ( norm ( seg . y - seg . x ) / maxseg ) )
2021-04-08 03:57:45 +00:00
lerpn ( seg . x , seg . y , steps , false )
2020-10-20 05:51:35 +00:00
] ,
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 ) ;
2020-03-12 05:26:43 +00:00
2019-12-02 23:35:03 +00:00
// Section: Offsets and Boolean 2D Geometry
function _offset_chamfer ( center , points , delta ) =
2020-05-30 02:04:34 +00:00
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 ] ) )
] ;
2019-12-02 23:35:03 +00:00
function _shift_segment ( segment , d ) =
2021-09-06 14:48:37 +00:00
assert ( ! approx ( segment [ 0 ] , segment [ 1 ] ) , "Path has repeated points" )
2020-05-30 02:04:34 +00:00
move ( d * line_normal ( segment ) , segment ) ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2021-09-20 22:34:22 +00:00
norm ( s1 [ 1 ] - s2 [ 0 ] ) < 1e-6 ? s1 [ 1 ] : line_intersection ( s1 , s2 , LINE , LINE ) ;
2019-12-02 23:35:03 +00:00
function _makefaces ( direction , startind , good , pointcount , closed ) =
2020-05-30 02:04:34 +00:00
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 ;
2019-12-02 23:35:03 +00:00
function _makefaces_recurse ( startind1 , startind2 , numfirst , numsecond , lenlist , closed , firstind = 0 , secondind = 0 , faces = [ ] ) =
2020-05-30 02:04:34 +00:00
// 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
]
]
)
)
) ;
2019-12-02 23:35:03 +00:00
// Determine which of the shifted segments are good
function _good_segments ( path , d , shiftsegs , closed , quality ) =
2020-05-30 02:04:34 +00:00
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 )
] ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2020-05-30 02:04:34 +00:00
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 ) ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2020-05-30 02:04:34 +00:00
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
] ) ;
2019-12-02 23:35:03 +00:00
2021-10-10 01:44:26 +00:00
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 (
2020-05-30 02:04:34 +00:00
paths , r , delta , chamfer , closed ,
2021-08-31 22:13:15 +00:00
check_valid , quality ,
2020-05-30 02:04:34 +00:00
return_faces , firstface_index ,
flip_faces , _acc = [ ] , _i = 0
2019-12-02 23:35:03 +00:00
) =
2020-05-30 02:04:34 +00:00
_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 ,
2021-08-31 22:13:15 +00:00
check_valid = check_valid , quality = quality ,
2020-05-30 02:04:34 +00:00
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
] )
) : (
difference ( _acc , [
offset (
paths [ _i ] . y ,
2021-01-06 21:45:24 +00:00
r = u_mul ( - 1 , r ) , delta = u_mul ( - 1 , delta ) , chamfer = chamfer , closed = closed ,
2021-08-31 22:13:15 +00:00
check_valid = check_valid , quality = quality ,
2020-05-30 02:04:34 +00:00
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
] )
) ,
r = r , delta = delta , chamfer = chamfer , closed = closed ,
2021-08-31 22:13:15 +00:00
check_valid = check_valid , quality = quality ,
2020-05-30 02:04:34 +00:00
return_faces = return_faces , firstface_index = firstface_index , flip_faces = flip_faces
) ;
2019-12-02 23:35:03 +00:00
// Function: offset()
2021-01-06 21:45:24 +00:00
// 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])
2019-12-02 23:35:03 +00:00
// 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
2021-01-06 21:45:24 +00:00
// 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).
2020-07-27 22:15:34 +00:00
// .
2019-12-02 23:35:03 +00:00
// 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.
2020-07-27 22:15:34 +00:00
// .
2019-12-02 23:35:03 +00:00
// 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.
2021-01-06 21:45:24 +00:00
// ---
2019-12-02 23:35:03 +00:00
// 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);
2021-08-31 22:13:15 +00:00
// stroke(closed=true, offset(star, r=-10, closed=true, $fn=20));
2019-12-02 23:35:03 +00:00
// 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 (
2020-05-30 02:04:34 +00:00
path , r = undef , delta = undef , chamfer = false ,
2021-08-31 22:13:15 +00:00
closed = false , check_valid = true ,
2020-05-30 02:04:34 +00:00
quality = 1 , return_faces = false , firstface_index = 0 ,
flip_faces = false
2021-01-07 00:59:31 +00:00
) =
2021-10-10 01:44:26 +00:00
is_region ( path ) ? _offset_region ( path , r = r , delta = delta , chamfer = chamfer , quality = quality , check_valid = check_valid )
/*
(
2020-05-30 02:04:34 +00:00
assert ( ! return_faces , "return_faces not supported for regions." )
let (
2021-09-11 22:48:23 +00:00
path = [ for ( p = path ) clockwise_polygon ( p ) ] ,
2020-05-30 02:04:34 +00:00
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 ,
2021-08-31 22:13:15 +00:00
check_valid = check_valid , quality = quality ,
2020-05-30 02:04:34 +00:00
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
2021-10-10 01:44:26 +00:00
) * /
: let ( rcount = num_defined ( [ r , delta ] ) )
2020-05-30 02:04:34 +00:00
assert ( rcount = = 1 , "Must define exactly one of 'delta' and 'r'" )
let (
chamfer = is_def ( r ) ? false : chamfer ,
quality = max ( 0 , round ( quality ) ) ,
2021-09-11 22:48:23 +00:00
flip_dir = closed && ! is_polygon_clockwise ( path ) ? - 1 : 1 ,
2020-05-30 02:04:34 +00:00
d = flip_dir * ( is_def ( r ) ? r : delta ) ,
2021-09-20 22:34:22 +00:00
// 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
] ,
2020-05-30 02:04:34 +00:00
// 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 ) ||
2021-09-24 21:33:18 +00:00
all_defined ( closed ? sharpcorners : select ( sharpcorners , 1 , - 2 ) )
2020-05-30 02:04:34 +00:00
)
2021-01-06 21:45:24 +00:00
assert ( parallelcheck , "Path contains sequential parallel segments (either 180 deg turn or 0 deg turn" )
2020-05-30 02:04:34 +00:00
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)
2021-01-06 21:45:24 +00:00
outsidecorner = len ( sharpcorners ) = = 2 ? [ false , false ]
:
[ for ( i = [ 0 : len ( goodsegs ) - 1 ] )
2021-02-20 16:45:10 +00:00
let ( prevseg = select ( goodsegs , i - 1 ) )
2021-09-22 00:20:18 +00:00
( i = = 0 || i = = len ( goodsegs ) - 1 ) && ! closed ? false // In open case first entry is bogus
2021-02-20 16:45:10 +00:00
:
2021-01-06 21:45:24 +00:00
( goodsegs [ i ] [ 1 ] - goodsegs [ i ] [ 0 ] ) * ( goodsegs [ i ] [ 0 ] - sharpcorners [ i ] ) > 0
&& ( prevseg [ 1 ] - prevseg [ 0 ] ) * ( sharpcorners [ i ] - prevseg [ 1 ] ) > 0
] ,
2020-05-30 02:04:34 +00:00
steps = is_def ( delta ) ? [ ] : [
for ( i = [ 0 : len ( goodsegs ) - 1 ] )
2021-08-31 22:13:15 +00:00
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 )
2020-05-30 02:04:34 +00:00
] ,
// 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.
2021-08-31 22:13:15 +00:00
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 ] )
] ,
2020-05-30 02:04:34 +00:00
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 ;
2019-12-02 23:35:03 +00:00
2021-09-28 20:36:32 +00:00
/// 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]
2021-10-10 01:44:26 +00:00
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.
2020-05-30 02:04:34 +00:00
let (
2021-10-10 01:44:26 +00:00
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 )
2021-09-28 20:36:32 +00:00
let (
2021-10-10 01:44:26 +00:00
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
2021-09-28 20:36:32 +00:00
)
rel1 = = rel2 ? [ "S" , subpath ] : [ "U" , subpath ]
2021-10-10 01:44:26 +00:00
] ;
2019-12-02 23:35:03 +00:00
function _tagged_region ( region1 , region2 , keep1 , keep2 , eps = EPSILON ) =
2020-05-30 02:04:34 +00:00
let (
2021-10-10 01:44:26 +00:00
tagged1 = _tag_subpaths ( region1 , region2 , eps = eps ) ,
tagged2 = _tag_subpaths ( region2 , region1 , eps = eps ) ,
2021-09-28 20:36:32 +00:00
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 ) ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2020-05-30 02:04:34 +00:00
b ! = undef ? union ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2021-10-10 01:44:26 +00:00
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
2020-05-30 02:04:34 +00:00
) ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2020-05-30 02:04:34 +00:00
b ! = undef ? difference ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2021-10-07 01:16:39 +00:00
len ( regions ) = = 0 ? [ ] :
len ( regions ) = = 1 ? regions [ 0 ] :
2021-10-10 01:44:26 +00:00
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
2020-05-30 02:04:34 +00:00
) ;
2019-12-02 23:35:03 +00:00
// 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 ) =
2021-10-10 01:44:26 +00:00
// echo(regions=regions)
2021-10-07 01:16:39 +00:00
b ! = undef ? intersection ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps )
: len ( regions ) = = 0 ? [ ]
: len ( regions ) = = 1 ? regions [ 0 ]
2021-10-10 01:44:26 +00:00
: regions [ 0 ] = = [ ] || regions [ 1 ] = = [ ] ? [ ]
2021-10-07 01:16:39 +00:00
: 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
) ;
2019-12-02 23:35:03 +00:00
// 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.
2021-10-07 01:16:39 +00:00
// 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.
2019-12-02 23:35:03 +00:00
// Arguments:
// regions = List of regions to exclusive_or. Each region is a list of closed paths.
2021-10-07 01:16:39 +00:00
// Example(2D): As Function. A linear_sweep of this shape fails to render in CGAL.
2019-12-02 23:35:03 +00:00
// 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));
2021-10-07 01:16:39 +00:00
// Example(2D): As Module. A linear_extrude() of the resulting geometry fails to render in CGAL.
2019-12-02 23:35:03 +00:00
// exclusive_or() {
// square(40,center=false);
// circle(d=40);
// }
function exclusive_or ( regions = [ ] , b = undef , c = undef , eps = EPSILON ) =
2021-09-27 22:33:44 +00:00
b ! = undef ? exclusive_or ( [ regions , b , if ( is_def ( c ) ) c ] , eps = eps ) :
2021-10-10 01:44:26 +00:00
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
2020-05-30 02:04:34 +00:00
) ;
2019-12-02 23:35:03 +00:00
module exclusive_or ( ) {
2020-05-30 02:04:34 +00:00
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." ) ;
}
2019-12-02 23:35:03 +00:00
}
2020-05-30 02:04:34 +00:00
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap