# Copyright 2012 United States Government, as represented by the Secretary of Defense, Under # Secretary of Defense (Personnel & Readiness). # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express # or implied. See the License for the specific language governing permissions and limitations under # the License. ## The component representation of a navigation scene ## ## @name navscene.vwf ## @namespace --- extends: http://vwf.example.com/scene.vwf properties: ## Navigation mode. Default value is 'walk.' Other options include 'orbit' and 'none.' ## ## @name navscene.vwf#navmode ## @property navmode: set: | switch ( value ) { case "walk": case "orbit": case "none": if ( this.setNavMode ) { this.setNavMode( value ); } this.navmode = value; break; } value: "walk" ## Orbit radius ## ## @name navscene.vwf#orbitRadius ## @property orbitRadius: -1 ## A node.vwf#find search pattern identifying the object to orbit. ## ## @name navscene.vwf#orbitObjectPattern ## @property orbitObjectPattern: "sceneCenter" ## Rotation speed. At a speed of 1, will take 4 seconds to go around 360 degrees. ## ## @name navscene.vwf#rotationSpeed ## @property rotationSpeed: 1.0 ## Translation speed ## ## @name navscene.vwf#translationSpeed ## @property translationSpeed: 1.0 ## Maximum speed ## ## @name navscene.vwf#maxSpeed ## @property maxSpeed: 500 ## Drag mode move boolean. Default value is false. ## ## @name navscene.vwf#dragModeMove ## @property dragModeMove: false events: keyUp: keyDown: pointerDown: pointerUp: pointerMove: pointerOut: pointerWheel: methods: ## Updates scene ## ## @name navscene.vwf#update ## @function ## ## @returns undefined update: ## Gets camera at vector ## ## @name navscene.vwf#getCameraAt ## @function ## ## @returns camera at vector getCameraAt: ## For internal use only - needs to be public for future calls ## ## @name navscene.vwf#disableViewSideNav ## @function ## ## @returns undefined disableViewSideNav: ## For internal use only - needs to be public for future calls ## ## @name navscene.vwf#setNavMode ## @function ## ## @returns undefined setNavMode: scripts: - | var counter = 1; var _piDiv180 = Math.PI / 180; var _180DivPI = 180 / Math.PI; var _2PI = 2 * Math.PI; var _85 = 85.0 * _piDiv180; var _10 = 10.0 * _piDiv180; var phi, theta; this.initialize = function() { this.input = { futureActive: function() { return ( this.lookActive || this.moveActive || this.button1Down || this.button2Down || this.keysAreDown() ); }, timeSinceLastEvent: function() { return this.time - this.pointerEventTime; }, timeSinceDownEvent: function() { return this.time - this.pointerDownTime; }, pointerDelta: function() { if ( this.pointerInfo && this.lastPointerInfo ) { return [ (this.lastPointerInfo.position[0] - this.pointerInfo.position[0]) * 50, (this.lastPointerInfo.position[1] - this.pointerInfo.position[1]) * 50 ]; } return undefined; }, keysAreDown: function() { return ( this.keyInfo && Object.keys( this.keyInfo.keysDown ).length > 0 ); }, keyInfo: undefined, pointerInfo: undefined, lastPointerInfo: undefined, pickInfo: undefined, lastPickInfo: undefined, pointerDownTime: undefined, pointerEventTime: undefined, lookActive: false, moveActive: false, look: [ 0, 0 ], move: [ 0, 0 ], button1Down: false, button2Down: false, turnLeftDown: false, turnRightDown: false, lastUpdateTime: undefined, lastInputTime: undefined, }; // Wait until the queue resumes to set the navigation mode, so that we are sure that the camera has been // created // TODO: When we can assume that initialize doesn't complete until all created nodes are in place, this // will no longer need to be a future call because we will be able to assume that the camera exists // Disable view-side navigation so navscene.vwf can do the navigation this.future( 0 ).disableViewSideNav(); this.future( 0 ).setNavMode( this.navmode ); } this.timeElapsed = function() { var timeElapsed = this.time - this.input.lastInputTime; if ( !this.input.lastInputTime || timeElapsed > 1 ) { timeElapsed = 1; } return timeElapsed; } this.disableViewSideNav = function() { if ( this.camera ) this.camera.navmode = "none"; } this.setNavMode = function( mode ) { if (!this.activeCameraComp) { this.getActiveCamera(); } if ( this.activeCameraComp ) { switch( mode ) { case "orbit": if ( this.orbitObjectPattern ) { this.find( this.orbitObjectPattern, function( orbitObject ) { this.activeCameraComp.lookAt = orbitObject.id; this.updateRadius(); } ); } break; default: this.activeCameraComp.lookAt = ""; break; } } } this.changeNavMode = function( state ) { if ( state == 1 ) { if ( this.navmode == "walk" ) this.navmode = "orbit"; else if ( this.navmode == "orbit" ) this.navmode = "walk"; } } this.raiseOrZoomIn = function( state ) { var active = this.input.futureActive(); if ( this.navMode == "walk" ) { this.cameraZ( 1.0 ); } else if ( this.navMode == "orbit" ) { this.zoom( true ); } this.input.button1Down = state; if ( state && !active ) { this.future( 0.1 ).update(); } } this.lowerOrZoomOut = function( state ) { var active = this.input.futureActive(); if ( this.navMode == "walk" ) { this.cameraZ( -1.0 ); } else if ( this.navMode == "orbit" ) { this.zoom( false ); } this.input.button2Down = state; if ( state && !active ) { this.future( 0.1 ).update(); } } this.pointerIsDown = function() { if ( this.input && this.input.pointerInfo ) { var bInfo = this.input.pointerInfo.buttons; if ( bInfo ) { return bInfo.left || bInfo.right || bInfo.middle; } } return false; } this.pointerDown = function( parms, pickInfo ){ this.input.lastPointerInfo = undefined; this.input.pointerInfo = parms; this.input.lastPickInfo = undefined; this.input.pickInfo = pickInfo; this.input.pointerDownTime = this.time; this.input.lastInputTime = this.time; } this.pointerUp = function( parms, pickInfo ){ this.input.lastPointerInfo = this.input.pointerInfo; this.input.pointerInfo = undefined; this.input.lastPickInfo = this.input.pickInfo; this.input.pickInfo = undefined; this.input.lastInputTime = this.time; } this.pointerMove = function( parms, pickInfo ){ this.input.pointerEventTime = this.time; this.input.lastPointerInfo = this.input.pointerInfo; this.input.pointerInfo = parms; this.input.lastPickInfo = this.input.pickInfo; this.input.pickInfo = pickInfo; if ( this.pointerIsDown() /* && this.input.pointerEventTime - this.input.pointerDownTime > 6 */ ) { var active = this.input.futureActive(); var delta = this.input.pointerDelta(); var valid = this.navmode == "none" ? false : true; if ( valid && !active ) { this.future( 1/30 ).update(); } } this.input.lastInputTime = this.time; } this.pointerOut = function( parms ){ this.input.lastPointerInfo = this.input.pointerInfo; this.input.pointerInfo = undefined; this.input.lastPickInfo = this.input.pickInfo; this.input.pickInfo = undefined; this.input.lastInputTime = this.time; } this.pointerWheel = function( parms, pickInfo ) { this.input.pointerEventTime = this.time; this.input.lastPointerInfo = this.input.pointerInfo; this.input.pointerInfo = parms; this.input.lastPickInfo = this.input.pickInfo; this.input.pickInfo = pickInfo; if (!this.activeCameraComp) { this.getActiveCamera(); } if ( this.activeCameraComp ) { var cameraPos = this.activeCameraComp.translation; var cameraAt = this.getCameraAt(); if ( this.orbitRadius == -1 || this.input.pointerEventTime - this.input.lastUpdateTime > 0.5 ) { this.updateRadius(); } cameraAt = goog.vec.Vec3.scale( cameraAt, this.input.pickInfo.wheel.deltaY * -1, cameraAt ); if ( goog.vec.Vec3.magnitudeSquared( cameraAt ) > goog.vec.EPSILON ) { this.activeCameraComp.translation = goog.vec.Vec3.add( this.activeCameraComp.translation, goog.vec.Vec3.scale( goog.vec.Vec3.normalize( cameraAt, cameraAt ), this.distance(), cameraAt ), cameraAt ); } } this.input.lastInputTime = this.time; } this.keyDown = function( parms ) { var active = this.input.futureActive(); // capture the event input locally this.input.keyInfo = $.extend(true, {}, parms);; // store the current Time this.input.lastInputTime = this.time; // call future.update if update was not currently occurring if ( !active ) { this.future( 0.1 ).update(); } } this.keyUp = function( parms ) { this.input.keyInfo = $.extend(true, {}, parms); if ( Object.keys( parms.keysDown ).length == 0 ){ this.handleKeyDown(); } this.input.lastInputTime = this.time; } this.handleKeyDown = function() { if (!this.activeCameraComp) { this.getActiveCamera(); } if ( this.activeCameraComp ) { var deltaZ = 0; var kd = $.extend(true, {}, this.input.keyInfo.keysDown, this.input.keyInfo.keysUp); this.input.look = [ 0, 0 ]; this.input.move = [ 0, 0 ]; var temp; for ( var keyPress in kd ) { switch ( Number( kd[ keyPress ].code ) ) { case 87: //w case 38: //up this.input.move[1] += 1; break; case 83: //s case 40: //down this.input.move[1] += -1; break; case 37: // left case 65: //a this.input.move[0] += -1; break; case 39: // right case 68: //d this.input.move[0] += 1; break; case 81: // q this.input.look[0] += -1; break; case 69: // e this.input.look[0] += 1; break; case 90: // z temp = this.logger.enabled; this.logger.enabled = true; this.logger.info( "translation: [ " + Array.prototype.slice.call( this.activeCameraComp.translation ) + " ]" ); this.logger.info( " quaternion: [ " + Array.prototype.slice.call( this.activeCameraComp.quaternion ) + " ]" ); this.logger.info( " rotation: [ " + Array.prototype.slice.call( this.activeCameraComp.rotation ) + " ]" ); this.logger.info( " transform: [ " + Array.prototype.slice.call( this.activeCameraComp.transform ) + " ]" ); this.logger.info( " worldTransform: [ " + Array.prototype.slice.call( this.activeCameraComp.worldTransform ) + " ]" ); this.logger.enabled = temp; break; case 88: // x break; case 82: // r deltaZ += 1.0; break; case 67: // c deltaZ += - 1.0; break; default: break; } } this.input.keyInfo.keysUp = {}; if ( this.input.look[0] != 0 || this.input.look[1] != 0 ) { this.look( this.input.look[0], this.input.look[1] ); } if ( this.input.move[0] != 0 || this.input.move[1] != 0 ) { this.move( this.input.move[0], this.input.move[1] ); } if ( deltaZ != 0 ) { if ( this.navmode == "walk" ) { this.cameraZ( deltaZ ); } } } } this.onMove = function( x, y ) { if ( Math.abs( x ) < 0.1 && Math.abs( y ) < 0.1 ) { if ( this.input.moveActive ) { this.input.moveActive = false; this.input.move = [ 0, 0 ]; } return; } this.input.move = [ x, y ]; var active = this.input.futureActive(); if ( !this.input.moveActive ) { this.input.moveActive = true; this.move( x, y ); } if ( !active ) { this.future( 0.1 ).update(); } } this.move = function( x, y ) { if ( this.navmode == "walk" ) { this.translate( x, y ); } else if ( this.navmode == "orbit" ) { this.orbit( x, y ); } } this.onLook = function( x, y ) { if ( Math.abs( x ) < 0.1 && Math.abs( y ) < 0.1 ) { if ( this.input.lookActive ) { this.input.lookActive = false; this.input.look = [ 0, 0 ]; } return; } this.input.look = [ x, y ]; var active = this.input.futureActive(); if ( !this.input.lookActive ) { this.input.lookActive = true; this.look( x, y ); } if ( !active ) { this.future( 0.1 ).update(); } } this.look = function( x, y ) { if (!this.activeCameraComp) this.getActiveCamera(); if ( this.activeCameraComp ) { if ( this.input.pointerInfo && this.input.pointerInfo.modifiers.ctrl ) { if ( y != 0 ) { this.activeCameraComp.rotateBy( [ 1, 0, 0, -y * this.angularDistance() ], 0 ) } } else { if ( x != 0 ) { this.activeCameraComp.rotateBy( [ 0, 0, 1, -x * this.angularDistance() ], 0 ) } } } } this.zoom = function( dirIn ) { if (!this.activeCameraComp) { this.getActiveCamera(); } if ( this.activeCameraComp ) { var camerapos = this.activeCameraComp.translation; var camerarot = this.getCameraAt(); var yinc = 0; var xinc = 0; var zinc = 0; var speed = this.distance(); if ( !dirIn ) speed = -speed; yinc = yinc + parseFloat( camerarot[1] ) * speed; xinc = xinc + parseFloat( camerarot[0] ) * speed; if ( this.orbitRadius == -1 || this.time - this.input.lastUpdateTime > 0.5 ) { this.updateRadius(); } if ( xinc != 0 || yinc != 0 ) { var origX, origY, origZ; var x, y, z, atBounds = false; origX = camerapos[0]; origY = camerapos[1]; origZ = camerapos[2]; x = origX + xinc * this.distance() * mult; y = origY + yinc * this.distance() * mult; z = origZ; var ratioHR = camerapos[2] / this.orbitRadius; zinc = Math.sqrt( Math.pow( x-origX, 2 ) + Math.pow( y-origY, 2 ) ) * ratioHR; if ( parms.wheelDeltaY > 0 ) zinc *= -1; z = origZ + zinc; this.activeCameraComp.translation = [ x, y, z ]; } } } this.distance = function(){ var dist = this.translationSpeed * 10 * this.timeElapsed(); return dist; } this.angularDistance = function() { // Should take 4 seconds to go around 360 degrees var dist = this.rotationSpeed * 90 * this.timeElapsed(); return dist; } this.cameraZ = function( dir ) { if (!this.activeCameraComp) this.getActiveCamera(); if ( this.activeCameraComp ) { var pos = this.activeCameraComp.translation; this.activeCameraComp.translation = [ pos[0], pos[1], pos[2] + ( this.distance() * dir ) ]; } } this.updateRadius = function() { var pos = this.activeCameraComp.translation || [ 1, 1, 1 ]; var oldRadius = this.orbitRadius; // Assuming the orbit center is the origin, the radius is just the length of the line segment from the // origin to the active camera component this.orbitRadius = Math.sqrt( (pos[0] * pos[0]) + (pos[1] * pos[1]) + (pos[2] * pos[2]) ); var dist2D = Math.sqrt( (pos[0] * pos[0]) + (pos[1] * pos[1]) ); phi = Math.acos( pos[2] / this.orbitRadius ); if ( 0 <= pos[0] ) theta = Math.asin( pos[1]/dist2D ); else theta = Math.PI - Math.asin( pos[1]/dist2D ); this.input.lastUpdateTime = this.time; } this.orbit = function( xDelta, yDelta ){ if (!this.activeCameraComp) this.getActiveCamera(); if ( this.activeCameraComp ) { this.updateRadius(); var pixelToRadian = _2PI * this.angularDistance() / 360; var phiDelta = -yDelta * pixelToRadian; var thetaDelta = -xDelta * pixelToRadian; phi = phi - phiDelta; theta = theta - thetaDelta; if ( theta >= _2PI ) theta -= _2PI; else if ( theta < 0 ) theta += _2PI; if ( phi > _85 ) phi = _85; else if ( phi < _10 ) phi = _10; // Spherical to Cartesian var x = this.orbitRadius * Math.sin( phi ) * Math.cos( theta ); var y = this.orbitRadius * Math.sin( phi ) * Math.sin( theta ); var z = this.orbitRadius * Math.cos( phi ); this.activeCameraComp.translation = [ x, y, z ]; } } this.getCameraAt = function() { var camRotMat = this.activeCameraComp.rotationMatrix; return goog.vec.Vec3.createFromValues( camRotMat[4], camRotMat[5], camRotMat[6] ); } this.getCameraUp = function() { var camRotMat = this.activeCameraComp.rotationMatrix; return goog.vec.Vec3.createFromValues( camRotMat[8], camRotMat[9], camRotMat[10] ); } this.getCameraRight = function() { var camRotMat = this.activeCameraComp.rotationMatrix; return goog.vec.Vec3.createFromValues( camRotMat[0], camRotMat[1], camRotMat[2] ); } this.getCameraVec = function( x, y, z ) { var camRotMat = this.activeCameraComp.rotationMatrix; var camAt = goog.vec.Mat4.multVec4( camRotMat, goog.vec.Vec4.createFromValues( x, y, z, 1 ), goog.vec.Vec3.create() ); return camAt; } this.translate = function( x, y ) { if (!this.activeCameraComp) this.getActiveCamera(); if ( this.activeCameraComp ) { var trans = this.getCameraVec( x, y, 0 ); trans[2] = 0; if ( goog.vec.Vec3.magnitudeSquared( trans ) > goog.vec.EPSILON ) { // TODO: Clean this up - right now the two vectors are added together and both vectors are replaced // w/ the result. That's probably not the desired behavior. this.activeCameraComp.translation = goog.vec.Vec3.add( this.activeCameraComp.translation, goog.vec.Vec3.scale( goog.vec.Vec3.normalize( trans, trans ), this.distance(), trans ), trans ); } } } this.update = function() { if ( this.input ) { var delta = this.input.pointerDelta(); var pi = this.input.pointerInfo; if ( delta ) { if ( this.pointerIsDown() ) { if ( this.input.pointerInfo.buttons.left ) { var pi = this.input.pointerInfo; switch( this.navmode ) { case "orbit": this.orbit( delta[0], delta[1] ); break; case "walk": if ( this.dragModeMove ) this.move( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 ); else this.look( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 ); break; } } } if ( this.input.lookActive ) { this.look( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 ); } if ( this.input.moveActive ) { this.move( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 ); } } if ( this.input.keysAreDown() ) { this.handleKeyDown(); } if ( this.input.button1Down ) { if ( this.navMode == "walk" ) { this.cameraZ( 1.0 ); } else if ( this.navMode == "orbit" ) { this.zoom( true ); } } if ( this.input.button2Down ) { if ( this.navMode == "walk" ) { this.cameraZ( -1.0 ); } else if ( this.navMode == "orbit" ) { this.zoom( false ); } } if ( this.input.futureActive() ) { this.future( 0.1 ).update(); } } this.input.lastInputTime = this.time; } //@ sourceURL=navscene.vwf