molstar
Version:
A comprehensive macromolecular library.
394 lines (393 loc) • 16.5 kB
JavaScript
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { List } from 'immutable';
import { UUID } from '../../mol-util';
import { StatefulPluginComponent } from '../component';
import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
import { Zip } from '../../mol-util/zip/zip';
import { readFromFile } from '../../mol-util/data-source';
import { objectForEach } from '../../mol-util/object';
import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { canvasToBlob } from '../../mol-canvas3d/util';
import { Task } from '../../mol-task';
import { StringLike } from '../../mol-io/common/string-like';
export { PluginStateSnapshotManager };
class PluginStateSnapshotManager extends StatefulPluginComponent {
get current() {
const id = this.state.current;
return this.state.entries.find(e => e.snapshot.id === id);
}
getIndex(e) {
return this.state.entries.indexOf(e);
}
getEntry(id) {
if (!id)
return;
return this.entryMap.get(id);
}
remove(id) {
const e = this.entryMap.get(id);
if (!e)
return;
if (e === null || e === void 0 ? void 0 : e.image)
this.plugin.managers.asset.delete(e.image);
this.entryMap.delete(id);
this.updateState({
current: this.state.current === id ? void 0 : this.state.current,
entries: this.state.entries.delete(this.getIndex(e))
});
this.events.changed.next(void 0);
}
add(e) {
this.entryMap.set(e.snapshot.id, e);
this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
this.events.changed.next(void 0);
}
replace(id, snapshot, params) {
var _a, _b, _c, _d;
const old = this.getEntry(id);
if (!old)
return;
this.defaultSnapshotId = undefined;
if (old === null || old === void 0 ? void 0 : old.image)
this.plugin.managers.asset.delete(old.image);
const idx = this.getIndex(old);
// The id changes here!
const e = PluginStateSnapshotManager.Entry(snapshot, {
key: (_a = params === null || params === void 0 ? void 0 : params.key) !== null && _a !== void 0 ? _a : old.key,
name: (_b = params === null || params === void 0 ? void 0 : params.name) !== null && _b !== void 0 ? _b : old.name,
description: (_c = params === null || params === void 0 ? void 0 : params.description) !== null && _c !== void 0 ? _c : old.description,
descriptionFormat: (_d = params === null || params === void 0 ? void 0 : params.descriptionFormat) !== null && _d !== void 0 ? _d : old.descriptionFormat,
image: params === null || params === void 0 ? void 0 : params.image,
});
this.entryMap.set(snapshot.id, e);
this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
this.events.changed.next(void 0);
}
move(id, dir) {
const len = this.state.entries.size;
if (len < 2)
return;
const e = this.getEntry(id);
if (!e)
return;
const from = this.getIndex(e);
let to = (from + dir) % len;
if (to < 0)
to += len;
const f = this.state.entries.get(to);
const entries = this.state.entries.asMutable();
entries.set(to, e);
entries.set(from, f);
this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
this.events.changed.next(void 0);
}
update(e, options) {
var _a, _b, _c;
const idx = this.getIndex(e);
if (idx < 0)
return;
const entries = this.state.entries.set(idx, {
...e,
key: ((_a = options.key) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
name: ((_b = options.name) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
description: ((_c = options.description) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
descriptionFormat: options.descriptionFormat,
});
this.updateState({ entries });
this.entryMap.set(e.snapshot.id, this.state.entries.get(idx));
this.events.changed.next(void 0);
}
clear() {
if (this.state.entries.size === 0)
return;
this.entryMap.forEach(e => {
if (e === null || e === void 0 ? void 0 : e.image)
this.plugin.managers.asset.delete(e.image);
});
this.entryMap.clear();
this.updateState({ current: void 0, entries: List() });
this.events.changed.next(void 0);
}
applyKey(key) {
const e = this.state.entries.find(e => e.key === key);
if (!e)
return;
this.updateState({ current: e.snapshot.id });
this.events.changed.next(void 0);
this.plugin.state.setSnapshot(e.snapshot);
}
setCurrent(id) {
const e = this.getEntry(id);
if (e) {
this.updateState({ current: id });
this.events.changed.next(void 0);
}
return e && e.snapshot;
}
getNextId(id, dir) {
const len = this.state.entries.size;
if (!id) {
if (len === 0)
return void 0;
const idx = dir === -1 ? len - 1 : 0;
return this.state.entries.get(idx).snapshot.id;
}
const e = this.getEntry(id);
if (!e)
return;
let idx = this.getIndex(e);
if (idx < 0)
return;
idx = (idx + dir) % len;
if (idx < 0)
idx += len;
return this.state.entries.get(idx).snapshot.id;
}
applyNext(dir) {
const next = this.getNextId(this.state.current, dir);
if (next) {
const snapshot = this.setCurrent(next);
if (snapshot)
return this.plugin.state.setSnapshot(snapshot);
}
}
async setStateSnapshot(snapshot) {
if (snapshot.version !== PLUGIN_VERSION) {
// TODO
// console.warn('state snapshot version mismatch');
}
this.clear();
const entries = List().asMutable();
for (const e of snapshot.entries) {
this.entryMap.set(e.snapshot.id, e);
entries.push(e);
}
const current = snapshot.current
? snapshot.current
: snapshot.entries.length > 0
? snapshot.entries[0].snapshot.id
: void 0;
this.updateState({
current,
entries: entries.asImmutable(),
isPlaying: false,
nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
});
this.events.changed.next(void 0);
if (!current)
return;
const entry = this.getEntry(current);
const next = entry && entry.snapshot;
if (!next)
return;
await this.plugin.state.setSnapshot(next);
if (snapshot.playback && snapshot.playback.isPlaying)
this.play(true);
return next;
}
async syncCurrent(options) {
var _a, _b;
const isEmpty = this.state.entries.size === 0;
const canReplace = this.state.entries.size === 1 && this.state.current && (!this.defaultSnapshotId || this.state.current === this.defaultSnapshotId);
if (!isEmpty && !canReplace)
return;
const snapshot = this.plugin.state.getSnapshot(options === null || options === void 0 ? void 0 : options.params);
const image = ((_b = (_a = options === null || options === void 0 ? void 0 : options.params) === null || _a === void 0 ? void 0 : _a.image) !== null && _b !== void 0 ? _b : this.plugin.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(this.plugin, `${snapshot.id}-image.png`) : undefined;
if (isEmpty) {
this.add(PluginStateSnapshotManager.Entry(snapshot, { name: options === null || options === void 0 ? void 0 : options.name, description: options === null || options === void 0 ? void 0 : options.description, descriptionFormat: options === null || options === void 0 ? void 0 : options.descriptionFormat, image }));
}
else if (canReplace) {
// Replace the current state only if there is a single snapshot that has been created automatically
const current = this.getEntry(this.state.current);
if (current === null || current === void 0 ? void 0 : current.image)
this.plugin.managers.asset.delete(current.image);
this.replace(this.state.current, snapshot, { image });
}
this.defaultSnapshotId = snapshot.id;
}
async getStateSnapshot(options) {
await this.syncCurrent(options);
return {
timestamp: +new Date(),
version: PLUGIN_VERSION,
name: options && options.name,
description: options && options.description,
current: this.state.current,
playback: {
isPlaying: !!(options && options.playOnLoad),
nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
},
entries: this.state.entries.valueSeq().toArray()
};
}
async serialize(options) {
const json = JSON.stringify(await this.getStateSnapshot({ params: options === null || options === void 0 ? void 0 : options.params }), null, 2);
if (!(options === null || options === void 0 ? void 0 : options.type) || options.type === 'json' || options.type === 'molj') {
return new Blob([json], { type: 'application/json;charset=utf-8' });
}
else {
const state = new Uint8Array(utf8ByteCount(json));
utf8Write(state, 0, json);
const zipDataObj = {
'state.json': state
};
const assets = [];
// TODO: there can be duplicate entries: check for this?
for (const { asset, file } of this.plugin.managers.asset.assets) {
assets.push([asset.id, asset]);
zipDataObj[`assets/${asset.id}`] = new Uint8Array(await file.arrayBuffer());
}
if (assets.length > 0) {
const index = JSON.stringify(assets, null, 2);
const data = new Uint8Array(utf8ByteCount(index));
utf8Write(data, 0, index);
zipDataObj['assets.json'] = data;
}
const zipFile = await this.plugin.runTask(Zip(zipDataObj));
return new Blob([zipFile], { type: 'application/zip' });
}
}
async open(file) {
try {
const fn = file.name.toLowerCase();
if (fn.endsWith('json') || fn.endsWith('molj')) {
const data = await this.plugin.runTask(readFromFile(file, 'string'));
const snapshot = JSON.parse(StringLike.toString(data));
if (PluginStateSnapshotManager.isStateSnapshot(snapshot)) {
await this.setStateSnapshot(snapshot);
}
else if (PluginStateSnapshotManager.isStateSnapshot(snapshot.data)) {
await this.setStateSnapshot(snapshot.data);
}
else {
await this.plugin.state.setSnapshot(snapshot);
}
}
else {
const data = await this.plugin.runTask(readFromFile(file, 'zip'));
const assetData = Object.create(null);
objectForEach(data, (v, k) => {
if (k === 'state.json' || k === 'assets.json')
return;
const name = k.substring(k.indexOf('/') + 1);
assetData[name] = v;
});
const stateFile = new File([data['state.json']], 'state.json');
const snapshot = await this.plugin.runTask(readFromFile(stateFile, 'json'));
if (data['assets.json']) {
const file = new File([data['assets.json']], 'assets.json');
const json = await this.plugin.runTask(readFromFile(file, 'json'));
for (const [id, asset] of json) {
this.plugin.managers.asset.set(asset, new File([assetData[id]], asset.name));
}
}
await this.setStateSnapshot(snapshot);
}
this.events.opened.next(void 0);
}
catch (e) {
console.error(e);
this.plugin.log.error('Error reading state');
}
}
play(delayFirst = false) {
this.updateState({ isPlaying: true });
if (delayFirst) {
const e = this.getEntry(this.state.current);
if (!e) {
this.next();
return;
}
this.events.changed.next(void 0);
const snapshot = e.snapshot;
const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
this.timeoutHandle = setTimeout(this.next, delay);
}
else {
this.next();
}
}
stop() {
this.updateState({ isPlaying: false });
if (typeof this.timeoutHandle !== 'undefined')
clearTimeout(this.timeoutHandle);
this.timeoutHandle = void 0;
this.events.changed.next(void 0);
}
togglePlay() {
if (this.state.isPlaying) {
this.stop();
this.plugin.managers.animation.stop();
}
else {
this.play();
}
}
dispose() {
super.dispose();
this.entryMap.clear();
}
constructor(plugin) {
super({
current: void 0,
entries: List(),
isPlaying: false,
nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
});
this.plugin = plugin;
this.entryMap = new Map();
this.defaultSnapshotId = undefined;
this.events = {
changed: this.ev(),
opened: this.ev(),
};
this.timeoutHandle = void 0;
this.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);
const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
if (this.state.isPlaying)
this.timeoutHandle = setTimeout(this.next, delay);
};
// TODO make nextSnapshotDelayInMs editable
}
}
PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs = 1500;
(function (PluginStateSnapshotManager) {
function Entry(snapshot, params) {
return { timestamp: +new Date(), snapshot, ...params };
}
PluginStateSnapshotManager.Entry = Entry;
function isStateSnapshot(x) {
const s = x;
return !!s && !!s.timestamp && !!s.entries;
}
PluginStateSnapshotManager.isStateSnapshot = isStateSnapshot;
async function getCanvasImageAsset(ctx, name) {
const task = Task.create('Render Screenshot', async (runtime) => {
if (!ctx.helpers.viewportScreenshot)
return;
const p = await ctx.helpers.viewportScreenshot.getPreview(runtime, 512);
if (!p)
return;
const blob = await canvasToBlob(p.canvas, 'png');
const file = new File([blob], name);
const image = { kind: 'file', id: UUID.create22(), name };
ctx.managers.asset.set(image, file);
return image;
});
return ctx.runTask(task);
}
PluginStateSnapshotManager.getCanvasImageAsset = getCanvasImageAsset;
})(PluginStateSnapshotManager || (PluginStateSnapshotManager = {}));