├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── public ├── favicon.ico └── index.html ├── rhino-viewer-2.gif ├── rhino-viewer.gif ├── src ├── App.vue ├── assets │ └── scss │ │ ├── loader.scss │ │ └── main.scss ├── components │ ├── Footer.vue │ ├── Loader.vue │ └── Scene.vue ├── main.js ├── rhinoService.js └── three │ ├── colors.js │ ├── encoders.js │ ├── environment.js │ ├── helpers.js │ ├── three.js │ └── vendor │ └── OrbitControls.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | src/three/vendor/OrbitControls.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | rhino3dm: false, 4 | Rhino3dm: false, 5 | RhinoCompute: false, 6 | }, 7 | 8 | root: true, 9 | 10 | env: { 11 | node: true 12 | }, 13 | 14 | extends: [ 15 | "plugin:vue/essential", 16 | "eslint:recommended", 17 | '@vue/standard' 18 | ], 19 | 20 | rules: { 21 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 23 | 'comma-dangle': ['error', 'only-multiline'] 24 | }, 25 | 26 | parserOptions: { 27 | parser: 'babel-eslint' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gui Talarico 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue ThreeJS Rhino Viewer Demo 2 | 3 | This is a demo project to test Rhino3dm & Compute integration with Three JS. 4 | I created this project to test and learn how these new technologies were designed to work, 5 | and to generate boiler plate code for use in future projects. 6 | 7 | It uses [resources and samples provided by McNeel here](https://github.com/mcneel/rhino3dm/blob/master/samples/javascript/rhinologo.html) for the basic setup, 8 | but also build on it with more reusable modules and new JS syntax. 9 | 10 | The application bootstrapping and [loading sequence of dependencies](https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src/rhinoService.js#L7) in the [context of a Vue application](https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src/components/Scene.vue#L49) was a bit tricky but seems to be working well. 11 | 12 | [**Live Demo**](https://vue-threejs-rhino-viewer.netlify.com/) 13 | (Requires Rhino Compute Token, get yours [here](https://www.rhino3d.com/compute/login)) 14 | 15 | ### What's included 16 | * A minimal VueJs project setup using Vue CLI 17 | * My preferred setup for [loading ThreeJS into a Vue project](https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src/three) and [setting up a configurable Scene ](https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src/three/environment.js) 18 | * A [RhinoService module](https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src/rhinoService.js) for making handling auth, Rhino Compute calls, and converting objects to a renderable object (eg. Brep to Meshes) 19 | * Samples [ThreeJs encoders]((https://github.com/gtalarico/vue-threejs-rhino-demo/blob/master/src//three/encoders.js)) to convert renderable Rhino Objects into ThreeJs equivalents (eg. Meshes, NurbsCurve, LineCurve, etc) 20 | 21 | ![demo-screenshot](https://github.com/gtalarico/vue-threejs-rhino-demo/raw/master/rhino-viewer.gif) 22 | ![demo-screenshot-2](https://github.com/gtalarico/vue-threejs-rhino-demo/raw/master/rhino-viewer-2.gif) 23 | 24 | 25 | ### Contributing 26 | 27 | Contributions of any kind are welcome, including UI improvement, additional encoders, application setup, etc 28 | 29 | ## Local Development 30 | ``` 31 | $ yarn install 32 | $ yarn run serve 33 | ``` 34 | 35 | **License MIT** 36 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowJs":true, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-threejs-rhino", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.0", 12 | "core-js": "^2.6.5", 13 | "milligram-scss": "^1.3.0", 14 | "three": "^0.108.0", 15 | "vue": "^2.6.10" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.11.0", 19 | "@vue/cli-plugin-eslint": "^3.11.0", 20 | "@vue/cli-service": "^3.11.0", 21 | "@vue/eslint-config-standard": "^4.0.0", 22 | "babel-eslint": "^10.0.1", 23 | "eslint": "^5.16.0", 24 | "eslint-plugin-vue": "^5.0.0", 25 | "node-sass": "^4.12.0", 26 | "sass-loader": "^8.0.0", 27 | "vue-template-compiler": "^2.6.10" 28 | }, 29 | "postcss": { 30 | "plugins": { 31 | "autoprefixer": {} 32 | } 33 | }, 34 | "browserslist": [ 35 | "> 1%", 36 | "last 2 versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/vue-threejs-rhino-demo/c85a1df01fa6cf0e8509eb55e726109bbd78bef9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | vue-three-scene 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rhino-viewer-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/vue-threejs-rhino-demo/c85a1df01fa6cf0e8509eb55e726109bbd78bef9/rhino-viewer-2.gif -------------------------------------------------------------------------------- /rhino-viewer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtalarico/vue-threejs-rhino-demo/c85a1df01fa6cf0e8509eb55e726109bbd78bef9/rhino-viewer.gif -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 41 | 42 | 66 | -------------------------------------------------------------------------------- /src/assets/scss/loader.scss: -------------------------------------------------------------------------------- 1 | .lds-dual-ring { 2 | display: inline-block; 3 | width: 64px; 4 | height: 64px; 5 | } 6 | .lds-dual-ring:after { 7 | content: " "; 8 | display: block; 9 | width: 46px; 10 | height: 46px; 11 | margin: 1px; 12 | border-radius: 50%; 13 | border: 5px solid #666; 14 | border-color: #666 transparent #666 transparent; 15 | animation: lds-dual-ring 1.2s linear infinite; 16 | } 17 | @keyframes lds-dual-ring { 18 | 0% { 19 | transform: rotate(0deg); 20 | } 21 | 100% { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'loader.scss'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .dark { 9 | color: #aaa; 10 | input { 11 | color: #aaa 12 | } 13 | } 14 | 15 | $color-primary: #f00b42; 16 | 17 | 18 | @import '~milligram-scss'; 19 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 57 | -------------------------------------------------------------------------------- /src/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 40 | -------------------------------------------------------------------------------- /src/components/Scene.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 94 | 95 | 98 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /src/rhinoService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default function RhinoService () { 4 | this.token = this.getToken() 5 | } 6 | 7 | RhinoService.prototype.init = function () { 8 | return new Promise((resolve, reject) => { 9 | const scripts = [ 10 | 'https://files.mcneel.com/rhino3dm/js/latest/rhino3dm.js', 11 | 'https://files.mcneel.com/rhino3dm/js/latest/compute.rhino3d.js', 12 | ] 13 | let loaded = 0 14 | 15 | for (let url of scripts) { 16 | let script = document.createElement('script') 17 | script.src = url 18 | document.head.appendChild(script) 19 | script.onerror = (err) => { 20 | return reject(err) 21 | } 22 | script.onload = () => { 23 | loaded++ 24 | if (loaded === scripts.length) { 25 | window.RhinoCompute.authToken = `Bearer ${this.getToken()}` 26 | window.rhino3dm() 27 | .then(module => { 28 | window.Rhino3dm = module 29 | resolve() 30 | }) 31 | } 32 | } 33 | } 34 | }) 35 | } 36 | 37 | RhinoService.prototype.getToken = function () { 38 | let token = localStorage.getItem('computeToken') 39 | if (token) return token 40 | window.open('https://www.rhino3d.com/compute/login') 41 | token = prompt('Rhino Compute Token') 42 | this.setToken(token) 43 | return token 44 | } 45 | 46 | RhinoService.prototype.setToken = function (token) { 47 | return localStorage.setItem('computeToken', token) 48 | } 49 | 50 | RhinoService.prototype.loadFileFromUrl = async function (url) { 51 | let response 52 | try { 53 | response = await axios.get(url, { responseType: 'arraybuffer' }) 54 | } catch (e) { 55 | throw Error('could not fetch url') 56 | } 57 | const longInt8View = new Uint8Array(response.data) 58 | const file = Rhino3dm.File3dm.fromByteArray(longInt8View) 59 | return new RhinoDoc(file) 60 | } 61 | 62 | function RhinoDoc (file) { 63 | this.file = file 64 | this.objects = [] 65 | const objectsTable = file.objects() 66 | for (let i = 0; i < objectsTable.count; i++) { 67 | this.objects.push(new RhinoObject(objectsTable.get(i))) 68 | } 69 | } 70 | 71 | function RhinoObject (modelObject) { 72 | this.object = modelObject 73 | this.geometry = modelObject.geometry() 74 | this.attributes = modelObject.attributes() 75 | } 76 | 77 | RhinoObject.prototype.toRenderable = async function () { 78 | const rhinoObjecTypeName = this.geometry.constructor.name 79 | const converter = converters[rhinoObjecTypeName] 80 | if (!converter) { 81 | console.warn(`no renderable obj for ${rhinoObjecTypeName}`) 82 | return null 83 | } 84 | let rhinoOBjects 85 | try { 86 | rhinoOBjects = await converter(this.geometry) 87 | } catch (e) { 88 | console.warn('Rhino Compute request failed') 89 | localStorage.removeItem('computeToken') 90 | throw Error('Rhino Compute request failed') 91 | } 92 | return rhinoOBjects 93 | } 94 | 95 | // Convert Rhino Objects into types that can be encoded into ThreeJs Objects 96 | const converters = { 97 | 'Brep': async function (brep) { 98 | const rhinoMeshes = await RhinoCompute.Mesh.createFromBrep(brep) 99 | const rhinoWireframes = await RhinoCompute.Brep.getWireframe(brep, 1) 100 | return rhinoMeshes 101 | .concat(rhinoWireframes) 102 | .map(r => Rhino3dm.CommonObject.decode(r)) 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /src/three/colors.js: -------------------------------------------------------------------------------- 1 | import { THREE } from '@/three/three.js' 2 | 3 | const Colors = { 4 | black: new THREE.Color('#333333'), 5 | offWhite: new THREE.Color('#f0f0f0'), 6 | white: new THREE.Color('#ffffff'), 7 | lightGray: new THREE.Color('#eaeaea'), 8 | gray: new THREE.Color('#cacaca'), 9 | darkGray: new THREE.Color('#aaaaaa'), 10 | } 11 | 12 | const Materials = { 13 | black: new THREE.MeshBasicMaterial({ color: Colors.black }), 14 | white: new THREE.MeshBasicMaterial({ color: Colors.white }), 15 | lightGray: new THREE.MeshBasicMaterial({ color: Colors.lightGray }), 16 | gray: new THREE.MeshBasicMaterial({ color: Colors.gray }), 17 | darkGray: new THREE.MeshBasicMaterial({ color: Colors.darkGray }), 18 | } 19 | 20 | export { Colors, Materials } 21 | -------------------------------------------------------------------------------- /src/three/encoders.js: -------------------------------------------------------------------------------- 1 | import { THREE } from '@/three/three' 2 | 3 | export default function (rhinoObject) { 4 | const rhinoObjTypeName = rhinoObject.constructor.name 5 | let encoderFunc = encoders[rhinoObjTypeName] 6 | if (!encoderFunc) { 7 | console.warn(`no three js encoder for ${rhinoObjTypeName}`) 8 | return null 9 | } 10 | return encoderFunc(rhinoObject) 11 | } 12 | 13 | const materials = { 14 | line: new THREE.LineBasicMaterial({ color: 0x000000 }), 15 | wire: new THREE.LineBasicMaterial({ color: 0x000000 }), 16 | mesh: new THREE.MeshPhongMaterial({ color: 0xffffff }), 17 | // mesh: new THREE.MeshBasicMaterial({color: 0xe0e0e0}), 18 | } 19 | 20 | const encoders = { 21 | 'LineCurve': (rhinoLineCurve) => { 22 | const pt1 = rhinoLineCurve.pointAtStart 23 | const pt2 = rhinoLineCurve.pointAtEnd 24 | const geometry = new THREE.Geometry() 25 | geometry.vertices.push(new THREE.Vector3(...pt1)) 26 | geometry.vertices.push(new THREE.Vector3(...pt2)) 27 | return new THREE.Line(geometry, materials.line) 28 | }, 29 | 'NurbsCurve': (rhinoCurve) => { 30 | const geometry = new THREE.Geometry() 31 | const domain = rhinoCurve.domain 32 | const start = domain[0] 33 | const range = domain[1] - domain[0] 34 | var interval = range / 50.0 35 | for (var i = 0; i < 51; i++) { 36 | let t = start + i * interval 37 | let pt = rhinoCurve.pointAt(t) 38 | geometry.vertices.push(new THREE.Vector3(pt[0], pt[1], pt[2])) 39 | } 40 | return new THREE.Line(geometry, materials.wire) 41 | }, 42 | 'Mesh': (rhinoMesh) => { 43 | const geometry = new THREE.BufferGeometry() 44 | const vertices = rhinoMesh.vertices() 45 | const faces = rhinoMesh.faces() 46 | const normals = rhinoMesh.normals() 47 | 48 | var vertexbuffer = new Float32Array(3 * vertices.count) 49 | for (let i = 0; i < vertices.count; i++) { 50 | let pt = vertices.get(i) 51 | vertexbuffer[i * 3] = pt[0] 52 | vertexbuffer[i * 3 + 1] = pt[1] 53 | vertexbuffer[i * 3 + 2] = pt[2] 54 | } 55 | geometry.addAttribute('position', new THREE.BufferAttribute(vertexbuffer, 3)) 56 | const indices = [] 57 | 58 | for (let i = 0; i < faces.count; i++) { 59 | const face = faces.get(i) 60 | indices.push(face[0], face[1], face[2]) 61 | if (face[2] !== face[3]) { 62 | indices.push(face[2], face[3], face[0]) 63 | } 64 | } 65 | geometry.setIndex(indices) 66 | 67 | var normalBuffer = new Float32Array(3 * normals.count) 68 | for (let i = 0; i < normals.count; i++) { 69 | const pt = normals.get(i) 70 | normalBuffer[i * 3] = pt[0] 71 | normalBuffer[i * 3 + 1] = pt[1] 72 | normalBuffer[i * 3 + 2] = pt[1] 73 | } 74 | geometry.addAttribute('normal', new THREE.BufferAttribute(normalBuffer, 3)) 75 | return new THREE.Mesh(geometry, materials.mesh) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/three/environment.js: -------------------------------------------------------------------------------- 1 | import { THREE } from '@/three/three' 2 | import { Colors } from '@/three/colors' 3 | import helper from '@/three/helpers' 4 | 5 | export default function Environment (options) { 6 | window.addEventListener('resize', this.handleResize.bind(this), false) 7 | 8 | const defaultOptions = { 9 | background: Colors.offWhite, 10 | axis: true, 11 | sceneSize: 500, 12 | axisSize: 100, 13 | grid: { 14 | show: true, 15 | size: 2000, 16 | cellSize: 10, 17 | major: 5, 18 | } 19 | } 20 | const envOptions = Object.assign(defaultOptions, options) 21 | 22 | this.container = document.getElementById('three-container') 23 | let canvasWidth = this.container.offsetWidth 24 | let canvasHeight = this.container.offsetHeight 25 | 26 | this.renderer = new THREE.WebGLRenderer({ antialias: true }) 27 | this.renderer.setPixelRatio(window.devicePixelRatio) 28 | this.renderer.setSize(canvasWidth, canvasHeight) 29 | this.container.appendChild(this.renderer.domElement) 30 | 31 | const cameraDistance = envOptions.sceneSize * 1 32 | this.camera = new THREE.PerspectiveCamera(45, canvasWidth / canvasHeight, 1, envOptions.sceneSize * 10) 33 | this.camera.position.set(cameraDistance, cameraDistance, cameraDistance / 2) 34 | this.camera.up.set(0, 0, 1) 35 | this.camera.lookAt(new THREE.Vector3(0, 0, 0)) 36 | 37 | this.controls = new THREE.OrbitControls(this.camera, this.container) 38 | this.controls.enableDamping = true 39 | this.controls.dampingFactor = 0.3 40 | this.controls.minDistance = envOptions.sceneSize / 5 41 | this.controls.maxDistance = envOptions.sceneSize 42 | 43 | this.raycaster = new THREE.Raycaster() 44 | this.mouse = new THREE.Vector2() 45 | this.raycaster.setFromCamera(this.mouse, this.camera) 46 | 47 | this.scene = new THREE.Scene() 48 | this.scene.background = envOptions.background 49 | this.scene.fog = new THREE.Fog(envOptions.background, 1, envOptions.sceneSize * 5) 50 | 51 | // Temporary - Generic Light 52 | let light = new THREE.DirectionalLight(0xffffff) 53 | light.position.set(0, 0, 1) 54 | this.scene.add(light) 55 | var light2 = new THREE.DirectionalLight(0xaaaaaa) 56 | light2.position.set(-1, -1, 0) 57 | this.scene.add(light2) 58 | 59 | if (envOptions.axis) { 60 | this.scene.add(new THREE.AxesHelper(envOptions.axisSize)) 61 | } 62 | 63 | if (envOptions.grid.show) { 64 | const grid = helper.makeGrid(envOptions.grid.size, envOptions.grid.cellSize, envOptions.grid.major, Colors.darkGray, Colors.lightGray) 65 | this.scene.add(grid) 66 | } 67 | 68 | // Setup flag native scene objects so we can keep them when wiping scene 69 | this.scene.children.map(o => { 70 | o.userData.isEnvironmentObject = true 71 | }) 72 | this.renderForever() 73 | } 74 | 75 | Environment.prototype.renderForever = function () { 76 | requestAnimationFrame(this.renderForever.bind(this)) 77 | this.controls.update() 78 | this.renderer.render(this.scene, this.camera) 79 | } 80 | 81 | Environment.prototype.clear = function () { 82 | this.scene.children = this.scene.children.filter(o => o.userData.isEnvironmentObject) 83 | } 84 | 85 | Environment.prototype.handleResize = function () { 86 | const canvasWidth = this.container.offsetWidth 87 | const canvasHeight = this.container.offsetHeight 88 | this.camera.aspect = canvasWidth / canvasHeight 89 | this.camera.updateProjectionMatrix() 90 | this.renderer.setSize(canvasWidth, canvasHeight) 91 | } 92 | 93 | Environment.prototype.tearDownScene = function () { 94 | window.removeEventListener('resize', this.handleResize) 95 | } 96 | -------------------------------------------------------------------------------- /src/three/helpers.js: -------------------------------------------------------------------------------- 1 | import { THREE } from '@/three/three.js' 2 | // import { Colors } from '@/three/colors.js' 3 | 4 | export default { 5 | 6 | makeGrid (gridSize, cellSize, majorInterval, colorMajor, colorMinor, centered = true) { 7 | let offset = centered ? gridSize / 2 : 0 8 | 9 | var geometry = new THREE.Geometry() 10 | geometry.vertices.push(new THREE.Vector3(0 - offset, 0, 0)) 11 | geometry.vertices.push(new THREE.Vector3(gridSize - offset, 0, 0)) 12 | let matLineMajor = new THREE.LineBasicMaterial({ color: colorMajor }) 13 | let matLineMinor = new THREE.LineBasicMaterial({ color: colorMinor }) 14 | 15 | let group = new THREE.Group() 16 | let currentSize = 0 17 | 18 | for (let i = 0; currentSize <= gridSize; i++) { 19 | currentSize = i * cellSize 20 | let color = i % majorInterval === 0 ? matLineMajor : matLineMinor 21 | let line = new THREE.Line(geometry, color) 22 | 23 | line.position.y = currentSize - offset 24 | group.add(line) 25 | 26 | let line2 = new THREE.Line(geometry, color) 27 | line2.rotation.z = Math.PI / 2 28 | line2.position.x = currentSize - offset 29 | group.add(line2) 30 | } 31 | return group 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /src/three/three.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | window.THREE = THREE 3 | require('@/three/vendor/OrbitControls.js') 4 | export { THREE } 5 | -------------------------------------------------------------------------------- /src/three/vendor/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 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one-finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 14 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.panSpeed = 1.0; 63 | this.screenSpacePanning = false; // if true, pan in screen-space 64 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 65 | 66 | // Set to true to automatically rotate around the target 67 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 68 | this.autoRotate = false; 69 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 70 | 71 | // Set to false to disable use of the keys 72 | this.enableKeys = true; 73 | 74 | // The four arrow keys 75 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 76 | 77 | // Mouse buttons 78 | this.mouseButtons = { LEFT: THREE.MOUSE.LEFT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: THREE.MOUSE.RIGHT }; 79 | 80 | // for reset 81 | this.target0 = this.target.clone(); 82 | this.position0 = this.object.position.clone(); 83 | this.zoom0 = this.object.zoom; 84 | 85 | // 86 | // public methods 87 | // 88 | 89 | this.getPolarAngle = function () { 90 | 91 | return spherical.phi; 92 | 93 | }; 94 | 95 | this.getAzimuthalAngle = function () { 96 | 97 | return spherical.theta; 98 | 99 | }; 100 | 101 | this.saveState = function () { 102 | 103 | scope.target0.copy( scope.target ); 104 | scope.position0.copy( scope.object.position ); 105 | scope.zoom0 = scope.object.zoom; 106 | 107 | }; 108 | 109 | this.reset = function () { 110 | 111 | scope.target.copy( scope.target0 ); 112 | scope.object.position.copy( scope.position0 ); 113 | scope.object.zoom = scope.zoom0; 114 | 115 | scope.object.updateProjectionMatrix(); 116 | scope.dispatchEvent( changeEvent ); 117 | 118 | scope.update(); 119 | 120 | state = STATE.NONE; 121 | 122 | }; 123 | 124 | // this method is exposed, but perhaps it would be better if we can make it private... 125 | this.update = function () { 126 | 127 | var offset = new THREE.Vector3(); 128 | 129 | // so camera.up is the orbit axis 130 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 131 | var quatInverse = quat.clone().inverse(); 132 | 133 | var lastPosition = new THREE.Vector3(); 134 | var lastQuaternion = new THREE.Quaternion(); 135 | 136 | return function update() { 137 | 138 | var position = scope.object.position; 139 | 140 | offset.copy( position ).sub( scope.target ); 141 | 142 | // rotate offset to "y-axis-is-up" space 143 | offset.applyQuaternion( quat ); 144 | 145 | // angle from z-axis around y-axis 146 | spherical.setFromVector3( offset ); 147 | 148 | if ( scope.autoRotate && state === STATE.NONE ) { 149 | 150 | rotateLeft( getAutoRotationAngle() ); 151 | 152 | } 153 | 154 | spherical.theta += sphericalDelta.theta; 155 | spherical.phi += sphericalDelta.phi; 156 | 157 | // restrict theta to be between desired limits 158 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 159 | 160 | // restrict phi to be between desired limits 161 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 162 | 163 | spherical.makeSafe(); 164 | 165 | 166 | spherical.radius *= scale; 167 | 168 | // restrict radius to be between desired limits 169 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 170 | 171 | // move target to panned location 172 | scope.target.add( panOffset ); 173 | 174 | offset.setFromSpherical( spherical ); 175 | 176 | // rotate offset back to "camera-up-vector-is-up" space 177 | offset.applyQuaternion( quatInverse ); 178 | 179 | position.copy( scope.target ).add( offset ); 180 | 181 | scope.object.lookAt( scope.target ); 182 | 183 | if ( scope.enableDamping === true ) { 184 | 185 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 186 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 187 | 188 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 189 | 190 | } else { 191 | 192 | sphericalDelta.set( 0, 0, 0 ); 193 | 194 | panOffset.set( 0, 0, 0 ); 195 | 196 | } 197 | 198 | scale = 1; 199 | 200 | // update condition is: 201 | // min(camera displacement, camera rotation in radians)^2 > EPS 202 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 203 | 204 | if ( zoomChanged || 205 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 206 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 207 | 208 | scope.dispatchEvent( changeEvent ); 209 | 210 | lastPosition.copy( scope.object.position ); 211 | lastQuaternion.copy( scope.object.quaternion ); 212 | zoomChanged = false; 213 | 214 | return true; 215 | 216 | } 217 | 218 | return false; 219 | 220 | }; 221 | 222 | }(); 223 | 224 | this.dispose = function () { 225 | 226 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 227 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 228 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 229 | 230 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 231 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 232 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 233 | 234 | document.removeEventListener( 'mousemove', onMouseMove, false ); 235 | document.removeEventListener( 'mouseup', onMouseUp, false ); 236 | 237 | window.removeEventListener( 'keydown', onKeyDown, false ); 238 | 239 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 240 | 241 | }; 242 | 243 | // 244 | // internals 245 | // 246 | 247 | var scope = this; 248 | 249 | var changeEvent = { type: 'change' }; 250 | var startEvent = { type: 'start' }; 251 | var endEvent = { type: 'end' }; 252 | 253 | var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4 }; 254 | 255 | var state = STATE.NONE; 256 | 257 | var EPS = 0.000001; 258 | 259 | // current position in spherical coordinates 260 | var spherical = new THREE.Spherical(); 261 | var sphericalDelta = new THREE.Spherical(); 262 | 263 | var scale = 1; 264 | var panOffset = new THREE.Vector3(); 265 | var zoomChanged = false; 266 | 267 | var rotateStart = new THREE.Vector2(); 268 | var rotateEnd = new THREE.Vector2(); 269 | var rotateDelta = new THREE.Vector2(); 270 | 271 | var panStart = new THREE.Vector2(); 272 | var panEnd = new THREE.Vector2(); 273 | var panDelta = new THREE.Vector2(); 274 | 275 | var dollyStart = new THREE.Vector2(); 276 | var dollyEnd = new THREE.Vector2(); 277 | var dollyDelta = new THREE.Vector2(); 278 | 279 | function getAutoRotationAngle() { 280 | 281 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 282 | 283 | } 284 | 285 | function getZoomScale() { 286 | 287 | return Math.pow( 0.95, scope.zoomSpeed ); 288 | 289 | } 290 | 291 | function rotateLeft( angle ) { 292 | 293 | sphericalDelta.theta -= angle; 294 | 295 | } 296 | 297 | function rotateUp( angle ) { 298 | 299 | sphericalDelta.phi -= angle; 300 | 301 | } 302 | 303 | var panLeft = function () { 304 | 305 | var v = new THREE.Vector3(); 306 | 307 | return function panLeft( distance, objectMatrix ) { 308 | 309 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 310 | v.multiplyScalar( - distance ); 311 | 312 | panOffset.add( v ); 313 | 314 | }; 315 | 316 | }(); 317 | 318 | var panUp = function () { 319 | 320 | var v = new THREE.Vector3(); 321 | 322 | return function panUp( distance, objectMatrix ) { 323 | 324 | if ( scope.screenSpacePanning === true ) { 325 | 326 | v.setFromMatrixColumn( objectMatrix, 1 ); 327 | 328 | } else { 329 | 330 | v.setFromMatrixColumn( objectMatrix, 0 ); 331 | v.crossVectors( scope.object.up, v ); 332 | 333 | } 334 | 335 | v.multiplyScalar( distance ); 336 | 337 | panOffset.add( v ); 338 | 339 | }; 340 | 341 | }(); 342 | 343 | // deltaX and deltaY are in pixels; right and down are positive 344 | var pan = function () { 345 | 346 | var offset = new THREE.Vector3(); 347 | 348 | return function pan( deltaX, deltaY ) { 349 | 350 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 351 | 352 | if ( scope.object.isPerspectiveCamera ) { 353 | 354 | // perspective 355 | var position = scope.object.position; 356 | offset.copy( position ).sub( scope.target ); 357 | var targetDistance = offset.length(); 358 | 359 | // half of the fov is center to top of screen 360 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 361 | 362 | // we use only clientHeight here so aspect ratio does not distort speed 363 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 364 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 365 | 366 | } else if ( scope.object.isOrthographicCamera ) { 367 | 368 | // orthographic 369 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 370 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 371 | 372 | } else { 373 | 374 | // camera neither orthographic nor perspective 375 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 376 | scope.enablePan = false; 377 | 378 | } 379 | 380 | }; 381 | 382 | }(); 383 | 384 | function dollyIn( dollyScale ) { 385 | 386 | if ( scope.object.isPerspectiveCamera ) { 387 | 388 | scale /= dollyScale; 389 | 390 | } else if ( scope.object.isOrthographicCamera ) { 391 | 392 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 393 | scope.object.updateProjectionMatrix(); 394 | zoomChanged = true; 395 | 396 | } else { 397 | 398 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 399 | scope.enableZoom = false; 400 | 401 | } 402 | 403 | } 404 | 405 | function dollyOut( dollyScale ) { 406 | 407 | if ( scope.object.isPerspectiveCamera ) { 408 | 409 | scale *= dollyScale; 410 | 411 | } else if ( scope.object.isOrthographicCamera ) { 412 | 413 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 414 | scope.object.updateProjectionMatrix(); 415 | zoomChanged = true; 416 | 417 | } else { 418 | 419 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 420 | scope.enableZoom = false; 421 | 422 | } 423 | 424 | } 425 | 426 | // 427 | // event callbacks - update the object state 428 | // 429 | 430 | function handleMouseDownRotate( event ) { 431 | 432 | //console.log( 'handleMouseDownRotate' ); 433 | 434 | rotateStart.set( event.clientX, event.clientY ); 435 | 436 | } 437 | 438 | function handleMouseDownDolly( event ) { 439 | 440 | //console.log( 'handleMouseDownDolly' ); 441 | 442 | dollyStart.set( event.clientX, event.clientY ); 443 | 444 | } 445 | 446 | function handleMouseDownPan( event ) { 447 | 448 | //console.log( 'handleMouseDownPan' ); 449 | 450 | panStart.set( event.clientX, event.clientY ); 451 | 452 | } 453 | 454 | function handleMouseMoveRotate( event ) { 455 | 456 | //console.log( 'handleMouseMoveRotate' ); 457 | 458 | rotateEnd.set( event.clientX, event.clientY ); 459 | 460 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 461 | 462 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 463 | 464 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 465 | 466 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 467 | 468 | rotateStart.copy( rotateEnd ); 469 | 470 | scope.update(); 471 | 472 | } 473 | 474 | function handleMouseMoveDolly( event ) { 475 | 476 | //console.log( 'handleMouseMoveDolly' ); 477 | 478 | dollyEnd.set( event.clientX, event.clientY ); 479 | 480 | dollyDelta.subVectors( dollyEnd, dollyStart ); 481 | 482 | if ( dollyDelta.y > 0 ) { 483 | 484 | dollyIn( getZoomScale() ); 485 | 486 | } else if ( dollyDelta.y < 0 ) { 487 | 488 | dollyOut( getZoomScale() ); 489 | 490 | } 491 | 492 | dollyStart.copy( dollyEnd ); 493 | 494 | scope.update(); 495 | 496 | } 497 | 498 | function handleMouseMovePan( event ) { 499 | 500 | //console.log( 'handleMouseMovePan' ); 501 | 502 | panEnd.set( event.clientX, event.clientY ); 503 | 504 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 505 | 506 | pan( panDelta.x, panDelta.y ); 507 | 508 | panStart.copy( panEnd ); 509 | 510 | scope.update(); 511 | 512 | } 513 | 514 | function handleMouseUp( event ) { 515 | 516 | // console.log( 'handleMouseUp' ); 517 | 518 | } 519 | 520 | function handleMouseWheel( event ) { 521 | 522 | // console.log( 'handleMouseWheel' ); 523 | 524 | if ( event.deltaY < 0 ) { 525 | 526 | dollyOut( getZoomScale() ); 527 | 528 | } else if ( event.deltaY > 0 ) { 529 | 530 | dollyIn( getZoomScale() ); 531 | 532 | } 533 | 534 | scope.update(); 535 | 536 | } 537 | 538 | function handleKeyDown( event ) { 539 | 540 | // console.log( 'handleKeyDown' ); 541 | 542 | var needsUpdate = false; 543 | 544 | switch ( event.keyCode ) { 545 | 546 | case scope.keys.UP: 547 | pan( 0, scope.keyPanSpeed ); 548 | needsUpdate = true; 549 | break; 550 | 551 | case scope.keys.BOTTOM: 552 | pan( 0, - scope.keyPanSpeed ); 553 | needsUpdate = true; 554 | break; 555 | 556 | case scope.keys.LEFT: 557 | pan( scope.keyPanSpeed, 0 ); 558 | needsUpdate = true; 559 | break; 560 | 561 | case scope.keys.RIGHT: 562 | pan( - scope.keyPanSpeed, 0 ); 563 | needsUpdate = true; 564 | break; 565 | 566 | } 567 | 568 | if ( needsUpdate ) { 569 | 570 | // prevent the browser from scrolling on cursor keys 571 | event.preventDefault(); 572 | 573 | scope.update(); 574 | 575 | } 576 | 577 | 578 | } 579 | 580 | function handleTouchStartRotate( event ) { 581 | 582 | //console.log( 'handleTouchStartRotate' ); 583 | 584 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 585 | 586 | } 587 | 588 | function handleTouchStartDollyPan( event ) { 589 | 590 | //console.log( 'handleTouchStartDollyPan' ); 591 | 592 | if ( scope.enableZoom ) { 593 | 594 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 595 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 596 | 597 | var distance = Math.sqrt( dx * dx + dy * dy ); 598 | 599 | dollyStart.set( 0, distance ); 600 | 601 | } 602 | 603 | if ( scope.enablePan ) { 604 | 605 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 606 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 607 | 608 | panStart.set( x, y ); 609 | 610 | } 611 | 612 | } 613 | 614 | function handleTouchMoveRotate( event ) { 615 | 616 | //console.log( 'handleTouchMoveRotate' ); 617 | 618 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 619 | 620 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 621 | 622 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 623 | 624 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 625 | 626 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 627 | 628 | rotateStart.copy( rotateEnd ); 629 | 630 | scope.update(); 631 | 632 | } 633 | 634 | function handleTouchMoveDollyPan( event ) { 635 | 636 | //console.log( 'handleTouchMoveDollyPan' ); 637 | 638 | if ( scope.enableZoom ) { 639 | 640 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 641 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 642 | 643 | var distance = Math.sqrt( dx * dx + dy * dy ); 644 | 645 | dollyEnd.set( 0, distance ); 646 | 647 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 648 | 649 | dollyIn( dollyDelta.y ); 650 | 651 | dollyStart.copy( dollyEnd ); 652 | 653 | } 654 | 655 | if ( scope.enablePan ) { 656 | 657 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 658 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 659 | 660 | panEnd.set( x, y ); 661 | 662 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 663 | 664 | pan( panDelta.x, panDelta.y ); 665 | 666 | panStart.copy( panEnd ); 667 | 668 | } 669 | 670 | scope.update(); 671 | 672 | } 673 | 674 | function handleTouchEnd( event ) { 675 | 676 | //console.log( 'handleTouchEnd' ); 677 | 678 | } 679 | 680 | // 681 | // event handlers - FSM: listen for events and reset state 682 | // 683 | 684 | function onMouseDown( event ) { 685 | 686 | if ( scope.enabled === false ) return; 687 | 688 | // Prevent the browser from scrolling. 689 | 690 | event.preventDefault(); 691 | 692 | // Manually set the focus since calling preventDefault above 693 | // prevents the browser from setting it automatically. 694 | 695 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 696 | 697 | switch ( event.button ) { 698 | 699 | case scope.mouseButtons.LEFT: 700 | 701 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 702 | 703 | if ( scope.enablePan === false ) return; 704 | 705 | handleMouseDownPan( event ); 706 | 707 | state = STATE.PAN; 708 | 709 | } else { 710 | 711 | if ( scope.enableRotate === false ) return; 712 | 713 | handleMouseDownRotate( event ); 714 | 715 | state = STATE.ROTATE; 716 | 717 | } 718 | 719 | break; 720 | 721 | case scope.mouseButtons.MIDDLE: 722 | 723 | if ( scope.enableZoom === false ) return; 724 | 725 | handleMouseDownDolly( event ); 726 | 727 | state = STATE.DOLLY; 728 | 729 | break; 730 | 731 | case scope.mouseButtons.RIGHT: 732 | 733 | if ( scope.enablePan === false ) return; 734 | 735 | handleMouseDownPan( event ); 736 | 737 | state = STATE.PAN; 738 | 739 | break; 740 | 741 | } 742 | 743 | if ( state !== STATE.NONE ) { 744 | 745 | document.addEventListener( 'mousemove', onMouseMove, false ); 746 | document.addEventListener( 'mouseup', onMouseUp, false ); 747 | 748 | scope.dispatchEvent( startEvent ); 749 | 750 | } 751 | 752 | } 753 | 754 | function onMouseMove( event ) { 755 | 756 | if ( scope.enabled === false ) return; 757 | 758 | event.preventDefault(); 759 | 760 | switch ( state ) { 761 | 762 | case STATE.ROTATE: 763 | 764 | if ( scope.enableRotate === false ) return; 765 | 766 | handleMouseMoveRotate( event ); 767 | 768 | break; 769 | 770 | case STATE.DOLLY: 771 | 772 | if ( scope.enableZoom === false ) return; 773 | 774 | handleMouseMoveDolly( event ); 775 | 776 | break; 777 | 778 | case STATE.PAN: 779 | 780 | if ( scope.enablePan === false ) return; 781 | 782 | handleMouseMovePan( event ); 783 | 784 | break; 785 | 786 | } 787 | 788 | } 789 | 790 | function onMouseUp( event ) { 791 | 792 | if ( scope.enabled === false ) return; 793 | 794 | handleMouseUp( event ); 795 | 796 | document.removeEventListener( 'mousemove', onMouseMove, false ); 797 | document.removeEventListener( 'mouseup', onMouseUp, false ); 798 | 799 | scope.dispatchEvent( endEvent ); 800 | 801 | state = STATE.NONE; 802 | 803 | } 804 | 805 | function onMouseWheel( event ) { 806 | 807 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 808 | 809 | event.preventDefault(); 810 | event.stopPropagation(); 811 | 812 | scope.dispatchEvent( startEvent ); 813 | 814 | handleMouseWheel( event ); 815 | 816 | scope.dispatchEvent( endEvent ); 817 | 818 | } 819 | 820 | function onKeyDown( event ) { 821 | 822 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 823 | 824 | handleKeyDown( event ); 825 | 826 | } 827 | 828 | function onTouchStart( event ) { 829 | 830 | if ( scope.enabled === false ) return; 831 | 832 | event.preventDefault(); 833 | 834 | switch ( event.touches.length ) { 835 | 836 | case 1: // one-fingered touch: rotate 837 | 838 | if ( scope.enableRotate === false ) return; 839 | 840 | handleTouchStartRotate( event ); 841 | 842 | state = STATE.TOUCH_ROTATE; 843 | 844 | break; 845 | 846 | case 2: // two-fingered touch: dolly-pan 847 | 848 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 849 | 850 | handleTouchStartDollyPan( event ); 851 | 852 | state = STATE.TOUCH_DOLLY_PAN; 853 | 854 | break; 855 | 856 | default: 857 | 858 | state = STATE.NONE; 859 | 860 | } 861 | 862 | if ( state !== STATE.NONE ) { 863 | 864 | scope.dispatchEvent( startEvent ); 865 | 866 | } 867 | 868 | } 869 | 870 | function onTouchMove( event ) { 871 | 872 | if ( scope.enabled === false ) return; 873 | 874 | event.preventDefault(); 875 | event.stopPropagation(); 876 | 877 | switch ( event.touches.length ) { 878 | 879 | case 1: // one-fingered touch: rotate 880 | 881 | if ( scope.enableRotate === false ) return; 882 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed? 883 | 884 | handleTouchMoveRotate( event ); 885 | 886 | break; 887 | 888 | case 2: // two-fingered touch: dolly-pan 889 | 890 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 891 | if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed? 892 | 893 | handleTouchMoveDollyPan( event ); 894 | 895 | break; 896 | 897 | default: 898 | 899 | state = STATE.NONE; 900 | 901 | } 902 | 903 | } 904 | 905 | function onTouchEnd( event ) { 906 | 907 | if ( scope.enabled === false ) return; 908 | 909 | handleTouchEnd( event ); 910 | 911 | scope.dispatchEvent( endEvent ); 912 | 913 | state = STATE.NONE; 914 | 915 | } 916 | 917 | function onContextMenu( event ) { 918 | 919 | if ( scope.enabled === false ) return; 920 | 921 | event.preventDefault(); 922 | 923 | } 924 | 925 | // 926 | 927 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 928 | 929 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 930 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 931 | 932 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 933 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 934 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 935 | 936 | window.addEventListener( 'keydown', onKeyDown, false ); 937 | 938 | // force an update at start 939 | 940 | this.update(); 941 | 942 | }; 943 | 944 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 945 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 946 | 947 | Object.defineProperties( THREE.OrbitControls.prototype, { 948 | 949 | center: { 950 | 951 | get: function () { 952 | 953 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 954 | return this.target; 955 | 956 | } 957 | 958 | }, 959 | 960 | // backward compatibility 961 | 962 | noZoom: { 963 | 964 | get: function () { 965 | 966 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 967 | return ! this.enableZoom; 968 | 969 | }, 970 | 971 | set: function ( value ) { 972 | 973 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 974 | this.enableZoom = ! value; 975 | 976 | } 977 | 978 | }, 979 | 980 | noRotate: { 981 | 982 | get: function () { 983 | 984 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 985 | return ! this.enableRotate; 986 | 987 | }, 988 | 989 | set: function ( value ) { 990 | 991 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 992 | this.enableRotate = ! value; 993 | 994 | } 995 | 996 | }, 997 | 998 | noPan: { 999 | 1000 | get: function () { 1001 | 1002 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 1003 | return ! this.enablePan; 1004 | 1005 | }, 1006 | 1007 | set: function ( value ) { 1008 | 1009 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 1010 | this.enablePan = ! value; 1011 | 1012 | } 1013 | 1014 | }, 1015 | 1016 | noKeys: { 1017 | 1018 | get: function () { 1019 | 1020 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 1021 | return ! this.enableKeys; 1022 | 1023 | }, 1024 | 1025 | set: function ( value ) { 1026 | 1027 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 1028 | this.enableKeys = ! value; 1029 | 1030 | } 1031 | 1032 | }, 1033 | 1034 | staticMoving: { 1035 | 1036 | get: function () { 1037 | 1038 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1039 | return ! this.enableDamping; 1040 | 1041 | }, 1042 | 1043 | set: function ( value ) { 1044 | 1045 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1046 | this.enableDamping = ! value; 1047 | 1048 | } 1049 | 1050 | }, 1051 | 1052 | dynamicDampingFactor: { 1053 | 1054 | get: function () { 1055 | 1056 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1057 | return this.dampingFactor; 1058 | 1059 | }, 1060 | 1061 | set: function ( value ) { 1062 | 1063 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1064 | this.dampingFactor = value; 1065 | 1066 | } 1067 | 1068 | } 1069 | 1070 | } ); 1071 | --------------------------------------------------------------------------------