ソースを参照

support for loading presets, updated molstar

Alexander Rose 5 年 前
コミット
42ee5ac750

+ 29 - 29
package-lock.json

@@ -4192,7 +4192,7 @@
             }
         },
         "molstar": {
-            "version": "0.5.4",
+            "version": "0.5.5",
             "dev": true,
             "requires": {
                 "@types/argparse": "^1.0.38",
@@ -7301,9 +7301,9 @@
                     }
                 },
                 "@types/node": {
-                    "version": "13.7.5",
-                    "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.5.tgz",
-                    "integrity": "sha512-PfSBCTQhAQg6QBP4UhXgrZ/wQ3pjfwBr4sA7Aul+pC9XwGgm9ezrJF7OiC/I4Kf+7VPu/5ThKngAruqxyctZfA=="
+                    "version": "13.7.7",
+                    "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz",
+                    "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg=="
                 },
                 "@types/node-fetch": {
                     "version": "2.5.5",
@@ -7385,7 +7385,7 @@
                 },
                 "@types/valid-url": {
                     "version": "1.0.2",
-                    "resolved": "http://registry.npmjs.org/@types/valid-url/-/valid-url-1.0.2.tgz",
+                    "resolved": "https://registry.npmjs.org/@types/valid-url/-/valid-url-1.0.2.tgz",
                     "integrity": "sha1-YPpDXOJL/VuhB7jSqAeWrq86j0U="
                 },
                 "@types/yargs": {
@@ -8749,7 +8749,7 @@
                 },
                 "camelcase-keys": {
                     "version": "2.1.0",
-                    "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+                    "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
                     "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
                     "requires": {
                         "camelcase": "^2.0.0",
@@ -11725,7 +11725,7 @@
                         },
                         "strip-ansi": {
                             "version": "3.0.1",
-                            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+                            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
                             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
                             "requires": {
                                 "ansi-regex": "^2.0.0"
@@ -16136,7 +16136,7 @@
                 },
                 "meow": {
                     "version": "3.7.0",
-                    "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+                    "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
                     "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
                     "requires": {
                         "camelcase-keys": "^2.0.0",
@@ -16153,7 +16153,7 @@
                     "dependencies": {
                         "minimist": {
                             "version": "1.2.0",
-                            "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+                            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
                             "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
                         }
                     }
@@ -16440,7 +16440,7 @@
                     "dependencies": {
                         "semver": {
                             "version": "5.3.0",
-                            "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+                            "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
                             "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
                         }
                     }
@@ -16563,7 +16563,7 @@
                         },
                         "chalk": {
                             "version": "1.1.3",
-                            "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+                            "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
                             "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
                             "requires": {
                                 "ansi-styles": "^2.2.1",
@@ -16584,7 +16584,7 @@
                         },
                         "strip-ansi": {
                             "version": "3.0.1",
-                            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+                            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
                             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
                             "requires": {
                                 "ansi-regex": "^2.0.0"
@@ -17551,9 +17551,9 @@
                     }
                 },
                 "react": {
-                    "version": "16.12.0",
-                    "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
-                    "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
+                    "version": "16.13.0",
+                    "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
+                    "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
                     "dev": true,
                     "requires": {
                         "loose-envify": "^1.1.0",
@@ -17562,15 +17562,15 @@
                     }
                 },
                 "react-dom": {
-                    "version": "16.12.0",
-                    "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
-                    "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
+                    "version": "16.13.0",
+                    "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz",
+                    "integrity": "sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg==",
                     "dev": true,
                     "requires": {
                         "loose-envify": "^1.1.0",
                         "object-assign": "^4.1.1",
                         "prop-types": "^15.6.2",
-                        "scheduler": "^0.18.0"
+                        "scheduler": "^0.19.0"
                     }
                 },
                 "react-is": {
@@ -18192,7 +18192,7 @@
                     "dependencies": {
                         "convert-source-map": {
                             "version": "0.3.5",
-                            "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+                            "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
                             "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA="
                         }
                     }
@@ -18610,7 +18610,7 @@
                         },
                         "os-locale": {
                             "version": "1.4.0",
-                            "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+                            "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
                             "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
                             "requires": {
                                 "lcid": "^1.0.0"
@@ -18633,7 +18633,7 @@
                         },
                         "strip-ansi": {
                             "version": "3.0.1",
-                            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+                            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
                             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
                             "requires": {
                                 "ansi-regex": "^2.0.0"
@@ -18716,9 +18716,9 @@
                     }
                 },
                 "scheduler": {
-                    "version": "0.18.0",
-                    "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
-                    "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
+                    "version": "0.19.0",
+                    "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz",
+                    "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==",
                     "dev": true,
                     "requires": {
                         "loose-envify": "^1.1.0",
@@ -18745,7 +18745,7 @@
                     "dependencies": {
                         "source-map": {
                             "version": "0.4.4",
-                            "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+                            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
                             "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
                             "requires": {
                                 "amdefine": ">=0.0.4"
@@ -19853,9 +19853,9 @@
                     }
                 },
                 "typescript": {
-                    "version": "3.8.2",
-                    "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz",
-                    "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ=="
+                    "version": "3.8.3",
+                    "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
+                    "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w=="
                 },
                 "ua-parser-js": {
                     "version": "0.7.21",

+ 1 - 1
package.json

@@ -50,7 +50,7 @@
         "extra-watch-webpack-plugin": "^1.0.3",
         "file-loader": "^5.1.0",
         "mini-css-extract-plugin": "^0.9.0",
-        "molstar": "^0.5.4",
+        "molstar": "0.5.5",
         "node-fetch": "^2.6.0",
         "node-sass": "^4.13.1",
         "raw-loader": "^4.0.0",

+ 2 - 7
src/structure-viewer/helpers/model.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -31,7 +31,7 @@ export class ModelLoader {
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: StateElements.Model })
     }
 
-    async load({ fileOrUrl, format = 'cif', assemblyId = 'deposited' }: LoadParams) {
+    async load({ fileOrUrl, format = 'cif' }: LoadParams) {
         if (!fileOrUrl) return
 
         const state = this.plugin.state.dataState;
@@ -43,11 +43,6 @@ export class ModelLoader {
             : this.download(state.build().toRoot(), fileOrUrl, isBinary)
         const model = this.model(data);
         await this.applyState(model)
-        await this.init(assemblyId)
-    }
-
-    async init(assemblyId = 'deposited') {
-        await this.customState.structureView.setAssembly(assemblyId)
     }
 
     async applyState(tree: StateBuilder) {

+ 177 - 84
src/structure-viewer/helpers/preset.ts

@@ -7,57 +7,82 @@
 import { StructureViewerState } from '../types';
 import { getStructureSize, StructureSize } from './util';
 import { PluginContext } from 'molstar/lib/mol-plugin/context';
-import { Structure } from 'molstar/lib/mol-model/structure';
-import { Loci, EmptyLoci } from 'molstar/lib/mol-model/loci';
+import { Structure, StructureSelection, QueryContext, StructureElement } from 'molstar/lib/mol-model/structure';
+import { Loci } from 'molstar/lib/mol-model/loci';
 import { Axes3D } from 'molstar/lib/mol-math/geometry';
 import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
 import { ValidationReport } from 'molstar/lib/mol-model-props/rcsb/validation-report';
 import { StructureSelectionQueries as SSQ } from 'molstar/lib/mol-plugin/util/structure-selection-helper';
 import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
-import { AssemblySymmetry } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry';
+import { AssemblySymmetry, AssemblySymmetryProvider } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry';
+import Expression from 'molstar/lib/mol-script/language/expression';
+import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
+import { Color } from 'molstar/lib/mol-util/color';
 
 type Target = {
     readonly auth_seq_id?: number
     readonly label_seq_id?: number
-    readonly label_comp_id?: number
-    readonly label_asym_id?: number
-    readonly pdbx_struct_oper_list_ids?: string[]
+    readonly label_comp_id?: string
+    readonly label_asym_id?: string
 }
 
-function targetToLoci(target: Target, structure: Structure): Loci {
-    return EmptyLoci
+function targetToExpression(target: Target): Expression {
+    const residueTests: Expression[] = []
+    const tests = Object.create(null)
+
+    if (target.auth_seq_id) {
+        residueTests.push(MS.core.rel.eq([target.auth_seq_id, MS.ammp('auth_seq_id')]))
+    } else if (target.label_seq_id) {
+        residueTests.push(MS.core.rel.eq([target.label_seq_id, MS.ammp('label_seq_id')]))
+    }
+    if (target.label_comp_id) {
+        residueTests.push(MS.core.rel.eq([target.label_comp_id, MS.ammp('label_comp_id')]))
+    }
+    if (residueTests.length === 1) {
+        tests['residue-test'] = residueTests[0]
+    } else if (residueTests.length > 1) {
+        tests['residue-test'] = MS.core.logic.and(residueTests)
+    }
+
+    if (target.label_asym_id) {
+        tests['chain-test'] = MS.core.rel.eq([target.label_asym_id, MS.ammp('label_asym_id')])
+    }
+
+    if (Object.keys(tests).length > 0) {
+        return MS.struct.modifier.union([
+            MS.struct.generator.atomGroups(tests)
+        ])
+    } else {
+        return MS.struct.generator.all
+    }
+}
+
+type BaseProps = {
+    assemblyId?: string
+    modelIndex?: number
 }
 
 type ValidationProps = {
     kind: 'validation'
     colorTheme?: string
     showClashes?: boolean
-    modelIndex?: number
-}
-
-type AssemblyProps = {
-    kind: 'assembly'
-    assemblyId: string
-    modelIndex?: number
-}
+} & BaseProps
 
 type StandardProps = {
     kind: 'standard'
-}
+} & BaseProps
 
 type SymmetryProps = {
     kind: 'symmetry'
-    assemblyId?: string
     symmetryIndex?: number
-}
+} & BaseProps
 
 type FeatureProps = {
     kind: 'feature'
-    assemblyId: string
     target: Target
-}
+} & BaseProps
 
-export type PresetProps = ValidationProps | AssemblyProps | StandardProps | SymmetryProps | FeatureProps
+export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps
 
 export class PresetManager {
     get customState() {
@@ -65,23 +90,22 @@ export class PresetManager {
     }
 
     async apply(props?: PresetProps) {
-        if (!props) props = { kind: 'assembly', assemblyId: 'deposited' }
+        if (!props) props = { kind: 'standard', assemblyId: 'deposited' }
+
         switch (props.kind) {
-            case 'assembly':
-                return this.assembly(props.assemblyId, props.modelIndex)
             case 'feature':
-                return this.feature(props.target, props.assemblyId)
+                return this.feature(props.target, props.assemblyId, props.modelIndex)
             case 'standard':
-                return this.standard()
+                return this.standard(props.assemblyId, props.modelIndex)
             case 'symmetry':
-                return this.symmetry(props.symmetryIndex, props.assemblyId)
+                return this.symmetry(props.symmetryIndex, props.assemblyId, props.modelIndex)
             case 'validation':
-                return this.validation(props.colorTheme, props.showClashes, props.modelIndex)
+                return this.validation(props.colorTheme, props.showClashes, props.assemblyId, props.modelIndex)
         }
     }
 
     async default() {
-        const assembly = this.customState.structureView.getAssembly()
+        const assembly = this.customState.structureView.getAssembly()?.obj
         if (!assembly || assembly.data.isEmpty) return
 
         const r = this.plugin.helpers.structureRepresentation
@@ -130,77 +154,87 @@ export class PresetManager {
         }
     }
 
-    async standard() {
+    async standard(assemblyId?: string, modelIndex?: number) {
+        await this.ensureAssembly(assemblyId, modelIndex)
         await this.customState.structureView.setSymmetry(-1)
         await this.default()
-        this.focus()
-    }
-
-    async assembly(assemblyId: string, modelIndex?: number) {
-        if (modelIndex !== undefined) {
-            await this.customState.structureView.setModel(modelIndex)
-        }
-        await this.customState.structureView.setAssembly(assemblyId)
-        await this.default()
-        this.focus()
-    }
-
-    async model(modelIndex: number) {
-        await this.customState.structureView.setModel(modelIndex)
-        await this.default()
-        this.focus()
+        this.focusOnLoci()
     }
 
     async feature(target: Target, assemblyId?: string, modelIndex?: number) {
-        if (modelIndex !== undefined) {
-            await this.customState.structureView.setModel(modelIndex)
-        }
-        if (assemblyId !== undefined) {
-            await this.customState.structureView.setAssembly(assemblyId)
-        }
-        const assembly = this.customState.structureView.getAssembly()
-        if (!assembly || assembly.data.isEmpty) return
+        await this.ensureAssembly(assemblyId, modelIndex, true)
+        const r = this.plugin.helpers.structureRepresentation
 
-        const loci = targetToLoci(target, assembly.data)
-        // TODO show target and surrounding residues in detail if small
-        this.focus(loci)
-    }
+        const assembly = this.customState.structureView.getAssembly()?.obj
+        if (!assembly || assembly.data.isEmpty) return
 
-    async symmetry(symmetryIndex?: number, assemblyId?: string) {
-        if (assemblyId !== undefined) {
-            await this.customState.structureView.setAssembly(assemblyId)
+        const expression = targetToExpression(target)
+        const query = compile<StructureSelection>(expression)
+        const result = query(new QueryContext(assembly.data))
+        const loci = StructureSelection.toLociWithSourceUnits(result)
+
+        if (target.auth_seq_id !== undefined || target.label_comp_id !== undefined || target.label_seq_id !== undefined ) {
+            const surroundings = MS.struct.modifier.includeSurroundings({
+                0: expression,
+                radius: 5,
+                'as-whole-residues': true
+            });
+            const surroundingsOnly = MS.struct.modifier.exceptBy({ 0: surroundings, by: expression });
+            await r.setFromExpression('add', 'ball-and-stick', surroundings)
+            await r.setFromExpression('add', 'interactions', surroundings)
+            await r.setFromExpression('add', 'label', surroundings)
+            await this.plugin.helpers.structureOverpaint.setFromExpression(Color(0xFFFFFF), surroundingsOnly, undefined, 2/3)
+            const firstResidue = StructureElement.Loci.firstResidue(loci)
+            this.focusOnLoci(Loci.isEmpty(firstResidue) ? Structure.Loci(assembly.data) : firstResidue)
+        } else if(target.label_asym_id) {
+            await this.default()
+            const firstChain = StructureElement.Loci.firstChain(loci)
+            this.focusOnLoci(Loci.isEmpty(firstChain) ? Structure.Loci(assembly.data) : firstChain)
+        } else {
             await this.default()
+            this.focusOnLoci()
         }
+    }
 
-        const assembly = this.customState.structureView.getAssembly()
+    async symmetry(symmetryIndex?: number, assemblyId?: string, modelIndex?: number) {
+        await this.ensureAssembly(assemblyId, modelIndex)
+        const r = this.plugin.helpers.structureRepresentation
+
+        const assembly = this.customState.structureView.getAssembly()?.obj
         if (!assembly || assembly.data.isEmpty) return
 
-        const r = this.plugin.helpers.structureRepresentation
+        await this.customState.structureView.attachAssemblySymmetry()
+        const assemblySymmetry = AssemblySymmetryProvider.get(assembly.data).value
+        if (!assemblySymmetry || !assemblySymmetry.find(s => s.symbol !== 'C1')) {
+            this.focusOnLoci()
+            return
+        }
 
-        await this.customState.structureView.setSymmetry(symmetryIndex || 0)
-        r.eachRepresentation((repr, type, update) => {
+        if (symmetryIndex === undefined) {
+            symmetryIndex = assemblySymmetry.findIndex(s => s.symbol !== 'C1')
+        }
+
+        await this.customState.structureView.setSymmetry(symmetryIndex)
+        await r.eachRepresentation((repr, type, update) => {
             if (type !== ValidationReport.Tag.Clashes) {
-                r.setRepresentationParams(repr, type, update, { color: AssemblySymmetry.Tag.Cluster })
+                r.setRepresentationParams(repr, type, update, {
+                    color: [AssemblySymmetry.Tag.Cluster, { symmetryIndex }]
+                })
             }
         })
 
-        // TODO focus on symmetry axes
-        this.focus()
+        this.focusOnSymmetry(symmetryIndex)
     }
 
-    async validation(colorTheme?: string, showClashes?: boolean, modelIndex?: number) {
-        if (modelIndex !== undefined) {
-            this.customState.structureView.setModel(modelIndex)
-            await this.default()
-        }
-
-        const assembly = this.customState.structureView.getAssembly()
-        if (!assembly || assembly.data.isEmpty) return
-
+    async validation(colorTheme?: string, showClashes?: boolean, assemblyId?: string, modelIndex?: number) {
+        await this.ensureAssembly(assemblyId, modelIndex)
         const r = this.plugin.helpers.structureRepresentation
 
+        const size = this.customState.structureView.getSize()
+        if (size === undefined) return
+
         if (showClashes === undefined) {
-            showClashes = getStructureSize(assembly.data) <= StructureSize.Medium
+            showClashes = size <= StructureSize.Medium
         }
 
         await this.customState.structureView.attachValidationReport()
@@ -212,18 +246,39 @@ export class PresetManager {
         }
 
         if (colorTheme === undefined) colorTheme = ValidationReport.Tag.GeometryQuality
-        r.eachRepresentation((repr, type, update) => {
+        await r.eachRepresentation((repr, type, update) => {
             if (type !== ValidationReport.Tag.Clashes) {
                 r.setRepresentationParams(repr, type, update, { color: colorTheme })
             }
         })
 
-        this.focus()
+        this.focusOnLoci()
+    }
+
+    async ensureAssembly(assemblyId?: string, modelIndex?: number, neverApplyDefault?: boolean) {
+        const oldSize = this.customState.structureView.getSize()
+
+        const model = this.customState.structureView.getModel()
+        if (!model && modelIndex === undefined) modelIndex = 0
+
+        const assembly = this.customState.structureView.getAssembly()
+        if (!assembly && assemblyId === undefined) assemblyId = 'deposited'
+
+        if (modelIndex !== undefined) {
+            await this.customState.structureView.setModel(modelIndex)
+        }
+
+        if (assemblyId !== undefined) {
+            await this.customState.structureView.setAssembly(assemblyId)
+        }
+        const newSize = this.customState.structureView.getSize()
+
+        if (!neverApplyDefault && oldSize !== newSize) await this.default()
     }
 
-    focus(loci?: Loci) {
+    focusOnLoci(loci?: Loci) {
         if (!loci) {
-            const assembly = this.customState.structureView.getAssembly()
+            const assembly = this.customState.structureView.getAssembly()?.obj
             if (!assembly || assembly.data.isEmpty) return
 
             loci = Structure.toStructureElementLoci(assembly.data)
@@ -232,13 +287,51 @@ export class PresetManager {
         const principalAxes = Loci.getPrincipalAxes(loci)
         if (!principalAxes) return
 
-        const extraRadius = 4, minRadius = 8, durationMs = 250
+        const extraRadius = 4, minRadius = 8, durationMs = 0
         const { origin, dirA, dirC } = principalAxes.boxAxes
         const axesRadius = Math.max(...Axes3D.size(Vec3(), principalAxes.boxAxes)) / 2
         const radius = Math.max(axesRadius + extraRadius, minRadius)
         this.plugin.canvas3d!.camera.focus(origin, radius, radius, durationMs, dirA, dirC);
     }
 
+    focusOnSymmetry(symmetryIndex: number) {
+        const assembly = this.customState.structureView.getAssembly()?.obj
+        if (!assembly || assembly.data.isEmpty) return
+
+        const assemblySymmetry = AssemblySymmetryProvider.get(assembly.data).value
+        const axes = assemblySymmetry?.[symmetryIndex].rotation_axes
+        if (!axes || !AssemblySymmetry.isRotationAxes(axes)) {
+            this.focusOnLoci()
+            return
+        }
+
+        const [aA, aB] = axes
+        if (!aA) return
+
+        const extraRadius = 4, minRadius = 8, durationMs = 0
+
+        const axisRadius = Vec3.distance(aA.start, aA.end) / 2
+        const radius = Math.max(axisRadius + extraRadius, minRadius)
+
+        const origin = Vec3()
+        Vec3.scale(origin, Vec3.add(origin, aA.start, aA.end), 0.5)
+
+        const dir = Vec3.sub(Vec3(), aA.start, aA.end)
+        const up = Vec3()
+
+        if (aB) {
+            Vec3.sub(up, aB.end, aB.start)
+        } else {
+            if (Vec3.dot(Vec3.unitY, Vec3.sub(Vec3(), aA.end, aA.start)) === 0) {
+                Vec3.copy(up, Vec3.unitY)
+            } else {
+                Vec3.copy(up, Vec3.unitX)
+            }
+        }
+
+        this.plugin.canvas3d!.camera.focus(origin, radius, radius, durationMs, up, dir);
+    }
+
     constructor(private plugin: PluginContext) {
 
     }

+ 65 - 65
src/structure-viewer/helpers/structure.ts

@@ -15,6 +15,7 @@ import { AssemblySymmetryProvider } from 'molstar/lib/mol-model-props/rcsb/assem
 import { Task } from 'molstar/lib/mol-task';
 import { AssemblySymmetry3D } from 'molstar/lib/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry';
 import { ValidationReportProvider } from 'molstar/lib/mol-model-props/rcsb/validation-report';
+import { getStructureSize } from './util';
 
 export class StructureView {
     get customState() {
@@ -38,7 +39,7 @@ export class StructureView {
         const trajectoryRef = this.findTrajectoryRef()
         if (!trajectoryRef || !this.plugin.state.dataState.transforms.has(trajectoryRef)) return
         const assemblies = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure, trajectoryRef))
-        return assemblies.length > 0 ? assemblies[0].obj : undefined
+        return assemblies.length > 0 ? assemblies[0] : undefined
     }
 
     getModel() {
@@ -47,6 +48,11 @@ export class StructureView {
         return models.length > 0 ? models[0].obj : undefined
     }
 
+    getSize() {
+        const assembly = this.getAssembly()
+        return assembly?.obj && getStructureSize(assembly.obj.data)
+    }
+
     private ensureModelUnitcell(tree: StateBuilder.Root, state: State) {
         if (!state.tree.transforms.has(StateElements.ModelUnitcell)) {
             tree.to(StateElements.Model).apply(
@@ -57,7 +63,7 @@ export class StructureView {
     }
 
     async attachAssemblySymmetry() {
-        const assembly = this.getAssembly()
+        const assembly = this.getAssembly()?.obj
         if (!assembly || assembly.data.isEmpty) return
 
         await this.plugin.runTask(Task.create('Assembly symmetry', async runtime => {
@@ -77,58 +83,20 @@ export class StructureView {
     async setAssembly(id: string) {
         const state = this.plugin.state.dataState;
         const tree = state.build();
-        if (id === AssemblyNames.Unitcell) {
-            const props = {
-                type: {
-                    name: 'symmetry' as const,
-                    params: { ijkMin: Vec3.create(0, 0, 0), ijkMax: Vec3.create(0, 0, 0) }
-                }
-            }
-            tree.delete(StateElements.Assembly)
-                .to(StateElements.Model).apply(
-                    StateTransforms.Model.StructureFromModel,
-                    props, { ref: StateElements.Assembly, tags: [ AssemblyNames.Unitcell ] }
-                )
-            this.ensureModelUnitcell(tree, state)
-        } else if (id === AssemblyNames.Supercell) {
-            const props = {
-                type: {
-                    name: 'symmetry' as const,
-                    params: { ijkMin: Vec3.create(-1, -1, -1), ijkMax: Vec3.create(1, 1, 1) }
-                }
-            }
-            tree.delete(StateElements.Assembly)
-                .to(StateElements.Model).apply(
-                    StateTransforms.Model.StructureFromModel,
-                    props, { ref: StateElements.Assembly, tags: [ AssemblyNames.Supercell ] }
-                )
-            this.ensureModelUnitcell(tree, state)
-        } else if (id === AssemblyNames.CrystalContacts) {
-            const props = {
-                type: {
-                    name: 'symmetry-mates' as const,
-                    params: { radius: 5 }
-                }
-            }
-            tree.delete(StateElements.ModelUnitcell)
-            tree.delete(StateElements.Assembly)
-                .to(StateElements.Model).apply(
-                    StateTransforms.Model.StructureFromModel,
-                    props, { ref: StateElements.Assembly, tags: [ AssemblyNames.CrystalContacts ] }
-                )
+
+        if (state.tree.transforms.has(StateElements.Assembly)) {
+            tree.to(StateElements.Assembly).update(
+                StateTransforms.Model.StructureFromModel,
+                props => ({ ...props, ...getAssemblyProps(id) })
+            )
         } else {
-            const props = {
-                type: {
-                    name: 'assembly' as const,
-                    params: { id }
-                }
-            }
-            tree.delete(StateElements.ModelUnitcell)
-            tree.delete(StateElements.Assembly)
-                .to(StateElements.Model).apply(
-                    StateTransforms.Model.StructureFromModel,
-                    props, { ref: StateElements.Assembly }
-                )
+            tree.to(StateElements.Model).apply(
+                StateTransforms.Model.StructureFromModel,
+                getAssemblyProps(id), { ref: StateElements.Assembly, tags: getAssemblyTag(id) }
+            )
+        }
+        if (id === AssemblyNames.Unitcell || id === AssemblyNames.Supercell) {
+            this.ensureModelUnitcell(tree, state)
         }
         await this.applyState(tree)
         await this.attachAssemblySymmetry()
@@ -151,25 +119,14 @@ export class StructureView {
                     props => ({ ...props, modelIndex })
                 )
             } else {
-                const props = {
-                    type: {
-                        name: 'assembly' as const,
-                        params: { id: AssemblyNames.Deposited }
-                    }
-                }
                 tree.delete(StateElements.Assembly)
                     .to(StateElements.Trajectory).apply(
                         StateTransforms.Model.ModelFromTrajectory,
                         { modelIndex }, { ref: StateElements.Model }
                     )
-                    .apply(
-                        StateTransforms.Model.StructureFromModel,
-                        props, { ref: StateElements.Assembly }
-                    )
             }
         }
         await this.applyState(tree)
-        await this.attachAssemblySymmetry()
     }
 
     async setSymmetry(symmetryIndex: number) {
@@ -184,7 +141,7 @@ export class StructureView {
                     props => ({ ...props, symmetryIndex })
                 )
             } else {
-                const assembly = this.getAssembly()
+                const assembly = this.getAssembly()?.obj
                 if (!assembly || assembly.data.isEmpty) return
 
                 const props = AssemblySymmetry3D.createDefaultParams(assembly, this.plugin)
@@ -200,4 +157,47 @@ export class StructureView {
     constructor(private plugin: PluginContext) {
 
     }
+}
+
+function getAssemblyProps(id: string) {
+    if (id === AssemblyNames.Unitcell) {
+        return {
+            type: {
+                name: 'symmetry' as const,
+                params: { ijkMin: Vec3.create(0, 0, 0), ijkMax: Vec3.create(0, 0, 0) }
+            }
+        }
+    } else if (id === AssemblyNames.Supercell) {
+        return {
+            type: {
+                name: 'symmetry' as const,
+                params: { ijkMin: Vec3.create(-1, -1, -1), ijkMax: Vec3.create(1, 1, 1) }
+            }
+        }
+    } else if (id === AssemblyNames.CrystalContacts) {
+        return {
+            type: {
+                name: 'symmetry-mates' as const,
+                params: { radius: 5 }
+            }
+        }
+    } else {
+        return {
+            type: {
+                name: 'assembly' as const,
+                params: { id }
+            }
+        }
+    }
+}
+
+function getAssemblyTag(id: string) {
+    switch (id) {
+        case AssemblyNames.Unitcell:
+        case AssemblyNames.Supercell:
+        case AssemblyNames.CrystalContacts:
+            return id
+        default:
+            return undefined
+    }
 }

+ 3 - 0
src/structure-viewer/helpers/volume.ts

@@ -23,6 +23,9 @@ export class VolumeData {
     }
 
     async init() {
+        const r = this.state.select(StateSelection.Generators.ofTransformer(CreateVolumeStreamingInfo))[0];
+        if (r) return;
+
         const { props } = this.customState
         const model = this.state.select(StateElements.Model)[0].obj;
         const asm = this.state.select(StateElements.Assembly)[0].obj;

+ 7 - 6
src/structure-viewer/index.html

@@ -45,16 +45,17 @@
                 return m ? decodeURIComponent(m[1]) : undefined
             }
 
-            const pdbId = getQueryParam('pdbId');
-            const url = getQueryParam('url');
-            const assemblyId = getQueryParam('assemblyId') || 'deposited';
+            const pdbId = getQueryParam('pdbId')
+            const url = getQueryParam('url')
+            const _props = getQueryParam('props')
+            const props = _props && JSON.parse(_props)
 
             // create an instance of the plugin
-            var viewer = new app.StructureViewer('app', { showOpenFileControls: !pdbId });
+            var viewer = new app.StructureViewer('app', { showOpenFileControls: !pdbId })
 
             // load pdbId or url
-            if (pdbId) viewer.loadPdbId(pdbId, assemblyId);
-            else if (url) viewer.loadUrl(url, assemblyId);
+            if (pdbId) viewer.loadPdbId(pdbId, props)
+            else if (url) viewer.loadUrl(url, props)
         </script>
         <div id="menu">
             <h2> RCSB PDB Mol* Viewer - Test Page</h2>

+ 8 - 4
src/structure-viewer/index.ts

@@ -12,7 +12,7 @@ import { PluginContext } from 'molstar/lib/mol-plugin/context';
 import { PluginCommands } from 'molstar/lib/mol-plugin/command';
 import { PluginBehaviors } from 'molstar/lib/mol-plugin/behavior';
 import { AnimateModelIndex } from 'molstar/lib/mol-plugin/state/animation/built-in';
-import { SupportedFormats, StructureViewerState, StructureViewerProps } from './types';
+import { SupportedFormats, StructureViewerState, StructureViewerProps, LoadParams } from './types';
 import { ControlsWrapper, ViewportWrapper } from './ui/controls';
 import { PluginSpec } from 'molstar/lib/mol-plugin/spec';
 import { StructureRepresentationInteraction } from 'molstar/lib/mol-plugin/behavior/dynamic/selection/structure-representation-interaction';
@@ -113,13 +113,17 @@ export class StructureViewer {
         })
     }
 
+    async load(load: LoadParams, props?: PresetProps) {
+        await this.customState.modelLoader.load(load)
+        await this.customState.presetManager.apply(props)
+    }
+
     async loadPdbId(pdbId: string, props?: PresetProps) {
         const p = this.props.modelUrlProvider(pdbId)
-        await this.customState.modelLoader.load({ fileOrUrl: p.url, format: p.format })
-        await this.customState.presetManager.apply(props)
+        this.load({ fileOrUrl: p.url, format: p.format }, props)
     }
 
     async loadUrl(url: string, props?: PresetProps) {
-        await this.customState.modelLoader.load({ fileOrUrl: url, format: 'cif', })
+        this.load({ fileOrUrl: url, format: 'cif', }, props)
     }
 }

+ 0 - 11
src/structure-viewer/types.ts

@@ -24,17 +24,6 @@ export interface LoadParams {
     fileOrUrl: File | string,
     /** A supported file format extension string */
     format?: SupportedFormats,
-    /**
-     * The assemblyId to show initially
-     * - 'deposited' for the structure as it is given in the file
-     * - a number as string, e.g. '1', '2', ... must be defined in the file
-     * - 'unitcell' for the unitcell of an X-ray structure
-     * - 'supercell' for the supercell of an X-ray structure
-     * - 'crystal-contacts' for the symmetry mates of an X-ray structure
-     */
-    assemblyId?: string,
-    // TODO The modelId to show initially
-    // modelId?: string
 }
 
 export enum StateElements {

+ 22 - 75
src/structure-viewer/ui/structure.tsx

@@ -15,10 +15,7 @@ import { StateTransforms } from 'molstar/lib/mol-plugin/state/transforms';
 import { stringToWords } from 'molstar/lib/mol-util/string';
 import { ModelSymmetry } from 'molstar/lib/mol-model-formats/structure/property/symmetry';
 import { AssemblySymmetryProvider } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry'
-import { ActionMenu } from 'molstar/lib/mol-plugin-ui/controls/action-menu';
-import { PresetProps } from '../helpers/preset';
-import { ValidationReport } from 'molstar/lib/mol-model-props/rcsb/validation-report';
-import { modelFromCrystallography, modelHasMap, modelHasSymmetry, modelFromNmr, getStructureSize, StructureSize } from '../helpers/util';
+import { modelFromCrystallography, modelHasSymmetry } from '../helpers/util';
 
 interface StructureControlsState extends CollapsableState {
     trajectoryRef: string
@@ -39,7 +36,7 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         const state = this.plugin.state.dataState;
         const tree = state.build();
 
-        const assembly = this.getAssembly()
+        const assembly = this.getAssembly()?.obj
         const symmetry = this.getSymmetry()
         const dataCtx = { structure: assembly && assembly.data }
 
@@ -83,12 +80,13 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
     onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
         // console.log('onChange', p.name, p.value)
         if (p.name === 'assembly') {
-            await this.customState.presetManager.assembly(p.value)
+            await this.customState.presetManager.standard(p.value)
         } else if (p.name === 'model') {
-            await this.customState.presetManager.model(p.value)
+            await this.customState.presetManager.standard(undefined, p.value)
         } else if (p.name === 'symmetry') {
             await this.customState.structureView.setSymmetry(p.value)
             await this.syncSymmetryIndex()
+            this.customState.presetManager.focusOnSymmetry(p.value)
         } else if (p.name === 'colorThemes') {
             await this.setColorTheme(p.value)
         }
@@ -102,7 +100,7 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         const { themeCtx, registry } = this.plugin.structureRepresentation
         const trajectory = this.getTrajectory()
         const model = this.getModel()
-        const assembly = this.getAssembly()
+        const assembly = this.getAssembly()?.obj
 
         const modelOptions: [number, string][] = []
         const assemblyOptions: [string, string][] = []
@@ -162,7 +160,9 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
                 symmetryValue = 0
                 for (let i = 0, il = assemblySymmetry.length; i < il; ++i) {
                     const { symbol, kind } = assemblySymmetry[i]
-                    symmetryOptions.push([i, `${i + 1}: ${symbol} ${kind}`])
+                    if (symbol !== 'C1') {
+                        symmetryOptions.push([i, `${i + 1}: ${symbol} ${kind}`])
+                    }
                 }
             }
         }
@@ -193,7 +193,7 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
                 isHidden: symmetryOptions.length === 1,
                 description: 'Show a specific assembly symmetry'
             }),
-            colorThemes: PD.Group(colorThemes, { isExpanded: true }),
+            colorThemes: PD.Group(colorThemes, { isExpanded: false }),
         }
     }
 
@@ -204,7 +204,7 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         const symmetry = this.getSymmetry()
 
         const { registry } = this.plugin.structureRepresentation
-        const types = assembly ? registry.getApplicableTypes(assembly.data) : registry.types
+        const types = assembly?.obj ? registry.getApplicableTypes(assembly.obj.data) : registry.types
 
         const colorThemes: { [k: string]: string } = {}
         for (let i = 0, il = types.length; i < il; ++i) {
@@ -219,16 +219,18 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         }
 
         let assemblyValue: string = AssemblyNames.Deposited
-        if (assembly) {
-            const tags = (assembly as StateObject).tags
-            if (tags && tags.includes('unitcell')) {
-                assemblyValue = AssemblyNames.Unitcell
-            } else if (tags && tags.includes('supercell')) {
-                assemblyValue = AssemblyNames.Supercell
-            } else if (tags && tags.includes('crystal-contacts')) {
+        if (assembly?.params) {
+            const type = assembly.params.values.type
+            if (type.name === 'symmetry') {
+                if (type.params.ijkMin[0] = 0) {
+                    assemblyValue = AssemblyNames.Unitcell
+                } else {
+                    assemblyValue = AssemblyNames.Supercell
+                }
+            } else if (type.name === 'symmetry') {
                 assemblyValue = AssemblyNames.CrystalContacts
             } else {
-                assemblyValue = assembly.data.units[0].conformation.operator.assembly.id || AssemblyNames.Deposited
+                assemblyValue = assembly.params.values.type.params.id || 'deposited'
             }
         }
 
@@ -301,61 +303,13 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
     private getAssembly() {
         if (!this.state.trajectoryRef || !this.plugin.state.dataState.transforms.has(this.state.trajectoryRef)) return
         const assemblies = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure, this.state.trajectoryRef))
-        return assemblies.length > 0 ? assemblies[0].obj : undefined
+        return assemblies.length > 0 ? assemblies[0] : undefined
     }
 
     private getSymmetry() {
         return this.plugin.state.dataState.transforms.get(StateElements.AssemblySymmetry)
     }
 
-    private actionMenu = new ActionMenu();
-
-    private applyPreset = (props: PresetProps) => {
-        this.customState.presetManager.apply(props)
-    }
-
-    private getPresets = () => {
-        const model = this.getModel()
-        const assembly = this.getAssembly()
-
-        const showClashes = assembly && getStructureSize(assembly.data) <= StructureSize.Medium
-
-        const validationItems = [
-            'Validation Report',
-            ActionMenu.Item(`Geometry Quality Coloring${showClashes ? ' & Clashes' : ''}`, {
-                kind: 'validation',
-                colorTheme: ValidationReport.Tag.GeometryQuality,
-                showClashes
-            }),
-        ]
-
-        if (model && modelHasMap(model.data)) {
-            validationItems.push(ActionMenu.Item('Density Fit Coloring', {
-                kind: 'validation',
-                colorTheme: ValidationReport.Tag.DensityFit,
-                showClashes: false
-            }))
-        }
-
-        if (model && modelFromNmr(model.data)) {
-            validationItems.push(ActionMenu.Item('Random Coil Index Coloring', {
-                kind: 'validation',
-                colorTheme: ValidationReport.Tag.RandomCoilIndex,
-                showClashes: false
-            }))
-        }
-
-        return [
-            ActionMenu.Item('Standard', {
-                kind: 'standard'
-            }),
-            ActionMenu.Item('Assembly Symmetry', {
-                kind: 'symmetry'
-            }),
-            validationItems
-        ] as unknown as ActionMenu.Spec
-    }
-
     defaultState() {
         return {
             isCollapsed: false,
@@ -371,13 +325,6 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         if (!this.getTrajectory() || !this.getAssembly()) return null
 
         return <div>
-            <div>
-                <div className='msp-control-row'>
-                    <ActionMenu.Toggle menu={this.actionMenu} items={this.getPresets()} label='Apply Preset' onSelect={this.applyPreset} disabled={this.state.isDisabled} />
-                </div>
-                <ActionMenu.Options menu={this.actionMenu} />
-            </div>
-
             <ParameterControls params={this.getParams()} values={this.values} onChange={this.onChange} isDisabled={this.state.isDisabled} />
         </div>
     }