Browse Source

Merge branch 'master' of https://github.com/molstar/molstar

DESKTOP-O6LIMN9\ludov 5 năm trước cách đây
mục cha
commit
0cd8b4ee8c
84 tập tin đã thay đổi với 1432 bổ sung881 xóa
  1. 13 7
      .eslintrc.json
  2. 18 2
      data/cif-field-names/cif-core-field-names.csv
  3. 1 1
      package.json
  4. 1 1
      src/apps/cifschema/index.ts
  5. 32 3
      src/apps/cifschema/util/cif-dic.ts
  6. 15 10
      src/apps/viewer/extensions/cellpack/model.ts
  7. 2 2
      src/apps/viewer/extensions/cellpack/preset.ts
  8. 5 3
      src/apps/viewer/extensions/cellpack/state.ts
  9. 2 3
      src/examples/proteopedia-wrapper/ui/controls.tsx
  10. 2 4
      src/mol-data/generic/linked-list.ts
  11. 19 8
      src/mol-data/int/impl/ordered-set.ts
  12. 58 26
      src/mol-data/int/impl/sorted-array.ts
  13. 5 10
      src/mol-io/common/msgpack/encode.ts
  14. 7 10
      src/mol-io/common/utf8.ts
  15. 27 6
      src/mol-io/reader/cif/data-model.ts
  16. 14 11
      src/mol-io/reader/cif/schema.ts
  17. 158 0
      src/mol-io/reader/cif/schema/cif-core.ts
  18. 4 2
      src/mol-io/reader/common/text/number-parser.ts
  19. 1 2
      src/mol-io/writer/cif/encoder/binary.ts
  20. 5 2
      src/mol-math/graph/int-adjacency-graph.ts
  21. 6 2
      src/mol-math/linear-algebra/3d/mat4.ts
  22. 1 2
      src/mol-math/linear-algebra/matrix/evd.ts
  23. 30 1
      src/mol-model-formats/structure/_spec/cif-core.spec.ts
  24. 201 0
      src/mol-model-formats/structure/cif-core.ts
  25. 14 2
      src/mol-model-formats/structure/common/property.ts
  26. 8 4
      src/mol-model-formats/structure/mmcif.ts
  27. 8 7
      src/mol-model-formats/structure/mol.ts
  28. 31 9
      src/mol-model-formats/structure/property/anisotropic.ts
  29. 20 7
      src/mol-model-formats/structure/property/bonds/index-pair.ts
  30. 3 3
      src/mol-model-formats/structure/property/symmetry.ts
  31. 1 0
      src/mol-model-props/rcsb/validation-report.ts
  32. 8 12
      src/mol-model/structure/model/model.ts
  33. 10 4
      src/mol-model/structure/query/utils/structure-set.ts
  34. 10 2
      src/mol-model/structure/structure/unit/bonds/inter-compute.ts
  35. 6 2
      src/mol-model/structure/structure/unit/bonds/intra-compute.ts
  36. 3 2
      src/mol-model/structure/structure/util/subset-builder.ts
  37. 1 2
      src/mol-model/structure/structure/util/unique-subset-builder.ts
  38. 1 1
      src/mol-plugin-state/actions.ts
  39. 0 170
      src/mol-plugin-state/actions/data-format.ts
  40. 58 0
      src/mol-plugin-state/actions/file.ts
  41. 0 33
      src/mol-plugin-state/actions/shape.ts
  42. 5 131
      src/mol-plugin-state/actions/structure.ts
  43. 18 100
      src/mol-plugin-state/actions/volume.ts
  44. 7 7
      src/mol-plugin-state/animation/built-in.ts
  45. 9 13
      src/mol-plugin-state/builder/data.ts
  46. 18 25
      src/mol-plugin-state/builder/structure.ts
  47. 6 5
      src/mol-plugin-state/builder/structure/representation-preset.ts
  48. 1 1
      src/mol-plugin-state/builder/structure/representation.ts
  49. 41 0
      src/mol-plugin-state/formats/provider.ts
  50. 26 40
      src/mol-plugin-state/formats/registry.ts
  51. 35 0
      src/mol-plugin-state/formats/shape.ts
  52. 49 0
      src/mol-plugin-state/formats/structure.ts
  53. 61 35
      src/mol-plugin-state/formats/trajectory.ts
  54. 118 0
      src/mol-plugin-state/formats/volume.ts
  55. 2 2
      src/mol-plugin-state/helpers/structure-overpaint.ts
  56. 17 12
      src/mol-plugin-state/manager/structure/component.ts
  57. 2 2
      src/mol-plugin-state/manager/structure/hierarchy.ts
  58. 2 1
      src/mol-plugin-state/snapshots.ts
  59. 8 4
      src/mol-plugin-state/transforms/data.ts
  60. 33 0
      src/mol-plugin-state/transforms/model.ts
  61. 26 30
      src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx
  62. 3 3
      src/mol-plugin-ui/controls/slider.tsx
  63. 2 3
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
  64. 1 1
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/ui/assembly-symmetry.tsx
  65. 1 1
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts
  66. 1 1
      src/mol-plugin/behavior/dynamic/representation.ts
  67. 1 1
      src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
  68. 10 17
      src/mol-plugin/context.ts
  69. 1 0
      src/mol-plugin/index.ts
  70. 2 2
      src/mol-plugin/state.ts
  71. 2 2
      src/mol-repr/structure/representation/ellipsoid.ts
  72. 30 20
      src/mol-script/runtime/query/table.ts
  73. 1 1
      src/mol-state/action.ts
  74. 33 16
      src/mol-state/state.ts
  75. 14 1
      src/mol-state/state/builder.ts
  76. 6 6
      src/mol-theme/label.ts
  77. 3 2
      src/mol-util/make-dir.ts
  78. 4 4
      src/mol-util/param-definition.ts
  79. 1 2
      src/mol-util/param-mapping.ts
  80. 11 5
      src/mol-util/zip/bin.ts
  81. 4 2
      src/mol-util/zip/deflate.ts
  82. 2 1
      src/mol-util/zip/zip.ts
  83. 3 2
      src/servers/volume/common/file.ts
  84. 3 2
      src/servers/volume/server/local-api.ts

+ 13 - 7
.eslintrc.json

@@ -14,8 +14,9 @@
     "rules": {
         "@typescript-eslint/ban-types": "warn",
         "@typescript-eslint/class-name-casing": "off",
+        "indent": "off",
         "@typescript-eslint/indent": [
-            "warn",
+            "error",
             4
         ],
         "@typescript-eslint/member-delimiter-style": [
@@ -33,7 +34,7 @@
         ],
         "@typescript-eslint/prefer-namespace-keyword": "warn",
         "@typescript-eslint/quotes": [
-            "warn",
+            "error",
             "single",
             {
                 "avoidEscape": true,
@@ -44,22 +45,27 @@
             "off",
             null
         ],
-        "@typescript-eslint/type-annotation-spacing": "warn",
+        "@typescript-eslint/type-annotation-spacing": "error",
         "arrow-parens": [
             "off",
             "as-needed"
         ],
+        "brace-style": "off",
+        "@typescript-eslint/brace-style": [
+            "error",
+            "1tbs", { "allowSingleLine": true }
+        ],
         "comma-dangle": "off",
         "eqeqeq": [
-            "warn",
+            "error",
             "smart"
         ],
         "import/order": "off",
         "no-eval": "warn",
         "no-new-wrappers": "warn",
-        "no-trailing-spaces": "warn",
+        "no-trailing-spaces": "error",
         "no-unsafe-finally": "warn",
-        "no-var": "warn",
-        "spaced-comment": "warn"
+        "no-var": "error",
+        "spaced-comment": "error"
     }
 }

+ 18 - 2
data/cif-field-names/cif-core-field-names.csv

@@ -1,6 +1,12 @@
 audit.block_doi
 
 database_code.depnum_ccdc_archive
+database_code.depnum_ccdc_fiz
+database_code.ICSD
+database_code.MDF
+database_code.NBS
+database_code.CSD
+database_code.COD
 
 chemical.name_systematic
 chemical.name_common
@@ -19,6 +25,7 @@ atom_type_scat.source
 
 space_group.crystal_system
 space_group.name_H-M_full
+space_group.IT_number
 space_group_symop.operation_xyz
 
 cell.length_a
@@ -42,19 +49,28 @@ atom_site.calc_flag
 atom_site.refinement_flags
 atom_site.disorder_assembly
 atom_site.disorder_group
-
 atom_site.site_symmetry_multiplicity
 
 atom_site_aniso.label
+atom_site_aniso.U
 atom_site_aniso.U_11
 atom_site_aniso.U_22
 atom_site_aniso.U_33
 atom_site_aniso.U_23
 atom_site_aniso.U_13
 atom_site_aniso.U_12
+atom_site_aniso.U_su
+atom_site_aniso.U_11_su
+atom_site_aniso.U_22_su
+atom_site_aniso.U_33_su
+atom_site_aniso.U_23_su
+atom_site_aniso.U_13_su
+atom_site_aniso.U_12_su
 
 geom_bond.atom_site_label_1
 geom_bond.atom_site_label_2
 geom_bond.distance
+geom_bond.site_symmetry_1
 geom_bond.site_symmetry_2
-geom_bond.publ_flag
+geom_bond.publ_flag
+geom_bond.valence

+ 1 - 1
package.json

@@ -11,7 +11,7 @@
     "url": "https://github.com/molstar/molstar/issues"
   },
   "scripts": {
-    "lint": "eslint src/**/*.ts",
+    "lint": "eslint src/**/*.{ts,tsx}",
     "test": "npm run lint && jest",
     "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
     "build-tsc": "tsc --incremental",

+ 1 - 1
src/apps/cifschema/index.ts

@@ -159,7 +159,7 @@ async function ensureDicAvailable(dicPath: string, dicUrl: string) {
     }
 }
 
