diff --git a/examples/js/controls/ArcballControls.js b/examples/js/controls/ArcballControls.js index fe0094be364b172c78f6be711dac00ffc89c0279..f03d1b7d57cbd171e440d8a28166c06bd4108ee3 100644 --- a/examples/js/controls/ArcballControls.js +++ b/examples/js/controls/ArcballControls.js @@ -1,8 +1,6 @@ ( function () { - - //trackball state + const STATE = { - IDLE: Symbol(), ROTATE: Symbol(), PAN: Symbol(), @@ -13,3159 +11,2985 @@ TOUCH_MULTI: Symbol(), ANIMATION_FOCUS: Symbol(), ANIMATION_ROTATE: Symbol() - }; - const INPUT = { - NONE: Symbol(), ONE_FINGER: Symbol(), ONE_FINGER_SWITCHED: Symbol(), TWO_FINGER: Symbol(), MULT_FINGER: Symbol(), CURSOR: Symbol() - - }; - - //cursor center coordinates + }; //cursor center coordinates + const _center = { - x: 0, y: 0 - - }; - - //transformation matrices for gizmos and camera + }; //transformation matrices for gizmos and camera + const _transformation = { - camera: new THREE.Matrix4(), gizmos: new THREE.Matrix4() - + }; //events + + const _changeEvent = { + type: 'change' + }; + const _startEvent = { + type: 'start' + }; + const _endEvent = { + type: 'end' }; - - //events - const _changeEvent = { type: 'change' }; - const _startEvent = { type: 'start' }; - const _endEvent = { type: 'end' }; - - /** - * - * @param {Camera} camera Virtual camera used in the scene - * @param {HTMLElement} domElement Renderer's dom element - * @param {Scene} scene The scene to be rendered - */ + * + * @param {Camera} camera Virtual camera used in the scene + * @param {HTMLElement} domElement Renderer's dom element + * @param {Scene} scene The scene to be rendered + */ + class ArcballControls extends THREE.Object3D { - - constructor( camera, domElement, scene = null ) { - + + constructor( _camera, domElement, scene = null ) { + super(); - this.camera = null; - this.domElement = domElement; - this.scene = scene; - this.mouseActions = []; - this._mouseOp = null; + this.onWindowResize = () => { - //global vectors and matrices that are used in some operations to avoid creating new objects every time (e.g. every time cursor moves) - this._v2_1 = new THREE.Vector2(); - this._v3_1 = new THREE.Vector3(); - this._v3_2 = new THREE.Vector3(); - - this._m4_1 = new THREE.Matrix4(); - this._m4_2 = new THREE.Matrix4(); - - this._quat = new THREE.Quaternion(); - - //transformation matrices - this._translationMatrix = new THREE.Matrix4(); //matrix for translation operation - this._rotationMatrix = new THREE.Matrix4(); //matrix for rotation operation - this._scaleMatrix = new THREE.Matrix4(); //matrix for scaling operation - - this._rotationAxis = new THREE.Vector3(); //axis for rotate operation - - - //camera state - this._cameraMatrixState = new THREE.Matrix4(); - this._cameraProjectionState = new THREE.Matrix4(); - - this._fovState = 1; - this._upState = new THREE.Vector3(); - this._zoomState = 1; - this._nearPos = 0; - this._farPos = 0; - - this._gizmoMatrixState = new THREE.Matrix4(); - - //initial values - this._up0 = new THREE.Vector3(); - this._zoom0 = 1; - this._fov0 = 0; - this._initialNear = 0; - this._nearPos0 = 0; - this._initialFar = 0; - this._farPos0 = 0; - this._cameraMatrixState0 = new THREE.Matrix4(); - this._gizmoMatrixState0 = new THREE.Matrix4(); - - //pointers array - this._button = -1; - this._touchStart = []; - this._touchCurrent = []; - this._input = INPUT.NONE; - - //two fingers touch interaction - this._switchSensibility = 32; //minimum movement to be performed to fire single pan start after the second finger has been released - this._startFingerDistance = 0; //distance between two fingers - this._currentFingerDistance = 0; - this._startFingerRotation = 0; //amount of rotation performed with two fingers - this._currentFingerRotation = 0; - - //double tap - this._devPxRatio = 0; - this._downValid = true; - this._nclicks = 0; - this._downEvents = []; - this._downStart = 0; //pointerDown time - this._clickStart = 0; //first click time - this._maxDownTime = 250; - this._maxInterval = 300; - this._posThreshold = 24; - this._movementThreshold = 24; - - //cursor positions - this._currentCursorPosition = new THREE.Vector3(); - this._startCursorPosition = new THREE.Vector3(); - - //grid - this._grid = null; //grid to be visualized during pan operation - this._gridPosition = new THREE.Vector3(); - - //gizmos - this._gizmos = new THREE.Group(); - this._curvePts = 128; - - - //animations - this._timeStart = -1; //initial time - this._animationId = -1; - - //focus animation - this.focusAnimationTime = 500; //duration of focus animation in ms - - //rotate animation - this._timePrev = 0; //time at which previous rotate operation has been detected - this._timeCurrent = 0; //time at which current rotate operation has been detected - this._anglePrev = 0; //angle of previous rotation - this._angleCurrent = 0; //angle of current rotation - this._cursorPosPrev = new THREE.Vector3(); //cursor position when previous rotate operation has been detected - this._cursorPosCurr = new THREE.Vector3();//cursor position when current rotate operation has been detected - this._wPrev = 0; //angular velocity of the previous rotate operation - this._wCurr = 0; //angular velocity of the current rotate operation - - - //parameters - this.adjustNearFar = false; - this.scaleFactor = 1.1; //zoom/distance multiplier - this.dampingFactor = 25; - this.wMax = 20; //maximum angular velocity allowed - this.enableAnimations = true; //if animations should be performed - this.enableGrid = false; //if grid should be showed during pan operation - this.cursorZoom = false; //if wheel zoom should be cursor centered - this.minFov = 5; - this.maxFov = 90; - - this.enabled = true; - this.enablePan = true; - this.enableRotate = true; - this.enableZoom = true; - this.enableGizmos = true; - - this.minDistance = 0; - this.maxDistance = Infinity; - this.minZoom = 0; - this.maxZoom = Infinity; - - //trackball parameters - this._tbCenter = new THREE.Vector3( 0, 0, 0 ); - this._tbRadius = 1; - - //FSA - this._state = STATE.IDLE; - - this.setCamera(camera); - - if ( this.scene != null ) { - - this.scene.add( this._gizmos ); - - } - - this.domElement.style.touchAction = 'none'; - this._devPxRatio = window.devicePixelRatio; - - this.initializeMouseActions(); - - this.domElement.addEventListener( 'contextmenu', this.onContextMenu ); - this.domElement.addEventListener( 'wheel', this.onWheel ); - this.domElement.addEventListener( 'pointerdown', this.onPointerDown ); - this.domElement.addEventListener( 'pointercancel', this.onPointerCancel ) - - window.addEventListener( 'keydown', this.onKeyDown ); - window.addEventListener( 'resize', this.onWindowResize ); - - }; - - //listeners - - onWindowResize = () => { - - const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3; - this._tbRadius = this.calculateTbRadius( this.camera ); - - const newRadius = this._tbRadius / scale; - const curve = new THREE.EllipseCurve( 0, 0, newRadius, newRadius ); - const points = curve.getPoints( this._curvePts ); - const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); - - - for( let gizmo in this._gizmos.children ) { - - this._gizmos.children[ gizmo ].geometry = curveGeometry; - - } - - this.dispatchEvent( _changeEvent ); - - }; + const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3; + this._tbRadius = this.calculateTbRadius( this.camera ); + const newRadius = this._tbRadius / scale; + const curve = new THREE.EllipseCurve( 0, 0, newRadius, newRadius ); + const points = curve.getPoints( this._curvePts ); + const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); - onContextMenu = ( event ) => { + for ( const gizmo in this._gizmos.children ) { + + this._gizmos.children[ gizmo ].geometry = curveGeometry; - if ( !this.enabled ) { - - return; - - } - - for( let i = 0; i < this.mouseActions.length; i++ ) { - - if ( this.mouseActions[ i ].mouse == 2 ) { - - //prevent only if button 2 is actually used - event.preventDefault(); - break; - } - - } - - }; - - onPointerCancel = ( event ) => { - - this._touchStart.splice( 0, this._touchStart.length ); - this._touchCurrent.splice( 0, this._touchCurrent.length ); - this._input = INPUT.NONE; - - }; - - onPointerDown = ( event ) => { - - if ( event.button == 0 && event.isPrimary ) { - - this._downValid = true; - this._downEvents.push( event ); - this._downStart = performance.now(); - - } else { - - this._downValid = false; - - } - - if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { - - this._touchStart.push( event ); - this._touchCurrent.push( event ); - - switch ( this._input ) { - - case INPUT.NONE: - - //singleStart - this._input = INPUT.ONE_FINGER; - this.onSinglePanStart( event, 'ROTATE' ); - - window.addEventListener( 'pointermove', this.onPointerMove ); - window.addEventListener( 'pointerup', this.onPointerUp ); - - break; - - case INPUT.ONE_FINGER: - case INPUT.ONE_FINGER_SWITCHED: - - //doubleStart - this._input = INPUT.TWO_FINGER; - - this.onRotateStart(); - this.onPinchStart(); - this.onDoublePanStart(); - - break; - - case INPUT.TWO_FINGER: - - //multipleStart - this._input = INPUT.MULT_FINGER; - this.onTriplePanStart( event ); - break; + + this.dispatchEvent( _changeEvent ); + + }; + + this.onContextMenu = event => { + + if ( ! this.enabled ) { + + return; + } - - } else if ( event.pointerType != 'touch' && this._input == INPUT.NONE ) { - - let modifier = null; - - if( event.ctrlKey || event.metaKey ) { - - modifier = 'CTRL'; - - } else if ( event.shiftKey ) { - - modifier = 'SHIFT'; - + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + if ( this.mouseActions[ i ].mouse == 2 ) { + + //prevent only if button 2 is actually used + event.preventDefault(); + break; + + } + } - - this._mouseOp = this.getOpFromAction( event.button, modifier ); - if ( this._mouseOp != null ) { - - window.addEventListener( 'pointermove', this.onPointerMove ); - window.addEventListener( 'pointerup', this.onPointerUp ); - - //singleStart - this._input = INPUT.CURSOR; - this._button = event.button; - this.onSinglePanStart( event, this._mouseOp ); - + + }; + + this.onPointerCancel = () => { + + this._touchStart.splice( 0, this._touchStart.length ); + + this._touchCurrent.splice( 0, this._touchCurrent.length ); + + this._input = INPUT.NONE; + + }; + + this.onPointerDown = event => { + + if ( event.button == 0 && event.isPrimary ) { + + this._downValid = true; + + this._downEvents.push( event ); + + this._downStart = performance.now(); + + } else { + + this._downValid = false; + } - - } - - }; - - onPointerMove = ( event ) => { - - if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { - - switch ( this._input ) { - - case INPUT.ONE_FINGER: - - //singleMove - this.updateTouchEvent( event ); - - this.onSinglePanMove( event, STATE.ROTATE ); - break; - - case INPUT.ONE_FINGER_SWITCHED: - - const movement = this.calculatePointersDistance( this._touchCurrent[ 0 ], event ) * this._devPxRatio; - - if ( movement >= this._switchSensibility ) { - - //singleMove + + if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { + + this._touchStart.push( event ); + + this._touchCurrent.push( event ); + + switch ( this._input ) { + + case INPUT.NONE: + //singleStart this._input = INPUT.ONE_FINGER; - this.updateTouchEvent( event ); - this.onSinglePanStart( event, 'ROTATE' ); + window.addEventListener( 'pointermove', this.onPointerMove ); + window.addEventListener( 'pointerup', this.onPointerUp ); break; - - } - - break; - - case INPUT.TWO_FINGER: - - //rotate/pan/pinchMove - this.updateTouchEvent( event ); - - this.onRotateMove(); - this.onPinchMove(); - this.onDoublePanMove(); - - break; - - case INPUT.MULT_FINGER: - - //multMove - this.updateTouchEvent( event ); - - this.onTriplePanMove( event ); - break; - - } - - } else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) { - - let modifier = null; - - if( event.ctrlKey || event.metaKey ) { - - modifier = 'CTRL'; - - } else if ( event.shiftKey ) { - - modifier = 'SHIFT'; - - } - - const mouseOpState = this.getOpStateFromAction( this._button, modifier ); - - if ( mouseOpState != null ) { - - this.onSinglePanMove( event, mouseOpState ); - - } - - } - - //checkDistance - if( this._downValid ) { - - const movement = this.calculatePointersDistance( this._downEvents[ this._downEvents.length -1 ], event ) * this._devPxRatio; - if ( movement > this._movementThreshold ) { - - this._downValid = false; - - } - - } - - }; - - onPointerUp = ( event ) => { - - if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { - - let nTouch = this._touchCurrent.length; - - for( let i = 0; i < nTouch; i++ ) { - - if ( this._touchCurrent[ i ].pointerId == event.pointerId ) { - - this._touchCurrent.splice( i, 1 ); - this._touchStart.splice( i, 1 ); - break; - + + case INPUT.ONE_FINGER: + case INPUT.ONE_FINGER_SWITCHED: + //doubleStart + this._input = INPUT.TWO_FINGER; + this.onRotateStart(); + this.onPinchStart(); + this.onDoublePanStart(); + break; + + case INPUT.TWO_FINGER: + //multipleStart + this._input = INPUT.MULT_FINGER; + this.onTriplePanStart( event ); + break; + } - - } - - switch ( this._input ) { - - case INPUT.ONE_FINGER: - case INPUT.ONE_FINGER_SWITCHED: - - //singleEnd - window.removeEventListener( 'pointermove', this.onPointerMove ); - window.removeEventListener( 'pointerup', this.onPointerUp ); - - this._input = INPUT.NONE; - this.onSinglePanEnd(); - - break; - - case INPUT.TWO_FINGER: - - //doubleEnd - this.onDoublePanEnd( event ); - this.onPinchEnd( event ); - this.onRotateEnd( event ); - - //switching to singleStart - this._input = INPUT.ONE_FINGER_SWITCHED; - - break; - - case INPUT.MULT_FINGER: - - if ( this._touchCurrent.length == 0 ) { - - window.removeEventListener( 'pointermove', this.onPointerMove ); - window.removeEventListener( 'pointerup', this.onPointerUp ); - //multCancel - this._input = INPUT.NONE; - this.onTriplePanEnd(); - - } - - break; - - } - - } else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) { - - window.removeEventListener( 'pointermove', this.onPointerMove ); - window.removeEventListener( 'pointerup', this.onPointerUp ); - - this._input = INPUT.NONE; - this.onSinglePanEnd(); - this._button = -1; - - } - - if ( event.isPrimary ) { - - if ( this._downValid ) { - - const downTime = event.timeStamp - this._downEvents[ this._downEvents.length -1 ].timeStamp; - - if ( downTime <= this._maxDownTime ) { - - if ( this._nclicks == 0 ) { - - //first valid click detected - this._nclicks = 1; - this._clickStart = performance.now(); - - } else { - - const clickInterval = event.timeStamp - this._clickStart; - const movement = this.calculatePointersDistance( this._downEvents[ 1 ], this._downEvents[ 0 ] ) * this._devPxRatio; - - if ( clickInterval <= this._maxInterval && movement <= this._posThreshold ) { - - //second valid click detected - //fire double tap and reset values - this._nclicks = 0; - this._downEvents.splice( 0, this._downEvents.length ); - this.onDoubleTap( event ); - - } else { - - //new 'first click' - this._nclicks = 1; - this._downEvents.shift(); - this._clickStart = performance.now(); - - } - - } - - } else { - - this._downValid = false; - this._nclicks = 0; - this._downEvents.splice( 0, this._downEvents.length ); - + } else if ( event.pointerType != 'touch' && this._input == INPUT.NONE ) { + + let modifier = null; + + if ( event.ctrlKey || event.metaKey ) { + + modifier = 'CTRL'; + + } else if ( event.shiftKey ) { + + modifier = 'SHIFT'; + } - - } else { - - this._nclicks = 0; - this._downEvents.splice( 0, this._downEvents.length ); - - } - } - - }; - - onWheel = ( event ) => { - - if ( this.enabled && this.enableZoom ) { - - let modifier = null; - - if( event.ctrlKey || event.metaKey ) { - - modifier = 'CTRL'; - - } else if ( event.shiftKey ) { - - modifier = 'SHIFT'; - - } - - const mouseOp = this.getOpFromAction( 'WHEEL', modifier ); - - if ( mouseOp != null ) { - - event.preventDefault(); - this.dispatchEvent( _startEvent ); - - const notchDeltaY = 125; //distance of one notch of mouse wheel - let sgn = event.deltaY / notchDeltaY; - - let size = 1; - - if ( sgn > 0 ) { - - size = 1 / this.scaleFactor; - - } else if ( sgn < 0 ) { - - size = this.scaleFactor; - + + this._mouseOp = this.getOpFromAction( event.button, modifier ); + + if ( this._mouseOp != null ) { + + window.addEventListener( 'pointermove', this.onPointerMove ); + window.addEventListener( 'pointerup', this.onPointerUp ); //singleStart + + this._input = INPUT.CURSOR; + this._button = event.button; + this.onSinglePanStart( event, this._mouseOp ); + } - - switch ( mouseOp ) { - - case 'ZOOM': - - this.updateTbState( STATE.SCALE, true ); - - if ( sgn > 0 ) { - - size = 1 / ( Math.pow( this.scaleFactor, sgn ) ); - - } else if ( sgn < 0 ) { - - size = Math.pow( this.scaleFactor, -sgn ); - - } - - if ( this.cursorZoom && this.enablePan ) { - - let scalePoint; - - if ( this.camera.isOrthographicCamera ) { - - scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar (1 / this.camera.zoom ).add( this._gizmos.position ); - - } else if ( this.camera.isPerspectiveCamera ) { - - scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position ); - - } - - this.applyTransformMatrix( this.scale( size, scalePoint ) ); - - } else { - - this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); - - } - - if ( this._grid != null ) { - - this.disposeGrid(); - this.drawGrid(); - - } - - this.updateTbState( STATE.IDLE, false ); - - this.dispatchEvent( _changeEvent ); - this.dispatchEvent( _endEvent ); - + + } + + }; + + this.onPointerMove = event => { + + if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { + + switch ( this._input ) { + + case INPUT.ONE_FINGER: + //singleMove + this.updateTouchEvent( event ); + this.onSinglePanMove( event, STATE.ROTATE ); break; - - case 'FOV': - - if ( this.camera.isPerspectiveCamera ) { - - this.updateTbState( STATE.FOV, true ); - - - //Vertigo effect - - // fov / 2 - // |\ - // | \ - // | \ - // x | \ - // | \ - // | \ - // | _ _ _\ - // y - - //check for iOs shift shortcut - if ( event.deltaX != 0 ) { - - sgn = event.deltaX / notchDeltaY; - - size = 1; - - if ( sgn > 0 ) { - - size = 1 / ( Math.pow( this.scaleFactor, sgn ) ); - - } else if ( sgn < 0 ) { - - size = Math.pow( this.scaleFactor, -sgn ); - - } - - } - - this._v3_1.setFromMatrixPosition(this._cameraMatrixState); - const x = this._v3_1.distanceTo(this._gizmos.position); - let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed - - //check min and max distance - xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); - - const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this.camera.fov * 0.5 ); - - //calculate new fov - let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); - - //check min and max fov - if ( newFov > this.maxFov ) { - - newFov = this.maxFov; - - } else if ( newFov < this.minFov ) { - - newFov = this.minFov; - - } - - const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); - size = x / newDistance; - - this.setFov( newFov ); - this.applyTransformMatrix( this.scale( size, this._gizmos.position, false ) ); - - } - - if ( this._grid != null) { - - this.disposeGrid(); - this.drawGrid(); - + + case INPUT.ONE_FINGER_SWITCHED: + const movement = this.calculatePointersDistance( this._touchCurrent[ 0 ], event ) * this._devPxRatio; + + if ( movement >= this._switchSensibility ) { + + //singleMove + this._input = INPUT.ONE_FINGER; + this.updateTouchEvent( event ); + this.onSinglePanStart( event, 'ROTATE' ); + break; + } - - this.updateTbState( STATE.IDLE, false ); - - this.dispatchEvent( _changeEvent ); - this.dispatchEvent( _endEvent ); - + + break; + + case INPUT.TWO_FINGER: + //rotate/pan/pinchMove + this.updateTouchEvent( event ); + this.onRotateMove(); + this.onPinchMove(); + this.onDoublePanMove(); + break; + + case INPUT.MULT_FINGER: + //multMove + this.updateTouchEvent( event ); + this.onTriplePanMove( event ); break; - + } - - } - - } - - }; - - onKeyDown = ( event ) => { - - if ( event.key == 'c' ) { - - if ( event.ctrlKey || event.metaKey) { - - this.copyState(); - - } - - } else if ( event.key == 'v' ) { - - if ( event.ctrlKey || event.metaKey) { - - this.pasteState(); - - } - - } - - }; - - onSinglePanStart = ( event, operation ) => { - - if ( this.enabled ) { - - this.dispatchEvent( _startEvent ); - - this.setCenter( event.clientX, event.clientY ); - - switch ( operation ) { - - case 'PAN': - - if ( !this.enablePan ) { - - return; - - } - - if (this._animationId != -1) { - - cancelAnimationFrame( this._animationId ); - this._animationId = -1; - this._timeStart = -1; - - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - - } - - this.updateTbState( STATE.PAN, true ); - this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); - if ( this.enableGrid ) { - - this.drawGrid(); - this.dispatchEvent( _changeEvent ); - - } - - break; - - case 'ROTATE': - - if ( !this.enableRotate ) { - - return; - - } - - if (this._animationId != -1) { - - cancelAnimationFrame( this._animationId ); - this._animationId = -1; - this._timeStart = -1; - - } - - this.updateTbState( STATE.ROTATE, true ); - this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); - this.activateGizmos( true ); - if ( this.enableAnimations ) { - - this._timePrev = this._timeCurrent = performance.now(); - this._angleCurrent = this._anglePrev = 0; - this._cursorPosPrev.copy( this._startCursorPosition ); - this._cursorPosCurr.copy( this._cursorPosPrev ); - this._wCurr = 0; - this._wPrev = this._wCurr; - + + } else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) { + + let modifier = null; + + if ( event.ctrlKey || event.metaKey ) { + + modifier = 'CTRL'; + + } else if ( event.shiftKey ) { + + modifier = 'SHIFT'; + + } + + const mouseOpState = this.getOpStateFromAction( this._button, modifier ); + + if ( mouseOpState != null ) { + + this.onSinglePanMove( event, mouseOpState ); + + } + + } //checkDistance + + + if ( this._downValid ) { + + const movement = this.calculatePointersDistance( this._downEvents[ this._downEvents.length - 1 ], event ) * this._devPxRatio; + + if ( movement > this._movementThreshold ) { + + this._downValid = false; + + } + + } + + }; + + this.onPointerUp = event => { + + if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) { + + const nTouch = this._touchCurrent.length; + + for ( let i = 0; i < nTouch; i ++ ) { + + if ( this._touchCurrent[ i ].pointerId == event.pointerId ) { + + this._touchCurrent.splice( i, 1 ); + + this._touchStart.splice( i, 1 ); + + break; + } - - this.dispatchEvent( _changeEvent ); - break; - - case 'FOV': - - if ( !this.camera.isPerspectiveCamera || !this.enableZoom ) { - - return; - - } - - if ( this._animationId != -1 ) { - - cancelAnimationFrame( this._animationId ); - this._animationId = -1; - this._timeStart = -1; - - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - - } - - this.updateTbState( STATE.FOV, true ); - this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - this._currentCursorPosition.copy( this._startCursorPosition ); + + } + + switch ( this._input ) { + + case INPUT.ONE_FINGER: + case INPUT.ONE_FINGER_SWITCHED: + //singleEnd + window.removeEventListener( 'pointermove', this.onPointerMove ); + window.removeEventListener( 'pointerup', this.onPointerUp ); + this._input = INPUT.NONE; + this.onSinglePanEnd(); break; - - case 'ZOOM': - - if ( !this.enableZoom ) { - - return; - - } - - if ( this._animationId != -1 ) { - - cancelAnimationFrame( this._animationId ); - this._animationId = -1; - this._timeStart = -1; - - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - + + case INPUT.TWO_FINGER: + //doubleEnd + this.onDoublePanEnd( event ); + this.onPinchEnd( event ); + this.onRotateEnd( event ); //switching to singleStart + + this._input = INPUT.ONE_FINGER_SWITCHED; + break; + + case INPUT.MULT_FINGER: + if ( this._touchCurrent.length == 0 ) { + + window.removeEventListener( 'pointermove', this.onPointerMove ); + window.removeEventListener( 'pointerup', this.onPointerUp ); //multCancel + + this._input = INPUT.NONE; + this.onTriplePanEnd(); + } - - this.updateTbState( STATE.SCALE, true ); - this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - this._currentCursorPosition.copy( this._startCursorPosition ); + break; + + } + + } else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) { + + window.removeEventListener( 'pointermove', this.onPointerMove ); + window.removeEventListener( 'pointerup', this.onPointerUp ); + this._input = INPUT.NONE; + this.onSinglePanEnd(); + this._button = - 1; + } - - } - - }; - - onSinglePanMove = ( event ) => { - - if ( this.enabled ) { - - const restart = opState != this._state; - this.setCenter( event.clientX, event.clientY ); - - switch ( opState ) { - - case STATE.PAN: - - if ( this.enablePan ) { - - if ( restart ) { - - //switch to pan operation - - this.dispatchEvent( _endEvent ); - this.dispatchEvent( _startEvent ); - - this.updateTbState( opState, true ); - this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); - if ( this.enableGrid ) { - - this.drawGrid(); - - } - - this.activateGizmos( false ); - + + if ( event.isPrimary ) { + + if ( this._downValid ) { + + const downTime = event.timeStamp - this._downEvents[ this._downEvents.length - 1 ].timeStamp; + + if ( downTime <= this._maxDownTime ) { + + if ( this._nclicks == 0 ) { + + //first valid click detected + this._nclicks = 1; + this._clickStart = performance.now(); + } else { - - //continue with pan operation - this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); - this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) ); - + + const clickInterval = event.timeStamp - this._clickStart; + + const movement = this.calculatePointersDistance( this._downEvents[ 1 ], this._downEvents[ 0 ] ) * this._devPxRatio; + + if ( clickInterval <= this._maxInterval && movement <= this._posThreshold ) { + + //second valid click detected + //fire double tap and reset values + this._nclicks = 0; + + this._downEvents.splice( 0, this._downEvents.length ); + + this.onDoubleTap( event ); + + } else { + + //new 'first click' + this._nclicks = 1; + + this._downEvents.shift(); + + this._clickStart = performance.now(); + + } + } - + + } else { + + this._downValid = false; + this._nclicks = 0; + + this._downEvents.splice( 0, this._downEvents.length ); + } - - break; - - case STATE.ROTATE: - - if ( this.enableRotate ) { - - if ( restart ) { - - //switch to rotate operation - - this.dispatchEvent( _endEvent ); - this.dispatchEvent( _startEvent ); - - this.updateTbState( opState, true ); - this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); - - if ( this.enableGrid ) { - - this.disposeGrid(); - + + } else { + + this._nclicks = 0; + + this._downEvents.splice( 0, this._downEvents.length ); + + } + + } + + }; + + this.onWheel = event => { + + if ( this.enabled && this.enableZoom ) { + + let modifier = null; + + if ( event.ctrlKey || event.metaKey ) { + + modifier = 'CTRL'; + + } else if ( event.shiftKey ) { + + modifier = 'SHIFT'; + + } + + const mouseOp = this.getOpFromAction( 'WHEEL', modifier ); + + if ( mouseOp != null ) { + + event.preventDefault(); + this.dispatchEvent( _startEvent ); + const notchDeltaY = 125; //distance of one notch of mouse wheel + + let sgn = event.deltaY / notchDeltaY; + let size = 1; + + if ( sgn > 0 ) { + + size = 1 / this.scaleFactor; + + } else if ( sgn < 0 ) { + + size = this.scaleFactor; + + } + + switch ( mouseOp ) { + + case 'ZOOM': + this.updateTbState( STATE.SCALE, true ); + + if ( sgn > 0 ) { + + size = 1 / Math.pow( this.scaleFactor, sgn ); + + } else if ( sgn < 0 ) { + + size = Math.pow( this.scaleFactor, - sgn ); + } - - this.activateGizmos( true ); - - } else { - - //continue with rotate operation - this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); - - const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition ); - const angle = this._startCursorPosition.angleTo( this._currentCursorPosition ); - const amount = Math.max( distance / this._tbRadius, angle ); //effective rotation angle - - this.applyTransformMatrix( this.rotate( this.calculateRotationAxis( this._startCursorPosition, this._currentCursorPosition ), amount ) ); - - if ( this.enableAnimations ) { - - this._timePrev = this._timeCurrent; - this._timeCurrent = performance.now(); - this._anglePrev = this._angleCurrent; - this._angleCurrent = amount; - this._cursorPosPrev.copy( this._cursorPosCurr ); - this._cursorPosCurr.copy( this._currentCursorPosition ); - this._wPrev = this._wCurr; - this._wCurr = this.calculateAngularSpeed( this._anglePrev, this._angleCurrent, this._timePrev, this._timeCurrent ); - + + if ( this.cursorZoom && this.enablePan ) { + + let scalePoint; + + if ( this.camera.isOrthographicCamera ) { + + scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._gizmos.position ); + + } else if ( this.camera.isPerspectiveCamera ) { + + scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position ); + + } + + this.applyTransformMatrix( this.scale( size, scalePoint ) ); + + } else { + + this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); + } - - } - - } - - break; - - case STATE.SCALE: - - if ( this.enableZoom ) { - - if ( restart ) { - - //switch to zoom operation - - this.dispatchEvent( _endEvent ); - this.dispatchEvent( _startEvent ); - - this.updateTbState( opState, true ); - this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - this._currentCursorPosition.copy( this._startCursorPosition ); - - if ( this.enableGrid ) { - + + if ( this._grid != null ) { + this.disposeGrid(); - - } - - this.activateGizmos( false ); - - } else { - - //continue with zoom operation - const screenNotches = 8; //how many wheel notches corresponds to a full screen pan - this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - - let movement = this._currentCursorPosition.y - this._startCursorPosition.y; - - let size = 1 ; - - if ( movement < 0 ) { - - size = 1 / ( Math.pow( this.scaleFactor, -movement * screenNotches ) ); - - } else if ( movement > 0 ) { - - size = Math.pow( this.scaleFactor, movement * screenNotches ); - + this.drawGrid(); + } - - this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); - - } - - } - - break; - - case STATE.FOV: - - if ( this.enableZoom && this.camera.isPerspectiveCamera ) { - - if ( restart ) { - - //switch to fov operation - + + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _changeEvent ); this.dispatchEvent( _endEvent ); - this.dispatchEvent( _startEvent ); - - this.updateTbState( opState, true ); - this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - this._currentCursorPosition.copy( this._startCursorPosition ); - - if ( this.enableGrid ) { - - this.disposeGrid(); - + break; + + case 'FOV': + if ( this.camera.isPerspectiveCamera ) { + + this.updateTbState( STATE.FOV, true ); //Vertigo effect + // fov / 2 + // |\ + // | \ + // | \ + // x | \ + // | \ + // | \ + // | _ _ _\ + // y + //check for iOs shift shortcut + + if ( event.deltaX != 0 ) { + + sgn = event.deltaX / notchDeltaY; + size = 1; + + if ( sgn > 0 ) { + + size = 1 / Math.pow( this.scaleFactor, sgn ); + + } else if ( sgn < 0 ) { + + size = Math.pow( this.scaleFactor, - sgn ); + + } + + } + + this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); + + const x = this._v3_1.distanceTo( this._gizmos.position ); + + let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed + //check min and max distance + + xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); + const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this.camera.fov * 0.5 ); //calculate new fov + + let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); //check min and max fov + + if ( newFov > this.maxFov ) { + + newFov = this.maxFov; + + } else if ( newFov < this.minFov ) { + + newFov = this.minFov; + + } + + const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); + size = x / newDistance; + this.setFov( newFov ); + this.applyTransformMatrix( this.scale( size, this._gizmos.position, false ) ); + } - - this.activateGizmos( false ); - - } else { - - //continue with fov operation - const screenNotches = 8; //how many wheel notches corresponds to a full screen pan - this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - - let movement = this._currentCursorPosition.y - this._startCursorPosition.y; - - let size = 1 ; - - if ( movement < 0 ) { - - size = 1 / ( Math.pow( this.scaleFactor, -movement * screenNotches ) ); - - } else if ( movement > 0 ) { - - size = Math.pow( this.scaleFactor, movement * screenNotches ); - + + if ( this._grid != null ) { + + this.disposeGrid(); + this.drawGrid(); + } - - this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); - const x = this._v3_1.distanceTo( this._gizmos.position ); - let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed - - //check min and max distance - xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); - - const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this._fovState * 0.5 ); - - //calculate new fov - let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); - - //check min and max fov - newFov = THREE.MathUtils.clamp( newFov, this.minFov, this.maxFov ); - - const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); - size = x / newDistance; - this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); - - this.setFov( newFov ); - this.applyTransformMatrix( this.scale( size, this._v3_2, false ) ); - - //adjusting distance - let direction = this._gizmos.position.clone().sub(this.camera.position).normalize().multiplyScalar( newDistance / x ); - this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); - - } - + + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _changeEvent ); + this.dispatchEvent( _endEvent ); + break; + } - - break; - - } - - this.dispatchEvent( _changeEvent ); - - } - - }; - - onSinglePanEnd = ( event ) => { - - if ( this._state == STATE.ROTATE ) { - - - if ( !this.enableRotate ) { - - return; - - } - - if ( this.enableAnimations ) { - - //perform rotation animation - const deltaTime = ( performance.now() - this._timeCurrent ); - if ( deltaTime < 120 ) { - - let w = Math.abs( ( this._wPrev + this._wCurr ) / 2 ); - - const self = this; - this._animationId = window.requestAnimationFrame( function( t ) { - - self.updateTbState( STATE.ANIMATION_ROTATE, true ); - const rotationAxis = self.calculateRotationAxis( self._cursorPosPrev, self._cursorPosCurr ); - - self.onRotationAnim( t, rotationAxis, Math.min( w, self.wMax ) ); - - } ); - - } else { - - //cursor has been standing still for over 120 ms since last movement - this.updateTbState( STATE.IDLE, false ); - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - + } - - } else { - - this.updateTbState( STATE.IDLE, false ); - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - - } - - } else if ( this._state == STATE.PAN || this._state == STATE.IDLE ) { - - this.updateTbState( STATE.IDLE, false ); - - if ( this.enableGrid ) { - - this.disposeGrid(); - + } - - this.activateGizmos( false ); - this.dispatchEvent( _changeEvent ); - - - } - - this.dispatchEvent( _endEvent ); - - }; - - onDoubleTap = ( event ) => { - - if ( this.enabled && this.enablePan && this.scene != null ) { - - this.dispatchEvent( _startEvent ); - - this.setCenter( event.clientX, event.clientY ); - const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera ); - - if ( hitP != null && this.enableAnimations ) { - - const self = this; - if ( this._animationId != -1) { - - window.cancelAnimationFrame( this._animationId ); - + + }; + + this.onKeyDown = event => { + + if ( event.key == 'c' ) { + + if ( event.ctrlKey || event.metaKey ) { + + this.copyState(); + } - - this._timeStart = -1; - this._animationId = window.requestAnimationFrame( function( t ) { - - self.updateTbState( STATE.ANIMATION_FOCUS, true ); - self.onFocusAnim( t, hitP, self._cameraMatrixState, self._gizmoMatrixState ); - - } ); - - } else if ( hitP != null && !this.enableAnimations ) { - - this.updateTbState( STATE.FOCUS, true ); - this.focus( hitP, this.scaleFactor ); - this.updateTbState( STATE.IDLE, false ); - this.dispatchEvent (_changeEvent ); - + + } else if ( event.key == 'v' ) { + + if ( event.ctrlKey || event.metaKey ) { + + this.pasteState(); + + } + } - - } - - this.dispatchEvent( _endEvent ); - - }; - - onDoublePanStart = ( event ) => { - - if ( this.enabled && this.enablePan ) { - - this.dispatchEvent( _startEvent ); - - this.updateTbState( STATE.PAN, true ); - - this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); - this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) ); - this._currentCursorPosition.copy( this._startCursorPosition ); - - this.activateGizmos( false ); - - } - - }; - - onDoublePanMove = ( event ) => { - - if( this.enabled && this.enablePan ) { - - this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); - - if ( this._state != STATE.PAN ) { - + + }; + + this.onSinglePanStart = ( event, operation ) => { + + if ( this.enabled ) { + + this.dispatchEvent( _startEvent ); + this.setCenter( event.clientX, event.clientY ); + + switch ( operation ) { + + case 'PAN': + if ( ! this.enablePan ) { + + return; + + } + + if ( this._animationId != - 1 ) { + + cancelAnimationFrame( this._animationId ); + this._animationId = - 1; + this._timeStart = - 1; + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + this.updateTbState( STATE.PAN, true ); + + this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); + + if ( this.enableGrid ) { + + this.drawGrid(); + this.dispatchEvent( _changeEvent ); + + } + + break; + + case 'ROTATE': + if ( ! this.enableRotate ) { + + return; + + } + + if ( this._animationId != - 1 ) { + + cancelAnimationFrame( this._animationId ); + this._animationId = - 1; + this._timeStart = - 1; + + } + + this.updateTbState( STATE.ROTATE, true ); + + this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); + + this.activateGizmos( true ); + + if ( this.enableAnimations ) { + + this._timePrev = this._timeCurrent = performance.now(); + this._angleCurrent = this._anglePrev = 0; + + this._cursorPosPrev.copy( this._startCursorPosition ); + + this._cursorPosCurr.copy( this._cursorPosPrev ); + + this._wCurr = 0; + this._wPrev = this._wCurr; + + } + + this.dispatchEvent( _changeEvent ); + break; + + case 'FOV': + if ( ! this.camera.isPerspectiveCamera || ! this.enableZoom ) { + + return; + + } + + if ( this._animationId != - 1 ) { + + cancelAnimationFrame( this._animationId ); + this._animationId = - 1; + this._timeStart = - 1; + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + this.updateTbState( STATE.FOV, true ); + + this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + break; + + case 'ZOOM': + if ( ! this.enableZoom ) { + + return; + + } + + if ( this._animationId != - 1 ) { + + cancelAnimationFrame( this._animationId ); + this._animationId = - 1; + this._timeStart = - 1; + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + this.updateTbState( STATE.SCALE, true ); + + this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + break; + + } + + } + + }; + + this.onSinglePanMove = ( event, opState ) => { + + if ( this.enabled ) { + + const restart = opState != this._state; + this.setCenter( event.clientX, event.clientY ); + + switch ( opState ) { + + case STATE.PAN: + if ( this.enablePan ) { + + if ( restart ) { + + //switch to pan operation + this.dispatchEvent( _endEvent ); + this.dispatchEvent( _startEvent ); + this.updateTbState( opState, true ); + + this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); + + if ( this.enableGrid ) { + + this.drawGrid(); + + } + + this.activateGizmos( false ); + + } else { + + //continue with pan operation + this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) ); + + this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) ); + + } + + } + + break; + + case STATE.ROTATE: + if ( this.enableRotate ) { + + if ( restart ) { + + //switch to rotate operation + this.dispatchEvent( _endEvent ); + this.dispatchEvent( _startEvent ); + this.updateTbState( opState, true ); + + this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); + + if ( this.enableGrid ) { + + this.disposeGrid(); + + } + + this.activateGizmos( true ); + + } else { + + //continue with rotate operation + this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) ); + + const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition ); + + const angle = this._startCursorPosition.angleTo( this._currentCursorPosition ); + + const amount = Math.max( distance / this._tbRadius, angle ); //effective rotation angle + + this.applyTransformMatrix( this.rotate( this.calculateRotationAxis( this._startCursorPosition, this._currentCursorPosition ), amount ) ); + + if ( this.enableAnimations ) { + + this._timePrev = this._timeCurrent; + this._timeCurrent = performance.now(); + this._anglePrev = this._angleCurrent; + this._angleCurrent = amount; + + this._cursorPosPrev.copy( this._cursorPosCurr ); + + this._cursorPosCurr.copy( this._currentCursorPosition ); + + this._wPrev = this._wCurr; + this._wCurr = this.calculateAngularSpeed( this._anglePrev, this._angleCurrent, this._timePrev, this._timeCurrent ); + + } + + } + + } + + break; + + case STATE.SCALE: + if ( this.enableZoom ) { + + if ( restart ) { + + //switch to zoom operation + this.dispatchEvent( _endEvent ); + this.dispatchEvent( _startEvent ); + this.updateTbState( opState, true ); + + this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + if ( this.enableGrid ) { + + this.disposeGrid(); + + } + + this.activateGizmos( false ); + + } else { + + //continue with zoom operation + const screenNotches = 8; //how many wheel notches corresponds to a full screen pan + + this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + const movement = this._currentCursorPosition.y - this._startCursorPosition.y; + let size = 1; + + if ( movement < 0 ) { + + size = 1 / Math.pow( this.scaleFactor, - movement * screenNotches ); + + } else if ( movement > 0 ) { + + size = Math.pow( this.scaleFactor, movement * screenNotches ); + + } + + this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); + + } + + } + + break; + + case STATE.FOV: + if ( this.enableZoom && this.camera.isPerspectiveCamera ) { + + if ( restart ) { + + //switch to fov operation + this.dispatchEvent( _endEvent ); + this.dispatchEvent( _startEvent ); + this.updateTbState( opState, true ); + + this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + if ( this.enableGrid ) { + + this.disposeGrid(); + + } + + this.activateGizmos( false ); + + } else { + + //continue with fov operation + const screenNotches = 8; //how many wheel notches corresponds to a full screen pan + + this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + const movement = this._currentCursorPosition.y - this._startCursorPosition.y; + let size = 1; + + if ( movement < 0 ) { + + size = 1 / Math.pow( this.scaleFactor, - movement * screenNotches ); + + } else if ( movement > 0 ) { + + size = Math.pow( this.scaleFactor, movement * screenNotches ); + + } + + this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); + + const x = this._v3_1.distanceTo( this._gizmos.position ); + + let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed + //check min and max distance + + xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); + const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this._fovState * 0.5 ); //calculate new fov + + let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); //check min and max fov + + newFov = THREE.MathUtils.clamp( newFov, this.minFov, this.maxFov ); + const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); + size = x / newDistance; + + this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); + + this.setFov( newFov ); + this.applyTransformMatrix( this.scale( size, this._v3_2, false ) ); //adjusting distance + + const direction = this._gizmos.position.clone().sub( this.camera.position ).normalize().multiplyScalar( newDistance / x ); + + this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); + + } + + } + + break; + + } + + this.dispatchEvent( _changeEvent ); + + } + + }; + + this.onSinglePanEnd = () => { + + if ( this._state == STATE.ROTATE ) { + + if ( ! this.enableRotate ) { + + return; + + } + + if ( this.enableAnimations ) { + + //perform rotation animation + const deltaTime = performance.now() - this._timeCurrent; + + if ( deltaTime < 120 ) { + + const w = Math.abs( ( this._wPrev + this._wCurr ) / 2 ); + const self = this; + this._animationId = window.requestAnimationFrame( function ( t ) { + + self.updateTbState( STATE.ANIMATION_ROTATE, true ); + const rotationAxis = self.calculateRotationAxis( self._cursorPosPrev, self._cursorPosCurr ); + self.onRotationAnim( t, rotationAxis, Math.min( w, self.wMax ) ); + + } ); + + } else { + + //cursor has been standing still for over 120 ms since last movement + this.updateTbState( STATE.IDLE, false ); + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + } else { + + this.updateTbState( STATE.IDLE, false ); + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + } else if ( this._state == STATE.PAN || this._state == STATE.IDLE ) { + + this.updateTbState( STATE.IDLE, false ); + + if ( this.enableGrid ) { + + this.disposeGrid(); + + } + + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + this.dispatchEvent( _endEvent ); + + }; + + this.onDoubleTap = event => { + + if ( this.enabled && this.enablePan && this.scene != null ) { + + this.dispatchEvent( _startEvent ); + this.setCenter( event.clientX, event.clientY ); + const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera ); + + if ( hitP != null && this.enableAnimations ) { + + const self = this; + + if ( this._animationId != - 1 ) { + + window.cancelAnimationFrame( this._animationId ); + + } + + this._timeStart = - 1; + this._animationId = window.requestAnimationFrame( function ( t ) { + + self.updateTbState( STATE.ANIMATION_FOCUS, true ); + self.onFocusAnim( t, hitP, self._cameraMatrixState, self._gizmoMatrixState ); + + } ); + + } else if ( hitP != null && ! this.enableAnimations ) { + + this.updateTbState( STATE.FOCUS, true ); + this.focus( hitP, this.scaleFactor ); + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _changeEvent ); + + } + + } + + this.dispatchEvent( _endEvent ); + + }; + + this.onDoublePanStart = () => { + + if ( this.enabled && this.enablePan ) { + + this.dispatchEvent( _startEvent ); this.updateTbState( STATE.PAN, true ); - this._startCursorPosition.copy( this._currentCursorPosition ); - + this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); + + this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + this.activateGizmos( false ); + + } + + }; + + this.onDoublePanMove = () => { + + if ( this.enabled && this.enablePan ) { + + this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); + + if ( this._state != STATE.PAN ) { + + this.updateTbState( STATE.PAN, true ); + + this._startCursorPosition.copy( this._currentCursorPosition ); + + } + + this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) ); + + this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) ); + this.dispatchEvent( _changeEvent ); + + } + + }; + + this.onDoublePanEnd = () => { + + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _endEvent ); + + }; + + this.onRotateStart = () => { + + if ( this.enabled && this.enableRotate ) { + + this.dispatchEvent( _startEvent ); + this.updateTbState( STATE.ZROTATE, true ); //this._startFingerRotation = event.rotation; + + this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] ); + this._currentFingerRotation = this._startFingerRotation; + this.camera.getWorldDirection( this._rotationAxis ); //rotation axis + + if ( ! this.enablePan && ! this.enableZoom ) { + + this.activateGizmos( true ); + + } + + } + + }; + + this.onRotateMove = () => { + + if ( this.enabled && this.enableRotate ) { + + this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); + let rotationPoint; + + if ( this._state != STATE.ZROTATE ) { + + this.updateTbState( STATE.ZROTATE, true ); + this._startFingerRotation = this._currentFingerRotation; + + } //this._currentFingerRotation = event.rotation; + + + this._currentFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] ); + + if ( ! this.enablePan ) { + + rotationPoint = new THREE.Vector3().setFromMatrixPosition( this._gizmoMatrixState ); + + } else { + + this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); + + rotationPoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._v3_2 ); + + } + + const amount = THREE.MathUtils.DEG2RAD * ( this._startFingerRotation - this._currentFingerRotation ); + this.applyTransformMatrix( this.zRotate( rotationPoint, amount ) ); + this.dispatchEvent( _changeEvent ); + + } + + }; + + this.onRotateEnd = () => { + + this.updateTbState( STATE.IDLE, false ); + this.activateGizmos( false ); + this.dispatchEvent( _endEvent ); + + }; + + this.onPinchStart = () => { + + if ( this.enabled && this.enableZoom ) { + + this.dispatchEvent( _startEvent ); + this.updateTbState( STATE.SCALE, true ); + this._startFingerDistance = this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ); + this._currentFingerDistance = this._startFingerDistance; + this.activateGizmos( false ); + + } + + }; + + this.onPinchMove = () => { + + if ( this.enabled && this.enableZoom ) { + + this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); + const minDistance = 12; //minimum distance between fingers (in css pixels) + + if ( this._state != STATE.SCALE ) { + + this._startFingerDistance = this._currentFingerDistance; + this.updateTbState( STATE.SCALE, true ); + + } + + this._currentFingerDistance = Math.max( this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ), minDistance * this._devPxRatio ); + const amount = this._currentFingerDistance / this._startFingerDistance; + let scalePoint; + + if ( ! this.enablePan ) { + + scalePoint = this._gizmos.position; + + } else { + + if ( this.camera.isOrthographicCamera ) { + + scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._gizmos.position ); + + } else if ( this.camera.isPerspectiveCamera ) { + + scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position ); + + } + + } + + this.applyTransformMatrix( this.scale( amount, scalePoint ) ); + this.dispatchEvent( _changeEvent ); + + } + + }; + + this.onPinchEnd = () => { + + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _endEvent ); + + }; + + this.onTriplePanStart = () => { + + if ( this.enabled && this.enableZoom ) { + + this.dispatchEvent( _startEvent ); + this.updateTbState( STATE.SCALE, true ); //const center = event.center; + + let clientX = 0; + let clientY = 0; + const nFingers = this._touchCurrent.length; + + for ( let i = 0; i < nFingers; i ++ ) { + + clientX += this._touchCurrent[ i ].clientX; + clientY += this._touchCurrent[ i ].clientY; + + } + + this.setCenter( clientX / nFingers, clientY / nFingers ); + + this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + this._currentCursorPosition.copy( this._startCursorPosition ); + + } + + }; + + this.onTriplePanMove = () => { + + if ( this.enabled && this.enableZoom ) { + + // fov / 2 + // |\ + // | \ + // | \ + // x | \ + // | \ + // | \ + // | _ _ _\ + // y + //const center = event.center; + let clientX = 0; + let clientY = 0; + const nFingers = this._touchCurrent.length; + + for ( let i = 0; i < nFingers; i ++ ) { + + clientX += this._touchCurrent[ i ].clientX; + clientY += this._touchCurrent[ i ].clientY; + + } + + this.setCenter( clientX / nFingers, clientY / nFingers ); + const screenNotches = 8; //how many wheel notches corresponds to a full screen pan + + this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); + + const movement = this._currentCursorPosition.y - this._startCursorPosition.y; + let size = 1; + + if ( movement < 0 ) { + + size = 1 / Math.pow( this.scaleFactor, - movement * screenNotches ); + + } else if ( movement > 0 ) { + + size = Math.pow( this.scaleFactor, movement * screenNotches ); + + } + + this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); + + const x = this._v3_1.distanceTo( this._gizmos.position ); + + let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed + //check min and max distance + + xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); + const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this._fovState * 0.5 ); //calculate new fov + + let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); //check min and max fov + + newFov = THREE.MathUtils.clamp( newFov, this.minFov, this.maxFov ); + const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); + size = x / newDistance; + + this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); + + this.setFov( newFov ); + this.applyTransformMatrix( this.scale( size, this._v3_2, false ) ); //adjusting distance + + const direction = this._gizmos.position.clone().sub( this.camera.position ).normalize().multiplyScalar( newDistance / x ); + + this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); + + this.dispatchEvent( _changeEvent ); + + } + + }; + + this.onTriplePanEnd = () => { + + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _endEvent ); //this.dispatchEvent( _changeEvent ); + + }; + + this.setCenter = ( clientX, clientY ) => { + + _center.x = clientX; + _center.y = clientY; + + }; + + this.initializeMouseActions = () => { + + this.setMouseAction( 'PAN', 0, 'CTRL' ); + this.setMouseAction( 'PAN', 2 ); + this.setMouseAction( 'ROTATE', 0 ); + this.setMouseAction( 'ZOOM', 'WHEEL' ); + this.setMouseAction( 'ZOOM', 1 ); + this.setMouseAction( 'FOV', 'WHEEL', 'SHIFT' ); + this.setMouseAction( 'FOV', 1, 'SHIFT' ); + + }; + + this.compareMouseAction = ( action1, action2 ) => { + + if ( action1.operation == action2.operation ) { + + if ( action1.mouse == action2.mouse && action1.key == action2.key ) { + + return true; + + } else { + + return false; + + } + + } else { + + return false; + + } + + }; + + this.setMouseAction = ( operation, mouse, key = null ) => { + + const operationInput = [ 'PAN', 'ROTATE', 'ZOOM', 'FOV' ]; + const mouseInput = [ 0, 1, 2, 'WHEEL' ]; + const keyInput = [ 'CTRL', 'SHIFT', null ]; + let state; + + if ( ! operationInput.includes( operation ) || ! mouseInput.includes( mouse ) || ! keyInput.includes( key ) ) { + + //invalid parameters + return false; + + } + + if ( mouse == 'WHEEL' ) { + + if ( operation != 'ZOOM' && operation != 'FOV' ) { + + //cannot associate 2D operation to 1D input + return false; + + } + + } + + switch ( operation ) { + + case 'PAN': + state = STATE.PAN; + break; + + case 'ROTATE': + state = STATE.ROTATE; + break; + + case 'ZOOM': + state = STATE.SCALE; + break; + + case 'FOV': + state = STATE.FOV; + break; + + } + + const action = { + operation: operation, + mouse: mouse, + key: key, + state: state + }; + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + if ( this.mouseActions[ i ].mouse == action.mouse && this.mouseActions[ i ].key == action.key ) { + + this.mouseActions.splice( i, 1, action ); + return true; + + } + + } + + this.mouseActions.push( action ); + return true; + + }; + + this.unsetMouseAction = ( mouse, key = null ) => { + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + if ( this.mouseActions[ i ].mouse == mouse && this.mouseActions[ i ].key == key ) { + + this.mouseActions.splice( i, 1 ); + return true; + + } + + } + + return false; + + }; + + this.getOpFromAction = ( mouse, key ) => { + + let action; + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + action = this.mouseActions[ i ]; + + if ( action.mouse == mouse && action.key == key ) { + + return action.operation; + + } + + } + + if ( key != null ) { + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + action = this.mouseActions[ i ]; + + if ( action.mouse == mouse && action.key == null ) { + + return action.operation; + + } + + } + + } + + return null; + + }; + + this.getOpStateFromAction = ( mouse, key ) => { + + let action; + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + action = this.mouseActions[ i ]; + + if ( action.mouse == mouse && action.key == key ) { + + return action.state; + + } + + } + + if ( key != null ) { + + for ( let i = 0; i < this.mouseActions.length; i ++ ) { + + action = this.mouseActions[ i ]; + + if ( action.mouse == mouse && action.key == null ) { + + return action.state; + + } + + } + + } + + return null; + + }; + + this.getAngle = ( p1, p2 ) => { + + return Math.atan2( p2.clientY - p1.clientY, p2.clientX - p1.clientX ) * 180 / Math.PI; + + }; + + this.updateTouchEvent = event => { + + for ( let i = 0; i < this._touchCurrent.length; i ++ ) { + + if ( this._touchCurrent[ i ].pointerId == event.pointerId ) { + + this._touchCurrent.splice( i, 1, event ); + + break; + + } + + } + + }; + + this.calculateAngularSpeed = ( p0, p1, t0, t1 ) => { + + const s = p1 - p0; + const t = ( t1 - t0 ) / 1000; + + if ( t == 0 ) { + + return 0; + + } + + return s / t; + + }; + + this.calculatePointersDistance = ( p0, p1 ) => { + + return Math.sqrt( Math.pow( p1.clientX - p0.clientX, 2 ) + Math.pow( p1.clientY - p0.clientY, 2 ) ); + + }; + + this.calculateRotationAxis = ( vec1, vec2 ) => { + + this._rotationMatrix.extractRotation( this._cameraMatrixState ); + + this._quat.setFromRotationMatrix( this._rotationMatrix ); + + this._rotationAxis.crossVectors( vec1, vec2 ).applyQuaternion( this._quat ); + + return this._rotationAxis.normalize().clone(); + + }; + + this.calculateTbRadius = camera => { + + const factor = 0.67; + const distance = camera.position.distanceTo( this._gizmos.position ); + + if ( camera.type == 'PerspectiveCamera' ) { + + const halfFovV = THREE.MathUtils.DEG2RAD * camera.fov * 0.5; //vertical fov/2 in radians + + const halfFovH = Math.atan( camera.aspect * Math.tan( halfFovV ) ); //horizontal fov/2 in radians + + return Math.tan( Math.min( halfFovV, halfFovH ) ) * distance * factor; + + } else if ( camera.type == 'OrthographicCamera' ) { + + return Math.min( camera.top, camera.right ) * factor; + + } + + }; + + this.focus = ( point, size, amount = 1 ) => { + + const focusPoint = point.clone(); //move center of camera (along with gizmos) towards point of interest + + focusPoint.sub( this._gizmos.position ).multiplyScalar( amount ); + + this._translationMatrix.makeTranslation( focusPoint.x, focusPoint.y, focusPoint.z ); + + const gizmoStateTemp = this._gizmoMatrixState.clone(); + + this._gizmoMatrixState.premultiply( this._translationMatrix ); + + this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + const cameraStateTemp = this._cameraMatrixState.clone(); + + this._cameraMatrixState.premultiply( this._translationMatrix ); + + this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); //apply zoom + + + if ( this.enableZoom ) { + + this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); + + } + + this._gizmoMatrixState.copy( gizmoStateTemp ); + + this._cameraMatrixState.copy( cameraStateTemp ); + + }; + + this.drawGrid = () => { + + if ( this.scene != null ) { + + const color = 0x888888; + const multiplier = 3; + let size, divisions, maxLength, tick; + + if ( this.camera.isOrthographicCamera ) { + + const width = this.camera.right - this.camera.left; + const height = this.camera.bottom - this.camera.top; + maxLength = Math.max( width, height ); + tick = maxLength / 20; + size = maxLength / this.camera.zoom * multiplier; + divisions = size / tick * this.camera.zoom; + + } else if ( this.camera.isPerspectiveCamera ) { + + const distance = this.camera.position.distanceTo( this._gizmos.position ); + const halfFovV = THREE.MathUtils.DEG2RAD * this.camera.fov * 0.5; + const halfFovH = Math.atan( this.camera.aspect * Math.tan( halfFovV ) ); + maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2; + tick = maxLength / 20; + size = maxLength * multiplier; + divisions = size / tick; + + } + + if ( this._grid == null ) { + + this._grid = new THREE.GridHelper( size, divisions, color, color ); + + this._grid.position.copy( this._gizmos.position ); + + this._gridPosition.copy( this._grid.position ); + + this._grid.quaternion.copy( this.camera.quaternion ); + + this._grid.rotateX( Math.PI * 0.5 ); + + this.scene.add( this._grid ); + + } + + } + + }; + + this.dispose = () => { + + if ( this._animationId != - 1 ) { + + window.cancelAnimationFrame( this._animationId ); + + } + + this.domElement.removeEventListener( 'pointerdown', this.onPointerDown ); + this.domElement.removeEventListener( 'pointercancel', this.onPointerCancel ); + this.domElement.removeEventListener( 'wheel', this.onWheel ); + this.domElement.removeEventListener( 'contextmenu', this.onContextMenu ); + window.removeEventListener( 'pointermove', this.onPointerMove ); + window.removeEventListener( 'pointerup', this.onPointerUp ); + window.removeEventListener( 'resize', this.onWindowResize ); + window.addEventListener( 'keydown', this.onKeyDown ); + this.scene.remove( this._gizmos ); + this.disposeGrid(); + + }; + + this.disposeGrid = () => { + + if ( this._grid != null && this.scene != null ) { + + this.scene.remove( this._grid ); + this._grid = null; + + } + + }; + + this.easeOutCubic = t => { + + return 1 - Math.pow( 1 - t, 3 ); + + }; + + this.activateGizmos = isActive => { + + const gizmoX = this._gizmos.children[ 0 ]; + const gizmoY = this._gizmos.children[ 1 ]; + const gizmoZ = this._gizmos.children[ 2 ]; + + if ( isActive ) { + + gizmoX.material.setValues( { + opacity: 1 + } ); + gizmoY.material.setValues( { + opacity: 1 + } ); + gizmoZ.material.setValues( { + opacity: 1 + } ); + + } else { + + gizmoX.material.setValues( { + opacity: 0.6 + } ); + gizmoY.material.setValues( { + opacity: 0.6 + } ); + gizmoZ.material.setValues( { + opacity: 0.6 + } ); + + } + + }; + + this.getCursorNDC = ( cursorX, cursorY, canvas ) => { + + const canvasRect = canvas.getBoundingClientRect(); + + this._v2_1.setX( ( cursorX - canvasRect.left ) / canvasRect.width * 2 - 1 ); + + this._v2_1.setY( ( canvasRect.bottom - cursorY ) / canvasRect.height * 2 - 1 ); + + return this._v2_1.clone(); + + }; + + this.getCursorPosition = ( cursorX, cursorY, canvas ) => { + + this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) ); + + this._v2_1.x *= ( this.camera.right - this.camera.left ) * 0.5; + this._v2_1.y *= ( this.camera.top - this.camera.bottom ) * 0.5; + return this._v2_1.clone(); + + }; + + this.setCamera = camera => { + + camera.lookAt( this._tbCenter ); + camera.updateMatrix(); //setting state + + if ( camera.type == 'PerspectiveCamera' ) { + + this._fov0 = camera.fov; + this._fovState = camera.fov; + + } + + this._cameraMatrixState0.copy( camera.matrix ); + + this._cameraMatrixState.copy( this._cameraMatrixState0 ); + + this._cameraProjectionState.copy( camera.projectionMatrix ); + + this._zoom0 = camera.zoom; + this._zoomState = this._zoom0; + this._initialNear = camera.near; + this._nearPos0 = camera.position.distanceTo( this._tbCenter ) - camera.near; + this._nearPos = this._initialNear; + this._initialFar = camera.far; + this._farPos0 = camera.position.distanceTo( this._tbCenter ) - camera.far; + this._farPos = this._initialFar; + + this._up0.copy( camera.up ); + + this._upState.copy( camera.up ); + + this.camera = camera; + this.camera.updateProjectionMatrix(); //making gizmos + + this._tbRadius = this.calculateTbRadius( camera ); + this.makeGizmos( this._tbCenter, this._tbRadius ); + + }; + + this.makeGizmos = ( tbCenter, tbRadius ) => { + + const curve = new THREE.EllipseCurve( 0, 0, tbRadius, tbRadius ); + const points = curve.getPoints( this._curvePts ); //geometry + + const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); //material + + const curveMaterialX = new THREE.LineBasicMaterial( { + color: 0xff8080, + fog: false, + transparent: true, + opacity: 0.6 + } ); + const curveMaterialY = new THREE.LineBasicMaterial( { + color: 0x80ff80, + fog: false, + transparent: true, + opacity: 0.6 + } ); + const curveMaterialZ = new THREE.LineBasicMaterial( { + color: 0x8080ff, + fog: false, + transparent: true, + opacity: 0.6 + } ); //line + + const gizmoX = new THREE.Line( curveGeometry, curveMaterialX ); + const gizmoY = new THREE.Line( curveGeometry, curveMaterialY ); + const gizmoZ = new THREE.Line( curveGeometry, curveMaterialZ ); + const rotation = Math.PI * 0.5; + gizmoX.rotation.x = rotation; + gizmoY.rotation.y = rotation; //setting state + + this._gizmoMatrixState0.identity().setPosition( tbCenter ); + + this._gizmoMatrixState.copy( this._gizmoMatrixState0 ); + + if ( this.camera.zoom != 1 ) { + + //adapt gizmos size to camera zoom + const size = 1 / this.camera.zoom; + + this._scaleMatrix.makeScale( size, size, size ); + + this._translationMatrix.makeTranslation( - tbCenter.x, - tbCenter.y, - tbCenter.z ); + + this._gizmoMatrixState.premultiply( this._translationMatrix ).premultiply( this._scaleMatrix ); + + this._translationMatrix.makeTranslation( tbCenter.x, tbCenter.y, tbCenter.z ); + + this._gizmoMatrixState.premultiply( this._translationMatrix ); + + } + + this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + this._gizmos.clear(); + + this._gizmos.add( gizmoX ); + + this._gizmos.add( gizmoY ); + + this._gizmos.add( gizmoZ ); + + }; + + this.onFocusAnim = ( time, point, cameraMatrix, gizmoMatrix ) => { + + if ( this._timeStart == - 1 ) { + + //animation start + this._timeStart = time; + + } + + if ( this._state == STATE.ANIMATION_FOCUS ) { + + const deltaTime = time - this._timeStart; + const animTime = deltaTime / this.focusAnimationTime; + + this._gizmoMatrixState.copy( gizmoMatrix ); + + if ( animTime >= 1 ) { + + //animation end + this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + this.focus( point, this.scaleFactor ); + this._timeStart = - 1; + this.updateTbState( STATE.IDLE, false ); + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } else { + + const amount = this.easeOutCubic( animTime ); + const size = 1 - amount + this.scaleFactor * amount; + + this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + this.focus( point, size, amount ); + this.dispatchEvent( _changeEvent ); + const self = this; + this._animationId = window.requestAnimationFrame( function ( t ) { + + self.onFocusAnim( t, point, cameraMatrix, gizmoMatrix.clone() ); + + } ); + + } + + } else { + + //interrupt animation + this._animationId = - 1; + this._timeStart = - 1; + + } + + }; + + this.onRotationAnim = ( time, rotationAxis, w0 ) => { + + if ( this._timeStart == - 1 ) { + + //animation start + this._anglePrev = 0; + this._angleCurrent = 0; + this._timeStart = time; + + } + + if ( this._state == STATE.ANIMATION_ROTATE ) { + + //w = w0 + alpha * t + const deltaTime = ( time - this._timeStart ) / 1000; + const w = w0 + - this.dampingFactor * deltaTime; + + if ( w > 0 ) { + + //tetha = 0.5 * alpha * t^2 + w0 * t + tetha0 + this._angleCurrent = 0.5 * - this.dampingFactor * Math.pow( deltaTime, 2 ) + w0 * deltaTime + 0; + this.applyTransformMatrix( this.rotate( rotationAxis, this._angleCurrent ) ); + this.dispatchEvent( _changeEvent ); + const self = this; + this._animationId = window.requestAnimationFrame( function ( t ) { + + self.onRotationAnim( t, rotationAxis, w0 ); + + } ); + + } else { + + this._animationId = - 1; + this._timeStart = - 1; + this.updateTbState( STATE.IDLE, false ); + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + } else { + + //interrupt animation + this._animationId = - 1; + this._timeStart = - 1; + + if ( this._state != STATE.ROTATE ) { + + this.activateGizmos( false ); + this.dispatchEvent( _changeEvent ); + + } + + } + + }; + + this.pan = ( p0, p1, adjust = false ) => { + + const movement = p0.clone().sub( p1 ); + + if ( this.camera.isOrthographicCamera ) { + + //adjust movement amount + movement.multiplyScalar( 1 / this.camera.zoom ); + + } else if ( this.camera.isPerspectiveCamera && adjust ) { + + //adjust movement amount + this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ); //camera's initial position + + + this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ); //gizmo's initial position + + + const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.camera.position.distanceTo( this._gizmos.position ); + movement.multiplyScalar( 1 / distanceFactor ); + + } + + this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion ); + + this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ); + + this.setTransformationMatrices( this._m4_1, this._m4_1 ); + return _transformation; + + }; + + this.reset = () => { + + this.camera.zoom = this._zoom0; + + if ( this.camera.isPerspectiveCamera ) { + + this.camera.fov = this._fov0; + } - - this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) ); - this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) ); + + this.camera.near = this._nearPos; + this.camera.far = this._farPos; + + this._cameraMatrixState.copy( this._cameraMatrixState0 ); + + this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); + + this.camera.up.copy( this._up0 ); + this.camera.updateMatrix(); + this.camera.updateProjectionMatrix(); + + this._gizmoMatrixState.copy( this._gizmoMatrixState0 ); + + this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + this._gizmos.updateMatrix(); + + this._tbRadius = this.calculateTbRadius( this.camera ); + this.makeGizmos( this._gizmos.position, this._tbRadius ); + this.camera.lookAt( this._gizmos.position ); + this.updateTbState( STATE.IDLE, false ); this.dispatchEvent( _changeEvent ); - } - - }; - - onDoublePanEnd = ( event ) => { - - this.updateTbState( STATE.IDLE, false ); - this.dispatchEvent( _endEvent ); - - }; - - - onRotateStart = ( event ) => { - - if ( this.enabled && this.enableRotate ) { - - this.dispatchEvent( _startEvent ); - - this.updateTbState( STATE.ZROTATE, true ); - - //this._startFingerRotation = event.rotation; - - this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] ); - this._currentFingerRotation = this._startFingerRotation; - - this.camera.getWorldDirection( this._rotationAxis ); //rotation axis - - if ( !this.enablePan && !this.enableZoom ) { - - this.activateGizmos( true ); - + + }; + + this.rotate = ( axis, angle ) => { + + const point = this._gizmos.position; //rotation center + + this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z ); + + this._rotationMatrix.makeRotationAxis( axis, - angle ); //rotate camera + + + this._m4_1.makeTranslation( point.x, point.y, point.z ); + + this._m4_1.multiply( this._rotationMatrix ); + + this._m4_1.multiply( this._translationMatrix ); + + this.setTransformationMatrices( this._m4_1 ); + return _transformation; + + }; + + this.copyState = () => { + + let state; + + if ( this.camera.isOrthographicCamera ) { + + state = JSON.stringify( { + arcballState: { + cameraFar: this.camera.far, + cameraMatrix: this.camera.matrix, + cameraNear: this.camera.near, + cameraUp: this.camera.up, + cameraZoom: this.camera.zoom, + gizmoMatrix: this._gizmos.matrix + } + } ); + + } else if ( this.camera.isPerspectiveCamera ) { + + state = JSON.stringify( { + arcballState: { + cameraFar: this.camera.far, + cameraFov: this.camera.fov, + cameraMatrix: this.camera.matrix, + cameraNear: this.camera.near, + cameraUp: this.camera.up, + cameraZoom: this.camera.zoom, + gizmoMatrix: this._gizmos.matrix + } + } ); + } - - } - - }; - - onRotateMove = ( event ) => { - - if ( this.enabled && this.enableRotate ) { - - this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); - let rotationPoint; - - if ( this._state != STATE.ZROTATE ) { - - this.updateTbState( STATE.ZROTATE, true ); - this._startFingerRotation = this._currentFingerRotation; - + + navigator.clipboard.writeText( state ); + + }; + + this.pasteState = () => { + + const self = this; + navigator.clipboard.readText().then( function resolved( value ) { + + self.setStateFromJSON( value ); + + } ); + + }; + + this.saveState = () => { + + this._cameraMatrixState0.copy( this.camera.matrix ); + + this._gizmoMatrixState0.copy( this._gizmos.matrix ); + + this._nearPos = this.camera.near; + this._farPos = this.camera.far; + this._zoom0 = this.camera.zoom; + + this._up0.copy( this.camera.up ); + + if ( this.camera.isPerspectiveCamera ) { + + this._fov0 = this.camera.fov; + + } + + }; + + this.scale = ( size, point, scaleGizmos = true ) => { + + const scalePoint = point.clone(); + let sizeInverse = 1 / size; + + if ( this.camera.isOrthographicCamera ) { + + //camera zoom + this.camera.zoom = this._zoomState; + this.camera.zoom *= size; //check min and max zoom + + if ( this.camera.zoom > this.maxZoom ) { + + this.camera.zoom = this.maxZoom; + sizeInverse = this._zoomState / this.maxZoom; + + } else if ( this.camera.zoom < this.minZoom ) { + + this.camera.zoom = this.minZoom; + sizeInverse = this._zoomState / this.minZoom; + + } + + this.camera.updateProjectionMatrix(); + + this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ); //gizmos position + //scale gizmos so they appear in the same spot having the same dimension + + + this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse ); + + this._translationMatrix.makeTranslation( - this._v3_1.x, - this._v3_1.y, - this._v3_1.z ); + + this._m4_2.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ).multiply( this._scaleMatrix ); + + this._m4_2.multiply( this._translationMatrix ); //move camera and gizmos to obtain pinch effect + + + scalePoint.sub( this._v3_1 ); + const amount = scalePoint.clone().multiplyScalar( sizeInverse ); + scalePoint.sub( amount ); + + this._m4_1.makeTranslation( scalePoint.x, scalePoint.y, scalePoint.z ); + + this._m4_2.premultiply( this._m4_1 ); + + this.setTransformationMatrices( this._m4_1, this._m4_2 ); + return _transformation; + + } else if ( this.camera.isPerspectiveCamera ) { + + this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); + + this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); //move camera + + + let distance = this._v3_1.distanceTo( scalePoint ); + + let amount = distance - distance * sizeInverse; //check min and max distance + + const newDistance = distance - amount; + + if ( newDistance < this.minDistance ) { + + sizeInverse = this.minDistance / distance; + amount = distance - distance * sizeInverse; + + } else if ( newDistance > this.maxDistance ) { + + sizeInverse = this.maxDistance / distance; + amount = distance - distance * sizeInverse; + + } + + let direction = scalePoint.clone().sub( this._v3_1 ).normalize().multiplyScalar( amount ); + + this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); + + if ( scaleGizmos ) { + + //scale gizmos so they appear in the same spot having the same dimension + const pos = this._v3_2; + distance = pos.distanceTo( scalePoint ); + amount = distance - distance * sizeInverse; + direction = scalePoint.clone().sub( this._v3_2 ).normalize().multiplyScalar( amount ); + + this._translationMatrix.makeTranslation( pos.x, pos.y, pos.z ); + + this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse ); + + this._m4_2.makeTranslation( direction.x, direction.y, direction.z ).multiply( this._translationMatrix ); + + this._m4_2.multiply( this._scaleMatrix ); + + this._translationMatrix.makeTranslation( - pos.x, - pos.y, - pos.z ); + + this._m4_2.multiply( this._translationMatrix ); + + this.setTransformationMatrices( this._m4_1, this._m4_2 ); + + } else { + + this.setTransformationMatrices( this._m4_1 ); + + } + + return _transformation; + } - - //this._currentFingerRotation = event.rotation; - this._currentFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] ); - - if ( !this.enablePan ) { - - rotationPoint = new THREE.Vector3().setFromMatrixPosition( this._gizmoMatrixState ); - - } else { - - this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); - rotationPoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._v3_2 ); - + + }; + + this.setFov = value => { + + if ( this.camera.isPerspectiveCamera ) { + + this.camera.fov = THREE.MathUtils.clamp( value, this.minFov, this.maxFov ); + this.camera.updateProjectionMatrix(); + } - - const amount = THREE.MathUtils.DEG2RAD * ( this._startFingerRotation - this._currentFingerRotation ); - - this.applyTransformMatrix( this.zRotate( rotationPoint, amount ) ); - this.dispatchEvent( _changeEvent ); - - } - - }; - - onRotateEnd = () => { - - this.updateTbState( STATE.IDLE, false ); - this.activateGizmos( false ); - this.dispatchEvent( _endEvent ); - - }; - - onPinchStart = ( event ) => { - - if ( this.enabled && this.enableZoom ) { - - this.dispatchEvent( _startEvent ); - this.updateTbState( STATE.SCALE, true ); - - this._startFingerDistance = this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ); - this._currentFingerDistance = this._startFingerDistance; - - this.activateGizmos( false ); - - } - - }; - - onPinchMove = ( event ) => { - - if ( this.enabled && this.enableZoom ) { - - this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 ); - const minDistance = 12; //minimum distance between fingers (in css pixels) - - if ( this._state != STATE.SCALE ) { - - this._startFingerDistance = this._currentFingerDistance; - this.updateTbState( STATE.SCALE, true ); - + + }; + + this.setTarget = ( x, y, z ) => { + + this._tbCenter.set( x, y, z ); + + this._gizmos.position.set( x, y, z ); //for correct radius calculation + + + this._tbRadius = this.calculateTbRadius( this.camera ); + this.makeGizmos( this._tbCenter, this._tbRadius ); + this.camera.lookAt( this._tbCenter ); + + }; + + this.zRotate = ( point, angle ) => { + + this._rotationMatrix.makeRotationAxis( this._rotationAxis, angle ); + + this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z ); + + this._m4_1.makeTranslation( point.x, point.y, point.z ); + + this._m4_1.multiply( this._rotationMatrix ); + + this._m4_1.multiply( this._translationMatrix ); + + this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ).sub( point ); //vector from rotation center to gizmos position + + + this._v3_2.copy( this._v3_1 ).applyAxisAngle( this._rotationAxis, angle ); //apply rotation + + + this._v3_2.sub( this._v3_1 ); + + this._m4_2.makeTranslation( this._v3_2.x, this._v3_2.y, this._v3_2.z ); + + this.setTransformationMatrices( this._m4_1, this._m4_2 ); + return _transformation; + + }; + + this.unprojectOnObj = ( cursor, camera ) => { + + const raycaster = new THREE.Raycaster(); + raycaster.near = camera.near; + raycaster.far = camera.far; + raycaster.setFromCamera( cursor, camera ); + const intersect = raycaster.intersectObjects( this.scene.children, true ); + + for ( let i = 0; i < intersect.length; i ++ ) { + + if ( intersect[ i ].object.uuid != this._gizmos.uuid && intersect[ i ].face != null ) { + + return intersect[ i ].point.clone(); + + } + } - - this._currentFingerDistance = Math.max( this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ), minDistance * this._devPxRatio ); - const amount = this._currentFingerDistance / this._startFingerDistance; - - let scalePoint; - - if ( !this.enablePan ) { - - scalePoint = this._gizmos.position; - - } else { - - if ( this.camera.isOrthographicCamera ) { - - scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) - .applyQuaternion( this.camera.quaternion ) - .multiplyScalar( 1 / this.camera.zoom ) - .add( this._gizmos.position ); - + + return null; + + }; + + this.unprojectOnTbSurface = ( camera, cursorX, cursorY, canvas, tbRadius ) => { + + if ( camera.type == 'OrthographicCamera' ) { + + this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) ); + + this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 ); + + const x2 = Math.pow( this._v2_1.x, 2 ); + const y2 = Math.pow( this._v2_1.y, 2 ); + const r2 = Math.pow( this._tbRadius, 2 ); + + if ( x2 + y2 <= r2 * 0.5 ) { + + //intersection with sphere + this._v3_1.setZ( Math.sqrt( r2 - ( x2 + y2 ) ) ); + + } else { + + //intersection with hyperboloid + this._v3_1.setZ( r2 * 0.5 / Math.sqrt( x2 + y2 ) ); + } - else if ( this.camera.isPerspectiveCamera ) { - - scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) - .applyQuaternion( this.camera.quaternion ) - .add( this._gizmos.position ); - + + return this._v3_1; + + } else if ( camera.type == 'PerspectiveCamera' ) { + + //unproject cursor on the near plane + this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) ); + + this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 ); + + this._v3_1.applyMatrix4( camera.projectionMatrixInverse ); + + const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction + + + const cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position ); + const radius2 = Math.pow( tbRadius, 2 ); // camera + // |\ + // | \ + // | \ + // h | \ + // | \ + // | \ + // _ _ | _ _ _\ _ _ near plane + // l + + const h = this._v3_1.z; + const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) ); + + if ( l == 0 ) { + + //ray aligned with camera + rayDir.set( this._v3_1.x, this._v3_1.y, tbRadius ); + return rayDir; + } - - } - - this.applyTransformMatrix( this.scale( amount, scalePoint ) ); - this.dispatchEvent( _changeEvent ); - } - }; - - onPinchEnd = () => { - - this.updateTbState( STATE.IDLE, false ); - this.dispatchEvent( _endEvent ); - - }; - - onTriplePanStart = ( event ) => { - - if ( this.enabled && this.enableZoom ) { - - this.dispatchEvent( _startEvent ); - - this.updateTbState( STATE.SCALE, true ); - - //const center = event.center; - let clientX = 0; - let clientY = 0; - const nFingers = this._touchCurrent.length; - - for ( let i = 0; i < nFingers; i++ ) { - - clientX += this._touchCurrent[ i ].clientX; - clientY += this._touchCurrent[ i ].clientY; - + + const m = h / l; + const q = cameraGizmoDistance; + /* + * calculate intersection point between unprojected ray and trackball surface + *|y = m * x + q + *|x^2 + y^2 = r^2 + * + * (m^2 + 1) * x^2 + (2 * m * q) * x + q^2 - r^2 = 0 + */ + + let a = Math.pow( m, 2 ) + 1; + let b = 2 * m * q; + let c = Math.pow( q, 2 ) - radius2; + let delta = Math.pow( b, 2 ) - 4 * a * c; + + if ( delta >= 0 ) { + + //intersection with sphere + this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) ); + + this._v2_1.setY( m * this._v2_1.x + q ); + + const angle = THREE.MathUtils.RAD2DEG * this._v2_1.angle(); + + if ( angle >= 45 ) { + + //if angle between intersection point and X' axis is >= 45°, return that point + //otherwise, calculate intersection point with hyperboloid + const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( cameraGizmoDistance - this._v2_1.y, 2 ) ); + rayDir.multiplyScalar( rayLength ); + rayDir.z += cameraGizmoDistance; + return rayDir; + + } + + } //intersection with hyperboloid + + /* + *|y = m * x + q + *|y = (1 / x) * (r^2 / 2) + * + * m * x^2 + q * x - r^2 / 2 = 0 + */ + + + a = m; + b = q; + c = - radius2 * 0.5; + delta = Math.pow( b, 2 ) - 4 * a * c; + + this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) ); + + this._v2_1.setY( m * this._v2_1.x + q ); + + const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( cameraGizmoDistance - this._v2_1.y, 2 ) ); + rayDir.multiplyScalar( rayLength ); + rayDir.z += cameraGizmoDistance; + return rayDir; + } - - this.setCenter( clientX / nFingers, clientY / nFingers ); - - this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - this._currentCursorPosition.copy( this._startCursorPosition ); - - } - - }; - - onTriplePanMove = ( event ) => { - - if ( this.enabled && this.enableZoom ) { - - // fov / 2 + + }; + + this.unprojectOnTbPlane = ( camera, cursorX, cursorY, canvas, initialDistance = false ) => { + + if ( camera.type == 'OrthographicCamera' ) { + + this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) ); + + this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 ); + + return this._v3_1.clone(); + + } else if ( camera.type == 'PerspectiveCamera' ) { + + this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) ); //unproject cursor on the near plane + + + this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 ); + + this._v3_1.applyMatrix4( camera.projectionMatrixInverse ); + + const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction + // camera // |\ // | \ // | \ - // x | \ + // h | \ // | \ // | \ - // | _ _ _\ - // y - - //const center = event.center; - let clientX = 0; - let clientY = 0; - const nFingers = this._touchCurrent.length; - - for ( let i = 0; i < nFingers; i++ ) { - - clientX += this._touchCurrent[ i ].clientX; - clientY += this._touchCurrent[ i ].clientY; - - } - - this.setCenter( clientX / nFingers, clientY / nFingers ); - - const screenNotches = 8; //how many wheel notches corresponds to a full screen pan - this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 ); - - let movement = this._currentCursorPosition.y - this._startCursorPosition.y; - - let size = 1 ; - - if ( movement < 0 ) { - - size = 1 / ( Math.pow( this.scaleFactor, -movement * screenNotches ) ); - - } else if ( movement > 0 ) { - - size = Math.pow( this.scaleFactor, movement * screenNotches ); - - } - - this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); - const x = this._v3_1.distanceTo( this._gizmos.position ); - let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed - - //check min and max distance - xNew = THREE.MathUtils.clamp( xNew, this.minDistance, this.maxDistance ); - - const y = x * Math.tan( THREE.MathUtils.DEG2RAD * this._fovState * 0.5 ); - - //calculate new fov - let newFov = THREE.MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 ); - - //check min and max fov - newFov = THREE.MathUtils.clamp( newFov, this.minFov, this.maxFov ); - - const newDistance = y / Math.tan( THREE.MathUtils.DEG2RAD * ( newFov / 2 ) ); - size = x / newDistance; - this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); - - this.setFov( newFov ); - this.applyTransformMatrix( this.scale( size, this._v3_2, false ) ); - - //adjusting distance - let direction = this._gizmos.position.clone().sub(this.camera.position).normalize().multiplyScalar( newDistance / x ); - this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); - - this.dispatchEvent( _changeEvent ); - - } - - }; - - onTriplePanEnd = ( event ) => { - - this.updateTbState( STATE.IDLE, false ); - this.dispatchEvent( _endEvent ); - //this.dispatchEvent( _changeEvent ); - - }; - - /** - * Set _center's x/y coordinates - * @param {Number} clientX - * @param {Number} clientY - */ - setCenter = ( clientX, clientY ) => { - - _center.x = clientX; - _center.y = clientY; - - }; - - /** - * Set default mouse actions - */ - initializeMouseActions = () => { - - this.setMouseAction( 'PAN', 0, 'CTRL' ); - this.setMouseAction( 'PAN', 2 ); - - this.setMouseAction( 'ROTATE', 0 ); - - this.setMouseAction( 'ZOOM', 'WHEEL' ); - this.setMouseAction( 'ZOOM', 1 ); - - this.setMouseAction( 'FOV', 'WHEEL', 'SHIFT' ); - this.setMouseAction( 'FOV', 1, 'SHIFT' ); - - }; + // _ _ | _ _ _\ _ _ near plane + // l - /** - * Compare two mouse actions - * @param {Object} action1 - * @param {Object} action2 - * @returns {Boolean} True if action1 and action 2 are the same mouse action, false otherwise - */ - compareMouseAction = ( action1, action2 ) => { - if ( action1.operation == action2.operation ) { + const h = this._v3_1.z; + const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) ); + let cameraGizmoDistance; - if ( action1.mouse == action2.mouse && action1.key == action2.key ) { + if ( initialDistance ) { - return true; + cameraGizmoDistance = this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ).distanceTo( this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ) ); - } else { + } else { - return false; + cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position ); + + } + /* + * calculate intersection point between unprojected ray and the plane + *|y = mx + q + *|y = 0 + * + * x = -q/m + */ + + + if ( l == 0 ) { + + //ray aligned with camera + rayDir.set( 0, 0, 0 ); + return rayDir; + + } + + const m = h / l; + const q = cameraGizmoDistance; + const x = - q / m; + const rayLength = Math.sqrt( Math.pow( q, 2 ) + Math.pow( x, 2 ) ); + rayDir.multiplyScalar( rayLength ); + rayDir.z = 0; + return rayDir; } - } else { + }; - return false; + this.updateMatrixState = () => { - } + //update camera and gizmos state + this._cameraMatrixState.copy( this.camera.matrix ); - }; + this._gizmoMatrixState.copy( this._gizmos.matrix ); - /** - * Set a new mouse action by specifying the operation to be performed and a mouse/key combination. In case of conflict, replaces the existing one - * @param {String} operation The operation to be performed ('PAN', 'ROTATE', 'ZOOM', 'FOV) - * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches - * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed - * @returns {Boolean} True if the mouse action has been successfully added, false otherwise - */ - setMouseAction = ( operation, mouse, key = null ) => { - - const operationInput = [ 'PAN', 'ROTATE', 'ZOOM', 'FOV' ]; - const mouseInput = [ 0, 1, 2, 'WHEEL' ]; - const keyInput = [ 'CTRL', 'SHIFT', null ]; - let state; - - if ( !operationInput.includes( operation ) || !mouseInput.includes( mouse ) || !keyInput.includes( key ) ) { - - //invalid parameters - return false; + if ( this.camera.isOrthographicCamera ) { - } + this._cameraProjectionState.copy( this.camera.projectionMatrix ); - if ( mouse == 'WHEEL' ) { + this.camera.updateProjectionMatrix(); + this._zoomState = this.camera.zoom; - if ( operation != 'ZOOM' && operation != 'FOV' ) { + } else if ( this.camera.isPerspectiveCamera ) { - //cannot associate 2D operation to 1D input - return false + this._fovState = this.camera.fov; } - } + }; - switch ( operation ) { + this.updateTbState = ( newState, updateMatrices ) => { - case 'PAN': + this._state = newState; - state = STATE.PAN; - break; - - case 'ROTATE': + if ( updateMatrices ) { - state = STATE.ROTATE; - break; + this.updateMatrixState(); - case 'ZOOM': + } - state = STATE.SCALE; - break; - - case 'FOV': + }; - state = STATE.FOV; - break; + this.update = () => { - } + const EPS = 0.000001; //check min/max parameters + + if ( this.camera.isOrthographicCamera ) { + + //check zoom + if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) { + + const newZoom = THREE.MathUtils.clamp( this.camera.zoom, this.minZoom, this.maxZoom ); + this.applyTransformMatrix( this.scale( newZoom / this.camera.zoom, this._gizmos.position, true ) ); + + } + + } else if ( this.camera.isPerspectiveCamera ) { + + //check distance + const distance = this.camera.position.distanceTo( this._gizmos.position ); + + if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) { + + const newDistance = THREE.MathUtils.clamp( distance, this.minDistance, this.maxDistance ); + this.applyTransformMatrix( this.scale( newDistance / distance, this._gizmos.position ) ); + this.updateMatrixState(); + + } //check fov + + + if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) { + + this.camera.fov = THREE.MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov ); + this.camera.updateProjectionMatrix(); + + } + + const oldRadius = this._tbRadius; + this._tbRadius = this.calculateTbRadius( this.camera ); - const action = { + if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) { - operation: operation, - mouse: mouse, - key: key, - state: state + const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3; + const newRadius = this._tbRadius / scale; + const curve = new THREE.EllipseCurve( 0, 0, newRadius, newRadius ); + const points = curve.getPoints( this._curvePts ); + const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); + + for ( const gizmo in this._gizmos.children ) { + + this._gizmos.children[ gizmo ].geometry = curveGeometry; + + } + + } + + } + + this.camera.lookAt( this._gizmos.position ); }; - for( let i = 0; i < this.mouseActions.length; i++ ) { + this.setStateFromJSON = json => { + + const state = JSON.parse( json ); + + if ( state.arcballState != undefined ) { + + this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements ); + + this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); + + this.camera.up.copy( state.arcballState.cameraUp ); + this.camera.near = state.arcballState.cameraNear; + this.camera.far = state.arcballState.cameraFar; + this.camera.zoom = state.arcballState.cameraZoom; + + if ( this.camera.isPerspectiveCamera ) { - if ( this.mouseActions[ i ].mouse == action.mouse && this.mouseActions[ i ].key == action.key ) { + this.camera.fov = state.arcballState.cameraFov; - this.mouseActions.splice( i, 1, action ); - return true; + } + + this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements ); + + this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + + this.camera.updateMatrix(); + this.camera.updateProjectionMatrix(); + + this._gizmos.updateMatrix(); + + this._tbRadius = this.calculateTbRadius( this.camera ); + const gizmoTmp = new THREE.Matrix4().copy( this._gizmoMatrixState0 ); + this.makeGizmos( this._gizmos.position, this._tbRadius ); + + this._gizmoMatrixState0.copy( gizmoTmp ); + + this.camera.lookAt( this._gizmos.position ); + this.updateTbState( STATE.IDLE, false ); + this.dispatchEvent( _changeEvent ); } - } + }; + + this.camera = null; + this.domElement = domElement; + this.scene = scene; + this.mouseActions = []; + this._mouseOp = null; //global vectors and matrices that are used in some operations to avoid creating new objects every time (e.g. every time cursor moves) + + this._v2_1 = new THREE.Vector2(); + this._v3_1 = new THREE.Vector3(); + this._v3_2 = new THREE.Vector3(); + this._m4_1 = new THREE.Matrix4(); + this._m4_2 = new THREE.Matrix4(); + this._quat = new THREE.Quaternion(); //transformation matrices - this.mouseActions.push( action ); - return true; + this._translationMatrix = new THREE.Matrix4(); //matrix for translation operation - }; + this._rotationMatrix = new THREE.Matrix4(); //matrix for rotation operation - /** - * Remove a mouse action by specifying its mouse/key combination - * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches - * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed - * @returns {Boolean} True if the operation has been succesfully removed, false otherwise - */ - unsetMouseAction = ( mouse, key = null ) => { + this._scaleMatrix = new THREE.Matrix4(); //matrix for scaling operation + + this._rotationAxis = new THREE.Vector3(); //axis for rotate operation + //camera state + + this._cameraMatrixState = new THREE.Matrix4(); + this._cameraProjectionState = new THREE.Matrix4(); + this._fovState = 1; + this._upState = new THREE.Vector3(); + this._zoomState = 1; + this._nearPos = 0; + this._farPos = 0; + this._gizmoMatrixState = new THREE.Matrix4(); //initial values + + this._up0 = new THREE.Vector3(); + this._zoom0 = 1; + this._fov0 = 0; + this._initialNear = 0; + this._nearPos0 = 0; + this._initialFar = 0; + this._farPos0 = 0; + this._cameraMatrixState0 = new THREE.Matrix4(); + this._gizmoMatrixState0 = new THREE.Matrix4(); //pointers array + + this._button = - 1; + this._touchStart = []; + this._touchCurrent = []; + this._input = INPUT.NONE; //two fingers touch interaction - for( let i = 0; i < this.mouseActions.length; i++ ) { + this._switchSensibility = 32; //minimum movement to be performed to fire single pan start after the second finger has been released - if ( this.mouseActions[ i ].mouse == mouse && this.mouseActions[ i ].key == key ) { + this._startFingerDistance = 0; //distance between two fingers - this.mouseActions.splice( i, 1 ); - return true; + this._currentFingerDistance = 0; + this._startFingerRotation = 0; //amount of rotation performed with two fingers - } + this._currentFingerRotation = 0; //double tap - } + this._devPxRatio = 0; + this._downValid = true; + this._nclicks = 0; + this._downEvents = []; + this._downStart = 0; //pointerDown time - return false; + this._clickStart = 0; //first click time - }; + this._maxDownTime = 250; + this._maxInterval = 300; + this._posThreshold = 24; + this._movementThreshold = 24; //cursor positions - /** - * Return the operation associated to a mouse/keyboard combination - * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches - * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed - * @returns The operation if it has been found, null otherwise - */ - getOpFromAction = ( mouse, key ) => { + this._currentCursorPosition = new THREE.Vector3(); + this._startCursorPosition = new THREE.Vector3(); //grid - let action; + this._grid = null; //grid to be visualized during pan operation - for( let i = 0; i < this.mouseActions.length; i++ ) { + this._gridPosition = new THREE.Vector3(); //gizmos - action = this.mouseActions[ i ]; - if( action.mouse == mouse && action.key == key ) { + this._gizmos = new THREE.Group(); + this._curvePts = 128; //animations - return action.operation; + this._timeStart = - 1; //initial time - } + this._animationId = - 1; //focus animation - } + this.focusAnimationTime = 500; //duration of focus animation in ms + //rotate animation - if ( key != null ) { + this._timePrev = 0; //time at which previous rotate operation has been detected - for( let i = 0; i < this.mouseActions.length; i++ ) { + this._timeCurrent = 0; //time at which current rotate operation has been detected - action = this.mouseActions[ i ]; - if ( action.mouse == mouse && action.key == null ) { - - return action.operation; - - } - - } + this._anglePrev = 0; //angle of previous rotation - } + this._angleCurrent = 0; //angle of current rotation - return null; + this._cursorPosPrev = new THREE.Vector3(); //cursor position when previous rotate operation has been detected - }; + this._cursorPosCurr = new THREE.Vector3(); //cursor position when current rotate operation has been detected - /** - * Get the operation associated to mouse and key combination and returns the corresponding FSA state - * @param {Number} mouse Mouse button - * @param {String} key Keyboard modifier - * @returns The FSA state obtained from the operation associated to mouse/keyboard combination - */ - getOpStateFromAction = ( mouse, key ) => { + this._wPrev = 0; //angular velocity of the previous rotate operation - let action; + this._wCurr = 0; //angular velocity of the current rotate operation + //parameters - for( let i = 0; i < this.mouseActions.length; i++ ) { + this.adjustNearFar = false; + this.scaleFactor = 1.1; //zoom/distance multiplier - action = this.mouseActions[ i ]; - if( action.mouse == mouse && action.key == key ) { + this.dampingFactor = 25; + this.wMax = 20; //maximum angular velocity allowed - return action.state; + this.enableAnimations = true; //if animations should be performed - } + this.enableGrid = false; //if grid should be showed during pan operation - } + this.cursorZoom = false; //if wheel zoom should be cursor centered - if ( key != null ) { + this.minFov = 5; + this.maxFov = 90; + this.enabled = true; + this.enablePan = true; + this.enableRotate = true; + this.enableZoom = true; + this.enableGizmos = true; + this.minDistance = 0; + this.maxDistance = Infinity; + this.minZoom = 0; + this.maxZoom = Infinity; //trackball parameters - for( let i = 0; i < this.mouseActions.length; i++ ) { + this._tbCenter = new THREE.Vector3( 0, 0, 0 ); + this._tbRadius = 1; //FSA - action = this.mouseActions[ i ]; - if ( action.mouse == mouse && action.key == null ) { - - return action.state; - - } - - } + this._state = STATE.IDLE; + this.setCamera( _camera ); - } + if ( this.scene != null ) { - return null; + this.scene.add( this._gizmos ); - }; + } - /** - * Calculate the angle between two pointers - * @param {PointerEvent} p1 - * @param {PointerEvent} p2 - * @returns {Number} The angle between two pointers in degrees - */ - getAngle = ( p1, p2 ) => { + this.domElement.style.touchAction = 'none'; + this._devPxRatio = window.devicePixelRatio; + this.initializeMouseActions(); + this.domElement.addEventListener( 'contextmenu', this.onContextMenu ); + this.domElement.addEventListener( 'wheel', this.onWheel ); + this.domElement.addEventListener( 'pointerdown', this.onPointerDown ); + this.domElement.addEventListener( 'pointercancel', this.onPointerCancel ); + window.addEventListener( 'keydown', this.onKeyDown ); + window.addEventListener( 'resize', this.onWindowResize ); - return Math.atan2( p2.clientY - p1.clientY, p2.clientX - p1.clientX ) * 180 / Math.PI; + } //listeners - }; /** - * Update a PointerEvent inside current pointerevents array - * @param {PointerEvent} event - */ - updateTouchEvent = ( event ) => { - - for ( let i = 0; i < this._touchCurrent.length; i++ ) { - - if ( this._touchCurrent[ i ].pointerId == event.pointerId ) { - - this._touchCurrent.splice( i, 1, event ) - break; - - } - - } - - }; - - /** - * Apply a transformation matrix, to the camera and gizmos - * @param {Object} transformation Object containing matrices to apply to camera and gizmos - */ + * Apply a transformation matrix, to the camera and gizmos + * @param {Object} transformation Object containing matrices to apply to camera and gizmos + */ applyTransformMatrix( transformation ) { - + if ( transformation.camera != null ) { - + this._m4_1.copy( this._cameraMatrixState ).premultiply( transformation.camera ); + this._m4_1.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); - this.camera.updateMatrix(); - - //update camera up vector + + this.camera.updateMatrix(); //update camera up vector + if ( this._state == STATE.ROTATE || this._state == STATE.ZROTATE || this._state == STATE.ANIMATION_ROTATE ) { - + this.camera.up.copy( this._upState ).applyQuaternion( this.camera.quaternion ); - + } - + } - + if ( transformation.gizmos != null ) { - + this._m4_1.copy( this._gizmoMatrixState ).premultiply( transformation.gizmos ); + this._m4_1.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); + this._gizmos.updateMatrix(); - + } - - if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS) { - - this._tbRadius = this.calculateTbRadius(this.camera); - + + if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS ) { + + this._tbRadius = this.calculateTbRadius( this.camera ); + if ( this.adjustNearFar ) { - + const cameraDistance = this.camera.position.distanceTo( this._gizmos.position ); - const bb = new THREE.Box3(); bb.setFromObject( this._gizmos ); const sphere = new THREE.Sphere(); bb.getBoundingSphere( sphere ); - const adjustedNearPosition = Math.max( this._nearPos0, sphere.radius + sphere.center.length() ); const regularNearPosition = cameraDistance - this._initialNear; - const minNearPos = Math.min( adjustedNearPosition, regularNearPosition ); this.camera.near = cameraDistance - minNearPos; - - - const adjustedFarPosition = Math.min( this._farPos0, -sphere.radius + sphere.center.length() ); + const adjustedFarPosition = Math.min( this._farPos0, - sphere.radius + sphere.center.length() ); const regularFarPosition = cameraDistance - this._initialFar; - const minFarPos = Math.min( adjustedFarPosition, regularFarPosition ); this.camera.far = cameraDistance - minFarPos; - this.camera.updateProjectionMatrix(); - + } else { - + let update = false; - - if (this.camera.near != this._initialNear ) { - + + if ( this.camera.near != this._initialNear ) { + this.camera.near = this._initialNear; update = true; - + } - + if ( this.camera.far != this._initialFar ) { - + this.camera.far = this._initialFar; update = true; - + } - + if ( update ) { - + this.camera.updateProjectionMatrix(); - + } - - } - - } - - }; - - /** - * Calculate the angular speed - * @param {Number} p0 Position at t0 - * @param {Number} p1 Position at t1 - * @param {Number} t0 Initial time in milliseconds - * @param {Number} t1 Ending time in milliseconds - */ - calculateAngularSpeed = ( p0, p1, t0, t1 ) => { - - const s = p1 - p0; - const t = ( t1 - t0 ) / 1000; - if ( t == 0 ) { - - return 0; - - } - - return s / t; - - }; - - /** - * Calculate the distance between two pointers - * @param {PointerEvent} p0 The first pointer - * @param {PointerEvent} p1 The second pointer - * @returns {number} The distance between the two pointers - */ - calculatePointersDistance = ( p0, p1 ) => { - - return Math.sqrt( Math.pow( p1.clientX - p0.clientX, 2 ) + Math.pow( p1.clientY - p0.clientY, 2 ) ); - - }; - - /** - * Calculate the rotation axis as the vector perpendicular between two vectors - * @param {THREE.Vector3} vec1 The first vector - * @param {THREE.Vector3} vec2 The second vector - * @returns {THREE.Vector3} The normalized rotation axis - */ - calculateRotationAxis = ( vec1, vec2 ) => { - - this._rotationMatrix.extractRotation( this._cameraMatrixState ); - this._quat.setFromRotationMatrix( this._rotationMatrix ); - - this._rotationAxis.crossVectors( vec1, vec2 ).applyQuaternion( this._quat ); - return this._rotationAxis.normalize().clone(); - - }; - - /** - * Calculate the trackball radius so that gizmo's diamater will be 2/3 of the minimum side of the camera frustum - * @param {Camera} camera - * @returns {Number} The trackball radius - */ - calculateTbRadius = ( camera ) => { - - const factor = 0.67; - const distance = camera.position.distanceTo( this._gizmos.position ); - - if ( camera.type == 'PerspectiveCamera' ) { - - const halfFovV = THREE.MathUtils.DEG2RAD * camera.fov * 0.5; //vertical fov/2 in radians - const halfFovH = Math.atan( ( camera.aspect ) * Math.tan( halfFovV ) ); //horizontal fov/2 in radians - return Math.tan( Math.min( halfFovV, halfFovH ) ) * distance * factor; - - } else if ( camera.type == 'OrthographicCamera' ) { - - return Math.min( camera.top, camera.right ) * factor; - - } - - }; - - /** - * Focus operation consist of positioning the point of interest in front of the camera and a slightly zoom in - * @param {THREE.Vector3} point The point of interest - * @param {Number} size Scale factor - * @param {Number} amount Amount of operation to be completed (used for focus animations, default is complete full operation) - */ - focus = ( point, size, amount = 1 ) => { - - const focusPoint = point.clone(); - - //move center of camera (along with gizmos) towards point of interest - focusPoint.sub( this._gizmos.position ).multiplyScalar( amount ); - this._translationMatrix.makeTranslation( focusPoint.x, focusPoint.y, focusPoint.z ); - - const gizmoStateTemp = this._gizmoMatrixState.clone(); - this._gizmoMatrixState.premultiply( this._translationMatrix ); - this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ) - - const cameraStateTemp = this._cameraMatrixState.clone(); - this._cameraMatrixState.premultiply( this._translationMatrix ); - this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); - - //apply zoom - if ( this.enableZoom ) { - - this.applyTransformMatrix( this.scale( size, this._gizmos.position ) ); - - } - - this._gizmoMatrixState.copy( gizmoStateTemp ); - this._cameraMatrixState.copy( cameraStateTemp ); - - }; - - /** - * Draw a grid and add it to the scene - */ - drawGrid = () => { - - if ( this.scene != null ) { - - const color = 0x888888; - const multiplier = 3; - let size, divisions, maxLength, tick; - - if ( this.camera.isOrthographicCamera ) { - - const width = this.camera.right - this.camera.left; - const height = this.camera.bottom - this.camera.top; - - maxLength = Math.max( width, height ); - tick = maxLength / 20; - - size = maxLength / this.camera.zoom * multiplier; - divisions = size / tick * this.camera.zoom; - - } else if ( this.camera.isPerspectiveCamera ) { - - const distance = this.camera.position.distanceTo( this._gizmos.position ); - const halfFovV = THREE.MathUtils.DEG2RAD * this.camera.fov * 0.5; - const halfFovH = Math.atan( ( this.camera.aspect ) * Math.tan( halfFovV ) ); - - maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2; - tick = maxLength / 20; - - size = maxLength * multiplier; - divisions = size / tick; - - } - - if ( this._grid == null ) { - - this._grid = new THREE.GridHelper( size, divisions, color, color ); - this._grid.position.copy( this._gizmos.position ); - this._gridPosition.copy( this._grid.position ); - this._grid.quaternion.copy( this.camera.quaternion ); - this._grid.rotateX( Math.PI * 0.5 ); - - this.scene.add( this._grid ); - - } - - } - - }; - - /** - * Remove all listeners, stop animations and clean scene - */ - dispose = () => { - - if ( this._animationId != -1 ) { - window.cancelAnimationFrame( this._animationId ); + } } - this.domElement.removeEventListener( 'pointerdown', this.onPointerDown ); - this.domElement.removeEventListener( 'pointercancel', this.onPointerCancel ) - this.domElement.removeEventListener( 'wheel', this.onWheel ); - this.domElement.removeEventListener('contextmenu', this.onContextMenu); - - window.removeEventListener( 'pointermove', this.onPointerMove ); - window.removeEventListener( 'pointerup', this.onPointerUp ); + } + /** + * Calculate the angular speed + * @param {Number} p0 Position at t0 + * @param {Number} p1 Position at t1 + * @param {Number} t0 Initial time in milliseconds + * @param {Number} t1 Ending time in milliseconds + */ - window.removeEventListener( 'resize', this.onWindowResize ); - window.addEventListener( 'keydown', this.onKeyDown ); - this.scene.remove( this._gizmos ); - this.disposeGrid(); - - }; - - /** - * remove the grid from the scene - */ - disposeGrid = () => { - - if ( this._grid != null && this.scene != null ) { - - this.scene.remove( this._grid ); - this._grid = null; - - } - - }; - - /** - * Compute the easing out cubic function for ease out effect in animation - * @param {Number} t The absolute progress of the animation in the bound of 0 (beginning of the) and 1 (ending of animation) - * @returns {Number} Result of easing out cubic at time t - */ - easeOutCubic = ( t ) => { - - return 1 - Math.pow( 1 - t, 3 ); - - }; - - /** - * Make rotation gizmos more or less visible - * @param {Boolean} isActive If true, make gizmos more visible - */ - activateGizmos = ( isActive ) => { - - const gizmoX = this._gizmos.children[ 0 ]; - const gizmoY = this._gizmos.children[ 1 ]; - const gizmoZ = this._gizmos.children[ 2 ]; - - if ( isActive ) { - - gizmoX.material.setValues( { opacity: 1 } ); - gizmoY.material.setValues( { opacity: 1 } ); - gizmoZ.material.setValues( { opacity: 1 } ); - - } else { - - gizmoX.material.setValues( { opacity: 0.6 } ); - gizmoY.material.setValues( { opacity: 0.6 } ); - gizmoZ.material.setValues( { opacity: 0.6 } ); - - } - - }; - - /** - * Calculate the cursor position in NDC - * @param {number} x Cursor horizontal coordinate within the canvas - * @param {number} y Cursor vertical coordinate within the canvas - * @param {HTMLElement} canvas The canvas where the renderer draws its output - * @returns {THREE.Vector2} Cursor normalized position inside the canvas - */ - getCursorNDC = ( cursorX, cursorY, canvas ) => { - - const canvasRect = canvas.getBoundingClientRect(); - this._v2_1.setX ( ( ( cursorX - canvasRect.left ) / canvasRect.width ) * 2 - 1 ); - this._v2_1.setY( ( ( canvasRect.bottom - cursorY ) / canvasRect.height ) * 2 - 1 ); - return this._v2_1.clone(); - - }; - - /** - * Calculate the cursor position inside the canvas x/y coordinates with the origin being in the center of the canvas - * @param {Number} x Cursor horizontal coordinate within the canvas - * @param {Number} y Cursor vertical coordinate within the canvas - * @param {HTMLElement} canvas The canvas where the renderer draws its output - * @returns {THREE.Vector2} Cursor position inside the canvas - */ - getCursorPosition = ( cursorX, cursorY, canvas ) => { - - this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas) ); - this._v2_1.x *= ( this.camera.right - this.camera.left ) * 0.5; - this._v2_1.y *= ( this.camera.top - this.camera.bottom ) * 0.5; - return this._v2_1.clone(); - - }; - - /** - * Set the camera to be controlled - * @param {Camera} camera The virtual camera to be controlled - */ - setCamera = ( camera ) => { - - camera.lookAt( this._tbCenter ); - camera.updateMatrix(); - - //setting state - if ( camera.type == 'PerspectiveCamera' ) { - - this._fov0 = camera.fov; - this._fovState = camera.fov; - - } - this._cameraMatrixState0.copy( camera.matrix ); - this._cameraMatrixState.copy( this._cameraMatrixState0 ); - this._cameraProjectionState.copy( camera.projectionMatrix ); - this._zoom0 = camera.zoom; - this._zoomState = this._zoom0; - - this._initialNear = camera.near; - this._nearPos0 = camera.position.distanceTo( this._tbCenter ) - camera.near; - this._nearPos = this._initialNear; - - this._initialFar = camera.far; - this._farPos0 = camera.position.distanceTo( this._tbCenter ) - camera.far; - this._farPos = this._initialFar; - - this._up0.copy( camera.up ); - this._upState.copy( camera.up ); - - this.camera = camera; - this.camera.updateProjectionMatrix(); - - //making gizmos - this._tbRadius = this.calculateTbRadius( camera ); - this.makeGizmos( this._tbCenter, this._tbRadius ); - - }; - /** - * Set gizmos visibility - * @param {Boolean} value Value of gizmos visibility - */ + * Set gizmos visibility + * @param {Boolean} value Value of gizmos visibility + */ setGizmosVisible( value ) { - + this._gizmos.visible = value; this.dispatchEvent( _changeEvent ); - - }; - - /** - * Creates the rotation gizmos matching trackball center and radius - * @param {THREE.Vector3} tbCenter The trackball center - * @param {number} tbRadius The trackball radius - */ - makeGizmos = ( tbCenter, tbRadius ) => { - - const curve = new THREE.EllipseCurve( 0, 0, tbRadius, tbRadius ); - const points = curve.getPoints( this._curvePts ); - - //geometry - const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); - - //material - const curveMaterialX = new THREE.LineBasicMaterial( { color: 0xff8080, fog: false, transparent: true, opacity: 0.6 } ); - const curveMaterialY = new THREE.LineBasicMaterial( { color: 0x80ff80, fog: false, transparent: true, opacity: 0.6 } ); - const curveMaterialZ = new THREE.LineBasicMaterial( { color: 0x8080ff, fog: false, transparent: true, opacity: 0.6 } ); - - //line - const gizmoX = new THREE.Line( curveGeometry, curveMaterialX ); - const gizmoY = new THREE.Line( curveGeometry, curveMaterialY ); - const gizmoZ = new THREE.Line( curveGeometry, curveMaterialZ ); - - const rotation = Math.PI * 0.5; - gizmoX.rotation.x = rotation; - gizmoY.rotation.y = rotation; - - - //setting state - this._gizmoMatrixState0.identity().setPosition( tbCenter ) ; - this._gizmoMatrixState.copy( this._gizmoMatrixState0 ); - - if ( this.camera.zoom != 1 ) { - - //adapt gizmos size to camera zoom - const size = 1 / this.camera.zoom; - this._scaleMatrix.makeScale( size, size, size ); - this._translationMatrix.makeTranslation( -tbCenter.x, -tbCenter.y, -tbCenter.z ); - - this._gizmoMatrixState.premultiply( this._translationMatrix ).premultiply( this._scaleMatrix ); - this._translationMatrix.makeTranslation( tbCenter.x, tbCenter.y, tbCenter.z ); - this._gizmoMatrixState.premultiply( this._translationMatrix ); - - } - - this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); - - this._gizmos.clear(); - - this._gizmos.add( gizmoX ); - this._gizmos.add( gizmoY ); - this._gizmos.add( gizmoZ ); - - }; - - /** - * Perform animation for focus operation - * @param {Number} time Instant in which this function is called as performance.now() - * @param {THREE.Vector3} point Point of interest for focus operation - * @param {THREE.Matrix4} cameraMatrix Camera matrix - * @param {THREE.Matrix4} gizmoMatrix Gizmos matrix - */ - onFocusAnim = ( time, point, cameraMatrix, gizmoMatrix ) => { - - if ( this._timeStart == -1 ) { - - //animation start - this._timeStart = time; - - } - - if ( this._state == STATE.ANIMATION_FOCUS ) { - - const deltaTime = time - this._timeStart; - const animTime = deltaTime / this.focusAnimationTime; - - this._gizmoMatrixState.copy( gizmoMatrix ); - - if ( animTime >= 1) { - - //animation end - this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); - - this.focus( point, this.scaleFactor ); - - this._timeStart = -1; - this.updateTbState( STATE.IDLE, false ); - this.activateGizmos( false ); - - this.dispatchEvent( _changeEvent ); - - } else { - - const amount = this.easeOutCubic( animTime ); - const size = ( ( 1 - amount ) + ( this.scaleFactor * amount ) ); - - this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); - this.focus( point, size, amount ); - - this.dispatchEvent( _changeEvent ); - const self = this; - this._animationId = window.requestAnimationFrame( function( t ) { - - self.onFocusAnim( t, point, cameraMatrix, gizmoMatrix.clone() ); - - } ); - - } - - } else { - - //interrupt animation - this._timeStart = -1; - this._animationId = -1; - - } - - }; - - /** - * Perform animation for rotation operation - * @param {Number} time Instant in which this function is called as performance.now() - * @param {THREE.Vector3} rotationAxis Rotation axis - * @param {number} w0 Initial angular velocity - */ - onRotationAnim = ( time, rotationAxis, w0 ) => { - - if ( this._timeStart == -1 ) { - - //animation start - this._anglePrev = 0 - this._angleCurrent = 0; - this._timeStart = time; - - } - - if ( this._state == STATE.ANIMATION_ROTATE ) { - - //w = w0 + alpha * t - const deltaTime = ( time - this._timeStart ) / 1000; - const w = w0 + ( ( -this.dampingFactor ) * deltaTime ); - - if ( w > 0 ) { - - //tetha = 0.5 * alpha * t^2 + w0 * t + tetha0 - this._angleCurrent = 0.5 * ( -this.dampingFactor ) * Math.pow( deltaTime, 2 ) + w0 * deltaTime + 0; - this.applyTransformMatrix( this.rotate( rotationAxis, this._angleCurrent ) ); - this.dispatchEvent( _changeEvent ); - const self = this; - this._animationId = window.requestAnimationFrame( function( t ) { - - self.onRotationAnim( t, rotationAxis, w0 ); - - } ); - - } else { - - this._animationId = -1; - this._timeStart = -1; - - this.updateTbState( STATE.IDLE, false ); - this.activateGizmos( false ); - - this.dispatchEvent( _changeEvent ); - - } - - } else { - - //interrupt animation - this._animationId = -1; - this._timeStart = -1; + } + /** + * Creates the rotation gizmos matching trackball center and radius + * @param {Vector3} tbCenter The trackball center + * @param {number} tbRadius The trackball radius + */ - if ( this._state != STATE.ROTATE ) { - - this.activateGizmos( false ); - this.dispatchEvent ( _changeEvent ); - } - - } - - }; - - /** - * Perform pan operation moving camera between two points - * @param {THREE.Vector3} p0 Initial point - * @param {THREE.Vector3} p1 Ending point - * @param {Boolean} adjust If movement should be adjusted considering camera distance (Perspective only) - */ - pan = ( p0, p1, adjust = false ) => { - - const movement = p0.clone().sub( p1 ); - - if ( this.camera.isOrthographicCamera ) { - - //adjust movement amount - movement.multiplyScalar( 1 / this.camera.zoom ); - - } else if ( this.camera.isPerspectiveCamera && adjust ) { - - //adjust movement amount - this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ); //camera's initial position - this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ); //gizmo's initial position - const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.camera.position.distanceTo( this._gizmos.position ); - movement.multiplyScalar( 1 / distanceFactor ); - - } - - this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion ); - - this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y,this. _v3_1.z ); - - this.setTransformationMatrices( this._m4_1, this._m4_1 ); - return _transformation; - - }; - - /** - * Reset trackball - */ - reset = () => { - - this.camera.zoom = this._zoom0; - - if( this.camera.isPerspectiveCamera ) { - - this.camera.fov = this._fov0; - - } - - this.camera.near = this._nearPos; - this.camera.far = this._farPos; - this._cameraMatrixState.copy( this._cameraMatrixState0 ); - this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); - this.camera.up.copy( this._up0 ); - - this.camera.updateMatrix(); - this.camera.updateProjectionMatrix(); - - this._gizmoMatrixState.copy (this._gizmoMatrixState0 ); - this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); - this._gizmos.updateMatrix(); - - this._tbRadius = this.calculateTbRadius( this.camera ); - this.makeGizmos(this._gizmos.position, this._tbRadius); - - this.camera.lookAt( this._gizmos.position ); - - this.updateTbState( STATE.IDLE, false ); - - this.dispatchEvent( _changeEvent ); - - }; - - /** - * Rotate the camera around an axis passing by trackball's center - * @param {THREE.Vector3} axis Rotation axis - * @param {number} angle Angle in radians - * @returns {Object} Object with 'camera' field containing transformation matrix resulting from the operation to be applied to the camera - */ - rotate = ( axis, angle ) => { - - const point = this._gizmos.position; //rotation center - this._translationMatrix.makeTranslation( -point.x, -point.y, -point.z ); - this._rotationMatrix.makeRotationAxis( axis, -angle ); - - //rotate camera - this._m4_1.makeTranslation( point.x, point.y, point.z ); - this._m4_1.multiply( this._rotationMatrix ); - this._m4_1.multiply( this._translationMatrix ); - - this.setTransformationMatrices( this._m4_1 ); - - return _transformation; - - }; - - copyState = () => { - - let state; - if ( this.camera.isOrthographicCamera ) { - - state = JSON.stringify( { arcballState: { - - cameraFar: this.camera.far, - cameraMatrix: this.camera.matrix, - cameraNear: this.camera.near, - cameraUp: this.camera.up, - cameraZoom: this.camera.zoom, - gizmoMatrix: this._gizmos.matrix - - } } ); - - } else if ( this.camera.isPerspectiveCamera ) { - - state = JSON.stringify( { arcballState: { - cameraFar: this.camera.far, - cameraFov: this.camera.fov, - cameraMatrix: this.camera.matrix, - cameraNear: this.camera.near, - cameraUp: this.camera.up, - cameraZoom: this.camera.zoom, - gizmoMatrix: this._gizmos.matrix - - } } ); - - } - - navigator.clipboard.writeText( state ); - - }; - - pasteState = () => { - - const self = this; - navigator.clipboard.readText().then( function resolved( value ) { - - self.setStateFromJSON( value ); - - } ); - - }; - - /** - * Save the current state of the control. This can later be recover with .reset - */ - saveState = () => { - - this._cameraMatrixState0.copy( this.camera.matrix ); - this._gizmoMatrixState0.copy( this._gizmos.matrix ); - this._nearPos = this.camera.near; - this._farPos = this.camera.far; - this._zoom0 = this.camera.zoom; - this._up0.copy( this.camera.up ); - - if ( this.camera.isPerspectiveCamera ) { - - this._fov0 = this.camera.fov; - - } - - }; - - /** - * Perform uniform scale operation around a given point - * @param {Number} size Scale factor - * @param {THREE.Vector3} point Point around which scale - * @param {Boolean} scaleGizmos If gizmos should be scaled (Perspective only) - * @returns {Object} Object with 'camera' and 'gizmo' fields containing transformation matrices resulting from the operation to be applied to the camera and gizmos - */ - scale = ( size, point, scaleGizmos = true ) => { - - const scalePoint = point.clone(); - let sizeInverse = 1 / size; - - if ( this.camera.isOrthographicCamera ) { - - //camera zoom - this.camera.zoom = this._zoomState; - this.camera.zoom *= size; - - //check min and max zoom - if ( this.camera.zoom > this.maxZoom ) { - - this.camera.zoom = this.maxZoom; - sizeInverse = this._zoomState / this.maxZoom; - - } - else if ( this.camera.zoom < this.minZoom ) { - - this.camera.zoom = this.minZoom; - sizeInverse = this._zoomState / this.minZoom; - - } - - this.camera.updateProjectionMatrix(); - - this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ); //gizmos position - - //scale gizmos so they appear in the same spot having the same dimension - this._scaleMatrix.makeScale(sizeInverse, sizeInverse, sizeInverse); - this._translationMatrix.makeTranslation( -this._v3_1.x, -this._v3_1.y, -this._v3_1.z ); - - this._m4_2.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ).multiply( this._scaleMatrix ); - this._m4_2.multiply( this._translationMatrix ); - - - //move camera and gizmos to obtain pinch effect - scalePoint.sub( this._v3_1 ); - - const amount = scalePoint.clone().multiplyScalar( sizeInverse ); - scalePoint.sub( amount ); - - this._m4_1.makeTranslation( scalePoint.x, scalePoint.y, scalePoint.z ); - this._m4_2.premultiply( this._m4_1 ); - - this.setTransformationMatrices( this._m4_1, this._m4_2 ); - return _transformation; - - } else if ( this.camera.isPerspectiveCamera ) { - - this._v3_1.setFromMatrixPosition( this._cameraMatrixState ); - this._v3_2.setFromMatrixPosition( this._gizmoMatrixState ); - - //move camera - let distance = this._v3_1.distanceTo( scalePoint ); - let amount = distance - ( distance * sizeInverse ); - - //check min and max distance - const newDistance = distance - amount - if ( newDistance < this.minDistance ) { - - sizeInverse = this.minDistance / distance; - amount = distance - ( distance * sizeInverse ); - - } else if ( newDistance > this.maxDistance ) { - - sizeInverse = this.maxDistance / distance; - amount = distance - ( distance * sizeInverse ); - - } - - let direction = scalePoint.clone().sub( this._v3_1 ).normalize().multiplyScalar( amount ); - - this._m4_1.makeTranslation( direction.x, direction.y, direction.z ); - - - if ( scaleGizmos ) { - - //scale gizmos so they appear in the same spot having the same dimension - const pos = this._v3_2; - - distance = pos.distanceTo( scalePoint ); - amount = distance - ( distance * sizeInverse ); - direction = scalePoint.clone().sub( this._v3_2 ).normalize().multiplyScalar( amount ); - - this._translationMatrix.makeTranslation( pos.x, pos.y, pos.z ); - this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse ); - - this._m4_2.makeTranslation( direction.x, direction.y, direction.z ).multiply( this._translationMatrix ); - this._m4_2.multiply( this._scaleMatrix ); - - this._translationMatrix.makeTranslation( -pos.x, -pos.y, -pos.z ); - - this._m4_2.multiply( this._translationMatrix ); - this.setTransformationMatrices( this._m4_1, this._m4_2 ); - - - } else { - - this.setTransformationMatrices( this._m4_1 ); - - } - - return _transformation; - - } - - }; - - /** - * Set camera fov - * @param {Number} value fov to be setted - */ - setFov = ( value ) => { - - if ( this.camera.isPerspectiveCamera ) { - - this.camera.fov = THREE.MathUtils.clamp( value, this.minFov, this.maxFov ); - this.camera.updateProjectionMatrix(); - - } - - }; - - /** - * Set the trackball's center point - * @param {Number} x X coordinate - * @param {Number} y Y coordinate - * @param {Number} z Z coordinate - */ - setTarget = ( x, y, z ) => { - - this._tbCenter.set( x, y, z ); - this._gizmos.position.set( x, y, z ); //for correct radius calculation - this._tbRadius = this.calculateTbRadius( this.camera ); - - this.makeGizmos( this._tbCenter, this._tbRadius ); - this.camera.lookAt( this._tbCenter ); - - }; - - /** - * Set values in transformation object - * @param {THREE.Matrix4} camera Transformation to be applied to the camera - * @param {THREE.Matrix4} gizmos Transformation to be applied to gizmos - */ - setTransformationMatrices( camera = null, gizmos = null) { - + * Set values in transformation object + * @param {Matrix4} camera Transformation to be applied to the camera + * @param {Matrix4} gizmos Transformation to be applied to gizmos + */ + setTransformationMatrices( camera = null, gizmos = null ) { + if ( camera != null ) { - + if ( _transformation.camera != null ) { - + _transformation.camera.copy( camera ); - + } else { - + _transformation.camera = camera.clone(); - + } - + } else { - + _transformation.camera = null; - + } - + if ( gizmos != null ) { - + if ( _transformation.gizmos != null ) { - + _transformation.gizmos.copy( gizmos ); - + } else { - + _transformation.gizmos = gizmos.clone(); - + } - + } else { - + _transformation.gizmos = null; - - } - - }; - - /** - * Rotate camera around its direction axis passing by a given point by a given angle - * @param {THREE.Vector3} point The point where the rotation axis is passing trough - * @param {Number} angle Angle in radians - * @returns The computed transormation matix - */ - zRotate = ( point, angle ) => { - - this._rotationMatrix.makeRotationAxis( this._rotationAxis, angle ); - this._translationMatrix.makeTranslation( -point.x, -point.y, -point.z ); - - this._m4_1.makeTranslation( point.x, point.y, point.z ); - this._m4_1.multiply( this._rotationMatrix ); - this._m4_1.multiply( this._translationMatrix ); - - this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ).sub( point ); //vector from rotation center to gizmos position - this._v3_2.copy( this._v3_1 ).applyAxisAngle( this._rotationAxis, angle ); //apply rotation - this._v3_2.sub( this._v3_1 ); - - this._m4_2.makeTranslation( this._v3_2.x, this._v3_2.y, this._v3_2.z ); - - this.setTransformationMatrices( this._m4_1, this._m4_2 ); - return _transformation; - - }; - - - /** - * Unproject the cursor on the 3D object surface - * @param {THREE.Vector2} cursor Cursor coordinates in NDC - * @param {Camera} camera Virtual camera - * @returns {THREE.Vector3} The point of intersection with the model, if exist, null otherwise - */ - unprojectOnObj = ( cursor, camera ) => { - - const raycaster = new THREE.Raycaster(); - raycaster.near = camera.near; - raycaster.far = camera.far; - raycaster.setFromCamera( cursor, camera ); - - const intersect = raycaster.intersectObjects( this.scene.children, true ); - - for ( let i = 0; i < intersect.length; i++ ) { - - if ( intersect[ i ].object.uuid != this._gizmos.uuid && intersect[ i ].face != null ) { - - return intersect[ i ].point.clone(); - - } - - } - - return null; - }; - - /** - * Unproject the cursor on the trackball surface - * @param {Camera} camera The virtual camera - * @param {Number} cursorX Cursor horizontal coordinate on screen - * @param {Number} cursorY Cursor vertical coordinate on screen - * @param {HTMLElement} canvas The canvas where the renderer draws its output - * @param {number} tbRadius The trackball radius - * @returns {THREE.Vector3} The unprojected point on the trackball surface - */ - unprojectOnTbSurface = ( camera, cursorX, cursorY, canvas, tbRadius ) => { - - if ( camera.type == 'OrthographicCamera' ) { - - this._v2_1.copy( this.getCursorPosition ( cursorX, cursorY, canvas ) ); - this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 ); - - const x2 = Math.pow( this._v2_1.x, 2 ); - const y2 = Math.pow( this._v2_1.y, 2 ); - const r2 = Math.pow( this._tbRadius, 2 ); - - if ( x2 + y2 <= r2 * 0.5 ) { - - //intersection with sphere - this._v3_1.setZ( Math.sqrt( r2 - ( x2 + y2 ) ) ); - - } else { - - //intersection with hyperboloid - this._v3_1.setZ( ( r2 * 0.5 ) / ( Math.sqrt( x2 + y2 ) ) ); - } - - return this._v3_1; - - } else if ( camera.type == 'PerspectiveCamera' ) { - - //unproject cursor on the near plane - this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) ); - - this._v3_1.set( this._v2_1.x, this._v2_1.y, -1 ); - this._v3_1.applyMatrix4( camera.projectionMatrixInverse ); - - const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction - const cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position ); - const radius2 = Math.pow( tbRadius, 2 ); - - // camera - // |\ - // | \ - // | \ - // h | \ - // | \ - // | \ - // _ _ | _ _ _\ _ _ near plane - // l - - const h = this._v3_1.z; - const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) ); - - if ( l == 0 ) { - - //ray aligned with camera - rayDir.set( this._v3_1.x, this._v3_1.y, tbRadius ); - return rayDir; - - } - - const m = h / l; - const q = cameraGizmoDistance; - - /* - * calculate intersection point between unprojected ray and trackball surface - *|y = m * x + q - *|x^2 + y^2 = r^2 - * - * (m^2 + 1) * x^2 + (2 * m * q) * x + q^2 - r^2 = 0 - */ - let a = Math.pow( m, 2 ) + 1; - let b = 2 * m * q; - let c = Math.pow( q, 2 ) - radius2; - let delta = Math.pow( b, 2 ) - ( 4 * a * c ); - - if ( delta >= 0 ) { - - //intersection with sphere - this._v2_1.setX( ( -b - Math.sqrt( delta ) ) / ( 2 * a ) ); - this._v2_1.setY( m * this._v2_1.x + q ); - - let angle = THREE.MathUtils.RAD2DEG * this._v2_1.angle(); - - if ( angle >= 45 ) { - - //if angle between intersection point and X' axis is >= 45°, return that point - //otherwise, calculate intersection point with hyperboloid - - let rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) ); - rayDir.multiplyScalar( rayLength ); - rayDir.z += cameraGizmoDistance; - return rayDir; - - } - - } - - //intersection with hyperboloid - /* - *|y = m * x + q - *|y = (1 / x) * (r^2 / 2) - * - * m * x^2 + q * x - r^2 / 2 = 0 - */ - - a = m; - b = q; - c = -radius2 * 0.5; - delta = Math.pow( b, 2 ) - ( 4 * a * c ); - this._v2_1.setX( ( -b - Math.sqrt( delta ) ) / ( 2 * a ) ); - this._v2_1.setY( m * this._v2_1.x + q ); - - let rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) ); - - rayDir.multiplyScalar( rayLength ); - rayDir.z += cameraGizmoDistance; - return rayDir; - - } - - }; - - - /** - * Unproject the cursor on the plane passing through the center of the trackball orthogonal to the camera - * @param {Camera} camera The virtual camera - * @param {Number} cursorX Cursor horizontal coordinate on screen - * @param {Number} cursorY Cursor vertical coordinate on screen - * @param {HTMLElement} canvas The canvas where the renderer draws its output - * @param {Boolean} initialDistance If initial distance between camera and gizmos should be used for calculations instead of current (Perspective only) - * @returns {THREE.Vector3} The unprojected point on the trackball plane - */ - unprojectOnTbPlane = ( camera, cursorX, cursorY, canvas, initialDistance = false ) => { - - if ( camera.type == 'OrthographicCamera' ) { - - this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) ); - this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 ); - - return this._v3_1.clone(); - - } else if ( camera.type == 'PerspectiveCamera' ) { - - this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) ); - - //unproject cursor on the near plane - this._v3_1.set( this._v2_1.x, this._v2_1.y, -1 ); - this._v3_1.applyMatrix4( camera.projectionMatrixInverse ); - - const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction - - // camera - // |\ - // | \ - // | \ - // h | \ - // | \ - // | \ - // _ _ | _ _ _\ _ _ near plane - // l - - const h = this._v3_1.z; - const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) ); - let cameraGizmoDistance; - - if ( initialDistance ) { - - cameraGizmoDistance = this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ).distanceTo( this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ) ); - - } else { - - cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position ); - - } - - /* - * calculate intersection point between unprojected ray and the plane - *|y = mx + q - *|y = 0 - * - * x = -q/m - */ - if ( l == 0 ) { - - //ray aligned with camera - rayDir.set( 0, 0, 0 ); - return rayDir; - - } - - const m = h / l; - const q = cameraGizmoDistance; - const x = -q / m; - - const rayLength = Math.sqrt( Math.pow( q, 2 ) + Math.pow( x, 2 ) ); - rayDir.multiplyScalar( rayLength ); - rayDir.z = 0; - return rayDir; - - } - - }; - - /** - * Update camera and gizmos state - */ - updateMatrixState = () => { - - //update camera and gizmos state - this._cameraMatrixState.copy( this.camera.matrix ); - this._gizmoMatrixState.copy( this._gizmos.matrix ); - - if ( this.camera.isOrthographicCamera ) { - - this._cameraProjectionState.copy( this.camera.projectionMatrix ); - this.camera.updateProjectionMatrix(); - this._zoomState = this.camera.zoom; - - } else if ( this.camera.isPerspectiveCamera) { - - this._fovState = this.camera.fov; - + } - - }; - + + } /** - * Update the trackball FSA - * @param {STATE} newState New state of the FSA - * @param {Boolean} updateMatrices If matriices state should be updated - */ - updateTbState = ( newState, updateMatrices ) => { - - this._state = newState; - if ( updateMatrices ) { - - this.updateMatrixState(); - - } - - }; - - update = () => { - - const EPS = 0.000001; - - //check min/max parameters - if ( this.camera.isOrthographicCamera ) { - - //check zoom - if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) { - - const newZoom = THREE.MathUtils.clamp( this.camera.zoom, this.minZoom, this.maxZoom ); - this.applyTransformMatrix( this.scale( newZoom / this.camera.zoom, this._gizmos.position, true ) ); - - } - - } else if ( this.camera.isPerspectiveCamera ) { - - //check distance - const distance = this.camera.position.distanceTo( this._gizmos.position ); - - if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) { - - const newDistance = THREE.MathUtils.clamp( distance, this.minDistance, this.maxDistance ); - this.applyTransformMatrix( this.scale( newDistance / distance, this._gizmos.position ) ); - this.updateMatrixState(); - - } - - //check fov - if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) { - - this.camera.fov = THREE.MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov ); - this.camera.updateProjectionMatrix(); - - } - - const oldRadius = this._tbRadius; - this._tbRadius = this.calculateTbRadius( this.camera ); - - if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) { - - const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3; - const newRadius = this._tbRadius / scale; - const curve = new THREE.EllipseCurve( 0, 0, newRadius, newRadius ); - const points = curve.getPoints( this._curvePts ); - const curveGeometry = new THREE.BufferGeometry().setFromPoints( points ); - - for( let gizmo in this._gizmos.children ) { - - this._gizmos.children[ gizmo ].geometry = curveGeometry; - - } - - } - - } - - this.camera.lookAt( this._gizmos.position ); - - }; - - setStateFromJSON = ( json ) => { - - const state = JSON.parse( json ); - - if ( state.arcballState != undefined ) { - - this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements ); - this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale ); - - this.camera.up.copy( state.arcballState.cameraUp ); - this.camera.near = state.arcballState.cameraNear; - this.camera.far = state.arcballState.cameraFar; - - this.camera.zoom = state.arcballState.cameraZoom; - - if ( this.camera.isPerspectiveCamera ) { - - this.camera.fov = state.arcballState.cameraFov; - - } - - this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements ); - this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale ); - - this.camera.updateMatrix(); - this.camera.updateProjectionMatrix(); - - this._gizmos.updateMatrix(); - - this._tbRadius = this.calculateTbRadius( this.camera ); - let gizmoTmp = new THREE.Matrix4().copy( this._gizmoMatrixState0 ); - this.makeGizmos( this._gizmos.position, this._tbRadius ); - this._gizmoMatrixState0.copy( gizmoTmp ); - - this.camera.lookAt( this._gizmos.position ); - this.updateTbState( STATE.IDLE, false ); - - this.dispatchEvent( _changeEvent ); - - } - - }; - - }; - + * Rotate camera around its direction axis passing by a given point by a given angle + * @param {Vector3} point The point where the rotation axis is passing trough + * @param {Number} angle Angle in radians + * @returns The computed transormation matix + */ + + + } + THREE.ArcballControls = ArcballControls; -} )(); \ No newline at end of file +} )(); diff --git a/examples/js/controls/TransformControls.js b/examples/js/controls/TransformControls.js index fdafa5d4d398148d28dcd281e9968096ea2a9cff..6610ec9c84f7a6c7fb8d464c2be1cabe018f83d0 100644 --- a/examples/js/controls/TransformControls.js +++ b/examples/js/controls/TransformControls.js @@ -217,27 +217,6 @@ 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(); diff --git a/examples/js/csm/Frustum.js b/examples/js/csm/Frustum.js index 7f324aab4b3b2d5868af75ac7f7fb18902143550..d9300b7a53475ae85c72f653af162e6faa0313a3 100644 --- a/examples/js/csm/Frustum.js +++ b/examples/js/csm/Frustum.js @@ -93,7 +93,7 @@ } - if ( i === breaks - 1 ) { + if ( i === breaks.length - 1 ) { for ( let j = 0; j < 4; j ++ ) { diff --git a/examples/js/postprocessing/SAOPass.js b/examples/js/postprocessing/SAOPass.js index 1e8f9094a149c53eb778efdd130252706128495b..d94f8e2ebb4cda37182a6bb0c8dc50faead0b2c1 100644 --- a/examples/js/postprocessing/SAOPass.js +++ b/examples/js/postprocessing/SAOPass.js @@ -6,15 +6,15 @@ class SAOPass extends THREE.Pass { - constructor( scene, camera, depthTexture, useNormals, resolution ) { + constructor( scene, camera, useDepthTexture = false, useNormals = false, resolution = new THREE.Vector2( 256, 256 ) ) { super(); this.scene = scene; this.camera = camera; this.clear = true; this.needsSwap = false; - this.supportsDepthTextureExtension = depthTexture !== undefined ? depthTexture : false; - this.supportsNormalTexture = useNormals !== undefined ? useNormals : false; + this.supportsDepthTextureExtension = useDepthTexture; + this.supportsNormalTexture = useNormals; this.originalClearColor = new THREE.Color(); this._oldClearColor = new THREE.Color(); this.oldClearAlpha = 1; @@ -30,7 +30,7 @@ saoBlurStdDev: 4, saoBlurDepthCutoff: 0.01 }; - this.resolution = resolution !== undefined ? new THREE.Vector2( resolution.x, resolution.y ) : new THREE.Vector2( 256, 256 ); + this.resolution = new THREE.Vector2( resolution.x, resolution.y ); this.saoRenderTarget = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, @@ -44,10 +44,11 @@ format: THREE.RGBAFormat } ); this.depthRenderTarget = this.normalRenderTarget.clone(); + let depthTexture; if ( this.supportsDepthTextureExtension ) { - const depthTexture = new THREE.DepthTexture(); + depthTexture = new THREE.DepthTexture(); depthTexture.type = THREE.UnsignedShortType; this.beautyRenderTarget.depthTexture = depthTexture; this.beautyRenderTarget.depthBuffer = true;