import { vec2 } from 'gl-matrix' ; import { Camera } from './camera' ; import { EventHandler } from './eventhandler' ; import { PointerLock } from './pointerlock' ; import { Invalidate } from './renderer' ; import { log , LogLevel } from './auxiliaries' ; import { FirstPersonModifier } from './firstpersonmodifier' ; import { PanModifier } from './panmodifier' ; import { PinchZoomModifier } from './pinchzoommodifier' ; import { TrackballModifier } from './trackballmodifier' ; import { TurntableModifier } from './turntablemodifier' ; import { EventProvider } from './eventhandler' ; import { WheelZoomModifier } from './wheelzoommodifier' ; todo export class Navigation { /**
     * The navigation's invalidation callback. This should usually be setup by the owning renderer and invoke the
     * same callback the renderer has been given by the canvas. This invalidation is required, when continuous
     * rendering is not present, events might cause need for new rendering requests.
     */ protected _invalidate : Invalidate ; see camera protected _camera : Camera ; /**
     * Currently active metaphor.
     */ protected _rotationMetaphor : Navigation . RotationMetaphor ; /**
     * Identifies the active camera modifier.
     */ protected _mode : Navigation . Modes | undefined ; /**
     * Specifies, whether or not rotation mode should be invoked on any move event, regardless of buttons.
     */ protected _alwaysRotateOnMove = false ; /**
     * First person camera modifier.
     */ protected _firstPerson : FirstPersonModifier | undefined ; /**
     * Trackball camera modifier.
     */ protected _trackball : TrackballModifier | undefined ; /**
     * Turntable camera modifier.
     */ protected _turntable : TurntableModifier | undefined ; /**
     * Pan camera modifier.
     */ protected _pan : PanModifier ; /**
     * Pinch camera modifier.
     */ protected _pinch : PinchZoomModifier ; /**
     * Wheel zoom modifier.
     */ protected _wheelZoom : WheelZoomModifier ; /**
     * Even handler used to forward/map events to specific camera modifiers.
     */ protected _eventHandler : EventHandler ; /**
     * This keeps track of all events that are currently interacting with the canvas.
     * It maps from pointer id to the currecnt position.
     */ protected _activeEvents : Map < number , PointerEvent > ; /**
     * Keep track of the latest interaction in order to allow a cooldown before the next
     * interaction is allowed.
     */ protected _lastInteractionTime : number ; constructor ( invalidate , eventProvider ) { this . _invalidate = invalidate ; this . _eventHandler = new EventHandler ( invalidate , eventProvider ) ; this . _eventHandler . pushPointerDownHandler ( ( latests , previous ) => this . onPointerDown ( latests , previous ) ) ; this . _eventHandler . pushPointerUpHandler ( ( latests , previous ) => this . onPointerUp ( latests , previous ) ) ; this . _eventHandler . pushPointerEnterHandler ( ( latests , previous ) => this . onPointerEnter ( latests , previous ) ) ; this . _eventHandler . pushPointerLeaveHandler ( ( latests , previous ) => this . onPointerLeave ( latests , previous ) ) ; this . _eventHandler . pushPointerMoveHandler ( ( latests , previous ) => this . onPointerMove ( latests , previous ) ) ; this . _eventHandler . pushPointerCancelHandler ( ( latests , previous ) => this . onPointerCancel ( latests , previous ) ) ; this . _eventHandler . pushMouseWheelHandler ( ( latests , previous ) => this . onWheel ( latests , previous ) ) ; this . rotationMetaphor = Navigation . RotationMetaphor . Turntable ; this . _pan = new PanModifier ( ) ; this . _pinch = new PinchZoomModifier ( ) ; this . _wheelZoom = new WheelZoomModifier ( ) ; this . _activeEvents = new Map ( ) ; } param event protected mode ( ) { const events = Array . from ( this . _activeEvents . values ( ) ) ; const primaryEvent = this . getPrimaryEvent ( events ) ; if ( primaryEvent === undefined ) { log ( LogLevel . Warning , 'No primary pointer event detected in Navigation::mode.' ) ; return ; } const isMouseEvent = primaryEvent . pointerType === 'mouse' ; const isTouchEvent = primaryEvent . pointerType === 'touch' || primaryEvent . pointerType === 'pen' ; const isPrimaryButtonDown = primaryEvent . buttons & 1 ; const isShiftKeyDown = primaryEvent . shiftKey ; const isPointerLockedRotate = PointerLock . active ( ) && this . _alwaysRotateOnMove ; const numPointers = this . _activeEvents . size ; const isMouseRotate = isMouseEvent && isPrimaryButtonDown && numPointers === 1 ; const isTouchRotate = isTouchEvent && numPointers === 1 ; const isMousePan = isMouseEvent && isPrimaryButtonDown && isShiftKeyDown && numPointers === 1 ; const isMultiTouch = isTouchEvent && numPointers === 2 ; if ( isPointerLockedRotate ) { return Navigation . Modes . Rotate ; } if ( isMousePan ) { return Navigation . Modes . Pan ; } else if ( isMultiTouch ) { return Navigation . Modes . MultiTouch ; } else if ( isMouseRotate || isTouchRotate ) { return Navigation . Modes . Rotate ; } return undefined ; } protected resolveMultiTouch ( ) { if ( this . _activeEvents . size < 2 ) { log ( LogLevel . Warning , 'MultiTouch resolution was canceled because less than two touches were detected.' ) ; return undefined ; } const events = Array . from ( this . _activeEvents . values ( ) ) ; const direction1 = vec2 . fromValues ( events [ 0 ] . movementX , events [ 0 ] . movementY ) ; const direction2 = vec2 . fromValues ( events [ 1 ] . movementX , events [ 1 ] . movementY ) ; if ( vec2 . length ( direction1 ) === 0 || vec2 . length ( direction2 ) === 0 ) { return Navigation . Modes . Zoom ; } vec2 . normalize ( direction1 , direction1 ) ; vec2 . normalize ( direction2 , direction2 ) ; const cosAngle = vec2 . dot ( direction1 , direction2 ) ; const panThreshold = 0.2 ; if ( cosAngle > panThreshold ) { return Navigation . Modes . Pan ; } return Navigation . Modes . Zoom ; } protected rotate ( start ) { if ( this . _activeEvents . size !== 1 ) { log ( LogLevel . Info , 'Rotate event was canceled because less or more than two pointers were detected.' ) ; return ; } const events = Array . from ( this . _activeEvents . values ( ) ) ; const point = this . _eventHandler . offsets ( events [ 0 ] ) [ 0 ] ; switch ( this . _rotationMetaphor ) { case Navigation . RotationMetaphor . FirstPerson : const firstPerson = this . _firstPerson as FirstPersonModifier ; let movement ; if ( PointerLock . active ( ) && event instanceof MouseEvent ) { movement = vec2 . fromValues ( ( event as MouseEvent ) . movementX , ( event as MouseEvent ) . movementY ) ; } start ? firstPerson . initiate ( point ) : firstPerson . process ( point , movement ) ; break ; case Navigation . RotationMetaphor . Trackball : const trackball = this . _trackball as TrackballModifier ; start ? trackball . initiate ( point ) : trackball . process ( point ) ; break ; case Navigation . RotationMetaphor . Turntable : const turntable = this . _turntable as TurntableModifier ; start ? turntable . initiate ( point ) : turntable . process ( point ) ; break ; default : break ; } } protected pan ( start ) { const events = Array . from ( this . _activeEvents . values ( ) ) ; const event = this . getPrimaryEvent ( events ) ; if ( event === undefined ) { log ( LogLevel . Warning , 'Pan event was canceled because no primary event was detected.' ) ; return ; } const point = this . _eventHandler . offsets ( event ) [ 0 ] ; const pan = this . _pan as PanModifier ; start ? pan . initiate ( point ) : pan . process ( point ) ; } protected pinch ( start ) { if ( this . _activeEvents . size !== 2 ) { log ( LogLevel . Info , 'Pinch event was canceled because less or more than two pointers were detected.' ) ; return ; } const events = Array . from ( this . _activeEvents . values ( ) ) ; const point1 = this . _eventHandler . offsets ( events [ 0 ] ) [ 0 ] ; const point2 = this . _eventHandler . offsets ( events [ 1 ] ) [ 0 ] ; const pinch = this . _pinch as PinchZoomModifier ; start ? pinch . initiate ( point1 , point2 ) : pinch . process ( point1 , point2 ) ; } protected getPrimaryEvent ( events ) { for ( const event of events ) { if ( event . isPrimary ) { return event ; } } return undefined ; } protected onPointerDown ( latests , previous ) { for ( const event of latests ) { this . _activeEvents . set ( event . pointerId , event ) ; } this . _mode = this . mode ( ) ; switch ( this . _mode ) { case Navigation . Modes . Rotate : this . rotate ( true ) ; break ; case Navigation . Modes . Pan : this . pan ( true ) ; break ; case Navigation . Modes . Zoom : this . pinch ( true ) ; break ; default : break ; } } protected onPointerUp ( latests , previous ) { for ( const pointer of latests ) { this . _activeEvents . delete ( pointer . pointerId ) ; } } protected onPointerEnter ( latests , previous ) { } protected onPointerLeave ( latests , previous ) { for ( const pointer of latests ) { this . _activeEvents . delete ( pointer . pointerId ) ; } } protected onPointerCancel ( latests , previous ) { for ( const pointer of latests ) { this . _activeEvents . delete ( pointer . pointerId ) ; } } protected onPointerMove ( latests , previous ) { for ( const event of latests ) { this . _activeEvents . set ( event . pointerId , event ) ; } if ( this . _mode === undefined ) { return ; } const events = Array . from ( this . _activeEvents . values ( ) ) ; const primaryEvent = this . getPrimaryEvent ( events ) ; /**
         * Update the mode for every movement when using a mouse. This is necessary since mouse events do not trigger
         * 'pointerup' events, so we need to figure out when the primary button is released manually
         */ if ( primaryEvent && primaryEvent . pointerType === 'mouse' ) { this . _mode = this . mode ( ) ; } /**
         * Handle the case where this is the first movement of a multi-touch gesture. We need to find out which
         * kind of gesture is executed.
         */ let modeUpdated = false ; if ( this . _mode === Navigation . Modes . MultiTouch ) { this . _mode = this . resolveMultiTouch ( ) ; modeUpdated = true ; } switch ( this . _mode ) { case Navigation . Modes . Rotate : this . rotate ( modeUpdated ) ; break ; case Navigation . Modes . Pan : this . pan ( modeUpdated ) ; break ; case Navigation . Modes . Zoom : this . pinch ( modeUpdated ) ; break ; default : break ; } this . _lastInteractionTime = performance . now ( ) ; } protected onWheel ( latests , previous ) { const event = latests [ 0 ] ; this . _wheelZoom . process ( event . deltaY ) ; } /**
     * Update should invoke navigation specific event processing. When using, e.g., an event handler, the event handlers
     * update method should be called in order to have navigation specific event processing invoked.
     */ update ( ) { this . _eventHandler . update ( ) ; } /**
     * The camera that is to be modified in response to various events.
     */ set camera ( camera ) { this . _camera = camera ; if ( this . _firstPerson ) { this . _firstPerson . camera = camera ; } if ( this . _trackball ) { this . _trackball . camera = camera ; } if ( this . _turntable ) { this . _turntable . camera = camera ; } if ( this . _pan ) { this . _pan . camera = camera ; } if ( this . _pinch ) { this . _pinch . camera = camera ; } if ( this . _wheelZoom ) { this . _wheelZoom . camera = camera ; } } /**
     * Configure this navigation's metaphor.
     */ set rotationMetaphor ( metaphor ) { if ( this . _rotationMetaphor === metaphor ) { return ; } this . _firstPerson = undefined ; this . _trackball = undefined ; this . _turntable = undefined ; this . _eventHandler . exitPointerLock ( ) ; this . _alwaysRotateOnMove = false ; this . _rotationMetaphor = metaphor ; switch ( this . _rotationMetaphor ) { case Navigation . RotationMetaphor . FirstPerson : this . _eventHandler . requestPointerLock ( ) ; this . _alwaysRotateOnMove = true ; this . _firstPerson = new FirstPersonModifier ( ) ; this . _firstPerson . camera = this . _camera ; break ; case Navigation . RotationMetaphor . Trackball : this . _trackball = new TrackballModifier ( ) ; this . _trackball . camera = this . _camera ; break ; case Navigation . RotationMetaphor . Turntable : this . _turntable = new TurntableModifier ( ) ; this . _turntable . camera = this . _camera ; break ; default : break ; } this . _invalidate ( true ) ; } get rotationMetaphor ( ) : Navigation . RotationMetaphor { return this . _rotationMetaphor ; } } export namespace Navigation { /**
     * Navigation modes used for identification of the current navigation intend, which is derived based on the event
     * types or gestures, regardless of the active navigation metaphor and its constraints.
     */ export enum Modes { Move , Pan , /**
         * MultiTouch is used when interaction with two fingers was initiated but it is not clear yet what
         * interaction the user intends
         */ MultiTouch , Rotate , Zoom , ZoomStep } /**
     * Navigation metaphors supported by the default navigation implementation.
     */ export enum RotationMetaphor { FirstPerson = 'firstperson' , Flight = 'flight' , Trackball = 'trackball' , Turntable = 'turntable' } }