Skip to content
Snippets Groups Projects
Commit 9d056a85 authored by Alexander Rose's avatar Alexander Rose
Browse files

state snapshot images

parent 3ed17fce
No related branches found
No related tags found
No related merge requests found
...@@ -11,6 +11,7 @@ Note that since we don't clearly distinguish between a public and private interf ...@@ -11,6 +11,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Add `NtC tube` visual. Applicable for structures with NtC annotation - Add `NtC tube` visual. Applicable for structures with NtC annotation
- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs` - [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs`
- Improve boundary calculation performance - Improve boundary calculation performance
- Add option to create & include images in state snapshots
## [v3.29.0] - 2023-01-15 ## [v3.29.0] - 2023-01-15
......
/** /**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
...@@ -16,6 +16,7 @@ import { Zip } from '../../mol-util/zip/zip'; ...@@ -16,6 +16,7 @@ import { Zip } from '../../mol-util/zip/zip';
import { readFromFile } from '../../mol-util/data-source'; import { readFromFile } from '../../mol-util/data-source';
import { objectForEach } from '../../mol-util/object'; import { objectForEach } from '../../mol-util/object';
import { PLUGIN_VERSION } from '../../mol-plugin/version'; import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { canvasToBlob } from '../../mol-canvas3d/util';
export { PluginStateSnapshotManager }; export { PluginStateSnapshotManager };
...@@ -46,6 +47,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -46,6 +47,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
const e = this.entryMap.get(id); const e = this.entryMap.get(id);
if (!e) return; if (!e) return;
if (e?.image) this.plugin.managers.asset.delete(e.image);
this.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,
...@@ -60,15 +62,17 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -60,15 +62,17 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
this.events.changed.next(void 0); this.events.changed.next(void 0);
} }
replace(id: string, snapshot: PluginState.Snapshot) { replace(id: string, snapshot: PluginState.Snapshot, params?: PluginStateSnapshotManager.EntryParams) {
const old = this.getEntry(id); const old = this.getEntry(id);
if (!old) return; if (!old) return;
if (old?.image) this.plugin.managers.asset.delete(old.image);
const idx = this.getIndex(old); const idx = this.getIndex(old);
// The id changes here! // The id changes here!
const e = PluginStateSnapshotManager.Entry(snapshot, { const e = PluginStateSnapshotManager.Entry(snapshot, {
name: old.name, name: params?.name ?? old.name,
description: old.description description: params?.description ?? old.description,
image: params?.image,
}); });
this.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) });
...@@ -96,6 +100,10 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -96,6 +100,10 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
clear() { clear() {
if (this.state.entries.size === 0) return; if (this.state.entries.size === 0) return;
this.entryMap.forEach(e => {
if (e?.image) this.plugin.managers.asset.delete(e.image);
});
this.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(void 0); this.events.changed.next(void 0);
...@@ -162,18 +170,22 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -162,18 +170,22 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
return next; return next;
} }
private syncCurrent(options?: { name?: string, description?: string, params?: PluginState.SnapshotParams }) { private async syncCurrent(options?: { name?: string, description?: string, params?: PluginState.SnapshotParams }) {
const snapshot = this.plugin.state.getSnapshot(options?.params); const snapshot = this.plugin.state.getSnapshot(options?.params);
if (this.state.entries.size === 0 || !this.state.current) { if (this.state.entries.size === 0 || !this.state.current) {
this.add(PluginStateSnapshotManager.Entry(snapshot, { name: options?.name, description: options?.description })); this.add(PluginStateSnapshotManager.Entry(snapshot, { name: options?.name, description: options?.description }));
} else { } else {
this.replace(this.state.current, snapshot); const current = this.getEntry(this.state.current);
if (current?.image) this.plugin.managers.asset.delete(current.image);
const image = (options?.params?.image ?? this.plugin.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(this.plugin, `${snapshot.id}-image.png`) : undefined;
// TODO: this replaces the current snapshot which is not always intended
this.replace(this.state.current, snapshot, { image });
} }
} }
getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.SnapshotParams }): PluginStateSnapshotManager.StateSnapshot { async getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.SnapshotParams }): Promise<PluginStateSnapshotManager.StateSnapshot> {
// TODO: diffing and all that fancy stuff // TODO: diffing and all that fancy stuff
this.syncCurrent(options); await this.syncCurrent(options);
return { return {
timestamp: +new Date(), timestamp: +new Date(),
...@@ -190,7 +202,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -190,7 +202,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
} }
async serialize(options?: { type: 'json' | 'molj' | 'zip' | 'molx', params?: PluginState.SnapshotParams }) { async serialize(options?: { type: 'json' | 'molj' | 'zip' | 'molx', params?: PluginState.SnapshotParams }) {
const json = JSON.stringify(this.getStateSnapshot({ params: options?.params }), null, 2); const json = JSON.stringify(await this.getStateSnapshot({ params: options?.params }), null, 2);
if (!options?.type || options.type === 'json' || options.type === 'molj') { if (!options?.type || options.type === 'json' || options.type === 'molj') {
return new Blob([json], { type: 'application/json;charset=utf-8' }); return new Blob([json], { type: 'application/json;charset=utf-8' });
...@@ -326,14 +338,18 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{ ...@@ -326,14 +338,18 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
} }
namespace PluginStateSnapshotManager { namespace PluginStateSnapshotManager {
export interface Entry { export interface EntryParams {
timestamp: number,
name?: string, name?: string,
description?: string, description?: string,
image?: Asset
}
export interface Entry extends EntryParams {
timestamp: number,
snapshot: PluginState.Snapshot snapshot: PluginState.Snapshot
} }
export function Entry(snapshot: PluginState.Snapshot, params: { name?: string, description?: string }): Entry { export function Entry(snapshot: PluginState.Snapshot, params: EntryParams): Entry {
return { timestamp: +new Date(), snapshot, ...params }; return { timestamp: +new Date(), snapshot, ...params };
} }
...@@ -354,4 +370,17 @@ namespace PluginStateSnapshotManager { ...@@ -354,4 +370,17 @@ namespace PluginStateSnapshotManager {
}, },
entries: Entry[] entries: Entry[]
} }
export async function getCanvasImageAsset(ctx: PluginContext, name: string): Promise<Asset | undefined> {
if (!ctx.helpers.viewportScreenshot) return;
const p = ctx.helpers.viewportScreenshot.getPreview(512);
if (!p) return;
const blob = await canvasToBlob(p.canvas, 'png');
const file = new File([blob], name);
const image: Asset = { kind: 'file', id: UUID.create22(), name };
ctx.managers.asset.set(image, file);
return image;
}
} }
\ No newline at end of file
...@@ -324,6 +324,26 @@ ...@@ -324,6 +324,26 @@
} }
} }
.msp-state-image-row {
@extend .msp-flex-row;
height: $state-image-height;
margin-top: 0px;
> button {
height: $state-image-height;
padding: 0px;
> img {
min-height: $state-image-height;
width: inherit;
transform: translateY(-50%);
top: 50%;
position: relative;
}
}
}
.msp-tree-row { .msp-tree-row {
position: relative; position: relative;
margin-top: 0; margin-top: 0;
......
...@@ -84,3 +84,6 @@ $entity-tag-color: color-lower-contrast($font-color, 20%); ...@@ -84,3 +84,6 @@ $entity-tag-color: color-lower-contrast($font-color, 20%);
// sequence // sequence
$sequence-background: $default-background; $sequence-background: $default-background;
$sequence-number-color: $hover-font-color; $sequence-number-color: $hover-font-color;
// state
$state-image-height: 96px;
\ No newline at end of file
/** /**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/ */
import { OrderedMap } from 'immutable'; import { OrderedMap } from 'immutable';
...@@ -160,6 +161,7 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> { ...@@ -160,6 +161,7 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
}; };
replace = (e: React.MouseEvent<HTMLElement>) => { replace = (e: React.MouseEvent<HTMLElement>) => {
// TODO: add option change name/description
const id = e.currentTarget.getAttribute('data-id'); const id = e.currentTarget.getAttribute('data-id');
if (!id) return; if (!id) return;
PluginCommands.State.Snapshots.Replace(this.plugin, { id }); PluginCommands.State.Snapshots.Replace(this.plugin, { id });
...@@ -167,8 +169,9 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> { ...@@ -167,8 +169,9 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
render() { render() {
const current = this.plugin.managers.snapshot.state.current; const current = this.plugin.managers.snapshot.state.current;
return <ul style={{ listStyle: 'none', marginTop: '10px' }} className='msp-state-list'> const items: JSX.Element[] = [];
{this.plugin.managers.snapshot.state.entries.map(e => <li key={e!.snapshot.id} className='msp-flex-row'> this.plugin.managers.snapshot.state.entries.forEach(e => {
items.push(<li key={e!.snapshot.id} className='msp-flex-row'>
<Button data-id={e!.snapshot.id} onClick={this.apply} className='msp-no-overflow'> <Button data-id={e!.snapshot.id} onClick={this.apply} className='msp-no-overflow'>
<span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0 }}> <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0 }}>
{e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small> {e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>
...@@ -179,8 +182,21 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> { ...@@ -179,8 +182,21 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
<IconButton svg={ArrowDownwardSvg} data-id={e!.snapshot.id} title='Move Down' onClick={this.moveDown} flex='20px' /> <IconButton svg={ArrowDownwardSvg} data-id={e!.snapshot.id} title='Move Down' onClick={this.moveDown} flex='20px' />
<IconButton svg={SwapHorizSvg} data-id={e!.snapshot.id} title='Replace' onClick={this.replace} flex='20px' /> <IconButton svg={SwapHorizSvg} data-id={e!.snapshot.id} title='Replace' onClick={this.replace} flex='20px' />
<IconButton svg={DeleteOutlinedSvg} data-id={e!.snapshot.id} title='Remove' onClick={this.remove} flex='20px' /> <IconButton svg={DeleteOutlinedSvg} data-id={e!.snapshot.id} title='Remove' onClick={this.remove} flex='20px' />
</li>)} </li>);
</ul>; const image = e.image && this.plugin.managers.asset.get(e.image)?.file;
if (image) {
items.push(<li key={`${e!.snapshot.id}-image`} className='msp-state-image-row'>
<Button data-id={e!.snapshot.id} onClick={this.apply}>
<img src={URL.createObjectURL(image)}/>
</Button>
</li>);
}
});
return <>
<ul style={{ listStyle: 'none', marginTop: '10px' }} className='msp-state-list'>
{items}
</ul>
</>;
} }
} }
......
/** /**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
...@@ -151,13 +151,17 @@ export function Snapshots(ctx: PluginContext) { ...@@ -151,13 +151,17 @@ export function Snapshots(ctx: PluginContext) {
ctx.managers.snapshot.remove(id); ctx.managers.snapshot.remove(id);
}); });
PluginCommands.State.Snapshots.Add.subscribe(ctx, ({ name, description, params }) => { PluginCommands.State.Snapshots.Add.subscribe(ctx, async ({ name, description, params }) => {
const entry = PluginStateSnapshotManager.Entry(ctx.state.getSnapshot(params), { name, description }); const snapshot = ctx.state.getSnapshot(params);
const image = (params?.image ?? ctx.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(ctx, `${snapshot.id}-image.png`) : undefined;
const entry = PluginStateSnapshotManager.Entry(snapshot, { name, description, image });
ctx.managers.snapshot.add(entry); ctx.managers.snapshot.add(entry);
}); });
PluginCommands.State.Snapshots.Replace.subscribe(ctx, ({ id, params }) => { PluginCommands.State.Snapshots.Replace.subscribe(ctx, async ({ id, params }) => {
ctx.managers.snapshot.replace(id, ctx.state.getSnapshot(params)); const snapshot = ctx.state.getSnapshot(params);
const image = (params?.image ?? ctx.state.snapshotParams.value.image) ? await PluginStateSnapshotManager.getCanvasImageAsset(ctx, `${snapshot.id}-image.png`) : undefined;
ctx.managers.snapshot.replace(id, ctx.state.getSnapshot(params), { image });
}); });
PluginCommands.State.Snapshots.Move.subscribe(ctx, ({ id, dir }) => { PluginCommands.State.Snapshots.Move.subscribe(ctx, ({ id, dir }) => {
...@@ -170,14 +174,14 @@ export function Snapshots(ctx: PluginContext) { ...@@ -170,14 +174,14 @@ export function Snapshots(ctx: PluginContext) {
return ctx.state.setSnapshot(snapshot); return ctx.state.setSnapshot(snapshot);
}); });
PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, playOnLoad, serverUrl, params }) => { PluginCommands.State.Snapshots.Upload.subscribe(ctx, async ({ name, description, playOnLoad, serverUrl, params }) => {
return fetch(urlCombine(serverUrl, `set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`), { return fetch(urlCombine(serverUrl, `set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`), {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
referrer: 'no-referrer', referrer: 'no-referrer',
headers: { 'Content-Type': 'application/json; charset=utf-8' }, headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify(ctx.managers.snapshot.getStateSnapshot({ name, description, playOnLoad })) body: JSON.stringify(await ctx.managers.snapshot.getStateSnapshot({ name, description, playOnLoad }))
}) as any as Promise<void>; }) as unknown as Promise<void>;
}); });
PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => { PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => {
......
/** /**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
*/ */
...@@ -157,7 +157,8 @@ namespace PluginState { ...@@ -157,7 +157,8 @@ namespace PluginState {
durationInMs: PD.Numeric(250, { min: 100, max: 5000, step: 500 }, { label: 'Duration in ms' }), durationInMs: PD.Numeric(250, { min: 100, max: 5000, step: 500 }, { label: 'Duration in ms' }),
}), }),
instant: PD.Group({ }) instant: PD.Group({ })
}, { options: [['animate', 'Animate'], ['instant', 'Instant']] }) }, { options: [['animate', 'Animate'], ['instant', 'Instant']] }),
image: PD.Boolean(false),
}; };
export type SnapshotParams = Partial<PD.Values<typeof SnapshotParams>> export type SnapshotParams = Partial<PD.Values<typeof SnapshotParams>>
export const DefaultSnapshotParams = PD.getDefaultValues(SnapshotParams); export const DefaultSnapshotParams = PD.getDefaultValues(SnapshotParams);
......
/** /**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
...@@ -86,6 +86,18 @@ class AssetManager { ...@@ -86,6 +86,18 @@ class AssetManager {
this._assets.set(asset.id, { asset, file, refCount: 0 }); this._assets.set(asset.id, { asset, file, refCount: 0 });
} }
get(asset: Asset) {
return this._assets.get(asset.id);
}
delete(asset: Asset) {
return this._assets.delete(asset.id);
}
has(asset: Asset) {
return this._assets.has(asset.id);
}
resolve<T extends DataType>(asset: Asset, type: T, store = true): Task<Asset.Wrapper<T>> { resolve<T extends DataType>(asset: Asset, type: T, store = true): Task<Asset.Wrapper<T>> {
if (Asset.isUrl(asset)) { if (Asset.isUrl(asset)) {
return Task.create(`Download ${asset.title || asset.url}`, async ctx => { return Task.create(`Download ${asset.title || asset.url}`, async ctx => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment