Browse Source

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

Alexander Rose 4 years ago
parent
commit
d653a96b25

+ 38 - 0
CHANGELOG.md

@@ -0,0 +1,38 @@
+# Change Log
+All notable changes to this project will be documented in this file, following the suggestions of [Keep a CHANGELOG](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/) for its most widely used - and defacto - public interfaces.
+
+Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
+
+## [Unreleased]
+
+- [WIP] Mesh export extension
+- ``Structure.eachAtomicHierarchyElement``
+- Fixed reading multi-line values in SDF format
+
+## [v2.0.3] - 2021-04-09
+### Added
+- Support for ``ColorTheme.palette`` designed for providing gradient-like coloring.
+
+### Changed
+- [Breaking] The `zip` function is now asynchronous and expects a `RuntimeContext`. Also added `Zip()` returning a `Task`.
+- [Breaking] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension.
+
+## [v2.0.2] - 2021-03-29
+### Added
+- `Canvas3D.getRenderObjects`.
+- [WIP] Animate state interpolating, including model trajectories
+
+### Changed
+- Recognise MSE, SEP, TPO, PTR and PCA as non-standard amino-acids.
+
+### Fixed
+- VolumeFromDensityServerCif transform label
+
+
+## [v2.0.1] - 2021-03-23
+### Fixed
+- Exclude tsconfig.commonjs.tsbuildinfo from npm bundle
+
+
+## [v2.0.0] - 2021-03-23
+Too many changes to list as this is the start of the changelog... Notably, default exports are now forbidden.

+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
   "name": "molstar",
-  "version": "2.0.2",
+  "version": "2.0.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "version": "2.0.2",
+      "version": "2.0.3",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^1.0.38",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "2.0.2",
+  "version": "2.0.3",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {

+ 3 - 1
src/apps/viewer/index.ts

@@ -10,6 +10,7 @@ import { CellPack } from '../../extensions/cellpack';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
 import { Mp4Export } from '../../extensions/mp4-export';
+import { GeometryExport } from '../../extensions/geo-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
@@ -55,7 +56,8 @@ const Extensions = {
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
     'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
     'g3d': PluginSpec.Behavior(G3DFormat),
-    'mp4-export': PluginSpec.Behavior(Mp4Export)
+    'mp4-export': PluginSpec.Behavior(Mp4Export),
+    'geo-export': PluginSpec.Behavior(GeometryExport)
 };
 
 const DefaultViewerOptions = {

+ 51 - 0
src/examples/basic-wrapper/custom-theme.ts

@@ -0,0 +1,51 @@
+import { isPositionLocation } from '../../mol-geo/util/location-iterator';
+import { Vec3 } from '../../mol-math/linear-algebra';
+import { ColorTheme } from '../../mol-theme/color';
+import { ThemeDataContext } from '../../mol-theme/theme';
+import { Color } from '../../mol-util/color';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+
+export function CustomColorTheme(
+    ctx: ThemeDataContext,
+    props: PD.Values<{}>
+): ColorTheme<{}> {
+    const { radius, center } = ctx.structure?.boundary.sphere!;
+    const radiusSq = Math.max(radius * radius, 0.001);
+    const scale = ColorTheme.PaletteScale;
+
+    return {
+        factory: CustomColorTheme,
+        granularity: 'vertex',
+        color: location => {
+            if (!isPositionLocation(location)) return ColorNames.black;
+            const dist = Vec3.squaredDistance(location.position, center);
+            const t = Math.min(dist / radiusSq, 1);
+            return ((t * scale) | 0) as Color;
+        },
+        palette: {
+            filter: 'nearest',
+            colors: [
+                ColorNames.red,
+                ColorNames.pink,
+                ColorNames.violet,
+                ColorNames.orange,
+                ColorNames.yellow,
+                ColorNames.green,
+                ColorNames.blue
+            ]
+        },
+        props: props,
+        description: '',
+    };
+}
+
+export const CustomColorThemeProvider: ColorTheme.Provider<{}, 'basic-wrapper-custom-color-theme'> = {
+    name: 'basic-wrapper-custom-color-theme',
+    label: 'Custom Color Theme',
+    category: ColorTheme.Category.Misc,
+    factory: CustomColorTheme,
+    getParams: () => ({}),
+    defaultValues: { },
+    isApplicable: (ctx: ThemeDataContext) => true,
+};

+ 1 - 0
src/examples/basic-wrapper/index.html

@@ -97,6 +97,7 @@
             addHeader('Misc');
 
             addControl('Apply Stripes', () => BasicMolStarWrapper.coloring.applyStripes());
+            addControl('Apply Custom Theme', () => BasicMolStarWrapper.coloring.applyCustomTheme());
             addControl('Default Coloring', () => BasicMolStarWrapper.coloring.applyDefault());
 
             addHeader('Interactivity');

+ 9 - 0
src/examples/basic-wrapper/index.ts

@@ -18,6 +18,7 @@ import { Asset } from '../../mol-util/assets';
 import { Color } from '../../mol-util/color';
 import { StripedResidues } from './coloring';
 import { CustomToastMessage } from './controls';
+import { CustomColorThemeProvider } from './custom-theme';
 import './index.html';
 import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
 require('mol-plugin-ui/skin/light.scss');
@@ -42,6 +43,7 @@ class BasicWrapper {
         });
 
         this.plugin.representation.structure.themes.colorThemeRegistry.add(StripedResidues.colorThemeProvider!);
+        this.plugin.representation.structure.themes.colorThemeRegistry.add(CustomColorThemeProvider);
         this.plugin.managers.lociLabels.addProvider(StripedResidues.labelProvider!);
         this.plugin.customModelProperties.register(StripedResidues.propertyProvider, true);
     }
@@ -103,6 +105,13 @@ class BasicWrapper {
                 }
             });
         },