-const DIC_DIR = path.resolve(__dirname, '../dics/')
+const DIC_DIR = path.resolve(__dirname, '../../../build/dics/')
 const MMCIF_DIC_PATH = `${DIC_DIR}/mmcif_pdbx_v50.dic`
 const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.dic'
 const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`

+ 32 - 3
src/apps/cifschema/util/cif-dic.ts

@@ -232,6 +232,32 @@ const FORCE_INT_FIELDS = [
     '_struct_sheet_range.end_auth_seq_id',
 ];
 
+const FORCE_MATRIX_FIELDS_MAP: { [k: string]: string } = {
+    'atom_site_aniso.U_11': 'U',
+    'atom_site_aniso.U_22': 'U',
+    'atom_site_aniso.U_33': 'U',
+    'atom_site_aniso.U_23': 'U',
+    'atom_site_aniso.U_13': 'U',
+    'atom_site_aniso.U_12': 'U',
+    'atom_site_aniso.U_11_su': 'U_su',
+    'atom_site_aniso.U_22_su': 'U_su',
+    'atom_site_aniso.U_33_su': 'U_su',
+    'atom_site_aniso.U_23_su': 'U_su',
+    'atom_site_aniso.U_13_su': 'U_su',
+    'atom_site_aniso.U_12_su': 'U_su',
+}
+const FORCE_MATRIX_FIELDS = Object.keys(FORCE_MATRIX_FIELDS_MAP)
+
+const EXTRA_ALIASES: Database['aliases'] = {
+    'atom_site_aniso.U': [
+        'atom_site_anisotrop_U'
+    ],
+    'atom_site_aniso.U_su': [
+        'atom_site_aniso_U_esd',
+        'atom_site_anisotrop_U_esd',
+    ],
+}
+
 const COMMA_SEPARATED_LIST_FIELDS = [
     '_atom_site.pdbx_struct_group_id',
     '_chem_comp.mon_nstd_parent_comp_id',
@@ -280,9 +306,8 @@ const EXTRA_ENUM_VALUES: { [k: string]: string[] } = {
 }
 
 export function generateSchema(frames: CifFrame[], imports: Imports = new Map()): Database {
-
     const tables: Database['tables'] = {}
-    const aliases: Database['aliases'] = {}
+    const aliases: Database['aliases'] = { ...EXTRA_ALIASES }
 
     const categories: FrameCategories = {}
     const links: FrameLinks = {}
@@ -291,7 +316,7 @@ export function generateSchema(frames: CifFrame[], imports: Imports = new Map())
     // get category metadata
     frames.forEach(d => {
         // category definitions in mmCIF start with '_' and don't include a '.'
-        // category definitions in cif don't include a '.'
+        // category definitions in cifCore don't include a '.'
         if (d.header[0] === '_'  || d.header.includes('.')) return
         const categoryName = d.header.toLowerCase()
         // console.log(d.header, d.categoryNames, d.categories)
@@ -399,6 +424,10 @@ export function generateSchema(frames: CifFrame[], imports: Imports = new Map())
         } else if (FORCE_INT_FIELDS.includes(d.header)) {
             fields[itemName] = IntCol(description)
             console.log(`forcing int: ${d.header}`)
+        } else if (FORCE_MATRIX_FIELDS.includes(d.header)) {
+            fields[itemName] = FloatCol(description)
+            fields[FORCE_MATRIX_FIELDS_MAP[d.header]] = MatrixCol(3, 3, description)
+            console.log(`forcing matrix: ${d.header}`)
         } else if (subCategory === 'matrix') {
             fields[itemName.replace(reMatrixField, '')] = MatrixCol(3, 3, description)
         } else if (subCategory === 'vector') {

+ 15 - 10
src/apps/viewer/extensions/cellpack/model.ts

@@ -321,20 +321,19 @@ async function handleHivRna(ctx: { runtime: RuntimeContext, fetch: AjaxTask }, p
 
 async function loadHivMembrane(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
     const url = `${params.baseUrl}/membranes/hiv_lipids.bcif`
-    const membraneBuilder = state.build().toRoot()
+    const membrane = await state.build().toRoot()
         .apply(StateTransforms.Data.Download, { label: 'hiv_lipids', url, isBinary: true }, { state: { isGhost: true } })
         .apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
         .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
         .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
         .apply(StateTransforms.Model.StructureFromModel)
-    await state.updateTree(membraneBuilder).runInContext(runtime)
+        .commit()
 
     const membraneParams = {
         representation: params.preset.representation,
     }
-    const membrane = state.build().to(membraneBuilder.ref)
-    await plugin.updateDataState(membrane, { revertOnError: true });
-    await CellpackMembranePreset.apply(membrane.selector, membraneParams, plugin)
+
+    await CellpackMembranePreset.apply(membrane, membraneParams, plugin)
 }
 
 async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
@@ -345,6 +344,10 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
             .apply(StateTransforms.Data.Download, { url, isBinary: false, label: params.source.params }, { state: { isGhost: true } })
     } else {
         const file = params.source.params
+        if (file === null) {
+            plugin.log.error('No file selected')
+            return
+        }
         cellPackJson = state.build().toRoot()
             .apply(StateTransforms.Data.ReadFile, { file, isBinary: false, label: file.name }, { state: { isGhost: true } })
     }
@@ -354,17 +357,19 @@ 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)
 
     for (let i = 0, il = packings.length; i < il; ++i) {
         const p = { packing: i, baseUrl: params.baseUrl, ingredientFiles: params.ingredients.files }
 
-        const packing = state.build().to(cellPackBuilder.ref).apply(StructureFromCellpack, p)
-        await plugin.updateDataState(packing, { revertOnError: true });
+        const packing = await state.build()
+            .to(cellPackBuilder.ref)
+            .apply(StructureFromCellpack, p)
+            .commit({ revertOnError: true })
 
-        const structure = packing.selector.obj?.data
+        const structure = packing.obj?.data
         if (structure) {
             await CellPackInfoProvider.attach({ fetch: plugin.fetch, runtime }, structure, {
                 info: { packingsCount: packings.length, packingIndex: i }
@@ -375,7 +380,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
             traceOnly: params.preset.traceOnly,
             representation: params.preset.representation,
         }
-        await CellpackPackingPreset.apply(packing.selector, packingParams, plugin)
+        await CellpackPackingPreset.apply(packing, packingParams, plugin)
     }
 }
 

+ 2 - 2
src/apps/viewer/extensions/cellpack/preset.ts

@@ -46,7 +46,7 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
             polymer: builder.buildRepresentation<any>(update, components.polymer, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color }, { tag: 'polymer' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true })
         return { components, representations };
     }
 });
@@ -84,7 +84,7 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
             membrane: builder.buildRepresentation(update, components.membrane, { type: 'gaussian-surface', typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true })
         return { components, representations };
     }
 });

+ 5 - 3
src/apps/viewer/extensions/cellpack/state.ts

@@ -70,9 +70,11 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
         return Task.create('Structure from CellPack', async ctx => {
             const packing = a.data.packings[params.packing]
             const ingredientFiles: IngredientFiles = {}
-            for (let i = 0, il = params.ingredientFiles.length; i < il; ++i) {
-                const file = params.ingredientFiles.item(i)
-                if (file) ingredientFiles[file.name] = file
+            if (params.ingredientFiles !== null) {
+                for (let i = 0, il = params.ingredientFiles.length; i < il; ++i) {
+                    const file = params.ingredientFiles.item(i)
+                    if (file) ingredientFiles[file.name] = file
+                }
             }
             const structure = await createStructureFromCellPack(packing, params.baseUrl, ingredientFiles).runInContext(ctx)
             return new PSO.Molecule.Structure(structure, { label: packing.name })

+ 2 - 3
src/examples/proteopedia-wrapper/ui/controls.tsx

@@ -13,7 +13,6 @@ import { StateElements } from '../helpers';
 
 export function volumeStreamingControls(plugin: PluginContext, parent: Element) {
     ReactDOM.render(<PluginContextContainer plugin={plugin}>
-            <TransformUpdaterControl nodeRef={StateElements.VolumeStreaming} />
-        </PluginContextContainer>,
-        parent);
+        <TransformUpdaterControl nodeRef={StateElements.VolumeStreaming} />
+    </PluginContextContainer>, parent);
 }

+ 2 - 4
src/mol-data/generic/linked-list.ts

@@ -89,15 +89,13 @@ class LinkedListImpl<T> implements LinkedList<T> {
 
         if (node.previous !== null) {
             node.previous.next = node.next;
-        }
-        else if (/* first == item*/ node.previous === null) {
+        } else if (/* first == item*/ node.previous === null) {
             this.first = node.next;
         }
 
         if (node.next !== null) {
             node.next.previous = node.previous;
-        }
-        else if (/* last == item*/ node.next === null) {
+        } else if (/* last == item*/ node.next === null) {
             this.last = node.previous;
         }
 

+ 19 - 8
src/mol-data/int/impl/ordered-set.ts

@@ -150,8 +150,11 @@ function unionII(a: I, b: I) {
     const minA = I.min(a), minB = I.min(b);
     if (areRangesIntersecting(a, b)) return I.ofRange(Math.min(minA, minB), Math.max(I.max(a), I.max(b)));
     let lSize, lMin, rSize, rMin;
-    if (minA < minB) { lSize = sizeA; lMin = minA; rSize = sizeB; rMin = minB; }
-    else { lSize = sizeB; lMin = minB; rSize = sizeA; rMin = minA; }
+    if (minA < minB) {
+        lSize = sizeA; lMin = minA; rSize = sizeB; rMin = minB;
+    } else {
+        lSize = sizeB; lMin = minB; rSize = sizeA; rMin = minA;
+    }
     const arr = new Int32Array(sizeA + sizeB);
     for (let i = 0; i < lSize; i++) arr[i] = i + lMin;
     for (let i = 0; i < rSize; i++) arr[i + lSize] = i + rMin;
@@ -328,9 +331,13 @@ export function indexedIntersect(idxA: OrderedSetImpl, a: S, b: S): OrderedSetIm
     let j = startJ;
     while (O < lenI && j < endJ) {
         const x = a[getAt(idxA, O)], y = b[j];
-        if (x < y) { O++; }
-        else if (x > y) { j++; }
-        else { commonCount++; O++; j++; }
+        if (x < y) {
+            O++;
+        } else if (x > y) {
+            j++;
+        } else {
+            commonCount++; O++; j++;
+        }
     }
 
     // no common elements
@@ -345,9 +352,13 @@ export function indexedIntersect(idxA: OrderedSetImpl, a: S, b: S): OrderedSetIm
     j = startJ;
     while (O < lenI && j < endJ) {
         const x = a[getAt(idxA, O)], y = b[j];
-        if (x < y) { O++; }
-        else if (x > y) { j++; }
-        else { indices[offset++] = j; O++; j++; }
+        if (x < y) {
+            O++;
+        } else if (x > y) {
+            j++;
+        } else {
+            indices[offset++] = j; O++; j++;
+        }
     }
 
     return ofSortedArray(indices);

+ 58 - 26
src/mol-data/int/impl/sorted-array.ts

@@ -146,8 +146,8 @@ export function areIntersecting(a: Nums, b: Nums) {
     let { startI: i, startJ: j, endI, endJ } = getSuitableIntersectionRange(a, b);
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
+        if (x < y) i++;
+        else if (x > y) j++;
         else return true;
     }
     return false;
@@ -164,9 +164,13 @@ export function isSubset(a: Nums, b: Nums) {
     let equal = 0;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; equal++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            i++; j++; equal++;
+        }
     }
     return equal === lenB;
 }
@@ -197,9 +201,13 @@ export function union(a: Nums, b: Nums): Nums {
     // insert the common part
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { indices[offset++] = x; i++; }
-        else if (x > y) { indices[offset++] = y; j++; }
-        else { indices[offset++] = x; i++; j++; }
+        if (x < y) {
+            indices[offset++] = x; i++;
+        } else if (x > y) {
+            indices[offset++] = y; j++;
+        } else {
+            indices[offset++] = x; i++; j++;
+        }
     }
 
     // insert the remaining common part
@@ -224,9 +232,13 @@ function getCommonCount(a: Nums, b: Nums, startI: number, startJ: number, endI:
     let commonCount = 0;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; commonCount++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            i++; j++; commonCount++;
+        }
     }
     return commonCount;
 }
@@ -251,9 +263,13 @@ export function intersect(a: Nums, b: Nums) {
     let j = startJ;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { indices[offset++] = x; i++; j++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            indices[offset++] = x; i++; j++;
+        }
     }
 
     return ofSortedArray(indices);
@@ -268,9 +284,13 @@ export function subtract(a: Nums, b: Nums) {
     let commonCount = 0;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; commonCount++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            i++; j++; commonCount++;
+        }
     }
 
     // A isnt intersecting B ===> A
@@ -288,9 +308,13 @@ export function subtract(a: Nums, b: Nums) {
     j = sJ;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { indices[offset++] = x; i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; }
+        if (x < y) {
+            indices[offset++] = x; i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            i++; j++;
+        }
     }
 
     // insert the "tail"
@@ -323,9 +347,13 @@ export function indicesOf(a: Nums, b: Nums): Nums {
     let commonCount = 0;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; commonCount++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            i++; j++; commonCount++;
+        }
     }
 
     const lenA = a.length;
@@ -340,9 +368,13 @@ export function indicesOf(a: Nums, b: Nums): Nums {
     j = sJ;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { indices[offset++] = i; i++; j++; }
+        if (x < y) {
+            i++;
+        } else if (x > y) {
+            j++;
+        } else {
+            indices[offset++] = i; i++; j++;
+        }
     }
 
     return ofSortedArray(indices);

+ 5 - 10
src/mol-io/common/msgpack/encode.ts

@@ -88,8 +88,7 @@ function encodedSize(value: any) {
             for (let i = 0; i < length; i++) {
                 size += encodedSize(value[i]);
             }
-        }
-        else {
+        } else {
             let keys = Object.keys(value);
             length = keys.length;
             for (let i = 0; i < length; i++) {
@@ -257,8 +256,7 @@ function encodeInternal(value: any, view: DataView, bytes: Uint8Array, offset: n
 
         if (isArray) {
             length = value.length;
-        }
-        else {
+        } else {
             keys = Object.keys(value);
             length = keys.length;
         }
@@ -266,13 +264,11 @@ function encodeInternal(value: any, view: DataView, bytes: Uint8Array, offset: n
         if (length < 0x10) {
             view.setUint8(offset, length | (isArray ? 0x90 : 0x80));
             size = 1;
-        }
-        else if (length < 0x10000) {
+        } else if (length < 0x10000) {
             view.setUint8(offset, isArray ? 0xdc : 0xde);
             view.setUint16(offset + 1, length);
             size = 3;
-        }
-        else if (length < 0x100000000) {
+        } else if (length < 0x100000000) {
             view.setUint8(offset, isArray ? 0xdd : 0xdf);
             view.setUint32(offset + 1, length);
             size = 5;
@@ -282,8 +278,7 @@ function encodeInternal(value: any, view: DataView, bytes: Uint8Array, offset: n
             for (let i = 0; i < length; i++) {
                 size += encodeInternal(value[i], view, bytes, offset + size);
             }
-        }
-        else {
+        } else {
             for (let i = 0, _i = keys!.length; i < _i; i++) {
                 const key = keys![i];
                 size += encodeInternal(key, view, bytes, offset + size);

+ 7 - 10
src/mol-io/common/utf8.ts

@@ -59,24 +59,21 @@ function _utf8Read(data: Uint8Array, offset: number, length: number) {
 
     for (let i = offset, end = offset + length; i < end; i++) {
         let byte = data[i];
-        // One byte character
         if ((byte & 0x80) === 0x00) {
+            // One byte character
             chunk[chunkOffset++] = chars[byte];
-        }
-        // Two byte character
-        else if ((byte & 0xe0) === 0xc0) {
+        } else if ((byte & 0xe0) === 0xc0) {
+            // Two byte character
             chunk[chunkOffset++] = chars[((byte & 0x0f) << 6) | (data[++i] & 0x3f)];
-        }
-        // Three byte character
-        else if ((byte & 0xf0) === 0xe0) {
+        } else if ((byte & 0xf0) === 0xe0) {
+            // Three byte character
             chunk[chunkOffset++] = String.fromCharCode(
                 ((byte & 0x0f) << 12) |
                 ((data[++i] & 0x3f) << 6) |
                 ((data[++i] & 0x3f) << 0)
             );
-        }
-        // Four byte character
-        else if ((byte & 0xf8) === 0xf0) {
+        } else if ((byte & 0xf8) === 0xf0) {
+            // Four byte character
             chunk[chunkOffset++] = String.fromCharCode(
                 ((byte & 0x07) << 18) |
                 ((data[++i] & 0x3f) << 12) |

+ 27 - 6
src/mol-io/reader/cif/data-model.ts

@@ -267,21 +267,40 @@ export namespace CifField {
     }
 }
 
-export function getTensor(category: CifCategory, field: string, space: Tensor.Space, row: number, zeroIndexed: boolean): Tensor.Data {
-    const ret = space.create();
+export function tensorFieldNameGetter(field: string, rank: number, zeroIndexed: boolean, namingVariant: 'brackets' | 'underscore') {
     const offset = zeroIndexed ? 0 : 1;
+    switch (rank) {
+        case 1:
+            return namingVariant === 'brackets'
+                ? (i: number) => `${field}[${i + offset}]`
+                : (i: number) => `${field}_${i + offset}`
+        case 2:
+            return namingVariant === 'brackets'
+                ? (i: number, j: number) => `${field}[${i + offset}][${j + offset}]`
+                : (i: number, j: number) => `${field}_${i + offset}${j + offset}`
+        case 3:
+            return namingVariant === 'brackets'
+                ? (i: number, j: number, k: number) => `${field}[${i + offset}][${j + offset}][${k + offset}]`
+                : (i: number, j: number, k: number) => `${field}_${i + offset}${j + offset}${k + offset}`
+        default:
+            throw new Error('Tensors with rank > 3 or rank 0 are currently not supported.');
+    }
+}
+
+export function getTensor(category: CifCategory, space: Tensor.Space, row: number, getName: (...args: number[]) => string): Tensor.Data {
+    const ret = space.create();
 
     if (space.rank === 1) {
         const rows = space.dimensions[0];
         for (let i = 0; i < rows; i++) {
-            const f = category.getField(`${field}[${i + offset}]`);
+            const f = category.getField(getName(i));
             space.set(ret, i, !!f ? f.float(row) : 0.0);
         }
     } else if (space.rank === 2) {
         const rows = space.dimensions[0], cols = space.dimensions[1];
         for (let i = 0; i < rows; i++) {
             for (let j = 0; j < cols; j++) {
-                const f = category.getField(`${field}[${i + offset}][${j + offset}]`);
+                const f = category.getField(getName(i, j));
                 space.set(ret, i, j, !!f ? f.float(row) : 0.0);
             }
         }
@@ -290,12 +309,14 @@ export function getTensor(category: CifCategory, field: string, space: Tensor.Sp
         for (let i = 0; i < d0; i++) {
             for (let j = 0; j < d1; j++) {
                 for (let k = 0; k < d2; k++) {
-                    const f = category.getField(`${field}[${i + offset}][${j + offset}][${k + offset}]`);
+                    const f = category.getField(getName(i, j, k));
                     space.set(ret, i, j, k, !!f ? f.float(row) : 0.0);
                 }
             }
         }
-    } else throw new Error('Tensors with rank > 3 or rank 0 are currently not supported.');
+    } else {
+        throw new Error('Tensors with rank > 3 or rank 0 are currently not supported.');
+    }
     return ret;
 }
 

+ 14 - 11
src/mol-io/reader/cif/schema.ts

@@ -88,18 +88,21 @@ function createListColumn<T extends number | string>(schema: Column.Schema.List<
 
 function createTensorColumn(schema: Column.Schema.Tensor, category: Data.CifCategory, key: string): Column<Tensor.Data> {
     const space = schema.space;
-    const zeroOffset = category.fieldNames.indexOf(`${key}[0]`) >= 0;
+    const zeroOffset = (
+        category.fieldNames.includes(`${key}[0]`) ||
+        category.fieldNames.includes(`${key}[0][0]`) ||
+        category.fieldNames.includes(`${key}[0][0][0]`)
+    );
     const fst = zeroOffset ? 0 : 1;
-
-    let firstFieldName: string;
-    switch (space.rank) {
-        case 1: firstFieldName = `${key}[${fst}]`; break;
-        case 2: firstFieldName = `${key}[${fst}][${fst}]`; break;
-        case 3: firstFieldName = `${key}[${fst}][${fst}][${fst}]`; break;
-        default: throw new Error('Tensors with rank > 3 or rank 0 are currently not supported.');
-    }
-    const first = category.getField(firstFieldName) || Column.Undefined(category.rowCount, schema);
-    const value = (row: number) => Data.getTensor(category, key, space, row, zeroOffset);
+    const namingVariant = (
+        category.fieldNames.includes(`${key}_1`) ||
+        category.fieldNames.includes(`${key}_11`) ||
+        category.fieldNames.includes(`${key}_111`)
+    ) ? 'underscore' : 'brackets'
+
+    const getName = Data.tensorFieldNameGetter(key, space.rank, zeroOffset, namingVariant)
+    const first = category.getField(getName(fst, fst, fst)) || Column.Undefined(category.rowCount, schema);
+    const value = (row: number) => Data.getTensor(category, space, row, getName);
     const toArray: Column<Tensor.Data>['toArray'] = params => ColumnHelpers.createAndFillArray(category.rowCount, value, params)
 
     return {

+ 158 - 0
src/mol-io/reader/cif/schema/cif-core.ts

@@ -13,6 +13,7 @@ import Schema = Column.Schema
 const int = Schema.int;
 const float = Schema.float;
 const str = Schema.str;
+const Matrix = Schema.Matrix;
 
 export const CifCore_Schema = {
     /**
@@ -181,6 +182,14 @@ export const CifCore_Schema = {
          * trigonal system.
          */
         crystal_system: str,
+        /**
+         * The number as assigned in International Tables for Crystallography
+         * Vol A, specifying the proper affine class (i.e. the orientation
+         * preserving affine class) of space groups (crystallographic space
+         * group type) to which the space group belongs. This number defines
+         * the space group type but not the coordinate system expressed.
+         */
+        IT_number: int,
         /**
          * The full international Hermann-Mauguin space-group symbol as
          * defined in Section 2.2.3 and given as the second item of the
@@ -272,6 +281,29 @@ export const CifCore_Schema = {
          * publication or should be placed in a table of significant angles.
          */
         publ_flag: str,
+        /**
+         * The set of data items which specify the symmetry operation codes
+         * which must be applied to the atom sites involved in the geometry angle.
+         *
+         * The symmetry code of each atom site as the symmetry-equivalent position
+         * number 'n' and the cell translation number 'pqr'. These numbers are
+         * combined to form the code 'n pqr' or n_pqr.
+         *
+         * The character string n_pqr is composed as follows:
+         *
+         * n refers to the symmetry operation that is applied to the
+         * coordinates stored in _atom_site.fract_xyz. It must match a
+         * number given in _symmetry_equiv.pos_site_id.
+         *
+         * p, q and r refer to the translations that are subsequently
+         * applied to the symmetry transformed coordinates to generate
+         * the atom used in calculating the angle. These translations
+         * (x,y,z) are related to (p,q,r) by the relations
+         * p = 5 + x
+         * q = 5 + y
+         * r = 5 + z
+         */
+        site_symmetry_1: str,
         /**
          * The set of data items which specify the symmetry operation codes
          * which must be applied to the atom sites involved in the geometry angle.
@@ -295,6 +327,10 @@ export const CifCore_Schema = {
          * r = 5 + z
          */
         site_symmetry_2: str,
+        /**
+         * Bond valence calculated from the bond distance.
+         */
+        valence: float,
     },
     /**
      * The CATEGORY of data items used to record details about the
@@ -329,12 +365,38 @@ export const CifCore_Schema = {
      * originate from that source.
      */
     database_code: {
+        /**
+         * Code assigned by Crystallography Open Database (COD).
+         */
+        COD: str,
+        /**
+         * Code assigned by the Cambridge Structural Database.
+         */
+        CSD: str,
         /**
          * Deposition numbers assigned by the Cambridge Crystallographic
          * Data Centre (CCDC) to files containing structural information
          * archived by the CCDC.
          */
         depnum_ccdc_archive: str,
+        /**
+         * Deposition numbers assigned by the Fachinformationszentrum
+         * Karlsruhe (FIZ) to files containing structural information
+         * archived by the Cambridge Crystallographic Data Centre (CCDC).
+         */
+        depnum_ccdc_fiz: str,
+        /**
+         * Code assigned by the Inorganic Crystal Structure Database.
+         */
+        ICSD: str,
+        /**
+         * Code assigned in the Metals Data File.
+         */
+        MDF: str,
+        /**
+         * Code assigned by the NBS (NIST) Crystal Data Database.
+         */
+        NBS: str,
     },
     /**
      * The CATEGORY of data items used to describe atom site information
@@ -476,6 +538,33 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_11: float,
+        /**
+         * These are the standard anisotropic atomic displacement
+         * components in angstroms squared which appear in the
+         * structure factor term:
+         *
+         * T = exp{-2pi^2^ sum~i~ [sum~j~ (U^ij^ h~i~ h~j~ a*~i~ a*~j~) ] }
+         *
+         * h = the Miller indices
+         * a* = the reciprocal-space cell lengths
+         *
+         * The unique elements of the real symmetric matrix are entered by row.
+         */
+        U: Matrix(3, 3),
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_11_su: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_su: Matrix(3, 3),
         /**
          * These are the standard anisotropic atomic displacement
          * components in angstroms squared which appear in the
@@ -489,6 +578,13 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_12: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_12_su: float,
         /**
          * These are the standard anisotropic atomic displacement
          * components in angstroms squared which appear in the
@@ -502,6 +598,13 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_13: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_13_su: float,
         /**
          * These are the standard anisotropic atomic displacement
          * components in angstroms squared which appear in the
@@ -515,6 +618,13 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_22: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_22_su: float,
         /**
          * These are the standard anisotropic atomic displacement
          * components in angstroms squared which appear in the
@@ -528,6 +638,13 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_23: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_23_su: float,
         /**
          * These are the standard anisotropic atomic displacement
          * components in angstroms squared which appear in the
@@ -541,6 +658,13 @@ export const CifCore_Schema = {
          * The unique elements of the real symmetric matrix are entered by row.
          */
         U_33: float,
+        /**
+         * These are the standard uncertainty values (SU) for the standard
+         * form of the Uij anisotropic atomic displacement components (see
+         * _aniso_UIJ. Because these values are TYPE measurand, the su values
+         * may in practice be auto generated as part of the Uij calculation.
+         */
+        U_33_su: float,
     },
     /**
      * The CATEGORY of data items used to describe atomic type information
@@ -586,6 +710,16 @@ export const CifCore_Schema = {
 }
 
 export const CifCore_Aliases = {
+    'atom_site_aniso.U': [
+        'atom_site_anisotrop_U',
+    ],
+    'atom_site_aniso.U_su': [
+        'atom_site_aniso_U_esd',
+        'atom_site_anisotrop_U_esd',
+    ],
+    'space_group.IT_number': [
+        'symmetry_Int_Tables_number',
+    ],
     'space_group.name_H-M_full': [
         'symmetry_space_group_name_H-M',
     ],
@@ -616,21 +750,45 @@ export const CifCore_Aliases = {
     'atom_site_aniso.U_11': [
         'atom_site_anisotrop_U_11',
     ],
+    'atom_site_aniso.U_11_su': [
+        'atom_site_aniso_U_11_esd',
+        'atom_site_anisotrop_U_11_esd',
+    ],
     'atom_site_aniso.U_12': [
         'atom_site_anisotrop_U_12',
     ],
+    'atom_site_aniso.U_12_su': [
+        'atom_site_aniso_U_12_esd',
+        'atom_site_anisotrop_U_12_esd',
+    ],
     'atom_site_aniso.U_13': [
         'atom_site_anisotrop_U_13',
     ],
+    'atom_site_aniso.U_13_su': [
+        'atom_site_aniso_U_13_esd',
+        'atom_site_anisotrop_U_13_esd',
+    ],
     'atom_site_aniso.U_22': [
         'atom_site_anisotrop_U_22',
     ],
+    'atom_site_aniso.U_22_su': [
+        'atom_site_aniso_U_22_esd',
+        'atom_site_anisotrop_U_22_esd',
+    ],
     'atom_site_aniso.U_23': [
         'atom_site_anisotrop_U_23',
     ],
+    'atom_site_aniso.U_23_su': [
+        'atom_site_aniso_U_23_esd',
+        'atom_site_anisotrop_U_23_esd',
+    ],
     'atom_site_aniso.U_33': [
         'atom_site_anisotrop_U_33',
     ],
+    'atom_site_aniso.U_33_su': [
+        'atom_site_aniso_U_33_esd',
+        'atom_site_anisotrop_U_33_esd',
+    ],
 }
 
 export type CifCore_Schema = typeof CifCore_Schema;

+ 4 - 2
src/mol-io/reader/common/text/number-parser.ts

@@ -71,8 +71,9 @@ export function parseFloat(str: string, start: number, end: number) {
             return neg * (ret + point / div);
         } else if (c === 53 || c === 21) { // 'e'/'E'
             return parseScientific(neg * ret, str, _start + 1, end);
+        } else {
+            break;
         }
-        else break;
     }
     return neg * ret;
 }
@@ -137,8 +138,9 @@ export function getNumberType(str: string): NumberType {
                 return NumberType.NaN; // string starts with e/E or -e/-E
             }
             return getNumberTypeScientific(str, start + 1, end);
+        } else {
+            break;
         }
-        else break;
     }
     return start === end ? NumberType.Int : NumberType.NaN;
 }

+ 1 - 2
src/mol-io/writer/cif/encoder/binary.ts

@@ -188,8 +188,7 @@ function getFieldData(field: Field<any, any>, arrayCtor: ArrayCtor<string | numb
                 if (isStr)
                     array[offset] = '';
                 allPresent = false;
-            }
-            else {
+            } else {
                 mask[offset] = Column.ValueKind.Present;
                 array[offset] = getter(key, d, offset);
             }

+ 5 - 2
src/mol-math/graph/int-adjacency-graph.ts

@@ -54,8 +54,11 @@ export namespace IntAdjacencyGraph {
 
         getEdgeIndex(i: VertexIndex, j: VertexIndex): number {
             let a, b;
-            if (i < j) { a = i; b = j; }
-            else { a = j; b = i; }
+            if (i < j) {
+                a = i; b = j;
+            } else {
+                a = j; b = i;
+            }
             for (let t = this.offset[a], _t = this.offset[a + 1]; t < _t; t++) {
                 if (this.b[t] === b) return t;
             }

+ 6 - 2
src/mol-math/linear-algebra/3d/mat4.ts

@@ -743,6 +743,8 @@ namespace Mat4 {
      * Check if the matrix has the form
      * [ Rotation    Translation ]
      * [ 0           1           ]
+     *
+     * Allows for improper rotations
      */
     export function isRotationAndTranslation(a: Mat4, eps?: number) {
         return _isRotationAndTranslation(a, typeof eps !== 'undefined' ? eps : EPSILON)
@@ -752,12 +754,14 @@ namespace Mat4 {
         const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
             a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
             a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
-            /* a30 = a[12], a31 = a[13], a32 = a[14],*/ a33 = a[15];
+            a33 = a[15];
 
         if (!equalEps(a33, 1, eps) || !equalEps(a03, 0, eps) || !equalEps(a13, 0, eps) || !equalEps(a23, 0, eps)) {
             return false;
         }
-        const det3x3 = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
+
+        // use `abs` to allow for improper rotations
+        const det3x3 = Math.abs(a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20));
         if (!equalEps(det3x3, 1, eps)) {
             return false;
         }

+ 1 - 2
src/mol-math/linear-algebra/matrix/evd.ts

@@ -77,8 +77,7 @@ function symmetricTridiagonalize(a: number[], d: number[], e: number[], order: n
                 a[(j * order) + i] = 0.0;
                 a[(i * order) + j] = 0.0;
             }
-        }
-        else {
+        } else {
             // Generate Householder vector.
             for (let k = 0; k < i; k++) {
                 d[k] /= scale;

+ 30 - 1
src/mol-model-formats/structure/_spec/cif-core.spec.ts

@@ -44,6 +44,34 @@ _cell_measurement_temperature    100(2)
 _cell_measurement_reflns_used    5934
 _cell_measurement_theta_min      2.86
 _cell_measurement_theta_max      64.30
+
+loop_
+_atom_site_aniso_label
+_atom_site_aniso_U_11
+_atom_site_aniso_U_22
+_atom_site_aniso_U_33
+_atom_site_aniso_U_23
+_atom_site_aniso_U_13
+_atom_site_aniso_U_12
+Pt1 0.0425(2) 0.0423(2) 0.0375(2) 0.00066(13) 0.01515(13) 0.00089(12)
+K1 0.0605(15) 0.0687(17) 0.0559(17) 0.000 0.0203(13) 0.000
+Cl2 0.0511(11) 0.0554(11) 0.0533(13) 0.0078(10) 0.0225(9) 0.0027(9)
+Cl3 0.0708(13) 0.0484(11) 0.0605(13) -0.0053(10) 0.0276(10) 0.0026(10)
+Cl1 0.0950(16) 0.0442(11) 0.0942(18) -0.0051(12) 0.0526(14) 0.0035(12)
+N9 0.045(3) 0.047(4) 0.035(4) 0.004(3) 0.014(3) -0.003(3)
+N7 0.040(3) 0.048(4) 0.036(3) 0.008(3) 0.004(3) -0.004(3)
+O2 0.052(3) 0.098(4) 0.046(4) -0.012(4) 0.006(3) -0.016(3)
+N3 0.041(3) 0.044(3) 0.044(4) 0.001(3) 0.008(3) -0.002(3)
+O6 0.053(3) 0.093(4) 0.052(3) 0.008(3) 0.021(3) -0.019(3)
+C4 0.044(4) 0.032(4) 0.050(5) 0.004(4) 0.011(4) 0.003(3)
+N1 0.049(4) 0.049(4) 0.040(4) 0.004(3) 0.014(3) -0.005(3)
+C8 0.050(4) 0.045(4) 0.033(4) -0.007(4) 0.000(3) -0.004(4)
+C5 0.036(4) 0.039(4) 0.045(5) 0.003(4) 0.013(3) -0.001(3)
+C2 0.047(4) 0.045(4) 0.039(5) -0.007(4) 0.011(4) -0.004(4)
+C7 0.041(4) 0.072(5) 0.055(5) 0.013(5) 0.006(4) -0.015(4)
+C1 0.061(5) 0.067(5) 0.043(5) -0.002(4) 0.017(4) -0.005(4)
+C3 0.038(4) 0.090(6) 0.054(5) 0.003(5) 0.013(4) -0.018(4)
+C6 0.045(4) 0.043(4) 0.038(4) 0.004(4) 0.008(3) -0.002(4)
 `
 
 describe('cif-core read', () => {
@@ -65,6 +93,7 @@ describe('cif-core read', () => {
         const cifCore = CIF.schema.cifCore(block)
 
         expect(cifCore.cell.length_a.value(0)).toBe(11.0829)
-        expect.assertions(1)
+        expect(cifCore.atom_site_aniso.U.value(0)).toEqual(new Float64Array([ 0.0425, 0, 0, 0.00089, 0.0423, 0, 0.01515, 0.00066, 0.0375 ]))
+        expect.assertions(2)
     });
 });

