/*! * General purpose geometry functions for polygon/Bezier calculations * Copyright 2015 Jack Qiao * Licensed under the MIT license */ (function(root){ 'use strict'; // private shared variables/methods // floating point comparison tolerance var TOL = Math.pow(10, -9); // Floating point error is likely to be above 1 epsilon function _almostEqual(a, b, tolerance){ if(!tolerance){ tolerance = TOL } return Math.abs(a - b) < tolerance; } // returns true if points are within the given distance function _withinDistance(p1, p2, distance){ var dx = p1.x-p2.x; var dy = p1.y-p2.y; return ((dx*dx + dy*dy) < distance*distance); } function _degreesToRadians(angle){ return angle*(Math.PI/180); } function _radiansToDegrees(angle){ return angle*(180/Math.PI); } // normalize vector into a unit vector function _normalizeVector(v){ if(_almostEqual(v.x*v.x + v.y*v.y, 1)){ return v; // given vector was already a unit vector } var len = Math.sqrt(v.x*v.x + v.y*v.y); var inverse = 1/len; return { x: v.x*inverse, y: v.y*inverse }; } // returns true if p lies on the line segment defined by AB, but not at any endpoints // may need work! function _onSegment(A,B,p){ // vertical line if(_almostEqual(A.x, B.x) && _almostEqual(p.x, A.x)){ if(!_almostEqual(p.y, B.y) && !_almostEqual(p.y, A.y) && p.y < Math.max(B.y, A.y) && p.y > Math.min(B.y, A.y)){ return true; } else{ return false; } } // horizontal line if(_almostEqual(A.y, B.y) && _almostEqual(p.y, A.y)){ if(!_almostEqual(p.x, B.x) && !_almostEqual(p.x, A.x) && p.x < Math.max(B.x, A.x) && p.x > Math.min(B.x, A.x)){ return true; } else{ return false; } } //range check if((p.x < A.x && p.x < B.x) || (p.x > A.x && p.x > B.x) || (p.y < A.y && p.y < B.y) || (p.y > A.y && p.y > B.y)){ return false; } // exclude end points if((_almostEqual(p.x, A.x) && _almostEqual(p.y, A.y)) || (_almostEqual(p.x, B.x) && _almostEqual(p.y, B.y))){ return false; } var cross = (p.y - A.y) * (B.x - A.x) - (p.x - A.x) * (B.y - A.y); if(Math.abs(cross) > TOL){ return false; } var dot = (p.x - A.x) * (B.x - A.x) + (p.y - A.y)*(B.y - A.y); if(dot < 0 || _almostEqual(dot, 0)){ return false; } var len2 = (B.x - A.x)*(B.x - A.x) + (B.y - A.y)*(B.y - A.y); if(dot > len2 || _almostEqual(dot, len2)){ return false; } return true; } // returns the intersection of AB and EF // or null if there are no intersections or other numerical error // if the infinite flag is set, AE and EF describe infinite lines without endpoints, they are finite line segments otherwise function _lineIntersect(A,B,E,F,infinite){ var a1, a2, b1, b2, c1, c2, x, y; a1= B.y-A.y; b1= A.x-B.x; c1= B.x*A.y - A.x*B.y; a2= F.y-E.y; b2= E.x-F.x; c2= F.x*E.y - E.x*F.y; var denom=a1*b2 - a2*b1; x = (b1*c2 - b2*c1)/denom, y = (a2*c1 - a1*c2)/denom if(!isFinite(x) || !isFinite(y)){ return null; } // lines are colinear /*var crossABE = (E.y - A.y) * (B.x - A.x) - (E.x - A.x) * (B.y - A.y); var crossABF = (F.y - A.y) * (B.x - A.x) - (F.x - A.x) * (B.y - A.y); if(_almostEqual(crossABE,0) && _almostEqual(crossABF,0)){ return null; }*/ if(!infinite){ // coincident points do not count as intersecting if (Math.abs(A.x-B.x) > TOL && (( A.x < B.x ) ? x < A.x || x > B.x : x > A.x || x < B.x )) return null; if (Math.abs(A.y-B.y) > TOL && (( A.y < B.y ) ? y < A.y || y > B.y : y > A.y || y < B.y )) return null; if (Math.abs(E.x-F.x) > TOL && (( E.x < F.x ) ? x < E.x || x > F.x : x > E.x || x < F.x )) return null; if (Math.abs(E.y-F.y) > TOL && (( E.y < F.y ) ? y < E.y || y > F.y : y > E.y || y < F.y )) return null; } return {x: x, y: y}; } // public methods root.GeometryUtil = { withinDistance: _withinDistance, lineIntersect: _lineIntersect, almostEqual: _almostEqual, // Bezier algos from http://algorithmist.net/docs/subdivision.pdf QuadraticBezier: { // Roger Willcocks bezier flatness criterion isFlat: function(p1, p2, c1, tol){ tol = 4*tol*tol; var ux = 2*c1.x - p1.x - p2.x; ux *= ux; var uy = 2*c1.y - p1.y - p2.y; uy *= uy; return (ux+uy <= tol); }, // turn Bezier into line segments via de Casteljau, returns an array of points linearize: function(p1, p2, c1, tol){ var finished = [p1]; // list of points to return var todo = [{p1: p1, p2: p2, c1: c1}]; // list of Beziers to divide // recursion could stack overflow, loop instead while(todo.length > 0){ var segment = todo[0]; if(this.isFlat(segment.p1, segment.p2, segment.c1, tol)){ // reached subdivision limit finished.push({x: segment.p2.x, y: segment.p2.y}); todo.shift(); } else{ var divided = this.subdivide(segment.p1, segment.p2, segment.c1, 0.5); todo.splice(0,1,divided[0],divided[1]); } } return finished; }, // subdivide a single Bezier // t is the percent along the Bezier to divide at. eg. 0.5 subdivide: function(p1, p2, c1, t){ var mid1 = { x: p1.x+(c1.x-p1.x)*t, y: p1.y+(c1.y-p1.y)*t }; var mid2 = { x: c1.x+(p2.x-c1.x)*t, y: c1.y+(p2.y-c1.y)*t }; var mid3 = { x: mid1.x+(mid2.x-mid1.x)*t, y: mid1.y+(mid2.y-mid1.y)*t }; var seg1 = {p1: p1, p2: mid3, c1: mid1}; var seg2 = {p1: mid3, p2: p2, c1: mid2}; return [seg1, seg2]; } }, CubicBezier: { isFlat: function(p1, p2, c1, c2, tol){ tol = 16*tol*tol; var ux = 3*c1.x - 2*p1.x - p2.x; ux *= ux; var uy = 3*c1.y - 2*p1.y - p2.y; uy *= uy; var vx = 3*c2.x - 2*p2.x - p1.x; vx *= vx; var vy = 3*c2.y - 2*p2.y - p1.y; vy *= vy; if (ux < vx){ ux = vx; } if (uy < vy){ uy = vy; } return (ux+uy <= tol); }, linearize: function(p1, p2, c1, c2, tol){ var finished = [p1]; // list of points to return var todo = [{p1: p1, p2: p2, c1: c1, c2: c2}]; // list of Beziers to divide // recursion could stack overflow, loop instead while(todo.length > 0){ var segment = todo[0]; if(this.isFlat(segment.p1, segment.p2, segment.c1, segment.c2, tol)){ // reached subdivision limit finished.push({x: segment.p2.x, y: segment.p2.y}); todo.shift(); } else{ var divided = this.subdivide(segment.p1, segment.p2, segment.c1, segment.c2, 0.5); todo.splice(0,1,divided[0],divided[1]); } } return finished; }, subdivide: function(p1, p2, c1, c2, t){ var mid1 = { x: p1.x+(c1.x-p1.x)*t, y: p1.y+(c1.y-p1.y)*t }; var mid2 = { x:c2.x+(p2.x-c2.x)*t, y:c2.y+(p2.y-c2.y)*t }; var mid3 = { x: c1.x+(c2.x-c1.x)*t, y: c1.y+(c2.y-c1.y)*t }; var mida = { x: mid1.x+(mid3.x-mid1.x)*t, y: mid1.y+(mid3.y-mid1.y)*t }; var midb = { x: mid3.x+(mid2.x-mid3.x)*t, y: mid3.y+(mid2.y-mid3.y)*t }; var midx = { x: mida.x+(midb.x-mida.x)*t, y: mida.y+(midb.y-mida.y)*t }; var seg1 = {p1: p1, p2: midx, c1: mid1, c2: mida}; var seg2 = {p1: midx, p2: p2, c1: midb, c2: mid2}; return [seg1, seg2]; } }, Arc: { linearize: function(p1, p2, rx, ry, angle, largearc, sweep, tol){ var finished = [p2]; // list of points to return var arc = this.svgToCenter(p1, p2, rx, ry, angle, largearc, sweep); var todo = [arc]; // list of arcs to divide // recursion could stack overflow, loop instead while(todo.length > 0){ arc = todo[0]; var fullarc = this.centerToSvg(arc.center, arc.rx, arc.ry, arc.theta, arc.extent, arc.angle); var subarc = this.centerToSvg(arc.center, arc.rx, arc.ry, arc.theta, 0.5*arc.extent, arc.angle); var arcmid = subarc.p2; var mid = { x: 0.5*(fullarc.p1.x + fullarc.p2.x), y: 0.5*(fullarc.p1.y + fullarc.p2.y) } // compare midpoint of line with midpoint of arc // this is not 100% accurate, but should be a good heuristic for flatness in most cases if(_withinDistance(mid, arcmid, tol)){ finished.unshift(fullarc.p2); todo.shift(); } else{ var arc1 = { center: arc.center, rx: arc.rx, ry: arc.ry, theta: arc.theta, extent: 0.5*arc.extent, angle: arc.angle }; var arc2 = { center: arc.center, rx: arc.rx, ry: arc.ry, theta: arc.theta+0.5*arc.extent, extent: 0.5*arc.extent, angle: arc.angle }; todo.splice(0,1,arc1,arc2); } } return finished; }, // convert from center point/angle sweep definition to SVG point and flag definition of arcs // ported from http://commons.oreilly.com/wiki/index.php/SVG_Essentials/Paths centerToSvg: function(center, rx, ry, theta1, extent, angleDegrees){ var theta2 = theta1 + extent; theta1 = _degreesToRadians(theta1); theta2 = _degreesToRadians(theta2); var angle = _degreesToRadians(angleDegrees); var cos = Math.cos(angle); var sin = Math.sin(angle); var t1cos = Math.cos(theta1); var t1sin = Math.sin(theta1); var t2cos = Math.cos(theta2); var t2sin = Math.sin(theta2); var x0 = center.x + cos * rx * t1cos + (-sin) * ry * t1sin; var y0 = center.y + sin * rx * t1cos + cos * ry * t1sin; var x1 = center.x + cos * rx * t2cos + (-sin) * ry * t2sin; var y1 = center.y + sin * rx * t2cos + cos * ry * t2sin; var largearc = (extent > 180) ? 1 : 0; var sweep = (extent > 0) ? 1 : 0; return { p1: {x: x0, y: y0}, p2: {x: x1, y: y1}, rx: rx, ry: ry, angle: angle, largearc: largearc, sweep: sweep }; }, // convert from SVG format arc to center point arc svgToCenter: function(p1, p2, rx, ry, angleDegrees, largearc, sweep){ var mid = { x: 0.5*(p1.x+p2.x), y: 0.5*(p1.y+p2.y) } var diff = { x: 0.5*(p2.x-p1.x), y: 0.5*(p2.y-p1.y) } var angle = _degreesToRadians(angleDegrees%360); var cos = Math.cos(angle); var sin = Math.sin(angle); var x1 = cos * diff.x + sin * diff.y; var y1 = -sin * diff.x + cos * diff.y; rx = Math.abs(rx); ry = Math.abs(ry); var Prx = rx * rx; var Pry = ry * ry; var Px1 = x1 * x1; var Py1 = y1 * y1; var radiiCheck = Px1/Prx + Py1/Pry; var radiiSqrt = Math.sqrt(radiiCheck); if (radiiCheck > 1) { rx = radiiSqrt * rx; ry = radiiSqrt * ry; Prx = rx * rx; Pry = ry * ry; } var sign = (largearc != sweep) ? -1 : 1; var sq = ((Prx * Pry) - (Prx * Py1) - (Pry * Px1)) / ((Prx * Py1) + (Pry * Px1)); sq = (sq < 0) ? 0 : sq; var coef = sign * Math.sqrt(sq); var cx1 = coef * ((rx * y1) / ry); var cy1 = coef * -((ry * x1) / rx); var cx = mid.x + (cos * cx1 - sin * cy1); var cy = mid.y + (sin * cx1 + cos * cy1); var ux = (x1 - cx1) / rx; var uy = (y1 - cy1) / ry; var vx = (-x1 - cx1) / rx; var vy = (-y1 - cy1) / ry; var n = Math.sqrt( (ux * ux) + (uy * uy) ); var p = ux; sign = (uy < 0) ? -1 : 1; var theta = sign * Math.acos( p / n ); theta = _radiansToDegrees(theta); n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); p = ux * vx + uy * vy; sign = ((ux * vy - uy * vx) < 0) ? -1 : 1; var delta = sign * Math.acos( p / n ); delta = _radiansToDegrees(delta); if (sweep == 1 && delta > 0) { delta -= 360; } else if (sweep == 0 && delta < 0) { delta += 360; } delta %= 360; theta %= 360; return { center: {x: cx, y: cy}, rx: rx, ry: ry, theta: theta, extent: delta, angle: angleDegrees }; } }, // returns the rectangular bounding box of the given polygon getPolygonBounds: function(polygon){ if(!polygon || polygon.length < 3){ return null; } var xmin = polygon[0].x; var xmax = polygon[0].x; var ymin = polygon[0].y; var ymax = polygon[0].y; for(var i=1; i xmax){ xmax = polygon[i].x; } else if(polygon[i].x < xmin){ xmin = polygon[i].x; } if(polygon[i].y > ymax){ ymax = polygon[i].y; } else if(polygon[i].y < ymin){ ymin = polygon[i].y; } } return { x: xmin, y: ymin, width: xmax-xmin, height: ymax-ymin }; }, // return true if point is in the polygon, false if outside, and null if exactly on a point or edge pointInPolygon: function(point, polygon){ if(!polygon || polygon.length < 3){ return null; } var inside = false; var offsetx = polygon.offsetx || 0; var offsety = polygon.offsety || 0; for (var i = 0, j = polygon.length - 1; i < polygon.length; j=i++) { var xi = polygon[i].x + offsetx; var yi = polygon[i].y + offsety; var xj = polygon[j].x + offsetx; var yj = polygon[j].y + offsety; if(_almostEqual(xi, point.x) && _almostEqual(yi, point.y)){ return null; // no result } if(_onSegment({x: xi, y: yi}, {x: xj, y: yj}, point)){ return null; // exactly on the segment } if(_almostEqual(xi, xj) && _almostEqual(yi, yj)){ // ignore very small lines continue; } var intersect = ((yi > point.y) != (yj > point.y)) && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }, // returns the area of the polygon, assuming no self-intersections // a negative area indicates counter-clockwise winding direction polygonArea: function(polygon){ var area = 0; var i, j; for (i=0, j=polygon.length-1; i max){ max = dot; } } // there may be multiple vertices with min/max values. In which case we choose the one that is normal-most (eg. left most) var indexmin = 0; var indexmax = 0; var normalmin = null; var normalmax = null; for(i=0; i normalmin){ normalmin = dot; indexmin = i; } } else if(_almostEqual(dotproduct[i] , max)){ var dot = polygon[i].x*normal.x + polygon[i].y*normal.y; if(normalmax === null || dot > normalmax){ normalmax = dot; indexmax = i; } } } // now we have two edges bound by min and max points, figure out which edge faces our direction vector var indexleft = indexmin-1; var indexright = indexmin+1; if(indexleft < 0){ indexleft = polygon.length-1; } if(indexright >= polygon.length){ indexright = 0; } var minvertex = polygon[indexmin]; var left = polygon[indexleft]; var right = polygon[indexright]; var leftvector = { x: left.x - minvertex.x, y: left.y - minvertex.y }; var rightvector = { x: right.x - minvertex.x, y: right.y - minvertex.y }; var dotleft = leftvector.x*direction.x + leftvector.y*direction.y; var dotright = rightvector.x*direction.x + rightvector.y*direction.y; // -1 = left, 1 = right var scandirection = -1; if(_almostEqual(dotleft, 0)){ scandirection = 1; } else if(_almostEqual(dotright, 0)){ scandirection = -1; } else{ var normaldotleft; var normaldotright; if(_almostEqual(dotleft, dotright)){ // the points line up exactly along the normal vector normaldotleft = leftvector.x*normal.x + leftvector.y*normal.y; normaldotright = rightvector.x*normal.x + rightvector.y*normal.y; } else if(dotleft < dotright){ // normalize right vertex so normal projection can be directly compared normaldotleft = leftvector.x*normal.x + leftvector.y*normal.y; normaldotright = (rightvector.x*normal.x + rightvector.y*normal.y)*(dotleft/dotright); } else{ // normalize left vertex so normal projection can be directly compared normaldotleft = leftvector.x*normal.x + leftvector.y*normal.y * (dotright/dotleft); normaldotright = rightvector.x*normal.x + rightvector.y*normal.y; } if(normaldotleft > normaldotright){ scandirection = -1; } else{ // technically they could be equal, (ie. the segments bound by left and right points are incident) // in which case we'll have to climb up the chain until lines are no longer incident // for now we'll just not handle it and assume people aren't giving us garbage input.. scandirection = 1; } } // connect all points between indexmin and indexmax along the scan direction var edge = []; var count = 0; i=indexmin; while(count < polygon.length){ if(i >= polygon.length){ i=0; } else if(i < 0){ i=polygon.length-1; } edge.push(polygon[i]); if(i == indexmax){ break; } i += scandirection; count++; } return edge; }, // returns the normal distance from p to a line segment defined by s1 s2 // this is basically algo 9 in [1], generalized for any vector direction // eg. normal of [-1, 0] returns the horizontal distance between the point and the line segment // sxinclusive: if true, include endpoints instead of excluding them pointLineDistance: function(p, s1, s2, normal, s1inclusive, s2inclusive){ normal = _normalizeVector(normal); var dir = { x: normal.y, y: -normal.x }; var pdot = p.x*dir.x + p.y*dir.y; var s1dot = s1.x*dir.x + s1.y*dir.y; var s2dot = s2.x*dir.x + s2.y*dir.y; var pdotnorm = p.x*normal.x + p.y*normal.y; var s1dotnorm = s1.x*normal.x + s1.y*normal.y; var s2dotnorm = s2.x*normal.x + s2.y*normal.y; // point is exactly along the edge in the normal direction if(_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)){ // point lies on an endpoint if(_almostEqual(pdotnorm, s1dotnorm) ){ return null; } if(_almostEqual(pdotnorm, s2dotnorm)){ return null; } // point is outside both endpoints if (pdotnorm>s1dotnorm && pdotnorm>s2dotnorm){ return Math.min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm); } if (pdotnorm 0){ return diff1; } else{ return diff2; } } // point else if(_almostEqual(pdot, s1dot)){ if(s1inclusive){ return pdotnorm-s1dotnorm; } else{ return null; } } else if(_almostEqual(pdot, s2dot)){ if(s2inclusive){ return pdotnorm-s2dotnorm; } else{ return null; } } else if ((pdots1dot && pdot>s2dot)){ return null; // point doesn't collide with segment } return (pdotnorm - s1dotnorm + (s1dotnorm - s2dotnorm)*(s1dot - pdot)/(s1dot - s2dot)); }, pointDistance: function(p, s1, s2, normal, infinite){ normal = _normalizeVector(normal); var dir = { x: normal.y, y: -normal.x }; var pdot = p.x*dir.x + p.y*dir.y; var s1dot = s1.x*dir.x + s1.y*dir.y; var s2dot = s2.x*dir.x + s2.y*dir.y; var pdotnorm = p.x*normal.x + p.y*normal.y; var s1dotnorm = s1.x*normal.x + s1.y*normal.y; var s2dotnorm = s2.x*normal.x + s2.y*normal.y; if(!infinite){ if (((pdots1dot || _almostEqual(pdot, s1dot)) && (pdot>s2dot || _almostEqual(pdot, s2dot)))){ return null; // dot doesn't collide with segment, or lies directly on the vertex } if ((_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)) && (pdotnorm>s1dotnorm && pdotnorm>s2dotnorm)){ return Math.min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm); } if ((_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)) && (pdotnorm EFmax){ return null; } var overlap; if((ABmax > EFmax && ABmin < EFmin) || (EFmax > ABmax && EFmin < ABmin)){ overlap = 1; } else{ var minMax = Math.min(ABmax, EFmax); var maxMin = Math.max(ABmin, EFmin); var maxMax = Math.max(ABmax, EFmax); var minMin = Math.min(ABmin, EFmin); overlap = (minMax-maxMin)/(maxMax-minMin); } var crossABE = (E.y - A.y) * (B.x - A.x) - (E.x - A.x) * (B.y - A.y); var crossABF = (F.y - A.y) * (B.x - A.x) - (F.x - A.x) * (B.y - A.y); // lines are colinear if(_almostEqual(crossABE,0) && _almostEqual(crossABF,0)){ var ABnorm = {x: B.y-A.y, y: A.x-B.x}; var EFnorm = {x: F.y-E.y, y: E.x-F.x}; var ABnormlength = Math.sqrt(ABnorm.x*ABnorm.x + ABnorm.y*ABnorm.y); ABnorm.x /= ABnormlength; ABnorm.y /= ABnormlength; var EFnormlength = Math.sqrt(EFnorm.x*EFnorm.x + EFnorm.y*EFnorm.y); EFnorm.x /= EFnormlength; EFnorm.y /= EFnormlength; // segment normals must point in opposite directions if(Math.abs(ABnorm.y * EFnorm.x - ABnorm.x * EFnorm.y) < TOL && ABnorm.y * EFnorm.y + ABnorm.x * EFnorm.x < 0){ // normal of AB segment must point in same direction as given direction vector var normdot = ABnorm.y * direction.y + ABnorm.x * direction.x; // the segments merely slide along eachother if(_almostEqual(normdot,0, TOL)){ return null; } if(normdot < 0){ return 0; } } return null; } var distances = []; // coincident points if(_almostEqual(dotA, dotE)){ distances.push(crossA-crossE); } else if(_almostEqual(dotA, dotF)){ distances.push(crossA-crossF); } else if(dotA > EFmin && dotA < EFmax){ var d = this.pointDistance(A,E,F,reverse); if(d !== null && _almostEqual(d, 0)){ // A currently touches EF, but AB is moving away from EF var dB = this.pointDistance(B,E,F,reverse,true); if(dB < 0 || _almostEqual(dB*overlap,0)){ d = null; } } if(d !== null){ distances.push(d); } } if(_almostEqual(dotB, dotE)){ distances.push(crossB-crossE); } else if(_almostEqual(dotB, dotF)){ distances.push(crossB-crossF); } else if(dotB > EFmin && dotB < EFmax){ var d = this.pointDistance(B,E,F,reverse); if(d !== null && _almostEqual(d, 0)){ // crossA>crossB A currently touches EF, but AB is moving away from EF var dA = this.pointDistance(A,E,F,reverse,true); if(dA < 0 || _almostEqual(dA*overlap,0)){ d = null; } } if(d !== null){ distances.push(d); } } if(dotE > ABmin && dotE < ABmax){ var d = this.pointDistance(E,A,B,direction); if(d !== null && _almostEqual(d, 0)){ // crossF ABmin && dotF < ABmax){ var d = this.pointDistance(F,A,B,direction); if(d !== null && _almostEqual(d, 0)){ // && crossE 0 || _almostEqual(d, 0)){ distance = d; } } } } return distance; }, // project each point of B onto A in the given direction, and return the polygonProjectionDistance: function(A, B, direction){ var Boffsetx = B.offsetx || 0; var Boffsety = B.offsety || 0; var Aoffsetx = A.offsetx || 0; var Aoffsety = A.offsety || 0; A = A.slice(0); B = B.slice(0); // close the loop for polygons if(A[0] != A[A.length-1]){ A.push(A[0]); } if(B[0] != B[B.length-1]){ B.push(B[0]); } var edgeA = A; var edgeB = B; var distance = null; var p, d, s1, s2; for(var i=0; i distance)){ distance = minprojection; } } return distance; }, // searches for an arrangement of A and B such that they do not overlap // if an NFP is given, only search for startpoints that have not already been traversed in the given NFP searchStartPoint: function(A,B,inside,NFP){ // clone arrays A = A.slice(0); B = B.slice(0); // close the loop for polygons if(A[0] != A[A.length-1]){ A.push(A[0]); } if(B[0] != B[B.length-1]){ B.push(B[0]); } for(var i=0; i 0){ } else{ continue; } var vd2 = vx*vx + vy*vy; if(d*d < vd2 && !_almostEqual(d*d, vd2)){ var vd = Math.sqrt(vx*vx + vy*vy); vx *= d/vd; vy *= d/vd; } B.offsetx += vx; B.offsety += vy; for(k=0; k maxAx){ maxAx = A[i].x; } if(A[i].y > maxAy){ maxAy = A[i].y; } } var minBx = B[0].x; var minBy = B[0].y; var maxBx = B[0].x; var maxBy = B[0].y; for(i=1; i maxBx){ maxBx = B[i].x; } if(B[i].y > maxBy){ maxBy = B[i].y; } } if(maxBx-minBx > maxAx-minAx){ return null; } if(maxBy-minBy > maxAy-minAy){ return null; } return [[ {x: minAx-minBx+B[0].x, y: minAy-minBy+B[0].y}, {x: maxAx-maxBx+B[0].x, y: minAy-minBy+B[0].y}, {x: maxAx-maxBx+B[0].x, y: maxAy-maxBy+B[0].y}, {x: minAx-minBx+B[0].x, y: maxAy-maxBy+B[0].y} ]]; }, // given a static polygon A and a movable polygon B, compute a no fit polygon by orbiting B about A // if the inside flag is set, B is orbited inside of A rather than outside // if the searchEdges flag is set, all edges of A are explored for NFPs - multiple noFitPolygon: function(A, B, inside, searchEdges){ if(!A || A.length < 3 || !B || B.length < 3){ return null; } A.offsetx = 0; A.offsety = 0; var i, j; var minA = A[0].y; var minAindex = 0; var maxB = B[0].y; var maxBindex = 0; for(i=1; i maxB){ maxB = B[i].y; maxBindex = i; } } if(!inside){ // shift B such that the bottom-most point of B is at the top-most point of A. This guarantees an initial placement with no intersections var startpoint = { x: A[minAindex].x-B[maxBindex].x, y: A[minAindex].y-B[maxBindex].y }; } else{ // no reliable heuristic for inside var startpoint = this.searchStartPoint(A,B,true); } var NFPlist = []; while(startpoint !== null){ B.offsetx = startpoint.x; B.offsety = startpoint.y; // maintain a list of touching points/edges var touching; var prevvector = null; // keep track of previous vector var NFP = [{ x: B[0].x+B.offsetx, y: B[0].y+B.offsety }]; var referencex = B[0].x+B.offsetx; var referencey = B[0].y+B.offsety; var startx = referencex; var starty = referencey; var counter = 0; while(counter < 10*(A.length + B.length)){ // sanity check, prevent infinite loop touching = []; // find touching vertices/edges for(i=0; i= A.length) ? 0 : nextAindex; // loop var prevA = A[prevAindex]; var nextA = A[nextAindex]; // adjacent B vertices var vertexB = B[touching[i].B]; var prevBindex = touching[i].B-1; var nextBindex = touching[i].B+1; prevBindex = (prevBindex < 0) ? B.length-1 : prevBindex; // loop nextBindex = (nextBindex >= B.length) ? 0 : nextBindex; // loop var prevB = B[prevBindex]; var nextB = B[nextBindex]; if(touching[i].type == 0){ var vA1 = { x: prevA.x-vertexA.x, y: prevA.y-vertexA.y, start: vertexA, end: prevA }; var vA2 = { x: nextA.x-vertexA.x, y: nextA.y-vertexA.y, start: vertexA, end: nextA }; // B vectors need to be inverted var vB1 = { x: vertexB.x-prevB.x, y: vertexB.y-prevB.y, start: prevB, end: vertexB }; var vB2 = { x: vertexB.x-nextB.x, y: vertexB.y-nextB.y, start: nextB, end: vertexB }; vectors.push(vA1); vectors.push(vA2); vectors.push(vB1); vectors.push(vB2); } else if(touching[i].type == 1){ vectors.push({ x: vertexA.x-(vertexB.x+B.offsetx), y: vertexA.y-(vertexB.y+B.offsety), start: prevA, end: vertexA }); vectors.push({ x: prevA.x-(vertexB.x+B.offsetx), y: prevA.y-(vertexB.y+B.offsety), start: vertexA, end: prevA }); } else if(touching[i].type == 2){ vectors.push({ x: vertexA.x-(vertexB.x+B.offsetx), y: vertexA.y-(vertexB.y+B.offsety), start: prevB, end: vertexB }); vectors.push({ x: vertexA.x-(prevB.x+B.offsetx), y: vertexA.y-(prevB.y+B.offsety), start: vertexB, end: prevB }); } } // todo: there should be a faster way to reject vectors that will cause immediate intersection. For now just check them all var translate = null; var maxd = 0; for(i=0; i vecd2){ var vecd = Math.sqrt(vectors[i].x*vectors[i].x + vectors[i].y*vectors[i].y); d = vecd; } if(d !== null && d > maxd){ maxd = d; translate = vectors[i]; } } if(translate === null || _almostEqual(maxd, 0)){ // didn't close the loop, something went wrong here NFP = null; break; } translate.start.marked = true; translate.end.marked = true; prevvector = translate; // trim var vlength2 = translate.x*translate.x + translate.y*translate.y; if(maxd*maxd < vlength2 && !_almostEqual(maxd*maxd, vlength2)){ var scale = Math.sqrt((maxd*maxd)/vlength2); translate.x *= scale; translate.y *= scale; } referencex += translate.x; referencey += translate.y; if(_almostEqual(referencex, startx) && _almostEqual(referencey, starty)){ // we've made a full loop break; } // if A and B start on a touching horizontal line, the end point may not be the start point var looped = false; if(NFP.length > 0){ for(i=0; i 0){ NFPlist.push(NFP); } if(!searchEdges){ // only get outer NFP or first inner NFP break; } startpoint = this.searchStartPoint(A,B,inside,NFPlist); } return NFPlist; }, // given two polygons that touch at at least one point, but do not intersect. Return the outer perimeter of both polygons as a single continuous polygon // A and B must have the same winding direction polygonHull: function(A,B){ if(!A || A.length < 3 || !B || B.length < 3){ return null; } var i, j; var Aoffsetx = A.offsetx || 0; var Aoffsety = A.offsety || 0; var Boffsetx = B.offsetx || 0; var Boffsety = B.offsety || 0; // start at an extreme point that is guaranteed to be on the final polygon var miny = A[0].y; var startPolygon = A; var startIndex = 0; for(i=0; i