+        applyCustomTheme: async () => {
+            this.plugin.dataTransaction(async () => {
+                for (const s of this.plugin.managers.structure.hierarchy.current.structures) {
+                    await this.plugin.managers.structure.component.updateRepresentationsTheme(s.components, { color: CustomColorThemeProvider.name as any });
+                }
+            });
+        },
         applyDefault: async () => {
             this.plugin.dataTransaction(async () => {
                 for (const s of this.plugin.managers.structure.hierarchy.current.structures) {

+ 12 - 0
src/extensions/alpha-orbitals/data-model.ts

@@ -9,6 +9,7 @@ import { Grid } from '../../mol-model/volume';
 import { SphericalBasisOrder } from './spherical-functions';
 import { Box3D, RegularGrid3d } from '../../mol-math/geometry';
 import { arrayMin, arrayMax, arrayRms, arrayMean } from '../../mol-util/array';
+import { ModelFormat } from '../../mol-model-formats/format';
 
 // Note: generally contracted gaussians are currently not supported.
 export interface SphericalElectronShell {
@@ -59,6 +60,17 @@ export interface CubeGrid {
     isovalues?: { negative?: number; positive?: number };
 }
 
+export type CubeGridFormat = ModelFormat<CubeGrid>;
+
+// eslint-disable-next-line
+export function CubeGridFormat(grid: CubeGrid): CubeGridFormat {
+    return { name: 'custom grid', kind: 'cube-grid', data: grid };
+}
+
+export function isCubeGridData(f: ModelFormat): f is CubeGridFormat {
+    return f.kind === 'cube-grid';
+}
+
 export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
     const geometry = params.basis.atoms.map(a => a.center);
     const { gridSpacing: spacing, boxExpand: expand } = params;

+ 5 - 5
src/extensions/alpha-orbitals/transforms.ts

@@ -17,7 +17,7 @@ import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers
 import { StateTransformer } from '../../mol-state';
 import { Theme } from '../../mol-theme/theme';
 import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
-import { AlphaOrbital, Basis, CubeGrid } from './data-model';
+import { AlphaOrbital, Basis, CubeGrid, CubeGridFormat, isCubeGridData } from './data-model';
 import { createSphericalCollocationDensityGrid } from './density';
 import { Tensor } from '../../mol-math/linear-algebra';
 
@@ -114,7 +114,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
             }, a.data.orbitals[params.index], plugin.canvas3d?.webgl).runInContext(ctx);
             const volume: Volume = {
                 grid: data.grid,
-                sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
+                sourceData: CubeGridFormat(data),
                 customProperties: new CustomProperties(),
                 _propertyData: Object.create(null),
             };
@@ -146,7 +146,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
             }, a.data.orbitals, plugin.canvas3d?.webgl).runInContext(ctx);
             const volume: Volume = {
                 grid: data.grid,
-                sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
+                sourceData: CubeGridFormat(data),
                 customProperties: new CustomProperties(),
                 _propertyData: Object.create(null),
             };
@@ -210,9 +210,9 @@ export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
 });
 
 function volumeParams(plugin: PluginContext, volume: PluginStateObject.Volume.Data, params: StateTransformer.Params<typeof CreateOrbitalRepresentation3D>) {
-    if (volume.data.sourceData.kind !== 'alpha-orbitals') throw new Error('Invalid data source kind.');
+    if (!isCubeGridData(volume.data.sourceData)) throw new Error('Invalid data source kind.');
 
-    const { isovalues } = volume.data.sourceData.data as CubeGrid;
+    const { isovalues } = volume.data.sourceData.data;
     if (!isovalues) throw new Error('Isovalues are not computed.');
 
     const value = isovalues[params.kind];

+ 69 - 0
src/extensions/geo-export/controls.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { PluginComponent } from '../../mol-plugin-state/component';
+import { PluginContext } from '../../mol-plugin/context';
+import { Task } from '../../mol-task';
+import { ObjExporter } from './export';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateSelection } from '../../mol-state';
+import { SetUtils } from '../../mol-util/set';
+import { zip } from '../../mol-util/zip/zip';
+
+export class GeometryControls extends PluginComponent {
+    getFilename() {
+        const models = this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Model)).map(s => s.obj!.data);
+        const uniqueIds = new Set<string>();
+        models.forEach(m => uniqueIds.add(m.entryId.toUpperCase()));
+        const idString = SetUtils.toArray(uniqueIds).join('-');
+        return `${idString || 'molstar-model'}`;
+    }
+
+    exportObj() {
+        const task = Task.create('Export OBJ', async ctx => {
+            try {
+                const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
+
+                const filename = this.getFilename();
+                const objExporter = new ObjExporter(filename);
+                for (let i = 0, il = renderObjects.length; i < il; ++i) {
+                    await ctx.update({ message: `Exporting object ${i}/${il}` });
+                    await objExporter.add(renderObjects[i], ctx);
+                }
+                const { obj, mtl } = objExporter.getData();
+
+                const asciiWrite = (data: Uint8Array, str: string) => {
+                    for (let i = 0, il = str.length; i < il; ++i) {
+                        data[i] = str.charCodeAt(i);
+                    }
+                };
+                const objData = new Uint8Array(obj.length);
+                asciiWrite(objData, obj);
+                const mtlData = new Uint8Array(mtl.length);
+                asciiWrite(mtlData, mtl);
+
+                const zipDataObj = {
+                    [filename + '.obj']: objData,
+                    [filename + '.mtl']: mtlData
+                };
+                const zipData = await zip(ctx, zipDataObj);
+                return {
+                    zipData,
+                    filename: filename + '.zip'
+                };
+            } catch (e) {
+                this.plugin.log.error('' + e);
+                throw e;
+            }
+        });
+
+        return this.plugin.runTask(task, { useOverlay: true });
+    }
+
+    constructor(private plugin: PluginContext) {
+        super();
+    }
+}

+ 321 - 0
src/extensions/geo-export/export.ts

