Kaynağa Gözat

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

# Conflicts:
#	src/mol-plugin/behavior/dynamic/representation.ts
Alexander Rose 6 yıl önce
ebeveyn
işleme
9b83ab5125

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

@@ -123,7 +123,7 @@ class BasicWrapper {
         applyStripes: async () => {
             const state = this.plugin.state.dataState;
 
-            const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
+            const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
             const tree = state.build();
             const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues };
 

+ 1 - 1
src/examples/proteopedia-wrapper/index.ts

@@ -190,7 +190,7 @@ class MolStarProteopediaWrapper {
 
             const state = this.state;
 
-            // const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
+            // const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
             const tree = state.build();
             const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
 

+ 9 - 2
src/mol-canvas3d/canvas3d.ts

@@ -52,7 +52,7 @@ interface Canvas3D {
 
     add: (repr: Representation.Any) => void
     remove: (repr: Representation.Any) => void
-    update: () => void
+    update: (repr?: Representation.Any, keepBoundingSphere?: boolean) => void
     clear: () => void
 
     // draw: (force?: boolean) => void
@@ -357,7 +357,14 @@ namespace Canvas3D {
                     reprCount.next(reprRenderObjects.size)
                 }
             },
-            update: () => scene.update(void 0, false),
+            update: (repr, keepSphere) => {
+                if (repr) {
+                    if (!reprRenderObjects.has(repr)) return;
+                    scene.update(repr.renderObjects, !!keepSphere);
+                } else {
+                    scene.update(void 0, !!keepSphere)
+                }
+            },
             clear: () => {
                 reprRenderObjects.clear()
                 scene.clear()

+ 6 - 0
src/mol-model/structure/structure/util/unit-transforms.ts

@@ -15,9 +15,11 @@ export class StructureUnitTransforms {
     /** maps unit.id to offset of transform in unitTransforms */
     private unitOffsetMap = IntMap.Mutable<number>();
     private groupIndexMap = IntMap.Mutable<number>();
+    private size: number;
 
     constructor(readonly structure: Structure) {
         this.unitTransforms = new Float32Array(structure.units.length * 16)
+        this.size = structure.units.length
         fillIdentityTransform(this.unitTransforms, structure.units.length)
         let groupOffset = 0
         for (let i = 0, il = structure.unitSymmetryGroups.length; i <il; ++i) {
@@ -32,6 +34,10 @@ export class StructureUnitTransforms {
         }
     }
 
+    reset() {
+        fillIdentityTransform(this.unitTransforms, this.size);
+    }
+
     setTransform(matrix: Mat4, unit: Unit) {
         Mat4.toArray(matrix, this.unitTransforms, this.unitOffsetMap.get(unit.id))
     }

+ 6 - 6
src/mol-plugin/behavior/dynamic/animation.ts

@@ -63,7 +63,7 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
         rotate(rad: number) {
             this.updatedUnitTransforms.clear()
             const state = this.ctx.state.dataState
-            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D))
+            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure.Representation3D))
             Mat4.rotate(this.rotMat, this.tmpMat, rad, this.rotVec)
             for (const r of reprs) {
                 if (!SO.isRepresentation3D(r.obj)) return
@@ -90,8 +90,8 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
                     this.updatedUnitTransforms.add(structure.obj)
                 }
 
-                r.obj.data.setState({ unitTransforms })
-                this.ctx.canvas3d.add(r.obj.data)
+                r.obj.data.repr.setState({ unitTransforms })
+                this.ctx.canvas3d.add(r.obj.data.repr)
             }
             this.ctx.canvas3d.requestDraw(true)
         }
@@ -111,7 +111,7 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
         explode(p: number) {
             this.updatedUnitTransforms.clear()
             const state = this.ctx.state.dataState
-            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
+            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure.Representation3D));
             for (const r of reprs) {
                 if (!SO.isRepresentation3D(r.obj)) return
                 const structure = getRootStructure(r, state)
@@ -134,8 +134,8 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
                     this.updatedUnitTransforms.add(structure.obj)
                 }
 
-                r.obj.data.setState({ unitTransforms })
-                this.ctx.canvas3d.add(r.obj.data)
+                r.obj.data.repr.setState({ unitTransforms })
+                this.ctx.canvas3d.add(r.obj.data.repr)
             }
             this.ctx.canvas3d.requestDraw(true)
         }

+ 14 - 159
src/mol-plugin/behavior/dynamic/representation.ts

@@ -6,27 +6,23 @@
  */
 
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { EmptyLoci } from 'mol-model/loci';
-import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+import { QueryContext, StructureElement, StructureSelection } from 'mol-model/structure';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { StateObjectTracker, StateSelection } from 'mol-state';
-import { labelFirst } from 'mol-theme/label';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { PluginBehavior } from '../behavior';
 import { Representation } from 'mol-repr/representation';
