├── src ├── App.css ├── reportWebVitals.js ├── index.js ├── App.js └── utils │ ├── DeviceOrientationControls.js │ ├── DragControls.js │ ├── OrbitControls.js │ └── TransformControls.js ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .gitignore ├── package.json └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-lcs/color-cube/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-lcs/color-cube/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-lcs/color-cube/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(console.log); 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-cube", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^17.0.2", 7 | "react-dom": "^17.0.2", 8 | "react-scripts": "4.0.3", 9 | "three": "^0.133.1", 10 | "web-vitals": "^1.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build" 15 | }, 16 | "eslintConfig": { 17 | "extends": [ 18 | "react-app" 19 | ] 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import * as THREE from 'three'; 3 | 4 | // import { DeviceOrientationControls } from './utils/DeviceOrientationControls'; 5 | // import { TransformControls } from './utils/TransformControls'; 6 | // import { DragControls } from './utils/DragControls'; 7 | import { OrbitControls } from './utils/OrbitControls'; 8 | 9 | import './App.css'; 10 | 11 | function App() { 12 | useEffect(() => { 13 | const scene = new THREE.Scene(); 14 | const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 15 | 16 | const renderer = new THREE.WebGLRenderer(); 17 | renderer.setSize(window.innerWidth, window.innerHeight); 18 | document.body.appendChild(renderer.domElement); 19 | 20 | const geometry = new THREE.BoxGeometry(); 21 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 22 | const cube = new THREE.Mesh(geometry, material); 23 | scene.add(cube); 24 | 25 | const controls = new OrbitControls( camera, renderer.domElement ); 26 | 27 | camera.position.z = 5 28 | 29 | controls.update(); 30 | renderer.render( scene, camera ); 31 | 32 | animate() 33 | 34 | function animate() { 35 | 36 | requestAnimationFrame( animate ); 37 | 38 | // required if controls.enableDamping or controls.autoRotate are set to true 39 | controls.update(); 40 | 41 | renderer.render( scene, camera ); 42 | 43 | } 44 | 45 | }, []) 46 | return ( 47 |
48 | ); 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Color Cube 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/utils/DeviceOrientationControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | Euler, 3 | EventDispatcher, 4 | MathUtils, 5 | Quaternion, 6 | Vector3 7 | } from 'three'; 8 | 9 | const _zee = new Vector3( 0, 0, 1 ); 10 | const _euler = new Euler(); 11 | const _q0 = new Quaternion(); 12 | const _q1 = new Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis 13 | 14 | const _changeEvent = { type: 'change' }; 15 | 16 | class DeviceOrientationControls extends EventDispatcher { 17 | 18 | constructor( object ) { 19 | 20 | super(); 21 | 22 | if ( window.isSecureContext === false ) { 23 | 24 | console.error( 'THREE.DeviceOrientationControls: DeviceOrientationEvent is only available in secure contexts (https)' ); 25 | 26 | } 27 | 28 | const scope = this; 29 | 30 | const EPS = 0.000001; 31 | const lastQuaternion = new Quaternion(); 32 | 33 | this.object = object; 34 | this.object.rotation.reorder( 'YXZ' ); 35 | 36 | this.enabled = true; 37 | 38 | this.deviceOrientation = {}; 39 | this.screenOrientation = 0; 40 | 41 | this.alphaOffset = 0; // radians 42 | 43 | const onDeviceOrientationChangeEvent = function ( event ) { 44 | 45 | scope.deviceOrientation = event; 46 | 47 | }; 48 | 49 | const onScreenOrientationChangeEvent = function () { 50 | 51 | scope.screenOrientation = window.orientation || 0; 52 | 53 | }; 54 | 55 | // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' 56 | 57 | const setObjectQuaternion = function ( quaternion, alpha, beta, gamma, orient ) { 58 | 59 | _euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us 60 | 61 | quaternion.setFromEuler( _euler ); // orient the device 62 | 63 | quaternion.multiply( _q1 ); // camera looks out the back of the device, not the top 64 | 65 | quaternion.multiply( _q0.setFromAxisAngle( _zee, - orient ) ); // adjust for screen orientation 66 | 67 | }; 68 | 69 | this.connect = function () { 70 | 71 | onScreenOrientationChangeEvent(); // run once on load 72 | 73 | // iOS 13+ 74 | 75 | if ( window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function' ) { 76 | 77 | window.DeviceOrientationEvent.requestPermission().then( function ( response ) { 78 | 79 | if ( response == 'granted' ) { 80 | 81 | window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 82 | window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 83 | 84 | } 85 | 86 | } ).catch( function ( error ) { 87 | 88 | console.error( 'THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error ); 89 | 90 | } ); 91 | 92 | } else { 93 | 94 | window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 95 | window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 96 | 97 | } 98 | 99 | scope.enabled = true; 100 | 101 | }; 102 | 103 | this.disconnect = function () { 104 | 105 | window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 106 | window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 107 | 108 | scope.enabled = false; 109 | 110 | }; 111 | 112 | this.update = function () { 113 | 114 | if ( scope.enabled === false ) return; 115 | 116 | const device = scope.deviceOrientation; 117 | 118 | if ( device ) { 119 | 120 | const alpha = device.alpha ? MathUtils.degToRad( device.alpha ) + scope.alphaOffset : 0; // Z 121 | 122 | const beta = device.beta ? MathUtils.degToRad( device.beta ) : 0; // X' 123 | 124 | const gamma = device.gamma ? MathUtils.degToRad( device.gamma ) : 0; // Y'' 125 | 126 | const orient = scope.screenOrientation ? MathUtils.degToRad( scope.screenOrientation ) : 0; // O 127 | 128 | setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient ); 129 | 130 | if ( 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 131 | 132 | lastQuaternion.copy( scope.object.quaternion ); 133 | scope.dispatchEvent( _changeEvent ); 134 | 135 | } 136 | 137 | } 138 | 139 | }; 140 | 141 | this.dispose = function () { 142 | 143 | scope.disconnect(); 144 | 145 | }; 146 | 147 | this.connect(); 148 | 149 | } 150 | 151 | } 152 | 153 | export { DeviceOrientationControls }; -------------------------------------------------------------------------------- /src/utils/DragControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | Matrix4, 4 | Plane, 5 | Raycaster, 6 | Vector2, 7 | Vector3 8 | } from 'three'; 9 | 10 | const _plane = new Plane(); 11 | const _raycaster = new Raycaster(); 12 | 13 | const _pointer = new Vector2(); 14 | const _offset = new Vector3(); 15 | const _intersection = new Vector3(); 16 | const _worldPosition = new Vector3(); 17 | const _inverseMatrix = new Matrix4(); 18 | 19 | class DragControls extends EventDispatcher { 20 | 21 | constructor( _objects, _camera, _domElement ) { 22 | 23 | super(); 24 | 25 | _domElement.style.touchAction = 'none'; // disable touch scroll 26 | 27 | let _selected = null, _hovered = null; 28 | 29 | const _intersections = []; 30 | 31 | // 32 | 33 | const scope = this; 34 | 35 | function activate() { 36 | 37 | _domElement.addEventListener( 'pointermove', onPointerMove ); 38 | _domElement.addEventListener( 'pointerdown', onPointerDown ); 39 | _domElement.addEventListener( 'pointerup', onPointerCancel ); 40 | _domElement.addEventListener( 'pointerleave', onPointerCancel ); 41 | 42 | } 43 | 44 | function deactivate() { 45 | 46 | _domElement.removeEventListener( 'pointermove', onPointerMove ); 47 | _domElement.removeEventListener( 'pointerdown', onPointerDown ); 48 | _domElement.removeEventListener( 'pointerup', onPointerCancel ); 49 | _domElement.removeEventListener( 'pointerleave', onPointerCancel ); 50 | 51 | _domElement.style.cursor = ''; 52 | 53 | } 54 | 55 | function dispose() { 56 | 57 | deactivate(); 58 | 59 | } 60 | 61 | function getObjects() { 62 | 63 | return _objects; 64 | 65 | } 66 | 67 | function onPointerMove( event ) { 68 | 69 | if ( scope.enabled === false ) return; 70 | 71 | updatePointer( event ); 72 | 73 | _raycaster.setFromCamera( _pointer, _camera ); 74 | 75 | if ( _selected ) { 76 | 77 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 78 | 79 | _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) ); 80 | 81 | } 82 | 83 | scope.dispatchEvent( { type: 'drag', object: _selected } ); 84 | 85 | return; 86 | 87 | } 88 | 89 | // hover support 90 | 91 | if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) { 92 | 93 | _intersections.length = 0; 94 | 95 | _raycaster.setFromCamera( _pointer, _camera ); 96 | _raycaster.intersectObjects( _objects, true, _intersections ); 97 | 98 | if ( _intersections.length > 0 ) { 99 | 100 | const object = _intersections[ 0 ].object; 101 | 102 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) ); 103 | 104 | if ( _hovered !== object && _hovered !== null ) { 105 | 106 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 107 | 108 | _domElement.style.cursor = 'auto'; 109 | _hovered = null; 110 | 111 | } 112 | 113 | if ( _hovered !== object ) { 114 | 115 | scope.dispatchEvent( { type: 'hoveron', object: object } ); 116 | 117 | _domElement.style.cursor = 'pointer'; 118 | _hovered = object; 119 | 120 | } 121 | 122 | } else { 123 | 124 | if ( _hovered !== null ) { 125 | 126 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 127 | 128 | _domElement.style.cursor = 'auto'; 129 | _hovered = null; 130 | 131 | } 132 | 133 | } 134 | 135 | } 136 | 137 | } 138 | 139 | function onPointerDown( event ) { 140 | 141 | if ( scope.enabled === false ) return; 142 | 143 | updatePointer( event ); 144 | 145 | _intersections.length = 0; 146 | 147 | _raycaster.setFromCamera( _pointer, _camera ); 148 | _raycaster.intersectObjects( _objects, true, _intersections ); 149 | 150 | if ( _intersections.length > 0 ) { 151 | 152 | _selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object; 153 | 154 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 155 | 156 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 157 | 158 | _inverseMatrix.copy( _selected.parent.matrixWorld ).invert(); 159 | _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 160 | 161 | } 162 | 163 | _domElement.style.cursor = 'move'; 164 | 165 | scope.dispatchEvent( { type: 'dragstart', object: _selected } ); 166 | 167 | } 168 | 169 | 170 | } 171 | 172 | function onPointerCancel() { 173 | 174 | if ( scope.enabled === false ) return; 175 | 176 | if ( _selected ) { 177 | 178 | scope.dispatchEvent( { type: 'dragend', object: _selected } ); 179 | 180 | _selected = null; 181 | 182 | } 183 | 184 | _domElement.style.cursor = _hovered ? 'pointer' : 'auto'; 185 | 186 | } 187 | 188 | function updatePointer( event ) { 189 | 190 | const rect = _domElement.getBoundingClientRect(); 191 | 192 | _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1; 193 | _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1; 194 | 195 | } 196 | 197 | activate(); 198 | 199 | // API 200 | 201 | this.enabled = true; 202 | this.transformGroup = false; 203 | 204 | this.activate = activate; 205 | this.deactivate = deactivate; 206 | this.dispose = dispose; 207 | this.getObjects = getObjects; 208 | 209 | } 210 | 211 | } 212 | 213 | export { DragControls }; -------------------------------------------------------------------------------- /src/utils/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from 'three'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 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 left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 29 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | this.domElement.style.touchAction = 'none'; // disable touch scroll 34 | 35 | // Set to false to disable this control 36 | this.enabled = true; 37 | 38 | // "target" sets the location of focus, where the object orbits around 39 | this.target = new Vector3(); 40 | 41 | // How far you can dolly in and out ( PerspectiveCamera only ) 42 | this.minDistance = 0; 43 | this.maxDistance = Infinity; 44 | 45 | // How far you can zoom in and out ( OrthographicCamera only ) 46 | this.minZoom = 0; 47 | this.maxZoom = Infinity; 48 | 49 | // How far you can orbit vertically, upper and lower limits. 50 | // Range is 0 to Math.PI radians. 51 | this.minPolarAngle = 0; // radians 52 | this.maxPolarAngle = Math.PI; // radians 53 | 54 | // How far you can orbit horizontally, upper and lower limits. 55 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 56 | this.minAzimuthAngle = - Infinity; // radians 57 | this.maxAzimuthAngle = Infinity; // radians 58 | 59 | // Set to true to enable damping (inertia) 60 | // If damping is enabled, you must call controls.update() in your animation loop 61 | this.enableDamping = false; 62 | this.dampingFactor = 0.05; 63 | 64 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 65 | // Set to false to disable zooming 66 | this.enableZoom = true; 67 | this.zoomSpeed = 1.0; 68 | 69 | // Set to false to disable rotating 70 | this.enableRotate = true; 71 | this.rotateSpeed = 1.0; 72 | 73 | // Set to false to disable panning 74 | this.enablePan = true; 75 | this.panSpeed = 1.0; 76 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 77 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 78 | 79 | // Set to true to automatically rotate around the target 80 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 81 | this.autoRotate = false; 82 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 83 | 84 | // The four arrow keys 85 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 86 | 87 | // Mouse buttons 88 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 89 | 90 | // Touch fingers 91 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 92 | 93 | // for reset 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; 97 | 98 | // the target DOM element for key events 99 | this._domElementKeyEvents = null; 100 | 101 | // 102 | // public methods 103 | // 104 | 105 | this.getPolarAngle = function () { 106 | 107 | return spherical.phi; 108 | 109 | }; 110 | 111 | this.getAzimuthalAngle = function () { 112 | 113 | return spherical.theta; 114 | 115 | }; 116 | 117 | this.getDistance = function () { 118 | 119 | return this.object.position.distanceTo( this.target ); 120 | 121 | }; 122 | 123 | this.listenToKeyEvents = function ( domElement ) { 124 | 125 | domElement.addEventListener( 'keydown', onKeyDown ); 126 | this._domElementKeyEvents = domElement; 127 | 128 | }; 129 | 130 | this.saveState = function () { 131 | 132 | scope.target0.copy( scope.target ); 133 | scope.position0.copy( scope.object.position ); 134 | scope.zoom0 = scope.object.zoom; 135 | 136 | }; 137 | 138 | this.reset = function () { 139 | 140 | scope.target.copy( scope.target0 ); 141 | scope.object.position.copy( scope.position0 ); 142 | scope.object.zoom = scope.zoom0; 143 | 144 | scope.object.updateProjectionMatrix(); 145 | scope.dispatchEvent( _changeEvent ); 146 | 147 | scope.update(); 148 | 149 | state = STATE.NONE; 150 | 151 | }; 152 | 153 | // this method is exposed, but perhaps it would be better if we can make it private... 154 | this.update = function () { 155 | 156 | const offset = new Vector3(); 157 | 158 | // so camera.up is the orbit axis 159 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 160 | const quatInverse = quat.clone().invert(); 161 | 162 | const lastPosition = new Vector3(); 163 | const lastQuaternion = new Quaternion(); 164 | 165 | const twoPI = 2 * Math.PI; 166 | 167 | return function update() { 168 | 169 | const position = scope.object.position; 170 | 171 | offset.copy( position ).sub( scope.target ); 172 | 173 | // rotate offset to "y-axis-is-up" space 174 | offset.applyQuaternion( quat ); 175 | 176 | // angle from z-axis around y-axis 177 | spherical.setFromVector3( offset ); 178 | 179 | if ( scope.autoRotate && state === STATE.NONE ) { 180 | 181 | rotateLeft( getAutoRotationAngle() ); 182 | 183 | } 184 | 185 | if ( scope.enableDamping ) { 186 | 187 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 188 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 189 | 190 | } else { 191 | 192 | spherical.theta += sphericalDelta.theta; 193 | spherical.phi += sphericalDelta.phi; 194 | 195 | } 196 | 197 | // restrict theta to be between desired limits 198 | 199 | let min = scope.minAzimuthAngle; 200 | let max = scope.maxAzimuthAngle; 201 | 202 | if ( isFinite( min ) && isFinite( max ) ) { 203 | 204 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 205 | 206 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 207 | 208 | if ( min <= max ) { 209 | 210 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 211 | 212 | } else { 213 | 214 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 215 | Math.max( min, spherical.theta ) : 216 | Math.min( max, spherical.theta ); 217 | 218 | } 219 | 220 | } 221 | 222 | // restrict phi to be between desired limits 223 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 224 | 225 | spherical.makeSafe(); 226 | 227 | 228 | spherical.radius *= scale; 229 | 230 | // restrict radius to be between desired limits 231 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 232 | 233 | // move target to panned location 234 | 235 | if ( scope.enableDamping === true ) { 236 | 237 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 238 | 239 | } else { 240 | 241 | scope.target.add( panOffset ); 242 | 243 | } 244 | 245 | offset.setFromSpherical( spherical ); 246 | 247 | // rotate offset back to "camera-up-vector-is-up" space 248 | offset.applyQuaternion( quatInverse ); 249 | 250 | position.copy( scope.target ).add( offset ); 251 | 252 | scope.object.lookAt( scope.target ); 253 | 254 | if ( scope.enableDamping === true ) { 255 | 256 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 257 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 258 | 259 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 260 | 261 | } else { 262 | 263 | sphericalDelta.set( 0, 0, 0 ); 264 | 265 | panOffset.set( 0, 0, 0 ); 266 | 267 | } 268 | 269 | scale = 1; 270 | 271 | // update condition is: 272 | // min(camera displacement, camera rotation in radians)^2 > EPS 273 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 274 | 275 | if ( zoomChanged || 276 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 277 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 278 | 279 | scope.dispatchEvent( _changeEvent ); 280 | 281 | lastPosition.copy( scope.object.position ); 282 | lastQuaternion.copy( scope.object.quaternion ); 283 | zoomChanged = false; 284 | 285 | return true; 286 | 287 | } 288 | 289 | return false; 290 | 291 | }; 292 | 293 | }(); 294 | 295 | this.dispose = function () { 296 | 297 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 298 | 299 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 300 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 301 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 302 | 303 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 304 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 305 | 306 | 307 | if ( scope._domElementKeyEvents !== null ) { 308 | 309 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 310 | 311 | } 312 | 313 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 314 | 315 | }; 316 | 317 | // 318 | // internals 319 | // 320 | 321 | const scope = this; 322 | 323 | const STATE = { 324 | NONE: - 1, 325 | ROTATE: 0, 326 | DOLLY: 1, 327 | PAN: 2, 328 | TOUCH_ROTATE: 3, 329 | TOUCH_PAN: 4, 330 | TOUCH_DOLLY_PAN: 5, 331 | TOUCH_DOLLY_ROTATE: 6 332 | }; 333 | 334 | let state = STATE.NONE; 335 | 336 | const EPS = 0.000001; 337 | 338 | // current position in spherical coordinates 339 | const spherical = new Spherical(); 340 | const sphericalDelta = new Spherical(); 341 | 342 | let scale = 1; 343 | const panOffset = new Vector3(); 344 | let zoomChanged = false; 345 | 346 | const rotateStart = new Vector2(); 347 | const rotateEnd = new Vector2(); 348 | const rotateDelta = new Vector2(); 349 | 350 | const panStart = new Vector2(); 351 | const panEnd = new Vector2(); 352 | const panDelta = new Vector2(); 353 | 354 | const dollyStart = new Vector2(); 355 | const dollyEnd = new Vector2(); 356 | const dollyDelta = new Vector2(); 357 | 358 | const pointers = []; 359 | const pointerPositions = {}; 360 | 361 | function getAutoRotationAngle() { 362 | 363 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 364 | 365 | } 366 | 367 | function getZoomScale() { 368 | 369 | return Math.pow( 0.95, scope.zoomSpeed ); 370 | 371 | } 372 | 373 | function rotateLeft( angle ) { 374 | 375 | sphericalDelta.theta -= angle; 376 | 377 | } 378 | 379 | function rotateUp( angle ) { 380 | 381 | sphericalDelta.phi -= angle; 382 | 383 | } 384 | 385 | const panLeft = function () { 386 | 387 | const v = new Vector3(); 388 | 389 | return function panLeft( distance, objectMatrix ) { 390 | 391 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 392 | v.multiplyScalar( - distance ); 393 | 394 | panOffset.add( v ); 395 | 396 | }; 397 | 398 | }(); 399 | 400 | const panUp = function () { 401 | 402 | const v = new Vector3(); 403 | 404 | return function panUp( distance, objectMatrix ) { 405 | 406 | if ( scope.screenSpacePanning === true ) { 407 | 408 | v.setFromMatrixColumn( objectMatrix, 1 ); 409 | 410 | } else { 411 | 412 | v.setFromMatrixColumn( objectMatrix, 0 ); 413 | v.crossVectors( scope.object.up, v ); 414 | 415 | } 416 | 417 | v.multiplyScalar( distance ); 418 | 419 | panOffset.add( v ); 420 | 421 | }; 422 | 423 | }(); 424 | 425 | // deltaX and deltaY are in pixels; right and down are positive 426 | const pan = function () { 427 | 428 | const offset = new Vector3(); 429 | 430 | return function pan( deltaX, deltaY ) { 431 | 432 | const element = scope.domElement; 433 | 434 | if ( scope.object.isPerspectiveCamera ) { 435 | 436 | // perspective 437 | const position = scope.object.position; 438 | offset.copy( position ).sub( scope.target ); 439 | let targetDistance = offset.length(); 440 | 441 | // half of the fov is center to top of screen 442 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 443 | 444 | // we use only clientHeight here so aspect ratio does not distort speed 445 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 446 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 447 | 448 | } else if ( scope.object.isOrthographicCamera ) { 449 | 450 | // orthographic 451 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 452 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 453 | 454 | } else { 455 | 456 | // camera neither orthographic nor perspective 457 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 458 | scope.enablePan = false; 459 | 460 | } 461 | 462 | }; 463 | 464 | }(); 465 | 466 | function dollyOut( dollyScale ) { 467 | 468 | if ( scope.object.isPerspectiveCamera ) { 469 | 470 | scale /= dollyScale; 471 | 472 | } else if ( scope.object.isOrthographicCamera ) { 473 | 474 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 475 | scope.object.updateProjectionMatrix(); 476 | zoomChanged = true; 477 | 478 | } else { 479 | 480 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 481 | scope.enableZoom = false; 482 | 483 | } 484 | 485 | } 486 | 487 | function dollyIn( dollyScale ) { 488 | 489 | if ( scope.object.isPerspectiveCamera ) { 490 | 491 | scale *= dollyScale; 492 | 493 | } else if ( scope.object.isOrthographicCamera ) { 494 | 495 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 496 | scope.object.updateProjectionMatrix(); 497 | zoomChanged = true; 498 | 499 | } else { 500 | 501 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 502 | scope.enableZoom = false; 503 | 504 | } 505 | 506 | } 507 | 508 | // 509 | // event callbacks - update the object state 510 | // 511 | 512 | function handleMouseDownRotate( event ) { 513 | 514 | rotateStart.set( event.clientX, event.clientY ); 515 | 516 | } 517 | 518 | function handleMouseDownDolly( event ) { 519 | 520 | dollyStart.set( event.clientX, event.clientY ); 521 | 522 | } 523 | 524 | function handleMouseDownPan( event ) { 525 | 526 | panStart.set( event.clientX, event.clientY ); 527 | 528 | } 529 | 530 | function handleMouseMoveRotate( event ) { 531 | 532 | rotateEnd.set( event.clientX, event.clientY ); 533 | 534 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 535 | 536 | const element = scope.domElement; 537 | 538 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 539 | 540 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 541 | 542 | rotateStart.copy( rotateEnd ); 543 | 544 | scope.update(); 545 | 546 | } 547 | 548 | function handleMouseMoveDolly( event ) { 549 | 550 | dollyEnd.set( event.clientX, event.clientY ); 551 | 552 | dollyDelta.subVectors( dollyEnd, dollyStart ); 553 | 554 | if ( dollyDelta.y > 0 ) { 555 | 556 | dollyOut( getZoomScale() ); 557 | 558 | } else if ( dollyDelta.y < 0 ) { 559 | 560 | dollyIn( getZoomScale() ); 561 | 562 | } 563 | 564 | dollyStart.copy( dollyEnd ); 565 | 566 | scope.update(); 567 | 568 | } 569 | 570 | function handleMouseMovePan( event ) { 571 | 572 | panEnd.set( event.clientX, event.clientY ); 573 | 574 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 575 | 576 | pan( panDelta.x, panDelta.y ); 577 | 578 | panStart.copy( panEnd ); 579 | 580 | scope.update(); 581 | 582 | } 583 | 584 | function handleMouseUp( /*event*/ ) { 585 | 586 | // no-op 587 | 588 | } 589 | 590 | function handleMouseWheel( event ) { 591 | 592 | if ( event.deltaY < 0 ) { 593 | 594 | dollyIn( getZoomScale() ); 595 | 596 | } else if ( event.deltaY > 0 ) { 597 | 598 | dollyOut( getZoomScale() ); 599 | 600 | } 601 | 602 | scope.update(); 603 | 604 | } 605 | 606 | function handleKeyDown( event ) { 607 | 608 | let needsUpdate = false; 609 | 610 | switch ( event.code ) { 611 | 612 | case scope.keys.UP: 613 | pan( 0, scope.keyPanSpeed ); 614 | needsUpdate = true; 615 | break; 616 | 617 | case scope.keys.BOTTOM: 618 | pan( 0, - scope.keyPanSpeed ); 619 | needsUpdate = true; 620 | break; 621 | 622 | case scope.keys.LEFT: 623 | pan( scope.keyPanSpeed, 0 ); 624 | needsUpdate = true; 625 | break; 626 | 627 | case scope.keys.RIGHT: 628 | pan( - scope.keyPanSpeed, 0 ); 629 | needsUpdate = true; 630 | break; 631 | 632 | } 633 | 634 | if ( needsUpdate ) { 635 | 636 | // prevent the browser from scrolling on cursor keys 637 | event.preventDefault(); 638 | 639 | scope.update(); 640 | 641 | } 642 | 643 | 644 | } 645 | 646 | function handleTouchStartRotate() { 647 | 648 | if ( pointers.length === 1 ) { 649 | 650 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 651 | 652 | } else { 653 | 654 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 655 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 656 | 657 | rotateStart.set( x, y ); 658 | 659 | } 660 | 661 | } 662 | 663 | function handleTouchStartPan() { 664 | 665 | if ( pointers.length === 1 ) { 666 | 667 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 668 | 669 | } else { 670 | 671 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 672 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 673 | 674 | panStart.set( x, y ); 675 | 676 | } 677 | 678 | } 679 | 680 | function handleTouchStartDolly() { 681 | 682 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 683 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 684 | 685 | const distance = Math.sqrt( dx * dx + dy * dy ); 686 | 687 | dollyStart.set( 0, distance ); 688 | 689 | } 690 | 691 | function handleTouchStartDollyPan() { 692 | 693 | if ( scope.enableZoom ) handleTouchStartDolly(); 694 | 695 | if ( scope.enablePan ) handleTouchStartPan(); 696 | 697 | } 698 | 699 | function handleTouchStartDollyRotate() { 700 | 701 | if ( scope.enableZoom ) handleTouchStartDolly(); 702 | 703 | if ( scope.enableRotate ) handleTouchStartRotate(); 704 | 705 | } 706 | 707 | function handleTouchMoveRotate( event ) { 708 | 709 | if ( pointers.length == 1 ) { 710 | 711 | rotateEnd.set( event.pageX, event.pageY ); 712 | 713 | } else { 714 | 715 | const position = getSecondPointerPosition( event ); 716 | 717 | const x = 0.5 * ( event.pageX + position.x ); 718 | const y = 0.5 * ( event.pageY + position.y ); 719 | 720 | rotateEnd.set( x, y ); 721 | 722 | } 723 | 724 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 725 | 726 | const element = scope.domElement; 727 | 728 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 729 | 730 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 731 | 732 | rotateStart.copy( rotateEnd ); 733 | 734 | } 735 | 736 | function handleTouchMovePan( event ) { 737 | 738 | if ( pointers.length === 1 ) { 739 | 740 | panEnd.set( event.pageX, event.pageY ); 741 | 742 | } else { 743 | 744 | const position = getSecondPointerPosition( event ); 745 | 746 | const x = 0.5 * ( event.pageX + position.x ); 747 | const y = 0.5 * ( event.pageY + position.y ); 748 | 749 | panEnd.set( x, y ); 750 | 751 | } 752 | 753 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 754 | 755 | pan( panDelta.x, panDelta.y ); 756 | 757 | panStart.copy( panEnd ); 758 | 759 | } 760 | 761 | function handleTouchMoveDolly( event ) { 762 | 763 | const position = getSecondPointerPosition( event ); 764 | 765 | const dx = event.pageX - position.x; 766 | const dy = event.pageY - position.y; 767 | 768 | const distance = Math.sqrt( dx * dx + dy * dy ); 769 | 770 | dollyEnd.set( 0, distance ); 771 | 772 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 773 | 774 | dollyOut( dollyDelta.y ); 775 | 776 | dollyStart.copy( dollyEnd ); 777 | 778 | } 779 | 780 | function handleTouchMoveDollyPan( event ) { 781 | 782 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 783 | 784 | if ( scope.enablePan ) handleTouchMovePan( event ); 785 | 786 | } 787 | 788 | function handleTouchMoveDollyRotate( event ) { 789 | 790 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 791 | 792 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 793 | 794 | } 795 | 796 | function handleTouchEnd( /*event*/ ) { 797 | 798 | // no-op 799 | 800 | } 801 | 802 | // 803 | // event handlers - FSM: listen for events and reset state 804 | // 805 | 806 | function onPointerDown( event ) { 807 | 808 | if ( scope.enabled === false ) return; 809 | 810 | if ( pointers.length === 0 ) { 811 | 812 | scope.domElement.setPointerCapture( event.pointerId ); 813 | 814 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 815 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 816 | 817 | } 818 | 819 | // 820 | 821 | addPointer( event ); 822 | 823 | if ( event.pointerType === 'touch' ) { 824 | 825 | onTouchStart( event ); 826 | 827 | } else { 828 | 829 | onMouseDown( event ); 830 | 831 | } 832 | 833 | } 834 | 835 | function onPointerMove( event ) { 836 | 837 | if ( scope.enabled === false ) return; 838 | 839 | if ( event.pointerType === 'touch' ) { 840 | 841 | onTouchMove( event ); 842 | 843 | } else { 844 | 845 | onMouseMove( event ); 846 | 847 | } 848 | 849 | } 850 | 851 | function onPointerUp( event ) { 852 | 853 | if ( scope.enabled === false ) return; 854 | 855 | if ( event.pointerType === 'touch' ) { 856 | 857 | onTouchEnd(); 858 | 859 | } else { 860 | 861 | onMouseUp( event ); 862 | 863 | } 864 | 865 | removePointer( event ); 866 | 867 | // 868 | 869 | if ( pointers.length === 0 ) { 870 | 871 | scope.domElement.releasePointerCapture( event.pointerId ); 872 | 873 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 874 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 875 | 876 | } 877 | 878 | } 879 | 880 | function onPointerCancel( event ) { 881 | 882 | removePointer( event ); 883 | 884 | } 885 | 886 | function onMouseDown( event ) { 887 | 888 | let mouseAction; 889 | 890 | switch ( event.button ) { 891 | 892 | case 0: 893 | 894 | mouseAction = scope.mouseButtons.LEFT; 895 | break; 896 | 897 | case 1: 898 | 899 | mouseAction = scope.mouseButtons.MIDDLE; 900 | break; 901 | 902 | case 2: 903 | 904 | mouseAction = scope.mouseButtons.RIGHT; 905 | break; 906 | 907 | default: 908 | 909 | mouseAction = - 1; 910 | 911 | } 912 | 913 | switch ( mouseAction ) { 914 | 915 | case MOUSE.DOLLY: 916 | 917 | if ( scope.enableZoom === false ) return; 918 | 919 | handleMouseDownDolly( event ); 920 | 921 | state = STATE.DOLLY; 922 | 923 | break; 924 | 925 | case MOUSE.ROTATE: 926 | 927 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 928 | 929 | if ( scope.enablePan === false ) return; 930 | 931 | handleMouseDownPan( event ); 932 | 933 | state = STATE.PAN; 934 | 935 | } else { 936 | 937 | if ( scope.enableRotate === false ) return; 938 | 939 | handleMouseDownRotate( event ); 940 | 941 | state = STATE.ROTATE; 942 | 943 | } 944 | 945 | break; 946 | 947 | case MOUSE.PAN: 948 | 949 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 950 | 951 | if ( scope.enableRotate === false ) return; 952 | 953 | handleMouseDownRotate( event ); 954 | 955 | state = STATE.ROTATE; 956 | 957 | } else { 958 | 959 | if ( scope.enablePan === false ) return; 960 | 961 | handleMouseDownPan( event ); 962 | 963 | state = STATE.PAN; 964 | 965 | } 966 | 967 | break; 968 | 969 | default: 970 | 971 | state = STATE.NONE; 972 | 973 | } 974 | 975 | if ( state !== STATE.NONE ) { 976 | 977 | scope.dispatchEvent( _startEvent ); 978 | 979 | } 980 | 981 | } 982 | 983 | function onMouseMove( event ) { 984 | 985 | if ( scope.enabled === false ) return; 986 | 987 | switch ( state ) { 988 | 989 | case STATE.ROTATE: 990 | 991 | if ( scope.enableRotate === false ) return; 992 | 993 | handleMouseMoveRotate( event ); 994 | 995 | break; 996 | 997 | case STATE.DOLLY: 998 | 999 | if ( scope.enableZoom === false ) return; 1000 | 1001 | handleMouseMoveDolly( event ); 1002 | 1003 | break; 1004 | 1005 | case STATE.PAN: 1006 | 1007 | if ( scope.enablePan === false ) return; 1008 | 1009 | handleMouseMovePan( event ); 1010 | 1011 | break; 1012 | 1013 | } 1014 | 1015 | } 1016 | 1017 | function onMouseUp( event ) { 1018 | 1019 | handleMouseUp( event ); 1020 | 1021 | scope.dispatchEvent( _endEvent ); 1022 | 1023 | state = STATE.NONE; 1024 | 1025 | } 1026 | 1027 | function onMouseWheel( event ) { 1028 | 1029 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 1030 | 1031 | event.preventDefault(); 1032 | 1033 | scope.dispatchEvent( _startEvent ); 1034 | 1035 | handleMouseWheel( event ); 1036 | 1037 | scope.dispatchEvent( _endEvent ); 1038 | 1039 | } 1040 | 1041 | function onKeyDown( event ) { 1042 | 1043 | if ( scope.enabled === false || scope.enablePan === false ) return; 1044 | 1045 | handleKeyDown( event ); 1046 | 1047 | } 1048 | 1049 | function onTouchStart( event ) { 1050 | 1051 | trackPointer( event ); 1052 | 1053 | switch ( pointers.length ) { 1054 | 1055 | case 1: 1056 | 1057 | switch ( scope.touches.ONE ) { 1058 | 1059 | case TOUCH.ROTATE: 1060 | 1061 | if ( scope.enableRotate === false ) return; 1062 | 1063 | handleTouchStartRotate(); 1064 | 1065 | state = STATE.TOUCH_ROTATE; 1066 | 1067 | break; 1068 | 1069 | case TOUCH.PAN: 1070 | 1071 | if ( scope.enablePan === false ) return; 1072 | 1073 | handleTouchStartPan(); 1074 | 1075 | state = STATE.TOUCH_PAN; 1076 | 1077 | break; 1078 | 1079 | default: 1080 | 1081 | state = STATE.NONE; 1082 | 1083 | } 1084 | 1085 | break; 1086 | 1087 | case 2: 1088 | 1089 | switch ( scope.touches.TWO ) { 1090 | 1091 | case TOUCH.DOLLY_PAN: 1092 | 1093 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1094 | 1095 | handleTouchStartDollyPan(); 1096 | 1097 | state = STATE.TOUCH_DOLLY_PAN; 1098 | 1099 | break; 1100 | 1101 | case TOUCH.DOLLY_ROTATE: 1102 | 1103 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1104 | 1105 | handleTouchStartDollyRotate(); 1106 | 1107 | state = STATE.TOUCH_DOLLY_ROTATE; 1108 | 1109 | break; 1110 | 1111 | default: 1112 | 1113 | state = STATE.NONE; 1114 | 1115 | } 1116 | 1117 | break; 1118 | 1119 | default: 1120 | 1121 | state = STATE.NONE; 1122 | 1123 | } 1124 | 1125 | if ( state !== STATE.NONE ) { 1126 | 1127 | scope.dispatchEvent( _startEvent ); 1128 | 1129 | } 1130 | 1131 | } 1132 | 1133 | function onTouchMove( event ) { 1134 | 1135 | trackPointer( event ); 1136 | 1137 | switch ( state ) { 1138 | 1139 | case STATE.TOUCH_ROTATE: 1140 | 1141 | if ( scope.enableRotate === false ) return; 1142 | 1143 | handleTouchMoveRotate( event ); 1144 | 1145 | scope.update(); 1146 | 1147 | break; 1148 | 1149 | case STATE.TOUCH_PAN: 1150 | 1151 | if ( scope.enablePan === false ) return; 1152 | 1153 | handleTouchMovePan( event ); 1154 | 1155 | scope.update(); 1156 | 1157 | break; 1158 | 1159 | case STATE.TOUCH_DOLLY_PAN: 1160 | 1161 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1162 | 1163 | handleTouchMoveDollyPan( event ); 1164 | 1165 | scope.update(); 1166 | 1167 | break; 1168 | 1169 | case STATE.TOUCH_DOLLY_ROTATE: 1170 | 1171 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1172 | 1173 | handleTouchMoveDollyRotate( event ); 1174 | 1175 | scope.update(); 1176 | 1177 | break; 1178 | 1179 | default: 1180 | 1181 | state = STATE.NONE; 1182 | 1183 | } 1184 | 1185 | } 1186 | 1187 | function onTouchEnd( event ) { 1188 | 1189 | handleTouchEnd( event ); 1190 | 1191 | scope.dispatchEvent( _endEvent ); 1192 | 1193 | state = STATE.NONE; 1194 | 1195 | } 1196 | 1197 | function onContextMenu( event ) { 1198 | 1199 | if ( scope.enabled === false ) return; 1200 | 1201 | event.preventDefault(); 1202 | 1203 | } 1204 | 1205 | function addPointer( event ) { 1206 | 1207 | pointers.push( event ); 1208 | 1209 | } 1210 | 1211 | function removePointer( event ) { 1212 | 1213 | delete pointerPositions[ event.pointerId ]; 1214 | 1215 | for ( let i = 0; i < pointers.length; i ++ ) { 1216 | 1217 | if ( pointers[ i ].pointerId == event.pointerId ) { 1218 | 1219 | pointers.splice( i, 1 ); 1220 | return; 1221 | 1222 | } 1223 | 1224 | } 1225 | 1226 | } 1227 | 1228 | function trackPointer( event ) { 1229 | 1230 | let position = pointerPositions[ event.pointerId ]; 1231 | 1232 | if ( position === undefined ) { 1233 | 1234 | position = new Vector2(); 1235 | pointerPositions[ event.pointerId ] = position; 1236 | 1237 | } 1238 | 1239 | position.set( event.pageX, event.pageY ); 1240 | 1241 | } 1242 | 1243 | function getSecondPointerPosition( event ) { 1244 | 1245 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1246 | 1247 | return pointerPositions[ pointer.pointerId ]; 1248 | 1249 | } 1250 | 1251 | // 1252 | 1253 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1254 | 1255 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1256 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1257 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1258 | 1259 | // force an update at start 1260 | 1261 | this.update(); 1262 | 1263 | } 1264 | 1265 | } 1266 | 1267 | 1268 | // This set of controls performs orbiting, dollying (zooming), and panning. 1269 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1270 | // This is very similar to OrbitControls, another set of touch behavior 1271 | // 1272 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1273 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1274 | // Pan - left mouse, or arrow keys / touch: one-finger move 1275 | 1276 | class MapControls extends OrbitControls { 1277 | 1278 | constructor( object, domElement ) { 1279 | 1280 | super( object, domElement ); 1281 | 1282 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1283 | 1284 | this.mouseButtons.LEFT = MOUSE.PAN; 1285 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1286 | 1287 | this.touches.ONE = TOUCH.PAN; 1288 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1289 | 1290 | } 1291 | 1292 | } 1293 | 1294 | export { OrbitControls, MapControls }; -------------------------------------------------------------------------------- /src/utils/TransformControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | BoxGeometry, 3 | BufferGeometry, 4 | CylinderGeometry, 5 | DoubleSide, 6 | Euler, 7 | Float32BufferAttribute, 8 | Line, 9 | LineBasicMaterial, 10 | Matrix4, 11 | Mesh, 12 | MeshBasicMaterial, 13 | Object3D, 14 | OctahedronGeometry, 15 | PlaneGeometry, 16 | Quaternion, 17 | Raycaster, 18 | SphereGeometry, 19 | TorusGeometry, 20 | Vector3 21 | } from 'three'; 22 | 23 | const _raycaster = new Raycaster(); 24 | 25 | const _tempVector = new Vector3(); 26 | const _tempVector2 = new Vector3(); 27 | const _tempQuaternion = new Quaternion(); 28 | const _unit = { 29 | X: new Vector3( 1, 0, 0 ), 30 | Y: new Vector3( 0, 1, 0 ), 31 | Z: new Vector3( 0, 0, 1 ) 32 | }; 33 | 34 | const _changeEvent = { type: 'change' }; 35 | const _mouseDownEvent = { type: 'mouseDown' }; 36 | const _mouseUpEvent = { type: 'mouseUp', mode: null }; 37 | const _objectChangeEvent = { type: 'objectChange' }; 38 | 39 | class TransformControls extends Object3D { 40 | 41 | constructor( camera, domElement ) { 42 | 43 | super(); 44 | 45 | if ( domElement === undefined ) { 46 | 47 | console.warn( 'THREE.TransformControls: The second parameter "domElement" is now mandatory.' ); 48 | domElement = document; 49 | 50 | } 51 | 52 | this.visible = false; 53 | this.domElement = domElement; 54 | this.domElement.style.touchAction = 'none'; // disable touch scroll 55 | 56 | const _gizmo = new TransformControlsGizmo(); 57 | this._gizmo = _gizmo; 58 | this.add( _gizmo ); 59 | 60 | const _plane = new TransformControlsPlane(); 61 | this._plane = _plane; 62 | this.add( _plane ); 63 | 64 | const scope = this; 65 | 66 | // Defined getter, setter and store for a property 67 | function defineProperty( propName, defaultValue ) { 68 | 69 | let propValue = defaultValue; 70 | 71 | Object.defineProperty( scope, propName, { 72 | 73 | get: function () { 74 | 75 | return propValue !== undefined ? propValue : defaultValue; 76 | 77 | }, 78 | 79 | set: function ( value ) { 80 | 81 | if ( propValue !== value ) { 82 | 83 | propValue = value; 84 | _plane[ propName ] = value; 85 | _gizmo[ propName ] = value; 86 | 87 | scope.dispatchEvent( { type: propName + '-changed', value: value } ); 88 | scope.dispatchEvent( _changeEvent ); 89 | 90 | } 91 | 92 | } 93 | 94 | } ); 95 | 96 | scope[ propName ] = defaultValue; 97 | _plane[ propName ] = defaultValue; 98 | _gizmo[ propName ] = defaultValue; 99 | 100 | } 101 | 102 | // Define properties with getters/setter 103 | // Setting the defined property will automatically trigger change event 104 | // Defined properties are passed down to gizmo and plane 105 | 106 | defineProperty( 'camera', camera ); 107 | defineProperty( 'object', undefined ); 108 | defineProperty( 'enabled', true ); 109 | defineProperty( 'axis', null ); 110 | defineProperty( 'mode', 'translate' ); 111 | defineProperty( 'translationSnap', null ); 112 | defineProperty( 'rotationSnap', null ); 113 | defineProperty( 'scaleSnap', null ); 114 | defineProperty( 'space', 'world' ); 115 | defineProperty( 'size', 1 ); 116 | defineProperty( 'dragging', false ); 117 | defineProperty( 'showX', true ); 118 | defineProperty( 'showY', true ); 119 | defineProperty( 'showZ', true ); 120 | 121 | // Reusable utility variables 122 | 123 | const worldPosition = new Vector3(); 124 | const worldPositionStart = new Vector3(); 125 | const worldQuaternion = new Quaternion(); 126 | const worldQuaternionStart = new Quaternion(); 127 | const cameraPosition = new Vector3(); 128 | const cameraQuaternion = new Quaternion(); 129 | const pointStart = new Vector3(); 130 | const pointEnd = new Vector3(); 131 | const rotationAxis = new Vector3(); 132 | const rotationAngle = 0; 133 | const eye = new Vector3(); 134 | 135 | // TODO: remove properties unused in plane and gizmo 136 | 137 | defineProperty( 'worldPosition', worldPosition ); 138 | defineProperty( 'worldPositionStart', worldPositionStart ); 139 | defineProperty( 'worldQuaternion', worldQuaternion ); 140 | defineProperty( 'worldQuaternionStart', worldQuaternionStart ); 141 | defineProperty( 'cameraPosition', cameraPosition ); 142 | defineProperty( 'cameraQuaternion', cameraQuaternion ); 143 | defineProperty( 'pointStart', pointStart ); 144 | defineProperty( 'pointEnd', pointEnd ); 145 | defineProperty( 'rotationAxis', rotationAxis ); 146 | defineProperty( 'rotationAngle', rotationAngle ); 147 | defineProperty( 'eye', eye ); 148 | 149 | this._offset = new Vector3(); 150 | this._startNorm = new Vector3(); 151 | this._endNorm = new Vector3(); 152 | this._cameraScale = new Vector3(); 153 | 154 | this._parentPosition = new Vector3(); 155 | this._parentQuaternion = new Quaternion(); 156 | this._parentQuaternionInv = new Quaternion(); 157 | this._parentScale = new Vector3(); 158 | 159 | this._worldScaleStart = new Vector3(); 160 | this._worldQuaternionInv = new Quaternion(); 161 | this._worldScale = new Vector3(); 162 | 163 | this._positionStart = new Vector3(); 164 | this._quaternionStart = new Quaternion(); 165 | this._scaleStart = new Vector3(); 166 | 167 | this._getPointer = getPointer.bind( this ); 168 | this._onPointerDown = onPointerDown.bind( this ); 169 | this._onPointerHover = onPointerHover.bind( this ); 170 | this._onPointerMove = onPointerMove.bind( this ); 171 | this._onPointerUp = onPointerUp.bind( this ); 172 | 173 | this.domElement.addEventListener( 'pointerdown', this._onPointerDown ); 174 | this.domElement.addEventListener( 'pointermove', this._onPointerHover ); 175 | this.domElement.addEventListener( 'pointerup', this._onPointerUp ); 176 | 177 | } 178 | 179 | // updateMatrixWorld updates key transformation variables 180 | updateMatrixWorld() { 181 | 182 | if ( this.object !== undefined ) { 183 | 184 | this.object.updateMatrixWorld(); 185 | 186 | if ( this.object.parent === null ) { 187 | 188 | console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' ); 189 | 190 | } else { 191 | 192 | this.object.parent.matrixWorld.decompose( this._parentPosition, this._parentQuaternion, this._parentScale ); 193 | 194 | } 195 | 196 | this.object.matrixWorld.decompose( this.worldPosition, this.worldQuaternion, this._worldScale ); 197 | 198 | this._parentQuaternionInv.copy( this._parentQuaternion ).invert(); 199 | this._worldQuaternionInv.copy( this.worldQuaternion ).invert(); 200 | 201 | } 202 | 203 | this.camera.updateMatrixWorld(); 204 | this.camera.matrixWorld.decompose( this.cameraPosition, this.cameraQuaternion, this._cameraScale ); 205 | 206 | this.eye.copy( this.cameraPosition ).sub( this.worldPosition ).normalize(); 207 | 208 | super.updateMatrixWorld( this ); 209 | 210 | } 211 | 212 | pointerHover( pointer ) { 213 | 214 | if ( this.object === undefined || this.dragging === true ) return; 215 | 216 | _raycaster.setFromCamera( pointer, this.camera ); 217 | 218 | const intersect = intersectObjectWithRay( this._gizmo.picker[ this.mode ], _raycaster ); 219 | 220 | if ( intersect ) { 221 | 222 | this.axis = intersect.object.name; 223 | 224 | } else { 225 | 226 | this.axis = null; 227 | 228 | } 229 | 230 | } 231 | 232 | pointerDown( pointer ) { 233 | 234 | if ( this.object === undefined || this.dragging === true || pointer.button !== 0 ) return; 235 | 236 | if ( this.axis !== null ) { 237 | 238 | _raycaster.setFromCamera( pointer, this.camera ); 239 | 240 | const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); 241 | 242 | if ( planeIntersect ) { 243 | 244 | this.object.updateMatrixWorld(); 245 | this.object.parent.updateMatrixWorld(); 246 | 247 | this._positionStart.copy( this.object.position ); 248 | this._quaternionStart.copy( this.object.quaternion ); 249 | this._scaleStart.copy( this.object.scale ); 250 | 251 | this.object.matrixWorld.decompose( this.worldPositionStart, this.worldQuaternionStart, this._worldScaleStart ); 252 | 253 | this.pointStart.copy( planeIntersect.point ).sub( this.worldPositionStart ); 254 | 255 | } 256 | 257 | this.dragging = true; 258 | _mouseDownEvent.mode = this.mode; 259 | this.dispatchEvent( _mouseDownEvent ); 260 | 261 | } 262 | 263 | } 264 | 265 | pointerMove( pointer ) { 266 | 267 | const axis = this.axis; 268 | const mode = this.mode; 269 | const object = this.object; 270 | let space = this.space; 271 | 272 | if ( mode === 'scale' ) { 273 | 274 | space = 'local'; 275 | 276 | } else if ( axis === 'E' || axis === 'XYZE' || axis === 'XYZ' ) { 277 | 278 | space = 'world'; 279 | 280 | } 281 | 282 | if ( object === undefined || axis === null || this.dragging === false || pointer.button !== - 1 ) return; 283 | 284 | _raycaster.setFromCamera( pointer, this.camera ); 285 | 286 | const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); 287 | 288 | if ( ! planeIntersect ) return; 289 | 290 | this.pointEnd.copy( planeIntersect.point ).sub( this.worldPositionStart ); 291 | 292 | if ( mode === 'translate' ) { 293 | 294 | // Apply translate 295 | 296 | this._offset.copy( this.pointEnd ).sub( this.pointStart ); 297 | 298 | if ( space === 'local' && axis !== 'XYZ' ) { 299 | 300 | this._offset.applyQuaternion( this._worldQuaternionInv ); 301 | 302 | } 303 | 304 | if ( axis.indexOf( 'X' ) === - 1 ) this._offset.x = 0; 305 | if ( axis.indexOf( 'Y' ) === - 1 ) this._offset.y = 0; 306 | if ( axis.indexOf( 'Z' ) === - 1 ) this._offset.z = 0; 307 | 308 | if ( space === 'local' && axis !== 'XYZ' ) { 309 | 310 | this._offset.applyQuaternion( this._quaternionStart ).divide( this._parentScale ); 311 | 312 | } else { 313 | 314 | this._offset.applyQuaternion( this._parentQuaternionInv ).divide( this._parentScale ); 315 | 316 | } 317 | 318 | object.position.copy( this._offset ).add( this._positionStart ); 319 | 320 | // Apply translation snap 321 | 322 | if ( this.translationSnap ) { 323 | 324 | if ( space === 'local' ) { 325 | 326 | object.position.applyQuaternion( _tempQuaternion.copy( this._quaternionStart ).invert() ); 327 | 328 | if ( axis.search( 'X' ) !== - 1 ) { 329 | 330 | object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; 331 | 332 | } 333 | 334 | if ( axis.search( 'Y' ) !== - 1 ) { 335 | 336 | object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; 337 | 338 | } 339 | 340 | if ( axis.search( 'Z' ) !== - 1 ) { 341 | 342 | object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; 343 | 344 | } 345 | 346 | object.position.applyQuaternion( this._quaternionStart ); 347 | 348 | } 349 | 350 | if ( space === 'world' ) { 351 | 352 | if ( object.parent ) { 353 | 354 | object.position.add( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); 355 | 356 | } 357 | 358 | if ( axis.search( 'X' ) !== - 1 ) { 359 | 360 | object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; 361 | 362 | } 363 | 364 | if ( axis.search( 'Y' ) !== - 1 ) { 365 | 366 | object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; 367 | 368 | } 369 | 370 | if ( axis.search( 'Z' ) !== - 1 ) { 371 | 372 | object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; 373 | 374 | } 375 | 376 | if ( object.parent ) { 377 | 378 | object.position.sub( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); 379 | 380 | } 381 | 382 | } 383 | 384 | } 385 | 386 | } else if ( mode === 'scale' ) { 387 | 388 | if ( axis.search( 'XYZ' ) !== - 1 ) { 389 | 390 | let d = this.pointEnd.length() / this.pointStart.length(); 391 | 392 | if ( this.pointEnd.dot( this.pointStart ) < 0 ) d *= - 1; 393 | 394 | _tempVector2.set( d, d, d ); 395 | 396 | } else { 397 | 398 | _tempVector.copy( this.pointStart ); 399 | _tempVector2.copy( this.pointEnd ); 400 | 401 | _tempVector.applyQuaternion( this._worldQuaternionInv ); 402 | _tempVector2.applyQuaternion( this._worldQuaternionInv ); 403 | 404 | _tempVector2.divide( _tempVector ); 405 | 406 | if ( axis.search( 'X' ) === - 1 ) { 407 | 408 | _tempVector2.x = 1; 409 | 410 | } 411 | 412 | if ( axis.search( 'Y' ) === - 1 ) { 413 | 414 | _tempVector2.y = 1; 415 | 416 | } 417 | 418 | if ( axis.search( 'Z' ) === - 1 ) { 419 | 420 | _tempVector2.z = 1; 421 | 422 | } 423 | 424 | } 425 | 426 | // Apply scale 427 | 428 | object.scale.copy( this._scaleStart ).multiply( _tempVector2 ); 429 | 430 | if ( this.scaleSnap ) { 431 | 432 | if ( axis.search( 'X' ) !== - 1 ) { 433 | 434 | object.scale.x = Math.round( object.scale.x / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 435 | 436 | } 437 | 438 | if ( axis.search( 'Y' ) !== - 1 ) { 439 | 440 | object.scale.y = Math.round( object.scale.y / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 441 | 442 | } 443 | 444 | if ( axis.search( 'Z' ) !== - 1 ) { 445 | 446 | object.scale.z = Math.round( object.scale.z / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; 447 | 448 | } 449 | 450 | } 451 | 452 | } else if ( mode === 'rotate' ) { 453 | 454 | this._offset.copy( this.pointEnd ).sub( this.pointStart ); 455 | 456 | const ROTATION_SPEED = 20 / this.worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) ); 457 | 458 | if ( axis === 'E' ) { 459 | 460 | this.rotationAxis.copy( this.eye ); 461 | this.rotationAngle = this.pointEnd.angleTo( this.pointStart ); 462 | 463 | this._startNorm.copy( this.pointStart ).normalize(); 464 | this._endNorm.copy( this.pointEnd ).normalize(); 465 | 466 | this.rotationAngle *= ( this._endNorm.cross( this._startNorm ).dot( this.eye ) < 0 ? 1 : - 1 ); 467 | 468 | } else if ( axis === 'XYZE' ) { 469 | 470 | this.rotationAxis.copy( this._offset ).cross( this.eye ).normalize(); 471 | this.rotationAngle = this._offset.dot( _tempVector.copy( this.rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED; 472 | 473 | } else if ( axis === 'X' || axis === 'Y' || axis === 'Z' ) { 474 | 475 | this.rotationAxis.copy( _unit[ axis ] ); 476 | 477 | _tempVector.copy( _unit[ axis ] ); 478 | 479 | if ( space === 'local' ) { 480 | 481 | _tempVector.applyQuaternion( this.worldQuaternion ); 482 | 483 | } 484 | 485 | this.rotationAngle = this._offset.dot( _tempVector.cross( this.eye ).normalize() ) * ROTATION_SPEED; 486 | 487 | } 488 | 489 | // Apply rotation snap 490 | 491 | if ( this.rotationSnap ) this.rotationAngle = Math.round( this.rotationAngle / this.rotationSnap ) * this.rotationSnap; 492 | 493 | // Apply rotate 494 | if ( space === 'local' && axis !== 'E' && axis !== 'XYZE' ) { 495 | 496 | object.quaternion.copy( this._quaternionStart ); 497 | object.quaternion.multiply( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ).normalize(); 498 | 499 | } else { 500 | 501 | this.rotationAxis.applyQuaternion( this._parentQuaternionInv ); 502 | object.quaternion.copy( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ); 503 | object.quaternion.multiply( this._quaternionStart ).normalize(); 504 | 505 | } 506 | 507 | } 508 | 509 | this.dispatchEvent( _changeEvent ); 510 | this.dispatchEvent( _objectChangeEvent ); 511 | 512 | } 513 | 514 | pointerUp( pointer ) { 515 | 516 | if ( pointer.button !== 0 ) return; 517 | 518 | if ( this.dragging && ( this.axis !== null ) ) { 519 | 520 | _mouseUpEvent.mode = this.mode; 521 | this.dispatchEvent( _mouseUpEvent ); 522 | 523 | } 524 | 525 | this.dragging = false; 526 | this.axis = null; 527 | 528 | } 529 | 530 | dispose() { 531 | 532 | this.domElement.removeEventListener( 'pointerdown', this._onPointerDown ); 533 | this.domElement.removeEventListener( 'pointermove', this._onPointerHover ); 534 | this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); 535 | this.domElement.removeEventListener( 'pointerup', this._onPointerUp ); 536 | 537 | this.traverse( function ( child ) { 538 | 539 | if ( child.geometry ) child.geometry.dispose(); 540 | if ( child.material ) child.material.dispose(); 541 | 542 | } ); 543 | 544 | } 545 | 546 | // Set current object 547 | attach( object ) { 548 | 549 | this.object = object; 550 | this.visible = true; 551 | 552 | return this; 553 | 554 | } 555 | 556 | // Detatch from object 557 | detach() { 558 | 559 | this.object = undefined; 560 | this.visible = false; 561 | this.axis = null; 562 | 563 | return this; 564 | 565 | } 566 | 567 | getRaycaster() { 568 | 569 | return _raycaster; 570 | 571 | } 572 | 573 | // TODO: deprecate 574 | 575 | getMode() { 576 | 577 | return this.mode; 578 | 579 | } 580 | 581 | setMode( mode ) { 582 | 583 | this.mode = mode; 584 | 585 | } 586 | 587 | setTranslationSnap( translationSnap ) { 588 | 589 | this.translationSnap = translationSnap; 590 | 591 | } 592 | 593 | setRotationSnap( rotationSnap ) { 594 | 595 | this.rotationSnap = rotationSnap; 596 | 597 | } 598 | 599 | setScaleSnap( scaleSnap ) { 600 | 601 | this.scaleSnap = scaleSnap; 602 | 603 | } 604 | 605 | setSize( size ) { 606 | 607 | this.size = size; 608 | 609 | } 610 | 611 | setSpace( space ) { 612 | 613 | this.space = space; 614 | 615 | } 616 | 617 | update() { 618 | 619 | console.warn( 'THREE.TransformControls: update function has no more functionality and therefore has been deprecated.' ); 620 | 621 | } 622 | 623 | } 624 | 625 | TransformControls.prototype.isTransformControls = true; 626 | 627 | // mouse / touch event handlers 628 | 629 | function getPointer( event ) { 630 | 631 | if ( this.domElement.ownerDocument.pointerLockElement ) { 632 | 633 | return { 634 | x: 0, 635 | y: 0, 636 | button: event.button 637 | }; 638 | 639 | } else { 640 | 641 | const rect = this.domElement.getBoundingClientRect(); 642 | 643 | return { 644 | x: ( event.clientX - rect.left ) / rect.width * 2 - 1, 645 | y: - ( event.clientY - rect.top ) / rect.height * 2 + 1, 646 | button: event.button 647 | }; 648 | 649 | } 650 | 651 | } 652 | 653 | function onPointerHover( event ) { 654 | 655 | if ( ! this.enabled ) return; 656 | 657 | switch ( event.pointerType ) { 658 | 659 | case 'mouse': 660 | case 'pen': 661 | this.pointerHover( this._getPointer( event ) ); 662 | break; 663 | 664 | } 665 | 666 | } 667 | 668 | function onPointerDown( event ) { 669 | 670 | if ( ! this.enabled ) return; 671 | 672 | this.domElement.setPointerCapture( event.pointerId ); 673 | 674 | this.domElement.addEventListener( 'pointermove', this._onPointerMove ); 675 | 676 | this.pointerHover( this._getPointer( event ) ); 677 | this.pointerDown( this._getPointer( event ) ); 678 | 679 | } 680 | 681 | function onPointerMove( event ) { 682 | 683 | if ( ! this.enabled ) return; 684 | 685 | this.pointerMove( this._getPointer( event ) ); 686 | 687 | } 688 | 689 | function onPointerUp( event ) { 690 | 691 | if ( ! this.enabled ) return; 692 | 693 | this.domElement.releasePointerCapture( event.pointerId ); 694 | 695 | this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); 696 | 697 | this.pointerUp( this._getPointer( event ) ); 698 | 699 | } 700 | 701 | function intersectObjectWithRay( object, raycaster, includeInvisible ) { 702 | 703 | const allIntersections = raycaster.intersectObject( object, true ); 704 | 705 | for ( let i = 0; i < allIntersections.length; i ++ ) { 706 | 707 | if ( allIntersections[ i ].object.visible || includeInvisible ) { 708 | 709 | return allIntersections[ i ]; 710 | 711 | } 712 | 713 | } 714 | 715 | return false; 716 | 717 | } 718 | 719 | // 720 | 721 | // Reusable utility variables 722 | 723 | const _tempEuler = new Euler(); 724 | const _alignVector = new Vector3( 0, 1, 0 ); 725 | const _zeroVector = new Vector3( 0, 0, 0 ); 726 | const _lookAtMatrix = new Matrix4(); 727 | const _tempQuaternion2 = new Quaternion(); 728 | const _identityQuaternion = new Quaternion(); 729 | const _dirVector = new Vector3(); 730 | const _tempMatrix = new Matrix4(); 731 | 732 | const _unitX = new Vector3( 1, 0, 0 ); 733 | const _unitY = new Vector3( 0, 1, 0 ); 734 | const _unitZ = new Vector3( 0, 0, 1 ); 735 | 736 | const _v1 = new Vector3(); 737 | const _v2 = new Vector3(); 738 | const _v3 = new Vector3(); 739 | 740 | class TransformControlsGizmo extends Object3D { 741 | 742 | constructor() { 743 | 744 | super(); 745 | 746 | this.type = 'TransformControlsGizmo'; 747 | 748 | // shared materials 749 | 750 | const gizmoMaterial = new MeshBasicMaterial( { 751 | depthTest: false, 752 | depthWrite: false, 753 | fog: false, 754 | toneMapped: false, 755 | transparent: true 756 | } ); 757 | 758 | const gizmoLineMaterial = new LineBasicMaterial( { 759 | depthTest: false, 760 | depthWrite: false, 761 | fog: false, 762 | toneMapped: false, 763 | transparent: true 764 | } ); 765 | 766 | // Make unique material for each axis/color 767 | 768 | const matInvisible = gizmoMaterial.clone(); 769 | matInvisible.opacity = 0.15; 770 | 771 | const matHelper = gizmoLineMaterial.clone(); 772 | matHelper.opacity = 0.5; 773 | 774 | const matRed = gizmoMaterial.clone(); 775 | matRed.color.setHex( 0xff0000 ); 776 | 777 | const matGreen = gizmoMaterial.clone(); 778 | matGreen.color.setHex( 0x00ff00 ); 779 | 780 | const matBlue = gizmoMaterial.clone(); 781 | matBlue.color.setHex( 0x0000ff ); 782 | 783 | const matRedTransparent = gizmoMaterial.clone(); 784 | matRedTransparent.color.setHex( 0xff0000 ); 785 | matRedTransparent.opacity = 0.5; 786 | 787 | const matGreenTransparent = gizmoMaterial.clone(); 788 | matGreenTransparent.color.setHex( 0x00ff00 ); 789 | matGreenTransparent.opacity = 0.5; 790 | 791 | const matBlueTransparent = gizmoMaterial.clone(); 792 | matBlueTransparent.color.setHex( 0x0000ff ); 793 | matBlueTransparent.opacity = 0.5; 794 | 795 | const matWhiteTransparent = gizmoMaterial.clone(); 796 | matWhiteTransparent.opacity = 0.25; 797 | 798 | const matYellowTransparent = gizmoMaterial.clone(); 799 | matYellowTransparent.color.setHex( 0xffff00 ); 800 | matYellowTransparent.opacity = 0.25; 801 | 802 | const matYellow = gizmoMaterial.clone(); 803 | matYellow.color.setHex( 0xffff00 ); 804 | 805 | const matGray = gizmoMaterial.clone(); 806 | matGray.color.setHex( 0x787878 ); 807 | 808 | // reusable geometry 809 | 810 | const arrowGeometry = new CylinderGeometry( 0, 0.04, 0.1, 12 ); 811 | arrowGeometry.translate( 0, 0.05, 0 ); 812 | 813 | const scaleHandleGeometry = new BoxGeometry( 0.08, 0.08, 0.08 ); 814 | scaleHandleGeometry.translate( 0, 0.04, 0 ); 815 | 816 | const lineGeometry = new BufferGeometry(); 817 | lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) ); 818 | 819 | const lineGeometry2 = new CylinderGeometry( 0.0075, 0.0075, 0.5, 3 ); 820 | lineGeometry2.translate( 0, 0.25, 0 ); 821 | 822 | function CircleGeometry( radius, arc ) { 823 | 824 | const geometry = new TorusGeometry( radius, 0.0075, 3, 64, arc * Math.PI * 2 ); 825 | geometry.rotateY( Math.PI / 2 ); 826 | geometry.rotateX( Math.PI / 2 ); 827 | return geometry; 828 | 829 | } 830 | 831 | // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position 832 | 833 | function TranslateHelperGeometry() { 834 | 835 | const geometry = new BufferGeometry(); 836 | 837 | geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 1, 1 ], 3 ) ); 838 | 839 | return geometry; 840 | 841 | } 842 | 843 | // Gizmo definitions - custom hierarchy definitions for setupGizmo() function 844 | 845 | const gizmoTranslate = { 846 | X: [ 847 | [ new Mesh( arrowGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 848 | [ new Mesh( arrowGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], 849 | [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]] 850 | ], 851 | Y: [ 852 | [ new Mesh( arrowGeometry, matGreen ), [ 0, 0.5, 0 ]], 853 | [ new Mesh( arrowGeometry, matGreen ), [ 0, - 0.5, 0 ], [ Math.PI, 0, 0 ]], 854 | [ new Mesh( lineGeometry2, matGreen ) ] 855 | ], 856 | Z: [ 857 | [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], 858 | [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]], 859 | [ new Mesh( lineGeometry2, matBlue ), null, [ Math.PI / 2, 0, 0 ]] 860 | ], 861 | XYZ: [ 862 | [ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent.clone() ), [ 0, 0, 0 ]] 863 | ], 864 | XY: [ 865 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent.clone() ), [ 0.15, 0.15, 0 ]] 866 | ], 867 | YZ: [ 868 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent.clone() ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 869 | ], 870 | XZ: [ 871 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent.clone() ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 872 | ] 873 | }; 874 | 875 | const pickerTranslate = { 876 | X: [ 877 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 878 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] 879 | ], 880 | Y: [ 881 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], 882 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] 883 | ], 884 | Z: [ 885 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], 886 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] 887 | ], 888 | XYZ: [ 889 | [ new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ) ] 890 | ], 891 | XY: [ 892 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]] 893 | ], 894 | YZ: [ 895 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 896 | ], 897 | XZ: [ 898 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 899 | ] 900 | }; 901 | 902 | const helperTranslate = { 903 | START: [ 904 | [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] 905 | ], 906 | END: [ 907 | [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] 908 | ], 909 | DELTA: [ 910 | [ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ] 911 | ], 912 | X: [ 913 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 914 | ], 915 | Y: [ 916 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] 917 | ], 918 | Z: [ 919 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] 920 | ] 921 | }; 922 | 923 | const gizmoRotate = { 924 | XYZE: [ 925 | [ new Mesh( CircleGeometry( 0.5, 1 ), matGray ), null, [ 0, Math.PI / 2, 0 ]] 926 | ], 927 | X: [ 928 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matRed ) ] 929 | ], 930 | Y: [ 931 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matGreen ), null, [ 0, 0, - Math.PI / 2 ]] 932 | ], 933 | Z: [ 934 | [ new Mesh( CircleGeometry( 0.5, 0.5 ), matBlue ), null, [ 0, Math.PI / 2, 0 ]] 935 | ], 936 | E: [ 937 | [ new Mesh( CircleGeometry( 0.75, 1 ), matYellowTransparent ), null, [ 0, Math.PI / 2, 0 ]] 938 | ] 939 | }; 940 | 941 | const helperRotate = { 942 | AXIS: [ 943 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 944 | ] 945 | }; 946 | 947 | const pickerRotate = { 948 | XYZE: [ 949 | [ new Mesh( new SphereGeometry( 0.25, 10, 8 ), matInvisible ) ] 950 | ], 951 | X: [ 952 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ]], 953 | ], 954 | Y: [ 955 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], 956 | ], 957 | Z: [ 958 | [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 959 | ], 960 | E: [ 961 | [ new Mesh( new TorusGeometry( 0.75, 0.1, 2, 24 ), matInvisible ) ] 962 | ] 963 | }; 964 | 965 | const gizmoScale = { 966 | X: [ 967 | [ new Mesh( scaleHandleGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 968 | [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 969 | [ new Mesh( scaleHandleGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], 970 | ], 971 | Y: [ 972 | [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, 0.5, 0 ]], 973 | [ new Mesh( lineGeometry2, matGreen ) ], 974 | [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, - 0.5, 0 ], [ 0, 0, Math.PI ]], 975 | ], 976 | Z: [ 977 | [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], 978 | [ new Mesh( lineGeometry2, matBlue ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], 979 | [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]] 980 | ], 981 | XY: [ 982 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]] 983 | ], 984 | YZ: [ 985 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] 986 | ], 987 | XZ: [ 988 | [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] 989 | ], 990 | XYZ: [ 991 | [ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent.clone() ) ], 992 | ] 993 | }; 994 | 995 | const pickerScale = { 996 | X: [ 997 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], 998 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] 999 | ], 1000 | Y: [ 1001 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], 1002 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] 1003 | ], 1004 | Z: [ 1005 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], 1006 | [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] 1007 | ], 1008 | XY: [ 1009 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]], 1010 | ], 1011 | YZ: [ 1012 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]], 1013 | ], 1014 | XZ: [ 1015 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]], 1016 | ], 1017 | XYZ: [ 1018 | [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 0, 0 ]], 1019 | ] 1020 | }; 1021 | 1022 | const helperScale = { 1023 | X: [ 1024 | [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] 1025 | ], 1026 | Y: [ 1027 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] 1028 | ], 1029 | Z: [ 1030 | [ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] 1031 | ] 1032 | }; 1033 | 1034 | // Creates an Object3D with gizmos described in custom hierarchy definition. 1035 | 1036 | function setupGizmo( gizmoMap ) { 1037 | 1038 | const gizmo = new Object3D(); 1039 | 1040 | for ( const name in gizmoMap ) { 1041 | 1042 | for ( let i = gizmoMap[ name ].length; i --; ) { 1043 | 1044 | const object = gizmoMap[ name ][ i ][ 0 ].clone(); 1045 | const position = gizmoMap[ name ][ i ][ 1 ]; 1046 | const rotation = gizmoMap[ name ][ i ][ 2 ]; 1047 | const scale = gizmoMap[ name ][ i ][ 3 ]; 1048 | const tag = gizmoMap[ name ][ i ][ 4 ]; 1049 | 1050 | // name and tag properties are essential for picking and updating logic. 1051 | object.name = name; 1052 | object.tag = tag; 1053 | 1054 | if ( position ) { 1055 | 1056 | object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] ); 1057 | 1058 | } 1059 | 1060 | if ( rotation ) { 1061 | 1062 | object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] ); 1063 | 1064 | } 1065 | 1066 | if ( scale ) { 1067 | 1068 | object.scale.set( scale[ 0 ], scale[ 1 ], scale[ 2 ] ); 1069 | 1070 | } 1071 | 1072 | object.updateMatrix(); 1073 | 1074 | const tempGeometry = object.geometry.clone(); 1075 | tempGeometry.applyMatrix4( object.matrix ); 1076 | object.geometry = tempGeometry; 1077 | object.renderOrder = Infinity; 1078 | 1079 | object.position.set( 0, 0, 0 ); 1080 | object.rotation.set( 0, 0, 0 ); 1081 | object.scale.set( 1, 1, 1 ); 1082 | 1083 | gizmo.add( object ); 1084 | 1085 | } 1086 | 1087 | } 1088 | 1089 | return gizmo; 1090 | 1091 | } 1092 | 1093 | // Gizmo creation 1094 | 1095 | this.gizmo = {}; 1096 | this.picker = {}; 1097 | this.helper = {}; 1098 | 1099 | this.add( this.gizmo[ 'translate' ] = setupGizmo( gizmoTranslate ) ); 1100 | this.add( this.gizmo[ 'rotate' ] = setupGizmo( gizmoRotate ) ); 1101 | this.add( this.gizmo[ 'scale' ] = setupGizmo( gizmoScale ) ); 1102 | this.add( this.picker[ 'translate' ] = setupGizmo( pickerTranslate ) ); 1103 | this.add( this.picker[ 'rotate' ] = setupGizmo( pickerRotate ) ); 1104 | this.add( this.picker[ 'scale' ] = setupGizmo( pickerScale ) ); 1105 | this.add( this.helper[ 'translate' ] = setupGizmo( helperTranslate ) ); 1106 | this.add( this.helper[ 'rotate' ] = setupGizmo( helperRotate ) ); 1107 | this.add( this.helper[ 'scale' ] = setupGizmo( helperScale ) ); 1108 | 1109 | // Pickers should be hidden always 1110 | 1111 | this.picker[ 'translate' ].visible = false; 1112 | this.picker[ 'rotate' ].visible = false; 1113 | this.picker[ 'scale' ].visible = false; 1114 | 1115 | } 1116 | 1117 | // updateMatrixWorld will update transformations and appearance of individual handles 1118 | 1119 | updateMatrixWorld( force ) { 1120 | 1121 | const space = ( this.mode === 'scale' ) ? 'local' : this.space; // scale always oriented to local rotation 1122 | 1123 | const quaternion = ( space === 'local' ) ? this.worldQuaternion : _identityQuaternion; 1124 | 1125 | // Show only gizmos for current transform mode 1126 | 1127 | this.gizmo[ 'translate' ].visible = this.mode === 'translate'; 1128 | this.gizmo[ 'rotate' ].visible = this.mode === 'rotate'; 1129 | this.gizmo[ 'scale' ].visible = this.mode === 'scale'; 1130 | 1131 | this.helper[ 'translate' ].visible = this.mode === 'translate'; 1132 | this.helper[ 'rotate' ].visible = this.mode === 'rotate'; 1133 | this.helper[ 'scale' ].visible = this.mode === 'scale'; 1134 | 1135 | 1136 | let handles = []; 1137 | handles = handles.concat( this.picker[ this.mode ].children ); 1138 | handles = handles.concat( this.gizmo[ this.mode ].children ); 1139 | handles = handles.concat( this.helper[ this.mode ].children ); 1140 | 1141 | for ( let i = 0; i < handles.length; i ++ ) { 1142 | 1143 | const handle = handles[ i ]; 1144 | 1145 | // hide aligned to camera 1146 | 1147 | handle.visible = true; 1148 | handle.rotation.set( 0, 0, 0 ); 1149 | handle.position.copy( this.worldPosition ); 1150 | 1151 | let factor; 1152 | 1153 | if ( this.camera.isOrthographicCamera ) { 1154 | 1155 | factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom; 1156 | 1157 | } else { 1158 | 1159 | factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 ); 1160 | 1161 | } 1162 | 1163 | handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 4 ); 1164 | 1165 | // TODO: simplify helpers and consider decoupling from gizmo 1166 | 1167 | if ( handle.tag === 'helper' ) { 1168 | 1169 | handle.visible = false; 1170 | 1171 | if ( handle.name === 'AXIS' ) { 1172 | 1173 | handle.position.copy( this.worldPositionStart ); 1174 | handle.visible = !! this.axis; 1175 | 1176 | if ( this.axis === 'X' ) { 1177 | 1178 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, 0 ) ); 1179 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1180 | 1181 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1182 | 1183 | handle.visible = false; 1184 | 1185 | } 1186 | 1187 | } 1188 | 1189 | if ( this.axis === 'Y' ) { 1190 | 1191 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, Math.PI / 2 ) ); 1192 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1193 | 1194 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1195 | 1196 | handle.visible = false; 1197 | 1198 | } 1199 | 1200 | } 1201 | 1202 | if ( this.axis === 'Z' ) { 1203 | 1204 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); 1205 | handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); 1206 | 1207 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { 1208 | 1209 | handle.visible = false; 1210 | 1211 | } 1212 | 1213 | } 1214 | 1215 | if ( this.axis === 'XYZE' ) { 1216 | 1217 | _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); 1218 | _alignVector.copy( this.rotationAxis ); 1219 | handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( _zeroVector, _alignVector, _unitY ) ); 1220 | handle.quaternion.multiply( _tempQuaternion ); 1221 | handle.visible = this.dragging; 1222 | 1223 | } 1224 | 1225 | if ( this.axis === 'E' ) { 1226 | 1227 | handle.visible = false; 1228 | 1229 | } 1230 | 1231 | 1232 | } else if ( handle.name === 'START' ) { 1233 | 1234 | handle.position.copy( this.worldPositionStart ); 1235 | handle.visible = this.dragging; 1236 | 1237 | } else if ( handle.name === 'END' ) { 1238 | 1239 | handle.position.copy( this.worldPosition ); 1240 | handle.visible = this.dragging; 1241 | 1242 | } else if ( handle.name === 'DELTA' ) { 1243 | 1244 | handle.position.copy( this.worldPositionStart ); 1245 | handle.quaternion.copy( this.worldQuaternionStart ); 1246 | _tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 ); 1247 | _tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() ); 1248 | handle.scale.copy( _tempVector ); 1249 | handle.visible = this.dragging; 1250 | 1251 | } else { 1252 | 1253 | handle.quaternion.copy( quaternion ); 1254 | 1255 | if ( this.dragging ) { 1256 | 1257 | handle.position.copy( this.worldPositionStart ); 1258 | 1259 | } else { 1260 | 1261 | handle.position.copy( this.worldPosition ); 1262 | 1263 | } 1264 | 1265 | if ( this.axis ) { 1266 | 1267 | handle.visible = this.axis.search( handle.name ) !== - 1; 1268 | 1269 | } 1270 | 1271 | } 1272 | 1273 | // If updating helper, skip rest of the loop 1274 | continue; 1275 | 1276 | } 1277 | 1278 | // Align handles to current local or world rotation 1279 | 1280 | handle.quaternion.copy( quaternion ); 1281 | 1282 | if ( this.mode === 'translate' || this.mode === 'scale' ) { 1283 | 1284 | // Hide translate and scale axis facing the camera 1285 | 1286 | const AXIS_HIDE_TRESHOLD = 0.99; 1287 | const PLANE_HIDE_TRESHOLD = 0.2; 1288 | 1289 | if ( handle.name === 'X' ) { 1290 | 1291 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { 1292 | 1293 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1294 | handle.visible = false; 1295 | 1296 | } 1297 | 1298 | } 1299 | 1300 | if ( handle.name === 'Y' ) { 1301 | 1302 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { 1303 | 1304 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1305 | handle.visible = false; 1306 | 1307 | } 1308 | 1309 | } 1310 | 1311 | if ( handle.name === 'Z' ) { 1312 | 1313 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { 1314 | 1315 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1316 | handle.visible = false; 1317 | 1318 | } 1319 | 1320 | } 1321 | 1322 | if ( handle.name === 'XY' ) { 1323 | 1324 | if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { 1325 | 1326 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1327 | handle.visible = false; 1328 | 1329 | } 1330 | 1331 | } 1332 | 1333 | if ( handle.name === 'YZ' ) { 1334 | 1335 | if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { 1336 | 1337 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1338 | handle.visible = false; 1339 | 1340 | } 1341 | 1342 | } 1343 | 1344 | if ( handle.name === 'XZ' ) { 1345 | 1346 | if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { 1347 | 1348 | handle.scale.set( 1e-10, 1e-10, 1e-10 ); 1349 | handle.visible = false; 1350 | 1351 | } 1352 | 1353 | } 1354 | 1355 | } else if ( this.mode === 'rotate' ) { 1356 | 1357 | // Align handles to current local or world rotation 1358 | 1359 | _tempQuaternion2.copy( quaternion ); 1360 | _alignVector.copy( this.eye ).applyQuaternion( _tempQuaternion.copy( quaternion ).invert() ); 1361 | 1362 | if ( handle.name.search( 'E' ) !== - 1 ) { 1363 | 1364 | handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( this.eye, _zeroVector, _unitY ) ); 1365 | 1366 | } 1367 | 1368 | if ( handle.name === 'X' ) { 1369 | 1370 | _tempQuaternion.setFromAxisAngle( _unitX, Math.atan2( - _alignVector.y, _alignVector.z ) ); 1371 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1372 | handle.quaternion.copy( _tempQuaternion ); 1373 | 1374 | } 1375 | 1376 | if ( handle.name === 'Y' ) { 1377 | 1378 | _tempQuaternion.setFromAxisAngle( _unitY, Math.atan2( _alignVector.x, _alignVector.z ) ); 1379 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1380 | handle.quaternion.copy( _tempQuaternion ); 1381 | 1382 | } 1383 | 1384 | if ( handle.name === 'Z' ) { 1385 | 1386 | _tempQuaternion.setFromAxisAngle( _unitZ, Math.atan2( _alignVector.y, _alignVector.x ) ); 1387 | _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); 1388 | handle.quaternion.copy( _tempQuaternion ); 1389 | 1390 | } 1391 | 1392 | } 1393 | 1394 | // Hide disabled axes 1395 | handle.visible = handle.visible && ( handle.name.indexOf( 'X' ) === - 1 || this.showX ); 1396 | handle.visible = handle.visible && ( handle.name.indexOf( 'Y' ) === - 1 || this.showY ); 1397 | handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ ); 1398 | handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) ); 1399 | 1400 | // highlight selected axis 1401 | 1402 | handle.material._color = handle.material._color || handle.material.color.clone(); 1403 | handle.material._opacity = handle.material._opacity || handle.material.opacity; 1404 | 1405 | handle.material.color.copy( handle.material._color ); 1406 | handle.material.opacity = handle.material._opacity; 1407 | 1408 | if ( this.enabled && this.axis ) { 1409 | 1410 | if ( handle.name === this.axis ) { 1411 | 1412 | handle.material.color.setHex( 0xffff00 ); 1413 | handle.material.opacity = 1.0; 1414 | 1415 | } else if ( this.axis.split( '' ).some( function ( a ) { 1416 | 1417 | return handle.name === a; 1418 | 1419 | } ) ) { 1420 | 1421 | handle.material.color.setHex( 0xffff00 ); 1422 | handle.material.opacity = 1.0; 1423 | 1424 | } 1425 | 1426 | } 1427 | 1428 | } 1429 | 1430 | super.updateMatrixWorld( force ); 1431 | 1432 | } 1433 | 1434 | } 1435 | 1436 | TransformControlsGizmo.prototype.isTransformControlsGizmo = true; 1437 | 1438 | // 1439 | 1440 | class TransformControlsPlane extends Mesh { 1441 | 1442 | constructor() { 1443 | 1444 | super( 1445 | new PlaneGeometry( 100000, 100000, 2, 2 ), 1446 | new MeshBasicMaterial( { visible: false, wireframe: true, side: DoubleSide, transparent: true, opacity: 0.1, toneMapped: false } ) 1447 | ); 1448 | 1449 | this.type = 'TransformControlsPlane'; 1450 | 1451 | } 1452 | 1453 | updateMatrixWorld( force ) { 1454 | 1455 | let space = this.space; 1456 | 1457 | this.position.copy( this.worldPosition ); 1458 | 1459 | if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation 1460 | 1461 | _v1.copy( _unitX ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1462 | _v2.copy( _unitY ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1463 | _v3.copy( _unitZ ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); 1464 | 1465 | // Align the plane for current transform mode, axis and space. 1466 | 1467 | _alignVector.copy( _v2 ); 1468 | 1469 | switch ( this.mode ) { 1470 | 1471 | case 'translate': 1472 | case 'scale': 1473 | switch ( this.axis ) { 1474 | 1475 | case 'X': 1476 | _alignVector.copy( this.eye ).cross( _v1 ); 1477 | _dirVector.copy( _v1 ).cross( _alignVector ); 1478 | break; 1479 | case 'Y': 1480 | _alignVector.copy( this.eye ).cross( _v2 ); 1481 | _dirVector.copy( _v2 ).cross( _alignVector ); 1482 | break; 1483 | case 'Z': 1484 | _alignVector.copy( this.eye ).cross( _v3 ); 1485 | _dirVector.copy( _v3 ).cross( _alignVector ); 1486 | break; 1487 | case 'XY': 1488 | _dirVector.copy( _v3 ); 1489 | break; 1490 | case 'YZ': 1491 | _dirVector.copy( _v1 ); 1492 | break; 1493 | case 'XZ': 1494 | _alignVector.copy( _v3 ); 1495 | _dirVector.copy( _v2 ); 1496 | break; 1497 | case 'XYZ': 1498 | case 'E': 1499 | _dirVector.set( 0, 0, 0 ); 1500 | break; 1501 | 1502 | } 1503 | 1504 | break; 1505 | case 'rotate': 1506 | default: 1507 | // special case for rotate 1508 | _dirVector.set( 0, 0, 0 ); 1509 | 1510 | } 1511 | 1512 | if ( _dirVector.length() === 0 ) { 1513 | 1514 | // If in rotate mode, make the plane parallel to camera 1515 | this.quaternion.copy( this.cameraQuaternion ); 1516 | 1517 | } else { 1518 | 1519 | _tempMatrix.lookAt( _tempVector.set( 0, 0, 0 ), _dirVector, _alignVector ); 1520 | 1521 | this.quaternion.setFromRotationMatrix( _tempMatrix ); 1522 | 1523 | } 1524 | 1525 | super.updateMatrixWorld( force ); 1526 | 1527 | } 1528 | 1529 | } 1530 | 1531 | TransformControlsPlane.prototype.isTransformControlsPlane = true; 1532 | 1533 | export { TransformControls, TransformControlsGizmo, TransformControlsPlane }; --------------------------------------------------------------------------------