You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1892 lines
50 KiB
JavaScript
1892 lines
50 KiB
JavaScript
|
1 month ago
|
/*!
|
||
|
|
* 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<polygon.length; i++){
|
||
|
|
if(polygon[i].x > 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<polygon.length; j=i++){
|
||
|
|
area += (polygon[j].x+polygon[i].x) * (polygon[j].y-polygon[i].y);
|
||
|
|
}
|
||
|
|
return 0.5*area;
|
||
|
|
},
|
||
|
|
|
||
|
|
// todo: swap this for a more efficient sweep-line implementation
|
||
|
|
// returnEdges: if set, return all edges on A that have intersections
|
||
|
|
|
||
|
|
intersect: function(A,B){
|
||
|
|
var Aoffsetx = A.offsetx || 0;
|
||
|
|
var Aoffsety = A.offsety || 0;
|
||
|
|
|
||
|
|
var Boffsetx = B.offsetx || 0;
|
||
|
|
var Boffsety = B.offsety || 0;
|
||
|
|
|
||
|
|
A = A.slice(0);
|
||
|
|
B = B.slice(0);
|
||
|
|
|
||
|
|
for(var i=0; i<A.length-1; i++){
|
||
|
|
for(var j=0; j<B.length-1; j++){
|
||
|
|
var a1 = {x: A[i].x+Aoffsetx, y:A[i].y+Aoffsety};
|
||
|
|
var a2 = {x: A[i+1].x+Aoffsetx, y:A[i+1].y+Aoffsety};
|
||
|
|
var b1 = {x: B[j].x+Boffsetx, y: B[j].y+Boffsety};
|
||
|
|
var b2 = {x: B[j+1].x+Boffsetx, y: B[j+1].y+Boffsety};
|
||
|
|
|
||
|
|
var prevbindex = (j == 0) ? B.length-1 : j-1;
|
||
|
|
var prevaindex = (i == 0) ? A.length-1 : i-1;
|
||
|
|
var nextbindex = (j+1 == B.length-1) ? 0 : j+2;
|
||
|
|
var nextaindex = (i+1 == A.length-1) ? 0 : i+2;
|
||
|
|
|
||
|
|
// go even further back if we happen to hit on a loop end point
|
||
|
|
if(B[prevbindex] == B[j] || (_almostEqual(B[prevbindex].x, B[j].x) && _almostEqual(B[prevbindex].y, B[j].y))){
|
||
|
|
prevbindex = (prevbindex == 0) ? B.length-1 : prevbindex-1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(A[prevaindex] == A[i] || (_almostEqual(A[prevaindex].x, A[i].x) && _almostEqual(A[prevaindex].y, A[i].y))){
|
||
|
|
prevaindex = (prevaindex == 0) ? A.length-1 : prevaindex-1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// go even further forward if we happen to hit on a loop end point
|
||
|
|
if(B[nextbindex] == B[j+1] || (_almostEqual(B[nextbindex].x, B[j+1].x) && _almostEqual(B[nextbindex].y, B[j+1].y))){
|
||
|
|
nextbindex = (nextbindex == B.length-1) ? 0 : nextbindex+1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(A[nextaindex] == A[i+1] || (_almostEqual(A[nextaindex].x, A[i+1].x) && _almostEqual(A[nextaindex].y, A[i+1].y))){
|
||
|
|
nextaindex = (nextaindex == A.length-1) ? 0 : nextaindex+1;
|
||
|
|
}
|
||
|
|
|
||
|
|
var a0 = {x: A[prevaindex].x + Aoffsetx, y: A[prevaindex].y + Aoffsety};
|
||
|
|
var b0 = {x: B[prevbindex].x + Boffsetx, y: B[prevbindex].y + Boffsety};
|
||
|
|
|
||
|
|
var a3 = {x: A[nextaindex].x + Aoffsetx, y: A[nextaindex].y + Aoffsety};
|
||
|
|
var b3 = {x: B[nextbindex].x + Boffsetx, y: B[nextbindex].y + Boffsety};
|
||
|
|
|
||
|
|
if(_onSegment(a1,a2,b1) || (_almostEqual(a1.x, b1.x) && _almostEqual(a1.y, b1.y))){
|
||
|
|
// if a point is on a segment, it could intersect or it could not. Check via the neighboring points
|
||
|
|
var b0in = this.pointInPolygon(b0, A);
|
||
|
|
var b2in = this.pointInPolygon(b2, A);
|
||
|
|
if((b0in === true && b2in === false) || (b0in === false && b2in === true)){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
else{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(_onSegment(a1,a2,b2) || (_almostEqual(a2.x, b2.x) && _almostEqual(a2.y, b2.y))){
|
||
|
|
// if a point is on a segment, it could intersect or it could not. Check via the neighboring points
|
||
|
|
var b1in = this.pointInPolygon(b1, A);
|
||
|
|
var b3in = this.pointInPolygon(b3, A);
|
||
|
|
|
||
|
|
if((b1in === true && b3in === false) || (b1in === false && b3in === true)){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
else{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(_onSegment(b1,b2,a1) || (_almostEqual(a1.x, b2.x) && _almostEqual(a1.y, b2.y))){
|
||
|
|
// if a point is on a segment, it could intersect or it could not. Check via the neighboring points
|
||
|
|
var a0in = this.pointInPolygon(a0, B);
|
||
|
|
var a2in = this.pointInPolygon(a2, B);
|
||
|
|
|
||
|
|
if((a0in === true && a2in === false) || (a0in === false && a2in === true)){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
else{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(_onSegment(b1,b2,a2) || (_almostEqual(a2.x, b1.x) && _almostEqual(a2.y, b1.y))){
|
||
|
|
// if a point is on a segment, it could intersect or it could not. Check via the neighboring points
|
||
|
|
var a1in = this.pointInPolygon(a1, B);
|
||
|
|
var a3in = this.pointInPolygon(a3, B);
|
||
|
|
|
||
|
|
if((a1in === true && a3in === false) || (a1in === false && a3in === true)){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
else{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var p = _lineIntersect(b1, b2, a1, a2);
|
||
|
|
|
||
|
|
if(p !== null){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
},
|
||
|
|
|
||
|
|
// placement algos as outlined in [1] http://www.cs.stir.ac.uk/~goc/papers/EffectiveHueristic2DAOR2013.pdf
|
||
|
|
|
||
|
|
// returns a continuous polyline representing the normal-most edge of the given polygon
|
||
|
|
// eg. a normal vector of [-1, 0] will return the left-most edge of the polygon
|
||
|
|
// this is essentially algo 8 in [1], generalized for any vector direction
|
||
|
|
polygonEdge: function(polygon, normal){
|
||
|
|
if(!polygon || polygon.length < 3){
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
normal = _normalizeVector(normal);
|
||
|
|
|
||
|
|
var direction = {
|
||
|
|
x: -normal.y,
|
||
|
|
y: normal.x
|
||
|
|
};
|
||
|
|
|
||
|
|
// find the max and min points, they will be the endpoints of our edge
|
||
|
|
var min = null;
|
||
|
|
var max = null;
|
||
|
|
|
||
|
|
var dotproduct = [];
|
||
|
|
|
||
|
|
for(var i=0; i<polygon.length; i++){
|
||
|
|
var dot = polygon[i].x*direction.x + polygon[i].y*direction.y;
|
||
|
|
dotproduct.push(dot);
|
||
|
|
if(min === null || dot < min){
|
||
|
|
min = dot;
|
||
|
|
}
|
||
|
|
if(max === null || dot > 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<polygon.length; i++){
|
||
|
|
if(_almostEqual(dotproduct[i] , min)){
|
||
|
|
var dot = polygon[i].x*normal.x + polygon[i].y*normal.y;
|
||
|
|
if(normalmin === null || dot > 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<s1dotnorm && pdotnorm<s2dotnorm){
|
||
|
|
return -Math.min(s1dotnorm-pdotnorm, s2dotnorm-pdotnorm);
|
||
|
|
}
|
||
|
|
|
||
|
|
// point lies between endpoints
|
||
|
|
var diff1 = pdotnorm - s1dotnorm;
|
||
|
|
var diff2 = pdotnorm - s2dotnorm;
|
||
|
|
if(diff1 > 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 ((pdot<s1dot && pdot<s2dot) || (pdot>s1dot && 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 (((pdot<s1dot || _almostEqual(pdot, s1dot)) && (pdot<s2dot || _almostEqual(pdot, s2dot))) || ((pdot>s1dot || _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<s1dotnorm && pdotnorm<s2dotnorm)){
|
||
|
|
return -Math.min(s1dotnorm-pdotnorm, s2dotnorm-pdotnorm);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return -(pdotnorm - s1dotnorm + (s1dotnorm - s2dotnorm)*(s1dot - pdot)/(s1dot - s2dot));
|
||
|
|
},
|
||
|
|
|
||
|
|
segmentDistance: function(A, B, E, F, direction){
|
||
|
|
var normal = {
|
||
|
|
x: direction.y,
|
||
|
|
y: -direction.x
|
||
|
|
};
|
||
|
|
|
||
|
|
var reverse = {
|
||
|
|
x: -direction.x,
|
||
|
|
y: -direction.y
|
||
|
|
};
|
||
|
|
|
||
|
|
var dotA = A.x*normal.x + A.y*normal.y;
|
||
|
|
var dotB = B.x*normal.x + B.y*normal.y;
|
||
|
|
var dotE = E.x*normal.x + E.y*normal.y;
|
||
|
|
var dotF = F.x*normal.x + F.y*normal.y;
|
||
|
|
|
||
|
|
var crossA = A.x*direction.x + A.y*direction.y;
|
||
|
|
var crossB = B.x*direction.x + B.y*direction.y;
|
||
|
|
var crossE = E.x*direction.x + E.y*direction.y;
|
||
|
|
var crossF = F.x*direction.x + F.y*direction.y;
|
||
|
|
|
||
|
|
var crossABmin = Math.min(crossA,crossB);
|
||
|
|
var crossABmax = Math.max(crossA,crossB);
|
||
|
|
|
||
|
|
var crossEFmax = Math.max(crossE,crossF);
|
||
|
|
var crossEFmin = Math.min(crossE,crossF);
|
||
|
|
|
||
|
|
var ABmin = Math.min(dotA,dotB);
|
||
|
|
var ABmax = Math.max(dotA,dotB);
|
||
|
|
|
||
|
|
var EFmax = Math.max(dotE,dotF);
|
||
|
|
var EFmin = Math.min(dotE,dotF);
|
||
|
|
|
||
|
|
// segments that will merely touch at one point
|
||
|
|
if(_almostEqual(ABmax, EFmin,TOL) || _almostEqual(ABmin, EFmax,TOL)){
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// segments miss eachother completely
|
||
|
|
if(ABmax < EFmin || ABmin > 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<crossE A currently touches EF, but AB is moving away from EF
|
||
|
|
var dF = this.pointDistance(F,A,B,direction, true);
|
||
|
|
if(dF < 0 || _almostEqual(dF*overlap,0)){
|
||
|
|
d = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if(d !== null){
|
||
|
|
distances.push(d);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(dotF > ABmin && dotF < ABmax){
|
||
|
|
var d = this.pointDistance(F,A,B,direction);
|
||
|
|
if(d !== null && _almostEqual(d, 0)){ // && crossE<crossF A currently touches EF, but AB is moving away from EF
|
||
|
|
var dE = this.pointDistance(E,A,B,direction, true);
|
||
|
|
if(dE < 0 || _almostEqual(dE*overlap,0)){
|
||
|
|
d = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if(d !== null){
|
||
|
|
distances.push(d);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(distances.length == 0){
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Math.min.apply(Math, distances);
|
||
|
|
},
|
||
|
|
|
||
|
|
polygonSlideDistance: function(A, B, direction, ignoreNegative){
|
||
|
|
|
||
|
|
var A1, A2, B1, B2, Aoffsetx, Aoffsety, Boffsetx, Boffsety;
|
||
|
|
|
||
|
|
Aoffsetx = A.offsetx || 0;
|
||
|
|
Aoffsety = A.offsety || 0;
|
||
|
|
|
||
|
|
Boffsetx = B.offsetx || 0;
|
||
|
|
Boffsety = B.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, s1, s2, d;
|
||
|
|
|
||
|
|
|
||
|
|
var dir = _normalizeVector(direction);
|
||
|
|
|
||
|
|
var normal = {
|
||
|
|
x: dir.y,
|
||
|
|
y: -dir.x
|
||
|
|
};
|
||
|
|
|
||
|
|
var reverse = {
|
||
|
|
x: -dir.x,
|
||
|
|
y: -dir.y,
|
||
|
|
};
|
||
|
|
|
||
|
|
for(var i=0; i<edgeB.length-1; i++){
|
||
|
|
var mind = null;
|
||
|
|
for(var j=0; j<edgeA.length-1; j++){
|
||
|
|
A1 = {x: edgeA[j].x + Aoffsetx, y: edgeA[j].y + Aoffsety};
|
||
|
|
A2 = {x: edgeA[j+1].x + Aoffsetx, y: edgeA[j+1].y + Aoffsety};
|
||
|
|
B1 = {x: edgeB[i].x + Boffsetx, y: edgeB[i].y + Boffsety};
|
||
|
|
B2 = {x: edgeB[i+1].x + Boffsetx, y: edgeB[i+1].y + Boffsety};
|
||
|
|
|
||
|
|
if((_almostEqual(A1.x, A2.x) && _almostEqual(A1.y, A2.y)) || (_almostEqual(B1.x, B2.x) && _almostEqual(B1.y, B2.y))){
|
||
|
|
continue; // ignore extremely small lines
|
||
|
|
}
|
||
|
|
|
||
|
|
d = this.segmentDistance(A1, A2, B1, B2, dir);
|
||
|
|
|
||
|
|
if(d !== null && (distance === null || d < distance)){
|
||
|
|
if(!ignoreNegative || d > 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<edgeB.length; i++){
|
||
|
|
// the shortest/most negative projection of B onto A
|
||
|
|
var minprojection = null;
|
||
|
|
var minp = null;
|
||
|
|
for(var j=0; j<edgeA.length-1; j++){
|
||
|
|
p = {x : edgeB[i].x + Boffsetx, y : edgeB[i].y + Boffsety};
|
||
|
|
s1 = {x: edgeA[j].x + Aoffsetx, y: edgeA[j].y + Aoffsety};
|
||
|
|
s2 = {x: edgeA[j+1].x + Aoffsetx, y: edgeA[j+1].y + Aoffsety} ;
|
||
|
|
|
||
|
|
if(Math.abs((s2.y-s1.y) * direction.x - (s2.x-s1.x) * direction.y) < TOL){
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// project point, ignore edge boundaries
|
||
|
|
d = this.pointDistance(p, s1, s2, direction);
|
||
|
|
|
||
|
|
if(d !== null && (minprojection === null || d < minprojection)){
|
||
|
|
minprojection = d;
|
||
|
|
minp = p;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if(minprojection !== null && (distance === null || minprojection > 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<A.length-1; i++){
|
||
|
|
if(!A[i].marked){
|
||
|
|
A[i].marked = true;
|
||
|
|
for(var j=0; j<B.length; j++){
|
||
|
|
B.offsetx = A[i].x - B[j].x;
|
||
|
|
B.offsety = A[i].y - B[j].y;
|
||
|
|
|
||
|
|
var Binside = null;
|
||
|
|
for(var k=0; k<B.length; k++){
|
||
|
|
var inpoly = this.pointInPolygon({x: B[k].x + B.offsetx, y: B[k].y + B.offsety}, A);
|
||
|
|
if(inpoly !== null){
|
||
|
|
Binside = inpoly;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(Binside === null){ // A and B are the same
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
var startPoint = {x: B.offsetx, y: B.offsety};
|
||
|
|
if(((Binside && inside) || (!Binside && !inside)) && !this.intersect(A,B) && !inNfp(startPoint, NFP)){
|
||
|
|
return startPoint;
|
||
|
|
}
|
||
|
|
|
||
|
|
// slide B along vector
|
||
|
|
var vx = A[i+1].x - A[i].x;
|
||
|
|
var vy = A[i+1].y - A[i].y;
|
||
|
|
|
||
|
|
var d1 = this.polygonProjectionDistance(A,B,{x: vx, y: vy});
|
||
|
|
var d2 = this.polygonProjectionDistance(B,A,{x: -vx, y: -vy});
|
||
|
|
|
||
|
|
var d = null;
|
||
|
|
|
||
|
|
// todo: clean this up
|
||
|
|
if(d1 === null && d2 === null){
|
||
|
|
// nothin
|
||
|
|
}
|
||
|
|
else if(d1 === null){
|
||
|
|
d = d2;
|
||
|
|
}
|
||
|
|
else if(d2 === null){
|
||
|
|
d = d1;
|
||
|
|
}
|
||
|
|
else{
|
||
|
|
d = Math.min(d1,d2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// only slide until no longer negative
|
||
|
|
// todo: clean this up
|
||
|
|
if(d !== null && !_almostEqual(d,0) && d > 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<B.length; k++){
|
||
|
|
var inpoly = this.pointInPolygon({x: B[k].x + B.offsetx, y: B[k].y + B.offsety}, A);
|
||
|
|
if(inpoly !== null){
|
||
|
|
Binside = inpoly;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
startPoint = {x: B.offsetx, y: B.offsety};
|
||
|
|
if(((Binside && inside) || (!Binside && !inside)) && !this.intersect(A,B) && !inNfp(startPoint, NFP)){
|
||
|
|
return startPoint;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// returns true if point already exists in the given nfp
|
||
|
|
function inNfp(p, nfp){
|
||
|
|
if(!nfp || nfp.length == 0){
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
for(var i=0; i<nfp.length; i++){
|
||
|
|
for(var j=0; j<nfp[i].length; j++){
|
||
|
|
if(_almostEqual(p.x, nfp[i][j].x) && _almostEqual(p.y, nfp[i][j].y)){
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
|
||
|
|
isRectangle: function(poly, tolerance){
|
||
|
|
var bb = this.getPolygonBounds(poly);
|
||
|
|
tolerance = tolerance || TOL;
|
||
|
|
|
||
|
|
for(var i=0; i<poly.length; i++){
|
||
|
|
if(!_almostEqual(poly[i].x, bb.x) && !_almostEqual(poly[i].x, bb.x+bb.width)){
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if(!_almostEqual(poly[i].y, bb.y) && !_almostEqual(poly[i].y, bb.y+bb.height)){
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
},
|
||
|
|
|
||
|
|
// returns an interior NFP for the special case where A is a rectangle
|
||
|
|
noFitPolygonRectangle: function(A, B){
|
||
|
|
var minAx = A[0].x;
|
||
|
|
var minAy = A[0].y;
|
||
|
|
var maxAx = A[0].x;
|
||
|
|
var maxAy = A[0].y;
|
||
|
|
|
||
|
|
for(var i=1; i<A.length; i++){
|
||
|
|
if(A[i].x < minAx){
|
||
|
|
minAx = A[i].x;
|
||
|
|
}
|
||
|
|
if(A[i].y < minAy){
|
||
|
|
minAy = A[i].y;
|
||
|
|
}
|
||
|
|
if(A[i].x > 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<B.length; i++){
|
||
|
|
if(B[i].x < minBx){
|
||
|
|
minBx = B[i].x;
|
||
|
|
}
|
||
|
|
if(B[i].y < minBy){
|
||
|
|
minBy = B[i].y;
|
||
|
|
}
|
||
|
|
if(B[i].x > 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<A.length; i++){
|
||
|
|
A[i].marked = false;
|
||
|
|
if(A[i].y < minA){
|
||
|
|
minA = A[i].y;
|
||
|
|
minAindex = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for(i=1; i<B.length; i++){
|
||
|
|
B[i].marked = false;
|
||
|
|
if(B[i].y > 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; i++){
|
||
|
|
var nexti = (i==A.length-1) ? 0 : i+1;
|
||
|
|
for(j=0; j<B.length; j++){
|
||
|
|
var nextj = (j==B.length-1) ? 0 : j+1;
|
||
|
|
if(_almostEqual(A[i].x, B[j].x+B.offsetx) && _almostEqual(A[i].y, B[j].y+B.offsety)){
|
||
|
|
touching.push({ type: 0, A: i, B: j });
|
||
|
|
}
|
||
|
|
else if(_onSegment(A[i],A[nexti],{x: B[j].x+B.offsetx, y: B[j].y + B.offsety})){
|
||
|
|
touching.push({ type: 1, A: nexti, B: j });
|
||
|
|
}
|
||
|
|
else if(_onSegment({x: B[j].x+B.offsetx, y: B[j].y + B.offsety},{x: B[nextj].x+B.offsetx, y: B[nextj].y + B.offsety},A[i])){
|
||
|
|
touching.push({ type: 2, A: i, B: nextj });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// generate translation vectors from touching vertices/edges
|
||
|
|
var vectors = [];
|
||
|
|
for(i=0; i<touching.length; i++){
|
||
|
|
var vertexA = A[touching[i].A];
|
||
|
|
vertexA.marked = true;
|
||
|
|
|
||
|
|
// adjacent A vertices
|
||
|
|
var prevAindex = touching[i].A-1;
|
||
|
|
var nextAindex = touching[i].A+1;
|
||
|
|
|
||
|
|
prevAindex = (prevAindex < 0) ? A.length-1 : prevAindex; // loop
|
||
|
|
nextAindex = (nextAindex >= 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<vectors.length; i++){
|
||
|
|
if(vectors[i].x == 0 && vectors[i].y == 0){
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// if this vector points us back to where we came from, ignore it.
|
||
|
|
// ie cross product = 0, dot product < 0
|
||
|
|
if(prevvector && vectors[i].y * prevvector.y + vectors[i].x * prevvector.x < 0){
|
||
|
|
|
||
|
|
// compare magnitude with unit vectors
|
||
|
|
var vectorlength = Math.sqrt(vectors[i].x*vectors[i].x+vectors[i].y*vectors[i].y);
|
||
|
|
var unitv = {x: vectors[i].x/vectorlength, y:vectors[i].y/vectorlength};
|
||
|
|
|
||
|
|
var prevlength = Math.sqrt(prevvector.x*prevvector.x+prevvector.y*prevvector.y);
|
||
|
|
var prevunit = {x: prevvector.x/prevlength, y:prevvector.y/prevlength};
|
||
|
|
|
||
|
|
// we need to scale down to unit vectors to normalize vector length. Could also just do a tan here
|
||
|
|
if(Math.abs(unitv.y * prevunit.x - unitv.x * prevunit.y) < 0.0001){
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var d = this.polygonSlideDistance(A, B, vectors[i], true);
|
||
|
|
var vecd2 = vectors[i].x*vectors[i].x + vectors[i].y*vectors[i].y;
|
||
|
|
|
||
|
|
if(d === null || d*d > 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<NFP.length-1; i++){
|
||
|
|
if(_almostEqual(referencex, NFP[i].x) && _almostEqual(referencey, NFP[i].y)){
|
||
|
|
looped = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(looped){
|
||
|
|
// we've made a full loop
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
NFP.push({
|
||
|
|
x: referencex,
|
||
|
|
y: referencey
|
||
|
|
});
|
||
|
|
|
||
|
|
B.offsetx += translate.x;
|
||
|
|
B.offsety += translate.y;
|
||
|
|
|
||
|
|
counter++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(NFP && NFP.length > 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<A.length; i++){
|
||
|
|
if(A[i].y+Aoffsety < miny){
|
||
|
|
miny = A[i].y+Aoffsety;
|
||
|
|
startPolygon = A;
|
||
|
|
startIndex = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for(i=0; i<B.length; i++){
|
||
|
|
if(B[i].y+Boffsety < miny){
|
||
|
|
miny = B[i].y+Boffsety;
|
||
|
|
startPolygon = B;
|
||
|
|
startIndex = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// for simplicity we'll define polygon A as the starting polygon
|
||
|
|
if(startPolygon == B){
|
||
|
|
B = A;
|
||
|
|
A = startPolygon;
|
||
|
|
Aoffsetx = A.offsetx || 0;
|
||
|
|
Aoffsety = A.offsety || 0;
|
||
|
|
Boffsetx = B.offsetx || 0;
|
||
|
|
Boffsety = B.offsety || 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
A = A.slice(0);
|
||
|
|
B = B.slice(0);
|
||
|
|
|
||
|
|
var C = [];
|
||
|
|
var current = startIndex;
|
||
|
|
var intercept1 = null;
|
||
|
|
var intercept2 = null;
|
||
|
|
|
||
|
|
// scan forward from the starting point
|
||
|
|
for(i=0; i<A.length+1; i++){
|
||
|
|
current = (current == A.length) ? 0 : current;
|
||
|
|
var next = (current==A.length-1) ? 0 : current+1;
|
||
|
|
var touching = false;
|
||
|
|
for(j=0; j<B.length; j++){
|
||
|
|
var nextj = (j==B.length-1) ? 0 : j+1;
|
||
|
|
if(_almostEqual(A[current].x+Aoffsetx, B[j].x+Boffsetx) && _almostEqual(A[current].y+Aoffsety, B[j].y+Boffsety)){
|
||
|
|
C.push({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
intercept1 = j;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
else if(_onSegment({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety},{x: A[next].x+Aoffsetx, y: A[next].y+Aoffsety},{x: B[j].x+Boffsetx, y: B[j].y + Boffsety})){
|
||
|
|
C.push({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
C.push({x: B[j].x+Boffsetx, y: B[j].y + Boffsety});
|
||
|
|
intercept1 = j;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
else if(_onSegment({x: B[j].x+Boffsetx, y: B[j].y + Boffsety},{x: B[nextj].x+Boffsetx, y: B[nextj].y + Boffsety},{x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety})){
|
||
|
|
C.push({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
C.push({x: B[nextj].x+Boffsetx, y: B[nextj].y + Boffsety});
|
||
|
|
intercept1 = nextj;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(touching){
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
C.push({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
|
||
|
|
current++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// scan backward from the starting point
|
||
|
|
current = startIndex-1;
|
||
|
|
for(i=0; i<A.length+1; i++){
|
||
|
|
current = (current<0) ? A.length-1 : current;
|
||
|
|
var next = (current==0) ? A.length-1 : current-1;
|
||
|
|
var touching = false;
|
||
|
|
for(j=0; j<B.length; j++){
|
||
|
|
var nextj = (j==B.length-1) ? 0 : j+1;
|
||
|
|
if(_almostEqual(A[current].x+Aoffsetx, B[j].x+Boffsetx) && _almostEqual(A[current].y, B[j].y+Boffsety)){
|
||
|
|
C.unshift({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
intercept2 = j;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
else if(_onSegment({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety},{x: A[next].x+Aoffsetx, y: A[next].y+Aoffsety},{x: B[j].x+Boffsetx, y: B[j].y + Boffsety})){
|
||
|
|
C.unshift({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
C.unshift({x: B[j].x+Boffsetx, y: B[j].y + Boffsety});
|
||
|
|
intercept2 = j;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
else if(_onSegment({x: B[j].x+Boffsetx, y: B[j].y + Boffsety},{x: B[nextj].x+Boffsetx, y: B[nextj].y + Boffsety},{x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety})){
|
||
|
|
C.unshift({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
intercept2 = j;
|
||
|
|
touching = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if(touching){
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
C.unshift({x: A[current].x+Aoffsetx, y: A[current].y+Aoffsety});
|
||
|
|
|
||
|
|
current--;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if(intercept1 === null || intercept2 === null){
|
||
|
|
// polygons not touching?
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// the relevant points on B now lie between intercept1 and intercept2
|
||
|
|
current = intercept1+1;
|
||
|
|
for(i=0; i<B.length; i++){
|
||
|
|
current = (current==B.length) ? 0 : current;
|
||
|
|
C.push({x: B[current].x+Boffsetx, y: B[current].y + Boffsety});
|
||
|
|
|
||
|
|
if(current == intercept2){
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
current++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// dedupe
|
||
|
|
for(i=0; i<C.length; i++){
|
||
|
|
var next = (i==C.length-1) ? 0 : i+1;
|
||
|
|
if(_almostEqual(C[i].x,C[next].x) && _almostEqual(C[i].y,C[next].y)){
|
||
|
|
C.splice(i,1);
|
||
|
|
i--;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return C;
|
||
|
|
},
|
||
|
|
|
||
|
|
rotatePolygon: function(polygon, angle){
|
||
|
|
var rotated = [];
|
||
|
|
angle = angle * Math.PI / 180;
|
||
|
|
for(var i=0; i<polygon.length; i++){
|
||
|
|
var x = polygon[i].x;
|
||
|
|
var y = polygon[i].y;
|
||
|
|
var x1 = x*Math.cos(angle)-y*Math.sin(angle);
|
||
|
|
var y1 = x*Math.sin(angle)+y*Math.cos(angle);
|
||
|
|
|
||
|
|
rotated.push({x:x1, y:y1});
|
||
|
|
}
|
||
|
|
// reset bounding box
|
||
|
|
var bounds = GeometryUtil.getPolygonBounds(rotated);
|
||
|
|
rotated.x = bounds.x;
|
||
|
|
rotated.y = bounds.y;
|
||
|
|
rotated.width = bounds.width;
|
||
|
|
rotated.height = bounds.height;
|
||
|
|
|
||
|
|
return rotated;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
})(typeof window !== 'undefined' ? window : self);
|