@@ -0,0 +1,321 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { GraphicsRenderObject } from '../../mol-gl/render-object';
+import { MeshValues } from '../../mol-gl/renderable/mesh';
+import { LinesValues } from '../../mol-gl/renderable/lines';
+import { PointsValues } from '../../mol-gl/renderable/points';
+import { SpheresValues } from '../../mol-gl/renderable/spheres';
+import { CylindersValues } from '../../mol-gl/renderable/cylinders';
+import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema';
+import { TextureImage } from '../../mol-gl/renderable/util';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
+import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
+import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
+import { RuntimeContext } from '../../mol-task';
+import { StringBuilder } from '../../mol-util';
+import { Color } from '../../mol-util/color/color';
+import { decodeFloatRGB } from '../../mol-util/float-packing';
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3transformMat4 = Vec3.transformMat4;
+const v3transformMat3 = Vec3.transformMat3;
+const mat3directionTransform = Mat3.directionTransform;
+
+type RenderObjectExportData = {
+    [k: string]: string | Uint8Array | undefined
+}
+
+interface RenderObjectExporter<D extends RenderObjectExportData> {
+    add(renderObject: GraphicsRenderObject, ctx: RuntimeContext): Promise<void> | undefined
+    getData(): D
+}
+
+// http://paulbourke.net/dataformats/obj/
+// http://paulbourke.net/dataformats/mtl/
+
+export type ObjData = {
+    obj: string
+    mtl: string
+}
+
+export class ObjExporter implements RenderObjectExporter<ObjData> {
+    private obj = StringBuilder.create();
+    private mtl = StringBuilder.create();
+    private vertexOffset = 0;
+    private currentColor: Color | undefined;
+    private currentAlpha: number | undefined;
+    private materialSet = new Set<string>();
+
+    private static getSizeFromTexture(tSize: TextureImage<Uint8Array>, i: number): number {
+        const r = tSize.array[i * 3];
+        const g = tSize.array[i * 3 + 1];
+        const b = tSize.array[i * 3 + 2];
+        return decodeFloatRGB(r, g, b);
+    }
+
+    private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
+        const tSize = values.tSize.ref.value;
+        let size = 0;
+        switch (values.dSizeType.ref.value) {
+            case 'uniform':
+                size = values.uSize.ref.value;
+                break;
+            case 'instance':
+                size = ObjExporter.getSizeFromTexture(tSize, instanceIndex) / 100;
+                break;
+            case 'group':
+                size = ObjExporter.getSizeFromTexture(tSize, group) / 100;
+                break;
+            case 'groupInstance':
+                const groupCount = values.uGroupCount.ref.value;
+                size = ObjExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group) / 100;
+                break;
+        }
+        return size * values.uSizeFactor.ref.value;
+    }
+
+    private updateMaterial(color: Color, alpha: number) {
+        if (this.currentColor === color && this.currentAlpha === alpha) return;
+
+        this.currentColor = color;
+        this.currentAlpha = alpha;
+        const material = Color.toHexString(color) + alpha;
+        StringBuilder.writeSafe(this.obj, `usemtl ${material}`);
+        StringBuilder.newline(this.obj);
+        if (!this.materialSet.has(material)) {
+            this.materialSet.add(material);
+            const [r, g, b] = Color.toRgbNormalized(color);
+            const mtl = this.mtl;
+            StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
+            StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
+            StringBuilder.writeSafe(mtl, 'Ns 163\n'); // specular exponent
+            StringBuilder.writeSafe(mtl, 'Ni 0.001\n'); // optical density a.k.a. index of refraction
+            StringBuilder.writeSafe(mtl, 'Ka 0 0 0\n'); // ambient reflectivity
+            StringBuilder.writeSafe(mtl, 'Kd '); // diffuse reflectivity
+            StringBuilder.writeFloat(mtl, r, 1000);
+            StringBuilder.whitespace1(mtl);
+            StringBuilder.writeFloat(mtl, g, 1000);
+            StringBuilder.whitespace1(mtl);
+            StringBuilder.writeFloat(mtl, b, 1000);
+            StringBuilder.newline(mtl);
+            StringBuilder.writeSafe(mtl, 'Ks 0.25 0.25 0.25\n'); // specular reflectivity
+            StringBuilder.writeSafe(mtl, 'd '); // dissolve
+            StringBuilder.writeFloat(mtl, alpha, 1000);
+            StringBuilder.newline(mtl);
+        }
+    }
+
+    private async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array, groups: Float32Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, ctx: RuntimeContext) {
+        const obj = this.obj;
+        const t = Mat4();
+        const n = Mat3();
+        const tmpV = Vec3();
+
+        const colorType = values.dColorType.ref.value;
+        const tColor = values.tColor.ref.value.array;
+        const uAlpha = values.uAlpha.ref.value;
+        const aTransform = values.aTransform.ref.value;
+
+        Mat4.fromArray(t, aTransform, instanceIndex * 16);
+        mat3directionTransform(n, t);
+
+        const currentProgress = (vertexCount * 2 + drawCount) * instanceIndex;
+        await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount * 2 + drawCount) * values.uInstanceCount.ref.value });
+
+        // position
+        for (let i = 0; i < vertexCount; ++i) {
+            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
+            v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * 3), t);
+            StringBuilder.writeSafe(obj, 'v ');
+            StringBuilder.writeFloat(obj, tmpV[0], 1000);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[1], 1000);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[2], 1000);
+            StringBuilder.newline(obj);
+        }
+
+        // normal
+        for (let i = 0; i < vertexCount; ++i) {
+            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
+            v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * 3), n);
+            StringBuilder.writeSafe(obj, 'vn ');
+            StringBuilder.writeFloat(obj, tmpV[0], 100);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[1], 100);
+            StringBuilder.whitespace1(obj);
+            StringBuilder.writeFloat(obj, tmpV[2], 100);
+            StringBuilder.newline(obj);
+        }
+
+        // face
+        for (let i = 0; i < drawCount; i += 3) {
+            if (i % 3000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount * 2 + i });
+            let color: Color;
+            switch (colorType) {
+                case 'uniform':
+                    color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
+                    break;
+                case 'instance':
+                    color = Color.fromArray(tColor, instanceIndex * 3);
+                    break;
+                case 'group':
+                    color = Color.fromArray(tColor, groups[indices[i]] * 3);
+                    break;
+                case 'groupInstance':
+                    const groupCount = values.uGroupCount.ref.value;
+                    const group = groups[indices[i]];
+                    color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                    break;
+                case 'vertex':
+                    color = Color.fromArray(tColor, i * 3);
+                    break;
+                case 'vertexInstance':
+                    color = Color.fromArray(tColor, (instanceIndex * drawCount + i) * 3);
+                    break;
+                default: throw new Error('Unsupported color type.');
+            }
+            this.updateMaterial(color, uAlpha);
+
+            const v1 = this.vertexOffset + indices[i] + 1;
+            const v2 = this.vertexOffset + indices[i + 1] + 1;
+            const v3 = this.vertexOffset + indices[i + 2] + 1;
+            StringBuilder.writeSafe(obj, 'f ');
+            StringBuilder.writeInteger(obj, v1);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeIntegerAndSpace(obj, v1);
+            StringBuilder.writeInteger(obj, v2);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeIntegerAndSpace(obj, v2);
+            StringBuilder.writeInteger(obj, v3);
+            StringBuilder.writeSafe(obj, '//');
+            StringBuilder.writeInteger(obj, v3);
+            StringBuilder.newline(obj);
+        }
+
+        this.vertexOffset += vertexCount;
+    }
+
+    private async addMesh(values: MeshValues, ctx: RuntimeContext) {
+        const aPosition = values.aPosition.ref.value;
+        const aNormal = values.aNormal.ref.value;
+        const elements = values.elements.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+        const drawCount = values.drawCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, ctx);
+        }
+    }
+
+    private async addLines(values: LinesValues, ctx: RuntimeContext) {
+        // TODO
+    }
+
+    private async addPoints(values: PointsValues, ctx: RuntimeContext) {
+        // TODO
+    }
+
+    private async addSpheres(values: SpheresValues, ctx: RuntimeContext) {
+        const center = Vec3();
+
+        const aPosition = values.aPosition.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            const state = MeshBuilder.createState(512, 256);
+
+            for (let i = 0; i < vertexCount; i += 4) {
+                v3fromArray(center, aPosition, i * 3);
+
+                const group = aGroup[i];
+                const radius = ObjExporter.getSize(values, instanceIndex, group);
+                state.currentGroup = group;
+                addSphere(state, center, radius, 2);
+            }
+
+            const mesh = MeshBuilder.getMesh(state);
+            const vertices = mesh.vertexBuffer.ref.value;
+            const normals = mesh.normalBuffer.ref.value;
+            const indices = mesh.indexBuffer.ref.value;
+            const groups = mesh.groupBuffer.ref.value;
+            await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
+        }
+    }
+
+    private async addCylinders(values: CylindersValues, ctx: RuntimeContext) {
+        const start = Vec3();
+        const end = Vec3();
+
+        const aStart = values.aStart.ref.value;
+        const aEnd = values.aEnd.ref.value;
+        const aScale = values.aScale.ref.value;
+        const aCap = values.aCap.ref.value;
+        const aGroup = values.aGroup.ref.value;
+        const instanceCount = values.instanceCount.ref.value;
+        const vertexCount = values.uVertexCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            const state = MeshBuilder.createState(512, 256);
+
+            for (let i = 0; i < vertexCount; i += 6) {
+                v3fromArray(start, aStart, i * 3);
+                v3fromArray(end, aEnd, i * 3);
+
+                const group = aGroup[i];
+                const radius = ObjExporter.getSize(values, instanceIndex, group) * aScale[i];
+                const cap = aCap[i];
+                const topCap = cap === 1 || cap === 3;
+                const bottomCap = cap >= 2;
+                const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments: 32 };
+                state.currentGroup = aGroup[i];
+                addCylinder(state, start, end, 1, cylinderProps);
+            }
+
+            const mesh = MeshBuilder.getMesh(state);
+            const vertices = mesh.vertexBuffer.ref.value;
+            const normals = mesh.normalBuffer.ref.value;
+            const indices = mesh.indexBuffer.ref.value;
+            const groups = mesh.groupBuffer.ref.value;
+            await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
+        }
+    }
+
+    add(renderObject: GraphicsRenderObject, ctx: RuntimeContext) {
+        if (!renderObject.state.visible) return;
+
+        switch (renderObject.type) {
+            case 'mesh':
+                return this.addMesh(renderObject.values as MeshValues, ctx);
+            case 'lines':
+                return this.addLines(renderObject.values as LinesValues, ctx);
+            case 'points':
+                return this.addPoints(renderObject.values as PointsValues, ctx);
+            case 'spheres':
+                return this.addSpheres(renderObject.values as SpheresValues, ctx);
+            case 'cylinders':
+                return this.addCylinders(renderObject.values as CylindersValues, ctx);
+        }
+    }
+
+    getData() {
+        return {
+            obj: StringBuilder.getString(this.obj),
+            mtl: StringBuilder.getString(this.mtl)
+        };
+    }
+
+    constructor(filename: string) {
+        StringBuilder.writeSafe(this.obj, `mtllib ${filename}.mtl\n`);
+    }
+}

