| @@ -1,12 +1,12 @@ | |||
| { | |||
| "name": "threepipe", | |||
| "version": "0.0.22", | |||
| "version": "0.0.23", | |||
| "lockfileVersion": 3, | |||
| "requires": true, | |||
| "packages": { | |||
| "": { | |||
| "name": "threepipe", | |||
| "version": "0.0.22", | |||
| "version": "0.0.23", | |||
| "license": "Apache-2.0", | |||
| "dependencies": { | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1020/package.tgz", | |||
| @@ -41,7 +41,7 @@ | |||
| "rollup-plugin-glsl": "^1.3.0", | |||
| "rollup-plugin-license": "^3.0.1", | |||
| "rollup-plugin-postcss": "^4.0.2", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2020/package.tgz", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2021/package.tgz", | |||
| "tslib": "^2.5.0", | |||
| "typedoc": "^0.25.7", | |||
| "typescript": "^5.3.3", | |||
| @@ -10464,9 +10464,9 @@ | |||
| } | |||
| }, | |||
| "node_modules/three": { | |||
| "version": "0.152.2020", | |||
| "resolved": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2020/package.tgz", | |||
| "integrity": "sha512-XpB018kSE/FZG5xmh6XG22lXTS9bParloATQ952tZaY3p7PsdiP3skXR49GYiLjG2Lui46wA18aq5aXJ9oabGA==", | |||
| "version": "0.152.2021", | |||
| "resolved": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2021/package.tgz", | |||
| "integrity": "sha512-va2FQU/ES07GLdflqysT82rh8x+cwx0sCYB8w+JGjDCAcwYDYqvjWafWzWP0UrDR8JM+8HoCLwh+rHtGU5Eoug==", | |||
| "dev": true, | |||
| "license": "MIT" | |||
| }, | |||
| @@ -1,6 +1,6 @@ | |||
| { | |||
| "name": "threepipe", | |||
| "version": "0.0.22", | |||
| "version": "0.0.23", | |||
| "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", | |||
| "main": "dist/index.js", | |||
| "module": "dist/index.mjs", | |||
| @@ -0,0 +1,182 @@ | |||
| import {Euler, EulerOrder, EventDispatcher, MathUtils, Object3D, Quaternion, Vector3} from 'three' | |||
| import {IEvent, now, serialize} from 'ts-browser-helpers' | |||
| import {uiPanelContainer, uiSlider} from 'uiconfig.js' | |||
| import {ICameraControls} from '../../core' | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _zee = new Vector3(0, 0, 1) | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _euler = new Euler() | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _q0 = new Quaternion() | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _q1 = new Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)) // - PI/2 around the x-axis | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _q2 = new Quaternion() // - PI/2 around the x-axis | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _changeEvent: IEvent<'change'> = {type: 'change'} | |||
| const EPS = 0.000001 | |||
| @uiPanelContainer('Device Orientation Controls') | |||
| export class DeviceOrientationControls2 extends EventDispatcher implements ICameraControls<'change'> { | |||
| object: Object3D | |||
| enabled = false // do not serialize this as it signifies weather this is active. | |||
| deviceOrientation?: DeviceOrientationEvent | |||
| screenOrientation?: ScreenOrientation | |||
| lastOrder: EulerOrder = 'XYZ' | |||
| @serialize() | |||
| @uiSlider('Damping', [0, 1], 0.01) | |||
| dampingFactor = 0.05 | |||
| lastQuaternion = new Quaternion() | |||
| constructor(object: Object3D) { | |||
| super() | |||
| if (window.isSecureContext === false) { | |||
| console.error('DeviceOrientationControls2: DeviceOrientationEvent is only available in secure contexts (https)') | |||
| } | |||
| this.object = object | |||
| this.lastOrder = this.object.rotation.order | |||
| this.object.rotation.reorder('YXZ') | |||
| // this.enabled = true | |||
| this.connect() | |||
| } | |||
| onDeviceOrientationChangeEvent = (event: DeviceOrientationEvent) => { | |||
| this.deviceOrientation = event | |||
| } | |||
| onScreenOrientationChangeEvent = () => { | |||
| this.screenOrientation = screen.orientation | |||
| } | |||
| private _initQuaternion = new Quaternion() | |||
| private _initQuaternionInvert = new Quaternion() | |||
| private _initQuaternionDest = new Quaternion() | |||
| connect() { | |||
| this.onScreenOrientationChangeEvent() // run once on load | |||
| // iOS 13+ | |||
| if (window.DeviceOrientationEvent !== undefined && typeof (window.DeviceOrientationEvent as any).requestPermission === 'function') { | |||
| (window.DeviceOrientationEvent as any).requestPermission().then((response: string)=>{ | |||
| if (response == 'granted') { | |||
| window.addEventListener('orientationchange', this.onScreenOrientationChangeEvent) | |||
| window.addEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) | |||
| } | |||
| }).catch((error: any)=>{ | |||
| console.error('DeviceOrientationControls2: Unable to use DeviceOrientation API:', error) | |||
| }) | |||
| } else { | |||
| window.addEventListener('orientationchange', this.onScreenOrientationChangeEvent) | |||
| window.addEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) | |||
| } | |||
| this.enabled = true | |||
| this._initQuaternion.copy(this.object.quaternion) | |||
| this._initQuaternionInvert.copy(this.object.quaternion).invert() | |||
| } | |||
| disconnect() { | |||
| window.removeEventListener('orientationchange', this.onScreenOrientationChangeEvent) | |||
| window.removeEventListener('deviceorientation', this.onDeviceOrientationChangeEvent) | |||
| this._initQuaternion.identity() | |||
| this._initQuaternionInvert.identity() | |||
| this._initQuaternionDest = new Quaternion() // need to set a new instance here. | |||
| this.object.rotation.reorder(this.lastOrder) | |||
| this.lastOrder = 'XYZ' | |||
| this.enabled = false | |||
| } | |||
| update() { | |||
| if (!this.enabled) return | |||
| const device = this.deviceOrientation | |||
| if (device) { | |||
| const alpha = device.alpha !== null ? MathUtils.degToRad(device.alpha) : 0 // Z | |||
| const beta = device.beta !== null ? MathUtils.degToRad(device.beta) : 0 // X' | |||
| const gamma = device.gamma !== null ? MathUtils.degToRad(device.gamma) : 0 // Y'' | |||
| const orient = this.screenOrientation ? MathUtils.degToRad(this.screenOrientation.angle) : 0 // O | |||
| this.setObjectQuaternion(alpha, beta, gamma, orient) | |||
| if (8 * (1 - this.lastQuaternion.dot(this.object.quaternion)) > EPS) { | |||
| this.lastQuaternion.copy(this.object.quaternion) | |||
| this.dispatchEvent(_changeEvent) | |||
| } | |||
| } | |||
| } | |||
| dispose() { | |||
| this.disconnect() | |||
| } | |||
| private _lastTime = -1 | |||
| // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' | |||
| setObjectQuaternion(alpha: number, beta: number, gamma: number, orient: number): void { | |||
| // if(_lastTime < 0) | |||
| const time = now() / 1000 | |||
| _euler.set(beta, alpha, -gamma, 'YXZ') // 'ZXY' for the device, but 'YXZ' for us | |||
| _q2.setFromEuler(_euler) // orient the device | |||
| _q2.multiply(_q1) // camera looks out the back of the device, not the top | |||
| _q2.multiply(_q0.setFromAxisAngle(_zee, -orient)) // adjust for screen orientation | |||
| if (!(this._initQuaternionDest as any).__init) { | |||
| this._initQuaternionDest.copy(_q2).invert() | |||
| ;(this._initQuaternionDest as any).__init = true | |||
| } | |||
| _q2.premultiply(this._initQuaternionDest) | |||
| const mTime = 1 / 60 | |||
| // this.object.quaternion.multiply(this._initQuaternionInvert) | |||
| this.object.quaternion.slerp(_q2, this.dampingFactor / (Math.min(1, time - this._lastTime) / mTime)) | |||
| // this.object.quaternion.multiply(this._initQuaternion) | |||
| // console.log(time - this._lastTime, mTime) | |||
| this._lastTime = time | |||
| } | |||
| } | |||
| @@ -0,0 +1,349 @@ | |||
| import {MathUtils, Object3D, Spherical, Vector3} from 'three' | |||
| import {IEvent, now, serialize, SimpleEventDispatcher} from 'ts-browser-helpers' | |||
| import {uiFolderContainer, uiInput, uiToggle} from 'uiconfig.js' | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _lookDirection = new Vector3() | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _spherical = new Spherical() | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _target = new Vector3() | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _changeEvent: IEvent<'change'> = {type: 'change'} | |||
| @uiFolderContainer('First Person Controls') | |||
| export class FirstPersonControls2 extends SimpleEventDispatcher<'change'> { | |||
| readonly object: Object3D | |||
| readonly domElement: HTMLElement | Document | |||
| // API | |||
| @serialize() @uiToggle() enabled = true | |||
| @serialize() @uiToggle() enableKeys = true | |||
| @serialize() @uiInput() movementSpeed = 1.0 | |||
| @serialize() @uiInput()lookSpeed = 0.005 | |||
| @serialize() @uiToggle() lookVertical = true | |||
| @serialize() @uiToggle() autoForward = false | |||
| @serialize() @uiToggle() activeLook = true | |||
| @serialize() @uiToggle() heightSpeed = false | |||
| @serialize() @uiInput()heightCoef = 1.0 | |||
| @serialize() @uiInput()heightMin = 0.0 | |||
| @serialize() @uiInput()heightMax = 1.0 | |||
| @serialize() @uiToggle() constrainVertical = false | |||
| @serialize() @uiInput() verticalMin = 0 | |||
| @serialize() @uiInput() verticalMax = Math.PI | |||
| @serialize() @uiToggle() mouseDragOn = false | |||
| // internals | |||
| autoSpeedFactor = 0.0 | |||
| pointerX = 0 | |||
| pointerY = 0 | |||
| moveForward = false | |||
| moveBackward = false | |||
| moveLeft = false | |||
| moveRight = false | |||
| moveUp = false | |||
| moveDown = false | |||
| viewHalfX = 0 | |||
| viewHalfY = 0 | |||
| // private variables | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| private lat = 0 | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| private lon = 0 | |||
| constructor(object: Object3D, domElement: HTMLElement|Document) { | |||
| super() | |||
| this.object = object | |||
| this.domElement = domElement | |||
| this.onPointerMove = this.onPointerMove.bind(this) | |||
| this.onPointerDown = this.onPointerDown.bind(this) | |||
| this.onPointerUp = this.onPointerUp.bind(this) | |||
| this.onKeyDown = this.onKeyDown.bind(this) | |||
| this.onKeyUp = this.onKeyUp.bind(this) | |||
| this.onContextMenu = this.onContextMenu.bind(this) | |||
| this.domElement.addEventListener('contextmenu', this.onContextMenu) | |||
| ;(this.domElement as HTMLElement).addEventListener('pointermove', this.onPointerMove) | |||
| ;(this.domElement as HTMLElement).addEventListener('pointerdown', this.onPointerDown) | |||
| ;(this.domElement as HTMLElement).addEventListener('pointerup', this.onPointerUp) | |||
| window.addEventListener('keydown', this.onKeyDown) | |||
| window.addEventListener('keyup', this.onKeyUp) | |||
| this.handleResize() | |||
| this.setOrientation() | |||
| } | |||
| setOrientation() { | |||
| const quaternion = this.object.quaternion | |||
| _lookDirection.set(0, 0, -1).applyQuaternion(quaternion) | |||
| _spherical.setFromVector3(_lookDirection) | |||
| this.lat = 90 - MathUtils.radToDeg(_spherical.phi) | |||
| this.lon = MathUtils.radToDeg(_spherical.theta) | |||
| } | |||
| handleResize() { | |||
| if (this.domElement === document) { | |||
| this.viewHalfX = window.innerWidth / 2 | |||
| this.viewHalfY = window.innerHeight / 2 | |||
| } else { | |||
| this.viewHalfX = (this.domElement as HTMLElement).offsetWidth / 2 | |||
| this.viewHalfY = (this.domElement as HTMLElement).offsetHeight / 2 | |||
| } | |||
| } | |||
| onPointerDown(event: PointerEvent) { | |||
| if (this.domElement !== document) { | |||
| (this.domElement as HTMLElement).focus() | |||
| } | |||
| if (this.activeLook) { | |||
| switch (event.button) { | |||
| case 0: this.moveForward = true; break | |||
| case 2: this.moveBackward = true; break | |||
| default: break | |||
| } | |||
| } | |||
| this.mouseDragOn = true | |||
| } | |||
| onPointerUp(event: PointerEvent) { | |||
| if (this.activeLook) { | |||
| switch (event.button) { | |||
| case 0: this.moveForward = false; break | |||
| case 2: this.moveBackward = false; break | |||
| default: break | |||
| } | |||
| } | |||
| this.mouseDragOn = false | |||
| } | |||
| onPointerMove(event: PointerEvent) { | |||
| if (this.domElement === document) { | |||
| this.pointerX = event.pageX - this.viewHalfX | |||
| this.pointerY = event.pageY - this.viewHalfY | |||
| } else { | |||
| this.pointerX = event.pageX - (this.domElement as HTMLElement).offsetLeft - this.viewHalfX | |||
| this.pointerY = event.pageY - (this.domElement as HTMLElement).offsetTop - this.viewHalfY | |||
| } | |||
| } | |||
| onKeyDown(event: KeyboardEvent) { | |||
| if (!this.enableKeys) return | |||
| 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 | |||
| default: break | |||
| } | |||
| } | |||
| onKeyUp(event: KeyboardEvent) { | |||
| if (!this.enableKeys) return | |||
| 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 | |||
| default: break | |||
| } | |||
| } | |||
| lookAt(x: number|Vector3, y?: number, z?: number) { | |||
| if ((x as Vector3).isVector3) { | |||
| _target.copy(x as Vector3) | |||
| } else { | |||
| if (y === undefined || z === undefined) console.error('FirstPersonControls2.lookAt: y and z parameters are required') | |||
| else _target.set(x as number, y, z) | |||
| } | |||
| this.object.lookAt(_target) | |||
| this.setOrientation() | |||
| return this | |||
| } | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| private targetPosition = new Vector3() | |||
| private _lastTime = -1 // in ms | |||
| update() { | |||
| const time = now() // in ms | |||
| const delta = (this._lastTime < 0 ? 16 : Math.min(time - this._lastTime, 1000)) / 1000 // in secs | |||
| this._lastTime = time | |||
| // console.log(delta) | |||
| if (!this.enabled) 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.pointerX * actualLookSpeed | |||
| if (this.lookVertical) this.lat -= this.pointerY * 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) | |||
| this.dispatchEvent(_changeEvent) | |||
| } | |||
| dispose() { | |||
| this.domElement.removeEventListener('contextmenu', this.onContextMenu) | |||
| ;(this.domElement as HTMLElement).removeEventListener('pointerdown', this.onPointerDown) | |||
| ;(this.domElement as HTMLElement).removeEventListener('pointermove', this.onPointerMove) | |||
| ;(this.domElement as HTMLElement).removeEventListener('pointerup', this.onPointerUp) | |||
| window.removeEventListener('keydown', this.onKeyDown) | |||
| window.removeEventListener('keyup', this.onKeyUp) | |||
| } | |||
| onContextMenu(event: Event) { | |||
| if (!this.enableKeys) return | |||
| event.preventDefault() | |||
| } | |||
| } | |||
| @@ -0,0 +1,91 @@ | |||
| import {Camera, PerspectiveCamera, Vector3} from 'three' | |||
| import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js' | |||
| const offset2 = new Vector3() | |||
| const targetDeltaX = new Vector3() | |||
| const targetDeltaY = new Vector3() | |||
| const targetDeltaZ = new Vector3() | |||
| const targetDelta = new Vector3() | |||
| const panOffset2 = new Vector3() | |||
| let scaleOffset = 1 | |||
| const upVec = new Vector3(0, 1, 0) | |||
| export class OrbitControls2 extends OrbitControls { | |||
| throttleUpdate = 60 | |||
| constructor(object: Camera, domElement: HTMLElement) { | |||
| super(object, domElement) | |||
| const sup = this.update | |||
| this.update = ()=>this._update(sup) | |||
| } | |||
| readonly targetOffset = new Vector3(0, 0, 0) | |||
| private _update(sup: ()=>boolean): boolean { | |||
| this.target.add(this.targetOffset) | |||
| offset2.copy(this.object.position).sub(this.target) | |||
| scaleOffset = offset2.length() | |||
| panOffset2.copy(this.target) | |||
| const ret = sup() | |||
| panOffset2.sub(this.target) // get the panOffset of this frame from OrbitControls | |||
| // if (panOffset2.length() > 0.0001) | |||
| // console.log(panOffset2.toArray()) | |||
| // console.log(offset2.clone().normalize().cross(upVec)) | |||
| offset2.copy(this.object.position).sub(this.target) | |||
| // panOffset2.multiplyScalar(-1) | |||
| // panOffset3.x = panOffset3.z | |||
| // console.log(panOffset3.z) | |||
| scaleOffset /= offset2.length() | |||
| this.target.add(panOffset2) | |||
| this.object.position.copy(this.target).add(offset2) | |||
| offset2.normalize() | |||
| targetDeltaX.crossVectors(upVec, offset2).normalize() | |||
| targetDeltaY.crossVectors(offset2, targetDeltaX).normalize() | |||
| targetDeltaZ.crossVectors(targetDeltaX, targetDeltaY).normalize().negate() | |||
| if (targetDeltaX.length() > 0.1) // check if not 0 | |||
| this.object.up.crossVectors(offset2.clone().normalize(), targetDeltaX) | |||
| if (this.enablePan) { | |||
| targetDelta.set(0, 0, 0) | |||
| .addScaledVector(targetDeltaX, panOffset2.x) | |||
| .addScaledVector(targetDeltaY, panOffset2.y) | |||
| .addScaledVector(targetDeltaZ, panOffset2.z) | |||
| this.targetOffset.add(targetDelta) | |||
| this.targetOffset.multiplyScalar(1. / scaleOffset) | |||
| } | |||
| targetDelta.set(0, 0, 0) | |||
| .addScaledVector(targetDeltaX, -this.targetOffset.x) | |||
| .addScaledVector(targetDeltaY, -this.targetOffset.y) | |||
| .addScaledVector(targetDeltaZ, -this.targetOffset.z) | |||
| // console.log(targetDelta) | |||
| this.object.lookAt(targetDelta.add(this.target)) | |||
| this.object.updateMatrixWorld() | |||
| if ((this.object as PerspectiveCamera).isCamera) { | |||
| (this.object as PerspectiveCamera).updateProjectionMatrix() | |||
| } | |||
| this.target.sub(this.targetOffset) | |||
| return ret | |||
| } | |||
| } | |||
| @@ -0,0 +1,191 @@ | |||
| import {Euler, EventDispatcher, Object3D, Vector3} from 'three' | |||
| import {IEvent, serialize} from 'ts-browser-helpers' | |||
| import {uiInput, uiPanelContainer, uiToggle} from 'uiconfig.js' | |||
| import {ICameraControls} from '../../core' | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _euler = new Euler(0, 0, 0, 'YXZ') | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _vector = new Vector3() | |||
| export type TPointerLockEvents = 'change'|'lock'|'unlock' | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _changeEvent: IEvent<TPointerLockEvents> = {type: 'change'} | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _lockEvent: IEvent<TPointerLockEvents> = {type: 'lock'} | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _unlockEvent: IEvent<TPointerLockEvents> = {type: 'unlock'} | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| const _PI_2 = Math.PI / 2 | |||
| @uiPanelContainer('Pointer Lock Controls') | |||
| export class PointerLockControls2 extends EventDispatcher implements ICameraControls<TPointerLockEvents> { | |||
| readonly domElement: HTMLElement | |||
| readonly object: Object3D | |||
| isLocked = false | |||
| @uiToggle() @serialize() enabled = true | |||
| // Set to constrain the pitch of the camera | |||
| // Range is 0 to Math.PI radians | |||
| @uiInput() @serialize() minPolarAngle = 0 // radians | |||
| @uiInput() @serialize() maxPolarAngle = Math.PI // radians | |||
| @uiInput() @serialize() pointerSpeed = 1.0 | |||
| @uiToggle() @serialize() autoLockOnClick = true | |||
| constructor(camera: Object3D, domElement: HTMLElement) { | |||
| super() | |||
| this.domElement = domElement | |||
| this.object = camera | |||
| this.onElementClick = this.onElementClick.bind(this) | |||
| this.onMouseMove = this.onMouseMove.bind(this) | |||
| this.onPointerlockChange = this.onPointerlockChange.bind(this) | |||
| this.onPointerlockError = this.onPointerlockError.bind(this) | |||
| this.connect() | |||
| } | |||
| onElementClick(event: Event) { | |||
| if (this.isLocked) return | |||
| if (!this.autoLockOnClick) return | |||
| event.preventDefault() | |||
| this.lock() | |||
| } | |||
| private _movementX = 0 | |||
| private _movementY = 0 | |||
| onMouseMove(event: MouseEvent) { | |||
| if (!this.isLocked) return | |||
| this._movementX += event.movementX || (event as any).mozMovementX || (event as any).webkitMovementX || 0 | |||
| this._movementY += event.movementY || (event as any).mozMovementY || (event as any).webkitMovementY || 0 | |||
| } | |||
| onPointerlockChange() { | |||
| if (this.domElement.ownerDocument.pointerLockElement === this.domElement) { | |||
| this.dispatchEvent(_lockEvent) | |||
| this.isLocked = true | |||
| } else { | |||
| this.dispatchEvent(_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) | |||
| this.domElement.addEventListener('click', this.onElementClick) | |||
| } | |||
| disconnect() { | |||
| this.domElement.ownerDocument.removeEventListener('mousemove', this.onMouseMove) | |||
| this.domElement.ownerDocument.removeEventListener('pointerlockchange', this.onPointerlockChange) | |||
| this.domElement.ownerDocument.removeEventListener('pointerlockerror', this.onPointerlockError) | |||
| this.domElement.removeEventListener('click', this.onElementClick) | |||
| } | |||
| dispose() { | |||
| this.disconnect() | |||
| } | |||
| // getObject() { // retaining this method for backward compatibility | |||
| // | |||
| // return this.object | |||
| // | |||
| // } | |||
| private _forwardDirection = new Vector3(0, 0, -1) | |||
| getDirection(v: Vector3) { | |||
| return v.copy(this._forwardDirection).applyQuaternion(this.object.quaternion) | |||
| } | |||
| moveForward(distance: number) { | |||
| // move forward parallel to the xz-plane | |||
| // assumes camera.up is y-up | |||
| _vector.setFromMatrixColumn(this.object.matrix, 0) | |||
| _vector.crossVectors(this.object.up, _vector) | |||
| this.object.position.addScaledVector(_vector, distance) | |||
| } | |||
| moveRight(distance: number) { | |||
| _vector.setFromMatrixColumn(this.object.matrix, 0) | |||
| this.object.position.addScaledVector(_vector, distance) | |||
| } | |||
| lock() { | |||
| this.domElement.requestPointerLock() | |||
| } | |||
| unlock() { | |||
| this.domElement.ownerDocument.exitPointerLock() | |||
| } | |||
| update() { | |||
| if (Math.abs(this._movementX) < 0.0001 && Math.abs(this._movementY) < 0.0001) return | |||
| _euler.setFromQuaternion(this.object.quaternion) | |||
| _euler.y -= this._movementX * 0.002 * this.pointerSpeed | |||
| _euler.x -= this._movementY * 0.002 * this.pointerSpeed | |||
| this._movementX = 0 | |||
| this._movementY = 0 | |||
| _euler.x = Math.max(_PI_2 - this.maxPolarAngle, Math.min(_PI_2 - this.minPolarAngle, _euler.x)) | |||
| this.object.quaternion.setFromEuler(_euler) | |||
| this.dispatchEvent(_changeEvent) | |||
| } | |||
| } | |||
| @@ -1,6 +1,10 @@ | |||
| export {OrbitControls3, type TOrbitControlsEvents} from './controls/OrbitControls3' | |||
| export {TransformControls2} from './controls/TransformControls2' | |||
| export {TransformControls, TransformControlsGizmo, TransformControlsPlane} from './controls/TransformControls' | |||
| export {FirstPersonControls2} from './controls/FirstPersonControls2' | |||
| export {PointerLockControls2, type TPointerLockEvents} from './controls/PointerLockControls2' | |||
| export {DeviceOrientationControls2} from './controls/DeviceOrientationControls2' | |||
| export {OrbitControls2} from './controls/OrbitControls2' | |||
| export {Box3B} from './math/Box3B' | |||
| export * from './utils/index' | |||
| export * from './widgets/index' | |||
| @@ -0,0 +1,26 @@ | |||
| import {Color, Vector4} from 'three' | |||
| // todo: move these to ts-browser-helpers maybe | |||
| // reference: http://iwasbeingirony.blogspot.ca/2010/06/difference-between-rgbm-and-rgbd.html | |||
| export function vRGBMToLinear(value: Vector4, maxRange: number): Vector4 { | |||
| value.multiplyScalar(value.w * maxRange) | |||
| value.w = 1.0 | |||
| return value | |||
| } | |||
| export function cRGBMToLinear(value: Vector4, maxRange: number): Color { | |||
| vRGBMToLinear(value, maxRange) | |||
| return new Color(value.x, value.y, value.z) | |||
| } | |||
| export function vLinearToRGBM(value: Vector4, maxRange: number): Vector4 { | |||
| const maxRGB = Math.max(value.x, Math.max(value.y, value.z)) | |||
| let M = Math.max(Math.min(maxRGB / maxRange, 1.0), 0.0) | |||
| M = Math.ceil(M * 255.0) / 255.0 | |||
| value.divideScalar(M * maxRange) | |||
| value.w = M | |||
| return value | |||
| } | |||
| export function cLinearToRGBM(value: Color, maxRange: number): Vector4 { | |||
| return vLinearToRGBM(new Vector4(value.r, value.g, value.b, 1.0), maxRange) | |||
| } | |||
| @@ -9,5 +9,7 @@ export {shaderReplaceString} from './shader-helpers' | |||
| export {makeGLBFile} from './gltf' | |||
| export {animateCameraToViewLinear, animateCameraToViewSpherical, sphericalFromCameraView} from './camera-anim' | |||
| export {animateAsync, animateTarget, EasingFunctions, makeSetterFor, animate, lerp, lerpAngle} from './animation' | |||
| export {cLinearToRGBM, vLinearToRGBM, cRGBMToLinear, vRGBMToLinear} from './color-encodings' | |||
| export {CanvasSnapshot, type CanvasSnapshotOptions, type CanvasSnapshotRect} from './canvas-snapshot' | |||
| export type {Easing, KeyframeOptions, AnimationOptions, EasingFunctionType, AnimateResult} from './animation' | |||
| @@ -1 +1 @@ | |||
| export const VERSION = '0.0.22' | |||
| export const VERSION = '0.0.23' | |||