Browse Source

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

Alexander Rose 5 years ago
parent
commit
598441a727

+ 1 - 2
src/apps/structure-info/volume.ts

@@ -34,9 +34,8 @@ function print(data: Volume) {
     const { volume_data_3d_info } = data.source;
     const row = Table.getRow(volume_data_3d_info, 0);
     console.log(row);
-    console.log(data.volume.cell);
+    if (data.volume.transform) console.log(data.volume.transform);
     console.log(data.volume.dataStats);
-    console.log(data.volume.fractionalBox);
 }
 
 async function doMesh(data: Volume, filename: string) {

+ 128 - 0
src/mol-io/reader/dx/parser.ts

@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from NGL.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { chunkedSubtask, RuntimeContext, Task } from '../../../mol-task';
+import { parseFloat as fastParseFloat } from '../common/text/number-parser';
+import { Tokenizer } from '../common/text/tokenizer';
+import { ReaderResult as Result } from '../result';
+import { utf8Read } from '../../common/utf8';
+
+// http://apbs-pdb2pqr.readthedocs.io/en/latest/formats/opendx.html
+
+export interface DxFile {
+    header: DxFile.Header,
+    values: Float64Array
+}
+
+export namespace DxFile {
+    export interface Header {
+        dim: Vec3,
+        min: Vec3,
+        h: Vec3
+    }
+}
+
+function readHeader(tokenizer: Tokenizer): { header: DxFile.Header, headerByteCount: number } {
+    const header: Partial<DxFile.Header> = { h: Vec3() };
+    let headerByteCount = 0;
+    let deltaLineCount = 0;
+
+    const reWhitespace = /\s+/g;
+
+    while (true) {
+        const line = Tokenizer.readLine(tokenizer);
+        let ls;
+
+        if (line.startsWith('object 1')) {
+            ls = line.split(reWhitespace);
+            header.dim = Vec3.create(parseInt(ls[5]), parseInt(ls[6]), parseInt(ls[7]));
+        } else if (line.startsWith('origin')) {
+            ls = line.split(reWhitespace);
+            header.min = Vec3.create(parseFloat(ls[1]), parseFloat(ls[2]), parseFloat(ls[3]));
+        } else if (line.startsWith('delta')) {
+            ls = line.split(reWhitespace);
+
+            if (deltaLineCount === 0) {
+                (header.h as any)[0] = parseFloat(ls[1]);
+            } else if (deltaLineCount === 1) {
+                (header.h as any)[1] = parseFloat(ls[2]);
+            } else if (deltaLineCount === 2) {
+                (header.h as any)[2] = parseFloat(ls[3]);
+            }
+
+            deltaLineCount += 1;
+        } else if (line.startsWith('object 3')) {
+            headerByteCount += line.length + 1;
+            break;
+        }
+
+        headerByteCount += line.length + 1;
+    }
+
+    return { header: header as DxFile.Header, headerByteCount };
+}
+
+function readValuesText(ctx: RuntimeContext, tokenizer: Tokenizer, header: DxFile.Header) {
+    const N = header.dim[0] * header.dim[1] * header.dim[2];
+    const chunkSize = 100 * 100 * 100;
+    const data = new Float64Array(N);
+    let offset = 0;
+
+    return chunkedSubtask(ctx, chunkSize, data, (count, data) => {
+        const max = Math.min(N, offset + count);
+        for (let i = offset; i < max; i++) {
+            Tokenizer.skipWhitespace(tokenizer);
+            tokenizer.tokenStart = tokenizer.position;
+            Tokenizer.eatValue(tokenizer);
+            data[i] = fastParseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
+        }
+        offset = max;
+        return max === N ? 0 : chunkSize;
+    }, (ctx, _, i) => ctx.update({ current: Math.min(i, N), max: N }));
+}
+
+async function parseText(taskCtx: RuntimeContext, data: string) {
+    await taskCtx.update('Reading header...');
+    const tokenizer = Tokenizer(data as string);
+    const { header } = readHeader(tokenizer);
+    await taskCtx.update('Reading values...');
+    const values = await readValuesText(taskCtx, tokenizer, header);
+    return Result.success({ header, values });
+}
+
+async function parseBinary(taskCtx: RuntimeContext, data: Uint8Array) {
+    await taskCtx.update('Reading header...');
+
+    const headerString = utf8Read(data, 0, 1000);
+
+    const tokenizer = Tokenizer(headerString);
+    const { header, headerByteCount } = readHeader(tokenizer);
+
+    await taskCtx.update('Reading values...');
+
+    const size = header.dim[0] * header.dim[1] * header.dim[2];
+    const dv = new DataView(data.buffer, data.byteOffset + headerByteCount);
+    const values = new Float64Array(size);
+
+    for (let i = 0; i < size; i++) {
+        values[i] = dv.getFloat64(i * 8, true);
+    }
+
+    // TODO: why doesnt this work? throw "attempting to construct out-of-bounds TypedArray"
+    // const values = new Float64Array(data.buffer, data.byteOffset + headerByteCount, header.dim[0] * header.dim[1] * header.dim[2]);
+    return Result.success({ header, values });
+}
+
+export function parseDx(data: string | Uint8Array) {
+    return Task.create<Result<DxFile>>('Parse Cube', taskCtx => {
+        if (typeof data === 'string') return parseText(taskCtx, data);
+        return parseBinary(taskCtx, data);
+    });
+}

+ 15 - 0
src/mol-math/linear-algebra/3d/mat4.ts

@@ -164,6 +164,21 @@ namespace Mat4 {
         return a;
     }
 
+    export function fromBasis(a: Mat4, x: Vec3, y: Vec3, z: Vec3) {
+        Mat4.setZero(a);
+        Mat4.setValue(a, 0, 0, x[0]);
+        Mat4.setValue(a, 1, 0, x[1]);
+        Mat4.setValue(a, 2, 0, x[2]);
+        Mat4.setValue(a, 0, 1, y[0]);
+        Mat4.setValue(a, 1, 1, y[1]);
+        Mat4.setValue(a, 2, 1, y[2]);
+        Mat4.setValue(a, 0, 2, z[0]);
+        Mat4.setValue(a, 1, 2, z[1]);
+        Mat4.setValue(a, 2, 2, z[2]);
+        Mat4.setValue(a, 3, 3, 1);
+        return a;
+    }
+
     export function copy(out: Mat4, a: Mat4) {
         out[0] = a[0];
         out[1] = a[1];

+ 3 - 3
src/mol-model-formats/volume/ccp4.ts

@@ -38,7 +38,7 @@ function getTypedArrayCtor(header: Ccp4Header) {
     throw Error(`${valueType} is not a supported value format.`);
 }
 
-export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, offset?: Vec3 }): Task<VolumeData> {
+export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, offset?: Vec3, label?: string }): Task<VolumeData> {
     return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { header, values } = source;
         const size = Vec3.create(header.xLength, header.yLength, header.zLength);
@@ -67,8 +67,8 @@ export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, of
         // These, however, calculate sigma, so no data on that.
 
         return {
-            cell,
-            fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)),
+            label: params?.label,
+            transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
             data,
             dataStats: {
                 min: isNaN(header.AMIN) ? arrayMin(values) : header.AMIN,

+ 10 - 22
src/mol-model-formats/volume/cube.ts

@@ -4,31 +4,15 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { CubeFile } from '../../mol-io/reader/cube/parser';
+import { Mat4, Tensor } from '../../mol-math/linear-algebra';
 import { VolumeData } from '../../mol-model/volume/data';
 import { Task } from '../../mol-task';
-import { SpacegroupCell, Box3D } from '../../mol-math/geometry';
-import { Tensor, Vec3 } from '../../mol-math/linear-algebra';
-import { arrayMin, arrayMax, arrayMean, arrayRms } from '../../mol-util/array';
-import { CubeFile } from '../../mol-io/reader/cube/parser';
+import { arrayMax, arrayMean, arrayMin, arrayRms } from '../../mol-util/array';
 
-export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number }): Task<VolumeData> {
+export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number, label?: string }): Task<VolumeData> {
     return Task.create<VolumeData>('Create Volume Data', async () => {
-        // TODO: support non-orthogonal axes
-
         const { header, values: sourceValues } = source;
-        const angles = SpacegroupCell.Zero.anglesInRadians;
-        const size = Vec3.create(header.basisX[0] * header.dim[0], header.basisY[1] * header.dim[1], header.basisZ[2] * header.dim[2]);
-        const cell = SpacegroupCell.create('P 1', size, angles);
-
-        if (header.basisX[1] !== 0 || header.basisX[2] !== 0
-            || header.basisY[0] !== 0 || header.basisY[2] !== 0
-            || header.basisZ[0] !== 0 || header.basisZ[1] !== 0) {
-            throw new Error('Non-orthogonal bases not supported. (TODO)');
-        }
-
-        const origin_frac = Vec3.div(Vec3(), header.origin, size);
-        const dimensions_frac = Vec3.add(Vec3(), origin_frac, Vec3.create(1, 1, 1));
-
         const space = Tensor.Space(header.dim, [0, 1, 2], Float64Array);
 
         let values: Float64Array;
@@ -54,9 +38,13 @@ export function volumeFromCube(source: CubeFile, params?: { dataIndex?: number }
 
         const data = Tensor.create(space, Tensor.Data1(values));
 
+        const matrix = Mat4.fromTranslation(Mat4(), header.origin);
+        const basis = Mat4.fromBasis(Mat4(), header.basisX, header.basisY, header.basisZ);
+        Mat4.mul(matrix, matrix, basis);
+
         return {
-            cell,
-            fractionalBox: Box3D.create(origin_frac, dimensions_frac),
+            label: params?.label,
+            transform: { kind: 'matrix', matrix },
             data,
             dataStats: {
                 min: arrayMin(values),

+ 1 - 2
src/mol-model-formats/volume/density-server.ts

@@ -34,8 +34,7 @@ function volumeFromDensityServerData(source: DensityServer_Data_Database): Task<
         const dimensions = Vec3.ofArray(normalizeOrder(info.dimensions.value(0)));
 
         return {
-            cell,
-            fractionalBox: Box3D.create(origin, Vec3.add(Vec3.zero(), origin, dimensions)),
+            transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin, Vec3.add(Vec3.zero(), origin, dimensions)) },
             data,
             dataStats: {
                 min: info.min_sampled.value(0),

+ 3 - 3
src/mol-model-formats/volume/dsn6.ts

@@ -12,7 +12,7 @@ import { degToRad } from '../../mol-math/misc';
 import { Dsn6File } from '../../mol-io/reader/dsn6/schema';
 import { arrayMin, arrayMax, arrayMean, arrayRms } from '../../mol-util/array';
 
-function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
+function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3, label?: string }): Task<VolumeData> {
     return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { header, values } = source;
         const size = Vec3.create(header.xlen, header.ylen, header.zlen);
@@ -32,8 +32,8 @@ function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3 }): Task<V
         const data = Tensor.create(space, Tensor.Data1(values));
 
         return {
-            cell,
-            fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)),
+            label: params?.label,
+            transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
             data,
             dataStats: {
                 min: arrayMin(values),

+ 34 - 0
src/mol-model-formats/volume/dx.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { DxFile } from '../../mol-io/reader/dx/parser';
+import { Mat4, Tensor } from '../../mol-math/linear-algebra';
+import { VolumeData } from '../../mol-model/volume/data';
+import { Task } from '../../mol-task';
+import { arrayMax, arrayMean, arrayMin, arrayRms } from '../../mol-util/array';
+
+export function volumeFromDx(source: DxFile, params?: { label?: string }): Task<VolumeData> {
+    return Task.create<VolumeData>('Create Volume Data', async () => {
+        const { header, values } = source;
+        const space = Tensor.Space(header.dim, [0, 1, 2], Float64Array);
+        const data = Tensor.create(space, Tensor.Data1(values));
+        const matrix = Mat4.fromTranslation(Mat4(), header.min);
+        const basis = Mat4.fromScaling(Mat4(), header.h);
+        Mat4.mul(matrix, matrix, basis);
+
+        return {
+            label: params?.label,
+            transform: { kind: 'matrix', matrix },
+            data,
+            dataStats: {
+                min: arrayMin(values),
+                max: arrayMax(values),
+                mean: arrayMean(values),
+                sigma: arrayRms(values)
+            }
+        };
+    });
+}

+ 19 - 9
src/mol-model/volume/data.ts

@@ -10,10 +10,9 @@ import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 
 /** The basic unit cell that contains the data. */
-interface VolumeData {
+interface VolumeDataBase {
     readonly label?: string,
-    readonly cell: SpacegroupCell,
-    readonly fractionalBox: Box3D,
+    readonly transform: { kind: 'spacegroup', cell: SpacegroupCell, fractionalBox: Box3D } | { kind: 'matrix', matrix: Mat4 },
     readonly data: Tensor,
     readonly dataStats: Readonly<{
         min: number,
@@ -23,20 +22,31 @@ interface VolumeData {
     }>
 }
 
+interface VolumeData extends VolumeDataBase {
+    readonly colorVolume?: VolumeDataBase
+}
+
 namespace VolumeData {
     export const One: VolumeData = {
-        cell: SpacegroupCell.Zero,
-        fractionalBox: Box3D.empty(),
+        transform: { kind: 'matrix', matrix: Mat4() },
         data: Tensor.create(Tensor.Space([1, 1, 1], [0, 1, 2]), Tensor.Data1([0])),
         dataStats: { min: 0, max: 0, mean: 0, sigma: 0 }
     };
 
     const _scale = Mat4.zero(), _translate = Mat4.zero();
     export function getGridToCartesianTransform(volume: VolumeData) {
-        const { data: { space } } = volume;
-        const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(Vec3.zero(), volume.fractionalBox), Vec3.ofArray(space.dimensions)));
-        const translate = Mat4.fromTranslation(_translate, volume.fractionalBox.min);
-        return Mat4.mul3(Mat4.zero(), volume.cell.fromFractional, translate, scale);
+        if (volume.transform.kind === 'matrix') {
+            return Mat4.copy(Mat4(), volume.transform.matrix);
+        }
+
+        if (volume.transform.kind === 'spacegroup') {
+            const { data: { space } } = volume;
+            const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(Vec3.zero(), volume.transform.fractionalBox), Vec3.ofArray(space.dimensions)));
+            const translate = Mat4.fromTranslation(_translate, volume.transform.fractionalBox.min);
+            return Mat4.mul3(Mat4.zero(), volume.transform.cell.fromFractional, translate, scale);
+        }
+
+        return Mat4.identity();
     }
 
     export function areEquivalent(volA: VolumeData, volB: VolumeData) {

+ 16 - 2
src/mol-plugin-state/actions/volume.ts

@@ -6,7 +6,7 @@
  */
 
 import { PluginContext } from '../../mol-plugin/context';
-import { StateAction, StateTransformer } from '../../mol-state';
+import { StateAction, StateTransformer, StateSelection } from '../../mol-state';
 import { Task } from '../../mol-task';
 import { getFileInfo } from '../../mol-util/file-info';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -14,6 +14,7 @@ import { PluginStateObject } from '../objects';
 import { Download } from '../transforms/data';
 import { DataFormatProvider } from '../formats/provider';
 import { Asset } from '../../mol-util/assets';
+import { StateTransforms } from '../transforms';
 
 export { DownloadDensity };
 type DownloadDensity = typeof DownloadDensity
@@ -135,4 +136,17 @@ const DownloadDensity = StateAction.build({
 
     const volumes = await provider.parse(plugin, data);
     await provider.visuals?.(plugin, volumes);
-}));
+}));
+
+export const AssignColorVolume = StateAction.build({
+    display: { name: 'Assign Volume Colors', description: 'Assigns another volume to be available for coloring.' },
+    from: PluginStateObject.Volume.Data,
+    isApplicable(a) { return !a.data.colorVolume; },
+    params(a, plugin: PluginContext) {
+        const cells = plugin.state.data.select(StateSelection.Generators.root.subtree().ofType(PluginStateObject.Volume.Data).filter(cell => !!cell.obj && !cell.obj?.data.colorVolume && cell.obj !== a));
+        if (cells.length === 0) return { ref: PD.Text('', { isHidden: true, label: 'Volume' }) };
+        return { ref: PD.Select(cells[0].transform.ref, cells.map(c => [c.transform.ref, c.obj!.label]), { label: 'Volume' }) };
+    }
+})(({ ref, params, state }, plugin: PluginContext) => {
+    return plugin.build().to(ref).apply(StateTransforms.Volume.AssignColorVolume, { ref: params.ref }, { dependsOn: [ params.ref ] }).commit();
+});

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