-import { ButtonsType } from 'mol-util/input/input-observer';
-import { StructureElement, StructureSelection, QueryContext } from 'mol-model/structure';
-import { ColorNames } from 'mol-util/color/tables';
-// import { MolScriptBuilder as MS } from 'mol-script/language/builder';
 import Expression from 'mol-script/language/expression';
-import { Color } from 'mol-util/color';
-import { compile } from 'mol-script/runtime/query/compiler';
-import { Overpaint } from 'mol-theme/overpaint';
 import { parseMolScript } from 'mol-script/language/parser';
+import { compile } from 'mol-script/runtime/query/compiler';
 import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
-import { SymmetryOperator } from 'mol-math/geometry';
+import { StateObjectTracker, StateSelection } from 'mol-state';
+import { labelFirst } from 'mol-theme/label';
+import { Overpaint } from 'mol-theme/overpaint';
+import { Color } from 'mol-util/color';
+import { ColorNames } from 'mol-util/color/tables';
+import { ButtonsType } from 'mol-util/input/input-observer';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginBehavior } from '../behavior';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
@@ -129,84 +125,6 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
     display: { name: 'Provide Default Loci Label' }
 });
 
-export namespace ExplodeRepresentation3D {
-    export const Params = {
-        t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 })
-    }
-    export type Params = PD.Values<typeof Params>
-
-    export class Behavior implements PluginBehavior<Params> {
-        private currentT = 0;
-        private repr: StateObjectTracker<PluginStateObject.Molecule.Representation3D>;
-        private structure: StateObjectTracker<PluginStateObject.Molecule.Structure>;
-        private transforms: StructureUnitTransforms;
-
-        private updateData() {
-            const reprUpdated = this.repr.update();
-            const strUpdated = this.structure.update();
-            if (strUpdated && this.structure.data) {
-                this.transforms = new StructureUnitTransforms(this.structure.data);
-            }
-            return reprUpdated || strUpdated;
-        }
-
-        register(ref: string): void {
-            this.repr.setQuery(StateSelection.Generators.byRef(ref).ancestorOfType([PluginStateObject.Molecule.Representation3D]));
-            this.structure.setQuery(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]));
-            this.update(this.params);
-        }
-
-        private centerVec = Vec3.zero();
-        private transVec = Vec3.zero();
-        private transMat = Mat4.zero();
-
-        update(params: Params): boolean | Promise<boolean> {
-            if (!this.updateData() && params.t === this.currentT) return false;
-            this.currentT = params.t;
-            if (!this.structure.data || !this.repr.data) return true;
-
-            const structure = this.structure.data;
-            const boundary = structure.boundary.sphere;
-            const d = boundary.radius * params.t;
-
-            for (let i = 0, _i = structure.units.length; i < _i; i++) {
-                const u = structure.units[i];
-
-                Vec3.transformMat4(this.centerVec, u.lookup3d.boundary.sphere.center, u.conformation.operator.matrix);
-                Vec3.sub(this.transVec, this.centerVec, boundary.center);
-                Vec3.setMagnitude(this.transVec, this.transVec, d);
-                Mat4.fromTranslation(this.transMat, this.transVec)
-
-                this.transforms.setTransform(this.transMat, u);
-            }
-
-            // TODO: should be be "auto updated"?
-            // perhaps have Representation3D.setState(state, autoSync = false)?
-
-            // TODO: where to handle unitTransforms composition?
-            // Manually or inside the representation? "inside" would better compose with future additions.
-            this.repr.data.setState({ unitTransforms: this.transforms });
-            this.ctx.canvas3d.add(this.repr.data);
-            this.ctx.canvas3d.requestDraw(true);
-
-            return true;
-        }
-
-        unregister(): void {
-            this.update({ t: 0 })
-            this.repr.cell = void 0;
-            this.structure.cell = void 0;
-        }
-
-        constructor(private ctx: PluginContext, private params: Params) {
-            this.repr = new StateObjectTracker(ctx.state.dataState);
-            this.structure = new StateObjectTracker(ctx.state.dataState);
-        }
-    }
-
-    export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Explode Representation3D Behavior' }) { }
-}
-
 type ColorMappings = { query: Expression, color: Color }[]
 namespace ColorMappings {
     export function areEqual(colorMappingsA: ColorMappings, colorMappingsB: ColorMappings) {
@@ -243,7 +161,7 @@ export namespace ColorRepresentation3D {
 
     export class Behavior implements PluginBehavior<Params> {
         private currentColorMappings: ColorMappings = [];
-        private repr: StateObjectTracker<PluginStateObject.Molecule.Representation3D>;
+        private repr: StateObjectTracker<PluginStateObject.Molecule.Structure.Representation3D>;
         private structure: StateObjectTracker<PluginStateObject.Molecule.Structure>;
 
         private updateData() {
@@ -253,7 +171,7 @@ export namespace ColorRepresentation3D {
         }
 
         register(ref: string): void {
-            this.repr.setQuery(StateSelection.Generators.byRef(ref).ancestorOfType([PluginStateObject.Molecule.Representation3D]));
+            this.repr.setQuery(StateSelection.Generators.byRef(ref).ancestorOfType([PluginStateObject.Molecule.Structure.Representation3D]));
             this.structure.setQuery(StateSelection.Generators.byRef(ref).ancestorOfType([PluginStateObject.Molecule.Structure]));
             this.update(this.params);
         }
@@ -288,8 +206,8 @@ export namespace ColorRepresentation3D {
 
         private applyLayers(layers: Overpaint.Layers, clear: boolean): boolean {
             if (!this.repr.data) return true;
-            this.repr.data.setOverpaint(layers, clear)
-            this.ctx.canvas3d.add(this.repr.data);
+            this.repr.data.repr.setOverpaint(layers)
+            this.ctx.canvas3d.add(this.repr.data.repr);
             this.ctx.canvas3d.requestDraw(true);
             return true;
         }
@@ -307,67 +225,4 @@ export namespace ColorRepresentation3D {
     }
 
     export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Color Representation3D Behavior' }) { }
-}
-
-export namespace UnwindAssemblyRepresentation3D {
-    export const Params = {
-        t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 })
-    }
-    export type Params = PD.Values<typeof Params>
-
-    export class Behavior implements PluginBehavior<Params> {
-        private currentT = 0;
-        private repr: StateObjectTracker<PluginStateObject.Molecule.Representation3D>;
-        private structure: StateObjectTracker<PluginStateObject.Molecule.Structure>;
-        private transforms: StructureUnitTransforms;
-
-        private updateData() {
-            const reprUpdated = this.repr.update();
-            const strUpdated = this.structure.update();
-            if (strUpdated && this.structure.data) {
-                this.transforms = new StructureUnitTransforms(this.structure.data);
-            }
-            return reprUpdated || strUpdated;
-        }
-
-        register(ref: string): void {
-            this.repr.setQuery(StateSelection.Generators.byRef(ref).ancestorOfType([PluginStateObject.Molecule.Representation3D]));
-            this.structure.setQuery(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]));
-            this.update(this.params);
-        }
-
-        private transMat = Mat4.zero();
-
-        update(params: Params): boolean | Promise<boolean> {
-            if (!this.updateData() && params.t === this.currentT) return false;
-            this.currentT = params.t;
-            if (!this.structure.data || !this.repr.data) return true;
-
-            const structure = this.structure.data;
-            for (let i = 0, _i = structure.units.length; i < _i; i++) {
-                const u = structure.units[i];
-                SymmetryOperator.lerpFromIdentity(this.transMat, u.conformation.operator, this.currentT);
-                this.transforms.setTransform(this.transMat, u);
-            }
-
-            this.repr.data.setState({ unitTransforms: this.transforms });
-            this.ctx.canvas3d.add(this.repr.data);
-            this.ctx.canvas3d.requestDraw(true);
-
-            return true;
-        }
-
-        unregister(): void {
-            this.update({ t: 0 })
-            this.repr.cell = void 0;
-            this.structure.cell = void 0;
-        }
-
-        constructor(private ctx: PluginContext, private params: Params) {
-            this.repr = new StateObjectTracker(ctx.state.dataState);
-            this.structure = new StateObjectTracker(ctx.state.dataState);
-        }
-    }
-
-    export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Explode Representation3D Behavior' }) { }
 }

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

@@ -179,7 +179,7 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
         const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
         repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
         await repr.createOrUpdate(props, channel.data).runInContext(ctx);
-        return new SO.Volume.Representation3D(repr, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
+        return new SO.Volume.Representation3D({ repr, source: a }, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
     }),
     update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
         // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
@@ -189,9 +189,9 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
         if (!channel) return StateTransformer.UpdateResult.Unchanged;
 
         const params = createVolumeProps(a.data, newParams.channel);
-        const props = { ...b.data.props, ...params.type.params };
-        b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
-        await b.data.createOrUpdate(props, channel.data).runInContext(ctx);
+        const props = { ...b.data.repr.props, ...params.type.params };
+        b.data.repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
+        await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx);
         return StateTransformer.UpdateResult.Updated;
     })
 });

