├── test ├── test.simulator.js ├── screeny.png └── test.utils.js ├── .gitignore ├── lib ├── util.js ├── box.js ├── math │ ├── point.js │ └── matrix.js ├── simulator.js ├── scene.js └── OrbitControls.js ├── readme.md ├── package.json ├── bin └── gsim └── index.html /test/test.simulator.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /test/screeny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gsim/master/test/screeny.png -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var util = module.exports = {}; 2 | 3 | util.pointOnPath = function(path,t) { 4 | } 5 | -------------------------------------------------------------------------------- /test/test.utils.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , utils = require('../lib/utils'); 3 | 4 | describe('Simulator', function() { 5 | it('#arcToPoints', function() { 6 | expect(result.end.y).closeTo(5, 0.000001); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Gsim 2 | 3 | Cross-platform command-line gcode simulator. 4 | 5 | ``` 6 | cat smile.gcode | gsim 7 | ``` 8 | 9 | ![alt](test/screeny.png) 10 | 11 | 12 | ## Installation 13 | ``` 14 | npm install -g gsim 15 | ``` 16 | 17 | ## Meta Comments 18 | 19 | Gsim can be annotated with addition information about the job with meta attribute comments. 20 | The format is simply `(key=value key2=value2)`. 21 | 22 | ``` 23 | (tooldiameter=0.5) 24 | G0 X1 25 | G0 Y1 26 | ... 27 | ``` 28 | 29 | My hope is that other generators and simulators will adopt this convention and collaborate towards a standard. 30 | 31 | Currently `tooldiameter` is the only attribute. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.html", 3 | "name": "gsim", 4 | "description": "Gcode Simulator", 5 | "version": "0.0.6", 6 | "keywords": [ 7 | "gcode", 8 | "simulator" 9 | ], 10 | "bin": { 11 | "gsim": "./bin/gsim" 12 | }, 13 | "window": { 14 | "title": "Gsim", 15 | "icon": "link.png", 16 | "toolbar": false, 17 | "frame": true, 18 | "width": 800, 19 | "height": 500, 20 | "position": "mouse", 21 | "min_width": 400, 22 | "min_height": 200 23 | }, 24 | "webkit": { 25 | "plugin": true 26 | }, 27 | "dependencies": { 28 | "nodewebkit": "~0.8.6-3", 29 | "readline": "0.0.3" 30 | }, 31 | "devDependencies": { 32 | "mocha": "~1.20.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/box.js: -------------------------------------------------------------------------------- 1 | module.exports = Box; 2 | 3 | function Box() { 4 | this.xmin = 0; 5 | this.xmax = 0; 6 | this.ymin = 0; 7 | this.ymax = 0; 8 | this.zmin = 0; 9 | this.zmax = 0; 10 | } 11 | 12 | Box.prototype.fitTo = function(p) { 13 | this.xmin = Math.min(p.x, this.xmin); 14 | this.ymin = Math.min(p.y, this.ymin); 15 | this.zmin = Math.min(p.z, this.zmin); 16 | this.xmax = Math.max(p.x, this.xmax); 17 | this.ymax = Math.max(p.y, this.ymax); 18 | this.zmax = Math.max(p.z, this.zmax); 19 | } 20 | 21 | Box.prototype.width = function() { 22 | return this.xmax - this.xmin; 23 | } 24 | 25 | Box.prototype.height = function() { 26 | return this.ymax - this.ymin; 27 | } 28 | 29 | Box.prototype.cx = function() { 30 | return this.xmin + this.width() / 2; 31 | } 32 | 33 | Box.prototype.cy = function() { 34 | return this.ymin + this.height() / 2; 35 | } 36 | -------------------------------------------------------------------------------- /bin/gsim: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var net = require('net'); 5 | var exec = require('child_process').spawn; 6 | var repl = require('repl'); 7 | var readline = require('readline'); 8 | 9 | 10 | // This server listens on a Unix socket at /var/run/mysocket 11 | var unixServer = net.createServer(function(client) { 12 | var rl = readline.createInterface({ 13 | input: process.stdin, 14 | output: process.stdout 15 | }); 16 | 17 | rl.on('line', function(gcode) { 18 | client.write(gcode+'\n'); 19 | }); 20 | }); 21 | 22 | 23 | // var socket = path.join(__dirname, '/socket'); 24 | var socket = 9999; 25 | 26 | unixServer.listen(socket); 27 | 28 | var gui = exec( path.join(__dirname, '../node_modules/.bin/nodewebkit'), [], { 29 | cwd: path.join(__dirname, '../') 30 | }); 31 | 32 | gui.on('exit', function(code, signal) { 33 | process.exit(code); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gsim 5 | 6 | 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/math/point.js: -------------------------------------------------------------------------------- 1 | module.exports = Point; 2 | 3 | function Point(x,y,z,a) { 4 | this.x = x; 5 | this.y = y; 6 | this.z = z; 7 | this.a = a; 8 | }; 9 | 10 | Point.prototype = { 11 | clone: function() { 12 | return new Point(this.x,this.y); 13 | }, 14 | 15 | round: function() { 16 | return new Point(Math.round(this.x),Math.round(this.y)); 17 | }, 18 | 19 | each: function(f) { 20 | return new Point(f(this.x),f(this.y)); 21 | }, 22 | 23 | /** 24 | * Check whether two points are equal. The x and y values must be exactly 25 | * equal for this method to return true. 26 | * @name equal 27 | * @methodOf Point# 28 | * 29 | * @param {Point} other The point to check for equality. 30 | * @returns true if this point is equal to the other point, false 31 | * otherwise. 32 | * @type Boolean 33 | */ 34 | equal: function(other) { 35 | return this.x === other.x && this.y === other.y; 36 | }, 37 | /** 38 | * Adds a point to this one and returns the new point. 39 | * @name add 40 | * @methodOf Point# 41 | * 42 | * @param {Point} other The point to add this point to. 43 | * @returns A new point, the sum of both. 44 | * @type Point 45 | */ 46 | add: function(other) { 47 | return new Point(this.x + other.x, this.y + other.y); 48 | }, 49 | /** 50 | * Subtracts a point from this one and returns the new point. 51 | * @name sub 52 | * @methodOf Point# 53 | * 54 | * @param {Point} other The point to subtract from this point. 55 | * @returns A new point, the difference of both. 56 | * @type Point 57 | */ 58 | sub: function(other) { 59 | return new Point(this.x - other.x, this.y - other.y); 60 | }, 61 | /** 62 | * Multiplies this point by a scalar value and returns the new point. 63 | * @name scale 64 | * @methodOf Point# 65 | * 66 | * @param {Point} scalar The value to scale this point by. 67 | * @returns A new point with x and y multiplied by the scalar value. 68 | * @type Point 69 | */ 70 | scale: function(scalar) { 71 | return new Point(this.x * scalar, this.y * scalar); 72 | }, 73 | 74 | /** 75 | * Returns the distance of this point from the origin. If this point is 76 | * thought of as a vector this distance is its magnitude. 77 | * @name magnitude 78 | * @methodOf Point# 79 | * 80 | * @returns The distance of this point from the origin. 81 | * @type Number 82 | */ 83 | magnitude: function(/* newMagnitude */) { 84 | if(arguments[0] === undefined) 85 | return Math.sqrt(this.x*this.x + this.y*this.y); 86 | 87 | return this.toUnit().multiply(arguments[0]); 88 | }, 89 | 90 | multiply: function(d) { 91 | return new Point(this.x * d, this.y * d); 92 | }, 93 | 94 | normalize: function() { 95 | return this.multiply(1/this.magnitude()); 96 | }, 97 | 98 | set: function(x,y) { 99 | this.x = x; 100 | this.y = y; 101 | }, 102 | 103 | dot: function(other) { 104 | return this.x * other.x + this.y * other.y; 105 | }, 106 | 107 | translate: function(x,y) { 108 | return new Point(this.x + x, this.y + y); 109 | }, 110 | 111 | 112 | rotate: function(a) { 113 | // Return a new vector that's a copy of this vector rotated by a radians 114 | return new Vector(this.x * Math.cos(a) - this.y*Math.sin(a), 115 | this.x * Math.sin(a) + this.y*Math.cos(a)); 116 | }, 117 | 118 | 119 | angleTo: function(other) { 120 | return Math.acos(this.dot(other) / (Math.abs(this.dist()) * Math.abs(other.dist()))); 121 | }, 122 | 123 | toUnit: function() { 124 | return this.multiply(1/this.magnitude()); 125 | } 126 | }; 127 | 128 | 129 | /** 130 | * @param {Point} p1 131 | * @param {Point} p2 132 | * @returns The Euclidean distance between two points. 133 | */ 134 | Point.distance = function(p1, p2) { 135 | return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); 136 | }; 137 | 138 | /** 139 | * If you have two dudes, one standing at point p1, and the other 140 | * standing at point p2, then this method will return the direction 141 | * that the dude standing at p1 will need to face to look at p2. 142 | * @param {Point} p1 The starting point. 143 | * @param {Point} p2 The ending point. 144 | * @returns The direction from p1 to p2 in radians. 145 | */ 146 | Point.direction = function(p1, p2) { 147 | return Math.atan2( 148 | p2.y - p1.y, 149 | p2.x - p1.x 150 | ); 151 | }; 152 | -------------------------------------------------------------------------------- /lib/simulator.js: -------------------------------------------------------------------------------- 1 | var Box = require('./lib/box'); 2 | 3 | window.Simulator = Simulator; 4 | 5 | function Simulator(scene) { 6 | this.scene = scene; 7 | this.dist = 0; 8 | this.all = []; 9 | this.cur = new THREE.Vector3(0,0,0); 10 | this.all = [this.cur]; 11 | this.box = new Box(); 12 | this.meta = {}; 13 | this.metaTypes = { 14 | tooldiameter: Number 15 | }; 16 | } 17 | 18 | Simulator.prototype = { 19 | parseMeta: function(gcode) { 20 | var attrs = gcode.match(/([a-z]+)\s*=\s*([^\s\)]+)/g) 21 | if(!attrs) return false; 22 | 23 | attrs.forEach(function(attr) { 24 | var kv = attr.split('='); 25 | var k = kv[0].trim(); 26 | var v = kv[1].trim(); 27 | var type = this.metaTypes[k.toLowerCase()] || String; 28 | this.meta[k] = type(v); 29 | }, this); 30 | 31 | return true; 32 | } 33 | , add: function(gcode) { 34 | gcode = gcode.toLowerCase(); 35 | 36 | // Try meta first 37 | if(this.parseMeta(gcode)) { 38 | return; 39 | } 40 | 41 | var parts = gcode.match(/([a-z][\d\.\-]+)/g); 42 | 43 | if(!parts) return; 44 | 45 | var params = {}; 46 | parts.forEach(function(part) { 47 | var kv = part.match(/([a-z])([\d\.\-]+)/) 48 | var k = kv[1]; 49 | var v = Number(kv[2]); 50 | params[k] = v; 51 | }); 52 | 53 | var g = params.g !== undefined ? params.g : this.cur.g; 54 | 55 | // Delegate to handler fn 56 | if(this['g'+g]) { 57 | this['g'+g](params); 58 | } 59 | 60 | if(params.m == 30) { 61 | scene.add(this.line); 62 | } 63 | } 64 | 65 | , g0: function(p) { 66 | this.setPathMode('rapid'); 67 | this.addPoint(p); 68 | } 69 | 70 | , g1: function(p) { 71 | this.setPathMode('linear'); 72 | this.addPoint(p); 73 | } 74 | , g2: function(p) { 75 | this.setPathMode('linear'); 76 | this.arc(p, false); 77 | } 78 | , g3: function(p) { 79 | this.setPathMode('linear'); 80 | this.arc(p, true); 81 | } 82 | 83 | , arc: function(p, ccw) { 84 | var divisions = 20; 85 | var x0 = this.cur.x || 0; 86 | var y0 = this.cur.y || 0; 87 | var z0 = this.cur.z || 0; 88 | var x1 = p.x; 89 | var y1 = p.y; 90 | var z1 = p.z; 91 | var cx = x0 + p.i; 92 | var cy = y0 + p.j; 93 | var rx = p.i; 94 | var ry = p.j; 95 | 96 | var astart = Math.atan2(y0 - cy, x0 - cx); 97 | var aend = Math.atan2(y1 - cy, x1 - cx); 98 | var radius = Math.sqrt(rx*rx+ry*ry); 99 | 100 | // Always assume a full circle 101 | // if they are the same 102 | // Handling of 0,0 optimized in the usage 103 | if(aend === astart) { 104 | aend += Math.PI*2; 105 | } 106 | 107 | // aend = aend % Math.PI*2; 108 | // astart = astart % Math.PI*2; 109 | 110 | 111 | var deltaAngle = aend - astart; 112 | 113 | for ( j = 0; j <= divisions; j ++ ) { 114 | t = j / divisions; 115 | 116 | if(deltaAngle === -Math.PI*2) { 117 | deltaAngle = Math.PI*2; 118 | } 119 | 120 | if(deltaAngle < 0) { 121 | deltaAngle += Math.PI*2; 122 | } 123 | 124 | if(deltaAngle > Math.PI*2) { 125 | deltaAngle -= Math.PI*2; 126 | } 127 | 128 | if ( ccw ) { 129 | // sin(pi) and sin(0) are the same 130 | // So we have to special case for full circles 131 | if(deltaAngle === Math.PI*2) { 132 | deltaAngle = 0; 133 | } 134 | 135 | angle = aend + ( 1 - t ) * ( Math.PI * 2 - deltaAngle ); 136 | } else { 137 | angle = astart + t * deltaAngle; 138 | } 139 | 140 | var tx = cx + radius * Math.cos( angle ); 141 | var ty = cy + radius * Math.sin( angle ); 142 | var tz = z0+(z1-z0)*t; 143 | 144 | this.addPoint( {x: tx, y:ty, z:tz } ); 145 | } 146 | } 147 | 148 | , setPathMode: function(mode) { 149 | if(mode === this.mode) return; 150 | 151 | if(this.toolpath) { 152 | 153 | scene.add(this.line); 154 | } 155 | 156 | var geometry = new THREE.Geometry(); 157 | 158 | var cur = this.cur; 159 | 160 | geometry.vertices.push( 161 | new THREE.Vector3(cur.x,-cur.y,cur.z) 162 | ); 163 | 164 | var material = new THREE.LineBasicMaterial({ 165 | color: mode=='rapid' ? 0x0000cc : 0x333333 166 | }); 167 | 168 | // material.opacity = 0.75; 169 | // material.linewidth = 1; 170 | 171 | var line = new THREE.Line(geometry, material); 172 | this.line = line; 173 | this.toolpath = geometry; 174 | this.mode = mode; 175 | 176 | // line.castShadow = true; 177 | 178 | } 179 | 180 | , addPoint: function(p) { 181 | var cur = this.cur; 182 | var x = p.x === undefined ? cur.x : p.x; 183 | var y = p.y === undefined ? cur.y : p.y; 184 | var z = p.z === undefined ? cur.z : p.z; 185 | 186 | 187 | var xo = x-cur.x; 188 | var yo = y-cur.y; 189 | var len = Math.sqrt(xo*xo + yo*yo); 190 | this.dist += len; 191 | 192 | var v = new THREE.Vector3(x,-y,z); 193 | this.toolpath.vertices.push(v.clone()); 194 | 195 | v.dist = this.dist; 196 | this.all.push(v); 197 | 198 | for(var k in p) { 199 | this.cur[k] = p[k]; 200 | } 201 | 202 | this.box.fitTo(this.cur); 203 | 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /lib/scene.js: -------------------------------------------------------------------------------- 1 | var container; 2 | var camera, scene, renderer; 3 | var mesh, group1, group2, group3, light; 4 | var mouseX = 0, mouseY = 0; 5 | var windowHalfX = window.innerWidth / 2; 6 | var windowHalfY = window.innerHeight / 2; 7 | 8 | init(); 9 | animate(); 10 | 11 | function init() { 12 | 13 | container = document.createElement( 'div' ); 14 | document.body.appendChild( container ); 15 | 16 | var info = document.createElement( 'div' ); 17 | info.style.position = 'absolute'; 18 | info.style.top = '10px'; 19 | info.style.width = '100%'; 20 | info.style.textAlign = 'center'; 21 | container.appendChild( info ); 22 | // 23 | /* camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 ); */ 24 | 25 | camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 ); 26 | camera.position.z = 1; 27 | camera.position.y = 1; 28 | camera.position.x = 1; 29 | camera.up = new THREE.Vector3(0,0,1); 30 | 31 | controls = new THREE.OrbitControls( camera ); 32 | controls.addEventListener( 'change', render ); 33 | 34 | scene = new THREE.Scene(); 35 | 36 | 37 | 38 | // tool 39 | 40 | tool = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 10, 20), new THREE.MeshNormalMaterial()); 41 | 42 | var line = new THREE.Line( geometry, material ); 43 | tool.rotation.x = Math.PI/2; 44 | tool.position.z = -20; 45 | tool.material.transparent = true; 46 | tool.material.opacity = 0.5; 47 | scene.add(tool); 48 | 49 | // Grid 50 | 51 | var size = 100, step = 1; 52 | 53 | var geometry = new THREE.Geometry(); 54 | 55 | for ( var i = - size; i <= size; i += step ) { 56 | 57 | geometry.vertices.push( new THREE.Vector3( - size, i, 0) ); 58 | geometry.vertices.push( new THREE.Vector3( size, i, 0 ) ); 59 | 60 | geometry.vertices.push( new THREE.Vector3( i, - size, 0 ) ); 61 | geometry.vertices.push( new THREE.Vector3( i, size, 0 ) ); 62 | 63 | } 64 | 65 | var material = new THREE.LineBasicMaterial( { color: 0xcccccc, opacity: 0.5 } ); 66 | 67 | var line = new THREE.Line( geometry, material ); 68 | 69 | line.receiveShadow = true; 70 | line.type = THREE.LinePieces; 71 | scene.add( line ); 72 | 73 | // Lights 74 | 75 | var ambientLight = new THREE.AmbientLight(0x10); 76 | scene.add( ambientLight ); 77 | 78 | var directionalLight = new THREE.DirectionalLight( Math.random() * 0xffffff ); 79 | directionalLight.position.x = Math.random() - 0.5; 80 | directionalLight.position.y = Math.random() - 0.5; 81 | directionalLight.position.z = Math.random() - 0.5; 82 | directionalLight.position.normalize(); 83 | scene.add( directionalLight ); 84 | 85 | var directionalLight = new THREE.DirectionalLight( Math.random() * 0xffffff ); 86 | directionalLight.position.x = Math.random() - 0.5; 87 | directionalLight.position.y = Math.random() - 0.5; 88 | directionalLight.position.z = Math.random() - 0.5; 89 | directionalLight.position.normalize(); 90 | scene.add( directionalLight ); 91 | 92 | renderer = new THREE.CanvasRenderer(); 93 | renderer.setSize( window.innerWidth, window.innerHeight ); 94 | // 95 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 96 | renderer.setClearColor( 0xeeeeee ); 97 | renderer.setSize( window.innerWidth, window.innerHeight ); 98 | 99 | 100 | container.appendChild( renderer.domElement ); 101 | 102 | window.addEventListener( 'resize', onWindowResize, false ); 103 | document.addEventListener( 'mousemove', onDocumentMouseMove, false ); 104 | // document.addEventListener( 'mousewheel', onMouseWheel, false ); 105 | // document.addEventListener( 'keypress', onKeyPress, false ); 106 | window.addEventListener( 'resize', onWindowResize, false ); 107 | 108 | } 109 | 110 | function onWindowResize() { 111 | 112 | windowHalfX = window.innerWidth / 2; 113 | windowHalfY = window.innerHeight / 2; 114 | 115 | camera.aspect = window.innerWidth / window.innerHeight; 116 | camera.updateProjectionMatrix(); 117 | 118 | renderer.setSize( window.innerWidth, window.innerHeight ); 119 | 120 | } 121 | 122 | function onDocumentMouseMove( event ) { 123 | mouseX = ( event.clientX - windowHalfX ); 124 | mouseY = ( event.clientY - windowHalfY ); 125 | } 126 | 127 | function onMouseWheel( event ){ 128 | var delta = 0; 129 | 130 | if (event.wheelDelta) { /* IE/Opera. */ 131 | delta = event.wheelDelta/120; 132 | } 133 | // firefox 134 | else if( event.detail ){ 135 | delta = -event.detail/3; 136 | } 137 | 138 | if (delta) 139 | handleMWheel(delta); 140 | 141 | event.preventDefault(); 142 | } 143 | 144 | function onKeyPress( event ) { 145 | var key = String.fromCharCode(event.charCode); 146 | 147 | switch(key) { 148 | case 't': 149 | camera.mode = 'fixed'; 150 | camera.position.x = 0; 151 | camera.position.y = 0; 152 | camera.position.z = 0; 153 | break; 154 | } 155 | } 156 | 157 | var zoom = 1; 158 | function handleMWheel( delta ) { 159 | zoom += delta * 0.1; 160 | zoom = Math.min(50.0,zoom); 161 | zoom = Math.max(0.001,zoom); 162 | 163 | camera.fov = 35 * zoom; 164 | camera.updateProjectionMatrix(); 165 | // camera.fov.set(fov,fov,fov); 166 | } 167 | 168 | // 169 | function animate() { 170 | requestAnimationFrame( animate ); 171 | 172 | render(); 173 | } 174 | 175 | function render() { 176 | var x = ( mouseX ) * 0.5; 177 | var y = ( mouseY ) * 0.5; 178 | 179 | // camera.position.z = 100 * zoom; 180 | 181 | var yt = -y / (window.innerHeight/2) + 0.5; 182 | // camera.position = new THREE.Vector3(0,0,0); 183 | // camera.position.z = 20; 184 | 185 | 186 | if(window.simulator) { 187 | var t = x / (window.innerWidth/2) + 0.5; 188 | var i = simulator.all.length*t; 189 | var a = Math.floor(i); 190 | var b = Math.ceil(i); 191 | var pa = simulator.all[a]; 192 | var pb = simulator.all[b] || pa; 193 | 194 | var l = (pb.dist - pa.dist)*(t%1); 195 | 196 | var p = pb.clone().sub(pa).setLength(l).add(pa); 197 | 198 | if(p) { 199 | tool.position = p.clone(); 200 | } 201 | 202 | 203 | // controls 204 | } 205 | 206 | var d = window.simulator && simulator.meta.tooldiameter || 1; 207 | tool.scale.x = d/2; 208 | tool.scale.y = d/2; 209 | tool.scale.z = d/2; 210 | tool.position.z += 5*d/2; 211 | // tool.scale.z = d; 212 | 213 | // camera.lookAt( tool.position ); 214 | 215 | renderer.render( scene, camera ); 216 | } 217 | 218 | window.scene = scene; 219 | -------------------------------------------------------------------------------- /lib/math/matrix.js: -------------------------------------------------------------------------------- 1 | module.exports = Matrix; 2 | 3 | var Point = require('./point'); 4 | 5 | /** 6 | *
  7 |  *  _        _
  8 |  * | a  c tx  |
  9 |  * | b  d ty  |
 10 |  * |_0  0  1 _|
 11 |  * 
12 | * Creates a matrix for 2d affine transformations. 13 | * 14 | * concat, inverse, rotate, scale and translate return new matrices with the 15 | * transformations applied. The matrix is not modified in place. 16 | * 17 | * Returns the identity matrix when called with no arguments. 18 | * @name Matrix 19 | * @param {Number} [a] 20 | * @param {Number} [b] 21 | * @param {Number} [c] 22 | * @param {Number} [d] 23 | * @param {Number} [tx] 24 | * @param {Number} [ty] 25 | * @constructor 26 | */ 27 | function Matrix(a, b, c, d, tx, ty) { 28 | this.a = a !== undefined ? a : 1; 29 | this.b = b || 0; 30 | this.c = c || 0; 31 | this.d = d !== undefined ? d : 1; 32 | this.tx = tx || 0; 33 | this.ty = ty || 0; 34 | } 35 | 36 | Matrix.prototype = { 37 | 38 | clone: function() { 39 | return new Matrix( 40 | this.a, 41 | this.b, 42 | this.c, 43 | this.d, 44 | this.tx, 45 | this.ty 46 | ); 47 | }, 48 | 49 | /** 50 | * Returns the result of this matrix multiplied by another matrix 51 | * combining the geometric effects of the two. In mathematical terms, 52 | * concatenating two matrixes is the same as combining them using matrix multiplication. 53 | * If this matrix is A and the matrix passed in is B, the resulting matrix is A x B 54 | * http://mathworld.wolfram.com/MatrixMultiplication.html 55 | * @name concat 56 | * @methodOf Matrix# 57 | * 58 | * @param {Matrix} matrix The matrix to multiply this matrix by. 59 | * @returns The result of the matrix multiplication, a new matrix. 60 | * @type Matrix 61 | */ 62 | concat: function(matrix) { 63 | return new Matrix( 64 | this.a * matrix.a + this.c * matrix.b, 65 | this.b * matrix.a + this.d * matrix.b, 66 | this.a * matrix.c + this.c * matrix.d, 67 | this.b * matrix.c + this.d * matrix.d, 68 | this.a * matrix.tx + this.c * matrix.ty + this.tx, 69 | this.b * matrix.tx + this.d * matrix.ty + this.ty 70 | ); 71 | }, 72 | 73 | /** 74 | * Given a point in the pretransform coordinate space, returns the coordinates of 75 | * that point after the transformation occurs. Unlike the standard transformation 76 | * applied using the transformnew Point() method, the deltaTransformnew Point() method's 77 | * transformation does not consider the translation parameters tx and ty. 78 | * @name deltaTransformPoint 79 | * @methodOf Matrix# 80 | * @see #transformPoint 81 | * 82 | * @return A new point transformed by this matrix ignoring tx and ty. 83 | * @type Point 84 | */ 85 | deltaTransformPoint: function(point) { 86 | return new Point( 87 | this.a * point.x + this.c * point.y, 88 | this.b * point.x + this.d * point.y 89 | ); 90 | }, 91 | 92 | /** 93 | * Returns the inverse of the matrix. 94 | * http://mathworld.wolfram.com/MatrixInverse.html 95 | * @name inverse 96 | * @methodOf Matrix# 97 | * 98 | * @returns A new matrix that is the inverse of this matrix. 99 | * @type Matrix 100 | */ 101 | inverse: function() { 102 | var determinant = this.a * this.d - this.b * this.c; 103 | return new Matrix( 104 | this.d / determinant, 105 | -this.b / determinant, 106 | -this.c / determinant, 107 | this.a / determinant, 108 | (this.c * this.ty - this.d * this.tx) / determinant, 109 | (this.b * this.tx - this.a * this.ty) / determinant 110 | ); 111 | }, 112 | 113 | /** 114 | * Returns a new matrix that corresponds this matrix multiplied by a 115 | * a rotation matrix. 116 | * @name rotate 117 | * @methodOf Matrix# 118 | * @see Matrix.rotation 119 | * 120 | * @param {Number} theta Amount to rotate in radians. 121 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 122 | * @returns A new matrix, rotated by the specified amount. 123 | * @type Matrix 124 | */ 125 | rotate: function(theta, aboutPoint) { 126 | return this.concat(Matrix.rotation(theta, aboutPoint)); 127 | }, 128 | 129 | /** 130 | * Returns a new matrix that corresponds this matrix multiplied by a 131 | * a scaling matrix. 132 | * @name scale 133 | * @methodOf Matrix# 134 | * @see Matrix.scale 135 | * 136 | * @param {Number} sx 137 | * @param {Number} [sy] 138 | * @param {Point} [aboutPoint] The point that remains fixed during the scaling 139 | * @type Matrix 140 | */ 141 | scale: function(sx, sy, aboutPoint) { 142 | return this.concat(Matrix.scale(sx, sy, aboutPoint)); 143 | }, 144 | 145 | /** 146 | * Returns the result of applying the geometric transformation represented by the 147 | * Matrix object to the specified point. 148 | * @name transformPoint 149 | * @methodOf Matrix# 150 | * @see #deltaTransformPoint 151 | * 152 | * @returns A new point with the transformation applied. 153 | * @type Point 154 | */ 155 | transformPoint: function(point) { 156 | return new Point( 157 | this.a * point.x + this.c * point.y + this.tx, 158 | this.b * point.x + this.d * point.y + this.ty 159 | ); 160 | }, 161 | 162 | /** 163 | * Translates the matrix along the x and y axes, as specified by the tx and ty parameters. 164 | * @name translate 165 | * @methodOf Matrix# 166 | * @see Matrix.translation 167 | * 168 | * @param {Number} tx The translation along the x axis. 169 | * @param {Number} ty The translation along the y axis. 170 | * @returns A new matrix with the translation applied. 171 | * @type Matrix 172 | */ 173 | translate: function(tx, ty) { 174 | return this.concat(Matrix.translation(tx, ty)); 175 | } 176 | }; 177 | 178 | /** 179 | * Creates a matrix transformation that corresponds to the given rotation, 180 | * around (0,0) or the specified point. 181 | * @see Matrix#rotate 182 | * 183 | * @param {Number} theta Rotation in radians. 184 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 185 | * @returns 186 | * @type Matrix 187 | */ 188 | Matrix.rotation = function(theta, aboutPoint) { 189 | var rotationMatrix = new Matrix( 190 | Math.cos(theta), 191 | Math.sin(theta), 192 | -Math.sin(theta), 193 | Math.cos(theta) 194 | ); 195 | 196 | if(aboutPoint) { 197 | rotationMatrix = 198 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 199 | rotationMatrix 200 | ).concat( 201 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 202 | ); 203 | } 204 | 205 | return rotationMatrix; 206 | }; 207 | 208 | /** 209 | * Returns a matrix that corresponds to scaling by factors of sx, sy along 210 | * the x and y axis respectively. 211 | * If only one parameter is given the matrix is scaled uniformly along both axis. 212 | * If the optional aboutPoint parameter is given the scaling takes place 213 | * about the given point. 214 | * @see Matrix#scale 215 | * 216 | * @param {Number} sx The amount to scale by along the x axis or uniformly if no sy is given. 217 | * @param {Number} [sy] The amount to scale by along the y axis. 218 | * @param {Point} [aboutPoint] The point about which the scaling occurs. Defaults to (0,0). 219 | * @returns A matrix transformation representing scaling by sx and sy. 220 | * @type Matrix 221 | */ 222 | Matrix.scale = function(sx, sy, aboutPoint) { 223 | sy = sy || sx; 224 | 225 | var scaleMatrix = new Matrix(sx, 0, 0, sy); 226 | 227 | if(aboutPoint) { 228 | scaleMatrix = 229 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 230 | scaleMatrix 231 | ).concat( 232 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 233 | ); 234 | } 235 | 236 | return scaleMatrix; 237 | }; 238 | 239 | /** 240 | * Returns a matrix that corresponds to a translation of tx, ty. 241 | * @see Matrix#translate 242 | * 243 | * @param {Number} tx The amount to translate in the x direction. 244 | * @param {Number} ty The amount to translate in the y direction. 245 | * @return A matrix transformation representing a translation by tx and ty. 246 | * @type Matrix 247 | */ 248 | Matrix.translation = function(tx, ty) { 249 | return new Matrix(1, 0, 0, 1, tx, ty); 250 | }; 251 | 252 | /** 253 | * A constant representing the identity matrix. 254 | * @name IDENTITY 255 | * @fieldOf Matrix 256 | */ 257 | Matrix.IDENTITY = new Matrix(); 258 | /** 259 | * A constant representing the horizontal flip transformation matrix. 260 | * @name HORIZONTAL_FLIP 261 | * @fieldOf Matrix 262 | */ 263 | Matrix.HORIZONTAL_FLIP = new Matrix(-1, 0, 0, 1); 264 | /** 265 | * A constant representing the vertical flip transformation matrix. 266 | * @name VERTICAL_FLIP 267 | * @fieldOf Matrix 268 | */ 269 | Matrix.VERTICAL_FLIP = new Matrix(1, 0, 0, -1); 270 | -------------------------------------------------------------------------------- /lib/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | // 18 | // This is a drop-in replacement for (most) TrackballControls used in examples. 19 | // That is, include this js file and wherever you see: 20 | // controls = new THREE.TrackballControls( camera ); 21 | // controls.target.z = 150; 22 | // Simple substitute "OrbitControls" and the control should work as-is. 23 | 24 | THREE.OrbitControls = function ( object, domElement ) { 25 | 26 | this.object = object; 27 | this.domElement = ( domElement !== undefined ) ? domElement : document; 28 | 29 | // API 30 | 31 | // Set to false to disable this control 32 | this.enabled = true; 33 | 34 | // "target" sets the location of focus, where the control orbits around 35 | // and where it pans with respect to. 36 | this.target = new THREE.Vector3(); 37 | 38 | // center is old, deprecated; use "target" instead 39 | this.center = this.target; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for 42 | // backwards compatibility 43 | this.noZoom = false; 44 | this.zoomSpeed = 1.0; 45 | 46 | // Limits to how far you can dolly in and out 47 | this.minDistance = 0; 48 | this.maxDistance = Infinity; 49 | 50 | // Set to true to disable this control 51 | this.noRotate = false; 52 | this.rotateSpeed = 1.0; 53 | 54 | // Set to true to disable this control 55 | this.noPan = false; 56 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 57 | 58 | // Set to true to automatically rotate around the target 59 | this.autoRotate = false; 60 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 61 | 62 | // How far you can orbit vertically, upper and lower limits. 63 | // Range is 0 to Math.PI radians. 64 | this.minPolarAngle = 0; // radians 65 | this.maxPolarAngle = Math.PI; // radians 66 | 67 | // Set to true to disable use of the keys 68 | this.noKeys = false; 69 | 70 | // The four arrow keys 71 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 72 | 73 | //////////// 74 | // internals 75 | 76 | var scope = this; 77 | 78 | var EPS = 0.000001; 79 | 80 | var rotateStart = new THREE.Vector2(); 81 | var rotateEnd = new THREE.Vector2(); 82 | var rotateDelta = new THREE.Vector2(); 83 | 84 | var panStart = new THREE.Vector2(); 85 | var panEnd = new THREE.Vector2(); 86 | var panDelta = new THREE.Vector2(); 87 | var panOffset = new THREE.Vector3(); 88 | 89 | var offset = new THREE.Vector3(); 90 | 91 | var dollyStart = new THREE.Vector2(); 92 | var dollyEnd = new THREE.Vector2(); 93 | var dollyDelta = new THREE.Vector2(); 94 | 95 | var phiDelta = 0; 96 | var thetaDelta = 0; 97 | var scale = 1; 98 | var pan = new THREE.Vector3(); 99 | 100 | var lastPosition = new THREE.Vector3(); 101 | 102 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 103 | 104 | var state = STATE.NONE; 105 | 106 | // for reset 107 | 108 | this.target0 = this.target.clone(); 109 | this.position0 = this.object.position.clone(); 110 | 111 | // so camera.up is the orbit axis 112 | 113 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 114 | var quatInverse = quat.clone().inverse(); 115 | 116 | // events 117 | 118 | var changeEvent = { type: 'change' }; 119 | var startEvent = { type: 'start'}; 120 | var endEvent = { type: 'end'}; 121 | 122 | this.rotateLeft = function ( angle ) { 123 | 124 | if ( angle === undefined ) { 125 | 126 | angle = getAutoRotationAngle(); 127 | 128 | } 129 | 130 | thetaDelta -= angle; 131 | 132 | }; 133 | 134 | this.rotateUp = function ( angle ) { 135 | 136 | if ( angle === undefined ) { 137 | 138 | angle = getAutoRotationAngle(); 139 | 140 | } 141 | 142 | phiDelta -= angle; 143 | 144 | }; 145 | 146 | // pass in distance in world space to move left 147 | this.panLeft = function ( distance ) { 148 | 149 | var te = this.object.matrix.elements; 150 | 151 | // get X column of matrix 152 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 153 | panOffset.multiplyScalar( - distance ); 154 | 155 | pan.add( panOffset ); 156 | 157 | }; 158 | 159 | // pass in distance in world space to move up 160 | this.panUp = function ( distance ) { 161 | 162 | var te = this.object.matrix.elements; 163 | 164 | // get Y column of matrix 165 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 166 | panOffset.multiplyScalar( distance ); 167 | 168 | pan.add( panOffset ); 169 | 170 | }; 171 | 172 | // pass in x,y of change desired in pixel space, 173 | // right and down are positive 174 | this.pan = function ( deltaX, deltaY ) { 175 | 176 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 177 | 178 | if ( scope.object.fov !== undefined ) { 179 | 180 | // perspective 181 | var position = scope.object.position; 182 | var offset = position.clone().sub( scope.target ); 183 | var targetDistance = offset.length(); 184 | 185 | // half of the fov is center to top of screen 186 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 187 | 188 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 189 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 190 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 191 | 192 | } else if ( scope.object.top !== undefined ) { 193 | 194 | // orthographic 195 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 196 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 197 | 198 | } else { 199 | 200 | // camera neither orthographic or perspective 201 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 202 | 203 | } 204 | 205 | }; 206 | 207 | this.dollyIn = function ( dollyScale ) { 208 | 209 | if ( dollyScale === undefined ) { 210 | 211 | dollyScale = getZoomScale(); 212 | 213 | } 214 | 215 | scale /= dollyScale; 216 | 217 | }; 218 | 219 | this.dollyOut = function ( dollyScale ) { 220 | 221 | if ( dollyScale === undefined ) { 222 | 223 | dollyScale = getZoomScale(); 224 | 225 | } 226 | 227 | scale *= dollyScale; 228 | 229 | }; 230 | 231 | this.update = function () { 232 | 233 | var position = this.object.position; 234 | 235 | offset.copy( position ).sub( this.target ); 236 | 237 | // rotate offset to "y-axis-is-up" space 238 | offset.applyQuaternion( quat ); 239 | 240 | // angle from z-axis around y-axis 241 | 242 | var theta = Math.atan2( offset.x, offset.z ); 243 | 244 | // angle from y-axis 245 | 246 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 247 | 248 | if ( this.autoRotate ) { 249 | 250 | this.rotateLeft( getAutoRotationAngle() ); 251 | 252 | } 253 | 254 | theta += thetaDelta; 255 | phi += phiDelta; 256 | 257 | // restrict phi to be between desired limits 258 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 259 | 260 | // restrict phi to be betwee EPS and PI-EPS 261 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 262 | 263 | var radius = offset.length() * scale; 264 | 265 | // restrict radius to be between desired limits 266 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 267 | 268 | // move target to panned location 269 | this.target.add( pan ); 270 | 271 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 272 | offset.y = radius * Math.cos( phi ); 273 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 274 | 275 | // rotate offset back to "camera-up-vector-is-up" space 276 | offset.applyQuaternion( quatInverse ); 277 | 278 | position.copy( this.target ).add( offset ); 279 | 280 | this.object.lookAt( this.target ); 281 | 282 | thetaDelta = 0; 283 | phiDelta = 0; 284 | scale = 1; 285 | pan.set( 0, 0, 0 ); 286 | 287 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS ) { 288 | 289 | this.dispatchEvent( changeEvent ); 290 | 291 | lastPosition.copy( this.object.position ); 292 | 293 | } 294 | 295 | }; 296 | 297 | 298 | this.reset = function () { 299 | 300 | state = STATE.NONE; 301 | 302 | this.target.copy( this.target0 ); 303 | this.object.position.copy( this.position0 ); 304 | 305 | this.update(); 306 | 307 | }; 308 | 309 | function getAutoRotationAngle() { 310 | 311 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 312 | 313 | } 314 | 315 | function getZoomScale() { 316 | 317 | return Math.pow( 0.95, scope.zoomSpeed ); 318 | 319 | } 320 | 321 | function onMouseDown( event ) { 322 | 323 | if ( scope.enabled === false ) return; 324 | event.preventDefault(); 325 | 326 | if ( event.button === 0 ) { 327 | if ( scope.noRotate === true ) return; 328 | 329 | state = STATE.ROTATE; 330 | 331 | rotateStart.set( event.clientX, event.clientY ); 332 | 333 | } else if ( event.button === 1 ) { 334 | if ( scope.noZoom === true ) return; 335 | 336 | state = STATE.DOLLY; 337 | 338 | dollyStart.set( event.clientX, event.clientY ); 339 | 340 | } else if ( event.button === 2 ) { 341 | if ( scope.noPan === true ) return; 342 | 343 | state = STATE.PAN; 344 | 345 | panStart.set( event.clientX, event.clientY ); 346 | 347 | } 348 | 349 | scope.domElement.addEventListener( 'mousemove', onMouseMove, false ); 350 | scope.domElement.addEventListener( 'mouseup', onMouseUp, false ); 351 | scope.dispatchEvent( startEvent ); 352 | 353 | } 354 | 355 | function onMouseMove( event ) { 356 | 357 | if ( scope.enabled === false ) return; 358 | 359 | event.preventDefault(); 360 | 361 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 362 | 363 | if ( state === STATE.ROTATE ) { 364 | 365 | if ( scope.noRotate === true ) return; 366 | 367 | rotateEnd.set( event.clientX, event.clientY ); 368 | rotateDelta.subVectors( rotateEnd, rotateStart ); 369 | 370 | // rotating across whole screen goes 360 degrees around 371 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 372 | 373 | // rotating up and down along whole screen attempts to go 360, but limited to 180 374 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 375 | 376 | rotateStart.copy( rotateEnd ); 377 | 378 | } else if ( state === STATE.DOLLY ) { 379 | 380 | if ( scope.noZoom === true ) return; 381 | 382 | dollyEnd.set( event.clientX, event.clientY ); 383 | dollyDelta.subVectors( dollyEnd, dollyStart ); 384 | 385 | if ( dollyDelta.y > 0 ) { 386 | 387 | scope.dollyIn(); 388 | 389 | } else { 390 | 391 | scope.dollyOut(); 392 | 393 | } 394 | 395 | dollyStart.copy( dollyEnd ); 396 | 397 | } else if ( state === STATE.PAN ) { 398 | 399 | if ( scope.noPan === true ) return; 400 | 401 | panEnd.set( event.clientX, event.clientY ); 402 | panDelta.subVectors( panEnd, panStart ); 403 | 404 | scope.pan( panDelta.x, panDelta.y ); 405 | 406 | panStart.copy( panEnd ); 407 | 408 | } 409 | 410 | scope.update(); 411 | 412 | } 413 | 414 | function onMouseUp( /* event */ ) { 415 | 416 | if ( scope.enabled === false ) return; 417 | 418 | scope.domElement.removeEventListener( 'mousemove', onMouseMove, false ); 419 | scope.domElement.removeEventListener( 'mouseup', onMouseUp, false ); 420 | scope.dispatchEvent( endEvent ); 421 | state = STATE.NONE; 422 | 423 | } 424 | 425 | function onMouseWheel( event ) { 426 | 427 | if ( scope.enabled === false || scope.noZoom === true ) return; 428 | 429 | event.preventDefault(); 430 | event.stopPropagation(); 431 | 432 | var delta = 0; 433 | 434 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 435 | 436 | delta = event.wheelDelta; 437 | 438 | } else if ( event.detail !== undefined ) { // Firefox 439 | 440 | delta = - event.detail; 441 | 442 | } 443 | 444 | if ( delta > 0 ) { 445 | 446 | scope.dollyOut(); 447 | 448 | } else { 449 | 450 | scope.dollyIn(); 451 | 452 | } 453 | 454 | scope.update(); 455 | scope.dispatchEvent( startEvent ); 456 | scope.dispatchEvent( endEvent ); 457 | 458 | } 459 | 460 | function onKeyDown( event ) { 461 | 462 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 463 | 464 | switch ( event.keyCode ) { 465 | 466 | case scope.keys.UP: 467 | scope.pan( 0, scope.keyPanSpeed ); 468 | scope.update(); 469 | break; 470 | 471 | case scope.keys.BOTTOM: 472 | scope.pan( 0, - scope.keyPanSpeed ); 473 | scope.update(); 474 | break; 475 | 476 | case scope.keys.LEFT: 477 | scope.pan( scope.keyPanSpeed, 0 ); 478 | scope.update(); 479 | break; 480 | 481 | case scope.keys.RIGHT: 482 | scope.pan( - scope.keyPanSpeed, 0 ); 483 | scope.update(); 484 | break; 485 | 486 | } 487 | 488 | } 489 | 490 | function touchstart( event ) { 491 | 492 | if ( scope.enabled === false ) return; 493 | 494 | switch ( event.touches.length ) { 495 | 496 | case 1: // one-fingered touch: rotate 497 | 498 | if ( scope.noRotate === true ) return; 499 | 500 | state = STATE.TOUCH_ROTATE; 501 | 502 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 503 | break; 504 | 505 | case 2: // two-fingered touch: dolly 506 | 507 | if ( scope.noZoom === true ) return; 508 | 509 | state = STATE.TOUCH_DOLLY; 510 | 511 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 512 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 513 | var distance = Math.sqrt( dx * dx + dy * dy ); 514 | dollyStart.set( 0, distance ); 515 | break; 516 | 517 | case 3: // three-fingered touch: pan 518 | 519 | if ( scope.noPan === true ) return; 520 | 521 | state = STATE.TOUCH_PAN; 522 | 523 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 524 | break; 525 | 526 | default: 527 | 528 | state = STATE.NONE; 529 | 530 | } 531 | 532 | scope.dispatchEvent( startEvent ); 533 | 534 | } 535 | 536 | function touchmove( event ) { 537 | 538 | if ( scope.enabled === false ) return; 539 | 540 | event.preventDefault(); 541 | event.stopPropagation(); 542 | 543 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 544 | 545 | switch ( event.touches.length ) { 546 | 547 | case 1: // one-fingered touch: rotate 548 | 549 | if ( scope.noRotate === true ) return; 550 | if ( state !== STATE.TOUCH_ROTATE ) return; 551 | 552 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 553 | rotateDelta.subVectors( rotateEnd, rotateStart ); 554 | 555 | // rotating across whole screen goes 360 degrees around 556 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 557 | // rotating up and down along whole screen attempts to go 360, but limited to 180 558 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 559 | 560 | rotateStart.copy( rotateEnd ); 561 | 562 | scope.update(); 563 | break; 564 | 565 | case 2: // two-fingered touch: dolly 566 | 567 | if ( scope.noZoom === true ) return; 568 | if ( state !== STATE.TOUCH_DOLLY ) return; 569 | 570 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 571 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 572 | var distance = Math.sqrt( dx * dx + dy * dy ); 573 | 574 | dollyEnd.set( 0, distance ); 575 | dollyDelta.subVectors( dollyEnd, dollyStart ); 576 | 577 | if ( dollyDelta.y > 0 ) { 578 | 579 | scope.dollyOut(); 580 | 581 | } else { 582 | 583 | scope.dollyIn(); 584 | 585 | } 586 | 587 | dollyStart.copy( dollyEnd ); 588 | 589 | scope.update(); 590 | break; 591 | 592 | case 3: // three-fingered touch: pan 593 | 594 | if ( scope.noPan === true ) return; 595 | if ( state !== STATE.TOUCH_PAN ) return; 596 | 597 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 598 | panDelta.subVectors( panEnd, panStart ); 599 | 600 | scope.pan( panDelta.x, panDelta.y ); 601 | 602 | panStart.copy( panEnd ); 603 | 604 | scope.update(); 605 | break; 606 | 607 | default: 608 | 609 | state = STATE.NONE; 610 | 611 | } 612 | 613 | } 614 | 615 | function touchend( /* event */ ) { 616 | 617 | if ( scope.enabled === false ) return; 618 | 619 | scope.dispatchEvent( endEvent ); 620 | state = STATE.NONE; 621 | 622 | } 623 | 624 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 625 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 626 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 627 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 628 | 629 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 630 | this.domElement.addEventListener( 'touchend', touchend, false ); 631 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 632 | 633 | window.addEventListener( 'keydown', onKeyDown, false ); 634 | 635 | // force an update at start 636 | this.update(); 637 | 638 | }; 639 | 640 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 641 | --------------------------------------------------------------------------------