diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts new file mode 100644 index 0000000000000000000000000000000000000000..824376263ab6be9e89d271a4927c650021e8cae8 --- /dev/null +++ b/src/apps/viewer/extensions/jolecule.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateTree, StateBuilder, StateAction } from 'mol-state'; +import { StateTransforms } from 'mol-plugin/state/transforms'; +import { createModelTree, complexRepresentation } from 'mol-plugin/state/actions/structure'; +import { PluginContext } from 'mol-plugin/context'; +import { PluginStateObject } from 'mol-plugin/state/objects'; +import { ParamDefinition } from 'mol-util/param-definition'; +import { PluginCommands } from 'mol-plugin/command'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots'; +import { MolScriptBuilder as MS } from 'mol-script/language/builder'; +import { Text } from 'mol-geo/geometry/text/text'; +import { UUID } from 'mol-util'; +import { ColorNames } from 'mol-util/color/tables'; +import { Camera } from 'mol-canvas3d/camera'; +import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation'; + +export const CreateJoleculeState = StateAction.build({ + display: { name: 'Jolecule State' }, + params: { id: ParamDefinition.Text('1mbo') }, + from: PluginStateObject.Root +})(async ({ ref, state, params }, plugin: PluginContext) => { + try { + const id = params.id.trim().toLowerCase(); + const data = await plugin.runTask(plugin.fetch({ url: `https://jolecule.appspot.com/pdb/${id}.views.json`, type: 'json' })) as JoleculeSnapshot[]; + + data.sort((a, b) => a.order - b.order); + + await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref }); + plugin.state.snapshots.clear(); + + const template = createTemplate(plugin, state.tree, id); + const snapshots = data.map((e, idx) => buildSnapshot(plugin, template, { e, idx, len: data.length })); + for (const s of snapshots) { + plugin.state.snapshots.add(s); + } + + PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id }); + } catch (e) { + plugin.log.error(`Jolecule Failed: ${e}`); + } +}); + +interface JoleculeSnapshot { + order: number, + distances: { i_atom1: number, i_atom2: number }[], + labels: { i_atom: number, text: string }[], + camera: { up: Vec3, pos: Vec3, in: Vec3, slab: { z_front: number, z_back: number, zoom: number } }, + selected: number[], + text: string +} + +function createTemplate(plugin: PluginContext, tree: StateTree, id: string) { + const b = new StateBuilder.Root(tree); + const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }}); + const model = createModelTree(data, 'cif'); + const structure = model.apply(StateTransforms.Model.StructureFromModel, {}, { ref: 'structure' }); + complexRepresentation(plugin, structure, { hideWater: true }); + return b.getTree(); +} + +const labelOptions = { + ...ParamDefinition.getDefaultValues(Text.Params), + sizeFactor: 1.5, + offsetX: 1, + offsetY: 1, + offsetZ: 10, + background: true, + backgroundMargin: 0.2, + backgroundColor: ColorNames.snow, + backgroundOpacity: 0.9 +} + +// const distanceLabelOptions = { +// ...ParamDefinition.getDefaultValues(Text.Params), +// sizeFactor: 1, +// offsetX: 0, +// offsetY: 0, +// offsetZ: 10, +// background: true, +// backgroundMargin: 0.2, +// backgroundColor: ColorNames.snow, +// backgroundOpacity: 0.9 +// } + +function buildSnapshot(plugin: PluginContext, template: StateTree, params: { e: JoleculeSnapshot, idx: number, len: number }): PluginStateSnapshotManager.Entry { + const b = new StateBuilder.Root(template); + + let i = 0; + for (const l of params.e.labels) { + b.to('structure') + .apply(StateTransforms.Model.StructureSelection, { query: createQuery([l.i_atom]), label: `Label ${++i}` }) + .apply(StateTransforms.Representation.StructureLabels3D, { + target: { name: 'static-text', params: { value: l.text || '' } }, + options: labelOptions + }); + } + if (params.e.selected && params.e.selected.length > 0) { + b.to('structure') + .apply(StateTransforms.Model.StructureSelection, { query: createQuery(params.e.selected), label: `Selected` }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'ball-and-stick')); + } + // TODO + // for (const l of params.e.distances) { + // b.to('structure') + // .apply(StateTransforms.Model.StructureSelection, { query: createQuery([l.i_atom1, l.i_atom2]), label: `Distance ${++i}` }) + // .apply(StateTransforms.Representation.StructureLabels3D, { + // target: { name: 'static-text', params: { value: l. || '' } }, + // options: labelOptions + // }); + // } + return PluginStateSnapshotManager.Entry({ + id: UUID.create22(), + data: { tree: StateTree.toJSON(b.getTree()) }, + camera: { + current: getCameraSnapshot(params.e.camera), + transitionStyle: 'animate', + transitionDurationInMs: 350 + } + }, { + name: params.e.text + }); +} + +function getCameraSnapshot(e: JoleculeSnapshot['camera']): Camera.Snapshot { + const direction = Vec3.sub(Vec3.zero(), e.pos, e.in); + Vec3.normalize(direction, direction); + const up = Vec3.sub(Vec3.zero(), e.pos, e.up); + Vec3.normalize(up, up); + + const s: Camera.Snapshot = { + mode: 'perspective', + position: Vec3.scaleAndAdd(Vec3.zero(), e.pos, direction, e.slab.zoom), + target: e.pos, + direction, + up, + near: e.slab.zoom + e.slab.z_front, + far: e.slab.zoom + e.slab.z_back, + fogNear: e.slab.zoom + e.slab.z_front, + fogFar: e.slab.zoom + e.slab.z_back, + fov: Math.PI / 4, + zoom: 1 + }; + return s; +} + +function createQuery(atomIndices: number[]) { + if (atomIndices.length === 0) return MS.struct.generator.empty(); + + return MS.struct.generator.atomGroups({ + 'atom-test': atomIndices.length === 1 + ? MS.core.rel.eq([MS.struct.atomProperty.core.sourceIndex(), atomIndices[0]]) + : MS.core.set.has([MS.set.apply(null, atomIndices), MS.struct.atomProperty.core.sourceIndex()]), + 'group-by': 0 + }); +} \ No newline at end of file diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 95ee7373824184c9efa64981504787cf8ab6cb6f..4f2b72751c4f43fc6d178625d01c3e3379a8c5d9 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -8,6 +8,8 @@ import { createPlugin, DefaultPluginSpec } from 'mol-plugin'; import './index.html' import { PluginContext } from 'mol-plugin/context'; import { PluginCommands } from 'mol-plugin/command'; +import { PluginSpec } from 'mol-plugin/spec'; +import { CreateJoleculeState } from './extensions/jolecule'; require('mol-plugin/skin/light.scss') function getParam(name: string, regex: string): string { @@ -18,15 +20,18 @@ function getParam(name: string, regex: string): string { const hideControls = getParam('hide-controls', `[^&]+`) === '1'; function init() { - const plugin = createPlugin(document.getElementById('app')!, { - ...DefaultPluginSpec, + const spec: PluginSpec = { + actions: [...DefaultPluginSpec.actions, PluginSpec.Action(CreateJoleculeState)], + behaviors: [...DefaultPluginSpec.behaviors], + animations: [...DefaultPluginSpec.animations || []], layout: { initial: { isExpanded: true, showControls: !hideControls } } - }); + }; + const plugin = createPlugin(document.getElementById('app')!, spec); trySetSnapshot(plugin); } diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index dd444dd5fe757e88a4ad7db9a7a24bc22cafd407..566d4410ef53caca3b6907f65382cba1334cf48c 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -76,7 +76,7 @@ export function RemoveObject(ctx: PluginContext) { curr = parent; } } else { - remove(state, ref); + return remove(state, ref); } }); } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 73accd0fc786cd73132a44538dadf92e8ac93b1f..5c28e8e992d919b6476e520b542b38ff3f230836 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -22,46 +22,46 @@ export const PluginCommands = { RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(), - ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), + ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), Snapshots: { - Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }), - Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }), - Move: PluginCommand<{ id: string, dir: -1 | 1 }>({ isImmediate: true }), - Remove: PluginCommand<{ id: string }>({ isImmediate: true }), - Apply: PluginCommand<{ id: string }>({ isImmediate: true }), - Clear: PluginCommand<{}>({ isImmediate: true }), + Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>(), + Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>(), + Move: PluginCommand<{ id: string, dir: -1 | 1 }>(), + Remove: PluginCommand<{ id: string }>(), + Apply: PluginCommand<{ id: string }>(), + Clear: PluginCommand<{}>(), - Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }), + Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>(), Fetch: PluginCommand<{ url: string }>(), - DownloadToFile: PluginCommand<{ name?: string }>({ isImmediate: true }), - OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }), + DownloadToFile: PluginCommand<{ name?: string }>(), + OpenFile: PluginCommand<{ file: File }>(), } }, Interactivity: { Structure: { - Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }), - Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }) + Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(), + Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>() } }, Layout: { - Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true }) + Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>() }, Camera: { - Reset: PluginCommand<{}>({ isImmediate: true }), - SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }), + Reset: PluginCommand<{}>(), + SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(), Snapshots: { - Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }), - Remove: PluginCommand<{ id: string }>({ isImmediate: true }), - Apply: PluginCommand<{ id: string }>({ isImmediate: true }), - Clear: PluginCommand<{}>({ isImmediate: true }), + Add: PluginCommand<{ name?: string, description?: string }>(), + Remove: PluginCommand<{ id: string }>(), + Apply: PluginCommand<{ id: string }>(), + Clear: PluginCommand<{}>(), } }, Canvas3D: { - SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>({ isImmediate: true }) + SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>() } } \ No newline at end of file diff --git a/src/mol-plugin/command/base.ts b/src/mol-plugin/command/base.ts index d58e5e1b709d80b1e28a530f9e681d0b28d2af24..70aca58427031bc88899b0e454839e4be830f30b 100644 --- a/src/mol-plugin/command/base.ts +++ b/src/mol-plugin/command/base.ts @@ -5,8 +5,6 @@ */ import { PluginContext } from '../context'; -import { LinkedList } from 'mol-data/generic'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; import { UUID } from 'mol-util'; export { PluginCommand } @@ -14,24 +12,23 @@ export { PluginCommand } interface PluginCommand<T = unknown> { readonly id: UUID, dispatch(ctx: PluginContext, params: T): Promise<void>, - subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription, - params: { isImmediate: boolean } + subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription } /** namespace.id must a globally unique identifier */ -function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> { - return new Impl({ isImmediate: false, ...params }); +function PluginCommand<T>(): PluginCommand<T> { + return new Impl(); } class Impl<T> implements PluginCommand<T> { dispatch(ctx: PluginContext, params: T): Promise<void> { - return ctx.commands.dispatch(this, params) + return ctx.commands.dispatch(this, params); } subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription { return ctx.commands.subscribe(this, action); } id = UUID.create22(); - constructor(public params: PluginCommand<T>['params']) { + constructor() { } } @@ -47,19 +44,8 @@ namespace PluginCommand { export class Manager { private subs = new Map<string, Action<any>[]>(); - private queue = LinkedList<Instance>(); private disposing = false; - private ev = RxEventHelper.create(); - - readonly behaviour = { - locked: this.ev.behavior<boolean>(false) - }; - - lock(locked: boolean = true) { - this.behaviour.locked.next(locked); - } - subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription { let actions = this.subs.get(cmd.id); if (!actions) { @@ -97,37 +83,22 @@ namespace PluginCommand { return; } - const instance: Instance = { cmd, params, resolve, reject }; - - if (cmd.params.isImmediate) { - this.resolve(instance); - } else { - this.queue.addLast({ cmd, params, resolve, reject }); - this.next(); - } + this.resolve({ cmd, params, resolve, reject }); }); } dispose() { this.subs.clear(); - while (this.queue.count > 0) { - this.queue.removeFirst(); - } } private async resolve(instance: Instance) { const actions = this.subs.get(instance.cmd.id); if (!actions) { - try { - instance.resolve(); - } finally { - if (!instance.cmd.params.isImmediate && !this.disposing) this.next(); - } + instance.resolve(); return; } try { - if (!instance.cmd.params.isImmediate) this.executing = true; // TODO: should actions be called "asynchronously" ("setImmediate") instead? for (const a of actions) { await a(instance.params); @@ -135,19 +106,7 @@ namespace PluginCommand { instance.resolve(); } catch (e) { instance.reject(e); - } finally { - if (!instance.cmd.params.isImmediate) { - this.executing = false; - if (!this.disposing) this.next(); - } } } - - private executing = false; - private async next() { - if (this.queue.count === 0 || this.executing) return; - const instance = this.queue.removeFirst()!; - this.resolve(instance); - } } } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 58a870c7682f37ee7f610b123dfdd1d146364785..6b2e8e6e44db466294b3d80b08593d3ff86472b9 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -76,9 +76,7 @@ export class PluginContext { }, labels: { highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] }) - }, - - command: this.commands.behaviour + } }; readonly canvas3d: Canvas3D; diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts index d0884d76602ea1d1d05118b7401102d930f08040..a2318fc63172903a21bd8136ea57cf9a7a478dbf 100644 --- a/src/mol-plugin/state/actions/structure.ts +++ b/src/mol-plugin/state/actions/structure.ts @@ -101,13 +101,13 @@ const DownloadStructure = StateAction.build({ }) }, { isFlat: true }) }, { - options: [ - ['pdbe-updated', 'PDBe Updated'], - ['rcsb', 'RCSB'], - ['bcif-static', 'BinaryCIF (static PDBe Updated)'], - ['url', 'URL'] - ] - }) + options: [ + ['pdbe-updated', 'PDBe Updated'], + ['rcsb', 'RCSB'], + ['bcif-static', 'BinaryCIF (static PDBe Updated)'], + ['url', 'URL'] + ] + }) } })(({ params, state }, ctx: PluginContext) => { const b = state.build(); @@ -143,7 +143,7 @@ const DownloadStructure = StateAction.build({ createStructureTree(ctx, traj, supportProps); } else { for (const download of downloadParams) { - const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true }}); + const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } }); const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); createStructureTree(ctx, traj, supportProps) } @@ -172,18 +172,18 @@ function createSingleTrajectoryModel(sources: StateTransformer.Params<Download>[ .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); } -function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' | 'gro' = 'cif') { +export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' | 'gro' = 'cif') { let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory> switch (format) { case 'cif': - parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true }}) - .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true }}) + parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } }) + .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } }) break case 'pdb': - parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true }}); + parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } }); break case 'gro': - parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true }}); + parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } }); break default: throw new Error('unsupported format') @@ -203,19 +203,30 @@ function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateO return root; } -function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) { - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); +export function complexRepresentation( + ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>, + params?: { hideSequence?: boolean, hideHET?: boolean, hideWater?: boolean, hideCoarse?: boolean; } +) { + if (!params || !params.hideSequence) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); + } + if (!params || !params.hideHET) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); + } + if (!params || !params.hideWater) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); + } + if (!params || !params.hideCoarse) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); + } } export const CreateComplexRepresentation = StateAction.build({ @@ -296,7 +307,7 @@ export const StructureFromSelection = StateAction.build({ export const TestBlob = StateAction.build({ - display: { name: 'Test Blob'}, + display: { name: 'Test Blob' }, from: PluginStateObject.Root })(({ ref, state }, ctx: PluginContext) => { diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 052ec2ba9062abd764c376c9c4db4e8d1c70f87d..57dd2bfa15dfd28f4b3f0d1434e76492e6faa73b 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -264,6 +264,7 @@ const StructureSelection = PluginStateTransform.BuiltIn({ apply({ a, params, cache }) { const compiled = compile<Sel>(params.query); (cache as { compiled: QueryFn<Sel> }).compiled = compiled; + (cache as { source: Structure }).source = a.data; const result = compiled(new QueryContext(a.data)); const s = Sel.unionStructure(result); @@ -274,6 +275,11 @@ const StructureSelection = PluginStateTransform.BuiltIn({ update: ({ a, b, oldParams, newParams, cache }) => { if (oldParams.query !== newParams.query) return StateTransformer.UpdateResult.Recreate; + if ((cache as { source: Structure }).source === a.data) { + return StateTransformer.UpdateResult.Unchanged; + } + (cache as { source: Structure }).source = a.data; + if (updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label)) { return StateTransformer.UpdateResult.Updated; } @@ -298,6 +304,7 @@ const UserStructureSelection = PluginStateTransform.BuiltIn({ const query = transpileMolScript(parsed[0]); const compiled = compile<Sel>(query); (cache as { compiled: QueryFn<Sel> }).compiled = compiled; + (cache as { source: Structure }).source = a.data; const result = compiled(new QueryContext(a.data)); const s = Sel.unionStructure(result); const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) }; @@ -308,6 +315,11 @@ const UserStructureSelection = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Recreate; } + if ((cache as { source: Structure }).source === a.data) { + return StateTransformer.UpdateResult.Unchanged; + } + (cache as { source: Structure }).source = a.data; + updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label); return StateTransformer.UpdateResult.Updated; } diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 11a8462f951616c81ec5f420ac3c5a11d4f5e4e9..6cc2eea162c2ab17fa51773c63738892fa6318a3 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -12,7 +12,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; import { Canvas3DParams } from 'mol-canvas3d/canvas3d'; import { PluginLayoutStateParams } from 'mol-plugin/layout'; -import { ControlGroup } from './controls/common'; +import { ControlGroup, IconButton } from './controls/common'; interface ViewportState { noWebGl: boolean @@ -59,22 +59,16 @@ export class ViewportControls extends PluginUIComponent { } icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) { - return <button - className={`msp-btn msp-btn-link msp-btn-link-toggle-${isOn ? 'on' : 'off'}`} - onClick={onClick} - title={title}> - <span className={`msp-icon msp-icon-${name}`}/> - </button> + return <IconButton icon={name} toggleState={isOn} onClick={onClick} title={title} />; } render() { - // TODO: show some icons dimmed etc.. return <div className={'msp-viewport-controls'}> <div className='msp-viewport-controls-buttons'> {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/> - {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/> {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/> - {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)} + {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br /> + {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)} </div> {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'> diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 4d2febfa85c2a2e1442173bab3b4eada0e1781c9..8e336eaed0c41cd12caadf774c725afe88323004 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ @@ -19,6 +19,7 @@ import { LogEntry } from 'mol-util/log-entry'; import { now, formatTimespan } from 'mol-util/now'; import { ParamDefinition } from 'mol-util/param-definition'; import { StateTreeSpine } from './tree/spine'; +import { AsyncQueue } from 'mol-util/async-queue'; export { State } @@ -122,7 +123,7 @@ class State { } /** - * Reconcialites the existing state tree with the new version. + * Queues up a reconciliation of the existing state tree. * * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task. * @param tree Tree instance or a tree builder instance @@ -131,27 +132,44 @@ class State { updateTree<T extends StateObject>(tree: StateBuilder.To<T>, options?: Partial<State.UpdateOptions>): Task<T> updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void> updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> { + const params: UpdateParams = { tree, options }; return Task.create('Update Tree', async taskCtx => { - this.events.isUpdating.next(true); - let updated = false; - const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, options); + const ok = await this.updateQueue.enqueue(params); + if (!ok) return; + try { - updated = await update(ctx); - if (StateBuilder.isTo(tree)) { - const cell = this.select(tree.ref)[0]; - return cell && cell.obj; - } + const ret = await this._updateTree(taskCtx, params); + return ret; } finally { - this.spine.setSurrent(); + this.updateQueue.handled(params); + } + }, () => { + this.updateQueue.remove(params); + }); + } - if (updated) this.events.changed.next(); - this.events.isUpdating.next(false); + private updateQueue = new AsyncQueue<UpdateParams>(); - for (const ref of ctx.stateChanges) { - this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); - } + private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) { + this.events.isUpdating.next(true); + let updated = false; + const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options); + try { + updated = await update(ctx); + if (StateBuilder.isTo(params.tree)) { + const cell = this.select(params.tree.ref)[0]; + return cell && cell.obj; } - }); + } finally { + this.spine.setSurrent(); + + if (updated) this.events.changed.next(); + this.events.isUpdating.next(false); + + for (const ref of ctx.stateChanges) { + this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); + } + } } private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, options: Partial<State.UpdateOptions> | undefined) { @@ -239,6 +257,8 @@ const StateUpdateDefaultOptions: State.UpdateOptions = { type Ref = StateTransform.Ref +type UpdateParams = { tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions> } + interface UpdateContext { parent: State, editInfo: StateBuilder.EditInfo | undefined diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts index 95b5ef5be19ea24383680e0e6ca84054cf6c70d7..ac3b10a8132ef7d885a482606b2488fb169e46c1 100644 --- a/src/mol-util/array.ts +++ b/src/mol-util/array.ts @@ -56,4 +56,22 @@ export function arrayRms(array: NumberArray) { export function fillSerial<T extends NumberArray> (array: T, n?: number) { for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i return array -} \ No newline at end of file +} + +export function arrayRemoveInPlace<T>(xs: T[], x: T) { + let i = 0, l = xs.length, found = false; + for (; i < l; i++) { + if (xs[i] === x) { + found = true; + break; + } + } + if (!found) return false; + i++; + for (; i < l; i++) { + xs[i] = xs[i - 1]; + } + xs.pop(); + return true; +} +(window as any).arrayRem = arrayRemoveInPlace \ No newline at end of file diff --git a/src/mol-util/async-queue.ts b/src/mol-util/async-queue.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2ef601c3f6ab3cd54d436a1e446b2373626538a --- /dev/null +++ b/src/mol-util/async-queue.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { arrayRemoveInPlace } from './array'; +import { Subject } from 'rxjs'; + +export class AsyncQueue<T> { + private queue: T[] = []; + private signal = new Subject<{ v: T, removed: boolean }>(); + + enqueue(v: T) { + this.queue.push(v); + if (this.queue.length === 1) return true; + return this.waitFor(v); + } + + handled(v: T) { + arrayRemoveInPlace(this.queue, v); + if (this.queue.length > 0) this.signal.next({ v: this.queue[0], removed: false }); + } + + remove(v: T) { + const rem = arrayRemoveInPlace(this.queue, v); + if (rem) + this.signal.next({ v, removed: true }) + return rem; + } + + private waitFor(t: T): Promise<boolean> { + return new Promise(res => { + const sub = this.signal.subscribe(({ v, removed }) => { + if (v === t) { + sub.unsubscribe(); + res(removed); + } + }); + }) + } +} \ No newline at end of file