Browse Source

Merge branch 'master' into graphics

Alexander Rose 6 years ago
parent
commit
55ff3cd7ab
72 changed files with 1041 additions and 645 deletions
  1. 1 1
      src/apps/basic-wrapper/index.ts
  2. 1 1
      src/apps/model-server-query/index.tsx
  3. 1 1
      src/apps/viewer/extensions/jolecule.ts
  4. 8 0
      src/examples/proteopedia-wrapper/changelog.md
  5. 106 0
      src/examples/proteopedia-wrapper/coloring.ts
  6. 18 1
      src/examples/proteopedia-wrapper/helpers.ts
  7. 58 4
      src/examples/proteopedia-wrapper/index.html
  8. 164 40
      src/examples/proteopedia-wrapper/index.ts
  9. 6 2
      src/mol-canvas3d/camera.ts
  10. 21 1
      src/mol-math/geometry/symmetry-operator.ts
  11. 2 2
      src/mol-model-formats/structure/mmcif/bonds/comp.ts
  12. 2 2
      src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
  13. 1 1
      src/mol-model-formats/structure/mmcif/parser.ts
  14. 2 2
      src/mol-model-props/common/custom-element-property.ts
  15. 2 2
      src/mol-model-props/common/custom-property-registry.ts
  16. 2 2
      src/mol-model-props/pdbe/preferred-assembly.ts
  17. 2 2
      src/mol-model-props/pdbe/struct-ref-domain.ts
  18. 2 2
      src/mol-model-props/pdbe/structure-quality-report.ts
  19. 2 2
      src/mol-model-props/rcsb/assembly-symmetry.ts
  20. 2 1
      src/mol-model/structure.ts
  21. 60 0
      src/mol-model/structure/common/custom-property.ts
  22. 34 21
      src/mol-model/structure/export/mmcif.ts
  23. 1 1
      src/mol-model/structure/model.ts
  24. 1 1
      src/mol-model/structure/model/model.ts
  25. 0 9
      src/mol-model/structure/model/properties/custom.ts
  26. 0 91
      src/mol-model/structure/model/properties/custom/chain.ts
  27. 0 27
      src/mol-model/structure/model/properties/custom/collection.ts
  28. 0 40
      src/mol-model/structure/model/properties/custom/descriptor.ts
  29. 0 91
      src/mol-model/structure/model/properties/custom/residue.ts
  30. 2 0
      src/mol-model/structure/query.ts
  31. 19 0
      src/mol-model/structure/query/queries/filters.ts
  32. 4 4
      src/mol-model/structure/query/queries/generators.ts
  33. 4 4
      src/mol-model/structure/query/queries/internal.ts
  34. 1 1
      src/mol-model/structure/query/selection.ts
  35. 2 2
      src/mol-model/structure/query/utils/structure-set.ts
  36. 72 12
      src/mol-model/structure/structure/structure.ts
  37. 8 3
      src/mol-model/structure/structure/symmetry.ts
  38. 1 1
      src/mol-model/structure/structure/util/subset-builder.ts
  39. 1 1
      src/mol-model/structure/structure/util/unique-subset-builder.ts
  40. 1 1
      src/mol-plugin/behavior/dynamic/labels.ts
  41. 6 6
      src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
  42. 4 4
      src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
  43. 6 6
      src/mol-plugin/behavior/static/representation.ts
  44. 3 2
      src/mol-plugin/behavior/static/state.ts
  45. 1 1
      src/mol-plugin/command.ts
  46. 1 1
      src/mol-plugin/context.ts
  47. 4 1
      src/mol-plugin/index.ts
  48. 1 1
      src/mol-plugin/state.ts
  49. 14 5
      src/mol-plugin/state/actions/structure.ts
  50. 54 4
      src/mol-plugin/state/animation/built-in.ts
  51. 49 1
      src/mol-plugin/state/transforms/model.ts
  52. 14 1
      src/mol-plugin/state/transforms/representation.ts
  53. 4 1
      src/mol-plugin/ui/base.tsx
  54. 1 3
      src/mol-plugin/ui/controls/parameters.tsx
  55. 60 60
      src/mol-plugin/ui/state/tree.tsx
  56. 0 1
      src/mol-repr/structure/units-representation.ts
  57. 5 1
      src/mol-repr/structure/units-visual.ts
  58. 4 0
      src/mol-script/language/symbol-table/structure-query.ts
  59. 2 2
      src/mol-script/runtime/query/compiler.ts
  60. 3 0
      src/mol-script/runtime/query/table.ts
  61. 1 0
      src/mol-script/script/mol-script/symbols.ts
  62. 0 7
      src/mol-state/manager.ts
  63. 4 19
      src/mol-state/object.ts
  64. 27 45
      src/mol-state/state.ts
  65. 25 11
      src/mol-state/state/builder.ts
  66. 11 3
      src/mol-state/state/selection.ts
  67. 87 14
      src/mol-state/transform.ts
  68. 4 1
      src/mol-state/transformer.ts
  69. 12 18
      src/mol-state/tree/immutable.ts
  70. 17 44
      src/mol-state/tree/transient.ts
  71. 1 1
      src/mol-util/param-definition.ts
  72. 2 2
      src/perf-tests/mol-script.ts

+ 1 - 1
src/apps/basic-wrapper/index.ts

@@ -50,7 +50,7 @@ class BasicWrapper {
 
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 

+ 1 - 1
src/apps/model-server-query/index.tsx

@@ -105,7 +105,7 @@ const state: State = {
     query: new Rx.BehaviorSubject(QueryList[1].definition),
     id: new Rx.BehaviorSubject('1cbs'),
     params: new Rx.BehaviorSubject({ }),
-    isBinary: new Rx.BehaviorSubject(false),
+    isBinary: new Rx.BehaviorSubject<boolean>(false),
     models: new Rx.BehaviorSubject<number[]>([]),
     url: new Rx.Subject()
 }

+ 1 - 1
src/apps/viewer/extensions/jolecule.ts

@@ -57,7 +57,7 @@ interface JoleculeSnapshot {
 
 function createTemplate(plugin: PluginContext, state: State, id: string) {
     const b = new StateBuilder.Root(state.tree);
-    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }});
+    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { state: { isGhost: true }});
     const model = createModelTree(data, 'cif');
     const structure = model.apply(StateTransforms.Model.StructureFromModel, {});
     complexRepresentation(plugin, structure, { hideWater: true });

+ 8 - 0
src/examples/proteopedia-wrapper/changelog.md

@@ -1,3 +1,11 @@
+== v3.0 ==
+
+* Fixed initial camera zoom.
+* Custom chain coloring.
+* Customize visualizations.
+* Show ligand list.
+* Show 3D-SNFG.
+
 == v2.0 ==
 
 * Changed how state saving works.

+ 106 - 0
src/examples/proteopedia-wrapper/coloring.ts

@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2019 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 { Unit, StructureProperties, StructureElement, Link } from 'mol-model/structure';
+
+import { Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { ColorTheme, LocationColor } from 'mol-theme/color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from 'mol-theme/theme';
+import { Column } from 'mol-data/db';
+
+const Description = 'Gives every chain a color from a list based on its `asym_id` value.'
+
+export function createProteopediaCustomTheme(colors: number[]) {
+    const ProteopediaCustomColorThemeParams = {
+        colors: PD.ObjectList({ color: PD.Color(Color(0xffffff)) }, ({ color }) => Color.toHexString(color),
+            { defaultValue: colors.map(c => ({ color: Color(c) })) })
+    }
+    type ProteopediaCustomColorThemeParams = typeof ProteopediaCustomColorThemeParams
+    function getChainIdColorThemeParams(ctx: ThemeDataContext) {
+        return ProteopediaCustomColorThemeParams // TODO return copy
+    }
+
+    function getAsymId(unit: Unit): StructureElement.Property<string> {
+        switch (unit.kind) {
+            case Unit.Kind.Atomic:
+                return StructureProperties.chain.label_asym_id
+            case Unit.Kind.Spheres:
+            case Unit.Kind.Gaussians:
+                return StructureProperties.coarse.asym_id
+        }
+    }
+
+    function addAsymIds(map: Map<string, number>, data: Column<string>) {
+        let j = map.size
+        for (let o = 0, ol = data.rowCount; o < ol; ++o) {
+            const k = data.value(o)
+            if (!map.has(k)) {
+                map.set(k, j)
+                j += 1
+            }
+        }
+    }
+
+    function ProteopediaCustomColorTheme(ctx: ThemeDataContext, props: PD.Values<ProteopediaCustomColorThemeParams>): ColorTheme<ProteopediaCustomColorThemeParams> {
+        let color: LocationColor
+
+        const colors = props.colors, colorCount = colors.length, defaultColor = colors[0].color;
+
+        if (ctx.structure) {
+            const l = StructureElement.create()
+            const { models } = ctx.structure
+            const asymIdSerialMap = new Map<string, number>()
+            for (let i = 0, il = models.length; i < il; ++i) {
+                const m = models[i]
+                addAsymIds(asymIdSerialMap, m.atomicHierarchy.chains.label_asym_id)
+                if (m.coarseHierarchy.isDefined) {
+                    addAsymIds(asymIdSerialMap, m.coarseHierarchy.spheres.asym_id)
+                    addAsymIds(asymIdSerialMap, m.coarseHierarchy.gaussians.asym_id)
+                }
+            }
+
+            color = (location: Location): Color => {
+                if (StructureElement.isLocation(location)) {
+                    const asym_id = getAsymId(location.unit);
+                    const o = asymIdSerialMap.get(asym_id(location)) || 0;
+                    return colors[o % colorCount].color;
+                } else if (Link.isLocation(location)) {
+                    const asym_id = getAsymId(location.aUnit)
+                    l.unit = location.aUnit
+                    l.element = location.aUnit.elements[location.aIndex]
+                    const o = asymIdSerialMap.get(asym_id(l)) || 0;
+                    return colors[o % colorCount].color;
+                }
+                return defaultColor
+            }
+        } else {
+            color = () => defaultColor
+        }
+
+        return {
+            factory: ProteopediaCustomColorTheme,
+            granularity: 'group',
+            color,
+            props,
+            description: Description,
+            legend: undefined
+        }
+    }
+
+    const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = {
+        label: 'Proteopedia Custom',
+        factory: ProteopediaCustomColorTheme,
+        getParams: getChainIdColorThemeParams,
+        defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams),
+        isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+    }
+
+    return ProteopediaCustomColorThemeProvider;
+}

+ 18 - 1
src/examples/proteopedia-wrapper/helpers.ts

@@ -92,9 +92,26 @@ export interface LoadParams {
 export interface RepresentationStyle {
     sequence?: RepresentationStyle.Entry,
     hetGroups?: RepresentationStyle.Entry,
+    snfg3d?: { hide?: boolean },
     water?: RepresentationStyle.Entry
 }
 
 export namespace RepresentationStyle {
-    export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName }
+    export type Entry = { hide?: boolean, kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName }
+}
+
+export enum StateElements {
+    Model = 'model',
+    ModelProps = 'model-props',
+    Assembly = 'assembly',
+
+    Sequence = 'sequence',
+    SequenceVisual = 'sequence-visual',
+    Het = 'het',
+    HetVisual = 'het-visual',
+    Het3DSNFG = 'het-3dsnfg',
+    Water = 'water',
+    WaterVisual = 'water-visual',
+
+    HetGroupFocus = 'het-group-focus'
 }

+ 58 - 4
src/examples/proteopedia-wrapper/index.html

@@ -55,11 +55,14 @@
             </select>
         </div>
         <div id="app"></div>
-        <script>  
+        <script>
+            // it might be a good idea to define these colors in a separate script file 
+            var CustomColors = [0x00ff00, 0x0000ff];
+
             // create an instance of the plugin
             var PluginWrapper = new MolStarProteopediaWrapper();
 
-            console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR);
+            console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR, MolStarProteopediaWrapper.VERSION_MINOR);
 
             function $(id) { return document.getElementById(id); }
         
@@ -78,13 +81,23 @@
             // var format = 'pdb';
             // var assemblyId = 'deposited';
 
-            PluginWrapper.init('app' /** or document.getElementById('app') */);
+            var representationStyle = {
+                sequence: { coloring: 'proteopedia-custom' }, // or just { }
+                hetGroups: { kind: 'ball-and-stick' }, // or 'spacefill
+                water: { hide: true },
+                snfg3d: { hide: false }
+            };
+
+            PluginWrapper.init('app' /** or document.getElementById('app') */, {
+                customColorList: CustomColors
+            });
             PluginWrapper.setBackground(0xffffff);
-            PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId });
+            PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId, representationStyle: representationStyle });
             PluginWrapper.toggleSpin();
 
             PluginWrapper.events.modelInfo.subscribe(function (info) {
                 console.log('Model Info', info);
+                listHetGroups(info);
             });
 
             addControl('Load Asym Unit', () => PluginWrapper.load({ url: url, format: format }));
@@ -92,6 +105,22 @@
 
             addSeparator();
 