+ 201 - 0
src/mol-model-formats/structure/cif-core.ts

@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Column, Table } from '../../mol-data/db';
+import { Model, Symmetry } from '../../mol-model/structure/model';
+import { MoleculeType } from '../../mol-model/structure/model/types';
+import { RuntimeContext, Task } from '../../mol-task';
+import { createModels } from './basic/parser';
+import { BasicSchema, createBasic } from './basic/schema';
+import { ComponentBuilder } from './common/component';
+import { EntityBuilder } from './common/entity';
+import { ModelFormat } from './format';
+import { CifCore_Database } from '../../mol-io/reader/cif/schema/cif-core';
+import { CifFrame, CIF } from '../../mol-io/reader/cif';
+import { Spacegroup, SpacegroupCell } from '../../mol-math/geometry';
+import { Vec3 } from '../../mol-math/linear-algebra';
+import { ModelSymmetry } from './property/symmetry';
+import { IndexPairBonds } from './property/bonds/index-pair';
+import { AtomSiteAnisotrop } from './property/anisotropic';
+
+function getSpacegroupNameOrNumber(space_group: CifCore_Database['space_group']) {
+    const groupNumber = space_group.IT_number.value(0)
+    const groupName = space_group['name_H-M_full'].value(0)
+    if (!space_group.IT_number.isDefined) return groupName
+    if (!space_group['name_H-M_full'].isDefined) return groupNumber
+    return groupNumber
+}
+
+function getSymmetry(db: CifCore_Database): Symmetry {
+    const { cell, space_group } = db
+    const nameOrNumber = getSpacegroupNameOrNumber(space_group)
+    const spaceCell = SpacegroupCell.create(nameOrNumber,
+        Vec3.create(cell.length_a.value(0), cell.length_b.value(0), cell.length_c.value(0)),
+        Vec3.scale(Vec3.zero(), Vec3.create(cell.angle_alpha.value(0), cell.angle_beta.value(0), cell.angle_gamma.value(0)), Math.PI / 180));
+
+    return {
+        spacegroup: Spacegroup.create(spaceCell),
+        assemblies : [],
+        isNonStandardCrytalFrame: false,
+        ncsOperators: []
+    }
+}
+
+async function getModels(db: CifCore_Database, format: CifCoreFormat, ctx: RuntimeContext): Promise<Model[]> {
+
+    const atomCount = db.atom_site._rowCount
+    const MOL = Column.ofConst('MOL', atomCount, Column.Schema.str);
+    const A = Column.ofConst('A', atomCount, Column.Schema.str);
+    const seq_id = Column.ofConst(1, atomCount, Column.Schema.int);
+
+    const symmetry = getSymmetry(db)
+    const m = symmetry.spacegroup.cell.fromFractional
+
+    const { fract_x, fract_y, fract_z } = db.atom_site
+    const x = new Float32Array(atomCount)
+    const y = new Float32Array(atomCount)
+    const z = new Float32Array(atomCount)
+    const v = Vec3()
+    for (let i = 0; i < atomCount; ++i) {
+        Vec3.set(v, fract_x.value(i), fract_y.value(i), fract_z.value(i))
+        Vec3.transformMat4(v, v, m)
+        x[i] = v[0], y[i] = v[1], z[i] = v[2]
+    }
+
+    const atom_site = Table.ofPartialColumns(BasicSchema.atom_site, {
+        auth_asym_id: A,
+        auth_atom_id: db.atom_site.label,
+        auth_comp_id: MOL,
+        auth_seq_id: seq_id,
+        Cartn_x: Column.ofFloatArray(x),
+        Cartn_y: Column.ofFloatArray(y),
+        Cartn_z: Column.ofFloatArray(z),
+        id: Column.range(0, atomCount - 1),
+
+        label_asym_id: A,
+        label_atom_id: db.atom_site.label,
+        label_comp_id: MOL,
+        label_seq_id: seq_id,
+        label_entity_id: Column.ofConst('1', atomCount, Column.Schema.str),
+
+        occupancy: db.atom_site.occupancy,
+        type_symbol: db.atom_site.type_symbol,
+
+        pdbx_PDB_model_num: Column.ofConst(1, atomCount, Column.Schema.int),
+    }, atomCount);
+
+    const name = (
+        db.chemical.name_common.value(0) ||
+        db.chemical.name_systematic.value(0) ||
+        db.chemical_formula.sum.value(0)
+    )
+
+    const entityBuilder = new EntityBuilder()
+    entityBuilder.setNames([['MOL', name || 'Unknown Entity']])
+    entityBuilder.getEntityId('MOL', MoleculeType.Unknown, 'A');
+
+    const componentBuilder = new ComponentBuilder(seq_id, db.atom_site.type_symbol);
+    componentBuilder.setNames([['MOL', name || 'Unknown Molecule']])
+    componentBuilder.add('MOL', 0);
+
+    const basics = createBasic({
+        entity: entityBuilder.getEntityTable(),
+        chem_comp: componentBuilder.getChemCompTable(),
+        atom_site
+    });
+
+    const models = await createModels(basics, format, ctx);
+
+    if (models.length > 0) {
+        ModelSymmetry.Provider.set(models[0], symmetry)
+
+        const bondCount = db.geom_bond._rowCount
+        if(bondCount > 0) {
+            const labelIndexMap: { [label: string]: number } = {}
+            const { label } = db.atom_site
+            for (let i = 0, il = label.rowCount; i < il; ++i) {
+                labelIndexMap[label.value(i)] = i
+            }
+
+            const indexA: number[] = []
+            const indexB: number[] = []
+            const order: number[] = []
+            const symmetryA: string[] = []
+            const symmetryB: string[] = []
+
+            const { atom_site_label_1, atom_site_label_2, valence, site_symmetry_1, site_symmetry_2 } = db.geom_bond
+            for (let i = 0; i < bondCount; ++i) {
+                indexA[i] = labelIndexMap[atom_site_label_1.value(i)]
+                indexB[i] = labelIndexMap[atom_site_label_2.value(i)]
+                // TODO derive order from bond length if undefined
+                order[i] = valence.isDefined ? valence.value(i) : 1
+                symmetryA[i] = site_symmetry_1.value(i) || '1_555'
+                symmetryB[i] = site_symmetry_2.value(i) || '1_555'
+            }
+
+            IndexPairBonds.Provider.set(models[0], IndexPairBonds.fromData({ pairs: {
+                indexA: Column.ofIntArray(indexA),
+                indexB: Column.ofIntArray(indexB),
+                order: Column.ofIntArray(order),
+                symmetryA: Column.ofStringArray(symmetryA),
+                symmetryB: Column.ofStringArray(symmetryB)
+            }, count: indexA.length }));
+        }
+    }
+
+    return models;
+}
+
+function atomSiteAnisotropFromCifCore(model: Model) {
+    if (!CifCoreFormat.is(model.sourceData)) return;
+    const { atom_site, atom_site_aniso } = model.sourceData.data.db
+    const data = Table.ofPartialColumns(AtomSiteAnisotrop.Schema, {
+        U: atom_site_aniso.U,
+        U_esd: atom_site_aniso.U_su
+    }, atom_site_aniso._rowCount);
+    const elementToAnsiotrop = AtomSiteAnisotrop.getElementToAnsiotropFromLabel(atom_site.label, atom_site_aniso.label)
+    return { data, elementToAnsiotrop }
+}
+function atomSiteAnisotropApplicableCifCore(model: Model) {
+    if (!CifCoreFormat.is(model.sourceData)) return false;
+    return model.sourceData.data.db.atom_site_aniso.U.isDefined
+}
+AtomSiteAnisotrop.Provider.formatRegistry.add('cifCore', atomSiteAnisotropFromCifCore, atomSiteAnisotropApplicableCifCore)
+
+//
+
+export { CifCoreFormat };
+
+type CifCoreFormat = ModelFormat<CifCoreFormat.Data>
+
+namespace CifCoreFormat {
+    export type Data = { db: CifCore_Database, frame: CifFrame }
+    export function is(x: ModelFormat): x is CifCoreFormat {
+        return x.kind === 'cifCore'
+    }
+
+    export function fromFrame(frame: CifFrame, db?: CifCore_Database): CifCoreFormat {
+        if (!db) db = CIF.schema.cifCore(frame)
+
+        const name = (
+            db.database_code.depnum_ccdc_archive.value(0) ||
+            db.database_code.depnum_ccdc_fiz.value(0) ||
+            db.database_code.ICSD.value(0) ||
+            db.database_code.MDF.value(0) ||
+            db.database_code.NBS.value(0) ||
+            db.database_code.CSD.value(0) ||
+            db.database_code.COD.value(0) ||
+            db._name
+        )
+
+        return { kind: 'cifCore', name, data: { db, frame } };
+    }
+}
+
+export function trajectoryFromCifCore(frame: CifFrame): Task<Model.Trajectory> {
+    const format = CifCoreFormat.fromFrame(frame)
+    return Task.create('Parse CIF Core', ctx => getModels(format.data.db, format, ctx))
+}

