diff --git a/CHANGELOG.md b/CHANGELOG.md index cbfc141d3..2dd62cf63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/docs/plugin/instance.md b/docs/docs/plugin/instance.md index 650a42e59..26a47bb2f 100644 --- a/docs/docs/plugin/instance.md +++ b/docs/docs/plugin/instance.md @@ -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 + - `
@@ -62,13 +76,15 @@ Example usage without using WebPack: ``` -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, diff --git a/scripts/build.mjs b/scripts/build.mjs index 8ba81eec2..79dd7c55f 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -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)); diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index 6035d8253..50067d974 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -7,34 +7,20 @@ * @author Adam Midlik */ -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 = {}) { @@ -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, 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 - } - } -}; +} \ No newline at end of file diff --git a/src/apps/viewer/extensions.ts b/src/apps/viewer/extensions.ts new file mode 100644 index 000000000..35bebdd9a --- /dev/null +++ b/src/apps/viewer/extensions.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + * @author Alexander Rose + * @author Adam Midlik + */ + +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 + } + } +}; diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 9d8b9c3c8..fb3868bb0 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -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 * @author Alexander Rose */ +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'; diff --git a/src/apps/viewer/lib.ts b/src/apps/viewer/lib.ts new file mode 100644 index 000000000..ae6f46da7 --- /dev/null +++ b/src/apps/viewer/lib.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + */ + +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 + } +}; \ No newline at end of file diff --git a/src/apps/viewer/mvs.html b/src/apps/viewer/mvs.html new file mode 100644 index 000000000..3c29665da --- /dev/null +++ b/src/apps/viewer/mvs.html @@ -0,0 +1,147 @@ + + + + + + + Mol* Viewer MolViewSpec Example + + + + +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/apps/viewer/options.ts b/src/apps/viewer/options.ts new file mode 100644 index 000000000..eda6e6041 --- /dev/null +++ b/src/apps/viewer/options.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + * @author Alexander Rose + */ + +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; \ No newline at end of file diff --git a/src/apps/viewer/presets.ts b/src/apps/viewer/presets.ts new file mode 100644 index 000000000..512333898 --- /dev/null +++ b/src/apps/viewer/presets.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose + * @author David Sehnal + */ + +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); + } + } +}); diff --git a/src/apps/viewer/theme/blue.ts b/src/apps/viewer/theme/blue.ts new file mode 100644 index 000000000..b0a1143c3 --- /dev/null +++ b/src/apps/viewer/theme/blue.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + */ + +import '../../../mol-plugin-ui/skin/blue.scss'; \ No newline at end of file diff --git a/src/apps/viewer/theme/dark.ts b/src/apps/viewer/theme/dark.ts new file mode 100644 index 000000000..23c0b3bca --- /dev/null +++ b/src/apps/viewer/theme/dark.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + */ + +import '../../../mol-plugin-ui/skin/dark.scss'; \ No newline at end of file diff --git a/src/apps/viewer/theme/light.ts b/src/apps/viewer/theme/light.ts new file mode 100644 index 000000000..13520de9a --- /dev/null +++ b/src/apps/viewer/theme/light.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal + */ + +import '../../../mol-plugin-ui/skin/light.scss'; \ No newline at end of file diff --git a/src/extensions/mvs/components/primitives.ts b/src/extensions/mvs/components/primitives.ts index 9d1832994..28f6674c0 100644 --- a/src/extensions/mvs/components/primitives.ts +++ b/src/extensions/mvs/components/primitives.ts @@ -67,6 +67,12 @@ export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives export class MVSPrimitivesData extends SO.Create({ name: 'Primitive Data', typeClass: 'Object' }) { } export class MVSPrimitiveShapes extends SO.Create<{ mesh?: Shape, labels?: Shape }>({ name: 'Primitive Shapes', typeClass: 'Object' }) { } +export interface MVSPrimitiveShapeSourceData { + kind: 'mvs-primitives', + node: MVSNode<'primitives'>, + groupToNode: Map>, +} + 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, diff --git a/src/extensions/mvs/util.ts b/src/extensions/mvs/util.ts index 2ab093db7..8a2e07af2 100644 --- a/src/extensions/mvs/util.ts +++ b/src/extensions/mvs/util.ts @@ -4,8 +4,13 @@ * @author David Sehnal */ +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; } \ No newline at end of file diff --git a/src/mol-plugin-state/component.ts b/src/mol-plugin-state/component.ts index 323e0b0e7..0125f1f5a 100644 --- a/src/mol-plugin-state/component.ts +++ b/src/mol-plugin-state/component.ts @@ -13,7 +13,7 @@ export class PluginComponent { private _ev: RxEventHelper | undefined; private subs: Subscription[] | undefined = void 0; - protected subscribe(obs: Observable | undefined, action: (v: T) => void) { + subscribe(obs: Observable | undefined, action: (v: T) => void) { if (!obs) return { unsubscribe: () => {} }; if (typeof this.subs === 'undefined') this.subs = []; diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index c81209b58..81f6e2d53 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -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; + 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> if (!this.state.isExpanded) { return
{ctrl} - +
; } @@ -1203,7 +1203,7 @@ export class GroupControl extends React.PureComponent> return
{ctrl} - +
{this.pivotedPresets()} @@ -1312,7 +1312,7 @@ export class MappedControl extends React.PureComponent if (!this.areParamsEmpty(param.params)) { return
{Select} - + {this.state.isExpanded && }
; } diff --git a/src/mol-plugin-ui/skin/base/components/controls-base.scss b/src/mol-plugin-ui/skin/base/components/controls-base.scss index 80ab4e99e..5a418119e 100644 --- a/src/mol-plugin-ui/skin/base/components/controls-base.scss +++ b/src/mol-plugin-ui/skin/base/components/controls-base.scss @@ -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;