├── 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($('