+            addHeader('Representation');
+
+            addControl('Custom Chain Colors', () => PluginWrapper.updateStyle({ sequence: { coloring: 'proteopedia-custom' } }, true));
+            addControl('Default Chain Colors', () => PluginWrapper.updateStyle({ sequence: { } }, true));
+
+            addControl('HET Spacefill', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'spacefill' } }, true));
+            addControl('HET Ball-and-stick', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'ball-and-stick' } }, true));
+
+            addControl('Hide 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: true } }, true));
+            addControl('Show 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: false } }, true));
+
+            addControl('Hide Water', () => PluginWrapper.updateStyle({ water: { hide: true } }, true));
+            addControl('Show Water', () => PluginWrapper.updateStyle({ water: { hide: false } }, true));
+
+            addSeparator();
+
             addHeader('Camera');
             addControl('Toggle Spin', () => PluginWrapper.toggleSpin());
             
@@ -115,6 +144,12 @@
             addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation());
             addControl('Default Visuals', () => PluginWrapper.updateStyle());
 
+            addSeparator();
+            addHeader('HET Groups');
+
+            addControl('Reset', () => PluginWrapper.hetGroups.reset());
+            addHetGroupsContainer();
+
             addSeparator();
             addHeader('State');
 
@@ -133,6 +168,12 @@
 
             ////////////////////////////////////////////////////////
 
+            function addHetGroupsContainer() {
+                var div = document.createElement('div');
+                div.id = 'het-groups';
+                $('controls').appendChild(div);
+            }
+
             function addControl(label, action) {
                 var btn = document.createElement('button');
                 btn.onclick = action;
@@ -150,6 +191,19 @@
                 h.innerText = header;
                 $('controls').appendChild(h);
             }
+
+            function listHetGroups(info) {
+                var div = $('het-groups');
+                div.innerHTML = '';
+                info.hetResidues.forEach(function (r) {
+                    var l = document.createElement('button');
+                    l.innerText = r.name;
+                    l.onclick = function () {
+                        PluginWrapper.hetGroups.focusFirst(r.name);
+                    };
+                    div.appendChild(l);
+                });
+            }
         </script>
     </body>
 </html>

+ 164 - 40
src/examples/proteopedia-wrapper/index.ts

@@ -15,15 +15,25 @@ import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/ob
 import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
 import { StateBuilder, StateObject } from 'mol-state';
 import { EvolutionaryConservation } from './annotation';
-import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers';
+import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo, StateElements } from './helpers';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { ControlsWrapper } from './ui/controls';
 import { PluginState } from 'mol-plugin/state';
+import { Scheduler } from 'mol-task';
+import { createProteopediaCustomTheme } from './coloring';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
+import { BuiltInStructureRepresentations } from 'mol-repr/structure/registry';
+import { BuiltInColorThemes } from 'mol-theme/color';
+import { BuiltInSizeThemes } from 'mol-theme/size';
+import { ColorNames } from 'mol-util/color/tables';
+// import { Vec3 } from 'mol-math/linear-algebra';
+// import { ParamDefinition } from 'mol-util/param-definition';
+// import { Text } from 'mol-geo/geometry/text/text';
 require('mol-plugin/skin/light.scss')
 
 class MolStarProteopediaWrapper {
-    static VERSION_MAJOR = 2;
-    static VERSION_MINOR = 0;
+    static VERSION_MAJOR = 3;
+    static VERSION_MINOR = 1;
 
     private _ev = RxEventHelper.create();
 
@@ -33,9 +43,14 @@ class MolStarProteopediaWrapper {
 
     plugin: PluginContext;
 
-    init(target: string | HTMLElement) {
+    init(target: string | HTMLElement, options?: {
+        customColorList?: number[]
+    }) {
         this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
             ...DefaultPluginSpec,
+            animations: [
+                AnimateModelIndex
+            ],
             layout: {
                 initial: {
                     isExpanded: false,
@@ -47,6 +62,9 @@ class MolStarProteopediaWrapper {
             }
         });
 
+        const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []);
+
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add('proteopedia-custom', customColoring);
         this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!);
         this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider);
         this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider);
@@ -66,43 +84,86 @@ class MolStarProteopediaWrapper {
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
         return parsed
-            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' });
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: StateElements.Model });
     }
 
     private structure(assemblyId: string) {
-        const model = this.state.build().to('model');
+        const model = this.state.build().to(StateElements.Model);
+
+        const s = model
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
 
-        return model
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: StateElements.Water });
+
+        return s;
     }
 
