UNPKG

molstar

Version:

A comprehensive macromolecular library.

447 lines (446 loc) 23.5 kB
/** * Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> * @author David Sehnal <david.sehnal@gmail.com> * @author Aliaksei Chareshneu <chareshneu.tech@gmail.com> */ import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots.js'; import { PluginStateObject } from '../../mol-plugin-state/objects.js'; import { Download, ParseCcp4, ParseCif, ParseDx, ParsePrmtop, ParsePsf, ParseTop } from '../../mol-plugin-state/transforms/data.js'; import { CoordinatesFromDcd, CoordinatesFromLammpstraj, CoordinatesFromNctraj, CoordinatesFromTrr, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TopologyFromPrmtop, TopologyFromPsf, TopologyFromTop, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model.js'; import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation.js'; import { VolumeFromCcp4, VolumeFromDensityServerCif, VolumeFromDx } from '../../mol-plugin-state/transforms/volume.js'; import { PluginCommands } from '../../mol-plugin/commands.js'; import { StateTree } from '../../mol-state/index.js'; import { Task } from '../../mol-task/index.js'; import { MolViewSpec } from './behavior.js'; import { createPluginStateSnapshotCamera, modifyCanvasProps, resetCanvasProps } from './camera.js'; import { MVSAnnotationsProvider } from './components/annotation-prop.js'; import { MVSAnnotationStructureComponent } from './components/annotation-structure-component.js'; import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop.js'; import { CustomLabelRepresentationProvider } from './components/custom-label/representation.js'; import { CustomTooltipsProvider } from './components/custom-tooltips-prop.js'; import { IsMVSModelProvider } from './components/is-mvs-model-prop.js'; import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData, MVSShapeRepresentation3D } from './components/primitives.js'; import { MVSTrajectoryWithCoordinates } from './components/trajectory.js'; import { generateStateTransition } from './helpers/animation.js'; import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state.js'; import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions.js'; import { loadTreeVirtual, UpdateTarget } from './load-generic.js'; import { clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers.js'; import { MVSData } from './mvs-data.js'; import { MVSAnimationSchema } from './tree/animation/animation-tree.js'; import { validateTree } from './tree/generic/tree-validation.js'; import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion.js'; import { MolstarTreeSchema } from './tree/molstar/molstar-tree.js'; import { MVSTreeSchema } from './tree/mvs/mvs-tree.js'; export function loadMVS(plugin, data, options = {}) { const task = Task.create('Load MVS', ctx => _loadMVS(ctx, plugin, data, options)); return plugin.runTask(task); } /** Load a MolViewSpec (MVS) state(s) into the Mol* plugin as plugin state snapshots. */ async function _loadMVS(ctx, plugin, data, options = {}) { plugin.errorContext.clear('mvs'); try { const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec); if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.'); // Stop any currently running audio plugin.managers.markdownExtensions.audio.dispose(); // Reset canvas props to default so that modifyCanvasProps works as expected resetCanvasProps(plugin); // console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`) const multiData = data.kind === 'multiple' ? data : MVSData.stateToStates(data); const entries = []; for (let i = 0; i < multiData.snapshots.length; i++) { const snapshot = multiData.snapshots[i]; const previousSnapshot = i > 0 ? multiData.snapshots[i - 1] : multiData.snapshots[multiData.snapshots.length - 1]; validateTree(MVSTreeSchema, snapshot.root, 'MVS', plugin); if (snapshot.animation) { validateTree(MVSAnimationSchema, snapshot.animation, 'Animation', plugin); } if (options.sanityChecks) mvsSanityCheck(snapshot.root); const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl); validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar', plugin); const entry = molstarTreeToEntry(plugin, molstarTree, snapshot.animation, { ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms }, options); await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length); entries.push({ ...entry, _transientData: { sourceMvsSnapshot: snapshot } }); if (ctx.shouldUpdate) { await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length }); } } if (!options.appendSnapshots) { plugin.managers.snapshot.clear(); } for (const entry of entries) { plugin.managers.snapshot.add(entry); } if (entries.length > 0) { await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id }); } } catch (err) { plugin.log.error(`${err}`); throw err; } finally { if (!options.doNotReportErrors) { for (const error of plugin.errorContext.get('mvs')) { plugin.log.warn(error); PluginCommands.Toast.Show(plugin, { title: 'Error', message: error, timeoutMs: 10000 }); } } plugin.errorContext.clear('mvs'); } } async function assignStateTransition(ctx, plugin, parentEntry, parent, options, snapshotIndex, snapshotCount) { var _a, _b, _c, _d; const transitions = await generateStateTransition(ctx, parent, snapshotIndex, snapshotCount); if (!(transitions === null || transitions === void 0 ? void 0 : transitions.frames.length)) return; const animation = { autoplay: !!((_a = transitions.tree.params) === null || _a === void 0 ? void 0 : _a.autoplay), loop: !!((_b = transitions.tree.params) === null || _b === void 0 ? void 0 : _b.loop), frames: [], }; for (let i = 0; i < transitions.frames.length; i++) { const frame = transitions.frames[i]; const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl); const entry = molstarTreeToEntry(plugin, molstarTree, parent.animation, { ...parent.metadata, previousTransitionDurationMs: transitions.frametimeMs }, options); StateTree.reuseTransformParams(entry.snapshot.data.tree, parentEntry.snapshot.data.tree); animation.frames.push({ durationInMs: frame[1], data: entry.snapshot.data, camera: ((_c = transitions.tree.params) === null || _c === void 0 ? void 0 : _c.include_camera) ? entry.snapshot.camera : undefined, canvas3d: ((_d = transitions.tree.params) === null || _d === void 0 ? void 0 : _d.include_canvas) ? entry.snapshot.canvas3d : undefined, }); if (ctx.shouldUpdate) { await ctx.update({ message: `Loading animation for snapshot ${snapshotIndex + 1}/${snapshotCount}...`, current: i + 1, max: transitions.frames.length }); } } parentEntry.snapshot.transition = animation; } function molstarTreeToEntry(plugin, tree, animation, metadata, options) { var _a, _b, _c, _d; const context = MolstarLoadingContext.create(); const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: (_a = options === null || options === void 0 ? void 0 : options.extensions) !== null && _a !== void 0 ? _a : BuiltinLoadingExtensions }); snapshot.canvas3d = { props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined, }; if (options === null || options === void 0 ? void 0 : options.keepCamera) { // do nothing } else if (options.keepCameraOrientation) { // load camera target, keep orientation snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs, ignoreCameraOrientation: true }); } else { // fully load camera snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs }); } snapshot.durationInMs = metadata.linger_duration_ms + ((_b = metadata.previousTransitionDurationMs) !== null && _b !== void 0 ? _b : 0); if ((_c = tree.custom) === null || _c === void 0 ? void 0 : _c.molstar_on_load_markdown_commands) { snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands; } const entryParams = { key: metadata.key, name: metadata.title, description: metadata.description, descriptionFormat: (_d = metadata.description_format) !== null && _d !== void 0 ? _d : 'markdown', }; const entry = PluginStateSnapshotManager.Entry(snapshot, entryParams); return entry; } export const MolstarLoadingContext = { create() { return { annotationMap: new Map(), camera: { focuses: [] }, }; }, }; /** Loading actions for loading a `MolstarTree`, per node kind. */ const MolstarLoadingActions = { root(updateParent, node, context) { context.nearestReprMap = makeNearestReprMap(node); return updateParent; }, download(updateParent, node) { return UpdateTarget.apply(updateParent, Download, { url: node.params.url, isBinary: node.params.is_binary, }); }, parse(updateParent, node) { const format = node.params.format; switch (format) { case 'cif': return UpdateTarget.apply(updateParent, ParseCif, {}); case 'pdb': case 'pdbqt': case 'gro': case 'xyz': case 'mol': case 'sdf': case 'mol2': case 'xtc': case 'lammpstrj': case 'dcd': case 'nctraj': case 'trr': return updateParent; case 'psf': return UpdateTarget.apply(updateParent, ParsePsf, {}); case 'prmtop': return UpdateTarget.apply(updateParent, ParsePrmtop, {}); case 'top': return UpdateTarget.apply(updateParent, ParseTop, {}); case 'map': return UpdateTarget.apply(updateParent, ParseCcp4, {}); case 'dx': case 'dxbin': return UpdateTarget.apply(updateParent, ParseDx, {}); default: console.error(`Unknown format in "parse" node: "${format}"`); return undefined; } }, coordinates(updateParent, node) { const format = node.params.format; switch (format) { case 'nctraj': return UpdateTarget.apply(updateParent, CoordinatesFromNctraj); case 'dcd': return UpdateTarget.apply(updateParent, CoordinatesFromDcd); case 'trr': return UpdateTarget.apply(updateParent, CoordinatesFromTrr); case 'xtc': return UpdateTarget.apply(updateParent, CoordinatesFromXtc); case 'lammpstrj': return UpdateTarget.apply(updateParent, CoordinatesFromLammpstraj); default: console.error(`Unknown format in "coordinates" node: "${format}"`); return undefined; } }, trajectory(updateParent, node) { var _a, _b; const format = node.params.format; switch (format) { case 'cif': return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, { blockHeader: (_a = node.params.block_header) !== null && _a !== void 0 ? _a : '', // Must set to '' because just undefined would get overwritten by createDefaults blockIndex: (_b = node.params.block_index) !== null && _b !== void 0 ? _b : undefined, }); case 'pdb': case 'pdbqt': return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { variant: format }); case 'gro': return UpdateTarget.apply(updateParent, TrajectoryFromGRO); case 'xyz': return UpdateTarget.apply(updateParent, TrajectoryFromXYZ); case 'mol': return UpdateTarget.apply(updateParent, TrajectoryFromMOL); case 'sdf': return UpdateTarget.apply(updateParent, TrajectoryFromSDF); case 'mol2': return UpdateTarget.apply(updateParent, TrajectoryFromMOL2); case 'lammpstrj': return UpdateTarget.apply(updateParent, TrajectoryFromLammpsTrajData); default: console.error(`Unknown format in "trajectory" node: "${format}"`); return undefined; } }, trajectory_with_coordinates(updateParent, node) { const result = UpdateTarget.apply(updateParent, MVSTrajectoryWithCoordinates, { coordinatesRef: node.params.coordinates_ref, }); return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]); }, topology_with_coordinates(updateParent, node) { let parsed; const format = node.params.format; switch (format) { case 'psf': parsed = UpdateTarget.apply(updateParent, TopologyFromPsf, {}); break; case 'prmtop': parsed = UpdateTarget.apply(updateParent, TopologyFromPrmtop, {}); break; case 'top': parsed = UpdateTarget.apply(updateParent, TopologyFromTop, {}); break; default: console.error(`Unknown format in "topology_with_coordinates" node: "${format}"`); return undefined; } const result = UpdateTarget.apply(parsed, MVSTrajectoryWithCoordinates, { coordinatesRef: node.params.coordinates_ref, }); return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]); }, model(updateParent, node, context) { const annotations = collectAnnotationReferences(node, context); const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, { modelIndex: node.params.model_index, }); UpdateTarget.apply(model, CustomModelProperties, { properties: { [IsMVSModelProvider.descriptor.name]: { isMvs: true }, [MVSAnnotationsProvider.descriptor.name]: { annotations }, }, autoAttach: [ IsMVSModelProvider.descriptor.name, MVSAnnotationsProvider.descriptor.name, ], }); return model; }, structure(updateParent, node, context) { var _a; const props = structureProps(node); const struct = UpdateTarget.apply(updateParent, StructureFromModel, props); const transformed = transformAndInstantiateStructure(struct, node); const annotationTooltips = collectAnnotationTooltips(node, context); const inlineTooltips = collectInlineTooltips(node, context); UpdateTarget.apply(struct, CustomStructureProperties, { properties: { [MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips }, [CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips }, }, autoAttach: [ MVSAnnotationTooltipsProvider.descriptor.name, CustomTooltipsProvider.descriptor.name, ], }); // CustomStructureProperties must be applied even when `annotationTooltips` and `inlineTooltips` are empty, otherwise tooltips would persists across MVS snapshots const inlineLabels = collectInlineLabels(node, context); if (inlineLabels.length > 0) { const nearestReprNode = (_a = context.nearestReprMap) === null || _a === void 0 ? void 0 : _a.get(node); UpdateTarget.apply(struct, StructureRepresentation3D, { type: { name: CustomLabelRepresentationProvider.name, params: { items: inlineLabels }, }, colorTheme: colorThemeForNode(nearestReprNode, context), }); } return transformed; }, tooltip: undefined, // No action needed, already loaded in `structure` tooltip_from_uri: undefined, // No action needed, already loaded in `structure` tooltip_from_source: undefined, // No action needed, already loaded in `structure` component(updateParent, node) { if (isPhantomComponent(node)) { return updateParent; } const selector = node.params.selector; return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, StructureComponent, { type: componentPropsFromSelector(selector), label: prettyNameFromSelector(selector), nullIfEmpty: false, }), node); }, component_from_uri(updateParent, node, context) { if (isPhantomComponent(node)) return undefined; const props = componentFromXProps(node, context); return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props), node); }, component_from_source(updateParent, node, context) { if (isPhantomComponent(node)) return undefined; const props = componentFromXProps(node, context); return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props), node); }, representation(updateParent, node, context) { return UpdateTarget.apply(updateParent, StructureRepresentation3D, { ...representationProps(node), colorTheme: colorThemeForNode(node, context), }); }, volume(updateParent, node) { var _a, _b, _c; let volume; if ((_a = updateParent.transformer) === null || _a === void 0 ? void 0 : _a.definition.to.includes(PluginStateObject.Format.Ccp4)) { volume = UpdateTarget.apply(updateParent, VolumeFromCcp4, {}); } else if ((_b = updateParent.transformer) === null || _b === void 0 ? void 0 : _b.definition.to.includes(PluginStateObject.Format.Dx)) { volume = UpdateTarget.apply(updateParent, VolumeFromDx, {}); } else if ((_c = updateParent.transformer) === null || _c === void 0 ? void 0 : _c.definition.to.includes(PluginStateObject.Format.Cif)) { volume = UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined }); } else { console.error(`Unsupported volume format`); return undefined; } return transformAndInstantiateVolume(volume, node); }, volume_representation(updateParent, node, context) { return UpdateTarget.apply(updateParent, VolumeRepresentation3D, { ...volumeRepresentationProps(node), colorTheme: volumeColorThemeForNode(node, context), }); }, color: undefined, // No action needed, already loaded in `representation` color_from_uri: undefined, // No action needed, already loaded in `representation` color_from_source: undefined, // No action needed, already loaded in `representation` label: undefined, // No action needed, already loaded in `structure` label_from_uri(updateParent, node, context) { const props = labelFromXProps(node, context); return UpdateTarget.apply(updateParent, StructureRepresentation3D, props); }, label_from_source(updateParent, node, context) { const props = labelFromXProps(node, context); return UpdateTarget.apply(updateParent, StructureRepresentation3D, props); }, focus(updateParent, node, context) { context.camera.focuses.push({ target: updateParent.selector, params: node.params }); return updateParent; }, camera(updateParent, node, context) { context.camera.cameraParams = node.params; return updateParent; }, canvas(updateParent, node, context) { context.canvas = node; return updateParent; }, primitives(updateParent, tree, context) { const refs = getPrimitiveStructureRefs(tree); const clip = clippingForNode(tree); const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree }); UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions return applyPrimitiveVisuals(data, refs, clip); }, primitives_from_uri(updateParent, tree, context) { const refs = new Set(tree.params.references); const clip = clippingForNode(tree); const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format }); UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions return applyPrimitiveVisuals(data, refs, clip); }, }; function applyPrimitiveVisuals(data, refs, clip) { const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh', clip }, { state: { isGhost: true } }), refs); UpdateTarget.apply(mesh, MVSShapeRepresentation3D); const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels', clip }, { state: { isGhost: true } }), refs); UpdateTarget.apply(labels, MVSShapeRepresentation3D); const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines', clip }, { state: { isGhost: true } }), refs); UpdateTarget.apply(lines, MVSShapeRepresentation3D); return data; } export const BuiltinLoadingExtensions = [ NonCovalentInteractionsExtension, IsHiddenCustomStateExtension, ];