+ 40 - 11
src/mol-plugin/behavior/static/representation.ts

@@ -11,6 +11,7 @@ import { State } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
+    SyncStructureRepresentation3DState(ctx); // should be AFTER SyncRepresentationToCanvas
     UpdateRepresentationVisibility(ctx);
 }
 
@@ -20,44 +21,72 @@ 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);
-        e.obj.data.setState({ syncManually: true });
-        ctx.canvas3d.add(e.obj.data);
+        updateVisibility(e, e.obj.data.repr);
+        e.obj.data.repr.setState({ syncManually: true });
+        ctx.canvas3d.add(e.obj.data.repr);
 
         if (reprCount === 0) ctx.canvas3d.resetCamera();
         reprCount++;
     });
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
-            ctx.canvas3d.remove(e.oldObj.data);
+            ctx.canvas3d.remove(e.oldObj.data.repr);
             ctx.canvas3d.requestDraw(true);
-            e.oldObj.data.destroy();
+            e.oldObj.data.repr.destroy();
         }
 
         if (!SO.isRepresentation3D(e.obj)) {
             return;
         }
 
-        updateVisibility(e, e.obj.data);
+        updateVisibility(e, e.obj.data.repr);
         if (e.action === 'recreate') {
-            e.obj.data.setState({ syncManually: true });
+            e.obj.data.repr.setState({ syncManually: true });
         }
