├── Stats.js ├── jquery.mousewheel.js ├── index.htm ├── ObjectControls.js ├── webGerber.js └── jquery-1.8.2.min.js /Stats.js: -------------------------------------------------------------------------------- 1 | // stats.js r10 - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=1E3,o=0,h=0,p=1E3,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /jquery.mousewheel.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 5 | * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 6 | * Thanks to: Seamus Leahy for adding deltaX and deltaY 7 | * 8 | * Version: 3.0.6 9 | * 10 | * Requires: 1.2.2+ 11 | */ 12 | 13 | (function($) { 14 | 15 | var types = ['DOMMouseScroll', 'mousewheel']; 16 | 17 | if ($.event.fixHooks) { 18 | for ( var i=types.length; i; ) { 19 | $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; 20 | } 21 | } 22 | 23 | $.event.special.mousewheel = { 24 | setup: function() { 25 | if ( this.addEventListener ) { 26 | for ( var i=types.length; i; ) { 27 | this.addEventListener( types[--i], handler, false ); 28 | } 29 | } else { 30 | this.onmousewheel = handler; 31 | } 32 | }, 33 | 34 | teardown: function() { 35 | if ( this.removeEventListener ) { 36 | for ( var i=types.length; i; ) { 37 | this.removeEventListener( types[--i], handler, false ); 38 | } 39 | } else { 40 | this.onmousewheel = null; 41 | } 42 | } 43 | }; 44 | 45 | $.fn.extend({ 46 | mousewheel: function(fn) { 47 | return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); 48 | }, 49 | 50 | unmousewheel: function(fn) { 51 | return this.unbind("mousewheel", fn); 52 | } 53 | }); 54 | 55 | 56 | function handler(event) { 57 | var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; 58 | event = $.event.fix(orgEvent); 59 | event.type = "mousewheel"; 60 | 61 | // Old school scrollwheel delta 62 | if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } 63 | if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } 64 | 65 | // New school multidimensional scroll (touchpads) deltas 66 | deltaY = delta; 67 | 68 | // Gecko 69 | if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 70 | deltaY = 0; 71 | deltaX = -1*delta; 72 | } 73 | 74 | // Webkit 75 | if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } 76 | if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } 77 | 78 | // Add event and delta to the front of the arguments 79 | args.unshift(event, delta, deltaX, deltaY); 80 | 81 | return ($.event.dispatch || $.event.handle).apply(this, args); 82 | } 83 | 84 | })(jQuery); 85 | -------------------------------------------------------------------------------- /index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webGerber 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /ObjectControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @note Edited by eddyb to provide object-centered controls instead of camera-centered. 4 | */ 5 | 6 | THREE.ObjectControls = function ( object, domElement ) { 7 | 8 | THREE.EventTarget.call( this ); 9 | 10 | var _this = this, 11 | STATE = { NONE : -1, ROTATE : 0, ZOOM : 1, PAN : 2 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { width: window.innerWidth, height: window.innerHeight, offsetLeft: 0, offsetTop: 0 }; 21 | this.radius = ( this.screen.width + this.screen.height ) / 4; 22 | 23 | this.rotateSpeed = 1.0; 24 | this.zoomSpeed = 1.2; 25 | this.panSpeed = 0.3; 26 | 27 | this.noRotate = false; 28 | this.noZoom = false; 29 | this.noPan = false; 30 | 31 | this.staticMoving = false; 32 | this.dynamicDampingFactor = 0.2; 33 | 34 | this.minDistance = 0; 35 | this.maxDistance = Infinity; 36 | 37 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 38 | 39 | // internals 40 | 41 | var lastPosition = new THREE.Vector3(); 42 | 43 | var _keyPressed = false, 44 | _state = STATE.NONE, 45 | 46 | //_eye = new THREE.Vector3(), 47 | 48 | _rotateStart = new THREE.Vector3(), 49 | _rotateEnd = new THREE.Vector3(), 50 | 51 | _zoomStart = new THREE.Vector2(), 52 | _zoomEnd = new THREE.Vector2(), 53 | 54 | _panStart = new THREE.Vector2(), 55 | _panEnd = new THREE.Vector2(); 56 | 57 | 58 | // methods 59 | 60 | this.handleEvent = function ( event ) { 61 | 62 | if ( typeof this[ event.type ] == 'function' ) { 63 | 64 | this[ event.type ]( event ); 65 | 66 | } 67 | 68 | }; 69 | 70 | this.getMouseOnScreen = function ( clientX, clientY ) { 71 | 72 | return new THREE.Vector2( 73 | ( clientX - _this.screen.offsetLeft ) / _this.radius * 0.5, 74 | ( clientY - _this.screen.offsetTop ) / _this.radius * 0.5 75 | ); 76 | 77 | }; 78 | 79 | this.getMouseProjectionOnBall = function ( clientX, clientY ) { 80 | 81 | var mouseOnBall = new THREE.Vector3( 82 | ( clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft ) / _this.radius, 83 | ( _this.screen.height * 0.5 + _this.screen.offsetTop - clientY ) / _this.radius, 84 | 0.0 85 | ); 86 | 87 | var length = mouseOnBall.length(); 88 | 89 | if ( length > 1.0 ) { 90 | 91 | mouseOnBall.normalize(); 92 | 93 | } else { 94 | 95 | mouseOnBall.z = Math.sqrt( 1.0 - length * length ); 96 | 97 | } 98 | 99 | var projection = _this.camera.up.clone().setLength( mouseOnBall.y ); 100 | projection.addSelf( _this.camera.up.clone().crossSelf( _this.eye ).setLength( mouseOnBall.x ) ); 101 | projection.addSelf( _this.eye.clone().setLength( mouseOnBall.z ) ); 102 | 103 | return projection;//(new THREE.Quaternion).setFromEuler(_this.object.rotation.clone()/*.negate()*/).multiplyVector3(projection); 104 | 105 | }; 106 | 107 | this.rotateCamera = function () { 108 | var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); 109 | 110 | if ( angle ) { 111 | 112 | var axis = ( new THREE.Vector3() ).cross( _rotateStart, _rotateEnd ).normalize(); 113 | angle *= _this.rotateSpeed; 114 | 115 | if ( _this.staticMoving ) 116 | _rotateStart = _rotateEnd; 117 | else { 118 | _rotateStart.multiplyScalar(1 - _this.dynamicDampingFactor).addSelf(_rotateEnd.clone().multiplyScalar(_this.dynamicDampingFactor)); 119 | //var quaternion = new THREE.Quaternion(); 120 | //quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); 121 | //quaternion.multiplyVector3( _rotateStart ); 122 | 123 | } 124 | 125 | _this.object.useQuaternion = true; 126 | _this.object.quaternion.clone().inverse().multiplyVector3(axis); 127 | _this.object.quaternion.multiplySelf((new THREE.Quaternion).setFromAxisAngle(axis, angle)); 128 | 129 | 130 | } 131 | 132 | }; 133 | 134 | this.zoomCamera = function () { 135 | 136 | var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 137 | 138 | if ( factor !== 1.0 && factor > 0.0 ) { 139 | _this.object.position.y *= factor; 140 | 141 | if ( _this.staticMoving ) 142 | _zoomStart = _zoomEnd; 143 | else 144 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 145 | 146 | } 147 | 148 | }; 149 | 150 | this.panCamera = function () { 151 | 152 | var mouseChange = _panEnd.clone().subSelf( _panStart ); 153 | 154 | if ( mouseChange.x || mouseChange.y ) { 155 | mouseChange.multiplyScalar( Math.abs(_this.object.position.y) * _this.panSpeed ); 156 | 157 | var pan = _this.eye.clone().crossSelf( _this.camera.up ).setLength( mouseChange.x ); 158 | pan.addSelf( _this.camera.up.clone().setLength( mouseChange.y ) ); 159 | 160 | _this.object.position.subSelf( pan ); 161 | 162 | if ( _this.staticMoving ) 163 | _panStart = _panEnd; 164 | else 165 | _panStart.addSelf( mouseChange.sub( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 166 | 167 | } 168 | 169 | }; 170 | 171 | this.update = function () { 172 | if ( !_this.noRotate ) 173 | _this.rotateCamera(); 174 | if ( !_this.noZoom ) 175 | _this.zoomCamera(); 176 | if ( !_this.noPan ) 177 | _this.panCamera(); 178 | }; 179 | 180 | // listeners 181 | 182 | function keydown( event ) { 183 | 184 | if ( ! _this.enabled ) return; 185 | 186 | if ( _state !== STATE.NONE ) { 187 | 188 | return; 189 | 190 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 191 | 192 | _state = STATE.ROTATE; 193 | 194 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 195 | 196 | _state = STATE.ZOOM; 197 | 198 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 199 | 200 | _state = STATE.PAN; 201 | 202 | } 203 | 204 | if ( _state !== STATE.NONE ) { 205 | 206 | _keyPressed = true; 207 | 208 | } 209 | 210 | }; 211 | 212 | function keyup( event ) { 213 | 214 | if ( ! _this.enabled ) return; 215 | 216 | if ( _state !== STATE.NONE ) { 217 | 218 | _state = STATE.NONE; 219 | 220 | } 221 | 222 | }; 223 | 224 | function mousedown( event ) { 225 | 226 | if ( ! _this.enabled ) return; 227 | 228 | event.preventDefault(); 229 | event.stopPropagation(); 230 | 231 | if ( _state === STATE.NONE ) { 232 | 233 | _state = event.button; 234 | 235 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 236 | 237 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 238 | 239 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 240 | 241 | _zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 242 | 243 | } else if ( !_this.noPan ) { 244 | 245 | _panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 246 | 247 | } 248 | 249 | } 250 | 251 | }; 252 | 253 | function mousemove( event ) { 254 | 255 | if ( ! _this.enabled ) return; 256 | 257 | if ( _keyPressed ) { 258 | 259 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 260 | _zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 261 | _panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 262 | 263 | _keyPressed = false; 264 | 265 | } 266 | 267 | if ( _state === STATE.NONE ) { 268 | 269 | return; 270 | 271 | } else if ( _state === STATE.ROTATE && !_this.noRotate ) { 272 | 273 | _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 274 | 275 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 276 | 277 | _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 278 | 279 | } else if ( _state === STATE.PAN && !_this.noPan ) { 280 | 281 | _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 282 | 283 | } 284 | 285 | }; 286 | 287 | function mouseup( event ) { 288 | 289 | if ( ! _this.enabled ) return; 290 | 291 | event.preventDefault(); 292 | event.stopPropagation(); 293 | 294 | _state = STATE.NONE; 295 | 296 | }; 297 | 298 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 299 | 300 | this.domElement.addEventListener( 'mousemove', mousemove, false ); 301 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 302 | this.domElement.addEventListener( 'mouseup', mouseup, false ); 303 | 304 | window.addEventListener( 'keydown', keydown, false ); 305 | window.addEventListener( 'keyup', keyup, false ); 306 | 307 | }; -------------------------------------------------------------------------------- /webGerber.js: -------------------------------------------------------------------------------- 1 | var wG = window.wG || {}; 2 | 3 | wG.BOTTOM = 1, wG.TOP = 2; 4 | wG.BOARD = 0, wG.COPPER = 1, wG.SOLDER = 2, wG.PASTE = 3, wG.SILK = 4, wG.OUTLINE = 5; 5 | 6 | // Layer names. 7 | wG.layerNames = {}; 8 | wG.layerNames[''] = 'No layer'; 9 | wG.layerNames[wG.BOTTOM+''+wG.COPPER] = 'Bottom copper'; 10 | wG.layerNames[wG.BOTTOM+''+wG.SOLDER] = 'Bottom solder mask'; 11 | wG.layerNames[wG.BOTTOM+''+wG.PASTE] = 'Bottom solder paste'; 12 | wG.layerNames[wG.BOTTOM+''+wG.SILK] = 'Bottom silk-screen'; 13 | wG.layerNames[wG.TOP+''+wG.COPPER] = 'Top copper'; 14 | wG.layerNames[wG.TOP+''+wG.SOLDER] = 'Top solder mask'; 15 | wG.layerNames[wG.TOP+''+wG.PASTE] = 'Top solder paste'; 16 | wG.layerNames[wG.TOP+''+wG.SILK] = 'Top silk-screen'; 17 | wG.layerNames[(wG.TOP|wG.BOTTOM)+''+wG.BOARD] = 'Drill'; 18 | wG.layerNames[(wG.TOP|wG.BOTTOM)+''+wG.OUTLINE] = 'Outline'; 19 | 20 | // All the layer types above. 21 | wG.layerTypes = [ 22 | '', 23 | wG.BOTTOM+''+wG.COPPER, 24 | wG.BOTTOM+''+wG.SOLDER, 25 | wG.BOTTOM+''+wG.PASTE, 26 | wG.BOTTOM+''+wG.SILK, 27 | wG.TOP+''+wG.COPPER, 28 | wG.TOP+''+wG.SOLDER, 29 | wG.TOP+''+wG.PASTE, 30 | wG.TOP+''+wG.SILK, 31 | (wG.TOP|wG.BOTTOM)+''+wG.BOARD, 32 | (wG.TOP|wG.BOTTOM)+''+wG.OUTLINE, 33 | ]; 34 | 35 | // Colors for the layers (comments contain fancy math for color deduction :P). 36 | wG.colors = []; 37 | // Board + SolderMask = #36620a 38 | // Board*.5 + (94*.5, 152*.5, 6*.5) = #36620a 39 | // Board = (#36620a - (94/2, 152/2, 6/2))/.5 40 | // Board = (54-47, 98-76, 10-3)*2 41 | // Board = (7*2, 22*2, 7*2) 42 | // Board = (14, 44, 14) = #0e2c0e 43 | wG.colors[wG.BOARD] = '#0e2c0e';//'#203020';//'#255005'; 44 | // Copper = #b87333 45 | wG.colors[wG.COPPER] = '#b87333';//'#c0b030' 46 | // Copper + SolderMask = #8b861d 47 | // SolderMask*.5 + (184*.5, 115*.5, 51*.5) = #8b861d 48 | // SolderMask = (#8b861d - (184/2, 115/2, 51/2))/.5 49 | // SolderMask = (139-92, 134-58, 29-26)*2 50 | // SolderMask = (47*2, 76*2, 3*2) = (94, 152, 6) 51 | wG.colors[wG.SOLDER] = 'rgba(94, 152, 6, .5)';//'rgba(37, 80, 5, .7)'; 52 | wG.colors[wG.PASTE] = '#e6e8fa'; 53 | wG.colors[wG.SILK] = '#ffffff'; 54 | 55 | // Guesses a layer's type from its filename. 56 | wG.guessLayer = function guessLayer(f) { 57 | f = f.toLowerCase(); 58 | if(f.match(/\.drl|\.drd|\.txt|\.xln/)) 59 | return [wG.BOTTOM|wG.TOP, wG.BOARD]; 60 | if(f.match(/\.out|\.gml|outline/)) 61 | return [wG.BOTTOM|wG.TOP, wG.OUTLINE]; 62 | if(f.match(/\.gbl|\.sol/) || f.match(/bot/) && f.match(/copper|signal/)) 63 | return [wG.BOTTOM, wG.COPPER]; 64 | if(f.match(/\.gbs|\.sts/) || f.match(/bot/) && f.match(/s(?:old(?:er|)|)ma?(?:sk|ks)/)) 65 | return [wG.BOTTOM, wG.SOLDER]; 66 | if(f.match(/\.gbp|\.crs/) || f.match(/bot/) && f.match(/pas/)) 67 | return [wG.BOTTOM, wG.PASTE]; 68 | if(f.match(/\.gbo|\.pls/) || f.match(/bot/) && f.match(/si?lk/)) 69 | return [wG.BOTTOM, wG.SILK]; 70 | if(f.match(/\.gtl|\.cmp/) || f.match(/top/) && f.match(/copper|signal/)) 71 | return [wG.TOP, wG.COPPER]; 72 | if(f.match(/\.gts|\.stc/) || f.match(/top/) && f.match(/s(?:old(?:er|)|)ma?(?:sk|ks)/)) 73 | return [wG.TOP, wG.SOLDER]; 74 | if(f.match(/\.gtp|\.crc/) || f.match(/top/) && f.match(/pas/)) 75 | return [wG.TOP, wG.PASTE]; 76 | if(f.match(/\.gto|\.plc/) || f.match(/top/) && f.match(/si?lk/)) 77 | return [wG.TOP, wG.SILK]; 78 | }; 79 | 80 | // Loads a Excellon drill file. 81 | wG.loadDrill = function loadDrill(text) { 82 | text = text.replace(/^[\s%]*M48/, ''); 83 | text = text.replace(/[^\S\n]+/g, ''); 84 | 85 | function numVal(x) { 86 | if(x[0] == '+') 87 | return numVal(x.slice(1)); 88 | if(x[0] == '-') 89 | return -numVal(x.slice(1)); 90 | if(x == '0') 91 | return 0; 92 | if(g.omitLead) 93 | while(x.length < g.num) 94 | x = '0'+x; 95 | else 96 | while(x.length < g.num) 97 | x += '0'; 98 | return parseFloat(x.slice(0, g.int)+'.'+x.slice(g.int), 10); 99 | } 100 | 101 | var cmds = text.split('\n'); 102 | 103 | var g = {offA: 0, offB: 0, shapes: [], cmds: [], scale: 1}, shape, body = false, prevX = 0, prevY = 0; 104 | 105 | for(var i = 0; i < cmds.length; i++) { 106 | var d = cmds[i]; 107 | if(!body) { 108 | if(d[0] == 'T') { 109 | var r = /^T(\d+)[^C]*C([\d.]+)/.exec(d); // assert(r); 110 | g.shapes[parseInt(r[1], 10)] = ['C', +r[2]]; 111 | } 112 | else if(d == 'METRIC,LZ') 113 | g.scale = 1, g.omitLead = false, g.int = 3, g.dec = 3, g.num = 6; 114 | else if(d == 'METRIC,TZ' || d == 'M71') 115 | g.scale = 1, g.omitLead = true, g.int = 3, g.dec = 3, g.num = 6; 116 | else if(d == 'INCH,LZ') 117 | g.scale = 25.4, g.omitLead = false, g.int = 2, g.dec = 4, g.num = 6; 118 | else if(d == 'INCH,TZ' || d == 'M72') 119 | g.scale = 25.4, g.omitLead = true, g.int = 2, g.dec = 4, g.num = 6; 120 | else if(d == '%') 121 | body = true; 122 | } else { 123 | function getNum(offset) { 124 | var r = /^[-+\d]*/.exec(d = d.slice(offset)); // assert(r); 125 | d = d.slice(r[0].length); 126 | return numVal(r[0]); 127 | } 128 | if(d[0] == 'T') 129 | shape = parseInt(d.slice(1), 10); 130 | else if(d[0] == 'R') { 131 | var r = /^\d+/.exec(d = d.slice(1)); // assert(r); 132 | var nr = parseInt(r[0], 10), dx = 0, dy = 0; 133 | d = d.slice(r[0].length); 134 | if(d[0] == 'X') 135 | dx = getNum(1); 136 | if(d[0] == 'Y') 137 | dy = getNum(1); 138 | 139 | // assert(!d.length); 140 | for(var x = prevX, y = prevY, j = 0; j < nr; j++) 141 | x += dx, y += dy, g.cmds.push([(1<<2) | 3, shape, x, y]); 142 | prevX = x, prevY = y; 143 | } 144 | else { 145 | var x = prevX, y = prevY, coords = false; 146 | if(d[0] == 'X') 147 | x = getNum(1), coords = true; 148 | if(d[0] == 'Y') 149 | y = getNum(1), coords = true; 150 | if(coords) { 151 | g.cmds.push([(1<<2) | 3, shape, x, y]); 152 | prevX = x, prevY = y; 153 | } 154 | } 155 | } 156 | } 157 | return g; 158 | }; 159 | 160 | // Loads a Gerber file. 161 | wG.load = function load(text) { 162 | if(text.match(/^[\s%]*M48/)) 163 | return wG.loadDrill(text); 164 | 165 | text = text.replace(/\s+/g, ''); // Get rid of any spaces/newlines. 166 | //text = text.replace(/%%+/g, ''); // Compact parameters. 167 | 168 | // Split into data and parameters sections; 169 | var sections = text.split('%'); 170 | 171 | var g = {offA: 0, offB: 0, shapes: [], cmds: [], scale: 1}, shape = 0, macros = {}, mode = 1, inverted = false, prevX = 0, prevY = 0; 172 | 173 | function numVal(x) { 174 | if(x[0] == '+') 175 | return numVal(x.slice(1)); 176 | if(x[0] == '-') 177 | return -numVal(x.slice(1)); 178 | if(x == '0') 179 | return 0; 180 | if(g.omitLead) 181 | while(x.length < g.num) 182 | x = '0'+x; 183 | else 184 | while(x.length < g.num) 185 | x += '0'; 186 | return parseFloat(x.slice(0, g.int)+'.'+x.slice(g.int), 10); 187 | } 188 | 189 | // Even positions are function codes, odd ones are parameters. 190 | for(var i = 0; i < sections.length; i++) { 191 | // Ignore empty sections. 192 | if(!sections[i].length) 193 | continue; 194 | // Get rid of data end markers at the end of data. 195 | sections[i][sections[i].length-1] == '*' && (sections[i] = sections[i].slice(0, -1)); 196 | sections[i] = sections[i].split('*'); 197 | for(var j = 0; j < sections[i].length; j++) { 198 | var d = sections[i][j]; 199 | if(i%2) { // Parameters. 200 | if(d[0] == 'F' && d[1] == 'S') {// Format Specification. 201 | var r = /^([LT]?)([AI])X(\d)(\d)Y(\d)(\d)$/.exec(d.slice(2)); // assert(r); 202 | g.omitLead = !r[1] || r[1] == 'L'; 203 | g.abs = r[2] == 'A'; 204 | if(!g.abs) throw new Error('Need absolute values'); 205 | g.int = +r[3], g.dec = +r[4], g.num = g.int+g.dec; 206 | } else if(d[0] == 'O' && d[1] == 'F') {// Offset. 207 | var r = /^(?:A([-+\d.]+)|)(?:B([-+\d.]+)|)$/.exec(d.slice(2)); // assert(r); 208 | g.offA = parseInt(r[1], 10), g.offB = parseInt(r[2], 10); 209 | } else if(d == 'IPNEG') // Image Polarity. 210 | throw new Error('Negative image polarity'); 211 | else if(d[0] == 'L' && d[1] == 'P') { // Layer Polarity. 212 | if(inverted && d[2] == 'D') // Switch to dark. 213 | g.cmds.push([16<<2, inverted = false]); 214 | else if(!inverted && d[2] == 'C') // Switch to clear. 215 | g.cmds.push([16<<2, inverted = true]); 216 | } else if(d[0] == 'A' && d[1] == 'M') { // Aperture Macro. 217 | var macro = []; 218 | for(j++; j < sections[i].length; j++) 219 | macro.push(sections[i][j]/*.split(',')*/); 220 | macros[d.slice(2)] = macro; 221 | } else if(d[0] == 'A' && d[1] == 'D' && d[2] == 'D') { // Aperture Definition. 222 | var r = /^(\d+)([^,]+)(?:,(.+)|)$/.exec(d.slice(3)); // assert(r); 223 | var j = r[1]-10, args = []; 224 | if(r[3]) 225 | args = r[3].split('X'); 226 | if(macros[r[2]]) { 227 | function applyArgs(m) { 228 | m = m.replace(/\$(\d+)/g, function(s, n) { 229 | return +args[n-1] || 0; 230 | }).toLowerCase(), repl = true; 231 | while(repl == true) 232 | repl = false, m = m.replace(/([\d.]+)x([\d.]+)/g, function(s, a, b) {return repl = true, a*b}); 233 | repl = true; 234 | while(repl == true) 235 | repl = false, m = m.replace(/([\d.]+)\/([\d.]+)/g, function(s, a, b) {return repl = true, a/b}); 236 | repl = true; 237 | while(repl == true) 238 | repl = false, m = m.replace(/([\d.]+)\+([\d.]+)/g, function(s, a, b) {return repl = true, a+b}); 239 | repl = true; 240 | while(repl == true) 241 | repl = false, m = m.replace(/([\d.]+)-([\d.]+)/g, function(s, a, b) {return repl = true, a-b}); 242 | return m; 243 | } 244 | var m1 = macros[r[2]], m2 = []; 245 | for(var k = 0; k < m1.length; k++) { 246 | var eq = /^\$(\d+)=(.+)$/.exec(m1[k]); 247 | if(eq) 248 | args[eq[1]-1] = +applyArgs(eq[2]); 249 | else 250 | m2.push(applyArgs(m1[k]).split(',').map(function(x) {return +x})); 251 | } 252 | g.shapes[j] = ['M', m2]; 253 | 254 | } else 255 | g.shapes[j] = [r[2]].concat(args.map(function(x) {return +x})); 256 | if(j < shape) 257 | shape = j; 258 | } else if(d == 'MOIN') // Specify Inches. 259 | g.scale = 25.4; 260 | else if(d == 'MOMM') // Specify MMs. 261 | g.scale = 1; 262 | else 263 | console.log(d); 264 | } else { // Function codes. 265 | if(d[0] == 'G' && d[1] == '0' && d[2] == '4' || d[0] == 'M') 266 | continue; 267 | if(d[0] == 'G' && d[1] == '5' && d[2] == '4') 268 | d = d.slice(3); 269 | if(d == 'G70') { // Specify Inches. 270 | g.scale = 25.4; 271 | continue; 272 | } 273 | if(d == 'G74') { // Set Single quadrant mode. 274 | mode &= ~4; 275 | continue; 276 | } 277 | if(d == 'G75') { // Set Multi quadrant mode. 278 | mode |= 4; 279 | continue; 280 | } 281 | if(d == 'G36') { // Start Outline fill. 282 | if(!(mode & 8)) 283 | g.cmds.push([8<<2, true]); 284 | mode |= 8; 285 | continue; 286 | } 287 | if(d == 'G37') { // End Outline fill. 288 | if(mode & 8) 289 | g.cmds.push([8<<2, false]); 290 | mode &= ~8; 291 | continue; 292 | } 293 | var cmode = 0; 294 | if(d[0] == 'G' && d.length > 4) { 295 | var r = /^\d*/.exec(d = d.slice(1)); // assert(r); 296 | mode = (mode & 12) | (cmode = parseInt(r[0], 10)); 297 | d = d.slice(r[0].length); 298 | } 299 | function getNum(offset) { 300 | var r = /^[-+\d]*/.exec(d = d.slice(offset)); // assert(r); 301 | d = d.slice(r[0].length); 302 | return numVal(r[0]); 303 | } 304 | var x = prevX, y = prevY, oi = 0, oj = 0, hasX = false, hasY = false; 305 | if(d[0] == 'X') 306 | x = getNum(1), hasX = true; 307 | if(d[0] == 'Y') 308 | y = getNum(1), hasY = true; 309 | if(d[0] == 'I') 310 | oi = getNum(1), (!(mode&2) && (x += oi, hasX = true)); 311 | if(d[0] == 'J') 312 | oj = getNum(1), (!(mode&2) && (y += oj, hasY = true)); 313 | if(d[0] == 'D') {// Draw. 314 | if(d[1] == '0') 315 | g.cmds.push([(mode<<2) | d[2], shape, x, y, oi, oj]); 316 | else 317 | shape = d.slice(1)-10; 318 | } else if(hasX && (x != prevX) || hasY && (y != prevY)) 319 | g.cmds.push([(mode<<2) | 1, shape, x, y, oi, oj]); 320 | else 321 | console.log(d); 322 | prevX = x, prevY = y; 323 | } 324 | } 325 | } 326 | return g; 327 | }; 328 | 329 | // Extends the limits to include all the shapes in the layer. 330 | wG.touchLimits = function touchLimits(g, r) { 331 | var scale = g.scale; 332 | r.minX /= scale, r.minY /= scale, r.maxX /= scale, r.maxY /= scale; 333 | for(var i = 0; i < g.cmds.length; i++) { 334 | var s = g.shapes[g.cmds[i][1]]; 335 | if(!s) 336 | continue; 337 | var x = g.cmds[i][2], y = g.cmds[i][3], rx = 0, ry = 0; 338 | if(s[0] == 'C') 339 | rx = ry = s[1]/2; 340 | else if(s[0] == 'R') 341 | rx = s[1]/2, ry = s[2]/2; 342 | else 343 | continue; 344 | 345 | if(x-rx < r.minX) 346 | r.minX = x-rx; 347 | if(y-ry < r.minY) 348 | r.minY = y-ry; 349 | if(x+rx > r.maxX) 350 | r.maxX = x+rx; 351 | if(y+ry > r.maxY) 352 | r.maxY = y+ry; 353 | } 354 | r.minX *= scale, r.minY *= scale, r.maxX *= scale, r.maxY *= scale; 355 | }; 356 | 357 | // Renders one layer onto a 2D canvas. 358 | wG.renderLayer = function renderLayer(canvas, g, limits) { 359 | var ctx = canvas.getContext('2d'); 360 | 361 | // Use only for debugging purposes 362 | //var color = g.type ? wG.colors[g.type] : 'black'; 363 | var color = 'black'; 364 | ctx.globalCompositeOperation = 'source-over'; 365 | ctx.fillStyle = color, ctx.strokeStyle = color; 366 | 367 | var scaleX = canvas.width / (limits.maxX-limits.minX) * g.scale, scaleY = canvas.height / (limits.maxY-limits.minY) * g.scale; 368 | var scaleMax = Math.max(scaleX, scaleY); 369 | ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0); 370 | 371 | var prevX = 0, prevY = 0, minX = limits.minX/g.scale, minY = limits.minY/g.scale; 372 | for(var i = 0; i < g.cmds.length; i++) { 373 | var mode = (g.cmds[i][0] >> 2), op = g.cmds[i][0] & 3; 374 | if(mode == 16) { // Switch layer polarity. 375 | ctx.globalCompositeOperation = g.cmds[i][1] ? 'destination-out' : 'source-over'; 376 | continue; 377 | } 378 | var x = g.cmds[i][2]-minX, y = g.cmds[i][3]-minY; 379 | if(mode & 8) { // Outline fill mode. 380 | mode &= ~8; 381 | if(op == 0) { // Start/End Outline fill mode. 382 | if(g.cmds[i][1]) 383 | ctx.beginPath(), ctx.moveTo(prevX, prevY); 384 | else 385 | ctx.fill(); 386 | continue; 387 | } 388 | if(op == 2) // Fill. 389 | ctx.fill(), ctx.beginPath(), ctx.moveTo(x, y); 390 | else if(op == 1) { // Draw. 391 | if(mode == 1 || mode == 5) // Linear Interpolation. 392 | ctx.lineTo(x, y); 393 | else if(mode == 2 || mode == 3) // Single quadrant Circular Interpolation. 394 | console.log('(FILL) Failed to single quadrant '+(mode==3?'CCW':'CW'), g.cmds[i], s); 395 | else if(mode == 6 || mode == 7) { // Multi quadrant Circular Interpolation. 396 | var ox = g.cmds[i][4], oy = g.cmds[i][5], cx = prevX+ox, cy = prevY+oy; 397 | ctx.arc(cx, cy, Math.sqrt(ox*ox+oy*oy), Math.atan2(-oy, -ox), Math.atan2(y-cy, x-cx), mode == 6); 398 | } else 399 | console.log(mode); 400 | } else 401 | console.log(mode, op); 402 | prevX = x, prevY = y; 403 | continue; 404 | } 405 | var s = g.shapes[g.cmds[i][1]]; 406 | if(!s) { 407 | console.log(g.cmds[i], s); 408 | continue; 409 | } 410 | if(op != 2) { 411 | if(op == 3) { // Expose. 412 | if(s[0] == 'C') 413 | ctx.beginPath(), ctx.arc(x, y, s[1]/2, 0, Math.PI*2), ctx.fill(); 414 | else if(s[0] == 'R') 415 | ctx.beginPath(), ctx.rect(x-s[1]/2, y-s[2]/2, s[1], s[2]), ctx.fill(); 416 | else if(s[0] == 'O') { 417 | ctx.beginPath(), ctx.moveTo(x, y - s[2] / 2); 418 | ctx.bezierCurveTo(x + s[1] / 2, y - s[2] / 2, x + s[1] / 2, y + s[2] / 2, x, y + s[2] / 2); 419 | ctx.bezierCurveTo(x - s[1] / 2, y + s[2] / 2, x - s[1] / 2, y - s[2] / 2, x, y - s[2] / 2); 420 | ctx.fill(); 421 | } else if(s[0] == 'M') { // Aperture Macro. 422 | for(var j = 0; j < s[1].length; j++) { 423 | var m = s[1][j]; 424 | if((m[0] == 2 || m[0] == 20) && m[1]) { // Line. 425 | ctx.lineWidth = m[2]; 426 | ctx.lineCap = 'square'; 427 | ctx.beginPath(); 428 | ctx.moveTo(x+m[3], y+m[4]), ctx.lineTo(x+m[5], y+m[6]); 429 | ctx.stroke(); 430 | } else if(m[0] == 21 && m[1]) { // Rectangle. 431 | ctx.beginPath(), ctx.rect(x+m[4]-m[2]/2, y+m[5]-m[3]/2, m[2], m[3]), ctx.fill(); 432 | } else if(m[0] == 4 && m[1]) { // Outline. 433 | ctx.beginPath(); 434 | ctx.moveTo(m[3], m[4]); 435 | for(var k = 1; k < m[2]; k++) 436 | ctx.lineTo(m[3+k*2], m[4+k*2]); 437 | ctx.fill(); 438 | } else if(m[0] == 5 && m[1]) { // Polygon (regular). 439 | var nSides = m[2], cx = x+m[3], cy = y+m[4], r = m[5]/2; 440 | ctx.beginPath(); 441 | var step = 2 * Math.PI / nSides, angle = m[6] * Math.PI / 180; 442 | ctx.moveTo(cx + r * Math.cos(angle), cy + r * Math.sin(angle)); 443 | for(var k = 0; k < nSides; k++) { 444 | angle += step; 445 | ctx.lineTo(cx + r * Math.cos(angle), cy + r * Math.sin(angle)); 446 | } 447 | ctx.fill(); 448 | } else { 449 | console.log('Failed to macro', m, g.cmds[i], s); 450 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 451 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 452 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 453 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 454 | ctx.fillStyle = color; 455 | } 456 | } 457 | } else { 458 | console.log('Failed to expose', g.cmds[i], s); 459 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 460 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 461 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 462 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 463 | ctx.fillStyle = color; 464 | } 465 | } 466 | else if(op == 1) { // Draw. 467 | if(s[0] == 'C') { 468 | if(!s[1]) { 469 | prevX = x, prevY = y; 470 | continue; 471 | } 472 | 473 | //HACK Copper lines get some extra thickness. 474 | if(g.type == wG.COPPER) 475 | ctx.lineWidth = Math.ceil(s[1]*scaleMax/3+.01)/scaleMax*3; 476 | else 477 | ctx.lineWidth = Math.max(s[1], 0.008); 478 | ctx.lineCap = 'round'; 479 | if(mode == 1 || mode == 5) { // Linear Interpolation. 480 | ctx.beginPath(); 481 | ctx.moveTo(prevX, prevY), ctx.lineTo(x, y); 482 | ctx.stroke(); 483 | } else if(mode == 2 || mode == 3) { // Single quadrant Circular Interpolation. 484 | console.log('Failed to single quadrant '+(mode==3?'CCW':'CW'), g.cmds[i], s); 485 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 486 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 487 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 488 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 489 | ctx.fillStyle = color; 490 | } else if(mode == 6 || mode == 7) { // Multi quadrant Circular Interpolation. 491 | var ox = g.cmds[i][4], oy = g.cmds[i][5], cx = prevX+ox, cy = prevY+oy; 492 | ctx.beginPath(); 493 | ctx.arc(cx, cy, Math.sqrt(ox*ox+oy*oy), Math.atan2(-oy, -ox), Math.atan2(y-cy, x-cx), mode == 6); 494 | ctx.stroke(); 495 | } else { 496 | console.log('Failed to draw with circle', g.cmds[i], s); 497 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 498 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 499 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 500 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 501 | ctx.fillStyle = color; 502 | } 503 | } else { 504 | console.log('Failed to draw', g.cmds[i], s); 505 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 506 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 507 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 508 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 509 | ctx.fillStyle = color; 510 | } 511 | } 512 | else { 513 | console.log('Failed to '+mode+' '+type, g.cmds[i], s); 514 | ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 515 | ctx.beginPath(), ctx.arc(x, y, .5, 0, Math.PI*2), ctx.fill(); 516 | ctx.fillStyle = 'rgba(255, 0, 0, .2)'; 517 | ctx.beginPath(), ctx.arc(x, y, 1.5, 0, Math.PI*2), ctx.fill(); 518 | ctx.fillStyle = color; 519 | } 520 | } 521 | prevX = x, prevY = y; 522 | } 523 | 524 | // Color the canvas. 525 | ctx.fillStyle = g.type ? wG.colors[g.type] : 'black'; 526 | ctx.globalCompositeOperation = g.type == wG.SOLDER ? 'source-out' : 'source-in'; 527 | ctx.setTransform(1, 0, 0, 1, 0, 0); 528 | ctx.fillRect(0, 0, canvas.width, canvas.height); 529 | }; 530 | 531 | // Clears a 2D canvas that is a board side texture. 532 | wG.clearBoard = function clearBoard(canvas) { 533 | var ctx = canvas.getContext('2d'); 534 | ctx.globalCompositeOperation = 'source-over'; 535 | ctx.fillStyle = wG.colors[wG.BOARD]; 536 | ctx.fillRect(0, 0, canvas.width, canvas.height); 537 | }; 538 | 539 | // Renders a layer onto a 2D canvas that is a board side texture. 540 | wG.renderBoard = function renderBoard(canvas, g, limits) { 541 | if(!g.canvas) { 542 | g.canvas = document.createElement('canvas'); 543 | g.canvas.width = canvas.width, g.canvas.height = canvas.height; 544 | wG.renderLayer(g.canvas, g, limits); 545 | } 546 | var ctx = canvas.getContext('2d'); 547 | ctx.globalCompositeOperation = g.type ? 'source-over' : 'destination-out'; 548 | if(canvas.invertedY) 549 | ctx.setTransform(1, 0, 0,-1, 0, canvas.height); 550 | else 551 | ctx.setTransform(1, 0, 0, 1, 0, 0); 552 | ctx.drawImage(g.canvas, 0, 0); 553 | }; 554 | 555 | wG.ppmm = 40; // Pixels per mm. 556 | wG.maxTexSize = 4096, wG.minTexSize = 256; // Largest possible texture size. 557 | 558 | // Finds the closest power of two to the given size of a texture (needed for mipmapping). 559 | wG.texSize = function texSize(x) { 560 | x = Math.min(Math.max(x, wG.minTexSize), wG.maxTexSize); 561 | var r = 1; 562 | while(r < x) 563 | r <<= 1; 564 | return r; 565 | }; 566 | 567 | // Creates a 2D canvas that is a board side texture, for the given width and height. 568 | wG.makeBoard = function makeBoard(w, h, invertedY) { 569 | var canvas = document.createElement('canvas'); 570 | //canvas.width = w*wG.ppmm, canvas.height = h*wG.ppmm; 571 | /*var maxSize = Math.max(w*wG.ppmm, h*wG.ppmm) >> 1, size = 1; 572 | while(size < maxSize && size < wG.maxTexSize) 573 | size <<= 1;*/ 574 | canvas.width = wG.texSize(w*wG.ppmm), canvas.height = wG.texSize(h*wG.ppmm); 575 | 576 | // Don't allow mipmapping for stretched textures. 577 | var stretch = canvas.width/canvas.height; 578 | if(stretch > 4) 579 | canvas.width--; 580 | else if(stretch < .25) 581 | canvas.height--; 582 | canvas.invertedY = invertedY; 583 | 584 | // Debugging: adds canvas to the page. 585 | /*canvas.className = 'layer'; 586 | document.body.appendChild(canvas);*/ 587 | return canvas; 588 | }; 589 | 590 | // Finds the largest continuous sequence of lines that forms a loop (i.e. most likely the outline of the board). 591 | wG.findOutline = function findOutline(layers) { 592 | var best = {path: [], minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity, area: 0}; // Best outline. 593 | var oPath, oPrev, oMinX, oMinY, oMaxX, oMaxY; // Current outline. 594 | function reset() { 595 | oPath = [], oPrev = undefined; 596 | oMinX = oMinY = Infinity; 597 | oMaxX = oMaxY = -Infinity; 598 | } 599 | for(var i = 0; i < layers.length; i++) { 600 | reset(); 601 | var cmds = layers[i].cmds, shapes = layers[i].shapes, scale = layers[i].scale, half = scale/2, prevX = 0, prevY = 0; 602 | for(var j = 0; j < cmds.length; j++) { 603 | var cmd = cmds[j], mode = cmd[0] >> 2, x = cmd[2]*scale, y = cmd[3]*scale; 604 | if(mode != 1 && mode != 5 && mode != 6 && mode != 7) { // Look only for lines. 605 | reset(), prevX = x, prevY = y; 606 | continue; 607 | } 608 | if((cmd[0] & 3) != 1) { 609 | ((cmd[0] & 3) == 2 || reset()), prevX = x, prevY = y; 610 | continue; 611 | } 612 | var s = shapes[cmd[1]]; 613 | if(s[0] != 'C') { // Look only for lines with circle ends. 614 | reset(), prevX = x, prevY = y; 615 | continue; 616 | } 617 | var r = s[1]*half; 618 | if(!r) { // Look only for visible lines. 619 | reset(), prevX = x, prevY = y; 620 | continue; 621 | } 622 | if(x-r < oMinX) 623 | oMinX = x-r; 624 | if(y-r < oMinY) 625 | oMinY = y-r; 626 | if(x+r > oMaxX) 627 | oMaxX = x+r; 628 | if(y+r > oMaxY) 629 | oMaxY = y+r; 630 | var line = [prevX, prevY, x, y, r]; 631 | if(mode == 6 || mode == 7) 632 | line.push(cmd[4]*scale, cmd[5]*scale, mode == 6); 633 | 634 | // Try to connect it with the previous line. 635 | if(oPrev) { 636 | var dx = oPrev[2]-prevX, dy = oPrev[3]-prevY, sr = (r+oPrev[4]); 637 | if(dx*dx+dy*dy <= sr*sr) 638 | oPath.push(oPrev = line); 639 | else if(oPath.length == 1 && (dx = oPrev[0]-prevX, dy = oPrev[1]-prevY, sr = (r+oPrev[4]), (dx*dx+dy*dy <= sr*sr))) { // Hack for some weird outlines. 640 | var px = oPrev[2], py = oPrev[3]; 641 | oPrev[2] = oPrev[0], oPrev[3] = oPrev[1]; 642 | oPrev[0] = px, oPrev[1] = py; 643 | oPath.push(oPrev = line); 644 | } else { 645 | reset(), prevX = x, prevY = y; 646 | continue; 647 | } 648 | // Try to connect it with the first line in this outline. 649 | if(oPath.length) { 650 | dx = oPath[0][0]-x, dy = oPath[0][1]-y, sr = (r+oPath[0][4]); 651 | if(dx*dx+dy*dy <= sr*sr) { 652 | var area = (oMaxX - oMinX)*(oMaxY - oMinY); 653 | // Is this outline larger? 654 | if(area > best.area) { 655 | best.path = oPath; 656 | best.minX = oMinX, best.minY = oMinY, best.maxX = oMaxX, best.maxY = oMaxY; 657 | best.area = area; 658 | reset(); 659 | } 660 | } 661 | } 662 | } else 663 | oPath.push(oPrev = line); 664 | prevX = x, prevY = y; 665 | } 666 | } 667 | return best; 668 | }; 669 | 670 | // Renders an outline onto a 2D canvas that is a board side texture, by removing everything outside the outline. 671 | wG.renderOutline = function renderOutline(canvas, outline, limits) { 672 | //if(!outline.path.length) 673 | // return; 674 | var ctx = canvas.getContext('2d'); 675 | ctx.fillStyle = 'black'; 676 | ctx.globalCompositeOperation = 'destination-in'; 677 | var scaleX = canvas.width / (limits.maxX-limits.minX), scaleY = canvas.height / (limits.maxY-limits.minY); 678 | if(canvas.invertedY) 679 | ctx.setTransform(scaleX, 0, 0,-scaleY, 0, canvas.height); 680 | else 681 | ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0); 682 | ctx.beginPath(); 683 | ctx.moveTo(outline.path[0][0]-limits.minX, outline.path[0][1]-limits.minY); 684 | for(var i = 0; i < outline.path.length; i++) { 685 | var cmd = outline.path[i]; 686 | if(cmd.length > 5) { 687 | var ox = cmd[5], oy = cmd[6], cx = cmd[0]+ox, cy = cmd[1]+oy; 688 | ctx.arc(cx-limits.minX, cy-limits.minY, Math.sqrt(ox*ox+oy*oy), Math.atan2(-oy, -ox), Math.atan2(cmd[3]-cy, cmd[2]-cx), cmd[7]); 689 | } else 690 | ctx.lineTo(cmd[2]-limits.minX, cmd[3]-limits.minY); 691 | } 692 | ctx.fill(); 693 | }; 694 | 695 | // Little debug function. 696 | function debug(x) { 697 | if(wG.debugLog) 698 | wG.debugLog.append($('
').text(x)); 699 | } 700 | 701 | function init(layers) { 702 | var limits = {minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity};outlineLayers = layers.filter(function(x) {return x.type == wG.OUTLINE}); 703 | if(outlineLayers.length) 704 | layers = layers.filter(function(x) {return x.type != wG.OUTLINE}); 705 | else 706 | outlineLayers = layers; 707 | for(var i = 0; i < layers.length; i++) { 708 | wG.touchLimits(layers[i], limits); 709 | layers[i].enabled = true; 710 | } 711 | var w = limits.maxX-limits.minX, h = limits.maxY-limits.minY; 712 | 713 | var renderer, has3D = true; 714 | try { 715 | renderer = new THREE.WebGLRenderer({antialias: true}); 716 | //wG.ppmm = 20; 717 | renderer.sortObjects = false; 718 | } catch(e) { 719 | debug('Got WebGL error, falling back to 2D canvas.'); 720 | has3D = false; 721 | wG.ppmm = 20; 722 | renderer = new THREE.CanvasRenderer({antialias: true}); 723 | } 724 | 725 | var scene = new THREE.Scene(), camera = new THREE.PerspectiveCamera(40); 726 | camera.up.set(0, 0, -1); 727 | scene.add(camera); 728 | 729 | // Ambient light. 730 | var ambientLight = new THREE.AmbientLight(0xcccccc); 731 | scene.add(ambientLight); 732 | 733 | // Sun light. 734 | if(has3D) { 735 | var sunLight = new THREE.SpotLight(0xcccccc, .3); 736 | sunLight.position.set(0, 150000, 0); 737 | scene.add(sunLight); 738 | } 739 | 740 | // Board. 741 | var Material = has3D ? THREE.MeshPhongMaterial : THREE.MeshBasicMaterial; 742 | var bottom = wG.makeBoard(w, h), top = wG.makeBoard(w, h, true); 743 | var bottomTexture = new THREE.Texture(bottom), topTexture = new THREE.Texture(top); 744 | wG.clearBoard(bottom), wG.clearBoard(top); 745 | bottomTexture.needsUpdate = true, topTexture.needsUpdate = true; 746 | var materials = [ 747 | null, 748 | null, 749 | new Material({shininess: 80, ambient: 0xaaaaaa, specular: 0xcccccc, map: topTexture}), 750 | new Material({shininess: 80, ambient: 0xaaaaaa, specular: 0xcccccc, map: bottomTexture}), 751 | null, 752 | null 753 | ]; 754 | if(!has3D) 755 | materials[2].overdraw = true, materials[3].overdraw = true; 756 | var board = new THREE.Mesh(new THREE.CubeGeometry(w, 1.54, h, has3D ? 1 : Math.ceil(w / 3), 1, has3D ? 1 : Math.ceil(h / 3), materials, {px: 0, nx: 0, pz: 0, nz: 0}), new THREE.MeshFaceMaterial()); 757 | board.position.y = -100; 758 | 759 | if(has3D) 760 | scene.add(board); 761 | 762 | // Add the sides. 763 | var boardMaterial = new Material({shininess: 80, ambient: 0x333333, specular: 0xcccccc, color: 0x255005}); 764 | var boardSides = new THREE.CubeGeometry(w, 1.54, h, 1, 1, 1, undefined, {py: 0, ny: 0}); 765 | //boardSides.computeVertexNormals(); 766 | boardSides = new THREE.Mesh(boardSides, boardMaterial); 767 | board.add(boardSides); 768 | 769 | // Create all the holes. 770 | var holeMaterial = boardMaterial.clone(); 771 | holeMaterial.side = THREE.BackSide; 772 | for(var i = 0; i < layers.length; i++) 773 | if(!layers[i].type) 774 | for(var j = 0; j < layers[i].cmds.length; j++) { 775 | var cmd = layers[i].cmds[j]; 776 | if(cmd[0] != ((1<<2) | 3)) 777 | continue; 778 | var r = layers[i].scale*layers[i].shapes[cmd[1]][1]/2; 779 | var hole = new THREE.CylinderGeometry(r, r, 1.54, 32, 0, true); 780 | //hole.computeVertexNormals(); 781 | hole = new THREE.Mesh(hole, holeMaterial); 782 | hole.position.x = (cmd[2]*layers[i].scale-limits.minX)-w/2; 783 | hole.position.z = h/2-(cmd[3]*layers[i].scale-limits.minY); 784 | board.add(hole); 785 | } 786 | if(!has3D) 787 | scene.add(board); 788 | 789 | camera.lookAt(board.position); 790 | 791 | var boardControls = new THREE.ObjectControls(board, renderer.domElement); 792 | boardControls.camera = camera; 793 | boardControls.eye = camera.position.clone().subSelf(board.position); 794 | 795 | // Window resize handler. 796 | $(window).resize(function() { 797 | renderer.setSize(window.innerWidth, window.innerHeight); 798 | camera.aspect = window.innerWidth / window.innerHeight; 799 | camera.updateProjectionMatrix(); 800 | boardControls.screen.width = window.innerWidth, boardControls.screen.height = window.innerHeight; 801 | boardControls.radius = (window.innerWidth + window.innerHeight) / 4; 802 | }).resize(); 803 | 804 | // Scrolling wheel handler. 805 | $(document).mousewheel(function(event, delta, deltaX, deltaY) { 806 | board.position.y *= 1-deltaY*.06; 807 | }); 808 | 809 | document.body.appendChild(renderer.domElement); 810 | 811 | // Stats. 812 | /*var stats = new Stats; 813 | $(stats.domElement).css({position: 'absolute', top: 0}).appendTo('body');*/ 814 | 815 | // Controls. 816 | var controls = $('
').appendTo('body'), controlsText = $('').click(function() { 817 | controls.toggleClass('open'); 818 | controlsText.text(controls.is('.open') ? 'Hide controls' : 'Show controls'); 819 | }).appendTo(controls), controlsBox = $('
').appendTo(controls); 820 | controlsText.click(); 821 | 822 | // "Load other files" button. 823 | controlsBox.append($('