+ 30 - 0
src/extensions/geo-export/index.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { GeometryExporterUI } from './ui';
+
+export const GeometryExport = PluginBehavior.create<{ }>({
+    name: 'extension-geo-export',
+    category: 'misc',
+    display: {
+        name: 'Geometry Export'
+    },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        register(): void {
+            this.ctx.customStructureControls.set('geo-export', GeometryExporterUI as any);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.customStructureControls.delete('geo-export');
+        }
+    },
+    params: () => ({ })
+});

+ 64 - 0
src/extensions/geo-export/ui.tsx

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
+import { Button } from '../../mol-plugin-ui/controls/common';
+import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
+import { download } from '../../mol-util/download';
+import { GeometryControls } from './controls';
+
+interface State {
+    busy?: boolean
+}
+
+export class GeometryExporterUI extends CollapsableControls<{}, State> {
+    private _controls: GeometryControls | undefined;
+
+    get controls() {
+        return this._controls || (this._controls = new GeometryControls(this.plugin));
+    }
+
+    protected defaultState(): State & CollapsableState {
+        return {
+            header: 'Export Geometries',
+            isCollapsed: true,
+            brand: { accent: 'cyan', svg: CubeSendSvg }
+        };
+    }
+
+    protected renderControls(): JSX.Element {
+        return <>
+            <Button icon={GetAppSvg}
+                onClick={this.saveObj} style={{ marginTop: 1 }}
+                disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
+                Save OBJ + MTL
+            </Button>
+        </>;
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.canvas3d!.reprCount, () => {
+            if (!this.state.isCollapsed) this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this._controls?.dispose();
+        this._controls = void 0;
+    }
+
+    saveObj = async () => {
+        try {
+            this.setState({ busy: true });
+            const data = await this.controls.exportObj();
+            this.setState({ busy: false });
+
+            download(new Blob([data.zipData]), data.filename);
+        } catch {
+            this.setState({ busy: false });
+        }
+    }
+}

+ 57 - 6
src/mol-geo/geometry/color-data.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2021 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 { ValueCell } from '../../mol-util';
@@ -18,13 +19,26 @@ export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 've
 export type ColorData = {
     uColor: ValueCell<Vec3>,
     tColor: ValueCell<TextureImage<Uint8Array>>,
+    tPalette: ValueCell<TextureImage<Uint8Array>>,
     uColorTexDim: ValueCell<Vec2>,
     uColorGridDim: ValueCell<Vec3>,
     uColorGridTransform: ValueCell<Vec4>,
     dColorType: ValueCell<string>,
+    dUsePalette: ValueCell<boolean>,
 }
 
 export function createColors(locationIt: LocationIterator, positionIt: LocationIterator, colorTheme: ColorTheme<any>, colorData?: ColorData): ColorData {
+    const data = _createColors(locationIt, positionIt, colorTheme, colorData);
+    if (colorTheme.palette) {
+        ValueCell.updateIfChanged(data.dUsePalette, true);
+        updatePaletteTexture(colorTheme.palette, data.tPalette);
+    } else {
+        ValueCell.updateIfChanged(data.dUsePalette, false);
+    }
+    return data;
+}
+
+function _createColors(locationIt: LocationIterator, positionIt: LocationIterator, colorTheme: ColorTheme<any>, colorData?: ColorData): ColorData {
     switch (Geometry.getGranularity(locationIt, colorTheme.granularity)) {
         case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData);
         case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData);
@@ -36,6 +50,37 @@ export function createColors(locationIt: LocationIterator, positionIt: LocationI
     }
 }
 
+function updatePaletteTexture(palette: ColorTheme.Palette, cell: ValueCell<TextureImage<Uint8Array>>) {
+    let isSynced = true;
+    const texture = cell.ref.value;
+    if (palette.colors.length !== texture.width || texture.filter !== palette.filter) {
+        isSynced = false;
+    } else {
+        const data = texture.array;
+        let o = 0;
+        for (const c of palette.colors) {
+            const [r, g, b] = Color.toRgb(c);
+            if (data[o++] !== r || data[o++] !== g || data[o++] !== b) {
+                isSynced = false;
+                break;
+            }
+        }
+    }
+
+    if (isSynced) return;
+
+    const array = new Uint8Array(palette.colors.length * 3);
+    let o = 0;
+    for (const c of palette.colors) {
+        const [r, g, b] = Color.toRgb(c);
+        array[o++] = r;
+        array[o++] = g;
+        array[o++] = b;
+    }
+
+    ValueCell.update(cell, { array, height: 1, width: palette.colors.length, filter: palette.filter });
+}
+
 //
 
 export function createValueColor(value: Color, colorData?: ColorData): ColorData {
@@ -47,16 +92,18 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
         return {
             uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
             tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
+            tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
             uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
             uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
             dColorType: ValueCell.create('uniform'),
+            dUsePalette: ValueCell.create(false),
         };
     }
 }
 
 /** Creates color uniform */
-export function createUniformColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createUniformColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     return createValueColor(color(NullLocation, false), colorData);
 }
 
@@ -72,16 +119,18 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
         return {
             uColor: ValueCell.create(Vec3()),
             tColor: ValueCell.create(colors),
+            tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
             uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
             uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
             dColorType: ValueCell.create(type),
+            dUsePalette: ValueCell.create(false),
         };
     }
 }
 
 /** Creates color texture with color for each instance */
-export function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { instanceCount } = locationIt;
     const colors = createTextureImage(Math.max(1, instanceCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
     locationIt.reset();
@@ -94,7 +143,7 @@ export function createInstanceColor(locationIt: LocationIterator, color: Locatio
 }
 
 /** Creates color texture with color for each group (i.e. shared across instances) */
-export function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount } = locationIt;
     const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
     locationIt.reset();
@@ -106,7 +155,7 @@ export function createGroupColor(locationIt: LocationIterator, color: LocationCo
 }
 
 /** Creates color texture with color for each group in each instance */
-export function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount, instanceCount } = locationIt;
     const count = instanceCount * groupCount;
     const colors = createTextureImage(Math.max(1, count), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
@@ -119,7 +168,7 @@ export function createGroupInstanceColor(locationIt: LocationIterator, color: Lo
 }
 
 /** Creates color texture with color for each vertex (i.e. shared across instances) */
-export function createVertexColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createVertexColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount, stride } = locationIt;
     const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
     locationIt.reset();
@@ -135,7 +184,7 @@ export function createVertexColor(locationIt: LocationIterator, color: LocationC
 }
 
 /** Creates color texture with color for each vertex in each instance */
-export function createVertexInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
+function createVertexInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount, instanceCount, stride } = locationIt;
     const count = instanceCount * groupCount;
     const colors = createTextureImage(Math.max(1, count), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
@@ -175,10 +224,12 @@ export function createGridColor(grid: ColorVolume, type: ColorType, colorData?:
         return {
             uColor: ValueCell.create(Vec3()),
             tColor: ValueCell.create(colors),
+            tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(width, height)),
             uColorGridDim: ValueCell.create(Vec3.clone(dimension)),
             uColorGridTransform: ValueCell.create(Vec4.clone(transform)),
             dColorType: ValueCell.create(type),
+            dUsePalette: ValueCell.create(false),
         };
     }
 }

+ 1 - 1
src/mol-gl/_spec/renderer.spec.ts