-        ctx.canvas3d.add(e.obj.data);
+        ctx.canvas3d.add(e.obj.data.repr);
     });
     events.object.removed.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        ctx.canvas3d.remove(e.obj.data);
+        ctx.canvas3d.remove(e.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
-        e.obj.data.destroy();
+        e.obj.data.repr.destroy();
         reprCount--;
     });
 }
 
+
+export function SyncStructureRepresentation3DState(ctx: PluginContext) {
+    // TODO: figure out how to do transform composition here?
+    const events = ctx.state.dataState.events;
+    events.object.created.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.state);
+        ctx.canvas3d.update(data.source.data.repr);
+        ctx.canvas3d.requestDraw(true);
+    });
+    events.object.updated.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.state);
+        ctx.canvas3d.update(data.source.data.repr);
+        ctx.canvas3d.requestDraw(true);
+    });
+    events.object.removed.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.initialState);
+        ctx.canvas3d.update(data.source.data.repr);
+        ctx.canvas3d.requestDraw(true);
+    });
+}
+
+
 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);
+        updateVisibility(e, cell.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
     })
 }

+ 1 - 0
src/mol-plugin/skin/base/components/viewport.scss

@@ -26,6 +26,7 @@
     -webkit-user-select: none;
     -webkit-tap-highlight-color: rgba(0,0,0,0);
     -webkit-touch-callout: none;
