2020-06-03 22:39:30 -04:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
2017-04-18 22:22:29 -04:00
2017-04-18 17:09:33 -04:00
"use strict" ;
2022-02-22 00:05:29 -05:00
// dep: geo.base
2022-10-04 00:25:44 -04:00
// dep: geo.paths
2022-01-31 22:27:48 -05:00
// dep: ext.clip2
2022-02-22 11:32:35 -05:00
// dep: ext.earcut
2022-02-20 14:31:27 -05:00
// dep: geo.point
2022-01-26 21:06:30 -05:00
// dep: geo.bounds
2022-02-22 11:32:35 -05:00
// dep: geo.polygons
2022-02-20 14:31:27 -05:00
gapp . register ( "geo.polygon" , [ ] , ( root , exports ) => {
2022-01-31 20:50:19 -05:00
2022-02-20 14:31:27 -05:00
const { base } = root ;
2022-01-31 20:50:19 -05:00
const { config , util , polygons , newBounds , newPoint } = base ;
2022-02-20 14:31:27 -05:00
2022-01-31 20:50:19 -05:00
const POLY = polygons ,
2023-02-13 14:46:15 -05:00
XAXIS = new THREE . Vector3 ( 1 , 0 , 0 ) ,
2022-01-31 20:50:19 -05:00
DEG2RAD = Math . PI / 180 ,
2024-07-02 22:03:35 -04:00
ClipperLib = self . ClipperLib ,
Clipper = ClipperLib . Clipper ,
ClipType = ClipperLib . ClipType ,
PolyType = ClipperLib . PolyType ,
PolyFillType = ClipperLib . PolyFillType ,
CleanPolygon = Clipper . CleanPolygon ,
FillNonZero = PolyFillType . pftNonZero ,
FillEvenOdd = PolyFillType . pftEvenOdd ,
PathSubject = PolyType . ptSubject ,
PathClip = PolyType . ptClip ,
EndType = ClipperLib . EndType ,
JoinType = ClipperLib . JoinType ,
PolyTree = ClipperLib . PolyTree ,
2024-07-13 16:51:22 -04:00
ClipXOR = ClipType . ctXor ,
2024-07-02 22:03:35 -04:00
ClipDiff = ClipType . ctDifference ,
ClipUnion = ClipType . ctUnion ,
ClipIntersect = ClipType . ctIntersection ,
ClipperOffset = ClipperLib . ClipperOffset
;
2022-01-31 20:50:19 -05:00
let seqid = Math . round ( Math . random ( ) * 0xffffffff ) ;
class Polygon {
constructor ( points ) {
this . id = seqid ++ ; // polygon unique id
this . open = false ;
this . points = [ ] ; // ordered array of points
this . depth = 0 ; // depth nested from top parent (density for support fill)
if ( points ) {
this . addPoints ( points ) ;
2021-06-18 22:14:37 -04:00
}
2022-01-31 20:50:19 -05:00
}
2021-06-18 22:14:37 -04:00
2022-01-31 20:50:19 -05:00
get length ( ) {
return this . points . length ;
}
2021-06-18 22:57:30 -04:00
2022-01-31 20:50:19 -05:00
get deepLength ( ) {
let len = this . length ;
if ( this . inner ) {
for ( let inner of this . inner ) {
len += inner . length ;
2021-06-19 15:17:36 -04:00
}
}
2022-01-31 20:50:19 -05:00
return len ;
}
2021-06-19 15:17:36 -04:00
2022-01-31 20:50:19 -05:00
get bounds ( ) {
if ( this . _bounds ) {
return this . _bounds ;
}
let bounds = this . _bounds = newBounds ( ) ;
for ( let point of this . points ) {
bounds . update ( point ) ;
2021-06-18 22:14:37 -04:00
}
2022-01-31 20:50:19 -05:00
return bounds ;
2017-04-18 17:09:33 -04:00
}
2022-10-04 00:25:44 -04:00
toPath2D ( offset ) {
return base . paths . pointsToPath ( this . points , offset , this . open ) ;
}
toPath3D ( offset , height , z ) {
return base . paths . pathTo3D ( this . toPath2D ( offset ) , height , z ) ;
}
2022-01-31 20:50:19 -05:00
toString ( verbose ) {
2020-04-21 17:38:34 -04:00
let l ;
if ( this . inner && this . inner . length ) {
2020-05-04 20:08:22 -04:00
l = '/' + this . inner . map ( i => i . toString ( verbose ) ) . join ( ',' ) ;
2020-04-21 17:38:34 -04:00
} else {
l = '' ;
}
2020-05-04 20:08:22 -04:00
if ( verbose ) {
return ` P[{ ${ this . area ( ) . toFixed ( 2 ) } }[ ${ this . points . length } ]( ${ this . points . map ( p => ` ${ p . x } , ${ p . y } ` ) . join ( '|' ) } ) ${ l } ] ` ;
} else {
return ` P[ ${ this . points . length , this . area ( ) . toFixed ( 2 ) } ${ l } ] ` ;
}
2022-01-31 20:50:19 -05:00
}
2020-04-21 17:38:34 -04:00
2022-01-31 20:50:19 -05:00
toArray ( ) {
2020-12-24 12:59:57 -05:00
let ov = this . open ? 1 : 0 ;
2022-01-31 20:50:19 -05:00
return this . points . map ( ( p , i ) => i === 0 ? [ ov , p . x , p . y , p . z ] : [ p . x , p . y , p . z ] ) . flat ( ) ;
}
2020-12-24 12:59:57 -05:00
2022-01-31 20:50:19 -05:00
fromArray ( array ) {
2020-12-24 12:59:57 -05:00
this . open = array [ 0 ] === 1 ;
2022-01-31 20:50:19 -05:00
for ( let i = 1 ; i < array . length ; ) {
2020-12-24 12:59:57 -05:00
this . add ( array [ i ++ ] , array [ i ++ ] , array [ i ++ ] ) ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2020-12-24 12:59:57 -05:00
2022-01-31 20:50:19 -05:00
matches ( poly ) {
2020-12-24 12:59:57 -05:00
let tarr = Array . isArray ( poly ) ? poly : poly . toArray ( ) ;
let parr = this . toArray ( ) ;
if ( tarr . length === parr . length ) {
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < tarr . length ; i ++ ) {
2020-12-24 12:59:57 -05:00
if ( Math . abs ( tarr [ i ] - parr [ i ] ) > 0.0001 ) return false ;
}
return true ;
}
return false ;
2022-01-31 20:50:19 -05:00
}
2020-12-24 12:59:57 -05:00
2022-01-31 20:50:19 -05:00
xray ( deep ) {
2020-11-10 19:56:34 -05:00
const xray = {
id : this . id ,
len : this . points . length ,
open : this . open ,
depth : this . depth ,
parent : this . parent ? true : false
} ;
if ( this . inner ) {
xray . inner = deep ? this . inner . xray ( deep ) : this . inner ;
}
return xray ;
2022-01-31 20:50:19 -05:00
}
2020-11-10 19:56:34 -05:00
2020-03-21 22:25:31 -04:00
// return which plane (x,y,z) this polygon is coplanar with
2022-01-31 20:50:19 -05:00
alignment ( ) {
2020-03-21 22:25:31 -04:00
if ( this . _aligned ) return this . _aligned ;
2022-01-31 20:50:19 -05:00
let diff = {
x : false ,
y : false ,
z : false
} ;
2020-03-21 22:25:31 -04:00
let last = undefined ;
2020-03-21 19:13:20 -04:00
// flatten points into array for earcut()
this . points . forEach ( p => {
if ( last ) {
diff . x = diff . x || last . x !== p . x ;
diff . y = diff . y || last . y !== p . y ;
diff . z = diff . z || last . z !== p . z ;
}
last = p ;
} ) ;
2020-03-21 22:25:31 -04:00
return this . _aligned =
diff . x === false ? 'yz' :
diff . y === false ? 'xz' : 'xy' ;
2022-01-31 20:50:19 -05:00
}
2020-03-21 19:13:20 -04:00
2020-03-21 22:25:31 -04:00
// ensure alignment with XY plane. mark if axes are swapped.
2022-01-31 20:50:19 -05:00
ensureXY ( ) {
2020-03-21 22:25:31 -04:00
if ( this . _swapped ) return this ;
switch ( this . alignment ( ) ) {
2022-01-31 20:50:19 -05:00
case 'xy' :
break ;
case 'yz' :
this . swap ( true , false ) . _swapped = true ;
break ;
case 'xz' :
this . swap ( false , true ) . _swapped = true ;
break ;
default :
throw ` invalid alignment ` ;
2020-03-21 19:13:20 -04:00
}
2020-03-21 22:25:31 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2020-03-21 22:25:31 -04:00
// restore to original planar alignment if swapped
2022-01-31 20:50:19 -05:00
restoreXY ( ) {
2020-03-21 22:25:31 -04:00
if ( ! this . _swapped ) return this ;
switch ( this . alignment ( ) ) {
2022-01-31 20:50:19 -05:00
case 'xy' :
break ;
case 'yz' :
this . swap ( true , false ) . _swapped = false ;
break ;
case 'xz' :
this . swap ( false , true ) . _swapped = false ;
break ;
2020-03-21 22:25:31 -04:00
}
return this ;
2022-01-31 20:50:19 -05:00
}
2020-03-21 22:25:31 -04:00
2022-01-31 20:50:19 -05:00
earcut ( ) {
2020-03-21 22:25:31 -04:00
// gather all points into a single array including inner polys
// keeping track of array offset indices for inners
let out = [ ] ;
let holes = [ ] ;
// flatten points into array for earcut()
this . points . forEach ( p => {
out . push ( p . x , p . y , p . z ) ;
} ) ;
2020-03-21 19:13:20 -04:00
// add hole offsets for inner polygons
if ( this . inner ) {
this . inner . forEach ( p => {
holes . push ( out . length / 3 ) ;
p . points . forEach ( p => {
out . push ( p . x , p . y , p . z ) ;
} )
} ) ;
}
2020-03-21 22:25:31 -04:00
// perform earcut()
2022-01-31 20:50:19 -05:00
let cut = self . earcut ( out , holes , 3 ) ;
2020-03-21 19:13:20 -04:00
let ret = [ ] ;
2020-03-21 22:25:31 -04:00
// preserve swaps in new polys
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < cut . length ; i += 3 ) {
2020-03-21 22:25:31 -04:00
let p = new Polygon ( ) ;
p . _aligned = this . _aligned ;
p . _swapped = this . _swapped ;
2022-01-31 20:50:19 -05:00
for ( let j = 0 ; j < 3 ; j ++ ) {
2020-03-21 22:25:31 -04:00
let n = cut [ i + j ] * 3 ;
2022-01-31 20:50:19 -05:00
p . add ( out [ n ] , out [ n + 1 ] , out [ n + 2 ] ) ;
2020-03-21 19:13:20 -04:00
}
2020-03-21 22:25:31 -04:00
ret . push ( p ) ;
2020-03-21 19:13:20 -04:00
}
return ret ;
2020-11-11 14:40:15 -05:00
}
2021-02-20 22:50:03 -05:00
// generate center crossing point cloud
2022-01-31 20:50:19 -05:00
centers ( step , z , min , max , opt = { } ) {
2021-02-20 22:50:03 -05:00
let cloud = [ ] ,
bounds = this . bounds ,
2021-02-21 00:17:21 -05:00
lines = opt . lines || false ,
stepoff = step / 2 ,
set = [ this . points ] ;
2021-02-20 22:50:03 -05:00
2021-02-21 00:17:21 -05:00
if ( this . inner ) {
for ( let inner of this . inner ) {
set . push ( inner . points ) ;
}
}
2022-01-31 20:50:19 -05:00
for ( let y of util . lerp ( bounds . miny + stepoff , bounds . maxy - stepoff , step , true ) ) {
2021-02-20 22:50:03 -05:00
let ints = [ ] ;
2021-02-21 00:17:21 -05:00
for ( let points of set ) {
let length = points . length ;
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < length ; i ++ ) {
2021-02-21 00:17:21 -05:00
let p1 = points [ i % length ] ;
2022-01-31 20:50:19 -05:00
let p2 = points [ ( i + 1 ) % length ] ;
2021-02-21 00:17:21 -05:00
if (
( p1 . y <= y && p2 . y > y ) ||
( p1 . y > y && p2 . y <= y )
2022-01-31 20:50:19 -05:00
) ints . push ( [ p1 , p2 ] ) ;
2021-02-21 00:17:21 -05:00
}
2021-02-20 22:50:03 -05:00
}
let cntr = [ ] ;
if ( ints . length && ints . length % 2 === 0 ) {
for ( let int of ints ) {
2022-01-31 20:50:19 -05:00
let [ p1 , p2 ] = int ;
2021-02-20 22:50:03 -05:00
if ( p2 . y < p1 . y ) {
let tp = p1 ;
p1 = p2 ;
p2 = tp ;
}
let minx = Math . min ( p1 . x , p2 . x ) ;
let maxx = Math . max ( p1 . x , p2 . x ) ;
let miny = Math . min ( p1 . y , p2 . y ) ;
let maxy = Math . max ( p1 . y , p2 . y ) ;
let dx = p2 . x - p1 . x ;
let dy = maxy - miny ;
let pct = ( y - miny ) / dy ;
let xpo = p1 . x + pct * dx ;
cntr . push ( xpo ) ;
}
}
cntr . sort ( ( a , b ) => {
return b - a ;
} ) ;
let lp , eo = 0 ;
for ( let x of cntr ) {
2021-06-18 22:14:37 -04:00
let p = newPoint ( x , y , z ) ;
2021-02-20 22:50:03 -05:00
if ( eo ++ % 2 ) {
let d = lp . distTo2D ( p ) ;
if ( d >= min && d <= max ) {
if ( lines ) {
cloud . push ( lp ) ;
cloud . push ( p ) ;
} else {
2021-06-18 22:14:37 -04:00
cloud . push ( newPoint (
2021-02-20 22:50:03 -05:00
( lp . x + p . x ) / 2 , y , z
) ) ;
}
}
} else {
lp = p ;
}
}
}
2022-01-31 20:50:19 -05:00
for ( let x of util . lerp ( bounds . minx + stepoff , bounds . maxx - stepoff , step , true ) ) {
2021-02-20 22:50:03 -05:00
let ints = [ ] ;
2021-02-21 00:17:21 -05:00
for ( let points of set ) {
let length = points . length ;
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < length ; i ++ ) {
2021-02-21 00:17:21 -05:00
let p1 = points [ i % length ] ;
2022-01-31 20:50:19 -05:00
let p2 = points [ ( i + 1 ) % length ] ;
2021-02-21 00:17:21 -05:00
if (
( p1 . x <= x && p2 . x > x ) ||
( p1 . x > x && p2 . x <= x )
2022-01-31 20:50:19 -05:00
) ints . push ( [ p1 , p2 ] ) ;
2021-02-21 00:17:21 -05:00
}
2021-02-20 22:50:03 -05:00
}
let cntr = [ ] ;
if ( ints . length && ints . length % 2 === 0 ) {
for ( let int of ints ) {
2022-01-31 20:50:19 -05:00
let [ p1 , p2 ] = int ;
2021-02-20 22:50:03 -05:00
if ( p2 . x < p1 . x ) {
let tp = p1 ;
p1 = p2 ;
p2 = tp ;
}
let minx = Math . min ( p1 . x , p2 . x ) ;
let maxx = Math . max ( p1 . x , p2 . x ) ;
let miny = Math . min ( p1 . y , p2 . y ) ;
let maxy = Math . max ( p1 . y , p2 . y ) ;
let dx = maxx - minx ;
let dy = p2 . y - p1 . y ;
let pct = ( x - minx ) / dx ;
let ypo = p1 . y + pct * dy ;
cntr . push ( ypo ) ;
}
}
cntr . sort ( ( a , b ) => {
return b - a ;
} ) ;
let lp , eo = 0 ;
for ( let y of cntr ) {
2021-06-18 22:14:37 -04:00
let p = newPoint ( x , y , z ) ;
2021-02-20 22:50:03 -05:00
if ( eo ++ % 2 ) {
let d = lp . distTo2D ( p ) ;
if ( d >= min && d <= max ) {
if ( lines ) {
cloud . push ( lp ) ;
cloud . push ( p ) ;
} else {
2021-06-18 22:14:37 -04:00
cloud . push ( newPoint (
2021-02-20 22:50:03 -05:00
x , ( lp . y + p . y ) / 2 , z
) ) ;
}
}
} else {
lp = p ;
}
}
}
if ( lines ) {
return cloud ;
}
let mindist = opt . mindist || step * 1.5 ;
function build ( poly ) {
let lastp = poly . last ( ) ;
let minp ;
let mind = Infinity ;
for ( let point of cloud ) {
let dist = point . distTo2D ( lastp ) ;
if ( dist < mindist && dist < mind ) {
mind = dist ;
minp = point ;
}
}
if ( minp ) {
cloud = cloud . filter ( p => p !== minp ) ;
poly . push ( minp ) ;
return true ;
}
return false ;
}
// join points into polys
let polys = [ ] ;
let poly = [ ] ;
while ( cloud . length ) {
if ( poly . length === 0 ) {
2022-01-31 20:50:19 -05:00
poly = [ cloud . shift ( ) ] ;
2021-02-20 22:50:03 -05:00
polys . push ( poly ) ;
continue ;
}
if ( build ( poly ) ) {
continue ;
}
if ( ! poly . flip ) {
poly . reverse ( ) ;
poly . flip = true ;
continue ;
}
if ( poly . length ) {
poly = [ ] ;
} else {
throw "whoop there it is" ;
}
}
return polys
. filter ( poly => poly . length > 1 )
. map ( poly => {
2022-01-31 20:50:19 -05:00
let np = base . newPolygon ( ) . setOpen ( ) ;
2021-02-20 22:50:03 -05:00
for ( let p of poly ) {
np . push ( p ) ;
}
2021-02-22 19:32:54 -05:00
if ( np . last ( ) . distTo2D ( np . first ( ) ) <= max ) {
2021-02-22 22:37:38 -05:00
np . setClosed ( ) ;
2021-02-22 19:32:54 -05:00
}
2021-02-22 22:37:38 -05:00
np = np . clean ( ) ;
2021-02-20 22:50:03 -05:00
return np ;
} ) ;
2022-01-31 20:50:19 -05:00
}
2021-02-20 22:50:03 -05:00
2022-01-31 20:50:19 -05:00
debur ( dist ) {
2020-11-11 17:13:54 -05:00
if ( this . len < 2 ) {
return null ;
}
const pa = this . points ,
pln = pa . length ,
open = this . open ,
2021-07-10 21:19:56 -04:00
newp = newPolygon ( ) . copyZ ( this . z ) ,
2022-01-31 20:50:19 -05:00
min = dist || base . config . precision _merge ;
2020-11-11 17:13:54 -05:00
let lo ;
newp . push ( lo = pa [ 0 ] ) ;
2022-01-31 20:50:19 -05:00
for ( let i = 1 ; i < pln ; i ++ ) {
2020-11-11 17:13:54 -05:00
if ( lo . distTo2D ( pa [ i ] ) >= min ) {
newp . push ( lo = pa [ i ] ) ;
}
}
newp . open = open ;
newp . parent = this . parent ;
if ( newp . length < 2 ) {
return null ;
}
return newp ;
2022-01-31 20:50:19 -05:00
}
2020-11-11 17:13:54 -05:00
2022-01-31 20:50:19 -05:00
miter ( debug ) {
2020-11-11 14:40:15 -05:00
if ( this . length < 3 ) return this ;
2022-01-31 20:50:19 -05:00
const slo = [ ] ,
pa = this . points ,
pln = pa . length ,
open = this . open ;
2020-11-11 14:40:15 -05:00
let last ;
2022-01-31 20:50:19 -05:00
for ( let i = 1 ; i < pln ; i ++ ) {
slo . push ( pa [ i - 1 ] . slopeTo ( last = pa [ i ] ) ) ;
2020-11-11 14:40:15 -05:00
}
if ( ! open ) {
slo . push ( last . slopeTo ( pa [ 0 ] ) ) ;
}
const ang = new Array ( pln ) . fill ( 0 ) ;
let redo = false ;
const aln = open ? pln - 1 : pln ;
2022-01-31 20:50:19 -05:00
for ( let i = 1 ; i < aln ; i ++ ) {
ang [ i ] = slopeDiff ( slo [ i - 1 ] , slo [ i ] ) ;
2020-11-11 18:19:57 -05:00
redo |= ang [ i ] > 90 ;
2020-11-11 14:40:15 -05:00
}
if ( ! open ) {
// ang[pln-1] = slopeDiff(slo[pln-2], slo[pln-1]);
2022-01-31 20:50:19 -05:00
ang [ 0 ] = slopeDiff ( slo [ pln - 1 ] , slo [ 0 ] ) ;
redo |= ang [ pln - 1 ] > 90 ;
2020-11-11 14:40:15 -05:00
redo |= ang [ 0 ] > 90 ;
}
if ( redo ) {
2021-07-10 21:19:56 -04:00
const newp = newPolygon ( ) . copyZ ( this . z ) ;
2020-11-11 14:40:15 -05:00
// newp.debug = this.debug = true;
newp . open = open ;
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < pln ; i ++ ) {
const p = pa [ ( i + pln ) % pln ] ;
const d = ang [ ( i + pln ) % pln ] ;
2020-12-19 23:24:03 -05:00
if ( d > 179 ) {
2022-01-31 20:50:19 -05:00
const s = slo [ ( i + pln ) % pln ] ;
const pp = pa [ ( i + pln - 1 ) % pln ] ;
const ps = slo [ ( i + pln - 1 ) % pln ] ;
2020-12-19 23:24:03 -05:00
newp . push ( p . follow ( p . slopeTo ( pp ) . normal ( ) , 0.001 ) ) ;
newp . push ( p . follow ( s . clone ( ) . normal ( ) . invert ( ) , 0.001 ) ) ;
} else if ( d > 90 ) {
2022-01-31 20:50:19 -05:00
const s = slo [ ( i + pln ) % pln ] ;
const pp = pa [ ( i + pln - 1 ) % pln ] ;
const ps = slo [ ( i + pln - 1 ) % pln ] ;
2020-11-11 18:19:57 -05:00
newp . push ( p . follow ( p . slopeTo ( pp ) , 0.001 ) ) ;
newp . push ( p . follow ( s , 0.001 ) ) ;
2020-11-11 14:40:15 -05:00
} else {
p . parent = newp ;
newp . push ( p ) ;
}
}
return newp ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2020-11-11 08:07:02 -05:00
2022-01-31 20:50:19 -05:00
createConvexHull ( points ) {
2017-04-18 17:09:33 -04:00
function removeMiddle ( a , b , c ) {
2020-06-03 22:39:30 -04:00
let cross = ( a . x - b . x ) * ( c . y - b . y ) - ( a . y - b . y ) * ( c . x - b . x ) ;
let dot = ( a . x - b . x ) * ( c . x - b . x ) + ( a . y - b . y ) * ( c . y - b . y ) ;
2017-04-18 17:09:33 -04:00
return cross < 0 || cross == 0 && dot <= 0 ;
}
2022-01-31 20:50:19 -05:00
points . sort ( function ( a , b ) {
2017-04-18 17:09:33 -04:00
return a . x != b . x ? a . x - b . x : a . y - b . y ;
} ) ;
2020-06-03 22:39:30 -04:00
let n = points . length ;
let hull = [ ] ;
2017-04-18 17:09:33 -04:00
2020-06-03 22:39:30 -04:00
for ( let i = 0 ; i < 2 * n ; i ++ ) {
let j = i < n ? i : 2 * n - 1 - i ;
2017-04-18 17:09:33 -04:00
while ( hull . length >= 2 && removeMiddle ( hull [ hull . length - 2 ] , hull [ hull . length - 1 ] , points [ j ] ) )
hull . pop ( ) ;
hull . push ( points [ j ] ) ;
}
hull . pop ( ) ;
this . addPoints ( hull ) ;
2022-01-31 20:50:19 -05:00
2017-04-18 17:09:33 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
stepsFromRoot ( ) {
let p = this . parent ,
steps = 0 ;
2017-04-18 17:09:33 -04:00
while ( p ) {
if ( p . inner && p . inner . length > 1 ) steps ++ ;
p = p . parent ;
}
return steps ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
first ( ) {
2017-04-18 17:09:33 -04:00
return this . points [ 0 ] ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
last ( ) {
return this . points [ this . length - 1 ] ;
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
swap ( x , y ) {
2021-06-18 22:14:37 -04:00
this . _bounds = undefined ;
2020-11-05 14:47:18 -05:00
if ( x ) {
2021-06-18 22:14:37 -04:00
for ( let p of this . points ) {
2020-11-05 14:47:18 -05:00
p . swapXZ ( ) ;
}
} else if ( y ) {
2021-06-18 22:14:37 -04:00
for ( let p of this . points ) {
2020-11-05 14:47:18 -05:00
p . swapYZ ( ) ;
}
2017-04-18 17:09:33 -04:00
}
2021-06-18 22:14:37 -04:00
if ( this . inner ) {
for ( let inner of this . inner ) {
2022-01-31 20:50:19 -05:00
inner . swap ( x , y ) ;
2021-06-18 22:14:37 -04:00
}
}
2017-04-18 17:09:33 -04:00
return this ;
}
2020-04-28 09:51:25 -04:00
// return average of all point positions
2022-01-31 20:50:19 -05:00
average ( ) {
let ap = newPoint ( 0 , 0 , 0 , null ) ;
2020-04-28 09:51:25 -04:00
this . points . forEach ( p => {
ap . x += p . x ;
ap . y += p . y ;
ap . z += p . z ;
} ) ;
ap . x /= this . points . length ;
ap . y /= this . points . length ;
ap . z /= this . points . length ;
return ap ;
2022-01-31 20:50:19 -05:00
}
2020-04-28 09:51:25 -04:00
2017-04-18 17:09:33 -04:00
/ * *
* @ param { boolean } [ point ] return just the center point
* @ returns { Polygon | Point } a new polygon centered on x = 0 , y = 0 , z = 0
* /
2022-01-31 20:50:19 -05:00
center ( point ) {
let ap = newPoint ( 0 , 0 , 0 , null ) ,
np = newPolygon ( ) ,
pa = this . points ;
2017-04-18 17:09:33 -04:00
pa . forEach ( function ( p ) {
ap . x += p . x ;
ap . y += p . y ;
ap . z += p . z ;
} ) ;
ap . x /= pa . length ;
ap . y /= pa . length ;
ap . z /= pa . length ;
if ( point ) return ap ;
pa . forEach ( function ( p ) {
np . push ( newPoint (
p . x - ap . x ,
p . y - ap . y ,
p . z - ap . z
) ) ;
} ) ;
return np ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
2024-07-02 13:28:13 -04:00
* using perimeter length , find 3 equally spaced points to
* return a more accruate center point .
2017-04-18 17:09:33 -04:00
* @ returns { Point } center of a polygon assuming it ' s a circle
* /
2024-07-02 13:28:13 -04:00
calcCircleCenter ( ) {
2023-02-26 15:49:24 -08:00
let pe = this . perimeter ( ) ,
2024-07-02 13:28:13 -04:00
pe1 = pe / 3 , // point 2
pe2 = pe1 * 2 , // point 3
2023-02-26 15:49:24 -08:00
p = this . points ,
l = p . length ,
i = 1 ,
td = 0 ,
2024-07-02 13:28:13 -04:00
lp = p [ 0 ] , // first point
ap = [ lp ] ; // array of 3 points
2023-02-26 15:49:24 -08:00
while ( i < l ) {
let np = p [ i ++ ] ;
let d = lp . distTo2D ( np ) ;
td += d ;
if ( ap . length === 1 && td >= pe1 ) {
ap . push ( np ) ;
} else if ( ap . length === 2 && td >= pe2 ) {
ap . push ( np ) ;
break ;
}
lp = np ;
2020-12-17 13:39:33 -05:00
}
2023-02-26 15:49:24 -08:00
let center = util . center2d ( ... ap ) ;
return newPoint ( center . x , center . y , p [ 0 ] . z , null ) ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
2024-07-02 13:28:13 -04:00
* iterate over poly points and find sequences of 5 or more points
* with a common center point and collect as a midpoint / radius
* /
findArcCenters ( opt = { } ) {
if ( this . length < 6 ) return [ ] ;
let tolerance = opt . tolerance || 1e-2 ,
inside = opt . inside ? ? true ,
seq = this . points . slice ( ) ,
util = base . util ;
if ( this . isClosed ( ) ) seq . appendAll ( seq . slice ( 0 , 5 ) ) ;
let recs = [ ] , // accumulated arc points
cand = [ ] ; // candidate center array
for ( let pos = 3 ; pos < seq . length ; pos ++ ) {
let next = util . circleCenter ( ... seq . slice ( pos - 3 , pos ) ) ;
let prev = cand . peek ( ) ;
if ( inside && ! newPoint ( next . x , next . y ) . inPolygon ( this ) ) {
// require point be inside current polygon
next = null ;
}
// console.log({ pos, next, prev, cand });
if ( next && ! prev ) {
// seed candidate list
cand . push ( next ) ;
continue ;
}
if ( ! next ) {
// next is a bust, reset candidate list
cand . length = 0 ;
continue ;
}
// const dist = util.dist2D(next,prev);
// console.log({ dist: dist.round(6) });
if ( util . dist2D ( next , prev ) < tolerance ) {
// add new candidate
cand . push ( next ) ;
continue ;
} else if ( cand . length >= 3 ) {
// emit record on 5 points
recs . push ( cand . peek ( ) ) ;
}
// reset candidate array
cand = [ next ] ;
}
if ( cand . length >= 3 ) {
recs . push ( cand . peek ( ) ) ;
}
// filter dups
recs . sort ( ( a , b ) => {
return util . dist2D ( a , b ) ;
} ) ;
recs = recs . filter ( ( r , i ) => {
if ( i > 0 ) {
return util . dist2D ( r , recs [ i - 1 ] ) < tolerance * 10 ? null : r ;
} else {
return r ;
}
} ) ;
return recs ;
}
/ * *
2017-04-18 17:09:33 -04:00
* add points forming a rectangle around a center point
*
* @ param { Point } center
* @ param { number } width
* @ param { number } height
* /
2022-01-31 20:50:19 -05:00
centerRectangle ( center , width , height ) {
2017-04-18 17:09:33 -04:00
width /= 2 ;
height /= 2 ;
this . push ( newPoint ( center . x - width , center . y - height , center . z ) ) ;
this . push ( newPoint ( center . x + width , center . y - height , center . z ) ) ;
this . push ( newPoint ( center . x + width , center . y + height , center . z ) ) ;
this . push ( newPoint ( center . x - width , center . y + height , center . z ) ) ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2020-04-10 21:26:07 -04:00
/ * *
* create square spiral ( used for purge blocks )
* /
2022-01-31 20:50:19 -05:00
centerSpiral ( center , lenx , leny , offset , count ) {
2020-04-10 21:26:07 -04:00
count *= 4 ;
offset /= 2 ;
2022-01-31 20:50:19 -05:00
let pos = {
x : center . x - lenx / 2 ,
y : center . y + leny / 2 ,
z : center . z
} ,
dir = {
x : 1 ,
y : 0 ,
i : 0
} ,
t ;
2020-04-10 21:26:07 -04:00
while ( count -- > 0 ) {
this . push ( newPoint ( pos . x , pos . y , pos . z ) ) ;
pos . x += dir . x * lenx ;
pos . y += dir . y * leny ;
switch ( dir . i ++ ) {
2022-01-31 20:50:19 -05:00
case 0 :
t = dir . x ;
dir . x = dir . y ;
dir . y = - t ;
break ;
case 1 :
t = dir . x ;
dir . x = dir . y ;
dir . y = t ;
break ;
case 2 :
t = dir . x ;
dir . x = dir . y ;
dir . y = - t ;
break ;
case 3 :
t = dir . x ;
dir . x = dir . y ;
dir . y = t ;
break ;
2020-04-10 21:26:07 -04:00
}
2022-01-31 20:50:19 -05:00
lenx -= offset / 2 ;
leny -= offset / 2 ;
2020-04-10 21:26:07 -04:00
dir . i = dir . i % 4 ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2020-04-10 21:26:07 -04:00
2017-04-18 17:09:33 -04:00
/ * *
* add points forming a circle around a center point
* /
2022-01-31 20:50:19 -05:00
centerCircle ( center , radius , points , clockwise ) {
let angle = 0 ,
add = 360 / points ;
2017-04-18 17:09:33 -04:00
if ( clockwise ) add = - add ;
while ( points -- > 0 ) {
this . push ( newPoint (
2022-01-31 20:50:19 -05:00
util . round ( Math . cos ( angle * DEG2RAD ) * radius , 7 ) + center . x ,
util . round ( Math . sin ( angle * DEG2RAD ) * radius , 7 ) + center . y ,
2017-04-18 17:09:33 -04:00
center . z
) ) ;
angle += add ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
2021-06-18 22:14:37 -04:00
* move all poly points by some offset
2017-04-18 17:09:33 -04:00
* /
2024-06-13 19:00:04 -04:00
move ( offset , skipinner ) {
2021-06-18 22:14:37 -04:00
this . _bounds = undefined ;
this . points = this . points . map ( point => point . move ( offset ) ) ;
2024-06-13 19:00:04 -04:00
if ( ! skipinner && this . inner ) {
2021-06-18 22:14:37 -04:00
for ( let inner of this . inner ) {
inner . move ( offset ) ;
}
2020-11-05 14:47:18 -05:00
}
2021-06-20 18:38:31 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2021-06-18 22:14:37 -04:00
/ * *
* scale polygon around origin
* /
2022-01-31 20:50:19 -05:00
scale ( scale , round ) {
let x , y , z ;
2021-11-16 22:07:49 -05:00
if ( typeof ( scale ) === 'number' ) {
x = y = z = scale ;
} else {
x = scale . x ;
y = scale . y ;
z = scale . z ;
}
2021-06-18 22:14:37 -04:00
this . _bounds = undefined ;
2021-11-16 22:07:49 -05:00
this . points . forEach ( point => {
2017-04-18 17:09:33 -04:00
if ( round ) {
2021-11-16 22:07:49 -05:00
point . x = ( point . x * x ) . round ( round ) ;
point . y = ( point . y * y ) . round ( round ) ;
point . z = ( point . z * z ) . round ( round ) ;
2021-06-18 22:14:37 -04:00
} else {
2021-11-16 22:07:49 -05:00
point . x = point . x * x ;
point . y = point . y * y ;
point . z = point . z * z ;
2017-04-18 17:09:33 -04:00
}
} ) ;
2021-06-18 22:14:37 -04:00
if ( this . inner ) {
for ( let inner of this . inner ) {
inner . scale ( scale , round ) ;
}
}
2021-06-20 18:38:31 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
2017-11-09 11:40:24 -05:00
* hint fill angle hinting from longest segment
2017-04-18 17:09:33 -04:00
* /
2022-01-31 20:50:19 -05:00
hintFillAngle ( ) {
2020-06-03 22:39:30 -04:00
let index = 0 ,
2017-04-18 17:09:33 -04:00
points = this . points ,
length = points . length ,
prev ,
next ,
dist2 ,
longest ,
2022-01-31 20:50:19 -05:00
mincir = config . hint _min _circ ,
minlen = config . hint _len _min ,
maxlen = config . hint _len _max || Infinity ;
2017-04-18 17:09:33 -04:00
while ( index < length ) {
prev = points [ index ] ;
next = points [ ++ index % length ] ;
dist2 = prev . distToSq2D ( next ) ;
if ( dist2 >= minlen && dist2 <= maxlen && ( ! longest || dist2 > longest . len ) ) {
2022-01-31 20:50:19 -05:00
longest = {
p1 : prev ,
p2 : next ,
len : dist2
} ;
2017-04-18 17:09:33 -04:00
}
}
if ( longest && this . circularity ( ) >= mincir ) {
this . fillang = longest . p1 . slopeTo ( longest . p2 ) . normal ( ) ;
}
return this . fillang ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* todo make more efficient
*
* @ param { Boolean } deep
* @ returns { Polygon }
* /
2022-01-31 20:50:19 -05:00
clone ( deep ) {
2021-07-10 21:19:56 -04:00
let np = newPolygon ( ) . copyZ ( this . z ) ,
2017-04-18 17:09:33 -04:00
ln = this . length ,
i = 0 ;
while ( i < ln ) np . push ( this . points [ i ++ ] ) ;
2021-06-18 22:14:37 -04:00
if ( this . fillang ) np . fillang = this . fillang ;
2017-04-18 17:09:33 -04:00
np . depth = this . depth ;
np . open = this . open ;
if ( deep && this . inner ) {
np . inner = this . inner . clone ( ) ;
}
return np ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2021-07-10 21:19:56 -04:00
// special shallow for-render-or-read-only cloning
2022-01-31 20:50:19 -05:00
cloneZ ( z , stop ) {
2021-07-10 21:19:56 -04:00
let p = newPolygon ( ) ;
p . z = z ;
p . open = this . open ;
p . points = this . points ;
if ( this . inner ) {
p . inner = this . inner . map ( p => p . cloneZ ( z , true ) ) ;
}
return p ;
2022-01-31 20:50:19 -05:00
}
2021-07-10 21:19:56 -04:00
2022-01-31 20:50:19 -05:00
copyZ ( z ) {
2021-07-10 21:19:56 -04:00
if ( z !== undefined ) {
this . z = z ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2021-07-10 21:19:56 -04:00
2023-01-17 22:42:24 -05:00
setA ( a ) {
for ( let p of this . points ) {
p . setA ( a ) ;
}
if ( this . inner ) {
for ( let inner of this . inner ) {
inner . setA ( a ) ;
}
}
return this ;
}
2017-04-18 17:09:33 -04:00
/ * *
* set all points ' z value
*
* @ param { number } z
* @ returns { Polygon } this
* /
2022-01-31 20:50:19 -05:00
setZ ( z ) {
2020-06-03 22:39:30 -04:00
let ar = this . points ,
2017-04-18 17:09:33 -04:00
ln = ar . length ,
i = 0 ;
while ( i < ln ) ar [ i ++ ] . z = z ;
2022-01-31 20:50:19 -05:00
if ( this . inner ) this . inner . forEach ( function ( c ) {
c . setZ ( z )
} ) ;
2017-04-18 17:09:33 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ returns { number } z value of first point
* /
2022-01-31 20:50:19 -05:00
getZ ( i ) {
2021-07-10 21:19:56 -04:00
return this . z !== undefined ? this . z : this . points [ i || 0 ] . z ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* /
2022-01-31 20:50:19 -05:00
render ( layer , color , recursive , open ) {
2017-04-18 17:09:33 -04:00
layer . poly ( this , color , recursive , open ) ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
renderSolid ( layer , color ) {
2020-04-22 00:24:06 -04:00
layer . solid ( this , color ) ;
2022-01-31 20:50:19 -05:00
}
2020-04-22 00:24:06 -04:00
2017-04-18 17:09:33 -04:00
/ * *
* add new point and return polygon reference for chaining
* /
2022-01-31 20:50:19 -05:00
add ( x , y , z ) {
this . push ( newPoint ( x , y , z ) ) ;
2017-04-18 17:09:33 -04:00
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
addObj ( obj ) {
2021-02-04 21:35:42 -05:00
if ( Array . isArray ( obj ) ) {
for ( let o of obj ) {
this . addObj ( o ) ;
}
return this ;
}
return this . add ( obj . x , obj . y , obj . z ) ;
}
2017-04-18 17:09:33 -04:00
/ * *
* append array of points to polygon and return polygon
* /
2022-01-31 20:50:19 -05:00
addPoints ( points ) {
2020-06-03 22:39:30 -04:00
let poly = this ,
2017-04-18 17:09:33 -04:00
length = points . length ,
i = 0 ;
while ( i < length ) {
poly . push ( points [ i ++ ] ) ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* append point to polygon and return point
* /
2022-01-31 20:50:19 -05:00
push ( p ) {
2017-04-18 17:09:33 -04:00
// clone any point belonging to another polygon
if ( p . poly ) p = p . clone ( ) ;
p . poly = this ;
this . points . push ( p ) ;
return p ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* append point to polygon and return polygon
* /
2022-01-31 20:50:19 -05:00
append ( p ) {
2017-04-18 17:09:33 -04:00
this . push ( p ) ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/** close polygon */
2022-01-31 20:50:19 -05:00
setClosed ( ) {
2017-04-18 17:09:33 -04:00
this . open = false ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/** open polygon */
2022-01-31 20:50:19 -05:00
setOpen ( ) {
2017-04-18 17:09:33 -04:00
this . open = true ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-10-14 12:42:51 -04:00
setOpenValue ( b ) {
this . open = b ;
return this ;
}
2022-01-31 20:50:19 -05:00
isOpen ( ) {
2017-04-18 17:09:33 -04:00
return this . open ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
isClosed ( ) {
2017-04-18 17:09:33 -04:00
return ! this . open ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
appearsClosed ( ) {
2020-11-12 09:55:19 -05:00
return this . first ( ) . isEqual ( this . last ( ) ) ;
2022-01-31 20:50:19 -05:00
}
2020-11-12 09:55:19 -05:00
2022-01-31 20:50:19 -05:00
setClockwise ( ) {
2017-04-18 17:09:33 -04:00
if ( ! this . isClockwise ( ) ) this . reverse ( ) ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
setCounterClockwise ( ) {
2017-04-18 17:09:33 -04:00
if ( this . isClockwise ( ) ) this . reverse ( ) ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
isClockwise ( ) {
2017-04-18 17:09:33 -04:00
return this . area ( true ) > 0 ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
showKey ( ) {
return [ this . first ( ) . key , this . last ( ) . key , this . length ] . join ( '~~' ) ;
}
2017-04-18 17:09:33 -04:00
2023-02-13 14:46:15 -05:00
applyRotations ( ) {
for ( let point of this . points ) {
if ( point . a ) {
let p2 = new THREE . Vector3 ( point . x , point . y , point . z )
. applyAxisAngle ( XAXIS , point . a * DEG2RAD ) ;
point . x = p2 . x ;
point . y = p2 . y ;
point . z = p2 . z ;
}
}
return this ;
}
2017-04-18 17:09:33 -04:00
/ * *
* set this polygon ' s winding in alignment with the supplied polygon
* /
2022-01-31 20:50:19 -05:00
alignWinding ( poly , toLongest ) {
2017-04-18 17:09:33 -04:00
if ( toLongest && this . length > poly . length ) {
poly . alignWinding ( this , false ) ;
} else if ( this . isClockwise ( ) !== poly . isClockwise ( ) ) {
this . reverse ( ) ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* set this polygon ' s winding in opposition to supplied polygon
* /
2022-01-31 20:50:19 -05:00
opposeWinding ( poly , toLongest ) {
2017-04-18 17:09:33 -04:00
if ( toLongest && this . length > poly . length ) {
poly . opposeWinding ( this , false ) ;
} else if ( this . isClockwise ( ) === poly . isClockwise ( ) ) {
this . reverse ( ) ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2020-10-31 22:05:45 -04:00
/ * *
* @ returns { boolean } true if both polygons wind the same way
* /
2022-01-31 20:50:19 -05:00
sameWindings ( poly ) {
2020-10-31 22:05:45 -04:00
return this . isClockwise ( ) === poly . isClockwise ( ) ;
2022-01-31 20:50:19 -05:00
}
2020-10-31 22:05:45 -04:00
2017-04-18 17:09:33 -04:00
/ * *
* reverse direction of polygon points .
* /
2022-01-31 20:50:19 -05:00
reverse ( ) {
2021-06-18 22:57:30 -04:00
if ( this . area2 ) {
this . area2 = - this . area2 ;
}
2017-04-18 17:09:33 -04:00
this . points = this . points . reverse ( ) ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* return true if this polygon is ( likely ) nested inside parent
* /
2022-01-31 20:50:19 -05:00
isNested ( parent ) {
2017-04-18 17:09:33 -04:00
if ( parent . bounds . contains ( this . bounds ) ) {
2022-01-31 20:50:19 -05:00
return this . isInside ( parent , config . precision _nested _sq ) ;
2017-04-18 17:09:33 -04:00
}
return false ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2024-07-15 15:00:50 -07:00
/ * *
* Ease down along the polygonal path .
*
* 1. Travel from fromPoint to closest point on polygon , to rampZ above that that point ,
* 2. ease - down starts , following the polygonal path , decreasing Z at a fixed slop until target Z is hit ,
* 3. then the rest of the path is completed and repeated at target Z until touchdown point is reached .
*
* /
2022-01-31 20:50:19 -05:00
forEachPointEaseDown ( fn , fromPoint ) {
2020-06-03 22:39:30 -04:00
let index = this . findClosestPointTo ( fromPoint ) . index ,
2017-04-18 17:09:33 -04:00
fromZ = fromPoint . z ,
offset = 0 ,
points = this . points ,
length = points . length ,
touch = - 1 , // first point to touch target z
targetZ = points [ 0 ] . z ,
dist2next ,
last ,
next ,
done ;
2024-07-15 15:00:50 -07:00
// XXX: These should be UI configurable options.
const degrees = 1 ; // Rough "slope" of the ease-down path in degrees. It's probably only a slope if projected onto XZ or YZ planes.
const slope = Math . tan ( ( degrees * Math . PI ) / 180 ) ; // Slope for computations.
const maxDist2d = 0.5 ; // Maximum segment length, not sure if that's neccessary. Maybe misunderstanding of existing code.
const rampZ = 2.0 ; // Z height above polygon Z from which to start the ease-down. Machine will travel from "fromPoint" to "nearest point, Z + rampZ", then start the easy down.
2017-04-18 17:09:33 -04:00
while ( true ) {
next = points [ index % length ] ;
2024-07-15 15:00:50 -07:00
2017-04-18 17:09:33 -04:00
if ( last && next . z < fromZ ) {
2024-07-15 15:00:50 -07:00
// Only "in Ease-Down" (while target Z not yet reached) - follow path while slowly decreasing Z.
2020-06-03 22:39:30 -04:00
let deltaZ = fromZ - next . z ;
2017-04-18 17:09:33 -04:00
dist2next = last . distTo2D ( next ) ;
2024-07-15 15:00:50 -07:00
if ( dist2next > maxDist2d ) {
// long move: synth intermediate - neccessary?
last = next = last . followTo ( next , maxDist2d ) . setZ ( last . z - maxDist2d * slope )
fromZ = next . z ;
fn ( next , offset ++ ) ;
continue ;
2017-04-18 17:09:33 -04:00
} else {
// too short: clone n move z
2024-07-15 15:00:50 -07:00
next = next . clone ( ) . setZ ( fromZ - maxDist2d * slope ) ;
2017-04-18 17:09:33 -04:00
}
fromZ = next . z ;
} else if ( offset === 0 && next . z < fromZ ) {
2024-07-15 15:00:50 -07:00
// First point, move to rampZ height above next.
let deltaZ = fromZ - next . z ;
fromZ = next . z + Math . min ( deltaZ , rampZ )
2017-04-18 17:09:33 -04:00
next = next . clone ( ) . setZ ( fromZ ) ;
}
last = next ;
fn ( next , offset ++ ) ;
2024-07-15 15:00:50 -07:00
if ( ( index % length ) === touch ) {
break ;
}
if ( touch < 0 && next . z <= targetZ ) {
// Save touch-down index so as to be able to "complete" the full cut at target Z, i.e. keep following the path loop until the touch down point is reached again.
touch = ( ( index - 1 ) % length ) ;
}
2017-04-18 17:09:33 -04:00
index ++ ;
}
2024-07-15 15:00:50 -07:00
2017-04-18 17:09:33 -04:00
return last ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
forEachPoint ( fn , close , start ) {
2020-06-03 22:39:30 -04:00
let index = start || 0 ,
2017-04-18 17:09:33 -04:00
points = this . points ,
length = points . length ,
count = close ? length + 1 : length ,
offset = 0 ,
pos ;
while ( count -- > 0 ) {
pos = index % length ;
if ( fn ( points [ pos ] , pos , points , offset ++ ) ) return ;
index ++ ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
forEachSegment ( fn , open , start ) {
2020-06-03 22:39:30 -04:00
let index = start || 0 ,
2017-04-18 17:09:33 -04:00
points = this . points ,
length = points . length ,
count = open ? length - 1 : length ,
pos1 , pos2 ;
while ( count -- > 0 ) {
pos1 = index % length ;
2022-01-31 20:50:19 -05:00
pos2 = ( index + 1 ) % length ;
2017-04-18 17:09:33 -04:00
if ( fn ( points [ pos1 ] , points [ pos2 ] , pos1 , pos2 ) ) return ;
index ++ ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* returns intersections sorted by closest to lp1
* /
2022-01-31 20:50:19 -05:00
intersections ( lp1 , lp2 , deep ) {
2020-06-03 22:39:30 -04:00
let list = [ ] ;
2017-04-18 17:09:33 -04:00
this . forEachSegment ( function ( pp1 , pp2 , ip1 , ip2 ) {
2022-01-31 20:50:19 -05:00
let int = util . intersect ( lp1 , lp2 , pp1 , pp2 , base . key . SEGINT , false ) ;
2017-04-18 17:09:33 -04:00
if ( int ) {
list . push ( int ) ;
2020-11-19 21:04:45 -05:00
// console.log('pp1.pos',pp1.pos,'to',ip1);
// console.log('pp2.pos',pp2.pos,'to',ip2);
2017-04-18 17:09:33 -04:00
pp1 . pos = ip1 ;
pp2 . pos = ip2 ;
}
} ) ;
list . sort ( function ( p1 , p2 ) {
2022-01-31 20:50:19 -05:00
return util . distSq ( lp1 , p1 ) - util . distSq ( lp1 , p2 ) ;
2017-04-18 17:09:33 -04:00
} ) ;
2020-11-04 22:13:17 -05:00
if ( deep && this . inner ) {
this . inner . forEach ( p => {
let ints = p . intersections ( lp1 , lp2 ) ;
if ( ints ) list . appendAll ( ints ) ;
} ) ;
}
2017-04-18 17:09:33 -04:00
return list ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2024-07-15 10:37:53 -04:00
// return true if any line segments on either poly crosses the other
// this is a shallow test and does not inspect inners
intersects ( poly ) {
let p0 = this . points . slice ( ) ; p0 . push ( p0 [ 0 ] ) ;
let p1 = poly . points . slice ( ) ; p1 . push ( p1 [ 0 ] ) ;
for ( let i = 1 ; i < p0 . length ; i ++ ) {
let a = p0 [ i - 1 ] ;
let b = p0 [ i ] ;
for ( let j = 1 ; j < p1 . length ; j ++ ) {
let c = p1 [ j - 1 ] ;
let d = p1 [ j ] ;
if ( util . intersect ( a , b , c , d , base . key . SEGINT ) ) {
return true ;
}
}
}
return false ;
}
2017-04-18 17:09:33 -04:00
/ * *
2017-10-29 16:51:32 -04:00
* using two points , split polygon into two open polygons
* or return null if p1 , p2 does not intersect or poly is open
* /
2022-01-31 20:50:19 -05:00
bisect ( p1 , p2 ) {
2017-10-29 16:51:32 -04:00
if ( this . isOpen ( ) ) return null ;
2020-06-03 22:39:30 -04:00
let copy = this . clone ( ) . setClockwise ( ) ;
2017-10-29 19:46:34 -04:00
2020-06-03 22:39:30 -04:00
let int = copy . intersections ( p1 , p2 ) ;
2022-01-31 20:50:19 -05:00
if ( ! int || int . length !== 2 ) return null ;
2017-10-29 16:51:32 -04:00
2022-01-31 20:50:19 -05:00
return [ copy . emitSegment ( int [ 0 ] , int [ 1 ] ) , copy . emitSegment ( int [ 1 ] , int [ 0 ] ) . reverse ( ) ] ;
}
2017-10-29 16:51:32 -04:00
/ * *
* emit new open poly between two intersection points of a clockwise poly .
* used in cam tabs and fdm output perimeter traces on infill
2017-04-18 17:09:33 -04:00
* /
2022-01-31 20:50:19 -05:00
emitSegment ( i1 , i2 ) {
2020-06-03 22:39:30 -04:00
let poly = newPolygon ( ) ,
2017-04-18 17:09:33 -04:00
start = i1 . p2 . pos ,
2017-10-29 19:46:34 -04:00
end = i2 . p1 . pos ;
2020-11-19 21:04:45 -05:00
// console.log({emitSeg: this, i1, i2, start, end});
2017-04-18 17:09:33 -04:00
poly . setOpen ( ) ;
poly . push ( i1 ) ;
2017-04-22 10:30:56 -04:00
this . forEachPoint ( function ( p , pos ) {
poly . push ( p ) ;
2020-11-19 21:04:45 -05:00
if ( p === i2 . p1 ) {
// console.log('hit end point @', pos);
return true ;
}
2017-04-22 10:30:56 -04:00
} , true , start ) ;
2017-04-18 17:09:33 -04:00
poly . push ( i2 ) ;
2020-11-19 21:04:45 -05:00
// console.log({emit: poly});
2017-04-18 17:09:33 -04:00
return poly ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon } poly
* @ param { number } [ tolerance ]
* @ returns { boolean } any points inside OR on edge
* /
2022-01-31 20:50:19 -05:00
hasPointsInside ( poly , tolerance ) {
2017-04-18 17:09:33 -04:00
if ( ! poly . overlaps ( this ) ) return false ;
2020-06-03 22:39:30 -04:00
let mid , exit = false ;
2017-04-18 17:09:33 -04:00
2022-02-19 16:16:49 -05:00
this . forEachSegment ( ( prev , next ) => {
2017-04-18 17:09:33 -04:00
// check midpoint on long lines
2022-01-31 20:50:19 -05:00
if ( prev . distTo2D ( next ) > config . precision _midpoint _check _dist ) {
2017-04-18 17:09:33 -04:00
mid = prev . midPointTo ( next ) ;
2022-01-31 20:50:19 -05:00
if ( mid . inPolygon ( poly ) || mid . nearPolygon ( poly , tolerance || config . precision _close _to _poly _sq ) ) {
2017-04-18 17:09:33 -04:00
return exit = true ;
}
}
2022-01-31 20:50:19 -05:00
if ( next . inPolygon ( poly ) || next . nearPolygon ( poly , tolerance || config . precision _close _to _poly _sq ) ) {
2017-04-18 17:09:33 -04:00
return exit = true ;
}
} ) ;
return exit ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2020-11-06 14:27:26 -05:00
/ * *
* returns true if any point on this polygon
* is within radius of a point on the target
* /
2022-01-31 20:50:19 -05:00
isNear ( poly , radius , cache ) {
const midcheck = config . precision _midpoint _check _dist ;
const dist = radius || config . precision _close _to _poly _sq ;
2020-11-06 14:27:26 -05:00
let near = false ;
let mem = cache ? this . cacheNear = this . cacheNear || { } : undefined ;
if ( mem && mem [ poly . id ] !== undefined ) {
return mem [ poly . id ] ;
}
2022-02-19 16:16:49 -05:00
this . forEachSegment ( ( prev , next ) => {
2020-11-06 14:27:26 -05:00
// check midpoint on long lines
if ( prev . distToSq2D ( next ) > midcheck ) {
if ( prev . midPointTo ( next ) . nearPolygon ( poly , dist ) ) {
return near = true ; // stops iteration
}
}
if ( next . nearPolygon ( poly , dist ) ) {
return near = true ; // stops iteration
}
} ) ;
if ( mem ) {
mem [ poly . id ] = near ;
}
return near ;
2022-01-31 20:50:19 -05:00
}
2020-11-06 14:27:26 -05:00
2017-04-18 17:09:33 -04:00
/ * *
* TODO replace isNested ( ) with isInside ( ) ?
*
* @ param { Polygon } poly
* @ param { number } [ tolerance ]
* @ returns { boolean } all points inside OR on edge
* /
2022-01-31 20:50:19 -05:00
isInside ( poly , tolerance ) {
2017-04-18 17:09:33 -04:00
// throw new Error("isInside");
2022-01-31 20:50:19 -05:00
const neardist = tolerance || config . precision _close _to _poly _sq ;
2020-11-10 19:56:34 -05:00
if ( ! this . bounds . isNested ( poly . bounds , neardist * 3 ) ) {
2020-10-30 20:47:45 -04:00
return false ;
}
2017-04-18 17:09:33 -04:00
2020-06-03 22:39:30 -04:00
let mid ,
2022-01-31 20:50:19 -05:00
midcheck = config . precision _midpoint _check _dist ,
2017-04-18 17:09:33 -04:00
exit = true ;
2022-02-19 16:16:49 -05:00
this . forEachSegment ( ( prev , next ) => {
2020-11-06 14:27:26 -05:00
// check midpoint on long lines (TODO: should be distToSq2D()?)
if ( prev . distTo2D ( next ) > midcheck ) {
2017-04-18 17:09:33 -04:00
mid = prev . midPointTo ( next ) ;
2020-11-10 19:56:34 -05:00
if ( ! ( mid . inPolygon ( poly ) || mid . nearPolygon ( poly , neardist ) ) ) {
2017-04-18 17:09:33 -04:00
exit = false ;
return true ;
}
}
2020-11-10 19:56:34 -05:00
if ( ! ( next . inPolygon ( poly ) || next . nearPolygon ( poly , neardist ) ) ) {
2017-04-18 17:09:33 -04:00
exit = false ;
return true ;
}
2020-11-15 19:13:53 -05:00
} , this . open ) ;
2017-04-18 17:09:33 -04:00
return exit ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon } poly
* @ param { number } [ tolerance ]
* @ returns { boolean } all points inside poly AND not inside children
* /
2020-05-31 18:21:05 -04:00
// PRO.contains = function(poly, tolerance) {
// return (poly && poly.isInside(this, tolerance) && poly.isOutsideAll(this.inner, tolerance));
// };
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
containedBySet ( polys ) {
2017-04-18 17:09:33 -04:00
if ( ! polys ) return false ;
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < polys . length ; i ++ ) {
2017-04-18 17:09:33 -04:00
if ( polys [ i ] . contains ( this ) ) return true ;
}
return false ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
addInner ( child ) {
2017-04-18 17:09:33 -04:00
child . parent = this ;
if ( this . inner ) {
this . inner . push ( child ) ;
} else {
this . inner = [ child ] ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ returns { number } number of inner polygons
* /
2022-01-31 20:50:19 -05:00
innerCount ( ) {
2017-04-18 17:09:33 -04:00
return this . inner ? this . inner . length : 0 ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ returns { boolean } if has 1 or more inner polygons
* /
2022-01-31 20:50:19 -05:00
hasInner ( ) {
2017-04-18 17:09:33 -04:00
return this . inner && this . inner . length > 0 ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* remove all inner polygons
* /
2022-01-31 20:50:19 -05:00
clearInner ( ) {
2017-04-18 17:09:33 -04:00
this . inner = null ;
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2023-01-10 21:28:35 -05:00
freeParentRefs ( ) {
if ( this . inner && this . inner . length > 0 ) {
for ( let inner of this . inner ) {
inner . freeParentRefs ( ) ;
delete inner . parent ;
}
}
for ( let p of this . points ) {
delete p . poly ;
}
}
2022-01-31 20:50:19 -05:00
newUndeleted ( ) {
2020-06-03 22:39:30 -04:00
let poly = newPolygon ( ) ;
2022-02-19 16:16:49 -05:00
this . forEachPoint ( p => {
2017-04-18 17:09:33 -04:00
if ( ! p . del ) poly . push ( p ) ;
} ) ;
return poly ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* http : //www.ehow.com/how_5138742_calculate-circularity.html
* @ returns { number } 0.0 - 1.0 from flat to perfectly circular
* /
2022-01-31 20:50:19 -05:00
circularity ( ) {
return ( 4 * Math . PI * this . area ( ) ) / util . sqr ( this . perimeter ( ) ) ;
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
circularityDeep ( ) {
return ( 4 * Math . PI * this . areaDeep ( ) ) / util . sqr ( this . perimeter ( ) ) ;
}
2017-04-18 17:09:33 -04:00
/ * *
* @ returns { number } perimeter length ( sum of all segment lengths )
* /
2022-01-31 20:50:19 -05:00
perimeter ( ) {
2021-06-20 18:59:59 -04:00
if ( this . perim ) {
2020-02-11 09:27:34 -05:00
return this . perim ;
}
2020-06-03 22:39:30 -04:00
let len = 0.0 ;
2017-04-18 17:09:33 -04:00
2022-02-19 16:16:49 -05:00
this . forEachSegment ( ( prev , next ) => {
2022-01-31 20:50:19 -05:00
len += Math . sqrt ( prev . distToSq2D ( next ) ) ;
2017-04-18 17:09:33 -04:00
} , this . open ) ;
2020-02-11 09:27:34 -05:00
return this . perim = len ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
perimeterDeep ( ) {
2020-06-03 22:39:30 -04:00
let len = this . perimeter ( ) ;
2022-02-19 16:16:49 -05:00
if ( this . inner ) this . inner . forEach ( p => {
2022-01-31 20:50:19 -05:00
len += p . perimeter ( )
} ) ;
2017-04-18 17:09:33 -04:00
return len ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* calculate and return the area enclosed by the polygon .
* if raw is true , return a signed area equal to 2 x the
* enclosed area which also indicates winding direction .
*
* @ param { boolean } [ raw ]
* @ returns { number } area
* /
2022-01-31 20:50:19 -05:00
area ( raw ) {
2021-06-18 22:57:30 -04:00
if ( this . length < 3 ) {
return 0 ;
}
if ( this . area2 === undefined ) {
this . area2 = 0.0 ;
2022-01-31 20:50:19 -05:00
for ( let p = this . points , pl = p . length , pi = 0 , p1 , p2 ; pi < pl ; pi ++ ) {
2017-04-18 17:09:33 -04:00
p1 = p [ pi ] ;
2022-01-31 20:50:19 -05:00
p2 = p [ ( pi + 1 ) % pl ] ;
2017-04-18 17:09:33 -04:00
this . area2 += ( p2 . x - p1 . x ) * ( p2 . y + p1 . y ) ;
}
}
2022-01-31 20:50:19 -05:00
return raw ? this . area2 : Math . abs ( this . area2 / 2 ) ;
}
2017-04-18 17:09:33 -04:00
/ * *
* return the area of a polygon with the area of all
* inner polygons subtracted
*
* @ returns { number } area
* /
2022-01-31 20:50:19 -05:00
areaDeep ( ) {
2021-06-18 22:57:30 -04:00
if ( ! this . inner ) {
return this . area ( ) ;
}
2022-01-31 20:50:19 -05:00
let i , c = this . inner ,
a = this . area ( ) ;
for ( i = 0 ; i < c . length ; i ++ ) {
2017-04-18 17:09:33 -04:00
a -= c [ i ] . area ( ) ;
}
return a ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon } poly
* @ returns { boolean }
* /
2022-01-31 20:50:19 -05:00
overlaps ( poly ) {
return this . bounds . overlaps ( poly . bounds , config . precision _merge ) ;
}
2017-04-18 17:09:33 -04:00
/ * *
* create poly from coordinate Array ( aka dump )
*
* @ param { number [ ] } arr
* @ param { number } [ z ]
* /
2022-01-31 20:50:19 -05:00
fromXYArray ( arr , z ) {
2020-06-03 22:39:30 -04:00
let i = 0 ;
2017-04-18 17:09:33 -04:00
while ( i < arr . length ) {
this . add ( arr [ i ++ ] , arr [ i ++ ] , z || 0 ) ;
}
return this ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2021-06-25 10:28:39 -04:00
/ * *
* shortcut to de - rez poly
* /
2022-01-31 20:50:19 -05:00
simple ( ) {
return this . clean ( true , undefined , Math . min ( config . clipper / 10 , config . clipperClean * 5 ) ) ;
}
2021-06-25 10:28:39 -04:00
2017-04-18 17:09:33 -04:00
/ * *
* simplify and merge collinear . only works for single
2022-01-31 20:50:19 -05:00
* non - nested polygons . used primarily in slicer / connectLines .
2017-04-18 17:09:33 -04:00
* /
2022-01-31 20:50:19 -05:00
clean ( deep , parent , merge = config . clipperClean ) {
2024-07-02 22:03:35 -04:00
let clean = CleanPolygon ( this . toClipper ( ) [ 0 ] , merge ) ,
2017-04-18 17:09:33 -04:00
poly = fromClipperPath ( clean , this . getZ ( ) ) ;
2020-11-11 15:19:35 -05:00
if ( poly . length === 0 ) return this ;
2020-05-04 23:00:39 -04:00
if ( deep && this . inner ) {
2021-06-19 15:17:36 -04:00
poly . inner = this . inner . map ( inr => inr . clean ( false , poly , merge ) ) ;
2020-05-04 23:00:39 -04:00
}
poly . parent = parent || this . parent ;
poly . area2 = this . area2 ;
2020-11-17 00:45:34 -05:00
poly . open = this . open ;
2021-02-22 22:37:38 -05:00
if ( this . open ) {
// when open, ensure first point on new poly matches old
let start = this . points [ 0 ] ;
let points = poly . points ;
let length = points . length ;
let mi , min = Infinity ;
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < length ; i ++ ) {
2021-02-22 22:37:38 -05:00
let d = points [ i ] . distTo2D ( start ) ;
if ( d < min ) {
min = d ;
mi = i ;
}
}
// mi > 0 means first point didn't match
if ( mi ) {
let nupoints = [ ] ;
2022-01-31 20:50:19 -05:00
for ( let i = mi ; i < length ; i ++ ) {
2021-02-22 22:37:38 -05:00
nupoints . push ( points [ i ] ) ;
}
2022-01-31 20:50:19 -05:00
for ( let i = 0 ; i < mi ; i ++ ) {
2021-02-22 22:37:38 -05:00
nupoints . push ( points [ i ] ) ;
}
poly . points = nupoints ;
}
}
2020-11-11 15:19:35 -05:00
return poly ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
toClipper ( inout ) {
2020-06-03 22:39:30 -04:00
let poly = this ,
2017-04-18 17:09:33 -04:00
out = inout || [ ] ;
2021-06-18 16:56:41 -04:00
out . push ( poly . points . map ( p => p . toClipper ( ) ) ) ;
2017-04-18 17:09:33 -04:00
if ( poly . inner ) {
2022-10-15 17:55:35 -04:00
for ( let inner of poly . inner ) {
inner . toClipper ( out ) ;
}
2017-04-18 17:09:33 -04:00
}
return out ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* return offset polygon ( s ) from original using distance . may result in
* more than one new polygon if trace is self - intersecting or null if new
* polygon is too small or offset is otherwise not possible due to geometry .
*
* @ param { number } offset positive = inset , negative = outset
* @ param { Polygon [ ] } [ output ]
* @ returns { ? Polygon [ ] } returns output array provided as input or new array if not provided
* /
2022-01-31 20:50:19 -05:00
offset ( offset , output ) {
return POLY . expand ( [ this ] , - offset , this . getZ ( ) , output ) ;
}
2017-04-18 17:09:33 -04:00
2024-07-01 20:52:49 -04:00
/ * *
* ofsetting an open line uses a different procedure and options
*
* @ param { number } distance
* @ param { 'square' | 'round' | 'miter' } type
* @ returns { Polygon [ ] }
* /
offset _open ( distance , type = 'miter' ) {
if ( this . isOpen ( ) ) {
2024-07-02 22:03:35 -04:00
let coff = new ClipperOffset ( ) ,
tree = new PolyTree ( ) ,
2024-07-01 20:52:49 -04:00
entt = {
2024-07-02 22:03:35 -04:00
'square' : EndType . etOpenSquare ,
'round' : EndType . etOpenRound ,
'miter' : EndType . etOpenSquare
} [ type ] || EndType . etOpenSquare ,
2024-07-01 20:52:49 -04:00
jntt = {
2024-07-02 22:03:35 -04:00
'square' : JoinType . jtSquare ,
'round' : JoinType . jtRound ,
'miter' : JoinType . jtMiter
} [ type ] || JoinType . jtMiter ;
2024-07-01 20:52:49 -04:00
coff . AddPaths ( this . toClipper ( ) , jntt , entt ) ;
2024-07-02 22:03:35 -04:00
coff . Execute ( tree , distance * config . clipper ) ;
return POLY . fromClipperTree ( tree , this . getZ ( ) , null , null , 0 ) ;
2024-07-01 20:52:49 -04:00
} else {
return this . offset ( distance ) ;
}
}
2017-04-18 17:09:33 -04:00
/ * *
* todo need something more clever for polygons that overlap with
* todo differing resolutions ( like circles )
*
* @ param { Polygon } poly
* @ param { boolean } [ recurse ]
* @ param { number } [ precision ]
* @ returns { boolean } true if polygons are , essentially , the same
* /
2022-01-31 20:50:19 -05:00
isEquivalent ( poly , recurse , precision ) {
2017-04-18 17:09:33 -04:00
// throw new Error("isEquivalent");
2020-05-31 18:21:05 -04:00
let area1 = Math . abs ( this . area ( ) ) ;
let area2 = Math . abs ( poly . area ( ) ) ;
2022-01-31 20:50:19 -05:00
if ( util . isCloseTo ( area1 , area2 , precision || config . precision _poly _area ) &&
this . bounds . equals ( poly . bounds , precision || config . precision _poly _bounds ) ) {
2017-04-18 17:09:33 -04:00
// use circularity near 1 to eliminate the extensive check below
2020-06-03 22:39:30 -04:00
let c1 = this . circularity ( ) ,
2017-04-18 17:09:33 -04:00
c2 = poly . circularity ( ) ;
2022-01-31 20:50:19 -05:00
if ( Math . abs ( c1 - c2 ) < config . precision _circularity && ( ( 1 - c1 ) < config . precision _circularity ) ) {
2017-04-18 17:09:33 -04:00
return true ;
}
if ( recurse ) {
2022-01-31 20:50:19 -05:00
let i , ai = this . inner ,
bi = poly . inner ;
2017-04-18 17:09:33 -04:00
if ( ai !== bi ) {
2020-11-06 14:27:26 -05:00
if ( ai === null || bi === null || ai . length != bi . length ) {
return false ;
}
2022-01-31 20:50:19 -05:00
for ( i = 0 ; i < ai . length ; i ++ ) {
2020-11-06 14:27:26 -05:00
if ( ! ai [ i ] . isEquivalent ( bi [ i ] ) ) {
return false ;
}
2017-04-18 17:09:33 -04:00
}
}
}
2020-06-03 22:39:30 -04:00
let exit = true ,
2017-04-18 17:09:33 -04:00
pointok ,
dist ,
min ;
2022-02-19 16:16:49 -05:00
this . forEachPoint ( i2p => {
2017-04-18 17:09:33 -04:00
pointok = false ;
2022-02-19 16:16:49 -05:00
poly . forEachSegment ( ( i1p1 , i1p2 ) => {
2017-04-18 17:09:33 -04:00
// if point is close to poly, terminate search, go to next point
2022-01-31 20:50:19 -05:00
if ( ( dist = i2p . distToLine ( i1p1 , i1p2 ) ) < config . precision _poly _merge ) {
2020-11-06 14:27:26 -05:00
return pointok = true ;
}
2017-04-18 17:09:33 -04:00
// otherwise track min and keep searching
min = Math . min ( min , dist ) ;
} ) ;
// fail poly if one point is bad
if ( ! pointok ) {
exit = false ;
// terminate search
return true ;
}
} ) ;
return exit ;
}
return false ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* find the point of this polygon closest to
* the provided point . assist generating optimal
* print paths .
*
* @ param { Point } target
* @ return { Object } { point : point , distance : distance }
* /
2022-01-31 20:50:19 -05:00
findClosestPointTo ( target ) {
2020-06-03 22:39:30 -04:00
let dist ,
2017-04-18 17:09:33 -04:00
index ,
closest ,
mindist = Infinity ;
2022-02-19 16:16:49 -05:00
this . forEachPoint ( ( point , pos ) => {
2022-01-31 20:50:19 -05:00
dist = Math . sqrt ( point . distToSq2D ( target ) ) ;
2017-04-18 17:09:33 -04:00
if ( dist < mindist ) {
index = pos ;
mindist = dist ;
closest = point ;
}
} ) ;
2022-01-31 20:50:19 -05:00
return {
point : closest ,
distance : mindist ,
index : index
} ;
}
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon [ ] } out
2024-06-30 12:45:22 -04:00
* @ param { [ ] } deep recurse and track recursion
* @ param { boolean } crush remove inner array after flatten
2017-04-18 17:09:33 -04:00
* @ returns { Polygon [ ] }
* /
2024-06-30 12:45:22 -04:00
flattenTo ( out , deep , crush ) {
2017-04-18 17:09:33 -04:00
out . push ( this ) ;
2024-06-30 12:45:22 -04:00
if ( deep ) {
if ( deep . contains ( this ) ) {
console . log ( 'flat recursion @' , this ) ;
return ;
}
deep . push ( this ) ;
}
if ( this . inner ) {
for ( let p of this . inner ) {
p . flattenTo ( out , deep , crush ) ;
}
}
if ( crush ) {
this . inner = undefined ;
}
2017-04-18 17:09:33 -04:00
return out ;
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
shortestSegmentLength ( ) {
2020-06-03 22:39:30 -04:00
let len = Infinity ;
2022-02-19 16:16:49 -05:00
this . forEachSegment ( ( p1 , p2 ) => {
2018-01-20 18:37:00 -05:00
len = Math . min ( len , p1 . distTo2D ( p2 ) ) ;
} ) ;
return len ;
2022-01-31 20:50:19 -05:00
}
2018-01-20 18:37:00 -05:00
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon } poly clipping mask
* @ returns { ? Polygon [ ] }
* /
2022-01-31 20:50:19 -05:00
diff ( poly ) {
2020-06-03 22:39:30 -04:00
let fillang = this . fillang && this . area ( ) > poly . area ( ) ? this . fillang : poly . fillang ,
2024-07-02 22:03:35 -04:00
clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
2017-04-18 17:09:33 -04:00
sp1 = this . toClipper ( ) ,
sp2 = poly . toClipper ( ) ;
2024-07-02 22:03:35 -04:00
clip . AddPaths ( sp1 , PathSubject , true ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
2017-04-18 17:09:33 -04:00
2024-07-02 22:03:35 -04:00
if ( clip . Execute ( ClipDiff , tree , FillEvenOdd , FillEvenOdd ) ) {
poly = POLY . fromClipperTree ( tree , poly . getZ ( ) ) ;
2024-07-13 16:51:22 -04:00
poly . forEach ( p => p . fillang = fillang ) ;
return poly ;
} else {
return null ;
}
}
/ * *
* @ param { Polygon } poly poly to xor against this one
* @ returns { ? Polygon [ ] }
* /
xor ( poly ) {
let fillang = this . fillang && this . area ( ) > poly . area ( ) ? this . fillang : poly . fillang ,
clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
sp1 = this . toClipper ( ) ,
sp2 = poly . toClipper ( ) ;
clip . AddPaths ( sp1 , PathSubject , true ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
if ( clip . Execute ( ClipXOR , tree , FillNonZero , FillNonZero ) ) {
poly = POLY . fromClipperTree ( tree , poly . getZ ( ) ) ;
poly . forEach ( p => p . fillang = fillang ) ;
2017-04-18 17:09:33 -04:00
return poly ;
} else {
return null ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
/ * *
* @ param { Polygon } poly clipping mask
* @ returns { ? Polygon [ ] }
* /
2022-06-16 18:24:49 -04:00
mask ( poly , nullOnEquiv , minarea ) {
2020-06-03 22:39:30 -04:00
let fillang = this . fillang && this . area ( ) > poly . area ( ) ? this . fillang : poly . fillang ,
2024-07-02 22:03:35 -04:00
clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
2017-04-18 17:09:33 -04:00
sp1 = this . toClipper ( ) ,
sp2 = poly . toClipper ( ) ;
2024-07-02 22:03:35 -04:00
clip . AddPaths ( sp1 , PathSubject , true ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
2017-04-18 17:09:33 -04:00
2024-07-02 22:03:35 -04:00
if ( clip . Execute ( ClipIntersect , tree , FillEvenOdd , FillEvenOdd ) ) {
poly = POLY . fromClipperTree ( tree , this . getZ ( ) , undefined , undefined , minarea ) ;
2022-02-19 16:16:49 -05:00
poly . forEach ( p => {
2017-04-18 17:09:33 -04:00
p . fillang = fillang ;
} )
2020-12-01 14:22:19 -05:00
if ( nullOnEquiv && poly . length === 1 && poly [ 0 ] . isEquivalent ( this ) ) {
return null ;
}
2017-04-18 17:09:33 -04:00
return poly ;
} else {
return null ;
}
2022-01-31 20:50:19 -05:00
}
2017-04-18 17:09:33 -04:00
2022-10-10 00:00:08 -04:00
// cut poly using array of closed polygons. used primarily in cnc
// to cut perimeters using tabs resulting in open poly lines.
2022-01-31 20:50:19 -05:00
cut ( polys , inter ) {
2020-11-25 22:50:44 -05:00
let target = this ;
if ( ! target . open ) {
target = this . clone ( true ) . setOpen ( ) ;
target . push ( target . first ( ) ) ;
if ( target . inner ) {
target . inner . forEach ( ip => {
ip . setOpen ( ) ;
ip . push ( ip . first ( ) ) ;
} ) ;
}
}
2024-07-02 22:03:35 -04:00
let clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
type = inter ? ClipIntersect : ClipDiff ,
2020-11-25 22:50:44 -05:00
sp1 = target . toClipper ( ) ,
2022-01-31 20:50:19 -05:00
sp2 = POLY . toClipper ( polys ) ;
2020-11-25 22:50:44 -05:00
2024-07-02 22:03:35 -04:00
clip . AddPaths ( sp1 , PathSubject , false ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
2020-11-25 22:50:44 -05:00
2024-07-02 22:03:35 -04:00
if ( clip . Execute ( type , tree , FillEvenOdd , FillEvenOdd ) ) {
let cuts = POLY . fromClipperTree ( tree , target . getZ ( ) , null , null , 0 ) ;
2020-11-25 22:50:44 -05:00
cuts . forEach ( no => {
// heal open but really closed polygons because cutting
// has to open the poly to perform the cut. but the result
// may have been no intersection leaving an open poly
if ( no . open && no . first ( ) . distTo2D ( no . last ( ) ) < 0.001 ) {
no . open = false ;
no . points . pop ( ) ;
}
2021-03-16 19:20:09 -04:00
no . depth = this . depth ;
2020-11-25 22:50:44 -05:00
} ) ;
return cuts ;
} else {
2020-12-01 14:22:19 -05:00
return null ;
2020-11-25 22:50:44 -05:00
}
2022-01-31 20:50:19 -05:00
}
2020-11-25 22:50:44 -05:00
2022-10-10 00:00:08 -04:00
// find the intersection of two polygons
2022-01-31 20:50:19 -05:00
intersect ( poly , min ) {
2020-05-31 18:21:05 -04:00
if ( ! this . overlaps ( poly ) ) return null ;
if ( this . isInside ( poly ) ) {
2022-01-31 20:50:19 -05:00
return [ this ] ;
2020-05-31 18:21:05 -04:00
}
2024-07-02 22:03:35 -04:00
let clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
sp1 = this . toClipper ( ) ,
sp2 = poly . toClipper ( ) ,
minarea = min >= 0 ? min : 0.1 ;
clip . AddPaths ( sp1 , PathSubject , true ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
2020-05-31 18:21:05 -04:00
2024-07-02 22:03:35 -04:00
if ( clip . Execute ( ClipIntersect , tree , FillNonZero , FillNonZero ) ) {
2022-01-31 20:50:19 -05:00
let inter = POLY
2024-07-02 22:03:35 -04:00
. fromClipperTreeUnion ( tree , poly . getZ ( ) , minarea )
2020-05-31 18:21:05 -04:00
// .filter(p => p.isEquivalent(this) || p.isInside(this))
2022-01-31 20:50:19 -05:00
. filter ( p => p . isInside ( this ) ) ;
2020-05-31 18:21:05 -04:00
return inter ;
}
return null ;
}
2022-01-31 20:50:19 -05:00
areaDiff ( poly ) {
2021-06-22 11:23:45 -04:00
let a1 = this . area ( ) ,
a2 = poly . area ( ) ;
return ( a1 > a2 ) ? a2 / a1 : a1 / a2 ;
2022-01-31 20:50:19 -05:00
}
2021-06-22 11:23:45 -04:00
2022-01-26 17:28:23 -05:00
// does not work with nested polys
2022-01-31 20:50:19 -05:00
simplify ( opt = { } ) {
2022-01-26 17:28:23 -05:00
let z = this . getZ ( ) ;
// use expand / deflate technique instead
if ( opt . pump ) {
2022-01-31 20:50:19 -05:00
let p2 = POLY . offset ( [ this ] , opt . pump , {
z
} ) ;
2022-01-26 17:28:23 -05:00
if ( p2 ) {
2022-01-31 20:50:19 -05:00
p2 = POLY . offset ( p2 , - opt . pump , {
z
} ) ;
2022-01-26 17:28:23 -05:00
return p2 ;
}
return null ;
}
2022-01-31 20:50:19 -05:00
let clip = this . toClipper ( ) ,
2024-07-15 10:55:09 -04:00
res = Clipper . SimplifyPolygons ( clip , FillNonZero ) ;
2022-01-26 17:28:23 -05:00
if ( ! ( res && res . length ) ) {
return null ;
}
return res . map ( array => {
let poly = newPolygon ( ) ;
for ( let pt of array ) {
2022-01-31 20:50:19 -05:00
poly . push ( base . pointFromClipper ( pt , z ) ) ;
2022-01-26 17:28:23 -05:00
}
return poly ;
} ) ;
2022-01-31 20:50:19 -05:00
}
2022-01-26 17:28:23 -05:00
2022-01-31 20:50:19 -05:00
unionMatch ( polys ) {
2021-12-12 18:37:55 -05:00
return polys . filter ( poly => poly . isEquivalent ( this ) ) . length ;
2022-01-31 20:50:19 -05:00
}
2021-12-12 13:49:30 -05:00
2017-04-18 17:09:33 -04:00
/ * *
* return logical OR of two polygons ' enclosed areas
*
* @ param { Polygon } poly
2021-12-12 18:37:55 -05:00
* @ returns { ? Polygon } intersected polygon , null if no intersection , or all when indicated
2017-04-18 17:09:33 -04:00
* /
2022-01-31 20:50:19 -05:00
union ( poly , min , all ) {
2017-04-18 17:09:33 -04:00
if ( ! this . overlaps ( poly ) ) return null ;
2021-12-12 13:49:30 -05:00
2022-01-31 20:50:19 -05:00
let fillang = this . fillang && this . area ( ) > poly . area ( ) ? this . fillang : poly . fillang ,
2024-07-02 22:03:35 -04:00
clip = new Clipper ( ) ,
tree = new PolyTree ( ) ,
2022-01-31 20:50:19 -05:00
sp1 = this . toClipper ( ) ,
sp2 = poly . toClipper ( ) ,
minarea = min >= 0 ? min : 0.1 ;
2024-07-02 22:03:35 -04:00
clip . AddPaths ( sp1 , PathSubject , true ) ;
clip . AddPaths ( sp2 , PathClip , true ) ;
2022-01-31 20:50:19 -05:00
2024-07-02 22:03:35 -04:00
if ( clip . Execute ( ClipUnion , tree , FillEvenOdd , FillEvenOdd ) ) {
let union = POLY . fromClipperTreeUnion ( tree , poly . getZ ( ) , minarea ) ;
2022-01-31 20:50:19 -05:00
if ( all ) {
if ( union . length === 2 ) {
return null ;
// if (this.unionMatch(union) || poly.unionMatch(union)) {
// return null;
// }
2021-12-12 13:49:30 -05:00
}
2022-01-31 20:50:19 -05:00
return union ;
2021-12-12 13:49:30 -05:00
}
2022-01-31 20:50:19 -05:00
if ( union . length === 1 ) {
union = union [ 0 ] ;
union . fillang = fillang ;
return union ;
} else {
console . trace ( {
check _union _call _path : union ,
this : this ,
poly
} ) ;
}
}
2021-12-12 13:49:30 -05:00
2022-01-31 20:50:19 -05:00
return null ;
}
2022-06-16 18:24:49 -04:00
annotate ( obj = { } ) {
Object . assign ( this , obj ) ;
return this ;
}
2024-06-29 19:21:22 -04:00
// turn 2d polygon into a 2.5D ribbon extruded in Z
2024-06-29 23:03:40 -04:00
ribbonZ ( z = 1 , zadd = 0 , rev ) {
2022-06-16 18:24:49 -04:00
let poly = this . clone ( ) . setClockwise ( ) ;
let faces = [ ] ;
let points = poly . points ;
let length = points . length ;
2024-06-29 23:03:40 -04:00
if ( rev ) {
points = points . slice ( ) . reverse ( ) ;
}
2022-06-16 18:24:49 -04:00
for ( let i = 0 ; i < length ; i ++ ) {
let p0 = points [ i ] ;
let p1 = points [ ( i + 1 ) % length ] ;
2024-06-29 23:03:40 -04:00
faces . push ( p0 . x , p0 . y , p0 . z + zadd ) ;
faces . push ( p1 . x , p1 . y , p1 . z + z + zadd ) ;
faces . push ( p1 . x , p1 . y , p0 . z + zadd ) ;
faces . push ( p0 . x , p0 . y , p0 . z + zadd ) ;
faces . push ( p0 . x , p0 . y , p0 . z + z + zadd ) ;
faces . push ( p1 . x , p1 . y , p1 . z + z + zadd ) ;
2022-06-16 18:24:49 -04:00
}
return faces ;
}
2022-10-10 00:00:08 -04:00
2024-06-29 19:21:22 -04:00
// extrude poly (with inner voids) into 3d mesh
2024-06-29 23:03:40 -04:00
extrude ( z = 1 , zadd = 0 ) {
2024-06-29 19:21:22 -04:00
let obj = [ ] ;
let earcut = this . earcut ( ) ;
for ( let poly of earcut ) {
for ( let point of poly . points ) {
2024-06-29 23:03:40 -04:00
obj . push ( point . x , point . y , z + zadd ) ;
2024-06-29 19:21:22 -04:00
}
for ( let point of poly . points . reverse ( ) ) {
2024-06-29 23:03:40 -04:00
obj . push ( point . x , point . y , zadd ) ;
2024-06-29 19:21:22 -04:00
}
}
2024-06-29 23:03:40 -04:00
obj . appendAll ( this . ribbonZ ( z , zadd ) ) ;
2024-06-29 19:21:22 -04:00
for ( let inner of this . inner || [ ] ) {
2024-06-29 23:03:40 -04:00
obj . appendAll ( inner . ribbonZ ( z , zadd , true ) ) ;
2024-06-29 19:21:22 -04:00
}
return obj ;
}
2022-10-10 00:00:08 -04:00
// split long straight lines into segments no longer than max
// and return a new polygon
segment ( max = 1 ) {
const newp = [ ] ;
const points = this . points ;
const length = points . length ;
2023-02-05 07:03:18 +01:00
const l0 = this . open ? length - 1 : length ;
for ( let i = 0 , p = points , l1 = length , l2 = l1 + 1 ; i < l0 ; i ++ ) {
2022-10-10 00:00:08 -04:00
const p1 = p [ i ] ;
const p2 = p [ ( i + 1 ) % l1 ] ;
const dx = p2 . x - p1 . x ;
const dy = p2 . y - p1 . y ;
const dl = Math . sqrt ( dx * dx + dy * dy ) ;
newp . push ( p1 ) ;
if ( dl < max ) {
continue ;
}
2022-10-10 11:54:51 -04:00
const div = dl / max ;
2022-10-10 00:00:08 -04:00
const fit = div | 0 ;
const add = fit - 1 ;
const ix = dx / fit ;
const iy = dy / fit ;
let ox = p1 . x + ix ;
let oy = p1 . y + iy ;
for ( let i = 0 ; i < add ; i ++ ) {
2022-10-10 11:54:51 -04:00
newp . push ( newPoint ( ox , oy , ( p1 . z + p2 . z ) / 2 ) ) ;
2022-10-10 00:00:08 -04:00
ox += ix ;
oy += iy ;
}
}
if ( newp . length > length ) {
2023-02-05 07:03:18 +01:00
return newPolygon ( ) . addPoints ( newp . map ( p => p . clone ( ) ) ) . setOpenValue ( this . open ) ;
2022-10-10 00:00:08 -04:00
}
return this ;
}
2023-03-12 14:01:26 -04:00
midpoints ( dist = 0.01 ) {
const newp = [ ] ;
const points = this . points ;
const length = points . length ;
const l0 = this . open ? length - 1 : length ;
let mod = 0 ;
for ( let i = 0 , p = points ; i < l0 ; i ++ ) {
const p1 = p [ i ] ;
const p2 = p [ ( i + 1 ) % length ] ;
const dx = p2 . x - p1 . x ;
const dy = p2 . y - p1 . y ;
const ln = Math . sqrt ( dx * dx + dy * dy ) ;
if ( ln < dist ) {
newp . push ( p1 . midPointTo ( p2 ) ) ;
mod ++ ;
} else {
newp . push ( p1 ) ;
}
}
if ( mod ) {
return newPolygon ( ) . addPoints ( newp . map ( p => p . clone ( ) ) ) . setOpenValue ( this . open ) ;
}
return this ;
}
2022-01-31 20:50:19 -05:00
}
// use Slope.angleDiff() then re-test path mitering / rendering
function slopeDiff ( s1 , s2 ) {
const n1 = s1 . angle ;
const n2 = s2 . angle ;
let diff = n2 - n1 ;
while ( diff < - 180 ) diff += 360 ;
while ( diff > 180 ) diff -= 360 ;
return Math . abs ( diff ) ;
}
function fromClipperPath ( path , z ) {
let poly = newPolygon ( ) ,
i = 0 ,
l = path . length ;
while ( i < l ) {
// poly.push(newPoint(null,null,z,null,path[i++]));
poly . push ( base . pointFromClipper ( path [ i ++ ] , z ) ) ;
}
return poly ;
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
function newPolygon ( points ) {
return new Polygon ( points ) ;
}
2017-04-18 17:09:33 -04:00
2022-01-31 20:50:19 -05:00
Polygon . fromArray = function ( array ) {
return newPolygon ( ) . fromArray ( array ) ;
} ;
2017-04-18 17:09:33 -04:00
2022-02-20 14:31:27 -05:00
gapp . overlay ( base , {
Polygon ,
newPolygon
} ) ;
} ) ;