import { Observable , ReplaySubject , Subject } from 'rxjs' ; import { assert , log , logIf , LogLevel , logVerbosity } from './auxiliaries' ; import { clamp } from './gl-matrix-extensions' ; /**
 * This internal interface is not intended for module export and should not be used as generic rendering/controller
 * interface. The renderer interface should be used for rendering related tasks instead.
 */ export interface Controllable { /**
     * Used to trigger an renderer update. Returns true iff rendering is invalidated and
     * a new multi-frame should be rendered-
     */ update ( multiFrameNumber ) : boolean ; /**
     * Used to prepare rendering of a multi-frame.
     */ prepare ( ) : void ; /**
     * Used to trigger rendering of a single, intermediate frame.
     */ frame ( frameNumber ) : void ; /**
     * Used to swap/blit frame from back to front buffer.
     */ swap ( ) : void ; } /**
 * This class controls the rendering flow by means of triggering rendering of a well defined amount of frames
 * (multi-frame number) for frame accumulation. Single frame rendering is handled with a multi-frame number of 1. If a
 * full multi-frame is accumulated, rendering halts. The rendering is not intended to be controlled by owning objects,
 * but via invalidation from within the renderer instead. However, an explicit redraw of a full single or multi-frame
 * can be invoked by calling `update()`. Furthermore, when using multi-frame rendering, the renderer can be halted at a
 * specific frame by setting a debug-frame number.
 *
 * Terminology: a multi-frame is the final result after accumulating a number of intermediate frames (frame). The
 * number of intermediate frames is defined by the multi-frame number. For a multi-frame, the controller invokes the
 * `prepare` on a controllable first, followed by multiple `frame` and `swap` calls. Please note that the
 * adaptive batch mode is yet experimental (can be enabled using `batchSize`).
 */ export class Controller { /**
     * Toggle for debug outputs; if true control flow will be logged.
     */ protected static _debug = false ; set debug ( value ) { if ( value && logVerbosity ( ) < LogLevel . Debug ) { logVerbosity ( LogLevel . Debug ) ; log ( LogLevel . Debug , `changed log verbosity to ${ LogLevel . Debug } (debug)` ) ; } Controller . _debug = value ; } /**
     * Number of intermediate frames that are rendered during one browser frame
     */ protected _batchSize = 1 ; set batch ( size ) { log ( LogLevel . Warning , `(adaptive) batch multi-frame rendering is experimental for now` ) ; this . _batchSize = Math . max ( 1 , size ) ; } see multiFrameNumber * This property can be observed, e.g., `aController.multiFrameNumberObservable.subscribe()`.
      protected _multiFrameNumber = 1 ; protected _multiFrameNumberSubject = new ReplaySubject < number > ( 1 ) ; see debugFrameNumber * This property can be observed, e.g., `aController.debugFrameNumberObservable.subscribe()`.
      protected _debugFrameNumber = 0 ; protected _debugFrameNumberSubject = new ReplaySubject < number > ( 1 ) ; see frameNumber * This property can be observed, e.g., `aController.frameNumberObservable.subscribe()`.
      protected _frameNumber = 0 ; protected _frameNumberSubject = new ReplaySubject < number > ( 1 ) ; see multiFrameDelay protected _multiFrameDelay = 0 ; /** Observable event that is triggered after frame invocation (renderer). */ protected _postFrameEventSubject = new Subject < number > ( ) ; /** Observable event that is triggered after swap invocation (renderer). */ protected _postSwapEventSubject = new Subject < number > ( ) ; /**
     * Controllable, e.g., an instance of a Renderer.
     */ protected _controllable : Controllable | undefined ; /**
     * Holds the handle of the pending / executed animate frame request, if requested. Throughout the controller, only a
     * single request at a time is allowed.
     */ protected _animationFrameID = 0 ; /**
     * Holds the handle of the running timeout to execute a new multi frame. Undefined if we currently do not wait for
     * a new multi frame.
     */ protected _timeoutID : number | undefined ; /**
     * Blocking updates can be used to re-configure the controller without triggering
     */ protected _block = false ; /**
     * Number of update requested while being in blocked mode. If there is one or more blocked requests, an update will
     * be triggered when unblocked.
     */ protected _blockedUpdates = 0 ; /**
     * Total number of rendered intermediate frames.
     */ protected _intermediateFrameCount = 0 ; /**
     * Total number of completed multi-frames.
     */ protected _multiFrameCount = 0 ; /**
     * Time tracker used to the minimum and maximum frame time of an intermediate frame (per multi-frame).
     */ protected _intermediateFrameTimes = new Array < number > ( 2 ) ; /**
     * Time tracker used to accumulate all durations of executed frame and swap callbacks per multi-frame. This is the
     * net rendering time and is used to derive the average frame time.
     */ protected _multiFrameTime : number ; /**
     * Time tracker used to capture the time the update callback took.
     */ protected _updateFrameTime : number ; /**
     * Used to measure the gross rendering time of a multi-frame. The first point in time denotes the start of the
     * rendering, the second, the point in time the last frame was rendered.
     *
     * Note: point in times might be shifted due to (un)pausing. Their intent is to allow measuring the rendering
     * duration, nothing else.
     */ protected _multiTime : Array < number > = [ 0.0 , 0.0 ] ; protected _invalidated = false ; protected _force = false ; protected request ( source = Controller . RequestType . Frame ) { this . _animationFrameID = 0 ; if ( this . _block ) { this . _blockedUpdates ++ ; return ; } this . _animationFrameID = window . requestAnimationFrame ( ( ) => this . invoke ( source ) ) ; } protected invoke ( source ) { if ( this . _animationFrameID === 0 ) { return ; } assert ( this . _controllable !== undefined , `frame sequence invoked without controllable set` ) ; if ( this . _invalidated ) { this . _invalidated = false ; const redraw = this . invokeUpdate ( ) ; if ( redraw || this . _force ) { this . _force = false ; this . _frameNumber = 0 ; this . cancelWaitMultiFrame ( ) ; this . invokePrepare ( ) ; } } if ( source === Controller . RequestType . Frame && this . _frameNumber === 1 ) { if ( this . _timeoutID === undefined ) { this . startWaitMultiFrame ( ) ; } this . _animationFrameID = 0 ; return ; } if ( this . isMultiFrameFinished ( ) ) { this . _animationFrameID = 0 ; return ; } this . invokeFrameAndSwap ( ) ; this . request ( ) ; } /**
     * Actual invocation of the controllable's update method. Returns true if multi frame rendering should be restarted,
     * false otherwise.
     */ protected invokeUpdate ( ) { logIf ( Controller . _debug , LogLevel . Debug , `c invoke update     | ` + `pending: '${ this . _animationFrameID }', mfnum: ${ this . _multiFrameNumber }` ) ; const redraw = ( this . _controllable as Controllable ) . update ( this . _multiFrameNumber ) ; return redraw ; } /**
     * Actual invocation of the controllable's prepare method.
     */ protected invokePrepare ( ) { logIf ( Controller . _debug , LogLevel . Debug , `c invoke prepare    |` ) ; this . _multiFrameTime = 0.0 ; this . _intermediateFrameTimes [ 0 ] = Number . MAX_VALUE ; this . _intermediateFrameTimes [ 1 ] = Number . MIN_VALUE ; this . _multiTime [ 0 ] = performance . now ( ) ; ( this . _controllable as Controllable ) . prepare ( ) ; this . _multiTime [ 1 ] = performance . now ( ) ; const updateDuration = this . _multiTime [ 1 ] - this . _multiTime [ 0 ] ; this . _multiFrameTime = updateDuration ; this . _updateFrameTime = updateDuration ; } /**
     * Invokes rendering of an intermediate frame, increments the frame counter, and requests rendering of the next
     * frame. The rendering is invoked by means of a callback to the canvas renderer. This function implements various
     * asserts to assure correct control logic and absolutely prevent unnecessary frame requests.
     */ protected invokeFrameAndSwap ( ) { logIf ( Controller . _debug , LogLevel . Debug , `c invoke frame      | pending: '${ this . _animationFrameID }'` ) ; const debug = this . _debugFrameNumber > 0 ; assert ( ! debug || this . _frameNumber < this . _debugFrameNumber , `frame number about to exceed debug-frame number` ) ; const t0 = performance . now ( ) ; let batchEnd = Math . min ( this . _multiFrameNumber , this . _frameNumber + this . _batchSize ) ; if ( this . _debugFrameNumber > 0 ) { batchEnd = Math . min ( batchEnd , this . _debugFrameNumber ) ; } for ( ; this . _frameNumber < batchEnd ; ++ this . _frameNumber ) { logIf ( Controller . _debug , LogLevel . Debug , `c -> frame          | frame: ${ this . _frameNumber }` ) ; ( this . _controllable as Controllable ) . frame ( this . _frameNumber ) ; this . _postFrameEventSubject . next ( this . _frameNumber ) ; ++ this . _intermediateFrameCount ; } logIf ( Controller . _debug , LogLevel . Debug , `c -> swap           |` ) ; ( this . _controllable as Controllable ) . swap ( ) ; this . _postSwapEventSubject . next ( this . _frameNumber ) ; this . _multiTime [ 1 ] = performance . now ( ) ; const frameDuration = this . _multiTime [ 1 ] - t0 ; this . _multiFrameTime += frameDuration ; this . _intermediateFrameTimes [ 0 ] = Math . min ( this . _intermediateFrameTimes [ 0 ] , frameDuration ) ; this . _intermediateFrameTimes [ 1 ] = Math . max ( this . _intermediateFrameTimes [ 1 ] , frameDuration ) ; this . frameNumberNext ( ) ; } protected startWaitMultiFrame ( ) { const startMultiFrame = ( ) => { this . request ( Controller . RequestType . MultiFrame ) ; this . _timeoutID = undefined ; } ; this . _timeoutID = window . setTimeout ( startMultiFrame , this . _multiFrameDelay ) ; } protected cancelWaitMultiFrame ( ) { if ( this . _timeoutID !== undefined ) { window . clearTimeout ( this . _timeoutID ) ; this . _timeoutID = undefined ; } } protected isMultiFrameFinished ( ) { if ( this . _debugFrameNumber > 0 ) { return this . _frameNumber === this . _debugFrameNumber ; } return this . _frameNumber === this . _multiFrameNumber ; } /**
     * Utility for communicating this._multiFrameNumber changes to its associated subject.
     */ protected multiFrameNumberNext ( ) { this . _multiFrameNumberSubject . next ( this . _multiFrameNumber ) ; } /**
     * Utility for communicating this._debugFrameNumber changes to its associated subject.
     */ protected debugFrameNumberNext ( ) { this . _debugFrameNumberSubject . next ( this . _debugFrameNumber ) ; } /**
     * Utility for communicating this._frameNumber changes to its associated subject.
     */ protected frameNumberNext ( ) { this . _frameNumberSubject . next ( this . _frameNumber ) ; } update ( force = false ) { this . _invalidated = true ; this . _force = this . _force || force ; if ( this . _animationFrameID === 0 ) { this . request ( ) ; } } /**
     * Block implicit updates, e.g., caused by various setters. This can be used to reconfigure the controller without
     * triggering to multiple intermediate updates. The block updates mode can be exited using `unblock`.
     */ block ( ) { logIf ( Controller . _debug , LogLevel . Debug , `c block   ${ this . _block ? '(ignored) ' : '          ' }|` ) ; this . _block = true ; } /**
     * Unblock updates. If there was at least one blocked update request, an immediate update is invoked.
     */ unblock ( ) { logIf ( Controller . _debug , LogLevel . Debug , `c unblock ${ ! this . _block ? '(ignored) ' : '          ' }` + `| blocked: #${ this . _blockedUpdates }` ) ; if ( ! this . _block ) { return ; } this . _block = false ; if ( this . _blockedUpdates > 0 ) { this . _blockedUpdates = 0 ; this . update ( ) ; } } cancel ( ) { if ( this . _animationFrameID === 0 ) { return ; } window . cancelAnimationFrame ( this . _animationFrameID ) ; this . _animationFrameID = 0 ; } returns get blocked ( ) : boolean { return this . _block ; } param controllable set controllable ( controllable ) { if ( controllable === this . _controllable ) { return ; } this . _controllable = controllable ; this . update ( true ) ; } returns get multiFrameNumber ( ) : number { return this . _multiFrameNumber ; } param multiFrameNumber set multiFrameNumber ( multiFrameNumber ) { const value = Math . max ( 1 , isNaN ( multiFrameNumber ) ? 1 : multiFrameNumber ) ; if ( value === this . _multiFrameNumber ) { return ; } this . _multiFrameNumber = value ; this . multiFrameNumberNext ( ) ; logIf ( value !== multiFrameNumber , LogLevel . Debug , `multi-frame number adjusted to ${ value }, given ${ multiFrameNumber }` ) ; if ( this . debugFrameNumber > this . multiFrameNumber ) { this . debugFrameNumber = this . multiFrameNumber ; } else { this . update ( ) ; } } /**
     * Observable that can be used to subscribe to multi-frame number changes.
     */ get multiFrameNumber$ ( ) : Observable < number > { return this . _multiFrameNumberSubject . asObservable ( ) ; } returns get debugFrameNumber ( ) : number { return this . _debugFrameNumber ; } param debugFrameNumber set debugFrameNumber ( debugFrameNumber ) { const value = clamp ( isNaN ( debugFrameNumber ) ? 0 : debugFrameNumber , 0 , this . multiFrameNumber ) ; if ( value === this . _debugFrameNumber ) { return ; } this . _debugFrameNumber = value ; this . debugFrameNumberNext ( ) ; logIf ( value !== debugFrameNumber , LogLevel . Debug , `debug-frame number adjusted to ${ value }, given ${ debugFrameNumber }` ) ; this . update ( this . debugFrameNumber < this . _frameNumber ) ; } /**
     * Observable that can be used to subscribe to debug-frame number changes.
     */ get debugFrameNumber$ ( ) : Observable < number > { return this . _debugFrameNumberSubject . asObservable ( ) ; } param multiFrameDelay set multiFrameDelay ( multiFrameDelay ) { const value = Math . max ( 0 , multiFrameDelay ) ; if ( value === this . _multiFrameDelay ) { return ; } this . _multiFrameDelay = value ; } returns get multiFrameDelay ( ) : number { return this . _multiFrameDelay ; } returns get frameNumber ( ) : number { return this . _frameNumber ; } /**
     * Observable that can be used to subscribe to frame number changes.
     */ get frameNumber$ ( ) : Observable < number > { return this . _frameNumberSubject . asObservable ( ) ; } returns get intermediateFrameCount ( ) : number { return this . _intermediateFrameCount ; } returns get multiFrameCount ( ) : number { return this . _multiFrameCount ; } returns get averageFrameTime ( ) : number { return this . _frameNumber === 0 ? 0.0 : this . _multiFrameTime / this . _frameNumber ; } returns get updateFrameTime ( ) : number { return this . _updateFrameTime ; } returns get minimumFrameTime ( ) : number { return this . _intermediateFrameTimes [ 0 ] ; } returns get maximumFrameTime ( ) : number { return this . _intermediateFrameTimes [ 1 ] ; } returns get multiFrameTime ( ) : number { return this . _frameNumber === 0 ? 0.0 : this . _multiTime [ 1 ] - this . _multiTime [ 0 ] ; } returns get framesPerSecond ( ) : number { return this . _frameNumber === 0 ? 0.0 : 1000.0 / ( this . multiFrameTime / this . _frameNumber ) ; } /**
     * Observable that can be used to subscribe to post frame events.
     */ get postFrameEvent$ ( ) : Observable < number > { return this . _postFrameEventSubject . asObservable ( ) ; } /**
     * Observable that can be used to subscribe to post swap events.
     */ get postSwapEvent$ ( ) : Observable < number > { return this . _postSwapEventSubject . asObservable ( ) ; } } export namespace Controller { export enum RequestType { Frame , MultiFrame } }