+    touch-action: manipulation;
 }
 
 .msp-viewport-controls {

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

@@ -10,7 +10,6 @@ import { StateTransforms } from '../transforms';
 import { StateSelection } from 'mol-state';
 import { PluginCommands } from 'mol-plugin/command';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { UnwindAssemblyRepresentation3D } from 'mol-plugin/behavior/dynamic/representation';
 
 export const AnimateModelIndex = PluginStateAnimation.create({
     name: 'built-in.animate-model-index',
@@ -93,7 +92,8 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
     initialState: () => ({ t: 0 }),
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.dataState;
-        const anims = state.selectQ(q => q.ofType(UnwindAssemblyRepresentation3D.Obj));
+        const anims = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure.Representation3DState)
+            .filter(c => c.transform.transformer === StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D));
 
         if (anims.length === 0) {
             // nothing more to do here

+ 36 - 0
src/mol-plugin/state/animation/helpers.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+
+import { SymmetryOperator } from 'mol-math/geometry';
+import { Mat4, Vec3 } from 'mol-math/linear-algebra';
+import { Structure } from 'mol-model/structure';
+import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+
+const _unwindMatrix = Mat4.zero();
+export function unwindStructureAssembly(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) {
+    for (let i = 0, _i = structure.units.length; i < _i; i++) {
+        const u = structure.units[i];
+        SymmetryOperator.lerpFromIdentity(_unwindMatrix, u.conformation.operator, t);
+        unitTransforms.setTransform(_unwindMatrix, u);
+    }
+}
+
+const _centerVec = Vec3.zero(), _transVec = Vec3.zero(), _transMat = Mat4.zero();
+export function explodeStructure(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) {
+    const boundary = structure.boundary.sphere;
+    const d = boundary.radius * t;
+
+    for (let i = 0, _i = structure.units.length; i < _i; i++) {
+        const u = structure.units[i];
+        Vec3.transformMat4(_centerVec, u.lookup3d.boundary.sphere.center, u.conformation.operator.matrix);
+        Vec3.sub(_transVec, _centerVec, boundary.center);
+        Vec3.setMagnitude(_transVec, _transVec, d);
+        Mat4.fromTranslation(_transMat, _transVec);
+
+        unitTransforms.setTransform(_transMat, u);
+    }
+}

+ 18 - 6
src/mol-plugin/state/objects.ts

@@ -10,7 +10,7 @@ import { Model as _Model, Structure as _Structure } from 'mol-model/structure';
 import { VolumeData } from 'mol-model/volume';
 import { PluginBehavior } from 'mol-plugin/behavior/behavior';
 import { Representation } from 'mol-repr/representation';
-import { StructureRepresentation } from 'mol-repr/structure/representation';
+import { StructureRepresentation, StructureRepresentationState } from 'mol-repr/structure/representation';
 import { VolumeRepresentation } from 'mol-repr/volume/representation';
 import { StateObject, StateTransformer } from 'mol-state';
 import { Ccp4File } from 'mol-io/reader/ccp4/schema';
@@ -27,7 +27,7 @@ export namespace PluginStateObject {
 
     export const Create = StateObject.factory<TypeInfo>();
 
-    export function isRepresentation3D(o?: Any): o is StateObject<Representation.Any, TypeInfo> {
+    export function isRepresentation3D(o?: Any): o is StateObject<Representation3DData<Representation.Any>, TypeInfo> {
         return !!o && o.type.typeClass === 'Representation3D';
     }
 
@@ -35,8 +35,9 @@ export namespace PluginStateObject {
         return !!o && o.type.typeClass === 'Behavior';
     }
 
-    export function CreateRepresentation3D<T extends Representation.Any>(type: { name: string }) {
-        return Create<T>({ ...type, typeClass: 'Representation3D' })
+    export interface Representation3DData<T extends Representation.Any, S extends StateObject = StateObject> { repr: T, source: S }
+    export function CreateRepresentation3D<T extends Representation.Any, S extends StateObject = StateObject>(type: { name: string }) {
+        return Create<Representation3DData<T, S>>({ ...type, typeClass: 'Representation3D' });
     }
 
     export function CreateBehavior<T extends PluginBehavior>(type: { name: string }) {
@@ -80,8 +81,19 @@ export namespace PluginStateObject {
         export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { }
         export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { }
         export class Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { }
-        export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any> | ShapeRepresentation<any, any, any>>({ name: 'Structure 3D' }) { }
-        export class Representation3DState extends Create<{}>({ name: 'Structure 3D State', typeClass: 'Object' }) { }
+
+        export namespace Structure {
+            export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any> | ShapeRepresentation<any, any, any>, Structure>({ name: 'Structure 3D' }) { }
+
+            export interface Representation3DStateData {
+                source: Representation3D,
+                /** used to restore state when the obj is removed */
+                initialState: Partial<StructureRepresentationState>,
+                state: Partial<StructureRepresentationState>,
+                info?: unknown
+            }
+            export class Representation3DState extends Create<Representation3DStateData>({ name: 'Structure 3D State', typeClass: 'Object' }) { }
+        }
     }
 
     export namespace Volume {

+ 62 - 36
src/mol-plugin/state/transforms/representation.ts

@@ -7,7 +7,7 @@
 
 import { Structure } from 'mol-model/structure';
 import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
-import { ExplodeRepresentation3D, ColorRepresentation3D, UnwindAssemblyRepresentation3D } from 'mol-plugin/behavior/dynamic/representation';
+import { ColorRepresentation3D } from 'mol-plugin/behavior/dynamic/representation';
 import { PluginContext } from 'mol-plugin/context';
 import { RepresentationProvider } from 'mol-repr/representation';
 import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry';
@@ -25,6 +25,8 @@ import { Text } from 'mol-geo/geometry/text/text';
 import { ColorNames } from 'mol-util/color/tables';
 import { getLabelRepresentation } from 'mol-plugin/util/structure-labels';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+import { unwindStructureAssembly, explodeStructure } from '../animation/helpers';
 
 export { StructureRepresentation3D }
 export { StructureRepresentation3DHelpers }
@@ -118,7 +120,7 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'structure-representation-3d',
     display: '3D Representation',
     from: SO.Molecule.Structure,
-    to: SO.Molecule.Representation3D,
+    to: SO.Molecule.Structure.Representation3D,
     params: (a, ctx: PluginContext) => {
         const { registry, themeCtx } = ctx.structureRepresentation
         const type = registry.get(registry.default.name);
@@ -173,15 +175,15 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, params))
             // TODO set initial state, repr.setState({})
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Molecule.Representation3D(repr, { label: provider.label });
+            return new SO.Molecule.Structure.Representation3D({ repr, source: a } , { label: provider.label });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
-            const props = { ...b.data.props, ...newParams.type.params }
-            b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams));
-            await b.data.createOrUpdate(props, a.data).runInContext(ctx);
+            const props = { ...b.data.repr.props, ...newParams.type.params }
+            b.data.repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams));
+            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             return StateTransformer.UpdateResult.Updated;
         });
     }
