Viewer improvements (#1725)

* separate exxtensions file

* subscribe method on the Viewer object

* export lib

* Add viewer.structureInteraction

* MVS tryGetPrimitivesFromLoci util

* param select indicator

* tweaks

* mapped control icon

* docs and tweaks

* viewer color themes

* tweak

* typo

* pr feedback
This commit is contained in:
David Sehnal
2025-12-21 11:35:49 +01:00
committed by GitHub
parent 3133dc1543
commit c21ba08fc7
18 changed files with 640 additions and 161 deletions

View File

@@ -15,6 +15,17 @@ Note that since we don't clearly distinguish between a public and private interf
- Fix program not compiled for sync picking
- Fix missing `gl.flush` for async picking (needed for Safari)
- Add Residue Charge color scheme (#1722)
- Add dropdown indicator for mapped parameter definitions and adjust "more options" icon
- MolViewSpec extension
- Add `tryGetPrimitivesFromLoci` that makes it easier to access primitive element data from hover/click interactions
- Viewer app
- Move viewer extensions, options, and presets to a separate file
- Add `molstar.lib` export providing access to a wide range of functionality previously not available from the compiled bundle
- Add `Viewer.subscribe` method that keeps track of subscribed plugin events and disposes them together with the parent viewer
- Add `Viewer.structureInteractivity` that makes it easy to highlight/select elements on the loaded structure
- Add `viewportBackgroundColor` and `viewportFocusBehavior` options
- Add `mvs.html` example to showcase the new functionality combined with MolViewSpec
- Add dark and blue color theme support (import `theme/dark.css` or `theme/blue.css` instead of the default `molstar.css`)
- Fix `flipSided` for meshes
## [v5.4.2] - 2025-12-07

View File

@@ -15,10 +15,24 @@ There are 4 basic ways of instantiating the Mol* plugin.
## ``Viewer`` wrapper
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require much custom behavior and are mostly about just displaying a structure.
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods and options.
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require custom behavior and are mostly about just displaying a structure.
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods
- See [options.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) for available plugin options
- See [embedded.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/embedded.html) and [mvs.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/mvs.html) for example usage
- Importing `molstar.js` will expose `molstar.lib` namespace that allow accessing various functionality without a bundler such as WebPack or esbuild. See the `mvs` example above for basic usage.
- Alternative color themes can be used by importing `theme/dark.css` (or `light/blue`) instead of `molstar.css`
Example usage without using WebPack:
### molstar.js and molstar.css sources
- Download `molstar` NPM package and use the files from `build/viewer` diractory
- Use `jsdelivr` CDN
- `<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.js" />`
- `<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.css" />`
- `@latest` can be replaced by a specific Mol* version, e.g., `@5.4.2`
- Clone & build the GitHub repository
- This option allows for quite straightforward extension customization, e.g., not including movie export, which reduces the bundle size by ~0.5MB
### Example
```HTML
<style>
@@ -35,7 +49,7 @@ Example usage without using WebPack:
- the folder build/viewer after cloning and building the molstar package
- from the build/viewer folder in the Mol* NPM package
-->
<link rel="stylesheet" type="text/css" href="molstar.css" />
<link rel="stylesheet" type="text/css" href="./molstar.css" />
<script type="text/javascript" src="./molstar.js"></script>
<div id="app"></div>
@@ -62,13 +76,15 @@ Example usage without using WebPack:
</script>
```
When using WebPack (or possibly other build tool) with the Mol* NPM package installed, the viewer class can be imported using
### Using WebPack/esbuild/...
When using WebPack (or other bundler) with the Mol* NPM package installed, the viewer class can be imported using
```ts
import { Viewer } from 'molstar/build/viewer/molstar'
import { Viewer } from 'molstar/lib/apps/viewer/app'
function initViewer(target: string | HTMLElement) {
return new Viewer(target, { /* options */})
return Viewer.create(target, { /* options */}) // returns a Promise
}
```
@@ -139,6 +155,8 @@ export function MolStarWrapper() {
// In debug mode of react's strict mode, this code will
// be called twice in a row, which might result in unexpected behavior.
useEffect(() => {
// By default, react will call each useEffect twice if using Strict mode in
// debug build, it is recommended to disable strict mode for this reason if possible
async function init() {
window.molstar = await createPluginUI({
target: parent.current as HTMLDivElement,

View File

@@ -13,7 +13,7 @@ import * as os from 'os';
const Apps = [
// Apps
{ kind: 'app', name: 'viewer' },
{ kind: 'app', name: 'viewer', themes: ['light', 'dark', 'blue'] },
{ kind: 'app', name: 'docking-viewer' },
{ kind: 'app', name: 'mesoscale-explorer' },
{ kind: 'app', name: 'mvs-stories', globalName: 'mvsStories', filename: 'mvs-stories.js' },
@@ -132,7 +132,6 @@ function getPaths(app) {
async function createBundle(app) {
const { name, kind } = app;
const { prefix, entry, outfile } = getPaths(app);
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
const ctx = await esbuild.context({
entryPoints: [entry],
@@ -173,6 +172,41 @@ async function createBundle(app) {
if (!isProduction) await ctx.watch();
}
async function createTheme(appName, themeName) {
// const { prefix, entry, outfile } = getPaths(app);
const ctx = await esbuild.context({
entryPoints: [resolveEntryPath(`./src/apps/${appName}/theme/${themeName}.ts`)],
tsconfig: './tsconfig.json',
bundle: true,
minify: isProduction,
minifyIdentifiers: false,
sourcemap: false,
outfile: `./build/${appName}/theme/${themeName}.js`,
plugins: [
// fileLoaderPlugin({ out: prefix }),
sassPlugin({
type: 'css',
silenceDeprecations: ['import'],
logger: {
warn: (msg) => console.warn(msg),
debug: () => { },
}
}),
],
color: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
},
});
await ctx.rebuild();
if (!isProduction) await ctx.watch();
}
function findBrowserTests(names) {
const dir = path.resolve('./src', 'tests', 'browser');
let files = fs.readdirSync(dir).filter(file => file.endsWith('.ts')).map(file => file.replace('.ts', ''));
@@ -230,6 +264,7 @@ const args = argParser.parse_args();
const isProduction = !!args.prd;
const includeSourceMap = !args.no_src_map;
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
const VERSION = isProduction ? JSON.parse(fs.readFileSync('./package.json', 'utf8')).version : '(dev build)';
const TIMESTAMP = Date.now();
@@ -261,7 +296,14 @@ async function main() {
const promises = [];
console.log(isProduction ? 'Building apps...' : 'Initial build...');
for (const app of apps) promises.push(createBundle(app));
for (const app of apps) {
promises.push(createBundle(app));
if (app.themes) {
for (const theme of app.themes) {
promises.push(createTheme(app.name, theme));
}
}
}
for (const example of examples) promises.push(createBundle(example));
for (const browserTest of browserTests) promises.push(createBundle(browserTest));

View File

@@ -7,34 +7,20 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { ModelExport } from '../../extensions/model-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { ZenodoImport } from '../../extensions/zenodo';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { StringLike } from '../../mol-io/common/string-like';
import { Structure, StructureElement, StructureSelection } from '../../mol-model/structure';
import { Volume } from '../../mol-model/volume';
import { OpenFiles } from '../../mol-plugin-state/actions/file';
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { PluginComponent } from '../../mol-plugin-state/component';
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
@@ -42,98 +28,37 @@ import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { createPluginUI } from '../../mol-plugin-ui';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginBehaviors } from '../../mol-plugin/behavior';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
import { MolScriptBuilder } from '../../mol-script/language/builder';
import { Expression } from '../../mol-script/language/expression';
import { Script } from '../../mol-script/script';
import { StateObjectSelector } from '../../mol-state';
import { Task } from '../../mol-task';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { OpenFiles } from '../../mol-plugin-state/actions/file';
import { StringLike } from '../../mol-io/common/string-like';
import { ExtensionMap } from './extensions';
import { DefaultViewerOptions, ViewerOptions } from './options';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
export const ExtensionMap = {
'backgrounds': PluginSpec.Behavior(Backgrounds),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
'g3d': PluginSpec.Behavior(G3DFormat),
'model-export': PluginSpec.Behavior(ModelExport),
'mp4-export': PluginSpec.Behavior(Mp4Export),
'geo-export': PluginSpec.Behavior(GeometryExport),
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
'mvs': PluginSpec.Behavior(MolViewSpec),
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
};
const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
layoutShowSequence: true,
layoutShowLog: true,
layoutShowLeftPanel: true,
collapseLeftPanel: false,
collapseRightPanel: false,
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
pixelScale: PluginConfig.General.PixelScale.defaultValue,
pickScale: PluginConfig.General.PickScale.defaultValue,
transparency: PluginConfig.General.Transparency.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
illumination: false,
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
config: [] as [PluginConfigItem, any][],
};
type ViewerOptions = typeof DefaultViewerOptions;
import '../../mol-util/polyfill';
import { ViewerAutoPreset } from './presets';
import { decodeColor } from '../../mol-util/color/utils';
export class Viewer {
constructor(public plugin: PluginUIContext) {
private _events = new PluginComponent();
public readonly plugin: PluginUIContext;
constructor(plugin: PluginUIContext) {
this.plugin = plugin;
}
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
@@ -148,11 +73,19 @@ export class Viewer {
const defaultSpec = DefaultPluginUISpec();
const disabledExtension = new Set(o.disabledExtensions ?? []);
let baseBehaviors = defaultSpec.behaviors;
if (o.viewportFocusBehavior === 'disabled') {
baseBehaviors = baseBehaviors.filter(b =>
b.transformer !== PluginBehaviors.Camera.FocusLoci
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
);
}
const spec: PluginUISpec = {
actions: defaultSpec.actions,
behaviors: [
...defaultSpec.behaviors,
...baseBehaviors,
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
],
animations: [...defaultSpec.animations || []],
@@ -228,10 +161,23 @@ export class Viewer {
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
}
});
plugin.canvas3d?.setProps({ illumination: { enabled: o.illumination } });
if (o.viewportBackgroundColor) {
const backgroundColor = decodeColor(o.viewportBackgroundColor);
if (typeof backgroundColor === 'number') {
plugin.canvas3d?.setProps({ renderer: { backgroundColor } });
}
}
return new Viewer(plugin);
}
/**
* Allows subscribing to rxjs observables in the context of the viewer.
* All subscriptions will be disposed of when the viewer is destroyed.
*/
subscribe = this._events.subscribe.bind(this._events);
setRemoteSnapshot(id: string) {
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
@@ -567,7 +513,51 @@ export class Viewer {
this.plugin.layout.events.updated.next(void 0);
}
/**
* Triggers structure element selection or highlighting based on the provided
* MolScript expression or StructureElement schema.
*
* If neither `expression` nor `elements` are provided, all selections/highlights
* will be cleared based on the specified `action`.
*/
structureInteractivity({ expression, elements, action, applyGranularity = false, filterStructure }: {
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
elements?: StructureElement.Schema,
action: 'highlight' | 'select',
applyGranularity?: boolean,
filterStructure?: (structure: Structure) => boolean
}) {
const plugin = this.plugin;
if (!expression && !elements) {
if (action === 'select') {
plugin.managers.interactivity.lociSelects.deselectAll();
} else if (action === 'highlight') {
plugin.managers.interactivity.lociHighlights.clearHighlights();
}
return;
}
const structures = this.plugin.state.data.selectQ(Q => Q.rootsOfType(PluginStateObject.Molecule.Structure));
for (const s of structures) {
if (!s.obj?.data) continue;
if (filterStructure && !filterStructure(s.obj.data)) continue;
const loci = expression
? StructureSelection.toLociWithSourceUnits(Script.getStructureSelection(expression, s.obj.data))
: StructureElement.Schema.toLoci(s.obj.data, elements!);
if (action === 'select') {
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
} else if (action === 'highlight') {
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
}
}
}
dispose() {
this._events.dispose();
this.plugin.dispose();
}
}
@@ -594,44 +584,4 @@ export interface LoadTrajectoryParams {
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInCoordinatesFormat },
coordinatesLabel?: string,
preset?: keyof PresetTrajectoryHierarchy
}
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-viewer-auto',
display: {
name: 'Automatic (w/ Annotation)', group: 'Annotation',
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
},
isApplicable(a) {
return (
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
);
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
} else {
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
}
}
});
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
mvs: { MVSData, loadMVS, loadMVSData },
modelArchive: {
qualityAssessment: {
config: MAQualityAssessmentConfig
}
}
};
}

View File

@@ -0,0 +1,69 @@
/**
* 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>
* @author Adam Midlik <midlik@gmail.com>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, MAQualityAssessmentConfig } from '../../extensions/model-archive/quality-assessment/behavior';
import { ModelExport } from '../../extensions/model-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { loadMVS } from '../../extensions/mvs';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVSData } from '../../extensions/mvs/components/formats';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { SbNcbrPartialCharges, SbNcbrTunnels } from '../../extensions/sb-ncbr';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { ZenodoImport } from '../../extensions/zenodo';
import { PluginSpec } from '../../mol-plugin/spec';
import { MVSData } from '../../extensions/mvs/mvs-data';
import * as MVSUtil from '../../extensions/mvs/util';
export const ExtensionMap = {
// Mol* built-in extensions
'mvs': PluginSpec.Behavior(MolViewSpec),
'backgrounds': PluginSpec.Behavior(Backgrounds),
'model-export': PluginSpec.Behavior(ModelExport),
'mp4-export': PluginSpec.Behavior(Mp4Export),
'geo-export': PluginSpec.Behavior(GeometryExport),
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
// 3rd party extensions
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
'g3d': PluginSpec.Behavior(G3DFormat), // TODO: consider removing this for Mol* 6.0
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
};
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
mvs: {
MVSData,
createBuilder: MVSData.createBuilder,
loadMVS,
loadMVSData,
util: {
...MVSUtil
}
},
modelArchive: {
qualityAssessment: {
config: MAQualityAssessmentConfig
}
}
};

View File

@@ -1,12 +1,16 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* 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 './mvs.html';
import './embedded.html';
import './favicon.ico';
import './index.html';
import '../../mol-plugin-ui/skin/light.scss';
export * from './lib';
export * from './extensions';
export * from './app';
export * from './presets';

58
src/apps/viewer/lib.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as Structure from '../../mol-model/structure';
import { DataLoci, EveryLoci, Loci } from '../../mol-model/loci';
import { Volume } from '../../mol-model/volume';
import { Shape, ShapeGroup } from '../../mol-model/shape';
import * as LinearAlgebra3D from '../../mol-math/linear-algebra/3d';
import { PluginContext } from '../../mol-plugin/context';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { StateActions } from '../../mol-plugin-state/actions';
import { PluginExtensions } from './extensions';
export const lib = {
structure: {
...Structure,
},
volume: {
Volume,
},
shape: {
Shape,
ShapeGroup,
},
loci: {
Loci,
DataLoci,
EveryLoci,
},
math: {
LinearAlgebra: {
...LinearAlgebra3D,
}
},
plugin: {
PluginContext,
PluginConfig,
PluginBehavior,
PluginSpec,
PluginStateObject,
PluginStateTransform,
StateTransforms,
StateActions,
DefaultPluginSpec,
DefaultPluginUISpec,
},
extensions: {
...PluginExtensions
}
};

147
src/apps/viewer/mvs.html Normal file
View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer MolViewSpec Example</title>
<style>
#app {
position: absolute;
left: 100px;
top: 100px;
width: 800px;
height: 600px;
}
#controls {
position: absolute;
display: flex;
align-items: center;
font-family: sans-serif;
gap: 8px;
left: 10px;
top: 10px;
z-index: 10;
background-color: black;
padding: 10px;
color: white;
}
</style>
<link rel="stylesheet" type="text/css" href="theme/dark.css" />
</head>
<body>
<div id="app"></div>
<div id="controls">
<button onclick="selectResidues()">Select Residues 10-50</button>
<button onclick="clearSelection()">Clear Selection</button>
<div id="selection-info"></div>
</div>
<script type="text/javascript" src="molstar.js"></script>
<script type="text/javascript">
function selectResidues() {
viewer.structureInteractivity({
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
action: 'select',
});
}
function clearSelection() {
viewer.structureInteractivity({ action: 'select' });
}
molstar.Viewer.create('app', {
layoutIsExpanded: true,
layoutShowControls: false,
layoutShowRemoteState: false,
layoutShowSequence: true,
layoutShowLog: false,
layoutShowLeftPanel: true,
viewportShowExpand: true,
viewportShowSelectionMode: false,
viewportShowAnimation: false,
viewportFocusBehavior: 'disabled',
viewportBackgroundColor: 'black',
pdbProvider: 'rcsb',
emdbProvider: 'rcsb',
}).then(viewer => {
// Make the viewer accessible globally for the demo buttons
window.viewer = viewer;
// Build MVS state
const builder = molstar.lib.extensions.mvs.createBuilder();
const structure = builder
.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif' })
.parse({ format: 'bcif' })
.modelStructure({});
structure
.component({ selector: 'polymer' })
.representation({ type: 'cartoon' })
.color({ color: 'green' });
structure
.component({ selector: 'ligand' })
.representation({ type: 'ball_and_stick' })
.color({ color: '#cc3399' });
builder.canvas({
background_color: "black",
})
structure.primitives()
.sphere({
center: { label_comp_id: 'REA' },
radius: 3,
custom: { action: 'Action 1' },
})
.label({
text: '1',
position: { label_comp_id: 'REA' },
label_size: 2.5,
label_color: 'blue',
});
structure.primitives()
.sphere({
center: { label_seq_id: 2 },
radius: 3,
custom: { action: 'Action 2' },
})
.label({
text: '2',
position: { label_seq_id: 2 },
label_size: 2.5,
label_color: 'blue',
});
const mvsData = builder.getState();
viewer.loadMvsData(mvsData, 'mvsj');
// Show current residue interaction
viewer.subscribe(viewer.plugin.behaviors.interaction.hover, e => {
const infoElement = document.getElementById('selection-info');
if (!infoElement) return;
if (molstar.lib.structure.StructureElement.Loci.is(e.current.loci)) {
molstar.lib.structure.StructureElement.Loci.forEachLocation(e.current.loci, location => {
const props = molstar.lib.structure.StructureProperties;
infoElement.innerText = `Hovered Residue: ${props.chain.label_asym_id(location)} ${props.residue.label_seq_id(location)}`;
});
} else {
infoElement.innerText = '';
}
});
// Show clicked primitive action
viewer.subscribe(viewer.plugin.behaviors.interaction.click, e => {
const nodes = molstar.lib.extensions.mvs.util.tryGetPrimitivesFromLoci(e.current.loci);
if (nodes?.length) {
alert('Clicked on: ' + (nodes[0].custom?.action || 'unknown'));
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
/**
* 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 { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { G3dProvider } from '../../extensions/g3d/format';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { ExtensionMap } from './extensions';
const CustomFormats: [string, DataFormatProvider][] = [
['g3d', G3dProvider] as const
];
export const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
layoutShowSequence: true,
layoutShowLog: true,
layoutShowLeftPanel: true,
collapseLeftPanel: false,
collapseRightPanel: false,
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
pixelScale: PluginConfig.General.PixelScale.defaultValue,
pickScale: PluginConfig.General.PickScale.defaultValue,
transparency: PluginConfig.General.Transparency.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
illumination: false,
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
viewportFocusBehavior: 'default' as 'default' | 'disabled',
viewportBackgroundColor: undefined as string | undefined,
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
config: [] as [PluginConfigItem, any][],
};
export type ViewerOptions = typeof DefaultViewerOptions;

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../mol-state';
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-viewer-auto',
display: {
name: 'Automatic (w/ Annotation)', group: 'Annotation',
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
},
isApplicable(a) {
return (
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
);
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
} else {
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
}
}
});

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/blue.scss';

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/dark.scss';

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/light.scss';

View File

@@ -67,6 +67,12 @@ export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives
export class MVSPrimitivesData extends SO.Create<PrimitiveBuilderContext>({ name: 'Primitive Data', typeClass: 'Object' }) { }
export class MVSPrimitiveShapes extends SO.Create<{ mesh?: Shape<Mesh>, labels?: Shape<Text> }>({ name: 'Primitive Shapes', typeClass: 'Object' }) { }
export interface MVSPrimitiveShapeSourceData {
kind: 'mvs-primitives',
node: MVSNode<'primitives'>,
groupToNode: Map<number, MVSNode<'primitive'>>,
}
export type MVSDownloadPrimitiveData = typeof MVSDownloadPrimitiveData
export const MVSDownloadPrimitiveData = MVSTransform({
name: 'mvs-download-primitive-data',
@@ -605,7 +611,7 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
MeshBuilder.getMesh(meshBuilder),
(g) => colors.get(g) as Color ?? color,
(g) => 1,
@@ -638,7 +644,7 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
linesBuilder.getLines(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
@@ -673,7 +679,7 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev: Text | und
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
labelsBuilder.getText(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,

View File

@@ -4,8 +4,13 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { OrderedSet } from '../../mol-data/int';
import { Loci } from '../../mol-model/loci';
import { ShapeGroup } from '../../mol-model/shape';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector, StateTree } from '../../mol-state';
import type { MVSPrimitiveShapeSourceData } from './components/primitives';
import type { MVSNode } from './tree/mvs/mvs-tree';
/**
@@ -33,4 +38,20 @@ export function createMVSRefMap(plugin: PluginContext) {
});
return mapping;
}
export function tryGetPrimitivesFromLoci(loci: Loci | undefined): MVSNode<'primitive'>[] | undefined {
if (!ShapeGroup.isLoci(loci)) return undefined;
const srcData = loci.shape.sourceData as MVSPrimitiveShapeSourceData;
if (srcData?.kind !== 'mvs-primitives') return undefined;
const nodes: MVSNode<'primitive'>[] = [];
for (const group of loci.groups) {
OrderedSet.forEach(group.ids, id => {
const node = srcData.groupToNode.get(id);
if (node) nodes.push(node);
});
}
return nodes.length > 0 ? nodes : undefined;
}

View File

@@ -13,7 +13,7 @@ export class PluginComponent {
private _ev: RxEventHelper | undefined;
private subs: Subscription[] | undefined = void 0;
protected subscribe<T>(obs: Observable<T> | undefined, action: (v: T) => void) {
subscribe<T>(obs: Observable<T> | undefined, action: (v: T) => void) {
if (!obs) return { unsubscribe: () => {} };
if (typeof this.subs === 'undefined') this.subs = [];

View File

@@ -25,7 +25,7 @@ import { PluginUIContext } from '../context';
import { ActionMenu } from './action-menu';
import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
import { Button, ControlGroup, ControlRow, ExpandGroup, IconButton, TextInput, ToggleButton } from './common';
import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, WarningSvg } from './icons';
import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, TuneSvg, WarningSvg } from './icons';
import { legendFor } from './legend';
import { LineGraphComponent } from './line-graph/line-graph-component';
import { Slider, Slider2 } from './slider';
@@ -526,7 +526,7 @@ export class SelectControl extends React.PureComponent<ParamProps<PD.Select<stri
: void 0;
return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign, overflow: 'hidden', textOverflow: 'ellipsis' }}
label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} />;
label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} className='msp-select-toggle' />;
}
renderAddOn() {
@@ -1192,7 +1192,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
if (!this.state.isExpanded) {
return <div className='msp-mapped-parameter-group'>
{ctrl}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} style={{ opacity: 0.7 }} />
</div>;
}
@@ -1203,7 +1203,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
return <div className='msp-mapped-parameter-group'>
{ctrl}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<div className='msp-control-offset'>
{this.pivotedPresets()}
<ParameterControls params={filtered} onEnter={this.props.onEnter} values={this.props.value} onChange={this.onChangeParam} isDisabled={this.props.isDisabled} />
@@ -1312,7 +1312,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
if (!this.areParamsEmpty(param.params)) {
return <div className='msp-mapped-parameter-group'>
{Select}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} style={{ opacity: this.state.isExpanded ? undefined : 0.7 }} />
{this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
</div>;
}

View File

@@ -286,6 +286,27 @@
color: $msp-btn-commit-on-hover-font-color;
}
.msp-select-toggle::after {
content: "";
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid $hover-font-color;
opacity: 0;
pointer-events: none;
}
.msp-select-toggle:hover::after {
opacity: 1;
}
.msp-btn-action {
height: $row-height;
line-height: $row-height;