diff --git a/examples/src/Components/CacheAndStreamingLogs.tsx b/examples/src/Components/CacheAndStreamingLogs.tsx new file mode 100644 index 00000000..bee1ac6e --- /dev/null +++ b/examples/src/Components/CacheAndStreamingLogs.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { CacheLog } from "../../../src"; + +interface StreamingReadoutProps { + playbackPlayingState: boolean; + isStreamingState: boolean; + cacheLog: CacheLog; + playbackFrame: number; + totalDuration: number; +} + +const CacheAndStreamingLogs: React.FC = ({ + playbackPlayingState, + isStreamingState, + cacheLog, + playbackFrame, + totalDuration, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const { size, enabled, maxSize, firstFrameNumber, lastFrameNumber } = + cacheLog; + return ( +
+ + + {isOpen && ( + <> +
+ Playback State:{" "} + {playbackPlayingState === true ? "playing" : "paused"} +
+
+ Streaming State:{" "} + {isStreamingState == true + ? "streaming" + : "not streaming"} +
+
Cache Size: {size}
+
Cache Enabled: {enabled ? "Yes" : "No"}
+
Max Size: {maxSize}
+
First Frame Number: {firstFrameNumber}
+
Last Frame Number: {lastFrameNumber}
+
Current playback frame: {playbackFrame}
+
Total Duration: {totalDuration}
+ + )} +
+ ); +}; + +export default CacheAndStreamingLogs; diff --git a/examples/src/Viewer.tsx b/examples/src/Viewer.tsx index f5fa2fb4..29a799f5 100644 --- a/examples/src/Viewer.tsx +++ b/examples/src/Viewer.tsx @@ -24,6 +24,7 @@ import SimulariumViewer, { ErrorLevel, NetConnectionParams, TrajectoryFileInfo, + CacheLog, } from "../../src/index"; import { nullAgent, TrajectoryType } from "../../src/constants"; @@ -57,9 +58,7 @@ import { UI_TEMPLATE_DOWNLOAD_URL_ROOT, UI_TEMPLATE_URL_ROOT, } from "./api-settings"; - -import "../../style/style.css"; -import "./style.css"; +import CacheAndStreamingLogs from "./Components/CacheAndStreamingLogs"; let playbackFile = "TEST_LIVEMODE_API"; let queryStringFile = ""; @@ -98,6 +97,9 @@ interface ViewerState { initialPlay: boolean; firstFrameTime: number; followObjectData: AgentData; + cacheLog: CacheLog; + playbackPlaying: boolean; + streaming: boolean; } const simulariumController = new SimulariumController({}); @@ -131,6 +133,18 @@ const initialState: ViewerState = { initialPlay: true, firstFrameTime: 0, followObjectData: nullAgent(), + cacheLog: { + size: 0, + numFrames: 0, + maxSize: 0, + enabled: false, + firstFrameNumber: 0, + firstFrameTime: 0, + lastFrameNumber: 0, + lastFrameTime: 0, + }, + playbackPlaying: false, + streaming: false, }; class Viewer extends React.Component { @@ -392,7 +406,7 @@ class Viewer extends React.Component { } this.setState({ currentFrame, currentTime }); if (currentFrame < 0) { - simulariumController.pause(); + this.handlePauseStreaming(); } } @@ -444,7 +458,8 @@ class Viewer extends React.Component { public handleTrajectoryInfo(data: TrajectoryFileInfo): void { console.log("Trajectory info arrived", data); // NOTE: Currently incorrectly assumes initial time of 0 - const totalDuration = (data.totalSteps - 1) * data.timeStepSize; + // const totalDuration = (data.totalSteps - 1) * data.timeStepSize; + const totalDuration = data.totalSteps; this.setState({ totalDuration, timeStep: data.timeStepSize, @@ -454,8 +469,29 @@ class Viewer extends React.Component { }); } + public handlePlay(): void { + simulariumController.resumePlayback(); + if (!simulariumController.isStreaming()) { + simulariumController.resumeStreaming(); + } + } + + public handlePause(): void { + simulariumController.pausePlayback(); + if ( + simulariumController.visData.currentFrameNumber > + simulariumController.visData.frameCache.getFirstFrameNumber() + ) { + simulariumController.resumeStreaming(); + } + } + public handleScrubTime(event): void { - simulariumController.gotoTime(parseFloat(event.target.value)); + simulariumController.movePlaybackTime(parseFloat(event.target.value)); + } + + public handleScrubFrame(event): void { + simulariumController.movePlaybackFrame(parseInt(event.target.value)); } public handleUIDisplayData(uiDisplayData: UIDisplayData): void { @@ -487,15 +523,16 @@ class Viewer extends React.Component { } public gotoNextFrame(): void { - simulariumController.gotoTime( - this.state.currentTime + this.state.timeStep - ); + simulariumController.movePlaybackFrame(this.state.currentFrame + 1); } public gotoPreviousFrame(): void { - simulariumController.gotoTime( - this.state.currentTime - this.state.timeStep - ); + simulariumController.movePlaybackFrame(this.state.currentFrame - 1); + } + + public handlePauseStreaming(): void { + simulariumController.pauseStreaming(); + this.setState({ streaming: simulariumController.isStreaming() }); } private translateAgent() { @@ -516,8 +553,8 @@ class Viewer extends React.Component { } private configureAndLoad() { - simulariumController.configureNetwork(this.netConnectionSettings); if (playbackFile.startsWith("http")) { + simulariumController.configureNetwork(this.netConnectionSettings); return this.loadFromUrl(playbackFile); } if (playbackFile === "TEST_LIVEMODE_API") { @@ -678,6 +715,18 @@ class Viewer extends React.Component { this.setState({ followObjectData: agentData }); }; + public handleCacheUpdate = (log: CacheLog) => { + this.setState({ + cacheLog: log, + playbackPlaying: simulariumController.isPlaying(), + streaming: simulariumController.isStreaming(), + }); + }; + + public handleStreamingChange = (streaming: boolean) => { + this.setState({ streaming }); + }; + public render(): JSX.Element { if (this.state.filePending) { const fileType = this.state.filePending.type; @@ -694,7 +743,7 @@ class Viewer extends React.Component {

{this.state.particleTypeNames.map((id, i) => { @@ -947,17 +1000,38 @@ class Viewer extends React.Component { updateAgentColorArray={this.updateAgentColorArray} setColorSelectionInfo={this.setColorSelectionInfo} /> - { - this.setRecordingEnabled( - !this.state.isRecordingEnabled - ); - }} - isRecordingEnabled={this.state.isRecordingEnabled} - /> + + {this.state.isRecordingEnabled && ( + + )} +
{ backgroundColor={[0, 0, 0]} lockedCamera={false} disableCache={false} - maxCacheSize={Infinity} // means no limit, provide limits in bytes, 1MB = 1000000, 1GB = 1000000000 + maxCacheSize={2e6} // means no limit, provide limits in bytes, 1MB = 1000000, 1GB = 1000000000 + onCacheUpdate={this.handleCacheUpdate.bind(this)} + onStreamingChange={(streaming) => { + this.handleStreamingChange(streaming); + }} />
diff --git a/src/constants.ts b/src/constants.ts index 3d12b4b8..ed39c617 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -54,3 +54,4 @@ export const nullAgent = (): AgentData => { export const AGENT_HEADER_SIZE = 3; // frameNumber, time, agentCount export const BYTE_SIZE_64_BIT_NUM = 8; +export const DEFAULT_PRE_FETCH_RATIO = 0; diff --git a/src/controller/index.ts b/src/controller/index.ts index c8ddf2d6..6683a22b 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -53,8 +53,7 @@ export default class SimulariumController { public onError?: (error: FrontEndError) => void; private networkEnabled: boolean; - private isPaused: boolean; - private isFileChanging: boolean; + public isFileChanging: boolean; private playBackFile: string; public constructor(params: SimulariumControllerParams) { @@ -102,7 +101,6 @@ export default class SimulariumController { } this.networkEnabled = true; - this.isPaused = false; this.isFileChanging = false; this.playBackFile = params.trajectoryPlaybackFile || ""; this.zoomIn = this.zoomIn.bind(this); @@ -160,6 +158,10 @@ export default class SimulariumController { this.handleTrajectoryInfo(trajFileInfo); } ); + this.visData.setOnCacheLimitReached(() => { + this.pauseStreaming(); + // this.simulator?.requestSingleFrame(this.visData.currentStreamingHead); + }); } public configureNetwork(config: NetConnectionParams): void { @@ -193,16 +195,17 @@ export default class SimulariumController { }); } - public start(): Promise { + public initializePrecomputedSimulation(): Promise { if (!this.simulator) { return Promise.reject(); } // switch back to 'networked' playback this.networkEnabled = true; - this.isPaused = false; + this.visData.isPlaying = false; this.visData.clearCache(); + // todo renaming of initalize/playback methods in ISimulator return this.simulator.startRemoteTrajectoryPlayback(this.playBackFile); } @@ -210,9 +213,10 @@ export default class SimulariumController { return this.visData.currentFrameData.time; } - public stop(): void { + public abortRemoteSimulation(): void { if (this.simulator) { this.simulator.abortRemoteSim(); + this.visData.updateStreamingState(false); } } @@ -249,16 +253,11 @@ export default class SimulariumController { ); } - public pause(): void { + public pauseStreaming(): void { if (this.networkEnabled && this.simulator) { + this.visData.updateStreamingState(false); this.simulator.pauseRemoteSim(); } - - this.isPaused = true; - } - - public paused(): boolean { - return this.isPaused; } public initializeTrajectoryFile(): void { @@ -267,32 +266,78 @@ export default class SimulariumController { } } - public gotoTime(time: number): void { + public movePlaybackTime(time: number): void { // If in the middle of changing files, ignore any gotoTime requests if (this.isFileChanging === true) return; if (this.visData.hasLocalCacheForTime(time)) { this.visData.gotoTime(time); + this.resumeStreaming(); } else { if (this.networkEnabled && this.simulator) { - // else reset the local cache, - // and play remotely from the desired simulation time - this.visData.clearCache(); this.simulator.gotoRemoteSimulationTime(time); + // get frame number for time + const frameNumber = time; + // time is framenumber *timestep + + this.visData.currentFrameNumber = frameNumber; + // this.resumeStreaming(); + } + } + } + + public movePlaybackFrame(frameNumber: number): void { + // If in the middle of changing files, ignore any gotoTime requests + console.log("movePlaybackFrame", frameNumber); + if (this.isFileChanging === true) return; + if (this.visData.hasLocalCacheForFrame(frameNumber)) { + this.visData.gotoFrame(frameNumber); + this.resumeStreaming(); + } else { + if (this.networkEnabled && this.simulator) { + this.clearLocalCache(); + this.visData.WaitForFrame(frameNumber); + this.visData.currentFrameNumber = frameNumber; + this.resumeStreaming(frameNumber); } } } public playFromTime(time: number): void { - this.gotoTime(time); - this.isPaused = false; + this.movePlaybackTime(time); + this.visData.isPlaying = true; + } + + public initalizeStreaming(): void { + if (this.simulator) { + this.simulator.requestSingleFrame(0); + this.simulator.resumeRemoteSim(); + this.visData.updateStreamingState(true); + } } - public resume(): void { + public resumeStreaming(startFrame?: number): void { + let requestFrame: number | null = null; + if (startFrame !== undefined) { + requestFrame = startFrame; + } else if (this.visData.remoteStreamingHeadPotentiallyOutOfSync) { + requestFrame = this.visData.currentStreamingHead; + } if (this.networkEnabled && this.simulator) { + if (requestFrame !== null) { + this.simulator.requestSingleFrame(requestFrame); + } this.simulator.resumeRemoteSim(); + this.visData.updateStreamingState(true); + this.visData.remoteStreamingHeadPotentiallyOutOfSync = false; } + } + + public pausePlayback(): void { + this.visData.isPlaying = false; + } - this.isPaused = false; + public resumePlayback(): void { + this.visData.isPlaying = true; } public clearFile(): void { @@ -300,7 +345,7 @@ export default class SimulariumController { this.playBackFile = ""; this.visData.clearForNewTrajectory(); this.disableNetworkCommands(); - this.pause(); + this.pauseStreaming(); if (this.visGeometry) { this.visGeometry.clearForNewTrajectory(); this.visGeometry.resetCamera(); @@ -336,16 +381,10 @@ export default class SimulariumController { this.simulator.handleError = () => noop; } + this.abortRemoteSimulation(); this.visData.WaitForFrame(0); this.visData.clearForNewTrajectory(); - this.stop(); - - // Do I still need this? test... - // if (this.simulator) { - // this.simulator.disconnect(); - // } - // don't create simulator if client wants to keep remote simulator and the // current simulator is a remote simulator if ( @@ -360,7 +399,7 @@ export default class SimulariumController { connectionParams.geoAssets ); this.networkEnabled = true; // This confuses me, because local files also go through this code path - this.isPaused = true; + this.visData.isPlaying = false; } else { // caught in following block, not sent to front end throw new Error("incomplete simulator config provided"); @@ -370,18 +409,21 @@ export default class SimulariumController { this.simulator = undefined; console.warn(error.message); this.networkEnabled = false; - this.isPaused = false; + this.visData.isPlaying = false; } } // start the simulation paused and get first frame if (this.simulator) { - return this.start() + return this.initializePrecomputedSimulation() .then(() => { if (this.simulator) { this.simulator.requestSingleFrame(0); } }) + .then(() => { + this.resumeStreaming(); + }) .then(() => ({ status: FILE_STATUS_SUCCESS, })); @@ -539,6 +581,22 @@ export default class SimulariumController { public setCameraType(ortho: boolean): void { this.visGeometry?.setCameraType(ortho); } + + public isStreaming(): boolean { + return this.visData.isStreaming; + } + + public isPlaying(): boolean { + return this.visData.isPlaying; + } + + public currentPlaybackHead(): number { + return this.visData.currentFrameNumber; + } + + public currentStreamingHead(): number { + return this.visData.currentStreamingHead; + } } export { SimulariumController }; diff --git a/src/index.ts b/src/index.ts index 19b33ec7..b59d95c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export type { VisDataMessage, Plot, AgentData, + CacheLog, } from "./simularium"; export type { ISimulariumFile } from "./simularium/ISimulariumFile"; export type { TimeData } from "./viewport"; diff --git a/src/simularium/VisData.ts b/src/simularium/VisData.ts index 38d689b9..a747e1ee 100644 --- a/src/simularium/VisData.ts +++ b/src/simularium/VisData.ts @@ -14,9 +14,16 @@ class VisData { private frameToWaitFor: number; private lockedForFrame: boolean; - private currentFrameNumber: number; + public currentFrameNumber: number; // playback head + public currentStreamingHead: number; + public remoteStreamingHeadPotentiallyOutOfSync: boolean; + public isPlaying: boolean; + public isStreaming: boolean; + public onStreamingChange: (streaming: boolean) => void; + public onCacheLimitReached: () => void; public timeStepSize: number; + public totalSteps: number; public onError: (error: FrontEndError) => void; private static parseOneBinaryFrame(data: ArrayBuffer): CachedFrame { @@ -38,18 +45,35 @@ class VisData { public constructor() { this.currentFrameNumber = -1; + this.currentStreamingHead = -1; + this.remoteStreamingHeadPotentiallyOutOfSync = false; this.frameCache = new VisDataCache(); this.frameToWaitFor = 0; this.lockedForFrame = false; this.timeStepSize = 0; + this.totalSteps = 0; + this.isPlaying = false; + this.isStreaming = false; this.onError = noop; + this.onStreamingChange = noop; + this.onCacheLimitReached = noop; } public setOnError(onError: (error: FrontEndError) => void): void { this.onError = onError; } + public setOnStreamingChange( + onStreamingChange: (streaming: boolean) => void + ): void { + this.onStreamingChange = onStreamingChange; + } + + public setOnCacheLimitReached(onCacheLimitReached: () => void): void { + this.onCacheLimitReached = onCacheLimitReached; + } + public get currentFrameData(): CachedFrame { if (!this.frameCache.hasFrames()) { return nullCachedFrame(); @@ -78,6 +102,10 @@ class VisData { return this.frameCache.containsTime(time); } + public hasLocalCacheForFrame(frameNumber: number): boolean { + return this.frameCache.containsFrameAtFrameNumber(frameNumber); + } + public gotoTime(time: number): void { const frameNumber = this.frameCache.getFrameAtTime(time)?.frameNumber; if (frameNumber !== undefined) { @@ -85,6 +113,12 @@ class VisData { } } + public gotoFrame(frameNumber: number): void { + if (this.hasLocalCacheForFrame(frameNumber)) { + this.currentFrameNumber = frameNumber; + } + } + public atLatestFrame(): boolean { return this.currentFrameNumber >= this.frameCache.getLastFrameNumber(); } @@ -95,6 +129,11 @@ class VisData { } } + public updateStreamingState(isStreaming: boolean): void { + this.isStreaming = isStreaming; + this.onStreamingChange(isStreaming); + } + /** * Data management * */ @@ -146,7 +185,7 @@ class VisData { this.frameExceedsCacheSizeError(parsedMsg.size); return; } - this.addFrameToCache(parsedMsg); + this.validateAndProcessFrame(parsedMsg); } public parseAgentsFromFrameData(msg: VisDataMessage | ArrayBuffer): void { @@ -155,7 +194,7 @@ class VisData { if (frame.frameNumber === 0) { this.clearCache(); // new data has arrived } - this.addFrameToCache(frame); + this.validateAndProcessFrame(frame); return; } this.parseAgentsFromVisDataMessage(msg); @@ -177,7 +216,10 @@ class VisData { this.parseAgentsFromFrameData(msg); } - private addFrameToCache(frame: CachedFrame): void { + /** + * Incoming frame management + */ + private handleOversizedFrame(frame: CachedFrame): void { if ( this.frameCache.cacheSizeLimited && frame.size > this.frameCache.maxSize @@ -185,7 +227,53 @@ class VisData { this.frameExceedsCacheSizeError(frame.size); return; } + } + + private trimAndAddFrame(frame: CachedFrame): void { + this.frameCache.trimCache(this.currentFrameData.size); + this.frameCache.addFrame(frame); + } + + private resetCacheWithFrame(frame: CachedFrame): void { + this.clearCache(); + this.frameCache.addFrame(frame); + } + + private handleCacheOverflow(frame: CachedFrame): boolean { + if (frame.size + this.frameCache.size <= this.frameCache.maxSize) { + return false; + } + const playbackFrame = this.currentFrameData; + const isCacheHeadBehindPlayback = + playbackFrame.frameNumber > this.frameCache.getFirstFrameNumber(); + + if (isCacheHeadBehindPlayback) { + this.trimAndAddFrame(frame); + } else if (this.isPlaying) { + // if currently playing, and cache head is ahead of playback head + // we clear the cache and add the frame + this.resetCacheWithFrame(frame); + } else { + // if paused we run out of space we need to stop streaming + // which is handled by the controller via a callback + this.currentStreamingHead = frame.frameNumber; + this.remoteStreamingHeadPotentiallyOutOfSync = true; + this.onCacheLimitReached(); + } + return true; + } + + private validateAndProcessFrame(frame: CachedFrame): void { + this.handleOversizedFrame(frame); + + if (!this.handleCacheOverflow(frame)) { + this.addFrameToCache(frame); + } + } + + private addFrameToCache(frame: CachedFrame): void { this.frameCache.addFrame(frame); + this.currentStreamingHead = this.frameCache.getLastFrameNumber(); } private frameExceedsCacheSizeError(frameSize: number): void { diff --git a/src/simularium/VisDataCache.ts b/src/simularium/VisDataCache.ts index bcb6107b..827b0431 100644 --- a/src/simularium/VisDataCache.ts +++ b/src/simularium/VisDataCache.ts @@ -1,18 +1,21 @@ +import { DEFAULT_PRE_FETCH_RATIO } from "../constants"; import { compareTimes } from "../util"; -import { CachedFrame, CacheNode } from "./types"; +import { CachedFrame, CacheNode, CacheLog } from "./types"; interface VisDataCacheSettings { maxSize: number; cacheEnabled: boolean; + onUpdate?: (log: CacheLog) => void; } class VisDataCache { - private head: CacheNode | null; + public head: CacheNode | null; private tail: CacheNode | null; public numFrames: number; public size: number; private _maxSize: number; private _cacheEnabled: boolean; + private cacheUpdateCallback: ((log: CacheLog) => void) | null; constructor(settings?: Partial) { /** @@ -27,6 +30,7 @@ class VisDataCache { this.size = 0; this._maxSize = Infinity; this._cacheEnabled = true; + this.cacheUpdateCallback = null; if (settings) { this.changeSettings(settings); @@ -36,14 +40,33 @@ class VisDataCache { public changeSettings(options: { maxSize?: number; cacheEnabled?: boolean; + onUpdate?: (log: CacheLog) => void; }): void { - const { maxSize, cacheEnabled } = options; + const { maxSize, cacheEnabled, onUpdate } = options; if (cacheEnabled !== undefined) { this._cacheEnabled = cacheEnabled; } if (maxSize !== undefined) { this._maxSize = maxSize; } + if (onUpdate !== undefined) { + this.cacheUpdateCallback = onUpdate; + } + } + + public onCacheUpdate(): void { + if (this.cacheUpdateCallback) { + this.cacheUpdateCallback({ + size: this.size, + numFrames: this.numFrames, + maxSize: this._maxSize, + enabled: this._cacheEnabled, + firstFrameNumber: this.getFirstFrameNumber(), + firstFrameTime: this.getFirstFrameTime(), + lastFrameNumber: this.getLastFrameNumber(), + lastFrameTime: this.getLastFrameTime(), + }); + } } public get maxSize(): number { @@ -111,7 +134,10 @@ class VisDataCache { } public getFirstFrameNumber(): number { - return this.head?.data.frameNumber || -1; + if (this.head) { + return this.head.data.frameNumber; + } + return -1; } public getFirstFrameTime(): number { @@ -162,7 +188,6 @@ class VisDataCache { next: null, prev: null, }; - this.head = newNode; this.tail = newNode; this.size = data.size; @@ -185,6 +210,7 @@ class VisDataCache { this.tail = newNode; this.numFrames++; this.size += data.size; + // todo: handle this logic at a higher level if (this.size > this._maxSize) { this.trimCache(); } @@ -197,9 +223,11 @@ class VisDataCache { } if (this.hasFrames() && this._cacheEnabled) { this.addFrameToEndOfCache(data); + this.onCacheUpdate(); return; } this.assignSingleFrameToCache(data); + this.onCacheUpdate(); } // generalized to remove any node, but in theory @@ -225,9 +253,10 @@ class VisDataCache { } this.numFrames--; this.size -= node.data.size; + this.onCacheUpdate(); } - private trimCache(incomingDataSize?: number): void { + public trimCache(incomingDataSize?: number): void { while ( this.hasFrames() && this.size + (incomingDataSize || 0) > this._maxSize && @@ -242,6 +271,7 @@ class VisDataCache { this.tail = null; this.numFrames = 0; this.size = 0; + this.onCacheUpdate(); } } diff --git a/src/simularium/index.ts b/src/simularium/index.ts index 66e7c3c1..15b1128c 100644 --- a/src/simularium/index.ts +++ b/src/simularium/index.ts @@ -9,6 +9,7 @@ export type { SimulariumFileFormat, Plot, AgentData, + CacheLog, } from "./types"; export type { diff --git a/src/simularium/types.ts b/src/simularium/types.ts index 4d3191bf..67a39437 100644 --- a/src/simularium/types.ts +++ b/src/simularium/types.ts @@ -209,3 +209,13 @@ export interface CacheNode { next: CacheNode | null; prev: CacheNode | null; } +export interface CacheLog { + size: number; + numFrames: number; + maxSize: number; + enabled: boolean; + firstFrameNumber: number; + firstFrameTime: number; + lastFrameNumber: number; + lastFrameTime: number; +} diff --git a/src/test/AgentSimController.test.ts b/src/test/AgentSimController.test.ts index 35730401..dfaa9973 100644 --- a/src/test/AgentSimController.test.ts +++ b/src/test/AgentSimController.test.ts @@ -14,8 +14,8 @@ describe("SimulariumController module", () => { remoteSimulator: netConn, }); - controller.start(); - controller.gotoTime(2); + controller.initializePrecomputedSimulation(); + controller.movePlaybackTime(2); // allow time for data streaming to occur setTimeout(() => { diff --git a/src/test/SimulariumController.test.ts b/src/test/SimulariumController.test.ts index 02eee257..b481851b 100644 --- a/src/test/SimulariumController.test.ts +++ b/src/test/SimulariumController.test.ts @@ -14,8 +14,8 @@ describe("SimulariumController module", () => { remoteSimulator: netConn, }); - controller.start(); - controller.gotoTime(2); + controller.initializePrecomputedSimulation(); + controller.movePlaybackTime(2); setTimeout(() => { expect(controller.time()).toEqual(2); done(); diff --git a/src/viewport/index.tsx b/src/viewport/index.tsx index e8c99a97..8047e8a2 100644 --- a/src/viewport/index.tsx +++ b/src/viewport/index.tsx @@ -11,6 +11,7 @@ import { SelectionInterface, SelectionStateInfo, UIDisplayData, + CacheLog, } from "../simularium"; import { AgentData, TrajectoryFileInfoAny } from "../simularium/types"; import { updateTrajectoryFileInfoFormat } from "../simularium/versionHandlers"; @@ -48,6 +49,8 @@ type ViewportProps = { disableCache?: boolean; onFollowObjectChanged?: (agentData: AgentData) => void; // passes agent data about the followed agent to the front end maxCacheSize?: number; + onCacheUpdate?: (log: CacheLog) => void; + onStreamingChange?: (streaming: boolean) => void; } & Partial; const defaultProps = { @@ -130,10 +133,16 @@ class Viewport extends React.Component< this.props.simulariumController.visData.frameCache.changeSettings({ cacheEnabled: !props.disableCache, maxSize: props.maxCacheSize, + onUpdate: props.onCacheUpdate, }); if (props.onError) { this.props.simulariumController.visData.setOnError(props.onError); } + if (props.onStreamingChange) { + this.props.simulariumController.visData.setOnStreamingChange( + props.onStreamingChange + ); + } this.props.simulariumController.visData.clearCache(); this.visGeometry.createMaterials(props.agentColors); this.vdomRef = React.createRef(); @@ -187,6 +196,7 @@ class Viewport extends React.Component< simulariumController.visData.timeStepSize = trajectoryFileInfo.timeStepSize; + simulariumController.visData.totalSteps = trajectoryFileInfo.totalSteps; const bx = trajectoryFileInfo.size.x; const by = trajectoryFileInfo.size.y; @@ -657,7 +667,7 @@ class Viewport extends React.Component< } } - if (!visData.atLatestFrame() && !simulariumController.paused()) { + if (!visData.atLatestFrame() && simulariumController.isPlaying()) { visData.gotoNextFrame(); } this.stats.begin();