@@ -193,7 +195,7 @@ const StructureLabels3D = PluginStateTransform.BuiltIn({
     name: 'structure-labels-3d',
     display: '3D Labels',
     from: SO.Molecule.Structure,
-    to: SO.Molecule.Representation3D,
+    to: SO.Molecule.Structure.Representation3D,
     params: {
         // TODO: other targets
         target: PD.MappedStatic('residues', {
@@ -219,12 +221,12 @@ const StructureLabels3D = PluginStateTransform.BuiltIn({
     apply({ a, params }) {
         return Task.create('Structure Labels', async ctx => {
             const repr = await getLabelRepresentation(ctx, a.data, params);
-            return new SO.Molecule.Representation3D(repr, { label: `Labels`, description: params.target.name });
+            return new SO.Molecule.Structure.Representation3D({ repr, source: a }, { label: `Labels`, description: params.target.name });
         });
     },
     update({ a, b, newParams }) {
         return Task.create('Structure Labels', async ctx => {
-            await getLabelRepresentation(ctx, a.data, newParams, b.data as ShapeRepresentation<any, any, any>);
+            await getLabelRepresentation(ctx, a.data, newParams, b.data.repr as ShapeRepresentation<any, any, any>);
             return StateTransformer.UpdateResult.Updated;
         });
     }
@@ -234,45 +236,69 @@ type UnwindStructureAssemblyRepresentation3D = typeof UnwindStructureAssemblyRep
 const UnwindStructureAssemblyRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'unwind-structure-assembly-representation-3d',
     display: 'Unwind Assembly 3D Representation',
-    from: SO.Molecule.Representation3D,
-    to: UnwindAssemblyRepresentation3D.Obj,
-    params: UnwindAssemblyRepresentation3D.Params
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: { t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) }
 })({
     canAutoUpdate() {
         return true;
     },
-    apply({ params }, plugin: PluginContext) {
-        return new UnwindAssemblyRepresentation3D.Obj(new UnwindAssemblyRepresentation3D.Behavior(plugin, params), { label: `Unwind T = ${params.t.toFixed(2)}` });
+    apply({ a, params }) {
+        const structure = a.data.source.data;
+        const unitTransforms = new StructureUnitTransforms(structure);
+        unwindStructureAssembly(structure, unitTransforms, params.t);
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { unitTransforms },
+            initialState: { unitTransforms: new StructureUnitTransforms(structure) },
+            info: structure,
+            source: a
+        }, { label: `Unwind T = ${params.t.toFixed(2)}` });
     },
-    update({ b, newParams }) {
-        return Task.create('Update Unwind', async () => {
-            const updated = await b.data.update(newParams);
-            b.label = `Unwind T = ${newParams.t.toFixed(2)}`;
-            return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
-        });
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure;
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
+        const unitTransforms = b.data.state.unitTransforms!;
+        unwindStructureAssembly(structure, unitTransforms, newParams.t);
+        b.label = `Unwind T = ${newParams.t.toFixed(2)}`;
+        b.data.source = a;
+        return StateTransformer.UpdateResult.Updated;
     }
 });
 
+
 type ExplodeStructureRepresentation3D = typeof ExplodeStructureRepresentation3D
 const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'explode-structure-representation-3d',
     display: 'Explode 3D Representation',
-    from: SO.Molecule.Representation3D,
-    to: ExplodeRepresentation3D.Obj,
-    params: ExplodeRepresentation3D.Params
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: { t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) }
 })({
     canAutoUpdate() {
         return true;
     },
-    apply({ params }, plugin: PluginContext) {
-        return new ExplodeRepresentation3D.Obj(new ExplodeRepresentation3D.Behavior(plugin, params), { label: `Explosion T = ${params.t.toFixed(2)}` });
+    apply({ a, params, spine }) {
+        const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data;
+        const unitTransforms = new StructureUnitTransforms(rootStructure);
+        explodeStructure(rootStructure, unitTransforms, params.t);
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { unitTransforms },
+            initialState: { unitTransforms: new StructureUnitTransforms(rootStructure) },
+            info: rootStructure,
+            source: a
+        }, { label: `Explode T = ${params.t.toFixed(2)}` });
     },
-    update({ b, newParams }) {
-        return Task.create('Update Explosion', async () => {
-            const updated = await b.data.update(newParams);
-            b.label = `Explosion T = ${newParams.t.toFixed(2)}`;
-            return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
-        });
+    update({ a, b, newParams, oldParams, spine }) {
+        const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data;
+        const structure = b.data.info as Structure;
+        if (rootStructure !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
+        const unitTransforms = b.data.state.unitTransforms!;
+        explodeStructure(structure, unitTransforms, newParams.t);
+        b.label = `Explode T = ${newParams.t.toFixed(2)}`;
+        b.data.source = a;
+        return StateTransformer.UpdateResult.Updated;
     }
 });
 
@@ -280,7 +306,7 @@ type ColorStructureRepresentation3D = typeof ColorStructureRepresentation3D
 const ColorStructureRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'color-structure-representation-3d',
     display: 'Color 3D Representation',
-    from: SO.Molecule.Representation3D,
+    from: SO.Molecule.Structure.Representation3D,
     to: ColorRepresentation3D.Obj,
     params: ColorRepresentation3D.Params
 })({
@@ -391,15 +417,15 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
             // TODO set initial state, repr.setState({})
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Volume.Representation3D(repr, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
+            return new SO.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Volume Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
-            const props = { ...b.data.props, ...newParams.type.params }
-            b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
-            await b.data.createOrUpdate(props, a.data).runInContext(ctx);
+            const props = { ...b.data.repr.props, ...newParams.type.params }
+            b.data.repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
+            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             b.description = VolumeRepresentation3DHelpers.getDescription(props)
             return StateTransformer.UpdateResult.Updated;
         });

+ 12 - 4
src/mol-state/state.ts

@@ -18,6 +18,7 @@ import { TransientTree } from './tree/transient';
 import { LogEntry } from 'mol-util/log-entry';
 import { now, formatTimespan } from 'mol-util/now';
 import { ParamDefinition } from 'mol-util/param-definition';
+import { StateTreeSpine } from './tree/spine';
 
 export { State }
 
@@ -59,6 +60,7 @@ class State {
     build() { return new StateBuilder.Root(this._tree); }
 
     readonly cells: State.Cells = new Map();
+    private spine = new StateTreeSpine.Impl(this.cells);
 
     getSnapshot(): State.Snapshot {
         return { tree: StateTree.toJSON(this._tree) };
@@ -140,6 +142,8 @@ class State {
                     return cell && cell.obj;
                 }
             } finally {
+                this.spine.setSurrent();
+
                 if (updated) this.events.changed.next();
                 this.events.isUpdating.next(false);
 
@@ -164,6 +168,7 @@ class State {
             oldTree,
             tree: _tree,
             cells: this.cells as Map<StateTransform.Ref, StateObjectCell>,
+            spine: this.spine,
 
             results: [],
             stateChanges: [],
@@ -243,6 +248,7 @@ interface UpdateContext {
     oldTree: StateTree,
     tree: TransientTree,
     cells: Map<StateTransform.Ref, StateObjectCell>,
+    spine: StateTreeSpine.Impl,
 
     results: UpdateNodeResult[],
     stateChanges: StateTransform.Ref[],
@@ -593,12 +599,14 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
     }
 
     let parentCell = transform.transformer.definition.from.length === 0
-        ? ctx.cells.get(current.transform.parent)
-        : StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
+    ? ctx.cells.get(current.transform.parent)
+    : StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
     if (!parentCell) {
         throw new Error(`No suitable parent found for '${currentRef}'`);
     }
 
+    ctx.spine.setSurrent(current);
+
     const parent = parentCell.obj!;
     current.sourceRef = parentCell.transform.ref;
 
@@ -653,7 +661,7 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
 
 function createObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, params: any) {
     if (!cell.cache) cell.cache = Object.create(null);
-    return runTask(transformer.definition.apply({ a, params, cache: cell.cache }, ctx.parent.globalContext), ctx.taskCtx);
+    return runTask(transformer.definition.apply({ a, params, cache: cell.cache, spine: ctx.spine }, ctx.parent.globalContext), ctx.taskCtx);
 }
 
 async function updateObject(ctx: UpdateContext, cell: StateObjectCell,  transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
@@ -661,5 +669,5 @@ async function updateObject(ctx: UpdateContext, cell: StateObjectCell,  transfor
         return StateTransformer.UpdateResult.Recreate;
     }
     if (!cell.cache) cell.cache = Object.create(null);
-    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache }, ctx.parent.globalContext), ctx.taskCtx);
+    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache, spine: ctx.spine }, ctx.parent.globalContext), ctx.taskCtx);
 }

