UNPKG

molstar

Version:

A comprehensive macromolecular library.

394 lines (393 loc) 16.5 kB
/** * 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 = {}));