|
@@ -0,0 +1,464 @@
|
|
|
+import {
|
|
|
+ Vector2,
|
|
|
+ Vector3
|
|
|
+} from "three";
|
|
|
+
|
|
|
+const STATE = {
|
|
|
+ NONE: - 1,
|
|
|
+ ROTATE: 0,
|
|
|
+ PAN: 1,
|
|
|
+ ZOOM: 2,
|
|
|
+ ZOOM_PAN: 3,
|
|
|
+ ZOOM_ROTATE: 4
|
|
|
+};
|
|
|
+const HANDLE = {
|
|
|
+ ROTATE: 0,
|
|
|
+ PAN: 1,
|
|
|
+ ZOOM: 2,
|
|
|
+ ZOOM_PAN: 3,
|
|
|
+ ZOOM_ROTATE: 4
|
|
|
+}
|
|
|
+
|
|
|
+const pointers = []
|
|
|
+const pointerPositions = {}
|
|
|
+
|
|
|
+export default class FloorplanControls {
|
|
|
+ constructor(camera, dom, player){
|
|
|
+ this.camera = camera
|
|
|
+ this.domElement = dom
|
|
|
+ this.domElement.style.touchAction = 'none'; // disable touch scroll
|
|
|
+ this.player = player
|
|
|
+
|
|
|
+
|
|
|
+ this.panSpeed = 1
|
|
|
+ this.zoomSpeed = 1
|
|
|
+ this.rotateSpeed = 1
|
|
|
+
|
|
|
+ this.maxDistance = 100
|
|
|
+ this.minDistance = 0.1
|
|
|
+ this.maxZoom = 500
|
|
|
+ this.minZoom = 5
|
|
|
+
|
|
|
+ this.target = new Vector3()
|
|
|
+
|
|
|
+ this.state = STATE.NONE
|
|
|
+
|
|
|
+ this.rotateStart = new Vector2()
|
|
|
+ this.rotateEnd = new Vector2()
|
|
|
+
|
|
|
+ this.panStart = new Vector2()
|
|
|
+ this.panEnd = new Vector2()
|
|
|
+
|
|
|
+ this.zoomStart = new Vector2()
|
|
|
+
|
|
|
+ this.locked = false //禁止用户操作
|
|
|
+ this.enabled = true //禁止update
|
|
|
+ this.enablePan = true
|
|
|
+ this.enableRotate = true
|
|
|
+ this.enableZoom = true
|
|
|
+
|
|
|
+ this.touchesEvent = {
|
|
|
+ ONE: HANDLE.PAN,
|
|
|
+ TWO: HANDLE.ZOOM
|
|
|
+ }
|
|
|
+ this.mouseEvent = {
|
|
|
+ LEFT: HANDLE.PAN,
|
|
|
+ RIGHT: HANDLE.ROTATE,
|
|
|
+ WHEEL: HANDLE.ZOOM
|
|
|
+ }
|
|
|
+
|
|
|
+ this.onbindEvent()
|
|
|
+ }
|
|
|
+
|
|
|
+ onbindEvent = () => {
|
|
|
+ this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this))
|
|
|
+ this.domElement.addEventListener('pointerup', this.onPointerUp.bind(this))
|
|
|
+ this.domElement.addEventListener('pointermove', this.onPointerMove.bind(this))
|
|
|
+ this.domElement.addEventListener('pointercancel', this.onPointerUp.bind(this))
|
|
|
+
|
|
|
+ this.domElement.addEventListener('mousewheel', this.onMouseWheel.bind(this), { passive: false })
|
|
|
+
|
|
|
+ this.domElement.addEventListener("contextmenu", this.onPreventDefault)
|
|
|
+ }
|
|
|
+
|
|
|
+ addPointer = (event) => {
|
|
|
+ pointers.push(event);
|
|
|
+ }
|
|
|
+ removePointer = (event) => {
|
|
|
+ for ( let i = 0; i < pointers.length; i ++ ) {
|
|
|
+ if ( pointers[ i ].pointerId == event.pointerId ) {
|
|
|
+ pointers.splice( i, 1 );
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ isTrackingPointer = (event) => {
|
|
|
+ for ( let i = 0; i < pointers.length; i ++ ) {
|
|
|
+ if ( pointers[ i ] == event.pointerId ) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ trackPointer = (event) => {
|
|
|
+ let position = pointerPositions[ event.pointerId ];
|
|
|
+ if ( position === undefined ) {
|
|
|
+ position = new Vector2();
|
|
|
+ pointerPositions[ event.pointerId ] = position;
|
|
|
+ }
|
|
|
+ position.set( event.pageX, event.pageY );
|
|
|
+ }
|
|
|
+ getSecondPointerPosition = (event) => {
|
|
|
+ const pointerId = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ].pointerId : pointers[ 0 ].pointerId;
|
|
|
+ return pointerPositions[ pointerId ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // pointer event
|
|
|
+ onPointerDown = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ if(pointers.length === 0) {
|
|
|
+ this.domElement.setPointerCapture( event.pointerId );
|
|
|
+ }
|
|
|
+ if(this.isTrackingPointer(event)) return;
|
|
|
+ this.addPointer(event)
|
|
|
+ if ( event.pointerType === 'touch' ) {
|
|
|
+ this.onTouchStart( event );
|
|
|
+ } else {
|
|
|
+ this.onMouseDown( event );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onPointerUp = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ this.removePointer(event)
|
|
|
+ if(pointers.length === 0) {
|
|
|
+ this.domElement.releasePointerCapture( event.pointerId );
|
|
|
+ this.state = STATE.NONE
|
|
|
+ } else if (pointers.length === 1) {
|
|
|
+ const pointerId = pointers[ 0 ].pointerId;
|
|
|
+ const position = pointerPositions[ pointerId ];
|
|
|
+ this.onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onPointerMove = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ if ( event.pointerType === 'touch' ) {
|
|
|
+ this.onTouchMove( event );
|
|
|
+ } else {
|
|
|
+ this.onMouseMove( event );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //touch event
|
|
|
+ onTouchStart = (event) => {
|
|
|
+ this.trackPointer(event)
|
|
|
+ switch(pointers.length) {
|
|
|
+ case 1:
|
|
|
+ switch(this.touchesEvent.ONE) {
|
|
|
+ case HANDLE.ROTATE: //rotate
|
|
|
+ if ( this.enableRotate === false ) return;
|
|
|
+ this.handleTouchStartRotate()
|
|
|
+ this.state = STATE.ROTATE
|
|
|
+ break
|
|
|
+ case HANDLE.PAN: //pan
|
|
|
+ if(this.enablePan === false) return;
|
|
|
+ this.handleTouchStartPan()
|
|
|
+ this.state = STATE.PAN
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ state = STATE.NONE;
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case 2:
|
|
|
+ switch (this.touchesEvent.TWO) {
|
|
|
+ case HANDLE.ZOOM: //zoom
|
|
|
+ if(this.enableZoom === false) return;
|
|
|
+ this.handleTouchStartZoom()
|
|
|
+ this.state = STATE.ZOOM
|
|
|
+ break
|
|
|
+ case HANDLE.ZOOM_PAN: //zoom_pan
|
|
|
+ if ( this.enableZoom === false && this.enablePan === false ) return;
|
|
|
+ this.handleTouchStartZoom()
|
|
|
+ this.handleTouchStartPan()
|
|
|
+ this.state = STATE.ZOOM_PAN
|
|
|
+ break
|
|
|
+ //todo case HANDLE.ZOOM_ROTATE:
|
|
|
+ default:
|
|
|
+ state = STATE.NONE;
|
|
|
+ }
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onTouchMove = (event) => {
|
|
|
+ this.trackPointer(event)
|
|
|
+ switch(this.state) {
|
|
|
+ case STATE.ROTATE:
|
|
|
+ if(this.enableRotate === false) return;
|
|
|
+ this.handleTouchMoveRotate(event)
|
|
|
+ break
|
|
|
+ case STATE.PAN:
|
|
|
+ if(this.enablePan === false) return;
|
|
|
+ this.handleTouchMovePan(event)
|
|
|
+ break
|
|
|
+ case STATE.ZOOM:
|
|
|
+ if(this.enableZoom === false) return;
|
|
|
+ this.handleTouchMoveZoom(event)
|
|
|
+ break
|
|
|
+ case STATE.ZOOM_PAN:
|
|
|
+ if(this.enableZoom) this.handleTouchMoveZoom(event)
|
|
|
+ if(this.enablePan) this.handleTouchMovePan(event)
|
|
|
+ break
|
|
|
+ //todo case STATE.ZOOM_ROTATE:
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //mouse event
|
|
|
+ onMouseDown = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ switch(event.button) {
|
|
|
+ case 0: //left
|
|
|
+ switch(this.mouseEvent.LEFT) {
|
|
|
+ case HANDLE.PAN:
|
|
|
+ if(this.enablePan === false) return
|
|
|
+ this.handleMouseDownPan(event)
|
|
|
+ this.state = STATE.PAN
|
|
|
+ break
|
|
|
+ case HANDLE.ROTATE:
|
|
|
+ if(this.enablePan === false) return
|
|
|
+ this.handleMouseDownRotate(event)
|
|
|
+ this.state = STATE.ROTATE
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case 2: //right
|
|
|
+ switch(this.mouseEvent.RIGHT) {
|
|
|
+ case HANDLE.PAN:
|
|
|
+ if(this.enablePan === false) return
|
|
|
+ this.handleMouseDownPan(event)
|
|
|
+ this.state = STATE.PAN
|
|
|
+ break
|
|
|
+ case HANDLE.ROTATE:
|
|
|
+ if(this.enablePan === false) return
|
|
|
+ this.handleMouseDownRotate(event)
|
|
|
+ this.state = STATE.ROTATE
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE
|
|
|
+ }
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onMouseMove = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ switch(this.state) {
|
|
|
+ case STATE.PAN:
|
|
|
+ if(this.enablePan === false) return
|
|
|
+ this.handleMouseMovePan(event)
|
|
|
+ break
|
|
|
+ case STATE.ROTATE:
|
|
|
+ if(this.enableRotate === false) return
|
|
|
+ this.handleMouseMoveRotate(event)
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.state = STATE.NONE
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onMouseWheel = (event) => {
|
|
|
+ if(this.locked) return
|
|
|
+ if(this.enableZoom === false) return
|
|
|
+ event.preventDefault()
|
|
|
+ this.handleMouseWheelZoom(event)
|
|
|
+ }
|
|
|
+ onPreventDefault = (event) => {
|
|
|
+ event.preventDefault()
|
|
|
+ }
|
|
|
+
|
|
|
+ //================================handle================================
|
|
|
+ //-------------------------rotate-------------------------
|
|
|
+ handleTouchStartRotate = () => {
|
|
|
+ const position = pointerPositions[pointers[ 0 ].pointerId]
|
|
|
+ this.rotateStart.set(position.x, position.y)
|
|
|
+ }
|
|
|
+ handleTouchMoveRotate = (event) => {
|
|
|
+ this.rotateEnd.set( event.pageX, event.pageY );
|
|
|
+ let rotateDelta = this.rotateEnd.clone().sub( this.rotateStart ).multiplyScalar( this.rotateSpeed );
|
|
|
+ let element = this.domElement
|
|
|
+ let rotateX = 2 * Math.PI * rotateDelta.x / element.clientHeight
|
|
|
+ let rotateY = 2 * Math.PI * rotateDelta.y / element.clientHeight
|
|
|
+
|
|
|
+ this.rotate(rotateX, rotateY)
|
|
|
+
|
|
|
+ this.rotateStart.copy( this.rotateEnd );
|
|
|
+ }
|
|
|
+ handleMouseDownRotate = (event) => {
|
|
|
+ this.rotateStart.set(event.pageX, event.pageY)
|
|
|
+ }
|
|
|
+ handleMouseMoveRotate = (event) => {
|
|
|
+ this.rotateEnd.set( event.pageX, event.pageY );
|
|
|
+ let rotateDelta = this.rotateEnd.clone().sub( this.rotateStart ).multiplyScalar( this.rotateSpeed );
|
|
|
+ let element = this.domElement
|
|
|
+ let rotateX = 2 * Math.PI * rotateDelta.x / element.clientHeight
|
|
|
+ let rotateY = 2 * Math.PI * rotateDelta.y / element.clientHeight
|
|
|
+
|
|
|
+ this.rotate(rotateX, rotateY)
|
|
|
+
|
|
|
+ this.rotateStart.copy( this.rotateEnd );
|
|
|
+ }
|
|
|
+ //-------------------------zoom-------------------------
|
|
|
+ handleTouchStartZoom = () => {
|
|
|
+ const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;
|
|
|
+ const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;
|
|
|
+ const distance = Math.sqrt( dx * dx + dy * dy );
|
|
|
+ this.zoomStart.set(0, distance)
|
|
|
+ }
|
|
|
+ handleTouchMoveZoom = (event) => {
|
|
|
+ const position = this.getSecondPointerPosition( event );
|
|
|
+ const dx = event.pageX - position.x;
|
|
|
+ const dy = event.pageY - position.y;
|
|
|
+ const distance = Math.sqrt( dx * dx + dy * dy );
|
|
|
+ let delta = Math.pow( distance / this.zoomStart.y, this.zoomSpeed )
|
|
|
+
|
|
|
+ this.zoom(1/delta)
|
|
|
+
|
|
|
+ this.zoomStart.set(0, distance)
|
|
|
+ }
|
|
|
+ handleMouseWheelZoom = (event) => {
|
|
|
+ if(event.deltaY > 0) { //zoom out
|
|
|
+ this.zoom(1.05 * this.zoomSpeed)
|
|
|
+ } else { //zoom in
|
|
|
+ this.zoom(0.95 * this.zoomSpeed)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //-------------------------pan-------------------------
|
|
|
+ handleTouchStartPan = () => {
|
|
|
+ if ( pointers.length === 1 ) {
|
|
|
+ const position = pointerPositions[pointers[ 0 ].pointerId]
|
|
|
+ this.panStart.set(position.x, position.y)
|
|
|
+ } else {
|
|
|
+ const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
|
|
|
+ const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
|
|
|
+ this.panStart.set( x, y );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handleTouchMovePan = (event) => {
|
|
|
+ if ( pointers.length === 1 ) {
|
|
|
+ this.panEnd.set( event.pageX, event.pageY );
|
|
|
+ } else {
|
|
|
+ const position = this.getSecondPointerPosition( event );
|
|
|
+ const x = 0.5 * ( event.pageX + position.x );
|
|
|
+ const y = 0.5 * ( event.pageY + position.y );
|
|
|
+ this.panEnd.set(x, y);
|
|
|
+ }
|
|
|
+ let panDelta = this.panEnd.clone().sub(this.panStart)
|
|
|
+
|
|
|
+ this.pan(panDelta)
|
|
|
+
|
|
|
+ this.panStart.copy(this.panEnd);
|
|
|
+ }
|
|
|
+ handleMouseDownPan = (event) => {
|
|
|
+ this.panStart.set(event.pageX, event.pageY)
|
|
|
+ }
|
|
|
+ handleMouseMovePan = (event) => {
|
|
|
+ this.panEnd.set(event.pageX, event.pageY);
|
|
|
+ let panDelta = this.panEnd.clone().sub(this.panStart)
|
|
|
+
|
|
|
+ this.pan(panDelta)
|
|
|
+
|
|
|
+ this.panStart.copy(this.panEnd);
|
|
|
+ }
|
|
|
+
|
|
|
+ rotate(x, y) {
|
|
|
+ let r = y
|
|
|
+ if(Math.abs(x) > Math.abs(y)) r = x
|
|
|
+
|
|
|
+ let cameraRZ = this.camera.rotation.z
|
|
|
+ cameraRZ += r
|
|
|
+ if(Math.abs(cameraRZ) >= Math.PI * 2) {
|
|
|
+ cameraRZ -= Math.sign(cameraRZ) * Math.PI * 2
|
|
|
+ }
|
|
|
+ this.camera.rotation.z = cameraRZ
|
|
|
+ this.cameraUpate()
|
|
|
+ }
|
|
|
+ zoom(delta) {
|
|
|
+ // if(this.camera.isPerspectiveCamera) {
|
|
|
+ // let cameraY = this.camera.position.y
|
|
|
+ // cameraY *= delta
|
|
|
+ // cameraY = Math.max(cameraY, this.minDistance)
|
|
|
+ // cameraY = Math.min(cameraY, this.maxDistance)
|
|
|
+ // this.camera.position.y = cameraY //handle
|
|
|
+ // } else if(this.camera.isOrthographicCamera) {
|
|
|
+ // let zoom = this.camera.zoom
|
|
|
+ // zoom *= 1/delta
|
|
|
+ // console.log(zoom)
|
|
|
+ // this.camera.zoom = zoom
|
|
|
+ // this.camera.updateProjectionMatrix()
|
|
|
+ // }
|
|
|
+
|
|
|
+ let cameraY = this.camera.position.y
|
|
|
+ cameraY *= delta
|
|
|
+ cameraY = Math.max(cameraY, this.minDistance)
|
|
|
+ cameraY = Math.min(cameraY, this.maxDistance)
|
|
|
+ this.camera.position.y = cameraY //handle
|
|
|
+ if(this.camera.isOrthographicCamera) {
|
|
|
+ let zoom = this.camera.zoom
|
|
|
+ zoom *= 1/delta
|
|
|
+ zoom = Math.max(zoom, this.minZoom)
|
|
|
+ zoom = Math.min(zoom, this.maxZoom)
|
|
|
+ this.camera.zoom = zoom
|
|
|
+ this.camera.updateProjectionMatrix()
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cameraUpate()
|
|
|
+ }
|
|
|
+ pan(delta) {
|
|
|
+ const element = this.domElement;
|
|
|
+ const matrix = this.camera.matrix.clone()
|
|
|
+ const left = new Vector3()
|
|
|
+ const up = new Vector3()
|
|
|
+ let panDelta = delta.multiplyScalar(this.panSpeed);
|
|
|
+
|
|
|
+ if(this.camera.isPerspectiveCamera ) {
|
|
|
+ let scalar = 2 * this.camera.position.y * Math.tan( ( this.camera.fov / 2 ) * Math.PI / 180.0 ) / element.clientHeight;
|
|
|
+ panDelta.multiplyScalar(scalar)
|
|
|
+
|
|
|
+ left.setFromMatrixColumn( matrix, 0 );
|
|
|
+ left.multiplyScalar( -panDelta.x );
|
|
|
+ up.setFromMatrixColumn( matrix, 1 );
|
|
|
+ up.multiplyScalar( panDelta.y );
|
|
|
+ } else if(this.camera.isOrthographicCamera){
|
|
|
+ panDelta.x = panDelta.x * ( this.camera.right - this.camera.left ) / this.camera.zoom / element.clientWidth, this.camera.matrix
|
|
|
+ panDelta.y = panDelta.y * ( this.camera.top - this.camera.bottom ) / this.camera.zoom / element.clientHeight, this.camera.matrix
|
|
|
+ left.setFromMatrixColumn( matrix, 0 );
|
|
|
+ left.multiplyScalar( -panDelta.x );
|
|
|
+ up.setFromMatrixColumn( matrix, 1 );
|
|
|
+ up.multiplyScalar( panDelta.y );
|
|
|
+ } else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.camera.position.add(left).add(up)
|
|
|
+ this.target.set(this.camera.position.x, 0, this.camera.position.z)
|
|
|
+ this.cameraUpate()
|
|
|
+ }
|
|
|
+
|
|
|
+ lookAt(target, height) {
|
|
|
+ if(!target) return
|
|
|
+ height = height !== undefined ? height : this.camera.position.y
|
|
|
+ this.camera.position.set(target.x, height, target.z)
|
|
|
+ this.target.set(target.x, 0, target.z)
|
|
|
+ this.camera.lookAt(this.target)
|
|
|
+ }
|
|
|
+
|
|
|
+ cameraUpate = () => {
|
|
|
+ this.camera.updateMatrix()
|
|
|
+ this.camera.updateProjectionMatrix()
|
|
|
+ }
|
|
|
+
|
|
|
+ update = () => {
|
|
|
+ if(!this.enabled) return
|
|
|
+ }
|
|
|
+}
|