+ 5 - 2
src/mol-state/transformer.ts

@@ -10,6 +10,7 @@ import { StateTransform } from './transform';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateAction } from './action';
 import { capitalize } from 'mol-util/string';
+import { StateTreeSpine } from './tree/spine';
 
 export { Transformer as StateTransformer }
 
@@ -36,7 +37,8 @@ namespace Transformer {
         a: A,
         params: P,
         /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
-        cache: unknown
+        cache: unknown,
+        spine: StateTreeSpine
     }
 
     export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
@@ -45,7 +47,8 @@ namespace Transformer {
         oldParams: P,
         newParams: P,
         /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
-        cache: unknown
+        cache: unknown,
+        spine: StateTreeSpine
     }
 
     export interface AutoUpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {

+ 55 - 0
src/mol-state/tree/spine.ts

@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { State } from '../state';
+import { StateTransform } from '../transform';
+import { StateObject, StateObjectCell } from '../object';
+
+export { StateTreeSpine }
+
+/** The tree spine allows access to ancestor of a node during reconciliation. */
+interface StateTreeSpine {
+    getAncestorOfType<T extends StateObject.Ctor>(type: T): StateObject.From<T> | undefined;
+    getRootOfType<T extends StateObject.Ctor>(type: T): StateObject.From<T> | undefined;
+}
+
+namespace StateTreeSpine {
+    export class Impl implements StateTreeSpine {
+        private current: StateObjectCell | undefined = void 0;
+        setSurrent(cell?: StateObjectCell) {
+            this.current = cell;
+        }
+
+        getAncestorOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
+            if (!this.current) return void 0;
+            let cell = this.current;
+            while (true) {
+                cell = this.cells.get(cell.transform.parent)!;
+                if (!cell.obj) return void 0;
+                if (cell.obj.type === t.type) return cell.obj as StateObject.From<T>;
+                if (cell.transform.ref === StateTransform.RootRef) return void 0;
+            }
+        }
+
+        getRootOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
+            if (!this.current) return void 0;
+            let cell = this.current;
+            let ret: StateObjectCell | undefined = void 0;
+            while (true) {
+                cell = this.cells.get(cell.transform.parent)!;
+                if (!cell.obj) return void 0;
+                if (cell.obj.type === t.type) {
+                    ret = cell;
+                }
+                if (cell.transform.ref === StateTransform.RootRef) return ret ? ret.obj as StateObject.From<T> : void 0;
+            }
+        }
+
+        constructor(private cells: State.Cells) {
+
+        }
+    }
+}

