mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-06 16:33:15 +01:00
2096 lines
68 KiB
JavaScript
2096 lines
68 KiB
JavaScript
// pnltri.js / raw.github.com/jahting/pnltri.js/master/LICENSE
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*
|
|
* (Simple) Polygon Near-Linear Triangulation
|
|
* with fast ear-clipping for polygons without holes
|
|
*
|
|
*/
|
|
|
|
var PNLTRI = { REVISION: '2.1.1' };
|
|
|
|
// ##### Global Constants #####
|
|
|
|
|
|
// ##### Global Variables #####
|
|
|
|
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*/
|
|
|
|
PNLTRI.Math = {
|
|
|
|
random: Math.random, // function to use for random number generation
|
|
|
|
// generate random ordering in place:
|
|
// Fisher-Yates shuffle
|
|
array_shuffle: function( inoutArray ) {
|
|
for (var i = inoutArray.length - 1; i > 0; i-- ) {
|
|
var j = Math.floor( PNLTRI.Math.random() * (i+1) );
|
|
var tmp = inoutArray[i];
|
|
inoutArray[i] = inoutArray[j];
|
|
inoutArray[j] = tmp;
|
|
}
|
|
return inoutArray;
|
|
},
|
|
|
|
|
|
// like compare (<=>)
|
|
// yA > yB resp. xA > xB: 1, equal: 0, otherwise: -1
|
|
compare_pts_yx: function ( inPtA, inPtB ) {
|
|
var deltaY = inPtA.y - inPtB.y;
|
|
if ( deltaY < PNLTRI.Math.EPSILON_N ) {
|
|
return -1;
|
|
} else if ( deltaY > PNLTRI.Math.EPSILON_P ) {
|
|
return 1;
|
|
} else {
|
|
var deltaX = inPtA.x - inPtB.x;
|
|
if ( deltaX < PNLTRI.Math.EPSILON_N ) {
|
|
return -1;
|
|
} else if ( deltaX > PNLTRI.Math.EPSILON_P ) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
ptsCrossProd: function ( inPtVertex, inPtFrom, inPtTo ) {
|
|
// two vectors: ( v0: inPtVertex -> inPtFrom ), ( v1: inPtVertex -> inPtTo )
|
|
// CROSS_SINE: sin(theta) * len(v0) * len(v1)
|
|
return ( inPtFrom.x - inPtVertex.x ) * ( inPtTo.y - inPtVertex.y ) -
|
|
( inPtFrom.y - inPtVertex.y ) * ( inPtTo.x - inPtVertex.x );
|
|
// <=> crossProd( inPtFrom-inPtVertex, inPtTo-inPtVertex )
|
|
// == 0: colinear (angle == 0 or 180 deg == PI rad)
|
|
// > 0: v1 lies left of v0, CCW angle from v0 to v1 is convex ( < 180 deg )
|
|
// < 0: v1 lies right of v0, CW angle from v0 to v1 is convex ( < 180 deg )
|
|
},
|
|
|
|
};
|
|
|
|
// precision of floating point arithmetic
|
|
// PNLTRI.Math.EPSILON_P = Math.pow(2,-32); // ~ 0.0000000001
|
|
PNLTRI.Math.EPSILON_P = Math.pow(2,-43); // ~ 0.0000000000001
|
|
PNLTRI.Math.EPSILON_N = -PNLTRI.Math.EPSILON_P;
|
|
|
|
// Problem with EPSILON-compares:
|
|
// - especially when there is a x-coordinate ordering on equal y-coordinates
|
|
// => either NO EPSILON-compares on y-coordinates, since almost equal y
|
|
// can have very different x - so they are not nearly close
|
|
// or EPSILON must be bigger: Solution so far.
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.PolygonData = function ( inPolygonChainList ) {
|
|
|
|
// list of polygon vertices
|
|
// .x, .y: coordinates
|
|
this.vertices = [];
|
|
|
|
// list of polygon segments, original polygons ane holes
|
|
// and additional ones added during the subdivision into
|
|
// uni-y-monotone polygons (s. this.monoSubPolyChains)
|
|
// doubly linked by: snext, sprev
|
|
this.segments = [];
|
|
this.diagonals = [];
|
|
|
|
// for the ORIGINAL polygon chains
|
|
this.idNextPolyChain = 0;
|
|
// for each original chain: lies the polygon inside to the left?
|
|
// "true": winding order is CCW for a contour or CW for a hole
|
|
// "false": winding order is CW for a contour or CCW for a hole
|
|
this.PolyLeftArr = [];
|
|
|
|
// indices into this.segments: at least one for each monoton chain for the polygon
|
|
// these subdivide the polygon into uni-y-monotone polygons, that is
|
|
// polygons that have only one segment between ymax and ymin on one side
|
|
// and the other side has monotone increasing y from ymin to ymax
|
|
// the monoSubPolyChains are doubly linked by: mnext, mprev
|
|
this.monoSubPolyChains = [];
|
|
|
|
// list of triangles: each 3 indices into this.vertices
|
|
this.triangles = [];
|
|
|
|
// initialize optional polygon chains
|
|
if ( inPolygonChainList ) {
|
|
for (var i=0, j=inPolygonChainList.length; i<j; i++) {
|
|
this.addPolygonChain( inPolygonChainList[i] );
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
|
|
PNLTRI.PolygonData.prototype = {
|
|
|
|
constructor: PNLTRI.PolygonData,
|
|
|
|
|
|
/* Accessors */
|
|
|
|
nbVertices: function () {
|
|
return this.vertices.length;
|
|
},
|
|
getSegments: function () {
|
|
return this.segments;
|
|
},
|
|
getFirstSegment: function () {
|
|
return this.segments[0];
|
|
},
|
|
getMonoSubPolys: function () {
|
|
return this.monoSubPolyChains;
|
|
},
|
|
getTriangles: function () {
|
|
return this.triangles.concat();
|
|
},
|
|
|
|
nbPolyChains: function () {
|
|
return this.idNextPolyChain;
|
|
},
|
|
|
|
// for the polygon data AFTER triangulation
|
|
// returns an Array of flags, one flag for each polygon chain:
|
|
// lies the inside of the polygon to the left?
|
|
// "true" implies CCW for contours and CW for holes
|
|
get_PolyLeftArr: function () {
|
|
return this.PolyLeftArr.concat();
|
|
},
|
|
set_PolyLeft_wrong: function ( inChainId ) {
|
|
this.PolyLeftArr[inChainId] = false;
|
|
},
|
|
|
|
|
|
/* Helper */
|
|
|
|
// checks winding order by calculating the area of the polygon
|
|
isClockWise: function ( inStartSeg ) {
|
|
var cursor = inStartSeg, doubleArea = 0;
|
|
do {
|
|
doubleArea += ( cursor.vFrom.x - cursor.vTo.x ) * ( cursor.vFrom.y + cursor.vTo.y );
|
|
cursor = cursor.snext;
|
|
} while ( cursor != inStartSeg );
|
|
return ( doubleArea < 0 );
|
|
},
|
|
|
|
|
|
/* Operations */
|
|
|
|
appendVertexEntry: function ( inVertexX, inVertexY ) { // private
|
|
var vertex = {
|
|
id: this.vertices.length, // vertex id, representing input sequence
|
|
x: inVertexX, // coordinates
|
|
y: inVertexY,
|
|
};
|
|
this.vertices.push( vertex );
|
|
return vertex;
|
|
},
|
|
|
|
|
|
createSegmentEntry: function ( inVertexFrom, inVertexTo ) { // private
|
|
return {
|
|
chainId: this.idNextPolyChain,
|
|
// end points of segment
|
|
vFrom: inVertexFrom, // -> start point entry in vertices
|
|
vTo: inVertexTo, // -> end point entry in vertices
|
|
// upward segment? (i.e. vTo > vFrom) !!! only valid for sprev,snext NOT for mprev,mnext !!!
|
|
upward: ( PNLTRI.Math.compare_pts_yx(inVertexTo, inVertexFrom) == 1 ),
|
|
// doubly linked list of original polygon chains (not the monoChains !)
|
|
sprev: null, // previous segment
|
|
snext: null, // next segment
|
|
//
|
|
// for performance reasons:
|
|
// initialization of all fields added later
|
|
//
|
|
// for trapezoids
|
|
rootFrom: null, // root of partial tree where vFrom is located
|
|
rootTo: null, // root of partial tree where vTo is located
|
|
is_inserted: false, // already inserted into QueryStructure ?
|
|
// for assigning depth: trapezoids
|
|
trLeft: null, // one trapezoid bordering on the left of this segment
|
|
trRight: null, // one trapezoid bordering on the right of this segment
|
|
// for monochains
|
|
mprev: null, // doubly linked list for monotone chains (sub-polygons)
|
|
mnext: null,
|
|
marked: false, // already visited during unique monoChain identification ?
|
|
};
|
|
},
|
|
|
|
appendSegmentEntry: function ( inSegment ) { // private
|
|
this.segments.push( inSegment );
|
|
return inSegment;
|
|
},
|
|
|
|
|
|
appendDiagonalsEntry: function ( inDiagonal ) { // <<<<< public
|
|
this.diagonals.push( inDiagonal );
|
|
return inDiagonal;
|
|
},
|
|
|
|
|
|
addVertexChain: function ( inRawPointList ) { // private
|
|
|
|
function verts_equal( inVert1, inVert2 ) {
|
|
return ( ( Math.abs(inVert1.x - inVert2.x) < PNLTRI.Math.EPSILON_P ) &&
|
|
( Math.abs(inVert1.y - inVert2.y) < PNLTRI.Math.EPSILON_P ) );
|
|
}
|
|
|
|
function verts_colinear_chain( inVert1, inVert2, inVert3 ) {
|
|
if ( Math.abs( PNLTRI.Math.ptsCrossProd( inVert2, inVert1, inVert3 ) ) > PNLTRI.Math.EPSILON_P ) return false;
|
|
// only real sequences, not direction reversals
|
|
var low, middle, high;
|
|
if ( Math.abs( inVert1.y - inVert2.y ) < PNLTRI.Math.EPSILON_P ) {
|
|
// horizontal line
|
|
middle = inVert2.x;
|
|
if ( inVert1.x < inVert3.x ) {
|
|
low = inVert1.x;
|
|
high = inVert3.x;
|
|
} else {
|
|
low = inVert3.x;
|
|
high = inVert1.x;
|
|
}
|
|
} else {
|
|
middle = inVert2.y;
|
|
if ( inVert1.y < inVert3.y ) {
|
|
low = inVert1.y;
|
|
high = inVert3.y;
|
|
} else {
|
|
low = inVert3.y;
|
|
high = inVert1.y;
|
|
}
|
|
}
|
|
return ( ( ( low - middle ) < PNLTRI.Math.EPSILON_P ) && ( ( middle - high ) < PNLTRI.Math.EPSILON_P ) );
|
|
}
|
|
|
|
var newVertices = [];
|
|
var newVertex, acceptVertex, lastIdx;
|
|
for ( var i=0; i < inRawPointList.length; i++ ) {
|
|
newVertex = this.appendVertexEntry( inRawPointList[i].x, inRawPointList[i].y );
|
|
// suppresses zero-length segments
|
|
acceptVertex = true;
|
|
lastIdx = newVertices.length-1;
|
|
if ( lastIdx >= 0 ) {
|
|
if ( verts_equal( newVertex, newVertices[lastIdx] ) ) {
|
|
acceptVertex = false;
|
|
} else if ( lastIdx > 0 ) {
|
|
if ( verts_colinear_chain( newVertices[lastIdx-1], newVertices[lastIdx], newVertex ) ) {
|
|
newVertices.pop();
|
|
}
|
|
}
|
|
}
|
|
if ( acceptVertex ) newVertices.push( newVertex );
|
|
}
|
|
// compare last vertices to first: suppresses zero-length and co-linear segments
|
|
lastIdx = newVertices.length - 1;
|
|
if ( ( lastIdx > 0 ) &&
|
|
verts_equal( newVertices[lastIdx], newVertices[0] ) ) {
|
|
newVertices.pop();
|
|
lastIdx--;
|
|
}
|
|
if ( lastIdx > 1 ) {
|
|
if ( verts_colinear_chain( newVertices[lastIdx-1], newVertices[lastIdx], newVertices[0] ) ) {
|
|
newVertices.pop();
|
|
lastIdx--;
|
|
}
|
|
if ( ( lastIdx > 1 ) &&
|
|
verts_colinear_chain( newVertices[lastIdx], newVertices[0], newVertices[1] ) ) {
|
|
newVertices.shift();
|
|
}
|
|
}
|
|
|
|
return newVertices;
|
|
},
|
|
|
|
|
|
addPolygonChain: function ( inRawPointList ) { // <<<<<< public
|
|
|
|
// vertices
|
|
var newVertices = this.addVertexChain( inRawPointList );
|
|
if ( newVertices.length < 3 ) {
|
|
console.log( "Polygon has < 3 vertices!", newVertices );
|
|
return 0;
|
|
}
|
|
|
|
// segments
|
|
var saveSegListLength = this.segments.length;
|
|
//
|
|
var segment, firstSeg, prevSeg;
|
|
for ( var i=0; i < newVertices.length-1; i++ ) {
|
|
segment = this.createSegmentEntry( newVertices[i], newVertices[i+1] );
|
|
if (prevSeg) {
|
|
segment.sprev = prevSeg;
|
|
prevSeg.snext = segment;
|
|
} else {
|
|
firstSeg = segment;
|
|
}
|
|
prevSeg = segment;
|
|
this.appendSegmentEntry( segment );
|
|
}
|
|
// close polygon
|
|
segment = this.createSegmentEntry( newVertices[newVertices.length-1], newVertices[0] );
|
|
segment.sprev = prevSeg;
|
|
prevSeg.snext = segment;
|
|
this.appendSegmentEntry( segment );
|
|
firstSeg.sprev = segment;
|
|
segment.snext = firstSeg;
|
|
|
|
this.PolyLeftArr[this.idNextPolyChain++] = true;
|
|
return this.segments.length - saveSegListLength;
|
|
},
|
|
|
|
|
|
/* Monotone Polygon Chains */
|
|
|
|
// Generate the uni-y-monotone sub-polygons from
|
|
// the trapezoidation of the polygon.
|
|
|
|
create_mono_chains: function () { // <<<<<< public
|
|
var newMono, newMonoTo, toFirstOutSeg, fromRevSeg;
|
|
for ( var i = 0, j = this.segments.length; i < j; i++) {
|
|
newMono = this.segments[i];
|
|
if ( this.PolyLeftArr[newMono.chainId] ) {
|
|
// preserve winding order
|
|
newMonoTo = newMono.vTo; // target of segment
|
|
newMono.mprev = newMono.sprev; // doubly linked list for monotone chains (sub-polygons)
|
|
newMono.mnext = newMono.snext;
|
|
} else {
|
|
// reverse winding order
|
|
newMonoTo = newMono.vFrom;
|
|
newMono = newMono.snext;
|
|
newMono.mprev = newMono.snext;
|
|
newMono.mnext = newMono.sprev;
|
|
}
|
|
if ( fromRevSeg = newMono.vFrom.lastInDiag ) { // assignment !
|
|
fromRevSeg.mnext = newMono;
|
|
newMono.mprev = fromRevSeg;
|
|
newMono.vFrom.lastInDiag = null; // cleanup
|
|
}
|
|
if ( toFirstOutSeg = newMonoTo.firstOutDiag ) { // assignment !
|
|
toFirstOutSeg.mprev = newMono;
|
|
newMono.mnext = toFirstOutSeg;
|
|
newMonoTo.firstOutDiag = null; // cleanup
|
|
}
|
|
}
|
|
},
|
|
|
|
// For each monotone polygon, find the ymax (to determine the two
|
|
// y-monotone chains) and skip duplicate monotone polygons
|
|
|
|
unique_monotone_chains_max: function () { // <<<<<< public
|
|
|
|
function find_monotone_chain_max( frontMono ) {
|
|
var frontPt, firstPt, ymaxPt;
|
|
|
|
var monoPosmax = frontMono;
|
|
firstPt = ymaxPt = frontMono.vFrom;
|
|
|
|
frontMono.marked = true;
|
|
frontMono = frontMono.mnext;
|
|
while ( frontPt = frontMono.vFrom ) { // assignment !
|
|
if (frontMono.marked) {
|
|
if ( frontPt == firstPt ) break; // mono chain completed
|
|
console.log("ERR unique_monotone: segment in two chains", firstPt, frontMono );
|
|
return null;
|
|
} else {
|
|
/* if ( frontPt == firstPt ) { // check for robustness
|
|
console.log("ERR unique_monotone: point double", firstPt, frontMono );
|
|
} */
|
|
frontMono.marked = true;
|
|
}
|
|
if ( PNLTRI.Math.compare_pts_yx( frontPt, ymaxPt ) == 1 ) {
|
|
ymaxPt = frontPt;
|
|
monoPosmax = frontMono;
|
|
}
|
|
frontMono = frontMono.mnext;
|
|
}
|
|
return monoPosmax;
|
|
}
|
|
|
|
var frontMono, monoPosmax;
|
|
|
|
// assumes attribute "marked" is NOT yet "true" for any mono chain segment
|
|
this.monoSubPolyChains = [];
|
|
// loop through all original segments
|
|
for ( var i = 0, j = this.segments.length; i < j; i++ ) {
|
|
frontMono = this.segments[i];
|
|
if ( frontMono.marked ) continue; // already in a processed mono chain
|
|
monoPosmax = find_monotone_chain_max( frontMono );
|
|
if ( monoPosmax ) this.monoSubPolyChains.push( monoPosmax );
|
|
}
|
|
// loop through all additional segments (diagonals) // TODO: Testcase for mono chain without original segments !!!
|
|
/* for ( var i = 0, j = this.diagonals.length; i < j; i++ ) {
|
|
frontMono = this.diagonals[i];
|
|
if ( frontMono.marked ) continue; // already in a processed mono chain
|
|
monoPosmax = find_monotone_chain_max( frontMono );
|
|
if ( monoPosmax ) this.monoSubPolyChains.push( monoPosmax );
|
|
} */
|
|
return this.monoSubPolyChains;
|
|
},
|
|
|
|
|
|
/* Triangles */
|
|
|
|
clearTriangles: function () {
|
|
this.triangles = [];
|
|
},
|
|
|
|
addTriangle: function ( inVert1, inVert2, inVert3 ) {
|
|
this.triangles.push( [ inVert1.id, inVert2.id, inVert3.id ] );
|
|
},
|
|
|
|
};
|
|
|
|
/**
|
|
* Simple Polygon Triangulation by Ear Clipping
|
|
*
|
|
* description of technique employed:
|
|
* http://www.siggraph.org/education/materials/HyperGraph/scanline/outprims/polygon1.htm
|
|
*
|
|
* This code is a quick port of code written in C++ which was submitted to
|
|
* flipcode.com by John W. Ratcliff // July 22, 2000
|
|
* See original code and more information here:
|
|
* http://www.flipcode.com/archives/Efficient_Polygon_Triangulation.shtml
|
|
*
|
|
* ported to actionscript by Zevan Rosser
|
|
* http://actionsnippet.com/?p=1462
|
|
*
|
|
* ported to javascript by Joshua Koo
|
|
* http://www.lab4games.net/zz85/blog
|
|
*
|
|
* adapted to doubly linked list by Juergen Ahting
|
|
* http://www.ameco.tv
|
|
*
|
|
*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.EarClipTriangulator = function ( inPolygonData ) {
|
|
|
|
this.polyData = inPolygonData;
|
|
|
|
};
|
|
|
|
|
|
PNLTRI.EarClipTriangulator.prototype = {
|
|
|
|
constructor: PNLTRI.EarClipTriangulator,
|
|
|
|
|
|
// triangulates first doubly linked segment list in this.polyData
|
|
// algorithm uses ear-clipping and runs in O(n^2) time
|
|
|
|
triangulate_polygon_no_holes: function () {
|
|
|
|
function isEarAt( vertex ) {
|
|
|
|
var prevX = vertex.mprev.vFrom.x;
|
|
var prevY = vertex.mprev.vFrom.y;
|
|
|
|
var vertX = vertex.vFrom.x;
|
|
var vertY = vertex.vFrom.y;
|
|
|
|
var nextX = vertex.mnext.vFrom.x;
|
|
var nextY = vertex.mnext.vFrom.y;
|
|
|
|
var vnX = nextX - vertX, vnY = nextY - vertY;
|
|
var npX = prevX - nextX, npY = prevY - nextY;
|
|
var pvX = vertX - prevX, pvY = vertY - prevY;
|
|
|
|
// concave angle at vertex -> not an ear to cut off
|
|
if ( PNLTRI.Math.EPSILON_P > ( ( pvX * vnY ) - ( vnX * pvY ) ) ) return false;
|
|
|
|
// check whether any other point lieas within the triangle abc
|
|
var vStop = vertex.mprev.mprev;
|
|
var vOther = vertex.mnext;
|
|
while ( vOther != vStop ) {
|
|
vOther = vOther.mnext;
|
|
var otherX = vOther.vFrom.x;
|
|
var otherY = vOther.vFrom.y;
|
|
|
|
var poX = otherX - prevX, poY = otherY - prevY;
|
|
// just in case there are several vertices with the same coordinate
|
|
if ( ( poX === 0 ) && ( poY === 0 ) ) continue; // vOther == vertex.mprev
|
|
var voX = otherX - vertX, voY = otherY - vertY;
|
|
if ( ( voX === 0 ) && ( voY === 0 ) ) continue; // vOther == vertex
|
|
var noX = otherX - nextX, noY = otherY - nextY;
|
|
if ( ( noX === 0 ) && ( noY === 0 ) ) continue; // vOther == vertex.mnext
|
|
|
|
// if vOther is inside triangle abc -> not an ear to cut off
|
|
if ( ( ( vnX * voY - vnY * voX ) >= PNLTRI.Math.EPSILON_N ) &&
|
|
( ( pvX * poY - pvY * poX ) >= PNLTRI.Math.EPSILON_N ) &&
|
|
( ( npX * noY - npY * noX ) >= PNLTRI.Math.EPSILON_N ) ) return false;
|
|
}
|
|
return true;
|
|
|
|
}
|
|
|
|
var myPolyData = this.polyData;
|
|
var startSeg = myPolyData.getFirstSegment();
|
|
|
|
// create a counter-clockwise ordered doubly linked list (monoChain links)
|
|
|
|
var cursor = startSeg;
|
|
if ( myPolyData.isClockWise( startSeg ) ) {
|
|
do { // reverses chain order
|
|
cursor.mprev = cursor.snext;
|
|
cursor.mnext = cursor.sprev;
|
|
cursor = cursor.sprev;
|
|
} while ( cursor != startSeg );
|
|
myPolyData.set_PolyLeft_wrong(0);
|
|
} else {
|
|
do {
|
|
cursor.mprev = cursor.sprev;
|
|
cursor.mnext = cursor.snext;
|
|
cursor = cursor.snext;
|
|
} while ( cursor != startSeg );
|
|
}
|
|
|
|
// remove all vertices except 2, creating 1 triangle every time
|
|
|
|
var vertex = startSeg;
|
|
var fullLoop = vertex; // prevent infinite loop on "defective" polygons
|
|
|
|
while ( vertex.mnext != vertex.mprev ) {
|
|
if ( isEarAt( vertex ) ) {
|
|
// found a triangle ear to cut off
|
|
this.polyData.addTriangle( vertex.mprev.vFrom, vertex.vFrom, vertex.mnext.vFrom );
|
|
// remove vertex from the remaining chain
|
|
vertex.mprev.mnext = vertex.mnext;
|
|
vertex.mnext.mprev = vertex.mprev;
|
|
vertex = vertex.mnext;
|
|
fullLoop = vertex; // reset error detection
|
|
} else {
|
|
vertex = vertex.mnext;
|
|
// loop?: probably non-simple polygon -> stop with error
|
|
if ( vertex == fullLoop ) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
/* // takes one element of a double linked segment list
|
|
// works on array of vertices
|
|
|
|
triangulate_polygon_no_holes: function () {
|
|
var startSeg = this.polyData.getFirstSegment();
|
|
|
|
function vertList( inStartSeg ) {
|
|
var verts = [];
|
|
// we want a counter-clockwise polygon in verts
|
|
var doubleArea = 0.0;
|
|
var cursor = inStartSeg;
|
|
var p,q;
|
|
var idx = 0;
|
|
do {
|
|
p = cursor.sprev.vFrom;
|
|
q = cursor.vFrom;
|
|
doubleArea += p.x * q.y - q.x * p.y;
|
|
verts[idx++] = q;
|
|
cursor = cursor.snext;
|
|
} while ( cursor != inStartSeg );
|
|
if ( doubleArea < 0.0 ) {
|
|
verts = verts.reverse();
|
|
var tmp = verts.pop();
|
|
verts.unshift( tmp );
|
|
}
|
|
return verts;
|
|
}
|
|
|
|
function snip( verts, u, v, w, n ) {
|
|
|
|
var ax = verts[ u ].x;
|
|
var ay = verts[ u ].y;
|
|
|
|
var bx = verts[ v ].x;
|
|
var by = verts[ v ].y;
|
|
|
|
var cx = verts[ w ].x;
|
|
var cy = verts[ w ].y;
|
|
|
|
if ( PNLTRI.Math.EPSILON_P > ( ( bx - ax ) * ( cy - ay ) - ( by - ay ) * ( cx - ax ) ) ) return false;
|
|
|
|
var aX, aY, bX, bY, cX, cY;
|
|
|
|
aX = cx - bx; aY = cy - by;
|
|
bX = ax - cx; bY = ay - cy;
|
|
cX = bx - ax; cY = by - ay;
|
|
|
|
var p, px, py;
|
|
|
|
var apx, apy, bpx, bpy, cpx, cpy;
|
|
var cCROSSap, bCROSScp, aCROSSbp;
|
|
|
|
for ( p = 0; p < n; p ++ ) {
|
|
|
|
px = verts[ p ].x
|
|
py = verts[ p ].y
|
|
|
|
apx = px - ax; apy = py - ay;
|
|
if ( ( apx == 0 ) && ( apy == 0 ) ) continue;
|
|
bpx = px - bx; bpy = py - by;
|
|
if ( ( bpx == 0 ) && ( bpy == 0 ) ) continue;
|
|
cpx = px - cx; cpy = py - cy;
|
|
if ( ( cpx == 0 ) && ( cpy == 0 ) ) continue;
|
|
|
|
// see if p is inside triangle abc
|
|
|
|
aCROSSbp = aX * bpy - aY * bpx;
|
|
cCROSSap = cX * apy - cY * apx;
|
|
bCROSScp = bX * cpy - bY * cpx;
|
|
|
|
if ( ( aCROSSbp >= PNLTRI.Math.EPSILON_N ) &&
|
|
( bCROSScp >= PNLTRI.Math.EPSILON_N ) &&
|
|
( cCROSSap >= PNLTRI.Math.EPSILON_N ) ) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
var result = [];
|
|
|
|
var verts = vertList( startSeg );
|
|
|
|
var n = verts.length;
|
|
var nv = n;
|
|
|
|
var u, v, w;
|
|
|
|
// remove nv - 2 vertices, creating 1 triangle every time
|
|
|
|
var count = 2 * nv; // error detection
|
|
|
|
for ( v = nv - 1; nv > 2; ) {
|
|
|
|
// if we loop, it is probably a non-simple polygon
|
|
|
|
if ( ( count -- ) <= 0 ) return false;
|
|
|
|
// three consecutive vertices in current polygon, <u,v,w>
|
|
|
|
u = v; if ( nv <= u ) u = 0; // previous
|
|
v = u + 1; if ( nv <= v ) v = 0; // new v
|
|
w = v + 1; if ( nv <= w ) w = 0; // next
|
|
|
|
if ( snip( verts, u, v, w, nv ) ) {
|
|
|
|
// output Triangle
|
|
|
|
this.polyData.addTriangle( verts[ u ], verts[ v ], verts[ w ] );
|
|
|
|
// remove v from the remaining polygon
|
|
|
|
var s, t;
|
|
|
|
for ( s = v, t = v + 1; t < nv; s++, t++ ) {
|
|
|
|
verts[ s ] = verts[ t ];
|
|
|
|
}
|
|
|
|
nv --;
|
|
|
|
v --;
|
|
if ( v < 0 ) v = nv-1;
|
|
|
|
// reset error detection counter
|
|
|
|
count = 2 * nv;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}, */
|
|
|
|
};
|
|
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*
|
|
* Algorithm to create the trapezoidation of a polygon with holes
|
|
* according to Seidel's algorithm [Sei91]
|
|
*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.Trapezoid = function ( inHigh, inLow, inLeft, inRight ) {
|
|
|
|
this.vHigh = inHigh ? inHigh : { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY };
|
|
this.vLow = inLow ? inLow : { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY };
|
|
|
|
this.lseg = inLeft;
|
|
this.rseg = inRight;
|
|
|
|
// this.uL = null; // -> Trapezoid: upper left neighbor
|
|
// this.uR = null; // -> Trapezoid: upper right neighbor
|
|
// this.dL = null; // -> Trapezoid: lower left neighbor
|
|
// this.dR = null; // -> Trapezoid: lower right neighbor
|
|
|
|
// this.sink = null; // link to corresponding SINK-Node in QueryStructure
|
|
|
|
// this.usave = null; // temp: uL/uR, preserved for next step
|
|
// this.uleft = null; // temp: from uL? (true) or uR (false)
|
|
|
|
this.depth = -1; // no depth assigned yet
|
|
|
|
this.monoDone = false; // monotonization: done with trying to split this trapezoid ?
|
|
|
|
};
|
|
|
|
PNLTRI.Trapezoid.prototype = {
|
|
|
|
constructor: PNLTRI.Trapezoid,
|
|
|
|
clone: function () {
|
|
var newTrap = new PNLTRI.Trapezoid( this.vHigh, this.vLow, this.lseg, this.rseg );
|
|
|
|
newTrap.uL = this.uL;
|
|
newTrap.uR = this.uR;
|
|
|
|
newTrap.dL = this.dL;
|
|
newTrap.dR = this.dR;
|
|
|
|
newTrap.sink = this.sink;
|
|
|
|
return newTrap;
|
|
},
|
|
|
|
|
|
splitOffLower: function ( inSplitPt ) {
|
|
var trLower = this.clone(); // new lower trapezoid
|
|
|
|
this.vLow = trLower.vHigh = inSplitPt;
|
|
|
|
// L/R unknown, anyway changed later
|
|
this.dL = trLower; // setBelow
|
|
trLower.uL = this; // setAbove
|
|
this.dR = trLower.uR = null;
|
|
|
|
// setAbove
|
|
if ( trLower.dL ) trLower.dL.uL = trLower; // dL always connects to uL
|
|
if ( trLower.dR ) trLower.dR.uR = trLower; // dR always connects to uR
|
|
|
|
return trLower;
|
|
},
|
|
|
|
};
|
|
|
|
|
|
/*==============================================================================
|
|
*
|
|
*============================================================================*/
|
|
|
|
// PNLTRI.qsCounter = 0;
|
|
|
|
/** @constructor */
|
|
PNLTRI.QsNode = function ( inTrapezoid ) {
|
|
// this.qsId = PNLTRI.qsCounter++; // Debug only
|
|
// Only SINK-nodes are created directly.
|
|
// The others originate from splitting trapezoids
|
|
// - by a horizontal line: SINK-Node -> Y-Node
|
|
// - by a segment: SINK-Node -> X-Node
|
|
this.trap = inTrapezoid;
|
|
inTrapezoid.sink = this;
|
|
};
|
|
|
|
PNLTRI.QsNode.prototype = {
|
|
|
|
constructor: PNLTRI.QsNode,
|
|
|
|
};
|
|
|
|
/*==============================================================================
|
|
*
|
|
*============================================================================*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.QueryStructure = function ( inPolygonData ) {
|
|
// initialise the query structure and trapezoid list
|
|
var initialTrap = new PNLTRI.Trapezoid( null, null, null, null );
|
|
this.trapArray = [];
|
|
this.appendTrapEntry( initialTrap );
|
|
|
|
// PNLTRI.qsCounter = 0;
|
|
this.root = new PNLTRI.QsNode( initialTrap );
|
|
|
|
if ( inPolygonData ) {
|
|
/*
|
|
* adds and initializes specific attributes for all segments
|
|
* // -> QueryStructure: roots of partial tree where vertex is located
|
|
* rootFrom, rootTo: for vFrom, vTo
|
|
* // marker
|
|
* is_inserted: already inserted into QueryStructure ?
|
|
*/
|
|
var segListArray = inPolygonData.getSegments();
|
|
for ( var i = 0; i < segListArray.length; i++ ) {
|
|
segListArray[i].rootFrom = segListArray[i].rootTo = this.root;
|
|
segListArray[i].is_inserted = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
PNLTRI.QueryStructure.prototype = {
|
|
|
|
constructor: PNLTRI.QueryStructure,
|
|
|
|
getRoot: function () {
|
|
return this.root;
|
|
},
|
|
|
|
|
|
appendTrapEntry: function ( inTrapezoid ) {
|
|
inTrapezoid.trapID = this.trapArray.length; // for Debug
|
|
this.trapArray.push( inTrapezoid );
|
|
},
|
|
cloneTrap: function ( inTrapezoid ) {
|
|
var trap = inTrapezoid.clone();
|
|
this.appendTrapEntry( trap );
|
|
return trap;
|
|
},
|
|
|
|
|
|
splitNodeAtPoint: function ( inNode, inPoint, inReturnUpper ) {
|
|
// inNode: SINK-Node with trapezoid containing inPoint
|
|
var trUpper = inNode.trap; // trUpper: trapezoid includes the point
|
|
if (trUpper.vHigh == inPoint) return inNode; // (ERROR) inPoint is already inserted
|
|
if (trUpper.vLow == inPoint) return inNode; // (ERROR) inPoint is already inserted
|
|
var trLower = trUpper.splitOffLower( inPoint ); // trLower: new lower trapezoid
|
|
this.appendTrapEntry( trLower );
|
|
|
|
// SINK-Node -> Y-Node
|
|
inNode.yval = inPoint;
|
|
inNode.trap = null;
|
|
|
|
inNode.right = new PNLTRI.QsNode( trUpper ); // Upper trapezoid sink
|
|
inNode.left = new PNLTRI.QsNode( trLower ); // Lower trapezoid sink
|
|
|
|
return inReturnUpper ? trUpper.sink : trLower.sink;
|
|
},
|
|
|
|
|
|
/*
|
|
* Mathematics & Geometry helper methods
|
|
*/
|
|
|
|
fpEqual: function ( inNum0, inNum1 ) {
|
|
return Math.abs( inNum0 - inNum1 ) < PNLTRI.Math.EPSILON_P;
|
|
},
|
|
|
|
|
|
// Checks, whether the vertex inPt is to the left of line segment inSeg.
|
|
// Returns:
|
|
// >0: inPt is left of inSeg,
|
|
// <0: inPt is right of inSeg,
|
|
// =0: inPt is co-linear with inSeg
|
|
//
|
|
// ATTENTION: always viewed from -y, not as if moving along the segment chain !!
|
|
|
|
is_left_of: function ( inSeg, inPt, inBetweenY ) {
|
|
var retVal;
|
|
var dXfrom = inSeg.vFrom.x - inPt.x;
|
|
var dXto = inSeg.vTo.x - inPt.x;
|
|
var dYfromZero = this.fpEqual( inSeg.vFrom.y, inPt.y );
|
|
if ( this.fpEqual( inSeg.vTo.y, inPt.y ) ) {
|
|
if ( dYfromZero ) return 0; // all points on a horizontal line
|
|
retVal = dXto;
|
|
} else if ( dYfromZero ) {
|
|
retVal = dXfrom;
|
|
/* } else if ( inBetweenY && ( dXfrom * dXto > 0 ) ) {
|
|
// both x-coordinates of inSeg are on the same side of inPt
|
|
if ( Math.abs( dXto ) >= PNLTRI.Math.EPSILON_P ) return dXto;
|
|
retVal = dXfrom; */
|
|
} else {
|
|
if ( inSeg.upward ) {
|
|
return PNLTRI.Math.ptsCrossProd( inSeg.vFrom, inSeg.vTo, inPt );
|
|
} else {
|
|
return PNLTRI.Math.ptsCrossProd( inSeg.vTo, inSeg.vFrom, inPt );
|
|
}
|
|
}
|
|
if ( Math.abs( retVal ) < PNLTRI.Math.EPSILON_P ) return 0;
|
|
return retVal;
|
|
},
|
|
|
|
|
|
/*
|
|
* Query structure main methods
|
|
*/
|
|
|
|
// This method finds the Nodes in the QueryStructure corresponding
|
|
// to the trapezoids that contain the endpoints of inSegment,
|
|
// starting from Nodes rootFrom/rootTo and replacing them with the results.
|
|
|
|
segNodes: function ( inSegment ) {
|
|
this.ptNode( inSegment, true );
|
|
this.ptNode( inSegment, false );
|
|
},
|
|
|
|
// TODO: may need to prevent infinite loop in case of messed up
|
|
// trapezoid structure (s. test_add_segment_special_6)
|
|
|
|
ptNode: function ( inSegment, inUseFrom ) {
|
|
var ptMain, ptOther, qsNode;
|
|
if ( inUseFrom ) {
|
|
ptMain = inSegment.vFrom;
|
|
ptOther = inSegment.vTo; // used if ptMain is not sufficient
|
|
qsNode = inSegment.rootFrom;
|
|
} else {
|
|
ptMain = inSegment.vTo;
|
|
ptOther = inSegment.vFrom;
|
|
qsNode = inSegment.rootTo;
|
|
}
|
|
var compPt, compRes;
|
|
var isInSegmentShorter;
|
|
|
|
while ( qsNode ) {
|
|
if ( qsNode.yval ) { // Y-Node: horizontal line
|
|
// 4 times as often as X-Node
|
|
qsNode = ( PNLTRI.Math.compare_pts_yx( ( ( ptMain == qsNode.yval ) ? // is the point already inserted ?
|
|
ptOther : ptMain ), qsNode.yval ) == -1 ) ?
|
|
qsNode.left : qsNode.right; // below : above
|
|
} else if ( qsNode.seg ) { // X-Node: segment (~vertical line)
|
|
// 0.8 to 1.5 times as often as SINK-Node
|
|
if ( ( ptMain == qsNode.seg.vFrom ) ||
|
|
( ptMain == qsNode.seg.vTo ) ) {
|
|
// the point is already inserted
|
|
if ( this.fpEqual( ptMain.y, ptOther.y ) ) {
|
|
// horizontal segment
|
|
if ( !this.fpEqual( qsNode.seg.vFrom.y, qsNode.seg.vTo.y ) ) {
|
|
qsNode = ( ptOther.x < ptMain.x ) ? qsNode.left : qsNode.right; // left : right
|
|
} else { // co-linear horizontal reversal: test_add_segment_special_7
|
|
if ( ptMain == qsNode.seg.vFrom ) {
|
|
// connected at qsNode.seg.vFrom
|
|
// console.log("ptNode: co-linear horizontal reversal, connected at qsNode.seg.vFrom", inUseFrom, inSegment, qsNode )
|
|
isInSegmentShorter = inSegment.upward ?
|
|
( ptOther.x >= qsNode.seg.vTo.x ) :
|
|
( ptOther.x < qsNode.seg.vTo.x );
|
|
qsNode = ( isInSegmentShorter ?
|
|
inSegment.sprev.upward :
|
|
qsNode.seg.snext.upward ) ? qsNode.right : qsNode.left; // above : below
|
|
} else {
|
|
// connected at qsNode.seg.vTo
|
|
// console.log("ptNode: co-linear horizontal reversal, connected at qsNode.seg.vTo", inUseFrom, inSegment, qsNode );
|
|
isInSegmentShorter = inSegment.upward ?
|
|
( ptOther.x < qsNode.seg.vFrom.x ) :
|
|
( ptOther.x >= qsNode.seg.vFrom.x );
|
|
qsNode = ( isInSegmentShorter ?
|
|
inSegment.snext.upward :
|
|
qsNode.seg.sprev.upward ) ? qsNode.left : qsNode.right; // below : above
|
|
}
|
|
}
|
|
continue;
|
|
} else {
|
|
compRes = this.is_left_of( qsNode.seg, ptOther, false );
|
|
if ( compRes === 0 ) {
|
|
// co-linear reversal (not horizontal)
|
|
// a co-linear continuation would not reach this point
|
|
// since the previous Y-node comparison would have led to a sink instead
|
|
// console.log("ptNode: co-linear, going back on previous segment", ptMain, ptOther, qsNode );
|
|
// now as we have two consecutive co-linear segments we have to avoid a cross-over
|
|
// for this we need the far point on the "next" segment to the SHORTER of our two
|
|
// segments to avoid that "next" segment to cross the longer of our two segments
|
|
if ( ptMain == qsNode.seg.vFrom ) {
|
|
// connected at qsNode.seg.vFrom
|
|
// console.log("ptNode: co-linear, going back on previous segment, connected at qsNode.seg.vFrom", ptMain, ptOther, qsNode );
|
|
isInSegmentShorter = inSegment.upward ?
|
|
( ptOther.y >= qsNode.seg.vTo.y ) :
|
|
( ptOther.y < qsNode.seg.vTo.y );
|
|
compRes = isInSegmentShorter ?
|
|
this.is_left_of( qsNode.seg, inSegment.sprev.vFrom, false ) :
|
|
-this.is_left_of( qsNode.seg, qsNode.seg.snext.vTo, false );
|
|
} else {
|
|
// connected at qsNode.seg.vTo
|
|
// console.log("ptNode: co-linear, going back on previous segment, connected at qsNode.seg.vTo", ptMain, ptOther, qsNode );
|
|
isInSegmentShorter = inSegment.upward ?
|
|
( ptOther.y < qsNode.seg.vFrom.y ) :
|
|
( ptOther.y >= qsNode.seg.vFrom.y );
|
|
compRes = isInSegmentShorter ?
|
|
this.is_left_of( qsNode.seg, inSegment.snext.vTo, false ) :
|
|
-this.is_left_of( qsNode.seg, qsNode.seg.sprev.vFrom, false );
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* if ( ( PNLTRI.Math.compare_pts_yx( ptMain, qsNode.seg.vFrom ) * // TODO: Testcase
|
|
PNLTRI.Math.compare_pts_yx( ptMain, qsNode.seg.vTo )
|
|
) == 0 ) {
|
|
console.log("ptNode: Pts too close together#2: ", ptMain, qsNode.seg );
|
|
} */
|
|
compRes = this.is_left_of( qsNode.seg, ptMain, true );
|
|
if ( compRes === 0 ) {
|
|
// touching: ptMain lies on qsNode.seg but is none of its endpoints
|
|
// should happen quite seldom
|
|
compRes = this.is_left_of( qsNode.seg, ptOther, false );
|
|
if ( compRes === 0 ) {
|
|
// co-linear: inSegment and qsNode.seg
|
|
// includes case with ptOther connected to qsNode.seg
|
|
var tmpPtOther = inUseFrom ? inSegment.sprev.vFrom : inSegment.snext.vTo;
|
|
compRes = this.is_left_of( qsNode.seg, tmpPtOther, false );
|
|
}
|
|
}
|
|
}
|
|
if ( compRes > 0 ) {
|
|
qsNode = qsNode.left;
|
|
} else if ( compRes < 0 ) {
|
|
qsNode = qsNode.right;
|
|
} else {
|
|
// ??? TODO - not reached with current tests
|
|
// possible at all ?
|
|
return qsNode;
|
|
// qsNode = qsNode.left; // left
|
|
// qsNode = qsNode.right; // right
|
|
}
|
|
} else { // SINK-Node: trapezoid area
|
|
// least often
|
|
if ( !qsNode.trap ) { console.log("ptNode: unknown type", qsNode); }
|
|
if ( inUseFrom ) { inSegment.rootFrom = qsNode; }
|
|
else { inSegment.rootTo = qsNode; }
|
|
return qsNode;
|
|
}
|
|
} // end while - should not exit here
|
|
},
|
|
|
|
|
|
// Add a new segment into the trapezoidation and update QueryStructure and Trapezoids
|
|
// 1) locates the two endpoints of the segment in the QueryStructure and inserts them
|
|
// 2) goes from the high-end trapezoid down to the low-end trapezoid
|
|
// changing all the trapezoids in between.
|
|
// Except for the high-end and low-end no new trapezoids are created.
|
|
// For all in between either:
|
|
// - the existing trapezoid is restricted to the left of the new segment
|
|
// and on the right side the trapezoid from above is extended downwards
|
|
// - or the other way round:
|
|
// the existing trapezoid is restricted to the right of the new segment
|
|
// and on the left side the trapezoid from above is extended downwards
|
|
|
|
add_segment: function ( inSegment ) {
|
|
var scope = this;
|
|
|
|
// functions handling the relationship to the upper neighbors (uL, uR)
|
|
// of trNewLeft and trNewRight
|
|
|
|
function fresh_seg_or_upward_cusp() {
|
|
// trCurrent has at most 1 upper neighbor
|
|
// and should also have at least 1, since the high-point trapezoid
|
|
// has been split off another one, which is now above
|
|
var trUpper = trCurrent.uL || trCurrent.uR;
|
|
|
|
// trNewLeft and trNewRight CANNOT have been extended from above
|
|
if ( trUpper.dL && trUpper.dR ) {
|
|
// upward cusp: top forms a triangle
|
|
|
|
// ATTENTION: the decision whether trNewLeft or trNewRight is the
|
|
// triangle trapezoid formed by the two segments has already been taken
|
|
// when selecting trCurrent as the left or right lower neighbor to trUpper !!
|
|
|
|
if ( trCurrent == trUpper.dL ) {
|
|
// *** Case: FUC_UC_LEFT; prev: ----
|
|
// console.log( "fresh_seg_or_upward_cusp: upward cusp, new seg to the left!" );
|
|
// upper
|
|
// -------*-------
|
|
// + \
|
|
// NL + \
|
|
// + NR \
|
|
// + \
|
|
trNewRight.uL = null; // setAbove; trNewRight.uR, trNewLeft unchanged
|
|
trUpper.dL = trNewLeft; // setBelow; dR: unchanged, NEVER null
|
|
} else {
|
|
// *** Case: FUC_UC_RIGHT; prev: ----
|
|
// console.log( "fresh_seg_or_upward_cusp: upward cusp, new seg from the right!" );
|
|
// upper
|
|
// -------*-------
|
|
// / +
|
|
// / + NR
|
|
// / NL +
|
|
// / +
|
|
trNewLeft.uR = null; // setAbove; trNewLeft.uL, trNewRight unchanged
|
|
trUpper.dR = trNewRight; // setBelow; dL: unchanged, NEVER null
|
|
}
|
|
} else {
|
|
// *** Case: FUC_FS; prev: "splitOffLower"
|
|
// console.log( "fresh_seg_or_upward_cusp: fresh segment, high adjacent segment still missing" );
|
|
// upper
|
|
// -------*-------
|
|
// +
|
|
// NL +
|
|
// + NR
|
|
// +
|
|
trNewRight.uL = null; // setAbove; trNewLeft unchanged, set by "splitOffLower"
|
|
trNewRight.uR = trUpper;
|
|
trUpper.dR = trNewRight; // setBelow; trUpper.dL unchanged, set by "splitOffLower"
|
|
}
|
|
}
|
|
|
|
function continue_chain_from_above() {
|
|
// trCurrent has at least 2 upper neighbors
|
|
if ( trCurrent.usave ) {
|
|
// 3 upper neighbors (part II)
|
|
if ( trCurrent.uleft ) {
|
|
// *** Case: CC_3UN_LEFT; prev: 1B_3UN_LEFT
|
|
// console.log( "continue_chain_from_above: 3 upper neighbors (part II): u0a, u0b, uR(usave)" );
|
|
// => left gets one, right gets two of the upper neighbors
|
|
// !! trNewRight cannot have been extended from above
|
|
// and trNewLeft must have been !!
|
|
// + /
|
|
// C.uL + C.uR / C.usave
|
|
// - - - -+----*----------
|
|
// NL + NR
|
|
trNewRight.uL = trCurrent.uR; // setAbove
|
|
trNewRight.uR = trCurrent.usave;
|
|
trNewRight.uL.dL = trNewRight; // setBelow; trNewRight.uL.dR == null, unchanged
|
|
trNewRight.uR.dR = trNewRight; // setBelow; trNewRight.uR.dL == null, unchanged
|
|
} else {
|
|
// *** Case: CC_3UN_RIGHT; prev: 1B_3UN_RIGHT
|
|
// console.log( "continue_chain_from_above: 3 upper neighbors (part II): uL(usave), u1a, u1b" );
|
|
// => left gets two, right gets one of the upper neighbors
|
|
// !! trNewLeft cannot have been extended from above
|
|
// and trNewRight must have been !!
|
|
// \ +
|
|
// C.usave \ C.uL + C.uR
|
|
// ---------*----+- - - -
|
|
// NL + NR
|
|
trNewLeft.uR = trCurrent.uL; // setAbove; first uR !!!
|
|
trNewLeft.uL = trCurrent.usave;
|
|
trNewLeft.uL.dL = trNewLeft; // setBelow; dR == null, unchanged
|
|
trNewLeft.uR.dR = trNewLeft; // setBelow; dL == null, unchanged
|
|
}
|
|
trNewLeft.usave = trNewRight.usave = null;
|
|
} else if ( trCurrent.vHigh == trFirst.vHigh ) { // && meetsHighAdjSeg ??? TODO
|
|
// *** Case: CC_2UN_CONN; prev: ----
|
|
// console.log( "continue_chain_from_above: 2 upper neighbors, fresh seg, continues high adjacent seg" );
|
|
// !! trNewLeft and trNewRight cannot have been extended from above !!
|
|
// C.uL / C.uR
|
|
// -------*---------
|
|
// NL + NR
|
|
trNewRight.uR.dR = trNewRight; // setBelow; dL == null, unchanged
|
|
trNewLeft.uR = trNewRight.uL = null; // setAbove; trNewLeft.uL, trNewRight.uR unchanged
|
|
} else {
|
|
// *** Case: CC_2UN; prev: 1B_1UN_CONT, 2B_NOCON_RIGHT/LEFT, 2B_TOUCH_RIGHT/LEFT, 2B_COLIN_RIGHT/LEFT
|
|
// console.log( "continue_chain_from_above: simple case, 2 upper neighbors (no usave, not fresh seg)" );
|
|
// !! trNewLeft XOR trNewRight will have been extended from above !!
|
|
// C.uL + C.uR
|
|
// -------+---------
|
|
// NL + NR
|
|
if ( trNewRight == trCurrent ) { // trNewLeft has been extended from above
|
|
// setAbove
|
|
trNewRight.uL = trNewRight.uR;
|
|
trNewRight.uR = null;
|
|
// setBelow; dR: unchanged, is NOT always null (prev: 2B_NOCON_LEFT, 2B_TOUCH_LEFT, 2B_COLIN_LEFT)
|
|
trNewRight.uL.dL = trNewRight;
|
|
} else { // trNewRight has been extended from above
|
|
trNewLeft.uR = trNewLeft.uL; // setAbove; first uR !!!
|
|
trNewLeft.uL = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// functions handling the relationship to the lower neighbors (dL, dR)
|
|
// of trNewLeft and trNewRight
|
|
// trNewLeft or trNewRight MIGHT have been extended from above
|
|
// !! in that case dL and dR are different from trCurrent and MUST be set here !!
|
|
|
|
function only_one_trap_below( inTrNext ) {
|
|
|
|
if ( trCurrent.vLow == trLast.vLow ) {
|
|
// final part of segment
|
|
|
|
if ( meetsLowAdjSeg ) {
|
|
// downward cusp: bottom forms a triangle
|
|
|
|
// ATTENTION: the decision whether trNewLeft and trNewRight are to the
|
|
// left or right of the already inserted segment the new one meets here
|
|
// has already been taken when selecting trLast to the left or right
|
|
// of that already inserted segment !!
|
|
|
|
if ( trCurrent.dL ) {
|
|
// *** Case: 1B_DC_LEFT; next: ----
|
|
// console.log( "only_one_trap_below: downward cusp, new seg from the left!" );
|
|
// + /
|
|
// + NR /
|
|
// NL + /
|
|
// + /
|
|
// -------*-------
|
|
// C.dL = next
|
|
|
|
// setAbove
|
|
inTrNext.uL = trNewLeft; // uR: unchanged, NEVER null
|
|
// setBelow part 1
|
|
trNewLeft.dL = inTrNext;
|
|
trNewRight.dR = null;
|
|
} else {
|
|
// *** Case: 1B_DC_RIGHT; next: ----
|
|
// console.log( "only_one_trap_below: downward cusp, new seg to the right!" );
|
|
// \ +
|
|
// \ NL +
|
|
// \ + NR
|
|
// \ +
|
|
// -------*-------
|
|
// C.dR = next
|
|
|
|
// setAbove
|
|
inTrNext.uR = trNewRight; // uL: unchanged, NEVER null
|
|
// setBelow part 1
|
|
trNewLeft.dL = null;
|
|
trNewRight.dR = inTrNext;
|
|
}
|
|
} else {
|
|
// *** Case: 1B_1UN_END; next: ----
|
|
// console.log( "only_one_trap_below: simple case, new seg ends here, low adjacent seg still missing" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// ------*-------
|
|
// next
|
|
|
|
// setAbove
|
|
inTrNext.uL = trNewLeft; // trNewLeft must
|
|
inTrNext.uR = trNewRight; // must
|
|
// setBelow part 1
|
|
trNewLeft.dL = trNewRight.dR = inTrNext; // Error
|
|
// trNewRight.dR = inTrNext;
|
|
}
|
|
// setBelow part 2
|
|
trNewLeft.dR = trNewRight.dL = null;
|
|
} else {
|
|
// NOT final part of segment
|
|
|
|
if ( inTrNext.uL && inTrNext.uR ) {
|
|
// inTrNext has two upper neighbors
|
|
// => a segment ends on the upper Y-line of inTrNext
|
|
// => inTrNext has temporarily 3 upper neighbors
|
|
// => marks whether the new segment cuts through
|
|
// uL or uR of inTrNext and saves the other in .usave
|
|
if ( inTrNext.uL == trCurrent ) {
|
|
// *** Case: 1B_3UN_LEFT; next: CC_3UN_LEFT
|
|
// console.log( "only_one_trap_below: inTrNext has 3 upper neighbors (part I): u0a, u0b, uR(usave)" );
|
|
// + /
|
|
// NL + NR /
|
|
// + /
|
|
// - - - -+--*----
|
|
// +
|
|
// next
|
|
// if ( inTrNext.uR != trNewRight ) { // for robustness TODO: prevent
|
|
inTrNext.usave = inTrNext.uR;
|
|
inTrNext.uleft = true;
|
|
// trNewLeft: L/R undefined, will be extended down and changed anyway
|
|
// } else {
|
|
// ERROR: should not happen
|
|
// console.log( "ERR add_segment: Trapezoid Loop right", inTrNext, trCurrent, trNewLeft, trNewRight, inSegment, this );
|
|
// }
|
|
} else {
|
|
// *** Case: 1B_3UN_RIGHT; next: CC_3UN_RIGHT
|
|
// console.log( "only_one_trap_below: inTrNext has 3 upper neighbors (part I): uL(usave), u1a, u1b" );
|
|
// \ +
|
|
// \ NL + NR
|
|
// \ +
|
|
// ---*---+- - - -
|
|
// +
|
|
// next
|
|
// if ( inTrNext.uL != trNewLeft ) { // for robustness TODO: prevent
|
|
inTrNext.usave = inTrNext.uL;
|
|
inTrNext.uleft = false;
|
|
// trNewRight: L/R undefined, will be extended down and changed anyway
|
|
// } else {
|
|
// ERROR: should not happen
|
|
// console.log( "ERR add_segment: Trapezoid Loop left", inTrNext, trCurrent, trNewLeft, trNewRight, inSegment, this );
|
|
// }
|
|
}
|
|
//} else {
|
|
// *** Case: 1B_1UN_CONT; next: CC_2UN
|
|
// console.log( "only_one_trap_below: simple case, new seg continues down" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// ------+-------
|
|
// +
|
|
// next
|
|
|
|
// L/R for one side undefined, which one is not fixed
|
|
// but that one will be extended down and changed anyway
|
|
// for the other side, vLow must lie at the opposite end
|
|
// thus both are set accordingly
|
|
}
|
|
// setAbove
|
|
inTrNext.uL = trNewLeft;
|
|
inTrNext.uR = trNewRight;
|
|
// setBelow
|
|
trNewLeft.dR = trNewRight.dL = inTrNext;
|
|
trNewLeft.dL = trNewRight.dR = null;
|
|
}
|
|
}
|
|
|
|
function two_trap_below() {
|
|
// Find out which one (dL,dR) is intersected by this segment and
|
|
// continue down that one
|
|
var trNext;
|
|
if ( ( trCurrent.vLow == trLast.vLow ) && meetsLowAdjSeg ) { // meetsLowAdjSeg necessary? TODO
|
|
// *** Case: 2B_CON_END; next: ----
|
|
// console.log( "two_trap_below: finished, meets low adjacent segment" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// ------*-------
|
|
// \ C.dR
|
|
// C.dL \
|
|
|
|
// setAbove
|
|
trCurrent.dL.uL = trNewLeft;
|
|
trCurrent.dR.uR = trNewRight;
|
|
// setBelow; sequence of assignments essential, just in case: trCurrent == trNewLeft
|
|
trNewLeft.dL = trCurrent.dL;
|
|
trNewRight.dR = trCurrent.dR;
|
|
trNewLeft.dR = trNewRight.dL = null;
|
|
|
|
trNext = null; // segment finished
|
|
} else {
|
|
// setAbove part 1
|
|
trCurrent.dL.uL = trNewLeft;
|
|
trCurrent.dR.uR = trNewRight;
|
|
|
|
var goDownRight;
|
|
// passes left or right of an already inserted NOT connected segment
|
|
// trCurrent.vLow: high-end of existing segment
|
|
var compRes = scope.is_left_of( inSegment, trCurrent.vLow, true );
|
|
if ( compRes > 0 ) { // trCurrent.vLow is left of inSegment
|
|
// *** Case: 2B_NOCON_RIGHT; next: CC_2UN
|
|
// console.log( "two_trap_below: (intersecting dR)" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// ---*---+- - - -
|
|
// \ +
|
|
// C.dL \ C.dR
|
|
goDownRight = true;
|
|
} else if ( compRes < 0 ) { // trCurrent.vLow is right of inSegment
|
|
// *** Case: 2B_NOCON_LEFT; next: CC_2UN
|
|
// console.log( "two_trap_below: (intersecting dL)" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// - - -+---*-------
|
|
// + \ C.dR
|
|
// C.dL \
|
|
goDownRight = false;
|
|
} else { // trCurrent.vLow lies ON inSegment
|
|
var vLowSeg = trCurrent.dL.rseg;
|
|
var directionIsUp = vLowSeg.upward;
|
|
var otherPt = directionIsUp ? vLowSeg.vFrom : vLowSeg.vTo;
|
|
compRes = scope.is_left_of( inSegment, otherPt, false );
|
|
if ( compRes > 0 ) { // otherPt is left of inSegment
|
|
// *** Case: 2B_TOUCH_RIGHT; next: CC_2UN
|
|
// console.log( "two_trap_below: vLow ON new segment, touching from right" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// -------*- - - -
|
|
// / +
|
|
// C.dL / C.dR
|
|
goDownRight = true; // like intersecting dR
|
|
} else if ( compRes < 0 ) { // otherPt is right of inSegment
|
|
// *** Case: 2B_TOUCH_LEFT; next: CC_2UN
|
|
// console.log( "two_trap_below: vLow ON new segment, touching from left" );
|
|
// +
|
|
// NL + NR
|
|
// +
|
|
// - - -*-------
|
|
// + \ C.dR
|
|
// C.dL \
|
|
goDownRight = false; // like intersecting dL
|
|
} else { // otherPt lies ON inSegment
|
|
vLowSeg = directionIsUp ? vLowSeg.snext : vLowSeg.sprev; // other segment with trCurrent.vLow
|
|
otherPt = directionIsUp ? vLowSeg.vTo : vLowSeg.vFrom;
|
|
compRes = scope.is_left_of( inSegment, otherPt, false );
|
|
if ( compRes > 0 ) { // otherPt is left of inSegment
|
|
// *** Case: 2B_COLIN_RIGHT; next: CC_2UN
|
|
// console.log( "two_trap_below: vLow ON new segment, touching from right" );
|
|
// +
|
|
// NL + NR
|
|
// -------*- - - -
|
|
// C.dL \+ C.dR
|
|
// \+
|
|
goDownRight = true; // like intersecting dR
|
|
// } else if ( compRes == 0 ) { // NOT POSSIBLE, since 3 points on a line is prevented during input of polychains
|
|
// goDownRight = true; // like intersecting dR
|
|
} else { // otherPt is right of inSegment
|
|
// *** Case: 2B_COLIN_LEFT; next: CC_2UN
|
|
// console.log( "two_trap_below: vLow ON new segment, touching from left" );
|
|
// +
|
|
// NL + NR
|
|
// - - - -*-------
|
|
// C.dL +/ C.dR
|
|
// +/
|
|
goDownRight = false; // TODO: for test_add_segment_special_4 -> like intersecting dL
|
|
}
|
|
}
|
|
}
|
|
if ( goDownRight ) {
|
|
trNext = trCurrent.dR;
|
|
// setAbove part 2
|
|
trCurrent.dR.uL = trNewLeft;
|
|
// setBelow part 1
|
|
trNewLeft.dL = trCurrent.dL;
|
|
trNewRight.dR = null; // L/R undefined, will be extended down and changed anyway
|
|
} else {
|
|
trNext = trCurrent.dL;
|
|
// setAbove part 2
|
|
trCurrent.dL.uR = trNewRight;
|
|
// setBelow part 1
|
|
trNewRight.dR = trCurrent.dR;
|
|
trNewLeft.dL = null; // L/R undefined, will be extended down and changed anyway
|
|
}
|
|
// setBelow part 2
|
|
trNewLeft.dR = trNewRight.dL = trNext;
|
|
}
|
|
|
|
return trNext;
|
|
}
|
|
|
|
//
|
|
// main function body
|
|
//
|
|
|
|
/* if ( ( inSegment.sprev.vTo != inSegment.vFrom ) || ( inSegment.vTo != inSegment.snext.vFrom ) ) {
|
|
console.log( "add_segment: inconsistent point order of adjacent segments: ",
|
|
inSegment.sprev.vTo, inSegment.vFrom, inSegment.vTo, inSegment.snext.vFrom );
|
|
return;
|
|
} */
|
|
|
|
// Find the top-most and bottom-most intersecting trapezoids -> rootXXX
|
|
this.segNodes( inSegment );
|
|
|
|
var segLowVert , segLowNode, meetsLowAdjSeg; // y-min vertex
|
|
var segHighVert, segHighNode, meetsHighAdjSeg; // y-max vertex
|
|
|
|
if ( inSegment.upward ) {
|
|
segLowVert = inSegment.vFrom;
|
|
segHighVert = inSegment.vTo;
|
|
segLowNode = inSegment.rootFrom;
|
|
segHighNode = inSegment.rootTo;
|
|
// was lower point already inserted earlier? => segments meet at their ends
|
|
meetsLowAdjSeg = inSegment.sprev.is_inserted;
|
|
// was higher point already inserted earlier? => segments meet at their ends
|
|
meetsHighAdjSeg = inSegment.snext.is_inserted;
|
|
} else {
|
|
segLowVert = inSegment.vTo;
|
|
segHighVert = inSegment.vFrom;
|
|
segLowNode = inSegment.rootTo;
|
|
segHighNode = inSegment.rootFrom;
|
|
meetsLowAdjSeg = inSegment.snext.is_inserted;
|
|
meetsHighAdjSeg = inSegment.sprev.is_inserted;
|
|
}
|
|
|
|
// insert higher vertex into QueryStructure
|
|
if ( !meetsHighAdjSeg ) {
|
|
// higher vertex not yet inserted => split trapezoid horizontally
|
|
var tmpNode = this.splitNodeAtPoint( segHighNode, segHighVert, false );
|
|
// move segLowNode to new (lower) trapezoid, if it was the one which was just split
|
|
if ( segHighNode == segLowNode ) segLowNode = tmpNode;
|
|
segHighNode = tmpNode;
|
|
}
|
|
var trFirst = segHighNode.trap; // top-most trapezoid for this segment
|
|
|
|
// check for robustness // TODO: prevent
|
|
if ( !trFirst.uL && !trFirst.uR ) {
|
|
console.log("ERR add_segment: missing trFirst.uX: ", trFirst );
|
|
return;
|
|
}
|
|
if ( trFirst.vHigh != segHighVert ) {
|
|
console.log("ERR add_segment: trFirstHigh != segHigh: ", trFirst );
|
|
return;
|
|
}
|
|
|
|
// insert lower vertex into QueryStructure
|
|
if ( !meetsLowAdjSeg ) {
|
|
// lower vertex not yet inserted => split trapezoid horizontally
|
|
segLowNode = this.splitNodeAtPoint( segLowNode, segLowVert, true );
|
|
}
|
|
var trLast = segLowNode.trap; // bottom-most trapezoid for this segment
|
|
|
|
//
|
|
// Thread the segment into the query "tree" from top to bottom.
|
|
// All the trapezoids which are intersected by inSegment are "split" into two.
|
|
// For each the SINK-QsNode is converted into an X-Node and
|
|
// new sinks for the new partial trapezoids are added.
|
|
// In fact a real split only happens at the top and/or bottom end of the segment
|
|
// since at every y-line seperating two trapezoids is traverses it
|
|
// cuts off the "beam" from the y-vertex on one side, so that at that side
|
|
// the trapezoid from above can be extended down.
|
|
//
|
|
|
|
var trCurrent = trFirst;
|
|
|
|
var trNewLeft, trNewRight, trPrevLeft, trPrevRight;
|
|
|
|
var counter = this.trapArray.length + 2; // just to prevent infinite loop
|
|
var trNext;
|
|
while ( trCurrent ) {
|
|
if ( --counter < 0 ) {
|
|
console.log( "ERR add_segment: infinite loop", trCurrent, inSegment, this );
|
|
return;
|
|
}
|
|
if ( !trCurrent.dL && !trCurrent.dR ) {
|
|
// ERROR: no successors, cannot arise if data is correct
|
|
console.log( "ERR add_segment: missing successors", trCurrent, inSegment, this );
|
|
return;
|
|
}
|
|
|
|
var qs_trCurrent = trCurrent.sink;
|
|
// SINK-Node -> X-Node
|
|
qs_trCurrent.seg = inSegment;
|
|
qs_trCurrent.trap = null;
|
|
//
|
|
// successive trapezoids bordered by the same segments are merged
|
|
// by extending the trPrevRight or trPrevLeft down
|
|
// and redirecting the parent X-Node to the extended sink
|
|
// !!! destroys tree structure since several nodes now point to the same SINK-Node !!!
|
|
// TODO: maybe it's not a problem;
|
|
// merging of X-Nodes is no option, since they are used as "rootFrom/rootTo" !
|
|
//
|
|
if ( trPrevRight && ( trPrevRight.rseg == trCurrent.rseg ) ) {
|
|
// console.log( "add_segment: extending right predecessor down!", trPrevRight );
|
|
trNewLeft = trCurrent;
|
|
trNewRight = trPrevRight;
|
|
trNewRight.vLow = trCurrent.vLow;
|
|
// redirect parent X-Node to extended sink
|
|
qs_trCurrent.left = new PNLTRI.QsNode( trNewLeft ); // trCurrent -> left SINK-Node
|
|
qs_trCurrent.right = trPrevRight.sink; // deforms tree by multiple links to trPrevRight.sink
|
|
} else if ( trPrevLeft && ( trPrevLeft.lseg == trCurrent.lseg ) ) {
|
|
// console.log( "add_segment: extending left predecessor down!", trPrevLeft );
|
|
trNewRight = trCurrent;
|
|
trNewLeft = trPrevLeft;
|
|
trNewLeft.vLow = trCurrent.vLow;
|
|
// redirect parent X-Node to extended sink
|
|
qs_trCurrent.left = trPrevLeft.sink; // deforms tree by multiple links to trPrevLeft.sink
|
|
qs_trCurrent.right = new PNLTRI.QsNode( trNewRight ); // trCurrent -> right SINK-Node
|
|
} else {
|
|
trNewLeft = trCurrent;
|
|
trNewRight = this.cloneTrap(trCurrent);
|
|
qs_trCurrent.left = new PNLTRI.QsNode( trNewLeft ); // trCurrent -> left SINK-Node
|
|
qs_trCurrent.right = new PNLTRI.QsNode( trNewRight ); // new clone -> right SINK-Node
|
|
}
|
|
|
|
// handle neighbors above
|
|
if ( trCurrent.uL && trCurrent.uR ) {
|
|
continue_chain_from_above();
|
|
} else {
|
|
fresh_seg_or_upward_cusp();
|
|
}
|
|
|
|
// handle neighbors below
|
|
if ( trCurrent.dL && trCurrent.dR ) {
|
|
trNext = two_trap_below();
|
|
} else {
|
|
if ( trCurrent.dL ) {
|
|
// console.log( "add_segment: only_one_trap_below! (dL)" );
|
|
trNext = trCurrent.dL;
|
|
} else {
|
|
// console.log( "add_segment: only_one_trap_below! (dR)" );
|
|
trNext = trCurrent.dR;
|
|
}
|
|
only_one_trap_below( trNext );
|
|
}
|
|
|
|
if ( trNewLeft.rseg ) trNewLeft.rseg.trLeft = trNewRight;
|
|
if ( trNewRight.lseg ) trNewRight.lseg.trRight = trNewLeft;
|
|
trNewLeft.rseg = trNewRight.lseg = inSegment;
|
|
inSegment.trLeft = trNewLeft;
|
|
inSegment.trRight = trNewRight;
|
|
|
|
// further loop-step down ?
|
|
if ( trCurrent.vLow != trLast.vLow ) {
|
|
trPrevLeft = trNewLeft;
|
|
trPrevRight = trNewRight;
|
|
|
|
trCurrent = trNext;
|
|
} else {
|
|
trCurrent = null;
|
|
}
|
|
} // end while
|
|
|
|
inSegment.is_inserted = true;
|
|
// console.log( "add_segment: ###### DONE ######" );
|
|
},
|
|
|
|
// Assigns a depth to all trapezoids;
|
|
// 0: outside, 1: main polygon, 2: holes, 3:polygons in holes, ...
|
|
// Checks segment orientation and marks those polygon chains for reversal
|
|
// where the polygon inside lies to their right (contour in CW, holes in CCW)
|
|
assignDepths: function ( inPolyData ) {
|
|
var thisDepth = [ this.trapArray[0] ];
|
|
var nextDepth = [];
|
|
|
|
var thisTrap, borderSeg, curDepth = 0;
|
|
do {
|
|
// rseg should exactely go upward on trapezoids inside the polygon (odd depth)
|
|
var expectedRsegUpward = ( ( curDepth % 2 ) == 1 );
|
|
while ( thisTrap = thisDepth.pop() ) { // assignment !
|
|
if ( thisTrap.depth != -1 ) continue;
|
|
thisTrap.depth = curDepth;
|
|
//
|
|
if ( thisTrap.uL ) thisDepth.push( thisTrap.uL );
|
|
if ( thisTrap.uR ) thisDepth.push( thisTrap.uR );
|
|
if ( thisTrap.dL ) thisDepth.push( thisTrap.dL );
|
|
if ( thisTrap.dR ) thisDepth.push( thisTrap.dR );
|
|
//
|
|
if ( ( borderSeg = thisTrap.lseg ) && ( borderSeg.trLeft.depth == -1 ) ) // assignment !
|
|
nextDepth.push( borderSeg.trLeft );
|
|
if ( borderSeg = thisTrap.rseg ) { // assignment !
|
|
if ( borderSeg.trRight.depth == -1 )
|
|
nextDepth.push( borderSeg.trRight );
|
|
if ( borderSeg.upward != expectedRsegUpward )
|
|
inPolyData.set_PolyLeft_wrong( borderSeg.chainId );
|
|
}
|
|
}
|
|
thisDepth = nextDepth; nextDepth = [];
|
|
curDepth++;
|
|
} while ( thisDepth.length > 0 );
|
|
},
|
|
|
|
// creates the visibility map:
|
|
// for each vertex the list of all vertices in CW order which are directly
|
|
// visible through neighboring trapezoids and thus can be connected by a diagonal
|
|
|
|
create_visibility_map: function ( inPolygonData ) {
|
|
// positional slots for neighboring trapezoid-diagonals
|
|
var DIAG_UL = 0, DIAG_UM = 1, DIAG_ULR = 2, DIAG_UR = 3;
|
|
var DIAG_DR = 4, DIAG_DM = 5, DIAG_DLR = 6, DIAG_DL = 7;
|
|
|
|
var i, j;
|
|
var nbVertices = inPolygonData.nbVertices();
|
|
|
|
// initialize arrays for neighboring trapezoid-diagonals and vertices
|
|
var myVisibleDiagonals = new Array(nbVertices);
|
|
for ( i = 0; i < nbVertices; i++ ) {
|
|
myVisibleDiagonals[i] = new Array(DIAG_DL+1);
|
|
}
|
|
// create the list of neighboring trapezoid-diagonals
|
|
// put into their positional slots
|
|
var myExternalNeighbors = new Array(nbVertices);
|
|
for ( i = 0, j = this.trapArray.length; i < j; i++ ) {
|
|
var curTrap = this.trapArray[i];
|
|
var highPos = curTrap.uL ?
|
|
( curTrap.uR ? DIAG_DM : DIAG_DL ) :
|
|
( curTrap.uR ? DIAG_DR : DIAG_DLR );
|
|
var lowPos = curTrap.dL ?
|
|
( curTrap.dR ? DIAG_UM : DIAG_UL ) :
|
|
( curTrap.dR ? DIAG_UR : DIAG_ULR );
|
|
|
|
if ( ( curTrap.depth % 2 ) == 1 ) { // inside ?
|
|
if ( ( highPos == DIAG_DM ) || ( lowPos == DIAG_UM ) ||
|
|
( ( highPos == DIAG_DL ) && ( lowPos == DIAG_UR ) ) ||
|
|
( ( highPos == DIAG_DR ) && ( lowPos == DIAG_UL ) ) ) {
|
|
var lhDiag = inPolygonData.appendDiagonalsEntry( {
|
|
vFrom: curTrap.vLow, vTo: curTrap.vHigh,
|
|
mprev: null, mnext: null, marked: false } );
|
|
var hlDiag = inPolygonData.appendDiagonalsEntry( {
|
|
vFrom: curTrap.vHigh, vTo: curTrap.vLow, revDiag: lhDiag,
|
|
mprev: null, mnext: null, marked: false } );
|
|
lhDiag.revDiag = hlDiag;
|
|
myVisibleDiagonals[ curTrap.vLow.id][ lowPos] = lhDiag;
|
|
myVisibleDiagonals[curTrap.vHigh.id][highPos] = hlDiag;
|
|
}
|
|
} else { // outside, hole
|
|
if ( curTrap.vHigh.id !== null ) myExternalNeighbors[curTrap.vHigh.id] = highPos;
|
|
if ( curTrap.vLow.id !== null ) myExternalNeighbors[ curTrap.vLow.id] = lowPos;
|
|
}
|
|
}
|
|
// create the list of outgoing diagonals in the right order (CW)
|
|
// from the ordered list of neighboring trapezoid-diagonals
|
|
// - starting from an external one
|
|
// and connect those incoming to
|
|
var curDiag, curDiags, firstElem, fromVertex, lastIncoming;
|
|
for ( i = 0; i < nbVertices; i++ ) {
|
|
curDiags = myVisibleDiagonals[i];
|
|
firstElem = myExternalNeighbors[i];
|
|
if ( firstElem == null ) continue; // eg. skipped vertices (zero length, co-linear // NOT: === !
|
|
j = firstElem;
|
|
lastIncoming = null;
|
|
do {
|
|
if ( j++ > DIAG_DL ) j = DIAG_UL; // circular positional list
|
|
if ( curDiag = curDiags[j] ) {
|
|
if ( lastIncoming ) {
|
|
curDiag.mprev = lastIncoming;
|
|
lastIncoming.mnext = curDiag;
|
|
} else {
|
|
fromVertex = curDiag.vFrom;
|
|
fromVertex.firstOutDiag = curDiag;
|
|
}
|
|
lastIncoming = curDiag.revDiag;
|
|
}
|
|
} while ( j != firstElem );
|
|
if ( lastIncoming ) fromVertex.lastInDiag = lastIncoming;
|
|
}
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
/*==============================================================================
|
|
*
|
|
*============================================================================*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.Trapezoider = function ( inPolygonData ) {
|
|
|
|
this.polyData = inPolygonData;
|
|
this.queryStructure = new PNLTRI.QueryStructure( this.polyData );
|
|
|
|
};
|
|
|
|
PNLTRI.Trapezoider.prototype = {
|
|
|
|
constructor: PNLTRI.Trapezoider,
|
|
|
|
|
|
/*
|
|
* Mathematics helper methods
|
|
*/
|
|
|
|
optimise_randomlist: function ( inOutSegListArray ) {
|
|
// makes sure that the first N segments are one from each of the N polygon chains
|
|
var mainIdx = 0;
|
|
var helpIdx = this.polyData.nbPolyChains();
|
|
if ( helpIdx == 1 ) return;
|
|
var chainMarker = new Array(helpIdx);
|
|
var oldSegListArray = inOutSegListArray.concat();
|
|
for (var i=0; i<oldSegListArray.length; i++) {
|
|
var chainId = oldSegListArray[i].chainId;
|
|
if ( chainMarker[chainId] ) {
|
|
inOutSegListArray[helpIdx++] = oldSegListArray[i];
|
|
} else {
|
|
inOutSegListArray[mainIdx++] = oldSegListArray[i];
|
|
chainMarker[chainId] = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* main methods
|
|
*/
|
|
|
|
// Creates the trapezoidation of the polygon
|
|
// and assigns a depth to all trapezoids (odd: inside, even: outside).
|
|
|
|
trapezoide_polygon: function () { // <<<< public
|
|
var randSegListArray = this.polyData.getSegments().concat();
|
|
// console.log( "Polygon Chains: ", dumpSegmentList( randSegListArray ) );
|
|
PNLTRI.Math.array_shuffle( randSegListArray );
|
|
this.optimise_randomlist( randSegListArray );
|
|
// console.log( "Random Segment Sequence: ", dumpRandomSequence( randSegListArray ) );
|
|
|
|
var nbSegs = randSegListArray.length;
|
|
var myQs = this.queryStructure;
|
|
|
|
var i, current = 0, logstar = nbSegs;
|
|
while ( current < nbSegs ) {
|
|
// The CENTRAL mechanism for the near-linear performance:
|
|
// stratefies the loop through all segments into log* parts
|
|
// and computes new root-Nodes for the remaining segments in each
|
|
// partition.
|
|
logstar = Math.log(logstar)/Math.LN2; // == log2(logstar)
|
|
var partEnd = ( logstar > 1 ) ? Math.floor( nbSegs / logstar ) : nbSegs;
|
|
|
|
// Core: adds next partition of the segments
|
|
for (; current < partEnd; current++ ) { myQs.add_segment( randSegListArray[current] ); }
|
|
// console.log( nbSegs, current );
|
|
|
|
// To speed up the segment insertion into the trapezoidation
|
|
// the endponts of those segments not yet inserted
|
|
// are repeatedly pre-located,
|
|
// thus their final location-query can start at the top of the
|
|
// appropriate sub-tree instead of the root of the whole
|
|
// query structure.
|
|
//
|
|
for (i = current; i < nbSegs; i++) { this.queryStructure.segNodes( randSegListArray[i] ); }
|
|
}
|
|
|
|
myQs.assignDepths( this.polyData );
|
|
// cleanup to support garbage collection
|
|
for (i = 0; i < nbSegs; i++) { randSegListArray[i].trLeft = randSegListArray[i].trRight = null; }
|
|
},
|
|
|
|
// Creates a visibility map:
|
|
// for each vertex the list of all vertices in CW order which are directly
|
|
// visible through neighboring trapezoids and thus can be connected by a diagonal
|
|
|
|
create_visibility_map: function () {
|
|
return this.queryStructure.create_visibility_map( this.polyData );
|
|
},
|
|
|
|
};
|
|
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*
|
|
* Algorithm to split a polygon into uni-y-monotone sub-polygons
|
|
*
|
|
* 1) creates a trapezoidation of the main polygon according to Seidel's
|
|
* algorithm [Sei91]
|
|
* 2) uses diagonals of the trapezoids as additional segments
|
|
* to split the main polygon into uni-y-monotone sub-polygons
|
|
*/
|
|
|
|
/** @constructor */
|
|
PNLTRI.MonoSplitter = function ( inPolygonData ) {
|
|
|
|
this.polyData = inPolygonData;
|
|
|
|
this.trapezoider = null;
|
|
|
|
};
|
|
|
|
|
|
PNLTRI.MonoSplitter.prototype = {
|
|
|
|
constructor: PNLTRI.MonoSplitter,
|
|
|
|
|
|
monotonate_trapezoids: function () { // <<<<<<<<<< public
|
|
// Trapezoidation
|
|
this.trapezoider = new PNLTRI.Trapezoider( this.polyData );
|
|
// => one triangular trapezoid which lies inside the polygon
|
|
this.trapezoider.trapezoide_polygon();
|
|
|
|
// create segments for diagonals
|
|
this.trapezoider.create_visibility_map();
|
|
// create mono chains by inserting diagonals
|
|
this.polyData.create_mono_chains();
|
|
|
|
// create UNIQUE monotone sub-polygons
|
|
this.polyData.unique_monotone_chains_max();
|
|
},
|
|
|
|
};
|
|
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*
|
|
* Algorithm to triangulate uni-y-monotone polygons [FoM84]
|
|
*
|
|
* expects list of doubly linked monoChains, with Y-max as first vertex
|
|
*/
|
|
|
|
|
|
/** @constructor */
|
|
PNLTRI.MonoTriangulator = function ( inPolygonData ) {
|
|
|
|
this.polyData = inPolygonData;
|
|
|
|
};
|
|
|
|
|
|
PNLTRI.MonoTriangulator.prototype = {
|
|
|
|
constructor: PNLTRI.MonoTriangulator,
|
|
|
|
|
|
// Pass each uni-y-monotone polygon with start at Y-max for greedy triangulation.
|
|
|
|
triangulate_all_polygons: function () { // <<<<<<<<<< public
|
|
var normedMonoChains = this.polyData.getMonoSubPolys();
|
|
this.polyData.clearTriangles();
|
|
for ( var i=0; i<normedMonoChains.length; i++ ) {
|
|
// loop through uni-y-monotone chains
|
|
// => monoPosmin is next to monoPosmax (left or right)
|
|
var monoPosmax = normedMonoChains[i];
|
|
var prevMono = monoPosmax.mprev;
|
|
var nextMono = monoPosmax.mnext;
|
|
|
|
if ( nextMono.mnext == prevMono ) { // already a triangle
|
|
this.polyData.addTriangle( monoPosmax.vFrom, nextMono.vFrom, prevMono.vFrom );
|
|
} else { // triangulate the polygon
|
|
this.triangulate_monotone_polygon( monoPosmax );
|
|
}
|
|
}
|
|
},
|
|
|
|
// algorithm to triangulate an uni-y-monotone polygon in O(n) time.[FoM84]
|
|
|
|
triangulate_monotone_polygon: function ( monoPosmax ) { // private
|
|
var scope = this;
|
|
|
|
function error_cleanup() {
|
|
// Error in algorithm OR polygon is not uni-y-monotone
|
|
console.log( "ERR uni-y-monotone: only concave angles left", vertBackLog );
|
|
// push all "wrong" triangles => loop ends
|
|
while (vertBackLogIdx > 1) {
|
|
vertBackLogIdx--;
|
|
scope.polyData.addTriangle( vertBackLog[vertBackLogIdx-1],
|
|
vertBackLog[vertBackLogIdx],
|
|
vertBackLog[vertBackLogIdx+1] );
|
|
}
|
|
}
|
|
|
|
//
|
|
// Decisive for this algorithm to work correctly is to make sure
|
|
// the polygon stays uni-y-monotone when cutting off ears, i.e.
|
|
// to make sure the top-most and bottom-most vertices are removed last
|
|
// Usually this is done by handling the LHS-case ("LeftHandSide is a single segment")
|
|
// and the RHS-case ("RightHandSide segment is a single segment")
|
|
// differently by starting at the bottom for LHS and at the top for RHS.
|
|
// This is not necessary. It can be seen easily, that starting
|
|
// from the vertex next to top handles both cases correctly.
|
|
//
|
|
|
|
var frontMono = monoPosmax.mnext; // == LHS: YminPoint; RHS: YmaxPoint.mnext
|
|
var endVert = monoPosmax.vFrom;
|
|
|
|
var vertBackLog = [ frontMono.vFrom ];
|
|
var vertBackLogIdx = 0;
|
|
|
|
frontMono = frontMono.mnext;
|
|
var frontVert = frontMono.vFrom;
|
|
|
|
// check for robustness // TODO
|
|
if (frontVert == endVert) return; // Error: only 2 vertices
|
|
|
|
while ( (frontVert != endVert) || (vertBackLogIdx > 1) ) {
|
|
if ( vertBackLogIdx > 0 ) {
|
|
// vertBackLog is not empty
|
|
var insideAngleCCW = PNLTRI.Math.ptsCrossProd( vertBackLog[vertBackLogIdx], frontVert, vertBackLog[vertBackLogIdx-1] );
|
|
if ( Math.abs(insideAngleCCW) <= PNLTRI.Math.EPSILON_P ) {
|
|
// co-linear
|
|
if ( (frontVert == endVert) || // all remaining triangles are co-linear (180 degree)
|
|
( PNLTRI.Math.compare_pts_yx( vertBackLog[vertBackLogIdx], frontVert ) == // co-linear-reversal
|
|
PNLTRI.Math.compare_pts_yx( vertBackLog[vertBackLogIdx], vertBackLog[vertBackLogIdx-1] ) ) ) {
|
|
insideAngleCCW = 1; // => create triangle
|
|
}
|
|
}
|
|
if ( insideAngleCCW > 0 ) {
|
|
// convex corner: cut if off
|
|
this.polyData.addTriangle( vertBackLog[vertBackLogIdx-1], vertBackLog[vertBackLogIdx], frontVert );
|
|
vertBackLogIdx--;
|
|
} else {
|
|
// non-convex: add frontVert to the vertBackLog
|
|
vertBackLog[++vertBackLogIdx] = frontVert;
|
|
if (frontVert == endVert) error_cleanup(); // should never happen !!
|
|
else {
|
|
frontMono = frontMono.mnext;
|
|
frontVert = frontMono.vFrom;
|
|
}
|
|
}
|
|
} else {
|
|
// vertBackLog contains only start vertex:
|
|
// add frontVert to the vertBackLog and advance frontVert
|
|
vertBackLog[++vertBackLogIdx] = frontVert;
|
|
frontMono = frontMono.mnext;
|
|
frontVert = frontMono.vFrom;
|
|
}
|
|
}
|
|
// reached the last vertex. Add in the triangle formed
|
|
this.polyData.addTriangle( vertBackLog[vertBackLogIdx - 1], vertBackLog[vertBackLogIdx], frontVert );
|
|
},
|
|
|
|
};
|
|
|
|
/**
|
|
* @author jahting / http://www.ameco.tv/
|
|
*/
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* Triangulator for Simple Polygons with Holes
|
|
*
|
|
* polygon with holes:
|
|
* - one closed contour polygon chain
|
|
* - zero or more closed hole polygon chains
|
|
*
|
|
* polygon chain (closed):
|
|
* - Array of vertex Objects with attributes "x" and "y"
|
|
* - representing the sequence of line segments
|
|
* - closing line segment (last->first vertex) is implied
|
|
* - line segments are non-zero length and non-crossing
|
|
*
|
|
* "global vertex index":
|
|
* - vertex number resulting from concatenation all polygon chains (starts with 0)
|
|
*
|
|
*
|
|
* Parameters (will not be changed):
|
|
* inPolygonChains:
|
|
* - Array of polygon chains
|
|
*
|
|
* Results (are a fresh copy):
|
|
* triangulate_polygon:
|
|
* - Array of Triangles ( Array of 3 "global vertex index" values )
|
|
*
|
|
******************************************************************************/
|
|
|
|
/** @constructor */
|
|
PNLTRI.Triangulator = function () {
|
|
|
|
this.lastPolyData = null; // for Debug purposes only
|
|
|
|
};
|
|
|
|
|
|
PNLTRI.Triangulator.prototype = {
|
|
|
|
constructor: PNLTRI.Triangulator,
|
|
|
|
|
|
clear_lastData: function () { // save memory after Debug
|
|
this.lastPolyData = null;
|
|
},
|
|
|
|
// for the polygon data AFTER triangulation
|
|
// returns an Array of flags, one flag for each polygon chain:
|
|
// lies the inside of the polygon to the left?
|
|
// "true" implies CCW for contours and CW for holes
|
|
get_PolyLeftArr: function () {
|
|
if ( this.lastPolyData ) return this.lastPolyData.get_PolyLeftArr();
|
|
return null;
|
|
},
|
|
|
|
|
|
triangulate_polygon: function ( inPolygonChains, inForceTrapezoidation ) {
|
|
|
|
// collected conditions for selecting EarClipTriangulator over Seidel's algorithm
|
|
function is_basic_polygon() {
|
|
if (inForceTrapezoidation) return false;
|
|
return ( myPolygonData.nbPolyChains() == 1 );
|
|
}
|
|
|
|
|
|
this.clear_lastData();
|
|
if ( ( !inPolygonChains ) || ( inPolygonChains.length === 0 ) ) return [];
|
|
//
|
|
// initializes general polygon data structure
|
|
//
|
|
var myPolygonData = new PNLTRI.PolygonData( inPolygonChains );
|
|
//
|
|
var basicPolygon = is_basic_polygon();
|
|
var myTriangulator;
|
|
if ( basicPolygon ) {
|
|
//
|
|
// triangulates single polygon without holes
|
|
//
|
|
myTriangulator = new PNLTRI.EarClipTriangulator( myPolygonData );
|
|
basicPolygon = myTriangulator.triangulate_polygon_no_holes();
|
|
}
|
|
if ( !basicPolygon ) {
|
|
//
|
|
// splits polygon into uni-y-monotone sub-polygons
|
|
//
|
|
var myMonoSplitter = new PNLTRI.MonoSplitter( myPolygonData );
|
|
myMonoSplitter.monotonate_trapezoids();
|
|
//
|
|
// triangulates all uni-y-monotone sub-polygons
|
|
//
|
|
myTriangulator = new PNLTRI.MonoTriangulator( myPolygonData );
|
|
myTriangulator.triangulate_all_polygons();
|
|
}
|
|
//
|
|
this.lastPolyData = myPolygonData;
|
|
return myPolygonData.getTriangles(); // copy of triangle list
|
|
}
|
|
|
|
|
|
};
|