diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6c1800a2d5be043a764a01a532955eac7cef2a9 --- /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 }, true); + 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 }, true); + } 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/command/base.ts b/src/mol-plugin/command/base.ts index d58e5e1b709d80b1e28a530f9e681d0b28d2af24..b2ea69de7667ceb2ee3b7fc5a8e6c4560f856f27 100644 --- a/src/mol-plugin/command/base.ts +++ b/src/mol-plugin/command/base.ts @@ -13,7 +13,7 @@ export { PluginCommand } interface PluginCommand<T = unknown> { readonly id: UUID, - dispatch(ctx: PluginContext, params: T): Promise<void>, + dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void>, subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription, params: { isImmediate: boolean } } @@ -24,8 +24,8 @@ function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginC } class Impl<T> implements PluginCommand<T> { - dispatch(ctx: PluginContext, params: T): Promise<void> { - return ctx.commands.dispatch(this, params) + dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void> { + return ctx.commands.dispatch(this, params, isChild); } subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription { return ctx.commands.subscribe(this, action); @@ -43,7 +43,7 @@ namespace PluginCommand { } export type Action<T> = (params: T) => unknown | Promise<unknown> - type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void } + type Instance = { cmd: PluginCommand<any>, params: any, isChild: boolean, resolve: () => void, reject: (e: any) => void } export class Manager { private subs = new Map<string, Action<any>[]>(); @@ -84,7 +84,7 @@ namespace PluginCommand { /** Resolves after all actions have completed */ - dispatch<T>(cmd: PluginCommand<T>, params: T) { + dispatch<T>(cmd: PluginCommand<T>, params: T, isChild = false) { return new Promise<void>((resolve, reject) => { if (this.disposing) { reject('disposed'); @@ -97,12 +97,12 @@ namespace PluginCommand { return; } - const instance: Instance = { cmd, params, resolve, reject }; + const instance: Instance = { cmd, params, resolve, reject, isChild }; - if (cmd.params.isImmediate) { + if (cmd.params.isImmediate || isChild) { this.resolve(instance); } else { - this.queue.addLast({ cmd, params, resolve, reject }); + this.queue.addLast(instance); this.next(); } }); @@ -127,7 +127,7 @@ namespace PluginCommand { } try { - if (!instance.cmd.params.isImmediate) this.executing = true; + if (!instance.cmd.params.isImmediate && !instance.isChild) this.executing = true; // TODO: should actions be called "asynchronously" ("setImmediate") instead? for (const a of actions) { await a(instance.params); @@ -136,7 +136,7 @@ namespace PluginCommand { } catch (e) { instance.reject(e); } finally { - if (!instance.cmd.params.isImmediate) { + if (!instance.cmd.params.isImmediate && !instance.isChild) { this.executing = false; if (!this.disposing) this.next(); } 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/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 11a8462f951616c81ec5f420ac3c5a11d4f5e4e9..2e3336bd65cf3131392f3e4ed8a4b69ff7f562df 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -72,9 +72,9 @@ export class ViewportControls extends PluginUIComponent { 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('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/> </div> {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'>