@benningfield-group/amc-graphics
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -1,1 +0,1 @@ | ||
"use strict";angular.module("AMC.Graphics",[]),angular.module("AMC.Graphics").directive("amcMapGraphic",["$parse",function(a){return{restrict:"A",link:function(b,c,d){function e(){var a=g();a&&f.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",a)}var f=c[0],g=a(d.amcMapGraphic).bind(this,b);b.$watch(g,e),e()}}}]),angular.module("AMC.Graphics").directive("amcSvgSize",["$parse",function(a){return{restrict:"A",link:function(b,c,d){function e(){var a=i();a&&(h.setAttributeNS(null,"width",a.getWidth()),h.setAttributeNS(null,"height",a.getHeight()))}function f(){var a=i();return a?a.getWidth():null}function g(){var a=i();return a?a.getHeight():null}var h=c[0],i=a(d.amcSvgSize).bind(this,b);b.$watch(f,e),b.$watch(g,e),e()}}}]),angular.module("AMC.Graphics").directive("amcTransform",["$parse","matrixHelper",function(a,b){return{restrict:"A",link:function(c,d,e){function f(){var a=h();a&&g.setAttributeNS(null,"transform",b.matrixToString(a))}var g=d[0],h=a(e.amcTransform).bind(this,c);c.$watch(h,f),f()}}}]),angular.module("AMC.Graphics").factory("linearMath",["vectorMath",function(a){function b(){}return b.prototype.getIntersections=function(b,c,d,e){var f=a.subtract(c,b),g=a.subtract(e,d),h=a.cross(a.subtract(d,b),f),i=a.cross(f,g);if(0===h&&0===i)return d.x>=b.x&&d.x<=c.x&&e.x>=b.x&&e.x<=c.x&&d.y>=b.y&&d.y<=c.y&&e.y>=b.y&&e.y<=c.y?[d,e]:b.x>=d.x&&b.x<=e.x&&c.x>=d.x&&c.x<=e.x&&b.y>=d.y&&b.y<=e.y&&c.y>=d.y&&c.y<=e.y?[b,c]:d.x>=b.x&&d.x<=c.x&&d.y>=b.y&&d.y<=c.y?[d]:e.x>=b.x&&e.x<=c.x&&e.y>=b.y&&e.y<=c.y?[e]:[];if(0===i)return[];var j=h/i,k=a.cross(a.subtract(d,b),g)/i;return k>=0&&k<=1&&j>=0&&j<=1?[a.add(b,a.multiplyScalar(f,k))]:[]},new b}]),angular.module("AMC.Graphics").factory("matrixHelper",["$window",function(a){function b(){this._svg=a.document.createElementNS("http://www.w3.org/2000/svg","svg")}return b.prototype.verticesToString=function(a){return a.map(function(a){return a.x+","+a.y}).join(" ")},b.prototype.matrixToArray=function(a){return[a.a,a.b,a.c,a.d,a.e,a.f]},b.prototype.matrixToString=function(a,b){var c=this.matrixToArray(a).join(" ");return b?c:"matrix("+c+")"},b.prototype.createSVGMatrix=function(a,b,c,d,e,f){var g=this._svg.createSVGMatrix();return void 0!==a&&(g.a=a),void 0!==b&&(g.b=b),void 0!==c&&(g.c=c),void 0!==d&&(g.d=d),void 0!==e&&(g.e=e),void 0!==f&&(g.f=f),g},b.prototype.createSVGPoint=function(a,b){var c=this._svg.createSVGPoint();return void 0!==a&&(c.x=a),void 0!==b&&(c.y=b),c},new b}]),angular.module("AMC.Graphics").factory("pointMath",[function(){function a(){}return a.prototype.distance=function(a,b){return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2))},a.prototype.pointsEqual=function(a,b){return a.x===b.x&&a.y===b.y},new a}]),angular.module("AMC.Graphics").factory("polygonMath",["vectorMath","pointMath","linearMath",function(a,b,c){function d(){}return d.prototype.calculateArea=function(a){var b,c,d=0;for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=a[b].x*a[c].y-a[c].x*a[b].y;return d/=2,d<0&&(d*=-1),d},d.prototype.calculateCentroid=function(a){var b,c,d=0,e=0,f=this.calculateArea(a),g={};for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=(a[b].x+a[c].x)*(a[b].x*a[c].y-a[c].x*a[b].y),e+=(a[b].y+a[c].y)*(a[b].x*a[c].y-a[c].x*a[b].y);return d*=1/(6*f),e*=1/(6*f),g.x=d,g.y=e,g},d.prototype.makeVerticesClockwise=function(a){var b,c,d=0;for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=a[b].x*a[c].y-a[c].x*a[b].y;d<0&&a.reverse()},d.prototype.makeVerticesStartWithMin=function(a){var b,c,d=[],e=a.length;for(c=0,b=1;b<e;++b)a[b].x<a[c].x?c=b:a[b].x===a[c].x&&a[b].y<a[c].y&&(c=b);for(b=0;b<e;++b)d.push(a[(b+c)%a.length]);for(a.length=0,b=0;b<e;++b)a.push(d[b])},d.prototype.arePolygonsAdjacent=function(b,c){function d(b,c){var d=a.subtract(b,c),e=a.transpose(d);return e.y*=-1,e}function e(b,c){var d={min:null,max:null};return b.forEach(function(b){var e=a.dot(b,c);(null===d.min||e<d.min)&&(d.min=e),(null===d.max||e>d.max)&&(d.max=e)}),d}function f(a,b){return b.max>=a.min&&a.max>=b.min}return[b,c].every(function(a){var g,h,i,j;for(g=0;g<a.length;++g)if(h=d(a[g],a[(g+1)%a.length]),i=e(b,h),j=e(c,h),!f(i,j))return!1;return!0})},d.prototype.combinePolygons=function(a,d){var e,f,g,h,i,j,k,l,m,n,o,p=0,q=0,r=null,s=[],t=0;f=a.slice(),g=d.slice(),[f,g].forEach(function(a){this.makeVerticesUnique(a),this.makeVerticesClockwise(a),this.makeVerticesStartWithMin(a)}.bind(this)),(f[0].x>g[0].x||f[0].x===g[0].x&&f[0].y>g[0].y)&&(o=f,f=g,g=o),h=f[0];do{if(n=!1,r=f[p],-1!==this.indexOf(r,s))throw new Error("Vertex already present in the combine result.");for(s.push(r),m=null,q=0;q<g.length&&!n;++q)for(i=c.getIntersections(f[p],f[(p+1)%f.length],g[q],g[(q+1)%g.length]),e=0;e<i.length;++e)-1===this.indexOf(i[e],s)&&(k=b.distance(r,i[e]),(null===m||k<j)&&(j=k,l=q,m=i[e]));null!==m&&(s.push(m),p=(l+1)%g.length,b.pointsEqual(m,g[p])&&(p=(p+1)%g.length),o=f,f=g,g=o,n=!0),n?++t:p=(p+1)%f.length}while(!b.pointsEqual(f[p],h));if(0===t)throw new Error("Polygons could not be combined because they are not adjacent.");return s},d.prototype.indexOf=function(a,c){for(var d=0;d<c.length;++d)if(b.pointsEqual(a,c[d]))return d;return-1},d.prototype.makeVerticesUnique=function(a){var b=a.filter(function(a,b,c){return this.indexOf(a,c)===b}.bind(this));a.length=0,b.forEach(function(b){a.push(b)})},new d}]),angular.module("AMC.Graphics").factory("vectorMath",[function(){function a(){}return a.prototype.subtract=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},a.prototype.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},a.prototype.multiplyScalar=function(a,b){return{x:a.x*b,y:a.y*b}},a.prototype.transpose=function(a){return{x:a.y,y:a.x}},a.prototype.dot=function(a,b){return a.x*b.x+a.y*b.y},a.prototype.cross=function(a,b){return a.x*b.y-a.y*b.x},new a}]),angular.module("AMC.Graphics").factory("ViewingTransformer",["matrixHelper",function(a){function b(b,c){this._vtm=a.createSVGMatrix(),this._wndWidth=b,this._wndHeight=c,this._aspectRatio=b/c,this._vWndWidth=b,this._vWndHeight=c}return b.prototype.getVTM=function(){return this._vtm},b.prototype.getInverseVTM=function(){return this.getVTM().inverse()},b.prototype.zoom=function(a,b,c){null!==b&&void 0!==b||(b=1/this._aspectRatio*a);var d=(this._wndWidth+a)/this._wndWidth,e=(this._wndHeight+b)/this._wndHeight;return this.scale(d,e,c)},b.prototype.scale=function(b,c,d){return d=d||{x:this._wndWidth/2,y:this._wndHeight/2},this._vtm=a.createSVGMatrix().translate(d.x,d.y).scaleNonUniform(b,c).translate(-d.x,-d.y).multiply(this._vtm),this._vWndWidth+=this._vWndWidth*(1-b),this._vWndHeight+=this._vWndHeight*(1-c),this.getVTM()},b.prototype.pan=function(b,c){var d=a.createSVGPoint(b,c);return d=d.matrixTransform(this.getInverseVTM()),this._vtm=a.createSVGMatrix().translate(-b,-c).multiply(this._vtm),this.getVTM()},b.prototype.panUp=function(a){return this.pan(0,-a)},b.prototype.panDown=function(a){return this.pan(0,a)},b.prototype.panLeft=function(a){return this.pan(-a,0)},b.prototype.panRight=function(a){return this.pan(a,0)},b.prototype.worldToWindowUnits=function(b){var c=1/this.getScaleFactor(),d=a.createSVGMatrix();return d=d.scale(c,c),b=b.matrixTransform(d)},b.prototype.getScaleFactor=function(){return this._wndWidth/this._vWndWidth},b.prototype.clone=function(){var b,c=JSON.parse(JSON.stringify(this));c._vtm=a.createSVGMatrix();for(b in this._vtm)c._vtm[b]=this._vtm[b];return c},b.prototype.restore=function(a){var b;for(b in a)this[b]=a[b];for(b in a._vtm)this._vtm[b]=a._vtm[b]},b}]); | ||
"use strict";function _toConsumableArray(a){if(Array.isArray(a)){for(var b=0,c=Array(a.length);b<a.length;b++)c[b]=a[b];return c}return Array.from(a)}var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};angular.module("AMC.Graphics",[]),angular.module("AMC.Graphics").directive("amcMapGraphic",["$parse",function(a){return{restrict:"A",link:function(b,c,d){function e(){var a=g();a&&f.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",a)}var f=c[0],g=a(d.amcMapGraphic).bind(this,b);b.$watch(g,e),e()}}}]),angular.module("AMC.Graphics").directive("amcSvgSize",["$parse",function(a){return{restrict:"A",link:function(b,c,d){function e(){var a=i();a&&(h.setAttributeNS(null,"width",a.getWidth()),h.setAttributeNS(null,"height",a.getHeight()))}function f(){var a=i();return a?a.getWidth():null}function g(){var a=i();return a?a.getHeight():null}var h=c[0],i=a(d.amcSvgSize).bind(this,b);b.$watch(f,e),b.$watch(g,e),e()}}}]),angular.module("AMC.Graphics").directive("amcTransform",["$parse","matrixHelper",function(a,b){return{restrict:"A",link:function(c,d,e){function f(){var a=h();a&&g.setAttributeNS(null,"transform",b.matrixToString(a))}var g=d[0],h=a(e.amcTransform).bind(this,c);c.$watch(h,f),f()}}}]),angular.module("AMC.Graphics").factory("linearMath",["vectorMath","pointMath",function(a,b){function c(){}return c.prototype.getIntersections=function(c,d,e,f){if(b.lessThan(d,c)){var g=d;d=c,c=g}if(b.lessThan(f,e)){var h=f;f=e,e=h}var i=a.subtract(d,c),j=a.subtract(f,e),k=a.cross(a.subtract(e,c),i),l=a.cross(i,j);if(0===k&&0===l)return e.x>=c.x&&e.x<=d.x&&f.x>=c.x&&f.x<=d.x&&e.y>=c.y&&e.y<=d.y&&f.y>=c.y&&f.y<=d.y?[e,f]:c.x>=e.x&&c.x<=f.x&&d.x>=e.x&&d.x<=f.x&&c.y>=e.y&&c.y<=f.y&&d.y>=e.y&&d.y<=f.y?[c,d]:e.x>=c.x&&e.x<=d.x&&e.y>=c.y&&e.y<=d.y?[e]:f.x>=c.x&&f.x<=d.x&&f.y>=c.y&&f.y<=d.y?[f]:[];if(0===l)return[];var m=k/l,n=a.cross(a.subtract(e,c),j)/l;return n>=0&&n<=1&&m>=0&&m<=1?[a.add(c,a.multiplyScalar(i,n))]:[]},c.prototype.pointOnLineSegment=function(a,c,d){var e=b.distance(a,c),f=b.distance(a,d),g=b.distance(c,d);return Math.abs(e+f-g)<1e-5},new c}]),angular.module("AMC.Graphics").factory("matrixHelper",["$window",function(a){function b(){this._svg=a.document.createElementNS("http://www.w3.org/2000/svg","svg")}return b.prototype.verticesToString=function(a){return a.map(function(a){return a.x+","+a.y}).join(" ")},b.prototype.matrixToArray=function(a){return[a.a,a.b,a.c,a.d,a.e,a.f]},b.prototype.matrixToString=function(a,b){var c=this.matrixToArray(a).join(" ");return b?c:"matrix("+c+")"},b.prototype.createSVGMatrix=function(a,b,c,d,e,f){var g=this._svg.createSVGMatrix();return void 0!==a&&(g.a=a),void 0!==b&&(g.b=b),void 0!==c&&(g.c=c),void 0!==d&&(g.d=d),void 0!==e&&(g.e=e),void 0!==f&&(g.f=f),g},b.prototype.createSVGPoint=function(a,b){var c=this._svg.createSVGPoint();return void 0!==a&&(c.x=a),void 0!==b&&(c.y=b),c},b.prototype.cloneMatrix=function(a){return this.createSVGMatrix(a.a,a.b,a.c,a.d,a.e,a.f)},b.prototype.getScale=function(a){var b=this.createSVGMatrix();return b.a=a.a,b.d=a.d,b},b.prototype.getTranslation=function(a){var b=this.createSVGMatrix();return b.e=a.e,b.f=a.f,b},new b}]),angular.module("AMC.Graphics").factory("pointMath",[function(){function a(){}return a.prototype.distance=function(a,b){return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2))},a.prototype.pointsEqual=function(a,b){return a.x===b.x&&a.y===b.y},a.prototype.lessThan=function(a,b){return a.x===b.x?a.y<b.y:a.x<b.x},new a}]),angular.module("AMC.Graphics").factory("polygonMath",["vectorMath","pointMath","linearMath",function(a,b,c){function d(){}return d.prototype.calculateArea=function(a){var b,c,d=0;for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=a[b].x*a[c].y-a[c].x*a[b].y;return d/=2,d<0&&(d*=-1),d},d.prototype.calculateCentroid=function(a){var b,c,d=0,e=0,f=this.calculateArea(a),g={};for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=(a[b].x+a[c].x)*(a[b].x*a[c].y-a[c].x*a[b].y),e+=(a[b].y+a[c].y)*(a[b].x*a[c].y-a[c].x*a[b].y);return d*=1/(6*f),e*=1/(6*f),g.x=d,g.y=e,g},d.prototype.makeVerticesClockwise=function(a){var b,c,d=0;for(b=0;b<a.length;++b)c=(b+1)%a.length,d+=a[b].x*a[c].y-a[c].x*a[b].y;d<0&&a.reverse()},d.prototype.makeVerticesStartWithMin=function(a){var b,c,d=[],e=a.length;for(c=0,b=1;b<e;++b)a[b].x<a[c].x?c=b:a[b].x===a[c].x&&a[b].y<a[c].y&&(c=b);for(b=0;b<e;++b)d.push(a[(b+c)%a.length]);for(a.length=0,b=0;b<e;++b)a.push(d[b])},d.prototype.arePolygonsAdjacent=function(){function b(a,b){return[a,b].some(function(f){for(var g=0;g<f.length-1;++g){var h=c(f[g],f[(g+1)%f.length]);if(!e(d(a,h),d(b,h)))return!0}return!1})}function c(b,c){var d=a.subtract(b,c),e=a.transpose(d);return e.y*=-1,a.normalize(e)}function d(b,c){var d={min:null,max:null};return b.forEach(function(b){var e=a.dot(b,c);(null===d.min||e<d.min)&&(d.min=e),(null===d.max||e>d.max)&&(d.max=e)}),d}function e(a,b){return b.max>=a.min&&a.max>=b.min}function f(a){return g(a,0).size===a.length}function g(a,b){var c=arguments.length>2&&void 0!==arguments[2]?arguments[2]:new Set;if(!c.has(b)){var d=a[b];c.add(b);for(var e=0;e<d.length;++e)g(a,d[e],c)}return c}for(var h=this,i=arguments.length,j=Array(i),k=0;k<i;k++)j[k]=arguments[k];return 1===j.length||(this.sortPolygons(j),j=j.map(function(a){return h.triangulate(a)}).reduce(function(a,b){return a.concat(b)},[]),f(j.map(function(a){return j.reduce(function(c,d,e){return a===d||b(a,d)?c:c.concat(e)},[])})))},d.prototype.combinePolygons=function(){for(var d=this,e=[],f=0,g=0,h=0,i=void 0,j=arguments.length,k=Array(j),l=0;l<j;l++)k[l]=arguments[l];if(k.forEach(function(a){d.makeVerticesUnique(a),d.makeVerticesClockwise(a),d.makeVerticesStartWithMin(a)}),this.sortPolygons(k),i=k[0],!this.arePolygonsAdjacent.apply(this,k))throw new Error("Cannot combine polygons because they are not adjacent.");do{if(-1!==this.indexOf(i[g],e))throw new Error("Cater-cornered polygons cannot be combined.");e.push(i[g]);for(var m={dist:null,angle:null,intPoint:null,polyInd:null,vertInd:null},n=0;n<k.length;++n)if(n!==f)for(var o=k[n],p=0;p<o.length;++p)for(var q=[i[g],i[(g+1)%i.length]],r=[o[p],o[(p+1)%o.length]],s=c.getIntersections.apply(c,q.concat(r)),t=0;t<s.length;++t)if(-1===this.indexOf(s[t],e)){var u=b.distance(i[g],s[t]),v=a.normalize(a.subtract(r[1],q[0])),w=Math.atan2(-v.y,v.x);(null===m.dist||u<m.dist||u===m.dist&&w>m.angle)&&(m.dist=u,m.angle=w,m.intPoint=s[t],m.polyInd=n,m.vertInd=p)}if(null!==m.dist){f=m.polyInd,i=k[f],g=m.vertInd;var x=b.pointsEqual(m.intPoint,i[g]),y=b.pointsEqual(m.intPoint,i[(g+1)%i.length]);x||(g=(g+1)%i.length),x||y||e.push(m.intPoint),++h}else g=(g+1)%i.length}while(!b.pointsEqual(i[g],k[0][0]));if(0===h)throw new Error("Polygons could not be combined because no intersections were found.");return this.removeExtraneousVertices(e),e},d.prototype.indexOf=function(a,c){for(var d=0;d<c.length;++d)if(b.pointsEqual(a,c[d]))return d;return-1},d.prototype.makeVerticesUnique=function(a){var b=a.filter(function(a,b,c){return this.indexOf(a,c)===b}.bind(this));a.length=0,b.forEach(function(b){a.push(b)})},d.prototype.removeExtraneousVertices=function(a){if(a.length<3)return a;for(var b=0;b<a.length;++b){var d=a[b],e=a[(b+1)%a.length],f=a[(b+2)%a.length];if(c.pointOnLineSegment(e,d,f))return a.splice((b+1)%a.length,1),this.removeExtraneousVertices(a)}return a},d.prototype.sortPolygons=function(a){return a.sort(function(a,b){var c=Math.min.apply(Math,_toConsumableArray(a.map(function(a){return a.x}))),d=Math.min.apply(Math,_toConsumableArray(b.map(function(a){return a.x})));return c!==d?c-d:Math.min.apply(Math,_toConsumableArray(a.map(function(a){return a.y})))-Math.min.apply(Math,_toConsumableArray(b.map(function(a){return a.y})))})},d.prototype.getCircumference=function(a){return a.reduce(function(a,c,d,e){return a+b.distance(c,e[(d+1)%e.length])},0)},d.prototype.triangulate=function(b){function c(b){if(3===b.length){var c=[b[0],b[1],b[2]];return d.makeVerticesStartWithMin(c),b.splice(0,1),c}for(var e=function(c){var e=[b[0===c?b.length-1:c-1],b[c],b[(c+1)%b.length]],f=a.subtract(e[0],e[1]),g=a.subtract(e[2],e[1]);if(a.cross(f,g)<0&&!b.some(function(a){return-1===d.indexOf(a,e)&&d.polygonContainsPoint(e,a)}))return d.makeVerticesStartWithMin(e),b.splice(c,1),{v:e}},f=0;f<b.length;++f){var g=e(f);if("object"===(void 0===g?"undefined":_typeof(g)))return g.v}throw new Error("Failed to clip ear.")}var d=this,e=[];if(b.length<3)throw new Error("Polygon cannot be clipped because it has fewer than three vertices.");b=b.slice();do{e.push(c(b))}while(b.length>=3);return e},d.prototype.polygonContainsPoint=function(a,b){var d=Math.max.apply(Math,_toConsumableArray(a.map(function(a){return a.x}))),e=Math.min.apply(Math,_toConsumableArray(a.map(function(a){return a.x}))),f=Math.max.apply(Math,_toConsumableArray(a.map(function(a){return a.y}))),g=Math.min.apply(Math,_toConsumableArray(a.map(function(a){return a.y})));if(b.x<e)return!1;if(b.x>d)return!1;if(b.y<g)return!1;if(b.y>f)return!1;for(var h=[b,{x:d,y:b.y}],i=[],j=0;j<a.length;++j){var k=[a[j],a[(j+1)%a.length]];if(c.pointOnLineSegment(b,k[0],k[1]))return!0;var l=c.getIntersections(h[0],h[1],k[0],k[1]);l.length&&-1===this.indexOf(l[0],i)&&i.push(l[0])}return i.length%2!=0},new d}]),angular.module("AMC.Graphics").factory("vectorMath",[function(){function a(){}return a.prototype.subtract=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},a.prototype.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},a.prototype.multiplyScalar=function(a,b){return{x:a.x*b,y:a.y*b}},a.prototype.transpose=function(a){return{x:a.y,y:a.x}},a.prototype.dot=function(a,b){return a.x*b.x+a.y*b.y},a.prototype.cross=function(a,b){return a.x*b.y-a.y*b.x},a.prototype.length=function(a){return Math.sqrt(Math.pow(a.x,2)+Math.pow(a.y,2))},a.prototype.normalize=function(a){var b=this.length(a);if(0===b)throw new Error("Cannot normalize a vector of length 0.");return this.multiplyScalar(a,1/b)},new a}]),angular.module("AMC.Graphics").factory("ViewingTransformer",["matrixHelper",function(a){function b(b,c){this._vtm=a.createSVGMatrix(),this._wndWidth=b,this._wndHeight=c,this._aspectRatio=b/c}return b.prototype.getScale=function(){return a.getScale(this._vtm)},b.prototype.getTranslate=function(){return a.getTranslation(this._vtm)},b.prototype.getVTM=function(){return this._vtm},b.prototype.getInverseVTM=function(){return this.getVTM().inverse()},b.prototype.zoom=function(a,b,c){null!==b&&void 0!==b||(b=1/this._aspectRatio*a);var d=(this._wndWidth+a)/this._wndWidth,e=(this._wndHeight+b)/this._wndHeight;return this.scale(d,e,c)},b.prototype.scale=function(b,c,d){return d=d||{x:this._wndWidth/2,y:this._wndHeight/2},this._vtm=a.createSVGMatrix().translate(d.x,d.y).scaleNonUniform(b,c).translate(-d.x,-d.y).multiply(this._vtm),this.getVTM()},b.prototype.pan=function(b,c){return this._vtm=a.createSVGMatrix().translate(-b,-c).multiply(this._vtm),this.getVTM()},b.prototype.panUp=function(a){return this.pan(0,-a)},b.prototype.panDown=function(a){return this.pan(0,a)},b.prototype.panLeft=function(a){return this.pan(-a,0)},b.prototype.panRight=function(a){return this.pan(a,0)},b.prototype.worldToWindowUnits=function(a){return a.matrixTransform(this.getInverseVTM())},b.prototype.clone=function(){var b=JSON.parse(JSON.stringify(this));return b._vtm=a.cloneMatrix(this._vtm),b},b.prototype.restore=function(b){for(var c in b)this[c]=b[c];this._vtm=a.cloneMatrix(b._vtm)},b}]); |
@@ -17,3 +17,4 @@ /** | ||
!script.match(/build/) && | ||
!script.match(/grunt/i); | ||
!script.match(/grunt/i) && | ||
!script.match(/Spec.js/); | ||
})); | ||
@@ -20,0 +21,0 @@ |
@@ -1,14 +0,13 @@ | ||
module.exports = function(grunt) | ||
{ | ||
module.exports = function(grunt) { | ||
'use strict'; | ||
var scripts = require('./grunt/scriptGarner')(); | ||
var buildFile = __dirname + '/build/AMC.Graphics.min.js'; | ||
const scripts = require('./grunt/scriptGarner')(); | ||
const buildFile = `${__dirname}/build/AMC.Graphics.min.js`; | ||
grunt.initConfig | ||
({ | ||
grunt.initConfig({ | ||
jshint: require('./grunt/jshint')(grunt, scripts), | ||
uglify: require('./grunt/uglify')(grunt, buildFile, buildFile), | ||
concat: require('./grunt/concat')(grunt, scripts, buildFile), | ||
babel: require('./grunt/babel')(grunt, buildFile) | ||
babel: require('./grunt/babel')(grunt, buildFile), | ||
karma: require('./grunt/karma')(grunt) | ||
}); | ||
@@ -15,0 +14,0 @@ |
{ | ||
"name": "@benningfield-group/amc-graphics", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Graphics math for AMC.", | ||
@@ -13,6 +13,8 @@ "main": "AMC.Graphics.js", | ||
"devDependencies": { | ||
"babel-preset-es2015": "^6.24.1", | ||
"glob": "~5.0.6", | ||
"angular": "~1.6.9", | ||
"angular-mocks": "~1.6.9", | ||
"babel-preset-es2015": "~6.24.1", | ||
"glob": "~7.1.2", | ||
"grunt": "~0.4.5", | ||
"grunt-babel": "^6.0.0", | ||
"grunt-babel": "~6.0.0", | ||
"grunt-cli": "~0.1.13", | ||
@@ -22,5 +24,9 @@ "grunt-contrib-concat": "~0.5.1", | ||
"grunt-contrib-uglify": "~0.9.1", | ||
"grunt-filerev": "~2.3.1" | ||
"grunt-filerev": "~2.3.1", | ||
"grunt-karma": "~2.0.0", | ||
"jasmine-core": "~3.1.0", | ||
"karma": "~2.0.0", | ||
"karma-jasmine": "~1.1.1" | ||
}, | ||
"repository": "git@bitbucket.org:benningfieldsuperitteam/graphics.git" | ||
} |
@@ -6,4 +6,4 @@ /** | ||
.factory('linearMath', | ||
['vectorMath', | ||
function(vectorMath) | ||
['vectorMath', 'pointMath', | ||
function(vectorMath, pointMath) | ||
{ | ||
@@ -30,7 +30,22 @@ 'use strict'; | ||
{ | ||
// Make sure the points are ordered left to right, top to bottom. | ||
if (pointMath.lessThan(p2, p1)) | ||
{ | ||
let hold = p2; | ||
p2 = p1; | ||
p1 = hold; | ||
} | ||
if (pointMath.lessThan(q2, q1)) | ||
{ | ||
let hold = q2; | ||
q2 = q1; | ||
q1 = hold; | ||
} | ||
// Line p runs from p1 to p1 + r. | ||
var r = vectorMath.subtract(p2, p1); | ||
const r = vectorMath.subtract(p2, p1); | ||
// Line q runs from p2 to p2 + s. | ||
var s = vectorMath.subtract(q2, q1); | ||
const s = vectorMath.subtract(q2, q1); | ||
@@ -44,4 +59,4 @@ // The two lines intersect if we can find t and u such that | ||
// u = (q1 - p1) x r / (r x s) | ||
var uNum = vectorMath.cross(vectorMath.subtract(q1, p1), r); | ||
var uDenom = vectorMath.cross(r, s); | ||
const uNum = vectorMath.cross(vectorMath.subtract(q1, p1), r); | ||
const uDenom = vectorMath.cross(r, s); | ||
@@ -90,7 +105,7 @@ // If both denominators are -0 then the segments are collinear. | ||
// Devision is safe - uDenom cannot be 0. | ||
var u = uNum / uDenom; | ||
const u = uNum / uDenom; | ||
// t is solved for in the same manner as u. | ||
// t = (q1 - p1) x s / (r x s) | ||
var t = vectorMath.cross(vectorMath.subtract(q1, p1), s) / uDenom; | ||
const t = vectorMath.cross(vectorMath.subtract(q1, p1), s) / uDenom; | ||
@@ -111,4 +126,22 @@ // If u and t are both between 0 and 1, the lines intersect (case 3), | ||
/** | ||
* Check if the point p is on the line segment created by points {q1, q2}. | ||
* @param p The point. | ||
* @param q1 The first point of the line segment. | ||
* @param q2 The second point of the line segment. | ||
*/ | ||
LinearMath.prototype.pointOnLineSegment = function(p, q1, q2) | ||
{ | ||
const distP_Q1 = pointMath.distance(p, q1); | ||
const distP_Q2 = pointMath.distance(p, q2); | ||
const distQ1_Q1 = pointMath.distance(q1, q2); | ||
const EPSILON = 1e-5; | ||
// If dist(p, q1) + dist(p, q2) === dist(q1, q2) the p falls on the segment, | ||
// otherwise the three points form a triangle. | ||
return Math.abs(distP_Q1 + distP_Q2 - distQ1_Q1) < EPSILON; | ||
}; | ||
return new LinearMath(); | ||
}]); | ||
@@ -96,2 +96,39 @@ /** | ||
/** | ||
* Clone an SVGMatrix. | ||
*/ | ||
MatrixHelper.prototype.cloneMatrix = function(matrix) | ||
{ | ||
return this.createSVGMatrix( | ||
matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f); | ||
}; | ||
/** | ||
* Given a matrix that is scaled and/or translated, extract the scale portion. | ||
* Note that this will not work if the matrix is rotated. | ||
*/ | ||
MatrixHelper.prototype.getScale = function(matrix) | ||
{ | ||
var scale = this.createSVGMatrix(); | ||
scale.a = matrix.a; | ||
scale.d = matrix.d; | ||
return scale; | ||
}; | ||
/** | ||
* Given a matrix that is scaled and/or translated, extract the translation | ||
* part. | ||
*/ | ||
MatrixHelper.prototype.getTranslation = function(matrix) | ||
{ | ||
var translation = this.createSVGMatrix(); | ||
translation.e = matrix.e; | ||
translation.f = matrix.f; | ||
return translation; | ||
}; | ||
// Single instance. | ||
@@ -98,0 +135,0 @@ return new MatrixHelper(); |
@@ -33,4 +33,17 @@ /** | ||
/** | ||
* Check if p1 is less than p2. | ||
* @param p1 The first point to compare. | ||
* @param p2 The second point to compare. | ||
*/ | ||
PointMath.prototype.lessThan = function(p1, p2) | ||
{ | ||
if (p1.x === p2.x) | ||
return p1.y < p2.y; | ||
return p1.x < p2.x; | ||
}; | ||
return new PointMath(); | ||
}]); | ||
@@ -18,3 +18,4 @@ /** | ||
* Calculate the area of the polygon made up of the passed-in vertices. | ||
* @param vertices The vertices of a polygon, for which the centroid will be calculated. | ||
* @param vertices The vertices of a polygon, for which the area will be | ||
* calculated. | ||
*/ | ||
@@ -48,3 +49,4 @@ PolygonMath.prototype.calculateArea = function(vertices) | ||
* Calculate the centroid of the polygon made up of the passed-in vertices. | ||
* @param vertices The vertices of a polygon, for which the centroid will be calculated. | ||
* @param vertices The vertices of a polygon, for which the centroid will be | ||
* calculated. | ||
*/ | ||
@@ -81,3 +83,3 @@ PolygonMath.prototype.calculateCentroid = function(vertices) | ||
* @param vertices An array of vertices. If they are CCW, they are | ||
* rearranged such that they are clockwise. | ||
* rearranged such that they are clockwise. | ||
*/ | ||
@@ -147,15 +149,60 @@ PolygonMath.prototype.makeVerticesClockwise = function(vertices) | ||
/** | ||
* Check if the polygons made up by verts1 and verts2 are adjacent (touching or overlapping). | ||
* This is an implementation of the Separating Axis Theorem, described on | ||
* wikipedia: https://en.wikipedia.org/wiki/Hyperplane_separation_theorem | ||
* @param verts1 The first array of polygons to check for adjacenty. | ||
* @param verts2 The second array of polygons, to compare to verts1. | ||
* Check if the polygons are adjacent (touching or overlapping). | ||
* This is an implementation of the Hyperplane Separation Theorem, described | ||
* on wikipedia: https://en.wikipedia.org/wiki/Hyperplane_separation_theorem | ||
* @param polygons An arbitrary number of polygons, each of which should be | ||
* an array of vertices. | ||
*/ | ||
PolygonMath.prototype.arePolygonsAdjacent = function(verts1, verts2) | ||
PolygonMath.prototype.arePolygonsAdjacent = function(...polygons) | ||
{ | ||
// Create a an axis that is parallel to the vector pointing from v_2 to v_1. | ||
if (polygons.length === 1) | ||
return true; | ||
// The check is performed left to right, top to bottom. | ||
this.sortPolygons(polygons); | ||
// The Hyperplan Separation Theorem only works on convex polygons, so each | ||
// polygon is triangulated (converted into triangles), then the triangles | ||
// are checked for adjacency. | ||
polygons = polygons | ||
.map(polygon => this.triangulate(polygon)) | ||
.reduce((polygons, polygon) => polygons.concat(polygon), []); | ||
// Create an adjacency map for each polygon. | ||
const adjacencyMatrix = polygons | ||
.map(curPoly => polygons | ||
.reduce((adjacencies, otherPoly, i) => { | ||
if (curPoly === otherPoly || hasLineBetween(curPoly, otherPoly)) | ||
return adjacencies; | ||
return adjacencies.concat(i); | ||
}, [])); | ||
// Now check if the polygons are fully connected using a graph search. | ||
return isFullyConnected(adjacencyMatrix); | ||
// Check if there exists a line between two convex polygons. | ||
function hasLineBetween(poly1, poly2) | ||
{ | ||
return [poly1, poly2].some(poly => { | ||
for (let i = 0; i < poly.length - 1; ++i) | ||
{ | ||
const axis = createAxis(poly[i], poly[(i + 1) % poly.length]); | ||
const extremum_b1 = project(poly1, axis); | ||
const extremum_b2 = project(poly2, axis); | ||
if (!overlap(extremum_b1, extremum_b2)) | ||
return true; | ||
} | ||
return false; | ||
}); | ||
} | ||
// Create a normalized axis that is perpendicular to the vector pointing | ||
// from v_2 to v_1. | ||
function createAxis(v_1, v_2) | ||
{ | ||
// This is a vector pointing in the direction from v_2 to v_1. | ||
var v_21 = vectorMath.subtract(v_1, v_2); | ||
const v_21 = vectorMath.subtract(v_1, v_2); | ||
@@ -166,6 +213,6 @@ // This is a perpendicular line to v_21, which is the axis upon which each | ||
// |1 0| |y| |1 0| | x| | ||
var axis = vectorMath.transpose(v_21); | ||
const axis = vectorMath.transpose(v_21); | ||
axis.y *= -1; | ||
return axis; | ||
return vectorMath.normalize(axis); | ||
} | ||
@@ -177,3 +224,3 @@ | ||
{ | ||
var extremum = | ||
const extremum = | ||
{ | ||
@@ -186,12 +233,9 @@ min: null, | ||
{ | ||
var projected = vectorMath.dot(vert, axis); | ||
const projected = vectorMath.dot(vert, axis); | ||
if (extremum.min === null || projected < extremum.min) | ||
{ | ||
extremum.min = projected; | ||
} | ||
if (extremum.max === null || projected > extremum.max) | ||
{ | ||
extremum.max = projected; | ||
} | ||
}); | ||
@@ -208,121 +252,118 @@ | ||
return [verts1, verts2].every(function(verts) | ||
// Check if a graph (adjacency matrix) is fully connected. | ||
function isFullyConnected(adjacencyMatrix) | ||
{ | ||
var i, axis, extremum_b1, extremum_b2; | ||
const connections = traverse(adjacencyMatrix, 0); | ||
for (i = 0; i < verts.length; ++i) | ||
return connections.size === adjacencyMatrix.length; | ||
} | ||
// Recursively traverse a graph and return a unique set of connections. | ||
function traverse(adjacencyMatrix, startNodeInd, connections = new Set()) | ||
{ | ||
if (!connections.has(startNodeInd)) | ||
{ | ||
axis = createAxis(verts[i], verts[(i + 1) % verts.length]); | ||
extremum_b1 = project(verts1, axis); | ||
extremum_b2 = project(verts2, axis); | ||
const node = adjacencyMatrix[startNodeInd]; | ||
// If the projected lines of the polygons do not overlap there exists a line between | ||
// them and the polygons are not colliding. These booths can't be combined. | ||
if (!overlap(extremum_b1, extremum_b2)) | ||
{ | ||
return false; | ||
} | ||
connections.add(startNodeInd); | ||
for (let i = 0; i < node.length; ++i) | ||
traverse(adjacencyMatrix, node[i], connections); | ||
} | ||
return true; | ||
}); | ||
return connections; | ||
} | ||
}; | ||
/** | ||
* Combine the two polygons into a single polygon. If the polygons are not | ||
* colliding then an exception is raised. | ||
* @param verts1 The array of vertices for the first polygon. | ||
* @param verts2 The array of vertices for the second polygon. | ||
* Combine (union) multiple polygons into a single polygon. | ||
* @param polygons Multiple polygons, each of which should be an array of | ||
* vertices. | ||
*/ | ||
PolygonMath.prototype.combinePolygons = function(verts1, verts2) | ||
PolygonMath.prototype.combinePolygons = function(...polygons) | ||
{ | ||
var i; | ||
let result = []; | ||
let curPolyInd = 0; | ||
let curVertInd = 0; | ||
let switchCount = 0; | ||
let curPoly; | ||
// This is the polygon that is being traversed, and the other polygon. | ||
var curVerts; | ||
var otherVerts; | ||
// This is the vertex that iterations starts with. It's used to indicate | ||
// when the polygon has been completely traversed. | ||
var startVert; | ||
// This is the index of the current vertex in curVerts. | ||
var curVertInd = 0; | ||
var otherVertInd = 0; | ||
// The current vertex being iterated over. | ||
var curVert = null; | ||
// These are the resulting vertices (the combination of verts1 and verts2). | ||
var result = []; | ||
// The total number of times the polygons switched due to intersections. | ||
var switchCount = 0; | ||
// This holds the intersection of two edges of the two polygons. Only the | ||
// closest one is of interest. | ||
var intersections, intersectionDist, holdDist, | ||
nearestIntersectionInd, nearestIntersection; | ||
// Whether or not the current and other verts need to be switched due to an intersection. | ||
var switched; | ||
// For swapping curVerts and otherVerts.. | ||
var swapHold; | ||
// Make sure verts1 and verts2 are clockwise. This also ensures that the | ||
// first vertex is the top-left one. | ||
curVerts = verts1.slice(); | ||
otherVerts = verts2.slice(); | ||
[curVerts, otherVerts].forEach(function(verts) | ||
// The polygons must be "normalized." E.g., no duplicates, with points in | ||
// clockwise orientation, and staring with the minimum (top-left) point. | ||
polygons.forEach(polygon => | ||
{ | ||
this.makeVerticesUnique(verts); | ||
this.makeVerticesClockwise(verts); | ||
this.makeVerticesStartWithMin(verts); | ||
}.bind(this)); | ||
this.makeVerticesUnique(polygon); | ||
this.makeVerticesClockwise(polygon); | ||
this.makeVerticesStartWithMin(polygon); | ||
}); | ||
// Iteration must start with the polygon that is on the left (or if left is the same, on the top). | ||
if (curVerts[0].x > otherVerts[0].x || curVerts[0].x === otherVerts[0].x && curVerts[0].y > otherVerts[0].y) | ||
{ | ||
swapHold = curVerts; curVerts = otherVerts; otherVerts = swapHold; | ||
} | ||
// Combinations are done left to right, top to bottom. | ||
this.sortPolygons(polygons); | ||
curPoly = polygons[0]; | ||
startVert = curVerts[0]; | ||
// All the polygons must be adjacent (touching or overlapping). | ||
if (!this.arePolygonsAdjacent(...polygons)) | ||
throw new Error('Cannot combine polygons because they are not adjacent.'); | ||
// Loop, making an outer perimeter around all the polygons (similar to a | ||
// convex hull), until reaching the first vertex again. | ||
do | ||
{ | ||
switched = false; | ||
// The resulting polygon must be a unique set of vertices. If the | ||
// current vertex has already been added to the result set, then the | ||
// polygons are cater-cornered which is considered to be an error. | ||
if (this.indexOf(curPoly[curVertInd], result) !== -1) | ||
throw new Error('Cater-cornered polygons cannot be combined.'); | ||
// Add the current vert to the resulting polygon. | ||
curVert = curVerts[curVertInd]; | ||
result.push(curPoly[curVertInd]); | ||
// This should never fire. If it does there is a bug in this combine routine. | ||
if (this.indexOf(curVert, result) !== -1) | ||
throw new Error('Vertex already present in the combine result.'); | ||
// Find the nearest intersection between the current edge and all other | ||
// polygons. | ||
const nearestIntersection = { | ||
dist : null, | ||
angle : null, | ||
intPoint : null, | ||
polyInd : null, | ||
vertInd : null | ||
}; | ||
result.push(curVert); | ||
for (let p = 0; p < polygons.length; ++p) | ||
{ | ||
if (p === curPolyInd) | ||
continue; | ||
// Iterate over the other polygon and check if the current edge intersects | ||
// with any edges of the other polygon. | ||
nearestIntersection = null; | ||
const otherPoly = polygons[p]; | ||
for (otherVertInd = 0; otherVertInd < otherVerts.length && !switched; ++otherVertInd) | ||
{ | ||
intersections = linearMath.getIntersections( | ||
curVerts[curVertInd], curVerts[(curVertInd + 1) % curVerts.length], | ||
otherVerts[otherVertInd], otherVerts[(otherVertInd + 1) % otherVerts.length]); | ||
for (let otherVertInd = 0; otherVertInd < otherPoly.length; ++otherVertInd) | ||
{ | ||
// Intersections between the current edge of the current polygon and | ||
// the current edge of the other polygon. | ||
const curEdge = [curPoly[curVertInd], curPoly[(curVertInd + 1) % curPoly.length]]; | ||
const otherEdge = [otherPoly[otherVertInd], otherPoly[(otherVertInd + 1) % otherPoly.length]]; | ||
const intersections = linearMath.getIntersections(...curEdge, ...otherEdge); | ||
// There could be 0, 1, or 2 intersections. Keep track of the index of | ||
// the nearest intersection that is a new point (e.g. not in result). | ||
for (i = 0; i < intersections.length; ++i) | ||
{ | ||
if (this.indexOf(intersections[i], result) === -1) | ||
// There can be 0, 1, or 2 intersection points, but only the nearest | ||
// unvisited one is part of the resulting polygon. | ||
for (let i = 0; i < intersections.length; ++i) | ||
{ | ||
holdDist = pointMath.distance(curVert, intersections[i]); | ||
if (this.indexOf(intersections[i], result) === -1) | ||
{ | ||
const dist = pointMath.distance(curPoly[curVertInd], intersections[i]); | ||
if (nearestIntersection === null || holdDist < intersectionDist) | ||
{ | ||
intersectionDist = holdDist; | ||
nearestIntersectionInd = otherVertInd; | ||
nearestIntersection = intersections[i]; | ||
// When two intersections have the same point, the one with the | ||
// largest angle is used (going from the tip of beginning of the | ||
// first edge to the end of the other). Keep in mind that in the | ||
// browser, y grows down (hence the y negation). | ||
const vec = vectorMath.normalize(vectorMath.subtract(otherEdge[1], curEdge[0])); | ||
const angle = Math.atan2(-vec.y, vec.x); | ||
if ((nearestIntersection.dist === null || dist < nearestIntersection.dist) || | ||
dist === nearestIntersection.dist && angle > nearestIntersection.angle) | ||
{ | ||
nearestIntersection.dist = dist; | ||
nearestIntersection.angle = angle; | ||
nearestIntersection.intPoint = intersections[i]; | ||
nearestIntersection.polyInd = p; | ||
nearestIntersection.vertInd = otherVertInd; | ||
} | ||
} | ||
@@ -333,44 +374,47 @@ } | ||
if (nearestIntersection !== null) | ||
// If there was an intersection between the current edge and another | ||
// polygon edge. | ||
if (nearestIntersection.dist !== null) | ||
{ | ||
// New intersection. The intersection point needs to be added to | ||
// the resulting polygon. | ||
result.push(nearestIntersection); | ||
// Move to the intersecting polygon, which will now be traversed | ||
// clockwise. | ||
curPolyInd = nearestIntersection.polyInd; | ||
curPoly = polygons[curPolyInd]; | ||
curVertInd = nearestIntersection.vertInd; | ||
// Note that traversal is always clockwise around the polygons. When | ||
// there is an intersection, always turn left of the current heading. | ||
// That is, if heading east, move north. If heading south, head | ||
// east. "Left" is always to the second point along otherVerts | ||
// current edge. | ||
curVertInd = (nearestIntersectionInd + 1) % otherVerts.length; | ||
// The intersection may occur at the beginning of the intersected | ||
// polygon's edge, in the middle, or at the end. If it's at the | ||
// beginning, traversal starts at that vertex, otherwise it starts at | ||
// the proceeding one. | ||
const isStart = pointMath.pointsEqual(nearestIntersection.intPoint, curPoly[curVertInd]); | ||
const isEnd = pointMath.pointsEqual(nearestIntersection.intPoint, curPoly[(curVertInd + 1) % curPoly.length]); | ||
if (pointMath.pointsEqual(nearestIntersection, otherVerts[curVertInd])) | ||
{ | ||
// The intersection was at the end of the edge. Move to the next point. | ||
curVertInd = (curVertInd + 1) % otherVerts.length; | ||
} | ||
if (!isStart) | ||
curVertInd = (curVertInd + 1) % curPoly.length; | ||
// Swap the curVerts and otherVerts; | ||
swapHold = curVerts; curVerts = otherVerts; otherVerts = swapHold; | ||
switched = true; | ||
} | ||
// If the intersection point is not one of the endpoints of the | ||
// interseted edge (i.e. it's in the middle somewhere), the | ||
// intersection point is part of the resulting polygon. | ||
if (!isStart && !isEnd) | ||
result.push(nearestIntersection.intPoint); | ||
// The polygons were not switched, so move to the next vertex in the current polygon. | ||
if (!switched) | ||
{ | ||
curVertInd = (curVertInd + 1) % curVerts.length; | ||
++switchCount; | ||
} | ||
else | ||
{ | ||
++switchCount; | ||
// No intersections with the current edge. Move to the next edge on the | ||
// current polygon. | ||
curVertInd = (curVertInd + 1) % curPoly.length; | ||
} | ||
} | ||
// Loop until the starting vertex is reached again. | ||
while (!pointMath.pointsEqual(curVerts[curVertInd], startVert)); | ||
while (!pointMath.pointsEqual(curPoly[curVertInd], polygons[0][0])); | ||
// This should not fire, but if it does then there is an error in the | ||
// algorithm. No intersections between any polygons were found. | ||
if (switchCount === 0) | ||
{ | ||
throw new Error('Polygons could not be combined because they are not adjacent.'); | ||
} | ||
throw new Error('Polygons could not be combined because no intersections were found.'); | ||
// Remove an extraneous vertices from the polygon. | ||
this.removeExtraneousVertices(result); | ||
return result; | ||
@@ -416,4 +460,211 @@ }; | ||
/** | ||
* Remove any extraneous points from a polygon. For example, two duplicate | ||
* points in succession, identical first and last points, or multiple points | ||
* that fall on a straight line. | ||
* @param verts An array of vertices that make up a polygon. | ||
*/ | ||
PolygonMath.prototype.removeExtraneousVertices = function(verts) | ||
{ | ||
// Fewer than three points is not a polygon. | ||
if (verts.length < 3) | ||
return verts; | ||
for (let i = 0; i < verts.length; ++i) | ||
{ | ||
// If p is on the line segment formed by {q1, q2}, then it's an | ||
// uneccessary point and should be removed. | ||
let q1 = verts[i]; | ||
let p = verts[(i + 1) % verts.length]; | ||
let q2 = verts[(i + 2) % verts.length]; | ||
if (linearMath.pointOnLineSegment(p, q1, q2)) | ||
{ | ||
verts.splice((i + 1) % verts.length, 1); | ||
return this.removeExtraneousVertices(verts); | ||
} | ||
} | ||
return verts; | ||
}; | ||
/** | ||
* Sort a series of polygons, left to right, top to bottom. | ||
* @param polygons One or more polygons, each of which is an array of | ||
* vertices. | ||
*/ | ||
PolygonMath.prototype.sortPolygons = function(polygons) | ||
{ | ||
return polygons | ||
.sort((l, r) => | ||
{ | ||
const lMinX = Math.min(...l.map(vert => vert.x)); | ||
const rMinX = Math.min(...r.map(vert => vert.x)); | ||
if (lMinX !== rMinX) | ||
return lMinX - rMinX; | ||
const lMinY = Math.min(...l.map(vert => vert.y)); | ||
const rMinY = Math.min(...r.map(vert => vert.y)); | ||
return lMinY - rMinY; | ||
}); | ||
}; | ||
/** | ||
* Get the circumference of a polygon. | ||
* @param polygon A polygon, for which the circumference will be calculated. | ||
*/ | ||
PolygonMath.prototype.getCircumference = function(polygon) | ||
{ | ||
return polygon | ||
.reduce((circ, vert, i, polygon) => | ||
{ | ||
return circ + pointMath.distance(vert, polygon[(i + 1) % polygon.length]); | ||
}, 0); | ||
}; | ||
/** | ||
* Decompose a polygon into a series of triangles by ear clipping. The | ||
* polygon's vertices must be normalized (clockwise with no extraneous | ||
* verts). | ||
* https://en.wikipedia.org/wiki/Polygon_triangulation | ||
* @param polygon The polygon, which is an array of vertices, to decompose. | ||
*/ | ||
PolygonMath.prototype.triangulate = function(polygon) | ||
{ | ||
const self = this; | ||
const triangles = []; | ||
if (polygon.length < 3) | ||
throw new Error('Polygon cannot be clipped because it has fewer than three vertices.'); | ||
polygon = polygon.slice(); | ||
do | ||
{ | ||
triangles.push(clipEar(polygon)); | ||
} | ||
while (polygon.length >= 3); | ||
return triangles; | ||
// Clip one ear off of the polygon, removing the starting vertex. | ||
function clipEar(polygon) | ||
{ | ||
if (polygon.length === 3) { | ||
const ear = [ | ||
polygon[0], | ||
polygon[1], | ||
polygon[2] | ||
]; | ||
self.makeVerticesStartWithMin(ear); | ||
polygon.splice(0, 1); | ||
return ear; | ||
} | ||
for (let i = 0; i < polygon.length; ++i) | ||
{ | ||
// These three points form a candidate ear. | ||
const ear = | ||
[ | ||
polygon[i === 0 ? polygon.length - 1 : i - 1], | ||
polygon[i], | ||
polygon[(i + 1) % polygon.length] | ||
]; | ||
// The mid point, ear[1], is either a convex point or a reflexive one. | ||
// The determinate of a matrix formed by the two edges of the ear that | ||
// are on the polygon, p1->p0 and p1->p2, will be positive if the mid | ||
// point is reflexive, meaning that the line {p0, p1} is not fully | ||
// contained by the polygon. (I.e. p0, p1, p2 is counter clockwise.) | ||
const v_p10 = vectorMath.subtract(ear[0], ear[1]); | ||
const v_p12 = vectorMath.subtract(ear[2], ear[1]); | ||
const det = vectorMath.cross(v_p10, v_p12); | ||
if (det < 0) | ||
{ | ||
// If any point on the polygon other than those making up the ear is | ||
// contained by the ear, then the ear cannot be removed. | ||
const hasPointInside = polygon | ||
.some(point => | ||
self.indexOf(point, ear) === -1 && | ||
self.polygonContainsPoint(ear, point)); | ||
if (!hasPointInside) | ||
{ | ||
// Good ear. Normalize it. | ||
self.makeVerticesStartWithMin(ear); | ||
// Remove the point from the polygon. | ||
polygon.splice(i, 1); | ||
return ear; | ||
} | ||
} | ||
} | ||
throw new Error('Failed to clip ear.'); | ||
} | ||
}; | ||
/** | ||
* Check if a point is inside a polygon. A point on an edge or at a vertex | ||
* is considered to be contained. | ||
* @param polygon The polygon. | ||
* @param point The point. | ||
*/ | ||
PolygonMath.prototype.polygonContainsPoint = function(polygon, point) | ||
{ | ||
const maxX = Math.max(...polygon.map(p => p.x)); | ||
const minX = Math.min(...polygon.map(p => p.x)); | ||
const maxY = Math.max(...polygon.map(p => p.y)); | ||
const minY = Math.min(...polygon.map(p => p.y)); | ||
if (point.x < minX) return false; // Left of polygon. | ||
if (point.x > maxX) return false; // Right of polygon. | ||
if (point.y < minY) return false; // Above polygon (window coords). | ||
if (point.y > maxY) return false; // Below polygon. | ||
// A horizontal ray from the point to the right-most side of the | ||
// polygon. | ||
const ray = | ||
[ | ||
point, | ||
{x: maxX, y: point.y} | ||
]; | ||
const uniqueInts = []; | ||
for (let i = 0; i < polygon.length; ++i) | ||
{ | ||
const segment = [ | ||
polygon[i], | ||
polygon[(i + 1) % polygon.length] | ||
]; | ||
// If the point lies directly on one of the sides of the polygon, | ||
// the point is contained. | ||
if (linearMath.pointOnLineSegment(point, segment[0], segment[1])) | ||
return true; | ||
// Otherwise the ray intercepts the side 0 or 1 times. | ||
const segInts = linearMath | ||
.getIntersections(ray[0], ray[1], segment[0], segment[1]); | ||
// The intercept must be unqiue, because the point may intercept the | ||
// triangle at a vertex (a vertex is shared by two segments). | ||
if (segInts.length && this.indexOf(segInts[0], uniqueInts) === -1) | ||
uniqueInts.push(segInts[0]); | ||
} | ||
// If there is an even number of intercepts, the point is outside, | ||
// otherwise it's contained. | ||
return (uniqueInts.length % 2) !== 0; | ||
}; | ||
return new PolygonMath(); | ||
}]); | ||
@@ -14,3 +14,4 @@ /** | ||
/** | ||
* Subtract vec2 from vec1 (vec1 - vec2) | ||
* Subtract vec2 from vec1 (vec1 - vec2) and return a vector from | ||
* vec2 to vec1. | ||
* @param vec1 The first vector, with x and y properties. | ||
@@ -66,3 +67,7 @@ * @param vec2 The second vector, with x and y properties. | ||
* 2D cross product of two vectors, as defined by Wolfram Math World (it's an | ||
* analog to 3D cross product but, unlike 3D, this returns a scalar). | ||
* analog to 3D cross product but, unlike 3D, this returns a scalar). The | ||
* result is the determinant of the 2x2 matrix formed by [vec1 vec2], and | ||
* it's also the signed area of the parallelogram for which vec1 and vec2 are | ||
* the sides. If the area is negative, then the rotation from vec1 to vec2 | ||
* is clockwise (in window coords, where +y is down and +x is right). | ||
* http://mathworld.wolfram.com/CrossProduct.html | ||
@@ -77,4 +82,27 @@ * @param vec1 The first vector, with x and y properties. | ||
/** | ||
* Get the length of a vector. | ||
* @param vec The vector to get the length of. | ||
*/ | ||
VectorMath.prototype.length = function(vec) | ||
{ | ||
return Math.sqrt(Math.pow(vec.x, 2) + Math.pow(vec.y, 2)); | ||
}; | ||
/** | ||
* Normalize the vector (make it unit length). | ||
* @param vec The vector to normalize. | ||
*/ | ||
VectorMath.prototype.normalize = function(vec) | ||
{ | ||
const len = this.length(vec); | ||
if (len === 0) | ||
throw new Error('Cannot normalize a vector of length 0.'); | ||
return this.multiplyScalar(vec, 1 / len); | ||
}; | ||
return new VectorMath(); | ||
}]); | ||
@@ -22,14 +22,26 @@ /** | ||
// World window variables. | ||
this._wndWidth = wndWidth; | ||
this._wndHeight = wndHeight; | ||
// Viewport (virtual). | ||
this._wndWidth = wndWidth; | ||
this._wndHeight = wndHeight; | ||
this._aspectRatio = wndWidth / wndHeight; | ||
this._vWndWidth = wndWidth; | ||
this._vWndHeight = wndHeight; | ||
} | ||
/** | ||
* Get the current viewing transformation matrix. It's by reference! | ||
* Get the scale portion of the VTM. | ||
*/ | ||
ViewingTransformer.prototype.getScale = function() | ||
{ | ||
return matrixHelper.getScale(this._vtm); | ||
}; | ||
/** | ||
* Get the translation portion of the VTM. | ||
*/ | ||
ViewingTransformer.prototype.getTranslate = function() | ||
{ | ||
return matrixHelper.getTranslation(this._vtm); | ||
}; | ||
/** | ||
* Get the current viewing transformation matrix by reference. | ||
*/ | ||
ViewingTransformer.prototype.getVTM = function() | ||
@@ -86,2 +98,3 @@ { | ||
// Scale in at the origin. | ||
this._vtm = matrixHelper | ||
@@ -94,5 +107,2 @@ .createSVGMatrix() | ||
this._vWndWidth += this._vWndWidth * (1 - xFactor); | ||
this._vWndHeight += this._vWndHeight * (1 - yFactor); | ||
return this.getVTM(); | ||
@@ -117,9 +127,2 @@ }; | ||
{ | ||
// This is a point (x, y) units from (0, 0) in view space. | ||
var topLeft = matrixHelper.createSVGPoint(x, y); | ||
// Transform the point from view space to windows space, which is the | ||
// inverse of the VTM. This gives the new viewport x and y. | ||
topLeft = topLeft.matrixTransform(this.getInverseVTM()); | ||
this._vtm = matrixHelper | ||
@@ -176,25 +179,6 @@ .createSVGMatrix() | ||
{ | ||
// Figure out how to transform the window units to world units. For | ||
// example, if the floor plan is zoomed by a factor of 1.10 (110% of it's | ||
// native size), then the movement amount should by 90% of the window units | ||
// (1 / scaleFactor). | ||
var invScaleFactor = 1 / this.getScaleFactor(); | ||
var scale = matrixHelper.createSVGMatrix(); | ||
// Scale the x and y units | ||
scale = scale.scale(invScaleFactor, invScaleFactor); | ||
pt = pt.matrixTransform(scale); | ||
return pt; | ||
return pt.matrixTransform(this.getInverseVTM()); | ||
}; | ||
/** | ||
* Get the scale factor (the amount the booth is scaled). | ||
*/ | ||
ViewingTransformer.prototype.getScaleFactor = function() | ||
{ | ||
return this._wndWidth / this._vWndWidth; | ||
}; | ||
/** | ||
* Clone this object. | ||
@@ -206,11 +190,5 @@ * @return {Object} A clone that can be restore()'d. | ||
var vtMomento = JSON.parse(JSON.stringify(this)); | ||
var prop; | ||
vtMomento._vtm = matrixHelper.createSVGMatrix(); | ||
vtMomento._vtm = matrixHelper.cloneMatrix(this._vtm); | ||
for (prop in this._vtm) | ||
{ | ||
vtMomento._vtm[prop] = this._vtm[prop]; | ||
} | ||
return vtMomento; | ||
@@ -226,13 +204,6 @@ }; | ||
{ | ||
var prop; | ||
for (prop in momento) | ||
{ | ||
for (var prop in momento) | ||
this[prop] = momento[prop]; | ||
} | ||
for (prop in momento._vtm) | ||
{ | ||
this._vtm[prop] = momento._vtm[prop]; | ||
} | ||
this._vtm = matrixHelper.cloneMatrix(momento._vtm); | ||
}; | ||
@@ -239,0 +210,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
227325
23
2210
0
15