+ 12 - 2
src/mol-util/input/input-observer.ts

@@ -39,7 +39,8 @@ function getButtons(event: MouseEvent | Touch) {
 
 export const DefaultInputObserverProps = {
     noScroll: true,
-    noContextMenu: true
+    noContextMenu: true,
+    noPinchZoom: true
 }
 export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
 
@@ -160,7 +161,7 @@ interface InputObserver {
 
 namespace InputObserver {
     export function create (element: Element, props: InputObserverProps = {}): InputObserver {
-        let { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props }
+        let { noScroll, noContextMenu, noPinchZoom } = { ...DefaultInputObserverProps, ...props }
 
         let lastTouchDistance = 0
         const pointerDown = Vec2.zero()
@@ -336,6 +337,15 @@ namespace InputObserver {
         }
 
         function onTouchMove (ev: TouchEvent) {
+            if (noPinchZoom) {
+                ev.preventDefault();
+                ev.stopPropagation();
+                if ((ev as any).originalEvent) {
+                    (ev as any).originalEvent.preventDefault();
+                    (ev as any).originalEvent.stopPropagation();
+                }
+            }
+
             if (ev.touches.length === 1) {
                 buttons = ButtonsType.Flag.Primary
                 onPointerMove(ev.touches[0])

+ 1 - 1
src/servers/model/preprocess/master.ts

@@ -51,7 +51,7 @@ if (Object.keys(cmdArgs).filter(k => (cmdArgs as any)[k] !== null).length === 0
 }
 
 let entries: PreprocessEntry[] = []
-let config: PreprocessConfig = { numProcesses: 1, customProperties: void 0 }
+let config: PreprocessConfig = { numProcesses: cmdArgs.folderIn ? +(cmdArgs.folderNumProcesses || 1) : 1, customProperties: void 0 }
 
 if (cmdArgs.input) entries.push({ source: cmdArgs.input, cif: cmdArgs.outCIF, bcif: cmdArgs.outBCIF });
 // else if (cmdArgs.bulk) runBulk(cmdArgs.bulk);