// 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 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; 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 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 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 } };