@@ -133,7 +133,7 @@ describe('renderer', () => {
         scene.add(points);
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
-        expect(ctx.stats.resourceCounts.texture).toBe(6);
+        expect(ctx.stats.resourceCounts.texture).toBe(7);
         expect(ctx.stats.resourceCounts.vertexArray).toBe(6);
         expect(ctx.stats.resourceCounts.program).toBe(6);
         expect(ctx.stats.resourceCounts.shader).toBe(12);

+ 2 - 0
src/mol-gl/renderable/schema.ts

@@ -187,7 +187,9 @@ export const ColorSchema = {
     uColorGridDim: UniformSpec('v3'),
     uColorGridTransform: UniformSpec('v4'),
     tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
+    tPalette: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
     dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance', 'vertex', 'vertexInstance']),
+    dUsePalette: DefineSpec('boolean'),
 } as const;
 export type ColorSchema = typeof ColorSchema
 export type ColorValues = Values<ColorSchema>

+ 2 - 0
src/mol-gl/renderable/util.ts

@@ -7,6 +7,7 @@
 import { Sphere3D } from '../../mol-math/geometry';
 import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
 import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
+import { TextureFilter } from '../webgl/texture';
 
 export function calculateTextureInfo (n: number, itemSize: number) {
     n = Math.max(n, 2); // observed issues with 1 pixel textures
@@ -22,6 +23,7 @@ export interface TextureImage<T extends Uint8Array | Float32Array | Int32Array>
     readonly width: number
     readonly height: number
     readonly flipY?: boolean
+    readonly filter?: TextureFilter
 }
 
 export interface TextureVolume<T extends Uint8Array | Float32Array> {

+ 4 - 0
src/mol-gl/shader/chunks/assign-color-varying.glsl.ts

@@ -17,6 +17,10 @@ export const assign_color_varying = `
         vColor.rgb = texture3dFrom2dLinear(tColor, gridPos, uColorGridDim, uColorTexDim).rgb;
     #endif
 
+    #ifdef dUsePalette
+        vPaletteV = ((vColor.r * 256.0 * 256.0 * 255.0 + vColor.g * 256.0 * 255.0 + vColor.b * 255.0) - 1.0) / 16777215.0;
+    #endif
+
     #ifdef dOverpaint
         vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + group, uOverpaintTexDim);
     #endif

+ 3 - 1
src/mol-gl/shader/chunks/assign-material-color.glsl.ts

@@ -1,6 +1,8 @@
 export const assign_material_color = `
 #if defined(dRenderVariant_color)
-    #if defined(dColorType_uniform)
+    #if defined(dUsePalette)
+        vec4 material = vec4(texture2D(tPalette, vec2(vPaletteV, 0.5)).rgb, uAlpha);
+    #elif defined(dColorType_uniform)
         vec4 material = vec4(uColor, uAlpha);
     #elif defined(dColorType_varying)
         vec4 material = vec4(vColor.rgb, uAlpha);

+ 5 - 0
src/mol-gl/shader/chunks/color-frag-params.glsl.ts

@@ -21,4 +21,9 @@ export const color_frag_params = `
     varying float vGroup;
     varying float vTransparency;
 #endif
+
+#ifdef dUsePalette
+    uniform sampler2D tPalette;
+    varying float vPaletteV;
+#endif
 `;

+ 4 - 0
src/mol-gl/shader/chunks/color-vert-params.glsl.ts

@@ -36,4 +36,8 @@ export const color_vert_params = `
     uniform vec2 uTransparencyTexDim;
     uniform sampler2D tTransparency;
 #endif
+
+#ifdef dUsePalette
+    varying float vPaletteV;
+#endif
 `;

+ 3 - 0
src/mol-gl/webgl/texture.ts

@@ -283,6 +283,9 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             gl.bindTexture(gl.TEXTURE_2D, texture);
             gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, data);
         } else if (isTexture2d(data, target, gl)) {
+            const _filter = data.filter ? getFilter(gl, data.filter) : filter;
+            gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, _filter);
+            gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, _filter);
             gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY);
             if (sub) {
                 gl.texSubImage2D(target, 0, 0, 0, data.width, data.height, format, type, data.array);

+ 211 - 3
src/mol-io/reader/_spec/sdf.spec.ts

@@ -2,7 +2,7 @@
 import { parseSdf } from '../sdf/parser';
 
 const SdfString = `
-  Mrv1718007121815122D          
+  Mrv1718007121815122D
 
   5  4  0  0  0  0            999 V2000
     0.0000    0.8250    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
@@ -129,7 +129,208 @@ Comp 2
 M  CHG  3   1  -1   3  -1   5  -1
 M  END
 > <DATABASE_ID>
-1`;
+1
+
+$$$$
+
+2244
+  -OEChem-04122119123D
+
+ 21 21  0     0  0  0  0  0  0999 V2000
+    1.2333    0.5540    0.7792 O   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.6952   -2.7148   -0.7502 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.7958   -2.1843    0.8685 O   0  0  0  0  0  0  0  0  0  0  0  0
+    1.7813    0.8105   -1.4821 O   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.0857    0.6088    0.4403 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.7927   -0.5515    0.1244 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.7288    1.8464    0.4133 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.1426   -0.4741   -0.2184 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.0787    1.9238    0.0706 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.7855    0.7636   -0.2453 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.1409   -1.8536    0.1477 C   0  0  0  0  0  0  0  0  0  0  0  0
+    2.1094    0.6715   -0.3113 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.5305    0.5996    0.1635 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.1851    2.7545    0.6593 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.7247   -1.3605   -0.4564 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.5797    2.8872    0.0506 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -3.8374    0.8238   -0.5090 H   0  0  0  0  0  0  0  0  0  0  0  0
+    3.7290    1.4184    0.8593 H   0  0  0  0  0  0  0  0  0  0  0  0
+    4.2045    0.6969   -0.6924 H   0  0  0  0  0  0  0  0  0  0  0  0
+    3.7105   -0.3659    0.6426 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.2555   -3.5916   -0.7337 H   0  0  0  0  0  0  0  0  0  0  0  0
+  1  5  1  0  0  0  0
+  1 12  1  0  0  0  0
+  2 11  1  0  0  0  0
+  2 21  1  0  0  0  0
+  3 11  2  0  0  0  0
+  4 12  2  0  0  0  0
+  5  6  1  0  0  0  0
+  5  7  2  0  0  0  0
+  6  8  2  0  0  0  0
+  6 11  1  0  0  0  0
+  7  9  1  0  0  0  0
+  7 14  1  0  0  0  0
+  8 10  1  0  0  0  0
+  8 15  1  0  0  0  0
+  9 10  2  0  0  0  0
+  9 16  1  0  0  0  0
+ 10 17  1  0  0  0  0
+ 12 13  1  0  0  0  0
+ 13 18  1  0  0  0  0
+ 13 19  1  0  0  0  0
+ 13 20  1  0  0  0  0
+M  END
+> <PUBCHEM_COMPOUND_CID>
+2244
+
+> <PUBCHEM_CONFORMER_RMSD>
+0.6
+
+> <PUBCHEM_CONFORMER_DIVERSEORDER>
+1
+11
+10
+3
+15
+17
+13
+5
+16
+7
+14
+9
+8
+4
+18
+6
+12
+2
+
+> <PUBCHEM_MMFF94_PARTIAL_CHARGES>
+18
+1 -0.23
+10 -0.15
+11 0.63
+12 0.66
+13 0.06
+14 0.15
+15 0.15
+16 0.15
+17 0.15
+2 -0.65
+21 0.5
+3 -0.57
+4 -0.57
+5 0.08
+6 0.09
+7 -0.15
+8 -0.15
+9 -0.15
+
+> <PUBCHEM_EFFECTIVE_ROTOR_COUNT>
+3
+
+> <PUBCHEM_PHARMACOPHORE_FEATURES>
+5
+1 2 acceptor
+1 3 acceptor
+1 4 acceptor
+3 2 3 11 anion
+6 5 6 7 8 9 10 rings
+
+> <PUBCHEM_HEAVY_ATOM_COUNT>
+13
+
+> <PUBCHEM_ATOM_DEF_STEREO_COUNT>
+0
+
+> <PUBCHEM_ATOM_UDEF_STEREO_COUNT>
+0
+
+> <PUBCHEM_BOND_DEF_STEREO_COUNT>
+0
+
+> <PUBCHEM_BOND_UDEF_STEREO_COUNT>
+0
+
+> <PUBCHEM_ISOTOPIC_ATOM_COUNT>
+0
+
+> <PUBCHEM_COMPONENT_COUNT>
+1
+
+> <PUBCHEM_CACTVS_TAUTO_COUNT>
+1
+
+> <PUBCHEM_CONFORMER_ID>
+000008C400000001
+
+> <PUBCHEM_MMFF94_ENERGY>
+39.5952
+
+> <PUBCHEM_FEATURE_SELFOVERLAP>
+25.432
+
+> <PUBCHEM_SHAPE_FINGERPRINT>
+1 1 18265615372930943622
+100427 49 16967750034970055351
+12138202 97 18271247217817981012
+12423570 1 16692715976000295083
+12524768 44 16753525617747228747
+12716758 59 18341332292274886536
+13024252 1 17968377969333732145
+14181834 199 17830728755827362645
+14614273 12 18262232214645093005
+15207287 21 17703787037639964108
+15775835 57 18340488876329928641
+16945 1 18271533103414939405
+193761 8 17907860604865584321
+20645476 183 17677348215414174190
+20871998 184 18198632231250704846
+21040471 1 18411412921197846465
+21501502 16 18123463883164380929
+23402539 116 18271795865171824860
+23419403 2 13539898140662769886
+23552423 10 18048876295495619569
+23559900 14 18272369794190581304
+241688 4 16179044415907240795
+257057 1 17478316999871287486
+2748010 2 18339085878070479087
+305870 269 18263645056784260212
+528862 383 18117272558388284091
+53812653 8 18410289211719108569
+7364860 26 17910392788380644719
+81228 2 18050568744116491203
+
+> <PUBCHEM_SHAPE_MULTIPOLES>
+244.06
+3.86
+2.45
+0.89
+1.95
+1.58
+0.15
+-1.85
+0.38
+-0.61
+-0.02
+0.29
+0.01
+-0.33
+
+> <PUBCHEM_SHAPE_SELFOVERLAP>
+513.037
+
+> <PUBCHEM_SHAPE_VOLUME>
+136
+
+> <PUBCHEM_COORDINATE_TYPE>
+2
+5
+10
+
+$$$$
+`;
 
 describe('sdf reader', () => {
     it('basic', async () => {
@@ -139,10 +340,11 @@ describe('sdf reader', () => {
         }
         const compound1 = parsed.result.compounds[0];
         const compound2 = parsed.result.compounds[1];
+        const compound3 = parsed.result.compounds[2];
         const { molFile, dataItems } = compound1;
         const { atoms, bonds } = molFile;
 
-        expect(parsed.result.compounds.length).toBe(2);
+        expect(parsed.result.compounds.length).toBe(3);
 
         // number of structures
         expect(atoms.count).toBe(5);
@@ -171,5 +373,11 @@ describe('sdf reader', () => {
 
         expect(compound1.dataItems.data.value(0)).toBe('0');
         expect(compound2.dataItems.data.value(0)).toBe('1');
+
+        expect(compound3.dataItems.dataHeader.value(2)).toBe('PUBCHEM_CONFORMER_DIVERSEORDER');
+        expect(compound3.dataItems.data.value(2)).toBe('1\n11\n10\n3\n15\n17\n13\n5\n16\n7\n14\n9\n8\n4\n18\n6\n12\n2');
+
+        expect(compound3.dataItems.dataHeader.value(21)).toBe('PUBCHEM_COORDINATE_TYPE');
+        expect(compound3.dataItems.data.value(21)).toBe('2\n5\n10');
     });
 });

+ 23 - 12
src/mol-io/reader/sdf/parser.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Column } from '../../../mol-data/db';
@@ -27,21 +28,31 @@ function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, da
     const dataHeader = TokenBuilder.create(tokenizer.data, 32);
     const data = TokenBuilder.create(tokenizer.data, 32);
 
-    let sawHeaderToken = false;
     while (tokenizer.position < tokenizer.length) {
         const line = Tokenizer.readLine(tokenizer);
         if (line.startsWith(delimiter)) break;
-        if (!!line) {
-            if (line.startsWith('> <')) {
-                TokenBuilder.add(dataHeader, tokenizer.tokenStart + 3, tokenizer.tokenEnd - 1);
-                sawHeaderToken = true;
-            } else if (sawHeaderToken) {
-                TokenBuilder.add(data, tokenizer.tokenStart, tokenizer.tokenEnd);
-                sawHeaderToken = false;
-                // TODO can there be multiline values?
+        if (!line) continue;
+
+        if (line.startsWith('> <')) {
+            TokenBuilder.add(dataHeader, tokenizer.tokenStart + 3, tokenizer.tokenEnd - 1);
+
+            Tokenizer.markLine(tokenizer);
+            const start = tokenizer.tokenStart;
+            let end = tokenizer.tokenEnd;
+            let added = false;
+            while (tokenizer.position < tokenizer.length) {
+                const line2 = Tokenizer.readLine(tokenizer);
+                if (!line2 || line2.startsWith(delimiter) || line2.startsWith('> <')) {
+                    TokenBuilder.add(data, start, end);
+                    added = true;
+                    break;
+                }
+                end = tokenizer.tokenEnd;
+            }
+
+            if (!added) {
+                TokenBuilder.add(data, start, end);
             }
-        } else {
-            sawHeaderToken = false;
         }
     }
 

+ 51 - 0
src/mol-model/structure/structure/structure.ts

@@ -1167,6 +1167,57 @@ namespace Structure {
         }
     }
 
+    export interface ForEachAtomicHierarchyElementParams {
+        // Called for 1st element of each chain
+        // Note that chains can be split, meaning each chain would be called multiple times.
+        chain?: (e: StructureElement.Location<Unit.Atomic>) => void,
+        // Called for 1st element of each residue
+        residue?: (e: StructureElement.Location<Unit.Atomic>) => void,
+        // Called for each element
+        atom?: (e: StructureElement.Location<Unit.Atomic>) => void,
+    };
+
+    export function eachAtomicHierarchyElement(structure: Structure, { chain, residue, atom }: ForEachAtomicHierarchyElementParams) {
+        const l = StructureElement.Location.create<Unit.Atomic>(structure);
+        for (const unit of structure.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
+            l.unit = unit;
+
+            const { elements } = unit;
+            const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
+            const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
+
+            while (chainsIt.hasNext) {
+                const chainSegment = chainsIt.move();
+
+                if (chain) {
+                    l.element = elements[chainSegment.start];
+                    chain(l);
+                }
+
+                if (!residue && !atom) continue;
+
+                residuesIt.setSegment(chainSegment);
+                while (residuesIt.hasNext) {
+                    const residueSegment = residuesIt.move();
+
+                    if (residue) {
+                        l.element = elements[residueSegment.start];
+                        residue(l);
+                    }
+
+                    if (!atom) continue;
+
+                    for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
+                        l.element = elements[j];
+                        atom(l);
+                    }
+                }
+            }
+        }
+    }
+
     //
 
     export const DefaultSizeThresholds = {

+ 3 - 3
src/mol-plugin-state/manager/snapshots.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -12,7 +12,7 @@ import { StatefulPluginComponent } from '../component';
 import { PluginContext } from '../../mol-plugin/context';
 import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
 import { Asset } from '../../mol-util/assets';
-import { zip } from '../../mol-util/zip/zip';
+import { Zip } from '../../mol-util/zip/zip';
 import { readFromFile } from '../../mol-util/data-source';
 import { objectForEach } from '../../mol-util/object';
 import { PLUGIN_VERSION } from '../../mol-plugin/version';
@@ -217,7 +217,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
                 zipDataObj['assets.json'] = data;
             }
 
-            const zipFile = zip(zipDataObj);
+            const zipFile = await this.plugin.runTask(Zip(zipDataObj));
             return new Blob([zipFile], {type : 'application/zip'});
         }
     }

+ 3 - 0
src/mol-plugin-ui/controls/icons.tsx

@@ -41,6 +41,9 @@ export function MoleculeSvg() { return _Molecule; }
 const _CubeOutline = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></svg>;
 export function CubeOutlineSvg() { return _CubeOutline; }
 
+const _CubeSend = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></svg>;
+export function CubeSendSvg() { return _CubeSend; }
+
 const _CursorDefaultOutline = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z' /></svg>;
 export function CursorDefaultOutlineSvg() { return _CursorDefaultOutline; }
 

+ 11 - 0
src/mol-theme/color.ts

@@ -35,6 +35,7 @@ import { OperatorHklColorThemeProvider } from './color/operator-hkl';
 import { PartialChargeColorThemeProvider } from './color/partial-charge';
 import { AtomIdColorThemeProvider } from './color/atom-id';
 import { EntityIdColorThemeProvider } from './color/entity-id';
+import { TextureFilter } from '../mol-gl/webgl/texture';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
@@ -44,6 +45,9 @@ interface ColorTheme<P extends PD.Params> {
     readonly granularity: ColorType
     readonly color: LocationColor
     readonly props: Readonly<PD.Values<P>>
+    // if palette is defined, 24bit RGB color value normalized to interval [0, 1]
+    // is used as index to the colors
+    readonly palette?: Readonly<ColorTheme.Palette>
     readonly contextHash?: number
     readonly description?: string
     readonly legend?: Readonly<ScaleLegend | TableLegend>
@@ -58,6 +62,13 @@ namespace ColorTheme {
         Misc = 'Miscellaneous',
     }
 
+    export interface Palette {
+        filter?: TextureFilter,
+        colors: Color[]
+    }
+
+    export const PaletteScale = (1 << 24) - 1;
+
     export type Props = { [k: string]: any }
     export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
     export const EmptyFactory = () => Empty;

+ 4 - 4
src/mol-util/_spec/zip.spec.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,16 +10,16 @@ import { SyncRuntimeContext } from '../../mol-task/execution/synchronous';
 describe('zip', () => {
     it('roundtrip deflate/inflate', async () => {
         const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7]);
-        const deflated = deflate(data);
+        const deflated = await deflate(SyncRuntimeContext, data);
         const inflated = await inflate(SyncRuntimeContext, deflated);
         expect(inflated).toEqual(data);
     });
 
-    it('roundtrip zip', async () => {
+    it('roundtrip zip/unzip', async () => {
         const data = {
             'test.foo': new Uint8Array([1, 2, 3, 4, 5, 6, 7])
         };
-        const zipped = zip(data);
+        const zipped = await zip(SyncRuntimeContext, data);
         const unzipped = await unzip(SyncRuntimeContext, zipped);
         expect(unzipped).toEqual(data);
     });

+ 99 - 58
src/mol-util/zip/deflate.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  *
@@ -7,65 +7,54 @@
  * MIT License, Copyright (c) 2018 Photopea
  */
 
+import { RuntimeContext } from '../../mol-task';
 import { NumberArray } from '../type-helpers';
 import { _hufTree } from './huffman';
 import { U, revCodes, makeCodes } from './util';
 
-export function _deflateRaw(data: Uint8Array, out: Uint8Array, opos: number, lvl: number) {
-    const opts = [
-        /*
-            ush good_length; /* reduce lazy search above this match length
-            ush max_lazy;    /* do not perform lazy search above this match length
-            ush nice_length; /* quit search above this match length
-        */
-        /*      good lazy nice chain */
-        /* 0 */ [ 0,   0,   0,    0, 0], /* store only */
-        /* 1 */ [ 4,   4,   8,    4, 0], /* max speed, no lazy matches */
-        /* 2 */ [ 4,   5,  16,    8, 0],
-        /* 3 */ [ 4,   6,  16,   16, 0],
-
-        /* 4 */ [ 4,  10,  16,   32, 0], /* lazy matches */
-        /* 5 */ [ 8,  16,  32,   32, 0],
-        /* 6 */ [ 8,  16, 128,  128, 0],
-        /* 7 */ [ 8,  32, 128,  256, 0],
-        /* 8 */ [32, 128, 258, 1024, 1],
-        /* 9 */ [32, 258, 258, 4096, 1] /* max compression */
-    ];
-
-    const opt = opts[lvl];
-
-    let i = 0, pos = opos << 3, cvrd = 0;
-    const dlen = data.length;
+function DeflateContext(data: Uint8Array, out: Uint8Array, opos: number, lvl: number) {
+    const { lits, strt, prev } = U;
+    return {
+        data,
+        out,
+        opt: Opts[lvl],
+        i: 0,
+        pos: opos << 3,
+        cvrd: 0,
+        dlen: data.length,
+
+        li: 0,
+        lc: 0,
+        bs: 0,
+        ebits: 0,
+        c: 0,
+        nc: 0,
+
+        lits,
+        strt,
+        prev
+    };
+}
+type DeflateContext = ReturnType<typeof DeflateContext>
 
-    if(lvl === 0) {
-        while(i < dlen) {
-            const len = Math.min(0xffff, dlen - i);
-            _putsE(out, pos, (i + len === dlen ? 1 : 0));
-            pos = _copyExact(data, i, len, out, pos + 8);
-            i += len;
-        }
-        return pos >>> 3;
-    }
 
+function deflateChunk(ctx: DeflateContext, count: number) {
+    const { data, dlen, out, opt } = ctx;
+    let { i, pos, cvrd, li, lc, bs, ebits, c, nc } = ctx;
     const { lits, strt, prev } = U;
-    let li = 0, lc = 0, bs = 0, ebits = 0, c = 0, nc = 0;  // last_item, literal_count, block_start
-    if(dlen > 2) {
-        nc = _hash(data, 0);
-        strt[nc] = 0;
-    }
 
-    // let nmch = 0
-    // let nmci = 0
+    const end = Math.min(i + count, dlen);
 
-    for(i = 0; i < dlen; i++)  {
+    for(; i < end; i++)  {
         c = nc;
-        //*
+
         if(i + 1 < dlen - 2) {
             nc = _hash(data, i + 1);
             const ii = ((i + 1) & 0x7fff);
             prev[ii] = strt[nc];
             strt[nc] = ii;
-        } // */
+        }
+
         if(cvrd <= i) {
             if((li > 14000 || lc > 26697) && (dlen - i) > 100) {
                 if(cvrd < i) {
@@ -79,19 +68,12 @@ export function _deflateRaw(data: Uint8Array, out: Uint8Array, opos: number, lvl
             }
 
             let mch = 0;
-            // if(nmci==i) mch= nmch;  else
             if(i < dlen - 2) {
                 mch = _bestMatch(data, i, prev, c, Math.min(opt[2], dlen - i), opt[3]);
             }
-            /*
-            if(mch!=0 && opt[4]==1 && (mch>>>16)<opt[1] && i+1<dlen-2) {
-                nmch = UZIP.F._bestMatch(data, i+1, prev, nc, opt[2], opt[3]);  nmci=i+1;
-                //var mch2 = UZIP.F._bestMatch(data, i+2, prev, nnc);  //nmci=i+1;
-                if((nmch>>>16)>(mch>>>16)) mch=0;
-            }//*/
-            // const len = mch>>>16, dst = mch & 0xffff;  // if(i-dst<0) throw "e";
+
             if(mch !== 0) {
-                const len = mch >>> 16, dst = mch & 0xffff;  // if(i-dst<0) throw "e";
+                const len = mch >>> 16, dst = mch & 0xffff;
                 const lgi = _goodIndex(len, U.of0);  U.lhst[257 + lgi]++;
                 const dgi = _goodIndex(dst, U.df0);  U.dhst[    dgi]++;  ebits += U.exb[lgi] + U.dxb[dgi];
                 lits[li] = (len << 23) | (i - cvrd);  lits[li + 1] = (dst << 16) | (lgi << 8) | dgi;  li += 2;
@@ -102,6 +84,69 @@ export function _deflateRaw(data: Uint8Array, out: Uint8Array, opos: number, lvl
             lc++;
         }
     }
+
+    ctx.i = i;
+    ctx.pos = pos;
+    ctx.cvrd = cvrd;
+    ctx.li = li;
+    ctx.lc = lc;
+    ctx.bs = bs;
+    ctx.ebits = ebits;
+    ctx.c = c;
+    ctx.nc = nc;
+}
+
+/**
+ * - good_length: reduce lazy search above this match length;
+ * - max_lazy: do not perform lazy search above this match length;
+ * - nice_length: quit search above this match length;
+ */
+const Opts = [
+    /*      good lazy nice chain */
+    /* 0 */ [ 0,   0,   0,    0, 0], /* store only */
+    /* 1 */ [ 4,   4,   8,    4, 0], /* max speed, no lazy matches */
+    /* 2 */ [ 4,   5,  16,    8, 0],
+    /* 3 */ [ 4,   6,  16,   16, 0],
+
+    /* 4 */ [ 4,  10,  16,   32, 0], /* lazy matches */
+    /* 5 */ [ 8,  16,  32,   32, 0],
+    /* 6 */ [ 8,  16, 128,  128, 0],
+    /* 7 */ [ 8,  32, 128,  256, 0],
+    /* 8 */ [32, 128, 258, 1024, 1],
+    /* 9 */ [32, 258, 258, 4096, 1] /* max compression */
+] as const;
+
+export async function _deflateRaw(runtime: RuntimeContext, data: Uint8Array, out: Uint8Array, opos: number, lvl: number) {
+    const ctx = DeflateContext(data, out, opos, lvl);
+    const { dlen } = ctx;
+
+    if(lvl === 0) {
+        let { i, pos } = ctx;
+
+        while(i < dlen) {
+            const len = Math.min(0xffff, dlen - i);
+            _putsE(out, pos, (i + len === dlen ? 1 : 0));
+            pos = _copyExact(data, i, len, out, pos + 8);
+            i += len;
+        }
+        return pos >>> 3;
+    }
+
+    if(dlen > 2) {
+        ctx.nc = _hash(data, 0);
+        ctx.strt[ctx.nc] = 0;
+    }
+
+    while (ctx.i < dlen) {
+        if (runtime.shouldUpdate) {
+            await runtime.update({ message: 'Deflating...', current: ctx.i, max: dlen });
+        }
+        deflateChunk(ctx, 1024 * 1024);
+    }
+
+    let { li, cvrd, pos } = ctx;
+    const { i, lits, bs, ebits } = ctx;
+
     if(bs !== i || data.length === 0) {
         if(cvrd < i) {
             lits[li] = i - cvrd;
@@ -109,10 +154,6 @@ export function _deflateRaw(data: Uint8Array, out: Uint8Array, opos: number, lvl
             cvrd = i;
         }
         pos = _writeBlock(1, lits, li, ebits, data, bs, i - bs, out, pos);
-        li = 0;
-        lc = 0;
-        li = lc = ebits = 0;
-        bs = i;
     }
     while((pos & 7) !== 0) pos++;
     return pos >>> 3;

+ 11 - 7
src/mol-util/zip/zip.ts

@@ -13,7 +13,7 @@ import { writeUint, writeUshort, sizeUTF8, writeUTF8, readUshort, readUint, read
 import { crc, adler } from './checksum';
 import { _inflate } from './inflate';
 import { _deflateRaw } from './deflate';
-import { RuntimeContext } from '../../mol-task';
+import { RuntimeContext, Task } from '../../mol-task';
 
 export async function unzip(runtime: RuntimeContext, buf: ArrayBuffer, onlyNames = false) {
     const out: { [k: string]: Uint8Array | { size: number, csize: number } } = Object.create(null);
@@ -174,12 +174,12 @@ export async function ungzip(runtime: RuntimeContext, file: Uint8Array, buf?: Ui
     return inflated;
 }
 
-export function deflate(data: Uint8Array, opts?: { level: number }/* , buf, off*/) {
+export async function deflate(runtime: RuntimeContext, data: Uint8Array, opts?: { level: number }/* , buf, off*/) {
     if(opts === undefined) opts = { level: 6 };
     let off = 0;
     const buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
     buf[off] = 120;  buf[off + 1] = 156;  off += 2;
-    off = _deflateRaw(data, buf, off, opts.level);
+    off = await _deflateRaw(runtime, data, buf, off, opts.level);
     const crcValue = adler(data, 0, data.length);
     buf[off + 0] = ((crcValue >>> 24) & 255);
     buf[off + 1] = ((crcValue >>> 16) & 255);
@@ -188,14 +188,18 @@ export function deflate(data: Uint8Array, opts?: { level: number }/* , buf, off*
     return new Uint8Array(buf.buffer, 0, off + 4);
 }
 
-function deflateRaw(data: Uint8Array, opts?: { level: number }) {
+async function deflateRaw(runtime: RuntimeContext, data: Uint8Array, opts?: { level: number }) {
     if(opts === undefined) opts = { level: 6 };
     const buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
-    const off = _deflateRaw(data, buf, 0, opts.level);
+    const off = await _deflateRaw(runtime, data, buf, 0, opts.level);
     return new Uint8Array(buf.buffer, 0, off);
 }
 
-export function zip(obj: { [k: string]: Uint8Array }, noCmpr = false) {
+export function Zip(obj: { [k: string]: Uint8Array }, noCmpr = false) {
+    return Task.create('Zip', ctx => zip(ctx, obj, noCmpr));
+}
+
+export async function zip(runtime: RuntimeContext, obj: { [k: string]: Uint8Array }, noCmpr = false) {
     let tot = 0;
     const zpd: { [k: string]: { cpr: boolean, usize: number, crc: number, file: Uint8Array } } = {};
     for(const p in obj) {
@@ -205,7 +209,7 @@ export function zip(obj: { [k: string]: Uint8Array }, noCmpr = false) {
             cpr,
             usize: buf.length,
             crc: crcValue,
-            file: (cpr ? deflateRaw(buf) : buf)
+            file: (cpr ? await deflateRaw(runtime, buf) : buf)
         };
     }