-    private visual(ref: string, style?: RepresentationStyle) {
-        const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref);
+    private visual(_style?: RepresentationStyle, partial?: boolean) {
+        const structure = this.getObj<PluginStateObject.Molecule.Structure>(StateElements.Assembly);
         if (!structure) return;
 
-        const root = this.state.build().to(ref);
-
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.sequence && style.sequence.kind) || 'cartoon',
-                    (style && style.sequence && style.sequence.coloring) || 'unit-index', structure),
-                    { ref: 'sequence-visual' });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
-                    (style && style.hetGroups && style.hetGroups.coloring), structure),
-                    { ref: 'het-visual' });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.water && style.water.kind) || 'ball-and-stick',
-                    (style && style.water && style.water.coloring), structure, { alpha: 0.51 }),
-                    { ref: 'water-visual' });
-
-        return root;
+        const style = _style || { };
+
+        const update = this.state.build();
+
+        if (!partial || (partial && style.sequence)) {
+            const root = update.to(StateElements.Sequence);
+            if (style.sequence && style.sequence.hide) {
+                root.delete(StateElements.SequenceVisual);
+            } else {
+                root.applyOrUpdate(StateElements.SequenceVisual, StateTransforms.Representation.StructureRepresentation3D,
+                    StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                        (style.sequence && style.sequence.kind) || 'cartoon',
+                        (style.sequence && style.sequence.coloring) || 'unit-index', structure));
+            }
+        }
+
+        if (!partial || (partial && style.hetGroups)) {
+            const root = update.to(StateElements.Het);
+            if (style.hetGroups && style.hetGroups.hide) {
+                root.delete(StateElements.HetVisual);
+            } else {
+                if (style.hetGroups && style.hetGroups.hide) {
+                    root.delete(StateElements.HetVisual);
+                } else {
+                    root.applyOrUpdate(StateElements.HetVisual, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                            (style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
+                            (style.hetGroups && style.hetGroups.coloring), structure));
+                }
+            }
+        }
+
+        if (!partial || (partial && style.snfg3d)) {
+            const root = update.to(StateElements.Het);
+            if (style.hetGroups && style.hetGroups.hide) {
+                root.delete(StateElements.HetVisual);
+            } else {
+                if (style.snfg3d && style.snfg3d.hide) {
+                    root.delete(StateElements.Het3DSNFG);
+                } else {
+                    root.applyOrUpdate(StateElements.Het3DSNFG, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, 'carbohydrate', void 0, structure));
+                }
+            }
+        }
+
+        if (!partial || (partial && style.water)) {
+            const root = update.to(StateElements.Water);
+            if (style.water && style.water.hide) {
+                root.delete(StateElements.WaterVisual);
+            } else {
+                root.applyOrUpdate(StateElements.WaterVisual, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                            (style.water && style.water.kind) || 'ball-and-stick',
+                            (style.water && style.water.coloring), structure, { alpha: 0.51 }));
+            }
+        }
+
+        return update;
     }
 
     private getObj<T extends StateObject>(ref: string): T['data'] {
@@ -134,7 +195,7 @@ class MolStarProteopediaWrapper {
         if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
             loadType = 'full';
         } else if (this.loadedParams.url === url) {
-            if (state.select('asm').length > 0) loadType = 'update';
+            if (state.select(StateElements.Assembly).length > 0) loadType = 'update';
         }
 
         if (loadType === 'full') {
@@ -146,18 +207,18 @@ class MolStarProteopediaWrapper {
             await this.applyState(structureTree);
         } else {
             const tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
             await this.applyState(tree);
         }
 
         await this.updateStyle(representationStyle);
 
         this.loadedParams = { url, format, assemblyId };
-        PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+        Scheduler.setImmediate(() => PluginCommands.Camera.Reset.dispatch(this.plugin, { }));
     }
 
-    async updateStyle(style?: RepresentationStyle) {
-        const tree = this.visual('asm', style);
+    async updateStyle(style?: RepresentationStyle, partial?: boolean) {
+        const tree = this.visual(style, partial);
         if (!tree) return;
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
     }
@@ -186,7 +247,7 @@ class MolStarProteopediaWrapper {
 
     coloring = {
         evolutionaryConservation: async () => {
-            await this.updateStyle({ sequence: { kind: 'spacefill' } });
+            await this.updateStyle({ sequence: { kind: 'spacefill' } }, true);
 
             const state = this.state;
 
@@ -194,7 +255,7 @@ class MolStarProteopediaWrapper {
             const tree = state.build();
             const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
 
-            tree.to('sequence-visual').update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
+            tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
             // for (const v of visuals) {
             // }
 
@@ -202,6 +263,69 @@ class MolStarProteopediaWrapper {
         }
     }
 
+    hetGroups = {
+        reset: () => {
+            const update = this.state.build().delete(StateElements.HetGroupFocus);
+            PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update });
+            PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+        },
+        focusFirst: async (resn: string) => {
+            if (!this.state.transforms.has(StateElements.Assembly)) return;
+
+            // const asm = (this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure).data;
+
+            const update = this.state.build();
+
+            update.delete(StateElements.HetGroupFocus);
+
+            const surroundings = MS.struct.modifier.includeSurroundings({
+                0: MS.struct.filter.first([
+                    MS.struct.generator.atomGroups({
+                        'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), resn]),
+                        'group-by': MS.struct.atomProperty.macromolecular.residueKey()
+                    })
+                ]),
+                radius: 5,
+                'as-whole-residues': true
+            });
+
+            const sel = update.to(StateElements.Assembly)
+                .apply(StateTransforms.Model.StructureSelection, { label: resn, query: surroundings }, { ref: StateElements.HetGroupFocus });
+
+            sel.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams());
+            // sel.apply(StateTransforms.Representation.StructureLabels3D, {
+            //     target: { name: 'residues', params: { } },
+            //     options: {
+            //         ...ParamDefinition.getDefaultValues(Text.Params),
+            //         background: true,
+            //         backgroundMargin: 0.2,
+            //         backgroundColor: ColorNames.snow,
+            //         backgroundOpacity: 0.9,
+            //     }
+            // });
+
+            await PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update });
+
+            const focus = (this.state.select(StateElements.HetGroupFocus)[0].obj as PluginStateObject.Molecule.Structure).data;
+            const sphere = focus.boundary.sphere;
+            // const asmCenter = asm.boundary.sphere.center;
+            // const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter);
+            // Vec3.normalize(position, position);
+            // Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius);
+            const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, 0.75 * sphere.radius);
+            PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 });
+        }
+    }
+
+    private createSurVisualParams() {
+        const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure;
+        return StructureRepresentation3DHelpers.createParams(this.plugin, asm.data, {
+            repr: BuiltInStructureRepresentations['ball-and-stick'],
+            color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })],
+            size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )]
+        });
+    }
+
     snapshot = {
         get: () => {
             return this.plugin.state.getSnapshot();

+ 6 - 2
src/mol-canvas3d/camera.ts

@@ -84,7 +84,7 @@ class Camera implements Object3D {
         return ret;
     }
 
-    focus(target: Vec3, radius: number) {
+    getFocus(target: Vec3, radius: number): Partial<Camera.Snapshot> {
         const fov = this.state.fov
         const { width, height } = this.viewport
         const aspect = width / height
@@ -98,7 +98,11 @@ class Camera implements Object3D {
         if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection)
         Vec3.add(this.newPosition, this.state.position, this.deltaDirection)
 
-        this.setState({ target, position: this.newPosition })
+        return { target, position: Vec3.clone(this.newPosition) };
+    }
+
+    focus(target: Vec3, radius: number) {
+        this.setState(this.getFocus(target, radius));
     }
 
     // lookAt(target: Vec3) {

+ 21 - 1
src/mol-math/geometry/symmetry-operator.ts

@@ -5,6 +5,7 @@
  */
 
 import { Vec3, Mat4, Mat3, Quat } from '../linear-algebra/3d'
+import { lerp as scalar_lerp } from 'mol-math/interpolate';
 
 interface SymmetryOperator {
     readonly name: string,
@@ -64,7 +65,7 @@ namespace SymmetryOperator {
         return create(name, t, { id: '', operList: [] }, ncsId);
     }
 
-    const _q1 = Quat.identity(), _q2 = Quat.zero(), _axis = Vec3.zero();
+    const _q1 = Quat.identity(), _q2 = Quat.zero(), _q3 = Quat.zero(), _axis = Vec3.zero();
     export function lerpFromIdentity(out: Mat4, op: SymmetryOperator, t: number): Mat4 {
         const m = op.inverse;
         if (op.isIdentity) return Mat4.copy(out, m);
@@ -84,6 +85,25 @@ namespace SymmetryOperator {
         return out;
     }
 
+    export function slerp(out: Mat4, src: Mat4, tar: Mat4, t: number): Mat4 {
+        if (Math.abs(t) <= 0.00001) return Mat4.copy(out, src);
+        if (Math.abs(t - 1) <= 0.00001) return Mat4.copy(out, tar);
+
+        // interpolate rotation
+        Mat4.getRotation(_q2, src);
+        Mat4.getRotation(_q3, tar);
+        Quat.slerp(_q3, _q2, _q3, t);
+        const angle = Quat.getAxisAngle(_axis, _q3);
+        Mat4.fromRotation(out, angle, _axis);
+
+        // interpolate translation
+        Mat4.setValue(out, 0, 3, scalar_lerp(Mat4.getValue(src, 0, 3), Mat4.getValue(tar, 0, 3), t));
+        Mat4.setValue(out, 1, 3, scalar_lerp(Mat4.getValue(src, 1, 3), Mat4.getValue(tar, 1, 3), t));
+        Mat4.setValue(out, 2, 3, scalar_lerp(Mat4.getValue(src, 2, 3), Mat4.getValue(tar, 2, 3), t));
+
+        return out;
+    }
+
     /**
      * Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix).
      * Keep `name`, `assembly`, `ncsId` and `hkl` properties from second.

+ 2 - 2
src/mol-model-formats/structure/mmcif/bonds/comp.ts

@@ -7,7 +7,7 @@
 
 import { Model } from 'mol-model/structure/model/model'
 import { LinkType } from 'mol-model/structure/model/types'
-import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
+import { CustomPropertyDescriptor } from 'mol-model/structure';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import { Structure, Unit, StructureProperties, StructureElement } from 'mol-model/structure';
 import { Segmentation } from 'mol-data/int';
@@ -18,7 +18,7 @@ export interface ComponentBond {
 }
 
 export namespace ComponentBond {
-    export const Descriptor: ModelPropertyDescriptor = {
+    export const Descriptor: CustomPropertyDescriptor = {
         isStatic: true,
         name: 'chem_comp_bond',
         cifExport: {

+ 2 - 2
src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts

@@ -10,7 +10,7 @@ import { Structure } from 'mol-model/structure'
 import { LinkType } from 'mol-model/structure/model/types'
 import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util'
 import { Column } from 'mol-data/db'
-import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
+import { CustomPropertyDescriptor } from 'mol-model/structure';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { SortedArray } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
@@ -23,7 +23,7 @@ export interface StructConn {
 }
 
 export namespace StructConn {
-    export const Descriptor: ModelPropertyDescriptor = {
+    export const Descriptor: CustomPropertyDescriptor = {
         isStatic: true,
         name: 'struct_conn',
         cifExport: {

+ 1 - 1
src/mol-model-formats/structure/mmcif/parser.ts

@@ -13,7 +13,7 @@ import { RuntimeContext } from 'mol-task';
 import UUID from 'mol-util/uuid';
 import { Model } from 'mol-model/structure/model/model';
 import { Entities } from 'mol-model/structure/model/properties/common';
-import { CustomProperties } from 'mol-model/structure/model/properties/custom';
+import { CustomProperties } from 'mol-model/structure';
 import { ModelSymmetry } from 'mol-model/structure/model/properties/symmetry';
 import { createAssemblies } from './assembly';
 import { getAtomicHierarchyAndConformation } from './atomic';

+ 2 - 2
src/mol-model-props/common/custom-element-property.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ElementIndex, Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { ElementIndex, Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { StructureElement } from 'mol-model/structure/structure';
 import { Location } from 'mol-model/location';
 import { CustomPropertyRegistry } from './custom-property-registry';
@@ -36,7 +36,7 @@ namespace CustomElementProperty {
     export function create<T>(params: CreateParams<T>) {
         const name = params.name;
 
-        const Descriptor = ModelPropertyDescriptor({
+        const Descriptor = CustomPropertyDescriptor({
             isStatic: params.isStatic,
             name: params.name,
         });

+ 2 - 2
src/mol-model-props/common/custom-property-registry.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ModelPropertyDescriptor, Model } from 'mol-model/structure';
+import { CustomPropertyDescriptor, Model } from 'mol-model/structure';
 import { OrderedMap } from 'immutable';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { Task } from 'mol-task';
@@ -58,7 +58,7 @@ namespace CustomPropertyRegistry {
     export interface Provider {
         option: [string, string],
         defaultSelected: boolean,
-        descriptor: ModelPropertyDescriptor<any, any>,
+        descriptor: CustomPropertyDescriptor<any, any>,
         attachableTo: (model: Model) => boolean,
         attach: (model: Model) => Task<boolean>
     }

+ 2 - 2
src/mol-model-props/pdbe/preferred-assembly.ts

@@ -7,7 +7,7 @@
 import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 
 export namespace PDBePreferredAssembly {
     export type Property = string
@@ -31,7 +31,7 @@ export namespace PDBePreferredAssembly {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: true,
         name: 'pdbe_preferred_assembly',
         cifExport: {

+ 2 - 2
src/mol-model-props/pdbe/struct-ref-domain.ts

@@ -7,7 +7,7 @@
 import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { PropertyWrapper } from '../common/wrapper';
 
 export namespace PDBeStructRefDomain {
@@ -39,7 +39,7 @@ export namespace PDBeStructRefDomain {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: true,
         name: 'pdbe_struct_ref_domain',
         cifExport: {

+ 2 - 2
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -8,7 +8,7 @@ import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure';
 import { residueIdFields } from 'mol-model/structure/export/categories/atom_site';
 import { StructureElement, CifExportContext } from 'mol-model/structure/structure';
 import { CustomPropSymbol } from 'mol-script/language/symbol';
@@ -43,7 +43,7 @@ export namespace StructureQualityReport {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: false,
         name: 'pdbe_structure_quality_report',
         cifExport: {

+ 2 - 2
src/mol-model-props/rcsb/assembly-symmetry.ts

@@ -7,7 +7,7 @@
 import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types';
 import query from './graphql/symmetry.gql';
 
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { CifWriter } from 'mol-io/writer/cif';
 import { Database as _Database, Column, Table } from 'mol-data/db'
 import { Category } from 'mol-io/writer/cif/encoder';
@@ -140,7 +140,7 @@ function createDatabaseFromCif(model: Model): AssemblySymmetry.Database {
     })
 }
 
-const _Descriptor: ModelPropertyDescriptor = {
+const _Descriptor: CustomPropertyDescriptor = {
     isStatic: true,
     name: 'rcsb_assembly_symmetry',
     cifExport: {

+ 2 - 1
src/mol-model/structure.ts

@@ -6,4 +6,5 @@
 
 export * from './structure/model'
 export * from './structure/structure'
-export * from './structure/query'
+export * from './structure/query'
+export * from './structure/common/custom-property'

+ 60 - 0
src/mol-model/structure/common/custom-property.ts

@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifWriter } from 'mol-io/writer/cif'
+import { CifExportContext } from '../export/mmcif';
+import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
+import { UUID } from 'mol-util';
+
+export { CustomPropertyDescriptor, CustomProperties }
+
+interface CustomPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> {
+    readonly isStatic: boolean,
+    readonly name: string,
+
+    cifExport?: {
+        // Prefix enforced during export.
+        prefix: string,
+        context?: (ctx: CifExportContext) => ExportCtx | undefined,
+        categories: CifWriter.Category<ExportCtx>[]
+    },
+
+    // TODO: add aliases when lisp-like mol-script is done
+    symbols?: Symbols
+}
+
+function CustomPropertyDescriptor<Ctx, Desc extends CustomPropertyDescriptor<Ctx>>(desc: Desc) {
+    return desc;
+}
+
+namespace CustomPropertyDescriptor {
+    export function getUUID(prop: CustomPropertyDescriptor): UUID {
+        if (!(prop as any).__key) {
+            (prop as any).__key = UUID.create22();
+        }
+        return (prop as any).__key;
+    }
+}
+
+class CustomProperties {
+    private _list: CustomPropertyDescriptor[] = [];
+    private _set = new Set<CustomPropertyDescriptor>();
+
+    get all(): ReadonlyArray<CustomPropertyDescriptor> {
+        return this._list;
+    }
+
+    add(desc: CustomPropertyDescriptor<any>) {
+        if (this._set.has(desc)) return;
+
+        this._list.push(desc);
+        this._set.add(desc);
+    }
+
+    has(desc: CustomPropertyDescriptor<any>): boolean {
+        return this._set.has(desc);
+    }
+}

+ 34 - 21
src/mol-model/structure/export/mmcif.ts

@@ -16,7 +16,7 @@ import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './
 import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils';
 import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence';
-import { ModelPropertyDescriptor } from '../model/properties/custom';
+import { CustomPropertyDescriptor } from '../common/custom-property';
 
 export interface CifExportContext {
     structures: Structure[],
@@ -100,8 +100,32 @@ export const mmCIF_Export_Filters = {
     }
 }
 
+function encodeCustomProp(customProp: CustomPropertyDescriptor, ctx: CifExportContext, encoder: CifWriter.Encoder, params: encode_mmCIF_categories_Params) {
+    if (!customProp.cifExport || customProp.cifExport.categories.length === 0) return;
+
+    const prefix = customProp.cifExport.prefix;
+    const cats = customProp.cifExport.categories;
+
+    let propCtx = ctx;
+    if (customProp.cifExport.context) {
+        const propId = CustomPropertyDescriptor.getUUID(customProp);
+        if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx'];
+        else {
+            propCtx = customProp.cifExport.context(ctx) || ctx;
+            ctx.cache[propId + '__ctx'] = propCtx;
+        }
+    }
+    for (const cat of cats) {
+        if (params.skipCategoryNames && params.skipCategoryNames.has(cat.name)) continue;
+        if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`);
+        encoder.writeCategory(cat, propCtx);
+    }
+}
+
+type encode_mmCIF_categories_Params = { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext }
+
 /** Doesn't start a data block */
-export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext }) {
+export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: encode_mmCIF_categories_Params) {
     const first = Array.isArray(structures) ? structures[0] : (structures as Structure);
     const models = first.models;
     if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.';
@@ -115,26 +139,15 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures:
     }
 
     for (const customProp of models[0].customProperties.all) {
-        if (!customProp.cifExport || customProp.cifExport.categories.length === 0) continue;
-
-        const prefix = customProp.cifExport.prefix;
-        const cats = customProp.cifExport.categories;
-
-        let propCtx = ctx;
-        if (customProp.cifExport.context) {
-            const propId = ModelPropertyDescriptor.getUUID(customProp);
-            if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx'];
-            else {
-                propCtx = customProp.cifExport.context(ctx) || ctx;
-                ctx.cache[propId + '__ctx'] = propCtx;
-            }
-        }
-        for (const cat of cats) {
-            if (_params.skipCategoryNames && _params.skipCategoryNames.has(cat.name)) continue;
-            if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`);
-            encoder.writeCategory(cat, propCtx);
-        }
+        encodeCustomProp(customProp, ctx, encoder, _params);
+    }
+
+    const structureCustomProps = new Set<CustomPropertyDescriptor>();
+    for (const s of ctx.structures) {
+        if (!s.hasCustomProperties) continue;
+        for (const p of s.customPropertyDescriptors.all) structureCustomProps.add(p);
     }
+    structureCustomProps.forEach(customProp => encodeCustomProp(customProp, ctx, encoder, _params));
 }
 
 function to_mmCIF(name: string, structure: Structure, asBinary = false) {

+ 1 - 1
src/mol-model/structure/model.ts

@@ -9,6 +9,6 @@ import * as Types from './model/types'
 import { ModelSymmetry } from './model/properties/symmetry'
 import StructureSequence from './model/properties/sequence'
 
-export * from './model/properties/custom'
+export * from './model/properties/custom/indexed'
 export * from './model/indexing'
 export { Model, Types, ModelSymmetry, StructureSequence }

+ 1 - 1
src/mol-model/structure/model/model.ts

@@ -10,7 +10,7 @@ import { AtomicHierarchy, AtomicConformation } from './properties/atomic';
 import { ModelSymmetry } from './properties/symmetry';
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
 import { Entities } from './properties/common';
-import { CustomProperties } from './properties/custom';
+import { CustomProperties } from '../common/custom-property';
 import { SecondaryStructure } from './properties/seconday-structure';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from 'mol-model-formats/structure/format';

+ 0 - 9
src/mol-model/structure/model/properties/custom.ts

@@ -1,9 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-export * from './custom/descriptor'
-export * from './custom/collection'
-export * from './custom/indexed'

+ 0 - 91
src/mol-model/structure/model/properties/custom/chain.ts

@@ -1,91 +0,0 @@
-// /**
-//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
-//  *
-//  * @author David Sehnal <david.sehnal@gmail.com>
-//  */
-
-// import { ChainIndex } from '../../indexing';
-// import { Unit, Structure, StructureElement } from '../../../structure';
-// import { Segmentation } from 'mol-data/int';
-// import { UUID } from 'mol-util';
-// import { CifWriter } from 'mol-io/writer/cif';
-
-// export interface ChainCustomProperty<T = any> {
-//     readonly id: UUID,
-//     readonly kind: Unit.Kind,
-//     has(idx: ChainIndex): boolean
-//     get(idx: ChainIndex): T | undefined
-// }
-
-// export namespace ChainCustomProperty {
-//     export interface ExportCtx<T> {
-//         elements: StructureElement[],
-//         property(index: number): T
-//     };
-
-//     function getExportCtx<T>(prop: ChainCustomProperty<T>, structure: Structure): ExportCtx<T> {
-//         const chainIndex = structure.model.atomicHierarchy.chainAtomSegments.index;
-//         const elements = getStructureElements(structure, prop);
-//         return { elements, property: i => prop.get(chainIndex[elements[i].element])! };
-//     }
-
-//     export function getCifDataSource<T>(structure: Structure, prop: ChainCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] {
-//         if (!prop) return { rowCount: 0 };
-//         if (cache && cache[prop.id]) return cache[prop.id];
-//         const data = getExportCtx(prop, structure);
-//         const ret = { data, rowCount: data.elements.length };
-//         if (cache) cache[prop.id] = ret;
-//         return ret;
-//     }
-
-//     class FromMap<T> implements ChainCustomProperty<T> {
-//         readonly id = UUID.create();
-
-//         has(idx: ChainIndex): boolean {
-//             return this.map.has(idx);
-//         }
-
-//         get(idx: ChainIndex) {
-//             return this.map.get(idx);
-//         }
-
-//         constructor(private map: Map<ChainIndex, T>, public kind: Unit.Kind) {
-//         }
-//     }
-
-//     export function fromMap<T>(map: Map<ChainIndex, T>, kind: Unit.Kind) {
-//         return new FromMap(map, kind);
-//     }
-
-//     /**
-//      * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned.
-//      * Only works correctly for structures with a single model.
-//      */
-//     export function getStructureElements(structure: Structure, property: ChainCustomProperty) {
-//         const models = structure.models;
-//         if (models.length !== 1) throw new Error(`Only works on structures with a single model.`);
-
-//         const seenChains = new Set<ChainIndex>();
-//         const unitGroups = structure.unitSymmetryGroups;
-//         const loci: StructureElement[] = [];
-
-//         for (const unitGroup of unitGroups) {
-//             const unit = unitGroup.units[0];
-//             if (unit.kind !== property.kind) {
-//                 continue;
-//             }
-
-//             const chains = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
-//             while (chains.hasNext) {
-//                 const seg = chains.move();
-//                 if (!property.has(seg.index) || seenChains.has(seg.index)) continue;
-
-//                 seenChains.add(seg.index);
-//                 loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
-//             }
-//         }
-
-//         loci.sort((x, y) => x.element - y.element);
-//         return loci;
-//     }
-// }

+ 0 - 27
src/mol-model/structure/model/properties/custom/collection.ts

@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { ModelPropertyDescriptor } from './descriptor'
-
-export class CustomProperties {
-    private _list: ModelPropertyDescriptor[] = [];
-    private _set = new Set<ModelPropertyDescriptor>();
-
-    get all(): ReadonlyArray<ModelPropertyDescriptor> {
-        return this._list;
-    }
-
-    add(desc: ModelPropertyDescriptor<any>) {
-        if (this._set.has(desc)) return;
-
-        this._list.push(desc);
-        this._set.add(desc);
-    }
-
-    has(desc: ModelPropertyDescriptor<any>): boolean {
-        return this._set.has(desc);
-    }
-}

+ 0 - 40
src/mol-model/structure/model/properties/custom/descriptor.ts

@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { CifWriter } from 'mol-io/writer/cif'
-import { CifExportContext } from '../../../export/mmcif';
-import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
-import { UUID } from 'mol-util';
-
-interface ModelPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> {
-    readonly isStatic: boolean,
-    readonly name: string,
-
-    cifExport?: {
-        // Prefix enforced during export.
-        prefix: string,
-        context?: (ctx: CifExportContext) => ExportCtx | undefined,
-        categories: CifWriter.Category<ExportCtx>[]
-    },
-
-    // TODO: add aliases when lisp-like mol-script is done
-    symbols?: Symbols
-}
-
-function ModelPropertyDescriptor<Ctx, Desc extends ModelPropertyDescriptor<Ctx>>(desc: Desc) {
-    return desc;
-}
-
-namespace ModelPropertyDescriptor {
-    export function getUUID(prop: ModelPropertyDescriptor): UUID {
-        if (!(prop as any).__key) {
-            (prop as any).__key = UUID.create22();
-        }
-        return (prop as any).__key;
-    }
-}
-
-export { ModelPropertyDescriptor }

+ 0 - 91
src/mol-model/structure/model/properties/custom/residue.ts

@@ -1,91 +0,0 @@
-// /**
-//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
-//  *
-//  * @author David Sehnal <david.sehnal@gmail.com>
-//  */
-
-// import { ResidueIndex } from '../../indexing';
-// import { Unit, Structure, StructureElement } from '../../../structure';
-// import { Segmentation } from 'mol-data/int';
-// import { UUID } from 'mol-util';
-// import { CifWriter } from 'mol-io/writer/cif';
-
-// export interface ResidueCustomProperty<T = any> {
-//     readonly id: UUID,
-//     readonly kind: Unit.Kind,
-//     has(idx: ResidueIndex): boolean
-//     get(idx: ResidueIndex): T | undefined
-// }
-
-// export namespace ResidueCustomProperty {
-//     export interface ExportCtx<T> {
-//         elements: StructureElement[],
-//         property(index: number): T
-//     };
-
-//     function getExportCtx<T>(prop: ResidueCustomProperty<T>, structure: Structure): ExportCtx<T> {
-//         const residueIndex = structure.model.atomicHierarchy.residueAtomSegments.index;
-//         const elements = getStructureElements(structure, prop);
-//         return { elements, property: i => prop.get(residueIndex[elements[i].element])! };
-//     }
-
-//     export function getCifDataSource<T>(structure: Structure, prop: ResidueCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] {
-//         if (!prop) return { rowCount: 0 };
-//         if (cache && cache[prop.id]) return cache[prop.id];
-//         const data = getExportCtx(prop, structure);
-//         const ret = { data, rowCount: data.elements.length };
-//         if (cache) cache[prop.id] = ret;
-//         return ret;
-//     }
-
-//     class FromMap<T> implements ResidueCustomProperty<T> {
-//         readonly id = UUID.create();
-
-//         has(idx: ResidueIndex): boolean {
-//             return this.map.has(idx);
-//         }
-
-//         get(idx: ResidueIndex) {
-//             return this.map.get(idx);
-//         }
-
-//         constructor(private map: Map<ResidueIndex, T>, public kind: Unit.Kind) {
-//         }
-//     }
-
-//     export function fromMap<T>(map: Map<ResidueIndex, T>, kind: Unit.Kind) {
-//         return new FromMap(map, kind);
-//     }
-
-//     /**
-//      * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned.
-//      * Only works correctly for structures with a single model.
-//      */
-//     export function getStructureElements(structure: Structure, property: ResidueCustomProperty) {
-//         const models = structure.models;
-//         if (models.length !== 1) throw new Error(`Only works on structures with a single model.`);
-
-//         const seenResidues = new Set<ResidueIndex>();
-//         const unitGroups = structure.unitSymmetryGroups;
-//         const loci: StructureElement[] = [];
-
-//         for (const unitGroup of unitGroups) {
-//             const unit = unitGroup.units[0];
-//             if (unit.kind !== property.kind) {
-//                 continue;
-//             }
-
-//             const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
-//             while (residues.hasNext) {
-//                 const seg = residues.move();
-//                 if (!property.has(seg.index) || seenResidues.has(seg.index)) continue;
-
-//                 seenResidues.add(seg.index);
-//                 loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
-//             }
-//         }
-
-//         loci.sort((x, y) => x.element - y.element);
-//         return loci;
-//     }
-// }

+ 2 - 0
src/mol-model/structure/query.ts

@@ -9,12 +9,14 @@ import { StructureQuery } from './query/query'
 export * from './query/context'
 import * as generators from './query/queries/generators'
 import * as modifiers from './query/queries/modifiers'
+import * as filters from './query/queries/filters'
 import * as combinators from './query/queries/combinators'
 import * as internal from './query/queries/internal'
 import pred from './query/predicates'
 
 export const Queries = {
     generators,
+    filters,
     modifiers,
     combinators,
     pred,

+ 19 - 0
src/mol-model/structure/query/queries/filters.ts

@@ -31,6 +31,25 @@ export function pick(query: StructureQuery, pred: QueryPredicate): StructureQuer
     };
 }
 
+export function first(query: StructureQuery): StructureQuery {
+    return ctx => {
+        const sel = query(ctx);
+        const ret = StructureSelection.LinearBuilder(ctx.inputStructure);
+        if (sel.kind === 'singletons') {
+            if (sel.structure.elementCount > 0) {
+                const u = sel.structure.units[0];
+                const s = Structure.create([u.getChild(SortedArray.ofSingleton(u.elements[0]))], ctx.inputStructure);
+                ret.add(s);
+            }
+        } else {
+            if (sel.structures.length > 0) {
+                ret.add(sel.structures[0]);
+            }
+        }
+        return ret.getSelection();
+    };
+}
+
 export interface UnitTypeProperties { atomic?: QueryFn, coarse?: QueryFn }
 
 export function getCurrentStructureProperties(ctx: QueryContext, props: UnitTypeProperties, set: Set<any>) {

+ 4 - 4
src/mol-model/structure/query/queries/generators.ts

@@ -168,10 +168,10 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
     };
 }
 
-function getRingStructure(unit: Unit.Atomic, ring: UnitRing) {
+function getRingStructure(unit: Unit.Atomic, ring: UnitRing, inputStructure: Structure) {
     const elements = new Int32Array(ring.length) as any as ElementIndex[];
     for (let i = 0, _i = ring.length; i < _i; i++) elements[i] = unit.elements[ring[i]];
-    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))])
+    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))], inputStructure);
 }
 
 export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): StructureQuery {
@@ -184,7 +184,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure
                 if (!Unit.isAtomic(u)) continue;
 
                 for (const r of u.rings.all) {
-                    ret.add(getRingStructure(u, r));
+                    ret.add(getRingStructure(u, r, ctx.inputStructure));
                 }
             }
         } else {
@@ -198,7 +198,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure
                 for (const fp of uniqueFps.array) {
                     if (!rings.byFingerprint.has(fp)) continue;
                     for (const r of rings.byFingerprint.get(fp)!) {
-                        ret.add(getRingStructure(u, rings.all[r]));
+                        ret.add(getRingStructure(u, rings.all[r], ctx.inputStructure));
                     }
                 }
             }

+ 4 - 4
src/mol-model/structure/query/queries/internal.ts

@@ -35,7 +35,7 @@ export function atomicSequence(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure, ));
     };
 }
 
@@ -54,7 +54,7 @@ export function water(): StructureQuery {
             if (P.entity.type(l) !== 'water') continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }
 
@@ -84,7 +84,7 @@ export function atomicHet(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }
 
@@ -97,6 +97,6 @@ export function spheres(): StructureQuery {
             if (unit.kind !== Unit.Kind.Spheres) continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }

+ 1 - 1
src/mol-model/structure/query/selection.ts

@@ -135,7 +135,7 @@ namespace StructureSelection {
                 const { elements } = unit;
                 for (let i = 0, _i = elements.length; i < _i; i++) {
                     // TODO: optimize this somehow???
-                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))]);
+                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))], sel.source);
                     fn(s, idx++);
                 }
             }

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

@@ -80,7 +80,7 @@ export function structureIntersect(sA: Structure, sB: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, sA.parent || sB.parent);
 }
 
 export function structureSubtract(a: Structure, b: Structure): Structure {
@@ -100,5 +100,5 @@ export function structureSubtract(a: Structure, b: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, a.parent || b.parent);
 }

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

@@ -26,6 +26,7 @@ import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { idFactory } from 'mol-util/id-factory';
 import { GridLookup3D } from 'mol-math/geometry';
 import { UUID } from 'mol-util';
+import { CustomProperties } from '../common/custom-property';
 
 class Structure {
     /** Maps unit.id to unit */
@@ -34,6 +35,7 @@ class Structure {
     readonly units: ReadonlyArray<Unit>;
 
     private _props: {
+        parent?: Structure,
         lookup3d?: StructureLookup3D,
         links?: InterUnitBonds,
         crossLinkRestraints?: PairRestraints<CrossLinkRestraint>,
@@ -49,7 +51,16 @@ class Structure {
         transformHash: number,
         elementCount: number,
         polymerResidueCount: number,
-    } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 };
+        coordinateSystem: SymmetryOperator,
+        propertyData?: any,
+        customProps?: CustomProperties
+    } = {
+        hashCode: -1,
+        transformHash: -1,
+        elementCount: 0,
+        polymerResidueCount: 0,
+        coordinateSystem: SymmetryOperator.Default
+    };
 
     subsetBuilder(isSorted: boolean) {
         return new StructureSubsetBuilder(this, isSorted);
@@ -60,6 +71,30 @@ class Structure {
         return this._props.elementCount;
     }
 
+    get hasCustomProperties() {
+        return !!this._props.customProps && this._props.customProps.all.length > 0;
+    }
+
+    get customPropertyDescriptors() {
+        if (!this._props.customProps) this._props.customProps = new CustomProperties();
+        return this._props.customProps;
+    }
+
+    /**
+     * Property data unique to this instance of the structure.
+     */
+    get currentPropertyData() {
+        if (!this._props.propertyData) this._props.propertyData = Object.create(null);
+        return this._props.propertyData;
+    }
+
+    /**
+     * Property data of the parent structure if it exists, currentPropertyData otherwise.
+     */
+    get inheritedPropertyData() {
+        return this.parent ? this.parent.currentPropertyData : this.currentPropertyData;
+    }
+
     /** Count of all polymer residues in the structure */
     get polymerResidueCount() {
         return this._props.polymerResidueCount;
@@ -106,6 +141,14 @@ class Structure {
         return new Structure.ElementLocationIterator(this);
     }
 
+    get parent() {
+        return this._props.parent;
+    }
+
+    get coordinateSystem() {
+        return this._props.coordinateSystem;
+    }
+
     get boundary() {
         return this.lookup3d.boundary;
     }
@@ -174,7 +217,7 @@ class Structure {
         return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element);
     }
 
-    constructor(units: ArrayLike<Unit>) {
+    private initUnits(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
         let polymerResidueCount = 0;
@@ -188,11 +231,18 @@ class Structure {
             if (u.id < lastId) isSorted = false;
             lastId = u.id;
         }
-        if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap)
-        this.unitMap = map;
-        this.units = units as ReadonlyArray<Unit>;
+        if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap);
         this._props.elementCount = elementCount;
         this._props.polymerResidueCount = polymerResidueCount;
+        return map;
+    }
+
+    constructor(units: ArrayLike<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator) {
+        this.unitMap = this.initUnits(units);
+        this.units = units as ReadonlyArray<Unit>;
+        if (parent) this._props.parent = parent;
+        if (coordinateSystem) this._props.coordinateSystem = coordinateSystem;
+        else if (parent) this._props.coordinateSystem = parent.coordinateSystem;
     }
 }
 
@@ -283,7 +333,7 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID,
 }
 
 namespace Structure {
-    export const Empty = new Structure([]);
+    export const Empty = new Structure([], void 0, void 0);
 
     /** Represents a single structure */
     export interface Loci {
@@ -302,7 +352,9 @@ namespace Structure {
         return a.structure === b.structure
     }
 
-    export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); }
+    export function create(units: ReadonlyArray<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator): Structure {
+        return new Structure(units, parent, coordinateSystem);
+    }
 
     /**
      * Construct a Structure from a model.
@@ -312,7 +364,7 @@ namespace Structure {
      */
     export function ofModel(model: Model): Structure {
         const chains = model.atomicHierarchy.chainAtomSegments;
-        const builder = new StructureBuilder();
+        const builder = new StructureBuilder(void 0, void 0);
 
         for (let c = 0; c < chains.count; c++) {
             const start = chains.offsets[c];
@@ -381,11 +433,13 @@ namespace Structure {
         const units: Unit[] = [];
         for (const u of s.units) {
             const old = u.conformation.operator;
-            const op = SymmetryOperator.create(old.name, transform, { id: '', operList: [] }, old.ncsId, old.hkl);
+            const op = SymmetryOperator.create(old.name, transform, old.assembly, old.ncsId, old.hkl);
             units.push(u.applyOperator(u.id, op));
         }
 
-        return new Structure(units);
+        const cs = s.coordinateSystem;
+        const newCS = SymmetryOperator.compose(SymmetryOperator.create(cs.name, transform, cs.assembly, cs.ncsId, cs.hkl), cs);
+        return new Structure(units, s, newCS);
     }
 
     export class StructureBuilder {
@@ -405,15 +459,21 @@ namespace Structure {
         }
 
         getStructure(): Structure {
-            return create(this.units);
+            return create(this.units, this.parent, this.coordinateSystem);
         }
 
         get isEmpty() {
             return this.units.length === 0;
         }
+
+        constructor(private parent: Structure | undefined, private coordinateSystem: SymmetryOperator | undefined) {
+
+        }
     }
 
-    export function Builder() { return new StructureBuilder(); }
+    export function Builder(parent: Structure | undefined, coordinateSystem: SymmetryOperator | undefined) {
+        return new StructureBuilder(parent, coordinateSystem);
+    }
 
     export function hashCode(s: Structure) {
         return s.hashCode;

+ 8 - 3
src/mol-model/structure/structure/symmetry.ts

@@ -24,7 +24,7 @@ namespace StructureSymmetry {
             const assembly = ModelSymmetry.findAssembly(models[0], asmName);
             if (!assembly) throw new Error(`Assembly '${asmName}' is not defined.`);
 
-            const assembler = Structure.Builder();
+            const assembler = Structure.Builder(void 0, SymmetryOperator.create(assembly.id, Mat4.identity(), { id: assembly.id, operList: [] }));
 
             const queryCtx = new QueryContext(structure);
 
@@ -83,7 +83,12 @@ namespace StructureSymmetry {
     export function areTransformGroupsEquivalent(a: ReadonlyArray<Unit.SymmetryGroup>, b: ReadonlyArray<Unit.SymmetryGroup>) {
         if (a.length !== b.length) return false
         for (let i = 0, il = a.length; i < il; ++i) {
+            const au = a[i].units, bu = b[i].units;
+            if (au.length !== bu.length) return false;
             if (a[i].hashCode !== b[i].hashCode) return false
+            for (let j = 0, _j = au.length; j < _j; j++) {
+                if (au[j].conformation !== bu[j].conformation) return false;
+            }
         }
         return true
     }
@@ -132,7 +137,7 @@ function getOperatorsCached333(symmetry: ModelSymmetry) {
 }
 
 function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
-    const assembler = Structure.Builder();
+    const assembler = Structure.Builder(void 0, void 0);
     const { units } = structure;
     for (const oper of operators) {
         for (const unit of units) {
@@ -174,7 +179,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
     const operators = getOperatorsCached333(symmetry);
     const lookup = structure.lookup3d;
 
-    const assembler = Structure.Builder();
+    const assembler = Structure.Builder(void 0, void 0);
 
     const { units } = structure;
     const center = Vec3.zero();

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

@@ -90,7 +90,7 @@ export class StructureSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent);
     }
 
     getStructure() {

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

@@ -85,7 +85,7 @@ export class StructureUniqueSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent, this.parent.coordinateSystem);
     }
 
     get isEmpty() {

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

@@ -118,7 +118,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
             for (const s of structures) {
                 const rootStructure = getRootStructure(s, state)
                 if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
-                if (!state.cellStates.get(s.transform.ref).isHidden) {
+                if (!s.state.isHidden) {
                     rootStructures.add(rootStructure.obj)
                 }
             }

+ 6 - 6
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -56,30 +56,30 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
         if (!refs['structure-interaction-group']) {
             refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup,
-                { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref;
+                { label: 'Current Interaction' }, { tags: Tags.Group }).ref;
         }
 
         // Selections
         if (!refs[Tags.ResidueSel]) {
             refs[Tags.ResidueSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Residue' }, { props: { tag: Tags.ResidueSel } }).ref;
+                { query: { } as any, label: 'Residue' }, { tags: Tags.ResidueSel }).ref;
         }
 
         if (!refs[Tags.SurrSel]) {
             refs[Tags.SurrSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Surroundings' }, { props: { tag: Tags.SurrSel } }).ref;
+                { query: { } as any, label: 'Surroundings' }, { tags: Tags.SurrSel }).ref;
         }
 
         // Representations
         // TODO: ability to customize how it looks in the behavior params
         if (!refs[Tags.ResidueRepr]) {
             refs[Tags.ResidueRepr] = builder.to(refs['structure-interaction-residue-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createResVisualParams(cell.obj!.data), { props: { tag: Tags.ResidueRepr } }).ref;
+                this.createResVisualParams(cell.obj!.data), { tags: Tags.ResidueRepr }).ref;
         }
 
         if (!refs[Tags.SurrRepr]) {
             refs[Tags.SurrRepr] = builder.to(refs['structure-interaction-surr-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createSurVisualParams(cell.obj!.data), { props: { tag: Tags.SurrRepr } }).ref;
+                this.createSurVisualParams(cell.obj!.data), { tags: Tags.SurrRepr }).ref;
         }
 
         return { state, builder, refs };
@@ -87,7 +87,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
     private clear(root: StateTransform.Ref) {
         const state = this.plugin.state.dataState;
-        const groups = state.select(StateSelection.Generators.byRef(root).subtree().filter(o => o.transform.props.tag === Tags.Group));
+        const groups = state.select(StateSelection.Generators.byRef(root).subtree().withTag(Tags.Group));
         if (groups.length === 0) return;
 
         const update = state.build();

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

@@ -59,11 +59,11 @@ export const InitVolumeStreaming = StateAction.build({
         PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data)));
 
     if (params.method === 'em') {
-        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true } });
     } else {
-        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true } });
     }
     await state.updateTree(behTree).runInContext(taskCtx);
 }));

+ 6 - 6
src/mol-plugin/behavior/static/representation.ts

@@ -7,7 +7,7 @@
 import { PluginStateObject as SO } from '../../state/objects';
 import { PluginContext } from 'mol-plugin/context';
 import { Representation } from 'mol-repr/representation';
-import { State } from 'mol-state';
+import { StateObjectCell } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
@@ -21,7 +21,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         e.obj.data.repr.setState({ syncManually: true });
         ctx.canvas3d.add(e.obj.data.repr);
 
@@ -39,7 +39,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
             return;
         }
 
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         if (e.action === 'recreate') {
             e.obj.data.repr.setState({ syncManually: true });
         }
@@ -86,11 +86,11 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
     ctx.state.dataState.events.cell.stateUpdated.subscribe(e => {
         const cell = e.state.cells.get(e.ref)!;
         if (!SO.isRepresentation3D(cell.obj)) return;
-        updateVisibility(e, cell.obj.data.repr);
+        updateVisibility(cell, cell.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
     })
 }
 
-function updateVisibility(e: State.ObjectEvent, r: Representation<any>) {
-    r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden });
+function updateVisibility(cell: StateObjectCell, r: Representation<any>) {
+    r.setState({ visible: !cell.state.isHidden });
 }

+ 3 - 2
src/mol-plugin/behavior/static/state.ts

@@ -72,7 +72,8 @@ export function RemoveObject(ctx: PluginContext) {
                 const children = tree.children.get(curr.parent);
                 if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
                 const parent = tree.transforms.get(curr.parent);
-                if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref);
+                // TODO: should this use "cell state" instead?
+                if (!parent.state.isGhost) return remove(state, curr.ref);
                 curr = parent;
             }
         } else {
@@ -86,7 +87,7 @@ export function ToggleExpanded(ctx: PluginContext) {
 }
 
 export function ToggleVisibility(ctx: PluginContext) {
-    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
+    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cells.get(ref)!.state.isHidden));
 }
 
 function setVisibility(state: State, root: StateTransform.Ref, value: boolean) {

+ 1 - 1
src/mol-plugin/command.ts

@@ -53,7 +53,7 @@ export const PluginCommands = {
     },
     Camera: {
         Reset: PluginCommand<{}>(),
-        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(),
+        SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>(),
             Remove: PluginCommand<{ id: string }>(),

+ 1 - 1
src/mol-plugin/context.ts

@@ -180,7 +180,7 @@ export class PluginContext {
         const tree = this.state.behaviorState.build();
 
         for (const cat of Object.keys(PluginBehavior.Categories)) {
-            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } });
+            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, state: { isLocked: true } });
         }
 
         for (const b of this.spec.behaviors) {

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

@@ -12,10 +12,11 @@ import * as ReactDOM from 'react-dom';
 import { PluginSpec } from './spec';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
-import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode } from './state/animation/built-in';
+import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode, AnimateStateInterpolation } from './state/animation/built-in';
 import { StateActions } from './state/actions';
 import { InitVolumeStreaming, BoxifyVolumeStreaming, CreateVolumeStreamingBehavior } from './behavior/dynamic/volume-streaming/transformers';
 import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction';
+import { TransformStructureConformation } from './state/actions/structure';
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
@@ -38,6 +39,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
         PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
+        PluginSpec.Action(TransformStructureConformation),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
         PluginSpec.Action(StateTransforms.Model.UserStructureSelection),
@@ -66,6 +68,7 @@ export const DefaultPluginSpec: PluginSpec = {
         AnimateModelIndex,
         AnimateAssemblyUnwind,
         AnimateUnitsExplode,
+        AnimateStateInterpolation
     ]
 }
 

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

@@ -99,7 +99,7 @@ class PluginState {
     constructor(private plugin: import('./context').PluginContext) {
         this.snapshots = new PluginStateSnapshotManager(plugin);
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootState: { isLocked: true } });
 
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o);

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

@@ -146,7 +146,7 @@ const DownloadStructure = StateAction.build({
         createStructureTree(ctx, traj, supportProps);
     } else {
         for (const download of downloadParams) {
-            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } });
+            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { state: { isGhost: true } });
             const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
             createStructureTree(ctx, traj, supportProps)
         }
@@ -179,14 +179,14 @@ export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary
     let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory>
     switch (format) {
         case 'cif':
-            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } })
-                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } })
+            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
+                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { state: { isGhost: true } })
             break
         case 'pdb':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { state: { isGhost: true } });
             break
         case 'gro':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { state: { isGhost: true } });
             break
         default:
             throw new Error('unsupported format')
@@ -287,6 +287,15 @@ export const EnableModelCustomProps = StateAction.build({
     return state.updateTree(root);
 });
 
+export const TransformStructureConformation = StateAction.build({
+    display: { name: 'Transform Conformation' },
+    from: PluginStateObject.Molecule.Structure,
+    params: StateTransforms.Model.TransformStructureConformation.definition.params,
+})(({ ref, params, state }) => {
+    const root = state.build().to(ref).insert(StateTransforms.Model.TransformStructureConformation, params as any);
+    return state.updateTree(root);
+});
+
 export const StructureFromSelection = StateAction.build({
     display: { name: 'Selection Structure', description: 'Create a new Structure from the current selection.' },
     from: PluginStateObject.Molecule.Structure,

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

@@ -121,7 +121,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
             changed = true;
             update.to(r)
-                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } });
+                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { tags: 'animate-assembly-unwind' });
         }
 
         if (!changed) return;
@@ -131,7 +131,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-assembly-unwind'));
+            .withTag('animate-assembly-unwind'));
         if (reprs.length === 0) return;
 
         const update = state.build();
@@ -191,7 +191,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
             changed = true;
             update.to(r.transform.ref)
-                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { props: { tag: 'animate-units-explode' } });
+                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { tags: 'animate-units-explode' });
         }
 
         if (!changed) return;
@@ -201,7 +201,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-units-explode'));
+            .withTag('animate-units-explode'));
         if (reprs.length === 0) return;
 
         const update = state.build();
@@ -229,4 +229,54 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         return { kind: 'next', state: { t: newTime } };
     }
+})
+
+export const AnimateStateInterpolation = PluginStateAnimation.create({
+    name: 'built-in.animate-state-interpolation',
+    display: { name: 'Animate State Interpolation' },
+    params: () => ({
+        transtionDurationInMs: PD.Numeric(2000, { min: 100, max: 30000, step: 10 })
+    }),
+    initialState: () => ({ }),
+    async apply(animState, t, ctx) {
+
+        const snapshots = ctx.plugin.state.snapshots.state.entries;
+        if (snapshots.size < 2) return { kind: 'finished' };
+
+        // const totalTime = (snapshots.size - 1) * ctx.params.transtionDurationInMs;
+        const currentT = (t.current % ctx.params.transtionDurationInMs) / ctx.params.transtionDurationInMs;
+
+        let srcIndex = Math.floor(t.current / ctx.params.transtionDurationInMs) % snapshots.size;
+        let tarIndex = Math.ceil(t.current / ctx.params.transtionDurationInMs);
+        if (tarIndex === srcIndex) tarIndex++;
+        tarIndex = tarIndex % snapshots.size;
+
+        const _src = snapshots.get(srcIndex)!.snapshot, _tar = snapshots.get(tarIndex)!.snapshot;
+
+        if (!_src.data || !_tar.data) return { kind: 'skip' };
+
+        const src = _src.data.tree.transforms, tar = _tar.data.tree.transforms;
+
+        const state = ctx.plugin.state.dataState;
+        const update = state.build();
+
+        for (const s of src) {
+            for (const t of tar) {
+                if (t.ref !== s.ref) continue;
+                if (t.version === s.version) continue;
+
+                const e = StateTransform.fromJSON(s), f = StateTransform.fromJSON(t);
+
+                if (!e.transformer.definition.interpolate) {
+                    update.to(s.ref).update(currentT <= 0.5 ? e.params : f.params);
+                } else {
+                    update.to(s.ref).update(e.transformer.definition.interpolate(e.params, f.params, currentT, ctx.plugin));
+                }
+            }
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+
+        return { kind: 'next', state: { } };
+    }
 })

+ 49 - 1
src/mol-plugin/state/transforms/model.ts

@@ -6,7 +6,7 @@
  */
 
 import { parsePDB } from 'mol-io/reader/pdb/parser';
-import { Vec3 } from 'mol-math/linear-algebra';
+import { Vec3, Mat4, Quat } from 'mol-math/linear-algebra';
 import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
 import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry, QueryFn } from 'mol-model/structure';
@@ -25,6 +25,7 @@ import { parseGRO } from 'mol-io/reader/gro/parser';
 import { parseMolScript } from 'mol-script/language/parser';
 import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
 import { shapeFromPly } from 'mol-model-formats/shape/ply';
+import { SymmetryOperator } from 'mol-math/geometry';
 
 export { TrajectoryFromBlob };
 export { TrajectoryFromMmCif };
@@ -34,6 +35,7 @@ export { ModelFromTrajectory };
 export { StructureFromModel };
 export { StructureAssemblyFromModel };
 export { StructureSymmetryFromModel };
+export { TransformStructureConformation }
 export { StructureSelection };
 export { UserStructureSelection };
 export { StructureComplexElement };
@@ -251,6 +253,52 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
+const _translation = Vec3.zero(), _m = Mat4.zero(), _n = Mat4.zero();
+type TransformStructureConformation = typeof TransformStructureConformation
+const TransformStructureConformation = PluginStateTransform.BuiltIn({
+    name: 'transform-structure-conformation',
+    display: { name: 'Transform Conformation' },
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure,
+    params: {
+        axis: PD.Vec3(Vec3.create(1, 0, 0)),
+        angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }),
+        translation: PD.Vec3(Vec3.create(0, 0, 0)),
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        // TODO: optimze
+
+        const center = a.data.boundary.sphere.center;
+        Mat4.fromTranslation(_m, Vec3.negate(_translation, center));
+        Mat4.fromTranslation(_n, Vec3.add(_translation, center, params.translation));
+        const rot = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * params.angle, Vec3.normalize(Vec3.zero(), params.axis));
+
+        const m = Mat4.zero();
+        Mat4.mul3(m, _n, rot, _m);
+
+        const s = Structure.transform(a.data, m);
+        const props = { label: `${a.label}`, description: `Transformed` };
+        return new SO.Molecule.Structure(s, props);
+    },
+    interpolate(src, tar, t) {
+        // TODO: optimize
+        const u = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * src.angle, Vec3.normalize(Vec3.zero(), src.axis));
+        Mat4.setTranslation(u, src.translation);
+        const v = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * tar.angle, Vec3.normalize(Vec3.zero(), tar.axis));
+        Mat4.setTranslation(v, tar.translation);
+        const m = SymmetryOperator.slerp(Mat4.zero(), u, v, t);
+        const rot = Mat4.getRotation(Quat.zero(), m);
+        const axis = Vec3.zero();
+        const angle = Quat.getAxisAngle(axis, rot);
+        const translation = Mat4.getTranslation(Vec3.zero(), m);
+        return { axis, angle, translation };
+    }
+});
+
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',

+ 14 - 1
src/mol-plugin/state/transforms/representation.ts

@@ -15,7 +15,7 @@ import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry';
 import { VolumeParams } from 'mol-repr/volume/representation';
 import { StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
-import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color';
+import { BuiltInColorThemeName, ColorTheme, BuiltInColorThemes } from 'mol-theme/color';
 import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size';
 import { createTheme, ThemeRegistryContext } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -191,6 +191,19 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             return StateTransformer.UpdateResult.Updated;
         });
+    },
+    interpolate(src, tar, t) {
+        if (src.colorTheme.name !== 'uniform' || tar.colorTheme.name !== 'uniform') {
+            return t <= 0.5 ? src : tar;
+        }
+        BuiltInColorThemes
+        const from = src.colorTheme.params.value as Color, to = tar.colorTheme.params.value as Color;
+        const value = Color.interpolate(from, to, t);
+        return {
+            type: t <= 0.5 ? src.type : tar.type,
+            colorTheme: { name: 'uniform', params: { value } },
+            sizeTheme: t <= 0.5 ? src.sizeTheme : tar.sizeTheme,
+        };
     }
 });
 

+ 4 - 1
src/mol-plugin/ui/base.tsx

@@ -58,4 +58,7 @@ export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends Rea
         this.plugin = context;
         if (this.init) this.init();
     }
-}
+}
+
+export type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
+export type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never

+ 1 - 3
src/mol-plugin/ui/controls/parameters.tsx

@@ -16,6 +16,7 @@ import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
 import { NumericInput, IconButton } from './common';
+import { _Props, _State } from '../base';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
@@ -513,9 +514,6 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
     }
 }
 
-type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
-type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
-
 class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> {
     state = { params: {}, value: void 0 as any, current: void 0 as any };
 

+ 60 - 60
src/mol-plugin/ui/state/tree.tsx

@@ -6,9 +6,9 @@
 
 import * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { State, StateObject, StateTransform } from 'mol-state'
+import { State, StateObject, StateTransform, StateObjectCell } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
-import { PluginUIComponent } from '../base';
+import { PluginUIComponent, _Props, _State } from '../base';
 import { StateObjectActions } from './actions';
 
 export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
@@ -37,74 +37,75 @@ export class StateTree extends PluginUIComponent<{ state: State }, { showActions
         if (this.state.showActions) {
             return <StateObjectActions state={this.props.state} nodeRef={ref} hideHeader={true} />
         }
-        return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />;
+        return <StateTreeNode cell={this.props.state.cells.get(ref)!} depth={0} />;
     }
 }
 
-class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> {
+class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, { isCollapsed: boolean }> {
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
     }
 
-    get cellState() {
-        return this.props.state.cellStates.get(this.props.nodeRef);
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
-            if (this.is(e) && e.state.transforms.has(this.props.nodeRef)) {
-                this.setState({ isCollapsed: e.cellState.isCollapsed });
+            if (this.props.cell === e.cell && this.is(e) && e.state.cells.has(this.ref)) {
+                this.forceUpdate();
+                // if (!!this.props.cell.state.isCollapsed !== this.state.isCollapsed) {
+                //     this.setState({ isCollapsed: !!e.cell.state.isCollapsed });
+                // }
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.created, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.cell.transform.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.cell.transform.parent) {
                 this.forceUpdate();
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.removed, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.parent) {
                 this.forceUpdate();
             }
         });
     }
 
     state = {
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null {
+        if (!!props.cell.state.isCollapsed === state.isCollapsed) return null;
+        return { isCollapsed: !!props.cell.state.isCollapsed };
     }
 
     render() {
-        const cell = this.props.state.cells.get(this.props.nodeRef);
-        if (!cell || cell.obj === StateObject.Null) return null;
+        const cell = this.props.cell;
+        if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
+            return null;
+        }
 
-        const cellState = this.cellState;
-        const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost;
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
+        const cellState = cell.state;
+        const showLabel = cell.status !== 'ok' || !cell.state.isGhost;
+        const children = cell.parent.tree.children.get(this.ref);
         const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
 
         if (!showLabel) {
             if (children.size === 0) return null;
             return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
             </div>;
         }
 
         return <>
-            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} />
+            <StateTreeNodeLabel cell={cell} depth={this.props.depth} />
             {children.size === 0
                 ? void 0
                 : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                    {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
                 </div>
             }
         </>;
@@ -112,11 +113,15 @@ class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, d
 }
 
 class StateTreeNodeLabel extends PluginUIComponent<
-    { nodeRef: string, state: State, depth: number },
-    { state: State, isCurrent: boolean, isCollapsed: boolean /*, updaterCollapsed: boolean */ }> {
+    { cell: StateObjectCell, depth: number },
+    { isCurrent: boolean, isCollapsed: boolean }> {
 
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
+    }
+
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
@@ -126,70 +131,66 @@ class StateTreeNodeLabel extends PluginUIComponent<
 
         this.subscribe(this.plugin.state.behavior.currentObject, e => {
             if (!this.is(e)) {
-                if (this.state.isCurrent && e.state.transforms.has(this.props.nodeRef)) {
-                    this.setState({ isCurrent: this.props.state.current === this.props.nodeRef });
+                if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
+                    this.setState({ isCurrent: this.props.cell.parent.current === this.ref });
                 }
                 return;
             }
 
-            if (e.state.transforms.has(this.props.nodeRef)) {
+            if (e.state.transforms.has(this.ref)) {
                 this.setState({
-                    isCurrent: this.props.state.current === this.props.nodeRef,
-                    isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed
+                    isCurrent: this.props.cell.parent.current === this.ref,
+                    isCollapsed: !!this.props.cell.state.isCollapsed
                 });
             }
         });
     }
 
     state = {
-        isCurrent: this.props.state.current === this.props.nodeRef,
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state,
-        // updaterCollapsed: true
+        isCurrent: this.props.cell.parent.current === this.ref,
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCurrent: props.state.current === props.nodeRef,
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state,
-            updaterCollapsed: true
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
+        const isCurrent = props.cell.parent.current === props.cell.transform.ref;
+        const isCollapsed = !!props.cell.state.isCollapsed;
+
+        if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
+        return { isCurrent, isCollapsed };
     }
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         e.currentTarget.blur();
-        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
     }
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true });
+        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
@@ -201,12 +202,11 @@ class StateTreeNodeLabel extends PluginUIComponent<
     // }
 
     render() {
-        const n = this.props.state.transforms.get(this.props.nodeRef)!;
-        const cell = this.props.state.cells.get(this.props.nodeRef);
+        const cell = this.props.cell;
+        const n = cell.transform;
         if (!cell) return null;
 
-        const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
-
+        const isCurrent = this.state.isCurrent; // this.is(cell.parent.behaviors.currentObject.value);
 
         let label: any;
         if (cell.status === 'pending' || cell.status === 'processing') {
@@ -226,8 +226,8 @@ class StateTreeNodeLabel extends PluginUIComponent<
             }
         }
 
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
-        const cellState = this.props.state.cellStates.get(this.props.nodeRef);
+        const children = cell.parent.tree.children.get(this.ref);
+        const cellState = cell.state;
 
         const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
             <span className='msp-icon msp-icon-visual-visibility' />
@@ -244,7 +244,7 @@ class StateTreeNodeLabel extends PluginUIComponent<
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
-            {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+            {!cell.state.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
                 <span className='msp-icon msp-icon-remove' />
             </button>}{visibility}
         </div>;

+ 0 - 1
src/mol-repr/structure/units-representation.ts

@@ -65,7 +65,6 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length })
                 }
             } else if (structure && !Structure.areEquivalent(structure, _structure)) {
-                // console.log(label, 'structure not equivalent')
                 // Tries to re-use existing visuals for the groups of the new structure.
                 // Creates additional visuals if needed, destroys left-over visuals.
                 _groups = structure.unitSymmetryGroups;

+ 5 - 1
src/mol-repr/structure/units-visual.ts

@@ -121,8 +121,12 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
         }
 
         // check if the conformation of unit.model has changed
-        if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) {
+        // if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) {
+        if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])
+            // TODO: this needs more attention
+            || newStructureGroup.group.units[0].conformation !== currentStructureGroup.group.units[0].conformation) {
             // console.log('new conformation')
+            updateState.updateTransform = true;
             updateState.createGeometry = true
         }
 

+ 4 - 0
src/mol-script/language/symbol-table/structure-query.ts

@@ -167,6 +167,10 @@ const filter = {
         test: Argument(Type.Bool)
     }), Types.ElementSelectionQuery, 'Pick all atom sets that satisfy the test.'),
 
+    first: symbol(Arguments.Dictionary({
+        0: Argument(Types.ElementSelectionQuery)
+    }), Types.ElementSelectionQuery, 'Take the 1st atom set in the sequence.'),
+
     withSameAtomProperties: symbol(Arguments.Dictionary({
         0: Argument(Types.ElementSelectionQuery),
         source: Argument(Types.ElementSelectionQuery),

+ 2 - 2
src/mol-script/runtime/query/compiler.ts

@@ -5,7 +5,7 @@
  */
 
 import Expression from '../../language/expression';
-import { QueryContext, QueryFn, Structure, ModelPropertyDescriptor } from 'mol-model/structure';
+import { QueryContext, QueryFn, Structure, CustomPropertyDescriptor } from 'mol-model/structure';
 import { MSymbol } from '../../language/symbol';
 
 export class QueryRuntimeTable {
@@ -18,7 +18,7 @@ export class QueryRuntimeTable {
         this.map.set(runtime.symbol.id, runtime);
     }
 
-    addCustomProp(desc: ModelPropertyDescriptor<any>) {
+    addCustomProp(desc: CustomPropertyDescriptor<any>) {
         if (!desc.symbols) return;
 
         for (const k of Object.keys(desc.symbols)) {

+ 3 - 0
src/mol-script/runtime/query/table.ts

@@ -186,6 +186,9 @@ const symbols = [
     C(MolScript.structureQuery.slot.element, (ctx, _) => ctx.element),
     // C(MolScript.structureQuery.slot.elementSetReduce, (ctx, _) => ctx.element),
 
+    // ============= FILTERS ================
+    D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)),
+
     // ============= GENERATORS ================
     D(MolScript.structureQuery.generator.atomGroups, (ctx, xs) => Queries.generators.atoms({
         entityTest: xs['entity-test'],

+ 1 - 0
src/mol-script/script/mol-script/symbols.ts

@@ -148,6 +148,7 @@ export const SymbolTable = [
         [
             'Filters',
             Alias(MolScript.structureQuery.filter.pick, 'sel.atom.pick'),
+            Alias(MolScript.structureQuery.filter.first, 'sel.atom.first'),
             Alias(MolScript.structureQuery.filter.withSameAtomProperties, 'sel.atom.with-same-atom-properties'),
             Alias(MolScript.structureQuery.filter.intersectedBy, 'sel.atom.intersected-by'),
             Alias(MolScript.structureQuery.filter.within, 'sel.atom.within'),

+ 0 - 7
src/mol-state/manager.ts

@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// TODO manage snapshots etc

+ 4 - 19
src/mol-state/object.ts

@@ -19,7 +19,7 @@ interface StateObject<D = any, T extends StateObject.Type = StateObject.Type<any
     readonly label: string,
     readonly description?: string,
     // assigned by reconciler to be StateTransform.props.tag
-    readonly tag?: string
+    readonly tags?: string[]
 }
 
 namespace StateObject {
@@ -56,12 +56,15 @@ namespace StateObject {
 }
 
 interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> {
+    parent: State,
+
     transform: F,
 
     // Which object was used as a parent to create data in this cell
     sourceRef: StateTransform.Ref | undefined,
 
     status: StateObjectCell.Status,
+    state: StateTransform.State,
 
     params: {
         definition: ParamDefinition.Params,
@@ -79,24 +82,6 @@ namespace StateObjectCell {
 
     export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never
     export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never
-
-    export interface State {
-        isHidden: boolean,
-        isCollapsed: boolean
-    }
-
-    export const DefaultState: State = { isHidden: false, isCollapsed: false };
-
-    export function areStatesEqual(a: State, b: State) {
-        return a.isHidden !== b.isHidden || a.isCollapsed !== b.isCollapsed;
-    }
-
-    export function isStateChange(a: State, b?: Partial<State>) {
-        if (!b) return false;
-        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
-        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
-        return false;
-    }
 }
 
 // TODO: improve the API?

+ 27 - 45
src/mol-state/state.ts

@@ -33,7 +33,7 @@ class State {
     readonly globalContext: unknown = void 0;
     readonly events = {
         cell: {
-            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
+            stateUpdated: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
@@ -55,7 +55,6 @@ class State {
 
     get tree(): StateTree { return this._tree; }
     get transforms() { return (this._tree as StateTree).transforms; }
-    get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
     build() { return new StateBuilder.Root(this.tree, this); }
@@ -76,13 +75,15 @@ class State {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
-        const update = typeof stateOrProvider === 'function'
-            ? stateOrProvider(this.tree.cellStates.get(ref))
-            : stateOrProvider;
+    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateTransform.State) => Partial<StateTransform.State>) | Partial<StateTransform.State>) {
+        const cell = this.cells.get(ref);
+        if (!cell) return;
 
-        if (this._tree.updateCellState(ref, update)) {
-            this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
+        const update = typeof stateOrProvider === 'function' ? stateOrProvider(cell.state) : stateOrProvider;
+
+        if (StateTransform.assignState(cell.state, update)) {
+            cell.transform = this._tree.assignState(cell.transform.ref, update);
+            this.events.cell.stateUpdated.next({ state: this, ref, cell });
         }
     }
 
@@ -165,10 +166,6 @@ class State {
 
             if (updated) this.events.changed.next();
             this.events.isUpdating.next(false);
-
-            for (const ref of ctx.stateChanges) {
-                this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
-            }
         }
     }
 
@@ -189,7 +186,6 @@ class State {
             spine: this.spine,
 
             results: [],
-            stateChanges: [],
 
             options: { ...StateUpdateDefaultOptions, ...options },
 
@@ -203,16 +199,18 @@ class State {
         return ctx;
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
-        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient();
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
+        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient();
         const tree = this._tree;
         const root = tree.root;
 
         (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
+            parent: this,
             transform: root,
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
+            state: { ...root.state },
             errorText: void 0,
             params: {
                 definition: {},
@@ -245,7 +243,7 @@ namespace State {
         doNotUpdateCurrent: boolean
     }
 
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
         return new State(rootObject, params);
     }
 }
@@ -271,7 +269,6 @@ interface UpdateContext {
     spine: StateTreeSpine.Impl,
 
     results: UpdateNodeResult[],
-    stateChanges: StateTransform.Ref[],
 
     // suppress timing messages
     options: State.UpdateOptions,
@@ -319,12 +316,6 @@ async function update(ctx: UpdateContext) {
         roots = findUpdateRoots(ctx.cells, ctx.tree);
     }
 
-    let newCellStates: StateTree.CellStates;
-    if (!ctx.editInfo) {
-        newCellStates = ctx.tree.cellStatesSnapshot();
-        syncOldStates(ctx);
-    }
-
     // Init empty cells where not present
     // this is done in "pre order", meaning that "parents" will be created 1st.
     const addedCells = initCells(ctx, roots);
@@ -353,7 +344,7 @@ async function update(ctx: UpdateContext) {
 
     // Sync cell states
     if (!ctx.editInfo) {
-        syncNewStates(ctx, newCellStates!);
+        syncNewStates(ctx);
     }
 
     let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
@@ -363,7 +354,7 @@ async function update(ctx: UpdateContext) {
             ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
             if (!ctx.newCurrent) {
                 const transform = ctx.tree.transforms.get(update.ref);
-                if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref;
+                if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref;
             }
         } else if (update.action === 'updated') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
@@ -415,25 +406,14 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
 }
 
-function syncOldStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) {
-    if (oldState.has(n.ref)) {
-        (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
-    }
-}
-function syncOldStates(ctx: UpdateContext) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncOldStatesVisitor);
+function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: UpdateContext) {
+    const cell = ctx.cells.get(n.ref);
+    if (!cell || !StateTransform.syncState(cell.state, n.state)) return;
+    ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref: n.ref, cell });
 }
 
-function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: { newState: StateTree.CellStates, changes: StateTransform.Ref[] }) {
-    if (ctx.newState.has(n.ref)) {
-        const changed = (tree as TransientTree).updateCellState(n.ref, ctx.newState.get(n.ref));
-        if (changed) {
-            ctx.changes.push(n.ref);
-        }
-    }
-}
-function syncNewStates(ctx: UpdateContext, newState: StateTree.CellStates) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, { newState, changes: ctx.stateChanges }, syncNewStatesVisitor);
+function syncNewStates(ctx: UpdateContext) {
+    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx, syncNewStatesVisitor);
 }
 
 function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
@@ -441,7 +421,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
     const changed = cell.status !== status;
     cell.status = status;
     cell.errorText = errorText;
-    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
+    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cell });
 }
 
 function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
@@ -462,9 +442,11 @@ function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: Ini
     }
 
     const cell: StateObjectCell = {
+        parent: ctx.parent,
         transform,
         sourceRef: void 0,
         status: 'pending',
+        state: { ...transform.state },
         errorText: void 0,
         params: void 0,
         cache: void 0
@@ -505,7 +487,7 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Ma
         }
 
         const t = tree.transforms.get(s.value);
-        if (t.props && t.props.isGhost) continue;
+        if (t.state.isGhost) continue;
         if (s.value === ref) {
             seenRef = true;
             if (!deletes.has(ref)) prevCandidate = ref;
@@ -671,7 +653,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
 function updateTag(obj: StateObject | undefined, transform: StateTransform) {
     if (!obj || obj === StateObject.Null) return;
-    (obj.tag as string | undefined) = transform.props.tag;
+    (obj.tags as string[] | undefined) = transform.tags;
 }
 
 function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {

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

@@ -36,7 +36,7 @@ namespace StateBuilder {
         | { kind: 'add', transform: StateTransform }
         | { kind: 'update', ref: string, params: any }
         | { kind: 'delete', ref: string }
-        | { kind: 'insert', ref: string, transform: StateTransform, initialCellState?: Partial<StateObjectCell.State> }
+        | { kind: 'insert', ref: string, transform: StateTransform }
 
     function buildTree(state: BuildState) {
         if (!state.state || state.state.tree === state.editInfo.sourceTree) {
@@ -52,7 +52,7 @@ namespace StateBuilder {
                 case 'delete': tree.remove(a.ref); break;
                 case 'insert': {
                     const children = tree.children.get(a.ref).toArray();
-                    tree.add(a.transform, a.initialCellState);
+                    tree.add(a.transform);
                     for (const c of children) {
                         tree.changeParent(c, a.transform.ref);
                     }
@@ -84,12 +84,13 @@ namespace StateBuilder {
         }
         toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
         delete(ref: StateTransform.Ref) {
+            if (!this.state.tree.transforms.has(ref)) return this;
             this.editInfo.count++;
             this.state.tree.remove(ref);
             this.state.actions.push({ kind: 'delete', ref });
             return this;
         }
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        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 } } }
     }
 
@@ -102,9 +103,9 @@ namespace StateBuilder {
          * Apply the transformed to the parent node
          * If no params are specified (params <- undefined), default params are lazily resolved.
          */
-        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
@@ -113,23 +114,36 @@ namespace StateBuilder {
             return new To(this.state, t.ref, this.root);
         }
 
+        /**
+         * If the ref is present, the transform is applied.
+         * Otherwise a transform with the specifed ref is created.
+         */
+        applyOrUpdate<T extends StateTransformer<A, any, any>>(ref: StateTransform.Ref, tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
+            if (this.state.tree.transforms.has(ref)) {
+                this.to(ref).update(params);
+                return this.to(ref) as To<StateTransformer.To<T>>;
+            } else {
+                return this.apply(tr, params, { ...options, ref });
+            }
+        }
+
         /**
          * A helper to greate a group-like state object and keep the current type.
          */
-        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<A> {
-            return this.apply(tr, params, options, initialCellState) as To<A>;
+        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<A> {
+            return this.apply(tr, params, options) as To<A>;
         }
 
         /**
          * Inserts a new transform that does not change the object type and move the original children to it.
          */
-        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             // cache the children
             const children = this.state.tree.children.get(this.ref).toArray();
 
             // add the new node
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
 
             // move the original children to the new node
             for (const c of children) {
@@ -139,7 +153,7 @@ namespace StateBuilder {
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
-            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t, initialCellState });
+            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t });
 
             return new To(this.state, t.ref, this.root);
         }
@@ -193,7 +207,7 @@ namespace StateBuilder {
         toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
         delete(ref: StateTransform.Ref) { return this.root.delete(ref); }
 
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        getTree(): StateTree { return buildTree(this.state); }
 
         constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) {
             this.ref = ref;

+ 11 - 3
src/mol-state/state/selection.ts

@@ -49,6 +49,7 @@ namespace StateSelection {
         parent(): Builder<C>;
         first(): Builder<C>;
         filter(p: (n: C) => boolean): Builder<C>;
+        withTag(tag: string): Builder<C>;
         withTransformer<T extends StateTransformer<any, StateObjectCell.Obj<C>, any>>(t: T): Builder<StateObjectCell<StateObjectCell.Obj<C>, StateTransform<T>>>;
         withStatus(s: StateObjectCell.Status): Builder<C>;
         subtree(): Builder;
@@ -200,6 +201,9 @@ namespace StateSelection {
     registerModifier('withStatus', withStatus);
     export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); }
 
+    registerModifier('withTag', withTag);
+    export function withTag(b: Selector, tag: string) { return filter(b, n => !!n.transform.tags && n.transform.tags.indexOf(tag) >= 0); }
+
     registerModifier('subtree', subtree);
     export function subtree(b: Selector) {
         return flatMap(b, (n, s) => {
@@ -268,8 +272,12 @@ namespace StateSelection {
     }
 
     function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) {
-        if (n.props.tag && s.tags.has(n.props.tag)) {
-            s.refs[n.props.tag] = n.ref;
+        if (n.tags) {
+            for (const t of n.tags) {
+                if (!s.tags.has(t)) continue;
+                s.refs[t] = n.ref;
+                break;
+            }
         }
         return true;
     }
@@ -279,7 +287,7 @@ namespace StateSelection {
     }
 
     function _findTagInSubtree(n: StateTransform, _: any, s: { ref: string | undefined, tag: string }) {
-        if (n.props.tag === s.tag) {
+        if (n.tags && n.tags.indexOf(s.tag) >= 0) {
             s.ref = n.ref;
             return false;
         }

+ 87 - 14
src/mol-state/transform.ts

@@ -12,7 +12,8 @@ export { Transform as StateTransform }
 interface Transform<T extends StateTransformer = StateTransformer> {
     readonly parent: Transform.Ref,
     readonly transformer: T,
-    readonly props: Transform.Props,
+    readonly state: Transform.State,
+    readonly tags?: string[],
     readonly ref: Transform.Ref,
     readonly params?: StateTransformer.Params<T>,
     readonly version: string
@@ -24,24 +25,80 @@ namespace Transform {
 
     export const RootRef = '-=root=-' as Ref;
 
-    export interface Props {
-        tag?: string
+    export interface State {
+        // is the cell shown in the UI
         isGhost?: boolean,
-        // determine if the corresponding cell can be deleted by the user.
-        isLocked?: boolean
+        // can the corresponding be deleted by the user.
+        isLocked?: boolean,
+        // is the representation associated with the cell hidden
+        isHidden?: boolean,
+        // is the tree node collapsed?
+        isCollapsed?: boolean
+    }
+
+    export function areStatesEqual(a: State, b: State) {
+        return !!a.isHidden !== !!b.isHidden || !!a.isCollapsed !== !!b.isCollapsed
+            || !!a.isGhost !== !!b.isGhost || !!a.isLocked !== !!b.isLocked;
+    }
+
+    export function isStateChange(a: State, b?: Partial<State>) {
+        if (!b) return false;
+        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
+        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
+        if (typeof b.isGhost !== 'undefined' && a.isGhost !== b.isGhost) return true;
+        if (typeof b.isLocked !== 'undefined' && a.isLocked !== b.isLocked) return true;
+        return false;
+    }
+
+    export function assignState(a: State, b?: Partial<State>): boolean {
+        if (!b) return false;
+
+        let changed = false;
+        for (const k of Object.keys(b)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        return changed;
+    }
+
+    export function syncState(a: State, b?: Partial<State>): boolean {
+        if (!b) return false;
+
+        let changed = false;
+        for (const k of Object.keys(b)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        for (const k of Object.keys(a)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        return changed;
     }
 
     export interface Options {
         ref?: string,
-        props?: Props
+        tags?: string | string[],
+        state?: State
     }
 
     export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
+        let tags: string[] | undefined = void 0;
+        if (options && options.tags) {
+            tags = typeof options.tags === 'string' ? [options.tags] : options.tags;
+        }
         return {
             parent,
             transformer,
-            props: (options && options.props) || { },
+            state: (options && options.state) || { },
+            tags,
             ref,
             params,
             version: UUID.create22()
@@ -52,23 +109,30 @@ namespace Transform {
         return { ...t, params, version: UUID.create22() };
     }
 
+    export function withState(t: Transform, state?: Partial<State>): Transform {
+        if (!state) return t;
+        return { ...t, state: { ...t.state, ...state } };
+    }
+
     export function withParent(t: Transform, parent: Ref): Transform {
         return { ...t, parent, version: UUID.create22() };
     }
 
-    export function withNewVersion(t: Transform): Transform {
-        return { ...t, version: UUID.create22() };
+    export function createRoot(state?: State): Transform {
+        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, state });
     }
 
-    export function createRoot(props?: Props): Transform {
-        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props });
+    export function hasTag(t: Transform, tag: string) {
+        if (!t.tags) return false;
+        return t.tags.indexOf(tag) >= 0;
     }
 
     export interface Serialized {
         parent: string,
         transformer: string,
         params: any,
-        props: Props,
+        state?: State,
+        tags?: string[],
         ref: string,
         version: string
     }
@@ -78,11 +142,19 @@ namespace Transform {
         const pToJson = t.transformer.definition.customSerialization
             ? t.transformer.definition.customSerialization.toJSON
             : _id;
+        let state: any = void 0;
+        for (const k of Object.keys(t.state)) {
+            const s = (t.state as any)[k];
+            if (!s) continue;
+            if (!state) state = { };
+            state[k] = true;
+        }
         return {
             parent: t.parent,
             transformer: t.transformer.id,
             params: t.params ? pToJson(t.params) : void 0,
-            props: t.props,
+            state,
+            tags: t.tags,
             ref: t.ref,
             version: t.version
         };
@@ -97,7 +169,8 @@ namespace Transform {
             parent: t.parent as Ref,
             transformer,
             params: t.params ? pFromJson(t.params) : void 0,
-            props: t.props,
+            state: t.state || { },
+            tags: t.tags,
             ref: t.ref as Ref,
             version: t.version
         };

+ 4 - 1
src/mol-state/transformer.ts

@@ -86,6 +86,9 @@ namespace Transformer {
         /** By default, returns true */
         isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string },
 
+        /** Parameter interpolation */
+        interpolate?(src: P, target: P, t: number, globalCtx: unknown): P
+
         /** Custom conversion to and from JSON */
         readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
     }
@@ -95,7 +98,7 @@ namespace Transformer {
         readonly from: StateObject.Ctor[],
         readonly to: StateObject.Ctor[],
         readonly display: { readonly name: string, readonly description?: string },
-        params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any },
+        params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any }
     }
 
     const registry = new Map<Id, Transformer<any, any>>();

+ 12 - 18
src/mol-state/tree/immutable.ts

@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
-import { StateObjectCell } from 'mol-state/object';
 
 export { StateTree }
 
@@ -19,7 +18,6 @@ interface StateTree {
     readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
-    readonly cellStates: StateTree.CellStates,
 
     asTransient(): TransientTree
 }
@@ -43,7 +41,6 @@ namespace StateTree {
 
     export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
-    export interface CellStates extends _Map<StateObjectCell.State> { }
 
     class Impl implements StateTree {
         get root() { return this.transforms.get(StateTransform.RootRef)! }
@@ -52,7 +49,7 @@ namespace StateTree {
             return new TransientTree(this);
         }
 
-        constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
+        constructor(public transforms: StateTree.Transforms, public children: Children) {
         }
     }
 
@@ -61,11 +58,11 @@ namespace StateTree {
      */
     export function createEmpty(customRoot?: StateTransform): StateTree {
         const root = customRoot || StateTransform.createRoot();
-        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]]));
+        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]));
     }
 
-    export function create(nodes: Transforms, children: Children, cellStates: CellStates): StateTree {
-        return new Impl(nodes, children, cellStates);
+    export function create(nodes: Transforms, children: Children): StateTree {
+        return new Impl(nodes, children);
     }
 
     type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void };
@@ -116,19 +113,19 @@ namespace StateTree {
         return doPostOrder<StateTransform[]>(tree, root, [], _subtree);
     }
 
-    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) {
+    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: StateTransform.Serialized[]) {
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
-        ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]);
+        ctx.push(StateTransform.toJSON(node));
     }
 
     export interface Serialized {
         /** Transforms serialized in pre-order */
-        transforms: [StateTransform.Serialized, StateObjectCell.State][]
+        transforms: StateTransform.Serialized[]
     }
 
     export function toJSON(tree: StateTree): Serialized {
-        const transforms: [StateTransform.Serialized, StateObjectCell.State][] = [];
+        const transforms: StateTransform.Serialized[] = [];
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
     }
@@ -136,12 +133,10 @@ namespace StateTree {
     export function fromJSON(data: Serialized): StateTree {
         const nodes = ImmutableMap<Ref, StateTransform>().asMutable();
         const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
-        const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
         for (const t of data.transforms) {
-            const transform = StateTransform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t);
             nodes.set(transform.ref, transform);
-            cellStates.set(transform.ref, t[1]);
 
             if (!children.has(transform.ref)) {
                 children.set(transform.ref, OrderedSet<Ref>().asMutable());
@@ -151,19 +146,18 @@ namespace StateTree {
         }
 
         for (const t of data.transforms) {
-            const ref = t[0].ref;
+            const ref = t.ref;
             children.set(ref, children.get(ref).asImmutable());
         }
 
-        return create(nodes.asImmutable(), children.asImmutable(), cellStates.asImmutable());
+        return create(nodes.asImmutable(), children.asImmutable());
     }
 
     export function dump(tree: StateTree) {
         console.log({
             tr: (tree.transforms as ImmutableMap<any, any>).keySeq().toArray(),
             tr1: (tree.transforms as ImmutableMap<any, any>).valueSeq().toArray().map(t => t.ref),
-            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray(),
-            cs: (tree.cellStates as ImmutableMap<any, any>).keySeq().toArray()
+            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray()
         });
     }
 }

+ 17 - 44
src/mol-state/tree/transient.ts

@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
-import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 
 export { TransientTree }
@@ -15,13 +14,12 @@ export { TransientTree }
 class TransientTree implements StateTree {
     transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
     children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
-    cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>;
 
     private changedNodes = false;
     private changedChildren = false;
-    private changedStates = false;
 
     private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
+    private _stateUpdates: Set<StateTransform.Ref> | undefined = void 0;
 
     private get childMutations() {
         if (this._childMutations) return this._childMutations;
@@ -29,12 +27,6 @@ class TransientTree implements StateTree {
         return this._childMutations;
     }
 
-    private changeStates() {
-        if (this.changedStates) return;
-        this.changedStates = true;
-        this.cellStates = this.cellStates.asMutable();
-    }
-
     private changeNodes() {
         if (this.changedNodes) return;
         this.changedNodes = true;
@@ -49,10 +41,6 @@ class TransientTree implements StateTree {
 
     get root() { return this.transforms.get(StateTransform.RootRef)! }
 
-    cellStatesSnapshot() {
-        return this.cellStates.asImmutable();
-    }
-
     asTransient() {
         return this.asImmutable().asTransient();
     }
@@ -104,15 +92,7 @@ class TransientTree implements StateTree {
         this.transforms.set(ref, StateTransform.withParent(old, newParent));
     }
 
-    updateVersion(ref: StateTransform.Ref) {
-        ensurePresent(this.transforms, ref);
-
-        const t = this.transforms.get(ref);
-        this.changeNodes();
-        this.transforms.set(ref, StateTransform.withNewVersion(t));
-    }
-
-    add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) {
+    add(transform: StateTransform) {
         const ref = transform.ref;
 
         if (this.transforms.has(transform.ref)) {
@@ -138,15 +118,6 @@ class TransientTree implements StateTree {
         this.changeNodes();
         this.transforms.set(ref, transform);
 
-        if (!this.cellStates.has(ref)) {
-            this.changeStates();
-            if (StateObjectCell.isStateChange(StateObjectCell.DefaultState, initialState)) {
-                this.cellStates.set(ref, { ...StateObjectCell.DefaultState, ...initialState });
-            } else {
-                this.cellStates.set(ref, StateObjectCell.DefaultState);
-            }
-        }
-
         return this;
     }
 
@@ -169,16 +140,21 @@ class TransientTree implements StateTree {
         return true;
     }
 
-    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
+    assignState(ref: StateTransform.Ref, state?: Partial<StateTransform.State>) {
         ensurePresent(this.transforms, ref);
 
-        const old = this.cellStates.get(ref);
-        if (!StateObjectCell.isStateChange(old, state)) return false;
-
-        this.changeStates();
-        this.cellStates.set(ref, { ...old, ...state });
-
-        return true;
+        const old = this.transforms.get(ref);
+        if (this._stateUpdates && this._stateUpdates.has(ref)) {
+            StateTransform.assignState(old.state, state);
+            return old;
+        } else {
+            if (!this._stateUpdates) this._stateUpdates = new Set();
+            this._stateUpdates.add(old.ref);
+            this.changeNodes();
+            const updated = StateTransform.withState(old, state);
+            this.transforms.set(ref, updated);
+            return updated;
+        }
     }
 
     remove(ref: StateTransform.Ref): StateTransform[] {
@@ -197,12 +173,10 @@ class TransientTree implements StateTree {
 
         this.changeNodes();
         this.changeChildren();
-        this.changeStates();
 
         for (const n of st) {
             this.transforms.delete(n.ref);
             this.children.delete(n.ref);
-            this.cellStates.delete(n.ref);
             if (this._childMutations) this._childMutations.delete(n.ref);
         }
 
@@ -210,12 +184,11 @@ class TransientTree implements StateTree {
     }
 
     asImmutable() {
-        if (!this.changedNodes && !this.changedChildren && !this.changedStates && !this._childMutations) return this.tree;
+        if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree;
         if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children);
         return StateTree.create(
             this.changedNodes ? this.transforms.asImmutable() : this.transforms,
-            this.changedChildren ? this.children.asImmutable() : this.children,
-            this.changedStates ? this.cellStates.asImmutable() : this.cellStates);
+            this.changedChildren ? this.children.asImmutable() : this.children);
     }
 
     constructor(private tree: StateTree) {

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

@@ -168,7 +168,7 @@ export namespace ParamDefinition {
     }
 
     export interface NamedParams<T = any, K = string> { name: K, params: T }
-    export type NamedParamUnion<P extends Params, K = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never
+    export type NamedParamUnion<P extends Params, K extends keyof P = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never
     export interface Mapped<T extends NamedParams<any, any>> extends Base<T> {
         type: 'mapped',
         select: Select<string>,

+ 2 - 2
src/perf-tests/mol-script.ts

@@ -1,6 +1,6 @@
 import { MolScriptBuilder } from 'mol-script/language/builder';
 import { compile, QuerySymbolRuntime, DefaultQueryRuntimeTable } from 'mol-script/runtime/query/compiler';
-import { QueryContext, Structure, StructureQuery, ModelPropertyDescriptor } from 'mol-model/structure';
+import { QueryContext, Structure, StructureQuery, CustomPropertyDescriptor } from 'mol-model/structure';
 import { readCifFile, getModelsAndStructure } from '../apps/structure-info/model';
 import { CustomPropSymbol } from 'mol-script/language/symbol';
 import Type from 'mol-script/language/type';
@@ -46,7 +46,7 @@ const compiled = compile<number>(expr);
 const result = compiled(new QueryContext(Structure.Empty));
 console.log(result);
 
-const CustomProp = ModelPropertyDescriptor({
+const CustomProp = CustomPropertyDescriptor({
     name: 'test_prop',
     isStatic: true,
     cifExport: { prefix: '', categories: [ ]},