diff --git a/examples/boolean.html b/examples/boolean.html new file mode 100644 index 00000000..bb1046da --- /dev/null +++ b/examples/boolean.html @@ -0,0 +1,16 @@ + + + + Boolean Example + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/boolean.js b/examples/boolean.js new file mode 100644 index 00000000..ee8bbf29 --- /dev/null +++ b/examples/boolean.js @@ -0,0 +1,31 @@ +function drawSolid(s){ + + s.faces().forEach(function(x){ + addMeshToScene( x.toThreeGeometry(), + new THREE.MeshNormalMaterial( + { side: THREE.FrontSide, wireframe: false, shading: THREE.SmoothShading, transparent: true, opacity: 0.95 } )); + }); + + s.halfEdges().forEach(function(x){ + addLineToScene( [x.v.pt,x.nxt.v.pt] ); + }); + + s.vertices().forEach(function(x){ + addPointsToScene( [x.pt] ); + }); +} + +threeSetup(true); + +var ptsa = [[0,0,0], [10,0,0], [10,10,0], [0,10,0] ]; +var a = verb.topo.Make.extrusion( ptsa, [0,0,10] ); + +var ptsb = [[5,5,-5], [15,5,-5], [15,15,-5], [5,15,-5] ]; +var b = verb.topo.Make.extrusion( ptsb, [0,0,10] ); + +var res = verb.topo.Boolean.union( a, b, 1e-6 ); + +drawSolid( a ); +drawSolid( b ); + +threeRender(); \ No newline at end of file diff --git a/examples/eulerOperators.html b/examples/eulerOperators.html new file mode 100644 index 00000000..d58b1c62 --- /dev/null +++ b/examples/eulerOperators.html @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/js/cytoscape.min.js b/examples/js/cytoscape.min.js new file mode 100644 index 00000000..3fc7b424 --- /dev/null +++ b/examples/js/cytoscape.min.js @@ -0,0 +1,25 @@ +/*! + * This file is part of Cytoscape.js 2.3.10. + * + * Cytoscape.js is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * Cytoscape.js is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * Cytoscape.js. If not, see . + */ +var cytoscape;!function(e){"use strict";var t=cytoscape=function(){return cytoscape.init.apply(cytoscape,arguments)};t.version="2.3.10",t.init=function(e){return void 0===e&&(e={}),t.is.plainObject(e)?new t.Core(e):t.is.string(e)?t.extension.apply(t.extension,arguments):void 0},t.fn={},"undefined"!=typeof module&&module.exports&&(module.exports=cytoscape),"undefined"!=typeof define&&define.amd&&define("cytoscape",function(){return cytoscape}),e&&(e.cytoscape=cytoscape)}("undefined"==typeof window?null:window),function(e,t){"use strict";e.is={defined:function(e){return null!=e},string:function(e){return null!=e&&"string"==typeof e},fn:function(e){return null!=e&&"function"==typeof e},array:function(e){return Array.isArray?Array.isArray(e):null!=e&&e instanceof Array},plainObject:function(t){return null!=t&&typeof t==typeof{}&&!e.is.array(t)&&t.constructor===Object},number:function(e){return null!=e&&"number"==typeof e&&!isNaN(e)},integer:function(t){return e.is.number(t)&&Math.floor(t)===t},color:function(e){return null!=e&&"string"==typeof e&&""!==$.Color(e).toString()},bool:function(e){return null!=e&&typeof e==typeof!0},elementOrCollection:function(t){return e.is.element(t)||e.is.collection(t)},element:function(t){return t instanceof e.Element&&t._private.single},collection:function(t){return t instanceof e.Collection&&!t._private.single},core:function(t){return t instanceof e.Core},style:function(t){return t instanceof e.Style},stylesheet:function(t){return t instanceof e.Stylesheet},event:function(t){return t instanceof e.Event},emptyString:function(t){return t?e.is.string(t)&&(""===t||t.match(/^\s+$/))?!0:!1:!0},nonemptyString:function(t){return t&&e.is.string(t)&&""!==t&&!t.match(/^\s+$/)?!0:!1},domElement:function(e){return"undefined"==typeof HTMLElement?!1:e instanceof HTMLElement},boundingBox:function(t){return e.is.plainObject(t)&&e.is.number(t.x1)&&e.is.number(t.x2)&&e.is.number(t.y1)&&e.is.number(t.y2)},touch:function(){return t&&("ontouchstart"in t||t.DocumentTouch&&document instanceof DocumentTouch)},gecko:function(){return"undefined"!=typeof InstallTrigger||"MozAppearance"in document.documentElement.style},webkit:function(){return"undefined"!=typeof webkitURL||"WebkitAppearance"in document.documentElement.style},chromium:function(){return"undefined"!=typeof chrome},khtml:function(){return navigator.vendor.match(/kde/i)},khtmlEtc:function(){return e.is.khtml()||e.is.webkit()||e.is.blink()},trident:function(){/*@cc_on!@*/ +return"undefined"!=typeof ActiveXObject||!1},windows:function(){return"undefined"!=typeof navigator&&navigator.appVersion.match(/Win/i)},mac:function(){return"undefined"!=typeof navigator&&navigator.appVersion.match(/Mac/i)},linux:function(){return"undefined"!=typeof navigator&&navigator.appVersion.match(/Linux/i)},unix:function(){return"undefined"!=typeof navigator&&navigator.appVersion.match(/X11/i)}}}(cytoscape,"undefined"==typeof window?null:window),function(e,t){"use strict";e.util={extend:function(){var t,i,r,n,a,o,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||e.is.fn(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(t=arguments[l]))for(i in t)r=s[i],n=t[i],s!==n&&(c&&n&&(e.is.plainObject(n)||(a=e.is.array(n)))?(a?(a=!1,o=r&&e.is.array(r)?r:[]):o=r&&e.is.plainObject(r)?r:{},s[i]=e.util.extend(c,o,n)):void 0!==n&&(s[i]=n));return s},require:function(i,r,n){var a;n=e.util.extend({msgIfNotFound:!0},n);var o=!1,s=function(e){o=!0,r(e)},l=function(e){t&&(a=t[i]),void 0!==a&&s(a),e&&e()},u=function(){o||c(d)},c=function(e){"undefined"!=typeof module&&module.exports&&require&&(a=require(i)),void 0!==a&&s(a),e&&e()},d=function(){o||p(h)},p=function(e){"undefined"!=typeof define&&define.amd&&require&&require([i],function(t){a=t,void 0!==a&&s(a),e&&e()})},h=function(){!o&&n.msgIfNotFound&&e.util.error("Cytoscape.js tried to pull in dependency `"+i+"` but no module (i.e. CommonJS, AMD, or window) was found")};l(u)},requires:function(t,i){for(var r=[],n=[],a=function(){for(var e=0;e=r){a&&clearTimeout(a);var p=c;a=u=c=void 0,p&&(d=e.util.now(),o=t.apply(l,n),u||a||(n=l=null))}else u=setTimeout(g,r)},f=function(){u&&clearTimeout(u),a=u=c=void 0,(h||p!==i)&&(d=e.util.now(),o=t.apply(l,n),u||a||(n=l=null))};return function(){if(n=arguments,s=e.util.now(),l=this,c=h&&(u||!v),p===!1)var r=v&&!u;else{a||v||(d=s);var y=p-(s-d),m=0>=y;m?(a&&(a=clearTimeout(a)),d=s,o=t.apply(l,n)):a||(a=setTimeout(f,y))}return m&&u?u=clearTimeout(u):u||i===p||(u=setTimeout(g,i)),r&&(m=!0,o=t.apply(l,n)),!m||u||a||(n=l=null),o}}},error:function(e){if(!console)throw e;if(console.error)console.error.apply(console,arguments);else{if(!console.log)throw e;console.log.apply(console,arguments)}},clone:function(e){var t={};for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t},copy:function(t){return null==t?t:e.is.array(t)?t.slice():e.is.plainObject(t)?e.util.clone(t):t},makeBoundingBox:function(e){if(null!=e.x1&&null!=e.y1){if(null!=e.x2&&null!=e.y2&&e.x2>=e.x1&&e.y2>=e.y1)return{x1:e.x1,y1:e.y1,x2:e.x2,y2:e.y2,w:e.x2-e.x1,h:e.y2-e.y1};if(null!=e.w&&null!=e.h&&e.w>=0&&e.h>=0)return{x1:e.x1,y1:e.y1,x2:e.x1+e.w,y2:e.y1+e.h,w:e.w,h:e.h}}},mapEmpty:function(e){var t=!0;if(null!=e)for(var i in e){t=!1;break}return t},pushMap:function(t){var i=e.util.getMap(t);null==i?e.util.setMap($.extend({},t,{value:[t.value]})):i.push(t.value)},setMap:function(t){for(var i,r=t.map,n=t.keys,a=n.length,o=0;a>o;o++){var i=n[o];e.is.plainObject(i)&&e.util.error("Tried to set map with object key"),oa;a++){var o=r[a];if(e.is.plainObject(o)&&e.util.error("Tried to get map with object key"),i=i[o],null==i)return i}return i},deleteMap:function(t){for(var i=t.map,r=t.keys,n=r.length,a=t.keepChildren,o=0;n>o;o++){var s=r[o];e.is.plainObject(s)&&e.util.error("Tried to delete map with object key");var l=o===t.keys.length-1;if(l)if(a)for(var u in i)a[u]||(i[u]=void 0);else i[s]=void 0;else i=i[s]}},capitalize:function(t){return e.is.emptyString(t)?t:t.charAt(0).toUpperCase()+t.substring(1)},camel2dash:function(e){for(var t=[],i=0;it&&" "===e[i];i--);return e.substring(t,i+1)},hex2tuple:function(e){if((4===e.length||7===e.length)&&"#"===e[0]){var t,i,r,n=4===e.length,a=16;return n?(t=parseInt(e[1]+e[1],a),i=parseInt(e[2]+e[2],a),r=parseInt(e[3]+e[3],a)):(t=parseInt(e[1]+e[2],a),i=parseInt(e[3]+e[4],a),r=parseInt(e[5]+e[6],a)),[t,i,r]}},hsl2tuple:function(t){function i(e,t,i){return 0>i&&(i+=1),i>1&&(i-=1),1/6>i?e+6*(t-e)*i:.5>i?t:2/3>i?e+(t-e)*(2/3-i)*6:e}var r,n,a,o,s,l,u,c,d=new RegExp("^"+e.util.regex.hsla+"$").exec(t);if(d){if(n=parseInt(d[1]),0>n?n=(360- -1*n%360)%360:n>360&&(n%=360),n/=360,a=parseFloat(d[2]),0>a||a>100)return;if(a/=100,o=parseFloat(d[3]),0>o||o>100)return;if(o/=100,s=d[4],void 0!==s&&(s=parseFloat(s),0>s||s>1))return;if(0===a)l=u=c=Math.round(255*o);else{var p=.5>o?o*(1+a):o+a-o*a,h=2*o-p;l=Math.round(255*i(h,p,n+1/3)),u=Math.round(255*i(h,p,n)),c=Math.round(255*i(h,p,n-1/3))}r=[l,u,c,s]}return r},rgb2tuple:function(t){var i,r=new RegExp("^"+e.util.regex.rgba+"$").exec(t);if(r){i=[];for(var n=[],a=1;3>=a;a++){var o=r[a];if("%"===o[o.length-1]&&(n[a]=!0),o=parseFloat(o),n[a]&&(o=o/100*255),0>o||o>255)return;i.push(Math.floor(o))}var s=n[1]||n[2]||n[3],l=n[1]&&n[2]&&n[3];if(s&&!l)return;var u=r[4];if(void 0!==u){if(u=parseFloat(u),0>u||u>1)return;i.push(u)}}return i},colorname2tuple:function(t){return e.util.colors[t.toLowerCase()]},color2tuple:function(t){return(e.is.array(t)?t:null)||e.util.colorname2tuple(t)||e.util.hex2tuple(t)||e.util.rgb2tuple(t)||e.util.hsl2tuple(t)},tuple2hex:function(e){function t(e){var t=e.toString(16);return 1===t.length&&(t="0"+t),t}var i=e[0],r=e[1],n=e[2];return"#"+t(i)+t(r)+t(n)},colors:{transparent:[0,0,0,0],aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],grey:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}},e.util.regex={},e.util.regex.number="(?:[-]?\\d*\\.\\d+|[-]?\\d+|[-]?\\d*\\.\\d+[eE]\\d+)",e.util.regex.rgba="rgb[a]?\\(("+e.util.regex.number+"[%]?)\\s*,\\s*("+e.util.regex.number+"[%]?)\\s*,\\s*("+e.util.regex.number+"[%]?)(?:\\s*,\\s*("+e.util.regex.number+"))?\\)",e.util.regex.rgbaNoBackRefs="rgb[a]?\\((?:"+e.util.regex.number+"[%]?)\\s*,\\s*(?:"+e.util.regex.number+"[%]?)\\s*,\\s*(?:"+e.util.regex.number+"[%]?)(?:\\s*,\\s*(?:"+e.util.regex.number+"))?\\)",e.util.regex.hsla="hsl[a]?\\(("+e.util.regex.number+")\\s*,\\s*("+e.util.regex.number+"[%])\\s*,\\s*("+e.util.regex.number+"[%])(?:\\s*,\\s*("+e.util.regex.number+"))?\\)",e.util.regex.hslaNoBackRefs="hsl[a]?\\((?:"+e.util.regex.number+")\\s*,\\s*(?:"+e.util.regex.number+"[%])\\s*,\\s*(?:"+e.util.regex.number+"[%])(?:\\s*,\\s*(?:"+e.util.regex.number+"))?\\)",e.util.regex.hex3="\\#[0-9a-fA-F]{3}",e.util.regex.hex6="\\#[0-9a-fA-F]{6}";var i=t?t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame:null;i=i||function(e){e&&setTimeout(e,1e3/60)},e.util.requestAnimationFrame=function(e){i(e)}}(cytoscape,"undefined"==typeof window?null:window),function(e){"use strict";e.math={},e.math.signum=function(e){return e>0?1:0>e?-1:0},e.math.distance=function(e,t){var i=t.x-e.x,r=t.y-e.y;return Math.sqrt(i*i+r*r)},e.math.qbezierAt=function(e,t,i,r){return(1-r)*(1-r)*e+2*(1-r)*r*t+r*r*i},e.math.qbezierPtAt=function(t,i,r,n){return{x:e.math.qbezierAt(t.x,i.x,r.x,n),y:e.math.qbezierAt(t.y,i.y,r.y,n)}},e.math.boundingBoxesIntersect=function(e,t){return e.x1>t.x2?!1:t.x1>e.x2?!1:e.x2t.y2?!1:t.y1>e.y2?!1:!0},e.math.inBoundingBox=function(e,t,i){return e.x1<=t&&t<=e.x2&&e.y1<=i&&i<=e.y2},e.math.pointInBoundingBox=function(e,t){return this.inBoundingBox(e,t.x,t.y)},e.math.roundRectangleIntersectLine=function(e,t,i,r,n,a,o){var s,l=this.getRoundRectangleRadius(n,a),u=n/2,c=a/2,d=i-u+l-o,p=r-c-o,h=i+u-l+o,v=p;if(s=this.finiteLinesIntersect(e,t,i,r,d,p,h,v,!1),s.length>0)return s;var g=i+u+o,f=r-c+l-o,y=g,m=r+c-l+o;if(s=this.finiteLinesIntersect(e,t,i,r,g,f,y,m,!1),s.length>0)return s;var x=i-u+l-o,b=r+c+o,w=i+u-l+o,_=b;if(s=this.finiteLinesIntersect(e,t,i,r,x,b,w,_,!1),s.length>0)return s;var E=i-u-o,S=r-c+l-o,P=E,D=r+c-l+o;if(s=this.finiteLinesIntersect(e,t,i,r,E,S,P,D,!1),s.length>0)return s;var k,C=i-u+l,T=r-c+l;if(k=this.intersectLineCircle(e,t,i,r,C,T,l+o),k.length>0&&k[0]<=C&&k[1]<=T)return[k[0],k[1]];var N=i+u-l,M=r-c+l;if(k=this.intersectLineCircle(e,t,i,r,N,M,l+o),k.length>0&&k[0]>=N&&k[1]<=M)return[k[0],k[1]];var B=i+u-l,z=r+c-l;if(k=this.intersectLineCircle(e,t,i,r,B,z,l+o),k.length>0&&k[0]>=B&&k[1]>=z)return[k[0],k[1]];var I=i-u+l,L=r+c-l;return k=this.intersectLineCircle(e,t,i,r,I,L,l+o),k.length>0&&k[0]<=I&&k[1]>=L?[k[0],k[1]]:[]},e.math.roundRectangleIntersectBox=function(e,t,i,r,n,a,o,s,l){var u=this.getRoundRectangleRadius(n,a),c=o-n/2-l,d=s-a/2+u-l,p=o+n/2+l,h=s+a/2-u+l,v=o-n/2+u-l,g=s-a/2-l,f=o+n/2-u+l,y=s+a/2+l,m=Math.min(e,i),x=Math.max(e,i),b=Math.min(t,r),w=Math.max(t,r);return c>x?!1:m>p?!1:g>w?!1:b>y?!1:c>=m&&x>=c&&d>=b&&w>=d?!0:p>=m&&x>=p&&d>=b&&w>=d?!0:p>=m&&x>=p&&h>=b&&w>=h?!0:c>=m&&x>=c&&h>=b&&w>=h?!0:m>=c&&p>=m&&b>=d&&h>=b?!0:x>=c&&p>=x&&b>=d&&h>=b?!0:x>=c&&p>=x&&w>=d&&h>=w?!0:m>=c&&p>=m&&w>=d&&h>=w?!0:v>=m&&x>=v&&g>=b&&w>=g?!0:f>=m&&x>=f&&g>=b&&w>=g?!0:f>=m&&x>=f&&y>=b&&w>=y?!0:v>=m&&x>=v&&y>=b&&w>=y?!0:m>=v&&f>=m&&b>=g&&y>=b?!0:x>=v&&f>=x&&b>=g&&y>=b?!0:x>=v&&f>=x&&w>=g&&y>=w?!0:m>=v&&f>=m&&w>=g&&y>=w?!0:this.boxIntersectEllipse(m,b,x,w,l,2*u,2*u,v+l,d+l)?!0:this.boxIntersectEllipse(m,b,x,w,l,2*u,2*u,f-l,d+l)?!0:this.boxIntersectEllipse(m,b,x,w,l,2*u,2*u,f-l,h-l)?!0:this.boxIntersectEllipse(m,b,x,w,l,2*u,2*u,v+l,h-l)?!0:!1},e.math.checkInBoundingCircle=function(e,t,i,r,n,a,o,s){return e=(e-o)/(n+r),t=(t-s)/(a+r),i>=e*e+t*t},e.math.boxInBezierVicinity=function(e,t,i,r,n,a,o,s,l,u,c){var d=.25*n+.5*o+.25*l,p=.25*a+.5*s+.25*u,h=Math.min(e,i)-c,v=Math.min(t,r)-c,g=Math.max(e,i)+c,f=Math.max(t,r)+c;if(n>=h&&g>=n&&a>=v&&f>=a)return 1;if(l>=h&&g>=l&&u>=v&&f>=u)return 1;if(d>=h&&g>=d&&p>=v&&f>=p)return 1;if(o>=h&&g>=o&&s>=v&&f>=s)return 1;var y=Math.min(n,d,l),m=Math.min(a,p,u),x=Math.max(n,d,l),b=Math.max(a,p,u);return y>g||h>x||m>f||v>b?0:1},e.math.checkBezierInBox=function(t,i,r,n,a,o,s,l,u,c){function d(d){var p=e.math.qbezierAt(a,s,u,d),h=e.math.qbezierAt(o,l,c,d);return p>=t&&r>=p&&h>=i&&n>=h}for(var p=0;1>=p;p+=.25)if(!d(p))return!1;return!0},e.math.checkStraightEdgeInBox=function(e,t,i,r,n,a,o,s){return n>=e&&i>=n&&o>=e&&i>=o&&a>=t&&r>=a&&s>=t&&r>=s},e.math.checkStraightEdgeCrossesBox=function(e,t,i,r,n,a,o,s,l){var u,c,d=Math.min(e,i)-l,p=Math.min(t,r)-l,h=Math.max(e,i)+l,v=Math.max(t,r)+l,g=o-n,f=n,y=s-a,m=a;if(Math.abs(g)<1e-4)return n>=d&&h>=n&&Math.min(a,s)<=p&&Math.max(a,s)>=v;var x=(d-f)/g;if(x>0&&1>=x&&(u=y*x+m,u>=p&&v>=u))return!0;var b=(h-f)/g;if(b>0&&1>=b&&(u=y*b+m,u>=p&&v>=u))return!0;var w=(p-m)/y;if(w>0&&1>=w&&(c=g*w+f,c>=d&&h>=c))return!0;var _=(v-m)/y;return _>0&&1>=_&&(c=g*_+f,c>=d&&h>=c)?!0:!1},e.math.checkBezierCrossesBox=function(e,t,i,r,n,a,o,s,l,u,c){var d=Math.min(e,i)-c,p=Math.min(t,r)-c,h=Math.max(e,i)+c,v=Math.max(t,r)+c;if(n>=d&&h>=n&&a>=p&&v>=a)return!0;if(l>=d&&h>=l&&u>=p&&v>=u)return!0;var g=n-2*o+l,f=-2*n+2*o,y=n,m=[];if(Math.abs(g)<1e-4){var x=(d-n)/f,b=(h-n)/f;m.push(x,b)}else{var w,_,E=f*f-4*g*(y-d);if(E>0){var S=Math.sqrt(E);w=(-f+S)/(2*g),_=(-f-S)/(2*g),m.push(w,_)}var P,D,k=f*f-4*g*(y-h);if(k>0){var S=Math.sqrt(k);P=(-f+S)/(2*g),D=(-f-S)/(2*g),m.push(P,D)}}m.sort(function(e,t){return e-t});var C=a-2*s+u,T=-2*a+2*s,N=a,M=[];if(Math.abs(C)<1e-4){var B=(p-a)/T,z=(v-a)/T;M.push(B,z)}else{var I,L,O=T*T-4*C*(N-p);if(O>0){var S=Math.sqrt(O);I=(-T+S)/(2*C),L=(-T-S)/(2*C),M.push(I,L)}var R,X,V=T*T-4*C*(N-v);if(V>0){var S=Math.sqrt(V);R=(-T+S)/(2*C),X=(-T-S)/(2*C),M.push(R,X)}}M.sort(function(e,t){return e-t});for(var Y=0;Y=0&&m[Y]<=1&&m[Y+1]>M[A-1]&&M[A-1]<=1&&m[Y+1]>=0)return!0;return!1},e.math.inLineVicinity=function(e,t,i,r,n,a,o){var s=o,l=Math.min(i,n),u=Math.max(i,n),c=Math.min(r,a),d=Math.max(r,a);return e>=l-s&&u+s>=e&&t>=c-s&&d+s>=t},e.math.inBezierVicinity=function(e,t,i,r,n,a,o,s){var l={x1:Math.min(i,o,n),x2:Math.max(i,o,n),y1:Math.min(r,s,a),y2:Math.max(r,s,a)};return el.x2||tl.y2?!1:!0},e.math.solveCubic=function(e,t,i,r,n){t/=e,i/=e,r/=e;var a,o,s,l,u,c,d,p;return o=(3*i-t*t)/9,s=-(27*r)+t*(9*i-2*t*t),s/=54,a=o*o*o+s*s,n[1]=0,d=t/3,a>0?(u=s+Math.sqrt(a),u=0>u?-Math.pow(-u,1/3):Math.pow(u,1/3),c=s-Math.sqrt(a),c=0>c?-Math.pow(-c,1/3):Math.pow(c,1/3),n[0]=-d+u+c,d+=(u+c)/2,n[4]=n[2]=-d,d=Math.sqrt(3)*(-c+u)/2,n[3]=d,void(n[5]=-d)):(n[5]=n[3]=0,0===a?(p=0>s?-Math.pow(-s,1/3):Math.pow(s,1/3),n[0]=-d+2*p,void(n[4]=n[2]=-(p+d))):(o=-o,l=o*o*o,l=Math.acos(s/Math.sqrt(l)),p=2*Math.sqrt(o),n[0]=-d+p*Math.cos(l/3),n[2]=-d+p*Math.cos((l+2*Math.PI)/3),void(n[4]=-d+p*Math.cos((l+4*Math.PI)/3))))},e.math.sqDistanceToQuadraticBezier=function(e,t,i,r,n,a,o,s){var l=1*i*i-4*i*n+2*i*o+4*n*n-4*n*o+o*o+r*r-4*r*a+2*r*s+4*a*a-4*a*s+s*s,u=9*i*n-3*i*i-3*i*o-6*n*n+3*n*o+9*r*a-3*r*r-3*r*s-6*a*a+3*a*s,c=3*i*i-6*i*n+i*o-i*e+2*n*n+2*n*e-o*e+3*r*r-6*r*a+r*s-r*t+2*a*a+2*a*t-s*t,d=1*i*n-i*i+i*e-n*e+r*a-r*r+r*t-a*t,p=[];this.solveCubic(l,u,c,d,p);for(var h=1e-7,v=[],g=0;6>g;g+=2)Math.abs(p[g+1])=0&&p[g]<=1&&v.push(p[g]);v.push(1),v.push(0);for(var f,y,m,x,b=-1,w=0;w=0?b>x&&(b=x,f=v[w]):(b=x,f=v[w]);return b},e.math.sqDistanceToFiniteLine=function(e,t,i,r,n,a){var o=[e-i,t-r],s=[n-i,a-r],l=s[0]*s[0]+s[1]*s[1],u=o[0]*o[0]+o[1]*o[1],c=o[0]*s[0]+o[1]*s[1],d=c*c/l;return 0>c?u:d>l?(e-n)*(e-n)+(t-a)*(t-a):u-d},e.math.pointInsidePolygon=function(e,t,i,r,n,a,o,s,l){var u=new Array(i.length),c=Math.asin(s[1]/Math.sqrt(s[0]*s[0]+s[1]*s[1]));s[0]<0?c+=Math.PI/2:c=-c-Math.PI/2;for(var d=Math.cos(-c),p=Math.sin(-c),h=0;h0){var g=this.expandPolygon(u,-l);v=this.joinLines(g)}else v=u;for(var f,y,m,x,b,w=0,_=0,h=0;h=e&&e>=m||e>=f&&m>=e))continue;b=(e-f)/(m-f)*(x-y)+y,b>t&&w++,t>b&&_++}return w%2===0?!1:!0},e.math.joinLines=function(e){for(var t,i,r,n,a,o,s,l,u=new Array(e.length/2),c=0;cu)return[];var c=u/l;return[(i-e)*c+e,(r-t)*c+t]},e.math.dotProduct=function(e,t){if(2!=e.length||2!=t.length)throw"dot product: arguments are not vectors";return e[0]*t[0]+e[1]*t[1]},e.math.intersectLineCircle=function(e,t,i,r,n,a,o){var s=[i-e,r-t],l=[n,a],u=[e-n,t-a],c=s[0]*s[0]+s[1]*s[1],d=2*(u[0]*s[0]+u[1]*s[1]),l=u[0]*u[0]+u[1]*u[1]-o*o,p=d*d-4*c*l;if(0>p)return[];var h=(-d+Math.sqrt(p))/(2*c),v=(-d-Math.sqrt(p))/(2*c),g=Math.min(h,v),f=Math.max(h,v),y=[];if(g>=0&&1>=g&&y.push(g),f>=0&&1>=f&&y.push(f),0===y.length)return[];var m=y[0]*s[0]+e,x=y[0]*s[1]+t;if(y.length>1){if(y[0]==y[1])return[m,x];var b=y[1]*s[0]+e,w=y[1]*s[1]+t;return[m,x,b,w]}return[m,x]},e.math.findCircleNearPoint=function(e,t,i,r,n){var a=r-e,o=n-t,s=Math.sqrt(a*a+o*o),l=a/s,u=o/s;return[e+l*i,t+u*i]},e.math.findMaxSqDistanceToOrigin=function(e){for(var t,i=1e-6,r=0;ri&&(i=t);return i},e.math.finiteLinesIntersect=function(e,t,i,r,n,a,o,s,l){var u=(o-n)*(t-a)-(s-a)*(e-n),c=(i-e)*(t-a)-(r-t)*(e-n),d=(s-a)*(i-e)-(o-n)*(r-t);if(0!==d){var p=u/d,h=c/d;return p>=0&&1>=p&&h>=0&&1>=h?[e+p*(i-e),t+p*(r-t)]:l?[e+p*(i-e),t+p*(r-t)]:[]}return 0===u||0===c?[e,i,o].sort()[1]===o?[o,s]:[e,i,n].sort()[1]===n?[n,a]:[n,o,i].sort()[1]===i?[i,r]:[]:[]},e.math.boxIntersectEllipse=function(e,t,i,r,n,a,o,s,l){if(e>i){var u=e;e=i,i=u}if(t>r){var c=t;t=r,r=c}var d=[s-a/2-n,l],p=[s+a/2+n,l],h=[s,l-o/2-n],v=[s,l+o/2+n];return ip[0]?!1:t>v[1]?!1:r=e*e+t*t?!0:1>=i*i+t*t?!0:1>=i*i+r*r?!0:1>=e*e+r*r?!0:!1)},e.math.boxIntersectPolygon=function(t,i,r,n,a,o,s,l,u,c,d){if(t>r){var p=t;t=r,r=p}if(i>n){var h=i;i=n,n=h}var v=new Array(a.length),g=Math.asin(c[1]/Math.sqrt(c[0]*c[0]+c[1]*c[1]));c[0]<0?g+=Math.PI/2:g=-g-Math.PI/2;for(var f=Math.cos(-g),y=Math.sin(-g),m=0;mb&&(b=v[2*m]),v[2*m]_&&(_=v[2*m+1]),v[2*m+1]r)return!1;if(t>b+d)return!1;if(w-d>n)return!1;if(i>_+d)return!1;var E;if(d>0){var S=e.math.expandPolygon(v,-d);E=e.math.joinLines(S)}else E=v;for(var m=0;m0)return!0;if(e.math.finiteLinesIntersect(k,C,P,D,t,n,r,n,!1).length>0)return!0;if(e.math.finiteLinesIntersect(k,C,P,D,t,i,t,n,!1).length>0)return!0;if(e.math.finiteLinesIntersect(k,C,P,D,r,i,r,n,!1).length>0)return!0}return!1},e.math.polygonIntersectLine=function(t,i,r,n,a,o,s,l){for(var u,c=[],d=new Array(r.length),p=0;p0){var v=e.math.expandPolygon(d,-l);h=e.math.joinLines(v)}else h=d;for(var g,f,y,m,p=0;pa&&(a=1e-5),[t[0]+a*r[0],t[1]+a*r[1]]},e.math.generateUnitNgonPointsFitToSquare=function(t,i){var r=e.math.generateUnitNgonPoints(t,i);return r=e.math.fitPolygonToSquare(r)},e.math.fitPolygonToSquare=function(e){for(var t,i,r=e.length/2,n=1/0,a=1/0,o=-1/0,s=-1/0,l=0;r>l;l++)t=e[2*l],i=e[2*l+1],n=Math.min(n,t),o=Math.max(o,t),a=Math.min(a,i),s=Math.max(s,i);for(var u=2/(o-n),c=2/(s-a),l=0;r>l;l++)t=e[2*l]=e[2*l]*u,i=e[2*l+1]=e[2*l+1]*c,n=Math.min(n,t),o=Math.max(o,t),a=Math.min(a,i),s=Math.max(s,i);if(-1>a)for(var l=0;r>l;l++)i=e[2*l+1]=e[2*l+1]+(-1-a);return e},e.math.generateUnitNgonPoints=function(e,t){var i=1/e*2*Math.PI,r=e%2===0?Math.PI/2+i/2:Math.PI/2;r+=t;for(var n,a,o,s=new Array(2*e),l=0;e>l;l++)n=l*i+r,a=s[2*l]=Math.cos(n),o=s[2*l+1]=Math.sin(-n);return s},e.math.getRoundRectangleRadius=function(e,t){return Math.min(e/4,t/4,8)}}(cytoscape),function(e){"use strict";function t(t,i,r){var n={};switch(n[i]=r,t){case"core":case"collection":e.fn[t](n)}if("layout"===t){for(var o=r.prototype,s=["stop"],l=0;ld;d++)n.canSet(s[d])&&(s[d]._private[n.field][i]=r);n.updateStyle&&a.updateStyle(),n.onSet(a),n.settingTriggersEvent&&a[n.triggerFnName](n.settingEvent)}}}else if(n.allowSetting&&e.is.plainObject(i)){var h,v,g=i;for(h in g){v=g[h];var c=!n.immutableKeys[h];if(c)for(var d=0,p=s.length;p>d;d++)n.canSet(s[d])&&(s[d]._private[n.field][h]=v)}n.updateStyle&&a.updateStyle(),n.onSet(a),n.settingTriggersEvent&&a[n.triggerFnName](n.settingEvent)}else if(n.allowBinding&&e.is.fn(i)){var f=i;a.bind(n.bindingEvent,f)}else if(n.allowGetting&&void 0===i){var u;return l&&(u=l._private[n.field]),u}return a}},removeData:function(t){var i={field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!1,immutableKeys:{}};return t=e.util.extend({},i,t),function(i){var r=t,n=this,a=void 0!==n.length,o=a?n:[n];if(e.is.string(i)){for(var s=i.split(/\s+/),l=s.length,u=0;l>u;u++){var c=s[u];if(!e.is.emptyString(c)){var d=!r.immutableKeys[c];if(d)for(var p=0,h=o.length;h>p;p++)o[p]._private[r.field][c]=void 0}}r.triggerEvent&&n[r.triggerFnName](r.event)}else if(void 0===i){for(var p=0,h=o.length;h>p;p++){var v=o[p]._private[r.field];for(var c in v){var g=!r.immutableKeys[c];g&&(v[c]=void 0)}}r.triggerEvent&&n[r.triggerFnName](r.event)}return n}},event:{regex:/(\w+)(\.\w+)?/,optionalTypeRegex:/(\w+)?(\.\w+)?/,falseCallback:function(){return!1}},on:function(t){var i={unbindSelfOnTrigger:!1,unbindAllBindersOnTrigger:!1};return t=e.util.extend({},i,t),function(i,r,n,a){var o=this,s=void 0!==o.length,l=s?o:[o],u=e.is.string(i),c=t;if(e.is.plainObject(r)?(a=n,n=r,r=void 0):(e.is.fn(r)||r===!1)&&(a=r,n=void 0,r=void 0),(e.is.fn(n)||n===!1)&&(a=n,n=void 0),!e.is.fn(a)&&a!==!1&&u)return o;if(u){var d={};d[i]=a,i=d}for(var p in i)if(a=i[p],a===!1&&(a=e.define.event.falseCallback),e.is.fn(a)){p=p.split(/\s+/);for(var h=0;h0:void 0}},clearQueue:function(t){var i={};return t=e.util.extend({},i,t),function(){var e=this,t=void 0!==e.length,i=t?e:[e],r=this._private.cy||this;if(!r.styleEnabled())return this;for(var n=0;n\\?\\@\\[\\]\\^\\`\\{\\|\\}\\~]",comparatorOp:"=|\\!=|>|>=|<|<=|\\$=|\\^=|\\*=",boolOp:"\\?|\\!|\\^",string:'"(?:\\\\"|[^"])+"|'+"'(?:\\\\'|[^'])+'",number:e.util.regex.number,meta:"degree|indegree|outdegree",separator:"\\s*,\\s*",descendant:"\\s+",child:"\\s+>\\s+",subject:"\\$"};s.variable="(?:[\\w-]|(?:\\\\"+s.metaChar+"))+",s.value=s.string+"|"+s.number,s.className=s.variable,s.id=s.variable;for(var l=function(e){return e.replace(new RegExp("\\\\("+s.metaChar+")","g"),function(e,t){return t})},u=s.comparatorOp.split("|"),c=0;c=0||"="!==d&&(s.comparatorOp+="|\\!"+d)}var p={group:{query:!0,regex:"(node|edge|\\*)",populate:function(e){this.group="*"==e?e:e+"s"}},state:{query:!0,regex:"(:selected|:unselected|:locked|:unlocked|:visible|:hidden|:transparent|:grabbed|:free|:removed|:inside|:grabbable|:ungrabbable|:animated|:unanimated|:selectable|:unselectable|:orphan|:nonorphan|:parent|:child|:loop|:simple|:active|:inactive|:touch)",populate:function(e){this.colonSelectors.push(e)}},id:{query:!0,regex:"\\#("+s.id+")",populate:function(e){this.ids.push(l(e))}},className:{query:!0,regex:"\\.("+s.className+")",populate:function(e){this.classes.push(l(e))}},dataExists:{query:!0,regex:"\\[\\s*("+s.variable+")\\s*\\]",populate:function(e){this.data.push({field:l(e)})}},dataCompare:{query:!0,regex:"\\[\\s*("+s.variable+")\\s*("+s.comparatorOp+")\\s*("+s.value+")\\s*\\]",populate:function(e,t,i){var r=null!=new RegExp("^"+s.string+"$").exec(i);i=r?i.substring(1,i.length-1):parseFloat(i),this.data.push({field:l(e),operator:t,value:i})}},dataBool:{query:!0,regex:"\\[\\s*("+s.boolOp+")\\s*("+s.variable+")\\s*\\]",populate:function(e,t){this.data.push({field:l(t),operator:e})}},metaCompare:{query:!0,regex:"\\[\\[\\s*("+s.meta+")\\s*("+s.comparatorOp+")\\s*("+s.number+")\\s*\\]\\]",populate:function(e,t,i){this.meta.push({field:l(e),operator:t,value:parseFloat(i)})}},nextQuery:{separator:!0,regex:s.separator,populate:function(){r[++c]=o(),a=null}},child:{separator:!0,regex:s.child,populate:function(){var e=o();e.parent=this,e.subject=a,r[c]=e}},descendant:{separator:!0,regex:s.descendant,populate:function(){var e=o();e.ancestor=this,e.subject=a,r[c]=e}},subject:{modifier:!0,regex:s.subject,populate:function(){return null!=a&&this.subject!=this?(e.util.error("Redefinition of subject in selector `"+i+"`"),!1):(a=this,void(this.subject=this))}}},h=0;for(var v in p)p[h]=p[v],p[h].name=v,h++;p.length=h,r._private.selectorText=i;var g=i,c=0,f=function(t){for(var i,r,n,a=0;a=0&&(d=d.toLowerCase(),p=p.toLowerCase(),s=s.replace("@",""),h=!0);var v=!1,g=!1;switch(s.indexOf("!")>=0&&(s=s.replace("!",""),v=!0),h&&(l=p.toLowerCase(),c=d.toLowerCase()),s){case"*=":a=d.search(p)>=0;break;case"$=":a=null!=new RegExp(p+"$").exec(d);break;case"^=":a=null!=new RegExp("^"+p).exec(d);break;case"=":a=c===l;break;case"!=":a=c!==l;break;case">":a=v?l>=c:c>l,g=!0;break;case">=":a=v?l>c:c>=l,g=!0;break;case"<":a=v?c>=l:l>c,g=!0;break;case"<=":a=v?c>l:l>=c,g=!0;break;default:a=!1}}else if(null!=s)switch(s){case"?":a=t.fieldTruthy(u);break;case"!":a=!t.fieldTruthy(u);break;case"^":a=t.fieldUndefined(u)}else a=!t.fieldUndefined(u);if(v&&!g&&(a=!a,g=!0),!a){r=!1;break}}return r},v=h({name:"data",fieldValue:function(e){return r._private.data[e]},fieldRef:function(e){return"element._private.data."+e},fieldUndefined:function(e){return void 0===r._private.data[e]},fieldTruthy:function(e){return r._private.data[e]?!0:!1}});if(!v)return!1;var g=h({name:"meta",fieldValue:function(e){return r[e]()},fieldRef:function(e){return"element."+e+"()"},fieldUndefined:function(e){return null==r[e]()},fieldTruthy:function(e){return r[e]()?!0:!1}});if(!g)return!1;if(null!=i.collection){var f=null!=i.collection._private.ids[r.id()];if(!f)return!1}if(null!=i.filter&&0===r.collection().filter(i.filter).size())return!1;var y=function(e,i){if(null!=e){var r=!1;if(!n.hasCompoundNodes())return!1;i=i();for(var a=0;a "+t),null!=e.ancestor&&(t=r(e.ancestor)+" "+t),null!=e.child&&(t+=" > "+r(e.child)),null!=e.descendant&&(t+=" "+r(e.descendant)),t},n=0;n1&&n node").css({width:"auto",height:"auto",shape:"rectangle","background-opacity":.5,"padding-top":10,"padding-right":10,"padding-left":10,"padding-bottom":10}).selector("edge").css({width:1}).selector(":active").css({"overlay-color":"black","overlay-padding":10,"overlay-opacity":.25}).selector("core").css({"selection-box-color":"#ddd","selection-box-opacity":.65,"selection-box-border-color":"#aaa","selection-box-border-width":1,"active-bg-color":"black","active-bg-opacity":.15,"active-bg-size":e.is.touch()?40:15,"outside-texture-bg-color":"#000","outside-texture-bg-opacity":.125}),this.defaultLength=this.length},e.styfn.clear=function(){for(var e=0;el.max)return null;var P={name:t,value:i,strValue:""+i+(x?x:""),units:x,bypass:r,hasPie:t.match(/pie-(\d+)-background-size/)&&null!=i&&0!==i&&""!==i};return l.unitless||"px"!==x&&"em"!==x||(P.pxValue="px"!==x&&x?this.getEmSizeInPixels()*i:i),("ms"===x||"s"===x)&&(P.msValue="ms"===x?i:1e3*i),P}if(l.propList){var D=[],k=""+i;if("none"===k);else{for(var C=k.split(","),E=0;E0;if(p||h){var v;p&&h?v=u.properties:p?v=u.properties:h&&(v=u.mappedProperties);for(var g=0;g0){a=!0;break}}i.hasPie=a;var l=n["text-transform"].strValue,u=n.content.strValue,c=n["font-style"].strValue,s=n["font-size"].pxValue+"px",d=n["font-family"].strValue,p=n["font-weight"].strValue,h=n["text-valign"].strValue,v=n["text-valign"].strValue,g=n["text-outline-width"].pxValue;i.labelKey=c+"$"+s+"$"+d+"$"+p+"$"+u+"$"+l+"$"+h+"$"+v+"$"+g,i.fontKey=c+"$"+p+"$"+s+"$"+d;var f=n.width.pxValue,y=n.height.pxValue,m=n["border-width"].pxValue;if(i.boundingBoxKey=f+"$"+y+"$"+m,"edges"===t._private.group){var x=n["control-point-step-size"].pxValue,b=n["control-point-distance"]?n["control-point-distance"].pxValue:void 0,w=n["control-point-weight"].value,_=n["curve-style"].strValue;i.boundingBoxKey+="$"+x+"$"+b+"$"+w+"$"+_}i.styleKey=Date.now()},e.styfn.applyParsedProperty=function(t,i){var r,n,a=i,o=t._private.style,s=e.style.properties[a.name].type,l=a.bypass,u=o[a.name],c=u&&u.bypass;if(("height"===i.name||"width"===i.name)&&"auto"===i.value&&t.isNode()&&!t.isParent())return!1;if(l&&a.deleteBypass){var d=o[a.name];return d?d.bypass&&d.bypassed?(o[a.name]=d.bypassed,!0):!1:!0}var p=function(){e.util.error("Do not assign mappings to elements without corresponding data (e.g. ele `"+t.id()+"` for property `"+a.name+"` with data field `"+a.field+"`); try a `["+a.field+"]` selector to limit scope to elements with `"+a.field+"` defined")};switch(a.mapped){case e.style.types.mapData:case e.style.types.mapLayoutData:for(var h=a.mapped===e.style.types.mapLayoutData,v=a.field.split("."),r=h?t._private.layoutData:t._private.data,g=0;gy?y=0:y>1&&(y=1),s.color){var m=a.valueMin[0],x=a.valueMax[0],b=a.valueMin[1],w=a.valueMax[1],_=a.valueMin[2],E=a.valueMax[2],S=null==a.valueMin[3]?1:a.valueMin[3],P=null==a.valueMax[3]?1:a.valueMax[3],D=[Math.round(m+(x-m)*y),Math.round(b+(w-b)*y),Math.round(_+(E-_)*y),Math.round(S+(P-S)*y)];n={bypass:a.bypass,name:a.name,value:D,strValue:"rgb("+D[0]+", "+D[1]+", "+D[2]+")"}}else{if(!s.number)return!1;var k=a.valueMin+(a.valueMax-a.valueMin)*y;n=this.parse(a.name,k,a.bypass,!0)}n||(n=this.parse(a.name,u.strValue,a.bypass,!0)),n||p(),n.mapping=a,a=n;break;case e.style.types.data:case e.style.types.layoutData:for(var h=a.mapped===e.style.types.layoutData,v=a.field.split("."),r=h?t._private.layoutData:t._private.data,g=0;g0&&s>0){for(var c=!1,d=0;d0&&t.delay(l),t.animate({css:u},{duration:s,queue:!1,complete:function(){r||n.removeBypasses(t,o),t._private.transitioning=!1}})}else t._private.transitioning&&(t.stop(),this.removeBypasses(t,o),t._private.transitioning=!1)}}(cytoscape),function(e){"use strict";e.styfn.applyBypass=function(t,i,r,n){var a=[],o=!0;if("*"===i||"**"===i){if(void 0!==r)for(var s=0;sa.length?l.substr(a.length):""}function n(){o=o.length>s.length?o.substr(s.length):""}var a,o,s,l=""+i;for(l=l.replace(/[/][*](\s|.)+?[*][/]/g,"");;){var u=l.match(/^\s*$/);if(u)break;var c=l.match(/^\s*((?:.|\s)+?)\s*\{((?:.|\s)+?)\}/);if(!c){e.util.error("Halting stylesheet parsing: String stylesheet contains more to parse but no selector and block found in: "+l);break}a=c[0];var d=c[1],p=new e.Selector(d);if(p._private.invalid&&"core"!==d)e.util.error("Skipping parsing of block: Invalid selector found in string stylesheet: "+d),r();else{var h=c[2],v=!1;o=h;for(var g=[];;){var u=o.match(/^\s*$/);if(u)break;var f=o.match(/^\s*(.+?)\s*:\s*(.+?)\s*;/);if(!f){e.util.error("Skipping parsing of block: Invalid formatting of style property and value definitions found in:"+h),v=!0;break}s=f[0];var y=f[1],m=f[2],x=e.style.properties[y];if(x){var b=t.parse(y,m);b?(g.push({name:y,val:m}),n()):(e.util.error("Skipping property: Invalid property definition in: "+s),n())}else e.util.error("Skipping property: Invalid property name in: "+s),n()}if(v){r();break}t.selector(d);for(var w=0;w0?c.wheelSensitivity:1,motionBlur:c.motionBlur,pixelRatio:e.is.number(c.pixelRatio)&&c.pixelRatio>0?c.pixelRatio:"auto"===c.pixelRatio?void 0:1,tapThreshold:d(e.is.touch()?8:4,e.is.touch()?c.touchTapThreshold:c.desktopTapThreshold)},c.renderer)),c.initrender&&(a.on("initrender",c.initrender),a.on("initrender",function(){a._private.initrender=!0})),a.load(c.elements,function(){a.startAnimationLoop(),a._private.ready=!0,e.is.fn(c.ready)&&a.on("ready",c.ready);for(var t=0;t0;)r.shift()();i.deferredTimeout=null},0))}})}(cytoscape,"undefined"==typeof window?null:window),function(e,t){"use strict";function i(e){var t=!document||"interactive"!==document.readyState&&"complete"!==document.readyState?i:e;setTimeout(t,9,e)}e.fn.core({add:function(t){var i,r=this;if(e.is.elementOrCollection(t)){var n=t;if(n._private.cy===r)i=n.restore();else{for(var a=[],o=0;oo;o++){var d=u[o],p=l[d];if(e.is.array(p))for(var h=0,v=p.length;v>h;h++){var g=p[h];g.group=d,a.push(g)}}i=new e.Collection(r,a)}else{var g=t;i=new e.Element(r,g).collection()}return i},remove:function(t){if(e.is.elementOrCollection(t))t=t;else if(e.is.string(t)){var i=t;t=this.$(i)}return t.remove()},load:function(r,n,a){function o(){s.one("layoutready",function(e){s.notifications(!0),s.trigger(e),s.notify({type:"load",collection:s.elements()}),s.one("load",n),s.trigger("load")}).one("layoutstop",function(){s.one("done",a),s.trigger("done")});var t=e.util.extend({},s._private.options.layout);t.eles=s.$(),s.layout(t)}var s=this;s.notifications(!1);var l=s.elements();return l.length>0&&l.remove(),null!=r&&(e.is.plainObject(r)||e.is.array(r))&&s.add(r),t?i(o):o(),this}})}(cytoscape,"undefined"==typeof window?null:window),function(e,t){"use strict";e.fn.core({animated:e.define.animated(),clearQueue:e.define.clearQueue(),delay:e.define.delay(),animate:e.define.animate(),stop:e.define.stop(),addToAnimationPool:function(e){var t=this;t.styleEnabled()&&t._private.aniEles.merge(e)},startAnimationLoop:function(){function i(){e.util.requestAnimationFrame(function(e){r(e),i()})}function r(t){function i(i,r){var s=i._private.animation.current,l=i._private.animation.queue,u=!1;if(0===s.length){var c=l.length>0?l.shift():null;c&&(c.callTime=t,s.push(c))}for(var d=[],p=s.length-1;p>=0;p--){var h=s[p];h.started||n(i,h),a(i,h,t,r),h.done&&(d.push(h),s.splice(p,1)),u=!0}for(var p=0;p0||c){var d;if(r.length>0){var p=r.updateCompoundBounds();d=p.length>0?r.add(p):r}l.notify({type:"draw",collection:d})}r.unmerge(o)}function n(t,i){var r=e.is.core(t),n=!r,a=t,o=l._private.style;if(n)var s=a._private.position,u={x:s.x,y:s.y},c=o.getValueStyle(a);if(r)var d=l._private.pan,p={x:d.x,y:d.y},h=l._private.zoom;i.started=!0,i.startTime=Date.now(),i.startPosition=u,i.startStyle=c,i.startPan=p,i.startZoom=h}function a(t,i,r,n){var a,u=l._private.style,c=i.properties,d=i.params,p=i.startTime,h=!n;if(a=0===i.duration?1:Math.min(1,(r-p)/i.duration),0>a?a=0:a>1&&(a=1),null==c.delay){var v=i.startPosition,g=c.position,f=t._private.position;g&&h&&(o(v.x,g.x)&&(f.x=s(v.x,g.x,a)),o(v.y,g.y)&&(f.y=s(v.y,g.y,a)));var y=i.startPan,m=c.pan,x=t._private.pan,b=null!=m&&n;b&&(o(y.x,m.x)&&(x.x=s(y.x,m.x,a)),o(y.y,m.y)&&(x.y=s(y.y,m.y,a)),t.trigger("pan"));var w=i.startZoom,_=c.zoom,E=null!=_&&n;if(E&&(o(w,_)&&(t._private.zoom=s(w,_,a)),t.trigger("zoom")),(b||E)&&t.trigger("viewport"),c.css&&h)for(var S=c.css,P=0;P=1&&(i.done=!0),a}function o(t,i){return null==t||null==i?!1:e.is.number(t)&&e.is.number(i)?!0:t&&i?!0:!1}function s(t,i,r){0>r?r=0:r>1&&(r=1);var n,a;if(n=null!=t.pxValue||null!=t.value?null!=t.pxValue?t.pxValue:t.value:t,a=null!=i.pxValue||null!=i.value?null!=i.pxValue?i.pxValue:i.value:i,e.is.number(n)&&e.is.number(a))return n+(a-n)*r;if(e.is.number(n[0])&&e.is.number(a[0])){var o=n,s=a,l=function(e,t){var i=t-e,n=e;return Math.round(r*i+n)},u=l(o[0],s[0]),c=l(o[1],s[1]),d=l(o[2],s[2]);return[u,c,d]}return void 0}var l=this;l.styleEnabled()&&t&&i()}})}(cytoscape,"undefined"==typeof window?null:window),function(e){"use strict";e.fn.core({data:e.define.data({field:"data",bindingEvent:"data",allowBinding:!0,allowSetting:!0,settingEvent:"data",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0}),removeData:e.define.removeData({field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!0}),scratch:e.define.data({field:"scratch",allowBinding:!1,allowSetting:!0,settingTriggersEvent:!1,allowGetting:!0}),removeScratch:e.define.removeData({field:"scratch",triggerEvent:!1})})}(cytoscape),function(e){"use strict";e.fn.core({on:e.define.on(),one:e.define.on({unbindSelfOnTrigger:!0}),once:e.define.on({unbindAllBindersOnTrigger:!0}),off:e.define.off(),trigger:e.define.trigger()}),e.corefn.bind=e.corefn.on,e.corefn.unbind=e.corefn.off}(cytoscape),function(e){"use strict";e.fn.core({png:function(e){var t=this._private.renderer;return e=e||{},t.png(e)}})}(cytoscape),function(e){"use strict";e.fn.core({layout:function(t){var i;return null==t&&(t=e.util.extend({},this._private.options.layout),t.eles=this.$()),i=this.initLayout(t),i.run(),this},makeLayout:function(e){return this.initLayout(e)},initLayout:function(t){if(null==t)return void e.util.error("Layout options must be specified to make a layout");if(null==t.name)return void e.util.error("A `name` must be specified to make a layout");var i=t.name,r=e.extension("layout",i);if(null==r)return void e.util.error("Can not apply layout: No such layout `"+i+"` found; did you include its JS file?");t.eles=null!=t.eles?t.eles:this.$(),e.is.string(t.eles)&&(t.eles=this.$(t.eles));var n=new r(e.util.extend({},t,{cy:this}));return e.is.plainObject(n._private)||(n._private={}),n._private.cy=this,n._private.listeners=[],n}})}(cytoscape),function(e){"use strict";e.fn.core({notify:function(e){if(this._private.batchingNotify){var t=this._private.batchNotifyEles,i=this._private.batchNotifyTypes;if(e.collection)for(var r=0;r0&&l>0&&!isNaN(r.w)&&!isNaN(r.h)&&r.w>0&&r.h>0){o=Math.min((s-2*i)/r.w,(l-2*i)/r.h),o=o>this._private.maxZoom?this._private.maxZoom:o,o=othis._private.maxZoom?this._private.maxZoom:r,r=ri.maxZoom||!i.zoomingEnabled?o=!0:(i.zoom=l,a.push("zoom"))}if(n&&(!o||!t.cancelOnFailedZoom)&&i.panningEnabled){var u=t.pan;e.is.number(u.x)&&(i.pan.x=u.x,s=!1),e.is.number(u.y)&&(i.pan.y=u.y,s=!1),s||a.push("pan")}return a.length>0&&(a.push("viewport"),this.trigger(a.join(" ")),this.notify({type:"viewport"})),this},center:function(e){var t=this.getCenterPan(e);return t&&(this._private.pan=t,this.trigger("pan viewport"),this.notify({type:"viewport"})),this},getCenterPan:function(t,i){if(this._private.panningEnabled){if(e.is.string(t)){var r=t;t=this.elements(r)}else e.is.elementOrCollection(t)||(t=this.elements());var n=t.boundingBox(),a=this.width(),o=this.height();i=void 0===i?this._private.zoom:i;var s={x:(a-i*(n.x1+n.x2))/2,y:(o-i*(n.y1+n.y2))/2};return s}},reset:function(){return this._private.panningEnabled&&this._private.zoomingEnabled?(this.viewport({pan:{x:0,y:0},zoom:1}),this):this},width:function(){var e=this._private.container;return e?e.clientWidth:1},height:function(){var e=this._private.container;return e?e.clientHeight:1},extent:function(){var e=this._private.pan,t=this._private.zoom,i=this.renderedExtent(),r={x1:(i.x1-e.x)/t,x2:(i.x2-e.x)/t,y1:(i.y1-e.y)/t,y2:(i.y2-e.y)/t};return r.w=r.x2-r.x1,r.h=r.y2-r.y1,r},renderedExtent:function(){var e=this.width(),t=this.height();return{x1:0,y1:0,x2:e,y2:t,w:e,h:t}}}),e.corefn.centre=e.corefn.center,e.corefn.autolockNodes=e.corefn.autolock,e.corefn.autoungrabifyNodes=e.corefn.autoungrabify}(cytoscape),function(e){"use strict";e.fn.collection=e.fn.eles=function(t){for(var i in t){var r=t[i];e.Collection.prototype[i]=r}};var t={prefix:{nodes:"n",edges:"e"},id:{nodes:0,edges:0},generate:function(t,i,r){var n=e.is.element(i)?i._private:i,a=n.group,o=null!=r?r:this.prefix[a]+this.id[a];if(t.getElementById(o).empty())this.id[a]++;else for(;!t.getElementById(o).empty();)o=this.prefix[a]+ ++this.id[a];return o}};e.Element=function(t,i,r){if(!(this instanceof e.Element))return new e.Element(t,i,r);var n=this;if(r=void 0===r||r?!0:!1,void 0===t||void 0===i||!e.is.core(t))return void e.util.error("An element must have a core reference and parameters set");if("nodes"!==i.group&&"edges"!==i.group)return void e.util.error("An element must be of type `nodes` or `edges`; you specified `"+i.group+"`");if(this.length=1,this[0]=this,this._private={cy:t,single:!0,data:i.data||{},layoutData:{},position:i.position||{},autoWidth:void 0,autoHeight:void 0,listeners:[],group:i.group,style:{},rstyle:{},styleCxts:[],removed:!0,selected:i.selected?!0:!1,selectable:void 0===i.selectable?!0:i.selectable?!0:!1,locked:i.locked?!0:!1,grabbed:!1,grabbable:void 0===i.grabbable?!0:i.grabbable?!0:!1,active:!1,classes:{},animation:{current:[],queue:[]},rscratch:{},scratch:{},edges:[],children:[]},i.renderedPosition){var a=i.renderedPosition,o=t.pan(),s=t.zoom();this._private.position={x:(a.x-o.x)/s,y:(a.y-o.y)/s}}if(e.is.string(i.classes))for(var l=i.classes.split(/\s+/),u=0,c=l.length;c>u;u++){var d=l[u];d&&""!==d&&(n._private.classes[d]=!0)}i.css&&t.style().applyBypass(this,i.css),(void 0===r||r)&&this.restore()},e.Collection=function(i,r,n){if(!(this instanceof e.Collection))return new e.Collection(i,r);if(void 0===i||!e.is.core(i))return void e.util.error("A collection must have a reference to the core");var a={},o={},s=!1;if(r){if(r.length>0&&e.is.plainObject(r[0])&&!e.is.element(r[0])){s=!0;for(var l=[],u={},c=0,d=r.length;d>c;c++){var p=r[c];null==p.data&&(p.data={});var h=p.data;if(null==h.id)h.id=t.generate(i,p);else if(0!==i.getElementById(h.id).length||u[h.id])continue;var v=new e.Element(i,p,!1);l.push(v),u[h.id]=!0}r=l}}else r=[];this.length=0;for(var c=0,d=r.length;d>c;c++){var g=r[c];if(g){var f=g._private.data.id;(!n||n.unique&&!a[f])&&(a[f]=g,o[f]=this.length,this[this.length]=g,this.length++)}}this._private={cy:i,ids:a,indexes:o},s&&this.restore()},e.elefn=e.elesfn=e.Element.prototype=e.Collection.prototype,e.elesfn.cy=function(){return this._private.cy},e.elesfn.element=function(){return this[0]},e.elesfn.collection=function(){return e.is.collection(this)?this:new e.Collection(this._private.cy,[this])},e.elesfn.unique=function(){return new e.Collection(this._private.cy,this,{unique:!0})},e.elesfn.getElementById=function(t){var i=this._private.cy,r=this._private.ids[t];return r?r:e.Collection(i)},e.elesfn.json=function(){var t=this.element();if(null==t)return void 0;var i=t._private,r=e.util.copy({data:i.data,position:i.position,group:i.group,bypass:i.bypass,removed:i.removed,selected:i.selected,selectable:i.selectable,locked:i.locked,grabbed:i.grabbed,grabbable:i.grabbable,classes:""}),n=[];for(var a in i.classes)i.classes[a]&&n.push(a);for(var o=0;od;d++){var h=r[d];h.isNode()?(s.push(h),u++):(l.push(h),c++)}o=s.concat(l);for(var d=0,p=o.length;p>d;d++){var h=o[d];if(h.removed()){var v=h._private,g=v.data;if(void 0===g.id)g.id=t.generate(a,h);else{if(e.is.emptyString(g.id)||!e.is.string(g.id)){e.util.error("Can not create element with invalid string ID `"+g.id+"`");continue}if(0!==a.getElementById(g.id).length){e.util.error("Can not create second element with ID `"+g.id+"`");continue}}var f=g.id;if(h.isEdge()){for(var y=h,m=["source","target"],x=m.length,b=!1,w=0;x>w;w++){var _=m[w],E=g[_];null==E||""===E?(e.util.error("Can not create edge `"+f+"` with unspecified "+_),b=!0):a.getElementById(E).empty()&&(e.util.error("Can not create edge `"+f+"` with nonexistant "+_+" `"+E+"`"),b=!0)}if(b)continue;var S=a.getElementById(g.source),P=a.getElementById(g.target);S._private.edges.push(y),P._private.edges.push(y),y._private.source=S,y._private.target=P}v.ids={},v.ids[f]=h,v.removed=!1,a.addToPool(h),n.push(h)}}for(var d=0;u>d;d++){var D=o[d],g=D._private.data,k=D._private.data.parent,C=null!=k;if(C){var T=a.getElementById(k);if(T.empty())g.parent=void 0;else{for(var N=!1,M=T;!M.empty();){if(D.same(M)){N=!0,g.parent=void 0;break}M=M.parent()}N||(T[0]._private.children.push(D),D._private.parent=T[0],a._private.hasCompoundNodes=!0)}}}if(n=new e.Collection(a,n),n.length>0){var B=n.add(n.connectedNodes()).add(n.parent());B.updateStyle(i),i?n.rtrigger("add"):n.trigger("add")}return r},e.elesfn.removed=function(){var e=this[0];return e&&e._private.removed},e.elesfn.inside=function(){var e=this[0];return e&&!e._private.removed},e.elesfn.remove=function(t){function i(e){for(var t=e._private.edges,i=0;ip;p++){var v=s[p];n(v)}for(var p=0;p0&&(t&&this.cy().notify({type:"remove",collection:x}),x.trigger("remove"));for(var b={},p=0;p0,a=t.getElementById(r).length>0;if(n||a){var o=this.jsons();this.remove();for(var s=0;s0;if(c){var o=this.jsons(),d=this.descendants(),p=d.merge(d.add(this).connectedEdges());this.remove();for(var s=0;su||!n)&&(o=u,n=l)}return{edge:n,dist:o}};g.size()>0;){var y=g.pop(),m=y.value,x=y.id,b=a.getElementById(x);if(u[x]=m,m===Math.Infinite)break;for(var w=b.neighborhood().intersect(d),h=0;h0)for(r.unshift(i);l[n.id()];){var o=l[n.id()];r.unshift(o.edge),r.unshift(o.node),n=o.node}return new e.Collection(a,r)}}}}),e.elesfn.bfs=e.elesfn.breadthFirstSearch,e.elesfn.dfs=e.elesfn.depthFirstSearch,e.elesfn.stdBfs=e.elesfn.stdBreadthFirstSearch,e.elesfn.stdDfs=e.elesfn.stdDepthFirstSearch}(cytoscape),function(e){"use strict";e.fn.eles({aStar:function(t){t=t||{};var i=function(){a&&console.log.apply(console,arguments)},r=function(e,t,i,n){if(e==t)return n.push(o.getElementById(t)),n;if(t in i){var a=i[t],s=g[t];return n.push(o.getElementById(t)),n.push(o.getElementById(s)),r(e,a,i,n)}return void 0},n=function(e,t){if(0===e.length)return void 0;for(var i=0,r=t[e[0]],n=1;na&&(r=a,i=n)}return i};if(null!=t.debug)var a=t.debug;else var a=!1;i("Starting aStar...");var o=this._private.cy;if(null==t||null==t.root)return void 0;var s=e.is.string(t.root)?this.filter(t.root)[0]:t.root[0];if(i("Source node: %s",s.id()),null==t.goal)return void 0;var l=e.is.string(t.goal)?this.filter(t.goal)[0]:t.goal[0];if(i("Target node: %s",l.id()),null!=t.heuristic&&e.is.fn(t.heuristic))var u=t.heuristic;else var u=function(){return 0};if(null!=t.weight&&e.is.fn(t.weight))var c=t.weight;else var c=function(){return 1};if(null!=t.directed)var d=t.directed;else var d=!1;var p=[],h=[s.id()],v={},g={},f={},y={};f[s.id()]=0,y[s.id()]=u(s);for(var m=this.edges().not(":loop"),x=this.nodes(),b=0;h.length>0;){var w=n(h,y),_=this.filter("#"+h[w])[0];if(b++,i("\nStep: %s",b),i("Processing node: %s, fScore = %s",_.id(),y[_.id()]),_.id()==l.id()){i("Found goal node!");var E=r(s.id(),l.id(),v,[]);return E.reverse(),i("Path: %s",E),{found:!0,distance:f[_.id()],path:new e.Collection(o,E),steps:b}}p.push(_.id()),h.splice(w,1),i("Added node to closedSet, removed from openSet."),i("Processing neighbors...");for(var S=_.connectedEdges(d?'[source = "'+_.id()+'"]':void 0).intersect(m),P=0;Pd;d++)c[l[d].id()]=d;for(var p=[],d=0;u>d;d++){for(var h=new Array(u),v=0;u>v;v++)h[v]=d==v?0:1/0;p.push(h)}var g=[],f=[],y=function(e){for(var t=0;u>t;t++){for(var i=new Array(u),r=0;u>r;r++)i[r]=void 0;e.push(i)}};y(g),y(f);for(var d=0;db&&(p[m][x]=b,g[m][x]=x,f[m][x]=s[d])}if(!o)for(var d=0;db&&(p[m][x]=b,g[m][x]=x,f[m][x]=s[d])}for(var w=0;u>w;w++)for(var d=0;u>d;d++)for(var v=0;u>v;v++)p[d][w]+p[w][v]d;d++)_.push(l[d].id());var E={distance:function(t,i){if(e.is.string(t))var r=n.filter(t)[0].id();else var r=t.id();if(e.is.string(i))var a=n.filter(i)[0].id();else var a=i.id();return p[c[r]][c[a]]},path:function(t,i){var r=function(e,t,i,r,a){if(e===t)return n.getElementById(r[e]);if(void 0===i[e][t])return void 0;for(var o=[n.getElementById(r[e])],s=e;e!==t;){s=e,e=i[e][t];var l=a[s][e];o.push(l),o.push(n.getElementById(r[e]))}return o};if(e.is.string(t))var a=n.filter(t)[0].id();else var a=t.id();if(e.is.string(i))var o=n.filter(i)[0].id();else var o=i.id();var s=r(c[a],c[o],g,_,f);return new e.Collection(n,s)}};return E},bellmanFord:function(t){t=t||{};var i=function(){r&&console.log.apply(console,arguments)};if(null!=t.debug)var r=t.debug;else var r=!1;if(i("Starting bellmanFord..."),null!=t.weight&&e.is.fn(t.weight))var n=t.weight;else var n=function(){return 1};if(null!=t.directed)var a=t.directed;else var a=!1;if(null==t.root)return void e.util.error("options.root required");if(e.is.string(t.root))var o=this.filter(t.root)[0];else var o=t.root[0];i("Source node: %s",o.id());for(var s=this._private.cy,l=this.edges().not(":loop"),u=this.nodes(),c=u.length,d={},p=0;c>p;p++)d[u[p].id()]=p;for(var h=[],v=[],g=[],p=0;c>p;p++)h[p]=u[p].id()===o.id()?0:1/0,v[p]=void 0;for(var f=!1,p=1;c>p;p++){f=!1;for(var y=0;yp;p++)_.push(u[p].id());var E={distanceTo:function(t){if(e.is.string(t))var i=s.filter(t)[0].id();else var i=t.id();return h[d[i]]},pathTo:function(t){var i=function(e,t,i,r,n,a){for(;;){if(n.push(s.getElementById(r[i])),n.push(a[i]),t===i)return n;var o=e[i];if("undefined"==typeof o)return void 0;i=o}};if(e.is.string(t))var r=s.filter(t)[0].id();else var r=t.id();var n=[],a=i(v,d[o.id()],d[r],_,n,g);return null!=a&&a.reverse(),new e.Collection(s,a)},hasNegativeWeightCycle:!1};return E},kargerStein:function(t){t=t||{};var i=function(){a&&console.log.apply(console,arguments)},r=function(e,t,i){for(var r=i[e],n=r[1],a=r[2],o=t[n],s=t[a],l=i.filter(function(e){return t[e[1]]===o&&t[e[2]]===s?!1:t[e[1]]===s&&t[e[2]]===o?!1:!0}),u=0;u=i)return t;var o=Math.floor(Math.random()*t.length),s=r(o,e,t);return n(e,s,i-1,a)};if(null!=t&&null!=t.debug)var a=t.debug;else var a=!1;i("Starting kargerStein...");var o=this._private.cy,s=this.edges().not(":loop"),l=this.nodes(),u=l.length,c=s.length,d=Math.ceil(Math.pow(Math.log(u)/Math.LN2,2)),p=Math.floor(u/Math.sqrt(2));if(2>u)return void e.util.error("At least 2 nodes are required for KargerSteing algorithm!");for(var h={},v=0;u>v;v++)h[l[v].id()]=v;for(var g=[],v=0;c>v;v++){var f=s[v];g.push([v,h[f.source().id()],h[f.target().id()]])}for(var y,m=1/0,x=[],v=0;u>v;v++)x.push(v);for(var b=0;d>=b;b++){var w=x.slice(0),_=n(w,g,u,p),E=w.slice(0),S=n(w,_,p,2),P=n(E,_,p,2);S.length<=P.length&&S.lengthr;r++)i+=e[r];for(var r=0;t>r;r++)e[r]=e[r]/i},r=function(){n&&console.log.apply(console,arguments)};if(null!=t&&null!=t.debug)var n=t.debug;else var n=!1;if(r("Starting pageRank..."),null!=t&&null!=t.dampingfactor)var a=t.dampingFactor;else var a=.8;if(null!=t&&null!=t.precision)var o=t.precision;else var o=1e-6;if(null!=t&&null!=t.iterations)var s=t.iterations;else var s=200;if(null!=t&&null!=t.weight&&e.is.fn(t.weight))var l=t.weight;else var l=function(){return 1};for(var u=this._private.cy,c=this.edges().not(":loop"),d=this.nodes(),p=d.length,h=c.length,v={},g=0;p>g;g++)v[d[g].id()]=g;for(var f=[],y=[],m=(1-a)/p,g=0;p>g;g++){for(var x=[],b=0;p>b;b++)x.push(0);f.push(x),y.push(0)}for(var g=0;h>g;g++){var w=c[g],_=v[w.source().id()],E=v[w.target().id()],S=l.apply(w,[w]);f[E][_]+=S,y[_]+=S}for(var P=1/p+m,b=0;p>b;b++)if(0===y[b])for(var g=0;p>g;g++)f[g][b]=P;else for(var g=0;p>g;g++)f[g][b]=f[g][b]/y[b]+m;for(var D,k=[],C=[],g=0;p>g;g++)k.push(1),C.push(0);for(var T=0;s>T;T++){for(var N=C.slice(0),g=0;p>g;g++)for(var b=0;p>b;b++)N[g]+=f[g][b]*k[b];i(N),D=k,k=N;for(var M=0,g=0;p>g;g++)M+=Math.pow(D[g]-k[g],2);if(o>M){r("Stoped at iteration %s",T);break}}r("Result:\n"+k);var B={rank:function(t){if(e.is.string(t))var i=u.filter(t)[0].id();else var i=t.id();return k[v[i]]}};return B}})}(cytoscape),function(e){"use strict";e.fn.eles({animated:e.define.animated(),clearQueue:e.define.clearQueue(),delay:e.define.delay(),animate:e.define.animate(),stop:e.define.stop()})}(cytoscape),function(e){"use strict";e.fn.eles({classes:function(t){var i,r=this,n=[];if(e.is.fn(t))i=t;else if(!e.is.plainObject(t))return this;for(var a=0;a0&&new e.Collection(this.cy(),n).updateStyle().trigger("class"),this},addClass:function(t){t=t.split(/\s+/);for(var i=this,r=[],n=0;n0&&new e.Collection(this._private.cy,r).updateStyle().trigger("class"),i},hasClass:function(e){var t=this[0];return null!=t&&t._private.classes[e]?!0:!1},toggleClass:function(t,i){for(var r=t.split(/\s+/),n=this,a=[],o=0,s=n.length;s>o;o++)for(var l=n[o],u=0;u0&&new e.Collection(this._private.cy,a).updateStyle().trigger("class"),n},removeClass:function(t){t=t.split(/\s+/);for(var i=this,r=[],n=0;n0&&new e.Collection(i._private.cy,r).updateStyle(),i.trigger("class"),i},flashClass:function(e,t){var i=this;if(null==t)t=250;else if(0===t)return i;return i.addClass(e),setTimeout(function(){i.removeClass(e)},t),i}})}(cytoscape),function(e){"use strict";e.fn.eles({allAre:function(e){return this.filter(e).length===this.length},is:function(e){return this.filter(e).length>0},some:function(e,t){for(var i=0;i0},allAreNeighbors:function(e){return e=this.cy().collection(e),this.neighborhood().intersect(e).length===e.length}})}(cytoscape),function(e){"use strict";e.fn.eles({parent:function(t){for(var i=[],r=this._private.cy,n=0;n0&&i.push(o)}return new e.Collection(r,i,{unique:!0}).filter(t)},parents:function(t){for(var i=[],r=this.parent();r.nonempty();){for(var n=0;n0?this.add(s):this;i?l.trigger("position"):l.rtrigger("position")}return this},silentPositions:function(e){return this.positions(e,!0)},updateCompoundBounds:function(){function t(e){var t=e.children(),i=e._private.style,n=t.boundingBox({includeLabels:!1,includeEdges:!1}),a={top:i["padding-top"].pxValue,bottom:i["padding-bottom"].pxValue,left:i["padding-left"].pxValue,right:i["padding-right"].pxValue},o=e._private.position,s=!1;"auto"===i.width.value&&(e._private.autoWidth=n.w+a.left+a.right,o.x=(n.x1+n.x2-a.left+a.right)/2,s=!0),"auto"===i.height.value&&(e._private.autoHeight=n.h+a.top+a.bottom,o.y=(n.y1+n.y2-a.top+a.bottom)/2,s=!0),s&&r.push(e)}var i=this.cy();if(!i.styleEnabled()||!i.hasCompoundNodes())return i.collection();for(var r=[],n=this.parent();n.nonempty();){for(var a=0;a0,d=c;c&&(u=u[0]);var p=d?u._private.position:{x:0,y:0};return a={x:l.x-p.x,y:l.y-p.y},void 0===t?a:a[t]}for(var h=0;h0,d=c;c&&(u=u[0]);var p=d?u._private.position:{x:0,y:0};void 0!==i?r._private.position[t]=i+p[t]:void 0!==a&&(r._private.position={x:a.x+p.x,y:a.y+p.y})}this.rtrigger("position")}else if(!o)return void 0;return this},width:function(){var e=this[0],t=e._private.cy,i=t._private.styleEnabled;if(e){if(i){var r=e._private.style.width;return"auto"===r.strValue?e._private.autoWidth:r.pxValue}return 1}},outerWidth:function(){var e=this[0],r=e._private.cy,n=r._private.styleEnabled;if(e){if(n){var a=e._private.style,o="auto"===a.width.strValue?e._private.autoWidth:a.width.pxValue,s=a["border-width"]?a["border-width"].pxValue*t+i:0;return o+s}return 1}},renderedWidth:function(){var e=this[0];if(e){var t=e.width();return t*this.cy().zoom()}},renderedOuterWidth:function(){var e=this[0];if(e){var t=e.outerWidth();return t*this.cy().zoom()}},height:function(){var e=this[0],t=e._private.cy,i=t._private.styleEnabled;if(e&&"nodes"===e._private.group){if(i){var r=e._private.style.height;return"auto"===r.strValue?e._private.autoHeight:r.pxValue}return 1}},outerHeight:function(){var e=this[0],r=e._private.cy,n=r._private.styleEnabled;if(e&&"nodes"===e._private.group){if(!n)return 1;var a=e._private.style,o="auto"===a.height.strValue?e._private.autoHeight:a.height.pxValue,s=a["border-width"]?a["border-width"].pxValue*t+i:0;return o+s}},renderedHeight:function(){var e=this[0];if(e&&"nodes"===e._private.group){var t=e.height();return t*this.cy().zoom()}},renderedOuterHeight:function(){var e=this[0];if(e&&"nodes"===e._private.group){var t=e.outerHeight();return t*this.cy().zoom()}},renderedBoundingBox:function(e){var t=this.boundingBox(e),i=this.cy(),r=i.zoom(),n=i.pan(),a=t.x1*r+n.x,o=t.x2*r+n.x,s=t.y1*r+n.y,l=t.y2*r+n.y;return{x1:a,x2:o,y1:s,y2:l,w:o-a,h:l-s}},boundingBox:function(e){var t=this,i=t._private.cy,r=i._private,n=r.styleEnabled;e=e||{};var a=void 0===e.includeNodes?!0:e.includeNodes,o=void 0===e.includeEdges?!0:e.includeEdges,s=void 0===e.includeLabels?!0:e.includeLabels;n&&r.renderer.recalculateRenderedStyle(this);for(var l=1/0,u=-1/0,c=1/0,d=-1/0,p=0;ph?h:l,u=v>u?v:u,c=c>g?g:c,d=f>d?f:d}else if(x.isEdge()&&o){E=!0;var T=x._private.source._private.position,N=x._private.target._private.position,M=x._private.rstyle||{};if(h=T.x,v=N.x,g=T.y,f=N.y,h>v){var B=h;h=v,v=B}if(g>f){var B=g;g=f,f=B}if(l=l>h?h:l,u=v>u?v:u,c=c>g?g:c,d=f>d?f:d,n)for(var z=M.bezierPts||[],P=x._private.style.width.pxValue,I=P/2,L=0;Lh?h:l,u=v>u?v:u,c=c>g?g:c,d=f>d?f:d}}if(n){var R=x._private.style,M=x._private.rstyle,X=R.content.strValue,V=R["font-size"],Y=R["text-halign"],A=R["text-valign"],q=M.labelWidth,F=M.labelHeight,j=M.labelX,W=M.labelY;if(E&&s&&X&&V&&null!=F&&null!=q&&null!=j&&null!=W&&Y&&A){var H,$,Z,U,G=F,K=q;if(x.isEdge())H=j-K/2,$=j+K/2,Z=W-G/2,U=W+G/2;else{switch(Y.value){case"left":H=j-K,$=j;break;case"center":H=j-K/2,$=j+K/2;break;case"right":H=j,$=j+K}switch(A.value){case"top":Z=W-G,U=W;break;case"center":Z=W-G/2,U=W+G/2;break;case"bottom":Z=W,U=W+G}}l=l>H?H:l,u=$>u?$:u,c=c>Z?Z:c,d=U>d?U:d}}}}return{x1:l,x2:u,y1:c,y2:d,w:u-l,h:d-c}}}),e.elesfn.modelPosition=e.elesfn.position,e.elesfn.modelPositions=e.elesfn.positions}(cytoscape),function(e){"use strict";function t(e){return function(t){var i=this;if(void 0===t&&(t=!0),0!==i.length&&i.isNode()&&!i.removed()){for(var r=0,n=i[0],a=n._private.edges,o=0;oe}),maxDegree:i("degree",function(e,t){return e>t}),minIndegree:i("indegree",function(e,t){return t>e}),maxIndegree:i("indegree",function(e,t){return e>t}),minOutdegree:i("outdegree",function(e,t){return t>e}),maxOutdegree:i("outdegree",function(e,t){return e>t})}),e.fn.eles({totalDegree:function(e){for(var t=0,i=this.nodes(),r=0;r1&&!n){var a=this.length-1,o=this[a];this[a]=void 0,this[r]=o,t.indexes[o.id()]=r}return this.length--,this},unmerge:function(t){var i=this._private.cy;if(!t)return this;if(e.is.string(t)){var r=t;t=i.elements(r)}for(var n=0;nr&&(r=s,i=o)}return{value:r,ele:i}},min:function(e,t){for(var i,r=1/0,n=this,a=0;as&&(r=s,i=o)}return{value:r,ele:i}}})}(cytoscape),function(e){"use strict";e.fn.eles({isNode:function(){return"nodes"===this.group()},isEdge:function(){return"edges"===this.group()},isLoop:function(){return this.isEdge()&&this.source().id()===this.target().id()},isSimple:function(){return this.isEdge()&&this.source().id()!==this.target().id()},group:function(){var e=this[0];return e?e._private.group:void 0}})}(cytoscape),function(e){"use strict";e.fn.eles({each:function(t){if(e.is.fn(t))for(var i=0;it&&(t=n+t),0>i&&(i=n+i);for(var a=t;a>=0&&i>a&&n>a;a++)r.push(this[a]);return new e.Collection(this.cy(),r)},size:function(){return this.length},eq:function(t){return this[t]||new e.Collection(this.cy())},first:function(){return this[0]||new e.Collection(this.cy())},last:function(){return this[this.length-1]||new e.Collection(this.cy())},empty:function(){return 0===this.length},nonempty:function(){return!this.empty()},sort:function(t){if(!e.is.fn(t))return this;var i=this.cy(),r=this.toArray().sort(t);return new e.Collection(i,r)},sortByZIndex:function(){return this.sort(e.Collection.zIndexSort)},zDepth:function(){var e=this[0];if(!e)return void 0;var t=e._private,i=t.group;if("nodes"===i)return t.data.parent?e.parents().size():0;var r=t.source,n=t.target,a=r._private.data.parent?r.parents().size():0,o=n._private.data.parent?n.parents().size():0;return Math.max(a-1,o-1,0)+.5}}),e.Collection.zIndexSort=function(e,t){var i=e.cy(),r=e._private,n=t._private,a=r.style["z-index"].value-n.style["z-index"].value,o=0,s=0,l=i.hasCompoundNodes(),u="nodes"===r.group,c="edges"===r.group,d="nodes"===n.group,p="edges"===n.group;l&&(o=e.zDepth(),s=t.zDepth());var h=o-s,v=0===h;return v?u&&p?1:c&&d?-1:0===a?r.index-n.index:a:h}}(cytoscape),function(e){"use strict";e.fn.eles({layoutPositions:function(t,i,r){var n=this.nodes(),a=this.cy();if(t.trigger({type:"layoutstart",layout:t}),i.animate){for(var o=0;o0?this.add(o):this;return e?s.rtrigger("style"):s.trigger("style"),this},updateMappers:function(e){var t=this._private.cy,i=t.style();if(e=e||void 0===e?!0:!1,!t.styleEnabled())return this;i.updateMappers(this);var r=this.updateCompoundBounds(),n=r.length>0?this.add(r):this;return e?n.rtrigger("style"):n.trigger("style"),this},renderedCss:function(e){var t=this.cy();if(!t.styleEnabled())return this;var i=this[0];if(i){var r=i.cy().style().getRenderedStyle(i);return void 0===e?r:r[e]}},css:function(t,i){var r=this.cy();if(!r.styleEnabled())return this;var n=!1,a=r.style();if(e.is.plainObject(t)){var o=t;a.applyBypass(this,o,n);var s=this.updateCompoundBounds(),l=s.length>0?this.add(s):this;l.rtrigger("style")}else if(e.is.string(t)){if(void 0===i){var u=this[0];return u?u._private.style[t].strValue:void 0}a.applyBypass(this,t,i,n);var s=this.updateCompoundBounds(),l=s.length>0?this.add(s):this;l.rtrigger("style")}else if(void 0===t){var u=this[0];return u?a.getRawStyle(u):void 0}return this},removeCss:function(e){var t=this.cy();if(!t.styleEnabled())return this;var i=!1,r=t.style(),n=this;if(void 0===e)for(var a=0;a0?this.add(s):this;return l.rtrigger("style"),this},show:function(){return this.css("display","element"),this},hide:function(){return this.css("display","none"),this},visible:function(){var e=this.cy();if(!e.styleEnabled())return!0; +var t=this[0],i=e.hasCompoundNodes();if(t){var r=t._private.style;if("visible"!==r.visibility.value||"element"!==r.display.value)return!1;if("nodes"===t._private.group){if(!i)return!0;var n=t._private.data.parent?t.parents():null;if(n)for(var a=0;a0;o||r.push(a)}}return new e.Collection(this._private.cy,r,{unique:!0}).filter(t)},leaves:function(t){for(var i=this,r=[],n=0;n0;o||r.push(a)}}return new e.Collection(this._private.cy,r,{unique:!0}).filter(t)},outgoers:function(t){for(var i=this,r=[],n=0;n0&&i.push(c[0]),i.push(u[0])}return new e.Collection(r,i,{unique:!0}).filter(t)},closedNeighborhood:function(e){return this.neighborhood().add(this).filter(e)},openNeighborhood:function(e){return this.neighborhood(e)}}),e.fn.eles({source:function(e){var t,i=this[0];return i&&(t=i._private.source),t&&e?t.filter(e):t},target:function(e){var t,i=this[0];return i&&(t=i._private.target),t&&e?t.filter(e):t},sources:t({attr:"source"}),targets:t({attr:"target"})}),e.fn.eles({edgesWith:i(),edgesTo:i({thisIs:"source"})}),e.fn.eles({connectedEdges:function(t){for(var i=[],r=this._private.cy,n=this,a=0;ad;d+=1){if(l.push(n.call(t,i[d],d,i)),a=i[d].id(),u.hasOwnProperty(a))throw"ERROR: Multiple items with the same id found: "+a;u[a]=d,c.push(a)}for(this._private={cy:t,heap:l,pointers:u,elements:c,comparator:r,extractor:n,length:s},d=Math.floor(s/2);d>=0;d-=1)o=this.heapify(d);return o}},e.Heap.idFn=function(e){return e.id()},e.Heap.minHeapComparator=function(e,t){return e>=t},e.Heap.maxHeapComparator=function(e,t){return t>=e},e.fn.heap=function(t){for(var i in t){var r=t[i];e.Heap.prototype[i]=r}},e.heapfn=e.Heap.prototype,e.heapfn.size=function(){return this._private.length},e.heapfn.getArgumentAsCollection=function(t,i){var r;if("undefined"==typeof i&&(i=this._private.cy),e.is.elementOrCollection(t))r=t;else{for(var n=[],a=[].concat.apply([],[t]),o=0;o0&&n.push(l)}r=new e.Collection(i,n)}return r},e.heapfn.isHeap=function(){var e,t,i,r,n,a=this._private.heap,o=a.length,s=this._private.comparator;for(e=0;o>e;e+=1)if(t=2*e+1,i=t+1,r=o>t?s(a[t],a[e]):!0,n=o>i?s(a[i],a[e]):!0,!r||!n)return!1;return!0},e.heapfn.heapSwap=function(e,t){var i=this._private.heap,r=this._private.pointers,n=this._private.elements,a=i[e],o=n[e],s=n[e],l=n[t];i[e]=i[t],n[e]=n[t],r[s]=t,r[l]=e,i[t]=a,n[t]=o},e.heapfn.heapify=function(e,t){var i,r,n,a,o,s,l,u=0,c=!1;for("undefined"==typeof t&&(t=!0),i=this._private.heap,u=i.length,s=this._private.comparator,r=e;!c;)t?(n=2*r+1,a=n+1,o=r,u>n&&!s(i[n],i[o])&&(o=n),u>a&&!s(i[a],i[o])&&(o=a),c=o===r,c||(this.heapSwap(o,r),r=o)):(l=Math.floor((r-1)/2),o=r,c=0>l||s(i[o],i[l]),c||(this.heapSwap(o,l),r=l))},e.heapfn.insert=function(e){var t,i,r,n,a,o=this.getArgumentAsCollection(e),s=o.length;for(a=0;s>a;a+=1){if(t=o[a],i=this._private.heap.length,r=this._private.extractor(t),n=t.id(),this._private.pointers.hasOwnProperty(n))throw"ERROR: Multiple items with the same id found: "+n;this._private.heap.push(r),this._private.elements.push(n),this._private.pointers[n]=i,this.heapify(i,!1)}this._private.length=this._private.heap.length},e.heapfn.getValueById=function(e){if(this._private.pointers.hasOwnProperty(e)){var t=this._private.pointers[e];return this._private.heap[t]}},e.heapfn.contains=function(e){for(var t=this.getArgumentAsCollection(e),i=0;i0?{value:this._private.heap[0],id:this._private.elements[0]}:void 0},e.heapfn.pop=function(){if(this._private.length>0){var e,t,i,r=this.top(),n=this._private.length-1;return this.heapSwap(0,n),e=this._private.elements[n],t=this._private.heap[n],i=e,this._private.heap.pop(),this._private.elements.pop(),this._private.length=this._private.heap.length,this._private.pointers[i]=void 0,this.heapify(0),r}},e.heapfn.findDirectionHeapify=function(e){var t=Math.floor((e-1)/2),i=this._private.heap,r=0>t||this._private.comparator(i[e],i[t]);this.heapify(e,r)},e.heapfn.edit=function(t,i){for(var r=this.getArgumentAsCollection(t),n=0;n=o&&s>=e&&t>=l&&u>=t},a=function(e,t,i,r,n){r=-r;var a=e*Math.cos(r)-t*Math.sin(r),o=e*Math.sin(r)+t*Math.cos(r),s=a*i,l=o*i,u=s+n.x,c=l+n.y;return{x:u,y:c}};r.arrow={_points:[-.15,-.3,0,0,.15,-.3],collide:function(t,i,n,a,o,s,l,u){var c=r.arrow._points;return e.math.pointInsidePolygon(t,i,c,n,a,o,s,l,u)},roughCollide:n,draw:function(e,t,i,n){for(var o=r.arrow._points,s=0;se.math.sqDistanceToQuadraticBezier(i,r,c.startX,c.startY,c.cp2ax,c.cp2ay,c.selfEdgeMidX,c.selfEdgeMidY)||(x=e.math.inBezierVicinity(i,r,c.selfEdgeMidX,c.selfEdgeMidY,c.cp2cx,c.cp2cy,c.endX,c.endY,g))&&b()&&g+h>e.math.sqDistanceToQuadraticBezier(i,r,c.selfEdgeMidX,c.selfEdgeMidY,c.cp2cx,c.cp2cy,c.endX,c.endY))&&u.push(o);else if("haystack"===c.edgeType){var w=d["haystack-radius"].value,_=w/2,E=m._private.position,S=m.width(),P=m.height(),D=y._private.position,k=y.width(),C=y.height(),T=D.x+c.source.x*k*_,N=D.y+c.source.y*C*_,M=E.x+c.target.x*S*_,B=E.y+c.target.y*P*_;(x=e.math.inLineVicinity(i,r,T,N,M,B,f))&&b()&&g+h>e.math.sqDistanceToFiniteLine(i,r,T,N,M,B)&&u.push(o)}else"straight"===c.edgeType?(x=e.math.inLineVicinity(i,r,c.startX,c.startY,c.endX,c.endY,f))&&b()&&g+h>e.math.sqDistanceToFiniteLine(i,r,c.startX,c.startY,c.endX,c.endY)&&u.push(o):"bezier"===c.edgeType&&(x=e.math.inBezierVicinity(i,r,c.startX,c.startY,c.cp2x,c.cp2y,c.endX,c.endY,g))&&b()&&g+h>e.math.sqDistanceToQuadraticBezier(i,r,c.startX,c.startY,c.cp2x,c.cp2y,c.endX,c.endY)&&u.push(o);if(x&&b()&&0===u.length||u[u.length-1]!==o){var z=t.arrowShapes[d["source-arrow-shape"].value],I=t.arrowShapes[d["target-arrow-shape"].value],y=y||o._private.source,m=m||o._private.target,E=m._private.position,D=y._private.position,L=s.getArrowWidth(d.width.pxValue),O=s.getArrowHeight(d.width.pxValue),R=L,X=O;(z.roughCollide(i,r,c.arrowStartX,c.arrowStartY,L,O,[c.arrowStartX-D.x,c.arrowStartY-D.y],0)&&z.collide(i,r,c.arrowStartX,c.arrowStartY,L,O,[c.arrowStartX-D.x,c.arrowStartY-D.y],0)||I.roughCollide(i,r,c.arrowEndX,c.arrowEndY,R,X,[c.arrowEndX-E.x,c.arrowEndY-E.y],0)&&I.collide(i,r,c.arrowEndX,c.arrowEndY,R,X,[c.arrowEndX-E.x,c.arrowEndY-E.y],0))&&u.push(o)}p&&u.length>0&&u[u.length-1]===o&&(a(y),a(m))}for(var s=this,l=this.getCachedZSortedEles(),u=[],c=t.isTouch,d=this.data.cy.zoom(),p=this.data.cy.hasCompoundNodes(),h=(c?256:32)/d,v=(c?16:0)/d,g=l.length-1;g>=0;g--){var f=l[g];if(u.length>0)break;"nodes"===f._private.group?a(l[g]):o(l[g])}return u.length>0?u[u.length-1]:null},t.prototype.getAllInBox=function(i,r,n,a){var o=this.getCachedNodes(),s=this.getCachedEdges(),l=[],u=Math.min(i,n),c=Math.max(i,n),d=Math.min(r,a),p=Math.max(r,a);i=u,n=c,r=d,a=p;for(var h,v=0;v=i&&n>=D&&k>=r&&a>=k,M=C>=i&&n>=C&&T>=r&&a>=T;N&&M&&l.push(s[v])}}return l},t.prototype.getNodeWidth=function(e){return e.width()},t.prototype.getNodeHeight=function(e){return e.height()},t.prototype.getNodeShape=function(e){var t=e._private.style.shape.value;return e.isParent()?"rectangle"===t||"roundrectangle"===t?t:"rectangle":t},t.prototype.getNodePadding=function(e){var t=e._private.style["padding-left"].pxValue,i=e._private.style["padding-right"].pxValue,r=e._private.style["padding-top"].pxValue,n=e._private.style["padding-bottom"].pxValue;return isNaN(t)&&(t=0),isNaN(i)&&(i=0),isNaN(r)&&(r=0),isNaN(n)&&(n=0),{left:t,right:i,top:r,bottom:n}},t.prototype.zOrderSort=e.Collection.zIndexSort,t.prototype.updateCachedZSortedEles=function(){this.getCachedZSortedEles(!0)},t.prototype.getCachedZSortedEles=function(e){var t=this.lastZOrderCachedNodes,i=this.lastZOrderCachedEdges,r=this.getCachedNodes(),n=this.getCachedEdges(),a=[];if(!e&&t&&i&&t===r&&i===n)a=this.cachedZSortedEles;else{for(var o=0;op?p+"-"+d:d+"-"+p,c&&(r="unbundled"+l._private.data.id),null==n[r]&&(n[r]=[],a.push(r)),n[r].push(l),c&&(n[r].hasUnbundled=!0)}else o.push(l)}for(var h,v,g,f,y,m,x,b,w,_,E,S,P,D,k=0;kv._private.data.id){var T=h;h=v,v=T}if(g=h._private.position,f=v._private.position,y=this.getNodeWidth(h),m=this.getNodeHeight(h),x=this.getNodeWidth(v),b=this.getNodeHeight(v),w=t.nodeShapes[this.getNodeShape(h)],_=t.nodeShapes[this.getNodeShape(v)],E=h._private.style["border-width"].pxValue,S=v._private.style["border-width"].pxValue,D=!1,C.length>1&&h!==v||C.hasUnbundled){var N=w.intersectLine(g.x,g.y,y,m,f.x,f.y,E/2),M=_.intersectLine(f.x,f.y,x,b,g.x,g.y,S/2),B={x1:N[0],x2:M[0],y1:N[1],y2:M[1]},z=M[1]-N[1],I=M[0]-N[0],L=Math.sqrt(I*I+z*z),O={x:I,y:z},R={x:O.x/L,y:O.y/L};P={x:-R.y,y:R.x},(_.checkPoint(N[0],N[1],S/2,x,b,f.x,f.y)||w.checkPoint(M[0],M[1],E/2,y,m,g.x,g.y))&&(P={},D=!0)}for(var l,X,s=0;sCt,Nt=e.math.distance({x:X.cp2x,y:X.cp2y},{x:X.endX,y:X.endY}),Mt=kt>Nt;if("bezier"===X.edgeType){var Bt=!1;if(wt||_t||Tt){Bt=!0;var zt={x:X.cp2x-g.x,y:X.cp2y-g.y},It=Math.sqrt(zt.x*zt.x+zt.y*zt.y),Lt={x:zt.x/It,y:zt.y/It},Ot=Math.max(y,m),Rt={x:X.cp2x+2*Lt.x*Ot,y:X.cp2y+2*Lt.y*Ot},Xt=w.intersectLine(g.x,g.y,y,m,Rt.x,Rt.y,E/2);Tt?(X.cp2x=X.cp2x+Lt.x*(kt-Ct),X.cp2y=X.cp2y+Lt.y*(kt-Ct)):(X.cp2x=Xt[0]+Lt.x*kt,X.cp2y=Xt[1]+Lt.y*kt)}if(Et||St||Mt){Bt=!0;var zt={x:X.cp2x-f.x,y:X.cp2y-f.y},It=Math.sqrt(zt.x*zt.x+zt.y*zt.y),Lt={x:zt.x/It,y:zt.y/It},Ot=Math.max(y,m),Rt={x:X.cp2x+2*Lt.x*Ot,y:X.cp2y+2*Lt.y*Ot},Vt=_.intersectLine(f.x,f.y,x,b,Rt.x,Rt.y,S/2);Mt?(X.cp2x=X.cp2x+Lt.x*(kt-Nt),X.cp2y=X.cp2y+Lt.y*(kt-Nt)):(X.cp2x=Vt[0]+Lt.x*kt,X.cp2y=Vt[1]+Lt.y*kt)}Bt&&this.findEndpoints(l)}else"straight"===X.edgeType&&(X.midX=(Z+it)/2,X.midY=(G+nt)/2);this.projectBezier(l)}}}for(var s=0;sk*T+C*N)n.straightEdgeTooShort=!0;else{var P=n;this.drawStyledEdge(i,e,[P.startX,P.startY,P.endX,P.endY],_,w),n.straightEdgeTooShort=!1}}else{var P=n;this.drawStyledEdge(i,e,[P.startX,P.startY,P.cp2x,P.cp2y,P.endX,P.endY],_,w)}"haystack"===n.edgeType?this.drawArrowheads(e,i,r):n.noArrowPlacement!==!0&&void 0!==n.startX&&this.drawArrowheads(e,i,r)}}},t.prototype.drawStyledEdge=function(e,i,r,n){var a,o=e._private.rscratch,s=i,l=!1,u=t.usePaths();if(u){for(var c=r,d=o.pathCacheKey&&c.length===o.pathCacheKey.length,p=d,h=0;p&&hu?y+=Math.PI/2:y=-(Math.PI/2+y);var m=this.getArrowWidth(a),x=t.arrowShapes[o];if(p){var b=m+"$"+o+"$"+y+"$"+s+"$"+l;h.arrowPathCacheKey=h.arrowPathCacheKey||{},h.arrowPathCache=h.arrowPathCache||{};var w=h.arrowPathCacheKey[i]===b;w?(d=r=h.arrowPathCache[i],v=!0):(d=r=new Path2D,h.arrowPathCacheKey[i]=b,h.arrowPathCache[i]=d)}r.beginPath&&r.beginPath(),v||x.draw(r,m,y,f),!x.leavePathOpen&&r.closePath&&r.closePath(),r=g,("filled"===n||"both"===n)&&(p?r.fill(d):r.fill()),("hollow"===n||"both"===n)&&(r.lineWidth=x.matchEdgeWidth?a:1,r.lineJoin="miter",p?r.stroke(d):r.stroke())}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.getCachedImage=function(e,t){var i=this,r=i.imageCache=i.imageCache||{};if(r[e]&&r[e].image)return r[e].image;var n=r[e]=r[e]||{},a=n.image=new Image;return a.addEventListener("load",t),a.src=e,a},t.prototype.drawInscribedImage=function(e,i,r){var n=this,a=r._private.position.x,o=r._private.position.y,s=r._private.style,l=s["background-fit"].value,u=s["background-position-x"],c=s["background-position-y"],d=s["background-repeat"].value,p=r.width(),h=r.height(),v=r._private.rscratch,g=s["background-clip"].value,f="node"===g,y=s["background-image-opacity"].value,m=i.width,x=i.height;if(0!==m&&0!==x){if("contain"===l){var b=Math.min(p/m,h/x);m*=b,x*=b}else if("cover"===l){var b=Math.max(p/m,h/x);m*=b,x*=b}var w=a-p/2;w+="%"===u.units?(p-m)*u.value/100:u.pxValue;var _=o-h/2;_+="%"===c.units?(h-x)*c.value/100:c.pxValue,v.pathCache&&(w-=a,_-=o,a=0,o=0);var E=e.globalAlpha;if(e.globalAlpha=y,"no-repeat"===d)f&&(e.save(),v.pathCache?e.clip(v.pathCache):(t.nodeShapes[n.getNodeShape(r)].drawPath(e,a,o,p,h),e.clip())),e.drawImage(i,0,0,i.width,i.height,w,_,m,x),f&&e.restore();else{var S=e.createPattern(i,d);e.fillStyle=S,t.nodeShapes[n.getNodeShape(r)].drawPath(e,a,o,p,h),e.translate(w,_),e.fill(),e.translate(-w,-_)}e.globalAlpha=E}}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.drawEdgeText=function(e,t){var i=t._private.style.content.strValue;if(!(!i||i.match(/^\s+$/)||this.hideEdgesOnViewport&&(this.dragData.didDrag||this.pinching||this.hoverData.dragging||this.data.wheel||this.swipePanning))){var r=t._private.style["font-size"].pxValue*t.cy().zoom(),n=t._private.style["min-zoomed-font-size"].pxValue;if(!(n>r)){e.textAlign="center",e.textBaseline="middle";var a=t._private.rscratch;this.drawText(e,t,a.labelX,a.labelY)}}},t.prototype.drawNodeText=function(e,t){var i=t._private.style.content.strValue;if(i&&!i.match(/^\s+$/)){var r=t._private.style["font-size"].pxValue*t.cy().zoom(),n=t._private.style["min-zoomed-font-size"].pxValue;if(!(n>r)){var a=t._private.style["text-halign"].strValue,o=t._private.style["text-valign"].strValue,s=t._private.rscratch;switch(a){case"left":e.textAlign="right";break;case"right":e.textAlign="left";break;default:e.textAlign="center"}switch(o){case"top":e.textBaseline="bottom";break;case"bottom":e.textBaseline="top";break;default:e.textBaseline="middle"}this.drawText(e,t,s.labelX,s.labelY)}}},t.prototype.getFontCache=function(e){var t;this.fontCaches=this.fontCaches||[];for(var i=0;i0&&(e.lineWidth=s,e.strokeText(o,i,r)),e.fillText(o,i,r)}}}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.drawNode=function(e,i,r){var n,a,o,s=this,l=i._private.style,u=i._private.rscratch,c=t.usePaths(),d=e,p=!1,h=l["overlay-padding"].pxValue,v=l["overlay-opacity"].value,g=l["overlay-color"].value;if(!r||0!==v){var f=i.effectiveOpacity();if(0!==f)if(n=this.getNodeWidth(i),a=this.getNodeHeight(i),e.lineWidth=l["border-width"].pxValue,void 0!==r&&r)v>0&&(this.fillStyle(e,g[0],g[1],g[2],v),t.nodeShapes.roundrectangle.drawPath(e,i._private.position.x,i._private.position.y,n+2*h,a+2*h),e.fill());else{var y=l["background-color"].value,m=l["border-color"].value,x=l["border-style"].value;if(this.fillStyle(e,y[0],y[1],y[2],l["background-opacity"].value*l.opacity.value*f),this.strokeStyle(e,m[0],m[1],m[2],l["border-opacity"].value*l.opacity.value*f),e.lineJoin="miter",e.setLineDash)switch(x){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash([4,2]);break;case"solid":case"double":e.setLineDash([])}var b=l["background-image"].value[2]||l["background-image"].value[1],w=l.shape.strValue,_=i._private.position;if(c){var E=w+"$"+n+"$"+a;e.translate(_.x,_.y),u.pathCacheKey===E?(o=e=u.pathCache,p=!0):(o=e=new Path2D,u.pathCacheKey=E,u.pathCache=o)}if(!p){var S=_;c&&(S={x:0,y:0}),t.nodeShapes[this.getNodeShape(i)].drawPath(e,S.x,S.y,n,a)}if(e=d,c?e.fill(o):e.fill(),void 0!==b){var P=this.getCachedImage(b,function(){s.data.canvasNeedsRedraw[t.NODE]=!0,s.data.canvasNeedsRedraw[t.DRAG]=!0,s.redraw()});P.complete&&this.drawInscribedImage(e,P,i)}var D=l["background-blacken"].value,k=l["border-width"].pxValue;if(this.hasPie(i)&&(this.drawPie(e,i),(0!==D||0!==k)&&(c||t.nodeShapes[this.getNodeShape(i)].drawPath(e,_.x,_.y,n,a))),D>0?(this.fillStyle(e,0,0,0,D),c?e.fill(o):e.fill()):0>D&&(this.fillStyle(e,255,255,255,-D),c?e.fill(o):e.fill()),k>0&&(c?e.stroke(o):e.stroke(),"double"===x)){e.lineWidth=l["border-width"].pxValue/3;var C=e.globalCompositeOperation;e.globalCompositeOperation="destination-out",c?e.stroke(o):e.stroke(),e.globalCompositeOperation=C}c&&e.translate(-_.x,-_.y),e.setLineDash&&e.setLineDash([])}}},t.prototype.hasPie=function(e){return e=e[0],e._private.hasPie},t.prototype.drawPie=function(i,r){r=r[0];var n=r._private.style["pie-size"],a=this.getNodeWidth(r),o=this.getNodeHeight(r),s=r._private.position.x,l=r._private.position.y,u=Math.min(a,o)/2,c=0,d=t.usePaths();d&&(s=0,l=0),"%"===n.units?u=u*n.value/100:void 0!==n.pxValue&&(u=n.pxValue/2);for(var p=1;p<=e.style.pieBackgroundN;p++){var h=r._private.style["pie-"+p+"-background-size"].value,v=r._private.style["pie-"+p+"-background-color"].value,g=r._private.style["pie-"+p+"-background-opacity"].value,f=h/100,y=1.5*Math.PI+2*Math.PI*c,m=2*Math.PI*f,x=y+m;0===h||c>=1||c+f>1||(i.beginPath(),i.moveTo(s,l),i.arc(s,l,u,y,x),i.closePath(),this.fillStyle(i,v[0],v[1],v[2],g),i.fill(),c+=f)}}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.getPixelRatio=function(){var e=this.data.contexts[0];if(null!=this.forcedPixelRatio)return this.forcedPixelRatio;var t=e.backingStorePixelRatio||e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return(window.devicePixelRatio||1)/t},t.prototype.paintCache=function(e){for(var t,i=this.paintCaches=this.paintCaches||[],r=!0,n=0;n=o&&(i=r.bufferCanvases[t.TEXTURE_BUFFER],this.textureMult=2,i.width=s*this.textureMult,i.height=l*this.textureMult),this.canvasWidth=s,this.canvasHeight=l}},t.prototype.renderTo=function(e,t,i,r){this.redraw({forcedContext:e,forcedZoom:t,forcedPan:i,drawAllLayers:!0,forcedPxRatio:r})},t.prototype.timeToRender=function(){return this.redrawTotalTime/this.redrawCount},t.minRedrawLimit=1e3/60,t.maxRedrawLimit=1e3,t.motionBlurDelay=100,t.prototype.redraw=function(i){function r(){function i(e,t){if(e.setTransform(1,0,0,1,0,0),"motionBlur"===t){var i=e.globalCompositeOperation;e.globalCompositeOperation="destination-out",u.fillStyle(e,255,255,255,.666),e.fillRect(0,0,u.canvasWidth,u.canvasHeight),e.globalCompositeOperation=i}else n||void 0!==t&&!t||e.clearRect(0,0,u.canvasWidth,u.canvasHeight);a||(e.translate(b.x,b.y),e.scale(m,m)),l&&e.translate(l.x,l.y),s&&e.scale(s,s)}function r(e,t){for(var i=e.eles,r=0;r0&&(k.strokeStyle="rgba("+f["selection-box-border-color"].value[0]+","+f["selection-box-border-color"].value[1]+","+f["selection-box-border-color"].value[2]+","+f["selection-box-opacity"].value+")",k.strokeRect(p.select[0],p.select[1],p.select[2]-p.select[0],p.select[3]-p.select[1]))}if(p.bgActivePosistion&&!u.hoverData.selecting){var y=p.cy.zoom(),q=p.bgActivePosistion;k.fillStyle="rgba("+f["active-bg-color"].value[0]+","+f["active-bg-color"].value[1]+","+f["active-bg-color"].value[2]+","+f["active-bg-opacity"].value+")",k.beginPath(),k.arc(q.x,q.y,f["active-bg-size"].pxValue/y,0,2*Math.PI),k.fill()}var F=u.averageRedrawTime;if(u.showFps&&F){F=Math.round(F);var j=Math.round(1e3/F);k.setTransform(1,0,0,1,0,0),k.fillStyle="rgba(255, 0, 0, 0.75)",k.strokeStyle="rgba(255, 0, 0, 0.75)",k.lineWidth=1,k.fillText("1 frame = "+F+" ms = "+j+" fps",0,20);var W=60;k.strokeRect(0,30,250,20),k.fillRect(0,30,250*Math.min(j/W,1),20)}a||(h[t.SELECT_BOX]=!1)}var H=+new Date;void 0===u.averageRedrawTime&&(u.averageRedrawTime=H-w),void 0===u.redrawCount&&(u.redrawCount=0),u.redrawCount++,void 0===u.redrawTotalTime&&(u.redrawTotalTime=0),u.redrawTotalTime+=H-w,u.lastRedrawTime=H-w,u.averageRedrawTime=u.averageRedrawTime/2+(H-w)/2,u.currentlyDrawing=!1,u.clearingMotionBlur&&(u.clearingMotionBlur=!1,u.motionBlurCleared=!0,u.motionBlur=!0),v&&(u.motionBlurTimeout=setTimeout(function(){u.motionBlurTimeout=null,u.clearedNodeLayerForMotionBlur=!1,u.motionBlur=!1,u.clearingMotionBlur=!0,h[t.NODE]=!0,h[t.DRAG]=!0,u.redraw()},t.motionBlurDelay))}i=i||{};var n=i.forcedContext,a=i.drawAllLayers,o=i.drawOnlyNodeLayer,s=i.forcedZoom,l=i.forcedPan,u=this,c=void 0===i.forcedPxRatio?this.getPixelRatio():i.forcedPxRatio,d=u.data.cy,p=u.data,h=p.canvasNeedsRedraw,v=void 0!==i.motionBlur?i.motionBlur:u.motionBlur;v=v&&!n&&u.motionBlurEnabled,v&&u.motionBlurTimeout&&clearTimeout(u.motionBlurTimeout),!n&&this.redrawTimeout&&clearTimeout(this.redrawTimeout),this.redrawTimeout=null,void 0===this.averageRedrawTime&&(this.averageRedrawTime=0);var g=t.minRedrawLimit,f=t.maxRedrawLimit,y=this.averageRedrawTime;y=g>y?g:y,y=f>y?y:f,void 0===this.lastDrawTime&&(this.lastDrawTime=0);var m=+new Date,x=m-this.lastDrawTime,b=x>=y;if(!n){if(!b||this.currentlyDrawing)return void(this.redrawTimeout=setTimeout(function(){u.redraw()},y));this.lastDrawTime=m,this.currentlyDrawing=!0}var w=+new Date;n?r():e.util.requestAnimationFrame(r),n||u.initrender||(u.initrender=!0,d.trigger("initrender"))}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.drawPolygonPath=function(e,t,i,r,n,a){var o=r/2,s=n/2;e.beginPath&&e.beginPath(),e.moveTo(t+o*a[0],i+s*a[1]);for(var l=1;l0&&a>0)if(l.clearRect(0,0,n,a),e.bg&&(l.fillStyle=e.bg,l.rect(0,0,n,a),l.fill()),l.globalCompositeOperation="source-over",e.full)this.redraw({forcedContext:l,drawAllLayers:!0,forcedZoom:o,forcedPan:{x:-r.x1*o,y:-r.y1*o},forcedPxRatio:1});else{var u=i.pan(),c={x:u.x*o,y:u.y*o},d=i.zoom()*o;this.redraw({forcedContext:l,drawAllLayers:!0,forcedZoom:d,forcedPan:c,forcedPxRatio:1})}return s},t.prototype.png=function(e){return this.bufferCanvasImage(e).toDataURL("image/png")}}(cytoscape),function(e){"use strict";var t=e("renderer","canvas");t.prototype.registerBinding=function(e,t,i,r){this.bindings.push({target:e,event:t,handler:i,useCapture:r}),e.addEventListener(t,i,r)},t.prototype.nodeIsDraggable=function(e){return 0!==e._private.style.opacity.value&&"visible"==e._private.style.visibility.value&&"element"==e._private.style.display.value&&!e.locked()&&e.grabbable()?!0:!1},t.prototype.load=function(){var i=this,r=function(e){var t;if(e.addToList&&i.data.cy.hasCompoundNodes()){if(!e.addToList.hasId){e.addToList.hasId={};for(var r=0;rs[0]&&r.clientXs[1]&&r.clientY=t.panOrBoxSelectDelay)&&!i.hoverData.selecting&&D>=i.tapThreshold2&&p.panningEnabled()&&p.userPanningEnabled())i.hoverData.dragging=!0,i.hoverData.selecting=!1,i.hoverData.justStartedPan=!0,g[4]=0;else{if(p.boxSelectionEnabled()&&Math.pow(g[2]-g[0],2)+Math.pow(g[3]-g[1],2)>7&&g[4]&&(clearTimeout(i.bgActiveTimeout),i.data.bgActivePosistion=void 0,i.hoverData.selecting=!0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,i.redraw()),m&&m.isEdge()&&m.active()&&m.unactivate(),f!=y&&(y&&(y.trigger(new e.Event(r,{type:"mouseout",cyPosition:{x:v[0],y:v[1]}})),y.trigger(new e.Event(r,{type:"tapdragout",cyPosition:{x:v[0],y:v[1]}}))),f&&(f.trigger(new e.Event(r,{type:"mouseover",cyPosition:{x:v[0],y:v[1]}})),f.trigger(new e.Event(r,{type:"tapdragover",cyPosition:{x:v[0],y:v[1]}}))),i.hoverData.last=f),m&&m.isNode()&&i.nodeIsDraggable(m))if(D>=i.tapThreshold2){var M=!i.dragData.didDrag;M&&(i.data.canvasNeedsRedraw[t.NODE]=!0),i.dragData.didDrag=!0;for(var B=[],z=0;z7&&l[4]||i.hoverData.dragging||(a.$(function(){return this.selected()}).unselect(),c.length>0&&(i.data.canvasNeedsRedraw[t.NODE]=!0),i.dragData.possibleDragElements=c=[]),null!=u?u.trigger(new e.Event(r,{type:"mouseup",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"tapend",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"vmouseup",cyPosition:{x:s[0],y:s[1]}})):null==u&&a.trigger(new e.Event(r,{type:"mouseup",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"tapend",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"vmouseup",cyPosition:{x:s[0],y:s[1]}})),Math.pow(l[2]-l[0],2)+Math.pow(l[3]-l[1],2)===0&&(null!=u?u.trigger(new e.Event(r,{type:"click",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"tap",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"vclick",cyPosition:{x:s[0],y:s[1]}})):null==u&&a.trigger(new e.Event(r,{type:"click",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"tap",cyPosition:{x:s[0],y:s[1]}})).trigger(new e.Event(r,{type:"vclick",cyPosition:{x:s[0],y:s[1]}}))),u!=d||i.dragData.didDrag||null!=u&&u._private.selectable&&(i.hoverData.dragging||("additive"===a.selectionType()||p?u.selected()?u.unselect():u.select():p||(a.$(":selected").not(u).unselect(),u.select())),i.data.canvasNeedsRedraw[t.NODE]=!0),a.boxSelectionEnabled()&&Math.pow(l[2]-l[0],2)+Math.pow(l[3]-l[1],2)>7&&l[4]){var g=[],f=i.getAllInBox(l[0],l[1],l[2],l[3]);i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,f.length>0&&(i.data.canvasNeedsRedraw[t.NODE]=!0);for(var y=0;y=0&&w>=d&&h>=0&&w>=h&&p>=0&&_>=p&&v>=0&&_>=v;var C=n.pan(),T=n.zoom();g=S(d,p,h,v),f=P(d,p,h,v),y=[(d+h)/2,(p+v)/2],m=[(y[0]-C.x)/T,(y[1]-C.y)/T];var N=200,M=N*N;if(M>f&&!r.touches[2]){var B=i.findNearestElement(l[0],l[1],!0),z=i.findNearestElement(l[2],l[3],!0);return B&&B.isNode()?(B.activate().trigger(new e.Event(r,{type:"cxttapstart",cyPosition:{x:l[0],y:l[1]}})),i.touchData.start=B):z&&z.isNode()?(z.activate().trigger(new e.Event(r,{type:"cxttapstart",cyPosition:{x:l[0],y:l[1]}})),i.touchData.start=z):(n.trigger(new e.Event(r,{type:"cxttapstart",cyPosition:{x:l[0],y:l[1]}})),i.touchData.start=null),i.touchData.start&&(i.touchData.start._private.grabbed=!1),i.touchData.cxt=!0,i.touchData.cxtDragged=!1,i.data.bgActivePosistion=void 0,void i.redraw()}}if(r.touches[2]);else if(r.touches[1]);else if(r.touches[0]){var I=i.findNearestElement(l[0],l[1],!0);if(null!=I){if(I.activate(),i.touchData.start=I,I.isNode()&&i.nodeIsDraggable(I)){var L=i.dragData.touchDragEles=[];if(i.data.canvasNeedsRedraw[t.NODE]=!0,i.data.canvasNeedsRedraw[t.DRAG]=!0,I.selected())for(var O=n.$(function(){return this.isNode()&&this.selected()}),R=0;R=q||R>=Y){i.touchData.cxt=!1,i.touchData.start&&(i.touchData.start.unactivate(),i.touchData.start=null),i.data.bgActivePosistion=void 0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0;var F=new e.Event(r,{type:"cxttapend",cyPosition:{x:l[0],y:l[1]}});i.touchData.start?i.touchData.start.trigger(F):s.trigger(F)}}if(o&&i.touchData.cxt){var F=new e.Event(r,{type:"cxtdrag",cyPosition:{x:l[0],y:l[1]}});i.data.bgActivePosistion=void 0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,i.touchData.start?i.touchData.start.trigger(F):s.trigger(F),i.touchData.start&&(i.touchData.start._private.grabbed=!1),i.touchData.cxtDragged=!0;var j=i.findNearestElement(l[0],l[1],!0);i.touchData.cxtOver&&j===i.touchData.cxtOver||(i.touchData.cxtOver&&i.touchData.cxtOver.trigger(new e.Event(r,{type:"cxtdragout",cyPosition:{x:l[0],y:l[1]}})),i.touchData.cxtOver=j,j&&j.trigger(new e.Event(r,{type:"cxtdragover",cyPosition:{x:l[0],y:l[1]}})))}else if(o&&r.touches[2]&&s.boxSelectionEnabled())i.data.bgActivePosistion=void 0,clearTimeout(this.threeFingerSelectTimeout),this.lastThreeTouch=+new Date,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,n&&0!==n.length&&void 0!==n[0]?(n[2]=(l[0]+l[2]+l[4])/3,n[3]=(l[1]+l[3]+l[5])/3):(n[0]=(l[0]+l[2]+l[4])/3,n[1]=(l[1]+l[3]+l[5])/3,n[2]=(l[0]+l[2]+l[4])/3+1,n[3]=(l[1]+l[3]+l[5])/3+1),n[4]=1,i.redraw();else if(o&&r.touches[1]&&s.zoomingEnabled()&&s.panningEnabled()&&s.userZoomingEnabled()&&s.userPanningEnabled()){i.data.bgActivePosistion=void 0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0;var W=i.dragData.touchDragEles;if(W){i.data.canvasNeedsRedraw[t.DRAG]=!0;for(var H=0;H=i.tapThreshold2){for(var W=i.dragData.touchDragEles,ut=0;ut4&&(i.touchData.singleTouchMoved=!0);if(o&&(null==st||st.isEdge())&&s.panningEnabled()&&s.userPanningEnabled()){i.swipePanning?s.panBy({x:w[0]*c,y:w[1]*c}):B>=i.tapThreshold2&&(i.swipePanning=!0,s.panBy({x:k*c,y:T*c})),st&&(st.unactivate(),i.data.bgActivePosistion||(i.data.bgActivePosistion={x:l[0],y:l[1]}),i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,i.touchData.start=null);var y=i.projectIntoViewport(r.touches[0].clientX,r.touches[0].clientY);l[0]=y[0],l[1]=y[1]}}for(var _=0;_0?i.data.canvasNeedsRedraw[t.NODE]=!0:i.redraw()}var x=!1;if(null!=n&&(n._private.active=!1,x=!0,n.unactivate()),r.touches[2])i.data.bgActivePosistion=void 0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0;else if(r.touches[1]);else if(r.touches[0]);else if(!r.touches[0]){if(i.data.bgActivePosistion=void 0,i.data.canvasNeedsRedraw[t.SELECT_BOX]=!0,null!=n){n._private.grabbed&&(n._private.grabbed=!1,n.trigger("free"),n._private.rscratch.inDragLayer=!1);for(var b=n._private.edges,w=0;wc;c++)d[4*c]=p[2*c],d[4*c+1]=p[2*c+1],d[4*c+2]=h[2*c],d[4*c+3]=h[2*c+1];d=e.math.fitPolygonToSquare(d),n.star5=n.star={points:d,draw:function(e,t,r,a,o){i.drawPolygon(e,t,r,a,o,n.star5.points)},drawPath:function(e,t,r,a,o){i.drawPolygonPath(e,t,r,a,o,n.star5.points)},intersectLine:function(e,t,r,a,o,s,l){return i.polygonIntersectLine(o,s,n.star5.points,e,t,r/2,a/2,l)},intersectBox:function(e,t,r,a,o,s,l,u,c){var d=n.star5.points;return i.boxIntersectPolygon(e,t,r,a,d,o,s,l,u,[0,-1],c)},checkPoint:function(t,i,r,a,o,s,l){return e.math.pointInsidePolygon(t,i,n.star5.points,s,l,a,o,[0,-1],r)}}}(cytoscape),function(e){"use strict";function t(t){this._private={},this._private.options=e.util.extend({},i,t)}var i={animate:!0,maxSimulationTime:4e3,fit:!0,padding:30,boundingBox:void 0,ungrabifyWhileSimulating:!1,ready:void 0,stop:void 0,repulsion:void 0,stiffness:void 0,friction:void 0,gravity:!0,fps:void 0,precision:void 0,nodeMass:void 0,edgeLength:void 0,stepSize:.1,stableEnergy:function(e){var t=e;return t.max<=.5||t.mean<=.3},infinite:!1};t.prototype.run=function(){var t=this,i=this._private.options;return e.util.require("arbor",function(r){function n(e,t){return null==t?void 0:"function"==typeof t?t.apply(e,[e._private.data,{nodes:u.length,edges:c.length,element:e}]):t}function a(e){if(!e.isFullAutoParent()){var t=e._private.data.id,r=n(e,i.nodeMass),a=e._private.locked,o=e.position(),s=h.fromScreen({x:o.x,y:o.y});e.scratch().arbor=h.addNode(t,{element:e,mass:r,fixed:a,x:a?s.x:void 0,y:a?s.y:void 0})}}function o(e){var t=e.source().id(),r=e.target().id(),a=n(e,i.edgeLength);e.scratch().arbor=h.addEdge(t,r,{length:a})}var s=i.cy,l=i.eles,u=l.nodes().not(":parent"),c=l.edges(),d=e.util.makeBoundingBox(i.boundingBox?i.boundingBox:{x1:0,y1:0,w:s.width(),h:s.height()}),p=!1;if(t.trigger({type:"layoutstart",layout:t}),void 0!==i.liveUpdate&&(i.animate=i.liveUpdate),s.nodes().size()<=1)return i.fit&&s.reset(),s.nodes().position({x:Math.round((d.x1+d.x2)/2),y:Math.round((d.y1+d.y2)/2)}),t.one("layoutready",i.ready),t.trigger({type:"layoutready",layout:t}),t.one("layoutstop",i.stop),void t.trigger({type:"layoutstop",layout:t});var h=t._private.system=r.ParticleSystem();h.parameters({repulsion:i.repulsion,stiffness:i.stiffness,friction:i.friction,gravity:i.gravity,fps:i.fps,dt:i.dt,precision:i.precision}),i.animate&&i.fit&&s.fit(d,i.padding);var v,g=250,f=!1,y=+new Date,m={init:function(){},redraw:function(){var e=h.energy();if(!i.infinite&&null!=i.stableEnergy&&null!=e&&e.n>0&&i.stableEnergy(e))return void t.stop();i.infinite||1/0==g||(clearTimeout(v),v=setTimeout(P,g));var r=s.collection();h.eachNode(function(e,t){var i=e.data,n=i.element;null!=n&&(n.locked()||n.grabbed()||(n.silentPosition({x:d.x1+t.x,y:d.y1+t.y}),r.merge(n)))}),i.animate&&r.length>0&&(p=!0,r.rtrigger("position"),i.fit&&s.fit(i.padding),y=+new Date,p=!1),f||(f=!0,t.one("layoutready",i.ready),t.trigger({type:"layoutready",layout:t}))}};h.renderer=m,h.screenSize(d.w,d.h),h.screenPadding(i.padding,i.padding,i.padding,i.padding),h.screenStep(i.stepSize);var x;u.on("grab free position",x=function(e){if(!p){var t=this.position(),n=h.fromScreen(t);if(n){var a=r.Point(n.x,n.y),o=i.padding;switch(d.x1+o<=t.x&&t.x<=d.x2-o&&d.y1+o<=t.y&&t.y<=d.y2-o&&(this.scratch().arbor.p=a),e.type){case"grab":this.scratch().arbor.fixed=!0;break;case"free":this.scratch().arbor.fixed=!1}}}});var b;u.on("lock unlock",b=function(){node.scratch().arbor.fixed=node.locked()});var w;l.on("remove",w=function(){});var _;s.on("add","*",_=function(){});var E;s.on("resize",E=function(){if(null==i.boundingBox&&null!=t._private.system){var e=s.width(),r=s.height();h.screenSize(e,r)}}),u.each(function(e,t){a(t)}),c.each(function(e,t){o(t)});var S=u.filter(":grabbable");i.ungrabifyWhileSimulating&&S.ungrabify();var P=t._private.doneHandler=function(){t._private.doneHandler=null,i.animate||(i.fit&&s.reset(),u.rtrigger("position")),u.off("grab free position",x),u.off("lock unlock",b),l.off("remove",w),s.off("add","*",_),s.off("resize",E),i.ungrabifyWhileSimulating&&S.grabify(),t.one("layoutstop",i.stop),t.trigger({type:"layoutstop",layout:t})};h.start(),!i.infinite&&null!=i.maxSimulationTime&&i.maxSimulationTime>0&&1/0!==i.maxSimulationTime&&setTimeout(function(){t.stop()},i.maxSimulationTime)}),this},t.prototype.stop=function(){return null!=this._private.system&&this._private.system.stop(),this._private.doneHandler&&this._private.doneHandler(),this},e("layout","arbor",t)}(cytoscape),function(e){"use strict";function t(t){this.options=e.util.extend({},i,t)}var i={fit:!0,directed:!1,padding:30,circle:!1,boundingBox:void 0,avoidOverlap:!0,roots:void 0,maximalAdjustments:0,animate:!1,animationDuration:500,ready:void 0,stop:void 0};t.prototype.run=function(){var t,i=this.options,r=i,n=i.cy,a=r.eles,o=a.nodes().not(":parent"),s=a,l=e.util.makeBoundingBox(r.boundingBox?r.boundingBox:{x1:0,y1:0,w:n.width(),h:n.height()});if(e.is.elementOrCollection(r.roots))t=r.roots;else if(e.is.array(r.roots)){for(var u=[],c=0;c0;){var g=n.collection();a.bfs({roots:v[0],visit:function(e,t,i){g=g.add(i)},directed:!1}),v=v.not(g),h.push(g)}t=n.collection();for(var c=0;ck;){for(var C=P.shift(),T=C.neighborhood().nodes(),N=!1,c=0;cc;c++)for(var M=x[c],R=M.length,X=0;R>X;X++){var p=M[X],V=p._private.scratch.breadthfirst,Y=z(p);Y&&(V.intEle=Y,O.push(p))}for(var c=0;cx.length-1;)x.push([]);x[q].push(p),V.depth=q,V.index=x[q].length-1}B()}var F=0;if(r.avoidOverlap){for(var c=0;cl||0===t)&&(r+=s/u,n++)}return n=Math.max(1,n),r/=n,0===n&&(r=void 0),H[e.id()]=r,r},Z=function(e,t){var i=$(e),r=$(t);return i-r},U=0;3>U;U++){for(var c=0;c0&&x[0].length<=3?c/2:0),f=2*Math.PI/x[n].length*a;return 0===n&&1===x[0].length&&(g=1),{x:K.x+g*Math.cos(f),y:K.y+g*Math.sin(f)}}return{x:K.x+(a+1-(o+1)/2)*s,y:(n+1)*u}},Q={},c=x.length-1;c>=0;c--)for(var M=x[c],X=0;X1&&r.avoidOverlap){d*=1.75;var c=2*Math.PI/o.length,g=Math.cos(c)-Math.cos(0),f=Math.sin(c)-Math.sin(0),y=Math.sqrt(d*d/(g*g+f*f));t=Math.max(y,t)}var m=function(){var e=t*Math.cos(u),i=t*Math.sin(u),n={x:l.x+e,y:l.y+i};return u=r.counterclockwise?u-c:u+c,n};return o.layoutPositions(this,r,m),this},e("layout","circle",t)}(cytoscape),function(e){"use strict";function t(t){this.options=e.util.extend(!0,{},i,t)}var i={animate:!0,refresh:1,maxSimulationTime:4e3,ungrabifyWhileSimulating:!1,fit:!0,padding:30,boundingBox:void 0,ready:function(){},stop:function(){},randomize:!1,avoidOverlap:!0,handleDisconnected:!0,nodeSpacing:function(){return 10},flow:void 0,alignment:void 0,edgeLength:void 0,edgeSymDiffLength:void 0,edgeJaccardLength:void 0,unconstrIter:void 0,userConstIter:void 0,allConstIter:void 0,infinite:!1};t.prototype.run=function(){var t=this,i=this.options;return e.util.require("cola",function(r){var n=i.cy,a=i.eles,o=a.nodes(),s=a.edges(),l=!1,u=e.util.makeBoundingBox(i.boundingBox?i.boundingBox:{x1:0,y1:0,w:n.width(),h:n.height()}),c=function(t,i){if(e.is.fn(t)){var r=t;return r.apply(i,[i])}return t},d=function(){for(var e={min:1/0,max:-1/0},t={min:1/0,max:-1/0},r=0;ri&&!e;i++)e=e||n();return e};if(i.animate){var o=function(){a()||e.util.requestAnimationFrame(o)};e.util.requestAnimationFrame(o)}else for(;!n(););},on:function(){},drag:function(){}});t.adaptor=f;var y=o.filter(":grabbable");i.ungrabifyWhileSimulating&&y.ungrabify();var m;o.on("grab free position",m=function(t){var i=this,r=i._private.scratch.cola,n=i._private.position;switch(i.grabbed()?(r.x=n.x-u.x1,r.y=n.y-u.y1,f.dragstart(r)):e.is.number(r.x)&&e.is.number(r.y)&&(n.x=r.x+u.x1,n.y=r.y+u.y1),t.type){case"grab":f.dragstart(r),f.resume();break;case"free":f.dragend(r)}});var x;o.on("lock unlock",x=function(){var e=this,t=e._private.scratch.cola;e.locked()?f.dragstart(t):f.dragend(t)});var b=o.stdFilter(function(e){return!e.isParent()});if(f.nodes(b.map(function(e,t){var r=c(i.nodeSpacing,e),n=e.position(),a=e._private.scratch.cola={x:i.randomize?Math.round(Math.random()*u.w):n.x,y:i.randomize?Math.round(Math.random()*u.h):n.y,width:e.outerWidth()+2*r,height:e.outerHeight()+2*r,index:t};return a})),i.alignment){var w=[],_=[];b.forEach(function(e){var t=c(i.alignment,e),r=e._private.scratch.cola,n=r.index;t&&(null!=t.x&&w.push({node:n,offset:t.x}),null!=t.y&&_.push({node:n,offset:t.y}))});var E=[];w.length>0&&E.push({type:"alignment",axis:"x",offsets:w}),_.length>0&&E.push({type:"alignment",axis:"y",offsets:_}),f.constraints(E)}f.groups(o.stdFilter(function(e){return e.isParent()}).map(function(e,t){return e._private.scratch.cola={index:t,leaves:e.children().stdFilter(function(e){return!e.isParent()}).map(function(e){return e[0]._private.scratch.cola.index})},e}).map(function(e){return e._private.scratch.cola.groups=e.children().stdFilter(function(e){return e.isParent()}).map(function(e){return e._private.scratch.cola.index}),e._private.scratch.cola}));var S,P;null!=i.edgeLength?(S=i.edgeLength,P="linkDistance"):null!=i.edgeSymDiffLength?(S=i.edgeSymDiffLength,P="symmetricDiffLinkLengths"):null!=i.edgeJaccardLength?(S=i.edgeJaccardLength,P="jaccardLinkLengths"):(S=100,P="linkDistance");var D=function(e){return e.calcLength};if(f.links(s.stdFilter(function(e){return!e.source().isParent()&&!e.target().isParent()}).map(function(e){var t=e._private.scratch.cola={source:e.source()[0]._private.scratch.cola.index,target:e.target()[0]._private.scratch.cola.index};return null!=S&&(t.calcLength=c(S,e)),t})),f.size([u.w,u.h]),null!=S&&f[P](D),i.flow){var k,C="y",T=50;e.is.string(i.flow)?k={axis:i.flow,minSeparation:T}:e.is.number(i.flow)?k={axis:C,minSeparation:i.flow}:e.is.plainObject(i.flow)?(k=i.flow,k.axis=k.axis||C,k.minSeparation=null!=k.minSeparation?k.minSeparation:T):k={axis:C,minSeparation:T},f.flowLayout(k.axis,k.minSeparation)}f.avoidOverlaps(i.avoidOverlap).handleDisconnected(i.handleDisconnected).start(i.unconstrIter,i.userConstIter,i.allConstIter),t.trigger({type:"layoutstart",layout:t}),i.infinite||setTimeout(function(){f.stop()},i.maxSimulationTime)}),this},t.prototype.stop=function(){return this.adaptor&&(this.manuallyStopped=!0,this.adaptor.stop()),this},e("layout","cola",t)}(cytoscape),function(e){"use strict";function t(t){this.options=e.util.extend({},i,t)}var i={fit:!0,padding:30,startAngle:1.5*Math.PI,counterclockwise:!1,minNodeSpacing:10,boundingBox:void 0,avoidOverlap:!0,height:void 0,width:void 0,concentric:function(){return this.degree()},levelWidth:function(e){return e.maxDegree()/4},animate:!1,animationDuration:500,ready:void 0,stop:void 0};t.prototype.run=function(){for(var t=this.options,i=t,r=t.cy,n=i.eles,a=n.nodes().not(":parent"),o=e.util.makeBoundingBox(i.boundingBox?i.boundingBox:{x1:0,y1:0,w:r.width(),h:r.height()}),s={x:o.x1+o.w/2,y:o.y1+o.h/2},l=[],u=i.startAngle,c=0,d=0;d0){var m=Math.abs(f[0].value-y.value);m>=v&&(f=[],g.push(f))}f.push(y)}var x={},b=0,w=c+i.minNodeSpacing;if(!i.avoidOverlap){var _=g.length>0&&g[0].length>1,E=Math.min(o.w,o.h)/2-w,S=E/(g.length+_?1:0);w=Math.min(w,S)}for(var d=0;d1&&i.avoidOverlap){var k=Math.cos(D)-Math.cos(0),C=Math.sin(D)-Math.sin(0),T=Math.sqrt(w*w/(k*k+C*C));b=Math.max(T,b)}for(var N=0;N=d;){var f=c[d++],y=s.idToIndex[f],m=s.layoutNodes[y],x=m.children;if(x.length>0){s.graphSet.push(x);for(var l=0;lr.count?0:r.graph},o=function(e,t,i,r){var n=r.graphSet[i];if(-1<$.inArray(e,n)&&-1<$.inArray(t,n))return{count:2,graph:i};for(var a=0,s=0;ss;s++)for(var l=e.layoutNodes[e.idToIndex[a[s]]],u=s+1;o>u;u++){var c=e.layoutNodes[e.idToIndex[a[u]]];p(l,c,e,t,i)}}},p=function(e,t,i,r,n){var a="Node repulsion. Node1: "+e.id+" Node2: "+t.id,o=t.positionX-e.positionX,s=t.positionY-e.positionY;if(a+="\ndirectionX: "+o+", directionY: "+s,0===o&&0===s)return void(a+="\nNodes have the same position.");var l=v(e,t,o,s);if(l>0){a+="\nNodes DO overlap.",a+="\nOverlap: "+l;var u=n.nodeOverlap*l,c=Math.sqrt(o*o+s*s);a+="\nDistance: "+c;var d=u*o/c,p=u*s/c}else{a+="\nNodes do NOT overlap.";var g=h(e,o,s),f=h(t,-1*o,-1*s),y=f.x-g.x,m=f.y-g.y,x=y*y+m*m,c=Math.sqrt(x);a+="\nDistance: "+c;var u=n.nodeRepulsion/x,d=u*y/c,p=u*m/c}e.offsetX-=d,e.offsetY-=p,t.offsetX+=d,t.offsetY+=p,a+="\nForceX: "+d+" ForceY: "+p,w(a)},h=function(e,t,i){var r=e.positionX,n=e.positionY,a=e.height,o=e.width,s=i/t,l=a/o,u="Computing clipping point of node "+e.id+" . Height: "+a+", Width: "+o+"\nDirection "+t+", "+i,c={};do{if(0===t&&i>0){c.x=r,u+="\nUp direction",c.y=n+a/2;break}if(0===t&&0>i){c.x=r,c.y=n+a/2,u+="\nDown direction";break}if(t>0&&s>=-1*l&&l>=s){c.x=r+o/2,c.y=n+o*i/2/t,u+="\nRightborder";break}if(0>t&&s>=-1*l&&l>=s){c.x=r-o/2,c.y=n-o*i/2/t,u+="\nLeftborder";break}if(i>0&&(-1*l>=s||s>=l)){c.x=r+a*t/2/i,c.y=n+a/2,u+="\nTop border";break}if(0>i&&(-1*l>=s||s>=l)){c.x=r-a*t/2/i,c.y=n-a/2,u+="\nBottom border";break}}while(!1);return u+="\nClipping point found at "+c.x+", "+c.y,w(u),c},v=function(e,t,i,r){if(i>0)var n=e.maxX-t.minX;else var n=t.maxX-e.minX;if(r>0)var a=e.maxY-t.minY;else var a=t.maxY-e.minY;return n>=0&&a>=0?Math.sqrt(n*n+a*a):0},g=function(e,t,i){for(var r=0;rd;d++){var p=e.layoutNodes[e.idToIndex[a[d]]];r="Node: "+p.id;var h=s-p.positionX,v=l-p.positionY,g=Math.sqrt(h*h+v*v);if(g>1){var f=i.gravity*h/g,y=i.gravity*v/g;p.offsetX+=f,p.offsetY+=y,r+=": Applied force: "+f+", "+y}else r+=": skypped since it's too close to center";w(r)}}},y=function(e){var t=[],i=0,r=-1;for(w("propagateForces"),t.push.apply(t,e.graphSet[0]),r+=e.graphSet[0].length;r>=i;){var n=t[i++],a=e.idToIndex[n],o=e.layoutNodes[a],s=o.children;if(0i)var a={x:i*e/n,y:i*t/n};else var a={x:e,y:t};return r+=".\nResult: ("+a.x+", "+a.y+")",w(r),a},b=function(e,t){var i="Propagating new position/size of node "+e.id,r=e.parentId;if(null==r)return i+=". No parent node.",void w(i);var n=t.layoutNodes[t.idToIndex[r]],a=!1;return(null==n.maxX||e.maxX+n.padRight>n.maxX)&&(n.maxX=e.maxX+n.padRight,a=!0,i+="\nNew maxX for parent node "+n.id+": "+n.maxX),(null==n.minX||e.minX-n.padLeftn.maxY)&&(n.maxY=e.maxY+n.padBottom,a=!0,i+="\nNew maxY for parent node "+n.id+": "+n.maxY),(null==n.minY||e.minY-n.padTops){var h=d(),v=p();(h-1)*v>=s?d(h-1):(v-1)*h>=s&&p(v-1)}else for(;s>c*u;){var h=d(),v=p();(v+1)*h>=s?p(v+1):d(h+1)}var g=o.w/c,f=o.h/u;if(i.avoidOverlap)for(var y=0;y=c&&(P=0,S++)},k={},y=0;y0&&r.animate&&(s=!0,m.rtrigger("position"),r.fit&&l.fit(r.padding),m=l.collection(),s=!1)},function(){},function(e,i){var n=f(i),a=e.data.element;a.locked()||a.grabbed()||(a._private.position={x:n.x,y:n.y},m.merge(a)),b==x&&(t.one("layoutready",r.ready),t.trigger({type:"layoutready",layout:t})),b++});c.each(function(e,t){r.random||a(t)});var _;c.on("position",_=function(){s||a(this)});var E=c.filter(":grabbable");i.stopSystem=function(){h.filterNodes(function(){return!1}),r.ungrabifyWhileSimulating&&E.grabify(),r.fit&&l.fit(r.padding),c.off("drag position",_),t.one("layoutstop",r.stop),t.trigger({type:"layoutstop",layout:t}),i.stopSystem=null},o(),r.infinite||setTimeout(function(){i.stop()},r.maxSimulationTime)}),this},t.prototype.stop=function(){return null!=this.stopSystem&&this.stopSystem(),this},e("layout","springy",t)}(cytoscape),function(e){"use strict";function t(e){this.options=e}t.prototype.recalculateRenderedStyle=function(){},t.prototype.notify=function(){},e("renderer","null",t)}(cytoscape); \ No newline at end of file diff --git a/examples/topoViz.html b/examples/topoViz.html new file mode 100644 index 00000000..ab13a211 --- /dev/null +++ b/examples/topoViz.html @@ -0,0 +1,42 @@ + + + + TopoViz + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/examples/topoViz.js b/examples/topoViz.js new file mode 100644 index 00000000..ed213272 --- /dev/null +++ b/examples/topoViz.js @@ -0,0 +1,78 @@ +$(function(){ // on dom ready + + var pts = [[0,0,0], [10,0,0], [10,10,0] ] //, [20,10,0], [20,20,0], [0,20,0] ] + var s = verb.topo.Make.extrusion( pts, [0,0,10] ); // hollowPrism(); + + var p = { n : [0,0,1], o : [0,0,5] }; + var r = verb.topo.Split.split( s, p ); + +// s = r.item1; + + var faces = s.faces(); +// var vertices = s.vertices(); + + var eles = faces; + + var graphEdges = eles.reduce(function(a, f){ + return a.concat( + f.neighbors().map(function(fo){ + return { data: { source: f.id.toString(), target: fo.id.toString() } }; + })); + }, []); + + var graphNodes = eles.map(function(x){ + +// var pos = verb.core.Vec.sub( x.l.e.v.pt, [5,15,-2]); +// +// var r = 3 * pos[2]; +// +// var xp = x.pt[0] + r * pos[0]; +// var yp = x.pt[1] + r * pos[1]; + + return { data: { id: x.id.toString(), name: 'Ele' + x.id.toString() } }; + }); + + $('#cy').cytoscape({ + style: cytoscape.stylesheet() + .selector('node') + .css({ + 'content': 'data(name)' + }) + .selector('edge') + .css({ + 'target-arrow-shape': 'triangle' + }) + .selector(':selected') + .css({ + 'background-color': 'black', + 'line-color': 'black', + 'target-arrow-color': 'black', + 'source-arrow-color': 'black' + }) + .selector('.faded') + .css({ + 'opacity': 0.25, + 'text-opacity': 0 + }), + + elements: { + nodes: graphNodes, + edges: graphEdges + }, + + layout: { + name: 'grid' + }, + + // on graph initial layout done (could be async depending on layout...) + ready: function(){ + window.cy = this; + + // giddy up... + + cy.elements().unselectify(); + + } + }); + +}); // on dom ready \ No newline at end of file diff --git a/src/verb/topo/Analyze.hx b/src/verb/topo/Analyze.hx new file mode 100644 index 00000000..68d39f42 --- /dev/null +++ b/src/verb/topo/Analyze.hx @@ -0,0 +1,71 @@ +package verb.topo; + +import verb.core.IDoublyLinkedList; +import verb.core.DoublyLinkedListExtensions; +using verb.core.DoublyLinkedListExtensions; + +import verb.core.Vec; +using verb.core.Vec; + +using Lambda; + +@:expose('topo.Analyze') +class Analyze { + + public static function volume(s : Solid, o : Array = null) : Float { + if (o == null) o = [0.0,0.0,0.0]; + var v = 0.0; + + for (f in s.f.iter()){ + for (l in f.l.iter()){ + var se : HalfEdge = l.e; + var ce = se.nxt; + do { + v += tetravol( se.v.pt, ce.v.pt, ce.nxt.v.pt, o ); + ce = ce.nxt; + } while ( ce.nxt != se ); + } + } + + return v / 6.0; + } + + //helper method for volume of a solid, as an optimization does not mult by 6 + private static function tetravol( a : Array, b : Array, c : Array, d : Array ){ + return ( a.sub(d) ).dot( b.sub(d).cross( c.sub(d) ) ); + } + + public static function area(s : Solid) : Float { + return s.f.iter().fold(function(f,a){ return a + faceArea( f ); }, 0.0); + } + + public static function faceArea(f : Face) : Float { + var n = f.normal(); + return f.l.iter().fold(function(l,a){ return a + loopArea( l, n ); }, 0.0); + } + + public static function loopArea(l : Loop, n : Array = null) : Float { + var e = l.e; + if (e == e.nxt || e == e.nxt.nxt) return 0.0; + + var v = 0.0; + + if (n == null) n = l.f.normal(); + + var se = l.e; + var o = l.e.v.pt; + var ce = se.nxt; + + do { + var a = ce.v.pt; + var b = ce.nxt.v.pt; + + v += n.dot( a.sub(o).cross(b.sub(o)) ); + + ce = ce.nxt; + } while ( ce.nxt != se ); + + return v / 2; + } +} + diff --git a/src/verb/topo/Boolean.hx b/src/verb/topo/Boolean.hx new file mode 100644 index 00000000..17bbd520 --- /dev/null +++ b/src/verb/topo/Boolean.hx @@ -0,0 +1,952 @@ +package verb.topo; + +import verb.core.Exception; +import haxe.ds.IntMap; +import verb.core.Trig; +import verb.topo.Split.Plane; +import verb.core.Vec; +import verb.core.Mat; +using verb.core.Vec; + +import verb.core.Constants; +import verb.core.Intersect; +import verb.core.NurbsCurveData.Point; +import verb.core.Pair; + +import verb.core.IDoublyLinkedList; +import verb.core.DoublyLinkedListExtensions; +using verb.core.DoublyLinkedListExtensions; + +import verb.topo.Split; + +typedef BooleanSplitResult = { + coincidentVertices : Array>, + coplanarVerticesOfA : Array>, + coplanarVerticesOfB : Array> +} + +typedef EdgeFacePosition = { + edge : HalfEdge, + pos : FacePosition +} + +enum FacePosition { + On; + + AonBp; + AonBm; + BonAp; + BonAm; + + AoutB; + AinB; + BoutA; + BinA; +} + +enum BoolOp { + Union; + Subtract; + Intersect; +} + +class SectorIntersection { + public var SectorA : SectorDescription; + public var SectorB : SectorDescription; + + // -1, 0, 1 + public var s1a : Int; //whether edge a of sector 1 is in, on, or out + public var s2a : Int; //whether edge a of sector 2 is in, on, or out + public var s1b : Int; //whether edge b of sector 1 is in, on, or out + public var s2b : Int; //whether edge b of sector 2 is in, on, or out + + public var intersect : Bool = true; + public function new() {} +} + +class SectorDescription { + public var edge : HalfEdge; + public var ref1 : Array; + public var ref2 : Array; + public var ref12 : Array; + public var i : Int; + public var list : Array; + + public function new( i : Int, list : Array ) { + this.i = i; + this.list = list; + } + + public function nxt() { + var j = (this.i + 1) % this.list.length; + return this.list[j]; + } + + public function prv() { + var j = this.i == 0 ? this.list.length - 1 : i - 1; + return this.list[j]; + } + + public function updateNormal(){ + this.ref12 = ref1.cross(ref2); + } +} + +@:expose("topo.Boolean") +class Boolean { + + public static var IN = 1; + public static var ON = 0; + public static var OUT = -1; + + public static function union( a : Solid, b : Solid, tol : Float ){ + + var op = BoolOp.Union; + + // 1. Perform geometric intersection of the two solids, and insert vertices where appropriate + + var sg = intersect( a, b, tol ); + + // 2. Classify the various intersection events + + var clvfa = classifyAllVertexFaceEvents( sg.coplanarVerticesOfA, op, true ); + var clvfb = classifyAllVertexFaceEvents( sg.coplanarVerticesOfB, op, false ); + + var clvv = classifyAllVertexVertexEvents( sg.coincidentVertices, op ); + + // 3. Insert null edges into the solids. We will separate the two solids through these edges. + + var nea = new Array(); + var neb = new Array(); + + insertAllVertexFaceEventNullEdges( sg.coplanarVerticesOfA, clvfa, op, true, nea, neb ); + insertAllVertexFaceEventNullEdges( sg.coplanarVerticesOfB, clvfb, op, false, nea, neb ); + + insertAllVertexVertexEventNullEdges( clvv, nea, neb ); + + // 4. Connect the null edges into a sequences of edges, forming new faces + + var afaces = []; + var bfaces = []; + + connect( nea, neb, afaces, bfaces ); + + // 5. + +// close( parts, op ); //from the various resultant parts BinA, AinB, BoutA, etc, reconnect + + } + + public static function connect( nesa : Array, + nesb : Array, + afaces : Array, + bfaces : Array ) : Void { + + Split.lexicographicalSort( nesa ); + Split.lexicographicalSort( nesb ); + + var h0 : Pair; + var h1 : Pair; + + var looseendsa = []; + var looseendsb = []; + + //for every pair, one of the edges will be naked! + + //join means to take two vertices and join them into one edge + + for (i in 0...nesa.length){ + + var nea = nesa[i]; + var neb = nesb[i]; + + if ( (h0 = canJoin( nea, neb.opp, looseendsa, looseendsb )) != null){ + + trace("joining 1!"); + + var h0a = h0.item0; + var h0b = h0.item1; + + trace("connecting:", h0a.id, nea.id); + + Split.join( h0a, nea ); + + if (!Split.isLoose(h0a.opp, looseendsa)) { + Split.cut(h0a, afaces); + } + + Split.join( h0b, neb.opp ); + if (!Split.isLoose(h0b.opp, looseendsb)) { + Split.cut(h0b, bfaces); + } + } + + if ( (h1 = canJoin( nea.opp, neb, looseendsa, looseendsb )) != null){ + + trace("joining 2!"); + + var h1a = h1.item0; + var h1b = h1.item1; + + trace("connecting:", h1a.id, h1b.id, neb.id, nea.opp.id); + + Split.join( h1a, nea.opp ); + if (!Split.isLoose(h1a.opp, /*ok*/looseendsa)) { + Split.cut(h1a, afaces); + } + + trace('alright'); + Split.join( h1b, neb ); + if (!Split.isLoose(h1b.opp, /*ok*/looseendsb)) { + Split.cut(h1b, bfaces); + } + } + + if (h0 != null && h1 != null) { + trace('cutting'); + + Split.cut(nea, afaces); + Split.cut(neb, bfaces); + } + } + } + + public static function canJoin(hea : HalfEdge, + heb : HalfEdge, + looseendsa : Array, + looseendsb : Array) : Pair { + + if (hea != null && heb != null){ + for (i in 0...looseendsa.length){ + + var n0 = Split.neighbor(hea, looseendsa[i]); + var n1 = Split.neighbor(heb, looseendsb[i]); + + if (n0 && n1){ + var ra = looseendsa[i]; + var rb = looseendsb[i]; + looseendsa.splice(i, 1); + looseendsb.splice(i, 1); + + return new Pair(ra, rb); + } + } + } + + if (hea != null){ + looseendsa.push(hea); + } + + if (heb != null){ + looseendsb.push(heb); + } + + return null; + } + + public static function insertAllVertexFaceEventNullEdges( vfs : Array>, + efs : Array>, + op : BoolOp, + isA : Bool, + nea : Array, + neb : Array ) : Void { + + for (i in 0...vfs.length){ + insertVertexFaceEventNullEdges( vfs[i].item0, vfs[i].item1, efs[i], op, isA, nea, neb ); + } + } + + public static function insertVertexFaceEventNullEdges( v : Vertex, f : Face, efs : Array, op : BoolOp, + isA : Bool, nea : Array, neb : Array ) : Void { + + insertVertexFaceEventNullEdgesCore( v, f, efs, isA, isA ? nea : neb ); + + //this results in invalid an edge with an invalid nxt field + insertNullEdgeIntoFace( v.pt.copy(), f, isA ? neb : nea ); + } + + public static function insertVertexFaceEventNullEdgesCore( v : Vertex, f : Face, efs : Array, + isA : Bool, nulledges : Array ) : Void { + + var s = v.e.l.f.s; + var i = nextOfClass( above(isA), efs, 0 ); + + //there's no above edge in the seq (all below), continue + if (i == -1) return; + + //if all of the edges are of the same type, just continue + if (nextOfClass( below(isA), efs, 0 ) == -1) return; + + var start = efs[i].edge; + var head = start; + var tail = start; + var el = efs.length; + + while(true){ + + //find the end of the ABOVE sequence + while (efs[i].pos == above(isA)){ + tail = efs[i].edge; + i = (i + 1) % el; + } + + //insert a null edge + s.lmev( head, tail.opp.nxt, head.v.pt.copy() ); + nulledges.push( head.prv ); + + //find the next start sequence and assign to head + i = nextOfClass( above(isA), efs, i ); + + //there is no other above edge, we're done with this vertex + if (i == -1) break; + + head = efs[i].edge; + + //we've come back to the beginning (should never happen) + if (head == start) break; + } + + } + + private static function nextOfClass( cl : FacePosition, ecs : Array, start : Int ) : Int { + + var i = start; + var head : EdgeFacePosition = null; + while (i < ecs.length){ + if ( ecs[i].pos == cl ) { + head = ecs[i]; + break; + } + i++; + } + return head != null ? i : -1; + } + + + public static function insertNullEdgeIntoFace( point : Point, f : Face, nes : Array ) : Void { + + var nv = f.s.lmev( f.ol.e, f.ol.e, point ); + var nl = f.s.lkemr(nv.e.prv); + + nes.push( nv.e ); + } + + public static function insertAllVertexVertexEventNullEdges( sps : Array>, + nea : Array, + neb : Array ) : Void { + + for (sp in sps){ + insertVertexVertexEventNullEdges( sp, nea, neb ); + } + } + + public static function classifyAllVertexVertexEvents ( vvs : Array>, op : BoolOp ) : Array>{ + return [for (vv in vvs) classifyVertexVertexEvent(vv.item0, vv.item1, op )]; + } + + public static function classifyVertexVertexEvent( a : Vertex, b : Vertex, op : BoolOp ) : Array { + var sps = classifyVertexVertexCore( a, b ); + reclassifyCoplanarSectorPairs( sps, op ); + reclassifyCoplanarSectorEdge( sps, op ); + return sps; + } + + public static function insertVertexVertexEventNullEdges( ar : Array, + nea : Array, + neb : Array ) : Void { + + var i = 0; + var arl = ar.length; + while (true){ + + while (!ar[i].intersect) + if (++i == arl) return; + + var ha1 : HalfEdge = null; + var ha2 : HalfEdge = null; + + var hb1 : HalfEdge = null; + var hb2 : HalfEdge = null; + + if (ar[i].s1a == OUT){ ha1 = ar[i].SectorA.edge; } else { ha2 = ar[i].SectorA.edge; } + if (ar[i].s1b == IN){ hb1 = ar[i++].SectorB.edge; } else { hb2 = ar[i++].SectorB.edge; } + + while (!ar[i].intersect) + if (++i == arl) return; + + if (ar[i].s1a == OUT){ ha1 = ar[i].SectorA.edge; } else { ha2 = ar[i].SectorA.edge; } + if (ar[i].s1b == IN){ hb1 = ar[i++].SectorB.edge; } else { hb2 = ar[i++].SectorB.edge; } + + if (ha1 == ha2){ + insertNullEdge(ha1, ha1, 0, nea, neb ); + insertNullEdge(hb1, hb2, 1, nea, neb ); + } else if (hb1 == hb2){ + insertNullEdge(hb1, hb1, 1, nea, neb ); + insertNullEdge(ha2, ha1, 0, nea, neb ); + } else { + insertNullEdge(ha2, ha1, 0, nea, neb ); + insertNullEdge(hb1, hb2, 1, nea, neb ); + } + + if (i == arl) return; + } + } + + public static function insertNullEdge(t : HalfEdge, f : HalfEdge, type : Int, nea : Array, neb : Array ){ + t.l.f.s.lmev(f, t, f.v.pt.copy()); + if (type == 0){ + nea.push( f.prv ); + } else { + neb.push( f.prv ); + } + } + + + public static function classifyAllVertexFaceEvents( a : Array>, op : BoolOp, isA : Bool ) : Array> { + return [for (vf in a) classifyVertexFaceEvent( vf.item0, vf.item1, op, isA )]; + } + + public static function classifyVertexFaceEvent( v : Vertex, f : Face, op : BoolOp, isA : Bool ) : Array { + + var p = planeFromFace(f); + var ecs = new Array(); + + // 1. classify vertex edges based on opposite vertex's signed distance from the cutting plane + for (e in v.halfEdges()){ + ecs.push({ edge: e, pos : asFacePosition(Split.classify(e, p), isA) }); + + //for each edge, we also need to check the sector width - i.e. the angle + //bisector between two adjacent edges. If more than 180, we bisect the edge + if ( Split.wideSector(e) ){ + ecs.push({ edge: e, pos : asFacePosition(Split.classifyBisector(e, p), isA) }); + } + } + + // 2. now, for each "on" edge, we need to determine if its face is aligned with the plane normal + //if so, + // if aligned with plane normal (dot(fn, sp) > 0), AonBp else AonBm + var el = ecs.length; + for (i in 0...el){ + var ep = ecs[i]; + if (ep.pos == FacePosition.On){ + var nc = reclassifyCoplanarEdge(ep.edge, p, isA); + ecs[i].pos = nc; + ecs[(i+1) % el].pos = nc; + } + } + + //reclassify faces that are on + for (i in 0...el){ + var ep = ecs[i]; + var ei = Type.enumIndex( ep.pos ); + if (ei > 0 && ei < 5){ + ep.pos = reclassifyOnSector( ep.pos, op ); + } + } + + //it's possible that an edge was on, but its neighbors were not, indicating that its parent face is not coplanar + //here, we reclassify based on neighbor edges + for (i in 0...el){ + var ep = ecs[i]; + if (ep.pos == FacePosition.On){ + + var a = i == 0 ? el-1 : i-1; + var b = (i+1) % el; + + var prv = ecs[a].pos; + var nxt = ecs[b].pos; + + if ( isAbove( prv ) && isAbove( nxt ) ){ + ep.pos = below( isA ); + } else if ( isBelow( prv ) && isAbove( nxt ) ) { + ep.pos = below( isA ); + } else if ( isAbove( prv ) && isBelow( nxt ) ) { + ep.pos = below( isA ); + } else if ( isBelow( prv ) && isBelow( nxt ) ) { + ep.pos = above( isA ); + } else { + throw new Exception("Double On edge encountered!"); + } + } + } + + return ecs; + } + + public static function reclassifyCoplanarSectorEdge( sps : Array, op : BoolOp ){ + + for (sp in sps){ + + if ( !(sp.s1a == 0 || sp.s1b == 0 || sp.s2a == 0 || sp.s2b == 0) ) continue; + + throw new Exception("Coplanar sector edge classification not yet implemented!"); + + //TODO: 2 varieties + //edges lying within a sector face + //look at neighbor sectors of the edge + //both in -> in + //both out -> out + //in-out -> not an intersection? + //edges aligned with an edge from the other solid + // + + } + + } + + public static function reclassifyCoplanarSectorPairs( sps : Array, op : BoolOp ) : Void { + + for (sp in sps){ + + if (!( sp.s1a == 0 && sp.s1b == 0 && sp.s2a == 0 && sp.s2b == 0 ) ) continue; + + //do reclassification + var sa = sp.SectorA; + var sb = sp.SectorB; + + var psa = sa.prv(); + var nsa = sa.nxt(); + + var psb = sb.prv(); + var nsb = sb.nxt(); + + var ha = sa.edge; + var hb = sb.edge; + + var newsa : Int = 0; + var newsb : Int = 0; + + var aligned = ha.l.f.normal().sub( hb.l.f.normal() ).norm() < Constants.EPSILON; + + if (aligned){ + newsa = (op == BoolOp.Union) ? -1 : 1; + newsb = (op == BoolOp.Union) ? 1 : -1; + } else { + newsa = (op == BoolOp.Union) ? 1 : -1; + newsb = (op == BoolOp.Union) ? 1 : -1; + } + + for (sp2 in sps){ + if ( sp2.SectorA == psa && sp2.SectorB == sb && sp2.s1a != 0 ){ + sp2.s2a = newsa; + } + + if ( sp2.SectorA == nsa && sp2.SectorB == sb && sp2.s2a != 0){ + sp2.s1a = newsa; + } + + if ( sp2.SectorA == sa && sp2.SectorB == psb && sp2.s1b != 0){ + sp2.s2b = newsb; + } + + if ( sp2.s1a == sp2.s2a && sp2.s1a != 0 ){ + sp2.intersect = false; + } + + if ( sp2.s1b == sp2.s2b && sp2.s1b != 0 ){ + sp2.intersect = false; + } + } + + sp.s1a = sp.s2a = newsa; + sp.s1b = sp.s2b = newsb; + sp.intersect = false; + } + + } + + public static function classifyVertexVertexCore( a : Vertex, b : Vertex ) : Array { + + var res = new Array(); + + //preprocess the sectors with extra geometric info + var svsa = preprocessVertexSectors( a ); + var svsb = preprocessVertexSectors( b ); + + //for each sector pair + for (sva in svsa){ + for (svb in svsb){ + + //if the pair intersects, classify the pair + if ( sectorsIntersect( sva, svb ) ){ + + var sp = new SectorIntersection(); + res.push( sp ); + + sp.SectorA = sva; + sp.SectorB = svb; + + var na = sva.edge.l.f.normal(); + var nb = svb.edge.l.f.normal(); + + sp.s1a = comp( nb.dot( sva.ref1 ), 0.0, Constants.EPSILON ); + sp.s2a = comp( nb.dot( sva.ref2 ), 0.0, Constants.EPSILON ); + sp.s1b = comp( na.dot( svb.ref1 ), 0.0, Constants.EPSILON ); + sp.s2b = comp( na.dot( svb.ref2 ), 0.0, Constants.EPSILON ); + } + } + } + + return res; + } + + public static function sectorsIntersect( a : SectorDescription, b : SectorDescription ) : Bool { + + var na = a.edge.l.f.normal(); + var nb = b.edge.l.f.normal(); + + var int = na.cross(nb); + + if (int.norm() < Constants.EPSILON){ + return sectorsOverlap( a, b ); + } + + if ( withinSector( int, a ) && withinSector( int, b ) ){ + return true; + } + + int = int.neg(); + return withinSector( int, a ) && withinSector( int, b ); + } + + public static function sectorsOverlap( a : SectorDescription, b : SectorDescription ) : Bool { + throw new Exception("sectorsOverlap not implemented!"); + return false; + } + + public static function withinSector( vec : Array, sv : SectorDescription ) : Bool { + //TODO: not sure how robust this is + return vec.positiveAngleBetween( sv.ref1, sv.ref12 ) < sv.ref1.positiveAngleBetween( sv.ref2, sv.ref12 ); + } + + public static function comp(a : Float, b : Float, tol : Float ) : Int { + if ( Math.abs(a - b) < tol ){ + return 0; + } + return ( a > b ) ? 1 : -1; + } + + public static function preprocessVertexSectors( v : Vertex, tol : Float = 1.0e-3 ) : Array { + + var svs = new Array(); + + var i = 0; + + for (e in v.halfEdges()){ + + var sv = new SectorDescription(i++, svs); + svs.push(sv); + + sv.edge = e; + sv.ref1 = e.prv.v.pt.sub( e.v.pt ); + sv.ref2 = e.nxt.v.pt.sub( e.v.pt ); + sv.updateNormal(); + + //are the two edges coincident? or is the angle between them > 180? + if (sv.ref12.norm() < tol || e.l.f.normal().dot( sv.ref12 ) > 0.0 ){ + + //bisect the sector! + var bisector : Array; + + if ( sv.ref12.norm() < tol ){ + //TODO not sure how to handle this case + throw new Exception("Coincident consecutive edges encountered!"); +// bisector = inside( e ); + } else { + bisector = sv.ref1.add( sv.ref2 ).neg(); + } + + var sv2 = new SectorDescription(i++, svs); + svs.push( sv2 ); + + sv2.edge = e; + sv2.ref2 = sv.ref2.copy(); + sv2.ref1 = bisector; + sv.ref2 = bisector; + + sv.updateNormal(); + sv2.updateNormal(); + } + } + + return svs; + } + + public static function above( isA : Bool ) : FacePosition { + return isA ? FacePosition.AoutB : FacePosition.BoutA; + } + + public static function below( isA : Bool ) : FacePosition { + return isA ? FacePosition.AinB : FacePosition.BinA; + } + + public static function isAbove( pos : FacePosition ) : Bool { + return pos == FacePosition.AoutB || pos == FacePosition.BoutA; + } + + public static function isBelow( pos : FacePosition ) : Bool { + return pos == FacePosition.AinB || pos == FacePosition.BinA; + } + + public static function planeFromFace( f : Face ){ + return { o : f.l.e.v.pt, n : f.normal() }; + } + + private static function reclassifyCoplanarEdge( e : HalfEdge, p : Plane, isA : Bool ) : FacePosition { + + var n = e.l.f.normal(); //TODO: cache me + var ndc = n.dot(p.n); + + var eps2 = Constants.EPSILON * Constants.EPSILON; + + //face and plane are aligned + if ( Math.abs(ndc - 1.0) < eps2 ) { + return isA ? FacePosition.AonBp : FacePosition.BonAp; + } + + if ( Math.abs(ndc + 1.0) < eps2 ) { + return isA ? FacePosition.AonBm : FacePosition.BonAm; + } + + return FacePosition.On; + } + + private static function asFacePosition( pos : PlanePosition, isA : Bool ) : FacePosition { + if (pos == PlanePosition.Above){ + return isA ? FacePosition.AoutB : FacePosition.BoutA; + } else if (pos == PlanePosition.Below) { + return isA ? FacePosition.AinB : FacePosition.BinA; + } + + return FacePosition.On; //will need to be recategorized later + } + + private static var boolOnSectorMap = + [ + [ FacePosition.AoutB, FacePosition.AinB, FacePosition.BinA, FacePosition.BinA ], + [ FacePosition.AinB, FacePosition.AoutB, FacePosition.BoutA, FacePosition.BoutA ], + [ FacePosition.AinB, FacePosition.AoutB, FacePosition.BoutA, FacePosition.BoutA ] + ]; + + public static function reclassifyOnSector( c : FacePosition, op : BoolOp ) : FacePosition { + return boolOnSectorMap[ Type.enumIndex(op) ][ Type.enumIndex(c) ]; + } + + public static function intersect( a : Solid, b : Solid, tol : Float ) : BooleanSplitResult { + + var va = splitAllEdges( a, b, tol ); + var vva = splitEdgesByVertices( a, b, tol ); + var vvb = splitEdgesByVertices( b, a, tol ); + var vfa = splitEdgesWithFaces( a, b, tol ); + var vfb = splitEdgesWithFaces( b, a, tol ); + + return { + coincidentVertices : getCoincidentVertices( a, b, va.concat(vva).concat(vvb), tol ), + coplanarVerticesOfA : getCoplanarVertices( a, b, vfa, tol ), + coplanarVerticesOfB : getCoplanarVertices( b, a, vfb, tol ) + }; + } + + //find the intersecting edges of the two solids and split them + public static function splitAllEdges( a : Solid, b : Solid, tol : Float ) : Array> { + var c = new Array>(); + + //TODO spatial partition + for (e0 in a.edges()){ + for (e1 in b.edges()){ + var a = splitEdges(e0.item0, e1.item0, tol); + if (a == null) continue; + c.push( a ); + } + } + + return c; + } + + public static function splitEdges( a : HalfEdge, b : HalfEdge, tol : Float ) : Pair { + + var i = verb.core.Intersect.segments( a.v.pt, a.nxt.v.pt, b.v.pt, b.nxt.v.pt, tol ); + + //no intersection + if (i == null) { + return null; + } + + //if the intersection is on a vertex, ignore it + if ( i.u0 > 1.0 - Constants.EPSILON || i.u0 < Constants.EPSILON || + i.u1 > 1.0 - Constants.EPSILON || i.u1 < Constants.EPSILON ){ + return null; + } + + return new Pair( splitEdge(a, i.point0), splitEdge(b, i.point1) ); + } + + private static function splitEdgeByVertex( e : HalfEdge, v : Vertex, tol : Float ) : Vertex { + + var i = Trig.segmentClosestPoint( v.pt, e.v.pt, e.nxt.v.pt, 0.0, 1.0 ); + var d = v.pt.distSquared( i.pt ); + + //too far + if ( d > tol*tol ) { + return null; + } + + //if the intersection is on a vertex, ignore it + if ( i.u > 1.0 - Constants.EPSILON || i.u < Constants.EPSILON ){ + return null; + } + + return splitEdge( e, i.pt ); + } + + private static function splitEdge( e : HalfEdge, pt : Point ) : Vertex { + var s = e.l.f.s; + return s.lmev( e, e.opp.nxt, pt ); + } + + //determine if a point is within a face of a solid + //TODO test + public static function isPointInFace( pt : Point, f : Face, tol : Float ) : Bool { + + var n = f.normal(); + var o = f.l.e.v.pt; + + if ( !Trig.isPointInPlane(pt, { n : n, o : o}, tol) ) return false; + + var iol = isPointInPolygon( pt, f.ol.points(), n ); + if (!iol) return iol; + + for (il in f.rings()) { + if ( isPointInPolygon(pt, il.points(), n) ) return false; + } + + return true; + } + + //determine if a point is within a polygon + public static function isPointInPolygon( pt : Point, pts : Array, n : Array ) : Bool { + var ptsl = pts.length; + var a = 0.0; + + for ( i in 0...ptsl ){ + var v0 = pts[i].sub( pt ); + var v1 = pts[(i+1) % ptsl].sub( pt ); + + a += Vec.positiveAngleBetween( v0, v1, n ); + } + + return Math.abs(a) > Math.PI; + } + + //find the edges of a that intersect with the vertices of b and intersect them + public static function splitEdgesByVertices( a : Solid, b : Solid, tol : Float ) : Array> { + var c = new Array>(); + + //TODO spatial partition + for (e in a.edges()){ + for (v in b.v.iter()){ + var a = splitEdgeByVertex(e.item0, v, tol); + if (a == null) continue; + c.push( new Pair(a,v)); + } + } + + return c; + } + + //get the vertex pairs that are coincident between a and b, third argument is array of pairs we already know are coincident + public static function getCoincidentVertices( a : Solid, b : Solid, v : Array>, tol : Float ) : Array> { + + //prevent formation of duplicates + var m = new IntMap(); + + for (p in v){ + m.set( p.item0.id, p.item0 ); + m.set( p.item1.id, p.item1 ); + } + + var tol2 = tol * tol; + + for ( v0 in a.v.iter() ){ + + if (m.exists(v0.id)) continue; + for ( v1 in b.v.iter() ){ + + if (m.exists(v1.id)) continue; + + if (v0.pt.distSquared(v1.pt) < tol2){ + v.push( new Pair(v0, v1) ); + } + } + } + + return v; + } + + //split the edges of a that intersect faces of b + public static function splitEdgesWithFaces( a : Solid, b : Solid, tol : Float ) : Array> { + var v = new Array>(); + + for (e in a.edges()){ + for (f in b.f.iter()){ + var r = splitEdgeWithFace( e.item0, f, tol ); + if (r == null) continue; + v.push( new Pair(r, f) ); + } + } + + return v; + } + + public static function splitEdgeWithFace( he : HalfEdge, f : Face, tol : Float ) : Vertex { + var n = f.normal(); + var o = f.ol.e.v.pt; + + var r = verb.core.Intersect.segmentAndPlane(he.v.pt, he.nxt.v.pt, o, n ); + + //no intersection + if (r == null) { + return null; + } + + //on vertex + if ( r.p > 1.0 - Constants.EPSILON || r.p < Constants.EPSILON ){ + return null; + } + + //get the actual intersection point + var pt = Vec.lerp( r.p, he.nxt.v.pt, he.v.pt ); + + if (!isPointInFace( pt, f, tol )){ + return null; + } + + return splitEdge( he, pt ); + } + + //find the vertices of a that are coplanar with a face of b, third argument is vertices we already know are coplanar + public static function getCoplanarVertices( a : Solid, b : Solid, ar : Array>, tol : Float ) : Array> { + + //prevent formation of duplicates + var m = new IntMap(); + + for (p in ar){ + m.set( p.item0.id, p.item0 ); + } + + for ( v in a.v.iter() ){ + + if (m.exists(v.id)) continue; + + for ( f in b.f.iter() ){ + + if (isPointInFace( v.pt, f, tol )){ + ar.push( new Pair(v, f) ); + } + } + } + + return ar; + } + +} diff --git a/src/verb/topo/Face.hx b/src/verb/topo/Face.hx new file mode 100644 index 00000000..31d3878c --- /dev/null +++ b/src/verb/topo/Face.hx @@ -0,0 +1,127 @@ +package verb.topo; + +import verb.core.Constants; +import verb.core.NurbsCurveData.Point; +import verb.core.Exception; +import haxe.ds.IntMap; + +import verb.core.DoublyLinkedListExtensions; +import verb.geom.ISurface; + +using Lambda; + +import verb.core.IDoublyLinkedList; +using verb.core.DoublyLinkedListExtensions; + +import verb.topo.Tess2; + +import verb.core.Vec; +using verb.core.Vec; + +@:expose("topo.Face") +class Face implements IDoublyLinkedList extends Topo { + + public var s : Solid; + public var ol : Loop; //outer loop + public var l : Loop; + public var prv : Face; + public var nxt : Face; +// public var srf : ISurface; //TODO: all faces now planar + + public function new(solid) { + this.s = solid; + } + + public function loops() : Array { + return l.iter().array(); + } + + //TODO: test + public function rings() : Array { + var a = []; + for (il in l.iter()){ + if (il == ol) continue; + a.push(il); + } + return a; + } + + public function addLoop(nl : Loop = null) : Loop { + if (nl == null) { + nl = new Loop(this); + } + if (ol == null) ol = nl; //the first loop is the outer loop + return l = l.push(nl); + } + + public function delLoop(kl : Loop) : Void { + if (kl == ol) { + throw new Exception("Cannot delete outer loop!"); //this may be overly conservative + } + l = l.kill(kl); + } + + //TODO: test + public function neighbors() : Array { + var memo = new IntMap(); + memo.set(id, this); //do not include self + + var a = []; + + var he = halfEdges(); + for (e in he){ + if (e.opp == null) continue; + + var f = e.opp.l.f; + if (memo.exists(f.id)) continue; + + memo.set(f.id, f); + a.push(f); + } + + return a; + } + + //TODO: test + public function halfEdges() : Array{ + return loops().fold(function(l : Loop, a : Array){ + return a.concat( l.halfEdges() ); + }, []); + } + + public function tessellate(){ + var opts = new Tess2Options(); + + opts.contours = loops().map(function(x : Loop){ return x.coords(); }) + .filter(function(x : Array){ return x.length > 3; }); + opts.windingRule = Tess2.WINDING_POSITIVE; + opts.elementType = Tess2.POLYGONS; + opts.polySize = 3; + opts.normal = normal(); + opts.vertexSize = 3; + + return Tess2.tessellate(opts); + } + + public function normal() : Array { + + var x = [0.0,0.0,0.0]; + + for (ei in ol.e.iter()){ + var v0 : Array = ei.v.pt; + var v1 : Array = ei.nxt.v.pt; + var v2 : Array = ei.nxt.nxt.v.pt; + + var v01 = v0.sub(v1); + var v21 = v2.sub(v1); + + var cv : Array = v21.cross(v01); + + if (cv.normSquared() > verb.core.Constants.EPSILON){ + x = x.add(cv); + } + } + + return x.normalized(); + } +} diff --git a/src/verb/topo/HalfEdge.hx b/src/verb/topo/HalfEdge.hx new file mode 100644 index 00000000..d06de4f3 --- /dev/null +++ b/src/verb/topo/HalfEdge.hx @@ -0,0 +1,30 @@ +package verb.topo; + +import verb.geom.ICurve; +import verb.core.IDoublyLinkedList; + +@:expose("topo.HalfEdge") +class HalfEdge implements IDoublyLinkedList extends Topo { + public var v : Vertex; + public var l : Loop; + public var opp : HalfEdge; + public var prv : HalfEdge; + public var nxt : HalfEdge; + public var crv : ICurve; + + public function new( loop : Loop, vertex : Vertex ) { + this.v = vertex; + this.v.e = this; + this.l = loop; + } + + public function mate(he : HalfEdge) : HalfEdge { + if (he == null) { + return this; + } + + this.opp = he; + he.opp = this; + return this; + } +} diff --git a/src/verb/topo/Loop.hx b/src/verb/topo/Loop.hx new file mode 100644 index 00000000..67d82ca5 --- /dev/null +++ b/src/verb/topo/Loop.hx @@ -0,0 +1,79 @@ +package verb.topo; + +import verb.core.NurbsCurveData.Point; +import verb.core.Exception; +import verb.core.DoublyLinkedListExtensions; + +using Lambda; + +import verb.core.IDoublyLinkedList; +using verb.core.DoublyLinkedListExtensions; + +@:expose("topo.Loop") +class Loop implements IDoublyLinkedList extends Topo { + public var f : Face; + public var e : HalfEdge; + public var prv : Loop; + public var nxt : Loop; + + public function new(face : Face) { + this.f = face; + } + + public function halfEdges() : Array { + return e.iter().array(); + } + + public function vertices() : Array { + return halfEdges().map(function(e : HalfEdge){ return e.v; }); + } + + public function coords() : Array { + return vertices().fold(function(v : Vertex, a : Array){ return a.concat(v.pt); }, []); + } + + public function points() : Array { + return vertices().map(function(v : Vertex){ return v.pt; }); + } + + public function addHalfEdge(vertex : Vertex, next : HalfEdge = null, opp : HalfEdge = null ) : HalfEdge { + if (next != null && next.l != this){ + throw new Exception("Next HalfEdge is not part of same Loop!"); + } + + if (next != null && next.opp == null && opp == null ){ + vertex.e = next; + next.v = vertex; + return next; + } + + if (next != null){ e = next; } + + var he = new HalfEdge( this, vertex ); + he.mate(opp); + + return e = e.push( he ); + } + + public function delHalfEdge( he : HalfEdge ) : Loop { + if ( he.l != this) { + throw new Exception("HalfEdge is not part of this Loop!"); + } + + //the base case - a half-edge cycle + if (he.nxt == he){ + he.opp = null; + this.e = he; + he.v.e = he; + return this; + } + + //reassign parent edge for vertex + if (he.opp.nxt != null){ + he.v.e = he.opp.nxt; + } + + this.e = e.kill(he); + return this; + } +} diff --git a/src/verb/topo/Make.hx b/src/verb/topo/Make.hx new file mode 100644 index 00000000..56eb8f92 --- /dev/null +++ b/src/verb/topo/Make.hx @@ -0,0 +1,73 @@ +package verb.topo; + + +import verb.core.Exception; +import verb.core.NurbsCurveData.Point; + +import verb.core.Vec; +using verb.core.Vec; + +typedef Polygon = Array; + +@:expose('topo.Make') +class Make { + + public static function lamina( profile : Polygon ) : Solid { + var s = Solid.mvfs( profile[0] ); + + var p0 = profile[0]; + + var e = s.f.l.e; + var ce = s.f.l.e; + + for (pt in profile){ + if (pt == p0) continue; + var nv = s.lmev( ce, ce, pt ); + ce = nv.e; + } + + //the initial lmev moves the first half-edge to + //start at the second vertex, so we need to + //use e.nxt here + s.lmef( e.nxt, ce ); + + return s; + } + + public static function extrusion( profile : Polygon, dir : Array ) : Solid { + + //assume all points are not coincident, form one continuous outline + if (profile.length < 3) { + throw new Exception("More than three points are required to define a polygon!"); + } + + //TODO: make sure you go through profile in the right order +// var a = profile[0]; +// var b = profile[1]; +// var c = profile[2]; +// +// //pick 3 points and use the extrusion direction to define the plane +// var n = a.sub(b).cross( c.sub(b) ); +// +// if ( n.dot(dir) > 0 ){ +// //reverse +// } + + var s = lamina( profile ); + + var nvs = s.f.l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, dir) ); + }); + + nvs.map(function(v : Vertex){ + return v.e; + }) + .map(function(e : HalfEdge){ + var nf = s.lmef(e, e.nxt.nxt.nxt); + return nf; + }); + + return s; + } + +} diff --git a/src/verb/topo/RayCast.hx b/src/verb/topo/RayCast.hx new file mode 100644 index 00000000..bc11c264 --- /dev/null +++ b/src/verb/topo/RayCast.hx @@ -0,0 +1,71 @@ +package verb.topo; +import verb.core.Trig; +import verb.core.Intersect; +import verb.core.Vec; +using verb.core.Vec; +import verb.core.Ray; + +import verb.core.DoublyLinkedListExtensions; +using verb.core.DoublyLinkedListExtensions; + +class RayCast { + + //TODO : slow as shit + public static function raycast(s : Solid, ray : Ray, tol : Float = 0.001) : RayCastResult { + + var s0 = ray.origin; + var s1 = Vec.add( ray.origin, Vec.mul(1.0e6, ray.dir) ); + + var d = Math.POSITIVE_INFINITY; + var ct = RayCastResultKind.None; + var ce : HalfEdge = null; + var cv : Vertex = null; + var cf : Face = null; + + for (e in s.edges()){ + var r = Intersect.segments( e.item0.v.pt, e.item0.nxt.v.pt, s0, s1, tol ); + if (r == null) continue; + + var cd = Vec.distSquared(r.point1, ray.origin); + if (r.u1 > 0.0 && cd < d ){ + ct = RayCastResultKind.HalfEdge; + d = cd; + } + } + + for (v in s.v.iter()){ + var r = Trig.rayClosestPoint( v.pt, ray.origin, ray.dir ); + if (Vec.distSquared(r, v.pt) > tol) continue; + + var cd = Vec.distSquared(r, ray.origin); + if (cd < d && ray.dir.dot(Vec.sub( r, ray.origin )) > 0.0){ + ct = RayCastResultKind.Vertex; + d = cd; + } + } + + for (f in s.faces()){ + + } + + return { + distance: d, + kind : ct, + edge : ce, + face : cf, + vertex : cv + }; + } +} + +enum RayCastResultKind { + None; HalfEdge; Vertex; //Face; +} + +typedef RayCastResult = { +distance : Float, +kind : RayCastResultKind, +edge : HalfEdge, +face : Face, +vertex : Vertex +} diff --git a/src/verb/topo/Solid.hx b/src/verb/topo/Solid.hx new file mode 100644 index 00000000..baf82d53 --- /dev/null +++ b/src/verb/topo/Solid.hx @@ -0,0 +1,401 @@ +package verb.topo; + +import verb.topo.Tess2.PriorityQ; +import verb.core.Intersect; +import verb.core.Vec; +import verb.core.Trig; +import verb.core.Ray; +import haxe.ds.IntMap; +import verb.core.Exception; + +using Lambda; + +using verb.core.Vec; + +import verb.core.Pair; +import verb.core.NurbsCurveData.Point; + +import verb.core.IDoublyLinkedList; +import verb.core.DoublyLinkedListExtensions; +using verb.core.DoublyLinkedListExtensions; + +@:expose("topo.Solid") +class Solid extends Topo { + + public var v : Vertex; + public var f : Face; + + public function new(){} + + //make vertex face solid + public static function mvfs(pt : Point) : Solid { + var s = new Solid(); + var f = s.addFace(); + var l = f.addLoop(); + var h = l.addHalfEdge( s.addVertex(pt) ); + + return s; + } + + //make edge vertex + public function lmev(he0 : HalfEdge, he1 : HalfEdge, pt : Point) : Vertex { + + //TODO: should the two half edges be from different loops unless duplicate? + + //create a new vertex + var v = this.addVertex( pt ); + + //store the original vertex we'll be "splitting" + var ov = he0.v; + + var he = he0; + while ( he != he1 ) { + he.v = v; + he = he.opp.nxt; + } + + var nhe0 = he1.l.addHalfEdge( v, he1 ); + var nhe1 = he0.l.addHalfEdge( ov, if (he0 == he1) nhe0 else he0, nhe0 ); + + return v; + } + + //make edge face + public function lmef(he0 : HalfEdge, he1 : HalfEdge) : Face { + if (he0.l != he1.l) { + throw new Exception("Both HalfEdge's must be part of the same loop!"); + } + + var nf = addFace(); + var nl = nf.addLoop(); + var ol = he1.l; + + var he = he0; + while (he != he1){ + he.l = nl; + he = he.nxt; + } + + //close the two loops + he1.prv.nxt = he0; + he0.prv.nxt = he1; + + var t = he1.prv; + he1.prv = he0.prv; + he0.prv = t; + + //insert new edges into the two loops + var nhe0 = nl.addHalfEdge( he1.v, he0 ); + var nhe1 = ol.addHalfEdge( he0.v, he1, nhe0 ); + + return nf; + } + + //kill edge make ring + public function lkemr(he0 : HalfEdge) : Loop { + + //result is -1 (sometimes -2) half edges and +1 loop + var he1 = he0.opp; + var ol = he0.l; + var nl = ol.f.addLoop(); + + var hea = he0.nxt; + var heb = he1.nxt; + + //ensure vertex edge pointers are pointing at valid edges + heb.v.e = heb; + hea.v.e = hea; + + //split the original loop + he0.prv.nxt = he1.nxt; + he1.nxt.prv = he0.prv; + + //ensure the old loop has a valid edge ptr + ol.e = he1.nxt; + + //the two edges now loop back around in nl + he1.nxt = he0; + he0.prv = he1; + + //move he0, he1 and edges in between to nl + var che = he0; + do { + che.l = nl; + che = che.nxt; + } while( che != he0 ); + + //move the edges to nl + nl.e = he1; + + //delete the half-edges from the new loop + nl.delHalfEdge( he0 ); + nl.delHalfEdge( he1 ); //properly handles the length-1 loop case + + return nl; + } + + //TODO: test + public function lkvfs(he : HalfEdge) : Solid { + //remove an face with a single vertex + //should remove a face that contains one vertex, halfedge, and loop + + //remove the face + //remove the vertex + + //TODO: check if other edges ref this one? + + var v = he.v; + this.delVertex( v ); + this.delFace( he.l.f ); + return this; + } + + //unlike lkev which splits a vertex in two, this should + //join two adj vertices together + public function lkev(he : HalfEdge) : Solid { + if (he.nxt == he){ + throw new Exception("Cannot lkev the base case!"); + } + + var kv = he.nxt.v; //in lkev, he.v's vertex needs to be set correctly + + //loop around the second vertex, assigning the start vertex + var che = he.nxt; + do { + che.v = he.v; + } while ( ( che.opp.nxt = che) != he.nxt); + + //remove the two half edges from their parent loops + var oe = he.opp; + he.l.delHalfEdge(he); + oe.l.delHalfEdge(oe); + + return this.delVertex( kv ); + } + + //unlike kef, which splits a face - this lkef joins two faces together + public function lkef(he : HalfEdge) : Solid { + + //what if the face has internal rings? + //what if the two faces are equal? + + if (he.opp == null){ + throw new Exception("Cannot kill base case!"); + } + + if (he.opp.l.f == he.l.f){ + throw new Exception("Edge does not traverse two distinct faces!"); + } + + var kl = he.l; //what about internal loops? + + //the face to remove + var kf = he.l.f; + + //the opposite edge and loop that we will maintain + var oe = he.opp; + var ol = oe.l; + var of = oe.l.f; + + //assign the edges of the given edge to the opposite loop + var che = he; + do { + che.l = ol; + } while ( (che = che.nxt) != he); + + //store the adjacent edges + var ha = he.prv; + var hb = he.nxt; + + var hc = oe.prv; + var hd = oe.nxt; + + //properly set adjacent edges + ha.nxt = hd; + hd.prv = ha; + + hc.nxt = hb; + hb.prv = hc; + + //set the vertex parents correctly + he.v.e = hd; + oe.v.e = hb; + + //assign the edge correctly + if (ol.e == oe){ + ol.e = hc; + } + + //move kf's internal rings to of + for (l in kf.rings()){ + l.f = of; + of.l.push( l ); + } + + //delete the old face from the solid + this.delFace( kf ); + + return this; + } + + //TODO: test with two rings with non-single length, with first argument as length1 + //inverse of lkemr + //introduces a new edge between two loops, forming a single loop + public function lmekr(he0: HalfEdge, he1 : HalfEdge) : HalfEdge { + + //check if the two edges aren't already part of the same loop + if (he0.l == he1.l){ + throw new Exception("HalfEdges are not from different loops!"); + } + + if (he0.l.f != he1.l.f){ + throw new Exception("HalfEdges must be part of the same face!"); + } + + //remove the second loop + var kl : Loop = he1.l; + kl.f.delLoop( kl ); + + //we keep the first loop arg + var l0 = he0.l; + + //for all the edges in loop1, set loop0 as parent + for (he in he1.iter()){ + he.l = l0; + } + + //create two new half edges in loop0, accounting for base case + var e0 : HalfEdge = he0.nxt == he0 ? he0 : new HalfEdge(l0, he0.v); + var e1 : HalfEdge = he1.nxt == he1 ? he1 : new HalfEdge(l0, he1.v); + + e0.mate(e1); + + //using the two new half edges, rejoin the loop + he0.prv.nxt = e0; + he1.prv.nxt = e1; + + e0.prv = he0.prv; + e1.prv = he1.prv; + + he1.prv = e0; + he0.prv = e1; + + e0.nxt = he1; + e1.nxt = he0; + + return e0; + } + + //kill face make ring hole + //we kill the face by making it a ring inside of the face + public function lkfmrh(kf : Face, tf : Face) : Loop { + + //what to do if the face has rings in it? + if (kf.rings().length > 0){ + throw new Exception("Cannot insert a face with rings as a ring of another face!"); + } + + delFace( kf ); + + kf.ol.f = tf; + tf.l.push( kf.ol ); + + return kf.ol; + } + + //TODO: test + //create a face by extracting a ring from a face + public function lmfkrh( ol : Loop ){ + var of = ol.f; + + //remove the original ring from its parent face + of.delLoop( ol ); + + //and add to a new face + var nf = this.addFace(); //what are this faces new neighbors? + nf.addLoop(ol); + + ol.f = nf; + + return nf; + } + + private function addFace() : Face { + return f = f.push( new Face(this) ); + } + + private function delFace( i : Face ) : Solid { + if ( i.s != this) { + throw new Exception("Face is not part of this Solid!"); + } + + f = f.kill(i); + return this; + } + + private function addVertex( pt ) : Vertex { + return v = v.push( new Vertex( pt ) ); + } + + private function delVertex( i : Vertex ) : Solid { + if ( i.e.l.f.s != this) { + throw new Exception("Face is not part of this Solid!"); + } + + v = v.kill(i); + return this; + } + + public function vertices() : Array { + return v.iter().array(); + } + + public function faces() : Array { + return f.iter().array(); + } + + public function loops() : Array { + return faces().fold(function(f : Face, acc : Array){ return acc.concat( f.loops() ); }, []); + } + + public function halfEdges() : Array { + return loops().fold(function(l : Loop, acc : Array){ return acc.concat( l.halfEdges() ); }, []); + } + + public function edges() : Array> { + var m = new IntMap(); + + var a = []; + for (e in halfEdges()){ + if (e.opp == null || m.exists(e.id) || m.exists(e.opp.id)) continue; + m.set( e.id, e ); + m.set( e.opp.id, e.opp ); + a.push( new Pair(e, e.opp) ); + } + + return a; + } + + public function print(){ + return "Solid (" + + vertices().length + " Vertices, " + + faces().length + " Faces, " + + loops().length + " Loops, " + + halfEdges().length + " HalfEdges" + + ")"; + } +} + + +//Key +// +//m = make +//k = kill +//s = split +//j = join +//v = vertex +//e = edge +//f = face +//s = solid +//h = hole +//r = ring \ No newline at end of file diff --git a/src/verb/topo/Split.hx b/src/verb/topo/Split.hx new file mode 100644 index 00000000..de3d547c --- /dev/null +++ b/src/verb/topo/Split.hx @@ -0,0 +1,428 @@ +package verb.topo; + +import verb.topo.Split.PlanePosition; +import verb.topo.Split.PlanePosition; +import verb.topo.Split.PlanePosition; +import verb.topo.Split.EdgePlanePosition; +import verb.core.Exception; +import haxe.ds.IntMap; +import verb.core.Constants; +import verb.core.Vec; +using verb.core.Vec; +import verb.core.NurbsCurveData.Point; +import verb.core.Pair; +import verb.core.Intersect; + +import verb.core.DoublyLinkedListExtensions; +using verb.core.DoublyLinkedListExtensions; + +typedef Plane = { + n : Array, + o : Array +} + +typedef EdgePlanePosition = { + edge : HalfEdge, + pos : PlanePosition +} + +enum PlanePosition { On; Above; Below; } + +@:expose("topo.Split") +class Split { + + public static function solidByPlane( s : Solid, p : Plane ) : Pair { + + // 1. intersect + + var r = intersect(s, p); + + // 2. reduce to coplanar vertices + + var vs = [for (ir in r) + if ( isCrossingEdge(ir.item1) ) + splitEdge(ir.item0, ir.item1).v + else ir.item0.v + ]; + + // 3. classify outgoing vertex edges and insert required null edges + + var nulledges = []; + for (v in vs){ + insertNullEdges( v, classifyVertex( v, p ), nulledges ); + } + + if (nulledges.length == 0) return null; + + // 4. connect the null edges, forming cross-section polygons + + var afaces = []; + connect( nulledges, afaces ); + + // 5. close the resultant solids + + var a = new Solid(); + var b = new Solid(); + + close( afaces, a, b ); + + return new Pair(b,a); + } + + public static function close( afaces : Array, a : Solid, b : Solid ){ + + var s = afaces[0].s; + + var bfaces = [for (f in afaces) s.lmfkrh(f.l)]; + + for (f in afaces) moveFace( f, a ); + for (f in bfaces) moveFace( f, b ); + + cleanup(a, s); + cleanup(b, s); + } + + public static function connect( nulledges : Array, afaces : Array ) : Void { + + //now we have all of the null edges inserted and need to separate the solid + //into two pieces + lexicographicalSort( nulledges ); + + var h0 : HalfEdge; + var h1 : HalfEdge; + + var looseends = []; + + for (ne in nulledges){ + if ( (h0 = canJoin( ne, looseends )) != null ){ + join( h0, ne ); + if (!isLoose(h0.opp, looseends)) cut(h0, afaces); + } + + if ( (h1 = canJoin( ne.opp, looseends )) != null ){ + join( h1, ne.opp ); + if (!isLoose(h1.opp, looseends)) cut(h1, afaces); + } + + if (h0 != null && h1 != null) cut(ne, afaces); + } + + } + + public static function insertNullEdges( v : Vertex, ecs : Array, nulledges : Array ) : Void { + + var s = v.e.l.f.s; + + //find the first in ABOVE seq + var i = nextOfClass( PlanePosition.Above, ecs, 0 ); + + //there's no above edge in the seq (all below), continue + if (i == -1) return; + + //if all of the edges are of the same type, just continue + if (nextOfClass( PlanePosition.Below, ecs, 0 ) == -1) return; + + var start = ecs[i].edge; + var head = start; + var tail = start; + var el = ecs.length; + + while(true){ + + //find the end of the ABOVE sequence + while (ecs[i].pos == PlanePosition.Above){ + tail = ecs[i].edge; + i = (i + 1) % el; + } + + //insert a null edge + s.lmev( head, tail.opp.nxt, head.v.pt.copy() ); + nulledges.push( head.prv ); + + //find the next start sequence and assign to head + i = nextOfClass( PlanePosition.Above, ecs, i ); + + //there is no other above edge, we're done with this vertex + if (i == -1) break; + + head = ecs[i].edge; + + //we've come back to the beginning (should never happen) + if (head == start) break; + } + } + + public static function classifyVertex( v : Vertex, p : Plane ) : Array { + + var ecs = new Array(); + + // 1. classify vertex edges based on opposite vertex's signed distance from the cutting plane + for (e in v.halfEdges()){ + ecs.push({ edge: e, pos : classify(e, p) }); + + //for each edge, we also need to check the sector width - i.e. the angle + //bisector between two adjacent edges. If more than 180, we bisect the edge + if ( wideSector(e) ){ + ecs.push({ edge: e, pos : classifyBisector(e, p) }); + } + } + + // 2. now, for each "on" edge, we need to determine its face - is this face aligned with the plane normal? + //if so, + // if aligned with plane normal (dot(fn, sp) > 0), BELOW, along with the next edge + // else ABOVE + var el = ecs.length; + for (i in 0...el){ + var ep = ecs[i]; + if (ep.pos == PlanePosition.On){ + var nc = reclassifyCoplanarSector(ep.edge, p); + ecs[i].pos = nc; + ecs[(i+1) % el].pos = nc; + } + } + + // 3. now, go through all of the edges, search for ON edges, reclassify them based on rules on pg 245 + for (i in 0...el){ + var ep = ecs[i]; + if (ep.pos == PlanePosition.On){ + + var a = i == 0 ? el-1 : i-1; + var b = (i+1) % el; + + var prv = ecs[a].pos; + var nxt = ecs[b].pos; + + if ( prv == PlanePosition.Above && nxt == PlanePosition.Above ){ + ep.pos = PlanePosition.Below; + } else if ( prv == PlanePosition.Below && nxt == PlanePosition.Above ) { + ep.pos = PlanePosition.Below; + } else if ( prv == PlanePosition.Above && nxt == PlanePosition.Below ) { + ep.pos = PlanePosition.Below; + } else if ( prv == PlanePosition.Below && nxt == PlanePosition.Below ) { + ep.pos = PlanePosition.Above; + } else { + throw new Exception("Double On edge encountered!"); + } + } + } + + return ecs; + } + + private static function moveFace( f : Face, s : Solid ) : Void { + if (f.s == s ) return; + + f.s.f = f.s.f.kill( f ); + s.f = s.f.push( f ); + f.s = s; + + for (nf in f.neighbors()){ + moveFace( nf, s ); + } + } + + //move vertices of ks to s by traversing the faces of s + private static function cleanup( s : Solid, ks : Solid ) : Void { + var memo = new IntMap(); + + for (f in s.f.iter()){ + for (l in f.l.iter()){ + for (v in l.vertices()){ + if (!memo.exists( v.id )){ + memo.set( v.id, v ); + ks.v = ks.v.kill( v ); + s.v = s.v.push( v ); + } + } + } + } + } + + public static function canJoin( e : HalfEdge, looseends : Array ) : HalfEdge { + for (i in 0...looseends.length){ + if ( neighbor( e, looseends[i]) ){ // + var r = looseends[i]; + looseends.splice(i, 1); + return r; + } + } + + looseends.push(e); + return null; + } + + public static function neighbor( e0 : HalfEdge, e1 : HalfEdge ) : Bool { + return e0.l.f == e1.l.f; // || e0.opp == e1; //opposite orientation + } + + public static function isLoose( e : HalfEdge, le : Array ) : Bool { + return le.indexOf(e) != -1; + } + + //join a new null edge into the expanding slice polygon + public static function join( e0 : HalfEdge, e1 : HalfEdge ) { + //get the face of e0 + var of = e0.l.f; + var nf : Face = null; + + //get the solid of e0 + var s = e0.l.f.s; + + //if e0 and e1 have the same loop, do lmef + if (e0.l == e1.l){ + if (e0.prv.prv != e1){ + nf = s.lmef( e0, e1.nxt ); + } + } + //otherwise, make edge kill ring + else { + s.lmekr( e0, e1.nxt ); + } + + if (e0.nxt.nxt != e1){ + s.lmef( e1, e0.nxt ); + if (nf != null && of.l.nxt != of.l){ + trace('PANIC!'); + //TODO - move internal rings to the new face as appropriate + //s.laringmv(of, nf); + } + } + } + + //remove a null edge + public static function cut( e : HalfEdge, faces : Array ) { + if (e.l == e.opp.l){ + faces.push(e.l.f); + e.l.f.s.lkemr( e ); + } else { + e.l.f.s.lkef( e ); + } + } + + public static function lexicographicalSort( es : Array ){ + es.sort(function(a : HalfEdge, b : HalfEdge){ + var ap = a.v.pt; + var bp = b.v.pt; + + if (ap[0] < bp[0]) + return -1; + else if (ap[0] > bp[0]) + return 1; + else if (ap[1] < bp[1]) + return -1; + else if (ap[1] > bp[1]) + return 1; + else if (ap[2] < bp[2]) + return -1; + else if (ap[2] > bp[2]) + return 1; + + return 0; + }); + } + + private static function nextOfClass( cl : PlanePosition, ecs : Array, start : Int ) : Int { + var i = start; + var head : EdgePlanePosition = null; + while (i < ecs.length){ + if ( ecs[i].pos == cl ) { + head = ecs[i]; + break; + } + i++; + } + return head != null ? i : -1; + } + + public static function wideSector ( e : HalfEdge ) : Bool { + var n = e.l.f.normal(); + + var a = e.nxt.v.pt.sub(e.v.pt).normalized(); + var b = e.prv.v.pt.sub(e.v.pt).normalized(); + + return a.signedAngleBetween(b, n) > Math.PI; + } + + public static function classifyBisector( e : HalfEdge, p : Plane ) : PlanePosition { + return classifyPoint( Vec.mul( 0.5, e.nxt.v.pt.add(e.prv.v.pt) ), p); + } + + private static function reclassifyCoplanarSector( e : HalfEdge, p : Plane ) : PlanePosition { + + var n = e.l.f.normal(); //TODO: cache me + var n1 = e.opp.l.f.normal(); //TODO: cache me + + var ndc = n.dot(p.n); + var ndc1 = n1.dot(p.n); + + var eps2 = Constants.EPSILON * Constants.EPSILON; + + if ( Math.abs(ndc - 1.0) < eps2 || Math.abs(ndc1 - 1.0) < eps2 ) { + return PlanePosition.Below; + } + + if ( Math.abs(ndc + 1.0) < eps2 || Math.abs(ndc1 + 1.0) < eps2 ) { + return PlanePosition.Above; + } + + return PlanePosition.On; + } + + public static function classify(e : HalfEdge, p : Plane) : PlanePosition { + return classifyPoint( e.nxt.v.pt, p ); + } + + private static function classifyPoint(pt : Array, p : Plane) : PlanePosition { + var s = pt.sub( p.o ).dot( p.n ); + + if (Math.abs(s) < Constants.EPSILON) return PlanePosition.On; + if (s > 0.0) return PlanePosition.Above else return PlanePosition.Below; + } + + private static function intersect( s : Solid, p : Plane ) : Array> { + var is = []; + for (e in s.edges()){ + + var he : HalfEdge = e.item0; + + var r = verb.core.Intersect.segmentAndPlane(he.v.pt, he.nxt.v.pt, p.o, p.n ); + + if (r == null) continue; + + if (r.p > 1.0 - Constants.EPSILON) { + r.p = 0.0; + he = he.nxt; + } + + is.push( new Pair(he, r.p) ); + } + return is; + } + + public static function splitEdge( e : HalfEdge, p : Float ) : HalfEdge { + var s = e.l.f.s; + + //given the intersecting halfedge, is there a way to use the euler operators? + var pt0 = pointOnHalfEdge( e, p ); + var pt1 = pt0.copy(); + + //insert a coincident vertices along the HalfEdge + var nv = s.lmev( e, e.opp.nxt, pt1 ); + + return nv.e; + } + + private static function isCrossingEdge( p : Float ){ + return p < 1.0 - Constants.EPSILON && p > Constants.EPSILON; + } + + private static function pointOnHalfEdge( e : HalfEdge, p : Float ) : Point { + return Vec.lerp( p, e.nxt.v.pt, e.v.pt ); + } + + public static function intersectionPoints( s : Solid, p : Plane ) : Array { + return [ for (i in intersect(s,p)) pointOnHalfEdge( i.item0, i.item1 ) ]; + } +} + + diff --git a/src/verb/topo/Tess2.hx b/src/verb/topo/Tess2.hx new file mode 100644 index 00000000..1a7d841c --- /dev/null +++ b/src/verb/topo/Tess2.hx @@ -0,0 +1,3699 @@ +/* +** SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008) +** Copyright (C) [dates of first publication] Silicon Graphics, Inc. +** All Rights Reserved. +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +** of the Software, and to permit persons to whom the Software is furnished to do so, +** subject to the following conditions: +** +** The above copyright notice including the dates of first publication and either this +** permission notice or a reference to http://oss.sgi.com/projects/FreeB/ shall be +** included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +** INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +** PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON GRAPHICS, INC. +** BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +** OR OTHER DEALINGS IN THE SOFTWARE. +** +** Except as contained in this notice, the name of Silicon Graphics, Inc. shall not +** be used in advertising or otherwise to promote the sale, use or other dealings in +** this Software without prior written authorization from Silicon Graphics, Inc. +*/ +/* +** Author: Mikko Mononen, Aug 2013. +** The code is based on GLU libtess by Eric Veach, July 1994 +*/ +/* +** Ported to Haxe: Peter Boyer, + */ + +package verb.topo; + +import verb.core.ArrayExtensions; +using verb.core.ArrayExtensions; + +@:expose("topo.Tess2Options") +class Tess2Options { + + public var windingRule = Tess2.WINDING_ODD; + public var elementType = Tess2.POLYGONS; + public var polySize : Int = 3; + public var vertexSize : Int = 2; + public var normal : Array = [0.0,0.0,1.0]; + public var debug : Bool = false; + public var contours : Array> = []; + + public function new(){ + + } +} + +@:expose("topo.Tess2Result") +class Tess2Result { + public var vertices : Array; + public var vertexIndices : Array; + public var vertexCount : Int; + public var elements : Array; + public var elementCount : Int; + public var mesh : TESSmesh; + + public function new(vertices, vertexIndices, vertexCount, elements, elementCount, mesh){ + this.vertices = vertices; + this.vertexIndices = vertexIndices; + this.vertexCount = vertexCount; + this.elements = elements; + this.elementCount = elementCount; + this.mesh = mesh; + } +} + +@:expose("topo.Tess2") +class Tess2 { + + public static var WINDING_ODD = 0; + public static var WINDING_NONZERO = 1; + public static var WINDING_POSITIVE = 2; + public static var WINDING_NEGATIVE = 3; + public static var WINDING_ABS_GEQ_TWO = 4; + + public static var POLYGONS = 0; + public static var CONNECTED_POLYGONS = 1; + public static var BOUNDARY_CONTOURS = 2; + + public static function tessellate(opts : Tess2Options) : Tess2Result { + var tess = new Tessellator(); + for (i in 0...opts.contours.length){ +// for (var i = 0; i < opts.contours.length; i++) { + tess.addContour(opts.vertexSize, opts.contours[i]); + } + + tess.tessellate(opts.windingRule, opts.elementType, opts.polySize, opts.vertexSize, opts.normal); + + return new Tess2Result( + tess.vertices, + tess.vertexIndices, + tess.vertexCount, + tess.elements, + tess.elementCount, + opts.debug ? tess.mesh : null + ); + } + + public static function assert(cond) { + if (!cond) { + throw "Assertion Failed!"; + } + } +} + +/* The mesh structure is similar in spirit, notation, and operations +* to the "quad-edge" structure (see L. Guibas and J. Stolfi, Primitives +* for the manipulation of general subdivisions and the computation of +* Voronoi diagrams, ACM Transactions on Graphics, 4(2):74-123, April 1985). +* For a simplified description, see the course notes for CS348a, +* "Mathematical Foundations of Computer Graphics", available at the +* Stanford bookstore (and taught during the fall quarter). +* The implementation also borrows a tiny subset of the graph-based approach +* use in Mantyla's Geometric Work Bench (see M. Mantyla, An Introduction +* to Sold Modeling, Computer Science Press, Rockville, Maryland, 1988). +* +* The fundamental data structure is the "half-edge". Two half-edges +* go together to make an edge, but they point in opposite directions. +* Each half-edge has a pointer to its mate (the "symmetric" half-edge Sym), +* its origin vertex (Org), the face on its left side (Lface), and the +* adjacent half-edges in the CCW direction around the origin vertex +* (Onext) and around the left face (Lnext). There is also a "next" +* pointer for the global edge list (see below). +* +* The notation used for mesh navigation: +* Sym = the mate of a half-edge (same edge, but opposite direction) +* Onext = edge CCW around origin vertex (keep same origin) +* Dnext = edge CCW around destination vertex (keep same dest) +* Lnext = edge CCW around left face (dest becomes new origin) +* Rnext = edge CCW around right face (origin becomes new dest) +* +* "prev" means to substitute CW for CCW in the definitions above. +* +* The mesh keeps global lists of all vertices, faces, and edges, +* stored as doubly-linked circular lists with a dummy header node. +* The mesh stores pointers to these dummy headers (vHead, fHead, eHead). +* +* The circular edge list is special; since half-edges always occur +* in pairs (e and e->Sym), each half-edge stores a pointer in only +* one direction. Starting at eHead and following the e->next pointers +* will visit each *edge* once (ie. e or e->Sym, but not both). +* e->Sym stores a pointer in the opposite direction, thus it is +* always true that e->Sym->next->Sym->next == e. +* +* Each vertex has a pointer to next and previous vertices in the +* circular list, and a pointer to a half-edge with this vertex as +* the origin (NULL if this is the dummy header). There is also a +* field "data" for client data. +* +* Each face has a pointer to the next and previous faces in the +* circular list, and a pointer to a half-edge with this face as +* the left face (NULL if this is the dummy header). There is also +* a field "data" for client data. +* +* Note that what we call a "face" is really a loop; faces may consist +* of more than one loop (ie. not simply connected), but there is no +* record of this in the data structure. The mesh may consist of +* several disconnected regions, so it may not be possible to visit +* the entire mesh by starting at a half-edge and traversing the edge +* structure. +* +* The mesh does NOT support isolated vertices; a vertex is deleted along +* with its last edge. Similarly when two faces are merged, one of the +* faces is deleted (see tessMeshDelete below). For mesh operations, +* all face (loop) and vertex pointers must not be NULL. However, once +* mesh manipulation is finished, TESSmeshZapFace can be used to delete +* faces of the mesh, one at a time. All external faces can be "zapped" +* before the mesh is returned to the client; then a NULL face indicates +* a region which is not part of the output polygon. +*/ + +class TESSvertex { + + public var next : TESSvertex; + public var prev : TESSvertex; + public var anEdge : TESShalfEdge; + + public var coords : Array; + public var s : Float; + public var t : Float; + public var pqHandle : Int; + public var n : Int; + public var idx : Int; + + public function new() { + this.next = null; /* next vertex (never NULL) */ + this.prev = null; /* previous vertex (never NULL) */ + this.anEdge = null; /* a half-edge with this origin */ + + /* Internal data (keep hidden) */ + this.coords = [0,0,0]; /* vertex location in 3D */ + this.s = 0.0; + this.t = 0.0; /* projection onto the sweep plane */ + this.pqHandle = 0; /* to allow deletion from priority queue */ + this.n = 0; /* to allow identify unique vertices */ + this.idx = 0; /* to allow map result to original verts */ + } +} + +class TESSface { + + public var next : TESSface; + public var prev : TESSface; + public var anEdge : TESShalfEdge; + +// public var trail; + public var n : Int; + public var marked : Bool; + public var inside : Bool; + + public function new () { + this.next = null; /* next face (never NULL) */ + this.prev = null; /* previous face (never NULL) */ + this.anEdge = null; /* a half edge with this left face */ + + /* Internal data (keep hidden) */ +// this.trail = null; /* "stack" for conversion to strips */ + this.n = 0; /* to allow identiy unique faces */ + this.marked = false; /* flag for conversion to strips */ + this.inside = false; /* this face is in the polygon interior */ + } +} + +class TESShalfEdge { + + public var next : TESShalfEdge; + public var Sym : TESShalfEdge; + public var Onext : TESShalfEdge; + public var Lnext : TESShalfEdge; + public var Org : TESSvertex; + public var Lface : TESSface; + + public var activeRegion : ActiveRegion; + public var winding : Int; + public var side : Int; + + public function new(side) { + this.next = null; /* doubly-linked list (prev==Sym->next) */ + this.Sym = null; /* same edge, opposite direction */ + this.Onext = null; /* next edge CCW around origin */ + this.Lnext = null; /* next edge CCW around left face */ + this.Org = null; /* origin vertex (Overtex too long) */ + this.Lface = null; /* left face */ + + /* Internal data (keep hidden) */ + this.activeRegion = null; /* a region with this upper edge (sweep.c) */ + this.winding = 0; /* change in winding number when crossing + from the right face to the left face */ + this.side = side; + } + + public function getRface() : TESSface { return this.Sym.Lface; } + public function setRface(v) : Void { this.Sym.Lface = v; } + + public function getDst() : TESSvertex { return this.Sym.Org; } + public function setDst(v) : Void { this.Sym.Org = v; } + + public function getOprev() { return this.Sym.Lnext; } + public function setOprev(v) : Void { this.Sym.Lnext = v; } + + public function getLprev() : TESShalfEdge { return this.Onext.Sym; } + public function setLprev(v) : Void { this.Onext.Sym = v; } + + public function getDprev() : TESShalfEdge { return this.Lnext.Sym; } + public function setDprev(v) : Void { this.Lnext.Sym = v; } + + public function getRprev() : TESShalfEdge { return this.Sym.Onext; } + public function setRprev(v) : Void { this.Sym.Onext = v; } + + public function getDnext() { return /*this.getRprev()*/this.Sym.Onext.Sym; } /* 3 pointers */ + public function setDnext(v) { /*this.getRprev()*/this.Sym.Onext.Sym = v; } /* 3 pointers */ // + + public function getRnext() { return /*this.getOprev()*/this.Sym.Lnext.Sym; } /* 3 pointers */ + public function setRnext(v) { /*this.getOprev()*/this.Sym.Lnext.Sym = v; } /* 3 pointers */ + +} + +class TESSmesh { + + public var vHead : TESSvertex; /* dummy header for vertex list */ + public var fHead : TESSface; /* dummy header for face list */ + public var eHead : TESShalfEdge; /* dummy header for edge list */ + public var eHeadSym : TESShalfEdge; /* and its symmetric counterpart */ + + public function new () { + var v = new TESSvertex(); + var f = new TESSface(); + var e = new TESShalfEdge(0); + var eSym = new TESShalfEdge(1); + + v.next = v.prev = v; + v.anEdge = null; + + f.next = f.prev = f; + f.anEdge = null; +// f.trail = null; + f.marked = false; + f.inside = false; + + e.next = e; + e.Sym = eSym; + e.Onext = null; + e.Lnext = null; + e.Org = null; + e.Lface = null; + e.winding = 0; + e.activeRegion = null; + + eSym.next = eSym; + eSym.Sym = e; + eSym.Onext = null; + eSym.Lnext = null; + eSym.Org = null; + eSym.Lface = null; + eSym.winding = 0; + eSym.activeRegion = null; + + this.vHead = v; /* dummy header for vertex list */ + this.fHead = f; /* dummy header for face list */ + this.eHead = e; /* dummy header for edge list */ + this.eHeadSym = eSym; /* and its symmetric counterpart */ + } + + /* The mesh operations below have three motivations: completeness, + * convenience, and efficiency. The basic mesh operations are MakeEdge, + * Splice, and Delete. All the other edge operations can be implemented + * in terms of these. The other operations are provided for convenience + * and/or efficiency. + * + * When a face is split or a vertex is added, they are inserted into the + * global list *before* the existing vertex or face (ie. e->Org or e->Lface). + * This makes it easier to process all vertices or faces in the global lists + * without worrying about processing the same data twice. As a convenience, + * when a face is split, the "inside" flag is copied from the old face. + * Other internal data (v->data, v->activeRegion, f->data, f->marked, + * f->trail, e->winding) is set to zero. + * + * ********************** Basic Edge Operations ************************** + * + * tessMeshMakeEdge( mesh ) creates one edge, two vertices, and a loop. + * The loop (face) consists of the two new half-edges. + * + * tessMeshSplice( eOrg, eDst ) is the basic operation for changing the + * mesh connectivity and topology. It changes the mesh so that + * eOrg->Onext <- OLD( eDst->Onext ) + * eDst->Onext <- OLD( eOrg->Onext ) + * where OLD(...) means the value before the meshSplice operation. + * + * This can have two effects on the vertex structure: + * - if eOrg->Org != eDst->Org, the two vertices are merged together + * - if eOrg->Org == eDst->Org, the origin is split into two vertices + * In both cases, eDst->Org is changed and eOrg->Org is untouched. + * + * Similarly (and independently) for the face structure, + * - if eOrg->Lface == eDst->Lface, one loop is split into two + * - if eOrg->Lface != eDst->Lface, two distinct loops are joined into one + * In both cases, eDst->Lface is changed and eOrg->Lface is unaffected. + * + * tessMeshDelete( eDel ) removes the edge eDel. There are several cases: + * if (eDel->Lface != eDel->Rface), we join two loops into one; the loop + * eDel->Lface is deleted. Otherwise, we are splitting one loop into two; + * the newly created loop will contain eDel->Dst. If the deletion of eDel + * would create isolated vertices, those are deleted as well. + * + * ********************** Other Edge Operations ************************** + * + * tessMeshAddEdgeVertex( eOrg ) creates a new edge eNew such that + * eNew == eOrg->Lnext, and eNew->Dst is a newly created vertex. + * eOrg and eNew will have the same left face. + * + * tessMeshSplitEdge( eOrg ) splits eOrg into two edges eOrg and eNew, + * such that eNew == eOrg->Lnext. The new vertex is eOrg->Dst == eNew->Org. + * eOrg and eNew will have the same left face. + * + * tessMeshConnect( eOrg, eDst ) creates a new edge from eOrg->Dst + * to eDst->Org, and returns the corresponding half-edge eNew. + * If eOrg->Lface == eDst->Lface, this splits one loop into two, + * and the newly created loop is eNew->Lface. Otherwise, two disjoint + * loops are merged into one, and the loop eDst->Lface is destroyed. + * + * ************************ Other Operations ***************************** + * + * tessMeshNewMesh() creates a new mesh with no edges, no vertices, + * and no loops (what we usually call a "face"). + * + * tessMeshUnion( mesh1, mesh2 ) forms the union of all structures in + * both meshes, and returns the new mesh (the old meshes are destroyed). + * + * tessMeshDeleteMesh( mesh ) will free all storage for any valid mesh. + * + * tessMeshZapFace( fZap ) destroys a face and removes it from the + * global face list. All edges of fZap will have a NULL pointer as their + * left face. Any edges which also have a NULL pointer as their right face + * are deleted entirely (along with any isolated vertices this produces). + * An entire mesh can be deleted by zapping its faces, one at a time, + * in any order. Zapped faces cannot be used in further mesh operations! + * + * tessMeshCheckMesh( mesh ) checks a mesh for self-consistency. + */ + + + + /* MakeEdge creates a new pair of half-edges which form their own loop. + * No vertex or face structures are allocated, but these must be assigned + * before the current edge operation is completed. + */ + //static TESShalfEdge *MakeEdge( TESSmesh* mesh, TESShalfEdge *eNext ) + public function makeEdge_( eNext : TESShalfEdge ) : TESShalfEdge { + var e = new TESShalfEdge(0); + var eSym = new TESShalfEdge(1); + + /* Make sure eNext points to the first edge of the edge pair */ + if( eNext.Sym.side < eNext.side ) { eNext = eNext.Sym; } + + /* Insert in circular doubly-linked list before eNext. + * Note that the prev pointer is stored in Sym->next. + */ + var ePrev = eNext.Sym.next; + eSym.next = ePrev; + ePrev.Sym.next = e; + e.next = eNext; + eNext.Sym.next = eSym; + + e.Sym = eSym; + e.Onext = e; + e.Lnext = eSym; + e.Org = null; + e.Lface = null; + e.winding = 0; + e.activeRegion = null; + + eSym.Sym = e; + eSym.Onext = eSym; + eSym.Lnext = e; + eSym.Org = null; + eSym.Lface = null; + eSym.winding = 0; + eSym.activeRegion = null; + + return e; + } + + /* Splice( a, b ) is best described by the Guibas/Stolfi paper or the + * CS348a notes (see mesh.h). Basically it modifies the mesh so that + * a->Onext and b->Onext are exchanged. This can have various effects + * depending on whether a and b belong to different face or vertex rings. + * For more explanation see tessMeshSplice() below. + */ + //static void Splice( TESShalfEdge *a, TESShalfEdge *b ) + public function splice_(a : TESShalfEdge, b : TESShalfEdge) : Void{ + var aOnext = a.Onext; + var bOnext = b.Onext; + aOnext.Sym.Lnext = b; + bOnext.Sym.Lnext = a; + a.Onext = bOnext; + b.Onext = aOnext; + } + + /* MakeVertex( newVertex, eOrig, vNext ) attaches a new vertex and makes it the + * origin of all edges in the vertex loop to which eOrig belongs. "vNext" gives + * a place to insert the new vertex in the global vertex list. We insert + * the new vertex *before* vNext so that algorithms which walk the vertex + * list will not see the newly created vertices. + */ + //static void MakeVertex( TESSvertex *newVertex, TESShalfEdge *eOrig, TESSvertex *vNext ) + public function makeVertex_(newVertex : TESSvertex, eOrig : TESShalfEdge, vNext : TESSvertex) : Void { + var vNew = newVertex; + Tess2.assert(vNew != null); + + /* insert in circular doubly-linked list before vNext */ + var vPrev = vNext.prev; + vNew.prev = vPrev; + vPrev.next = vNew; + vNew.next = vNext; + vNext.prev = vNew; + + vNew.anEdge = eOrig; + /* leave coords, s, t undefined */ + + /* fix other edges on this vertex loop */ + var e = eOrig; + do { + e.Org = vNew; + e = e.Onext; + } while(e != eOrig); + } + + /* MakeFace( newFace, eOrig, fNext ) attaches a new face and makes it the left + * face of all edges in the face loop to which eOrig belongs. "fNext" gives + * a place to insert the new face in the global face list. We insert + * the new face *before* fNext so that algorithms which walk the face + * list will not see the newly created faces. + */ + //static void MakeFace( TESSface *newFace, TESShalfEdge *eOrig, TESSface *fNext ) + public function makeFace_(newFace : TESSface, eOrig : TESShalfEdge, fNext : TESSface ) : Void { + var fNew = newFace; + Tess2.assert(fNew != null); + + /* insert in circular doubly-linked list before fNext */ + var fPrev = fNext.prev; + fNew.prev = fPrev; + fPrev.next = fNew; + fNew.next = fNext; + fNext.prev = fNew; + + fNew.anEdge = eOrig; +// fNew.trail = null; + fNew.marked = false; + + /* The new face is marked "inside" if the old one was. This is a + * convenience for the common case where a face has been split in two. + */ + fNew.inside = fNext.inside; + + /* fix other edges on this face loop */ + var e = eOrig; + do { + e.Lface = fNew; + e = e.Lnext; + } while(e != eOrig); + } + + /* KillEdge( eDel ) destroys an edge (the half-edges eDel and eDel->Sym), + * and removes from the global edge list. + */ + //static void KillEdge( TESSmesh *mesh, TESShalfEdge *eDel ) + public function killEdge_(eDel : TESShalfEdge) : Void { + /* Half-edges are allocated in pairs, see EdgePair above */ + if( eDel.Sym.side < eDel.side ) { eDel = eDel.Sym; } + + /* delete from circular doubly-linked list */ + var eNext = eDel.next; + var ePrev = eDel.Sym.next; + eNext.Sym.next = ePrev; + ePrev.Sym.next = eNext; + } + + /* KillVertex( vDel ) destroys a vertex and removes it from the global + * vertex list. It updates the vertex loop to point to a given new vertex. + */ + //static void KillVertex( TESSmesh *mesh, TESSvertex *vDel, TESSvertex *newOrg ) + public function killVertex_(vDel : TESSvertex, newOrg : TESSvertex) : Void { + var eStart = vDel.anEdge; + /* change the origin of all affected edges */ + var e = eStart; + do { + e.Org = newOrg; + e = e.Onext; + } while(e != eStart); + + /* delete from circular doubly-linked list */ + var vPrev = vDel.prev; + var vNext = vDel.next; + vNext.prev = vPrev; + vPrev.next = vNext; + } + + /* KillFace( fDel ) destroys a face and removes it from the global face + * list. It updates the face loop to point to a given new face. + */ + //static void KillFace( TESSmesh *mesh, TESSface *fDel, TESSface *newLface ) + public function killFace_(fDel : TESSface, newLface : TESSface) : Void { + var eStart = fDel.anEdge; + + /* change the left face of all affected edges */ + var e = eStart; + do { + e.Lface = newLface; + e = e.Lnext; + } while(e != eStart); + + /* delete from circular doubly-linked list */ + var fPrev = fDel.prev; + var fNext = fDel.next; + fNext.prev = fPrev; + fPrev.next = fNext; + } + + /****************** Basic Edge Operations **********************/ + + /* tessMeshMakeEdge creates one edge, two vertices, and a loop (face). + * The loop consists of the two new half-edges. + */ + //TESShalfEdge *tessMeshMakeEdge( TESSmesh *mesh ) + public function makeEdge() : TESShalfEdge { + var newVertex1 = new TESSvertex(); + var newVertex2 = new TESSvertex(); + var newFace = new TESSface(); + var e = this.makeEdge_( this.eHead); + this.makeVertex_( newVertex1, e, this.vHead ); + this.makeVertex_( newVertex2, e.Sym, this.vHead ); + this.makeFace_( newFace, e, this.fHead ); + return e; + } + + /* tessMeshSplice( eOrg, eDst ) is the basic operation for changing the + * mesh connectivity and topology. It changes the mesh so that + * eOrg->Onext <- OLD( eDst->Onext ) + * eDst->Onext <- OLD( eOrg->Onext ) + * where OLD(...) means the value before the meshSplice operation. + * + * This can have two effects on the vertex structure: + * - if eOrg->Org != eDst->Org, the two vertices are merged together + * - if eOrg->Org == eDst->Org, the origin is split into two vertices + * In both cases, eDst->Org is changed and eOrg->Org is untouched. + * + * Similarly (and independently) for the face structure, + * - if eOrg->Lface == eDst->Lface, one loop is split into two + * - if eOrg->Lface != eDst->Lface, two distinct loops are joined into one + * In both cases, eDst->Lface is changed and eOrg->Lface is unaffected. + * + * Some special cases: + * If eDst == eOrg, the operation has no effect. + * If eDst == eOrg->Lnext, the new face will have a single edge. + * If eDst == eOrg->Lprev, the old face will have a single edge. + * If eDst == eOrg->Onext, the new vertex will have a single edge. + * If eDst == eOrg->Oprev, the old vertex will have a single edge. + */ + //int tessMeshSplice( TESSmesh* mesh, TESShalfEdge *eOrg, TESShalfEdge *eDst ) + public function splice(eOrg : TESShalfEdge, eDst : TESShalfEdge) : Void { + var joiningLoops = false; + var joiningVertices = false; + + if( eOrg == eDst ) return; + + if( eDst.Org != eOrg.Org ) { + /* We are merging two disjoint vertices -- destroy eDst->Org */ + joiningVertices = true; + this.killVertex_( eDst.Org, eOrg.Org ); + } + if( eDst.Lface != eOrg.Lface ) { + /* We are connecting two disjoint loops -- destroy eDst->Lface */ + joiningLoops = true; + this.killFace_( eDst.Lface, eOrg.Lface ); + } + + /* Change the edge structure */ + this.splice_( eDst, eOrg ); + + if( ! joiningVertices ) { + var newVertex = new TESSvertex(); + + /* We split one vertex into two -- the new vertex is eDst->Org. + * Make sure the old vertex points to a valid half-edge. + */ + this.makeVertex_( newVertex, eDst, eOrg.Org ); + eOrg.Org.anEdge = eOrg; + } + if( ! joiningLoops ) { + var newFace = new TESSface(); + + /* We split one loop into two -- the new loop is eDst->Lface. + * Make sure the old face points to a valid half-edge. + */ + this.makeFace_( newFace, eDst, eOrg.Lface ); + eOrg.Lface.anEdge = eOrg; + } + } + + /* tessMeshDelete( eDel ) removes the edge eDel. There are several cases: + * if (eDel->Lface != eDel->Rface), we join two loops into one; the loop + * eDel->Lface is deleted. Otherwise, we are splitting one loop into two; + * the newly created loop will contain eDel->Dst. If the deletion of eDel + * would create isolated vertices, those are deleted as well. + * + * This function could be implemented as two calls to tessMeshSplice + * plus a few calls to memFree, but this would allocate and delete + * unnecessary vertices and faces. + */ + //int tessMeshDelete( TESSmesh *mesh, TESShalfEdge *eDel ) + public function delete(eDel : TESShalfEdge) : Void { + var eDelSym = eDel.Sym; + var joiningLoops = false; + + /* First step: disconnect the origin vertex eDel->Org. We make all + * changes to get a consistent mesh in this "intermediate" state. + */ + if( eDel.Lface != eDel.getRface() ) { + /* We are joining two loops into one -- remove the left face */ + joiningLoops = true; + this.killFace_( eDel.Lface, eDel.getRface() ); + } + + if( eDel.Onext == eDel ) { + this.killVertex_( eDel.Org, null ); + } else { + /* Make sure that eDel->Org and eDel->Rface point to valid half-edges */ + eDel.getRface().anEdge = eDel.getOprev(); + eDel.Org.anEdge = eDel.Onext; + + this.splice_( eDel, eDel.getOprev() ); + if( ! joiningLoops ) { + var newFace = new TESSface(); + + /* We are splitting one loop into two -- create a new loop for eDel. */ + this.makeFace_( newFace, eDel, eDel.Lface ); + } + } + + /* Claim: the mesh is now in a consistent state, except that eDel->Org + * may have been deleted. Now we disconnect eDel->Dst. + */ + if( eDelSym.Onext == eDelSym ) { + this.killVertex_( eDelSym.Org, null ); + this.killFace_( eDelSym.Lface, null ); + } else { + /* Make sure that eDel->Dst and eDel->Lface point to valid half-edges */ + eDel.Lface.anEdge = eDelSym.getOprev(); + eDelSym.Org.anEdge = eDelSym.Onext; + this.splice_( eDelSym, eDelSym.getOprev() ); + } + + /* Any isolated vertices or faces have already been freed. */ + this.killEdge_( eDel ); + } + + /******************** Other Edge Operations **********************/ + + /* All these routines can be implemented with the basic edge + * operations above. They are provided for convenience and efficiency. + */ + + + /* tessMeshAddEdgeVertex( eOrg ) creates a new edge eNew such that + * eNew == eOrg->Lnext, and eNew->Dst is a newly created vertex. + * eOrg and eNew will have the same left face. + */ + //TESShalfEdge *tessMeshAddEdgeVertex( TESSmesh *mesh, TESShalfEdge *eOrg ); + public function addEdgeVertex(eOrg : TESShalfEdge) : TESShalfEdge { + var eNew = this.makeEdge_( eOrg ); + var eNewSym = eNew.Sym; + + /* Connect the new edge appropriately */ + this.splice_( eNew, eOrg.Lnext ); + + /* Set the vertex and face information */ + eNew.Org = eOrg.getDst(); + + var newVertex = new TESSvertex(); + this.makeVertex_( newVertex, eNewSym, eNew.Org ); + + eNew.Lface = eNewSym.Lface = eOrg.Lface; + + return eNew; + } + + /* tessMeshSplitEdge( eOrg ) splits eOrg into two edges eOrg and eNew, + * such that eNew == eOrg->Lnext. The new vertex is eOrg->Dst == eNew->Org. + * eOrg and eNew will have the same left face. + */ + //TESShalfEdge *tessMeshSplitEdge( TESSmesh *mesh, TESShalfEdge *eOrg ); + public function splitEdge( eOrg : TESShalfEdge ) : TESShalfEdge{ + var tempHalfEdge = this.addEdgeVertex( eOrg ); + var eNew = tempHalfEdge.Sym; + + /* Disconnect eOrg from eOrg->Dst and connect it to eNew->Org */ + this.splice_( eOrg.Sym, eOrg.Sym.getOprev() ); + this.splice_( eOrg.Sym, eNew ); + + /* Set the vertex and face information */ + eOrg.setDst( eNew.Org ); + eNew.getDst().anEdge = eNew.Sym; /* may have pointed to eOrg->Sym */ + eNew.setRface( eOrg.getRface() ); + eNew.winding = eOrg.winding; /* copy old winding information */ + eNew.Sym.winding = eOrg.Sym.winding; + + return eNew; + } + + /* tessMeshConnect( eOrg, eDst ) creates a new edge from eOrg->Dst + * to eDst->Org, and returns the corresponding half-edge eNew. + * If eOrg->Lface == eDst->Lface, this splits one loop into two, + * and the newly created loop is eNew->Lface. Otherwise, two disjoint + * loops are merged into one, and the loop eDst->Lface is destroyed. + * + * If (eOrg == eDst), the new face will have only two edges. + * If (eOrg->Lnext == eDst), the old face is reduced to a single edge. + * If (eOrg->Lnext->Lnext == eDst), the old face is reduced to two edges. + */ + + //TESShalfEdge *tessMeshConnect( TESSmesh *mesh, TESShalfEdge *eOrg, TESShalfEdge *eDst ); + public function connect(eOrg : TESShalfEdge, eDst : TESShalfEdge) : TESShalfEdge{ + var joiningLoops = false; + var eNew = this.makeEdge_( eOrg ); + var eNewSym = eNew.Sym; + + if( eDst.Lface != eOrg.Lface ) { + /* We are connecting two disjoint loops -- destroy eDst->Lface */ + joiningLoops = true; + this.killFace_( eDst.Lface, eOrg.Lface ); + } + + /* Connect the new edge appropriately */ + this.splice_( eNew, eOrg.Lnext ); + this.splice_( eNewSym, eDst ); + + /* Set the vertex and face information */ + eNew.Org = eOrg.getDst(); + eNewSym.Org = eDst.Org; + eNew.Lface = eNewSym.Lface = eOrg.Lface; + + /* Make sure the old face points to a valid half-edge */ + eOrg.Lface.anEdge = eNewSym; + + if( ! joiningLoops ) { + var newFace = new TESSface(); + /* We split one loop into two -- the new loop is eNew->Lface */ + this.makeFace_( newFace, eNew, eOrg.Lface ); + } + return eNew; + } + + /* tessMeshZapFace( fZap ) destroys a face and removes it from the + * global face list. All edges of fZap will have a NULL pointer as their + * left face. Any edges which also have a NULL pointer as their right face + * are deleted entirely (along with any isolated vertices this produces). + * An entire mesh can be deleted by zapping its faces, one at a time, + * in any order. Zapped faces cannot be used in further mesh operations! + */ + public function zapFace( fZap : TESSface ) : Void + { + var eStart = fZap.anEdge; + var e, eNext, eSym; + var fPrev, fNext; + + /* walk around face, deleting edges whose right face is also NULL */ + eNext = eStart.Lnext; + do { + e = eNext; + eNext = e.Lnext; + + e.Lface = null; + if( e.getRface() == null ) { + /* delete the edge -- see TESSmeshDelete above */ + + if( e.Onext == e ) { + this.killVertex_( e.Org, null ); + } else { + /* Make sure that e->Org points to a valid half-edge */ + e.Org.anEdge = e.Onext; + this.splice_( e, e.getOprev() ); + } + eSym = e.Sym; + if( eSym.Onext == eSym ) { + this.killVertex_( eSym.Org, null ); + } else { + /* Make sure that eSym->Org points to a valid half-edge */ + eSym.Org.anEdge = eSym.Onext; + this.splice_( eSym, eSym.getOprev() ); + } + this.killEdge_( e ); + } + } while( e != eStart ); + + /* delete from circular doubly-linked list */ + fPrev = fZap.prev; + fNext = fZap.next; + fNext.prev = fPrev; + fPrev.next = fNext; + } + + public function countFaceVerts_(f : TESSface) : Int { + var eCur = f.anEdge; + var n = 0; + do + { + n++; + eCur = eCur.Lnext; + } + while (eCur != f.anEdge); + return n; + } + + //int tessMeshMergeConvexFaces( TESSmesh *mesh, int maxVertsPerFace ) + public function mergeConvexFaces(maxVertsPerFace : Int) : Bool { + + var eCur, eNext, eSym; + var vStart; + var curNv, symNv; + + var f = this.fHead.next; + while (f != this.fHead){ +// for( f = this.fHead.next; ; f = f.next ){ + //Skip faces which are outside the result. + if( !f.inside ){ + f = f.next; + continue; + } + + eCur = f.anEdge; + vStart = eCur.Org; + + while (true) + { + eNext = eCur.Lnext; + eSym = eCur.Sym; + + //Try to merge if the neighbour face is valid. + if( eSym != null && eSym.Lface != null && eSym.Lface.inside ) + { + //Try to merge the neighbour faces if the resulting polygons + //does not exceed maximum number of vertices. + curNv = this.countFaceVerts_( f ); + symNv = this.countFaceVerts_( eSym.Lface ); + if( (curNv+symNv-2) <= maxVertsPerFace ) + { + //Merge if the resulting poly is convex. + if( Geom.vertCCW( eCur.getLprev().Org, eCur.Org, eSym.Lnext.Lnext.Org ) && + Geom.vertCCW( eSym.getLprev().Org, eSym.Org, eCur.Lnext.Lnext.Org ) ) + { + eNext = eSym.Lnext; + this.delete( eSym ); + eCur = null; + eSym = null; + } + } + } + + if( eCur != null && eCur.Lnext.Org == vStart ) + break; + + //Continue to next edge. + eCur = eNext; + } + f = f.next; + } + + return true; + } + + /* tessMeshCheckMesh( mesh ) checks a mesh for self-consistency. + */ + public function check() : Void { + var fHead = this.fHead; + var vHead = this.vHead; + var eHead = this.eHead; + var f, v, vPrev, e, ePrev; + + var fPrev = fHead; + while ( (f = fPrev.next) != fHead ){ +// for( fPrev = fHead ; (f = fPrev.next) != fHead; fPrev = f) { + Tess2.assert( f.prev == fPrev ); + e = f.anEdge; + do { + Tess2.assert( e.Sym != e ); + Tess2.assert( e.Sym.Sym == e ); + Tess2.assert( e.Lnext.Onext.Sym == e ); + Tess2.assert( e.Onext.Sym.Lnext == e ); + Tess2.assert( e.Lface == f ); + e = e.Lnext; + } while( e != f.anEdge ); + fPrev = f; + } + Tess2.assert( f.prev == fPrev && f.anEdge == null ); + + vPrev = vHead; + while( (v = vPrev.next) != vHead ){ +// for( vPrev = vHead ; (v = vPrev.next) != vHead; vPrev = v) { + Tess2.assert( v.prev == vPrev ); + e = v.anEdge; + do { + Tess2.assert( e.Sym != e ); + Tess2.assert( e.Sym.Sym == e ); + Tess2.assert( e.Lnext.Onext.Sym == e ); + Tess2.assert( e.Onext.Sym.Lnext == e ); + Tess2.assert( e.Org == v ); + e = e.Onext; + } while( e != v.anEdge ); + vPrev = v; + } + Tess2.assert( v.prev == vPrev && v.anEdge == null ); + + ePrev = eHead; + while( (e = ePrev.next) != eHead ){ +// for( ePrev = eHead ; (e = ePrev.next) != eHead; ePrev = e) { + Tess2.assert( e.Sym.next == ePrev.Sym ); + Tess2.assert( e.Sym != e ); + Tess2.assert( e.Sym.Sym == e ); + Tess2.assert( e.Org != null ); + Tess2.assert( e.getDst() != null ); + Tess2.assert( e.Lnext.Onext.Sym == e ); + Tess2.assert( e.Onext.Sym.Lnext == e ); + ePrev = e; + } + Tess2.assert( e.Sym.next == ePrev.Sym + && e.Sym == this.eHeadSym + && e.Sym.Sym == e + && e.Org == null && e.getDst() == null + && e.Lface == null && e.getRface() == null ); + } +} + +class Geom { + + public static function vertEq(u : TESSvertex,v : TESSvertex) : Bool { + return (u.s == v.s && u.t == v.t); + } + + /* Returns TRUE if u is lexicographically <= v. */ + public static function vertLeq(u : TESSvertex,v : TESSvertex) : Bool { + return ((u.s < v.s) || (u.s == v.s && u.t <= v.t)); + } + + /* Versions of VertLeq, EdgeSign, EdgeEval with s and t transposed. */ + public static function transLeq(u : TESSvertex,v : TESSvertex) : Bool { + return ((u.t < v.t) || (u.t == v.t && u.s <= v.s)); + } + + public static function edgeGoesLeft(e : TESShalfEdge) : Bool { + return Geom.vertLeq( e.getDst(), e.Org ); + } + + public static function edgeGoesRight(e : TESShalfEdge) : Bool { + return Geom.vertLeq( e.Org, e.getDst() ); + } + + public static function vertL1dist(u : TESSvertex, v : TESSvertex) : Float { + return (Math.abs(u.s - v.s) + Math.abs(u.t - v.t)); + } + + //TESSreal tesedgeEval( TESSvertex *u, TESSvertex *v, TESSvertex *w ) + public static function edgeEval( u : TESSvertex, v : TESSvertex, w : TESSvertex ) : Float { + /* Given three vertices u,v,w such that VertLeq(u,v) && VertLeq(v,w), + * evaluates the t-coord of the edge uw at the s-coord of the vertex v. + * Returns v->t - (uw)(v->s), ie. the signed distance from uw to v. + * If uw is vertical (and thus passes thru v), the result is zero. + * + * The calculation is extremely accurate and stable, even when v + * is very close to u or w. In particular if we set v->t = 0 and + * let r be the negated result (this evaluates (uw)(v->s)), then + * r is guaranteed to satisfy MIN(u->t,w->t) <= r <= MAX(u->t,w->t). + */ + Tess2.assert( Geom.vertLeq( u, v ) && Geom.vertLeq( v, w )); + + var gapL = v.s - u.s; + var gapR = w.s - v.s; + + if( gapL + gapR > 0.0 ) { + if( gapL < gapR ) { + return (v.t - u.t) + (u.t - w.t) * (gapL / (gapL + gapR)); + } else { + return (v.t - w.t) + (w.t - u.t) * (gapR / (gapL + gapR)); + } + } + /* vertical line */ + return 0.0; + } + + //TESSreal tesedgeSign( TESSvertex *u, TESSvertex *v, TESSvertex *w ) + public static function edgeSign( u : TESSvertex, v : TESSvertex, w : TESSvertex ) : Float { + /* Returns a number whose sign matches EdgeEval(u,v,w) but which + * is cheaper to evaluate. Returns > 0, == 0 , or < 0 + * as v is above, on, or below the edge uw. + */ + Tess2.assert( Geom.vertLeq( u, v ) && Geom.vertLeq( v, w )); + + var gapL = v.s - u.s; + var gapR = w.s - v.s; + + if( gapL + gapR > 0.0 ) { + return (v.t - w.t) * gapL + (v.t - u.t) * gapR; + } + /* vertical line */ + return 0.0; + } + + /*********************************************************************** + * Define versions of EdgeSign, EdgeEval with s and t transposed. + */ + + //TESSreal testransEval( TESSvertex *u, TESSvertex *v, TESSvertex *w ) + public static function transEval( u : TESSvertex, v : TESSvertex, w : TESSvertex ) : Float { + /* Given three vertices u,v,w such that TransLeq(u,v) && TransLeq(v,w), + * evaluates the t-coord of the edge uw at the s-coord of the vertex v. + * Returns v->s - (uw)(v->t), ie. the signed distance from uw to v. + * If uw is vertical (and thus passes thru v), the result is zero. + * + * The calculation is extremely accurate and stable, even when v + * is very close to u or w. In particular if we set v->s = 0 and + * let r be the negated result (this evaluates (uw)(v->t)), then + * r is guaranteed to satisfy MIN(u->s,w->s) <= r <= MAX(u->s,w->s). + */ + Tess2.assert( Geom.transLeq( u, v ) && Geom.transLeq( v, w )); + + var gapL = v.t - u.t; + var gapR = w.t - v.t; + + if( gapL + gapR > 0.0 ) { + if( gapL < gapR ) { + return (v.s - u.s) + (u.s - w.s) * (gapL / (gapL + gapR)); + } else { + return (v.s - w.s) + (w.s - u.s) * (gapR / (gapL + gapR)); + } + } + /* vertical line */ + return 0.0; + } + + //TESSreal testransSign( TESSvertex *u, TESSvertex *v, TESSvertex *w ) + public static function transSign( u : TESSvertex, v : TESSvertex, w : TESSvertex ) : Float{ + /* Returns a number whose sign matches TransEval(u,v,w) but which + * is cheaper to evaluate. Returns > 0, == 0 , or < 0 + * as v is above, on, or below the edge uw. + */ + Tess2.assert( Geom.transLeq( u, v ) && Geom.transLeq( v, w )); + + var gapL = v.t - u.t; + var gapR = w.t - v.t; + + if( gapL + gapR > 0.0 ) { + return (v.s - w.s) * gapL + (v.s - u.s) * gapR; + } + /* vertical line */ + return 0.0; + } + + + //int tesvertCCW( TESSvertex *u, TESSvertex *v, TESSvertex *w ) + public static function vertCCW( u : TESSvertex, v : TESSvertex, w : TESSvertex) : Bool { + /* For almost-degenerate situations, the results are not reliable. + * Unless the floating-point arithmetic can be performed without + * rounding errors, *any* implementation will give incorrect results + * on some degenerate inputs, so the client must have some way to + * handle this situation. + */ + return (u.s*(v.t - w.t) + v.s*(w.t - u.t) + w.s*(u.t - v.t)) >= 0.0; + } + + /* Given parameters a,x,b,y returns the value (b*x+a*y)/(a+b), + * or (x+y)/2 if a==b==0. It requires that a,b >= 0, and enforces + * this in the rare case that one argument is slightly negative. + * The implementation is extremely stable numerically. + * In particular it guarantees that the result r satisfies + * MIN(x,y) <= r <= MAX(x,y), and the results are very accurate + * even when a and b differ greatly in magnitude. + */ + public static function interpolate(a : Float,x : Float,b : Float,y : Float) : Float { + var a = (a < 0) ? 0 : a; + var b = (b < 0) ? 0 : b; + return ((a <= b) ? ((b == 0) ? ((x+y) / 2) : (x + (y-x) * (a/(a+b)))) : (y + (x-y) * (b/(a+b)))); + } + + /* + #ifndef FOR_TRITE_TEST_PROGRAM + #define Interpolate(a,x,b,y) RealInterpolate(a,x,b,y) + #else + + //Claim: the ONLY property the sweep algorithm relies on is that + //MIN(x,y) <= r <= MAX(x,y). This is a nasty way to test that. + #include + extern int RandomInterpolate; + + double Interpolate( double a, double x, double b, double y) + { + printf("*********************%d\n",RandomInterpolate); + if( RandomInterpolate ) { + a = 1.2 * drand48() - 0.1; + a = (a < 0) ? 0 : ((a > 1) ? 1 : a); + b = 1.0 - a; + } + return RealInterpolate(a,x,b,y); + } + #endif*/ + + public static function intersect( o1 : TESSvertex, d1 : TESSvertex, o2 : TESSvertex, d2 : TESSvertex, v : TESSvertex ) { + /* Given edges (o1,d1) and (o2,d2), compute their point of intersection. + * The computed point is guaranteed to lie in the intersection of the + * bounding rectangles defined by each edge. + */ + var z1, z2; + var t; + + /* This is certainly not the most efficient way to find the intersection + * of two line segments, but it is very numerically stable. + * + * Strategy: find the two middle vertices in the VertLeq ordering, + * and interpolate the intersection s-value from these. Then repeat + * using the TransLeq ordering to find the intersection t-value. + */ + + if( ! Geom.vertLeq( o1, d1 )) { t = o1; o1 = d1; d1 = t; } //swap( o1, d1 ); } + if( ! Geom.vertLeq( o2, d2 )) { t = o2; o2 = d2; d2 = t; } //swap( o2, d2 ); } + if( ! Geom.vertLeq( o1, o2 )) { t = o1; o1 = o2; o2 = t; t = d1; d1 = d2; d2 = t; }//swap( o1, o2 ); swap( d1, d2 ); } + + if( ! Geom.vertLeq( o2, d1 )) { + /* Technically, no intersection -- do our best */ + v.s = (o2.s + d1.s) / 2; + } else if( Geom.vertLeq( d1, d2 )) { + /* Interpolate between o2 and d1 */ + z1 = Geom.edgeEval( o1, o2, d1 ); + z2 = Geom.edgeEval( o2, d1, d2 ); + if( z1+z2 < 0 ) { z1 = -z1; z2 = -z2; } + v.s = Geom.interpolate( z1, o2.s, z2, d1.s ); + } else { + /* Interpolate between o2 and d2 */ + z1 = Geom.edgeSign( o1, o2, d1 ); + z2 = -Geom.edgeSign( o1, d2, d1 ); + if( z1+z2 < 0 ) { z1 = -z1; z2 = -z2; } + v.s = Geom.interpolate( z1, o2.s, z2, d2.s ); + } + + /* Now repeat the process for t */ + + if( ! Geom.transLeq( o1, d1 )) { t = o1; o1 = d1; d1 = t; } //swap( o1, d1 ); } + if( ! Geom.transLeq( o2, d2 )) { t = o2; o2 = d2; d2 = t; } //swap( o2, d2 ); } + if( ! Geom.transLeq( o1, o2 )) { t = o1; o1 = o2; o2 = t; t = d1; d1 = d2; d2 = t; } //swap( o1, o2 ); swap( d1, d2 ); } + + if( ! Geom.transLeq( o2, d1 )) { + /* Technically, no intersection -- do our best */ + v.t = (o2.t + d1.t) / 2; + } else if( Geom.transLeq( d1, d2 )) { + /* Interpolate between o2 and d1 */ + z1 = Geom.transEval( o1, o2, d1 ); + z2 = Geom.transEval( o2, d1, d2 ); + if( z1+z2 < 0 ) { z1 = -z1; z2 = -z2; } + v.t = Geom.interpolate( z1, o2.t, z2, d1.t ); + } else { + /* Interpolate between o2 and d2 */ + z1 = Geom.transSign( o1, o2, d1 ); + z2 = -Geom.transSign( o1, d2, d1 ); + if( z1+z2 < 0 ) { z1 = -z1; z2 = -z2; } + v.t = Geom.interpolate( z1, o2.t, z2, d2.t ); + } + } +} + +class DictNode { + + public var key : ActiveRegion; + public var next : DictNode; + public var prev : DictNode; + + public function new () { + this.key = null; + this.next = null; + this.prev = null; + } +} + +class Dict { + + public var head : DictNode; + public var frame : Tessellator; + public var leq : Tessellator -> ActiveRegion -> ActiveRegion -> Bool; + + public function new(frame, leq) { + this.head = new DictNode(); + this.head.next = this.head; + this.head.prev = this.head; + this.frame = frame; + this.leq = leq; + } + + public function min() : DictNode { + return this.head.next; + } + + public function max() : DictNode { + return this.head.prev; + } + + public function insert(k : ActiveRegion) : DictNode { + return this.insertBefore(this.head, k); + } + + public function search(key : ActiveRegion) : DictNode { + /* Search returns the node with the smallest key greater than or equal + * to the given key. If there is no such key, returns a node whose + * key is NULL. Similarly, Succ(Max(d)) has a NULL key, etc. + */ + var node = this.head; + do { + node = node.next; + } while( node.key != null && ! this.leq(this.frame, key, node.key)); + + return node; + } + + public function insertBefore(node : DictNode, key : ActiveRegion ) : DictNode { + do { + node = node.prev; + } while( node.key != null && ! this.leq(this.frame, node.key, key)); + + var newNode = new DictNode(); + newNode.key = key; + newNode.next = node.next; + node.next.prev = newNode; + newNode.prev = node; + node.next = newNode; + + return newNode; + } + + public function delete(node : DictNode) : Void { + node.next.prev = node.prev; + node.prev.next = node.next; + } +} + +class PQnode { + + public var handle : Int; + + public function new() { + this.handle = null; + } +} + +class PQhandleElem { + + public var key : TESSvertex; + public var node : Int; + + public function new(){ + this.key = null; + this.node = null; + } +} + +class PriorityQ { + + public var size : Int; + public var max : Int; + public var nodes : Array; + public var handles : Array; + public var initialized : Bool; + public var freeList : Int; + public var leq : TESSvertex -> TESSvertex -> Bool; + + public function new (size : Int, leq) { + this.size = 0; + this.max = size; + + this.nodes = []; + this.nodes.alloc( size+1 ); + for (i in 0...this.nodes.length) + //for (var i = 0; i < this.nodes.length; i++) + { + this.nodes[i] = new PQnode(); + } + + this.handles = []; + this.handles.alloc( size+1 ); + + for (i in 0...this.handles.length) + //for (var i = 0; i < this.handles.length; i++) + { + this.handles[i] = new PQhandleElem(); + } + + this.initialized = false; + this.freeList = 0; + this.leq = leq; + + this.nodes[1].handle = 1; /* so that Minimum() returns NULL */ + this.handles[1].key = null; + } + +// floatDown_: function( curr ) +// { +// var n = this.nodes; +// var h = this.handles; +// var hCurr, hChild; +// var child; +// +// hCurr = n[curr].handle; +// for( ;; ) { +// child = curr << 1; +// if( child < this.size && this.leq( h[n[child+1].handle].key, h[n[child].handle].key )) { +// ++child; +// } +// +// assert(child <= this.max); +// +// hChild = n[child].handle; +// if( child > this.size || this.leq( h[hCurr].key, h[hChild].key )) { +// n[curr].handle = hCurr; +// h[hCurr].node = curr; +// break; +// } +// n[curr].handle = hChild; +// h[hChild].node = curr; +// curr = child; +// } +// }, + + public function floatDown_( curr : Int ) : Void + { + var n = this.nodes; + var h = this.handles; + var hCurr, hChild; + var child; + + hCurr = n[curr].handle; + while (true){ +// for( ;; ) { + child = curr << 1; + if( child < this.size && this.leq( h[n[child+1].handle].key, h[n[child].handle].key )) { + ++child; + } + + Tess2.assert(child <= this.max); + + hChild = n[child].handle; + if( child > this.size || this.leq( h[hCurr].key, h[hChild].key )) { + n[curr].handle = hCurr; + h[hCurr].node = curr; + break; + } + n[curr].handle = hChild; + h[hChild].node = curr; + curr = child; + } + } + + public function floatUp_( curr : Int ) : Void + { + var n = this.nodes; + var h = this.handles; + var hCurr, hParent; + var parent; + + hCurr = n[curr].handle; + while (true){ +// for( ;; ) { + parent = curr >> 1; + hParent = n[parent].handle; + if( parent == 0 || this.leq( h[hParent].key, h[hCurr].key )) { + n[curr].handle = hCurr; + h[hCurr].node = curr; + break; + } + n[curr].handle = hParent; + h[hParent].node = curr; + curr = parent; + } + } + + public function init() : Void { + /* This method of building a heap is O(n), rather than O(n lg n). */ + var i = this.size; + while (i >= 1){ +// for( var i = this.size; i >= 1; --i ) { + this.floatDown_( i ); + --i; + } + this.initialized = true; + } + + public function min() { + return this.handles[this.nodes[1].handle].key; + } + + public function isEmpty() { + this.size == 0; + } + + /* really pqHeapInsert */ + /* returns INV_HANDLE iff out of memory */ + //PQhandle pqHeapInsert( TESSalloc* alloc, PriorityQHeap *pq, PQkey keyNew ) + public function insert(keyNew : TESSvertex) + { + var curr; + var free; + + curr = ++this.size; + if( (curr*2) > this.max ) { + this.max *= 2; + var s; + s = this.nodes.length; + this.nodes.alloc( this.max+1 ); + for (i in s...this.nodes.length) + //for (var i = s; i < this.nodes.length; i++) + { + this.nodes[i] = new PQnode(); + } + + s = this.handles.length; + this.handles.alloc( this.max+1 ); + for (i in s...this.handles.length) + //for (var i = s; i < this.handles.length; i++) + { + this.handles[i] = new PQhandleElem(); + } + + } + + if( this.freeList == 0 ) { + free = curr; + } else { + free = this.freeList; + this.freeList = this.handles[free].node; + } + + this.nodes[curr].handle = free; + this.handles[free].node = curr; + this.handles[free].key = keyNew; + + if( this.initialized ) { + this.floatUp_( curr ); + } + return free; + } + + //PQkey pqHeapExtractMin( PriorityQHeap *pq ) + public function extractMin() { + var n = this.nodes; + var h = this.handles; + var hMin = n[1].handle; + var min = h[hMin].key; + + if( this.size > 0 ) { + n[1].handle = n[this.size].handle; + h[n[1].handle].node = 1; + + h[hMin].key = null; + h[hMin].node = this.freeList; + this.freeList = hMin; + + --this.size; + if( this.size > 0 ) { + this.floatDown_( 1 ); + } + } + return min; + } + + public function delete( hCurr ) { + var n = this.nodes; + var h = this.handles; + var curr; + + Tess2.assert( hCurr >= 1 && hCurr <= this.max && h[hCurr].key != null ); + + curr = h[hCurr].node; + n[curr].handle = n[this.size].handle; + h[n[curr].handle].node = curr; + + --this.size; + if( curr <= this.size ) { + if( curr <= 1 || this.leq( h[n[curr>>1].handle].key, h[n[curr].handle].key )) { + this.floatDown_( curr ); + } else { + this.floatUp_( curr ); + } + } + h[hCurr].key = null; + h[hCurr].node = this.freeList; + this.freeList = hCurr; + } +} + +/* For each pair of adjacent edges crossing the sweep line, there is +* an ActiveRegion to represent the region between them. The active +* regions are kept in sorted order in a dynamic dictionary. As the +* sweep line crosses each vertex, we update the affected regions. +*/ + +class ActiveRegion { + + public var eUp : TESShalfEdge; + public var nodeUp : DictNode; + public var windingNumber : Int; + public var inside : Bool; + public var sentinel : Bool; + public var dirty : Bool; + public var fixUpperEdge : Bool; + + public function new() { + this.eUp = null; /* upper edge, directed right to left */ + this.nodeUp = null; /* dictionary node corresponding to eUp */ + this.windingNumber = 0; /* used to determine which regions are + * inside the polygon */ + this.inside = false; /* is this region inside the polygon? */ + this.sentinel = false; /* marks fake edges at t = +/-infinity */ + this.dirty = false; /* marks regions where the upper or lower + * edge has changed, but we haven't checked + * whether they intersect yet */ + this.fixUpperEdge = false; /* marks temporary edges introduced when + * we process a "right vertex" (one without + * any edges leaving to the right) */ + } +} + +class Sweep { + + public static function regionBelow(r : ActiveRegion) : ActiveRegion { + return r.nodeUp.prev.key; + } + + public static function regionAbove(r : ActiveRegion) : ActiveRegion { + return r.nodeUp.next.key; + } + + public static function debugEvent( tess : Tessellator ) : Void { + //empty + } + + /* + * Invariants for the Edge Dictionary. + * - each pair of adjacent edges e2=Succ(e1) satisfies EdgeLeq(e1,e2) + * at any valid location of the sweep event + * - if EdgeLeq(e2,e1) as well (at any valid sweep event), then e1 and e2 + * share a common endpoint + * - for each e, e->Dst has been processed, but not e->Org + * - each edge e satisfies VertLeq(e->Dst,event) && VertLeq(event,e->Org) + * where "event" is the current sweep line event. + * - no edge e has zero length + * + * Invariants for the Mesh (the processed portion). + * - the portion of the mesh left of the sweep line is a planar graph, + * ie. there is *some* way to embed it in the plane + * - no processed edge has zero length + * - no two processed vertices have identical coordinates + * - each "inside" region is monotone, ie. can be broken into two chains + * of monotonically increasing vertices according to VertLeq(v1,v2) + * - a non-invariant: these chains may intersect (very slightly) + * + * Invariants for the Sweep. + * - if none of the edges incident to the event vertex have an activeRegion + * (ie. none of these edges are in the edge dictionary), then the vertex + * has only right-going edges. + * - if an edge is marked "fixUpperEdge" (it is a temporary edge introduced + * by ConnectRightVertex), then it is the only right-going edge from + * its associated vertex. (This says that these edges exist only + * when it is necessary.) + */ + + /* When we merge two edges into one, we need to compute the combined + * winding of the new edge. + */ + public static function addWinding(eDst : TESShalfEdge,eSrc : TESShalfEdge ) { + eDst.winding += eSrc.winding; + eDst.Sym.winding += eSrc.Sym.winding; + } + + //static int EdgeLeq( TESStesselator *tess, ActiveRegion *reg1, ActiveRegion *reg2 ) + public static function edgeLeq( tess : Tessellator, reg1 : ActiveRegion, reg2 : ActiveRegion ) : Bool { + /* + * Both edges must be directed from right to left (this is the canonical + * direction for the upper edge of each region). + * + * The strategy is to evaluate a "t" value for each edge at the + * current sweep line position, given by tess->event. The calculations + * are designed to be very stable, but of course they are not perfect. + * + * Special case: if both edge destinations are at the sweep event, + * we sort the edges by slope (they would otherwise compare equally). + */ + var ev = tess.event; + var t1, t2; + + var e1 = reg1.eUp; + var e2 = reg2.eUp; + + if( e1.getDst() == ev ) { + if( e2.getDst() == ev ) { + /* Two edges right of the sweep line which meet at the sweep event. + * Sort them by slope. + */ + if( Geom.vertLeq( e1.Org, e2.Org )) { + return Geom.edgeSign( e2.getDst(), e1.Org, e2.Org ) <= 0; + } + return Geom.edgeSign( e1.getDst(), e2.Org, e1.Org ) >= 0; + } + return Geom.edgeSign( e2.getDst(), ev, e2.Org ) <= 0; + } + if( e2.getDst() == ev ) { + return Geom.edgeSign( e1.getDst(), ev, e1.Org ) >= 0; + } + + /* General case - compute signed distance *from* e1, e2 to event */ + var t1 = Geom.edgeEval( e1.getDst(), ev, e1.Org ); + var t2 = Geom.edgeEval( e2.getDst(), ev, e2.Org ); + return (t1 >= t2); + } + + //static void DeleteRegion( TESStesselator *tess, ActiveRegion *reg ) + public static function deleteRegion( tess : Tessellator, reg : ActiveRegion ) : Void { + if( reg.fixUpperEdge ) { + /* It was created with zero winding number, so it better be + * deleted with zero winding number (ie. it better not get merged + * with a real edge). + */ + Tess2.assert( reg.eUp.winding == 0 ); + } + reg.eUp.activeRegion = null; + tess.dict.delete( reg.nodeUp ); + } + + //static int FixUpperEdge( TESStesselator *tess, ActiveRegion *reg, TESShalfEdge *newEdge ) + public static function fixUpperEdge( tess : Tessellator, reg : ActiveRegion, newEdge : TESShalfEdge ) : Void { + /* + * Replace an upper edge which needs fixing (see ConnectRightVertex). + */ + Tess2.assert( reg.fixUpperEdge ); + tess.mesh.delete( reg.eUp ); + reg.fixUpperEdge = false; + reg.eUp = newEdge; + newEdge.activeRegion = reg; + } + + //static ActiveRegion *TopLeftRegion( TESStesselator *tess, ActiveRegion *reg ) + public static function topLeftRegion( tess : Tessellator, reg : ActiveRegion ) { + var org = reg.eUp.Org; + var e; + + /* Find the region above the uppermost edge with the same origin */ + do { + reg = Sweep.regionAbove( reg ); + } while( reg.eUp.Org == org ); + + /* If the edge above was a temporary edge introduced by ConnectRightVertex, + * now is the time to fix it. + */ + if( reg.fixUpperEdge ) { + e = tess.mesh.connect( Sweep.regionBelow(reg).eUp.Sym, reg.eUp.Lnext ); + if (e == null) return null; + Sweep.fixUpperEdge( tess, reg, e ); + reg = Sweep.regionAbove( reg ); + } + return reg; + } + + //static ActiveRegion *TopRightRegion( ActiveRegion *reg ) + public static function topRightRegion( reg : ActiveRegion ) + { + var dst = reg.eUp.getDst(); + var reg = null; + /* Find the region above the uppermost edge with the same destination */ + do { + reg = Sweep.regionAbove( reg ); + } while( reg.eUp.getDst() == dst ); + return reg; + } + + //static ActiveRegion *AddRegionBelow( TESStesselator *tess, ActiveRegion *regAbove, TESShalfEdge *eNewUp ) + public static function addRegionBelow( tess : Tessellator, regAbove : ActiveRegion, eNewUp : TESShalfEdge ) { + /* + * Add a new active region to the sweep line, *somewhere* below "regAbove" + * (according to where the new edge belongs in the sweep-line dictionary). + * The upper edge of the new region will be "eNewUp". + * Winding number and "inside" flag are not updated. + */ + var regNew = new ActiveRegion(); + regNew.eUp = eNewUp; + regNew.nodeUp = tess.dict.insertBefore( regAbove.nodeUp, regNew ); + // if (regNew->nodeUp == NULL) longjmp(tess->env,1); + regNew.fixUpperEdge = false; + regNew.sentinel = false; + regNew.dirty = false; + + eNewUp.activeRegion = regNew; + return regNew; + } + + //static int IsWindingInside( TESStesselator *tess, int n ) + public static function isWindingInside( tess : Tessellator, n : Int ) { + switch( tess.windingRule ) { + case Tess2.WINDING_ODD: + return (n & 1) != 0; + case Tess2.WINDING_NONZERO: + return (n != 0); + case Tess2.WINDING_POSITIVE: + return (n > 0); + case Tess2.WINDING_NEGATIVE: + return (n < 0); + case Tess2.WINDING_ABS_GEQ_TWO: + return (n >= 2) || (n <= -2); + } + Tess2.assert( false ); + return false; + } + + //static void ComputeWinding( TESStesselator *tess, ActiveRegion *reg ) + public static function computeWinding( tess : Tessellator, reg : ActiveRegion ) { + reg.windingNumber = Sweep.regionAbove(reg).windingNumber + reg.eUp.winding; + reg.inside = Sweep.isWindingInside( tess, reg.windingNumber ); + } + + + //static void FinishRegion( TESStesselator *tess, ActiveRegion *reg ) + public static function finishRegion( tess : Tessellator, reg : ActiveRegion ) { + /* + * Delete a region from the sweep line. This happens when the upper + * and lower chains of a region meet (at a vertex on the sweep line). + * The "inside" flag is copied to the appropriate mesh face (we could + * not do this before -- since the structure of the mesh is always + * changing, this face may not have even existed until now). + */ + var e = reg.eUp; + var f = e.Lface; + + f.inside = reg.inside; + f.anEdge = e; /* optimization for tessMeshTessellateMonoRegion() */ + Sweep.deleteRegion( tess, reg ); + } + + + //static TESShalfEdge *FinishLeftRegions( TESStesselator *tess, ActiveRegion *regFirst, ActiveRegion *regLast ) + public static function finishLeftRegions( tess : Tessellator, regFirst : ActiveRegion, regLast : ActiveRegion ) { + /* + * We are given a vertex with one or more left-going edges. All affected + * edges should be in the edge dictionary. Starting at regFirst->eUp, + * we walk down deleting all regions where both edges have the same + * origin vOrg. At the same time we copy the "inside" flag from the + * active region to the face, since at this point each face will belong + * to at most one region (this was not necessarily true until this point + * in the sweep). The walk stops at the region above regLast; if regLast + * is NULL we walk as far as possible. At the same time we relink the + * mesh if necessary, so that the ordering of edges around vOrg is the + * same as in the dictionary. + */ + var e, ePrev; + var reg = null; + var regPrev = regFirst; + var ePrev = regFirst.eUp; + while( regPrev != regLast ) { + regPrev.fixUpperEdge = false; /* placement was OK */ + reg = Sweep.regionBelow( regPrev ); + e = reg.eUp; + if( e.Org != ePrev.Org ) { + if( ! reg.fixUpperEdge ) { + /* Remove the last left-going edge. Even though there are no further + * edges in the dictionary with this origin, there may be further + * such edges in the mesh (if we are adding left edges to a vertex + * that has already been processed). Thus it is important to call + * FinishRegion rather than just DeleteRegion. + */ + Sweep.finishRegion( tess, regPrev ); + break; + } + /* If the edge below was a temporary edge introduced by + * ConnectRightVertex, now is the time to fix it. + */ + e = tess.mesh.connect( ePrev.getLprev(), e.Sym ); + // if (e == NULL) longjmp(tess->env,1); + Sweep.fixUpperEdge( tess, reg, e ); + } + + /* Relink edges so that ePrev->Onext == e */ + if( ePrev.Onext != e ) { + tess.mesh.splice( e.getOprev(), e ); + tess.mesh.splice( ePrev, e ); + } + Sweep.finishRegion( tess, regPrev ); /* may change reg->eUp */ + ePrev = reg.eUp; + regPrev = reg; + } + return ePrev; + } + + + //static void AddRightEdges( TESStesselator *tess, ActiveRegion *regUp, TESShalfEdge *eFirst, TESShalfEdge *eLast, TESShalfEdge *eTopLeft, int cleanUp ) + public static function addRightEdges( tess : Tessellator, regUp : ActiveRegion, eFirst : TESShalfEdge, eLast : TESShalfEdge, eTopLeft : TESShalfEdge, cleanUp : Bool ) { + /* + * Purpose: insert right-going edges into the edge dictionary, and update + * winding numbers and mesh connectivity appropriately. All right-going + * edges share a common origin vOrg. Edges are inserted CCW starting at + * eFirst; the last edge inserted is eLast->Oprev. If vOrg has any + * left-going edges already processed, then eTopLeft must be the edge + * such that an imaginary upward vertical segment from vOrg would be + * contained between eTopLeft->Oprev and eTopLeft; otherwise eTopLeft + * should be NULL. + */ + var reg = null, regPrev; + var e, ePrev; + var firstTime = true; + + /* Insert the new right-going edges in the dictionary */ + e = eFirst; + do { + Tess2.assert( Geom.vertLeq( e.Org, e.getDst() )); + Sweep.addRegionBelow( tess, regUp, e.Sym ); + e = e.Onext; + } while ( e != eLast ); + + /* Walk *all* right-going edges from e->Org, in the dictionary order, + * updating the winding numbers of each region, and re-linking the mesh + * edges to match the dictionary ordering (if necessary). + */ + if( eTopLeft == null ) { + eTopLeft = Sweep.regionBelow( regUp ).eUp.getRprev(); + } + regPrev = regUp; + ePrev = eTopLeft; + while(true){ + //for( ;; ) { + reg = Sweep.regionBelow( regPrev ); + e = reg.eUp.Sym; + if( e.Org != ePrev.Org ) break; + + if( e.Onext != ePrev ) { + /* Unlink e from its current position, and relink below ePrev */ + tess.mesh.splice( e.getOprev(), e ); + tess.mesh.splice( ePrev.getOprev(), e ); + } + /* Compute the winding number and "inside" flag for the new regions */ + reg.windingNumber = regPrev.windingNumber - e.winding; + reg.inside = Sweep.isWindingInside( tess, reg.windingNumber ); + + /* Check for two outgoing edges with same slope -- process these + * before any intersection tests (see example in tessComputeInterior). + */ + regPrev.dirty = true; + if( ! firstTime && Sweep.checkForRightSplice( tess, regPrev )) { + Sweep.addWinding( e, ePrev ); + Sweep.deleteRegion( tess, regPrev ); + tess.mesh.delete( ePrev ); + } + firstTime = false; + regPrev = reg; + ePrev = e; + } + regPrev.dirty = true; + Tess2.assert( regPrev.windingNumber - e.winding == reg.windingNumber ); + + if( cleanUp ) { + /* Check for intersections between newly adjacent edges. */ + Sweep.walkDirtyRegions( tess, regPrev ); + } + } + + + //static void SpliceMergeVertices( TESStesselator *tess, TESShalfEdge *e1, TESShalfEdge *e2 ) + public static function spliceMergeVertices( tess : Tessellator, e1 : TESShalfEdge, e2 : TESShalfEdge ) { + /* + * Two vertices with idential coordinates are combined into one. + * e1->Org is kept, while e2->Org is discarded. + */ + tess.mesh.splice( e1, e2 ); + } + + //static void VertexWeights( TESSvertex *isect, TESSvertex *org, TESSvertex *dst, TESSreal *weights ) + public static function vertexWeights( isect : TESSvertex, org : TESSvertex, dst : TESSvertex ) { + /* + * Find some weights which describe how the intersection vertex is + * a linear combination of "org" and "dest". Each of the two edges + * which generated "isect" is allocated 50% of the weight; each edge + * splits the weight between its org and dst according to the + * relative distance to "isect". + */ + var t1 = Geom.vertL1dist( org, isect ); + var t2 = Geom.vertL1dist( dst, isect ); + var w0 = 0.5 * t2 / (t1 + t2); + var w1 = 0.5 * t1 / (t1 + t2); + isect.coords[0] += w0*org.coords[0] + w1*dst.coords[0]; + isect.coords[1] += w0*org.coords[1] + w1*dst.coords[1]; + isect.coords[2] += w0*org.coords[2] + w1*dst.coords[2]; + } + + + //static void GetIntersectData( TESStesselator *tess, TESSvertex *isect, TESSvertex *orgUp, TESSvertex *dstUp, TESSvertex *orgLo, TESSvertex *dstLo ) + public static function getIntersectData( tess : Tessellator, isect : TESSvertex, orgUp: TESSvertex, dstUp: TESSvertex, orgLo: TESSvertex, dstLo: TESSvertex ) { + /* + * We've computed a new intersection point, now we need a "data" pointer + * from the user so that we can refer to this new vertex in the + * rendering callbacks. + */ + isect.coords[0] = isect.coords[1] = isect.coords[2] = 0; + isect.idx = -1; + Sweep.vertexWeights( isect, orgUp, dstUp ); + Sweep.vertexWeights( isect, orgLo, dstLo ); + } + + //static int CheckForRightSplice( TESStesselator *tess, ActiveRegion *regUp ) + public static function checkForRightSplice( tess : Tessellator, regUp : ActiveRegion ) : Bool { + /* + * Check the upper and lower edge of "regUp", to make sure that the + * eUp->Org is above eLo, or eLo->Org is below eUp (depending on which + * origin is leftmost). + * + * The main purpose is to splice right-going edges with the same + * dest vertex and nearly identical slopes (ie. we can't distinguish + * the slopes numerically). However the splicing can also help us + * to recover from numerical errors. For example, suppose at one + * point we checked eUp and eLo, and decided that eUp->Org is barely + * above eLo. Then later, we split eLo into two edges (eg. from + * a splice operation like this one). This can change the result of + * our test so that now eUp->Org is incident to eLo, or barely below it. + * We must correct this condition to maintain the dictionary invariants. + * + * One possibility is to check these edges for intersection again + * (ie. CheckForIntersect). This is what we do if possible. However + * CheckForIntersect requires that tess->event lies between eUp and eLo, + * so that it has something to fall back on when the intersection + * calculation gives us an unusable answer. So, for those cases where + * we can't check for intersection, this routine fixes the problem + * by just splicing the offending vertex into the other edge. + * This is a guaranteed solution, no matter how degenerate things get. + * Basically this is a combinatorial solution to a numerical problem. + */ + var regLo = Sweep.regionBelow(regUp); + var eUp = regUp.eUp; + var eLo = regLo.eUp; + + if( Geom.vertLeq( eUp.Org, eLo.Org )) { + if( Geom.edgeSign( eLo.getDst(), eUp.Org, eLo.Org ) > 0 ) return false; + + /* eUp->Org appears to be below eLo */ + if( ! Geom.vertEq( eUp.Org, eLo.Org )) { + /* Splice eUp->Org into eLo */ + tess.mesh.splitEdge( eLo.Sym ); + tess.mesh.splice( eUp, eLo.getOprev() ); + regUp.dirty = regLo.dirty = true; + + } else if( eUp.Org != eLo.Org ) { + /* merge the two vertices, discarding eUp->Org */ + tess.pq.delete( eUp.Org.pqHandle ); + Sweep.spliceMergeVertices( tess, eLo.getOprev(), eUp ); + } + } else { + if( Geom.edgeSign( eUp.getDst(), eLo.Org, eUp.Org ) < 0 ) return false; + + /* eLo->Org appears to be above eUp, so splice eLo->Org into eUp */ + Sweep.regionAbove(regUp).dirty = regUp.dirty = true; + tess.mesh.splitEdge( eUp.Sym ); + tess.mesh.splice( eLo.getOprev(), eUp ); + } + return true; + } + + //static int CheckForLeftSplice( TESStesselator *tess, ActiveRegion *regUp ) + public static function checkForLeftSplice( tess : Tessellator, regUp : ActiveRegion ) : Bool { + /* + * Check the upper and lower edge of "regUp", to make sure that the + * eUp->Dst is above eLo, or eLo->Dst is below eUp (depending on which + * destination is rightmost). + * + * Theoretically, this should always be true. However, splitting an edge + * into two pieces can change the results of previous tests. For example, + * suppose at one point we checked eUp and eLo, and decided that eUp->Dst + * is barely above eLo. Then later, we split eLo into two edges (eg. from + * a splice operation like this one). This can change the result of + * the test so that now eUp->Dst is incident to eLo, or barely below it. + * We must correct this condition to maintain the dictionary invariants + * (otherwise new edges might get inserted in the wrong place in the + * dictionary, and bad stuff will happen). + * + * We fix the problem by just splicing the offending vertex into the + * other edge. + */ + var regLo = Sweep.regionBelow(regUp); + var eUp = regUp.eUp; + var eLo = regLo.eUp; + var e; + + Tess2.assert( ! Geom.vertEq( eUp.getDst(), eLo.getDst() )); + + if( Geom.vertLeq( eUp.getDst(), eLo.getDst() )) { + if( Geom.edgeSign( eUp.getDst(), eLo.getDst(), eUp.Org ) < 0 ) return false; + + /* eLo->Dst is above eUp, so splice eLo->Dst into eUp */ + Sweep.regionAbove(regUp).dirty = regUp.dirty = true; + e = tess.mesh.splitEdge( eUp ); + tess.mesh.splice( eLo.Sym, e ); + e.Lface.inside = regUp.inside; + } else { + if( Geom.edgeSign( eLo.getDst(), eUp.getDst(), eLo.Org ) > 0 ) return false; + + /* eUp->Dst is below eLo, so splice eUp->Dst into eLo */ + regUp.dirty = regLo.dirty = true; + e = tess.mesh.splitEdge( eLo ); + tess.mesh.splice( eUp.Lnext, eLo.Sym ); + e.getRface().inside = regUp.inside; + } + return true; + } + + + //static int CheckForIntersect( TESStesselator *tess, ActiveRegion *regUp ) + public static function checkForIntersect( tess : Tessellator, regUp : ActiveRegion ) : Bool { + /* + * Check the upper and lower edges of the given region to see if + * they intersect. If so, create the intersection and add it + * to the data structures. + * + * Returns TRUE if adding the new intersection resulted in a recursive + * call to AddRightEdges(); in this case all "dirty" regions have been + * checked for intersections, and possibly regUp has been deleted. + */ + var regLo = Sweep.regionBelow(regUp); + var eUp = regUp.eUp; + var eLo = regLo.eUp; + var orgUp = eUp.Org; + var orgLo = eLo.Org; + var dstUp = eUp.getDst(); + var dstLo = eLo.getDst(); + var tMinUp, tMaxLo; + var isect = new TESSvertex(); + var orgMin; + var e; + + Tess2.assert( !Geom.vertEq( dstLo, dstUp ) ); + Tess2.assert( Geom.edgeSign( dstUp, tess.event, orgUp ) <= 0 ); + Tess2.assert( Geom.edgeSign( dstLo, tess.event, orgLo ) >= 0 ); + Tess2.assert( orgUp != tess.event && orgLo != tess.event ); + Tess2.assert( !regUp.fixUpperEdge && ! regLo.fixUpperEdge ); + + if( orgUp == orgLo ) return false; /* right endpoints are the same */ + + tMinUp = Math.min( orgUp.t, dstUp.t ); + tMaxLo = Math.max( orgLo.t, dstLo.t ); + if( tMinUp > tMaxLo ) return false; /* t ranges do not overlap */ + + if( Geom.vertLeq( orgUp, orgLo )) { + if( Geom.edgeSign( dstLo, orgUp, orgLo ) > 0 ) return false; + } else { + if( Geom.edgeSign( dstUp, orgLo, orgUp ) < 0 ) return false; + } + + /* At this point the edges intersect, at least marginally */ + Sweep.debugEvent( tess ); + + Geom.intersect( dstUp, orgUp, dstLo, orgLo, isect ); + /* The following properties are guaranteed: */ + Tess2.assert( Math.min( orgUp.t, dstUp.t ) <= isect.t ); + Tess2.assert( isect.t <= Math.max( orgLo.t, dstLo.t )); + Tess2.assert( Math.min( dstLo.s, dstUp.s ) <= isect.s ); + Tess2.assert( isect.s <= Math.max( orgLo.s, orgUp.s )); + + if( Geom.vertLeq( isect, tess.event )) { + /* The intersection point lies slightly to the left of the sweep line, + * so move it until it''s slightly to the right of the sweep line. + * (If we had perfect numerical precision, this would never happen + * in the first place). The easiest and safest thing to do is + * replace the intersection by tess->event. + */ + isect.s = tess.event.s; + isect.t = tess.event.t; + } + /* Similarly, if the computed intersection lies to the right of the + * rightmost origin (which should rarely happen), it can cause + * unbelievable inefficiency on sufficiently degenerate inputs. + * (If you have the test program, try running test54.d with the + * "X zoom" option turned on). + */ + orgMin = Geom.vertLeq( orgUp, orgLo ) ? orgUp : orgLo; + if( Geom.vertLeq( orgMin, isect )) { + isect.s = orgMin.s; + isect.t = orgMin.t; + } + + if( Geom.vertEq( isect, orgUp ) || Geom.vertEq( isect, orgLo )) { + /* Easy case -- intersection at one of the right endpoints */ + Sweep.checkForRightSplice( tess, regUp ); + return false; + } + + if( (! Geom.vertEq( dstUp, tess.event ) + && Geom.edgeSign( dstUp, tess.event, isect ) >= 0) + || (! Geom.vertEq( dstLo, tess.event ) + && Geom.edgeSign( dstLo, tess.event, isect ) <= 0 )) + { + /* Very unusual -- the new upper or lower edge would pass on the + * wrong side of the sweep event, or through it. This can happen + * due to very small numerical errors in the intersection calculation. + */ + if( dstLo == tess.event ) { + /* Splice dstLo into eUp, and process the new region(s) */ + tess.mesh.splitEdge( eUp.Sym ); + tess.mesh.splice( eLo.Sym, eUp ); + regUp = Sweep.topLeftRegion( tess, regUp ); + // if (regUp == NULL) longjmp(tess->env,1); + eUp = Sweep.regionBelow(regUp).eUp; + Sweep.finishLeftRegions( tess, Sweep.regionBelow(regUp), regLo ); + Sweep.addRightEdges( tess, regUp, eUp.getOprev(), eUp, eUp, true ); + return true; + } + if( dstUp == tess.event ) { + /* Splice dstUp into eLo, and process the new region(s) */ + tess.mesh.splitEdge( eLo.Sym ); + tess.mesh.splice( eUp.Lnext, eLo.getOprev() ); + regLo = regUp; + regUp = Sweep.topRightRegion( regUp ); + e = Sweep.regionBelow(regUp).eUp.getRprev(); + regLo.eUp = eLo.getOprev(); + eLo = Sweep.finishLeftRegions( tess, regLo, null ); + Sweep.addRightEdges( tess, regUp, eLo.Onext, eUp.getRprev(), e, true ); + return true; + } + /* Special case: called from ConnectRightVertex. If either + * edge passes on the wrong side of tess->event, split it + * (and wait for ConnectRightVertex to splice it appropriately). + */ + if( Geom.edgeSign( dstUp, tess.event, isect ) >= 0 ) { + Sweep.regionAbove(regUp).dirty = regUp.dirty = true; + tess.mesh.splitEdge( eUp.Sym ); + eUp.Org.s = tess.event.s; + eUp.Org.t = tess.event.t; + } + if( Geom.edgeSign( dstLo, tess.event, isect ) <= 0 ) { + regUp.dirty = regLo.dirty = true; + tess.mesh.splitEdge( eLo.Sym ); + eLo.Org.s = tess.event.s; + eLo.Org.t = tess.event.t; + } + /* leave the rest for ConnectRightVertex */ + return false; + } + + /* General case -- split both edges, splice into new vertex. + * When we do the splice operation, the order of the arguments is + * arbitrary as far as correctness goes. However, when the operation + * creates a new face, the work done is proportional to the size of + * the new face. We expect the faces in the processed part of + * the mesh (ie. eUp->Lface) to be smaller than the faces in the + * unprocessed original contours (which will be eLo->Oprev->Lface). + */ + tess.mesh.splitEdge( eUp.Sym ); + tess.mesh.splitEdge( eLo.Sym ); + tess.mesh.splice( eLo.getOprev(), eUp ); + eUp.Org.s = isect.s; + eUp.Org.t = isect.t; + eUp.Org.pqHandle = tess.pq.insert( eUp.Org ); + Sweep.getIntersectData( tess, eUp.Org, orgUp, dstUp, orgLo, dstLo ); + Sweep.regionAbove(regUp).dirty = regUp.dirty = regLo.dirty = true; + return false; + } + + //static void WalkDirtyRegions( TESStesselator *tess, ActiveRegion *regUp ) + public static function walkDirtyRegions( tess : Tessellator, regUp : ActiveRegion ) : Void { + /* + * When the upper or lower edge of any region changes, the region is + * marked "dirty". This routine walks through all the dirty regions + * and makes sure that the dictionary invariants are satisfied + * (see the comments at the beginning of this file). Of course + * new dirty regions can be created as we make changes to restore + * the invariants. + */ + var regLo = Sweep.regionBelow(regUp); + var eUp, eLo; + + while(true){ + //for( ;; ) { + /* Find the lowest dirty region (we walk from the bottom up). */ + while( regLo.dirty ) { + regUp = regLo; + regLo = Sweep.regionBelow(regLo); + } + if( ! regUp.dirty ) { + regLo = regUp; + regUp = Sweep.regionAbove( regUp ); + if( regUp == null || ! regUp.dirty ) { + /* We've walked all the dirty regions */ + return; + } + } + regUp.dirty = false; + eUp = regUp.eUp; + eLo = regLo.eUp; + + if( eUp.getDst() != eLo.getDst() ) { + /* Check that the edge ordering is obeyed at the Dst vertices. */ + if( Sweep.checkForLeftSplice( tess, regUp )) { + + /* If the upper or lower edge was marked fixUpperEdge, then + * we no longer need it (since these edges are needed only for + * vertices which otherwise have no right-going edges). + */ + if( regLo.fixUpperEdge ) { + Sweep.deleteRegion( tess, regLo ); + tess.mesh.delete( eLo ); + regLo = Sweep.regionBelow( regUp ); + eLo = regLo.eUp; + } else if( regUp.fixUpperEdge ) { + Sweep.deleteRegion( tess, regUp ); + tess.mesh.delete( eUp ); + regUp = Sweep.regionAbove( regLo ); + eUp = regUp.eUp; + } + } + } + if( eUp.Org != eLo.Org ) { + if( eUp.getDst() != eLo.getDst() + && ! regUp.fixUpperEdge && ! regLo.fixUpperEdge + && (eUp.getDst() == tess.event || eLo.getDst() == tess.event) ) + { + /* When all else fails in CheckForIntersect(), it uses tess->event + * as the intersection location. To make this possible, it requires + * that tess->event lie between the upper and lower edges, and also + * that neither of these is marked fixUpperEdge (since in the worst + * case it might splice one of these edges into tess->event, and + * violate the invariant that fixable edges are the only right-going + * edge from their associated vertex). + */ + if( Sweep.checkForIntersect( tess, regUp )) { + /* WalkDirtyRegions() was called recursively; we're done */ + return; + } + } else { + /* Even though we can't use CheckForIntersect(), the Org vertices + * may violate the dictionary edge ordering. Check and correct this. + */ + Sweep.checkForRightSplice( tess, regUp ); + } + } + if( eUp.Org == eLo.Org && eUp.getDst() == eLo.getDst() ) { + /* A degenerate loop consisting of only two edges -- delete it. */ + Sweep.addWinding( eLo, eUp ); + Sweep.deleteRegion( tess, regUp ); + tess.mesh.delete( eUp ); + regUp = Sweep.regionAbove( regLo ); + } + } + } + + + //static void ConnectRightVertex( TESStesselator *tess, ActiveRegion *regUp, TESShalfEdge *eBottomLeft ) + public static function connectRightVertex( tess : Tessellator, regUp : ActiveRegion, eBottomLeft : TESShalfEdge ) : Void { + /* + * Purpose: connect a "right" vertex vEvent (one where all edges go left) + * to the unprocessed portion of the mesh. Since there are no right-going + * edges, two regions (one above vEvent and one below) are being merged + * into one. "regUp" is the upper of these two regions. + * + * There are two reasons for doing this (adding a right-going edge): + * - if the two regions being merged are "inside", we must add an edge + * to keep them separated (the combined region would not be monotone). + * - in any case, we must leave some record of vEvent in the dictionary, + * so that we can merge vEvent with features that we have not seen yet. + * For example, maybe there is a vertical edge which passes just to + * the right of vEvent; we would like to splice vEvent into this edge. + * + * However, we don't want to connect vEvent to just any vertex. We don''t + * want the new edge to cross any other edges; otherwise we will create + * intersection vertices even when the input data had no self-intersections. + * (This is a bad thing; if the user's input data has no intersections, + * we don't want to generate any false intersections ourselves.) + * + * Our eventual goal is to connect vEvent to the leftmost unprocessed + * vertex of the combined region (the union of regUp and regLo). + * But because of unseen vertices with all right-going edges, and also + * new vertices which may be created by edge intersections, we don''t + * know where that leftmost unprocessed vertex is. In the meantime, we + * connect vEvent to the closest vertex of either chain, and mark the region + * as "fixUpperEdge". This flag says to delete and reconnect this edge + * to the next processed vertex on the boundary of the combined region. + * Quite possibly the vertex we connected to will turn out to be the + * closest one, in which case we won''t need to make any changes. + */ + var eNew; + var eTopLeft = eBottomLeft.Onext; + var regLo = Sweep.regionBelow(regUp); + var eUp = regUp.eUp; + var eLo = regLo.eUp; + var degenerate = false; + + if( eUp.getDst() != eLo.getDst() ) { + Sweep.checkForIntersect( tess, regUp ); + } + + /* Possible new degeneracies: upper or lower edge of regUp may pass + * through vEvent, or may coincide with new intersection vertex + */ + if( Geom.vertEq( eUp.Org, tess.event )) { + tess.mesh.splice( eTopLeft.getOprev(), eUp ); + regUp = Sweep.topLeftRegion( tess, regUp ); + eTopLeft = Sweep.regionBelow( regUp ).eUp; + Sweep.finishLeftRegions( tess, Sweep.regionBelow(regUp), regLo ); + degenerate = true; + } + if( Geom.vertEq( eLo.Org, tess.event )) { + tess.mesh.splice( eBottomLeft, eLo.getOprev() ); + eBottomLeft = Sweep.finishLeftRegions( tess, regLo, null ); + degenerate = true; + } + if( degenerate ) { + Sweep.addRightEdges( tess, regUp, eBottomLeft.Onext, eTopLeft, eTopLeft, true ); + return; + } + + /* Non-degenerate situation -- need to add a temporary, fixable edge. + * Connect to the closer of eLo->Org, eUp->Org. + */ + if( Geom.vertLeq( eLo.Org, eUp.Org )) { + eNew = eLo.getOprev(); + } else { + eNew = eUp; + } + eNew = tess.mesh.connect( eBottomLeft.getLprev(), eNew ); + + /* Prevent cleanup, otherwise eNew might disappear before we've even + * had a chance to mark it as a temporary edge. + */ + Sweep.addRightEdges( tess, regUp, eNew, eNew.Onext, eNew.Onext, false ); + eNew.Sym.activeRegion.fixUpperEdge = true; + Sweep.walkDirtyRegions( tess, regUp ); + } + + /* Because vertices at exactly the same location are merged together + * before we process the sweep event, some degenerate cases can't occur. + * However if someone eventually makes the modifications required to + * merge features which are close together, the cases below marked + * TOLERANCE_NONZERO will be useful. They were debugged before the + * code to merge identical vertices in the main loop was added. + */ + //#define TOLERANCE_NONZERO FALSE + + //static void ConnectLeftDegenerate( TESStesselator *tess, ActiveRegion *regUp, TESSvertex *vEvent ) + public static function connectLeftDegenerate( tess : Tessellator, regUp : ActiveRegion, vEvent ) { + /* + * The event vertex lies exacty on an already-processed edge or vertex. + * Adding the new vertex involves splicing it into the already-processed + * part of the mesh. + */ + var e, eTopLeft, eTopRight, eLast; + var reg; + + e = regUp.eUp; + if( Geom.vertEq( e.Org, vEvent )) { + /* e->Org is an unprocessed vertex - just combine them, and wait + * for e->Org to be pulled from the queue + */ + Tess2.assert( false /*TOLERANCE_NONZERO*/ ); + Sweep.spliceMergeVertices( tess, e, vEvent.anEdge ); + return; + } + + if( ! Geom.vertEq( e.getDst(), vEvent )) { + /* General case -- splice vEvent into edge e which passes through it */ + tess.mesh.splitEdge( e.Sym ); + if( regUp.fixUpperEdge ) { + /* This edge was fixable -- delete unused portion of original edge */ + tess.mesh.delete( e.Onext ); + regUp.fixUpperEdge = false; + } + tess.mesh.splice( vEvent.anEdge, e ); + Sweep.sweepEvent( tess, vEvent ); /* recurse */ + return; + } + + /* vEvent coincides with e->Dst, which has already been processed. + * Splice in the additional right-going edges. + */ + Tess2.assert( false /*TOLERANCE_NONZERO*/ ); + regUp = Sweep.topRightRegion( regUp ); + reg = Sweep.regionBelow( regUp ); + eTopRight = reg.eUp.Sym; + eTopLeft = eLast = eTopRight.Onext; + if( reg.fixUpperEdge ) { + /* Here e->Dst has only a single fixable edge going right. + * We can delete it since now we have some real right-going edges. + */ + Tess2.assert( eTopLeft != eTopRight ); /* there are some left edges too */ + Sweep.deleteRegion( tess, reg ); + tess.mesh.delete( eTopRight ); + eTopRight = eTopLeft.getOprev(); + } + tess.mesh.splice( vEvent.anEdge, eTopRight ); + if( ! Geom.edgeGoesLeft( eTopLeft )) { + /* e->Dst had no left-going edges -- indicate this to AddRightEdges() */ + eTopLeft = null; + } + Sweep.addRightEdges( tess, regUp, eTopRight.Onext, eLast, eTopLeft, true ); + } + + + //static void ConnectLeftVertex( TESStesselator *tess, TESSvertex *vEvent ) + public static function connectLeftVertex( tess : Tessellator, vEvent : TESSvertex ) { + /* + * Purpose: connect a "left" vertex (one where both edges go right) + * to the processed portion of the mesh. Let R be the active region + * containing vEvent, and let U and L be the upper and lower edge + * chains of R. There are two possibilities: + * + * - the normal case: split R into two regions, by connecting vEvent to + * the rightmost vertex of U or L lying to the left of the sweep line + * + * - the degenerate case: if vEvent is close enough to U or L, we + * merge vEvent into that edge chain. The subcases are: + * - merging with the rightmost vertex of U or L + * - merging with the active edge of U or L + * - merging with an already-processed portion of U or L + */ + var regUp : ActiveRegion, regLo : ActiveRegion, reg : ActiveRegion; + var eUp : TESShalfEdge, eLo : TESShalfEdge, eNew : TESShalfEdge; + var tmp = new ActiveRegion(); + + /* Tess2.assert( vEvent->anEdge->Onext->Onext == vEvent->anEdge ); */ + + /* Get a pointer to the active region containing vEvent */ + tmp.eUp = vEvent.anEdge.Sym; + /* __GL_DICTLISTKEY */ /* tessDictListSearch */ + regUp = tess.dict.search( tmp ).key; + regLo = Sweep.regionBelow( regUp ); + if( regLo == null ) { + //This may happen if the input polygon is coplanar. + return; + } + eUp = regUp.eUp; + eLo = regLo.eUp; + + /* Try merging with U or L first */ + if( Geom.edgeSign( eUp.getDst(), vEvent, eUp.Org ) == 0.0 ) { + Sweep.connectLeftDegenerate( tess, regUp, vEvent ); + return; + } + + /* Connect vEvent to rightmost processed vertex of either chain. + * e->Dst is the vertex that we will connect to vEvent. + */ + reg = Geom.vertLeq( eLo.getDst(), eUp.getDst() ) ? regUp : regLo; + + if( regUp.inside || reg.fixUpperEdge) { + if( reg == regUp ) { + eNew = tess.mesh.connect( vEvent.anEdge.Sym, eUp.Lnext ); + } else { + var tempHalfEdge = tess.mesh.connect( eLo.getDnext(), vEvent.anEdge); + eNew = tempHalfEdge.Sym; + } + if( reg.fixUpperEdge ) { + Sweep.fixUpperEdge( tess, reg, eNew ); + } else { + Sweep.computeWinding( tess, Sweep.addRegionBelow( tess, regUp, eNew )); + } + Sweep.sweepEvent( tess, vEvent ); + } else { + /* The new vertex is in a region which does not belong to the polygon. + * We don''t need to connect this vertex to the rest of the mesh. + */ + Sweep.addRightEdges( tess, regUp, vEvent.anEdge, vEvent.anEdge, null, true ); + } + } + + //static void SweepEvent( TESStesselator *tess, TESSvertex *vEvent ) + public static function sweepEvent( tess : Tessellator, vEvent : TESSvertex ) { + /* + * Does everything necessary when the sweep line crosses a vertex. + * Updates the mesh and the edge dictionary. + */ + + tess.event = vEvent; /* for access in EdgeLeq() */ + Sweep.debugEvent( tess ); + + /* Check if this vertex is the right endpoint of an edge that is + * already in the dictionary. In this case we don't need to waste + * time searching for the location to insert new edges. + */ + var e = vEvent.anEdge; + while( e.activeRegion == null ) { + e = e.Onext; + if( e == vEvent.anEdge ) { + /* All edges go right -- not incident to any processed edges */ + Sweep.connectLeftVertex( tess, vEvent ); + return; + } + } + + /* Processing consists of two phases: first we "finish" all the + * active regions where both the upper and lower edges terminate + * at vEvent (ie. vEvent is closing off these regions). + * We mark these faces "inside" or "outside" the polygon according + * to their winding number, and delete the edges from the dictionary. + * This takes care of all the left-going edges from vEvent. + */ + var regUp = Sweep.topLeftRegion( tess, e.activeRegion ); + Tess2.assert( regUp != null ); + // if (regUp == NULL) longjmp(tess->env,1); + var reg = Sweep.regionBelow( regUp ); + var eTopLeft = reg.eUp; + var eBottomLeft = Sweep.finishLeftRegions( tess, reg, null ); + + /* Next we process all the right-going edges from vEvent. This + * involves adding the edges to the dictionary, and creating the + * associated "active regions" which record information about the + * regions between adjacent dictionary edges. + */ + if( eBottomLeft.Onext == eTopLeft ) { + /* No right-going edges -- add a temporary "fixable" edge */ + Sweep.connectRightVertex( tess, regUp, eBottomLeft ); + } else { + Sweep.addRightEdges( tess, regUp, eBottomLeft.Onext, eTopLeft, eTopLeft, true ); + } + } + + /* Make the sentinel coordinates big enough that they will never be + * merged with real input features. + */ + + //static void AddSentinel( TESStesselator *tess, TESSreal smin, TESSreal smax, TESSreal t ) + public static function addSentinel( tess : Tessellator, smin : Float, smax : Float, t : Float ) { + /* + * We add two sentinel edges above and below all other edges, + * to avoid special cases at the top and bottom. + */ + var reg = new ActiveRegion(); + var e = tess.mesh.makeEdge(); + // if (e == NULL) longjmp(tess->env,1); + + e.Org.s = smax; + e.Org.t = t; + e.getDst().s = smin; + e.getDst().t = t; + tess.event = e.getDst(); /* initialize it */ + + reg.eUp = e; + reg.windingNumber = 0; + reg.inside = false; + reg.fixUpperEdge = false; + reg.sentinel = true; + reg.dirty = false; + reg.nodeUp = tess.dict.insert( reg ); + // if (reg->nodeUp == NULL) longjmp(tess->env,1); + } + + //static void InitEdgeDict( TESStesselator *tess ) + public static function initEdgeDict( tess : Tessellator ) { + /* + * We maintain an ordering of edge intersections with the sweep line. + * This order is maintained in a dynamic dictionary. + */ + tess.dict = new Dict( tess, Sweep.edgeLeq ); + // if (tess->dict == NULL) longjmp(tess->env,1); + + var w = (tess.bmax[0] - tess.bmin[0]); + var h = (tess.bmax[1] - tess.bmin[1]); + + var smin = tess.bmin[0] - w; + var smax = tess.bmax[0] + w; + var tmin = tess.bmin[1] - h; + var tmax = tess.bmax[1] + h; + + Sweep.addSentinel( tess, smin, smax, tmin ); + Sweep.addSentinel( tess, smin, smax, tmax ); + } + + public static function doneEdgeDict( tess : Tessellator ) + { + var reg; + var fixedEdges = 0; + + while( (reg = tess.dict.min().key) != null ) { + /* + * At the end of all processing, the dictionary should contain + * only the two sentinel edges, plus at most one "fixable" edge + * created by ConnectRightVertex(). + */ + if( ! reg.sentinel ) { + Tess2.assert( reg.fixUpperEdge ); + Tess2.assert( ++fixedEdges == 1 ); + } + Tess2.assert( reg.windingNumber == 0 ); + Sweep.deleteRegion( tess, reg ); + /* tessMeshDelete( reg->eUp );*/ + } + // dictDeleteDict( &tess->alloc, tess->dict ); + } + + public static function removeDegenerateEdges( tess : Tessellator ) { + /* + * Remove zero-length edges, and contours with fewer than 3 vertices. + */ + var eNext : TESShalfEdge, eLnext : TESShalfEdge ; + var eHead : TESShalfEdge = tess.mesh.eHead; + + /*LINTED*/ + var e = eHead.next; + while (e != eHead){ + //for( e = eHead.next; e != eHead; e = eNext ) { + eNext = e.next; + eLnext = e.Lnext; + + if( Geom.vertEq( e.Org, e.getDst() ) && e.Lnext.Lnext != e ) { + /* Zero-length edge, contour has at least 3 edges */ + Sweep.spliceMergeVertices( tess, eLnext, e ); /* deletes e->Org */ + tess.mesh.delete( e ); /* e is a self-loop */ + e = eLnext; + eLnext = e.Lnext; + } + if( eLnext.Lnext == e ) { + /* Degenerate contour (one or two edges) */ + if( eLnext != e ) { + if( eLnext == eNext || eLnext == eNext.Sym ) { eNext = eNext.next; } + tess.mesh.delete( eLnext ); + } + if( e == eNext || e == eNext.Sym ) { eNext = eNext.next; } + tess.mesh.delete( e ); + } + e = eNext; + } + } + + public static function initPriorityQ( tess : Tessellator ) { + /* + * Insert all vertices into the priority queue which determines the + * order in which vertices cross the sweep line. + */ + var pq : PriorityQ; + var vHead : TESSvertex; + var vertexCount = 0; + + vHead = tess.mesh.vHead; + var v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + vertexCount++; + v = v.next; + } + /* Make sure there is enough space for sentinels. */ + vertexCount += 8; //MAX( 8, tess->alloc.extraVertices ); + + pq = tess.pq = new PriorityQ( vertexCount, Geom.vertLeq ); + // if (pq == NULL) return 0; + + vHead = tess.mesh.vHead; + v = vHead.next; + while(v != vHead){ + //for( v = vHead.next; v != vHead; v = v.next ) { + v.pqHandle = pq.insert( v ); + // if (v.pqHandle == INV_HANDLE) + // break; + v = v.next; + } + + if (v != vHead) { + return false; + } + + pq.init(); + + return true; + } + + public static function donePriorityQ( tess : Tessellator ) { + tess.pq = null; + } + + public static function removeDegenerateFaces( tess : Tessellator, mesh : TESSmesh ) { + /* + * Delete any degenerate faces with only two edges. WalkDirtyRegions() + * will catch almost all of these, but it won't catch degenerate faces + * produced by splice operations on already-processed edges. + * The two places this can happen are in FinishLeftRegions(), when + * we splice in a "temporary" edge produced by ConnectRightVertex(), + * and in CheckForLeftSplice(), where we splice already-processed + * edges to ensure that our dictionary invariants are not violated + * by numerical errors. + * + * In both these cases it is *very* dangerous to delete the offending + * edge at the time, since one of the routines further up the stack + * will sometimes be keeping a pointer to that edge. + */ + var fNext; + var e; + + /*LINTED*/ + var f = mesh.fHead.next; + while (f != mesh.fHead ){ + //for( f = mesh.fHead.next; f != mesh.fHead; f = fNext ) { + fNext = f.next; + e = f.anEdge; + Tess2.assert( e.Lnext != e ); + + if( e.Lnext.Lnext == e ) { + /* A face with only two edges */ + Sweep.addWinding( e.Onext, e ); + tess.mesh.delete( e ); + } + f = fNext; + } + return true; + } + + public static function computeInterior( tess : Tessellator) { + /* + * tessComputeInterior( tess ) computes the planar arrangement specified + * by the given contours, and further subdivides this arrangement + * into regions. Each region is marked "inside" if it belongs + * to the polygon, according to the rule given by tess->windingRule. + * Each interior region is guaranteed be monotone. + */ + var v, vNext; + + /* Each vertex defines an event for our sweep line. Start by inserting + * all the vertices in a priority queue. Events are processed in + * lexicographic order, ie. + * + * e1 < e2 iff e1.x < e2.x || (e1.x == e2.x && e1.y < e2.y) + */ + Sweep.removeDegenerateEdges( tess ); + if ( !Sweep.initPriorityQ( tess ) ) return false; /* if error */ + Sweep.initEdgeDict( tess ); + + while( (v = tess.pq.extractMin()) != null ) { + while (true){ + //for( ;; ) { + vNext = tess.pq.min(); + if( vNext == null || ! Geom.vertEq( vNext, v )) break; + + /* Merge together all vertices at exactly the same location. + * This is more efficient than processing them one at a time, + * simplifies the code (see ConnectLeftDegenerate), and is also + * important for correct handling of certain degenerate cases. + * For example, suppose there are two identical edges A and B + * that belong to different contours (so without this code they would + * be processed by separate sweep events). Suppose another edge C + * crosses A and B from above. When A is processed, we split it + * at its intersection point with C. However this also splits C, + * so when we insert B we may compute a slightly different + * intersection point. This might leave two edges with a small + * gap between them. This kind of error is especially obvious + * when using boundary extraction (TESS_BOUNDARY_ONLY). + */ + vNext = tess.pq.extractMin(); + Sweep.spliceMergeVertices( tess, v.anEdge, vNext.anEdge ); + } + Sweep.sweepEvent( tess, v ); + } + + /* Set tess->event for debugging purposes */ + tess.event = tess.dict.min().key.eUp.Org; + Sweep.debugEvent( tess ); + Sweep.doneEdgeDict( tess ); + Sweep.donePriorityQ( tess ); + + if ( !Sweep.removeDegenerateFaces( tess, tess.mesh ) ) return false; + tess.mesh.check(); + + return true; + } +} + +class Tessellator { + + public var mesh : TESSmesh; + public var normal : Array; + public var sUnit : Array; + public var tUnit : Array; + + public var bmin : Array; + public var bmax : Array; + + public var windingRule : Int; + + public var dict : Dict; + public var pq : PriorityQ; + public var event : TESSvertex; + + public var vertexIndexCounter : Int; + + public var vertices : Array; + public var vertexIndices : Array; + public var vertexCount : Int; + public var elements : Array; + public var elementCount : Int; + + public function new() { + + /*** state needed for collecting the input data ***/ + this.mesh = null; /* stores the input contours, and eventually + the tessellation itself */ + + /*** state needed for projecting onto the sweep plane ***/ + + this.normal = [0.0, 0.0, 0.0]; /* user-specified normal (if provided) */ + this.sUnit = [0.0, 0.0, 0.0]; /* unit vector in s-direction (debugging) */ + this.tUnit = [0.0, 0.0, 0.0]; /* unit vector in t-direction (debugging) */ + + this.bmin = [0.0, 0.0]; + this.bmax = [0.0, 0.0]; + + /*** state needed for the line sweep ***/ + this.windingRule = Tess2.WINDING_ODD; /* rule for determining polygon interior */ + + this.dict = null; /* edge dictionary for sweep line */ + this.pq = null; /* priority queue of vertex events */ + this.event = null; /* current sweep event being processed */ + + this.vertexIndexCounter = 0; + + this.vertices = []; + this.vertexIndices = []; + this.vertexCount = 0; + this.elements = []; + this.elementCount = 0; + } + + public function dot_(u : Array, v : Array) : Float { + return (u[0]*v[0] + u[1]*v[1] + u[2]*v[2]); + } + + public function normalize_( v : Array ) { + var len = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; + Tess2.assert( len > 0.0 ); + len = Math.sqrt( len ); + v[0] /= len; + v[1] /= len; + v[2] /= len; + } + + public function longAxis_( v : Array) : Int { + var i = 0; + if( Math.abs(v[1]) > Math.abs(v[0]) ) { i = 1; } + if( Math.abs(v[2]) > Math.abs(v[i]) ) { i = 2; } + return i; + } + + public function computeNormal_( norm : Array) : Void + { + var v, v1, v2; + var c, tLen2, maxLen2; + var maxVal = [0.0,0.0,0.0], minVal = [0.0,0.0,0.0], d1 = [0.0,0.0,0.0], d2 = [0.0,0.0,0.0], tNorm = [0.0,0.0,0.0]; + var maxVert = [null,null,null], minVert = [null,null,null]; + var vHead = this.mesh.vHead; + + v = vHead.next; + for ( i in 0...3 ){ +// for( i = 0; i < 3; ++i ) { + c = v.coords[i]; + minVal[i] = c; + minVert[i] = v; + maxVal[i] = c; + maxVert[i] = v; + } + + v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + for ( i in 0...3){ + //for( i = 0; i < 3; ++i ) { + c = v.coords[i]; + if( c < minVal[i] ) { minVal[i] = c; minVert[i] = v; } + if( c > maxVal[i] ) { maxVal[i] = c; maxVert[i] = v; } + } + v = v.next; + } + + /* Find two vertices separated by at least 1/sqrt(3) of the maximum + * distance between any two vertices + */ + var i = 0; + if( maxVal[1] - minVal[1] > maxVal[0] - minVal[0] ) { i = 1; } + if( maxVal[2] - minVal[2] > maxVal[i] - minVal[i] ) { i = 2; } + if( minVal[i] >= maxVal[i] ) { + /* All vertices are the same -- normal doesn't matter */ + norm[0] = 0.0; norm[1] = 0.0; norm[2] = 1.0; + return; + } + + /* Look for a third vertex which forms the triangle with maximum area + * (Length of normal == twice the triangle area) + */ + maxLen2 = 0.0; + v1 = minVert[i]; + v2 = maxVert[i]; + d1[0] = v1.coords[0] - v2.coords[0]; + d1[1] = v1.coords[1] - v2.coords[1]; + d1[2] = v1.coords[2] - v2.coords[2]; + v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + d2[0] = v.coords[0] - v2.coords[0]; + d2[1] = v.coords[1] - v2.coords[1]; + d2[2] = v.coords[2] - v2.coords[2]; + tNorm[0] = d1[1]*d2[2] - d1[2]*d2[1]; + tNorm[1] = d1[2]*d2[0] - d1[0]*d2[2]; + tNorm[2] = d1[0]*d2[1] - d1[1]*d2[0]; + tLen2 = tNorm[0]*tNorm[0] + tNorm[1]*tNorm[1] + tNorm[2]*tNorm[2]; + if( tLen2 > maxLen2 ) { + maxLen2 = tLen2; + norm[0] = tNorm[0]; + norm[1] = tNorm[1]; + norm[2] = tNorm[2]; + } + v = v.next; + } + + if( maxLen2 <= 0 ) { + /* All points lie on a single line -- any decent normal will do */ + norm[0] = norm[1] = norm[2] = 0; + norm[this.longAxis_(d1)] = 1; + } + } + + public function checkOrientation_() { + var area; + var f, fHead = this.mesh.fHead; + var v, vHead = this.mesh.vHead; + var e; + + /* When we compute the normal automatically, we choose the orientation + * so that the the sum of the signed areas of all contours is non-negative. + */ + area = 0.0; + f = fHead.next; + while (f != fHead){ +// for( f = fHead.next; f != fHead; f = f.next ) { + e = f.anEdge; + if( e.winding <= 0 ) continue; + do { + area += (e.Org.s - e.getDst().s) * (e.Org.t + e.getDst().t); + e = e.Lnext; + } while( e != f.anEdge ); + f = f.next; + } + if( area < 0 ) { + /* Reverse the orientation by flipping all the t-coordinates */ + v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + v.t = - v.t; + v = v.next; + } + this.tUnit[0] = - this.tUnit[0]; + this.tUnit[1] = - this.tUnit[1]; + this.tUnit[2] = - this.tUnit[2]; + } + } + +/* #ifdef FOR_TRITE_TEST_PROGRAM + #include + extern int RandomSweep; + #define S_UNIT_X (RandomSweep ? (2*drand48()-1) : 1.0) + #define S_UNIT_Y (RandomSweep ? (2*drand48()-1) : 0.0) + #else + #if defined(SLANTED_SWEEP) */ + /* The "feature merging" is not intended to be complete. There are + * special cases where edges are nearly parallel to the sweep line + * which are not implemented. The algorithm should still behave + * robustly (ie. produce a reasonable tesselation) in the presence + * of such edges, however it may miss features which could have been + * merged. We could minimize this effect by choosing the sweep line + * direction to be something unusual (ie. not parallel to one of the + * coordinate axes). + */ +/* #define S_UNIT_X (TESSreal)0.50941539564955385 //Pre-normalized + #define S_UNIT_Y (TESSreal)0.86052074622010633 + #else + #define S_UNIT_X (TESSreal)1.0 + #define S_UNIT_Y (TESSreal)0.0 + #endif + #endif*/ + + /* Determine the polygon normal and project vertices onto the plane + * of the polygon. + */ + public function projectPolygon_() { + var v, vHead = this.mesh.vHead; + var norm = [0.0,0.0,0.0]; + var sUnit, tUnit; + var i, first, computedNormal = false; + + norm[0] = this.normal[0]; + norm[1] = this.normal[1]; + norm[2] = this.normal[2]; + if( norm[0] == 0.0 && norm[1] == 0.0 && norm[2] == 0.0 ) { + this.computeNormal_( norm ); + computedNormal = true; + } + sUnit = this.sUnit; + tUnit = this.tUnit; + i = this.longAxis_( norm ); + +/* #if defined(FOR_TRITE_TEST_PROGRAM) || defined(TRUE_PROJECT) + //Choose the initial sUnit vector to be approximately perpendicular + //to the normal. + + Normalize( norm ); + + sUnit[i] = 0; + sUnit[(i+1)%3] = S_UNIT_X; + sUnit[(i+2)%3] = S_UNIT_Y; + + //Now make it exactly perpendicular + w = Dot( sUnit, norm ); + sUnit[0] -= w * norm[0]; + sUnit[1] -= w * norm[1]; + sUnit[2] -= w * norm[2]; + Normalize( sUnit ); + + //Choose tUnit so that (sUnit,tUnit,norm) form a right-handed frame + tUnit[0] = norm[1]*sUnit[2] - norm[2]*sUnit[1]; + tUnit[1] = norm[2]*sUnit[0] - norm[0]*sUnit[2]; + tUnit[2] = norm[0]*sUnit[1] - norm[1]*sUnit[0]; + Normalize( tUnit ); + #else*/ + /* Project perpendicular to a coordinate axis -- better numerically */ + sUnit[i] = 0; + sUnit[(i+1)%3] = 1.0; + sUnit[(i+2)%3] = 0.0; + + tUnit[i] = 0; + tUnit[(i+1)%3] = 0.0; + tUnit[(i+2)%3] = (norm[i] > 0) ? 1.0 : -1.0; +// #endif + + /* Project the vertices onto the sweep plane */ + v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + v.s = this.dot_( v.coords, sUnit ); + v.t = this.dot_( v.coords, tUnit ); + v = v.next; + } + if( computedNormal ) { + this.checkOrientation_(); + } + + /* Compute ST bounds. */ + first = true; + v = vHead.next; + while (v != vHead){ +// for( v = vHead.next; v != vHead; v = v.next ) { + if (first) { + this.bmin[0] = this.bmax[0] = v.s; + this.bmin[1] = this.bmax[1] = v.t; + first = false; + } else { + if (v.s < this.bmin[0]) this.bmin[0] = v.s; + if (v.s > this.bmax[0]) this.bmax[0] = v.s; + if (v.t < this.bmin[1]) this.bmin[1] = v.t; + if (v.t > this.bmax[1]) this.bmax[1] = v.t; + } + v = v.next; + } + } + + public function addWinding_(eDst,eSrc) { + eDst.winding += eSrc.winding; + eDst.Sym.winding += eSrc.Sym.winding; + } + + /* tessMeshTessellateMonoRegion( face ) tessellates a monotone region + * (what else would it do??) The region must consist of a single + * loop of half-edges (see mesh.h) oriented CCW. "Monotone" in this + * case means that any vertical line intersects the interior of the + * region in a single interval. + * + * Tessellation consists of adding interior edges (actually pairs of + * half-edges), to split the region into non-overlapping triangles. + * + * The basic idea is explained in Preparata and Shamos (which I don''t + * have handy right now), although their implementation is more + * complicated than this one. The are two edge chains, an upper chain + * and a lower chain. We process all vertices from both chains in order, + * from right to left. + * + * The algorithm ensures that the following invariant holds after each + * vertex is processed: the untessellated region consists of two + * chains, where one chain (say the upper) is a single edge, and + * the other chain is concave. The left vertex of the single edge + * is always to the left of all vertices in the concave chain. + * + * Each step consists of adding the rightmost unprocessed vertex to one + * of the two chains, and forming a fan of triangles from the rightmost + * of two chain endpoints. Determining whether we can add each triangle + * to the fan is a simple orientation test. By making the fan as large + * as possible, we restore the invariant (check it yourself). + */ + // int tessMeshTessellateMonoRegion( TESSmesh *mesh, TESSface *face ) + public function tessellateMonoRegion_( mesh : TESSmesh, face : TESSface ) { + var up, lo; + + /* All edges are oriented CCW around the boundary of the region. + * First, find the half-edge whose origin vertex is rightmost. + * Since the sweep goes from left to right, face->anEdge should + * be close to the edge we want. + */ + up = face.anEdge; + Tess2.assert( up.Lnext != up && up.Lnext.Lnext != up ); + +// for( ; Geom.vertLeq( up.Dst, up.Org ); up = up.Lprev ) +// ; + while (Geom.vertLeq( up.getDst(), up.Org )){ + up = up.getLprev(); + } + +// for( ; Geom.vertLeq( up.Org, up.getDst() ); up = up.Lnext; ) +// ; + while(Geom.vertLeq( up.Org, up.getDst() )){ + up = up.Lnext; + } + + lo = up.getLprev(); + + while( up.Lnext != lo ) { + if( Geom.vertLeq( up.getDst(), lo.Org )) { + /* up->Dst is on the left. It is safe to form triangles from lo->Org. + * The EdgeGoesLeft test guarantees progress even when some triangles + * are CW, given that the upper and lower chains are truly monotone. + */ + while( lo.Lnext != up && (Geom.edgeGoesLeft( lo.Lnext ) + || Geom.edgeSign( lo.Org, lo.getDst(), lo.Lnext.getDst() ) <= 0.0 )) { + var tempHalfEdge = mesh.connect( lo.Lnext, lo ); + //if (tempHalfEdge == NULL) return 0; + lo = tempHalfEdge.Sym; + } + lo = lo.getLprev(); + } else { + /* lo->Org is on the left. We can make CCW triangles from up->Dst. */ + while( lo.Lnext != up && (Geom.edgeGoesRight( up.getLprev() ) + || Geom.edgeSign( up.getDst(), up.Org, up.getLprev().Org ) >= 0.0 )) { + var tempHalfEdge = mesh.connect( up, up.getLprev() ); + //if (tempHalfEdge == NULL) return 0; + up = tempHalfEdge.Sym; + } + up = up.Lnext; + } + } + + /* Now lo->Org == up->Dst == the leftmost vertex. The remaining region + * can be tessellated in a fan from this leftmost vertex. + */ + Tess2.assert( lo.Lnext != up ); + while( lo.Lnext.Lnext != up ) { + var tempHalfEdge = mesh.connect( lo.Lnext, lo ); + //if (tempHalfEdge == NULL) return 0; + lo = tempHalfEdge.Sym; + } + + return true; + } + + + /* tessMeshTessellateInterior( mesh ) tessellates each region of + * the mesh which is marked "inside" the polygon. Each such region + * must be monotone. + */ + //int tessMeshTessellateInterior( TESSmesh *mesh ) + public function tessellateInterior_( mesh : TESSmesh ) { + var next; + + /*LINTED*/ + var f = mesh.fHead.next; + while (f != mesh.fHead ){ +// for( f = mesh.fHead.next; f != mesh.fHead; f = next ) { + /* Make sure we don''t try to tessellate the new triangles. */ + next = f.next; + if( f.inside ) { + if ( !this.tessellateMonoRegion_( mesh, f ) ) return false; + } + f = next; + } + + return true; + } + + /* tessMeshDiscardExterior( mesh ) zaps (ie. sets to NULL) all faces + * which are not marked "inside" the polygon. Since further mesh operations + * on NULL faces are not allowed, the main purpose is to clean up the + * mesh so that exterior loops are not represented in the data structure. + */ + //void tessMeshDiscardExterior( TESSmesh *mesh ) + public function discardExterior_( mesh : TESSmesh ) { + var next; + + /*LINTED*/ + var f = mesh.fHead.next; + while (f != mesh.fHead ){ +// for( f = mesh.fHead.next; f != mesh.fHead; f = next ) { + /* Since f will be destroyed, save its next pointer. */ + next = f.next; + if( ! f.inside ) { + mesh.zapFace( f ); + } + f = next; + } + } + + /* tessMeshSetWindingNumber( mesh, value, keepOnlyBoundary ) resets the + * winding numbers on all edges so that regions marked "inside" the + * polygon have a winding number of "value", and regions outside + * have a winding number of 0. + * + * If keepOnlyBoundary is TRUE, it also deletes all edges which do not + * separate an interior region from an exterior one. + */ + // int tessMeshSetWindingNumber( TESSmesh *mesh, int value, int keepOnlyBoundary ) + public function setWindingNumber_( mesh : TESSmesh, value : Int, keepOnlyBoundary : Bool ) { + var e : TESShalfEdge, eNext : TESShalfEdge; + + var e = mesh.eHead.next; + while ( e != mesh.eHead ){ +// for( e = mesh.eHead.next; e != mesh.eHead; e = eNext ) { + eNext = e.next; + if( e.getRface().inside != e.Lface.inside ) { + + /* This is a boundary edge (one side is interior, one is exterior). */ + e.winding = (e.Lface.inside) ? value : -value; + } else { + + /* Both regions are interior, or both are exterior. */ + if( ! keepOnlyBoundary ) { + e.winding = 0; + } else { + mesh.delete( e ); + } + } + e = eNext; + } + } + + public function getNeighbourFace_(edge : TESShalfEdge) : Int { + if (edge.getRface() == null) + return -1; + if (!edge.getRface().inside) + return -1; + return edge.getRface().n; + } + + public function outputPolymesh_( mesh : TESSmesh, elementType : Int, polySize : Int, vertexSize : Int ) { + var v : TESSvertex; + var f : TESSface; + var edge : TESShalfEdge; + var maxFaceCount = 0; + var maxVertexCount = 0; + var faceVerts, i; + var elements = 0; + var vert; + + //Assume that the input data is triangles now. + //Try to merge as many polygons as possible + if (polySize > 3) + { + mesh.mergeConvexFaces( polySize ); + } + + //Mark unused +// for ( v = mesh.vHead.next; v != mesh.vHead; v = v.next ) +// v.n = -1; + v = mesh.vHead.next; + while( v != mesh.vHead ){ + v.n = -1; + v = v.next; + } + + //Create unique IDs for all vertices and faces. + f = mesh.fHead.next; + while( f != mesh.fHead ){ +// for ( f = mesh.fHead.next; f != mesh.fHead; f = f.next ) + f.n = -1; + if( !f.inside ) { + f = f.next; + continue; + } + + edge = f.anEdge; + faceVerts = 0; + do + { + v = edge.Org; + if ( v.n == -1 ) + { + v.n = maxVertexCount; + maxVertexCount++; + } + faceVerts++; + edge = edge.Lnext; + } + while (edge != f.anEdge); + + Tess2.assert( faceVerts <= polySize ); + + f.n = maxFaceCount; + ++maxFaceCount; + f = f.next; + } + + this.elementCount = maxFaceCount; + if (elementType == Tess2.CONNECTED_POLYGONS) + maxFaceCount *= 2; +/* tess.elements = (TESSindex*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSindex) * maxFaceCount * polySize ); + if (!tess->elements) + { + tess->outOfMemory = 1; + return; + }*/ + this.elements = []; + this.elements.alloc( maxFaceCount * polySize ); + + this.vertexCount = maxVertexCount; +/* tess->vertices = (TESSreal*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSreal) * tess->vertexCount * vertexSize ); + if (!tess->vertices) + { + tess->outOfMemory = 1; + return; + }*/ + this.vertices = []; + this.vertices.alloc( maxVertexCount * vertexSize ); + +/* tess->vertexIndices = (TESSindex*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSindex) * tess->vertexCount ); + if (!tess->vertexIndices) + { + tess->outOfMemory = 1; + return; + }*/ + this.vertexIndices = []; + this.vertexIndices.alloc( maxVertexCount ); + + + //Output vertices. + v = mesh.vHead.next; + while( v != mesh.vHead ){ +// for ( v = mesh.vHead.next; v != mesh.vHead; v = v.next ) + if ( v.n != -1 ) + { + //Store coordinate + var idx = v.n * vertexSize; + this.vertices[idx+0] = v.coords[0]; + this.vertices[idx+1] = v.coords[1]; + if ( vertexSize > 2 ) + this.vertices[idx+2] = v.coords[2]; + //Store vertex index. + this.vertexIndices[v.n] = v.idx; + } + v = v.next; + } + + //Output indices. + var nel = 0; + f = mesh.fHead.next; + while( f != mesh.fHead ){ +// for ( f = mesh.fHead.next; f != mesh.fHead; f = f.next ) + if ( !f.inside ) { + f = f.next; + continue; + } + + //Store polygon + edge = f.anEdge; + faceVerts = 0; + do + { + v = edge.Org; + this.elements[nel++] = v.n; + faceVerts++; + edge = edge.Lnext; + } + while (edge != f.anEdge); + //Fill unused. + for (i in faceVerts...polySize){ +// for (i = faceVerts; i < polySize; ++i){ + this.elements[nel++] = -1; + } + + //Store polygon connectivity + if ( elementType == Tess2.CONNECTED_POLYGONS ) + { + edge = f.anEdge; + do + { + this.elements[nel++] = this.getNeighbourFace_( edge ); + edge = edge.Lnext; + } + while (edge != f.anEdge); + //Fill unused. + for (i in faceVerts...polySize){ +// for (i = faceVerts; i < polySize; ++i) + this.elements[nel++] = -1; + } + } + f = f.next; + } + } + +// void OutputContours( TESStesselator *tess, TESSmesh *mesh, int vertexSize ) + public function outputContours_( mesh : TESSmesh, vertexSize : Int ) { + var f; + var edge; + var start; + var verts; + var elements; + var vertInds; + var startVert = 0; + var vertCount = 0; + + this.vertexCount = 0; + this.elementCount = 0; + + f = mesh.fHead.next; + while ( f != mesh.fHead ){ +// for ( f = mesh.fHead.next; f != mesh.fHead; f = f.next ) + if ( !f.inside ) { + f = f.next; + continue; + } + + start = edge = f.anEdge; + do + { + this.vertexCount++; + edge = edge.Lnext; + } + while ( edge != start ); + + this.elementCount++; + f = f.next; + } + +/* tess->elements = (TESSindex*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSindex) * tess->elementCount * 2 ); + if (!tess->elements) + { + tess->outOfMemory = 1; + return; + }*/ + this.elements = []; + this.elements.alloc( this.elementCount * 2 ); + +/* tess->vertices = (TESSreal*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSreal) * tess->vertexCount * vertexSize ); + if (!tess->vertices) + { + tess->outOfMemory = 1; + return; + }*/ + this.vertices = []; + this.vertices.alloc( this.vertexCount * vertexSize ); + +/* tess->vertexIndices = (TESSindex*)tess->alloc.memalloc( tess->alloc.userData, + sizeof(TESSindex) * tess->vertexCount ); + if (!tess->vertexIndices) + { + tess->outOfMemory = 1; + return; + }*/ + this.vertexIndices = []; + this.vertexIndices.alloc( this.vertexCount ); + + var nv = 0; + var nvi = 0; + var nel = 0; + startVert = 0; + + f = mesh.fHead.next; + while ( f != mesh.fHead ){ +// for ( f = mesh.fHead.next; f != mesh.fHead; f = f.next ) + if ( !f.inside ) { + f = f.next; + continue; + } + + vertCount = 0; + start = edge = f.anEdge; + do + { + this.vertices[nv++] = edge.Org.coords[0]; + this.vertices[nv++] = edge.Org.coords[1]; + if ( vertexSize > 2 ) + this.vertices[nv++] = edge.Org.coords[2]; + this.vertexIndices[nvi++] = edge.Org.idx; + vertCount++; + edge = edge.Lnext; + } + while ( edge != start ); + + this.elements[nel++] = startVert; + this.elements[nel++] = vertCount; + + startVert += vertCount; + f = f.next; + } + } + + public function addContour( size : Int, vertices : Array ) + { + var e; + var i; + + if ( this.mesh == null ) + this.mesh = new TESSmesh(); +/* if ( tess->mesh == NULL ) { + tess->outOfMemory = 1; + return; + }*/ + + if ( size < 2 ) + size = 2; + if ( size > 3 ) + size = 3; + + e = null; + + i = 0; + while (i < vertices.length){ +// for( i = 0; i < vertices.length; i += size ) + if( e == null ) { + /* Make a self-loop (one vertex, one edge). */ + e = this.mesh.makeEdge(); +/* if ( e == NULL ) { + tess->outOfMemory = 1; + return; + }*/ + this.mesh.splice( e, e.Sym ); + } else { + /* Create a new vertex and edge which immediately follow e + * in the ordering around the left face. + */ + this.mesh.splitEdge( e ); + e = e.Lnext; + } + + /* The new vertex is now e->Org. */ + e.Org.coords[0] = vertices[i+0]; + e.Org.coords[1] = vertices[i+1]; + if ( size > 2 ) + e.Org.coords[2] = vertices[i+2]; + else + e.Org.coords[2] = 0.0; + /* Store the insertion number so that the vertex can be later recognized. */ + e.Org.idx = this.vertexIndexCounter++; + + /* The winding of an edge says how the winding number changes as we + * cross from the edge''s right face to its left face. We add the + * vertices in such an order that a CCW contour will add +1 to + * the winding number of the region inside the contour. + */ + e.winding = 1; + e.Sym.winding = -1; + i += size; + } + } + +// int tessTesselate( TESStesselator *tess, int windingRule, int elementType, int polySize, int vertexSize, const TESSreal* normal ) + public function tessellate( windingRule : Int, elementType : Int, polySize : Int, vertexSize : Int, normal : Array ) { + this.vertices = []; + this.elements = []; + this.vertexIndices = []; + + this.vertexIndexCounter = 0; + + if (normal != null) + { + this.normal[0] = normal[0]; + this.normal[1] = normal[1]; + this.normal[2] = normal[2]; + } + + this.windingRule = windingRule; + + if (vertexSize < 2) + vertexSize = 2; + if (vertexSize > 3) + vertexSize = 3; + +/* if (setjmp(tess->env) != 0) { + //come back here if out of memory + return 0; + }*/ + + if (this.mesh == null) + { + return false; + } + + /* Determine the polygon normal and project vertices onto the plane + * of the polygon. + */ + this.projectPolygon_(); + + /* tessComputeInterior( tess ) computes the planar arrangement specified + * by the given contours, and further subdivides this arrangement + * into regions. Each region is marked "inside" if it belongs + * to the polygon, according to the rule given by tess->windingRule. + * Each interior region is guaranteed be monotone. + */ + Sweep.computeInterior( this ); + + var mesh = this.mesh; + + /* If the user wants only the boundary contours, we throw away all edges + * except those which separate the interior from the exterior. + * Otherwise we tessellate all the regions marked "inside". + */ + if (elementType == Tess2.BOUNDARY_CONTOURS) { + this.setWindingNumber_( mesh, 1, true ); + } else { + this.tessellateInterior_( mesh ); + } +// if (rc == 0) longjmp(tess->env,1); /* could've used a label */ + + mesh.check(); + + if (elementType == Tess2.BOUNDARY_CONTOURS) { + this.outputContours_( mesh, vertexSize ); /* output contours */ + } + else + { + this.outputPolymesh_( mesh, elementType, polySize, vertexSize ); /* output polygons */ + } + +// tess.mesh = null; + + return true; + } +} diff --git a/src/verb/topo/Topo.hx b/src/verb/topo/Topo.hx new file mode 100644 index 00000000..113cdf99 --- /dev/null +++ b/src/verb/topo/Topo.hx @@ -0,0 +1,5 @@ +package verb.topo; +class Topo { + private static var counter = 0; + public var id : Int = counter++; +} diff --git a/src/verb/topo/Vertex.hx b/src/verb/topo/Vertex.hx new file mode 100644 index 00000000..ffa70377 --- /dev/null +++ b/src/verb/topo/Vertex.hx @@ -0,0 +1,47 @@ +package verb.topo; + +import verb.core.Ray; +import haxe.ds.IntMap; +import verb.core.IDoublyLinkedList; +import verb.core.NurbsCurveData.Point; + +@:expose("topo.Vertex") +class Vertex implements IDoublyLinkedList extends Topo { + public var pt : Point; + public var e : HalfEdge; //a vertex may be "owned" by many HalfEdge's - this is one of them + public var prv : Vertex; + public var nxt : Vertex; + + public function new(point) { + this.pt = point; + } + + //TODO: test + public function neighbors() : Array { + var memo = new IntMap(); + memo.set(id, this); //do not include self ref's + + var a = []; + var ce = e; + for (e in halfEdges()){ + var v = e.nxt.v; + if (memo.exists(v.id)) continue; + a.push( v ); + } + + return a; + } + + //TODO: test + public function halfEdges() : Array { + var a = []; + var ce = e; + do { + a.push(ce); + if (ce.opp == null) break; //the solid base case + ce = ce.opp.nxt; + } while (ce != e); + + return a; + } +} diff --git a/test/testTopo.js b/test/testTopo.js new file mode 100755 index 00000000..95b3723a --- /dev/null +++ b/test/testTopo.js @@ -0,0 +1,828 @@ +var should = require('should') + , verb = require('../build/js/verb.js'); + +// necessary for multi-threading +verb.exe.WorkerPool.basePath = process.cwd() + "/build/js/"; + +// some testing utilities +function vecShouldBe( expected, test, tol ){ + + if (tol === undefined) tol = verb.core.Constants.TOLERANCE; + + test.length.should.be.equal( expected.length ); + + for (var i = 0; i < test.length; i++){ + test[i].should.be.approximately( expected[i], tol ); + } +} + +function last(a){ + return a[a.length-1]; +} + +describe("verb.topo.Solid.mvfs",function(){ + it('provides correctly linked lists, correct number of elements', function(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + s.should.not.be.null; + + var e0 = s.f.l.e; + e0.nxt.should.be.equal(e0); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + l.length.should.be.equal(1); + v.length.should.be.equal(1); + f.length.should.be.equal(1); + he.length.should.be.equal(1); + + he[0].v.pt.should.eql([0,0,0]); + he[0].nxt.should.eql(he[0]); + he[0].prv.should.eql(he[0]); + + f[0].nxt.should.eql(f[0]); + f[0].prv.should.eql(f[0]); + + v[0].nxt.should.eql(v[0]); + v[0].prv.should.eql(v[0]); + + l[0].nxt.should.eql(l[0]); + l[0].prv.should.eql(l[0]); + }); +}); + +var Tess2 = require('tess2'); + +describe("verb.topo.Tess2",function(){ + it('has the same results as JS library for simple example', function(){ + + // has library + var ca = [0,0, 10,0, 5,10]; + var cb = [0,2, 10,2, 10,6, 0,6]; + var contours = [ca,cb]; + + var res0 = Tess2.tesselate({ + contours: contours, + windingRule: Tess2.WINDING_ODD, + elementType: Tess2.POLYGONS, + polySize: 3, + vertexSize: 2 + }); + + // haxe port + var opts2 = new verb.topo.Tess2Options(); + + opts2.contours = contours; + opts2.windingRule = verb.topo.Tess2.WINDING_ODD; + opts2.elementType = verb.topo.Tess2.POLYGONS; + opts2.polySize = 3; + opts2.vertexSize = 2; + + var res1 = verb.topo.Tess2.tessellate(opts2); + + res1.vertices.should.eql(res0.vertices); + res1.vertexIndices.should.eql(res0.vertexIndices); + res1.vertexCount.should.eql(res0.vertexCount); + res1.elements.should.eql(res0.elements); + res1.elementCount.should.eql(res0.elementCount); + }); + +}); + +describe("verb.core.Intersect.segmentAndPlane",function(){ + it('works for simple cases', function(){ + verb.core.Intersect.segmentAndPlane( [0,0,0], [0,0,1], [0,0,0.5], [0,0,1] ).p.should.be.approximately( 0.5, verb.core.Constants.EPSILON ); + verb.core.Intersect.segmentAndPlane( [0,0,0], [0,0,1], [0,0,0.1], [0,0,1] ).p.should.be.approximately( 0.1, verb.core.Constants.EPSILON ); + verb.core.Intersect.segmentAndPlane( [0,0,0], [0,0,1], [0,0,0.9], [0,0,1] ).p.should.be.approximately( 0.9, verb.core.Constants.EPSILON ); + verb.core.Intersect.segmentAndPlane( [0,0,0], [0,1,0], [0,0.5,0], [0,1,0] ).p.should.be.approximately( 0.5, verb.core.Constants.EPSILON ); + verb.core.Intersect.segmentAndPlane( [0,0,0], [0,1,0], [0,0.1,0], [0,1,0] ).p.should.be.approximately( 0.1, verb.core.Constants.EPSILON ); + }); +}); + +function triangularLamina(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + var e0 = s.f.l.e; + var v0 = s.v; + var f0 = s.f; + + var nv0 = s.lmev( e0, e0, [10,0,0] ); + nv1 = s.lmev( nv0.e, nv0.e, [10,10,0] ); + + var nf = s.lmef( v0.e, v0.e.nxt.nxt ); + + return s; +} + +function triangularPrism(){ + var s = triangularLamina(); + + var nvs = s.f.l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,10]) ); + }); + + var nfs = nvs + .map(function(v){ + return v.e; + }) + .map(function(e){ + var nf = s.lmef(e, e.nxt.nxt.nxt); + return nf; + }); + + return s; +} + + +describe("verb.topo.Solid.lmef",function(){ + it('can close a single triangular loop into a lamina', function(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + var e0 = s.f.l.e; + var v0 = s.v; + var f0 = s.f; + + var nv0 = s.lmev( e0, e0, [1,0,0] ); + nv1 = s.lmev( nv0.e, nv0.e, [1,1,0] ); + + var nf = s.lmef( v0.e, v0.e.nxt.nxt ); + + e0.should.equal( e0.nxt.nxt.nxt ); + e0.opp.should.not.equal( e0 ); + e0.opp.should.equal( e0.opp.nxt.nxt.nxt ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + f[0].loops()[0].halfEdges().length.should.be.equal( 3 ); + f[1].loops()[0].halfEdges().length.should.be.equal( 3 ); + + l.length.should.be.equal(2); + v.length.should.be.equal(3); + f.length.should.be.equal(2); + he.length.should.be.equal(6); + + v.forEach(function(x){ + x.neighbors().length.should.be.equal(2); + }); + + l.forEach(function(x){ + x.vertices().length.should.be.equal(3); + }); + + l.forEach(function(x){ + x.f.should.not.be.null; + }); + + }); + + it('can close a single rectangular loop into a lamina', function(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + var e0 = s.f.l.e; + var v0 = s.v; + var f0 = s.f; + + var nv0 = s.lmev( e0, e0, [1,0,0] ); + nv1 = s.lmev( nv0.e, nv0.e, [1,1,0] ); + s.lmev( nv1.e, nv1.e, [0,1,0] ); + + var nf = s.lmef( v0.e, v0.e.nxt.nxt.nxt ); + + e0.should.equal( e0.nxt.nxt.nxt.nxt ); + e0.opp.should.not.equal( e0 ); + e0.opp.l.should.not.equal( e0.l ); + e0.opp.should.equal( e0.opp.nxt.nxt.nxt.nxt ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + f[0].loops()[0].halfEdges().length.should.be.equal( 4 ); + f[1].loops()[0].halfEdges().length.should.be.equal( 4 ); + + l.length.should.be.equal(2); + v.length.should.be.equal(4); + f.length.should.be.equal(2); + he.length.should.be.equal(8); + }); + + it('can be used to create a single new face, extending from a triangular lamina', function(){ + var s = triangularLamina(); + + var l = s.f.l; + + var nvs = l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,1]) ); + }); + + var he0 = l.halfEdges()[1]; + var he1 = he0.nxt.nxt.nxt; + + var nf = s.lmef(he0, he1); + + nf.l.halfEdges().length.should.equal( 4 ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + l.length.should.be.equal(3); + v.length.should.be.equal(6); + f.length.should.be.equal(3); + he.length.should.be.equal(14); + + // each vertex has >=1 neighbors + v.forEach(function(x){ + x.neighbors().length.should.be.greaterThan(0); + }); + + // each loop has >=3 vertices + l.forEach(function(x){ + x.vertices().length.should.be.greaterThan(2); + }); + + // each loop is non-null + f.forEach(function(x){ + x.l.should.not.be.null; + }); + }); + + it('can be used to close a lamina with extended edges into a prism', function(){ + var s = triangularLamina(); + + var nvs = s.f.l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,1]) ); + }); + + var nfs = nvs + .map(function(v){ + // get the halfEdges + return v.e; + }) + .map(function(e){ + // form the new side faces + var nf = s.lmef(e, e.nxt.nxt.nxt); + nf.l.halfEdges().length.should.equal( 4 ); + return nf; + }); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + v.length.should.be.equal(6); + f.length.should.be.equal(5); + l.length.should.be.equal(5); + he.length.should.be.equal(18); + + // each vertex has 3 neighbors + v.forEach(function(x){ + x.neighbors().length.should.be.equal(3); + }); + + // each loop has >=3 vertices + l.forEach(function(x){ + x.vertices().length.should.be.greaterThan(2); + }); + + // each loop is non-null + f.forEach(function(x){ + x.l.should.not.be.null; + }); + }); + + it('can split a quad face of a solid into two triangles', function(){ + + var s = triangularPrism(); + + var tfl = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,-1,0] ) > 0; + }); + + tfl.length.should.be.equal( 1 ); + + var of = tfl[0]; + var nf = s.lmef( of.ol.e, of.ol.e.nxt.nxt ); + + of.ol.vertices().length.should.be.equal(3); + nf.ol.vertices().length.should.be.equal(3); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + l.length.should.be.equal(6); + v.length.should.be.equal(6); + f.length.should.be.equal(6); + he.length.should.be.equal(20); + + }); +}); + +describe("verb.topo.Solid.lkemr",function(){ + it('can be used to insert an empty ring into a face', function(){ + var s = triangularPrism(); + + // get the top face + var tfl = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,1] ) > 0; + }); + + tfl.length.should.be.equal( 1 ); + + var tf = tfl[0]; + + // insert new vertex and two new half-edges into face + var nv = s.lmev( tf.l.e, tf.l.e, [0.1,0.1,1] ); + + // now the face has 5 edges + nv.e.l.halfEdges().length.should.be.equal(5); + + // remove the edges connecting this vertex to the outer face, making this into a new ring (i.e. inner loop) + var nl = s.lkemr(nv.e.prv); + + nl.halfEdges().length.should.be.equal(1); + nl.f.loops().length.should.be.equal(2); + nl.f.neighbors().length.should.be.equal(3); + + nv.e.l.halfEdges().length.should.be.equal(1); + nv.neighbors().length.should.be.equal(0); + + var fl = nl.f.loops(); + var ol = fl[0] != nl ? fl[0] : fl[1]; + + ol.halfEdges().length.should.be.equal(3); + nl.f.rings().length.should.be.equal(1); + }); +}); + +describe("verb.topo.Solid.lkev",function(){ + it('can be used to reduce a single edge back to the base', function(){ + // 2 vertex, 2 half edge topo + var s = verb.topo.Solid.mvfs([0,0,0]); + var e = s.f.l.e; + var v = s.lmev( e, e, [0,0,1] ); + + s.lkev( e ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + v.length.should.be.equal(1); + f.length.should.be.equal(1); + l.length.should.be.equal(1); + he.length.should.be.equal(1); + + v[0].neighbors().length.should.be.equal(0); + f[0].neighbors().length.should.be.equal(0); + l[0].halfEdges().length.should.be.equal(1); + he[0].nxt.should.be.equal(he[0]); // loop + + s.edges().length.should.be.equal(0); + }); +}); + +describe("verb.topo.Solid.lkef",function(){ + it('can rejoin a face on a quad face of a solid', function(){ + + var s = triangularPrism(); + + var tf = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,-1,0] ) > 0; + })[0]; + + var oe = tf.ol.e; + var f = s.lmef( oe, oe.nxt.nxt ); + + s.faces().length.should.be.equal( 6 ); + + s.lkef( oe.prv ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + v.length.should.be.equal(6); + f.length.should.be.equal(5); + l.length.should.be.equal(5); + he.length.should.be.equal(18); + + // each vertex has 3 neighbors + v.forEach(function(x){ + x.neighbors().length.should.be.equal(3); + }); + + // each loop has >=3 vertices + l.forEach(function(x){ + x.vertices().length.should.be.greaterThan(2); + }); + + // each loop is non-null + f.forEach(function(x){ + x.l.should.not.be.null; + }); + + }); +}); + +describe("verb.topo.Solid.lmekr",function(){ + it('can be used to undo a single lkemr to a lone vertex', function(){ + var s = triangularPrism(); + + var tfl = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,1] ) > 0; + }); + + var tf = tfl[0]; + + var e0 = tf.l.e.prv; + + var nv = s.lmev( tf.l.e, tf.l.e, [0.1,0.1,1] ); + var nl = s.lkemr(nv.e.prv); // now there's a hanging vertex in the face + + var ohe = s.halfEdges(); + var oe = s.edges(); + var ol = s.loops(); + + var ne = s.lmekr( e0.nxt, nl.e ); + + s.edges().length.should.be.equal( oe.length + 1 ); + + // the solid should have one more edge + s.halfEdges().length.should.be.equal( ohe.length + 1 ); + + // one less loop + s.loops().length.should.be.equal( ol.length - 1 ); + + // the face should have a single loop + ne.l.halfEdges().length.should.equal( 5 ); + }); +}); + +describe("verb.topo.Solid.lkfmrh",function(){ + it('can be used to form a new hole', function(){ + + var s = triangularPrism(); + + var tfl = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,1] ) > 0; + }); + + var bf = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,-1] ) > 0; + })[0]; + + var tf = tfl[0]; + + var e0 = tf.l.e.prv; + + var nv = s.lmev( tf.l.e, tf.l.e, [2,1,0] ); + var nl = s.lkemr(nv.e.prv); + + var v0 = nl.e.v; + + // make new face inside of this one + var nv1 = s.lmev( nl.e, nl.e, [9,1,0] ); + var nv2 = s.lmev( nv1.e, nv1.e, [9,8,0] ); + var nf = s.lmef( v0.e, v0.e.nxt.nxt ); + + // on each vertex of the outer loop of the new face, do an mev + var nvs = nf.l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,10]) ); // this extends up to the top face + }); + + // now iterate around, introducing new faces on every side, now we're extruding the interior face + var nfs = nvs + .map(function(v){ + return v.e; + }) + .map(function(e){ + var nf = s.lmef(e, e.nxt.nxt.nxt); + nf.l.halfEdges().length.should.equal( 4 ); + return nf; + }); + + // before apply lkfmrh + var ohe = s.halfEdges(); + var oe = s.edges(); + var ol = s.loops(); + var of = s.faces(); + + // insert nf into the top face as a ring + s.lkfmrh( nf, bf ); + + s.edges().length.should.be.equal( oe.length ); + s.halfEdges().length.should.be.equal( ohe.length ); + s.loops().length.should.be.equal( ol.length ); + + // the face should now have an inner ring + bf.loops().length.should.equal( 2 ); + + // one less face! + s.faces().length.should.equal( of.length - 1 ); + }); +}); + + +function hollowPrism(){ + + var s = triangularPrism(); + + var tfl = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,1] ) > 0; + }); + + var bf = s.faces().filter(function(x){ + return verb.core.Vec.dot( x.normal(), [0,0,-1] ) > 0; + })[0]; + + var tf = tfl[0]; + var e0 = tf.l.e.prv; + + var nv = s.lmev( tf.l.e, tf.l.e, [2,1,10] ); + var nl = s.lkemr(nv.e.prv); + + var v0 = nl.e.v; + + // make new face inside of this one + var nv1 = s.lmev( nl.e, nl.e, [9,1,10] ); + var nv2 = s.lmev( nv1.e, nv1.e, [9,8,10] ); + var nf = s.lmef( v0.e, v0.e.nxt.nxt ); + + // on each vertex of the outer loop of the new face, do an mev + var nvs = nf.l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,-10]) ); // this extends up to the top face + }); + + // now iterate around, introducing new faces on every side, now we're extruding the interior face + var nfs = nvs + .map(function(v){ + return v.e; + }) + .map(function(e){ + return s.lmef(e, e.nxt.nxt.nxt); + }); + + s.lkfmrh( nf, bf ); + + return s; +} + +describe("verb.topo.Analyze.volume",function(){ + it('computes correct volume for triangular prism', function(){ + var s = triangularPrism(); + verb.topo.Analyze.volume( s ).should.equal( 500 ); + }); + + it('computes correct volume for hollow prism', function(){ + var s = hollowPrism(); + verb.topo.Analyze.volume( s ).should.equal( 500 - 245 ); + }); +}); + +describe("verb.topo.Analyze.area",function(){ + it('computes correct area for triangular prism', function(){ + var s = triangularPrism(); + verb.topo.Analyze.area( s ).should.equal( 200 + 100 + 10 * 10 * Math.sqrt(2) ); + }); + + it('computes correct volume for hollow prism', function(){ + var s = hollowPrism(); + verb.topo.Analyze.area( s ).should.equal( 200 + (100 - 49) + 10 * 10 * Math.sqrt(2) + 10*7*Math.sqrt(2) + 2*7*10 ); + }); +}); + + + +describe("verb.topo.Solid.lmev",function(){ + it('adds 2 new HalfEdges, one new vertex on first call', function(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + var e0 = s.f.l.e; + var v0 = s.f.l.e.v; + + var nv = s.lmev( e0, e0, [1,0,0] ); + nv.pt.should.eql([1,0,0]); + + e0.v.should.be.equal( nv ); + e0.nxt.v.should.be.equal( v0 ); + e0.nxt.nxt.v.should.be.equal( e0.v ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + l.length.should.be.equal(1); + v.length.should.be.equal(2); + f.length.should.be.equal(1); + he.length.should.be.equal(2); + }); + + it('adds 2 new HalfEdges, one new vertex on second call', function(){ + var s = verb.topo.Solid.mvfs( [0,0,0] ); + + var e0 = s.f.l.e; + var v0 = s.f.l.e.v; + + var nv0 = s.lmev( e0, e0, [1,0,0] ); + nv0.pt.should.eql([1,0,0]); + + nv1 = s.lmev( nv0.e, nv0.e, [1,1,0] ); + nv1.pt.should.eql( [1,1,0] ); + + e0.v.should.be.equal( nv0 ); + e0.nxt.v.should.be.equal( v0 ); + e0.nxt.nxt.v.should.be.equal( nv0 ); + e0.nxt.nxt.nxt.v.should.be.equal( nv1 ); + + var l = s.loops(); + var v = s.vertices(); + var f = s.faces(); + var he = s.halfEdges(); + + l.length.should.be.equal(1); + v.length.should.be.equal(3); + f.length.should.be.equal(1); + he.length.should.be.equal(4); + + }); + + it('can extend edges from a triangular lamina', function(){ + var s = triangularLamina(); + + var vs = s.vertices(); + + // each vertex has 2 neighbors originally + vs.forEach(function(x){ + x.neighbors().length.should.be.equal(2); + }); + + var l = s.f.l; + + // extend every vertex in the loop with an lmev + var nvs = l.halfEdges().map(function(e){ + return s.lmev( e, e, verb.core.Vec.add( e.v.pt, [0,0,1]) ); + }); + + l.halfEdges().length.should.be.equal(9); + s.f.nxt.l.halfEdges().length.should.be.equal(3); + + // each of the original vertices has 3 neighbors afterwards + vs.forEach(function(x){ + x.neighbors().length.should.be.equal(3); + }); + }); + +}); + + +describe("verb.topo.Split.solidByPlane",function(){ + function cube(){ + var pts = [[0,0,0], [10,0,0], [10,10,0], [0,10,0] ]; + return verb.topo.Make.extrusion( pts, [0,0,10] ); + } + + it('can split a cube in half', function(){ + var p = { n : [0,0,1], o : [0,0,5] }; + var res = verb.topo.Split.solidByPlane( cube(), p ); + + verb.topo.Analyze.volume(res.item0).should.be.approximately( 500, verb.core.Constants.EPSILON ); + verb.topo.Analyze.volume(res.item1).should.be.approximately( 500, verb.core.Constants.EPSILON ); + }); + + it('can split a cube in quarters', function(){ + var p = { n : [0,0,1], o : [0,0,2.5] }; + var res = verb.topo.Split.solidByPlane( cube(), p ); + + verb.topo.Analyze.volume(res.item0).should.be.approximately( 750, verb.core.Constants.EPSILON ); + verb.topo.Analyze.volume(res.item1).should.be.approximately( 250, verb.core.Constants.EPSILON ); + }); + + it('does not split when plane is coplanar with bottom face of cube', function(){ + var res = verb.topo.Split.solidByPlane( cube(), { n : [0,0,1], o : [0,0,0] } ); + should.equal(null, res); + }); + + it('does not split when plane is coplanar with top face of cube', function(){ + var res = verb.topo.Split.solidByPlane( cube(), { n : [0,0,1], o : [0,0,10] } ); + should.equal(null, res); + }); + + it('does not split when plane is coplanar with side face of cube', function(){ + var res = verb.topo.Split.solidByPlane( cube(), { n : [1,0,0], o : [10,0,0] } ); + should.equal(null, res); + }); + + it('does not split when plane is non-intersecting', function(){ + var res = verb.topo.Split.solidByPlane( cube(), { n : [1,0,0], o : [20,0,0] } ); + should.equal(null, res); + }); + + it('can split cube through the center', function(){ + var n = [ 1 / Math.sqrt(3), 1 / Math.sqrt(3), 1 / Math.sqrt(3) ]; + var res = verb.topo.Split.solidByPlane( cube(), { n : n, o : [5,5,5] } ); + + verb.topo.Analyze.volume(res.item0).should.be.approximately( 500, verb.core.Constants.EPSILON ); + verb.topo.Analyze.volume(res.item1).should.be.approximately( 500, verb.core.Constants.EPSILON ); + }); + + it('can split an l-shaped solid', function(){ + var pts = [[0,0,0], [10,0,0], [10,10,0] , [20,10,0], [20,20,0], [0,20,0] ]; + var s = verb.topo.Make.extrusion( pts, [0,0,10] ); + var v = verb.topo.Analyze.volume( s ); + + var p = { n : [0,0,1], o : [0,0,5] }; + var res = verb.topo.Split.solidByPlane( s, p ); + + verb.topo.Analyze.volume(res.item0).should.be.approximately( v/2, verb.core.Constants.EPSILON ); + verb.topo.Analyze.volume(res.item1).should.be.approximately( v/2, verb.core.Constants.EPSILON ); + }); +}); + +describe("verb.topo.Boolean.isPointInPolygon",function(){ + it('works for a few basic cases', function(){ + var tri = [ [0,0,0], [1,0,0], [1,1,0] ]; + var rect = [ [0,0,0], [1,0,0], [1,1,0], [0,1,0] ]; + var rectin = [ [0,0,0], [0.5,0.2,0], [1,0,0], [1,1,0], [0,1,0], [0.5,0.5,0] ]; + var l = [ [0,0,0], [1,0,0], [1,1,0], [2,1,0], [2,2,0], [0,2,0] ]; + + verb.topo.Boolean.isPointInPolygon( [0.5,0.5,0], tri, [0,0,1] ).should.be.equal( true ); + verb.topo.Boolean.isPointInPolygon( [0.5,0.5,0], rect, [0,0,1] ).should.be.equal( true ); + verb.topo.Boolean.isPointInPolygon( [0.1,0.3,0], rect, [0,0,1] ).should.be.equal( true ); + verb.topo.Boolean.isPointInPolygon( [0.9,0.1,0], rect, [0,0,1] ).should.be.equal( true ); + + verb.topo.Boolean.isPointInPolygon( [2,0.5,0], rect, [0,0,1] ).should.be.equal( false ); + verb.topo.Boolean.isPointInPolygon( [-2,0.5,0], rect, [0,0,1] ).should.be.equal( false ); + verb.topo.Boolean.isPointInPolygon( [2,2,0], rect, [0,0,1] ).should.be.equal( false ); + verb.topo.Boolean.isPointInPolygon( [-2,-2,0], rect, [0,0,1] ).should.be.equal( false ); + + verb.topo.Boolean.isPointInPolygon( [0,0.5,0], rectin, [0,0,1] ).should.be.equal( false ); + verb.topo.Boolean.isPointInPolygon( [0.5,0.9,0], rectin, [0,0,1] ).should.be.equal( true ); + + verb.topo.Boolean.isPointInPolygon( [2,0,0], l, [0,0,1] ).should.be.equal( false ); + verb.topo.Boolean.isPointInPolygon( [0.5,0.9,0], l, [0,0,1] ).should.be.equal( true ); + }); +}); + +function cube(){ + var ptsa = [[0,0,0], [10,0,0], [10,10,0], [0,10,0] ]; + return verb.topo.Make.extrusion( ptsa, [0,0,10] ); +} + +describe("verb.topo.Boolean.insertNullEdgeIntoFace",function(){ + it('testing', function(){ + + var c = cube(); + + var f = c.faces()[0]; + + var centroid = verb.core.Vec.div( verb.core.Vec.addAll( f.l.points() ), f.l.points().length ); + + // insertNullEdgeIntoFace( point : Point, f : Face, nes : Array ) + + var nes = []; + verb.topo.Boolean.insertNullEdgeIntoFace( centroid, f, nes ); + + f.ol.halfEdges().forEach(function(e){ + should.notEqual(null, e.nxt); + should.notEqual(null, e.prv); + }); + + f.l.halfEdges().forEach(function(e){ + should.notEqual(null, e.nxt); + should.notEqual(null, e.prv); + }); + + + }); +}); + + +describe("verb.topo.Boolean.union",function(){ + it('testing', function(){ +// var a = cube(); +// +// var ptsb = [[5,5,-5], [15,5,-5], [15,15,-5], [5,15,-5] ]; +// var b = verb.topo.Make.extrusion( ptsb, [0,0,10] ); +// +// var res = verb.topo.Boolean.union( a, b, 1e-6 ); +// +// // TODO test that classifyVertexFace returns 3 inside, outside pairs +// + }); +}); +