+ 14 - 2
src/mol-model-formats/structure/common/property.ts

@@ -8,19 +8,27 @@ import { CustomPropertyDescriptor, Model } from '../../../mol-model/structure';
 import { ModelFormat } from '../format';
 
 class FormatRegistry<T> {
-    map = new Map<ModelFormat['kind'], (model: Model) => T | undefined>()
+    private map = new Map<ModelFormat['kind'], (model: Model) => T | undefined>()
+    private applicable = new Map<ModelFormat['kind'], (model: Model) => boolean>()
 
-    add(kind: ModelFormat['kind'], obtain: (model: Model) => T | undefined) {
+    add(kind: ModelFormat['kind'], obtain: (model: Model) => T | undefined, applicable?: (model: Model) => boolean) {
         this.map.set(kind, obtain)
+        if (applicable) this.applicable.set(kind, applicable)
     }
 
     remove(kind: ModelFormat['kind']) {
         this.map.delete(kind)
+        this.applicable.delete(kind)
     }
 
     get(kind: ModelFormat['kind']) {
         return this.map.get(kind)
     }
+
+    isApplicable(model: Model) {
+        const isApplicable = this.applicable.get(model.sourceData.kind)
+        return isApplicable ? isApplicable(model) : true
+    }
 }
 
 export { FormatPropertyProvider as FormatPropertyProvider }
@@ -28,6 +36,7 @@ export { FormatPropertyProvider as FormatPropertyProvider }
 interface FormatPropertyProvider<T> {
     readonly descriptor: CustomPropertyDescriptor
     readonly formatRegistry: FormatRegistry<T>
+    isApplicable(model: Model): boolean
     get(model: Model): T | undefined
     set(model: Model, value: T): void
 }
@@ -40,6 +49,9 @@ namespace FormatPropertyProvider {
         return {
             descriptor,
             formatRegistry,
+            isApplicable(model: Model) {
+                return formatRegistry.isApplicable(model)
+            },
             get(model: Model): T | undefined {
                 if (model._staticPropertyData[name]) return model._staticPropertyData[name]
                 if (model.customProperties.has(descriptor)) return

+ 8 - 4
src/mol-model-formats/structure/mmcif.ts

@@ -9,7 +9,7 @@ import { Model } from '../../mol-model/structure/model/model';
 import { Task } from '../../mol-task';
 import { ModelFormat } from './format';
 import { CifFrame, CIF } from '../../mol-io/reader/cif';
-import { mmCIF_Database, mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
+import { mmCIF_Database } from '../../mol-io/reader/cif/schema/mmcif';
 import { createModels } from './basic/parser';
 import { ModelSymmetry } from './property/symmetry';
 import { ModelSecondaryStructure } from './property/secondary-structure';
@@ -34,11 +34,15 @@ ModelSecondaryStructure.Provider.formatRegistry.add('mmCIF', secondaryStructureF
 function atomSiteAnisotropFromMmcif(model: Model) {
     if (!MmcifFormat.is(model.sourceData)) return;
     const { atom_site_anisotrop } = model.sourceData.data.db
-    const data = Table.ofColumns(mmCIF_Schema['atom_site_anisotrop'], atom_site_anisotrop);
-    const elementToAnsiotrop = AtomSiteAnisotrop.getElementToAnsiotrop(model, data)
+    const data = Table.ofColumns(AtomSiteAnisotrop.Schema, atom_site_anisotrop);
+    const elementToAnsiotrop = AtomSiteAnisotrop.getElementToAnsiotrop(model.atomicConformation.atomId, atom_site_anisotrop.id)
     return { data, elementToAnsiotrop }
 }
-AtomSiteAnisotrop.Provider.formatRegistry.add('mmCIF', atomSiteAnisotropFromMmcif)
+function atomSiteAnisotropApplicableMmcif(model: Model) {
+    if (!MmcifFormat.is(model.sourceData)) return false;
+    return model.sourceData.data.db.atom_site_anisotrop.U.isDefined
+}
+AtomSiteAnisotrop.Provider.formatRegistry.add('mmCIF', atomSiteAnisotropFromMmcif, atomSiteAnisotropApplicableMmcif)
 
 function componentBondFromMmcif(model: Model) {
     if (!MmcifFormat.is(model.sourceData)) return;

+ 8 - 7
src/mol-model-formats/structure/mol.ts

@@ -1,6 +1,7 @@
 /**
  * Copyright (c) 2019-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>
  */
 
@@ -19,7 +20,7 @@ import { IndexPairBonds } from './property/bonds/index-pair';
 async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
     const { atoms, bonds } = mol;
 
-    const UNK = Column.ofConst('UNK', mol.atoms.count, Column.Schema.str);
+    const MOL = Column.ofConst('MOL', mol.atoms.count, Column.Schema.str);
     const A = Column.ofConst('A', mol.atoms.count, Column.Schema.str);
     const type_symbol = Column.asArrayColumn(atoms.type_symbol);
     const seq_id = Column.ofConst(1, atoms.count, Column.Schema.int);
@@ -27,7 +28,7 @@ async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
     const atom_site = Table.ofPartialColumns(BasicSchema.atom_site, {
         auth_asym_id: A,
         auth_atom_id: type_symbol,
-        auth_comp_id: UNK,
+        auth_comp_id: MOL,
         auth_seq_id: seq_id,
         Cartn_x: Column.asArrayColumn(atoms.x, Float32Array),
         Cartn_y: Column.asArrayColumn(atoms.y, Float32Array),
@@ -36,7 +37,7 @@ async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
 
         label_asym_id: A,
         label_atom_id: type_symbol,
-        label_comp_id: UNK,
+        label_comp_id: MOL,
         label_seq_id: seq_id,
         label_entity_id: Column.ofConst('1', atoms.count, Column.Schema.str),
 
@@ -47,12 +48,12 @@ async function getModels(mol: MolFile, ctx: RuntimeContext): Promise<Model[]> {
     }, atoms.count);
 
     const entityBuilder = new EntityBuilder()
-    entityBuilder.setNames([['UNK', 'Unknown Entity']])
-    entityBuilder.getEntityId('UNK', MoleculeType.Unknown, 'A');
+    entityBuilder.setNames([['MOL', 'Unknown Entity']])
+    entityBuilder.getEntityId('MOL', MoleculeType.Unknown, 'A');
 
     const componentBuilder = new ComponentBuilder(seq_id, type_symbol);
-    componentBuilder.setNames([['UNK', 'Unknown Residue']])
-    componentBuilder.add('UNK', 0);
+    componentBuilder.setNames([['MOL', 'Unknown Molecule']])
+    componentBuilder.add('MOL', 0);
 
     const basics = createBasic({
         entity: entityBuilder.getEntityTable(),

+ 31 - 9
src/mol-model-formats/structure/property/anisotropic.ts

@@ -4,15 +4,20 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Table } from '../../../mol-data/db';
-import { Model, CustomPropertyDescriptor } from '../../../mol-model/structure';
+import { Table, Column } from '../../../mol-data/db';
+import { CustomPropertyDescriptor } from '../../../mol-model/structure';
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { CifWriter } from '../../../mol-io/writer/cif';
 import { FormatPropertyProvider } from '../common/property';
+import { MmcifFormat } from '../mmcif';
 
 export { AtomSiteAnisotrop }
 
-type Anisotrop = Table<mmCIF_Schema['atom_site_anisotrop']>
+const Anisotrop = {
+    U: mmCIF_Schema.atom_site_anisotrop.U,
+    U_esd: mmCIF_Schema.atom_site_anisotrop.U_esd
+}
+type Anisotrop = Table<typeof Anisotrop>
 
 interface AtomSiteAnisotrop {
     data: Anisotrop
@@ -21,6 +26,8 @@ interface AtomSiteAnisotrop {
 }
 
 namespace AtomSiteAnisotrop {
+    export const Schema = Anisotrop
+
     export const Descriptor: CustomPropertyDescriptor = {
         name: 'atom_site_anisotrop',
         cifExport: {
@@ -30,8 +37,9 @@ namespace AtomSiteAnisotrop {
                 instance(ctx) {
                     const p = Provider.get(ctx.firstModel);
                     if (!p) return CifWriter.Category.Empty;
+                    if (!MmcifFormat.is(ctx.firstModel.sourceData)) return CifWriter.Category.Empty;
                     // TODO filter to write only data for elements that exist in model
-                    return CifWriter.Category.ofTable(p.data);
+                    return CifWriter.Category.ofTable(ctx.firstModel.sourceData.data.db.atom_site_anisotrop);
                 }
             }]
         }
@@ -39,22 +47,36 @@ namespace AtomSiteAnisotrop {
 
     export const Provider = FormatPropertyProvider.create<AtomSiteAnisotrop>(Descriptor)
 
-    export function getElementToAnsiotrop(model: Model, data: Anisotrop) {
-        const { atomId } = model.atomicConformation
+    export function getElementToAnsiotrop(atomId: Column<number>, ansioId: Column<number>) {
         const atomIdToElement = new Int32Array(atomId.rowCount)
         atomIdToElement.fill(-1)
         for (let i = 0, il = atomId.rowCount; i < il; i++) {
             atomIdToElement[atomId.value(i)] = i
         }
 
-        const { id } = data
         const elementToAnsiotrop = new Int32Array(atomId.rowCount)
         elementToAnsiotrop.fill(-1)
-        for (let i = 0, il = id.rowCount; i < il; ++i) {
-            const ei = atomIdToElement[id.value(i)]
+        for (let i = 0, il = ansioId.rowCount; i < il; ++i) {
+            const ei = atomIdToElement[ansioId.value(i)]
             if (ei !== -1) elementToAnsiotrop[ei] = i
         }
 
         return elementToAnsiotrop
     }
+
+    export function getElementToAnsiotropFromLabel(atomLabel: Column<string>, ansioLabel: Column<string>) {
+        const atomLabelToElement: { [k: string]: number | undefined } = {}
+        for (let i = 0, il = atomLabel.rowCount; i < il; i++) {
+            atomLabelToElement[atomLabel.value(i)] = i
+        }
+
+        const elementToAnsiotrop = new Int32Array(atomLabel.rowCount)
+        elementToAnsiotrop.fill(-1)
+        for (let i = 0, il = ansioLabel.rowCount; i < il; ++i) {
+            const ei = atomLabelToElement[ansioLabel.value(i)]
+            if (ei !== undefined) elementToAnsiotrop[ei] = i
+        }
+
+        return elementToAnsiotrop
+    }
 }

+ 20 - 7
src/mol-model-formats/structure/property/bonds/index-pair.ts

@@ -9,17 +9,26 @@ import { IntAdjacencyGraph } from '../../../../mol-math/graph';
 import { Column } from '../../../../mol-data/db';
 import { FormatPropertyProvider } from '../../common/property';
 
-export type IndexPairBonds = IntAdjacencyGraph<number, { readonly order: ArrayLike<number> }>
+export type IndexPairBondsProps = {
+    readonly order: ArrayLike<number>
+    readonly symmetryA: ArrayLike<string>
+    readonly symmetryB: ArrayLike<string>
+}
+export type IndexPairBonds = IntAdjacencyGraph<number, IndexPairBondsProps>
 
-function getGraph(indexA: ArrayLike<number>, indexB: ArrayLike<number>, _order: ArrayLike<number>, count: number): IndexPairBonds {
+function getGraph(indexA: ArrayLike<number>, indexB: ArrayLike<number>, props: Partial<IndexPairBondsProps>, count: number): IndexPairBonds {
     const builder = new IntAdjacencyGraph.EdgeBuilder(count, indexA, indexB);
     const order = new Int8Array(builder.slotCount);
+    const symmetryA = new Array(builder.slotCount);
+    const symmetryB = new Array(builder.slotCount);
     for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
         builder.addNextEdge();
-        builder.assignProperty(order, _order[i]);
+        builder.assignProperty(order, props.order ? props.order[i] : 1);
+        builder.assignProperty(symmetryA, props.symmetryA ? props.symmetryA[i] : '');
+        builder.assignProperty(symmetryB, props.symmetryB ? props.symmetryB[i] : '');
     }
 
-    return builder.createGraph({ order });
+    return builder.createGraph({ order, symmetryA, symmetryB });
 }
 
 export namespace IndexPairBonds {
@@ -33,7 +42,9 @@ export namespace IndexPairBonds {
         pairs: {
             indexA: Column<number>,
             indexB: Column<number>
-            order: Column<number>
+            order?: Column<number>,
+            symmetryA?: Column<string>,
+            symmetryB?: Column<string>,
         },
         count: number
     }
@@ -42,7 +53,9 @@ export namespace IndexPairBonds {
         const { pairs, count } = data
         const indexA = pairs.indexA.toArray()
         const indexB = pairs.indexB.toArray()
-        const order = pairs.order.toArray()
-        return getGraph(indexA, indexB, order, count);
+        const order = pairs.order && pairs.order.toArray()
+        const symmetryA = pairs.symmetryA && pairs.symmetryA.toArray()
+        const symmetryB = pairs.symmetryB && pairs.symmetryB.toArray()
+        return getGraph(indexA, indexB, { order, symmetryA, symmetryB }, count);
     }
 }

+ 3 - 3
src/mol-model-formats/structure/property/symmetry.ts

@@ -8,7 +8,7 @@
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { Spacegroup, SpacegroupCell, SymmetryOperator } from '../../../mol-math/geometry';
 import { Tensor, Vec3, Mat3 } from '../../../mol-math/linear-algebra';
-import { Symmetry as _ModelSymmetry } from '../../../mol-model/structure/model/properties/symmetry';
+import { Symmetry } from '../../../mol-model/structure/model/properties/symmetry';
 import { createAssemblies } from './assembly';
 import { CustomPropertyDescriptor } from '../../../mol-model/structure';
 import { FormatPropertyProvider } from '../common/property';
@@ -21,7 +21,7 @@ namespace ModelSymmetry {
         name: 'model_symmetry',
     };
 
-    export const Provider = FormatPropertyProvider.create<_ModelSymmetry>(Descriptor)
+    export const Provider = FormatPropertyProvider.create<Symmetry>(Descriptor)
 
     type Data = {
         symmetry: Table<mmCIF_Schema['symmetry']>
@@ -33,7 +33,7 @@ namespace ModelSymmetry {
         pdbx_struct_oper_list: Table<mmCIF_Schema['pdbx_struct_oper_list']>
     }
 
-    export function fromData(data: Data): _ModelSymmetry {
+    export function fromData(data: Data): Symmetry {
         const assemblies = createAssemblies(data.pdbx_struct_assembly, data.pdbx_struct_assembly_gen, data.pdbx_struct_oper_list);
         const spacegroup = getSpacegroup(data.symmetry, data.cell);
         const isNonStandardCrytalFrame = checkNonStandardCrystalFrame(data.atom_sites, spacegroup);

+ 1 - 0
src/mol-model-props/rcsb/validation-report.ts

@@ -111,6 +111,7 @@ namespace ValidationReport {
     }
 
     export async function open(ctx: CustomProperty.Context, model: Model, props: FileSourceProps): Promise<ValidationReport> {
+        if (props.input === null) throw new Error('No file given')
         const xml = await readFromFile(props.input, 'xml').runInContext(ctx.runtime)
         return fromXml(xml, model)
     }

+ 8 - 12
src/mol-model/structure/model/model.ts

@@ -24,6 +24,7 @@ import { createModels } from '../../../mol-model-formats/structure/basic/parser'
 import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 import { ChainIndex } from './indexing';
 import { SymmetryOperator } from '../../../mol-math/geometry';
+import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 
 /**
  * Interface to the "source data" of the molecule.
@@ -159,19 +160,14 @@ export namespace Model {
         )
     }
 
+    const tmpAngles90 = Vec3.create(1.5708, 1.5708, 1.5708) // in radians
+    const tmpLengths1 = Vec3.create(1, 1, 1)
     export function hasCrystalSymmetry(model: Model) {
-        if (!MmcifFormat.is(model.sourceData)) return false
-        const { db } = model.sourceData.data
-        return (
-            db.symmetry._rowCount === 1 && db.cell._rowCount === 1 && !(
-                db.symmetry.Int_Tables_number.value(0) === 1 &&
-                db.cell.angle_alpha.value(0) === 90 &&
-                db.cell.angle_beta.value(0) === 90 &&
-                db.cell.angle_gamma.value(0) === 90 &&
-                db.cell.length_a.value(0) === 1 &&
-                db.cell.length_b.value(0) === 1 &&
-                db.cell.length_c.value(0) === 1
-            )
+        const spacegroup = ModelSymmetry.Provider.get(model)?.spacegroup
+        return !!spacegroup && !(
+            spacegroup.num === 1 &&
+            Vec3.equals(spacegroup.cell.anglesInRadians, tmpAngles90) &&
+            Vec3.equals(spacegroup.cell.size, tmpLengths1)
         )
     }
 

+ 10 - 4
src/mol-model/structure/query/utils/structure-set.ts

@@ -44,8 +44,11 @@ export function structureAreIntersecting(sA: Structure, sB: Structure): boolean
     if (sA === sB) return true;
 
     let a, b;
-    if (sA.units.length < sB.units.length) { a = sA; b = sB; }
-    else { a = sB; b = sA; }
+    if (sA.units.length < sB.units.length) {
+        a = sA; b = sB;
+    } else {
+        a = sB; b = sA;
+    }
 
     const aU = a.units, bU = b.unitMap;
 
@@ -64,8 +67,11 @@ export function structureIntersect(sA: Structure, sB: Structure): Structure {
     if (!structureAreIntersecting(sA, sB)) return Structure.Empty;
 
     let a, b;
-    if (sA.units.length < sB.units.length) { a = sA; b = sB; }
-    else { a = sB; b = sA; }
+    if (sA.units.length < sB.units.length) {
+        a = sA; b = sB;
+    } else {
+        a = sB; b = sA;
+    }
 
     const aU = a.units, bU = b.unitMap;
     const units: Unit[] = [];

+ 10 - 2
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -59,6 +59,8 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
     const testDistanceSq = (bRadius + MAX_RADIUS) * (bRadius + MAX_RADIUS);
 
     builder.startUnitPair(unitA, unitB)
+    const symmUnitA = unitA.conformation.operator.name
+    const symmUnitB = unitB.conformation.operator.name
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI = atomsA[_aI];
@@ -67,10 +69,14 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
         if (Vec3.squaredDistance(imageA, bCenter) > testDistanceSq) continue;
 
         if (!props.forceCompute && indexPairs) {
+            const { order, symmetryA, symmetryB } = indexPairs.edgeProps
             for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
                 const _bI = SortedArray.indexOf(unitA.elements, indexPairs.b[i]) as StructureElement.UnitIndex;
                 if (_bI < 0) continue;
-                builder.add(_aI, _bI, { order: indexPairs.edgeProps.order[i], flag: BondType.Flag.Covalent });
+                if (symmetryA[i] === symmetryB[i]) continue;
+                if (symmUnitA === symmetryA[i] && symmUnitB === symmetryB[i]) {
+                    builder.add(_aI, _bI, { order: order[i], flag: BondType.Flag.Covalent });
+                }
             }
             continue // assume `indexPairs` supplies all bonds
         }
@@ -137,7 +143,9 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
             const thresholdAB = getElementPairThreshold(aeI, beI);
             const pairingThreshold = thresholdAB > 0
                 ? thresholdAB
-                : beI < 0 ? thresholdA : Math.max(thresholdA, getElementThreshold(beI));
+                : beI < 0
+                    ? thresholdA
+                    : (thresholdA + getElementThreshold(beI)) / 2; // not sure if avg or min but max is too big
 
             if (dist <= pairingThreshold) {
                 const atomIdB = label_atom_idB.value(bI);

+ 6 - 2
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -60,12 +60,14 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
         const aI =  atoms[_aI];
 
         if (!props.forceCompute && indexPairs) {
+            const { edgeProps } = indexPairs
             for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
                 const _bI = SortedArray.indexOf(unit.elements, indexPairs.b[i]) as StructureElement.UnitIndex;
                 if (_bI < 0) continue;
+                if (edgeProps.symmetryA[i] !== edgeProps.symmetryB[i]) continue;
                 atomA[atomA.length] = _aI;
                 atomB[atomB.length] = _bI;
-                order[order.length] = indexPairs.edgeProps.order[i];
+                order[order.length] = edgeProps.order[i];
                 flags[flags.length] = BondType.Flag.Covalent;
             }
             continue // assume `indexPairs` supplies all bonds
@@ -157,7 +159,9 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
             const thresholdAB = getElementPairThreshold(aeI, beI);
             const pairingThreshold = thresholdAB > 0
                 ? thresholdAB
-                : beI < 0 ? thresholdA : Math.max(thresholdA, getElementThreshold(beI));
+                : beI < 0
+                    ? thresholdA
+                    : (thresholdA + getElementThreshold(beI)) / 2; // not sure if avg or min but max is too big
 
             if (dist <= pairingThreshold) {
                 atomA[atomA.length] = _aI;

+ 3 - 2
src/mol-model/structure/structure/util/subset-builder.ts

@@ -21,8 +21,9 @@ export class StructureSubsetBuilder {
 
     addToUnit(parentId: number, e: ElementIndex) {
         const unit = this.unitMap.get(parentId);
-        if (!!unit) { unit[unit.length] = e; }
-        else {
+        if (!!unit) {
+            unit[unit.length] = e;
+        } else {
             this.unitMap.set(parentId, [e]);
             this.ids[this.ids.length] = parentId;
         }

+ 1 - 2
src/mol-model/structure/structure/util/unique-subset-builder.ts

@@ -24,8 +24,7 @@ export class StructureUniqueSubsetBuilder {
         const unit = this.unitMap.get(parentId);
         if (!!unit) {
             if (UniqueArray.add(unit, e, e)) this.elementCount++;
-        }
-        else {
+        } else {
             const arr: UArray = UniqueArray.create();
             UniqueArray.add(arr, e, e);
             this.unitMap.set(parentId, arr);

+ 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 - 170
src/mol-plugin-state/actions/data-format.ts

@@ -1,170 +0,0 @@
-/**
- * Copyright (c) 2019 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 } 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('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 () => {
-        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 data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file, isBinary });
-                // const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
-                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' | -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') {
-        if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
-    }
-    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 - 131
src/mol-plugin-state/actions/structure.ts

@@ -8,140 +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 cif/bcif files that are not DensityServer CIF are mmCIF
-        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif'
-        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 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,
@@ -192,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);
 }));

+ 7 - 7
src/mol-plugin-state/animation/built-in.ts

@@ -105,7 +105,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
         };
     },
     initialState: () => ({ t: 0 }),
-    async setup(params, plugin) {
+    setup(params, plugin) {
         const state = plugin.state.data;
         const root = !params.target || params.target === 'all' ? StateTransform.RootRef : params.target;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D, root));
@@ -123,9 +123,9 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
         if (!changed) return;
 
-        return plugin.updateDataState(update, { doNotUpdateCurrent: true });
+        return update.commit({ doNotUpdateCurrent: true });
     },
-    async teardown(_, plugin) {
+    teardown(_, plugin) {
         const state = plugin.state.data;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
             .withTag('animate-assembly-unwind'));
@@ -133,7 +133,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
         const update = state.build();
         for (const r of reprs) update.delete(r.transform.ref);
-        return plugin.updateDataState(update);
+        return update.commit();
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.data;
@@ -190,9 +190,9 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         if (!changed) return;
 
-        return plugin.updateDataState(update, { doNotUpdateCurrent: true });
+        return update.commit({ doNotUpdateCurrent: true });
     },
-    async teardown(_, plugin) {
+    teardown(_, plugin) {
         const state = plugin.state.data;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
             .withTag('animate-units-explode'));
@@ -200,7 +200,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         const update = state.build();
         for (const r of reprs) update.delete(r.transform.ref);
-        return plugin.updateDataState(update);
+        return update.commit();
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.data;

+ 9 - 13
src/mol-plugin-state/builder/data.ts

@@ -14,29 +14,25 @@ export class DataBuilder {
         return this.plugin.state.data;
     }
 
-    async rawData(params: StateTransformer.Params<RawData>, options?: Partial<StateTransform.Options>) {
+    rawData(params: StateTransformer.Params<RawData>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(RawData, params, options);
-        await this.plugin.updateDataState(data, { revertOnError: true });
-        return data.selector;
+        return data.commit({ revertOnError: true });
     }
 
-    async download(params: StateTransformer.Params<Download>, options?: Partial<StateTransform.Options>) {
+    download(params: StateTransformer.Params<Download>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(Download, params, options);
-        await this.plugin.updateDataState(data, { revertOnError: true });
-        return data.selector;
+        return data.commit({ revertOnError: true });
     }
 
-    async downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {
+    downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(DownloadBlob, params, options);
-        await this.plugin.updateDataState(data, { revertOnError: true });
-        return data.selector;
+        return data.commit({ revertOnError: true });
     }
 
     async readFile(params: StateTransformer.Params<ReadFile>, options?: Partial<StateTransform.Options>) {
-        const data = this.dataState.build().toRoot().apply(ReadFile, params, options);
-        const fileInfo = getFileInfo(params.file);
-        await this.plugin.updateDataState(data, { revertOnError: true });
-        return { data: data.selector, fileInfo };
+        const data = await this.dataState.build().toRoot().apply(ReadFile, params, options).commit({ revertOnError: true });
+        const fileInfo = getFileInfo(params.file || '');
+        return { data: data, fileInfo };
     }
 
     constructor(public plugin: PluginContext) {

+ 18 - 25
src/mol-plugin-state/builder/structure.ts

@@ -26,27 +26,26 @@ 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;
     }
 
-    private async parseTrajectoryBlob(data: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>) {
+    private parseTrajectoryBlob(data: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>) {
         const state = this.dataState;
         const trajectory = state.build().to(data)
             .apply(StateTransforms.Data.ParseBlob, params, { state: { isGhost: true } })
             .apply(StateTransforms.Model.TrajectoryFromBlob, void 0);
-        await this.plugin.updateDataState(trajectory, { revertOnError: true });
-        return trajectory.selector;
+        return trajectory.commit({ revertOnError: true });
     }
 
     readonly hierarchy = new TrajectoryHierarchyBuilder(this.plugin);
     readonly representation = new StructureRepresentationBuilder(this.plugin);
 
-    async parseTrajectory(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: BuiltInTrajectoryFormat | TrajectoryFormatProvider): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
-    async parseTrajectory(blob: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
-    async parseTrajectory(data: StateObjectRef, params: any) {
+    parseTrajectory(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: BuiltInTrajectoryFormat | TrajectoryFormatProvider): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
+    parseTrajectory(blob: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
+    parseTrajectory(data: StateObjectRef, params: any) {
         const cell = StateObjectRef.resolveAndCheck(this.dataState, data as StateObjectRef);
         if (!cell) throw new Error('Invalid data cell.');
 
@@ -57,24 +56,22 @@ export class StructureBuilder {
         }
     }
 
-    async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>, initialState?: Partial<StateTransform.State>) {
+    createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>, initialState?: Partial<StateTransform.State>) {
         const state = this.dataState;
         const model = state.build().to(trajectory)
             .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }, { state: initialState });
 
-        await this.plugin.updateDataState(model, { revertOnError: true });
-        return model.selector;
+        return model.commit({ revertOnError: true });
     }
 
-    async insertModelProperties(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Model']['CustomModelProperties']>, initialState?: Partial<StateTransform.State>) {
+    insertModelProperties(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Model']['CustomModelProperties']>, initialState?: Partial<StateTransform.State>) {
         const state = this.dataState;
         const props = state.build().to(model)
             .apply(StateTransforms.Model.CustomModelProperties, params, { state: initialState });
-        await this.plugin.updateDataState(props, { revertOnError: true });
-        return props.selector;
+        return props.commit({ revertOnError: true });
     }
 
-    async tryCreateUnitcell(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Representation']['ModelUnitcell3D']>, initialState?: Partial<StateTransform.State>) {
+    tryCreateUnitcell(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Representation']['ModelUnitcell3D']>, initialState?: Partial<StateTransform.State>) {
         const state = this.dataState;
         const m = StateObjectRef.resolveAndCheck(state, model)?.obj?.data;
         if (!m) return;
@@ -83,11 +80,10 @@ export class StructureBuilder {
 
         const unitcell = state.build().to(model)
             .apply(StateTransforms.Representation.ModelUnitcell3D, params, { state: initialState });
-        await this.plugin.updateDataState(unitcell, { revertOnError: true });
-        return unitcell.selector;
+        return unitcell.commit({ revertOnError: true });
     }
 
-    async createStructure(modelRef: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params, initialState?: Partial<StateTransform.State>) {
+    createStructure(modelRef: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params, initialState?: Partial<StateTransform.State>) {
         const state = this.dataState;
 
         if (!params) {
@@ -101,16 +97,14 @@ export class StructureBuilder {
         const structure = state.build().to(modelRef)
             .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { state: initialState });
 
-        await this.plugin.updateDataState(structure, { revertOnError: true });
-        return structure.selector;
+        return structure.commit({ revertOnError: true });
     }
 
-    async insertStructureProperties(structure: StateObjectRef<SO.Molecule.Structure>, params?: StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']>) {
+    insertStructureProperties(structure: StateObjectRef<SO.Molecule.Structure>, params?: StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']>) {
         const state = this.dataState;
         const props = state.build().to(structure)
             .apply(StateTransforms.Model.CustomStructureProperties, params);
-        await this.plugin.updateDataState(props, { revertOnError: true });
-        return props.selector;
+        return props.commit({ revertOnError: true });
     }
 
     isComponentTransform(cell: StateObjectCell) {
@@ -128,13 +122,12 @@ export class StructureBuilder {
             tags: tags ? [...tags, keyTag] : [keyTag]
         });
 
-        await this.plugin.updateDataState(component);
+        await component.commit();
 
         const selector = component.selector;
 
         if (!selector.isOk || selector.cell?.obj?.data.elementCount === 0) {
-            const del = state.build().delete(selector.ref);
-            await this.plugin.updateDataState(del);
+            await state.build().delete(selector.ref).commit();
             return;
         }
 

+ 6 - 5
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 };
     }
 });
@@ -148,7 +149,7 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
             nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams, color }, { tag: 'nucleic' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true });
         return { components, representations };
     }
 });
@@ -189,7 +190,7 @@ const coarseSurface = StructureRepresentationPresetProvider({
             polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color }, { tag: 'polymer' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true });
         return { components, representations };
     }
 });
@@ -214,7 +215,7 @@ const polymerCartoon = StructureRepresentationPresetProvider({
             polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }, { tag: 'polymer' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true });
         return { components, representations };
     }
 });
@@ -239,7 +240,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
             all: builder.buildRepresentation(update, components.all, { type: 'ball-and-stick', typeParams, color }, { tag: 'all' })
         };
 
-        await plugin.updateDataState(update, { revertOnError: true });
+        await update.commit({ revertOnError: true });
         return { components, representations };
     }
 });

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

@@ -117,7 +117,7 @@ export class StructureRepresentationBuilder {
         const selector = this.buildRepresentation(repr, structure, props, options);
         if (!selector) return;
 
-        await this.plugin.updateDataState(repr);
+        await repr.commit();
         return selector;
     }
 

+ 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 - 40
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,27 +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' | -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') {
-        if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
-    }
-    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 format.commit();
+
+        return { format: format.selector, shape: shape.selector };
+    }
+});
+
+export const BuiltInShapeFormats = [
+    ['ply', PlyProvider] as const,
+] as const
+
+export type BuildInShapeFormat = (typeof BuiltInShapeFormats)[number][0]

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

@@ -0,0 +1,49 @@
+/**
+ * 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 format.commit();
+
+        return { format: format.selector, topology: topology.selector };
+    }
+});
+
+export const DcdProvider = DataFormatProvider({
+    label: 'DCD',
+    description: 'DCD',
+    category: Category,
+    binaryExtensions: ['dcd'],
+    parse: (plugin, data) => {
+        const coordinates = plugin.state.data.build()
+            .to(data)
+            .apply(StateTransforms.Model.CoordinatesFromDcd);
+
+        return coordinates.commit();
+    }
+});
+
+export const BuiltInStructureFormats = [
+    ['psf', PsfProvider] as const,
+    ['dcd', DcdProvider] as const,
+] as const
+
+export type BuildInStructureFormat = (typeof BuiltInStructureFormats)[number][0]

+ 61 - 35
src/mol-plugin-state/formats/trajectory.ts

@@ -5,105 +5,131 @@
  * @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 cif/bcif files that are not DensityServer CIF are mmCIF
-        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif'
+        // assume undetermined cif/bcif files are mmCIF
+        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) === -1
         return false
     },
     parse: async (plugin, data, params) => {
         const state = plugin.state.data;
         const cif = state.build().to(data)
             .apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
-        const trajectory = cif.apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { tags: params?.trajectoryTags })
-        await plugin.updateDataState(trajectory, { revertOnError: true });
+        const trajectory = await cif
+            .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { tags: params?.trajectoryTags })
+            .commit({ revertOnError: true });
+
         if ((cif.selector.cell?.obj?.data.blocks.length || 0) > 1) {
             plugin.state.data.updateCellState(cif.ref, { isGhost: false });
         }
-        return { trajectory: trajectory.selector };
-    }
+
+        return { trajectory };
+    },
+    visuals: defaultVisuals
+}
+
+export const CifCoreProvider: TrajectoryFormatProvider = {
+    label: 'cifCore',
+    description: 'CIF Core',
+    category: Category,
+    stringExtensions: ['cif'],
+    isApplicable: (info, data) => {
+        if (info.ext === 'cif') return guessCifVariant(info, data) === 'coreCif'
+        return false
+    },
+    parse: async (plugin, data, params) => {
+        const state = plugin.state.data;
+        const cif = state.build().to(data)
+            .apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
+        const trajectory = await cif
+            .apply(StateTransforms.Model.TrajectoryFromCifCore, void 0, { tags: params?.trajectoryTags })
+            .commit({ revertOnError: true });
+
+        if ((cif.selector.cell?.obj?.data.blocks.length || 0) > 1) {
+            plugin.state.data.updateCellState(cif.ref, { isGhost: false });
+        }
+        return { trajectory };
+    },
+    visuals: defaultVisuals
 }
 
 function directTrajectory(transformer: StateTransformer<PluginStateObject.Data.String | PluginStateObject.Data.Binary, PluginStateObject.Molecule.Trajectory>): TrajectoryFormatProvider['parse'] {
     return async (plugin, data, params) => {
         const state = plugin.state.data;
-        const trajectory = state.build().to(data)
+        const trajectory = await state.build().to(data)
             .apply(transformer, void 0, { tags: params?.trajectoryTags })
-        await plugin.updateDataState(trajectory, { revertOnError: true });
-        return { trajectory: trajectory.selector };
+            .commit({ revertOnError: true });
+        return { trajectory };
     }
 }
 
 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,
     ['gro', GroProvider] as const,
     ['3dg', Provider3dg] as const,
     ['mol', MolProvider] as const
 ] as const
 
-export type BuiltInTrajectoryFormat = (typeof BuildInTrajectoryFormats)[number][0]
+export type BuiltInTrajectoryFormat = (typeof BuiltInTrajectoryFormats)[number][0]

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

@@ -0,0 +1,118 @@
+/**
+ * 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);
+    return [await visual.commit()];
+}
+
+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]

+ 2 - 2
src/mol-plugin-state/helpers/structure-overpaint.ts

@@ -55,7 +55,7 @@ export async function clearStructureOverpaint(plugin: PluginContext, components:
     });
 }
 
-async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: OverpaintEachReprCallback) {
+function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: OverpaintEachReprCallback) {
     const state = plugin.state.data;
     const update = state.build();
     for (const c of components) {
@@ -65,7 +65,7 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
         }
     }
 
-    await plugin.updateDataState(update, { doNotUpdateCurrent: true });
+    return update.commit({ doNotUpdateCurrent: true });
 }
 
 /** filter overpaint layers for given structure */

+ 17 - 12
src/mol-plugin-state/manager/structure/component.ts

@@ -60,7 +60,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }
 
         return this.plugin.dataTransaction(async () => {
-            await this.plugin.updateDataState(update);
+            await update.commit();
             if (interactionChanged) await this.updateInterationProps();
         });
     }
@@ -89,11 +89,11 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
                 const oldParams = s.properties.cell.transform.params?.properties[InteractionsProvider.descriptor.name];
                 if (PD.areEqual(interactionParams, oldParams, this.state.options.interactions)) continue;
 
-                const b = this.dataState.build();
-                b.to(s.properties.cell).update(old => {
-                    old.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
-                });
-                await this.plugin.updateDataState(b);
+                await this.dataState.build().to(s.properties.cell)
+                    .update(old => {
+                        old.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
+                    })
+                    .commit();
             } else {
                 const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
                 const params = PD.getDefaultValues(pd);
@@ -113,7 +113,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }, { canUndo: 'Preset' });
     }
 
-    private async syncPreset(root: StructureRef, preset?: StructureRepresentationPresetProvider.Result) {
+    private syncPreset(root: StructureRef, preset?: StructureRepresentationPresetProvider.Result) {
         if (!preset || !preset.components) return this.clearComponents([root]);
 
         const keptRefs = new Set<string>();
@@ -153,7 +153,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
             }
         }
 
-        if (changed) return this.plugin.updateDataState(update);
+        if (changed) return update.commit();
     }
 
     clear(structures: ReadonlyArray<StructureRef>) {
@@ -247,7 +247,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
             update.to(repr.cell).update(params);
         }
 
-        return this.plugin.updateDataState(update, { canUndo: 'Update Representation' });
+        return update.commit({ canUndo: 'Update Representation' });
     }
 
     /**
@@ -288,7 +288,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
             }
         }
 
-        return this.plugin.updateDataState(update, { canUndo: 'Update Theme' });
+        return update.commit({ canUndo: 'Update Theme' });
     }
 
     addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
@@ -313,6 +313,10 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
             const xs = structures || this.currentStructures;
             if (xs.length === 0) return;
 
+            const { showHydrogens, visualQuality: quality } = this.state.options;
+            const ignoreHydrogens = !showHydrogens;
+            const typeParams = { ignoreHydrogens, quality };
+
             const componentKey = UUID.create22();
             for (const s of xs) {
                 const component = await this.plugin.builders.structure.tryCreateComponentFromSelection(s.cell, params.selection, componentKey, {
@@ -320,7 +324,8 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
                 });
                 if (params.representation === 'none' || !component) continue;
                 await this.plugin.builders.structure.representation.addRepresentation(component, {
-                    type: this.plugin.representation.structure.registry.get(params.representation)
+                    type: this.plugin.representation.structure.registry.get(params.representation),
+                    typeParams
                 });
             }
         }, { canUndo: 'Add Selection' });
@@ -379,7 +384,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
                 deletes.delete(c.cell.transform.ref);
             }
         }
-        return this.plugin.updateDataState(deletes, { canUndo: 'Clear Selections' });
+        return deletes.commit({ canUndo: 'Clear Selections' });
     }
 
     constructor(public plugin: PluginContext) {

+ 2 - 2
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -155,7 +155,7 @@ export class StructureHierarchyManager extends PluginComponent {
         if (refs.length === 0) return;
         const deletes = this.plugin.state.data.build();
         for (const r of refs) deletes.delete(typeof r === 'string' ? r : r.cell.transform.ref);
-        return this.plugin.updateDataState(deletes, { canUndo: canUndo ? 'Remove' : false });
+        return deletes.commit({ canUndo: canUndo ? 'Remove' : false });
     }
 
     toggleVisibility(refs: ReadonlyArray<HierarchyRef>, action?: 'show' | 'hide') {
@@ -196,7 +196,7 @@ export class StructureHierarchyManager extends PluginComponent {
         for (const m of trajectory.models) {
             builder.delete(m.cell);
         }
-        return this.plugin.updateDataState(builder);
+        return builder.commit();
     }
 
     constructor(private plugin: PluginContext) {

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

@@ -210,8 +210,9 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         if (this.state.isPlaying) {
             this.stop();
             this.plugin.state.animation.stop();
+        } else {
+            this.play();
         }
-        else this.play();
     }
 
     constructor(private plugin: PluginContext) {

+ 8 - 4
src/mol-plugin-state/transforms/data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * 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>
@@ -11,7 +11,7 @@ import { Task } from '../../mol-task';
 import { CIF } from '../../mol-io/reader/cif'
 import { PluginContext } from '../../mol-plugin/context';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { StateTransformer } from '../../mol-state';
+import { StateTransformer, StateObject } from '../../mol-state';
 import { readFromFile, ajaxGetMany } from '../../mol-util/data-source';
 import * as CCP4 from '../../mol-io/reader/ccp4/parser'
 import * as DSN6 from '../../mol-io/reader/dsn6/parser'
@@ -143,8 +143,12 @@ const ReadFile = PluginStateTransform.BuiltIn({
         isBinary: PD.Optional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
     }
 })({
-    apply({ params: p }) {
+    apply({ params: p }, plugin: PluginContext) {
         return Task.create('Open File', async ctx => {
+            if (p.file === null) {
+                plugin.log.error('No file(s) selected')
+                return StateObject.Null;
+            }
             const data = await readFromFile(p.file, p.isBinary ? 'binary' : 'string').runInContext(ctx);
             return p.isBinary
                 ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.file.name })
@@ -153,7 +157,7 @@ const ReadFile = PluginStateTransform.BuiltIn({
     },
     update({ oldParams, newParams, b }) {
         if (oldParams.label !== newParams.label) {
-            (b.label as string) = newParams.label || oldParams.file.name;
+            (b.label as string) = newParams.label || oldParams.file?.name || '';
             return StateTransformer.UpdateResult.Updated;
         }
         return StateTransformer.UpdateResult.Unchanged;

+ 33 - 0
src/mol-plugin-state/transforms/model.ts

@@ -34,6 +34,7 @@ import { StructureSelectionQueries } from '../helpers/structure-selection-query'
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { parseMol } from '../../mol-io/reader/mol/parser';
 import { trajectoryFromMol } from '../../mol-model-formats/structure/mol';
+import { trajectoryFromCifCore } from '../../mol-model-formats/structure/cif-core';
 
 export { CoordinatesFromDcd };
 export { TopologyFromPsf };
@@ -43,6 +44,7 @@ export { TrajectoryFromMmCif };
 export { TrajectoryFromPDB };
 export { TrajectoryFromGRO };
 export { TrajectoryFromMOL };
+export { TrajectoryFromCifCore };
 export { TrajectoryFrom3DG };
 export { ModelFromTrajectory };
 export { StructureFromTrajectory };
@@ -233,6 +235,37 @@ const TrajectoryFromMOL = PluginStateTransform.BuiltIn({
     }
 });
 
+type TrajectoryFromCifCore = typeof TrajectoryFromCifCore
+const TrajectoryFromCifCore = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-cif-core',
+    display: { name: 'Parse CIF Core', description: 'Identify and create all separate models in the specified CIF data block' },
+    from: SO.Format.Cif,
+    to: SO.Molecule.Trajectory,
+    params(a) {
+        if (!a) {
+            return {
+                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+            };
+        }
+        const { blocks } = a.data;
+        return {
+            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Parse CIF Core', async ctx => {
+            const header = params.blockHeader || a.data.blocks[0].header;
+            const block = a.data.blocks.find(b => b.header === header);
+            if (!block) throw new Error(`Data block '${[header]}' not found.`);
+            const models = await trajectoryFromCifCore(block).runInContext(ctx);
+            if (models.length === 0) throw new Error('No models found.');
+            const props = { label: `${models[0].entry}`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
+
 type TrajectoryFrom3DG = typeof TrajectoryFrom3DG
 const TrajectoryFrom3DG = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-3dg',

+ 26 - 30
src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx

@@ -15,7 +15,7 @@ interface LineGraphComponentState {
 }
 
 export default class LineGraphComponent extends React.Component<any, LineGraphComponentState> {
-    private myRef:any;
+    private myRef: any;
     private height: number;
     private width: number;
     private padding: number;
@@ -123,11 +123,11 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
         // TODO: SET canSelectMultiple = fasle
     }
 
-    private handleClick = (id:number) => (event:any) => {
+    private handleClick = (id: number) => (event: any) => {
         // TODO: add point to selected array
     }
 
-    private handleMouseDown = (id:number) => (event: any) => {
+    private handleMouseDown = (id: number) => (event: any) => {
         if(id === 0 || id === this.state.points.length-1){
             return;
         }
@@ -164,17 +164,13 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
 
         if ((svgP.x < (padding) || svgP.x > (this.width+(padding))) && (svgP.y > (this.height+(padding)) || svgP.y < (padding))) {
             updatedCopyPoint = Vec2.create(this.updatedX, this.updatedY);
-        }
-        else if (svgP.x < padding) {
+        } else if (svgP.x < padding) {
             updatedCopyPoint = Vec2.create(padding, svgP.y);
-        }
-        else if( svgP.x > (this.width+(padding))) {
+        } else if( svgP.x > (this.width+(padding))) {
             updatedCopyPoint = Vec2.create(this.width+padding, svgP.y);
-        }
-        else if (svgP.y > (this.height+(padding))) {
+        } else if (svgP.y > (this.height+(padding))) {
             updatedCopyPoint = Vec2.create(svgP.x, this.height+padding);
-        }
-        else if (svgP.y < (padding)) {
+        } else if (svgP.y < (padding)) {
             updatedCopyPoint = Vec2.create(svgP.x, padding);
         } else {
             updatedCopyPoint = Vec2.create(svgP.x, svgP.y);
@@ -230,8 +226,8 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
         this.change(points);
         this.gElement.innerHTML = '';
         this.ghostPoints = [];
-        document.removeEventListener("mousemove", this.handleDrag, true);
-        document.removeEventListener("mouseup", this.handlePointUpdate, true);
+        document.removeEventListener('mousemove', this.handleDrag, true);
+        document.removeEventListener('mouseup', this.handlePointUpdate, true);
     }
 
     private handleDoubleClick(event: any) {
@@ -267,8 +263,8 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
         this.change(points);
     }
 
-    private deletePoint = (i:number) => (event: any) => {
-    if(i===0 || i===this.state.points.length-1){ return; }
+    private deletePoint = (i: number) => (event: any) => {
+        if(i===0 || i===this.state.points.length-1){ return; }
         const points = this.state.points.filter((_,j) => j !== i);
         points.sort((a, b) => {
             if(a[0] === b[0]){
@@ -334,21 +330,21 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
         const points: any[] = [];
         let point: Vec2;
         for (let i = 0; i < this.state.points.length; i++){
-            if(i != 0 && i != this.state.points.length-1){
+            if(i !== 0 && i !== this.state.points.length-1){
                 point = this.normalizePoint(this.state.points[i]);
                 points.push(<PointComponent
-                        key={i}
-                        id={i}
-                        x={point[0]}
-                        y={point[1]}
-                        nX={this.state.points[i][0]}
-                        nY={this.state.points[i][1]}
-                        selected={false}
-                        delete={this.deletePoint}
-                        onmouseover={this.props.onHover}
-                        onmousedown={this.handleMouseDown(i)}
-                        onclick={this.handleClick(i)}
-                    />);
+                    key={i}
+                    id={i}
+                    x={point[0]}
+                    y={point[1]}
+                    nX={this.state.points[i][0]}
+                    nY={this.state.points[i][1]}
+                    selected={false}
+                    delete={this.deletePoint}
+                    onmouseover={this.props.onHover}
+                    onmousedown={this.handleMouseDown(i)}
+                    onclick={this.handleClick(i)}
+                />);
             }
         }
         return points;
@@ -357,8 +353,8 @@ export default class LineGraphComponent extends React.Component<any, LineGraphCo
     private renderLines() {
         const points: Vec2[] = [];
         let lines = [];
-        let min:number;
-        let maxX:number;
+        let min: number;
+        let maxX: number;
         let maxY: number;
         let normalizedX: number;
         let normalizedY: number;

+ 3 - 3
src/mol-plugin-ui/controls/slider.tsx

@@ -733,7 +733,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
             ref: this.handleElements[i]
         }));
         if (!range) { handles.shift(); }
- 
+
         const sliderClassName = classNames({
             [prefixCls!]: true,
             [`${prefixCls}-with-marks`]: Object.keys(marks).length,
@@ -743,8 +743,8 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         });
 
         return (
-            <div ref={this.sliderElement} className={sliderClassName} 
-                onTouchStart={disabled ? noop : this.onTouchStart as any} 
+            <div ref={this.sliderElement} className={sliderClassName}
+                onTouchStart={disabled ? noop : this.onTouchStart as any}
                 onMouseDown={disabled ? noop : this.onMouseDown as any}
             >
                 <div className={`${prefixCls}-rail`} />

+ 2 - 3
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -188,10 +188,9 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
     }
 });
 
-export async function tryCreateAssemblySymmetry(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, params?: StateTransformer.Params<AssemblySymmetry3D>, initialState?: Partial<StateTransform.State>) {
+export function tryCreateAssemblySymmetry(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, params?: StateTransformer.Params<AssemblySymmetry3D>, initialState?: Partial<StateTransform.State>) {
     const state = plugin.state.data;
     const assemblySymmetry = state.build().to(structure)
         .applyOrUpdateTagged(AssemblySymmetry.Tag.Representation, AssemblySymmetry3D, params, { state: initialState });
-    await plugin.updateDataState(assemblySymmetry, { revertOnError: true });
-    return assemblySymmetry.selector
+    return assemblySymmetry.commit({ revertOnError: true });
 }

+ 1 - 1
src/mol-plugin/behavior/dynamic/custom-props/rcsb/ui/assembly-symmetry.tsx

@@ -96,7 +96,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
             b.to(s.properties.cell).update(old => {
                 old.properties[AssemblySymmetryProvider.descriptor.name] = values;
             });
-            await this.plugin.updateDataState(b);
+            await b.commit();
         } else {
             const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
             const params = PD.getDefaultValues(pd);

+ 1 - 1
src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts

@@ -329,7 +329,7 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
             clashesSnfg3d = builder.buildRepresentation<any>(update, clashes, { type: ClashesRepresentationProvider.name, typeParams, color }, { tag: 'clashes-snfg-3d' });
         }
 
-        await plugin.updateDataState(update, { revertOnError: false });
+        await update.commit({ revertOnError: true });
         return { components: { ...components, clashes }, representations: { ...representations, clashesBallAndStick, clashesSnfg3d } };
     }
 });

+ 1 - 1
src/mol-plugin/behavior/dynamic/representation.ts

@@ -184,7 +184,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
                     label.push(name)
                 }
                 label.push(lociLabel(loci))
-                return label.join('</br>')
+                return label.filter(l => !!l).join('</br>')
             },
             group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
             priority: 100

+ 1 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -99,7 +99,7 @@ export const InitVolumeStreaming = StateAction.build({
             entries
         });
 
-    await plugin.updateDataState(infoTree);
+    await infoTree.commit();
 
     const info = infoTree.selector;
     if (!info.isOk) return;

+ 10 - 17
src/mol-plugin/context.ts

@@ -11,10 +11,9 @@ 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 { DataFormatRegistry } from '../mol-plugin-state/formats/registry';
 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';
@@ -30,7 +29,7 @@ import { StateTransformParameters } from '../mol-plugin-ui/state/common';
 import { Representation } from '../mol-repr/representation';
 import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
 import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry';
-import { State, StateBuilder, StateTree, StateTransform } from '../mol-state';
+import { StateTransform } from '../mol-state';
 import { Progress, Task } from '../mol-task';
 import { ColorTheme } from '../mol-theme/color';
 import { SizeTheme } from '../mol-theme/size';
@@ -45,7 +44,7 @@ import { BuiltInPluginBehaviors } from './behavior';
 import { PluginBehavior } from './behavior/behavior';
 import { PluginCommandManager } from './command';
 import { PluginCommands } from './commands';
-import { PluginConfigManager, PluginConfig } from './config';
+import { PluginConfig, PluginConfigManager } from './config';
 import { LeftPanelTabName, PluginLayout } from './layout';
 import { PluginSpec } from './spec';
 import { PluginState } from './state';
@@ -56,6 +55,8 @@ import { ViewportScreenshotHelper } from './util/viewport-screenshot';
 import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 
 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,18 +219,10 @@ 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));
     }
 
-    updateDataState(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>) {
-        return this.runTask(this.state.data.updateTree(tree, options));
-    }
-
     requestTaskAbort(progress: Progress, reason?: string) {
         this.tasks.requestAbort(progress, reason);
     }

+ 1 - 0
src/mol-plugin/index.ts

@@ -39,6 +39,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Data.ParseDsn6),
 
         PluginSpec.Action(StateTransforms.Model.TrajectoryFromMmCif),
+        PluginSpec.Action(StateTransforms.Model.TrajectoryFromCifCore),
         PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
         PluginSpec.Action(StateTransforms.Model.TransformStructureConformation),
         PluginSpec.Action(StateTransforms.Model.StructureCoordinateSystem),

+ 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);
     }

+ 2 - 2
src/mol-repr/structure/representation/ellipsoid.ts

@@ -28,7 +28,7 @@ export const EllipsoidParams = {
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(1, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(0.1, { min: 0.01, max: 3, step: 0.01 }),
-    bondCap: PD.Boolean(true),
+    linkCap: PD.Boolean(true),
     visuals: PD.MultiSelect(['ellipsoid-mesh', 'intra-bond', 'inter-bond'], PD.objectToOptions(EllipsoidVisuals)),
 }
 export type EllipsoidParams = typeof EllipsoidParams
@@ -50,5 +50,5 @@ export const EllipsoidRepresentationProvider = StructureRepresentationProvider({
     defaultValues: PD.getDefaultValues(EllipsoidParams),
     defaultColorTheme: { name: 'element-symbol' },
     defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.elementCount > 0 && structure.models.some(m => m.customProperties.has(AtomSiteAnisotrop.Descriptor))
+    isApplicable: (structure: Structure) => structure.elementCount > 0 && structure.models.some(m => AtomSiteAnisotrop.Provider.isApplicable(m))
 })

+ 30 - 20
src/mol-script/runtime/query/table.ts

@@ -228,36 +228,46 @@ const symbols = [
     })(ctx)),
 
     // ============= GENERATORS ================
-    D(MolScript.structureQuery.generator.atomGroups, function structureQuery_generator_atomGroups(ctx, xs) { return Queries.generators.atoms({
-        entityTest: xs['entity-test'],
-        chainTest: xs['chain-test'],
-        residueTest: xs['residue-test'],
-        atomTest: xs['atom-test'],
-        groupBy: xs['group-by']
-    })(ctx) }),
+    D(MolScript.structureQuery.generator.atomGroups, function structureQuery_generator_atomGroups(ctx, xs) {
+        return Queries.generators.atoms({
+            entityTest: xs['entity-test'],
+            chainTest: xs['chain-test'],
+            residueTest: xs['residue-test'],
+            atomTest: xs['atom-test'],
+            groupBy: xs['group-by']
+        })(ctx)
+    }),
 
     D(MolScript.structureQuery.generator.all, function structureQuery_generator_all(ctx) { return Queries.generators.all(ctx) }),
     D(MolScript.structureQuery.generator.empty, function structureQuery_generator_empty(ctx) { return Queries.generators.none(ctx) }),
-    D(MolScript.structureQuery.generator.bondedAtomicPairs, function structureQuery_generator_bondedAtomicPairs(ctx, xs) { return Queries.generators.bondedAtomicPairs(xs && xs[0])(ctx) }),
-    D(MolScript.structureQuery.generator.rings, function structureQuery_generator_rings(ctx, xs) { return Queries.generators.rings(xs?.['fingerprint']?.(ctx) as any, xs?.['only-aromatic']?.(ctx))(ctx) }),
+    D(MolScript.structureQuery.generator.bondedAtomicPairs, function structureQuery_generator_bondedAtomicPairs(ctx, xs) {
+        return Queries.generators.bondedAtomicPairs(xs && xs[0])(ctx)
+    }),
+    D(MolScript.structureQuery.generator.rings, function structureQuery_generator_rings(ctx, xs) {
+        return Queries.generators.rings(xs?.['fingerprint']?.(ctx) as any, xs?.['only-aromatic']?.(ctx))(ctx)
+    }),
 
     // ============= MODIFIERS ================
 
-    D(MolScript.structureQuery.modifier.includeSurroundings, function structureQuery_modifier_includeSurroundings(ctx, xs) { return Queries.modifiers.includeSurroundings(xs[0] as any, {
-        radius: xs['radius'](ctx),
-        wholeResidues: !!(xs['as-whole-residues'] && xs['as-whole-residues'](ctx)),
-        elementRadius: xs['atom-radius']
-    })(ctx) }),
+    D(MolScript.structureQuery.modifier.includeSurroundings, function structureQuery_modifier_includeSurroundings(ctx, xs) {
+        return Queries.modifiers.includeSurroundings(xs[0] as any, {
+            radius: xs['radius'](ctx),
+            wholeResidues: !!(xs['as-whole-residues'] && xs['as-whole-residues'](ctx)),
+            elementRadius: xs['atom-radius']
+        })(ctx)
+    }),
     D(MolScript.structureQuery.modifier.wholeResidues, function structureQuery_modifier_wholeResidues(ctx, xs) { return Queries.modifiers.wholeResidues(xs[0] as any)(ctx) }),
     D(MolScript.structureQuery.modifier.union, function structureQuery_modifier_union(ctx, xs) { return Queries.modifiers.union(xs[0] as any)(ctx) }),
     D(MolScript.structureQuery.modifier.expandProperty, function structureQuery_modifier_expandProperty(ctx, xs) { return Queries.modifiers.expandProperty(xs[0] as any, xs['property'])(ctx) }),
     D(MolScript.structureQuery.modifier.exceptBy, function structureQuery_modifier_exceptBy(ctx, xs) { return Queries.modifiers.exceptBy(xs[0] as any, xs['by'] as any)(ctx) }),
-    D(MolScript.structureQuery.modifier.includeConnected, function structureQuery_modifier_includeConnected(ctx, xs) { return Queries.modifiers.includeConnected({
-        query: xs[0] as any,
-        bondTest: xs['bond-test'],
-        wholeResidues: !!(xs['as-whole-residues'] && xs['as-whole-residues'](ctx)),
-        layerCount: (xs['layer-count'] && xs['layer-count'](ctx)) || 1
-    })(ctx) }),
+    D(MolScript.structureQuery.modifier.includeConnected, function structureQuery_modifier_includeConnected(ctx, xs) {
+        return Queries.modifiers.includeConnected({
+            query: xs[0] as any,
+            bondTest: xs['bond-test'],
+            wholeResidues: !!(xs['as-whole-residues'] && xs['as-whole-residues'](ctx)),
+            layerCount: (xs['layer-count'] && xs['layer-count'](ctx)) || 1
+        })(ctx)
+    }),
 
     // ============= COMBINATORS ================
 

+ 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>;
             }
         })
     }

+ 33 - 16
src/mol-state/state.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateObject, StateObjectCell } from './object';
+import { StateObject, StateObjectCell, StateObjectSelector } from './object';
 import { StateTree } from './tree';
 import { StateTransform } from './transform';
 import { StateTransformer } from './transformer';
@@ -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<StateObjectSelector<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 };
@@ -247,6 +249,11 @@ class State {
 
             if (!this.inTransaction) this.behaviors.isUpdating.next(true);
             try {
+                if (StateBuilder.is(tree)) {
+                    if (tree.editInfo.applied) throw new Error('This builder has already been applied. Create a new builder for further state updates');
+                    tree.editInfo.applied = true;
+                }
+
                 this.reverted = false;
                 const ret = options && (options.revertIfAborted || options.revertOnError)
                     ? await this._revertibleTreeUpdate(taskCtx, params, options)
@@ -255,7 +262,9 @@ class State {
 
                 if (ret.ctx.hadError) this.inTransactionError = true;
 
-                return ret.cell;
+                if (!ret.cell) return;
+
+                return new StateObjectSelector(ret.cell.transform.ref, this);
             } finally {
                 this._inUpdate = false;
                 this.updateQueue.handled(params);
@@ -294,7 +303,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 +344,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 +373,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 +411,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 +902,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 +917,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);
 }

+ 14 - 1
src/mol-state/state/builder.ts

@@ -21,6 +21,7 @@ interface StateBuilder {
 
 namespace StateBuilder {
     export interface EditInfo {
+        applied: boolean,
         sourceTree: StateTree,
         count: number,
         lastUpdate?: StateTransform.Ref
@@ -102,7 +103,13 @@ namespace StateBuilder {
             return this;
         }
         getTree(): StateTree { return buildTree(this.state); }
-        constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
+
+        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: { applied: false, sourceTree: tree, count: 0, lastUpdate: void 0 } } }
     }
 
     export class To<A extends StateObject, T extends StateTransformer = StateTransformer> implements StateBuilder {
@@ -252,6 +259,12 @@ namespace StateBuilder {
 
         getTree(): StateTree { return buildTree(this.state); }
 
+        /** Returns selector to this node. */
+        commit(options?: Partial<State.UpdateOptions>): Promise<StateObjectSelector<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)) {

+ 6 - 6
src/mol-theme/label.ts

@@ -28,7 +28,7 @@ export type LabelOptions = typeof DefaultLabelOptions
 export function lociLabel(loci: Loci, options: Partial<LabelOptions> = {}): string {
     switch (loci.kind) {
         case 'structure-loci':
-            return loci.structure.models.map(m => m.entry).join(', ')
+            return loci.structure.models.map(m => m.entry).filter(l => !!l).join(', ')
         case 'element-loci':
             return structureElementStatsLabel(StructureElement.Stats.ofLoci(loci), options)
         case 'bond-loci':
@@ -148,7 +148,7 @@ export function _bundleLabel(bundle: Loci.Bundle<any>, options: LabelOptions) {
         const labels = locations.map(l => _elementLabel(l, granularity, hidePrefix, reverse || condensed))
 
         if (condensed) {
-            return labels.map(l => l[0].replace(/\[.*\]/g, '').trim()).join(' \u2014 ')
+            return labels.map(l => l[0].replace(/\[.*\]/g, '').trim()).filter(l => !!l).join(' \u2014 ')
         }
 
         let offset = 0
@@ -167,22 +167,22 @@ export function _bundleLabel(bundle: Loci.Bundle<any>, options: LabelOptions) {
         if (offset > 0) {
             const offsetLabels = [labels[0].join(' | ')]
             for (let j = 1, jl = labels.length; j < jl; ++j) {
-                offsetLabels.push(labels[j].slice(offset).join(' | '))
+                offsetLabels.push(labels[j].slice(offset).filter(l => !!l).join(' | '))
             }
             return offsetLabels.join(' \u2014 ')
         } else {
-            return labels.map(l => l.join(' | ')).join('</br>')
+            return labels.map(l => l.filter(l => !!l).join(' | ')).filter(l => !!l).join('</br>')
         }
     } else {
         const labels = bundle.loci.map(l => lociLabel(l, options))
-        return labels.join(condensed ? ' \u2014 ' : '</br>')
+        return labels.filter(l => !!l).join(condensed ? ' \u2014 ' : '</br>')
     }
 }
 
 export function elementLabel(location: StructureElement.Location, options: Partial<LabelOptions> = {}): string {
     const o = { ...DefaultLabelOptions, ...options }
     const _label = _elementLabel(location, o.granularity, o.hidePrefix, o.reverse || o.condensed)
-    const label = o.condensed ? _label[0].replace(/\[.*\]/g, '').trim() : _label.join(' | ')
+    const label = o.condensed ? _label[0].replace(/\[.*\]/g, '').trim() : _label.filter(l => !!l).join(' | ')
     return o.htmlStyling ? label : stripTags(label)
 }
 

+ 3 - 2
src/mol-util/make-dir.ts

@@ -12,8 +12,9 @@ export function makeDir(path: string, root?: string): boolean {
 
     root = (root || '') + dir + '/';
 
-    try { fs.mkdirSync(root); }
-    catch (e) {
+    try {
+        fs.mkdirSync(root);
+    } catch (e) {
         if (!fs.statSync(root).isDirectory()) throw new Error(e);
     }
 

+ 4 - 4
src/mol-util/param-definition.ts

@@ -147,22 +147,22 @@ export namespace ParamDefinition {
         return setInfo<Mat4>({ type: 'mat4', defaultValue }, info)
     }
 
-    export interface FileParam extends Base<File> {
+    export interface FileParam extends Base<File | null> {
         type: 'file'
         accept?: string
     }
     export function File(info?: Info & { accept?: string, multiple?: boolean }): FileParam {
-        const ret = setInfo<FileParam>({ type: 'file', defaultValue: void 0 as any }, info);
+        const ret = setInfo<FileParam>({ type: 'file', defaultValue: null }, info);
         if (info?.accept) ret.accept = info.accept;
         return ret;
     }
 
-    export interface FileListParam extends Base<FileList> {
+    export interface FileListParam extends Base<FileList | null> {
         type: 'file-list'
         accept?: string
     }
     export function FileList(info?: Info & { accept?: string, multiple?: boolean }): FileListParam {
-        const ret = setInfo<FileListParam>({ type: 'file-list', defaultValue: void 0 as any }, info);
+        const ret = setInfo<FileListParam>({ type: 'file-list', defaultValue: null }, info);
         if (info?.accept) ret.accept = info.accept;
         return ret;
     }

+ 1 - 2
src/mol-util/param-mapping.ts

@@ -23,8 +23,7 @@ export function ParamMapping<S, T, Ctx>(def: {
         values(t: T, ctx: Ctx): S,
         update(s: S, t: Mutable<T>, ctx: Ctx): void,
         apply?(t: T, ctx: Ctx): void | Promise<void>
-    }) => ParamMapping<S, T, Ctx>
-{
+    }) => ParamMapping<S, T, Ctx> {
     return ({ values, update, apply }) => ({
         params: typeof def.params === 'function' ? def.params as any : ctx => def.params,
         getTarget: def.target,

+ 11 - 5
src/mol-util/zip/bin.ts

@@ -91,11 +91,17 @@ export function sizeUTF8(str: string) {
     let i = 0;
     for(let ci = 0; ci < strl; ci++) {
         const code = str.charCodeAt(ci);
-        if     ((code&(0xffffffff-(1<< 7)+1)) === 0) {  i++ ;  }
-        else if((code&(0xffffffff-(1<<11)+1)) === 0) {  i+=2;  }
-        else if((code&(0xffffffff-(1<<16)+1)) === 0) {  i+=3;  }
-        else if((code&(0xffffffff-(1<<21)+1)) === 0) {  i+=4;  }
-        else throw 'e';
+        if ((code&(0xffffffff-(1<< 7)+1)) === 0) {
+            i++ ;
+        } else if((code&(0xffffffff-(1<<11)+1)) === 0) {
+            i+=2;
+        } else if((code&(0xffffffff-(1<<16)+1)) === 0) {
+            i+=3;
+        } else if((code&(0xffffffff-(1<<21)+1)) === 0) {
+            i+=4;
+        } else {
+            throw 'e';
+        }
     }
     return i;
 }

+ 4 - 2
src/mol-util/zip/deflate.ts

@@ -96,8 +96,9 @@ export function _deflateRaw(data: Uint8Array, out: Uint8Array, opos: number, lvl
                 const dgi = _goodIndex(dst, U.df0);  U.dhst[    dgi]++;  ebits += U.exb[lgi] + U.dxb[dgi];
                 lits[li] = (len<<23)|(i-cvrd);  lits[li+1] = (dst<<16)|(lgi<<8)|dgi;  li+=2;
                 cvrd = i + len;
+            } else {
+                U.lhst[data[i]]++;
             }
-            else {	U.lhst[data[i]]++;  }
             lc++;
         }
     }
@@ -302,8 +303,9 @@ function _lenCodes(tree: number[], set: number[]) {
             const zc = Math.min((lz+1-i)>>>1, 6);
             set.push(16, zc-3);
             i += zc*2-2;
+        } else {
+            set.push(l, 0);
         }
-        else set.push(l, 0);
     }
     return len>>>1;
 }

+ 2 - 1
src/mol-util/zip/zip.ts

@@ -105,8 +105,9 @@ async function _readLocal(runtime: RuntimeContext, data: Uint8Array, o: number,
         const buf = new Uint8Array(usize);
         await inflateRaw(runtime, file, buf);
         out[name] = buf;
+    } else {
+        throw `unknown compression method: ${cmpr}`;
     }
-    else throw `unknown compression method: ${cmpr}`;
 }
 
 export async function inflateRaw(runtime: RuntimeContext, file: Uint8Array, buf?: Uint8Array) {

+ 3 - 2
src/servers/volume/common/file.ts

@@ -34,8 +34,9 @@ function makeDir(path: string, root?: string): boolean {
 
     root = (root || '') + dir + '/';
 
-    try { fs.mkdirSync(root); }
-    catch (e) {
+    try {
+        fs.mkdirSync(root);
+    } catch (e) {
         if (!fs.statSync(root).isDirectory()) throw new Error(e);
     }
 

+ 3 - 2
src/servers/volume/server/local-api.ts

@@ -104,8 +104,9 @@ function makeDir(path: string, root?: string): boolean {
 
     root = (root || '') + dir + '/';
 
-    try { fs.mkdirSync(root); }
-    catch (e) {
+    try {
+        fs.mkdirSync(root);
+    } catch (e) {
         if (!fs.statSync(root).isDirectory()) throw new Error(e);
     }