├── 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 };
--------------------------------------------------------------------------------