@@ -61,6 +61,24 @@ export const Dsn6Provider = DataFormatProvider({
     visuals: defaultVisuals
 });
 
+export const DxProvider = DataFormatProvider({
+    label: 'DX',
+    description: 'DX',
+    category: Category,
+    stringExtensions: ['dx'],
+    binaryExtensions: ['dxbin'],
+    parse: async (plugin, data) => {
+        const volume = plugin.build()
+            .to(data)
+            .apply(StateTransforms.Volume.VolumeFromDx);
+
+        await volume.commit({ revertOnError: true });
+
+        return { volume: volume.selector };
+    },
+    visuals: defaultVisuals
+});
+
 export const CubeProvider = DataFormatProvider({
     label: 'Cube',
     description: 'Cube',
@@ -165,6 +183,7 @@ export const BuiltInVolumeFormats = [
     ['ccp4', Ccp4Provider] as const,
     ['dsn6', Dsn6Provider] as const,
     ['cube', CubeProvider] as const,
+    ['dx', DxProvider] as const,
     ['dscif', DscifProvider] as const,
 ] as const;
 

+ 3 - 3
src/mol-plugin-state/manager/volume/hierarchy-state.ts

@@ -84,14 +84,14 @@ type TestCell = (cell: StateObjectCell, state: BuildState) => boolean
 type ApplyRef = (state: BuildState, cell: StateObjectCell) => boolean | void
 type LeaveRef = (state: BuildState) => any
 
-function isType(t: StateObject.Ctor): TestCell {
-    return (cell) => t.is(cell.obj);
+function isTypeRoot(t: StateObject.Ctor, target: (state: BuildState) => any): TestCell {
+    return (cell, state) => !target(state) && t.is(cell.obj);
 }
 
 function noop() { }
 
 const Mapping: [TestCell, ApplyRef, LeaveRef][] = [
-    [isType(SO.Volume.Data), (state, cell) => {
+    [isTypeRoot(SO.Volume.Data, t => t.currentVolume), (state, cell) => {
         state.currentVolume = createOrUpdateRefList(state, cell, state.hierarchy.volumes, VolumeRef, cell);
     }, state => state.currentVolume = void 0],
 

+ 1 - 0
src/mol-plugin-state/manager/volume/hierarchy.ts

@@ -78,6 +78,7 @@ export class VolumeHierarchyManager extends PluginComponent {
     }
 
     setCurrent(volume?: VolumeRef) {
+        this.state.selection = volume || this.state.hierarchy.volumes[0];
         this.behaviors.selection.next({ hierarchy: this.state.hierarchy, volume: volume || this.state.hierarchy.volumes[0] });
     }
 

+ 62 - 7
src/mol-plugin-state/transforms/volume.ts

@@ -14,10 +14,17 @@ import { Task } from '../../mol-task';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { volumeFromCube } from '../../mol-model-formats/volume/cube';
+import { parseDx } from '../../mol-io/reader/dx/parser';
+import { volumeFromDx } from '../../mol-model-formats/volume/dx';
+import { VolumeData } from '../../mol-model/volume';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateSelection } from '../../mol-state';
 
 export { VolumeFromCcp4 };
 export { VolumeFromDsn6 };
 export { VolumeFromCube };
+export { VolumeFromDx };
+export { AssignColorVolume };
 export { VolumeFromDensityServerCif };
 
 type VolumeFromCcp4 = typeof VolumeFromCcp4
@@ -35,8 +42,8 @@ const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
 })({
     apply({ a, params }) {
         return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
-            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx);
-            const props = { label: 'Volume' };
+            const volume = await volumeFromCcp4(a.data, { ...params, label: a.label }).runInContext(ctx);
+            const props = { label: volume.label || 'Volume', description: 'Volume' };
             return new SO.Volume.Data(volume, props);
         });
     }
@@ -56,8 +63,8 @@ const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
 })({
     apply({ a, params }) {
         return Task.create('Create volume from DSN6/BRIX', async ctx => {
-            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx);
-            const props = { label: 'Volume' };
+            const volume = await volumeFromDsn6(a.data, { ...params, label: a.label }).runInContext(ctx);
+            const props = { label: volume.label || 'Volume', description: 'Volume' };
             return new SO.Volume.Data(volume, props);
         });
     }
@@ -78,8 +85,26 @@ const VolumeFromCube = PluginStateTransform.BuiltIn({
 })({
     apply({ a, params }) {
         return Task.create('Create volume from Cube', async ctx => {
-            const volume = await volumeFromCube(a.data, params).runInContext(ctx);
-            const props = { label: 'Volume' };
+            const volume = await volumeFromCube(a.data, { ...params, label: a.label }).runInContext(ctx);
+            const props = { label: volume.label || 'Volume', description: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDx = typeof VolumeFromDx
+const VolumeFromDx = PluginStateTransform.BuiltIn({
+    name: 'volume-from-dx',
+    display: { name: 'Parse PX', description: 'Parse DX string/binary and create volume.' },
+    from: [SO.Data.String, SO.Data.Binary],
+    to: SO.Volume.Data
+})({
+    apply({ a }) {
+        return Task.create('Parse DX', async ctx => {
+            const parsed = await parseDx(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            const volume = await volumeFromDx(parsed.result, { label: a.label }).runInContext(ctx);
+            const props = { label: volume.label || 'Volume', description: 'Volume' };
             return new SO.Volume.Data(volume, props);
         });
     }
@@ -115,4 +140,34 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
             return new SO.Volume.Data(volume, props);
         });
     }
-});
+});
+
+type AssignColorVolume = typeof AssignColorVolume
+const AssignColorVolume = PluginStateTransform.BuiltIn({
+    name: 'assign-color-volume',
+    display: { name: 'Assign Color Volume', description: 'Assigns another volume to be available for coloring.' },
+    from: SO.Volume.Data,
+    to: SO.Volume.Data,
+    isDecorator: true,
+    params(a, plugin: PluginContext) {
+        if (!a) return { ref: PD.Text() };
+        const cells = plugin.state.data.select(StateSelection.Generators.root.subtree().ofType(SO.Volume.Data).filter(cell => !!cell.obj && !cell.obj?.data.colorVolume && cell.obj !== a));
+        if (cells.length === 0) return { ref: PD.Text('', { isHidden: true }) };
+        return { ref: PD.Select(cells[0].transform.ref, cells.map(c => [c.transform.ref, c.obj!.label])) };
+    }
+})({
+    apply({ a, params, dependencies }) {
+        return Task.create('Assign Color Volume', async ctx => {
+            if (!dependencies || !dependencies[params.ref]) {
+                throw new Error('Dependency not available.');
+            }
+            const colorVolume = dependencies[params.ref].data as VolumeData;
+            const volume: VolumeData = {
+                ...a.data,
+                colorVolume
+            };
+            const props = { label: a.label, description: 'Volume + Colors' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});

+ 3 - 1
src/mol-plugin-ui/state/tree.tsx

@@ -245,7 +245,9 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         e.currentTarget.blur();
     }
 
-    hideApply = () => this.setState({ action: 'options', currentAction: void 0 });
+    hideApply = () => {
+        this.setCurrentRoot();
+    }
 
     get actions() {
         const cell = this.props.cell;

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

@@ -18,6 +18,7 @@ import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreami
 import { PluginConfig } from './config';
 import { PluginContext } from './context';
 import { PluginSpec } from './spec';
+import { AssignColorVolume } from '../mol-plugin-state/actions/volume';
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
@@ -59,9 +60,11 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
         PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
 
+        PluginSpec.Action(AssignColorVolume),
         PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),
         PluginSpec.Action(StateTransforms.Volume.VolumeFromDsn6),
         PluginSpec.Action(StateTransforms.Volume.VolumeFromCube),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromDx),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
     ],
     behaviors: [

+ 2 - 2
src/mol-util/assets.ts

@@ -16,10 +16,10 @@ type _File = File;
 type Asset = Asset.Url | Asset.File
 
 namespace Asset {
-    export type Url = { kind: 'url', id: UUID, url: string, title?: string, body?: string }
+    export type Url = { kind: 'url', id: UUID, url: string, title?: string, body?: string, headers?: [string, string][] }
     export type File = { kind: 'file', id: UUID, name: string, file?: _File }
 
-    export function Url(url: string, options?: { body?: string, title?: string }): Url {
+    export function Url(url: string, options?: { body?: string, title?: string, headers?: [string, string][] }): Url {
         return { kind: 'url', id: UUID.create22(), url, ...options };
     }
 

+ 8 - 2
src/mol-util/data-source.ts

@@ -42,6 +42,7 @@ export interface AjaxGetParams<T extends DataType = 'string'> {
     url: string,
     type?: T,
     title?: string,
+    headers?: [string, string][],
     body?: string
 }
 
@@ -61,7 +62,7 @@ export function ajaxGet(url: string): Task<DataValue>
 export function ajaxGet<T extends DataType>(params: AjaxGetParams<T>): Task<DataResponse<T>>
 export function ajaxGet<T extends DataType>(params: AjaxGetParams<T> | string) {
     if (typeof params === 'string') return ajaxGetInternal(params, params, 'string');
-    return ajaxGetInternal(params.title, params.url, params.type || 'string', params.body);
+    return ajaxGetInternal(params.title, params.url, params.type || 'string', params.body, params.headers);
 }
 
 export type AjaxTask = typeof ajaxGet
@@ -256,12 +257,17 @@ function getRequestResponseType(type: DataType): XMLHttpRequestResponseType {
     }
 }
 
-function ajaxGetInternal<T extends DataType>(title: string | undefined, url: string, type: T, body?: string): Task<DataResponse<T>> {
+function ajaxGetInternal<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: [string, string][]): Task<DataResponse<T>> {
     let xhttp: XMLHttpRequest | undefined = void 0;
     return Task.create(title ? title : 'Download', async ctx => {
         xhttp = RequestPool.get();
 
         xhttp.open(body ? 'post' : 'get', url, true);
+        if (headers) {
+            for (const [name, value] of headers) {
+                xhttp.setRequestHeader(name, value);
+            }
+        }
         xhttp.responseType = getRequestResponseType(type);
         xhttp.send(body);