Quellcode durchsuchen

refactored format registry & added StateBuilder.commit()

David Sehnal vor 5 Jahren
Ursprung
Commit
394ff05626

+ 1 - 1
src/apps/viewer/extensions/cellpack/model.ts

@@ -358,7 +358,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
         .apply(ParseCellPack)
 
     const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime)
-    const { packings } = cellPackObject.data
+    const { packings } = cellPackObject.obj!.data
 
     await handleHivRna({ runtime, fetch: plugin.fetch }, packings, params.baseUrl)
 

+ 1 - 1
src/mol-plugin-state/actions.ts

@@ -6,7 +6,7 @@
 
 import * as Structure from './actions/structure'
 import * as Volume from './actions/volume'
-import * as DataFormat from './actions/data-format'
+import * as DataFormat from './actions/file'
 
 export const StateActions = {
     Structure,

+ 0 - 175
src/mol-plugin-state/actions/data-format.ts

@@ -1,175 +0,0 @@
-/**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import msgpackDecode from '../../mol-io/common/msgpack/decode';
-import { PluginContext } from '../../mol-plugin/context';
-import { State, StateAction, StateObjectRef } from '../../mol-state';
-import { Task } from '../../mol-task';
-import { FileInfo, getFileInfo } from '../../mol-util/file-info';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { PluginStateObject } from '../objects';
-import { PlyProvider } from './shape';
-import { DcdProvider, GroProvider, MmcifProvider, PdbProvider, Provider3dg, PsfProvider, MolProvider, CifCoreProvider } from './structure';
-import { Ccp4Provider, DscifProvider, Dsn6Provider } from './volume';
-
-export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
-    private _list: { name: string, provider: DataFormatProvider<D> }[] = []
-    private _map = new Map<string, DataFormatProvider<D>>()
-    private _extensions: Set<string> | undefined = undefined
-    private _binaryExtensions: Set<string> | undefined = undefined
-    private _options: [string, string][] | undefined = undefined
-
-    get types(): [string, string][] {
-        return this._list.map(e => [e.name, e.provider.label] as [string, string]);
-    }
-
-    get extensions() {
-        if (this._extensions) return this._extensions
-        const extensions = new Set<string>()
-        this._list.forEach(({ provider }) => {
-            provider.stringExtensions.forEach(ext => extensions.add(ext))
-            provider.binaryExtensions.forEach(ext => extensions.add(ext))
-        })
-        this._extensions = extensions
-        return extensions
-    }
-
-    get binaryExtensions() {
-        if (this._binaryExtensions) return this._binaryExtensions
-        const binaryExtensions = new Set<string>()
-        this._list.forEach(({ provider }) => provider.binaryExtensions.forEach(ext => binaryExtensions.add(ext)))
-        this._binaryExtensions = binaryExtensions
-        return binaryExtensions
-    }
-
-    get options() {
-        if (this._options) return this._options
-        const options: [string, string][] = [['auto', 'Automatic']]
-        this._list.forEach(({ name, provider }) => options.push([ name, provider.label ]))
-        this._options = options
-        return options
-    }
-
-    constructor() {
-        this.add('3dg', Provider3dg)
-        this.add('ccp4', Ccp4Provider)
-        this.add('cifCore', CifCoreProvider)
-        this.add('dcd', DcdProvider)
-        this.add('dscif', DscifProvider)
-        this.add('dsn6', Dsn6Provider)
-        this.add('gro', GroProvider)
-        this.add('mol', MolProvider)
-        this.add('mmcif', MmcifProvider)
-        this.add('pdb', PdbProvider)
-        this.add('ply', PlyProvider)
-        this.add('psf', PsfProvider)
-    };
-
-    private _clear() {
-        this._extensions = undefined
-        this._binaryExtensions = undefined
-        this._options = undefined
-    }
-
-    add(name: string, provider: DataFormatProvider<D>) {
-        this._clear()
-        this._list.push({ name, provider })
-        this._map.set(name, provider)
-    }
-
-    remove(name: string) {
-        this._clear()
-        this._list.splice(this._list.findIndex(e => e.name === name), 1)
-        this._map.delete(name)
-    }
-
-    auto(info: FileInfo, dataStateObject: D) {
-        for (let i = 0, il = this.list.length; i < il; ++i) {
-            const { provider } = this._list[i]
-            if (provider.isApplicable(info, dataStateObject.data)) return provider
-        }
-        throw new Error('no compatible data format provider available')
-    }
-
-    get(name: string): DataFormatProvider<D> {
-        if (this._map.has(name)) {
-            return this._map.get(name)!
-        } else {
-            throw new Error(`unknown data format name '${name}'`)
-        }
-    }
-
-    get list() {
-        return this._list
-    }
-}
-
-export type DataFormatBuilderOptions = { visuals: boolean }
-
-export interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
-    label: string
-    description: string
-    stringExtensions: string[]
-    binaryExtensions: string[]
-    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
-    getDefaultBuilder(ctx: PluginContext, data: StateObjectRef<D>, options: DataFormatBuilderOptions, state: State): Task<void>
-}
-
-//
-
-export const OpenFiles = StateAction.build({
-    display: { name: 'Open Files', description: 'Load one or more files and optionally create default visuals' },
-    from: PluginStateObject.Root,
-    params: (a, ctx: PluginContext) => {
-        const { extensions, options } = ctx.dataFormat.registry
-        return {
-            files: PD.FileList({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',') + ',.gz,.zip', multiple: true }),
-            format: PD.Select('auto', options),
-            visuals: PD.Boolean(true, { description: 'Add default visuals' }),
-        }
-    }
-})(({ params, state }, plugin: PluginContext) => Task.create('Open Files', async taskCtx => {
-    await state.transaction(async () => {
-        if (params.files === null) {
-            plugin.log.error('No file(s) selected')
-            return
-        }
-        for (let i = 0, il = params.files.length; i < il; ++i) {
-            try {
-                const file = params.files[i]
-                const info = getFileInfo(file)
-                const isBinary = plugin.dataFormat.registry.binaryExtensions.has(info.ext)
-                const { data } = await plugin.builders.data.readFile({ file, isBinary });
-                const provider = params.format === 'auto'
-                    ? plugin.dataFormat.registry.auto(info, data.cell?.obj!)
-                    : plugin.dataFormat.registry.get(params.format)
-
-                // need to await so that the enclosing Task finishes after the update is done.
-                await provider.getDefaultBuilder(plugin, data, { visuals: params.visuals }, state).runInContext(taskCtx)
-            } catch (e) {
-                plugin.log.error(e)
-            }
-        }
-    }).runInContext(taskCtx);
-}));
-
-//
-
-type cifVariants = 'dscif' | 'coreCif' | -1
-export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
-    if (info.ext === 'bcif') {
-        try {
-            // TODO: find a way to run msgpackDecode only once
-            //      now it is run twice, here and during file parsing
-            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
-        } catch { }
-    } else if (info.ext === 'cif') {
-        const str = data as string
-        if (str.startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
-        if (str.includes('atom_site_fract_x') || str.includes('atom_site.fract_x')) return 'coreCif'
-    }
-    return -1
-}

+ 58 - 0
src/mol-plugin-state/actions/file.ts

@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from '../../mol-plugin/context';
+import { StateAction } from '../../mol-state';
+import { Task } from '../../mol-task';
+import { getFileInfo } from '../../mol-util/file-info';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+
+export const OpenFiles = StateAction.build({
+    display: { name: 'Open Files', description: 'Load one or more files and optionally create default visuals' },
+    from: PluginStateObject.Root,
+    params: (a, ctx: PluginContext) => {
+        const { extensions, options } = ctx.dataFormats
+        return {
+            files: PD.FileList({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',') + ',.gz,.zip', multiple: true }),
+            format: PD.Select('auto', options),
+            visuals: PD.Boolean(true, { description: 'Add default visuals' }),
+        }
+    }
+})(({ params, state }, plugin: PluginContext) => Task.create('Open Files', async taskCtx => {
+    plugin.behaviors.layout.leftPanelTabName.next('data');
+
+    await state.transaction(async () => {
+        if (params.files === null) {
+            plugin.log.error('No file(s) selected')
+            return
+        }
+        for (let i = 0, il = params.files.length; i < il; ++i) {
+            try {
+                const file = params.files[i]
+                const info = getFileInfo(file)
+                const isBinary = plugin.dataFormats.binaryExtensions.has(info.ext)
+                const { data } = await plugin.builders.data.readFile({ file, isBinary });
+                const provider = params.format === 'auto'
+                    ? plugin.dataFormats.auto(info, data.cell?.obj!)
+                    : plugin.dataFormats.get(params.format)
+
+                if (!provider) {
+                    plugin.log.warn(`OpenFiles: could not find data provider for '${info.name}.${info.ext}'`);
+                    continue;
+                }
+
+                // need to await so that the enclosing Task finishes after the update is done.
+                const parsed = await provider.parse(plugin, data);
+                if (params.visuals) {
+                    await provider.visuals?.(plugin, parsed);
+                }
+            } catch (e) {
+                plugin.log.error(e)
+            }
+        }
+    }).runInContext(taskCtx);
+}));

+ 0 - 33
src/mol-plugin-state/actions/shape.ts

@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { PluginContext } from '../../mol-plugin/context';
-import { State, StateBuilder } from '../../mol-state';
-import { Task } from '../../mol-task';
-import { FileInfo } from '../../mol-util/file-info';
-import { StateTransforms } from '../transforms';
-import { DataFormatProvider, DataFormatBuilderOptions } from './data-format';
-
-export const PlyProvider: DataFormatProvider<any> = {
-    label: 'PLY',
-    description: 'PLY',
-    stringExtensions: ['ply'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'ply'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options: DataFormatBuilderOptions, state: State) => {
-        return Task.create('PLY default builder', async taskCtx => {
-            let tree: StateBuilder.To<any> = state.build().to(data)
-                .apply(StateTransforms.Data.ParsePly)
-                .apply(StateTransforms.Model.ShapeFromPly)
-            if (options.visuals) {
-                tree = tree.apply(StateTransforms.Representation.ShapeRepresentation3D)
-            }
-            await state.updateTree(tree).runInContext(taskCtx)
-        })
-    }
-}

+ 5 - 149
src/mol-plugin-state/actions/structure.ts

@@ -8,158 +8,14 @@
 import { PluginContext } from '../../mol-plugin/context';
 import { StateAction, StateSelection, StateTransformer } from '../../mol-state';
 import { Task } from '../../mol-task';
-import { FileInfo } from '../../mol-util/file-info';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { BuiltInTrajectoryFormat, BuildInTrajectoryFormats } from '../formats/trajectory';
+import { PresetStructureRepresentations } from '../builder/structure/representation-preset';
+import { BuiltInTrajectoryFormat, BuiltInTrajectoryFormats } from '../formats/trajectory';
 import { RootStructureDefinition } from '../helpers/root-structure';
 import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
-import { Download, ParsePsf } from '../transforms/data';
-import { CoordinatesFromDcd, CustomModelProperties, CustomStructureProperties, TopologyFromPsf, TrajectoryFromModelAndCoordinates } from '../transforms/model';
-import { DataFormatProvider, guessCifVariant } from './data-format';
-import { PresetStructureRepresentations } from '../builder/structure/representation-preset';
-
-// TODO make unitcell creation part of preset
-
-export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | PluginStateObject.Data.Binary> = {
-    label: 'mmCIF',
-    description: 'mmCIF',
-    stringExtensions: ['cif', 'mmcif', 'mcif'],
-    binaryExtensions: ['bcif'],
-    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
-        if (info.ext === 'mmcif' || info.ext === 'mcif') return true
-        // assume undetermined cif/bcif files are mmCIF
-        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) === -1
-        return false
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('mmCIF default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, 'mmcif');
-            const representationPreset = options.visuals ? 'auto' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const CifCoreProvider: DataFormatProvider<any> = {
-    label: 'cifCore',
-    description: 'CIF Core',
-    stringExtensions: ['cif'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
-        if (info.ext === 'cif') return guessCifVariant(info, data) === 'coreCif'
-        return false
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('mmCIF default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, 'cifCore');
-            const representationPreset = options.visuals ? 'auto' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const PdbProvider: DataFormatProvider<any> = {
-    label: 'PDB',
-    description: 'PDB',
-    stringExtensions: ['pdb', 'ent'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'pdb' || info.ext === 'ent'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('PDB default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, 'pdb');
-            const representationPreset = options.visuals ? 'auto' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const GroProvider: DataFormatProvider<any> = {
-    label: 'GRO',
-    description: 'GRO',
-    stringExtensions: ['gro'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'gro'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('GRO default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, 'gro');
-            const representationPreset = options.visuals ? 'auto' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const MolProvider: DataFormatProvider<any> = {
-    label: 'MOL',
-    description: 'MOL',
-    stringExtensions: ['mol', 'sdf'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'mol' || info.ext === 'sdf'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('MOL default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, 'mol');
-            const representationPreset = options.visuals ? 'atomic-detail' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const Provider3dg: DataFormatProvider<any> = {
-    label: '3DG',
-    description: '3DG',
-    stringExtensions: ['3dg'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === '3dg'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options) => {
-        return Task.create('3DG default builder', async () => {
-            const trajectory = await ctx.builders.structure.parseTrajectory(data, '3dg');
-            const representationPreset = options.visuals ? 'auto' : 'empty';
-            await ctx.builders.structure.hierarchy.applyPreset(trajectory, 'default', { showUnitcell: options.visuals, representationPreset });
-        })
-    }
-}
-
-export const PsfProvider: DataFormatProvider<any> = {
-    label: 'PSF',
-    description: 'PSF',
-    stringExtensions: ['psf'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'psf'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options, state) => {
-        return Task.create('PSF default builder', async taskCtx => {
-            const build = state.build().to(data).apply(ParsePsf, {}, { state: { isGhost: true } }).apply(TopologyFromPsf)
-            await state.updateTree(build).runInContext(taskCtx)
-        })
-    }
-}
-
-export const DcdProvider: DataFormatProvider<any> = {
-    label: 'DCD',
-    description: 'DCD',
-    stringExtensions: [],
-    binaryExtensions: ['dcd'],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'dcd'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options, state) => {
-        return Task.create('DCD default builder', async taskCtx => {
-            const build = state.build().to(data).apply(CoordinatesFromDcd);
-            await state.updateTree(build).runInContext(taskCtx)
-        })
-    }
-}
-
-//
+import { Download } from '../transforms/data';
+import { CustomModelProperties, CustomStructureProperties, TrajectoryFromModelAndCoordinates } from '../transforms/model';
 
 const DownloadModelRepresentationOptions = (plugin: PluginContext) => PD.Group({
     type: RootStructureDefinition.getParams(void 0, 'auto').type,
@@ -210,7 +66,7 @@ const DownloadStructure = StateAction.build({
                 }, { isFlat: true, label: 'PubChem', description: 'Loads 3D conformer from PubChem.' }),
                 'url': PD.Group({
                     url: PD.Text(''),
-                    format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuildInTrajectoryFormats.map(f => f[0]), f => f)),
+                    format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuiltInTrajectoryFormats.map(f => f[0]), f => f)),
                     isBinary: PD.Boolean(false),
                     options
                 }, { isFlat: true, label: 'URL' })

+ 18 - 100
src/mol-plugin-state/actions/volume.ts

@@ -5,102 +5,14 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { VolumeIsoValue } from '../../mol-model/volume';
 import { PluginContext } from '../../mol-plugin/context';
-import { State, StateAction, StateBuilder, StateTransformer } from '../../mol-state';
+import { StateAction, StateTransformer } from '../../mol-state';
 import { Task } from '../../mol-task';
-import { ColorNames } from '../../mol-util/color/names';
-import { FileInfo, getFileInfo } from '../../mol-util/file-info';
+import { getFileInfo } from '../../mol-util/file-info';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginStateObject } from '../objects';
-import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
-import { VolumeRepresentation3DHelpers } from '../transforms/representation';
-import { DataFormatProvider, guessCifVariant, DataFormatBuilderOptions } from './data-format';
-
-export const Ccp4Provider: DataFormatProvider<any> = {
-    label: 'CCP4/MRC/BRIX',
-    description: 'CCP4/MRC/BRIX',
-    stringExtensions: [],
-    binaryExtensions: ['ccp4', 'mrc', 'map'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options: DataFormatBuilderOptions, state: State) => {
-        return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => {
-            let tree: StateBuilder.To<any> = state.build().to(data)
-                .apply(StateTransforms.Data.ParseCcp4, {}, { state: { isGhost: true } })
-                .apply(StateTransforms.Volume.VolumeFromCcp4)
-            if (options.visuals) {
-                tree = tree.apply(StateTransforms.Representation.VolumeRepresentation3D)
-            }
-            await state.updateTree(tree).runInContext(taskCtx)
-        })
-    }
-}
-
-export const Dsn6Provider: DataFormatProvider<any> = {
-    label: 'DSN6/BRIX',
-    description: 'DSN6/BRIX',
-    stringExtensions: [],
-    binaryExtensions: ['dsn6', 'brix'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'dsn6' || info.ext === 'brix'
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options: DataFormatBuilderOptions, state: State) => {
-        return Task.create('DSN6/BRIX default builder', async taskCtx => {
-            let tree: StateBuilder.To<any> = state.build().to(data)
-                .apply(StateTransforms.Data.ParseDsn6, {}, { state: { isGhost: true } })
-                .apply(StateTransforms.Volume.VolumeFromDsn6)
-            if (options.visuals) {
-                tree = tree.apply(StateTransforms.Representation.VolumeRepresentation3D)
-            }
-            await state.updateTree(tree).runInContext(taskCtx)
-        })
-    }
-}
-
-export const DscifProvider: DataFormatProvider<any> = {
-    label: 'DensityServer CIF',
-    description: 'DensityServer CIF',
-    stringExtensions: ['cif'],
-    binaryExtensions: ['bcif'],
-    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
-        return guessCifVariant(info, data) === 'dscif' ? true : false
-    },
-    getDefaultBuilder: (ctx: PluginContext, data, options: DataFormatBuilderOptions, state: State) => {
-        return Task.create('DensityServer CIF default builder', async taskCtx => {
-            const cifBuilder = state.build().to(data).apply(StateTransforms.Data.ParseCif)
-            const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx)
-            const b = state.build().to(cifBuilder.ref);
-            const blocks = cifStateObject.data.blocks.slice(1); // zero block contains query meta-data
-            let tree: StateBuilder.To<any, any>
-            if (blocks.length === 1) {
-                tree = b
-                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
-                if (options.visuals) {
-                    tree = tree.apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
-                }
-            } else if (blocks.length === 2) {
-                tree = b
-                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
-                if (options.visuals) {
-                    tree = tree.apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue }))
-                }
-                const vol = tree.to(cifBuilder.ref)
-                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
-                const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
-                tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams)
-                const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })
-                tree = tree.to(vol.ref).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams)
-            } else {
-                throw new Error('unknown number of blocks')
-            }
-
-            await state.updateTree(tree).runInContext(taskCtx);
-        })
-    }
-}
+import { DataFormatProvider } from '../formats/provider';
 
 export { DownloadDensity };
 type DownloadDensity = typeof DownloadDensity
@@ -108,7 +20,7 @@ const DownloadDensity = StateAction.build({
     from: PluginStateObject.Root,
     display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
     params: (a, ctx: PluginContext) => {
-        const { options } = ctx.dataFormat.registry
+        const { options } = ctx.dataFormats
         return {
             source: PD.MappedStatic('pdb-xray', {
                 'pdb-xray': PD.Group({
@@ -147,10 +59,10 @@ const DownloadDensity = StateAction.build({
             })
         }
     }
-})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => {
+})(({ params }, plugin: PluginContext) => Task.create('Download Density', async taskCtx => {
     const src = params.source;
     let downloadParams: StateTransformer.Params<Download>;
-    let provider: DataFormatProvider<any>
+    let provider: DataFormatProvider | undefined;
 
     switch (src.name) {
         case 'url':
@@ -196,24 +108,30 @@ const DownloadDensity = StateAction.build({
         default: throw new Error(`${(src as any).name} not supported.`);
     }
 
-    const data = await ctx.builders.data.download(downloadParams);
+    const data = await plugin.builders.data.download(downloadParams);
 
     switch (src.name) {
         case 'url':
             downloadParams = src.params;
-            provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), data.cell?.obj!) : ctx.dataFormat.registry.get(src.params.format)
+            provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileInfo(downloadParams.url), data.cell?.obj!) : plugin.dataFormats.get(src.params.format)
             break;
         case 'pdb-xray':
             provider = src.params.provider.server === 'pdbe'
-                ? ctx.dataFormat.registry.get('ccp4')
-                : ctx.dataFormat.registry.get('dsn6')
+                ? plugin.dataFormats.get('ccp4')
+                : plugin.dataFormats.get('dsn6')
             break;
         case 'pdb-emd-ds':
         case 'pdb-xray-ds':
-            provider = ctx.dataFormat.registry.get('dscif')
+            provider = plugin.dataFormats.get('dscif')
             break;
         default: throw new Error(`${(src as any).name} not supported.`);
     }
 
-    await provider.getDefaultBuilder(ctx, data, { visuals: true }, state).runInContext(taskCtx)
+    if (!provider) {
+        plugin.log.warn('DownloadDensity: Format provider not found.');
+        return;
+    }
+
+    const volumes = await provider.parse(plugin, data);
+    await provider.visuals?.(plugin, volumes);
 }));

+ 1 - 1
src/mol-plugin-state/builder/structure.ts

@@ -26,7 +26,7 @@ export class StructureBuilder {
     }
 
     private async parseTrajectoryData(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: BuiltInTrajectoryFormat | TrajectoryFormatProvider) {
-        const provider = typeof format === 'string' ? this.plugin.dataFormat.trajectory.get(format) : format;
+        const provider = typeof format === 'string' ? this.plugin.dataFormats.get(format) as TrajectoryFormatProvider : format;
         if (!provider) throw new Error(`'${format}' is not a supported data format.`);
         const { trajectory } = await provider.parse(this.plugin, data);
         return trajectory;

+ 2 - 1
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -121,7 +121,8 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
             coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'polymer-id' }, { tag: 'coarse' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: false });
+        await update.commit({ revertOnError: false });
+
         return { components, representations };
     }
 });

+ 41 - 0
src/mol-plugin-state/formats/provider.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2019-2020 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 msgpackDecode from '../../mol-io/common/msgpack/decode';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateObjectRef } from '../../mol-state';
+import { FileInfo } from '../../mol-util/file-info';
+import { PluginStateObject } from '../objects';
+
+export interface DataFormatProvider<P = any, R = any, V = any> {
+    label: string,
+    description: string,
+    category?: string,
+    stringExtensions?: string[],
+    binaryExtensions?: string[],
+    isApplicable?(info: FileInfo, data: string | Uint8Array): boolean,
+    parse(plugin: PluginContext, data: StateObjectRef<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, params?: P): Promise<R>,
+    visuals?(plugin: PluginContext, data: R): Promise<V> | undefined
+}
+
+export function DataFormatProvider<P extends DataFormatProvider>(provider: P): P { return provider; }
+
+type cifVariants = 'dscif' | 'coreCif' | -1
+export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
+    if (info.ext === 'bcif') {
+        try {
+            // TODO: find a way to run msgpackDecode only once
+            //      now it is run twice, here and during file parsing
+            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
+        } catch { }
+    } else if (info.ext === 'cif') {
+        const str = data as string
+        if (str.startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
+        if (str.includes('atom_site_fract_x') || str.includes('atom_site.fract_x')) return 'coreCif'
+    }
+    return -1
+}

+ 26 - 42
src/mol-plugin-state/formats/registry.ts

@@ -5,18 +5,20 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import msgpackDecode from '../../mol-io/common/msgpack/decode';
-import { PluginContext } from '../../mol-plugin/context';
-import { StateObjectRef } from '../../mol-state';
 import { FileInfo } from '../../mol-util/file-info';
 import { PluginStateObject } from '../objects';
+import { DataFormatProvider } from './provider';
+import { BuiltInTrajectoryFormats } from './trajectory';
+import { BuiltInVolumeFormats } from './volume';
+import { BuiltInShapeFormats } from './shape';
+import { BuiltInStructureFormats } from './structure';
 
-export class DataFormatRegistry<Provider extends DataFormatProvider> {
-    private _list: { name: string, provider: Provider }[] = []
-    private _map = new Map<string, Provider>()
+export class DataFormatRegistry {
+    private _list: { name: string, provider: DataFormatProvider }[] = []
+    private _map = new Map<string, DataFormatProvider>()
     private _extensions: Set<string> | undefined = undefined
     private _binaryExtensions: Set<string> | undefined = undefined
-    private _options: [string, string][] | undefined = undefined
+    private _options: [string, string, string][] | undefined = undefined
 
     get types(): [string, string][] {
         return this._list.map(e => [e.name, e.provider.label] as [string, string]);
@@ -26,8 +28,8 @@ export class DataFormatRegistry<Provider extends DataFormatProvider> {
         if (this._extensions) return this._extensions
         const extensions = new Set<string>()
         this._list.forEach(({ provider }) => {
-            provider.stringExtensions.forEach(ext => extensions.add(ext))
-            provider.binaryExtensions.forEach(ext => extensions.add(ext))
+            provider.stringExtensions?.forEach(ext => extensions.add(ext))
+            provider.binaryExtensions?.forEach(ext => extensions.add(ext))
         })
         this._extensions = extensions
         return extensions
@@ -36,21 +38,24 @@ export class DataFormatRegistry<Provider extends DataFormatProvider> {
     get binaryExtensions() {
         if (this._binaryExtensions) return this._binaryExtensions
         const binaryExtensions = new Set<string>()
-        this._list.forEach(({ provider }) => provider.binaryExtensions.forEach(ext => binaryExtensions.add(ext)))
+        this._list.forEach(({ provider }) => provider.binaryExtensions?.forEach(ext => binaryExtensions.add(ext)))
         this._binaryExtensions = binaryExtensions
         return binaryExtensions
     }
 
     get options() {
         if (this._options) return this._options
-        const options: [string, string][] = [['auto', 'Automatic']]
-        this._list.forEach(({ name, provider }) => options.push([ name, provider.label ]))
+        const options: [string, string, string][] = [['auto', 'Automatic', '']]
+        this._list.forEach(({ name, provider }) => options.push([ name, provider.label, provider.category || '' ]))
         this._options = options
         return options
     }
 
-    constructor(buildInFormats: ReadonlyArray<readonly [string, Provider]>) {
-        for (const [id, p] of buildInFormats) this.add(id, p);
+    constructor() {
+        for (const [id, p] of BuiltInVolumeFormats) this.add(id, p);
+        for (const [id, p] of BuiltInStructureFormats) this.add(id, p);
+        for (const [id, p] of BuiltInShapeFormats) this.add(id, p);
+        for (const [id, p] of BuiltInTrajectoryFormats) this.add(id, p);
     };
 
     private _clear() {
@@ -59,7 +64,7 @@ export class DataFormatRegistry<Provider extends DataFormatProvider> {
         this._options = undefined
     }
 
-    add(name: string, provider: Provider) {
+    add(name: string, provider: DataFormatProvider) {
         this._clear()
         this._list.push({ name, provider })
         this._map.set(name, provider)
@@ -74,12 +79,16 @@ export class DataFormatRegistry<Provider extends DataFormatProvider> {
     auto(info: FileInfo, dataStateObject: PluginStateObject.Data.Binary | PluginStateObject.Data.String) {
         for (let i = 0, il = this.list.length; i < il; ++i) {
             const { provider } = this._list[i];
-            if (provider.isApplicable(info, dataStateObject.data)) return provider;
+
+            let hasExt = false;
+            if (provider.binaryExtensions && provider.binaryExtensions.indexOf(info.ext) >= 0) hasExt = true;
+            else if (provider.stringExtensions && provider.stringExtensions.indexOf(info.ext) >= 0) hasExt = true;
+            if (hasExt && (!provider.isApplicable || provider.isApplicable(info, dataStateObject.data))) return provider;
         }
         return;
     }
 
-    get(name: string): Provider | undefined {
+    get(name: string): DataFormatProvider | undefined {
         if (this._map.has(name)) {
             return this._map.get(name)!
         } else {
@@ -90,29 +99,4 @@ export class DataFormatRegistry<Provider extends DataFormatProvider> {
     get list() {
         return this._list
     }
-}
-
-export interface DataFormatProvider<P = any, R = any> {
-    label: string
-    description: string
-    stringExtensions: string[]
-    binaryExtensions: string[]
-    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
-    parse(plugin: PluginContext, data: StateObjectRef<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, params?: P): Promise<R>
-}
-
-type cifVariants = 'dscif' | 'coreCif' | -1
-export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
-    if (info.ext === 'bcif') {
-        try {
-            // TODO: find a way to run msgpackDecode only once
-            //      now it is run twice, here and during file parsing
-            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
-        } catch { }
-    } else if (info.ext === 'cif') {
-        const str = data as string
-        if (str.startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
-        if (str.includes('atom_site_fract_x') || str.includes('atom_site.fract_x')) return 'coreCif'
-    }
-    return -1
 }

+ 35 - 0
src/mol-plugin-state/formats/shape.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018-2020 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 { StateTransforms } from '../transforms';
+import { DataFormatProvider } from './provider';
+
+const Category = 'Shape';
+
+export const PlyProvider = DataFormatProvider({
+    label: 'PLY',
+    description: 'PLY',
+    category: Category,
+    stringExtensions: ['ply'],
+    parse: async (plugin, data) => {
+        const format = plugin.state.data.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParsePly, {}, { state: { isGhost: true } });
+
+        const shape = format.apply(StateTransforms.Model.ShapeFromPly);
+
+        await plugin.updateDataState(format);
+
+        return { format: format.selector, shape: shape.selector };
+    }
+});
+
+export const BuiltInShapeFormats = [
+    ['ply', PlyProvider] as const,
+] as const
+
+export type BuildInShapeFormat = (typeof BuiltInShapeFormats)[number][0]

+ 51 - 0
src/mol-plugin-state/formats/structure.ts

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2018-2020 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 { StateTransforms } from '../transforms';
+import { DataFormatProvider } from './provider';
+
+const Category = 'Structure';
+
+export const PsfProvider = DataFormatProvider({
+    label: 'PSF',
+    description: 'PSF',
+    category: Category,
+    stringExtensions: ['psf'],
+    parse: async (plugin, data) => {
+        const format = plugin.state.data.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParsePsf, {}, { state: { isGhost: true } });
+        const topology = format.apply(StateTransforms.Model.TopologyFromPsf);
+
+        await plugin.updateDataState(format);
+
+        return { format: format.selector, topology: topology.selector };
+    }
+});
+
+export const DcdProvider = DataFormatProvider({
+    label: 'DCD',
+    description: 'DCD',
+    category: Category,
+    binaryExtensions: ['dcd'],
+    parse: async (plugin, data) => {
+        const coordinates = plugin.state.data.build()
+            .to(data)
+            .apply(StateTransforms.Model.CoordinatesFromDcd);
+
+        await plugin.updateDataState(coordinates);
+
+        return { coordinates: coordinates.selector };
+    }
+});
+
+export const BuiltInStructureFormats = [
+    ['psf', PsfProvider] as const,
+    ['dcd', DcdProvider] as const,
+] as const
+
+export type BuildInStructureFormat = (typeof BuiltInStructureFormats)[number][0]

+ 28 - 30
src/mol-plugin-state/formats/trajectory.ts

@@ -5,26 +5,29 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { FileInfo } from '../../mol-util/file-info';
 import { StateTransforms } from '../transforms';
-import { guessCifVariant, DataFormatProvider, DataFormatRegistry } from './registry';
+import { guessCifVariant, DataFormatProvider } from './provider';
 import { StateTransformer, StateObjectRef } from '../../mol-state';
 import { PluginStateObject } from '../objects';
+import { PluginContext } from '../../mol-plugin/context';
 
 export interface TrajectoryFormatProvider<P extends { trajectoryTags?: string | string[] } = { trajectoryTags?: string | string[] }, R extends { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> } = { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> }>
     extends DataFormatProvider<P, R> {
 }
 
-export function TrajectoryFormatRegistry() {
-    return new DataFormatRegistry<TrajectoryFormatProvider>(BuildInTrajectoryFormats);
+const Category = 'Trajectory';
+
+function defaultVisuals(plugin: PluginContext, data: { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> }) {
+    return plugin.builders.structure.hierarchy.applyPreset(data.trajectory, 'default');
 }
 
 export const MmcifProvider: TrajectoryFormatProvider = {
     label: 'mmCIF',
     description: 'mmCIF',
+    category: Category,
     stringExtensions: ['cif', 'mmcif', 'mcif'],
     binaryExtensions: ['bcif'],
-    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+    isApplicable: (info, data) => {
         if (info.ext === 'mmcif' || info.ext === 'mcif') return true
         // assume undetermined cif/bcif files are mmCIF
         if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) === -1
@@ -40,15 +43,16 @@ export const MmcifProvider: TrajectoryFormatProvider = {
             plugin.state.data.updateCellState(cif.ref, { isGhost: false });
         }
         return { trajectory: trajectory.selector };
-    }
+    },
+    visuals: defaultVisuals
 }
 
 export const CifCoreProvider: TrajectoryFormatProvider = {
     label: 'cifCore',
     description: 'CIF Core',
+    category: Category,
     stringExtensions: ['cif'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+    isApplicable: (info, data) => {
         if (info.ext === 'cif') return guessCifVariant(info, data) === 'coreCif'
         return false
     },
@@ -62,7 +66,8 @@ export const CifCoreProvider: TrajectoryFormatProvider = {
             plugin.state.data.updateCellState(cif.ref, { isGhost: false });
         }
         return { trajectory: trajectory.selector };
-    }
+    },
+    visuals: defaultVisuals
 }
 
 function directTrajectory(transformer: StateTransformer<PluginStateObject.Data.String | PluginStateObject.Data.Binary, PluginStateObject.Molecule.Trajectory>): TrajectoryFormatProvider['parse'] {
@@ -78,49 +83,42 @@ function directTrajectory(transformer: StateTransformer<PluginStateObject.Data.S
 export const PdbProvider: TrajectoryFormatProvider = {
     label: 'PDB',
     description: 'PDB',
+    category: Category,
     stringExtensions: ['pdb', 'ent'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'pdb' || info.ext === 'ent'
-    },
-    parse: directTrajectory(StateTransforms.Model.TrajectoryFromPDB)
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFromPDB),
+    visuals: defaultVisuals
 }
 
 export const GroProvider: TrajectoryFormatProvider = {
     label: 'GRO',
     description: 'GRO',
+    category: Category,
     stringExtensions: ['gro'],
     binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'gro'
-    },
-    parse: directTrajectory(StateTransforms.Model.TrajectoryFromGRO)
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFromGRO),
+    visuals: defaultVisuals
 }
 
 export const Provider3dg: TrajectoryFormatProvider = {
     label: '3DG',
     description: '3DG',
+    category: Category,
     stringExtensions: ['3dg'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === '3dg'
-    },
-    parse: directTrajectory(StateTransforms.Model.TrajectoryFrom3DG)
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFrom3DG),
+    visuals: defaultVisuals
 }
 
 export const MolProvider: TrajectoryFormatProvider = {
     label: 'MOL',
     description: 'MOL',
+    category: Category,
     stringExtensions: ['mol', 'sdf'],
-    binaryExtensions: [],
-    isApplicable: (info: FileInfo, data: string) => {
-        return info.ext === 'mol' || info.ext === 'sdf'
-    },
-    parse: directTrajectory(StateTransforms.Model.TrajectoryFromMOL)
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFromMOL),
+    visuals: defaultVisuals
 }
 
 
-export const BuildInTrajectoryFormats = [
+export const BuiltInTrajectoryFormats = [
     ['mmcif', MmcifProvider] as const,
     ['cifCore', CifCoreProvider] as const,
     ['pdb', PdbProvider] as const,
@@ -129,4 +127,4 @@ export const BuildInTrajectoryFormats = [
     ['mol', MolProvider] as const
 ] as const
 
-export type BuiltInTrajectoryFormat = (typeof BuildInTrajectoryFormats)[number][0]
+export type BuiltInTrajectoryFormat = (typeof BuiltInTrajectoryFormats)[number][0]

+ 119 - 0
src/mol-plugin-state/formats/volume.ts

@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2018-2020 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 { StateTransforms } from '../transforms';
+import { DataFormatProvider, guessCifVariant } from './provider';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateObjectSelector } from '../../mol-state';
+import { PluginStateObject } from '../objects';
+import { VolumeRepresentation3DHelpers } from '../transforms/representation';
+import { ColorNames } from '../../mol-util/color/names';
+import { VolumeIsoValue } from '../../mol-model/volume';
+
+const Category = 'Volume';
+
+async function defaultVisuals(plugin: PluginContext, data: { volume: StateObjectSelector<PluginStateObject.Volume.Data> }) {
+    const visual = plugin.build().to(data.volume).apply(StateTransforms.Representation.VolumeRepresentation3D);
+    await visual.commit();
+    return [visual];
+}
+
+export const Ccp4Provider = DataFormatProvider({
+    label: 'CCP4/MRC/BRIX',
+    description: 'CCP4/MRC/BRIX',
+    category: Category,
+    binaryExtensions: ['ccp4', 'mrc', 'map'],
+    parse: async (plugin, data) => {
+        const format = plugin.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParseCcp4, {}, { state: { isGhost: true } });
+
+        const volume = format.apply(StateTransforms.Volume.VolumeFromCcp4);
+
+        await format.commit({ revertOnError: true });
+
+        return { format: format.selector, volume: volume.selector };
+    },
+    visuals: defaultVisuals
+});
+
+export const Dsn6Provider = DataFormatProvider({
+    label: 'DSN6/BRIX',
+    description: 'DSN6/BRIX',
+    category: Category,
+    binaryExtensions: ['dsn6', 'brix'],
+    parse: async (plugin, data) => {
+        const format = plugin.build()
+            .to(data)
+            .apply(StateTransforms.Data.ParseDsn6, {}, { state: { isGhost: true } });
+
+        const volume = format.apply(StateTransforms.Volume.VolumeFromDsn6);
+
+        await format.commit({ revertOnError: true });
+
+        return { format: format.selector, volume: volume.selector };
+    },
+    visuals: defaultVisuals
+});
+
+export const DscifProvider = DataFormatProvider({
+    label: 'DensityServer CIF',
+    description: 'DensityServer CIF',
+    category: Category,
+    stringExtensions: ['cif'],
+    binaryExtensions: ['bcif'],
+    isApplicable: (info, data) => {
+        return guessCifVariant(info, data) === 'dscif' ? true : false
+    },
+    parse: async (plugin, data) => {
+        const cifCell = await plugin.build().to(data).apply(StateTransforms.Data.ParseCif).commit()
+        const b = plugin.build().to(cifCell);
+        const blocks = cifCell.obj!.data.blocks.slice(1); // zero block contains query meta-data
+
+        if (blocks.length !== 1 && blocks.length !== 2) throw new Error('unknown number of blocks')
+
+        const volumes: StateObjectSelector<PluginStateObject.Volume.Data>[] = [];
+        for (const block of blocks) {
+            volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header }).selector);
+        }
+
+        await b.commit();
+
+        return { volumes };
+    },
+    visuals: async (plugin, data: { volumes: StateObjectSelector<PluginStateObject.Volume.Data>[] }) => {
+        const { volumes } = data;
+        const tree = plugin.build();
+        const visuals: StateObjectSelector<PluginStateObject.Volume.Representation3D>[] = [];
+
+        if (volumes.length > 0) {
+            visuals[0] = tree
+                .to(volumes[0])
+                .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
+                .selector;
+        }
+
+        if (volumes.length > 1) {
+            const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
+            const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })
+            visuals[visuals.length] = tree.to(volumes[1]).apply(StateTransforms.Representation.VolumeRepresentation3D, posParams).selector;
+            visuals[visuals.length] = tree.to(volumes[1]).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams).selector;
+        }
+
+        await tree.commit();
+
+        return visuals;
+    }
+});
+
+export const BuiltInVolumeFormats = [
+    ['ccp4', Ccp4Provider] as const,
+    ['dns6', Dsn6Provider] as const,
+    ['dscif', DscifProvider] as const,
+] as const
+
+export type BuildInVolumeFormat = (typeof BuiltInVolumeFormats)[number][0]

+ 8 - 11
src/mol-plugin/context.ts

@@ -11,10 +11,8 @@ import { merge } from 'rxjs';
 import { Canvas3D, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
 import { Model, Structure } from '../mol-model/structure';
-import { DataFormatRegistry } from '../mol-plugin-state/actions/data-format';
 import { DataBuilder } from '../mol-plugin-state/builder/data';
 import { StructureBuilder } from '../mol-plugin-state/builder/structure';
-import { TrajectoryFormatRegistry } from '../mol-plugin-state/formats/trajectory';
 import { StructureSelectionQueryRegistry } from '../mol-plugin-state/helpers/structure-selection-query';
 import { CameraManager } from '../mol-plugin-state/manager/camera';
 import { InteractivityManager } from '../mol-plugin-state/manager/interactivity';
@@ -54,8 +52,11 @@ import { TaskManager } from './util/task-manager';
 import { PluginToastManager } from './util/toast';
 import { ViewportScreenshotHelper } from './util/viewport-screenshot';
 import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
+import { DataFormatRegistry } from '../mol-plugin-state/formats/registry';
 
 export class PluginContext {
+    runTask = <T>(task: Task<T>) => this.tasks.run(task);
+
     private disposed = false;
     private ev = RxEventHelper.create();
     private tasks = new TaskManager();
@@ -127,17 +128,17 @@ export class PluginContext {
         }
     } as const;
 
-    readonly dataFormat = {
-        trajectory: TrajectoryFormatRegistry(),
-        // TODO: separate registries for format catgories
-        registry: new DataFormatRegistry()
-    } as const
+    readonly dataFormats = new DataFormatRegistry();
 
     readonly builders = {
         data: new DataBuilder(this),
         structure: void 0 as any as StructureBuilder
     };
 
+    build() {
+        return this.state.data.build();
+    }
+
     readonly managers = {
         structure: {
             hierarchy: new StructureHierarchyManager(this),
@@ -218,10 +219,6 @@ export class PluginContext {
         this.behaviors.interaction.selectionMode.next(mode);
     }
 
-    runTask<T>(task: Task<T>) {
-        return this.tasks.run(task);
-    }
-
     dataTransaction(f: () => Promise<void> | void, options?: { canUndo?: string | boolean }) {
         return this.runTask(this.state.data.transaction(f, options));
     }

+ 2 - 2
src/mol-plugin/state.ts

@@ -108,8 +108,8 @@ class PluginState {
 
     constructor(private plugin: import('./context').PluginContext) {
         this.snapshots = new PluginStateSnapshotManager(plugin);
-        this.data = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviors = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootState: { isLocked: true } });
+        this.data = State.create(new SO.Root({ }), { runTask: plugin.runTask, globalContext: plugin });
+        this.behaviors = State.create(new PluginBehavior.Root({ }), { runTask: plugin.runTask, globalContext: plugin, rootState: { isLocked: true } });
 
         this.animation = new PluginAnimationManager(plugin);
     }

+ 1 - 1
src/mol-state/action.ts

@@ -78,7 +78,7 @@ namespace StateAction {
                 : void 0,
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
-                return state.updateTree(tree) as Task<void>;
+                return state.updateTree(tree) as unknown as Task<void>;
             }
         })
     }

+ 24 - 14
src/mol-state/state.ts

@@ -58,6 +58,8 @@ class State {
 
     readonly actions = new StateActionManager();
 
+    readonly runTask: <T>(task: Task<T>) => Promise<T>;
+
     get tree(): StateTree { return this._tree; }
     get transforms() { return (this._tree as StateTree).transforms; }
     get current() { return this.behaviors.currentObject.value.ref; }
@@ -232,7 +234,7 @@ class State {
      * @param tree Tree instance or a tree builder instance
      * @param doNotReportTiming Indicates whether to log timing of the individual transforms
      */
-    updateTree<T extends StateObject>(tree: StateBuilder.To<T, any>, options?: Partial<State.UpdateOptions>): Task<T>
+    updateTree<T extends StateObject>(tree: StateBuilder.To<T, any>, options?: Partial<State.UpdateOptions>): Task<StateObjectCell<T>>
     updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void>
     updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> {
         const params: UpdateParams = { tree, options };
@@ -294,7 +296,7 @@ class State {
             updated = await update(ctx);
             if (StateBuilder.isTo(params.tree)) {
                 const cell = this.select(params.tree.ref)[0];
-                return { ctx, cell: cell && cell.obj };
+                return { ctx, cell };
             }
             return { ctx };
         } finally {
@@ -335,10 +337,11 @@ class State {
         return ctx;
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State, historyCapacity?: number }) {
+    constructor(rootObject: StateObject, params: State.Params) {
         this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient();
         const tree = this._tree;
         const root = tree.root;
+        this.runTask = params.runTask;
 
         if (params?.historyCapacity !== void 0) this.historyCapacity = params.historyCapacity;
 
@@ -363,6 +366,17 @@ class State {
 }
 
 namespace State {
+    export interface Params {
+        runTask<T>(task: Task<T>): Promise<T>,
+        globalContext?: unknown,
+        rootState?: StateTransform.State,
+        historyCapacity?: number
+    }
+
+    export function create(rootObject: StateObject, params: Params) {
+        return new State(rootObject, params);
+    }
+
     export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell>
 
     export type Tree = StateTree
@@ -390,10 +404,6 @@ namespace State {
         revertOnError: boolean,
         canUndo: boolean | string
     }
-
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
-        return new State(rootObject, params);
-    }
 }
 
 const StateUpdateDefaultOptions: State.UpdateOptions = {
@@ -885,12 +895,7 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
     return t as T;
 }
 
-function createObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, params: any) {
-    if (!cell.cache) cell.cache = Object.create(null);
-    return runTask(transformer.definition.apply({ a, params, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(ctx, cell) }, ctx.parent.globalContext), ctx.taskCtx);
-}
-
-function resolveDependencies(ctx: UpdateContext, cell: StateObjectCell) {
+function resolveDependencies(cell: StateObjectCell) {
     if (cell.dependencies.dependsOn.length === 0) return void 0;
 
     const deps = Object.create(null);
@@ -905,10 +910,15 @@ function resolveDependencies(ctx: UpdateContext, cell: StateObjectCell) {
     return deps;
 }
 
+function createObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, params: any) {
+    if (!cell.cache) cell.cache = Object.create(null);
+    return runTask(transformer.definition.apply({ a, params, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(cell) }, ctx.parent.globalContext), ctx.taskCtx);
+}
+
 async function updateObject(ctx: UpdateContext, cell: StateObjectCell,  transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
     if (!transformer.definition.update) {
         return StateTransformer.UpdateResult.Recreate;
     }
     if (!cell.cache) cell.cache = Object.create(null);
-    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(ctx, cell) }, ctx.parent.globalContext), ctx.taskCtx);
+    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(cell) }, ctx.parent.globalContext), ctx.taskCtx);
 }

+ 11 - 0
src/mol-state/state/builder.ts

@@ -102,6 +102,12 @@ namespace StateBuilder {
             return this;
         }
         getTree(): StateTree { return buildTree(this.state); }
+
+        commit(options?: Partial<State.UpdateOptions>) {
+            if (!this.state.state) throw new Error('Cannot commit template tree');
+            return this.state.state.runTask(this.state.state.updateTree(this, options));
+        }
+
         constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
     }
 
@@ -252,6 +258,11 @@ namespace StateBuilder {
 
         getTree(): StateTree { return buildTree(this.state); }
 
+        commit(options?: Partial<State.UpdateOptions>): Promise<StateObjectCell<A>> {
+            if (!this.state.state) throw new Error('Cannot commit template tree');
+            return this.state.state.runTask(this.state.state.updateTree(this, options));
+        }
+
         constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) {
             this.ref = ref;
             if (!this.state.tree.transforms.has(ref)) {