Skip to content
Snippets Groups Projects
Commit c9a07888 authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: state snapshot playback

parent d15b902e
No related branches found
No related tags found
No related merge requests found
...@@ -158,9 +158,7 @@ export function Snapshots(ctx: PluginContext) { ...@@ -158,9 +158,7 @@ export function Snapshots(ctx: PluginContext) {
PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => { PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => {
const json = await ctx.runTask(ctx.fetch({ url, type: 'json' })); // fetch(url, { referrer: 'no-referrer' }); const json = await ctx.runTask(ctx.fetch({ url, type: 'json' })); // fetch(url, { referrer: 'no-referrer' });
const snapshot = ctx.state.snapshots.setRemoteSnapshot(json.data); await ctx.state.snapshots.setRemoteSnapshot(json.data);
if (!snapshot) return;
return ctx.state.setSnapshot(snapshot);
}); });
PluginCommands.State.Snapshots.DownloadToFile.subscribe(ctx, ({ name }) => { PluginCommands.State.Snapshots.DownloadToFile.subscribe(ctx, ({ name }) => {
......
...@@ -165,3 +165,23 @@ ...@@ -165,3 +165,23 @@
.msp-icon-switch:before { .msp-icon-switch:before {
content: "\e896"; content: "\e896";
} }
.msp-icon-play:before {
content: "\e897";
}
.msp-icon-stop:before {
content: "\e898";
}
.msp-icon-pause:before {
content: "\e899";
}
.msp-icon-left-open:before {
content: "\e87c";
}
.msp-icon-right-open:before {
content: "\e87d";
}
\ No newline at end of file
...@@ -25,7 +25,7 @@ class PluginState { ...@@ -25,7 +25,7 @@ class PluginState {
readonly behaviorState: State; readonly behaviorState: State;
readonly animation: PluginAnimationManager; readonly animation: PluginAnimationManager;
readonly cameraSnapshots = new CameraSnapshotManager(); readonly cameraSnapshots = new CameraSnapshotManager();
readonly snapshots = new PluginStateSnapshotManager(); readonly snapshots: PluginStateSnapshotManager;
readonly behavior = { readonly behavior = {
kind: this.ev.behavior<PluginState.Kind>('data'), kind: this.ev.behavior<PluginState.Kind>('data'),
...@@ -89,6 +89,7 @@ class PluginState { ...@@ -89,6 +89,7 @@ class PluginState {
} }
constructor(private plugin: import('./context').PluginContext) { constructor(private plugin: import('./context').PluginContext) {
this.snapshots = new PluginStateSnapshotManager(plugin);
this.dataState = State.create(new SO.Root({ }), { globalContext: plugin }); this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } }); this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
......
...@@ -8,14 +8,20 @@ import { List } from 'immutable'; ...@@ -8,14 +8,20 @@ import { List } from 'immutable';
import { UUID } from 'mol-util'; import { UUID } from 'mol-util';
import { PluginState } from '../state'; import { PluginState } from '../state';
import { PluginComponent } from 'mol-plugin/component'; import { PluginComponent } from 'mol-plugin/component';
import { PluginContext } from 'mol-plugin/context';
export { PluginStateSnapshotManager } export { PluginStateSnapshotManager }
class PluginStateSnapshotManager extends PluginComponent<{ class PluginStateSnapshotManager extends PluginComponent<{
current?: UUID | undefined, current?: UUID | undefined,
entries: List<PluginStateSnapshotManager.Entry>, entries: List<PluginStateSnapshotManager.Entry>,
entryMap: Map<string, PluginStateSnapshotManager.Entry> isPlaying: boolean,
nextSnapshotDelayInMs: number
}> { }> {
static DefaultNextSnapshotDelayInMs = 1500;
private entryMap = new Map<string, PluginStateSnapshotManager.Entry>();
readonly events = { readonly events = {
changed: this.ev() changed: this.ev()
}; };
...@@ -28,14 +34,14 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -28,14 +34,14 @@ class PluginStateSnapshotManager extends PluginComponent<{
getEntry(id: string | undefined) { getEntry(id: string | undefined) {
if (!id) return; if (!id) return;
return this.state.entryMap.get(id); return this.entryMap.get(id);
} }
remove(id: string) { remove(id: string) {
const e = this.state.entryMap.get(id); const e = this.entryMap.get(id);
if (!e) return; if (!e) return;
this.state.entryMap.delete(id); this.entryMap.delete(id);
this.updateState({ this.updateState({
current: this.state.current === id ? void 0 : this.state.current, current: this.state.current === id ? void 0 : this.state.current,
entries: this.state.entries.delete(this.getIndex(e)) entries: this.state.entries.delete(this.getIndex(e))
...@@ -44,7 +50,7 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -44,7 +50,7 @@ class PluginStateSnapshotManager extends PluginComponent<{
} }
add(e: PluginStateSnapshotManager.Entry) { add(e: PluginStateSnapshotManager.Entry) {
this.state.entryMap.set(e.snapshot.id, e); this.entryMap.set(e.snapshot.id, e);
this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) }); this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
this.events.changed.next(); this.events.changed.next();
} }
...@@ -56,7 +62,7 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -56,7 +62,7 @@ class PluginStateSnapshotManager extends PluginComponent<{
const idx = this.getIndex(old); const idx = this.getIndex(old);
// The id changes here! // The id changes here!
const e = PluginStateSnapshotManager.Entry(snapshot, old.name, old.description); const e = PluginStateSnapshotManager.Entry(snapshot, old.name, old.description);
this.state.entryMap.set(snapshot.id, e); this.entryMap.set(snapshot.id, e);
this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) }); this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
this.events.changed.next(); this.events.changed.next();
} }
...@@ -82,7 +88,7 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -82,7 +88,7 @@ class PluginStateSnapshotManager extends PluginComponent<{
clear() { clear() {
if (this.state.entries.size === 0) return; if (this.state.entries.size === 0) return;
this.state.entryMap.clear(); this.entryMap.clear();
this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() }); this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
this.events.changed.next(); this.events.changed.next();
} }
...@@ -115,11 +121,11 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -115,11 +121,11 @@ class PluginStateSnapshotManager extends PluginComponent<{
return this.state.entries.get(idx).snapshot.id; return this.state.entries.get(idx).snapshot.id;
} }
setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): PluginState.Snapshot | undefined { async setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): Promise<PluginState.Snapshot | undefined> {
this.clear(); this.clear();
const entries = List<PluginStateSnapshotManager.Entry>().asMutable() const entries = List<PluginStateSnapshotManager.Entry>().asMutable()
for (const e of snapshot.entries) { for (const e of snapshot.entries) {
this.state.entryMap.set(e.snapshot.id, e); this.entryMap.set(e.snapshot.id, e);
entries.push(e); entries.push(e);
} }
const current = snapshot.current const current = snapshot.current
...@@ -127,11 +133,20 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -127,11 +133,20 @@ class PluginStateSnapshotManager extends PluginComponent<{
: snapshot.entries.length > 0 : snapshot.entries.length > 0
? snapshot.entries[0].snapshot.id ? snapshot.entries[0].snapshot.id
: void 0; : void 0;
this.updateState({ current, entries: entries.asImmutable() }); this.updateState({
current,
entries: entries.asImmutable(),
isPlaying: false,
nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
});
this.events.changed.next(); this.events.changed.next();
if (!current) return; if (!current) return;
const ret = this.getEntry(current); const entry = this.getEntry(current);
return ret && ret.snapshot; const next = entry && entry.snapshot;
if (!next) return;
await this.plugin.state.setSnapshot(next);
if (snapshot.playback && snapshot.playback.isPlaying) this.play();
return next;
} }
getRemoteSnapshot(options?: { name?: string, description?: string }): PluginStateSnapshotManager.RemoteSnapshot { getRemoteSnapshot(options?: { name?: string, description?: string }): PluginStateSnapshotManager.RemoteSnapshot {
...@@ -141,12 +156,52 @@ class PluginStateSnapshotManager extends PluginComponent<{ ...@@ -141,12 +156,52 @@ class PluginStateSnapshotManager extends PluginComponent<{
name: options && options.name, name: options && options.name,
description: options && options.description, description: options && options.description,
current: this.state.current, current: this.state.current,
playback: {
isPlaying: this.state.isPlaying,
nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
},
entries: this.state.entries.valueSeq().toArray() entries: this.state.entries.valueSeq().toArray()
}; };
} }
constructor() { private timeoutHandle: any = void 0;
super({ current: void 0, entries: List(), entryMap: new Map() }); private next = async () => {
this.timeoutHandle = void 0;
const next = this.getNextId(this.state.current, 1);
if (!next || next === this.state.current) {
this.stop();
return;
}
const snapshot = this.setCurrent(next)!;
await this.plugin.state.setSnapshot(snapshot);
this.timeoutHandle = setTimeout(this.next, this.state.nextSnapshotDelayInMs);
};
play() {
this.updateState({ isPlaying: true });
this.next();
}
stop() {
this.updateState({ isPlaying: false });
if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
this.timeoutHandle = void 0;
this.events.changed.next();
}
togglePlay() {
if (this.state.isPlaying) this.stop();
else this.play();
}
constructor(private plugin: PluginContext) {
super({
current: void 0,
entries: List(),
isPlaying: false,
nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
});
// TODO make nextSnapshotDelayInMs editable
} }
} }
...@@ -167,6 +222,10 @@ namespace PluginStateSnapshotManager { ...@@ -167,6 +222,10 @@ namespace PluginStateSnapshotManager {
name?: string, name?: string,
description?: string, description?: string,
current: UUID | undefined, current: UUID | undefined,
playback: {
isPlaying: boolean,
nextSnapshotDelayInMs: number,
},
entries: Entry[] entries: Entry[]
} }
} }
\ No newline at end of file
...@@ -121,6 +121,10 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus ...@@ -121,6 +121,10 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
if (id) this.update(id); if (id) this.update(id);
} }
togglePlay = () => {
this.plugin.state.snapshots.togglePlay();
}
render() { render() {
const snapshots = this.plugin.state.snapshots; const snapshots = this.plugin.state.snapshots;
const count = snapshots.state.entries.size; const count = snapshots.state.entries.size;
...@@ -130,14 +134,18 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus ...@@ -130,14 +134,18 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
} }
const current = snapshots.state.current; const current = snapshots.state.current;
const isPlaying = snapshots.state.isPlaying;
// TODO: better handle disabled state
return <div className='msp-state-snapshot-viewport-controls'> return <div className='msp-state-snapshot-viewport-controls'>
<select className='msp-form-control' value={current || 'none'} onChange={this.change} disabled={this.state.isBusy}> <select className='msp-form-control' value={current || 'none'} onChange={this.change} disabled={this.state.isBusy || isPlaying}>
{!current && <option key='none' value='none'></option>} {!current && <option key='none' value='none'></option>}
{snapshots.state.entries.valueSeq().map((e, i) => <option key={e!.snapshot.id} value={e!.snapshot.id}>{`[${i! + 1}/${count}]`} {e!.name || new Date(e!.timestamp).toLocaleString()}</option>)} {snapshots.state.entries.valueSeq().map((e, i) => <option key={e!.snapshot.id} value={e!.snapshot.id}>{`[${i! + 1}/${count}]`} {e!.name || new Date(e!.timestamp).toLocaleString()}</option>)}
</select> </select>
<IconButton icon='model-prev' title='Previous State' onClick={this.prev} disabled={this.state.isBusy} /> <IconButton icon='left-open' title='Previous State' onClick={this.prev} disabled={this.state.isBusy || isPlaying} />
<IconButton icon='model-next' title='Next State' onClick={this.next} disabled={this.state.isBusy} /> <IconButton icon='right-open' title='Next State' onClick={this.next} disabled={this.state.isBusy || isPlaying} />
<IconButton icon={isPlaying ? 'pause' : 'play'} title={isPlaying ? 'Pause' : 'Play'} onClick={this.togglePlay} />
</div>; </div>;
} }
} }
......
...@@ -139,7 +139,7 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> { ...@@ -139,7 +139,7 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
render() { render() {
const current = this.plugin.state.snapshots.state.current; const current = this.plugin.state.snapshots.state.current;
return <ul style={{ listStyle: 'none' }} className='msp-state-list'> return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
{this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.snapshot.id}> {this.plugin.state.snapshots.state.entries.map(e =><li key={e!.snapshot.id}>
<button data-id={e!.snapshot.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.apply}> <button data-id={e!.snapshot.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.apply}>
<span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>{e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>{e!.description}</small> <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>{e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>{e!.description}</small>
</button> </button>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment