diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..0016a02d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,72 @@ +{ + "env": { + "browser": true, + "shared-node-browser": true, + "es6": true, + "node": true + }, + "extends": [ + "prettier", + "prettier/react", + "prettier/@typescript-eslint", + "plugin:prettier/recommended", + "plugin:react-hooks/recommended", + "plugin:import/errors", + "plugin:import/warnings" + ], + "plugins": ["@typescript-eslint", "react", "prettier", "react-hooks", "import", "jest"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "ecmaVersion": 2018, + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "curly": ["warn", "multi-line", "consistent"], + "no-console": "off", + "no-empty-pattern": "warn", + "no-duplicate-imports": "error", + "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], + "import/export": "error", + "import/named": "off", + "import/no-named-as-default": "off", + "import/no-named-as-default-member": "off", + "import/namespace": "off", + "import/default": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "no-unused-vars": ["warn"], + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "jest/consistent-test-it": ["error", { "fn": "it", "withinDescribe": "it" }] + }, + "settings": { + "react": { + "version": "detect" + }, + "import/extensions": [".js", ".jsx", ".ts", ".tsx"], + "import/parsers": { + "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] + }, + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], + "paths": ["src"] + } + } + }, + "overrides": [ + { + "files": ["src"], + "parserOptions": { + "project": ["./tsconfig.json", "./storybook/tsconfig.json"] + } + } + ] +} diff --git a/package.json b/package.json index 827acb76..9b25ab14 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "prebuild": "rimraf dist", "build": "rollup -c && npm run copy", "prepare": "npm run build", + "types": "tsc --noEmit --emitDeclarationOnly false", + "eslint": "eslint --cache --fix src/**/*.{ts,tsx}", + "eslint:ci": "eslint src/**/*.{ts,tsx}", "copy": "copyfiles package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"", "test": "echo \"Error: no test specified\"" }, @@ -20,7 +23,11 @@ }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ - "prettier --write" + "prettier --write", + "eslint --fix" + ], + "*.{ts,tsx}": [ + "tsc --noEmit --emitDeclarationOnly false" ] }, "repository": { @@ -43,8 +50,20 @@ "@babel/preset-env": "7.11.0", "@babel/preset-react": "7.10.4", "@babel/preset-typescript": "^7.10.4", + "@rollup/plugin-typescript": "^8.2.0", + "@types/three": "^0.125.3", + "@typescript-eslint/eslint-plugin": "^4.15.2", + "@typescript-eslint/parser": "^4.15.2", + "babel-eslint": "^10.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "copyfiles": "^2.3.0", + "eslint": "^7.7.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^23.20.0", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.20.6", + "eslint-plugin-react-hooks": "^4.1.0", "husky": "^4.2.5", "json": "^10.0.0", "lint-staged": "^10.2.11", @@ -58,7 +77,9 @@ "rollup-plugin-multi-input": "^1.1.1", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-size-snapshot": "^0.12.0", - "three": "^0.126.0" + "three": "^0.126.0", + "tslib": "^2.1.0", + "typescript": "<4.2.0" }, "dependencies": { "@webgpu/glslang": "^0.0.15", diff --git a/rollup.config.js b/rollup.config.js index c4be0cae..a461420d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,7 @@ import json from 'rollup-plugin-json' import glslify from 'rollup-plugin-glslify' import multiInput from 'rollup-plugin-multi-input' import { sizeSnapshot } from 'rollup-plugin-size-snapshot' +import typescript from '@rollup/plugin-typescript' const root = process.platform === 'win32' ? path.resolve('/') : '/' const external = (id) => !id.startsWith('.') && !id.startsWith(root) @@ -31,11 +32,12 @@ export default [ json(), babel(getBabelOptions({ useESModules: true }, '>1%, not dead, not ie 11, not op_mini all')), resolve({ extensions }), + typescript(), ], preserveModules: true, }, { - input: ['src/**/*.js', '!src/index.js'], + input: ['src/**/*.js', 'src/**/*.ts', '!src/index.js'], output: { dir: `dist`, format: 'esm' }, external, plugins: [ @@ -43,10 +45,11 @@ export default [ json(), babel(getBabelOptions({ useESModules: true }, '>1%, not dead, not ie 11, not op_mini all')), resolve({ extensions }), + typescript(), ], }, { - input: ['src/**/*.js', '!src/index.js'], + input: ['src/**/*.js', 'src/**/*.ts', '!src/index.js'], output: { dir: `dist`, format: 'cjs' }, external, plugins: [ @@ -56,12 +59,13 @@ export default [ json(), babel(getBabelOptions({ useESModules: false })), resolve({ extensions }), + typescript(), ], }, { input: `./src/index.js`, output: { file: `dist/index.cjs.js`, format: 'cjs' }, external, - plugins: [json(), babel(getBabelOptions({ useESModules: false })), resolve({ extensions })], + plugins: [json(), babel(getBabelOptions({ useESModules: false })), resolve({ extensions }, typescript())], }, ] diff --git a/src/controls/DeviceOrientationControls.js b/src/controls/DeviceOrientationControls.js deleted file mode 100644 index ce54f017..00000000 --- a/src/controls/DeviceOrientationControls.js +++ /dev/null @@ -1,109 +0,0 @@ -import { Euler, EventDispatcher, MathUtils, Quaternion, Vector3 } from 'three' - -/** - * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html) - */ - -class DeviceOrientationControls extends EventDispatcher { - constructor(object) { - super() - - const scope = this - const changeEvent = { type: 'change' } - const EPS = 0.000001 - - this.object = object - this.object.rotation.reorder('YXZ') - this.enabled = true - this.deviceOrientation = {} - this.screenOrientation = 0 - this.alphaOffset = 0 // radians - - const onDeviceOrientationChangeEvent = (event) => { - scope.deviceOrientation = event - } - - const onScreenOrientationChangeEvent = () => { - scope.screenOrientation = window.orientation || 0 - } - - // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' - - const setObjectQuaternion = (() => { - const zee = new Vector3(0, 0, 1) - const euler = new Euler() - const q0 = new Quaternion() - const q1 = new Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)) // - PI/2 around the x-axis - - return (quaternion, alpha, beta, gamma, orient) => { - euler.set(beta, alpha, -gamma, 'YXZ') // 'ZXY' for the device, but 'YXZ' for us - quaternion.setFromEuler(euler) // orient the device - quaternion.multiply(q1) // camera looks out the back of the device, not the top - quaternion.multiply(q0.setFromAxisAngle(zee, -orient)) // adjust for screen orientation - } - })() - - this.connect = () => { - onScreenOrientationChangeEvent() // run once on load - - // iOS 13+ - - if ( - window.DeviceOrientationEvent !== undefined && - typeof window.DeviceOrientationEvent.requestPermission === 'function' - ) { - window.DeviceOrientationEvent.requestPermission() - .then((response) => { - if (response == 'granted') { - window.addEventListener('orientationchange', onScreenOrientationChangeEvent) - window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent) - } - }) - .catch((error) => { - console.error('THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error) - }) - } else { - window.addEventListener('orientationchange', onScreenOrientationChangeEvent) - window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent) - } - - scope.enabled = true - } - - this.disconnect = () => { - window.removeEventListener('orientationchange', onScreenOrientationChangeEvent) - window.removeEventListener('deviceorientation', onDeviceOrientationChangeEvent) - - scope.enabled = false - } - - this.update = (() => { - const lastQuaternion = new Quaternion() - - return () => { - if (scope.enabled === false) return - - const device = scope.deviceOrientation - - if (device) { - const alpha = device.alpha ? MathUtils.degToRad(device.alpha) + scope.alphaOffset : 0 // Z - const beta = device.beta ? MathUtils.degToRad(device.beta) : 0 // X' - const gamma = device.gamma ? MathUtils.degToRad(device.gamma) : 0 // Y'' - const orient = scope.screenOrientation ? MathUtils.degToRad(scope.screenOrientation) : 0 // O - - setObjectQuaternion(scope.object.quaternion, alpha, beta, gamma, orient) - - if (8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { - lastQuaternion.copy(scope.object.quaternion) - scope.dispatchEvent(changeEvent) - } - } - } - })() - - this.dispose = () => scope.disconnect() - this.connect() - } -} - -export { DeviceOrientationControls } diff --git a/src/controls/DeviceOrientationControls.ts b/src/controls/DeviceOrientationControls.ts new file mode 100644 index 00000000..9ef8de8f --- /dev/null +++ b/src/controls/DeviceOrientationControls.ts @@ -0,0 +1,112 @@ +import { Camera, Euler, EventDispatcher, MathUtils, Quaternion, Vector3 } from 'three' + +/** + * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html) + */ + +class DeviceOrientationControls extends EventDispatcher { + object: Camera + + private changeEvent = { type: 'change' } + private EPS = 0.000001 + + enabled = true + deviceOrientation = { alpha: 0, beta: 0, gamma: 0 } + screenOrientation: string | number = 0 + alphaOffset = 0 // radians + + constructor(object: Camera) { + super() + + this.object = object + this.object.rotation.reorder('YXZ') + + this.connect() + } + + private onDeviceOrientationChangeEvent = (event: any) => { + this.deviceOrientation = event + } + + private onScreenOrientationChangeEvent = () => { + this.screenOrientation = window.orientation || 0 + } + + // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' + + private zee = new Vector3(0, 0, 1) + private euler = new Euler() + private q0 = new Quaternion() + private q1 = new Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)) // - PI/2 around the x-axis + private setObjectQuaternion = ( + quaternion: Quaternion, + alpha: number, + beta: number, + gamma: number, + orient: number, + ) => { + this.euler.set(beta, alpha, -gamma, 'YXZ') // 'ZXY' for the device, but 'YXZ' for us + quaternion.setFromEuler(this.euler) // orient the device + quaternion.multiply(this.q1) // camera looks out the back of the device, not the top + quaternion.multiply(this.q0.setFromAxisAngle(this.zee, -orient)) // adjust for screen orientation + } + + connect = () => { + this.onScreenOrientationChangeEvent() // run once on load + + // iOS 13+ + + if ( + window.DeviceOrientationEvent !== undefined && + typeof window.DeviceOrientationEvent.requestPermission === 'function' + ) { + window.DeviceOrientationEvent.requestPermission() + .then((response) => { + if (response == 'granted') { + window.addEventListener('orientationchange', this.onScreenOrientationChangeEvent) + window.addEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) + } + }) + .catch((error) => { + console.error('THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error) + }) + } else { + window.addEventListener('orientationchange', this.onScreenOrientationChangeEvent) + window.addEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) + } + + this.enabled = true + } + + disconnect = () => { + window.removeEventListener('orientationchange', this.onScreenOrientationChangeEvent) + window.removeEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) + + this.enabled = false + } + + private lastQuaternion = new Quaternion() + update = () => { + if (this.enabled === false) return + + const device = this.deviceOrientation + + if (device) { + const alpha = device.alpha ? MathUtils.degToRad(device.alpha) + this.alphaOffset : 0 // Z + const beta = device.beta ? MathUtils.degToRad(device.beta) : 0 // X' + const gamma = device.gamma ? MathUtils.degToRad(device.gamma) : 0 // Y'' + const orient = this.screenOrientation ? MathUtils.degToRad(this.screenOrientation as number) : 0 // O + + this.setObjectQuaternion(this.object.quaternion, alpha, beta, gamma, orient) + + if (8 * (1 - this.lastQuaternion.dot(this.object.quaternion)) > this.EPS) { + this.lastQuaternion.copy(this.object.quaternion) + this.dispatchEvent(this.changeEvent) + } + } + } + + dispose = () => this.disconnect() +} + +export { DeviceOrientationControls } diff --git a/src/controls/DragControls.js b/src/controls/DragControls.js deleted file mode 100644 index 844d638f..00000000 --- a/src/controls/DragControls.js +++ /dev/null @@ -1,255 +0,0 @@ -import { EventDispatcher, Matrix4, Plane, Raycaster, Vector2, Vector3 } from 'three' - -class DragControls extends EventDispatcher { - constructor(_objects, _camera, _domElement) { - super() - - const _plane = new Plane() - const _raycaster = new Raycaster() - - const _mouse = new Vector2() - const _offset = new Vector3() - const _intersection = new Vector3() - const _worldPosition = new Vector3() - const _inverseMatrix = new Matrix4() - const _intersections = [] - - let _selected = null, - _hovered = null - - // - - const scope = this - - function activate() { - _domElement.addEventListener('pointermove', onPointerMove) - _domElement.addEventListener('pointerdown', onPointerDown) - _domElement.addEventListener('pointerup', onPointerCancel) - _domElement.addEventListener('pointerleave', onPointerCancel) - _domElement.addEventListener('touchmove', onTouchMove) - _domElement.addEventListener('touchstart', onTouchStart) - _domElement.addEventListener('touchend', onTouchEnd) - } - - function deactivate() { - _domElement.removeEventListener('pointermove', onPointerMove) - _domElement.removeEventListener('pointerdown', onPointerDown) - _domElement.removeEventListener('pointerup', onPointerCancel) - _domElement.removeEventListener('pointerleave', onPointerCancel) - _domElement.removeEventListener('touchmove', onTouchMove) - _domElement.removeEventListener('touchstart', onTouchStart) - _domElement.removeEventListener('touchend', onTouchEnd) - - _domElement.style.cursor = '' - } - - function dispose() { - deactivate() - } - - function getObjects() { - return _objects - } - - function onPointerMove(event) { - event.preventDefault() - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseMove(event) - break - - // TODO touch - } - } - - function onMouseMove(event) { - const rect = _domElement.getBoundingClientRect() - - _mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 - _mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 - - _raycaster.setFromCamera(_mouse, _camera) - - if (_selected && scope.enabled) { - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _selected.position.copy(_intersection.sub(_offset).applyMatrix4(_inverseMatrix)) - } - - scope.dispatchEvent({ type: 'drag', object: _selected }) - - return - } - - _intersections.length = 0 - - _raycaster.setFromCamera(_mouse, _camera) - _raycaster.intersectObjects(_objects, true, _intersections) - - if (_intersections.length > 0) { - const object = _intersections[0].object - - _plane.setFromNormalAndCoplanarPoint( - _camera.getWorldDirection(_plane.normal), - _worldPosition.setFromMatrixPosition(object.matrixWorld), - ) - - if (_hovered !== object) { - scope.dispatchEvent({ type: 'hoveron', object }) - - _domElement.style.cursor = 'pointer' - _hovered = object - } - } else { - if (_hovered !== null) { - scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) - - _domElement.style.cursor = 'auto' - _hovered = null - } - } - } - - function onPointerDown(event) { - event.preventDefault() - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseDown(event) - break - - // TODO touch - } - } - - function onMouseDown(event) { - event.preventDefault() - - _intersections.length = 0 - - _raycaster.setFromCamera(_mouse, _camera) - _raycaster.intersectObjects(_objects, true, _intersections) - - if (_intersections.length > 0) { - _selected = scope.transformGroup === true ? _objects[0] : _intersections[0].object - - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _inverseMatrix.copy(_selected.parent.matrixWorld).invert() - _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)) - } - - _domElement.style.cursor = 'move' - - scope.dispatchEvent({ type: 'dragstart', object: _selected }) - } - } - - function onPointerCancel(event) { - event.preventDefault() - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseCancel(event) - break - - // TODO touch - } - } - - function onMouseCancel(event) { - event.preventDefault() - - if (_selected) { - scope.dispatchEvent({ type: 'dragend', object: _selected }) - - _selected = null - } - - _domElement.style.cursor = _hovered ? 'pointer' : 'auto' - } - - function onTouchMove(event) { - event.preventDefault() - event = event.changedTouches[0] - - const rect = _domElement.getBoundingClientRect() - - _mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 - _mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 - - _raycaster.setFromCamera(_mouse, _camera) - - if (_selected && scope.enabled) { - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _selected.position.copy(_intersection.sub(_offset).applyMatrix4(_inverseMatrix)) - } - - scope.dispatchEvent({ type: 'drag', object: _selected }) - - return - } - } - - function onTouchStart(event) { - event.preventDefault() - event = event.changedTouches[0] - - const rect = _domElement.getBoundingClientRect() - - _mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 - _mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 - - _intersections.length = 0 - - _raycaster.setFromCamera(_mouse, _camera) - _raycaster.intersectObjects(_objects, true, _intersections) - - if (_intersections.length > 0) { - _selected = scope.transformGroup === true ? _objects[0] : _intersections[0].object - - _plane.setFromNormalAndCoplanarPoint( - _camera.getWorldDirection(_plane.normal), - _worldPosition.setFromMatrixPosition(_selected.matrixWorld), - ) - - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _inverseMatrix.copy(_selected.parent.matrixWorld).invert() - _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)) - } - - _domElement.style.cursor = 'move' - - scope.dispatchEvent({ type: 'dragstart', object: _selected }) - } - } - - function onTouchEnd(event) { - event.preventDefault() - - if (_selected) { - scope.dispatchEvent({ type: 'dragend', object: _selected }) - - _selected = null - } - - _domElement.style.cursor = 'auto' - } - - activate() - - // API - - this.enabled = true - this.transformGroup = false - - this.activate = activate - this.deactivate = deactivate - this.dispose = dispose - this.getObjects = getObjects - } -} - -export { DragControls } diff --git a/src/controls/DragControls.ts b/src/controls/DragControls.ts new file mode 100644 index 00000000..105d0b89 --- /dev/null +++ b/src/controls/DragControls.ts @@ -0,0 +1,248 @@ +import { Camera, EventDispatcher, Intersection, Matrix4, Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three' + +class DragControls extends EventDispatcher { + enabled = true + transformGroup = false + + private _objects: Object3D[] + private _camera: Camera + private _domElement: HTMLElement + + private _plane = new Plane() + private _raycaster = new Raycaster() + + private _mouse = new Vector2() + private _offset = new Vector3() + private _intersection = new Vector3() + private _worldPosition = new Vector3() + private _inverseMatrix = new Matrix4() + private _intersections: Intersection[] = [] + private _selected: Object3D | null = null + private _hovered: Object3D | null = null + + constructor(_objects: Object3D[], _camera: Camera, _domElement: HTMLElement) { + super() + + this._objects = _objects + this._camera = _camera + this._domElement = _domElement + + this.activate() + } + + activate = () => { + this._domElement.addEventListener('pointermove', this.onPointerMove) + this._domElement.addEventListener('pointerdown', this.onPointerDown) + this._domElement.addEventListener('pointerup', this.onPointerCancel) + this._domElement.addEventListener('pointerleave', this.onPointerCancel) + this._domElement.addEventListener('touchmove', this.onTouchMove) + this._domElement.addEventListener('touchstart', this.onTouchStart) + this._domElement.addEventListener('touchend', this.onTouchEnd) + } + + deactivate = () => { + this._domElement.removeEventListener('pointermove', this.onPointerMove) + this._domElement.removeEventListener('pointerdown', this.onPointerDown) + this._domElement.removeEventListener('pointerup', this.onPointerCancel) + this._domElement.removeEventListener('pointerleave', this.onPointerCancel) + this._domElement.removeEventListener('touchmove', this.onTouchMove) + this._domElement.removeEventListener('touchstart', this.onTouchStart) + this._domElement.removeEventListener('touchend', this.onTouchEnd) + + this._domElement.style.cursor = '' + } + + // TODO: confirm if this can be removed? + dispose = () => this.deactivate() + + getObjects = () => this._objects + + onMouseMove = (event: MouseEvent) => { + const rect = this._domElement.getBoundingClientRect() + + this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 + this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 + + this._raycaster.setFromCamera(this._mouse, this._camera) + + if (this._selected && this.enabled) { + if (this._raycaster.ray.intersectPlane(this._plane, this._intersection)) { + this._selected.position.copy(this._intersection.sub(this._offset).applyMatrix4(this._inverseMatrix)) + } + + this.dispatchEvent({ type: 'drag', object: this._selected }) + + return + } + + this._intersections.length = 0 + + this._raycaster.setFromCamera(this._mouse, this._camera) + this._raycaster.intersectObjects(this._objects, true, this._intersections) + + if (this._intersections.length > 0) { + const object = this._intersections[0].object + + this._plane.setFromNormalAndCoplanarPoint( + this._camera.getWorldDirection(this._plane.normal), + this._worldPosition.setFromMatrixPosition(object.matrixWorld), + ) + + if (this._hovered !== object) { + this.dispatchEvent({ type: 'hoveron', object }) + + this._domElement.style.cursor = 'pointer' + this._hovered = object + } + } else { + if (this._hovered !== null) { + this.dispatchEvent({ type: 'hoveroff', object: this._hovered }) + + this._domElement.style.cursor = 'auto' + this._hovered = null + } + } + } + + onMouseDown = (event: MouseEvent) => { + event.preventDefault() + + this._intersections.length = 0 + + this._raycaster.setFromCamera(this._mouse, this._camera) + this._raycaster.intersectObjects(this._objects, true, this._intersections) + + if (this._intersections.length > 0) { + this._selected = this.transformGroup === true ? this._objects[0] : this._intersections[0].object + + if (this._raycaster.ray.intersectPlane(this._plane, this._intersection) && this._selected.parent) { + this._inverseMatrix.copy(this._selected.parent.matrixWorld).invert() + this._offset.copy(this._intersection).sub(this._worldPosition.setFromMatrixPosition(this._selected.matrixWorld)) + } + + this._domElement.style.cursor = 'move' + + this.dispatchEvent({ type: 'dragstart', object: this._selected }) + } + } + + onMouseCancel = (event: MouseEvent) => { + event.preventDefault() + + if (this._selected) { + this.dispatchEvent({ type: 'dragend', object: this._selected }) + + this._selected = null + } + + this._domElement.style.cursor = this._hovered ? 'pointer' : 'auto' + } + + onPointerMove = (event: PointerEvent) => { + event.preventDefault() + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseMove(event) + break + + // TODO touch + } + } + + onPointerDown = (event: PointerEvent) => { + event.preventDefault() + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseDown(event) + break + + // TODO touch + } + } + + onPointerCancel = (event: PointerEvent) => { + event.preventDefault() + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseCancel(event) + break + + // TODO touch + } + } + + onTouchMove = (event: TouchEvent) => { + event.preventDefault() + const newEvent = event.changedTouches[0] + + const rect = this._domElement.getBoundingClientRect() + + this._mouse.x = ((newEvent.clientX - rect.left) / rect.width) * 2 - 1 + this._mouse.y = -((newEvent.clientY - rect.top) / rect.height) * 2 + 1 + + this._raycaster.setFromCamera(this._mouse, this._camera) + + if (this._selected && this.enabled) { + if (this._raycaster.ray.intersectPlane(this._plane, this._intersection)) { + this._selected.position.copy(this._intersection.sub(this._offset).applyMatrix4(this._inverseMatrix)) + } + + this.dispatchEvent({ type: 'drag', object: this._selected }) + + return + } + } + + onTouchStart = (event: TouchEvent) => { + event.preventDefault() + const newEvent = event.changedTouches[0] + + const rect = this._domElement.getBoundingClientRect() + + this._mouse.x = ((newEvent.clientX - rect.left) / rect.width) * 2 - 1 + this._mouse.y = -((newEvent.clientY - rect.top) / rect.height) * 2 + 1 + + this._intersections.length = 0 + + this._raycaster.setFromCamera(this._mouse, this._camera) + this._raycaster.intersectObjects(this._objects, true, this._intersections) + + if (this._intersections.length > 0) { + this._selected = this.transformGroup === true ? this._objects[0] : this._intersections[0].object + + this._plane.setFromNormalAndCoplanarPoint( + this._camera.getWorldDirection(this._plane.normal), + this._worldPosition.setFromMatrixPosition(this._selected.matrixWorld), + ) + + if (this._raycaster.ray.intersectPlane(this._plane, this._intersection) && this._selected.parent) { + this._inverseMatrix.copy(this._selected.parent.matrixWorld).invert() + this._offset.copy(this._intersection).sub(this._worldPosition.setFromMatrixPosition(this._selected.matrixWorld)) + } + + this._domElement.style.cursor = 'move' + + this.dispatchEvent({ type: 'dragstart', object: this._selected }) + } + } + + onTouchEnd = (event: TouchEvent) => { + event.preventDefault() + + if (this._selected) { + this.dispatchEvent({ type: 'dragend', object: this._selected }) + + this._selected = null + } + + this._domElement.style.cursor = 'auto' + } +} + +export { DragControls } diff --git a/src/controls/FirstPersonControls.js b/src/controls/FirstPersonControls.js deleted file mode 100644 index bdb8d948..00000000 --- a/src/controls/FirstPersonControls.js +++ /dev/null @@ -1,314 +0,0 @@ -import { MathUtils, Spherical, Vector3, EventDispatcher } from 'three' - -class FirstPersonControls extends EventDispatcher { - constructor(object, domElement) { - super() - - if (domElement === undefined) { - console.warn('THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.') - domElement = document - } - - this.object = object - this.domElement = domElement - - // API - - this.enabled = true - - this.movementSpeed = 1.0 - this.lookSpeed = 0.005 - - this.lookVertical = true - this.autoForward = false - - this.activeLook = true - - this.heightSpeed = false - this.heightCoef = 1.0 - this.heightMin = 0.0 - this.heightMax = 1.0 - - this.constrainVertical = false - this.verticalMin = 0 - this.verticalMax = Math.PI - - this.mouseDragOn = false - - // internals - - this.autoSpeedFactor = 0.0 - - this.mouseX = 0 - this.mouseY = 0 - - this.moveForward = false - this.moveBackward = false - this.moveLeft = false - this.moveRight = false - - this.viewHalfX = 0 - this.viewHalfY = 0 - - // private variables - - let lat = 0 - let lon = 0 - - const lookDirection = new Vector3() - const spherical = new Spherical() - const target = new Vector3() - - // - - if (this.domElement !== document) { - this.domElement.setAttribute('tabindex', -1) - } - - // - - this.handleResize = function () { - if (this.domElement === document) { - this.viewHalfX = window.innerWidth / 2 - this.viewHalfY = window.innerHeight / 2 - } else { - this.viewHalfX = this.domElement.offsetWidth / 2 - this.viewHalfY = this.domElement.offsetHeight / 2 - } - } - - this.onMouseDown = function (event) { - if (this.domElement !== document) { - this.domElement.focus() - } - - event.preventDefault() - - if (this.activeLook) { - switch (event.button) { - case 0: - this.moveForward = true - break - case 2: - this.moveBackward = true - break - } - } - - this.mouseDragOn = true - } - - this.onMouseUp = function (event) { - event.preventDefault() - - if (this.activeLook) { - switch (event.button) { - case 0: - this.moveForward = false - break - case 2: - this.moveBackward = false - break - } - } - - this.mouseDragOn = false - } - - this.onMouseMove = function (event) { - if (this.domElement === document) { - this.mouseX = event.pageX - this.viewHalfX - this.mouseY = event.pageY - this.viewHalfY - } else { - this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX - this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY - } - } - - this.onKeyDown = function (event) { - //event.preventDefault(); - - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - this.moveForward = true - break - - case 'ArrowLeft': - case 'KeyA': - this.moveLeft = true - break - - case 'ArrowDown': - case 'KeyS': - this.moveBackward = true - break - - case 'ArrowRight': - case 'KeyD': - this.moveRight = true - break - - case 'KeyR': - this.moveUp = true - break - case 'KeyF': - this.moveDown = true - break - } - } - - this.onKeyUp = function (event) { - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - this.moveForward = false - break - - case 'ArrowLeft': - case 'KeyA': - this.moveLeft = false - break - - case 'ArrowDown': - case 'KeyS': - this.moveBackward = false - break - - case 'ArrowRight': - case 'KeyD': - this.moveRight = false - break - - case 'KeyR': - this.moveUp = false - break - case 'KeyF': - this.moveDown = false - break - } - } - - this.lookAt = function (x, y, z) { - if (x.isVector3) { - target.copy(x) - } else { - target.set(x, y, z) - } - - this.object.lookAt(target) - - setOrientation(this) - - return this - } - - this.update = (() => { - const targetPosition = new Vector3() - - return function update(delta) { - if (this.enabled === false) return - - if (this.heightSpeed) { - const y = MathUtils.clamp(this.object.position.y, this.heightMin, this.heightMax) - const heightDelta = y - this.heightMin - - this.autoSpeedFactor = delta * (heightDelta * this.heightCoef) - } else { - this.autoSpeedFactor = 0.0 - } - - const actualMoveSpeed = delta * this.movementSpeed - - if (this.moveForward || (this.autoForward && !this.moveBackward)) - this.object.translateZ(-(actualMoveSpeed + this.autoSpeedFactor)) - if (this.moveBackward) this.object.translateZ(actualMoveSpeed) - - if (this.moveLeft) this.object.translateX(-actualMoveSpeed) - if (this.moveRight) this.object.translateX(actualMoveSpeed) - - if (this.moveUp) this.object.translateY(actualMoveSpeed) - if (this.moveDown) this.object.translateY(-actualMoveSpeed) - - let actualLookSpeed = delta * this.lookSpeed - - if (!this.activeLook) { - actualLookSpeed = 0 - } - - let verticalLookRatio = 1 - - if (this.constrainVertical) { - verticalLookRatio = Math.PI / (this.verticalMax - this.verticalMin) - } - - lon -= this.mouseX * actualLookSpeed - if (this.lookVertical) lat -= this.mouseY * actualLookSpeed * verticalLookRatio - - lat = Math.max(-85, Math.min(85, lat)) - - let phi = MathUtils.degToRad(90 - lat) - const theta = MathUtils.degToRad(lon) - - if (this.constrainVertical) { - phi = MathUtils.mapLinear(phi, 0, Math.PI, this.verticalMin, this.verticalMax) - } - - const position = this.object.position - - targetPosition.setFromSphericalCoords(1, phi, theta).add(position) - - this.object.lookAt(targetPosition) - } - })() - - function contextmenu(event) { - event.preventDefault() - } - - this.dispose = function () { - this.domElement.removeEventListener('contextmenu', contextmenu) - this.domElement.removeEventListener('mousedown', _onMouseDown) - this.domElement.removeEventListener('mousemove', _onMouseMove) - this.domElement.removeEventListener('mouseup', _onMouseUp) - - window.removeEventListener('keydown', _onKeyDown) - window.removeEventListener('keyup', _onKeyUp) - } - - var _onMouseMove = bind(this, this.onMouseMove) - var _onMouseDown = bind(this, this.onMouseDown) - var _onMouseUp = bind(this, this.onMouseUp) - var _onKeyDown = bind(this, this.onKeyDown) - var _onKeyUp = bind(this, this.onKeyUp) - - this.domElement.addEventListener('contextmenu', contextmenu) - this.domElement.addEventListener('mousemove', _onMouseMove) - this.domElement.addEventListener('mousedown', _onMouseDown) - this.domElement.addEventListener('mouseup', _onMouseUp) - - window.addEventListener('keydown', _onKeyDown) - window.addEventListener('keyup', _onKeyUp) - - function bind(scope, fn) { - return function () { - fn.apply(scope, arguments) - } - } - - function setOrientation(controls) { - const quaternion = controls.object.quaternion - - lookDirection.set(0, 0, -1).applyQuaternion(quaternion) - spherical.setFromVector3(lookDirection) - lat = 90 - MathUtils.radToDeg(spherical.phi) - lon = MathUtils.radToDeg(spherical.theta) - } - - this.handleResize() - - setOrientation(this) - } -} - -export { FirstPersonControls } diff --git a/src/controls/FirstPersonControls.ts b/src/controls/FirstPersonControls.ts new file mode 100644 index 00000000..d0d026b6 --- /dev/null +++ b/src/controls/FirstPersonControls.ts @@ -0,0 +1,295 @@ +import { MathUtils, Spherical, Vector3, EventDispatcher, Camera } from 'three' + +class FirstPersonControls extends EventDispatcher { + object: Camera + domElement: HTMLElement | Document + + enabled = true + + movementSpeed = 1.0 + lookSpeed = 0.005 + + lookVertical = true + autoForward = false + + activeLook = true + + heightSpeed = false + heightCoef = 1.0 + heightMin = 0.0 + heightMax = 1.0 + + constrainVertical = false + verticalMin = 0 + verticalMax = Math.PI + + mouseDragOn = false + + // internals + + autoSpeedFactor = 0.0 + + mouseX = 0 + mouseY = 0 + + moveForward = false + moveBackward = false + moveLeft = false + moveRight = false + moveUp = false + moveDown = false + + viewHalfX = 0 + viewHalfY = 0 + + private lat = 0 + private lon = 0 + + private lookDirection = new Vector3() + private spherical = new Spherical() + private target = new Vector3() + + constructor(object: Camera, domElement: HTMLElement | Document) { + super() + + if (domElement === undefined) { + console.warn('THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.') + domElement = document + } + + this.object = object + this.domElement = domElement + + if (this.domElement instanceof HTMLElement) { + this.domElement.setAttribute('tabindex', '-1') + } + + this.handleResize() + + this.setOrientation(this) + + this.domElement.addEventListener('contextmenu', this.contextmenu) + ;(this.domElement as HTMLElement).addEventListener('mousemove', this.onMouseMove) + ;(this.domElement as HTMLElement).addEventListener('mousedown', this.onMouseDown) + ;(this.domElement as HTMLElement).addEventListener('mouseup', this.onMouseUp) + + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + } + + dispose = () => { + this.domElement.removeEventListener('contextmenu', this.contextmenu) + ;(this.domElement as HTMLElement).removeEventListener('mousedown', this.onMouseDown) + ;(this.domElement as HTMLElement).removeEventListener('mousemove', this.onMouseMove) + ;(this.domElement as HTMLElement).removeEventListener('mouseup', this.onMouseUp) + + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) + } + + handleResize = () => { + if (this.domElement instanceof Document) { + this.viewHalfX = window.innerWidth / 2 + this.viewHalfY = window.innerHeight / 2 + } else { + this.viewHalfX = this.domElement.offsetWidth / 2 + this.viewHalfY = this.domElement.offsetHeight / 2 + } + } + + onMouseDown = (event: MouseEvent) => { + if (this.domElement instanceof HTMLElement) { + this.domElement.focus() + } + + event.preventDefault() + + if (this.activeLook) { + switch (event.button) { + case 0: + this.moveForward = true + break + case 2: + this.moveBackward = true + break + } + } + + this.mouseDragOn = true + } + + onMouseUp = (event: MouseEvent) => { + event.preventDefault() + + if (this.activeLook) { + switch (event.button) { + case 0: + this.moveForward = false + break + case 2: + this.moveBackward = false + break + } + } + + this.mouseDragOn = false + } + + onMouseMove = (event: MouseEvent) => { + if (this.domElement instanceof Document) { + this.mouseX = event.pageX - this.viewHalfX + this.mouseY = event.pageY - this.viewHalfY + } else { + this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX + this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY + } + } + + onKeyDown = (event: KeyboardEvent) => { + //event.preventDefault(); + + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + this.moveForward = true + break + + case 'ArrowLeft': + case 'KeyA': + this.moveLeft = true + break + + case 'ArrowDown': + case 'KeyS': + this.moveBackward = true + break + + case 'ArrowRight': + case 'KeyD': + this.moveRight = true + break + + case 'KeyR': + this.moveUp = true + break + case 'KeyF': + this.moveDown = true + break + } + } + + onKeyUp = (event: KeyboardEvent) => { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + this.moveForward = false + break + + case 'ArrowLeft': + case 'KeyA': + this.moveLeft = false + break + + case 'ArrowDown': + case 'KeyS': + this.moveBackward = false + break + + case 'ArrowRight': + case 'KeyD': + this.moveRight = false + break + + case 'KeyR': + this.moveUp = false + break + case 'KeyF': + this.moveDown = false + break + } + } + + lookAt = (x: Vector3 | number, y?: number, z?: number) => { + if (x instanceof Vector3) { + this.target.copy(x) + } else if (y && z) { + this.target.set(x, y, z) + } + + this.object.lookAt(this.target) + + this.setOrientation(this) + + return this + } + + private targetPosition = new Vector3() + update = (delta: number) => { + if (this.enabled === false) return + + if (this.heightSpeed) { + const y = MathUtils.clamp(this.object.position.y, this.heightMin, this.heightMax) + const heightDelta = y - this.heightMin + + this.autoSpeedFactor = delta * (heightDelta * this.heightCoef) + } else { + this.autoSpeedFactor = 0.0 + } + + const actualMoveSpeed = delta * this.movementSpeed + + if (this.moveForward || (this.autoForward && !this.moveBackward)) { + this.object.translateZ(-(actualMoveSpeed + this.autoSpeedFactor)) + } + if (this.moveBackward) this.object.translateZ(actualMoveSpeed) + + if (this.moveLeft) this.object.translateX(-actualMoveSpeed) + if (this.moveRight) this.object.translateX(actualMoveSpeed) + + if (this.moveUp) this.object.translateY(actualMoveSpeed) + if (this.moveDown) this.object.translateY(-actualMoveSpeed) + + let actualLookSpeed = delta * this.lookSpeed + + if (!this.activeLook) { + actualLookSpeed = 0 + } + + let verticalLookRatio = 1 + + if (this.constrainVertical) { + verticalLookRatio = Math.PI / (this.verticalMax - this.verticalMin) + } + + this.lon -= this.mouseX * actualLookSpeed + if (this.lookVertical) this.lat -= this.mouseY * actualLookSpeed * verticalLookRatio + + this.lat = Math.max(-85, Math.min(85, this.lat)) + + let phi = MathUtils.degToRad(90 - this.lat) + const theta = MathUtils.degToRad(this.lon) + + if (this.constrainVertical) { + phi = MathUtils.mapLinear(phi, 0, Math.PI, this.verticalMin, this.verticalMax) + } + + const position = this.object.position + + this.targetPosition.setFromSphericalCoords(1, phi, theta).add(position) + + this.object.lookAt(this.targetPosition) + } + + contextmenu = (event: Event) => event.preventDefault() + + setOrientation = (controls: FirstPersonControls) => { + const quaternion = controls.object.quaternion + + this.lookDirection.set(0, 0, -1).applyQuaternion(quaternion) + this.spherical.setFromVector3(this.lookDirection) + this.lat = 90 - MathUtils.radToDeg(this.spherical.phi) + this.lon = MathUtils.radToDeg(this.spherical.theta) + } +} + +export { FirstPersonControls } diff --git a/src/controls/FlyControls.js b/src/controls/FlyControls.js deleted file mode 100644 index 7c486eca..00000000 --- a/src/controls/FlyControls.js +++ /dev/null @@ -1,327 +0,0 @@ -import { EventDispatcher, Quaternion, Vector3 } from 'three' - -class FlyControls extends EventDispatcher { - constructor(object, domElement) { - super() - - if (domElement === undefined) { - console.warn('THREE.FlyControls: The second parameter "domElement" is now mandatory.') - domElement = document - } - - this.object = object - this.domElement = domElement - - if (domElement) this.domElement.setAttribute('tabindex', -1) - - // API - - this.movementSpeed = 1.0 - this.rollSpeed = 0.005 - - this.dragToLook = false - this.autoForward = false - - // disable default target object behavior - - // internals - - const scope = this - const changeEvent = { type: 'change' } - const EPS = 0.000001 - - this.tmpQuaternion = new Quaternion() - - this.mouseStatus = 0 - - this.moveState = { - up: 0, - down: 0, - left: 0, - right: 0, - forward: 0, - back: 0, - pitchUp: 0, - pitchDown: 0, - yawLeft: 0, - yawRight: 0, - rollLeft: 0, - rollRight: 0, - } - this.moveVector = new Vector3(0, 0, 0) - this.rotationVector = new Vector3(0, 0, 0) - - this.keydown = function (event) { - if (event.altKey) { - return - } - - //event.preventDefault(); - - switch (event.code) { - case 'ShiftLeft': - case 'ShiftRight': - this.movementSpeedMultiplier = 0.1 - break - - case 'KeyW': - this.moveState.forward = 1 - break - case 'KeyS': - this.moveState.back = 1 - break - - case 'KeyA': - this.moveState.left = 1 - break - case 'KeyD': - this.moveState.right = 1 - break - - case 'KeyR': - this.moveState.up = 1 - break - case 'KeyF': - this.moveState.down = 1 - break - - case 'ArrowUp': - this.moveState.pitchUp = 1 - break - case 'ArrowDown': - this.moveState.pitchDown = 1 - break - - case 'ArrowLeft': - this.moveState.yawLeft = 1 - break - case 'ArrowRight': - this.moveState.yawRight = 1 - break - - case 'KeyQ': - this.moveState.rollLeft = 1 - break - case 'KeyE': - this.moveState.rollRight = 1 - break - } - - this.updateMovementVector() - this.updateRotationVector() - } - - this.keyup = function (event) { - switch (event.code) { - case 'ShiftLeft': - case 'ShiftRight': - this.movementSpeedMultiplier = 1 - break - - case 'KeyW': - this.moveState.forward = 0 - break - case 'KeyS': - this.moveState.back = 0 - break - - case 'KeyA': - this.moveState.left = 0 - break - case 'KeyD': - this.moveState.right = 0 - break - - case 'KeyR': - this.moveState.up = 0 - break - case 'KeyF': - this.moveState.down = 0 - break - - case 'ArrowUp': - this.moveState.pitchUp = 0 - break - case 'ArrowDown': - this.moveState.pitchDown = 0 - break - - case 'ArrowLeft': - this.moveState.yawLeft = 0 - break - case 'ArrowRight': - this.moveState.yawRight = 0 - break - - case 'KeyQ': - this.moveState.rollLeft = 0 - break - case 'KeyE': - this.moveState.rollRight = 0 - break - } - - this.updateMovementVector() - this.updateRotationVector() - } - - this.mousedown = function (event) { - if (this.domElement !== document) { - this.domElement.focus() - } - - event.preventDefault() - - if (this.dragToLook) { - this.mouseStatus++ - } else { - switch (event.button) { - case 0: - this.moveState.forward = 1 - break - case 2: - this.moveState.back = 1 - break - } - - this.updateMovementVector() - } - } - - this.mousemove = function (event) { - if (!this.dragToLook || this.mouseStatus > 0) { - const container = this.getContainerDimensions() - const halfWidth = container.size[0] / 2 - const halfHeight = container.size[1] / 2 - - this.moveState.yawLeft = -(event.pageX - container.offset[0] - halfWidth) / halfWidth - this.moveState.pitchDown = (event.pageY - container.offset[1] - halfHeight) / halfHeight - - this.updateRotationVector() - } - } - - this.mouseup = function (event) { - event.preventDefault() - - if (this.dragToLook) { - this.mouseStatus-- - - this.moveState.yawLeft = this.moveState.pitchDown = 0 - } else { - switch (event.button) { - case 0: - this.moveState.forward = 0 - break - case 2: - this.moveState.back = 0 - break - } - - this.updateMovementVector() - } - - this.updateRotationVector() - } - - this.update = (() => { - const lastQuaternion = new Quaternion() - const lastPosition = new Vector3() - - return (delta) => { - const moveMult = delta * scope.movementSpeed - const rotMult = delta * scope.rollSpeed - - scope.object.translateX(scope.moveVector.x * moveMult) - scope.object.translateY(scope.moveVector.y * moveMult) - scope.object.translateZ(scope.moveVector.z * moveMult) - - scope.tmpQuaternion - .set(scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1) - .normalize() - scope.object.quaternion.multiply(scope.tmpQuaternion) - - if ( - lastPosition.distanceToSquared(scope.object.position) > EPS || - 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS - ) { - scope.dispatchEvent(changeEvent) - lastQuaternion.copy(scope.object.quaternion) - lastPosition.copy(scope.object.position) - } - } - })() - - this.updateMovementVector = function () { - const forward = this.moveState.forward || (this.autoForward && !this.moveState.back) ? 1 : 0 - - this.moveVector.x = -this.moveState.left + this.moveState.right - this.moveVector.y = -this.moveState.down + this.moveState.up - this.moveVector.z = -forward + this.moveState.back - - //console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] ); - } - - this.updateRotationVector = function () { - this.rotationVector.x = -this.moveState.pitchDown + this.moveState.pitchUp - this.rotationVector.y = -this.moveState.yawRight + this.moveState.yawLeft - this.rotationVector.z = -this.moveState.rollRight + this.moveState.rollLeft - - //console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] ); - } - - this.getContainerDimensions = function () { - if (this.domElement != document) { - return { - size: [this.domElement.offsetWidth, this.domElement.offsetHeight], - offset: [this.domElement.offsetLeft, this.domElement.offsetTop], - } - } else { - return { - size: [window.innerWidth, window.innerHeight], - offset: [0, 0], - } - } - } - - function bind(scope, fn) { - return function () { - fn.apply(scope, arguments) - } - } - - function contextmenu(event) { - event.preventDefault() - } - - this.dispose = function () { - this.domElement.removeEventListener('contextmenu', contextmenu) - this.domElement.removeEventListener('mousedown', _mousedown) - this.domElement.removeEventListener('mousemove', _mousemove) - this.domElement.removeEventListener('mouseup', _mouseup) - - window.removeEventListener('keydown', _keydown) - window.removeEventListener('keyup', _keyup) - } - - var _mousemove = bind(this, this.mousemove) - var _mousedown = bind(this, this.mousedown) - var _mouseup = bind(this, this.mouseup) - var _keydown = bind(this, this.keydown) - var _keyup = bind(this, this.keyup) - - this.domElement.addEventListener('contextmenu', contextmenu) - - this.domElement.addEventListener('mousemove', _mousemove) - this.domElement.addEventListener('mousedown', _mousedown) - this.domElement.addEventListener('mouseup', _mouseup) - - window.addEventListener('keydown', _keydown) - window.addEventListener('keyup', _keyup) - - this.updateMovementVector() - this.updateRotationVector() - } -} - -export { FlyControls } diff --git a/src/controls/FlyControls.ts b/src/controls/FlyControls.ts new file mode 100644 index 00000000..2851c456 --- /dev/null +++ b/src/controls/FlyControls.ts @@ -0,0 +1,323 @@ +import { Camera, EventDispatcher, Quaternion, Vector3 } from 'three' + +function contextmenu(event: Event) { + event.preventDefault() +} + +class FlyControls extends EventDispatcher { + object: Camera + domElement: HTMLElement | Document + + movementSpeed = 1.0 + rollSpeed = 0.005 + + dragToLook = false + autoForward = false + + scope = this + changeEvent = { type: 'change' } + EPS = 0.000001 + + tmpQuaternion = new Quaternion() + + mouseStatus = 0 + + movementSpeedMultiplier = 1 + + moveState = { + up: 0, + down: 0, + left: 0, + right: 0, + forward: 0, + back: 0, + pitchUp: 0, + pitchDown: 0, + yawLeft: 0, + yawRight: 0, + rollLeft: 0, + rollRight: 0, + } + moveVector = new Vector3(0, 0, 0) + rotationVector = new Vector3(0, 0, 0) + + constructor(object: Camera, domElement: HTMLElement | Document) { + super() + + if (domElement === undefined) { + console.warn('THREE.FlyControls: The second parameter "domElement" is now mandatory.') + domElement = document + } + + this.object = object + this.domElement = domElement + + if (domElement && !(domElement instanceof Document)) { + domElement.setAttribute('tabindex', -1 as any) + } + + // API + + // disable default target object behavior + + // internals + + this.domElement.addEventListener('contextmenu', contextmenu) + + this.domElement.addEventListener('mousemove', this.mousemove as any) + this.domElement.addEventListener('mousedown', this.mousedown as any) + this.domElement.addEventListener('mouseup', this.mouseup as any) + + window.addEventListener('keydown', this.keydown) + window.addEventListener('keyup', this.keyup) + + this.updateMovementVector() + this.updateRotationVector() + } + + keydown = (event: KeyboardEvent) => { + if (event.altKey) { + return + } + + //event.preventDefault(); + + switch (event.code) { + case 'ShiftLeft': + case 'ShiftRight': + this.movementSpeedMultiplier = 0.1 + break + + case 'KeyW': + this.moveState.forward = 1 + break + case 'KeyS': + this.moveState.back = 1 + break + + case 'KeyA': + this.moveState.left = 1 + break + case 'KeyD': + this.moveState.right = 1 + break + + case 'KeyR': + this.moveState.up = 1 + break + case 'KeyF': + this.moveState.down = 1 + break + + case 'ArrowUp': + this.moveState.pitchUp = 1 + break + case 'ArrowDown': + this.moveState.pitchDown = 1 + break + + case 'ArrowLeft': + this.moveState.yawLeft = 1 + break + case 'ArrowRight': + this.moveState.yawRight = 1 + break + + case 'KeyQ': + this.moveState.rollLeft = 1 + break + case 'KeyE': + this.moveState.rollRight = 1 + break + } + + this.updateMovementVector() + this.updateRotationVector() + } + + keyup = (event: KeyboardEvent) => { + switch (event.code) { + case 'ShiftLeft': + case 'ShiftRight': + this.movementSpeedMultiplier = 1 + break + + case 'KeyW': + this.moveState.forward = 0 + break + case 'KeyS': + this.moveState.back = 0 + break + + case 'KeyA': + this.moveState.left = 0 + break + case 'KeyD': + this.moveState.right = 0 + break + + case 'KeyR': + this.moveState.up = 0 + break + case 'KeyF': + this.moveState.down = 0 + break + + case 'ArrowUp': + this.moveState.pitchUp = 0 + break + case 'ArrowDown': + this.moveState.pitchDown = 0 + break + + case 'ArrowLeft': + this.moveState.yawLeft = 0 + break + case 'ArrowRight': + this.moveState.yawRight = 0 + break + + case 'KeyQ': + this.moveState.rollLeft = 0 + break + case 'KeyE': + this.moveState.rollRight = 0 + break + } + + this.updateMovementVector() + this.updateRotationVector() + } + + mousedown = (event: MouseEvent) => { + if (this.domElement !== document && !(this.domElement instanceof Document)) { + this.domElement.focus() + } + + event.preventDefault() + + if (this.dragToLook) { + this.mouseStatus++ + } else { + switch (event.button) { + case 0: + this.moveState.forward = 1 + break + case 2: + this.moveState.back = 1 + break + } + + this.updateMovementVector() + } + } + + mousemove = (event: MouseEvent) => { + if (!this.dragToLook || this.mouseStatus > 0) { + const container = this.getContainerDimensions() + const halfWidth = container.size[0] / 2 + const halfHeight = container.size[1] / 2 + + this.moveState.yawLeft = -(event.pageX - container.offset[0] - halfWidth) / halfWidth + this.moveState.pitchDown = (event.pageY - container.offset[1] - halfHeight) / halfHeight + + this.updateRotationVector() + } + } + + mouseup = (event: MouseEvent) => { + event.preventDefault() + + if (this.dragToLook) { + this.mouseStatus-- + + this.moveState.yawLeft = this.moveState.pitchDown = 0 + } else { + switch (event.button) { + case 0: + this.moveState.forward = 0 + break + case 2: + this.moveState.back = 0 + break + } + + this.updateMovementVector() + } + + this.updateRotationVector() + } + + update = (() => { + const lastQuaternion = new Quaternion() + const lastPosition = new Vector3() + const scope = this + + return (delta: number) => { + const moveMult = delta * scope.movementSpeed + const rotMult = delta * scope.rollSpeed + + scope.object.translateX(scope.moveVector.x * moveMult) + scope.object.translateY(scope.moveVector.y * moveMult) + scope.object.translateZ(scope.moveVector.z * moveMult) + + scope.tmpQuaternion + .set(scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1) + .normalize() + scope.object.quaternion.multiply(scope.tmpQuaternion) + + if ( + lastPosition.distanceToSquared(scope.object.position) > this.EPS || + 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > this.EPS + ) { + scope.dispatchEvent(this.changeEvent) + lastQuaternion.copy(scope.object.quaternion) + lastPosition.copy(scope.object.position) + } + } + })() + + updateMovementVector = () => { + const forward = this.moveState.forward || (this.autoForward && !this.moveState.back) ? 1 : 0 + + this.moveVector.x = -this.moveState.left + this.moveState.right + this.moveVector.y = -this.moveState.down + this.moveState.up + this.moveVector.z = -forward + this.moveState.back + + //console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] ); + } + + updateRotationVector = () => { + this.rotationVector.x = -this.moveState.pitchDown + this.moveState.pitchUp + this.rotationVector.y = -this.moveState.yawRight + this.moveState.yawLeft + this.rotationVector.z = -this.moveState.rollRight + this.moveState.rollLeft + + //console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] ); + } + + getContainerDimensions = () => { + if (this.domElement != document && !(this.domElement instanceof Document)) { + return { + size: [this.domElement.offsetWidth, this.domElement.offsetHeight], + offset: [this.domElement.offsetLeft, this.domElement.offsetTop], + } + } else { + return { + size: [window.innerWidth, window.innerHeight], + offset: [0, 0], + } + } + } + + dispose = () => { + this.domElement.removeEventListener('contextmenu', contextmenu) + this.domElement.removeEventListener('mousemove', this.mousemove as any) + this.domElement.removeEventListener('mousedown', this.mousedown as any) + this.domElement.removeEventListener('mouseup', this.mouseup as any) + + window.removeEventListener('keydown', this.keydown) + window.removeEventListener('keyup', this.keyup) + } +} + +export { FlyControls } diff --git a/src/controls/OrbitControls.js b/src/controls/OrbitControls.ts similarity index 72% rename from src/controls/OrbitControls.js rename to src/controls/OrbitControls.ts index 4c6892f7..7df9528d 100644 --- a/src/controls/OrbitControls.js +++ b/src/controls/OrbitControls.ts @@ -1,4 +1,16 @@ -import { EventDispatcher, MOUSE, Quaternion, Spherical, TOUCH, Vector2, Vector3 } from 'three' +import { + Camera, + EventDispatcher, + Matrix4, + MOUSE, + OrthographicCamera, + PerspectiveCamera, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, +} from 'three' // This set of controls performs orbiting, dollying (zooming), and panning. // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). @@ -8,72 +20,80 @@ import { EventDispatcher, MOUSE, Quaternion, Spherical, TOUCH, Vector2, Vector3 // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move class OrbitControls extends EventDispatcher { - constructor(object, domElement) { + object: Camera + domElement: HTMLElement | undefined + // Set to false to disable this control + enabled = true + // "target" sets the location of focus, where the object orbits around + target = new Vector3() + // How far you can dolly in and out ( PerspectiveCamera only ) + minDistance = 0 + maxDistance = Infinity + // How far you can zoom in and out ( OrthographicCamera only ) + minZoom = 0 + maxZoom = Infinity + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + minPolarAngle = 0 // radians + maxPolarAngle = Math.PI // radians + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) + minAzimuthAngle = -Infinity // radians + maxAzimuthAngle = Infinity // radians + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + enableDamping = false + dampingFactor = 0.05 + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + enableZoom = true + zoomSpeed = 1.0 + // Set to false to disable rotating + enableRotate = true + rotateSpeed = 1.0 + // Set to false to disable panning + enablePan = true + panSpeed = 1.0 + screenSpacePanning = true // if false, pan orthogonal to world-space direction camera.up + keyPanSpeed = 7.0 // pixels moved per arrow key push + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + autoRotate = false + autoRotateSpeed = 2.0 // 30 seconds per orbit when fps is 60 + // The four arrow keys + keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 } + // Mouse buttons + mouseButtons = { + LEFT: MOUSE.ROTATE, + MIDDLE: MOUSE.DOLLY, + RIGHT: MOUSE.PAN, + } + // Touch fingers + touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN } + target0: Vector3 + position0: Vector3 + zoom0: number + // the target DOM element for key events + _domElementKeyEvents: any = null + + getPolarAngle: () => number + getAzimuthalAngle: () => number + listenToKeyEvents: (domElement: any) => void + saveState: () => void + reset: () => void + update: () => void + connect: (domElement: HTMLElement) => void + dispose: () => void + + constructor(object: Camera, domElement?: HTMLElement) { super() - if (domElement === undefined) - console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.') - if (domElement === document) - console.error( - 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.', - ) - this.object = object this.domElement = domElement - // Set to false to disable this control - this.enabled = true - // "target" sets the location of focus, where the object orbits around - this.target = new Vector3() - // How far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0 - this.maxDistance = Infinity - // How far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0 - this.maxZoom = Infinity - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0 // radians - this.maxPolarAngle = Math.PI // radians - // How far you can orbit horizontally, upper and lower limits. - // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) - this.minAzimuthAngle = -Infinity // radians - this.maxAzimuthAngle = Infinity // radians - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false - this.dampingFactor = 0.05 - // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. - // Set to false to disable zooming - this.enableZoom = true - this.zoomSpeed = 1.0 - // Set to false to disable rotating - this.enableRotate = true - this.rotateSpeed = 1.0 - // Set to false to disable panning - this.enablePan = true - this.panSpeed = 1.0 - this.screenSpacePanning = true // if false, pan orthogonal to world-space direction camera.up - this.keyPanSpeed = 7.0 // pixels moved per arrow key push - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - this.autoRotate = false - this.autoRotateSpeed = 2.0 // 30 seconds per orbit when fps is 60 - // The four arrow keys - this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 } - // Mouse buttons - this.mouseButtons = { - LEFT: MOUSE.ROTATE, - MIDDLE: MOUSE.DOLLY, - RIGHT: MOUSE.PAN, - } - // Touch fingers - this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN } // for reset this.target0 = this.target.clone() this.position0 = this.object.position.clone() - this.zoom0 = this.object.zoom - // the target DOM element for key events - this._domElementKeyEvents = null + this.zoom0 = this.object instanceof PerspectiveCamera ? this.object.zoom : 1 // // public methods @@ -83,7 +103,7 @@ class OrbitControls extends EventDispatcher { this.getAzimuthalAngle = () => spherical.theta - this.listenToKeyEvents = function (domElement) { + this.listenToKeyEvents = (domElement) => { domElement.addEventListener('keydown', onKeyDown) this._domElementKeyEvents = domElement } @@ -91,15 +111,17 @@ class OrbitControls extends EventDispatcher { this.saveState = () => { scope.target0.copy(scope.target) scope.position0.copy(scope.object.position) - scope.zoom0 = scope.object.zoom + scope.zoom0 = scope.object instanceof PerspectiveCamera ? scope.object.zoom : 1 } this.reset = () => { scope.target.copy(scope.target0) scope.object.position.copy(scope.position0) - scope.object.zoom = scope.zoom0 + if (scope.object instanceof PerspectiveCamera) { + scope.object.zoom = scope.zoom0 + scope.object.updateProjectionMatrix() + } - scope.object.updateProjectionMatrix() scope.dispatchEvent(changeEvent) scope.update() @@ -223,23 +245,34 @@ class OrbitControls extends EventDispatcher { } })() - this.dispose = () => { - scope.domElement.removeEventListener('contextmenu', onContextMenu) - - scope.domElement.removeEventListener('pointerdown', onPointerDown) - scope.domElement.removeEventListener('wheel', onMouseWheel) - - scope.domElement.removeEventListener('touchstart', onTouchStart) - scope.domElement.removeEventListener('touchend', onTouchEnd) - scope.domElement.removeEventListener('touchmove', onTouchMove) - - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp) + // https://github.com/mrdoob/three.js/issues/20575 + this.connect = (domElement: HTMLElement) => { + if ((domElement as any) === document) { + console.error( + 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.', + ) + } + scope.domElement = domElement + scope.domElement.addEventListener('contextmenu', onContextMenu) + scope.domElement.addEventListener('pointerdown', onPointerDown) + scope.domElement.addEventListener('wheel', onMouseWheel) + scope.domElement.addEventListener('touchstart', onTouchStart) + scope.domElement.addEventListener('touchend', onTouchEnd) + scope.domElement.addEventListener('touchmove', onTouchMove) + } + this.dispose = () => { + scope.domElement?.removeEventListener('contextmenu', onContextMenu) + scope.domElement?.removeEventListener('pointerdown', onPointerDown) + scope.domElement?.removeEventListener('wheel', onMouseWheel) + scope.domElement?.removeEventListener('touchstart', onTouchStart) + scope.domElement?.removeEventListener('touchend', onTouchEnd) + scope.domElement?.removeEventListener('touchmove', onTouchMove) + scope.domElement?.ownerDocument.removeEventListener('pointermove', onPointerMove) + scope.domElement?.ownerDocument.removeEventListener('pointerup', onPointerUp) if (scope._domElementKeyEvents !== null) { scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown) } - //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? } @@ -296,18 +329,18 @@ class OrbitControls extends EventDispatcher { return Math.pow(0.95, scope.zoomSpeed) } - function rotateLeft(angle) { + function rotateLeft(angle: number) { sphericalDelta.theta -= angle } - function rotateUp(angle) { + function rotateUp(angle: number) { sphericalDelta.phi -= angle } const panLeft = (() => { const v = new Vector3() - return function panLeft(distance, objectMatrix) { + return function panLeft(distance: number, objectMatrix: Matrix4) { v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix v.multiplyScalar(-distance) @@ -318,7 +351,7 @@ class OrbitControls extends EventDispatcher { const panUp = (() => { const v = new Vector3() - return function panUp(distance, objectMatrix) { + return function panUp(distance: number, objectMatrix: Matrix4) { if (scope.screenSpacePanning === true) { v.setFromMatrixColumn(objectMatrix, 1) } else { @@ -336,10 +369,10 @@ class OrbitControls extends EventDispatcher { const pan = (() => { const offset = new Vector3() - return function pan(deltaX, deltaY) { + return function pan(deltaX: number, deltaY: number) { const element = scope.domElement - if (scope.object.isPerspectiveCamera) { + if (element && scope.object instanceof PerspectiveCamera && scope.object.isPerspectiveCamera) { // perspective const position = scope.object.position offset.copy(position).sub(scope.target) @@ -351,7 +384,7 @@ class OrbitControls extends EventDispatcher { // we use only clientHeight here so aspect ratio does not distort speed panLeft((2 * deltaX * targetDistance) / element.clientHeight, scope.object.matrix) panUp((2 * deltaY * targetDistance) / element.clientHeight, scope.object.matrix) - } else if (scope.object.isOrthographicCamera) { + } else if (element && scope.object instanceof OrthographicCamera && scope.object.isOrthographicCamera) { // orthographic panLeft( (deltaX * (scope.object.right - scope.object.left)) / scope.object.zoom / element.clientWidth, @@ -369,10 +402,10 @@ class OrbitControls extends EventDispatcher { } })() - function dollyOut(dollyScale) { - if (scope.object.isPerspectiveCamera) { + function dollyOut(dollyScale: number) { + if (scope.object instanceof PerspectiveCamera && scope.object.isPerspectiveCamera) { scale /= dollyScale - } else if (scope.object.isOrthographicCamera) { + } else if (scope.object instanceof OrthographicCamera && scope.object.isOrthographicCamera) { scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)) scope.object.updateProjectionMatrix() zoomChanged = true @@ -382,10 +415,10 @@ class OrbitControls extends EventDispatcher { } } - function dollyIn(dollyScale) { - if (scope.object.isPerspectiveCamera) { + function dollyIn(dollyScale: number) { + if (scope.object instanceof PerspectiveCamera && scope.object.isPerspectiveCamera) { scale *= dollyScale - } else if (scope.object.isOrthographicCamera) { + } else if (scope.object instanceof OrthographicCamera && scope.object.isOrthographicCamera) { scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)) scope.object.updateProjectionMatrix() zoomChanged = true @@ -399,31 +432,33 @@ class OrbitControls extends EventDispatcher { // event callbacks - update the object state // - function handleMouseDownRotate(event) { + function handleMouseDownRotate(event: MouseEvent) { rotateStart.set(event.clientX, event.clientY) } - function handleMouseDownDolly(event) { + function handleMouseDownDolly(event: MouseEvent) { dollyStart.set(event.clientX, event.clientY) } - function handleMouseDownPan(event) { + function handleMouseDownPan(event: MouseEvent) { panStart.set(event.clientX, event.clientY) } - function handleMouseMoveRotate(event) { + function handleMouseMoveRotate(event: MouseEvent) { rotateEnd.set(event.clientX, event.clientY) rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed) const element = scope.domElement - rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height - rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) + if (element) { + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) + } rotateStart.copy(rotateEnd) scope.update() } - function handleMouseMoveDolly(event) { + function handleMouseMoveDolly(event: MouseEvent) { dollyEnd.set(event.clientX, event.clientY) dollyDelta.subVectors(dollyEnd, dollyStart) @@ -437,7 +472,7 @@ class OrbitControls extends EventDispatcher { scope.update() } - function handleMouseMovePan(event) { + function handleMouseMovePan(event: MouseEvent) { panEnd.set(event.clientX, event.clientY) panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed) pan(panDelta.x, panDelta.y) @@ -449,7 +484,7 @@ class OrbitControls extends EventDispatcher { // no-op } - function handleMouseWheel(event) { + function handleMouseWheel(event: WheelEvent) { if (event.deltaY < 0) { dollyIn(getZoomScale()) } else if (event.deltaY > 0) { @@ -459,7 +494,7 @@ class OrbitControls extends EventDispatcher { scope.update() } - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { let needsUpdate = false switch (event.keyCode) { @@ -491,7 +526,7 @@ class OrbitControls extends EventDispatcher { } } - function handleTouchStartRotate(event) { + function handleTouchStartRotate(event: TouchEvent) { if (event.touches.length == 1) { rotateStart.set(event.touches[0].pageX, event.touches[0].pageY) } else { @@ -502,7 +537,7 @@ class OrbitControls extends EventDispatcher { } } - function handleTouchStartPan(event) { + function handleTouchStartPan(event: TouchEvent) { if (event.touches.length == 1) { panStart.set(event.touches[0].pageX, event.touches[0].pageY) } else { @@ -513,7 +548,7 @@ class OrbitControls extends EventDispatcher { } } - function handleTouchStartDolly(event) { + function handleTouchStartDolly(event: TouchEvent) { const dx = event.touches[0].pageX - event.touches[1].pageX const dy = event.touches[0].pageY - event.touches[1].pageY const distance = Math.sqrt(dx * dx + dy * dy) @@ -521,17 +556,17 @@ class OrbitControls extends EventDispatcher { dollyStart.set(0, distance) } - function handleTouchStartDollyPan(event) { + function handleTouchStartDollyPan(event: TouchEvent) { if (scope.enableZoom) handleTouchStartDolly(event) if (scope.enablePan) handleTouchStartPan(event) } - function handleTouchStartDollyRotate(event) { + function handleTouchStartDollyRotate(event: TouchEvent) { if (scope.enableZoom) handleTouchStartDolly(event) if (scope.enableRotate) handleTouchStartRotate(event) } - function handleTouchMoveRotate(event) { + function handleTouchMoveRotate(event: TouchEvent) { if (event.touches.length == 1) { rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY) } else { @@ -545,12 +580,14 @@ class OrbitControls extends EventDispatcher { const element = scope.domElement - rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height - rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) + if (element) { + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) + } rotateStart.copy(rotateEnd) } - function handleTouchMovePan(event) { + function handleTouchMovePan(event: TouchEvent) { if (event.touches.length == 1) { panEnd.set(event.touches[0].pageX, event.touches[0].pageY) } else { @@ -565,7 +602,7 @@ class OrbitControls extends EventDispatcher { panStart.copy(panEnd) } - function handleTouchMoveDolly(event) { + function handleTouchMoveDolly(event: TouchEvent) { const dx = event.touches[0].pageX - event.touches[1].pageX const dy = event.touches[0].pageY - event.touches[1].pageY const distance = Math.sqrt(dx * dx + dy * dy) @@ -576,12 +613,12 @@ class OrbitControls extends EventDispatcher { dollyStart.copy(dollyEnd) } - function handleTouchMoveDollyPan(event) { + function handleTouchMoveDollyPan(event: TouchEvent) { if (scope.enableZoom) handleTouchMoveDolly(event) if (scope.enablePan) handleTouchMovePan(event) } - function handleTouchMoveDollyRotate(event) { + function handleTouchMoveDollyRotate(event: TouchEvent) { if (scope.enableZoom) handleTouchMoveDolly(event) if (scope.enableRotate) handleTouchMoveRotate(event) } @@ -594,7 +631,7 @@ class OrbitControls extends EventDispatcher { // event handlers - FSM: listen for events and reset state // - function onPointerDown(event) { + function onPointerDown(event: PointerEvent) { if (scope.enabled === false) return switch (event.pointerType) { @@ -607,7 +644,7 @@ class OrbitControls extends EventDispatcher { } } - function onPointerMove(event) { + function onPointerMove(event: PointerEvent) { if (scope.enabled === false) return switch (event.pointerType) { @@ -620,7 +657,7 @@ class OrbitControls extends EventDispatcher { } } - function onPointerUp(event) { + function onPointerUp(event: PointerEvent) { switch (event.pointerType) { case 'mouse': case 'pen': @@ -631,14 +668,14 @@ class OrbitControls extends EventDispatcher { } } - function onMouseDown(event) { + function onMouseDown(event: MouseEvent) { // Prevent the browser from scrolling. event.preventDefault() // Manually set the focus since calling preventDefault above // prevents the browser from setting it automatically. - scope.domElement.focus ? scope.domElement.focus() : window.focus() + scope.domElement?.focus ? scope.domElement.focus() : window.focus() let mouseAction @@ -695,13 +732,13 @@ class OrbitControls extends EventDispatcher { } if (state !== STATE.NONE) { - scope.domElement.ownerDocument.addEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp) + scope.domElement?.ownerDocument.addEventListener('pointermove', onPointerMove) + scope.domElement?.ownerDocument.addEventListener('pointerup', onPointerUp) scope.dispatchEvent(startEvent) } } - function onMouseMove(event) { + function onMouseMove(event: MouseEvent) { if (scope.enabled === false) return event.preventDefault() @@ -724,19 +761,20 @@ class OrbitControls extends EventDispatcher { } } - function onMouseUp(event) { - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp) + function onMouseUp(event: MouseEvent) { + scope.domElement?.ownerDocument.removeEventListener('pointermove', onPointerMove) + scope.domElement?.ownerDocument.removeEventListener('pointerup', onPointerUp) if (scope.enabled === false) return - handleMouseUp(event) + handleMouseUp() scope.dispatchEvent(endEvent) state = STATE.NONE } - function onMouseWheel(event) { - if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) + function onMouseWheel(event: WheelEvent) { + if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) { return + } event.preventDefault() @@ -747,12 +785,12 @@ class OrbitControls extends EventDispatcher { scope.dispatchEvent(endEvent) } - function onKeyDown(event) { + function onKeyDown(event: KeyboardEvent) { if (scope.enabled === false || scope.enablePan === false) return handleKeyDown(event) } - function onTouchStart(event) { + function onTouchStart(event: TouchEvent) { if (scope.enabled === false) return event.preventDefault() // prevent scrolling @@ -807,7 +845,7 @@ class OrbitControls extends EventDispatcher { } } - function onTouchMove(event) { + function onTouchMove(event: TouchEvent) { if (scope.enabled === false) return event.preventDefault() // prevent scrolling @@ -842,32 +880,22 @@ class OrbitControls extends EventDispatcher { } } - function onTouchEnd(event) { + function onTouchEnd(event: TouchEvent) { if (scope.enabled === false) return - handleTouchEnd(event) + handleTouchEnd() scope.dispatchEvent(endEvent) state = STATE.NONE } - function onContextMenu(event) { + function onContextMenu(event: Event) { if (scope.enabled === false) return event.preventDefault() } - // - - scope.domElement.addEventListener('contextmenu', onContextMenu) - - scope.domElement.addEventListener('pointerdown', onPointerDown) - scope.domElement.addEventListener('wheel', onMouseWheel) - - scope.domElement.addEventListener('touchstart', onTouchStart) - scope.domElement.addEventListener('touchend', onTouchEnd) - scope.domElement.addEventListener('touchmove', onTouchMove) - + // connect events + if (domElement !== undefined) this.connect(domElement) // force an update at start - this.update() } } @@ -881,7 +909,7 @@ class OrbitControls extends EventDispatcher { // Pan - left mouse, or arrow keys / touch: one-finger move class MapControls extends OrbitControls { - constructor(object, domElement) { + constructor(object: Camera, domElement: HTMLElement) { super(object, domElement) this.screenSpacePanning = false // pan orthogonal to world-space direction camera.up diff --git a/src/controls/PointerLockControls.js b/src/controls/PointerLockControls.js deleted file mode 100644 index 401cb5b2..00000000 --- a/src/controls/PointerLockControls.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Euler, EventDispatcher, Vector3 } from 'three' - -class PointerLockControls extends EventDispatcher { - constructor(camera, domElement) { - super() - - if (domElement === undefined) { - console.warn('THREE.PointerLockControls: The second parameter "domElement" is now mandatory.') - domElement = document.body - } - - this.domElement = domElement - this.isLocked = false - - // Set to constrain the pitch of the camera - // Range is 0 to Math.PI radians - this.minPolarAngle = 0 // radians - this.maxPolarAngle = Math.PI // radians - - // - // internals - // - - const scope = this - - const changeEvent = { type: 'change' } - const lockEvent = { type: 'lock' } - const unlockEvent = { type: 'unlock' } - - const euler = new Euler(0, 0, 0, 'YXZ') - - const PI_2 = Math.PI / 2 - - const vec = new Vector3() - - function onMouseMove(event) { - if (scope.isLocked === false) return - - const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0 - const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0 - - euler.setFromQuaternion(camera.quaternion) - - euler.y -= movementX * 0.002 - euler.x -= movementY * 0.002 - - euler.x = Math.max(PI_2 - scope.maxPolarAngle, Math.min(PI_2 - scope.minPolarAngle, euler.x)) - - camera.quaternion.setFromEuler(euler) - - scope.dispatchEvent(changeEvent) - } - - function onPointerlockChange() { - if (scope.domElement.ownerDocument.pointerLockElement === scope.domElement) { - scope.dispatchEvent(lockEvent) - - scope.isLocked = true - } else { - scope.dispatchEvent(unlockEvent) - - scope.isLocked = false - } - } - - function onPointerlockError() { - console.error('THREE.PointerLockControls: Unable to use Pointer Lock API') - } - - this.connect = () => { - scope.domElement.ownerDocument.addEventListener('mousemove', onMouseMove) - scope.domElement.ownerDocument.addEventListener('pointerlockchange', onPointerlockChange) - scope.domElement.ownerDocument.addEventListener('pointerlockerror', onPointerlockError) - } - - this.disconnect = () => { - scope.domElement.ownerDocument.removeEventListener('mousemove', onMouseMove) - scope.domElement.ownerDocument.removeEventListener('pointerlockchange', onPointerlockChange) - scope.domElement.ownerDocument.removeEventListener('pointerlockerror', onPointerlockError) - } - - this.dispose = function () { - this.disconnect() - } - - this.getObject = () => - // retaining this method for backward compatibility - - camera - - this.getDirection = (() => { - const direction = new Vector3(0, 0, -1) - - return (v) => v.copy(direction).applyQuaternion(camera.quaternion) - })() - - this.moveForward = (distance) => { - // move forward parallel to the xz-plane - // assumes camera.up is y-up - - vec.setFromMatrixColumn(camera.matrix, 0) - - vec.crossVectors(camera.up, vec) - - camera.position.addScaledVector(vec, distance) - } - - this.moveRight = (distance) => { - vec.setFromMatrixColumn(camera.matrix, 0) - - camera.position.addScaledVector(vec, distance) - } - - this.lock = function () { - this.domElement.requestPointerLock() - } - - this.unlock = () => { - scope.domElement.ownerDocument.exitPointerLock() - } - - this.connect() - } -} - -export { PointerLockControls } diff --git a/src/controls/PointerLockControls.ts b/src/controls/PointerLockControls.ts new file mode 100644 index 00000000..9240675c --- /dev/null +++ b/src/controls/PointerLockControls.ts @@ -0,0 +1,121 @@ +import { Camera, Euler, EventDispatcher, Vector3 } from 'three' + +class PointerLockControls extends EventDispatcher { + camera: Camera + domElement: HTMLElement + + isLocked = false + + // Set to constrain the pitch of the camera + // Range is 0 to Math.PI radians + minPolarAngle = 0 // radians + maxPolarAngle = Math.PI // radians + + changeEvent = { type: 'change' } + lockEvent = { type: 'lock' } + unlockEvent = { type: 'unlock' } + + euler = new Euler(0, 0, 0, 'YXZ') + + PI_2 = Math.PI / 2 + + vec = new Vector3() + + constructor(camera: Camera, domElement: HTMLElement) { + super() + + if (domElement === undefined) { + console.warn('THREE.PointerLockControls: The second parameter "domElement" is now mandatory.') + domElement = document.body + } + + this.domElement = domElement + this.camera = camera + + this.connect() + } + + onMouseMove = (event: MouseEvent) => { + if (this.isLocked === false) return + + const movementX = event.movementX || (event as any).mozMovementX || (event as any).webkitMovementX || 0 + const movementY = event.movementY || (event as any).mozMovementY || (event as any).webkitMovementY || 0 + + this.euler.setFromQuaternion(this.camera.quaternion) + + this.euler.y -= movementX * 0.002 + this.euler.x -= movementY * 0.002 + + this.euler.x = Math.max(this.PI_2 - this.maxPolarAngle, Math.min(this.PI_2 - this.minPolarAngle, this.euler.x)) + + this.camera.quaternion.setFromEuler(this.euler) + + this.dispatchEvent(this.changeEvent) + } + + onPointerlockChange = () => { + if (this.domElement.ownerDocument.pointerLockElement === this.domElement) { + this.dispatchEvent(this.lockEvent) + + this.isLocked = true + } else { + this.dispatchEvent(this.unlockEvent) + + this.isLocked = false + } + } + + onPointerlockError = () => { + console.error('THREE.PointerLockControls: Unable to use Pointer Lock API') + } + + connect = () => { + this.domElement.ownerDocument.addEventListener('mousemove', this.onMouseMove) + this.domElement.ownerDocument.addEventListener('pointerlockchange', this.onPointerlockChange) + this.domElement.ownerDocument.addEventListener('pointerlockerror', this.onPointerlockError) + } + + disconnect = () => { + this.domElement.ownerDocument.removeEventListener('mousemove', this.onMouseMove) + this.domElement.ownerDocument.removeEventListener('pointerlockchange', this.onPointerlockChange) + this.domElement.ownerDocument.removeEventListener('pointerlockerror', this.onPointerlockError) + } + + dispose = () => { + this.disconnect() + } + + getObject = () => + // retaining this method for backward compatibility + this.camera + + private direction = new Vector3(0, 0, -1) + getDirection = (v: Vector3) => v.copy(this.direction).applyQuaternion(this.camera.quaternion) + + moveForward = (distance: number) => { + // move forward parallel to the xz-plane + // assumes this.camera.up is y-up + + this.vec.setFromMatrixColumn(this.camera.matrix, 0) + + this.vec.crossVectors(this.camera.up, this.vec) + + this.camera.position.addScaledVector(this.vec, distance) + } + + moveRight = (distance: number) => { + this.vec.setFromMatrixColumn(this.camera.matrix, 0) + + this.camera.position.addScaledVector(this.vec, distance) + } + + lock = () => { + this.domElement.requestPointerLock() + } + + unlock = () => { + this.domElement.ownerDocument.exitPointerLock() + } +} + +export { PointerLockControls } diff --git a/src/controls/TrackballControls.js b/src/controls/TrackballControls.js deleted file mode 100644 index 09e32e05..00000000 --- a/src/controls/TrackballControls.js +++ /dev/null @@ -1,600 +0,0 @@ -import { EventDispatcher, MOUSE, Quaternion, Vector2, Vector3 } from 'three' - -class TrackballControls extends EventDispatcher { - constructor(object, domElement) { - super() - - if (domElement === undefined) - console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.') - if (domElement === document) - console.error( - 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.', - ) - - const scope = this - const STATE = { - NONE: -1, - ROTATE: 0, - ZOOM: 1, - PAN: 2, - TOUCH_ROTATE: 3, - TOUCH_ZOOM_PAN: 4, - } - - this.object = object - this.domElement = domElement - - // API - - this.enabled = true - - this.screen = { left: 0, top: 0, width: 0, height: 0 } - - this.rotateSpeed = 1.0 - this.zoomSpeed = 1.2 - this.panSpeed = 0.3 - - this.noRotate = false - this.noZoom = false - this.noPan = false - - this.staticMoving = false - this.dynamicDampingFactor = 0.2 - - this.minDistance = 0 - this.maxDistance = Infinity - - this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/] - - this.mouseButtons = { - LEFT: MOUSE.ROTATE, - MIDDLE: MOUSE.DOLLY, - RIGHT: MOUSE.PAN, - } - - // internals - - this.target = new Vector3() - - const EPS = 0.000001 - - const lastPosition = new Vector3() - let lastZoom = 1 - - let _state = STATE.NONE - let _keyState = STATE.NONE - const _eye = new Vector3() - const _movePrev = new Vector2() - const _moveCurr = new Vector2() - const _lastAxis = new Vector3() - let _lastAngle = 0 - const _zoomStart = new Vector2() - const _zoomEnd = new Vector2() - let _touchZoomDistanceStart = 0 - let _touchZoomDistanceEnd = 0 - const _panStart = new Vector2() - const _panEnd = new Vector2() - - // for reset - - this.target0 = this.target.clone() - this.position0 = this.object.position.clone() - this.up0 = this.object.up.clone() - this.zoom0 = this.object.zoom - - // events - - const changeEvent = { type: 'change' } - const startEvent = { type: 'start' } - const endEvent = { type: 'end' } - - // methods - - this.handleResize = () => { - const box = scope.domElement.getBoundingClientRect() - // adjustments come from similar code in the jquery offset() function - const d = scope.domElement.ownerDocument.documentElement - scope.screen.left = box.left + window.pageXOffset - d.clientLeft - scope.screen.top = box.top + window.pageYOffset - d.clientTop - scope.screen.width = box.width - scope.screen.height = box.height - } - - const getMouseOnScreen = (() => { - const vector = new Vector2() - - return function getMouseOnScreen(pageX, pageY) { - vector.set((pageX - scope.screen.left) / scope.screen.width, (pageY - scope.screen.top) / scope.screen.height) - - return vector - } - })() - - const getMouseOnCircle = (() => { - const vector = new Vector2() - - return function getMouseOnCircle(pageX, pageY) { - vector.set( - (pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5), - (scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width, // screen.width intentional - ) - - return vector - } - })() - - this.rotateCamera = (() => { - const axis = new Vector3() - const quaternion = new Quaternion() - const eyeDirection = new Vector3() - const objectUpDirection = new Vector3() - const objectSidewaysDirection = new Vector3() - const moveDirection = new Vector3() - let angle - - return function rotateCamera() { - moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0) - angle = moveDirection.length() - - if (angle) { - _eye.copy(scope.object.position).sub(scope.target) - - eyeDirection.copy(_eye).normalize() - objectUpDirection.copy(scope.object.up).normalize() - objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize() - - objectUpDirection.setLength(_moveCurr.y - _movePrev.y) - objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x) - - moveDirection.copy(objectUpDirection.add(objectSidewaysDirection)) - - axis.crossVectors(moveDirection, _eye).normalize() - - angle *= scope.rotateSpeed - quaternion.setFromAxisAngle(axis, angle) - - _eye.applyQuaternion(quaternion) - scope.object.up.applyQuaternion(quaternion) - - _lastAxis.copy(axis) - _lastAngle = angle - } else if (!scope.staticMoving && _lastAngle) { - _lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor) - _eye.copy(scope.object.position).sub(scope.target) - quaternion.setFromAxisAngle(_lastAxis, _lastAngle) - _eye.applyQuaternion(quaternion) - scope.object.up.applyQuaternion(quaternion) - } - - _movePrev.copy(_moveCurr) - } - })() - - this.zoomCamera = function () { - let factor - - if (_state === STATE.TOUCH_ZOOM_PAN) { - factor = _touchZoomDistanceStart / _touchZoomDistanceEnd - _touchZoomDistanceStart = _touchZoomDistanceEnd - - if (scope.object.isPerspectiveCamera) { - _eye.multiplyScalar(factor) - } else if (scope.object.isOrthographicCamera) { - scope.object.zoom *= factor - scope.object.updateProjectionMatrix() - } else { - console.warn('THREE.TrackballControls: Unsupported camera type') - } - } else { - factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed - - if (factor !== 1.0 && factor > 0.0) { - if (scope.object.isPerspectiveCamera) { - _eye.multiplyScalar(factor) - } else if (scope.object.isOrthographicCamera) { - scope.object.zoom /= factor - scope.object.updateProjectionMatrix() - } else { - console.warn('THREE.TrackballControls: Unsupported camera type') - } - } - - if (scope.staticMoving) { - _zoomStart.copy(_zoomEnd) - } else { - _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor - } - } - } - - this.panCamera = (() => { - const mouseChange = new Vector2(), - objectUp = new Vector3(), - pan = new Vector3() - - return function panCamera() { - mouseChange.copy(_panEnd).sub(_panStart) - - if (mouseChange.lengthSq()) { - if (scope.object.isOrthographicCamera) { - const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth - const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth - - mouseChange.x *= scale_x - mouseChange.y *= scale_y - } - - mouseChange.multiplyScalar(_eye.length() * scope.panSpeed) - - pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x) - pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y)) - - scope.object.position.add(pan) - scope.target.add(pan) - - if (scope.staticMoving) { - _panStart.copy(_panEnd) - } else { - _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor)) - } - } - } - })() - - this.checkDistances = () => { - if (!scope.noZoom || !scope.noPan) { - if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) { - scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance)) - _zoomStart.copy(_zoomEnd) - } - - if (_eye.lengthSq() < scope.minDistance * scope.minDistance) { - scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance)) - _zoomStart.copy(_zoomEnd) - } - } - } - - this.update = () => { - _eye.subVectors(scope.object.position, scope.target) - - if (!scope.noRotate) { - scope.rotateCamera() - } - - if (!scope.noZoom) { - scope.zoomCamera() - } - - if (!scope.noPan) { - scope.panCamera() - } - - scope.object.position.addVectors(scope.target, _eye) - - if (scope.object.isPerspectiveCamera) { - scope.checkDistances() - - scope.object.lookAt(scope.target) - - if (lastPosition.distanceToSquared(scope.object.position) > EPS) { - scope.dispatchEvent(changeEvent) - - lastPosition.copy(scope.object.position) - } - } else if (scope.object.isOrthographicCamera) { - scope.object.lookAt(scope.target) - - if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) { - scope.dispatchEvent(changeEvent) - - lastPosition.copy(scope.object.position) - lastZoom = scope.object.zoom - } - } else { - console.warn('THREE.TrackballControls: Unsupported camera type') - } - } - - this.reset = () => { - _state = STATE.NONE - _keyState = STATE.NONE - - scope.target.copy(scope.target0) - scope.object.position.copy(scope.position0) - scope.object.up.copy(scope.up0) - scope.object.zoom = scope.zoom0 - - scope.object.updateProjectionMatrix() - - _eye.subVectors(scope.object.position, scope.target) - - scope.object.lookAt(scope.target) - - scope.dispatchEvent(changeEvent) - - lastPosition.copy(scope.object.position) - lastZoom = scope.object.zoom - } - - // listeners - - function onPointerDown(event) { - if (scope.enabled === false) return - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseDown(event) - break - - // TODO touch - } - } - - function onPointerMove(event) { - if (scope.enabled === false) return - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseMove(event) - break - - // TODO touch - } - } - - function onPointerUp(event) { - if (scope.enabled === false) return - - switch (event.pointerType) { - case 'mouse': - case 'pen': - onMouseUp(event) - break - - // TODO touch - } - } - - function keydown(event) { - if (scope.enabled === false) return - - window.removeEventListener('keydown', keydown) - - if (_keyState !== STATE.NONE) { - return - } else if (event.keyCode === scope.keys[STATE.ROTATE] && !scope.noRotate) { - _keyState = STATE.ROTATE - } else if (event.keyCode === scope.keys[STATE.ZOOM] && !scope.noZoom) { - _keyState = STATE.ZOOM - } else if (event.keyCode === scope.keys[STATE.PAN] && !scope.noPan) { - _keyState = STATE.PAN - } - } - - function keyup() { - if (scope.enabled === false) return - - _keyState = STATE.NONE - - window.addEventListener('keydown', keydown) - } - - function onMouseDown(event) { - event.preventDefault() - - if (_state === STATE.NONE) { - switch (event.button) { - case scope.mouseButtons.LEFT: - _state = STATE.ROTATE - break - - case scope.mouseButtons.MIDDLE: - _state = STATE.ZOOM - break - - case scope.mouseButtons.RIGHT: - _state = STATE.PAN - break - - default: - _state = STATE.NONE - } - } - - const state = _keyState !== STATE.NONE ? _keyState : _state - - if (state === STATE.ROTATE && !scope.noRotate) { - _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)) - _movePrev.copy(_moveCurr) - } else if (state === STATE.ZOOM && !scope.noZoom) { - _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY)) - _zoomEnd.copy(_zoomStart) - } else if (state === STATE.PAN && !scope.noPan) { - _panStart.copy(getMouseOnScreen(event.pageX, event.pageY)) - _panEnd.copy(_panStart) - } - - scope.domElement.ownerDocument.addEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp) - - scope.dispatchEvent(startEvent) - } - - function onMouseMove(event) { - if (scope.enabled === false) return - - event.preventDefault() - - const state = _keyState !== STATE.NONE ? _keyState : _state - - if (state === STATE.ROTATE && !scope.noRotate) { - _movePrev.copy(_moveCurr) - _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)) - } else if (state === STATE.ZOOM && !scope.noZoom) { - _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY)) - } else if (state === STATE.PAN && !scope.noPan) { - _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY)) - } - } - - function onMouseUp(event) { - if (scope.enabled === false) return - - event.preventDefault() - - _state = STATE.NONE - - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp) - - scope.dispatchEvent(endEvent) - } - - function mousewheel(event) { - if (scope.enabled === false) return - - if (scope.noZoom === true) return - - event.preventDefault() - - switch (event.deltaMode) { - case 2: - // Zoom in pages - _zoomStart.y -= event.deltaY * 0.025 - break - - case 1: - // Zoom in lines - _zoomStart.y -= event.deltaY * 0.01 - break - - default: - // undefined, 0, assume pixels - _zoomStart.y -= event.deltaY * 0.00025 - break - } - - scope.dispatchEvent(startEvent) - scope.dispatchEvent(endEvent) - } - - function touchstart(event) { - if (scope.enabled === false) return - - event.preventDefault() - - switch (event.touches.length) { - case 1: - _state = STATE.TOUCH_ROTATE - _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) - _movePrev.copy(_moveCurr) - break - - default: - // 2 or more - _state = STATE.TOUCH_ZOOM_PAN - const dx = event.touches[0].pageX - event.touches[1].pageX - const dy = event.touches[0].pageY - event.touches[1].pageY - _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy) - - const x = (event.touches[0].pageX + event.touches[1].pageX) / 2 - const y = (event.touches[0].pageY + event.touches[1].pageY) / 2 - _panStart.copy(getMouseOnScreen(x, y)) - _panEnd.copy(_panStart) - break - } - - scope.dispatchEvent(startEvent) - } - - function touchmove(event) { - if (scope.enabled === false) return - - event.preventDefault() - - switch (event.touches.length) { - case 1: - _movePrev.copy(_moveCurr) - _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) - break - - default: - // 2 or more - const dx = event.touches[0].pageX - event.touches[1].pageX - const dy = event.touches[0].pageY - event.touches[1].pageY - _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy) - - const x = (event.touches[0].pageX + event.touches[1].pageX) / 2 - const y = (event.touches[0].pageY + event.touches[1].pageY) / 2 - _panEnd.copy(getMouseOnScreen(x, y)) - break - } - } - - function touchend(event) { - if (scope.enabled === false) return - - switch (event.touches.length) { - case 0: - _state = STATE.NONE - break - - case 1: - _state = STATE.TOUCH_ROTATE - _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) - _movePrev.copy(_moveCurr) - break - } - - scope.dispatchEvent(endEvent) - } - - function contextmenu(event) { - if (scope.enabled === false) return - - event.preventDefault() - } - - this.dispose = () => { - scope.domElement.removeEventListener('contextmenu', contextmenu) - - scope.domElement.removeEventListener('pointerdown', onPointerDown) - scope.domElement.removeEventListener('wheel', mousewheel) - - scope.domElement.removeEventListener('touchstart', touchstart) - scope.domElement.removeEventListener('touchend', touchend) - scope.domElement.removeEventListener('touchmove', touchmove) - - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp) - - window.removeEventListener('keydown', keydown) - window.removeEventListener('keyup', keyup) - } - - this.domElement.addEventListener('contextmenu', contextmenu) - - this.domElement.addEventListener('pointerdown', onPointerDown) - this.domElement.addEventListener('wheel', mousewheel) - - this.domElement.addEventListener('touchstart', touchstart) - this.domElement.addEventListener('touchend', touchend) - this.domElement.addEventListener('touchmove', touchmove) - - this.domElement.ownerDocument.addEventListener('pointermove', onPointerMove) - this.domElement.ownerDocument.addEventListener('pointerup', onPointerUp) - - window.addEventListener('keydown', keydown) - window.addEventListener('keyup', keyup) - - this.handleResize() - - // force an update at start - this.update() - } -} - -export { TrackballControls } diff --git a/src/controls/TrackballControls.ts b/src/controls/TrackballControls.ts new file mode 100644 index 00000000..a30ac922 --- /dev/null +++ b/src/controls/TrackballControls.ts @@ -0,0 +1,597 @@ +import { EventDispatcher, MOUSE, Quaternion, Vector2, Vector3, PerspectiveCamera, OrthographicCamera } from 'three' + +class TrackballControls extends EventDispatcher { + enabled = true + + screen = { left: 0, top: 0, width: 0, height: 0 } + + rotateSpeed = 1.0 + zoomSpeed = 1.2 + panSpeed = 0.3 + + noRotate = false + noZoom = false + noPan = false + + staticMoving = false + dynamicDampingFactor = 0.2 + + minDistance = 0 + maxDistance = Infinity + + keys = [65 /*A*/, 83 /*S*/, 68 /*D*/] + + mouseButtons = { + LEFT: MOUSE.ROTATE, + MIDDLE: MOUSE.DOLLY, + RIGHT: MOUSE.PAN, + } + + object: PerspectiveCamera | OrthographicCamera + domElement: HTMLElement + + private target = new Vector3() + + // internals + private STATE = { + NONE: -1, + ROTATE: 0, + ZOOM: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_ZOOM_PAN: 4, + } + + private EPS = 0.000001 + + private lastPosition = new Vector3() + private lastZoom = 1 + + private _state = this.STATE.NONE + private _keyState = this.STATE.NONE + private _eye = new Vector3() + private _movePrev = new Vector2() + private _moveCurr = new Vector2() + private _lastAxis = new Vector3() + private _lastAngle = 0 + private _zoomStart = new Vector2() + private _zoomEnd = new Vector2() + private _touchZoomDistanceStart = 0 + private _touchZoomDistanceEnd = 0 + private _panStart = new Vector2() + private _panEnd = new Vector2() + + private target0: Vector3 + private position0: Vector3 + private up0: Vector3 + private zoom0: number + + // events + + private changeEvent = { type: 'change' } + private startEvent = { type: 'start' } + private endEvent = { type: 'end' } + + constructor(object: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super() + + if (domElement === undefined) { + console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.') + } + + this.object = object + this.domElement = domElement + + // for reset + + this.target0 = this.target.clone() + this.position0 = this.object.position.clone() + this.up0 = this.object.up.clone() + this.zoom0 = this.object.zoom + + // listeners + + this.domElement.addEventListener('contextmenu', this.contextmenu) + + this.domElement.addEventListener('pointerdown', this.onPointerDown) + this.domElement.addEventListener('wheel', this.mousewheel) + + this.domElement.addEventListener('touchstart', this.touchstart) + this.domElement.addEventListener('touchend', this.touchend) + this.domElement.addEventListener('touchmove', this.touchmove) + + this.domElement.ownerDocument.addEventListener('pointermove', this.onPointerMove) + this.domElement.ownerDocument.addEventListener('pointerup', this.onPointerUp) + + window.addEventListener('keydown', this.keydown) + window.addEventListener('keyup', this.keyup) + + this.handleResize() + + // force an update at start + this.update() + } + + private onScreenVector = new Vector2() + + private getMouseOnScreen = (pageX: number, pageY: number) => { + this.onScreenVector.set( + (pageX - this.screen.left) / this.screen.width, + (pageY - this.screen.top) / this.screen.height, + ) + + return this.onScreenVector + } + + private onCircleVector = new Vector2() + + private getMouseOnCircle = (pageX: number, pageY: number) => { + this.onCircleVector.set( + (pageX - this.screen.width * 0.5 - this.screen.left) / (this.screen.width * 0.5), + (this.screen.height + 2 * (this.screen.top - pageY)) / this.screen.width, // screen.width intentional + ) + + return this.onCircleVector + } + + private axis = new Vector3() + private quaternion = new Quaternion() + private eyeDirection = new Vector3() + private objectUpDirection = new Vector3() + private objectSidewaysDirection = new Vector3() + private moveDirection = new Vector3() + private angle: number = 0 + + private rotateCamera = () => { + this.moveDirection.set(this._moveCurr.x - this._movePrev.x, this._moveCurr.y - this._movePrev.y, 0) + this.angle = this.moveDirection.length() + + if (this.angle) { + this._eye.copy(this.object.position).sub(this.target) + + this.eyeDirection.copy(this._eye).normalize() + this.objectUpDirection.copy(this.object.up).normalize() + this.objectSidewaysDirection.crossVectors(this.objectUpDirection, this.eyeDirection).normalize() + + this.objectUpDirection.setLength(this._moveCurr.y - this._movePrev.y) + this.objectSidewaysDirection.setLength(this._moveCurr.x - this._movePrev.x) + + this.moveDirection.copy(this.objectUpDirection.add(this.objectSidewaysDirection)) + + this.axis.crossVectors(this.moveDirection, this._eye).normalize() + + this.angle *= this.rotateSpeed + this.quaternion.setFromAxisAngle(this.axis, this.angle) + + this._eye.applyQuaternion(this.quaternion) + this.object.up.applyQuaternion(this.quaternion) + + this._lastAxis.copy(this.axis) + this._lastAngle = this.angle + } else if (!this.staticMoving && this._lastAngle) { + this._lastAngle *= Math.sqrt(1.0 - this.dynamicDampingFactor) + this._eye.copy(this.object.position).sub(this.target) + this.quaternion.setFromAxisAngle(this._lastAxis, this._lastAngle) + this._eye.applyQuaternion(this.quaternion) + this.object.up.applyQuaternion(this.quaternion) + } + + this._movePrev.copy(this._moveCurr) + } + + private zoomCamera = () => { + let factor + + if (this._state === this.STATE.TOUCH_ZOOM_PAN) { + factor = this._touchZoomDistanceStart / this._touchZoomDistanceEnd + this._touchZoomDistanceStart = this._touchZoomDistanceEnd + + if ((this.object as PerspectiveCamera).isPerspectiveCamera) { + this._eye.multiplyScalar(factor) + } else if ((this.object as OrthographicCamera).isOrthographicCamera) { + this.object.zoom *= factor + this.object.updateProjectionMatrix() + } else { + console.warn('THREE.TrackballControls: Unsupported camera type') + } + } else { + factor = 1.0 + (this._zoomEnd.y - this._zoomStart.y) * this.zoomSpeed + + if (factor !== 1.0 && factor > 0.0) { + if ((this.object as PerspectiveCamera).isPerspectiveCamera) { + this._eye.multiplyScalar(factor) + } else if ((this.object as OrthographicCamera).isOrthographicCamera) { + this.object.zoom /= factor + this.object.updateProjectionMatrix() + } else { + console.warn('THREE.TrackballControls: Unsupported camera type') + } + } + + if (this.staticMoving) { + this._zoomStart.copy(this._zoomEnd) + } else { + this._zoomStart.y += (this._zoomEnd.y - this._zoomStart.y) * this.dynamicDampingFactor + } + } + } + + private mouseChange = new Vector2() + private objectUp = new Vector3() + private pan = new Vector3() + + private panCamera = () => { + this.mouseChange.copy(this._panEnd).sub(this._panStart) + + if (this.mouseChange.lengthSq()) { + if ((this.object as OrthographicCamera).isOrthographicCamera) { + const orthoObject = this.object as OrthographicCamera + const scale_x = (orthoObject.right - orthoObject.left) / this.object.zoom / this.domElement.clientWidth + const scale_y = (orthoObject.top - orthoObject.bottom) / this.object.zoom / this.domElement.clientWidth + + this.mouseChange.x *= scale_x + this.mouseChange.y *= scale_y + } + + this.mouseChange.multiplyScalar(this._eye.length() * this.panSpeed) + + this.pan.copy(this._eye).cross(this.object.up).setLength(this.mouseChange.x) + this.pan.add(this.objectUp.copy(this.object.up).setLength(this.mouseChange.y)) + + this.object.position.add(this.pan) + this.target.add(this.pan) + + if (this.staticMoving) { + this._panStart.copy(this._panEnd) + } else { + this._panStart.add( + this.mouseChange.subVectors(this._panEnd, this._panStart).multiplyScalar(this.dynamicDampingFactor), + ) + } + } + } + + private checkDistances = () => { + if (!this.noZoom || !this.noPan) { + if (this._eye.lengthSq() > this.maxDistance * this.maxDistance) { + this.object.position.addVectors(this.target, this._eye.setLength(this.maxDistance)) + this._zoomStart.copy(this._zoomEnd) + } + + if (this._eye.lengthSq() < this.minDistance * this.minDistance) { + this.object.position.addVectors(this.target, this._eye.setLength(this.minDistance)) + this._zoomStart.copy(this._zoomEnd) + } + } + } + + private handleResize = () => { + const box = this.domElement.getBoundingClientRect() + // adjustments come from similar code in the jquery offset() function + const d = this.domElement.ownerDocument.documentElement + this.screen.left = box.left + window.pageXOffset - d.clientLeft + this.screen.top = box.top + window.pageYOffset - d.clientTop + this.screen.width = box.width + this.screen.height = box.height + } + + public update = () => { + this._eye.subVectors(this.object.position, this.target) + + if (!this.noRotate) { + this.rotateCamera() + } + + if (!this.noZoom) { + this.zoomCamera() + } + + if (!this.noPan) { + this.panCamera() + } + + this.object.position.addVectors(this.target, this._eye) + + if ((this.object as PerspectiveCamera).isPerspectiveCamera) { + this.checkDistances() + + this.object.lookAt(this.target) + + if (this.lastPosition.distanceToSquared(this.object.position) > this.EPS) { + this.dispatchEvent(this.changeEvent) + + this.lastPosition.copy(this.object.position) + } + } else if ((this.object as OrthographicCamera).isOrthographicCamera) { + this.object.lookAt(this.target) + + if (this.lastPosition.distanceToSquared(this.object.position) > this.EPS || this.lastZoom !== this.object.zoom) { + this.dispatchEvent(this.changeEvent) + + this.lastPosition.copy(this.object.position) + this.lastZoom = this.object.zoom + } + } else { + console.warn('THREE.TrackballControls: Unsupported camera type') + } + } + + public reset = () => { + this._state = this.STATE.NONE + this._keyState = this.STATE.NONE + + this.target.copy(this.target0) + this.object.position.copy(this.position0) + this.object.up.copy(this.up0) + this.object.zoom = this.zoom0 + + this.object.updateProjectionMatrix() + + this._eye.subVectors(this.object.position, this.target) + + this.object.lookAt(this.target) + + this.dispatchEvent(this.changeEvent) + + this.lastPosition.copy(this.object.position) + this.lastZoom = this.object.zoom + } + + private keydown = (event: KeyboardEvent) => { + if (this.enabled === false) return + + window.removeEventListener('keydown', this.keydown) + + if (this._keyState !== this.STATE.NONE) { + return + } else if (event.keyCode === this.keys[this.STATE.ROTATE] && !this.noRotate) { + this._keyState = this.STATE.ROTATE + } else if (event.keyCode === this.keys[this.STATE.ZOOM] && !this.noZoom) { + this._keyState = this.STATE.ZOOM + } else if (event.keyCode === this.keys[this.STATE.PAN] && !this.noPan) { + this._keyState = this.STATE.PAN + } + } + + private onPointerDown = (event: PointerEvent) => { + if (this.enabled === false) return + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseDown(event) + break + + // TODO touch + } + } + + private onPointerMove = (event: PointerEvent) => { + if (this.enabled === false) return + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseMove(event) + break + + // TODO touch + } + } + + private onPointerUp = (event: PointerEvent) => { + if (this.enabled === false) return + + switch (event.pointerType) { + case 'mouse': + case 'pen': + this.onMouseUp(event) + break + + // TODO touch + } + } + + private keyup = () => { + if (this.enabled === false) return + + this._keyState = this.STATE.NONE + + window.addEventListener('keydown', this.keydown) + } + + private onMouseDown = (event: MouseEvent) => { + event.preventDefault() + + if (this._state === this.STATE.NONE) { + switch (event.button) { + case this.mouseButtons.LEFT: + this._state = this.STATE.ROTATE + break + + case this.mouseButtons.MIDDLE: + this._state = this.STATE.ZOOM + break + + case this.mouseButtons.RIGHT: + this._state = this.STATE.PAN + break + + default: + this._state = this.STATE.NONE + } + } + + const state = this._keyState !== this.STATE.NONE ? this._keyState : this._state + + if (state === this.STATE.ROTATE && !this.noRotate) { + this._moveCurr.copy(this.getMouseOnCircle(event.pageX, event.pageY)) + this._movePrev.copy(this._moveCurr) + } else if (state === this.STATE.ZOOM && !this.noZoom) { + this._zoomStart.copy(this.getMouseOnScreen(event.pageX, event.pageY)) + this._zoomEnd.copy(this._zoomStart) + } else if (state === this.STATE.PAN && !this.noPan) { + this._panStart.copy(this.getMouseOnScreen(event.pageX, event.pageY)) + this._panEnd.copy(this._panStart) + } + + this.domElement.ownerDocument.addEventListener('pointermove', this.onPointerMove) + this.domElement.ownerDocument.addEventListener('pointerup', this.onPointerUp) + + this.dispatchEvent(this.startEvent) + } + + private onMouseMove = (event: MouseEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + const state = this._keyState !== this.STATE.NONE ? this._keyState : this._state + + if (state === this.STATE.ROTATE && !this.noRotate) { + this._movePrev.copy(this._moveCurr) + this._moveCurr.copy(this.getMouseOnCircle(event.pageX, event.pageY)) + } else if (state === this.STATE.ZOOM && !this.noZoom) { + this._zoomEnd.copy(this.getMouseOnScreen(event.pageX, event.pageY)) + } else if (state === this.STATE.PAN && !this.noPan) { + this._panEnd.copy(this.getMouseOnScreen(event.pageX, event.pageY)) + } + } + + private onMouseUp = (event: MouseEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + this._state = this.STATE.NONE + + this.domElement.ownerDocument.removeEventListener('pointermove', this.onPointerMove) + this.domElement.ownerDocument.removeEventListener('pointerup', this.onPointerUp) + + this.dispatchEvent(this.endEvent) + } + + private mousewheel = (event: WheelEvent) => { + if (this.enabled === false) return + + if (this.noZoom === true) return + + event.preventDefault() + + switch (event.deltaMode) { + case 2: + // Zoom in pages + this._zoomStart.y -= event.deltaY * 0.025 + break + + case 1: + // Zoom in lines + this._zoomStart.y -= event.deltaY * 0.01 + break + + default: + // undefined, 0, assume pixels + this._zoomStart.y -= event.deltaY * 0.00025 + break + } + + this.dispatchEvent(this.startEvent) + this.dispatchEvent(this.endEvent) + } + + private touchstart = (event: TouchEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + switch (event.touches.length) { + case 1: + this._state = this.STATE.TOUCH_ROTATE + this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) + this._movePrev.copy(this._moveCurr) + break + + default: + // 2 or more + this._state = this.STATE.TOUCH_ZOOM_PAN + const dx = event.touches[0].pageX - event.touches[1].pageX + const dy = event.touches[0].pageY - event.touches[1].pageY + this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy) + + const x = (event.touches[0].pageX + event.touches[1].pageX) / 2 + const y = (event.touches[0].pageY + event.touches[1].pageY) / 2 + this._panStart.copy(this.getMouseOnScreen(x, y)) + this._panEnd.copy(this._panStart) + break + } + + this.dispatchEvent(this.startEvent) + } + + private touchmove = (event: TouchEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + switch (event.touches.length) { + case 1: + this._movePrev.copy(this._moveCurr) + this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) + break + + default: + // 2 or more + const dx = event.touches[0].pageX - event.touches[1].pageX + const dy = event.touches[0].pageY - event.touches[1].pageY + this._touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy) + + const x = (event.touches[0].pageX + event.touches[1].pageX) / 2 + const y = (event.touches[0].pageY + event.touches[1].pageY) / 2 + this._panEnd.copy(this.getMouseOnScreen(x, y)) + break + } + } + + private touchend = (event: TouchEvent) => { + if (this.enabled === false) return + + switch (event.touches.length) { + case 0: + this._state = this.STATE.NONE + break + + case 1: + this._state = this.STATE.TOUCH_ROTATE + this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)) + this._movePrev.copy(this._moveCurr) + break + } + + this.dispatchEvent(this.endEvent) + } + + private contextmenu = (event: MouseEvent) => { + if (this.enabled === false) return + + event.preventDefault() + } + + public dispose = () => { + this.domElement.removeEventListener('contextmenu', this.contextmenu) + + this.domElement.removeEventListener('pointerdown', this.onPointerDown) + this.domElement.removeEventListener('wheel', this.mousewheel) + + this.domElement.removeEventListener('touchstart', this.touchstart) + this.domElement.removeEventListener('touchend', this.touchend) + this.domElement.removeEventListener('touchmove', this.touchmove) + + this.domElement.ownerDocument.removeEventListener('pointermove', this.onPointerMove) + this.domElement.ownerDocument.removeEventListener('pointerup', this.onPointerUp) + + window.removeEventListener('keydown', this.keydown) + window.removeEventListener('keyup', this.keyup) + } +} + +export { TrackballControls } diff --git a/src/controls/TransformControls.js b/src/controls/TransformControls.js deleted file mode 100644 index 2543c874..00000000 --- a/src/controls/TransformControls.js +++ /dev/null @@ -1,1319 +0,0 @@ -import { - BoxGeometry, - BufferGeometry, - Color, - CylinderGeometry, - DoubleSide, - Euler, - Float32BufferAttribute, - Line, - LineBasicMaterial, - Matrix4, - Mesh, - MeshBasicMaterial, - Object3D, - OctahedronGeometry, - PlaneGeometry, - Quaternion, - Raycaster, - SphereGeometry, - TorusGeometry, - Vector3, -} from 'three' - -const TransformControls = function (camera, domElement) { - if (domElement === undefined) { - console.warn('THREE.TransformControls: The second parameter "domElement" is now mandatory.') - domElement = document - } - - Object3D.call(this) - - this.visible = false - this.domElement = domElement - - const _gizmo = new TransformControlsGizmo() - this.add(_gizmo) - - const _plane = new TransformControlsPlane() - this.add(_plane) - - const scope = this - - // Define properties with getters/setter - // Setting the defined property will automatically trigger change event - // Defined properties are passed down to gizmo and plane - - defineProperty('camera', camera) - defineProperty('object', undefined) - defineProperty('enabled', true) - defineProperty('axis', null) - defineProperty('mode', 'translate') - defineProperty('translationSnap', null) - defineProperty('rotationSnap', null) - defineProperty('scaleSnap', null) - defineProperty('space', 'world') - defineProperty('size', 1) - defineProperty('dragging', false) - defineProperty('showX', true) - defineProperty('showY', true) - defineProperty('showZ', true) - - const changeEvent = { type: 'change' } - const mouseDownEvent = { type: 'mouseDown' } - const mouseUpEvent = { type: 'mouseUp', mode: scope.mode } - const objectChangeEvent = { type: 'objectChange' } - - // Reusable utility variables - - const raycaster = new Raycaster() - - function intersectObjectWithRay(object, raycaster, includeInvisible) { - const allIntersections = raycaster.intersectObject(object, true) - - for (let i = 0; i < allIntersections.length; i++) { - if (allIntersections[i].object.visible || includeInvisible) { - return allIntersections[i] - } - } - - return false - } - - const _tempVector = new Vector3() - const _tempVector2 = new Vector3() - const _tempQuaternion = new Quaternion() - const _unit = { - X: new Vector3(1, 0, 0), - Y: new Vector3(0, 1, 0), - Z: new Vector3(0, 0, 1), - } - - const pointStart = new Vector3() - const pointEnd = new Vector3() - const offset = new Vector3() - const rotationAxis = new Vector3() - const startNorm = new Vector3() - const endNorm = new Vector3() - let rotationAngle = 0 - - const cameraPosition = new Vector3() - const cameraQuaternion = new Quaternion() - const cameraScale = new Vector3() - - const parentPosition = new Vector3() - const parentQuaternion = new Quaternion() - const parentQuaternionInv = new Quaternion() - const parentScale = new Vector3() - - const worldPositionStart = new Vector3() - const worldQuaternionStart = new Quaternion() - const worldScaleStart = new Vector3() - - const worldPosition = new Vector3() - const worldQuaternion = new Quaternion() - const worldQuaternionInv = new Quaternion() - const worldScale = new Vector3() - - const eye = new Vector3() - - const positionStart = new Vector3() - const quaternionStart = new Quaternion() - const scaleStart = new Vector3() - - // TODO: remove properties unused in plane and gizmo - - defineProperty('worldPosition', worldPosition) - defineProperty('worldPositionStart', worldPositionStart) - defineProperty('worldQuaternion', worldQuaternion) - defineProperty('worldQuaternionStart', worldQuaternionStart) - defineProperty('cameraPosition', cameraPosition) - defineProperty('cameraQuaternion', cameraQuaternion) - defineProperty('pointStart', pointStart) - defineProperty('pointEnd', pointEnd) - defineProperty('rotationAxis', rotationAxis) - defineProperty('rotationAngle', rotationAngle) - defineProperty('eye', eye) - - { - domElement.addEventListener('pointerdown', onPointerDown) - domElement.addEventListener('pointermove', onPointerHover) - scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp) - } - - this.dispose = function () { - domElement.removeEventListener('pointerdown', onPointerDown) - domElement.removeEventListener('pointermove', onPointerHover) - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp) - - this.traverse((child) => { - if (child.geometry) child.geometry.dispose() - if (child.material) child.material.dispose() - }) - } - - // Set current object - this.attach = function (object) { - this.object = object - this.visible = true - - return this - } - - // Detatch from object - this.detach = function () { - this.object = undefined - this.visible = false - this.axis = null - - return this - } - - // Defined getter, setter and store for a property - function defineProperty(propName, defaultValue) { - let propValue = defaultValue - - Object.defineProperty(scope, propName, { - get: function () { - return propValue !== undefined ? propValue : defaultValue - }, - - set: function (value) { - if (propValue !== value) { - propValue = value - _plane[propName] = value - _gizmo[propName] = value - - scope.dispatchEvent({ type: `${propName}-changed`, value }) - scope.dispatchEvent(changeEvent) - } - }, - }) - - scope[propName] = defaultValue - _plane[propName] = defaultValue - _gizmo[propName] = defaultValue - } - - // updateMatrixWorld updates key transformation variables - this.updateMatrixWorld = function () { - if (this.object !== undefined) { - this.object.updateMatrixWorld() - - if (this.object.parent === null) { - console.error('TransformControls: The attached 3D object must be a part of the scene graph.') - } else { - this.object.parent.matrixWorld.decompose(parentPosition, parentQuaternion, parentScale) - } - - this.object.matrixWorld.decompose(worldPosition, worldQuaternion, worldScale) - - parentQuaternionInv.copy(parentQuaternion).invert() - worldQuaternionInv.copy(worldQuaternion).invert() - } - - this.camera.updateMatrixWorld() - this.camera.matrixWorld.decompose(cameraPosition, cameraQuaternion, cameraScale) - - eye.copy(cameraPosition).sub(worldPosition).normalize() - - Object3D.prototype.updateMatrixWorld.call(this) - } - - this.pointerHover = function (pointer) { - if (this.object === undefined || this.dragging === true) return - - raycaster.setFromCamera(pointer, this.camera) - - const intersect = intersectObjectWithRay(_gizmo.picker[this.mode], raycaster) - - if (intersect) { - this.axis = intersect.object.name - } else { - this.axis = null - } - } - - this.pointerDown = function (pointer) { - if (this.object === undefined || this.dragging === true || pointer.button !== 0) return - - if (this.axis !== null) { - raycaster.setFromCamera(pointer, this.camera) - - const planeIntersect = intersectObjectWithRay(_plane, raycaster, true) - - if (planeIntersect) { - let space = this.space - - if (this.mode === 'scale') { - space = 'local' - } else if (this.axis === 'E' || this.axis === 'XYZE' || this.axis === 'XYZ') { - space = 'world' - } - - if (space === 'local' && this.mode === 'rotate') { - const snap = this.rotationSnap - - if (this.axis === 'X' && snap) this.object.rotation.x = Math.round(this.object.rotation.x / snap) * snap - if (this.axis === 'Y' && snap) this.object.rotation.y = Math.round(this.object.rotation.y / snap) * snap - if (this.axis === 'Z' && snap) this.object.rotation.z = Math.round(this.object.rotation.z / snap) * snap - } - - this.object.updateMatrixWorld() - this.object.parent.updateMatrixWorld() - - positionStart.copy(this.object.position) - quaternionStart.copy(this.object.quaternion) - scaleStart.copy(this.object.scale) - - this.object.matrixWorld.decompose(worldPositionStart, worldQuaternionStart, worldScaleStart) - - pointStart.copy(planeIntersect.point).sub(worldPositionStart) - } - - this.dragging = true - mouseDownEvent.mode = this.mode - this.dispatchEvent(mouseDownEvent) - } - } - - this.pointerMove = function (pointer) { - const axis = this.axis - const mode = this.mode - const object = this.object - let space = this.space - - if (mode === 'scale') { - space = 'local' - } else if (axis === 'E' || axis === 'XYZE' || axis === 'XYZ') { - space = 'world' - } - - if (object === undefined || axis === null || this.dragging === false || pointer.button !== -1) return - - raycaster.setFromCamera(pointer, this.camera) - - const planeIntersect = intersectObjectWithRay(_plane, raycaster, true) - - if (!planeIntersect) return - - pointEnd.copy(planeIntersect.point).sub(worldPositionStart) - - if (mode === 'translate') { - // Apply translate - - offset.copy(pointEnd).sub(pointStart) - - if (space === 'local' && axis !== 'XYZ') { - offset.applyQuaternion(worldQuaternionInv) - } - - if (axis.indexOf('X') === -1) offset.x = 0 - if (axis.indexOf('Y') === -1) offset.y = 0 - if (axis.indexOf('Z') === -1) offset.z = 0 - - if (space === 'local' && axis !== 'XYZ') { - offset.applyQuaternion(quaternionStart).divide(parentScale) - } else { - offset.applyQuaternion(parentQuaternionInv).divide(parentScale) - } - - object.position.copy(offset).add(positionStart) - - // Apply translation snap - - if (this.translationSnap) { - if (space === 'local') { - object.position.applyQuaternion(_tempQuaternion.copy(quaternionStart).invert()) - - if (axis.search('X') !== -1) { - object.position.x = Math.round(object.position.x / this.translationSnap) * this.translationSnap - } - - if (axis.search('Y') !== -1) { - object.position.y = Math.round(object.position.y / this.translationSnap) * this.translationSnap - } - - if (axis.search('Z') !== -1) { - object.position.z = Math.round(object.position.z / this.translationSnap) * this.translationSnap - } - - object.position.applyQuaternion(quaternionStart) - } - - if (space === 'world') { - if (object.parent) { - object.position.add(_tempVector.setFromMatrixPosition(object.parent.matrixWorld)) - } - - if (axis.search('X') !== -1) { - object.position.x = Math.round(object.position.x / this.translationSnap) * this.translationSnap - } - - if (axis.search('Y') !== -1) { - object.position.y = Math.round(object.position.y / this.translationSnap) * this.translationSnap - } - - if (axis.search('Z') !== -1) { - object.position.z = Math.round(object.position.z / this.translationSnap) * this.translationSnap - } - - if (object.parent) { - object.position.sub(_tempVector.setFromMatrixPosition(object.parent.matrixWorld)) - } - } - } - } else if (mode === 'scale') { - if (axis.search('XYZ') !== -1) { - let d = pointEnd.length() / pointStart.length() - - if (pointEnd.dot(pointStart) < 0) d *= -1 - - _tempVector2.set(d, d, d) - } else { - _tempVector.copy(pointStart) - _tempVector2.copy(pointEnd) - - _tempVector.applyQuaternion(worldQuaternionInv) - _tempVector2.applyQuaternion(worldQuaternionInv) - - _tempVector2.divide(_tempVector) - - if (axis.search('X') === -1) { - _tempVector2.x = 1 - } - - if (axis.search('Y') === -1) { - _tempVector2.y = 1 - } - - if (axis.search('Z') === -1) { - _tempVector2.z = 1 - } - } - - // Apply scale - - object.scale.copy(scaleStart).multiply(_tempVector2) - - if (this.scaleSnap) { - if (axis.search('X') !== -1) { - object.scale.x = Math.round(object.scale.x / this.scaleSnap) * this.scaleSnap || this.scaleSnap - } - - if (axis.search('Y') !== -1) { - object.scale.y = Math.round(object.scale.y / this.scaleSnap) * this.scaleSnap || this.scaleSnap - } - - if (axis.search('Z') !== -1) { - object.scale.z = Math.round(object.scale.z / this.scaleSnap) * this.scaleSnap || this.scaleSnap - } - } - } else if (mode === 'rotate') { - offset.copy(pointEnd).sub(pointStart) - - const ROTATION_SPEED = 20 / worldPosition.distanceTo(_tempVector.setFromMatrixPosition(this.camera.matrixWorld)) - - if (axis === 'E') { - rotationAxis.copy(eye) - rotationAngle = pointEnd.angleTo(pointStart) - - startNorm.copy(pointStart).normalize() - endNorm.copy(pointEnd).normalize() - - rotationAngle *= endNorm.cross(startNorm).dot(eye) < 0 ? 1 : -1 - } else if (axis === 'XYZE') { - rotationAxis.copy(offset).cross(eye).normalize() - rotationAngle = offset.dot(_tempVector.copy(rotationAxis).cross(this.eye)) * ROTATION_SPEED - } else if (axis === 'X' || axis === 'Y' || axis === 'Z') { - rotationAxis.copy(_unit[axis]) - - _tempVector.copy(_unit[axis]) - - if (space === 'local') { - _tempVector.applyQuaternion(worldQuaternion) - } - - rotationAngle = offset.dot(_tempVector.cross(eye).normalize()) * ROTATION_SPEED - } - - // Apply rotation snap - - if (this.rotationSnap) rotationAngle = Math.round(rotationAngle / this.rotationSnap) * this.rotationSnap - - this.rotationAngle = rotationAngle - - // Apply rotate - if (space === 'local' && axis !== 'E' && axis !== 'XYZE') { - object.quaternion.copy(quaternionStart) - object.quaternion.multiply(_tempQuaternion.setFromAxisAngle(rotationAxis, rotationAngle)).normalize() - } else { - rotationAxis.applyQuaternion(parentQuaternionInv) - object.quaternion.copy(_tempQuaternion.setFromAxisAngle(rotationAxis, rotationAngle)) - object.quaternion.multiply(quaternionStart).normalize() - } - } - - this.dispatchEvent(changeEvent) - this.dispatchEvent(objectChangeEvent) - } - - this.pointerUp = function (pointer) { - if (pointer.button !== 0) return - - if (this.dragging && this.axis !== null) { - mouseUpEvent.mode = this.mode - this.dispatchEvent(mouseUpEvent) - } - - this.dragging = false - this.axis = null - } - - // normalize mouse / touch pointer and remap {x,y} to view space. - - function getPointer(event) { - if (scope.domElement.ownerDocument.pointerLockElement) { - return { - x: 0, - y: 0, - button: event.button, - } - } else { - const pointer = event.changedTouches ? event.changedTouches[0] : event - - const rect = domElement.getBoundingClientRect() - - return { - x: ((pointer.clientX - rect.left) / rect.width) * 2 - 1, - y: (-(pointer.clientY - rect.top) / rect.height) * 2 + 1, - button: event.button, - } - } - } - - // mouse / touch event handlers - - function onPointerHover(event) { - if (!scope.enabled) return - - switch (event.pointerType) { - case 'mouse': - case 'pen': - scope.pointerHover(getPointer(event)) - break - } - } - - function onPointerDown(event) { - if (!scope.enabled) return - - scope.domElement.style.touchAction = 'none' // disable touch scroll - scope.domElement.ownerDocument.addEventListener('pointermove', onPointerMove) - - scope.pointerHover(getPointer(event)) - scope.pointerDown(getPointer(event)) - } - - function onPointerMove(event) { - if (!scope.enabled) return - - scope.pointerMove(getPointer(event)) - } - - function onPointerUp(event) { - if (!scope.enabled) return - - scope.domElement.style.touchAction = '' - scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove) - - scope.pointerUp(getPointer(event)) - } - - // TODO: deprecate - - this.getMode = () => scope.mode - - this.setMode = (mode) => { - scope.mode = mode - } - - this.setTranslationSnap = (translationSnap) => { - scope.translationSnap = translationSnap - } - - this.setRotationSnap = (rotationSnap) => { - scope.rotationSnap = rotationSnap - } - - this.setScaleSnap = (scaleSnap) => { - scope.scaleSnap = scaleSnap - } - - this.setSize = (size) => { - scope.size = size - } - - this.setSpace = (space) => { - scope.space = space - } - - this.update = () => { - console.warn( - 'THREE.TransformControls: update function has no more functionality and therefore has been deprecated.', - ) - } -} - -TransformControls.prototype = Object.assign(Object.create(Object3D.prototype), { - constructor: TransformControls, - - isTransformControls: true, -}) - -var TransformControlsGizmo = function () { - 'use strict' - - Object3D.call(this) - - this.type = 'TransformControlsGizmo' - - // shared materials - - const gizmoMaterial = new MeshBasicMaterial({ - depthTest: false, - depthWrite: false, - transparent: true, - side: DoubleSide, - fog: false, - toneMapped: false, - }) - - const gizmoLineMaterial = new LineBasicMaterial({ - depthTest: false, - depthWrite: false, - transparent: true, - linewidth: 1, - fog: false, - toneMapped: false, - }) - - // Make unique material for each axis/color - - const matInvisible = gizmoMaterial.clone() - matInvisible.opacity = 0.15 - - const matHelper = gizmoMaterial.clone() - matHelper.opacity = 0.33 - - const matRed = gizmoMaterial.clone() - matRed.color.set(0xff0000) - - const matGreen = gizmoMaterial.clone() - matGreen.color.set(0x00ff00) - - const matBlue = gizmoMaterial.clone() - matBlue.color.set(0x0000ff) - - const matWhiteTransparent = gizmoMaterial.clone() - matWhiteTransparent.opacity = 0.25 - - const matYellowTransparent = matWhiteTransparent.clone() - matYellowTransparent.color.set(0xffff00) - - const matCyanTransparent = matWhiteTransparent.clone() - matCyanTransparent.color.set(0x00ffff) - - const matMagentaTransparent = matWhiteTransparent.clone() - matMagentaTransparent.color.set(0xff00ff) - - const matYellow = gizmoMaterial.clone() - matYellow.color.set(0xffff00) - - const matLineRed = gizmoLineMaterial.clone() - matLineRed.color.set(0xff0000) - - const matLineGreen = gizmoLineMaterial.clone() - matLineGreen.color.set(0x00ff00) - - const matLineBlue = gizmoLineMaterial.clone() - matLineBlue.color.set(0x0000ff) - - const matLineCyan = gizmoLineMaterial.clone() - matLineCyan.color.set(0x00ffff) - - const matLineMagenta = gizmoLineMaterial.clone() - matLineMagenta.color.set(0xff00ff) - - const matLineYellow = gizmoLineMaterial.clone() - matLineYellow.color.set(0xffff00) - - const matLineGray = gizmoLineMaterial.clone() - matLineGray.color.set(0x787878) - - const matLineYellowTransparent = matLineYellow.clone() - matLineYellowTransparent.opacity = 0.25 - - // reusable geometry - - const arrowGeometry = new CylinderGeometry(0, 0.05, 0.2, 12, 1, false) - - const scaleHandleGeometry = new BoxGeometry(0.125, 0.125, 0.125) - - const lineGeometry = new BufferGeometry() - lineGeometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3)) - - const CircleGeometry = (radius, arc) => { - const geometry = new BufferGeometry() - const vertices = [] - - for (let i = 0; i <= 64 * arc; ++i) { - vertices.push(0, Math.cos((i / 32) * Math.PI) * radius, Math.sin((i / 32) * Math.PI) * radius) - } - - geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)) - - return geometry - } - - // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position - - const TranslateHelperGeometry = () => { - const geometry = new BufferGeometry() - - geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 1, 1], 3)) - - return geometry - } - - // Gizmo definitions - custom hierarchy definitions for setupGizmo() function - - const gizmoTranslate = { - X: [ - [new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, -Math.PI / 2], null, 'fwd'], - [new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, Math.PI / 2], null, 'bwd'], - [new Line(lineGeometry, matLineRed)], - ], - Y: [ - [new Mesh(arrowGeometry, matGreen), [0, 1, 0], null, null, 'fwd'], - [new Mesh(arrowGeometry, matGreen), [0, 1, 0], [Math.PI, 0, 0], null, 'bwd'], - [new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2]], - ], - Z: [ - [new Mesh(arrowGeometry, matBlue), [0, 0, 1], [Math.PI / 2, 0, 0], null, 'fwd'], - [new Mesh(arrowGeometry, matBlue), [0, 0, 1], [-Math.PI / 2, 0, 0], null, 'bwd'], - [new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0]], - ], - XYZ: [[new Mesh(new OctahedronGeometry(0.1, 0), matWhiteTransparent.clone()), [0, 0, 0], [0, 0, 0]]], - XY: [ - [new Mesh(new PlaneGeometry(0.295, 0.295), matYellowTransparent.clone()), [0.15, 0.15, 0]], - [new Line(lineGeometry, matLineYellow), [0.18, 0.3, 0], null, [0.125, 1, 1]], - [new Line(lineGeometry, matLineYellow), [0.3, 0.18, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]], - ], - YZ: [ - [new Mesh(new PlaneGeometry(0.295, 0.295), matCyanTransparent.clone()), [0, 0.15, 0.15], [0, Math.PI / 2, 0]], - [new Line(lineGeometry, matLineCyan), [0, 0.18, 0.3], [0, 0, Math.PI / 2], [0.125, 1, 1]], - [new Line(lineGeometry, matLineCyan), [0, 0.3, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]], - ], - XZ: [ - [new Mesh(new PlaneGeometry(0.295, 0.295), matMagentaTransparent.clone()), [0.15, 0, 0.15], [-Math.PI / 2, 0, 0]], - [new Line(lineGeometry, matLineMagenta), [0.18, 0, 0.3], null, [0.125, 1, 1]], - [new Line(lineGeometry, matLineMagenta), [0.3, 0, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]], - ], - } - - const pickerTranslate = { - X: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0.6, 0, 0], [0, 0, -Math.PI / 2]]], - Y: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0.6, 0]]], - Z: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0, 0.6], [Math.PI / 2, 0, 0]]], - XYZ: [[new Mesh(new OctahedronGeometry(0.2, 0), matInvisible)]], - XY: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0.2, 0.2, 0]]], - YZ: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0, 0.2, 0.2], [0, Math.PI / 2, 0]]], - XZ: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0.2, 0, 0.2], [-Math.PI / 2, 0, 0]]], - } - - const helperTranslate = { - START: [[new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, 'helper']], - END: [[new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, 'helper']], - DELTA: [[new Line(TranslateHelperGeometry(), matHelper), null, null, null, 'helper']], - X: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], - Y: [[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']], - Z: [[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']], - } - - const gizmoRotate = { - X: [ - [new Line(CircleGeometry(1, 0.5), matLineRed)], - [new Mesh(new OctahedronGeometry(0.04, 0), matRed), [0, 0, 0.99], null, [1, 3, 1]], - ], - Y: [ - [new Line(CircleGeometry(1, 0.5), matLineGreen), null, [0, 0, -Math.PI / 2]], - [new Mesh(new OctahedronGeometry(0.04, 0), matGreen), [0, 0, 0.99], null, [3, 1, 1]], - ], - Z: [ - [new Line(CircleGeometry(1, 0.5), matLineBlue), null, [0, Math.PI / 2, 0]], - [new Mesh(new OctahedronGeometry(0.04, 0), matBlue), [0.99, 0, 0], null, [1, 3, 1]], - ], - E: [ - [new Line(CircleGeometry(1.25, 1), matLineYellowTransparent), null, [0, Math.PI / 2, 0]], - [ - new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), - [1.17, 0, 0], - [0, 0, -Math.PI / 2], - [1, 1, 0.001], - ], - [ - new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), - [-1.17, 0, 0], - [0, 0, Math.PI / 2], - [1, 1, 0.001], - ], - [ - new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), - [0, -1.17, 0], - [Math.PI, 0, 0], - [1, 1, 0.001], - ], - [ - new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), - [0, 1.17, 0], - [0, 0, 0], - [1, 1, 0.001], - ], - ], - XYZE: [[new Line(CircleGeometry(1, 1), matLineGray), null, [0, Math.PI / 2, 0]]], - } - - const helperRotate = { - AXIS: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], - } - - const pickerRotate = { - X: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, -Math.PI / 2, -Math.PI / 2]]], - Y: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [Math.PI / 2, 0, 0]]], - Z: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, 0, -Math.PI / 2]]], - E: [[new Mesh(new TorusGeometry(1.25, 0.1, 2, 24), matInvisible)]], - XYZE: [[new Mesh(new SphereGeometry(0.7, 10, 8), matInvisible)]], - } - - const gizmoScale = { - X: [ - [new Mesh(scaleHandleGeometry, matRed), [0.8, 0, 0], [0, 0, -Math.PI / 2]], - [new Line(lineGeometry, matLineRed), null, null, [0.8, 1, 1]], - ], - Y: [ - [new Mesh(scaleHandleGeometry, matGreen), [0, 0.8, 0]], - [new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2], [0.8, 1, 1]], - ], - Z: [ - [new Mesh(scaleHandleGeometry, matBlue), [0, 0, 0.8], [Math.PI / 2, 0, 0]], - [new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0], [0.8, 1, 1]], - ], - XY: [ - [new Mesh(scaleHandleGeometry, matYellowTransparent), [0.85, 0.85, 0], null, [2, 2, 0.2]], - [new Line(lineGeometry, matLineYellow), [0.855, 0.98, 0], null, [0.125, 1, 1]], - [new Line(lineGeometry, matLineYellow), [0.98, 0.855, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]], - ], - YZ: [ - [new Mesh(scaleHandleGeometry, matCyanTransparent), [0, 0.85, 0.85], null, [0.2, 2, 2]], - [new Line(lineGeometry, matLineCyan), [0, 0.855, 0.98], [0, 0, Math.PI / 2], [0.125, 1, 1]], - [new Line(lineGeometry, matLineCyan), [0, 0.98, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]], - ], - XZ: [ - [new Mesh(scaleHandleGeometry, matMagentaTransparent), [0.85, 0, 0.85], null, [2, 0.2, 2]], - [new Line(lineGeometry, matLineMagenta), [0.855, 0, 0.98], null, [0.125, 1, 1]], - [new Line(lineGeometry, matLineMagenta), [0.98, 0, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]], - ], - XYZX: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [1.1, 0, 0]]], - XYZY: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [0, 1.1, 0]]], - XYZZ: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [0, 0, 1.1]]], - } - - const pickerScale = { - X: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0.5, 0, 0], [0, 0, -Math.PI / 2]]], - Y: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0.5, 0]]], - Z: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0, 0.5], [Math.PI / 2, 0, 0]]], - XY: [[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0.85, 0], null, [3, 3, 0.2]]], - YZ: [[new Mesh(scaleHandleGeometry, matInvisible), [0, 0.85, 0.85], null, [0.2, 3, 3]]], - XZ: [[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0, 0.85], null, [3, 0.2, 3]]], - XYZX: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [1.1, 0, 0]]], - XYZY: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [0, 1.1, 0]]], - XYZZ: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [0, 0, 1.1]]], - } - - const helperScale = { - X: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], - Y: [[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']], - Z: [[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']], - } - - // Creates an Object3D with gizmos described in custom hierarchy definition. - - const setupGizmo = (gizmoMap) => { - const gizmo = new Object3D() - - for (let name in gizmoMap) { - for (let i = gizmoMap[name].length; i--; ) { - const object = gizmoMap[name][i][0].clone() - const position = gizmoMap[name][i][1] - const rotation = gizmoMap[name][i][2] - const scale = gizmoMap[name][i][3] - const tag = gizmoMap[name][i][4] - - // name and tag properties are essential for picking and updating logic. - object.name = name - object.tag = tag - - if (position) { - object.position.set(position[0], position[1], position[2]) - } - - if (rotation) { - object.rotation.set(rotation[0], rotation[1], rotation[2]) - } - - if (scale) { - object.scale.set(scale[0], scale[1], scale[2]) - } - - object.updateMatrix() - - const tempGeometry = object.geometry.clone() - tempGeometry.applyMatrix4(object.matrix) - object.geometry = tempGeometry - object.renderOrder = Infinity - - object.position.set(0, 0, 0) - object.rotation.set(0, 0, 0) - object.scale.set(1, 1, 1) - - gizmo.add(object) - } - } - - return gizmo - } - - // Reusable utility variables - - const tempVector = new Vector3(0, 0, 0) - const tempEuler = new Euler() - const alignVector = new Vector3(0, 1, 0) - const zeroVector = new Vector3(0, 0, 0) - const lookAtMatrix = new Matrix4() - const tempQuaternion = new Quaternion() - const tempQuaternion2 = new Quaternion() - const identityQuaternion = new Quaternion() - - const unitX = new Vector3(1, 0, 0) - const unitY = new Vector3(0, 1, 0) - const unitZ = new Vector3(0, 0, 1) - - // Gizmo creation - - this.gizmo = {} - this.picker = {} - this.helper = {} - - this.add((this.gizmo['translate'] = setupGizmo(gizmoTranslate))) - this.add((this.gizmo['rotate'] = setupGizmo(gizmoRotate))) - this.add((this.gizmo['scale'] = setupGizmo(gizmoScale))) - this.add((this.picker['translate'] = setupGizmo(pickerTranslate))) - this.add((this.picker['rotate'] = setupGizmo(pickerRotate))) - this.add((this.picker['scale'] = setupGizmo(pickerScale))) - this.add((this.helper['translate'] = setupGizmo(helperTranslate))) - this.add((this.helper['rotate'] = setupGizmo(helperRotate))) - this.add((this.helper['scale'] = setupGizmo(helperScale))) - - // Pickers should be hidden always - - this.picker['translate'].visible = false - this.picker['rotate'].visible = false - this.picker['scale'].visible = false - - // updateMatrixWorld will update transformations and appearance of individual handles - - this.updateMatrixWorld = function () { - let space = this.space - - if (this.mode === 'scale') space = 'local' // scale always oriented to local rotation - - const quaternion = space === 'local' ? this.worldQuaternion : identityQuaternion - - // Show only gizmos for current transform mode - - this.gizmo['translate'].visible = this.mode === 'translate' - this.gizmo['rotate'].visible = this.mode === 'rotate' - this.gizmo['scale'].visible = this.mode === 'scale' - - this.helper['translate'].visible = this.mode === 'translate' - this.helper['rotate'].visible = this.mode === 'rotate' - this.helper['scale'].visible = this.mode === 'scale' - - let handles = [] - handles = handles.concat(this.picker[this.mode].children) - handles = handles.concat(this.gizmo[this.mode].children) - handles = handles.concat(this.helper[this.mode].children) - - for (let i = 0; i < handles.length; i++) { - const handle = handles[i] - - // hide aligned to camera - - handle.visible = true - handle.rotation.set(0, 0, 0) - handle.position.copy(this.worldPosition) - - let factor - - if (this.camera.isOrthographicCamera) { - factor = (this.camera.top - this.camera.bottom) / this.camera.zoom - } else { - factor = - this.worldPosition.distanceTo(this.cameraPosition) * - Math.min((1.9 * Math.tan((Math.PI * this.camera.fov) / 360)) / this.camera.zoom, 7) - } - - handle.scale.set(1, 1, 1).multiplyScalar((factor * this.size) / 7) - - // TODO: simplify helpers and consider decoupling from gizmo - - if (handle.tag === 'helper') { - handle.visible = false - - if (handle.name === 'AXIS') { - handle.position.copy(this.worldPositionStart) - handle.visible = !!this.axis - - if (this.axis === 'X') { - tempQuaternion.setFromEuler(tempEuler.set(0, 0, 0)) - handle.quaternion.copy(quaternion).multiply(tempQuaternion) - - if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) > 0.9) { - handle.visible = false - } - } - - if (this.axis === 'Y') { - tempQuaternion.setFromEuler(tempEuler.set(0, 0, Math.PI / 2)) - handle.quaternion.copy(quaternion).multiply(tempQuaternion) - - if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) > 0.9) { - handle.visible = false - } - } - - if (this.axis === 'Z') { - tempQuaternion.setFromEuler(tempEuler.set(0, Math.PI / 2, 0)) - handle.quaternion.copy(quaternion).multiply(tempQuaternion) - - if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) > 0.9) { - handle.visible = false - } - } - - if (this.axis === 'XYZE') { - tempQuaternion.setFromEuler(tempEuler.set(0, Math.PI / 2, 0)) - alignVector.copy(this.rotationAxis) - handle.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(zeroVector, alignVector, unitY)) - handle.quaternion.multiply(tempQuaternion) - handle.visible = this.dragging - } - - if (this.axis === 'E') { - handle.visible = false - } - } else if (handle.name === 'START') { - handle.position.copy(this.worldPositionStart) - handle.visible = this.dragging - } else if (handle.name === 'END') { - handle.position.copy(this.worldPosition) - handle.visible = this.dragging - } else if (handle.name === 'DELTA') { - handle.position.copy(this.worldPositionStart) - handle.quaternion.copy(this.worldQuaternionStart) - tempVector.set(1e-10, 1e-10, 1e-10).add(this.worldPositionStart).sub(this.worldPosition).multiplyScalar(-1) - tempVector.applyQuaternion(this.worldQuaternionStart.clone().invert()) - handle.scale.copy(tempVector) - handle.visible = this.dragging - } else { - handle.quaternion.copy(quaternion) - - if (this.dragging) { - handle.position.copy(this.worldPositionStart) - } else { - handle.position.copy(this.worldPosition) - } - - if (this.axis) { - handle.visible = this.axis.search(handle.name) !== -1 - } - } - - // If updating helper, skip rest of the loop - continue - } - - // Align handles to current local or world rotation - - handle.quaternion.copy(quaternion) - - if (this.mode === 'translate' || this.mode === 'scale') { - // Hide translate and scale axis facing the camera - - const AXIS_HIDE_TRESHOLD = 0.99 - const PLANE_HIDE_TRESHOLD = 0.2 - const AXIS_FLIP_TRESHOLD = 0.0 - - if (handle.name === 'X' || handle.name === 'XYZX') { - if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - if (handle.name === 'Y' || handle.name === 'XYZY') { - if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - if (handle.name === 'Z' || handle.name === 'XYZZ') { - if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - if (handle.name === 'XY') { - if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - if (handle.name === 'YZ') { - if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - if (handle.name === 'XZ') { - if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) { - handle.scale.set(1e-10, 1e-10, 1e-10) - handle.visible = false - } - } - - // Flip translate and scale axis ocluded behind another axis - - if (handle.name.search('X') !== -1) { - if (alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) { - if (handle.tag === 'fwd') { - handle.visible = false - } else { - handle.scale.x *= -1 - } - } else if (handle.tag === 'bwd') { - handle.visible = false - } - } - - if (handle.name.search('Y') !== -1) { - if (alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) { - if (handle.tag === 'fwd') { - handle.visible = false - } else { - handle.scale.y *= -1 - } - } else if (handle.tag === 'bwd') { - handle.visible = false - } - } - - if (handle.name.search('Z') !== -1) { - if (alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) { - if (handle.tag === 'fwd') { - handle.visible = false - } else { - handle.scale.z *= -1 - } - } else if (handle.tag === 'bwd') { - handle.visible = false - } - } - } else if (this.mode === 'rotate') { - // Align handles to current local or world rotation - - tempQuaternion2.copy(quaternion) - alignVector.copy(this.eye).applyQuaternion(tempQuaternion.copy(quaternion).invert()) - - if (handle.name.search('E') !== -1) { - handle.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(this.eye, zeroVector, unitY)) - } - - if (handle.name === 'X') { - tempQuaternion.setFromAxisAngle(unitX, Math.atan2(-alignVector.y, alignVector.z)) - tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion) - handle.quaternion.copy(tempQuaternion) - } - - if (handle.name === 'Y') { - tempQuaternion.setFromAxisAngle(unitY, Math.atan2(alignVector.x, alignVector.z)) - tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion) - handle.quaternion.copy(tempQuaternion) - } - - if (handle.name === 'Z') { - tempQuaternion.setFromAxisAngle(unitZ, Math.atan2(alignVector.y, alignVector.x)) - tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion) - handle.quaternion.copy(tempQuaternion) - } - } - - // Hide disabled axes - handle.visible = handle.visible && (handle.name.indexOf('X') === -1 || this.showX) - handle.visible = handle.visible && (handle.name.indexOf('Y') === -1 || this.showY) - handle.visible = handle.visible && (handle.name.indexOf('Z') === -1 || this.showZ) - handle.visible = handle.visible && (handle.name.indexOf('E') === -1 || (this.showX && this.showY && this.showZ)) - - // highlight selected axis - - handle.material._opacity = handle.material._opacity || handle.material.opacity - handle.material._color = handle.material._color || handle.material.color.clone() - - handle.material.color.copy(handle.material._color) - handle.material.opacity = handle.material._opacity - - if (!this.enabled) { - handle.material.opacity *= 0.5 - handle.material.color.lerp(new Color(1, 1, 1), 0.5) - } else if (this.axis) { - if (handle.name === this.axis) { - handle.material.opacity = 1.0 - handle.material.color.lerp(new Color(1, 1, 1), 0.5) - } else if (this.axis.split('').some((a) => handle.name === a)) { - handle.material.opacity = 1.0 - handle.material.color.lerp(new Color(1, 1, 1), 0.5) - } else { - handle.material.opacity *= 0.25 - handle.material.color.lerp(new Color(1, 1, 1), 0.5) - } - } - } - - Object3D.prototype.updateMatrixWorld.call(this) - } -} - -TransformControlsGizmo.prototype = Object.assign(Object.create(Object3D.prototype), { - constructor: TransformControlsGizmo, - - isTransformControlsGizmo: true, -}) - -var TransformControlsPlane = function () { - 'use strict' - - Mesh.call( - this, - new PlaneGeometry(100000, 100000, 2, 2), - new MeshBasicMaterial({ - visible: false, - wireframe: true, - side: DoubleSide, - transparent: true, - opacity: 0.1, - toneMapped: false, - }), - ) - - this.type = 'TransformControlsPlane' - - const unitX = new Vector3(1, 0, 0) - const unitY = new Vector3(0, 1, 0) - const unitZ = new Vector3(0, 0, 1) - - const tempVector = new Vector3() - const dirVector = new Vector3() - const alignVector = new Vector3() - const tempMatrix = new Matrix4() - const identityQuaternion = new Quaternion() - - this.updateMatrixWorld = function () { - let space = this.space - - this.position.copy(this.worldPosition) - - if (this.mode === 'scale') space = 'local' // scale always oriented to local rotation - - unitX.set(1, 0, 0).applyQuaternion(space === 'local' ? this.worldQuaternion : identityQuaternion) - unitY.set(0, 1, 0).applyQuaternion(space === 'local' ? this.worldQuaternion : identityQuaternion) - unitZ.set(0, 0, 1).applyQuaternion(space === 'local' ? this.worldQuaternion : identityQuaternion) - - // Align the plane for current transform mode, axis and space. - - alignVector.copy(unitY) - - switch (this.mode) { - case 'translate': - case 'scale': - switch (this.axis) { - case 'X': - alignVector.copy(this.eye).cross(unitX) - dirVector.copy(unitX).cross(alignVector) - break - case 'Y': - alignVector.copy(this.eye).cross(unitY) - dirVector.copy(unitY).cross(alignVector) - break - case 'Z': - alignVector.copy(this.eye).cross(unitZ) - dirVector.copy(unitZ).cross(alignVector) - break - case 'XY': - dirVector.copy(unitZ) - break - case 'YZ': - dirVector.copy(unitX) - break - case 'XZ': - alignVector.copy(unitZ) - dirVector.copy(unitY) - break - case 'XYZ': - case 'E': - dirVector.set(0, 0, 0) - break - } - - break - case 'rotate': - default: - // special case for rotate - dirVector.set(0, 0, 0) - } - - if (dirVector.length() === 0) { - // If in rotate mode, make the plane parallel to camera - this.quaternion.copy(this.cameraQuaternion) - } else { - tempMatrix.lookAt(tempVector.set(0, 0, 0), dirVector, alignVector) - - this.quaternion.setFromRotationMatrix(tempMatrix) - } - - Object3D.prototype.updateMatrixWorld.call(this) - } -} - -TransformControlsPlane.prototype = Object.assign(Object.create(Mesh.prototype), { - constructor: TransformControlsPlane, - - isTransformControlsPlane: true, -}) - -export { TransformControls, TransformControlsGizmo, TransformControlsPlane } diff --git a/src/controls/TransformControls.ts b/src/controls/TransformControls.ts new file mode 100644 index 00000000..b5f4d914 --- /dev/null +++ b/src/controls/TransformControls.ts @@ -0,0 +1,1388 @@ +import { + BoxGeometry, + BufferGeometry, + Color, + CylinderGeometry, + DoubleSide, + Euler, + Float32BufferAttribute, + Line, + LineBasicMaterial, + Material, + Matrix4, + Mesh, + MeshBasicMaterial, + Object3D, + OctahedronGeometry, + OrthographicCamera, + PerspectiveCamera, + PlaneGeometry, + Quaternion, + Raycaster, + SphereGeometry, + TorusGeometry, + Vector3, +} from 'three' + +export interface TransformControlsPointerObject { + x: number + y: number + button: number +} + +class TransformControls extends Object3D { + isTransformControls = true + + visible = false + + domElement: HTMLElement | Document + + private raycaster = new Raycaster() + + private _gizmo = new TransformControlsGizmo() + private _plane = new TransformControlsPlane() + + private _tempVector = new Vector3() + private _tempVector2 = new Vector3() + private _tempQuaternion = new Quaternion() + private _unit = { + X: new Vector3(1, 0, 0), + Y: new Vector3(0, 1, 0), + Z: new Vector3(0, 0, 1), + } + + private _pointStart = new Vector3() + private _pointEnd = new Vector3() + private _offset = new Vector3() + private _rotationAxis = new Vector3() + private _startNorm = new Vector3() + private _endNorm = new Vector3() + private _rotationAngle = 0 + + private _cameraPosition = new Vector3() + private _cameraQuaternion = new Quaternion() + private _cameraScale = new Vector3() + + private _parentPosition = new Vector3() + private _parentQuaternion = new Quaternion() + private _parentQuaternionInv = new Quaternion() + private _parentScale = new Vector3() + + private _worldPositionStart = new Vector3() + private _worldQuaternionStart = new Quaternion() + private _worldScaleStart = new Vector3() + + private _worldPosition = new Vector3() + private _worldQuaternion = new Quaternion() + private _worldQuaternionInv = new Quaternion() + private _worldScale = new Vector3() + + private _eye = new Vector3() + + private _positionStart = new Vector3() + private _quaternionStart = new Quaternion() + private _scaleStart = new Vector3() + + private _camera: PerspectiveCamera | OrthographicCamera + private _object: Object3D | undefined + private _enabled = true + private _axis: string | null = null + private _mode: 'translate' | 'rotate' | 'scale' = 'translate' + private _translationSnap: number | null = null + private _rotationSnap: number | null = null + private _scaleSnap: number | null = null + private _space = 'world' + private _size = 1 + private _dragging = false + private _showX = true + private _showY = true + private _showZ = true + + // events + changeEvent = { type: 'change' } + mouseDownEvent = { type: 'mouseDown' } + mouseUpEvent = { type: 'mouseUp', mode: this._mode } + objectChangeEvent = { type: 'objectChange' } + + constructor(camera: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super() + + if (domElement === undefined) { + console.warn('THREE.TransformControls: The second parameter "domElement" is now mandatory.') + this.domElement = document + } + + this.domElement = domElement + this._camera = camera + + this.add(this._gizmo) + this.add(this._plane) + + defineProperty('camera', this._camera) + defineProperty('object', this._object) + defineProperty('enabled', this._enabled) + defineProperty('axis', this._axis) + defineProperty('mode', this._mode) + defineProperty('translationSnap', this._translationSnap) + defineProperty('rotationSnap', this._rotationSnap) + defineProperty('scaleSnap', this._scaleSnap) + defineProperty('space', this._space) + defineProperty('size', this._size) + defineProperty('dragging', this._dragging) + defineProperty('showX', this._showX) + defineProperty('showY', this._showY) + defineProperty('showZ', this._showZ) + defineProperty('worldPosition', this._worldPosition) + defineProperty('worldPositionStart', this._worldPositionStart) + defineProperty('worldQuaternion', this._worldQuaternion) + defineProperty('worldQuaternionStart', this._worldQuaternionStart) + defineProperty('cameraPosition', this._cameraPosition) + defineProperty('cameraQuaternion', this._cameraQuaternion) + defineProperty('pointStart', this._pointStart) + defineProperty('pointEnd', this._pointEnd) + defineProperty('rotationAxis', this._rotationAxis) + defineProperty('rotationAngle', this._rotationAngle) + defineProperty('eye', this._eye) + + { + domElement.addEventListener('pointerdown', this.onPointerDown) + domElement.addEventListener('pointermove', this.onPointerHover) + this.domElement.ownerDocument.addEventListener('pointerup', this.onPointerUp) + } + + // Defined getter, setter and store for a property + function defineProperty(propName: string, defaultValue: TValue) { + Object.defineProperty(this, propName, { + get: function () { + return this[propName] + }, + + set: function (value) { + if (this[propName] !== value) { + this[propName] = value + this._plane[propName] = value + this._gizmo[propName] = value + + this.dispatchEvent({ type: `${propName}-changed`, value }) + this.dispatchEvent(this.changeEvent) + } + }, + }) + + this._plane[propName] = defaultValue + this._gizmo[propName] = defaultValue + } + } + + private intersectObjectWithRay = (object: Object3D, raycaster: Raycaster, includeInvisible?: boolean) => { + const allIntersections = raycaster.intersectObject(object, true) + + for (let i = 0; i < allIntersections.length; i++) { + if (allIntersections[i].object.visible || includeInvisible) { + return allIntersections[i] + } + } + + return false + } + + // Set current object + public attach = (object: Object3D): this => { + this._object = object + this.visible = true + + return this + } + + // Detatch from object + public detach = (): this => { + this._object = undefined + this.visible = false + this._axis = null + + return this + } + + public updateMatrixWorld = () => { + if (this._object !== undefined) { + this._object.updateMatrixWorld() + + if (this._object.parent === null) { + console.error('TransformControls: The attached 3D object must be a part of the scene graph.') + } else { + this._object.parent.matrixWorld.decompose(this._parentPosition, this._parentQuaternion, this._parentScale) + } + + this._object.matrixWorld.decompose(this._worldPosition, this._worldQuaternion, this._worldScale) + + this._parentQuaternionInv.copy(this._parentQuaternion).invert() + this._worldQuaternionInv.copy(this._worldQuaternion).invert() + } + + this._camera.updateMatrixWorld() + this._camera.matrixWorld.decompose(this._cameraPosition, this._cameraQuaternion, this._cameraScale) + + this._eye.copy(this._cameraPosition).sub(this._worldPosition).normalize() + + Object3D.prototype.updateMatrixWorld.call(this) + } + + private pointerHover = (pointer: TransformControlsPointerObject) => { + if (this._object === undefined || this._dragging === true) return + + this.raycaster.setFromCamera(pointer, this._camera) + + const intersect = this.intersectObjectWithRay(this._gizmo.picker[this._mode], this.raycaster) + + if (intersect) { + this._axis = intersect.object.name + } else { + this._axis = null + } + } + + private pointerDown = (pointer: TransformControlsPointerObject) => { + if (this._object === undefined || this._dragging === true || pointer.button !== 0) return + + if (this._axis !== null) { + this.raycaster.setFromCamera(pointer, this._camera) + + const planeIntersect = this.intersectObjectWithRay(this._plane, this.raycaster, true) + + if (planeIntersect) { + let space = this._space + + if (this._mode === 'scale') { + space = 'local' + } else if (this._axis === 'E' || this._axis === 'XYZE' || this._axis === 'XYZ') { + space = 'world' + } + + if (space === 'local' && this._mode === 'rotate') { + const snap = this._rotationSnap + + if (this._axis === 'X' && snap) this._object.rotation.x = Math.round(this._object.rotation.x / snap) * snap + if (this._axis === 'Y' && snap) this._object.rotation.y = Math.round(this._object.rotation.y / snap) * snap + if (this._axis === 'Z' && snap) this._object.rotation.z = Math.round(this._object.rotation.z / snap) * snap + } + + this._object.updateMatrixWorld() + + if (this._object.parent) { + this._object.parent.updateMatrixWorld() + } + + this._positionStart.copy(this._object.position) + this._quaternionStart.copy(this._object.quaternion) + this._scaleStart.copy(this._object.scale) + + this._object.matrixWorld.decompose(this._worldPositionStart, this._worldQuaternionStart, this._worldScaleStart) + + this._pointStart.copy(planeIntersect.point).sub(this._worldPositionStart) + } + + this._dragging = true + this.mouseDownEvent.type = this._mode + this.dispatchEvent(this.mouseDownEvent) + } + } + + private pointerMove = (pointer: TransformControlsPointerObject) => { + const axis = this._axis + const mode = this._mode + const object = this._object + let space = this._space + + if (mode === 'scale') { + space = 'local' + } else if (axis === 'E' || axis === 'XYZE' || axis === 'XYZ') { + space = 'world' + } + + if (object === undefined || axis === null || this._dragging === false || pointer.button !== -1) return + + this.raycaster.setFromCamera(pointer, this._camera) + + const planeIntersect = this.intersectObjectWithRay(this._plane, this.raycaster, true) + + if (!planeIntersect) return + + this._pointEnd.copy(planeIntersect.point).sub(this._worldPositionStart) + + if (mode === 'translate') { + // Apply translate + + this._offset.copy(this._pointEnd).sub(this._pointStart) + + if (space === 'local' && axis !== 'XYZ') { + this._offset.applyQuaternion(this._worldQuaternionInv) + } + + if (axis.indexOf('X') === -1) this._offset.x = 0 + if (axis.indexOf('Y') === -1) this._offset.y = 0 + if (axis.indexOf('Z') === -1) this._offset.z = 0 + + if (space === 'local' && axis !== 'XYZ') { + this._offset.applyQuaternion(this._quaternionStart).divide(this._parentScale) + } else { + this._offset.applyQuaternion(this._parentQuaternionInv).divide(this._parentScale) + } + + object.position.copy(this._offset).add(this._positionStart) + + // Apply translation snap + + if (this._translationSnap) { + if (space === 'local') { + object.position.applyQuaternion(this._tempQuaternion.copy(this._quaternionStart).invert()) + + if (axis.search('X') !== -1) { + object.position.x = Math.round(object.position.x / this._translationSnap) * this._translationSnap + } + + if (axis.search('Y') !== -1) { + object.position.y = Math.round(object.position.y / this._translationSnap) * this._translationSnap + } + + if (axis.search('Z') !== -1) { + object.position.z = Math.round(object.position.z / this._translationSnap) * this._translationSnap + } + + object.position.applyQuaternion(this._quaternionStart) + } + + if (space === 'world') { + if (object.parent) { + object.position.add(this._tempVector.setFromMatrixPosition(object.parent.matrixWorld)) + } + + if (axis.search('X') !== -1) { + object.position.x = Math.round(object.position.x / this._translationSnap) * this._translationSnap + } + + if (axis.search('Y') !== -1) { + object.position.y = Math.round(object.position.y / this._translationSnap) * this._translationSnap + } + + if (axis.search('Z') !== -1) { + object.position.z = Math.round(object.position.z / this._translationSnap) * this._translationSnap + } + + if (object.parent) { + object.position.sub(this._tempVector.setFromMatrixPosition(object.parent.matrixWorld)) + } + } + } + } else if (mode === 'scale') { + if (axis.search('XYZ') !== -1) { + let d = this._pointEnd.length() / this._pointStart.length() + + if (this._pointEnd.dot(this._pointStart) < 0) d *= -1 + + this._tempVector2.set(d, d, d) + } else { + this._tempVector.copy(this._pointStart) + this._tempVector2.copy(this._pointEnd) + + this._tempVector.applyQuaternion(this._worldQuaternionInv) + this._tempVector2.applyQuaternion(this._worldQuaternionInv) + + this._tempVector2.divide(this._tempVector) + + if (axis.search('X') === -1) { + this._tempVector2.x = 1 + } + + if (axis.search('Y') === -1) { + this._tempVector2.y = 1 + } + + if (axis.search('Z') === -1) { + this._tempVector2.z = 1 + } + } + + // Apply scale + + object.scale.copy(this._scaleStart).multiply(this._tempVector2) + + if (this._scaleSnap && this._object) { + if (axis.search('X') !== -1) { + this._object.scale.x = Math.round(object.scale.x / this._scaleSnap) * this._scaleSnap || this._scaleSnap + } + + if (axis.search('Y') !== -1) { + object.scale.y = Math.round(object.scale.y / this._scaleSnap) * this._scaleSnap || this._scaleSnap + } + + if (axis.search('Z') !== -1) { + object.scale.z = Math.round(object.scale.z / this._scaleSnap) * this._scaleSnap || this._scaleSnap + } + } + } else if (mode === 'rotate') { + this._offset.copy(this._pointEnd).sub(this._pointStart) + + const ROTATION_SPEED = + 20 / this._worldPosition.distanceTo(this._tempVector.setFromMatrixPosition(this._camera.matrixWorld)) + + if (axis === 'E') { + this._rotationAxis.copy(this._eye) + this._rotationAngle = this._pointEnd.angleTo(this._pointStart) + + this._startNorm.copy(this._pointStart).normalize() + this._endNorm.copy(this._pointEnd).normalize() + + this._rotationAngle *= this._endNorm.cross(this._startNorm).dot(this._eye) < 0 ? 1 : -1 + } else if (axis === 'XYZE') { + this._rotationAxis.copy(this._offset).cross(this._eye).normalize() + this._rotationAngle = + this._offset.dot(this._tempVector.copy(this._rotationAxis).cross(this._eye)) * ROTATION_SPEED + } else if (axis === 'X' || axis === 'Y' || axis === 'Z') { + this._rotationAxis.copy(this._unit[axis]) + + this._tempVector.copy(this._unit[axis]) + + if (space === 'local') { + this._tempVector.applyQuaternion(this._worldQuaternion) + } + + this._rotationAngle = this._offset.dot(this._tempVector.cross(this._eye).normalize()) * ROTATION_SPEED + } + + // Apply rotation snap + + if (this._rotationSnap) { + this._rotationAngle = Math.round(this._rotationAngle / this._rotationSnap) * this._rotationSnap + } + + // Apply rotate + if (space === 'local' && axis !== 'E' && axis !== 'XYZE') { + object.quaternion.copy(this._quaternionStart) + object.quaternion + .multiply(this._tempQuaternion.setFromAxisAngle(this._rotationAxis, this._rotationAngle)) + .normalize() + } else { + this._rotationAxis.applyQuaternion(this._parentQuaternionInv) + object.quaternion.copy(this._tempQuaternion.setFromAxisAngle(this._rotationAxis, this._rotationAngle)) + object.quaternion.multiply(this._quaternionStart).normalize() + } + } + + this.dispatchEvent(this.changeEvent) + this.dispatchEvent(this.objectChangeEvent) + } + + private pointerUp = (pointer: TransformControlsPointerObject) => { + if (pointer.button !== 0) return + + if (this._dragging && this._axis !== null) { + this.mouseUpEvent.mode = this._mode + this.dispatchEvent(this.mouseUpEvent) + } + + this._dragging = false + this._axis = null + } + + private getPointer = (event: Event): TransformControlsPointerObject => { + if (this.domElement && this.domElement.ownerDocument?.pointerLockElement) { + return { + x: 0, + y: 0, + button: (event as MouseEvent).button, + } + } else { + const pointer = (event as TouchEvent).changedTouches + ? (event as TouchEvent).changedTouches[0] + : (event as MouseEvent) + + const rect = (this.domElement as HTMLElement)?.getBoundingClientRect() + + return { + x: ((pointer.clientX - rect.left) / rect.width) * 2 - 1, + y: (-(pointer.clientY - rect.top) / rect.height) * 2 + 1, + button: (event as MouseEvent).button, + } + } + } + + private onPointerHover = (event: Event) => { + if (!this._enabled) return + + switch ((event as PointerEvent).pointerType) { + case 'mouse': + case 'pen': + this.pointerHover(this.getPointer(event)) + break + } + } + + private onPointerDown = (event: Event) => { + if (!this._enabled) return + ;(this.domElement as HTMLElement).style.touchAction = 'none' // disable touch scroll + this.domElement.ownerDocument?.addEventListener('pointermove', this.onPointerMove) + + this.pointerHover(this.getPointer(event)) + this.pointerDown(this.getPointer(event)) + } + + private onPointerMove = (event: Event) => { + if (!this._enabled) return + + this.pointerMove(this.getPointer(event)) + } + + private onPointerUp = (event: Event) => { + if (!this._enabled) return + ;(this.domElement as HTMLElement).style.touchAction = '' + this.domElement.ownerDocument?.removeEventListener('pointermove', this.onPointerMove) + + this.pointerUp(this.getPointer(event)) + } + + getMode = () => this._mode + + setMode = (mode: 'translate' | 'rotate' | 'scale') => { + this._mode = mode + } + + setTranslationSnap = (translationSnap: number) => { + this._translationSnap = translationSnap + } + + setRotationSnap = (rotationSnap: number) => { + this._rotationSnap = rotationSnap + } + + setScaleSnap = (scaleSnap: number) => { + this._scaleSnap = scaleSnap + } + + setSize = (size: number) => { + this._size = size + } + + setSpace = (space: string) => { + this._space = space + } + + public update = (): void => { + console.warn( + 'THREE.TransformControls: update function has no more functionality and therefore has been deprecated.', + ) + } + + public dispose = () => { + this.domElement.removeEventListener('pointerdown', this.onPointerDown) + this.domElement.removeEventListener('pointermove', this.onPointerHover) + this.domElement.ownerDocument?.removeEventListener('pointermove', this.onPointerMove) + this.domElement.ownerDocument?.removeEventListener('pointerup', this.onPointerUp) + + this.traverse((child) => { + const mesh = child as Mesh + if (mesh.geometry) { + mesh.geometry.dispose() + } + if (mesh.material) { + mesh.material.dispose() + } + }) + } +} + +type TransformControlsGizmoPrivateGizmos = { + ['translate']: Object3D + ['scale']: Object3D + ['rotate']: Object3D + ['visible']: boolean +} + +class TransformControlsGizmo extends Object3D { + isTransformControlsGizmo = true + type = 'TransformControlsGizmo' + + private tempVector = new Vector3(0, 0, 0) + private tempEuler = new Euler() + private alignVector = new Vector3(0, 1, 0) + private zeroVector = new Vector3(0, 0, 0) + private lookAtMatrix = new Matrix4() + private tempQuaternion = new Quaternion() + private tempQuaternion2 = new Quaternion() + private identityQuaternion = new Quaternion() + + private unitX = new Vector3(1, 0, 0) + private unitY = new Vector3(0, 1, 0) + private unitZ = new Vector3(0, 0, 1) + + gizmo: TransformControlsGizmoPrivateGizmos + picker: TransformControlsGizmoPrivateGizmos + helper: TransformControlsGizmoPrivateGizmos + + // these are set from parent class TransformControls + private _rotationAxis = new Vector3() + + private _cameraPosition = new Vector3() + + private _worldPositionStart = new Vector3() + private _worldQuaternionStart = new Quaternion() + + private _worldPosition = new Vector3() + private _worldQuaternion = new Quaternion() + + private _eye = new Vector3() + + private _camera: PerspectiveCamera | OrthographicCamera = null! + private _enabled = true + private _axis: string | null = null + private _mode: 'translate' | 'rotate' | 'scale' = 'translate' + private _space = 'world' + private _size = 1 + private _dragging = false + private _showX = true + private _showY = true + private _showZ = true + + constructor() { + super() + + const gizmoMaterial = new MeshBasicMaterial({ + depthTest: false, + depthWrite: false, + transparent: true, + side: DoubleSide, + fog: false, + toneMapped: false, + }) + + const gizmoLineMaterial = new LineBasicMaterial({ + depthTest: false, + depthWrite: false, + transparent: true, + linewidth: 1, + fog: false, + toneMapped: false, + }) + + // Make unique material for each axis/color + + const matInvisible = gizmoMaterial.clone() + matInvisible.opacity = 0.15 + + const matHelper = gizmoMaterial.clone() + matHelper.opacity = 0.33 + + const matRed = gizmoMaterial.clone() as MeshBasicMaterial + matRed.color.set(0xff0000) + + const matGreen = gizmoMaterial.clone() as MeshBasicMaterial + matGreen.color.set(0x00ff00) + + const matBlue = gizmoMaterial.clone() as MeshBasicMaterial + matBlue.color.set(0x0000ff) + + const matWhiteTransparent = gizmoMaterial.clone() as MeshBasicMaterial + matWhiteTransparent.opacity = 0.25 + + const matYellowTransparent = matWhiteTransparent.clone() as MeshBasicMaterial + matYellowTransparent.color.set(0xffff00) + + const matCyanTransparent = matWhiteTransparent.clone() as MeshBasicMaterial + matCyanTransparent.color.set(0x00ffff) + + const matMagentaTransparent = matWhiteTransparent.clone() as MeshBasicMaterial + matMagentaTransparent.color.set(0xff00ff) + + const matYellow = gizmoMaterial.clone() as MeshBasicMaterial + matYellow.color.set(0xffff00) + + const matLineRed = gizmoLineMaterial.clone() as LineBasicMaterial + matLineRed.color.set(0xff0000) + + const matLineGreen = gizmoLineMaterial.clone() as LineBasicMaterial + matLineGreen.color.set(0x00ff00) + + const matLineBlue = gizmoLineMaterial.clone() as LineBasicMaterial + matLineBlue.color.set(0x0000ff) + + const matLineCyan = gizmoLineMaterial.clone() as LineBasicMaterial + matLineCyan.color.set(0x00ffff) + + const matLineMagenta = gizmoLineMaterial.clone() as LineBasicMaterial + matLineMagenta.color.set(0xff00ff) + + const matLineYellow = gizmoLineMaterial.clone() as LineBasicMaterial + matLineYellow.color.set(0xffff00) + + const matLineGray = gizmoLineMaterial.clone() as LineBasicMaterial + matLineGray.color.set(0x787878) + + const matLineYellowTransparent = matLineYellow.clone() as LineBasicMaterial + matLineYellowTransparent.opacity = 0.25 + + // reusable geometry + + const arrowGeometry = new CylinderGeometry(0, 0.05, 0.2, 12, 1, false) + + const scaleHandleGeometry = new BoxGeometry(0.125, 0.125, 0.125) + + const lineGeometry = new BufferGeometry() + lineGeometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3)) + + const CircleGeometry = (radius: number, arc: number) => { + const geometry = new BufferGeometry() + const vertices = [] + + for (let i = 0; i <= 64 * arc; ++i) { + vertices.push(0, Math.cos((i / 32) * Math.PI) * radius, Math.sin((i / 32) * Math.PI) * radius) + } + + geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)) + + return geometry + } + + // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position + + const TranslateHelperGeometry = () => { + const geometry = new BufferGeometry() + + geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 1, 1], 3)) + + return geometry + } + + // Gizmo definitions - custom hierarchy definitions for setupGizmo() function + + const gizmoTranslate = { + X: [ + [new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, -Math.PI / 2], null, 'fwd'], + [new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, Math.PI / 2], null, 'bwd'], + [new Line(lineGeometry, matLineRed)], + ], + Y: [ + [new Mesh(arrowGeometry, matGreen), [0, 1, 0], null, null, 'fwd'], + [new Mesh(arrowGeometry, matGreen), [0, 1, 0], [Math.PI, 0, 0], null, 'bwd'], + [new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2]], + ], + Z: [ + [new Mesh(arrowGeometry, matBlue), [0, 0, 1], [Math.PI / 2, 0, 0], null, 'fwd'], + [new Mesh(arrowGeometry, matBlue), [0, 0, 1], [-Math.PI / 2, 0, 0], null, 'bwd'], + [new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0]], + ], + XYZ: [[new Mesh(new OctahedronGeometry(0.1, 0), matWhiteTransparent.clone()), [0, 0, 0], [0, 0, 0]]], + XY: [ + [new Mesh(new PlaneGeometry(0.295, 0.295), matYellowTransparent.clone()), [0.15, 0.15, 0]], + [new Line(lineGeometry, matLineYellow), [0.18, 0.3, 0], null, [0.125, 1, 1]], + [new Line(lineGeometry, matLineYellow), [0.3, 0.18, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]], + ], + YZ: [ + [new Mesh(new PlaneGeometry(0.295, 0.295), matCyanTransparent.clone()), [0, 0.15, 0.15], [0, Math.PI / 2, 0]], + [new Line(lineGeometry, matLineCyan), [0, 0.18, 0.3], [0, 0, Math.PI / 2], [0.125, 1, 1]], + [new Line(lineGeometry, matLineCyan), [0, 0.3, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]], + ], + XZ: [ + [ + new Mesh(new PlaneGeometry(0.295, 0.295), matMagentaTransparent.clone()), + [0.15, 0, 0.15], + [-Math.PI / 2, 0, 0], + ], + [new Line(lineGeometry, matLineMagenta), [0.18, 0, 0.3], null, [0.125, 1, 1]], + [new Line(lineGeometry, matLineMagenta), [0.3, 0, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]], + ], + } + + const pickerTranslate = { + X: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0.6, 0, 0], [0, 0, -Math.PI / 2]]], + Y: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0.6, 0]]], + Z: [[new Mesh(new CylinderGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0, 0.6], [Math.PI / 2, 0, 0]]], + XYZ: [[new Mesh(new OctahedronGeometry(0.2, 0), matInvisible)]], + XY: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0.2, 0.2, 0]]], + YZ: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0, 0.2, 0.2], [0, Math.PI / 2, 0]]], + XZ: [[new Mesh(new PlaneGeometry(0.4, 0.4), matInvisible), [0.2, 0, 0.2], [-Math.PI / 2, 0, 0]]], + } + + const helperTranslate = { + START: [[new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, 'helper']], + END: [[new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, 'helper']], + DELTA: [[new Line(TranslateHelperGeometry(), matHelper), null, null, null, 'helper']], + X: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], + Y: [[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']], + Z: [[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']], + } + + const gizmoRotate = { + X: [ + [new Line(CircleGeometry(1, 0.5), matLineRed)], + [new Mesh(new OctahedronGeometry(0.04, 0), matRed), [0, 0, 0.99], null, [1, 3, 1]], + ], + Y: [ + [new Line(CircleGeometry(1, 0.5), matLineGreen), null, [0, 0, -Math.PI / 2]], + [new Mesh(new OctahedronGeometry(0.04, 0), matGreen), [0, 0, 0.99], null, [3, 1, 1]], + ], + Z: [ + [new Line(CircleGeometry(1, 0.5), matLineBlue), null, [0, Math.PI / 2, 0]], + [new Mesh(new OctahedronGeometry(0.04, 0), matBlue), [0.99, 0, 0], null, [1, 3, 1]], + ], + E: [ + [new Line(CircleGeometry(1.25, 1), matLineYellowTransparent), null, [0, Math.PI / 2, 0]], + [ + new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), + [1.17, 0, 0], + [0, 0, -Math.PI / 2], + [1, 1, 0.001], + ], + [ + new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), + [-1.17, 0, 0], + [0, 0, Math.PI / 2], + [1, 1, 0.001], + ], + [ + new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), + [0, -1.17, 0], + [Math.PI, 0, 0], + [1, 1, 0.001], + ], + [ + new Mesh(new CylinderGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), + [0, 1.17, 0], + [0, 0, 0], + [1, 1, 0.001], + ], + ], + XYZE: [[new Line(CircleGeometry(1, 1), matLineGray), null, [0, Math.PI / 2, 0]]], + } + + const helperRotate = { + AXIS: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], + } + + const pickerRotate = { + X: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, -Math.PI / 2, -Math.PI / 2]]], + Y: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [Math.PI / 2, 0, 0]]], + Z: [[new Mesh(new TorusGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, 0, -Math.PI / 2]]], + E: [[new Mesh(new TorusGeometry(1.25, 0.1, 2, 24), matInvisible)]], + XYZE: [[new Mesh(new SphereGeometry(0.7, 10, 8), matInvisible)]], + } + + const gizmoScale = { + X: [ + [new Mesh(scaleHandleGeometry, matRed), [0.8, 0, 0], [0, 0, -Math.PI / 2]], + [new Line(lineGeometry, matLineRed), null, null, [0.8, 1, 1]], + ], + Y: [ + [new Mesh(scaleHandleGeometry, matGreen), [0, 0.8, 0]], + [new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2], [0.8, 1, 1]], + ], + Z: [ + [new Mesh(scaleHandleGeometry, matBlue), [0, 0, 0.8], [Math.PI / 2, 0, 0]], + [new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0], [0.8, 1, 1]], + ], + XY: [ + [new Mesh(scaleHandleGeometry, matYellowTransparent), [0.85, 0.85, 0], null, [2, 2, 0.2]], + [new Line(lineGeometry, matLineYellow), [0.855, 0.98, 0], null, [0.125, 1, 1]], + [new Line(lineGeometry, matLineYellow), [0.98, 0.855, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]], + ], + YZ: [ + [new Mesh(scaleHandleGeometry, matCyanTransparent), [0, 0.85, 0.85], null, [0.2, 2, 2]], + [new Line(lineGeometry, matLineCyan), [0, 0.855, 0.98], [0, 0, Math.PI / 2], [0.125, 1, 1]], + [new Line(lineGeometry, matLineCyan), [0, 0.98, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]], + ], + XZ: [ + [new Mesh(scaleHandleGeometry, matMagentaTransparent), [0.85, 0, 0.85], null, [2, 0.2, 2]], + [new Line(lineGeometry, matLineMagenta), [0.855, 0, 0.98], null, [0.125, 1, 1]], + [new Line(lineGeometry, matLineMagenta), [0.98, 0, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]], + ], + XYZX: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [1.1, 0, 0]]], + XYZY: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [0, 1.1, 0]]], + XYZZ: [[new Mesh(new BoxGeometry(0.125, 0.125, 0.125), matWhiteTransparent.clone()), [0, 0, 1.1]]], + } + + const pickerScale = { + X: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0.5, 0, 0], [0, 0, -Math.PI / 2]]], + Y: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0.5, 0]]], + Z: [[new Mesh(new CylinderGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0, 0.5], [Math.PI / 2, 0, 0]]], + XY: [[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0.85, 0], null, [3, 3, 0.2]]], + YZ: [[new Mesh(scaleHandleGeometry, matInvisible), [0, 0.85, 0.85], null, [0.2, 3, 3]]], + XZ: [[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0, 0.85], null, [3, 0.2, 3]]], + XYZX: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [1.1, 0, 0]]], + XYZY: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [0, 1.1, 0]]], + XYZZ: [[new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), [0, 0, 1.1]]], + } + + const helperScale = { + X: [[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']], + Y: [[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']], + Z: [[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']], + } + + // Creates an Object3D with gizmos described in custom hierarchy definition. + // this is nearly impossible to Type so i'm leaving it + const setupGizmo = (gizmoMap: any) => { + const gizmo = new Object3D() + + for (let name in gizmoMap) { + for (let i = gizmoMap[name].length; i--; ) { + const object = gizmoMap[name][i][0].clone() as Mesh + const position = gizmoMap[name][i][1] + const rotation = gizmoMap[name][i][2] + const scale = gizmoMap[name][i][3] + const tag = gizmoMap[name][i][4] + + // name and tag properties are essential for picking and updating logic. + object.name = name + // @ts-ignore + object.tag = tag + + if (position) { + object.position.set(position[0], position[1], position[2]) + } + + if (rotation) { + object.rotation.set(rotation[0], rotation[1], rotation[2]) + } + + if (scale) { + object.scale.set(scale[0], scale[1], scale[2]) + } + + object.updateMatrix() + + const tempGeometry = object.geometry.clone() + tempGeometry.applyMatrix4(object.matrix) + object.geometry = tempGeometry + object.renderOrder = Infinity + + object.position.set(0, 0, 0) + object.rotation.set(0, 0, 0) + object.scale.set(1, 1, 1) + + gizmo.add(object) + } + } + + return gizmo + } + + this.gizmo = {} as TransformControlsGizmoPrivateGizmos + this.picker = {} as TransformControlsGizmoPrivateGizmos + this.helper = {} as TransformControlsGizmoPrivateGizmos + + this.add((this.gizmo['translate'] = setupGizmo(gizmoTranslate))) + this.add((this.gizmo['rotate'] = setupGizmo(gizmoRotate))) + this.add((this.gizmo['scale'] = setupGizmo(gizmoScale))) + this.add((this.picker['translate'] = setupGizmo(pickerTranslate))) + this.add((this.picker['rotate'] = setupGizmo(pickerRotate))) + this.add((this.picker['scale'] = setupGizmo(pickerScale))) + this.add((this.helper['translate'] = setupGizmo(helperTranslate))) + this.add((this.helper['rotate'] = setupGizmo(helperRotate))) + this.add((this.helper['scale'] = setupGizmo(helperScale))) + + // Pickers should be hidden always + + this.picker['translate'].visible = false + this.picker['rotate'].visible = false + this.picker['scale'].visible = false + } + + // updateMatrixWorld will update transformations and appearance of individual handles + public updateMatrixWorld = () => { + let space = this._space + + if (this._mode === 'scale') space = 'local' // scale always oriented to local rotation + + const quaternion = space === 'local' ? this._worldQuaternion : this.identityQuaternion + + // Show only gizmos for current transform mode + + this.gizmo['translate'].visible = this._mode === 'translate' + this.gizmo['rotate'].visible = this._mode === 'rotate' + this.gizmo['scale'].visible = this._mode === 'scale' + + this.helper['translate'].visible = this._mode === 'translate' + this.helper['rotate'].visible = this._mode === 'rotate' + this.helper['scale'].visible = this._mode === 'scale' + + let handles: Array & { tag?: string }> = [] + handles = handles.concat(this.picker[this._mode].children as Mesh[]) + handles = handles.concat(this.gizmo[this._mode].children as Mesh[]) + handles = handles.concat(this.helper[this._mode].children as Mesh[]) + + for (let i = 0; i < handles.length; i++) { + const handle = handles[i] + + // hide aligned to camera + + handle.visible = true + handle.rotation.set(0, 0, 0) + handle.position.copy(this._worldPosition) + + let factor: number + + if ((this._camera as OrthographicCamera).isOrthographicCamera) { + factor = + ((this._camera as OrthographicCamera).top - (this._camera as OrthographicCamera).bottom) / this._camera.zoom + } else { + factor = + this._worldPosition.distanceTo(this._cameraPosition) * + Math.min((1.9 * Math.tan((Math.PI * (this._camera as PerspectiveCamera).fov) / 360)) / this._camera.zoom, 7) + } + + handle.scale.set(1, 1, 1).multiplyScalar((factor * this._size) / 7) + + // TODO: simplify helpers and consider decoupling from gizmo + + if (handle.tag === 'helper') { + handle.visible = false + + if (handle.name === 'AXIS') { + handle.position.copy(this._worldPositionStart) + handle.visible = !!this._axis + + if (this._axis === 'X') { + this.tempQuaternion.setFromEuler(this.tempEuler.set(0, 0, 0)) + handle.quaternion.copy(quaternion).multiply(this.tempQuaternion) + + if (Math.abs(this.alignVector.copy(this.unitX).applyQuaternion(quaternion).dot(this._eye)) > 0.9) { + handle.visible = false + } + } + + if (this._axis === 'Y') { + this.tempQuaternion.setFromEuler(this.tempEuler.set(0, 0, Math.PI / 2)) + handle.quaternion.copy(quaternion).multiply(this.tempQuaternion) + + if (Math.abs(this.alignVector.copy(this.unitY).applyQuaternion(quaternion).dot(this._eye)) > 0.9) { + handle.visible = false + } + } + + if (this._axis === 'Z') { + this.tempQuaternion.setFromEuler(this.tempEuler.set(0, Math.PI / 2, 0)) + handle.quaternion.copy(quaternion).multiply(this.tempQuaternion) + + if (Math.abs(this.alignVector.copy(this.unitZ).applyQuaternion(quaternion).dot(this._eye)) > 0.9) { + handle.visible = false + } + } + + if (this._axis === 'XYZE') { + this.tempQuaternion.setFromEuler(this.tempEuler.set(0, Math.PI / 2, 0)) + this.alignVector.copy(this._rotationAxis) + handle.quaternion.setFromRotationMatrix( + this.lookAtMatrix.lookAt(this.zeroVector, this.alignVector, this.unitY), + ) + handle.quaternion.multiply(this.tempQuaternion) + handle.visible = this._dragging + } + + if (this._axis === 'E') { + handle.visible = false + } + } else if (handle.name === 'START') { + handle.position.copy(this._worldPositionStart) + handle.visible = this._dragging + } else if (handle.name === 'END') { + handle.position.copy(this._worldPosition) + handle.visible = this._dragging + } else if (handle.name === 'DELTA') { + handle.position.copy(this._worldPositionStart) + handle.quaternion.copy(this._worldQuaternionStart) + this.tempVector + .set(1e-10, 1e-10, 1e-10) + .add(this._worldPositionStart) + .sub(this._worldPosition) + .multiplyScalar(-1) + this.tempVector.applyQuaternion(this._worldQuaternionStart.clone().invert()) + handle.scale.copy(this.tempVector) + handle.visible = this._dragging + } else { + handle.quaternion.copy(quaternion) + + if (this._dragging) { + handle.position.copy(this._worldPositionStart) + } else { + handle.position.copy(this._worldPosition) + } + + if (this._axis) { + handle.visible = this._axis.search(handle.name) !== -1 + } + } + + // If updating helper, skip rest of the loop + continue + } + + // Align handles to current local or world rotation + + handle.quaternion.copy(quaternion) + + if (this._mode === 'translate' || this._mode === 'scale') { + // Hide translate and scale axis facing the camera + + const AXIS_HIDE_TRESHOLD = 0.99 + const PLANE_HIDE_TRESHOLD = 0.2 + const AXIS_FLIP_TRESHOLD = 0.0 + + if (handle.name === 'X' || handle.name === 'XYZX') { + if ( + Math.abs(this.alignVector.copy(this.unitX).applyQuaternion(quaternion).dot(this._eye)) > AXIS_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + if (handle.name === 'Y' || handle.name === 'XYZY') { + if ( + Math.abs(this.alignVector.copy(this.unitY).applyQuaternion(quaternion).dot(this._eye)) > AXIS_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + if (handle.name === 'Z' || handle.name === 'XYZZ') { + if ( + Math.abs(this.alignVector.copy(this.unitZ).applyQuaternion(quaternion).dot(this._eye)) > AXIS_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + if (handle.name === 'XY') { + if ( + Math.abs(this.alignVector.copy(this.unitZ).applyQuaternion(quaternion).dot(this._eye)) < PLANE_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + if (handle.name === 'YZ') { + if ( + Math.abs(this.alignVector.copy(this.unitX).applyQuaternion(quaternion).dot(this._eye)) < PLANE_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + if (handle.name === 'XZ') { + if ( + Math.abs(this.alignVector.copy(this.unitY).applyQuaternion(quaternion).dot(this._eye)) < PLANE_HIDE_TRESHOLD + ) { + handle.scale.set(1e-10, 1e-10, 1e-10) + handle.visible = false + } + } + + // Flip translate and scale axis ocluded behind another axis + + if (handle.name.search('X') !== -1) { + if (this.alignVector.copy(this.unitX).applyQuaternion(quaternion).dot(this._eye) < AXIS_FLIP_TRESHOLD) { + if (handle.tag === 'fwd') { + handle.visible = false + } else { + handle.scale.x *= -1 + } + } else if (handle.tag === 'bwd') { + handle.visible = false + } + } + + if (handle.name.search('Y') !== -1) { + if (this.alignVector.copy(this.unitY).applyQuaternion(quaternion).dot(this._eye) < AXIS_FLIP_TRESHOLD) { + if (handle.tag === 'fwd') { + handle.visible = false + } else { + handle.scale.y *= -1 + } + } else if (handle.tag === 'bwd') { + handle.visible = false + } + } + + if (handle.name.search('Z') !== -1) { + if (this.alignVector.copy(this.unitZ).applyQuaternion(quaternion).dot(this._eye) < AXIS_FLIP_TRESHOLD) { + if (handle.tag === 'fwd') { + handle.visible = false + } else { + handle.scale.z *= -1 + } + } else if (handle.tag === 'bwd') { + handle.visible = false + } + } + } else if (this._mode === 'rotate') { + // Align handles to current local or world rotation + + this.tempQuaternion2.copy(quaternion) + this.alignVector.copy(this._eye).applyQuaternion(this.tempQuaternion.copy(quaternion).invert()) + + if (handle.name.search('E') !== -1) { + handle.quaternion.setFromRotationMatrix(this.lookAtMatrix.lookAt(this._eye, this.zeroVector, this.unitY)) + } + + if (handle.name === 'X') { + this.tempQuaternion.setFromAxisAngle(this.unitX, Math.atan2(-this.alignVector.y, this.alignVector.z)) + this.tempQuaternion.multiplyQuaternions(this.tempQuaternion2, this.tempQuaternion) + handle.quaternion.copy(this.tempQuaternion) + } + + if (handle.name === 'Y') { + this.tempQuaternion.setFromAxisAngle(this.unitY, Math.atan2(this.alignVector.x, this.alignVector.z)) + this.tempQuaternion.multiplyQuaternions(this.tempQuaternion2, this.tempQuaternion) + handle.quaternion.copy(this.tempQuaternion) + } + + if (handle.name === 'Z') { + this.tempQuaternion.setFromAxisAngle(this.unitZ, Math.atan2(this.alignVector.y, this.alignVector.x)) + this.tempQuaternion.multiplyQuaternions(this.tempQuaternion2, this.tempQuaternion) + handle.quaternion.copy(this.tempQuaternion) + } + } + + // Hide disabled axes + handle.visible = handle.visible && (handle.name.indexOf('X') === -1 || this._showX) + handle.visible = handle.visible && (handle.name.indexOf('Y') === -1 || this._showY) + handle.visible = handle.visible && (handle.name.indexOf('Z') === -1 || this._showZ) + handle.visible = + handle.visible && (handle.name.indexOf('E') === -1 || (this._showX && this._showY && this._showZ)) + + // highlight selected axis + + handle.material.color.copy(handle.material.color) + handle.material.opacity = handle.material.opacity + + if (!this._enabled) { + handle.material.opacity *= 0.5 + handle.material.color.lerp(new Color(1, 1, 1), 0.5) + } else if (this._axis) { + if (handle.name === this._axis) { + handle.material.opacity = 1.0 + handle.material.color.lerp(new Color(1, 1, 1), 0.5) + } else if (this._axis.split('').some((a) => handle.name === a)) { + handle.material.opacity = 1.0 + handle.material.color.lerp(new Color(1, 1, 1), 0.5) + } else { + handle.material.opacity *= 0.25 + handle.material.color.lerp(new Color(1, 1, 1), 0.5) + } + } + } + + Object3D.prototype.updateMatrixWorld.call(this) + } +} + +class TransformControlsPlane extends Mesh { + isTransformControlsPlane = true + type = 'TransformControlsPlane' + + constructor() { + super( + new PlaneGeometry(100000, 100000, 2, 2), + new MeshBasicMaterial({ + visible: false, + wireframe: true, + side: DoubleSide, + transparent: true, + opacity: 0.1, + toneMapped: false, + }), + ) + } + + private unitX = new Vector3(1, 0, 0) + private unitY = new Vector3(0, 1, 0) + private unitZ = new Vector3(0, 0, 1) + + private tempVector = new Vector3() + private dirVector = new Vector3() + private alignVector = new Vector3() + private tempMatrix = new Matrix4() + private identityQuaternion = new Quaternion() + + // these are set from parent class TransformControls + private _cameraQuaternion = new Quaternion() + + private _worldPosition = new Vector3() + private _worldQuaternion = new Quaternion() + + private _eye = new Vector3() + + private _axis: string | null = null + private _mode: 'translate' | 'rotate' | 'scale' = 'translate' + private _space = 'world' + + updateMatrixWorld = () => { + let space = this._space + + this.position.copy(this._worldPosition) + + if (this._mode === 'scale') space = 'local' // scale always oriented to local rotation + + this.unitX.set(1, 0, 0).applyQuaternion(space === 'local' ? this._worldQuaternion : this.identityQuaternion) + this.unitY.set(0, 1, 0).applyQuaternion(space === 'local' ? this._worldQuaternion : this.identityQuaternion) + this.unitZ.set(0, 0, 1).applyQuaternion(space === 'local' ? this._worldQuaternion : this.identityQuaternion) + + // Align the plane for current transform mode, axis and space. + + this.alignVector.copy(this.unitY) + + switch (this._mode) { + case 'translate': + case 'scale': + switch (this._axis) { + case 'X': + this.alignVector.copy(this._eye).cross(this.unitX) + this.dirVector.copy(this.unitX).cross(this.alignVector) + break + case 'Y': + this.alignVector.copy(this._eye).cross(this.unitY) + this.dirVector.copy(this.unitY).cross(this.alignVector) + break + case 'Z': + this.alignVector.copy(this._eye).cross(this.unitZ) + this.dirVector.copy(this.unitZ).cross(this.alignVector) + break + case 'XY': + this.dirVector.copy(this.unitZ) + break + case 'YZ': + this.dirVector.copy(this.unitX) + break + case 'XZ': + this.alignVector.copy(this.unitZ) + this.dirVector.copy(this.unitY) + break + case 'XYZ': + case 'E': + this.dirVector.set(0, 0, 0) + break + } + + break + case 'rotate': + default: + // special case for rotate + this.dirVector.set(0, 0, 0) + } + + if (this.dirVector.length() === 0) { + // If in rotate mode, make the plane parallel to camera + this.quaternion.copy(this._cameraQuaternion) + } else { + this.tempMatrix.lookAt(this.tempVector.set(0, 0, 0), this.dirVector, this.alignVector) + + this.quaternion.setFromRotationMatrix(this.tempMatrix) + } + + Object3D.prototype.updateMatrixWorld.call(this) + } +} + +export { TransformControls, TransformControlsGizmo, TransformControlsPlane } diff --git a/src/controls/experimental/CameraControls.js b/src/controls/experimental/CameraControls.js deleted file mode 100644 index 5dee89ae..00000000 --- a/src/controls/experimental/CameraControls.js +++ /dev/null @@ -1,1008 +0,0 @@ -import { EventDispatcher, MOUSE, Quaternion, Spherical, TOUCH, Vector2, Vector3 } from 'three' - -class CameraControls extends EventDispatcher { - constructor(object, domElement) { - if (domElement === undefined) - console.warn('THREE.CameraControls: The second parameter "domElement" is now mandatory.') - if (domElement === document) - console.error( - 'THREE.CameraControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.', - ) - - this.object = object - this.domElement = domElement - - // Set to false to disable this control - this.enabled = true - - // "target" sets the location of focus, where the object orbits around - this.target = new Vector3() - - // Set to true to enable trackball behavior - this.trackball = false - - // How far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0 - this.maxDistance = Infinity - - // How far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0 - this.maxZoom = Infinity - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0 // radians - this.maxPolarAngle = Math.PI // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. - this.minAzimuthAngle = -Infinity // radians - this.maxAzimuthAngle = Infinity // radians - - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false - this.dampingFactor = 0.05 - - // This option enables dollying in and out; property named as "zoom" for backwards compatibility - // Set to false to disable zooming - this.enableZoom = true - this.zoomSpeed = 1.0 - - // Set to false to disable rotating - this.enableRotate = true - this.rotateSpeed = 1.0 - - // Set to false to disable panning - this.enablePan = true - this.panSpeed = 1.0 - this.screenSpacePanning = false // if true, pan in screen-space - this.keyPanSpeed = 7.0 // pixels moved per arrow key push - - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - // auto-rotate is not supported for trackball behavior - this.autoRotate = false - this.autoRotateSpeed = 2.0 // 30 seconds per round when fps is 60 - - // Set to false to disable use of the keys - this.enableKeys = true - - // The four arrow keys - this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 } - - // Mouse buttons - this.mouseButtons = { - LEFT: MOUSE.ROTATE, - MIDDLE: MOUSE.DOLLY, - RIGHT: MOUSE.PAN, - } - - // Touch fingers - this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN } - - // for reset - this.target0 = this.target.clone() - this.position0 = this.object.position.clone() - this.quaternion0 = this.object.quaternion.clone() - this.zoom0 = this.object.zoom - - // - // public methods - // - - this.getPolarAngle = () => spherical.phi - - this.getAzimuthalAngle = () => spherical.theta - - this.saveState = () => { - scope.target0.copy(scope.target) - scope.position0.copy(scope.object.position) - scope.quaternion0.copy(scope.object.quaternion) - scope.zoom0 = scope.object.zoom - } - - this.reset = () => { - scope.target.copy(scope.target0) - scope.object.position.copy(scope.position0) - scope.object.quaternion.copy(scope.quaternion0) - scope.object.zoom = scope.zoom0 - - scope.object.updateProjectionMatrix() - scope.dispatchEvent(changeEvent) - - scope.update() - - state = STATE.NONE - } - - // this method is exposed, but perhaps it would be better if we can make it private... - this.update = (() => { - const offset = new Vector3() - - // so camera.up is the orbit axis - const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)) - const quatInverse = quat.clone().invert() - - const lastPosition = new Vector3() - const lastQuaternion = new Quaternion() - - const q = new Quaternion() - const vec = new Vector3() - - return function update() { - const position = scope.object.position - - offset.copy(position).sub(scope.target) - - if (scope.trackball) { - // rotate around screen-space y-axis - - if (sphericalDelta.theta) { - vec.set(0, 1, 0).applyQuaternion(scope.object.quaternion) - - var factor = scope.enableDamping ? scope.dampingFactor : 1 - - q.setFromAxisAngle(vec, sphericalDelta.theta * factor) - - scope.object.quaternion.premultiply(q) - offset.applyQuaternion(q) - } - - // rotate around screen-space x-axis - - if (sphericalDelta.phi) { - vec.set(1, 0, 0).applyQuaternion(scope.object.quaternion) - - var factor = scope.enableDamping ? scope.dampingFactor : 1 - - q.setFromAxisAngle(vec, sphericalDelta.phi * factor) - - scope.object.quaternion.premultiply(q) - offset.applyQuaternion(q) - } - - offset.multiplyScalar(scale) - offset.clampLength(scope.minDistance, scope.maxDistance) - } else { - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion(quat) - - if (scope.autoRotate && state === STATE.NONE) { - rotateLeft(getAutoRotationAngle()) - } - - spherical.setFromVector3(offset) - - if (scope.enableDamping) { - spherical.theta += sphericalDelta.theta * scope.dampingFactor - spherical.phi += sphericalDelta.phi * scope.dampingFactor - } else { - spherical.theta += sphericalDelta.theta - spherical.phi += sphericalDelta.phi - } - - // restrict theta to be between desired limits - spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)) - - // restrict phi to be between desired limits - spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)) - - spherical.makeSafe() - - spherical.radius *= scale - - // restrict radius to be between desired limits - spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)) - - offset.setFromSpherical(spherical) - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion(quatInverse) - } - - // move target to panned location - - if (scope.enableDamping === true) { - scope.target.addScaledVector(panOffset, scope.dampingFactor) - } else { - scope.target.add(panOffset) - } - - position.copy(scope.target).add(offset) - - if (scope.trackball === false) { - scope.object.lookAt(scope.target) - } - - if (scope.enableDamping === true) { - sphericalDelta.theta *= 1 - scope.dampingFactor - sphericalDelta.phi *= 1 - scope.dampingFactor - - panOffset.multiplyScalar(1 - scope.dampingFactor) - } else { - sphericalDelta.set(0, 0, 0) - - panOffset.set(0, 0, 0) - } - - scale = 1 - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - - if ( - zoomChanged || - lastPosition.distanceToSquared(scope.object.position) > EPS || - 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS - ) { - scope.dispatchEvent(changeEvent) - - lastPosition.copy(scope.object.position) - lastQuaternion.copy(scope.object.quaternion) - zoomChanged = false - - return true - } - - return false - } - })() - - this.dispose = () => { - scope.domElement.removeEventListener('contextmenu', onContextMenu, false) - scope.domElement.removeEventListener('mousedown', onMouseDown, false) - scope.domElement.removeEventListener('wheel', onMouseWheel, false) - - scope.domElement.removeEventListener('touchstart', onTouchStart, false) - scope.domElement.removeEventListener('touchend', onTouchEnd, false) - scope.domElement.removeEventListener('touchmove', onTouchMove, false) - - document.removeEventListener('mousemove', onMouseMove, false) - document.removeEventListener('mouseup', onMouseUp, false) - - scope.domElement.removeEventListener('keydown', onKeyDown, false) - - //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? - } - - // - // internals - // - - var scope = this - - var changeEvent = { type: 'change' } - const startEvent = { type: 'start' } - const endEvent = { type: 'end' } - - var STATE = { - NONE: -1, - ROTATE: 0, - DOLLY: 1, - PAN: 2, - TOUCH_ROTATE: 3, - TOUCH_PAN: 4, - TOUCH_DOLLY_PAN: 5, - TOUCH_DOLLY_ROTATE: 6, - } - - var state = STATE.NONE - - var EPS = 0.000001 - - // current position in spherical coordinates - var spherical = new Spherical() - var sphericalDelta = new Spherical() - - var scale = 1 - var panOffset = new Vector3() - var zoomChanged = false - - const rotateStart = new Vector2() - const rotateEnd = new Vector2() - const rotateDelta = new Vector2() - - const panStart = new Vector2() - const panEnd = new Vector2() - const panDelta = new Vector2() - - const dollyStart = new Vector2() - const dollyEnd = new Vector2() - const dollyDelta = new Vector2() - - function getAutoRotationAngle() { - return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed - } - - function getZoomScale() { - return Math.pow(0.95, scope.zoomSpeed) - } - - function rotateLeft(angle) { - sphericalDelta.theta -= angle - } - - function rotateUp(angle) { - sphericalDelta.phi -= angle - } - - const panLeft = (() => { - const v = new Vector3() - - return function panLeft(distance, objectMatrix) { - v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix - v.multiplyScalar(-distance) - - panOffset.add(v) - } - })() - - const panUp = (() => { - const v = new Vector3() - - return function panUp(distance, objectMatrix) { - if (scope.screenSpacePanning === true) { - v.setFromMatrixColumn(objectMatrix, 1) - } else { - v.setFromMatrixColumn(objectMatrix, 0) - v.crossVectors(scope.object.up, v) - } - - v.multiplyScalar(distance) - - panOffset.add(v) - } - })() - - // deltaX and deltaY are in pixels; right and down are positive - const pan = (() => { - const offset = new Vector3() - - return function pan(deltaX, deltaY) { - const element = scope.domElement - - if (scope.object.isPerspectiveCamera) { - // perspective - const position = scope.object.position - offset.copy(position).sub(scope.target) - let targetDistance = offset.length() - - // half of the fov is center to top of screen - targetDistance *= Math.tan(((scope.object.fov / 2) * Math.PI) / 180.0) - - // we use only clientHeight here so aspect ratio does not distort speed - panLeft((2 * deltaX * targetDistance) / element.clientHeight, scope.object.matrix) - panUp((2 * deltaY * targetDistance) / element.clientHeight, scope.object.matrix) - } else if (scope.object.isOrthographicCamera) { - // orthographic - panLeft( - (deltaX * (scope.object.right - scope.object.left)) / scope.object.zoom / element.clientWidth, - scope.object.matrix, - ) - panUp( - (deltaY * (scope.object.top - scope.object.bottom)) / scope.object.zoom / element.clientHeight, - scope.object.matrix, - ) - } else { - // camera neither orthographic nor perspective - console.warn('WARNING: CameraControls.js encountered an unknown camera type - pan disabled.') - scope.enablePan = false - } - } - })() - - function dollyIn(dollyScale) { - if (scope.object.isPerspectiveCamera) { - scale /= dollyScale - } else if (scope.object.isOrthographicCamera) { - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)) - scope.object.updateProjectionMatrix() - zoomChanged = true - } else { - console.warn('WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.') - scope.enableZoom = false - } - } - - function dollyOut(dollyScale) { - if (scope.object.isPerspectiveCamera) { - scale *= dollyScale - } else if (scope.object.isOrthographicCamera) { - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)) - scope.object.updateProjectionMatrix() - zoomChanged = true - } else { - console.warn('WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.') - scope.enableZoom = false - } - } - - // - // event callbacks - update the object state - // - - function handleMouseDownRotate(event) { - rotateStart.set(event.clientX, event.clientY) - } - - function handleMouseDownDolly(event) { - dollyStart.set(event.clientX, event.clientY) - } - - function handleMouseDownPan(event) { - panStart.set(event.clientX, event.clientY) - } - - function handleMouseMoveRotate(event) { - rotateEnd.set(event.clientX, event.clientY) - - rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed) - - const element = scope.domElement - - rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height - - rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) - - rotateStart.copy(rotateEnd) - - scope.update() - } - - function handleMouseMoveDolly(event) { - dollyEnd.set(event.clientX, event.clientY) - - dollyDelta.subVectors(dollyEnd, dollyStart) - - if (dollyDelta.y > 0) { - dollyIn(getZoomScale()) - } else if (dollyDelta.y < 0) { - dollyOut(getZoomScale()) - } - - dollyStart.copy(dollyEnd) - - scope.update() - } - - function handleMouseMovePan(event) { - panEnd.set(event.clientX, event.clientY) - - panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed) - - pan(panDelta.x, panDelta.y) - - panStart.copy(panEnd) - - scope.update() - } - - function handleMouseUp(/*event*/) { - // no-op - } - - function handleMouseWheel(event) { - if (event.deltaY < 0) { - dollyOut(getZoomScale()) - } else if (event.deltaY > 0) { - dollyIn(getZoomScale()) - } - - scope.update() - } - - function handleKeyDown(event) { - let needsUpdate = false - - switch (event.keyCode) { - case scope.keys.UP: - pan(0, scope.keyPanSpeed) - needsUpdate = true - break - - case scope.keys.BOTTOM: - pan(0, -scope.keyPanSpeed) - needsUpdate = true - break - - case scope.keys.LEFT: - pan(scope.keyPanSpeed, 0) - needsUpdate = true - break - - case scope.keys.RIGHT: - pan(-scope.keyPanSpeed, 0) - needsUpdate = true - break - } - - if (needsUpdate) { - // prevent the browser from scrolling on cursor keys - event.preventDefault() - - scope.update() - } - } - - function handleTouchStartRotate(event) { - if (event.touches.length == 1) { - rotateStart.set(event.touches[0].pageX, event.touches[0].pageY) - } else { - const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) - const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) - - rotateStart.set(x, y) - } - } - - function handleTouchStartPan(event) { - if (event.touches.length == 1) { - panStart.set(event.touches[0].pageX, event.touches[0].pageY) - } else { - const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) - const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) - - panStart.set(x, y) - } - } - - function handleTouchStartDolly(event) { - const dx = event.touches[0].pageX - event.touches[1].pageX - const dy = event.touches[0].pageY - event.touches[1].pageY - - const distance = Math.sqrt(dx * dx + dy * dy) - - dollyStart.set(0, distance) - } - - function handleTouchStartDollyPan(event) { - if (scope.enableZoom) handleTouchStartDolly(event) - - if (scope.enablePan) handleTouchStartPan(event) - } - - function handleTouchStartDollyRotate(event) { - if (scope.enableZoom) handleTouchStartDolly(event) - - if (scope.enableRotate) handleTouchStartRotate(event) - } - - function handleTouchMoveRotate(event) { - if (event.touches.length == 1) { - rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY) - } else { - const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) - const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) - - rotateEnd.set(x, y) - } - - rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed) - - const element = scope.domElement - - rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight) // yes, height - - rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight) - - rotateStart.copy(rotateEnd) - } - - function handleTouchMovePan(event) { - if (event.touches.length == 1) { - panEnd.set(event.touches[0].pageX, event.touches[0].pageY) - } else { - const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) - const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) - - panEnd.set(x, y) - } - - panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed) - - pan(panDelta.x, panDelta.y) - - panStart.copy(panEnd) - } - - function handleTouchMoveDolly(event) { - const dx = event.touches[0].pageX - event.touches[1].pageX - const dy = event.touches[0].pageY - event.touches[1].pageY - - const distance = Math.sqrt(dx * dx + dy * dy) - - dollyEnd.set(0, distance) - - dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)) - - dollyIn(dollyDelta.y) - - dollyStart.copy(dollyEnd) - } - - function handleTouchMoveDollyPan(event) { - if (scope.enableZoom) handleTouchMoveDolly(event) - - if (scope.enablePan) handleTouchMovePan(event) - } - - function handleTouchMoveDollyRotate(event) { - if (scope.enableZoom) handleTouchMoveDolly(event) - - if (scope.enableRotate) handleTouchMoveRotate(event) - } - - function handleTouchEnd(/*event*/) { - // no-op - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onMouseDown(event) { - if (scope.enabled === false) return - - // Prevent the browser from scrolling. - - event.preventDefault() - - // Manually set the focus since calling preventDefault above - // prevents the browser from setting it automatically. - - scope.domElement.focus ? scope.domElement.focus() : window.focus() - - let mouseAction - - switch (event.button) { - case 0: - mouseAction = scope.mouseButtons.LEFT - break - - case 1: - mouseAction = scope.mouseButtons.MIDDLE - break - - case 2: - mouseAction = scope.mouseButtons.RIGHT - break - - default: - mouseAction = -1 - } - - switch (mouseAction) { - case MOUSE.DOLLY: - if (scope.enableZoom === false) return - - handleMouseDownDolly(event) - - state = STATE.DOLLY - - break - - case MOUSE.ROTATE: - if (event.ctrlKey || event.metaKey || event.shiftKey) { - if (scope.enablePan === false) return - - handleMouseDownPan(event) - - state = STATE.PAN - } else { - if (scope.enableRotate === false) return - - handleMouseDownRotate(event) - - state = STATE.ROTATE - } - - break - - case MOUSE.PAN: - if (event.ctrlKey || event.metaKey || event.shiftKey) { - if (scope.enableRotate === false) return - - handleMouseDownRotate(event) - - state = STATE.ROTATE - } else { - if (scope.enablePan === false) return - - handleMouseDownPan(event) - - state = STATE.PAN - } - - break - - default: - state = STATE.NONE - } - - if (state !== STATE.NONE) { - document.addEventListener('mousemove', onMouseMove, false) - document.addEventListener('mouseup', onMouseUp, false) - - scope.dispatchEvent(startEvent) - } - } - - function onMouseMove(event) { - if (scope.enabled === false) return - - event.preventDefault() - - switch (state) { - case STATE.ROTATE: - if (scope.enableRotate === false) return - - handleMouseMoveRotate(event) - - break - - case STATE.DOLLY: - if (scope.enableZoom === false) return - - handleMouseMoveDolly(event) - - break - - case STATE.PAN: - if (scope.enablePan === false) return - - handleMouseMovePan(event) - - break - } - } - - function onMouseUp(event) { - if (scope.enabled === false) return - - handleMouseUp(event) - - document.removeEventListener('mousemove', onMouseMove, false) - document.removeEventListener('mouseup', onMouseUp, false) - - scope.dispatchEvent(endEvent) - - state = STATE.NONE - } - - function onMouseWheel(event) { - if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) - return - - event.preventDefault() - - scope.dispatchEvent(startEvent) - - handleMouseWheel(event) - - scope.dispatchEvent(endEvent) - } - - function onKeyDown(event) { - if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return - - handleKeyDown(event) - } - - function onTouchStart(event) { - if (scope.enabled === false) return - - event.preventDefault() - - switch (event.touches.length) { - case 1: - switch (scope.touches.ONE) { - case TOUCH.ROTATE: - if (scope.enableRotate === false) return - - handleTouchStartRotate(event) - - state = STATE.TOUCH_ROTATE - - break - - case TOUCH.PAN: - if (scope.enablePan === false) return - - handleTouchStartPan(event) - - state = STATE.TOUCH_PAN - - break - - default: - state = STATE.NONE - } - - break - - case 2: - switch (scope.touches.TWO) { - case TOUCH.DOLLY_PAN: - if (scope.enableZoom === false && scope.enablePan === false) return - - handleTouchStartDollyPan(event) - - state = STATE.TOUCH_DOLLY_PAN - - break - - case TOUCH.DOLLY_ROTATE: - if (scope.enableZoom === false && scope.enableRotate === false) return - - handleTouchStartDollyRotate(event) - - state = STATE.TOUCH_DOLLY_ROTATE - - break - - default: - state = STATE.NONE - } - - break - - default: - state = STATE.NONE - } - - if (state !== STATE.NONE) { - scope.dispatchEvent(startEvent) - } - } - - function onTouchMove(event) { - if (scope.enabled === false) return - - event.preventDefault() - - switch (state) { - case STATE.TOUCH_ROTATE: - if (scope.enableRotate === false) return - - handleTouchMoveRotate(event) - - scope.update() - - break - - case STATE.TOUCH_PAN: - if (scope.enablePan === false) return - - handleTouchMovePan(event) - - scope.update() - - break - - case STATE.TOUCH_DOLLY_PAN: - if (scope.enableZoom === false && scope.enablePan === false) return - - handleTouchMoveDollyPan(event) - - scope.update() - - break - - case STATE.TOUCH_DOLLY_ROTATE: - if (scope.enableZoom === false && scope.enableRotate === false) return - - handleTouchMoveDollyRotate(event) - - scope.update() - - break - - default: - state = STATE.NONE - } - } - - function onTouchEnd(event) { - if (scope.enabled === false) return - - handleTouchEnd(event) - - scope.dispatchEvent(endEvent) - - state = STATE.NONE - } - - function onContextMenu(event) { - if (scope.enabled === false) return - - event.preventDefault() - } - - // - - scope.domElement.addEventListener('contextmenu', onContextMenu, false) - - scope.domElement.addEventListener('mousedown', onMouseDown, false) - scope.domElement.addEventListener('wheel', onMouseWheel, false) - - scope.domElement.addEventListener('touchstart', onTouchStart, false) - scope.domElement.addEventListener('touchend', onTouchEnd, false) - scope.domElement.addEventListener('touchmove', onTouchMove, false) - - scope.domElement.addEventListener('keydown', onKeyDown, false) - - // make sure element can receive keys. - - if (scope.domElement.tabIndex === -1) { - scope.domElement.tabIndex = 0 - } - - // force an update at start - - this.object.lookAt(scope.target) - this.update() - this.saveState() - } -} - -// OrbitControls maintains the "up" direction, camera.up (+Y by default). -// -// Orbit - left mouse / touch: one-finger move -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move - -class OrbitControlsExp extends EventDispatcher { - constructor(object, domElement) { - CameraControls.call(this, object, domElement) - - this.mouseButtons.LEFT = MOUSE.ROTATE - this.mouseButtons.RIGHT = MOUSE.PAN - - this.touches.ONE = TOUCH.ROTATE - this.touches.TWO = TOUCH.DOLLY_PAN - } -} - -// MapControls maintains the "up" direction, camera.up (+Y by default) -// -// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - left mouse, or left right + ctrl/meta/shiftKey, or arrow keys / touch: one-finger move - -class MapControlsExp extends EventDispatcher { - constructor(object, domElement) { - CameraControls.call(this, object, domElement) - - this.mouseButtons.LEFT = MOUSE.PAN - this.mouseButtons.RIGHT = MOUSE.ROTATE - - this.touches.ONE = TOUCH.PAN - this.touches.TWO = TOUCH.DOLLY_ROTATE - } -} - -// TrackballControls allows the camera to rotate over the polls and does not maintain camera.up -// -// Orbit - left mouse / touch: one-finger move -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move - -class TrackballControlsExp extends EventDispatcher { - constructor(object, domElement) { - CameraControls.call(this, object, domElement) - - this.trackball = true - this.screenSpacePanning = true - this.autoRotate = false - - this.mouseButtons.LEFT = MOUSE.ROTATE - this.mouseButtons.RIGHT = MOUSE.PAN - - this.touches.ONE = TOUCH.ROTATE - this.touches.TWO = TOUCH.DOLLY_PAN - } -} - -export { CameraControls, OrbitControlsExp, MapControlsExp, TrackballControlsExp } diff --git a/src/controls/experimental/CameraControls.ts b/src/controls/experimental/CameraControls.ts new file mode 100644 index 00000000..a20d454d --- /dev/null +++ b/src/controls/experimental/CameraControls.ts @@ -0,0 +1,1075 @@ +import { + EventDispatcher, + MOUSE, + Matrix4, + OrthographicCamera, + PerspectiveCamera, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, +} from 'three' + +type CHANGE_EVENT = { + type: 'change' | 'start' | 'end' +} + +enum STATE { + NONE = -1, + ROTATE = 0, + DOLLY = 1, + PAN = 2, + TOUCH_ROTATE = 3, + TOUCH_PAN = 4, + TOUCH_DOLLY_PAN = 5, + TOUCH_DOLLY_ROTATE = 6, +} + +class CameraControls extends EventDispatcher { + object: PerspectiveCamera | OrthographicCamera + domElement: HTMLElement + + /** Set to false to disable this control */ + enabled = true + + /** "target" sets the location of focus, where the object orbits around */ + target = new Vector3() + + /** Set to true to enable trackball behavior */ + trackball = false + + /** How far you can dolly in ( PerspectiveCamera only ) */ + minDistance = 0 + /** How far you can dolly out ( PerspectiveCamera only ) */ + maxDistance = Infinity + + // How far you can zoom in and out ( OrthographicCamera only ) + minZoom = 0 + maxZoom = Infinity + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + minPolarAngle = 0 + maxPolarAngle = Math.PI + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + minAzimuthAngle = -Infinity // radians + maxAzimuthAngle = Infinity // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + enableDamping = false + dampingFactor = 0.05 + + /** + * This option enables dollying in and out; property named as "zoom" for backwards compatibility + * Set to false to disable zooming + */ + enableZoom = true + zoomSpeed = 1.0 + + /** Set to false to disable rotating */ + enableRotate = true + rotateSpeed = 1.0 + + /** Set to false to disable panning */ + enablePan = true + panSpeed = 1.0 + /** if true, pan in screen-space */ + screenSpacePanning = false + /** pixels moved per arrow key push */ + keyPanSpeed = 7.0 + + /** + * Set to true to automatically rotate around the target + * If auto-rotate is enabled, you must call controls.update() in your animation loop + * auto-rotate is not supported for trackball behavior + */ + autoRotate = false + autoRotateSpeed = 2.0 // 30 seconds per round when fps is 60 + + /** Set to false to disable use of the keys */ + enableKeys = true + + /** The four arrow keys */ + keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 } + + mouseButtons: { + LEFT: MOUSE + MIDDLE?: MOUSE + RIGHT: MOUSE + } + + /** Touch fingers */ + touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN } + + // for reset + target0: Vector3 + position0: Vector3 + quaternion0: Quaternion + zoom0: number + + // current position in spherical coordinates + spherical = new Spherical() + sphericalDelta = new Spherical() + + private changeEvent = { type: 'change' } + private startEvent = { type: 'start' } + private endEvent = { type: 'end' } + private state = STATE.NONE + + private EPS = 0.000001 + + private scale = 1 + private panOffset = new Vector3() + private zoomChanged = false + + private rotateStart = new Vector2() + private rotateEnd = new Vector2() + private rotateDelta = new Vector2() + + private panStart = new Vector2() + private panEnd = new Vector2() + private panDelta = new Vector2() + + private dollyStart = new Vector2() + private dollyEnd = new Vector2() + private dollyDelta = new Vector2() + + private offset = new Vector3() + + private lastPosition = new Vector3() + private lastQuaternion = new Quaternion() + + private q = new Quaternion() + private v = new Vector3() + private vec = new Vector3() + + private quat: Quaternion + private quatInverse: Quaternion + + constructor(object: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super() + + if (domElement === undefined) { + console.warn('THREE.CameraControls: The second parameter "domElement" is now mandatory.') + } + if (domElement instanceof Document) { + console.error( + 'THREE.CameraControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.', + ) + } + + this.object = object + this.domElement = domElement + + this.mouseButtons = { + LEFT: MOUSE.ROTATE, + MIDDLE: MOUSE.DOLLY, + RIGHT: MOUSE.PAN, + } + + // for reset + this.target0 = this.target.clone() + this.position0 = this.object.position.clone() + this.quaternion0 = this.object.quaternion.clone() + this.zoom0 = this.object.zoom + + // + // internals + // + + // so camera.up is the orbit axis + this.quat = new Quaternion().setFromUnitVectors(this.object.up, new Vector3(0, 1, 0)) + this.quatInverse = this.quat.clone().invert() + + this.lastPosition = new Vector3() + this.lastQuaternion = new Quaternion() + + this.domElement.addEventListener('contextmenu', this.onContextMenu, false) + + this.domElement.addEventListener('mousedown', this.onMouseDown, false) + this.domElement.addEventListener('wheel', this.onMouseWheel, false) + + this.domElement.addEventListener('touchstart', this.onTouchStart, false) + this.domElement.addEventListener('touchend', this.onTouchEnd, false) + this.domElement.addEventListener('touchmove', this.onTouchMove, false) + + this.domElement.addEventListener('keydown', this.onKeyDown, false) + + // make sure element can receive keys. + + if (this.domElement.tabIndex === -1) { + this.domElement.tabIndex = 0 + } + + // force an update at start + + this.object.lookAt(this.target) + this.update() + this.saveState() + } + + getPolarAngle = () => this.spherical.phi + + getAzimuthalAngle = () => this.spherical.theta + + saveState = () => { + this.target0.copy(this.target) + this.position0.copy(this.object.position) + this.quaternion0.copy(this.object.quaternion) + this.zoom0 = this.object.zoom + } + + reset = () => { + this.target.copy(this.target0) + this.object.position.copy(this.position0) + this.object.quaternion.copy(this.quaternion0) + this.object.zoom = this.zoom0 + + this.object.updateProjectionMatrix() + this.dispatchEvent(this.changeEvent) + + this.update() + + this.state = STATE.NONE + } + + dispose = () => { + this.domElement.removeEventListener('contextmenu', this.onContextMenu, false) + this.domElement.removeEventListener('mousedown', this.onMouseDown, false) + this.domElement.removeEventListener('wheel', this.onMouseWheel, false) + + this.domElement.removeEventListener('touchstart', this.onTouchStart, false) + this.domElement.removeEventListener('touchend', this.onTouchEnd, false) + this.domElement.removeEventListener('touchmove', this.onTouchMove, false) + + document.removeEventListener('mousemove', this.onMouseMove, false) + document.removeEventListener('mouseup', this.onMouseUp, false) + + this.domElement.removeEventListener('keydown', this.onKeyDown, false) + + //this.dispatchEvent( { type: 'dispose' } ); // should this be added here? + } + + private update = () => { + const position = this.object.position + + this.offset.copy(position).sub(this.target) + + if (this.trackball) { + // rotate around screen-space y-axis + + if (this.sphericalDelta.theta) { + this.vec.set(0, 1, 0).applyQuaternion(this.object.quaternion) + + const factor = this.enableDamping ? this.dampingFactor : 1 + + this.q.setFromAxisAngle(this.vec, this.sphericalDelta.theta * factor) + + this.object.quaternion.premultiply(this.q) + this.offset.applyQuaternion(this.q) + } + + // rotate around screen-space x-axis + + if (this.sphericalDelta.phi) { + this.vec.set(1, 0, 0).applyQuaternion(this.object.quaternion) + + const factor = this.enableDamping ? this.dampingFactor : 1 + + this.q.setFromAxisAngle(this.vec, this.sphericalDelta.phi * factor) + + this.object.quaternion.premultiply(this.q) + this.offset.applyQuaternion(this.q) + } + + this.offset.multiplyScalar(this.scale) + this.offset.clampLength(this.minDistance, this.maxDistance) + } else { + // rotate offset to "y-axis-is-up" space + this.offset.applyQuaternion(this.quat) + + if (this.autoRotate && this.state === STATE.NONE) { + this.rotateLeft(this.getAutoRotationAngle()) + } + + this.spherical.setFromVector3(this.offset) + + if (this.enableDamping) { + this.spherical.theta += this.sphericalDelta.theta * this.dampingFactor + this.spherical.phi += this.sphericalDelta.phi * this.dampingFactor + } else { + this.spherical.theta += this.sphericalDelta.theta + this.spherical.phi += this.sphericalDelta.phi + } + + // restrict theta to be between desired limits + this.spherical.theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, this.spherical.theta)) + + // restrict phi to be between desired limits + this.spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.spherical.phi)) + + this.spherical.makeSafe() + + this.spherical.radius *= this.scale + + // restrict radius to be between desired limits + this.spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, this.spherical.radius)) + + this.offset.setFromSpherical(this.spherical) + + // rotate offset back to "camera-up-vector-is-up" space + this.offset.applyQuaternion(this.quatInverse) + } + + // move target to panned location + + if (this.enableDamping === true) { + this.target.addScaledVector(this.panOffset, this.dampingFactor) + } else { + this.target.add(this.panOffset) + } + + position.copy(this.target).add(this.offset) + + if (this.trackball === false) { + this.object.lookAt(this.target) + } + + if (this.enableDamping === true) { + this.sphericalDelta.theta *= 1 - this.dampingFactor + this.sphericalDelta.phi *= 1 - this.dampingFactor + + this.panOffset.multiplyScalar(1 - this.dampingFactor) + } else { + this.sphericalDelta.set(0, 0, 0) + + this.panOffset.set(0, 0, 0) + } + + this.scale = 1 + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( + this.zoomChanged || + this.lastPosition.distanceToSquared(this.object.position) > this.EPS || + 8 * (1 - this.lastQuaternion.dot(this.object.quaternion)) > this.EPS + ) { + this.dispatchEvent(this.changeEvent) + + this.lastPosition.copy(this.object.position) + this.lastQuaternion.copy(this.object.quaternion) + this.zoomChanged = false + + return true + } + + return false + } + + private getAutoRotationAngle = () => ((2 * Math.PI) / 60 / 60) * this.autoRotateSpeed + + private getZoomScale = () => Math.pow(0.95, this.zoomSpeed) + + private rotateLeft = (angle: number) => (this.sphericalDelta.theta -= angle) + + private rotateUp = (angle: number) => (this.sphericalDelta.phi -= angle) + + private panLeft = (distance: number, objectMatrix: Matrix4) => { + this.v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix + this.v.multiplyScalar(-distance) + + this.panOffset.add(this.v) + } + + private panUp = (distance: number, objectMatrix: Matrix4) => { + if (this.screenSpacePanning === true) { + this.v.setFromMatrixColumn(objectMatrix, 1) + } else { + this.v.setFromMatrixColumn(objectMatrix, 0) + this.v.crossVectors(this.object.up, this.v) + } + + this.v.multiplyScalar(distance) + + this.panOffset.add(this.v) + } + + // deltaX and deltaY are in pixels; right and down are positive + private pan = (deltaX: number, deltaY: number) => { + const element = this.domElement + + if (this.object instanceof PerspectiveCamera) { + // perspective + const position = this.object.position + this.offset.copy(position).sub(this.target) + let targetDistance = this.offset.length() + + // half of the fov is center to top of screen + targetDistance *= Math.tan(((this.object.fov / 2) * Math.PI) / 180.0) + + // we use only clientHeight here so aspect ratio does not distort speed + this.panLeft((2 * deltaX * targetDistance) / element.clientHeight, this.object.matrix) + this.panUp((2 * deltaY * targetDistance) / element.clientHeight, this.object.matrix) + } else if (this.object.isOrthographicCamera) { + // orthographic + this.panLeft( + (deltaX * (this.object.right - this.object.left)) / this.object.zoom / element.clientWidth, + this.object.matrix, + ) + this.panUp( + (deltaY * (this.object.top - this.object.bottom)) / this.object.zoom / element.clientHeight, + this.object.matrix, + ) + } else { + // camera neither orthographic nor perspective + console.warn('WARNING: CameraControls.js encountered an unknown camera type - pan disabled.') + this.enablePan = false + } + } + + private dollyIn = (dollyScale: number) => { + // TODO: replace w/.isPerspectiveCamera ? + if (this.object instanceof PerspectiveCamera) { + this.scale /= dollyScale + // TODO: replace w/.isOrthographicCamera ? + } else if (this.object instanceof OrthographicCamera) { + this.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom * dollyScale)) + this.object.updateProjectionMatrix() + this.zoomChanged = true + } else { + console.warn('WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.') + this.enableZoom = false + } + } + + private dollyOut = (dollyScale: number) => { + // TODO: replace w/.isPerspectiveCamera ? + if (this.object instanceof PerspectiveCamera) { + this.scale *= dollyScale + // TODO: replace w/.isOrthographicCamera ? + } else if (this.object instanceof OrthographicCamera) { + this.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom / dollyScale)) + this.object.updateProjectionMatrix() + this.zoomChanged = true + } else { + console.warn('WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.') + this.enableZoom = false + } + } + + // event callbacks - update the object state + + private handleMouseDownRotate = (event: MouseEvent) => { + this.rotateStart.set(event.clientX, event.clientY) + } + + // TODO: confirm if worthwhile to return the Vector2 instead of void + private handleMouseDownDolly = (event: MouseEvent) => { + this.dollyStart.set(event.clientX, event.clientY) + } + + private handleMouseDownPan = (event: MouseEvent) => { + this.panStart.set(event.clientX, event.clientY) + } + + private handleMouseMoveRotate = (event: MouseEvent) => { + this.rotateEnd.set(event.clientX, event.clientY) + + this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart).multiplyScalar(this.rotateSpeed) + + const element = this.domElement + + this.rotateLeft((2 * Math.PI * this.rotateDelta.x) / element.clientHeight) // yes, height + + this.rotateUp((2 * Math.PI * this.rotateDelta.y) / element.clientHeight) + + this.rotateStart.copy(this.rotateEnd) + + this.update() + } + + private handleMouseMoveDolly = (event: MouseEvent) => { + this.dollyEnd.set(event.clientX, event.clientY) + + this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart) + + if (this.dollyDelta.y > 0) { + this.dollyIn(this.getZoomScale()) + } else if (this.dollyDelta.y < 0) { + this.dollyOut(this.getZoomScale()) + } + + this.dollyStart.copy(this.dollyEnd) + + this.update() + } + + private handleMouseMovePan = (event: MouseEvent) => { + this.panEnd.set(event.clientX, event.clientY) + + this.panDelta.subVectors(this.panEnd, this.panStart).multiplyScalar(this.panSpeed) + + this.pan(this.panDelta.x, this.panDelta.y) + + this.panStart.copy(this.panEnd) + + this.update() + } + + private handleMouseUp(/*event*/) { + // no-op + } + + private handleMouseWheel = (event: WheelEvent) => { + if (event.deltaY < 0) { + this.dollyOut(this.getZoomScale()) + } else if (event.deltaY > 0) { + this.dollyIn(this.getZoomScale()) + } + + this.update() + } + + private handleKeyDown = (event: KeyboardEvent) => { + let needsUpdate = false + + // TODO: keyCode deprecated? + switch (event.keyCode) { + case this.keys.UP: + this.pan(0, this.keyPanSpeed) + needsUpdate = true + break + + case this.keys.BOTTOM: + this.pan(0, -this.keyPanSpeed) + needsUpdate = true + break + + case this.keys.LEFT: + this.pan(this.keyPanSpeed, 0) + needsUpdate = true + break + + case this.keys.RIGHT: + this.pan(-this.keyPanSpeed, 0) + needsUpdate = true + break + } + + if (needsUpdate) { + // prevent the browser from scrolling on cursor keys + event.preventDefault() + + this.update() + } + } + + private handleTouchStartRotate = (event: TouchEvent) => { + if (event.touches.length == 1) { + this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY) + } else { + const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) + const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) + + this.rotateStart.set(x, y) + } + } + + private handleTouchStartPan = (event: TouchEvent) => { + if (event.touches.length == 1) { + this.panStart.set(event.touches[0].pageX, event.touches[0].pageY) + } else { + const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) + const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) + + this.panStart.set(x, y) + } + } + + private handleTouchStartDolly = (event: TouchEvent) => { + const dx = event.touches[0].pageX - event.touches[1].pageX + const dy = event.touches[0].pageY - event.touches[1].pageY + + const distance = Math.sqrt(dx * dx + dy * dy) + + this.dollyStart.set(0, distance) + } + + private handleTouchStartDollyPan = (event: TouchEvent) => { + if (this.enableZoom) this.handleTouchStartDolly(event) + + if (this.enablePan) this.handleTouchStartPan(event) + } + + private handleTouchStartDollyRotate = (event: TouchEvent) => { + if (this.enableZoom) this.handleTouchStartDolly(event) + + if (this.enableRotate) this.handleTouchStartRotate(event) + } + + private handleTouchMoveRotate = (event: TouchEvent) => { + if (event.touches.length == 1) { + this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY) + } else { + const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) + const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) + + this.rotateEnd.set(x, y) + } + + this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart).multiplyScalar(this.rotateSpeed) + + const element = this.domElement + + this.rotateLeft((2 * Math.PI * this.rotateDelta.x) / element.clientHeight) // yes, height + + this.rotateUp((2 * Math.PI * this.rotateDelta.y) / element.clientHeight) + + this.rotateStart.copy(this.rotateEnd) + } + + private handleTouchMovePan = (event: TouchEvent) => { + if (event.touches.length == 1) { + this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY) + } else { + const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX) + const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY) + + this.panEnd.set(x, y) + } + + this.panDelta.subVectors(this.panEnd, this.panStart).multiplyScalar(this.panSpeed) + + this.pan(this.panDelta.x, this.panDelta.y) + + this.panStart.copy(this.panEnd) + } + + private handleTouchMoveDolly = (event: TouchEvent) => { + const dx = event.touches[0].pageX - event.touches[1].pageX + const dy = event.touches[0].pageY - event.touches[1].pageY + + const distance = Math.sqrt(dx * dx + dy * dy) + + this.dollyEnd.set(0, distance) + + this.dollyDelta.set(0, Math.pow(this.dollyEnd.y / this.dollyStart.y, this.zoomSpeed)) + + this.dollyIn(this.dollyDelta.y) + + this.dollyStart.copy(this.dollyEnd) + } + + private handleTouchMoveDollyPan = (event: TouchEvent) => { + if (this.enableZoom) this.handleTouchMoveDolly(event) + + if (this.enablePan) this.handleTouchMovePan(event) + } + + private handleTouchMoveDollyRotate = (event: TouchEvent) => { + if (this.enableZoom) this.handleTouchMoveDolly(event) + + if (this.enableRotate) this.handleTouchMoveRotate(event) + } + + private handleTouchEnd(/*event*/) { + // no-op + } + + // + // event handlers - FSM: listen for events and reset state + // + + private onMouseDown = (event: MouseEvent) => { + if (this.enabled === false) return + + // Prevent the browser from scrolling. + + event.preventDefault() + + // Manually set the focus since calling preventDefault above + // prevents the browser from setting it automatically. + + this.domElement.focus ? this.domElement.focus() : window.focus() + + let mouseAction + + switch (event.button) { + case 0: + mouseAction = this.mouseButtons.LEFT + break + + case 1: + mouseAction = this.mouseButtons.MIDDLE + break + + case 2: + mouseAction = this.mouseButtons.RIGHT + break + + default: + mouseAction = -1 + } + + switch (mouseAction) { + case MOUSE.DOLLY: + if (this.enableZoom === false) return + + this.handleMouseDownDolly(event) + + this.state = STATE.DOLLY + + break + + case MOUSE.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (this.enablePan === false) return + + this.handleMouseDownPan(event) + + this.state = STATE.PAN + } else { + if (this.enableRotate === false) return + + this.handleMouseDownRotate(event) + + this.state = STATE.ROTATE + } + + break + + case MOUSE.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (this.enableRotate === false) return + + this.handleMouseDownRotate(event) + + this.state = STATE.ROTATE + } else { + if (this.enablePan === false) return + + this.handleMouseDownPan(event) + + this.state = STATE.PAN + } + + break + + default: + this.state = STATE.NONE + } + + if (this.state !== STATE.NONE) { + document.addEventListener('mousemove', this.onMouseMove, false) + document.addEventListener('mouseup', this.onMouseUp, false) + + this.dispatchEvent(this.startEvent) + } + } + + private onMouseMove = (event: MouseEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + switch (this.state) { + case STATE.ROTATE: + if (this.enableRotate === false) return + + this.handleMouseMoveRotate(event) + + break + + case STATE.DOLLY: + if (this.enableZoom === false) return + + this.handleMouseMoveDolly(event) + + break + + case STATE.PAN: + if (this.enablePan === false) return + + this.handleMouseMovePan(event) + + break + } + } + + private onMouseUp = (event: MouseEvent) => { + if (this.enabled === false) return + + // this.handleMouseUp() + + document.removeEventListener('mousemove', this.onMouseMove, false) + document.removeEventListener('mouseup', this.onMouseUp, false) + + this.dispatchEvent(this.endEvent) + + this.state = STATE.NONE + } + + private onMouseWheel = (event: WheelEvent) => { + if ( + this.enabled === false || + this.enableZoom === false || + (this.state !== STATE.NONE && this.state !== STATE.ROTATE) + ) { + return + } + + event.preventDefault() + + this.dispatchEvent(this.startEvent) + + this.handleMouseWheel(event) + + this.dispatchEvent(this.endEvent) + } + + private onKeyDown = (event: KeyboardEvent) => { + if (this.enabled === false || this.enableKeys === false || this.enablePan === false) return + + this.handleKeyDown(event) + } + + private onTouchStart = (event: TouchEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + switch (event.touches.length) { + case 1: + switch (this.touches.ONE) { + case TOUCH.ROTATE: + if (this.enableRotate === false) return + + this.handleTouchStartRotate(event) + + this.state = STATE.TOUCH_ROTATE + + break + + case TOUCH.PAN: + if (this.enablePan === false) return + + this.handleTouchStartPan(event) + + this.state = STATE.TOUCH_PAN + + break + + default: + this.state = STATE.NONE + } + + break + + case 2: + switch (this.touches.TWO) { + case TOUCH.DOLLY_PAN: + if (this.enableZoom === false && this.enablePan === false) return + + this.handleTouchStartDollyPan(event) + + this.state = STATE.TOUCH_DOLLY_PAN + + break + + case TOUCH.DOLLY_ROTATE: + if (this.enableZoom === false && this.enableRotate === false) return + + this.handleTouchStartDollyRotate(event) + + this.state = STATE.TOUCH_DOLLY_ROTATE + + break + + default: + this.state = STATE.NONE + } + + break + + default: + this.state = STATE.NONE + } + + if (this.state !== STATE.NONE) { + this.dispatchEvent(this.startEvent) + } + } + + private onTouchMove = (event: TouchEvent) => { + if (this.enabled === false) return + + event.preventDefault() + + switch (this.state) { + case STATE.TOUCH_ROTATE: + if (this.enableRotate === false) return + + this.handleTouchMoveRotate(event) + + this.update() + + break + + case STATE.TOUCH_PAN: + if (this.enablePan === false) return + + this.handleTouchMovePan(event) + + this.update() + + break + + case STATE.TOUCH_DOLLY_PAN: + if (this.enableZoom === false && this.enablePan === false) return + + this.handleTouchMoveDollyPan(event) + + this.update() + + break + + case STATE.TOUCH_DOLLY_ROTATE: + if (this.enableZoom === false && this.enableRotate === false) return + + this.handleTouchMoveDollyRotate(event) + + this.update() + + break + + default: + this.state = STATE.NONE + } + } + + private onTouchEnd = (event: TouchEvent) => { + if (this.enabled === false) return + + // this.handleTouchEnd() + + this.dispatchEvent(this.endEvent) + + this.state = STATE.NONE + } + + private onContextMenu = (event: Event) => { + if (this.enabled === false) return + + event.preventDefault() + } +} + +/** + * OrbitControls maintains the "up" direction, camera.up (+Y by default). + * + * @event Orbit - left mouse / touch: one-finger move + * @event Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish + * @event Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + */ +class OrbitControlsExp extends CameraControls { + mouseButtons: { + LEFT: MOUSE + RIGHT: MOUSE + } + touches: { + ONE: TOUCH + TWO: TOUCH + } + + constructor(object: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super(object, domElement) + + this.mouseButtons = { + LEFT: MOUSE.ROTATE, + RIGHT: MOUSE.PAN, + } + this.touches = { + ONE: TOUCH.ROTATE, + TWO: TOUCH.DOLLY_PAN, + } + } +} + +/** + * MapControls maintains the "up" direction, camera.up (+Y by default) + * + * @event Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate + * @event Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish + * @event Pan - left mouse, or left right + ctrl/meta/shiftKey, or arrow keys / touch: one-finger move + */ +class MapControlsExp extends CameraControls { + mouseButtons: { + LEFT: MOUSE + RIGHT: MOUSE + } + touches: { + ONE: TOUCH + TWO: TOUCH + } + + constructor(object: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super(object, domElement) + + this.mouseButtons = { + LEFT: MOUSE.PAN, + RIGHT: MOUSE.ROTATE, + } + this.touches = { + ONE: TOUCH.PAN, + TWO: TOUCH.DOLLY_ROTATE, + } + } +} + +/** + * TrackballControls allows the camera to rotate over the polls and does not maintain camera.up + * + * @event Orbit - left mouse / touch: one-finger move + * @event Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish + * @event Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + */ +class TrackballControlsExp extends CameraControls { + trackball: boolean + screenSpacePanning: boolean + autoRotate: boolean + mouseButtons: { + LEFT: MOUSE + RIGHT: MOUSE + } + touches: { + ONE: TOUCH + TWO: TOUCH + } + + constructor(object: PerspectiveCamera | OrthographicCamera, domElement: HTMLElement) { + super(object, domElement) + + this.trackball = true + this.screenSpacePanning = true + this.autoRotate = false + + this.mouseButtons = { + LEFT: MOUSE.ROTATE, + RIGHT: MOUSE.PAN, + } + + this.touches = { + ONE: TOUCH.ROTATE, + TWO: TOUCH.DOLLY_PAN, + } + } +} + +export { CameraControls, OrbitControlsExp, MapControlsExp, TrackballControlsExp } diff --git a/src/lines/Line2.js b/src/lines/Line2.js deleted file mode 100644 index 3835e007..00000000 --- a/src/lines/Line2.js +++ /dev/null @@ -1,20 +0,0 @@ -import { LineSegments2 } from '../lines/LineSegments2' -import { LineGeometry } from '../lines/LineGeometry' -import { LineMaterial } from '../lines/LineMaterial' - -var Line2 = function (geometry, material) { - if (geometry === undefined) geometry = new LineGeometry() - if (material === undefined) material = new LineMaterial({ color: Math.random() * 0xffffff }) - - LineSegments2.call(this, geometry, material) - - this.type = 'Line2' -} - -Line2.prototype = Object.assign(Object.create(LineSegments2.prototype), { - constructor: Line2, - - isLine2: true, -}) - -export { Line2 } diff --git a/src/lines/Line2.ts b/src/lines/Line2.ts new file mode 100644 index 00000000..ba1999c6 --- /dev/null +++ b/src/lines/Line2.ts @@ -0,0 +1,14 @@ +import { LineSegments2 } from './LineSegments2' +import { LineGeometry } from './LineGeometry' +import { LineMaterial } from './LineMaterial' + +class Line2 extends LineSegments2 { + type = 'Line2' + isLine2 = true + + constructor(geometry = new LineGeometry(), material = new LineMaterial({ color: Math.random() * 0xffffff })) { + super(geometry, material) + } +} + +export { Line2 } diff --git a/src/lines/LineGeometry.js b/src/lines/LineGeometry.ts similarity index 50% rename from src/lines/LineGeometry.js rename to src/lines/LineGeometry.ts index 3a4d98cc..7820cea4 100644 --- a/src/lines/LineGeometry.js +++ b/src/lines/LineGeometry.ts @@ -1,21 +1,19 @@ -import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry' +import { Line } from 'three' +import { LineSegmentsGeometry } from './LineSegmentsGeometry' -var LineGeometry = function () { - LineSegmentsGeometry.call(this) +class LineGeometry extends LineSegmentsGeometry { + type = 'LineGeometry' + isLineGeometry = true - this.type = 'LineGeometry' -} - -LineGeometry.prototype = Object.assign(Object.create(LineSegmentsGeometry.prototype), { - constructor: LineGeometry, + constructor() { + super() + } - isLineGeometry: true, - - setPositions: function (array) { + setPositions = (array: number[] | Float32Array): this => { // converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format - var length = array.length - 3 - var points = new Float32Array(2 * length) + const length = array.length - 3 + const points = new Float32Array(2 * length) for (let i = 0; i < length; i += 3) { points[2 * i] = array[i] @@ -30,13 +28,13 @@ LineGeometry.prototype = Object.assign(Object.create(LineSegmentsGeometry.protot LineSegmentsGeometry.prototype.setPositions.call(this, points) return this - }, + } - setColors: function (array) { + setColors = (array: number[] | Float32Array): this => { // converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format - var length = array.length - 3 - var colors = new Float32Array(2 * length) + const length = array.length - 3 + const colors = new Float32Array(2 * length) for (let i = 0; i < length; i += 3) { colors[2 * i] = array[i] @@ -51,28 +49,25 @@ LineGeometry.prototype = Object.assign(Object.create(LineSegmentsGeometry.protot LineSegmentsGeometry.prototype.setColors.call(this, colors) return this - }, + } - fromLine: function (line) { - var geometry = line.geometry + fromLine = (line: Line): this => { + const geometry = line.geometry - if (geometry.isGeometry) { - console.error('THREE.LineGeometry no longer supports Geometry. Use THREE.BufferGeometry instead.') - return - } else if (geometry.isBufferGeometry) { - this.setPositions(geometry.attributes.position.array) // assumes non-indexed + if (geometry.isBufferGeometry) { + this.setPositions(Array.from(geometry.attributes.position.array)) // assumes non-indexed } // set colors, maybe return this - }, + } - copy: function (/* source */) { + copy = (/* source */): this => { // todo return this - }, -}) + } +} export { LineGeometry } diff --git a/src/lines/LineMaterial.js b/src/lines/LineMaterial.ts similarity index 66% rename from src/lines/LineMaterial.js rename to src/lines/LineMaterial.ts index 365d892e..a48a2b38 100644 --- a/src/lines/LineMaterial.js +++ b/src/lines/LineMaterial.ts @@ -1,17 +1,17 @@ -import { ShaderLib, ShaderMaterial, UniformsLib, UniformsUtils, Vector2 } from 'three' - -/** - * parameters = { - * color: , - * linewidth: , - * dashed: , - * dashScale: , - * dashSize: , - * dashOffset: , - * gapSize: , - * resolution: , // to be set by renderer - * } - */ +import { ShaderLib, ShaderMaterial, ShaderMaterialParameters, UniformsLib, UniformsUtils, Vector2 } from 'three' + +import { ColorOptions } from '../types/shared' + +export type LineMaterialParameters = ShaderMaterialParameters & { + color?: ColorOptions + linewidth?: number + dashed?: boolean + dashScale?: number + dashSize?: number + dashOffset?: number + gapSize?: number + resolution?: Vector2 +} UniformsLib.line = { linewidth: { value: 1 }, @@ -26,7 +26,7 @@ UniformsLib.line = { ShaderLib['line'] = { uniforms: UniformsUtils.merge([UniformsLib.common, UniformsLib.fog, UniformsLib.line]), - vertexShader: ` + vertexShader: /* glsl */ ` #include #include #include @@ -172,7 +172,7 @@ ShaderLib['line'] = { } `, - fragmentShader: ` + fragmentShader: /* glsl */ ` uniform vec3 diffuse; uniform float opacity; @@ -232,124 +232,110 @@ ShaderLib['line'] = { `, } -var LineMaterial = function (parameters) { - ShaderMaterial.call(this, { - type: 'LineMaterial', +class LineMaterial extends ShaderMaterial { + dashed: boolean - uniforms: UniformsUtils.clone(ShaderLib['line'].uniforms), + isLineMaterial = true - vertexShader: ShaderLib['line'].vertexShader, - fragmentShader: ShaderLib['line'].fragmentShader, + get color(): any { + return this.uniforms.diffuse.value + } - clipping: true, // required for clipping support - }) + set color(value: any) { + this.uniforms.diffuse.value = value + } - this.dashed = false + get dashScale(): number { + return this.uniforms.dashScale.value + } - Object.defineProperties(this, { - color: { - enumerable: true, + set dashScale(value: number) { + this.uniforms.dashScale.value = value + } - get: function () { - return this.uniforms.diffuse.value - }, + get dashSize(): number { + return this.uniforms.dashSize.value + } - set: function (value) { - this.uniforms.diffuse.value = value - }, - }, + set dashSize(value: number) { + this.uniforms.dashSize.value = value + } - linewidth: { - enumerable: true, + get dashOffset(): number { + return this.uniforms.dashOffset.value + } - get: function () { - return this.uniforms.linewidth.value - }, + set dashOffset(value: number) { + this.uniforms.dashOffset.value = value + } - set: function (value) { - this.uniforms.linewidth.value = value - }, - }, + get gapSize(): number { + return this.uniforms.gapSize.value + } - dashScale: { - enumerable: true, + set gapSize(value: number) { + this.uniforms.gapSize.value = value + } - get: function () { - return this.uniforms.dashScale.value - }, + get resolution(): Vector2 { + return this.uniforms.gapSize.value + } - set: function (value) { - this.uniforms.dashScale.value = value - }, - }, + set resolution(value: Vector2) { + this.uniforms.gapSize.value = value + } - dashSize: { - enumerable: true, + constructor(parameters: LineMaterialParameters = {}) { + super({ + uniforms: UniformsUtils.clone(ShaderLib['line'].uniforms), + vertexShader: ShaderLib['line'].vertexShader, + fragmentShader: ShaderLib['line'].fragmentShader, + clipping: true, // required for clipping support + }) - get: function () { - return this.uniforms.dashSize.value - }, + this.dashed = false - set: function (value) { - this.uniforms.dashSize.value = value + Object.defineProperties(this, { + color: { + enumerable: true, }, - }, - - dashOffset: { - enumerable: true, - - get: function () { - return this.uniforms.dashOffset.value + linewidth: { + enumerable: true, + get: function (): number { + return this.uniforms.linewidth.value + }, + set: function (value: number) { + this.uniforms.linewidth.value = value + }, }, - - set: function (value) { - this.uniforms.dashOffset.value = value + dashScale: { + enumerable: true, }, - }, - - gapSize: { - enumerable: true, - - get: function () { - return this.uniforms.gapSize.value + dashSize: { + enumerable: true, }, - - set: function (value) { - this.uniforms.gapSize.value = value + dashOffset: { + enumerable: true, }, - }, - - opacity: { - enumerable: true, - - get: function () { - return this.uniforms.opacity.value + gapSize: { + enumerable: true, }, - - set: function (value) { - this.uniforms.opacity.value = value + opacity: { + enumerable: true, + get: function (): number { + return this.uniforms.opacity.value + }, + set: function (value: number) { + this.uniforms.opacity.value = value + }, }, - }, - - resolution: { - enumerable: true, - - get: function () { - return this.uniforms.resolution.value - }, - - set: function (value) { - this.uniforms.resolution.value.copy(value) + resolution: { + enumerable: true, }, - }, - }) + }) - this.setValues(parameters) + this.setValues(parameters) + } } -LineMaterial.prototype = Object.create(ShaderMaterial.prototype) -LineMaterial.prototype.constructor = LineMaterial - -LineMaterial.prototype.isLineMaterial = true - export { LineMaterial } diff --git a/src/lines/LineSegments2.js b/src/lines/LineSegments2.js deleted file mode 100644 index 94cca463..00000000 --- a/src/lines/LineSegments2.js +++ /dev/null @@ -1,200 +0,0 @@ -import { - InstancedInterleavedBuffer, - InterleavedBufferAttribute, - Line3, - MathUtils, - Matrix4, - Mesh, - Vector3, - Vector4, -} from 'three' -import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry' -import { LineMaterial } from '../lines/LineMaterial' - -var LineSegments2 = function (geometry, material) { - if (geometry === undefined) geometry = new LineSegmentsGeometry() - if (material === undefined) material = new LineMaterial({ color: Math.random() * 0xffffff }) - - Mesh.call(this, geometry, material) - - this.type = 'LineSegments2' -} - -LineSegments2.prototype = Object.assign(Object.create(Mesh.prototype), { - constructor: LineSegments2, - - isLineSegments2: true, - - computeLineDistances: (function () { - // for backwards-compatability, but could be a method of LineSegmentsGeometry... - - var start = new Vector3() - var end = new Vector3() - - return function computeLineDistances() { - var geometry = this.geometry - - var instanceStart = geometry.attributes.instanceStart - var instanceEnd = geometry.attributes.instanceEnd - var lineDistances = new Float32Array(2 * instanceStart.data.count) - - for (let i = 0, j = 0, l = instanceStart.data.count; i < l; i++, j += 2) { - start.fromBufferAttribute(instanceStart, i) - end.fromBufferAttribute(instanceEnd, i) - - lineDistances[j] = j === 0 ? 0 : lineDistances[j - 1] - lineDistances[j + 1] = lineDistances[j] + start.distanceTo(end) - } - - var instanceDistanceBuffer = new InstancedInterleavedBuffer(lineDistances, 2, 1) // d0, d1 - - geometry.setAttribute('instanceDistanceStart', new InterleavedBufferAttribute(instanceDistanceBuffer, 1, 0)) // d0 - geometry.setAttribute('instanceDistanceEnd', new InterleavedBufferAttribute(instanceDistanceBuffer, 1, 1)) // d1 - - return this - } - })(), - - raycast: (function () { - var start = new Vector4() - var end = new Vector4() - - var ssOrigin = new Vector4() - var ssOrigin3 = new Vector3() - var mvMatrix = new Matrix4() - var line = new Line3() - var closestPoint = new Vector3() - - return function raycast(raycaster, intersects) { - if (raycaster.camera === null) { - console.error('LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2.') - } - - var threshold = raycaster.params.Line2 !== undefined ? raycaster.params.Line2.threshold || 0 : 0 - - var ray = raycaster.ray - var camera = raycaster.camera - var projectionMatrix = camera.projectionMatrix - - var geometry = this.geometry - var material = this.material - var resolution = material.resolution - var lineWidth = material.linewidth + threshold - - var instanceStart = geometry.attributes.instanceStart - var instanceEnd = geometry.attributes.instanceEnd - - // camera forward is negative - var near = -camera.near - - // pick a point 1 unit out along the ray to avoid the ray origin - // sitting at the camera origin which will cause "w" to be 0 when - // applying the projection matrix. - ray.at(1, ssOrigin) - - // ndc space [ - 1.0, 1.0 ] - ssOrigin.w = 1 - ssOrigin.applyMatrix4(camera.matrixWorldInverse) - ssOrigin.applyMatrix4(projectionMatrix) - ssOrigin.multiplyScalar(1 / ssOrigin.w) - - // screen space - ssOrigin.x *= resolution.x / 2 - ssOrigin.y *= resolution.y / 2 - ssOrigin.z = 0 - - ssOrigin3.copy(ssOrigin) - - var matrixWorld = this.matrixWorld - mvMatrix.multiplyMatrices(camera.matrixWorldInverse, matrixWorld) - - for (let i = 0, l = instanceStart.count; i < l; i++) { - start.fromBufferAttribute(instanceStart, i) - end.fromBufferAttribute(instanceEnd, i) - - start.w = 1 - end.w = 1 - - // camera space - start.applyMatrix4(mvMatrix) - end.applyMatrix4(mvMatrix) - - // skip the segment if it's entirely behind the camera - var isBehindCameraNear = start.z > near && end.z > near - if (isBehindCameraNear) { - continue - } - - // trim the segment if it extends behind camera near - if (start.z > near) { - const deltaDist = start.z - end.z - const t = (start.z - near) / deltaDist - start.lerp(end, t) - } else if (end.z > near) { - const deltaDist = end.z - start.z - const t = (end.z - near) / deltaDist - end.lerp(start, t) - } - - // clip space - start.applyMatrix4(projectionMatrix) - end.applyMatrix4(projectionMatrix) - - // ndc space [ - 1.0, 1.0 ] - start.multiplyScalar(1 / start.w) - end.multiplyScalar(1 / end.w) - - // screen space - start.x *= resolution.x / 2 - start.y *= resolution.y / 2 - - end.x *= resolution.x / 2 - end.y *= resolution.y / 2 - - // create 2d segment - line.start.copy(start) - line.start.z = 0 - - line.end.copy(end) - line.end.z = 0 - - // get closest point on ray to segment - var param = line.closestPointToPointParameter(ssOrigin3, true) - line.at(param, closestPoint) - - // check if the intersection point is within clip space - var zPos = MathUtils.lerp(start.z, end.z, param) - var isInClipSpace = zPos >= -1 && zPos <= 1 - - var isInside = ssOrigin3.distanceTo(closestPoint) < lineWidth * 0.5 - - if (isInClipSpace && isInside) { - line.start.fromBufferAttribute(instanceStart, i) - line.end.fromBufferAttribute(instanceEnd, i) - - line.start.applyMatrix4(matrixWorld) - line.end.applyMatrix4(matrixWorld) - - var pointOnLine = new Vector3() - var point = new Vector3() - - ray.distanceSqToSegment(line.start, line.end, point, pointOnLine) - - intersects.push({ - point: point, - pointOnLine: pointOnLine, - distance: ray.origin.distanceTo(point), - - object: this, - face: null, - faceIndex: i, - uv: null, - uv2: null, - }) - } - } - } - })(), -}) - -export { LineSegments2 } diff --git a/src/lines/LineSegments2.ts b/src/lines/LineSegments2.ts new file mode 100644 index 00000000..cac19c68 --- /dev/null +++ b/src/lines/LineSegments2.ts @@ -0,0 +1,190 @@ +import { + InstancedInterleavedBuffer, + InterleavedBufferAttribute, + Line3, + MathUtils, + Matrix4, + Mesh, + PerspectiveCamera, + Raycaster, + Vector3, + Vector4, + BufferAttribute, + Intersection, +} from 'three' +import { LineSegmentsGeometry } from '../lines/LineSegmentsGeometry' +import { LineMaterial } from '../lines/LineMaterial' + +class LineSegments2 extends Mesh { + type = 'LineSegments2' + isLineSegments2 = true + + constructor(geometry = new LineSegmentsGeometry(), material = new LineMaterial({ color: Math.random() * 0xffffff })) { + super(geometry, material) + } + + private distStart = new Vector3() + private distEnd = new Vector3() + + computeLineDistances = () => { + const geometry = this.geometry + + const instanceStart = geometry.attributes.instanceStart as InterleavedBufferAttribute + const instanceEnd = geometry.attributes.instanceEnd + const lineDistances = new Float32Array(2 * instanceStart.data.count) + + for (let i = 0, j = 0, l = instanceStart.data.count; i < l; i++, j += 2) { + this.distStart.fromBufferAttribute(instanceStart, i) + this.distEnd.fromBufferAttribute(instanceEnd, i) + + lineDistances[j] = j === 0 ? 0 : lineDistances[j - 1] + lineDistances[j + 1] = lineDistances[j] + this.distStart.distanceTo(this.distEnd) + } + + const instanceDistanceBuffer = new InstancedInterleavedBuffer(lineDistances, 2, 1) // d0, d1 + + geometry.setAttribute('instanceDistanceStart', new InterleavedBufferAttribute(instanceDistanceBuffer, 1, 0)) // d0 + geometry.setAttribute('instanceDistanceEnd', new InterleavedBufferAttribute(instanceDistanceBuffer, 1, 1)) // d1 + + return this + } + + private rayStart = new Vector4() + private rayEnd = new Vector4() + + private ssOrigin = new Vector4() + private ssOrigin3 = new Vector3() + private mvMatrix = new Matrix4() + private line = new Line3() + private closestPoint = new Vector3() + + raycast = (raycaster: Raycaster, intersects: Array) => { + if (raycaster.camera === null) { + console.error('LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2.') + } + + const threshold = 0 + + const ray = raycaster.ray + const camera = raycaster.camera as PerspectiveCamera + const projectionMatrix = camera.projectionMatrix + + const geometry = this.geometry + const material = this.material + const resolution = material.resolution + const lineWidth = material.linewidth + threshold + + const instanceStart = geometry.attributes.instanceStart as BufferAttribute + const instanceEnd = geometry.attributes.instanceEnd as BufferAttribute + + // camera forward is negative + const near = -camera.near + + // pick a point 1 unit out along the ray to avoid the ray origin + // sitting at the camera origin which will cause "w" to be 0 when + // applying the projection matrix. + ray.at(1, new Vector3(this.ssOrigin.x, this.ssOrigin.y, this.ssOrigin.z)) + + // ndc space [ - 1.0, 1.0 ] + this.ssOrigin.w = 1 + this.ssOrigin.applyMatrix4(camera.matrixWorldInverse) + this.ssOrigin.applyMatrix4(projectionMatrix) + this.ssOrigin.multiplyScalar(1 / this.ssOrigin.w) + + // screen space + this.ssOrigin.x *= resolution.x / 2 + this.ssOrigin.y *= resolution.y / 2 + this.ssOrigin.z = 0 + + this.ssOrigin3.set(this.ssOrigin.x, this.ssOrigin.y, this.ssOrigin.z) + + const matrixWorld = this.matrixWorld + this.mvMatrix.multiplyMatrices(camera.matrixWorldInverse, matrixWorld) + + for (let i = 0, l = instanceStart.count; i < l; i++) { + this.rayStart.fromBufferAttribute(instanceStart, i) + this.rayEnd.fromBufferAttribute(instanceEnd, i) + + this.rayStart.w = 1 + this.rayEnd.w = 1 + + // camera space + this.rayStart.applyMatrix4(this.mvMatrix) + this.rayEnd.applyMatrix4(this.mvMatrix) + + // skip the segment if it's entirely behind the camera + const isBehindCameraNear = this.rayStart.z > near && this.rayEnd.z > near + if (isBehindCameraNear) { + continue + } + + // trim the segment if it extends behind camera near + if (this.rayStart.z > near) { + const deltaDist = this.rayStart.z - this.rayEnd.z + const t = (this.rayStart.z - near) / deltaDist + this.rayStart.lerp(this.rayEnd, t) + } else if (this.rayEnd.z > near) { + const deltaDist = this.rayEnd.z - this.rayStart.z + const t = (this.rayEnd.z - near) / deltaDist + this.rayEnd.lerp(this.rayStart, t) + } + + // clip space + this.rayStart.applyMatrix4(projectionMatrix) + this.rayEnd.applyMatrix4(projectionMatrix) + + // ndc space [ - 1.0, 1.0 ] + this.rayStart.multiplyScalar(1 / this.rayStart.w) + this.rayEnd.multiplyScalar(1 / this.rayEnd.w) + + // screen space + this.rayStart.x *= resolution.x / 2 + this.rayStart.y *= resolution.y / 2 + + this.rayEnd.x *= resolution.x / 2 + this.rayEnd.y *= resolution.y / 2 + + // create 2d segment + this.line.start.set(this.rayStart.x, this.rayStart.y, this.rayStart.z) + this.line.start.z = 0 + + this.line.end.set(this.rayEnd.x, this.rayEnd.y, this.rayEnd.z) + this.line.end.z = 0 + + // get closest point on ray to segment + const param = this.line.closestPointToPointParameter(this.ssOrigin3, true) + this.line.at(param, this.closestPoint) + + // check if the intersection point is within clip space + const zPos = MathUtils.lerp(this.rayStart.z, this.rayEnd.z, param) + const isInClipSpace = zPos >= -1 && zPos <= 1 + + const isInside = this.ssOrigin3.distanceTo(this.closestPoint) < lineWidth * 0.5 + + if (isInClipSpace && isInside) { + this.line.start.fromBufferAttribute(instanceStart, i) + this.line.end.fromBufferAttribute(instanceEnd, i) + + this.line.start.applyMatrix4(matrixWorld) + this.line.end.applyMatrix4(matrixWorld) + + const pointOnLine = new Vector3() + const point = new Vector3() + + ray.distanceSqToSegment(this.line.start, this.line.end, point, pointOnLine) + + intersects.push({ + distance: ray.origin.distanceTo(point), + point: point, + face: null, + faceIndex: i, + object: this, + uv: undefined, + pointOnLine, + }) + } + } + } +} + +export { LineSegments2 } diff --git a/src/lines/LineSegmentsGeometry.js b/src/lines/LineSegmentsGeometry.js deleted file mode 100644 index 4fc89c89..00000000 --- a/src/lines/LineSegmentsGeometry.js +++ /dev/null @@ -1,202 +0,0 @@ -import { - Box3, - Float32BufferAttribute, - InstancedBufferGeometry, - InstancedInterleavedBuffer, - InterleavedBufferAttribute, - Sphere, - Vector3, - WireframeGeometry, -} from 'three' - -var LineSegmentsGeometry = function () { - InstancedBufferGeometry.call(this) - - this.type = 'LineSegmentsGeometry' - - var positions = [-1, 2, 0, 1, 2, 0, -1, 1, 0, 1, 1, 0, -1, 0, 0, 1, 0, 0, -1, -1, 0, 1, -1, 0] - var uvs = [-1, 2, 1, 2, -1, 1, 1, 1, -1, -1, 1, -1, -1, -2, 1, -2] - var index = [0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5] - - this.setIndex(index) - this.setAttribute('position', new Float32BufferAttribute(positions, 3)) - this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) -} - -LineSegmentsGeometry.prototype = Object.assign(Object.create(InstancedBufferGeometry.prototype), { - constructor: LineSegmentsGeometry, - - isLineSegmentsGeometry: true, - - applyMatrix4: function (matrix) { - var start = this.attributes.instanceStart - var end = this.attributes.instanceEnd - - if (start !== undefined) { - start.applyMatrix4(matrix) - - end.applyMatrix4(matrix) - - start.needsUpdate = true - } - - if (this.boundingBox !== null) { - this.computeBoundingBox() - } - - if (this.boundingSphere !== null) { - this.computeBoundingSphere() - } - - return this - }, - - setPositions: function (array) { - var lineSegments - - if (array instanceof Float32Array) { - lineSegments = array - } else if (Array.isArray(array)) { - lineSegments = new Float32Array(array) - } - - var instanceBuffer = new InstancedInterleavedBuffer(lineSegments, 6, 1) // xyz, xyz - - this.setAttribute('instanceStart', new InterleavedBufferAttribute(instanceBuffer, 3, 0)) // xyz - this.setAttribute('instanceEnd', new InterleavedBufferAttribute(instanceBuffer, 3, 3)) // xyz - - // - - this.computeBoundingBox() - this.computeBoundingSphere() - - return this - }, - - setColors: function (array) { - var colors - - if (array instanceof Float32Array) { - colors = array - } else if (Array.isArray(array)) { - colors = new Float32Array(array) - } - - var instanceColorBuffer = new InstancedInterleavedBuffer(colors, 6, 1) // rgb, rgb - - this.setAttribute('instanceColorStart', new InterleavedBufferAttribute(instanceColorBuffer, 3, 0)) // rgb - this.setAttribute('instanceColorEnd', new InterleavedBufferAttribute(instanceColorBuffer, 3, 3)) // rgb - - return this - }, - - fromWireframeGeometry: function (geometry) { - this.setPositions(geometry.attributes.position.array) - - return this - }, - - fromEdgesGeometry: function (geometry) { - this.setPositions(geometry.attributes.position.array) - - return this - }, - - fromMesh: function (mesh) { - this.fromWireframeGeometry(new WireframeGeometry(mesh.geometry)) - - // set colors, maybe - - return this - }, - - fromLineSegments: function (lineSegments) { - var geometry = lineSegments.geometry - - if (geometry.isGeometry) { - console.error('THREE.LineSegmentsGeometry no longer supports Geometry. Use THREE.BufferGeometry instead.') - return - } else if (geometry.isBufferGeometry) { - this.setPositions(geometry.attributes.position.array) // assumes non-indexed - } - - // set colors, maybe - - return this - }, - - computeBoundingBox: (function () { - var box = new Box3() - - return function computeBoundingBox() { - if (this.boundingBox === null) { - this.boundingBox = new Box3() - } - - var start = this.attributes.instanceStart - var end = this.attributes.instanceEnd - - if (start !== undefined && end !== undefined) { - this.boundingBox.setFromBufferAttribute(start) - - box.setFromBufferAttribute(end) - - this.boundingBox.union(box) - } - } - })(), - - computeBoundingSphere: (function () { - var vector = new Vector3() - - return function computeBoundingSphere() { - if (this.boundingSphere === null) { - this.boundingSphere = new Sphere() - } - - if (this.boundingBox === null) { - this.computeBoundingBox() - } - - var start = this.attributes.instanceStart - var end = this.attributes.instanceEnd - - if (start !== undefined && end !== undefined) { - var center = this.boundingSphere.center - - this.boundingBox.getCenter(center) - - var maxRadiusSq = 0 - - for (let i = 0, il = start.count; i < il; i++) { - vector.fromBufferAttribute(start, i) - maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(vector)) - - vector.fromBufferAttribute(end, i) - maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(vector)) - } - - this.boundingSphere.radius = Math.sqrt(maxRadiusSq) - - if (isNaN(this.boundingSphere.radius)) { - console.error( - 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', - this, - ) - } - } - } - })(), - - toJSON: function () { - // todo - }, - - applyMatrix: function (matrix) { - console.warn('THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().') - - return this.applyMatrix4(matrix) - }, -}) - -export { LineSegmentsGeometry } diff --git a/src/lines/LineSegmentsGeometry.ts b/src/lines/LineSegmentsGeometry.ts new file mode 100644 index 00000000..8d004612 --- /dev/null +++ b/src/lines/LineSegmentsGeometry.ts @@ -0,0 +1,206 @@ +import { + Box3, + BufferGeometry, + BufferAttribute, + Float32BufferAttribute, + InstancedBufferGeometry, + InstancedInterleavedBuffer, + InterleavedBufferAttribute, + LineSegments, + Matrix4, + Mesh, + Sphere, + Vector3, + WireframeGeometry, +} from 'three' + +class LineSegmentsGeometry extends InstancedBufferGeometry { + isLineSegmentsGeometry = true + type = 'LineSegmentsGeometry' + + boundingBox: Box3 | null = null + boundingSphere: Sphere | null = null + + constructor() { + super() + + const positions = [-1, 2, 0, 1, 2, 0, -1, 1, 0, 1, 1, 0, -1, 0, 0, 1, 0, 0, -1, -1, 0, 1, -1, 0] + const uvs = [-1, 2, 1, 2, -1, 1, 1, 1, -1, -1, 1, -1, -1, -2, 1, -2] + const index = [0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5] + + this.setIndex(index) + this.setAttribute('position', new Float32BufferAttribute(positions, 3)) + this.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) + } + + applyMatrix4 = (matrix: Matrix4): this => { + const start = this.attributes.instanceStart + const end = this.attributes.instanceEnd + + if (start !== undefined) { + start.applyMatrix4(matrix) + + end.applyMatrix4(matrix) + + start.needsUpdate = true + } + + if (this.boundingBox !== null) { + this.computeBoundingBox() + } + + if (this.boundingSphere !== null) { + this.computeBoundingSphere() + } + + return this + } + + setPositions = (array: number[] | Float32Array): this => { + let lineSegments + + if (array instanceof Float32Array) { + lineSegments = array + } else if (Array.isArray(array)) { + lineSegments = new Float32Array(array) + } else { + console.error('LineSegmentsGeometry.setPosition requires either a Float32Array or regular array of numbers') + return this + } + + const instanceBuffer = new InstancedInterleavedBuffer(lineSegments, 6, 1) // xyz, xyz + + this.setAttribute('instanceStart', new InterleavedBufferAttribute(instanceBuffer, 3, 0)) // xyz + this.setAttribute('instanceEnd', new InterleavedBufferAttribute(instanceBuffer, 3, 3)) // xyz + + // + + this.computeBoundingBox() + this.computeBoundingSphere() + + return this + } + + setColors = (array: number[] | Float32Array): this => { + let colors + + if (array instanceof Float32Array) { + colors = array + } else if (Array.isArray(array)) { + colors = new Float32Array(array) + } else { + console.error('LineSegmentsGeometry.setColors requires either a Float32Array or regular array of numbers') + return this + } + + const instanceColorBuffer = new InstancedInterleavedBuffer(colors, 6, 1) // rgb, rgb + + this.setAttribute('instanceColorStart', new InterleavedBufferAttribute(instanceColorBuffer, 3, 0)) // rgb + this.setAttribute('instanceColorEnd', new InterleavedBufferAttribute(instanceColorBuffer, 3, 3)) // rgb + + return this + } + + fromWireframeGeometry = (geometry: BufferGeometry): this => { + this.setPositions(Array.from(geometry.attributes.position.array)) + + return this + } + + fromEdgesGeometry = (geometry: BufferGeometry): this => { + this.setPositions(Array.from(geometry.attributes.position.array)) + + return this + } + + fromMesh = (mesh: Mesh): this => { + this.fromWireframeGeometry(new WireframeGeometry(mesh.geometry)) + + return this + } + + fromLineSegments = (lineSegments: LineSegments): this => { + const geometry = lineSegments.geometry + + if (geometry.isBufferGeometry) { + this.setPositions(Array.from(geometry.attributes.position.array)) // assumes non-indexed + } + + // set colors, maybe + + return this + } + + private box = new Box3() + + computeBoundingBox = () => { + if (this.boundingBox === null) { + this.boundingBox = new Box3() + } + + const start = this.attributes.instanceStart as BufferAttribute + const end = this.attributes.instanceEnd as BufferAttribute + + if (start !== undefined && end !== undefined) { + this.boundingBox.setFromBufferAttribute(start) + + this.box.setFromBufferAttribute(end) + + this.boundingBox.union(this.box) + } + } + + private vector = new Vector3() + + computeBoundingSphere = () => { + if (this.boundingSphere === null) { + this.boundingSphere = new Sphere() + } + + if (this.boundingBox === null) { + this.computeBoundingBox() + } + + const start = this.attributes.instanceStart + const end = this.attributes.instanceEnd + + if (start !== undefined && end !== undefined) { + const center = this.boundingSphere.center + + if (this.boundingBox) { + this.boundingBox.getCenter(center) + } + + let maxRadiusSq = 0 + + for (let i = 0, il = start.count; i < il; i++) { + this.vector.fromBufferAttribute(start, i) + maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(this.vector)) + + this.vector.fromBufferAttribute(end, i) + maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(this.vector)) + } + + this.boundingSphere.radius = Math.sqrt(maxRadiusSq) + + if (isNaN(this.boundingSphere.radius)) { + console.error( + 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', + this, + ) + } + } + } + + toJSON = () => { + // todo + } + + applyMatrix = (matrix: Matrix4) => { + console.warn('THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().') + + return this.applyMatrix4(matrix) + } +} + +export { LineSegmentsGeometry } diff --git a/src/math/SimplexNoise.js b/src/math/SimplexNoise.js deleted file mode 100644 index 6eadf58d..00000000 --- a/src/math/SimplexNoise.js +++ /dev/null @@ -1,491 +0,0 @@ -// Ported from Stefan Gustavson's java implementation -// http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf -// Read Stefan's excellent paper for details on how this code works. -// -// Sean McCullough banksean@gmail.com -// -// Added 4D noise - -/** - * You can pass in a random number generator object if you like. - * It is assumed to have a random() method. - */ -var SimplexNoise = function (r) { - if (r == undefined) r = Math - this.grad3 = [ - [1, 1, 0], - [-1, 1, 0], - [1, -1, 0], - [-1, -1, 0], - [1, 0, 1], - [-1, 0, 1], - [1, 0, -1], - [-1, 0, -1], - [0, 1, 1], - [0, -1, 1], - [0, 1, -1], - [0, -1, -1], - ] - - this.grad4 = [ - [0, 1, 1, 1], - [0, 1, 1, -1], - [0, 1, -1, 1], - [0, 1, -1, -1], - [0, -1, 1, 1], - [0, -1, 1, -1], - [0, -1, -1, 1], - [0, -1, -1, -1], - [1, 0, 1, 1], - [1, 0, 1, -1], - [1, 0, -1, 1], - [1, 0, -1, -1], - [-1, 0, 1, 1], - [-1, 0, 1, -1], - [-1, 0, -1, 1], - [-1, 0, -1, -1], - [1, 1, 0, 1], - [1, 1, 0, -1], - [1, -1, 0, 1], - [1, -1, 0, -1], - [-1, 1, 0, 1], - [-1, 1, 0, -1], - [-1, -1, 0, 1], - [-1, -1, 0, -1], - [1, 1, 1, 0], - [1, 1, -1, 0], - [1, -1, 1, 0], - [1, -1, -1, 0], - [-1, 1, 1, 0], - [-1, 1, -1, 0], - [-1, -1, 1, 0], - [-1, -1, -1, 0], - ] - - this.p = [] - - for (let i = 0; i < 256; i++) { - this.p[i] = Math.floor(r.random() * 256) - } - - // To remove the need for index wrapping, double the permutation table length - this.perm = [] - - for (let i = 0; i < 512; i++) { - this.perm[i] = this.p[i & 255] - } - - // A lookup table to traverse the simplex around a given point in 4D. - // Details can be found where this table is used, in the 4D noise method. - this.simplex = [ - [0, 1, 2, 3], - [0, 1, 3, 2], - [0, 0, 0, 0], - [0, 2, 3, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [1, 2, 3, 0], - [0, 2, 1, 3], - [0, 0, 0, 0], - [0, 3, 1, 2], - [0, 3, 2, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [1, 3, 2, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [1, 2, 0, 3], - [0, 0, 0, 0], - [1, 3, 0, 2], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [2, 3, 0, 1], - [2, 3, 1, 0], - [1, 0, 2, 3], - [1, 0, 3, 2], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [2, 0, 3, 1], - [0, 0, 0, 0], - [2, 1, 3, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [2, 0, 1, 3], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [3, 0, 1, 2], - [3, 0, 2, 1], - [0, 0, 0, 0], - [3, 1, 2, 0], - [2, 1, 0, 3], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [3, 1, 0, 2], - [0, 0, 0, 0], - [3, 2, 0, 1], - [3, 2, 1, 0], - ] -} - -SimplexNoise.prototype.dot = function (g, x, y) { - return g[0] * x + g[1] * y -} - -SimplexNoise.prototype.dot3 = function (g, x, y, z) { - return g[0] * x + g[1] * y + g[2] * z -} - -SimplexNoise.prototype.dot4 = function (g, x, y, z, w) { - return g[0] * x + g[1] * y + g[2] * z + g[3] * w -} - -SimplexNoise.prototype.noise = function (xin, yin) { - var n0, n1, n2 // Noise contributions from the three corners - // Skew the input space to determine which simplex cell we're in - var F2 = 0.5 * (Math.sqrt(3.0) - 1.0) - var s = (xin + yin) * F2 // Hairy factor for 2D - var i = Math.floor(xin + s) - var j = Math.floor(yin + s) - var G2 = (3.0 - Math.sqrt(3.0)) / 6.0 - var t = (i + j) * G2 - var X0 = i - t // Unskew the cell origin back to (x,y) space - var Y0 = j - t - var x0 = xin - X0 // The x,y distances from the cell origin - var y0 = yin - Y0 - // For the 2D case, the simplex shape is an equilateral triangle. - // Determine which simplex we are in. - var i1, j1 // Offsets for second (middle) corner of simplex in (i,j) coords - if (x0 > y0) { - i1 = 1 - j1 = 0 - - // lower triangle, XY order: (0,0)->(1,0)->(1,1) - } else { - i1 = 0 - j1 = 1 - } // upper triangle, YX order: (0,0)->(0,1)->(1,1) - - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - var x1 = x0 - i1 + G2 // Offsets for middle corner in (x,y) unskewed coords - var y1 = y0 - j1 + G2 - var x2 = x0 - 1.0 + 2.0 * G2 // Offsets for last corner in (x,y) unskewed coords - var y2 = y0 - 1.0 + 2.0 * G2 - // Work out the hashed gradient indices of the three simplex corners - var ii = i & 255 - var jj = j & 255 - var gi0 = this.perm[ii + this.perm[jj]] % 12 - var gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12 - var gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12 - // Calculate the contribution from the three corners - var t0 = 0.5 - x0 * x0 - y0 * y0 - if (t0 < 0) n0 = 0.0 - else { - t0 *= t0 - n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0) // (x,y) of grad3 used for 2D gradient - } - - var t1 = 0.5 - x1 * x1 - y1 * y1 - if (t1 < 0) n1 = 0.0 - else { - t1 *= t1 - n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1) - } - - var t2 = 0.5 - x2 * x2 - y2 * y2 - if (t2 < 0) n2 = 0.0 - else { - t2 *= t2 - n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2) - } - - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1,1]. - return 70.0 * (n0 + n1 + n2) -} - -// 3D simplex noise -SimplexNoise.prototype.noise3d = function (xin, yin, zin) { - var n0, n1, n2, n3 // Noise contributions from the four corners - // Skew the input space to determine which simplex cell we're in - var F3 = 1.0 / 3.0 - var s = (xin + yin + zin) * F3 // Very nice and simple skew factor for 3D - var i = Math.floor(xin + s) - var j = Math.floor(yin + s) - var k = Math.floor(zin + s) - var G3 = 1.0 / 6.0 // Very nice and simple unskew factor, too - var t = (i + j + k) * G3 - var X0 = i - t // Unskew the cell origin back to (x,y,z) space - var Y0 = j - t - var Z0 = k - t - var x0 = xin - X0 // The x,y,z distances from the cell origin - var y0 = yin - Y0 - var z0 = zin - Z0 - // For the 3D case, the simplex shape is a slightly irregular tetrahedron. - // Determine which simplex we are in. - var i1, j1, k1 // Offsets for second corner of simplex in (i,j,k) coords - var i2, j2, k2 // Offsets for third corner of simplex in (i,j,k) coords - if (x0 >= y0) { - if (y0 >= z0) { - i1 = 1 - j1 = 0 - k1 = 0 - i2 = 1 - j2 = 1 - k2 = 0 - - // X Y Z order - } else if (x0 >= z0) { - i1 = 1 - j1 = 0 - k1 = 0 - i2 = 1 - j2 = 0 - k2 = 1 - - // X Z Y order - } else { - i1 = 0 - j1 = 0 - k1 = 1 - i2 = 1 - j2 = 0 - k2 = 1 - } // Z X Y order - } else { - // x0 y0 ? 32 : 0 - var c2 = x0 > z0 ? 16 : 0 - var c3 = y0 > z0 ? 8 : 0 - var c4 = x0 > w0 ? 4 : 0 - var c5 = y0 > w0 ? 2 : 0 - var c6 = z0 > w0 ? 1 : 0 - var c = c1 + c2 + c3 + c4 + c5 + c6 - var i1, j1, k1, l1 // The integer offsets for the second simplex corner - var i2, j2, k2, l2 // The integer offsets for the third simplex corner - var i3, j3, k3, l3 // The integer offsets for the fourth simplex corner - // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. - // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0 - j1 = simplex[c][1] >= 3 ? 1 : 0 - k1 = simplex[c][2] >= 3 ? 1 : 0 - l1 = simplex[c][3] >= 3 ? 1 : 0 - // The number 2 in the "simplex" array is at the second largest coordinate. - i2 = simplex[c][0] >= 2 ? 1 : 0 - j2 = simplex[c][1] >= 2 ? 1 : 0 - k2 = simplex[c][2] >= 2 ? 1 : 0 - l2 = simplex[c][3] >= 2 ? 1 : 0 - // The number 1 in the "simplex" array is at the second smallest coordinate. - i3 = simplex[c][0] >= 1 ? 1 : 0 - j3 = simplex[c][1] >= 1 ? 1 : 0 - k3 = simplex[c][2] >= 1 ? 1 : 0 - l3 = simplex[c][3] >= 1 ? 1 : 0 - // The fifth corner has all coordinate offsets = 1, so no need to look that up. - var x1 = x0 - i1 + G4 // Offsets for second corner in (x,y,z,w) coords - var y1 = y0 - j1 + G4 - var z1 = z0 - k1 + G4 - var w1 = w0 - l1 + G4 - var x2 = x0 - i2 + 2.0 * G4 // Offsets for third corner in (x,y,z,w) coords - var y2 = y0 - j2 + 2.0 * G4 - var z2 = z0 - k2 + 2.0 * G4 - var w2 = w0 - l2 + 2.0 * G4 - var x3 = x0 - i3 + 3.0 * G4 // Offsets for fourth corner in (x,y,z,w) coords - var y3 = y0 - j3 + 3.0 * G4 - var z3 = z0 - k3 + 3.0 * G4 - var w3 = w0 - l3 + 3.0 * G4 - var x4 = x0 - 1.0 + 4.0 * G4 // Offsets for last corner in (x,y,z,w) coords - var y4 = y0 - 1.0 + 4.0 * G4 - var z4 = z0 - 1.0 + 4.0 * G4 - var w4 = w0 - 1.0 + 4.0 * G4 - // Work out the hashed gradient indices of the five simplex corners - var ii = i & 255 - var jj = j & 255 - var kk = k & 255 - var ll = l & 255 - var gi0 = perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32 - var gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32 - var gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32 - var gi3 = perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32 - var gi4 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32 - // Calculate the contribution from the five corners - var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0 - if (t0 < 0) n0 = 0.0 - else { - t0 *= t0 - n0 = t0 * t0 * this.dot4(grad4[gi0], x0, y0, z0, w0) - } - - var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1 - if (t1 < 0) n1 = 0.0 - else { - t1 *= t1 - n1 = t1 * t1 * this.dot4(grad4[gi1], x1, y1, z1, w1) - } - - var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2 - if (t2 < 0) n2 = 0.0 - else { - t2 *= t2 - n2 = t2 * t2 * this.dot4(grad4[gi2], x2, y2, z2, w2) - } - - var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3 - if (t3 < 0) n3 = 0.0 - else { - t3 *= t3 - n3 = t3 * t3 * this.dot4(grad4[gi3], x3, y3, z3, w3) - } - - var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4 - if (t4 < 0) n4 = 0.0 - else { - t4 *= t4 - n4 = t4 * t4 * this.dot4(grad4[gi4], x4, y4, z4, w4) - } - - // Sum up and scale the result to cover the range [-1,1] - return 27.0 * (n0 + n1 + n2 + n3 + n4) -} - -export { SimplexNoise } diff --git a/src/math/SimplexNoise.ts b/src/math/SimplexNoise.ts new file mode 100644 index 00000000..37d63cf5 --- /dev/null +++ b/src/math/SimplexNoise.ts @@ -0,0 +1,534 @@ +// Ported from Stefan Gustavson's java implementation +// http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf +// Read Stefan's excellent paper for details on how this code works. +// +// Sean McCullough banksean@gmail.com +// + +interface NumberGenerator { + random: () => number +} + +// Added 4D noise +class SimplexNoise { + grad3 = [ + [1, 1, 0], + [-1, 1, 0], + [1, -1, 0], + [-1, -1, 0], + [1, 0, 1], + [-1, 0, 1], + [1, 0, -1], + [-1, 0, -1], + [0, 1, 1], + [0, -1, 1], + [0, 1, -1], + [0, -1, -1], + ] + + grad4 = [ + [0, 1, 1, 1], + [0, 1, 1, -1], + [0, 1, -1, 1], + [0, 1, -1, -1], + [0, -1, 1, 1], + [0, -1, 1, -1], + [0, -1, -1, 1], + [0, -1, -1, -1], + [1, 0, 1, 1], + [1, 0, 1, -1], + [1, 0, -1, 1], + [1, 0, -1, -1], + [-1, 0, 1, 1], + [-1, 0, 1, -1], + [-1, 0, -1, 1], + [-1, 0, -1, -1], + [1, 1, 0, 1], + [1, 1, 0, -1], + [1, -1, 0, 1], + [1, -1, 0, -1], + [-1, 1, 0, 1], + [-1, 1, 0, -1], + [-1, -1, 0, 1], + [-1, -1, 0, -1], + [1, 1, 1, 0], + [1, 1, -1, 0], + [1, -1, 1, 0], + [1, -1, -1, 0], + [-1, 1, 1, 0], + [-1, 1, -1, 0], + [-1, -1, 1, 0], + [-1, -1, -1, 0], + ] + + p: number[] = [] + + // To remove the need for index wrapping, double the permutation table length + perm: number[] = [] + + // A lookup table to traverse the simplex around a given point in 4D. + // Details can be found where this table is used, in the 4D noise method. + simplex = [ + [0, 1, 2, 3], + [0, 1, 3, 2], + [0, 0, 0, 0], + [0, 2, 3, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 2, 3, 0], + [0, 2, 1, 3], + [0, 0, 0, 0], + [0, 3, 1, 2], + [0, 3, 2, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 3, 2, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 2, 0, 3], + [0, 0, 0, 0], + [1, 3, 0, 2], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [2, 3, 0, 1], + [2, 3, 1, 0], + [1, 0, 2, 3], + [1, 0, 3, 2], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [2, 0, 3, 1], + [0, 0, 0, 0], + [2, 1, 3, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [2, 0, 1, 3], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [3, 0, 1, 2], + [3, 0, 2, 1], + [0, 0, 0, 0], + [3, 1, 2, 0], + [2, 1, 0, 3], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [3, 1, 0, 2], + [0, 0, 0, 0], + [3, 2, 0, 1], + [3, 2, 1, 0], + ] + + /** + * You can pass in a random number generator object if you like. + * It is assumed to have a random() method. + */ + constructor(r: NumberGenerator = Math) { + for (let i = 0; i < 256; i++) { + this.p[i] = Math.floor(r.random() * 256) + } + + for (let i = 0; i < 512; i++) { + this.perm[i] = this.p[i & 255] + } + } + + dot = (g: number[], x: number, y: number): number => { + return g[0] * x + g[1] * y + } + + dot3 = (g: number[], x: number, y: number, z: number): number => { + return g[0] * x + g[1] * y + g[2] * z + } + + dot4 = (g: number[], x: number, y: number, z: number, w: number) => { + return g[0] * x + g[1] * y + g[2] * z + g[3] * w + } + + noise = (xin: number, yin: number) => { + let n0 + let n1 + let n2 // Noise contributions from the three corners + // Skew the input space to determine which simplex cell we're in + const F2 = 0.5 * (Math.sqrt(3.0) - 1.0) + const s = (xin + yin) * F2 // Hairy factor for 2D + const i = Math.floor(xin + s) + const j = Math.floor(yin + s) + const G2 = (3.0 - Math.sqrt(3.0)) / 6.0 + const t = (i + j) * G2 + const X0 = i - t // Unskew the cell origin back to (x,y) space + const Y0 = j - t + const x0 = xin - X0 // The x,y distances from the cell origin + const y0 = yin - Y0 + // For the 2D case, the simplex shape is an equilateral triangle. + // Determine which simplex we are in. + // upper triangle, YX order: (0,0)->(0,1)->(1,1) + let i1 = 0 + // Offsets for second (middle) corner of simplex in (i,j) coords + let j1 = 1 + if (x0 > y0) { + i1 = 1 + j1 = 0 + } + + // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + // c = (3-sqrt(3))/6 + const x1 = x0 - i1 + G2 // Offsets for middle corner in (x,y) unskewed coords + const y1 = y0 - j1 + G2 + const x2 = x0 - 1.0 + 2.0 * G2 // Offsets for last corner in (x,y) unskewed coords + const y2 = y0 - 1.0 + 2.0 * G2 + // Work out the hashed gradient indices of the three simplex corners + const ii = i & 255 + const jj = j & 255 + const gi0 = this.perm[ii + this.perm[jj]] % 12 + const gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12 + const gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12 + // Calculate the contribution from the three corners + let t0 = 0.5 - x0 * x0 - y0 * y0 + if (t0 < 0) { + n0 = 0.0 + } else { + t0 *= t0 + n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0) // (x,y) of grad3 used for 2D gradient + } + + let t1 = 0.5 - x1 * x1 - y1 * y1 + if (t1 < 0) { + n1 = 0.0 + } else { + t1 *= t1 + n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1) + } + + let t2 = 0.5 - x2 * x2 - y2 * y2 + if (t2 < 0) { + n2 = 0.0 + } else { + t2 *= t2 + n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2) + } + + // Add contributions from each corner to get the final noise value. + // The result is scaled to return values in the interval [-1,1]. + return 70.0 * (n0 + n1 + n2) + } + + // 3D simplex noise + noise3d = (xin: number, yin: number, zin: number): number => { + // Noise contributions from the four corners + let n0 + let n1 + let n2 + let n3 + // Skew the input space to determine which simplex cell we're in + const F3 = 1.0 / 3.0 + const s = (xin + yin + zin) * F3 // Very nice and simple skew factor for 3D + const i = Math.floor(xin + s) + const j = Math.floor(yin + s) + const k = Math.floor(zin + s) + const G3 = 1.0 / 6.0 // Very nice and simple unskew factor, too + const t = (i + j + k) * G3 + const X0 = i - t // Unskew the cell origin back to (x,y,z) space + const Y0 = j - t + const Z0 = k - t + const x0 = xin - X0 // The x,y,z distances from the cell origin + const y0 = yin - Y0 + const z0 = zin - Z0 + // For the 3D case, the simplex shape is a slightly irregular tetrahedron. + // Determine which simplex we are in. + let i1 + let j1 + let k1 // Offsets for second corner of simplex in (i,j,k) coords + let i2 + let j2 + let k2 // Offsets for third corner of simplex in (i,j,k) coords + if (x0 >= y0) { + if (y0 >= z0) { + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 1 + k2 = 0 + + // X Y Z order + } else if (x0 >= z0) { + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 0 + k2 = 1 + + // X Z Y order + } else { + i1 = 0 + j1 = 0 + k1 = 1 + i2 = 1 + j2 = 0 + k2 = 1 + } // Z X Y order + } else { + // x0 { + // For faster and easier lookups + const grad4 = this.grad4 + const simplex = this.simplex + const perm = this.perm + + // The skewing and unskewing factors are hairy again for the 4D case + const F4 = (Math.sqrt(5.0) - 1.0) / 4.0 + const G4 = (5.0 - Math.sqrt(5.0)) / 20.0 + let n0 + let n1 + let n2 + let n3 + let n4 // Noise contributions from the five corners + // Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in + const s = (x + y + z + w) * F4 // Factor for 4D skewing + const i = Math.floor(x + s) + const j = Math.floor(y + s) + const k = Math.floor(z + s) + const l = Math.floor(w + s) + const t = (i + j + k + l) * G4 // Factor for 4D unskewing + const X0 = i - t // Unskew the cell origin back to (x,y,z,w) space + const Y0 = j - t + const Z0 = k - t + const W0 = l - t + const x0 = x - X0 // The x,y,z,w distances from the cell origin + const y0 = y - Y0 + const z0 = z - Z0 + const w0 = w - W0 + + // For the 4D case, the simplex is a 4D shape I won't even try to describe. + // To find out which of the 24 possible simplices we're in, we need to + // determine the magnitude ordering of x0, y0, z0 and w0. + // The method below is a good way of finding the ordering of x,y,z,w and + // then find the correct traversal order for the simplex we’re in. + // First, six pair-wise comparisons are performed between each possible pair + // of the four coordinates, and the results are used to add up binary bits + // for an integer index. + const c1 = x0 > y0 ? 32 : 0 + const c2 = x0 > z0 ? 16 : 0 + const c3 = y0 > z0 ? 8 : 0 + const c4 = x0 > w0 ? 4 : 0 + const c5 = y0 > w0 ? 2 : 0 + const c6 = z0 > w0 ? 1 : 0 + const c = c1 + c2 + c3 + c4 + c5 + c6 + // The integer offsets for the second simplex corner + let i1 + let j1 + let k1 + let l1 + + // The integer offsets for the third simplex corner + let i2 + let j2 + let k2 + let l2 + + // The integer offsets for the fourth simplex corner + let i3 + let j3 + let k3 + let l3 + // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. + // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0 + j1 = simplex[c][1] >= 3 ? 1 : 0 + k1 = simplex[c][2] >= 3 ? 1 : 0 + l1 = simplex[c][3] >= 3 ? 1 : 0 + // The number 2 in the "simplex" array is at the second largest coordinate. + i2 = simplex[c][0] >= 2 ? 1 : 0 + j2 = simplex[c][1] >= 2 ? 1 : 0 + k2 = simplex[c][2] >= 2 ? 1 : 0 + l2 = simplex[c][3] >= 2 ? 1 : 0 + // The number 1 in the "simplex" array is at the second smallest coordinate. + i3 = simplex[c][0] >= 1 ? 1 : 0 + j3 = simplex[c][1] >= 1 ? 1 : 0 + k3 = simplex[c][2] >= 1 ? 1 : 0 + l3 = simplex[c][3] >= 1 ? 1 : 0 + // The fifth corner has all coordinate offsets = 1, so no need to look that up. + const x1 = x0 - i1 + G4 // Offsets for second corner in (x,y,z,w) coords + const y1 = y0 - j1 + G4 + const z1 = z0 - k1 + G4 + const w1 = w0 - l1 + G4 + const x2 = x0 - i2 + 2.0 * G4 // Offsets for third corner in (x,y,z,w) coords + const y2 = y0 - j2 + 2.0 * G4 + const z2 = z0 - k2 + 2.0 * G4 + const w2 = w0 - l2 + 2.0 * G4 + const x3 = x0 - i3 + 3.0 * G4 // Offsets for fourth corner in (x,y,z,w) coords + const y3 = y0 - j3 + 3.0 * G4 + const z3 = z0 - k3 + 3.0 * G4 + const w3 = w0 - l3 + 3.0 * G4 + const x4 = x0 - 1.0 + 4.0 * G4 // Offsets for last corner in (x,y,z,w) coords + const y4 = y0 - 1.0 + 4.0 * G4 + const z4 = z0 - 1.0 + 4.0 * G4 + const w4 = w0 - 1.0 + 4.0 * G4 + // Work out the hashed gradient indices of the five simplex corners + const ii = i & 255 + const jj = j & 255 + const kk = k & 255 + const ll = l & 255 + const gi0 = perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32 + const gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32 + const gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32 + const gi3 = perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32 + const gi4 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32 + // Calculate the contribution from the five corners + let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0 + if (t0 < 0) { + n0 = 0.0 + } else { + t0 *= t0 + n0 = t0 * t0 * this.dot4(grad4[gi0], x0, y0, z0, w0) + } + + let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1 + if (t1 < 0) { + n1 = 0.0 + } else { + t1 *= t1 + n1 = t1 * t1 * this.dot4(grad4[gi1], x1, y1, z1, w1) + } + + let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2 + if (t2 < 0) { + n2 = 0.0 + } else { + t2 *= t2 + n2 = t2 * t2 * this.dot4(grad4[gi2], x2, y2, z2, w2) + } + + let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3 + if (t3 < 0) { + n3 = 0.0 + } else { + t3 *= t3 + n3 = t3 * t3 * this.dot4(grad4[gi3], x3, y3, z3, w3) + } + + let t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4 + if (t4 < 0) { + n4 = 0.0 + } else { + t4 *= t4 + n4 = t4 * t4 * this.dot4(grad4[gi4], x4, y4, z4, w4) + } + + // Sum up and scale the result to cover the range [-1,1] + return 27.0 * (n0 + n1 + n2 + n3 + n4) + } +} + +export { SimplexNoise, NumberGenerator } diff --git a/src/misc/WebGL.js b/src/misc/WebGL.ts similarity index 83% rename from src/misc/WebGL.js rename to src/misc/WebGL.ts index e60ce365..d333156a 100644 --- a/src/misc/WebGL.js +++ b/src/misc/WebGL.ts @@ -1,7 +1,7 @@ -var WEBGL = { +export const WEBGL = { isWebGLAvailable: function () { try { - var canvas = document.createElement('canvas') + const canvas = document.createElement('canvas') return !!(window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))) } catch (e) { return false @@ -10,7 +10,7 @@ var WEBGL = { isWebGL2Available: function () { try { - var canvas = document.createElement('canvas') + const canvas = document.createElement('canvas') return !!(window.WebGL2RenderingContext && canvas.getContext('webgl2')) } catch (e) { return false @@ -25,21 +25,18 @@ var WEBGL = { return this.getErrorMessage(2) }, - getErrorMessage: function (version) { - var names = { + getErrorMessage: function (version: 1 | 2) { + const names = { 1: 'WebGL', 2: 'WebGL 2', } - var contexts = { + const contexts = { 1: window.WebGLRenderingContext, 2: window.WebGL2RenderingContext, } - var message = - 'Your $0 does not seem to support $1' - - var element = document.createElement('div') + const element = document.createElement('div') element.id = 'webglmessage' element.style.fontFamily = 'monospace' element.style.fontSize = '13px' @@ -51,6 +48,9 @@ var WEBGL = { element.style.width = '400px' element.style.margin = '5em auto 0' + let message = + 'Your $0 does not seem to support $1' + if (contexts[version]) { message = message.replace('$0', 'graphics card') } else { @@ -64,5 +64,3 @@ var WEBGL = { return element }, } - -export { WEBGL } diff --git a/src/modifiers/CurveModifier.js b/src/modifiers/CurveModifier.ts similarity index 64% rename from src/modifiers/CurveModifier.js rename to src/modifiers/CurveModifier.ts index aaccbd1f..7b872308 100644 --- a/src/modifiers/CurveModifier.js +++ b/src/modifiers/CurveModifier.ts @@ -13,19 +13,25 @@ import { NearestFilter, DynamicDrawUsage, Matrix4, + Material, + Shader, + Curve, + BufferGeometry, } from 'three' +import { TUniform } from 'types/shared' + /** * Make a new DataTexture to store the descriptions of the curves. * * @param { number } numberOfCurves the number of curves needed to be described by this texture. */ -export function initSplineTexture(numberOfCurves = 1) { +export const initSplineTexture = (numberOfCurves = 1): DataTexture => { const dataArray = new Float32Array(TEXTURE_WIDTH * TEXTURE_HEIGHT * numberOfCurves * BITS) const dataTexture = new DataTexture(dataArray, TEXTURE_WIDTH, TEXTURE_HEIGHT * numberOfCurves, RGBFormat, FloatType) dataTexture.wrapS = RepeatWrapping - dataTexture.wrapY = RepeatWrapping + dataTexture.wrapT = RepeatWrapping dataTexture.magFilter = NearestFilter dataTexture.needsUpdate = true @@ -39,7 +45,11 @@ export function initSplineTexture(numberOfCurves = 1) { * @param { Curve } splineCurve The curve to describe * @param { number } offset Which curve slot to write to */ -export function updateSplineTexture(texture, splineCurve, offset = 0) { +export const updateSplineTexture = >( + texture: DataTexture, + splineCurve: TCurve, + offset = 0, +): void => { const numberOfPoints = Math.floor(TEXTURE_WIDTH * (TEXTURE_HEIGHT / 4)) splineCurve.arcLengthDivisions = numberOfPoints / 2 splineCurve.updateArcLengths() @@ -63,7 +73,7 @@ export function updateSplineTexture(texture, splineCurve, offset = 0) { texture.needsUpdate = true } -function setTextureValue(texture, index, x, y, z, o) { +const setTextureValue = (texture: DataTexture, index: number, x: number, y: number, z: number, o: number): void => { const image = texture.image const { data } = image const i = BITS * TEXTURE_WIDTH * o // Row Offset @@ -72,12 +82,21 @@ function setTextureValue(texture, index, x, y, z, o) { data[index * BITS + i + 2] = z } +export interface CurveModifierUniforms { + spineTexture: TUniform + pathOffset: TUniform + pathSegment: TUniform + spineOffset: TUniform + spineLength: TUniform + flow: TUniform +} + /** * Create a new set of uniforms for describing the curve modifier * * @param { DataTexture } Texture which holds the curve description */ -export function getUniforms(splineTexture) { +export function getUniforms(splineTexture: DataTexture): CurveModifierUniforms { const uniforms = { spineTexture: { value: splineTexture }, pathOffset: { type: 'f', value: 0 }, // time of path curve @@ -89,17 +108,25 @@ export function getUniforms(splineTexture) { return uniforms } -export function modifyShader(material, uniforms, numberOfCurves = 1) { +export type ModifiedMaterial = TMaterial & { + __ok: boolean +} + +export function modifyShader( + material: ModifiedMaterial, + uniforms: CurveModifierUniforms, + numberOfCurves = 1, +) { if (material.__ok) return material.__ok = true - material.onBeforeCompile = (shader) => { + material.onBeforeCompile = (shader: Shader & { __modified: boolean }) => { if (shader.__modified) return shader.__modified = true Object.assign(shader.uniforms, uniforms) - const vertexShader = ` + const vertexShader = /* glsl */ ` uniform sampler2D spineTexture; uniform float pathOffset; uniform float pathSegment; @@ -124,48 +151,48 @@ export function modifyShader(material, uniforms, numberOfCurves = 1) { // shader override .replace( /void\s*main\s*\(\)\s*\{/, - ` -void main() { -#include - -vec4 worldPos = modelMatrix * vec4(position, 1.); - -bool bend = flow > 0; -float xWeight = bend ? 0. : 1.; - -#ifdef USE_INSTANCING -float pathOffsetFromInstanceMatrix = instanceMatrix[3][2]; -float spineLengthFromInstanceMatrix = instanceMatrix[3][0]; -float spinePortion = bend ? (worldPos.x + spineOffset) / spineLengthFromInstanceMatrix : 0.; -float mt = (spinePortion * pathSegment + pathOffset + pathOffsetFromInstanceMatrix)*textureStacks; -#else -float spinePortion = bend ? (worldPos.x + spineOffset) / spineLength : 0.; -float mt = (spinePortion * pathSegment + pathOffset)*textureStacks; -#endif - -mt = mod(mt, textureStacks); -float rowOffset = floor(mt); - -#ifdef USE_INSTANCING -rowOffset += instanceMatrix[3][1] * ${TEXTURE_HEIGHT}.; -#endif - -vec3 spinePos = texture2D(spineTexture, vec2(mt, (0. + rowOffset + 0.5) / textureLayers)).xyz; -vec3 a = texture2D(spineTexture, vec2(mt, (1. + rowOffset + 0.5) / textureLayers)).xyz; -vec3 b = texture2D(spineTexture, vec2(mt, (2. + rowOffset + 0.5) / textureLayers)).xyz; -vec3 c = texture2D(spineTexture, vec2(mt, (3. + rowOffset + 0.5) / textureLayers)).xyz; -mat3 basis = mat3(a, b, c); - -vec3 transformed = basis - * vec3(worldPos.x * xWeight, worldPos.y * 1., worldPos.z * 1.) - + spinePos; - -vec3 transformedNormal = normalMatrix * (basis * objectNormal); + /* glsl */ ` + void main() { + #include + + vec4 worldPos = modelMatrix * vec4(position, 1.); + + bool bend = flow > 0; + float xWeight = bend ? 0. : 1.; + + #ifdef USE_INSTANCING + float pathOffsetFromInstanceMatrix = instanceMatrix[3][2]; + float spineLengthFromInstanceMatrix = instanceMatrix[3][0]; + float spinePortion = bend ? (worldPos.x + spineOffset) / spineLengthFromInstanceMatrix : 0.; + float mt = (spinePortion * pathSegment + pathOffset + pathOffsetFromInstanceMatrix)*textureStacks; + #else + float spinePortion = bend ? (worldPos.x + spineOffset) / spineLength : 0.; + float mt = (spinePortion * pathSegment + pathOffset)*textureStacks; + #endif + + mt = mod(mt, textureStacks); + float rowOffset = floor(mt); + + #ifdef USE_INSTANCING + rowOffset += instanceMatrix[3][1] * ${TEXTURE_HEIGHT}.; + #endif + + vec3 spinePos = texture2D(spineTexture, vec2(mt, (0. + rowOffset + 0.5) / textureLayers)).xyz; + vec3 a = texture2D(spineTexture, vec2(mt, (1. + rowOffset + 0.5) / textureLayers)).xyz; + vec3 b = texture2D(spineTexture, vec2(mt, (2. + rowOffset + 0.5) / textureLayers)).xyz; + vec3 c = texture2D(spineTexture, vec2(mt, (3. + rowOffset + 0.5) / textureLayers)).xyz; + mat3 basis = mat3(a, b, c); + + vec3 transformed = basis + * vec3(worldPos.x * xWeight, worldPos.y * 1., worldPos.z * 1.) + + spinePos; + + vec3 transformedNormal = normalMatrix * (basis * objectNormal); `, ) .replace( '#include ', - `vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 ); + /* glsl */ `vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 ); gl_Position = projectionMatrix * mvPosition;`, ) @@ -176,16 +203,24 @@ vec3 transformedNormal = normalMatrix * (basis * objectNormal); /** * A helper class for making meshes bend aroudn curves */ -export class Flow { +export class Flow { + curveArray: Curve[] + curveLengthArray: number[] + + object3D: TMesh + splineTexure: DataTexture + uniforms: CurveModifierUniforms + /** * @param {Mesh} mesh The mesh to clone and modify to bend around the curve * @param {number} numberOfCurves The amount of space that should preallocated for additional curves */ - constructor(mesh, numberOfCurves = 1) { - const obj3D = mesh.clone() + constructor(mesh: TMesh, numberOfCurves = 1) { + const obj3D = mesh.clone() as TMesh const splineTexure = initSplineTexture(numberOfCurves) const uniforms = getUniforms(splineTexure) - obj3D.traverse(function (child) { + + obj3D.traverse((child) => { if (child instanceof Mesh || child instanceof InstancedMesh) { child.material = child.material.clone() modifyShader(child.material, uniforms, numberOfCurves) @@ -200,7 +235,7 @@ export class Flow { this.uniforms = uniforms } - updateCurve(index, curve) { + updateCurve>(index: number, curve: TCurve): void { if (index >= this.curveArray.length) throw Error('Index out of range for Flow') const curveLength = curve.getLength() this.uniforms.spineLength.value = curveLength @@ -209,7 +244,7 @@ export class Flow { updateSplineTexture(this.splineTexure, curve, index) } - moveAlongCurve(amount) { + moveAlongCurve(amount: number): void { this.uniforms.pathOffset.value += amount } } @@ -218,7 +253,13 @@ const matrix = new Matrix4() /** * A helper class for creating instanced versions of flow, where the instances are placed on the curve. */ -export class InstancedFlow extends Flow { +export class InstancedFlow< + TGeometry extends BufferGeometry = BufferGeometry, + TMaterial extends Material = Material +> extends Flow> { + offsets: number[] + whichCurve: number[] + /** * * @param {number} count The number of instanced elements @@ -226,7 +267,7 @@ export class InstancedFlow extends Flow { * @param {Geometry} geometry The geometry to use for the instanced mesh * @param {Material} material The material to use for the instanced mesh */ - constructor(count, curveCount, geometry, material) { + constructor(count: number, curveCount: number, geometry: TGeometry, material: TMaterial) { const mesh = new InstancedMesh(geometry, material, count) mesh.instanceMatrix.setUsage(DynamicDrawUsage) super(mesh, curveCount) @@ -241,7 +282,7 @@ export class InstancedFlow extends Flow { * * @param {number} index of the instanced element to update */ - writeChanges(index) { + writeChanges(index: number): void { matrix.makeTranslation(this.curveLengthArray[this.whichCurve[index]], this.whichCurve[index], this.offsets[index]) this.object3D.setMatrixAt(index, matrix) this.object3D.instanceMatrix.needsUpdate = true @@ -253,7 +294,7 @@ export class InstancedFlow extends Flow { * @param {number} index Which element to update * @param {number} offset Move by how much */ - moveIndividualAlongCurve(index, offset) { + moveIndividualAlongCurve(index: number, offset: number): void { this.offsets[index] += offset this.writeChanges(index) } @@ -264,7 +305,7 @@ export class InstancedFlow extends Flow { * @param {number} index the index of the instanced element to update * @param {number} curveNo the index of the curve it should use */ - setCurve(index, curveNo) { + setCurve(index: number, curveNo: number): void { if (isNaN(curveNo)) throw Error('curve index being set is Not a Number (NaN)') this.whichCurve[index] = curveNo this.writeChanges(index) diff --git a/src/postprocessing/SSAOPass.js b/src/postprocessing/SSAOPass.js index d79286be..87430ba2 100644 --- a/src/postprocessing/SSAOPass.js +++ b/src/postprocessing/SSAOPass.js @@ -23,9 +23,7 @@ import { } from 'three' import { Pass } from '../postprocessing/Pass' import { SimplexNoise } from '../math/SimplexNoise' -import { SSAOShader } from '../shaders/SSAOShader' -import { SSAOBlurShader } from '../shaders/SSAOShader' -import { SSAODepthShader } from '../shaders/SSAOShader' +import { SSAOBlurShader, SSAODepthShader, SSAOShader } from '../shaders/SSAOShader' import { CopyShader } from '../shaders/CopyShader' var SSAOPass = function (scene, camera, width, height) { diff --git a/src/shaders/GammaCorrectionShader.js b/src/shaders/GammaCorrectionShader.ts similarity index 66% rename from src/shaders/GammaCorrectionShader.js rename to src/shaders/GammaCorrectionShader.ts index 6bcc3621..f782829f 100644 --- a/src/shaders/GammaCorrectionShader.js +++ b/src/shaders/GammaCorrectionShader.ts @@ -3,7 +3,16 @@ * http://en.wikipedia.org/wiki/gamma_correction */ -var GammaCorrectionShader = { +import { Texture } from 'three' +import { TUniform, GenericShader } from 'types/shared' + +export interface GammaCorrectionShaderUniforms { + tDiffuse: TUniform +} + +export type GammaCorrectionShaderImpl = GenericShader + +const GammaCorrectionShader: GammaCorrectionShaderImpl = { uniforms: { tDiffuse: { value: null }, }, diff --git a/src/shaders/HorizontalBlurShader.js b/src/shaders/HorizontalBlurShader.js deleted file mode 100644 index 97cfb1af..00000000 --- a/src/shaders/HorizontalBlurShader.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Two pass Gaussian blur filter (horizontal and vertical blur shaders) - * - described in http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/ - * and used in http://www.cake23.de/traveling-wavefronts-lit-up.html - * - * - 9 samples per pass - * - standard deviation 2.7 - * - "h" and "v" parameters should be set to "1 / width" and "1 / height" - */ - -var HorizontalBlurShader = { - uniforms: { - tDiffuse: { value: null }, - h: { value: 1.0 / 512.0 }, - }, - - vertexShader: [ - 'varying vec2 vUv;', - - 'void main() {', - - ' vUv = uv;', - ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', - - '}', - ].join('\n'), - - fragmentShader: [ - 'uniform sampler2D tDiffuse;', - 'uniform float h;', - - 'varying vec2 vUv;', - - 'void main() {', - - ' vec4 sum = vec4( 0.0 );', - - ' sum += texture2D( tDiffuse, vec2( vUv.x - 4.0 * h, vUv.y ) ) * 0.051;', - ' sum += texture2D( tDiffuse, vec2( vUv.x - 3.0 * h, vUv.y ) ) * 0.0918;', - ' sum += texture2D( tDiffuse, vec2( vUv.x - 2.0 * h, vUv.y ) ) * 0.12245;', - ' sum += texture2D( tDiffuse, vec2( vUv.x - 1.0 * h, vUv.y ) ) * 0.1531;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633;', - ' sum += texture2D( tDiffuse, vec2( vUv.x + 1.0 * h, vUv.y ) ) * 0.1531;', - ' sum += texture2D( tDiffuse, vec2( vUv.x + 2.0 * h, vUv.y ) ) * 0.12245;', - ' sum += texture2D( tDiffuse, vec2( vUv.x + 3.0 * h, vUv.y ) ) * 0.0918;', - ' sum += texture2D( tDiffuse, vec2( vUv.x + 4.0 * h, vUv.y ) ) * 0.051;', - - ' gl_FragColor = sum;', - - '}', - ].join('\n'), -} - -export { HorizontalBlurShader } diff --git a/src/shaders/HorizontalBlurShader.ts b/src/shaders/HorizontalBlurShader.ts new file mode 100644 index 00000000..a0d0fa95 --- /dev/null +++ b/src/shaders/HorizontalBlurShader.ts @@ -0,0 +1,62 @@ +/** + * Two pass Gaussian blur filter (horizontal and vertical blur shaders) + * - described in http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/ + * and used in http://www.cake23.de/traveling-wavefronts-lit-up.html + * + * - 9 samples per pass + * - standard deviation 2.7 + * - "h" and "v" parameters should be set to "1 / width" and "1 / height" + */ + +import { Texture } from 'three' +import { TUniform, GenericShader } from 'types/shared' + +export interface HorizontalBlurShaderUniforms { + tDiffuse: TUniform + h: TUniform +} + +export type HorizontalBlurShaderImpl = GenericShader + +const HorizontalBlurShader: HorizontalBlurShaderImpl = { + uniforms: { + tDiffuse: { value: null }, + h: { value: 1.0 / 512.0 }, + }, + vertexShader: /* glsl */ ` + varying vec2 vUv; + + void main() { + + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + } + `, + fragmentShader: /* glsl */ ` + uniform sampler2D tDiffuse; + uniform float h; + + varying vec2 vUv; + + void main() { + + vec4 sum = vec4( 0.0 ); + + sum += texture2D( tDiffuse, vec2( vUv.x - 4.0 * h, vUv.y ) ) * 0.051; + sum += texture2D( tDiffuse, vec2( vUv.x - 3.0 * h, vUv.y ) ) * 0.0918; + sum += texture2D( tDiffuse, vec2( vUv.x - 2.0 * h, vUv.y ) ) * 0.12245; + sum += texture2D( tDiffuse, vec2( vUv.x - 1.0 * h, vUv.y ) ) * 0.1531; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633; + sum += texture2D( tDiffuse, vec2( vUv.x + 1.0 * h, vUv.y ) ) * 0.1531; + sum += texture2D( tDiffuse, vec2( vUv.x + 2.0 * h, vUv.y ) ) * 0.12245; + sum += texture2D( tDiffuse, vec2( vUv.x + 3.0 * h, vUv.y ) ) * 0.0918; + sum += texture2D( tDiffuse, vec2( vUv.x + 4.0 * h, vUv.y ) ) * 0.051; + + gl_FragColor = sum; + + } + `, +} + +export { HorizontalBlurShader } diff --git a/src/shaders/VerticalBlurShader.js b/src/shaders/VerticalBlurShader.js deleted file mode 100644 index 8a647056..00000000 --- a/src/shaders/VerticalBlurShader.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Two pass Gaussian blur filter (horizontal and vertical blur shaders) - * - described in http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/ - * and used in http://www.cake23.de/traveling-wavefronts-lit-up.html - * - * - 9 samples per pass - * - standard deviation 2.7 - * - "h" and "v" parameters should be set to "1 / width" and "1 / height" - */ - -var VerticalBlurShader = { - uniforms: { - tDiffuse: { value: null }, - v: { value: 1.0 / 512.0 }, - }, - - vertexShader: [ - 'varying vec2 vUv;', - - 'void main() {', - - ' vUv = uv;', - ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', - - '}', - ].join('\n'), - - fragmentShader: [ - 'uniform sampler2D tDiffuse;', - 'uniform float v;', - - 'varying vec2 vUv;', - - 'void main() {', - - ' vec4 sum = vec4( 0.0 );', - - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 4.0 * v ) ) * 0.051;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 3.0 * v ) ) * 0.0918;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 2.0 * v ) ) * 0.12245;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 1.0 * v ) ) * 0.1531;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 1.0 * v ) ) * 0.1531;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 2.0 * v ) ) * 0.12245;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 3.0 * v ) ) * 0.0918;', - ' sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 4.0 * v ) ) * 0.051;', - - ' gl_FragColor = sum;', - - '}', - ].join('\n'), -} - -export { VerticalBlurShader } diff --git a/src/shaders/VerticalBlurShader.ts b/src/shaders/VerticalBlurShader.ts new file mode 100644 index 00000000..7169ff72 --- /dev/null +++ b/src/shaders/VerticalBlurShader.ts @@ -0,0 +1,63 @@ +/** + * Two pass Gaussian blur filter (horizontal and vertical blur shaders) + * - described in http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/ + * and used in http://www.cake23.de/traveling-wavefronts-lit-up.html + * + * - 9 samples per pass + * - standard deviation 2.7 + * - "h" and "v" parameters should be set to "1 / width" and "1 / height" + */ + +import { Texture } from 'three' +import { TUniform, GenericShader } from 'types/shared' + +export interface VerticalBlurShaderUniforms { + tDiffuse: TUniform + v: TUniform +} + +export type VerticalBlurShaderImpl = GenericShader + +const VerticalBlurShader: VerticalBlurShaderImpl = { + uniforms: { + tDiffuse: { value: null }, + v: { value: 1.0 / 512.0 }, + }, + vertexShader: /* glsl */ ` + varying vec2 vUv; + + void main() { + + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + } + `, + fragmentShader: /* glsl */ ` + + uniform sampler2D tDiffuse; + uniform float v; + + varying vec2 vUv; + + void main() { + + vec4 sum = vec4( 0.0 ); + + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 4.0 * v ) ) * 0.051; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 3.0 * v ) ) * 0.0918; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 2.0 * v ) ) * 0.12245; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 1.0 * v ) ) * 0.1531; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 1.0 * v ) ) * 0.1531; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 2.0 * v ) ) * 0.12245; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 3.0 * v ) ) * 0.0918; + sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 4.0 * v ) ) * 0.051; + + gl_FragColor = sum; + + } + `, +} + +export { VerticalBlurShader } diff --git a/src/types/shared.ts b/src/types/shared.ts new file mode 100644 index 00000000..c4b4f8fa --- /dev/null +++ b/src/types/shared.ts @@ -0,0 +1,19 @@ +import { Color } from 'three' + +export type TUniform = { + type?: string + value: TValue +} + +export type GenericUniforms = { [key: string]: TUniform } + +export type GenericShader = { + defines?: { + [key: string]: any + } + uniforms?: TShaderUniforms + fragmentShader: string + vertexShader: string +} + +export type ColorOptions = Color | string | number diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a658f710 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "jsx": "react", + "pretty": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "removeComments": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "resolveJsonModule": true, + "noImplicitThis": false, + "baseUrl": "./src", + }, + "include": ["./src", "tests"], + "exclude": ["./node_modules/**/*"] +} diff --git a/yarn.lock b/yarn.lock index a67cfedd..b6f922d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" @@ -230,6 +237,15 @@ "@babel/traverse" "^7.13.0" "@babel/types" "^7.13.0" +"@babel/highlight@^7.10.4": + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" + integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" @@ -239,7 +255,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.11.1", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0": +"@babel/parser@^7.11.1", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.7.0": version "7.13.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== @@ -912,7 +928,7 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.11.0", "@babel/traverse@^7.13.0": +"@babel/traverse@^7.11.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.7.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== @@ -927,7 +943,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.12.17", "@babel/types@^7.13.0", "@babel/types@^7.4.4": +"@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.12.17", "@babel/types@^7.13.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== @@ -943,6 +959,22 @@ dependencies: commander "^2.15.1" +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.20" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -983,6 +1015,14 @@ "@rollup/pluginutils" "^3.1.0" magic-string "^0.25.7" +"@rollup/plugin-typescript@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.2.0.tgz#3e2059cbcae916785d8d7bf07816210c829f817c" + integrity sha512-5DyVsb7L+ehLfNPu/nat8Gq3uJGzku4bMFPt90XahtgiSBf7z9YKPLqFUJKMT41W/mJ98SVGDPOhzikGrr/Lhg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" @@ -1021,6 +1061,16 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/json-schema@^7.0.3": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/node@*": version "14.14.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" @@ -1038,6 +1088,11 @@ dependencies: "@types/node" "*" +"@types/three@^0.125.3": + version "0.125.3" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.125.3.tgz#5d5c29acea66af42a3a32a13a9cc8e88e367d8a3" + integrity sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA== + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -1050,6 +1105,99 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.2.tgz#981b26b4076c62a5a55873fbef3fe98f83360c61" + integrity sha512-uiQQeu9tWl3f1+oK0yoAv9lt/KXO24iafxgQTkIYO/kitruILGx3uH+QtIAHqxFV+yIsdnJH+alel9KuE3J15Q== + dependencies: + "@typescript-eslint/experimental-utils" "4.15.2" + "@typescript-eslint/scope-manager" "4.15.2" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + lodash "^4.17.15" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.2.tgz#5efd12355bd5b535e1831282e6cf465b9a71cf36" + integrity sha512-Fxoshw8+R5X3/Vmqwsjc8nRO/7iTysRtDqx6rlfLZ7HbT8TZhPeQqbPjTyk2RheH3L8afumecTQnUc9EeXxohQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.15.2" + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/typescript-estree" "4.15.2" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/experimental-utils@^2.5.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" + integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.2.tgz#c804474321ef76a3955aec03664808f0d6e7872e" + integrity sha512-SHeF8xbsC6z2FKXsaTb1tBCf0QZsjJ94H6Bo51Y1aVEZ4XAefaw5ZAilMoDPlGghe+qtq7XdTiDlGfVTOmvA+Q== + dependencies: + "@typescript-eslint/scope-manager" "4.15.2" + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/typescript-estree" "4.15.2" + debug "^4.1.1" + +"@typescript-eslint/scope-manager@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.2.tgz#5725bda656995960ae1d004bfd1cd70320f37f4f" + integrity sha512-Zm0tf/MSKuX6aeJmuXexgdVyxT9/oJJhaCkijv0DvJVT3ui4zY6XYd6iwIo/8GEZGy43cd7w1rFMiCLHbRzAPQ== + dependencies: + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/visitor-keys" "4.15.2" + +"@typescript-eslint/types@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.2.tgz#04acf3a2dc8001a88985291744241e732ef22c60" + integrity sha512-r7lW7HFkAarfUylJ2tKndyO9njwSyoy6cpfDKWPX6/ctZA+QyaYscAHXVAfJqtnY6aaTwDYrOhp+ginlbc7HfQ== + +"@typescript-eslint/typescript-estree@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" + integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/typescript-estree@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.2.tgz#c2f7a1e94f3428d229d5ecff3ead6581ee9b62fa" + integrity sha512-cGR8C2g5SPtHTQvAymEODeqx90pJHadWsgTtx6GbnTWKqsg7yp6Eaya9nFzUd4KrKhxdYTTFBiYeTPQaz/l8bw== + dependencies: + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/visitor-keys" "4.15.2" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/visitor-keys@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.2.tgz#3d1c7979ce75bf6acf9691109bd0d6b5706192b9" + integrity sha512-TME1VgSb7wTwgENN5KVj4Nqg25hP8DisXxNBojM4Nn31rYaNDIocNm5cmjOFfh42n7NVERxWrDFoETO/76ePyg== + dependencies: + "@typescript-eslint/types" "4.15.2" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -1215,12 +1363,17 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.1, acorn@^7.2.0: +acorn@^7.1.1, acorn@^7.2.0, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -1243,7 +1396,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.2: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1253,6 +1406,16 @@ ajv@^6.1.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84" + integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -1305,6 +1468,13 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1320,11 +1490,46 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-includes@^3.1.1, array-includes@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" + integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.1.1" + is-string "^1.0.5" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +array.prototype.flat@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" + integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + +array.prototype.flatmap@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" + integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + function-bind "^1.1.1" + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -1363,6 +1568,18 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +babel-eslint@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" + integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" + eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" + babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -1621,7 +1838,7 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -call-bind@^1.0.0: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -1848,6 +2065,11 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1945,7 +2167,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^7.0.0: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1976,14 +2198,14 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -debug@^2.2.0, debug@^2.3.3: +debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.2.0: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -2000,7 +2222,7 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -2056,6 +2278,35 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -2120,7 +2371,7 @@ enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.6: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -2134,13 +2385,42 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.3.1: +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" +es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: + version "1.18.0-next.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" + integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.1" + is-regex "^1.1.1" + object-inspect "^1.9.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.3" + string.prototype.trimstart "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -2163,6 +2443,84 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" +eslint-config-prettier@^6.11.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" + integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw== + dependencies: + get-stdin "^6.0.0" + +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-import@^2.22.0: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + +eslint-plugin-jest@^23.20.0: + version "23.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.20.0.tgz#e1d69c75f639e99d836642453c4e75ed22da4099" + integrity sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw== + dependencies: + "@typescript-eslint/experimental-utils" "^2.5.0" + +eslint-plugin-prettier@^3.1.4: + version "3.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7" + integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-react-hooks@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + +eslint-plugin-react@^7.20.6: + version "7.22.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" + integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== + dependencies: + array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1 || ^3.0.0" + object.entries "^1.1.2" + object.fromentries "^2.0.2" + object.values "^1.1.1" + prop-types "^15.7.2" + resolve "^1.18.1" + string.prototype.matchall "^4.0.2" + eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -2171,12 +2529,96 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -esprima@^4.0.1: +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.0.0, eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + +eslint@^7.7.0: + version "7.20.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.20.0.tgz#db07c4ca4eda2e2316e7aa57ac7fc91ec550bdc7" + integrity sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.3.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + file-entry-cache "^6.0.0" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.20" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.4" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esrecurse@^4.1.0: +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0, esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== @@ -2188,7 +2630,7 @@ estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.2.0: +estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== @@ -2293,7 +2735,12 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.0: +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.0.0, fast-glob@^3.1.1: version "3.2.5" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== @@ -2310,7 +2757,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -2339,6 +2786,13 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" +file-entry-cache@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -2370,6 +2824,13 @@ find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -2392,6 +2853,19 @@ find-versions@^4.0.0: dependencies: semver-regex "^3.1.2" +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -2458,6 +2932,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + gensync@^1.0.0-beta.1: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2468,7 +2947,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -2482,6 +2961,11 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -2502,14 +2986,14 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== dependencies: is-glob "^4.0.1" -glob@^7.0.5, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -2526,6 +3010,25 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +globby@^11.0.1: + version "11.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + glsl-inject-defines@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz#dd1aacc2c17fcb2bd3fc32411c6633d0d7b60fd4" @@ -2745,6 +3248,11 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -2781,7 +3289,17 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= -import-fresh@^3.2.1: +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -2827,6 +3345,15 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -2872,6 +3399,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== + is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -2893,6 +3425,11 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -2952,6 +3489,11 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -2983,6 +3525,14 @@ is-reference@^1.1.2: dependencies: "@types/estree" "*" +is-regex@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" + integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== + dependencies: + call-bind "^1.0.2" + has-symbols "^1.0.1" + is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -2993,6 +3543,18 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3055,6 +3617,14 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3080,6 +3650,16 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -3099,6 +3679,14 @@ json@^10.0.0: resolved "https://registry.yarnpkg.com/json/-/json-10.0.0.tgz#c49a939d4abc7067cc225419d5dd17ab7bf2f66c" integrity sha512-iK7tAZtpoghibjdB1ncCWykeBMmke3JThUe+rnkD4qkZaglOIQ70Pw7r5UJ4lyUT+7gnw7ehmmLUHDuhqzQD+g== +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" + integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== + dependencies: + array-includes "^3.1.2" + object.assign "^4.1.2" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3140,6 +3728,14 @@ levenary@^1.1.1: dependencies: leven "^3.1.0" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -3189,6 +3785,16 @@ listr2@^3.2.2: through "^2.3.8" wrap-ansi "^7.0.0" +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -3203,6 +3809,14 @@ loader-utils@^1.2.3: emojis-list "^3.0.0" json5 "^1.0.1" +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -3218,7 +3832,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash@^4.17.11, lodash@^4.17.19: +lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3240,7 +3854,7 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3254,6 +3868,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + magic-string@^0.25.2, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -3475,6 +4096,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + neo-async@^2.5.0, neo-async@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -3522,6 +4148,16 @@ noms@0.0.0: inherits "^2.0.1" readable-stream "~1.0.31" +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -3555,6 +4191,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" + integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== + object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -3567,7 +4208,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0: +object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -3577,6 +4218,26 @@ object.assign@^4.1.0: has-symbols "^1.0.1" object-keys "^1.1.1" +object.entries@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6" + integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + +object.fromentries@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8" + integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + has "^1.0.3" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -3584,6 +4245,16 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +object.values@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" + integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + has "^1.0.3" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3630,11 +4301,30 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + p-limit@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -3649,6 +4339,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -3670,6 +4367,11 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -3707,6 +4409,13 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -3757,6 +4466,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -3778,11 +4494,23 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -3809,11 +4537,23 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + prettier@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" @@ -3839,11 +4579,25 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -3931,11 +4685,33 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -4021,6 +4797,19 @@ regexp-to-ast@0.5.0: resolved "https://registry.yarnpkg.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24" integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw== +regexp.prototype.flags@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.0.0, regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + regexpu-core@^4.7.1: version "4.7.1" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" @@ -4065,6 +4854,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4080,7 +4874,7 @@ resolve@^0.6.1: resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.6.3.tgz#dd957982e7e736debdf53b58a4dd91754575dd46" integrity sha1-3ZV5gufnNt699TtYpN2RdUV13UY= -resolve@^1.0.0, resolve@^1.1.5, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2, resolve@^1.8.1: +resolve@^1.0.0, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -4274,15 +5068,22 @@ semver-regex@^3.1.2: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^7.2.1, semver@^7.3.2: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" serialize-javascript@^4.0.0: version "4.0.0" @@ -4331,6 +5132,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4433,6 +5243,32 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" + integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -4440,6 +5276,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -4518,6 +5359,35 @@ string.prototype.codepointat@^0.2.1: resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== +string.prototype.matchall@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.4.tgz#608f255e93e072107f5de066f81a2dfb78cf6b29" + integrity sha512-pknFIWVachNcyqRfaQSeu/FUfpvJTe4uskUSZ9Wc1RijsPuzbZ8TyYT8WCNnntCjUEqQ3vUHMAfVj2+wLAisPQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + has-symbols "^1.0.1" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.3.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -4553,11 +5423,21 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4572,6 +5452,16 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +table@^6.0.4: + version "6.0.7" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" + integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== + dependencies: + ajv "^7.0.2" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -4601,6 +5491,11 @@ terser@^4.1.2, terser@^4.7.0: source-map "~0.6.1" source-map-support "~0.5.12" +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + three@^0.126.0: version "0.126.0" resolved "https://registry.yarnpkg.com/three/-/three-0.126.0.tgz#924818341cd4441ef247e3cbf236f6160b90a216" @@ -4681,16 +5576,45 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -tslib@^1.9.0: +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +tsutils@^3.17.1: + version "3.20.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" + integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -4703,11 +5627,21 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@<4.2.0: + version "4.1.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" + integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -4817,6 +5751,19 @@ util@^0.11.0: dependencies: inherits "2.0.3" +v8-compile-cache@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" + integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" @@ -4889,7 +5836,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -4949,6 +5896,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"